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