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 |