This tutorial uses a simple inventory tracker app to demonstrate the peer-to-peer database sync functionality introduced in Couchbase Lite 2.8.
Couchbase Lite 2.8 release supports out-of-the-box support for secure Peer-to-Peer Sync, over websockets, between Couchbase Lite enabled clients in IP-based networks without the need for a centralized control point (i.e. you do not need a Sync Gateway or Couchbase Server to get peer-to-peer database sync going)
This tutorial will demonstrate how to:
Throughout this tutorial, the terms "passive peer" and "server" and "listener" will be used interchangeably to refer to the peer on which the websockets listener is started. The "active peer" and "client" will be used interchangeably to refer to the peer on which the replicator is initialized.
We will be using a simple inventory app in swift as an example to demonstrate the peer-to-peer functionality.
You can learn more about Couchbase Lite here
This tutorial assumes familiarity with building swift apps with Xcode and with Couchbase Lite.
If you are unfamiliar with the basics of Couchbase Lite, it is recommended that you follow the Getting Started guides
iOS (Xcode 11.4+)
Wi-Fi network that the peers can communicate over
This is a simple inventory app that can be used as a websocket using passive or websocket using active.
The app uses a local database that is pre-populated with data. There is no Sync Gateway or Couchbase Server installed.
When used as a passive peer:
When used as a active peer:
git clone https://github.com/couchbaselabs/couchbase-lite-peer-to-peer-sync-examples
cd /path/to/cloned/repo/couchbase-lite-peer-to-peer-sync-examples/ios/list-sync
sh install_11.sh
open list-sync.xcodeproj
samplelist.json
: JSON data that is loaded into the local Couchbase Lite database. It includes the data for a single document. See Data Modeluserallowlist.json
: List of valid client users (and passwords) in the system. This list is looked up when the server tries to authenticate credentials associated with incoming connection request.listener-cert-pkey.p12
: This is PKCS12 file archive that includes a public key cert corresponding to the listener and associated private key. The cert is a sample cert that was generated using OpenSSL tool.listener-pinned-cert.cer
: This is the public key listener cert (the same cert that is embedded in the listener-cert-pkey.p12
file) in DER encoded format. This cert is pinned on the client replicator and is used for validating server cert during connection setupCouchbase Lite is a JSON Document Store. A Document is a logical collection of named fields and values.The values are any valid JSON types. In addition to the standard JSON types, Couchbase Lite supports some special types like Date
and Blob
.
While it is not required or enforced, it is a recommended practice to include a "type" property that can serve as a namespace for related.
The app deals with a single Document with a "type" property of "list". This document is loaded from the samplelist.json
file bundled with the project
An example of a document would be
{
"type":"list",
"list":[
{
"image":{"length":16608,"digest":"sha1-LEFKeUfywGIjASSBa0l/cg5rlm8=","content_type":"image/jpeg","@type":"blob"},
"value":10,
"key":"Apples"
},
{
"image":{"length":16608,"digest":"sha1-LEFKeUsswGIjASssSBa0l/cg5rlm8=","content_type":"image/jpeg","@type":"blob"},
"value":110,
"key":"oranges"
}
]
}
The document is encoded as a ListRecord
struct defined in the ListRecord.swift
file
The app loads the data from the JSON document named samplelist.json
the first time the database is created. This is done regardless of whether the app is launched in passive or active mode.
openOrCreateDatabaseForUser()
method. This method creates an instance of Couchbase Lite database for the user if one does not exist and loads the empty database with data ready from bundled sample JSON filevar exists = false
if Database.exists(withName: kUserDBName, inDirectory: userFolderPath) == true {
_userDb = try? Database(name: kUserDBName, config: options)
exists = true
}
else {
_userDb = try? Database(name: kUserDBName, config: options)
}
Open the SampleFileLoaderUtils.swift file and locate the loadSampleJSONDataForUserFromFile()
method. This function parses the document in JSON and updates it to embed the "image" property into every object in the "list" array. The "image" property holds a blob entry to an image asset. The image for the blob is available in the "Assets.xcassets" folder
Open the DatabaseManager.swift file and locate the createUserDocumentWithData()
method. This is where the document is saved into the database. Again, this is only done if there is no preexisting database for the user
for (key,value) in data {
let docId = "\(kDocPrefix)\(key)"
print("DocId is \(docId)")
let doc = MutableDocument(id:docId, data:value as! Dictionary<String, Any>)
try db.saveDocument(doc)
}
First, we will walk through the steps of using the app in passive peer mode.
initWebsocketsListenerForUserDb()
function. This is where the websockets listener for peer-to-peer sync is initialized// Include websockets listener initializer code
let listenerConfig = URLEndpointListenerConfiguration(database: db) // <1>
// Configure the appropriate auth test mode
switch listenerTLSSupportMode { //<2>
case .TLSDisabled:
listenerConfig.disableTLS = true
listenerConfig.tlsIdentity = nil
case .TLSWithAnonymousAuth:
listenerConfig.disableTLS = false // Use with anonymous self signed cert
listenerConfig.tlsIdentity = nil
case .TLSWithBundledCert:
if let tlsIdentity = self.importTLSIdentityFromPKCS12DataWithCertLabel(kListenerCertLabel) {
listenerConfig.disableTLS = false
listenerConfig.tlsIdentity = tlsIdentity
}
else {
print("Could not create identity from provided cert")
throw ListDocError.WebsocketsListenerNotInitialized
}
case .TLSWithGeneratedSelfSignedCert:
if let tlsIdentity = self.createIdentityWithCertLabel(kListenerCertLabel) {
listenerConfig.disableTLS = false
listenerConfig.tlsIdentity = tlsIdentity
}
else {
print("Could not create identity from generated self signed cert")
throw ListDocError.WebsocketsListenerNotInitialized
}
}
listenerConfig.enableDeltaSync = true // <3>
listenerConfig.authenticator = ListenerPasswordAuthenticator.init { // <4>
(username, password) -> Bool in
if (self._allowlistedUsers.contains(["password" : password, "name":username])) {
return true
}
return false
}
_websocketListener = URLEndpointListener(config: listenerConfig
URLEndpointListenerConfiguration
for the specified database. There is a listener for a given database. You can specify a port to be associated with the listener. In our app, we let Couchbase Lite choose the portlistenerTLSSupportMode
that allows the app to switch between the various modes. You can change the mode by changing the value of the variable. See Testing Different TLS Modesuserallowlist.json
file bundled with the appThe app can be configured to test different TLS modes as follows by setting the listenerTLSSupportMode
property in the DatabaseManager.swift
file
fileprivate let listenerCertValidationMode:ListenerCertValidationTestMode = .TLSEnableValidationWithCertPinning
listenerTLSSupportMode Value | Behavior |
---|---|
TLSDisabled | There is no TLS. All communication is plaintext (insecure mode and not recommended in production) |
TLSWithAnonymousAuth | The app uses self-signed cert that is auto-generated by Couchbase Lite as TLSIdentity of the server. While server authentication is skipped, all communication is still |
encrypted. This is the default mode of Couchbase Lite. | |
TLSWithBundledCert | The app generates TLSIdentity of the server from public key cert and private key bundled in the listener-cert-pkey.p12 archive. Communication is encrypted |
TLSWithGeneratedSelfSignedCert | The app uses Couchbase Lite CreateIdentity convenience API to generate the TLSIdentity of the server. Communication is encrypted |
startWebsocketsListenerForUserDb()
method.DispatchQueue.global().sync {
do {
try websocketListener.start()
handler(websocketListener.urls,nil)
}
catch {
handler(nil,error)
}
}
In the app, we use NetService advertise the websockets listener service listening at the specified listener port. This aspect of the app has nothing to do with Couchbase Lite. In your production app, you can use any suitable mechanism including using a well known URL to advertise your service that active clients can be pre-configured to connect to.
ServiceAdvertiser
class. Here, we advertise a Bonjour service with service type of _cblistservicesync._tcp
/// The Bonjour service name. Setting it to an empty String will be
/// mapped to the device's name.
public var serviceName: String = ""
/// The Bonjour service type.
public var serviceType = "_cblistservicesync._tcp"
/// The Bonjour domain type.
public var serviceDomain = ""
doStart()
method.private func doStart(database:String, _ port:UInt16) {
let service = NetService(domain: serviceDomain, type: serviceType,
name: serviceName, port:Int32(port))
service.delegate = self
service.includesPeerToPeer = true
service.publish()
services[database] = service
}
Explore the content in the ServiceAdvertiser.swift
. It includes implementation of the NetServiceDelegate
delegate callback methods to accept incoming connections.
stopWebsocketsListenerForUserDb()
method. You can stop the listener at any point. If there are connected clients, it will warn you that there are active connections. If you choose to stop listener, all connected clients will be disconnectedfunc stopWebsocketsListenerForUserDb() throws{
print(#function)
guard let websocketListener = _websocketListener else {
throw ListDocError.WebsocketsListenerNotInitialized
}
websocketListener.stop()
_websocketListener = nil
}
userallowlist.json
file such as "bob" and "password"We will walk through the steps of using the app in active peer mode.
In the app, we use NetService to browse for devices that are advertising services with name _cblistservicesync._tcp
. This aspect of the app has nothing to do with Couchbase Lite. In your production app, you could launch your listener at well known URL well and pre-configure your active peer to connect to the URL.
ServiceBrowser
class. Here, we browse for a service with service type of _cblistservicesync._tcp
using Bonjourpublic func startSearch(withDelegate delegate:ServiceBrowserDelegate? ){
peerBrowserDelegate = delegate
self.browser = NetServiceBrowser()
self.browser?.delegate = self
self.browser?.searchForServices(ofType: serviceType, inDomain: domain)
}
Explore the content in the ServiceBrowser.swift
. It includes implementation of the NetServiceDelegate
delegate callback methods to resolve the service to its IP Address and port that will be used by the client to connect to the listener.
Initialilzing a replicator for peer-to-peer sync is fundamentally the same as the case if the Couchbase Lite client were to sync with a remote Sync Gateway.
startP2PReplicationWithUserDatabaseToRemotePeer()
method. If you have been using Couchbase Lite to sync data with Sync Gateway, this code should seem very familiar. In this function, we initialize a bi-directional replication to the listener peer in continuous mode. We also register a Replication Listener to be notified of status to the replication status.if replicatorForUserDb == nil {
// Start replicator to connect to the URLListenerEndpoint
guard let targetUrl = URL(string: "wss://\(peer)/\(kUserDBName)") else {
throw ListDocError.URLInvalid
}
let config = ReplicatorConfiguration.init(database: userDb, target: URLEndpoint.init(url:targetUrl)) //<1>
config.replicatorType = .pushAndPull
config.continuous = true
// Explicitly allows self signed certificates. By default, only
// CA signed cert is allowed
switch listenerCertValidationMode { //<2>
case .TLSSkipValidation :
// Use acceptOnlySelfSignedServerCertificate set to true to only accept self signed certs.
// There is no cert validation
config.acceptOnlySelfSignedServerCertificate = true
case .TLSEnableValidationWithCertPinning:
// Use acceptOnlySelfSignedServerCertificate set to false to only accept CA signed certs
// Self signed certs will fail validation
config.acceptOnlySelfSignedServerCertificate = false
// Enable cert pinning to only allow certs that match pinned cert
if let pinnedCert = self.loadSelfSignedCertForListenerFromBundle() {
config.pinnedServerCertificate = pinnedCert
}
else {
print("Failed to load server cert to pin. Will proceed without pinning")
}
case .TLSEnableValidation:
// Use acceptOnlySelfSignedServerCertificate set to false to only accept CA signed certs
// Self signed certs will fail validation. There is no cert pinning
config.acceptOnlySelfSignedServerCertificate = false
}
let authenticator = BasicAuthenticator(username: user, password: password)//<3>
config.authenticator = authenticator
replicatorForUserDb = Replicator.init(config: config) //<4>
_replicatorsToPeers[peer] = replicatorForUserDb
}
if let pushPullReplListenerForUserDb = registerForEventsForReplicator(replicatorForUserDb,handler:handler) {
_replicatorListenersToPeers[peer] = pushPullReplListenerForUserDb
}
replicatorForUserDb?.start() //<5>
handler(PeerConnectionStatus.Connecting)
listenerCertValidationMode
that allows you to try the various modes. You can change the mode by changing the value of the variable. See Testing Different Server Authentication ModesIn Initializing Websockets Listener section, we discussed the various ways the listener TLSIdentity can be configured. Here, we describe the corresponding changes on the replicator side to authenticate the server identity. The app can be configured to test different TLS modes as follows by setting the listenerCertValidationMode
property in the DatabaseManager.swift
file.
Naturally, if you had initialized the listener with TLSDisabled
mode, then skip this section as there is no TLS.
fileprivate let listenerCertValidationMode:ListenerCertValidationTestMode = .TLSEnableValidationWithCertPinning
listenerCertValidationMode Value | Behavior |
---|---|
TLSSkipValidation | There is no authentication of server cert. The server cert is a self-signed cert. This is typically in used in dev or test environments. Skipping server cert |
authentication is | |
TLSEnableValidation | If the listener cert is from well known CA then you will use this mode. Of course, in our sample app, the listener cert as specified in listener-cert-pkey is a self |
signed cert - so you probably will not use this mode to test. But if you have a CA signed cert, you can configure your listener with the CA signed cert and use this | |
mode to test. Communication is encrypted | |
TLSEnableValidationWithCertPinning | In this mode, the app uses the pinned cert,listener-pinned-cert.cer that is bundled in the app to validate the listener identity. Only the server cert that exactly |
matches the pinned cert will be authenticated. Communication is encrypted |
stopP2PReplicationWithUserDatabaseToRemotePeer()
method. If you have been using Couchbase Lite to sync data with Sync Gateway, this code should seem very familiar. In this function, we remove any listeners attached to the replicator and stop it. You can restart the replicator again in startP2PReplicationWithUserDatabaseToRemotePeer()
methodif let listener = _replicatorListenersToPeers[peer] {
replicator.removeChangeListener(withToken: listener)
_replicatorListenersToPeers.removeValue(forKey: peer)
}
replicator.stop()
userallowlist.json
file such as "bob" and "password". An an exercise, try with an invalid user and ensure it failsOnce the connection is established between the peers, you can start syncing. Couchbase Lite takes care of it.
As an exercise, switch between the various TLS modes and server cert validation modes and see how the app behaves. You can also try with different topologies to connect the peers.
Congratulations on completing this tutorial!
This tutorial walked you through an example of how to directly synchronize data between Couchbase Lite enabled clients. While the tutorial is for iOS, the concepts apply equally to other Couchbase Lite platforms.
Complete documentation is available here