From 221624ef4c64a11721cce1e03bee48d78ad27dc9 Mon Sep 17 00:00:00 2001 From: CaS Date: Wed, 2 Mar 2005 10:00:24 +0000 Subject: [PATCH] experimental updates for performance and security git-svn-id: svn+ssh://svn.gna.org/svn/gnustep/libs/sqlclient/trunk@20826 72102866-910b-0410-8b05-ffd578937521 --- ChangeLog | 9 +++ SQLClient.h | 56 +++++++++++++++++- SQLClient.m | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++++ WebServer.h | 47 +++++++++++++++ WebServer.m | 139 +++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 406 insertions(+), 5 deletions(-) diff --git a/ChangeLog b/ChangeLog index 044320d..e692277 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,12 @@ +2005-03-02 Richard Frith-Macdonald + + * WebServer.[hm]: Add support for basic http authentication either + via username/password pairs in property list or in database table. + * SQLClient.[hm]: Add methods to query database with local caching + of results, for use on systems needing high performance, where + database query (and/or database client-server comms) overheads are + important. + 2005-02-25 Adam Fedor * Version 1.1.0: diff --git a/SQLClient.h b/SQLClient.h index ac03d15..cf40121 100644 --- a/SQLClient.h +++ b/SQLClient.h @@ -177,6 +177,7 @@ @class NSData; @class NSDate; +@class NSMutableSet; @class NSRecursiveLock; @class NSString; @class SQLTransaction; @@ -264,11 +265,12 @@ extern NSTimeInterval SQLClientTimeNow(); NSMutableArray *_statements; /** Uncommitted statements */ /** * Timestamp of last operation.
- * Maintained by the -simpleExecute: and -simpleQuery: methods. + * Maintained by -simpleExecute: -simpleQuery: -cache:simpleQuery: */ NSTimeInterval _lastOperation; NSTimeInterval _duration; unsigned int _debugging; /** The current debugging level */ + NSMutableSet *_cache; } /** @@ -906,6 +908,58 @@ extern NSTimeInterval SQLClientTimeNow(); @end +/** + * This category porovides methods for caching the results of queries + * in order to reduce the number of client-server trips and the database + * load produced by an application which needs update its information + * from the database frequently. + */ +@interface SQLClient (Caching) + +/** + * If the result of the query is already cached and is still valid, + * return it. Otherwise, perform the query and cache the result + * giving it the specified lifetime in seconds.
+ * If seconds is negative, the query is performed irrespective of + * whether it is already cached, and its absolute value is used to + * set the lifetime of the results.
+ * If seconds is zero, the cache for this query is emptied. + */ +- (NSMutableArray*) cache: (int)seconds + query: (NSString*)stmt,...; + +/** + * If the result of the query is already cached and is still valid, + * return it. Otherwise, perform the query and cache the result + * giving it the specified lifetime in seconds.
+ * If seconds is negative, the query is performed irrespective of + * whether it is already cached, and its absolute value is used to + * set the lifetime of the results.
+ * If seconds is zero, the cache for this query is emptied. + */ +- (NSMutableArray*) cache: (int)seconds + query: (NSString*)stmt + with: (NSDictionary*)values; + +/** + * If the result of the query is already cached and is still valid, + * return it. Otherwise, perform the query and cache the result + * giving it the specified lifetime in seconds.
+ * If seconds is negative, the query is performed irrespective of + * whether it is already cached, and its absolute value is used to + * set the lifetime of the results.
+ * If seconds is zero, the cache for this query is emptied.
+ * Handles locking.
+ * Maintains -lastOperation date. + */ +- (NSMutableArray*) cache: (int)seconds simpleQuery: (NSString*)stmt; + +/** + * Purge any expired items from the cache. + */ +- (void) cachePurge; +@end + /** * The SQLTransaction transaction class provides a convenient mechanism * for grouping together a series of SQL statements to be executed as a diff --git a/SQLClient.m b/SQLClient.m index a9ad8a7..357ff77 100644 --- a/SQLClient.m +++ b/SQLClient.m @@ -42,6 +42,7 @@ #include #include #include +#include #include @@ -558,6 +559,7 @@ static unsigned int maxConnections = 8; DESTROY(_user); DESTROY(_name); DESTROY(_statements); + DESTROY(_cache); [super dealloc]; } @@ -1632,6 +1634,164 @@ static void quoteString(NSMutableString *s) } @end + + +@interface SQLClientCacheInfo : NSObject +{ +@public + NSString *query; + NSMutableArray *result; + NSTimeInterval expires; +} +@end + +@implementation SQLClientCacheInfo +- (void) dealloc +{ + DESTROY(query); + DESTROY(result); + [super dealloc]; +} +- (unsigned) hash +{ + return [query hash]; +} +- (BOOL) isEqual: (SQLClientCacheInfo*)other +{ + return [query isEqual: other->query]; +} +@end + +@implementation SQLClient (Caching) +- (NSMutableArray*) cache: (int)seconds + query: (NSString*)stmt,... +{ + va_list ap; + + va_start (ap, stmt); + stmt = [[self _prepare: stmt args: ap] objectAtIndex: 0]; + va_end (ap); + + return [self cache: seconds simpleQuery: stmt]; +} + +- (NSMutableArray*) cache: (int)seconds + query: (NSString*)stmt + with: (NSDictionary*)values +{ + stmt = [[self _substitute: stmt with: values] objectAtIndex: 0]; + return [self cache: seconds simpleQuery: stmt]; +} + +- (NSMutableArray*) cache: (int)seconds simpleQuery: (NSString*)stmt +{ + NSMutableArray *result = nil; + + [lock lock]; + NS_DURING + { + NSTimeInterval start; + SQLClientCacheInfo *item; + SQLClientCacheInfo *old; + + item = AUTORELEASE([SQLClientCacheInfo new]); + item->query = [stmt copy]; + old = [_cache member: item]; + start = SQLClientTimeNow(); + if (seconds > 0 && old != nil && old->expires > start) + { + // Cached item still valid ... just use it. + result = old->result; + } + else + { + result = [self backendQuery: stmt]; + _lastOperation = SQLClientTimeNow(); + if (_duration >= 0) + { + NSTimeInterval d; + + d = _lastOperation - start; + if (d >= _duration) + { + [self debug: @"Duration %g for query %@", d, stmt]; + } + } + if (seconds < 0) + { + seconds = -seconds; + } + if (old != nil) + { + if (seconds == 0) + { + // We hve been tod to remove the existing cached item. + [_cache removeObject: old]; + } + else + { + // We read a new value ... store it in cache + ASSIGN(old->result, result); + old->expires = _lastOperation + seconds; + } + } + else + { + /* + * Unless removing from cache (seconds == 0) we must + * add the new item to the cache with the appropriate + * expiry. + */ + if (seconds > 0) + { + ASSIGN(item->result, result); + item->expires = _lastOperation + seconds; + if (_cache == nil) + { + _cache = [NSMutableSet new]; + } + [_cache addObject: item]; + } + } + } + /* + * Return an autoreleased copy ... not the original cached data. + */ + result = [NSMutableArray arrayWithArray: result]; + } + NS_HANDLER + { + [lock unlock]; + [localException raise]; + } + NS_ENDHANDLER + [lock unlock]; + return result; +} + +- (void) cachePurge +{ + NSEnumerator *e; + + [lock lock]; + e = [_cache objectEnumerator]; + if (e != nil) + { + NSTimeInterval start = SQLClientTimeNow(); + SQLClientCacheInfo *item; + + while ((item = [e nextObject]) != nil) + { + if (item->expires < start) + { + [_cache removeObject: item]; + } + } + } + [lock unlock]; +} +@end + @implementation SQLTransaction - (void) dealloc { diff --git a/WebServer.h b/WebServer.h index b198578..83ea16b 100644 --- a/WebServer.h +++ b/WebServer.h @@ -54,6 +54,7 @@ Parsing of form encoded data in a POST request Substitution into template pages on output SSL support + HTTP Basic authentication Limit access by IP address Limit total number of simultaneous connections Limit number of simultaneous connectionsform one address @@ -201,6 +202,52 @@ NSCountedSet *_perHost; } +/** + * This method is called for each incoming request, and checks that the + * requested resource is accessible (basic user/password access control).
+ * The method returns YES if access is granted, or returns NO and sets the + * appropriate response values if access is refused.
+ * If access is refused by this method, the delegate is not informed of the + * request at all ... so this forms an initial access control mechanism, + * but if it is passed, the delegate is still free to implement its own + * additional access control within the + * [(WebServerDelegate)-processRequest:response:for:] method.
+ * The access control is managed by the WebServerAccess + * user default, which is a dictionary whose keys are paths, and whose + * values are dictionaries specifying the access control for those paths. + * Access control is done on the basis of the longest matching path.
+ * Each access control dictionary contains an authentication realm string + * (keyed on Realm) and a dictionary containing username/password + * pairs (keyed on Users) or a dictionary containing information + * to perform a database lookup of username and password + * (keyed on UserDB).
+ * eg. + * + * WebServerAccess = { + * "" = { + * Realm = "general"; + * Users = { + * Fred = 1942; + * }; + * }; + * "/private" = { + * Realm = "private"; + * UserDB = { + * // System will contact database using SQLClient and lookup password + * // The SQLClient library must be linked in and used by the tool + * // using WebServer ... it is not linked in by the WebServer library. + * Name = databasename; + * Table = tablename; + * UsernameField = fielname1; + * PasswordField = fielname2; + * }; + * }; + * }; + * + */ +- (BOOL) accessRequest: (GSMimeDocument*)request + response: (GSMimeDocument*)response; + /** * Decode an application/x-www-form-urlencoded form and store its * contents into the supplied dictionary.
diff --git a/WebServer.m b/WebServer.m index 7f201fb..911cfcd 100644 --- a/WebServer.m +++ b/WebServer.m @@ -25,6 +25,7 @@ #include #include "WebServer.h" +#include "SQLClient.h" @interface WebServerSession : NSObject { @@ -176,6 +177,134 @@ @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) + { + stored = [[access objectForKey: @"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 = [c 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) @@ -1312,7 +1441,6 @@ unescapeData(const unsigned char* bytes, unsigned length, unsigned char *buf) { GSMimeDocument *request; GSMimeDocument *response; - BOOL responded = NO; NSString *str; NSString *con; NSMutableData *raw; @@ -1390,9 +1518,12 @@ unescapeData(const unsigned char* bytes, unsigned length, unsigned char *buf) { [session setProcessing: YES]; [session setTicked: _ticked]; - responded = [_delegate processRequest: request - response: response - for: self]; + if ([self accessRequest: request response: response] == YES) + { + [_delegate processRequest: request + response: response + for: self]; + } _ticked = [NSDate timeIntervalSinceReferenceDate]; [session setTicked: _ticked]; [session setProcessing: NO];