Boost changes
[shibboleth/cpp-sp.git] / shibsp / handler / impl / DiscoveryFeed.cpp
1 /**
2  * Licensed to the University Corporation for Advanced Internet
3  * Development, Inc. (UCAID) under one or more contributor license
4  * agreements. See the NOTICE file distributed with this work for
5  * additional information regarding copyright ownership.
6  *
7  * UCAID licenses this file to you under the Apache License,
8  * Version 2.0 (the "License"); you may not use this file except
9  * in compliance with the License. You may obtain a copy of the
10  * License at
11  *
12  * http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing,
15  * software distributed under the License is distributed on an
16  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
17  * either express or implied. See the License for the specific
18  * language governing permissions and limitations under the License.
19  */
20
21 /**
22  * DiscoveryFeed.cpp
23  *
24  * Handler for generating a JSON discovery feed based on metadata.
25  */
26
27 #include "internal.h"
28 #include "Application.h"
29 #include "exceptions.h"
30 #include "ServiceProvider.h"
31 #include "SPRequest.h"
32 #include "handler/AbstractHandler.h"
33 #include "handler/RemotedHandler.h"
34
35 #include <ctime>
36 #include <fstream>
37 #include <xmltooling/XMLToolingConfig.h>
38 #include <xmltooling/util/Threads.h>
39 #include <xmltooling/util/PathResolver.h>
40
41 #ifndef SHIBSP_LITE
42 # include <queue>
43 # include <boost/scoped_ptr.hpp>
44 # include <saml/exceptions.h>
45 # include <saml/SAMLConfig.h>
46 # include <saml/saml2/metadata/DiscoverableMetadataProvider.h>
47 #endif
48
49 using namespace shibsp;
50 #ifndef SHIBSP_LITE
51 using namespace opensaml::saml2md;
52 using namespace opensaml;
53 using namespace boost;
54 #endif
55 using namespace xmltooling;
56 using namespace std;
57
58 namespace shibsp {
59
60 #if defined (_MSC_VER)
61     #pragma warning( push )
62     #pragma warning( disable : 4250 )
63 #endif
64
65     class SHIBSP_DLLLOCAL Blocker : public DOMNodeFilter
66     {
67     public:
68 #ifdef SHIBSP_XERCESC_SHORT_ACCEPTNODE
69         short
70 #else
71         FilterAction
72 #endif
73         acceptNode(const DOMNode* node) const {
74             return FILTER_REJECT;
75         }
76     };
77
78     static SHIBSP_DLLLOCAL Blocker g_Blocker;
79
80     class SHIBSP_API DiscoveryFeed : public AbstractHandler, public RemotedHandler
81     {
82     public:
83         DiscoveryFeed(const DOMElement* e, const char* appId);
84         virtual ~DiscoveryFeed();
85
86         pair<bool,long> run(SPRequest& request, bool isHandler=true) const;
87         void receive(DDF& in, ostream& out);
88
89     private:
90         void feedToFile(const Application& application, string& cacheTag) const;
91         void feedToStream(const Application& application, string& cacheTag, ostream& os) const;
92
93         string m_dir;
94         bool m_cacheToClient;
95 #ifndef SHIBSP_LITE
96         // A queue of feed files, linked to the last time of "access".
97         // Each filename is also a cache tag.
98         mutable queue< pair<string,time_t> > m_feedQueue;
99         scoped_ptr<Mutex> m_feedLock;
100 #endif
101     };
102
103 #if defined (_MSC_VER)
104     #pragma warning( pop )
105 #endif
106
107     Handler* SHIBSP_DLLLOCAL DiscoveryFeedFactory(const pair<const DOMElement*,const char*>& p)
108     {
109         return new DiscoveryFeed(p.first, p.second);
110     }
111
112 };
113
114 DiscoveryFeed::DiscoveryFeed(const DOMElement* e, const char* appId)
115     : AbstractHandler(e, Category::getInstance(SHIBSP_LOGCAT".DiscoveryFeed"), &g_Blocker), m_cacheToClient(false)
116 {
117     pair<bool,const char*> prop = getString("Location");
118     if (!prop.first)
119         throw ConfigurationException("DiscoveryFeed handler requires Location property.");
120     string address(appId);
121     address += prop.second;
122     setAddress(address.c_str());
123
124     pair<bool,bool> flag = getBool("cacheToClient");
125     m_cacheToClient = flag.first && flag.second;
126     flag = getBool("cacheToDisk");
127     if (!flag.first || flag.second) {
128         prop = getString("dir");
129         if (prop.first)
130             m_dir = prop.second;
131         XMLToolingConfig::getConfig().getPathResolver()->resolve(m_dir, PathResolver::XMLTOOLING_RUN_FILE);
132         m_log.info("feed files will be cached in %s", m_dir.c_str());
133 #ifndef SHIBSP_LITE
134         m_feedLock.reset(Mutex::create());
135 #endif
136     }
137 }
138
139 DiscoveryFeed::~DiscoveryFeed()
140 {
141 #ifndef SHIBSP_LITE
142     if (m_feedLock) {
143         // Remove any files unused for more than a couple of minutes.
144         // Anything left will be orphaned, but that shouldn't happen too often.
145         time_t now = time(nullptr);
146         while (!m_feedQueue.empty() && now - m_feedQueue.front().second > 120) {
147             string fname = m_dir + '/' + m_feedQueue.front().first;
148             remove(fname.c_str());
149             m_feedQueue.pop();
150         }
151     }
152 #endif
153 }
154
155 pair<bool,long> DiscoveryFeed::run(SPRequest& request, bool isHandler) const
156 {
157     try {
158         SPConfig& conf = SPConfig::getConfig();
159
160         string s;
161         if (m_cacheToClient)
162             s = request.getHeader("If-None-Match");
163
164         if (conf.isEnabled(SPConfig::OutOfProcess)) {
165             // When out of process, we run natively and directly process the message.
166             if (m_dir.empty()) {
167                 // The feed is directly returned.
168                 stringstream buf;
169                 feedToStream(request.getApplication(), s, buf);
170                 if (!s.empty()) {
171                     if (m_cacheToClient) {
172                         string etag = '"' + s + '"';
173                         request.setResponseHeader("ETag", etag.c_str());
174                     }
175                     request.setContentType("application/json");
176                     return make_pair(true, request.sendResponse(buf));
177                 }
178             }
179             else {
180                 // Indirect the feed through a file.
181                 feedToFile(request.getApplication(), s);
182             }
183         }
184         else {
185             // When not out of process, we remote all the message processing.
186             DDF out,in = DDF(m_address.c_str());
187             in.addmember("application_id").string(request.getApplication().getId());
188             if (!s.empty())
189                 in.addmember("cache_tag").string(s.c_str());
190             DDFJanitor jin(in), jout(out);
191             out = request.getServiceProvider().getListenerService()->send(in);
192             s.erase();
193             if (m_dir.empty()) {
194                 // The cache tag and feed are in the response struct.
195                 if (m_cacheToClient && out["cache_tag"].string()) {
196                     string etag = string("\"") + out["cache_tag"].string() + '"';
197                     request.setResponseHeader("ETag", etag.c_str());
198                 }
199                 if (out["feed"].string()) {
200                     istringstream buf(out["feed"].string());
201                     request.setContentType("application/json");
202                     return make_pair(true, request.sendResponse(buf));
203                 }
204                 throw ConfigurationException("Discovery feed was empty.");
205             }
206             else {
207                 // The response object is a string containing the cache tag.
208                 if (out.isstring() && out.string())
209                     s = out.string();
210             }
211         }
212
213         if (s.empty()) {
214             m_log.debug("client's cache tag matches our feed");
215             istringstream msg("Not Modified");
216             return make_pair(true, request.sendResponse(msg, HTTPResponse::XMLTOOLING_HTTP_STATUS_NOTMODIFIED));
217         }
218
219         string fname = m_dir + '/' + s + ".json";
220         ifstream feed(fname.c_str());
221         if (!feed)
222             throw ConfigurationException("Unable to access cached feed in ($1).", params(1,fname.c_str()));
223         if (m_cacheToClient) {
224             string etag = '"' + s + '"';
225             request.setResponseHeader("ETag", etag.c_str());
226         }
227         request.setContentType("application/json");
228         return make_pair(true, request.sendResponse(feed));
229     }
230     catch (std::exception& ex) {
231         request.log(SPRequest::SPError, string("error while processing request:") + ex.what());
232         istringstream msg("Discovery Request Failed");
233         return make_pair(true, request.sendResponse(msg, HTTPResponse::XMLTOOLING_HTTP_STATUS_ERROR));
234     }
235 }
236
237 void DiscoveryFeed::receive(DDF& in, ostream& out)
238 {
239     // Find application.
240     const char* aid = in["application_id"].string();
241     const Application* app=aid ? SPConfig::getConfig().getServiceProvider()->getApplication(aid) : nullptr;
242     if (!app) {
243         // Something's horribly wrong.
244         m_log.error("couldn't find application (%s) for discovery feed request", aid ? aid : "(missing)");
245         throw ConfigurationException("Unable to locate application for discovery feed request, deleted?");
246     }
247
248     string cacheTag;
249     if (in["cache_tag"].string())
250         cacheTag = in["cache_tag"].string();
251
252     DDF ret(nullptr);
253     DDFJanitor jout(ret);
254
255     if (!m_dir.empty()) {
256         // We're relaying the feed through a file.
257         feedToFile(*app, cacheTag);
258         if (!cacheTag.empty())
259             ret.string(cacheTag.c_str());
260     }
261     else {
262         // We're relaying the feed directly.
263         ostringstream os;
264         feedToStream(*app, cacheTag, os);
265         if (!cacheTag.empty())
266             ret.addmember("cache_tag").string(cacheTag.c_str());
267         string feed = os.str();
268         if (!feed.empty())
269             ret.addmember("feed").string(feed.c_str());
270     }
271     out << ret;
272 }
273
274 void DiscoveryFeed::feedToFile(const Application& application, string& cacheTag) const
275 {
276 #ifndef SHIBSP_LITE
277     m_log.debug("processing discovery feed request");
278
279     DiscoverableMetadataProvider* m = dynamic_cast<DiscoverableMetadataProvider*>(application.getMetadataProvider(false));
280     if (!m)
281         m_log.warn("MetadataProvider missing or does not support discovery feed");
282     Locker locker(m);
283     string feedTag = m ? m->getCacheTag() : "empty";
284     if (cacheTag == ('"' + feedTag + '"')) {
285         // The client already has the same feed we do.
286         m_log.debug("client's cache tag matches our feed (%s)", feedTag.c_str());
287         cacheTag.erase();   // clear the tag to signal no change
288         return;
289     }
290
291     cacheTag = feedTag;
292
293     // The client is out of date or not caching, so we need to see if our copy is good.
294     Lock lock(m_feedLock);
295     time_t now = time(nullptr);
296
297     // Clean up any old files.
298     while (m_feedQueue.size() > 1 && (now - m_feedQueue.front().second > 120)) {
299         string fname = m_dir + '/' + m_feedQueue.front().first;
300         remove(fname.c_str());
301         m_feedQueue.pop();
302     }
303
304     if (m_feedQueue.empty() || m_feedQueue.back().first != feedTag) {
305         // We're out of date.
306         string fname = m_dir + '/' + feedTag + ".json";
307         ofstream ofile(fname.c_str());
308         if (!ofile)
309             throw ConfigurationException("Unable to create feed in ($1).", params(1,fname.c_str()));
310         bool first = true;
311         if (m)
312             m->outputFeed(ofile, first);
313         else
314             ofile << "[\n]";
315         ofile.close();
316         m_feedQueue.push(make_pair(feedTag, now));
317     }
318     else {
319         // Update the back of the queue.
320         m_feedQueue.back().second = now;
321     }
322 #else
323     throw ConfigurationException("Build does not support discovery feed.");
324 #endif
325 }
326
327 void DiscoveryFeed::feedToStream(const Application& application, string& cacheTag, ostream& os) const
328 {
329 #ifndef SHIBSP_LITE
330     m_log.debug("processing discovery feed request");
331
332     DiscoverableMetadataProvider* m = dynamic_cast<DiscoverableMetadataProvider*>(application.getMetadataProvider(false));
333     if (!m)
334         m_log.warn("MetadataProvider missing or does not support discovery feed");
335     Locker locker(m);
336     string feedTag = m ? m->getCacheTag() : "empty";
337     if (cacheTag == ('"' + feedTag + '"')) {
338         // The client already has the same feed we do.
339         m_log.debug("client's cache tag matches our feed (%s)", feedTag.c_str());
340         cacheTag.erase();   // clear the tag to signal no change
341         return;
342     }
343
344     cacheTag = feedTag;
345     bool first = true;
346     if (m)
347         m->outputFeed(os, first);
348     else
349         os << "[\n]";
350 #else
351     throw ConfigurationException("Build does not support discovery feed.");
352 #endif
353 }