1/* 2 Copyright (C) 2004-2005 SKYRIX Software AG 3 4 This file is part of SOPE. 5 6 SOPE is free software; you can redistribute it and/or modify it under 7 the terms of the GNU Lesser General Public License as published by the 8 Free Software Foundation; either version 2, or (at your option) any 9 later version. 10 11 SOPE is distributed in the hope that it will be useful, but WITHOUT ANY 12 WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 14 License for more details. 15 16 You should have received a copy of the GNU Lesser General Public 17 License along with SOPE; see the file COPYING. If not, write to the 18 Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA 19 02111-1307, USA. 20*/ 21 22#include "SoObjectSOAPDispatcher.h" 23#include "SoObject.h" 24#include "NSException+HTTP.h" 25#include "WOContext+SoObjects.h" 26#include "SoDefaultRenderer.h" 27#include <NGObjWeb/WOActionResults.h> 28#include <NGObjWeb/WOContext.h> 29#include <NGObjWeb/WOResponse.h> 30#include <NGObjWeb/WORequest.h> 31#include "common.h" 32#include <DOM/DOM.h> 33#include <SaxObjC/XMLNamespaces.h> 34 35/* 36 TODO: is it required by SOAP that the HTTP method is POST? 37 38 Note: 39 Servers also set a SOAPAction HTTP header. 40 41 SOAP sample: 42 <?xml version="1.0" encoding="UTF-8" standalone="no"?> 43 <SOAP-ENV:Envelope 44 xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" 45 xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" 46 xmlns:xsd="http://www.w3.org/1999/XMLSchema" 47 xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" 48 > 49 <SOAP-ENV:Body 50 xmlns:types="http://schemas.novell.com/2003/10/NCSP/types.xsd" 51 SOAP-ENV:encodingStyle="" 52 > 53 <loginRequest> 54 <types:auth xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 55 xsi:type="types:PlainText" 56 > 57 <types:username>dummy</types:username> 58 <types:password>user</types:password> 59 </types:auth> 60 </loginRequest> 61 </SOAP-ENV:Body> 62 </SOAP-ENV:Envelope> 63 64 Another (http://novell.com/simias/domain/GetDomainID): 65 <?xml version="1.0" encoding="utf-8"?> 66 <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" 67 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 68 xmlns:xsd="http://www.w3.org/2001/XMLSchema"> 69 <soap:Body> 70 <GetDomainID xmlns="http://novell.com/simias/domain" /> 71 </soap:Body> 72 </soap:Envelope> 73*/ 74 75@interface SoSOAPRenderer : SoDefaultRenderer 76@end 77 78@implementation SoObjectSOAPDispatcher 79 80static BOOL debugOn = NO; 81static BOOL debugParsing = NO; 82 83+ (void)initialize { 84 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; 85 86 debugOn = [ud boolForKey:@"SoObjectSOAPDispatcherDebugEnabled"]; 87 if (debugOn) NSLog(@"Note: SOPE SOAP dispatcher debugging turned on."); 88 89 debugParsing = [ud boolForKey:@"SoObjectSOAPDispatcherParserDebugEnabled"]; 90 if (debugParsing) NSLog(@"Note: SOPE SOAP parsing debugging turned on."); 91} 92 93/* XML actions */ 94 95- (id)performSOAPAction:(NSString *)_actionName 96 header:(id<DOMElement>)_header body:(id<DOMElement>)_body 97 inContext:(WOContext *)_ctx 98{ 99 id clientObject; 100 id methodObject; 101 id resultObject; 102 103 if (debugOn) 104 [self debugWithFormat:@"calling SOAP method: '%@'", _actionName]; 105 106 /* find client object */ 107 108 if ((clientObject = [_ctx clientObject]) != nil) { 109 if (debugOn) 110 [self debugWithFormat:@" client object from ctx: %@", clientObject]; 111 } 112 else if ((clientObject = [self->object clientObject])) { 113 if (debugOn) 114 [self debugWithFormat:@" setting client object: %@", clientObject]; 115 [_ctx setClientObject:clientObject]; 116 } 117 118 /* find callable (method) object */ 119 120 // TODO: should we allow acquisition? 121 methodObject = [clientObject lookupName:_actionName inContext:_ctx 122 acquire:NO]; 123 if (methodObject == nil) { 124 /* check for common names like "GetFolderRequest" => "GetFolder" */ 125 if ([_actionName hasSuffix:@"Request"]) { 126 NSString *an; 127 128 an = [_actionName substringToIndex:([_actionName length] - 7)]; 129 if (debugOn) [self debugWithFormat:@" try special name: %@", an]; 130 methodObject = [clientObject lookupName:an inContext:_ctx acquire:NO]; 131 if (methodObject != nil) _actionName = an; 132 } 133 } 134 if (methodObject == nil) { 135 /* check for names like "http://novell.com/domain/GetID" => "GetID" */ 136 NSRange r; 137 138 r = [_actionName rangeOfString:@"/" options:NSBackwardsSearch]; 139 if (r.length > 0) { 140 NSString *an; 141 142 an = [_actionName substringFromIndex:(r.location + r.length)]; 143 if (debugOn) [self debugWithFormat:@" try special name: %@", an]; 144 145 methodObject = [clientObject lookupName:an inContext:_ctx acquire:NO]; 146 if (methodObject != nil) _actionName = an; 147 } 148 } 149 150 if (methodObject == nil) { 151 [self warnWithFormat:@"could not locate SOAP method: %@", 152 _actionName]; 153 return [NSException exceptionWithHTTPStatus:501 /* not implemented */ 154 reason:@"did not find the specified SOAP method"]; 155 } 156 else if (![methodObject isCallable]) { 157 [self warnWithFormat: 158 @"object found for SOAP method '%@' is not callable: " 159 @"%@", _actionName, methodObject]; 160 return [NSException exceptionWithHTTPStatus:501 /* not implemented */ 161 reason:@"did not find the specified SOAP method"]; 162 } 163 if (debugOn) [self debugWithFormat:@" method: %@", methodObject]; 164 165 /* apply arguments */ 166 167 // TODO: use some query syntax in product.plist to retrieve parameters 168 // from SOAP 169 170 // TODO: somehow apply SOPE header/body? 171 if (_header) [_ctx setObject:_header forKey:@"SOAPHeader"]; 172 if (_body) [_ctx setObject:_body forKey:@"SOAPBody"]; 173 174 if ([methodObject respondsToSelector: 175 @selector(takeValuesFromRequest:inContext:)]) { 176 if (debugOn) 177 [self debugWithFormat:@" applying values from request ..."]; 178 [methodObject takeValuesFromRequest:[_ctx request] inContext:_ctx]; 179 } 180 181 /* perform call */ 182 183 resultObject = [methodObject callOnObject:[_ctx clientObject] 184 inContext:_ctx]; 185 if (debugOn) [self debugWithFormat:@"got SOAP result: %@", resultObject]; 186 return resultObject; 187} 188 189- (id)performSOAPAction:(NSString *)_actionName document:(id)_dom 190 inContext:(WOContext *)_ctx 191{ 192 id<DOMElement> envelope; 193 id<DOMElement> header; 194 id<DOMElement> body; 195 id<DOMNodeList> list; 196 197 /* envelope */ 198 199 envelope = [_dom documentElement]; 200 if (![[envelope tagName] isEqualToString:@"Envelope"] || 201 ![[envelope namespaceURI] isEqualToString:XMLNS_SOAP_ENVELOPE]) { 202 [self debugWithFormat:@"Note: missing SOAP envelope at document root."]; 203 return [NSException exceptionWithHTTPStatus:400 /* bad request */ 204 reason:@"could not parse SOAP content of request"]; 205 } 206 if (debugParsing) [self debugWithFormat:@"envelope: %@", envelope]; 207 208 [_ctx setObject:envelope forKey:@"SOAPEnvelope"]; 209 210 /* header */ 211 212 list = [envelope getElementsByTagName:@"Header"]; 213 // TODO: not yet supported by DOMElement: namespaceURI:XMLNS_SOAP_ENVELOPE]; 214 if ([list length] > 1) { 215 [self warnWithFormat:@"multiple SOAP headers in request?! (using first)"]; 216 } 217 header = [list length] > 0 ? [list objectAtIndex:0] : nil; 218 if (debugParsing) [self debugWithFormat:@"header: %@", header]; 219 220 /* body */ 221 222 list = [envelope getElementsByTagName:@"Body"]; 223 // TODO: not yet supported by DOMElement: namespaceURI:XMLNS_SOAP_ENVELOPE]; 224 if ([list length] == 0) { 225 [self debugWithFormat:@"Note: missing SOAP body."]; 226 return [NSException exceptionWithHTTPStatus:400 /* bad request */ 227 reason:@"could not parse SOAP body of request"]; 228 } 229 else if ([list length] > 1) { 230 [self warnWithFormat:@"multiple SOAP bodies in request?! (using first)"]; 231 } 232 body = [list objectAtIndex:0]; 233 if (debugParsing) [self debugWithFormat:@"body: %@", body]; 234 235 /* process */ 236 237 return [self performSOAPAction:_actionName 238 header:header body:body inContext:_ctx]; 239} 240 241/* main dispatcher */ 242 243- (id)dispatchInContext:(WOContext *)_ctx { 244 NSAutoreleasePool *pool; 245 WORequest *rq; 246 NSString *SOAPAction; 247 id<DOMDocument> dom; 248 id resultObject; 249 250 pool = [[NSAutoreleasePool alloc] init]; 251 252 if ((rq = [_ctx request]) == nil) { 253 [self errorWithFormat:@"missing request in context!"]; 254 return nil; 255 } 256 257 /* 258 Note: the SOAPAction is also contained in the body which is probably 259 considered the authority? We currently prefer the header when 260 available. 261 */ 262 SOAPAction = [rq headerForKey:@"soapaction"]; 263 if ([SOAPAction length] > 1) { 264 265 if ([SOAPAction characterAtIndex:0] == '"' && 266 [SOAPAction characterAtIndex:([SOAPAction length] - 1)] == '"') { 267 /* a quoted header, like "http://novell.com/simias/domain/GetDomainID" */ 268 NSRange r; 269 270 r.location = 1; 271 r.length = [SOAPAction length] - 2; 272 SOAPAction = [SOAPAction substringWithRange:r]; 273 } 274 } 275 if (![SOAPAction isNotEmpty]) { 276 [self errorWithFormat:@"missing SOAPAction HTTP header!"]; 277 return nil; 278 } 279 280 /* parse XML */ 281 282 if ((dom = [rq contentAsDOMDocument]) == nil) { 283 [self debugWithFormat:@"Note: could not parse XML content of request"]; 284 return [NSException exceptionWithHTTPStatus:400 /* bad request */ 285 reason:@"could not parse XML content of request"]; 286 } 287 288 resultObject = 289 [[self performSOAPAction:SOAPAction document:dom inContext:_ctx]retain]; 290 [pool release]; 291 292 return [resultObject autorelease]; 293} 294 295/* debugging */ 296 297- (NSString *)loggingPrefix { 298 return @"[obj-soap-dispatch]"; 299} 300- (BOOL)isDebuggingEnabled { 301 return debugOn; 302} 303 304@end /* SoObjectSOAPDispatcher */ 305 306@implementation SoSOAPRenderer 307 308// TODO: render exceptions as SOAP faults 309// TODO: maybe support rendering of DOM trees? (should be supported by default) 310// TODO: maybe some "schema" driven rendering 311 312@end /* SoSOAPRenderer */ 313