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