├── 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 |
--------------------------------------------------------------------------------