Skip to content

Commit

Permalink
Merge pull request #867 from couchbase/feature/issue_838_synclistener…
Browse files Browse the repository at this point in the history
…_readonly

Fix #838 Implement readOnly to CBLSyncListener
  • Loading branch information
snej committed Aug 15, 2015
2 parents 9f85960 + 6fd7e8a commit d486504
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 2 deletions.
4 changes: 4 additions & 0 deletions Source/CBLSyncConnection+Checkpoints.m
Expand Up @@ -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:^{
Expand Down Expand Up @@ -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;
Expand Down
10 changes: 9 additions & 1 deletion Source/CBLSyncConnection+Pull.m
Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions Source/CBLSyncConnection+Push.m
Expand Up @@ -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]);
Expand Down Expand Up @@ -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: ^{
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions Source/CBLSyncConnection.h
Expand Up @@ -8,6 +8,7 @@

#import "BLIPConnection.h"
#import <CouchbaseLite/CBLDatabase.h>
#import <CouchbaseLite/CBLStatus.h>
@class CBLQueryEnumerator, CBLBlipReplicator;


Expand All @@ -18,6 +19,7 @@ typedef NS_ENUM(unsigned, SyncState) {
kSyncActive,
};

typedef CBLStatus (^OnSyncAccessCheckBlock)(BLIPRequest* req, NSString* docID);

@interface CBLSyncConnection : NSObject <BLIPConnectionDelegate>

Expand All @@ -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;
Expand Down
17 changes: 17 additions & 0 deletions Source/CBLSyncConnection.m
Expand Up @@ -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
Expand Down Expand Up @@ -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:


Expand Down
2 changes: 2 additions & 0 deletions Source/CBLSyncConnection_Internal.h
Expand Up @@ -111,6 +111,8 @@
- (void) removeAttachmentProgress: (NSProgress*)attProgress
pulling: (BOOL)pulling;

- (BOOL) accessCheckForRequest: (BLIPRequest*)request docID: (NSString*)docID;

@end


Expand Down
11 changes: 11 additions & 0 deletions Source/CBLSyncListener.m
Expand Up @@ -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"
Expand Down Expand Up @@ -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];
Expand Down
106 changes: 105 additions & 1 deletion Unit-Tests/Listener_Tests.m
Expand Up @@ -12,7 +12,7 @@
#import "CBLHTTPListener.h"
#import "CBLRemoteRequest.h"
#import "CBLClientCertAuthorizer.h"
#import "BLIPPocketSocketConnection.h"
#import "BLIP.h"
#import "MYAnonymousIdentity.h"

#import <arpa/inet.h>
Expand Down Expand Up @@ -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]
Expand All @@ -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


Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit d486504

Please sign in to comment.