Implement Avatica HTTP authentication

Project: http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/repo
Commit: 
http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/commit/472802ba
Tree: http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/tree/472802ba
Diff: http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/diff/472802ba

Branch: refs/heads/master
Commit: 472802ba29f522092fad657b586415e3604f7481
Parents: b1a6e6b
Author: Francis Chuang <francis.chu...@boostport.com>
Authored: Mon May 29 14:04:52 2017 +1000
Committer: Julian Hyde <jh...@apache.org>
Committed: Thu Aug 10 18:47:11 2017 -0700

----------------------------------------------------------------------
 README.md          | 10 +++++++
 driver.go          |  6 +++-
 dsn.go             | 54 +++++++++++++++++++++++++++++++----
 dsn_test.go        | 75 +++++++++++++++++++++++++++++++++++++++++++++----
 http_client.go     | 41 +++++++++++++++++++++++----
 vendor/vendor.json |  6 ++++
 6 files changed, 175 insertions(+), 17 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/blob/472802ba/README.md
----------------------------------------------------------------------
diff --git a/README.md b/README.md
index e211292..429e43f 100644
--- a/README.md
+++ b/README.md
@@ -56,6 +56,16 @@ If schema is set, you can still work on tables in other 
schemas by supplying a s
 
 The following parameters are supported:
 
+#### avaticaUser
+The user to use when authenticating against Avatica.
+
+#### avaticaPassword
+The password to use when authentication against Avatica.
+
+#### authentication
+The authentication type to use when authenticating against Avatica. Valid 
values are `BASIC` for HTTP Basic authentication
+and `DIGEST` for HTTP Digest authentication.
+
 #### location
 
 The `location` will be set as the location of unserialized `time.Time` values. 
It must be a valid timezone.

http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/blob/472802ba/driver.go
----------------------------------------------------------------------
diff --git a/driver.go b/driver.go
index 537aa16..0dd87e8 100644
--- a/driver.go
+++ b/driver.go
@@ -38,7 +38,11 @@ func (a *Driver) Open(dsn string) (driver.Conn, error) {
                return nil, fmt.Errorf("Unable to open connection: %s", err)
        }
 
-       httpClient := NewHTTPClient(config.endpoint)
+       httpClient := NewHTTPClient(config.endpoint, httpClientAuthConfig{
+               username:           config.avaticaUser,
+               password:           config.avaticaPassword,
+               authenticationType: config.authentication,
+       })
        connectionId := uuid.NewV4().String()
 
        info := map[string]string{

http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/blob/472802ba/dsn.go
----------------------------------------------------------------------
diff --git a/dsn.go b/dsn.go
index 1311026..1bfa542 100644
--- a/dsn.go
+++ b/dsn.go
@@ -8,6 +8,14 @@ import (
        "time"
 )
 
+type authentication int
+
+const (
+       none  authentication = iota
+       basic
+       digest
+)
+
 // Config is a configuration parsed from a DSN string
 type Config struct {
        endpoint             string
@@ -16,8 +24,13 @@ type Config struct {
        location             *time.Location
        schema               string
        transactionIsolation uint32
-       user                 string
-       password             string
+
+       user     string
+       password string
+
+       authentication  authentication
+       avaticaUser     string
+       avaticaPassword string
 }
 
 // ParseDSN parses a DSN string to a Config
@@ -83,10 +96,6 @@ func ParseDSN(dsn string) (*Config, error) {
                conf.location = loc
        }
 
-       if parsed.Path != "" {
-               conf.schema = strings.TrimPrefix(parsed.Path, "/")
-       }
-
        if v := queries.Get("transactionIsolation"); v != "" {
 
                isolation, err := strconv.Atoi(v)
@@ -102,6 +111,39 @@ func ParseDSN(dsn string) (*Config, error) {
                conf.transactionIsolation = uint32(isolation)
        }
 
+       if v := queries.Get("authentication"); v != "" {
+
+               auth := strings.ToUpper(v)
+
+               if auth == "BASIC" {
+                       conf.authentication = basic
+               } else if auth == "DIGEST" {
+                       conf.authentication = digest
+               } else {
+                       return nil, fmt.Errorf("authentication must be either 
BASIC or DIGEST")
+               }
+
+               user := queries.Get("avaticaUser")
+
+               if user == "" {
+                       return nil, fmt.Errorf("authentication is set to %s, 
but avaticaUser is empty", v)
+               }
+
+               conf.avaticaUser = user
+
+               pass := queries.Get("avaticaPassword")
+
+               if pass == "" {
+                       return nil, fmt.Errorf("authentication is set to %s, 
but avaticaPassword is empty", v)
+               }
+
+               conf.avaticaPassword = pass
+       }
+
+       if parsed.Path != "" {
+               conf.schema = strings.TrimPrefix(parsed.Path, "/")
+       }
+
        parsed.User = nil
        parsed.RawQuery = ""
        parsed.Fragment = ""

http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/blob/472802ba/dsn_test.go
----------------------------------------------------------------------
diff --git a/dsn_test.go b/dsn_test.go
index 8d10f7d..3e0ecf9 100644
--- a/dsn_test.go
+++ b/dsn_test.go
@@ -8,17 +8,17 @@ import (
 
 func TestParseDSN(t *testing.T) {
 
-       config, err := 
ParseDSN("http://username:password@localhost:8765/myschema?maxRowsTotal=1&frameMaxSize=1&location=Australia/Melbourne&transactionIsolation=8";)
+       config, err := 
ParseDSN("http://username:password@localhost:8765/myschema?maxRowsTotal=1&frameMaxSize=1&location=Australia/Melbourne&transactionIsolation=8&authentication=BASIC&avaticaUser=someuser&avaticaPassword=somepassword";)
 
        if err != nil {
                t.Fatalf("Unexpected error: %s", err)
        }
 
-       if config.user != "username"{
+       if config.user != "username" {
                t.Errorf("Expected username to be %s, got %s", "username", 
config.user)
        }
 
-       if config.password != "password"{
+       if config.password != "password" {
                t.Errorf("Expected password to be %s, got %s", "password", 
config.password)
        }
 
@@ -45,6 +45,18 @@ func TestParseDSN(t *testing.T) {
        if config.transactionIsolation != 8 {
                t.Errorf("Expected transactionIsolation to be %d, got %d", 8, 
config.transactionIsolation)
        }
+
+       if config.authentication != basic {
+               t.Errorf("Expected authentication to be BASIC (%d) got %d", 
basic, config.authentication)
+       }
+
+       if config.avaticaUser != "someuser" {
+               t.Errorf("Expected avaticaUser to be %s, got %s", "someuser", 
config.avaticaUser)
+       }
+
+       if config.avaticaPassword != "somepassword" {
+               t.Errorf("Expected avaticaPassword to be %s, got %s", 
"somepassword", config.avaticaPassword)
+       }
 }
 
 func TestParseEmptyDSN(t *testing.T) {
@@ -64,11 +76,11 @@ func TestDSNDefaults(t *testing.T) {
                t.Fatalf("Unexpected error: %s", err)
        }
 
-       if config.user != ""{
+       if config.user != "" {
                t.Errorf("Default username should be empty, got %s", 
config.user)
        }
 
-       if config.password != ""{
+       if config.password != "" {
                t.Errorf("Default password should be empty, got %s", 
config.password)
        }
 
@@ -91,6 +103,18 @@ func TestDSNDefaults(t *testing.T) {
        if config.transactionIsolation != 0 {
                t.Errorf("Default transaction level should be %d, got %d.", 0, 
config.transactionIsolation)
        }
+
+       if config.authentication != none {
+               t.Errorf("Default authentication should be NONE (%d), got %d", 
none, config.authentication)
+       }
+
+       if config.avaticaUser != "" {
+               t.Errorf("Default avaticaUser should be empty, got %s", 
config.avaticaUser)
+       }
+
+       if config.avaticaPassword != "" {
+               t.Errorf("Default avaticaPassword should be empty, got %s", 
config.avaticaPassword)
+       }
 }
 
 func TestLocallocation(t *testing.T) {
@@ -154,3 +178,44 @@ func TestValidTransactionIsolation(t *testing.T) {
                }
        }
 }
+
+func TestInvalidAuthentication(t *testing.T) {
+
+       _, err := ParseDSN("http://localhost:8765?authentication=ASDF";)
+
+       if err == nil {
+               t.Fatal("Expected error due to invalid authentication, but did 
not receive any.")
+       }
+
+       _, err = ParseDSN("http://localhost:8765?authentication=BASIC";)
+
+       if err == nil {
+               t.Fatal("Expected error due to missing avaticaUser and 
avaticaPassword, but did not receive any.")
+       }
+
+       _, err = 
ParseDSN("http://localhost:8765?authentication=BASIC&avaticaUser=test";)
+
+       if err == nil {
+               t.Fatal("Expected error due to missing avaticaPassword, but did 
not receive any.")
+       }
+
+       _, err = 
ParseDSN("http://localhost:8765?authentication=BASIC&avaticaPassword=test";)
+
+       if err == nil {
+               t.Fatal("Expected error due to missing avaticaUser, but did not 
receive any.")
+       }
+}
+
+func TestValidAuthentication(t *testing.T) {
+       _, err := 
ParseDSN("http://localhost:8765?authentication=BASIC&avaticaUser=test&avaticaPassword=test";)
+
+       if err != nil {
+               t.Fatal("Unexpected error when DSN contains an authentication 
method, avaticaUser and avaticaPassword")
+       }
+
+       _, err = 
ParseDSN("http://localhost:8765?authentication=DIGEST&avaticaUser=test&avaticaPassword=test";)
+
+       if err != nil {
+               t.Fatal("Unexpected error when DSN contains an authentication 
method, avaticaUser and avaticaPassword")
+       }
+}

http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/blob/472802ba/http_client.go
----------------------------------------------------------------------
diff --git a/http_client.go b/http_client.go
index fd9b850..6613cd0 100644
--- a/http_client.go
+++ b/http_client.go
@@ -2,27 +2,46 @@ package avatica
 
 import (
        "bytes"
+       "io/ioutil"
+       "net/http"
+
        avaticaMessage "github.com/Boostport/avatica/message"
        "github.com/golang/protobuf/proto"
        "github.com/hashicorp/go-cleanhttp"
+       "github.com/xinsnake/go-http-digest-auth-client"
        "golang.org/x/net/context"
        "golang.org/x/net/context/ctxhttp"
-       "io/ioutil"
-       "net/http"
 )
 
+type httpClientAuthConfig struct {
+       username           string
+       password           string
+       authenticationType authentication
+}
+
 // httpClient wraps the default http.Client to communicate with the Avatica 
server.
 type httpClient struct {
        host       string
+       authConfig httpClientAuthConfig
+
        httpClient *http.Client
 }
 
 // NewHTTPClient creates a new httpClient from a host.
-func NewHTTPClient(host string) *httpClient {
+func NewHTTPClient(host string, authenticationConf httpClientAuthConfig) 
*httpClient {
+
+       client := cleanhttp.DefaultPooledClient()
+
+       if authenticationConf.authenticationType == digest {
+               rt := 
digest_auth_client.NewTransport(authenticationConf.username, 
authenticationConf.password)
+               client.Transport = &rt
+       }
 
        return &httpClient{
                host:       host,
-               httpClient: cleanhttp.DefaultPooledClient(),
+               authConfig: authenticationConf,
+
+               httpClient: client,
        }
 }
 
@@ -46,7 +65,19 @@ func (c *httpClient) post(ctx context.Context, message 
proto.Message) (proto.Mes
                return nil, err
        }
 
-       res, err := ctxhttp.Post(ctx, c.httpClient, c.host, 
"application/x-google-protobuf", bytes.NewReader(body))
+       req, err := http.NewRequest("POST", c.host, bytes.NewReader(body))
+
+       if err != nil {
+               return nil, err
+       }
+
+       req.Header.Set("Content-Type", "application/x-google-protobuf")
+
+       if c.authConfig.authenticationType == basic {
+               req.SetBasicAuth(c.authConfig.username, c.authConfig.password)
+       }
+
+       res, err := ctxhttp.Do(ctx, c.httpClient, req)
 
        if err != nil {
                return nil, err

http://git-wip-us.apache.org/repos/asf/calcite-avatica-go/blob/472802ba/vendor/vendor.json
----------------------------------------------------------------------
diff --git a/vendor/vendor.json b/vendor/vendor.json
index 9bee373..f66753a 100644
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -157,6 +157,12 @@
                        "revisionTime": "2016-09-27T10:08:44Z"
                },
                {
+                       "checksumSHA1": "pZz/7xcCYgBXbV0T8s+7722/ZOQ=",
+                       "path": 
"github.com/xinsnake/go-http-digest-auth-client",
+                       "revision": "ddd37fe1722021e526546a269b5b5829a3d7b109",
+                       "revisionTime": "2017-05-25T13:53:53Z"
+               },
+               {
                        "checksumSHA1": "D34ZqOW907kHAGTRU6lmxJs1ToU=",
                        "path": "go4.org/syncutil/singleflight",
                        "revision": "7ce08ca145dbe0e66a127c447b80ee7914f3e4f9",

Reply via email to