2 * Copyright 2010 Internet2
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
20 * Handler for generating a JSON discovery feed based on metadata.
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"
33 #include <xmltooling/XMLToolingConfig.h>
34 #include <xmltooling/util/Threads.h>
35 #include <xmltooling/util/PathResolver.h>
39 # include <saml/exceptions.h>
40 # include <saml/SAMLConfig.h>
41 # include <saml/saml2/metadata/DiscoverableMetadataProvider.h>
44 using namespace shibsp;
46 using namespace opensaml::saml2md;
47 using namespace opensaml;
49 using namespace xmltooling;
54 #if defined (_MSC_VER)
55 #pragma warning( push )
56 #pragma warning( disable : 4250 )
59 class SHIBSP_DLLLOCAL Blocker : public DOMNodeFilter
62 #ifdef SHIBSP_XERCESC_SHORT_ACCEPTNODE
67 acceptNode(const DOMNode* node) const {
72 static SHIBSP_DLLLOCAL Blocker g_Blocker;
74 class SHIBSP_API DiscoveryFeed : public AbstractHandler, public RemotedHandler
77 DiscoveryFeed(const DOMElement* e, const char* appId);
78 virtual ~DiscoveryFeed();
80 pair<bool,long> run(SPRequest& request, bool isHandler=true) const;
81 void receive(DDF& in, ostream& out);
84 void feedToFile(const Application& application, string& cacheTag) const;
85 void feedToStream(const Application& application, string& cacheTag, ostream& os) const;
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;
97 #if defined (_MSC_VER)
98 #pragma warning( pop )
101 Handler* SHIBSP_DLLLOCAL DiscoveryFeedFactory(const pair<const DOMElement*,const char*>& p)
103 return new DiscoveryFeed(p.first, p.second);
108 DiscoveryFeed::DiscoveryFeed(const DOMElement* e, const char* appId)
109 : AbstractHandler(e, Category::getInstance(SHIBSP_LOGCAT".DiscoveryFeed"), &g_Blocker), m_cacheToClient(false)
111 , m_feedLock(nullptr)
114 pair<bool,const char*> prop = getString("Location");
116 throw ConfigurationException("DiscoveryFeed handler requires Location property.");
117 string address(appId);
118 address += prop.second;
119 setAddress(address.c_str());
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");
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());
131 m_feedLock = Mutex::create();
136 DiscoveryFeed::~DiscoveryFeed()
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());
153 pair<bool,long> DiscoveryFeed::run(SPRequest& request, bool isHandler) const
156 SPConfig& conf = SPConfig::getConfig();
160 s = request.getHeader("If-None-Match");
162 if (conf.isEnabled(SPConfig::OutOfProcess)) {
163 // When out of process, we run natively and directly process the message.
165 // The feed is directly returned.
167 feedToStream(request.getApplication(), s, buf);
169 if (m_cacheToClient) {
170 string etag = '"' + s + '"';
171 request.setResponseHeader("ETag", etag.c_str());
173 request.setContentType("application/json");
174 return make_pair(true, request.sendResponse(buf));
178 // Indirect the feed through a file.
179 feedToFile(request.getApplication(), s);
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());
187 in.addmember("cache_tag").string(s.c_str());
188 DDFJanitor jin(in), jout(out);
189 out = request.getServiceProvider().getListenerService()->send(in);
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());
197 if (out["feed"].string()) {
198 istringstream buf(out["feed"].string());
199 request.setContentType("application/json");
200 return make_pair(true, request.sendResponse(buf));
202 throw ConfigurationException("Discovery feed was empty.");
205 // The response object is a string containing the cache tag.
206 if (out.isstring() && out.string())
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));
217 string fname = m_dir + '/' + s + ".json";
218 ifstream feed(fname.c_str());
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());
225 request.setContentType("application/json");
226 return make_pair(true, request.sendResponse(feed));
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));
235 void DiscoveryFeed::receive(DDF& in, ostream& out)
238 const char* aid = in["application_id"].string();
239 const Application* app=aid ? SPConfig::getConfig().getServiceProvider()->getApplication(aid) : nullptr;
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?");
247 if (in["cache_tag"].string())
248 cacheTag = in["cache_tag"].string();
251 DDFJanitor jout(ret);
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());
260 // We're relaying the feed directly.
262 feedToStream(*app, cacheTag, os);
263 if (!cacheTag.empty())
264 ret.addmember("cache_tag").string(cacheTag.c_str());
265 string feed = os.str();
267 ret.addmember("feed").string(feed.c_str());
272 void DiscoveryFeed::feedToFile(const Application& application, string& cacheTag) const
275 m_log.debug("processing discovery feed request");
277 DiscoverableMetadataProvider* m=dynamic_cast<DiscoverableMetadataProvider*>(application.getMetadataProvider());
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
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);
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());
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());
306 throw ConfigurationException("Unable to create feed in ($1).", params(1,fname.c_str()));
308 m->outputFeed(ofile, first);
310 m_feedQueue.push(make_pair(feedTag, now));
313 // Update the back of the queue.
314 m_feedQueue.back().second = now;
318 throw MetadataException("MetadataProvider does not support discovery feed.");
321 throw ConfigurationException("Build does not support discovery feed.");
325 void DiscoveryFeed::feedToStream(const Application& application, string& cacheTag, ostream& os) const
328 m_log.debug("processing discovery feed request");
330 DiscoverableMetadataProvider* m=dynamic_cast<DiscoverableMetadataProvider*>(application.getMetadataProvider());
333 string feedTag = m->getCacheTag();
334 if (cacheTag == ('"' + feedTag + '"')) {
335 // The client already has the same feed we do.
336 m_log.debug("client's cache tag matches our feed (%s)", feedTag.c_str());
337 cacheTag.erase(); // clear the tag to signal no change
343 m->outputFeed(os, first);
346 throw MetadataException("MetadataProvider does not support discovery feed.");
349 throw ConfigurationException("Build does not support discovery feed.");