/** Copyright (C) 2004 Free Software Foundation, Inc. Written by: Richard Frith-Macdonald Date: June 2004 This file is part of the SQLClient Library. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111 USA. $Date$ $Revision$ */ #include #include "WebServer.h" @interface WebServerSession : NSObject { NSString *address; NSFileHandle *handle; GSMimeParser *parser; NSMutableData *buffer; unsigned byteCount; NSTimeInterval ticked; BOOL processing; BOOL shouldEnd; BOOL hasReset; } - (NSString*) address; - (NSMutableData*) buffer; - (NSFileHandle*) handle; - (BOOL) hasReset; - (unsigned) moreBytes: (unsigned)count; - (GSMimeParser*) parser; - (BOOL) processing; - (void) reset; - (void) setAddress: (NSString*)aString; - (void) setBuffer: (NSMutableData*)aBuffer; - (void) setHandle: (NSFileHandle*)aHandle; - (void) setParser: (GSMimeParser*)aParser; - (void) setProcessing: (BOOL)aFlag; - (void) setShouldEnd: (BOOL)aFlag; - (void) setTicked: (NSTimeInterval)when; - (BOOL) shouldEnd; - (NSTimeInterval) ticked; @end @implementation WebServerSession - (NSString*) address { return address; } - (NSMutableData*) buffer { return buffer; } - (void) dealloc { [handle closeFile]; DESTROY(address); DESTROY(buffer); DESTROY(handle); DESTROY(parser); [super dealloc]; } - (NSString*) description { return [NSString stringWithFormat: @"%@ [%@] ", [super description], [self address]]; } - (NSFileHandle*) handle { return handle; } - (BOOL) hasReset { return hasReset; } - (unsigned) moreBytes: (unsigned)count { byteCount += count; return byteCount; } - (GSMimeParser*) parser { return parser; } - (BOOL) processing { return processing; } - (void) reset { hasReset = YES; [self setBuffer: [NSMutableData dataWithCapacity: 1024]]; [self setParser: nil]; [self setProcessing: NO]; } - (void) setAddress: (NSString*)aString { ASSIGN(address, aString); } - (void) setBuffer: (NSMutableData*)aBuffer { ASSIGN(buffer, aBuffer); } - (void) setHandle: (NSFileHandle*)aHandle { ASSIGN(handle, aHandle); } - (void) setParser: (GSMimeParser*)aParser { ASSIGN(parser, aParser); } - (void) setProcessing: (BOOL)aFlag { processing = aFlag; } - (void) setShouldEnd: (BOOL)aFlag { shouldEnd = aFlag; } - (void) setTicked: (NSTimeInterval)when { ticked = when; } - (BOOL) shouldEnd { return shouldEnd; } - (NSTimeInterval) ticked { return ticked; } @end @interface WebServer (Private) - (void) _alert: (NSString*)fmt, ...; - (void) _didConnect: (NSNotification*)notification; - (void) _didRead: (NSNotification*)notification; - (void) _didWrite: (NSNotification*)notification; - (void) _endSession: (WebServerSession*)session; - (void) _process: (WebServerSession*)session; - (void) _timeout: (NSTimer*)timer; @end @implementation WebServer - (void) dealloc { if (_ticker != nil) { [_ticker invalidate]; _ticker = nil; } [self setPort: nil secure: nil]; DESTROY(_nc); DESTROY(_root); DESTROY(_quiet); DESTROY(_hosts); DESTROY(_perHost); if (_sessions != 0) { NSFreeMapTable(_sessions); _sessions = 0; } [super dealloc]; } static unsigned unescapeData(const unsigned char* bytes, unsigned length, unsigned char *buf) { unsigned int to = 0; unsigned int from = 0; while (from < length) { unsigned char c = bytes[from++]; if (c == '+') { c = ' '; } else if (c == '%' && from < length - 1) { unsigned char tmp; c = 0; tmp = bytes[from++]; if (tmp <= '9' && tmp >= '0') { c = tmp - '0'; } else if (tmp <= 'F' && tmp >= 'A') { c = tmp + 10 - 'A'; } else if (tmp <= 'f' && tmp >= 'a') { c = tmp + 10 - 'a'; } else { c = 0; } c <<= 4; tmp = bytes[from++]; if (tmp <= '9' && tmp >= '0') { c += tmp - '0'; } else if (tmp <= 'F' && tmp >= 'A') { c += tmp + 10 - 'A'; } else if (tmp <= 'f' && tmp >= 'a') { c += tmp + 10 - 'a'; } else { c = 0; } } buf[to++] = c; } return to; } - (unsigned) decodeURLEncodedForm: (NSData*)data into: (NSMutableDictionary*)dict { const unsigned char *bytes = (const unsigned char*)[data bytes]; unsigned length = [data length]; unsigned pos = 0; unsigned fields = 0; while (pos < length) { unsigned int keyStart = pos; unsigned int keyEnd; unsigned int valStart; unsigned int valEnd; unsigned char *buf; unsigned int buflen; BOOL escape = NO; NSData *d; NSString *k; NSMutableArray *a; while (pos < length && bytes[pos] != '&') { pos++; } valEnd = pos; if (pos < length) { pos++; // Step past '&' } keyEnd = keyStart; while (keyEnd < pos && bytes[keyEnd] != '=') { if (bytes[keyEnd] == '%' || bytes[keyEnd] == '+') { escape = YES; } keyEnd++; } if (escape == YES) { buf = NSZoneMalloc(NSDefaultMallocZone(), keyEnd - keyStart); buflen = unescapeData(&bytes[keyStart], keyEnd - keyStart, buf); d = [[NSData alloc] initWithBytesNoCopy: buf length: buflen freeWhenDone: YES]; } else { d = [[NSData alloc] initWithBytesNoCopy: (void*)&bytes[keyStart] length: keyEnd - keyStart freeWhenDone: NO]; } k = [[NSString alloc] initWithData: d encoding: NSUTF8StringEncoding]; if (k == nil) { [NSException raise: NSInvalidArgumentException format: @"Bad UTF-8 form data (key of field %d)", fields]; } RELEASE(d); valStart = keyEnd; if (valStart < pos) { valStart++; // Step past '=' } if (valStart < valEnd) { buf = NSZoneMalloc(NSDefaultMallocZone(), valEnd - valStart); buflen = unescapeData(&bytes[valStart], valEnd - valStart, buf); d = [[NSData alloc] initWithBytesNoCopy: buf length: buflen freeWhenDone: YES]; } else { d = [NSData new]; } a = [dict objectForKey: k]; if (a == nil) { a = [[NSMutableArray alloc] initWithCapacity: 1]; [dict setObject: a forKey: k]; RELEASE(a); } [a addObject: d]; RELEASE(d); RELEASE(k); fields++; } return fields; } - (NSString*) description { return [NSString stringWithFormat: @"%@ on %@, %u of %u sessions active, %u total, %u requests, listening: %@", [super description], _port, NSCountMapTable(_sessions), _maxSessions, _handled, _requests, _accepting == YES ? @"yes" : @"no"]; } - (id) init { NSUserDefaults *defs = [NSUserDefaults standardUserDefaults]; _hosts = RETAIN([defs arrayForKey: @"WebServerHosts"]); _quiet = RETAIN([defs arrayForKey: @"WebServerQuiet"]); _nc = RETAIN([NSNotificationCenter defaultCenter]); _sessionTimeout = 30.0; _maxPerHost = 8; _maxSessions = 32; _maxBodySize = 8*1024; _maxRequestSize = 4*1024*1024; _substitutionLimit = 4; _sessions = NSCreateMapTable(NSNonOwnedPointerMapKeyCallBacks, NSObjectMapValueCallBacks, 0); _perHost = [NSCountedSet new]; _ticker = [NSTimer scheduledTimerWithTimeInterval: 0.8 target: self selector: @selector(_timeout:) userInfo: 0 repeats: YES]; return self; } - (BOOL) isSecure { if (_sslConfig == nil) { return NO; } return YES; } - (BOOL) produceResponse: (GSMimeDocument*)aResponse fromTemplate: (NSString*)aPath using: (NSDictionary*)map { CREATE_AUTORELEASE_POOL(arp); NSString *path = (_root == nil) ? (id)@"" : (id)_root; NSString *str; NSFileManager *mgr; BOOL result; path = [path stringByAppendingString: @"/"]; str = [path stringByStandardizingPath]; path = [path stringByAppendingPathComponent: aPath]; path = [path stringByStandardizingPath]; mgr = [NSFileManager defaultManager]; if ([path hasPrefix: str] == NO) { [self _alert: @"Illegal template '%@' ('%@')", aPath, path]; result = NO; } else if ([mgr isReadableFileAtPath: path] == NO) { [self _alert: @"Can't read template '%@' ('%@')", aPath, path]; result = NO; } else if ((str = [NSString stringWithContentsOfFile: path]) == nil) { [self _alert: @"Failed to load template '%@' ('%@')", aPath, path]; result = NO; } else { NSMutableString *m = [NSMutableString stringWithCapacity: [str length]]; result = [self substituteFrom: str using: map into: m depth: 0]; if (result == YES) { [aResponse setContent: m type: @"text/html" name: nil]; [[aResponse headerNamed: @"content-type"] setParameter: @"utf-8" forKey: @"charset"]; } } DESTROY(arp); return result; } - (NSMutableDictionary*) parameters: (GSMimeDocument*)request { NSMutableDictionary *params; NSString *str = [[request headerNamed: @"x-http-query"] value]; NSData *data; params = [NSMutableDictionary dictionaryWithCapacity: 32]; if ([str length] > 0) { data = [str dataUsingEncoding: NSASCIIStringEncoding]; [self decodeURLEncodedForm: data into: params]; } str = [[request headerNamed: @"content-type"] value]; if ([str isEqualToString: @"application/x-www-form-urlencoded"] == YES) { data = [request convertToData]; [self decodeURLEncodedForm: data into: params]; } else if ([str isEqualToString: @"multipart/form-data"] == YES) { NSArray *contents = [request content]; unsigned count = [contents count]; unsigned i; for (i = 0; i < count; i++) { GSMimeDocument *doc = [contents objectAtIndex: i]; GSMimeHeader *hdr = [doc headerNamed: @"content-type"]; NSString *k = [hdr parameterForKey: @"name"]; if (k == nil) { hdr = [doc headerNamed: @"content-disposition"]; k = [hdr parameterForKey: @"name"]; } if (k != nil) { NSMutableArray *a; a = [params objectForKey: k]; if (a == nil) { a = [[NSMutableArray alloc] initWithCapacity: 1]; [params setObject: a forKey: k]; RELEASE(a); } [a addObject: [doc convertToData]]; } } } return params; } - (NSData*) parameter: (NSString*)name at: (unsigned)index from: (NSDictionary*)params { NSArray *a = [params objectForKey: name]; if (a == nil) { NSEnumerator *e = [params keyEnumerator]; NSString *k; while ((k = [e nextObject]) != nil) { if ([k caseInsensitiveCompare: name] == NSOrderedSame) { a = [params objectForKey: k]; break; } } } if (index >= [a count]) { return nil; } return [a objectAtIndex: index]; } - (NSData*) parameter: (NSString*)name from: (NSDictionary*)params { return [self parameter: name at: 0 from: params]; } - (NSString*) parameterString: (NSString*)name at: (unsigned)index from: (NSDictionary*)params { return [self parameterString: name at: index from: params charset: nil]; } - (NSString*) parameterString: (NSString*)name at: (unsigned)index from: (NSDictionary*)params charset: (NSString*)charset { NSData *d = [self parameter: name at: index from: params]; NSString *s = nil; if (d != nil) { s = [NSString alloc]; if (charset == nil || [charset length] == 0) { s = [s initWithData: d encoding: NSUTF8StringEncoding]; } else { NSStringEncoding enc; enc = [GSMimeDocument encodingFromCharset: charset]; s = [s initWithData: d encoding: enc]; } } return AUTORELEASE(s); } - (NSString*) parameterString: (NSString*)name from: (NSDictionary*)params { return [self parameterString: name at: 0 from: params charset: nil]; } - (NSString*) parameterString: (NSString*)name from: (NSDictionary*)params charset: (NSString*)charset { return [self parameterString: name at: 0 from: params charset: charset]; } - (void) setDelegate: (id)anObject { _delegate = anObject; } - (void) setMaxBodySize: (unsigned)max { _maxBodySize = max; } - (void) setMaxRequestSize: (unsigned)max { _maxRequestSize = max; } - (void) setMaxSessions: (unsigned)max { _maxSessions = max; } - (void) setMaxSessionsPerHost: (unsigned)max { _maxPerHost = max; } - (BOOL) setPort: (NSString*)aPort secure: (NSDictionary*)secure { BOOL ok = YES; BOOL update = NO; if (aPort == nil || [aPort isEqual: _port] == NO) { update = YES; } if ((secure == nil && _sslConfig != nil) || (secure != nil && [secure isEqual: _sslConfig] == NO)) { update = YES; } if (update == YES) { ASSIGN(_sslConfig, secure); if (_listener != nil) { [_nc removeObserver: self name: NSFileHandleConnectionAcceptedNotification object: _listener]; DESTROY(_listener); } _accepting = NO; // No longer listening for connections. DESTROY(_port); if (aPort != nil) { _port = [aPort copy]; if (_sslConfig != nil) { _listener = [[NSFileHandle sslClass] fileHandleAsServerAtAddress: nil service: _port protocol: @"tcp"]; } else { _listener = [NSFileHandle fileHandleAsServerAtAddress: nil service: _port protocol: @"tcp"]; } if (_listener == nil) { [self _alert: @"Failed to listen on port %@", _port]; DESTROY(_port); ok = NO; } else { RETAIN(_listener); [_nc addObserver: self selector: @selector(_didConnect:) name: NSFileHandleConnectionAcceptedNotification object: _listener]; if (_accepting == NO && (_maxSessions <= 0 || NSCountMapTable(_sessions) < _maxSessions)) { [_listener acceptConnectionInBackgroundAndNotify]; _accepting = YES; } } } } return ok; } - (void) setRoot: (NSString*)aPath { ASSIGN(_root, aPath); } - (void) setSessionTimeout: (NSTimeInterval)aDelay { _sessionTimeout = aDelay; } - (void) setSubstitutionLimit: (unsigned)depth { _substitutionLimit = depth; } - (void) setVerbose: (BOOL)aFlag { _verbose = aFlag; } - (BOOL) substituteFrom: (NSString*)aTemplate using: (NSDictionary*)map into: (NSMutableString*)result depth: (unsigned)depth { unsigned length; unsigned pos = 0; NSRange r = NSMakeRange(pos, length); if (depth > _substitutionLimit) { [self _alert: @"Substitution exceeded limit (%u)", _substitutionLimit]; return NO; } length = [aTemplate length]; r = NSMakeRange(pos, length); r = [aTemplate rangeOfString: @"" options: NSLiteralSearch range: r]; if (r.length > 0) { unsigned end = NSMaxRange(r); NSString *subFrom; NSString *subTo; r = NSMakeRange(start + 4, r.location - start - 4); subFrom = [aTemplate substringWithRange: r]; subTo = [map objectForKey: subFrom]; if (subTo == nil) { [result appendString: @"