libs-sqlclient/WebServer.m
Richard Frith-Macdonald d50f4ea8e6 Improve ignoring of connections.
git-svn-id: svn+ssh://svn.gna.org/svn/gnustep/libs/sqlclient/trunk@20180 72102866-910b-0410-8b05-ffd578937521
2004-09-29 13:09:47 +00:00

1421 lines
32 KiB
Objective-C

/**
Copyright (C) 2004 Free Software Foundation, Inc.
Written by: Richard Frith-Macdonald <rfm@gnu.org>
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 <Foundation/Foundation.h>
#include "WebServer.h"
@interface WebServerSession : NSObject
{
NSString *address;
NSFileHandle *handle;
GSMimeParser *parser;
NSMutableData *buffer;
unsigned byteCount;
NSTimeInterval ticked;
BOOL processing;
BOOL shouldEnd;
}
- (NSString*) address;
- (NSMutableData*) buffer;
- (NSFileHandle*) handle;
- (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;
}
- (unsigned) moreBytes: (unsigned)count
{
byteCount += count;
return byteCount;
}
- (GSMimeParser*) parser
{
return parser;
}
- (BOOL) processing
{
return processing;
}
- (void) reset
{
[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];
while (r.length > 0)
{
unsigned start = r.location;
if (start > pos)
{
r = NSMakeRange(pos, r.location - pos);
[result appendString: [aTemplate substringWithRange: r]];
}
pos = r.location;
r = NSMakeRange(start + 4, length - start - 4);
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: @"<!--"];
pos += 4;
}
else
{
/*
* Unless the value substituted in is a comment,
* perform recursive substitution.
*/
if ([subTo hasPrefix: @"<!--"] == NO)
{
BOOL v;
v = [self substituteFrom: subTo
using: map
into: result
depth: depth + 1];
if (v == NO)
{
return NO;
}
}
else
{
[result appendString: subTo];
}
pos = end;
}
}
else
{
[result appendString: @"<!--"];
pos += 4;
}
r = NSMakeRange(pos, length - pos);
r = [aTemplate rangeOfString: @"<!--"
options: NSLiteralSearch
range: r];
}
if (pos < length)
{
r = NSMakeRange(pos, length - pos);
[result appendString: [aTemplate substringWithRange: r]];
}
return YES;
}
@end
@implementation WebServer (Private)
- (void) _alert: (NSString*)fmt, ...
{
va_list args;
va_start(args, fmt);
if ([_delegate respondsToSelector: @selector(webAlert:for:)] == YES)
{
NSString *s;
s = [NSString stringWithFormat: fmt arguments: args];
[_delegate webAlert: s for: self];
}
else
{
NSLogv(fmt, args);
}
va_end(args);
}
- (void) _didConnect: (NSNotification*)notification
{
NSDictionary *userInfo = [notification userInfo];
NSFileHandle *hdl;
NSString *a;
NSHost *h;
_accepting = NO;
hdl = [userInfo objectForKey: NSFileHandleNotificationFileHandleItem];
if (hdl == nil)
{
[NSException raise: NSInternalInconsistencyException
format: @"[%@ -%@] missing handle",
NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
}
else if ((a = [hdl socketAddress]) == nil)
{
[self _alert: @"Unknown address for new connection."];
[hdl closeFile];
}
else
{
if (_sslConfig != nil)
{
[hdl sslSetCertificate: [_sslConfig objectForKey: @"CertificateFile"]
privateKey: [_sslConfig objectForKey: @"KeyFile"]
PEMpasswd: [_sslConfig objectForKey: @"Password"]];
}
if ((h = [NSHost hostWithAddress: a]) == nil)
{
/*
* Don't log this in quiet mode as it could just be a
* test connection that we are ignoring.
*/
if ([_quiet containsObject: a] == NO)
{
[self _alert: @"Unknown host (%@) on new connection.", a];
}
}
else if (_hosts != nil && [_hosts containsObject: a] == NO)
{
/*
* Don't log this in quiet mode as it could just be a
* test connection that we are ignoring.
*/
if ([_quiet containsObject: a] == NO)
{
[self _alert: @"Invalid host (%@) on new connection.", a];
}
}
else if (_maxPerHost > 0 && [_perHost countForObject: a] >= _maxPerHost)
{
[self _alert: @"Too many connections from (%@) for new connect.", a];
}
else if (_sslConfig != nil && [hdl sslAccept] == NO)
{
/*
* Don't log this in quiet mode as it could just be a
* test connection that we are ignoring.
*/
if ([_quiet containsObject: a] == NO)
{
[self _alert: @"SSL accept fail on new connection (%@).", a];
}
}
else
{
WebServerSession *session = [WebServerSession new];
[session setAddress: a];
[session setHandle: hdl];
[session setBuffer: [NSMutableData dataWithCapacity: 1024]];
[session setTicked: _ticked];
NSMapInsert(_sessions, (void*)hdl, (void*)session);
[_perHost addObject: [session address]];
RELEASE(session);
[_nc addObserver: self
selector: @selector(_didRead:)
name: NSFileHandleReadCompletionNotification
object: hdl];
[_nc addObserver: self
selector: @selector(_didWrite:)
name: GSFileHandleWriteCompletionNotification
object: hdl];
[hdl readInBackgroundAndNotify];
if (_verbose == YES && [_quiet containsObject: a] == NO)
{
NSLog(@"%@ connect", session);
}
}
}
if (_accepting == NO
&& (_maxSessions == 0 || NSCountMapTable(_sessions) < _maxSessions))
{
[_listener acceptConnectionInBackgroundAndNotify];
_accepting = YES;
}
}
- (void) _didRead: (NSNotification*)notification
{
NSDictionary *dict = [notification userInfo];
NSFileHandle *hdl = [notification object];
NSData *d;
id parser;
NSString *method = @"";
NSString *query = @"";
NSString *path = @"";
NSString *version = @"";
WebServerSession *session;
GSMimeDocument *doc;
session = (WebServerSession*)NSMapGet(_sessions, (void*)hdl);
NSAssert(session != nil, NSInternalInconsistencyException);
parser = [session parser];
d = [dict objectForKey: NSFileHandleNotificationDataItem];
if ([d length] == 0)
{
if (parser == nil)
{
NSMutableData *buffer = [session buffer];
if ([buffer length] == 0)
{
/*
* Don't log this in quiet mode as it could just be a
* test connection that we are ignoring.
*/
if ([_quiet containsObject: [session address]] == NO)
{
[self _alert: @"%@ read end-of-file in empty request",
session];
}
}
else
{
[self _alert: @"%@ read end-of-file in partial request - %@",
session, buffer];
}
}
else
{
[self _alert: @"%@ read end-of-file in incomplete request - %@",
session, [parser mimeDocument]];
}
[self _endSession: session];
return;
}
// NSLog(@"Data read on %@ ... %@", session, d);
// Mark session as having had I/O ... not idle.
[session setTicked: _ticked];
if (parser == nil)
{
unsigned char *bytes;
unsigned int length;
unsigned int pos;
NSMutableData *buffer;
/*
* Add new data to any we already have and search for the end
* of the initial request line.
*/
buffer = [session buffer];
[buffer appendData: d];
bytes = [buffer mutableBytes];
length = [buffer length];
for (pos = 0; pos < length; pos++)
{
if (bytes[pos] == '\n')
{
break;
}
}
/*
* Attackers may try to send too much data in the hope of causing
* a buffer overflow ... so we try to detect it here.
*/
if (pos >= _maxBodySize)
{
[self _alert: @"Request too long ... rejected"];
[session setShouldEnd: YES];
[hdl writeInBackgroundAndNotify:
[@"HTTP/1.0 500 Request data too long\r\n\r\n"
dataUsingEncoding: NSASCIIStringEncoding]];
return;
}
if (pos == length)
{
[hdl readInBackgroundAndNotify]; // Needs more data.
return;
}
else
{
unsigned back = pos;
unsigned start = 0;
unsigned end;
/*
* Trim trailing whitespace from request line.
*/
bytes[pos++] = '\0';
while (back > 0 && isspace(bytes[--back]))
{
bytes[back] = '\0';
}
/*
* Remove and store trailing HTTP version extension
*/
while (back > 0 && !isspace(bytes[back]))
{
back--;
}
if (isspace(bytes[back]))
{
bytes[back] = '\0';
end = back + 1;
if (strncmp(bytes + end, "HTTP/", 5) == 0)
{
end += 5;
version = [NSString stringWithUTF8String: bytes + end];
}
}
if ([version floatValue] < 1.1)
{
[session setShouldEnd: YES]; // Not persistent.
}
/*
* Remove leading white space.
*/
start = 0;
while (start < back && isspace(bytes[start]))
{
start++;
}
/*
* Extract method string as uppercase value.
*/
end = start;
while (end < back && !isspace(bytes[end]))
{
if (islower(bytes[end]))
{
bytes[end] = toupper(bytes[end]);
}
end++;
}
bytes[end++] = '\0';
method = [NSString stringWithUTF8String: bytes + start];
/*
* Extract path string.
*/
start = end;
while (start < back && isspace(bytes[start]))
{
start++;
}
end = start;
while (end < back && bytes[end] != '?')
{
end++;
}
if (bytes[end] == '?')
{
/*
* Extract query string.
*/
bytes[end++] = '\0';
query = [NSString stringWithUTF8String: bytes + end];
}
else
{
bytes[end] = '\0';
}
path = [NSString stringWithUTF8String: bytes + start];
if ([method isEqualToString: @"GET"] == NO
&& [method isEqualToString: @"POST"] == NO)
{
[session setShouldEnd: YES]; // Not persistent.
[hdl writeInBackgroundAndNotify:
[@"HTTP/1.0 501 Not Implemented\r\n\r\n"
dataUsingEncoding: NSASCIIStringEncoding]];
return;
}
/*
* Any left over data is passed to the mime parser.
*/
if (pos < length)
{
memmove(bytes, &bytes[pos], length - pos);
[buffer setLength: length - pos];
d = AUTORELEASE(RETAIN(buffer));
}
parser = [GSMimeParser new];
[parser setIsHttp];
[parser setDefaultCharset: @"utf-8"];
doc = [parser mimeDocument];
[doc setHeader: @"x-http-method"
value: method
parameters: nil];
[doc setHeader: @"x-http-path"
value: path
parameters: nil];
[doc setHeader: @"x-http-query"
value: query
parameters: nil];
[doc setHeader: @"x-http-version"
value: version
parameters: nil];
[session setParser: parser];
RELEASE(parser);
if (pos >= length)
{
[hdl readInBackgroundAndNotify]; // Needs more data.
return;
}
// Fall through to parse remaining data with mime parser
}
}
doc = [parser mimeDocument];
method = [[doc headerNamed: @"x-http-method"] value];
if ([session moreBytes: [d length]] > _maxRequestSize)
{
[self _alert: @"Request body too long ... rejected"];
[session setShouldEnd: YES]; // Not persistent.
[hdl writeInBackgroundAndNotify:
[@"HTTP/1.0 500 Request body too long\r\n\r\n"
dataUsingEncoding: NSASCIIStringEncoding]];
return;
}
else if ([parser parse: d] == NO)
{
if ([parser isComplete] == YES)
{
[self _process: session];
}
else
{
[self _alert: @"HTTP parse failure - %@", parser];
[self _endSession: session];
}
}
else if (([parser isComplete] == YES)
|| ([parser isInHeaders] == NO && ([method isEqualToString: @"GET"])))
{
[self _process: session];
}
else
{
[hdl readInBackgroundAndNotify];
}
}
- (void) _didWrite: (NSNotification*)notification
{
NSFileHandle *hdl = [notification object];
WebServerSession *session;
session = (WebServerSession*)NSMapGet(_sessions, (void*)hdl);
NSAssert(session != nil, NSInternalInconsistencyException);
if ([session shouldEnd] == YES)
{
[self _endSession: session];
}
else
{
if (_verbose == YES && [_quiet containsObject: [session address]] == NO)
{
NSLog(@"%@ reset", session);
}
[session reset];
[hdl readInBackgroundAndNotify]; // Want another request.
}
}
- (void) _endSession: (WebServerSession*)session
{
NSFileHandle *hdl = [session handle];
if ([_quiet containsObject: [session address]] == NO)
{
if (_verbose == YES)
{
NSLog(@"%@ disconnect", session);
}
_handled++;
}
[_nc removeObserver: self
name: NSFileHandleReadCompletionNotification
object: hdl];
[_nc removeObserver: self
name: GSFileHandleWriteCompletionNotification
object: hdl];
[_perHost removeObject: [session address]];
NSMapRemove(_sessions, (void*)hdl);
if (_accepting == NO
&& (_maxSessions <= 0 || NSCountMapTable(_sessions) < _maxSessions))
{
[_listener acceptConnectionInBackgroundAndNotify];
_accepting = YES;
}
}
- (void) _process: (WebServerSession*)session
{
GSMimeDocument *request;
GSMimeDocument *response;
BOOL responded = NO;
NSString *con;
NSMutableData *raw;
NSMutableData *out;
unsigned char *buf;
unsigned int len;
unsigned int pos;
unsigned int contentLength;
NSEnumerator *enumerator;
GSMimeHeader *hdr;
AUTORELEASE(RETAIN(session));
request = [[session parser] mimeDocument];
/*
* If the client specified that the connection should close, we don't
* keep it open.
*/
con = [[request headerNamed: @"connection"] value];
if (con != nil && [con caseInsensitiveCompare: @"close"] == NSOrderedSame)
{
[session setShouldEnd: YES]; // Not persistent.
}
/*
* Provide more information about the connection.
*/
[request setHeader: @"x-local-address"
value: [[session handle] socketLocalAddress]
parameters: nil];
[request setHeader: @"x-local-port"
value: [[session handle] socketLocalService]
parameters: nil];
[request setHeader: @"x-remote-address"
value: [[session handle] socketAddress]
parameters: nil];
[request setHeader: @"x-remote-port"
value: [[session handle] socketService]
parameters: nil];
response = AUTORELEASE([GSMimeDocument new]);
[response setContent: [NSData data] type: @"text/plain" name: nil];
if ([_quiet containsObject: [session address]] == NO)
{
_requests++;
if (_verbose == YES)
{
NSLog(@"Request %@ - %@", session, request);
}
}
NS_DURING
{
[session setProcessing: YES];
[session setTicked: _ticked];
responded = [_delegate processRequest: request
response: response
for: self];
_ticked = [NSDate timeIntervalSinceReferenceDate];
[session setTicked: _ticked];
[session setProcessing: NO];
}
NS_HANDLER
{
[session setProcessing: NO];
[self _alert: @"Exception %@, processing %@", localException, request];
[response setHeader: @"http"
value: @"HTTP/1.0 500 Internal Server Error"
parameters: nil];
}
NS_ENDHANDLER
[response setHeader: @"content-transfer-encoding"
value: @"binary"
parameters: nil];
raw = [response rawMimeData];
buf = [raw mutableBytes];
len = [raw length];
for (pos = 4; pos < len; pos++)
{
if (strncmp(&buf[pos-4], "\r\n\r\n", 4) == 0)
{
break;
}
}
contentLength = len - pos;
pos -= 2;
[raw replaceBytesInRange: NSMakeRange(0, pos) withBytes: 0 length: 0];
out = [NSMutableData dataWithCapacity: len + 1024];
[response deleteHeaderNamed: @"mime-version"];
[response deleteHeaderNamed: @"content-length"];
[response deleteHeaderNamed: @"content-encoding"];
[response deleteHeaderNamed: @"content-transfer-encoding"];
if (contentLength > 0)
{
NSString *str;
str = [NSString stringWithFormat: @"%u", contentLength];
[response setHeader: @"content-length" value: str parameters: nil];
}
else
{
[response deleteHeaderNamed: @"content-type"];
}
hdr = [response headerNamed: @"http"];
if (hdr == nil)
{
const char *s;
if (contentLength == 0)
{
s = "HTTP/1.1 204 No Content\r\n";
}
else
{
s = "HTTP/1.1 200 Success\r\n";
}
[out appendBytes: s length: strlen(s)];
}
else
{
NSString *s = [[hdr value] stringByTrimmingSpaces];
s = [s stringByAppendingString: @"\r\n"];
[out appendData: [s dataUsingEncoding: NSASCIIStringEncoding]];
[response deleteHeader: hdr];
/*
* If the http version has been set to be an old one,
* we must be prepared to close the connection at once.
*/
if ([s hasPrefix: @"HTTP/"] == NO
|| [[s substringFromIndex: 5] floatValue] < 1.1)
{
[session setShouldEnd: YES];
}
}
enumerator = [[response allHeaders] objectEnumerator];
while ((hdr = [enumerator nextObject]) != nil)
{
[out appendData: [hdr rawMimeData]];
}
if ([raw length] > 0)
{
[out appendData: raw];
}
else
{
[out appendBytes: "\r\n" length: 2]; // Terminate headers
}
if (_verbose == YES && [_quiet containsObject: [session address]] == NO)
{
NSLog(@"Response %@ - %@", session, out);
}
[[session handle] writeInBackgroundAndNotify: out];
}
- (void) _timeout: (NSTimer*)timer
{
unsigned count;
_ticked = [NSDate timeIntervalSinceReferenceDate];
count = NSCountMapTable(_sessions);
if (count > 0)
{
NSMapEnumerator enumerator;
WebServerSession *session;
NSFileHandle *handle;
NSMutableArray *array;
array = [NSMutableArray arrayWithCapacity: count];
enumerator = NSEnumerateMapTable(_sessions);
while (NSNextMapEnumeratorPair(&enumerator,
(void **)(&handle), (void**)(&session)))
{
if (_ticked - [session ticked] > _sessionTimeout
&& [session processing] == NO)
{
[array addObject: session];
}
}
NSEndMapTableEnumeration(&enumerator);
while ([array count] > 0)
{
session = [array objectAtIndex: 0];
[self _alert: @"Session timed out - %@", session];
[self _endSession: session];
[array removeObjectAtIndex: 0];
}
}
}
@end