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
This commit is contained in:
CaS 2005-03-02 10:00:24 +00:00
parent 5474c4bba8
commit 221624ef4c
5 changed files with 406 additions and 5 deletions

View file

@ -1,3 +1,12 @@
2005-03-02 Richard Frith-Macdonald <rfm@gnu.org>
* 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 <fedor@gnu.org>
* Version 1.1.0:

View file

@ -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.<br />
* 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.<br />
* 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.<br />
* 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.<br />
* 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.<br />
* 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.<br />
* 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.<br />
* If seconds is zero, the cache for this query is emptied.<br />
* Handles locking.<br />
* 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

View file

@ -42,6 +42,7 @@
#include <Foundation/NSNull.h>
#include <Foundation/NSDebug.h>
#include <Foundation/NSPathUtilities.h>
#include <Foundation/NSSet.h>
#include <GNUstepBase/GSLock.h>
@ -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
{

View file

@ -54,6 +54,7 @@
<item>Parsing of form encoded data in a POST request</item>
<item>Substitution into template pages on output</item>
<item>SSL support</item>
<item>HTTP Basic authentication</item>
<item>Limit access by IP address</item>
<item>Limit total number of simultaneous connections</item>
<item>Limit number of simultaneous connectionsform one address</item>
@ -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).<br />
* The method returns YES if access is granted, or returns NO and sets the
* appropriate response values if access is refused.<br />
* 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.<br />
* The access control is managed by the <code>WebServerAccess</code>
* 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.<br />
* Each access control dictionary contains an authentication realm string
* (keyed on <em>Realm</em>) and a dictionary containing username/password
* pairs (keyed on <em>Users</em>) or a dictionary containing information
* to perform a database lookup of username and password
* (keyed on <em>UserDB</em>).<br />
* eg.
* <example>
* 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;
* };
* };
* };
* </example>
*/
- (BOOL) accessRequest: (GSMimeDocument*)request
response: (GSMimeDocument*)response;
/**
* Decode an application/x-www-form-urlencoded form and store its
* contents into the supplied dictionary.<br />

View file

@ -25,6 +25,7 @@
#include <Foundation/Foundation.h>
#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:
@"<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n"
@"<html><head><title>401 Authorization Required</title></head><body>\n"
@"<h1>Authorization Required</h1>\n"
@"<p>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.</p>\n"
@"</body></html>\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];