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