2 Copyright (C) 2000-2005 SKYRIX Software AG
4 This file is part of SOPE.
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
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.
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
22 #include "NGXmlRpcClient.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>
33 @interface NSString(DigestInfo)
34 - (NSDictionary *)parseHTTPDigestInfo;
37 @implementation NGXmlRpcClient
43 - (Class)connectionClass {
44 return [WOHTTPConnection class];
46 - (Class)requestClass {
47 return [WORequest class];
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];
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
65 if ((self = [self initWithHost:_host uri:_uri port:_port])) {
66 self->userName = [_userName copy];
67 self->password = [_password copy];
71 - (id)initWithURL:(id)_url {
74 url = [_url isKindOfClass:[NSURL class]]
76 : [NSURL URLWithString:[_url stringValue]];
82 if ((self = [super init])) {
83 self->httpConnection = [[[self connectionClass] alloc] initWithURL:url];
85 if ([[url scheme] hasPrefix:@"http"])
86 self->uri = [[url path] copy];
88 /* hack for easier XMLRPC-over-Unix-Domain-sockets */
90 self->userName = [[url user] copy];
91 self->password = [[url password] copy];
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];
103 - (id)initWithRawAddress:(id)_address {
104 if (_address == nil) {
108 if ((self = [super init])) {
109 self->address = [_address retain];
115 [self->additionalHeaders release];
116 [self->address release];
117 [self->httpConnection release];
118 [self->userName release];
119 [self->password release];
130 // TODO: not final yet ... (hh asks: bjoern, is this used anywhere anyway ?)
131 p = [[NSString alloc] initWithFormat:@"http://%@:%i%@",
135 url = [NSURL URLWithString:p];
140 - (void)setUserName:(NSString *)_userName {
141 ASSIGNCOPY(self->userName, _userName);
143 - (NSString *)userName {
144 return self->userName;
146 - (NSString *)login {
147 return self->userName;
150 - (void)setPassword:(NSString *)_password {
151 ASSIGNCOPY(self->password, _password);
153 - (NSString *)password {
154 return self->password;
157 - (void)setUri:(NSString *)_uri {
158 ASSIGNCOPY(self->uri, _uri);
164 - (void)setAdditionalHeaders:(NSDictionary *)_headers {
165 ASSIGNCOPY(self->additionalHeaders, _headers);
167 - (NSDictionary *)additionalHeaders {
168 return self->additionalHeaders;
171 /* performing the method */
173 - (id)invokeMethodNamed:(NSString *)_methodName {
174 return [self invokeMethodNamed:_methodName parameters:nil];
177 - (id)invokeMethodNamed:(NSString *)_methodName withParameter:(id)_param {
178 NSArray *params = nil;
181 params = [NSArray arrayWithObject:_param];
183 return [self invokeMethodNamed:_methodName parameters:params];
186 - (id)invoke:(NSString *)_methodName params:(id)firstObj,... {
187 id array, obj, *objects;
191 va_start(list, firstObj);
192 for (count = 0, obj = firstObj; obj; obj = va_arg(list,id))
196 objects = calloc(count, sizeof(id));
198 va_start(list, firstObj);
199 for (count = 0, obj = firstObj; obj; obj = va_arg(list,id))
200 objects[count++] = obj;
203 array = [NSArray arrayWithObjects:objects count:count];
207 return [self invokeMethodNamed:_methodName parameters:array];
210 - (id)call:(NSString *)_methodName,... {
211 id array, obj, *objects;
215 va_start(list, _methodName);
216 for (count = 0, obj = va_arg(list, id); obj; obj = va_arg(list,id))
220 objects = calloc(count, sizeof(id));
222 va_start(list, _methodName);
223 for (count = 0, obj = va_arg(list, id); obj; obj = va_arg(list,id))
224 objects[count++] = obj;
227 array = [NSArray arrayWithObjects:objects count:count];
230 return [self invokeMethodNamed:_methodName parameters:array];
233 - (NSString *)_authorization {
236 if (self->userName == nil)
239 if (self->digestInfo) {
240 [self logWithFormat:@"need to construct digest authentication using %@",
246 tmp = [tmp stringByAppendingString:self->userName];
247 tmp = [tmp stringByAppendingString:@":"];
250 tmp = [tmp stringByAppendingString:self->password];
253 tmp = [tmp stringByEncodingBase64];
254 tmp = [@"Basic " stringByAppendingString:tmp];
259 - (id)sendFailed:(NSException *)e {
263 return [NSException exceptionWithName:@"XmlRpcSendFailed"
265 @"unknown reason, no exception set in "
271 - (id)callFailed:(WOResponse *)_response {
277 NSLog(@"%s: XML-RPC response status: %i", __PRETTY_FUNCTION__,
281 /* construct exception */
283 r = [NSString stringWithFormat:@"call failed with HTTP status code %i",
286 ui = [NSDictionary dictionaryWithObjectsAndKeys:
287 self, @"NGXmlRpcClient",
288 _response, @"WOResponse",
289 [NSNumber numberWithInt:[_response status]],
293 exc = [NSException exceptionWithName:@"XmlRpcCallFailed"
298 - (id)invalidXmlRpcResponse:(WOResponse *)_response {
299 return [NSException exceptionWithName:@"XmlRpcCallFailed"
300 reason:@"got malformed XML-RPC response?!"
304 - (id)processHTMLResponse:(WOResponse *)_response {
307 if (_response == nil) return nil;
308 [self debugWithFormat:@"Note: got HTML response: %@", _response];
310 ui = [NSDictionary dictionaryWithObjectsAndKeys:
311 _response, @"response",
313 return [NSException exceptionWithName:@"XmlRpcCallFailed"
314 reason:@"got HTML response"
318 - (id)doCallViaHTTP:(XmlRpcMethodCall *)_call {
319 XmlRpcMethodResponse *methodResponse;
320 WOResponse *response;
322 NSString *authorization, *ctype;
324 request = [[[self requestClass] alloc] initWithMethod:@"POST"
326 httpVersion:@"HTTP/1.0"
327 headers:self->additionalHeaders
330 [request setHeader:@"text/xml" forKey:@"content-type"];
331 [request setContentEncoding:NSUTF8StringEncoding];
332 [request appendContentString:[_call xmlRpcString]];
333 request = [request autorelease];
335 if ((authorization = [self _authorization]) != nil)
336 [request setHeader:authorization forKey:@"Authorization"];
338 if (![self->httpConnection sendRequest:request])
339 return [self sendFailed:[self->httpConnection lastException]];
341 response = [self->httpConnection readResponse];
343 [self->digestInfo release]; self->digestInfo = nil;
345 if ([response status] != 200) {
346 if ([response status] == 401 /* authentication required */) {
347 /* process info required for digest authentication */
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];
357 return [self callFailed:response];
360 if ((ctype = [response headerForKey:@"content-type"]) == nil)
361 ctype = @"text/xml"; // TODO, does it make sense? For simplistic servers?
363 if ([ctype hasPrefix:@"text/html"])
364 return [self processHTMLResponse:response];
367 [[XmlRpcMethodResponse alloc] initWithXmlRpcString:
368 [response contentAsString]];
369 if (methodResponse == nil)
370 return [self invalidXmlRpcResponse:response];
372 return [methodResponse autorelease];
375 - (id)doRawCall:(XmlRpcMethodCall *)_call {
376 XmlRpcMethodResponse *methodResponse;
377 NGActiveSocket *socket;
378 NGBufferedStream *io;
382 /* get body for XML-RPC request */
384 if ((s = [_call xmlRpcString]) == nil)
386 if ((rq = [s dataUsingEncoding:NSUTF8StringEncoding]) == nil)
391 // TODO: add timeout values
392 socket = [NGActiveSocket socketConnectedToAddress:self->address];
394 [self logWithFormat:@"could not connect %@", self->address];
395 return [self sendFailed:nil];
397 io = [NGBufferedStream filterWithSource:socket bufferSize:4096];
399 /* write body + \r\n\r\n */
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]];
406 return [self sendFailed:[io lastException]];
414 data = [NSMutableData dataWithCapacity:1024];
417 unsigned char buf[1024 + 10];
419 readCount = [io readBytes:&buf count:1024];
420 if (readCount == NGStreamError) {
423 if ((e = [io lastException]) == nil)
425 else if ([e isKindOfClass:[NGEndOfStreamException class]])
429 return [self sendFailed:e];
431 buf[readCount] = '\0';
433 [data appendBytes:buf length:readCount];
437 s = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
438 methodResponse = [[XmlRpcMethodResponse alloc] initWithXmlRpcString:s];
444 return [methodResponse autorelease];
447 - (id)invokeMethodNamed:(NSString *)_methodName parameters:(NSArray *)_params {
448 XmlRpcMethodCall *methodCall;
451 methodCall = [[XmlRpcMethodCall alloc] initWithMethodName:_methodName
454 if (self->httpConnection)
455 result = [self doCallViaHTTP:methodCall];
457 result = [self doRawCall:methodCall];
459 [methodCall release]; methodCall = nil;
461 if ([result isKindOfClass:[XmlRpcMethodResponse class]])
462 result = [result result];
465 [self logWithFormat:@"got nil value from XML-RPC ..."];
469 @end /* NGXmlRpcClient */
471 @implementation NSString(DigestInfo)
473 - (NSDictionary *)parseHTTPDigestInfo {
476 www-authenticate: Digest realm="RCD", \
477 nonce="1572920321042107679", \
478 qop="auth,auth-int", \
479 algorithm="MD5,MD5-sess"
481 NSMutableDictionary *md;
485 md = [NSMutableDictionary dictionaryWithCapacity:8];
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 ...
493 parts = [[self componentsSeparatedByString:@", "] objectEnumerator];
495 while ((part = [parts nextObject])) {
497 NSString *key, *value;
499 r = [part rangeOfString:@"="];
500 if (r.length == 0) continue;
502 key = [[part substringToIndex:r.location] stringByTrimmingSpaces];
503 value = [[part substringFromIndex:(r.location + r.length)]
504 stringByTrimmingSpaces];
506 //[self logWithFormat:@"key '%@' value '%@'", key, value];
508 if ([value hasPrefix:@"\""] && [value hasSuffix:@"\""]) {
510 r.length = [value length] - 2;
511 value = [value substringWithRange:r];
513 //[self logWithFormat:@"key '%@' value '%@'", key, value];
515 [md setObject:value forKey:[key lowercaseString]];
520 @end /* NSString(DigestInfo) */