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