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..f9524826d 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..4ea4480fc 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; + int code = CBLStatusToHTTPStatus(status, &message); + [request respondWithErrorCode: code 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 {