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  * SAML2NameIDMgmt.cpp
23  *
24  * Handles SAML 2.0 NameID management protocol messages.
25  */
26 
27 #include "internal.h"
28 #include "exceptions.h"
29 #include "Application.h"
30 #include "ServiceProvider.h"
31 #include "SPRequest.h"
32 #include "TransactionLog.h"
33 #include "handler/AbstractHandler.h"
34 #include "handler/RemotedHandler.h"
35 #include "util/SPConstants.h"
36 
37 #ifndef SHIBSP_LITE
38 # include "SessionCache.h"
39 # include "security/SecurityPolicy.h"
40 # include "security/SecurityPolicyProvider.h"
41 # include <fstream>
42 # include <boost/algorithm/string.hpp>
43 # include <boost/iterator/indirect_iterator.hpp>
44 # include <saml/exceptions.h>
45 # include <saml/SAMLConfig.h>
46 # include <saml/saml2/core/Protocols.h>
47 # include <saml/saml2/metadata/EndpointManager.h>
48 # include <saml/saml2/metadata/Metadata.h>
49 # include <saml/saml2/metadata/MetadataCredentialCriteria.h>
50 # include <xmltooling/util/URLEncoder.h>
51 using namespace opensaml::saml2;
52 using namespace opensaml::saml2p;
53 using namespace opensaml::saml2md;
54 using namespace opensaml;
55 #else
56 # include "lite/SAMLConstants.h"
57 #endif
58 
59 #include <boost/scoped_ptr.hpp>
60 
61 using namespace shibsp;
62 using namespace xmltooling;
63 using namespace boost;
64 using namespace std;
65 
66 namespace shibsp {
67 
68 #if defined (_MSC_VER)
69     #pragma warning( push )
70     #pragma warning( disable : 4250 )
71 #endif
72 
73     class SHIBSP_DLLLOCAL SAML2NameIDMgmt : public AbstractHandler, public RemotedHandler
74     {
75     public:
76         SAML2NameIDMgmt(const DOMElement* e, const char* appId, bool deprecationSupport=true);
~SAML2NameIDMgmt()77         virtual ~SAML2NameIDMgmt() {}
78 
79         void receive(DDF& in, ostream& out);
80         pair<bool,long> run(SPRequest& request, bool isHandler=true) const;
81 
82 #ifndef SHIBSP_LITE
generateMetadata(SPSSODescriptor & role,const char * handlerURL) const83         void generateMetadata(SPSSODescriptor& role, const char* handlerURL) const {
84             const char* loc = getString("Location").second;
85             string hurl(handlerURL);
86             if (*loc != '/')
87                 hurl += '/';
88             hurl += loc;
89             auto_ptr_XMLCh widen(hurl.c_str());
90             ManageNameIDService* ep = ManageNameIDServiceBuilder::buildManageNameIDService();
91             ep->setLocation(widen.get());
92             ep->setBinding(getXMLString("Binding").second);
93             role.getManageNameIDServices().push_back(ep);
94             role.addSupport(samlconstants::SAML20P_NS);
95         }
96 
getType() const97         const char* getType() const {
98             return "ManageNameIDService";
99         }
100 #endif
getProtocolFamily() const101         const XMLCh* getProtocolFamily() const {
102             return samlconstants::SAML20P_NS;
103         }
104 
getEventType() const105         const char* getEventType() const {
106             return NAMEIDMGMT_EVENT;
107         }
108 
109     private:
110         pair<bool,long> doRequest(const Application& application, HTTPRequest& httpRequest, HTTPResponse& httpResponse) const;
111 
112 #ifndef SHIBSP_LITE
113         bool notifyBackChannel(const Application& application, const char* requestURL, const NameID& nameid, const NewID* newid) const;
114 
115         pair<bool,long> sendResponse(
116             const XMLCh* requestID,
117             const XMLCh* code,
118             const XMLCh* subcode,
119             const char* msg,
120             const char* relayState,
121             const RoleDescriptor* role,
122             const Application& application,
123             HTTPResponse& httpResponse,
124             bool front
125             ) const;
126 
127         scoped_ptr<MessageDecoder> m_decoder;
128         vector<string> m_bindings;
129         map< string,boost::shared_ptr<MessageEncoder> > m_encoders;
130 #endif
131     };
132 
133 #if defined (_MSC_VER)
134     #pragma warning( pop )
135 #endif
136 
SAML2NameIDMgmtFactory(const pair<const DOMElement *,const char * > & p,bool deprecationSupport)137     Handler* SHIBSP_DLLLOCAL SAML2NameIDMgmtFactory(const pair<const DOMElement*,const char*>& p, bool deprecationSupport)
138     {
139         return new SAML2NameIDMgmt(p.first, p.second, deprecationSupport);
140     }
141 };
142 
SAML2NameIDMgmt(const DOMElement * e,const char * appId,bool deprecationSupport)143 SAML2NameIDMgmt::SAML2NameIDMgmt(const DOMElement* e, const char* appId, bool deprecationSupport)
144     : AbstractHandler(e, Category::getInstance(SHIBSP_LOGCAT ".NameIDMgmt.SAML2"))
145 {
146     SPConfig::getConfig().deprecation().warn("SAML 2.0 NameID Management support");
147 #ifndef SHIBSP_LITE
148     if (SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
149         SAMLConfig& conf = SAMLConfig::getConfig();
150 
151         // Handle incoming binding.
152         m_decoder.reset(conf.MessageDecoderManager.newPlugin(getString("Binding").second, e, deprecationSupport));
153         m_decoder->setArtifactResolver(SPConfig::getConfig().getArtifactResolver());
154 
155         if (m_decoder->isUserAgentPresent()) {
156             // Handle front-channel binding setup.
157             string dupBindings;
158             pair<bool,const char*> outgoing = getString("outgoingBindings", shibspconstants::ASCII_SHIBSPCONFIG_NS);
159             if (outgoing.first) {
160                 dupBindings = outgoing.second;
161                 trim(dupBindings);
162             }
163             else {
164                 // No override, so we'll install a default binding precedence.
165                 dupBindings = string(samlconstants::SAML20_BINDING_HTTP_REDIRECT) + ' ' + samlconstants::SAML20_BINDING_HTTP_POST + ' ' +
166                     samlconstants::SAML20_BINDING_HTTP_POST_SIMPLESIGN + ' ' + samlconstants::SAML20_BINDING_HTTP_ARTIFACT;
167             }
168 
169             split(m_bindings, dupBindings, is_space(), algorithm::token_compress_on);
170             for (vector<string>::const_iterator b = m_bindings.begin(); b != m_bindings.end(); ++b) {
171                 try {
172                     boost::shared_ptr<MessageEncoder> encoder(conf.MessageEncoderManager.newPlugin(*b, e, deprecationSupport));
173                     if (encoder->isUserAgentPresent() && XMLString::equals(getProtocolFamily(), encoder->getProtocolFamily())) {
174                         m_encoders[*b] = encoder;
175                         m_log.debug("supporting outgoing binding (%s)", b->c_str());
176                     }
177                     else {
178                         m_log.warn("skipping outgoing binding (%s), not a SAML 2.0 front-channel mechanism", b->c_str());
179                     }
180                 }
181                 catch (std::exception& ex) {
182                     m_log.error("error building MessageEncoder: %s", ex.what());
183                 }
184             }
185         }
186         else {
187             pair<bool,const char*> b = getString("Binding");
188             boost::shared_ptr<MessageEncoder> encoder(conf.MessageEncoderManager.newPlugin(b.second, e, deprecationSupport));
189             m_encoders[b.second] = encoder;
190         }
191     }
192 #endif
193 
194     string address(appId);
195     address += getString("Location").second;
196     setAddress(address.c_str());
197 }
198 
run(SPRequest & request,bool isHandler) const199 pair<bool,long> SAML2NameIDMgmt::run(SPRequest& request, bool isHandler) const
200 {
201     SPConfig& conf = SPConfig::getConfig();
202     if (conf.isEnabled(SPConfig::OutOfProcess)) {
203         // When out of process, we run natively and directly process the message.
204         return doRequest(request.getApplication(), request, request);
205     }
206     else {
207         // When not out of process, we remote all the message processing.
208         vector<string> headers(1,"Cookie");
209         headers.push_back("User-Agent");
210         DDF out,in = wrap(request, &headers, true);
211         DDFJanitor jin(in), jout(out);
212         out = send(request, in);
213         return unwrap(request, out);
214     }
215 }
216 
receive(DDF & in,ostream & out)217 void SAML2NameIDMgmt::receive(DDF& in, ostream& out)
218 {
219     // Find application.
220     const char* aid = in["application_id"].string();
221     const Application* app = aid ? SPConfig::getConfig().getServiceProvider()->getApplication(aid) : nullptr;
222     if (!app) {
223         // Something's horribly wrong.
224         m_log.error("couldn't find application (%s) for NameID mgmt", aid ? aid : "(missing)");
225         throw ConfigurationException("Unable to locate application for NameID mgmt, deleted?");
226     }
227 
228     // Unpack the request.
229     scoped_ptr<HTTPRequest> req(getRequest(*app, in));
230 
231     // Wrap a response shim.
232     DDF ret(nullptr);
233     DDFJanitor jout(ret);
234     scoped_ptr<HTTPResponse> resp(getResponse(*app, ret));
235 
236     // Since we're remoted, the result should either be a throw, which we pass on,
237     // a false/0 return, which we just return as an empty structure, or a response/redirect,
238     // which we capture in the facade and send back.
239     doRequest(*app, *req, *resp);
240     out << ret;
241 }
242 
doRequest(const Application & application,HTTPRequest & request,HTTPResponse & response) const243 pair<bool,long> SAML2NameIDMgmt::doRequest(const Application& application, HTTPRequest& request, HTTPResponse& response) const
244 {
245 #ifndef SHIBSP_LITE
246     SessionCache* cache = application.getServiceProvider().getSessionCache();
247 
248     // Locate policy key.
249     pair<bool,const char*> policyId = getString("policyId", shibspconstants::ASCII_SHIBSPCONFIG_NS);  // may be namespace-qualified inside handler element
250     if (!policyId.first)
251         policyId = getString("policyId");   // try unqualified
252     if (!policyId.first)
253         policyId = application.getString("policyId");   // unqualified in Application(s) element
254 
255     // Lock metadata for use by policy.
256     Locker metadataLocker(application.getMetadataProvider());
257 
258     // Create the policy.
259     scoped_ptr<SecurityPolicy> policy(
260         application.getServiceProvider().getSecurityPolicyProvider()->createSecurityPolicy(
261             samlconstants::SAML20_PROFILE_SSO_NAMEID_MGMT, application, &IDPSSODescriptor::ELEMENT_QNAME, policyId.second
262             )
263         );
264 
265     // Decode the message.
266     string relayState;
267     scoped_ptr<XMLObject> msg(m_decoder->decode(relayState, request, &response, *policy));
268     const ManageNameIDRequest* mgmtRequest = dynamic_cast<ManageNameIDRequest*>(msg.get());
269     if (mgmtRequest) {
270         if (!policy->isAuthenticated())
271             throw SecurityPolicyException("Security of ManageNameIDRequest not established.");
272 
273         // Message from IdP to change or terminate a NameID.
274 
275         // If this is front-channel, we have to have a session_id to use already.
276         string session_id = cache->active(application, request);
277         if (m_decoder->isUserAgentPresent() && session_id.empty()) {
278             m_log.error("no active session");
279             return sendResponse(
280                 mgmtRequest->getID(),
281                 StatusCode::REQUESTER, StatusCode::UNKNOWN_PRINCIPAL, "No active session found in request.",
282                 relayState.c_str(),
283                 policy->getIssuerMetadata(),
284                 application,
285                 response,
286                 true
287                 );
288         }
289 
290         EntityDescriptor* entity = policy->getIssuerMetadata() ?
291                 dynamic_cast<EntityDescriptor*>(policy->getIssuerMetadata()->getParent()) : nullptr;
292 
293         scoped_ptr<XMLObject> decryptedID;
294         NameID* nameid = mgmtRequest->getNameID();
295         if (!nameid) {
296             // Check for EncryptedID.
297             EncryptedID* encname = mgmtRequest->getEncryptedID();
298             if (encname) {
299                 CredentialResolver* cr=application.getCredentialResolver();
300                 if (!cr)
301                     m_log.warn("found encrypted NameID, but no decryption credential was available");
302                 else {
303                     Locker credlocker(cr);
304                     scoped_ptr<MetadataCredentialCriteria> mcc(
305                         policy->getIssuerMetadata() ? new MetadataCredentialCriteria(*policy->getIssuerMetadata()) : nullptr
306                         );
307                     try {
308                         decryptedID.reset(encname->decrypt(*cr, application.getRelyingParty(entity)->getXMLString("entityID").second, mcc.get()));
309                         nameid = dynamic_cast<NameID*>(decryptedID.get());
310                     }
311                     catch (std::exception& ex) {
312                         m_log.error(ex.what());
313                     }
314                 }
315             }
316         }
317         if (!nameid) {
318             // No NameID, so must respond with an error.
319             m_log.error("NameID not found in request");
320             return sendResponse(
321                 mgmtRequest->getID(),
322                 StatusCode::REQUESTER, StatusCode::UNKNOWN_PRINCIPAL, "NameID not found in request.",
323                 relayState.c_str(),
324                 policy->getIssuerMetadata(),
325                 application,
326                 response,
327                 m_decoder->isUserAgentPresent()
328                 );
329         }
330 
331         // For a front-channel request, we have to match the information in the request
332         // against the current session.
333         if (!session_id.empty()) {
334             if (!cache->matches(application, request, entity, *nameid, nullptr)) {
335                 return sendResponse(
336                     mgmtRequest->getID(),
337                     StatusCode::REQUESTER, StatusCode::REQUEST_DENIED, "Active session did not match NameID mgmt request.",
338                     relayState.c_str(),
339                     policy->getIssuerMetadata(),
340                     application,
341                     response,
342                     true
343                     );
344             }
345 
346         }
347 
348         // Determine what's happening...
349         scoped_ptr<XMLObject> newDecryptedID;
350         NewID* newid = nullptr;
351         if (!mgmtRequest->getTerminate()) {
352             // Better be a NewID in there.
353             newid = mgmtRequest->getNewID();
354             if (!newid) {
355                 // Check for NewEncryptedID.
356                 NewEncryptedID* encnewid = mgmtRequest->getNewEncryptedID();
357                 if (encnewid) {
358                     CredentialResolver* cr=application.getCredentialResolver();
359                     if (!cr)
360                         m_log.warn("found encrypted NewID, but no decryption credential was available");
361                     else {
362                         Locker credlocker(cr);
363                         scoped_ptr<MetadataCredentialCriteria> mcc(
364                             policy->getIssuerMetadata() ? new MetadataCredentialCriteria(*policy->getIssuerMetadata()) : nullptr
365                             );
366                         try {
367                             newDecryptedID.reset(encnewid->decrypt(*cr, application.getRelyingParty(entity)->getXMLString("entityID").second, mcc.get()));
368                             newid = dynamic_cast<NewID*>(newDecryptedID.get());
369                         }
370                         catch (std::exception& ex) {
371                             m_log.error(ex.what());
372                         }
373                     }
374                 }
375             }
376 
377             if (!newid) {
378                 // No NewID, so must respond with an error.
379                 m_log.error("NewID not found in request");
380                 return sendResponse(
381                     mgmtRequest->getID(),
382                     StatusCode::REQUESTER, nullptr, "NewID not found in request.",
383                     relayState.c_str(),
384                     policy->getIssuerMetadata(),
385                     application,
386                     response,
387                     m_decoder->isUserAgentPresent()
388                     );
389             }
390         }
391 
392         // TODO: maybe support in-place modification of sessions?
393         /*
394         vector<string> sessions;
395         try {
396             time_t expires = logoutRequest->getNotOnOrAfter() ? logoutRequest->getNotOnOrAfterEpoch() : 0;
397             cache->logout(entity, *nameid, &indexes, expires, application, sessions);
398 
399             // Now we actually terminate everything except for the active session,
400             // if this is front-channel, for notification purposes.
401             for (vector<string>::const_iterator sit = sessions.begin(); sit != sessions.end(); ++sit)
402                 if (session_id && strcmp(sit->c_str(), session_id))
403                     cache->remove(sit->c_str(), application);
404         }
405         catch (exception& ex) {
406             m_log.error("error while logging out matching sessions: %s", ex.what());
407             return sendResponse(
408                 logoutRequest->getID(),
409                 StatusCode::RESPONDER, nullptr, ex.what(),
410                 relayState.c_str(),
411                 policy.getIssuerMetadata(),
412                 application,
413                 response,
414                 m_decoder->isUserAgentPresent()
415                 );
416         }
417         */
418 
419         // Do back-channel app notifications.
420         // Not supporting front-channel due to privacy concerns.
421         bool worked = notifyBackChannel(application, request.getRequestURL(), *nameid, newid);
422 
423         return sendResponse(
424             mgmtRequest->getID(),
425             worked ? StatusCode::SUCCESS : StatusCode::RESPONDER,
426             nullptr,
427             nullptr,
428             relayState.c_str(),
429             policy->getIssuerMetadata(),
430             application,
431             response,
432             m_decoder->isUserAgentPresent()
433             );
434     }
435 
436     // A ManageNameIDResponse completes an SP-initiated sequence, currently not supported.
437     /*
438     const ManageNameIDResponse* mgmtResponse = dynamic_cast<ManageNameIDResponse*>(msg.get());
439     if (mgmtResponse) {
440         if (!policy.isAuthenticated()) {
441             SecurityPolicyException ex("Security of ManageNameIDResponse not established.");
442             if (policy.getIssuerMetadata())
443                 annotateException(&ex, policy.getIssuerMetadata()); // throws it
444             ex.raise();
445         }
446         checkError(mgmtResponse, policy.getIssuerMetadata()); // throws if Status doesn't look good...
447 
448         // Return template for completion.
449         return sendLogoutPage(application, response, false, "Global logout completed.");
450     }
451     */
452 
453     FatalProfileException ex("Incoming message was not a samlp:ManageNameIDRequest.");
454     annotateException(&ex, policy->getIssuerMetadata()); // throws it
455     return make_pair(false, 0L);  // never happen, satisfies compiler
456 #else
457     throw ConfigurationException("Cannot process NameID mgmt message using lite version of shibsp library.");
458 #endif
459 }
460 
461 #ifndef SHIBSP_LITE
462 
sendResponse(const XMLCh * requestID,const XMLCh * code,const XMLCh * subcode,const char * msg,const char * relayState,const RoleDescriptor * role,const Application & application,HTTPResponse & httpResponse,bool front) const463 pair<bool,long> SAML2NameIDMgmt::sendResponse(
464     const XMLCh* requestID,
465     const XMLCh* code,
466     const XMLCh* subcode,
467     const char* msg,
468     const char* relayState,
469     const RoleDescriptor* role,
470     const Application& application,
471     HTTPResponse& httpResponse,
472     bool front
473     ) const
474 {
475     // Get endpoint and encoder to use.
476     const EndpointType* ep = nullptr;
477     const MessageEncoder* encoder = nullptr;
478     if (front) {
479         const IDPSSODescriptor* idp = dynamic_cast<const IDPSSODescriptor*>(role);
480         for (vector<string>::const_iterator b = m_bindings.begin(); idp && b != m_bindings.end(); ++b) {
481             auto_ptr_XMLCh wideb(b->c_str());
482             if ((ep = EndpointManager<ManageNameIDService>(idp->getManageNameIDServices()).getByBinding(wideb.get()))) {
483                 map< string,boost::shared_ptr<MessageEncoder> >::const_iterator enc = m_encoders.find(*b);
484                 if (enc != m_encoders.end())
485                     encoder = enc->second.get();
486                 break;
487             }
488         }
489         if (!ep || !encoder) {
490             auto_ptr_char id(role ? dynamic_cast<EntityDescriptor*>(role->getParent())->getEntityID() : nullptr);
491             m_log.error("unable to locate compatible NIM service for provider (%s)", id.get() ? id.get() : "unknown");
492             MetadataException ex("Unable to locate endpoint at IdP ($entityID) to send ManageNameIDResponse.");
493             annotateException(&ex, role);   // throws it
494         }
495     }
496     else {
497         encoder = m_encoders.begin()->second.get();
498     }
499 
500     // Prepare response.
501     auto_ptr<ManageNameIDResponse> nim(ManageNameIDResponseBuilder::buildManageNameIDResponse());
502     nim->setInResponseTo(requestID);
503     if (ep) {
504         const XMLCh* loc = ep->getResponseLocation();
505         if (!loc || !*loc)
506             loc = ep->getLocation();
507         nim->setDestination(loc);
508     }
509     Issuer* issuer = IssuerBuilder::buildIssuer();
510     nim->setIssuer(issuer);
511     issuer->setName(application.getRelyingParty(role ? dynamic_cast<EntityDescriptor*>(role->getParent()) :
512             nullptr)->getXMLString("entityID").second);
513     fillStatus(*nim, code, subcode, msg);
514 
515     auto_ptr_char dest(nim->getDestination());
516 
517     long ret = sendMessage(*encoder, nim.get(), relayState, dest.get(), role, application, httpResponse, "conditional");
518     nim.release();  // freed by encoder
519     return make_pair(true, ret);
520 }
521 
522 #include "util/SPConstants.h"
523 #include <xmltooling/impl/AnyElement.h>
524 #include <xmltooling/soap/SOAP.h>
525 #include <xmltooling/soap/SOAPClient.h>
526 #include <xmltooling/soap/HTTPSOAPTransport.h>
527 using namespace soap11;
528 namespace {
529     static const XMLCh NameIDNotification[] =   UNICODE_LITERAL_18(N,a,m,e,I,D,N,o,t,i,f,i,c,a,t,i,o,n);
530 
531     class SHIBSP_DLLLOCAL SOAPNotifier : public soap11::SOAPClient
532     {
533     public:
SOAPNotifier()534         SOAPNotifier() {}
~SOAPNotifier()535         virtual ~SOAPNotifier() {}
536     private:
prepareTransport(SOAPTransport & transport)537         void prepareTransport(SOAPTransport& transport) {
538             transport.setVerifyHost(false);
539             HTTPSOAPTransport* http = dynamic_cast<HTTPSOAPTransport*>(&transport);
540             if (http) {
541                 http->useChunkedEncoding(false);
542                 http->setRequestHeader(PACKAGE_NAME, PACKAGE_VERSION);
543             }
544         }
545     };
546 };
547 
notifyBackChannel(const Application & application,const char * requestURL,const NameID & nameid,const NewID * newid) const548 bool SAML2NameIDMgmt::notifyBackChannel(
549     const Application& application, const char* requestURL, const NameID& nameid, const NewID* newid
550     ) const
551 {
552     unsigned int index = 0;
553     string endpoint = application.getNotificationURL(requestURL, false, index++);
554     if (endpoint.empty())
555         return true;
556 
557     scoped_ptr<Envelope> env(EnvelopeBuilder::buildEnvelope());
558     Body* body = BodyBuilder::buildBody();
559     env->setBody(body);
560     ElementProxy* msg = new AnyElementImpl(shibspconstants::SHIB2SPNOTIFY_NS, NameIDNotification);
561     body->getUnknownXMLObjects().push_back(msg);
562     msg->getUnknownXMLObjects().push_back(nameid.clone());
563     if (newid)
564         msg->getUnknownXMLObjects().push_back(newid->clone());
565     else
566         msg->getUnknownXMLObjects().push_back(TerminateBuilder::buildTerminate());
567 
568     bool result = true;
569     SOAPNotifier soaper;
570     while (!endpoint.empty()) {
571         try {
572             soaper.send(*env, SOAPTransport::Address(application.getId(), application.getId(), endpoint.c_str()));
573             delete soaper.receive();
574         }
575         catch (std::exception& ex) {
576             m_log.error("error notifying application of logout event: %s", ex.what());
577             result = false;
578         }
579         soaper.reset();
580         endpoint = application.getNotificationURL(requestURL, false, index++);
581     }
582     return result;
583 }
584 
585 #endif
586