2 Copyright (C) 2000-2007 SKYRIX Software AG
3 Copyright (C) 2007 Helge Hess
5 This file is part of SOPE.
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
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.
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
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 =
85 [(WOHTTPConnection *)[[self connectionClass] alloc] initWithURL:url];
87 if ([[url scheme] hasPrefix:@"http"])
88 self->uri = [[url path] copy];
90 /* hack for easier XMLRPC-over-Unix-Domain-sockets */
92 self->userName = [[url user] copy];
93 self->password = [[url password] copy];
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];
105 - (id)initWithRawAddress:(id)_address {
106 if (_address == nil) {
110 if ((self = [super init])) {
111 self->address = [_address retain];
117 [self->additionalHeaders release];
118 [self->address release];
119 [self->httpConnection release];
120 [self->userName release];
121 [self->password release];
132 // TODO: not final yet ... (hh asks: bjoern, is this used anywhere anyway ?)
133 p = [[NSString alloc] initWithFormat:@"http://%@:%i%@",
137 url = [NSURL URLWithString:p];
142 - (void)setUserName:(NSString *)_userName {
143 ASSIGNCOPY(self->userName, _userName);
145 - (NSString *)userName {
146 return self->userName;
148 - (NSString *)login {
149 return self->userName;
152 - (void)setPassword:(NSString *)_password {
153 ASSIGNCOPY(self->password, _password);
155 - (NSString *)password {
156 return self->password;
159 - (void)setUri:(NSString *)_uri {
160 ASSIGNCOPY(self->uri, _uri);
166 - (void)setAdditionalHeaders:(NSDictionary *)_headers {
167 ASSIGNCOPY(self->additionalHeaders, _headers);
169 - (NSDictionary *)additionalHeaders {
170 return self->additionalHeaders;
173 /* performing the method */
175 - (id)invokeMethodNamed:(NSString *)_methodName {
176 return [self invokeMethodNamed:_methodName parameters:nil];
179 - (id)invokeMethodNamed:(NSString *)_methodName withParameter:(id)_param {
180 NSArray *params = nil;
183 params = [NSArray arrayWithObject:_param];
185 return [self invokeMethodNamed:_methodName parameters:params];
188 - (id)invoke:(NSString *)_methodName params:(id)firstObj,... {
189 id array, obj, *objects;
193 va_start(list, firstObj);
194 for (count = 0, obj = firstObj; obj; obj = va_arg(list,id))
198 objects = calloc(count, sizeof(id));
200 va_start(list, firstObj);
201 for (count = 0, obj = firstObj; obj; obj = va_arg(list,id))
202 objects[count++] = obj;
205 array = [NSArray arrayWithObjects:objects count:count];
209 return [self invokeMethodNamed:_methodName parameters:array];
212 - (id)call:(NSString *)_methodName,... {
213 id array, obj, *objects;
217 va_start(list, _methodName);
218 for (count = 0, obj = va_arg(list, id); obj; obj = va_arg(list,id))
222 objects = calloc(count, sizeof(id));
224 va_start(list, _methodName);
225 for (count = 0, obj = va_arg(list, id); obj; obj = va_arg(list,id))
226 objects[count++] = obj;
229 array = [NSArray arrayWithObjects:objects count:count];
232 return [self invokeMethodNamed:_methodName parameters:array];
235 - (NSString *)_authorization {
238 if (self->userName == nil)
241 if (self->digestInfo) {
242 [self logWithFormat:@"need to construct digest authentication using %@",
248 tmp = [tmp stringByAppendingString:self->userName];
249 tmp = [tmp stringByAppendingString:@":"];
252 tmp = [tmp stringByAppendingString:self->password];
255 tmp = [tmp stringByEncodingBase64];
256 tmp = [@"Basic " stringByAppendingString:tmp];
261 - (id)sendFailed:(NSException *)e {
265 return [NSException exceptionWithName:@"XmlRpcSendFailed"
267 @"unknown reason, no exception set in "
273 - (id)callFailed:(WOResponse *)_response {
280 NSLog(@"%s: XML-RPC response status: %i", __PRETTY_FUNCTION__,
284 /* construct exception */
286 status = [_response status];
287 r = [NSString stringWithFormat:@"call failed with HTTP status code %i",
289 if (status == 301 || status == 302) {
290 NSString *l = [_response headerForKey:@"location"];
292 r = [NSString stringWithFormat:@"%@ [location=%@]", r, l];
295 ui = [NSDictionary dictionaryWithObjectsAndKeys:
296 self, @"NGXmlRpcClient",
297 _response, @"WOResponse",
298 [NSNumber numberWithInt:status],
302 exc = [NSException exceptionWithName:@"XmlRpcCallFailed"
307 - (id)invalidXmlRpcResponse:(WOResponse *)_response {
308 return [NSException exceptionWithName:@"XmlRpcCallFailed"
309 reason:@"got malformed XML-RPC response?!"
313 - (id)processHTMLResponse:(WOResponse *)_response {
316 if (_response == nil) return nil;
317 [self debugWithFormat:@"Note: got HTML response: %@", _response];
319 ui = [NSDictionary dictionaryWithObjectsAndKeys:
320 _response, @"response",
322 return [NSException exceptionWithName:@"XmlRpcCallFailed"
323 reason:@"got HTML response"
327 - (id)doCallViaHTTP:(XmlRpcMethodCall *)_call {
328 XmlRpcMethodResponse *methodResponse;
329 WOResponse *response;
331 NSString *authorization, *ctype;
333 request = [[[self requestClass] alloc] initWithMethod:@"POST"
335 httpVersion:@"HTTP/1.0"
336 headers:self->additionalHeaders
339 [request setHeader:@"text/xml" forKey:@"content-type"];
340 [request setContentEncoding:NSUTF8StringEncoding];
341 [request appendContentString:[_call xmlRpcString]];
342 request = [request autorelease];
344 if ((authorization = [self _authorization]) != nil)
345 [request setHeader:authorization forKey:@"Authorization"];
347 if (![self->httpConnection sendRequest:request])
348 return [self sendFailed:[self->httpConnection lastException]];
350 response = [self->httpConnection readResponse];
352 [self->digestInfo release]; self->digestInfo = nil;
354 if ([response status] != 200) {
355 if ([response status] == 401 /* authentication required */) {
356 /* process info required for digest authentication */
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];
366 return [self callFailed:response];
369 if ((ctype = [response headerForKey:@"content-type"]) == nil)
370 ctype = @"text/xml"; // TODO, does it make sense? For simplistic servers?
372 if ([ctype hasPrefix:@"text/html"])
373 return [self processHTMLResponse:response];
376 [[XmlRpcMethodResponse alloc] initWithXmlRpcString:
377 [response contentAsString]];
378 if (methodResponse == nil)
379 return [self invalidXmlRpcResponse:response];
381 return [methodResponse autorelease];
384 - (id)doRawCall:(XmlRpcMethodCall *)_call {
385 XmlRpcMethodResponse *methodResponse;
386 NGActiveSocket *socket;
387 NGBufferedStream *io;
391 /* get body for XML-RPC request */
393 if ((s = [_call xmlRpcString]) == nil)
395 if ((rq = [s dataUsingEncoding:NSUTF8StringEncoding]) == nil)
400 // TODO: add timeout values
401 socket = [NGActiveSocket socketConnectedToAddress:self->address];
403 [self logWithFormat:@"could not connect %@", self->address];
404 return [self sendFailed:nil];
406 io = [NGBufferedStream filterWithSource:socket bufferSize:4096];
408 /* write body + \r\n\r\n */
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]];
415 return [self sendFailed:[io lastException]];
423 data = [NSMutableData dataWithCapacity:1024];
426 unsigned char buf[1024 + 10];
428 readCount = [io readBytes:&buf count:1024];
429 if (readCount == NGStreamError) {
432 if ((e = [io lastException]) == nil)
434 else if ([e isKindOfClass:[NGEndOfStreamException class]])
438 return [self sendFailed:e];
440 buf[readCount] = '\0';
442 [data appendBytes:buf length:readCount];
446 s = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
447 methodResponse = [[XmlRpcMethodResponse alloc] initWithXmlRpcString:s];
453 return [methodResponse autorelease];
456 - (id)invokeMethodNamed:(NSString *)_methodName parameters:(NSArray *)_params {
457 XmlRpcMethodCall *methodCall;
460 methodCall = [[XmlRpcMethodCall alloc] initWithMethodName:_methodName
463 if (self->httpConnection)
464 result = [self doCallViaHTTP:methodCall];
466 result = [self doRawCall:methodCall];
468 [methodCall release]; methodCall = nil;
470 if ([result isKindOfClass:[XmlRpcMethodResponse class]])
471 result = [result result];
474 [self logWithFormat:@"got nil value from XML-RPC ..."];
478 @end /* NGXmlRpcClient */
480 @implementation NSString(DigestInfo)
482 - (NSDictionary *)parseHTTPDigestInfo {
485 www-authenticate: Digest realm="RCD", \
486 nonce="1572920321042107679", \
487 qop="auth,auth-int", \
488 algorithm="MD5,MD5-sess"
490 NSMutableDictionary *md;
494 md = [NSMutableDictionary dictionaryWithCapacity:8];
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 ...
502 parts = [[self componentsSeparatedByString:@", "] objectEnumerator];
504 while ((part = [parts nextObject])) {
506 NSString *key, *value;
508 r = [part rangeOfString:@"="];
509 if (r.length == 0) continue;
511 key = [[part substringToIndex:r.location] stringByTrimmingSpaces];
512 value = [[part substringFromIndex:(r.location + r.length)]
513 stringByTrimmingSpaces];
515 //[self logWithFormat:@"key '%@' value '%@'", key, value];
517 if ([value hasPrefix:@"\""] && [value hasSuffix:@"\""]) {
519 r.length = [value length] - 2;
520 value = [value substringWithRange:r];
522 //[self logWithFormat:@"key '%@' value '%@'", key, value];
524 [md setObject:value forKey:[key lowercaseString]];
529 @end /* NSString(DigestInfo) */