Added license.
[shibboleth/sp.git] / shib-target / shib-ini.cpp
1 /*
2  * The Shibboleth License, Version 1.
3  * Copyright (c) 2002
4  * University Corporation for Advanced Internet Development, Inc.
5  * All rights reserved
6  *
7  *
8  * Redistribution and use in source and binary forms, with or without
9  * modification, are permitted provided that the following conditions are met:
10  *
11  * Redistributions of source code must retain the above copyright notice, this
12  * list of conditions and the following disclaimer.
13  *
14  * Redistributions in binary form must reproduce the above copyright notice,
15  * this list of conditions and the following disclaimer in the documentation
16  * and/or other materials provided with the distribution, if any, must include
17  * the following acknowledgment: "This product includes software developed by
18  * the University Corporation for Advanced Internet Development
19  * <http://www.ucaid.edu>Internet2 Project. Alternately, this acknowledegement
20  * may appear in the software itself, if and wherever such third-party
21  * acknowledgments normally appear.
22  *
23  * Neither the name of Shibboleth nor the names of its contributors, nor
24  * Internet2, nor the University Corporation for Advanced Internet Development,
25  * Inc., nor UCAID may be used to endorse or promote products derived from this
26  * software without specific prior written permission. For written permission,
27  * please contact shibboleth@shibboleth.org
28  *
29  * Products derived from this software may not be called Shibboleth, Internet2,
30  * UCAID, or the University Corporation for Advanced Internet Development, nor
31  * may Shibboleth appear in their name, without prior written permission of the
32  * University Corporation for Advanced Internet Development.
33  *
34  *
35  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
36  * AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
37  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
38  * PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE DISCLAIMED AND THE ENTIRE RISK
39  * OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE.
40  * IN NO EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY
41  * CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC. BE LIABLE FOR ANY DIRECT,
42  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
43  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
44  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
45  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
46  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
47  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
48  */
49
50 /*
51  * shib-ini.h -- INI file handling
52  *
53  * Created By:  Derek Atkins <derek@ihtfp.com>
54  *
55  * $Id$
56  */
57
58 // eventually we might be able to support autoconf via cygwin...
59 #if defined (_MSC_VER) || defined(__BORLANDC__)
60 # include "config_win32.h"
61 #else
62 # include "config.h"
63 #endif
64
65 #ifdef WIN32
66 # define SHIBTARGET_EXPORTS __declspec(dllexport)
67 #endif
68
69 #include "shib-target.h"
70 #include <shib/shib-threads.h>
71
72 #include <sstream>
73 #include <iostream>
74 #include <fstream>
75 #include <ctype.h>
76
77 #include <sys/types.h>
78 #include <sys/stat.h>
79
80 #include <log4cpp/Category.hh>
81
82 using namespace std;
83 using namespace shibboleth;
84 using namespace shibtarget;
85
86 class HeaderIterator : public shibtarget::ShibINI::Iterator {
87 public:
88   HeaderIterator (ShibINIPriv* ini);
89   ~HeaderIterator ();
90
91   const string* begin();
92   const string* next();
93 private:
94   ShibINIPriv* ini;
95   map<string, map<string,string> >::const_iterator iter;
96   bool valid;
97 };
98
99 class TagIterator : public ShibINI::Iterator {
100 public:
101   TagIterator (ShibINIPriv* ini, const string& header);
102   ~TagIterator ();
103
104   const string* begin();
105   const string* next();
106 private:
107   ShibINIPriv* ini;
108   string header;
109   map<string,string>::const_iterator iter;
110   bool valid;
111 };
112
113 class shibtarget::ShibINIPriv {
114 public:
115   ShibINIPriv();
116   ~ShibINIPriv() { delete rwlock; }
117   log4cpp::Category *log;
118
119   map<string, map<string, string> > table;
120   string file;
121   bool cs;
122
123   unsigned long modtime;
124
125   unsigned long iterators;
126   RWLock *rwlock;
127
128   bool exists(const std::string& header);
129   bool exists(const std::string& header, const std::string& tag);
130 };
131
132 ShibINIPriv::ShibINIPriv()
133 {
134   string ctx = "shibtarget.ShibINI";
135   log = &(log4cpp::Category::getInstance(ctx));
136   rwlock = RWLock::create();
137   modtime = 0;
138   iterators = 0;
139 }
140
141 static void trimline (string& s)
142 {
143   int end = s.size() - 1, start = 0;
144
145   // Trim stuff on right.
146   while (end > 0 && !isgraph(s[end])) end--;
147
148   // Trim stuff on left.
149   while (start < end && !isgraph(s[start])) start++;
150
151   // Modify the string.
152   s = s.substr(start, end - start + 1);
153 }
154
155 static void to_lowercase (string& s)
156 {
157   for (int i = 0, sz = s.size(); i < sz; i++)
158     s[i] = tolower(s[i]);
159 }
160
161 ShibINI::~ShibINI() {
162   delete m_priv;
163 }
164
165 void ShibINI::init (string& f, bool case_sensitive)
166 {
167   m_priv = new ShibINIPriv();
168   m_priv->file = f;
169   m_priv->cs = case_sensitive;
170   m_priv->log->info ("initializing INI file: %s (sensitive=%s)", f.c_str(),
171                      (case_sensitive ? "true" : "false"));
172
173   ReadLock lock(m_priv->rwlock);
174   refresh();
175 }
176
177 //
178 // Must be called holding the ReadLock.
179 //
180 void ShibINI::refresh(void)
181 {
182   saml::NDC ndc("refresh");
183
184   // check if we need to refresh
185 #ifdef _WIN32
186   struct _stat stat_buf;
187   if (_stat (m_priv->file.c_str(), &stat_buf) < 0)
188 #else
189   struct stat stat_buf;
190   if (stat (m_priv->file.c_str(), &stat_buf) < 0)
191 #endif
192     m_priv->log->error("stat failed: %s", m_priv->file.c_str());
193
194 #ifdef DEBUG
195   m_priv->log->info("refresh: last modtime at %d; file is %d; iters: %d",
196                     m_priv->modtime, stat_buf.st_mtime, m_priv->iterators);
197 #endif
198
199   if (m_priv->modtime >= stat_buf.st_mtime || m_priv->iterators > 0)
200     return;
201
202   // Release the read lock -- grab the write lock.  Don't worry if
203   // this is non-atomic -- we'll recheck the status.
204   m_priv->rwlock->unlock();
205   m_priv->rwlock->wrlock();
206
207   // Recheck the modtime
208   if (m_priv->modtime >= stat_buf.st_mtime) {
209     // Yep, another thread got to it.  We can exit now...  Release
210     // the write lock and reaquire the read-lock.
211
212     m_priv->rwlock->unlock();
213     m_priv->rwlock->rdlock();
214     return;
215   }
216
217   // Ok, we've got the write lock.  Let's update our state.
218
219   m_priv->modtime = stat_buf.st_mtime;
220
221   // clear the existing maps
222   m_priv->table.clear();
223
224   m_priv->log->info("reading %s", m_priv->file.c_str());
225   
226   // read the file
227   try
228   {
229     ifstream infile (m_priv->file.c_str());
230     if (!infile) {
231       m_priv->log->warn("cannot open file: %s", m_priv->file.c_str());
232       m_priv->rwlock->unlock();
233       m_priv->rwlock->rdlock();
234       return;
235     }
236
237     const int MAXLEN = 1024;
238     char linebuffer[MAXLEN];
239     string current_header;
240     bool have_header = false;
241
242     while (infile) {
243       infile.getline (linebuffer, MAXLEN);
244       string line (linebuffer);
245
246       if (line[0] == '#') continue;
247
248       trimline (line);
249       if (line.size() <= 1) continue;
250
251       if (line[0] == '[') {
252         // this is a header
253
254 #ifdef DEBUG
255         m_priv->log->info("Found what appears to be a header line");
256 #endif
257
258         have_header = false;
259
260         // find the end of the header
261         int endpos = line.find (']');
262         if (endpos == line.npos) {
263 #ifdef DEBUG
264           m_priv->log->info("Weird: no end found.. punting");
265 #endif
266           continue; // HUH?  No end?
267         }
268
269         // found it
270         current_header = line.substr (1, endpos-1);
271         trimline (current_header);
272
273         if (!m_priv->cs) to_lowercase (current_header);
274
275         m_priv->table[current_header] = map<string,string>();
276         have_header = true;
277 #ifdef DEBUG
278         m_priv->log->info("current header: \"%s\"", current_header.c_str());
279 #endif
280
281       } else if (have_header) {
282         // this is a tag
283
284 #ifdef DEBUG
285         m_priv->log->info("Found what appears to be a tag line");
286 #endif
287
288         string tag, setting;
289         int mid = line.find ('=');
290
291         if (mid == line.npos) {
292 #ifdef DEBUG
293           m_priv->log->info("Weird: no '=' found.. punting");
294 #endif
295           continue; // Can't find the value's setting
296         }
297
298         tag = line.substr (0,mid);
299         setting = line.substr (mid+1, line.size()-mid);
300
301         trimline (tag);
302         trimline (setting);
303
304         if (!m_priv->cs) to_lowercase (tag);
305
306         // If it already exists, log an error and do not save it
307         if (m_priv->exists (current_header, tag))
308           m_priv->log->error("Duplicate tag found in section %s: \"%s\"",
309                              current_header.c_str(), tag.c_str());
310         else
311           (m_priv->table[current_header])[tag] = setting;
312
313 #ifdef DEBUG
314         m_priv->log->info("new tag: \"%s\" = \"%s\"",
315                           tag.c_str(), setting.c_str());
316 #endif
317
318       }
319
320     } // until the file ends
321
322   } catch (...) {
323     // In case there are exceptions.
324   }
325
326   // Now release the write lock and reaquire the read lock
327   m_priv->rwlock->unlock();
328   m_priv->rwlock->rdlock();
329 }
330
331 const std::string ShibINI::get (const string& header, const string& tag)
332 {
333   ReadLock rwlock(m_priv->rwlock);
334   refresh();
335
336   static string empty = "";
337
338   string h = header;
339   string t = tag;
340
341   if (!m_priv->cs) {
342     to_lowercase (h);
343     to_lowercase (t);
344   }
345
346   if (!m_priv->exists(h)) return empty;
347
348   map<string,string>::const_iterator i = m_priv->table[h].find(t);
349   if (i == m_priv->table[h].end())
350     return empty;
351   return i->second;
352 }
353
354 bool ShibINIPriv::exists(const std::string& header)
355 {
356   string h = header;
357   if (!cs) to_lowercase (h);
358
359   return (table.find(h) != table.end());
360 }
361
362 bool ShibINI::exists(const std::string& header)
363 {
364   ReadLock rwlock(m_priv->rwlock);
365   refresh();
366
367   return m_priv->exists(header);
368 }
369
370 bool ShibINIPriv::exists(const std::string& header, const std::string& tag)
371 {
372   string h = header;
373   string t = tag;
374
375   if (!cs) {
376     to_lowercase (h);
377     to_lowercase (t);
378   }
379
380   if (!exists(h)) return false;
381   return (table[h].find(t) != table[h].end());
382 }
383
384 bool ShibINI::exists(const std::string& header, const std::string& tag)
385 {
386   ReadLock rwlock(m_priv->rwlock);
387   refresh();
388
389   return m_priv->exists(header, tag);
390 }
391
392 bool ShibINI::get_tag (string& header, string& tag, bool try_general, string* result)
393 {
394   if (!result) return false;
395
396   m_priv->rwlock->rdlock();
397   refresh();
398   m_priv->rwlock->unlock();
399
400   if (m_priv->exists (header, tag)) {
401     *result = get (header, tag);
402     return true;
403   }
404   if (try_general && exists (SHIBTARGET_GENERAL, tag)) {
405     *result = get (SHIBTARGET_GENERAL, tag);
406     return true;
407   }
408   return false;
409 }
410
411
412 void ShibINI::dump (ostream& os)
413 {
414   ReadLock rwlock(m_priv->rwlock);
415   refresh();
416
417   os << "File: " << m_priv->file << "\n";
418   os << "Case-Sensitive: " << ( m_priv->cs ? "Yes\n" : "No\n" );
419   os << "File Entries:\n";
420
421   for (map<string, map<string, string> >::const_iterator i = m_priv->table.begin();
422        i != m_priv->table.end(); i++) {
423
424     os << "[" << i->first << "]\n";
425
426     for (map<string,string>::const_iterator j=i->second.begin();
427          j != i->second.end(); j++) {
428
429       os << "  " << j->first << " = " << j->second << "\n";
430     }
431   }
432
433   os << "END\n";
434 }
435
436 ShibINI::Iterator* ShibINI::header_iterator()
437 {
438   ReadLock rwlock(m_priv->rwlock);
439   refresh();
440   HeaderIterator* iter = new HeaderIterator(m_priv);
441   return (ShibINI::Iterator*) iter;
442 }
443
444 ShibINI::Iterator* ShibINI::tag_iterator(const std::string& header)
445 {
446   ReadLock rwlock(m_priv->rwlock);
447   refresh();
448   TagIterator* iter = new TagIterator(m_priv, header);
449   return (ShibINI::Iterator*) iter;
450 }
451
452 //
453 // XXX: FIXME: there may be a race condition in the iterators if a
454 // caller holds an active Iterator, the underlying file changes, and
455 // then calls one of the get() routines.  It's possible the iterator
456 // may screw up -- I don't know whether the iterator actually depends
457 // on the underlying infrastructure or not.
458 //
459
460 HeaderIterator::HeaderIterator (ShibINIPriv* inip)
461 {
462   saml::NDC ndc("HeaderIterator");
463   ini = inip;
464   valid = false;
465   ini->rwlock->rdlock();
466   ini->iterators++;
467   ini->log->debug("iterators: %d", ini->iterators);
468 }
469
470 HeaderIterator::~HeaderIterator ()
471 {
472   saml::NDC ndc("~HeaderIterator");
473   ini->iterators--;
474   ini->rwlock->unlock();
475   ini->log->debug("iterators: %d", ini->iterators);
476 }
477
478 const string* HeaderIterator::begin ()
479 {
480   iter = ini->table.begin();
481   if (iter == ini->table.end()) {
482     valid = false;
483     return 0;
484   }
485   valid = true;
486   return &iter->first;
487 }
488
489 const string* HeaderIterator::next ()
490 {
491   if (!valid)
492     return 0;
493   iter++;
494   if (iter == ini->table.end()) {
495     valid = false;
496     return 0;
497   }
498   return &iter->first;
499 }
500
501 TagIterator::TagIterator (ShibINIPriv* inip, const string& headerp)
502   : header(headerp)
503 {
504   saml::NDC ndc("TagIterator");
505   ini = inip;
506   valid = false;
507   ini->rwlock->rdlock();
508   ini->iterators++;
509   ini->log->debug("iterators: %d", ini->iterators);
510 }
511
512 TagIterator::~TagIterator ()
513 {
514   saml::NDC ndc("~TagIterator");
515   ini->iterators--;
516   ini->rwlock->unlock();
517   ini->log->debug("iterators: %d", ini->iterators);
518 }
519
520 const string* TagIterator::begin ()
521 {
522   iter = ini->table[header].begin();
523   if (iter == ini->table[header].end()) {
524     valid = false;
525     return 0;
526   }
527   valid = true;
528   return &iter->first;
529 }
530
531 const string* TagIterator::next ()
532 {
533   if (!valid)
534     return 0;
535   iter++;
536   if (iter == ini->table[header].end()) {
537     valid = false;
538     return 0;
539   }
540   return &iter->first;
541 }
542
543 bool ShibINI::boolean(string& value)
544 {
545   const char* v = value.c_str();
546 #ifdef HAVE_STRCASECMP
547   if (!strncasecmp (v, "on", 2) || !strncasecmp (v, "true", 4) || !strncmp(v, "1", 1))
548     return true;
549 #else
550   if (!strnicmp (v, "on", 2) || !strnicmp (v, "true", 4) || !strncmp(v, "1", 1))
551     return true;
552 #endif
553   return false;
554 }
555
556 ShibINI::Iterator::~Iterator() {}