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