Permalink
Browse files

Initial commit

  • Loading branch information...
1 parent 26855dc commit 700621b87878eec666bd10c6e54849019eb3d39a @mxcl committed Oct 18, 2011
Showing with 5,306 additions and 0 deletions.
  1. +659 −0 AsyncSocket.h
  2. +4,350 −0 AsyncSocket.m
  3. +33 −0 MBWebSocketServer.h
  4. +227 −0 MBWebSocketServer.m
  5. +37 −0 README.md
View
659 AsyncSocket.h
@@ -0,0 +1,659 @@
+//
+// 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 <Foundation/Foundation.h>
+
+@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.
+ AsyncSocketConnectTimeoutError,
+ AsyncSocketReadMaxedOutError, // Reached set maxLength without completing
+ AsyncSocketReadTimeoutError,
+ AsyncSocketWriteTimeoutError
+};
+typedef enum AsyncSocketError AsyncSocketError;
+
+@protocol AsyncSocketDelegate
+@optional
+
+/**
+ * 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:".
+ *
+ * If you call the disconnect method, and the socket wasn't already disconnected,
+ * this delegate method will be called before the disconnect method returns.
+**/
+- (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 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:(NSUInteger)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;
+
+/**
+ * Called when a socket has written some data, but has not yet completed the entire write.
+ * It may be used to for things such as updating progress bars.
+**/
+- (void)onSocket:(AsyncSocket *)sock didWritePartialDataOfLength:(NSUInteger)partialLength tag:(long)tag;
+
+/**
+ * Called if a read operation has reached its timeout without completing.
+ * This method allows you to optionally extend the timeout.
+ * If you return a positive time interval (> 0) the read's timeout will be extended by the given amount.
+ * If you don't implement this method, or return a non-positive time interval (<= 0) the read will timeout as usual.
+ *
+ * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method.
+ * The length parameter is the number of bytes that have been read so far for the read operation.
+ *
+ * Note that this method may be called multiple times for a single read if you return positive numbers.
+**/
+- (NSTimeInterval)onSocket:(AsyncSocket *)sock
+ shouldTimeoutReadWithTag:(long)tag
+ elapsed:(NSTimeInterval)elapsed
+ bytesDone:(NSUInteger)length;
+
+/**
+ * Called if a write operation has reached its timeout without completing.
+ * This method allows you to optionally extend the timeout.
+ * If you return a positive time interval (> 0) the write's timeout will be extended by the given amount.
+ * If you don't implement this method, or return a non-positive time interval (<= 0) the write will timeout as usual.
+ *
+ * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method.
+ * The length parameter is the number of bytes that have been written so far for the write operation.
+ *
+ * Note that this method may be called multiple times for a single write if you return positive numbers.
+**/
+- (NSTimeInterval)onSocket:(AsyncSocket *)sock
+ shouldTimeoutWriteWithTag:(long)tag
+ elapsed:(NSTimeInterval)elapsed
+ bytesDone:(NSUInteger)length;
+
+/**
+ * Called after the socket has successfully completed SSL/TLS negotiation.
+ * This method is not called unless you use the provided startTLS method.
+ *
+ * If a SSL/TLS negotiation fails (invalid certificate, etc) then the socket will immediately close,
+ * and the onSocket:willDisconnectWithError: delegate method will be called with the specific SSL error code.
+**/
+- (void)onSocketDidSecure:(AsyncSocket *)sock;
+
+@end
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+#pragma mark -
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+@interface AsyncSocket : NSObject
+{
+ CFSocketNativeHandle theNativeSocket4;
+ CFSocketNativeHandle theNativeSocket6;
+
+ CFSocketRef theSocket4; // IPv4 accept or connect socket
+ CFSocketRef theSocket6; // IPv6 accept or connect socket
+
+ CFReadStreamRef theReadStream;
+ CFWriteStreamRef theWriteStream;
+
+ CFRunLoopSourceRef theSource4; // For theSocket4
+ CFRunLoopSourceRef theSource6; // For theSocket6
+ CFRunLoopRef theRunLoop;
+ CFSocketContext theContext;
+ NSArray *theRunLoopModes;
+
+ NSTimer *theConnectTimer;
+
+ NSMutableArray *theReadQueue;
+ AsyncReadPacket *theCurrentRead;
+ NSTimer *theReadTimer;
+ NSMutableData *partialReadBuffer;
+
+ NSMutableArray *theWriteQueue;
+ AsyncWritePacket *theCurrentWrite;
+ NSTimer *theWriteTimer;
+
+ id theDelegate;
+ UInt16 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 the accept or connect methods are called, the AsyncSocket instance is locked in
+// and the other accept/connect methods can't be called without disconnecting the socket first.
+// If the attempt fails or times out, these methods either return NO or
+// call "onSocket:willDisconnectWithError:" and "onSockedDidDisconnect:".
+
+// When an incoming connection is accepted, AsyncSocket invokes several delegate methods.
+// These methods are (in chronological order):
+// 1. onSocket:didAcceptNewSocket:
+// 2. onSocket:wantsRunLoopForNewSocket:
+// 3. onSocketWillConnect:
+//
+// Your server code will need to retain the accepted socket (if you want to accept it).
+// The best place to do this is probably in the onSocket:didAcceptNewSocket: method.
+//
+// After the read and write streams have been setup for the newly accepted socket,
+// the onSocket:didConnectToHost:port: method will be called on the proper run loop.
+//
+// Multithreading Note: If you're going to be moving the newly accepted socket to another run
+// loop by implementing onSocket:wantsRunLoopForNewSocket:, then you should wait until the
+// onSocket:didConnectToHost:port: method before calling read, write, or startTLS methods.
+// Otherwise read/write events are scheduled on the incorrect runloop, and chaos may ensue.
+
+/**
+ * Tells the socket to begin listening and accepting connections on the given port.
+ * When a connection comes in, the AsyncSocket instance will call the various delegate methods (see above).
+ * The socket will listen on all available interfaces (e.g. wifi, ethernet, etc)
+**/
+- (BOOL)acceptOnPort:(UInt16)port error:(NSError **)errPtr;
+
+/**
+ * This method is the same as acceptOnPort:error: with the additional option
+ * of specifying which interface to listen on. So, for example, if you were writing code for a server that
+ * has multiple IP addresses, you could specify which address you wanted to listen on. Or you could use it
+ * to specify that the socket should only accept connections over ethernet, and not other interfaces such as wifi.
+ * You may also use the special strings "localhost" or "loopback" to specify that
+ * the socket only accept connections from the local machine.
+ *
+ * To accept connections on any interface pass nil, or simply use the acceptOnPort:error: method.
+**/
+- (BOOL)acceptOnInterface:(NSString *)interface port:(UInt16)port error:(NSError **)errPtr;
+
+/**
+ * Connects to the given host and port.
+ * The host may be a domain name (e.g. "deusty.com") or an IP address string (e.g. "192.168.0.2")
+**/
+- (BOOL)connectToHost:(NSString *)hostname onPort:(UInt16)port error:(NSError **)errPtr;
+
+/**
+ * This method is the same as connectToHost:onPort:error: with an additional timeout option.
+ * To not time out use a negative time interval, or simply use the connectToHost:onPort:error: method.
+**/
+- (BOOL)connectToHost:(NSString *)hostname
+ onPort:(UInt16)port
+ withTimeout:(NSTimeInterval)timeout
+ error:(NSError **)errPtr;
+
+/**
+ * Connects to the given address, specified as a sockaddr structure wrapped in a NSData object.
+ * For example, a NSData object returned from NSNetservice's addresses method.
+ *
+ * If you have an existing struct sockaddr you can convert it to a 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;
+
+/**
+ * This method is the same as connectToAddress:error: with an additional timeout option.
+ * To not time out use a negative time interval, or simply use the connectToAddress:error: method.
+**/
+- (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr;
+
+- (BOOL)connectToAddress:(NSData *)remoteAddr
+ viaInterfaceAddress:(NSData *)interfaceAddr
+ withTimeout:(NSTimeInterval)timeout
+ error:(NSError **)errPtr;
+
+/**
+ * Disconnects immediately. Any pending reads or writes are dropped.
+ * If the socket is not already disconnected, the onSocketDidDisconnect delegate method
+ * will be called immediately, before this method returns.
+ *
+ * Please note the recommended way of releasing an AsyncSocket instance (e.g. in a dealloc method)
+ * [asyncSocket setDelegate:nil];
+ * [asyncSocket disconnect];
+ * [asyncSocket release];
+**/
+- (void)disconnect;
+
+/**
+ * Disconnects after all pending reads have completed.
+ * After calling this, the read and write methods will do nothing.
+ * The socket will disconnect even if there are still pending writes.
+**/
+- (void)disconnectAfterReading;
+
+/**
+ * Disconnects after all pending writes have completed.
+ * After calling this, the read and write methods will do nothing.
+ * The socket will disconnect even if there are still pending reads.
+**/
+- (void)disconnectAfterWriting;
+
+/**
+ * Disconnects after all pending reads and writes have completed.
+ * After calling this, the read and write methods will do nothing.
+**/
+- (void)disconnectAfterReadingAndWriting;
+
+/* 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;
+
+/**
+ * Returns the local or remote address to which this socket is connected,
+ * specified as a sockaddr structure wrapped in a NSData object.
+ *
+ * See also the connectedHost, connectedPort, localHost and localPort methods.
+**/
+- (NSData *)connectedAddress;
+- (NSData *)localAddress;
+
+/**
+ * Returns whether the socket is IPv4 or IPv6.
+ * An accepting socket may be both.
+**/
+- (BOOL)isIPv4;
+- (BOOL)isIPv6;
+
+// The readData and writeData methods won't block (they are asynchronous).
+//
+// When a read is complete the onSocket:didReadData:withTag: delegate method is called.
+// When a write is complete the onSocket:didWriteDataWithTag: delegate method is called.
+//
+// You may optionally set a timeout for any read/write operation. (To not timeout, use a negative time interval.)
+// If a read/write opertion times out, the corresponding "onSocket:shouldTimeout..." delegate method
+// is called to optionally allow you to extend the timeout.
+// Upon a timeout, the "onSocket:willDisconnectWithError:" method is called, followed by "onSocketDidDisconnect".
+//
+// The tag is for your convenience.
+// You can use it as an array index, step number, state id, pointer, etc.
+
+/**
+ * Reads the first available bytes that become available on the socket.
+ *
+ * If the timeout value is negative, the read operation will not use a timeout.
+**/
+- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag;
+
+/**
+ * Reads the first available bytes that become available on the socket.
+ * The bytes will be appended to the given byte buffer starting at the given offset.
+ * The given buffer will automatically be increased in size if needed.
+ *
+ * If the timeout value is negative, the read operation will not use a timeout.
+ * If the buffer if nil, the socket will create a buffer for you.
+ *
+ * If the bufferOffset is greater than the length of the given buffer,
+ * the method will do nothing, and the delegate will not be called.
+ *
+ * If you pass a buffer, you must not alter it in any way while AsyncSocket is using it.
+ * After completion, the data returned in onSocket:didReadData:withTag: will be a subset of the given buffer.
+ * That is, it will reference the bytes that were appended to the given buffer.
+**/
+- (void)readDataWithTimeout:(NSTimeInterval)timeout
+ buffer:(NSMutableData *)buffer
+ bufferOffset:(NSUInteger)offset
+ tag:(long)tag;
+
+/**
+ * Reads the first available bytes that become available on the socket.
+ * The bytes will be appended to the given byte buffer starting at the given offset.
+ * The given buffer will automatically be increased in size if needed.
+ * A maximum of length bytes will be read.
+ *
+ * If the timeout value is negative, the read operation will not use a timeout.
+ * If the buffer if nil, a buffer will automatically be created for you.
+ * If maxLength is zero, no length restriction is enforced.
+ *
+ * If the bufferOffset is greater than the length of the given buffer,
+ * the method will do nothing, and the delegate will not be called.
+ *
+ * If you pass a buffer, you must not alter it in any way while AsyncSocket is using it.
+ * After completion, the data returned in onSocket:didReadData:withTag: will be a subset of the given buffer.
+ * That is, it will reference the bytes that were appended to the given buffer.
+**/
+- (void)readDataWithTimeout:(NSTimeInterval)timeout
+ buffer:(NSMutableData *)buffer
+ bufferOffset:(NSUInteger)offset
+ maxLength:(NSUInteger)length
+ tag:(long)tag;
+
+/**
+ * Reads the given number of bytes.
+ *
+ * If the timeout value is negative, the read operation will not use a timeout.
+ *
+ * If the length is 0, this method does nothing and the delegate is not called.
+**/
+- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag;
+
+/**
+ * Reads the given number of bytes.
+ * The bytes will be appended to the given byte buffer starting at the given offset.
+ * The given buffer will automatically be increased in size if needed.
+ *
+ * If the timeout value is negative, the read operation will not use a timeout.
+ * If the buffer if nil, a buffer will automatically be created for you.
+ *
+ * If the length is 0, this method does nothing and the delegate is not called.
+ * If the bufferOffset is greater than the length of the given buffer,
+ * the method will do nothing, and the delegate will not be called.
+ *
+ * If you pass a buffer, you must not alter it in any way while AsyncSocket is using it.
+ * After completion, the data returned in onSocket:didReadData:withTag: will be a subset of the given buffer.
+ * That is, it will reference the bytes that were appended to the given buffer.
+**/
+- (void)readDataToLength:(NSUInteger)length
+ withTimeout:(NSTimeInterval)timeout
+ buffer:(NSMutableData *)buffer
+ bufferOffset:(NSUInteger)offset
+ tag:(long)tag;
+
+/**
+ * Reads bytes until (and including) the passed "data" parameter, which acts as a separator.
+ *
+ * If the timeout value is negative, the read operation will not use a timeout.
+ *
+ * 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;
+
+/**
+ * Reads bytes until (and including) the passed "data" parameter, which acts as a separator.
+ * The bytes will be appended to the given byte buffer starting at the given offset.
+ * The given buffer will automatically be increased in size if needed.
+ *
+ * If the timeout value is negative, the read operation will not use a timeout.
+ * If the buffer if nil, a buffer will automatically be created for you.
+ *
+ * If the bufferOffset is greater than the length of the given buffer,
+ * the method will do nothing, and the delegate will not be called.
+ *
+ * If you pass a buffer, you must not alter it in any way while AsyncSocket is using it.
+ * After completion, the data returned in onSocket:didReadData:withTag: will be a subset of the given buffer.
+ * That is, it will reference the bytes that were appended to the given buffer.
+ *
+ * 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
+ buffer:(NSMutableData *)buffer
+ bufferOffset:(NSUInteger)offset
+ tag:(long)tag;
+
+/**
+ * Reads bytes until (and including) the passed "data" parameter, which acts as a separator.
+ *
+ * If the timeout value is negative, the read operation will not use a timeout.
+ *
+ * If maxLength is zero, no length restriction is enforced.
+ * Otherwise if maxLength bytes are read without completing the read,
+ * it is treated similarly to a timeout - the socket is closed with a AsyncSocketReadMaxedOutError.
+ * The read will complete successfully if exactly maxLength bytes are read and the given data is found at the end.
+ *
+ * If you pass nil or zero-length data as the "data" parameter,
+ * the method will do nothing, and the delegate will not be called.
+ * 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.
+ *
+ * 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 maxLength:(NSUInteger)length tag:(long)tag;
+
+/**
+ * Reads bytes until (and including) the passed "data" parameter, which acts as a separator.
+ * The bytes will be appended to the given byte buffer starting at the given offset.
+ * The given buffer will automatically be increased in size if needed.
+ * A maximum of length bytes will be read.
+ *
+ * If the timeout value is negative, the read operation will not use a timeout.
+ * If the buffer if nil, a buffer will automatically be created for you.
+ *
+ * If maxLength is zero, no length restriction is enforced.
+ * Otherwise if maxLength bytes are read without completing the read,
+ * it is treated similarly to a timeout - the socket is closed with a AsyncSocketReadMaxedOutError.
+ * The read will complete successfully if exactly maxLength bytes are read and the given data is found at the end.
+ *
+ * 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 bufferOffset is greater than the length of the given buffer,
+ * the method will do nothing, and the delegate will not be called.
+ *
+ * If you pass a buffer, you must not alter it in any way while AsyncSocket is using it.
+ * After completion, the data returned in onSocket:didReadData:withTag: will be a subset of the given buffer.
+ * That is, it will reference the bytes that were appended to the given buffer.
+ *
+ * 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
+ buffer:(NSMutableData *)buffer
+ bufferOffset:(NSUInteger)offset
+ maxLength:(NSUInteger)length
+ 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.
+ * If the timeout value is negative, the write operation will not use a timeout.
+**/
+- (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:(NSUInteger *)done total:(NSUInteger *)total;
+- (float)progressOfWriteReturningTag:(long *)tag bytesDone:(NSUInteger *)done total:(NSUInteger *)total;
+
+/**
+ * Secures the connection using SSL/TLS.
+ *
+ * This method may be called at any time, and the TLS handshake will occur after all pending reads and writes
+ * are finished. This allows one the option of sending a protocol dependent StartTLS message, and queuing
+ * the upgrade to TLS at the same time, without having to wait for the write to finish.
+ * Any reads or writes scheduled after this method is called will occur over the secured connection.
+ *
+ * The possible keys and values for the TLS settings are well documented.
+ * Some possible keys are:
+ * - kCFStreamSSLLevel
+ * - kCFStreamSSLAllowsExpiredCertificates
+ * - kCFStreamSSLAllowsExpiredRoots
+ * - kCFStreamSSLAllowsAnyRoot
+ * - kCFStreamSSLValidatesCertificateChain
+ * - kCFStreamSSLPeerName
+ * - kCFStreamSSLCertificates
+ * - kCFStreamSSLIsServer
+ *
+ * Please refer to Apple's documentation for associated values, as well as other possible keys.
+ *
+ * If you pass in nil or an empty dictionary, the default settings will be used.
+ *
+ * The default settings will check to make sure the remote party's certificate is signed by a
+ * trusted 3rd party certificate agency (e.g. verisign) and that the certificate is not expired.
+ * However it will not verify the name on the certificate unless you
+ * give it a name to verify against via the kCFStreamSSLPeerName key.
+ * The security implications of this are important to understand.
+ * Imagine you are attempting to create a secure connection to MySecureServer.com,
+ * but your socket gets directed to MaliciousServer.com because of a hacked DNS server.
+ * If you simply use the default settings, and MaliciousServer.com has a valid certificate,
+ * the default settings will not detect any problems since the certificate is valid.
+ * To properly secure your connection in this particular scenario you
+ * should set the kCFStreamSSLPeerName property to "MySecureServer.com".
+ * If you do not know the peer name of the remote host in advance (for example, you're not sure
+ * if it will be "domain.com" or "www.domain.com"), then you can use the default settings to validate the
+ * certificate, and then use the X509Certificate class to verify the issuer after the socket has been secured.
+ * The X509Certificate class is part of the CocoaAsyncSocket open source project.
+**/
+- (void)startTLS:(NSDictionary *)tlsSettings;
+
+/**
+ * For handling readDataToData requests, data is necessarily read from the socket in small increments.
+ * The performance can be much 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 will result in better performance, especially on the iPhone.
+ *
+ * The default pre-buffering state is controlled by the DEFAULT_PREBUFFERING definition.
+ * It is highly recommended one leave this set to YES.
+ *
+ * This method exists in case pre-buffering needs to be disabled by default for some unforeseen reason.
+ * In that case, this method exists to allow one to easily enable pre-buffering when ready.
+**/
+- (void)enablePreBuffering;
+
+/**
+ * When you create an AsyncSocket, it is added to the runloop of the current thread.
+ * So for manually created sockets, it is easiest to simply create the socket on the thread you intend to use it.
+ *
+ * If a new socket is accepted, the delegate method onSocket:wantsRunLoopForNewSocket: is called to
+ * allow you to place the socket on a separate thread. This works best in conjunction with a thread pool design.
+ *
+ * If, however, you need to move the socket to a separate thread at a later time, this
+ * method may be used to accomplish the task.
+ *
+ * This method must be called from the thread/runloop the socket is currently running on.
+ *
+ * Note: After calling this method, all further method calls to this object should be done from the given runloop.
+ * Also, all delegate calls will be sent on the given runloop.
+**/
+- (BOOL)moveToRunLoop:(NSRunLoop *)runLoop;
+
+/**
+ * Allows you to configure which run loop modes the socket uses.
+ * The default set of run loop modes is NSDefaultRunLoopMode.
+ *
+ * If you'd like your socket to continue operation during other modes, you may want to add modes such as
+ * NSModalPanelRunLoopMode or NSEventTrackingRunLoopMode. Or you may simply want to use NSRunLoopCommonModes.
+ *
+ * Accepted sockets will automatically inherit the same run loop modes as the listening socket.
+ *
+ * Note: NSRunLoopCommonModes is defined in 10.5. For previous versions one can use kCFRunLoopCommonModes.
+**/
+- (BOOL)setRunLoopModes:(NSArray *)runLoopModes;
+- (BOOL)addRunLoopMode:(NSString *)runLoopMode;
+- (BOOL)removeRunLoopMode:(NSString *)runLoopMode;
+
+/**
+ * Returns the current run loop modes the AsyncSocket instance is operating in.
+ * The default set of run loop modes is NSDefaultRunLoopMode.
+**/
+- (NSArray *)runLoopModes;
+
+/**
+ * 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 the readDataToData:... methods. */
++ (NSData *)CRLFData; // 0x0D0A
++ (NSData *)CRData; // 0x0D
++ (NSData *)LFData; // 0x0A
++ (NSData *)ZeroData; // 0x00
+
+@end
View
4,350 AsyncSocket.m
4,350 additions, 0 deletions not shown because the diff is too large. Please use a local Git client to view these changes.
View
33 MBWebSocketServer.h
@@ -0,0 +1,33 @@
+// Originally created by Max Howell in October 2011.
+// This class is in the public domain.
+//
+// MBWebSocketServer accepts client connections as soon as it is created.
+// MBWebSocketServer only accepts a single client connection at any time.
+// Implementated against: http://tools.ietf.org/id/draft-ietf-hybi-thewebsocketprotocol-10
+
+#import <CoreFoundation/CoreFoundation.h>
+@protocol MBWebSocketServerDelegate;
+@class AsyncSocket;
+
+
+@interface MBWebSocketServer : NSObject {
+ NSUInteger port;
+ id<MBWebSocketServerDelegate> delegate;
+ AsyncSocket *socket;
+ AsyncSocket *client;
+}
+
+- (id)initWithPort:(NSUInteger)port delegate:(id<MBWebSocketServerDelegate>)delegate;
+
+- (void)send:(id)utf8StringOrData;
+
+@property (nonatomic, readonly) NSUInteger port;
+@property (nonatomic, assign) id<MBWebSocketServerDelegate> delegate;
+@end
+
+
+@protocol MBWebSocketServerDelegate
+- (void)webSocketServerDidConnect:(MBWebSocketServer *)webSocket;
+- (void)webSocketServerDidDisconnect:(MBWebSocketServer *)webSocket;
+- (void)webSocketServer:(MBWebSocketServer *)webSocket didReceiveData:(NSData *)data;
+@end
View
227 MBWebSocketServer.m
@@ -0,0 +1,227 @@
+// Originally created by Max Howell in October 2011.
+// This class is in the public domain.
+
+#import "AsyncSocket.h"
+#import <CommonCrypto/CommonDigest.h>
+#import "MBWebSocketServer.h"
+
+
+@interface MBWebSocketServer () <AsyncSocketDelegate>
+@end
+
+@interface NSArray (MBWebSocketServer)
+- (id)secWebSocketKey;
+- (id)secWebSocketAccept;
+@end
+
+@interface NSString (MBWebSocketServer)
+- (id)sha1base64;
+@end
+
+
+
+@implementation MBWebSocketServer
+
+- (id)initWithPort:(NSUInteger)aport delegate:(id<MBWebSocketServerDelegate>)adelegate {
+ port = aport;
+ delegate = adelegate;
+ socket = [[AsyncSocket alloc] initWithDelegate:self];
+
+ NSError *error = nil; //TODO
+ [socket acceptOnPort:port error:&error];
+
+ return self;
+}
+
+- (void)close {
+ if (client) {
+ [client release];
+ client = nil;
+ [delegate webSocketServerDidDisconnect:self];
+ }
+}
+
+- (void)respondToHandshake:(NSData *)data {
+ NSString *string = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease];
+
+ NSArray *strings = [string componentsSeparatedByString:@"\r\n"];
+
+ if (strings.count == 0 || ![[strings objectAtIndex:0] isEqualToString:@"GET / HTTP/1.1"]) {
+ NSLog(@"MBWebSocketServer invalid handshake from client");
+ return [self close];
+ }
+
+ NSString *response = [NSString stringWithFormat:
+ @"HTTP/1.1 101 Switching Protocols\r\n"
+ "Upgrade: websocket\r\n"
+ "Connection: Upgrade\r\n"
+ "Sec-WebSocket-Accept: %@\r\n\r\n",
+ [strings secWebSocketAccept]];
+
+ [client writeData:[response dataUsingEncoding:NSUTF8StringEncoding] withTimeout:-1 tag:2];
+}
+
+- (unsigned int)readFrame:(const char*)bytes {
+ if (!bytes[0] & 0x81)
+ @throw @"Cannot handle this websocket frame format!";
+
+ if (!bytes[1] & 0x80)
+ @throw @"Can only handle websocket frames with masks!";
+
+ unsigned n = 2;
+ int16_t N = bytes[1] & 0x7f;
+ switch (N) {
+ case 126: {
+ N = ntohs((short) bytes[3] << 8 | bytes[2]);
+ n += 2;
+ break;
+ }
+ case 127:
+ @throw @"8 byte lengths unsupported currently!";
+ default:
+ break;
+ }
+
+ const char *mask = bytes + n;
+ char unmaskedData[N];
+ for (int x = 0; x < N; ++x)
+ unmaskedData[x] = bytes[x+n+4] ^ mask[x%4];
+
+ [delegate webSocketServer:self didReceiveData:[NSData dataWithBytes:unmaskedData length:N]];
+
+ return n + 4 + N;
+}
+
+- (void)readFrames:(NSData *)data {
+ @try {
+ uint x = 0;
+ while (x < data.length)
+ x += [self readFrame:data.bytes + x];
+ }
+ @catch (id e) {
+ NSLog(@"%@", e);
+ }
+}
+
+- (void)send:(NSData *)payload {
+ if ([payload isKindOfClass:[NSString class]])
+ payload = [[(NSString *)payload dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
+
+ NSMutableData *data = [NSMutableData dataWithLength:4];
+ char *header = data.mutableBytes;
+ header[0] = 0x81;
+
+ if (payload.length > 125) {
+ header[1] = 126;
+ header[2] = payload.length >> 8;
+ header[3] = payload.length & 0xff;
+ } else {
+ header[1] = payload.length;
+ data.length = 2;
+ }
+
+ [data appendData:payload];
+ [client writeData:data withTimeout:-1 tag:3];
+}
+
+
+#pragma mark - ASyncSocketDelegate
+
+- (void)onSocket:(AsyncSocket *)sock didAcceptNewSocket:(AsyncSocket *)aclient {
+ client = [aclient retain];
+ [client readDataWithTimeout:-1 tag:1];
+}
+
+- (void)onSocket:(AsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
+{
+ if (tag == 1) { // waiting for handshake
+ [self respondToHandshake:data];
+ } else {
+ [self readFrames:data];
+ [client readDataWithTimeout:-1 tag:3];
+ }
+}
+
+- (void)onSocket:(AsyncSocket *)sock didWriteDataWithTag:(long)tag {
+ if (sock == client) {
+ switch (tag) {
+ case 2:
+ [delegate webSocketServerDidConnect:self];
+ case 3:
+ [client readDataWithTimeout:-1 tag:3];
+ }
+ }
+}
+
+- (void)onSocketDidDisconnect:(AsyncSocket *)sock {
+ if (sock == client)
+ [self close];
+}
+
+
+#pragma mark - Boilerplate
+
+- (void)dealloc {
+ [socket release];
+ [client release];
+ [super dealloc];
+}
+
+@synthesize port;
+@synthesize delegate;
+@end
+
+
+
+@implementation NSArray (MBWebSocketServer)
+
+- (id)secWebSocketKey {
+ for (NSString *line in self) {
+ //TODO better efficiency!
+ NSArray *parts = [line componentsSeparatedByString:@":"];
+ if (parts.count == 2) {
+ if ([[parts objectAtIndex:0] isEqualToString:@"Sec-WebSocket-Key"])
+ return [[parts objectAtIndex:1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];;
+ }
+ }
+ return nil;
+}
+
+- (id)secWebSocketAccept {
+ return [[NSString stringWithFormat:@"%@%@", [self secWebSocketKey], @"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"] sha1base64];
+}
+
+@end
+
+
+
+@implementation NSString (MBWebSocketServer)
+
+- (id)sha1base64 {
+ NSMutableData* data = (id) [self dataUsingEncoding:NSUTF8StringEncoding];
+ unsigned char input[CC_SHA1_DIGEST_LENGTH];
+ CC_SHA1(data.bytes, (unsigned)data.length, input);
+
+//////
+ static const char map[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+ data = [NSMutableData dataWithLength:28];
+ uint8_t* out = (uint8_t*) data.mutableBytes;
+
+ for (int i = 0; i < 20;) {
+ int v = 0;
+ for (const int N = i + 3; i < N; i++) {
+ v <<= 8;
+ v |= 0xFF & input[i];
+ }
+ *out++ = map[v >> 18 & 0x3F];
+ *out++ = map[v >> 12 & 0x3F];
+ *out++ = map[v >> 6 & 0x3F];
+ *out++ = map[v >> 0 & 0x3F];
+ }
+ out[-2] = map[(input[19] & 0x0F) << 2];
+ out[-1] = '=';
+ return [[[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding] autorelease];
+}
+
+@end
View
37 README.md
@@ -0,0 +1,37 @@
+MBWebSocket
+===========
+So far just a *server implementation*, (you cannot instantiate an instance that does not bind to a port). Also, the server can only manage a single connection at a time.
+
+So, seriously, you will probably have to do some work to this class to make it
+work how you need it too. But it’s nice and simple so just get straight in.
+
+Tested against Chrome 10/2011. Probably will only work against that.
+
+Example Usage
+-------------
+```objc
+- (void)applicationDidFinishLaunching:(NSNotification *)note {
+ self.ws = [[MBWebSocketServer alloc] initWithPort:13581 delegate:self];
+}
+
+- (void)webSocketServerDidConnect:(MBWebSocketServer *)webSocket {
+ NSLog(@"Connected to a client (and we only work with one for now!)");
+}
+
+- (void)webSocketServer:(MBWebSocketServer *)webSocket didReceiveData:(NSData *)data
+{
+ NSLog(@"%@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
+ [webSocket send:@"Thanks!"];
+}
+
+- (void)webSocketServerDidDisconnect:(MBWebSocketServer *)webSocket {
+ NSLog(@"Disconnected from client");
+}
+
+```
+
+Author
+------
+I’m [Max Howell][mxcl] and I'm a splendid chap
+
+[mxcl]:http://twitter.com/mxcl

0 comments on commit 700621b

Please sign in to comment.