diff --git a/.gitignore b/.gitignore index 1da23701..cfefd0dd 100644 --- a/.gitignore +++ b/.gitignore @@ -318,6 +318,12 @@ build-* *.includes *.files +# ios +ios/cl_dlls/* +ios/dlls/* +ios/*.framework +scripts/ios/mod-build + # Waf build_current *waf-*/ @@ -339,6 +345,7 @@ __pycache__ .history/* .cache/* enc_temp_folder/ +Taskfile.yml # KDevelop4 *.kdev4 diff --git a/3rdparty/maintui b/3rdparty/maintui index a07f8780..abe56ff6 160000 --- a/3rdparty/maintui +++ b/3rdparty/maintui @@ -1 +1 @@ -Subproject commit a07f8780531f9fe24a8679ff97e28f1145ac2d88 +Subproject commit abe56ff698b8a604eda59981d284b4468e7daf1d diff --git a/3rdparty/mainui b/3rdparty/mainui index a5da3a02..5e8d9a9e 160000 --- a/3rdparty/mainui +++ b/3rdparty/mainui @@ -1 +1 @@ -Subproject commit a5da3a02fc783a5d5677fc4ab40bf2b4710c4d66 +Subproject commit 5e8d9a9e7ac26972e1e14c7d2a4fd138100a2bf9 diff --git a/README.md b/README.md index 1ad5a1f8..2bbe7f61 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,16 @@ This repository contains our fork of HLSDK and restored source code for Half-Lif * Clone this repostory: `$ git clone --recursive https://github.com/FWGS/xash3d-fwgs`. +#### iOS/iPadOS +* Install Xcode from the appstore. +* Install [Homebrew package manager](https://brew.sh). + +* Install build dependencies by running: `brew install python sdl2 freetype opus opusfile libvorbis libogg`. + +* Clone the SDL2 repo `$ git clone --recursive https://github.com/libsdl-org/SDL.git -b SDL2` and compile the iOS framework by navigating to SDL/Xcode/SDL and opening the Xcode project. + +* Clone this repository: `$ git clone --recursive https://github.com/FWGS/xash3d-fwgs`. + ### Building #### Windows (Visual Studio) 0) Open command line. @@ -117,3 +127,10 @@ If compiling 32-bit on amd64, make sure `PKG_CONFIG_PATH` from the previous step #### Android (Windows/Linux/macOS) You can just open the `android` folder in Android Studio and build from here, or use `gradlew` to build from command line. + +#### iOS/iPadOS (MacOS only) +0) (optional) Examine which build options are available: `./waf --help`. +1) Configure build: `./waf configure --ios --sdl2 (path/to/SDL2.framework)`, set `--ios-simulator` instead of `--ios` if you want to build for simulator. +2) Compile `./waf build`. +3) Navigate to `build` and copy your compiled SDL2.framework there, then add your client dylibs to `build/ios/cl_dlls` and any other dylibs to `build/ios/dlls` +4) Run `scripts/ios/createipa.sh` to create an installable ipa diff --git a/common/defaults.h b/common/defaults.h index 139f20a1..d2dd93c4 100644 --- a/common/defaults.h +++ b/common/defaults.h @@ -152,12 +152,6 @@ Default build-depended cvar and constant values #define DEFAULT_M_IGNORE "1" #endif // !XASH_MOBILE_PLATFORM && !XASH_NSWITCH -#if XASH_IOS - // this means that libraries are provided with engine, but not in game data - // You need add library loading code to library.c when adding new platform - #define XASH_INTERNAL_GAMELIBS -#endif // XASH_IOS - // Defaults #ifndef DEFAULT_TOUCH_ENABLE #define DEFAULT_TOUCH_ENABLE "0" @@ -193,6 +187,8 @@ Default build-depended cvar and constant values #define DEFAULT_ACCELERATED_RENDERER "gu" #elif XASH_MOBILE_PLATFORM #define DEFAULT_ACCELERATED_RENDERER "gles1" + #elif XASH_IOS + #define DEFAULT_ACCELERATED_RENDERER "gles2" #else // !XASH_MOBILE_PLATFORM #define DEFAULT_ACCELERATED_RENDERER "gl" #endif // !XASH_MOBILE_PLATFORM diff --git a/engine/common/launcher.c b/engine/common/launcher.c index 0ab717e2..bfc61fe6 100644 --- a/engine/common/launcher.c +++ b/engine/common/launcher.c @@ -62,6 +62,7 @@ static int Sys_Start( void ) #if XASH_IOS IOS_LaunchDialog(); + szArgc = IOS_GetArgs( &szArgv ); #endif // XASH_IOS return Host_Main( szArgc, szArgv, szGameDir, 0, Sys_ChangeGame ); diff --git a/engine/platform/ios/AsyncSocket.h b/engine/platform/ios/AsyncSocket.h new file mode 100644 index 00000000..27b51859 --- /dev/null +++ b/engine/platform/ios/AsyncSocket.h @@ -0,0 +1,282 @@ +// +// AsyncSocket.h +// +// This class is in the public domain. +// Originally created by Dustin Voss on Wed Jan 29 2003. +// Updated and maintained by Deusty Designs and the Mac development community. +// +// http://code.google.com/p/cocoaasyncsocket/ +// + +#import + +@class AsyncSocket; +@class AsyncReadPacket; +@class AsyncWritePacket; + +extern NSString *const AsyncSocketException; +extern NSString *const AsyncSocketErrorDomain; + +enum AsyncSocketError +{ + AsyncSocketCFSocketError = kCFSocketError, // From CFSocketError enum. + AsyncSocketNoError = 0, // Never used. + AsyncSocketCanceledError, // onSocketWillConnect: returned NO. + AsyncSocketReadMaxedOutError, // Reached set maxLength without completing + AsyncSocketReadTimeoutError, + AsyncSocketWriteTimeoutError +}; +typedef enum AsyncSocketError AsyncSocketError; + +@interface NSObject (AsyncSocketDelegate) + +/** + * In the event of an error, the socket is closed. + * You may call "unreadData" during this call-back to get the last bit of data off the socket. + * When connecting, this delegate method may be called + * before"onSocket:didAcceptNewSocket:" or "onSocket:didConnectToHost:". + **/ +- (void)onSocket:(AsyncSocket *)sock willDisconnectWithError:(NSError *)err; + +/** + * Called when a socket disconnects with or without error. If you want to release a socket after it disconnects, + * do so here. It is not safe to do that during "onSocket:willDisconnectWithError:". + **/ +- (void)onSocketDidDisconnect:(AsyncSocket *)sock; + +/** + * Called when a socket accepts a connection. Another socket is spawned to handle it. The new socket will have + * the same delegate and will call "onSocket:didConnectToHost:port:". + **/ +- (void)onSocket:(AsyncSocket *)sock didAcceptNewSocket:(AsyncSocket *)newSocket; + +/** + * Called when a new socket is spawned to handle a connection. This method should return the run-loop of the + * thread on which the new socket and its delegate should operate. If omitted, [NSRunLoop currentRunLoop] is used. + **/ +- (NSRunLoop *)onSocket:(AsyncSocket *)sock wantsRunLoopForNewSocket:(AsyncSocket *)newSocket; + +/** + * Called when a socket is about to connect. This method should return YES to continue, or NO to abort. + * If aborted, will result in AsyncSocketCanceledError. + * + * If the connectToHost:onPort:error: method was called, the delegate will be able to access and configure the + * CFReadStream and CFWriteStream as desired prior to connection. + * + * If the connectToAddress:error: method was called, the delegate will be able to access and configure the + * CFSocket and CFSocketNativeHandle (BSD socket) as desired prior to connection. You will be able to access and + * configure the CFReadStream and CFWriteStream in the onSocket:didConnectToHost:port: method. + **/ +- (BOOL)onSocketWillConnect:(AsyncSocket *)sock; + + +/** + * Called when a read stream is closed. This method should return YES, if you + * want to close the socket of the stream, or NO then the socket won't be closed. + * This was added for the workaround to solve the issue of firefox and os-x finder, + * which seem to close a read stream for ftp's list command. + * 'iosftpserver' returns YES only when the socket is for FTP::STORE command. + **/ +- (BOOL)onReadStreamEnded:(AsyncSocket *)sock; + +/** + * Called when a socket connects and is ready for reading and writing. + * The host parameter will be an IP address, not a DNS name. + **/ +- (void)onSocket:(AsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port; + +/** + * Called when a socket has completed reading the requested data into memory. + * Not called if there is an error. + **/ +- (void)onSocket:(AsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag; + +/** + * Called when a socket has read in data, but has not yet completed the read. + * This would occur if using readToData: or readToLength: methods. + * It may be used to for things such as updating progress bars. + **/ +- (void)onSocket:(AsyncSocket *)sock didReadPartialDataOfLength:(CFIndex)partialLength tag:(long)tag; + +/** + * Called when a socket has completed writing the requested data. Not called if there is an error. + **/ +- (void)onSocket:(AsyncSocket *)sock didWriteDataWithTag:(long)tag; + +@end + +@interface AsyncSocket : NSObject +{ + CFSocketRef theSocket; // IPv4 accept or connect socket + CFSocketRef theSocket6; // IPv6 accept or connect socket + CFReadStreamRef theReadStream; + CFWriteStreamRef theWriteStream; + + CFRunLoopSourceRef theSource; // For theSocket + CFRunLoopSourceRef theSource6; // For theSocket6 + CFRunLoopRef theRunLoop; + CFSocketContext theContext; + + NSMutableArray *theReadQueue; + AsyncReadPacket *theCurrentRead; + NSTimer *theReadTimer; + NSMutableData *partialReadBuffer; + + NSMutableArray *theWriteQueue; + AsyncWritePacket *theCurrentWrite; + NSTimer *theWriteTimer; + + id theDelegate; + Byte theFlags; + + long theUserData; +} + +- (id)init; +- (id)initWithDelegate:(id)delegate; +- (id)initWithDelegate:(id)delegate userData:(long)userData; + +/* String representation is long but has no "\n". */ +- (NSString *)description; + +/** + * Use "canSafelySetDelegate" to see if there is any pending business (reads and writes) with the current delegate + * before changing it. It is, of course, safe to change the delegate before connecting or accepting connections. + **/ +- (id)delegate; +- (BOOL)canSafelySetDelegate; +- (void)setDelegate:(id)delegate; + +/* User data can be a long, or an id or void * cast to a long. */ +- (long)userData; +- (void)setUserData:(long)userData; + +/* Don't use these to read or write. And don't close them, either! */ +- (CFSocketRef)getCFSocket; +- (CFReadStreamRef)getCFReadStream; +- (CFWriteStreamRef)getCFWriteStream; + +/** + * Once one of these methods is called, the AsyncSocket instance is locked in, and the rest can't be called without + * disconnecting the socket first. If the attempt times out or fails, these methods either return NO or + * call "onSocket:willDisconnectWithError:" and "onSockedDidDisconnect:". + **/ +- (BOOL)acceptOnPort:(UInt16)port error:(NSError **)errPtr; +- (BOOL)acceptOnAddress:(NSString *)hostaddr port:(UInt16)port error:(NSError **)errPtr; +- (BOOL)connectToHost:(NSString *)hostname onPort:(UInt16)port error:(NSError **)errPtr; +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr; + +/** + * Disconnects immediately. Any pending reads or writes are dropped. + **/ +- (void)disconnect; + +/** + * Disconnects after all pending writes have completed. + * After calling this, the read and write methods (including "readDataWithTimeout:tag:") will do nothing. + * The socket will disconnect even if there are still pending reads. + **/ +- (void)disconnectAfterWriting; + +/* Returns YES if the socket and streams are open, connected, and ready for reading and writing. */ +- (BOOL)isConnected; + +/** + * Returns the local or remote host and port to which this socket is connected, or nil and 0 if not connected. + * The host will be an IP address. + **/ +- (NSString *)connectedHost; +- (UInt16)connectedPort; + +- (NSString *)localHost; +- (UInt16)localPort; + +- (BOOL)isIPv4; +- (BOOL)isIPv6; + +// The readData and writeData methods won't block. To not time out, use a negative time interval. +// If they time out, "onSocket:disconnectWithError:" is called. The tag is for your convenience. +// You can use it as an array index, step number, state id, pointer, etc., just like the socket's user data. + +/** + * This will read a certain number of bytes into memory, and call the delegate method when those bytes have been read. + * If there is an error, partially read data is lost. + * If the length is 0, this method does nothing and the delegate is not called. + **/ +- (void)readDataToLength:(CFIndex)length withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * This reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * The bytes and the separator are returned by the delegate method. + * + * If you pass nil or zero-length data as the "data" parameter, + * the method will do nothing, and the delegate will not be called. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * Note that this method is not character-set aware, so if a separator can occur naturally as part of the encoding for + * a character, the read will prematurely end. + **/ +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Same as readDataToData:withTimeout:tag, with the additional restriction that the amount of data read + * may not surpass the given maxLength (specified in bytes). + * + * If you pass a maxLength parameter that is less than the length of the data parameter, + * the method will do nothing, and the delegate will not be called. + * + * If the max length is surpassed, it is treated the same as a timeout - the socket is closed. + * + * Pass -1 as maxLength if no length restriction is desired, or simply use the readDataToData:withTimeout:tag method. + **/ +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(CFIndex)length tag:(long)tag; + +/** + * Reads the first available bytes that become available on the socket. + **/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/* Writes data to the socket, and calls the delegate when finished. + * + * If you pass in nil or zero-length data, this method does nothing and the delegate will not be called. + **/ +- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Returns progress of current read or write, from 0.0 to 1.0, or NaN if no read/write (use isnan() to check). + * "tag", "done" and "total" will be filled in if they aren't NULL. + **/ +- (float)progressOfReadReturningTag:(long *)tag bytesDone:(CFIndex *)done total:(CFIndex *)total; +- (float)progressOfWriteReturningTag:(long *)tag bytesDone:(CFIndex *)done total:(CFIndex *)total; + +/** + * For handling readDataToData requests, data is necessarily read from the socket in small increments. + * The performance can be improved by allowing AsyncSocket to read larger chunks at a time and + * store any overflow in a small internal buffer. + * This is termed pre-buffering, as some data may be read for you before you ask for it. + * If you use readDataToData a lot, enabling pre-buffering may offer a small performance improvement. + * + * Pre-buffering is disabled by default. You must explicitly enable it to turn it on. + * + * Note: If your protocol negotiates upgrades to TLS (as opposed to using TLS from the start), you should + * consider how, if at all, pre-buffering could affect the TLS negotiation sequence. + * This is because TLS runs atop TCP, and requires sending/receiving a TLS handshake over the TCP socket. + * If the negotiation sequence is poorly designed, pre-buffering could potentially pre-read part of the TLS handshake, + * thus causing TLS to fail. In almost all cases, especially when implementing a formalized protocol, this will never + * be a hazard. + **/ +- (void)enablePreBuffering; + +/** + * In the event of an error, this method may be called during onSocket:willDisconnectWithError: to read + * any data that's left on the socket. + **/ +- (NSData *)unreadData; + +/* A few common line separators, for use with "readDataToData:withTimeout:tag:". */ ++ (NSData *)CRLFData; // 0x0D0A ++ (NSData *)CRData; // 0x0D ++ (NSData *)LFData; // 0x0A ++ (NSData *)ZeroData; // 0x00 + +@end diff --git a/engine/platform/ios/AsyncSocket.m b/engine/platform/ios/AsyncSocket.m new file mode 100644 index 00000000..b85a4796 --- /dev/null +++ b/engine/platform/ios/AsyncSocket.m @@ -0,0 +1,2426 @@ +// +// AsyncSocket.m +// +// This class is in the public domain. +// Originally created by Dustin Voss on Wed Jan 29 2003. +// Updated and maintained by Deusty Designs and the Mac development community. +// +// http://code.google.com/p/cocoaasyncsocket/ +// + +#import "AsyncSocket.h" +#import +#import +#import +#import + +#if TARGET_OS_IPHONE +// Note: You may need to add the CFNetwork Framework to your project +#import +#endif + +#pragma mark Declarations + +#define READQUEUE_CAPACITY 5 // Initial capacity +#define WRITEQUEUE_CAPACITY 5 // Initial capacity +#define READALL_CHUNKSIZE 256 // Incremental increase in buffer size +#define WRITE_CHUNKSIZE (1024 * 4) // Limit on size of each write pass + +NSString *const AsyncSocketException = @"AsyncSocketException"; +NSString *const AsyncSocketErrorDomain = @"AsyncSocketErrorDomain"; + +// This is a mutex lock used by all instances of AsyncSocket, to protect getaddrinfo. +// The man page says it is not thread-safe. (As of Mac OS X 10.4.7, and possibly earlier) +static NSString *getaddrinfoLock = @"lock"; + +enum AsyncSocketFlags +{ + kEnablePreBuffering = 1 << 0, // If set, pre-buffering is enabled. + kDidCallConnectDeleg = 1 << 1, // If set, connect delegate has been called. + kDidPassConnectMethod = 1 << 2, // If set, disconnection results in delegate call. + kForbidReadsWrites = 1 << 3, // If set, no new reads or writes are allowed. + kDisconnectSoon = 1 << 4, // If set, disconnect as soon as nothing is queued. + kClosingWithError = 1 << 5, // If set, the socket is being closed due to an error. +}; + +@interface AsyncSocket (Private) + +// Socket Implementation +- (CFSocketRef) createAcceptSocketForAddress:(NSData *)addr error:(NSError **)errPtr; +- (BOOL) createSocketForAddress:(NSData *)remoteAddr error:(NSError **)errPtr; +- (BOOL) attachSocketsToRunLoop:(NSRunLoop *)runLoop error:(NSError **)errPtr; +- (BOOL) configureSocketAndReturnError:(NSError **)errPtr; +- (BOOL) connectSocketToAddress:(NSData *)remoteAddr error:(NSError **)errPtr; +- (void) doAcceptWithSocket:(CFSocketNativeHandle)newSocket; +- (void) doSocketOpen:(CFSocketRef)sock withCFSocketError:(CFSocketError)err; + +// Stream Implementation +- (BOOL) createStreamsFromNative:(CFSocketNativeHandle)native error:(NSError **)errPtr; +- (BOOL) createStreamsToHost:(NSString *)hostname onPort:(UInt16)port error:(NSError **)errPtr; +- (BOOL) attachStreamsToRunLoop:(NSRunLoop *)runLoop error:(NSError **)errPtr; +- (BOOL) configureStreamsAndReturnError:(NSError **)errPtr; +- (BOOL) openStreamsAndReturnError:(NSError **)errPtr; +- (void) doStreamOpen; +- (BOOL) setSocketFromStreamsAndReturnError:(NSError **)errPtr; + +// Disconnect Implementation +- (void) closeWithError:(NSError *)err; +- (void) recoverUnreadData; +- (void) emptyQueues; +- (void) close; + +// Errors +- (NSError *) getErrnoError; +- (NSError *) getAbortError; +- (NSError *) getStreamError; +- (NSError *) getSocketError; +- (NSError *) getReadMaxedOutError; +- (NSError *) getReadTimeoutError; +- (NSError *) getWriteTimeoutError; +- (NSError *) errorFromCFStreamError:(CFStreamError)err; + +// Diagnostics +- (BOOL) isSocketConnected; +- (BOOL) areStreamsConnected; +- (NSString *) connectedHost: (CFSocketRef)socket; +- (UInt16) connectedPort: (CFSocketRef)socket; +- (NSString *) localHost: (CFSocketRef)socket; +- (UInt16) localPort: (CFSocketRef)socket; +- (NSString *) addressHost: (CFDataRef)cfaddr; +- (UInt16) addressPort: (CFDataRef)cfaddr; + +// Reading +- (void) doBytesAvailable; +- (void) completeCurrentRead; +- (void) endCurrentRead; +- (void) scheduleDequeueRead; +- (void) maybeDequeueRead; +- (void) doReadTimeout:(NSTimer *)timer; + +// Writing +- (void) doSendBytes; +- (void) completeCurrentWrite; +- (void) endCurrentWrite; +- (void) scheduleDequeueWrite; +- (void) maybeDequeueWrite; +- (void) maybeScheduleDisconnect; +- (void) doWriteTimeout:(NSTimer *)timer; + +// Callbacks +- (void) doCFCallback:(CFSocketCallBackType)type forSocket:(CFSocketRef)sock withAddress:(NSData *)address withData:(const void *)pData; +- (void) doCFReadStreamCallback:(CFStreamEventType)type forStream:(CFReadStreamRef)stream; +- (void) doCFWriteStreamCallback:(CFStreamEventType)type forStream:(CFWriteStreamRef)stream; + +@end + +static void MyCFSocketCallback (CFSocketRef, CFSocketCallBackType, CFDataRef, const void *, void *); +static void MyCFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type, void *pInfo); +static void MyCFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo); + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The AsyncReadPacket encompasses the instructions for a current read. + * The content of a read packet allows the code to determine if we're: + * reading to a certain length, reading to a certain separator, or simply reading the first chunk of data. + **/ +@interface AsyncReadPacket : NSObject +{ +@public + NSMutableData *buffer; + CFIndex bytesDone; + NSTimeInterval timeout; + CFIndex maxLength; + long tag; + NSData *term; + BOOL readAllAvailableData; +} +- (id)initWithData:(NSMutableData *)d + timeout:(NSTimeInterval)t + tag:(long)i + readAllAvailable:(BOOL)a + terminator:(NSData *)e + maxLength:(CFIndex)m; + +- (unsigned)readLengthForTerm; + +- (unsigned)prebufferReadLengthForTerm; +- (CFIndex)searchForTermAfterPreBuffering:(CFIndex)numBytes; + +- (void)dealloc; +@end + +@implementation AsyncReadPacket + +- (id)initWithData:(NSMutableData *)d + timeout:(NSTimeInterval)t + tag:(long)i + readAllAvailable:(BOOL)a + terminator:(NSData *)e + maxLength:(CFIndex)m +{ + if(self = [super init]) + { + buffer = [d retain]; + timeout = t; + tag = i; + readAllAvailableData = a; + term = [e copy]; + bytesDone = 0; + maxLength = m; + } + return self; +} + +/** + * For read packets with a set terminator, returns the safe length of data that can be read + * without going over a terminator, or the maxLength. + * + * It is assumed the terminator has not already been read. + **/ +- (unsigned)readLengthForTerm +{ + NSAssert(term != nil, @"Searching for term in data when there is no term."); + + // What we're going to do is look for a partial sequence of the terminator at the end of the buffer. + // If a partial sequence occurs, then we must assume the next bytes to arrive will be the rest of the term, + // and we can only read that amount. + // Otherwise, we're safe to read the entire length of the term. + + unsigned result = [term length]; + + // i = index within buffer at which to check data + // j = length of term to check against + + // Note: Beware of implicit casting rules + // This could give you -1: MAX(0, (0 - [term length] + 1)); + + CFIndex i = MAX(0, (CFIndex)(bytesDone - [term length] + 1)); + CFIndex j = MIN([term length] - 1, bytesDone); + + while(i < bytesDone) + { + const void *subBuffer = [buffer bytes] + i; + + if(memcmp(subBuffer, [term bytes], j) == 0) + { + result = [term length] - j; + break; + } + + i++; + j--; + } + + if(maxLength > 0) + return MIN(result, (maxLength - bytesDone)); + else + return result; +} + +/** + * Assuming pre-buffering is enabled, returns the amount of data that can be read + * without going over the maxLength. + **/ +- (unsigned)prebufferReadLengthForTerm +{ + if(maxLength > 0) + return MIN(READALL_CHUNKSIZE, (maxLength - bytesDone)); + else + return READALL_CHUNKSIZE; +} + +/** + * For read packets with a set terminator, scans the packet buffer for the term. + * It is assumed the terminator had not been fully read prior to the new bytes. + * + * If the term is found, the number of excess bytes after the term are returned. + * If the term is not found, this method will return -1. + * + * Note: A return value of zero means the term was found at the very end. + **/ +- (CFIndex)searchForTermAfterPreBuffering:(CFIndex)numBytes +{ + NSAssert(term != nil, @"Searching for term in data when there is no term."); + + // We try to start the search such that the first new byte read matches up with the last byte of the term. + // We continue searching forward after this until the term no longer fits into the buffer. + + // Note: Beware of implicit casting rules + // This could give you -1: MAX(0, 1 - 1 - [term length] + 1); + + CFIndex i = MAX(0, (CFIndex)(bytesDone - numBytes - [term length] + 1)); + + while(i + [term length] <= bytesDone) + { + const void *subBuffer = [buffer bytes] + i; + + if(memcmp(subBuffer, [term bytes], [term length]) == 0) + { + return bytesDone - (i + [term length]); + } + + i++; + } + + return -1; +} + +- (void)dealloc +{ + [buffer release]; + [term release]; + [super dealloc]; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface AsyncWritePacket : NSObject +{ +@public + NSData *buffer; + CFIndex bytesDone; + long tag; + NSTimeInterval timeout; +} +- (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i; +- (void)dealloc; +@end + +@implementation AsyncWritePacket + +- (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i; +{ + if(self = [super init]) + { + buffer = [d retain]; + timeout = t; + tag = i; + bytesDone = 0; + } + return self; +} + +- (void)dealloc +{ + [buffer release]; + [super dealloc]; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation AsyncSocket + +- (id)init +{ + return [self initWithDelegate:nil userData:0]; +} + +- (id)initWithDelegate:(id)delegate +{ + return [self initWithDelegate:delegate userData:0]; +} + +// Designated initializer. +- (id)initWithDelegate:(id)delegate userData:(long)userData +{ + if(self = [super init]) + { + theFlags = 0x00; + theDelegate = delegate; + theUserData = userData; + + theSocket = NULL; + theSource = NULL; + theSocket6 = NULL; + theSource6 = NULL; + theRunLoop = NULL; + theReadStream = NULL; + theWriteStream = NULL; + + theReadQueue = [[NSMutableArray alloc] initWithCapacity:READQUEUE_CAPACITY]; + theCurrentRead = nil; + theReadTimer = nil; + + partialReadBuffer = [[NSMutableData alloc] initWithCapacity:READALL_CHUNKSIZE]; + + theWriteQueue = [[NSMutableArray alloc] initWithCapacity:WRITEQUEUE_CAPACITY]; + theCurrentWrite = nil; + theWriteTimer = nil; + + // Socket context + NSAssert(sizeof(CFSocketContext) == sizeof(CFStreamClientContext), @"CFSocketContext != CFStreamClientContext"); + theContext.version = 0; + theContext.info = self; + theContext.retain = nil; + theContext.release = nil; + theContext.copyDescription = nil; + } + return self; +} + +// The socket may been initialized in a connected state and auto-released, so this should close it down cleanly. +- (void)dealloc +{ + [self close]; + [theReadQueue release]; + [theWriteQueue release]; + [NSObject cancelPreviousPerformRequestsWithTarget:theDelegate selector:@selector(onSocketDidDisconnect:) object:self]; + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + [super dealloc]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Accessors +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (long)userData +{ + return theUserData; +} + +- (void)setUserData:(long)userData +{ + theUserData = userData; +} + +- (id)delegate +{ + return theDelegate; +} + +- (void)setDelegate:(id)delegate +{ + theDelegate = delegate; +} + +- (BOOL)canSafelySetDelegate +{ + return ([theReadQueue count] == 0 && [theWriteQueue count] == 0 && theCurrentRead == nil && theCurrentWrite == nil); +} + +- (CFSocketRef)getCFSocket +{ + if(theSocket) + return theSocket; + else + return theSocket6; +} + +- (CFReadStreamRef)getCFReadStream +{ + return theReadStream; +} + +- (CFWriteStreamRef)getCFWriteStream +{ + return theWriteStream; +} + +- (float)progressOfReadReturningTag:(long *)tag bytesDone:(CFIndex *)done total:(CFIndex *)total +{ + // Check to make sure we're actually reading something right now + if (!theCurrentRead) return NAN; + + // It's only possible to know the progress of our read if we're reading to a certain length + // If we're reading to data, we of course have no idea when the data will arrive + // If we're reading to timeout, then we have no idea when the next chunk of data will arrive. + BOOL hasTotal = (theCurrentRead->readAllAvailableData == NO && theCurrentRead->term == nil); + + CFIndex d = theCurrentRead->bytesDone; + CFIndex t = hasTotal ? [theCurrentRead->buffer length] : 0; + if (tag != NULL) *tag = theCurrentRead->tag; + if (done != NULL) *done = d; + if (total != NULL) *total = t; + float ratio = (float)d/(float)t; + return isnan(ratio) ? 1.0 : ratio; // 0 of 0 bytes is 100% done. +} + +- (float)progressOfWriteReturningTag:(long *)tag bytesDone:(CFIndex *)done total:(CFIndex *)total +{ + if (!theCurrentWrite) return NAN; + CFIndex d = theCurrentWrite->bytesDone; + CFIndex t = [theCurrentWrite->buffer length]; + if (tag != NULL) *tag = theCurrentWrite->tag; + if (done != NULL) *done = d; + if (total != NULL) *total = t; + return (float)d/(float)t; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * See the header file for a full explanation of pre-buffering. + **/ +- (void)enablePreBuffering +{ + theFlags |= kEnablePreBuffering; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Connection +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)acceptOnPort:(UInt16)port error:(NSError **)errPtr +{ + return [self acceptOnAddress:nil port:port error:errPtr]; +} + +/** + * To accept on a certain address, pass the address to accept on. + * To accept on any address, pass nil or an empty string. + * To accept only connections from localhost pass "localhost" or "loopback". + **/ +- (BOOL)acceptOnAddress:(NSString *)hostaddr port:(UInt16)port error:(NSError **)errPtr +{ + if (theDelegate == NULL) + [NSException raise:AsyncSocketException format:@"Attempting to accept without a delegate. Set a delegate first."]; + + if (theSocket != NULL || theSocket6 != NULL) + [NSException raise:AsyncSocketException format:@"Attempting to accept while connected or accepting connections. Disconnect first."]; + + // Set up the listen sockaddr structs if needed. + + NSData *address = nil, *address6 = nil; + if(hostaddr == nil || ([hostaddr length] == 0)) + { + // Accept on ANY address + struct sockaddr_in nativeAddr; + nativeAddr.sin_len = sizeof(struct sockaddr_in); + nativeAddr.sin_family = AF_INET; + nativeAddr.sin_port = htons(port); + nativeAddr.sin_addr.s_addr = htonl(INADDR_ANY); + memset(&(nativeAddr.sin_zero), 0, sizeof(nativeAddr.sin_zero)); + + struct sockaddr_in6 nativeAddr6; + nativeAddr6.sin6_len = sizeof(struct sockaddr_in6); + nativeAddr6.sin6_family = AF_INET6; + nativeAddr6.sin6_port = htons(port); + nativeAddr6.sin6_flowinfo = 0; + nativeAddr6.sin6_addr = in6addr_any; + nativeAddr6.sin6_scope_id = 0; + + // Wrap the native address structures for CFSocketSetAddress. + address = [NSData dataWithBytes:&nativeAddr length:sizeof(nativeAddr)]; + address6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + } + else if([hostaddr isEqualToString:@"localhost"] || [hostaddr isEqualToString:@"loopback"]) + { + // Accept only on LOOPBACK address + struct sockaddr_in nativeAddr; + nativeAddr.sin_len = sizeof(struct sockaddr_in); + nativeAddr.sin_family = AF_INET; + nativeAddr.sin_port = htons(port); + nativeAddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + memset(&(nativeAddr.sin_zero), 0, sizeof(nativeAddr.sin_zero)); + + struct sockaddr_in6 nativeAddr6; + nativeAddr6.sin6_len = sizeof(struct sockaddr_in6); + nativeAddr6.sin6_family = AF_INET6; + nativeAddr6.sin6_port = htons(port); + nativeAddr6.sin6_flowinfo = 0; + nativeAddr6.sin6_addr = in6addr_loopback; + nativeAddr6.sin6_scope_id = 0; + + // Wrap the native address structures for CFSocketSetAddress. + address = [NSData dataWithBytes:&nativeAddr length:sizeof(nativeAddr)]; + address6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + } + else + { + NSString *portStr = [NSString stringWithFormat:@"%hu", port]; + + @synchronized (getaddrinfoLock) + { + struct addrinfo hints, *res, *res0; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + hints.ai_flags = AI_PASSIVE; + + int error = getaddrinfo([hostaddr UTF8String], [portStr UTF8String], &hints, &res0); + + if(error) + { + if(errPtr) + { + NSString *errMsg = [NSString stringWithCString:gai_strerror(error) encoding:NSASCIIStringEncoding]; + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + *errPtr = [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:error userInfo:info]; + } + } + + for(res = res0; res; res = res->ai_next) + { + if(!address && (res->ai_family == AF_INET)) + { + // Found IPv4 address + // Wrap the native address structures for CFSocketSetAddress. + address = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + } + else if(!address6 && (res->ai_family == AF_INET6)) + { + // Found IPv6 address + // Wrap the native address structures for CFSocketSetAddress. + address6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + } + } + freeaddrinfo(res0); + } + + if(!address && !address6) return NO; + } + + // Create the sockets. + + if (address) + { + theSocket = [self createAcceptSocketForAddress:address error:errPtr]; + if (theSocket == NULL) goto Failed; + } + + if (address6) + { + theSocket6 = [self createAcceptSocketForAddress:address6 error:errPtr]; + + // Note: The iPhone doesn't currently support IPv6 + +#if !TARGET_OS_IPHONE + if (theSocket6 == NULL) goto Failed; +#endif + } + + // Attach the sockets to the run loop so that callback methods work + + [self attachSocketsToRunLoop:nil error:nil]; + + // Set the SO_REUSEADDR flags. + + int reuseOn = 1; + if (theSocket) setsockopt(CFSocketGetNative(theSocket), SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + if (theSocket6) setsockopt(CFSocketGetNative(theSocket6), SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + + // Set the local bindings which causes the sockets to start listening. + + CFSocketError err; + if (theSocket) + { + err = CFSocketSetAddress (theSocket, (CFDataRef)address); + if (err != kCFSocketSuccess) goto Failed; + + //NSLog(@"theSocket4: %hu", [self localPort:theSocket]); + } + + if(port == 0 && theSocket && theSocket6) + { + // The user has passed in port 0, which means he wants to allow the kernel to choose the port for them + // However, the kernel will choose a different port for both theSocket and theSocket6 + // So we grab the port the kernel choose for theSocket, and set it as the port for theSocket6 + UInt16 chosenPort = [self localPort:theSocket]; + + struct sockaddr_in6 *pSockAddr6 = (struct sockaddr_in6 *)[address6 bytes]; + pSockAddr6->sin6_port = htons(chosenPort); + } + + if (theSocket6) + { + err = CFSocketSetAddress (theSocket6, (CFDataRef)address6); + if (err != kCFSocketSuccess) goto Failed; + + //NSLog(@"theSocket6: %hu", [self localPort:theSocket6]); + } + + theFlags |= kDidPassConnectMethod; + return YES; + +Failed:; + if(errPtr) *errPtr = [self getSocketError]; + if(theSocket != NULL) + { + CFSocketInvalidate(theSocket); + CFRelease(theSocket); + theSocket = NULL; + } + if(theSocket6 != NULL) + { + CFSocketInvalidate(theSocket6); + CFRelease(theSocket6); + theSocket6 = NULL; + } + return NO; +} + +/** + * This method creates an initial CFReadStream and CFWriteStream to the given host on the given port. + * The connection is then opened, and the corresponding CFSocket will be extracted after the connection succeeds. + * + * Thus the delegate will have access to the CFReadStream and CFWriteStream prior to connection, + * specifically in the onSocketWillConnect: method. + **/ +- (BOOL)connectToHost:(NSString*)hostname onPort:(UInt16)port error:(NSError **)errPtr +{ + if(theDelegate == NULL) + { + NSString *message = @"Attempting to connect without a delegate. Set a delegate first."; + [NSException raise:AsyncSocketException format:@"%@", message]; + } + + if(theSocket != NULL || theSocket6 != NULL) + { + NSString *message = @"Attempting to connect while connected or accepting connections. Disconnect first."; + [NSException raise:AsyncSocketException format:@"%@", message]; + } + + BOOL pass = YES; + + if(pass && ![self createStreamsToHost:hostname onPort:port error:errPtr]) pass = NO; + if(pass && ![self attachStreamsToRunLoop:nil error:errPtr]) pass = NO; + if(pass && ![self configureStreamsAndReturnError:errPtr]) pass = NO; + if(pass && ![self openStreamsAndReturnError:errPtr]) pass = NO; + + if(pass) + theFlags |= kDidPassConnectMethod; + else + [self close]; + + return pass; +} + +/** + * This method creates an initial CFSocket to the given address. + * The connection is then opened, and the corresponding CFReadStream and CFWriteStream will be + * created from the low-level sockets after the connection succeeds. + * + * Thus the delegate will have access to the CFSocket and CFSocketNativeHandle (BSD socket) prior to connection, + * specifically in the onSocketWillConnect: method. + * + * Note: The NSData parameter is expected to be a sockaddr structure. For example, an NSData object returned from + * NSNetservice addresses method. + * If you have an existing struct sockaddr you can convert it to an NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; + **/ +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr +{ + if (theDelegate == NULL) + { + NSString *message = @"Attempting to connect without a delegate. Set a delegate first."; + [NSException raise:AsyncSocketException format:@"%@", message]; + } + + if (theSocket != NULL || theSocket6 != NULL) + { + NSString *message = @"Attempting to connect while connected or accepting connections. Disconnect first."; + [NSException raise:AsyncSocketException format:@"%@", message]; + } + + BOOL pass = YES; + + if(pass && ![self createSocketForAddress:remoteAddr error:errPtr]) pass = NO; + if(pass && ![self attachSocketsToRunLoop:nil error:errPtr]) pass = NO; + if(pass && ![self configureSocketAndReturnError:errPtr]) pass = NO; + if(pass && ![self connectSocketToAddress:remoteAddr error:errPtr]) pass = NO; + + if(pass) + theFlags |= kDidPassConnectMethod; + else + [self close]; + + return pass; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Socket Implementation: +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Creates the accept sockets. + * Returns true if either IPv4 or IPv6 is created. + * If either is missing, an error is returned (even though the method may return true). + **/ +- (CFSocketRef)createAcceptSocketForAddress:(NSData *)addr error:(NSError **)errPtr +{ + struct sockaddr *pSockAddr = (struct sockaddr *)[addr bytes]; + int addressFamily = pSockAddr->sa_family; + + CFSocketRef socket = CFSocketCreate(kCFAllocatorDefault, + addressFamily, + SOCK_STREAM, + 0, + kCFSocketAcceptCallBack, // Callback flags + (CFSocketCallBack)&MyCFSocketCallback, // Callback method + &theContext); + + if(socket == NULL) + { + if(errPtr) *errPtr = [self getSocketError]; + } + + return socket; +} + +- (BOOL)createSocketForAddress:(NSData *)remoteAddr error:(NSError **)errPtr +{ + struct sockaddr *pSockAddr = (struct sockaddr *)[remoteAddr bytes]; + + if(pSockAddr->sa_family == AF_INET) + { + theSocket = CFSocketCreate(NULL, // Default allocator + PF_INET, // Protocol Family + SOCK_STREAM, // Socket Type + IPPROTO_TCP, // Protocol + kCFSocketConnectCallBack, // Callback flags + (CFSocketCallBack)&MyCFSocketCallback, // Callback method + &theContext); // Socket Context + + if(theSocket == NULL) + { + if (errPtr) *errPtr = [self getSocketError]; + return NO; + } + } + else if(pSockAddr->sa_family == AF_INET6) + { + theSocket6 = CFSocketCreate(NULL, // Default allocator + PF_INET6, // Protocol Family + SOCK_STREAM, // Socket Type + IPPROTO_TCP, // Protocol + kCFSocketConnectCallBack, // Callback flags + (CFSocketCallBack)&MyCFSocketCallback, // Callback method + &theContext); // Socket Context + + if(theSocket6 == NULL) + { + if (errPtr) *errPtr = [self getSocketError]; + return NO; + } + } + else + { + if (errPtr) *errPtr = [self getSocketError]; + return NO; + } + + return YES; +} + +/** + * Adds the CFSocket's to the run-loop so that callbacks will work properly. + **/ +- (BOOL)attachSocketsToRunLoop:(NSRunLoop *)runLoop error:(NSError **)errPtr +{ + // Get the CFRunLoop to which the socket should be attached. + theRunLoop = (runLoop == nil) ? CFRunLoopGetCurrent() : [runLoop getCFRunLoop]; + + if(theSocket) + { + theSource = CFSocketCreateRunLoopSource (kCFAllocatorDefault, theSocket, 0); + CFRunLoopAddSource (theRunLoop, theSource, kCFRunLoopDefaultMode); + } + + if(theSocket6) + { + theSource6 = CFSocketCreateRunLoopSource (kCFAllocatorDefault, theSocket6, 0); + CFRunLoopAddSource (theRunLoop, theSource6, kCFRunLoopDefaultMode); + } + + return YES; +} + +/** + * Allows the delegate method to configure the CFSocket or CFNativeSocket as desired before we connect. + * Note that the CFReadStream and CFWriteStream will not be available until after the connection is opened. + **/ +- (BOOL)configureSocketAndReturnError:(NSError **)errPtr +{ + // Call the delegate method for further configuration. + if([theDelegate respondsToSelector:@selector(onSocketWillConnect:)]) + { + if([theDelegate onSocketWillConnect:self] == NO) + { + if (errPtr) *errPtr = [self getAbortError]; + return NO; + } + } + return YES; +} + +- (BOOL)connectSocketToAddress:(NSData *)remoteAddr error:(NSError **)errPtr +{ + // Start connecting to the given address in the background + // The MyCFSocketCallback method will be called when the connection succeeds or fails + if(theSocket) + { + CFSocketError err = CFSocketConnectToAddress(theSocket, (CFDataRef)remoteAddr, -1); + if(err != kCFSocketSuccess) + { + if (errPtr) *errPtr = [self getSocketError]; + return NO; + } + } + else if(theSocket6) + { + CFSocketError err = CFSocketConnectToAddress(theSocket6, (CFDataRef)remoteAddr, -1); + if(err != kCFSocketSuccess) + { + if (errPtr) *errPtr = [self getSocketError]; + return NO; + } + } + + return YES; +} + +/** + * Attempt to make the new socket. + * If an error occurs, ignore this event. + **/ +- (void)doAcceptWithSocket:(CFSocketNativeHandle)newNative +{ + AsyncSocket *newSocket = [[[[self class] alloc] initWithDelegate:theDelegate] autorelease]; + + // Note: We use [self class] to support subclassing AsyncSocket. + + if(newSocket) + { + if ([theDelegate respondsToSelector:@selector(onSocket:didAcceptNewSocket:)]) + [theDelegate onSocket:self didAcceptNewSocket:newSocket]; + + NSRunLoop *runLoop = nil; + if ([theDelegate respondsToSelector:@selector(onSocket:wantsRunLoopForNewSocket:)]) + runLoop = [theDelegate onSocket:self wantsRunLoopForNewSocket:newSocket]; + + BOOL pass = YES; + + if(pass && ![newSocket createStreamsFromNative:newNative error:nil]) pass = NO; + if(pass && ![newSocket attachStreamsToRunLoop:runLoop error:nil]) pass = NO; + if(pass && ![newSocket configureStreamsAndReturnError:nil]) pass = NO; + if(pass && ![newSocket openStreamsAndReturnError:nil]) pass = NO; + + if(pass) + newSocket->theFlags |= kDidPassConnectMethod; + else { + // No NSError, but errors will still get logged from the above functions. + [newSocket close]; + } + + } +} + +/** + * Description forthcoming... + **/ +- (void)doSocketOpen:(CFSocketRef)sock withCFSocketError:(CFSocketError)socketError +{ + NSParameterAssert ((sock == theSocket) || (sock == theSocket6)); + + if(socketError == kCFSocketTimeout || socketError == kCFSocketError) + { + [self closeWithError:[self getSocketError]]; + return; + } + + // Get the underlying native (BSD) socket + CFSocketNativeHandle nativeSocket = CFSocketGetNative(sock); + + // Setup the socket so that invalidating the socket will not close the native socket + CFSocketSetSocketFlags(sock, 0); + + // Invalidate and release the CFSocket - All we need from here on out is the nativeSocket + // Note: If we don't invalidate the socket (leaving the native socket open) + // then theReadStream and theWriteStream won't function properly. + // Specifically, their callbacks won't work, with the exception of kCFStreamEventOpenCompleted. + // I'm not entirely sure why this is, but I'm guessing that events on the socket fire to the CFSocket we created, + // as opposed to the CFReadStream/CFWriteStream. + + CFSocketInvalidate(sock); + CFRelease(sock); + theSocket = NULL; + theSocket6 = NULL; + + NSError *err; + BOOL pass = YES; + + if(pass && ![self createStreamsFromNative:nativeSocket error:&err]) pass = NO; + if(pass && ![self attachStreamsToRunLoop:nil error:&err]) pass = NO; + if(pass && ![self openStreamsAndReturnError:&err]) pass = NO; + + if(!pass) + { + [self closeWithError:err]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Stream Implementation: +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Creates the CFReadStream and CFWriteStream from the given native socket. + * The CFSocket may be extracted from either stream after the streams have been opened. + * + * Note: The given native socket must already be connected! + **/ +- (BOOL)createStreamsFromNative:(CFSocketNativeHandle)native error:(NSError **)errPtr +{ + // Create the socket & streams. + CFStreamCreatePairWithSocket(kCFAllocatorDefault, native, &theReadStream, &theWriteStream); + if (theReadStream == NULL || theWriteStream == NULL) + { + NSError *err = [self getStreamError]; + NSLog (@"AsyncSocket %p couldn't create streams from accepted socket: %@", self, err); + if (errPtr) *errPtr = err; + return NO; + } + + // Ensure the CF & BSD socket is closed when the streams are closed. + CFReadStreamSetProperty(theReadStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue); + CFWriteStreamSetProperty(theWriteStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue); + + return YES; +} + +/** + * Creates the CFReadStream and CFWriteStream from the given hostname and port number. + * The CFSocket may be extracted from either stream after the streams have been opened. + **/ +- (BOOL)createStreamsToHost:(NSString *)hostname onPort:(UInt16)port error:(NSError **)errPtr +{ + // Create the socket & streams. + CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, (CFStringRef)hostname, port, &theReadStream, &theWriteStream); + if (theReadStream == NULL || theWriteStream == NULL) + { + if (errPtr) *errPtr = [self getStreamError]; + return NO; + } + + // Ensure the CF & BSD socket is closed when the streams are closed. + CFReadStreamSetProperty(theReadStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue); + CFWriteStreamSetProperty(theWriteStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue); + + return YES; +} + +- (BOOL)attachStreamsToRunLoop:(NSRunLoop *)runLoop error:(NSError **)errPtr +{ + // Get the CFRunLoop to which the socket should be attached. + theRunLoop = (runLoop == nil) ? CFRunLoopGetCurrent() : [runLoop getCFRunLoop]; + + // Make read stream non-blocking. + if (!CFReadStreamSetClient (theReadStream, + kCFStreamEventHasBytesAvailable | kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered | kCFStreamEventOpenCompleted, + (CFReadStreamClientCallBack)&MyCFReadStreamCallback, + (CFStreamClientContext *)(&theContext) )) + { + NSError *err = [self getStreamError]; + + NSLog (@"AsyncSocket %p couldn't attach read stream to run-loop,", self); + NSLog (@"Error: %@", err); + + if (errPtr) *errPtr = err; + return NO; + } + CFReadStreamScheduleWithRunLoop (theReadStream, theRunLoop, kCFRunLoopDefaultMode); + + // Make write stream non-blocking. + if (!CFWriteStreamSetClient (theWriteStream, + kCFStreamEventCanAcceptBytes | kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered | kCFStreamEventOpenCompleted, + (CFWriteStreamClientCallBack)&MyCFWriteStreamCallback, + (CFStreamClientContext *)(&theContext) )) + { + NSError *err = [self getStreamError]; + + NSLog (@"AsyncSocket %p couldn't attach write stream to run-loop,", self); + NSLog (@"Error: %@", err); + + if (errPtr) *errPtr = err; + return NO; + + } + CFWriteStreamScheduleWithRunLoop (theWriteStream, theRunLoop, kCFRunLoopDefaultMode); + + return YES; +} + +/** + * Allows the delegate method to configure the CFReadStream and/or CFWriteStream as desired before we connect. + * Note that the CFSocket and CFNativeSocket will not be available until after the connection is opened. + **/ +- (BOOL)configureStreamsAndReturnError:(NSError **)errPtr +{ + // Call the delegate method for further configuration. + if([theDelegate respondsToSelector:@selector(onSocketWillConnect:)]) + { + if([theDelegate onSocketWillConnect:self] == NO) + { + if (errPtr) *errPtr = [self getAbortError]; + return NO; + } + } + return YES; +} + +- (BOOL)openStreamsAndReturnError:(NSError **)errPtr +{ + BOOL pass = YES; + + if(pass && !CFReadStreamOpen (theReadStream)) + { + NSLog (@"AsyncSocket %p couldn't open read stream,", self); + pass = NO; + } + + if(pass && !CFWriteStreamOpen (theWriteStream)) + { + NSLog (@"AsyncSocket %p couldn't open write stream,", self); + pass = NO; + } + + if(!pass) + { + if (errPtr) *errPtr = [self getStreamError]; + } + + return pass; +} + +/** + * Called when read or write streams open. + * When the socket is connected and both streams are open, consider the AsyncSocket instance to be ready. + **/ +- (void)doStreamOpen +{ + NSError *err = nil; + if ([self areStreamsConnected] && !(theFlags & kDidCallConnectDeleg)) + { + // Get the socket. + if (![self setSocketFromStreamsAndReturnError: &err]) + { + NSLog (@"AsyncSocket %p couldn't get socket from streams, %@. Disconnecting.", self, err); + [self closeWithError:err]; + return; + } + + // Call the delegate. + theFlags |= kDidCallConnectDeleg; + if ([theDelegate respondsToSelector:@selector(onSocket:didConnectToHost:port:)]) + { + [theDelegate onSocket:self didConnectToHost:[self connectedHost] port:[self connectedPort]]; + } + + // Immediately deal with any already-queued requests. + [self maybeDequeueRead]; + [self maybeDequeueWrite]; + } +} + +- (BOOL)setSocketFromStreamsAndReturnError:(NSError **)errPtr +{ + // Get the CFSocketNativeHandle from theReadStream + CFSocketNativeHandle native; + CFDataRef nativeProp = CFReadStreamCopyProperty(theReadStream, kCFStreamPropertySocketNativeHandle); + if(nativeProp == NULL) + { + if (errPtr) *errPtr = [self getStreamError]; + return NO; + } + + CFDataGetBytes(nativeProp, CFRangeMake(0, CFDataGetLength(nativeProp)), (UInt8 *)&native); + CFRelease(nativeProp); + + CFSocketRef socket = CFSocketCreateWithNative(kCFAllocatorDefault, native, 0, NULL, NULL); + if(socket == NULL) + { + if (errPtr) *errPtr = [self getSocketError]; + return NO; + } + + // Determine whether the connection was IPv4 or IPv6 + CFDataRef peeraddr = CFSocketCopyPeerAddress(socket); + struct sockaddr *sa = (struct sockaddr *)CFDataGetBytePtr(peeraddr); + + if(sa->sa_family == AF_INET) + { + theSocket = socket; + } + else + { + theSocket6 = socket; + } + + CFRelease(peeraddr); + + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Disconnect Implementation: +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// Sends error message and disconnects +- (void)closeWithError:(NSError *)err +{ + theFlags |= kClosingWithError; + + if (theFlags & kDidPassConnectMethod) + { + // Try to salvage what data we can. + [self recoverUnreadData]; + + // Let the delegate know, so it can try to recover if it likes. + if ([theDelegate respondsToSelector:@selector(onSocket:willDisconnectWithError:)]) + { + [theDelegate onSocket:self willDisconnectWithError:err]; + } + } + [self close]; +} + +// Prepare partially read data for recovery. +- (void)recoverUnreadData +{ + if((theCurrentRead != nil) && (theCurrentRead->bytesDone > 0)) + { + // We never finished the current read. + // We need to move its data into the front of the partial read buffer. + + [partialReadBuffer replaceBytesInRange:NSMakeRange(0, 0) + withBytes:[theCurrentRead->buffer bytes] + length:theCurrentRead->bytesDone]; + } + + [self emptyQueues]; +} + +- (void)emptyQueues +{ + if (theCurrentRead != nil) [self endCurrentRead]; + if (theCurrentWrite != nil) [self endCurrentWrite]; + [theReadQueue removeAllObjects]; + [theWriteQueue removeAllObjects]; + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(maybeDequeueRead) object:nil]; + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(maybeDequeueWrite) object:nil]; +} + +// Disconnects. This is called for both error and clean disconnections. +- (void)close +{ + // Empty queues. + [self emptyQueues]; + [partialReadBuffer release]; + partialReadBuffer = nil; + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(disconnect) object:nil]; + + // Close streams. + if (theReadStream != NULL) + { + CFReadStreamSetClient(theReadStream, kCFStreamEventNone, NULL, NULL); + CFReadStreamUnscheduleFromRunLoop (theReadStream, theRunLoop, kCFRunLoopDefaultMode); + CFReadStreamClose (theReadStream); + CFRelease (theReadStream); + theReadStream = NULL; + } + if (theWriteStream != NULL) + { + CFWriteStreamSetClient(theWriteStream, kCFStreamEventNone, NULL, NULL); + CFWriteStreamUnscheduleFromRunLoop (theWriteStream, theRunLoop, kCFRunLoopDefaultMode); + CFWriteStreamClose (theWriteStream); + CFRelease (theWriteStream); + theWriteStream = NULL; + } + + // Close sockets. + if (theSocket != NULL) + { + CFSocketInvalidate (theSocket); + CFRelease (theSocket); + theSocket = NULL; + } + if (theSocket6 != NULL) + { + CFSocketInvalidate (theSocket6); + CFRelease (theSocket6); + theSocket6 = NULL; + } + if (theSource != NULL) + { + CFRunLoopRemoveSource (theRunLoop, theSource, kCFRunLoopDefaultMode); + CFRelease (theSource); + theSource = NULL; + } + if (theSource6 != NULL) + { + CFRunLoopRemoveSource (theRunLoop, theSource6, kCFRunLoopDefaultMode); + CFRelease (theSource6); + theSource6 = NULL; + } + theRunLoop = NULL; + + // If the client has passed the connect/accept method, then the connection has at least begun. + // Notify delegate that it is now ending. + if (theFlags & kDidPassConnectMethod) + { + // Delay notification to give him freedom to release without returning here and core-dumping. + if ([theDelegate respondsToSelector: @selector(onSocketDidDisconnect:)]) + { + [theDelegate performSelector:@selector(onSocketDidDisconnect:) withObject:self afterDelay:0]; + } + } + + // Clear flags. + theFlags = 0x00; +} + +/** + * Disconnects immediately. Any pending reads or writes are dropped. + **/ +- (void)disconnect +{ + [self close]; +} + +/** + * Disconnects after all pending writes have completed. + * After calling this, the read and write methods (including "readDataWithTimeout:tag:") will do nothing. + * The socket will disconnect even if there are still pending reads. + **/ +- (void)disconnectAfterWriting +{ + theFlags |= kForbidReadsWrites; + theFlags |= kDisconnectSoon; + [self maybeScheduleDisconnect]; +} + +/** + * In the event of an error, this method may be called during onSocket:willDisconnectWithError: to read + * any data that's left on the socket. + **/ +- (NSData *)unreadData +{ + // Ensure this method will only return data in the event of an error + if(!(theFlags & kClosingWithError)) return nil; + + if(theReadStream == NULL) return nil; + + CFIndex totalBytesRead = [partialReadBuffer length]; + BOOL error = NO; + while(!error && CFReadStreamHasBytesAvailable(theReadStream)) + { + [partialReadBuffer increaseLengthBy:READALL_CHUNKSIZE]; + + // Number of bytes to read is space left in packet buffer. + CFIndex bytesToRead = [partialReadBuffer length] - totalBytesRead; + + // Read data into packet buffer + UInt8 *packetbuf = (UInt8 *)( [partialReadBuffer mutableBytes] + totalBytesRead ); + CFIndex bytesRead = CFReadStreamRead(theReadStream, packetbuf, bytesToRead); + + // Check results + if(bytesRead < 0) + { + error = YES; + } + else + { + totalBytesRead += bytesRead; + } + } + + [partialReadBuffer setLength:totalBytesRead]; + + return partialReadBuffer; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Errors +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Returns a standard error object for the current errno value. + * Errno is used for low-level BSD socket errors. + **/ +- (NSError *)getErrnoError +{ + NSString *errorMsg = [NSString stringWithUTF8String:strerror(errno)]; + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errorMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; +} + +/** + * Returns a standard error message for a CFSocket error. + * Unfortunately, CFSocket offers no feedback on its errors. + **/ +- (NSError *)getSocketError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"AsyncSocketCFSocketError", + @"AsyncSocket", [NSBundle mainBundle], + @"General CFSocket error", nil); + + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:AsyncSocketErrorDomain code:AsyncSocketCFSocketError userInfo:info]; +} + +- (NSError *) getStreamError +{ + CFStreamError err; + if (theReadStream != NULL) + { + err = CFReadStreamGetError (theReadStream); + if (err.error != 0) return [self errorFromCFStreamError: err]; + } + + if (theWriteStream != NULL) + { + err = CFWriteStreamGetError (theWriteStream); + if (err.error != 0) return [self errorFromCFStreamError: err]; + } + + return nil; +} + +/** + * Returns a standard AsyncSocket abort error. + **/ +- (NSError *)getAbortError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"AsyncSocketCanceledError", + @"AsyncSocket", [NSBundle mainBundle], + @"Connection canceled", nil); + + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:AsyncSocketErrorDomain code:AsyncSocketCanceledError userInfo:info]; +} + +- (NSError *)getReadMaxedOutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"AsyncSocketReadMaxedOutError", + @"AsyncSocket", [NSBundle mainBundle], + @"Read operation reached set maximum length", nil); + + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:AsyncSocketErrorDomain code:AsyncSocketReadMaxedOutError userInfo:info]; +} + +/** + * Returns a standard AsyncSocket read timeout error. + **/ +- (NSError *)getReadTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"AsyncSocketReadTimeoutError", + @"AsyncSocket", [NSBundle mainBundle], + @"Read operation timed out", nil); + + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:AsyncSocketErrorDomain code:AsyncSocketReadTimeoutError userInfo:info]; +} + +/** + * Returns a standard AsyncSocket write timeout error. + **/ +- (NSError *)getWriteTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"AsyncSocketWriteTimeoutError", + @"AsyncSocket", [NSBundle mainBundle], + @"Write operation timed out", nil); + + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:AsyncSocketErrorDomain code:AsyncSocketWriteTimeoutError userInfo:info]; +} + +- (NSError *)errorFromCFStreamError:(CFStreamError)err +{ + if (err.domain == 0 && err.error == 0) return nil; + + // Can't use switch; these constants aren't int literals. + NSString *domain = @"CFStreamError (unlisted domain)"; + NSString *message = nil; + + if(err.domain == kCFStreamErrorDomainPOSIX) { + domain = NSPOSIXErrorDomain; + } + else if(err.domain == kCFStreamErrorDomainMacOSStatus) { + domain = NSOSStatusErrorDomain; + } + else if(err.domain == kCFStreamErrorDomainMach) { + domain = NSMachErrorDomain; + } + else if(err.domain == kCFStreamErrorDomainNetDB) + { + domain = @"kCFStreamErrorDomainNetDB"; + message = [NSString stringWithCString:gai_strerror(err.error) encoding:NSASCIIStringEncoding]; + } + else if(err.domain == kCFStreamErrorDomainNetServices) { + domain = @"kCFStreamErrorDomainNetServices"; + } + else if(err.domain == kCFStreamErrorDomainSOCKS) { + domain = @"kCFStreamErrorDomainSOCKS"; + } + else if(err.domain == kCFStreamErrorDomainSystemConfiguration) { + domain = @"kCFStreamErrorDomainSystemConfiguration"; + } + else if(err.domain == kCFStreamErrorDomainSSL) { + domain = @"kCFStreamErrorDomainSSL"; + } + + NSDictionary *info = nil; + if(message != nil) + { + info = [NSDictionary dictionaryWithObject:message forKey:NSLocalizedDescriptionKey]; + } + return [NSError errorWithDomain:domain code:err.error userInfo:info]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Diagnostics +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)isConnected +{ + return [self isSocketConnected] && [self areStreamsConnected]; +} + +- (NSString *)connectedHost +{ + if(theSocket) + return [self connectedHost:theSocket]; + else + return [self connectedHost:theSocket6]; +} + +- (UInt16)connectedPort +{ + if(theSocket) + return [self connectedPort:theSocket]; + else + return [self connectedPort:theSocket6]; +} + +- (NSString *)localHost +{ + if(theSocket) + return [self localHost:theSocket]; + else + return [self localHost:theSocket6]; +} + +- (UInt16)localPort +{ + if(theSocket) + return [self localPort:theSocket]; + else + return [self localPort:theSocket6]; +} + +- (NSString *)connectedHost:(CFSocketRef)socket +{ + if (socket == NULL) return nil; + CFDataRef peeraddr; + NSString *peerstr = nil; + + if(socket && (peeraddr = CFSocketCopyPeerAddress(socket))) + { + peerstr = [self addressHost:peeraddr]; + CFRelease (peeraddr); + } + + return peerstr; +} + +- (UInt16)connectedPort:(CFSocketRef)socket +{ + if (socket == NULL) return 0; + CFDataRef peeraddr; + UInt16 peerport = 0; + + if(socket && (peeraddr = CFSocketCopyPeerAddress(socket))) + { + peerport = [self addressPort:peeraddr]; + CFRelease (peeraddr); + } + + return peerport; +} + +- (NSString *)localHost:(CFSocketRef)socket +{ + if (socket == NULL) return nil; + CFDataRef selfaddr; + NSString *selfstr = nil; + + if(socket && (selfaddr = CFSocketCopyAddress(socket))) + { + selfstr = [self addressHost:selfaddr]; + CFRelease (selfaddr); + } + + return selfstr; +} + +- (UInt16)localPort:(CFSocketRef)socket +{ + if (socket == NULL) return 0; + CFDataRef selfaddr; + UInt16 selfport = 0; + + if (socket && (selfaddr = CFSocketCopyAddress(socket))) + { + selfport = [self addressPort:selfaddr]; + CFRelease (selfaddr); + } + + return selfport; +} + +- (BOOL)isSocketConnected +{ + if(theSocket != NULL) + return CFSocketIsValid(theSocket); + else if(theSocket6 != NULL) + return CFSocketIsValid(theSocket6); + else + return NO; +} + +- (BOOL)areStreamsConnected +{ + CFStreamStatus s; + + if (theReadStream != NULL) + { + s = CFReadStreamGetStatus (theReadStream); + if ( !(s == kCFStreamStatusOpen || s == kCFStreamStatusReading || s == kCFStreamStatusError) ) + return NO; + } + else return NO; + + if (theWriteStream != NULL) + { + s = CFWriteStreamGetStatus (theWriteStream); + if ( !(s == kCFStreamStatusOpen || s == kCFStreamStatusWriting || s == kCFStreamStatusError) ) + return NO; + } + else return NO; + + return YES; +} + +- (NSString *)addressHost:(CFDataRef)cfaddr +{ + if (cfaddr == NULL) return nil; + + //char addrBuf[ MAX(INET_ADDRSTRLEN, INET6_ADDRSTRLEN) ]; + char addrBuf[46]; + struct sockaddr *pSockAddr = (struct sockaddr *) CFDataGetBytePtr (cfaddr); + struct sockaddr_in *pSockAddrV4 = (struct sockaddr_in *) pSockAddr; + struct sockaddr_in6 *pSockAddrV6 = (struct sockaddr_in6 *)pSockAddr; + + const void *pAddr = (pSockAddr->sa_family == AF_INET) ? + (void *)(&(pSockAddrV4->sin_addr)) : + (void *)(&(pSockAddrV6->sin6_addr)); + + const char *pStr = inet_ntop (pSockAddr->sa_family, pAddr, addrBuf, sizeof(addrBuf)); + if (pStr == NULL) [NSException raise: NSInternalInconsistencyException + format: @"Cannot convert address to string."]; + + return [NSString stringWithCString:pStr encoding:NSASCIIStringEncoding]; +} + +- (UInt16)addressPort:(CFDataRef)cfaddr +{ + if (cfaddr == NULL) return 0; + struct sockaddr_in *pAddr = (struct sockaddr_in *) CFDataGetBytePtr (cfaddr); + return ntohs (pAddr->sin_port); +} + +- (BOOL)isIPv4 +{ + return (theSocket != NULL); +} + +- (BOOL)isIPv6 +{ + return (theSocket6 != NULL); +} + +- (NSString *)description +{ + static const char *statstr[] = { "not open", "opening", "open", "reading", "writing", "at end", "closed", "has error" }; + CFStreamStatus rs = (theReadStream != NULL) ? CFReadStreamGetStatus (theReadStream) : 0; + CFStreamStatus ws = (theWriteStream != NULL) ? CFWriteStreamGetStatus (theWriteStream) : 0; + NSString *peerstr, *selfstr; + CFDataRef peeraddr = NULL, peeraddr6 = NULL, selfaddr = NULL, selfaddr6 = NULL; + + if (theSocket || theSocket6) + { + if (theSocket) peeraddr = CFSocketCopyPeerAddress(theSocket); + if (theSocket6) peeraddr6 = CFSocketCopyPeerAddress(theSocket6); + + if(theSocket6 && theSocket) + { + peerstr = [NSString stringWithFormat: @"%@/%@ %u", [self addressHost:peeraddr], [self addressHost:peeraddr6], [self addressPort:peeraddr]]; + } + else if(theSocket6) + { + peerstr = [NSString stringWithFormat: @"%@ %u", [self addressHost:peeraddr6], [self addressPort:peeraddr6]]; + } + else + { + peerstr = [NSString stringWithFormat: @"%@ %u", [self addressHost:peeraddr], [self addressPort:peeraddr]]; + } + + if(peeraddr) CFRelease(peeraddr); + if(peeraddr6) CFRelease(peeraddr6); + peeraddr = NULL; + peeraddr6 = NULL; + } + else peerstr = @"nowhere"; + + if (theSocket || theSocket6) + { + if (theSocket) selfaddr = CFSocketCopyAddress (theSocket); + if (theSocket6) selfaddr6 = CFSocketCopyAddress (theSocket6); + + if (theSocket6 && theSocket) + { + selfstr = [NSString stringWithFormat: @"%@/%@ %u", [self addressHost:selfaddr], [self addressHost:selfaddr6], [self addressPort:selfaddr]]; + } + else if (theSocket6) + { + selfstr = [NSString stringWithFormat: @"%@ %u", [self addressHost:selfaddr6], [self addressPort:selfaddr6]]; + } + else + { + selfstr = [NSString stringWithFormat: @"%@ %u", [self addressHost:selfaddr], [self addressPort:selfaddr]]; + } + + if(selfaddr) CFRelease(selfaddr); + if(selfaddr6) CFRelease(selfaddr6); + selfaddr = NULL; + selfaddr6 = NULL; + } + else selfstr = @"nowhere"; + + NSMutableString *ms = [[NSMutableString alloc] init]; + [ms appendString: [NSString stringWithFormat:@"buffer length] != 0) + percentDone = (float)theCurrentRead->bytesDone / + (float)[theCurrentRead->buffer length] * 100.0; + else + percentDone = 100; + + [ms appendString: [NSString stringWithFormat:@"currently read %u bytes (%d%% done), ", + [theCurrentRead->buffer length], + theCurrentRead->bytesDone ? percentDone : 0]]; + } + + if (theCurrentWrite == nil) + [ms appendString: @"no current write, "]; + else + { + int percentDone; + if ([theCurrentWrite->buffer length] != 0) + percentDone = (float)theCurrentWrite->bytesDone / + (float)[theCurrentWrite->buffer length] * 100.0; + else + percentDone = 100; + + [ms appendString: [NSString stringWithFormat:@"currently written %u (%d%%), ", + [theCurrentWrite->buffer length], + theCurrentWrite->bytesDone ? percentDone : 0]]; + } + + [ms appendString: [NSString stringWithFormat:@"read stream %p %s, write stream %p %s", theReadStream, statstr [rs], theWriteStream, statstr [ws] ]]; + if (theFlags & kDisconnectSoon) [ms appendString: @", will disconnect soon"]; + if (![self isConnected]) [ms appendString: @", not connected"]; + + [ms appendString: @">"]; + + return [ms autorelease]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Reading +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)readDataToLength:(CFIndex)length withTimeout:(NSTimeInterval)timeout tag:(long)tag; +{ + if(length == 0) return; + if(theFlags & kForbidReadsWrites) return; + + NSMutableData *buffer = [[NSMutableData alloc] initWithLength:length]; + AsyncReadPacket *packet = [[AsyncReadPacket alloc] initWithData:buffer + timeout:timeout + tag:tag + readAllAvailable:NO + terminator:nil + maxLength:length]; + + [theReadQueue addObject:packet]; + [self scheduleDequeueRead]; + + [packet release]; + [buffer release]; +} + +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataToData:data withTimeout:timeout maxLength:-1 tag:tag]; +} + +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(CFIndex)length tag:(long)tag +{ + if(data == nil || [data length] == 0) return; + if(length >= 0 && length < [data length]) return; + if(theFlags & kForbidReadsWrites) return; + + NSMutableData *buffer = [[NSMutableData alloc] initWithLength:0]; + AsyncReadPacket *packet = [[AsyncReadPacket alloc] initWithData:buffer + timeout:timeout + tag:tag + readAllAvailable:NO + terminator:data + maxLength:length]; + + [theReadQueue addObject:packet]; + [self scheduleDequeueRead]; + + [packet release]; + [buffer release]; +} + +- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + if (theFlags & kForbidReadsWrites) return; + + NSMutableData *buffer = [[NSMutableData alloc] initWithLength:0]; + AsyncReadPacket *packet = [[AsyncReadPacket alloc] initWithData:buffer + timeout:timeout + tag:tag + readAllAvailable:YES + terminator:nil + maxLength:-1]; + + [theReadQueue addObject:packet]; + [self scheduleDequeueRead]; + + [packet release]; + [buffer release]; +} + +/** + * Puts a maybeDequeueRead on the run loop. + * An assumption here is that selectors will be performed consecutively within their priority. + **/ +- (void)scheduleDequeueRead +{ + [self performSelector:@selector(maybeDequeueRead) withObject:nil afterDelay:0]; +} + +/** + * This method starts a new read, if needed. + * It is called when a user requests a read, + * or when a stream opens that may have requested reads sitting in the queue, etc. + **/ +- (void)maybeDequeueRead +{ + // If we're not currently processing a read AND + // we have read requests sitting in the queue AND we have actually have a read stream + if(theCurrentRead == nil && [theReadQueue count] != 0 && theReadStream != NULL) + { + // Get new current read AsyncReadPacket. + AsyncReadPacket *newPacket = [theReadQueue objectAtIndex:0]; + theCurrentRead = [newPacket retain]; + [theReadQueue removeObjectAtIndex:0]; + + // Start time-out timer. + if(theCurrentRead->timeout >= 0.0) + { + theReadTimer = [NSTimer scheduledTimerWithTimeInterval:theCurrentRead->timeout + target:self + selector:@selector(doReadTimeout:) + userInfo:nil + repeats:NO]; + } + + // Immediately read, if possible. + [self doBytesAvailable]; + } +} + +/** + * Call this method in doBytesAvailable instead of CFReadStreamHasBytesAvailable(). + * This method supports pre-buffering properly. + **/ +- (BOOL)hasBytesAvailable +{ + return ([partialReadBuffer length] > 0) || CFReadStreamHasBytesAvailable(theReadStream); +} + +/** + * Call this method in doBytesAvailable instead of CFReadStreamRead(). + * This method support pre-buffering properly. + **/ +- (CFIndex)readIntoBuffer:(UInt8 *)buffer maxLength:(CFIndex)length +{ + if([partialReadBuffer length] > 0) + { + // Determine the maximum amount of data to read + CFIndex bytesToRead = MIN(length, [partialReadBuffer length]); + + // Copy the bytes from the buffer + memcpy(buffer, [partialReadBuffer bytes], bytesToRead); + + // Remove the copied bytes from the buffer + [partialReadBuffer replaceBytesInRange:NSMakeRange(0, bytesToRead) withBytes:NULL length:0]; + + return bytesToRead; + } + else + { + return CFReadStreamRead(theReadStream, buffer, length); + } +} + +/** + * This method is called when a new read is taken from the read queue or when new data becomes available on the stream. + **/ +- (void)doBytesAvailable +{ + // If data is available on the stream, but there is no read request, then we don't need to process the data yet. + // Also, if there is a read request, but no read stream setup yet, we can't process any data yet. + if(theCurrentRead != nil && theReadStream != NULL) + { +#if 1 +#define BUFFERSIZE (1024*1024) // the default buffer chunk size + NSMutableData *buffer = [[NSMutableData alloc] initWithLength:BUFFERSIZE]; // buffer + while ( theReadStream && CFReadStreamHasBytesAvailable(theReadStream) ){ + // read 1K and call didReadData + CFIndex readSize = CFReadStreamRead( theReadStream, (UInt8*)[buffer bytes], BUFFERSIZE ); + if ( readSize > 0 ){ + if([theDelegate respondsToSelector:@selector(onSocket:didReadData:withTag:)]) + { + // when delegate is set, pass the current data(up to 1k size) to it. + + NSData *buffer_; + if ( readSize == BUFFERSIZE ){ + // when the buffer size is same as the max size, doesn't create a NSData instance. + buffer_ = buffer; + } + else{ + // create a NSData instance with readSize. + buffer_ = [[NSData alloc] initWithBytesNoCopy: (void*)[buffer bytes] length: readSize freeWhenDone:NO]; + } + [theDelegate onSocket:self didReadData: buffer_ withTag:theCurrentRead->tag]; + if ( readSize < BUFFERSIZE ){ + [buffer_ release]; + } + + } + } + if ( readSize <= 0 ){ + break; + } + } + [buffer release]; +#else + CFIndex totalBytesRead = 0; + + BOOL done = NO; + BOOL socketError = NO; + BOOL maxoutError = NO; + + while(!done && !socketError && !maxoutError && [self hasBytesAvailable]) + { + BOOL didPreBuffer = NO; + + // If reading all available data, make sure there's room in the packet buffer. + if(theCurrentRead->readAllAvailableData == YES) + { + // Make sure there is at least READALL_CHUNKSIZE bytes available. + // We don't want to increase the buffer any more than this or we'll waste space. + // With prebuffering it's possible to read in a small chunk on the first read. + + unsigned buffInc = READALL_CHUNKSIZE - ([theCurrentRead->buffer length] - theCurrentRead->bytesDone); + [theCurrentRead->buffer increaseLengthBy:buffInc]; + } + + // If reading until data, we may only want to read a few bytes. + // Just enough to ensure we don't go past our term or over our max limit. + // Unless pre-buffering is enabled, in which case we may want to read in a larger chunk. + if(theCurrentRead->term != nil) + { + // If we already have data pre-buffered, we obviously don't want to pre-buffer it again. + // So in this case we'll just read as usual. + + if(([partialReadBuffer length] > 0) || !(theFlags & kEnablePreBuffering)) + { + unsigned maxToRead = [theCurrentRead readLengthForTerm]; + + unsigned bufInc = maxToRead - ([theCurrentRead->buffer length] - theCurrentRead->bytesDone); + [theCurrentRead->buffer increaseLengthBy:bufInc]; + } + else + { + didPreBuffer = YES; + unsigned maxToRead = [theCurrentRead prebufferReadLengthForTerm]; + + unsigned buffInc = maxToRead - ([theCurrentRead->buffer length] - theCurrentRead->bytesDone); + [theCurrentRead->buffer increaseLengthBy:buffInc]; + + } + } + + // Number of bytes to read is space left in packet buffer. + CFIndex bytesToRead = [theCurrentRead->buffer length] - theCurrentRead->bytesDone; + + // Read data into packet buffer + UInt8 *subBuffer = (UInt8 *)([theCurrentRead->buffer mutableBytes] + theCurrentRead->bytesDone); + CFIndex bytesRead = [self readIntoBuffer:subBuffer maxLength:bytesToRead]; + + // Check results + if(bytesRead < 0) + { + socketError = YES; + } + else + { + // Update total amound read for the current read + theCurrentRead->bytesDone += bytesRead; + + // Update total amount read in this method invocation + totalBytesRead += bytesRead; + } + + // Is packet done? + if(theCurrentRead->readAllAvailableData != YES) + { + if(theCurrentRead->term != nil) + { + if(didPreBuffer) + { + // Search for the terminating sequence within the big chunk we just read. + CFIndex overflow = [theCurrentRead searchForTermAfterPreBuffering:bytesRead]; + + if(overflow > 0) + { + // Copy excess data into partialReadBuffer + NSMutableData *buffer = theCurrentRead->buffer; + const void *overflowBuffer = [buffer bytes] + theCurrentRead->bytesDone - overflow; + + [partialReadBuffer appendBytes:overflowBuffer length:overflow]; + + // Update the bytesDone variable. + // Note: The completeCurrentRead method will trim the buffer for us. + theCurrentRead->bytesDone -= overflow; + } + + done = (overflow >= 0); + } + else + { + // Search for the terminating sequence at the end of the buffer + int termlen = [theCurrentRead->term length]; + if(theCurrentRead->bytesDone >= termlen) + { + const void *buf = [theCurrentRead->buffer bytes] + (theCurrentRead->bytesDone - termlen); + const void *seq = [theCurrentRead->term bytes]; + done = (memcmp (buf, seq, termlen) == 0); + } + } + + if(!done && theCurrentRead->maxLength >= 0 && theCurrentRead->bytesDone >= theCurrentRead->maxLength) + { + // There's a set maxLength, and we've reached that maxLength without completing the read + maxoutError = YES; + } + } + else + { + // Done when (sized) buffer is full. + done = ([theCurrentRead->buffer length] == theCurrentRead->bytesDone); + } + } + // else readAllAvailable doesn't end until all readable is read. + } + + if(theCurrentRead->readAllAvailableData && theCurrentRead->bytesDone > 0) + done = YES; // Ran out of bytes, so the "read-all-data" type packet is done + + if(done) + { + [self completeCurrentRead]; + if (!socketError) [self scheduleDequeueRead]; + } + else if(theCurrentRead->bytesDone > 0) + { + // We're not done with the readToLength or readToData yet, but we have read in some bytes + if ([theDelegate respondsToSelector:@selector(onSocket:didReadPartialDataOfLength:tag:)]) + { + [theDelegate onSocket:self didReadPartialDataOfLength:totalBytesRead tag:theCurrentRead->tag]; + } + } + + if(socketError) + { + CFStreamError err = CFReadStreamGetError(theReadStream); + [self closeWithError:[self errorFromCFStreamError:err]]; + return; + } + if(maxoutError) + { + [self closeWithError:[self getReadMaxedOutError]]; + return; + } +#endif + } +} + +// Ends current read and calls delegate. +- (void)completeCurrentRead +{ + NSAssert (theCurrentRead, @"Trying to complete current read when there is no current read."); + + [theCurrentRead->buffer setLength:theCurrentRead->bytesDone]; + if([theDelegate respondsToSelector:@selector(onSocket:didReadData:withTag:)]) + { + [theDelegate onSocket:self didReadData:theCurrentRead->buffer withTag:theCurrentRead->tag]; + } + + if (theCurrentRead != nil) [self endCurrentRead]; // Caller may have disconnected. +} + +// Ends current read. +- (void)endCurrentRead +{ + NSAssert (theCurrentRead, @"Trying to end current read when there is no current read."); + + [theReadTimer invalidate]; + theReadTimer = nil; + + [theCurrentRead release]; + theCurrentRead = nil; +} + +- (void)doReadTimeout:(NSTimer *)timer +{ + if (timer != theReadTimer) return; // Old timer. Ignore it. + if (theCurrentRead != nil) + { + [self endCurrentRead]; + } + [self closeWithError:[self getReadTimeoutError]]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Writing +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; +{ + if (data == nil || [data length] == 0) return; + if (theFlags & kForbidReadsWrites) return; + + AsyncWritePacket *packet = [[AsyncWritePacket alloc] initWithData:data timeout:timeout tag:tag]; + + [theWriteQueue addObject:packet]; + [self scheduleDequeueWrite]; + + [packet release]; +} + +- (void)scheduleDequeueWrite +{ + [self performSelector:@selector(maybeDequeueWrite) withObject:nil afterDelay:0]; +} + +// Start a new write. +- (void)maybeDequeueWrite +{ + if (theCurrentWrite == nil && [theWriteQueue count] != 0 && theWriteStream != NULL) + { + // Get new current write AsyncWritePacket. + AsyncWritePacket *newPacket = [theWriteQueue objectAtIndex:0]; + theCurrentWrite = [newPacket retain]; + [theWriteQueue removeObjectAtIndex:0]; + + // Start time-out timer. + if (theCurrentWrite->timeout >= 0.0) + { + theWriteTimer = [NSTimer scheduledTimerWithTimeInterval:theCurrentWrite->timeout + target:self + selector:@selector(doWriteTimeout:) + userInfo:nil + repeats:NO]; + } + + // Immediately write, if possible. + [self doSendBytes]; + } +} + +- (void)doSendBytes +{ + if (theCurrentWrite != nil && theWriteStream != NULL) + { + BOOL done = NO, error = NO; + while (!done && !error && CFWriteStreamCanAcceptBytes (theWriteStream)) + { + // Figure out what to write. + CFIndex bytesRemaining = [theCurrentWrite->buffer length] - theCurrentWrite->bytesDone; + CFIndex bytesToWrite = (bytesRemaining < WRITE_CHUNKSIZE) ? bytesRemaining : WRITE_CHUNKSIZE; + UInt8 *writestart = (UInt8 *)([theCurrentWrite->buffer bytes] + theCurrentWrite->bytesDone); + + // Write. + CFIndex bytesWritten = CFWriteStreamWrite (theWriteStream, writestart, bytesToWrite); + + // Check results. + if (bytesWritten < 0) + { + bytesWritten = 0; + error = YES; + } + + // Is packet done? + theCurrentWrite->bytesDone += bytesWritten; + done = ([theCurrentWrite->buffer length] == theCurrentWrite->bytesDone); + } + + if(done) + { + [self completeCurrentWrite]; + if (!error) [self scheduleDequeueWrite]; + } + + if(error) + { + CFStreamError err = CFWriteStreamGetError (theWriteStream); + [self closeWithError: [self errorFromCFStreamError:err]]; + return; + } + } +} + +// Ends current write and calls delegate. +- (void)completeCurrentWrite +{ + NSAssert (theCurrentWrite, @"Trying to complete current write when there is no current write."); + + if ([theDelegate respondsToSelector:@selector(onSocket:didWriteDataWithTag:)]) + { + [theDelegate onSocket:self didWriteDataWithTag:theCurrentWrite->tag]; + } + + if (theCurrentWrite != nil) [self endCurrentWrite]; // Caller may have disconnected. +} + +// Ends current write. +- (void)endCurrentWrite +{ + NSAssert (theCurrentWrite, @"Trying to complete current write when there is no current write."); + + [theWriteTimer invalidate]; + theWriteTimer = nil; + + [theCurrentWrite release]; + theCurrentWrite = nil; + + [self maybeScheduleDisconnect]; +} + +// Checks to see if all writes have been completed for disconnectAfterWriting. +- (void)maybeScheduleDisconnect +{ + if(theFlags & kDisconnectSoon) + { + if(([theWriteQueue count] == 0) && (theCurrentWrite == nil)) + { + [self performSelector:@selector(disconnect) withObject:nil afterDelay:0]; + } + } +} + +- (void)doWriteTimeout:(NSTimer *)timer +{ + if (timer != theWriteTimer) return; // Old timer. Ignore it. + if (theCurrentWrite != nil) + { + [self endCurrentWrite]; + } + [self closeWithError:[self getWriteTimeoutError]]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark CF Callbacks +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)doCFSocketCallback:(CFSocketCallBackType)type + forSocket:(CFSocketRef)sock + withAddress:(NSData *)address + withData:(const void *)pData +{ + NSParameterAssert ((sock == theSocket) || (sock == theSocket6)); + + switch (type) + { + case kCFSocketConnectCallBack: + // The data argument is either NULL or a pointer to an SInt32 error code, if the connect failed. + if(pData) + [self doSocketOpen:sock withCFSocketError:kCFSocketError]; + else + [self doSocketOpen:sock withCFSocketError:kCFSocketSuccess]; + break; + case kCFSocketAcceptCallBack: + [self doAcceptWithSocket: *((CFSocketNativeHandle *)pData)]; + break; + default: + NSLog (@"AsyncSocket %p received unexpected CFSocketCallBackType %d.", self, type); + break; + } +} + +- (void)doCFReadStreamCallback:(CFStreamEventType)type forStream:(CFReadStreamRef)stream +{ + NSParameterAssert(theReadStream != NULL); + + CFStreamError err; + switch (type) + { + case kCFStreamEventOpenCompleted: + [self doStreamOpen]; + break; + case kCFStreamEventHasBytesAvailable: + [self doBytesAvailable]; + break; + case kCFStreamEventEndEncountered: + if([theDelegate respondsToSelector:@selector(onReadStreamEnded:)]){ + if([theDelegate onReadStreamEnded:self] == NO) break; + } + case kCFStreamEventErrorOccurred: + err = CFReadStreamGetError (theReadStream); + [self closeWithError: [self errorFromCFStreamError:err]]; + break; + default: + NSLog (@"AsyncSocket %p received unexpected CFReadStream callback, CFStreamEventType %d.", self, type); + } +} + +- (void)doCFWriteStreamCallback:(CFStreamEventType)type forStream:(CFWriteStreamRef)stream +{ + NSParameterAssert(theWriteStream != NULL); + + CFStreamError err; + switch (type) + { + case kCFStreamEventOpenCompleted: + [self doStreamOpen]; + break; + case kCFStreamEventCanAcceptBytes: + [self doSendBytes]; + break; + case kCFStreamEventErrorOccurred: + case kCFStreamEventEndEncountered: + err = CFWriteStreamGetError (theWriteStream); + [self closeWithError: [self errorFromCFStreamError:err]]; + break; + default: + NSLog (@"AsyncSocket %p received unexpected CFWriteStream callback, CFStreamEventType %d.", self, type); + } +} + +/** + * This is the callback we setup for CFSocket. + * This method does nothing but forward the call to it's Objective-C counterpart + **/ +static void MyCFSocketCallback (CFSocketRef sref, CFSocketCallBackType type, CFDataRef address, const void *pData, void *pInfo) +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + AsyncSocket *socket = [[(AsyncSocket *)pInfo retain] autorelease]; + [socket doCFSocketCallback:type forSocket:sref withAddress:(NSData *)address withData:pData]; + + [pool release]; +} + +/** + * This is the callback we setup for CFReadStream. + * This method does nothing but forward the call to it's Objective-C counterpart + **/ +static void MyCFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type, void *pInfo) +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + AsyncSocket *socket = [[(AsyncSocket *)pInfo retain] autorelease]; + [socket doCFReadStreamCallback:type forStream:stream]; + + [pool release]; +} + +/** + * This is the callback we setup for CFWriteStream. + * This method does nothing but forward the call to it's Objective-C counterpart + **/ +static void MyCFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo) +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + AsyncSocket *socket = [[(AsyncSocket *)pInfo retain] autorelease]; + [socket doCFWriteStreamCallback:type forStream:stream]; + + [pool release]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Class Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// Return line separators. ++ (NSData *)CRLFData +{ + return [NSData dataWithBytes:"\x0D\x0A" length:2]; +} + ++ (NSData *)CRData +{ + return [NSData dataWithBytes:"\x0D" length:1]; +} + ++ (NSData *)LFData +{ + return [NSData dataWithBytes:"\x0A" length:1]; +} + ++ (NSData *)ZeroData +{ + return [NSData dataWithBytes:"" length:1]; +} + +@end diff --git a/engine/platform/ios/FtpConnection.h b/engine/platform/ios/FtpConnection.h new file mode 100644 index 00000000..dc262166 --- /dev/null +++ b/engine/platform/ios/FtpConnection.h @@ -0,0 +1,129 @@ +/* + iosFtpServer + Copyright (C) 2008 Richard Dearlove ( monsta ) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + + +#import + +#import "AsyncSocket.h" + +#import "FtpDataConnection.h" +#import "FtpDefines.h" +#include +#include + + +@class FtpServer; + +@interface FtpConnection : NSObject { + AsyncSocket *connectionSocket; // Socket for this particular connection + FtpServer *server; // pointer to Server object + + AsyncSocket *dataListeningSocket; // Socket to listen for a data connection to spawn on <-- think this is now redundant + AsyncSocket *dataSocket; // duplicates the listening socket - remove listening socket from code when working - which seems to be the case. + + FtpDataConnection *dataConnection; // instance handling spawned data connection socket + + NSArray *msgComponents; // The Message rcvd broken into an array + UInt16 dataPort; + int transferMode; + NSMutableArray *queuedData; + + NSString *currentUser; // The current user for this connection + NSString *currentDir; // The current directory for this connection + NSString *currentFile; // File that is about to be uploaded + NSFileHandle *currentFileHandle; // File handle of what to save + + NSString *rnfrFilename; // rnfr + + +} + +-(id)initWithAsyncSocket:(AsyncSocket*)newSocket forServer:(id)myServer ; +#pragma mark STATE + +@property(readwrite)int transferMode; + +@property(readwrite, retain ) NSString *currentFile; +@property(readwrite, retain ) NSString *currentDir; +@property(readwrite, retain ) NSString *rnfrFilename; + +-(NSString*)connectionAddress; + + +#pragma mark ASYNCSOCKET DATACONN + +-(BOOL)openDataSocket:(int)portNumber; +-(int)choosePasvDataPort; + + +-(BOOL)onSocketWillConnect:(AsyncSocket *)sock; +-(void)onSocket:(AsyncSocket *)sock didAcceptNewSocket:(AsyncSocket *)newSocket; + +#pragma mark ASYNCSOCKET FTPCLIENT CONNECTION +-(void)onSocket:(AsyncSocket*)sock didReadData:(NSData*)data withTag:(long)tag; +-(void)onSocket:(AsyncSocket*)sock didWriteDataWithTag:(long)tag; +-(void)sendMessage:(NSString*)ftpMessage; // calls FC writedata +-(void)sendDataString:(NSString*)dataString; // calls FDC writedata +-(void)sendData:(NSMutableData*)data; +-(void)didReceiveDataWritten; // notification that FDC wrote data +-(void)didReceiveDataRead; // notification that FDC read data ie a transfer +-(void)didFinishReading; // Called at the closing == end of a data connection from the client we presume + +#pragma mark PROCESS + +-(void)processDataRead:(NSData*)data; +-(void)processCommand; + +#pragma mark COMMANDS +-(void)doQuit:(id)sender arguments:(NSArray*)arguments; +-(void)doUser:(id)sender arguments:(NSArray*)arguments; +-(void)doPass:(id)sender arguments:(NSArray*)arguments; +-(void)doStat:(id)sender arguments:(NSArray*)arguments; +-(void)doFeat:(id)sender arguments:(NSArray*)arguments; +-(void)doList:(id)sender arguments:(NSArray*)arguments; +-(void)doPwd:(id)sender arguments:(NSArray*)arguments; +-(void)doNoop:(id)sender arguments:(NSArray*)arguments; +-(void)doSyst:(id)sender arguments:(NSArray*)arguments; +-(void)doLprt:(id)sender arguments:(NSArray*)arguments; +-(void)doPasv:(id)sender arguments:(NSArray*)arguments; +-(void)doEpsv:(id)sender arguments:(NSArray*)arguments; +-(void)doPort:(id)sender arguments:(NSArray*)arguments; +-(void)doNlst:(id)sender arguments:(NSArray*)arguments; +-(void)doStor:(id)sender arguments:(NSArray*)arguments; +-(void)doRetr:(id)sender arguments:(NSArray*)arguments; +-(void)doDele:(id)sender arguments:(NSArray*)arguments; +-(void)doMlst:(id)sender arguments:(NSArray*)arguments; +-(void)doSize:(id)sender arguments:(NSArray*)arguments; +-(void)doMkdir:(id)sender arguments:(NSArray*)arguments; +-(void)doCdUp:(id)sender arguments:(NSArray*)arguments; +-(void)doRnfr:(id)sender arguments:(NSArray*)arguments; +-(void)doRnto:(id)sender arguments:(NSArray*)arguments; + +#pragma mark UTITILITES +-(NSString*)makeFilePathFrom:(NSString*)filename; +-(unsigned long long)fileSize:(NSString*)filePath; +-(NSString*)fileNameFromArgs:(NSArray*)arguments; +- (Boolean)changedCurrentDirectoryTo:(NSString *)newDirectory; +-(Boolean)canChangeDirectoryTo:(NSString *)testDirectory; +- (Boolean)accessibleFilePath:(NSString*)filePath; // check filepath exists and is in basedir ( if set ) +- (Boolean)validNewFilePath:(NSString*)filePath; +- (NSString *)visibleCurrentDir; +-(NSString *)rootedPath:(NSString*)path; + +@end diff --git a/engine/platform/ios/FtpConnection.m b/engine/platform/ios/FtpConnection.m new file mode 100644 index 00000000..5ab35d54 --- /dev/null +++ b/engine/platform/ios/FtpConnection.m @@ -0,0 +1,1220 @@ +/* + iosFtpServer + Copyright (C) 2008 Richard Dearlove ( monsta ) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +#import "FtpConnection.h" +#import "FtpServer.h" + + +@implementation FtpConnection + +@synthesize transferMode; +@synthesize currentFile; +@synthesize currentDir; +@synthesize rnfrFilename; + +// ---------------------------------------------------------------------------------------------------------- +-(id)initWithAsyncSocket:(AsyncSocket*)newSocket forServer:(id)myServer +// ---------------------------------------------------------------------------------------------------------- +{ + self = [super init ]; + if (self) + { + connectionSocket = [newSocket retain ]; + server = myServer; + [ connectionSocket setDelegate:self ]; + [ connectionSocket writeData:DATASTR(@"220 iosFtp server ready.\r\n") withTimeout:-1 tag:0 ]; // send out the welcome message to the client + [ connectionSocket readDataToData:[AsyncSocket CRLFData] withTimeout:READ_TIMEOUT tag:FTP_CLIENT_REQUEST ]; // start listening for commands on this connection to client + dataListeningSocket = nil; + dataPort=2001; + transferMode = pasvftp; + queuedData = [[ NSMutableArray alloc ] init ]; // A buffer for sending data when the connection isn't quite up yet + self.currentDir = [ server.baseDir copy]; // the working directory for this connection, Starts in the directory the server is set to. set chroot=true in server code to sandbox in + currentFile = nil; + currentFileHandle = nil; // not saving to a file yet + rnfrFilename = nil; + + currentUser = nil; + NSLog(@"FC: Current Directory starting at %@",currentDir ); + } + return self; +} +// ---------------------------------------------------------------------------------------------------------- +-(void)dealloc +// ---------------------------------------------------------------------------------------------------------- +{ + if(connectionSocket) + { + [connectionSocket setDelegate:nil]; + [connectionSocket disconnect]; + [connectionSocket release]; + } + + if(dataListeningSocket){ // 12/2/10 - think this code is now redundant, dataListeningSocket, does it do anything anymore...? + [dataListeningSocket setDelegate:nil]; + [dataListeningSocket disconnect]; + [dataListeningSocket release]; + + } + if(dataSocket) + { + [dataSocket setDelegate:nil]; + [dataSocket disconnect]; + [dataSocket release]; + + } + if(dataConnection)[dataConnection release]; + // [msgComponents release]; + if(queuedData)[queuedData release]; + if(currentFile)[currentFile release]; + if(currentUser)[currentUser release]; + [currentDir release]; + if(currentFileHandle) [currentFileHandle release]; + + [super dealloc]; +} +#pragma mark STATE +// ---------------------------------------------------------------------------------------------------------- +//-(void)setTransferMode:(int)mode +// ---------------------------------------------------------------------------------------------------------- +//{ +// transferMode = mode; +//} + +// ---------------------------------------------------------------------------------------------------------- +-(NSString*)connectionAddress +// ---------------------------------------------------------------------------------------------------------- +{ + return [connectionSocket connectedHost]; + +} + + +// ========================================================================================================== +#pragma mark CHOOSE DATA SOCKET +// ========================================================================================================== + + + +// ---------------------------------------------------------------------------------------------------------- +-(BOOL)openDataSocket:(int)portNumber +// ---------------------------------------------------------------------------------------------------------- +{ + NSString *responseString; + NSError *error = nil; + + if (dataSocket) // Code changed 2010-02-12 - As per Jon's fix. stops memory leak on socket and connection + { + [dataSocket release]; + dataSocket = nil; + } + dataSocket = [ [ AsyncSocket alloc ] initWithDelegate:self ]; // create our socket for Listening or Direct Connection + if (dataConnection) + { + [dataConnection release]; + dataConnection = nil; + } + + + + switch (transferMode) { + case portftp: + dataPort = portNumber; + responseString = [ NSString stringWithFormat:@"200 PORT command successful."]; + [ dataSocket connectToHost:[self connectionAddress] onPort:portNumber error:&error ]; + // sleep(1); + dataConnection = [[ FtpDataConnection alloc ] initWithAsyncSocket:dataSocket forConnection:self withQueuedData:queuedData ]; + + break; + + case lprtftp: // FIXME wrong return message + dataPort = portNumber; + responseString = [ NSString stringWithFormat:@"228 Entering Long Passive Mode (af, hal, h1, h2, h3,..., pal, p1, p2...)", dataPort >>8, dataPort & 0xff]; + [ dataSocket connectToHost:[self connectionAddress] onPort:portNumber error:&error ]; + dataConnection = [[ FtpDataConnection alloc ] initWithAsyncSocket:dataSocket forConnection:self withQueuedData:queuedData ]; + break; + + case eprtftp: + dataPort = portNumber; + responseString = @"200 EPRT command successful."; + [ dataSocket connectToHost:[self connectionAddress] onPort:portNumber error:&error ]; + dataConnection = [[ FtpDataConnection alloc ] initWithAsyncSocket:dataSocket forConnection:self withQueuedData:queuedData ]; + break; + + case pasvftp: + dataPort = [ self choosePasvDataPort ]; + NSString *address = [ [connectionSocket localHost ] stringByReplacingOccurrencesOfString:@"." withString:@"," ]; // autoreleased + responseString = [ NSString stringWithFormat:@"227 Entering Passive Mode (%@,%d,%d)",address, dataPort >>8, dataPort & 0xff]; + [ dataSocket acceptOnPort: dataPort error:&error ]; + dataConnection = nil; // will pickup from the listening socket + break; + + case epsvftp: + dataPort = [ self choosePasvDataPort ]; + responseString = [ NSString stringWithFormat:@"229 Entering Extended Passive Mode (|||%d|)", dataPort ]; + [ dataSocket acceptOnPort: dataPort error:&error ]; + + dataConnection = nil; // will pickup from the listening socket + break; + + + default: + break; + } + NSLog( @"-- %@", [ error localizedDescription ] ); + + [ self sendMessage:responseString ]; + + return YES; +} + + +// ---------------------------------------------------------------------------------------------------------- +-(int)choosePasvDataPort +// ---------------------------------------------------------------------------------------------------------- +{ + struct timeval tv; + unsigned short int seed[3]; + + gettimeofday(&tv, NULL); + seed[0] = (tv.tv_sec >> 16) & 0xFFFF; + seed[1] = tv.tv_sec & 0xFFFF; + seed[2] = tv.tv_usec & 0xFFFF; + seed48(seed); + + int portNumber; + portNumber = (lrand48() % 64512) + 1024; + // NSLog(@"New Port number is %i", portNumber ); + + return portNumber; // FIXME - presetting to 2001 for the moment + + //return 2001; + +} + + + +// ========================================================================================================== +#pragma mark ASYNCSOCKET DATACONNECTION +// ========================================================================================================== + + +// ---------------------------------------------------------------------------------------------------------- +-(BOOL)onSocketWillConnect:(AsyncSocket *)sock +// ---------------------------------------------------------------------------------------------------------- +{ + NSLog(@"FC:onSocketWillConnect"); + [ sock readDataWithTimeout:READ_TIMEOUT tag:0 ]; + + return YES; +} + +// ---------------------------------------------------------------------------------------------------------- +-(void)onSocket:(AsyncSocket *)sock didAcceptNewSocket:(AsyncSocket *)newSocket // should be for a data connection on 2001 +// ---------------------------------------------------------------------------------------------------------- +{ + // opened socket for passive data - new socket connected to this which is the passive connection + + NSLog(@"FC:New Connection -- should be for the data port"); + + dataConnection = [[ FtpDataConnection alloc ] initWithAsyncSocket:newSocket forConnection:self withQueuedData:queuedData]; +} + + + +// ========================================================================================================== +#pragma mark ASYNCSOCKET FTPCLIENT CONNECTION +// ========================================================================================================== + +// ---------------------------------------------------------------------------------------------------------- +-(void)onSocket:(AsyncSocket*)sock didReadData:(NSData*)data withTag:(long)tag // DATA READ +// ---------------------------------------------------------------------------------------------------------- +{ + + NSLog(@"FC:didReadData"); + [ connectionSocket readDataToData:[AsyncSocket CRLFData] withTimeout:READ_TIMEOUT tag:FTP_CLIENT_REQUEST ]; // start reading again + + [ self processDataRead:data ]; + +} + + +// ---------------------------------------------------------------------------------------------------------- +-(void)onSocket:(AsyncSocket*)sock didWriteDataWithTag:(long)tag // DATA WRITTEN +// ---------------------------------------------------------------------------------------------------------- +{ + [ connectionSocket readDataToData:[AsyncSocket CRLFData] withTimeout:READ_TIMEOUT tag:FTP_CLIENT_REQUEST ]; // start reading again + // NSLog(@"FC:didWriteData"); + +} + +// ---------------------------------------------------------------------------------------------------------- +-(void)sendMessage:(NSString*)ftpMessage // REDUNDANT really - FIXME +// ---------------------------------------------------------------------------------------------------------- +{ + NSLog(@">%@",ftpMessage ); + NSMutableData *dataString = [[ ftpMessage dataUsingEncoding:NSUTF8StringEncoding ] mutableCopy]; // Autoreleased + [ dataString appendData:[AsyncSocket CRLFData] ]; + + [ connectionSocket writeData:dataString withTimeout:READ_TIMEOUT tag:FTP_CLIENT_REQUEST ]; + [dataString release]; + [ connectionSocket readDataToData:[AsyncSocket CRLFData] withTimeout:READ_TIMEOUT tag:FTP_CLIENT_REQUEST ]; // start reading again + // [ connectionSocket readDataWithTimeout:READ_TIMEOUT tag:FTP_CLIENT_REQUEST ]; +} +// ---------------------------------------------------------------------------------------------------------- +-(void)sendDataString:(NSString*)dataString +// ---------------------------------------------------------------------------------------------------------- +{ + NSMutableString *message = [[NSMutableString alloc] initWithString:dataString]; + CFStringNormalize((CFMutableStringRef)message, kCFStringNormalizationFormC); + NSMutableData *data = [[ message dataUsingEncoding:server.clientEncoding ] mutableCopy]; // Autoreleased + [message release]; + + if (dataConnection ) + { NSLog(@"FC:sendData"); + [ dataConnection writeData:data ]; + + } + else + { + [ queuedData addObject:data ]; + } + [data release]; + +} +// ---------------------------------------------------------------------------------------------------------- +-(void)sendData:(NSMutableData*)data +// ---------------------------------------------------------------------------------------------------------- +{ + if (dataConnection ) + { NSLog(@"FC:sendData"); + [ dataConnection writeData:data ]; + } + else + { + [ queuedData addObject:data ]; + } + +} +// ---------------------------------------------------------------------------------------------------------- +-(void)didReceiveDataWritten // notification from FtpDataConnection that the dataWasWritten +// ---------------------------------------------------------------------------------------------------------- +{ + + + + NSLog(@"SENDING COMPLETED"); + + [ self sendMessage:@"226 Transfer complete." ]; // send completed message to client + [ dataConnection closeConnection ]; +} + +// ---------------------------------------------------------------------------------------------------------- +-(void)didReceiveDataRead // notification from FtpDataConnection that the dataWasWritten +// ---------------------------------------------------------------------------------------------------------- +{ + + NSLog(@"FC:didReceiveDataRead"); + + // must have sent a file + + // Should start writing file out if not written yet : FIXNOW + + if ( currentFileHandle != nil ) + { + // Append data on + NSLog(@"FC:Writing File to %@", currentFile ); + [ currentFileHandle writeData:dataConnection.receivedData ]; + + } + else + { + NSLog(@"Couldnt write data"); + } + + + +} + +// ---------------------------------------------------------------------------------------------------------- +-(void)didFinishReading // Called at the end of a data connection from the client we presume +// ---------------------------------------------------------------------------------------------------------- +{ + if (currentFile) + { + + NSLog(@"Closing File Handle"); + [currentFile release]; + currentFile = nil; + } + else + { + NSLog(@"FC:Data Sent but not sure where its for "); + } + + [ self sendMessage:@"226 Transfer complete." ]; // send completed message to client + + // [ dataConnection closeConnection ]; // It must be closed, it dropped us + + if ( currentFileHandle != nil ) + { + NSLog(@"Closing File Handle"); + + [ currentFileHandle closeFile ]; // Close the file handle + [ currentFileHandle release ]; + currentFileHandle = nil; + [ server didReceiveFileListChanged]; + } + + dataConnection.connectionState = clientQuiet; + +} + + +// ========================================================================================================== +#pragma mark PROCESS +// ========================================================================================================= + +// ---------------------------------------------------------------------------------------------------------- +-(void)processDataRead:(NSData*)data // convert to commands as Client Connection +// ---------------------------------------------------------------------------------------------------------- +{ + NSData *strData = [data subdataWithRange:NSMakeRange(0, [data length] - 2)]; // remove last 2 chars + NSString *crlfmessage = [[[NSString alloc] initWithData:strData encoding:server.clientEncoding] autorelease]; + NSString *message; + + // message = [ crlfmessage stringByReplacingOccurrencesOfString:@"\r\n" withString:@""]; // gets autoreleased + + message = [ crlfmessage stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet ]]; + NSLog(@"<%@",message ); + msgComponents = [message componentsSeparatedByString:@" "]; // change this to use spaces - for the FTP protocol + + [ self processCommand ]; + + [connectionSocket readDataToData:[AsyncSocket CRLFData] withTimeout:-1 tag:0 ]; // force to readdata CHECK +} + +// ---------------------------------------------------------------------------------------------------------- +-(void)processCommand // assumes data has been place in Array msgComponents +// ---------------------------------------------------------------------------------------------------------- +{ + NSString *commandString = [ msgComponents objectAtIndex:0]; + + if ([ commandString length ] > 0) // If there is a command here + { + // Search through dictionary for correct matching command and method that it calls + + NSString *commandSelector = [ [[server commands] objectForKey:[commandString lowercaseString] ] stringByAppendingString:@"arguments:"]; + + if ( commandSelector ) // If we have a matching command + { + + SEL action = NSSelectorFromString(commandSelector); // Turn into a method + + if ( [ self respondsToSelector:action ]) // If we respond to this method + { + // DO COMMAND + [self performSelector:action withObject:self withObject:msgComponents ]; // Preform method with arguments + } + else + { // UNKNOWN COMMAND + NSString *outputString =[ NSString stringWithFormat:@"500 '%@': command not understood.", commandString ]; + [ self sendMessage:outputString ]; + + NSLog(@"DONT UNDERSTAND"); + } + } + else // UNKNOWN COMMAND + { + NSString *outputString =[ NSString stringWithFormat:@"500 '%@': command not understood.", commandString ]; + [ self sendMessage:outputString ]; + } + } + else + { + // Write out an error msg + } + +} + +// ========================================================================================================== +#pragma mark COMMANDS +// ========================================================================================================== + +// ---------------------------------------------------------------------------------------------------------- +-(void)doQuit:(id)sender arguments:(NSArray*)arguments +// ---------------------------------------------------------------------------------------------------------- +{ + NSLog(@"Quit : %@",arguments); + + [ self sendMessage:@"221- Data traffic for this session was 0 bytes in 0 files"]; + + [self sendMessage:@"221 Thank you for using the FTP service on localhost." ]; + + if(connectionSocket) + { + [connectionSocket disconnectAfterWriting ]; // Will this close the socket ? + // [connectionSocket disconnect]; + } + + [ server closeConnection:self ]; // Tell the server to close us down, remove us from the list of connections + + + // FIXME - delete the dataconnection if its open + + +} +// ---------------------------------------------------------------------------------------------------------- +-(void)doUser:(id)sender arguments:(NSArray*)arguments +// ---------------------------------------------------------------------------------------------------------- +{ + // send out confirmation message -- 331 password required for + if ( currentUser != nil ) + [currentUser release]; + currentUser = [[ arguments objectAtIndex:1 ] retain]; + NSString *outputString = [ NSString stringWithFormat:@"331 Password required for %@", currentUser ]; + [ sender sendMessage:outputString]; +} +// ---------------------------------------------------------------------------------------------------------- +-(void)doPass:(id)sender arguments:(NSArray*)arguments +// ---------------------------------------------------------------------------------------------------------- +{ +// NSString *pass = [ arguments objectAtIndex:1 ]; + NSString *outputString = [ NSString stringWithFormat:@"230 User %@ logged in.", currentUser ]; + [ sender sendMessage:outputString]; +} +// ---------------------------------------------------------------------------------------------------------- +-(void)doStat:(id)sender arguments:(NSArray*)arguments +// ---------------------------------------------------------------------------------------------------------- +{ + // Send out stat message + [ sender sendMessage:@"211-localhost FTP server status:"]; + // FIXME - add in the stats + [ sender sendMessage:@"211 End of Status"]; +} +// ---------------------------------------------------------------------------------------------------------- +-(void)doFeat:(id)sender arguments:(NSArray*)arguments +// ---------------------------------------------------------------------------------------------------------- +{ + [ sender sendMessage:@"211-Features supported"]; + + // If encoding is UTF8, notify the client + if (server.clientEncoding == NSUTF8StringEncoding) + [ sender sendMessage:@" UTF8" ]; + + [ sender sendMessage:@"211 End"]; +} + + + + +// ---------------------------------------------------------------------------------------------------------- +-(void)doList:(id)sender arguments:(NSArray*)arguments +// ---------------------------------------------------------------------------------------------------------- +{ + // Get the name of any additional directory that we are asked to list, if its empty we use the current directory + + NSString *lsDir = [ self fileNameFromArgs:arguments] ; // autoreleased, get the directory that we're being asked for + NSString *listText; + + + if ([lsDir length]<1) { + lsDir = currentDir; + } + else { + lsDir = [self rootedPath:lsDir ]; + } + + + + NSLog( @"doList currentDir(%@) changeRoot%d", lsDir, server.changeRoot ); + NSLog(@"Will list %@ ",lsDir); + listText = [ [ server createList:lsDir] retain ]; // Can list directory so do it +// if ([self canChangeDirectoryTo:lsDir]) { +// listText = [ [ server createList:lsDir] retain ]; // Can list directory so do it +// } +// else { +// listText = @""; // return nothing as not in chroot +// } + + NSLog( @"doList sending this. %@", listText ); + + [ sender sendMessage:@"150 Opening ASCII mode data connection for '/bin/ls'."]; + + [ sender sendDataString:listText ]; + [listText release ]; +} +// ---------------------------------------------------------------------------------------------------------- +-(void)doPwd:(id)sender arguments:(NSArray*)arguments +// ---------------------------------------------------------------------------------------------------------- +{ + + NSLog(@"Will PWD %@ ",[self visibleCurrentDir]); + + // CHECKME - changed to show basedir - currentdir + + + NSString *cmdString = [ NSString stringWithFormat:@"257 \"%@\" is the current directory.", [self visibleCurrentDir] ]; // autoreleased + [ sender sendMessage:cmdString ]; // FIXME - seems to be buggy on ftp command line client + +} +// ---------------------------------------------------------------------------------------------------------- +-(void)doNoop:(id)sender arguments:(NSArray*)arguments +// ---------------------------------------------------------------------------------------------------------- +{ + [sender sendMessage:@"200 NOOP command successful." ]; + +} +// ---------------------------------------------------------------------------------------------------------- +-(void)doSyst:(id)sender arguments:(NSArray*)arguments +// ---------------------------------------------------------------------------------------------------------- +{ + [ sender sendMessage:@"215 UNIX Type: L8 Version: iosFtp 20080912" ]; +} +// ---------------------------------------------------------------------------------------------------------- +-(void)doLprt:(id)sender arguments:(NSArray*)arguments +// ---------------------------------------------------------------------------------------------------------- +{ + // LPRT,"6,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,231,92" + NSString *socketDesc = [ arguments objectAtIndex:1 ] ; + NSArray *socketAddr = [ socketDesc componentsSeparatedByString:@"," ]; + + int hb = [[socketAddr objectAtIndex:19] intValue ]; + int lb = [[socketAddr objectAtIndex:20] intValue ]; + + NSLog(@"%d %d %d",hb <<8, hb,lb ); + int clientPort = (hb <<8 ) + lb; + + [sender setTransferMode:lprtftp]; + + [ sender openDataSocket:clientPort ]; +} +// ---------------------------------------------------------------------------------------------------------- +-(void)doEprt:(id)sender arguments:(NSArray*)arguments +// ---------------------------------------------------------------------------------------------------------- +{ + // EPRT |2|1080::8:800:200C:417A|5282| + + NSString *socketDesc = [ arguments objectAtIndex:1 ] ; + NSArray *socketAddr = [ socketDesc componentsSeparatedByString:@"|" ]; + + NSString *item; + for (item in socketAddr) { + NSLog(@"%@", item); + } + int clientPort = [[ socketAddr objectAtIndex:3 ] intValue ]; + + NSLog(@"Got Send Port %d", clientPort ); + + [sender setTransferMode:eprtftp]; + // [ sender initDataSocket:clientPort ]; + [ sender openDataSocket:clientPort ]; +} + + +// ---------------------------------------------------------------------------------------------------------- +-(void)doPasv:(id)sender arguments:(NSArray*)arguments +// ---------------------------------------------------------------------------------------------------------- +{ + [sender setTransferMode:pasvftp]; + // [ sender initDataSocket:0 ]; + [ sender openDataSocket:0 ]; + +} +// ---------------------------------------------------------------------------------------------------------- +-(void)doEpsv:(id)sender arguments:(NSArray*)arguments +// ---------------------------------------------------------------------------------------------------------- +{ + // FIXME - open a port random high address + [sender setTransferMode:epsvftp]; + // [ sender initDataSocket:0 ]; + [ sender openDataSocket:0 ]; +} +// ---------------------------------------------------------------------------------------------------------- +-(void)doPort:(id)sender arguments:(NSArray*)arguments +// ---------------------------------------------------------------------------------------------------------- +{ + int hb, lb; + + // PORT 127,0,0,1,197,251 looks like this + // get 2nd argument and split up by , and then take the last 2 bits + + NSString *socketDesc = [ arguments objectAtIndex:1 ] ; + NSArray *socketAddr = [ socketDesc componentsSeparatedByString:@"," ]; + + hb = [[socketAddr objectAtIndex:4] intValue ]; + lb = [[socketAddr objectAtIndex:5] intValue ]; + + + int clientPort = (hb <<8 ) + lb; + + + [sender setTransferMode:portftp]; + + [ sender openDataSocket:clientPort ]; + +} + +// ---------------------------------------------------------------------------------------------------------- +-(void)doOpts:(id)sender arguments:(NSArray*)arguments +// ---------------------------------------------------------------------------------------------------------- +{ + NSString *cmd = [ arguments objectAtIndex:1 ]; + NSString *cmdstr = [ NSString stringWithFormat:@"502 Unknown command '%@'",cmd ]; + [ sender sendMessage:cmdstr ]; + // 502 Unknown command 'sj' +} +// ---------------------------------------------------------------------------------------------------------- +-(void)doType:(id)sender arguments:(NSArray*)arguments +// ---------------------------------------------------------------------------------------------------------- +{ + // 200 Type set to A. + + // FIXME - change the data output to the matching type -- we dont do anything with this yet,, there are many types apart from this one. + + NSString *cmd = [ arguments objectAtIndex:1 ]; + + //if ( [ [cmd lowercaseString] isEqualToString:@"i" ]) + // { + NSString *cmdstr = [ NSString stringWithFormat:@"200 Type set to %@.",cmd ]; + [ sender sendMessage:cmdstr ]; + // } + // else + // { + // NSString *cmdstr = [ NSString stringWithFormat:@"500 'type %@': command not understood.",cmd ]; + // [ sender sendMessage:cmdstr ]; + // } +} +// ---------------------------------------------------------------------------------------------------------- +-(void)doCwd:(id)sender arguments:(NSArray*)arguments +// ---------------------------------------------------------------------------------------------------------- +{ + // 250 + // 500, 501, 502, 421, 530, 550 + // 250 CWD command successful. + + NSLog(@"DoCwd arguments is %@",arguments); + + NSString *cmdstr; + NSString *cwdDir = [ self fileNameFromArgs:arguments] ; // autoreleased, get the directory that we're being asked for + + if ( [ self changedCurrentDirectoryTo:cwdDir ] ) // tries to change to that directory, checks in bounds and viable + { + cmdstr = [ NSString stringWithFormat:@"250 OK. Current directory is %@", [self visibleCurrentDir]]; // currentDir is now in the new place + cmdstr = @"250 CWD command successful."; + } + else + { + cmdstr = @"550 CWD failed."; + } + + [ sender sendMessage:cmdstr]; + + NSLog(@"currentDir is now %@",currentDir ); + +} +// ---------------------------------------------------------------------------------------------------------- +-(void)doNlst:(id)sender arguments:(NSArray*)arguments +// ---------------------------------------------------------------------------------------------------------- +{ + [self doList:sender arguments:arguments ]; +} +// ---------------------------------------------------------------------------------------------------------- +-(void)doStor:(id)sender arguments:(NSArray*)arguments +// ---------------------------------------------------------------------------------------------------------- +{ + // eg STOR my filename here.mp3 STOR=0 my=1 filename=2 etc + + NSFileManager *fs = [NSFileManager defaultManager]; + NSString *filename = [ self fileNameFromArgs:arguments]; // autoreleased + NSString *cmdstr; + + self.currentFile = [self makeFilePathFrom:filename]; // makes a filepath, using absolute or relative filename + + // Check this falls within the area of filesystem we are allowed to write to + if ( [self validNewFilePath:self.currentFile] ) // FIXME - finish function for test + { + // CREATE and then OPEN NEW FILE FOR WRITING + if ([fs createFileAtPath:self.currentFile contents:nil attributes:nil]==YES) + { + currentFileHandle = [[ NSFileHandle fileHandleForWritingAtPath:currentFile] retain]; // Open the file handle to write to + cmdstr = [ NSString stringWithFormat:@"150 Opening BINARY mode data connection for '%@'.",filename ]; // autoreleased + } + else + { + // couldn't make file, send out the error + cmdstr = [ NSString stringWithFormat:@"553 %@: Permission denied.", filename ]; + + } + } + else + { + // couldn't make file as out of root area + cmdstr = [ NSString stringWithFormat:@"553 %@: Permission denied.", filename ]; + } + + NSLog(@"FC:doStor %@", currentFile ); + + + [sender sendMessage:cmdstr]; + + // CHECKME - data connection should have been brought up by client? + if (dataConnection ) // if not we're in trouble. mind u currentFile is doing similar job. + { + NSLog(@"FC:setting connection state to clientSending"); + dataConnection.connectionState = clientSending; + } + else + { + NSLog(@"FC:Erorr Cant set connection state to Client Sending : no Connection yet "); + } + + + +} + +// ---------------------------------------------------------------------------------------------------------- +-(void)doRetr:(id)sender arguments:(NSArray*)arguments // DOWNLOAD to CLIENT +// ---------------------------------------------------------------------------------------------------------- +{ + BOOL isDir; + NSString *cmdstr; + + NSString *filename = [self fileNameFromArgs:arguments]; // autoreleased + NSString *filePath = [self makeFilePathFrom:filename]; // turns relative or absolute path into format we need + + NSLog(@"FC:doRetr: %@", filePath ); + + if ( [self accessibleFilePath:filePath ] ) // if this filepath is in rooted area, and is a real file + { + if ( [ [ NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory: &isDir ]) // FIXME - fold into previous line + { + if ( isDir ){ // reject if its a directory request + [ sender sendMessage: [NSString stringWithFormat:@"550 %@: Not a plain file.",filename]]; + } + else // SEND FILE + { // FIXME URGENT - need to stop loading whole file into memory to send + NSMutableData *fileData = [[ NSMutableData dataWithContentsOfFile:filePath ] retain]; // FIXME - open in bits ? seems risky opening file in one piece + cmdstr = [ NSString stringWithFormat:@"150 Opening BINARY mode data connection for '%@'.",filename ]; + [sender sendMessage:cmdstr]; + + [ sender sendData:fileData ]; // Send file + [fileData release ]; + } + } + } + else // doesn't exist or not in basedir sandbox + { + cmdstr = [ NSString stringWithFormat:@"50 %@ No such file or directory.",filename ]; + NSLog(@"FC:doRetr: file %@ doesnt' exist ", filePath); + [sender sendMessage:cmdstr]; + } + + +} + +// ---------------------------------------------------------------------------------------------------------- +-(void)doDele:(id)sender arguments:(NSArray*)arguments +// ---------------------------------------------------------------------------------------------------------- +{ + NSString *cmdStr; + NSError *error; + NSString *filename =[self fileNameFromArgs:arguments]; // autoreleased + NSString *filePath = [self makeFilePathFrom:filename]; + + NSLog(@"filename is %@",filename); + + // attempt to delete the file + + if ( [self accessibleFilePath:filePath ]) // exists and can access + { + if ([[ NSFileManager defaultManager ] removeItemAtPath:filePath error:&error ]) + { + cmdStr = [ NSString stringWithFormat:@"250 DELE command successful.",filename ]; + [ server didReceiveFileListChanged]; // Addition by Brendan copied in by Rich 16/12/08 + } + else + { + cmdStr = [ NSString stringWithFormat:@"550 DELE command unsuccessful.",filename ]; // FIXME put correct error code in + } + } + else + { + cmdStr = [ NSString stringWithFormat:@"550 %@ No such file or directory.", filename]; + } + + [ sender sendMessage:cmdStr ]; + + +} +// ---------------------------------------------------------------------------------------------------------- +-(void)doMlst:(id)sender arguments:(NSArray*)arguments +// ---------------------------------------------------------------------------------------------------------- +{ + NSString *filename = [self fileNameFromArgs: arguments]; + NSString *cmdstr = [ NSString stringWithFormat:@"150 Opening BINARY mode data connection for '%@'.",filename ]; + // tell connection to expect a file + + [sender sendMessage:cmdstr]; // FIXME - this doesn't do anything beyond respond with first message + /* typiccal output to generate + 250: MLST 2012.pdf + Type=file;Size=2420017;Modify=20080808074805;Perm=adfrw;Unique=AgAADpIHZwA; /Users/monsta/Documents/2012.pdf + End + */ +} +// ---------------------------------------------------------------------------------------------------------- +-(void)doSize:(id)sender arguments:(NSArray*)arguments +// ---------------------------------------------------------------------------------------------------------- +{ + NSString *cmdStr; + NSString *filename = [self fileNameFromArgs: arguments]; // Autoreleased + NSString *filePath = [self makeFilePathFrom:filename]; // Autoreleased + + if ([self accessibleFilePath:filePath]) // Can we reach that file ? + { + if ([self fileSize:filePath] < 10240) // If small enough for old style size command + { + cmdStr = [ NSString stringWithFormat:@"213 %qu",[self fileSize:filePath] ]; // report size + } + else + { + cmdStr = [ NSString stringWithFormat:@"550 %@ file too large for SIZE.",filename ]; + } + } + else + { + cmdStr = [ NSString stringWithFormat:@"550 %@ No such file or directory.",filename ]; // report file not found + } + + // tell connection to expect a file + + [sender sendMessage:cmdStr]; + +} + + +// ---------------------------------------------------------------------------------------------------------- +-(void)doMkdir:(id)sender arguments:(NSArray*)arguments +// ---------------------------------------------------------------------------------------------------------- +{ + NSLog(@"current dir is %@",currentDir); + + NSString *cmdStr; + NSString *p = [self makeFilePathFrom:[self fileNameFromArgs:arguments]]; + NSFileManager *fs = [NSFileManager defaultManager]; + + if ( [self validNewFilePath:p ]) // FIXME - make sure function works. see function + { + if( [fs fileExistsAtPath:p isDirectory:nil] ) + { + cmdStr = [ NSString stringWithFormat:@"Error %@ exists",[self fileNameFromArgs:arguments] ]; + } + else + { + // FIXME - check that its ok to make a directory in this position + + [fs createDirectoryAtPath:p withIntermediateDirectories:YES attributes:nil error:nil]; + cmdStr = [ NSString stringWithFormat:@"250 MKD command successful."]; + + } + } + [sender sendMessage:cmdStr]; +} + +// ---------------------------------------------------------------------------------------------------------- +-(void)doCdUp:(id)sender arguments:(NSArray*)arguments +// ---------------------------------------------------------------------------------------------------------- +{ + NSLog(@"CurrentDir is %@",[self visibleCurrentDir]); + + NSString *upDir=[[self visibleCurrentDir] stringByDeletingLastPathComponent]; + + + + if ( [self changedCurrentDirectoryTo:upDir] ) // checks to see if its ok before moving + { + [sender sendMessage:@"250 CDUP command successful."]; + } + else + { + // create message saying you cant go to that directory - FIXME + [ sender sendMessage:@"550 CDUP command failed." ]; // CHECKME - look at a typical ftp mkdr command failure message + } +} + +// ---------------------------------------------------------------------------------------------------------- +-(void)doRnfr:(id)sender arguments:(NSArray*)arguments +// ---------------------------------------------------------------------------------------------------------- +{ + self.rnfrFilename = [self makeFilePathFrom:[self fileNameFromArgs:arguments]]; + + if ( [ self accessibleFilePath:self.rnfrFilename ] ) // FIXME - finish function + { + + if ( [[ NSFileManager defaultManager] fileExistsAtPath: rnfrFilename ] ) + { + [ sender sendMessage:@"350 RNFR command successful." ]; + } + else + [ sender sendMessage:@"550 RNFR command failed." ]; + } +} + +// ---------------------------------------------------------------------------------------------------------- +-(void)doRnto:(id)sender arguments:(NSArray*)arguments +// ---------------------------------------------------------------------------------------------------------- +{ + // FIXME - check its ok to use the new filename - ie in sandbox/basedir + + if ( self.rnfrFilename == nil ){ + [ sender sendMessage:@"550 RNTO command failed." ]; + return; + } + + NSError *error; + NSString *rntoFilename = [self makeFilePathFrom:[self fileNameFromArgs:arguments]]; + + NSLog( @"%@", rnfrFilename ); + NSLog( @"%@", rntoFilename ); + + if ([self validNewFilePath:rntoFilename]) // FIXME - finish function + { + if ( [[ NSFileManager defaultManager] moveItemAtPath:rnfrFilename toPath: rntoFilename error:&error ] ){ + [ server didReceiveFileListChanged]; + [ sender sendMessage:@"250 RNTO command successful." ]; + } + else{ + NSString *errorString = [error localizedDescription]; + NSLog( @"RNTO failed %@", errorString ); + [ sender sendMessage:@"550 RNTO command failed." ]; + } +// [rnfrFilename release]; +// rnfrFilename = nil; +// [rntoFilename release]; // auto released, should be fine to just forget + + } + else + { + [ sender sendMessage:@"550 RNTO command failed." ]; // CHECKME - have a look at a typical server response for this error + } +} + +///////////////////////////////////////// +#pragma mark UTILITIES + +// ---------------------------------------------------------------------------------------------------------- +-(NSString*)makeFilePathFrom:(NSString*)filename +// ---------------------------------------------------------------------------------------------------------- +{ + if ( [filename characterAtIndex:0] == '/' ) // if absolute file position + { + if ( server.changeRoot ) // then its actually relative to basedir + { + return [ [server.baseDir stringByAppendingPathComponent: filename] stringByResolvingSymlinksInPath ]; + } + else + { + return [[filename copy] autorelease ]; // Don't want to hang onto a variable, user should retain if needed. + } + + } + else // add onto the current file postion + { + return [ currentDir stringByAppendingPathComponent:filename]; // FIXME / CHECKME - need to set autorelease or retain etc. + + } +} + +// ---------------------------------------------------------------------------------------------------------- +-(unsigned long long)fileSize:(NSString*)filePath +// ---------------------------------------------------------------------------------------------------------- +{ + + + NSError *error; + NSDictionary *fileAttribs = [ [ NSFileManager defaultManager ] attributesOfItemAtPath:filePath error:&error ]; + + NSNumber *fileSize = [ fileAttribs valueForKey:NSFileSize ]; + NSLog(@"File size is %qu ", [fileSize unsignedLongLongValue]); + return [ fileSize unsignedLongLongValue]; + +} +// ---------------------------------------------------------------------------------------------------------- +-(NSString*)fileNameFromArgs:(NSArray*)arguments +// ---------------------------------------------------------------------------------------------------------- +{ + NSString *filename = [NSString string]; + NSLog(@"%@", [arguments description ] ); + if ([arguments count] >1) { + + + for ( int n=1; n<[arguments count]; n++) { // Start at arg2, + // test to see if argument begins with a - as it might be just a parameter, not the name + if (![[[arguments objectAtIndex:n] substringToIndex:1] isEqualToString:@"-"]) + { + if ([filename length]==0) { + filename = [arguments objectAtIndex:n ]; + } + else { + filename = [ NSString stringWithFormat:@"%@ %@", filename, [arguments objectAtIndex:n ] ]; // autoreleased + } + + + } + else { + NSLog(@"HYPHEN FOUND IGNORE"); + } + + + } + } + + + + return filename ; // autoreleased +} + + +// ---------------------------------------------------------------------------------------------------------- +- (Boolean)changedCurrentDirectoryTo:(NSString *)newDirectory // NOTE - there is probably a unix way of expanding pathnames and taking all the ../. stuff out. : should look this up +// ---------------------------------------------------------------------------------------------------------- +{ + NSFileManager *fileManager = [ NSFileManager defaultManager ]; + + NSString *currentDirectory = [ fileManager currentDirectoryPath ]; // store current directory + NSString *testDirectory, *expandedPath; + + + expandedPath = [[self rootedPath:newDirectory ] retain ]; + + // TEST IF ALLOWED TO USE THAT DIRECTORY ( ie is it in allowable filesystem ) + + if (![self canChangeDirectoryTo:expandedPath]) { + return false; + } + + + // CHANGE TO NEW DIRECTORY & GET SYSTEM FILE PATH ( no .. etc ) + if ( ! [ fileManager changeCurrentDirectoryPath:expandedPath ] ) // try changing to new directory + { + return false; // Not a valid directory + } + + testDirectory = [ fileManager currentDirectoryPath ]; // get new directory as string + + + // CHANGE BACK + if ( ! [ fileManager changeCurrentDirectoryPath:currentDirectory ] )// Change back to our own directory + { + return false; // Not a valid directory, shouldnt happen, but could u know + } + + + + self.currentDir = [ testDirectory copy ]; // make a copy, and retain release + + [ expandedPath release]; + return true; // its fine. would have failed before if not possible +} +// ---------------------------------------------------------------------------------------------------------- +-(Boolean)canChangeDirectoryTo:(NSString *)testDirectory // NOTE - there is probably a unix way of expanding pathnames and taking all the ../. stuff out. : should look this up +// ---------------------------------------------------------------------------------------------------------- +{ + if ( [server changeRoot] && ( ! [ testDirectory hasPrefix:server.baseDir ]) ) { + return false; + } + else { + return true; + } + + +} +// ---------------------------------------------------------------------------------------------------------- +- (Boolean)accessibleFilePath:(NSString*)filePath // checks its a proper name, and also in the chroot sandbox if set +// ---------------------------------------------------------------------------------------------------------- +{ + // check, file is accessible within servers rights + // && file exists + + // FIXME - need to expand the filename reference somehow to check where it really is, whether in sandbox. praps look at tnftpd src code for this. + + return [ [ NSFileManager defaultManager ] fileExistsAtPath:filePath ]; +} +// ---------------------------------------------------------------------------------------------------------- +- (Boolean)validNewFilePath:(NSString*)filePath +// ---------------------------------------------------------------------------------------------------------- +{ + return true; // FIXME - check this is within the area we can write to +} +// ---------------------------------------------------------------------------------------------------------- +- (NSString *)visibleCurrentDir +// ---------------------------------------------------------------------------------------------------------- +{ + + if ( server.changeRoot ) // if root changed, to basedir + { + int alength = [server.baseDir length ]; // length of basedir + + NSLog(@"Length is %u", alength ); + NSString *aString = [ currentDir substringFromIndex:alength ]; // get the bit after basedir + + if ( ! [ aString hasSuffix:@"/" ] ) // add a / if needed + { + aString = [ aString stringByAppendingString:@"/" ]; + } + + + // return with basedir prefix removed. + + return aString; // returns just the end part, not the base part + } + else + { + return currentDir; // return complete path + } + + +} +// ---------------------------------------------------------------------------------------------------------- +-(NSString *)rootedPath:(NSString*)path +// ---------------------------------------------------------------------------------------------------------- +{ + NSString *expandedPath; + + // GET FULL PATH OF NEW DIRECTORY + if ( [ path characterAtIndex:0 ] == '/') // if its an absolute path + { + if ( server.changeRoot ) // if rooted, its a relative path really, so add to basedir + { + expandedPath = [ [server.baseDir stringByAppendingPathComponent: path] stringByResolvingSymlinksInPath ]; + } + else // it really is an absolute path + { + expandedPath = path; // use the absolute path + } + } + else // or append onto currentdir ( the one the client chose ), as its a relative path + { + expandedPath =[[ currentDir stringByAppendingPathComponent:path ] stringByResolvingSymlinksInPath]; + } + + expandedPath = [ expandedPath stringByStandardizingPath ]; + + return expandedPath; // Autoreleased etc + +} +@end diff --git a/engine/platform/ios/FtpDataConnection.h b/engine/platform/ios/FtpDataConnection.h new file mode 100644 index 00000000..057ab84c --- /dev/null +++ b/engine/platform/ios/FtpDataConnection.h @@ -0,0 +1,54 @@ +/* + iosFtpServer + Copyright (C) 2008 Richard Dearlove ( monsta ) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#import +#import "AsyncSocket.h" +#import "FtpDefines.h" + +@class FtpConnection; + +@interface FtpDataConnection : NSObject { + AsyncSocket *dataSocket; + FtpConnection *ftpConnection; // connection which generated data socket we are tied to + + AsyncSocket *dataListeningSocket; + id dataConnection; + NSMutableData *receivedData; + int connectionState; + +} +-(id)initWithAsyncSocket:(AsyncSocket*)newSocket forConnection:(id)aConnection withQueuedData:(NSMutableArray*)queuedData; +-(void)writeString:(NSString*)dataString; +-(void)writeData:(NSMutableData*)data; +-(void)writeQueuedData:(NSMutableArray*)queuedData; +-(void)closeConnection; + +#pragma mark ASYNCSOCKET DELEGATES +-(BOOL)onSocketWillConnect:(AsyncSocket *)sock; +-(void)onSocket:(AsyncSocket *)sock didAcceptNewSocket:(AsyncSocket *)newSocket; +-(void)onSocket:(AsyncSocket*)sock didReadData:(NSData*)data withTag:(long)tag; +-(void)onSocket:(AsyncSocket*)sock didWriteDataWithTag:(long)tag; + +-(void)onSocket:(AsyncSocket *)sock willDisconnectWithError:(NSError *)err; + +@property (readonly) NSMutableData *receivedData; +@property (readwrite) int connectionState; + + +@end diff --git a/engine/platform/ios/FtpDataConnection.m b/engine/platform/ios/FtpDataConnection.m new file mode 100644 index 00000000..f80fb047 --- /dev/null +++ b/engine/platform/ios/FtpDataConnection.m @@ -0,0 +1,192 @@ +/* + iosFtpServer + Copyright (C) 2008 Richard Dearlove ( monsta ) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#import "FtpDataConnection.h" +#import "FtpConnection.h" + + +@implementation FtpDataConnection + +@synthesize receivedData; +@synthesize connectionState; + +// ---------------------------------------------------------------------------------------------------------- +-(id)initWithAsyncSocket:(AsyncSocket*)newSocket forConnection:(id)aConnection withQueuedData:(NSMutableArray*)queuedData +// ---------------------------------------------------------------------------------------------------------- +{ + self = [super init ]; + if (self) + { + dataSocket = [newSocket retain ]; // Hang onto the socket that was generated - the FDC is retained by FC + ftpConnection = aConnection; + + [ dataSocket setDelegate:self ]; + + if ( [queuedData count ] ) + { + NSLog(@"FC:Write Queued Data"); + [self writeQueuedData:queuedData ]; + [ queuedData removeAllObjects ]; // Clear out queue + } + // [ dataSocket readDataToData:[AsyncSocket CRLFData] withTimeout:READ_TIMEOUT tag:FTP_CLIENT_REQUEST ]; + [ dataSocket readDataWithTimeout:READ_TIMEOUT tag:FTP_CLIENT_REQUEST ]; + dataListeningSocket = nil; + receivedData = nil; // [[ NSMutableData alloc ] init ] 12/nov/08 - no need for this. rcd is just a pointer + + connectionState = clientQuiet; // Nothing coming through + } + return self; +} +// ---------------------------------------------------------------------------------------------------------- +-(void)dealloc +// ---------------------------------------------------------------------------------------------------------- +{ + + [dataSocket release]; + [dataListeningSocket release]; + [dataConnection release]; +// [receivedData release]; + + + [super dealloc]; +} +// ---------------------------------------------------------------------------------------------------------- +-(void)writeString:(NSString*)dataString +// ---------------------------------------------------------------------------------------------------------- +{ + NSLog(@"FDC:writeStringData"); + + NSMutableData *data = [[ dataString dataUsingEncoding:NSUTF8StringEncoding ] mutableCopy]; // Autoreleased + [ data appendData:[AsyncSocket CRLFData] ]; + + [ dataSocket writeData:data withTimeout:READ_TIMEOUT tag:FTP_CLIENT_REQUEST ]; + [ dataSocket readDataWithTimeout:READ_TIMEOUT tag:FTP_CLIENT_REQUEST ]; +} + +// ---------------------------------------------------------------------------------------------------------- +-(void)writeData:(NSMutableData*)data +// ---------------------------------------------------------------------------------------------------------- +{ + NSLog(@"FDC:writeData"); +// [ data appendData:[AsyncSocket CRLFData] ]; // Add on CRLF to end of data - as Windows Explorer needs it + + connectionState = clientReceiving; // We hope + + [ dataSocket writeData:data withTimeout:READ_TIMEOUT tag:FTP_CLIENT_REQUEST ]; + + [ dataSocket readDataWithTimeout:READ_TIMEOUT tag:FTP_CLIENT_REQUEST ]; +} +// ---------------------------------------------------------------------------------------------------------- +-(void)writeQueuedData:(NSMutableArray*)queuedData +// ---------------------------------------------------------------------------------------------------------- +{ + for (NSMutableData* data in queuedData) { + [self writeData:data ]; + } +} + +// ---------------------------------------------------------------------------------------------------------- +-(void)closeConnection +// ---------------------------------------------------------------------------------------------------------- +{ + NSLog(@"FDC:closeConnection"); + [ dataSocket disconnect ]; + +} +#pragma mark ASYNCSOCKET DELEGATES +// ---------------------------------------------------------------------------------------------------------- +-(BOOL)onSocketWillConnect:(AsyncSocket *)sock +// ---------------------------------------------------------------------------------------------------------- +{ + + NSLog(@"FDC:onSocketWillConnect"); + [ dataSocket readDataWithTimeout:READ_TIMEOUT tag:0 ]; + return YES; +} + +// ---------------------------------------------------------------------------------------------------------- +-(void)onSocket:(AsyncSocket *)sock didAcceptNewSocket:(AsyncSocket *)newSocket +// ---------------------------------------------------------------------------------------------------------- +{ + // This shouldnt happen - we should be connected already - and havent set up a listening socket + NSLog(@"FDC:New Connection -- shouldn't be called"); + +} + + + +// ---------------------------------------------------------------------------------------------------------- +-(void)onSocket:(AsyncSocket*)sock didReadData:(NSData*)data withTag:(long)tag +// ---------------------------------------------------------------------------------------------------------- +{ + NSLog(@"FDC:didReadData"); + +// [ dataSocket readDataToData:[AsyncSocket CRLFData] withTimeout:READ_TIMEOUT tag:FTP_CLIENT_REQUEST ]; // continue reading + [ dataSocket readDataWithTimeout:READ_TIMEOUT tag:FTP_CLIENT_REQUEST ]; + + NSMutableData *mutdata = [data mutableCopy]; + receivedData = [ mutdata retain]; // make autoreleased copy of data + + // notify connection data came through, ( will write data for us ) + [ftpConnection didReceiveDataRead ]; // notify, the connection, so it knows to write the data + [ receivedData release ]; // let go, not our business anymore + connectionState = clientSent; +} + + +// ---------------------------------------------------------------------------------------------------------- +-(void)onSocket:(AsyncSocket*)sock didWriteDataWithTag:(long)tag +// ---------------------------------------------------------------------------------------------------------- +{ + NSLog(@"FDC:didWriteData"); + [ ftpConnection didReceiveDataWritten ]; // notify that we are finished writing + +// [ dataSocket readDataToData:[AsyncSocket CRLFData] withTimeout:READ_TIMEOUT tag:FTP_CLIENT_REQUEST ]; // continue reading + [ dataSocket readDataWithTimeout:READ_TIMEOUT tag:FTP_CLIENT_REQUEST ]; +} + + +// ---------------------------------------------------------------------------------------------------------- +-(void)onSocket:(AsyncSocket *)sock willDisconnectWithError:(NSError *)err +// ---------------------------------------------------------------------------------------------------------- +{ + NSLog(@"FDC:willDisconnect"); + // if we were writing and there's no error, then it must be the end of file + + if ( connectionState == clientSending ) + { + NSLog(@"FDC::did FinishReading"); + // hopefully this is the end of the connection. not sure how we can tell + } + else + { + NSLog(@"FDC: we werent expecting this as we never set clientSending prob late coming up"); + } + [ ftpConnection didFinishReading ]; // its over, please send the message +} + +- (BOOL)onReadStreamEnded:(AsyncSocket*)sock +{ + NSLog( @"FDC: onReadStreamEnded %d(clientSending is %d)", connectionState, clientSending ); + if ( connectionState == clientSent || + connectionState == clientSending ) return YES; + return NO; +} + +@end diff --git a/engine/platform/ios/FtpDefines.h b/engine/platform/ios/FtpDefines.h new file mode 100644 index 00000000..5830ac25 --- /dev/null +++ b/engine/platform/ios/FtpDefines.h @@ -0,0 +1,15 @@ +enum { + pasvftp=0,epsvftp,portftp,lprtftp, eprtftp +}; + +#define DATASTR(args) [ args dataUsingEncoding:NSUTF8StringEncoding ] + +#define SERVER_PORT 20000 +#define READ_TIMEOUT -1 + +#define FTP_CLIENT_REQUEST 0 + +enum { + + clientSending=0, clientReceiving=1, clientQuiet=2,clientSent=3 +}; diff --git a/engine/platform/ios/FtpServer.h b/engine/platform/ios/FtpServer.h new file mode 100644 index 00000000..f736c3d7 --- /dev/null +++ b/engine/platform/ios/FtpServer.h @@ -0,0 +1,72 @@ +/* + iosFtpServer + Copyright (C) 2008 Richard Dearlove ( monsta ) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + + +#import + + +#import "AsyncSocket.h" +#import "FtpDefines.h" +#import "FtpConnection.h" +#import "list.h" + +@interface FtpServer : NSObject { + + AsyncSocket *listenSocket; + NSMutableArray *connectedSockets; + id server; + id notificationObject; + + int portNumber; + id delegate; + + NSMutableArray *connections; + + NSDictionary *commands; + NSString *baseDir; + Boolean changeRoot; // Change root to virtual root ( basedir ) + int clientEncoding; // FTP client encoding type +} +- (id)initWithPort:(unsigned)serverPort withDir:(NSString *)aDirectory notifyObject:(id)sender; +- (void)stopFtpServer; + +#pragma mark ASYNCSOCKET DELEGATES +- (void)onSocket:(AsyncSocket *)sock didAcceptNewSocket:(AsyncSocket *)newSocket; +- (void)onSocket:(AsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port; + +#pragma mark NOTIFICATIONS +- (void)didReceiveFileListChanged; + +#pragma mark CONNECTIONS +- (void)closeConnection:(id)theConnection; +- (NSString *)createList:(NSString *)directoryPath; + +@property (readwrite, retain) AsyncSocket *listenSocket; +@property (readwrite, retain) NSMutableArray *connectedSockets; +@property (readwrite, retain) id server; +@property (readwrite, retain) id notificationObject; +@property (readwrite) int portNumber; +@property (readwrite, retain) id delegate; +@property (readwrite, retain) NSMutableArray *connections; +@property (readwrite, retain) NSDictionary *commands; +@property (readwrite, retain) NSString *baseDir; +@property (readwrite) Boolean changeRoot; +@property (readwrite) int clientEncoding; + +@end diff --git a/engine/platform/ios/FtpServer.m b/engine/platform/ios/FtpServer.m new file mode 100644 index 00000000..5f801561 --- /dev/null +++ b/engine/platform/ios/FtpServer.m @@ -0,0 +1,198 @@ +/* + iosFtpServer + Copyright (C) 2008 Richard Dearlove ( monsta ) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + + +// 11/02/08 changes to made to allow stopping of the server. Also added a bit of code to show users IP address. +// Cleaned up the code to make the retains and releases a bit more obvious... mostly programming style preferences. +// Added a stop Ftp server method to be called from where ever it was start. See iphoneLibTestViewController.m to see how I was calling it. +// Most users wouldn't want the server to continue to run once the transfers are done. + + +#import "FtpServer.h" + +@implementation FtpServer + +@synthesize listenSocket, connectedSockets, server, notificationObject, portNumber, delegate, commands, baseDir, connections; + +@synthesize clientEncoding; +@synthesize changeRoot; + +// ---------------------------------------------------------------------------------------------------------- +- (id)initWithPort:(unsigned)serverPort withDir:(NSString*)aDirectory notifyObject:(id)sender +// ---------------------------------------------------------------------------------------------------------- +{ + if( self = [super init] ) { + + NSError *error = nil; + + self.notificationObject = sender; + + // Load up commands + NSString *plistPath = [[ NSBundle mainBundle ] pathForResource:@"ftp_commands" ofType:@"plist"]; + if ( ! [ [ NSFileManager defaultManager ] fileExistsAtPath:plistPath ] ) + { + NSLog(@"ftp_commands.plist missing"); + } + commands = [ [ NSDictionary alloc ] initWithContentsOfFile:plistPath]; + + // Clear out connections list + NSMutableArray *myConnections = [[NSMutableArray alloc] init]; + self.connections = myConnections; + [myConnections release]; + + + + // Create a socket + self.portNumber = serverPort; + + AsyncSocket *myListenSocket = [[AsyncSocket alloc] initWithDelegate:self]; + self.listenSocket = myListenSocket; + [myListenSocket release]; + + NSLog(@"Listening on %u", portNumber); + [listenSocket acceptOnPort:serverPort error:&error]; // start lisetning on this port. + + NSMutableArray *myConnectedSockets = [[NSMutableArray alloc] initWithCapacity:1]; + self.connectedSockets = myConnectedSockets; + [myConnectedSockets release]; + + // Set directory - have to do this because on iphone, some directories arent what they report back as, so need real path it resolves to, CHECKME - might be an easier way + NSFileManager *fileManager = [ NSFileManager defaultManager ]; + NSString *expandedPath = [ aDirectory stringByStandardizingPath ]; + + // CHANGE TO NEW DIRECTORY & GET SYSTEM FILE PATH ( no .. etc ) + if ([ fileManager changeCurrentDirectoryPath:expandedPath ]) // try changing to directory + { + + self.baseDir = [[ fileManager currentDirectoryPath ] copy] ; // Gets the real path. CHECKME. + // self.baseDir = @"/Users"; // REMOVEME - added for testing 7/6/10 + } + else + { + self.baseDir = aDirectory; // shouldnt get to this line really + } + + self.changeRoot = false; // true if you want them to be sandboxed/chrooted into the basedir + + // the default client encoding is UTF8 + self.clientEncoding = NSUTF8StringEncoding; + } + return self; +} +// ---------------------------------------------------------------------------------------------------------- +-(void)stopFtpServer +// ---------------------------------------------------------------------------------------------------------- +{ + if(listenSocket)[listenSocket disconnect]; + [connectedSockets removeAllObjects]; + + [connections removeAllObjects]; + +} +#pragma mark ASYNCSOCKET DELEGATES +// ---------------------------------------------------------------------------------------------------------- +- (void)onSocket:(AsyncSocket *)sock didAcceptNewSocket:(AsyncSocket *)newSocket +// ---------------------------------------------------------------------------------------------------------- +{ + + FtpConnection *newConnection = [[[ FtpConnection alloc ] initWithAsyncSocket:newSocket forServer:self] autorelease]; // Create an ftp connection + + + + + [ connections addObject:newConnection ]; // Add this to our list of connections + + NSLog(@"FS:didAcceptNewSocket port:%i", [sock localPort]); + + if ([sock localPort] == portNumber ) + { + NSLog(@"Connection on Server Port"); + + } + else + { + // must be a data comms port + // spawn a data comms port + // look for the connection with the same port + // and attach it + NSLog(@"--ERROR %i, %i", [sock localPort],portNumber); + + } +} + +// ---------------------------------------------------------------------------------------------------------- +- (void)onSocket:(AsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port +// ---------------------------------------------------------------------------------------------------------- +{ + NSLog(@"FtpServer:didConnectToHost port:%i", [sock localPort]); +} + +#pragma mark NOTIFICATIONS +// ---------------------------------------------------------------------------------------------------------- +-(void)didReceiveFileListChanged +// ---------------------------------------------------------------------------------------------------------- +{ + if ([notificationObject respondsToSelector:@selector(didReceiveFileListChanged)]) + [notificationObject didReceiveFileListChanged ]; +} +#pragma mark CONNECTIONS +// ---------------------------------------------------------------------------------------------------------- +- (void)closeConnection:(id)theConnection +// ---------------------------------------------------------------------------------------------------------- +{ + // Search through connections for this one - and delete + // this should release it - and delloc + + [connections removeObject:theConnection ]; + + +} + +// ---------------------------------------------------------------------------------------------------------- +-(NSString*)createList:(NSString*)directoryPath +// ---------------------------------------------------------------------------------------------------------- +{ + return createList(directoryPath); + + +} + +// ---------------------------------------------------------------------------------------------------------- +- (void)dealloc +// ---------------------------------------------------------------------------------------------------------- +{ + + + if(listenSocket) + { + [listenSocket disconnect]; + [listenSocket release]; + } + + [connectedSockets release]; + [notificationObject release]; + [connections release]; + [commands release]; + [baseDir release]; + [super dealloc]; + +} + + +@end diff --git a/engine/platform/ios/NetworkController.h b/engine/platform/ios/NetworkController.h new file mode 100644 index 00000000..5723a419 --- /dev/null +++ b/engine/platform/ios/NetworkController.h @@ -0,0 +1,26 @@ +// +// networkController.h +// DiddyDJ +// +// Created by Richard Dearlove on 21/10/2008. +// Copyright 2008 DiddySoft. All rights reserved. +// + +#import +#import +#include +#include +#include + #include +#include + +@interface NetworkController : NSObject { + +} ++ (NSString *)localWifiIPAddress; ++ (NSString *) localIPAddress; ++ (BOOL)addressFromString:(NSString *)IPAddress address:(struct sockaddr_in *)address; ++ (NSString *) getIPAddressForHost: (NSString *) theHost; ++ (BOOL) hostAvailable: (NSString *) theHost; ++ (BOOL) connectedToNetwork; +@end diff --git a/engine/platform/ios/NetworkController.m b/engine/platform/ios/NetworkController.m new file mode 100644 index 00000000..55893a84 --- /dev/null +++ b/engine/platform/ios/NetworkController.m @@ -0,0 +1,184 @@ +// +// networkController.m +// +// Created by Richard Dearlove on 21/10/2008. +// Copyright 2008 DiddySoft. All rights reserved. +// + +#import "NetworkController.h" + +@implementation NetworkController + +// Return the localized IP address + +// ---------------------------------------------------------------------------------------------------------- ++ (NSString *)localWifiIPAddress +// ---------------------------------------------------------------------------------------------------------- + +{ + NSString *address = @"error"; + struct ifaddrs *interfaces = NULL; + struct ifaddrs *temp_addr = NULL; + int success = 0; + + // retrieve the current interfaces - returns 0 on success + success = getifaddrs(&interfaces); + if (success == 0) + { + // Loop through linked list of interfaces + temp_addr = interfaces; + while(temp_addr != NULL) + { + if(temp_addr->ifa_addr->sa_family == AF_INET) + { + // Check if interface is en0 which is the wifi connection on the iPhone + if([[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"en0"]) + { + // Get NSString from C String + address = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr)]; + } + } + + temp_addr = temp_addr->ifa_next; + } + } + + // Free memory + freeifaddrs(interfaces); + + return address; +} +// ---------------------------------------------------------------------------------------------------------- + +// ---------------------------------------------------------------------------------------------------------- ++ (NSString *) localIPAddress +// ---------------------------------------------------------------------------------------------------------- +{ + char baseHostName[255]; + gethostname(baseHostName, 255); + + // Adjust for iPhone -- add .local to the host name + char hn[255]; + sprintf(hn, "%s.local", baseHostName); + + struct hostent *host = gethostbyname(hn); + if (host == NULL) + { + herror("resolv"); + return NULL; + } + else { + struct in_addr **list = (struct in_addr **)host->h_addr_list; + return [NSString stringWithCString:inet_ntoa(*list[0])]; + } + + return NULL; +} + +// ---------------------------------------------------------------------------------------------------------- ++ (BOOL)addressFromString:(NSString *)IPAddress address:(struct sockaddr_in *)address +// ---------------------------------------------------------------------------------------------------------- +{ + if (!IPAddress || ![IPAddress length]) { + return NO; + } + + memset((char *) address, sizeof(struct sockaddr_in), 0); + address->sin_family = AF_INET; + address->sin_len = sizeof(struct sockaddr_in); + + int conversionResult = inet_aton([IPAddress UTF8String], &address->sin_addr); + if (conversionResult == 0) { + NSAssert1(conversionResult != 1, @"Failed to convert the IP address string into a sockaddr_in: %@", IPAddress); + return NO; + } + + return YES; +} + +// ---------------------------------------------------------------------------------------------------------- ++ (NSString *) getIPAddressForHost: (NSString *) theHost +// ---------------------------------------------------------------------------------------------------------- +{ + struct hostent *host = gethostbyname([theHost UTF8String]); + + if (host == NULL) { + herror("resolv"); + return NULL; + } + + struct in_addr **list = (struct in_addr **)host->h_addr_list; + NSString *addressString = [NSString stringWithCString:inet_ntoa(*list[0])]; + return addressString; +} + + +// ---------------------------------------------------------------------------------------------------------- ++ (BOOL) hostAvailable: (NSString *) theHost +// ---------------------------------------------------------------------------------------------------------- +{ + + NSString *addressString = [self getIPAddressForHost:theHost]; + if (!addressString) + { + printf("Error recovering IP address from host name\n"); + return NO; + } + + struct sockaddr_in address; + BOOL gotAddress = [self addressFromString:addressString address:&address]; + + if (!gotAddress) + { + printf("Error recovering sockaddr address from %s\n", [addressString UTF8String]); + return NO; + } + + SCNetworkReachabilityRef defaultRouteReachability = SCNetworkReachabilityCreateWithAddress(NULL, (struct sockaddr *)&address); + SCNetworkReachabilityFlags flags; + + BOOL didRetrieveFlags = SCNetworkReachabilityGetFlags(defaultRouteReachability, &flags); + CFRelease(defaultRouteReachability); + + if (!didRetrieveFlags) + { + printf("Error. Could not recover network reachability flags\n"); + return NO; + } + + BOOL isReachable = flags & kSCNetworkFlagsReachable; + return isReachable ? YES : NO;; +} + + +// ---------------------------------------------------------------------------------------------------------- ++ (BOOL) connectedToNetwork +// ---------------------------------------------------------------------------------------------------------- +{ + // Create zero addy + struct sockaddr_in zeroAddress; + bzero(&zeroAddress, sizeof(zeroAddress)); + zeroAddress.sin_len = sizeof(zeroAddress); + zeroAddress.sin_family = AF_INET; + + // Recover reachability flags + SCNetworkReachabilityRef defaultRouteReachability = SCNetworkReachabilityCreateWithAddress(NULL, (struct sockaddr *)&zeroAddress); + CFRelease(defaultRouteReachability); + + SCNetworkReachabilityFlags flags; + + BOOL didRetrieveFlags = SCNetworkReachabilityGetFlags(defaultRouteReachability, &flags); + if (!didRetrieveFlags) + { + printf("Error. Could not recover network reachability flags\n"); + return 0; + } + + BOOL isReachable = flags & kSCNetworkFlagsReachable; + BOOL needsConnection = flags & kSCNetworkFlagsConnectionRequired; + // BOOL isEDGE = flags & kSCNetworkReachabilityFlagsIsWWAN; + return (isReachable && !needsConnection) ? YES : NO; +} + + +@end diff --git a/engine/platform/ios/SDL_uikit_main.c b/engine/platform/ios/SDL_uikit_main.c new file mode 100644 index 00000000..6ce34929 --- /dev/null +++ b/engine/platform/ios/SDL_uikit_main.c @@ -0,0 +1,23 @@ +/* + SDL_uikit_main.c, placed in the public domain by Sam Lantinga 3/18/2019 +*/ + +/* Include the SDL main definition header */ +#include "SDL_main.h" + +#if defined(__IPHONEOS__) || defined(__TVOS__) + +#ifndef SDL_MAIN_HANDLED +#ifdef main +#undef main +#endif + +int main(int argc, char *argv[]) +{ + return SDL_UIKitRunApp(argc, argv, SDL_main); +} +#endif /* !SDL_MAIN_HANDLED */ + +#endif /* __IPHONEOS__ || __TVOS__ */ + +/* vi: set ts=4 sw=4 expandtab: */ diff --git a/engine/platform/ios/bundle/Info.plist b/engine/platform/ios/bundle/Info.plist new file mode 100644 index 00000000..b8e45288 --- /dev/null +++ b/engine/platform/ios/bundle/Info.plist @@ -0,0 +1,78 @@ + + + + + BuildMachineOSBuild + 25A362 + CFBundleDevelopmentRegion + en + CFBundleExecutable + xash + CFBundleIdentifier + su.xash.engine + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + xash3d-fwgs + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSupportedPlatforms + + iPhoneOS + + CFBundleVersion + 1 + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + 23A339 + DTPlatformName + iphoneos + DTPlatformVersion + 26.0 + DTSDKBuild + 23A339 + DTSDKName + iphoneos26.0 + DTXcode + 2601 + DTXcodeBuild + 17A400 + LSRequiresIPhoneOS + + LSSupportsGameMode + + MinimumOSVersion + 12.0 + NSMicrophoneUsageDescription + Microphone permissions are required for voice chat + UIApplicationSupportsIndirectInputEvents + + UIDeviceFamily + + 1 + 2 + + UIFileSharingEnabled + + UILaunchStoryboardName + Launch + UIRequiredDeviceCapabilities + + arm64 + + UIRequiresFullScreen + + UIStatusBarStyle + UIStatusBarStyleLightContent + UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportsDocumentBrowser + + + diff --git a/engine/platform/ios/bundle/ftp_commands.plist b/engine/platform/ios/bundle/ftp_commands.plist new file mode 100644 index 00000000..a951c6be --- /dev/null +++ b/engine/platform/ios/bundle/ftp_commands.plist @@ -0,0 +1,64 @@ + + + + + ack + doAck: + cdup + doCdUp: + cwd + doCwd: + dele + doDele: + eprt + doEprt: + epsv + doEpsv: + feat + doFeat: + list + doList: + lprt + doLprt: + mkd + doMkdir: + mlst + doMlst: + nlst + doList: + noop + doNoop: + opts + doOpts: + pass + doPass: + pasv + doPasv: + port + doPort: + pwd + doPwd: + quit + doQuit: + retr + doRetr: + rmd + doDele: + rnfr + doRnfr: + rnto + doRnto: + size + doSize: + stat + doStat: + stor + doStor: + syst + doSyst: + type + doType: + user + doUser: + + diff --git a/engine/platform/ios/launchdialog.m b/engine/platform/ios/launchdialog.m new file mode 100644 index 00000000..f666228a --- /dev/null +++ b/engine/platform/ios/launchdialog.m @@ -0,0 +1,334 @@ +/* + launchdialog.m - iOS lauch dialog + Copyright (C) 2016 mittorn + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + */ + +#include "SDL_syswm.h" +#import +#import +#import +#import "FtpServer.h" +#include +#include "dlfcn.h" + +#ifndef XASH_GAMEDIR +#define XASH_GAMEDIR "valve" // !!! Replace with your default (base) game directory !!! +#endif +#define XASHLIB "@rpath/libxash.dylib" + + +@interface XashPromptAlertViewDelegate : NSObject + +@property (nonatomic, assign) int *button; + +@end + +@implementation XashPromptAlertViewDelegate + +- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{ + *_button = buttonIndex; +} +@end + + + +int szArgc; +char **szArgv; +char *g_szLibrarySuffix; +float g_iOSVer; +bool isdark; + +const char *IOS_GetDocsDir(void) +{ + if( g_iOSVer >= 8.0 ) + { + static const char *dir = NULL; + + if( dir ) + return dir; + + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString *documentsDirctory = [paths objectAtIndex:0]; + [[NSFileManager defaultManager] createDirectoryAtPath:documentsDirctory withIntermediateDirectories:YES attributes:nil error:nil]; + + dir = [documentsDirctory fileSystemRepresentation]; + NSLog(@"IOS_GetDocsDir: %s", dir); + + return dir; + } + else + { + static char dir[1024]; + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString *basePath = paths.firstObject; + [[NSFileManager defaultManager] createDirectoryAtPath:basePath withIntermediateDirectories:YES attributes:nil error:nil]; + strcpy(dir,[basePath UTF8String]); + mkdir(dir,777); + + NSLog(@"IOS_GetDocsDir: %s", dir); + + return dir; + } +} + +UIBackgroundTaskIdentifier task; +FtpServer *server = NULL; + +void IOS_StartBackgroundTask(void) +{ + if( !server ) return; + + if( task != UIBackgroundTaskInvalid ) + return; + + UIApplication* app = [UIApplication sharedApplication]; + + task = [app beginBackgroundTaskWithExpirationHandler:^{ + [app endBackgroundTask:task]; + task = UIBackgroundTaskInvalid; + }]; + + // Start the long-running task and return immediately. + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + UIBackgroundTaskIdentifier local = task; + + // do not keep "zombie" tasks + while( 1 ) + sleep(1); + + [app endBackgroundTask:local]; + task = UIBackgroundTaskInvalid; + }); +} + +#define SETTINGS_MAGIC 111 + +typedef struct settings_s +{ + unsigned char magic; + char args[1024]; + unsigned int port; + char suffix[32]; + unsigned int ftpserver; +} settings_t; +@interface ButtonHandler :NSObject +@property (nonatomic, assign) int *button1; +@end +@implementation ButtonHandler + + +-(void) buttonClicked:(UIButton*)sender +{ + *_button1 = 0; +} +@end +void IOS_PrepareView(void) +{ + ButtonHandler *handler = [[ButtonHandler alloc] init]; + UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + UIViewController *controller = [[UIViewController alloc] init]; + [[controller view] setBackgroundColor:[UIColor grayColor]]; + [window setRootViewController:controller]; + [window makeKeyAndVisible]; + if([[controller traitCollection] userInterfaceStyle] == UIUserInterfaceStyleDark && g_iOSVer >= 13.0) isdark = true; else isdark = false; +#if 0 + UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(10, 10, 100, 20)]; + int button1 = -1; + handler.button1 = &button1; + + [button addTarget:handler action:@selector(buttonClicked:) forControlEvents:UIControlEventValueChanged]; + @autoreleasepool { + while( button1 == -1 ) { + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; + } + } + [[controller view] addSubview:button]; +#endif +} + +void IOS_LaunchDialog( void ) +{ + NSLog(@"System Version is %@",[[UIDevice currentDevice] systemVersion]); + NSString *ver = [[UIDevice currentDevice] systemVersion]; + g_iOSVer = [ver floatValue]; + + //request microphone permissions otherwise we will crash when joining an online server + [[AVAudioSession sharedInstance] requestRecordPermission:^(BOOL granted){}]; + + IOS_PrepareView(); + int button = -1, bExit, bStart; + UIAlertView * alert = [[UIAlertView alloc] init]; + bExit = [alert addButtonWithTitle:@"Exit"]; + bStart = [alert addButtonWithTitle:@"Start"]; + XashPromptAlertViewDelegate *delegate = [[XashPromptAlertViewDelegate alloc] init]; + delegate.button = &button; + + alert.delegate = delegate; + + const char *docsDir = IOS_GetDocsDir(); + + //set working directory to documents so logs can be generated there + NSString *workingDir = [NSString stringWithUTF8String:IOS_GetDocsDir()]; + [[NSFileManager defaultManager] changeCurrentDirectoryPath:workingDir]; + + FILE *settingsfile; + char settingspath[256]; + snprintf(settingspath, sizeof(settingspath), "%s/settings.bin", docsDir ); + settingspath[255] = 0; + settings_t settings; + + [alert setTransform:CGAffineTransformMakeTranslation(0,109)]; + + UIScrollView *scroll = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 300, 200)]; + + UISwitch *ftpswitch = [[UISwitch alloc] initWithFrame:CGRectMake(210,60,80,30)]; + + UILabel *argstitle = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 300, 30)]; + [argstitle setText:@"Command-line arguments:"]; + + UITextField *args = [[UITextField alloc] initWithFrame:CGRectMake(0, 30, 300, 30)]; + [args setBackgroundColor:[[UIColor alloc] initWithRed:1 green:1 blue:1 alpha:1]]; + if(isdark) [args setBackgroundColor:[[UIColor alloc] initWithRed:0 green:0 blue:0 alpha:1]]; + + + UILabel *ftptitle = [[UILabel alloc] initWithFrame:CGRectMake(0, 60, 200, 30)]; + [ftptitle setText:@"FTP Server"]; + + + UITextField *port = [[UITextField alloc] initWithFrame:CGRectMake(110, 60, 100, 30)]; + + UITextField *suffix = [[UITextField alloc] initWithFrame:CGRectMake(140, 90, 160, 30 )]; + [suffix setBackgroundColor:[[UIColor alloc] initWithRed:1 green:1 blue:1 alpha:1]]; + if(isdark) [suffix setBackgroundColor:[[UIColor alloc] initWithRed:0 green:0 blue:0 alpha:1]]; + + UILabel *suffixtitle = [[UILabel alloc] initWithFrame:CGRectMake(0, 90, 140, 30)]; + [suffixtitle setText:@"Library suffix"]; + + [scroll addSubview:argstitle]; + [scroll addSubview:args]; + [scroll addSubview:suffix]; + [scroll addSubview:ftpswitch]; + [scroll addSubview:ftptitle]; + [scroll addSubview:port]; + [scroll addSubview:suffixtitle]; + + settingsfile = fopen( settingspath, "rb" ); + if( settingsfile && ( fread(&settings, sizeof( settings ), 1, settingsfile ) == 1 ) && ( settings.magic == SETTINGS_MAGIC ) ) + { + settings.args[1023] = 0; + settings.suffix[31] = 0; + [args setText:@(settings.args)]; + [port setText:[NSString stringWithFormat:@"%u", settings.port]]; + [suffix setText:@(settings.suffix)]; + ftpswitch.on = settings.ftpserver; + } + else + { + [args setText:@"-dev 2 -log"]; + [port setText:@"21135"]; + } + + scroll.contentSize=CGSizeMake(250, 200); + [alert setValue:scroll forKey:@"accessoryView"]; + + [alert show]; + + @autoreleasepool { + while( button == -1 ) { + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; + } + } + + if( (settingsfile = fopen( settingspath, "wb" )) ) + { + settings.ftpserver = ftpswitch.on; + strlcpy(settings.args, [args.text UTF8String], 1024); + strlcpy(settings.suffix, [suffix.text UTF8String], 32 ); + settings.port = [port.text intValue]; + settings.magic = 111; + + fwrite(&settings, sizeof(settings), 1, settingsfile); + fclose(settingsfile); + } + if( button == bExit ) + { + printf("Exit selected\n"); + exit(0); + } + + if( ftpswitch.on ) + { + + button = -1; + + [[[UIAlertView alloc] initWithTitle:@"Xash3D" message:[NSString stringWithFormat:@"Started FTP server on port %@", port.text] delegate:delegate cancelButtonTitle:@"Ok" otherButtonTitles:nil] show]; + + server = [[ FtpServer alloc ] initWithPort:[port.text integerValue] withDir:@(docsDir) notifyObject:nil ]; + + IOS_StartBackgroundTask(); + + @autoreleasepool { + while( button == -1 ) { + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; + IOS_StartBackgroundTask(); // keep running + } + } + } + + NSArray *argv = [ args.text componentsSeparatedByString:@" " ]; + + int count = [argv count]; + szArgv = calloc( count + 2, sizeof( char* ) ); + int i; + for( i = 0; i #include #include "common.h" #include "library.h" #include "filesystem.h" #include "server.h" -#include "platform/apple/ios_lib.h" +#include "platform/ios/lib_ios.h" +#include static void *IOS_LoadLibraryInternal( const char *dllname ) { @@ -52,20 +53,19 @@ static void *IOS_LoadLibraryInternal( const char *dllname ) } return pHandle; } -extern char *g_szLibrarySuffix; -static void *IOS_LoadLibrary( const char *dllname ) +char *g_szLibrarySuffix; +void *IOS_LoadLibrary( const char *dllname ) { string name; char *postfix = g_szLibrarySuffix; char *pHandle; - - if( !postfix ) postfix = GI->gamefolder; - - Q_snprintf( name, MAX_STRING, "%s_%s", dllname, postfix ); + + if( !postfix ) postfix = ""; + + Q_snprintf( name, MAX_STRING, "%s%s", dllname, postfix ); pHandle = IOS_LoadLibraryInternal( name ); if( pHandle ) return pHandle; return IOS_LoadLibraryInternal( dllname ); } -#endif // TARGET_OS_IPHONE diff --git a/engine/platform/apple/lib_ios.h b/engine/platform/ios/lib_ios.h similarity index 100% rename from engine/platform/apple/lib_ios.h rename to engine/platform/ios/lib_ios.h diff --git a/engine/platform/ios/list.h b/engine/platform/ios/list.h new file mode 100644 index 00000000..c4c7ac3e --- /dev/null +++ b/engine/platform/ios/list.h @@ -0,0 +1,29 @@ +/* + iosFtpServer + Copyright (C) 2008 Richard Dearlove ( monsta ) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#import + +#pragma mark LS replacement +NSString* createList(NSString* directoryPath); + +#pragma mark Supporting Functions +int filesinDirectory(NSString* filePath ); +NSMutableString* int2BinString(int x); +NSMutableString *byte2String(int x ); +NSMutableString *bin2perms(NSString *binaryValue); diff --git a/engine/platform/ios/list.m b/engine/platform/ios/list.m new file mode 100644 index 00000000..875819b6 --- /dev/null +++ b/engine/platform/ios/list.m @@ -0,0 +1,202 @@ +/* + iosFtpServer + Copyright (C) 2008 Richard Dearlove ( monsta ) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +#import "list.h" + +// ---------------------------------------------------------------------------------------------------------- +NSString* createList(NSString* directoryPath) +// ---------------------------------------------------------------------------------------------------------- +{ + NSFileManager *fileManager = [ NSFileManager defaultManager ]; + NSDictionary *fileAttributes; + NSError *error; + + NSString* fileType; + NSNumber* filePermissions; + long fileSubdirCount; + NSString* fileOwner; + NSString* fileGroup; + NSNumber* fileSize; + NSDate* fileModified; + NSString* fileDateFormatted; + NSDateFormatter* dateFormatter = [[[ NSDateFormatter alloc ] init ] autorelease ]; + + BOOL fileIsDirectory; + + + NSMutableString* returnString= [ NSMutableString new ]; + NSString* formattedString; + + NSString* binaryString; + + [returnString appendString:@"\r\n"]; + + NSDirectoryEnumerator *dirEnum = [fileManager enumeratorAtPath:directoryPath]; + NSString *filePath; + + NSString* firstChar; + NSString* fullFilePath; + + [dateFormatter setDateFormat:@"MMM dd HH:mm"]; + NSLocale *englishLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en"]; + [dateFormatter setLocale:englishLocale]; + [englishLocale release]; + + + NSLog(@"Get LS for %@", directoryPath ); + int numberOfFiles = 0; + while (filePath = [dirEnum nextObject]) { + + firstChar = [ filePath substringToIndex:1 ]; + + [dirEnum skipDescendents ]; // don't go down that recursive road + + if ( ![ firstChar isEqualToString:@"."] ) // dont show hidden files + { + fullFilePath = [directoryPath stringByAppendingPathComponent:filePath]; + + fileAttributes = [ fileManager attributesOfItemAtPath:fullFilePath error:&error ]; + + fileType = [ fileAttributes valueForKey:NSFileType ]; + + filePermissions = [ fileAttributes valueForKey:NSFilePosixPermissions ]; + fileSubdirCount = filesinDirectory(fullFilePath); + fileOwner = [ fileAttributes valueForKey:NSFileOwnerAccountName ]; + fileGroup = [ fileAttributes valueForKey:NSFileGroupOwnerAccountName ]; + fileSize = [ fileAttributes valueForKey:NSFileSize ]; + fileModified = [ fileAttributes valueForKey:NSFileModificationDate ]; + fileDateFormatted = [ dateFormatter stringFromDate:fileModified ]; + + fileIsDirectory = (fileType == NSFileTypeDirectory ); + + + fileSubdirCount = fileSubdirCount <1 ? 1 : fileSubdirCount; + + binaryString = int2BinString([filePermissions unsignedLongValue]) ; + binaryString = [ binaryString substringFromIndex:7 ];// snip off the front + formattedString = [ NSString stringWithFormat:@"%@%@ %5i %12@ %12@ %10qu %@ %@", fileIsDirectory ? @"d" : @"-" ,bin2perms(binaryString),fileSubdirCount, fileOwner, fileGroup, [fileSize unsignedLongLongValue], fileDateFormatted , filePath ]; + + [ returnString appendString:formattedString ]; + [ returnString appendString:@"\n" ]; + numberOfFiles++; + } + } + [returnString insertString: [NSString stringWithFormat:@"total %d", numberOfFiles] atIndex:0]; + // NSLog(returnString ); + return [returnString autorelease]; // FIXME - release count + +} +// ---------------------------------------------------------------------------------------------------------- +int filesinDirectory(NSString* filePath ) +// ---------------------------------------------------------------------------------------------------------- +{ + int no_files =0; + NSFileManager *fileManager = [ NSFileManager defaultManager ]; + NSDirectoryEnumerator *dirEnum = [fileManager enumeratorAtPath:filePath]; + + while (filePath = [dirEnum nextObject]) { + [dirEnum skipDescendents ]; // don't want children + no_files++; + } + + return no_files; +} +// ---------------------------------------------------------------------------------------------------------- +NSMutableString* int2BinString(int x) +// ---------------------------------------------------------------------------------------------------------- +{ + NSMutableString *returnString = [[ NSMutableString alloc ] init]; + int hi, lo; + hi=(x>>8) & 0xff; + lo=x&0xff; + + [ returnString appendString:byte2String(hi) ]; + [ returnString appendString:byte2String(lo) ]; + return [ returnString autorelease ]; +} + + + +// ---------------------------------------------------------------------------------------------------------- +NSMutableString *byte2String(int x ) +// ---------------------------------------------------------------------------------------------------------- +{ + NSMutableString *returnString = [[ NSMutableString alloc ]init]; + + int n; + + for(n=0; n<8; n++) + { + if((x & 0x80) !=0) + { + + [ returnString appendString:@"1"]; + + + } + else + { + [ returnString appendString:@"0"]; + } + x = x<<1; + } + + return [returnString autorelease]; +} + +// ---------------------------------------------------------------------------------------------------------- +NSMutableString *bin2perms(NSString *binaryValue) +// ---------------------------------------------------------------------------------------------------------- +{ + NSMutableString *returnString = [[ NSMutableString alloc ] init]; + NSRange subStringRange; + subStringRange.length = 1; + NSString *replaceWithChar; + + for (int n=0; n < [binaryValue length]; n++) + { + subStringRange.location = n; + // take the char + // if pos = 0, 3,6 + if ( n == 0 || n == 3 || n ==6) + { + replaceWithChar = @"r"; + } + if ( n == 1 || n == 4 || n ==7) + { + replaceWithChar = @"w"; + } + if ( n == 2 || n == 5 || n ==7) + { + replaceWithChar = @"x"; + } + + if ( [[binaryValue substringWithRange:subStringRange ] isEqualToString:@"1" ] ) + { + [ returnString appendString:replaceWithChar ]; + } + else + { + [ returnString appendString:@"-" ]; + } + + } + + return [ returnString autorelease ]; +} + diff --git a/engine/platform/platform.h b/engine/platform/platform.h index 772ed47f..fce82b98 100644 --- a/engine/platform/platform.h +++ b/engine/platform/platform.h @@ -40,6 +40,7 @@ qboolean Platform_DebuggerPresent( void ); // legacy iOS port functions #if TARGET_OS_IOS +int IOS_GetArgs( char ***argv ); const char *IOS_GetDocsDir( void ); void IOS_LaunchDialog( void ); #endif // TARGET_OS_IOS diff --git a/engine/platform/posix/lib_posix.c b/engine/platform/posix/lib_posix.c index 2bc4dcc3..babb38dd 100644 --- a/engine/platform/posix/lib_posix.c +++ b/engine/platform/posix/lib_posix.c @@ -36,7 +36,7 @@ GNU General Public License for more details. #include "filesystem.h" #include "server.h" #include "platform/android/lib_android.h" -#include "platform/apple/lib_ios.h" +#include "platform/ios/lib_ios.h" #ifdef XASH_NO_LIBDL void *dlsym( void *handle, const char *symbol ) diff --git a/engine/platform/sdl2/vid_sdl2.c b/engine/platform/sdl2/vid_sdl2.c index 99c8cb41..0d34221f 100644 --- a/engine/platform/sdl2/vid_sdl2.c +++ b/engine/platform/sdl2/vid_sdl2.c @@ -496,10 +496,9 @@ static qboolean VID_SetScreenResolution( int width, int height, window_mode_t wi { SDL_DisplayMode got; Uint32 wndFlags = 0; - + if( vid_highdpi.value ) SetBits( wndFlags, SDL_WINDOW_ALLOW_HIGHDPI ); - SDL_SetWindowBordered( host.hWnd, SDL_FALSE ); if( window_mode == WINDOW_MODE_BORDERLESS ) diff --git a/engine/wscript b/engine/wscript index a8bcacf5..372f071c 100644 --- a/engine/wscript +++ b/engine/wscript @@ -2,7 +2,7 @@ # encoding: utf-8 # mittorn, 2018 -from waflib import Logs +from waflib import Logs, TaskGen from waflib.extras import pthread import os from copy import copy @@ -42,6 +42,12 @@ int main(int argc, char **argv) } ''' +frameworks = ['Foundation', 'UIKit', 'QuartzCore', 'GameController', 'SystemConfiguration', 'CFNetwork', 'AVFoundation', 'CoreGraphics'] + +@TaskGen.extension('.m') +def m_hook(self, node): + return self.create_compiled_task('c', node) + def options(opt): grp = opt.add_option_group('Engine options') @@ -141,6 +147,10 @@ def configure(conf): if not conf.env.DEST_OS in ['win32', 'android']: conf.check_pthreads(mode='c') + + if conf.env.IOS: + for i in frameworks : + conf.check(features='c cprogram', framework=i, uselib_store=i, msg='Checking for %s framework' % i) conf.check_cc(fragment=EXECINFO_TEST, msg='Checking for glibc backtrace()', mandatory=False, define_name='HAVE_EXECINFO') @@ -205,7 +215,11 @@ def build(bld): # include common POSIX conformant code if bld.env.DEST_OS not in ['win32', 'dos']: source += bld.path.ant_glob('platform/posix/*.c') - + if bld.env.IOS: + source += bld.path.ant_glob('platform/ios/*.c') + source += bld.path.ant_glob('platform/ios/*.m') + includes += bld.path.ant_glob('platform/ios') + # include sources for optional features if bld.get_define('XASH_CUSTOM_SWAP'): source += ['platform/misc/kmalloc.c', 'platform/misc/sbrk.c'] @@ -284,6 +298,10 @@ def build(bld): f = bld.shlib install_path = bld.env.LIBDIR + if bld.env.IOS: + libs += frameworks + defines += ['XASH_SDLMAIN=1'] + if bld.env.DEST_OS in ['nswitch', 'psvita', 'emscripten']: install_path = None diff --git a/scripts/ios/buildhlsdk.sh b/scripts/ios/buildhlsdk.sh new file mode 100755 index 00000000..594a2483 --- /dev/null +++ b/scripts/ios/buildhlsdk.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +#cd into script directory +SCRIPTDIR=${0%/*} +cd $SCRIPTDIR + +MODPATH=mod-build/$1 +if [ -z $1 ]; then + MODPATH=mod-build/hlsdk + git clone --recursive https://github.com/FWGS/hlsdk-portable -b mobile_hacks $MODPATH +else + git clone --recursive https://github.com/FWGS/hlsdk-portable -b $1 $MODPATH +fi + +mkdir ../../build/ios +IOSDIR=$(realpath ../../build/ios) +cd $MODPATH + +#compiling hlsdk for ios release is broken, so just build for debug +cmake -DCMAKE_SYSTEM_NAME=iOS -DGAMEDIR=$IOSDIR -DCMAKE_BUILD_TYPE=Debug -B build -S . +cmake --build build --target install + +cd ../../ +if [ -d mod-build ]; then + rm -rf mod-build/ +fi +exit 0 \ No newline at end of file diff --git a/scripts/ios/createipa.sh b/scripts/ios/createipa.sh new file mode 100755 index 00000000..2f464b82 --- /dev/null +++ b/scripts/ios/createipa.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +#cd into script directory +cd "${0%/*}" +BUILDDIR=$(realpath ../../build) +echo $BUILDDIR +cd "../../engine/platform/ios/bundle" + +if [ -d $BUILDDIR ]; then + mkdir -p "$BUILDDIR/ios/xash3d.app" + + cp -r "$BUILDDIR/ios/dlls" "$BUILDDIR/ios/xash3d.app" + cp -r "$BUILDDIR/ios/cl_dlls" "$BUILDDIR/ios/xash3d.app" + cp *.plist "$BUILDDIR/ios/xash3d.app" + if [ ! -d $BUILDDIR/SDL2.framework ]; then + echo "Couldn't find SDL2.framework, place it in the build directory" + exit 1 + fi + cp -r $BUILDDIR/SDL2.framework "$BUILDDIR/ios/xash3d.app" + + cd ../../../../ + + ./waf install --destdir="$BUILDDIR/ios/xash3d.app" + + cd $BUILDDIR + + rm -r "$BUILDDIR/ios/Payload/" + mkdir "$BUILDDIR/ios/Payload" + + cp -r "$BUILDDIR/ios/xash3d.app" ios/Payload/ + rm -r "$BUILDDIR/ios/xash3d.app" + cd ios + zip -r ../xash3d.ipa Payload + else + echo "Couldn't find the build directory, compile the engine before running this script!" + exit 1 +fi +exit 0 diff --git a/scripts/waifulib/xcompile.py b/scripts/waifulib/xcompile.py index e473761e..0ce19582 100644 --- a/scripts/waifulib/xcompile.py +++ b/scripts/waifulib/xcompile.py @@ -45,6 +45,34 @@ PSVITA_ENVVARS = ['VITASDK'] PSP_ENVVARS = ['PSPDEV', 'PSPSDK', 'PSPTOOLCHAIN'] +class iOS: + ctx = None + sdkpath = None + target = None + + def __init__(self, ctx, issim): + self.ctx = ctx + sdk = 'iphonesimulator' if issim else 'iphoneos' + self.sdkpath = ctx.cmd_and_log('xcrun --show-sdk-path --sdk %s' % sdk).strip() + self.target = '--target=aarch64-apple-ios' + if issim: self.target += '-simulator' + + def cflags(self, cxx = False): + + cflags = [ '-isysroot' + self.sdkpath, self.target, '-mios-version-min=12.0' ] + return cflags + + def linkflags(self): + + linkflags = [ '-isysroot' + self.sdkpath, self.target, '-mios-version-min=12.0' ] + return linkflags + + def cc(self): + return 'clang' + + def cxx(self): + return 'clang++' + # This class does support ONLY r10e and r19c/r20 NDK class Android: ctx = None # waf context @@ -610,6 +638,10 @@ def options(opt): help='enable building for Emscripten') xc.add_option('--psp', action='store', dest='PSP_OPTS', default=None, help='enable building for PlayStation Portable, format: --psp=,,, example: --psp=prx,660,HW') + xc.add_option('--ios', action='store_true', dest='IOS', default=False, + help='enable building for iOS [default: %(default)s]') + xc.add_option('--ios-simulator', action='store_true', dest='IOSSIM', default=False, + help='enable building for iOS Simulator [default: %(default)s]') def configure(conf): if 'CROSS_COMPILE' in conf.environ: @@ -754,6 +786,18 @@ def configure(conf): conf.env.PSP_RENDER_TYPE = psp.render_type conf.env.PSP_BUILD_PRX = psp.build_prx + elif conf.options.IOS or conf.options.IOSSIM: + issim = True if conf.options.IOSSIM else False + + conf.ios = ios = iOS(conf, issim) + + conf.environ['CC'] = ios.cc() + conf.environ['CXX'] = ios.cxx() + + conf.env.CFLAGS += ios.cflags() + conf.env.CXXFLAGS += ios.cflags() + conf.env.LINKFLAGS += ios.linkflags() + conf.env.IOS = 1 conf.env.MAGX = conf.options.MAGX conf.env.MSVC_WINE = conf.options.MSVC_WINE diff --git a/wscript b/wscript index 6ad4b4fd..77de89b2 100644 --- a/wscript +++ b/wscript @@ -247,6 +247,12 @@ def configure(conf): conf.options.GL4ES = True conf.options.GLES3COMPAT = True conf.options.GL = False + elif conf.env.IOS: + conf.options.NANOGL = True + conf.options.GLWES = False # deprecated + conf.options.GL4ES = False # doesn't compile on ios yet + conf.options.GLES3COMPAT = True + conf.options.GL = False elif conf.env.MAGX: conf.options.SDL12 = True conf.options.GL = False @@ -399,7 +405,7 @@ def configure(conf): if not conf.options.DEDICATED: conf.env.SERVER = conf.options.ENABLE_DEDICATED conf.env.CLIENT = True - conf.env.LAUNCHER = conf.env.DEST_OS not in ['android', 'nswitch', 'psvita', 'dos', 'emscripten'] and not conf.env.MAGX and not conf.env.STATIC_LINKING + conf.env.LAUNCHER = conf.env.DEST_OS not in ['android', 'nswitch', 'psvita', 'dos', 'emscripten'] and not conf.env.IOS and not conf.env.MAGX and not conf.env.STATIC_LINKING else: conf.env.SERVER = True conf.env.CLIENT = False