Added export specifiers.
[shibboleth/cpp-sp.git] / shib-target / shib-ini.cpp
1 /*
2  * shib-ini.h -- INI file handling
3  *
4  * Created By:  Derek Atkins <derek@ihtfp.com>
5  *
6  * $Id$
7  */
8
9 // eventually we might be able to support autoconf via cygwin...
10 #if defined (_MSC_VER) || defined(__BORLANDC__)
11 # include "config_win32.h"
12 #else
13 # include "config.h"
14 #endif
15
16 #ifdef WIN32
17 # define SHIBTARGET_EXPORTS __declspec(dllexport)
18 #endif
19
20 #include "shib-target.h"
21 #include <shib/shib-threads.h>
22
23 #include <sstream>
24 #include <iostream>
25 #include <fstream>
26 #include <ctype.h>
27
28 #include <sys/types.h>
29 #include <sys/stat.h>
30
31 #include <log4cpp/Category.hh>
32
33 using namespace std;
34 using namespace shibboleth;
35 using namespace shibtarget;
36
37 class HeaderIterator : public shibtarget::ShibINI::Iterator {
38 public:
39   HeaderIterator (ShibINIPriv* ini);
40   ~HeaderIterator ();
41
42   const string* begin();
43   const string* next();
44 private:
45   ShibINIPriv* ini;
46   map<string, map<string,string> >::const_iterator iter;
47   bool valid;
48 };
49
50 class TagIterator : public ShibINI::Iterator {
51 public:
52   TagIterator (ShibINIPriv* ini, const string& header);
53   ~TagIterator ();
54
55   const string* begin();
56   const string* next();
57 private:
58   ShibINIPriv* ini;
59   string header;
60   map<string,string>::const_iterator iter;
61   bool valid;
62 };
63
64 class shibtarget::ShibINIPriv {
65 public:
66   ShibINIPriv();
67   ~ShibINIPriv() { delete rwlock; }
68   log4cpp::Category *log;
69
70   map<string, map<string, string> > table;
71   string file;
72   bool cs;
73
74   unsigned long modtime;
75
76   unsigned long iterators;
77   RWLock *rwlock;
78
79   bool exists(const std::string& header);
80   bool exists(const std::string& header, const std::string& tag);
81 };
82
83 ShibINIPriv::ShibINIPriv()
84 {
85   string ctx = "shibtarget.ShibINI";
86   log = &(log4cpp::Category::getInstance(ctx));
87   rwlock = RWLock::create();
88   modtime = 0;
89   iterators = 0;
90 }
91
92 static void trimline (string& s)
93 {
94   int end = s.size() - 1, start = 0;
95
96   // Trim stuff on right.
97   while (end > 0 && !isgraph(s[end])) end--;
98
99   // Trim stuff on left.
100   while (start < end && !isgraph(s[start])) start++;
101
102   // Modify the string.
103   s = s.substr(start, end - start + 1);
104 }
105
106 static void to_lowercase (string& s)
107 {
108   for (int i = 0, sz = s.size(); i < sz; i++)
109     s[i] = tolower(s[i]);
110 }
111
112 ShibINI::~ShibINI() {
113   delete m_priv;
114 }
115
116 void ShibINI::init (string& f, bool case_sensitive)
117 {
118   m_priv = new ShibINIPriv();
119   m_priv->file = f;
120   m_priv->cs = case_sensitive;
121   m_priv->log->info ("initializing INI file: %s (sensitive=%s)", f.c_str(),
122                      (case_sensitive ? "true" : "false"));
123
124   ReadLock lock(m_priv->rwlock);
125   refresh();
126 }
127
128 //
129 // Must be called holding the ReadLock.
130 //
131 void ShibINI::refresh(void)
132 {
133   saml::NDC ndc("refresh");
134
135   // check if we need to refresh
136 #ifdef _WIN32
137   struct _stat stat_buf;
138   if (_stat (m_priv->file.c_str(), &stat_buf) < 0)
139 #else
140   struct stat stat_buf;
141   if (stat (m_priv->file.c_str(), &stat_buf) < 0)
142 #endif
143     m_priv->log->error("stat failed: %s", m_priv->file.c_str());
144
145 #ifdef DEBUG
146   m_priv->log->info("refresh: last modtime at %d; file is %d; iters: %d",
147                     m_priv->modtime, stat_buf.st_mtime, m_priv->iterators);
148 #endif
149
150   if (m_priv->modtime >= stat_buf.st_mtime || m_priv->iterators > 0)
151     return;
152
153   // Release the read lock -- grab the write lock.  Don't worry if
154   // this is non-atomic -- we'll recheck the status.
155   m_priv->rwlock->unlock();
156   m_priv->rwlock->wrlock();
157
158   // Recheck the modtime
159   if (m_priv->modtime >= stat_buf.st_mtime) {
160     // Yep, another thread got to it.  We can exit now...  Release
161     // the write lock and reaquire the read-lock.
162
163     m_priv->rwlock->unlock();
164     m_priv->rwlock->rdlock();
165     return;
166   }
167
168   // Ok, we've got the write lock.  Let's update our state.
169
170   m_priv->modtime = stat_buf.st_mtime;
171
172   // clear the existing maps
173   m_priv->table.clear();
174
175   m_priv->log->info("reading %s", m_priv->file.c_str());
176   
177   // read the file
178   try
179   {
180     ifstream infile (m_priv->file.c_str());
181     if (!infile) {
182       m_priv->log->warn("cannot open file: %s", m_priv->file.c_str());
183       m_priv->rwlock->unlock();
184       m_priv->rwlock->rdlock();
185       return;
186     }
187
188     const int MAXLEN = 1024;
189     char linebuffer[MAXLEN];
190     string current_header;
191     bool have_header = false;
192
193     while (infile) {
194       infile.getline (linebuffer, MAXLEN);
195       string line (linebuffer);
196
197       if (line[0] == '#') continue;
198
199       trimline (line);
200       if (line.size() <= 1) continue;
201
202       if (line[0] == '[') {
203         // this is a header
204
205 #ifdef DEBUG
206         m_priv->log->info("Found what appears to be a header line");
207 #endif
208
209         have_header = false;
210
211         // find the end of the header
212         int endpos = line.find (']');
213         if (endpos == line.npos) {
214 #ifdef DEBUG
215           m_priv->log->info("Weird: no end found.. punting");
216 #endif
217           continue; // HUH?  No end?
218         }
219
220         // found it
221         current_header = line.substr (1, endpos-1);
222         trimline (current_header);
223
224         if (!m_priv->cs) to_lowercase (current_header);
225
226         m_priv->table[current_header] = map<string,string>();
227         have_header = true;
228 #ifdef DEBUG
229         m_priv->log->info("current header: \"%s\"", current_header.c_str());
230 #endif
231
232       } else if (have_header) {
233         // this is a tag
234
235 #ifdef DEBUG
236         m_priv->log->info("Found what appears to be a tag line");
237 #endif
238
239         string tag, setting;
240         int mid = line.find ('=');
241
242         if (mid == line.npos) {
243 #ifdef DEBUG
244           m_priv->log->info("Weird: no '=' found.. punting");
245 #endif
246           continue; // Can't find the value's setting
247         }
248
249         tag = line.substr (0,mid);
250         setting = line.substr (mid+1, line.size()-mid);
251
252         trimline (tag);
253         trimline (setting);
254
255         if (!m_priv->cs) to_lowercase (tag);
256
257         // If it already exists, log an error and do not save it
258         if (m_priv->exists (current_header, tag))
259           m_priv->log->error("Duplicate tag found in section %s: \"%s\"",
260                              current_header.c_str(), tag.c_str());
261         else
262           (m_priv->table[current_header])[tag] = setting;
263
264 #ifdef DEBUG
265         m_priv->log->info("new tag: \"%s\" = \"%s\"",
266                           tag.c_str(), setting.c_str());
267 #endif
268
269       }
270
271     } // until the file ends
272
273   } catch (...) {
274     // In case there are exceptions.
275   }
276
277   // Now release the write lock and reaquire the read lock
278   m_priv->rwlock->unlock();
279   m_priv->rwlock->rdlock();
280 }
281
282 const std::string ShibINI::get (const string& header, const string& tag)
283 {
284   ReadLock rwlock(m_priv->rwlock);
285   refresh();
286
287   static string empty = "";
288
289   string h = header;
290   string t = tag;
291
292   if (!m_priv->cs) {
293     to_lowercase (h);
294     to_lowercase (t);
295   }
296
297   if (!m_priv->exists(h)) return empty;
298
299   map<string,string>::const_iterator i = m_priv->table[h].find(t);
300   if (i == m_priv->table[h].end())
301     return empty;
302   return i->second;
303 }
304
305 bool ShibINIPriv::exists(const std::string& header)
306 {
307   string h = header;
308   if (!cs) to_lowercase (h);
309
310   return (table.find(h) != table.end());
311 }
312
313 bool ShibINI::exists(const std::string& header)
314 {
315   ReadLock rwlock(m_priv->rwlock);
316   refresh();
317
318   return m_priv->exists(header);
319 }
320
321 bool ShibINIPriv::exists(const std::string& header, const std::string& tag)
322 {
323   string h = header;
324   string t = tag;
325
326   if (!cs) {
327     to_lowercase (h);
328     to_lowercase (t);
329   }
330
331   if (!exists(h)) return false;
332   return (table[h].find(t) != table[h].end());
333 }
334
335 bool ShibINI::exists(const std::string& header, const std::string& tag)
336 {
337   ReadLock rwlock(m_priv->rwlock);
338   refresh();
339
340   return m_priv->exists(header, tag);
341 }
342
343 bool ShibINI::get_tag (string& header, string& tag, bool try_general, string* result)
344 {
345   if (!result) return false;
346
347   m_priv->rwlock->rdlock();
348   refresh();
349   m_priv->rwlock->unlock();
350
351   if (m_priv->exists (header, tag)) {
352     *result = get (header, tag);
353     return true;
354   }
355   if (try_general && exists (SHIBTARGET_GENERAL, tag)) {
356     *result = get (SHIBTARGET_GENERAL, tag);
357     return true;
358   }
359   return false;
360 }
361
362
363 void ShibINI::dump (ostream& os)
364 {
365   ReadLock rwlock(m_priv->rwlock);
366   refresh();
367
368   os << "File: " << m_priv->file << "\n";
369   os << "Case-Sensitive: " << ( m_priv->cs ? "Yes\n" : "No\n" );
370   os << "File Entries:\n";
371
372   for (map<string, map<string, string> >::const_iterator i = m_priv->table.begin();
373        i != m_priv->table.end(); i++) {
374
375     os << "[" << i->first << "]\n";
376
377     for (map<string,string>::const_iterator j=i->second.begin();
378          j != i->second.end(); j++) {
379
380       os << "  " << j->first << " = " << j->second << "\n";
381     }
382   }
383
384   os << "END\n";
385 }
386
387 ShibINI::Iterator* ShibINI::header_iterator()
388 {
389   ReadLock rwlock(m_priv->rwlock);
390   refresh();
391   HeaderIterator* iter = new HeaderIterator(m_priv);
392   return (ShibINI::Iterator*) iter;
393 }
394
395 ShibINI::Iterator* ShibINI::tag_iterator(const std::string& header)
396 {
397   ReadLock rwlock(m_priv->rwlock);
398   refresh();
399   TagIterator* iter = new TagIterator(m_priv, header);
400   return (ShibINI::Iterator*) iter;
401 }
402
403 //
404 // XXX: FIXME: there may be a race condition in the iterators if a
405 // caller holds an active Iterator, the underlying file changes, and
406 // then calls one of the get() routines.  It's possible the iterator
407 // may screw up -- I don't know whether the iterator actually depends
408 // on the underlying infrastructure or not.
409 //
410
411 HeaderIterator::HeaderIterator (ShibINIPriv* inip)
412 {
413   saml::NDC ndc("HeaderIterator");
414   ini = inip;
415   valid = false;
416   ini->rwlock->rdlock();
417   ini->iterators++;
418   ini->log->debug("iterators: %d", ini->iterators);
419 }
420
421 HeaderIterator::~HeaderIterator ()
422 {
423   saml::NDC ndc("~HeaderIterator");
424   ini->iterators--;
425   ini->rwlock->unlock();
426   ini->log->debug("iterators: %d", ini->iterators);
427 }
428
429 const string* HeaderIterator::begin ()
430 {
431   iter = ini->table.begin();
432   if (iter == ini->table.end()) {
433     valid = false;
434     return 0;
435   }
436   valid = true;
437   return &iter->first;
438 }
439
440 const string* HeaderIterator::next ()
441 {
442   if (!valid)
443     return 0;
444   iter++;
445   if (iter == ini->table.end()) {
446     valid = false;
447     return 0;
448   }
449   return &iter->first;
450 }
451
452 TagIterator::TagIterator (ShibINIPriv* inip, const string& headerp)
453   : header(headerp)
454 {
455   saml::NDC ndc("TagIterator");
456   ini = inip;
457   valid = false;
458   ini->rwlock->rdlock();
459   ini->iterators++;
460   ini->log->debug("iterators: %d", ini->iterators);
461 }
462
463 TagIterator::~TagIterator ()
464 {
465   saml::NDC ndc("~TagIterator");
466   ini->iterators--;
467   ini->rwlock->unlock();
468   ini->log->debug("iterators: %d", ini->iterators);
469 }
470
471 const string* TagIterator::begin ()
472 {
473   iter = ini->table[header].begin();
474   if (iter == ini->table[header].end()) {
475     valid = false;
476     return 0;
477   }
478   valid = true;
479   return &iter->first;
480 }
481
482 const string* TagIterator::next ()
483 {
484   if (!valid)
485     return 0;
486   iter++;
487   if (iter == ini->table[header].end()) {
488     valid = false;
489     return 0;
490   }
491   return &iter->first;
492 }
493
494 bool ShibINI::boolean(string& value)
495 {
496   const char* v = value.c_str();
497 #ifdef HAVE_STRCASECMP
498   if (!strncasecmp (v, "on", 2) || !strncasecmp (v, "true", 4) || !strncmp(v, "1", 1))
499     return true;
500 #else
501   if (!strnicmp (v, "on", 2) || !strnicmp (v, "true", 4) || !strncmp(v, "1", 1))
502     return true;
503 #endif
504   return false;
505 }
506
507 ShibINI::Iterator::~Iterator() {}