/** 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" #include "SQLClient.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 - (BOOL) accessRequest: (GSMimeDocument*)request response: (GSMimeDocument*)response { NSUserDefaults *defs = [NSUserDefaults standardUserDefaults]; NSDictionary *conf = [defs dictionaryForKey: @"WebServerAccess"]; NSString *path = [[request headerNamed: @"x-http-path"] value]; NSDictionary *access = nil; NSString *stored; NSString *username; NSString *password; while (access == nil) { access = [conf objectForKey: path]; if ([access isKindOfClass: [NSDictionary class]] == NO) { NSRange r; r = [path rangeOfString: @"/" options: NSBackwardsSearch]; if (r.length > 0) { path = [path substringToIndex: r.location]; } else { return YES; // No access dictionary - permit access } } } username = [[request headerNamed: @"x-http-username"] value]; password = [[request headerNamed: @"x-http-password"] value]; if ([access objectForKey: @"Users"] != nil) { NSDictionary *users = [access objectForKey: @"Users"]; stored = [users objectForKey: username]; } else if ([access objectForKey: @"UserDB"] != nil) { static Class c = nil; static BOOL beenHere = NO; /* * We get the SQLClient class from thee runtime, so we don't have to * link the library directly ... which means that this class caan be * used without it as long as database accesss is not needed. */ if (beenHere == NO) { beenHere = YES; c = NSClassFromString(@"SQLClient"); if (c == nil) { [self _alert: @"SQLClient library has not been linked"]; } } NS_DURING { NSDictionary *info = [access objectForKey: @"UserDB"]; NSString *name = [info objectForKey: @"Name"]; SQLClient *sql; /* * try to re-use an existing client if possible. */ sql = [c existingClient: name]; if (sql == nil) { sql = [c alloc]; sql = [sql initWithConfiguration: nil name: name]; } stored = [sql queryString: @"SELECT ", [info objectForKey: @"Password"], @" FROM ", [info objectForKey: @"Table"], @" WHERE ", [info objectForKey: @"Username"], @" = ", [sql quote: username], nil]; } NS_HANDLER { [self _alert: @"Read from database failed - %@", localException]; stored = nil; } NS_ENDHANDLER } if (username == nil || password == nil || [password isEqual: stored] == NO) { NSString *realm = [access objectForKey: @"Realm"]; NSString *auth; auth = [NSString stringWithFormat: @"Basic realm=\"%@\"", realm]; /* * Return status code 401 (Aunauthorised) */ [response setHeader: @"http" value: @"HTTP/1.1 401 Unauthorised" parameters: nil]; [response setHeader: @"WWW-authenticate" value: auth parameters: nil]; [response setContent: @"\n" @"401 Authorization Required\n" @"

Authorization Required

\n" @"

This server could not verify that you " @"are authorized to access the resource " @"requested. Either you supplied the wrong " @"credentials (e.g., bad password), or your " @"browser doesn't understand how to supply " @"the credentials required.

\n" @"\n" type: @"text/html"]; return NO; } else { return YES; // OK to access } } - (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; } static NSMutableData* escapeData(const unsigned char* bytes, unsigned length, NSMutableData *d) { unsigned char *dst; unsigned int spos = 0; unsigned int dpos = [d length]; [d setLength: dpos + 3 * length]; dst = (unsigned char*)[d mutableBytes]; while (spos < length) { unsigned char c = bytes[spos++]; unsigned int hi; unsigned int lo; switch (c) { case ',': case ';': case '"': case '\'': case '&': case '=': case '(': case ')': case '<': case '>': case '?': case '#': case '{': case '}': case '%': case ' ': case '+': dst[dpos++] = '%'; hi = (c & 0xf0) >> 4; dst[dpos++] = (hi > 9) ? 'A' + hi - 10 : '0' + hi; lo = (c & 0x0f); dst[dpos++] = (lo > 9) ? 'A' + lo - 10 : '0' + lo; break; default: if (c < ' ' || c > 127) { dst[dpos++] = '%'; hi = (c & 0xf0) >> 4; dst[dpos++] = (hi > 9) ? 'A' + hi - 10 : '0' + hi; lo = (c & 0x0f); dst[dpos++] = (lo > 9) ? 'A' + lo - 10 : '0' + lo; } else { dst[dpos++] = c; } break; } } [d setLength: dpos]; return d; } - (unsigned) encodeURLEncodedForm: (NSDictionary*)dict into: (NSMutableData*)data { CREATE_AUTORELEASE_POOL(arp); NSEnumerator *keyEnumerator; id key; unsigned valueCount = 0; NSMutableData *md = [NSMutableData dataWithCapacity: 100]; keyEnumerator = [dict keyEnumerator]; while ((key = [keyEnumerator nextObject]) != nil) { id values = [dict objectForKey: key]; NSData *keyData; NSEnumerator *valueEnumerator; id value; if ([key isKindOfClass: [NSData class]] == YES) { keyData = key; } else { key = [key description]; keyData = [key dataUsingEncoding: NSUTF8StringEncoding]; } [md setLength: 0]; escapeData([keyData bytes], [keyData length], md); keyData = md; if ([values isKindOfClass: [NSArray class]] == NO) { values = [NSArray arrayWithObject: values]; } valueEnumerator = [values objectEnumerator]; while ((value = [valueEnumerator nextObject]) != nil) { NSData *valueData; if ([data length] > 0) { [data appendBytes: "&" length: 1]; } [data appendData: keyData]; [data appendBytes: "=" length: 1]; if ([value isKindOfClass: [NSData class]] == YES) { valueData = value; } else { value = [value description]; valueData = [value dataUsingEncoding: NSUTF8StringEncoding]; } escapeData([valueData bytes], [valueData length], data); valueCount++; } } RELEASE(arp); return valueCount; } - (NSString*) description { return [NSString stringWithFormat: @"%@ on %@(%@), %u of %u sessions active," @" %u ended, %u requests, listening: %@", [super description], _port, ([self isSecure] ? @"https" : @"http"), 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 fromStaticPage: (NSString*)aPath using: (NSDictionary*)map { CREATE_AUTORELEASE_POOL(arp); NSString *path = (_root == nil) ? (id)@"" : (id)_root; NSString *ext = [aPath pathExtension]; NSString *type; NSString *str; id data; NSFileManager *mgr; BOOL string = NO; BOOL result = YES; if (map == nil) { static NSDictionary *defaultMap = nil; if (defaultMap == nil) { defaultMap = [[NSDictionary alloc] initWithObjectsAndKeys: @"image/gif", @"gif", @"image/png", @"png", @"image/jpeg", @"jpeg", @"text/html", @"html", @"text/plain", @"txt", @"text/xml", @"xml", nil]; } map = defaultMap; } type = [map objectForKey: ext]; if (type == nil) { type = [map objectForKey: [ext lowercaseString]]; } if (type == nil) { type = @"application/octet-stream"; } string = [type hasPrefix: @"text/"]; path = [path stringByAppendingString: @"/"]; str = [path stringByStandardizingPath]; path = [path stringByAppendingPathComponent: aPath]; path = [path stringByStandardizingPath]; mgr = [NSFileManager defaultManager]; if ([path hasPrefix: str] == NO) { [self _alert: @"Illegal static page '%@' ('%@')", aPath, path]; result = NO; } else if ([mgr isReadableFileAtPath: path] == NO) { [self _alert: @"Can't read static page '%@' ('%@')", aPath, path]; result = NO; } else if (string == YES && (data = [NSString stringWithContentsOfFile: path]) == nil) { [self _alert: @"Failed to load string '%@' ('%@')", aPath, path]; result = NO; } else if (string == NO && (data = [NSData dataWithContentsOfFile: path]) == nil) { [self _alert: @"Failed to load data '%@' ('%@')", aPath, path]; result = NO; } else { [aResponse setContent: data type: type name: nil]; } DESTROY(arp); return result; } - (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: @"