2 Copyright (C) 2000-2004 SKYRIX Software AG
4 This file is part of OpenGroupware.org.
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
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.
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
23 #include "NGXmlRpcClient.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>
34 @interface NSString(DigestInfo)
35 - (NSDictionary *)parseHTTPDigestInfo;
38 @implementation NGXmlRpcClient
44 - (Class)connectionClass {
45 return [WOHTTPConnection class];
47 - (Class)requestClass {
48 return [WORequest class];
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];
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
66 if ((self = [self initWithHost:_host uri:_uri port:_port])) {
67 self->userName = [_userName copy];
68 self->password = [_password copy];
72 - (id)initWithURL:(id)_url {
75 url = [_url isKindOfClass:[NSURL class]]
77 : [NSURL URLWithString:[_url stringValue]];
83 if ((self = [super init])) {
84 self->httpConnection = [[[self connectionClass] alloc] initWithURL:url];
86 if ([[url scheme] hasPrefix:@"http"])
87 self->uri = [[url path] copy];
89 /* hack for easier XMLRPC-over-Unix-Domain-sockets */
91 self->userName = [[url user] copy];
92 self->password = [[url password] copy];
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];
104 - (id)initWithRawAddress:(id)_address {
105 if (_address == nil) {
109 if ((self = [super init])) {
110 self->address = [_address retain];
116 [self->additionalHeaders release];
117 [self->address release];
118 [self->httpConnection release];
119 [self->userName release];
120 [self->password release];
131 // TODO: not final yet ... (hh asks: bjoern, is this used anywhere anyway ?)
132 p = [[NSString alloc] initWithFormat:@"http://%@:%i%@",
136 url = [NSURL URLWithString:p];
141 - (void)setUserName:(NSString *)_userName {
142 ASSIGNCOPY(self->userName, _userName);
144 - (NSString *)userName {
145 return self->userName;
147 - (NSString *)login {
148 return self->userName;
151 - (void)setPassword:(NSString *)_password {
152 ASSIGNCOPY(self->password, _password);
154 - (NSString *)password {
155 return self->password;
158 - (void)setUri:(NSString *)_uri {
159 ASSIGNCOPY(self->uri, _uri);
165 - (void)setAdditionalHeaders:(NSDictionary *)_headers {
166 ASSIGNCOPY(self->additionalHeaders, _headers);
168 - (NSDictionary *)additionalHeaders {
169 return self->additionalHeaders;
172 /* performing the method */
174 - (id)invokeMethodNamed:(NSString *)_methodName {
175 return [self invokeMethodNamed:_methodName parameters:nil];
178 - (id)invokeMethodNamed:(NSString *)_methodName withParameter:(id)_param {
179 NSArray *params = nil;
182 params = [NSArray arrayWithObject:_param];
184 return [self invokeMethodNamed:_methodName parameters:params];
187 - (id)invoke:(NSString *)_methodName params:(id)firstObj,... {
188 id array, obj, *objects;
192 va_start(list, firstObj);
193 for (count = 0, obj = firstObj; obj; obj = va_arg(list,id))
197 objects = calloc(count, sizeof(id));
199 va_start(list, firstObj);
200 for (count = 0, obj = firstObj; obj; obj = va_arg(list,id))
201 objects[count++] = obj;
204 array = [NSArray arrayWithObjects:objects count:count];
208 return [self invokeMethodNamed:_methodName parameters:array];
211 - (id)call:(NSString *)_methodName,... {
212 id array, obj, *objects;
216 va_start(list, _methodName);
217 for (count = 0, obj = va_arg(list, id); obj; obj = va_arg(list,id))
221 objects = calloc(count, sizeof(id));
223 va_start(list, _methodName);
224 for (count = 0, obj = va_arg(list, id); obj; obj = va_arg(list,id))
225 objects[count++] = obj;
228 array = [NSArray arrayWithObjects:objects count:count];
231 return [self invokeMethodNamed:_methodName parameters:array];
234 - (NSString *)_authorization {
237 if (self->userName == nil)
240 if (self->digestInfo) {
241 [self logWithFormat:@"need to construct digest authentication using %@",
247 tmp = [tmp stringByAppendingString:self->userName];
248 tmp = [tmp stringByAppendingString:@":"];
251 tmp = [tmp stringByAppendingString:self->password];
254 tmp = [tmp stringByEncodingBase64];
255 tmp = [@"Basic " stringByAppendingString:tmp];
260 - (id)sendFailed:(NSException *)e {
264 return [NSException exceptionWithName:@"XmlRpcSendFailed"
266 @"unknown reason, no exception set in "
272 - (id)callFailed:(WOResponse *)_response {
278 NSLog(@"%s: XML-RPC response status: %i", __PRETTY_FUNCTION__,
282 /* construct exception */
284 r = [NSString stringWithFormat:@"call failed with HTTP status code %i",
287 ui = [NSDictionary dictionaryWithObjectsAndKeys:
288 self, @"NGXmlRpcClient",
289 _response, @"WOResponse",
290 [NSNumber numberWithInt:[_response status]],
294 exc = [NSException exceptionWithName:@"XmlRpcCallFailed"
299 - (id)invalidXmlRpcResponse:(WOResponse *)_response {
300 return [NSException exceptionWithName:@"XmlRpcCallFailed"
301 reason:@"got malformed XML-RPC response?!"
305 - (id)processHTMLResponse:(WOResponse *)_response {
308 if (_response == nil) return nil;
309 [self debugWithFormat:@"Note: got HTML response: %@", _response];
311 ui = [NSDictionary dictionaryWithObjectsAndKeys:
312 _response, @"response",
314 return [NSException exceptionWithName:@"XmlRpcCallFailed"
315 reason:@"got HTML response"
319 - (id)doCallViaHTTP:(XmlRpcMethodCall *)_call {
320 XmlRpcMethodResponse *methodResponse;
321 WOResponse *response;
323 NSString *authorization, *ctype;
325 request = [[[self requestClass] alloc] initWithMethod:@"POST"
327 httpVersion:@"HTTP/1.0"
328 headers:self->additionalHeaders
331 [request setHeader:@"text/xml" forKey:@"content-type"];
332 [request setContentEncoding:NSUTF8StringEncoding];
333 [request appendContentString:[_call xmlRpcString]];
334 request = [request autorelease];
336 if ((authorization = [self _authorization]) != nil)
337 [request setHeader:authorization forKey:@"Authorization"];
339 if (![self->httpConnection sendRequest:request])
340 return [self sendFailed:[self->httpConnection lastException]];
342 response = [self->httpConnection readResponse];
344 [self->digestInfo release]; self->digestInfo = nil;
346 if ([response status] != 200) {
347 if ([response status] == 401 /* authentication required */) {
348 /* process info required for digest authentication */
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];
358 return [self callFailed:response];
361 if ((ctype = [response headerForKey:@"content-type"]) == nil)
362 ctype = @"text/xml"; // TODO, does it make sense? For simplistic servers?
364 if ([ctype hasPrefix:@"text/html"])
365 return [self processHTMLResponse:response];
368 [[XmlRpcMethodResponse alloc] initWithXmlRpcString:
369 [response contentAsString]];
370 if (methodResponse == nil)
371 return [self invalidXmlRpcResponse:response];
373 return [methodResponse autorelease];
376 - (id)doRawCall:(XmlRpcMethodCall *)_call {
377 XmlRpcMethodResponse *methodResponse;
378 NGActiveSocket *socket;
379 NGBufferedStream *io;
383 /* get body for XML-RPC request */
385 if ((s = [_call xmlRpcString]) == nil)
387 if ((rq = [s dataUsingEncoding:NSUTF8StringEncoding]) == nil)
392 // TODO: add timeout values
393 socket = [NGActiveSocket socketConnectedToAddress:self->address];
395 [self logWithFormat:@"could not connect %@", self->address];
396 return [self sendFailed:nil];
398 io = [NGBufferedStream filterWithSource:socket bufferSize:4096];
400 /* write body + \r\n\r\n */
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]];
407 return [self sendFailed:[io lastException]];
415 data = [NSMutableData dataWithCapacity:1024];
418 unsigned char buf[1024 + 10];
420 readCount = [io readBytes:&buf count:1024];
421 if (readCount == NGStreamError) {
424 if ((e = [io lastException]) == nil)
426 else if ([e isKindOfClass:[NGEndOfStreamException class]])
430 return [self sendFailed:e];
432 buf[readCount] = '\0';
434 [data appendBytes:buf length:readCount];
438 s = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
439 methodResponse = [[XmlRpcMethodResponse alloc] initWithXmlRpcString:s];
445 return [methodResponse autorelease];
448 - (id)invokeMethodNamed:(NSString *)_methodName parameters:(NSArray *)_params {
449 XmlRpcMethodCall *methodCall;
452 methodCall = [[XmlRpcMethodCall alloc] initWithMethodName:_methodName
455 if (self->httpConnection)
456 result = [self doCallViaHTTP:methodCall];
458 result = [self doRawCall:methodCall];
460 [methodCall release]; methodCall = nil;
462 if ([result isKindOfClass:[XmlRpcMethodResponse class]])
463 result = [result result];
466 [self logWithFormat:@"got nil value from XML-RPC ..."];
470 @end /* NGXmlRpcClient */
472 @implementation NSString(DigestInfo)
474 - (NSDictionary *)parseHTTPDigestInfo {
477 www-authenticate: Digest realm="RCD", \
478 nonce="1572920321042107679", \
479 qop="auth,auth-int", \
480 algorithm="MD5,MD5-sess"
482 NSMutableDictionary *md;
486 md = [NSMutableDictionary dictionaryWithCapacity:8];
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 ...
494 parts = [[self componentsSeparatedByString:@", "] objectEnumerator];
496 while ((part = [parts nextObject])) {
498 NSString *key, *value;
500 r = [part rangeOfString:@"="];
501 if (r.length == 0) continue;
503 key = [[part substringToIndex:r.location] stringByTrimmingSpaces];
504 value = [[part substringFromIndex:(r.location + r.length)]
505 stringByTrimmingSpaces];
507 //[self logWithFormat:@"key '%@' value '%@'", key, value];
509 if ([value hasPrefix:@"\""] && [value hasSuffix:@"\""]) {
511 r.length = [value length] - 2;
512 value = [value substringWithRange:r];
514 //[self logWithFormat:@"key '%@' value '%@'", key, value];
516 [md setObject:value forKey:[key lowercaseString]];
521 @end /* NSString(DigestInfo) */