diff --git a/src/Couchbase.Lite.Tests.Shared/DocumentTest.cs b/src/Couchbase.Lite.Tests.Shared/DocumentTest.cs index cd624f40a..13f2a01d4 100644 --- a/src/Couchbase.Lite.Tests.Shared/DocumentTest.cs +++ b/src/Couchbase.Lite.Tests.Shared/DocumentTest.cs @@ -1155,7 +1155,7 @@ public void TestRemoveKeys() doc.Contains("name").Should().BeFalse("because that key was removed"); doc.Contains("weight").Should().BeFalse("because that key was removed"); doc.Contains("age").Should().BeFalse("because that key was removed"); - doc.Contains("address").Should().BeFalse("because that key was removed"); + doc.Contains("active").Should().BeFalse("because that key was removed"); doc.GetDictionary("address").Contains("city").Should().BeFalse("because that key was removed"); var address = doc.GetDictionary("address"); diff --git a/src/Couchbase.Lite.Tests.Shared/ReplicationTest.cs b/src/Couchbase.Lite.Tests.Shared/ReplicationTest.cs index 98ca583df..108160f10 100644 --- a/src/Couchbase.Lite.Tests.Shared/ReplicationTest.cs +++ b/src/Couchbase.Lite.Tests.Shared/ReplicationTest.cs @@ -24,6 +24,9 @@ using System.Text; using Couchbase.Lite; using Couchbase.Lite.Sync; +using FluentAssertions; +using LiteCore; +using LiteCore.Interop; #if !WINDOWS_UWP using Xunit; using Xunit.Abstractions; @@ -58,15 +61,41 @@ public void TestEmptyPush() PerformReplication(true, false); } - private void PerformReplication(bool push, bool pull) + // TODO: Figure out what to do about the current .NET limitation + // which doesn't give any information about why a web socket failed + // to connect (https://github.com/dotnet/corefx/issues/13773) + //[Fact] + public void TestAuthenticationFailure() + { + _repl = Db.CreateReplication(new Uri("ws://localhost:4984/seekrit")); + RunReplication(false, true); + _repl.LastError.Should().BeAssignableTo(); + _repl.LastError.As().Error.code.Should().Be(401); + _repl.LastError.As().Error.domain.Should().Be(C4ErrorDomain.WebSocketDomain); + } + + //[Fact] + public void TestAuthenticationPullHardcoded() + { + _repl = Db.CreateReplication(new Uri("ws://pupshaw:frank@localhost:4984/seekrit")); + RunReplication(false, true); + _repl.LastError.Should().BeNull("because otherwise the authentication failed"); + } + + private void RunReplication(bool push, bool pull) { - _repl = Db.CreateReplication(_otherDB); _repl.Push = push; _repl.Pull = pull; _repl.StatusChanged += ReplicationStatusChanged; _waitAssert = new WaitAssert(); _repl.Start(); - _waitAssert.WaitForResult(TimeSpan.FromSeconds(5)); + _waitAssert.WaitForResult(TimeSpan.FromSeconds(50)); + } + + private void PerformReplication(bool push, bool pull) + { + _repl = Db.CreateReplication(_otherDB); + RunReplication(push, pull); } private void ReplicationStatusChanged(object sender, ReplicationStatusChangedEventArgs e) diff --git a/src/Couchbase.Lite/API/Database/DocumentChangedEventArgs.cs b/src/Couchbase.Lite/API/Database/DocumentChangedEventArgs.cs index c1168a704..84da9ce8d 100644 --- a/src/Couchbase.Lite/API/Database/DocumentChangedEventArgs.cs +++ b/src/Couchbase.Lite/API/Database/DocumentChangedEventArgs.cs @@ -24,7 +24,8 @@ namespace Couchbase.Lite { /// - /// The arguments for the event + /// The arguments for the + /// event /// public sealed class DocumentChangedEventArgs : EventArgs { diff --git a/src/Couchbase.Lite/API/Document/DictionaryObject.cs b/src/Couchbase.Lite/API/Document/DictionaryObject.cs index c26c7312b..d6f2b1314 100644 --- a/src/Couchbase.Lite/API/Document/DictionaryObject.cs +++ b/src/Couchbase.Lite/API/Document/DictionaryObject.cs @@ -256,7 +256,11 @@ private void SetValue(string key, object value, bool isChange) /// public override bool Contains(string key) { - return (_dict.ContainsKey(key) && !ReferenceEquals(_dict[key], RemovedValue)) || base.Contains(key); + if (_dict.ContainsKey(key)) { + return !ReferenceEquals(_dict[key], RemovedValue); + } + + return base.Contains(key); } /// diff --git a/src/Couchbase.Lite/API/IQueryRow.cs b/src/Couchbase.Lite/API/IQueryRow.cs index ef0ded159..1e477c8cc 100644 --- a/src/Couchbase.Lite/API/IQueryRow.cs +++ b/src/Couchbase.Lite/API/IQueryRow.cs @@ -19,8 +19,6 @@ // limitations under the License. // using System; -using Couchbase.Lite.Internal.Serialization; -using LiteCore.Interop; namespace Couchbase.Lite { @@ -54,18 +52,74 @@ public interface IQueryRow #region Public Methods + /// + /// Gets the value of the nth selected + /// value of the query row (in order of what was specified in the + /// SELECT portion of the query) + /// + /// The index of the element to retrieve in terms + /// of the SELECT query + /// The value at the index bool GetBoolean(int index); + /// + /// Gets the value of the nth selected + /// value of the query row (in order of what was specified in the + /// SELECT portion of the query) + /// + /// The index of the element to retrieve in terms + /// of the SELECT query + /// The value at the index DateTimeOffset GetDate(int index); + /// + /// Gets the value of the nth selected + /// value of the query row (in order of what was specified in the + /// SELECT portion of the query) + /// + /// The index of the element to retrieve in terms + /// of the SELECT query + /// The value at the index double GetDouble(int index); + /// + /// Gets the value of the nth selected + /// value of the query row (in order of what was specified in the + /// SELECT portion of the query) + /// + /// The index of the element to retrieve in terms + /// of the SELECT query + /// The value at the index int GetInt(int index); + /// + /// Gets the value of the nth selected + /// value of the query row (in order of what was specified in the + /// SELECT portion of the query) + /// + /// The index of the element to retrieve in terms + /// of the SELECT query + /// The value at the index long GetLong(int index); + /// + /// Gets the value of the nth selected + /// value of the query row (in order of what was specified in the + /// SELECT portion of the query) + /// + /// The index of the element to retrieve in terms + /// of the SELECT query + /// The value at the index object GetObject(int index); + /// + /// Gets the value of the nth selected + /// value of the query row (in order of what was specified in the + /// SELECT portion of the query) + /// + /// The index of the element to retrieve in terms + /// of the SELECT query + /// The value at the index string GetString(int index); #endregion diff --git a/src/Couchbase.Lite/API/Query/ILiveQuery.cs b/src/Couchbase.Lite/API/Query/ILiveQuery.cs index b0209231e..35d8c52bb 100644 --- a/src/Couchbase.Lite/API/Query/ILiveQuery.cs +++ b/src/Couchbase.Lite/API/Query/ILiveQuery.cs @@ -23,12 +23,21 @@ namespace Couchbase.Lite.Query { + /// + /// Arguments for the event + /// public sealed class LiveQueryChangedEventArgs : EventArgs { #region Properties + /// + /// Gets the updated rows of the query + /// public IReadOnlyList Rows { get; } + /// + /// Gets the error that occurred, if any + /// public Exception Error { get; } #endregion diff --git a/src/Couchbase.Lite/API/Sync/IReplication.cs b/src/Couchbase.Lite/API/Sync/IReplication.cs index 2a263da66..a5ee90eef 100644 --- a/src/Couchbase.Lite/API/Sync/IReplication.cs +++ b/src/Couchbase.Lite/API/Sync/IReplication.cs @@ -19,6 +19,7 @@ // limitations under the License. // using System; +using System.Collections.Generic; namespace Couchbase.Lite.Sync { @@ -61,6 +62,12 @@ public interface IReplication : IDisposable /// Exception LastError { get; } + ///// + ///// Get or set options affecting replication (See + ///// for a list of keys) + ///// + //IDictionary Options { get; set; } + /// /// Gets the remote being replicated to, if this is /// a local replication. diff --git a/src/Couchbase.Lite/API/Sync/ReplicationOptionKeys.cs b/src/Couchbase.Lite/API/Sync/ReplicationOptionKeys.cs new file mode 100644 index 000000000..fa361b1f1 --- /dev/null +++ b/src/Couchbase.Lite/API/Sync/ReplicationOptionKeys.cs @@ -0,0 +1,30 @@ +// +// ReplicationOptionKeys.cs +// +// Author: +// Jim Borden +// +// Copyright (c) 2017 Couchbase, Inc All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Couchbase.Lite.Sync +{ + internal struct ReplicationOptionKeys + { + public static readonly string AuthOption = "auth"; + public static readonly string AuthUsername = "username"; + public static readonly string AuthPassword = "password"; + } +} diff --git a/src/Couchbase.Lite/Sync/Replication.cs b/src/Couchbase.Lite/Sync/Replication.cs index 688c020a0..4193141d5 100644 --- a/src/Couchbase.Lite/Sync/Replication.cs +++ b/src/Couchbase.Lite/Sync/Replication.cs @@ -19,6 +19,7 @@ // limitations under the License. // using System; +using System.Collections.Generic; using System.Linq; using System.Text; @@ -55,6 +56,7 @@ internal sealed unsafe class Replication : IReplication public bool Continuous { get; set; } public Database Database { get; } + public IDictionary Options { get; set; } public Database OtherDatabase { get; } public bool Pull { get; set; } public bool Push { get; set; } @@ -207,12 +209,6 @@ public void Start() dbNameStr = RemoteUrl.Segments.Last().TrimEnd('/'); } - var database = Database as Database; - var otherDatabase = OtherDatabase as Database; - if (database == null) { - throw new NotSupportedException("Custom IDatabase not supported in Replication"); - } - C4Error err; using (var scheme = new C4String(RemoteUrl?.Scheme)) using (var host = new C4String(RemoteUrl?.Host)) @@ -229,17 +225,26 @@ public void Start() path = path.AsC4Slice() }; + var options = Options ?? new Dictionary(); + var userComponents = RemoteUrl?.UserInfo?.Split(':'); + if (userComponents?.Length == 2 && !options.ContainsKey(ReplicationOptionKeys.AuthOption)) { + var auth = new Dictionary(2); + auth[ReplicationOptionKeys.AuthUsername] = userComponents[0]; + auth[ReplicationOptionKeys.AuthPassword] = userComponents[1]; + options[ReplicationOptionKeys.AuthOption] = auth; + } + _callback = new ReplicatorStateChangedCallback(StatusChangedCallback, this); - var otherDb = otherDatabase == null ? null : otherDatabase.c4db; - _repl = Native.c4repl_new(database.c4db, addr, dbNameStr, otherDb, Mkmode(Push, Continuous), - Mkmode(Pull, Continuous), null, _callback, &err); + var otherDb = OtherDatabase == null ? null : OtherDatabase.c4db; + _repl = Native.c4repl_new(Database.c4db, addr, dbNameStr, otherDb, Mkmode(Push, Continuous), + Mkmode(Pull, Continuous), options, _callback, &err); } C4ReplicatorStatus status; if (_repl != null) { status = Native.c4repl_getStatus(_repl); - database.ActiveReplications.Add(this); + Database.ActiveReplications.Add(this); } else { status = new C4ReplicatorStatus { level = C4ReplicatorActivityLevel.Stopped, diff --git a/src/Couchbase.Lite/Sync/WebSocketTransport.cs b/src/Couchbase.Lite/Sync/WebSocketTransport.cs index bbe8c6e24..7d3317a42 100644 --- a/src/Couchbase.Lite/Sync/WebSocketTransport.cs +++ b/src/Couchbase.Lite/Sync/WebSocketTransport.cs @@ -66,7 +66,7 @@ private static void DoCompleteReceive(C4Socket* socket, ulong bytecount) socketWrapper.CompletedReceive(bytecount); } - private static void DoOpen(C4Socket* socket, C4Address* address) + private static void DoOpen(C4Socket* socket, C4Address* address, C4Slice options) { var builder = new UriBuilder { Host = address->hostname.CreateString(), @@ -88,7 +88,10 @@ private static void DoOpen(C4Socket* socket, C4Address* address) return; } - var socketWrapper = new WebSocketWrapper(uri, socket); + var opts = + FLSliceExtensions.ToObject(NativeRaw.FLValue_FromTrustedData((FLSlice) options)) as + IReadOnlyDictionary; + var socketWrapper = new WebSocketWrapper(uri, socket, opts); var id = Interlocked.Increment(ref _NextID); socket->nativeHandle = (void*)id; Sockets[id] = socketWrapper; diff --git a/src/Couchbase.Lite/Sync/WebSocketWrapper.cs b/src/Couchbase.Lite/Sync/WebSocketWrapper.cs index c188e42a1..1b8db3fdd 100644 --- a/src/Couchbase.Lite/Sync/WebSocketWrapper.cs +++ b/src/Couchbase.Lite/Sync/WebSocketWrapper.cs @@ -21,12 +21,14 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Net.WebSockets; using System.Text; using System.Threading; using Couchbase.Lite.Logging; using Couchbase.Lite.Support; +using Couchbase.Lite.Util; using LiteCore.Interop; namespace Couchbase.Lite.Sync @@ -50,6 +52,7 @@ internal sealed unsafe class WebSocketWrapper private readonly SerialQueue _queue = new SerialQueue(); private readonly SemaphoreSlim _mutex = new SemaphoreSlim(1, 1); private readonly ManualResetEventSlim _connected = new ManualResetEventSlim(); + private readonly IReadOnlyDictionary _options; private readonly C4Socket* _socket; private readonly Uri _url; @@ -66,11 +69,12 @@ internal sealed unsafe class WebSocketWrapper #region Constructors - public WebSocketWrapper(Uri url, C4Socket* socket) + public WebSocketWrapper(Uri url, C4Socket* socket, IReadOnlyDictionary options) { WebSocket.Options.AddSubProtocol("BLIP"); _socket = socket; _url = url; + _options = options; } #endregion @@ -116,6 +120,7 @@ public void Start() { _queue.DispatchAsync(() => { + SetupAuth(); var cts = new CancellationTokenSource(); cts.CancelAfter(ConnectTimeout); WebSocket.ConnectAsync(_url, cts.Token).ContinueWith(t => @@ -148,7 +153,7 @@ public void Write(byte[] data) _connected.Wait(); var cts = new CancellationTokenSource(); cts.CancelAfter(IdleTimeout); - _mutex.Wait(); + _mutex.Wait(cts.Token); WebSocket.SendAsync(new ArraySegment(data), WebSocketMessageType.Binary, true, cts.Token) .ContinueWith(t => { @@ -231,6 +236,18 @@ private void Receive() } + private void SetupAuth() + { + var auth = _options?.Get(ReplicationOptionKeys.AuthOption) as IDictionary; + if (auth != null) { + var username = auth.GetCast(ReplicationOptionKeys.AuthUsername); + var password = auth.GetCast(ReplicationOptionKeys.AuthPassword); + if (username != null && password != null) { + WebSocket.Options.Credentials = new NetworkCredential(username, password); + } + } + } + #endregion } } diff --git a/vendor/couchbase-lite-core b/vendor/couchbase-lite-core index 037a4c5c8..f62aecc01 160000 --- a/vendor/couchbase-lite-core +++ b/vendor/couchbase-lite-core @@ -1 +1 @@ -Subproject commit 037a4c5c851a1c9b1be2051a9c31cfedc77f803b +Subproject commit f62aecc01b278aa8315237f166a88b0ad1ee5cb6