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