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