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