From ae7cd86605e6d89137b5e4ecdfcaf1754e18b7d9 Mon Sep 17 00:00:00 2001
From: Matthew Fisher <matt.fisher@fermyon.com>
Date: Tue, 7 Jun 2022 13:10:56 -0700
Subject: [PATCH 1/7] implement connection string support

Signed-off-by: Matthew Fisher <matt.fisher@fermyon.com>
---
 src/Bindle/BindleClient.cs                    | 52 ++++++++---------
 src/Bindle/ConnectionInfo.cs                  | 57 +++++++++++++++++++
 src/Bindle/SslMode.cs                         | 17 ++++++
 tests/Bindle.IntegrationTests/Integration.cs  | 16 +++---
 .../Bindle.UnitTests/TomlNamingHelperTests.cs |  1 -
 5 files changed, 107 insertions(+), 36 deletions(-)
 create mode 100644 src/Bindle/ConnectionInfo.cs
 create mode 100644 src/Bindle/SslMode.cs

diff --git a/src/Bindle/BindleClient.cs b/src/Bindle/BindleClient.cs
index 9929220..acce558 100644
--- a/src/Bindle/BindleClient.cs
+++ b/src/Bindle/BindleClient.cs
@@ -15,34 +15,38 @@ namespace Deislabs.Bindle;
 public class BindleClient
 {
     public BindleClient(
-        string baseUri
+        string connectionString
     )
     {
-        _baseUri = new Uri(SlashSafe(baseUri));
-        _httpClient = new HttpClient();
-    }
+        ConnectionInfo connectionInfo = new ConnectionInfo(connectionString);
 
-    public BindleClient(
-        string baseUri,
-        HttpMessageHandler messageHandler
-    )
-    {
-        _baseUri = new Uri(SlashSafe(baseUri));
-        _httpClient = new HttpClient(messageHandler);
+        // TODO: do we want to assume the default listening address?
+        if (string.IsNullOrEmpty(connectionInfo.BaseUri))
+            throw new ArgumentException("base URI cannot be empty");
+
+        var handler = new HttpClientHandler();
+
+        if (connectionInfo.SslMode == SslMode.Disable)
+        {
+            handler.ServerCertificateCustomValidationCallback =
+                (httpRequestMessage, cert, cetChain, policyErrors) =>
+            {
+                return true;
+            };
+        }
+
+        _httpClient = new HttpClient(handler) { BaseAddress = new Uri(SlashSafe(connectionInfo.BaseUri)) };
     }
 
     private const string INVOICE_PATH = "_i";
     private const string QUERY_PATH = "_q";
     private const string RELATIONSHIP_PATH = "_r";
-
-    private readonly Uri _baseUri;
     private readonly HttpClient _httpClient;
 
     public async Task<Invoice> GetInvoice(string invoiceId, GetInvoiceOptions options = GetInvoiceOptions.None)
     {
         var query = GetInvoiceQueryString(options);
-        var uri = new Uri(_baseUri, $"{INVOICE_PATH}/{invoiceId}{query}");
-        var response = await _httpClient.GetAsync(uri);
+        var response = await _httpClient.GetAsync($"{INVOICE_PATH}/{invoiceId}{query}");
         await ExpectResponseCode(response, HttpStatusCode.OK, HttpStatusCode.Forbidden);
 
         if (response.StatusCode == HttpStatusCode.Forbidden)
@@ -74,8 +78,7 @@ public async Task<Matches> QueryInvoices(string? queryString = null,
         bool? yanked = null)
     {
         var query = GetDistinctInvoicesNamesQueryString(queryString, offset, limit, strict, semVer, yanked);
-        var uri = new Uri(_baseUri, $"{QUERY_PATH}?{query}");
-        var response = await _httpClient.GetAsync(uri);
+        var response = await _httpClient.GetAsync($"{QUERY_PATH}?{query}");
         await ExpectResponseCode(response, HttpStatusCode.OK, HttpStatusCode.Forbidden);
 
         if (response.StatusCode == HttpStatusCode.Forbidden)
@@ -99,9 +102,8 @@ public async Task<CreateInvoiceResult> CreateInvoice(Invoice invoice)
             ConvertPropertyName = name => TomlNamingHelper.PascalToCamelCase(name)
         });
 
-        var uri = new Uri(_baseUri, INVOICE_PATH);
         var requestContent = new StringContent(invoiceToml, null, "application/toml");
-        var response = await _httpClient.PostAsync(uri, requestContent);
+        var response = await _httpClient.PostAsync(INVOICE_PATH, requestContent);
         await ExpectResponseCode(response, HttpStatusCode.Created, HttpStatusCode.Accepted);
 
         var content = await response.Content.ReadAsStringAsync();
@@ -115,15 +117,13 @@ public async Task<CreateInvoiceResult> CreateInvoice(Invoice invoice)
 
     public async Task YankInvoice(string invoiceId)
     {
-        var uri = new Uri(_baseUri, $"{INVOICE_PATH}/{invoiceId}");
-        var response = await _httpClient.DeleteAsync(uri);
+        var response = await _httpClient.DeleteAsync($"{INVOICE_PATH}/{invoiceId}");
         await ExpectResponseCode(response, HttpStatusCode.OK);
     }
 
     public async Task<HttpContent> GetParcel(string invoiceId, string parcelId)
     {
-        var uri = new Uri(_baseUri, $"{INVOICE_PATH}/{invoiceId}@{parcelId}");
-        var response = await _httpClient.GetAsync(uri);
+        var response = await _httpClient.GetAsync($"{INVOICE_PATH}/{invoiceId}@{parcelId}");
         await ExpectResponseCode(response, HttpStatusCode.OK);
 
         return response.Content;
@@ -146,15 +146,13 @@ public async Task CreateParcel(string invoiceId, string parcelId, byte[] content
 
     public async Task CreateParcel(string invoiceId, string parcelId, HttpContent content)
     {
-        var uri = new Uri(_baseUri, $"{INVOICE_PATH}/{invoiceId}@{parcelId}");
-        var response = await _httpClient.PostAsync(uri, content);
+        var response = await _httpClient.PostAsync($"{INVOICE_PATH}/{invoiceId}@{parcelId}", content);
         await ExpectResponseCode(response, HttpStatusCode.OK, HttpStatusCode.Created);
     }
 
     public async Task<MissingParcelsResponse> ListMissingParcels(string invoiceId)
     {
-        var uri = new Uri(_baseUri, $"{RELATIONSHIP_PATH}/missing/{invoiceId}");
-        var response = await _httpClient.GetAsync(uri);
+        var response = await _httpClient.GetAsync($"{RELATIONSHIP_PATH}/missing/{invoiceId}");
         await ExpectResponseCode(response, HttpStatusCode.OK);
 
         var content = await response.Content.ReadAsStringAsync();
diff --git a/src/Bindle/ConnectionInfo.cs b/src/Bindle/ConnectionInfo.cs
new file mode 100644
index 0000000..5efecff
--- /dev/null
+++ b/src/Bindle/ConnectionInfo.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Linq;
+
+namespace Deislabs.Bindle;
+
+public class ConnectionInfo
+{
+    static readonly string[] serverAliases = {
+        "server",
+        "host",
+        "data source",
+        "datasource",
+        "address",
+        "addr",
+        "network address",
+    };
+
+    static readonly string[] sslModeAliases = {
+        "sslmode",
+        "ssl mode"
+    };
+
+    public string? BaseUri;
+
+    public SslMode? SslMode;
+
+    public ConnectionInfo(string connectionString)
+    {
+        BaseUri = GetValue(connectionString, serverAliases);
+
+        try
+        {
+            SslMode = Enum.Parse<SslMode>(GetValue(connectionString, sslModeAliases));
+        }
+        catch (Exception)
+        {
+            SslMode = null;
+        }
+    }
+
+    static string GetValue(string connectionString, params string[] keyAliases)
+    {
+        var keyValuePairs = connectionString.Split(';')
+                                            .Where(kvp => kvp.Contains('='))
+                                            .Select(kvp => kvp.Split(new char[] { '=' }, 2))
+                                            .ToDictionary(kvp => kvp[0].Trim(),
+                                                        kvp => kvp[1].Trim(),
+                                                        StringComparer.InvariantCultureIgnoreCase);
+        foreach (var alias in keyAliases)
+        {
+            string? value;
+            if (keyValuePairs.TryGetValue(alias, out value))
+                return value;
+        }
+        return string.Empty;
+    }
+}
diff --git a/src/Bindle/SslMode.cs b/src/Bindle/SslMode.cs
new file mode 100644
index 0000000..f86069a
--- /dev/null
+++ b/src/Bindle/SslMode.cs
@@ -0,0 +1,17 @@
+namespace Deislabs.Bindle;
+
+public enum SslMode
+{
+    // I don't care about security, and I don't want to pay the overhead of encryption.
+    Disable,
+    // I don't care about security, but I will pay the overhead of encryption if the server insists on it.
+    Allow,
+    // I don't care about encryption, but I wish to pay the overhead of encryption if the server supports it.
+    Prefer,
+    // I want my data to be encrypted, and I accept the overhead. I trust that the network will make sure I always connect to the server I want.
+    Require,
+    // I want my data encrypted, and I accept the overhead. I want to be sure that I connect to a server that I trust.
+    VerifyCA,
+    // I want my data encrypted, and I accept the overhead. I want to be sure that I connect to a server I trust, and that it's the one I specify.
+    VerifyFull,
+}
diff --git a/tests/Bindle.IntegrationTests/Integration.cs b/tests/Bindle.IntegrationTests/Integration.cs
index 5b8d6d0..5d025fd 100644
--- a/tests/Bindle.IntegrationTests/Integration.cs
+++ b/tests/Bindle.IntegrationTests/Integration.cs
@@ -23,7 +23,7 @@ public class Integration : IClassFixture<IntegrationFixture>
     [Fact]
     public async Task CanFetchInvoice()
     {
-        var client = new BindleClient(DEMO_SERVER_URL);
+        var client = new BindleClient($"host=${DEMO_SERVER_URL}");
         var invoice = await client.GetInvoice("your/fancy/bindle/0.3.0");
         Assert.Equal("1.0.0", invoice.BindleVersion);
         Assert.Equal("your/fancy/bindle", invoice.Bindle.Name);
@@ -37,7 +37,7 @@ public async Task CanFetchInvoice()
     [Fact]
     public async Task CanQueryInvoices()
     {
-        var client = new BindleClient(DEMO_SERVER_URL);
+        var client = new BindleClient($"host=${DEMO_SERVER_URL}");
         var matches = await client.QueryInvoices(queryString: "my");
         Assert.True(matches.Invoices.All(i => i.Bindle.Name.Contains("my")));
     }
@@ -45,7 +45,7 @@ public async Task CanQueryInvoices()
     [Fact]
     public async Task CanFetchYankedInvoices()
     {
-        var client = new BindleClient(DEMO_SERVER_URL);
+        var client = new BindleClient($"host=${DEMO_SERVER_URL}");
         var invoice = await client.GetInvoice("yourbindle/0.1.1", IncludeYanked);
         Assert.Equal("1.0.0", invoice.BindleVersion);
         Assert.Equal("yourbindle", invoice.Bindle.Name);
@@ -55,7 +55,7 @@ public async Task CanFetchYankedInvoices()
     [Fact]
     public async Task CanCreateInvoices()
     {
-        var client = new BindleClient(DEMO_SERVER_URL);
+        var client = new BindleClient($"host=${DEMO_SERVER_URL}");
         var invoice = new Invoice
         {
             BindleVersion = "1.0.0",
@@ -98,7 +98,7 @@ public async Task CanCreateInvoices()
     [TestPriority(10)]
     public async Task CanYankInvoice()
     {
-        var client = new BindleClient(DEMO_SERVER_URL);
+        var client = new BindleClient($"host=${DEMO_SERVER_URL}");
         await client.YankInvoice("your/fancy/bindle/0.3.0");
         await Assert.ThrowsAsync<BindleYankedException>(async () =>
         {
@@ -111,7 +111,7 @@ await Assert.ThrowsAsync<BindleYankedException>(async () =>
     [Fact]
     public async Task CanFetchParcel()
     {
-        var client = new BindleClient(DEMO_SERVER_URL);
+        var client = new BindleClient($"host=${DEMO_SERVER_URL}");
         var parcel = await client.GetParcel("mybindle/0.1.0", "f7f3b33707fb76d208f5839a40e770452dcf9f348bfd7faf2c524e0fa6710ed6");
         Assert.Equal("Fie on you Gary", await parcel.ReadAsStringAsync());
     }
@@ -119,7 +119,7 @@ public async Task CanFetchParcel()
     [Fact]
     public async Task CanCreateParcel()
     {
-        var client = new BindleClient(DEMO_SERVER_URL);
+        var client = new BindleClient($"host=${DEMO_SERVER_URL}");
         await client.CreateParcel("mybindle/0.1.0", "460d5965e4d1909e8c7a3748a414956b7038ab5fd79937c9fcb2b214e6b0160a", "The front fell off");
         var fetched = await client.GetParcel("mybindle/0.1.0", "460d5965e4d1909e8c7a3748a414956b7038ab5fd79937c9fcb2b214e6b0160a");
         Assert.Equal("The front fell off", await fetched.ReadAsStringAsync());
@@ -128,7 +128,7 @@ public async Task CanCreateParcel()
     [Fact]
     public async Task CanListMissingParcels()
     {
-        var client = new BindleClient(DEMO_SERVER_URL);
+        var client = new BindleClient($"host=${DEMO_SERVER_URL}");
         var resp = await client.ListMissingParcels("mybindle/0.3.0");
         Assert.Contains(resp.Missing, (label) => label.Sha256 == "e1706ab0a39ac88094b6d54a3f5cdba41fe5a901");
         Assert.DoesNotContain(resp.Missing, (label) => label.Sha256 == "f7f3b33707fb76d208f5839a40e770452dcf9f348bfd7faf2c524e0fa6710ed6");
diff --git a/tests/Bindle.UnitTests/TomlNamingHelperTests.cs b/tests/Bindle.UnitTests/TomlNamingHelperTests.cs
index 63f8288..2f66e2c 100644
--- a/tests/Bindle.UnitTests/TomlNamingHelperTests.cs
+++ b/tests/Bindle.UnitTests/TomlNamingHelperTests.cs
@@ -1,4 +1,3 @@
-using Deislabs.Bindle;
 using Xunit;
 
 namespace Deislabs.Bindle.UnitTests;

From 8d8cab7a9448eb6c5222f47d51aec9d0a92cfd9f Mon Sep 17 00:00:00 2001
From: Matthew Fisher <matt.fisher@fermyon.com>
Date: Tue, 7 Jun 2022 13:15:27 -0700
Subject: [PATCH 2/7] ignore ssl mode casing

Signed-off-by: Matthew Fisher <matt.fisher@fermyon.com>
---
 src/Bindle/ConnectionInfo.cs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Bindle/ConnectionInfo.cs b/src/Bindle/ConnectionInfo.cs
index 5efecff..3348549 100644
--- a/src/Bindle/ConnectionInfo.cs
+++ b/src/Bindle/ConnectionInfo.cs
@@ -30,7 +30,7 @@ public ConnectionInfo(string connectionString)
 
         try
         {
-            SslMode = Enum.Parse<SslMode>(GetValue(connectionString, sslModeAliases));
+            SslMode = Enum.Parse<SslMode>(GetValue(connectionString, sslModeAliases), true);
         }
         catch (Exception)
         {

From 487f3ede4784c0785ff835daac96ec278ac6b23f Mon Sep 17 00:00:00 2001
From: Matthew Fisher <matt.fisher@fermyon.com>
Date: Tue, 7 Jun 2022 13:38:54 -0700
Subject: [PATCH 3/7] add unit tests

Signed-off-by: Matthew Fisher <matt.fisher@fermyon.com>
---
 src/Bindle/ConnectionInfo.cs                  |  2 +-
 tests/Bindle.UnitTests/ConnectionInfoTests.cs | 64 +++++++++++++++++++
 2 files changed, 65 insertions(+), 1 deletion(-)
 create mode 100644 tests/Bindle.UnitTests/ConnectionInfoTests.cs

diff --git a/src/Bindle/ConnectionInfo.cs b/src/Bindle/ConnectionInfo.cs
index 3348549..aea5a5b 100644
--- a/src/Bindle/ConnectionInfo.cs
+++ b/src/Bindle/ConnectionInfo.cs
@@ -20,7 +20,7 @@ public class ConnectionInfo
         "ssl mode"
     };
 
-    public string? BaseUri;
+    public string BaseUri;
 
     public SslMode? SslMode;
 
diff --git a/tests/Bindle.UnitTests/ConnectionInfoTests.cs b/tests/Bindle.UnitTests/ConnectionInfoTests.cs
new file mode 100644
index 0000000..9a41ffb
--- /dev/null
+++ b/tests/Bindle.UnitTests/ConnectionInfoTests.cs
@@ -0,0 +1,64 @@
+using System;
+using Xunit;
+
+namespace Deislabs.Bindle.UnitTests;
+
+public class ConnectionInfoTests
+{
+    [Fact]
+    public void ShouldAcceptServerAliases()
+    {
+        Assert.Equal("", (new ConnectionInfo("").BaseUri));
+        Assert.Equal("", (new ConnectionInfo("server=").BaseUri));
+        Assert.Equal("localhost", (new ConnectionInfo("server=localhost").BaseUri));
+        Assert.Equal("localhost", (new ConnectionInfo("host=localhost").BaseUri));
+        Assert.Equal("localhost", (new ConnectionInfo("data source=localhost").BaseUri));
+        Assert.Equal("localhost", (new ConnectionInfo("datasource=localhost").BaseUri));
+        Assert.Equal("localhost", (new ConnectionInfo("address=localhost").BaseUri));
+        Assert.Equal("localhost", (new ConnectionInfo("addr=localhost").BaseUri));
+        Assert.Equal("localhost", (new ConnectionInfo("network address=localhost").BaseUri));
+    }
+
+    [Fact]
+    public void ShouldAcceptSslModeAliases()
+    {
+        Assert.Null(new ConnectionInfo("").SslMode);
+        Assert.Null(new ConnectionInfo("sslmode=").SslMode);
+        Assert.Null(new ConnectionInfo("sslmode=doesnotexist").SslMode);
+        Assert.Equal(SslMode.Disable, (new ConnectionInfo("sslmode=disable").SslMode));
+        Assert.Equal(SslMode.Disable, (new ConnectionInfo("sslmode=Disable").SslMode));
+        Assert.Equal(SslMode.Disable, (new ConnectionInfo("ssl mode=disable").SslMode));
+        Assert.Equal(SslMode.Allow, (new ConnectionInfo("sslmode=allow").SslMode));
+        Assert.Equal(SslMode.Prefer, (new ConnectionInfo("sslmode=prefer").SslMode));
+        Assert.Equal(SslMode.Require, (new ConnectionInfo("sslmode=require").SslMode));
+        Assert.Equal(SslMode.VerifyCA, (new ConnectionInfo("sslmode=verifyca").SslMode));
+        Assert.Equal(SslMode.VerifyFull, (new ConnectionInfo("sslmode=verifyfull").SslMode));
+    }
+
+    [Fact]
+    public void ShouldAcceptMultipleOptions()
+    {
+        var connectionInfos = new ConnectionInfo[]
+        {
+            new ConnectionInfo("server=localhost;sslmode=verifyfull"),
+            new ConnectionInfo("server=localhost; sslmode=verifyfull"),
+            new ConnectionInfo(" server=localhost;sslmode=verifyfull"),
+            new ConnectionInfo(" server=localhost; sslmode=verifyfull"),
+            new ConnectionInfo("server=localhost;sslmode=verifyfull "),
+            new ConnectionInfo(" server=localhost;sslmode=verifyfull "),
+            new ConnectionInfo("server=localhost;;sslmode=verifyfull"),
+            new ConnectionInfo("server=localhost;    sslmode=verifyfull"),
+        };
+        foreach (var connectionInfo in connectionInfos)
+        {
+            Assert.Equal("localhost", connectionInfo.BaseUri);
+            Assert.Equal(SslMode.VerifyFull, connectionInfo.SslMode);
+        }
+    }
+
+    [Fact]
+    public void ShouldNotAcceptDuplicates()
+    {
+        Assert.Throws<ArgumentException>(() => new ConnectionInfo("sslmode=disable;sslmode=verifyfull"));
+    }
+}

From 80d593afa50b96d4504a22dc2f2e9abb41ca2a1d Mon Sep 17 00:00:00 2001
From: Matthew Fisher <matt.fisher@fermyon.com>
Date: Tue, 7 Jun 2022 14:04:28 -0700
Subject: [PATCH 4/7] optimize parse loop

Signed-off-by: Matthew Fisher <matt.fisher@fermyon.com>
---
 src/Bindle/BindleClient.cs   |  1 -
 src/Bindle/ConnectionInfo.cs | 27 ++++++++++++++++++---------
 src/Bindle/SslMode.cs        |  5 +++++
 3 files changed, 23 insertions(+), 10 deletions(-)

diff --git a/src/Bindle/BindleClient.cs b/src/Bindle/BindleClient.cs
index acce558..c600ba8 100644
--- a/src/Bindle/BindleClient.cs
+++ b/src/Bindle/BindleClient.cs
@@ -20,7 +20,6 @@ string connectionString
     {
         ConnectionInfo connectionInfo = new ConnectionInfo(connectionString);
 
-        // TODO: do we want to assume the default listening address?
         if (string.IsNullOrEmpty(connectionInfo.BaseUri))
             throw new ArgumentException("base URI cannot be empty");
 
diff --git a/src/Bindle/ConnectionInfo.cs b/src/Bindle/ConnectionInfo.cs
index aea5a5b..cdde5c9 100644
--- a/src/Bindle/ConnectionInfo.cs
+++ b/src/Bindle/ConnectionInfo.cs
@@ -1,10 +1,13 @@
 using System;
+using System.Collections.Generic;
 using System.Linq;
 
 namespace Deislabs.Bindle;
 
 public class ConnectionInfo
 {
+    private readonly Dictionary<string, string> keyValuePairs;
+
     static readonly string[] serverAliases = {
         "server",
         "host",
@@ -26,11 +29,13 @@ public class ConnectionInfo
 
     public ConnectionInfo(string connectionString)
     {
-        BaseUri = GetValue(connectionString, serverAliases);
+        keyValuePairs = GetKeyValuePairs(connectionString);
+
+        BaseUri = GetValue(serverAliases);
 
         try
         {
-            SslMode = Enum.Parse<SslMode>(GetValue(connectionString, sslModeAliases), true);
+            SslMode = Enum.Parse<SslMode>(GetValue(sslModeAliases), true);
         }
         catch (Exception)
         {
@@ -38,14 +43,18 @@ public ConnectionInfo(string connectionString)
         }
     }
 
-    static string GetValue(string connectionString, params string[] keyAliases)
+    private Dictionary<string, string> GetKeyValuePairs(string connectionString)
+    {
+        return connectionString.Split(';')
+            .Where(kvp => kvp.Contains('='))
+            .Select(kvp => kvp.Split(new char[] { '=' }, 2))
+            .ToDictionary(kvp => kvp[0].Trim(),
+                          kvp => kvp[1].Trim(),
+                          StringComparer.InvariantCultureIgnoreCase);
+    }
+
+    private string GetValue(string[] keyAliases)
     {
-        var keyValuePairs = connectionString.Split(';')
-                                            .Where(kvp => kvp.Contains('='))
-                                            .Select(kvp => kvp.Split(new char[] { '=' }, 2))
-                                            .ToDictionary(kvp => kvp[0].Trim(),
-                                                        kvp => kvp[1].Trim(),
-                                                        StringComparer.InvariantCultureIgnoreCase);
         foreach (var alias in keyAliases)
         {
             string? value;
diff --git a/src/Bindle/SslMode.cs b/src/Bindle/SslMode.cs
index f86069a..00c7c48 100644
--- a/src/Bindle/SslMode.cs
+++ b/src/Bindle/SslMode.cs
@@ -5,13 +5,18 @@ public enum SslMode
     // I don't care about security, and I don't want to pay the overhead of encryption.
     Disable,
     // I don't care about security, but I will pay the overhead of encryption if the server insists on it.
+    // TODO(bacongobbler): not implemented
     Allow,
     // I don't care about encryption, but I wish to pay the overhead of encryption if the server supports it.
+    // TODO(bacongobbler): not implemented
     Prefer,
     // I want my data to be encrypted, and I accept the overhead. I trust that the network will make sure I always connect to the server I want.
+    // TODO(bacongobbler): not implemented
     Require,
     // I want my data encrypted, and I accept the overhead. I want to be sure that I connect to a server that I trust.
+    // TODO(bacongobbler): not implemented
     VerifyCA,
     // I want my data encrypted, and I accept the overhead. I want to be sure that I connect to a server I trust, and that it's the one I specify.
+    // TODO(bacongobbler): not implemented
     VerifyFull,
 }

From 3df29732afddc7ca38e551b81305962cb897ffb5 Mon Sep 17 00:00:00 2001
From: Matthew Fisher <matt.fisher@fermyon.com>
Date: Tue, 7 Jun 2022 14:14:14 -0700
Subject: [PATCH 5/7] implement ConnectionInfo ctor

Signed-off-by: Matthew Fisher <matt.fisher@fermyon.com>
---
 src/Bindle/BindleClient.cs   |  8 +++-----
 src/Bindle/ConnectionInfo.cs | 10 ++++++++--
 2 files changed, 11 insertions(+), 7 deletions(-)

diff --git a/src/Bindle/BindleClient.cs b/src/Bindle/BindleClient.cs
index c600ba8..698850d 100644
--- a/src/Bindle/BindleClient.cs
+++ b/src/Bindle/BindleClient.cs
@@ -14,12 +14,10 @@ namespace Deislabs.Bindle;
 
 public class BindleClient
 {
-    public BindleClient(
-        string connectionString
-    )
-    {
-        ConnectionInfo connectionInfo = new ConnectionInfo(connectionString);
+    public BindleClient(string connectionString) : this(new ConnectionInfo(connectionString)) { }
 
+    public BindleClient(ConnectionInfo connectionInfo)
+    {
         if (string.IsNullOrEmpty(connectionInfo.BaseUri))
             throw new ArgumentException("base URI cannot be empty");
 
diff --git a/src/Bindle/ConnectionInfo.cs b/src/Bindle/ConnectionInfo.cs
index cdde5c9..c59b6e9 100644
--- a/src/Bindle/ConnectionInfo.cs
+++ b/src/Bindle/ConnectionInfo.cs
@@ -6,8 +6,6 @@ namespace Deislabs.Bindle;
 
 public class ConnectionInfo
 {
-    private readonly Dictionary<string, string> keyValuePairs;
-
     static readonly string[] serverAliases = {
         "server",
         "host",
@@ -23,10 +21,18 @@ public class ConnectionInfo
         "ssl mode"
     };
 
+    private readonly Dictionary<string, string> keyValuePairs;
+
     public string BaseUri;
 
     public SslMode? SslMode;
 
+    public ConnectionInfo()
+    {
+        keyValuePairs = new Dictionary<string, string>();
+        BaseUri = "http://localhost:8080/v1/";
+    }
+
     public ConnectionInfo(string connectionString)
     {
         keyValuePairs = GetKeyValuePairs(connectionString);

From 3b56e2b90f6e7ab59094d981f83f8761e7b4d13b Mon Sep 17 00:00:00 2001
From: Matthew Fisher <matt.fisher@fermyon.com>
Date: Tue, 7 Jun 2022 14:31:00 -0700
Subject: [PATCH 6/7] fix string interpolation

Signed-off-by: Matthew Fisher <matt.fisher@fermyon.com>
---
 src/Bindle/BindleClient.cs                   |  2 --
 tests/Bindle.IntegrationTests/Integration.cs | 16 ++++++++--------
 2 files changed, 8 insertions(+), 10 deletions(-)

diff --git a/src/Bindle/BindleClient.cs b/src/Bindle/BindleClient.cs
index 698850d..c4b2b2f 100644
--- a/src/Bindle/BindleClient.cs
+++ b/src/Bindle/BindleClient.cs
@@ -1,11 +1,9 @@
 using System;
-using System.Collections.Generic;
 using System.Collections.Specialized;
 using System.IO;
 using System.Linq;
 using System.Net;
 using System.Net.Http;
-using System.Net.Http.Headers;
 using System.Threading.Tasks;
 using Tomlyn;
 using Tomlyn.Syntax;
diff --git a/tests/Bindle.IntegrationTests/Integration.cs b/tests/Bindle.IntegrationTests/Integration.cs
index 5d025fd..7a663fa 100644
--- a/tests/Bindle.IntegrationTests/Integration.cs
+++ b/tests/Bindle.IntegrationTests/Integration.cs
@@ -23,7 +23,7 @@ public class Integration : IClassFixture<IntegrationFixture>
     [Fact]
     public async Task CanFetchInvoice()
     {
-        var client = new BindleClient($"host=${DEMO_SERVER_URL}");
+        var client = new BindleClient($"host={DEMO_SERVER_URL}");
         var invoice = await client.GetInvoice("your/fancy/bindle/0.3.0");
         Assert.Equal("1.0.0", invoice.BindleVersion);
         Assert.Equal("your/fancy/bindle", invoice.Bindle.Name);
@@ -37,7 +37,7 @@ public async Task CanFetchInvoice()
     [Fact]
     public async Task CanQueryInvoices()
     {
-        var client = new BindleClient($"host=${DEMO_SERVER_URL}");
+        var client = new BindleClient($"host={DEMO_SERVER_URL}");
         var matches = await client.QueryInvoices(queryString: "my");
         Assert.True(matches.Invoices.All(i => i.Bindle.Name.Contains("my")));
     }
@@ -45,7 +45,7 @@ public async Task CanQueryInvoices()
     [Fact]
     public async Task CanFetchYankedInvoices()
     {
-        var client = new BindleClient($"host=${DEMO_SERVER_URL}");
+        var client = new BindleClient($"host={DEMO_SERVER_URL}");
         var invoice = await client.GetInvoice("yourbindle/0.1.1", IncludeYanked);
         Assert.Equal("1.0.0", invoice.BindleVersion);
         Assert.Equal("yourbindle", invoice.Bindle.Name);
@@ -55,7 +55,7 @@ public async Task CanFetchYankedInvoices()
     [Fact]
     public async Task CanCreateInvoices()
     {
-        var client = new BindleClient($"host=${DEMO_SERVER_URL}");
+        var client = new BindleClient($"host={DEMO_SERVER_URL}");
         var invoice = new Invoice
         {
             BindleVersion = "1.0.0",
@@ -98,7 +98,7 @@ public async Task CanCreateInvoices()
     [TestPriority(10)]
     public async Task CanYankInvoice()
     {
-        var client = new BindleClient($"host=${DEMO_SERVER_URL}");
+        var client = new BindleClient($"host={DEMO_SERVER_URL}");
         await client.YankInvoice("your/fancy/bindle/0.3.0");
         await Assert.ThrowsAsync<BindleYankedException>(async () =>
         {
@@ -111,7 +111,7 @@ await Assert.ThrowsAsync<BindleYankedException>(async () =>
     [Fact]
     public async Task CanFetchParcel()
     {
-        var client = new BindleClient($"host=${DEMO_SERVER_URL}");
+        var client = new BindleClient($"host={DEMO_SERVER_URL}");
         var parcel = await client.GetParcel("mybindle/0.1.0", "f7f3b33707fb76d208f5839a40e770452dcf9f348bfd7faf2c524e0fa6710ed6");
         Assert.Equal("Fie on you Gary", await parcel.ReadAsStringAsync());
     }
@@ -119,7 +119,7 @@ public async Task CanFetchParcel()
     [Fact]
     public async Task CanCreateParcel()
     {
-        var client = new BindleClient($"host=${DEMO_SERVER_URL}");
+        var client = new BindleClient($"host={DEMO_SERVER_URL}");
         await client.CreateParcel("mybindle/0.1.0", "460d5965e4d1909e8c7a3748a414956b7038ab5fd79937c9fcb2b214e6b0160a", "The front fell off");
         var fetched = await client.GetParcel("mybindle/0.1.0", "460d5965e4d1909e8c7a3748a414956b7038ab5fd79937c9fcb2b214e6b0160a");
         Assert.Equal("The front fell off", await fetched.ReadAsStringAsync());
@@ -128,7 +128,7 @@ public async Task CanCreateParcel()
     [Fact]
     public async Task CanListMissingParcels()
     {
-        var client = new BindleClient($"host=${DEMO_SERVER_URL}");
+        var client = new BindleClient($"host={DEMO_SERVER_URL}");
         var resp = await client.ListMissingParcels("mybindle/0.3.0");
         Assert.Contains(resp.Missing, (label) => label.Sha256 == "e1706ab0a39ac88094b6d54a3f5cdba41fe5a901");
         Assert.DoesNotContain(resp.Missing, (label) => label.Sha256 == "f7f3b33707fb76d208f5839a40e770452dcf9f348bfd7faf2c524e0fa6710ed6");

From 45e087c4fad23e7120092c0f8bfec2a44ba2d47b Mon Sep 17 00:00:00 2001
From: Matthew Fisher <matt.fisher@fermyon.com>
Date: Tue, 7 Jun 2022 15:27:01 -0700
Subject: [PATCH 7/7] basic auth support

Signed-off-by: Matthew Fisher <matt.fisher@fermyon.com>
---
 src/Bindle/BindleClient.cs                    |  5 +++++
 src/Bindle/ConnectionInfo.cs                  | 21 +++++++++++++++++++
 tests/Bindle.UnitTests/ConnectionInfoTests.cs | 19 +++++++++++++++++
 3 files changed, 45 insertions(+)

diff --git a/src/Bindle/BindleClient.cs b/src/Bindle/BindleClient.cs
index c4b2b2f..8bc74c3 100644
--- a/src/Bindle/BindleClient.cs
+++ b/src/Bindle/BindleClient.cs
@@ -31,6 +31,11 @@ public BindleClient(ConnectionInfo connectionInfo)
         }
 
         _httpClient = new HttpClient(handler) { BaseAddress = new Uri(SlashSafe(connectionInfo.BaseUri)) };
+
+        if (!string.IsNullOrEmpty(connectionInfo.UserName) && !string.IsNullOrEmpty(connectionInfo.Password))
+        {
+            _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", $"{connectionInfo.UserName}:{connectionInfo.Password}");
+        }
     }
 
     private const string INVOICE_PATH = "_i";
diff --git a/src/Bindle/ConnectionInfo.cs b/src/Bindle/ConnectionInfo.cs
index c59b6e9..b8d6334 100644
--- a/src/Bindle/ConnectionInfo.cs
+++ b/src/Bindle/ConnectionInfo.cs
@@ -21,16 +21,33 @@ public class ConnectionInfo
         "ssl mode"
     };
 
+    static readonly string[] usernameAliases = {
+        "user",
+        "username"
+    };
+
+    static readonly string[] passwordAliases = {
+        "pass",
+        "passwd",
+        "password"
+    };
+
     private readonly Dictionary<string, string> keyValuePairs;
 
     public string BaseUri;
 
+    public string UserName;
+
+    public string Password;
+
     public SslMode? SslMode;
 
     public ConnectionInfo()
     {
         keyValuePairs = new Dictionary<string, string>();
         BaseUri = "http://localhost:8080/v1/";
+        UserName = String.Empty;
+        Password = String.Empty;
     }
 
     public ConnectionInfo(string connectionString)
@@ -39,6 +56,10 @@ public ConnectionInfo(string connectionString)
 
         BaseUri = GetValue(serverAliases);
 
+        UserName = GetValue(usernameAliases);
+
+        Password = GetValue(passwordAliases);
+
         try
         {
             SslMode = Enum.Parse<SslMode>(GetValue(sslModeAliases), true);
diff --git a/tests/Bindle.UnitTests/ConnectionInfoTests.cs b/tests/Bindle.UnitTests/ConnectionInfoTests.cs
index 9a41ffb..d2b44aa 100644
--- a/tests/Bindle.UnitTests/ConnectionInfoTests.cs
+++ b/tests/Bindle.UnitTests/ConnectionInfoTests.cs
@@ -35,6 +35,25 @@ public void ShouldAcceptSslModeAliases()
         Assert.Equal(SslMode.VerifyFull, (new ConnectionInfo("sslmode=verifyfull").SslMode));
     }
 
+    [Fact]
+    public void ShouldAcceptUserNameAliases()
+    {
+        Assert.Equal("", (new ConnectionInfo("").UserName));
+        Assert.Equal("", (new ConnectionInfo("username=").UserName));
+        Assert.Equal("spongebob", (new ConnectionInfo("username=spongebob").UserName));
+        Assert.Equal("patrick", (new ConnectionInfo("user=patrick").UserName));
+    }
+
+    [Fact]
+    public void ShouldAcceptPasswordAliases()
+    {
+        Assert.Equal("", (new ConnectionInfo("").Password));
+        Assert.Equal("", (new ConnectionInfo("password=").Password));
+        Assert.Equal("imagoofygooberyeah", (new ConnectionInfo("password=imagoofygooberyeah").Password));
+        Assert.Equal("uragoofygooberyeah", (new ConnectionInfo("pass=uragoofygooberyeah").Password));
+        Assert.Equal("wereallgoofygoobersyeah", (new ConnectionInfo("passwd=wereallgoofygoobersyeah").Password));
+    }
+
     [Fact]
     public void ShouldAcceptMultipleOptions()
     {