]> err.no Git - sope/blob - sope-appserver/NGXmlRpc/NGXmlRpcClient.m
define some default defaults for the simple http parser (when the Defaults.plist...
[sope] / sope-appserver / NGXmlRpc / NGXmlRpcClient.m
1 /*
2   Copyright (C) 2000-2004 SKYRIX Software AG
3
4   This file is part of OpenGroupware.org.
5
6   OGo 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   OGo 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 OGo; 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 "NGXmlRpcClient.h"
23 #include "common.h"
24 #include <XmlRpc/XmlRpcMethodCall.h>
25 #include <XmlRpc/XmlRpcMethodResponse.h>
26 #include <NGObjWeb/WOHTTPConnection.h>
27 #include <NGObjWeb/WOResponse.h>
28 #include <NGObjWeb/WORequest.h>
29 #include <NGStreams/NGBufferedStream.h>
30 #include <NGStreams/NGActiveSocket.h>
31 #include <NGStreams/NGStreamExceptions.h>
32
33 @interface NSString(DigestInfo)
34 - (NSDictionary *)parseHTTPDigestInfo;
35 @end
36
37 @implementation NGXmlRpcClient
38
39 + (int)version {
40   return 2;
41 }
42
43 - (Class)connectionClass {
44   return [WOHTTPConnection class];
45 }
46 - (Class)requestClass {
47   return [WORequest class];
48 }
49
50 - (id)initWithHost:(NSString *)_h uri:(NSString *)_u port:(unsigned int)_port {
51   if ((self = [super init])) {
52     self->httpConnection = 
53       [[[self connectionClass] alloc] initWithHost:_h onPort:_port];
54     self->uri = [_u copy];
55   }
56   return self;
57 }
58
59 - (id)initWithHost:(NSString *)_host   // e.g. @"inster.in.skyrix.com"
60   uri:(NSString *)_uri    // e.g. @"skyxmlrpc.woa/xmlrpc"
61   port:(unsigned int)_port // e.g. 20000
62   userName:(NSString *)_userName
63   password:(NSString *)_password
64 {
65   if ((self = [self initWithHost:_host uri:_uri port:_port])) {
66     self->userName = [_userName copy];
67     self->password = [_password copy];
68   }
69   return self;
70 }
71 - (id)initWithURL:(id)_url {
72   NSURL *url;
73   
74   url = [_url isKindOfClass:[NSURL class]]
75     ? _url
76     : [NSURL URLWithString:[_url stringValue]];
77   if (url == nil) {
78     [self release];
79     return nil;
80   }
81
82   if ((self = [super init])) {
83     self->httpConnection = [[[self connectionClass] alloc] initWithURL:url];
84     
85     if ([[url scheme] hasPrefix:@"http"])
86       self->uri = [[url path] copy];
87     else
88       /* hack for easier XMLRPC-over-Unix-Domain-sockets */
89       self->uri = @"/RPC2";
90     self->userName = [[url user]     copy];
91     self->password = [[url password] copy];
92   }
93   return self;
94 }
95 - (id)initWithURL:(id)_url login:(NSString *)_login password:(NSString *)_pwd {
96   if ((self = [self initWithURL:_url])) {
97     if (_login) [self setUserName:_login];
98     if (_pwd)   [self setPassword:_pwd];
99   }
100   return self;
101 }
102
103 - (id)initWithRawAddress:(id)_address {
104   if (_address == nil) {
105     [self release];
106     return nil;
107   }
108   if ((self = [super init])) {
109     self->address = [_address retain];
110   }
111   return self;
112 }
113
114 - (void)dealloc {
115   [self->additionalHeaders release];
116   [self->address        release];
117   [self->httpConnection release];
118   [self->userName       release];
119   [self->password       release];
120   [self->uri            release];
121   [super dealloc];
122 }
123
124 /* accessors */
125
126 - (NSURL *)url {
127   NSString *p;
128   NSURL    *url;
129   
130   // TODO: not final yet ... (hh asks: bjoern, is this used anywhere anyway ?)
131   p = [[NSString alloc] initWithFormat:@"http://%@:%i%@",
132                   @"localhost",
133                   80,
134                   self->uri];
135   url = [NSURL URLWithString:p];
136   [p release];
137   return url;
138 }
139
140 - (void)setUserName:(NSString *)_userName {
141   ASSIGNCOPY(self->userName, _userName);
142 }
143 - (NSString *)userName {
144   return self->userName;
145 }
146 - (NSString *)login {
147   return self->userName;
148 }
149
150 - (void)setPassword:(NSString *)_password {
151   ASSIGNCOPY(self->password, _password);
152 }
153 - (NSString *)password {
154   return self->password;
155 }
156
157 - (void)setUri:(NSString *)_uri {
158   ASSIGNCOPY(self->uri, _uri);
159 }
160 - (NSString *)uri {
161   return self->uri;
162 }
163
164 - (void)setAdditionalHeaders:(NSDictionary *)_headers {
165   ASSIGNCOPY(self->additionalHeaders, _headers);
166 }
167 - (NSDictionary *)additionalHeaders {
168   return self->additionalHeaders;
169 }
170
171 /* performing the method */
172
173 - (id)invokeMethodNamed:(NSString *)_methodName {
174   return [self invokeMethodNamed:_methodName parameters:nil];
175 }
176
177 - (id)invokeMethodNamed:(NSString *)_methodName withParameter:(id)_param {
178   NSArray *params = nil;
179
180   if (_param)
181     params = [NSArray arrayWithObject:_param];
182                 
183   return [self invokeMethodNamed:_methodName parameters:params];
184 }
185
186 - (id)invoke:(NSString *)_methodName params:(id)firstObj,... {
187   id array, obj, *objects;
188   va_list list;
189   unsigned int count;
190   
191   va_start(list, firstObj);
192   for (count = 0, obj = firstObj; obj; obj = va_arg(list,id))
193     count++;
194   va_end(list);
195   
196   objects = calloc(count, sizeof(id));
197   {
198     va_start(list, firstObj);
199     for (count = 0, obj = firstObj; obj; obj = va_arg(list,id))
200       objects[count++] = obj;
201     va_end(list);
202
203     array = [NSArray arrayWithObjects:objects count:count];
204   }
205   free(objects);
206   
207   return [self invokeMethodNamed:_methodName parameters:array];
208 }
209
210 - (id)call:(NSString *)_methodName,... {
211   id array, obj, *objects;
212   va_list list;
213   unsigned int count;
214   
215   va_start(list, _methodName);
216   for (count = 0, obj = va_arg(list, id); obj; obj = va_arg(list,id))
217     count++;
218   va_end(list);
219   
220   objects = calloc(count, sizeof(id));
221   {
222     va_start(list, _methodName);
223     for (count = 0, obj = va_arg(list, id); obj; obj = va_arg(list,id))
224       objects[count++] = obj;
225     va_end(list);
226     
227     array = [NSArray arrayWithObjects:objects count:count];
228   }
229   free(objects);
230   return [self invokeMethodNamed:_methodName parameters:array];
231 }
232
233 - (NSString *)_authorization {
234   NSString *tmp = nil;
235   
236   if (self->userName == nil)
237     return nil;
238   
239   if (self->digestInfo) {
240     [self logWithFormat:@"need to construct digest authentication using %@", 
241             self->digestInfo];
242     return nil;
243   }
244
245   tmp = @"";
246   tmp = [tmp stringByAppendingString:self->userName];
247   tmp = [tmp stringByAppendingString:@":"];
248   
249   if (self->password)
250     tmp = [tmp stringByAppendingString:self->password];
251   
252   if (tmp != nil) {
253     tmp = [tmp stringByEncodingBase64];
254     tmp = [@"Basic " stringByAppendingString:tmp];
255   }
256   return tmp;
257 }
258
259 - (id)sendFailed:(NSException *)e {
260   if (e)
261     return e;
262   else {
263     return [NSException exceptionWithName:@"XmlRpcSendFailed"
264                         reason:
265                           @"unknown reason, no exception set in "
266                           @"http-connection"
267                         userInfo:nil];
268   }
269 }
270
271 - (id)callFailed:(WOResponse *)_response {
272   NSException  *exc;
273   NSString     *r;
274   NSDictionary *ui;
275   
276 #if 0
277   NSLog(@"%s: XML-RPC response status: %i", __PRETTY_FUNCTION__,
278         [_response status]);
279 #endif
280   
281   /* construct exception */
282   
283   r = [NSString stringWithFormat:@"call failed with HTTP status code %i",
284                   [_response status]];
285   
286   ui = [NSDictionary dictionaryWithObjectsAndKeys:
287                        self,      @"NGXmlRpcClient",
288                        _response, @"WOResponse",
289                        [NSNumber numberWithInt:[_response status]],
290                        @"HTTPStatusCode",
291                        nil];
292   
293   exc = [NSException exceptionWithName:@"XmlRpcCallFailed"
294                      reason:r
295                      userInfo:ui];
296   return exc;
297 }
298 - (id)invalidXmlRpcResponse:(WOResponse *)_response {
299   return [NSException exceptionWithName:@"XmlRpcCallFailed"
300                       reason:@"got malformed XML-RPC response?!"
301                       userInfo:nil];
302 }
303
304 - (id)processHTMLResponse:(WOResponse *)_response {
305   NSDictionary *ui;
306   
307   if (_response == nil) return nil;
308   [self debugWithFormat:@"Note: got HTML response: %@", _response];
309
310   ui = [NSDictionary dictionaryWithObjectsAndKeys:
311                        _response, @"response",
312                      nil];
313   return [NSException exceptionWithName:@"XmlRpcCallFailed"
314                       reason:@"got HTML response"
315                       userInfo:ui];
316 }
317
318 - (id)doCallViaHTTP:(XmlRpcMethodCall *)_call {
319   XmlRpcMethodResponse *methodResponse;
320   WOResponse           *response;
321   WORequest            *request;
322   NSString             *authorization, *ctype;
323
324   request = [[[self requestClass] alloc] initWithMethod:@"POST"
325                                uri:self->uri
326                                httpVersion:@"HTTP/1.0"
327                                headers:self->additionalHeaders
328                                content:nil
329                                userInfo:nil];
330   [request setHeader:@"text/xml" forKey:@"content-type"];
331   [request setContentEncoding:NSUTF8StringEncoding];
332   [request appendContentString:[_call xmlRpcString]];
333   request = [request autorelease];
334   
335   if ((authorization = [self _authorization]) != nil)
336     [request setHeader:authorization forKey:@"Authorization"];
337   
338   if (![self->httpConnection sendRequest:request])
339     return [self sendFailed:[self->httpConnection lastException]];
340   
341   response = [self->httpConnection readResponse];
342   
343   [self->digestInfo release]; self->digestInfo = nil;
344   
345   if ([response status] != 200) {
346     if ([response status] == 401 /* authentication required */) {
347       /* process info required for digest authentication */
348       NSString *wwwauth;
349       
350       wwwauth = [response headerForKey:@"www-authenticate"];
351       if ([[wwwauth lowercaseString] hasPrefix:@"digest"]) {
352         self->digestInfo = [[wwwauth parseHTTPDigestInfo] retain];
353         //[self debugWithFormat:@"got HTTP digest info: %@", self->digestInfo];
354       }
355     }
356     
357     return [self callFailed:response];
358   }
359
360   if ((ctype = [response headerForKey:@"content-type"]) == nil)
361     ctype = @"text/xml"; // TODO, does it make sense? For simplistic servers?
362   
363   if ([ctype hasPrefix:@"text/html"])
364     return [self processHTMLResponse:response];
365   
366   methodResponse = 
367     [[XmlRpcMethodResponse alloc] initWithXmlRpcString:
368         [response contentAsString]];
369   if (methodResponse == nil)
370     return [self invalidXmlRpcResponse:response];
371   
372   return [methodResponse autorelease];
373 }
374
375 - (id)doRawCall:(XmlRpcMethodCall *)_call {
376   XmlRpcMethodResponse *methodResponse;
377   NGActiveSocket   *socket;
378   NGBufferedStream *io;
379   NSString *s;
380   NSData   *rq;
381
382   /* get body for XML-RPC request */
383   
384   if ((s = [_call xmlRpcString]) == nil)
385     return nil;
386   if ((rq = [s dataUsingEncoding:NSUTF8StringEncoding]) == nil)
387     return nil;
388   
389   /* connect */
390   
391   // TODO: add timeout values
392   socket = [NGActiveSocket socketConnectedToAddress:self->address];
393   if (socket == nil) {
394     [self logWithFormat:@"could not connect %@", self->address];
395     return [self sendFailed:nil];
396   }
397   io = [NGBufferedStream filterWithSource:socket bufferSize:4096];
398   
399   /* write body + \r\n\r\n */
400   
401   if (![io writeData:rq])
402     return [self sendFailed:[io lastException]];
403   if (![io safeWriteBytes:"\r\n\r\n" count:4])
404     return [self sendFailed:[io lastException]];
405   if (![io flush])
406     return [self sendFailed:[io lastException]];
407   
408   /* read response */
409   
410   {
411     NSMutableData *data;
412     NSString *s;
413     
414     data = [NSMutableData dataWithCapacity:1024];
415     do {
416       unsigned readCount;
417       unsigned char buf[1024 + 10];
418       
419       readCount = [io readBytes:&buf count:1024];
420       if (readCount == NGStreamError) {
421         NSException *e;
422         
423         if ((e = [io lastException]) == nil)
424           break;
425         else if ([e isKindOfClass:[NGEndOfStreamException class]])
426           break;
427         else
428           /* an error */
429           return [self sendFailed:e];
430       }
431       buf[readCount] = '\0';
432       
433       [data appendBytes:buf length:readCount];
434     }
435     while (YES);
436     
437     s = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
438     methodResponse = [[XmlRpcMethodResponse alloc] initWithXmlRpcString:s];
439     [s release];
440   }
441   
442   [io close];
443   
444   return [methodResponse autorelease];
445 }
446
447 - (id)invokeMethodNamed:(NSString *)_methodName parameters:(NSArray *)_params {
448   XmlRpcMethodCall *methodCall;
449   id result;
450   
451   methodCall = [[XmlRpcMethodCall alloc] initWithMethodName:_methodName
452                                          parameters:_params];
453   
454   if (self->httpConnection)
455     result = [self doCallViaHTTP:methodCall];
456   else
457     result = [self doRawCall:methodCall];
458   
459   [methodCall release]; methodCall = nil;
460   
461   if ([result isKindOfClass:[XmlRpcMethodResponse class]])
462     result = [result result];
463   
464   if (result == nil)
465     [self logWithFormat:@"got nil value from XML-RPC ..."];
466   return result;
467 }
468
469 @end /* NGXmlRpcClient */
470
471 @implementation NSString(DigestInfo)
472
473 - (NSDictionary *)parseHTTPDigestInfo {
474   /*
475     eg: 
476       www-authenticate: Digest realm="RCD", \
477         nonce="1572920321042107679", \
478         qop="auth,auth-int", \
479         algorithm="MD5,MD5-sess"
480   */
481   NSMutableDictionary *md;
482   NSEnumerator *parts;
483   NSString *part;
484   
485   md = [NSMutableDictionary dictionaryWithCapacity:8];
486   
487   /* 
488      TODO: fix this parser, it only works if the components of the header
489      value are separated using ", " and the component *values* are separated
490      by a "," (not followed by a space).
491      Works with rcd, probably with nothing else ...
492   */
493   parts = [[self componentsSeparatedByString:@", "] objectEnumerator];
494   
495   while ((part = [parts nextObject])) {
496     NSRange  r;
497     NSString *key, *value;
498     
499     r = [part rangeOfString:@"="];
500     if (r.length == 0) continue;
501     
502     key   = [[part substringToIndex:r.location] stringByTrimmingSpaces];
503     value = [[part substringFromIndex:(r.location + r.length)] 
504                    stringByTrimmingSpaces];
505
506     //[self logWithFormat:@"key '%@' value '%@'", key, value];
507     
508     if ([value hasPrefix:@"\""] && [value hasSuffix:@"\""]) {
509       r.location = 1;
510       r.length   = [value length] - 2;
511       value = [value substringWithRange:r];
512     }
513     //[self logWithFormat:@"key '%@' value '%@'", key, value];
514     
515     [md setObject:value forKey:[key lowercaseString]];
516   }
517   return md;
518 }
519
520 @end /* NSString(DigestInfo) */