├── Docs ├── logo.png ├── BLIP Protocol v1.md └── BLIP Protocol.md ├── BLIP.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── BLIP-iOS-Carthage.xcscheme ├── .gitmodules ├── BLIP ├── BLIP.h ├── BLIPProperties.h ├── BLIPPocketSocket_Internal.h ├── BLIPPocketSocketConnection.h ├── BLIPResponse.h ├── BLIPPool.h ├── BLIPConnection+Transport.h ├── BLIPPocketSocketListener.h ├── BLIPHTTPLogic.h ├── BLIPRequest.h ├── BLIP_Internal.h ├── BLIPRequest.m ├── BLIPConnection.h ├── BLIPProperties.m ├── BLIPPool.m ├── BLIPMessage.h ├── BLIPResponse.m ├── BLIPPocketSocketConnection.m ├── BLIPPocketSocketListener.m ├── BLIPHTTPLogic.m ├── BLIPMessage.m └── BLIPConnection.m ├── .gitignore ├── BLIPTests ├── Info.plist └── Properties_Test.m ├── README.md └── LICENSE /Docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/BLIP-Cocoa/HEAD/Docs/logo.png -------------------------------------------------------------------------------- /BLIP.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/MYUtilities"] 2 | path = vendor/MYUtilities 3 | url = git://github.com/snej/MYUtilities.git 4 | [submodule "PocketSocket"] 5 | path = vendor/PocketSocket 6 | url = git://github.com/couchbasedeps/PocketSocket.git 7 | -------------------------------------------------------------------------------- /BLIP/BLIP.h: -------------------------------------------------------------------------------- 1 | // 2 | // BLIP.h 3 | // BLIP 4 | // 5 | // Created by Jens Alfke on 9/12/13. 6 | // Copyright (c) 2013-2015 Couchbase, Inc. All rights reserved. 7 | 8 | #import "BLIPPocketSocketConnection.h" 9 | #import "BLIPProperties.h" 10 | #import "BLIPRequest.h" 11 | #import "BLIPResponse.h" 12 | -------------------------------------------------------------------------------- /BLIP/BLIPProperties.h: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPProperties.h 3 | // BLIP 4 | // 5 | // Created by Jens Alfke on 5/13/08. 6 | // Copyright 2008-2013 Jens Alfke. 7 | // Copyright (c) 2013-2015 Couchbase, Inc. All rights reserved. 8 | 9 | #import 10 | #import "MYData.h" 11 | @class MYBuffer; 12 | 13 | 14 | NSDictionary* BLIPParseProperties(MYSlice *data, BOOL *complete); 15 | NSDictionary* BLIPReadPropertiesFromBuffer(MYBuffer*, BOOL *complete); 16 | NSData* BLIPEncodeProperties(NSDictionary* properties); 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | *.xccheckout 14 | *.moved-aside 15 | DerivedData 16 | *.hmap 17 | *.ipa 18 | *.xcuserstate 19 | 20 | # CocoaPods 21 | # 22 | # We recommend against adding the Pods directory to your .gitignore. However 23 | # you should judge for yourself, the pros and cons are mentioned at: 24 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 25 | # 26 | #Pods/ 27 | -------------------------------------------------------------------------------- /BLIP/BLIPPocketSocket_Internal.h: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPPocketSocket_Internal.h 3 | // BLIP 4 | // 5 | // Created by Jens Alfke on 4/11/15. 6 | // Copyright (c) 2015 Couchbase. All rights reserved. 7 | 8 | #import "BLIPPocketSocketConnection.h" 9 | #import "PSWebSocket.h" 10 | 11 | 12 | @interface BLIPPocketSocketConnection () 13 | 14 | // Designated initializer 15 | - (instancetype) initWithWebSocket: (PSWebSocket*)webSocket 16 | transportQueue: (dispatch_queue_t)transportQueue 17 | URL: (NSURL*)url 18 | incoming: (BOOL)incoming; 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /BLIP/BLIPPocketSocketConnection.h: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPPocketSocketConnection.h 3 | // BLIP 4 | // 5 | // Created by Jens Alfke on 4/10/15. 6 | // Copyright (c) 2015 Couchbase. All rights reserved. 7 | 8 | #import "BLIPConnection.h" 9 | @class PSWebSocket; 10 | 11 | 12 | @interface BLIPPocketSocketConnection : BLIPConnection 13 | 14 | - (instancetype) initWithURLRequest:(NSURLRequest *)request; 15 | - (instancetype) initWithURL:(NSURL *)url; 16 | 17 | - (void) setCredential: (NSURLCredential*)credential; 18 | 19 | - (BOOL) connect: (NSError**)outError; 20 | 21 | /** The underlying WebSocket. */ 22 | @property (readonly) PSWebSocket* webSocket; 23 | 24 | - (void) closeWithCode: (NSInteger)code reason:(NSString *)reason; 25 | 26 | @end 27 | -------------------------------------------------------------------------------- /BLIP/BLIPResponse.h: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPResponse.h 3 | // BLIP 4 | // 5 | // Created by Jens Alfke on 9/15/13. 6 | // Copyright (c) 2013-2015 Couchbase, Inc. All rights reserved. 7 | 8 | #import "BLIPMessage.h" 9 | 10 | 11 | /** A reply to a BLIPRequest, in the BLIP protocol. */ 12 | @interface BLIPResponse : BLIPMessage 13 | 14 | /** Sends this response. */ 15 | - (BOOL) send; 16 | 17 | /** The error returned by the peer, or nil if the response is successful. */ 18 | @property (strong) NSError* error; 19 | 20 | /** Sets a target/action to be called when an incoming response is complete. 21 | Use this on the response returned from -[BLIPRequest send], to be notified when the response is available. */ 22 | @property (strong) void (^onComplete)(BLIPResponse*); 23 | 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /BLIPTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /BLIPTests/Properties_Test.m: -------------------------------------------------------------------------------- 1 | // 2 | // Properties_Test.m 3 | // BLIP 4 | // 5 | // Created by Jens Alfke on 5/13/15. 6 | // Copyright (c) 2015 Couchbase, Inc. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "BLIPProperties.h" 11 | 12 | 13 | @interface Properties_Test : XCTestCase 14 | @end 15 | 16 | 17 | @implementation Properties_Test 18 | 19 | - (void)setUp { 20 | [super setUp]; 21 | // Put setup code here. This method is called before the invocation of each test method in the class. 22 | } 23 | 24 | - (void)tearDown { 25 | // Put teardown code here. This method is called after the invocation of each test method in the class. 26 | [super tearDown]; 27 | } 28 | 29 | - (void)testExample { 30 | // This is an example of a functional test case. 31 | XCTAssert(YES, @"Pass"); 32 | } 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /BLIP/BLIPPool.h: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPPool.h 3 | // BLIP 4 | // 5 | // Created by Jens Alfke on 9/16/13. 6 | // Copyright (c) 2013-2015 Couchbase, Inc. All rights reserved. 7 | 8 | #import "BLIPConnection.h" 9 | 10 | 11 | /** A pool of open BLIPWebSocketConnections to different URLs. 12 | By using this class you can conserve sockets by opening only a single socket to a WebSocket endpoint. */ 13 | @interface BLIPPool : NSObject 14 | 15 | - (instancetype) initWithDelegate: (id)delegate 16 | dispatchQueue: (dispatch_queue_t)queue; 17 | 18 | /** All the opened sockets will call this delegate. */ 19 | @property (weak) id delegate; 20 | 21 | /** Returns an open BLIPConnection to the given URL. If none is open yet, it will open one. */ 22 | - (BLIPConnection*) socketToURL: (NSURL*)url error: (NSError**)outError; 23 | 24 | /** Returns an already-open BLIPConnection to the given URL, or nil if there is none. */ 25 | - (BLIPConnection*) existingSocketToURL: (NSURL*)url error: (NSError**)outError; 26 | 27 | /** Closes all open sockets, with the default status code. */ 28 | - (void) close; 29 | 30 | /** Closes all open sockets. */ 31 | - (void) closeWithCode:(NSInteger)code reason:(NSString *)reason; 32 | 33 | @end 34 | -------------------------------------------------------------------------------- /BLIP/BLIPConnection+Transport.h: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPConnection+Transport.h 3 | // BLIP 4 | // 5 | // Created by Jens Alfke on 4/10/15. 6 | // Copyright (c) 2015 Couchbase. All rights reserved. 7 | 8 | #import "BLIPConnection.h" 9 | 10 | 11 | /** Protected API of BLIPConnection, for use by concrete subclasses. */ 12 | @interface BLIPConnection () 13 | 14 | /** Designated initializer. 15 | @param transportQueue The dispatch queue on which the transport should be called and on 16 | which it will call the BLIPConnection. 17 | @param isOpen YES if the transport is already open and ready to send/receive messages. */ 18 | - (instancetype) initWithTransportQueue: (dispatch_queue_t)transportQueue 19 | isOpen: (BOOL)isOpen; 20 | 21 | // This is settable. 22 | @property (readwrite) NSError* error; 23 | 24 | @property (readonly) dispatch_queue_t transportQueue; 25 | 26 | // Internal methods for subclasses to call: 27 | 28 | /** Call this when the transport opens and is ready to send/receive messages. */ 29 | - (void) transportDidOpen; 30 | 31 | /** Call this when the transport closes or fails to open. 32 | @param error The error, or nil if this is a normal closure. */ 33 | - (void) transportDidCloseWithError: (NSError*)error; 34 | 35 | /** Call this when the transport is done sending data and is ready to send more. 36 | If any messages are ready, it will call -sendFrame:. */ 37 | - (void) feedTransport; 38 | 39 | /** Call this when the transport receives a frame from the peer. */ 40 | - (void) didReceiveFrame: (NSData*)frame; 41 | 42 | // Abstract internal methods that subclasses must implement: 43 | 44 | /** Subclass must implement this to return YES if the transport is in a state where it can send 45 | messages, NO if not. Most importantly, it should return NO if it's in the process of closing.*/ 46 | - (BOOL) transportCanSend; 47 | 48 | /** Subclass must implement this to send the frame to the peer. */ 49 | - (void) sendFrame: (NSData*)frame; 50 | 51 | // Abstract public methods that that subclasses must implement: 52 | // - (BOOL) connect: (NSError**)outError; 53 | // - (void) close; 54 | 55 | @end 56 | -------------------------------------------------------------------------------- /BLIP/BLIPPocketSocketListener.h: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPPocketSocketListener.h 3 | // BLIP 4 | // 5 | // Created by Jens Alfke on 4/11/15. 6 | // Copyright (c) 2015 Couchbase. All rights reserved. 7 | 8 | #import "BLIPPocketSocketConnection.h" 9 | 10 | 11 | @interface BLIPPocketSocketListener : NSObject 12 | 13 | - (instancetype) initWithPaths: (NSArray*)paths 14 | delegate: (id)delegate 15 | queue: (dispatch_queue_t)queue NS_DESIGNATED_INITIALIZER; 16 | 17 | - (instancetype) init NS_UNAVAILABLE; 18 | 19 | /** Starts the listener. 20 | @param interface The name of the network interface, or nil to listen on all interfaces 21 | (See the GCDAsyncSocket documentation for more details.) 22 | @param port The TCP port to listen on. 23 | @param certs SSL identity and supporting certificates, or nil to not use SSL 24 | @param error On return, will be filled in with an error if the method returned NO. 25 | @return YES on success, NO on failure. */ 26 | - (BOOL) acceptOnInterface: (NSString*)interface 27 | port: (uint16_t)port 28 | SSLCertificates: (NSArray*)certs 29 | error: (NSError**)error; 30 | 31 | /** Stops the listener from accepting any more connections. */ 32 | - (void) disconnect; 33 | 34 | @property (readonly) uint16_t port; 35 | 36 | #pragma mark - AUTHENTICATION: 37 | 38 | /** Security realm string to return in authentication challenges. */ 39 | @property (copy) NSString* realm; 40 | 41 | /** Sets user names and passwords for authentication. 42 | @param passwords A dictionary mapping user names to passwords. */ 43 | - (void) setPasswords: (NSDictionary*)passwords; 44 | 45 | #pragma mark - PROTECTED: 46 | 47 | - (void) listenerDidStart; 48 | - (void) listenerDidStop; 49 | - (void) listenerDidFailWithError: (NSError*)error; 50 | 51 | - (void)blipConnectionDidOpen:(BLIPConnection*)b; 52 | 53 | + (BOOL) fromRequest: (NSURLRequest*)request 54 | getUsername: (NSString**)outUser 55 | password: (NSString**)outPassword; 56 | 57 | - (BOOL) checkClientCertificateAuthentication: (SecTrustRef)trust 58 | fromAddress: (NSData*)address; 59 | 60 | @end 61 | 62 | 63 | @interface BLIPPocketSocketConnection (Incoming) 64 | @property (readonly) NSURLCredential* credential; 65 | @end 66 | -------------------------------------------------------------------------------- /BLIP/BLIPHTTPLogic.h: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPHTTPLogic.h 3 | // BLIP 4 | // 5 | // Created by Jens Alfke on 11/13/13. 6 | // Copyright (c) 2013-2015 Couchbase, Inc. All rights reserved. 7 | 8 | #import 9 | 10 | 11 | /** Implements the core logic of HTTP request/response handling, especially processing 12 | redirects and authentication challenges, without actually doing any of the networking. 13 | It just tells you what HTTP request to send and how to interpret the response. */ 14 | @interface BLIPHTTPLogic : NSObject 15 | 16 | - (instancetype) initWithURLRequest:(NSURLRequest *)request; 17 | 18 | - (void) setValue: (NSString*)value forHTTPHeaderField:(NSString*)header; 19 | - (void) setObject: (NSString*)value forKeyedSubscript: (NSString*)key; 20 | 21 | /** Set this to YES to handle redirects. 22 | If enabled, redirects are handled by updating the URL and setting shouldRetry. */ 23 | @property BOOL handleRedirects; 24 | 25 | @property (readonly) NSURLRequest* URLRequest; 26 | 27 | /** Creates an HTTP request message to send. Caller is responsible for releasing it. */ 28 | - (CFHTTPMessageRef) newHTTPRequest; 29 | 30 | /** Call this when a response is received, then check shouldContinue and shouldRetry. */ 31 | - (void) receivedResponse: (CFHTTPMessageRef)response; 32 | 33 | /** After a response is received, this will be YES if the HTTP status indicates success. */ 34 | @property (readonly) BOOL shouldContinue; 35 | 36 | /** After a response is received, this will be YES if the client needs to retry with a new 37 | request. If so, it should call -createHTTPRequest again to get the new request, which will 38 | have either a different URL or new authentication headers. */ 39 | @property (readonly) BOOL shouldRetry; 40 | 41 | /** The URL. This will change after receiving a redirect response. */ 42 | @property (readonly) NSURL* URL; 43 | 44 | /** The TCP port number, based on the URL. */ 45 | @property (readonly) UInt16 port; 46 | 47 | /** Yes if TLS/SSL should be used (based on the URL). */ 48 | @property (readonly) BOOL useTLS; 49 | 50 | /** The auth credential being used. */ 51 | @property (readwrite) NSURLCredential* credential; 52 | 53 | /** A default User-Agent header string that will be used if the URLRequest doesn't contain one. 54 | You can use this as the basis for your own by appending to it. */ 55 | + (NSString*) userAgent; 56 | 57 | /** The HTTP status code of the response. */ 58 | @property (readonly) int httpStatus; 59 | 60 | /** The error from a failed redirect or authentication. This isn't set for regular non-success 61 | HTTP statuses like 404, only for failures to redirect or authenticate. */ 62 | @property (readonly) NSError* error; 63 | 64 | /** Parses the value of a "WWW-Authenticate" header into a dictionary. In the dictionary, the key 65 | "WWW-Authenticate" will contain the entire header, "Scheme" will contain the scheme (the first 66 | word), and the first parameter and value will appear as an extra key/value. (Only the first 67 | parameter is parsed; this could be improved.) */ 68 | + (NSDictionary*) parseAuthHeader: (NSString*)authHeader; 69 | 70 | @end 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ⚠️ This repo is not maintained, is outdated, and will no longer receive updates. 2 | 3 | # BLIP-Cocoa 4 | 5 | This is the latest Objective-C implementation of the BLIP network messaging protocol. [Version 2 of BLIP][BLIPDOCS] is layered on WebSockets instead of running directly over a TCP socket. The WebSocket implementation used here is [PocketSocket][POCKETSOCKET] 6 | 7 | ## "What's BLIP?" 8 | 9 | You can think of BLIP as an extension that adds a number of useful features that aren't supported by the [WebSocket][WEBSOCKET] protocol: 10 | 11 | * **Request/response:** Messages can have responses, and the responses don't have to be sent in the same order as the original messages. Responses are optional; a message can be sent in no-reply mode if it doesn't need one, otherwise a response (even an empty one) will always be sent after the message is handled. 12 | * **Metadata:** Messages are structured, with a set of key/value headers and a binary body, much like HTTP or MIME messages. Peers can use the metadata to route incoming messages to different handlers, effectively creating multiple independent channels on the same connection. 13 | * **Multiplexing:** Large messages are broken into fragments, and if multiple messages are ready to send their fragments will be interleaved on the connection, so they're sent in parallel. This prevents huge messages from blocking the connection. 14 | * **Priorities:** Messages can be marked Urgent, which gives them higher priority in the multiplexing (but without completely starving normal-priority messages.) This is very useful for streaming media. 15 | 16 | ## "Oh yeah, I know about BLIP" 17 | 18 | The first version of BLIP was released as part of my [MYNetwork][MYNETWORK] library. (It's still available because there are projects using it, but I haven't been actively developing it for a while.) This version of the protocol talked directly to a TCP socket and included its own framing layer. 19 | 20 | There was an intermediate version of BLIP in the [WebSockets-Cocoa][WEBSOCKETS_COCOA] library, an implementation of WebSockets I wrote for use in Couchbase Lite 1.0. Couchbase Lite didn't use that BLIP code; it was purely experimental. 21 | 22 | This new version has been extensively modified and improved: 23 | 24 | * The WebSocket implementation is now [PocketSocket][POCKETSOCKET], instead of my own code based on GCDAsyncSocket. PocketSocket is significantly smaller and its source code is more modern and easier to work with than GCDAsyncSocket's. 25 | * The underlying framing and network transport has been made pluggable. `BLIPConnection` is now an abstract class, with sequential frame delivery and receipt now implemented by subclasses. the `BLIPPocketSocketConnection` subclass uses PocketSocket; this is the class that clients will instantiate. Other implementations (not necessarily even using WebSockets) are possible. 26 | * Significant changes to the [BLIP protocol][BLIPDOCS]. Most importantly, it now supports flow control of frames within a single message, so that a fast sender won't end up flooding a slow receiver. 27 | * The API supports streaming message bodies, on either the sending or receiving end, so large messages can be sent without eating up a lot of memory. 28 | * Message compression/decompression is now incremental, which also reduces memory usage. 29 | * The old BLIPDispatcher class has been removed in favor of a simple way to register an action message to be sent to the connection's delegate when messages with a specific profile arrive. 30 | 31 | 32 | [WEBSOCKET]: http://www.websocket.org 33 | [POCKETSOCKET]: https://github.com/zwopple/PocketSocket 34 | [MYNETWORK]: https://github.com/snej/mynetwork 35 | [WEBSOCKETS_COCOA]: https://github.com/couchbaselabs/WebSockets-Cocoa 36 | [BLIPDOCS]: Docs/BLIP%20Protocol.md 37 | -------------------------------------------------------------------------------- /BLIP/BLIPRequest.h: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPRequest.h 3 | // BLIP 4 | // 5 | // Created by Jens Alfke on 5/22/08. 6 | // Copyright 2008-2013 Jens Alfke. 7 | // Copyright (c) 2013-2015 Couchbase, Inc. All rights reserved. 8 | 9 | #import "BLIPMessage.h" 10 | @class BLIPResponse, MYTarget; 11 | 12 | 13 | /** A Request, or initiating message, in the BLIP protocol. */ 14 | @interface BLIPRequest : BLIPMessage 15 | 16 | /** Creates an outgoing request. 17 | The body may be nil. 18 | The request is not associated with any BLIPConnection yet, so you must either set its 19 | connection property before calling -send, or pass the request as a parameter to 20 | -[BLIPConnection sendRequest:]. */ 21 | + (BLIPRequest*) requestWithBody: (NSData*)body; 22 | 23 | /** Creates an outgoing request. 24 | This is just like requestWithBody: except that you supply a string. */ 25 | + (BLIPRequest*) requestWithBodyString: (NSString*)bodyString; 26 | 27 | /** Creates an outgoing request. 28 | The body or properties may be nil. 29 | The request is not associated with any BLIPConnection yet, so you must either set its 30 | connection property before calling -send, or pass the request as a parameter to 31 | -[BLIPConnection sendRequest:]. */ 32 | + (BLIPRequest*) requestWithBody: (NSData*)body 33 | properties: (NSDictionary*)properties; 34 | 35 | /** BLIPRequest extends the -connection property to be settable. 36 | This allows a request to be created without a connection (i.e. before the connection is created). 37 | It can later be sent by setting the connection property and calling -send. */ 38 | @property (strong) BLIPConnection* connection; 39 | 40 | /** Does this request not need a response? 41 | This property can only be set before sending the request. */ 42 | @property BOOL noReply; 43 | 44 | /** Returns YES if you've replied to this request (by accessing its -response property.) */ 45 | @property (readonly) BOOL repliedTo; 46 | 47 | /** The request's response. This can be accessed at any time, even before sending the request, 48 | but the contents of the response won't be filled in until it arrives, of course. */ 49 | @property (readonly) BLIPResponse *response; 50 | 51 | /** Sends this request over its connection. 52 | (Actually, the connection queues it to be sent; this method always returns immediately.) 53 | Its matching response object will be returned, or nil if the request couldn't be sent. 54 | If this request has not been assigned to a connection, an exception will be raised. */ 55 | - (BLIPResponse*) send; 56 | 57 | /** Call this when a request arrives, to indicate that you want to respond to it later. 58 | It will prevent a default empty response from being sent upon return from the request handler. */ 59 | - (void) deferResponse; 60 | 61 | #pragma mark - RESPONSE SHORTCUTS: 62 | 63 | /** Shortcut to respond to this request with the given data. 64 | The contentType, if not nil, is stored in the "Content-Type" property. */ 65 | - (void) respondWithData: (NSData*)data contentType: (NSString*)contentType; 66 | 67 | /** Shortcut to respond to this request with the given string (which will be encoded in UTF-8). */ 68 | - (void) respondWithString: (NSString*)string; 69 | 70 | /** Shortcut to respond to this request with JSON. */ 71 | - (void) respondWithJSON: (id)jsonObject; 72 | 73 | /** Shortcut to respond to this request with an error. */ 74 | - (void) respondWithError: (NSError*)error; 75 | 76 | /** Shortcut to respond to this request with the given error code and message. 77 | The BLIPErrorDomain is assumed. */ 78 | - (void) respondWithErrorCode: (int)code message: (NSString*)message; //, ... __attribute__ ((format (__NSString__, 2,3)));; 79 | 80 | /** Shortcut to respond to this message with an error indicating that an exception occurred. */ 81 | - (void) respondWithException: (NSException*)exception; 82 | 83 | @end 84 | -------------------------------------------------------------------------------- /BLIP.xcodeproj/xcshareddata/xcschemes/BLIP-iOS-Carthage.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /BLIP/BLIP_Internal.h: -------------------------------------------------------------------------------- 1 | // 2 | // BLIP_Internal.h 3 | // BLIP 4 | // 5 | // Created by Jens Alfke on 5/10/08. 6 | // Copyright 2008-2013 Jens Alfke. 7 | // Copyright (c) 2013-2015 Couchbase, Inc. All rights reserved. 8 | 9 | #import "BLIPConnection.h" 10 | #import "BLIPRequest.h" 11 | #import "BLIPResponse.h" 12 | #import "BLIPProperties.h" 13 | 14 | #import "MYLogging.h" 15 | #import "Test.h" 16 | 17 | 18 | UsingLogDomain(BLIP); 19 | UsingLogDomain(BLIPLifecycle); 20 | 21 | 22 | @class MYBuffer; 23 | @protocol MYReader, MYWriter; 24 | 25 | 26 | /* Private declarations and APIs for BLIP implementation. Not for use by clients! */ 27 | 28 | 29 | /* Flag bits in a BLIP frame header */ 30 | typedef NS_OPTIONS(UInt8, BLIPMessageFlags) { 31 | kBLIP_MSG = 0x00, // initiating message 32 | kBLIP_RPY = 0x01, // response to a MSG 33 | kBLIP_ERR = 0x02, // error response to a MSG 34 | kBLIP_ACKMSG = 0x04, // acknowledging data received in a MSG 35 | kBLIP_ACKRPY = 0x05, // acknowledging data received in a RPY 36 | 37 | kBLIP_TypeMask = 0x07, // bits reserved for storing message type 38 | kBLIP_Compressed= 0x08, // data is gzipped 39 | kBLIP_Urgent = 0x10, // please send sooner/faster 40 | kBLIP_NoReply = 0x20, // no RPY needed 41 | kBLIP_MoreComing= 0x40, // More frames coming (Applies only to individual frame) 42 | kBLIP_Meta = 0x80, // Special message type, handled internally (hello, bye, ...) 43 | 44 | kBLIP_MaxFlag = 0xFF 45 | }; 46 | 47 | /* BLIP message types; encoded in each frame's header. */ 48 | typedef BLIPMessageFlags BLIPMessageType; 49 | 50 | 51 | @interface BLIPConnection () 52 | - (BOOL) _sendRequest: (BLIPRequest*)q response: (BLIPResponse*)response; 53 | - (BOOL) _sendResponse: (BLIPResponse*)response; 54 | - (void) _messageReceivedProperties: (BLIPMessage*)message; 55 | - (void) _sendAckWithNumber: (uint32_t)number 56 | isRequest: (BOOL)isRequest 57 | bytesReceived: (uint64_t)bytesReceived; 58 | @end 59 | 60 | 61 | @interface BLIPMessage () 62 | { 63 | @protected 64 | BLIPConnection* _connection; 65 | BLIPMessageFlags _flags; 66 | uint32_t _number; 67 | NSDictionary *_properties; 68 | NSData *_body; 69 | MYBuffer *_encodedBody; 70 | id _outgoing; 71 | id _incoming; 72 | NSMutableData *_mutableBody; 73 | NSMutableArray* _bodyStreams; 74 | BOOL _isMine, _isMutable, _sent, _propertiesAvailable, _complete; 75 | int64_t _bytesWritten, _bytesReceived; 76 | id _representedObject; 77 | } 78 | @property BOOL sent, propertiesAvailable, complete; 79 | - (BLIPMessageFlags) _flags; 80 | - (void) _setFlag: (BLIPMessageFlags)flag value: (BOOL)value; 81 | - (void) _encode; 82 | @end 83 | 84 | 85 | @interface BLIPMessage () 86 | - (instancetype) _initWithConnection: (BLIPConnection*)connection 87 | isMine: (BOOL)isMine 88 | flags: (BLIPMessageFlags)flags 89 | number: (uint32_t)msgNo 90 | body: (NSData*)body; 91 | - (NSData*) nextFrameWithMaxSize: (uint16_t)maxSize moreComing: (BOOL*)outMoreComing; 92 | @property (readonly) int64_t _bytesWritten; 93 | @property (readonly) BOOL _needsAckToContinue; 94 | - (void) _assignedNumber: (uint32_t)number; 95 | - (BOOL) _receivedFrameWithFlags: (BLIPMessageFlags)flags body: (NSData*)body; 96 | - (BOOL) _receivedAck: (uint64_t)bytesReceived; 97 | - (BOOL) _needsAckToContinue; 98 | - (void) _connectionClosed; 99 | @end 100 | 101 | 102 | @interface BLIPRequest () 103 | - (instancetype) _initWithConnection: (BLIPConnection*)connection 104 | body: (NSData*)body 105 | properties: (NSDictionary*)properties; 106 | @end 107 | 108 | 109 | @interface BLIPResponse () 110 | - (instancetype) _initWithRequest: (BLIPRequest*)request; 111 | #if DEBUG 112 | - (instancetype) _initIncomingWithProperties: (NSDictionary*)properties body: (NSData*)body; 113 | #endif 114 | @end 115 | -------------------------------------------------------------------------------- /BLIP/BLIPRequest.m: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPRequest.m 3 | // BLIP 4 | // 5 | // Created by Jens Alfke on 5/22/08. 6 | // Copyright 2008-2013 Jens Alfke. 7 | // Copyright (c) 2013-2015 Couchbase, Inc. All rights reserved. 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 10 | // except in compliance with the License. You may obtain a copy of the License at 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // Unless required by applicable law or agreed to in writing, software distributed under the 13 | // License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 14 | // either express or implied. See the License for the specific language governing permissions 15 | // and limitations under the License. 16 | 17 | #import "BLIPRequest.h" 18 | #import "BLIPConnection.h" 19 | #import "BLIP_Internal.h" 20 | 21 | #import "ExceptionUtils.h" 22 | 23 | 24 | @implementation BLIPRequest 25 | { 26 | BLIPResponse *_response; 27 | } 28 | 29 | 30 | - (instancetype) _initWithConnection: (BLIPConnection*)connection 31 | body: (NSData*)body 32 | properties: (NSDictionary*)properties 33 | { 34 | self = [self _initWithConnection: connection 35 | isMine: YES 36 | flags: kBLIP_MSG 37 | number: 0 38 | body: body]; 39 | if (self) { 40 | if (body) 41 | self.body = body; 42 | if (properties) 43 | _properties = [properties copy]; 44 | } 45 | return self; 46 | } 47 | 48 | + (BLIPRequest*) requestWithBody: (NSData*)body { 49 | return [[self alloc] _initWithConnection: nil body: body properties: nil]; 50 | } 51 | 52 | + (BLIPRequest*) requestWithBodyString: (NSString*)bodyString { 53 | return [self requestWithBody: [bodyString dataUsingEncoding: NSUTF8StringEncoding]]; 54 | } 55 | 56 | + (BLIPRequest*) requestWithBody: (NSData*)body 57 | properties: (NSDictionary*)properties 58 | { 59 | return [[self alloc] _initWithConnection: nil body: body properties: properties]; 60 | } 61 | 62 | - (id)mutableCopyWithZone:(NSZone *)zone { 63 | Assert(self.complete); 64 | BLIPRequest *copy = [[self class] requestWithBody: self.body 65 | properties: self.properties]; 66 | copy.compressed = self.compressed; 67 | copy.urgent = self.urgent; 68 | copy.noReply = self.noReply; 69 | return copy; 70 | } 71 | 72 | 73 | - (BOOL) noReply {return (_flags & kBLIP_NoReply) != 0;} 74 | - (void) setNoReply: (BOOL)noReply {[self _setFlag: kBLIP_NoReply value: noReply];} 75 | - (BLIPConnection*) connection {return _connection;} 76 | 77 | - (void) setConnection: (BLIPConnection*)conn { 78 | Assert(_isMine && !_sent,@"Connection can only be set before sending"); 79 | _connection = conn; 80 | } 81 | 82 | 83 | - (BLIPResponse*) send { 84 | Assert(_connection,@"%@ has no connection to send over",self); 85 | Assert(!_sent,@"%@ was already sent",self); 86 | [self _encode]; 87 | BLIPResponse *response = self.response; 88 | if ([_connection _sendRequest: self response: response]) 89 | self.sent = YES; 90 | else 91 | response = nil; 92 | return response; 93 | } 94 | 95 | 96 | - (BLIPResponse*) response { 97 | if (! _response && ! self.noReply) 98 | _response = [[BLIPResponse alloc] _initWithRequest: self]; 99 | return _response; 100 | } 101 | 102 | - (void) deferResponse { 103 | // This will allocate _response, causing -repliedTo to become YES, so BLIPConnection won't 104 | // send an automatic empty response after the current request handler returns. 105 | LogTo(BLIP,@"Deferring response to %@",self); 106 | [self response]; 107 | } 108 | 109 | - (BOOL) repliedTo { 110 | return _response != nil; 111 | } 112 | 113 | - (void) respondWithData: (NSData*)data contentType: (NSString*)contentType { 114 | BLIPResponse *response = self.response; 115 | response.body = data; 116 | response.contentType = contentType; 117 | [response send]; 118 | } 119 | 120 | - (void) respondWithString: (NSString*)string { 121 | [self respondWithData: [string dataUsingEncoding: NSUTF8StringEncoding] 122 | contentType: @"text/plain; charset=UTF-8"]; 123 | } 124 | 125 | - (void) respondWithJSON: (id)jsonObject { 126 | BLIPResponse *response = self.response; 127 | response.bodyJSON = jsonObject; 128 | [response send]; 129 | } 130 | 131 | - (void) respondWithError: (NSError*)error { 132 | self.response.error = error; 133 | [self.response send]; 134 | } 135 | 136 | - (void) respondWithErrorCode: (int)errorCode message: (NSString*)errorMessage { 137 | [self respondWithError: BLIPMakeError(errorCode, @"%@",errorMessage)]; 138 | } 139 | 140 | - (void) respondWithException: (NSException*)exception { 141 | [self respondWithError: BLIPMakeError(kBLIPError_HandlerFailed, @"%@", exception.reason)]; 142 | } 143 | 144 | 145 | @end 146 | -------------------------------------------------------------------------------- /BLIP/BLIPConnection.h: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPConnection.h 3 | // BLIP 4 | // 5 | // Created by Jens Alfke on 4/1/13. 6 | // Copyright (c) 2013-2015 Couchbase, Inc. All rights reserved. 7 | 8 | #import "BLIPMessage.h" 9 | @class BLIPRequest, BLIPResponse; 10 | @protocol BLIPConnectionDelegate; 11 | 12 | 13 | /** A network connection to a peer that can send and receive BLIP messages. 14 | This is an abstract class that doesn't use any specific transport. Subclasses must use and 15 | implement the methods declared in BLIPConnection+Transport.h. Clients should instantiate 16 | BLIPPocketSocketConnection. */ 17 | @interface BLIPConnection : NSObject 18 | 19 | /** Attaches a delegate, and specifies what GCD queue it should be called on. */ 20 | - (void) setDelegate: (id)delegate 21 | queue: (dispatch_queue_t)delegateQueue; 22 | 23 | /** Registers an action message to be sent to the delegate when a request with a specific profile 24 | is received. The method invoked should look like: 25 | - (void) methodname: (BLIPRequest*)rq; 26 | This occurs instead of sending the regular -blipConnection:receivedRequest:. */ 27 | - (void) onRequestProfile: (NSString*)profile 28 | sendDelegateAction: (SEL)action; 29 | 30 | /** Bulk registration of per-profile delegate actions (see -onRequestProfile:sendDelegateAction:.) 31 | The dictionary's keys are request profiles, and the corresponding values are the action message 32 | selectors converted to NSStrings via NSStringFromSelector(). */ 33 | - (void) registerDelegateActions: (NSDictionary*)actions; 34 | 35 | @property (readonly) id delegate; 36 | 37 | /** URL this socket is connected to, _if_ it's a client socket; if it's an incoming one received 38 | by a BLIPConnectionListener, this is nil. */ 39 | @property (readonly) NSURL* URL; 40 | 41 | @property (readonly) NSError* error; 42 | 43 | - (BOOL) connect: (NSError**)outError; 44 | 45 | - (void)close; 46 | 47 | /** If set to YES, an incoming message will be dispatched to the delegate and/or dispatcher before it's complete, as soon as its properties are available. The application should then set a dataDelegate on the message to receive its data a frame at a time. */ 48 | @property BOOL dispatchPartialMessages; 49 | 50 | /** Creates a new, empty outgoing request. 51 | You should add properties and/or body data to the request, before sending it by 52 | calling its -send method. */ 53 | - (BLIPRequest*) request; 54 | 55 | /** Creates a new outgoing request. 56 | The body or properties may be nil; you can add additional data or properties by calling 57 | methods on the request itself, before sending it by calling its -send method. */ 58 | - (BLIPRequest*) requestWithBody: (NSData*)body 59 | properties: (NSDictionary*)properies; 60 | 61 | /** Sends a request over this connection. 62 | (Actually, it queues it to be sent; this method always returns immediately.) 63 | Call this instead of calling -send on the request itself, if the request was created with 64 | +[BLIPRequest requestWithBody:] and hasn't yet been assigned to any connection. 65 | This method will assign it to this connection before sending it. 66 | The request's matching response object will be returned, or nil if the request couldn't be sent. */ 67 | - (BLIPResponse*) sendRequest: (BLIPRequest*)request; 68 | 69 | /** Are any messages currently being sent or received? (Observable) */ 70 | @property (readonly) BOOL active; 71 | 72 | @end 73 | 74 | 75 | 76 | /** The delegate messages that BLIPConnection will send. 77 | All methods are optional. */ 78 | @protocol BLIPConnectionDelegate 79 | @optional 80 | 81 | - (BOOL)blipConnection: (BLIPConnection*)connection 82 | validateServerTrust: (SecTrustRef)trust; 83 | 84 | - (void)blipConnectionDidOpen:(BLIPConnection*)connection; 85 | 86 | - (void)blipConnection: (BLIPConnection*)connection 87 | didFailWithError: (NSError*)error; 88 | 89 | - (void)blipConnection: (BLIPConnection*)connection 90 | didCloseWithError: (NSError*)error; 91 | 92 | /** Called when a BLIPRequest is received from the peer (unless a delegate action has 93 | been registered for this type of request.) 94 | If the delegate wants to accept the request it should return YES; if it returns NO, 95 | a kBLIPError_NotFound error will be returned to the sender. 96 | The delegate should get the request's response object, fill in its data and properties 97 | or error property, and send it. 98 | If it doesn't explicitly send a response, a default empty one will be sent; 99 | to prevent this, call -deferResponse on the request if you want to send a response later. */ 100 | - (BOOL) blipConnection: (BLIPConnection*)connection 101 | receivedRequest: (BLIPRequest*)request; 102 | 103 | /** Called when a BLIPResponse (to one of your requests) is received from the peer. 104 | This is called after the response object's onComplete target, if any, is invoked.*/ 105 | - (void) blipConnection: (BLIPConnection*)connection 106 | receivedResponse: (BLIPResponse*)response; 107 | 108 | @end 109 | -------------------------------------------------------------------------------- /BLIP/BLIPProperties.m: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPProperties.m 3 | // BLIP 4 | // 5 | // Created by Jens Alfke on 5/13/08. 6 | // Copyright 2008-2013 Jens Alfke. 7 | // Copyright (c) 2013-2015 Couchbase, Inc. All rights reserved. 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 10 | // except in compliance with the License. You may obtain a copy of the License at 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // Unless required by applicable law or agreed to in writing, software distributed under the 13 | // License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 14 | // either express or implied. See the License for the specific language governing permissions 15 | // and limitations under the License. 16 | 17 | #import "BLIPProperties.h" 18 | #import "MYBuffer.h" 19 | #import "MYData.h" 20 | #import "MYLogging.h" 21 | #import "Test.h" 22 | #import "MYData.h" 23 | 24 | 25 | /** Common strings are abbreviated as single-byte strings in the packed form. 26 | The ascii value of the single character minus one is the index into this table. */ 27 | static const char* kAbbreviations[] = { 28 | "Profile", 29 | "Error-Code", 30 | "Error-Domain", 31 | 32 | "Content-Type", 33 | "application/json", 34 | "application/octet-stream", 35 | "text/plain; charset=UTF-8", 36 | "text/xml", 37 | 38 | "Accept", 39 | "Cache-Control", 40 | "must-revalidate", 41 | "If-Match", 42 | "If-None-Match", 43 | "Location", 44 | }; 45 | #define kNAbbreviations ((sizeof(kAbbreviations)/sizeof(const char*))) // cannot exceed 31! 46 | 47 | 48 | 49 | static NSString* readCString(MYSlice* slice) { 50 | const char* key = slice->bytes; 51 | size_t len = strlen(key); 52 | MYSliceMoveStart(slice, len+1); 53 | if (len == 0) 54 | return @""; 55 | uint8_t first = (uint8_t)key[0]; 56 | if (first < ' ' && key[1]=='\0') { 57 | // Single-control-character property string is an abbreviation: 58 | if (first > kNAbbreviations) 59 | return nil; 60 | static NSMutableArray* kAbbrevNSStrings; 61 | static dispatch_once_t onceToken; 62 | dispatch_once(&onceToken, ^{ 63 | kAbbrevNSStrings = [[NSMutableArray alloc] initWithCapacity: kNAbbreviations]; 64 | for (int i=0; i 0) { 90 | NSString* key = readCString(&buf); 91 | if (!key) 92 | return nil; 93 | NSString* value = readCString(&buf); 94 | if (!value) 95 | return nil; 96 | result[key] = value; 97 | } 98 | MYSliceMoveStartTo(data, buf.bytes); 99 | return result; 100 | } 101 | 102 | 103 | NSDictionary* BLIPReadPropertiesFromBuffer(MYBuffer* buffer, BOOL *complete) { 104 | MYSlice slice = buffer.flattened.my_asSlice; 105 | MYSlice readSlice = slice; 106 | NSDictionary* props = BLIPParseProperties(&readSlice, complete); 107 | if (props) 108 | [buffer readSliceOfMaxLength: slice.length - readSlice.length]; 109 | return props; 110 | } 111 | 112 | 113 | static void appendStr( NSMutableData *data, NSString *str ) { 114 | const char *utf8 = [str UTF8String]; 115 | for (uint8_t i=0; i 22 | @end 23 | 24 | 25 | @implementation BLIPPool 26 | { 27 | __weak id _delegate; 28 | dispatch_queue_t _queue; 29 | NSMutableDictionary* _sockets; 30 | } 31 | 32 | 33 | @synthesize delegate=_delegate; 34 | 35 | 36 | - (instancetype) initWithDelegate: (id)delegate 37 | dispatchQueue: (dispatch_queue_t)queue 38 | { 39 | self = [super init]; 40 | if (self) { 41 | _delegate = delegate; 42 | _queue = queue; 43 | _sockets = [[NSMutableDictionary alloc] init]; 44 | } 45 | return self; 46 | } 47 | 48 | 49 | - (void) dealloc { 50 | [self closeWithCode: PSWebSocketStatusCodeGoingAway reason: nil]; 51 | } 52 | 53 | 54 | // Returns an already-open BLIPWebSocketConnection to use to communicate with a given URL. 55 | - (BLIPConnection*) existingSocketToURL: (NSURL*)url error: (NSError**)outError { 56 | @synchronized(self) { 57 | return _sockets[url]; 58 | } 59 | } 60 | 61 | 62 | // Returns an open BLIPConnection to use to communicate with a given URL. 63 | - (BLIPConnection*) socketToURL: (NSURL*)url error: (NSError**)outError { 64 | @synchronized(self) { 65 | BLIPConnection* socket = _sockets[url]; 66 | if (!socket) { 67 | if (!_sockets) { 68 | // I'm closed already 69 | if (outError) 70 | *outError = nil; 71 | return nil; 72 | } 73 | socket = [[BLIPPocketSocketConnection alloc] initWithURL: url]; 74 | [socket setDelegate: self queue: _queue]; 75 | if (![socket connect: outError]) 76 | return nil; 77 | _sockets[url] = socket; 78 | } 79 | return socket; 80 | } 81 | } 82 | 83 | 84 | - (void) forgetSocket: (BLIPConnection*)webSocket { 85 | @synchronized(self) { 86 | [_sockets removeObjectForKey: webSocket.URL]; 87 | } 88 | } 89 | 90 | 91 | - (void) closeWithCode:(NSInteger)code reason:(NSString *)reason { 92 | NSDictionary* sockets; 93 | @synchronized(self) { 94 | sockets = _sockets; 95 | _sockets = nil; // marks that I'm closed 96 | } 97 | for (NSURL* url in sockets) { 98 | BLIPPocketSocketConnection* socket = sockets[url]; 99 | [socket closeWithCode: code reason: reason]; 100 | } 101 | } 102 | 103 | - (void) close { 104 | [self closeWithCode: PSWebSocketStatusCodeNormal reason: nil]; 105 | } 106 | 107 | 108 | #pragma mark - DELEGATE API: 109 | 110 | 111 | // These forward to the delegate, and didClose/didFail also forget the socket: 112 | 113 | 114 | - (void)blipConnectionDidOpen:(BLIPConnection*)webSocket { 115 | id delegate = _delegate; 116 | if ([delegate respondsToSelector: @selector(blipConnectionDidOpen:)]) 117 | [delegate blipConnectionDidOpen: webSocket]; 118 | } 119 | 120 | - (void)blipConnection: (BLIPConnection*)webSocket didFailWithError:(NSError *)error { 121 | [self forgetSocket: webSocket]; 122 | id delegate = _delegate; 123 | if ([delegate respondsToSelector: @selector(blipConnection:didFailWithError:)]) 124 | [delegate blipConnection: webSocket didFailWithError: error]; 125 | } 126 | 127 | - (void)blipConnection: (BLIPConnection*)webSocket 128 | didCloseWithError: (NSError*)error 129 | { 130 | [self forgetSocket: webSocket]; 131 | id delegate = _delegate; 132 | if ([delegate respondsToSelector: @selector(blipConnection:didCloseWithError:)]) 133 | [delegate blipConnection: webSocket didCloseWithError: error]; 134 | } 135 | 136 | - (BOOL) blipConnection: (BLIPConnection*)webSocket receivedRequest: (BLIPRequest*)request { 137 | id delegate = _delegate; 138 | return [delegate respondsToSelector: @selector(blipConnection:receivedRequest:)] 139 | && [delegate blipConnection: webSocket receivedRequest: request]; 140 | } 141 | 142 | - (void) blipConnection: (BLIPConnection*)webSocket receivedResponse: (BLIPResponse*)response { 143 | id delegate = _delegate; 144 | if ([delegate respondsToSelector: @selector(blipConnection:receivedResponse:)]) 145 | [delegate blipConnection: webSocket receivedResponse: response]; 146 | } 147 | 148 | 149 | @end 150 | -------------------------------------------------------------------------------- /BLIP/BLIPMessage.h: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPMessage.h 3 | // BLIP 4 | // 5 | // Created by Jens Alfke on 5/10/08. 6 | // Copyright 2008-2013 Jens Alfke. 7 | // Copyright (c) 2013-2015 Couchbase, Inc. All rights reserved. 8 | 9 | #import 10 | @class BLIPMessage, BLIPRequest, BLIPResponse, BLIPConnection; 11 | @protocol MYReader; 12 | 13 | 14 | /** NSError domain and codes for BLIP */ 15 | extern NSString* const BLIPErrorDomain; 16 | enum { 17 | kBLIPError_BadData = 1, 18 | kBLIPError_BadFrame, 19 | kBLIPError_Disconnected, 20 | kBLIPError_PeerNotAllowed, 21 | 22 | kBLIPError_Misc = 99, 23 | 24 | // errors returned in responses: 25 | kBLIPError_BadRequest = 400, 26 | kBLIPError_Forbidden = 403, 27 | kBLIPError_NotFound = 404, 28 | kBLIPError_BadRange = 416, 29 | 30 | kBLIPError_HandlerFailed = 501, 31 | kBLIPError_Unspecified = 599 // peer didn't send any detailed error info 32 | }; 33 | 34 | NSError *BLIPMakeError( int errorCode, NSString *message, ... ) __attribute__ ((format (__NSString__, 2, 3))); 35 | 36 | 37 | /** Abstract superclass for BLIP requests and responses. */ 38 | @interface BLIPMessage : NSObject 39 | 40 | /** The BLIPConnection associated with this message. */ 41 | @property (readonly,strong) BLIPConnection* connection; 42 | 43 | /** The onDataReceived callback allows for streaming incoming message data. If it's set, the block 44 | will be called every time more data arrives. The block can read data from the MYReader if it 45 | wants. Any data left unread will appear in the next call, and any data unread when the message 46 | is complete will be left in the .body property. 47 | (Note: If the message is compressed, onDataReceived won't be called while data arrives, just 48 | once at the end after decompression. This may be improved in the future.) */ 49 | @property (strong) void (^onDataReceived)(BLIPMessage*, id); 50 | 51 | /** Called after message data is sent over the socket. */ 52 | @property (strong) void (^onDataSent)(BLIPMessage*, uint64_t totalBytesSent); 53 | 54 | /** Called when the message has been completely sent over the socket. */ 55 | @property (strong) void (^onSent)(BLIPMessage*); 56 | 57 | /** This message's serial number in its connection. 58 | A BLIPRequest's number is initially zero, then assigned when it's sent. 59 | A BLIPResponse is automatically assigned the same number as the request it replies to. */ 60 | @property (readonly) uint32_t number; 61 | 62 | /** Is this a message sent by me (as opposed to the peer)? */ 63 | @property (readonly) BOOL isMine; 64 | 65 | /** Is this a request or a response? */ 66 | @property (readonly) BOOL isRequest; 67 | 68 | /** Has this message been sent yet? (Only makes sense when isMine is true.) */ 69 | @property (readonly) BOOL sent; 70 | 71 | /** Has enough of the message arrived to read its properies? */ 72 | @property (readonly) BOOL propertiesAvailable; 73 | 74 | /** Has the entire message, including the body, arrived? */ 75 | @property (readonly) BOOL complete; 76 | 77 | /** Should the message body be compressed with gzip? 78 | This property can only be set before sending the message. */ 79 | @property BOOL compressed; 80 | 81 | /** Should the message be sent ahead of normal-priority messages? 82 | This property can only be set before sending the message. */ 83 | @property BOOL urgent; 84 | 85 | /** Can this message be changed? (Only true for outgoing messages, before you send them.) */ 86 | @property (readonly) BOOL isMutable; 87 | 88 | /** The message body, a blob of arbitrary data. */ 89 | @property (copy) NSData *body; 90 | 91 | /** Appends data to the body. */ 92 | - (void) addToBody: (NSData*)data; 93 | 94 | /** Appends the contents of a stream to the body. Don't close the stream afterwards, or read from 95 | it; the BLIPMessage will read from it later, while the message is being delivered, and close 96 | it when it's done. */ 97 | - (void) addStreamToBody: (NSInputStream*)stream; 98 | 99 | /** The message body as an NSString. 100 | The UTF-8 character encoding is used to convert. */ 101 | @property (copy) NSString *bodyString; 102 | 103 | /** The message body as a JSON-serializable object. 104 | The setter will raise an exception if the value can't be serialized; 105 | the getter just warns and returns nil. */ 106 | @property (copy) id bodyJSON; 107 | 108 | /** An arbitrary object that you can associate with this message for your own purposes. 109 | The message retains it, but doesn't do anything else with it. */ 110 | @property (strong) id representedObject; 111 | 112 | #pragma mark PROPERTIES: 113 | 114 | /** The message's properties, a dictionary-like object. 115 | Message properties are much like the headers in HTTP, MIME and RFC822. */ 116 | @property (readonly) NSDictionary* properties; 117 | 118 | /** Mutable version of the message's properties; only available if this mesage is mutable. */ 119 | @property (readonly) NSMutableDictionary* mutableProperties; 120 | 121 | /** The value of the "Content-Type" property, which is by convention the MIME type of the body. */ 122 | @property (copy) NSString *contentType; 123 | 124 | /** The value of the "Profile" property, which by convention identifies the purpose of the message. */ 125 | @property (copy) NSString *profile; 126 | 127 | /** A shortcut to get the value of a property. */ 128 | - (NSString*)objectForKeyedSubscript:(NSString*)key; 129 | 130 | /** A shortcut to set the value of a property. A nil value deletes that property. */ 131 | - (void) setObject: (NSString*)value forKeyedSubscript:(NSString*)key; 132 | 133 | /** Similar to -description, but also shows the properties and their values. */ 134 | @property (readonly) NSString* descriptionWithProperties; 135 | 136 | 137 | @end 138 | -------------------------------------------------------------------------------- /BLIP/BLIPResponse.m: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPResponse.m 3 | // BLIP 4 | // 5 | // Created by Jens Alfke on 9/15/13. 6 | // Copyright (c) 2013-2015 Couchbase, Inc. All rights reserved. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 9 | // except in compliance with the License. You may obtain a copy of the License at 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // Unless required by applicable law or agreed to in writing, software distributed under the 12 | // License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | // either express or implied. See the License for the specific language governing permissions 14 | // and limitations under the License. 15 | 16 | #import "BLIPResponse.h" 17 | #import "BLIPConnection.h" 18 | #import "BLIP_Internal.h" 19 | 20 | #import "ExceptionUtils.h" 21 | 22 | 23 | @implementation BLIPResponse 24 | { 25 | void (^_onComplete)(); 26 | } 27 | 28 | - (instancetype) _initWithRequest: (BLIPRequest*)request { 29 | Assert(request); 30 | self = [super _initWithConnection: request.connection 31 | isMine: !request.isMine 32 | flags: kBLIP_RPY | kBLIP_MoreComing 33 | number: request.number 34 | body: nil]; 35 | if (self != nil) { 36 | if (_isMine && request.urgent) 37 | _flags |= kBLIP_Urgent; 38 | } 39 | return self; 40 | } 41 | 42 | 43 | #if DEBUG 44 | // For testing only 45 | - (instancetype) _initIncomingWithProperties: (NSDictionary*)properties body: (NSData*)body { 46 | self = [self _initWithConnection: nil 47 | isMine: NO 48 | flags: kBLIP_MSG 49 | number: 0 50 | body: nil]; 51 | if (self != nil ) { 52 | _body = [body copy]; 53 | _isMutable = NO; 54 | _properties = properties; 55 | } 56 | return self; 57 | } 58 | #endif 59 | 60 | 61 | - (NSError*) error { 62 | if ((_flags & kBLIP_TypeMask) != kBLIP_ERR) 63 | return nil; 64 | 65 | NSMutableDictionary *userInfo = [_properties mutableCopy]; 66 | NSString *domain = userInfo[@"Error-Domain"]; 67 | int code = [userInfo[@"Error-Code"] intValue]; 68 | if (domain==nil || code==0) { 69 | domain = BLIPErrorDomain; 70 | if (code==0) 71 | code = kBLIPError_Unspecified; 72 | } 73 | [userInfo removeObjectForKey: @"Error-Domain"]; 74 | [userInfo removeObjectForKey: @"Error-Code"]; 75 | 76 | NSString* message = self.bodyString; 77 | if (message.length > 0) 78 | userInfo[NSLocalizedDescriptionKey] = message; 79 | 80 | return [NSError errorWithDomain: domain code: code userInfo: userInfo]; 81 | } 82 | 83 | - (void) _setError: (NSError*)error { 84 | _flags &= ~kBLIP_TypeMask; 85 | if (error) { 86 | // Setting this stuff is a PITA because this object might be technically immutable, 87 | // in which case the standard setters would barf if I called them. 88 | _flags |= kBLIP_ERR; 89 | 90 | NSMutableDictionary *errorProps = [self.properties mutableCopy]; 91 | if (! errorProps) 92 | errorProps = [[NSMutableDictionary alloc] init]; 93 | NSDictionary *userInfo = error.userInfo; 94 | for (NSString *key in userInfo) { 95 | id value = $castIf(NSString,userInfo[key]); 96 | if (value && ![key isEqualToString: NSLocalizedDescriptionKey] 97 | && ![key isEqualToString: NSLocalizedFailureReasonErrorKey]) { 98 | errorProps[key] = value; 99 | } 100 | } 101 | errorProps[@"Error-Domain"] = error.domain; 102 | errorProps[@"Error-Code"] = $sprintf(@"%li",(long)error.code); 103 | _properties = errorProps; 104 | 105 | NSString* message = userInfo[NSLocalizedDescriptionKey] 106 | ?: userInfo[NSLocalizedFailureReasonErrorKey]; 107 | _mutableBody = [[message dataUsingEncoding: NSUTF8StringEncoding] mutableCopy]; 108 | _body = nil; 109 | 110 | } else { 111 | _flags |= kBLIP_RPY; 112 | [self.mutableProperties removeAllObjects]; 113 | } 114 | } 115 | 116 | - (void) setError: (NSError*)error { 117 | Assert(_isMine && _isMutable); 118 | [self _setError: error]; 119 | } 120 | 121 | 122 | - (BOOL) send { 123 | Assert(_connection,@"%@ has no connection to send over",self); 124 | Assert(!_sent,@"%@ was already sent",self); 125 | [self _encode]; 126 | BOOL sent = self.sent = [_connection _sendResponse: self]; 127 | Assert(sent); 128 | return sent; 129 | } 130 | 131 | 132 | @synthesize onComplete=_onComplete; 133 | 134 | 135 | - (void) setComplete: (BOOL)complete { 136 | [super setComplete: complete]; 137 | if (complete && _onComplete) { 138 | @try{ 139 | _onComplete(self); 140 | }catchAndReport(@"BLIPRequest onComplete block"); 141 | _onComplete = nil; 142 | } 143 | } 144 | 145 | 146 | - (void) _connectionClosed { 147 | [super _connectionClosed]; 148 | if (!_isMine && !_complete) { 149 | NSError *error = _connection.error; 150 | if (!error) 151 | error = BLIPMakeError(kBLIPError_Disconnected, 152 | @"Connection closed before response was received"); 153 | // Change incoming response to an error: 154 | _isMutable = YES; 155 | _properties = [_properties mutableCopy]; 156 | [self _setError: error]; 157 | _isMutable = NO; 158 | 159 | self.complete = YES; // Calls onComplete target 160 | } 161 | } 162 | 163 | 164 | @end 165 | -------------------------------------------------------------------------------- /BLIP/BLIPPocketSocketConnection.m: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPPocketSocketConnection.m 3 | // BLIP 4 | // 5 | // Created by Jens Alfke on 4/10/15. 6 | // Copyright (c) 2015 Couchbase, Inc. All rights reserved. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 9 | // except in compliance with the License. You may obtain a copy of the License at 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // Unless required by applicable law or agreed to in writing, software distributed under the 12 | // License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | // either express or implied. See the License for the specific language governing permissions 14 | // and limitations under the License. 15 | 16 | #import "BLIPPocketSocket_Internal.h" 17 | #import "BLIPConnection+Transport.h" 18 | #import "BLIPHTTPLogic.h" 19 | #import "BLIP_Internal.h" 20 | #import "PSWebSocket.h" 21 | 22 | #import "MYErrorUtils.h" 23 | 24 | 25 | @implementation BLIPPocketSocketConnection 26 | { 27 | BLIPHTTPLogic* _httpLogic; 28 | } 29 | 30 | @synthesize webSocket=_webSocket, URL=_URL; 31 | 32 | // Designated initializer 33 | - (instancetype) initWithWebSocket: (PSWebSocket*)webSocket 34 | transportQueue: (dispatch_queue_t)transportQueue 35 | URL: (NSURL*)url 36 | incoming: (BOOL)incoming { 37 | Assert(transportQueue); 38 | self = [super initWithTransportQueue: transportQueue isOpen: incoming]; 39 | if (self) { 40 | _webSocket = webSocket; 41 | if (!incoming) 42 | _webSocket.delegate = self; 43 | _URL = url; 44 | } 45 | return self; 46 | } 47 | 48 | // Public API 49 | - (instancetype) initWithURLRequest:(NSURLRequest *)request { 50 | Assert(request); 51 | dispatch_queue_t queue = dispatch_queue_create("BLIPConnection", DISPATCH_QUEUE_SERIAL); 52 | self = [self initWithWebSocket: nil 53 | transportQueue: queue 54 | URL: request.URL 55 | incoming: NO]; 56 | if (self) { 57 | _httpLogic = [[BLIPHTTPLogic alloc] initWithURLRequest: request]; 58 | [_httpLogic setValue: @"BLIP" forHTTPHeaderField: @"Sec-WebSocket-Protocol"]; 59 | } 60 | return self; 61 | } 62 | 63 | // Public API 64 | - (instancetype) initWithURL:(NSURL *)url { 65 | return [self initWithURLRequest: [NSURLRequest requestWithURL: url]]; 66 | } 67 | 68 | 69 | - (void) setCredential: (NSURLCredential*)credential { 70 | //FIX!! 71 | _httpLogic.credential = credential; 72 | } 73 | 74 | 75 | // Public API 76 | - (BOOL) connect: (NSError**)outError { 77 | LogTo(BLIP, @"%@ connecting to <%@>...", self, _httpLogic.URL.absoluteString); 78 | _webSocket = [PSWebSocket clientSocketWithRequest: _httpLogic.URLRequest]; 79 | _webSocket.delegate = self; 80 | _webSocket.delegateQueue = self.transportQueue; 81 | 82 | NSURLCredential* credential = _httpLogic.credential; 83 | if (credential.identity) { 84 | NSArray* certs = @[(__bridge id)credential.identity]; 85 | if (credential.certificates) 86 | certs = [certs arrayByAddingObjectsFromArray: credential.certificates]; 87 | _webSocket.SSLClientCertificates = certs; 88 | } 89 | 90 | [_webSocket open]; 91 | return YES; 92 | } 93 | 94 | // Public API 95 | - (void) close { 96 | NSError* error = self.error; 97 | if (error == nil) { 98 | [_webSocket close]; 99 | } else if ([error.domain isEqualToString: @"WebSocketCloseCode"]) { 100 | [_webSocket closeWithCode: error.code reason: error.localizedFailureReason]; 101 | } else { 102 | Warn(@"BLIPPocketSocketConnection closing due to %@", error.my_compactDescription); 103 | [_webSocket closeWithCode: 1008 /*PolicyError*/ reason: error.localizedDescription]; 104 | } 105 | } 106 | 107 | // Public API 108 | - (void) closeWithCode: (NSInteger)code reason:(NSString *)reason { 109 | [_webSocket closeWithCode: code reason: reason]; 110 | } 111 | 112 | // override 113 | - (BOOL) transportCanSend { 114 | return _webSocket.readyState == PSWebSocketReadyStateOpen; 115 | } 116 | 117 | // override 118 | - (void) sendFrame:(NSData *)frame { 119 | [_webSocket send: frame]; 120 | } 121 | 122 | // WebSocket delegate method 123 | - (void)webSocket:(PSWebSocket *)webSocket didReceiveMessage:(id)message { 124 | if ([message isKindOfClass: [NSData class]]) 125 | [self didReceiveFrame: message]; 126 | } 127 | 128 | // WebSocket delegate method 129 | - (void)webSocketDidOpen:(PSWebSocket *)webSocket { 130 | [self transportDidOpen]; 131 | } 132 | 133 | // WebSocket delegate method 134 | - (BOOL)webSocket:(PSWebSocket *)webSocket validateServerTrust: (SecTrustRef)trust { 135 | return [self.delegate respondsToSelector: @selector(blipConnection:validateServerTrust:)] 136 | && [self.delegate blipConnection: self validateServerTrust: trust]; 137 | } 138 | 139 | // WebSocket delegate method 140 | - (void) webSocket: (PSWebSocket *)webSocket didFailWithError: (NSError *)error { 141 | if ([error my_hasDomain: PSWebSocketErrorDomain code: PSWebSocketErrorCodeTimedOut]) { 142 | error = [NSError errorWithDomain: NSURLErrorDomain code: NSURLErrorTimedOut 143 | userInfo: error.userInfo]; 144 | } else if ([error my_hasDomain: NSOSStatusErrorDomain code: errSSLXCertChainInvalid]) { 145 | error = [NSError errorWithDomain: NSURLErrorDomain 146 | code: NSURLErrorServerCertificateUntrusted 147 | userInfo: nil]; 148 | } else if ([error my_hasDomain: PSWebSocketErrorDomain code: PSWebSocketErrorCodeHandshakeFailed]) { 149 | // HTTP error; ask _httpLogic what to do: 150 | CFHTTPMessageRef response = (__bridge CFHTTPMessageRef)error.userInfo[PSHTTPResponseErrorKey]; 151 | NSInteger status = CFHTTPMessageGetResponseStatusCode(response); 152 | [_httpLogic receivedResponse: response]; 153 | if (_httpLogic.shouldRetry) { 154 | LogTo(BLIP, @"%@ got HTTP response %ld, retrying...", self, (long)status); 155 | _webSocket.delegate = nil; 156 | [self connect: NULL]; 157 | return; 158 | } 159 | NSString* message = CFBridgingRelease(CFHTTPMessageCopyResponseStatusLine(response)); 160 | // Failed, but map the error back to HTTP: 161 | NSString* urlStr = webSocket.URLRequest.URL.absoluteString; 162 | error = [NSError errorWithDomain: @"HTTP" 163 | code: status 164 | userInfo: @{NSLocalizedDescriptionKey: message, 165 | NSURLErrorFailingURLStringErrorKey: urlStr}]; 166 | } else { 167 | NSDictionary* kErrorMap = @{ 168 | PSWebSocketErrorDomain: @{@(PSWebSocketErrorCodeTimedOut): 169 | @[NSURLErrorDomain, @(NSURLErrorTimedOut)]}, 170 | NSOSStatusErrorDomain: @{@(NSURLErrorServerCertificateUntrusted): 171 | @[NSURLErrorDomain, @(NSURLErrorServerCertificateUntrusted)]}, 172 | (id)kCFErrorDomainCFNetwork: @{@(kCFHostErrorUnknown): @[NSURLErrorDomain, @(kCFURLErrorCannotFindHost)]}, 173 | }; 174 | error = MYMapError(error, kErrorMap); 175 | } 176 | [self transportDidCloseWithError: error]; 177 | } 178 | 179 | // WebSocket delegate method 180 | - (void) webSocket: (PSWebSocket *)webSocket 181 | didCloseWithCode:(NSInteger)code 182 | reason:(NSString *)reason 183 | wasClean:(BOOL)wasClean 184 | { 185 | NSError* error = nil; 186 | if (code != PSWebSocketStatusCodeNormal || !wasClean) { 187 | NSDictionary* info = $dict({NSLocalizedFailureReasonErrorKey, reason}); 188 | error = [NSError errorWithDomain: @"WebSocketCloseCode" code: code userInfo: info]; 189 | } 190 | [self transportDidCloseWithError: error]; 191 | } 192 | 193 | // WebSocket delegate method 194 | - (void) webSocketIsHungry: (PSWebSocket *)ws { 195 | [self feedTransport]; 196 | } 197 | 198 | @end 199 | -------------------------------------------------------------------------------- /BLIP/BLIPPocketSocketListener.m: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPPocketSocketListener.m 3 | // BLIP 4 | // 5 | // Created by Jens Alfke on 4/11/15. 6 | // Copyright (c) 2015 Couchbase, Inc. All rights reserved. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 9 | // except in compliance with the License. You may obtain a copy of the License at 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // Unless required by applicable law or agreed to in writing, software distributed under the 12 | // License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | // either express or implied. See the License for the specific language governing permissions 14 | // and limitations under the License. 15 | 16 | #import "BLIPPocketSocketListener.h" 17 | #import "BLIPPocketSocket_Internal.h" 18 | #import "BLIPConnection+Transport.h" 19 | #import "BLIP_Internal.h" 20 | #import "PSWebSocket.h" 21 | #import "PSWebSocketServer.h" 22 | 23 | 24 | @interface BLIPPocketSocketListener () 25 | @end 26 | 27 | 28 | @implementation BLIPPocketSocketListener 29 | { 30 | PSWebSocketServer* _server; 31 | NSArray* _paths; 32 | id _delegate; 33 | dispatch_queue_t _delegateQueue; 34 | NSMutableDictionary* _sockets; // Maps NSValue (PSWebSocket*) to BLIPPocketSocketConnection 35 | 36 | NSMutableDictionary* _passwords; 37 | } 38 | 39 | - (instancetype) initWithPaths: (NSArray*)paths 40 | delegate: (id)delegate 41 | queue: (dispatch_queue_t)queue 42 | { 43 | self = [super init]; 44 | if (self) { 45 | _paths = [paths copy]; 46 | _delegate = delegate; 47 | _delegateQueue = queue ?: dispatch_get_main_queue(); 48 | } 49 | return self; 50 | } 51 | 52 | - (instancetype)init { 53 | NSAssert(NO, @"init method is unavailable"); 54 | return nil; 55 | } 56 | 57 | - (BOOL) acceptOnInterface: (NSString*)interface 58 | port: (uint16_t)port 59 | SSLCertificates: (NSArray*)certs 60 | error: (NSError**)error 61 | { 62 | LogTo(BLIP, @"%@ opening on %@:%d at paths {'%@'}", self, interface, port, 63 | [_paths componentsJoinedByString: @"', '"]); 64 | _sockets = [NSMutableDictionary new]; 65 | _server = [PSWebSocketServer serverWithHost: interface port: port SSLCertificates: certs]; 66 | _server.delegate = self; 67 | _server.delegateQueue = dispatch_queue_create("BLIP Listener", DISPATCH_QUEUE_SERIAL); 68 | [_server start]; 69 | return YES; 70 | } 71 | 72 | - (void)serverDidStart:(PSWebSocketServer *)server { 73 | LogTo(BLIP, @"BLIPPocketSocketListener is listening on port %d...", _server.realPort); 74 | [self listenerDidStart]; 75 | } 76 | 77 | - (void)serverDidStop:(PSWebSocketServer *)server { 78 | LogTo(BLIP, @"BLIPPocketSocketListener stopped"); 79 | _server.delegate = nil; 80 | _server = nil; 81 | _delegate = nil; 82 | _sockets = nil; 83 | [self listenerDidStop]; 84 | } 85 | 86 | - (void)server:(PSWebSocketServer *)server 87 | didFailWithError:(NSError *)error 88 | { 89 | Warn(@"BLIPPocketSocketListener failed to open: %@", error.my_compactDescription); 90 | [self listenerDidFailWithError: error]; 91 | } 92 | 93 | - (void)listenerDidStart { } 94 | - (void)listenerDidStop { } 95 | - (void)listenerDidFailWithError:(NSError *)error { } 96 | 97 | - (void) disconnect { 98 | LogTo(BLIP, @"BLIPPocketSocketListener disconnect"); 99 | [_server stop]; 100 | _server.delegate = nil; 101 | _server = nil; 102 | _sockets = nil; 103 | } 104 | 105 | - (uint16_t) port { 106 | return _server.realPort; 107 | } 108 | 109 | 110 | #pragma mark - AUTHENTICATION: 111 | 112 | 113 | @synthesize realm=_realm; 114 | 115 | - (void) setPasswords: (NSDictionary*)passwords { 116 | _passwords = [passwords copy]; 117 | } 118 | 119 | - (NSString*) passwordForUser:(NSString *)username { 120 | return _passwords[username]; 121 | } 122 | 123 | 124 | + (BOOL) fromRequest: (NSURLRequest*)request 125 | getUsername: (NSString**)outUser 126 | password: (NSString**)outPassword 127 | { 128 | *outUser = nil; 129 | NSString* auth = [request valueForHTTPHeaderField: @"Authorization"]; 130 | if (!auth) 131 | return YES; 132 | if (![auth hasPrefix: @"Basic "]) 133 | return NO; 134 | NSData* credData = [[NSData alloc] 135 | initWithBase64EncodedString: [auth substringFromIndex: 6] 136 | options: NSDataBase64DecodingIgnoreUnknownCharacters]; 137 | if (!credData) 138 | return NO; 139 | NSString* cred = [[NSString alloc] initWithData: credData encoding: NSUTF8StringEncoding]; 140 | NSRange colon = [cred rangeOfString:@":"]; 141 | if (colon.length == 0) 142 | return NO; 143 | *outUser = [cred substringToIndex: colon.location]; 144 | if (outPassword) 145 | *outPassword = [cred substringFromIndex: NSMaxRange(colon)]; 146 | return YES; 147 | } 148 | 149 | 150 | - (BOOL) checkAuthentication: (NSURLRequest*)request user: (NSString**)outUser { 151 | NSString* password; 152 | if (![[self class] fromRequest: request getUsername: outUser password: &password]) 153 | return NO; 154 | else if (*outUser == nil) 155 | return (_passwords == nil); 156 | else 157 | return [password isEqualToString: [self passwordForUser: *outUser]]; 158 | } 159 | 160 | 161 | - (BOOL) checkClientCertificateAuthentication: (SecTrustRef)trust 162 | fromAddress: (NSData*)address 163 | { 164 | return YES; 165 | } 166 | 167 | 168 | #pragma mark - DELEGATE: 169 | 170 | 171 | - (BOOL)server:(PSWebSocketServer *)server 172 | acceptWebSocketFrom:(NSData*)address 173 | withRequest:(NSURLRequest *)request 174 | trust:(SecTrustRef)trust 175 | response:(NSHTTPURLResponse**)outResponse 176 | { 177 | LogTo(BLIP, @"Got request for %@ ; trust %@ ; headers = %@", 178 | request.URL, trust, request.allHTTPHeaderFields); 179 | 180 | int status; 181 | NSDictionary* headers = nil; 182 | if (![self checkClientCertificateAuthentication: trust fromAddress: address]) { 183 | LogTo(BLIP, @"Rejected bad client cert"); 184 | status = 401; 185 | } else { 186 | NSString* username; 187 | if (![self checkAuthentication: request user: &username]) { 188 | // Auth failure: 189 | LogTo(BLIP, @"Rejected bad login for user '%@'", username); 190 | headers = @{@"WWW-Authenticate": $sprintf(@"Basic realm=\"%@\"", _realm)}; 191 | status = 401; 192 | } else { 193 | if (username) 194 | LogTo(BLIP, @"Authenticated user '%@'", username); 195 | NSString* path = request.URL.path; 196 | if (![_paths containsObject: path]) { 197 | // Unknown path: 198 | status = 404; 199 | } else { 200 | // Success: 201 | status = 200; 202 | headers = @{@"Sec-WebSocket-Protocol": @"BLIP"}; 203 | } 204 | } 205 | } 206 | 207 | *outResponse = [[NSHTTPURLResponse alloc] initWithURL: request.URL 208 | statusCode: status 209 | HTTPVersion: @"HTTP/1.1" 210 | headerFields: headers]; 211 | return (status < 300); 212 | } 213 | 214 | 215 | - (void)server:(PSWebSocketServer *)server 216 | webSocketDidOpen:(PSWebSocket *)webSocket 217 | { 218 | CFTypeRef ref = [webSocket copyStreamPropertyForKey: (id)kCFStreamPropertySSLContext]; 219 | NSString* scheme = @"ws"; 220 | if (ref) { 221 | scheme = @"wss"; 222 | CFRelease(ref); 223 | } 224 | NSString* host = webSocket.remoteHost; 225 | NSURL* url = [NSURL URLWithString: $sprintf(@"%@://%@/", scheme, host)]; 226 | LogTo(BLIP, @"Accepted incoming connection from %@", url); 227 | 228 | BLIPPocketSocketConnection* conn; 229 | conn = [[BLIPPocketSocketConnection alloc] initWithWebSocket: webSocket 230 | transportQueue: _server.delegateQueue 231 | URL: url 232 | incoming: YES]; 233 | id key = [NSValue valueWithNonretainedObject: webSocket]; 234 | _sockets[key] = conn; 235 | 236 | [conn setDelegate: _delegate queue: _delegateQueue]; 237 | dispatch_async(_delegateQueue, ^{ 238 | [self blipConnectionDidOpen: conn]; 239 | }); 240 | } 241 | 242 | - (void)server:(PSWebSocketServer *)server 243 | webSocket:(PSWebSocket *)webSocket 244 | didReceiveMessage:(id)message 245 | { 246 | id key = [NSValue valueWithNonretainedObject: webSocket]; 247 | BLIPPocketSocketConnection* conn = _sockets[key]; 248 | [conn webSocket: webSocket didReceiveMessage: message]; 249 | } 250 | 251 | - (void)server:(PSWebSocketServer *)server 252 | webSocketIsHungry:(PSWebSocket *)webSocket 253 | { 254 | id key = [NSValue valueWithNonretainedObject: webSocket]; 255 | BLIPPocketSocketConnection* conn = _sockets[key]; 256 | [conn webSocketIsHungry: webSocket]; 257 | } 258 | 259 | - (void)server:(PSWebSocketServer *)server 260 | webSocket:(PSWebSocket *)webSocket 261 | didFailWithError:(NSError *)error 262 | { 263 | id key = [NSValue valueWithNonretainedObject: webSocket]; 264 | BLIPPocketSocketConnection* conn = _sockets[key]; 265 | [conn webSocket: webSocket didFailWithError: error]; 266 | [_sockets removeObjectForKey: key]; 267 | } 268 | 269 | - (void)server:(PSWebSocketServer *)server 270 | webSocket:(PSWebSocket *)webSocket 271 | didCloseWithCode:(NSInteger)code 272 | reason:(NSString *)reason 273 | wasClean:(BOOL)wasClean 274 | { 275 | id key = [NSValue valueWithNonretainedObject: webSocket]; 276 | BLIPPocketSocketConnection* conn = _sockets[key]; 277 | [conn webSocket: webSocket didCloseWithCode: code reason: reason wasClean: wasClean]; 278 | [_sockets removeObjectForKey: key]; 279 | } 280 | 281 | 282 | - (void)blipConnectionDidOpen:(BLIPConnection*)b { 283 | } 284 | 285 | 286 | @end 287 | 288 | 289 | 290 | @implementation BLIPPocketSocketConnection (Incoming) 291 | 292 | - (NSURLCredential*) credential { 293 | // First check for an SSL client certificate: 294 | PSWebSocket* webSocket = self.webSocket; 295 | NSArray* clientCerts = webSocket.SSLClientCertificates; 296 | if (clientCerts) { 297 | if (clientCerts.count == 0) 298 | return nil; // should never happen 299 | SecIdentityRef identity = (__bridge SecIdentityRef)clientCerts[0]; 300 | clientCerts = [clientCerts subarrayWithRange: NSMakeRange(1, clientCerts.count -1)]; 301 | return [NSURLCredential credentialWithIdentity: identity certificates: clientCerts 302 | persistence: NSURLCredentialPersistenceNone]; 303 | } 304 | 305 | // Then check for HTTP auth: 306 | NSURLRequest* request = webSocket.URLRequest; 307 | if (!request) 308 | return nil; 309 | NSString *username, *password; 310 | if (![BLIPPocketSocketListener fromRequest: request getUsername: &username password: &password] 311 | || !username) 312 | return nil; 313 | return [NSURLCredential credentialWithUser: username password: password 314 | persistence: NSURLCredentialPersistenceNone]; 315 | } 316 | 317 | @end 318 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /BLIP/BLIPHTTPLogic.m: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPHTTPLogic.m 3 | // BLIP 4 | // 5 | // Created by Jens Alfke on 11/13/13. 6 | // Copyright (c) 2013-2015 Couchbase, Inc. All rights reserved. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 9 | // except in compliance with the License. You may obtain a copy of the License at 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // Unless required by applicable law or agreed to in writing, software distributed under the 12 | // License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | // either express or implied. See the License for the specific language governing permissions 14 | // and limitations under the License. 15 | 16 | #import "BLIPHTTPLogic.h" 17 | #import "MYLogging.h" 18 | #import "Test.h" 19 | #import "MYURLUtils.h" 20 | 21 | 22 | UsingLogDomain(BLIP); 23 | 24 | 25 | #define kMaxRedirects 10 26 | 27 | 28 | @implementation BLIPHTTPLogic 29 | { 30 | NSMutableURLRequest* _urlRequest; 31 | NSString* _nonceKey; 32 | NSString* _authorizationHeader; 33 | CFHTTPMessageRef _responseMsg; 34 | NSURLCredential* _credential; 35 | NSUInteger _redirectCount; 36 | } 37 | 38 | 39 | @synthesize handleRedirects=_handleRedirects, shouldContinue=_shouldContinue, 40 | shouldRetry=_shouldRetry, credential=_credential, httpStatus=_httpStatus, error=_error; 41 | 42 | 43 | - (instancetype) initWithURLRequest:(NSURLRequest *)urlRequest { 44 | NSParameterAssert(urlRequest); 45 | self = [super init]; 46 | if (self) { 47 | _urlRequest = [urlRequest mutableCopy]; 48 | _handleRedirects = YES; 49 | } 50 | return self; 51 | } 52 | 53 | 54 | - (void) dealloc { 55 | if (_responseMsg) CFRelease(_responseMsg); 56 | } 57 | 58 | 59 | - (NSURL*) URL { 60 | return _urlRequest.URL; 61 | } 62 | 63 | 64 | - (UInt16) port { 65 | NSNumber* portObj = self.URL.port; 66 | if (portObj) 67 | return (UInt16)portObj.intValue; 68 | else 69 | return self.useTLS ? 443 : 80; 70 | } 71 | 72 | - (BOOL) useTLS { 73 | NSString* scheme = self.URL.scheme.lowercaseString; 74 | return [scheme isEqualToString: @"https"] || [scheme isEqualToString: @"wss"]; 75 | } 76 | 77 | 78 | + (NSString*) userAgent { 79 | NSProcessInfo* process = [NSProcessInfo processInfo]; 80 | NSString* appVers = (__bridge NSString*)CFBundleGetValueForInfoDictionaryKey 81 | (CFBundleGetMainBundle(), kCFBundleVersionKey); 82 | return $sprintf(@"%@/%@ (%@ %@)", 83 | process.processName, appVers, 84 | #if TARGET_OS_IPHONE 85 | @"iOS", 86 | #else 87 | @"Mac OS X", 88 | #endif 89 | process.operatingSystemVersionString); 90 | } 91 | 92 | 93 | - (void) setValue: (NSString*)value forHTTPHeaderField:(NSString*)header { 94 | [_urlRequest setValue: value forHTTPHeaderField: header]; 95 | } 96 | 97 | - (void) setObject: (NSString*)value forKeyedSubscript: (NSString*)key { 98 | [_urlRequest setValue: value forHTTPHeaderField: key]; 99 | } 100 | 101 | 102 | - (NSURLRequest*) URLRequest { 103 | CFHTTPMessageRef httpMessage = [self newHTTPRequest]; 104 | 105 | NSMutableURLRequest* request = [_urlRequest mutableCopy]; 106 | NSDictionary* headers = CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(httpMessage)); 107 | for (NSString* headerName in headers) { 108 | if (![request valueForHTTPHeaderField: headerName]) 109 | [request setValue: headers[headerName] forHTTPHeaderField: headerName]; 110 | } 111 | CFRelease(httpMessage); 112 | return request; 113 | } 114 | 115 | 116 | - (CFHTTPMessageRef) newHTTPRequest { 117 | NSURL* url = self.URL; 118 | // Set/update the "Host" header: 119 | NSString* host = url.host; 120 | if (url.port) 121 | host = [host stringByAppendingFormat: @":%@", url.port]; 122 | [self setValue: host forHTTPHeaderField: @"Host"]; 123 | 124 | // Create the CFHTTPMessage: 125 | CFHTTPMessageRef httpMsg = CFHTTPMessageCreateRequest(NULL, 126 | (__bridge CFStringRef)_urlRequest.HTTPMethod, 127 | (__bridge CFURLRef)url, 128 | kCFHTTPVersion1_1); 129 | NSDictionary* headers = _urlRequest.allHTTPHeaderFields; 130 | for (NSString* header in headers) 131 | CFHTTPMessageSetHeaderFieldValue(httpMsg, (__bridge CFStringRef)header, 132 | (__bridge CFStringRef)headers[header]); 133 | 134 | // Add cookie headers from the NSHTTPCookieStorage: 135 | if (_urlRequest.HTTPShouldHandleCookies) { 136 | NSArray* cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL: url]; 137 | NSDictionary* cookieHeaders = [NSHTTPCookie requestHeaderFieldsWithCookies: cookies]; 138 | for (NSString* headerName in cookieHeaders) { 139 | CFHTTPMessageSetHeaderFieldValue(httpMsg, 140 | (__bridge CFStringRef)headerName, 141 | (__bridge CFStringRef)cookieHeaders[headerName]); 142 | } 143 | } 144 | 145 | // Add User-Agent if necessary: 146 | if (![_urlRequest valueForHTTPHeaderField: @"User-Agent"]) 147 | CFHTTPMessageSetHeaderFieldValue(httpMsg, CFSTR("User-Agent"), 148 | (__bridge CFStringRef)[[self class] userAgent]); 149 | 150 | // If this is a retry, set auth headers from the credential we got: 151 | if (_responseMsg && _credential.user) { 152 | NSString* password = _credential.password; 153 | if (!password) { 154 | // For some reason the password sometimes isn't accessible, even though we checked 155 | // .hasPassword when setting _credential earlier. (See #195.) Keychain bug?? 156 | // If this happens, try looking up the credential again: 157 | Log(@"Huh, couldn't get password of %@; trying again", _credential); 158 | _credential = [self credentialForAuthHeader: 159 | getHeader(_responseMsg, @"WWW-Authenticate")]; 160 | password = _credential.password; 161 | if (!password) 162 | Warn(@"%@: Unable to get password of credential %@", self, _credential); 163 | } 164 | if (!password || 165 | (!CFHTTPMessageAddAuthentication(httpMsg, _responseMsg, 166 | (__bridge CFStringRef)_credential.user, 167 | (__bridge CFStringRef)password, 168 | NULL, 169 | _httpStatus == 407) 170 | && !CFHTTPMessageAddAuthentication(httpMsg, _responseMsg, 171 | (__bridge CFStringRef)_credential.user, 172 | (__bridge CFStringRef)password, 173 | kCFHTTPAuthenticationSchemeBasic, // fallback 174 | _httpStatus == 407))) 175 | { 176 | // The 2nd call above works around a bug where it can fail if the auth scheme is NULL. 177 | Warn(@"%@: Unable to add authentication", self); 178 | _credential = nil; 179 | CFRelease(_responseMsg); 180 | _responseMsg = NULL; 181 | } 182 | } 183 | 184 | NSData* body = _urlRequest.HTTPBody; 185 | if (body) { 186 | CFHTTPMessageSetHeaderFieldValue(httpMsg, CFSTR("Content-Length"), 187 | (__bridge CFStringRef)[@(body.length) description]); 188 | CFHTTPMessageSetBody(httpMsg, (__bridge CFDataRef)body); 189 | } 190 | 191 | _authorizationHeader = getHeader(httpMsg, @"Authorization"); 192 | _shouldContinue = _shouldRetry = NO; 193 | _httpStatus = 0; 194 | 195 | return httpMsg; 196 | } 197 | 198 | 199 | - (void) receivedResponse: (CFHTTPMessageRef)response { 200 | NSParameterAssert(response); 201 | if (response == _responseMsg) 202 | return; 203 | if (_responseMsg) 204 | CFRelease(_responseMsg); 205 | _responseMsg = response; 206 | CFRetain(_responseMsg); 207 | 208 | _shouldContinue = _shouldRetry = NO; 209 | _httpStatus = (int) CFHTTPMessageGetResponseStatusCode(_responseMsg); 210 | switch (_httpStatus) { 211 | case 301: 212 | case 302: 213 | case 307: { 214 | // Redirect: 215 | if (!_handleRedirects) 216 | break; 217 | if (++_redirectCount > kMaxRedirects) { 218 | [self setErrorCode: NSURLErrorHTTPTooManyRedirects userInfo: nil]; 219 | } else if (![self redirect]) { 220 | [self setErrorCode: NSURLErrorRedirectToNonExistentLocation userInfo: nil]; 221 | } else { 222 | _shouldRetry = YES; 223 | } 224 | break; 225 | } 226 | 227 | case 401: 228 | case 407: { 229 | NSString* authResponse = getHeader(_responseMsg, @"WWW-Authenticate"); 230 | if (!_authorizationHeader) { 231 | if (!_credential) 232 | _credential = [self credentialForAuthHeader: authResponse]; 233 | LogTo(BLIP, @"%@: Auth challenge; credential = %@", self, _credential); 234 | if (_credential) { 235 | // Recoverable auth failure -- try again with new _credential: 236 | _shouldRetry = YES; 237 | break; 238 | } 239 | } 240 | Log(@"%@: HTTP auth failed; sent Authorization: %@ ; got WWW-Authenticate: %@", 241 | self, (_authorizationHeader ? @"XXXX" : _authorizationHeader), authResponse); 242 | NSDictionary* challengeInfo = [[self class] parseAuthHeader: authResponse]; 243 | NSDictionary* errorInfo = $dict({@"HTTPAuthorization", _authorizationHeader}, 244 | {@"HTTPAuthenticateHeader", authResponse}, 245 | {@"AuthChallenge", challengeInfo}); 246 | [self setErrorCode: NSURLErrorUserAuthenticationRequired userInfo: errorInfo]; 247 | break; 248 | } 249 | 250 | default: 251 | if (_httpStatus < 300) 252 | _shouldContinue = YES; 253 | break; 254 | } 255 | } 256 | 257 | 258 | - (BOOL) redirect { 259 | NSString* location = getHeader(_responseMsg, @"Location"); 260 | if (!location) 261 | return NO; 262 | NSURL* url = [NSURL URLWithString: location relativeToURL: self.URL]; 263 | if (!url) 264 | return NO; 265 | if ([url.scheme caseInsensitiveCompare: @"http"] != 0 && 266 | [url.scheme caseInsensitiveCompare: @"https"] != 0) 267 | return NO; 268 | _urlRequest.URL = url; 269 | return YES; 270 | } 271 | 272 | 273 | - (NSURLCredential*) credentialForAuthHeader: (NSString*)authHeader { 274 | // Basic & digest auth: http://www.ietf.org/rfc/rfc2617.txt 275 | NSDictionary* challenge = [[self class] parseAuthHeader: authHeader]; 276 | if (!challenge) 277 | return nil; 278 | 279 | // Get the auth type: 280 | NSString* authenticationMethod; 281 | NSString* scheme = challenge[@"Scheme"]; 282 | if ([scheme isEqualToString: @"Basic"]) 283 | authenticationMethod = NSURLAuthenticationMethodHTTPBasic; 284 | else if ([scheme isEqualToString: @"Digest"]) 285 | authenticationMethod = NSURLAuthenticationMethodHTTPDigest; 286 | else 287 | return nil; 288 | 289 | // Get the realm: 290 | NSString* realm = challenge[@"realm"]; 291 | if (!realm) 292 | return nil; 293 | 294 | NSURLCredential* cred; 295 | cred = [self.URL my_credentialForRealm: realm authenticationMethod: authenticationMethod]; 296 | if (!cred.hasPassword) 297 | cred = nil; // TODO: Add support for client certs 298 | return cred; 299 | } 300 | 301 | 302 | + (NSDictionary*) parseAuthHeader: (NSString*)authHeader { 303 | if (!authHeader) 304 | return nil; 305 | NSMutableDictionary* challenge = $mdict(); 306 | NSRegularExpression* re = [NSRegularExpression 307 | regularExpressionWithPattern: @"\(\\w+)\\s+(\\w+)=((\\w+)|\"([^\"]+))" 308 | options: 0 error: NULL]; 309 | Assert(re); 310 | NSArray* matches = [re matchesInString: authHeader options: 0 311 | range: NSMakeRange(0, authHeader.length)]; 312 | NSTextCheckingResult* groups = matches.firstObject; 313 | if (groups) { 314 | NSString* key = [authHeader substringWithRange: [groups rangeAtIndex: 2]]; 315 | NSRange k = [groups rangeAtIndex: 4]; 316 | if (k.length == 0) 317 | k = [groups rangeAtIndex: 5]; 318 | challenge[key] = [authHeader substringWithRange: k]; 319 | challenge[@"Scheme"] = [authHeader substringWithRange: [groups rangeAtIndex: 1]]; 320 | } 321 | challenge[@"WWW-Authenticate"] = authHeader; 322 | return challenge; 323 | } 324 | 325 | 326 | - (void) setErrorCode: (NSInteger)code userInfo: (NSDictionary*)userInfo { 327 | NSMutableDictionary* info = $mdict({NSURLErrorFailingURLErrorKey, self.URL}); 328 | if (userInfo) 329 | [info addEntriesFromDictionary: userInfo]; 330 | _error = [NSError errorWithDomain: NSURLErrorDomain code: code userInfo: info]; 331 | } 332 | 333 | 334 | static NSString* getHeader(CFHTTPMessageRef message, NSString* header) { 335 | return CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(message, 336 | (__bridge CFStringRef)header)); 337 | } 338 | 339 | 340 | @end 341 | -------------------------------------------------------------------------------- /Docs/BLIP Protocol v1.md: -------------------------------------------------------------------------------- 1 | !logo.png! 2 | 3 | # The BLIP Protocol 4 | 5 | **Version 1.1** 6 | 7 | By [Jens Alfke](mailto:jens@mooseyard.com) (2008) 8 | 9 | ## 1. Messages 10 | 11 | The BLIP protocol runs over a bidirectional stream, typically a TCP socket, and allows the peers on either end to send **messages** back and forth. The two types of messages are called **requests** and **responses**. Either peer may send a request at any time, to which the other peer will send back a response (unless the request has a special flag that indicates that it doesn't need a response.) 12 | 13 | Messages have a structure similar to HTTP entities. Every message (request or response) has a **body**, and zero or more **properties**, or key-value pairs. The body is an uninterpreted sequence of bytes, up to 2^32-1 bytes long. Property keys and values must be UTF-8 strings, and the total size of properties cannot exceed 64k bytes. 14 | 15 | Every message has a **request Number:** Requests are numbered sequentially, starting from 1 when the connection opens. Each peer has its own independent sequence for numbering the requests that it sends. Each response is given the number of the corresponding request. 16 | 17 | ### 1.1. Message Flags 18 | 19 | Every request or response message has a set of flags that can be set by the application when it's created: 20 | 21 | * **Compressed:** If this flag is set, the body of the message (but not the property data!) will be compressed in transit via the gzip algorithm. 22 | * **Urgent:** The implementation will attempt to expedite delivery of messages with this flag, by allocating them a greater share of the available bandwidth (but not to the extent of completely starving non-urgent messages.) 23 | * **No-Reply**: This request does not need or expect a response. (This flag has no meaning in a response.) 24 | * **Meta**: This flag indicates a message intended for internal use by the peer's BLIP implementation, and should not be delivered directly to the client application. (An example is the "bye" request used to negotiate closing the connection.) 25 | 26 | ### 1.2. Error Replies 27 | 28 | A reply can indicate an error, at either the BLIP level (i.e. couldn't deliver the request to the recipient application) or the application level. In an error reply, the message properties provide information about the error: 29 | 30 | * The "Error-Code" property's value is a decimal integer expressed in ASCII, in the range of a signed 32-bit integer. 31 | * The "Error-Domain" property's value is a string denoting a domain in which the error code should be interpreted. If missing, its default value is "BLIP". 32 | * Other properties may provide additional data about the error; applications can define their own schema for this. 33 | 34 | The "BLIP" error domain uses the HTTP status codes, insofar as they make sense, as its error codes. The ones used thus far are: 35 | 36 | ``` 37 | BadRequest = 400, 38 | Forbidden = 403, 39 | NotFound = 404, 40 | BadRange = 416, 41 | HandlerFailed = 501, 42 | Unspecified = 599 43 | ``` 44 | 45 | Other error domains are application-specific, undefined by the protocol itself. 46 | 47 | (**Note:** The Objective-C implementation encodes Foundation framework NSErrors into error responses by storing the NSError's code and domain as the BLIP Error-Code and Error-Domain properties, and adding the contents of the NSError's userInfo dictionary as additional properties. When receiving an error response it decodes an NSError in the same way. This behavior is of course platform-specific, and is a convenience not mandated by the protocol.) 48 | 49 | ## 2. Message Delivery 50 | 51 | Messages are **multiplexed** over the connection, so several may be in transit at once. This ensures that a long message doesn't block the delivery of others. It does mean that, on the receiving end, messages will not necessarily be _completed_ in order: if request number 3 is longer than average, then requests 4 and 6 might finish before it does and be delivered to the application first. 52 | 53 | However, BLIP does guarantee that requests are _begun_ in order: the receiving peer will always get the first **frame** (chunk of bytes) of request number 3 before the first frame of any higher-numbered request. 54 | 55 | The two peers can each send messages at whatever pace they wish. They don't have to take turns. Either peer can send a request whenever it wants. 56 | 57 | Every incoming request must be responded to unless its "no-reply" flag is set. However, requests do not need to be responded to in order, and there's no built-in constraint on how long a peer can take to respond. If a peer has nothing meaningful to send back, but must respond, it can send back an empty response (with no properties and zero-length body.) 58 | 59 | ## 3. Protocol Details 60 | 61 | ### 3.1. The Connection 62 | 63 | The connection between the two peers is opened in the normal way for the underlying protocol (i.e. TCP); BLIP doesn't specify how this happens. 64 | 65 | BLIP can run over SSL/TLS. There is no in-band negotiation of whether to use SSL (unlike [[BLIP/BEEP|BEEP]]), so the application code is responsible for telling the BLIP implementation whether or not to use it before the connection opens. Once the SSL handshake, and certificate authentication, complete, BLIP begins just as if it had just opened a regular unencrypted connection. 66 | 67 | There are currently no greetings or other preliminaries sent when the connection opens. Either peer (or both peers) just start sending messages when ready. [A special greeting message may be defined in the future.] 68 | 69 | ### 3.2. Closing The Connection 70 | 71 | To initiate closing the connection, the peer does the following: 72 | 73 | 1. It sends a special request, with the "meta" flag set and the "Profile" property set to the string "Bye". 74 | 2. It waits for a response. While waiting it must not send any further requests, although it must continue sending frames of requests that are already being sent, and must send responses to any incoming requests. 75 | 3. Upon receiving a response to the "bye" request, if the response contains an error, the attempt to close has failed (most likely because the other peer refused) and the peer should return to the normal open state. If the close request was initiated by client code, it should notify the client of the failure. 76 | 4. Otherwise, if the response was successful, the peer must wait until all of its outgoing responses have been completely sent, and until all of its requests have received a complete response. It can then close the socket. 77 | 78 | The protocol for receiving a close request is similar: 79 | 80 | 1. The peer decides whether to accept or refuse the request, probably by asking the client code. If it accepts, it sends back an empty response. If it refuses, it sends back an error response (403 Forbidden is the default error code) and remains in the regular "open" state. 81 | 2. After accepting the close, the peer goes into the same waiting state as in step 4 above. It must not send any new requests, although it must continue sending any partially-sent messages and must reply to responses; and it must wait until all responses are sent and all requests have been responded to before closing the socket. 82 | 83 | Note that it's possible for both peers to decide to close the connection simultaneously, which means their "bye" requests will "cross in the mail". They should handle this gracefully. If a peer has sent a "bye" request and receives one from the other peer, it should respond affirmatively and continue waiting for its reply. 84 | 85 | Note also that both peers are likely to close the socket at almost the same time, since each will be waiting for the final frames to be sent/received. This means that if a peer receives an EOF on the socket, it should check whether it's already ready to close the socket itself (i.e. it's exchanged "bye"s and has no pending frames to send or receive); if so, it should treat the EOF as a normal close, just as if it had closed the socket itself. (Otherwise, of course, the EOF is unexpected and should be treated as a fatal error.) 86 | 87 | ### 3.3. Sending Messages 88 | 89 | Outgoing messages are multiplexed over the peer's output stream, so that multiple large messages may be sent at once. Each message is encoded as binary data (including compression of the body, if desired) and that data is broken into a sequence of **frames**. Frames must be less than 64k, and are typically 4k. The multiplexer then repeatedly chooses a message that's ready to send, and sends its next frame over the output stream. The algorithm works like this: 90 | 91 | 1. When the application submits a new message to be sent, the BLIP implementation assigns it a number: if it's a request it gets the next available request number, and if it's a response it gets the number of its corresponding request. It then puts the message into the out-box queue. 92 | 2. When the output stream is ready to send data, the BLIP implementation pops the first message from the head of the out-box and removes its next frame. 93 | 3. If the message has more frames remaining after this one, a **more-coming** flag is set in the frame's header, and the message is placed back into the out-box queue. 94 | 4. The frame is sent over the output stream. 95 | 96 | Normal messages are always placed into the queue at the tail end, which results in round-robin scheduling. Urgent messages follow a more complex rule: 97 | 98 | * An urgent message is placed after the last other urgent message in the queue. 99 | * If there are one or more normal messages after that one, the message is inserted after the _first_ normal message (this prevents normal messages from being starved and never reaching the head of the queue.) Or if there are no urgent messages in the queue, the message is placed after the first normal message. If there are no messages at all, then there's only one place to put the message, of course. 100 | * When a newly-ready urgent message is being added to the queue for the _first time_ (in step 1 above), it has the additional restriction that it must go _after_ any other message that has not yet had any of its frames sent. (This is so that messages are begun in sequential order; otherwise the first frame of urgent message number 10 might be sent before the first frame of regular message number 8, for example.) 101 | 102 | ### 3.4. Receiving Messages 103 | 104 | The receiver simply reads the frames one at a time from the input stream and uses their message types and request numbers to group them together into messages. 105 | 106 | When the current frame does not have its more-coming flag set, that message is complete. Its properties are decoded, its body is decompressed if necessary, and the message is delivered to the application. 107 | 108 | ### 3.5. Message Encoding 109 | 110 | A message is encoded into binary data, prior to being broken into frames, as follows: 111 | 112 | 1. The properties are written out in pairs as alternating key and value strings. Each string is in C format: UTF-8 characters ending with a NUL byte. There is no padding. 113 | 2. Certain common strings are abbreviated using a hardcoded dictionary. The abbreviations are strings consisting of a single control character (the ascii value of the character is the index of the string in the dictionary, starting at 1.) The current dictionary can be found in BLIPProperties.m in the reference implementation. 114 | 3. The total length in bytes of the encoded properties is prepended to the property data as a 16-bit integer in network byte order (big-endian). **Important Note:** If there are no properties, the length (zero) still needs to be written! 115 | 4. If the message's "compressed" flag is set, the body is compressed using the gzip "deflate" algorithm. 116 | 5. The body is appended to the property data. 117 | 118 | ### 3.6. Framing 119 | 120 | Frames — chunks of messages — are what is actually written to the stream. Each frame needs a header to identify it to the reader. The header is a fixed 12 bytes long and consists of the following fields, each in network byte order (big-endian): 121 | 122 | ``` 123 | [4 bytes] Magic Number 124 | [4 bytes] Request Number 125 | [2 bytes] Flags 126 | [2 bytes] Frame Size 127 | ``` 128 | 129 | The Magic Number is a fixed constant defined as hexadecimal `9B34F206` [changed from the 9B34F205 used in protocol version 1!]. 130 | The Request Number is the serial number of the request, as described above. 131 | The Flags are the message flags, plus a frame-level "more-coming" flag, as described above. 132 | The Frame Size is the total size in bytes of the frame, _including the header_. 133 | 134 | The flags are defined as follows: 135 | ``` 136 | TypeMask = 0x000F 137 | Compressed= 0x0010 138 | Urgent = 0x0020 139 | NoReply = 0x0040 140 | MoreComing= 0x0080 141 | Meta = 0x0100 142 | ``` 143 | 144 | The TypeMask is actually a 4-bit integer, not a flag. Of the 16 possible message types, the ones currently defined are 0 for a request, 1 for a reply, and 2 for an error reply. [The error-reply type is likely to disappear in the future, though.] 145 | 146 | The frame data follows after the header, of course. There is no trailer; each frame's header follows right after the previous frame's data. 147 | 148 | ### 3.7. Protocol Error Handling 149 | 150 | Many types of errors could be found in the incoming data stream while the receiver is parsing it. Some errors are fatal, and the peer should respond by immediately closing the connection. Other errors, called frame errors, can be handled by ignoring the frame and going on to the next. 151 | 152 | Fatal errors are: 153 | 154 | * Unexpected EOF on input stream (i.e. in mid-frame) 155 | * Wrong frame magic number 156 | * Frame Size value less than 12 157 | 158 | Frame errors are: 159 | 160 | * Unknown message type (neither request nor response) 161 | * Request number refers to an already-completed request or response (i.e. a prior frame with this number had its "more-coming" flag set to false) 162 | * A property string contains invalid UTF-8 163 | * The property data's length field is longer than the remaining frame data 164 | * The property data, if non-empty, does not end with a NUL byte 165 | * The body of a compressed frame fails to decompress 166 | 167 | Note that it is _not_ an error if: 168 | 169 | * Undefined flag bits are set (except for the ones that encode the message type). These bits can be ignored. 170 | * Property keys are not recognized by the application (BLIP itself doesn't care what the property keys mean. It's up to the application to decide what to do about such properties.) 171 | -------------------------------------------------------------------------------- /BLIP/BLIPMessage.m: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPMessage.m 3 | // BLIP 4 | // 5 | // Created by Jens Alfke on 5/10/08. 6 | // Copyright 2008-2013 Jens Alfke. 7 | // Copyright (c) 2013-2015 Couchbase, Inc. All rights reserved. 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 10 | // except in compliance with the License. You may obtain a copy of the License at 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // Unless required by applicable law or agreed to in writing, software distributed under the 13 | // License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 14 | // either express or implied. See the License for the specific language governing permissions 15 | // and limitations under the License. 16 | 17 | #import "BLIPMessage.h" 18 | #import "BLIPConnection.h" 19 | #import "BLIP_Internal.h" 20 | 21 | #import "ExceptionUtils.h" 22 | #import "MYData.h" 23 | #import "MYBuffer+Zip.h" 24 | 25 | 26 | #define kMaxUnackedBytes 128000 27 | #define kAckByteInterval 50000 28 | 29 | 30 | NSString* const BLIPErrorDomain = @"BLIP"; 31 | 32 | NSError *BLIPMakeError( int errorCode, NSString *message, ... ) { 33 | va_list args; 34 | va_start(args,message); 35 | message = [[NSString alloc] initWithFormat: message arguments: args]; 36 | va_end(args); 37 | LogTo(BLIP,@"BLIPError #%i: %@",errorCode,message); 38 | NSDictionary *userInfo = @{NSLocalizedDescriptionKey: message}; 39 | return [NSError errorWithDomain: BLIPErrorDomain code: errorCode userInfo: userInfo]; 40 | } 41 | 42 | 43 | @implementation BLIPMessage 44 | 45 | 46 | @synthesize onDataReceived=_onDataReceived, onDataSent=_onDataSent, onSent=_onSent; 47 | 48 | 49 | - (instancetype) _initWithConnection: (BLIPConnection*)connection 50 | isMine: (BOOL)isMine 51 | flags: (BLIPMessageFlags)flags 52 | number: (uint32_t)msgNo 53 | body: (NSData*)body 54 | { 55 | self = [super init]; 56 | if (self != nil) { 57 | _connection = connection; 58 | _isMine = isMine; 59 | _isMutable = isMine; 60 | _flags = flags; 61 | _number = msgNo; 62 | if (isMine) { 63 | _body = body.copy; 64 | _properties = [[NSMutableDictionary alloc] init]; 65 | _propertiesAvailable = YES; 66 | _complete = YES; 67 | } else { 68 | Assert(!body); 69 | } 70 | LogTo(BLIPLifecycle,@"INIT %@",self); 71 | } 72 | return self; 73 | } 74 | 75 | #if DEBUG 76 | - (void) dealloc { 77 | LogTo(BLIPLifecycle,@"DEALLOC %@",self); 78 | } 79 | #endif 80 | 81 | 82 | - (NSString*) description { 83 | NSUInteger length = (_body.length ?: _mutableBody.length) ?: _encodedBody.minLength; 84 | NSMutableString *desc = [NSMutableString stringWithFormat: @"%@[#%u%s, %lu bytes", 85 | self.class, 86 | (unsigned int)_number, 87 | (_isMine ? "->" : "<-"), 88 | (unsigned long)length]; 89 | if (_flags & kBLIP_Compressed) { 90 | if (_encodedBody && _encodedBody.minLength != length) 91 | [desc appendFormat: @" (%lu gzipped)", (unsigned long)_encodedBody.minLength]; 92 | else 93 | [desc appendString: @", gzipped"]; 94 | } 95 | if (_bodyStreams.count > 0) 96 | [desc appendString: @" +stream"]; 97 | if (_flags & kBLIP_Urgent) 98 | [desc appendString: @", urgent"]; 99 | if (_flags & kBLIP_NoReply) 100 | [desc appendString: @", noreply"]; 101 | if (_flags & kBLIP_Meta) 102 | [desc appendString: @", META"]; 103 | if (_flags & kBLIP_MoreComing) 104 | [desc appendString: @", incomplete"]; 105 | [desc appendString: @"]"]; 106 | return desc; 107 | } 108 | 109 | - (NSString*) descriptionWithProperties { 110 | NSMutableString *desc = (NSMutableString*)self.description; 111 | [desc appendFormat: @" %@", self.properties]; 112 | return desc; 113 | } 114 | 115 | 116 | #pragma mark - 117 | #pragma mark PROPERTIES & METADATA: 118 | 119 | 120 | @synthesize connection=_connection, number=_number, isMine=_isMine, isMutable=_isMutable, 121 | _bytesWritten, sent=_sent, propertiesAvailable=_propertiesAvailable, complete=_complete, 122 | representedObject=_representedObject; 123 | 124 | 125 | - (void) _setFlag: (BLIPMessageFlags)flag value: (BOOL)value { 126 | Assert(_isMine && _isMutable); 127 | if (value) 128 | _flags |= flag; 129 | else 130 | _flags &= ~flag; 131 | } 132 | 133 | - (BLIPMessageFlags) _flags {return _flags;} 134 | 135 | - (BOOL) isRequest {return (_flags & kBLIP_TypeMask) == kBLIP_MSG;} 136 | - (BOOL) compressed {return (_flags & kBLIP_Compressed) != 0;} 137 | - (BOOL) urgent {return (_flags & kBLIP_Urgent) != 0;} 138 | - (void) setCompressed: (BOOL)compressed {[self _setFlag: kBLIP_Compressed value: compressed];} 139 | - (void) setUrgent: (BOOL)high {[self _setFlag: kBLIP_Urgent value: high];} 140 | 141 | 142 | - (NSData*) body { 143 | if (! _body && _isMine) 144 | return [_mutableBody copy]; 145 | else 146 | return _body; 147 | } 148 | 149 | - (void) setBody: (NSData*)body { 150 | Assert(_isMine && _isMutable); 151 | if (_mutableBody) 152 | [_mutableBody setData: body]; 153 | else 154 | _mutableBody = [body mutableCopy]; 155 | } 156 | 157 | - (void) _addToBody: (NSData*)data { 158 | if (data.length) { 159 | if (_mutableBody) 160 | [_mutableBody appendData: data]; 161 | else 162 | _mutableBody = [data mutableCopy]; 163 | _body = nil; 164 | } 165 | } 166 | 167 | - (void) addToBody: (NSData*)data { 168 | Assert(_isMine && _isMutable); 169 | [self _addToBody: data]; 170 | } 171 | 172 | - (void) addStreamToBody:(NSInputStream *)stream { 173 | if (!_bodyStreams) 174 | _bodyStreams = [NSMutableArray new]; 175 | [_bodyStreams addObject: stream]; 176 | } 177 | 178 | 179 | - (NSString*) bodyString { 180 | NSData *body = self.body; 181 | if (body) 182 | return [[NSString alloc] initWithData: body encoding: NSUTF8StringEncoding]; 183 | else 184 | return nil; 185 | } 186 | 187 | - (void) setBodyString: (NSString*)string { 188 | self.body = [string dataUsingEncoding: NSUTF8StringEncoding]; 189 | self.contentType = @"text/plain; charset=UTF-8"; 190 | } 191 | 192 | 193 | - (id) bodyJSON { 194 | NSData* body = self.body; 195 | if (body.length == 0) 196 | return nil; 197 | NSError* error; 198 | id jsonObj = [NSJSONSerialization JSONObjectWithData: body 199 | options: NSJSONReadingAllowFragments 200 | error: &error]; 201 | if (!jsonObj) 202 | Warn(@"Couldn't parse %@ body as JSON: %@", self, error.my_compactDescription); 203 | return jsonObj; 204 | } 205 | 206 | 207 | - (void) setBodyJSON: (id)jsonObj { 208 | NSError* error; 209 | NSData* body = [NSJSONSerialization dataWithJSONObject: jsonObj options: 0 error: &error]; 210 | Assert(body, @"Couldn't encode as JSON: %@", error.my_compactDescription); 211 | self.body = body; 212 | self.contentType = @"application/json"; 213 | self.compressed = (body.length > 100); 214 | } 215 | 216 | 217 | - (NSDictionary*) properties { 218 | return _properties; 219 | } 220 | 221 | - (NSMutableDictionary*) mutableProperties { 222 | Assert(_isMine && _isMutable); 223 | return (NSMutableDictionary*)_properties; 224 | } 225 | 226 | - (NSString*) objectForKeyedSubscript: (NSString*)key { 227 | return _properties[key]; 228 | } 229 | 230 | - (void) setObject: (NSString*)value forKeyedSubscript:(NSString*)key { 231 | [self.mutableProperties setValue: value forKey: key]; 232 | } 233 | 234 | 235 | - (NSString*) contentType {return self[@"Content-Type"];} 236 | - (void) setContentType: (NSString*)t {self[@"Content-Type"] = t;} 237 | - (NSString*) profile {return self[@"Profile"];} 238 | - (void) setProfile: (NSString*)p {self[@"Profile"] = p;} 239 | 240 | 241 | #pragma mark - 242 | #pragma mark I/O: 243 | 244 | 245 | - (void) _encode { 246 | Assert(_isMine && _isMutable); 247 | _isMutable = NO; 248 | _properties = [_properties copy]; 249 | 250 | // _encodedBody and _outgoing do not include the properties 251 | _encodedBody = [[MYBuffer alloc] init]; 252 | [_encodedBody writeData: (_body ?: _mutableBody)]; 253 | for (NSInputStream* stream in _bodyStreams) 254 | [_encodedBody writeContentsOfStream: stream]; 255 | _bodyStreams = nil; 256 | if (self.compressed) 257 | _outgoing = [[MYZipReader alloc] initWithReader: _encodedBody compressing: YES]; 258 | else 259 | _outgoing = _encodedBody; 260 | } 261 | 262 | 263 | - (void) _assignedNumber: (uint32_t)number { 264 | Assert(_number==0,@"%@ has already been sent",self); 265 | _number = number; 266 | _isMutable = NO; 267 | } 268 | 269 | 270 | // Generates the next outgoing frame. 271 | - (NSData*) nextFrameWithMaxSize: (uint16_t)maxSize moreComing: (BOOL*)outMoreComing { 272 | Assert(_number!=0); 273 | Assert(_isMine); 274 | Assert(_outgoing); 275 | *outMoreComing = NO; 276 | if (_bytesWritten==0) 277 | LogTo(BLIP,@"Now sending %@",self); 278 | size_t headerSize = MYLengthOfVarUInt(_number) + MYLengthOfVarUInt(_flags); 279 | 280 | NSMutableData* frame = [NSMutableData dataWithCapacity: maxSize]; 281 | frame.length = headerSize; 282 | int64_t prevBytesWritten = _bytesWritten; 283 | if (_bytesWritten == 0) { 284 | // First frame: always write entire properties: 285 | NSData* propertyData = BLIPEncodeProperties(_properties); 286 | [frame appendData: propertyData]; 287 | _bytesWritten += propertyData.length; 288 | } 289 | 290 | // Now read from payload: 291 | ssize_t frameLen = frame.length; 292 | if (frameLen < maxSize) { 293 | frame.length = maxSize; 294 | ssize_t bytesRead = [_outgoing readBytes: (uint8_t*)frame.mutableBytes + frameLen 295 | maxLength: maxSize - frameLen]; 296 | if (bytesRead < 0) { 297 | // Yikes! Couldn't read message content. Abort. 298 | Warn(@"Unable to send %@: Couldn't read body from stream", self); 299 | if (_onDataSent) 300 | _onDataSent(self, 0); 301 | self.complete = YES; 302 | return nil; 303 | } 304 | frame.length = frameLen + bytesRead; 305 | _bytesWritten += bytesRead; 306 | } 307 | 308 | // Write the header at the start of the frame: 309 | if (_outgoing.atEnd) { 310 | _flags &= ~kBLIP_MoreComing; 311 | _outgoing = nil; 312 | } else { 313 | _flags |= kBLIP_MoreComing; 314 | *outMoreComing = YES; 315 | } 316 | void* pos = MYEncodeVarUInt(frame.mutableBytes, _number); 317 | MYEncodeVarUInt(pos, _flags); 318 | 319 | LogVerbose(BLIP,@"%@ pushing frame, bytes %lu-%lu%@", self, 320 | (unsigned long)prevBytesWritten, (unsigned long)_bytesWritten, 321 | (*outMoreComing ? @"" : @" (finished)")); 322 | if (_onDataSent) 323 | _onDataSent(self, _bytesWritten); 324 | if (!*outMoreComing) 325 | self.complete = YES; 326 | return frame; 327 | } 328 | 329 | 330 | - (BOOL) _needsAckToContinue { 331 | Assert(_isMine); 332 | return _bytesWritten - _bytesReceived >= kMaxUnackedBytes; 333 | } 334 | 335 | 336 | - (BOOL) _receivedAck: (uint64_t)bytesReceived { 337 | Assert(_isMine); 338 | if (bytesReceived <= _bytesReceived || bytesReceived > _bytesWritten) 339 | return NO; 340 | _bytesReceived = bytesReceived; 341 | return YES; 342 | } 343 | 344 | 345 | // Parses the next incoming frame. 346 | - (BOOL) _receivedFrameWithFlags: (BLIPMessageFlags)flags body: (NSData*)frameBody { 347 | LogVerbose(BLIP,@"%@ rcvd bytes %lu-%lu, flags=%x", 348 | self, (unsigned long)_bytesReceived, (unsigned long)_bytesReceived+frameBody.length, flags); 349 | Assert(!_isMine); 350 | Assert(_flags & kBLIP_MoreComing); 351 | 352 | if (!self.isRequest) 353 | _flags = flags | kBLIP_MoreComing; 354 | 355 | int64_t oldBytesReceived = _bytesReceived; 356 | _bytesReceived += frameBody.length; 357 | BOOL shouldAck = (flags & kBLIP_MoreComing) 358 | && oldBytesReceived > 0 359 | && (oldBytesReceived / kAckByteInterval) < (_bytesReceived / kAckByteInterval); 360 | 361 | if (!_incoming) 362 | _incoming = _encodedBody = [[MYBuffer alloc] init]; 363 | if (![_incoming writeData: frameBody]) 364 | return NO; 365 | 366 | if (! _properties) { 367 | // Try to extract the properties: 368 | BOOL complete; 369 | _properties = BLIPReadPropertiesFromBuffer(_encodedBody, &complete); 370 | if (_properties) { 371 | if (flags & kBLIP_Compressed) { 372 | // Now that properties are read, enable decompression for the rest of the stream: 373 | _flags |= kBLIP_Compressed; 374 | NSData* restOfFrame = _encodedBody.flattened; 375 | _encodedBody = [[MYBuffer alloc] init]; 376 | _incoming = [[MYZipWriter alloc] initWithWriter: _encodedBody compressing:NO]; 377 | if (restOfFrame.length > 0 && ![_incoming writeData: restOfFrame]) 378 | return NO; 379 | } 380 | self.propertiesAvailable = YES; 381 | [_connection _messageReceivedProperties: self]; 382 | } else if (complete) { 383 | return NO; 384 | } 385 | } 386 | 387 | if (_properties && _onDataReceived) { 388 | LogVerbose(BLIP, @"%@ -> calling onDataReceived(%lu bytes)", 389 | self, (unsigned long)_encodedBody.maxLength); 390 | _onDataReceived(self, _encodedBody); 391 | } 392 | 393 | if (! (flags & kBLIP_MoreComing)) { 394 | // End of message: 395 | _flags &= ~kBLIP_MoreComing; 396 | if (! _properties) 397 | return NO; 398 | _body = _encodedBody.flattened; 399 | _encodedBody = nil; 400 | _incoming = nil; 401 | _onDataReceived = nil; 402 | self.complete = YES; 403 | } 404 | 405 | if (shouldAck) 406 | [_connection _sendAckWithNumber: _number isRequest: self.isRequest 407 | bytesReceived: _bytesReceived]; 408 | 409 | return YES; 410 | } 411 | 412 | 413 | - (void) _connectionClosed { 414 | if (_isMine) { 415 | _bytesWritten = 0; 416 | _flags |= kBLIP_MoreComing; 417 | } 418 | } 419 | 420 | 421 | @end 422 | -------------------------------------------------------------------------------- /Docs/BLIP Protocol.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # The BLIP Protocol 4 | 5 | **Version 2.0.1** 6 | 7 | By [Jens Alfke](mailto:jens@mooseyard.com) (January 2017) 8 | 9 | v2.0.1: No protocol changes; mostly just cleanup and extra details, especially about the WebSocket encoding. (It does add a confession that the "Bye" protocol for closing connections isn't actually implemented or honored by current implementations.) 10 | v2.0: Major update that bases the protocol on an underlying message transport (usually WebSockets), simplifies the header, and uses varint encodings. 11 | 12 | ## 1. Messages 13 | 14 | The BLIP protocol runs over a bidirectional network connection and allows the peers on either end to send **messages** back and forth. The two types of messages are called **requests** and **responses**. Either peer may send a request at any time, to which the other peer will send back a response (unless the request has a special flag that indicates that it doesn't need a response.) 15 | 16 | Messages are **multiplexed** in transit, so any number may be sent (or received) simultaneously. This differs from protocols like HTTP 1.x and WebSockets where a message blocks the stream until it's completely sent. 17 | 18 | Messages have a structure similar to HTTP entities. Every message (request or response) has a **body**, and zero or more **properties**, or key-value pairs. The body is an uninterpreted sequence of bytes. Property keys and values must be UTF-8 strings. The only length constraint is that the encoded message-plus-properties cannot exceed 2^64-1 bytes. 19 | 20 | Every message has a **request number:** Requests are numbered sequentially, starting from 1 when the connection opens. Each peer has its own independent sequence for numbering the requests that it sends. Each response is given the number of the corresponding request. 21 | 22 | Version 2 of BLIP is typically layered atop the WebSocket protocol. The details of this are presented later. (Version 1 included its own framing protocol and talked directly to a TCP socket.) 23 | 24 | ### 1.1. Message Flags 25 | 26 | Every request or response message has a set of flags that can be set by the application when it's created: 27 | 28 | * **Compressed:** If this flag is set, the body of the message (but not the property data!) will be compressed in transit via the gzip algorithm. 29 | * **Urgent:** The implementation will attempt to expedite delivery of messages with this flag, by allocating them a greater share of the available bandwidth (but not to the extent of completely starving non-urgent messages.) 30 | * **No-Reply**: This request does not need or expect a response. (This flag has no meaning in a response.) 31 | * **Meta**: This flag indicates a message intended for internal use by the peer's BLIP implementation, and should not be delivered directly to the client application. (An example is the "bye" request used to negotiate closing the connection.) 32 | 33 | ### 1.2. Error Replies 34 | 35 | A reply can indicate an error, at either the BLIP level (i.e. couldn't deliver the request to the recipient application) or the application level. In an error reply, the message properties provide structured information about the error: 36 | 37 | * The "Error-Code" property's value is a decimal integer expressed in ASCII, in the range of a signed 32-bit integer. 38 | * The "Error-Domain" property's value is a string denoting a domain in which the error code should be interpreted. If missing, its default value is "BLIP". 39 | * Other properties may provide additional data about the error; applications can define their own schema for this. 40 | * The message body, if non-empty, contains an error message in UTF-8 format. 41 | 42 | The "BLIP" error domain uses the HTTP status codes, insofar as they make sense, as its error codes. The ones used thus far are: 43 | 44 | ``` 45 | BadRequest = 400, 46 | Forbidden = 403, 47 | NotFound = 404, 48 | BadRange = 416, 49 | HandlerFailed = 501, 50 | Unspecified = 599 51 | ``` 52 | 53 | Other error domains are application-specific, undefined by the protocol itself. 54 | 55 | >**Note:** The Objective-C implementation encodes Foundation framework NSErrors into error responses by storing the NSError's code and domain as the BLIP Error-Code and Error-Domain properties, and adding the contents of the NSError's userInfo dictionary as additional properties. When receiving an error response it decodes an NSError in the same way. This behavior is of course platform-specific, and is a convenience not mandated by the protocol.) 56 | 57 | ## 2. Message Delivery 58 | 59 | Messages are **multiplexed** over the connection, so several may be in transit at once. This ensures that a long message doesn't block the delivery of others. It does mean that, on the receiving end, messages will not necessarily be _completed_ in order: if request number 3 is longer than average, then requests 4 and 6 might finish before it does and be delivered to the application first. 60 | 61 | However, BLIP does guarantee that requests are _begun_ in order: the receiving peer will always get the first **frame** (chunk of bytes) of request number 3 before the first frame of any higher-numbered request. 62 | 63 | The two peers can each send messages at whatever pace they wish. They don't have to take turns. Either peer can send a request whenever it wants. 64 | 65 | Every incoming request must be responded to unless its "no-reply" flag is set. However, requests do not need to be responded to in order, and there's no built-in constraint on how long a peer can take to respond. If a peer has nothing meaningful to send back, but must respond, it can send back an empty response (with no properties and zero-length body.) 66 | 67 | ## 3. Protocol Details 68 | 69 | ### 3.1. The Transport 70 | 71 | BLIP 2 is layered atop a message-based protocol called the **transport**, typically WebSockets. The details of this protocol are unimportant except that it must: 72 | 73 | * Connect two peers (multicast is not supported) 74 | * Deliver arbitrary-sized binary blobs (called **frames** in this document) in either direction 75 | * Deliver these frames _reliably_ and _in order_ 76 | 77 | The transport connection between the two peers is opened in the normal way for the underlying protocol (i.e. WebSockets' HTTP-based handshake, optionally over SSL); BLIP doesn't specify how this happens. 78 | 79 | There are currently no greetings or other preliminaries sent when the connection opens. Either peer (or both peers) just start sending messages when ready. [A special greeting message may be defined in the future.] 80 | 81 | #### 3.2.1. WebSocket transport details 82 | 83 | * A BLIP client opening a connection MUST request the WebSocket [subprotocol](https://hpbn.co/websocket/#subprotocol-negotiation) `BLIP`. A server supporting BLIP also MUST advertise support for this subprotocol. If one side supports BLIP but the other doesn't, BLIP messages cannot be sent; either side can close the connection or downgrade to some other WebSocket based schema. 84 | * BLIP messages are sent in binary WebSocket messages; text messages are not used, and receiving one is a fatal connection error. 85 | * Both BLIP and WebSocket use the terminology "messages" and "frames", where messages can be broken into sequences of frames. Try not to get them confused! A BLIP frame corresponds to (is sent as) a WebSocket message. 86 | 87 | ### 3.2. Closing The Connection 88 | 89 | >**Note:** Current implementations don't actually follow this protocol for closing connections. It would be a good idea to; it just hasn't come up as necessary yet. 90 | 91 | To initiate closing the connection, the peer does the following: 92 | 93 | 1. It sends a special request, with the "meta" flag set and the "Profile" property set to the string "Bye". 94 | 2. It waits for a response. While waiting it must not send any further requests, although it must continue sending frames of requests that are already being sent, and must send responses to any incoming requests. 95 | 3. Upon receiving a response to the "bye" request, if the response contains an error, the attempt to close has failed (most likely because the other peer refused) and the peer should return to the normal open state. If the close request was initiated by client code, it should notify the client of the failure. 96 | 4. Otherwise, if the response was successful, the peer must wait until all of its outgoing responses have been completely sent, and until all of its requests have received a complete response. It can then close the socket. 97 | 98 | The protocol for receiving a close request is similar: 99 | 100 | 1. The peer decides whether to accept or refuse the request, probably by asking the client code. If it accepts, it sends back an empty response. If it refuses, it sends back an error response (403 Forbidden is the default error code) and remains in the regular "open" state. 101 | 2. After accepting the close, the peer goes into the same waiting state as in step 4 above. It must not send any new requests, although it must continue sending any partially-sent messages and must reply to responses; and it must wait until all responses are sent and all requests have been responded to before closing the socket. 102 | 103 | Note that it's possible for both peers to decide to close the connection simultaneously, which means their "bye" requests will "cross in the mail". They should handle this gracefully. If a peer has sent a "bye" request and receives one from the other peer, it should respond affirmatively and continue waiting for its reply. 104 | 105 | Note also that both peers are likely to close the socket at almost the same time, since each will be waiting for the final frames to be sent/received. This means that if a peer receives an EOF on the socket, it should check whether it's already ready to close the socket itself (i.e. it's exchanged "bye"s and has no pending frames to send or receive); if so, it should treat the EOF as a normal close, just as if it had closed the socket itself. (Otherwise, of course, the EOF is unexpected and should be treated as a fatal error.) 106 | 107 | ### 3.3. Sending Messages 108 | 109 | Outgoing messages are multiplexed over the peer's transport, so that multiple large messages may be sent at once. Each message is encoded as binary data (including compression of the body, if desired) and that data is broken into a sequence of **frames**, typically either 16k or 4k bytes in size. The multiplexer then repeatedly chooses a message that's ready to send, and sends its next frame over the underlying transport (e.g. as a binary WebSocket message.) The algorithm works like this: 110 | 111 | 1. When the application submits a new message to be sent, the BLIP implementation assigns it a number: if it's a request it gets the next available request number, and if it's a response it gets the number of its corresponding request. It then puts the message into the out-box queue. 112 | 2. When the output stream is ready to send data, the BLIP implementation pops the first message from the head of the out-box and removes its next frame. 113 | 3. If the message has more frames remaining after this one, a **more-coming** flag is set in the frame's header, and the message is placed back into the out-box queue. 114 | 4. The frame is sent across the transport. 115 | 116 | Normal messages are always placed into the queue at the tail end, which results in round-robin scheduling. Urgent messages follow a more complex rule: 117 | 118 | * An urgent message is placed after the last other urgent message in the queue. 119 | * If there are one or more normal messages after that one, the message is inserted after the _first_ normal message (this prevents normal messages from being starved and never reaching the head of the queue.) Or if there are no urgent messages in the queue, the message is placed after the first normal message. If there are no messages at all, then there's only one place to put the message, of course. 120 | * When a newly-ready urgent message is being added to the queue for the _first time_ (in step 1 above), it has the additional restriction that it must go _after_ any other message that has not yet had any of its frames sent. (This is so that messages are begun in sequential order; otherwise the first frame of urgent message number 10 might be sent before the first frame of regular message number 8, for example.) 121 | 122 | ### 3.4. Receiving Messages 123 | 124 | The receiver simply reads the frames one at a time from the input transport and uses their message types and request numbers (sec. 3.6) to group them together into messages. 125 | 126 | >**Note:** Requests (MSG) and responses (RPY) have _independent_ message number sequences, so a MSG frame with number 1 refers to a different message than an RPY frame with number 1. This is because RPY frames use the message numbering of the MSG they reply to. This is important to keep in mind when building a data structure that maps incoming message numbers to message objects! 127 | 128 | When the current frame does not have its more-coming flag set, that message is complete. Its properties are decoded, its body is decompressed if necessary, and the message is delivered to the application. 129 | 130 | ### 3.5. Message Encoding 131 | 132 | A message is encoded into binary data, prior to being broken into frames, as follows: 133 | 134 | 1. First the properties are encoded. 135 | 1. The properties are written out in pairs as alternating key and value strings. Each string is in C format: UTF-8 characters ending with a NUL byte. There is no padding. 136 | 2. Certain common strings are abbreviated using a hardcoded dictionary. The abbreviations are strings consisting of a single control character: the ascii value of the character is the index of the string in the dictionary, starting at 1. (The current dictionary can be found in BLIPProperties.m in the reference implementation.) For example, the string `Profile` is encoded as the single byte `0x01`. 137 | 2. Then the message is encoded: 138 | 1. The encoded message begins with the length in bytes of the encoded properties, as an unsigned **[varint](http://techoverflow.net/blog/2013/01/25/efficiently-encoding-variable-length-integers-in-cc/)**. **Important Note:** If there are no properties, the length (zero) still needs to be written! 139 | 2. After that come the encoded properties (if any). 140 | 3. Then comes the message body. (It doesn't need a delimiter; it ends at the end of the final frame.) 141 | * If the message's "compressed" flag is set, the body (but not properties) is compressed using the gzip "deflate" algorithm. 142 | 143 | ### 3.6. Framing 144 | 145 | Frames — chunks of messages — are what is actually sent to the transport. Each frame needs a header to identify it to the reader. The header consists of the _request number_ and the _frame flags_, each encoded as an unsigned **[varint](http://techoverflow.net/blog/2013/01/25/efficiently-encoding-variable-length-integers-in-cc/)**. 146 | 147 | >**Note:** The frame flags are currently always written as a single byte. The encoding is defined as a varint to leave room for future expansion. If a flag with value 0x80 or higher is ever defined, then flags may encode to two bytes. 148 | 149 | The Request Number is the serial number of the request, as described above. (A reply frame uses the serial number of the request that it's a reply to.) 150 | 151 | The Flags are as described in section 1.1, plus the more-coming flag described in 3.3. The encoding is defined as follows: 152 | ``` 153 | TypeMask = 0x07 154 | Compressed= 0x04 155 | Urgent = 0x08 156 | NoReply = 0x10 157 | MoreComing= 0x20 158 | Meta = 0x40 159 | ``` 160 | 161 | The `TypeMask` is actually a 3-bit field, not a flag. Of the 8 possible message types, the ones currently defined are: 162 | ``` 163 | MSG = 0x00 164 | RPY = 0x01 165 | ERR = 0x02 166 | ACKMSG = 0x04 167 | ACKRPY = 0x05 168 | ``` 169 | 170 | The frame data follows after the header, of course. 171 | 172 | >**Note:** Properties are encoded at the message level, not the frame level. That means that the first frame of a message -- but _only_ the first frame -- will have the properties' byte-count immediately following its header. In most cases the properties will appear only in the first frame, but if the encoded properties are too long to fit, the remainder might end up in subsequent frames. 173 | 174 | ### 3.7. Flow Control 175 | 176 | Flow control is necessary because different messages can be processed at different rates. A process might be receiving two large messages at once, and the frames of one message are processed more slowly (maybe they're being written to a file.) If the sender sends those frames too fast, the receiver will have to buffer them and its memory usage will keep going up. But the receiver can't just stop reading from the socket, or the other faster message receiver will stop getting data. 177 | 178 | BLIP provides per-message flow control via ACK frames that acknowledge receipt of data from a message. There are two types, ACKMSG and ACKRPY, the only difference being whether they acknowledge a MSG or RPY frame. The content of an ACK frame is a varint representing the total number of payload bytes received of that message so far. 179 | 180 | * A process receiving a multi-frame message (request or reply) should send an ACK frame every time the number of bytes received exceeds a multiple of some byte interval (currently 50000 bytes.) 181 | * A process sending a multi-frame message should stop sending frames of that message whenever the number of unacknowledged bytes (bytes sent minus highest byte count received in an ACK) exceeds a threshold (which is currenly 128000 bytes.) A message suspended this way is removed from the normal queue (sec. 3.3) until an ACK with a sufficiently high byte count is received. 182 | 183 | ### 3.8. Protocol Error Handling 184 | 185 | Many types of errors could be found in the incoming data while the receiver is parsing it. Some errors are fatal, and the peer should respond by immediately closing the connection. Other errors, called frame errors, can be handled by ignoring the frame and going on to the next. 186 | 187 | Fatal errors are: 188 | 189 | * Bad varint encoding -- the frame cuts off in the middle of a multi-byte varint 190 | * Missing header value -- either no flags, or just an empty frame 191 | * Receiving a frame type that isn't used for BLIP, e.g. a non-binary WebSocket message 192 | 193 | Frame errors are: 194 | 195 | * Unknown message type (neither request nor response) 196 | * Request number refers to an already-completed request or response (i.e. a prior frame with this number had its "more-coming" flag set to false) 197 | * A property string contains invalid UTF-8 198 | * The property data's length field is longer than the remaining frame data 199 | * The property data, if non-empty, does not end with a NUL byte 200 | * The body of a compressed frame fails to decompress 201 | 202 | Note that it is _not_ an error if: 203 | 204 | * Undefined flag bits are set (except for the ones that encode the message type). These bits can be ignored. 205 | * Property keys are not recognized by the application (BLIP itself doesn't care what the property keys mean. It's up to the application to decide what to do about such properties.) 206 | -------------------------------------------------------------------------------- /BLIP/BLIPConnection.m: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPConnection.m 3 | // BLIP 4 | // 5 | // Created by Jens Alfke on 4/1/13. 6 | // Copyright (c) 2013-2015 Couchbase, Inc. All rights reserved. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 9 | // except in compliance with the License. You may obtain a copy of the License at 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // Unless required by applicable law or agreed to in writing, software distributed under the 12 | // License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | // either express or implied. See the License for the specific language governing permissions 14 | // and limitations under the License. 15 | 16 | #import "BLIPConnection.h" 17 | #import "BLIPConnection+Transport.h" 18 | #import "BLIPRequest.h" 19 | #import "BLIP_Internal.h" 20 | 21 | #import "ExceptionUtils.h" 22 | #import "MYData.h" 23 | #import 24 | 25 | 26 | DefineLogDomain(BLIP); 27 | DefineLogDomain(BLIPLifecycle); 28 | 29 | 30 | #define kDefaultFrameSize 4096 31 | 32 | 33 | #if DEBUG 34 | static char kQueueSpecificKey = 0; 35 | #define OnTransportQueue() (dispatch_get_specific(&kQueueSpecificKey) == (void*)1) 36 | #else 37 | #define OnTransportQueue() YES 38 | #endif 39 | 40 | 41 | @interface BLIPConnection () 42 | @property (readwrite) BOOL active; 43 | @end 44 | 45 | 46 | @implementation BLIPConnection 47 | { 48 | dispatch_queue_t _transportQueue; 49 | bool _transportIsOpen; 50 | NSError* _error; 51 | __weak id _delegate; 52 | dispatch_queue_t _delegateQueue; 53 | 54 | NSMutableArray *_outBox; // Outgoing messages to be sent 55 | NSMutableArray *_iceBox; // Outgoing messages paused pending acks 56 | BLIPMessage* _sendingMsg; // Message currently being sent (popped from _outBox) 57 | uint32_t _numRequestsSent; 58 | 59 | uint32_t _numRequestsReceived; 60 | NSMutableDictionary *_pendingRequests, *_pendingResponses; // Messages being received 61 | NSUInteger _pendingDelegateCalls; 62 | #if DEBUG 63 | NSUInteger _maxPendingDelegateCalls; 64 | #endif 65 | NSMutableDictionary* _registeredActions; 66 | } 67 | 68 | @synthesize error=_error, dispatchPartialMessages=_dispatchPartialMessages, active=_active; 69 | @synthesize delegate=_delegate, transportQueue=_transportQueue; 70 | 71 | 72 | - (instancetype) initWithTransportQueue: (dispatch_queue_t)transportQueue 73 | isOpen: (BOOL)isOpen 74 | { 75 | Assert(transportQueue); 76 | self = [super init]; 77 | if (self) { 78 | _transportQueue = transportQueue; 79 | _transportIsOpen = isOpen; 80 | #if DEBUG 81 | dispatch_queue_set_specific(_transportQueue, &kQueueSpecificKey, (void*)1, NULL); 82 | #endif 83 | _delegateQueue = dispatch_get_main_queue(); 84 | _pendingRequests = [[NSMutableDictionary alloc] init]; 85 | _pendingResponses = [[NSMutableDictionary alloc] init]; 86 | } 87 | return self; 88 | } 89 | 90 | 91 | // Public API 92 | - (void) setDelegate: (id)delegate 93 | queue: (dispatch_queue_t)delegateQueue 94 | { 95 | Assert(!_delegate, @"Don't change the delegate"); 96 | _delegate = delegate; 97 | _delegateQueue = delegateQueue ?: dispatch_get_main_queue(); 98 | } 99 | 100 | 101 | - (void) _onDelegateQueue: (void (^)())block { 102 | ++_pendingDelegateCalls; 103 | #if DEBUG 104 | if (_pendingDelegateCalls > _maxPendingDelegateCalls) { 105 | LogTo(BLIP, @"New record: %lu pending delegate calls", (unsigned long)_pendingDelegateCalls); 106 | _maxPendingDelegateCalls = _pendingDelegateCalls; 107 | } 108 | #endif 109 | dispatch_async(_delegateQueue, ^{ 110 | block(); 111 | [self _endDelegateCall]; 112 | }); 113 | } 114 | 115 | 116 | - (void) _callDelegate: (SEL)selector block: (void(^)(id))block { 117 | Assert(OnTransportQueue()); 118 | id delegate = _delegate; 119 | if (delegate && [delegate respondsToSelector: selector]) { 120 | [self _onDelegateQueue: ^{ 121 | block(delegate); 122 | }]; 123 | } 124 | } 125 | 126 | - (void) _endDelegateCall { 127 | dispatch_async(_transportQueue, ^{ 128 | if (--_pendingDelegateCalls == 0) 129 | [self updateActive]; 130 | }); 131 | } 132 | 133 | 134 | // Public API 135 | - (NSURL*) URL { 136 | return nil; // Subclasses should override 137 | } 138 | 139 | 140 | - (void) updateActive { 141 | BOOL active = _outBox.count || _iceBox.count || _pendingRequests.count || 142 | _pendingResponses.count || _sendingMsg || _pendingDelegateCalls; 143 | if (active != _active) { 144 | LogVerbose(BLIP, @"%@ active = %@", self, (active ?@"YES" : @"NO")); 145 | self.active = active; 146 | } 147 | } 148 | 149 | 150 | #pragma mark - OPEN/CLOSE: 151 | 152 | 153 | // Public API 154 | - (BOOL) connect: (NSError**)outError { 155 | AssertAbstractMethod(); 156 | } 157 | 158 | // Public API 159 | - (void)close { 160 | AssertAbstractMethod(); 161 | } 162 | 163 | 164 | - (void) _closeWithError: (NSError*)error { 165 | self.error = error; 166 | [self close]; 167 | } 168 | 169 | 170 | // Subclasses call this 171 | - (void) transportDidOpen { 172 | LogTo(BLIP, @"%@ is open!", self); 173 | _transportIsOpen = true; 174 | if (_outBox.count > 0) 175 | [self feedTransport]; // kick the queue to start sending 176 | 177 | [self _callDelegate: @selector(blipConnectionDidOpen:) 178 | block: ^(id delegate) { 179 | [delegate blipConnectionDidOpen: self]; 180 | }]; 181 | } 182 | 183 | 184 | // Subclasses call this 185 | - (void) transportDidCloseWithError:(NSError *)error { 186 | LogTo(BLIP, @"%@ closed with error %@", self, error.my_compactDescription); 187 | if (_transportIsOpen) { 188 | _transportIsOpen = NO; 189 | [self _callDelegate: @selector(blipConnection:didCloseWithError:) 190 | block: ^(id delegate) { 191 | [delegate blipConnection: self didCloseWithError: error]; 192 | }]; 193 | } else { 194 | if (error && !_error) 195 | self.error = error; 196 | [self _callDelegate: @selector(blipConnection:didFailWithError:) 197 | block: ^(id delegate) { 198 | [delegate blipConnection: self didFailWithError: error]; 199 | }]; 200 | } 201 | } 202 | 203 | 204 | #pragma mark - SENDING: 205 | 206 | 207 | // Public API 208 | - (BLIPRequest*) request { 209 | return [[BLIPRequest alloc] _initWithConnection: self body: nil properties: nil]; 210 | } 211 | 212 | // Public API 213 | - (BLIPRequest*) requestWithBody: (NSData*)body 214 | properties: (NSDictionary*)properties 215 | { 216 | return [[BLIPRequest alloc] _initWithConnection: self body: body properties: properties]; 217 | } 218 | 219 | // Public API 220 | - (BLIPResponse*) sendRequest: (BLIPRequest*)request { 221 | if (!request.isMine || request.sent) { 222 | // This was an incoming request that I'm being asked to forward or echo; 223 | // or it's an outgoing request being sent to multiple connections. 224 | // Since a particular BLIPRequest can only be sent once, make a copy of it to send: 225 | request = [request mutableCopy]; 226 | } 227 | BLIPConnection* itsConnection = request.connection; 228 | if (itsConnection==nil) 229 | request.connection = self; 230 | else 231 | Assert(itsConnection==self,@"%@ is already assigned to a different connection",request); 232 | return [request send]; 233 | } 234 | 235 | 236 | - (void) _queueMessage: (BLIPMessage*)msg isNew: (BOOL)isNew sendNow: (BOOL)sendNow { 237 | Assert(![_outBox containsObject: msg]); 238 | Assert(![_iceBox containsObject: msg]); 239 | Assert(msg != _sendingMsg); 240 | 241 | NSInteger n = _outBox.count, index; 242 | if (msg.urgent && n > 1) { 243 | // High-priority gets queued after the last existing high-priority message, 244 | // leaving one regular-priority message in between if possible. 245 | for (index=n-1; index>0; index--) { 246 | BLIPMessage *otherMsg = _outBox[index]; 247 | if ([otherMsg urgent]) { 248 | index = MIN(index+2, n); 249 | break; 250 | } else if (isNew && otherMsg._bytesWritten==0) { 251 | // But have to keep message starts in order 252 | index = index+1; 253 | break; 254 | } 255 | } 256 | if (index==0) 257 | index = 1; 258 | } else { 259 | // Regular priority goes at the end of the queue: 260 | index = n; 261 | } 262 | if (! _outBox) 263 | _outBox = [[NSMutableArray alloc] init]; 264 | [_outBox insertObject: msg atIndex: index]; 265 | 266 | if (isNew) 267 | LogTo(BLIP,@"%@ queuing outgoing %@ at index %li",self,msg,(long)index); 268 | if (sendNow) { 269 | if (n==0 && _transportIsOpen) { 270 | dispatch_async(_transportQueue, ^{ 271 | [self feedTransport]; // send the first message now 272 | }); 273 | } 274 | } 275 | [self updateActive]; 276 | } 277 | 278 | 279 | - (void) _pauseMessage: (BLIPMessage*)msg { 280 | Assert(![_outBox containsObject: msg]); 281 | Assert(![_iceBox containsObject: msg]); 282 | LogVerbose(BLIP, @"%@: Pausing %@", self, msg); 283 | if (!_iceBox) 284 | _iceBox = [NSMutableArray new]; 285 | [_iceBox addObject: msg]; 286 | } 287 | 288 | 289 | - (void) _unpauseMessage: (BLIPMessage*)msg { 290 | if (!_iceBox) 291 | return; 292 | NSUInteger index = [_iceBox indexOfObjectIdenticalTo: msg]; 293 | if (index != NSNotFound) { 294 | Assert(![_outBox containsObject: msg]); 295 | LogVerbose(BLIP, @"%@: Resuming %@", self, msg); 296 | [_iceBox removeObjectAtIndex: index]; 297 | if (msg != _sendingMsg) 298 | [self _queueMessage: msg isNew: NO sendNow: YES]; 299 | } 300 | } 301 | 302 | 303 | // BLIPMessageSender protocol: Called from -[BLIPRequest send] 304 | - (BOOL) _sendRequest: (BLIPRequest*)q response: (BLIPResponse*)response { 305 | Assert(!q.sent,@"message has already been sent"); 306 | __block BOOL result; 307 | dispatch_sync(_transportQueue, ^{ 308 | if (_transportIsOpen && !self.transportCanSend) { 309 | Warn(@"%@: Attempt to send a request after the connection has started closing: %@",self,q); 310 | result = NO; 311 | return; 312 | } 313 | [q _assignedNumber: ++_numRequestsSent]; 314 | if (response) { 315 | [response _assignedNumber: _numRequestsSent]; 316 | _pendingResponses[@(response.number)] = response; 317 | [self updateActive]; 318 | } 319 | [self _queueMessage: q isNew: YES sendNow: YES]; 320 | result = YES; 321 | }); 322 | return result; 323 | } 324 | 325 | // Internal API: Called from -[BLIPResponse send] 326 | - (BOOL) _sendResponse: (BLIPResponse*)response { 327 | Assert(!response.sent,@"message has already been sent"); 328 | dispatch_async(_transportQueue, ^{ 329 | [self _queueMessage: response isNew: YES sendNow: YES]; 330 | }); 331 | return YES; 332 | } 333 | 334 | 335 | // Subclasses call this 336 | // Pull a frame from the outBox queue and send it to the transport: 337 | - (void) feedTransport { 338 | if (_outBox.count > 0 && !_sendingMsg) { 339 | // Pop first message in queue: 340 | BLIPMessage *msg = _outBox[0]; 341 | [_outBox removeObjectAtIndex: 0]; 342 | _sendingMsg = msg; // Remember that this message is being sent 343 | 344 | // As an optimization, allow message to send a big frame unless there's a higher-priority 345 | // message right behind it: 346 | size_t frameSize = kDefaultFrameSize; 347 | if (msg.urgent || _outBox.count==0 || ! [_outBox[0] urgent]) 348 | frameSize *= 4; 349 | 350 | // Ask the message to generate its next frame. Do this on the delegate queue: 351 | __block BOOL moreComing; 352 | __block NSData* frame; 353 | dispatch_async(_delegateQueue, ^{ 354 | frame = [msg nextFrameWithMaxSize: (uint16_t)frameSize moreComing: &moreComing]; 355 | BOOL requeue = !msg._needsAckToContinue; 356 | void (^onSent)() = moreComing ? nil : msg.onSent; 357 | dispatch_async(_transportQueue, ^{ 358 | // SHAZAM! Send the frame to the transport: 359 | if (frame) 360 | [self sendFrame: frame]; 361 | _sendingMsg = nil; 362 | 363 | if (moreComing) { 364 | // add the message back so it can send its next frame later: 365 | if (requeue) 366 | [self _queueMessage: msg isNew: NO sendNow: NO]; 367 | else 368 | [self _pauseMessage: msg]; 369 | } else { 370 | if (onSent) 371 | [self _onDelegateQueue: onSent]; 372 | } 373 | [self updateActive]; 374 | }); 375 | }); 376 | } else { 377 | //LogVerbose(BLIP,@"%@: no more work for writer",self); 378 | } 379 | } 380 | 381 | 382 | - (BLIPMessage*) outgoingMessageWithNumber: (uint32_t)number isRequest: (BOOL)isRequest { 383 | for (BLIPMessage* msg in _outBox) { 384 | if (msg.number == number && msg.isRequest == isRequest) 385 | return msg; 386 | } 387 | for (BLIPMessage* msg in _iceBox) { 388 | if (msg.number == number && msg.isRequest == isRequest) 389 | return msg; 390 | } 391 | if (_sendingMsg.number == number && _sendingMsg.isRequest == isRequest) 392 | return _sendingMsg; 393 | return nil; 394 | } 395 | 396 | 397 | // Can be called from any queue. 398 | - (void) _sendAckWithNumber: (uint32_t)number 399 | isRequest: (BOOL)isRequest 400 | bytesReceived: (uint64_t)bytesReceived 401 | { 402 | LogVerbose(BLIP, @"%@: Sending %s of %u (%llu bytes)", 403 | self, (isRequest ? "ACKMSG" : "ACKRPY"), number, bytesReceived); 404 | BLIPMessageFlags flags = (isRequest ?kBLIP_ACKMSG :kBLIP_ACKRPY) | kBLIP_Urgent | kBLIP_NoReply; 405 | char buf[3*10]; // max size of varint is 10 bytes 406 | void* pos = &buf[0]; 407 | pos = MYEncodeVarUInt(pos, number); 408 | pos = MYEncodeVarUInt(pos, flags); 409 | pos = MYEncodeVarUInt(pos, bytesReceived); 410 | NSData* frame = [[NSData alloc] initWithBytes: &buf[0] length: ((char*)pos - &buf[0])]; 411 | [self sendFrame: frame]; 412 | } 413 | 414 | 415 | // Subclass must override. 416 | - (BOOL) transportCanSend { 417 | AssertAbstractMethod(); 418 | } 419 | 420 | // Subclass must override. Can be called from any queue. 421 | - (void) sendFrame:(NSData *)frame { 422 | AssertAbstractMethod(); 423 | } 424 | 425 | 426 | #pragma mark - RECEIVING FRAMES: 427 | 428 | 429 | // Subclasses call this 430 | - (void) didReceiveFrame:(NSData*)frame { 431 | const void* start = frame.bytes; 432 | const void* end = start + frame.length; 433 | uint64_t messageNum; 434 | const void* pos = MYDecodeVarUInt(start, end, &messageNum); 435 | if (pos) { 436 | uint64_t flags; 437 | pos = MYDecodeVarUInt(pos, end, &flags); 438 | if (pos && flags <= kBLIP_MaxFlag) { 439 | NSData* body = [NSData dataWithBytes: pos length: frame.length - (pos-start)]; 440 | [self receivedFrameWithNumber: (uint32_t)messageNum 441 | flags: (BLIPMessageFlags)flags 442 | body: body]; 443 | return; 444 | } 445 | } 446 | [self _closeWithError: BLIPMakeError(kBLIPError_BadFrame, 447 | @"Bad varint encoding in frame flags")]; 448 | } 449 | 450 | 451 | - (void) receivedFrameWithNumber: (uint32_t)requestNumber 452 | flags: (BLIPMessageFlags)flags 453 | body: (NSData*)body 454 | { 455 | static const char* kTypeStrs[8] = {"MSG","RPY","ERR","3??", "ACKMSG", "ACKRPY", "6??", "7??"}; 456 | BLIPMessageType type = flags & kBLIP_TypeMask; 457 | LogVerbose(BLIP,@"%@ rcvd frame of %s #%u, length %lu",self,kTypeStrs[type],(unsigned int)requestNumber,(unsigned long)body.length); 458 | 459 | id key = @(requestNumber); 460 | BOOL complete = ! (flags & kBLIP_MoreComing); 461 | switch(type) { 462 | case kBLIP_MSG: { 463 | // Incoming request: 464 | BLIPRequest *request = _pendingRequests[key]; 465 | if (request) { 466 | // Continuation frame of a request: 467 | if (complete) { 468 | [_pendingRequests removeObjectForKey: key]; 469 | } 470 | } else if (requestNumber == _numRequestsReceived+1) { 471 | // Next new request: 472 | request = [[BLIPRequest alloc] _initWithConnection: self 473 | isMine: NO 474 | flags: flags | kBLIP_MoreComing 475 | number: requestNumber 476 | body: nil]; 477 | if (! complete) 478 | _pendingRequests[key] = request; 479 | _numRequestsReceived++; 480 | } else { 481 | return [self _closeWithError: BLIPMakeError(kBLIPError_BadFrame, 482 | @"Received bad request frame #%u (next is #%u)", 483 | (unsigned int)requestNumber, 484 | (unsigned)_numRequestsReceived+1)]; 485 | } 486 | 487 | [self _receivedFrameWithFlags: flags body: body complete: complete forMessage: request]; 488 | break; 489 | } 490 | 491 | case kBLIP_RPY: 492 | case kBLIP_ERR: { 493 | BLIPResponse *response = _pendingResponses[key]; 494 | if (response) { 495 | if (complete) { 496 | [_pendingResponses removeObjectForKey: key]; 497 | } 498 | [self _receivedFrameWithFlags: flags body: body complete: complete forMessage: response]; 499 | 500 | } else { 501 | if (requestNumber <= _numRequestsSent) 502 | LogTo(BLIP,@"??? %@ got unexpected response frame to my msg #%u", 503 | self,(unsigned int)requestNumber); //benign 504 | else 505 | return [self _closeWithError: BLIPMakeError(kBLIPError_BadFrame, 506 | @"Bogus message number %u in response", 507 | (unsigned int)requestNumber)]; 508 | } 509 | break; 510 | } 511 | 512 | case kBLIP_ACKMSG: 513 | case kBLIP_ACKRPY: { 514 | BLIPMessage* msg = [self outgoingMessageWithNumber: requestNumber 515 | isRequest: (type == kBLIP_ACKMSG)]; 516 | if (!msg) { 517 | LogTo(BLIP, @"??? %@ Received ACK for non-current message (%s %u)", 518 | self, kTypeStrs[type], requestNumber); 519 | break; 520 | } 521 | uint64_t bytesReceived; 522 | if (!MYDecodeVarUInt(body.bytes, body.bytes + body.length, &bytesReceived)) 523 | return [self _closeWithError: BLIPMakeError(kBLIPError_BadFrame, @"Bad ACK body")]; 524 | [self _onDelegateQueue: ^{ 525 | BOOL ok = [msg _receivedAck: bytesReceived]; 526 | dispatch_async(_transportQueue, ^{ 527 | if (ok) 528 | [self _unpauseMessage: msg]; 529 | else 530 | [self _closeWithError: BLIPMakeError(kBLIPError_BadFrame,@"Bad ACK count")]; 531 | }); 532 | }]; 533 | break; 534 | } 535 | 536 | default: 537 | // To leave room for future expansion, undefined message types are just ignored. 538 | Log(@"??? %@ received header with unknown message type %i", self,type); 539 | break; 540 | } 541 | [self updateActive]; 542 | } 543 | 544 | 545 | - (void) _receivedFrameWithFlags: (BLIPMessageFlags)flags 546 | body: (NSData*)body 547 | complete: (BOOL)complete 548 | forMessage: (BLIPMessage*)message 549 | { 550 | [self _onDelegateQueue: ^{ 551 | BOOL ok = [message _receivedFrameWithFlags: flags body: body]; 552 | if (!ok) { 553 | dispatch_async(_transportQueue, ^{ 554 | [self _closeWithError: BLIPMakeError(kBLIPError_BadFrame, 555 | @"Couldn't parse message frame")]; 556 | }); 557 | } else if (complete && !self.dispatchPartialMessages) { 558 | if (message.isRequest) 559 | [self _dispatchRequest: (BLIPRequest*)message]; 560 | else 561 | [self _dispatchResponse: (BLIPResponse*)message]; 562 | } 563 | }]; 564 | } 565 | 566 | 567 | #pragma mark - REGISTERING ACTIONS: 568 | 569 | 570 | - (void) onRequestProfile: (NSString*)profile sendDelegateAction: (SEL)action { 571 | [self _onDelegateQueue: ^{ 572 | if (action) { 573 | Assert([_delegate respondsToSelector: action]); 574 | if (!_registeredActions) 575 | _registeredActions = [NSMutableDictionary new]; 576 | _registeredActions[profile] = NSStringFromSelector(action); 577 | } else { 578 | [_registeredActions removeObjectForKey: profile]; 579 | } 580 | }]; 581 | } 582 | 583 | - (void) registerDelegateActions: (NSDictionary*)actions { 584 | [self _onDelegateQueue: ^{ 585 | if (!_registeredActions) 586 | _registeredActions = [NSMutableDictionary new]; 587 | [_registeredActions addEntriesFromDictionary: actions]; 588 | }]; 589 | } 590 | 591 | 592 | - (BOOL) _sendRegisteredAction: (BLIPRequest*)request { 593 | typedef void (*ActionMethodCall)(id self, SEL cmd, BLIPRequest* request); 594 | 595 | NSString* profile = request.profile; 596 | if (profile) { 597 | NSString* actionStr = _registeredActions[profile]; 598 | if (actionStr) { 599 | SEL action = NSSelectorFromString(actionStr); 600 | // ARC-safe equivalent of [_delegate performSelector: action withObject: request] : 601 | ((ActionMethodCall)objc_msgSend)(_delegate, action, request); 602 | return YES; 603 | } 604 | } 605 | return NO; 606 | } 607 | 608 | 609 | #pragma mark - DISPATCHING: 610 | 611 | 612 | // called on delegate queue 613 | - (void) _messageReceivedProperties: (BLIPMessage*)message { 614 | if (self.dispatchPartialMessages) { 615 | if (message.isRequest) 616 | [self _dispatchRequest: (BLIPRequest*)message]; 617 | else 618 | [self _dispatchResponse: (BLIPResponse*)message]; 619 | } 620 | } 621 | 622 | 623 | // Called on the delegate queue (by _dispatchRequest)! 624 | - (BOOL) _dispatchMetaRequest: (BLIPRequest*)request { 625 | #if 0 626 | NSString* profile = request.profile; 627 | if ([profile isEqualToString: kBLIPProfile_Bye]) { 628 | [self _handleCloseRequest: request]; 629 | return YES; 630 | } 631 | #endif 632 | return NO; 633 | } 634 | 635 | 636 | // called on delegate queue 637 | - (void) _dispatchRequest: (BLIPRequest*)request { 638 | id delegate = _delegate; 639 | LogTo(BLIP,@"Dispatching %@",request.descriptionWithProperties); 640 | @try{ 641 | BOOL handled; 642 | if (request._flags & kBLIP_Meta) 643 | handled =[self _dispatchMetaRequest: request]; 644 | else { 645 | handled = [self _sendRegisteredAction: request] 646 | || ([delegate respondsToSelector: @selector(blipConnection:receivedRequest:)] 647 | && [delegate blipConnection: self receivedRequest: request]); 648 | } 649 | 650 | if (request.complete) { 651 | if (!handled) { 652 | LogTo(BLIP,@"No handler found for incoming %@",request); 653 | [request respondWithErrorCode: kBLIPError_NotFound message: @"No handler was found"]; 654 | } else if (! request.noReply && ! request.repliedTo) { 655 | LogTo(BLIP,@"Returning default empty response to %@",request); 656 | [request respondWithData: nil contentType: nil]; 657 | } 658 | } 659 | }@catch( NSException *x ) { 660 | MYReportException(x,@"Dispatching BLIP request"); 661 | [request respondWithException: x]; 662 | } 663 | } 664 | 665 | // called on delegate queue 666 | - (void) _dispatchResponse: (BLIPResponse*)response { 667 | LogTo(BLIP,@"Dispatching %@",response); 668 | // (Don't use _callDelegate:block: because I'm already on the delegate queue) 669 | id delegate = _delegate; 670 | if ([delegate respondsToSelector: @selector(blipConnection:receivedResponse:)]) 671 | [delegate blipConnection: self receivedResponse: response]; 672 | } 673 | 674 | 675 | @end 676 | --------------------------------------------------------------------------------