├── .gitignore ├── .gitmodules ├── BLIP ├── BLIP.h ├── BLIPConnection+Transport.h ├── BLIPConnection.h ├── BLIPConnection.m ├── BLIPDispatcher.h ├── BLIPDispatcher.m ├── BLIPHTTPProtocol.h ├── BLIPHTTPProtocol.m ├── BLIPMessage.h ├── BLIPMessage.m ├── BLIPPool.h ├── BLIPPool.m ├── BLIPProperties.h ├── BLIPProperties.m ├── BLIPRequest+HTTP.h ├── BLIPRequest+HTTP.m ├── BLIPRequest.h ├── BLIPRequest.m ├── BLIPResponse.h ├── BLIPResponse.m ├── BLIPWebSocketConnection.h ├── BLIPWebSocketConnection.m ├── BLIPWebSocketListener.h ├── BLIPWebSocketListener.m └── BLIP_Internal.h ├── Categories ├── DDData.h └── DDData.m ├── LICENSE ├── README.md ├── Testing ├── blip_echo.go ├── bliptest_main.m ├── test_main.m ├── ws_client.go └── ws_echo.go ├── WebSocket.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata └── WebSocket ├── WebSocket.h ├── WebSocket.m ├── WebSocketClient.h ├── WebSocketClient.m ├── WebSocketHTTPLogic.h ├── WebSocketHTTPLogic.m ├── WebSocketListener.h ├── WebSocketListener.m └── WebSocket_Internal.h /.gitignore: -------------------------------------------------------------------------------- 1 | /blip_echo 2 | /ws_client 3 | /ws_echo 4 | # Generic ignores: 5 | .DS_Store 6 | *~ 7 | xcuserdata/ 8 | /build/ 9 | /DerivedData/ 10 | *.xccheckout 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/MYUtilities"] 2 | path = vendor/MYUtilities 3 | url = https://github.com/snej/MYUtilities.git 4 | [submodule "vendor/CocoaAsyncSocket"] 5 | path = vendor/CocoaAsyncSocket 6 | url = https://github.com/robbiehanson/CocoaAsyncSocket.git 7 | -------------------------------------------------------------------------------- /BLIP/BLIP.h: -------------------------------------------------------------------------------- 1 | // 2 | // BLIP.h 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 9/12/13. 6 | // Copyright (c) 2013 Couchbase. All rights reserved. 7 | 8 | #import "BLIPConnection.h" 9 | #import "BLIPRequest.h" 10 | #import "BLIPResponse.h" 11 | -------------------------------------------------------------------------------- /BLIP/BLIPConnection+Transport.h: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPConnection+Transport.h 3 | // BLIPSync 4 | // 5 | // Created by Jens Alfke on 4/10/15. 6 | // Copyright (c) 2015 Couchbase. All rights reserved. 7 | // 8 | 9 | #import "BLIPConnection.h" 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 | // Internal methods for subclasses to call: 25 | 26 | /** Call this when the transport opens and is ready to send/receive messages. */ 27 | - (void) transportDidOpen; 28 | 29 | /** Call this when the transport closes or fails to open. 30 | @param error The error, or nil if this is a normal closure. */ 31 | - (void) transportDidCloseWithError: (NSError*)error; 32 | 33 | /** Call this when the transport is done sending data and is ready to send more. 34 | If any messages are ready, it will call -sendFrame:. */ 35 | - (void) feedTransport; 36 | 37 | /** Call this when the transport receives a frame from the peer. */ 38 | - (void) didReceiveFrame: (NSData*)frame; 39 | 40 | // Abstract internal methods that subclasses must implement: 41 | 42 | /** Subclass must implement this to return YES if the transport is in a state where it can send 43 | messages, NO if not. Most importantly, it should return NO if it's in the process of closing.*/ 44 | - (BOOL) transportCanSend; 45 | 46 | /** Subclass must implement this to send the frame to the peer. */ 47 | - (void) sendFrame: (NSData*)frame; 48 | 49 | // Abstract public methods that that subclasses must implement: 50 | // - (BOOL) connect: (NSError**)outError; 51 | // - (void) close; 52 | 53 | @end 54 | -------------------------------------------------------------------------------- /BLIP/BLIPConnection.h: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPConnection.h 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 4/1/13. 6 | // Copyright (c) 2013 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. */ 16 | @interface BLIPConnection : NSObject 17 | 18 | /** Attaches a delegate, and specifies what GCD queue it should be called on. */ 19 | - (void) setDelegate: (id)delegate 20 | queue: (dispatch_queue_t)delegateQueue; 21 | 22 | /** URL this socket is connected to, _if_ it's a client socket; if it's an incoming one received 23 | by a BLIPConnectionListener, this is nil. */ 24 | @property (readonly) NSURL* URL; 25 | 26 | @property (readonly) NSError* error; 27 | 28 | - (BOOL) connect: (NSError**)outError; 29 | 30 | - (void)close; 31 | 32 | /** 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. */ 33 | @property BOOL dispatchPartialMessages; 34 | 35 | /** Creates a new, empty outgoing request. 36 | You should add properties and/or body data to the request, before sending it by 37 | calling its -send method. */ 38 | - (BLIPRequest*) request; 39 | 40 | /** Creates a new outgoing request. 41 | The body or properties may be nil; you can add additional data or properties by calling 42 | methods on the request itself, before sending it by calling its -send method. */ 43 | - (BLIPRequest*) requestWithBody: (NSData*)body 44 | properties: (NSDictionary*)properies; 45 | 46 | /** Sends a request over this connection. 47 | (Actually, it queues it to be sent; this method always returns immediately.) 48 | Call this instead of calling -send on the request itself, if the request was created with 49 | +[BLIPRequest requestWithBody:] and hasn't yet been assigned to any connection. 50 | This method will assign it to this connection before sending it. 51 | The request's matching response object will be returned, or nil if the request couldn't be sent. */ 52 | - (BLIPResponse*) sendRequest: (BLIPRequest*)request; 53 | 54 | /** Are any messages currently being sent or received? (Observable) */ 55 | @property (readonly) BOOL active; 56 | 57 | @end 58 | 59 | 60 | 61 | /** The delegate messages that BLIPConnection will send. 62 | All methods are optional. */ 63 | @protocol BLIPConnectionDelegate 64 | @optional 65 | 66 | - (void)blipConnectionDidOpen:(BLIPConnection*)connection; 67 | 68 | - (void)blipConnection: (BLIPConnection*)connection didFailWithError:(NSError *)error; 69 | 70 | - (void)blipConnection: (BLIPConnection*)connection 71 | didCloseWithError: (NSError*)error; 72 | 73 | /** Called when a BLIPRequest is received from the peer, if there is no BLIPDispatcher 74 | rule to handle it. 75 | If the delegate wants to accept the request it should return YES; if it returns NO, 76 | a kBLIPError_NotFound error will be returned to the sender. 77 | The delegate should get the request's response object, fill in its data and properties 78 | or error property, and send it. 79 | If it doesn't explicitly send a response, a default empty one will be sent; 80 | to prevent this, call -deferResponse on the request if you want to send a response later. */ 81 | - (BOOL) blipConnection: (BLIPConnection*)connection receivedRequest: (BLIPRequest*)request; 82 | 83 | /** Called when a BLIPResponse (to one of your requests) is received from the peer. 84 | This is called after the response object's onComplete target, if any, is invoked.*/ 85 | - (void) blipConnection: (BLIPConnection*)connection receivedResponse: (BLIPResponse*)response; 86 | 87 | @end 88 | -------------------------------------------------------------------------------- /BLIP/BLIPConnection.m: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPConnection.m 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 4/1/13. 6 | // Copyright (c) 2013 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 "Logging.h" 23 | #import "Test.h" 24 | #import "MYData.h" 25 | 26 | 27 | #define kDefaultFrameSize 4096 28 | 29 | 30 | @interface BLIPConnection () 31 | @property (readwrite) BOOL active; 32 | @end 33 | 34 | 35 | @implementation BLIPConnection 36 | { 37 | dispatch_queue_t _transportQueue; 38 | bool _transportIsOpen; 39 | NSError* _error; 40 | __weak id _delegate; 41 | dispatch_queue_t _delegateQueue; 42 | 43 | NSMutableArray *_outBox; 44 | UInt32 _numRequestsSent; 45 | 46 | UInt32 _numRequestsReceived; 47 | NSMutableDictionary *_pendingRequests, *_pendingResponses; 48 | NSUInteger _poppedMessageCount; 49 | } 50 | 51 | @synthesize error=_error, dispatchPartialMessages=_dispatchPartialMessages, active=_active; 52 | 53 | 54 | - (instancetype) initWithTransportQueue: (dispatch_queue_t)transportQueue 55 | isOpen: (BOOL)isOpen 56 | { 57 | Assert(transportQueue); 58 | self = [super init]; 59 | if (self) { 60 | _transportQueue = transportQueue; 61 | _transportIsOpen = isOpen; 62 | _delegateQueue = dispatch_get_main_queue(); 63 | _pendingRequests = [[NSMutableDictionary alloc] init]; 64 | _pendingResponses = [[NSMutableDictionary alloc] init]; 65 | } 66 | return self; 67 | } 68 | 69 | 70 | // Public API 71 | - (void) setDelegate: (id)delegate 72 | queue: (dispatch_queue_t)delegateQueue 73 | { 74 | Assert(!_delegate, @"Don't change the delegate"); 75 | _delegate = delegate; 76 | _delegateQueue = delegateQueue ?: dispatch_get_main_queue(); 77 | } 78 | 79 | 80 | - (void) _callDelegate: (SEL)selector block: (void(^)(id))block { 81 | id delegate = _delegate; 82 | if (delegate && [delegate respondsToSelector: selector]) { 83 | dispatch_async(_delegateQueue, ^{ 84 | block(delegate); 85 | }); 86 | } 87 | } 88 | 89 | 90 | // Public API 91 | - (NSURL*) URL { 92 | return nil; // Subclasses should override 93 | } 94 | 95 | 96 | - (void) updateActive { 97 | BOOL active = _outBox.count || _pendingRequests.count || 98 | _pendingResponses.count || _poppedMessageCount; 99 | if (active != _active) { 100 | LogTo(BLIPVerbose, @"%@ active = %@", self, (active ?@"YES" : @"NO")); 101 | self.active = active; 102 | } 103 | } 104 | 105 | 106 | #pragma mark - OPEN/CLOSE: 107 | 108 | 109 | // Public API 110 | - (BOOL) connect: (NSError**)outError { 111 | AssertAbstractMethod(); 112 | } 113 | 114 | // Public API 115 | - (void)close { 116 | AssertAbstractMethod(); 117 | } 118 | 119 | 120 | - (void) _closeWithError: (NSError*)error { 121 | self.error = error; 122 | [self close]; 123 | } 124 | 125 | 126 | // Subclasses call this 127 | - (void) transportDidOpen { 128 | LogTo(BLIP, @"%@ is open!", self); 129 | _transportIsOpen = true; 130 | if (_outBox.count > 0) 131 | [self feedTransport]; // kick the queue to start sending 132 | 133 | [self _callDelegate: @selector(blipConnectionDidOpen:) 134 | block: ^(id delegate) { 135 | [delegate blipConnectionDidOpen: self]; 136 | }]; 137 | } 138 | 139 | 140 | // Subclasses call this 141 | - (void) transportDidCloseWithError:(NSError *)error { 142 | LogTo(BLIP, @"%@ closed with error %@", self, error); 143 | if (_transportIsOpen) { 144 | _transportIsOpen = NO; 145 | [self _callDelegate: @selector(blipConnection:didCloseWithError:) 146 | block: ^(id delegate) { 147 | [delegate blipConnection: self didCloseWithError: error]; 148 | }]; 149 | } else { 150 | if (error && !_error) 151 | self.error = error; 152 | [self _callDelegate: @selector(blipConnection:didFailWithError:) 153 | block: ^(id delegate) { 154 | [delegate blipConnection: self didFailWithError: error]; 155 | }]; 156 | } 157 | } 158 | 159 | 160 | #pragma mark - SENDING: 161 | 162 | 163 | // Public API 164 | - (BLIPRequest*) request { 165 | return [[BLIPRequest alloc] _initWithConnection: self body: nil properties: nil]; 166 | } 167 | 168 | // Public API 169 | - (BLIPRequest*) requestWithBody: (NSData*)body 170 | properties: (NSDictionary*)properties 171 | { 172 | return [[BLIPRequest alloc] _initWithConnection: self body: body properties: properties]; 173 | } 174 | 175 | // Public API 176 | - (BLIPResponse*) sendRequest: (BLIPRequest*)request { 177 | if (!request.isMine || request.sent) { 178 | // This was an incoming request that I'm being asked to forward or echo; 179 | // or it's an outgoing request being sent to multiple connections. 180 | // Since a particular BLIPRequest can only be sent once, make a copy of it to send: 181 | request = [request mutableCopy]; 182 | } 183 | BLIPConnection* itsConnection = request.connection; 184 | if (itsConnection==nil) 185 | request.connection = self; 186 | else 187 | Assert(itsConnection==self,@"%@ is already assigned to a different connection",request); 188 | return [request send]; 189 | } 190 | 191 | 192 | - (void) _queueMessage: (BLIPMessage*)msg isNew: (BOOL)isNew { 193 | NSInteger n = _outBox.count, index; 194 | if (msg.urgent && n > 1) { 195 | // High-priority gets queued after the last existing high-priority message, 196 | // leaving one regular-priority message in between if possible. 197 | for (index=n-1; index>0; index--) { 198 | BLIPMessage *otherMsg = _outBox[index]; 199 | if ([otherMsg urgent]) { 200 | index = MIN(index+2, n); 201 | break; 202 | } else if (isNew && otherMsg._bytesWritten==0) { 203 | // But have to keep message starts in order 204 | index = index+1; 205 | break; 206 | } 207 | } 208 | if (index==0) 209 | index = 1; 210 | } else { 211 | // Regular priority goes at the end of the queue: 212 | index = n; 213 | } 214 | if (! _outBox) 215 | _outBox = [[NSMutableArray alloc] init]; 216 | [_outBox insertObject: msg atIndex: index]; 217 | 218 | if (isNew) { 219 | LogTo(BLIP,@"%@ queuing outgoing %@ at index %li",self,msg,(long)index); 220 | if (n==0 && _transportIsOpen) { 221 | dispatch_async(_transportQueue, ^{ 222 | [self feedTransport]; // queue the first message now 223 | }); 224 | } 225 | } 226 | [self updateActive]; 227 | } 228 | 229 | 230 | // BLIPMessageSender protocol: Called from -[BLIPRequest send] 231 | - (BOOL) _sendRequest: (BLIPRequest*)q response: (BLIPResponse*)response { 232 | Assert(!q.sent,@"message has already been sent"); 233 | __block BOOL result; 234 | dispatch_sync(_transportQueue, ^{ 235 | if (_transportIsOpen && !self.transportCanSend) { 236 | Warn(@"%@: Attempt to send a request after the connection has started closing: %@",self,q); 237 | result = NO; 238 | return; 239 | } 240 | [q _assignedNumber: ++_numRequestsSent]; 241 | if (response) { 242 | [response _assignedNumber: _numRequestsSent]; 243 | _pendingResponses[$object(response.number)] = response; 244 | [self updateActive]; 245 | } 246 | [self _queueMessage: q isNew: YES]; 247 | result = YES; 248 | }); 249 | return result; 250 | } 251 | 252 | // Internal API: Called from -[BLIPResponse send] 253 | - (BOOL) _sendResponse: (BLIPResponse*)response { 254 | Assert(!response.sent,@"message has already been sent"); 255 | dispatch_async(_transportQueue, ^{ 256 | [self _queueMessage: response isNew: YES]; 257 | }); 258 | return YES; 259 | } 260 | 261 | 262 | // Subclasses call this 263 | // Pull a frame from the outBox queue and send it to the transport: 264 | - (void) feedTransport { 265 | if (_outBox.count > 0) { 266 | // Pop first message in queue: 267 | BLIPMessage *msg = _outBox[0]; 268 | [_outBox removeObjectAtIndex: 0]; 269 | ++_poppedMessageCount; // remember that this message is still active 270 | 271 | // As an optimization, allow message to send a big frame unless there's a higher-priority 272 | // message right behind it: 273 | size_t frameSize = kDefaultFrameSize; 274 | if (msg.urgent || _outBox.count==0 || ! [_outBox[0] urgent]) 275 | frameSize *= 4; 276 | 277 | // Ask the message to generate its next frame. Do this on the delegate queue: 278 | __block BOOL moreComing; 279 | __block NSData* frame; 280 | dispatch_async(_delegateQueue, ^{ 281 | frame = [msg nextFrameWithMaxSize: (UInt16)frameSize moreComing: &moreComing]; 282 | void (^onSent)() = moreComing ? nil : msg.onSent; 283 | dispatch_async(_transportQueue, ^{ 284 | // SHAZAM! Send the frame to the transport: 285 | [self sendFrame: frame]; 286 | 287 | if (moreComing) { 288 | // add the message back so it can send its next frame later: 289 | [self _queueMessage: msg isNew: NO]; 290 | } else { 291 | if (onSent) 292 | dispatch_async(_delegateQueue, onSent); 293 | } 294 | --_poppedMessageCount; 295 | [self updateActive]; 296 | }); 297 | }); 298 | } else { 299 | //LogTo(BLIPVerbose,@"%@: no more work for writer",self); 300 | } 301 | } 302 | 303 | 304 | - (BOOL) transportCanSend { 305 | AssertAbstractMethod(); 306 | } 307 | 308 | - (void) sendFrame:(NSData *)frame { 309 | AssertAbstractMethod(); 310 | } 311 | 312 | 313 | #pragma mark - RECEIVING FRAMES: 314 | 315 | 316 | // Subclasses call this 317 | - (void) didReceiveFrame:(NSData*)frame { 318 | const void* start = frame.bytes; 319 | const void* end = start + frame.length; 320 | UInt64 messageNum; 321 | const void* pos = MYDecodeVarUInt(start, end, &messageNum); 322 | if (pos) { 323 | UInt64 flags; 324 | pos = MYDecodeVarUInt(pos, end, &flags); 325 | if (pos && flags <= kBLIP_MaxFlag) { 326 | NSData* body = [NSData dataWithBytes: pos length: frame.length - (pos-start)]; 327 | [self receivedFrameWithNumber: (UInt32)messageNum 328 | flags: (BLIPMessageFlags)flags 329 | body: body]; 330 | return; 331 | } 332 | } 333 | [self _closeWithError: BLIPMakeError(kBLIPError_BadFrame, 334 | @"Bad varint encoding in frame flags")]; 335 | } 336 | 337 | 338 | - (void) receivedFrameWithNumber: (UInt32)requestNumber 339 | flags: (BLIPMessageFlags)flags 340 | body: (NSData*)body 341 | { 342 | static const char* kTypeStrs[16] = {"MSG","RPY","ERR","3??","4??","5??","6??","7??"}; 343 | BLIPMessageType type = flags & kBLIP_TypeMask; 344 | LogTo(BLIPVerbose,@"%@ rcvd frame of %s #%u, length %lu",self,kTypeStrs[type],(unsigned int)requestNumber,(unsigned long)body.length); 345 | 346 | id key = $object(requestNumber); 347 | BOOL complete = ! (flags & kBLIP_MoreComing); 348 | switch(type) { 349 | case kBLIP_MSG: { 350 | // Incoming request: 351 | BLIPRequest *request = _pendingRequests[key]; 352 | if (request) { 353 | // Continuation frame of a request: 354 | if (complete) { 355 | [_pendingRequests removeObjectForKey: key]; 356 | } 357 | } else if (requestNumber == _numRequestsReceived+1) { 358 | // Next new request: 359 | request = [[BLIPRequest alloc] _initWithConnection: self 360 | isMine: NO 361 | flags: flags | kBLIP_MoreComing 362 | number: requestNumber 363 | body: nil]; 364 | if (! complete) 365 | _pendingRequests[key] = request; 366 | _numRequestsReceived++; 367 | } else { 368 | return [self _closeWithError: BLIPMakeError(kBLIPError_BadFrame, 369 | @"Received bad request frame #%u (next is #%u)", 370 | (unsigned int)requestNumber, 371 | (unsigned)_numRequestsReceived+1)]; 372 | } 373 | 374 | [self _receiveFrameWithFlags: flags body: body complete: complete forMessage: request]; 375 | break; 376 | } 377 | 378 | case kBLIP_RPY: 379 | case kBLIP_ERR: { 380 | BLIPResponse *response = _pendingResponses[key]; 381 | if (response) { 382 | if (complete) { 383 | [_pendingResponses removeObjectForKey: key]; 384 | } 385 | [self _receiveFrameWithFlags: flags body: body complete: complete forMessage: response]; 386 | 387 | } else { 388 | if (requestNumber <= _numRequestsSent) 389 | LogTo(BLIP,@"??? %@ got unexpected response frame to my msg #%u", 390 | self,(unsigned int)requestNumber); //benign 391 | else 392 | return [self _closeWithError: BLIPMakeError(kBLIPError_BadFrame, 393 | @"Bogus message number %u in response", 394 | (unsigned int)requestNumber)]; 395 | } 396 | break; 397 | } 398 | 399 | default: 400 | // To leave room for future expansion, undefined message types are just ignored. 401 | Log(@"??? %@ received header with unknown message type %i", self,type); 402 | break; 403 | } 404 | } 405 | 406 | 407 | - (void) _receiveFrameWithFlags: (BLIPMessageFlags)flags 408 | body: (NSData*)body 409 | complete: (BOOL)complete 410 | forMessage: (BLIPMessage*)message 411 | { 412 | [self updateActive]; 413 | dispatch_async(_delegateQueue, ^{ 414 | BOOL ok = [message _receivedFrameWithFlags: flags body: body]; 415 | if (!ok) { 416 | dispatch_async(_transportQueue, ^{ 417 | [self _closeWithError: BLIPMakeError(kBLIPError_BadFrame, 418 | @"Couldn't parse message frame")]; 419 | }); 420 | } else if (complete && !self.dispatchPartialMessages) { 421 | if (message.isRequest) 422 | [self _dispatchRequest: (BLIPRequest*)message]; 423 | else 424 | [self _dispatchResponse: (BLIPResponse*)message]; 425 | } 426 | }); 427 | } 428 | 429 | 430 | #pragma mark - DISPATCHING: 431 | 432 | 433 | // called on delegate queue 434 | - (void) _messageReceivedProperties: (BLIPMessage*)message { 435 | if (self.dispatchPartialMessages) { 436 | if (message.isRequest) 437 | [self _dispatchRequest: (BLIPRequest*)message]; 438 | else 439 | [self _dispatchResponse: (BLIPResponse*)message]; 440 | } 441 | } 442 | 443 | 444 | // Called on the delegate queue (by _dispatchRequest)! 445 | - (BOOL) _dispatchMetaRequest: (BLIPRequest*)request { 446 | #if 0 447 | NSString* profile = request.profile; 448 | if ([profile isEqualToString: kBLIPProfile_Bye]) { 449 | [self _handleCloseRequest: request]; 450 | return YES; 451 | } 452 | #endif 453 | return NO; 454 | } 455 | 456 | 457 | // called on delegate queue 458 | - (void) _dispatchRequest: (BLIPRequest*)request { 459 | id delegate = _delegate; 460 | LogTo(BLIP,@"Dispatching %@",request.descriptionWithProperties); 461 | @try{ 462 | BOOL handled; 463 | if (request._flags & kBLIP_Meta) 464 | handled =[self _dispatchMetaRequest: request]; 465 | else { 466 | handled = [delegate respondsToSelector: @selector(blipConnection:receivedRequest:)] 467 | && [delegate blipConnection: self receivedRequest: request]; 468 | } 469 | 470 | if (request.complete) { 471 | if (!handled) { 472 | LogTo(BLIP,@"No handler found for incoming %@",request); 473 | [request respondWithErrorCode: kBLIPError_NotFound message: @"No handler was found"]; 474 | } else if (! request.noReply && ! request.repliedTo) { 475 | LogTo(BLIP,@"Returning default empty response to %@",request); 476 | [request respondWithData: nil contentType: nil]; 477 | } 478 | } 479 | }@catch( NSException *x ) { 480 | MYReportException(x,@"Dispatching BLIP request"); 481 | [request respondWithException: x]; 482 | } 483 | } 484 | 485 | - (void) _dispatchResponse: (BLIPResponse*)response { 486 | LogTo(BLIP,@"Dispatching %@",response); 487 | [self _callDelegate: @selector(blipConnection:receivedResponse:) 488 | block: ^(id delegate) { 489 | [delegate blipConnection: self receivedResponse: response]; 490 | }]; 491 | } 492 | 493 | 494 | @end 495 | -------------------------------------------------------------------------------- /BLIP/BLIPDispatcher.h: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPDispatcher.h 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 5/15/08. 6 | // Copyright 2008-2013 Jens Alfke. 7 | // Copyright (c) 2013 Couchbase, Inc. All rights reserved. 8 | 9 | #import 10 | @class MYTarget, BLIPMessage; 11 | 12 | 13 | /** A block that gets called if a dispatcher rule matches. */ 14 | typedef void (^BLIPDispatchBlock)(BLIPMessage*); 15 | 16 | 17 | /** Routes BLIP messages to targets based on a series of rules. */ 18 | @interface BLIPDispatcher : NSObject 19 | 20 | /** The inherited parent dispatcher. 21 | If a message does not match any of this dispatcher's rules, it will next be passed to 22 | the parent, if there is one. */ 23 | @property (strong) BLIPDispatcher *parent; 24 | 25 | /** Adds a new rule, to call a given target method if a given predicate matches the message. The return value is a token that you can later pass to -removeRule: to unregister this rule. */ 26 | - (id) onPredicate: (NSPredicate*)predicate do: (BLIPDispatchBlock)block; 27 | 28 | /** Convenience method that adds a rule that compares a property against a string. */ 29 | - (id) onProperty: (NSString*)property value: (NSString*)value do: (BLIPDispatchBlock)block; 30 | 31 | /** Removes all rules with the given target. */ 32 | - (void) removeRule: (id)rule; 33 | 34 | /** Tests the message against all the rules, in the order they were added, and calls the 35 | target of the first matching rule. 36 | If no rule matches, the message is passed to the parent dispatcher's -dispatchMessage:, 37 | if there is a parent. 38 | If no rules at all match, NO is returned. */ 39 | - (BOOL) dispatchMessage: (BLIPMessage*)message; 40 | 41 | /** Returns a target object that will call this dispatcher's -dispatchMessage: method. 42 | This can be used to make this dispatcher the target of another dispatcher's rule, 43 | stringing them together hierarchically. */ 44 | - (BLIPDispatchBlock) asDispatchBlock; 45 | 46 | @end 47 | -------------------------------------------------------------------------------- /BLIP/BLIPDispatcher.m: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPDispatcher.m 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 5/15/08. 6 | // Copyright 2008-2013 Jens Alfke. 7 | // Copyright (c) 2013 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 "BLIPDispatcher.h" 18 | #import "BLIPRequest.h" 19 | #import "BLIPProperties.h" 20 | #import "Test.h" 21 | 22 | 23 | @implementation BLIPDispatcher 24 | { 25 | NSMutableArray *_predicates, *_targets; 26 | BLIPDispatcher *_parent; 27 | } 28 | 29 | 30 | - (instancetype) init { 31 | self = [super init]; 32 | if (self != nil) { 33 | _targets = [[NSMutableArray alloc] init]; 34 | _predicates = [[NSMutableArray alloc] init]; 35 | } 36 | return self; 37 | } 38 | 39 | 40 | 41 | @synthesize parent=_parent; 42 | 43 | 44 | - (id) onPredicate: (NSPredicate*)predicate do: (BLIPDispatchBlock)block { 45 | [_targets addObject: block]; 46 | [_predicates addObject: predicate]; 47 | return @(_targets.count - 1); 48 | } 49 | 50 | 51 | - (void) removeRule: (id)rule { 52 | NSUInteger ruleID = [$cast(NSNumber, rule) unsignedIntegerValue]; 53 | _targets[ruleID] = [NSNull null]; 54 | _predicates[ruleID] = [NSNull null]; 55 | } 56 | 57 | 58 | - (id) onProperty: (NSString*)key value: (NSString*)value do: (BLIPDispatchBlock)block { 59 | return [self onPredicate: [NSComparisonPredicate 60 | predicateWithLeftExpression: [NSExpression expressionForKeyPath: key] 61 | rightExpression: [NSExpression expressionForConstantValue: value] 62 | modifier: NSDirectPredicateModifier 63 | type: NSEqualToPredicateOperatorType 64 | options: 0] 65 | do: block]; 66 | } 67 | 68 | 69 | - (BOOL) dispatchMessage: (BLIPMessage*)message { 70 | NSDictionary *properties = message.properties; 71 | NSUInteger n = _predicates.count; 72 | for (NSUInteger i=0; i. All rights reserved. 96 | 97 | Redistribution and use in source and binary forms, with or without modification, are permitted 98 | provided that the following conditions are met: 99 | 100 | * Redistributions of source code must retain the above copyright notice, this list of conditions 101 | and the following disclaimer. 102 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions 103 | and the following disclaimer in the documentation and/or other materials provided with the 104 | distribution. 105 | 106 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 107 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 108 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRI- 109 | BUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 110 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 111 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 112 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 113 | THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 114 | */ 115 | -------------------------------------------------------------------------------- /BLIP/BLIPHTTPProtocol.h: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPHTTPProtocol.h 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 4/15/13. 6 | // Copyright (c) 2013 Couchbase, Inc. All rights reserved. 7 | 8 | #import 9 | 10 | /** Implementation of the "wshttp" URL protocol, for sending HTTP-style requests over WebSockets 11 | using BLIP. */ 12 | @interface BLIPHTTPProtocol : NSURLProtocol 13 | 14 | + (void) registerWebSocketURL: (NSURL*)wsURL forURL: (NSURL*)baseURL; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /BLIP/BLIPHTTPProtocol.m: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPHTTPProtocol.m 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 4/15/13. 6 | // Copyright (c) 2013 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 "BLIPHTTPProtocol.h" 17 | #import "BLIPWebSocket.h" 18 | #import "BLIPPool.h" 19 | #import "BLIPRequest+HTTP.h" 20 | 21 | #import "CollectionUtils.h" 22 | #import "Logging.h" 23 | 24 | 25 | @interface BLIPHTTPProtocol () 26 | @end 27 | 28 | 29 | @implementation BLIPHTTPProtocol 30 | { 31 | NSURL* _webSocketURL; 32 | BLIPResponse* _response; 33 | BOOL _gotHeaders; 34 | NSMutableData* _responseBody; 35 | } 36 | 37 | 38 | static NSMutableDictionary* sMappings; 39 | static NSMutableSet* sMappedHosts; 40 | 41 | static BLIPPool* sSockets; 42 | 43 | 44 | + (void) registerWebSocketURL: (NSURL*)wsURL forURL: (NSURL*)baseURL { 45 | @synchronized(self) { 46 | if (!sMappings) { 47 | [NSURLProtocol registerClass: self]; 48 | sMappings = [[NSMutableDictionary alloc] init]; 49 | } 50 | if (!sMappedHosts) 51 | sMappedHosts = [[NSMutableSet alloc] init]; 52 | sMappings[baseURL.absoluteString] = wsURL; 53 | [sMappedHosts addObject: baseURL.host]; 54 | } 55 | } 56 | 57 | 58 | static inline bool urlPrefixMatch(NSString* prefix, NSString* urlString) { 59 | if (![urlString hasPrefix: prefix]) 60 | return false; 61 | if (urlString.length == prefix.length || [prefix hasSuffix: @"/"]) 62 | return true; 63 | // Make sure there's a path component boundary after the prefix: 64 | unichar nextChar = nextChar = [urlString characterAtIndex: prefix.length]; 65 | return (nextChar == '/' || nextChar == '?' || nextChar == '#'); 66 | } 67 | 68 | 69 | // Returns the HTTP URL to use to connect to the WebSocket server. 70 | + (NSURL*) webSocketURLForURL: (NSURL*)url { 71 | @synchronized(self) { 72 | if (![sMappedHosts containsObject: url.host]) // quick shortcut 73 | return nil; 74 | NSString* urlString = [url absoluteString]; 75 | for (NSString* prefix in sMappings) { 76 | if (urlPrefixMatch(prefix, urlString)) 77 | return sMappings[prefix]; 78 | } 79 | return nil; 80 | } 81 | } 82 | 83 | 84 | #pragma mark - INITIALIZATION: 85 | 86 | 87 | + (BOOL)canInitWithRequest:(NSURLRequest *)request { 88 | return [self webSocketURLForURL: request.URL] != nil; 89 | } 90 | 91 | 92 | + (NSURLRequest*) canonicalRequestForRequest: (NSURLRequest*)request { 93 | return request; 94 | } 95 | 96 | 97 | - (instancetype) initWithRequest:(NSURLRequest *)request 98 | cachedResponse:(NSCachedURLResponse *)cachedResponse 99 | client:(id )client 100 | { 101 | self = [super initWithRequest: request cachedResponse: cachedResponse client:client]; 102 | if (self) { 103 | _webSocketURL = [[self class] webSocketURLForURL: self.request.URL]; 104 | if (!_webSocketURL) { 105 | return nil; 106 | } 107 | } 108 | return self; 109 | } 110 | 111 | 112 | 113 | - (void) startLoading { 114 | if (!sSockets) 115 | sSockets = [[BLIPPool alloc] initWithDelegate: nil 116 | dispatchQueue: dispatch_get_current_queue()]; 117 | NSError* error; 118 | BLIPWebSocket* socket = [sSockets socketToURL: _webSocketURL error: &error]; 119 | if (!socket) { 120 | [self.client URLProtocol: self didFailWithError: error]; 121 | return; 122 | } 123 | _response = [socket sendRequest: [BLIPRequest requestWithHTTPRequest: self.request]]; 124 | _response.dataDelegate = self; 125 | } 126 | 127 | 128 | - (void)stopLoading { 129 | // The Obj-C BLIP API has no way to stop a request, so just ignore its data: 130 | _response.dataDelegate = nil; 131 | _response = nil; 132 | } 133 | 134 | 135 | - (void) blipMessage: (BLIPMessage*)msg didReceiveData: (NSData*)data { 136 | id client = self.client; 137 | NSError* error = _response.error; 138 | if (error) { 139 | [client URLProtocol: self didFailWithError: error]; 140 | return; 141 | } 142 | 143 | if (!_gotHeaders) { 144 | if (!_responseBody) 145 | _responseBody = [data mutableCopy]; 146 | else 147 | [_responseBody appendData: data]; 148 | 149 | NSData* body = nil; 150 | NSURLResponse* response = [_response asHTTPResponseWithBody: &body 151 | forURL: self.request.URL]; 152 | if (response) { 153 | _gotHeaders = YES; 154 | [client URLProtocol: self didReceiveResponse: response 155 | cacheStoragePolicy: NSURLCacheStorageNotAllowed]; 156 | } 157 | data = body; 158 | } 159 | 160 | if (data.length > 0) 161 | [client URLProtocol: self didLoadData: data]; 162 | 163 | if (msg.complete) 164 | [client URLProtocolDidFinishLoading: self]; 165 | } 166 | 167 | 168 | @end 169 | -------------------------------------------------------------------------------- /BLIP/BLIPMessage.h: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPMessage.h 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 5/10/08. 6 | // Copyright 2008-2013 Jens Alfke. 7 | // Copyright (c) 2013 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)(id); 50 | 51 | /** Called after message data is sent over the socket. */ 52 | @property (strong) void (^onDataSent)(uint64_t totalBytesSent); 53 | 54 | /** Called when the message has been completely sent over the socket. */ 55 | @property (strong) void (^onSent)(); 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 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/BLIPMessage.m: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPMessage.m 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 5/10/08. 6 | // Copyright 2008-2013 Jens Alfke. 7 | // Copyright (c) 2013 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 "Logging.h" 22 | #import "Test.h" 23 | #import "ExceptionUtils.h" 24 | #import "MYData.h" 25 | #import "MYBuffer.h" 26 | 27 | // From Google Toolbox For Mac 28 | #import "GTMNSData+zlib.h" 29 | 30 | 31 | NSString* const BLIPErrorDomain = @"BLIP"; 32 | 33 | NSError *BLIPMakeError( int errorCode, NSString *message, ... ) { 34 | va_list args; 35 | va_start(args,message); 36 | message = [[NSString alloc] initWithFormat: message arguments: args]; 37 | va_end(args); 38 | LogTo(BLIP,@"BLIPError #%i: %@",errorCode,message); 39 | NSDictionary *userInfo = @{NSLocalizedDescriptionKey: message}; 40 | return [NSError errorWithDomain: BLIPErrorDomain code: errorCode userInfo: userInfo]; 41 | } 42 | 43 | 44 | @implementation BLIPMessage 45 | 46 | 47 | @synthesize onDataReceived=_onDataReceived, onDataSent=_onDataSent, onSent=_onSent; 48 | 49 | 50 | - (instancetype) _initWithConnection: (BLIPConnection*)connection 51 | isMine: (BOOL)isMine 52 | flags: (BLIPMessageFlags)flags 53 | number: (UInt32)msgNo 54 | body: (NSData*)body 55 | { 56 | self = [super init]; 57 | if (self != nil) { 58 | _connection = connection; 59 | _isMine = isMine; 60 | _isMutable = isMine; 61 | _flags = flags; 62 | _number = msgNo; 63 | if (isMine) { 64 | _body = body.copy; 65 | _properties = [[NSMutableDictionary alloc] init]; 66 | _propertiesAvailable = YES; 67 | _complete = YES; 68 | } else { 69 | _encodedBody = [[MYBuffer alloc] initWithData: body]; 70 | } 71 | LogTo(BLIPLifecycle,@"INIT %@",self); 72 | } 73 | return self; 74 | } 75 | 76 | #if DEBUG 77 | - (void) dealloc { 78 | LogTo(BLIPLifecycle,@"DEALLOC %@",self); 79 | } 80 | #endif 81 | 82 | 83 | - (NSString*) description { 84 | NSUInteger length = (_body.length ?: _mutableBody.length) ?: _encodedBody.minLength; 85 | NSMutableString *desc = [NSMutableString stringWithFormat: @"%@[#%u, %lu bytes", 86 | self.class,(unsigned int)_number, (unsigned long)length]; 87 | if (_flags & kBLIP_Compressed) { 88 | if (_encodedBody && _encodedBody.minLength != length) 89 | [desc appendFormat: @" (%lu gzipped)", (unsigned long)_encodedBody.minLength]; 90 | else 91 | [desc appendString: @", gzipped"]; 92 | } 93 | if (_flags & kBLIP_Urgent) 94 | [desc appendString: @", urgent"]; 95 | if (_flags & kBLIP_NoReply) 96 | [desc appendString: @", noreply"]; 97 | if (_flags & kBLIP_Meta) 98 | [desc appendString: @", META"]; 99 | if (_flags & kBLIP_MoreComing) 100 | [desc appendString: @", incomplete"]; 101 | [desc appendString: @"]"]; 102 | return desc; 103 | } 104 | 105 | - (NSString*) descriptionWithProperties { 106 | NSMutableString *desc = (NSMutableString*)self.description; 107 | [desc appendFormat: @" %@", self.properties]; 108 | return desc; 109 | } 110 | 111 | 112 | #pragma mark - 113 | #pragma mark PROPERTIES & METADATA: 114 | 115 | 116 | @synthesize connection=_connection, number=_number, isMine=_isMine, isMutable=_isMutable, 117 | _bytesWritten, sent=_sent, propertiesAvailable=_propertiesAvailable, complete=_complete, 118 | representedObject=_representedObject; 119 | 120 | 121 | - (void) _setFlag: (BLIPMessageFlags)flag value: (BOOL)value { 122 | Assert(_isMine && _isMutable); 123 | if (value) 124 | _flags |= flag; 125 | else 126 | _flags &= ~flag; 127 | } 128 | 129 | - (BLIPMessageFlags) _flags {return _flags;} 130 | 131 | - (BOOL) isRequest {return (_flags & kBLIP_TypeMask) == kBLIP_MSG;} 132 | - (BOOL) compressed {return (_flags & kBLIP_Compressed) != 0;} 133 | - (BOOL) urgent {return (_flags & kBLIP_Urgent) != 0;} 134 | - (void) setCompressed: (BOOL)compressed {[self _setFlag: kBLIP_Compressed value: compressed];} 135 | - (void) setUrgent: (BOOL)high {[self _setFlag: kBLIP_Urgent value: high];} 136 | 137 | 138 | - (NSData*) body { 139 | if (! _body && _isMine) 140 | return [_mutableBody copy]; 141 | else 142 | return _body; 143 | } 144 | 145 | - (void) setBody: (NSData*)body { 146 | Assert(_isMine && _isMutable); 147 | if (_mutableBody) 148 | [_mutableBody setData: body]; 149 | else 150 | _mutableBody = [body mutableCopy]; 151 | } 152 | 153 | - (void) _addToBody: (NSData*)data { 154 | if (data.length) { 155 | if (_mutableBody) 156 | [_mutableBody appendData: data]; 157 | else 158 | _mutableBody = [data mutableCopy]; 159 | _body = nil; 160 | } 161 | } 162 | 163 | - (void) addToBody: (NSData*)data { 164 | Assert(_isMine && _isMutable); 165 | [self _addToBody: data]; 166 | } 167 | 168 | - (void) addStreamToBody:(NSInputStream *)stream { 169 | if (!_bodyStreams) 170 | _bodyStreams = [NSMutableArray new]; 171 | [_bodyStreams addObject: stream]; 172 | } 173 | 174 | 175 | - (NSString*) bodyString { 176 | NSData *body = self.body; 177 | if (body) 178 | return [[NSString alloc] initWithData: body encoding: NSUTF8StringEncoding]; 179 | else 180 | return nil; 181 | } 182 | 183 | - (void) setBodyString: (NSString*)string { 184 | self.body = [string dataUsingEncoding: NSUTF8StringEncoding]; 185 | self.contentType = @"text/plain; charset=UTF-8"; 186 | } 187 | 188 | 189 | - (id) bodyJSON { 190 | NSData* body = self.body; 191 | if (body.length == 0) 192 | return nil; 193 | NSError* error; 194 | id jsonObj = [NSJSONSerialization JSONObjectWithData: body 195 | options: NSJSONReadingAllowFragments 196 | error: &error]; 197 | if (!jsonObj) 198 | Warn(@"Couldn't parse %@ body as JSON: %@", self, error.localizedFailureReason); 199 | return jsonObj; 200 | } 201 | 202 | 203 | - (void) setBodyJSON: (id)jsonObj { 204 | NSError* error; 205 | NSData* body = [NSJSONSerialization dataWithJSONObject: jsonObj options: 0 error: &error]; 206 | Assert(body, @"Couldn't encode as JSON: %@", error.localizedFailureReason); 207 | self.body = body; 208 | self.contentType = @"application/json"; 209 | self.compressed = (body.length > 100); 210 | } 211 | 212 | 213 | - (NSDictionary*) properties { 214 | return _properties; 215 | } 216 | 217 | - (NSMutableDictionary*) mutableProperties { 218 | Assert(_isMine && _isMutable); 219 | return (NSMutableDictionary*)_properties; 220 | } 221 | 222 | - (NSString*) objectForKeyedSubscript: (NSString*)key { 223 | return _properties[key]; 224 | } 225 | 226 | - (void) setObject: (NSString*)value forKeyedSubscript:(NSString*)key { 227 | [self.mutableProperties setValue: value forKey: key]; 228 | } 229 | 230 | 231 | - (NSString*) contentType {return self[@"Content-Type"];} 232 | - (void) setContentType: (NSString*)t {self[@"Content-Type"] = t;} 233 | - (NSString*) profile {return self[@"Profile"];} 234 | - (void) setProfile: (NSString*)p {self[@"Profile"] = p;} 235 | 236 | 237 | #pragma mark - 238 | #pragma mark I/O: 239 | 240 | 241 | - (void) _encode { 242 | Assert(_isMine && _isMutable); 243 | _isMutable = NO; 244 | 245 | NSDictionary *oldProps = _properties; 246 | _properties = [oldProps copy]; 247 | 248 | _encodedBody = [[MYBuffer alloc] initWithData: BLIPEncodeProperties(_properties)]; 249 | Assert(_encodedBody.maxLength > 0); 250 | 251 | NSData *body = _body ?: _mutableBody; 252 | NSUInteger length = body.length; 253 | if (length > 0) { 254 | if (self.compressed) 255 | body = [NSData gtm_dataByGzippingData: body compressionLevel: 5]; 256 | [_encodedBody writeData: body]; 257 | } 258 | for (NSInputStream* stream in _bodyStreams) 259 | [_encodedBody writeContentsOfStream: stream]; 260 | _bodyStreams = nil; 261 | } 262 | 263 | 264 | - (void) _assignedNumber: (UInt32)number { 265 | Assert(_number==0,@"%@ has already been sent",self); 266 | _number = number; 267 | _isMutable = NO; 268 | } 269 | 270 | 271 | // Generates the next outgoing frame. 272 | - (NSData*) nextFrameWithMaxSize: (UInt16)maxSize moreComing: (BOOL*)outMoreComing { 273 | Assert(_number!=0); 274 | Assert(_isMine); 275 | Assert(_encodedBody); 276 | *outMoreComing = NO; 277 | if (_bytesWritten==0) 278 | LogTo(BLIP,@"Now sending %@",self); 279 | size_t headerSize = MYLengthOfVarUInt(_number) + MYLengthOfVarUInt(_flags); 280 | 281 | // Allocate frame and read bytes from body into it: 282 | NSUInteger frameSize = MIN(headerSize + _encodedBody.maxLength, maxSize); 283 | NSMutableData* frame = [NSMutableData dataWithLength: frameSize]; 284 | ssize_t bytesRead = [_encodedBody readBytes: (uint8_t*)frame.mutableBytes + headerSize 285 | maxLength: frameSize - headerSize]; 286 | if (bytesRead < 0) 287 | return nil; 288 | frame.length = headerSize + bytesRead; 289 | _bytesWritten += bytesRead; 290 | 291 | // Write the header: 292 | if (_encodedBody.atEnd) { 293 | _flags &= ~kBLIP_MoreComing; 294 | } else { 295 | _flags |= kBLIP_MoreComing; 296 | *outMoreComing = YES; 297 | } 298 | void* pos = MYEncodeVarUInt(frame.mutableBytes, _number); 299 | MYEncodeVarUInt(pos, _flags); 300 | 301 | LogTo(BLIPVerbose,@"%@ pushing frame, bytes %lu-%lu%@", self, 302 | (unsigned long)_bytesWritten-bytesRead, (unsigned long)_bytesWritten, 303 | (*outMoreComing ? @"" : @" (finished)")); 304 | if (_onDataSent) 305 | _onDataSent(_bytesWritten); 306 | if (!*outMoreComing) 307 | self.complete = YES; 308 | return frame; 309 | } 310 | 311 | 312 | // Parses the next incoming frame. 313 | - (BOOL) _receivedFrameWithFlags: (BLIPMessageFlags)flags body: (NSData*)frameBody { 314 | Assert(!_isMine); 315 | Assert(_flags & kBLIP_MoreComing); 316 | 317 | if (!self.isRequest) 318 | _flags = flags | kBLIP_MoreComing; 319 | 320 | _bytesReceived += frameBody.length; 321 | if (!_encodedBody) 322 | _encodedBody = [[MYBuffer alloc] init]; 323 | [_encodedBody writeData: frameBody]; 324 | LogTo(BLIPVerbose,@"%@ rcvd bytes %lu-%lu, flags=%x", 325 | self, (unsigned long)_bytesReceived-frameBody.length, (unsigned long)_bytesReceived, flags); 326 | 327 | if (! _properties) { 328 | // Try to extract the properties: 329 | BOOL complete; 330 | _properties = BLIPReadPropertiesFromBuffer(_encodedBody, &complete); 331 | if (_properties) { 332 | self.propertiesAvailable = YES; 333 | [_connection _messageReceivedProperties: self]; 334 | } else if (complete) { 335 | return NO; 336 | } 337 | } 338 | 339 | void (^onDataReceived)(id) = (_properties && !self.compressed) ? _onDataReceived : nil; 340 | if (onDataReceived) { 341 | LogTo(BLIPVerbose, @"%@ -> calling onDataReceived(%lu bytes)", self, (unsigned long)frameBody.length); 342 | onDataReceived(_encodedBody); 343 | } 344 | 345 | if (! (flags & kBLIP_MoreComing)) { 346 | // After last frame, decode the data: 347 | _flags &= ~kBLIP_MoreComing; 348 | if (! _properties) 349 | return NO; 350 | _body = _encodedBody.flattened; 351 | _encodedBody = nil; 352 | NSUInteger encodedLength = _body.length; 353 | if (self.compressed && encodedLength>0) { 354 | _body = [[NSData gtm_dataByInflatingData: _body] copy]; 355 | if (! _body) { 356 | Warn(@"Failed to decompress %@", self); 357 | return NO; 358 | } 359 | LogTo(BLIPVerbose,@"Uncompressed %@ from %lu bytes (%.1fx)", self, (unsigned long)encodedLength, 360 | _body.length/(double)encodedLength); 361 | if (_onDataReceived) { 362 | MYBuffer* buffer = [[MYBuffer alloc] initWithData: _body]; 363 | _onDataReceived(buffer); 364 | _body = buffer.flattened; 365 | } 366 | } 367 | _onDataReceived = nil; 368 | self.propertiesAvailable = self.complete = YES; 369 | } 370 | 371 | return YES; 372 | } 373 | 374 | 375 | - (void) _connectionClosed { 376 | if (_isMine) { 377 | _bytesWritten = 0; 378 | _flags |= kBLIP_MoreComing; 379 | } 380 | } 381 | 382 | 383 | @end 384 | 385 | 386 | /* 387 | Copyright (c) 2008-2013, Jens Alfke . All rights reserved. 388 | 389 | Redistribution and use in source and binary forms, with or without modification, are permitted 390 | provided that the following conditions are met: 391 | 392 | * Redistributions of source code must retain the above copyright notice, this list of conditions 393 | and the following disclaimer. 394 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions 395 | and the following disclaimer in the documentation and/or other materials provided with the 396 | distribution. 397 | 398 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 399 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 400 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRI- 401 | BUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 402 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 403 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 404 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 405 | THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 406 | */ 407 | -------------------------------------------------------------------------------- /BLIP/BLIPPool.h: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPPool.h 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 9/16/13. 6 | // Copyright (c) 2013 Couchbase. All rights reserved. 7 | 8 | #import "BLIPWebSocketConnection.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 | - (BLIPWebSocketConnection*) socketToURL: (NSURL*)url error: (NSError**)outError; 23 | 24 | /** Returns an already-open BLIPWebSocketConnection to the given URL, or nil if there is none. */ 25 | - (BLIPWebSocketConnection*) 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:(WebSocketCloseCode)code reason:(NSString *)reason; 32 | 33 | @end 34 | -------------------------------------------------------------------------------- /BLIP/BLIPPool.m: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPPool.m 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 9/16/13. 6 | // Copyright (c) 2013 Couchbase. 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 "BLIPPool.h" 17 | #import "BLIPWebSocketConnection.h" 18 | 19 | 20 | @interface BLIPPool () 21 | @end 22 | 23 | 24 | @implementation BLIPPool 25 | { 26 | __weak id _delegate; 27 | dispatch_queue_t _queue; 28 | NSMutableDictionary* _sockets; 29 | } 30 | 31 | 32 | @synthesize delegate=_delegate; 33 | 34 | 35 | - (instancetype) initWithDelegate: (id)delegate 36 | dispatchQueue: (dispatch_queue_t)queue 37 | { 38 | self = [super init]; 39 | if (self) { 40 | _delegate = delegate; 41 | _queue = queue; 42 | _sockets = [[NSMutableDictionary alloc] init]; 43 | } 44 | return self; 45 | } 46 | 47 | 48 | - (void) dealloc { 49 | [self closeWithCode: kWebSocketCloseGoingAway reason: nil]; 50 | } 51 | 52 | 53 | // Returns an already-open BLIPWebSocketConnection to use to communicate with a given URL. 54 | - (BLIPWebSocketConnection*) existingSocketToURL: (NSURL*)url error: (NSError**)outError { 55 | @synchronized(self) { 56 | return _sockets[url]; 57 | } 58 | } 59 | 60 | 61 | // Returns an open BLIPWebSocketConnection to use to communicate with a given URL. 62 | - (BLIPWebSocketConnection*) socketToURL: (NSURL*)url error: (NSError**)outError { 63 | @synchronized(self) { 64 | BLIPWebSocketConnection* socket = _sockets[url]; 65 | if (!socket) { 66 | if (!_sockets) { 67 | // I'm closed already 68 | if (outError) 69 | *outError = nil; 70 | return nil; 71 | } 72 | socket = [[BLIPWebSocketConnection alloc] initWithURL: url]; 73 | [socket setDelegate: self queue: _queue]; 74 | if (![socket connect: outError]) 75 | return nil; 76 | _sockets[url] = socket; 77 | } 78 | return socket; 79 | } 80 | } 81 | 82 | 83 | - (void) forgetSocket: (BLIPConnection*)webSocket { 84 | @synchronized(self) { 85 | [_sockets removeObjectForKey: webSocket.URL]; 86 | } 87 | } 88 | 89 | 90 | - (void) closeWithCode:(WebSocketCloseCode)code reason:(NSString *)reason { 91 | NSDictionary* sockets; 92 | @synchronized(self) { 93 | sockets = _sockets; 94 | _sockets = nil; // marks that I'm closed 95 | } 96 | for (NSURL* url in sockets) { 97 | BLIPWebSocketConnection* socket = sockets[url]; 98 | [socket closeWithCode: code reason: reason]; 99 | } 100 | } 101 | 102 | - (void) close { 103 | [self closeWithCode: kWebSocketCloseNormal reason: nil]; 104 | } 105 | 106 | 107 | #pragma mark - DELEGATE API: 108 | 109 | 110 | // These forward to the delegate, and didClose/didFail also forget the socket: 111 | 112 | 113 | - (void)blipConnectionDidOpen:(BLIPConnection*)webSocket { 114 | id delegate = _delegate; 115 | if ([delegate respondsToSelector: @selector(blipConnectionDidOpen:)]) 116 | [delegate blipConnectionDidOpen: webSocket]; 117 | } 118 | 119 | - (void)blipConnection: (BLIPConnection*)webSocket didFailWithError:(NSError *)error { 120 | [self forgetSocket: webSocket]; 121 | id delegate = _delegate; 122 | if ([delegate respondsToSelector: @selector(blipConnection:didFailWithError:)]) 123 | [delegate blipConnection: webSocket didFailWithError: error]; 124 | } 125 | 126 | - (void)blipConnection: (BLIPConnection*)webSocket 127 | didCloseWithError: (NSError*)error 128 | { 129 | [self forgetSocket: webSocket]; 130 | id delegate = _delegate; 131 | if ([delegate respondsToSelector: @selector(blipConnection:didCloseWithError:)]) 132 | [delegate blipConnection: webSocket didCloseWithError: error]; 133 | } 134 | 135 | - (BOOL) blipConnection: (BLIPConnection*)webSocket receivedRequest: (BLIPRequest*)request { 136 | id delegate = _delegate; 137 | return [delegate respondsToSelector: @selector(blipConnection:receivedRequest:)] 138 | && [delegate blipConnection: webSocket receivedRequest: request]; 139 | } 140 | 141 | - (void) blipConnection: (BLIPConnection*)webSocket receivedResponse: (BLIPResponse*)response { 142 | id delegate = _delegate; 143 | if ([delegate respondsToSelector: @selector(blipConnection:receivedResponse:)]) 144 | [delegate blipConnection: webSocket receivedResponse: response]; 145 | } 146 | 147 | 148 | @end 149 | -------------------------------------------------------------------------------- /BLIP/BLIPProperties.h: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPProperties.h 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 5/13/08. 6 | // Copyright 2008-2013 Jens Alfke. 7 | // Copyright (c) 2013 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 | -------------------------------------------------------------------------------- /BLIP/BLIPProperties.m: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPProperties.m 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 5/13/08. 6 | // Copyright 2008-2013 Jens Alfke. 7 | // Copyright (c) 2013 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 "Logging.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 nil; 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 | key = kAbbreviations[first-1]; 61 | } 62 | return [NSString stringWithUTF8String: key]; 63 | } 64 | 65 | 66 | NSDictionary* BLIPParseProperties(MYSlice *data, BOOL* complete) { 67 | MYSlice slice = *data; 68 | uint64_t length; 69 | if (!MYSliceReadVarUInt(&slice, &length) || slice.length < length) { 70 | *complete = NO; 71 | return nil; 72 | } 73 | *complete = YES; 74 | if (length == 0) { 75 | MYSliceMoveStart(data, 1); 76 | return @{}; 77 | } 78 | MYSlice buf = MYMakeSlice(slice.bytes, (size_t)length); 79 | if (((const char*)slice.bytes)[buf.length - 1] != '\0') 80 | return nil; // checking for nul at end makes it safe to use strlen in readCString 81 | NSMutableDictionary* result = [NSMutableDictionary new]; 82 | while (buf.length > 0) { 83 | NSString* key = readCString(&buf); 84 | if (!key) 85 | return nil; 86 | NSString* value = readCString(&buf); 87 | if (!value) 88 | return nil; 89 | result[key] = value; 90 | } 91 | MYSliceMoveStartTo(data, buf.bytes); 92 | return result; 93 | } 94 | 95 | 96 | NSDictionary* BLIPReadPropertiesFromBuffer(MYBuffer* buffer, BOOL *complete) { 97 | MYSlice slice = buffer.flattened.my_asSlice; 98 | MYSlice readSlice = slice; 99 | NSDictionary* props = BLIPParseProperties(&readSlice, complete); 100 | if (props) 101 | [buffer readSliceOfMaxLength: slice.length - readSlice.length]; 102 | return props; 103 | } 104 | 105 | 106 | static void appendStr( NSMutableData *data, NSString *str ) { 107 | const char *utf8 = [str UTF8String]; 108 | size_t size = strlen(utf8)+1; 109 | for (uint8_t i=0; i. All rights reserved. 138 | 139 | Redistribution and use in source and binary forms, with or without modification, are permitted 140 | provided that the following conditions are met: 141 | 142 | * Redistributions of source code must retain the above copyright notice, this list of conditions 143 | and the following disclaimer. 144 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions 145 | and the following disclaimer in the documentation and/or other materials provided with the 146 | distribution. 147 | 148 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 149 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 150 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRI- 151 | BUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 152 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 153 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 154 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 155 | THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 156 | */ 157 | -------------------------------------------------------------------------------- /BLIP/BLIPRequest+HTTP.h: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPRequest+HTTP.h 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 4/15/13. 6 | // Copyright (c) 2013 Couchbase, Inc. All rights reserved. 7 | 8 | #import "BLIPRequest.h" 9 | #import "BLIPResponse.h" 10 | 11 | 12 | /** Methods for converting a BLIPRequest to and from an HTTP NSURLRequest. */ 13 | @interface BLIPRequest (HTTP) 14 | 15 | // Creates a BLIPRequest from an NSURLRequest. 16 | + (instancetype) requestWithHTTPRequest: (NSURLRequest*)httpRequest; 17 | 18 | // Creates an NSURLRequest from a BLIPRequest. 19 | - (NSURLRequest*) asHTTPRequest; 20 | 21 | @end 22 | 23 | 24 | /** Methods for converting a BLIPResponse to and from an NSHTTPURLResponse. */ 25 | @interface BLIPResponse (HTTP) 26 | 27 | // Stores an HTTP response into a BLIPResponse. 28 | - (void) setHTTPResponse: (NSHTTPURLResponse*)httpResponse 29 | withBody: (NSData*)httpBody; 30 | 31 | // Creates an HTTP response from a BLIPResponse. 32 | - (NSHTTPURLResponse*) asHTTPResponseWithBody: (NSData**)outHTTPBody 33 | forURL: (NSURL*)url; 34 | 35 | @end 36 | -------------------------------------------------------------------------------- /BLIP/BLIPRequest+HTTP.m: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPRequest+HTTP.m 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 4/15/13. 6 | // Copyright (c) 2013 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 "BLIPRequest+HTTP.h" 17 | #import "BLIP_Internal.h" 18 | #import "Test.h" 19 | 20 | 21 | @implementation BLIPRequest (HTTP) 22 | 23 | 24 | static NSSet* kIgnoredHeaders; 25 | 26 | 27 | + (instancetype) requestWithHTTPRequest: (NSURLRequest*)req { 28 | if (!kIgnoredHeaders) { 29 | kIgnoredHeaders = [NSSet setWithObjects: @"host", nil]; 30 | } 31 | CFHTTPMessageRef msg = CFHTTPMessageCreateRequest(NULL, 32 | (__bridge CFStringRef)req.HTTPMethod, 33 | (__bridge CFURLRef)req.URL, 34 | kCFHTTPVersion1_1); 35 | NSDictionary* headers = req.allHTTPHeaderFields; 36 | for (NSString* header in headers) { 37 | if (![kIgnoredHeaders member: header.lowercaseString]) { 38 | CFHTTPMessageSetHeaderFieldValue(msg, (__bridge CFStringRef)header, 39 | (__bridge CFStringRef)headers[header]); 40 | } 41 | } 42 | CFHTTPMessageSetBody(msg, (__bridge CFDataRef)req.HTTPBody); 43 | 44 | NSData* body = CFBridgingRelease(CFHTTPMessageCopySerializedMessage(msg)); 45 | CFRelease(msg); 46 | return [self requestWithBody: body 47 | properties: @{@"Profile": @"HTTP"}]; 48 | } 49 | 50 | 51 | - (NSURLRequest*) asHTTPRequest { 52 | NSMutableURLRequest* request = nil; 53 | CFHTTPMessageRef msg = CFHTTPMessageCreateEmpty(NULL, true); 54 | if (CFHTTPMessageAppendBytes(msg, self.body.bytes, self.body.length) && 55 | CFHTTPMessageIsHeaderComplete(msg)) { 56 | NSURL* url = CFBridgingRelease(CFHTTPMessageCopyRequestURL(msg)); 57 | request = [[NSMutableURLRequest alloc] initWithURL: url]; 58 | request.HTTPMethod = CFBridgingRelease(CFHTTPMessageCopyRequestMethod(msg)); 59 | request.allHTTPHeaderFields = CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(msg)); 60 | request.HTTPBody = CFBridgingRelease(CFHTTPMessageCopyBody(msg)); 61 | } 62 | CFRelease(msg); 63 | return request; 64 | } 65 | 66 | 67 | @end 68 | 69 | 70 | 71 | 72 | 73 | @implementation BLIPResponse (HTTP) 74 | 75 | - (void) setHTTPResponse: (NSHTTPURLResponse*)httpResponse 76 | withBody: (NSData*)httpBody 77 | { 78 | NSInteger status = httpResponse.statusCode; 79 | NSString* statusDesc = [NSHTTPURLResponse localizedStringForStatusCode: status]; 80 | CFHTTPMessageRef msg = CFHTTPMessageCreateResponse(NULL, 81 | status, 82 | (__bridge CFStringRef)statusDesc, 83 | kCFHTTPVersion1_1); 84 | NSDictionary* headers = httpResponse.allHeaderFields; 85 | for (NSString* header in headers) { 86 | CFHTTPMessageSetHeaderFieldValue(msg, (__bridge CFStringRef)header, 87 | (__bridge CFStringRef)headers[header]); 88 | } 89 | CFHTTPMessageSetBody(msg, (__bridge CFDataRef)httpBody); 90 | self.profile = @"HTTP"; 91 | self.body = CFBridgingRelease(CFHTTPMessageCopySerializedMessage(msg)); 92 | CFRelease(msg); 93 | } 94 | 95 | 96 | - (NSHTTPURLResponse*) asHTTPResponseWithBody: (NSData**)outHTTPBody forURL: (NSURL*)url { 97 | NSHTTPURLResponse* response = nil; 98 | CFHTTPMessageRef msg = CFHTTPMessageCreateEmpty(NULL, false); 99 | if (CFHTTPMessageAppendBytes(msg, self.body.bytes, self.body.length) && 100 | CFHTTPMessageIsHeaderComplete(msg)) { 101 | response = [[NSHTTPURLResponse alloc] 102 | initWithURL: url 103 | statusCode: CFHTTPMessageGetResponseStatusCode(msg) 104 | HTTPVersion: @"HTTP/1.1" 105 | headerFields: CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(msg))]; 106 | if (outHTTPBody) 107 | *outHTTPBody = CFBridgingRelease(CFHTTPMessageCopyBody(msg)); 108 | } 109 | CFRelease(msg); 110 | return response; 111 | } 112 | 113 | @end 114 | 115 | 116 | #if DEBUG 117 | 118 | TestCase(HTTPRequest) { 119 | NSURL* url = [NSURL URLWithString:@"http://example.org/some/path?query=value"]; 120 | NSMutableURLRequest* httpReq = [NSMutableURLRequest requestWithURL: url]; 121 | httpReq.HTTPMethod = @"PUT"; 122 | [httpReq setValue: @"application/json" forHTTPHeaderField: @"Content-Type"]; 123 | httpReq.HTTPBody = [@"{\"foo\"=23}" dataUsingEncoding: NSUTF8StringEncoding]; 124 | BLIPRequest* blipReq = [BLIPRequest requestWithHTTPRequest: httpReq]; 125 | 126 | CAssert(blipReq != nil); 127 | CAssertEqual(blipReq.profile, @"HTTP"); 128 | NSString* expectedBody = @"PUT /some/path?query=value HTTP/1.1\r\nContent-Type: application/json\r\n\r\n{\"foo\"=23}"; 129 | CAssertEqual([[NSString alloc] initWithData: blipReq.body encoding: NSUTF8StringEncoding], 130 | expectedBody); 131 | 132 | NSURLRequest* httpReq2 = [blipReq asHTTPRequest]; 133 | CAssert(httpReq2 != nil); 134 | CAssertEqual(httpReq2.HTTPMethod, @"PUT"); 135 | CAssertEqual(httpReq2.URL.path, @"/some/path"); 136 | CAssertEqual(httpReq2.URL.query, @"query=value"); 137 | CAssertEqual(httpReq2.allHTTPHeaderFields, @{@"Content-Type": @"application/json"}); 138 | CAssertEqual([[NSString alloc] initWithData: httpReq.HTTPBody encoding: NSUTF8StringEncoding], 139 | @"{\"foo\"=23}"); 140 | } 141 | 142 | 143 | TestCase(httpResponse) { 144 | NSURL* url = [NSURL URLWithString:@"http://example.org/some/path?query=value"]; 145 | NSString* bodyString = @"HTTP/1.1 201 Created\r\nLocation: /foo\r\n\r\nBody goes here"; 146 | BLIPResponse* blipRes = [[BLIPResponse alloc] _initIncomingWithProperties: nil 147 | body: [bodyString dataUsingEncoding: NSUTF8StringEncoding]]; 148 | 149 | NSData* body; 150 | NSHTTPURLResponse* httpRes = [blipRes asHTTPResponseWithBody: &body forURL: url]; 151 | CAssert(httpRes != nil); 152 | CAssertEq(httpRes.statusCode, 201); 153 | CAssertEqual(httpRes.allHeaderFields, @{@"Location": @"/foo"}); 154 | CAssertEqual([[NSString alloc] initWithData: body encoding: NSUTF8StringEncoding], 155 | @"Body goes here"); 156 | } 157 | 158 | TestCase(HTTP) { 159 | RequireTestCase(HTTPRequest); 160 | RequireTestCase(httpResponse); 161 | } 162 | 163 | #endif 164 | -------------------------------------------------------------------------------- /BLIP/BLIPRequest.h: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPRequest.h 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 5/22/08. 6 | // Copyright 2008-2013 Jens Alfke. 7 | // Copyright (c) 2013 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/BLIPRequest.m: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPRequest.m 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 5/22/08. 6 | // Copyright 2008-2013 Jens Alfke. 7 | // Copyright (c) 2013 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 "Logging.h" 22 | #import "Test.h" 23 | #import "ExceptionUtils.h" 24 | 25 | 26 | @implementation BLIPRequest 27 | { 28 | BLIPResponse *_response; 29 | } 30 | 31 | 32 | - (instancetype) _initWithConnection: (BLIPConnection*)connection 33 | body: (NSData*)body 34 | properties: (NSDictionary*)properties 35 | { 36 | self = [self _initWithConnection: connection 37 | isMine: YES 38 | flags: kBLIP_MSG 39 | number: 0 40 | body: body]; 41 | if (self) { 42 | if (body) 43 | self.body = body; 44 | if (properties) 45 | _properties = [properties copy]; 46 | } 47 | return self; 48 | } 49 | 50 | + (BLIPRequest*) requestWithBody: (NSData*)body { 51 | return [[self alloc] _initWithConnection: nil body: body properties: nil]; 52 | } 53 | 54 | + (BLIPRequest*) requestWithBodyString: (NSString*)bodyString { 55 | return [self requestWithBody: [bodyString dataUsingEncoding: NSUTF8StringEncoding]]; 56 | } 57 | 58 | + (BLIPRequest*) requestWithBody: (NSData*)body 59 | properties: (NSDictionary*)properties 60 | { 61 | return [[self alloc] _initWithConnection: nil body: body properties: properties]; 62 | } 63 | 64 | - (id)mutableCopyWithZone:(NSZone *)zone { 65 | Assert(self.complete); 66 | BLIPRequest *copy = [[self class] requestWithBody: self.body 67 | properties: self.properties]; 68 | copy.compressed = self.compressed; 69 | copy.urgent = self.urgent; 70 | copy.noReply = self.noReply; 71 | return copy; 72 | } 73 | 74 | 75 | - (BOOL) noReply {return (_flags & kBLIP_NoReply) != 0;} 76 | - (void) setNoReply: (BOOL)noReply {[self _setFlag: kBLIP_NoReply value: noReply];} 77 | - (BLIPConnection*) connection {return _connection;} 78 | 79 | - (void) setConnection: (BLIPConnection*)conn { 80 | Assert(_isMine && !_sent,@"Connection can only be set before sending"); 81 | _connection = conn; 82 | } 83 | 84 | 85 | - (BLIPResponse*) send { 86 | Assert(_connection,@"%@ has no connection to send over",self); 87 | Assert(!_sent,@"%@ was already sent",self); 88 | [self _encode]; 89 | BLIPResponse *response = self.response; 90 | if ([_connection _sendRequest: self response: response]) 91 | self.sent = YES; 92 | else 93 | response = nil; 94 | return response; 95 | } 96 | 97 | 98 | - (BLIPResponse*) response { 99 | if (! _response && ! self.noReply) 100 | _response = [[BLIPResponse alloc] _initWithRequest: self]; 101 | return _response; 102 | } 103 | 104 | - (void) deferResponse { 105 | // This will allocate _response, causing -repliedTo to become YES, so BLIPConnection won't 106 | // send an automatic empty response after the current request handler returns. 107 | LogTo(BLIP,@"Deferring response to %@",self); 108 | [self response]; 109 | } 110 | 111 | - (BOOL) repliedTo { 112 | return _response != nil; 113 | } 114 | 115 | - (void) respondWithData: (NSData*)data contentType: (NSString*)contentType { 116 | BLIPResponse *response = self.response; 117 | response.body = data; 118 | response.contentType = contentType; 119 | [response send]; 120 | } 121 | 122 | - (void) respondWithString: (NSString*)string { 123 | [self respondWithData: [string dataUsingEncoding: NSUTF8StringEncoding] 124 | contentType: @"text/plain; charset=UTF-8"]; 125 | } 126 | 127 | - (void) respondWithJSON: (id)jsonObject { 128 | BLIPResponse *response = self.response; 129 | response.bodyJSON = jsonObject; 130 | [response send]; 131 | } 132 | 133 | - (void) respondWithError: (NSError*)error { 134 | self.response.error = error; 135 | [self.response send]; 136 | } 137 | 138 | - (void) respondWithErrorCode: (int)errorCode message: (NSString*)errorMessage { 139 | [self respondWithError: BLIPMakeError(errorCode, @"%@",errorMessage)]; 140 | } 141 | 142 | - (void) respondWithException: (NSException*)exception { 143 | [self respondWithError: BLIPMakeError(kBLIPError_HandlerFailed, @"%@", exception.reason)]; 144 | } 145 | 146 | 147 | @end 148 | 149 | 150 | 151 | 152 | /* 153 | Copyright (c) 2008-2013, Jens Alfke . All rights reserved. 154 | 155 | Redistribution and use in source and binary forms, with or without modification, are permitted 156 | provided that the following conditions are met: 157 | 158 | * Redistributions of source code must retain the above copyright notice, this list of conditions 159 | and the following disclaimer. 160 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions 161 | and the following disclaimer in the documentation and/or other materials provided with the 162 | distribution. 163 | 164 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 165 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 166 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRI- 167 | BUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 168 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 169 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 170 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 171 | THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 172 | */ 173 | -------------------------------------------------------------------------------- /BLIP/BLIPResponse.h: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPResponse.h 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 9/15/13. 6 | // Copyright (c) 2013 Couchbase. 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)(); 23 | 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /BLIP/BLIPResponse.m: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPResponse.m 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 9/15/13. 6 | // Copyright (c) 2013 Couchbase. 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 "Logging.h" 21 | #import "Test.h" 22 | #import "ExceptionUtils.h" 23 | 24 | 25 | @implementation BLIPResponse 26 | { 27 | void (^_onComplete)(); 28 | } 29 | 30 | - (instancetype) _initWithRequest: (BLIPRequest*)request { 31 | Assert(request); 32 | self = [super _initWithConnection: request.connection 33 | isMine: !request.isMine 34 | flags: kBLIP_RPY | kBLIP_MoreComing 35 | number: request.number 36 | body: nil]; 37 | if (self != nil) { 38 | if (_isMine && request.urgent) 39 | _flags |= kBLIP_Urgent; 40 | } 41 | return self; 42 | } 43 | 44 | 45 | #if DEBUG 46 | // For testing only 47 | - (instancetype) _initIncomingWithProperties: (NSDictionary*)properties body: (NSData*)body { 48 | self = [self _initWithConnection: nil 49 | isMine: NO 50 | flags: kBLIP_MSG 51 | number: 0 52 | body: nil]; 53 | if (self != nil ) { 54 | _body = [body copy]; 55 | _isMutable = NO; 56 | _properties = properties; 57 | } 58 | return self; 59 | } 60 | #endif 61 | 62 | 63 | - (NSError*) error { 64 | if ((_flags & kBLIP_TypeMask) != kBLIP_ERR) 65 | return nil; 66 | 67 | NSMutableDictionary *userInfo = [_properties mutableCopy]; 68 | NSString *domain = userInfo[@"Error-Domain"]; 69 | int code = [userInfo[@"Error-Code"] intValue]; 70 | if (domain==nil || code==0) { 71 | domain = BLIPErrorDomain; 72 | if (code==0) 73 | code = kBLIPError_Unspecified; 74 | } 75 | [userInfo removeObjectForKey: @"Error-Domain"]; 76 | [userInfo removeObjectForKey: @"Error-Code"]; 77 | return [NSError errorWithDomain: domain code: code userInfo: userInfo]; 78 | } 79 | 80 | - (void) _setError: (NSError*)error { 81 | _flags &= ~kBLIP_TypeMask; 82 | if (error) { 83 | // Setting this stuff is a PITA because this object might be technically immutable, 84 | // in which case the standard setters would barf if I called them. 85 | _flags |= kBLIP_ERR; 86 | _body = nil; 87 | _mutableBody = nil; 88 | 89 | NSMutableDictionary *errorProps = [self.properties mutableCopy]; 90 | if (! errorProps) 91 | errorProps = [[NSMutableDictionary alloc] init]; 92 | NSDictionary *userInfo = error.userInfo; 93 | for (NSString *key in userInfo) { 94 | id value = $castIf(NSString,userInfo[key]); 95 | if (value) 96 | errorProps[key] = value; 97 | } 98 | errorProps[@"Error-Domain"] = error.domain; 99 | errorProps[@"Error-Code"] = $sprintf(@"%li",(long)error.code); 100 | _properties = errorProps; 101 | 102 | } else { 103 | _flags |= kBLIP_RPY; 104 | [self.mutableProperties removeAllObjects]; 105 | } 106 | } 107 | 108 | - (void) setError: (NSError*)error { 109 | Assert(_isMine && _isMutable); 110 | [self _setError: error]; 111 | } 112 | 113 | 114 | - (BOOL) send { 115 | Assert(_connection,@"%@ has no connection to send over",self); 116 | Assert(!_sent,@"%@ was already sent",self); 117 | [self _encode]; 118 | BOOL sent = self.sent = [_connection _sendResponse: self]; 119 | Assert(sent); 120 | return sent; 121 | } 122 | 123 | 124 | @synthesize onComplete=_onComplete; 125 | 126 | 127 | - (void) setComplete: (BOOL)complete { 128 | [super setComplete: complete]; 129 | if (complete && _onComplete) { 130 | @try{ 131 | _onComplete(); 132 | }catchAndReport(@"BLIPRequest onComplete block"); 133 | _onComplete = nil; 134 | } 135 | } 136 | 137 | 138 | - (void) _connectionClosed { 139 | [super _connectionClosed]; 140 | if (!_isMine && !_complete) { 141 | NSError *error = _connection.error; 142 | if (!error) 143 | error = BLIPMakeError(kBLIPError_Disconnected, 144 | @"Connection closed before response was received"); 145 | // Change incoming response to an error: 146 | _isMutable = YES; 147 | _properties = [_properties mutableCopy]; 148 | [self _setError: error]; 149 | _isMutable = NO; 150 | 151 | self.complete = YES; // Calls onComplete target 152 | } 153 | } 154 | 155 | 156 | @end 157 | 158 | 159 | 160 | 161 | /* 162 | Copyright (c) 2008-2013, Jens Alfke . All rights reserved. 163 | 164 | Redistribution and use in source and binary forms, with or without modification, are permitted 165 | provided that the following conditions are met: 166 | 167 | * Redistributions of source code must retain the above copyright notice, this list of conditions 168 | and the following disclaimer. 169 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions 170 | and the following disclaimer in the documentation and/or other materials provided with the 171 | distribution. 172 | 173 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 174 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 175 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRI- 176 | BUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 177 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 178 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 179 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 180 | THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 181 | */ 182 | -------------------------------------------------------------------------------- /BLIP/BLIPWebSocketConnection.h: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPWebSocketConnection.h 3 | // BLIPSync 4 | // 5 | // Created by Jens Alfke on 4/10/15. 6 | // Copyright (c) 2015 Couchbase. All rights reserved. 7 | // 8 | 9 | #import "BLIPConnection.h" 10 | #import "WebSocket.h" 11 | 12 | @interface BLIPWebSocketConnection : BLIPConnection 13 | 14 | - (instancetype) initWithURLRequest:(NSURLRequest *)request; 15 | - (instancetype) initWithURL:(NSURL *)url; 16 | 17 | - (instancetype) initWithWebSocket: (WebSocket*)webSocket; 18 | 19 | - (void)closeWithCode:(WebSocketCloseCode)code reason:(NSString *)reason; 20 | 21 | /** The underlying WebSocket. */ 22 | @property (readonly) WebSocket* webSocket; 23 | 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /BLIP/BLIPWebSocketConnection.m: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPWebSocketConnection.m 3 | // BLIPSync 4 | // 5 | // Created by Jens Alfke on 4/10/15. 6 | // Copyright (c) 2015 Couchbase. All rights reserved. 7 | // 8 | 9 | #import "BLIPWebSocketConnection.h" 10 | #import "BLIPConnection+Transport.h" 11 | #import "WebSocketClient.h" 12 | #import "Test.h" 13 | 14 | 15 | @interface BLIPWebSocketConnection () 16 | @end 17 | 18 | 19 | @implementation BLIPWebSocketConnection 20 | 21 | @synthesize webSocket=_webSocket; 22 | 23 | // Public API; Designated initializer 24 | - (instancetype) initWithWebSocket: (WebSocket*)webSocket { 25 | Assert(webSocket); 26 | self = [super initWithTransportQueue: webSocket.websocketQueue 27 | isOpen: webSocket.state == kWebSocketOpen]; 28 | if (self) { 29 | _webSocket = webSocket; 30 | _webSocket.delegate = self; 31 | } 32 | return self; 33 | } 34 | 35 | // Public API 36 | - (instancetype) initWithURLRequest:(NSURLRequest *)request { 37 | return [self initWithWebSocket: [[WebSocketClient alloc] initWithURLRequest: request]]; 38 | } 39 | 40 | // Public API 41 | - (instancetype) initWithURL:(NSURL *)url { 42 | return [self initWithWebSocket: [[WebSocketClient alloc] initWithURL: url]]; 43 | } 44 | 45 | 46 | - (NSURL*) URL { 47 | return ((WebSocketClient*)_webSocket).URL; 48 | } 49 | 50 | // Public API 51 | - (BOOL) connect: (NSError**)outError { 52 | NSError* error; 53 | if (![(WebSocketClient*)_webSocket connect: &error]) { 54 | self.error = error; 55 | if (outError) 56 | *outError = error; 57 | return NO; 58 | } 59 | return YES; 60 | } 61 | 62 | // Public API 63 | - (void) close { 64 | NSError* error = self.error; 65 | if (error == nil) { 66 | [_webSocket close]; 67 | } else if ([error.domain isEqualToString: WebSocketErrorDomain]) { 68 | [_webSocket closeWithCode: error.code reason: error.localizedFailureReason]; 69 | } else { 70 | [_webSocket closeWithCode: kWebSocketClosePolicyError reason: error.localizedDescription]; 71 | } 72 | } 73 | 74 | // Public API 75 | - (void) closeWithCode: (WebSocketCloseCode)code reason:(NSString *)reason { 76 | [_webSocket closeWithCode: code reason: reason]; 77 | } 78 | 79 | // WebSocket delegate method 80 | - (void) webSocketDidOpen: (WebSocket *)webSocket { 81 | [self transportDidOpen]; 82 | } 83 | 84 | // WebSocket delegate method 85 | - (void) webSocket: (WebSocket *)webSocket didFailWithError: (NSError *)error { 86 | [self transportDidCloseWithError: error]; 87 | } 88 | 89 | // WebSocket delegate method 90 | - (void) webSocket: (WebSocket *)webSocket didCloseWithError: (NSError*)error 91 | { 92 | [self transportDidCloseWithError: error]; 93 | } 94 | 95 | // WebSocket delegate method 96 | - (void) webSocketIsHungry: (WebSocket *)ws { 97 | [self feedTransport]; 98 | } 99 | 100 | - (BOOL) transportCanSend { 101 | return _webSocket.state == kWebSocketOpen; 102 | } 103 | 104 | - (void) sendFrame:(NSData *)frame { 105 | [_webSocket sendBinaryMessage: frame]; 106 | } 107 | 108 | // WebSocket delegate method 109 | - (BOOL)webSocket:(WebSocket *)webSocket didReceiveBinaryMessage:(NSData*)message { 110 | [self didReceiveFrame: message]; 111 | return YES; 112 | } 113 | 114 | @end 115 | -------------------------------------------------------------------------------- /BLIP/BLIPWebSocketListener.h: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPWebSocketListener.h 3 | // BLIPSync 4 | // 5 | // Created by Jens Alfke on 4/1/15. 6 | // Copyright (c) 2015 Couchbase. All rights reserved. 7 | // 8 | 9 | #import "WebSocketListener.h" 10 | #import "BLIPConnection.h" 11 | 12 | @interface BLIPWebSocketListener : WebSocketListener 13 | 14 | - (instancetype) initWithPath: (NSString*)path 15 | delegate: (id)delegate 16 | queue: (dispatch_queue_t)queue; 17 | 18 | - (void) blipConnectionDidOpen:(BLIPConnection *)connection; 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /BLIP/BLIPWebSocketListener.m: -------------------------------------------------------------------------------- 1 | // 2 | // BLIPWebSocketListener.m 3 | // BLIPSync 4 | // 5 | // Created by Jens Alfke on 4/1/15. 6 | // Copyright (c) 2015 Couchbase. All rights reserved. 7 | // 8 | 9 | #import "BLIPWebSocketListener.h" 10 | #import "BLIPWebSocketConnection.h" 11 | #import "WebSocketListener.h" 12 | #import "Logging.h" 13 | 14 | 15 | @interface BLIPWebSocketListener () 16 | @end 17 | 18 | 19 | @implementation BLIPWebSocketListener 20 | { 21 | id _blipDelegate; 22 | dispatch_queue_t _delegateQueue; 23 | NSMutableSet* _openSockets; 24 | } 25 | 26 | - (instancetype) initWithPath: (NSString*)path 27 | delegate: (id)delegate 28 | { 29 | return [self initWithPath: path delegate: delegate queue: nil]; 30 | } 31 | 32 | 33 | - (instancetype) initWithPath: (NSString*)path 34 | delegate: (id)delegate 35 | queue: (dispatch_queue_t)queue; 36 | { 37 | self = [super initWithPath: path delegate: self]; 38 | if (self) { 39 | _blipDelegate = delegate; 40 | _delegateQueue = queue ?: dispatch_get_main_queue(); 41 | _openSockets = [NSMutableSet new]; 42 | } 43 | return self; 44 | } 45 | 46 | 47 | - (void) webSocketDidOpen:(WebSocket *)ws { 48 | BLIPWebSocketConnection* b = [[BLIPWebSocketConnection alloc] initWithWebSocket: ws]; 49 | [_openSockets addObject: b]; //FIX: How to remove it since I'm not the delegate when it closes?? 50 | LogTo(BLIP, @"Listener got connection: %@", b); 51 | dispatch_async(_delegateQueue, ^{ 52 | [self blipConnectionDidOpen: b]; 53 | }); 54 | } 55 | 56 | 57 | - (void)blipConnectionDidOpen:(BLIPConnection*)b { 58 | [b setDelegate: _blipDelegate queue: _delegateQueue]; 59 | } 60 | 61 | 62 | @end 63 | -------------------------------------------------------------------------------- /BLIP/BLIP_Internal.h: -------------------------------------------------------------------------------- 1 | // 2 | // BLIP_Internal.h 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 5/10/08. 6 | // Copyright 2008-2013 Jens Alfke. 7 | // Copyright (c) 2013 Couchbase, Inc. All rights reserved. 8 | 9 | #import "BLIPConnection.h" 10 | #import "BLIPRequest.h" 11 | #import "BLIPResponse.h" 12 | #import "BLIPProperties.h" 13 | @class MYBuffer; 14 | 15 | 16 | /* Private declarations and APIs for BLIP implementation. Not for use by clients! */ 17 | 18 | 19 | /* Flag bits in a BLIP frame header */ 20 | typedef NS_OPTIONS(UInt8, BLIPMessageFlags) { 21 | kBLIP_MSG = 0x00, // initiating message 22 | kBLIP_RPY = 0x01, // response to a MSG 23 | kBLIP_ERR = 0x02, // error response to a MSG 24 | 25 | kBLIP_TypeMask = 0x03, // bits reserved for storing message type 26 | kBLIP_Compressed= 0x04, // data is gzipped 27 | kBLIP_Urgent = 0x08, // please send sooner/faster 28 | kBLIP_NoReply = 0x10, // no RPY needed 29 | kBLIP_MoreComing= 0x20, // More frames coming (Applies only to individual frame) 30 | kBLIP_Meta = 0x40, // Special message type, handled internally (hello, bye, ...) 31 | 32 | kBLIP_MaxFlag = 0xFF 33 | }; 34 | 35 | /* BLIP message types; encoded in each frame's header. */ 36 | typedef BLIPMessageFlags BLIPMessageType; 37 | 38 | 39 | @interface BLIPConnection () 40 | - (BOOL) _sendRequest: (BLIPRequest*)q response: (BLIPResponse*)response; 41 | - (BOOL) _sendResponse: (BLIPResponse*)response; 42 | - (void) _messageReceivedProperties: (BLIPMessage*)message; 43 | @end 44 | 45 | 46 | @interface BLIPMessage () 47 | { 48 | @protected 49 | BLIPConnection* _connection; 50 | BLIPMessageFlags _flags; 51 | UInt32 _number; 52 | NSDictionary *_properties; 53 | NSData *_body; 54 | MYBuffer *_encodedBody; 55 | NSMutableData *_mutableBody; 56 | NSMutableArray* _bodyStreams; 57 | BOOL _isMine, _isMutable, _sent, _propertiesAvailable, _complete; 58 | NSInteger _bytesWritten, _bytesReceived; 59 | id _representedObject; 60 | } 61 | @property BOOL sent, propertiesAvailable, complete; 62 | - (BLIPMessageFlags) _flags; 63 | - (void) _setFlag: (BLIPMessageFlags)flag value: (BOOL)value; 64 | - (void) _encode; 65 | @end 66 | 67 | 68 | @interface BLIPMessage () 69 | - (instancetype) _initWithConnection: (BLIPConnection*)connection 70 | isMine: (BOOL)isMine 71 | flags: (BLIPMessageFlags)flags 72 | number: (UInt32)msgNo 73 | body: (NSData*)body; 74 | - (NSData*) nextFrameWithMaxSize: (UInt16)maxSize moreComing: (BOOL*)outMoreComing; 75 | @property (readonly) NSInteger _bytesWritten; 76 | - (void) _assignedNumber: (UInt32)number; 77 | - (BOOL) _receivedFrameWithFlags: (BLIPMessageFlags)flags body: (NSData*)body; 78 | - (void) _connectionClosed; 79 | @end 80 | 81 | 82 | @interface BLIPRequest () 83 | - (instancetype) _initWithConnection: (BLIPConnection*)connection 84 | body: (NSData*)body 85 | properties: (NSDictionary*)properties; 86 | @end 87 | 88 | 89 | @interface BLIPResponse () 90 | - (instancetype) _initWithRequest: (BLIPRequest*)request; 91 | #if DEBUG 92 | - (instancetype) _initIncomingWithProperties: (NSDictionary*)properties body: (NSData*)body; 93 | #endif 94 | @end 95 | -------------------------------------------------------------------------------- /Categories/DDData.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface NSData (DDData) 4 | 5 | - (NSData *)md5Digest; 6 | 7 | - (NSData *)sha1Digest; 8 | 9 | - (NSString *)hexStringValue; 10 | 11 | - (NSString *)base64Encoded; 12 | - (NSData *)base64Decoded; 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /Categories/DDData.m: -------------------------------------------------------------------------------- 1 | #import "DDData.h" 2 | #import 3 | 4 | 5 | @implementation NSData (DDData) 6 | 7 | static char encodingTable[64] = { 8 | 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P', 9 | 'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f', 10 | 'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v', 11 | 'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/' }; 12 | 13 | - (NSData *)md5Digest 14 | { 15 | unsigned char result[CC_MD5_DIGEST_LENGTH]; 16 | 17 | CC_MD5([self bytes], (CC_LONG)[self length], result); 18 | return [NSData dataWithBytes:result length:CC_MD5_DIGEST_LENGTH]; 19 | } 20 | 21 | - (NSData *)sha1Digest 22 | { 23 | unsigned char result[CC_SHA1_DIGEST_LENGTH]; 24 | 25 | CC_SHA1([self bytes], (CC_LONG)[self length], result); 26 | return [NSData dataWithBytes:result length:CC_SHA1_DIGEST_LENGTH]; 27 | } 28 | 29 | - (NSString *)hexStringValue 30 | { 31 | NSMutableString *stringBuffer = [NSMutableString stringWithCapacity:([self length] * 2)]; 32 | 33 | const unsigned char *dataBuffer = [self bytes]; 34 | NSUInteger i; 35 | 36 | for (i = 0; i < [self length]; ++i) 37 | { 38 | [stringBuffer appendFormat:@"%02x", (unsigned int)dataBuffer[i]]; 39 | } 40 | 41 | return [stringBuffer copy]; 42 | } 43 | 44 | - (NSString *)base64Encoded 45 | { 46 | const unsigned char *bytes = [self bytes]; 47 | NSMutableString *result = [NSMutableString stringWithCapacity:[self length]]; 48 | unsigned long ixtext = 0; 49 | unsigned long lentext = [self length]; 50 | long ctremaining = 0; 51 | unsigned char inbuf[3], outbuf[4]; 52 | unsigned short i = 0; 53 | unsigned short charsonline = 0, ctcopy = 0; 54 | unsigned long ix = 0; 55 | 56 | while( YES ) 57 | { 58 | ctremaining = lentext - ixtext; 59 | if( ctremaining <= 0 ) break; 60 | 61 | for( i = 0; i < 3; i++ ) { 62 | ix = ixtext + i; 63 | if( ix < lentext ) inbuf[i] = bytes[ix]; 64 | else inbuf [i] = 0; 65 | } 66 | 67 | outbuf [0] = (inbuf [0] & 0xFC) >> 2; 68 | outbuf [1] = (unsigned char)((inbuf [0] & 0x03) << 4) | ((inbuf [1] & 0xF0) >> 4); 69 | outbuf [2] = (unsigned char)((inbuf [1] & 0x0F) << 2) | ((inbuf [2] & 0xC0) >> 6); 70 | outbuf [3] = inbuf [2] & 0x3F; 71 | ctcopy = 4; 72 | 73 | switch( ctremaining ) 74 | { 75 | case 1: 76 | ctcopy = 2; 77 | break; 78 | case 2: 79 | ctcopy = 3; 80 | break; 81 | } 82 | 83 | for( i = 0; i < ctcopy; i++ ) 84 | [result appendFormat:@"%c", encodingTable[outbuf[i]]]; 85 | 86 | for( i = ctcopy; i < 4; i++ ) 87 | [result appendString:@"="]; 88 | 89 | ixtext += 3; 90 | charsonline += 4; 91 | } 92 | 93 | return [NSString stringWithString:result]; 94 | } 95 | 96 | - (NSData *)base64Decoded 97 | { 98 | const unsigned char *bytes = [self bytes]; 99 | NSMutableData *result = [NSMutableData dataWithCapacity:[self length]]; 100 | 101 | unsigned long ixtext = 0; 102 | unsigned long lentext = [self length]; 103 | unsigned char ch = 0; 104 | unsigned char inbuf[4] = {0, 0, 0, 0}; 105 | unsigned char outbuf[3] = {0, 0, 0}; 106 | short i = 0, ixinbuf = 0; 107 | BOOL flignore = NO; 108 | BOOL flendtext = NO; 109 | 110 | while( YES ) 111 | { 112 | if( ixtext >= lentext ) break; 113 | ch = bytes[ixtext++]; 114 | flignore = NO; 115 | 116 | if( ( ch >= 'A' ) && ( ch <= 'Z' ) ) ch = ch - 'A'; 117 | else if( ( ch >= 'a' ) && ( ch <= 'z' ) ) ch = ch - 'a' + 26; 118 | else if( ( ch >= '0' ) && ( ch <= '9' ) ) ch = ch - '0' + 52; 119 | else if( ch == '+' ) ch = 62; 120 | else if( ch == '=' ) flendtext = YES; 121 | else if( ch == '/' ) ch = 63; 122 | else flignore = YES; 123 | 124 | if( ! flignore ) 125 | { 126 | short ctcharsinbuf = 3; 127 | BOOL flbreak = NO; 128 | 129 | if( flendtext ) 130 | { 131 | if( ! ixinbuf ) break; 132 | if( ( ixinbuf == 1 ) || ( ixinbuf == 2 ) ) ctcharsinbuf = 1; 133 | else ctcharsinbuf = 2; 134 | ixinbuf = 3; 135 | flbreak = YES; 136 | } 137 | 138 | inbuf [ixinbuf++] = ch; 139 | 140 | if( ixinbuf == 4 ) 141 | { 142 | ixinbuf = 0; 143 | outbuf [0] = (unsigned char)( inbuf[0] << 2 ) | ( ( inbuf[1] & 0x30) >> 4 ); 144 | outbuf [1] = (unsigned char)( ( inbuf[1] & 0x0F ) << 4 ) | ( ( inbuf[2] & 0x3C ) >> 2 ); 145 | outbuf [2] = (unsigned char)( ( inbuf[2] & 0x03 ) << 6 ) | ( inbuf[3] & 0x3F ); 146 | 147 | for( i = 0; i < ctcharsinbuf; i++ ) 148 | [result appendBytes:&outbuf[i] length:1]; 149 | } 150 | 151 | if( flbreak ) break; 152 | } 153 | } 154 | 155 | return [NSData dataWithData:result]; 156 | } 157 | 158 | @end 159 | -------------------------------------------------------------------------------- /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, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ⚠️ This repo is obsolete and not used. The implementation of BLIP in Couchbase Lite for Cocoa is split between [Couchbase Lite iOS](https://github.com/couchbase/couchbase-lite-ios) for the transport and [BLIP](https://github.com/couchbase/couchbase-lite-core/tree/master/Networking/BLIP) for the protocol. 2 | 3 | This is an Objective-C implementation of the [WebSocket][WEBSOCKET] protocol, for iOS and Mac OS. It includes a protocol extension called BLIP that adds RPC-style functionality. 4 | 5 | It's built on top of Robbie Hanson's excellent [CocoaAsyncSocket][ASYNC] library. Its WebSocket class started out as a hack of a class from his [CocoaHTTPServer][HTTPSERVER], but has evolved from there. 6 | 7 | (Note: this code is unrelated to the [cocoa-websocket][COCOAWEBSOCKET] library, except that they both use AsyncSocket.) 8 | 9 | # Status 10 | 11 | This is pretty early in development; pre-alpha for sure. But we plan to use it in Couchbase Lite in the near future. (Sept. 2013) 12 | 13 | # BLIP 14 | 15 | ## Why BLIP? 16 | 17 | BLIP adds several useful features that aren't supported directly by WebSocket: 18 | 19 | * 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. 20 | * 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. 21 | * 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. 22 | * 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. 23 | 24 | ## BLIP Protocol 25 | 26 | A BLIP message is either a request or a response; a request will have zero or one response associated with it. Messages can be arbitrarily long, but are broken up into smaller frames, generally between 4k and 16k bytes, during transmission to support multiplexing of messages. 27 | 28 | BLIP frames are WebSocket binary messages that start with two [varint][VARINT]-encoded unsigned integers: 29 | 30 | 1. The first is a request number; this identifies which frames belong to the same message. Outgoing request messages are assigned sequential integers starting at 1. A response message uses the same number as its request, so the peer knows which request it answers. 31 | 2. The second is a set of flags: 32 | 33 | 0x03 Message type (0-3) 34 | 0x04 Compressed (if set, message data is gzipped) 35 | 0x08 Urgent (prioritizes outgoing messages) 36 | 0x10 No-reply (in requests only; recipient should not send a response) 37 | 0x20 More coming (not the final frame of a message) 38 | 0x40 Meta (messages for internal use; currently unused) 39 | 40 | where the message types (stored in the lower two bits) are: 41 | 42 | 0 Request 43 | 1 Response 44 | 2 Error (a special type of response) 45 | 46 | The frame data begins immediately after the flags. 47 | 48 | Long messages will be broken into multiple frames. Every frame but the last will have the "More-coming" flag set. (Frame sizes aren't mandated by the protocol, but if a frame is too large it can hog the socket and starve other messages that are trying to be sent at the same time.) The message data is the concatenation of all the frame data. 49 | 50 | The message data (_not_ each frame's data) begins with a block of properties. These start with a varint-encoded byte count (zero if there are no properties.) The properties follow, encoded as a series of NUL-terminated UTF-8 strings, alternating names and values. Certain commonly-used strings are encoded as single-byte strings whose one character has a value less than 0x20; there's a table of these in BLIPProperties.m. 51 | 52 | The message data after the properties is the payload, i.e. the data the client is sending. If the Compressed flag is set, this payload is compressed with the Gzip algorithm. 53 | 54 | [WEBSOCKET]: http://www.websocket.org 55 | [ASYNC]: https://github.com/robbiehanson/CocoaAsyncSocket 56 | [HTTPSERVER]: https://github.com/robbiehanson/CocoaHTTPServer 57 | [BLIP]: https://bitbucket.org/snej/mynetwork/wiki/BLIP/Overview 58 | [VARINT]: https://developers.google.com/protocol-buffers/docs/encoding#varints 59 | [COCOAWEBSOCKET]: https://github.com/talkative/cocoa-websocket 60 | -------------------------------------------------------------------------------- /Testing/blip_echo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | 10 | "code.google.com/p/go.net/websocket" 11 | "github.com/snej/go-blip" 12 | ) 13 | 14 | const Logging = true 15 | 16 | func main() { 17 | http.Handle("/dump", NewWebSocketHandler(DumpHandler)) 18 | http.Handle("/dump-blip", NewWebSocketHandler(DumpBLIPHandler)) 19 | http.Handle("/echo", NewWebSocketHandler(EchoHandler)) 20 | 21 | context := blip.NewContext() 22 | context.LogFrames = Logging 23 | context.HandlerForProfile["BLIPTest/EchoData"] = dispatchEcho 24 | http.Handle("/blip", context.HTTPHandler()) 25 | log.Printf("Listening on :12345/blip ...") 26 | if err := http.ListenAndServe(":12345", nil); err != nil { 27 | panic("ListenAndServe: " + err.Error()) 28 | } 29 | } 30 | 31 | func dispatchEcho(request *blip.Message) { 32 | body, err := request.Body() 33 | if err != nil { 34 | log.Printf("ERROR reading body of %s: %s", request, err) 35 | return 36 | } 37 | if Logging { 38 | log.Printf("Got request, properties = %v", request.Properties) 39 | DumpByteArray(body) 40 | } 41 | if response := request.Response(); response != nil { 42 | response.SetBody(body) 43 | response.Properties["Content-Type"] = request.Properties["Content-Type"] 44 | } 45 | } 46 | 47 | // Dump structure of a BLIP frame 48 | func DumpBLIPHandler(frame []byte) []byte { 49 | reader := bytes.NewReader(frame) 50 | var sequence uint32 51 | var flags uint16 52 | binary.Read(reader, binary.BigEndian, &sequence) 53 | binary.Read(reader, binary.BigEndian, &flags) 54 | log.Printf("Frame #%4d flags = %016b", sequence, flags) 55 | DumpByteArray(frame[6:]) 56 | return nil 57 | } 58 | 59 | // Echo the data received on the WebSocket. 60 | func DumpHandler(frame []byte) []byte { 61 | DumpByteArray(frame) 62 | return nil 63 | } 64 | 65 | // Echo the data received on the WebSocket. 66 | func EchoHandler(frame []byte) []byte { 67 | log.Printf("Read frame: %q", frame) 68 | return frame 69 | } 70 | 71 | // Echo the data received on the WebSocket. 72 | func NewWebSocketHandler(fn func([]byte) []byte) http.Handler { 73 | var server websocket.Server 74 | server.Handler = func(ws *websocket.Conn) { 75 | log.Printf("--- Received connection") 76 | buffer := make([]byte, 8000) 77 | var err error 78 | for { 79 | var nBytes int 80 | nBytes, err = ws.Read(buffer) 81 | if err != nil { 82 | break 83 | } 84 | frame := buffer[:nBytes] 85 | if response := fn(frame); response != nil { 86 | ws.Write(response) 87 | } 88 | } 89 | log.Printf("--- End connection (%v)", err) 90 | } 91 | return server 92 | } 93 | 94 | func DumpByteArray(frame []byte) { 95 | for line := 0; line <= 1; line++ { 96 | fmt.Print("\t") 97 | for i := 0; i < len(frame); i += 4 { 98 | end := i + 4 99 | if end > len(frame) { 100 | end = len(frame) 101 | } 102 | chunk := frame[i:end] 103 | if line == 0 { 104 | fmt.Printf("%x", chunk) 105 | } else { 106 | for _, c := range chunk { 107 | if c < ' ' || c >= 127 { 108 | c = ' ' 109 | } 110 | fmt.Printf("%c ", c) 111 | } 112 | } 113 | fmt.Print(" ") 114 | } 115 | fmt.Printf("\n") 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Testing/bliptest_main.m: -------------------------------------------------------------------------------- 1 | // 2 | // blip_main.m 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 9/10/13. 6 | // Copyright (c) 2013 Couchbase. All rights reserved. 7 | 8 | #import 9 | #import "BLIP.h" 10 | #import "WebSocketClient.h" 11 | 12 | #import "CollectionUtils.h" 13 | #import "Logging.h" 14 | #import "Test.h" 15 | 16 | 17 | #define kSendInterval 1.0 18 | #define kBodySize 50 19 | #define kVerifyResponses YES 20 | #define kStreaming YES 21 | 22 | 23 | @interface BLIPTest : NSObject 24 | 25 | @end 26 | 27 | 28 | 29 | @implementation BLIPTest 30 | { 31 | BLIPWebSocket* _webSocket; 32 | UInt64 _count; 33 | CFAbsoluteTime _startTime; 34 | } 35 | 36 | - (instancetype) initWithURL: (NSURL*)url { 37 | self = [super init]; 38 | if (self) { 39 | _webSocket = [[BLIPWebSocket alloc] initWithURL: url]; 40 | [_webSocket setDelegate: self queue: NULL]; // use current queue 41 | _webSocket.dispatchPartialMessages = kStreaming; 42 | NSError* error; 43 | if (![_webSocket connect: &error]) { 44 | Warn(@"Failed to connect: %@", error); 45 | exit(1); 46 | } 47 | Log(@"** Connecting..."); 48 | } 49 | return self; 50 | } 51 | 52 | - (void) disconnect { 53 | [_webSocket close]; 54 | } 55 | 56 | - (void) send { 57 | if (_startTime == 0) 58 | _startTime = CFAbsoluteTimeGetCurrent(); 59 | Log(@"SEND"); 60 | 61 | NSMutableData* body = [NSMutableData dataWithLength: kBodySize]; 62 | char* bytes = body.mutableBytes; 63 | for (NSUInteger i = 0; i < kBodySize; ++i) 64 | bytes[i] = 'A' + ((i + _count) % 26); 65 | 66 | BLIPRequest* req = [BLIPRequest requestWithBody: body]; 67 | req.profile = @"BLIPTest/EchoData"; 68 | BLIPResponse* response = [_webSocket sendRequest: req]; 69 | if (kStreaming) 70 | response.dataDelegate = self; 71 | } 72 | 73 | - (void)blipWebSocketDidOpen:(BLIPWebSocket*)webSocket { 74 | Log(@"** webSocketDidOpen!"); 75 | [self send]; 76 | } 77 | 78 | - (BOOL) blipWebSocket: (BLIPWebSocket*)webSocket receivedRequest: (BLIPRequest*)request { 79 | Log(@"** Received request: %@", request); 80 | return NO; 81 | } 82 | 83 | - (void) blipWebSocket: (BLIPWebSocket*)webSocket receivedResponse: (BLIPResponse*)response { 84 | NSString* body = [response.body my_UTF8ToString]; 85 | ++_count; 86 | Log(@"** Received response: %@ -- \"%@\"", response, body); 87 | 88 | if (kVerifyResponses && !kStreaming) { 89 | AssertEq(response.body.length, kBodySize); 90 | const UInt8* bytes = response.body.bytes; 91 | for (int i=1; i 0) { 102 | dispatch_async(dispatch_get_main_queue(), ^{ 103 | [self performSelector: @selector(send) withObject: nil afterDelay: kSendInterval]; 104 | }); 105 | } else { 106 | [self send]; 107 | } 108 | } 109 | 110 | - (void)blipWebSocket: (BLIPWebSocket*)webSocket didFailWithError:(NSError *)error { 111 | Warn(@"webSocketDidFail: %@", error); 112 | exit(1); 113 | } 114 | 115 | - (void)blipWebSocket: (BLIPWebSocket*)webSocket 116 | didCloseWithCode:(WebSocketCloseCode)code 117 | reason:(NSString *)reason 118 | { 119 | Log(@"** Closed with code %d: \"%@\"", (int)code, reason); 120 | } 121 | 122 | - (void) blipMessage:(BLIPMessage *)msg didReceiveData:(NSData *)data { 123 | Log(@"**didReceiveData %@: (%u bytes), complete=%d: %@", 124 | msg, (unsigned)data.length, msg.complete, data.my_UTF8ToString); 125 | Assert(data.length > 0); 126 | } 127 | 128 | @end 129 | 130 | 131 | 132 | int main(int argc, const char * argv[]) 133 | { 134 | RunTestCases(argc, argv); 135 | 136 | @autoreleasepool { 137 | 138 | BLIPTest *test = [[BLIPTest alloc] initWithURL: [NSURL URLWithString: @"ws://localhost:12345/blip"]]; 139 | 140 | [[NSRunLoop currentRunLoop] run]; 141 | 142 | [test disconnect]; 143 | } 144 | return 0; 145 | } 146 | 147 | -------------------------------------------------------------------------------- /Testing/test_main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 9/10/13. 6 | // Copyright (c) 2013 Couchbase. 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 17 | #import "WebSocketClient.h" 18 | #import "WebSocketListener.h" 19 | #import "Logging.h" 20 | 21 | 22 | @interface Test : NSObject 23 | @end 24 | 25 | 26 | 27 | @implementation Test 28 | { 29 | WebSocketClient* _webSocket; 30 | WebSocketListener* _listener; 31 | } 32 | 33 | - (instancetype) initWithURL: (NSURL*)url { 34 | self = [super init]; 35 | if (self) { 36 | _webSocket = [[WebSocketClient alloc] initWithURL: url]; 37 | _webSocket.delegate = self; 38 | NSError* error; 39 | if (![_webSocket connect: &error]) { 40 | NSLog(@"Failed to connect: %@", error); 41 | exit(1); 42 | } 43 | NSLog(@"Connecting..."); 44 | } 45 | return self; 46 | } 47 | 48 | - (instancetype) initListenerWithPort: (UInt16)port path: (NSString*)path 49 | { 50 | self = [super init]; 51 | if (self) { 52 | NSError* error; 53 | _listener = [[WebSocketListener alloc] initWithPath: path delegate: self]; 54 | if (![_listener acceptOnInterface: nil port: port error: &error]) { 55 | Warn(@"Couldn't open listener: %@", error); 56 | return nil; 57 | } 58 | 59 | } 60 | return self; 61 | } 62 | 63 | - (void) disconnect { 64 | [_webSocket disconnect]; 65 | [_listener disconnect]; 66 | } 67 | 68 | - (int) webSocket: (WebSocket*)ws shouldAccept: (NSURLRequest*)request { 69 | NSLog(@"webSocketShouldAccept: %@ %@", request.URL, request.allHTTPHeaderFields); 70 | return 101; 71 | } 72 | 73 | - (void)webSocketDidOpen:(WebSocket *)ws { 74 | NSLog(@"webSocketDidOpen: %@", ws); 75 | if (!_listener) 76 | [ws sendMessage: @"Hello, World!"]; 77 | } 78 | 79 | - (void)webSocket:(WebSocket *)ws didReceiveMessage:(NSString *)msg { 80 | NSLog(@"Received message from %@: %@", ws, msg); 81 | [ws sendMessage: @"Thanks for your message!"]; 82 | [ws close]; 83 | } 84 | 85 | - (void)webSocket:(WebSocket *)ws didReceiveBinaryMessage:(NSData *)msg { 86 | NSLog(@"Received binary message from %@: %@", ws, msg); 87 | [ws sendBinaryMessage: msg]; 88 | [ws close]; 89 | } 90 | 91 | - (void)webSocket:(WebSocket *)ws didCloseWithCode: (WebSocketCloseCode)code 92 | reason: (NSString*)reason 93 | { 94 | NSLog(@"webSocketDidClose %@: %d, %@", ws, (int)code, reason); 95 | } 96 | 97 | 98 | @end 99 | 100 | 101 | 102 | int main(int argc, const char * argv[]) 103 | { 104 | 105 | @autoreleasepool { 106 | 107 | Test *test = [[Test alloc] initWithURL: [NSURL URLWithString: @"ws://localhost:12345/echo"]]; 108 | Test *testListener = [[Test alloc] initListenerWithPort: 2345 path: @"/ws"]; 109 | 110 | [[NSRunLoop currentRunLoop] run]; 111 | 112 | [test disconnect]; 113 | [testListener disconnect]; 114 | } 115 | return 0; 116 | } 117 | 118 | -------------------------------------------------------------------------------- /Testing/ws_client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "code.google.com/p/go.net/websocket" 8 | ) 9 | 10 | // Connect to a WebSocket server and exchange messages. 11 | func main() { 12 | url := "ws://localhost:2345/ws" 13 | origin := "http://localhost" 14 | ws, err := websocket.Dial(url, "", origin) 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | if _, err := ws.Write([]byte("hello, world!\n")); err != nil { 19 | log.Fatal(err) 20 | } 21 | var msg = make([]byte, 512) 22 | var n int 23 | if n, err = ws.Read(msg); err != nil { 24 | log.Fatal(err) 25 | } 26 | fmt.Printf("Received: %s.\n", msg[:n]) 27 | } 28 | -------------------------------------------------------------------------------- /Testing/ws_echo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | 10 | "code.google.com/p/go.net/websocket" 11 | ) 12 | 13 | // This example demonstrates a trivial echo server. 14 | func main() { 15 | log.Printf("Listening on :12345 ...") 16 | 17 | http.Handle("/dump", NewWebSocketHandler(DumpHandler)) 18 | http.Handle("/blip", NewWebSocketHandler(DumpBLIPHandler)) 19 | http.Handle("/echo", NewWebSocketHandler(EchoHandler)) 20 | 21 | if err := http.ListenAndServe(":12345", nil); err != nil { 22 | panic("ListenAndServe: " + err.Error()) 23 | } 24 | } 25 | 26 | func DumpByteArray(frame []byte) { 27 | for line := 0; line <= 1; line++ { 28 | fmt.Print("\t") 29 | for i := 0; i < len(frame); i += 4 { 30 | end := i + 4 31 | if end > len(frame) { 32 | end = len(frame) 33 | } 34 | chunk := frame[i:end] 35 | if line == 0 { 36 | fmt.Printf("%x", chunk) 37 | } else { 38 | for _, c := range chunk { 39 | if c < ' ' || c >= 127 { 40 | c = ' ' 41 | } 42 | fmt.Printf("%c ", c) 43 | } 44 | } 45 | fmt.Print(" ") 46 | } 47 | fmt.Printf("\n") 48 | } 49 | } 50 | 51 | // Dump structure of a BLIP frame 52 | func DumpBLIPHandler(frame []byte) []byte { 53 | reader := bytes.NewReader(frame) 54 | var sequence uint32 55 | var flags uint16 56 | binary.Read(reader, binary.BigEndian, &sequence) 57 | binary.Read(reader, binary.BigEndian, &flags) 58 | log.Printf("Frame #%4d flags = %016b", sequence, flags) 59 | DumpByteArray(frame[6:]) 60 | return nil 61 | } 62 | 63 | // Echo the data received on the WebSocket. 64 | func DumpHandler(frame []byte) []byte { 65 | DumpByteArray(frame) 66 | return nil 67 | } 68 | 69 | // Echo the data received on the WebSocket. 70 | func EchoHandler(frame []byte) []byte { 71 | log.Printf("Read frame: %q", frame) 72 | return frame 73 | } 74 | 75 | // Echo the data received on the WebSocket. 76 | func NewWebSocketHandler(fn func([]byte) []byte) http.Handler { 77 | var server websocket.Server 78 | server.Handler = func(ws *websocket.Conn) { 79 | log.Printf("--- Received connection") 80 | buffer := make([]byte, 8000) 81 | var err error 82 | for { 83 | var nBytes int 84 | nBytes, err = ws.Read(buffer) 85 | if err != nil { 86 | break 87 | } 88 | frame := buffer[:nBytes] 89 | if response := fn(frame); response != nil { 90 | ws.Write(response) 91 | } 92 | } 93 | log.Printf("--- End connection (%v)", err) 94 | } 95 | return server 96 | } 97 | -------------------------------------------------------------------------------- /WebSocket.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /WebSocket/WebSocket.h: -------------------------------------------------------------------------------- 1 | // 2 | // WebSocket.h 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 9/10/13, based on Robbie Hanson's original code from 6 | // https://github.com/robbiehanson/CocoaHTTPServer 7 | 8 | #import 9 | @class GCDAsyncSocket; 10 | @protocol WebSocketDelegate; 11 | 12 | 13 | /** States that a WebSocket can be in during its lifetime. */ 14 | enum WebSocketState { 15 | kWebSocketUnopened, 16 | kWebSocketOpening, 17 | kWebSocketOpen, 18 | kWebSocketClosing, 19 | kWebSocketClosed 20 | }; 21 | typedef enum WebSocketState WebSocketState; 22 | 23 | 24 | /** Predefined status codes to use with -closeWithCode:reason:. 25 | They are defined at */ 26 | enum WebSocketCloseCode : UInt16 { 27 | kWebSocketCloseNormal = 1000, 28 | kWebSocketCloseGoingAway = 1001, // Peer has to close, e.g. because host app is quitting 29 | kWebSocketCloseProtocolError = 1002, // Protocol violation: invalid framing data 30 | kWebSocketCloseDataError = 1003, // Message payload cannot be handled 31 | kWebSocketCloseNoCode = 1005, // Never sent, only received 32 | kWebSocketCloseAbnormal = 1006, // Never sent, only received 33 | kWebSocketCloseBadMessageFormat = 1007, // Unparseable message 34 | kWebSocketClosePolicyError = 1008, 35 | kWebSocketCloseMessageTooBig = 1009, 36 | kWebSocketCloseMissingExtension = 1010, // Peer doesn't provide a necessary extension 37 | kWebSocketCloseCantFulfill = 1011, // Can't fulfill request due to "unexpected condition" 38 | kWebSocketCloseTLSFailure = 1015, // Never sent, only received 39 | 40 | kWebSocketCloseFirstAvailable = 4000, // First unregistered code for freeform use 41 | }; 42 | typedef enum WebSocketCloseCode WebSocketCloseCode; 43 | 44 | extern NSString* const WebSocketErrorDomain; 45 | 46 | 47 | /** Abstract superclass WebSocket implementation. 48 | (If you want to connect to a server, look at WebSocketClient.) 49 | All methods are thread-safe unless otherwise noted. */ 50 | @interface WebSocket : NSObject 51 | 52 | /** Designated initializer (for subclasses to call; remember, this class is abstract) */ 53 | - (instancetype) init; 54 | 55 | /** Starts a server-side WebSocket on an already-open GCDAsyncSocket. */ 56 | - (instancetype) initWithConnectedSocket: (GCDAsyncSocket*)socket 57 | delegate: (id)delegate; 58 | 59 | @property (weak) id delegate; 60 | 61 | /** Socket timeout interval. If no traffic is received for this long, the socket will close. 62 | Default is 60 seconds. Use a negative value to disable timeouts. */ 63 | @property NSTimeInterval timeout; 64 | 65 | /** Configures the socket to use TLS/SSL. Settings dict is same as used with CFStream. */ 66 | - (void) useTLS: (NSDictionary*)tlsSettings; 67 | 68 | /** Status of the connection (unopened, opening, ...) 69 | This is observable, but KVO notifications will be sent on the WebSocket's dispatch queue. */ 70 | @property (readonly) WebSocketState state; 71 | 72 | /** Begins an orderly shutdown of the WebSocket connection, with code kWebSocketCloseNormal. */ 73 | - (void) close; 74 | 75 | /** Begins an orderly shutdown of the WebSocket connection. 76 | @param code The WebSocket status code to close with 77 | @param reason Optional message associated with the code. This is not supposed to be shown 78 | to a human but may be useful in troubleshooting. */ 79 | - (void) closeWithCode:(WebSocketCloseCode)code reason:(NSString *)reason; 80 | 81 | /** Abrupt socket disconnection. Normally you should call -close instead. */ 82 | - (void) disconnect; 83 | 84 | /** Sends a text message over the WebSocket. */ 85 | - (void) sendMessage:(NSString *)msg; 86 | 87 | /** Sends a binary message over the WebSocket. */ 88 | - (void) sendBinaryMessage:(NSData*)msg; 89 | 90 | /** If set to YES, no incoming messages will be read from the socket or sent to the delegate. 91 | Messages will resume when it's set back to NO. */ 92 | @property BOOL readPaused; 93 | 94 | /////////////////////////////////////////////////////////////////////////////////////////////////// 95 | #pragma mark - UNDER THE HOOD 96 | /////////////////////////////////////////////////////////////////////////////////////////////////// 97 | 98 | /** The GCD queue the WebSocket and its GCDAsyncSocket run on, and delegate methods are called on. 99 | This queue is created when the WebSocket is created. Don't use it for anything else. */ 100 | @property (nonatomic, readonly) dispatch_queue_t websocketQueue; 101 | 102 | /** Calls -didCloseWithError:. */ 103 | - (void) didCloseWithCode: (WebSocketCloseCode)code reason: (NSString*)reason; 104 | 105 | 106 | /////////////////////////////////////////////////////////////////////////////////////////////////// 107 | #pragma mark - OVERRIDEABLE METHODS 108 | /////////////////////////////////////////////////////////////////////////////////////////////////// 109 | 110 | // These correspond to the delegate methods. 111 | // Subclasses can override them, but those should call through to the superclass method. 112 | 113 | - (void) didOpen; 114 | - (void) didReceiveMessage:(NSString *)msg; 115 | - (void) didReceiveBinaryMessage:(NSData *)msg; 116 | - (void) isHungry; 117 | - (void) didCloseWithError: (NSError*)error; 118 | 119 | 120 | @end 121 | 122 | /////////////////////////////////////////////////////////////////////////////////////////////////// 123 | #pragma mark - DELEGATE API 124 | /////////////////////////////////////////////////////////////////////////////////////////////////// 125 | 126 | /** Delegate API for WebSocket and its subclasses. 127 | Delegate messages are delivered on the WebSocket's dispatch queue (websocketQueue). 128 | Implementations will probably want to re-dispatch to their own queue. */ 129 | @protocol WebSocketDelegate 130 | @optional 131 | 132 | /** Only sent to a WebSocket opened from a WebSocketListener, before -webSocketDidOpen:. 133 | This method can determine whether the incoming connection should be accepted. 134 | @param request The incoming HTTP request. 135 | @return An HTTP status code: should be 101 to accept, or a value >= 300 to refuse. 136 | (As a convenience, any status code < 300 is mapped to 101. Also, if the client wants to return a boolean value, YES maps to 101 and NO maps to 403.)*/ 137 | - (int) webSocket: (WebSocket*)ws shouldAccept: (NSURLRequest*)request; 138 | 139 | /** If SSL certificate verification is disabled (through the socket options), this delegate 140 | method will be called to ask whether to trust the peer's certificate. The trust ref has 141 | already been evaluated. Return YES to trust, NO to abort. */ 142 | - (BOOL) webSocket:(WebSocket *)ws 143 | shouldSecureWithTrust: (SecTrustRef)trust; 144 | 145 | /** Called when a WebSocket has opened its connection and is ready to send and receive messages. */ 146 | - (void) webSocketDidOpen:(WebSocket *)ws; 147 | 148 | /** Called when a WebSocket receives a textual message from its peer. 149 | If it returns NO, the WebSocket's read queue will be paused until the client sets the 150 | readPaused property back to NO. */ 151 | - (BOOL) webSocket:(WebSocket *)ws 152 | didReceiveMessage:(NSString *)msg; 153 | 154 | /** Called when a WebSocket receives a binary message from its peer. 155 | If it returns NO, the WebSocket's read queue will be paused until the client sets the 156 | readPaused property back to NO. */ 157 | - (BOOL) webSocket:(WebSocket *)ws 158 | didReceiveBinaryMessage:(NSData *)msg; 159 | 160 | /** Called when the WebSocket has finished sending all queued messages and is ready for more. */ 161 | - (void) webSocketIsHungry:(WebSocket *)ws; 162 | 163 | /** Called after the WebSocket closes, either intentionally or due to an error. 164 | If the condition is an abnormal WebSocketCloseCode, it will be the `code` property of the 165 | NSError, and the `domain` will be WebSocketErrorDomain. */ 166 | - (void) webSocket:(WebSocket *)ws 167 | didCloseWithError: (NSError*)error; 168 | 169 | @end 170 | -------------------------------------------------------------------------------- /WebSocket/WebSocket.m: -------------------------------------------------------------------------------- 1 | // 2 | // WebSocket.m 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 9/10/13, based on Robbie Hanson's original code from 6 | // https://github.com/robbiehanson/CocoaHTTPServer 7 | // 8 | // Copyright (c) 2013 Couchbase, Inc. All rights reserved. 9 | // 10 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 11 | // except in compliance with the License. You may obtain a copy of the License at 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // Unless required by applicable law or agreed to in writing, software distributed under the 14 | // License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 15 | // either express or implied. See the License for the specific language governing permissions 16 | // and limitations under the License. 17 | 18 | #import "WebSocket.h" 19 | #import "WebSocket_Internal.h" 20 | #import "GCDAsyncSocket.h" 21 | #import 22 | @class HTTPMessage; 23 | 24 | #if ! __has_feature(objc_arc) 25 | #warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). 26 | #endif 27 | 28 | // Does ARC support GCD objects? 29 | // It does if the minimum deployment target is iOS 6+ or Mac OS X 8+ 30 | 31 | #if TARGET_OS_IPHONE 32 | 33 | // Compiling for iOS 34 | 35 | #if __IPHONE_OS_VERSION_MIN_REQUIRED >= 60000 // iOS 6.0 or later 36 | #define NEEDS_DISPATCH_RETAIN_RELEASE 0 37 | #else // iOS 5.X or earlier 38 | #define NEEDS_DISPATCH_RETAIN_RELEASE 1 39 | #endif 40 | 41 | #else 42 | 43 | // Compiling for Mac OS X 44 | 45 | #if MAC_OS_X_VERSION_MIN_REQUIRED >= 1080 // Mac OS X 10.8 or later 46 | #define NEEDS_DISPATCH_RETAIN_RELEASE 0 47 | #else 48 | #define NEEDS_DISPATCH_RETAIN_RELEASE 1 // Mac OS X 10.7 or earlier 49 | #endif 50 | 51 | #endif 52 | 53 | #define TIMEOUT_NONE -1 54 | #define TIMEOUT_REQUEST_BODY 10 55 | 56 | #define HUNGRY_SIZE 5 57 | 58 | 59 | DefineLogDomain(WS); 60 | 61 | 62 | /* unused 63 | static inline BOOL WS_OP_IS_FINAL_FRAGMENT(UInt8 frame) { 64 | return (frame & 0x80) ? YES : NO; 65 | }*/ 66 | 67 | static inline BOOL WS_PAYLOAD_IS_MASKED(UInt8 frame) { 68 | return (frame & 0x80) ? YES : NO; 69 | } 70 | 71 | static inline NSUInteger WS_PAYLOAD_LENGTH(UInt8 frame) { 72 | return frame & 0x7F; 73 | } 74 | 75 | static inline void maskBytes(NSMutableData* data, NSUInteger offset, NSUInteger length, 76 | const UInt8* mask) 77 | { 78 | // OPT: Is there a vector operation to do this more quickly? 79 | UInt8* bytes = (UInt8*)data.mutableBytes + offset; 80 | for (NSUInteger i = 0; i < length; ++i) { 81 | bytes[i] ^= mask[i % 4]; 82 | } 83 | } 84 | 85 | 86 | NSString* const WebSocketErrorDomain = @"WebSocket"; 87 | 88 | 89 | @interface WebSocket () 90 | @property (readwrite) WebSocketState state; 91 | @end 92 | 93 | 94 | /////////////////////////////////////////////////////////////////////////////////////////////////// 95 | #pragma mark - 96 | /////////////////////////////////////////////////////////////////////////////////////////////////// 97 | 98 | @implementation WebSocket 99 | { 100 | BOOL _isRFC6455; 101 | NSTimeInterval _timeout; 102 | BOOL _nextFrameMasked; 103 | NSData *_maskingKey; 104 | NSUInteger _nextOpCode; 105 | NSUInteger _writeQueueSize; 106 | BOOL _readyToReadMessage; // YES when message read and ready to start reading the next 107 | BOOL _readPaused; // While YES, stop reading messages from the socket 108 | } 109 | 110 | 111 | /////////////////////////////////////////////////////////////////////////////////////////////////// 112 | #pragma mark - Setup and Teardown 113 | /////////////////////////////////////////////////////////////////////////////////////////////////// 114 | 115 | @synthesize timeout=_timeout, websocketQueue=_websocketQueue, state=_state; 116 | 117 | static NSData* kTerminator; 118 | 119 | + (void) initialize { 120 | if (!kTerminator) 121 | kTerminator = [[NSData alloc] initWithBytes:"\xFF" length:1]; 122 | } 123 | 124 | // For compatibility with the WebSocket class in the CocoaHTTPServer library 125 | + (BOOL)isWebSocketRequest:(HTTPMessage *)request { 126 | return NO; 127 | } 128 | 129 | - (instancetype) init { 130 | HTTPLogTrace(); 131 | 132 | if ((self = [super init])) { 133 | _timeout = TIMEOUT_DEFAULT; 134 | _state = kWebSocketUnopened; 135 | _websocketQueue = dispatch_queue_create("WebSocket", NULL); 136 | _isRFC6455 = YES; 137 | } 138 | return self; 139 | } 140 | 141 | - (instancetype) initWithConnectedSocket: (GCDAsyncSocket*)socket 142 | delegate: (id)delegate 143 | { 144 | self = [self init]; 145 | if (self) { 146 | _delegate = delegate; 147 | self.asyncSocket = socket; 148 | [self didOpen]; 149 | } 150 | return self; 151 | } 152 | 153 | - (void)dealloc { 154 | HTTPLogTrace(); 155 | 156 | #if NEEDS_DISPATCH_RETAIN_RELEASE 157 | dispatch_release(_websocketQueue); 158 | #endif 159 | 160 | [_asyncSocket setDelegate:nil delegateQueue:NULL]; 161 | [_asyncSocket disconnect]; 162 | } 163 | 164 | - (GCDAsyncSocket*) asyncSocket { 165 | return _asyncSocket; 166 | } 167 | 168 | - (void) setAsyncSocket: (GCDAsyncSocket*)socket { 169 | NSAssert(!_asyncSocket, @"Already have a socket"); 170 | _asyncSocket = socket; 171 | [_asyncSocket setDelegate:self delegateQueue:_websocketQueue]; 172 | if (_tlsSettings) 173 | [_asyncSocket startTLS: _tlsSettings]; 174 | } 175 | 176 | - (id)delegate { 177 | __block id result = nil; 178 | 179 | dispatch_sync(_websocketQueue, ^{ 180 | result = _delegate; 181 | }); 182 | 183 | return result; 184 | } 185 | 186 | - (void)setDelegate:(id)newDelegate { 187 | dispatch_async(_websocketQueue, ^{ 188 | _delegate = newDelegate; 189 | }); 190 | } 191 | 192 | - (void) useTLS: (NSDictionary*)tlsSettings { 193 | dispatch_async(_websocketQueue, ^{ 194 | _tlsSettings = tlsSettings; 195 | }); 196 | } 197 | 198 | 199 | /////////////////////////////////////////////////////////////////////////////////////////////////// 200 | #pragma mark - Start and Stop 201 | /////////////////////////////////////////////////////////////////////////////////////////////////// 202 | 203 | /** 204 | * Starting point for the WebSocket after it has been fully initialized (including subclasses). 205 | **/ 206 | - (void)start { 207 | // This method is not exactly designed to be overriden. 208 | // Subclasses are encouraged to override the didOpen method instead. 209 | 210 | dispatch_async(_websocketQueue, ^{ @autoreleasepool { 211 | if (_state > kWebSocketUnopened) 212 | return; 213 | self.state = kWebSocketOpening; 214 | 215 | [self didOpen]; 216 | }}); 217 | } 218 | 219 | /** 220 | * Abrupt disconnection. 221 | **/ 222 | - (void)disconnect { 223 | // This method is not exactly designed to be overriden. 224 | // Subclasses are encouraged to override the didClose method instead. 225 | 226 | dispatch_async(_websocketQueue, ^{ @autoreleasepool { 227 | [_asyncSocket disconnect]; 228 | }}); 229 | } 230 | 231 | - (void) close { 232 | // Codes are defined in http://tools.ietf.org/html/rfc6455#section-7.4 233 | [self closeWithCode: kWebSocketCloseNormal reason: nil]; 234 | } 235 | 236 | - (void) closeWithCode:(WebSocketCloseCode)code reason:(NSString *)reason { 237 | HTTPLogTrace(); 238 | 239 | // http://tools.ietf.org/html/rfc6455#section-5.5.1 240 | NSMutableData* msg = nil; 241 | if (code > 0 || reason != nil) { 242 | UInt16 rawCode = NSSwapHostShortToBig(code); 243 | if (reason) 244 | msg = [[reason dataUsingEncoding: NSUTF8StringEncoding] mutableCopy]; 245 | else 246 | msg = [NSMutableData dataWithCapacity: sizeof(rawCode)]; 247 | [msg replaceBytesInRange: NSMakeRange(0, 0) withBytes: &rawCode length: sizeof(rawCode)]; 248 | } 249 | 250 | dispatch_async(_websocketQueue, ^{ @autoreleasepool { 251 | if (_state == kWebSocketOpen) { 252 | [self sendFrame: msg type: WS_OP_CONNECTION_CLOSE tag: 0]; 253 | self.state = kWebSocketClosing; 254 | } 255 | }}); 256 | } 257 | 258 | - (void)didOpen { 259 | HTTPLogTrace(); 260 | 261 | // Override me to perform any custom actions once the WebSocket has been opened. 262 | // This method is invoked on the websocketQueue. 263 | // 264 | // Don't forget to invoke [super didOpen] in your method. 265 | 266 | self.state = kWebSocketOpen; 267 | 268 | // Start reading for messages 269 | _readyToReadMessage = YES; 270 | [self startReadingNextMessage]; 271 | // Notify delegate 272 | id delegate = _delegate; 273 | if ([delegate respondsToSelector:@selector(webSocketDidOpen:)]) { 274 | [delegate webSocketDidOpen:self]; 275 | } 276 | } 277 | 278 | - (void)didCloseWithCode: (WebSocketCloseCode)code reason: (NSString*)reason { 279 | NSError* error; 280 | if (code != kWebSocketCloseNormal) { 281 | error = [NSError errorWithDomain: WebSocketErrorDomain 282 | code: code 283 | userInfo: @{NSLocalizedFailureReasonErrorKey: reason}]; 284 | } 285 | [self didCloseWithError: error]; 286 | } 287 | 288 | - (void)didCloseWithError: (NSError*)error { 289 | HTTPLogTrace(); 290 | 291 | // Override me to perform any cleanup when the socket is closed 292 | // This method is invoked on the websocketQueue. 293 | // 294 | // Don't forget to invoke [super didCloseWithError:] at the end of your method. 295 | 296 | // Notify delegate 297 | id delegate = _delegate; 298 | if ([delegate respondsToSelector:@selector(webSocket:didCloseWithError:)]) { 299 | [delegate webSocket:self didCloseWithError: error]; 300 | } 301 | } 302 | 303 | #pragma mark - SENDING MESSAGES: 304 | 305 | - (void)sendMessage:(NSString *)msg { 306 | [self sendFrame: [msg dataUsingEncoding: NSUTF8StringEncoding] 307 | type: WS_OP_TEXT_FRAME 308 | tag: TAG_MESSAGE]; 309 | } 310 | 311 | - (void)sendBinaryMessage:(NSData*)msg { 312 | [self sendFrame: msg type: WS_OP_BINARY_FRAME tag: TAG_MESSAGE]; 313 | } 314 | 315 | - (void) sendFrame: (NSData*)msgData type: (unsigned)type tag: (long)tag { 316 | HTTPLogTrace(); 317 | 318 | if (_state >= kWebSocketClosing) 319 | return; 320 | 321 | NSMutableData *data = nil; 322 | 323 | if (_isRFC6455) { 324 | // Framing format: http://tools.ietf.org/html/rfc6455#section-5.2 325 | UInt8 header[14] = {(0x80 | (UInt8)type) /*, 0x00...*/}; 326 | NSUInteger headerLen; 327 | 328 | NSUInteger length = msgData.length; 329 | if (length <= 125) { 330 | header[1] = (UInt8)length; 331 | headerLen = 2; 332 | } else if (length <= 0xFFFF) { 333 | header[1] = 0x7E; 334 | UInt16 bigLen = NSSwapHostShortToBig((UInt16)length); 335 | memcpy(&header[2], &bigLen, sizeof(bigLen)); 336 | headerLen = 4; 337 | } else { 338 | header[1] = 0x7F; 339 | UInt64 bigLen = NSSwapHostLongLongToBig(length); 340 | memcpy(&header[2], &bigLen, sizeof(bigLen)); 341 | headerLen = 10; 342 | } 343 | 344 | UInt8 mask[4]; 345 | if (_isClient) { 346 | header[1] |= 0x80; // Sets the 'mask' flag 347 | (void)SecRandomCopyBytes(kSecRandomDefault, sizeof(mask), mask); 348 | memcpy(&header[headerLen], mask, sizeof(mask)); 349 | headerLen += sizeof(mask); 350 | } 351 | 352 | data = [NSMutableData dataWithCapacity: headerLen + length]; 353 | [data appendBytes: header length: headerLen]; 354 | [data appendData: msgData]; 355 | 356 | if (_isClient) { 357 | maskBytes(data, headerLen, length, mask); 358 | } 359 | } else { 360 | data = [NSMutableData dataWithCapacity:([msgData length] + 2)]; 361 | [data appendBytes:"\x00" length:1]; 362 | [data appendData:msgData]; 363 | [data appendBytes:"\xFF" length:1]; 364 | } 365 | 366 | dispatch_async(_websocketQueue, ^{ @autoreleasepool { 367 | if (_state == kWebSocketOpen) { 368 | if (tag == TAG_MESSAGE) { 369 | _writeQueueSize += 1; // data.length would be better 370 | } 371 | [_asyncSocket writeData:data withTimeout:_timeout tag: tag]; 372 | if (tag == TAG_MESSAGE && _writeQueueSize <= HUNGRY_SIZE) 373 | [self isHungry]; 374 | } 375 | }}); 376 | } 377 | 378 | - (void) finishedSendingFrame { 379 | _writeQueueSize -= 1; // data.length would be better but we don't know it anymore 380 | if (_writeQueueSize <= HUNGRY_SIZE) 381 | [self isHungry]; 382 | } 383 | 384 | - (void) isHungry { 385 | HTTPLogTrace(); 386 | // Notify delegate 387 | id delegate = _delegate; 388 | if ([delegate respondsToSelector:@selector(webSocketIsHungry:)]) 389 | [delegate webSocketIsHungry:self]; 390 | } 391 | 392 | #pragma mark - RECEIVING MESSAGES: 393 | 394 | - (BOOL) didReceiveFrame: (NSData*)frame type: (NSUInteger)type { 395 | HTTPLogTrace(); 396 | switch (type) { 397 | case WS_OP_TEXT_FRAME: { 398 | NSString *msg = [[NSString alloc] initWithData: frame encoding: NSUTF8StringEncoding]; 399 | [self didReceiveMessage: msg]; 400 | return YES; 401 | } 402 | case WS_OP_BINARY_FRAME: 403 | [self didReceiveBinaryMessage:frame]; 404 | return YES; 405 | case WS_OP_PING: 406 | [self sendFrame: frame type: WS_OP_PONG tag: 0]; 407 | return YES; 408 | case WS_OP_PONG: 409 | return YES; 410 | case WS_OP_CONNECTION_CLOSE: 411 | if (_state >= kWebSocketClosing) { 412 | // This is presumably an echo of the close frame I sent; time to stop: 413 | [self disconnect]; 414 | } else { 415 | // Peer requested a close, so echo it and then stop: 416 | [self sendFrame: frame type: WS_OP_CONNECTION_CLOSE tag: TAG_STOP]; 417 | self.state = kWebSocketClosing; 418 | } 419 | return NO; 420 | default: 421 | [self didCloseWithCode: kWebSocketCloseProtocolError 422 | reason: @"Unsupported frame type"]; 423 | return NO; 424 | } 425 | } 426 | 427 | - (void)didReceiveMessage:(NSString *)msg { 428 | HTTPLogTrace(); 429 | 430 | // Override me to process incoming messages. 431 | // This method is invoked on the websocketQueue. 432 | // 433 | // For completeness, you should invoke [super didReceiveMessage:msg] in your method. 434 | 435 | // Notify delegate 436 | id delegate = _delegate; 437 | if ([delegate respondsToSelector:@selector(webSocket:didReceiveMessage:)]) { 438 | if (![delegate webSocket:self didReceiveMessage:msg]) 439 | [self _setReadPaused: YES]; 440 | } 441 | } 442 | 443 | - (void)didReceiveBinaryMessage:(NSData *)msg { 444 | HTTPLogTrace(); 445 | 446 | // Override me to process incoming messages. 447 | // This method is invoked on the websocketQueue. 448 | // 449 | // For completeness, you should invoke [super didReceiveBinaryMessage:msg] in your method. 450 | 451 | // Notify delegate 452 | id delegate = _delegate; 453 | if ([delegate respondsToSelector:@selector(webSocket:didReceiveBinaryMessage:)]) { 454 | if (![delegate webSocket:self didReceiveBinaryMessage:msg]) 455 | [self _setReadPaused: YES]; 456 | } 457 | } 458 | 459 | - (void) startReadingNextMessage { 460 | if (_readyToReadMessage && !_readPaused && _state == kWebSocketOpen) { 461 | _readyToReadMessage = NO; 462 | [_asyncSocket readDataToLength:1 withTimeout:_timeout 463 | tag:(_isRFC6455 ? TAG_PAYLOAD_PREFIX : TAG_PREFIX)]; 464 | } 465 | } 466 | 467 | - (BOOL) readPaused { 468 | __block BOOL result; 469 | dispatch_sync(_websocketQueue, ^{ 470 | result = _readPaused; 471 | }); 472 | return result; 473 | } 474 | 475 | - (void) setReadPaused: (BOOL)paused { 476 | dispatch_async(_websocketQueue, ^{ 477 | [self _setReadPaused: paused]; 478 | }); 479 | } 480 | 481 | - (void) _setReadPaused: (BOOL)paused { 482 | _readPaused = paused; 483 | if (!paused) 484 | [self startReadingNextMessage]; 485 | } 486 | 487 | /////////////////////////////////////////////////////////////////////////////////////////////////// 488 | #pragma mark AsyncSocket Delegate 489 | /////////////////////////////////////////////////////////////////////////////////////////////////// 490 | 491 | 492 | - (void)socket:(GCDAsyncSocket *)sock 493 | didReceiveTrust:(SecTrustRef)trust 494 | completionHandler:(void (^)(BOOL shouldTrustPeer))completionHandler 495 | { 496 | // This only gets called if the SSL settings disable regular cert validation. 497 | SecTrustEvaluateAsync(trust, dispatch_get_main_queue(), 498 | ^(SecTrustRef trustRef, SecTrustResultType result) 499 | { 500 | BOOL ok; 501 | id delegate = _delegate; 502 | if ([delegate respondsToSelector: @selector(webSocket:shouldSecureWithTrust:)]) { 503 | ok = [delegate webSocket: self shouldSecureWithTrust: trust]; 504 | } else { 505 | ok = (result == kSecTrustResultProceed || result == kSecTrustResultUnspecified); 506 | } 507 | completionHandler(ok); 508 | }); 509 | } 510 | 511 | // 0 1 2 3 512 | // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 513 | // +-+-+-+-+-------+-+-------------+-------------------------------+ 514 | // |F|R|R|R| opcode|M| Payload len | Extended payload length | 515 | // |I|S|S|S| (4) |A| (7) | (16/64) | 516 | // |N|V|V|V| |S| | (if payload len==126/127) | 517 | // | |1|2|3| |K| | | 518 | // +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + 519 | // | Extended payload length continued, if payload len == 127 | 520 | // + - - - - - - - - - - - - - - - +-------------------------------+ 521 | // | |Masking-key, if MASK set to 1 | 522 | // +-------------------------------+-------------------------------+ 523 | // | Masking-key (continued) | Payload Data | 524 | // +-------------------------------- - - - - - - - - - - - - - - - + 525 | // : Payload Data continued ... : 526 | // + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + 527 | // | Payload Data continued ... | 528 | // +---------------------------------------------------------------+ 529 | 530 | - (BOOL)isValidWebSocketFrame:(UInt8)frame { 531 | NSUInteger rsv = frame & 0x70; 532 | NSUInteger opcode = frame & 0x0F; 533 | return ! ((rsv || (3 <= opcode && opcode <= 7) || (0xB <= opcode && opcode <= 0xF))); 534 | } 535 | 536 | - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag { 537 | HTTPLogTrace(); 538 | 539 | if (tag == TAG_PREFIX) { 540 | UInt8 *pFrame = (UInt8 *)[data bytes]; 541 | UInt8 frame = *pFrame; 542 | 543 | if (frame <= 0x7F) { 544 | [_asyncSocket readDataToData: kTerminator withTimeout:_timeout tag:TAG_MSG_PLUS_SUFFIX]; 545 | } else { 546 | // Unsupported frame type 547 | [self didCloseWithCode: kWebSocketCloseProtocolError 548 | reason: @"Unsupported frame type"]; 549 | } 550 | } else if (tag == TAG_PAYLOAD_PREFIX) { 551 | UInt8 *pFrame = (UInt8 *)[data bytes]; 552 | UInt8 frame = *pFrame; 553 | 554 | if ([self isValidWebSocketFrame: frame]) { 555 | _nextOpCode = (frame & 0x0F); 556 | [_asyncSocket readDataToLength:1 withTimeout:_timeout tag:TAG_PAYLOAD_LENGTH]; 557 | } else { 558 | // Unsupported frame type 559 | [self didCloseWithCode: kWebSocketCloseProtocolError 560 | reason: @"Invalid incoming frame"]; 561 | } 562 | } else if (tag == TAG_PAYLOAD_LENGTH) { 563 | UInt8 frame = *(UInt8 *)[data bytes]; 564 | BOOL masked = WS_PAYLOAD_IS_MASKED(frame); 565 | NSUInteger length = WS_PAYLOAD_LENGTH(frame); 566 | _nextFrameMasked = masked; 567 | _maskingKey = nil; 568 | if (length <= 125) { 569 | if (_nextFrameMasked) 570 | { 571 | [_asyncSocket readDataToLength:4 withTimeout:_timeout tag:TAG_MSG_MASKING_KEY]; 572 | } 573 | if (length > 0) { 574 | [_asyncSocket readDataToLength:length withTimeout:_timeout tag:TAG_MSG_WITH_LENGTH]; 575 | } else { 576 | // Special case: zero-length payload doesn't need any read call at all 577 | [self socket: sock didReadData: [NSData data] withTag: TAG_MSG_WITH_LENGTH]; 578 | } 579 | } else if (length == 126) { 580 | [_asyncSocket readDataToLength:2 withTimeout:_timeout tag:TAG_PAYLOAD_LENGTH16]; 581 | } else { 582 | [_asyncSocket readDataToLength:8 withTimeout:_timeout tag:TAG_PAYLOAD_LENGTH64]; 583 | } 584 | } else if (tag == TAG_PAYLOAD_LENGTH16) { 585 | UInt8 *pFrame = (UInt8 *)[data bytes]; 586 | NSUInteger length = ((NSUInteger)pFrame[0] << 8) | (NSUInteger)pFrame[1]; 587 | if (_nextFrameMasked) { 588 | [_asyncSocket readDataToLength:4 withTimeout:_timeout tag:TAG_MSG_MASKING_KEY]; 589 | } 590 | [_asyncSocket readDataToLength:length withTimeout:_timeout tag:TAG_MSG_WITH_LENGTH]; 591 | } else if (tag == TAG_PAYLOAD_LENGTH64) { 592 | // TODO: 64bit data size in memory? 593 | [self didCloseWithCode: kWebSocketClosePolicyError 594 | reason: @"Oops, 64-bit frame size not yet supported"]; 595 | } else if (tag == TAG_MSG_WITH_LENGTH) { 596 | NSUInteger msgLength = [data length]; 597 | if (_nextFrameMasked && _maskingKey) { 598 | NSMutableData *masked = data.mutableCopy; 599 | maskBytes(masked, 0, msgLength, _maskingKey.bytes); 600 | data = masked; 601 | } 602 | _readyToReadMessage = YES; 603 | if ([self didReceiveFrame: data type: _nextOpCode]) { 604 | // Read next frame 605 | [self startReadingNextMessage]; 606 | } 607 | } else if (tag == TAG_MSG_MASKING_KEY) { 608 | _maskingKey = data.copy; 609 | } else { 610 | NSUInteger msgLength = [data length] - 1; // Excluding ending 0xFF frame 611 | NSString *msg = [[NSString alloc] initWithBytes:[data bytes] length:msgLength encoding:NSUTF8StringEncoding]; 612 | _readyToReadMessage = YES; 613 | [self didReceiveMessage:msg]; 614 | // Read next message 615 | [self startReadingNextMessage]; 616 | } 617 | } 618 | 619 | - (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag { 620 | HTTPLogTrace(); 621 | if (tag == TAG_STOP) { 622 | [self disconnect]; 623 | } else if (tag == TAG_MESSAGE) { 624 | [self finishedSendingFrame]; 625 | } 626 | } 627 | 628 | - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)error { 629 | HTTPLogTraceWith("error= %@", error.localizedDescription); 630 | if ([error.domain isEqualToString: @"kCFStreamErrorDomainSSL"]) { 631 | // This is CGDAsyncSocket returning a SecureTransport error code. Map to NSURLError: 632 | NSInteger urlCode; 633 | switch (error.code) { 634 | case errSSLXCertChainInvalid: 635 | case errSSLUnknownRootCert: 636 | urlCode = NSURLErrorServerCertificateUntrusted; 637 | break; 638 | case errSSLNoRootCert: 639 | urlCode = NSURLErrorServerCertificateHasUnknownRoot; 640 | break; 641 | case errSSLCertExpired: 642 | urlCode = NSURLErrorServerCertificateHasBadDate; 643 | break; 644 | case errSSLCertNotYetValid: 645 | urlCode = NSURLErrorServerCertificateNotYetValid; 646 | break; 647 | default: 648 | urlCode = NSURLErrorSecureConnectionFailed; 649 | break; 650 | } 651 | error = [NSError errorWithDomain: NSURLErrorDomain 652 | code: urlCode 653 | userInfo: @{NSUnderlyingErrorKey: error}]; 654 | } 655 | [self didCloseWithError: error]; 656 | } 657 | 658 | @end 659 | -------------------------------------------------------------------------------- /WebSocket/WebSocketClient.h: -------------------------------------------------------------------------------- 1 | // 2 | // WebSocketClient.h 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 9/10/13. 6 | // Copyright (c) 2013 Couchbase, Inc. All rights reserved. 7 | 8 | #import "WebSocket.h" 9 | 10 | 11 | /** Client WebSocket -- opens a connection to a remote host. */ 12 | @interface WebSocketClient : WebSocket 13 | 14 | /** Designated initializer. 15 | The WebSocket's timeout will be set from the timeout of the URL request. 16 | If the scheme of the URL is "https", default TLS settings will be used. */ 17 | - (instancetype) initWithURLRequest:(NSURLRequest *)request; 18 | 19 | /** Convenience initializer that calls -initWithURLRequest using a GET request to the given URL 20 | with no special HTTP headers and the default timeout. */ 21 | - (instancetype) initWithURL: (NSURL*)url; 22 | 23 | /** Once the request has been customized, call this to open the connection. */ 24 | - (BOOL) connect: (NSError**)outError; 25 | 26 | /** The URL this WebSocket connects to. */ 27 | @property (readonly) NSURL* URL; 28 | 29 | /** Authentication credential. */ 30 | @property NSURLCredential* credential; 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /WebSocket/WebSocketClient.m: -------------------------------------------------------------------------------- 1 | // 2 | // WebSocketClient.m 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 9/10/13. 6 | // Copyright (c) 2013 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 "WebSocketClient.h" 17 | #import "WebSocket_Internal.h" 18 | #import "WebSocketHTTPLogic.h" 19 | #import "GCDAsyncSocket.h" 20 | #import "DDData.h" 21 | 22 | #import 23 | 24 | 25 | @implementation WebSocketClient 26 | { 27 | GCDAsyncSocket* _httpSocket; 28 | WebSocketHTTPLogic *_logic; 29 | NSArray* _protocols; 30 | NSString* _nonceKey; 31 | } 32 | 33 | 34 | @synthesize credential=_credential; 35 | 36 | 37 | - (instancetype) initWithURLRequest:(NSURLRequest *)urlRequest { 38 | self = [super init]; 39 | if (self) { 40 | _isClient = YES; 41 | _logic = [[WebSocketHTTPLogic alloc] initWithURLRequest: urlRequest]; 42 | _logic.handleRedirects = YES; 43 | self.timeout = urlRequest.timeoutInterval; 44 | } 45 | return self; 46 | } 47 | 48 | - (instancetype) initWithURL:(NSURL*)url { 49 | return [self initWithURLRequest: [NSURLRequest requestWithURL: url]]; 50 | } 51 | 52 | - (void)dealloc { 53 | HTTPLogTrace(); 54 | [_httpSocket setDelegate:nil delegateQueue:NULL]; 55 | [_httpSocket disconnect]; 56 | } 57 | 58 | 59 | - (NSURL*) URL { 60 | return _logic.URL; 61 | } 62 | 63 | 64 | - (BOOL) connect: (NSError**)outError { 65 | __block BOOL result = NO; 66 | dispatch_sync(_websocketQueue, ^{ 67 | result = [self _connect: outError]; 68 | }); 69 | return result; 70 | } 71 | 72 | 73 | // called on the _websocketQueue 74 | - (BOOL) _connect: (NSError**)outError { 75 | HTTPLogTrace(); 76 | NSParameterAssert(!_asyncSocket); 77 | NSParameterAssert(!_httpSocket); 78 | NSURL* url = _logic.URL; 79 | _httpSocket = [[GCDAsyncSocket alloc] initWithDelegate: self 80 | delegateQueue: _websocketQueue]; 81 | [_httpSocket setDelegate:self delegateQueue:_websocketQueue]; 82 | if (![_httpSocket connectToHost: url.host 83 | onPort: _logic.port 84 | withTimeout: self.timeout 85 | error: outError]) { 86 | return NO; 87 | } 88 | 89 | if (_tlsSettings || _logic.useTLS) { 90 | NSMutableDictionary* settings = [_tlsSettings mutableCopy]; 91 | if (!settings) 92 | settings = [NSMutableDictionary dictionary]; 93 | if (!settings[(id)kCFStreamSSLPeerName]) 94 | settings[(id)kCFStreamSSLPeerName] = url.host; 95 | [_httpSocket startTLS: settings]; 96 | } 97 | 98 | // Configure the nonce/key for the request: 99 | uint8_t nonceBytes[16]; 100 | SecRandomCopyBytes(kSecRandomDefault, sizeof(nonceBytes), nonceBytes); 101 | NSData* nonceData = [NSData dataWithBytes: nonceBytes length: sizeof(nonceBytes)]; 102 | _nonceKey = [nonceData base64Encoded]; 103 | 104 | // Construct the HTTP request: 105 | _logic[@"Connection"] = @"Upgrade"; 106 | _logic[@"Upgrade"] = @"websocket"; 107 | _logic[@"Sec-WebSocket-Version"] = @"13"; 108 | _logic[@"Sec-WebSocket-Key"] = _nonceKey; 109 | if (_protocols) 110 | _logic[@"Sec-WebSocket-Protocol"] = [_protocols componentsJoinedByString: @","]; 111 | 112 | CFHTTPMessageRef httpMsg = [_logic newHTTPRequest]; 113 | NSData* requestData = CFBridgingRelease(CFHTTPMessageCopySerializedMessage(httpMsg)); 114 | CFRelease(httpMsg); 115 | 116 | // Send the request and await the response: 117 | //NSLog(@"Sending HTTP request:\n%@", [[NSString alloc] initWithData: requestData encoding:NSUTF8StringEncoding]); 118 | [_httpSocket writeData: requestData withTimeout: self.timeout 119 | tag: TAG_HTTP_REQUEST_HEADERS]; 120 | [_httpSocket readDataToData: [@"\r\n\r\n" dataUsingEncoding: NSASCIIStringEncoding] 121 | withTimeout: self.timeout 122 | tag: TAG_HTTP_RESPONSE_HEADERS]; 123 | return YES; 124 | } 125 | 126 | 127 | // called on the _websocketQueue 128 | - (void) gotHTTPResponse: (CFHTTPMessageRef)httpResponse data: (NSData*)responseData { 129 | HTTPLogTrace(); 130 | //NSLog(@"Got HTTP response:\n%@", [[NSString alloc] initWithData: responseData encoding:NSUTF8StringEncoding]); 131 | if (!CFHTTPMessageAppendBytes(httpResponse, responseData.bytes, responseData.length) || 132 | !CFHTTPMessageIsHeaderComplete(httpResponse)) { 133 | // Error reading response! 134 | [self didCloseWithCode: kWebSocketCloseProtocolError 135 | reason: @"Unreadable HTTP response"]; 136 | return; 137 | } 138 | 139 | [_logic receivedResponse: httpResponse]; 140 | if (_logic.shouldRetry) { 141 | // Retry the connection, due to a redirect or auth challenge: 142 | [_httpSocket setDelegate:nil delegateQueue:NULL]; 143 | [_httpSocket disconnect]; 144 | _httpSocket = nil; 145 | [self _connect: NULL]; 146 | return; 147 | } 148 | 149 | NSInteger httpStatus = _logic.httpStatus; 150 | if (httpStatus != 101) { 151 | WebSocketCloseCode closeCode = kWebSocketClosePolicyError; 152 | if (httpStatus >= 300 && httpStatus < 1000) 153 | closeCode = (WebSocketCloseCode)httpStatus; 154 | NSString* reason = CFBridgingRelease(CFHTTPMessageCopyResponseStatusLine(httpResponse)); 155 | [self didCloseWithCode: closeCode reason: reason]; 156 | return; 157 | } else if (!checkHeader(httpResponse, @"Connection", @"Upgrade", NO)) { 158 | [self didCloseWithCode: kWebSocketCloseProtocolError 159 | reason: @"Invalid 'Connection' header"]; 160 | return; 161 | } else if (!checkHeader(httpResponse, @"Upgrade", @"websocket", NO)) { 162 | [self didCloseWithCode: kWebSocketCloseProtocolError 163 | reason: @"Invalid 'Upgrade' header"]; 164 | return; 165 | } 166 | 167 | // Compute the value for the Sec-WebSocket-Accept header: 168 | NSString* str = [_nonceKey stringByAppendingString: @"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"]; 169 | str = [[[str dataUsingEncoding: NSASCIIStringEncoding] sha1Digest] base64Encoded]; 170 | 171 | if (!checkHeader(httpResponse, @"Sec-WebSocket-Accept", str, YES)) { 172 | [self didCloseWithCode: kWebSocketCloseProtocolError 173 | reason: @"Invalid 'Sec-WebSocket-Accept' header"]; 174 | return; 175 | } 176 | 177 | // TODO: Check Sec-WebSocket-Extensions for unknown extensions 178 | 179 | // Now I can hook up the socket as my asyncSocket and start the WebSocket protocol: 180 | _asyncSocket = _httpSocket; 181 | _httpSocket = nil; 182 | [self start]; 183 | } 184 | 185 | 186 | // called on the _websocketQueue 187 | - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag { 188 | if (sock == _httpSocket) { 189 | if (tag == TAG_HTTP_RESPONSE_HEADERS) { 190 | // HTTP response received: 191 | CFHTTPMessageRef httpResponse = CFHTTPMessageCreateEmpty(NULL, false); 192 | [self gotHTTPResponse: httpResponse data: data]; 193 | CFRelease(httpResponse); 194 | } 195 | } else { 196 | [super socket: sock didReadData: data withTag: tag]; 197 | } 198 | } 199 | 200 | 201 | // Tests whether a header value matches the expected string. 202 | static BOOL checkHeader(CFHTTPMessageRef msg, NSString* header, NSString* expected, BOOL caseSens) { 203 | NSString* value = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(msg, 204 | (__bridge CFStringRef)header)); 205 | if (caseSens) 206 | return [value isEqualToString: expected]; 207 | else 208 | return value && [value caseInsensitiveCompare: expected] == 0; 209 | } 210 | 211 | 212 | @end 213 | -------------------------------------------------------------------------------- /WebSocket/WebSocketHTTPLogic.h: -------------------------------------------------------------------------------- 1 | // 2 | // WebSocketHTTPLogic.h 3 | // CouchbaseLite 4 | // 5 | // Created by Jens Alfke on 11/13/13. 6 | // 7 | // 8 | 9 | #import 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 WebSocketHTTPLogic : 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 | /** Creates an HTTP request message to send. Caller is responsible for releasing it. */ 26 | - (CFHTTPMessageRef) newHTTPRequest; 27 | 28 | /** Call this when a response is received, then check shouldContinue and shouldRetry. */ 29 | - (void) receivedResponse: (CFHTTPMessageRef)response; 30 | 31 | /** After a response is received, this will be YES if the HTTP status indicates success. */ 32 | @property (readonly) BOOL shouldContinue; 33 | 34 | /** After a response is received, this will be YES if the client needs to retry with a new 35 | request. If so, it should call -createHTTPRequest again to get the new request, which will 36 | have either a different URL or new authentication headers. */ 37 | @property (readonly) BOOL shouldRetry; 38 | 39 | /** The URL. This will change after receiving a redirect response. */ 40 | @property (readonly) NSURL* URL; 41 | 42 | /** The TCP port number, based on the URL. */ 43 | @property (readonly) UInt16 port; 44 | 45 | /** Yes if TLS/SSL should be used (based on the URL). */ 46 | @property (readonly) BOOL useTLS; 47 | 48 | /** The auth credential being used. */ 49 | @property (readonly) NSURLCredential* credential; 50 | 51 | /** The HTTP status code of the response. */ 52 | @property (readonly) int httpStatus; 53 | 54 | /** The error from a failed redirect or authentication. This isn't set for regular non-success 55 | HTTP statuses like 404, only for failures to redirect or authenticate. */ 56 | @property (readonly) NSError* error; 57 | 58 | @end 59 | -------------------------------------------------------------------------------- /WebSocket/WebSocketHTTPLogic.m: -------------------------------------------------------------------------------- 1 | // 2 | // WebSocketHTTPLogic.m 3 | // CouchbaseLite 4 | // 5 | // Created by Jens Alfke on 11/13/13. 6 | // 7 | // 8 | 9 | #import "WebSocketHTTPLogic.h" 10 | #import "DDData.h" 11 | #import "Logging.h" 12 | #import "Test.h" 13 | #import "MYURLUtils.h" 14 | 15 | 16 | #define kMaxRedirects 10 17 | 18 | 19 | @implementation WebSocketHTTPLogic 20 | { 21 | NSMutableURLRequest* _urlRequest; 22 | NSString* _nonceKey; 23 | NSString* _authorizationHeader; 24 | CFHTTPMessageRef _responseMsg; 25 | NSURLCredential* _credential; 26 | NSUInteger _redirectCount; 27 | } 28 | 29 | 30 | @synthesize handleRedirects=_handleRedirects, shouldContinue=_shouldContinue, 31 | shouldRetry=_shouldRetry, credential=_credential, httpStatus=_httpStatus, error=_error; 32 | 33 | 34 | - (instancetype) initWithURLRequest:(NSURLRequest *)urlRequest { 35 | NSParameterAssert(urlRequest); 36 | self = [super init]; 37 | if (self) { 38 | _urlRequest = [urlRequest mutableCopy]; 39 | } 40 | return self; 41 | } 42 | 43 | 44 | - (void) dealloc { 45 | if (_responseMsg) CFRelease(_responseMsg); 46 | } 47 | 48 | 49 | - (NSURL*) URL { 50 | return _urlRequest.URL; 51 | } 52 | 53 | 54 | - (UInt16) port { 55 | NSNumber* portObj = self.URL.port; 56 | if (portObj) 57 | return (UInt16)portObj.intValue; 58 | else 59 | return self.useTLS ? 443 : 80; 60 | } 61 | 62 | - (BOOL) useTLS { 63 | NSString* scheme = self.URL.scheme.lowercaseString; 64 | return [scheme isEqualToString: @"https"] || [scheme isEqualToString: @"wss"]; 65 | } 66 | 67 | 68 | - (void) setValue: (NSString*)value forHTTPHeaderField:(NSString*)header { 69 | [_urlRequest setValue: value forHTTPHeaderField: header]; 70 | } 71 | 72 | - (void) setObject: (NSString*)value forKeyedSubscript: (NSString*)key { 73 | [_urlRequest setValue: value forHTTPHeaderField: key]; 74 | } 75 | 76 | 77 | - (CFHTTPMessageRef) newHTTPRequest { 78 | NSURL* url = self.URL; 79 | // Set/update the "Host" header: 80 | NSString* host = url.host; 81 | if (url.port) 82 | host = [host stringByAppendingFormat: @":%@", url.port]; 83 | [self setValue: host forHTTPHeaderField: @"Host"]; 84 | 85 | // Create the CFHTTPMessage: 86 | CFHTTPMessageRef httpMsg = CFHTTPMessageCreateRequest(NULL, 87 | (__bridge CFStringRef)_urlRequest.HTTPMethod, 88 | (__bridge CFURLRef)url, 89 | kCFHTTPVersion1_1); 90 | NSDictionary* headers = _urlRequest.allHTTPHeaderFields; 91 | for (NSString* header in headers) 92 | CFHTTPMessageSetHeaderFieldValue(httpMsg, (__bridge CFStringRef)header, 93 | (__bridge CFStringRef)headers[header]); 94 | 95 | // Add cookie headers from the NSHTTPCookieStorage: 96 | if (_urlRequest.HTTPShouldHandleCookies) { 97 | NSArray* cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL: url]; 98 | NSDictionary* cookieHeaders = [NSHTTPCookie requestHeaderFieldsWithCookies: cookies]; 99 | for (NSString* headerName in cookieHeaders) { 100 | CFHTTPMessageSetHeaderFieldValue(httpMsg, 101 | (__bridge CFStringRef)headerName, 102 | (__bridge CFStringRef)cookieHeaders[headerName]); 103 | } 104 | } 105 | 106 | // If this is a retry, set auth headers from the credential we got: 107 | if (_responseMsg && _credential) { 108 | NSString* password = _credential.password; 109 | if (!password) { 110 | // For some reason the password sometimes isn't accessible, even though we checked 111 | // .hasPassword when setting _credential earlier. (See #195.) Keychain bug?? 112 | // If this happens, try looking up the credential again: 113 | LogTo(ChangeTracker, @"Huh, couldn't get password of %@; trying again", _credential); 114 | _credential = [self credentialForAuthHeader: 115 | getHeader(_responseMsg, @"WWW-Authenticate")]; 116 | password = _credential.password; 117 | } 118 | if (password) { 119 | Assert(CFHTTPMessageAddAuthentication(httpMsg, _responseMsg, 120 | (__bridge CFStringRef)_credential.user, 121 | (__bridge CFStringRef)password, 122 | NULL, 123 | _httpStatus == 407)); 124 | } else { 125 | Warn(@"%@: Unable to get password of credential %@", self, _credential); 126 | _credential = nil; 127 | CFRelease(_responseMsg); 128 | _responseMsg = NULL; 129 | } 130 | } 131 | 132 | NSData* body = _urlRequest.HTTPBody; 133 | if (body) { 134 | CFHTTPMessageSetHeaderFieldValue(httpMsg, CFSTR("Content-Length"), 135 | (__bridge CFStringRef)[@(body.length) description]); 136 | CFHTTPMessageSetBody(httpMsg, (__bridge CFDataRef)body); 137 | } 138 | 139 | _authorizationHeader = getHeader(httpMsg, @"Authorization"); 140 | _shouldContinue = _shouldRetry = NO; 141 | _httpStatus = 0; 142 | 143 | return httpMsg; 144 | } 145 | 146 | 147 | - (void) receivedResponse: (CFHTTPMessageRef)response { 148 | NSParameterAssert(response); 149 | if (response == _responseMsg) 150 | return; 151 | if (_responseMsg) 152 | CFRelease(_responseMsg); 153 | _responseMsg = response; 154 | CFRetain(_responseMsg); 155 | 156 | _shouldContinue = _shouldRetry = NO; 157 | _httpStatus = (int) CFHTTPMessageGetResponseStatusCode(_responseMsg); 158 | switch (_httpStatus) { 159 | case 301: 160 | case 302: 161 | case 307: { 162 | // Redirect: 163 | if (!_handleRedirects) 164 | break; 165 | if (++_redirectCount > kMaxRedirects) { 166 | [self setErrorCode: NSURLErrorHTTPTooManyRedirects userInfo: nil]; 167 | } else if (![self redirect]) { 168 | [self setErrorCode: NSURLErrorRedirectToNonExistentLocation userInfo: nil]; 169 | } else { 170 | _shouldRetry = YES; 171 | } 172 | break; 173 | } 174 | 175 | case 401: 176 | case 407: { 177 | NSString* authResponse = getHeader(_responseMsg, @"WWW-Authenticate"); 178 | if (!_credential && !_authorizationHeader) { 179 | _credential = [self credentialForAuthHeader: authResponse]; 180 | LogTo(ChangeTracker, @"%@: Auth challenge; credential = %@", self, _credential); 181 | if (_credential) { 182 | // Recoverable auth failure -- try again with new _credential: 183 | _shouldRetry = YES; 184 | break; 185 | } 186 | } 187 | Log(@"%@: HTTP auth failed; sent Authorization: %@ ; got WWW-Authenticate: %@", 188 | self, _authorizationHeader, authResponse); 189 | NSDictionary* errorInfo = $dict({@"HTTPAuthorization", _authorizationHeader}, 190 | {@"HTTPAuthenticateHeader", authResponse}); 191 | [self setErrorCode: NSURLErrorUserAuthenticationRequired userInfo: errorInfo]; 192 | break; 193 | } 194 | 195 | default: 196 | if (_httpStatus < 300) 197 | _shouldContinue = YES; 198 | break; 199 | } 200 | } 201 | 202 | 203 | - (BOOL) redirect { 204 | NSString* location = getHeader(_responseMsg, @"Location"); 205 | if (!location) 206 | return NO; 207 | NSURL* url = [NSURL URLWithString: location relativeToURL: self.URL]; 208 | if (!url) 209 | return NO; 210 | if ([url.scheme caseInsensitiveCompare: @"http"] != 0 && 211 | [url.scheme caseInsensitiveCompare: @"https"] != 0) 212 | return NO; 213 | _urlRequest.URL = url; 214 | return YES; 215 | } 216 | 217 | 218 | - (NSURLCredential*) credentialForAuthHeader: (NSString*)authHeader { 219 | NSString* realm; 220 | NSString* authenticationMethod; 221 | 222 | // Basic & digest auth: http://www.ietf.org/rfc/rfc2617.txt 223 | if (!authHeader) 224 | return nil; 225 | 226 | // Get the auth type: 227 | if ([authHeader hasPrefix: @"Basic"]) 228 | authenticationMethod = NSURLAuthenticationMethodHTTPBasic; 229 | else if ([authHeader hasPrefix: @"Digest"]) 230 | authenticationMethod = NSURLAuthenticationMethodHTTPDigest; 231 | else 232 | return nil; 233 | 234 | // Get the realm: 235 | NSRange r = [authHeader rangeOfString: @"realm=\""]; 236 | if (r.length == 0) 237 | return nil; 238 | NSUInteger start = NSMaxRange(r); 239 | r = [authHeader rangeOfString: @"\"" options: 0 240 | range: NSMakeRange(start, authHeader.length - start)]; 241 | if (r.length == 0) 242 | return nil; 243 | realm = [authHeader substringWithRange: NSMakeRange(start, r.location - start)]; 244 | 245 | NSURLCredential* cred; 246 | cred = [self.URL my_credentialForRealm: realm authenticationMethod: authenticationMethod]; 247 | if (!cred.hasPassword) 248 | cred = nil; // TODO: Add support for client certs 249 | return cred; 250 | } 251 | 252 | 253 | - (void) setErrorCode: (NSInteger)code userInfo: (NSDictionary*)userInfo { 254 | NSMutableDictionary* info = $mdict({NSURLErrorFailingURLErrorKey, self.URL}); 255 | if (userInfo) 256 | [info addEntriesFromDictionary: userInfo]; 257 | _error = [NSError errorWithDomain: NSURLErrorDomain code: code userInfo: info]; 258 | } 259 | 260 | 261 | static NSString* getHeader(CFHTTPMessageRef message, NSString* header) { 262 | return CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(message, 263 | (__bridge CFStringRef)header)); 264 | } 265 | 266 | 267 | @end 268 | -------------------------------------------------------------------------------- /WebSocket/WebSocketListener.h: -------------------------------------------------------------------------------- 1 | // 2 | // WebSocketListener.h 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 9/16/13. 6 | // Copyright (c) 2013 Couchbase. All rights reserved. 7 | 8 | #import "WebSocket.h" 9 | 10 | 11 | /** A listener/server for incoming WebSocket connections. 12 | This is actually a special-purpose HTTP listener that only handles a GET for the given path, with the right WebSocket upgrade headers. */ 13 | @interface WebSocketListener : NSObject 14 | 15 | /** Initializes a WebSocketListener. 16 | @param path The URI path to accept requests on (other paths will get a 404 response.) 17 | @param delegate The object that will become the delegate of WebSockets accepted by this listener. */ 18 | - (instancetype) initWithPath: (NSString*)path 19 | delegate: (id)delegate; 20 | 21 | /** The URI path the listener is accepting requests on. */ 22 | @property (readonly) NSString* path; 23 | 24 | /** Starts the listener. 25 | @param interface The name of the network interface, or nil to listen on all interfaces 26 | (See the GCDAsyncSocket documentation for more details.) 27 | @param port The TCP port to listen on. 28 | @param error On return, will be filled in with an error if the method returned NO. 29 | @return YES on success, NO on failure. */ 30 | - (BOOL) acceptOnInterface: (NSString*)interface 31 | port: (UInt16)port 32 | error: (NSError**)error; 33 | 34 | /** Stops the listener from accepting any more connections. */ 35 | - (void) disconnect; 36 | 37 | @end 38 | 39 | 40 | /** A WebSocket created from an incoming request by a WebSocketListener. 41 | You don't instantiate this class directly. */ 42 | @interface WebSocketIncoming : WebSocket 43 | 44 | @property WebSocketListener* listener; 45 | 46 | /** The approximate URL of the peer connection; will contain just an IP address and port. */ 47 | @property (readonly) NSURL* URL; 48 | 49 | @end -------------------------------------------------------------------------------- /WebSocket/WebSocketListener.m: -------------------------------------------------------------------------------- 1 | // 2 | // WebSocketListener.m 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 9/16/13. 6 | // Copyright (c) 2013 Couchbase. 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 "WebSocketListener.h" 17 | #import "WebSocket_Internal.h" 18 | #import "GCDAsyncSocket.h" 19 | #import "DDData.h" 20 | #import "CollectionUtils.h" 21 | #import "Logging.h" 22 | #import "MYURLUtils.h" 23 | 24 | 25 | @interface WebSocketListener () 26 | @end 27 | 28 | 29 | @implementation WebSocketListener 30 | { 31 | NSString* _path; 32 | NSString* _desc; 33 | GCDAsyncSocket* _listenerSocket; 34 | __weak id _delegate; 35 | NSMutableSet* _connections; 36 | } 37 | 38 | 39 | @synthesize path=_path; 40 | 41 | 42 | - (instancetype) initWithPath: (NSString*)path delegate: (id)delegate { 43 | self = [super init]; 44 | if (self) { 45 | _path = path; 46 | _delegate = delegate; 47 | _desc = path; 48 | dispatch_queue_t delegateQueue = dispatch_queue_create("WebSocketListener", 0); 49 | _listenerSocket = [[GCDAsyncSocket alloc] initWithDelegate: self 50 | delegateQueue: delegateQueue]; 51 | _connections = [[NSMutableSet alloc] init]; 52 | } 53 | return self; 54 | } 55 | 56 | 57 | - (NSString*) description { 58 | return [NSString stringWithFormat: @"%@[%@]", self.class, _desc]; 59 | } 60 | 61 | 62 | 63 | - (BOOL) acceptOnInterface: (NSString*)interface 64 | port: (UInt16)port 65 | error: (NSError**)outError 66 | { 67 | if (![_listenerSocket acceptOnInterface: interface port: port error: outError]) 68 | return NO; 69 | _desc = $sprintf(@"%@:%d%@", (interface ?: @""), port, _path); 70 | LogTo(WS, @"%@ now listening on port %d", self, port); 71 | return YES; 72 | } 73 | 74 | 75 | - (void) disconnect { 76 | [_listenerSocket disconnect]; 77 | } 78 | 79 | 80 | - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket { 81 | WebSocketIncoming* ws = [[WebSocketIncoming alloc] initWithConnectedSocket: newSocket 82 | delegate: _delegate]; 83 | ws.listener = self; 84 | LogTo(WS, @"Opened incoming %@ with delegate %@", ws, ws.delegate); 85 | [_connections addObject: ws]; 86 | } 87 | 88 | @end 89 | 90 | 91 | 92 | 93 | @implementation WebSocketIncoming 94 | 95 | @synthesize listener=_listener; 96 | 97 | - (void)dealloc 98 | { 99 | LogTo(WS, @"DEALLOC %@", self); 100 | } 101 | 102 | - (void) didOpen { 103 | // When the underlying socket opens, start reading the incoming HTTP request, 104 | // but don't call the inherited -isOpen till we're ready to talk WebSocket protocol. 105 | 106 | [_asyncSocket readDataToData: [@"\r\n\r\n" dataUsingEncoding: NSASCIIStringEncoding] 107 | withTimeout: self.timeout 108 | tag: TAG_HTTP_REQUEST_HEADERS]; 109 | } 110 | 111 | 112 | static NSString* getHeader(CFHTTPMessageRef msg, NSString* header) { 113 | return CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(msg, (__bridge CFStringRef)header)); 114 | } 115 | 116 | static BOOL checkHeader(CFHTTPMessageRef msg, NSString* header, NSString* expected, BOOL caseSens) { 117 | NSString* value = getHeader(msg, header); 118 | if (caseSens) 119 | return [value isEqualToString: expected]; 120 | else 121 | return value && [value caseInsensitiveCompare: expected] == 0; 122 | } 123 | 124 | 125 | - (void) gotHTTPRequest: (CFHTTPMessageRef)httpRequest data: (NSData*)requestData { 126 | HTTPLogTrace(); 127 | //NSLog(@"Got HTTP request:\n%@", [[NSString alloc] initWithData: requestData encoding:NSUTF8StringEncoding]); 128 | if (!CFHTTPMessageAppendBytes(httpRequest, requestData.bytes, requestData.length) || 129 | !CFHTTPMessageIsHeaderComplete(httpRequest)) { 130 | // Error reading request! 131 | LogTo(WS, @"Unreadable HTTP request:\n%@", requestData.my_UTF8ToString); 132 | [_asyncSocket disconnect]; 133 | [self didCloseWithCode: kWebSocketCloseProtocolError 134 | reason: @"Unreadable HTTP request"]; 135 | return; 136 | } 137 | 138 | NSString* method = CFBridgingRelease(CFHTTPMessageCopyRequestMethod(httpRequest)); 139 | NSURL* url = CFBridgingRelease(CFHTTPMessageCopyRequestURL(httpRequest)); 140 | 141 | int status; 142 | NSString* statusText = @""; 143 | NSString* acceptStr; 144 | if (![url.path isEqualToString: _listener.path]) { 145 | status = 404; 146 | statusText = @"Not found"; 147 | } else if (![method isEqualToString: @"GET"]) { 148 | status = 405; 149 | statusText = @"Unsupported method"; 150 | } else if (!checkHeader(httpRequest, @"Connection", @"Upgrade", NO) 151 | || !checkHeader(httpRequest, @"Upgrade", @"websocket", NO)) { 152 | status = 400; 153 | statusText = @"Invalid upgrade request"; 154 | } else if (!checkHeader(httpRequest, @"Sec-WebSocket-Version", @"13", YES)) { 155 | status = 426; 156 | statusText = @"Unsupported WebSocket protocol version"; 157 | } else { 158 | NSString* nonceKey = getHeader(httpRequest, @"Sec-WebSocket-Key"); 159 | if (nonceKey.length != 24) { 160 | status = 400; 161 | statusText = @"Invalid/missing Sec-WebSocket-Key header"; 162 | } else { 163 | // Ask the delegate; 164 | status = [self statusForHTTPRequest: httpRequest]; 165 | if (status >= 300) { 166 | statusText = @"Connection refused"; 167 | } else { 168 | // Accept it! 169 | status = 101; 170 | acceptStr = [nonceKey stringByAppendingString: @"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"]; 171 | acceptStr = [[[acceptStr dataUsingEncoding: NSASCIIStringEncoding] sha1Digest] base64Encoded]; 172 | } 173 | } 174 | } 175 | 176 | // Write the HTTP response: 177 | NSMutableString* response = [NSMutableString stringWithFormat: 178 | @"HTTP/1.1 %d %@\r\n" 179 | "Server: CocoaWebSocket\r\n" 180 | "Sec-WebSocket-Version: 13\r\n", 181 | status, statusText]; 182 | if (status == 101) { 183 | [response appendFormat: @"Connection: Upgrade\r\n" 184 | "Upgrade: websocket\r\n" 185 | "Sec-WebSocket-Accept: %@\r\n", 186 | acceptStr]; 187 | } 188 | [response appendString: @"\r\n"]; 189 | [_asyncSocket writeData: [response dataUsingEncoding: NSUTF8StringEncoding] 190 | withTimeout: self.timeout tag: TAG_HTTP_RESPONSE_HEADERS]; 191 | 192 | if (status != 101) { 193 | [_asyncSocket disconnectAfterWriting]; 194 | [self didCloseWithCode: kWebSocketCloseProtocolError 195 | reason: $sprintf(@"Invalid HTTP request: %@", statusText)]; 196 | return; 197 | } 198 | 199 | // Now I can finally tell the delegate I'm open (see explanation in my -didOpen method.) 200 | LogTo(WS, @"Accepted incoming connection"); 201 | [super didOpen]; 202 | } 203 | 204 | 205 | - (int) statusForHTTPRequest: (CFHTTPMessageRef)httpRequest { 206 | int status = 101; 207 | if ([_delegate respondsToSelector: @selector(webSocket:shouldAccept:)]) { 208 | NSURL* url = CFBridgingRelease(CFHTTPMessageCopyRequestURL(httpRequest)); 209 | NSDictionary* headers = CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(httpRequest)); 210 | NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL: url]; 211 | for (NSString* header in headers) 212 | [request setValue: headers[header] forHTTPHeaderField: header]; 213 | status = [_delegate webSocket: self shouldAccept: request]; 214 | if (status == NO) 215 | status = 403; 216 | else if (status == YES) 217 | status = 101; 218 | } 219 | return status; 220 | } 221 | 222 | 223 | - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag { 224 | if (tag == TAG_HTTP_REQUEST_HEADERS) { 225 | // HTTP request received: 226 | CFHTTPMessageRef httpRequest = CFHTTPMessageCreateEmpty(NULL, true); 227 | [self gotHTTPRequest: httpRequest data: data]; 228 | CFRelease(httpRequest); 229 | } else { 230 | [super socket: sock didReadData: data withTag: tag]; 231 | } 232 | } 233 | 234 | 235 | - (NSURL*) URL { 236 | return $url($sprintf(@"ws://%@:%d/", _asyncSocket.connectedHost, _asyncSocket.connectedPort)); 237 | } 238 | 239 | 240 | @end 241 | -------------------------------------------------------------------------------- /WebSocket/WebSocket_Internal.h: -------------------------------------------------------------------------------- 1 | // 2 | // WebSocket_Internal.h 3 | // WebSocket 4 | // 5 | // Created by Jens Alfke on 9/10/13. 6 | // Copyright (c) 2013 Couchbase. All rights reserved. 7 | 8 | #import "WebSocket.h" 9 | #import "GCDAsyncSocket.h" 10 | #import "MYLogging.h" 11 | 12 | 13 | #define TIMEOUT_NONE -1 14 | #define TIMEOUT_DEFAULT TIMEOUT_NONE //60 15 | #define TIMEOUT_REQUEST_BODY 10 16 | 17 | // WebSocket frame types: 18 | #define WS_OP_CONTINUATION_FRAME 0 19 | #define WS_OP_TEXT_FRAME 1 20 | #define WS_OP_BINARY_FRAME 2 21 | #define WS_OP_CONNECTION_CLOSE 8 22 | #define WS_OP_PING 9 23 | #define WS_OP_PONG 10 24 | 25 | UsingLogDomain(WS); 26 | #define HTTPLogTrace() LogTo(WS, @"TRACE: %s", __func__ ) 27 | #define HTTPLogTraceWith(MSG, PARAM...) LogTo(WS, @"TRACE: %s " MSG, __func__, PARAM) 28 | 29 | enum { 30 | // Tags for reads: 31 | TAG_PREFIX = 300, 32 | TAG_MSG_PLUS_SUFFIX, 33 | TAG_MSG_WITH_LENGTH, 34 | TAG_MSG_MASKING_KEY, 35 | TAG_PAYLOAD_PREFIX, 36 | TAG_PAYLOAD_LENGTH, 37 | TAG_PAYLOAD_LENGTH16, 38 | TAG_PAYLOAD_LENGTH64, 39 | 40 | // Tags for writes: 41 | TAG_MESSAGE = 400, 42 | TAG_STOP, 43 | 44 | // Tags for WebSocketClient initial HTTP handshake: 45 | TAG_HTTP_REQUEST_HEADERS = 500, 46 | TAG_HTTP_RESPONSE_HEADERS, 47 | }; 48 | 49 | 50 | @interface WebSocket () 51 | { 52 | @protected 53 | __weak id _delegate; 54 | dispatch_queue_t _websocketQueue; 55 | GCDAsyncSocket *_asyncSocket; 56 | 57 | NSDictionary* _tlsSettings; 58 | WebSocketState _state; 59 | // BOOL _isStarted; 60 | // BOOL _isOpen; 61 | BOOL _isClient; 62 | } 63 | 64 | @property GCDAsyncSocket* asyncSocket; 65 | 66 | - (void) start; 67 | 68 | - (void) sendFrame: (NSData*)msgData type: (unsigned)type tag: (long)tag; 69 | 70 | @end 71 | --------------------------------------------------------------------------------