This is an automated email from the ASF dual-hosted git repository.

francischuang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/calcite-avatica-go.git


The following commit(s) were added to refs/heads/master by this push:
     new 30a59fc  [CALCITE-3248] add Connector implementation. (Tino Rusch)
30a59fc is described below

commit 30a59fca23f6dbd0f45b75c24cc3b438ed712969
Author: Tino Rusch <tino.ru...@gmail.com>
AuthorDate: Tue Jul 23 12:00:43 2019 +0200

    [CALCITE-3248] add Connector implementation. (Tino Rusch)
    
    This is to support passing externally prepared HTTP clients for the jdbc 
connection.
    It also includes the refactoring of the authentication methods to be 
separate
    from the driver implementation, so that the user can setup the http client 
as
    wanted and pass that in to the Connector. Also the unused username/password
    fields in the dsn are removed.
---
 driver.go                         |  53 ++++++++++-------
 dsn.go                            |  15 -----
 dsn_test.go                       |  18 +-----
 go.mod                            |   1 +
 go.sum                            |   7 +++
 http_client.go                    | 105 +++++++-------------------------
 http_client_wrappers.go           | 122 ++++++++++++++++++++++++++++++++++++++
 site/_docs/go_client_reference.md |  14 +----
 8 files changed, 187 insertions(+), 148 deletions(-)

diff --git a/driver.go b/driver.go
index d2b9288..7e63488 100644
--- a/driver.go
+++ b/driver.go
@@ -27,7 +27,7 @@ Import the database/sql package along with the avatica driver.
 
        db, err := sql.Open("avatica", "http://phoenix-query-server:8765";)
 
-See https://calcite.apache.org/avatica/go_client_reference.html for more 
details
+See https://calcite.apache.org/avatica/docs/go_client_reference.html for more 
details
 */
 package avatica
 
@@ -36,6 +36,7 @@ import (
        "database/sql"
        "database/sql/driver"
        "fmt"
+       "net/http"
 
        "github.com/apache/calcite-avatica-go/v4/generic"
        "github.com/apache/calcite-avatica-go/v4/hsqldb"
@@ -47,26 +48,28 @@ import (
 // Driver is exported to allow it to be used directly.
 type Driver struct{}
 
-// Open a Connection to the server.
-// See https://github.com/apache/calcite-avatica-go#dsn for more information
-// on how the DSN is formatted.
-func (a *Driver) Open(dsn string) (driver.Conn, error) {
+// Connector implements the driver.Connector interface
+type Connector struct {
+       Info   map[string]string
+       Client *http.Client
+
+       dsn string
+}
+
+// NewConnector creates a new connector
+func NewConnector(dsn string) driver.Connector {
+       return &Connector{nil, nil, dsn}
+}
 
-       config, err := ParseDSN(dsn)
+func (c *Connector) Connect(context.Context) (driver.Conn, error) {
+
+       config, err := ParseDSN(c.dsn)
 
        if err != nil {
                return nil, fmt.Errorf("Unable to open connection: %s", err)
        }
 
-       httpClient, err := NewHTTPClient(config.endpoint, httpClientAuthConfig{
-               authenticationType:  config.authentication,
-               username:            config.avaticaUser,
-               password:            config.avaticaPassword,
-               principal:           config.principal,
-               keytab:              config.keytab,
-               krb5Conf:            config.krb5Conf,
-               krb5CredentialCache: config.krb5CredentialCache,
-       })
+       httpClient, err := NewHTTPClient(config.endpoint, c.Client, config)
 
        if err != nil {
                return nil, fmt.Errorf("Unable to create HTTP client: %s", err)
@@ -82,12 +85,8 @@ func (a *Driver) Open(dsn string) (driver.Conn, error) {
                "Consistency": "8",
        }
 
-       if config.user != "" {
-               info["user"] = config.user
-       }
-
-       if config.password != "" {
-               info["password"] = config.password
+       for k, v := range c.Info {
+               info[k] = v
        }
 
        conn := &conn{
@@ -135,6 +134,18 @@ func (a *Driver) Open(dsn string) (driver.Conn, error) {
        return conn, nil
 }
 
+// Driver returns the underlying driver
+func (c *Connector) Driver() driver.Driver {
+       return &Driver{}
+}
+
+// Open a Connection to the server.
+// See https://github.com/apache/calcite-avatica-go#dsn for more information
+// on how the DSN is formatted.
+func (a *Driver) Open(dsn string) (driver.Conn, error) {
+       return NewConnector(dsn).Connect(context.TODO())
+}
+
 func getAdapter(e string) Adapter {
        switch e {
        case "HSQL Database Engine Driver":
diff --git a/dsn.go b/dsn.go
index c836b9d..b5fc8f5 100644
--- a/dsn.go
+++ b/dsn.go
@@ -43,9 +43,6 @@ type Config struct {
        schema               string
        transactionIsolation uint32
 
-       user     string
-       password string
-
        authentication      authentication
        avaticaUser         string
        avaticaPassword     string
@@ -76,18 +73,6 @@ func ParseDSN(dsn string) (*Config, error) {
                return nil, fmt.Errorf("Unable to parse DSN: %s", err)
        }
 
-       userInfo := parsed.User
-
-       if userInfo != nil {
-               if userInfo.Username() != "" {
-                       conf.user = userInfo.Username()
-               }
-
-               if pass, ok := userInfo.Password(); ok {
-                       conf.password = pass
-               }
-       }
-
        queries := parsed.Query()
 
        if v := queries.Get("maxRowsTotal"); v != "" {
diff --git a/dsn_test.go b/dsn_test.go
index 54da2f1..e178903 100644
--- a/dsn_test.go
+++ b/dsn_test.go
@@ -25,20 +25,12 @@ import (
 
 func TestParseDSN(t *testing.T) {
 
-       config, err := 
ParseDSN("http://username:password@localhost:8765/myschema?maxRowsTotal=1&frameMaxSize=1&location=Australia/Melbourne&transactionIsolation=8&authentication=BASIC&avaticaUser=someuser&avaticaPassword=somepassword";)
+       config, err := 
ParseDSN("http://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" {
-               t.Errorf("Expected username to be %s, got %s", "username", 
config.user)
-       }
-
-       if config.password != "password" {
-               t.Errorf("Expected password to be %s, got %s", "password", 
config.password)
-       }
-
        if config.endpoint != "http://localhost:8765/myschema"; {
                t.Errorf("Expected endpoint to be %s, got %s", 
"http://localhost:8765/myschema";, config.endpoint)
        }
@@ -93,14 +85,6 @@ func TestDSNDefaults(t *testing.T) {
                t.Fatalf("Unexpected error: %s", err)
        }
 
-       if config.user != "" {
-               t.Errorf("Default username should be empty, got %s", 
config.user)
-       }
-
-       if config.password != "" {
-               t.Errorf("Default password should be empty, got %s", 
config.password)
-       }
-
        if config.location.String() == "" {
                t.Error("There was no timezone set.")
        }
diff --git a/go.mod b/go.mod
index 09c2285..18ac111 100644
--- a/go.mod
+++ b/go.mod
@@ -6,6 +6,7 @@ require (
        github.com/golang/protobuf v1.3.1
        github.com/hashicorp/go-uuid v1.0.1
        github.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03 // 
indirect
+       github.com/stretchr/testify v1.3.0 // indirect
        github.com/xinsnake/go-http-digest-auth-client v0.4.0
        golang.org/x/crypto v0.0.0-20190424203555-c05e17bb3b2d // indirect
        gopkg.in/jcmturner/aescts.v1 v1.0.1 // indirect
diff --git a/go.sum b/go.sum
index f49301f..0b1d162 100644
--- a/go.sum
+++ b/go.sum
@@ -1,9 +1,16 @@
+github.com/davecgh/go-spew v1.1.0 
h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+github.com/davecgh/go-spew v1.1.0/go.mod 
h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/golang/protobuf v1.3.1 
h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
 github.com/golang/protobuf v1.3.1/go.mod 
h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/hashicorp/go-uuid v1.0.1 
h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=
 github.com/hashicorp/go-uuid v1.0.1/go.mod 
h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
 github.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03 
h1:FUwcHNlEqkqLjLBdCp5PRlCFijNjvcYANOZXzCfXwCM=
 github.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03/go.mod 
h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
+github.com/pmezard/go-difflib v1.0.0 
h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod 
h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod 
h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0 
h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
+github.com/stretchr/testify v1.3.0/go.mod 
h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/xinsnake/go-http-digest-auth-client v0.4.0 
h1:tTaEBUSDiMi7RDIuj0fy/pszIub8g2DmLjTelB3/3Tk=
 github.com/xinsnake/go-http-digest-auth-client v0.4.0/go.mod 
h1:QK1t1v7ylyGb363vGWu+6Irh7gyFj+N7+UZzM0L6g8I=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod 
h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
diff --git a/http_client.go b/http_client.go
index df0828c..d877e45 100644
--- a/http_client.go
+++ b/http_client.go
@@ -31,36 +31,16 @@ import (
 
        avaticaMessage "github.com/apache/calcite-avatica-go/v4/message"
        "github.com/golang/protobuf/proto"
-       "github.com/xinsnake/go-http-digest-auth-client"
-       "gopkg.in/jcmturner/gokrb5.v7/client"
-       "gopkg.in/jcmturner/gokrb5.v7/config"
-       "gopkg.in/jcmturner/gokrb5.v7/credentials"
-       "gopkg.in/jcmturner/gokrb5.v7/keytab"
-       gokrbSPNEGO "gopkg.in/jcmturner/gokrb5.v7/spnego"
 )
 
 var (
        badConnRe = 
regexp.MustCompile(`org\.apache\.calcite\.avatica\.NoSuchConnectionException`)
 )
 
-type httpClientAuthConfig struct {
-       authenticationType authentication
-
-       username string
-       password string
-
-       principal           krb5Principal
-       keytab              string
-       krb5Conf            string
-       krb5CredentialCache string
-}
-
 // httpClient wraps the default http.Client to communicate with the Avatica 
server.
 type httpClient struct {
-       host           string
-       authConfig     httpClientAuthConfig
-       httpClient     *http.Client
-       kerberosClient *client.Client
+       host       string
+       httpClient *http.Client
 }
 
 type avaticaError struct {
@@ -72,13 +52,10 @@ func (e avaticaError) Error() string {
 }
 
 // NewHTTPClient creates a new httpClient from a host.
-func NewHTTPClient(host string, authenticationConf httpClientAuthConfig) 
(*httpClient, error) {
+func NewHTTPClient(host string, baseClient *http.Client, config *Config) 
(*httpClient, error) {
 
-       c := &httpClient{
-               host:       host,
-               authConfig: authenticationConf,
-
-               httpClient: &http.Client{
+       if baseClient == nil {
+               baseClient = &http.Client{
                        Transport: &http.Transport{
                                Proxy: http.ProxyFromEnvironment,
                                DialContext: (&net.Dialer{
@@ -92,56 +69,28 @@ func NewHTTPClient(host string, authenticationConf 
httpClientAuthConfig) (*httpC
                                ExpectContinueTimeout: 1 * time.Second,
                                MaxIdleConnsPerHost:   runtime.GOMAXPROCS(0) + 
1,
                        },
-               },
-       }
-
-       if authenticationConf.authenticationType == digest {
-               rt := 
digest_auth_client.NewTransport(authenticationConf.username, 
authenticationConf.password)
-               c.httpClient.Transport = &rt
-
-       } else if authenticationConf.authenticationType == spnego {
-
-               if authenticationConf.krb5CredentialCache != "" {
-
-                       tc, err := 
credentials.LoadCCache(authenticationConf.krb5CredentialCache)
-
-                       if err != nil {
-                               return nil, fmt.Errorf("error reading kerberos 
ticket cache: %s", err)
-                       }
-
-                       kc, err := client.NewClientFromCCache(tc, 
config.NewConfig())
-                       if err != nil {
-                               return nil, fmt.Errorf("error creating kerberos 
client: %s", err)
-                       }
-
-                       c.kerberosClient = kc
-
-               } else {
-
-                       cfg, err := config.Load(authenticationConf.krb5Conf)
-
-                       if err != nil {
-                               return nil, fmt.Errorf("error reading kerberos 
config: %s", err)
-                       }
-
-                       kt, err := keytab.Load(authenticationConf.keytab)
-
-                       if err != nil {
-                               return nil, fmt.Errorf("error reading kerberos 
keytab: %s", err)
-                       }
-
-                       kc := 
client.NewClientWithKeytab(authenticationConf.principal.username, 
authenticationConf.principal.realm, kt, cfg)
-
-                       err = kc.Login()
-
+               }
+               switch config.authentication {
+               case digest:
+                       baseClient = WithDigestAuth(baseClient, 
config.avaticaUser, config.avaticaPassword)
+               case basic:
+                       baseClient = WithBasicAuth(baseClient, 
config.avaticaUser, config.avaticaPassword)
+               case spnego:
+                       user := config.principal.username
+                       realm := config.principal.realm
+                       cli, err := WithKerberosAuth(baseClient, user, realm, 
config.keytab, config.krb5Conf, config.krb5CredentialCache)
                        if err != nil {
-                               return nil, fmt.Errorf("error performing 
kerberos login with keytab: %s", err)
+                               return nil, err
                        }
-
-                       c.kerberosClient = kc
+                       baseClient = cli
                }
        }
 
+       c := &httpClient{
+               host:       host,
+               httpClient: baseClient,
+       }
+
        return c, nil
 }
 
@@ -173,16 +122,6 @@ func (c *httpClient) post(ctx context.Context, message 
proto.Message) (proto.Mes
 
        req.Header.Set("Content-Type", "application/x-google-protobuf")
 
-       if c.authConfig.authenticationType == basic {
-               req.SetBasicAuth(c.authConfig.username, c.authConfig.password)
-       } else if c.authConfig.authenticationType == spnego {
-               err := gokrbSPNEGO.SetSPNEGOHeader(c.kerberosClient, req, "")
-
-               if err != nil{
-                       return nil, err
-               }
-       }
-
        req = req.WithContext(ctx)
 
        res, err := c.httpClient.Do(req)
diff --git a/http_client_wrappers.go b/http_client_wrappers.go
new file mode 100644
index 0000000..c45e0f7
--- /dev/null
+++ b/http_client_wrappers.go
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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.
+ */
+
+package avatica
+
+import (
+       "fmt"
+       "net/http"
+
+       digest_auth_client "github.com/xinsnake/go-http-digest-auth-client"
+       "gopkg.in/jcmturner/gokrb5.v7/client"
+       "gopkg.in/jcmturner/gokrb5.v7/config"
+       "gopkg.in/jcmturner/gokrb5.v7/credentials"
+       "gopkg.in/jcmturner/gokrb5.v7/keytab"
+       gokrbSPNEGO "gopkg.in/jcmturner/gokrb5.v7/spnego"
+)
+
+// WithDigestAuth takes an http client and prepares it to authenticate using 
digest authentication
+func WithDigestAuth(cli *http.Client, username, password string) *http.Client {
+       rt := digest_auth_client.NewTransport(username, password)
+       cli.Transport = &rt
+       return cli
+}
+
+// WithBasicAuth takes an http client and prepares it to authenticate using 
basic authentication
+func WithBasicAuth(cli *http.Client, username, password string) *http.Client {
+       rt := &basicAuthTransport{cli.Transport, username, password}
+       cli.Transport = rt
+       return cli
+}
+
+// WithKerberosAuth takes an http client prepares it to authenticate using 
kerberos
+func WithKerberosAuth(cli *http.Client, username, realm, keyTab, krb5Conf, 
krb5CredentialCache string) (*http.Client, error) {
+       var kerberosClient *client.Client
+       if krb5CredentialCache != "" {
+               tc, err := credentials.LoadCCache(krb5CredentialCache)
+               if err != nil {
+                       return nil, fmt.Errorf("error reading kerberos ticket 
cache: %s", err)
+               }
+               kc, err := client.NewClientFromCCache(tc, config.NewConfig())
+               if err != nil {
+                       return nil, fmt.Errorf("error creating kerberos client: 
%s", err)
+               }
+               kerberosClient = kc
+       } else {
+               cfg, err := config.Load(krb5Conf)
+               if err != nil {
+                       return nil, fmt.Errorf("error reading kerberos config: 
%s", err)
+               }
+               kt, err := keytab.Load(keyTab)
+               if err != nil {
+                       return nil, fmt.Errorf("error reading kerberos keytab: 
%s", err)
+               }
+               kc := client.NewClientWithKeytab(username, realm, kt, cfg)
+               err = kc.Login()
+               if err != nil {
+                       return nil, fmt.Errorf("error performing kerberos login 
with keytab: %s", err)
+               }
+               kerberosClient = kc
+       }
+       rt := &krb5Transport{cli.Transport, kerberosClient}
+       cli.Transport = rt
+       return cli, nil
+}
+
+// WithAdditionalHeaders wraps a http client to always include the given set 
of headers
+func WithAdditionalHeaders(cli *http.Client, headers http.Header) *http.Client 
{
+       rt := &additionalHeaderTransport{cli.Transport, headers}
+       cli.Transport = rt
+       return cli
+}
+
+type basicAuthTransport struct {
+       baseTransport      http.RoundTripper
+       username, password string
+}
+
+// RoundTrip implements the http.RoundTripper interface
+func (t *basicAuthTransport) RoundTrip(req *http.Request) (resp 
*http.Response, err error) {
+       req.SetBasicAuth(t.username, t.password)
+       return t.baseTransport.RoundTrip(req)
+}
+
+type krb5Transport struct {
+       baseTransport  http.RoundTripper
+       kerberosClient *client.Client
+}
+
+// RoundTrip implements the http.RoundTripper interface
+func (t *krb5Transport) RoundTrip(req *http.Request) (resp *http.Response, err 
error) {
+       err = gokrbSPNEGO.SetSPNEGOHeader(t.kerberosClient, req, "")
+       if err != nil {
+               return nil, err
+       }
+       return t.baseTransport.RoundTrip(req)
+}
+
+type additionalHeaderTransport struct {
+       baseTransport http.RoundTripper
+       headers       http.Header
+}
+
+func (t *additionalHeaderTransport) RoundTrip(req *http.Request) (resp 
*http.Response, err error) {
+       for key, vals := range t.headers {
+               req.Header[key] = append(req.Header[key], vals...)
+       }
+       return t.baseTransport.RoundTrip(req)
+}
diff --git a/site/_docs/go_client_reference.md 
b/site/_docs/go_client_reference.md
index 6168842..2782294 100644
--- a/site/_docs/go_client_reference.md
+++ b/site/_docs/go_client_reference.md
@@ -71,7 +71,7 @@ rows := db.Query("SELECT COUNT(*) FROM test")
 The DSN has the following format (optional parts are marked by square 
brackets):
 
 {% highlight shell %}
-http://[username:password@]address:port[/schema][?parameter1=value&...parameterN=value]
+http://address:port[/schema][?parameter1=value&...parameterN=value]
 {% endhighlight %}
 
 In other words, the scheme (http), address and port are mandatory, but the 
schema and parameters are optional.
@@ -82,16 +82,6 @@ header tags from kramdown screw up the definition list. We 
lose the pretty
 on-hover images for the permalink, but oh well.
 {% endcomment %}
 
-<strong><a name="username" href="#username">username</a></strong>
-
-This is the JDBC username that is passed directly to the backing database. It 
is *NOT* used for authenticating
-against Avatica.
-
-<strong><a name="password" href="#password">password</a></strong>
-
-This is the JDBC password that is passed directly to the backing database. It 
is *NOT* used for authenticating
-against Avatica.
-
 <strong><a name="schema" href="#schema">schema</a></strong>
 
 The `schema` path sets the default schema to use for this connection. For 
example, if you set it to `myschema`,
@@ -209,4 +199,4 @@ fmt.Println(perr.Name) // Prints: table_undefined
 
 | Driver Version  | Phoenix Version   | Calcite-Avatica Version |
 | :-------------- | :---------------- | :---------------------- |
-| 3.x.x           | >= 4.8.0          | >= 1.11.0               |
\ No newline at end of file
+| 3.x.x           | >= 4.8.0          | >= 1.11.0               |

Reply via email to