diff --git a/ChangeLog b/ChangeLog index 1a2aab1fc..93af3f4a9 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,12 @@ +2002-06-16 Richard Frith-Macdonald + + * Source/GSFTPURLHandle.m: partial implementation added (compiles + but does not work). + * Source/NSURLHandle.m: Register GSFTPURLHandle so once it works + it can be used. + * Source/GSHTTPURLHandle.m: modified so that data being read is + reported periodically during the read proces rather than just at end. + 2002-06-15 Adam Fedor * Source/NSProcessInfo.m: Include sys/fcntl for Solaris. diff --git a/Source/GNUmakefile b/Source/GNUmakefile index a3f865dc5..0c86ad5bd 100644 --- a/Source/GNUmakefile +++ b/Source/GNUmakefile @@ -142,6 +142,7 @@ GSAttributedString.m \ GSCountedSet.m \ GSDictionary.m \ GSFormat.m \ +GSFTPURLHandle.m \ GSHTTPURLHandle.m \ GSSet.m \ GSString.m \ diff --git a/Source/GSFTPURLHandle.m b/Source/GSFTPURLHandle.m new file mode 100644 index 000000000..80894f266 --- /dev/null +++ b/Source/GSFTPURLHandle.m @@ -0,0 +1,809 @@ +/** GSFTPURLHandle.m - Class GSFTPURLHandle + Copyright (C) 2002 Free Software Foundation, Inc. + + Written by: Richard Frith-Macdonald + Date: June 2002 + + This file is part of the GNUstep 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. +*/ + +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +GS_EXPORT NSString * const GSTelnetNotification; +GS_EXPORT NSString * const GSTelnetErrorKey; +GS_EXPORT NSString * const GSTelnetTextKey; + +@interface GSTelnetHandle : NSObject +{ + NSStringEncoding enc; + NSFileHandle *remote; + NSMutableData *ibuf; + unsigned pos; + BOOL lineMode; + BOOL connected; +} +- (id) initWithHandle: (NSFileHandle*)handle isConnected: (BOOL)flag; +- (void) putTelnetLine: (NSString*)s; +- (void) putTelnetText: (NSString*)s; +- (void) setEncoding: (NSStringEncoding)e; +- (void) setLineMode: (BOOL)flag; +@end + + + +@interface GSTelnetHandle (Private) +- (void) _didConnect: (NSNotification*)notification; +- (void) _didRead: (NSNotification*)notification; +- (void) _didWrite: (NSNotification*)notification; +@end + +NSString * const GSTelnetNotification = @"GSTelnetNotification"; +NSString * const GSTelnetErrorKey = @"GSTelnetErrorKey"; +NSString * const GSTelnetTextKey = @"GSTelnetTextKey"; + +@implementation GSTelnetHandle + +#define WILL 251 +#define WONT 252 +#define DO 253 +#define DONT 254 +#define IAC 255 + +- (void) dealloc +{ + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + + [nc removeObserver: self]; + RELEASE(remote); + RELEASE(ibuf); + [super dealloc]; +} + +- (id) init +{ + return [self initWithHandle: nil isConnected: NO]; +} + +- (id) initWithHandle: (NSFileHandle*)handle isConnected: (BOOL)flag +{ + if (handle == nil) + { + DESTROY(self); + } + else + { + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + + connected = flag; + enc = NSUTF8StringEncoding; + ibuf = [NSMutableData new]; + remote = RETAIN(handle); + if (connected == YES) + { + [nc addObserver: self + selector: @selector(_didRead:) + name: NSFileHandleReadCompletionNotification + object: remote]; + [nc addObserver: self + selector: @selector(_didWrite:) + name: GSFileHandleWriteCompletionNotification + object: remote]; + [remote readInBackgroundAndNotify]; + } + else + { + [nc addObserver: self + selector: @selector(_didConnect:) + name: GSFileHandleConnectCompletionNotification + object: remote]; + } + } + return self; +} + +- (void) putTelnetLine: (NSString*)s +{ + if ([s hasSuffix: @"\n"] == NO) + { + s = [s stringByAppendingString: @"\r\n"]; + } + [self putTelnetText: s]; +} + +- (void) putTelnetText: (NSString*)s +{ + NSMutableData *md; + unsigned char *to; + NSData *d = [s dataUsingEncoding: enc]; + unsigned char *from = (unsigned char *)[d bytes]; + unsigned int len = [d length]; + unsigned int i = 0; + unsigned int count = 0; + + for (i = 0; i < len; i++) + { + if (from[i] == IAC) + { + count++; + } + } + + md = [[NSMutableData alloc] initWithLength: len + count]; + to = [md mutableBytes]; + for (i = 0; i < len; i++) + { + if (*from == IAC) + { + *to++ = IAC; + } + *to++ = *from++; + } +//NSLog(@"Write - '%*.*s'", [md length], [md length], [md bytes]); + [remote writeInBackgroundAndNotify: md]; + DESTROY(md); +} + +/** + * Set the string encoding used to convert strings to be sent to the + * remote system into raw data, and to convert incoming data from that + * system inot input text. + */ +- (void) setEncoding: (NSStringEncoding)e +{ + enc = e; +} + +/** + * Sets a flag to say whether observers are to be notified of incoming + * data after every chunk read, or only when one or more entire lines + * are read.
+ * When switching out of line mode, this will cause a notification to be + * generated if there is any buffered data available for use. + */ +- (void) setLineMode: (BOOL)flag +{ + if (lineMode != flag) + { + lineMode = flag; + if (lineMode == NO) + { + [self _didRead: nil]; + } + } +} + +@end + +@implementation GSTelnetHandle (Private) +- (void) _didConnect: (NSNotification*)notification +{ + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + NSDictionary *info = [notification userInfo]; + NSString *e; + + e = [info objectForKey: GSFileHandleNotificationError]; + if (e == nil) + { + [nc removeObserver: self + name: GSFileHandleConnectCompletionNotification + object: [notification object]]; + [nc addObserver: self + selector: @selector(_didRead:) + name: NSFileHandleReadCompletionNotification + object: remote]; + [nc addObserver: self + selector: @selector(_didWrite:) + name: GSFileHandleWriteCompletionNotification + object: remote]; + [remote readInBackgroundAndNotify]; + } + else + { + info = [NSDictionary dictionaryWithObject: e + forKey: GSTelnetErrorKey]; + [nc postNotificationName: GSTelnetNotification + object: self + userInfo: info]; + } +} + +- (void) _didRead: (NSNotification*)notification +{ + NSDictionary *userInfo = [notification userInfo]; + NSMutableArray *text = nil; + NSData *d; + + d = [userInfo objectForKey: NSFileHandleNotificationDataItem]; + /* + * If the notification is nil, this method has been called to flush + * any buffered data out. + */ + if (notification != nil && (d == nil || [d length] == 0)) + { + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + NSDictionary *info; + + info = [NSDictionary dictionaryWithObject: @"EOF" + forKey: GSTelnetErrorKey]; + [nc postNotificationName: GSTelnetNotification + object: self + userInfo: info]; + } + else + { + NSMutableData *toWrite = nil; + unsigned char *ptr; + unsigned char c; + unsigned int s = 0; + unsigned int old; + unsigned int len; + int i; // May be negative. + + if (d != nil) + { +//NSLog(@"Read - '%@'", d); + [ibuf appendData: d]; + } + old = len = [ibuf length]; + ptr = [ibuf mutableBytes]; + + for (i = pos; i < len; i++) + { + NSData *line = nil; + + c = ptr[i]; + if (c == IAC) + { + if (i < len - 1) + { + c = ptr[i+1]; + if (c == WILL || c == WONT || c == DO || c == DONT) + { + /* + * refuse any negotiation attempts. + */ + if (c == WILL || c == DO) + { + unsigned char opt[3]; + + if (toWrite == nil) + { + toWrite = [NSMutableData alloc]; + toWrite = [toWrite initWithCapacity: 12]; + } + opt[0] = IAC; + if (c == DO) + { + opt[1] = WONT; + } + else + { + opt[1] = DONT; + } + opt[2] = ptr[i+2]; + [toWrite appendBytes: opt length: 3]; + } + if (i < len - 2) + { +NSLog(@"Command: %d %d", ptr[1], ptr[2]); + len -= 3; + if (len - i > 0) + { + memcpy(ptr, &ptr[3], len - i); + } + i--; // Try again. + } + else + { + i--; + break; // Need more data + } + } + else if (c == IAC) // Escaped IAC + { + len--; + if (len - i > 0) + { + memcpy(ptr, &ptr[1], len - i); + } + } + else + { +NSLog(@"Command: %d", ptr[1]); + /* + * Strip unimplemented escape sequence. + */ + len -= 2; + if (len - i > 0) + { + memcpy(ptr, &ptr[2], len - i); + } + i--; // Try again from here. + } + } + else + { + i--; + break; // Need more data + } + } + else if (c == '\r' && i < len - 1 && ptr[i+1] == '\n') + { + line = [[NSData alloc] initWithBytes: &ptr[s] length: i-s+2]; + i++; + s = i + 1; + } + else if (c == '\n') + { + line = [[NSData alloc] initWithBytes: &ptr[s] length: i-s+1]; + s = i + 1; + } + if (line != nil) + { + NSString *lineString; + + lineString = [[NSString alloc] initWithData: line encoding: enc]; + DESTROY(line); + if (text == nil) + { + text = [[NSMutableArray alloc] initWithCapacity: 4]; + } + [text addObject: lineString]; + DESTROY(lineString); + } + } + pos = i; + + /* + * If not in line mode, we can add the remainder of the data to + * the array of strings for notification. + */ + if (lineMode == NO && s != pos) + { + NSString *lineString; + NSData *line; + + line = [[NSData alloc] initWithBytes: &ptr[s] length: pos - s]; + s = pos; + lineString = [[NSString alloc] initWithData: line encoding: enc]; + DESTROY(line); + if (text == nil) + { + text = [[NSMutableArray alloc] initWithCapacity: 4]; + } + [text addObject: lineString]; + DESTROY(lineString); + } + + /* + * Adjust size of data remaining in buffer if necessary. + */ + if (old != len || s > 0) + { + if (s > 0) + { + len -= s; + pos -= s; + if (len > 0) + { + memcpy(ptr, &ptr[s], len); + } + } + [ibuf setLength: len]; + } + + /* + * Send telnet protocol negotion info if necessary. + */ + if (toWrite != nil) + { +//NSLog(@"Write - '%@'", toWrite); + [remote writeInBackgroundAndNotify: toWrite]; + DESTROY(toWrite); + } + + /* + * Send notification for text read as necessary. + */ + if (text != nil) + { + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + NSNotification *n; + NSDictionary *info; + + info = [NSDictionary dictionaryWithObject: text + forKey: GSTelnetTextKey]; + DESTROY(text); + n = [NSNotification notificationWithName: GSTelnetNotification + object: self + userInfo: info]; + [nc postNotification: n]; + } + [remote readInBackgroundAndNotify]; + } +} + +- (void) _didWrite: (NSNotification*)notification +{ + NSDictionary *userInfo = [notification userInfo]; + NSString *e; + + e = [userInfo objectForKey: GSFileHandleNotificationError]; + if (e) + { + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + NSDictionary *info; + + info = [NSDictionary dictionaryWithObject: e + forKey: GSTelnetErrorKey]; + [nc postNotificationName: GSTelnetNotification + object: self + userInfo: info]; + } +} +@end + + + +@interface GSFTPURLHandle : NSURLHandle +{ + GSTelnetHandle *cHandle; + NSFileHandle *dHandle; + NSURL *url; + NSData *wData; + enum { + idle, + cConnect, // Establishing control connection + sentUser, // Sent username + sentPass, // Sent password + sentType, // Sent data type + sentPasv, // Requesting host/port information for data link + data, // Establishing or using data connection + } state; +} +- (void) _control: (NSNotification*)n; +- (void) _data: (NSNotification*)n; +@end + +/** + *

+ * This is a PRIVATE subclass of NSURLHandle. + * It is documented here in order to give you information about the + * default behavior of an NSURLHandle created to deal with a URL + * that has the ftp scheme. + * The name and/or other implementation details of this class + * may be changed at any time. + *

+ *

+ * A GSFTPURLHandle instance is used to manage connections to + * ftp URLs. + *

+ */ +@implementation GSFTPURLHandle + +static NSMutableDictionary *urlCache = nil; +static NSLock *urlLock = nil; + ++ (NSURLHandle*) cachedHandleForURL: (NSURL*)newUrl +{ + NSURLHandle *obj = nil; + + if ([[newUrl scheme] caseInsensitiveCompare: @"ftp"] == NSOrderedSame) + { + NSString *page = [newUrl absoluteString]; + //NSLog(@"Lookup for handle for '%@'", page); + [urlLock lock]; + obj = [urlCache objectForKey: page]; + AUTORELEASE(RETAIN(obj)); + [urlLock unlock]; + //NSLog(@"Found handle %@", obj); + } + return obj; +} + ++ (void) initialize +{ + if (self == [GSFTPURLHandle class]) + { + urlCache = [NSMutableDictionary new]; + urlLock = [NSLock new]; + } +} + +- (void) dealloc +{ + if (state != idle) + { + [self endLoadInBackground]; + } + RELEASE(url); + RELEASE(wData); + [super dealloc]; +} + +- (id) initWithURL: (NSURL*)newUrl + cached: (BOOL)cached +{ + if ((self = [super initWithURL: newUrl cached: cached]) != nil) + { + ASSIGN(url, newUrl); + state = idle; + if (cached == YES) + { + NSString *page = [newUrl absoluteString]; + + [urlLock lock]; + [urlCache setObject: self forKey: page]; + [urlLock unlock]; + //NSLog(@"Cache handle %@ for '%@'", self, page); + } + } + return self; +} + ++ (BOOL) canInitWithURL: (NSURL*)newUrl +{ + if ([[newUrl scheme] isEqualToString: @"ftp"] == YES) + { + return YES; + } + return NO; +} + +- (void) _control: (NSNotification*)n +{ + NSDictionary *info = [n userInfo]; + NSString *e = [info objectForKey: GSTelnetErrorKey]; + NSArray *text; + + if (e == nil) + { + /* + * Tell superclass that the load failed - let it do housekeeping. + */ + [self endLoadInBackground]; + [self backgroundLoadDidFailWithReason: e]; + return; + } + text = [info objectForKey: GSTelnetTextKey]; +NSLog(@"Ctl: %@", text); + if (state == cConnect) + { + NSString *user = [url user]; + + if (user == nil) + { + user = @"anonymous"; + } + [cHandle putTelnetLine: [@"USER " stringByAppendingString: user]]; + state = sentUser; + } + else if (state == sentUser) + { + NSString *pass = [url password]; + + if (pass == nil) + { + pass = [url user]; + if (pass == nil) + { + pass = @"GNUstep@here"; + } + else + { + pass = @""; + } + } + [cHandle putTelnetLine: [@"PASS " stringByAppendingString: pass]]; + state = sentPass; + } + else if (state == sentPass) + { + [cHandle putTelnetLine: @"TYPE I"]; + state = sentType; + } + else if (state == sentType) + { + [cHandle putTelnetLine: @"PASV"]; + state = sentPasv; + } + else if (state == sentPasv) + { + } +} + +- (void) _data: (NSNotification*)n +{ + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + NSString *name = [n name]; + NSDictionary *info = [n userInfo]; + NSString *e = [info objectForKey: GSFileHandleNotificationError]; + + [nc removeObserver: self name: name object: dHandle]; + + /* + * See if the connection attempt caused an error. + */ + if (e != nil) + { + if ([name isEqualToString: GSFileHandleConnectCompletionNotification]) + { + NSLog(@"Unable to connect to %@:%@ via socket", + [dHandle socketAddress], [dHandle socketService]); + } + /* + * Tell superclass that the load failed - let it do housekeeping. + */ + [self endLoadInBackground]; + [self backgroundLoadDidFailWithReason: e]; + return; + } + if ([name isEqualToString: GSFileHandleConnectCompletionNotification]) + { + if (wData == nil) + { + [nc addObserver: self + selector: @selector(_data:) + name: NSFileHandleReadCompletionNotification + object: dHandle]; + [dHandle readInBackgroundAndNotify]; + } + else + { + [nc addObserver: self + selector: @selector(_data:) + name: GSFileHandleWriteCompletionNotification + object: dHandle]; + [dHandle writeInBackgroundAndNotify: wData]; + } + } + else + { + if (wData == nil) + { + NSData *d; + + d = [info objectForKey: NSFileHandleNotificationDataItem]; + if ([d length] > 0) + { + [self didLoadBytes: d loadComplete: NO]; + [nc addObserver: self + selector: @selector(_data:) + name: NSFileHandleReadCompletionNotification + object: dHandle]; + [dHandle readInBackgroundAndNotify]; + } + else + { + [self didLoadBytes: nil loadComplete: YES]; + [self endLoadInBackground]; + } + } + else + { + /* + * Tell superclass that we have successfully loaded the data. + */ + [self didLoadBytes: wData loadComplete: YES]; + DESTROY(wData); + } + } +} + +- (void) endLoadInBackground +{ + if (state != idle) + { + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + + if (dHandle != nil) + { + [nc removeObserver: self name: nil object: dHandle]; + [dHandle closeFile]; + DESTROY(dHandle); + } + [nc removeObserver: self name: GSTelnetNotification object: cHandle]; + DESTROY(cHandle); + state = idle; + } + [super endLoadInBackground]; +} + +- (void) loadInBackground +{ + NSNotificationCenter *nc; + NSString *host = nil; + NSString *port = nil; + NSNumber *p; + NSFileHandle *sock; + + /* + * Don't start a load if one is in progress. + */ + if (state != idle) + { + NSLog(@"Attempt to load an ftp handle which is not idle ... ignored"); + return; + } + + [self beginLoadInBackground]; + if (dHandle != nil) + { + [dHandle closeFile]; + DESTROY(dHandle); + } + + host = [url host]; + p = [url port]; + if (p != nil) + { + port = [NSString stringWithFormat: @"%u", [p unsignedIntValue]]; + } + else + { + port = [url scheme]; + } + sock = [NSFileHandle fileHandleAsClientInBackgroundAtAddress: host + service: port + protocol: @"tcp"]; + if (sock == nil) + { + /* + * Tell superclass that the load failed - let it do housekeeping. + */ + [self backgroundLoadDidFailWithReason: [NSString stringWithFormat: + @"Unable to connect to %@:%@", host, port]]; + return; + } + cHandle = [[GSTelnetHandle alloc] initWithHandle: sock isConnected: NO]; + nc = [NSNotificationCenter defaultCenter]; + [nc addObserver: self + selector: @selector(_control:) + name: GSTelnetNotification + object: cHandle]; + state = cConnect; +} + +/** + * Writes the specified data as the ftp file. + * Returns YES on success, + * NO on failure. + * On completion, the resource data for this handle is set to the + * data written. + */ +- (BOOL) writeData: (NSData*)d +{ + ASSIGN(wData, d); + return YES; +} + +@end + diff --git a/Source/GSHTTPURLHandle.m b/Source/GSHTTPURLHandle.m index 60d575499..291c6ad6f 100644 --- a/Source/GSHTTPURLHandle.m +++ b/Source/GSHTTPURLHandle.m @@ -70,7 +70,7 @@ char emp[64] = { NSMutableDictionary *wProperties; NSData *wData; NSMutableDictionary *request; - unsigned int contentLength; + unsigned int bodyPos; enum { idle, connecting, @@ -294,6 +294,7 @@ static void debugWrite(NSData *data) NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; NSDictionary *dict = [not userInfo]; NSData *d; + NSRange r; d = [dict objectForKey: NSFileHandleNotificationDataItem]; if (debug == YES) debugRead(d); @@ -326,10 +327,25 @@ static void debugWrite(NSData *data) /* * Tell superclass that we have successfully loaded the data. */ - [self didLoadBytes: [parser data] loadComplete: YES]; + d = [parser data]; + r = NSMakeRange(bodyPos, [d length] - bodyPos); + bodyPos = 0; + [self didLoadBytes: [d subdataWithRange: r] + loadComplete: YES]; } else { + /* + * Report partial data if possible. + */ + if ([parser isInBody]) + { + d = [parser data]; + r = NSMakeRange(bodyPos, [d length] - bodyPos); + bodyPos = [d length]; + [self didLoadBytes: [d subdataWithRange: r] + loadComplete: NO]; + } [sock readInBackgroundAndNotify]; } } @@ -404,7 +420,6 @@ static void debugWrite(NSData *data) [sock closeFile]; DESTROY(sock); } - contentLength = 0; if ([[request objectForKey: GSHTTPPropertyProxyHostKey] length] == 0) { NSNumber *p; @@ -783,6 +798,7 @@ static void debugWrite(NSData *data) } else { + bodyPos = 0; [nc addObserver: self selector: @selector(bgdRead:) name: NSFileHandleReadCompletionNotification diff --git a/Source/NSURLHandle.m b/Source/NSURLHandle.m index 97f6c0eed..ace978c00 100644 --- a/Source/NSURLHandle.m +++ b/Source/NSURLHandle.m @@ -41,6 +41,7 @@ #include @class GSFileURLHandle; +@class GSFTPURLHandle; @class GSHTTPURLHandle; /* @@ -155,6 +156,7 @@ static Class NSURLHandleClass = 0; registry = [NSMutableArray new]; registryLock = [NSLock new]; [self registerURLHandleClass: [GSFileURLHandle class]]; + [self registerURLHandleClass: [GSFTPURLHandle class]]; [self registerURLHandleClass: [GSHTTPURLHandle class]]; } }