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.
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
12 * http://www.apache.org/licenses/LICENSE-2.0
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.
24 * Handler for generating a JSON discovery feed based on metadata.
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"
37 #include <xmltooling/XMLToolingConfig.h>
38 #include <xmltooling/util/Threads.h>
39 #include <xmltooling/util/PathResolver.h>
43 # include <saml/exceptions.h>
44 # include <saml/SAMLConfig.h>
45 # include <saml/saml2/metadata/DiscoverableMetadataProvider.h>
48 using namespace shibsp;
50 using namespace opensaml::saml2md;
51 using namespace opensaml;
52 using namespace boost;
54 using namespace xmltooling;
59 #if defined (_MSC_VER)
60 #pragma warning( push )
61 #pragma warning( disable : 4250 )
64 class SHIBSP_DLLLOCAL Blocker : public DOMNodeFilter
67 #ifdef SHIBSP_XERCESC_SHORT_ACCEPTNODE
72 acceptNode(const DOMNode* node) const {
77 static SHIBSP_DLLLOCAL Blocker g_Blocker;
79 class SHIBSP_API DiscoveryFeed : public AbstractHandler, public RemotedHandler
82 DiscoveryFeed(const DOMElement* e, const char* appId);
83 virtual ~DiscoveryFeed();
85 pair<bool,long> run(SPRequest& request, bool isHandler=true) const;
86 void receive(DDF& in, ostream& out);
89 void feedToFile(const Application& application, string& cacheTag) const;
90 void feedToStream(const Application& application, string& cacheTag, ostream& os) const;
95 // Application-specific queues of feed files, linked to the last time of "access".
96 // Each filename is also a cache tag.
97 typedef queue< pair<string, time_t> > feedqueue_t;
98 mutable map<string,feedqueue_t> m_feedQueues;
99 scoped_ptr<Mutex> m_feedLock;
103 #if defined (_MSC_VER)
104 #pragma warning( pop )
107 Handler* SHIBSP_DLLLOCAL DiscoveryFeedFactory(const pair<const DOMElement*,const char*>& p)
109 return new DiscoveryFeed(p.first, p.second);
114 DiscoveryFeed::DiscoveryFeed(const DOMElement* e, const char* appId)
115 : AbstractHandler(e, Category::getInstance(SHIBSP_LOGCAT ".DiscoveryFeed"), &g_Blocker), m_cacheToClient(false)
117 pair<bool,const char*> prop = getString("Location");
119 throw ConfigurationException("DiscoveryFeed handler requires Location property.");
120 string address(appId);
121 address += prop.second;
122 setAddress(address.c_str());
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");
131 XMLToolingConfig::getConfig().getPathResolver()->resolve(m_dir, PathResolver::XMLTOOLING_CACHE_FILE);
132 m_log.info("feed files will be cached in %s", m_dir.c_str());
134 m_feedLock.reset(Mutex::create());
139 DiscoveryFeed::~DiscoveryFeed()
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 for (map<string, feedqueue_t>::iterator i = m_feedQueues.begin(); i != m_feedQueues.end(); ++i) {
147 while (!i->second.empty() && now - i->second.front().second > 120) {
148 string fname = m_dir + '/' + i->second.front().first + ".json";
149 remove(fname.c_str());
157 pair<bool,long> DiscoveryFeed::run(SPRequest& request, bool isHandler) const
160 SPConfig& conf = SPConfig::getConfig();
163 if (m_cacheToClient) {
164 s = request.getHeader("If-None-Match");
167 if (conf.isEnabled(SPConfig::OutOfProcess)) {
168 // When out of process, we run natively and directly process the message.
170 // The feed is directly returned.
172 feedToStream(request.getApplication(), s, buf);
174 if (m_cacheToClient) {
175 string etag = '"' + s + '"';
176 request.setResponseHeader("ETag", etag.c_str());
178 request.setContentType("application/json; charset=UTF-8");
179 return make_pair(true, request.sendResponse(buf));
183 // Indirect the feed through a file.
184 feedToFile(request.getApplication(), s);
188 // When not out of process, we remote all the message processing.
189 DDF out,in = DDF(m_address.c_str());
190 in.addmember("application_id").string(request.getApplication().getId());
192 in.addmember("cache_tag").string(s.c_str());
193 DDFJanitor jin(in), jout(out);
194 out = request.getServiceProvider().getListenerService()->send(in);
197 // The cache tag and feed are in the response struct.
198 if (m_cacheToClient && out["cache_tag"].string()) {
199 string etag = string("\"") + out["cache_tag"].string() + '"';
200 request.setResponseHeader("ETag", etag.c_str());
202 if (out["feed"].string()) {
203 istringstream buf(out["feed"].string());
204 request.setContentType("application/json; charset=UTF-8");
205 return make_pair(true, request.sendResponse(buf));
207 throw ConfigurationException("Discovery feed was empty.");
210 // The response object is a string containing the cache tag.
211 if (out.isstring() && out.string())
217 m_log.debug("client's cache tag matches our feed");
218 istringstream msg("Not Modified");
219 return make_pair(true, request.sendResponse(msg, HTTPResponse::XMLTOOLING_HTTP_STATUS_NOTMODIFIED));
222 string fname = m_dir + '/' + request.getApplication().getHash() + '_' + s + ".json";
223 ifstream feed(fname.c_str());
225 throw ConfigurationException("Unable to access cached feed in ($1).", params(1,fname.c_str()));
226 if (m_cacheToClient) {
227 string etag = '"' + s + '"';
228 request.setResponseHeader("ETag", etag.c_str());
230 request.setContentType("application/json; charset=UTF-8");
231 return make_pair(true, request.sendResponse(feed));
233 catch (std::exception& ex) {
234 request.log(SPRequest::SPError, string("error while processing request:") + ex.what());
235 istringstream msg("Discovery Request Failed");
236 return make_pair(true, request.sendResponse(msg, HTTPResponse::XMLTOOLING_HTTP_STATUS_ERROR));
240 void DiscoveryFeed::receive(DDF& in, ostream& out)
243 const char* aid = in["application_id"].string();
244 const Application* app=aid ? SPConfig::getConfig().getServiceProvider()->getApplication(aid) : nullptr;
246 // Something's horribly wrong.
247 m_log.error("couldn't find application (%s) for discovery feed request", aid ? aid : "(missing)");
248 throw ConfigurationException("Unable to locate application for discovery feed request, deleted?");
252 if (in["cache_tag"].string())
253 cacheTag = in["cache_tag"].string();
256 DDFJanitor jout(ret);
258 if (!m_dir.empty()) {
259 // We're relaying the feed through a file.
260 feedToFile(*app, cacheTag);
261 if (!cacheTag.empty())
262 ret.string(cacheTag.c_str());
265 // We're relaying the feed directly.
267 feedToStream(*app, cacheTag, os);
268 if (!cacheTag.empty())
269 ret.addmember("cache_tag").string(cacheTag.c_str());
270 string feed = os.str();
272 ret.addmember("feed").string(feed.c_str());
277 void DiscoveryFeed::feedToFile(const Application& application, string& cacheTag) const
280 m_log.debug("processing discovery feed request");
282 DiscoverableMetadataProvider* m = dynamic_cast<DiscoverableMetadataProvider*>(application.getMetadataProvider(false));
284 m_log.warn("MetadataProvider missing or does not support discovery feed");
286 string feedTag = m ? m->getCacheTag() : "empty";
287 if (cacheTag == ('"' + feedTag + '"')) {
288 // The client already has the same feed we do.
289 m_log.debug("client's cache tag matches our feed (%s)", feedTag.c_str());
290 cacheTag.erase(); // clear the tag to signal no change
296 // The client is out of date or not caching, so we need to see if our copy is good.
297 Lock lock(m_feedLock);
298 time_t now = time(nullptr);
300 // Clean up any old files.
301 feedqueue_t q = m_feedQueues[application.getId()];
302 while (q.size() > 1 && (now - q.front().second > 120)) {
303 string fname = m_dir + '/' + application.getHash() + '_' + q.front().first + ".json";
304 remove(fname.c_str());
308 if (q.empty() || q.back().first != feedTag) {
309 // We're out of date.
310 string fname = m_dir + '/' + application.getHash() + '_' + feedTag + ".json";
311 ofstream ofile(fname.c_str());
313 throw ConfigurationException("Unable to create feed in ($1).", params(1,fname.c_str()));
316 m->outputFeed(ofile, first);
320 q.push(make_pair(feedTag, now));
323 // Update the back of the queue.
324 q.back().second = now;
327 throw ConfigurationException("Build does not support discovery feed.");
331 void DiscoveryFeed::feedToStream(const Application& application, string& cacheTag, ostream& os) const
334 m_log.debug("processing discovery feed request");
336 DiscoverableMetadataProvider* m = dynamic_cast<DiscoverableMetadataProvider*>(application.getMetadataProvider(false));
338 m_log.warn("MetadataProvider missing or does not support discovery feed");
340 string feedTag = m ? m->getCacheTag() : "empty";
341 if (cacheTag == ('"' + feedTag + '"')) {
342 // The client already has the same feed we do.
343 m_log.debug("client's cache tag matches our feed (%s)", feedTag.c_str());
344 cacheTag.erase(); // clear the tag to signal no change
351 m->outputFeed(os, first);
355 throw ConfigurationException("Build does not support discovery feed.");