Commit e960f2d5 authored by Eric Chiang's avatar Eric Chiang Committed by GitHub

Merge pull request #577 from coreos/dev-sql

dev branch: add SQL storage implementation
parents 1ad04d19 3e8907b8
......@@ -3,6 +3,13 @@ language: go
go:
- 1.7
services:
- postgresql
env:
- DEX_POSTGRES_DATABASE=postgres DEX_POSTGRES_USER=postgres DEX_POSTGRES_HOST="localhost"
install:
- go get -u github.com/golang/lint/golint
......
# Running database tests
Running database tests locally require:
* A systemd based Linux distro.
* A recent version of [rkt](https://github.com/coreos/rkt) installed.
The `standup.sh` script in the SQL directory is used to run databases in
containers with systemd daemonizing the process.
```
$ sudo ./storage/sql/standup.sh create postgres
Starting postgres. To view progress run
journalctl -fu dex-postgres
Running as unit dex-postgres.service.
To run tests export the following environment variables:
export DEX_POSTGRES_DATABASE=postgres; export DEX_POSTGRES_USER=postgres; export DEX_POSTGRES_PASSWORD=postgres; export DEX_POSTGRES_HOST=172.16.28.3:5432
```
Exporting the variables will cause the database tests to be run, rather than
skipped.
```
$ # sqlite takes forever to compile, be sure to install test dependencies
$ go test -v -i ./storage/sql
$ go test -v ./storage/sql
```
When you're done, tear down the unit using the `standup.sh` script.
```
$ sudo ./storage/sql/standup.sh destroy postgres
```
......@@ -10,7 +10,6 @@ DOCKER_IMAGE=$(DOCKER_REPO):$(VERSION)
export GOBIN=$(PWD)/bin
export GO15VENDOREXPERIMENT=1
export CGO_ENABLED:=0
LD_FLAGS="-w -X $(REPO_PATH)/version.Version=$(VERSION)"
......@@ -26,10 +25,12 @@ bin/example-app: FORCE
@go install -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/example-app
test:
@go test $(shell go list ./... | grep -v '/vendor/')
@go test -v -i $(shell go list ./... | grep -v '/vendor/')
@go test -v $(shell go list ./... | grep -v '/vendor/')
testrace:
@CGO_ENABLED=1 go test --race $(shell go list ./... | grep -v '/vendor/')
@go test -v -i --race $(shell go list ./... | grep -v '/vendor/')
@go test -v --race $(shell go list ./... | grep -v '/vendor/')
vet:
@go vet $(shell go list ./... | grep -v '/vendor/')
......@@ -39,7 +40,7 @@ fmt:
lint:
@for package in $(shell go list ./... | grep -v '/vendor/' | grep -v 'api/apipb'); do \
golint $$package; \
golint -set_exit_status $$package; \
done
server/templates_default.go: $(wildcard web/templates/**)
......
......@@ -12,6 +12,7 @@ import (
"github.com/coreos/dex/storage"
"github.com/coreos/dex/storage/kubernetes"
"github.com/coreos/dex/storage/memory"
"github.com/coreos/dex/storage/sql"
)
// Config is the config format for the main application.
......@@ -71,6 +72,18 @@ func (s *Storage) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
err = unmarshal(&config)
s.Config = &config.Config
case "sqlite3":
var config struct {
Config sql.SQLite3 `yaml:"config"`
}
err = unmarshal(&config)
s.Config = &config.Config
case "postgres":
var config struct {
Config sql.Postgres `yaml:"config"`
}
err = unmarshal(&config)
s.Config = &config.Config
default:
return fmt.Errorf("unknown storage type %q", storageMeta.Type)
}
......
issuer: http://127.0.0.1:5556
storage:
# NOTE(ericchiang): This will be replaced by sqlite3 in the future.
type: memory
type: sqlite3
config:
file: examples/dex.db
web:
http: 127.0.0.1:5556
......
hash: 2af4a276277d2ab2ba9de9b0fd67ab7d6b70c07f4171a9efb225f30306d6f3eb
updated: 2016-08-08T11:20:44.300140564-07:00
hash: 149c717bc83cc279ab6192364776b8c1b6bad8a620ce9d64c56d946276630437
updated: 2016-09-30T21:01:57.607704513-07:00
imports:
- name: github.com/cockroachdb/cockroach-go
version: 31611c0501c812f437d4861d87d117053967c955
subpackages:
- crdb
- name: github.com/ericchiang/oidc
version: 1907f0e61549f9081f26bdf269f11603496c9dee
- name: github.com/go-sql-driver/mysql
version: 0b58b37b664c21f3010e836f1b931e1d0b0b0685
- name: github.com/golang/protobuf
version: 874264fbbb43f4d91e999fecb4b40143ed611400
subpackages:
......@@ -21,6 +27,12 @@ imports:
subpackages:
- diff
- pretty
- name: github.com/lib/pq
version: 50761b0867bd1d9d069276790bcd4a3bccf2324a
subpackages:
- oid
- name: github.com/mattn/go-sqlite3
version: 3fb7a0e792edd47bf0cf1e919dfc14e2be412e15
- name: github.com/mitchellh/go-homedir
version: 756f7b183b7ab78acdbbee5c7f392838ed459dda
- name: github.com/pquerna/cachecontrol
......@@ -48,13 +60,13 @@ imports:
- name: google.golang.org/appengine
version: 267c27e7492265b84fc6719503b14a1e17975d79
subpackages:
- urlfetch
- internal
- internal/urlfetch
- internal/base
- internal/datastore
- internal/log
- internal/remote_api
- internal/urlfetch
- urlfetch
- name: gopkg.in/asn1-ber.v1
version: 4e86f4367175e39f69d9358a5f17b4dda270378d
- name: gopkg.in/ldap.v2
......
......@@ -77,3 +77,15 @@ import:
- diff
- pretty
version: eadb3ce320cbab8393bea5ca17bebac3f78a021b
# SQL drivers
- package: github.com/mattn/go-sqlite3
version: 3fb7a0e792edd47bf0cf1e919dfc14e2be412e15
- package: github.com/lib/pq
version: 50761b0867bd1d9d069276790bcd4a3bccf2324a
- package: github.com/go-sql-driver/mysql
version: 0b58b37b664c21f3010e836f1b931e1d0b0b0685
- package: github.com/cockroachdb/cockroach-go
version: 31611c0501c812f437d4861d87d117053967c955
subpackages:
- crdb
......@@ -264,7 +264,8 @@ func (s *Server) finalizeLogin(identity connector.Identity, authReqID, connector
}
updater := func(a storage.AuthRequest) (storage.AuthRequest, error) {
a.Claims = &claims
a.LoggedIn = true
a.Claims = claims
a.ConnectorID = connectorID
a.ConnectorData = identity.ConnectorData
return a, nil
......@@ -282,7 +283,7 @@ func (s *Server) handleApproval(w http.ResponseWriter, r *http.Request) {
s.renderError(w, http.StatusInternalServerError, errServerError, "")
return
}
if authReq.Claims == nil {
if !authReq.LoggedIn {
log.Printf("Auth request does not have an identity for approval")
s.renderError(w, http.StatusInternalServerError, errServerError, "")
return
......@@ -341,7 +342,7 @@ func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authRe
ConnectorID: authReq.ConnectorID,
Nonce: authReq.Nonce,
Scopes: authReq.Scopes,
Claims: *authReq.Claims,
Claims: authReq.Claims,
Expiry: s.now().Add(time.Minute * 5),
RedirectURI: authReq.RedirectURI,
}
......@@ -358,7 +359,7 @@ func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authRe
}
q.Set("code", code.ID)
case responseTypeToken:
idToken, expiry, err := s.newIDToken(authReq.ClientID, *authReq.Claims, authReq.Scopes, authReq.Nonce)
idToken, expiry, err := s.newIDToken(authReq.ClientID, authReq.Claims, authReq.Scopes, authReq.Nonce)
if err != nil {
log.Printf("failed to create ID token: %v", err)
tokenErr(w, errServerError, "", http.StatusInternalServerError)
......
// +build go1.7
// Package conformance provides conformance tests for storage implementations.
package conformance
import (
"reflect"
"testing"
"time"
"github.com/coreos/dex/storage"
"github.com/kylelemons/godebug/pretty"
)
// ensure that values being tested on never expire.
var neverExpire = time.Now().UTC().Add(time.Hour * 24 * 365 * 100)
// StorageFactory is a method for creating a new storage. The returned storage sould be initialized
// but shouldn't have any existing data in it.
type StorageFactory func() storage.Storage
// RunTestSuite runs a set of conformance tests against a storage.
func RunTestSuite(t *testing.T, sf StorageFactory) {
tests := []struct {
name string
run func(t *testing.T, s storage.Storage)
}{
{"AuthCodeCRUD", testAuthCodeCRUD},
{"AuthRequestCRUD", testAuthRequestCRUD},
{"ClientCRUD", testClientCRUD},
{"RefreshTokenCRUD", testRefreshTokenCRUD},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
test.run(t, sf())
})
}
}
func mustBeErrNotFound(t *testing.T, kind string, err error) {
switch {
case err == nil:
t.Errorf("deleting non-existant %s should return an error", kind)
case err != storage.ErrNotFound:
t.Errorf("deleting %s expected storage.ErrNotFound, got %v", kind, err)
}
}
func testAuthRequestCRUD(t *testing.T, s storage.Storage) {
a := storage.AuthRequest{
ID: storage.NewID(),
ClientID: "foobar",
ResponseTypes: []string{"code"},
Scopes: []string{"openid", "email"},
RedirectURI: "https://localhost:80/callback",
Nonce: "foo",
State: "bar",
ForceApprovalPrompt: true,
LoggedIn: true,
Expiry: neverExpire,
ConnectorID: "ldap",
ConnectorData: []byte(`{"some":"data"}`),
Claims: storage.Claims{
UserID: "1",
Username: "jane",
Email: "jane.doe@example.com",
EmailVerified: true,
Groups: []string{"a", "b"},
},
}
identity := storage.Claims{Email: "foobar"}
if err := s.CreateAuthRequest(a); err != nil {
t.Fatalf("failed creating auth request: %v", err)
}
if err := s.UpdateAuthRequest(a.ID, func(old storage.AuthRequest) (storage.AuthRequest, error) {
old.Claims = identity
old.ConnectorID = "connID"
return old, nil
}); err != nil {
t.Fatalf("failed to update auth request: %v", err)
}
got, err := s.GetAuthRequest(a.ID)
if err != nil {
t.Fatalf("failed to get auth req: %v", err)
}
if !reflect.DeepEqual(got.Claims, identity) {
t.Fatalf("update failed, wanted identity=%#v got %#v", identity, got.Claims)
}
}
func testAuthCodeCRUD(t *testing.T, s storage.Storage) {
a := storage.AuthCode{
ID: storage.NewID(),
ClientID: "foobar",
RedirectURI: "https://localhost:80/callback",
Nonce: "foobar",
Scopes: []string{"openid", "email"},
Expiry: neverExpire,
ConnectorID: "ldap",
ConnectorData: []byte(`{"some":"data"}`),
Claims: storage.Claims{
UserID: "1",
Username: "jane",
Email: "jane.doe@example.com",
EmailVerified: true,
Groups: []string{"a", "b"},
},
}
if err := s.CreateAuthCode(a); err != nil {
t.Fatalf("failed creating auth code: %v", err)
}
got, err := s.GetAuthCode(a.ID)
if err != nil {
t.Fatalf("failed to get auth req: %v", err)
}
if a.Expiry.Unix() != got.Expiry.Unix() {
t.Errorf("auth code expiry did not match want=%s vs got=%s", a.Expiry, got.Expiry)
}
got.Expiry = a.Expiry // time fields do not compare well
if diff := pretty.Compare(a, got); diff != "" {
t.Errorf("auth code retrieved from storage did not match: %s", diff)
}
if err := s.DeleteAuthCode(a.ID); err != nil {
t.Fatalf("delete auth code: %v", err)
}
_, err = s.GetAuthCode(a.ID)
mustBeErrNotFound(t, "auth code", err)
}
func testClientCRUD(t *testing.T, s storage.Storage) {
id := storage.NewID()
c := storage.Client{
ID: id,
Secret: "foobar",
RedirectURIs: []string{"foo://bar.com/", "https://auth.example.com"},
Name: "dex client",
LogoURL: "https://goo.gl/JIyzIC",
}
err := s.DeleteClient(id)
mustBeErrNotFound(t, "client", err)
if err := s.CreateClient(c); err != nil {
t.Fatalf("create client: %v", err)
}
getAndCompare := func(id string, want storage.Client) {
gc, err := s.GetClient(id)
if err != nil {
t.Errorf("get client: %v", err)
return
}
if diff := pretty.Compare(want, gc); diff != "" {
t.Errorf("client retrieved from storage did not match: %s", diff)
}
}
getAndCompare(id, c)
newSecret := "barfoo"
err = s.UpdateClient(id, func(old storage.Client) (storage.Client, error) {
old.Secret = newSecret
return old, nil
})
if err != nil {
t.Errorf("update client: %v", err)
}
c.Secret = newSecret
getAndCompare(id, c)
if err := s.DeleteClient(id); err != nil {
t.Fatalf("delete client: %v", err)
}
_, err = s.GetClient(id)
mustBeErrNotFound(t, "client", err)
}
func testRefreshTokenCRUD(t *testing.T, s storage.Storage) {
id := storage.NewID()
refresh := storage.RefreshToken{
RefreshToken: id,
ClientID: "client_id",
ConnectorID: "client_secret",
Scopes: []string{"openid", "email", "profile"},
Claims: storage.Claims{
UserID: "1",
Username: "jane",
Email: "jane.doe@example.com",
EmailVerified: true,
Groups: []string{"a", "b"},
},
}
if err := s.CreateRefresh(refresh); err != nil {
t.Fatalf("create refresh token: %v", err)
}
getAndCompare := func(id string, want storage.RefreshToken) {
gr, err := s.GetRefresh(id)
if err != nil {
t.Errorf("get refresh: %v", err)
return
}
if diff := pretty.Compare(want, gr); diff != "" {
t.Errorf("refresh token retrieved from storage did not match: %s", diff)
}
}
getAndCompare(id, refresh)
if err := s.DeleteRefresh(id); err != nil {
t.Fatalf("failed to delete refresh request: %v", err)
}
if _, err := s.GetRefresh(id); err != storage.ErrNotFound {
t.Errorf("after deleting refresh expected storage.ErrNotFound, got %v", err)
}
}
......@@ -4,7 +4,8 @@ import (
"os"
"testing"
"github.com/coreos/dex/storage/storagetest"
"github.com/coreos/dex/storage"
"github.com/coreos/dex/storage/conformance"
)
func TestLoadClient(t *testing.T) {
......@@ -73,5 +74,8 @@ func TestURLFor(t *testing.T) {
func TestStorage(t *testing.T) {
client := loadClient(t)
storagetest.RunTestSuite(t, client)
conformance.RunTestSuite(t, func() storage.Storage {
// TODO(erichiang): Tear down namespaces between each iteration.
return client
})
}
......@@ -118,9 +118,11 @@ type AuthRequest struct {
// attempts.
ForceApprovalPrompt bool `json:"forceApprovalPrompt,omitempty"`
LoggedIn bool `json:"loggedIn"`
// The identity of the end user. Generally nil until the user authenticates
// with a backend.
Claims *Claims `json:"claims,omitempty"`
Claims Claims `json:"claims,omitempty"`
// The connector used to login the user. Set when the user authenticates.
ConnectorID string `json:"connectorID,omitempty"`
ConnectorData []byte `json:"connectorData,omitempty"`
......@@ -145,13 +147,11 @@ func toStorageAuthRequest(req AuthRequest) storage.AuthRequest {
Nonce: req.Nonce,
State: req.State,
ForceApprovalPrompt: req.ForceApprovalPrompt,
LoggedIn: req.LoggedIn,
ConnectorID: req.ConnectorID,
ConnectorData: req.ConnectorData,
Expiry: req.Expiry,
}
if req.Claims != nil {
i := toStorageClaims(*req.Claims)
a.Claims = &i
Claims: toStorageClaims(req.Claims),
}
return a
}
......@@ -172,14 +172,12 @@ func (cli *client) fromStorageAuthRequest(a storage.AuthRequest) AuthRequest {
RedirectURI: a.RedirectURI,
Nonce: a.Nonce,
State: a.State,
LoggedIn: a.LoggedIn,
ForceApprovalPrompt: a.ForceApprovalPrompt,
ConnectorID: a.ConnectorID,
ConnectorData: a.ConnectorData,
Expiry: a.Expiry,
}
if a.Claims != nil {
i := fromStorageClaims(*a.Claims)
req.Claims = &i
Claims: fromStorageClaims(a.Claims),
}
return req
}
......
......@@ -3,10 +3,9 @@ package memory
import (
"testing"
"github.com/coreos/dex/storage/storagetest"
"github.com/coreos/dex/storage/conformance"
)
func TestStorage(t *testing.T) {
s := New()
storagetest.RunTestSuite(t, s)
conformance.RunTestSuite(t, New)
}
package sql
import (
"database/sql"
"fmt"
"net/url"
"strconv"
"github.com/coreos/dex/storage"
)
// SQLite3 options for creating an SQL db.
type SQLite3 struct {
// File to
File string `yaml:"file"`
}
// Open creates a new storage implementation backed by SQLite3
func (s *SQLite3) Open() (storage.Storage, error) {
return s.open()
}
func (s *SQLite3) open() (*conn, error) {
db, err := sql.Open("sqlite3", s.File)
if err != nil {
return nil, err
}
if s.File == ":memory:" {
// sqlite3 uses file locks to coordinate concurrent access. In memory
// doesn't support this, so limit the number of connections to 1.
db.SetMaxOpenConns(1)
}
c := &conn{db, flavorSQLite3}
if _, err := c.migrate(); err != nil {
return nil, fmt.Errorf("failed to perform migrations: %v", err)
}
return c, nil
}
const (
sslDisable = "disable"
sslRequire = "require"
sslVerifyCA = "verify-ca"
sslVerifyFull = "verify-full"
)
// PostgresSSL represents SSL options for Postgres databases.
type PostgresSSL struct {
Mode string
CAFile string
// Files for client auth.
KeyFile string
CertFile string
}
// Postgres options for creating an SQL db.
type Postgres struct {
Database string
User string
Password string
Host string
SSL PostgresSSL `json:"ssl" yaml:"ssl"`
ConnectionTimeout int // Seconds
}
// Open creates a new storage implementation backed by Postgres.
func (p *Postgres) Open() (storage.Storage, error) {
return p.open()
}
func (p *Postgres) open() (*conn, error) {
v := url.Values{}
set := func(key, val string) {
if val != "" {
v.Set(key, val)
}
}
set("connect_timeout", strconv.Itoa(p.ConnectionTimeout))
set("sslkey", p.SSL.KeyFile)
set("sslcert", p.SSL.CertFile)
set("sslrootcert", p.SSL.CAFile)
if p.SSL.Mode == "" {
// Assume the strictest mode if unspecified.
p.SSL.Mode = sslVerifyFull
}
set("sslmode", p.SSL.Mode)
u := url.URL{
Scheme: "postgres",
Host: p.Host,
Path: "/" + p.Database,
RawQuery: v.Encode(),
}
if p.User != "" {
if p.Password != "" {
u.User = url.UserPassword(p.User, p.Password)
} else {
u.User = url.User(p.User)
}
}
db, err := sql.Open("postgres", u.String())
if err != nil {
return nil, err
}
c := &conn{db, flavorPostgres}
if _, err := c.migrate(); err != nil {
return nil, fmt.Errorf("failed to perform migrations: %v", err)
}
return c, nil
}
package sql
import (
"fmt"
"os"
"runtime"
"testing"
"time"
"github.com/coreos/dex/storage"
"github.com/coreos/dex/storage/conformance"
)
func withTimeout(t time.Duration, f func()) {
c := make(chan struct{})
defer close(c)
go func() {
select {
case <-c:
case <-time.After(t):
// Dump a stack trace of the program. Useful for debugging deadlocks.
buf := make([]byte, 2<<20)
fmt.Fprintf(os.Stderr, "%s\n", buf[:runtime.Stack(buf, true)])
panic("test took too long")
}
}()
f()
}
func cleanDB(c *conn) error {
_, err := c.Exec(`
delete from client;
delete from auth_request;
delete from auth_code;
delete from refresh_token;
delete from keys;
`)
return err
}
func TestSQLite3(t *testing.T) {
newStorage := func() storage.Storage {
// NOTE(ericchiang): In memory means we only get one connection at a time. If we
// ever write tests that require using multiple connections, for instance to test
// transactions, we need to move to a file based system.
s := &SQLite3{":memory:"}
conn, err := s.open()
if err != nil {
t.Fatal(err)
}
return conn
}
withTimeout(time.Second*10, func() {
conformance.RunTestSuite(t, newStorage)
})
}
func TestPostgres(t *testing.T) {
if os.Getenv("DEX_POSTGRES_HOST") == "" {
t.Skip("postgres envs not set, skipping tests")
}
p := Postgres{
Database: os.Getenv("DEX_POSTGRES_DATABASE"),
User: os.Getenv("DEX_POSTGRES_USER"),
Password: os.Getenv("DEX_POSTGRES_PASSWORD"),
Host: os.Getenv("DEX_POSTGRES_HOST"),
SSL: PostgresSSL{
Mode: sslDisable, // Postgres container doesn't support SSL.
},
ConnectionTimeout: 5,
}
conn, err := p.open()
if err != nil {
t.Fatal(err)
}
defer conn.Close()
newStorage := func() storage.Storage {
if err := cleanDB(conn); err != nil {
t.Fatal(err)
}
return conn
}
withTimeout(time.Minute*1, func() {
conformance.RunTestSuite(t, newStorage)
})
}
This diff is collapsed.
package sql
import (
"database/sql"
"reflect"
"testing"
)
func TestDecoder(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
if _, err := db.Exec(`create table foo ( id integer primary key, bar blob );`); err != nil {
t.Fatal(err)
}
if _, err := db.Exec(`insert into foo ( id, bar ) values (1, ?);`, []byte(`["a", "b"]`)); err != nil {
t.Fatal(err)
}
var got []string
if err := db.QueryRow(`select bar from foo where id = 1;`).Scan(decoder(&got)); err != nil {
t.Fatal(err)
}
want := []string{"a", "b"}
if !reflect.DeepEqual(got, want) {
t.Errorf("wanted %q got %q", want, got)
}
}
func TestEncoder(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
if _, err := db.Exec(`create table foo ( id integer primary key, bar blob );`); err != nil {
t.Fatal(err)
}
put := []string{"a", "b"}
if _, err := db.Exec(`insert into foo ( id, bar ) values (1, ?)`, encoder(put)); err != nil {
t.Fatal(err)
}
var got []byte
if err := db.QueryRow(`select bar from foo where id = 1;`).Scan(&got); err != nil {
t.Fatal(err)
}
want := []byte(`["a","b"]`)
if !reflect.DeepEqual(got, want) {
t.Errorf("wanted %q got %q", want, got)
}
}
package sql
import (
"fmt"
"time"
)
type gc struct {
now func() time.Time
conn *conn
}
var tablesWithGC = []string{"auth_request", "auth_code"}
func (gc gc) run() error {
for _, table := range tablesWithGC {
_, err := gc.conn.Exec(`delete from `+table+` where expiry < $1`, gc.now())
if err != nil {
return fmt.Errorf("gc %s: %v", table, err)
}
// TODO(ericchiang): when we have levelled logging print how many rows were gc'd
}
return nil
}
package sql
import (
"testing"
"time"
"github.com/coreos/dex/storage"
)
func TestGC(t *testing.T) {
// TODO(ericchiang): Add a GarbageCollect method to the storage interface so
// we can write conformance tests instead of directly testing each implementation.
s := &SQLite3{":memory:"}
conn, err := s.open()
if err != nil {
t.Fatal(err)
}
defer conn.Close()
clock := time.Now()
now := func() time.Time { return clock }
runGC := (gc{now, conn}).run
a := storage.AuthRequest{
ID: storage.NewID(),
Expiry: now().Add(time.Second),
}
if err := conn.CreateAuthRequest(a); err != nil {
t.Fatal(err)
}
if err := runGC(); err != nil {
t.Errorf("gc failed: %v", err)
}
if _, err := conn.GetAuthRequest(a.ID); err != nil {
t.Errorf("failed to get auth request after gc: %v", err)
}
clock = clock.Add(time.Minute)
if err := runGC(); err != nil {
t.Errorf("gc failed: %v", err)
}
if _, err := conn.GetAuthRequest(a.ID); err == nil {
t.Errorf("expected error after gc'ing auth request: %v", err)
} else if err != storage.ErrNotFound {
t.Errorf("expected error storage.NotFound got: %v", err)
}
}
package sql
import (
"database/sql"
"fmt"
)
func (c *conn) migrate() (int, error) {
_, err := c.Exec(`
create table if not exists migrations (
num integer not null,
at timestamp not null
);
`)
if err != nil {
return 0, fmt.Errorf("creating migration table: %v", err)
}
i := 0
done := false
for {
err := c.ExecTx(func(tx *trans) error {
// Within a transaction, perform a single migration.
var (
num sql.NullInt64
n int
)
if err := tx.QueryRow(`select max(num) from migrations;`).Scan(&num); err != nil {
return fmt.Errorf("select max migration: %v", err)
}
if num.Valid {
n = int(num.Int64)
}
if n >= len(migrations) {
done = true
return nil
}
migrationNum := n + 1
m := migrations[n]
if _, err := tx.Exec(m.stmt); err != nil {
return fmt.Errorf("migration %d failed: %v", migrationNum, err)
}
q := `insert into migrations (num, at) values ($1, now());`
if _, err := tx.Exec(q, migrationNum); err != nil {
return fmt.Errorf("update migration table: %v", err)
}
return nil
})
if err != nil {
return i, err
}
if done {
break
}
i++
}
return i, nil
}
type migration struct {
stmt string
// TODO(ericchiang): consider adding additional fields like "forDrivers"
}
// All SQL flavors share migration strategies.
var migrations = []migration{
{
stmt: `
create table client (
id text not null primary key,
secret text not null,
redirect_uris bytea not null, -- JSON array of strings
trusted_peers bytea not null, -- JSON array of strings
public boolean not null,
name text not null,
logo_url text not null
);
create table auth_request (
id text not null primary key,
client_id text not null,
response_types bytea not null, -- JSON array of strings
scopes bytea not null, -- JSON array of strings
redirect_uri text not null,
nonce text not null,
state text not null,
force_approval_prompt boolean not null,
logged_in boolean not null,
claims_user_id text not null,
claims_username text not null,
claims_email text not null,
claims_email_verified boolean not null,
claims_groups bytea not null, -- JSON array of strings
connector_id text not null,
connector_data bytea,
expiry timestamp not null
);
create table auth_code (
id text not null primary key,
client_id text not null,
scopes bytea not null, -- JSON array of strings
nonce text not null,
redirect_uri text not null,
claims_user_id text not null,
claims_username text not null,
claims_email text not null,
claims_email_verified boolean not null,
claims_groups bytea not null, -- JSON array of strings
connector_id text not null,
connector_data bytea,
expiry timestamp not null
);
create table refresh_token (
id text not null primary key,
client_id text not null,
scopes bytea not null, -- JSON array of strings
nonce text not null,
claims_user_id text not null,
claims_username text not null,
claims_email text not null,
claims_email_verified boolean not null,
claims_groups bytea not null, -- JSON array of strings
connector_id text not null,
connector_data bytea
);
-- keys is a weird table because we only ever expect there to be a single row
create table keys (
id text not null primary key,
verification_keys bytea not null, -- JSON array
signing_key bytea not null, -- JSON object
signing_key_pub bytea not null, -- JSON object
next_rotation timestamp not null
);
`,
},
}
package sql
import (
"database/sql"
"testing"
)
func TestMigrate(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
c := &conn{db, flavorSQLite3}
for _, want := range []int{len(migrations), 0} {
got, err := c.migrate()
if err != nil {
t.Fatal(err)
}
if got != want {
t.Errorf("expected %d migrations, got %d", want, got)
}
}
}
// Package sql provides SQL implementations of the storage interface.
package sql
import (
"database/sql"
"regexp"
"github.com/cockroachdb/cockroach-go/crdb"
// import third party drivers
_ "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
)
// flavor represents a specific SQL implementation, and is used to translate query strings
// between different drivers. Flavors shouldn't aim to translate all possible SQL statements,
// only the specific queries used by the SQL storages.
type flavor struct {
queryReplacers []replacer
// Optional function to create and finish a transaction. This is mainly for
// cockroachdb support which requires special retry logic provided by their
// client package.
//
// This will be nil for most flavors.
//
// See: https://github.com/cockroachdb/docs/blob/63761c2e/_includes/app/txn-sample.go#L41-L44
executeTx func(db *sql.DB, fn func(*sql.Tx) error) error
}
// A regexp with a replacement string.
type replacer struct {
re *regexp.Regexp
with string
}
// Match a postgres query binds. E.g. "$1", "$12", etc.
var bindRegexp = regexp.MustCompile(`\$\d+`)
func matchLiteral(s string) *regexp.Regexp {
return regexp.MustCompile(`\b` + regexp.QuoteMeta(s) + `\b`)
}
var (
// The "github.com/lib/pq" driver is the default flavor. All others are
// translations of this.
flavorPostgres = flavor{}
flavorSQLite3 = flavor{
queryReplacers: []replacer{
{bindRegexp, "?"},
// Translate for booleans to integers.
{matchLiteral("true"), "1"},
{matchLiteral("false"), "0"},
{matchLiteral("boolean"), "integer"},
// Translate other types.
{matchLiteral("bytea"), "blob"},
// {matchLiteral("timestamp"), "integer"},
// SQLite doesn't have a "now()" method, replace with "date('now')"
{regexp.MustCompile(`\bnow\(\)`), "date('now')"},
},
}
// Incomplete.
flavorMySQL = flavor{
queryReplacers: []replacer{
{bindRegexp, "?"},
},
}
// Not tested.
flavorCockroach = flavor{
executeTx: crdb.ExecuteTx,
}
)
func (f flavor) translate(query string) string {
// TODO(ericchiang): Heavy cashing.
for _, r := range f.queryReplacers {
query = r.re.ReplaceAllString(query, r.with)
}
return query
}
// conn is the main database connection.
type conn struct {
db *sql.DB
flavor flavor
}
func (c *conn) Close() error {
return c.db.Close()
}
// conn implements the same method signatures as encoding/sql.DB.
func (c *conn) Exec(query string, args ...interface{}) (sql.Result, error) {
query = c.flavor.translate(query)
return c.db.Exec(query, args...)
}
func (c *conn) Query(query string, args ...interface{}) (*sql.Rows, error) {
query = c.flavor.translate(query)
return c.db.Query(query, args...)
}
func (c *conn) QueryRow(query string, args ...interface{}) *sql.Row {
query = c.flavor.translate(query)
return c.db.QueryRow(query, args...)
}
// ExecTx runs a method which operates on a transaction.
func (c *conn) ExecTx(fn func(tx *trans) error) error {
if c.flavor.executeTx != nil {
return c.flavor.executeTx(c.db, func(sqlTx *sql.Tx) error {
return fn(&trans{sqlTx, c})
})
}
sqlTx, err := c.db.Begin()
if err != nil {
return err
}
if err := fn(&trans{sqlTx, c}); err != nil {
sqlTx.Rollback()
return err
}
return sqlTx.Commit()
}
type trans struct {
tx *sql.Tx
c *conn
}
// trans implements the same method signatures as encoding/sql.Tx.
func (t *trans) Exec(query string, args ...interface{}) (sql.Result, error) {
query = t.c.flavor.translate(query)
return t.tx.Exec(query, args...)
}
func (t *trans) Query(query string, args ...interface{}) (*sql.Rows, error) {
query = t.c.flavor.translate(query)
return t.tx.Query(query, args...)
}
func (t *trans) QueryRow(query string, args ...interface{}) *sql.Row {
query = t.c.flavor.translate(query)
return t.tx.QueryRow(query, args...)
}
package sql
import "testing"
func TestTranslate(t *testing.T) {
tests := []struct {
testCase string
flavor flavor
query string
exp string
}{
{
"sqlite3 query bind replacement",
flavorSQLite3,
`select foo from bar where foo.zam = $1;`,
`select foo from bar where foo.zam = ?;`,
},
{
"sqlite3 query bind replacement at newline",
flavorSQLite3,
`select foo from bar where foo.zam = $1`,
`select foo from bar where foo.zam = ?`,
},
{
"sqlite3 query true",
flavorSQLite3,
`select foo from bar where foo.zam = true`,
`select foo from bar where foo.zam = 1`,
},
{
"sqlite3 query false",
flavorSQLite3,
`select foo from bar where foo.zam = false`,
`select foo from bar where foo.zam = 0`,
},
{
"sqlite3 bytea",
flavorSQLite3,
`"connector_data" bytea not null,`,
`"connector_data" blob not null,`,
},
{
"sqlite3 now",
flavorSQLite3,
`now(),`,
`date('now'),`,
},
}
for _, tc := range tests {
if got := tc.flavor.translate(tc.query); got != tc.exp {
t.Errorf("%s: want=%q, got=%q", tc.testCase, tc.exp, got)
}
}
}
#!/bin/bash
if [ "$EUID" -ne 0 ]
then echo "Please run as root"
exit
fi
function usage {
cat << EOF >> /dev/stderr
Usage: sudo ./standup.sh [create|destroy] [postgres|mysql|cockroach]
This is a script for standing up test databases. It uses systemd to daemonize
rkt containers running on a local loopback IP.
The general workflow is to create a daemonized container, use the output to set
the test environment variables, run the tests, then destroy the container.
sudo ./standup.sh create postgres
# Copy environment variables and run tests.
go test -v -i # always install test dependencies
go test -v
sudo ./standup.sh destroy postgres
EOF
exit 2
}
function main {
if [ "$#" -ne 2 ]; then
usage
exit 2
fi
case "$1" in
"create")
case "$2" in
"postgres")
create_postgres;;
"mysql")
create_mysql;;
*)
usage
exit 2
;;
esac
;;
"destroy")
case "$2" in
"postgres")
destroy_postgres;;
"mysql")
destroy_mysql;;
*)
usage
exit 2
;;
esac
;;
*)
usage
exit 2
;;
esac
}
function wait_for_file {
while [ ! -f $1 ]; do
sleep 1
done
}
function wait_for_container {
while [ -z "$( rkt list --full | grep $1 )" ]; do
sleep 1
done
}
function create_postgres {
UUID_FILE=/tmp/dex-postgres-uuid
if [ -f $UUID_FILE ]; then
echo "postgres database already exists, try ./standup.sh destroy postgres"
exit 2
fi
echo "Starting postgres. To view progress run:"
echo ""
echo " journalctl -fu dex-postgres"
echo ""
systemd-run --unit=dex-postgres \
rkt run --uuid-file-save=$UUID_FILE --insecure-options=image docker://postgres:9.6
wait_for_file $UUID_FILE
UUID=$( cat $UUID_FILE )
wait_for_container $UUID
HOST=$( rkt list --full | grep "$UUID" | awk '{ print $NF }' | sed -e 's/default:ip4=//g' )
echo "To run tests export the following environment variables:"
echo ""
echo " export DEX_POSTGRES_DATABASE=postgres; export DEX_POSTGRES_USER=postgres; export DEX_POSTGRES_PASSWORD=postgres; export DEX_POSTGRES_HOST=$HOST:5432"
echo ""
}
function destroy_postgres {
UUID_FILE=/tmp/dex-postgres-uuid
systemctl stop dex-postgres
rkt rm --uuid-file=$UUID_FILE
rm $UUID_FILE
}
main $@
......@@ -70,28 +70,41 @@ type Storage interface {
DeleteRefresh(id string) error
// Update functions are assumed to be a performed within a single object transaction.
//
// updaters may be called multiple times.
UpdateClient(id string, updater func(old Client) (Client, error)) error
UpdateKeys(updater func(old Keys) (Keys, error)) error
UpdateAuthRequest(id string, updater func(a AuthRequest) (AuthRequest, error)) error
// TODO(ericchiang): Add a GarbageCollect(now time.Time) method so conformance tests
// can test implementations.
}
// Client is an OAuth2 client.
// Client represents an OAuth2 client.
//
// For further reading see:
// * Trusted peers: https://developers.google.com/identity/protocols/CrossClientAuth
// * Public clients: https://developers.google.com/api-client-library/python/auth/installed-app
type Client struct {
ID string `json:"id" yaml:"id"`
Secret string `json:"secret" yaml:"secret"`
// Client ID and secret used to identify the client.
ID string `json:"id" yaml:"id"`
Secret string `json:"secret" yaml:"secret"`
// A registered set of redirect URIs. When redirecting from dex to the client, the URI
// requested to redirect to MUST match one of these values, unless the client is "public".
RedirectURIs []string `json:"redirectURIs" yaml:"redirectURIs"`
// TrustedPeers are a list of peers which can issue tokens on this client's behalf.
// TrustedPeers are a list of peers which can issue tokens on this client's behalf using
// the dynamic "oauth2:server:client_id:(client_id)" scope. If a peer makes such a request,
// this client's ID will appear as the ID Token's audience.
//
// Clients inherently trust themselves.
TrustedPeers []string `json:"trustedPeers" yaml:"trustedPeers"`
// Public clients must use either use a redirectURL 127.0.0.1:X or "urn:ietf:wg:oauth:2.0:oob"
Public bool `json:"public" yaml:"public"`
// Name and LogoURL used when displaying this client to the end user.
Name string `json:"name" yaml:"name"`
LogoURL string `json:"logoURL" yaml:"logoURL"`
}
......@@ -109,53 +122,79 @@ type Claims struct {
// AuthRequest represents a OAuth2 client authorization request. It holds the state
// of a single auth flow up to the point that the user authorizes the client.
type AuthRequest struct {
ID string
// ID used to identify the authorization request.
ID string
// ID of the client requesting authorization from a user.
ClientID string
// Values parsed from the initial request. These describe the resources the client is
// requesting as well as values describing the form of the response.
ResponseTypes []string
Scopes []string
RedirectURI string
Nonce string
State string
Nonce string
State string
// The client has indicated that the end user must be shown an approval prompt
// on all requests. The server cannot cache their initial action for subsequent
// attempts.
ForceApprovalPrompt bool
Expiry time.Time
// Has the user proved their identity through a backing identity provider?
//
// If false, the following fields are invalid.
LoggedIn bool
// The identity of the end user. Generally nil until the user authenticates
// with a backend.
Claims *Claims
Claims Claims
// The connector used to login the user and any data the connector wishes to persists.
// Set when the user authenticates.
ConnectorID string
ConnectorData []byte
Expiry time.Time
}
// AuthCode represents a code which can be exchanged for an OAuth2 token response.
//
// This value is created once an end user has authorized a client, the server has
// redirect the end user back to the client, but the client hasn't exchanged the
// code for an access_token and id_token.
type AuthCode struct {
// Actual string returned as the "code" value.
ID string
ClientID string
RedirectURI string
// The client this code value is valid for. When exchanging the code for a
// token response, the client must use its client_secret to authenticate.
ClientID string
ConnectorID string
ConnectorData []byte
// As part of the OAuth2 spec when a client makes a token request it MUST
// present the same redirect_uri as the initial redirect. This values is saved
// to make this check.
//
// https://tools.ietf.org/html/rfc6749#section-4.1.3
RedirectURI string
// If provided by the client in the initial request, the provider MUST create
// a ID Token with this nonce in the JWT payload.
Nonce string
// Scopes authorized by the end user for the client.
Scopes []string
Claims Claims
// Authentication data provided by an upstream source.
ConnectorID string
ConnectorData []byte
Claims Claims
Expiry time.Time
}
// RefreshToken is an OAuth2 refresh token.
// RefreshToken is an OAuth2 refresh token which allows a client to request new
// tokens on the end user's behalf.
type RefreshToken struct {
// The actual refresh token.
RefreshToken string
......@@ -163,17 +202,19 @@ type RefreshToken struct {
// Client this refresh token is valid for.
ClientID string
// Authentication data provided by an upstream source.
ConnectorID string
ConnectorData []byte
Claims Claims
// Scopes present in the initial request. Refresh requests may specify a set
// of scopes different from the initial request when refreshing a token,
// however those scopes must be encompassed by this set.
Scopes []string
// Nonce value supplied during the initial redirect. This is required to be part
// of the claims of any future id_token generated by the client.
Nonce string
Claims Claims
}
// VerificationKey is a rotated signing key which can still be used to verify
......@@ -188,6 +229,7 @@ type Keys struct {
// Key for creating and verifying signatures. These may be nil.
SigningKey *jose.JSONWebKey
SigningKeyPub *jose.JSONWebKey
// Old signing keys which have been rotated but can still be used to validate
// existing signatures.
VerificationKeys []VerificationKey
......
// +build go1.7
// Package storagetest provides conformance tests for storage implementations.
package storagetest
import (
"reflect"
"testing"
"time"
"github.com/coreos/dex/storage"
)
var neverExpire = time.Now().Add(time.Hour * 24 * 365 * 100)
// RunTestSuite runs a set of conformance tests against a storage.
func RunTestSuite(t *testing.T, s storage.Storage) {
t.Run("UpdateAuthRequest", func(t *testing.T) { testUpdateAuthRequest(t, s) })
t.Run("CreateRefresh", func(t *testing.T) { testCreateRefresh(t, s) })
}
func testUpdateAuthRequest(t *testing.T, s storage.Storage) {
a := storage.AuthRequest{
ID: storage.NewID(),
ClientID: "foobar",
ResponseTypes: []string{"code"},
Scopes: []string{"openid", "email"},
RedirectURI: "https://localhost:80/callback",
Expiry: neverExpire,
}
identity := storage.Claims{Email: "foobar"}
if err := s.CreateAuthRequest(a); err != nil {
t.Fatalf("failed creating auth request: %v", err)
}
if err := s.UpdateAuthRequest(a.ID, func(old storage.AuthRequest) (storage.AuthRequest, error) {
old.Claims = &identity
old.ConnectorID = "connID"
return old, nil
}); err != nil {
t.Fatalf("failed to update auth request: %v", err)
}
got, err := s.GetAuthRequest(a.ID)
if err != nil {
t.Fatalf("failed to get auth req: %v", err)
}
if got.Claims == nil {
t.Fatalf("no identity in auth request")
}
if !reflect.DeepEqual(*got.Claims, identity) {
t.Fatalf("update failed, wanted identity=%#v got %#v", identity, *got.Claims)
}
}
func testCreateRefresh(t *testing.T, s storage.Storage) {
id := storage.NewID()
refresh := storage.RefreshToken{
RefreshToken: id,
ClientID: "client_id",
ConnectorID: "client_secret",
Scopes: []string{"openid", "email", "profile"},
}
if err := s.CreateRefresh(refresh); err != nil {
t.Fatalf("create refresh token: %v", err)
}
gotRefresh, err := s.GetRefresh(id)
if err != nil {
t.Fatalf("get refresh: %v", err)
}
if !reflect.DeepEqual(gotRefresh, refresh) {
t.Errorf("refresh returned did not match expected")
}
if err := s.DeleteRefresh(id); err != nil {
t.Fatalf("failed to delete refresh request: %v", err)
}
if _, err := s.GetRefresh(id); err != storage.ErrNotFound {
t.Errorf("after deleting refresh expected storage.ErrNotFound, got %v", err)
}
}
This diff is collapsed.
# Copyright 2016 The Cockroach Authors.
#
# 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. See the AUTHORS file
# for names of contributors.
#
# Author: Spencer Kimball (spencer.kimball@gmail.com)
#
# Cockroach build rules.
GO ?= go
# Allow setting of go build flags from the command line.
GOFLAGS :=
.PHONY: all
all: test check
.PHONY: test
test:
$(GO) test -v -i ./...
$(GO) test -v ./...
.PHONY: deps
deps:
$(GO) get -d -t ./...
.PHONY: check
check:
@echo "checking for \"path\" imports"
@! git grep -F '"path"' -- '*.go'
@echo "errcheck"
@errcheck ./...
@echo "vet"
@! go tool vet . 2>&1 | \
grep -vE '^vet: cannot process directory .git'
@echo "vet --shadow"
@! go tool vet --shadow . 2>&1 | \
grep -vE '(declaration of err shadows|^vet: cannot process directory \.git)'
@echo "golint"
@! golint ./... | grep -vE '(\.pb\.go)'
@echo "varcheck"
@varcheck -e ./...
@echo "gofmt (simplify)"
@! gofmt -s -d -l . 2>&1 | grep -vE '^\.git/'
@echo "goimports"
@! goimports -l . | grep -vF 'No Exceptions'
# testing
Testing helpers for cockroach clients.
machine:
environment:
GOROOT: ${HOME}/go
PATH: ${PATH}:${HOME}/go/bin
post:
- sudo rm -rf /usr/local/go
- if [ ! -e go1.6.linux-amd64.tar.gz ]; then curl -O https://storage.googleapis.com/golang/go1.6.linux-amd64.tar.gz; fi
- tar -C ${HOME} -xzf go1.6.linux-amd64.tar.gz
dependencies:
override:
- make deps
cache_directories:
- ~/go1.6.linux-amd64.tar.gz
test:
override:
- make test
\ No newline at end of file
// Copyright 2016 The Cockroach Authors.
//
// 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.
//
// Author: Andrei Matei (andrei@cockroachlabs.com)
// Package crdb provides helpers for using CockroachDB in client
// applications.
package crdb
import (
"database/sql"
"github.com/lib/pq"
)
// AmbiguousCommitError represents an error that left a transaction in an
// ambiguous state: unclear if it committed or not.
type AmbiguousCommitError struct {
error
}
// ExecuteTx runs fn inside a transaction and retries it as needed.
// On non-retryable failures, the transaction is aborted and rolled
// back; on success, the transaction is committed.
// There are cases where the state of a transaction is inherently ambiguous: if
// we err on RELEASE with a communication error it's unclear if the transaction
// has been committed or not (similar to erroring on COMMIT in other databases).
// In that case, we return AmbiguousCommitError.
//
// For more information about CockroachDB's transaction model see
// https://cockroachlabs.com/docs/transactions.html.
//
// NOTE: the supplied exec closure should not have external side
// effects beyond changes to the database.
func ExecuteTx(db *sql.DB, fn func(*sql.Tx) error) (err error) {
// Start a transaction.
var tx *sql.Tx
tx, err = db.Begin()
if err != nil {
return err
}
defer func() {
if err == nil {
// Ignore commit errors. The tx has already been committed by RELEASE.
_ = tx.Commit()
} else {
// We always need to execute a Rollback() so sql.DB releases the
// connection.
_ = tx.Rollback()
}
}()
// Specify that we intend to retry this txn in case of CockroachDB retryable
// errors.
if _, err = tx.Exec("SAVEPOINT cockroach_restart"); err != nil {
return err
}
for {
released := false
err = fn(tx)
if err == nil {
// RELEASE acts like COMMIT in CockroachDB. We use it since it gives us an
// opportunity to react to retryable errors, whereas tx.Commit() doesn't.
released = true
if _, err = tx.Exec("RELEASE SAVEPOINT cockroach_restart"); err == nil {
return nil
}
}
// We got an error; let's see if it's a retryable one and, if so, restart. We look
// for either the standard PG errcode SerializationFailureError:40001 or the Cockroach extension
// errcode RetriableError:CR000. The Cockroach extension has been removed server-side, but support
// for it has been left here for now to maintain backwards compatibility.
pqErr, ok := err.(*pq.Error)
if retryable := ok && (pqErr.Code == "CR000" || pqErr.Code == "40001"); !retryable {
if released {
err = &AmbiguousCommitError{err}
}
return err
}
if _, err = tx.Exec("ROLLBACK TO SAVEPOINT cockroach_restart"); err != nil {
return err
}
}
}
// Copyright 2016 The Cockroach Authors.
//
// 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.
//
// Author: Spencer Kimball (spencer@cockroachlabs.com)
package crdb
import (
"database/sql"
"fmt"
"sync"
"testing"
"github.com/cockroachdb/cockroach-go/testserver"
)
// TestExecuteTx verifies transaction retry using the classic
// example of write skew in bank account balance transfers.
func TestExecuteTx(t *testing.T) {
db, stop := testserver.NewDBForTest(t)
defer stop()
initStmt := `
CREATE DATABASE d;
CREATE TABLE d.t (acct INT PRIMARY KEY, balance INT);
INSERT INTO d.t (acct, balance) VALUES (1, 100), (2, 100);
`
if _, err := db.Exec(initStmt); err != nil {
t.Fatal(err)
}
type queryI interface {
Query(string, ...interface{}) (*sql.Rows, error)
}
getBalances := func(q queryI) (bal1, bal2 int, err error) {
var rows *sql.Rows
rows, err = q.Query(`SELECT balance FROM d.t WHERE acct IN (1, 2);`)
if err != nil {
return
}
defer rows.Close()
balances := []*int{&bal1, &bal2}
i := 0
for ; rows.Next(); i += 1 {
if err = rows.Scan(balances[i]); err != nil {
return
}
}
if i != 2 {
err = fmt.Errorf("expected two balances; got %d", i)
return
}
return
}
runTxn := func(wg *sync.WaitGroup, iter *int) <-chan error {
errCh := make(chan error, 1)
go func() {
*iter = 0
errCh <- ExecuteTx(db, func(tx *sql.Tx) error {
*iter++
bal1, bal2, err := getBalances(tx)
if err != nil {
return err
}
// If this is the first iteration, wait for the other tx to also read.
if *iter == 1 {
wg.Done()
wg.Wait()
}
// Now, subtract from one account and give to the other.
if bal1 > bal2 {
if _, err := tx.Exec(`
UPDATE d.t SET balance=balance-100 WHERE acct=1;
UPDATE d.t SET balance=balance+100 WHERE acct=2;
`); err != nil {
return err
}
} else {
if _, err := tx.Exec(`
UPDATE d.t SET balance=balance+100 WHERE acct=1;
UPDATE d.t SET balance=balance-100 WHERE acct=2;
`); err != nil {
return err
}
}
return nil
})
}()
return errCh
}
var wg sync.WaitGroup
wg.Add(2)
var iters1, iters2 int
txn1Err := runTxn(&wg, &iters1)
txn2Err := runTxn(&wg, &iters2)
if err := <-txn1Err; err != nil {
t.Errorf("expected success in txn1; got %s", err)
}
if err := <-txn2Err; err != nil {
t.Errorf("expected success in txn2; got %s", err)
}
if iters1+iters2 <= 2 {
t.Errorf("expected at least one retry between the competing transactions; "+
"got txn1=%d, txn2=%d", iters1, iters2)
}
bal1, bal2, err := getBalances(db)
if err != nil || bal1 != 100 || bal2 != 100 {
t.Errorf("expected balances to be restored without error; "+
"got acct1=%d, acct2=%d: %s", bal1, bal2, err)
}
}
package testserver
import (
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"time"
)
const (
awsBaseURL = "https://s3.amazonaws.com/cockroach/cockroach"
latestSuffix = "LATEST"
localBinaryPath = "/var/tmp"
finishedFileMode = 0555
)
func binaryName() string {
return fmt.Sprintf("cockroach.%s-%s", runtime.GOOS, runtime.GOARCH)
}
func binaryNameWithSha(sha string) string {
return fmt.Sprintf("%s.%s", binaryName(), sha)
}
func binaryPath(sha string) string {
return filepath.Join(localBinaryPath, binaryNameWithSha(sha))
}
func latestMarkerURL() string {
return fmt.Sprintf("%s/%s.%s", awsBaseURL, binaryName(), latestSuffix)
}
func binaryURL(sha string) string {
return fmt.Sprintf("%s/%s.%s", awsBaseURL, binaryName(), sha)
}
func findLatestSha() (string, error) {
markerURL := latestMarkerURL()
marker, err := http.Get(markerURL)
if err != nil {
return "", fmt.Errorf("could not download %s: %s", markerURL)
}
if marker.StatusCode == 404 {
return "", fmt.Errorf("for 404 from GET %s: make sure OS and ARCH are supported",
markerURL)
} else if marker.StatusCode != 200 {
return "", fmt.Errorf("bad response got GET %s: %d (%s)",
markerURL, marker.StatusCode, marker.Status)
}
defer marker.Body.Close()
body, err := ioutil.ReadAll(marker.Body)
if err != nil {
return "", err
}
return strings.TrimSpace(string(body)), nil
}
func downloadFile(url, filePath string) error {
output, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0200)
if err != nil {
return fmt.Errorf("error creating %s: %s", filePath, "-", err)
}
defer output.Close()
log.Printf("downloading %s to %s, this may take some time", url, filePath)
response, err := http.Get(url)
if err != nil {
return fmt.Errorf("error downloading %s: %s", url, err)
}
defer response.Body.Close()
if response.StatusCode != 200 {
return fmt.Errorf("error downloading %s: %d (%s)", url, response.StatusCode, response.Status)
}
_, err = io.Copy(output, response.Body)
if err != nil {
return fmt.Errorf("problem downloading %s to %s: %s", url, filePath, err)
}
// Download was successful, add the rw bits.
return os.Chmod(filePath, finishedFileMode)
}
func downloadLatestBinary() (string, error) {
sha, err := findLatestSha()
if err != nil {
return "", err
}
localFile := binaryPath(sha)
for {
finfo, err := os.Stat(localFile)
if err != nil {
// File does not exist: download it.
break
}
// File already present: check mode.
if finfo.Mode().Perm() == finishedFileMode {
return localFile, nil
}
time.Sleep(time.Millisecond * 10)
}
err = downloadFile(binaryURL(sha), localFile)
if err != nil {
_ = os.Remove(localFile)
return "", err
}
return localFile, nil
}
// Copyright 2016 The Cockroach Authors.
//
// 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.
//
// Author: Marc Berhault (marc@cockroachlabs.com)
package testserver_test
import (
"testing"
// Needed for postgres driver test.
"github.com/cockroachdb/cockroach-go/testserver"
_ "github.com/lib/pq"
)
func TestRunServer(t *testing.T) {
db, stop := testserver.NewDBForTest(t)
defer stop()
_, err := db.Exec("SELECT 1")
if err != nil {
t.Fatal(err)
}
}
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
Icon?
ehthumbs.db
Thumbs.db
sudo: false
language: go
go:
- 1.2
- 1.3
- 1.4
- 1.5
- 1.6
- tip
before_script:
- mysql -e 'create database gotest;'
# This is the official list of Go-MySQL-Driver authors for copyright purposes.
# If you are submitting a patch, please add your name or the name of the
# organization which holds the copyright to this list in alphabetical order.
# Names should be added to this file as
# Name <email address>
# The email address is not required for organizations.
# Please keep the list sorted.
# Individual Persons
Aaron Hopkins <go-sql-driver at die.net>
Arne Hormann <arnehormann at gmail.com>
Carlos Nieto <jose.carlos at menteslibres.net>
Chris Moos <chris at tech9computers.com>
Daniel Nichter <nil at codenode.com>
Daniël van Eeden <git at myname.nl>
DisposaBoy <disposaboy at dby.me>
Frederick Mayle <frederickmayle at gmail.com>
Gustavo Kristic <gkristic at gmail.com>
Hanno Braun <mail at hannobraun.com>
Henri Yandell <flamefew at gmail.com>
Hirotaka Yamamoto <ymmt2005 at gmail.com>
INADA Naoki <songofacandy at gmail.com>
James Harr <james.harr at gmail.com>
Jian Zhen <zhenjl at gmail.com>
Joshua Prunier <joshua.prunier at gmail.com>
Julien Lefevre <julien.lefevr at gmail.com>
Julien Schmidt <go-sql-driver at julienschmidt.com>
Kamil Dziedzic <kamil at klecza.pl>
Kevin Malachowski <kevin at chowski.com>
Leonardo YongUk Kim <dalinaum at gmail.com>
Luca Looz <luca.looz92 at gmail.com>
Lucas Liu <extrafliu at gmail.com>
Luke Scott <luke at webconnex.com>
Michael Woolnough <michael.woolnough at gmail.com>
Nicola Peduzzi <thenikso at gmail.com>
Paul Bonser <misterpib at gmail.com>
Runrioter Wung <runrioter at gmail.com>
Soroush Pour <me at soroushjp.com>
Stan Putrya <root.vagner at gmail.com>
Stanley Gunawan <gunawan.stanley at gmail.com>
Xiaobing Jiang <s7v7nislands at gmail.com>
Xiuming Chen <cc at cxm.cc>
# Organizations
Barracuda Networks, Inc.
Google Inc.
Stripe Inc.
## HEAD
Changes:
- Go 1.1 is no longer supported
- Use decimals field from MySQL to format time types (#249)
- Buffer optimizations (#269)
- TLS ServerName defaults to the host (#283)
Bugfixes:
- Enable microsecond resolution on TIME, DATETIME and TIMESTAMP (#249)
- Fixed handling of queries without columns and rows (#255)
- Fixed a panic when SetKeepAlive() failed (#298)
- Support receiving ERR packet while reading rows (#321)
- Fixed reading NULL length-encoded integers in MySQL 5.6+ (#349)
- Fixed absolute paths support in LOAD LOCAL DATA INFILE (#356)
- Actually zero out bytes in handshake response (#378)
- Fixed race condition in registering LOAD DATA INFILE handler (#383)
- Fixed tests with MySQL 5.7.9+ (#380)
- QueryUnescape TLS config names (#397)
- Fixed "broken pipe" error by writing to closed socket (#390)
New Features:
- Support for returning table alias on Columns() (#289, #359, #382)
- Placeholder interpolation, can be actived with the DSN parameter `interpolateParams=true` (#309, #318)
- Support for uint64 parameters with high bit set (#332, #345)
- Cleartext authentication plugin support (#327)
## Version 1.2 (2014-06-03)
Changes:
- We switched back to a "rolling release". `go get` installs the current master branch again
- Version v1 of the driver will not be maintained anymore. Go 1.0 is no longer supported by this driver
- Exported errors to allow easy checking from application code
- Enabled TCP Keepalives on TCP connections
- Optimized INFILE handling (better buffer size calculation, lazy init, ...)
- The DSN parser also checks for a missing separating slash
- Faster binary date / datetime to string formatting
- Also exported the MySQLWarning type
- mysqlConn.Close returns the first error encountered instead of ignoring all errors
- writePacket() automatically writes the packet size to the header
- readPacket() uses an iterative approach instead of the recursive approach to merge splitted packets
New Features:
- `RegisterDial` allows the usage of a custom dial function to establish the network connection
- Setting the connection collation is possible with the `collation` DSN parameter. This parameter should be preferred over the `charset` parameter
- Logging of critical errors is configurable with `SetLogger`
- Google CloudSQL support
Bugfixes:
- Allow more than 32 parameters in prepared statements
- Various old_password fixes
- Fixed TestConcurrent test to pass Go's race detection
- Fixed appendLengthEncodedInteger for large numbers
- Renamed readLengthEnodedString to readLengthEncodedString and skipLengthEnodedString to skipLengthEncodedString (fixed typo)
## Version 1.1 (2013-11-02)
Changes:
- Go-MySQL-Driver now requires Go 1.1
- Connections now use the collation `utf8_general_ci` by default. Adding `&charset=UTF8` to the DSN should not be necessary anymore
- Made closing rows and connections error tolerant. This allows for example deferring rows.Close() without checking for errors
- `[]byte(nil)` is now treated as a NULL value. Before, it was treated like an empty string / `[]byte("")`
- DSN parameter values must now be url.QueryEscape'ed. This allows text values to contain special characters, such as '&'.
- Use the IO buffer also for writing. This results in zero allocations (by the driver) for most queries
- Optimized the buffer for reading
- stmt.Query now caches column metadata
- New Logo
- Changed the copyright header to include all contributors
- Improved the LOAD INFILE documentation
- The driver struct is now exported to make the driver directly accessible
- Refactored the driver tests
- Added more benchmarks and moved all to a separate file
- Other small refactoring
New Features:
- Added *old_passwords* support: Required in some cases, but must be enabled by adding `allowOldPasswords=true` to the DSN since it is insecure
- Added a `clientFoundRows` parameter: Return the number of matching rows instead of the number of rows changed on UPDATEs
- Added TLS/SSL support: Use a TLS/SSL encrypted connection to the server. Custom TLS configs can be registered and used
Bugfixes:
- Fixed MySQL 4.1 support: MySQL 4.1 sends packets with lengths which differ from the specification
- Convert to DB timezone when inserting `time.Time`
- Splitted packets (more than 16MB) are now merged correctly
- Fixed false positive `io.EOF` errors when the data was fully read
- Avoid panics on reuse of closed connections
- Fixed empty string producing false nil values
- Fixed sign byte for positive TIME fields
## Version 1.0 (2013-05-14)
Initial Release
# Contributing Guidelines
## Reporting Issues
Before creating a new Issue, please check first if a similar Issue [already exists](https://github.com/go-sql-driver/mysql/issues?state=open) or was [recently closed](https://github.com/go-sql-driver/mysql/issues?direction=desc&page=1&sort=updated&state=closed).
## Contributing Code
By contributing to this project, you share your code under the Mozilla Public License 2, as specified in the LICENSE file.
Don't forget to add yourself to the AUTHORS file.
### Code Review
Everyone is invited to review and comment on pull requests.
If it looks fine to you, comment with "LGTM" (Looks good to me).
If changes are required, notice the reviewers with "PTAL" (Please take another look) after committing the fixes.
Before merging the Pull Request, at least one [team member](https://github.com/go-sql-driver?tab=members) must have commented with "LGTM".
## Development Ideas
If you are looking for ideas for code contributions, please check our [Development Ideas](https://github.com/go-sql-driver/mysql/wiki/Development-Ideas) Wiki page.
### Issue description
Tell us what should happen and what happens instead
### Example code
```go
If possible, please enter some example code here to reproduce the issue.
```
### Error log
```
If you have an error log, please paste it here.
```
### Configuration
*Driver version (or git SHA):*
*Go version:* run `go version` in your console
*Server version:* E.g. MySQL 5.6, MariaDB 10.0.20
*Server OS:* E.g. Debian 8.1 (Jessie), Windows 10
This diff is collapsed.
### Description
Please explain the changes you made here.
### Checklist
- [ ] Code compiles correctly
- [ ] Created tests which fail without the change (if possible)
- [ ] All tests passing
- [ ] Extended the README / documentation, if necessary
- [ ] Added myself / the copyright holder to the AUTHORS file
This diff is collapsed.
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
// +build appengine
package mysql
import (
"appengine/cloudsql"
)
func init() {
RegisterDial("cloudsql", cloudsql.Dial)
}
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package mysql
import (
"bytes"
"database/sql"
"database/sql/driver"
"math"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
)
type TB testing.B
func (tb *TB) check(err error) {
if err != nil {
tb.Fatal(err)
}
}
func (tb *TB) checkDB(db *sql.DB, err error) *sql.DB {
tb.check(err)
return db
}
func (tb *TB) checkRows(rows *sql.Rows, err error) *sql.Rows {
tb.check(err)
return rows
}
func (tb *TB) checkStmt(stmt *sql.Stmt, err error) *sql.Stmt {
tb.check(err)
return stmt
}
func initDB(b *testing.B, queries ...string) *sql.DB {
tb := (*TB)(b)
db := tb.checkDB(sql.Open("mysql", dsn))
for _, query := range queries {
if _, err := db.Exec(query); err != nil {
if w, ok := err.(MySQLWarnings); ok {
b.Logf("warning on %q: %v", query, w)
} else {
b.Fatalf("error on %q: %v", query, err)
}
}
}
return db
}
const concurrencyLevel = 10
func BenchmarkQuery(b *testing.B) {
tb := (*TB)(b)
b.StopTimer()
b.ReportAllocs()
db := initDB(b,
"DROP TABLE IF EXISTS foo",
"CREATE TABLE foo (id INT PRIMARY KEY, val CHAR(50))",
`INSERT INTO foo VALUES (1, "one")`,
`INSERT INTO foo VALUES (2, "two")`,
)
db.SetMaxIdleConns(concurrencyLevel)
defer db.Close()
stmt := tb.checkStmt(db.Prepare("SELECT val FROM foo WHERE id=?"))
defer stmt.Close()
remain := int64(b.N)
var wg sync.WaitGroup
wg.Add(concurrencyLevel)
defer wg.Wait()
b.StartTimer()
for i := 0; i < concurrencyLevel; i++ {
go func() {
for {
if atomic.AddInt64(&remain, -1) < 0 {
wg.Done()
return
}
var got string
tb.check(stmt.QueryRow(1).Scan(&got))
if got != "one" {
b.Errorf("query = %q; want one", got)
wg.Done()
return
}
}
}()
}
}
func BenchmarkExec(b *testing.B) {
tb := (*TB)(b)
b.StopTimer()
b.ReportAllocs()
db := tb.checkDB(sql.Open("mysql", dsn))
db.SetMaxIdleConns(concurrencyLevel)
defer db.Close()
stmt := tb.checkStmt(db.Prepare("DO 1"))
defer stmt.Close()
remain := int64(b.N)
var wg sync.WaitGroup
wg.Add(concurrencyLevel)
defer wg.Wait()
b.StartTimer()
for i := 0; i < concurrencyLevel; i++ {
go func() {
for {
if atomic.AddInt64(&remain, -1) < 0 {
wg.Done()
return
}
if _, err := stmt.Exec(); err != nil {
b.Fatal(err.Error())
}
}
}()
}
}
// data, but no db writes
var roundtripSample []byte
func initRoundtripBenchmarks() ([]byte, int, int) {
if roundtripSample == nil {
roundtripSample = []byte(strings.Repeat("0123456789abcdef", 1024*1024))
}
return roundtripSample, 16, len(roundtripSample)
}
func BenchmarkRoundtripTxt(b *testing.B) {
b.StopTimer()
sample, min, max := initRoundtripBenchmarks()
sampleString := string(sample)
b.ReportAllocs()
tb := (*TB)(b)
db := tb.checkDB(sql.Open("mysql", dsn))
defer db.Close()
b.StartTimer()
var result string
for i := 0; i < b.N; i++ {
length := min + i
if length > max {
length = max
}
test := sampleString[0:length]
rows := tb.checkRows(db.Query(`SELECT "` + test + `"`))
if !rows.Next() {
rows.Close()
b.Fatalf("crashed")
}
err := rows.Scan(&result)
if err != nil {
rows.Close()
b.Fatalf("crashed")
}
if result != test {
rows.Close()
b.Errorf("mismatch")
}
rows.Close()
}
}
func BenchmarkRoundtripBin(b *testing.B) {
b.StopTimer()
sample, min, max := initRoundtripBenchmarks()
b.ReportAllocs()
tb := (*TB)(b)
db := tb.checkDB(sql.Open("mysql", dsn))
defer db.Close()
stmt := tb.checkStmt(db.Prepare("SELECT ?"))
defer stmt.Close()
b.StartTimer()
var result sql.RawBytes
for i := 0; i < b.N; i++ {
length := min + i
if length > max {
length = max
}
test := sample[0:length]
rows := tb.checkRows(stmt.Query(test))
if !rows.Next() {
rows.Close()
b.Fatalf("crashed")
}
err := rows.Scan(&result)
if err != nil {
rows.Close()
b.Fatalf("crashed")
}
if !bytes.Equal(result, test) {
rows.Close()
b.Errorf("mismatch")
}
rows.Close()
}
}
func BenchmarkInterpolation(b *testing.B) {
mc := &mysqlConn{
cfg: &Config{
InterpolateParams: true,
Loc: time.UTC,
},
maxPacketAllowed: maxPacketSize,
maxWriteSize: maxPacketSize - 1,
buf: newBuffer(nil),
}
args := []driver.Value{
int64(42424242),
float64(math.Pi),
false,
time.Unix(1423411542, 807015000),
[]byte("bytes containing special chars ' \" \a \x00"),
"string containing special chars ' \" \a \x00",
}
q := "SELECT ?, ?, ?, ?, ?, ?"
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := mc.interpolateParams(q, args)
if err != nil {
b.Fatal(err)
}
}
}
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package mysql
import (
"io"
"net"
"time"
)
const defaultBufSize = 4096
// A buffer which is used for both reading and writing.
// This is possible since communication on each connection is synchronous.
// In other words, we can't write and read simultaneously on the same connection.
// The buffer is similar to bufio.Reader / Writer but zero-copy-ish
// Also highly optimized for this particular use case.
type buffer struct {
buf []byte
nc net.Conn
idx int
length int
timeout time.Duration
}
func newBuffer(nc net.Conn) buffer {
var b [defaultBufSize]byte
return buffer{
buf: b[:],
nc: nc,
}
}
// fill reads into the buffer until at least _need_ bytes are in it
func (b *buffer) fill(need int) error {
n := b.length
// move existing data to the beginning
if n > 0 && b.idx > 0 {
copy(b.buf[0:n], b.buf[b.idx:])
}
// grow buffer if necessary
// TODO: let the buffer shrink again at some point
// Maybe keep the org buf slice and swap back?
if need > len(b.buf) {
// Round up to the next multiple of the default size
newBuf := make([]byte, ((need/defaultBufSize)+1)*defaultBufSize)
copy(newBuf, b.buf)
b.buf = newBuf
}
b.idx = 0
for {
if b.timeout > 0 {
if err := b.nc.SetReadDeadline(time.Now().Add(b.timeout)); err != nil {
return err
}
}
nn, err := b.nc.Read(b.buf[n:])
n += nn
switch err {
case nil:
if n < need {
continue
}
b.length = n
return nil
case io.EOF:
if n >= need {
b.length = n
return nil
}
return io.ErrUnexpectedEOF
default:
return err
}
}
}
// returns next N bytes from buffer.
// The returned slice is only guaranteed to be valid until the next read
func (b *buffer) readNext(need int) ([]byte, error) {
if b.length < need {
// refill
if err := b.fill(need); err != nil {
return nil, err
}
}
offset := b.idx
b.idx += need
b.length -= need
return b.buf[offset:b.idx], nil
}
// returns a buffer with the requested size.
// If possible, a slice from the existing buffer is returned.
// Otherwise a bigger buffer is made.
// Only one buffer (total) can be used at a time.
func (b *buffer) takeBuffer(length int) []byte {
if b.length > 0 {
return nil
}
// test (cheap) general case first
if length <= defaultBufSize || length <= cap(b.buf) {
return b.buf[:length]
}
if length < maxPacketSize {
b.buf = make([]byte, length)
return b.buf
}
return make([]byte, length)
}
// shortcut which can be used if the requested buffer is guaranteed to be
// smaller than defaultBufSize
// Only one buffer (total) can be used at a time.
func (b *buffer) takeSmallBuffer(length int) []byte {
if b.length == 0 {
return b.buf[:length]
}
return nil
}
// takeCompleteBuffer returns the complete existing buffer.
// This can be used if the necessary buffer size is unknown.
// Only one buffer (total) can be used at a time.
func (b *buffer) takeCompleteBuffer() []byte {
if b.length == 0 {
return b.buf
}
return nil
}
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2014 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package mysql
const defaultCollation = "utf8_general_ci"
// A list of available collations mapped to the internal ID.
// To update this map use the following MySQL query:
// SELECT COLLATION_NAME, ID FROM information_schema.COLLATIONS
var collations = map[string]byte{
"big5_chinese_ci": 1,
"latin2_czech_cs": 2,
"dec8_swedish_ci": 3,
"cp850_general_ci": 4,
"latin1_german1_ci": 5,
"hp8_english_ci": 6,
"koi8r_general_ci": 7,
"latin1_swedish_ci": 8,
"latin2_general_ci": 9,
"swe7_swedish_ci": 10,
"ascii_general_ci": 11,
"ujis_japanese_ci": 12,
"sjis_japanese_ci": 13,
"cp1251_bulgarian_ci": 14,
"latin1_danish_ci": 15,
"hebrew_general_ci": 16,
"tis620_thai_ci": 18,
"euckr_korean_ci": 19,
"latin7_estonian_cs": 20,
"latin2_hungarian_ci": 21,
"koi8u_general_ci": 22,
"cp1251_ukrainian_ci": 23,
"gb2312_chinese_ci": 24,
"greek_general_ci": 25,
"cp1250_general_ci": 26,
"latin2_croatian_ci": 27,
"gbk_chinese_ci": 28,
"cp1257_lithuanian_ci": 29,
"latin5_turkish_ci": 30,
"latin1_german2_ci": 31,
"armscii8_general_ci": 32,
"utf8_general_ci": 33,
"cp1250_czech_cs": 34,
"ucs2_general_ci": 35,
"cp866_general_ci": 36,
"keybcs2_general_ci": 37,
"macce_general_ci": 38,
"macroman_general_ci": 39,
"cp852_general_ci": 40,
"latin7_general_ci": 41,
"latin7_general_cs": 42,
"macce_bin": 43,
"cp1250_croatian_ci": 44,
"utf8mb4_general_ci": 45,
"utf8mb4_bin": 46,
"latin1_bin": 47,
"latin1_general_ci": 48,
"latin1_general_cs": 49,
"cp1251_bin": 50,
"cp1251_general_ci": 51,
"cp1251_general_cs": 52,
"macroman_bin": 53,
"utf16_general_ci": 54,
"utf16_bin": 55,
"utf16le_general_ci": 56,
"cp1256_general_ci": 57,
"cp1257_bin": 58,
"cp1257_general_ci": 59,
"utf32_general_ci": 60,
"utf32_bin": 61,
"utf16le_bin": 62,
"binary": 63,
"armscii8_bin": 64,
"ascii_bin": 65,
"cp1250_bin": 66,
"cp1256_bin": 67,
"cp866_bin": 68,
"dec8_bin": 69,
"greek_bin": 70,
"hebrew_bin": 71,
"hp8_bin": 72,
"keybcs2_bin": 73,
"koi8r_bin": 74,
"koi8u_bin": 75,
"latin2_bin": 77,
"latin5_bin": 78,
"latin7_bin": 79,
"cp850_bin": 80,
"cp852_bin": 81,
"swe7_bin": 82,
"utf8_bin": 83,
"big5_bin": 84,
"euckr_bin": 85,
"gb2312_bin": 86,
"gbk_bin": 87,
"sjis_bin": 88,
"tis620_bin": 89,
"ucs2_bin": 90,
"ujis_bin": 91,
"geostd8_general_ci": 92,
"geostd8_bin": 93,
"latin1_spanish_ci": 94,
"cp932_japanese_ci": 95,
"cp932_bin": 96,
"eucjpms_japanese_ci": 97,
"eucjpms_bin": 98,
"cp1250_polish_ci": 99,
"utf16_unicode_ci": 101,
"utf16_icelandic_ci": 102,
"utf16_latvian_ci": 103,
"utf16_romanian_ci": 104,
"utf16_slovenian_ci": 105,
"utf16_polish_ci": 106,
"utf16_estonian_ci": 107,
"utf16_spanish_ci": 108,
"utf16_swedish_ci": 109,
"utf16_turkish_ci": 110,
"utf16_czech_ci": 111,
"utf16_danish_ci": 112,
"utf16_lithuanian_ci": 113,
"utf16_slovak_ci": 114,
"utf16_spanish2_ci": 115,
"utf16_roman_ci": 116,
"utf16_persian_ci": 117,
"utf16_esperanto_ci": 118,
"utf16_hungarian_ci": 119,
"utf16_sinhala_ci": 120,
"utf16_german2_ci": 121,
"utf16_croatian_ci": 122,
"utf16_unicode_520_ci": 123,
"utf16_vietnamese_ci": 124,
"ucs2_unicode_ci": 128,
"ucs2_icelandic_ci": 129,
"ucs2_latvian_ci": 130,
"ucs2_romanian_ci": 131,
"ucs2_slovenian_ci": 132,
"ucs2_polish_ci": 133,
"ucs2_estonian_ci": 134,
"ucs2_spanish_ci": 135,
"ucs2_swedish_ci": 136,
"ucs2_turkish_ci": 137,
"ucs2_czech_ci": 138,
"ucs2_danish_ci": 139,
"ucs2_lithuanian_ci": 140,
"ucs2_slovak_ci": 141,
"ucs2_spanish2_ci": 142,
"ucs2_roman_ci": 143,
"ucs2_persian_ci": 144,
"ucs2_esperanto_ci": 145,
"ucs2_hungarian_ci": 146,
"ucs2_sinhala_ci": 147,
"ucs2_german2_ci": 148,
"ucs2_croatian_ci": 149,
"ucs2_unicode_520_ci": 150,
"ucs2_vietnamese_ci": 151,
"ucs2_general_mysql500_ci": 159,
"utf32_unicode_ci": 160,
"utf32_icelandic_ci": 161,
"utf32_latvian_ci": 162,
"utf32_romanian_ci": 163,
"utf32_slovenian_ci": 164,
"utf32_polish_ci": 165,
"utf32_estonian_ci": 166,
"utf32_spanish_ci": 167,
"utf32_swedish_ci": 168,
"utf32_turkish_ci": 169,
"utf32_czech_ci": 170,
"utf32_danish_ci": 171,
"utf32_lithuanian_ci": 172,
"utf32_slovak_ci": 173,
"utf32_spanish2_ci": 174,
"utf32_roman_ci": 175,
"utf32_persian_ci": 176,
"utf32_esperanto_ci": 177,
"utf32_hungarian_ci": 178,
"utf32_sinhala_ci": 179,
"utf32_german2_ci": 180,
"utf32_croatian_ci": 181,
"utf32_unicode_520_ci": 182,
"utf32_vietnamese_ci": 183,
"utf8_unicode_ci": 192,
"utf8_icelandic_ci": 193,
"utf8_latvian_ci": 194,
"utf8_romanian_ci": 195,
"utf8_slovenian_ci": 196,
"utf8_polish_ci": 197,
"utf8_estonian_ci": 198,
"utf8_spanish_ci": 199,
"utf8_swedish_ci": 200,
"utf8_turkish_ci": 201,
"utf8_czech_ci": 202,
"utf8_danish_ci": 203,
"utf8_lithuanian_ci": 204,
"utf8_slovak_ci": 205,
"utf8_spanish2_ci": 206,
"utf8_roman_ci": 207,
"utf8_persian_ci": 208,
"utf8_esperanto_ci": 209,
"utf8_hungarian_ci": 210,
"utf8_sinhala_ci": 211,
"utf8_german2_ci": 212,
"utf8_croatian_ci": 213,
"utf8_unicode_520_ci": 214,
"utf8_vietnamese_ci": 215,
"utf8_general_mysql500_ci": 223,
"utf8mb4_unicode_ci": 224,
"utf8mb4_icelandic_ci": 225,
"utf8mb4_latvian_ci": 226,
"utf8mb4_romanian_ci": 227,
"utf8mb4_slovenian_ci": 228,
"utf8mb4_polish_ci": 229,
"utf8mb4_estonian_ci": 230,
"utf8mb4_spanish_ci": 231,
"utf8mb4_swedish_ci": 232,
"utf8mb4_turkish_ci": 233,
"utf8mb4_czech_ci": 234,
"utf8mb4_danish_ci": 235,
"utf8mb4_lithuanian_ci": 236,
"utf8mb4_slovak_ci": 237,
"utf8mb4_spanish2_ci": 238,
"utf8mb4_roman_ci": 239,
"utf8mb4_persian_ci": 240,
"utf8mb4_esperanto_ci": 241,
"utf8mb4_hungarian_ci": 242,
"utf8mb4_sinhala_ci": 243,
"utf8mb4_german2_ci": 244,
"utf8mb4_croatian_ci": 245,
"utf8mb4_unicode_520_ci": 246,
"utf8mb4_vietnamese_ci": 247,
}
// A blacklist of collations which is unsafe to interpolate parameters.
// These multibyte encodings may contains 0x5c (`\`) in their trailing bytes.
var unsafeCollations = map[string]bool{
"big5_chinese_ci": true,
"sjis_japanese_ci": true,
"gbk_chinese_ci": true,
"big5_bin": true,
"gb2312_bin": true,
"gbk_bin": true,
"sjis_bin": true,
"cp932_japanese_ci": true,
"cp932_bin": true,
}
This diff is collapsed.
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2012 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package mysql
const (
minProtocolVersion byte = 10
maxPacketSize = 1<<24 - 1
timeFormat = "2006-01-02 15:04:05.999999"
)
// MySQL constants documentation:
// http://dev.mysql.com/doc/internals/en/client-server-protocol.html
const (
iOK byte = 0x00
iLocalInFile byte = 0xfb
iEOF byte = 0xfe
iERR byte = 0xff
)
// https://dev.mysql.com/doc/internals/en/capability-flags.html#packet-Protocol::CapabilityFlags
type clientFlag uint32
const (
clientLongPassword clientFlag = 1 << iota
clientFoundRows
clientLongFlag
clientConnectWithDB
clientNoSchema
clientCompress
clientODBC
clientLocalFiles
clientIgnoreSpace
clientProtocol41
clientInteractive
clientSSL
clientIgnoreSIGPIPE
clientTransactions
clientReserved
clientSecureConn
clientMultiStatements
clientMultiResults
clientPSMultiResults
clientPluginAuth
clientConnectAttrs
clientPluginAuthLenEncClientData
clientCanHandleExpiredPasswords
clientSessionTrack
clientDeprecateEOF
)
const (
comQuit byte = iota + 1
comInitDB
comQuery
comFieldList
comCreateDB
comDropDB
comRefresh
comShutdown
comStatistics
comProcessInfo
comConnect
comProcessKill
comDebug
comPing
comTime
comDelayedInsert
comChangeUser
comBinlogDump
comTableDump
comConnectOut
comRegisterSlave
comStmtPrepare
comStmtExecute
comStmtSendLongData
comStmtClose
comStmtReset
comSetOption
comStmtFetch
)
// https://dev.mysql.com/doc/internals/en/com-query-response.html#packet-Protocol::ColumnType
const (
fieldTypeDecimal byte = iota
fieldTypeTiny
fieldTypeShort
fieldTypeLong
fieldTypeFloat
fieldTypeDouble
fieldTypeNULL
fieldTypeTimestamp
fieldTypeLongLong
fieldTypeInt24
fieldTypeDate
fieldTypeTime
fieldTypeDateTime
fieldTypeYear
fieldTypeNewDate
fieldTypeVarChar
fieldTypeBit
)
const (
fieldTypeJSON byte = iota + 0xf5
fieldTypeNewDecimal
fieldTypeEnum
fieldTypeSet
fieldTypeTinyBLOB
fieldTypeMediumBLOB
fieldTypeLongBLOB
fieldTypeBLOB
fieldTypeVarString
fieldTypeString
fieldTypeGeometry
)
type fieldFlag uint16
const (
flagNotNULL fieldFlag = 1 << iota
flagPriKey
flagUniqueKey
flagMultipleKey
flagBLOB
flagUnsigned
flagZeroFill
flagBinary
flagEnum
flagAutoIncrement
flagTimestamp
flagSet
flagUnknown1
flagUnknown2
flagUnknown3
flagUnknown4
)
// http://dev.mysql.com/doc/internals/en/status-flags.html
type statusFlag uint16
const (
statusInTrans statusFlag = 1 << iota
statusInAutocommit
statusReserved // Not in documentation
statusMoreResultsExists
statusNoGoodIndexUsed
statusNoIndexUsed
statusCursorExists
statusLastRowSent
statusDbDropped
statusNoBackslashEscapes
statusMetadataChanged
statusQueryWasSlow
statusPsOutParams
statusInTransReadonly
statusSessionStateChanged
)
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Copyright (c) 2011-2013, 'pq' Contributors
Portions Copyright (C) 2011 Blake Mizerany
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment