Active Peer
Description — Couchbase Lite’s Peer-to-Peer Synchronization enables edge devices to synchronize securely without consuming centralized cloud-server resources
Abstract — How to set up a Replicator to connect with a Listener and replicate changes using peer-to-peer sync
Related Content — API Reference | Passive Peer | Active Peer
Enterprise Edition only
This an Enterprise Edition feature.
Purchase the Enterprise License, which includes official Couchbase Support, to use it in production (see the license and support https://www.couchbase.com/licensing-and-support-faq).
|
Code Snippets
The code examples are indicative only.
They demonstrate basic concepts and approaches to using a feature.
Use them as inspiration and adapt these examples to best practice when developing applications for your platform.
|
Introduction
This content provides sample code and configuration examples covering the implementation of Peer-to-Peer Sync over websockets. Specifically it covers the implementation of an Active Peer.
This active peer (also referred to as a client and-or a replicator) will initiate connection with a Passive Peer (also referred to as a server and-or listener) and participate in the replication of database changes to bring both databases into sync.
Subsequent sections provide additional details and examples for the main configuration options.
Secure Storage
The use of TLS, its associated keys and certificates requires using secure storage to minimize the chances of a security breach.
The implementation of this storage differs from platform to platform — see Using secure storage.
|
Configuration Summary
You should configure and initialize a replicator for each Couchbase Lite database instance you want to sync. Example 1 shows the initialization and configuration process.
// Set listener DB endpoint
NSURL *url = [NSURL URLWithString:@"ws://listener.com:55990/otherDB"];
CBLURLEndpoint *thisListener = [[CBLURLEndpoint alloc] initWithURL:url];
CBLReplicatorConfiguration *thisConfig
= [[CBLReplicatorConfiguration alloc]
initWithDatabase:thisDB target:thisListener]; (1)
thisConfig.replicatorType = kCBLReplicatorTypePush;
thisConfig.continuous = YES;
// Configure Server Authentication
// Here - expect and accept self-signed certs
thisConfig.acceptOnlySelfSignedServerCertificate = YES; (2)
// Configure Client Authentication
// Here set client to use basic authentication
// Providing username and password credentials
// If prompted for them by server
thisConfig.authenticator = [[CBLBasicAuthenticator alloc] initWithUsername:@"Our Username" password:@"Our Password"]; (3)
/* Optionally set custom conflict resolver call back */
thisConfig.conflictResolver = [[LocalWinConflictResolver alloc] (4)
// Apply configuration settings to the replicator
_thisReplicator = [[CBLReplicator alloc] initWithConfig:thisConfig]; (5)
// Optionally add a change listener (6)
// Retain token for use in deletion
id<CBLListenerToken> thisListenerToken
= [thisReplicator addChangeListener:^(CBLReplicatorChange *thisChange) {
if (thisChange.status.activity == kCBLReplicatorStopped) {
NSLog(@"Replication stopped");
} else {
NSLog(@"Status: %d", thisChange.status.activity);
};
}];
// Run the replicator using the config settings
[thisReplicator start]; (7)
Notes on Example
1 | Use the ReplicatorConfiguration class’s constructor — -initWithDatabase:target: — to initialize the replicator configuration with the local database — see also: Configure Target |
2 | Configure how the client will authenticate the server. Here we say connect only to servers presenting a self-signed certificate. By default, clients accept only servers presenting certificates that can be verified using the OS bundled Root CA Certificates — see: Authenticating the Listener. |
3 | Configure the credentials the client will present to the server. Here we say to provide Basic Authentication credentials. Other options are available — see: Client Authentication. |
4 | Configure how the replication should perform Conflict Resolution. |
5 | Initialize the replicator using your configuration object. |
6 | Register an observer, which will notify you of changes to the replication status. |
7 | Start the replicator. |
API References
You can find Objective-C API References here.
Device Discovery
This phase is optional: If the listener is initialized on a well known URL endpoint (for example, a static IP Address or well known DNS address) then you can configure active peers to connect to those.
Prior to connecting with a listener you may execute a peer discovery phase to dynamically discover peers.
For the active peer this involves browsing-for and selecting the appropriate service using a zero-config protocol such as Bonjour-- see: https://developer.apple.com/bonjour/.
Configure Replicator
In this section: Configure Target | Sync Mode | Authenticating the Listener | Client Authentication
Configure Target
Use the ReplicatorConfiguration class and -initWithDatabase:target: constructor to initialize the replication configuration with local and remote database locations.
The constructor provides
-
the name of the local database to be sync’d
-
the listener’s URL (including its port and the name of the remote database to sync with) — whether pre-configured or gathered during a discovery phase.
It is expected that the app will identify the IP address and URL and append the remote database name to the URL endpoint (this is similar to the way you would handle a Sync Gateway endpoint.
The URL scheme for web socket URLs uses
ws:
(non-TLS) orwss:
(SSL/TLS) prefixes.
// Set listener DB endpoint
NSURL *url = [NSURL URLWithString:@"ws://listener.com:55990/otherDB"];
CBLURLEndpoint *thisListener = [[CBLURLEndpoint alloc] initWithURL:url];
CBLReplicatorConfiguration *thisConfig
= [[CBLReplicatorConfiguration alloc]
initWithDatabase:thisDB target:thisListener]; (1)
Notes on Example
1 | Note use of the wss:// prefix to ensure TLS encryption (strongly recommended in production). |
Sync Mode
Here we define the direction and type of replication we want to initiate.
We use ReplicatorConfiguration
class’s replicatorType and
continuous
parameters, to tell the replicator:
-
The direction of the replication:
pushAndPull
;pull
;push
-
The type of replication, that is:
-
Continuous — remaining active indefinitely to replicate changed documents (
continuous=true
). -
Ad-hoc — a one-shot replication of changed documents (
continuous=false
).
-
thisConfig.replicatorType = kCBLReplicatorTypePush;
thisConfig.continuous = YES;
Authenticating the Listener
Define the credentials the client is expecting fom the server in order to ensure that the server (listener) is one it is prepared to interact with.
Note that the client cannot authenticate the server if TLS is turned off. When TLS is enabled (default) the client must authenticate the server. If the server cannot provide acceptable credentials then the connection will fail.
Use ReplicatorConfiguration
methods acceptOnlySelfSignedServerCertificate and {url-api-prop-replicator-config-setPinnedServerCertificate}, to tell the replicator how to verify server-supplied TLS server certificates.
-
If there is a pinned certificate, nothing else matters, the server cert must exactly match the pinned certificate.
-
If there are no pinned certs and acceptOnlySelfSignedServerCertificate is
true
then any self-signed certificate is accepted. Certificates that are not self signed are rejected, no matter who signed them. -
If there are no pinned certificates and acceptOnlySelfSignedServerCertificate is
false
(default), the client validates the server’s certificates against the system CA certificates. The server must supply a chain of certificates whose root is signed by one of the certificates in the system CA bundle.
Set the client to expect and accept only CA attested certificates.
// Configure Server Security -- only accept CA Certs
thisConfig.acceptOnlySelfSignedServerCertificate = NO; (1)
Notes on Example
1 | This is the default. Only certificate chains with roots signed by a trusted CA are allowed. Self signed certificates are not allowed. |
Set the client to expect and accept only self-signed certificates
// Configure Server Authentication
// Here - expect and accept self-signed certs
thisConfig.acceptOnlySelfSignedServerCertificate = YES; (1)
Notes on Example
1 | Set this to true to accept any self signed cert.
Any certificates that are not self-signed are rejected. |
Set the client to expect and accept only a pinned certificate.
NSURL *certURL =
[[NSBundle mainBundle] URLForResource: @"cert" withExtension: @"cer"];
NSData *data =
[[NSData alloc] initWithContentsOfURL: certURL];
SecCertificateRef certificate =
SecCertificateCreateWithData(NULL, (__bridge CFDataRef)data);
NSURL *url =
[NSURL URLWithString:@"ws://localhost:4984/db"];
CBLURLEndpoint *target = [[CBLURLEndpoint alloc] initWithURL: url];
CBLReplicatorConfiguration *thisConfig =
[[CBLReplicatorConfiguration alloc] initWithDatabase:database
target:target];
thisConfig.pinnedServerCertificate =
(SecCertificateRef)CFAutorelease(certificate);
thisConfig.acceptOnlySelfSignedServerCertificate=false;
Client Authentication
Here we define the credentials that the client can present to the server if prompted to do so in order that the server can authenticate it.
We use ReplicatorConfiguration's authenticator method to define the authentication method to the replicator - see Example 6.
Basic Authentication
Use the BasicAuthenticator
to supply basic authentication credentials (username and password).
This example shows basic authentication using user name and password:
// Here set client to use basic authentication
// Providing username and password credentials
// If prompted for them by server
thisConfig.authenticator = [[CBLBasicAuthenticator alloc] initWithUsername:@"Our Username" password:@"Our Password"]; (1)
Certificate Authentication
Use the ClientCertificateAuthenticator
to configure the client TLS certificates to be presented to the server, on connection.
This applies only to the URLEndpointListener.
The server (listener) must have disableTLS set false and have a ClientCertificateAuthenticator configured, or it will never ask for this client’s certificate.
|
The certificate to be presented to the server will need to be signed by the root certificates or be valid based on the authentication callback set to the listener via ListenerCertificateAuthenticator.
This example shows client certificate authentication using an identity from secure storage.
// Check if Id exists in keychain and if so, use it
CBLTLSIdentity* identity =
[CBLTLSIdentity identityWithLabel: @"doco-sync-server" error: &error]; (1)
thisConfig.authenticator =
[[CBLClientCertificateAuthenticator alloc] initWithIdentity: identity]; (2)
Notes on Example <.> Get an identity from secure storage and create a TLS Identity object <.> Set the authenticator to ClientCertificateAuthenticator and configure it to use the retrieved identity
Initialize Replicator
Use the Replicator
class’s initWith(config:) constructor, to initialize the replicator with the configuration you have defined.
You can, optionally, add a change listener (see Monitor Sync) before starting the replicator running using start().
// Apply configuration settings to the replicator
_thisReplicator = [[CBLReplicator alloc] initWithConfig:thisConfig]; (1)
// Run the replicator using the config settings
[thisReplicator start]; (2)
Notes on Example
1 | Initialize the replicator with the configuration |
2 | Start the replicator |
Monitor Sync
Change Listeners
Use the Replicator class to add a change listener as a callback to the Replicator (addChangeListener(_:)) — see Example 8. You will then be asynchronously notified of state changes. Use this to monitor changes and to inform on sync progress, as shown below; this is an optional step.
Replicator Status
You can use the Replicator class’s status property to check the replicator status. That is, whether it is actively transferring data or if it has stopped.
The returned ReplicationStatus structure comprises:
-
activity enum — stopped, offline, connecting, idle or busy
-
-
completed — the total number of changes completed
-
total — the total number of changes to be processed
-
-
error enum — the current error, if any
For more on replication status, see: Replication Status
// Retain token for use in deletion
id<CBLListenerToken> thisListenerToken
= [thisReplicator addChangeListener:^(CBLReplicatorChange *thisChange) {
if (thisChange.status.activity == kCBLReplicatorStopped) {
NSLog(@"Replication stopped");
} else {
NSLog(@"Status: %d", thisChange.status.activity);
};
}];
if (thisChange.status.activity == kCBLReplicatorStopped) {
NSLog(@"Replication stopped");
} else {
NSLog(@"Status: %d", thisChange.status.activity);
};
Stop Sync
If you added an optional change listener (see Monitor Sync for how) you should also remove it using the removeChangeListenerWithToken(CBLListenerToken:) method. |
// Remove the change listener
[thisReplicator removeChangeListenerWithToken: thisListenerToken];
// Stop the replicator
[thisReplicator stop];
Notes on Example
1 | Stopping the replication is straightforward using the stop() method. |
Conflict Resolution
Unless you specify otherwise, Couchbase Lite’s default conflict resolution policy is applied — see Automatic Conflict Resolution.
To use a different policy, specify a conflict resolver using conflictResolver as shown in Example 10.
For more complex solutions you can provide a custom conflict resolver - see: Custom Conflict Resolution.
@interface LocalWinConflictResolver: NSObject<CBLConflictResolver>
@end
@implementation LocalWinConflictResolver
- (CBLDocument*) resolve: (CBLConflict*)conflict {
return conflict.localDocument;
}
@end
@interface RemoteWinConflictResolver: NSObject<CBLConflictResolver>
@end
@implementation RemoteWinConflictResolver
- (CBLDocument*) resolve: (CBLConflict*)conflict {
return conflict.remoteDocument;
}
@end
@interface MergeConflictResolver: NSObject<CBLConflictResolver>
@end
@implementation MergeConflictResolver
- (CBLDocument*) resolve: (CBLConflict*)conflict {
NSDictionary *localDict = conflict.localDocument.toDictionary;
NSDictionary *remoteDict = conflict.remoteDocument.toDictionary;
NSMutableDictionary *result = [NSMutableDictionary dictionaryWithDictionary:localDict];
[result addEntriesFromDictionary:remoteDict];
return [[CBLMutableDocument alloc] initWithID:conflict.documentID
data:result];
}
@end
Just as a replicator may observe a conflict — when updating a document that has changed both in the local database and in a remote database — any attempt to save a document may also observe a conflict, if a replication has taken place since the local app retrieved the document from the database. To address that possibility, a version of the Database.save()
method also takes a conflict resolver as shown in [merging-document-properties].
The following code snippet shows an example of merging properties from the existing document (current
) into the one being saved (new
).
In the event of conflicting keys, it will pick the key value from new
.
CBLDocument *document = [database documentWithID:@"xyz"];
CBLMutableDocument *mutableDocument = [document toMutable];
[mutableDocument setString:@"apples" forKey:@"name"];
[database saveDocument:mutableDocument
conflictHandler:^BOOL(CBLMutableDocument *new, CBLDocument *current) {
NSDictionary *currentDict = current.toDictionary;
NSDictionary *newDict = new.toDictionary;
NSMutableDictionary *result = [NSMutableDictionary dictionaryWithDictionary:currentDict];
[result addEntriesFromDictionary:newDict];
[new setData: result];
return YES;
}
error: &error];