From 29bcbccca1f65ea96e219e029846ee1c6c76362b Mon Sep 17 00:00:00 2001 From: Pasin Suriyentrakorn Date: Wed, 12 Aug 2015 15:17:51 -0700 Subject: [PATCH 1/2] Implement readOnly to CBLSyncListener - Added OnSyncAccessCheckBlock property to CBLSyncConnection similarly to CBL_Router.OnAccessCheckBlock property. OnSyncAccessCheckBlock (if specified) will be called to check the access for each sync request coming to the CBLSyncConnection's handlers. The handlers will then return the corresponding error responses depending on the result of the OnSyncAccessCheckBlock call. - In read only mode, CBLSyncListener will block access to setCheckpoint, changes, and rev requests. #838 --- Source/CBLSyncConnection+Checkpoints.m | 4 + Source/CBLSyncConnection+Pull.m | 10 ++- Source/CBLSyncConnection+Push.m | 9 +++ Source/CBLSyncConnection.h | 4 + Source/CBLSyncConnection.m | 17 ++++ Source/CBLSyncConnection_Internal.h | 2 + Source/CBLSyncListener.m | 11 +++ Unit-Tests/Listener_Tests.m | 106 ++++++++++++++++++++++++- 8 files changed, 161 insertions(+), 2 deletions(-) diff --git a/Source/CBLSyncConnection+Checkpoints.m b/Source/CBLSyncConnection+Checkpoints.m index 3ff8cb040..2d009bb67 100644 --- a/Source/CBLSyncConnection+Checkpoints.m +++ b/Source/CBLSyncConnection+Checkpoints.m @@ -168,6 +168,8 @@ - (void) handleGetCheckpoint: (BLIPRequest*)request { [request respondWithErrorCode: 400 message: @"Bad Request"]; return; } + if (![self accessCheckForRequest: request docID: [@"_local/" stringByAppendingString: docID]]) + return; [request deferResponse]; [self onDatabaseQueue:^{ @@ -201,6 +203,8 @@ - (void) handleSetCheckpoint: (BLIPRequest*)request { [request respondWithErrorCode: 400 message: @"Bad Request"]; return; } + if (![self accessCheckForRequest: request docID: [@"_local/" stringByAppendingString: docID]]) + return; NSString* revID = request[@"rev"]; if (revID) checkpoint[@"_rev"] = revID; diff --git a/Source/CBLSyncConnection+Pull.m b/Source/CBLSyncConnection+Pull.m index 60669c7fe..d19133aac 100644 --- a/Source/CBLSyncConnection+Pull.m +++ b/Source/CBLSyncConnection+Pull.m @@ -72,6 +72,8 @@ - (void) handleIncomingChanges: (BLIPRequest*)request { // (Note: even if we got 0 changes (i.e. caught up) we still need to go through the db queue // before announcing it, so previously queued change processing blocks get to run first.) LogTo(Sync, @"Received %u changes", (unsigned)changes.count); + if (![self accessCheckForRequest: request docID: nil]) + return; [request deferResponse]; [self onDatabaseQueue: ^{ CFAbsoluteTime time = CFAbsoluteTimeGetCurrent(); @@ -162,11 +164,17 @@ - (void) handleIncomingRevision: (BLIPRequest*)request { NSDictionary* attachments = nil; NSString* docID; NSData* json = request.body; - if (memmem(json.bytes, json.length, "\"_attachments\":", 15) != NULL) { + if (self.onSyncAccessCheck && memmem(json.bytes, json.length, "\"_attachments\":", 15) != NULL) { NSDictionary* props = [NSJSONSerialization JSONObjectWithData: json options: 0 error: NULL]; attachments = $castIf(NSDictionary, props[@"_attachments"]); docID = props[@"_id"]; } + + if (![self accessCheckForRequest: request docID: docID]) { + --_insertingRevs; + return; + } + if (attachments.count == 0) { [self queueRevisionToInsert: request withAttachments: nil]; return; diff --git a/Source/CBLSyncConnection+Push.m b/Source/CBLSyncConnection+Push.m index 3775bed9d..59de2bae4 100644 --- a/Source/CBLSyncConnection+Push.m +++ b/Source/CBLSyncConnection+Push.m @@ -21,6 +21,9 @@ @implementation CBLSyncConnection (Push) // Starting point of a passive push (called by peer when it starts pulling.) - (void) handleSubscribeToChanges: (BLIPRequest*)request { + if (![self accessCheckForRequest: request docID: nil]) + return; + uint64_t since = MAX(0, [request[@"since"] longLongValue]); if (request[@"batch"]) _changesBatchSize = MAX(0, [request[@"batch"] integerValue]); @@ -292,6 +295,9 @@ - (void) sendDoc: (NSString*)docID - (void) handleGetAttachment: (BLIPRequest*)request { + if (![self accessCheckForRequest: request docID: nil]) + return; + NSString* digest = request[@"digest"]; [request deferResponse]; [self onDatabaseQueue: ^{ @@ -332,6 +338,9 @@ - (void) handleGetAttachment: (BLIPRequest*)request { - (void) handleProveAttachment: (BLIPRequest*)request { + if (![self accessCheckForRequest: request docID: nil]) + return; + NSString* digest = request[@"digest"]; NSData* nonce = request.body; if (!digest || nonce.length == 0 || nonce.length > 255) diff --git a/Source/CBLSyncConnection.h b/Source/CBLSyncConnection.h index 2a6496ddb..c42fdb2ab 100644 --- a/Source/CBLSyncConnection.h +++ b/Source/CBLSyncConnection.h @@ -8,6 +8,7 @@ #import "BLIPConnection.h" #import +#import @class CBLQueryEnumerator, CBLBlipReplicator; @@ -18,6 +19,7 @@ typedef NS_ENUM(unsigned, SyncState) { kSyncActive, }; +typedef CBLStatus (^OnSyncAccessCheckBlock)(BLIPRequest* req, NSString* docID); @interface CBLSyncConnection : NSObject @@ -39,6 +41,8 @@ typedef NS_ENUM(unsigned, SyncState) { @property (readonly) dispatch_queue_t syncQueue; @property (readonly) NSURL* peerURL; +@property (copy) OnSyncAccessCheckBlock onSyncAccessCheck; + // The below properties are observable, but the changes happen on the syncQueue @property (readonly) SyncState state; diff --git a/Source/CBLSyncConnection.m b/Source/CBLSyncConnection.m index 536fc204c..bea058e7e 100644 --- a/Source/CBLSyncConnection.m +++ b/Source/CBLSyncConnection.m @@ -21,6 +21,7 @@ @implementation CBLSyncConnection @synthesize pullProgress=_pullProgress, nestedPullProgress=_nestedPullProgress; @synthesize pushProgress=_pushProgress, nestedPushProgress=_nestedPushProgress; @synthesize remoteCheckpointDocID=_remoteCheckpointDocID, replicator=_replicator; +@synthesize onSyncAccessCheck=_onSyncAccessCheck; #if DEBUG @synthesize savingCheckpoint=_savingCheckpoint; // for unit tests #endif @@ -265,6 +266,22 @@ - (void) removeAttachmentProgress: (NSProgress*)attProgress } +- (BOOL) accessCheckForRequest: (BLIPRequest*)request + docID: (NSString*)docID +{ + if (self.onSyncAccessCheck) { + CBLStatus status = self.onSyncAccessCheck(request, docID); + if (CBLStatusIsError(status)) { + NSString* message; + [request respondWithErrorCode: CBLStatusToHTTPStatus(status, &message) + message: message]; + return NO; + } + } + return YES; +} + + #pragma mark - BLIP WEB SOCKET DELEGATE: diff --git a/Source/CBLSyncConnection_Internal.h b/Source/CBLSyncConnection_Internal.h index a76d2a098..f33ec2481 100644 --- a/Source/CBLSyncConnection_Internal.h +++ b/Source/CBLSyncConnection_Internal.h @@ -111,6 +111,8 @@ - (void) removeAttachmentProgress: (NSProgress*)attProgress pulling: (BOOL)pulling; +- (BOOL) accessCheckForRequest: (BLIPRequest*)request docID: (NSString*)docID; + @end diff --git a/Source/CBLSyncListener.m b/Source/CBLSyncListener.m index 9907a2649..09fbac05f 100644 --- a/Source/CBLSyncListener.m +++ b/Source/CBLSyncListener.m @@ -9,6 +9,7 @@ #import "CBLSyncListener.h" #import "CBLListener+Internal.h" #import "CBLSyncConnection.h" +#import "BLIPRequest.h" #import "CouchbaseLite.h" #import "CBLInternal.h" #import "BLIPPocketSocketListener.h" @@ -89,6 +90,16 @@ - (void) blipConnectionDidOpen:(BLIPConnection *)connection { CBLSyncConnection* handler = [[CBLSyncConnection alloc] initWithDatabase: db connection: connection queue: queue]; + if (_facade.readOnly) { + handler.onSyncAccessCheck = ^CBLStatus(BLIPRequest* request, NSString* docID) { + NSString* profile = request.profile; + if ([profile isEqualToString:@"setCheckpoint"] || + [profile isEqualToString:@"changes"] || + [profile isEqualToString:@"rev"]) + return kCBLStatusForbidden; + return kCBLStatusOK; + }; + } [handler addObserver: self forKeyPath: @"state" options: 0 context: (void*)1]; dispatch_sync(_queue, ^{ [_handlers addObject: handler]; diff --git a/Unit-Tests/Listener_Tests.m b/Unit-Tests/Listener_Tests.m index bf15bf455..dbf58329a 100644 --- a/Unit-Tests/Listener_Tests.m +++ b/Unit-Tests/Listener_Tests.m @@ -12,7 +12,7 @@ #import "CBLHTTPListener.h" #import "CBLRemoteRequest.h" #import "CBLClientCertAuthorizer.h" -#import "BLIPPocketSocketConnection.h" +#import "BLIP.h" #import "MYAnonymousIdentity.h" #import @@ -107,6 +107,92 @@ - (void)test02_SSL_ClientCert { [self connect]; } +- (void)test03_ReadOnly { + if (!self.isSQLiteDB || ![self isKindOfClass: [CBLSyncListener class]]) + return; + + // Wait for listener to start: + if (listener.port == 0) { + [self keyValueObservingExpectationForObject: listener keyPath: @"port" expectedValue: @(sPort)]; + [listener start: NULL]; + [self waitForExpectationsWithTimeout: kTimeout handler: nil]; + } + + // Enable readOnly mode: + listener.readOnly = YES; + + NSURL* url = [[listener.URL URLByAppendingPathComponent: db.name] + URLByAppendingPathComponent: @"_blipsync"]; + Log(@"Connecting to <%@>", url); + BLIPPocketSocketConnection* conn = [[BLIPPocketSocketConnection alloc] initWithURL: url]; + [conn setDelegate: self queue: dispatch_get_main_queue()]; + + NSError* error; + Assert([conn connect: &error], @"Can't connect: %@", error); + _expectDidOpen = [self expectationWithDescription: @"didOpen"]; + [self waitForExpectationsWithTimeout: kTimeout handler: nil]; + + // getCheckpoint: + BLIPRequest* request = [conn request]; + request.profile = @"getCheckpoint"; + request[@"client"] = @"TestReadOnly"; + [self sendRequest: request expectedErrorCode: 0 expectedResult: nil]; + + // setCheckpoint: + request = [conn request]; + request.profile = @"setCheckpoint"; + request[@"client"] = @"TestReadOnly"; + request[@"rev"] = @"1-000"; + request.bodyJSON = $dict({@"lastSequence", @(1)}); + [self sendRequest: request expectedErrorCode: 403 expectedResult: nil]; + + // subChanges: + request = [conn request]; + request.profile = @"subChanges"; + [self sendRequest: request expectedErrorCode: 0 expectedResult: nil]; + + // changes: + request = [conn request]; + request.profile = @"changes"; + request.bodyJSON = @[@[@(1), @"doc1", @"1-001"]]; + [self sendRequest: request expectedErrorCode: 403 expectedResult: nil]; + + // rev: + request = [conn request]; + request.profile = @"rev"; + request[@"history"] = @"1-001"; + request.bodyJSON = @{@"_id": @"doc1", @"_rev": @"1-001"}; + [self sendRequest: request expectedErrorCode: 403 expectedResult: nil]; + + // getAttachment: + CBLUnsavedRevision* newRev =[[db createDocument] newRevision]; + NSData* body = [@"This is an attachment." dataUsingEncoding: NSUTF8StringEncoding]; + [newRev setAttachmentNamed: @"attach" withContentType: @"text/plain" content: body]; + CBLAttachment* att = [[newRev save: nil] attachmentNamed: @"attach"]; + Assert(att); + + request = [conn request]; + request.profile = @"getAttachment"; + request[@"digest"] = att.metadata[@"digest"]; + [self sendRequest: request expectedErrorCode: 0 expectedResult: nil]; + + // proveAttachment: + request = [conn request]; + request.profile = @"proveAttachment"; + request[@"digest"] = att.metadata[@"digest"]; + uint8_t nonceBytes[16]; + SecRandomCopyBytes(kSecRandomDefault, sizeof(nonceBytes), nonceBytes); + NSData* nonceData = [NSData dataWithBytes: nonceBytes length: sizeof(nonceBytes)]; + request.body = nonceData; + [self sendRequest: request expectedErrorCode: 0 expectedResult: nil]; + + _expectDidClose = [self expectationWithDescription: @"didClose"]; + [conn close]; + [self waitForExpectationsWithTimeout: kTimeout handler: nil]; +} + + +#pragma mark - Test Utilities - (void) connect { NSURL* url = [[listener.URL URLByAppendingPathComponent: db.name] @@ -129,6 +215,22 @@ - (void) connect { } +- (void)sendRequest: (BLIPRequest*)request + expectedErrorCode: (NSInteger)expectedErrorCode + expectedResult: (id)expectedResult +{ + XCTestExpectation* expectDidComplete = [self expectationWithDescription: @"didComplete"]; + [request send].onComplete = ^(BLIPResponse* response) { + Assert(response); + AssertEq(response.error.code, expectedErrorCode); + if (expectedResult) + AssertEqual(response.bodyJSON, expectedResult); + [expectDidComplete fulfill]; + }; + [self waitForExpectationsWithTimeout: kTimeout handler: nil]; +} + + #pragma mark - CBLListenerDelegate @@ -215,9 +317,11 @@ - (Class) listenerClass { return [CBLHTTPListener class]; } +// Have to override these so Xcode will recognize that these tests exist in this class: // Have to override these so Xcode will recognize that these tests exist in this class: - (void)test01_SSL_NoClientCert {[super test01_SSL_NoClientCert];} - (void)test02_SSL_ClientCert {[super test02_SSL_ClientCert];} +- (void)test03_ReadOnly {[super test03_ReadOnly];} - (void) connect { From 6fd7e8a1d5a6bb3c71af6df21c05fba7f56420d9 Mon Sep 17 00:00:00 2001 From: Pasin Suriyentrakorn Date: Fri, 14 Aug 2015 17:07:35 -0700 Subject: [PATCH 2/2] Fixed CBLSyncConnection bug per code review #838 --- Source/CBLSyncConnection+Pull.m | 2 +- Source/CBLSyncConnection.m | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/CBLSyncConnection+Pull.m b/Source/CBLSyncConnection+Pull.m index d19133aac..f9524826d 100644 --- a/Source/CBLSyncConnection+Pull.m +++ b/Source/CBLSyncConnection+Pull.m @@ -164,7 +164,7 @@ - (void) handleIncomingRevision: (BLIPRequest*)request { NSDictionary* attachments = nil; NSString* docID; NSData* json = request.body; - if (self.onSyncAccessCheck && memmem(json.bytes, json.length, "\"_attachments\":", 15) != NULL) { + if (self.onSyncAccessCheck || memmem(json.bytes, json.length, "\"_attachments\":", 15) != NULL) { NSDictionary* props = [NSJSONSerialization JSONObjectWithData: json options: 0 error: NULL]; attachments = $castIf(NSDictionary, props[@"_attachments"]); docID = props[@"_id"]; diff --git a/Source/CBLSyncConnection.m b/Source/CBLSyncConnection.m index bea058e7e..4ea4480fc 100644 --- a/Source/CBLSyncConnection.m +++ b/Source/CBLSyncConnection.m @@ -273,8 +273,8 @@ - (BOOL) accessCheckForRequest: (BLIPRequest*)request CBLStatus status = self.onSyncAccessCheck(request, docID); if (CBLStatusIsError(status)) { NSString* message; - [request respondWithErrorCode: CBLStatusToHTTPStatus(status, &message) - message: message]; + int code = CBLStatusToHTTPStatus(status, &message); + [request respondWithErrorCode: code message: message]; return NO; } }