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