diff --git a/src/core/api/admin_job.go b/src/core/api/admin_job.go index e982a5c09..43e663d2e 100644 --- a/src/core/api/admin_job.go +++ b/src/core/api/admin_job.go @@ -226,7 +226,7 @@ func (aj *AJAPI) getLog(id int64) { // submit submits a job to job service per request func (aj *AJAPI) submit(ajr *models.AdminJobReq) { // when the schedule is saved as None without any schedule, just return 200 and do nothing. - if ajr.Schedule.Type == models.ScheduleNone { + if ajr.Schedule == nil || ajr.Schedule.Type == models.ScheduleNone { return } diff --git a/src/core/api/reg_gc_test.go b/src/core/api/reg_gc_test.go index f9a81601f..2726629b4 100644 --- a/src/core/api/reg_gc_test.go +++ b/src/core/api/reg_gc_test.go @@ -20,7 +20,7 @@ func TestGCPost(t *testing.T) { t.Error("Error occurred while add a admin job", err.Error()) t.Log(err) } else { - assert.Equal(200, code, "Add adminjob status should be 200") + assert.Equal(201, code, "Add adminjob status should be 201") } } diff --git a/src/core/api/retention_test.go b/src/core/api/retention_test.go index e81632dcf..8cc9097b4 100644 --- a/src/core/api/retention_test.go +++ b/src/core/api/retention_test.go @@ -1,16 +1,33 @@ +// Copyright Project Harbor 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. + package api import ( "encoding/json" "fmt" - "github.com/goharbor/harbor/src/pkg/retention/dao" - "github.com/goharbor/harbor/src/pkg/retention/dao/models" - "github.com/goharbor/harbor/src/pkg/retention/policy" - "github.com/goharbor/harbor/src/pkg/retention/policy/rule" - "github.com/stretchr/testify/require" "net/http" "testing" "time" + + "github.com/goharbor/harbor/src/pkg/retention/dao" + "github.com/goharbor/harbor/src/pkg/retention/dao/models" + "github.com/goharbor/harbor/src/pkg/retention/mocks" + "github.com/goharbor/harbor/src/pkg/retention/policy" + "github.com/goharbor/harbor/src/pkg/retention/policy/rule" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" ) func TestGetMetadatas(t *testing.T) { @@ -30,6 +47,16 @@ func TestGetMetadatas(t *testing.T) { } func TestCreatePolicy(t *testing.T) { + // mock retention api controller + mockController := &mocks.APIController{} + mockController.On("CreateRetention", mock.AnythingOfType("*policy.Metadata")).Return(int64(1), nil) + + controller := retentionController + retentionController = mockController + defer func() { + retentionController = controller + }() + p1 := &policy.Metadata{ Algorithm: "or", Rules: []rule.Metadata{ @@ -87,7 +114,7 @@ func TestCreatePolicy(t *testing.T) { bodyJSON: p1, credential: sysAdmin, }, - code: http.StatusOK, + code: http.StatusCreated, }, { request: &testingRequest{ @@ -267,6 +294,23 @@ func TestPolicy(t *testing.T) { require.Nil(t, err) require.True(t, id > 0) + // mock retention api controller + mockController := &mocks.APIController{} + mockController.On("GetRetention", mock.AnythingOfType("int64")).Return(p, nil) + mockController.On("UpdateRetention", mock.AnythingOfType("*policy.Metadata")).Return(nil) + mockController.On("TriggerRetentionExec", + mock.AnythingOfType("int64"), + mock.AnythingOfType("string"), + mock.AnythingOfType("bool")).Return(int64(1), nil) + mockController.On("ListRetentionExecs", mock.AnythingOfType("int64"), mock.AnythingOfType("*q.Query")).Return(nil, nil) + mockController.On("GetTotalOfRetentionExecs", mock.AnythingOfType("int64")).Return(int64(0), nil) + + controller := retentionController + retentionController = mockController + defer func() { + retentionController = controller + }() + cases := []*codeCheckingCase{ { request: &testingRequest{ @@ -408,7 +452,7 @@ func TestPolicy(t *testing.T) { }, credential: sysAdmin, }, - code: http.StatusOK, + code: http.StatusCreated, }, { request: &testingRequest{ diff --git a/src/core/api/scan_all_test.go b/src/core/api/scan_all_test.go index 9f20e2ad4..604ec0fe2 100644 --- a/src/core/api/scan_all_test.go +++ b/src/core/api/scan_all_test.go @@ -1,3 +1,17 @@ +// Copyright Project Harbor 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. + package api import ( @@ -56,7 +70,7 @@ func (suite *ScanAllAPITestSuite) TestScanAllPost() { // case 1: add a new scan all job code, err := apiTest.AddScanAll(*admin, adminJob002) require.NoError(suite.T(), err, "Error occurred while add a scan all job") - suite.Equal(200, code, "Add scan all status should be 200") + suite.Equal(201, code, "Add scan all status should be 200") } func (suite *ScanAllAPITestSuite) TestScanAllGet() { diff --git a/src/core/controllers/controllers_test.go b/src/core/controllers/controllers_test.go index 15af159bb..0e94e5406 100644 --- a/src/core/controllers/controllers_test.go +++ b/src/core/controllers/controllers_test.go @@ -15,23 +15,21 @@ package controllers import ( "context" - "github.com/goharbor/harbor/src/core/filter" + "fmt" "net/http" "net/http/httptest" - // "net/url" + "os" "path/filepath" "runtime" - "testing" - - "fmt" - "os" "strings" + "testing" "github.com/astaxie/beego" "github.com/goharbor/harbor/src/common" "github.com/goharbor/harbor/src/common/models" utilstest "github.com/goharbor/harbor/src/common/utils/test" "github.com/goharbor/harbor/src/core/config" + "github.com/goharbor/harbor/src/core/filter" "github.com/goharbor/harbor/src/core/middlewares" "github.com/stretchr/testify/assert" ) @@ -41,6 +39,7 @@ func init() { dir := filepath.Dir(file) dir = filepath.Join(dir, "..") apppath, _ := filepath.Abs(dir) + beego.BConfig.WebConfig.EnableXSRF = true beego.BConfig.WebConfig.Session.SessionOn = true beego.TestBeegoInit(apppath) beego.AddTemplateExt("htm") @@ -106,9 +105,6 @@ func TestAll(t *testing.T) { err := middlewares.Init() assert.Nil(err) - // Has to set to dev so that the xsrf panic can be rendered as 403 - beego.BConfig.RunMode = beego.DEV - r, _ := http.NewRequest("POST", "/c/login", nil) w := httptest.NewRecorder() beego.BeeApp.Handlers.ServeHTTP(w, r) diff --git a/src/core/filter/orm.go b/src/core/filter/orm.go deleted file mode 100644 index ba034d877..000000000 --- a/src/core/filter/orm.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright Project Harbor 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. - -package filter - -import ( - "github.com/astaxie/beego/context" - o "github.com/astaxie/beego/orm" - "github.com/goharbor/harbor/src/internal/orm" -) - -// OrmFilter set orm.Ormer instance to the context of the http.Request -func OrmFilter(ctx *context.Context) { - if ctx == nil || ctx.Request == nil { - return - } - // This is a temp workaround for beego bug: https://github.com/goharbor/harbor/issues/10446 - // After we upgrading beego to the latest version and moving the filter to middleware, - // this workaround can be removed - *(ctx.Request) = *(ctx.Request.WithContext(orm.NewContext(ctx.Request.Context(), o.NewOrm()))) -} diff --git a/src/core/main.go b/src/core/main.go index 50a3a34e8..88caf7b13 100755 --- a/src/core/main.go +++ b/src/core/main.go @@ -17,9 +17,11 @@ package main import ( "encoding/gob" "fmt" + "net/http" "os" "os/signal" "strconv" + "strings" "syscall" "time" @@ -53,6 +55,8 @@ import ( "github.com/goharbor/harbor/src/pkg/types" "github.com/goharbor/harbor/src/pkg/version" "github.com/goharbor/harbor/src/replication" + "github.com/goharbor/harbor/src/server/middleware/orm" + "github.com/goharbor/harbor/src/server/middleware/requestid" ) const ( @@ -247,7 +251,6 @@ func main() { filter.Init() beego.InsertFilter("/api/*", beego.BeforeStatic, filter.SessionCheck) - beego.InsertFilter("/*", beego.BeforeRouter, filter.OrmFilter) beego.InsertFilter("/*", beego.BeforeRouter, filter.SecurityFilter) beego.InsertFilter("/*", beego.BeforeRouter, filter.ReadonlyFilter) @@ -288,6 +291,22 @@ func main() { } log.Infof("Version: %s, Git commit: %s", version.ReleaseVersion, version.GitCommit) - beego.Run() + + middlewares := []beego.MiddleWare{ + requestid.Middleware(), + orm.Middleware(legacyAPISkipper), + } + beego.RunWithMiddleWares("", middlewares...) } + +// legacyAPISkipper skip middleware for legacy APIs +func legacyAPISkipper(r *http.Request) bool { + for _, prefix := range []string{"/v2/", "/api/v2.0/"} { + if strings.HasPrefix(r.URL.Path, prefix) { + return false + } + } + + return true +} diff --git a/src/go.mod b/src/go.mod index e4ada2248..f5873c1c7 100644 --- a/src/go.mod +++ b/src/go.mod @@ -5,14 +5,13 @@ go 1.12 replace github.com/goharbor/harbor => ../ require ( - github.com/Knetic/govaluate v3.0.0+incompatible // indirect github.com/Masterminds/semver v1.4.2 github.com/Microsoft/go-winio v0.4.12 // indirect github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d // indirect github.com/Unknwon/goconfig v0.0.0-20160216183935-5f601ca6ef4d // indirect github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 // indirect github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190726115642-cd293c93fd97 - github.com/astaxie/beego v1.9.0 + github.com/astaxie/beego v1.12.0 github.com/aws/aws-sdk-go v1.19.47 github.com/beego/i18n v0.0.0-20140604031826-e87155e8f0c0 github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 // indirect @@ -71,6 +70,7 @@ require ( github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect github.com/prometheus/client_golang v0.9.4 // indirect github.com/robfig/cron v1.0.0 + github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 // indirect github.com/sirupsen/logrus v1.4.1 // indirect github.com/spf13/viper v1.4.0 // indirect github.com/stretchr/testify v1.4.0 diff --git a/src/go.sum b/src/go.sum index c9a5b037b..f5d938696 100644 --- a/src/go.sum +++ b/src/go.sum @@ -11,6 +11,7 @@ github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF0 github.com/Microsoft/go-winio v0.4.12 h1:xAfWHN1IrQ0NJ9TBC0KBZoqLjzDTr1ML+4MywiUOryc= github.com/Microsoft/go-winio v0.4.12/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/OwnLocal/goes v1.0.0/go.mod h1:8rIFjBGTue3lCU0wplczcUgt9Gxgrkkrw7etMIcn8TM= github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= @@ -34,13 +35,15 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5 github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/astaxie/beego v1.9.0 h1:tPzS+D1oCLi+SEb/TLNRNYpCjaMVfAGoy9OTLwS5ul4= -github.com/astaxie/beego v1.9.0/go.mod h1:0R4++1tUqERR0WYFWdfkcrsyoVBCG4DgpDGokT3yb+U= +github.com/astaxie/beego v1.12.0 h1:MRhVoeeye5N+Flul5PoVfD9CslfdoH+xqC/xvSQ5u2Y= +github.com/astaxie/beego v1.12.0/go.mod h1:fysx+LZNZKnvh4GED/xND7jWtjCR6HzydR2Hh2Im57o= github.com/aws/aws-sdk-go v1.19.47 h1:ZEze0mpk8Fttrsz6UNLqhH/jRGYbMPfWFA2ILas4AmM= github.com/aws/aws-sdk-go v1.19.47/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= +github.com/beego/goyaml2 v0.0.0-20130207012346-5545475820dd/go.mod h1:1b+Y/CofkYwXMUU0OhQqGvsY2Bvgr4j6jfT699wyZKQ= github.com/beego/i18n v0.0.0-20140604031826-e87155e8f0c0 h1:fQaDnUQvBXHHQdGBu9hz8nPznB4BeiPQokvmQVjmNEw= github.com/beego/i18n v0.0.0-20140604031826-e87155e8f0c0/go.mod h1:KLeFCpAMq2+50NkXC8iiJxLLiiTfTqrGtKEVm+2fk7s= +github.com/beego/x2j v0.0.0-20131220205130-a0352aadc542/go.mod h1:kSeGC/p1AbBiEp5kat81+DSQrZenVBZXklMLaELspWU= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -52,6 +55,7 @@ github.com/bmatcuk/doublestar v1.1.1 h1:YroD6BJCZBYx06yYFEWvUuKVWQn3vLLQAVmDmvTS github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60= github.com/bugsnag/bugsnag-go v1.5.2 h1:fdaGJJEReigPzSE6HajOhpJwE2IEP/TdHDHXKGeOJtc= github.com/bugsnag/bugsnag-go v1.5.2/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= github.com/bugsnag/panicwrap v1.2.0 h1:OzrKrRvXis8qEvOkfcxNcYbOd2O7xXS2nnKMEMABFQA= @@ -64,6 +68,7 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/cfssl v0.0.0-20190510060611-9c027c93ba9e h1:ZtyhUG4s94BMUCdgvRZySr/AXYL5CDcjxhIV/83xJog= github.com/cloudflare/cfssl v0.0.0-20190510060611-9c027c93ba9e/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= +github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-oidc v2.0.0+incompatible h1:+RStIopZ8wooMx+Vs5Bt8zMXxV1ABl5LbakNExNmZIg= @@ -71,6 +76,10 @@ github.com/coreos/go-oidc v2.0.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHo github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/couchbase/go-couchbase v0.0.0-20181122212707-3e9b6e1258bb/go.mod h1:TWI8EKQMs5u5jLKW/tsb9VwauIrMIxQG1r5fMsswK5U= +github.com/couchbase/gomemcached v0.0.0-20181122193126-5125a94a666c/go.mod h1:srVSlQLB8iXBVXHgnqemxUXqN6FCvClgCMPCsjBDR7c= +github.com/couchbase/goutils v0.0.0-20180530154633-e865a1461c8a/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs= +github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76/go.mod h1:vYwsqCOLxGiisLwp9rITslkFNpZD5rz43tf41QFkTWY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -99,6 +108,9 @@ github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNE github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk= +github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= @@ -163,6 +175,7 @@ github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+ github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= github.com/go-openapi/validate v0.19.3 h1:PAH/2DylwWcIU1s0Y7k3yNmeAgWOcKrNE2Q7Ww/kCg4= github.com/go-openapi/validate v0.19.3/go.mod h1:90Vh6jjkTn+OT1Eefm0ZixWNFjhtOH7vS9k0lo6zwJo= +github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= @@ -262,6 +275,7 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0 h1:/5u4a+KGJptBRqGzPvYQL9p0d/tPR4S31+Tnzj9lEO4= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= @@ -336,6 +350,11 @@ github.com/robfig/cron v1.0.0 h1:slmQxIUH6U9ruw4XoJ7C2pyyx4yYeiHx8S9pNootHsM= github.com/robfig/cron v1.0.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 h1:X+yvsM2yrEktyI+b2qND5gpH8YhURn0k8OCaeRnkINo= +github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg= +github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw= +github.com/siddontang/ledisdb v0.0.0-20181029004158-becf5f38d373/go.mod h1:mF1DpOSOUiJRMR+FDqaqu3EBqrybQtrDDszLUZ6oxPg= +github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z92TR1JKMkLLoaOQk++LVnOKL3ScbJ8GNGA= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= @@ -355,6 +374,7 @@ github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec/go.mod h1:QBvMkMya+gXctz3kmljlUCu/yB3GZ6oee+dUozsezQE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -365,12 +385,14 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0= github.com/theupdateframework/notary v0.6.1 h1:7wshjstgS9x9F5LuB1L5mBI2xNMObWqjz+cjWoom6l0= github.com/theupdateframework/notary v0.6.1/go.mod h1:MOfgIfmox8s7/7fduvB2xyPPMJCrjRLRizA8OFwpnKY= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/wendal/errors v0.0.0-20130201093226-f66c77a7882b/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= @@ -382,6 +404,7 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI= diff --git a/src/pkg/retention/controller.go b/src/pkg/retention/controller.go index c18206de7..57d1411c6 100644 --- a/src/pkg/retention/controller.go +++ b/src/pkg/retention/controller.go @@ -25,6 +25,8 @@ import ( "github.com/goharbor/harbor/src/pkg/scheduler" ) +// go:generate mockery -name APIController -case snake + // APIController to handle the requests related with retention type APIController interface { GetRetention(id int64) (*policy.Metadata, error) diff --git a/src/pkg/retention/mocks/api_controller.go b/src/pkg/retention/mocks/api_controller.go new file mode 100644 index 000000000..9c0ec3942 --- /dev/null +++ b/src/pkg/retention/mocks/api_controller.go @@ -0,0 +1,257 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import ( + policy "github.com/goharbor/harbor/src/pkg/retention/policy" + q "github.com/goharbor/harbor/src/pkg/retention/q" + mock "github.com/stretchr/testify/mock" + + retention "github.com/goharbor/harbor/src/pkg/retention" +) + +// APIController is an autogenerated mock type for the APIController type +type APIController struct { + mock.Mock +} + +// CreateRetention provides a mock function with given fields: p +func (_m *APIController) CreateRetention(p *policy.Metadata) (int64, error) { + ret := _m.Called(p) + + var r0 int64 + if rf, ok := ret.Get(0).(func(*policy.Metadata) int64); ok { + r0 = rf(p) + } else { + r0 = ret.Get(0).(int64) + } + + var r1 error + if rf, ok := ret.Get(1).(func(*policy.Metadata) error); ok { + r1 = rf(p) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DeleteRetention provides a mock function with given fields: id +func (_m *APIController) DeleteRetention(id int64) error { + ret := _m.Called(id) + + var r0 error + if rf, ok := ret.Get(0).(func(int64) error); ok { + r0 = rf(id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetRetention provides a mock function with given fields: id +func (_m *APIController) GetRetention(id int64) (*policy.Metadata, error) { + ret := _m.Called(id) + + var r0 *policy.Metadata + if rf, ok := ret.Get(0).(func(int64) *policy.Metadata); ok { + r0 = rf(id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*policy.Metadata) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(int64) error); ok { + r1 = rf(id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetRetentionExec provides a mock function with given fields: eid +func (_m *APIController) GetRetentionExec(eid int64) (*retention.Execution, error) { + ret := _m.Called(eid) + + var r0 *retention.Execution + if rf, ok := ret.Get(0).(func(int64) *retention.Execution); ok { + r0 = rf(eid) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*retention.Execution) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(int64) error); ok { + r1 = rf(eid) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetRetentionExecTaskLog provides a mock function with given fields: taskID +func (_m *APIController) GetRetentionExecTaskLog(taskID int64) ([]byte, error) { + ret := _m.Called(taskID) + + var r0 []byte + if rf, ok := ret.Get(0).(func(int64) []byte); ok { + r0 = rf(taskID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(int64) error); ok { + r1 = rf(taskID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetTotalOfRetentionExecTasks provides a mock function with given fields: executionID +func (_m *APIController) GetTotalOfRetentionExecTasks(executionID int64) (int64, error) { + ret := _m.Called(executionID) + + var r0 int64 + if rf, ok := ret.Get(0).(func(int64) int64); ok { + r0 = rf(executionID) + } else { + r0 = ret.Get(0).(int64) + } + + var r1 error + if rf, ok := ret.Get(1).(func(int64) error); ok { + r1 = rf(executionID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetTotalOfRetentionExecs provides a mock function with given fields: policyID +func (_m *APIController) GetTotalOfRetentionExecs(policyID int64) (int64, error) { + ret := _m.Called(policyID) + + var r0 int64 + if rf, ok := ret.Get(0).(func(int64) int64); ok { + r0 = rf(policyID) + } else { + r0 = ret.Get(0).(int64) + } + + var r1 error + if rf, ok := ret.Get(1).(func(int64) error); ok { + r1 = rf(policyID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListRetentionExecTasks provides a mock function with given fields: executionID, query +func (_m *APIController) ListRetentionExecTasks(executionID int64, query *q.Query) ([]*retention.Task, error) { + ret := _m.Called(executionID, query) + + var r0 []*retention.Task + if rf, ok := ret.Get(0).(func(int64, *q.Query) []*retention.Task); ok { + r0 = rf(executionID, query) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*retention.Task) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(int64, *q.Query) error); ok { + r1 = rf(executionID, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListRetentionExecs provides a mock function with given fields: policyID, query +func (_m *APIController) ListRetentionExecs(policyID int64, query *q.Query) ([]*retention.Execution, error) { + ret := _m.Called(policyID, query) + + var r0 []*retention.Execution + if rf, ok := ret.Get(0).(func(int64, *q.Query) []*retention.Execution); ok { + r0 = rf(policyID, query) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*retention.Execution) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(int64, *q.Query) error); ok { + r1 = rf(policyID, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// OperateRetentionExec provides a mock function with given fields: eid, action +func (_m *APIController) OperateRetentionExec(eid int64, action string) error { + ret := _m.Called(eid, action) + + var r0 error + if rf, ok := ret.Get(0).(func(int64, string) error); ok { + r0 = rf(eid, action) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// TriggerRetentionExec provides a mock function with given fields: policyID, trigger, dryRun +func (_m *APIController) TriggerRetentionExec(policyID int64, trigger string, dryRun bool) (int64, error) { + ret := _m.Called(policyID, trigger, dryRun) + + var r0 int64 + if rf, ok := ret.Get(0).(func(int64, string, bool) int64); ok { + r0 = rf(policyID, trigger, dryRun) + } else { + r0 = ret.Get(0).(int64) + } + + var r1 error + if rf, ok := ret.Get(1).(func(int64, string, bool) error); ok { + r1 = rf(policyID, trigger, dryRun) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateRetention provides a mock function with given fields: p +func (_m *APIController) UpdateRetention(p *policy.Metadata) error { + ret := _m.Called(p) + + var r0 error + if rf, ok := ret.Get(0).(func(*policy.Metadata) error); ok { + r0 = rf(p) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/src/server/middleware/middleware.go b/src/server/middleware/middleware.go index fb8669082..eeae2a356 100644 --- a/src/server/middleware/middleware.go +++ b/src/server/middleware/middleware.go @@ -29,3 +29,23 @@ func WithMiddlewares(handler http.Handler, middlewares ...Middleware) http.Handl } return handler } + +// Skipper defines a function to skip middleware. +// Returning true skips processing the middleware. +type Skipper func(*http.Request) bool + +// New make a middleware from fn which type is func(w http.ResponseWriter, r *http.Request, next http.Handler) +func New(fn func(http.ResponseWriter, *http.Request, http.Handler), skippers ...Skipper) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + for _, skipper := range skippers { + if skipper(r) { + next.ServeHTTP(w, r) + return + } + } + + fn(w, r, next) + }) + } +} diff --git a/src/server/middleware/middleware_test.go b/src/server/middleware/middleware_test.go index 6f85ac653..413ecd189 100644 --- a/src/server/middleware/middleware_test.go +++ b/src/server/middleware/middleware_test.go @@ -15,10 +15,11 @@ package middleware import ( - "github.com/stretchr/testify/suite" "net/http" "net/http/httptest" "testing" + + "github.com/stretchr/testify/suite" ) type middlewareTestSuite struct { @@ -46,6 +47,32 @@ func (m *middlewareTestSuite) TestWithMiddlewares() { m.Equal("middleware1middleware2handler", record.Header().Get("key")) } +func (m *middlewareTestSuite) TestNew() { + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + f := func(w http.ResponseWriter, r *http.Request, next http.Handler) { + w.Header().Set("key", "value") + next.ServeHTTP(w, r) + } + + req1 := httptest.NewRequest(http.MethodGet, "/req", nil) + rec1 := httptest.NewRecorder() + New(f)(next).ServeHTTP(rec1, req1) + m.Equal("value", rec1.Header().Get("key")) + + req2 := httptest.NewRequest(http.MethodGet, "/req", nil) + rec2 := httptest.NewRecorder() + New(f, func(r *http.Request) bool { return r.URL.Path == "/req" })(next).ServeHTTP(rec2, req2) + m.Equal("", rec2.Header().Get("key")) + + req3 := httptest.NewRequest(http.MethodGet, "/req3", nil) + rec3 := httptest.NewRecorder() + New(f, func(r *http.Request) bool { return r.URL.Path == "/req" })(next).ServeHTTP(rec3, req3) + m.Equal("value", rec3.Header().Get("key")) +} + func TestMiddlewareTestSuite(t *testing.T) { suite.Run(t, &middlewareTestSuite{}) } diff --git a/src/server/middleware/orm/orm.go b/src/server/middleware/orm/orm.go new file mode 100644 index 000000000..76fb9e539 --- /dev/null +++ b/src/server/middleware/orm/orm.go @@ -0,0 +1,55 @@ +// Copyright Project Harbor 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. + +package orm + +import ( + "net/http" + + o "github.com/astaxie/beego/orm" + "github.com/goharbor/harbor/src/internal/orm" + "github.com/goharbor/harbor/src/server/middleware" +) + +// Config defines the config for Orm middleware. +type Config struct { + // Creator defines a function to create ormer + Creator func() o.Ormer +} + +var ( + // DefaultConfig default orm config + DefaultConfig = Config{ + Creator: func() o.Ormer { + return o.NewOrm() + }, + } +) + +// Middleware middleware which add ormer to the http request context with default config +func Middleware(skippers ...middleware.Skipper) func(http.Handler) http.Handler { + return MiddlewareWithConfig(DefaultConfig, skippers...) +} + +// MiddlewareWithConfig middleware which add ormer to the http request context with config +func MiddlewareWithConfig(config Config, skippers ...middleware.Skipper) func(http.Handler) http.Handler { + if config.Creator == nil { + config.Creator = DefaultConfig.Creator + } + + return middleware.New(func(w http.ResponseWriter, r *http.Request, next http.Handler) { + ctx := orm.NewContext(r.Context(), config.Creator()) + next.ServeHTTP(w, r.WithContext(ctx)) + }, skippers...) +} diff --git a/src/server/middleware/orm/orm_test.go b/src/server/middleware/orm/orm_test.go new file mode 100644 index 000000000..f452079ab --- /dev/null +++ b/src/server/middleware/orm/orm_test.go @@ -0,0 +1,53 @@ +// Copyright Project Harbor 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. + +package orm + +import ( + "net/http" + "net/http/httptest" + "testing" + + o "github.com/astaxie/beego/orm" + "github.com/goharbor/harbor/src/internal/orm" + "github.com/stretchr/testify/assert" +) + +type mockOrmer struct { + o.Ormer +} + +func TestOrm(t *testing.T) { + assert := assert.New(t) + + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := orm.FromContext(r.Context()) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + } else { + w.WriteHeader(http.StatusOK) + } + }) + + req1 := httptest.NewRequest(http.MethodGet, "/req1", nil) + rec1 := httptest.NewRecorder() + next.ServeHTTP(rec1, req1) + assert.Equal(http.StatusInternalServerError, rec1.Code) + + req2 := httptest.NewRequest(http.MethodGet, "/req2", nil) + rec2 := httptest.NewRecorder() + + MiddlewareWithConfig(Config{Creator: func() o.Ormer { return &mockOrmer{} }})(next).ServeHTTP(rec2, req2) + assert.Equal(http.StatusOK, rec2.Code) +} diff --git a/src/server/middleware/readonly.go b/src/server/middleware/readonly.go deleted file mode 100644 index b9ecd4c2e..000000000 --- a/src/server/middleware/readonly.go +++ /dev/null @@ -1,27 +0,0 @@ -package middleware - -import ( - "github.com/goharbor/harbor/src/common/utils/log" - "github.com/goharbor/harbor/src/core/config" - internal_errors "github.com/goharbor/harbor/src/internal/error" - "net/http" -) - -type readonlyHandler struct { - next http.Handler -} - -// ReadOnly middleware reject request when harbor set to readonly -func ReadOnly() func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - if config.ReadOnly() { - log.Warningf("The request is prohibited in readonly mode, url is: %s", req.URL.Path) - pkgE := internal_errors.New(nil).WithCode(internal_errors.DENIED).WithMessage("The system is in read only mode. Any modification is prohibited.") - http.Error(rw, internal_errors.NewErrs(pkgE).Error(), http.StatusForbidden) - return - } - next.ServeHTTP(rw, req) - }) - } -} diff --git a/src/server/middleware/readonly/readonly.go b/src/server/middleware/readonly/readonly.go new file mode 100644 index 000000000..07b65c1f2 --- /dev/null +++ b/src/server/middleware/readonly/readonly.go @@ -0,0 +1,78 @@ +// Copyright Project Harbor 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. + +package readonly + +import ( + "net/http" + + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/core/config" + internal_errors "github.com/goharbor/harbor/src/internal/error" + "github.com/goharbor/harbor/src/server/middleware" +) + +// Config defines the config for ReadOnly middleware. +type Config struct { + // ReadOnly defines a function to check whether is readonly mode for request + ReadOnly func(*http.Request) bool +} + +var ( + // DefaultConfig default readonly config + DefaultConfig = Config{ + ReadOnly: func(r *http.Request) bool { + return config.ReadOnly() + }, + } + + // See more for safe method at https://developer.mozilla.org/en-US/docs/Glossary/safe + safeMethods = map[string]bool{ + http.MethodGet: true, + http.MethodHead: true, + http.MethodOptions: true, + } +) + +// safeMethodSkipper returns false when the request method is safe methods +func safeMethodSkipper(r *http.Request) bool { + return safeMethods[r.Method] +} + +// Middleware middleware reject request when harbor set to readonly with default config +func Middleware(skippers ...middleware.Skipper) func(http.Handler) http.Handler { + return MiddlewareWithConfig(DefaultConfig, skippers...) +} + +// MiddlewareWithConfig middleware reject request when harbor set to readonly with config +func MiddlewareWithConfig(config Config, skippers ...middleware.Skipper) func(http.Handler) http.Handler { + if len(skippers) == 0 { + skippers = []middleware.Skipper{safeMethodSkipper} + } + + if config.ReadOnly == nil { + config.ReadOnly = DefaultConfig.ReadOnly + } + + return middleware.New(func(w http.ResponseWriter, r *http.Request, next http.Handler) { + if config.ReadOnly(r) { + log.Warningf("The request is prohibited in readonly mode, url is: %s", r.URL.Path) + pkgE := internal_errors.New(nil).WithCode(internal_errors.DENIED).WithMessage("The system is in read only mode. Any modification is prohibited.") + http.Error(w, internal_errors.NewErrs(pkgE).Error(), http.StatusForbidden) + return + } + + next.ServeHTTP(w, r) + }, skippers...) +} diff --git a/src/server/middleware/readonly/readonly_test.go b/src/server/middleware/readonly/readonly_test.go new file mode 100644 index 000000000..9d14f38ce --- /dev/null +++ b/src/server/middleware/readonly/readonly_test.go @@ -0,0 +1,55 @@ +// Copyright Project Harbor 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. + +package readonly + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReadOnly(t *testing.T) { + assert := assert.New(t) + + next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + readOnly := func(readOnly bool) Config { + return Config{ + ReadOnly: func(*http.Request) bool { + return readOnly + }, + } + } + + req1 := httptest.NewRequest(http.MethodDelete, "/resource", nil) + rec1 := httptest.NewRecorder() + MiddlewareWithConfig(readOnly(true))(next).ServeHTTP(rec1, req1) + assert.Equal(http.StatusForbidden, rec1.Code) + + req2 := httptest.NewRequest(http.MethodDelete, "/resource", nil) + rec2 := httptest.NewRecorder() + MiddlewareWithConfig(readOnly(false))(next).ServeHTTP(rec2, req2) + assert.Equal(http.StatusOK, rec2.Code) + + // safe method + req3 := httptest.NewRequest(http.MethodGet, "/resource", nil) + rec3 := httptest.NewRecorder() + MiddlewareWithConfig(readOnly(true))(next).ServeHTTP(rec3, req3) + assert.Equal(http.StatusOK, rec3.Code) +} diff --git a/src/server/middleware/readonly_test.go b/src/server/middleware/readonly_test.go deleted file mode 100644 index 96369ce5e..000000000 --- a/src/server/middleware/readonly_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package middleware - -import ( - "github.com/goharbor/harbor/src/common" - config2 "github.com/goharbor/harbor/src/common/config" - "github.com/goharbor/harbor/src/core/config" - "net/http" - "net/http/httptest" - "os" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestMain(m *testing.M) { - conf := map[string]interface{}{ - common.ReadOnly: "true", - } - kp := &config2.PresetKeyProvider{Key: "naa4JtarA1Zsc3uY"} - config.InitWithSettings(conf, kp) - result := m.Run() - if result != 0 { - os.Exit(result) - } -} - -func TestReadOnly(t *testing.T) { - assert := assert.New(t) - - next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) - }) - - // delete - req := httptest.NewRequest(http.MethodDelete, "/readonly1", nil) - rec := httptest.NewRecorder() - ReadOnly()(next).ServeHTTP(rec, req) - assert.Equal(rec.Code, http.StatusForbidden) - - update := map[string]interface{}{ - common.ReadOnly: "false", - } - config.GetCfgManager().UpdateConfig(update) - - req2 := httptest.NewRequest(http.MethodDelete, "/readonly2", nil) - rec2 := httptest.NewRecorder() - ReadOnly()(next).ServeHTTP(rec2, req2) - assert.Equal(rec2.Code, http.StatusOK) - -} diff --git a/src/server/middleware/request_id.go b/src/server/middleware/requestid/requestid.go similarity index 57% rename from src/server/middleware/request_id.go rename to src/server/middleware/requestid/requestid.go index b9bdd2734..a543f29bd 100644 --- a/src/server/middleware/request_id.go +++ b/src/server/middleware/requestid/requestid.go @@ -12,29 +12,28 @@ // See the License for the specific language governing permissions and // limitations under the License. -package middleware +package requestid import ( "net/http" + "github.com/goharbor/harbor/src/server/middleware" "github.com/google/uuid" ) // HeaderXRequestID X-Request-ID header const HeaderXRequestID = "X-Request-ID" -// RequestID middleware which add X-Request-ID header in the http request when not exist -func RequestID() func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - rid := r.Header.Get(HeaderXRequestID) - if rid == "" { - rid = uuid.New().String() - r.Header.Set(HeaderXRequestID, rid) - } +// Middleware middleware which add X-Request-ID header in the http request when not exist +func Middleware(skippers ...middleware.Skipper) func(http.Handler) http.Handler { + return middleware.New(func(w http.ResponseWriter, r *http.Request, next http.Handler) { + rid := r.Header.Get(HeaderXRequestID) + if rid == "" { + rid = uuid.New().String() + r.Header.Set(HeaderXRequestID, rid) + } - w.Header().Set(HeaderXRequestID, rid) - next.ServeHTTP(w, r) - }) - } + w.Header().Set(HeaderXRequestID, rid) + next.ServeHTTP(w, r) + }, skippers...) } diff --git a/src/server/middleware/request_id_test.go b/src/server/middleware/requestid/requestid_test.go similarity index 66% rename from src/server/middleware/request_id_test.go rename to src/server/middleware/requestid/requestid_test.go index 2cb0c996d..d6dedb31a 100644 --- a/src/server/middleware/request_id_test.go +++ b/src/server/middleware/requestid/requestid_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package middleware +package requestid import ( "net/http" @@ -26,22 +26,22 @@ func TestRequestID(t *testing.T) { assert := assert.New(t) next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) + w.WriteHeader(http.StatusOK) }) req1 := httptest.NewRequest(http.MethodGet, "/req1", nil) - rr1 := httptest.NewRecorder() - next.ServeHTTP(rr1, req1) - assert.Equal(rr1.Header().Get(HeaderXRequestID), "") + rec1 := httptest.NewRecorder() + next.ServeHTTP(rec1, req1) + assert.Equal("", rec1.Header().Get(HeaderXRequestID)) req2 := httptest.NewRequest(http.MethodGet, "/req2", nil) - rr2 := httptest.NewRecorder() - RequestID()(next).ServeHTTP(rr2, req2) - assert.NotEqual(rr2.Header().Get(HeaderXRequestID), "") + rec2 := httptest.NewRecorder() + Middleware()(next).ServeHTTP(rec2, req2) + assert.NotEqual("", rec2.Header().Get(HeaderXRequestID)) - req3 := httptest.NewRequest(http.MethodGet, "/req2", nil) + req3 := httptest.NewRequest(http.MethodGet, "/req3", nil) req3.Header.Add(HeaderXRequestID, "852803be-e5fe-499b-bbea-c9e5b5f43916") - rr3 := httptest.NewRecorder() - RequestID()(next).ServeHTTP(rr3, req3) - assert.Equal(rr3.Header().Get(HeaderXRequestID), "852803be-e5fe-499b-bbea-c9e5b5f43916") + rec3 := httptest.NewRecorder() + Middleware()(next).ServeHTTP(rec3, req3) + assert.Equal("852803be-e5fe-499b-bbea-c9e5b5f43916", rec3.Header().Get(HeaderXRequestID)) } diff --git a/src/server/registry/handler.go b/src/server/registry/handler.go index 31267f3dc..e3a8d6b56 100644 --- a/src/server/registry/handler.go +++ b/src/server/registry/handler.go @@ -15,17 +15,18 @@ package registry import ( + "net/http" + "net/http/httputil" + "net/url" + "github.com/goharbor/harbor/src/pkg/project" pkg_repo "github.com/goharbor/harbor/src/pkg/repository" pkg_tag "github.com/goharbor/harbor/src/pkg/tag" "github.com/goharbor/harbor/src/server/middleware" "github.com/goharbor/harbor/src/server/middleware/manifestinfo" + "github.com/goharbor/harbor/src/server/middleware/readonly" "github.com/goharbor/harbor/src/server/middleware/regtoken" "github.com/goharbor/harbor/src/server/registry/blob" - "net/http" - "net/http/httputil" - "net/url" - "github.com/goharbor/harbor/src/server/registry/catalog" "github.com/goharbor/harbor/src/server/registry/manifest" "github.com/goharbor/harbor/src/server/registry/tag" @@ -53,18 +54,18 @@ func New(url *url.URL) http.Handler { manifestRouter := rootRouter.Path("/v2/{name:.*}/manifests/{reference}").Subrouter() manifestRouter.NewRoute().Methods(http.MethodGet).Handler(middleware.WithMiddlewares(manifest.NewHandler(project.Mgr, proxy), manifestinfo.Middleware(), regtoken.Middleware())) manifestRouter.NewRoute().Methods(http.MethodHead).Handler(manifest.NewHandler(project.Mgr, proxy)) - manifestRouter.NewRoute().Methods(http.MethodPut).Handler(middleware.WithMiddlewares(manifest.NewHandler(project.Mgr, proxy), middleware.ReadOnly())) - manifestRouter.NewRoute().Methods(http.MethodDelete).Handler(middleware.WithMiddlewares(manifest.NewHandler(project.Mgr, proxy), middleware.ReadOnly())) + manifestRouter.NewRoute().Methods(http.MethodPut).Handler(middleware.WithMiddlewares(manifest.NewHandler(project.Mgr, proxy), readonly.Middleware())) + manifestRouter.NewRoute().Methods(http.MethodDelete).Handler(middleware.WithMiddlewares(manifest.NewHandler(project.Mgr, proxy), readonly.Middleware())) // handle blob // as we need to apply middleware to the blob requests, so create a sub router to handle the blob APIs blobRouter := rootRouter.PathPrefix("/v2/{name:.*}/blobs/").Subrouter() blobRouter.NewRoute().Methods(http.MethodGet).Handler(blob.NewHandler(proxy)) blobRouter.NewRoute().Methods(http.MethodHead).Handler(blob.NewHandler(proxy)) - blobRouter.NewRoute().Methods(http.MethodPost).Handler(middleware.WithMiddlewares(blob.NewHandler(proxy), middleware.ReadOnly())) - blobRouter.NewRoute().Methods(http.MethodPut).Handler(middleware.WithMiddlewares(blob.NewHandler(proxy), middleware.ReadOnly())) - blobRouter.NewRoute().Methods(http.MethodPatch).Handler(middleware.WithMiddlewares(blob.NewHandler(proxy), middleware.ReadOnly())) - blobRouter.NewRoute().Methods(http.MethodDelete).Handler(middleware.WithMiddlewares(blob.NewHandler(proxy), middleware.ReadOnly())) + blobRouter.NewRoute().Methods(http.MethodPost).Handler(middleware.WithMiddlewares(blob.NewHandler(proxy), readonly.Middleware())) + blobRouter.NewRoute().Methods(http.MethodPut).Handler(middleware.WithMiddlewares(blob.NewHandler(proxy), readonly.Middleware())) + blobRouter.NewRoute().Methods(http.MethodPatch).Handler(middleware.WithMiddlewares(blob.NewHandler(proxy), readonly.Middleware())) + blobRouter.NewRoute().Methods(http.MethodDelete).Handler(middleware.WithMiddlewares(blob.NewHandler(proxy), readonly.Middleware())) // all other APIs are proxy to the backend docker registry rootRouter.PathPrefix("/").Handler(proxy) diff --git a/src/vendor/github.com/astaxie/beego/.gosimpleignore b/src/vendor/github.com/astaxie/beego/.gosimpleignore deleted file mode 100644 index 84df9b95d..000000000 --- a/src/vendor/github.com/astaxie/beego/.gosimpleignore +++ /dev/null @@ -1,4 +0,0 @@ -github.com/astaxie/beego/*/*:S1012 -github.com/astaxie/beego/*:S1012 -github.com/astaxie/beego/*/*:S1007 -github.com/astaxie/beego/*:S1007 \ No newline at end of file diff --git a/src/vendor/github.com/astaxie/beego/.travis.yml b/src/vendor/github.com/astaxie/beego/.travis.yml index 2937e6e85..1bb121a21 100644 --- a/src/vendor/github.com/astaxie/beego/.travis.yml +++ b/src/vendor/github.com/astaxie/beego/.travis.yml @@ -1,19 +1,26 @@ language: go go: - - 1.6.4 - - 1.7.5 - - 1.8.1 + - "1.11.x" services: - redis-server - mysql - postgresql - memcached env: - - ORM_DRIVER=sqlite3 ORM_SOURCE=$TRAVIS_BUILD_DIR/orm_test.db - - ORM_DRIVER=mysql ORM_SOURCE="root:@/orm_test?charset=utf8" - - ORM_DRIVER=postgres ORM_SOURCE="user=postgres dbname=orm_test sslmode=disable" + global: + - GO_REPO_FULLNAME="github.com/astaxie/beego" + matrix: + - ORM_DRIVER=sqlite3 ORM_SOURCE=$TRAVIS_BUILD_DIR/orm_test.db + - ORM_DRIVER=postgres ORM_SOURCE="user=postgres dbname=orm_test sslmode=disable" before_install: + # link the local repo with ${GOPATH}/src// + - GO_REPO_NAMESPACE=${GO_REPO_FULLNAME%/*} + # relies on GOPATH to contain only one directory... + - mkdir -p ${GOPATH}/src/${GO_REPO_NAMESPACE} + - ln -sv ${TRAVIS_BUILD_DIR} ${GOPATH}/src/${GO_REPO_FULLNAME} + - cd ${GOPATH}/src/${GO_REPO_FULLNAME} + # get and build ssdb - git clone git://github.com/ideawu/ssdb.git - cd ssdb - make @@ -23,10 +30,11 @@ install: - go get github.com/go-sql-driver/mysql - go get github.com/mattn/go-sqlite3 - go get github.com/bradfitz/gomemcache/memcache - - go get github.com/garyburd/redigo/redis + - go get github.com/gomodule/redigo/redis - go get github.com/beego/x2j - go get github.com/couchbase/go-couchbase - go get github.com/beego/goyaml2 + - go get gopkg.in/yaml.v2 - go get github.com/belogik/goes - go get github.com/siddontang/ledisdb/config - go get github.com/siddontang/ledisdb/ledis @@ -35,28 +43,32 @@ install: - go get github.com/gogo/protobuf/proto - go get github.com/Knetic/govaluate - go get github.com/casbin/casbin - - go get -u honnef.co/go/tools/cmd/gosimple + - go get github.com/elazarl/go-bindata-assetfs + - go get github.com/OwnLocal/goes + - go get github.com/shiena/ansicolor + - go get -u honnef.co/go/tools/cmd/staticcheck - go get -u github.com/mdempsky/unconvert - go get -u github.com/gordonklaus/ineffassign - go get -u github.com/golang/lint/golint + - go get -u github.com/go-redis/redis before_script: - psql --version - sh -c "if [ '$ORM_DRIVER' = 'postgres' ]; then psql -c 'create database orm_test;' -U postgres; fi" - sh -c "if [ '$ORM_DRIVER' = 'mysql' ]; then mysql -u root -e 'create database orm_test;'; fi" - sh -c "if [ '$ORM_DRIVER' = 'sqlite' ]; then touch $TRAVIS_BUILD_DIR/orm_test.db; fi" - - sh -c "if [ $(go version) == *1.[5-9]* ]; then go get github.com/golang/lint/golint; golint ./...; fi" - - sh -c "if [ $(go version) == *1.[5-9]* ]; then go tool vet .; fi" + - sh -c "go get github.com/golang/lint/golint; golint ./...;" + - sh -c "go list ./... | grep -v vendor | xargs go vet -v" - mkdir -p res/var - ./ssdb/ssdb-server ./ssdb/ssdb.conf -d after_script: - -killall -w ssdb-server + - killall -w ssdb-server - rm -rf ./res/var/* script: - go test -v ./... - - gosimple -ignore "$(cat .gosimpleignore)" $(go list ./... | grep -v /vendor/) + - staticcheck -show-ignored -checks "-ST1017,-U1000,-ST1005,-S1034,-S1012,-SA4006,-SA6005,-SA1019,-SA1024" - unconvert $(go list ./... | grep -v /vendor/) - ineffassign . - find . ! \( -path './vendor' -prune \) -type f -name '*.go' -print0 | xargs -0 gofmt -l -s - golint ./... addons: - postgresql: "9.4" + postgresql: "9.6" diff --git a/src/vendor/github.com/astaxie/beego/README.md b/src/vendor/github.com/astaxie/beego/README.md index c08927fb1..5063645c4 100644 --- a/src/vendor/github.com/astaxie/beego/README.md +++ b/src/vendor/github.com/astaxie/beego/README.md @@ -1,8 +1,11 @@ -# Beego [![Build Status](https://travis-ci.org/astaxie/beego.svg?branch=master)](https://travis-ci.org/astaxie/beego) [![GoDoc](http://godoc.org/github.com/astaxie/beego?status.svg)](http://godoc.org/github.com/astaxie/beego) [![Foundation](https://img.shields.io/badge/Golang-Foundation-green.svg)](http://golangfoundation.org) +# Beego [![Build Status](https://travis-ci.org/astaxie/beego.svg?branch=master)](https://travis-ci.org/astaxie/beego) [![GoDoc](http://godoc.org/github.com/astaxie/beego?status.svg)](http://godoc.org/github.com/astaxie/beego) [![Foundation](https://img.shields.io/badge/Golang-Foundation-green.svg)](http://golangfoundation.org) [![Go Report Card](https://goreportcard.com/badge/github.com/astaxie/beego)](https://goreportcard.com/report/github.com/astaxie/beego) + beego is used for rapid development of RESTful APIs, web apps and backend services in Go. It is inspired by Tornado, Sinatra and Flask. beego has some Go-specific features such as interfaces and struct embedding. + Response time ranking: [web-frameworks](https://github.com/the-benchmarker/web-frameworks). + ###### More info at [beego.me](http://beego.me). ## Quick Start diff --git a/src/vendor/github.com/astaxie/beego/admin.go b/src/vendor/github.com/astaxie/beego/admin.go index 0688dcbcc..256065011 100644 --- a/src/vendor/github.com/astaxie/beego/admin.go +++ b/src/vendor/github.com/astaxie/beego/admin.go @@ -20,11 +20,10 @@ import ( "fmt" "net/http" "os" + "reflect" "text/template" "time" - "reflect" - "github.com/astaxie/beego/grace" "github.com/astaxie/beego/logs" "github.com/astaxie/beego/toolbox" @@ -35,7 +34,7 @@ import ( var beeAdminApp *adminApp // FilterMonitorFunc is default monitor filter when admin module is enable. -// if this func returns, admin module records qbs for this request by condition of this function logic. +// if this func returns, admin module records qps for this request by condition of this function logic. // usage: // func MyFilterMonitor(method, requestPath string, t time.Duration, pattern string, statusCode int) bool { // if method == "POST" { @@ -67,15 +66,27 @@ func init() { // AdminIndex is the default http.Handler for admin module. // it matches url pattern "/". -func adminIndex(rw http.ResponseWriter, r *http.Request) { +func adminIndex(rw http.ResponseWriter, _ *http.Request) { execTpl(rw, map[interface{}]interface{}{}, indexTpl, defaultScriptsTpl) } -// QpsIndex is the http.Handler for writing qbs statistics map result info in http.ResponseWriter. -// it's registered with url pattern "/qbs" in admin module. -func qpsIndex(rw http.ResponseWriter, r *http.Request) { +// QpsIndex is the http.Handler for writing qps statistics map result info in http.ResponseWriter. +// it's registered with url pattern "/qps" in admin module. +func qpsIndex(rw http.ResponseWriter, _ *http.Request) { data := make(map[interface{}]interface{}) data["Content"] = toolbox.StatisticsMap.GetMap() + + // do html escape before display path, avoid xss + if content, ok := (data["Content"]).(M); ok { + if resultLists, ok := (content["Data"]).([][]string); ok { + for i := range resultLists { + if len(resultLists[i]) > 0 { + resultLists[i][0] = template.HTMLEscapeString(resultLists[i][0]) + } + } + } + } + execTpl(rw, data, qpsTpl, defaultScriptsTpl) } @@ -92,7 +103,7 @@ func listConf(rw http.ResponseWriter, r *http.Request) { data := make(map[interface{}]interface{}) switch command { case "conf": - m := make(map[string]interface{}) + m := make(M) list("BConfig", BConfig, m) m["AppConfigPath"] = appConfigPath m["AppConfigProvider"] = appConfigProvider @@ -116,14 +127,14 @@ func listConf(rw http.ResponseWriter, r *http.Request) { execTpl(rw, data, routerAndFilterTpl, defaultScriptsTpl) case "filter": var ( - content = map[string]interface{}{ + content = M{ "Fields": []string{ "Router Pattern", "Filter Function", }, } filterTypes = []string{} - filterTypeData = make(map[string]interface{}) + filterTypeData = make(M) ) if BeeApp.Handlers.enableFilter { @@ -161,7 +172,7 @@ func listConf(rw http.ResponseWriter, r *http.Request) { } } -func list(root string, p interface{}, m map[string]interface{}) { +func list(root string, p interface{}, m M) { pt := reflect.TypeOf(p) pv := reflect.ValueOf(p) if pt.Kind() == reflect.Ptr { @@ -184,11 +195,11 @@ func list(root string, p interface{}, m map[string]interface{}) { } // PrintTree prints all registered routers. -func PrintTree() map[string]interface{} { +func PrintTree() M { var ( - content = map[string]interface{}{} + content = M{} methods = []string{} - methodsData = make(map[string]interface{}) + methodsData = make(M) ) for method, t := range BeeApp.Handlers.routers { @@ -279,12 +290,12 @@ func profIndex(rw http.ResponseWriter, r *http.Request) { // Healthcheck is a http.Handler calling health checking and showing the result. // it's in "/healthcheck" pattern in admin module. -func healthcheck(rw http.ResponseWriter, req *http.Request) { +func healthcheck(rw http.ResponseWriter, _ *http.Request) { var ( result []string data = make(map[interface{}]interface{}) resultList = new([][]string) - content = map[string]interface{}{ + content = M{ "Fields": []string{"Name", "Message", "Status"}, } ) @@ -332,7 +343,7 @@ func taskStatus(rw http.ResponseWriter, req *http.Request) { } // List Tasks - content := make(map[string]interface{}) + content := make(M) resultList := new([][]string) var fields = []string{ "Task Name", diff --git a/src/vendor/github.com/astaxie/beego/app.go b/src/vendor/github.com/astaxie/beego/app.go index 25ea2a04f..d9e85e9b6 100644 --- a/src/vendor/github.com/astaxie/beego/app.go +++ b/src/vendor/github.com/astaxie/beego/app.go @@ -15,17 +15,22 @@ package beego import ( + "crypto/tls" + "crypto/x509" "fmt" + "io/ioutil" "net" "net/http" "net/http/fcgi" "os" "path" + "strings" "time" "github.com/astaxie/beego/grace" "github.com/astaxie/beego/logs" "github.com/astaxie/beego/utils" + "golang.org/x/crypto/acme/autocert" ) var ( @@ -51,8 +56,11 @@ func NewApp() *App { return app } +// MiddleWare function for http.Handler +type MiddleWare func(http.Handler) http.Handler + // Run beego application. -func (app *App) Run() { +func (app *App) Run(mws ...MiddleWare) { addr := BConfig.Listen.HTTPAddr if BConfig.Listen.HTTPPort != 0 { @@ -94,6 +102,12 @@ func (app *App) Run() { } app.Server.Handler = app.Handlers + for i := len(mws) - 1; i >= 0; i-- { + if mws[i] == nil { + continue + } + app.Server.Handler = mws[i](app.Server.Handler) + } app.Server.ReadTimeout = time.Duration(BConfig.Listen.ServerTimeOut) * time.Second app.Server.WriteTimeout = time.Duration(BConfig.Listen.ServerTimeOut) * time.Second app.Server.ErrorLog = logs.GetLogger("HTTP") @@ -102,9 +116,9 @@ func (app *App) Run() { if BConfig.Listen.Graceful { httpsAddr := BConfig.Listen.HTTPSAddr app.Server.Addr = httpsAddr - if BConfig.Listen.EnableHTTPS { + if BConfig.Listen.EnableHTTPS || BConfig.Listen.EnableMutualHTTPS { go func() { - time.Sleep(20 * time.Microsecond) + time.Sleep(1000 * time.Microsecond) if BConfig.Listen.HTTPSPort != 0 { httpsAddr = fmt.Sprintf("%s:%d", BConfig.Listen.HTTPSAddr, BConfig.Listen.HTTPSPort) app.Server.Addr = httpsAddr @@ -112,10 +126,27 @@ func (app *App) Run() { server := grace.NewServer(httpsAddr, app.Handlers) server.Server.ReadTimeout = app.Server.ReadTimeout server.Server.WriteTimeout = app.Server.WriteTimeout - if err := server.ListenAndServeTLS(BConfig.Listen.HTTPSCertFile, BConfig.Listen.HTTPSKeyFile); err != nil { - logs.Critical("ListenAndServeTLS: ", err, fmt.Sprintf("%d", os.Getpid())) - time.Sleep(100 * time.Microsecond) - endRunning <- true + if BConfig.Listen.EnableMutualHTTPS { + if err := server.ListenAndServeMutualTLS(BConfig.Listen.HTTPSCertFile, BConfig.Listen.HTTPSKeyFile, BConfig.Listen.TrustCaFile); err != nil { + logs.Critical("ListenAndServeTLS: ", err, fmt.Sprintf("%d", os.Getpid())) + time.Sleep(100 * time.Microsecond) + endRunning <- true + } + } else { + if BConfig.Listen.AutoTLS { + m := autocert.Manager{ + Prompt: autocert.AcceptTOS, + HostPolicy: autocert.HostWhitelist(BConfig.Listen.Domains...), + Cache: autocert.DirCache(BConfig.Listen.TLSCacheDir), + } + app.Server.TLSConfig = &tls.Config{GetCertificate: m.GetCertificate} + BConfig.Listen.HTTPSCertFile, BConfig.Listen.HTTPSKeyFile = "", "" + } + if err := server.ListenAndServeTLS(BConfig.Listen.HTTPSCertFile, BConfig.Listen.HTTPSKeyFile); err != nil { + logs.Critical("ListenAndServeTLS: ", err, fmt.Sprintf("%d", os.Getpid())) + time.Sleep(100 * time.Microsecond) + endRunning <- true + } } }() } @@ -139,22 +170,44 @@ func (app *App) Run() { } // run normal mode - if BConfig.Listen.EnableHTTPS { + if BConfig.Listen.EnableHTTPS || BConfig.Listen.EnableMutualHTTPS { go func() { - time.Sleep(20 * time.Microsecond) + time.Sleep(1000 * time.Microsecond) if BConfig.Listen.HTTPSPort != 0 { app.Server.Addr = fmt.Sprintf("%s:%d", BConfig.Listen.HTTPSAddr, BConfig.Listen.HTTPSPort) } else if BConfig.Listen.EnableHTTP { - BeeLogger.Info("Start https server error, confict with http.Please reset https port") + logs.Info("Start https server error, conflict with http. Please reset https port") return } logs.Info("https server Running on https://%s", app.Server.Addr) + if BConfig.Listen.AutoTLS { + m := autocert.Manager{ + Prompt: autocert.AcceptTOS, + HostPolicy: autocert.HostWhitelist(BConfig.Listen.Domains...), + Cache: autocert.DirCache(BConfig.Listen.TLSCacheDir), + } + app.Server.TLSConfig = &tls.Config{GetCertificate: m.GetCertificate} + BConfig.Listen.HTTPSCertFile, BConfig.Listen.HTTPSKeyFile = "", "" + } else if BConfig.Listen.EnableMutualHTTPS { + pool := x509.NewCertPool() + data, err := ioutil.ReadFile(BConfig.Listen.TrustCaFile) + if err != nil { + logs.Info("MutualHTTPS should provide TrustCaFile") + return + } + pool.AppendCertsFromPEM(data) + app.Server.TLSConfig = &tls.Config{ + ClientCAs: pool, + ClientAuth: tls.RequireAndVerifyClientCert, + } + } if err := app.Server.ListenAndServeTLS(BConfig.Listen.HTTPSCertFile, BConfig.Listen.HTTPSKeyFile); err != nil { logs.Critical("ListenAndServeTLS: ", err) time.Sleep(100 * time.Microsecond) endRunning <- true } }() + } if BConfig.Listen.EnableHTTP { go func() { @@ -207,6 +260,84 @@ func Router(rootpath string, c ControllerInterface, mappingMethods ...string) *A return BeeApp } +// UnregisterFixedRoute unregisters the route with the specified fixedRoute. It is particularly useful +// in web applications that inherit most routes from a base webapp via the underscore +// import, and aim to overwrite only certain paths. +// The method parameter can be empty or "*" for all HTTP methods, or a particular +// method type (e.g. "GET" or "POST") for selective removal. +// +// Usage (replace "GET" with "*" for all methods): +// beego.UnregisterFixedRoute("/yourpreviouspath", "GET") +// beego.Router("/yourpreviouspath", yourControllerAddress, "get:GetNewPage") +func UnregisterFixedRoute(fixedRoute string, method string) *App { + subPaths := splitPath(fixedRoute) + if method == "" || method == "*" { + for m := range HTTPMETHOD { + if _, ok := BeeApp.Handlers.routers[m]; !ok { + continue + } + if BeeApp.Handlers.routers[m].prefix == strings.Trim(fixedRoute, "/ ") { + findAndRemoveSingleTree(BeeApp.Handlers.routers[m]) + continue + } + findAndRemoveTree(subPaths, BeeApp.Handlers.routers[m], m) + } + return BeeApp + } + // Single HTTP method + um := strings.ToUpper(method) + if _, ok := BeeApp.Handlers.routers[um]; ok { + if BeeApp.Handlers.routers[um].prefix == strings.Trim(fixedRoute, "/ ") { + findAndRemoveSingleTree(BeeApp.Handlers.routers[um]) + return BeeApp + } + findAndRemoveTree(subPaths, BeeApp.Handlers.routers[um], um) + } + return BeeApp +} + +func findAndRemoveTree(paths []string, entryPointTree *Tree, method string) { + for i := range entryPointTree.fixrouters { + if entryPointTree.fixrouters[i].prefix == paths[0] { + if len(paths) == 1 { + if len(entryPointTree.fixrouters[i].fixrouters) > 0 { + // If the route had children subtrees, remove just the functional leaf, + // to allow children to function as before + if len(entryPointTree.fixrouters[i].leaves) > 0 { + entryPointTree.fixrouters[i].leaves[0] = nil + entryPointTree.fixrouters[i].leaves = entryPointTree.fixrouters[i].leaves[1:] + } + } else { + // Remove the *Tree from the fixrouters slice + entryPointTree.fixrouters[i] = nil + + if i == len(entryPointTree.fixrouters)-1 { + entryPointTree.fixrouters = entryPointTree.fixrouters[:i] + } else { + entryPointTree.fixrouters = append(entryPointTree.fixrouters[:i], entryPointTree.fixrouters[i+1:len(entryPointTree.fixrouters)]...) + } + } + return + } + findAndRemoveTree(paths[1:], entryPointTree.fixrouters[i], method) + } + } +} + +func findAndRemoveSingleTree(entryPointTree *Tree) { + if entryPointTree == nil { + return + } + if len(entryPointTree.fixrouters) > 0 { + // If the route had children subtrees, remove just the functional leaf, + // to allow children to function as before + if len(entryPointTree.leaves) > 0 { + entryPointTree.leaves[0] = nil + entryPointTree.leaves = entryPointTree.leaves[1:] + } + } +} + // Include will generate router file in the router/xxx.go from the controller's comments // usage: // beego.Include(&BankAccount{}, &OrderController{},&RefundController{},&ReceiptController{}) diff --git a/src/vendor/github.com/astaxie/beego/beego.go b/src/vendor/github.com/astaxie/beego/beego.go index 0512dc60a..66b19f36d 100644 --- a/src/vendor/github.com/astaxie/beego/beego.go +++ b/src/vendor/github.com/astaxie/beego/beego.go @@ -23,7 +23,7 @@ import ( const ( // VERSION represent beego web framework version. - VERSION = "1.9.0" + VERSION = "1.12.0" // DEV is for develop DEV = "dev" @@ -31,7 +31,10 @@ const ( PROD = "prod" ) -//hook function to run +// M is Map shortcut +type M map[string]interface{} + +// Hook function to run type hookfunc func() error var ( @@ -62,11 +65,29 @@ func Run(params ...string) { if len(strs) > 1 && strs[1] != "" { BConfig.Listen.HTTPPort, _ = strconv.Atoi(strs[1]) } + + BConfig.Listen.Domains = params } BeeApp.Run() } +// RunWithMiddleWares Run beego application with middlewares. +func RunWithMiddleWares(addr string, mws ...MiddleWare) { + initBeforeHTTPRun() + + strs := strings.Split(addr, ":") + if len(strs) > 0 && strs[0] != "" { + BConfig.Listen.HTTPAddr = strs[0] + BConfig.Listen.Domains = []string{strs[0]} + } + if len(strs) > 1 && strs[1] != "" { + BConfig.Listen.HTTPPort, _ = strconv.Atoi(strs[1]) + } + + BeeApp.Run(mws...) +} + func initBeforeHTTPRun() { //init hooks AddAPPStartHook( diff --git a/src/vendor/github.com/astaxie/beego/cache/README.md b/src/vendor/github.com/astaxie/beego/cache/README.md index 957790e7b..b467760af 100644 --- a/src/vendor/github.com/astaxie/beego/cache/README.md +++ b/src/vendor/github.com/astaxie/beego/cache/README.md @@ -52,7 +52,7 @@ Configure like this: ## Redis adapter -Redis adapter use the [redigo](http://github.com/garyburd/redigo) client. +Redis adapter use the [redigo](http://github.com/gomodule/redigo) client. Configure like this: diff --git a/src/vendor/github.com/astaxie/beego/cache/cache.go b/src/vendor/github.com/astaxie/beego/cache/cache.go index f7158741d..82585c4e5 100644 --- a/src/vendor/github.com/astaxie/beego/cache/cache.go +++ b/src/vendor/github.com/astaxie/beego/cache/cache.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package cache provide a Cache interface and some implemetn engine +// Package cache provide a Cache interface and some implement engine // Usage: // // import( diff --git a/src/vendor/github.com/astaxie/beego/cache/file.go b/src/vendor/github.com/astaxie/beego/cache/file.go index 691ce7cd7..6f12d3eee 100644 --- a/src/vendor/github.com/astaxie/beego/cache/file.go +++ b/src/vendor/github.com/astaxie/beego/cache/file.go @@ -62,11 +62,14 @@ func NewFileCache() Cache { } // StartAndGC will start and begin gc for file cache. -// the config need to be like {CachePath:"/cache","FileSuffix":".bin","DirectoryLevel":2,"EmbedExpiry":0} +// the config need to be like {CachePath:"/cache","FileSuffix":".bin","DirectoryLevel":"2","EmbedExpiry":"0"} func (fc *FileCache) StartAndGC(config string) error { - var cfg map[string]string - json.Unmarshal([]byte(config), &cfg) + cfg := make(map[string]string) + err := json.Unmarshal([]byte(config), &cfg) + if err != nil { + return err + } if _, ok := cfg["CachePath"]; !ok { cfg["CachePath"] = FileCachePath } @@ -142,12 +145,12 @@ func (fc *FileCache) GetMulti(keys []string) []interface{} { // Put value into file cache. // timeout means how long to keep this file, unit of ms. -// if timeout equals FileCacheEmbedExpiry(default is 0), cache this item forever. +// if timeout equals fc.EmbedExpiry(default is 0), cache this item forever. func (fc *FileCache) Put(key string, val interface{}, timeout time.Duration) error { gob.Register(val) item := FileCacheItem{Data: val} - if timeout == FileCacheEmbedExpiry { + if timeout == time.Duration(fc.EmbedExpiry) { item.Expired = time.Now().Add((86400 * 365 * 10) * time.Second) // ten years } else { item.Expired = time.Now().Add(timeout) @@ -179,7 +182,7 @@ func (fc *FileCache) Incr(key string) error { } else { incr = data.(int) + 1 } - fc.Put(key, incr, FileCacheEmbedExpiry) + fc.Put(key, incr, time.Duration(fc.EmbedExpiry)) return nil } @@ -192,7 +195,7 @@ func (fc *FileCache) Decr(key string) error { } else { decr = data.(int) - 1 } - fc.Put(key, decr, FileCacheEmbedExpiry) + fc.Put(key, decr, time.Duration(fc.EmbedExpiry)) return nil } diff --git a/src/vendor/github.com/astaxie/beego/cache/memory.go b/src/vendor/github.com/astaxie/beego/cache/memory.go index 57e868cfe..1fec2eff9 100644 --- a/src/vendor/github.com/astaxie/beego/cache/memory.go +++ b/src/vendor/github.com/astaxie/beego/cache/memory.go @@ -110,25 +110,25 @@ func (bc *MemoryCache) Delete(name string) error { // Incr increase cache counter in memory. // it supports int,int32,int64,uint,uint32,uint64. func (bc *MemoryCache) Incr(key string) error { - bc.RLock() - defer bc.RUnlock() + bc.Lock() + defer bc.Unlock() itm, ok := bc.items[key] if !ok { return errors.New("key not exist") } - switch itm.val.(type) { + switch val := itm.val.(type) { case int: - itm.val = itm.val.(int) + 1 + itm.val = val + 1 case int32: - itm.val = itm.val.(int32) + 1 + itm.val = val + 1 case int64: - itm.val = itm.val.(int64) + 1 + itm.val = val + 1 case uint: - itm.val = itm.val.(uint) + 1 + itm.val = val + 1 case uint32: - itm.val = itm.val.(uint32) + 1 + itm.val = val + 1 case uint64: - itm.val = itm.val.(uint64) + 1 + itm.val = val + 1 default: return errors.New("item val is not (u)int (u)int32 (u)int64") } @@ -137,34 +137,34 @@ func (bc *MemoryCache) Incr(key string) error { // Decr decrease counter in memory. func (bc *MemoryCache) Decr(key string) error { - bc.RLock() - defer bc.RUnlock() + bc.Lock() + defer bc.Unlock() itm, ok := bc.items[key] if !ok { return errors.New("key not exist") } - switch itm.val.(type) { + switch val := itm.val.(type) { case int: - itm.val = itm.val.(int) - 1 + itm.val = val - 1 case int64: - itm.val = itm.val.(int64) - 1 + itm.val = val - 1 case int32: - itm.val = itm.val.(int32) - 1 + itm.val = val - 1 case uint: - if itm.val.(uint) > 0 { - itm.val = itm.val.(uint) - 1 + if val > 0 { + itm.val = val - 1 } else { return errors.New("item val is less than 0") } case uint32: - if itm.val.(uint32) > 0 { - itm.val = itm.val.(uint32) - 1 + if val > 0 { + itm.val = val - 1 } else { return errors.New("item val is less than 0") } case uint64: - if itm.val.(uint64) > 0 { - itm.val = itm.val.(uint64) - 1 + if val > 0 { + itm.val = val - 1 } else { return errors.New("item val is less than 0") } @@ -203,13 +203,17 @@ func (bc *MemoryCache) StartAndGC(config string) error { dur := time.Duration(cf["interval"]) * time.Second bc.Every = cf["interval"] bc.dur = dur - go bc.vaccuum() + go bc.vacuum() return nil } // check expiration. -func (bc *MemoryCache) vaccuum() { - if bc.Every < 1 { +func (bc *MemoryCache) vacuum() { + bc.RLock() + every := bc.Every + bc.RUnlock() + + if every < 1 { return } for { diff --git a/src/vendor/github.com/astaxie/beego/cache/redis/redis.go b/src/vendor/github.com/astaxie/beego/cache/redis/redis.go index 3e71fb53a..372dd48b3 100644 --- a/src/vendor/github.com/astaxie/beego/cache/redis/redis.go +++ b/src/vendor/github.com/astaxie/beego/cache/redis/redis.go @@ -14,9 +14,9 @@ // Package redis for cache provider // -// depend on github.com/garyburd/redigo/redis +// depend on github.com/gomodule/redigo/redis // -// go install github.com/garyburd/redigo/redis +// go install github.com/gomodule/redigo/redis // // Usage: // import( @@ -32,12 +32,14 @@ package redis import ( "encoding/json" "errors" + "fmt" "strconv" "time" - "github.com/garyburd/redigo/redis" + "github.com/gomodule/redigo/redis" "github.com/astaxie/beego/cache" + "strings" ) var ( @@ -52,6 +54,7 @@ type Cache struct { dbNum int key string password string + maxIdle int } // NewRedisCache create new redis cache with default collection name. @@ -59,14 +62,23 @@ func NewRedisCache() cache.Cache { return &Cache{key: DefaultKey} } -// actually do the redis cmds +// actually do the redis cmds, args[0] must be the key name. func (rc *Cache) do(commandName string, args ...interface{}) (reply interface{}, err error) { + if len(args) < 1 { + return nil, errors.New("missing required arguments") + } + args[0] = rc.associate(args[0]) c := rc.p.Get() defer c.Close() return c.Do(commandName, args...) } +// associate with config key. +func (rc *Cache) associate(originKey interface{}) string { + return fmt.Sprintf("%s:%s", rc.key, originKey) +} + // Get cache from redis. func (rc *Cache) Get(key string) interface{} { if v, err := rc.do("GET", key); err == nil { @@ -77,57 +89,28 @@ func (rc *Cache) Get(key string) interface{} { // GetMulti get cache from redis. func (rc *Cache) GetMulti(keys []string) []interface{} { - size := len(keys) - var rv []interface{} c := rc.p.Get() defer c.Close() - var err error + var args []interface{} for _, key := range keys { - err = c.Send("GET", key) - if err != nil { - goto ERROR - } + args = append(args, rc.associate(key)) } - if err = c.Flush(); err != nil { - goto ERROR + values, err := redis.Values(c.Do("MGET", args...)) + if err != nil { + return nil } - for i := 0; i < size; i++ { - if v, err := c.Receive(); err == nil { - rv = append(rv, v.([]byte)) - } else { - rv = append(rv, err) - } - } - return rv -ERROR: - rv = rv[0:0] - for i := 0; i < size; i++ { - rv = append(rv, nil) - } - - return rv + return values } // Put put cache to redis. func (rc *Cache) Put(key string, val interface{}, timeout time.Duration) error { - var err error - if _, err = rc.do("SETEX", key, int64(timeout/time.Second), val); err != nil { - return err - } - - if _, err = rc.do("HSET", rc.key, key, true); err != nil { - return err - } + _, err := rc.do("SETEX", key, int64(timeout/time.Second), val) return err } // Delete delete cache in redis. func (rc *Cache) Delete(key string) error { - var err error - if _, err = rc.do("DEL", key); err != nil { - return err - } - _, err = rc.do("HDEL", rc.key, key) + _, err := rc.do("DEL", key) return err } @@ -137,11 +120,6 @@ func (rc *Cache) IsExist(key string) bool { if err != nil { return false } - if !v { - if _, err = rc.do("HDEL", rc.key, key); err != nil { - return false - } - } return v } @@ -159,16 +137,17 @@ func (rc *Cache) Decr(key string) error { // ClearAll clean all cache in redis. delete this redis collection. func (rc *Cache) ClearAll() error { - cachedKeys, err := redis.Strings(rc.do("HKEYS", rc.key)) + c := rc.p.Get() + defer c.Close() + cachedKeys, err := redis.Strings(c.Do("KEYS", rc.key+":*")) if err != nil { return err } for _, str := range cachedKeys { - if _, err = rc.do("DEL", str); err != nil { + if _, err = c.Do("DEL", str); err != nil { return err } } - _, err = rc.do("DEL", rc.key) return err } @@ -186,16 +165,28 @@ func (rc *Cache) StartAndGC(config string) error { if _, ok := cf["conn"]; !ok { return errors.New("config has no conn key") } + + // Format redis://@: + cf["conn"] = strings.Replace(cf["conn"], "redis://", "", 1) + if i := strings.Index(cf["conn"], "@"); i > -1 { + cf["password"] = cf["conn"][0:i] + cf["conn"] = cf["conn"][i+1:] + } + if _, ok := cf["dbNum"]; !ok { cf["dbNum"] = "0" } if _, ok := cf["password"]; !ok { cf["password"] = "" } + if _, ok := cf["maxIdle"]; !ok { + cf["maxIdle"] = "3" + } rc.key = cf["key"] rc.conninfo = cf["conn"] rc.dbNum, _ = strconv.Atoi(cf["dbNum"]) rc.password = cf["password"] + rc.maxIdle, _ = strconv.Atoi(cf["maxIdle"]) rc.connectInit() @@ -229,7 +220,7 @@ func (rc *Cache) connectInit() { } // initialize a new pool rc.p = &redis.Pool{ - MaxIdle: 3, + MaxIdle: rc.maxIdle, IdleTimeout: 180 * time.Second, Dial: dialFunc, } diff --git a/src/vendor/github.com/astaxie/beego/config.go b/src/vendor/github.com/astaxie/beego/config.go index e6e99570f..7969dcea5 100644 --- a/src/vendor/github.com/astaxie/beego/config.go +++ b/src/vendor/github.com/astaxie/beego/config.go @@ -49,22 +49,27 @@ type Config struct { // Listen holds for http and https related config type Listen struct { - Graceful bool // Graceful means use graceful module to start the server - ServerTimeOut int64 - ListenTCP4 bool - EnableHTTP bool - HTTPAddr string - HTTPPort int - EnableHTTPS bool - HTTPSAddr string - HTTPSPort int - HTTPSCertFile string - HTTPSKeyFile string - EnableAdmin bool - AdminAddr string - AdminPort int - EnableFcgi bool - EnableStdIo bool // EnableStdIo works with EnableFcgi Use FCGI via standard I/O + Graceful bool // Graceful means use graceful module to start the server + ServerTimeOut int64 + ListenTCP4 bool + EnableHTTP bool + HTTPAddr string + HTTPPort int + AutoTLS bool + Domains []string + TLSCacheDir string + EnableHTTPS bool + EnableMutualHTTPS bool + HTTPSAddr string + HTTPSPort int + HTTPSCertFile string + HTTPSKeyFile string + TrustCaFile string + EnableAdmin bool + AdminAddr string + AdminPort int + EnableFcgi bool + EnableStdIo bool // EnableStdIo works with EnableFcgi Use FCGI via standard I/O } // WebConfig holds web related config @@ -96,16 +101,18 @@ type SessionConfig struct { SessionAutoSetCookie bool SessionDomain string SessionDisableHTTPOnly bool // used to allow for cross domain cookies/javascript cookies. - SessionEnableSidInHTTPHeader bool // enable store/get the sessionId into/from http headers + SessionEnableSidInHTTPHeader bool // enable store/get the sessionId into/from http headers SessionNameInHTTPHeader string - SessionEnableSidInURLQuery bool // enable get the sessionId from Url Query params + SessionEnableSidInURLQuery bool // enable get the sessionId from Url Query params } // LogConfig holds Log related config type LogConfig struct { - AccessLogs bool - FileLineNum bool - Outputs map[string]string // Store Adaptor : config + AccessLogs bool + EnableStaticLogs bool //log static files requests default: false + AccessLogsFormat string //access log format: JSON_FORMAT, APACHE_FORMAT or empty string + FileLineNum bool + Outputs map[string]string // Store Adaptor : config } var ( @@ -134,9 +141,13 @@ func init() { if err != nil { panic(err) } - appConfigPath = filepath.Join(workPath, "conf", "app.conf") + var filename = "app.conf" + if os.Getenv("BEEGO_RUNMODE") != "" { + filename = os.Getenv("BEEGO_RUNMODE") + ".app.conf" + } + appConfigPath = filepath.Join(workPath, "conf", filename) if !utils.FileExists(appConfigPath) { - appConfigPath = filepath.Join(AppPath, "conf", "app.conf") + appConfigPath = filepath.Join(AppPath, "conf", filename) if !utils.FileExists(appConfigPath) { AppConfig = &beegoAppConfig{innerConfig: config.NewFakeConfig()} return @@ -175,13 +186,18 @@ func recoverPanic(ctx *context.Context) { if BConfig.RunMode == DEV && BConfig.EnableErrorsRender { showErr(err, ctx, stack) } + if ctx.Output.Status != 0 { + ctx.ResponseWriter.WriteHeader(ctx.Output.Status) + } else { + ctx.ResponseWriter.WriteHeader(500) + } } } func newBConfig() *Config { return &Config{ AppName: "beego", - RunMode: DEV, + RunMode: PROD, RouterCaseSensitive: true, ServerName: "beegoServer:" + VERSION, RecoverPanic: true, @@ -196,6 +212,9 @@ func newBConfig() *Config { ServerTimeOut: 0, ListenTCP4: false, EnableHTTP: true, + AutoTLS: false, + Domains: []string{}, + TLSCacheDir: ".", HTTPAddr: "", HTTPPort: 8080, EnableHTTPS: false, @@ -233,15 +252,17 @@ func newBConfig() *Config { SessionCookieLifeTime: 0, //set cookie default is the browser life SessionAutoSetCookie: true, SessionDomain: "", - SessionEnableSidInHTTPHeader: false, // enable store/get the sessionId into/from http headers + SessionEnableSidInHTTPHeader: false, // enable store/get the sessionId into/from http headers SessionNameInHTTPHeader: "Beegosessionid", - SessionEnableSidInURLQuery: false, // enable get the sessionId from Url Query params + SessionEnableSidInURLQuery: false, // enable get the sessionId from Url Query params }, }, Log: LogConfig{ - AccessLogs: false, - FileLineNum: true, - Outputs: map[string]string{"console": ""}, + AccessLogs: false, + EnableStaticLogs: false, + AccessLogsFormat: "APACHE_FORMAT", + FileLineNum: true, + Outputs: map[string]string{"console": ""}, }, } } diff --git a/src/vendor/github.com/astaxie/beego/config/config.go b/src/vendor/github.com/astaxie/beego/config/config.go index c620504a1..bfd79e85d 100644 --- a/src/vendor/github.com/astaxie/beego/config/config.go +++ b/src/vendor/github.com/astaxie/beego/config/config.go @@ -150,12 +150,12 @@ func ExpandValueEnv(value string) (realValue string) { } key := "" - defalutV := "" + defaultV := "" // value start with "${" for i := 2; i < vLen; i++ { if value[i] == '|' && (i+1 < vLen && value[i+1] == '|') { key = value[2:i] - defalutV = value[i+2 : vLen-1] // other string is default value. + defaultV = value[i+2 : vLen-1] // other string is default value. break } else if value[i] == '}' { key = value[2:i] @@ -165,7 +165,7 @@ func ExpandValueEnv(value string) (realValue string) { realValue = os.Getenv(key) if realValue == "" { - realValue = defalutV + realValue = defaultV } return diff --git a/src/vendor/github.com/astaxie/beego/config/fake.go b/src/vendor/github.com/astaxie/beego/config/fake.go index f5144598e..d21ab820d 100644 --- a/src/vendor/github.com/astaxie/beego/config/fake.go +++ b/src/vendor/github.com/astaxie/beego/config/fake.go @@ -126,7 +126,7 @@ func (c *fakeConfigContainer) SaveConfigFile(filename string) error { var _ Configer = new(fakeConfigContainer) -// NewFakeConfig return a fake Congiger +// NewFakeConfig return a fake Configer func NewFakeConfig() Configer { return &fakeConfigContainer{ data: make(map[string]string), diff --git a/src/vendor/github.com/astaxie/beego/config/ini.go b/src/vendor/github.com/astaxie/beego/config/ini.go index a681bc1b1..002e5e056 100644 --- a/src/vendor/github.com/astaxie/beego/config/ini.go +++ b/src/vendor/github.com/astaxie/beego/config/ini.go @@ -78,15 +78,37 @@ func (ini *IniConfig) parseData(dir string, data []byte) (*IniConfigContainer, e } } section := defaultSection + tmpBuf := bytes.NewBuffer(nil) for { - line, _, err := buf.ReadLine() - if err == io.EOF { + tmpBuf.Reset() + + shouldBreak := false + for { + tmp, isPrefix, err := buf.ReadLine() + if err == io.EOF { + shouldBreak = true + break + } + + //It might be a good idea to throw a error on all unknonw errors? + if _, ok := err.(*os.PathError); ok { + return nil, err + } + + tmpBuf.Write(tmp) + if isPrefix { + continue + } + + if !isPrefix { + break + } + } + if shouldBreak { break } - //It might be a good idea to throw a error on all unknonw errors? - if _, ok := err.(*os.PathError); ok { - return nil, err - } + + line := tmpBuf.Bytes() line = bytes.TrimSpace(line) if bytes.Equal(line, bEmpty) { continue @@ -215,7 +237,7 @@ func (c *IniConfigContainer) Bool(key string) (bool, error) { } // DefaultBool returns the boolean value for a given key. -// if err != nil return defaltval +// if err != nil return defaultval func (c *IniConfigContainer) DefaultBool(key string, defaultval bool) bool { v, err := c.Bool(key) if err != nil { @@ -230,7 +252,7 @@ func (c *IniConfigContainer) Int(key string) (int, error) { } // DefaultInt returns the integer value for a given key. -// if err != nil return defaltval +// if err != nil return defaultval func (c *IniConfigContainer) DefaultInt(key string, defaultval int) int { v, err := c.Int(key) if err != nil { @@ -245,7 +267,7 @@ func (c *IniConfigContainer) Int64(key string) (int64, error) { } // DefaultInt64 returns the int64 value for a given key. -// if err != nil return defaltval +// if err != nil return defaultval func (c *IniConfigContainer) DefaultInt64(key string, defaultval int64) int64 { v, err := c.Int64(key) if err != nil { @@ -260,7 +282,7 @@ func (c *IniConfigContainer) Float(key string) (float64, error) { } // DefaultFloat returns the float64 value for a given key. -// if err != nil return defaltval +// if err != nil return defaultval func (c *IniConfigContainer) DefaultFloat(key string, defaultval float64) float64 { v, err := c.Float(key) if err != nil { @@ -275,7 +297,7 @@ func (c *IniConfigContainer) String(key string) string { } // DefaultString returns the string value for a given key. -// if err != nil return defaltval +// if err != nil return defaultval func (c *IniConfigContainer) DefaultString(key string, defaultval string) string { v := c.String(key) if v == "" { @@ -295,7 +317,7 @@ func (c *IniConfigContainer) Strings(key string) []string { } // DefaultStrings returns the []string value for a given key. -// if err != nil return defaltval +// if err != nil return defaultval func (c *IniConfigContainer) DefaultStrings(key string, defaultval []string) []string { v := c.Strings(key) if v == nil { @@ -314,7 +336,7 @@ func (c *IniConfigContainer) GetSection(section string) (map[string]string, erro // SaveConfigFile save the config into file. // -// BUG(env): The environment variable config item will be saved with real value in SaveConfigFile Funcation. +// BUG(env): The environment variable config item will be saved with real value in SaveConfigFile Function. func (c *IniConfigContainer) SaveConfigFile(filename string) (err error) { // Write configuration file by filename. f, err := os.Create(filename) diff --git a/src/vendor/github.com/astaxie/beego/config/json.go b/src/vendor/github.com/astaxie/beego/config/json.go index a0d932105..74c18c9c1 100644 --- a/src/vendor/github.com/astaxie/beego/config/json.go +++ b/src/vendor/github.com/astaxie/beego/config/json.go @@ -101,7 +101,7 @@ func (c *JSONConfigContainer) Int(key string) (int, error) { } // DefaultInt returns the integer value for a given key. -// if err != nil return defaltval +// if err != nil return defaultval func (c *JSONConfigContainer) DefaultInt(key string, defaultval int) int { if v, err := c.Int(key); err == nil { return v @@ -122,7 +122,7 @@ func (c *JSONConfigContainer) Int64(key string) (int64, error) { } // DefaultInt64 returns the int64 value for a given key. -// if err != nil return defaltval +// if err != nil return defaultval func (c *JSONConfigContainer) DefaultInt64(key string, defaultval int64) int64 { if v, err := c.Int64(key); err == nil { return v @@ -143,7 +143,7 @@ func (c *JSONConfigContainer) Float(key string) (float64, error) { } // DefaultFloat returns the float64 value for a given key. -// if err != nil return defaltval +// if err != nil return defaultval func (c *JSONConfigContainer) DefaultFloat(key string, defaultval float64) float64 { if v, err := c.Float(key); err == nil { return v @@ -163,7 +163,7 @@ func (c *JSONConfigContainer) String(key string) string { } // DefaultString returns the string value for a given key. -// if err != nil return defaltval +// if err != nil return defaultval func (c *JSONConfigContainer) DefaultString(key string, defaultval string) string { // TODO FIXME should not use "" to replace non existence if v := c.String(key); v != "" { @@ -182,7 +182,7 @@ func (c *JSONConfigContainer) Strings(key string) []string { } // DefaultStrings returns the []string value for a given key. -// if err != nil return defaltval +// if err != nil return defaultval func (c *JSONConfigContainer) DefaultStrings(key string, defaultval []string) []string { if v := c.Strings(key); v != nil { return v diff --git a/src/vendor/github.com/astaxie/beego/context/context.go b/src/vendor/github.com/astaxie/beego/context/context.go index 8b32062c2..bbd58299f 100644 --- a/src/vendor/github.com/astaxie/beego/context/context.go +++ b/src/vendor/github.com/astaxie/beego/context/context.go @@ -38,6 +38,14 @@ import ( "github.com/astaxie/beego/utils" ) +//commonly used mime-types +const ( + ApplicationJSON = "application/json" + ApplicationXML = "application/xml" + ApplicationYAML = "application/x-yaml" + TextXML = "text/xml" +) + // NewContext return the Context with Input and Output func NewContext() *Context { return &Context{ @@ -193,6 +201,7 @@ type Response struct { http.ResponseWriter Started bool Status int + Elapsed time.Duration } func (r *Response) reset(rw http.ResponseWriter) { @@ -244,3 +253,11 @@ func (r *Response) CloseNotify() <-chan bool { } return nil } + +// Pusher http.Pusher +func (r *Response) Pusher() (pusher http.Pusher) { + if pusher, ok := r.ResponseWriter.(http.Pusher); ok { + return pusher + } + return nil +} diff --git a/src/vendor/github.com/astaxie/beego/context/input.go b/src/vendor/github.com/astaxie/beego/context/input.go index 2c53c6014..760406160 100644 --- a/src/vendor/github.com/astaxie/beego/context/input.go +++ b/src/vendor/github.com/astaxie/beego/context/input.go @@ -20,12 +20,14 @@ import ( "errors" "io" "io/ioutil" + "net" "net/http" "net/url" "reflect" "regexp" "strconv" "strings" + "sync" "github.com/astaxie/beego/session" ) @@ -36,6 +38,7 @@ var ( acceptsHTMLRegex = regexp.MustCompile(`(text/html|application/xhtml\+xml)(?:,|$)`) acceptsXMLRegex = regexp.MustCompile(`(application/xml|text/xml)(?:,|$)`) acceptsJSONRegex = regexp.MustCompile(`(application/json)(?:,|$)`) + acceptsYAMLRegex = regexp.MustCompile(`(application/x-yaml)(?:,|$)`) maxParam = 50 ) @@ -47,6 +50,7 @@ type BeegoInput struct { pnames []string pvalues []string data map[interface{}]interface{} // store some values in this context when calling context in filter or controller. + dataLock sync.RWMutex RequestBody []byte RunMethod string RunController reflect.Type @@ -115,9 +119,8 @@ func (input *BeegoInput) Domain() string { // if no host info in request, return localhost. func (input *BeegoInput) Host() string { if input.Context.Request.Host != "" { - hostParts := strings.Split(input.Context.Request.Host, ":") - if len(hostParts) > 0 { - return hostParts[0] + if hostPart, _, err := net.SplitHostPort(input.Context.Request.Host); err == nil { + return hostPart } return input.Context.Request.Host } @@ -204,22 +207,27 @@ func (input *BeegoInput) AcceptsJSON() bool { return acceptsJSONRegex.MatchString(input.Header("Accept")) } +// AcceptsYAML Checks if request accepts json response +func (input *BeegoInput) AcceptsYAML() bool { + return acceptsYAMLRegex.MatchString(input.Header("Accept")) +} + // IP returns request client ip. // if in proxy, return first proxy id. -// if error, return 127.0.0.1. +// if error, return RemoteAddr. func (input *BeegoInput) IP() string { ips := input.Proxy() if len(ips) > 0 && ips[0] != "" { - rip := strings.Split(ips[0], ":") - return rip[0] - } - ip := strings.Split(input.Context.Request.RemoteAddr, ":") - if len(ip) > 0 { - if ip[0] != "[" { - return ip[0] + rip, _, err := net.SplitHostPort(ips[0]) + if err != nil { + rip = ips[0] } + return rip } - return "127.0.0.1" + if ip, _, err := net.SplitHostPort(input.Context.Request.RemoteAddr); err == nil { + return ip + } + return input.Context.Request.RemoteAddr } // Proxy returns proxy client ips slice. @@ -253,9 +261,8 @@ func (input *BeegoInput) SubDomains() string { // Port returns request client port. // when error or empty, return 80. func (input *BeegoInput) Port() int { - parts := strings.Split(input.Context.Request.Host, ":") - if len(parts) == 2 { - port, _ := strconv.Atoi(parts[1]) + if _, portPart, err := net.SplitHostPort(input.Context.Request.Host); err == nil { + port, _ := strconv.Atoi(portPart) return port } return 80 @@ -373,6 +380,8 @@ func (input *BeegoInput) CopyBody(MaxMemory int64) []byte { // Data return the implicit data in the input func (input *BeegoInput) Data() map[interface{}]interface{} { + input.dataLock.Lock() + defer input.dataLock.Unlock() if input.data == nil { input.data = make(map[interface{}]interface{}) } @@ -381,6 +390,8 @@ func (input *BeegoInput) Data() map[interface{}]interface{} { // GetData returns the stored data in this context. func (input *BeegoInput) GetData(key interface{}) interface{} { + input.dataLock.Lock() + defer input.dataLock.Unlock() if v, ok := input.data[key]; ok { return v } @@ -390,6 +401,8 @@ func (input *BeegoInput) GetData(key interface{}) interface{} { // SetData stores data with given key in this context. // This data are only available in this context. func (input *BeegoInput) SetData(key, val interface{}) { + input.dataLock.Lock() + defer input.dataLock.Unlock() if input.data == nil { input.data = make(map[interface{}]interface{}) } diff --git a/src/vendor/github.com/astaxie/beego/context/output.go b/src/vendor/github.com/astaxie/beego/context/output.go index 61ce8cc76..238dcf45e 100644 --- a/src/vendor/github.com/astaxie/beego/context/output.go +++ b/src/vendor/github.com/astaxie/beego/context/output.go @@ -30,6 +30,8 @@ import ( "strconv" "strings" "time" + + yaml "gopkg.in/yaml.v2" ) // BeegoOutput does work for sending response header. @@ -182,8 +184,8 @@ func errorRenderer(err error) Renderer { } // JSON writes json to response body. -// if coding is true, it converts utf-8 to \u0000 type. -func (output *BeegoOutput) JSON(data interface{}, hasIndent bool, coding bool) error { +// if encoding is true, it converts utf-8 to \u0000 type. +func (output *BeegoOutput) JSON(data interface{}, hasIndent bool, encoding bool) error { output.Header("Content-Type", "application/json; charset=utf-8") var content []byte var err error @@ -196,12 +198,25 @@ func (output *BeegoOutput) JSON(data interface{}, hasIndent bool, coding bool) e http.Error(output.Context.ResponseWriter, err.Error(), http.StatusInternalServerError) return err } - if coding { + if encoding { content = []byte(stringsToJSON(string(content))) } return output.Body(content) } +// YAML writes yaml to response body. +func (output *BeegoOutput) YAML(data interface{}) error { + output.Header("Content-Type", "application/x-yaml; charset=utf-8") + var content []byte + var err error + content, err = yaml.Marshal(data) + if err != nil { + http.Error(output.Context.ResponseWriter, err.Error(), http.StatusInternalServerError) + return err + } + return output.Body(content) +} + // JSONP writes jsonp to response body. func (output *BeegoOutput) JSONP(data interface{}, hasIndent bool) error { output.Header("Content-Type", "application/javascript; charset=utf-8") @@ -245,6 +260,19 @@ func (output *BeegoOutput) XML(data interface{}, hasIndent bool) error { return output.Body(content) } +// ServeFormatted serve YAML, XML OR JSON, depending on the value of the Accept header +func (output *BeegoOutput) ServeFormatted(data interface{}, hasIndent bool, hasEncode ...bool) { + accept := output.Context.Input.Header("Accept") + switch accept { + case ApplicationYAML: + output.YAML(data) + case ApplicationXML, TextXML: + output.XML(data, hasIndent) + default: + output.JSON(data, hasIndent, len(hasEncode) > 0 && hasEncode[0]) + } +} + // Download forces response for download file. // it prepares the download response header automatically. func (output *BeegoOutput) Download(file string, filename ...string) { @@ -260,7 +288,20 @@ func (output *BeegoOutput) Download(file string, filename ...string) { } else { fName = filepath.Base(file) } - output.Header("Content-Disposition", "attachment; filename="+url.QueryEscape(fName)) + //https://tools.ietf.org/html/rfc6266#section-4.3 + fn := url.PathEscape(fName) + if fName == fn { + fn = "filename=" + fn + } else { + /** + The parameters "filename" and "filename*" differ only in that + "filename*" uses the encoding defined in [RFC5987], allowing the use + of characters not present in the ISO-8859-1 character set + ([ISO-8859-1]). + */ + fn = "filename=" + fName + "; filename*=utf-8''" + fn + } + output.Header("Content-Disposition", "attachment; "+fn) output.Header("Content-Description", "File Transfer") output.Header("Content-Type", "application/octet-stream") output.Header("Content-Transfer-Encoding", "binary") @@ -325,13 +366,13 @@ func (output *BeegoOutput) IsForbidden() bool { } // IsNotFound returns boolean of this request is not found. -// HTTP 404 means forbidden. +// HTTP 404 means not found. func (output *BeegoOutput) IsNotFound() bool { return output.Status == 404 } // IsClientError returns boolean of this request client sends error data. -// HTTP 4xx means forbidden. +// HTTP 4xx means client error. func (output *BeegoOutput) IsClientError() bool { return output.Status >= 400 && output.Status < 500 } @@ -350,6 +391,11 @@ func stringsToJSON(str string) string { jsons.WriteRune(r) } else { jsons.WriteString("\\u") + if rint < 0x100 { + jsons.WriteString("00") + } else if rint < 0x1000 { + jsons.WriteString("0") + } jsons.WriteString(strconv.FormatInt(int64(rint), 16)) } } diff --git a/src/vendor/github.com/astaxie/beego/context/param/options.go b/src/vendor/github.com/astaxie/beego/context/param/options.go index 58bdc3d03..3d5ba013e 100644 --- a/src/vendor/github.com/astaxie/beego/context/param/options.go +++ b/src/vendor/github.com/astaxie/beego/context/param/options.go @@ -7,7 +7,7 @@ import ( // MethodParamOption defines a func which apply options on a MethodParam type MethodParamOption func(*MethodParam) -// IsRequired indicates that this param is required and can not be ommited from the http request +// IsRequired indicates that this param is required and can not be omitted from the http request var IsRequired MethodParamOption = func(p *MethodParam) { p.required = true } diff --git a/src/vendor/github.com/astaxie/beego/controller.go b/src/vendor/github.com/astaxie/beego/controller.go index c104eb2a2..0e8853b31 100644 --- a/src/vendor/github.com/astaxie/beego/controller.go +++ b/src/vendor/github.com/astaxie/beego/controller.go @@ -17,6 +17,7 @@ package beego import ( "bytes" "errors" + "fmt" "html/template" "io" "mime/multipart" @@ -32,24 +33,44 @@ import ( "github.com/astaxie/beego/session" ) -//commonly used mime-types -const ( - applicationJSON = "application/json" - applicationXML = "application/xml" - textXML = "text/xml" -) - var ( // ErrAbort custom error when user stop request handler manually. - ErrAbort = errors.New("User stop run") + ErrAbort = errors.New("user stop run") // GlobalControllerRouter store comments with controller. pkgpath+controller:comments GlobalControllerRouter = make(map[string][]ControllerComments) ) +// ControllerFilter store the filter for controller +type ControllerFilter struct { + Pattern string + Pos int + Filter FilterFunc + ReturnOnOutput bool + ResetParams bool +} + +// ControllerFilterComments store the comment for controller level filter +type ControllerFilterComments struct { + Pattern string + Pos int + Filter string // NOQA + ReturnOnOutput bool + ResetParams bool +} + +// ControllerImportComments store the import comment for controller needed +type ControllerImportComments struct { + ImportPath string + ImportAlias string +} + // ControllerComments store the comment for the controller method type ControllerComments struct { Method string Router string + Filters []*ControllerFilter + ImportComments []*ControllerImportComments + FilterComments []*ControllerFilterComments AllowHTTPMethods []string Params []map[string]string MethodParams []*param.MethodParam @@ -73,7 +94,6 @@ type Controller struct { controllerName string actionName string methodMapping map[string]func() //method:routertree - gotofunc string AppController interface{} // template data @@ -105,6 +125,7 @@ type ControllerInterface interface { Head() Patch() Options() + Trace() Finish() Render() error XSRFToken() string @@ -136,37 +157,59 @@ func (c *Controller) Finish() {} // Get adds a request function to handle GET request. func (c *Controller) Get() { - http.Error(c.Ctx.ResponseWriter, "Method Not Allowed", 405) + http.Error(c.Ctx.ResponseWriter, "Method Not Allowed", http.StatusMethodNotAllowed) } // Post adds a request function to handle POST request. func (c *Controller) Post() { - http.Error(c.Ctx.ResponseWriter, "Method Not Allowed", 405) + http.Error(c.Ctx.ResponseWriter, "Method Not Allowed", http.StatusMethodNotAllowed) } // Delete adds a request function to handle DELETE request. func (c *Controller) Delete() { - http.Error(c.Ctx.ResponseWriter, "Method Not Allowed", 405) + http.Error(c.Ctx.ResponseWriter, "Method Not Allowed", http.StatusMethodNotAllowed) } // Put adds a request function to handle PUT request. func (c *Controller) Put() { - http.Error(c.Ctx.ResponseWriter, "Method Not Allowed", 405) + http.Error(c.Ctx.ResponseWriter, "Method Not Allowed", http.StatusMethodNotAllowed) } // Head adds a request function to handle HEAD request. func (c *Controller) Head() { - http.Error(c.Ctx.ResponseWriter, "Method Not Allowed", 405) + http.Error(c.Ctx.ResponseWriter, "Method Not Allowed", http.StatusMethodNotAllowed) } // Patch adds a request function to handle PATCH request. func (c *Controller) Patch() { - http.Error(c.Ctx.ResponseWriter, "Method Not Allowed", 405) + http.Error(c.Ctx.ResponseWriter, "Method Not Allowed", http.StatusMethodNotAllowed) } // Options adds a request function to handle OPTIONS request. func (c *Controller) Options() { - http.Error(c.Ctx.ResponseWriter, "Method Not Allowed", 405) + http.Error(c.Ctx.ResponseWriter, "Method Not Allowed", http.StatusMethodNotAllowed) +} + +// Trace adds a request function to handle Trace request. +// this method SHOULD NOT be overridden. +// https://tools.ietf.org/html/rfc7231#section-4.3.8 +// The TRACE method requests a remote, application-level loop-back of +// the request message. The final recipient of the request SHOULD +// reflect the message received, excluding some fields described below, +// back to the client as the message body of a 200 (OK) response with a +// Content-Type of "message/http" (Section 8.3.1 of [RFC7230]). +func (c *Controller) Trace() { + ts := func(h http.Header) (hs string) { + for k, v := range h { + hs += fmt.Sprintf("\r\n%s: %s", k, v) + } + return + } + hs := fmt.Sprintf("\r\nTRACE %s %s%s\r\n", c.Ctx.Request.RequestURI, c.Ctx.Request.Proto, ts(c.Ctx.Request.Header)) + c.Ctx.Output.Header("Content-Type", "message/http") + c.Ctx.Output.Header("Content-Length", fmt.Sprint(len(hs))) + c.Ctx.Output.Header("Cache-Control", "no-cache, no-store, must-revalidate") + c.Ctx.WriteString(hs) } // HandlerFunc call function with the name @@ -272,9 +315,23 @@ func (c *Controller) viewPath() string { // Redirect sends the redirection response to url with status code. func (c *Controller) Redirect(url string, code int) { + LogAccess(c.Ctx, nil, code) c.Ctx.Redirect(code, url) } +// SetData set the data depending on the accepted +func (c *Controller) SetData(data interface{}) { + accept := c.Ctx.Input.Header("Accept") + switch accept { + case context.ApplicationYAML: + c.Data["yaml"] = data + case context.ApplicationXML, context.TextXML: + c.Data["xml"] = data + default: + c.Data["json"] = data + } +} + // Abort stops controller handler and show the error data if code is defined in ErrorMap or code string. func (c *Controller) Abort(code string) { status, err := strconv.Atoi(code) @@ -317,47 +374,35 @@ func (c *Controller) URLFor(endpoint string, values ...interface{}) string { // ServeJSON sends a json response with encoding charset. func (c *Controller) ServeJSON(encoding ...bool) { var ( - hasIndent = true - hasEncoding = false + hasIndent = BConfig.RunMode != PROD + hasEncoding = len(encoding) > 0 && encoding[0] ) - if BConfig.RunMode == PROD { - hasIndent = false - } - if len(encoding) > 0 && encoding[0] { - hasEncoding = true - } + c.Ctx.Output.JSON(c.Data["json"], hasIndent, hasEncoding) } // ServeJSONP sends a jsonp response. func (c *Controller) ServeJSONP() { - hasIndent := true - if BConfig.RunMode == PROD { - hasIndent = false - } + hasIndent := BConfig.RunMode != PROD c.Ctx.Output.JSONP(c.Data["jsonp"], hasIndent) } // ServeXML sends xml response. func (c *Controller) ServeXML() { - hasIndent := true - if BConfig.RunMode == PROD { - hasIndent = false - } + hasIndent := BConfig.RunMode != PROD c.Ctx.Output.XML(c.Data["xml"], hasIndent) } -// ServeFormatted serve Xml OR Json, depending on the value of the Accept header -func (c *Controller) ServeFormatted() { - accept := c.Ctx.Input.Header("Accept") - switch accept { - case applicationJSON: - c.ServeJSON() - case applicationXML, textXML: - c.ServeXML() - default: - c.ServeJSON() - } +// ServeYAML sends yaml response. +func (c *Controller) ServeYAML() { + c.Ctx.Output.YAML(c.Data["yaml"]) +} + +// ServeFormatted serve YAML, XML OR JSON, depending on the value of the Accept header +func (c *Controller) ServeFormatted(encoding ...bool) { + hasIndent := BConfig.RunMode != PROD + hasEncoding := len(encoding) > 0 && encoding[0] + c.Ctx.Output.ServeFormatted(c.Data, hasIndent, hasEncoding) } // Input returns the input data map from POST or PUT request body and query string. diff --git a/src/vendor/github.com/astaxie/beego/error.go b/src/vendor/github.com/astaxie/beego/error.go index b913db39d..e5e9fd474 100644 --- a/src/vendor/github.com/astaxie/beego/error.go +++ b/src/vendor/github.com/astaxie/beego/error.go @@ -28,7 +28,7 @@ import ( ) const ( - errorTypeHandler = iota + errorTypeHandler = iota errorTypeController ) @@ -93,11 +93,6 @@ func showErr(err interface{}, ctx *context.Context, stack string) { "BeegoVersion": VERSION, "GoVersion": runtime.Version(), } - if ctx.Output.Status != 0 { - ctx.ResponseWriter.WriteHeader(ctx.Output.Status) - } else { - ctx.ResponseWriter.WriteHeader(500) - } t.Execute(ctx.ResponseWriter, data) } @@ -366,7 +361,7 @@ func gatewayTimeout(rw http.ResponseWriter, r *http.Request) { func responseError(rw http.ResponseWriter, r *http.Request, errCode int, errContent string) { t, _ := template.New("beegoerrortemp").Parse(errtpl) - data := map[string]interface{}{ + data := M{ "Title": http.StatusText(errCode), "BeegoVersion": VERSION, "Content": template.HTML(errContent), @@ -439,6 +434,9 @@ func exception(errCode string, ctx *context.Context) { } func executeError(err *errorInfo, ctx *context.Context, code int) { + //make sure to log the error in the access log + LogAccess(ctx, nil, code) + if err.errorType == errorTypeHandler { ctx.ResponseWriter.WriteHeader(code) err.handler(ctx.ResponseWriter, ctx.Request) diff --git a/src/vendor/github.com/astaxie/beego/fs.go b/src/vendor/github.com/astaxie/beego/fs.go new file mode 100644 index 000000000..41cc6f6e0 --- /dev/null +++ b/src/vendor/github.com/astaxie/beego/fs.go @@ -0,0 +1,74 @@ +package beego + +import ( + "net/http" + "os" + "path/filepath" +) + +type FileSystem struct { +} + +func (d FileSystem) Open(name string) (http.File, error) { + return os.Open(name) +} + +// Walk walks the file tree rooted at root in filesystem, calling walkFn for each file or +// directory in the tree, including root. All errors that arise visiting files +// and directories are filtered by walkFn. +func Walk(fs http.FileSystem, root string, walkFn filepath.WalkFunc) error { + + f, err := fs.Open(root) + if err != nil { + return err + } + info, err := f.Stat() + if err != nil { + err = walkFn(root, nil, err) + } else { + err = walk(fs, root, info, walkFn) + } + if err == filepath.SkipDir { + return nil + } + return err +} + +// walk recursively descends path, calling walkFn. +func walk(fs http.FileSystem, path string, info os.FileInfo, walkFn filepath.WalkFunc) error { + var err error + if !info.IsDir() { + return walkFn(path, info, nil) + } + + dir, err := fs.Open(path) + if err != nil { + if err1 := walkFn(path, info, err); err1 != nil { + return err1 + } + return err + } + defer dir.Close() + dirs, err := dir.Readdir(-1) + err1 := walkFn(path, info, err) + // If err != nil, walk can't walk into this directory. + // err1 != nil means walkFn want walk to skip this directory or stop walking. + // Therefore, if one of err and err1 isn't nil, walk will return. + if err != nil || err1 != nil { + // The caller's behavior is controlled by the return value, which is decided + // by walkFn. walkFn may ignore err and return nil. + // If walkFn returns SkipDir, it will be handled by the caller. + // So walk should return whatever walkFn returns. + return err1 + } + + for _, fileInfo := range dirs { + filename := filepath.Join(path, fileInfo.Name()) + if err = walk(fs, filename, fileInfo, walkFn); err != nil { + if !fileInfo.IsDir() || err != filepath.SkipDir { + return err + } + } + } + return nil +} diff --git a/src/vendor/github.com/astaxie/beego/go.mod b/src/vendor/github.com/astaxie/beego/go.mod new file mode 100644 index 000000000..fbdec124d --- /dev/null +++ b/src/vendor/github.com/astaxie/beego/go.mod @@ -0,0 +1,39 @@ +module github.com/astaxie/beego + +require ( + github.com/Knetic/govaluate v3.0.0+incompatible // indirect + github.com/OwnLocal/goes v1.0.0 + github.com/beego/goyaml2 v0.0.0-20130207012346-5545475820dd + github.com/beego/x2j v0.0.0-20131220205130-a0352aadc542 + github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737 + github.com/casbin/casbin v1.7.0 + github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 + github.com/couchbase/go-couchbase v0.0.0-20181122212707-3e9b6e1258bb + github.com/couchbase/gomemcached v0.0.0-20181122193126-5125a94a666c // indirect + github.com/couchbase/goutils v0.0.0-20180530154633-e865a1461c8a // indirect + github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76 // indirect + github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 // indirect + github.com/elazarl/go-bindata-assetfs v1.0.0 + github.com/go-redis/redis v6.14.2+incompatible + github.com/go-sql-driver/mysql v1.4.1 + github.com/gogo/protobuf v1.1.1 + github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect + github.com/gomodule/redigo v2.0.0+incompatible + github.com/lib/pq v1.0.0 + github.com/mattn/go-sqlite3 v1.10.0 + github.com/pelletier/go-toml v1.2.0 // indirect + github.com/pkg/errors v0.8.0 // indirect + github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 // indirect + github.com/siddontang/ledisdb v0.0.0-20181029004158-becf5f38d373 + github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d // indirect + github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec + github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c // indirect + github.com/wendal/errors v0.0.0-20130201093226-f66c77a7882b // indirect + golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85 + golang.org/x/net v0.0.0-20181114220301-adae6a3d119a // indirect + gopkg.in/yaml.v2 v2.2.1 +) + +replace golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85 => github.com/golang/crypto v0.0.0-20181127143415-eb0de9b17e85 + +replace gopkg.in/yaml.v2 v2.2.1 => github.com/go-yaml/yaml v0.0.0-20180328195020-5420a8b6744d diff --git a/src/vendor/github.com/astaxie/beego/go.sum b/src/vendor/github.com/astaxie/beego/go.sum new file mode 100644 index 000000000..ab2331621 --- /dev/null +++ b/src/vendor/github.com/astaxie/beego/go.sum @@ -0,0 +1,68 @@ +github.com/Knetic/govaluate v3.0.0+incompatible h1:7o6+MAPhYTCF0+fdvoz1xDedhRb4f6s9Tn1Tt7/WTEg= +github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/OwnLocal/goes v1.0.0/go.mod h1:8rIFjBGTue3lCU0wplczcUgt9Gxgrkkrw7etMIcn8TM= +github.com/beego/goyaml2 v0.0.0-20130207012346-5545475820dd h1:jZtX5jh5IOMu0fpOTC3ayh6QGSPJ/KWOv1lgPvbRw1M= +github.com/beego/goyaml2 v0.0.0-20130207012346-5545475820dd/go.mod h1:1b+Y/CofkYwXMUU0OhQqGvsY2Bvgr4j6jfT699wyZKQ= +github.com/beego/x2j v0.0.0-20131220205130-a0352aadc542 h1:nYXb+3jF6Oq/j8R/y90XrKpreCxIalBWfeyeKymgOPk= +github.com/beego/x2j v0.0.0-20131220205130-a0352aadc542/go.mod h1:kSeGC/p1AbBiEp5kat81+DSQrZenVBZXklMLaELspWU= +github.com/belogik/goes v0.0.0-20151229125003-e54d722c3aff h1:/kO0p2RTGLB8R5gub7ps0GmYpB2O8LXEoPq8tzFDCUI= +github.com/belogik/goes v0.0.0-20151229125003-e54d722c3aff/go.mod h1:PhH1ZhyCzHKt4uAasyx+ljRCgoezetRNf59CUtwUkqY= +github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737 h1:rRISKWyXfVxvoa702s91Zl5oREZTrR3yv+tXrrX7G/g= +github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60= +github.com/casbin/casbin v1.7.0 h1:PuzlE8w0JBg/DhIqnkF1Dewf3z+qmUZMVN07PonvVUQ= +github.com/casbin/casbin v1.7.0/go.mod h1:c67qKN6Oum3UF5Q1+BByfFxkwKvhwW57ITjqwtzR1KE= +github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 h1:F1EaeKL/ta07PY/k9Os/UFtwERei2/XzGemhpGnBKNg= +github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= +github.com/couchbase/go-couchbase v0.0.0-20181122212707-3e9b6e1258bb h1:w3RapLhkA5+km9Z8vUkC6VCaskduJXvXwJg5neKnfDU= +github.com/couchbase/go-couchbase v0.0.0-20181122212707-3e9b6e1258bb/go.mod h1:TWI8EKQMs5u5jLKW/tsb9VwauIrMIxQG1r5fMsswK5U= +github.com/couchbase/gomemcached v0.0.0-20181122193126-5125a94a666c h1:K4FIibkr4//ziZKOKmt4RL0YImuTjLLBtwElf+F2lSQ= +github.com/couchbase/gomemcached v0.0.0-20181122193126-5125a94a666c/go.mod h1:srVSlQLB8iXBVXHgnqemxUXqN6FCvClgCMPCsjBDR7c= +github.com/couchbase/goutils v0.0.0-20180530154633-e865a1461c8a h1:Y5XsLCEhtEI8qbD9RP3Qlv5FXdTDHxZM9UPUnMRgBp8= +github.com/couchbase/goutils v0.0.0-20180530154633-e865a1461c8a/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs= +github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76 h1:Lgdd/Qp96Qj8jqLpq2cI1I1X7BJnu06efS+XkhRoLUQ= +github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76/go.mod h1:vYwsqCOLxGiisLwp9rITslkFNpZD5rz43tf41QFkTWY= +github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 h1:aaQcKT9WumO6JEJcRyTqFVq4XUZiUcKR2/GI31TOcz8= +github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk= +github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= +github.com/go-redis/redis v6.14.2+incompatible h1:UE9pLhzmWf+xHNmZsoccjXosPicuiNaInPgym8nzfg0= +github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-yaml/yaml v0.0.0-20180328195020-5420a8b6744d h1:xy93KVe+KrIIwWDEAfQBdIfsiHJkepbYsDr+VY3g9/o= +github.com/go-yaml/yaml v0.0.0-20180328195020-5420a8b6744d/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/crypto v0.0.0-20181127143415-eb0de9b17e85 h1:B7ZbAFz7NOmvpUE5RGtu3u0WIizy5GdvbNpEf4RPnWs= +github.com/golang/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:uZvAcrsnNaCxlh1HorK5dUQHGmEKPh2H/Rl1kehswPo= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= +github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= +github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 h1:xT+JlYxNGqyT+XcU8iUrN18JYed2TvG9yN5ULG2jATM= +github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw= +github.com/siddontang/ledisdb v0.0.0-20181029004158-becf5f38d373 h1:p6IxqQMjab30l4lb9mmkIkkcE1yv6o0SKbPhW5pxqHI= +github.com/siddontang/ledisdb v0.0.0-20181029004158-becf5f38d373/go.mod h1:mF1DpOSOUiJRMR+FDqaqu3EBqrybQtrDDszLUZ6oxPg= +github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d h1:NVwnfyR3rENtlz62bcrkXME3INVUa4lcdGt+opvxExs= +github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z92TR1JKMkLLoaOQk++LVnOKL3ScbJ8GNGA= +github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec h1:q6XVwXmKvCRHRqesF3cSv6lNqqHi0QWOvgDlSohg8UA= +github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec/go.mod h1:QBvMkMya+gXctz3kmljlUCu/yB3GZ6oee+dUozsezQE= +github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c h1:3eGShk3EQf5gJCYW+WzA0TEJQd37HLOmlYF7N0YJwv0= +github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0= +github.com/wendal/errors v0.0.0-20130201093226-f66c77a7882b h1:0Ve0/CCjiAiyKddUMUn3RwIGlq2iTW4GuVzyoKBYO/8= +github.com/wendal/errors v0.0.0-20130201093226-f66c77a7882b/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc= +golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85 h1:et7+NAX3lLIk5qUCTA9QelBjGE/NkhzYw/mhnr0s7nI= +golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a h1:gOpx8G595UYyvj8UK4+OFyY4rx037g3fmfhe5SasG3U= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/src/vendor/github.com/astaxie/beego/grace/conn.go b/src/vendor/github.com/astaxie/beego/grace/conn.go deleted file mode 100644 index e020f8507..000000000 --- a/src/vendor/github.com/astaxie/beego/grace/conn.go +++ /dev/null @@ -1,39 +0,0 @@ -package grace - -import ( - "errors" - "net" - "sync" -) - -type graceConn struct { - net.Conn - server *Server - m sync.Mutex - closed bool -} - -func (c *graceConn) Close() (err error) { - defer func() { - if r := recover(); r != nil { - switch x := r.(type) { - case string: - err = errors.New(x) - case error: - err = x - default: - err = errors.New("Unknown panic") - } - } - }() - - c.m.Lock() - if c.closed { - c.m.Unlock() - return - } - c.server.wg.Done() - c.closed = true - c.m.Unlock() - return c.Conn.Close() -} diff --git a/src/vendor/github.com/astaxie/beego/grace/grace.go b/src/vendor/github.com/astaxie/beego/grace/grace.go index 6ebf8455f..fb0cb7bb6 100644 --- a/src/vendor/github.com/astaxie/beego/grace/grace.go +++ b/src/vendor/github.com/astaxie/beego/grace/grace.go @@ -78,7 +78,7 @@ var ( DefaultReadTimeOut time.Duration // DefaultWriteTimeOut is the HTTP Write timeout DefaultWriteTimeOut time.Duration - // DefaultMaxHeaderBytes is the Max HTTP Herder size, default is 0, no limit + // DefaultMaxHeaderBytes is the Max HTTP Header size, default is 0, no limit DefaultMaxHeaderBytes int // DefaultTimeout is the shutdown server's timeout. default is 60s DefaultTimeout = 60 * time.Second @@ -122,7 +122,6 @@ func NewServer(addr string, handler http.Handler) (srv *Server) { } srv = &Server{ - wg: sync.WaitGroup{}, sigChan: make(chan os.Signal), isChild: isChild, SignalHooks: map[int]map[os.Signal][]func(){ @@ -137,20 +136,21 @@ func NewServer(addr string, handler http.Handler) (srv *Server) { syscall.SIGTERM: {}, }, }, - state: StateInit, - Network: "tcp", + state: StateInit, + Network: "tcp", + terminalChan: make(chan error), //no cache channel + } + srv.Server = &http.Server{ + Addr: addr, + ReadTimeout: DefaultReadTimeOut, + WriteTimeout: DefaultWriteTimeOut, + MaxHeaderBytes: DefaultMaxHeaderBytes, + Handler: handler, } - srv.Server = &http.Server{} - srv.Server.Addr = addr - srv.Server.ReadTimeout = DefaultReadTimeOut - srv.Server.WriteTimeout = DefaultWriteTimeOut - srv.Server.MaxHeaderBytes = DefaultMaxHeaderBytes - srv.Server.Handler = handler runningServersOrder = append(runningServersOrder, addr) runningServers[addr] = srv - - return + return srv } // ListenAndServe refer http.ListenAndServe diff --git a/src/vendor/github.com/astaxie/beego/grace/listener.go b/src/vendor/github.com/astaxie/beego/grace/listener.go deleted file mode 100644 index 7ede63a30..000000000 --- a/src/vendor/github.com/astaxie/beego/grace/listener.go +++ /dev/null @@ -1,62 +0,0 @@ -package grace - -import ( - "net" - "os" - "syscall" - "time" -) - -type graceListener struct { - net.Listener - stop chan error - stopped bool - server *Server -} - -func newGraceListener(l net.Listener, srv *Server) (el *graceListener) { - el = &graceListener{ - Listener: l, - stop: make(chan error), - server: srv, - } - go func() { - <-el.stop - el.stopped = true - el.stop <- el.Listener.Close() - }() - return -} - -func (gl *graceListener) Accept() (c net.Conn, err error) { - tc, err := gl.Listener.(*net.TCPListener).AcceptTCP() - if err != nil { - return - } - - tc.SetKeepAlive(true) - tc.SetKeepAlivePeriod(3 * time.Minute) - - c = &graceConn{ - Conn: tc, - server: gl.server, - } - - gl.server.wg.Add(1) - return -} - -func (gl *graceListener) Close() error { - if gl.stopped { - return syscall.EINVAL - } - gl.stop <- nil - return <-gl.stop -} - -func (gl *graceListener) File() *os.File { - // returns a dup(2) - FD_CLOEXEC flag *not* set - tl := gl.Listener.(*net.TCPListener) - fl, _ := tl.File() - return fl -} diff --git a/src/vendor/github.com/astaxie/beego/grace/server.go b/src/vendor/github.com/astaxie/beego/grace/server.go index b82423353..1ce8bc782 100644 --- a/src/vendor/github.com/astaxie/beego/grace/server.go +++ b/src/vendor/github.com/astaxie/beego/grace/server.go @@ -1,8 +1,11 @@ package grace import ( + "context" "crypto/tls" + "crypto/x509" "fmt" + "io/ioutil" "log" "net" "net/http" @@ -10,7 +13,6 @@ import ( "os/exec" "os/signal" "strings" - "sync" "syscall" "time" ) @@ -18,14 +20,13 @@ import ( // Server embedded http.Server type Server struct { *http.Server - GraceListener net.Listener - SignalHooks map[int]map[os.Signal][]func() - tlsInnerListener *graceListener - wg sync.WaitGroup - sigChan chan os.Signal - isChild bool - state uint8 - Network string + ln net.Listener + SignalHooks map[int]map[os.Signal][]func() + sigChan chan os.Signal + isChild bool + state uint8 + Network string + terminalChan chan error } // Serve accepts incoming connections on the Listener l, @@ -33,11 +34,19 @@ type Server struct { // The service goroutines read requests and then call srv.Handler to reply to them. func (srv *Server) Serve() (err error) { srv.state = StateRunning - err = srv.Server.Serve(srv.GraceListener) - log.Println(syscall.Getpid(), "Waiting for connections to finish...") - srv.wg.Wait() - srv.state = StateTerminate - return + defer func() { srv.state = StateTerminate }() + + // When Shutdown is called, Serve, ListenAndServe, and ListenAndServeTLS + // immediately return ErrServerClosed. Make sure the program doesn't exit + // and waits instead for Shutdown to return. + if err = srv.Server.Serve(srv.ln); err != nil && err != http.ErrServerClosed { + log.Println(syscall.Getpid(), "Server.Serve() error:", err) + return err + } + + log.Println(syscall.Getpid(), srv.ln.Addr(), "Listener closed.") + // wait for Shutdown to return + return <-srv.terminalChan } // ListenAndServe listens on the TCP network address srv.Addr and then calls Serve @@ -51,21 +60,19 @@ func (srv *Server) ListenAndServe() (err error) { go srv.handleSignals() - l, err := srv.getListener(addr) + srv.ln, err = srv.getListener(addr) if err != nil { log.Println(err) return err } - srv.GraceListener = newGraceListener(l, srv) - if srv.isChild { process, err := os.FindProcess(os.Getppid()) if err != nil { log.Println(err) return err } - err = process.Kill() + err = process.Signal(syscall.SIGTERM) if err != nil { return err } @@ -105,14 +112,67 @@ func (srv *Server) ListenAndServeTLS(certFile, keyFile string) (err error) { go srv.handleSignals() - l, err := srv.getListener(addr) + ln, err := srv.getListener(addr) if err != nil { log.Println(err) return err } + srv.ln = tls.NewListener(tcpKeepAliveListener{ln.(*net.TCPListener)}, srv.TLSConfig) - srv.tlsInnerListener = newGraceListener(l, srv) - srv.GraceListener = tls.NewListener(srv.tlsInnerListener, srv.TLSConfig) + if srv.isChild { + process, err := os.FindProcess(os.Getppid()) + if err != nil { + log.Println(err) + return err + } + err = process.Signal(syscall.SIGTERM) + if err != nil { + return err + } + } + + log.Println(os.Getpid(), srv.Addr) + return srv.Serve() +} + +// ListenAndServeMutualTLS listens on the TCP network address srv.Addr and then calls +// Serve to handle requests on incoming mutual TLS connections. +func (srv *Server) ListenAndServeMutualTLS(certFile, keyFile, trustFile string) (err error) { + addr := srv.Addr + if addr == "" { + addr = ":https" + } + + if srv.TLSConfig == nil { + srv.TLSConfig = &tls.Config{} + } + if srv.TLSConfig.NextProtos == nil { + srv.TLSConfig.NextProtos = []string{"http/1.1"} + } + + srv.TLSConfig.Certificates = make([]tls.Certificate, 1) + srv.TLSConfig.Certificates[0], err = tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return + } + srv.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert + pool := x509.NewCertPool() + data, err := ioutil.ReadFile(trustFile) + if err != nil { + log.Println(err) + return err + } + pool.AppendCertsFromPEM(data) + srv.TLSConfig.ClientCAs = pool + log.Println("Mutual HTTPS") + go srv.handleSignals() + + ln, err := srv.getListener(addr) + if err != nil { + log.Println(err) + return err + } + srv.ln = tls.NewListener(tcpKeepAliveListener{ln.(*net.TCPListener)}, srv.TLSConfig) if srv.isChild { process, err := os.FindProcess(os.Getppid()) @@ -125,6 +185,7 @@ func (srv *Server) ListenAndServeTLS(certFile, keyFile string) (err error) { return err } } + log.Println(os.Getpid(), srv.Addr) return srv.Serve() } @@ -155,6 +216,20 @@ func (srv *Server) getListener(laddr string) (l net.Listener, err error) { return } +type tcpKeepAliveListener struct { + *net.TCPListener +} + +func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { + tc, err := ln.AcceptTCP() + if err != nil { + return + } + tc.SetKeepAlive(true) + tc.SetKeepAlivePeriod(3 * time.Minute) + return tc, nil +} + // handleSignals listens for os Signals and calls any hooked in function that the // user had registered with the signal. func (srv *Server) handleSignals() { @@ -207,37 +282,14 @@ func (srv *Server) shutdown() { } srv.state = StateShuttingDown + log.Println(syscall.Getpid(), "Waiting for connections to finish...") + ctx := context.Background() if DefaultTimeout >= 0 { - go srv.serverTimeout(DefaultTimeout) - } - err := srv.GraceListener.Close() - if err != nil { - log.Println(syscall.Getpid(), "Listener.Close() error:", err) - } else { - log.Println(syscall.Getpid(), srv.GraceListener.Addr(), "Listener closed.") - } -} - -// serverTimeout forces the server to shutdown in a given timeout - whether it -// finished outstanding requests or not. if Read/WriteTimeout are not set or the -// max header size is very big a connection could hang -func (srv *Server) serverTimeout(d time.Duration) { - defer func() { - if r := recover(); r != nil { - log.Println("WaitGroup at 0", r) - } - }() - if srv.state != StateShuttingDown { - return - } - time.Sleep(d) - log.Println("[STOP - Hammer Time] Forcefully shutting down parent") - for { - if srv.state == StateTerminate { - break - } - srv.wg.Done() + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(context.Background(), DefaultTimeout) + defer cancel() } + srv.terminalChan <- srv.Server.Shutdown(ctx) } func (srv *Server) fork() (err error) { @@ -251,12 +303,8 @@ func (srv *Server) fork() (err error) { var files = make([]*os.File, len(runningServers)) var orderArgs = make([]string, len(runningServers)) for _, srvPtr := range runningServers { - switch srvPtr.GraceListener.(type) { - case *graceListener: - files[socketPtrOffsetMap[srvPtr.Server.Addr]] = srvPtr.GraceListener.(*graceListener).File() - default: - files[socketPtrOffsetMap[srvPtr.Server.Addr]] = srvPtr.tlsInnerListener.File() - } + f, _ := srvPtr.ln.(*net.TCPListener).File() + files[socketPtrOffsetMap[srvPtr.Server.Addr]] = f orderArgs[socketPtrOffsetMap[srvPtr.Server.Addr]] = srvPtr.Server.Addr } diff --git a/src/vendor/github.com/astaxie/beego/hooks.go b/src/vendor/github.com/astaxie/beego/hooks.go index c5ec8e2dd..b8671d353 100644 --- a/src/vendor/github.com/astaxie/beego/hooks.go +++ b/src/vendor/github.com/astaxie/beego/hooks.go @@ -11,7 +11,7 @@ import ( "github.com/astaxie/beego/session" ) -// +// register MIME type with content type func registerMime() error { for k, v := range mimemaps { mime.AddExtensionType(k, v) diff --git a/src/vendor/github.com/astaxie/beego/log.go b/src/vendor/github.com/astaxie/beego/log.go index e9412f920..cc4c0f81a 100644 --- a/src/vendor/github.com/astaxie/beego/log.go +++ b/src/vendor/github.com/astaxie/beego/log.go @@ -21,6 +21,7 @@ import ( ) // Log levels to control the logging output. +// Deprecated: use github.com/astaxie/beego/logs instead. const ( LevelEmergency = iota LevelAlert @@ -33,75 +34,90 @@ const ( ) // BeeLogger references the used application logger. +// Deprecated: use github.com/astaxie/beego/logs instead. var BeeLogger = logs.GetBeeLogger() // SetLevel sets the global log level used by the simple logger. +// Deprecated: use github.com/astaxie/beego/logs instead. func SetLevel(l int) { logs.SetLevel(l) } // SetLogFuncCall set the CallDepth, default is 3 +// Deprecated: use github.com/astaxie/beego/logs instead. func SetLogFuncCall(b bool) { logs.SetLogFuncCall(b) } // SetLogger sets a new logger. +// Deprecated: use github.com/astaxie/beego/logs instead. func SetLogger(adaptername string, config string) error { return logs.SetLogger(adaptername, config) } // Emergency logs a message at emergency level. +// Deprecated: use github.com/astaxie/beego/logs instead. func Emergency(v ...interface{}) { logs.Emergency(generateFmtStr(len(v)), v...) } // Alert logs a message at alert level. +// Deprecated: use github.com/astaxie/beego/logs instead. func Alert(v ...interface{}) { logs.Alert(generateFmtStr(len(v)), v...) } // Critical logs a message at critical level. +// Deprecated: use github.com/astaxie/beego/logs instead. func Critical(v ...interface{}) { logs.Critical(generateFmtStr(len(v)), v...) } // Error logs a message at error level. +// Deprecated: use github.com/astaxie/beego/logs instead. func Error(v ...interface{}) { logs.Error(generateFmtStr(len(v)), v...) } // Warning logs a message at warning level. +// Deprecated: use github.com/astaxie/beego/logs instead. func Warning(v ...interface{}) { logs.Warning(generateFmtStr(len(v)), v...) } // Warn compatibility alias for Warning() +// Deprecated: use github.com/astaxie/beego/logs instead. func Warn(v ...interface{}) { logs.Warn(generateFmtStr(len(v)), v...) } // Notice logs a message at notice level. +// Deprecated: use github.com/astaxie/beego/logs instead. func Notice(v ...interface{}) { logs.Notice(generateFmtStr(len(v)), v...) } // Informational logs a message at info level. +// Deprecated: use github.com/astaxie/beego/logs instead. func Informational(v ...interface{}) { logs.Informational(generateFmtStr(len(v)), v...) } // Info compatibility alias for Warning() +// Deprecated: use github.com/astaxie/beego/logs instead. func Info(v ...interface{}) { logs.Info(generateFmtStr(len(v)), v...) } // Debug logs a message at debug level. +// Deprecated: use github.com/astaxie/beego/logs instead. func Debug(v ...interface{}) { logs.Debug(generateFmtStr(len(v)), v...) } // Trace logs a message at trace level. // compatibility alias for Warning() +// Deprecated: use github.com/astaxie/beego/logs instead. func Trace(v ...interface{}) { logs.Trace(generateFmtStr(len(v)), v...) } diff --git a/src/vendor/github.com/astaxie/beego/logs/README.md b/src/vendor/github.com/astaxie/beego/logs/README.md index 57d7abc39..c05bcc044 100644 --- a/src/vendor/github.com/astaxie/beego/logs/README.md +++ b/src/vendor/github.com/astaxie/beego/logs/README.md @@ -16,48 +16,57 @@ As of now this logs support console, file,smtp and conn. First you must import it - import ( - "github.com/astaxie/beego/logs" - ) +```golang +import ( + "github.com/astaxie/beego/logs" +) +``` Then init a Log (example with console adapter) - log := NewLogger(10000) - log.SetLogger("console", "") +```golang +log := logs.NewLogger(10000) +log.SetLogger("console", "") +``` > the first params stand for how many channel -Use it like this: - - log.Trace("trace") - log.Info("info") - log.Warn("warning") - log.Debug("debug") - log.Critical("critical") +Use it like this: +```golang +log.Trace("trace") +log.Info("info") +log.Warn("warning") +log.Debug("debug") +log.Critical("critical") +``` ## File adapter Configure file adapter like this: - log := NewLogger(10000) - log.SetLogger("file", `{"filename":"test.log"}`) - +```golang +log := NewLogger(10000) +log.SetLogger("file", `{"filename":"test.log"}`) +``` ## Conn adapter Configure like this: - log := NewLogger(1000) - log.SetLogger("conn", `{"net":"tcp","addr":":7020"}`) - log.Info("info") - +```golang +log := NewLogger(1000) +log.SetLogger("conn", `{"net":"tcp","addr":":7020"}`) +log.Info("info") +``` ## Smtp adapter Configure like this: - log := NewLogger(10000) - log.SetLogger("smtp", `{"username":"beegotest@gmail.com","password":"xxxxxxxx","host":"smtp.gmail.com:587","sendTos":["xiemengjun@gmail.com"]}`) - log.Critical("sendmail critical") - time.Sleep(time.Second * 30) +```golang +log := NewLogger(10000) +log.SetLogger("smtp", `{"username":"beegotest@gmail.com","password":"xxxxxxxx","host":"smtp.gmail.com:587","sendTos":["xiemengjun@gmail.com"]}`) +log.Critical("sendmail critical") +time.Sleep(time.Second * 30) +``` diff --git a/src/vendor/github.com/astaxie/beego/logs/accesslog.go b/src/vendor/github.com/astaxie/beego/logs/accesslog.go new file mode 100644 index 000000000..3ff9e20fc --- /dev/null +++ b/src/vendor/github.com/astaxie/beego/logs/accesslog.go @@ -0,0 +1,83 @@ +// Copyright 2014 beego Author. All Rights Reserved. +// +// 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. + +package logs + +import ( + "bytes" + "strings" + "encoding/json" + "fmt" + "time" +) + +const ( + apacheFormatPattern = "%s - - [%s] \"%s %d %d\" %f %s %s" + apacheFormat = "APACHE_FORMAT" + jsonFormat = "JSON_FORMAT" +) + +// AccessLogRecord struct for holding access log data. +type AccessLogRecord struct { + RemoteAddr string `json:"remote_addr"` + RequestTime time.Time `json:"request_time"` + RequestMethod string `json:"request_method"` + Request string `json:"request"` + ServerProtocol string `json:"server_protocol"` + Host string `json:"host"` + Status int `json:"status"` + BodyBytesSent int64 `json:"body_bytes_sent"` + ElapsedTime time.Duration `json:"elapsed_time"` + HTTPReferrer string `json:"http_referrer"` + HTTPUserAgent string `json:"http_user_agent"` + RemoteUser string `json:"remote_user"` +} + +func (r *AccessLogRecord) json() ([]byte, error) { + buffer := &bytes.Buffer{} + encoder := json.NewEncoder(buffer) + disableEscapeHTML(encoder) + + err := encoder.Encode(r) + return buffer.Bytes(), err +} + +func disableEscapeHTML(i interface{}) { + if e, ok := i.(interface { + SetEscapeHTML(bool) + }); ok { + e.SetEscapeHTML(false) + } +} + +// AccessLog - Format and print access log. +func AccessLog(r *AccessLogRecord, format string) { + var msg string + switch format { + case apacheFormat: + timeFormatted := r.RequestTime.Format("02/Jan/2006 03:04:05") + msg = fmt.Sprintf(apacheFormatPattern, r.RemoteAddr, timeFormatted, r.Request, r.Status, r.BodyBytesSent, + r.ElapsedTime.Seconds(), r.HTTPReferrer, r.HTTPUserAgent) + case jsonFormat: + fallthrough + default: + jsonData, err := r.json() + if err != nil { + msg = fmt.Sprintf(`{"Error": "%s"}`, err) + } else { + msg = string(jsonData) + } + } + beeLogger.writeMsg(levelLoggerImpl, strings.TrimSpace(msg)) +} diff --git a/src/vendor/github.com/astaxie/beego/logs/color.go b/src/vendor/github.com/astaxie/beego/logs/color.go deleted file mode 100644 index 41d23638a..000000000 --- a/src/vendor/github.com/astaxie/beego/logs/color.go +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2014 beego Author. All Rights Reserved. -// -// 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. - -// +build !windows - -package logs - -import "io" - -type ansiColorWriter struct { - w io.Writer - mode outputMode -} - -func (cw *ansiColorWriter) Write(p []byte) (int, error) { - return cw.w.Write(p) -} diff --git a/src/vendor/github.com/astaxie/beego/logs/conn.go b/src/vendor/github.com/astaxie/beego/logs/conn.go index 6d5bf6bfc..afe0cbb75 100644 --- a/src/vendor/github.com/astaxie/beego/logs/conn.go +++ b/src/vendor/github.com/astaxie/beego/logs/conn.go @@ -63,7 +63,7 @@ func (c *connWriter) WriteMsg(when time.Time, msg string, level int) error { defer c.innerWriter.Close() } - c.lg.println(when, msg) + c.lg.writeln(when, msg) return nil } diff --git a/src/vendor/github.com/astaxie/beego/logs/console.go b/src/vendor/github.com/astaxie/beego/logs/console.go index e75f2a1b1..3dcaee1df 100644 --- a/src/vendor/github.com/astaxie/beego/logs/console.go +++ b/src/vendor/github.com/astaxie/beego/logs/console.go @@ -17,8 +17,10 @@ package logs import ( "encoding/json" "os" - "runtime" + "strings" "time" + + "github.com/shiena/ansicolor" ) // brush is a color join function @@ -54,9 +56,9 @@ type consoleWriter struct { // NewConsole create ConsoleWriter returning as LoggerInterface. func NewConsole() Logger { cw := &consoleWriter{ - lg: newLogWriter(os.Stdout), + lg: newLogWriter(ansicolor.NewAnsiColorWriter(os.Stdout)), Level: LevelDebug, - Colorful: runtime.GOOS != "windows", + Colorful: true, } return cw } @@ -67,11 +69,7 @@ func (c *consoleWriter) Init(jsonConfig string) error { if len(jsonConfig) == 0 { return nil } - err := json.Unmarshal([]byte(jsonConfig), c) - if runtime.GOOS == "windows" { - c.Colorful = false - } - return err + return json.Unmarshal([]byte(jsonConfig), c) } // WriteMsg write message in console. @@ -80,9 +78,9 @@ func (c *consoleWriter) WriteMsg(when time.Time, msg string, level int) error { return nil } if c.Colorful { - msg = colors[level](msg) + msg = strings.Replace(msg, levelPrefix[level], colors[level](levelPrefix[level]), 1) } - c.lg.println(when, msg) + c.lg.writeln(when, msg) return nil } diff --git a/src/vendor/github.com/astaxie/beego/logs/file.go b/src/vendor/github.com/astaxie/beego/logs/file.go index e8c1f37e8..588f78603 100644 --- a/src/vendor/github.com/astaxie/beego/logs/file.go +++ b/src/vendor/github.com/astaxie/beego/logs/file.go @@ -21,6 +21,7 @@ import ( "fmt" "io" "os" + "path" "path/filepath" "strconv" "strings" @@ -40,6 +41,9 @@ type fileLogWriter struct { MaxLines int `json:"maxlines"` maxLinesCurLines int + MaxFiles int `json:"maxfiles"` + MaxFilesCurFiles int + // Rotate at size MaxSize int `json:"maxsize"` maxSizeCurSize int @@ -50,6 +54,12 @@ type fileLogWriter struct { dailyOpenDate int dailyOpenTime time.Time + // Rotate hourly + Hourly bool `json:"hourly"` + MaxHours int64 `json:"maxhours"` + hourlyOpenDate int + hourlyOpenTime time.Time + Rotate bool `json:"rotate"` Level int `json:"level"` @@ -66,25 +76,30 @@ func newFileWriter() Logger { w := &fileLogWriter{ Daily: true, MaxDays: 7, + Hourly: false, + MaxHours: 168, Rotate: true, RotatePerm: "0440", Level: LevelTrace, Perm: "0660", + MaxLines: 10000000, + MaxFiles: 999, + MaxSize: 1 << 28, } return w } // Init file logger with json config. // jsonConfig like: -// { -// "filename":"logs/beego.log", -// "maxLines":10000, -// "maxsize":1024, -// "daily":true, -// "maxDays":15, -// "rotate":true, -// "perm":"0600" -// } +// { +// "filename":"logs/beego.log", +// "maxLines":10000, +// "maxsize":1024, +// "daily":true, +// "maxDays":15, +// "rotate":true, +// "perm":"0600" +// } func (w *fileLogWriter) Init(jsonConfig string) error { err := json.Unmarshal([]byte(jsonConfig), w) if err != nil { @@ -115,10 +130,16 @@ func (w *fileLogWriter) startLogger() error { return w.initFd() } -func (w *fileLogWriter) needRotate(size int, day int) bool { +func (w *fileLogWriter) needRotateDaily(size int, day int) bool { return (w.MaxLines > 0 && w.maxLinesCurLines >= w.MaxLines) || (w.MaxSize > 0 && w.maxSizeCurSize >= w.MaxSize) || (w.Daily && day != w.dailyOpenDate) +} + +func (w *fileLogWriter) needRotateHourly(size int, hour int) bool { + return (w.MaxLines > 0 && w.maxLinesCurLines >= w.MaxLines) || + (w.MaxSize > 0 && w.maxSizeCurSize >= w.MaxSize) || + (w.Hourly && hour != w.hourlyOpenDate) } @@ -127,14 +148,23 @@ func (w *fileLogWriter) WriteMsg(when time.Time, msg string, level int) error { if level > w.Level { return nil } - h, d := formatTimeHeader(when) - msg = string(h) + msg + "\n" + hd, d, h := formatTimeHeader(when) + msg = string(hd) + msg + "\n" if w.Rotate { w.RLock() - if w.needRotate(len(msg), d) { + if w.needRotateHourly(len(msg), h) { w.RUnlock() w.Lock() - if w.needRotate(len(msg), d) { + if w.needRotateHourly(len(msg), h) { + if err := w.doRotate(when); err != nil { + fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err) + } + } + w.Unlock() + } else if w.needRotateDaily(len(msg), d) { + w.RUnlock() + w.Lock() + if w.needRotateDaily(len(msg), d) { if err := w.doRotate(when); err != nil { fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err) } @@ -161,6 +191,10 @@ func (w *fileLogWriter) createLogFile() (*os.File, error) { if err != nil { return nil, err } + + filepath := path.Dir(w.Filename) + os.MkdirAll(filepath, os.FileMode(perm)) + fd, err := os.OpenFile(w.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(perm)) if err == nil { // Make sure file perm is user set perm cause of `os.OpenFile` will obey umask @@ -178,11 +212,15 @@ func (w *fileLogWriter) initFd() error { w.maxSizeCurSize = int(fInfo.Size()) w.dailyOpenTime = time.Now() w.dailyOpenDate = w.dailyOpenTime.Day() + w.hourlyOpenTime = time.Now() + w.hourlyOpenDate = w.hourlyOpenTime.Hour() w.maxLinesCurLines = 0 - if w.Daily { + if w.Hourly { + go w.hourlyRotate(w.hourlyOpenTime) + } else if w.Daily { go w.dailyRotate(w.dailyOpenTime) } - if fInfo.Size() > 0 { + if fInfo.Size() > 0 && w.MaxLines > 0 { count, err := w.lines() if err != nil { return err @@ -198,7 +236,22 @@ func (w *fileLogWriter) dailyRotate(openTime time.Time) { tm := time.NewTimer(time.Duration(nextDay.UnixNano() - openTime.UnixNano() + 100)) <-tm.C w.Lock() - if w.needRotate(0, time.Now().Day()) { + if w.needRotateDaily(0, time.Now().Day()) { + if err := w.doRotate(time.Now()); err != nil { + fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err) + } + } + w.Unlock() +} + +func (w *fileLogWriter) hourlyRotate(openTime time.Time) { + y, m, d := openTime.Add(1 * time.Hour).Date() + h, _, _ := openTime.Add(1 * time.Hour).Clock() + nextHour := time.Date(y, m, d, h, 0, 0, 0, openTime.Location()) + tm := time.NewTimer(time.Duration(nextHour.UnixNano() - openTime.UnixNano() + 100)) + <-tm.C + w.Lock() + if w.needRotateHourly(0, time.Now().Hour()) { if err := w.doRotate(time.Now()); err != nil { fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err) } @@ -238,8 +291,10 @@ func (w *fileLogWriter) lines() (int, error) { func (w *fileLogWriter) doRotate(logTime time.Time) error { // file exists // Find the next available number - num := 1 + num := w.MaxFilesCurFiles + 1 fName := "" + format := "" + var openTime time.Time rotatePerm, err := strconv.ParseInt(w.RotatePerm, 8, 64) if err != nil { return err @@ -251,19 +306,26 @@ func (w *fileLogWriter) doRotate(logTime time.Time) error { goto RESTART_LOGGER } + if w.Hourly { + format = "2006010215" + openTime = w.hourlyOpenTime + } else if w.Daily { + format = "2006-01-02" + openTime = w.dailyOpenTime + } + + // only when one of them be setted, then the file would be splited if w.MaxLines > 0 || w.MaxSize > 0 { - for ; err == nil && num <= 999; num++ { - fName = w.fileNameOnly + fmt.Sprintf(".%s.%03d%s", logTime.Format("2006-01-02"), num, w.suffix) + for ; err == nil && num <= w.MaxFiles; num++ { + fName = w.fileNameOnly + fmt.Sprintf(".%s.%03d%s", logTime.Format(format), num, w.suffix) _, err = os.Lstat(fName) } } else { - fName = fmt.Sprintf("%s.%s%s", w.fileNameOnly, w.dailyOpenTime.Format("2006-01-02"), w.suffix) + fName = w.fileNameOnly + fmt.Sprintf(".%s.%03d%s", openTime.Format(format), num, w.suffix) _, err = os.Lstat(fName) - for ; err == nil && num <= 999; num++ { - fName = w.fileNameOnly + fmt.Sprintf(".%s.%03d%s", w.dailyOpenTime.Format("2006-01-02"), num, w.suffix) - _, err = os.Lstat(fName) - } + w.MaxFilesCurFiles = num } + // return error if the last file checked still existed if err == nil { return fmt.Errorf("Rotate: Cannot find free log number to rename %s", w.Filename) @@ -307,13 +369,21 @@ func (w *fileLogWriter) deleteOldLog() { if info == nil { return } - - if !info.IsDir() && info.ModTime().Add(24*time.Hour*time.Duration(w.MaxDays)).Before(time.Now()) { - if strings.HasPrefix(filepath.Base(path), filepath.Base(w.fileNameOnly)) && - strings.HasSuffix(filepath.Base(path), w.suffix) { - os.Remove(path) - } - } + if w.Hourly { + if !info.IsDir() && info.ModTime().Add(1 * time.Hour * time.Duration(w.MaxHours)).Before(time.Now()) { + if strings.HasPrefix(filepath.Base(path), filepath.Base(w.fileNameOnly)) && + strings.HasSuffix(filepath.Base(path), w.suffix) { + os.Remove(path) + } + } + } else if w.Daily { + if !info.IsDir() && info.ModTime().Add(24 * time.Hour * time.Duration(w.MaxDays)).Before(time.Now()) { + if strings.HasPrefix(filepath.Base(path), filepath.Base(w.fileNameOnly)) && + strings.HasSuffix(filepath.Base(path), w.suffix) { + os.Remove(path) + } + } + } return }) } diff --git a/src/vendor/github.com/astaxie/beego/logs/log.go b/src/vendor/github.com/astaxie/beego/logs/log.go index 0e97a70e3..49f3794f3 100644 --- a/src/vendor/github.com/astaxie/beego/logs/log.go +++ b/src/vendor/github.com/astaxie/beego/logs/log.go @@ -92,7 +92,7 @@ type Logger interface { } var adapters = make(map[string]newLoggerFunc) -var levelPrefix = [LevelDebug + 1]string{"[M] ", "[A] ", "[C] ", "[E] ", "[W] ", "[N] ", "[I] ", "[D] "} +var levelPrefix = [LevelDebug + 1]string{"[M]", "[A]", "[C]", "[E]", "[W]", "[N]", "[I]", "[D]"} // Register makes a log provide available by the provided name. // If Register is called twice with the same name or if driver is nil, @@ -116,6 +116,7 @@ type BeeLogger struct { enableFuncCallDepth bool loggerFuncCallDepth int asynchronous bool + prefix string msgChanLen int64 msgChan chan *logMsg signalChan chan string @@ -186,12 +187,12 @@ func (bl *BeeLogger) setLogger(adapterName string, configs ...string) error { } } - log, ok := adapters[adapterName] + logAdapter, ok := adapters[adapterName] if !ok { return fmt.Errorf("logs: unknown adaptername %q (forgotten Register?)", adapterName) } - lg := log() + lg := logAdapter() err := lg.Init(config) if err != nil { fmt.Fprintln(os.Stderr, "logs.BeeLogger.SetLogger: "+err.Error()) @@ -267,6 +268,9 @@ func (bl *BeeLogger) writeMsg(logLevel int, msg string, v ...interface{}) error if len(v) > 0 { msg = fmt.Sprintf(msg, v...) } + + msg = bl.prefix + " " + msg + when := time.Now() if bl.enableFuncCallDepth { _, file, line, ok := runtime.Caller(bl.loggerFuncCallDepth) @@ -283,7 +287,7 @@ func (bl *BeeLogger) writeMsg(logLevel int, msg string, v ...interface{}) error // set to emergency to ensure all log will be print out correctly logLevel = LevelEmergency } else { - msg = levelPrefix[logLevel] + msg + msg = levelPrefix[logLevel] + " " + msg } if bl.asynchronous { @@ -305,6 +309,11 @@ func (bl *BeeLogger) SetLevel(l int) { bl.level = l } +// GetLevel Get Current log message level. +func (bl *BeeLogger) GetLevel() int { + return bl.level +} + // SetLogFuncCallDepth set log funcCallDepth func (bl *BeeLogger) SetLogFuncCallDepth(d int) { bl.loggerFuncCallDepth = d @@ -320,6 +329,11 @@ func (bl *BeeLogger) EnableFuncCallDepth(b bool) { bl.enableFuncCallDepth = b } +// set prefix +func (bl *BeeLogger) SetPrefix(s string) { + bl.prefix = s +} + // start logger chan reading. // when chan is not empty, write logs. func (bl *BeeLogger) startLogger() { @@ -544,6 +558,11 @@ func SetLevel(l int) { beeLogger.SetLevel(l) } +// SetPrefix sets the prefix +func SetPrefix(s string) { + beeLogger.SetPrefix(s) +} + // EnableFuncCallDepth enable log funcCallDepth func EnableFuncCallDepth(b bool) { beeLogger.enableFuncCallDepth = b diff --git a/src/vendor/github.com/astaxie/beego/logs/logger.go b/src/vendor/github.com/astaxie/beego/logs/logger.go index b5d7255f0..c7cf8a56e 100644 --- a/src/vendor/github.com/astaxie/beego/logs/logger.go +++ b/src/vendor/github.com/astaxie/beego/logs/logger.go @@ -15,9 +15,8 @@ package logs import ( - "fmt" "io" - "os" + "runtime" "sync" "time" ) @@ -31,47 +30,13 @@ func newLogWriter(wr io.Writer) *logWriter { return &logWriter{writer: wr} } -func (lg *logWriter) println(when time.Time, msg string) { +func (lg *logWriter) writeln(when time.Time, msg string) { lg.Lock() - h, _ := formatTimeHeader(when) + h, _, _ := formatTimeHeader(when) lg.writer.Write(append(append(h, msg...), '\n')) lg.Unlock() } -type outputMode int - -// DiscardNonColorEscSeq supports the divided color escape sequence. -// But non-color escape sequence is not output. -// Please use the OutputNonColorEscSeq If you want to output a non-color -// escape sequences such as ncurses. However, it does not support the divided -// color escape sequence. -const ( - _ outputMode = iota - DiscardNonColorEscSeq - OutputNonColorEscSeq -) - -// NewAnsiColorWriter creates and initializes a new ansiColorWriter -// using io.Writer w as its initial contents. -// In the console of Windows, which change the foreground and background -// colors of the text by the escape sequence. -// In the console of other systems, which writes to w all text. -func NewAnsiColorWriter(w io.Writer) io.Writer { - return NewModeAnsiColorWriter(w, DiscardNonColorEscSeq) -} - -// NewModeAnsiColorWriter create and initializes a new ansiColorWriter -// by specifying the outputMode. -func NewModeAnsiColorWriter(w io.Writer, mode outputMode) io.Writer { - if _, ok := w.(*ansiColorWriter); !ok { - return &ansiColorWriter{ - w: w, - mode: mode, - } - } - return w -} - const ( y1 = `0123456789` y2 = `0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789` @@ -87,13 +52,15 @@ const ( mi2 = `012345678901234567890123456789012345678901234567890123456789` s1 = `000000000011111111112222222222333333333344444444445555555555` s2 = `012345678901234567890123456789012345678901234567890123456789` + ns1 = `0123456789` ) -func formatTimeHeader(when time.Time) ([]byte, int) { +func formatTimeHeader(when time.Time) ([]byte, int, int) { y, mo, d := when.Date() h, mi, s := when.Clock() - //len("2006/01/02 15:04:05 ")==20 - var buf [20]byte + ns := when.Nanosecond() / 1000000 + //len("2006/01/02 15:04:05.123 ")==24 + var buf [24]byte buf[0] = y1[y/1000%10] buf[1] = y2[y/100] @@ -114,9 +81,14 @@ func formatTimeHeader(when time.Time) ([]byte, int) { buf[16] = ':' buf[17] = s1[s] buf[18] = s2[s] - buf[19] = ' ' + buf[19] = '.' + buf[20] = ns1[ns/100] + buf[21] = ns1[ns%100/10] + buf[22] = ns1[ns%10] - return buf[0:], d + buf[23] = ' ' + + return buf[0:], d, h } var ( @@ -139,63 +111,65 @@ var ( reset = string([]byte{27, 91, 48, 109}) ) +var once sync.Once +var colorMap map[string]string + +func initColor() { + if runtime.GOOS == "windows" { + green = w32Green + white = w32White + yellow = w32Yellow + red = w32Red + blue = w32Blue + magenta = w32Magenta + cyan = w32Cyan + } + colorMap = map[string]string{ + //by color + "green": green, + "white": white, + "yellow": yellow, + "red": red, + //by method + "GET": blue, + "POST": cyan, + "PUT": yellow, + "DELETE": red, + "PATCH": green, + "HEAD": magenta, + "OPTIONS": white, + } +} + // ColorByStatus return color by http code // 2xx return Green // 3xx return White // 4xx return Yellow // 5xx return Red -func ColorByStatus(cond bool, code int) string { +func ColorByStatus(code int) string { + once.Do(initColor) switch { case code >= 200 && code < 300: - return map[bool]string{true: green, false: w32Green}[cond] + return colorMap["green"] case code >= 300 && code < 400: - return map[bool]string{true: white, false: w32White}[cond] + return colorMap["white"] case code >= 400 && code < 500: - return map[bool]string{true: yellow, false: w32Yellow}[cond] + return colorMap["yellow"] default: - return map[bool]string{true: red, false: w32Red}[cond] + return colorMap["red"] } } // ColorByMethod return color by http code -// GET return Blue -// POST return Cyan -// PUT return Yellow -// DELETE return Red -// PATCH return Green -// HEAD return Magenta -// OPTIONS return WHITE -func ColorByMethod(cond bool, method string) string { - switch method { - case "GET": - return map[bool]string{true: blue, false: w32Blue}[cond] - case "POST": - return map[bool]string{true: cyan, false: w32Cyan}[cond] - case "PUT": - return map[bool]string{true: yellow, false: w32Yellow}[cond] - case "DELETE": - return map[bool]string{true: red, false: w32Red}[cond] - case "PATCH": - return map[bool]string{true: green, false: w32Green}[cond] - case "HEAD": - return map[bool]string{true: magenta, false: w32Magenta}[cond] - case "OPTIONS": - return map[bool]string{true: white, false: w32White}[cond] - default: - return reset +func ColorByMethod(method string) string { + once.Do(initColor) + if c := colorMap[method]; c != "" { + return c } + return reset } -// Guard Mutex to guarantee atomic of W32Debug(string) function -var mu sync.Mutex - -// W32Debug Helper method to output colored logs in Windows terminals -func W32Debug(msg string) { - mu.Lock() - defer mu.Unlock() - - current := time.Now() - w := NewAnsiColorWriter(os.Stdout) - - fmt.Fprintf(w, "[beego] %v %s\n", current.Format("2006/01/02 - 15:04:05"), msg) +// ResetColor return reset color +func ResetColor() string { + return reset } diff --git a/src/vendor/github.com/astaxie/beego/logs/multifile.go b/src/vendor/github.com/astaxie/beego/logs/multifile.go index 63204e176..901682743 100644 --- a/src/vendor/github.com/astaxie/beego/logs/multifile.go +++ b/src/vendor/github.com/astaxie/beego/logs/multifile.go @@ -67,7 +67,10 @@ func (f *multiFileLogWriter) Init(config string) error { jsonMap["level"] = i bs, _ := json.Marshal(jsonMap) writer = newFileWriter().(*fileLogWriter) - writer.Init(string(bs)) + err := writer.Init(string(bs)) + if err != nil { + return err + } f.writers[i] = writer } } diff --git a/src/vendor/github.com/astaxie/beego/namespace.go b/src/vendor/github.com/astaxie/beego/namespace.go index 72f22a720..4952c9d56 100644 --- a/src/vendor/github.com/astaxie/beego/namespace.go +++ b/src/vendor/github.com/astaxie/beego/namespace.go @@ -207,11 +207,11 @@ func (n *Namespace) Include(cList ...ControllerInterface) *Namespace { func (n *Namespace) Namespace(ns ...*Namespace) *Namespace { for _, ni := range ns { for k, v := range ni.handlers.routers { - if t, ok := n.handlers.routers[k]; ok { + if _, ok := n.handlers.routers[k]; ok { addPrefix(v, ni.prefix) n.handlers.routers[k].AddTree(ni.prefix, v) } else { - t = NewTree() + t := NewTree() t.AddTree(ni.prefix, v) addPrefix(t, ni.prefix) n.handlers.routers[k] = t @@ -236,11 +236,11 @@ func (n *Namespace) Namespace(ns ...*Namespace) *Namespace { func AddNamespace(nl ...*Namespace) { for _, n := range nl { for k, v := range n.handlers.routers { - if t, ok := BeeApp.Handlers.routers[k]; ok { + if _, ok := BeeApp.Handlers.routers[k]; ok { addPrefix(v, n.prefix) BeeApp.Handlers.routers[k].AddTree(n.prefix, v) } else { - t = NewTree() + t := NewTree() t.AddTree(n.prefix, v) addPrefix(t, n.prefix) BeeApp.Handlers.routers[k] = t diff --git a/src/vendor/github.com/astaxie/beego/orm/README.md b/src/vendor/github.com/astaxie/beego/orm/README.md index fa7fdca10..6e808d2ad 100644 --- a/src/vendor/github.com/astaxie/beego/orm/README.md +++ b/src/vendor/github.com/astaxie/beego/orm/README.md @@ -61,6 +61,9 @@ func init() { // set default database orm.RegisterDataBase("default", "mysql", "root:root@/my_db?charset=utf8", 30) + + // create table + orm.RunSyncdb("default", false, true) } func main() { diff --git a/src/vendor/github.com/astaxie/beego/orm/cmd_utils.go b/src/vendor/github.com/astaxie/beego/orm/cmd_utils.go index de47cb023..7c9aa51ed 100644 --- a/src/vendor/github.com/astaxie/beego/orm/cmd_utils.go +++ b/src/vendor/github.com/astaxie/beego/orm/cmd_utils.go @@ -51,12 +51,14 @@ checkColumn: switch fieldType { case TypeBooleanField: col = T["bool"] - case TypeCharField: + case TypeVarCharField: if al.Driver == DRPostgres && fi.toText { col = T["string-text"] } else { col = fmt.Sprintf(T["string"], fieldSize) } + case TypeCharField: + col = fmt.Sprintf(T["string-char"], fieldSize) case TypeTextField: col = T["string-text"] case TypeTimeField: @@ -96,13 +98,13 @@ checkColumn: } case TypeJSONField: if al.Driver != DRPostgres { - fieldType = TypeCharField + fieldType = TypeVarCharField goto checkColumn } col = T["json"] case TypeJsonbField: if al.Driver != DRPostgres { - fieldType = TypeCharField + fieldType = TypeVarCharField goto checkColumn } col = T["jsonb"] @@ -195,6 +197,10 @@ func getDbCreateSQL(al *alias) (sqls []string, tableIndexes map[string][]dbIndex if strings.Contains(column, "%COL%") { column = strings.Replace(column, "%COL%", fi.column, -1) } + + if fi.description != "" { + column += " " + fmt.Sprintf("COMMENT '%s'",fi.description) + } columns = append(columns, column) } diff --git a/src/vendor/github.com/astaxie/beego/orm/db.go b/src/vendor/github.com/astaxie/beego/orm/db.go index 12f0f54d2..2148daaa0 100644 --- a/src/vendor/github.com/astaxie/beego/orm/db.go +++ b/src/vendor/github.com/astaxie/beego/orm/db.go @@ -142,7 +142,7 @@ func (d *dbBase) collectFieldValue(mi *modelInfo, fi *fieldInfo, ind reflect.Val } else { value = field.Bool() } - case TypeCharField, TypeTextField, TypeJSONField, TypeJsonbField: + case TypeVarCharField, TypeCharField, TypeTextField, TypeJSONField, TypeJsonbField: if ns, ok := field.Interface().(sql.NullString); ok { value = nil if ns.Valid { @@ -536,6 +536,8 @@ func (d *dbBase) InsertOrUpdate(q dbQuerier, mi *modelInfo, ind reflect.Value, a updates := make([]string, len(names)) var conflitValue interface{} for i, v := range names { + // identifier in database may not be case-sensitive, so quote it + v = fmt.Sprintf("%s%s%s", Q, v, Q) marks[i] = "?" valueStr := argsMap[strings.ToLower(v)] if v == args0 { @@ -619,6 +621,31 @@ func (d *dbBase) Update(q dbQuerier, mi *modelInfo, ind reflect.Value, tz *time. return 0, err } + var findAutoNowAdd, findAutoNow bool + var index int + for i, col := range setNames { + if mi.fields.GetByColumn(col).autoNowAdd { + index = i + findAutoNowAdd = true + } + if mi.fields.GetByColumn(col).autoNow { + findAutoNow = true + } + } + if findAutoNowAdd { + setNames = append(setNames[0:index], setNames[index+1:]...) + setValues = append(setValues[0:index], setValues[index+1:]...) + } + + if !findAutoNow { + for col, info := range mi.fields.columns { + if info.autoNow { + setNames = append(setNames, col) + setValues = append(setValues, time.Now()) + } + } + } + setValues = append(setValues, pkValue) Q := d.ins.TableQuote() @@ -760,7 +787,13 @@ func (d *dbBase) UpdateBatch(q dbQuerier, qs *querySet, mi *modelInfo, cond *Con } d.ins.ReplaceMarks(&query) - res, err := q.Exec(query, values...) + var err error + var res sql.Result + if qs != nil && qs.forContext { + res, err = q.ExecContext(qs.ctx, query, values...) + } else { + res, err = q.Exec(query, values...) + } if err == nil { return res.RowsAffected() } @@ -849,11 +882,16 @@ func (d *dbBase) DeleteBatch(q dbQuerier, qs *querySet, mi *modelInfo, cond *Con for i := range marks { marks[i] = "?" } - sql := fmt.Sprintf("IN (%s)", strings.Join(marks, ", ")) - query = fmt.Sprintf("DELETE FROM %s%s%s WHERE %s%s%s %s", Q, mi.table, Q, Q, mi.fields.pk.column, Q, sql) + sqlIn := fmt.Sprintf("IN (%s)", strings.Join(marks, ", ")) + query = fmt.Sprintf("DELETE FROM %s%s%s WHERE %s%s%s %s", Q, mi.table, Q, Q, mi.fields.pk.column, Q, sqlIn) d.ins.ReplaceMarks(&query) - res, err := q.Exec(query, args...) + var res sql.Result + if qs != nil && qs.forContext { + res, err = q.ExecContext(qs.ctx, query, args...) + } else { + res, err = q.Exec(query, args...) + } if err == nil { num, err := res.RowsAffected() if err != nil { @@ -926,7 +964,7 @@ func (d *dbBase) ReadBatch(q dbQuerier, qs *querySet, mi *modelInfo, cond *Condi maps[fi.column] = true } } else { - panic(fmt.Errorf("wrong field/column name `%s`", col)) + return 0, fmt.Errorf("wrong field/column name `%s`", col) } } if hasRel { @@ -969,14 +1007,25 @@ func (d *dbBase) ReadBatch(q dbQuerier, qs *querySet, mi *modelInfo, cond *Condi } query := fmt.Sprintf("%s %s FROM %s%s%s T0 %s%s%s%s%s", sqlSelect, sels, Q, mi.table, Q, join, where, groupBy, orderBy, limit) + if qs.forupdate { + query += " FOR UPDATE" + } + d.ins.ReplaceMarks(&query) var rs *sql.Rows - r, err := q.Query(query, args...) - if err != nil { - return 0, err + var err error + if qs != nil && qs.forContext { + rs, err = q.QueryContext(qs.ctx, query, args...) + if err != nil { + return 0, err + } + } else { + rs, err = q.Query(query, args...) + if err != nil { + return 0, err + } } - rs = r refs := make([]interface{}, colsNum) for i := range refs { @@ -1105,8 +1154,12 @@ func (d *dbBase) Count(q dbQuerier, qs *querySet, mi *modelInfo, cond *Condition d.ins.ReplaceMarks(&query) - row := q.QueryRow(query, args...) - + var row *sql.Row + if qs != nil && qs.forContext { + row = q.QueryRowContext(qs.ctx, query, args...) + } else { + row = q.QueryRow(query, args...) + } err = row.Scan(&cnt) return } @@ -1240,7 +1293,7 @@ setValue: } value = b } - case fieldType == TypeCharField || fieldType == TypeTextField || fieldType == TypeJSONField || fieldType == TypeJsonbField: + case fieldType == TypeVarCharField || fieldType == TypeCharField || fieldType == TypeTextField || fieldType == TypeJSONField || fieldType == TypeJsonbField: if str == nil { value = ToStr(val) } else { @@ -1386,7 +1439,7 @@ setValue: field.SetBool(value.(bool)) } } - case fieldType == TypeCharField || fieldType == TypeTextField || fieldType == TypeJSONField || fieldType == TypeJsonbField: + case fieldType == TypeVarCharField || fieldType == TypeCharField || fieldType == TypeTextField || fieldType == TypeJSONField || fieldType == TypeJsonbField: if isNative { if ns, ok := field.Interface().(sql.NullString); ok { if value == nil { diff --git a/src/vendor/github.com/astaxie/beego/orm/db_alias.go b/src/vendor/github.com/astaxie/beego/orm/db_alias.go index c70892392..51ce10f34 100644 --- a/src/vendor/github.com/astaxie/beego/orm/db_alias.go +++ b/src/vendor/github.com/astaxie/beego/orm/db_alias.go @@ -15,6 +15,7 @@ package orm import ( + "context" "database/sql" "fmt" "reflect" @@ -103,6 +104,96 @@ func (ac *_dbCache) getDefault() (al *alias) { return } +type DB struct { + *sync.RWMutex + DB *sql.DB + stmts map[string]*sql.Stmt +} + +func (d *DB) Begin() (*sql.Tx, error) { + return d.DB.Begin() +} + +func (d *DB) BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error) { + return d.DB.BeginTx(ctx, opts) +} + +func (d *DB) getStmt(query string) (*sql.Stmt, error) { + d.RLock() + if stmt, ok := d.stmts[query]; ok { + d.RUnlock() + return stmt, nil + } + d.RUnlock() + + stmt, err := d.Prepare(query) + if err != nil { + return nil, err + } + d.Lock() + d.stmts[query] = stmt + d.Unlock() + return stmt, nil +} + +func (d *DB) Prepare(query string) (*sql.Stmt, error) { + return d.DB.Prepare(query) +} + +func (d *DB) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) { + return d.DB.PrepareContext(ctx, query) +} + +func (d *DB) Exec(query string, args ...interface{}) (sql.Result, error) { + stmt, err := d.getStmt(query) + if err != nil { + return nil, err + } + return stmt.Exec(args...) +} + +func (d *DB) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { + stmt, err := d.getStmt(query) + if err != nil { + return nil, err + } + return stmt.ExecContext(ctx, args...) +} + +func (d *DB) Query(query string, args ...interface{}) (*sql.Rows, error) { + stmt, err := d.getStmt(query) + if err != nil { + return nil, err + } + return stmt.Query(args...) +} + +func (d *DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) { + stmt, err := d.getStmt(query) + if err != nil { + return nil, err + } + return stmt.QueryContext(ctx, args...) +} + +func (d *DB) QueryRow(query string, args ...interface{}) *sql.Row { + stmt, err := d.getStmt(query) + if err != nil { + panic(err) + } + return stmt.QueryRow(args...) + +} + +func (d *DB) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row { + + stmt, err := d.getStmt(query) + if err != nil { + panic(err) + } + return stmt.QueryRowContext(ctx, args) +} + type alias struct { Name string Driver DriverType @@ -110,7 +201,7 @@ type alias struct { DataSource string MaxIdleConns int MaxOpenConns int - DB *sql.DB + DB *DB DbBaser dbBaser TZ *time.Location Engine string @@ -119,7 +210,7 @@ type alias struct { func detectTZ(al *alias) { // orm timezone system match database // default use Local - al.TZ = time.Local + al.TZ = DefaultTimeLoc if al.DriverName == "sphinx" { return @@ -136,7 +227,9 @@ func detectTZ(al *alias) { } t, err := time.Parse("-07:00:00", tz) if err == nil { - al.TZ = t.Location() + if t.Location().String() != "" { + al.TZ = t.Location() + } } else { DebugLog.Printf("Detect DB timezone: %s %s\n", tz, err.Error()) } @@ -174,7 +267,11 @@ func addAliasWthDB(aliasName, driverName string, db *sql.DB) (*alias, error) { al := new(alias) al.Name = aliasName al.DriverName = driverName - al.DB = db + al.DB = &DB{ + RWMutex: new(sync.RWMutex), + DB: db, + stmts: make(map[string]*sql.Stmt), + } if dr, ok := drivers[driverName]; ok { al.DbBaser = dbBasers[dr] @@ -270,7 +367,7 @@ func SetDataBaseTZ(aliasName string, tz *time.Location) error { func SetMaxIdleConns(aliasName string, maxIdleConns int) { al := getDbAlias(aliasName) al.MaxIdleConns = maxIdleConns - al.DB.SetMaxIdleConns(maxIdleConns) + al.DB.DB.SetMaxIdleConns(maxIdleConns) } // SetMaxOpenConns Change the max open conns for *sql.DB, use specify database alias name @@ -294,7 +391,7 @@ func GetDB(aliasNames ...string) (*sql.DB, error) { } al, ok := dataBaseCache.get(name) if ok { - return al.DB, nil + return al.DB.DB, nil } return nil, fmt.Errorf("DataBase of alias name `%s` not found", name) } diff --git a/src/vendor/github.com/astaxie/beego/orm/db_mysql.go b/src/vendor/github.com/astaxie/beego/orm/db_mysql.go index 51185563f..6e99058ec 100644 --- a/src/vendor/github.com/astaxie/beego/orm/db_mysql.go +++ b/src/vendor/github.com/astaxie/beego/orm/db_mysql.go @@ -46,6 +46,7 @@ var mysqlTypes = map[string]string{ "pk": "NOT NULL PRIMARY KEY", "bool": "bool", "string": "varchar(%d)", + "string-char": "char(%d)", "string-text": "longtext", "time.Time-date": "date", "time.Time": "datetime", diff --git a/src/vendor/github.com/astaxie/beego/orm/db_oracle.go b/src/vendor/github.com/astaxie/beego/orm/db_oracle.go index f5d6aaa26..5d121f834 100644 --- a/src/vendor/github.com/astaxie/beego/orm/db_oracle.go +++ b/src/vendor/github.com/astaxie/beego/orm/db_oracle.go @@ -34,6 +34,7 @@ var oracleTypes = map[string]string{ "pk": "NOT NULL PRIMARY KEY", "bool": "bool", "string": "VARCHAR2(%d)", + "string-char": "CHAR(%d)", "string-text": "VARCHAR2(%d)", "time.Time-date": "DATE", "time.Time": "TIMESTAMP", diff --git a/src/vendor/github.com/astaxie/beego/orm/db_postgres.go b/src/vendor/github.com/astaxie/beego/orm/db_postgres.go index e972c4a25..c488fb388 100644 --- a/src/vendor/github.com/astaxie/beego/orm/db_postgres.go +++ b/src/vendor/github.com/astaxie/beego/orm/db_postgres.go @@ -43,6 +43,7 @@ var postgresTypes = map[string]string{ "pk": "NOT NULL PRIMARY KEY", "bool": "bool", "string": "varchar(%d)", + "string-char": "char(%d)", "string-text": "text", "time.Time-date": "date", "time.Time": "timestamp with time zone", diff --git a/src/vendor/github.com/astaxie/beego/orm/db_sqlite.go b/src/vendor/github.com/astaxie/beego/orm/db_sqlite.go index a43a5594c..0f54d81a4 100644 --- a/src/vendor/github.com/astaxie/beego/orm/db_sqlite.go +++ b/src/vendor/github.com/astaxie/beego/orm/db_sqlite.go @@ -43,6 +43,7 @@ var sqliteTypes = map[string]string{ "pk": "NOT NULL PRIMARY KEY", "bool": "bool", "string": "varchar(%d)", + "string-char": "character(%d)", "string-text": "text", "time.Time-date": "date", "time.Time": "datetime", diff --git a/src/vendor/github.com/astaxie/beego/orm/db_tables.go b/src/vendor/github.com/astaxie/beego/orm/db_tables.go index 42be5550e..4b21a6fc7 100644 --- a/src/vendor/github.com/astaxie/beego/orm/db_tables.go +++ b/src/vendor/github.com/astaxie/beego/orm/db_tables.go @@ -372,7 +372,13 @@ func (t *dbTables) getCondSQL(cond *Condition, sub bool, tz *time.Location) (whe operator = "exact" } - operSQL, args := t.base.GenerateOperatorSQL(mi, fi, operator, p.args, tz) + var operSQL string + var args []interface{} + if p.isRaw { + operSQL = p.sql + } else { + operSQL, args = t.base.GenerateOperatorSQL(mi, fi, operator, p.args, tz) + } leftCol := fmt.Sprintf("%s.%s%s%s", index, Q, fi.column, Q) t.base.GenerateOperatorLeftCol(fi, operator, &leftCol) diff --git a/src/vendor/github.com/astaxie/beego/orm/models.go b/src/vendor/github.com/astaxie/beego/orm/models.go index 1d5a4dc26..4776bcba6 100644 --- a/src/vendor/github.com/astaxie/beego/orm/models.go +++ b/src/vendor/github.com/astaxie/beego/orm/models.go @@ -52,7 +52,7 @@ func (mc *_modelCache) all() map[string]*modelInfo { return m } -// get orderd model info +// get ordered model info func (mc *_modelCache) allOrdered() []*modelInfo { m := make([]*modelInfo, 0, len(mc.orders)) for _, table := range mc.orders { diff --git a/src/vendor/github.com/astaxie/beego/orm/models_boot.go b/src/vendor/github.com/astaxie/beego/orm/models_boot.go index 5327f754c..456e58963 100644 --- a/src/vendor/github.com/astaxie/beego/orm/models_boot.go +++ b/src/vendor/github.com/astaxie/beego/orm/models_boot.go @@ -89,7 +89,7 @@ func registerModel(PrefixOrSuffix string, model interface{}, isPrefix bool) { modelCache.set(table, mi) } -// boostrap models +// bootstrap models func bootStrap() { if modelCache.done { return @@ -332,14 +332,14 @@ func RegisterModelWithSuffix(suffix string, models ...interface{}) { } } -// BootStrap bootrap models. +// BootStrap bootstrap models. // make all model parsed and can not add more models func BootStrap() { + modelCache.Lock() + defer modelCache.Unlock() if modelCache.done { return } - modelCache.Lock() - defer modelCache.Unlock() bootStrap() modelCache.done = true } diff --git a/src/vendor/github.com/astaxie/beego/orm/models_fields.go b/src/vendor/github.com/astaxie/beego/orm/models_fields.go index 578206009..b4fad94f4 100644 --- a/src/vendor/github.com/astaxie/beego/orm/models_fields.go +++ b/src/vendor/github.com/astaxie/beego/orm/models_fields.go @@ -23,6 +23,7 @@ import ( // Define the Type enum const ( TypeBooleanField = 1 << iota + TypeVarCharField TypeCharField TypeTextField TypeTimeField @@ -49,9 +50,9 @@ const ( // Define some logic enum const ( - IsIntegerField = ^-TypePositiveBigIntegerField >> 5 << 6 - IsPositiveIntegerField = ^-TypePositiveBigIntegerField >> 9 << 10 - IsRelField = ^-RelReverseMany >> 17 << 18 + IsIntegerField = ^-TypePositiveBigIntegerField >> 6 << 7 + IsPositiveIntegerField = ^-TypePositiveBigIntegerField >> 10 << 11 + IsRelField = ^-RelReverseMany >> 18 << 19 IsFieldType = ^-RelReverseMany<<1 + 1 ) @@ -85,7 +86,7 @@ func (e *BooleanField) SetRaw(value interface{}) error { e.Set(d) case string: v, err := StrTo(d).Bool() - if err != nil { + if err == nil { e.Set(v) } return err @@ -126,7 +127,7 @@ func (e *CharField) String() string { // FieldType return the enum type func (e *CharField) FieldType() int { - return TypeCharField + return TypeVarCharField } // SetRaw set the interface to string @@ -190,7 +191,7 @@ func (e *TimeField) SetRaw(value interface{}) error { e.Set(d) case string: v, err := timeParse(d, formatTime) - if err != nil { + if err == nil { e.Set(v) } return err @@ -232,7 +233,7 @@ func (e *DateField) Set(d time.Time) { *e = DateField(d) } -// String convert datatime to string +// String convert datetime to string func (e *DateField) String() string { return e.Value().String() } @@ -249,7 +250,7 @@ func (e *DateField) SetRaw(value interface{}) error { e.Set(d) case string: v, err := timeParse(d, formatDate) - if err != nil { + if err == nil { e.Set(v) } return err @@ -272,12 +273,12 @@ var _ Fielder = new(DateField) // Takes the same extra arguments as DateField. type DateTimeField time.Time -// Value return the datatime value +// Value return the datetime value func (e DateTimeField) Value() time.Time { return time.Time(e) } -// Set set the time.Time to datatime +// Set set the time.Time to datetime func (e *DateTimeField) Set(d time.Time) { *e = DateTimeField(d) } @@ -299,7 +300,7 @@ func (e *DateTimeField) SetRaw(value interface{}) error { e.Set(d) case string: v, err := timeParse(d, formatDateTime) - if err != nil { + if err == nil { e.Set(v) } return err @@ -309,12 +310,12 @@ func (e *DateTimeField) SetRaw(value interface{}) error { return nil } -// RawValue return the datatime value +// RawValue return the datetime value func (e *DateTimeField) RawValue() interface{} { return e.Value() } -// verify datatime implement fielder +// verify datetime implement fielder var _ Fielder = new(DateTimeField) // FloatField A floating-point number represented in go by a float32 value. @@ -349,9 +350,10 @@ func (e *FloatField) SetRaw(value interface{}) error { e.Set(d) case string: v, err := StrTo(d).Float64() - if err != nil { + if err == nil { e.Set(v) } + return err default: return fmt.Errorf(" unknown value `%s`", value) } @@ -396,9 +398,10 @@ func (e *SmallIntegerField) SetRaw(value interface{}) error { e.Set(d) case string: v, err := StrTo(d).Int16() - if err != nil { + if err == nil { e.Set(v) } + return err default: return fmt.Errorf(" unknown value `%s`", value) } @@ -443,9 +446,10 @@ func (e *IntegerField) SetRaw(value interface{}) error { e.Set(d) case string: v, err := StrTo(d).Int32() - if err != nil { + if err == nil { e.Set(v) } + return err default: return fmt.Errorf(" unknown value `%s`", value) } @@ -490,9 +494,10 @@ func (e *BigIntegerField) SetRaw(value interface{}) error { e.Set(d) case string: v, err := StrTo(d).Int64() - if err != nil { + if err == nil { e.Set(v) } + return err default: return fmt.Errorf(" unknown value `%s`", value) } @@ -537,9 +542,10 @@ func (e *PositiveSmallIntegerField) SetRaw(value interface{}) error { e.Set(d) case string: v, err := StrTo(d).Uint16() - if err != nil { + if err == nil { e.Set(v) } + return err default: return fmt.Errorf(" unknown value `%s`", value) } @@ -584,9 +590,10 @@ func (e *PositiveIntegerField) SetRaw(value interface{}) error { e.Set(d) case string: v, err := StrTo(d).Uint32() - if err != nil { + if err == nil { e.Set(v) } + return err default: return fmt.Errorf(" unknown value `%s`", value) } @@ -631,9 +638,10 @@ func (e *PositiveBigIntegerField) SetRaw(value interface{}) error { e.Set(d) case string: v, err := StrTo(d).Uint64() - if err != nil { + if err == nil { e.Set(v) } + return err default: return fmt.Errorf(" unknown value `%s`", value) } diff --git a/src/vendor/github.com/astaxie/beego/orm/models_info_f.go b/src/vendor/github.com/astaxie/beego/orm/models_info_f.go index bbb7d71fe..7044b0bdb 100644 --- a/src/vendor/github.com/astaxie/beego/orm/models_info_f.go +++ b/src/vendor/github.com/astaxie/beego/orm/models_info_f.go @@ -136,6 +136,7 @@ type fieldInfo struct { decimals int isFielder bool // implement Fielder interface onDelete string + description string } // new field info @@ -244,8 +245,10 @@ checkType: if err != nil { goto end } - if fieldType == TypeCharField { + if fieldType == TypeVarCharField { switch tags["type"] { + case "char": + fieldType = TypeCharField case "text": fieldType = TypeTextField case "json": @@ -298,6 +301,7 @@ checkType: fi.sf = sf fi.fullName = mi.fullName + mName + "." + sf.Name + fi.description = tags["description"] fi.null = attrs["null"] fi.index = attrs["index"] fi.auto = attrs["auto"] @@ -357,7 +361,7 @@ checkType: switch fieldType { case TypeBooleanField: - case TypeCharField, TypeJSONField, TypeJsonbField: + case TypeVarCharField, TypeCharField, TypeJSONField, TypeJsonbField: if size != "" { v, e := StrTo(size).Int32() if e != nil { diff --git a/src/vendor/github.com/astaxie/beego/orm/models_info_m.go b/src/vendor/github.com/astaxie/beego/orm/models_info_m.go index 4a3a37f94..a4d733b6c 100644 --- a/src/vendor/github.com/astaxie/beego/orm/models_info_m.go +++ b/src/vendor/github.com/astaxie/beego/orm/models_info_m.go @@ -75,7 +75,8 @@ func addModelFields(mi *modelInfo, ind reflect.Value, mName string, index []int) break } //record current field index - fi.fieldIndex = append(index, i) + fi.fieldIndex = append(fi.fieldIndex, index...) + fi.fieldIndex = append(fi.fieldIndex, i) fi.mi = mi fi.inModel = true if !mi.fields.Add(fi) { diff --git a/src/vendor/github.com/astaxie/beego/orm/models_utils.go b/src/vendor/github.com/astaxie/beego/orm/models_utils.go index 44a0e76a1..71127a6ba 100644 --- a/src/vendor/github.com/astaxie/beego/orm/models_utils.go +++ b/src/vendor/github.com/astaxie/beego/orm/models_utils.go @@ -44,6 +44,7 @@ var supportTag = map[string]int{ "decimals": 2, "on_delete": 2, "type": 2, + "description": 2, } // get reflect.Type name with package path. @@ -65,7 +66,7 @@ func getTableName(val reflect.Value) string { return snakeString(reflect.Indirect(val).Type().Name()) } -// get table engine, mysiam or innodb. +// get table engine, myisam or innodb. func getTableEngine(val reflect.Value) string { fun := val.MethodByName("TableEngine") if fun.IsValid() { @@ -109,7 +110,7 @@ func getTableUnique(val reflect.Value) [][]string { func getColumnName(ft int, addrField reflect.Value, sf reflect.StructField, col string) string { column := col if col == "" { - column = snakeString(sf.Name) + column = nameStrategyMap[nameStrategy](sf.Name) } switch ft { case RelForeignKey, RelOneToOne: @@ -149,7 +150,7 @@ func getFieldType(val reflect.Value) (ft int, err error) { case reflect.TypeOf(new(bool)): ft = TypeBooleanField case reflect.TypeOf(new(string)): - ft = TypeCharField + ft = TypeVarCharField case reflect.TypeOf(new(time.Time)): ft = TypeDateTimeField default: @@ -176,7 +177,7 @@ func getFieldType(val reflect.Value) (ft int, err error) { case reflect.Bool: ft = TypeBooleanField case reflect.String: - ft = TypeCharField + ft = TypeVarCharField default: if elm.Interface() == nil { panic(fmt.Errorf("%s is nil pointer, may be miss setting tag", val)) @@ -189,7 +190,7 @@ func getFieldType(val reflect.Value) (ft int, err error) { case sql.NullBool: ft = TypeBooleanField case sql.NullString: - ft = TypeCharField + ft = TypeVarCharField case time.Time: ft = TypeDateTimeField } diff --git a/src/vendor/github.com/astaxie/beego/orm/orm.go b/src/vendor/github.com/astaxie/beego/orm/orm.go index fcf82590f..11e38fd94 100644 --- a/src/vendor/github.com/astaxie/beego/orm/orm.go +++ b/src/vendor/github.com/astaxie/beego/orm/orm.go @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +// +build go1.8 + // Package orm provide ORM for MySQL/PostgreSQL/sqlite // Simple Usage // @@ -52,11 +54,13 @@ package orm import ( + "context" "database/sql" "errors" "fmt" "os" "reflect" + "sync" "time" ) @@ -69,7 +73,7 @@ const ( var ( Debug = false DebugLog = NewLog(os.Stdout) - DefaultRowsLimit = 1000 + DefaultRowsLimit = -1 DefaultRelsDepth = 2 DefaultTimeLoc = time.Local ErrTxHasBegan = errors.New(" transaction already begin") @@ -422,7 +426,7 @@ func (o *orm) getRelQs(md interface{}, mi *modelInfo, fi *fieldInfo) *querySet { func (o *orm) QueryTable(ptrStructOrTableName interface{}) (qs QuerySeter) { var name string if table, ok := ptrStructOrTableName.(string); ok { - name = snakeString(table) + name = nameStrategyMap[defaultNameStrategy](table) if mi, ok := modelCache.get(name); ok { qs = newQuerySet(o, mi) } @@ -458,11 +462,15 @@ func (o *orm) Using(name string) error { // begin transaction func (o *orm) Begin() error { + return o.BeginTx(context.Background(), nil) +} + +func (o *orm) BeginTx(ctx context.Context, opts *sql.TxOptions) error { if o.isTx { return ErrTxHasBegan } var tx *sql.Tx - tx, err := o.db.(txer).Begin() + tx, err := o.db.(txer).BeginTx(ctx, opts) if err != nil { return err } @@ -515,6 +523,15 @@ func (o *orm) Driver() Driver { return driver(o.alias.Name) } +// return sql.DBStats for current database +func (o *orm) DBStats() *sql.DBStats { + if o.alias != nil && o.alias.DB != nil { + stats := o.alias.DB.DB.Stats() + return &stats + } + return nil +} + // NewOrm create new orm func NewOrm() Ormer { BootStrap() // execute only once @@ -541,6 +558,13 @@ func NewOrmWithDB(driverName, aliasName string, db *sql.DB) (Ormer, error) { al.Name = aliasName al.DriverName = driverName + al.DB = &DB{ + RWMutex: new(sync.RWMutex), + DB: db, + stmts: make(map[string]*sql.Stmt), + } + + detectTZ(al) o := new(orm) o.alias = al diff --git a/src/vendor/github.com/astaxie/beego/orm/orm_conds.go b/src/vendor/github.com/astaxie/beego/orm/orm_conds.go index f6e389ec7..f3fd66f0b 100644 --- a/src/vendor/github.com/astaxie/beego/orm/orm_conds.go +++ b/src/vendor/github.com/astaxie/beego/orm/orm_conds.go @@ -31,6 +31,8 @@ type condValue struct { isOr bool isNot bool isCond bool + isRaw bool + sql string } // Condition struct. @@ -45,6 +47,15 @@ func NewCondition() *Condition { return c } +// Raw add raw sql to condition +func (c Condition) Raw(expr string, sql string) *Condition { + if len(sql) == 0 { + panic(fmt.Errorf(" sql cannot empty")) + } + c.params = append(c.params, condValue{exprs: strings.Split(expr, ExprSep), sql: sql, isRaw: true}) + return &c +} + // And add expression to condition func (c Condition) And(expr string, args ...interface{}) *Condition { if expr == "" || len(args) == 0 { diff --git a/src/vendor/github.com/astaxie/beego/orm/orm_log.go b/src/vendor/github.com/astaxie/beego/orm/orm_log.go index 26c73f9ee..f107bb59e 100644 --- a/src/vendor/github.com/astaxie/beego/orm/orm_log.go +++ b/src/vendor/github.com/astaxie/beego/orm/orm_log.go @@ -15,6 +15,7 @@ package orm import ( + "context" "database/sql" "fmt" "io" @@ -28,6 +29,9 @@ type Log struct { *log.Logger } +//costomer log func +var LogFunc func(query map[string]interface{}) + // NewLog set io.Writer to create a Logger. func NewLog(out io.Writer) *Log { d := new(Log) @@ -36,12 +40,15 @@ func NewLog(out io.Writer) *Log { } func debugLogQueies(alias *alias, operaton, query string, t time.Time, err error, args ...interface{}) { + var logMap = make(map[string]interface{}) sub := time.Now().Sub(t) / 1e5 elsp := float64(int(sub)) / 10.0 + logMap["cost_time"] = elsp flag := " OK" if err != nil { flag = "FAIL" } + logMap["flag"] = flag con := fmt.Sprintf(" -[Queries/%s] - [%s / %11s / %7.1fms] - [%s]", alias.Name, flag, operaton, elsp, query) cons := make([]string, 0, len(args)) for _, arg := range args { @@ -53,6 +60,10 @@ func debugLogQueies(alias *alias, operaton, query string, t time.Time, err error if err != nil { con += " - " + err.Error() } + logMap["sql"] = fmt.Sprintf("%s-`%s`", query, strings.Join(cons, "`, `")) + if LogFunc != nil{ + LogFunc(logMap) + } DebugLog.Println(con) } @@ -122,6 +133,13 @@ func (d *dbQueryLog) Prepare(query string) (*sql.Stmt, error) { return stmt, err } +func (d *dbQueryLog) PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) { + a := time.Now() + stmt, err := d.db.PrepareContext(ctx, query) + debugLogQueies(d.alias, "db.Prepare", query, a, err) + return stmt, err +} + func (d *dbQueryLog) Exec(query string, args ...interface{}) (sql.Result, error) { a := time.Now() res, err := d.db.Exec(query, args...) @@ -129,6 +147,13 @@ func (d *dbQueryLog) Exec(query string, args ...interface{}) (sql.Result, error) return res, err } +func (d *dbQueryLog) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { + a := time.Now() + res, err := d.db.ExecContext(ctx, query, args...) + debugLogQueies(d.alias, "db.Exec", query, a, err, args...) + return res, err +} + func (d *dbQueryLog) Query(query string, args ...interface{}) (*sql.Rows, error) { a := time.Now() res, err := d.db.Query(query, args...) @@ -136,6 +161,13 @@ func (d *dbQueryLog) Query(query string, args ...interface{}) (*sql.Rows, error) return res, err } +func (d *dbQueryLog) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) { + a := time.Now() + res, err := d.db.QueryContext(ctx, query, args...) + debugLogQueies(d.alias, "db.Query", query, a, err, args...) + return res, err +} + func (d *dbQueryLog) QueryRow(query string, args ...interface{}) *sql.Row { a := time.Now() res := d.db.QueryRow(query, args...) @@ -143,6 +175,13 @@ func (d *dbQueryLog) QueryRow(query string, args ...interface{}) *sql.Row { return res } +func (d *dbQueryLog) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row { + a := time.Now() + res := d.db.QueryRowContext(ctx, query, args...) + debugLogQueies(d.alias, "db.QueryRow", query, a, nil, args...) + return res +} + func (d *dbQueryLog) Begin() (*sql.Tx, error) { a := time.Now() tx, err := d.db.(txer).Begin() @@ -150,6 +189,13 @@ func (d *dbQueryLog) Begin() (*sql.Tx, error) { return tx, err } +func (d *dbQueryLog) BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error) { + a := time.Now() + tx, err := d.db.(txer).BeginTx(ctx, opts) + debugLogQueies(d.alias, "db.BeginTx", "START TRANSACTION", a, err) + return tx, err +} + func (d *dbQueryLog) Commit() error { a := time.Now() err := d.db.(txEnder).Commit() diff --git a/src/vendor/github.com/astaxie/beego/orm/orm_queryset.go b/src/vendor/github.com/astaxie/beego/orm/orm_queryset.go index 4e33646d6..7f2fdb8fb 100644 --- a/src/vendor/github.com/astaxie/beego/orm/orm_queryset.go +++ b/src/vendor/github.com/astaxie/beego/orm/orm_queryset.go @@ -15,6 +15,7 @@ package orm import ( + "context" "fmt" ) @@ -55,16 +56,19 @@ func ColValue(opt operator, value interface{}) interface{} { // real query struct type querySet struct { - mi *modelInfo - cond *Condition - related []string - relDepth int - limit int64 - offset int64 - groups []string - orders []string - distinct bool - orm *orm + mi *modelInfo + cond *Condition + related []string + relDepth int + limit int64 + offset int64 + groups []string + orders []string + distinct bool + forupdate bool + orm *orm + ctx context.Context + forContext bool } var _ QuerySeter = new(querySet) @@ -78,6 +82,15 @@ func (o querySet) Filter(expr string, args ...interface{}) QuerySeter { return &o } +// add raw sql to querySeter. +func (o querySet) FilterRaw(expr string, sql string) QuerySeter { + if o.cond == nil { + o.cond = NewCondition() + } + o.cond = o.cond.Raw(expr, sql) + return &o +} + // add NOT condition to querySeter. func (o querySet) Exclude(expr string, args ...interface{}) QuerySeter { if o.cond == nil { @@ -127,6 +140,12 @@ func (o querySet) Distinct() QuerySeter { return &o } +// add FOR UPDATE to SELECT +func (o querySet) ForUpdate() QuerySeter { + o.forupdate = true + return &o +} + // set relation model to query together. // it will query relation models and assign to parent model. func (o querySet) RelatedSel(params ...interface{}) QuerySeter { @@ -259,6 +278,13 @@ func (o *querySet) RowsToStruct(ptrStruct interface{}, keyCol, valueCol string) panic(ErrNotImplement) } +// set context to QuerySeter. +func (o querySet) WithContext(ctx context.Context) QuerySeter { + o.ctx = ctx + o.forContext = true + return &o +} + // create new QuerySeter. func newQuerySet(orm *orm, mi *modelInfo) QuerySeter { o := new(querySet) diff --git a/src/vendor/github.com/astaxie/beego/orm/orm_raw.go b/src/vendor/github.com/astaxie/beego/orm/orm_raw.go index c8e741ea0..3325a7ea7 100644 --- a/src/vendor/github.com/astaxie/beego/orm/orm_raw.go +++ b/src/vendor/github.com/astaxie/beego/orm/orm_raw.go @@ -150,8 +150,10 @@ func (o *rawSet) setFieldValue(ind reflect.Value, value interface{}) { case reflect.Struct: if value == nil { ind.Set(reflect.Zero(ind.Type())) - - } else if _, ok := ind.Interface().(time.Time); ok { + return + } + switch ind.Interface().(type) { + case time.Time: var str string switch d := value.(type) { case time.Time: @@ -178,7 +180,25 @@ func (o *rawSet) setFieldValue(ind reflect.Value, value interface{}) { } } } + case sql.NullString, sql.NullInt64, sql.NullFloat64, sql.NullBool: + indi := reflect.New(ind.Type()).Interface() + sc, ok := indi.(sql.Scanner) + if !ok { + return + } + err := sc.Scan(value) + if err == nil { + ind.Set(reflect.Indirect(reflect.ValueOf(sc))) + } } + + case reflect.Ptr: + if value == nil { + ind.Set(reflect.Zero(ind.Type())) + break + } + ind.Set(reflect.New(ind.Type().Elem())) + o.setFieldValue(reflect.Indirect(ind), value) } } @@ -358,7 +378,7 @@ func (o *rawSet) QueryRow(containers ...interface{}) error { _, tags := parseStructTag(fe.Tag.Get(defaultStructTagName)) var col string if col = tags["column"]; col == "" { - col = snakeString(fe.Name) + col = nameStrategyMap[nameStrategy](fe.Name) } if v, ok := columnsMp[col]; ok { value := reflect.ValueOf(v).Elem().Interface() @@ -509,7 +529,7 @@ func (o *rawSet) QueryRows(containers ...interface{}) (int64, error) { _, tags := parseStructTag(fe.Tag.Get(defaultStructTagName)) var col string if col = tags["column"]; col == "" { - col = snakeString(fe.Name) + col = nameStrategyMap[nameStrategy](fe.Name) } if v, ok := columnsMp[col]; ok { value := reflect.ValueOf(v).Elem().Interface() diff --git a/src/vendor/github.com/astaxie/beego/orm/types.go b/src/vendor/github.com/astaxie/beego/orm/types.go index 3e6a9e87d..2fd10774f 100644 --- a/src/vendor/github.com/astaxie/beego/orm/types.go +++ b/src/vendor/github.com/astaxie/beego/orm/types.go @@ -15,6 +15,7 @@ package orm import ( + "context" "database/sql" "reflect" "time" @@ -54,7 +55,7 @@ type Ormer interface { // for example: // user := new(User) // id, err = Ormer.Insert(user) - // user must a pointer and Insert will set user's pk field + // user must be a pointer and Insert will set user's pk field Insert(interface{}) (int64, error) // mysql:InsertOrUpdate(model) or InsertOrUpdate(model,"colu=colu+value") // if colu type is integer : can use(+-*/), string : convert(colu,"value") @@ -106,6 +107,17 @@ type Ormer interface { // ... // err = o.Rollback() Begin() error + // begin transaction with provided context and option + // the provided context is used until the transaction is committed or rolled back. + // if the context is canceled, the transaction will be rolled back. + // the provided TxOptions is optional and may be nil if defaults should be used. + // if a non-default isolation level is used that the driver doesn't support, an error will be returned. + // for example: + // o := NewOrm() + // err := o.BeginTx(context.Background(), &sql.TxOptions{Isolation: sql.LevelRepeatableRead}) + // ... + // err = o.Rollback() + BeginTx(ctx context.Context, opts *sql.TxOptions) error // commit transaction Commit() error // rollback transaction @@ -116,6 +128,7 @@ type Ormer interface { // // update user testing's name to slene Raw(query string, args ...interface{}) RawSeter Driver() Driver + DBStats() *sql.DBStats } // Inserter insert prepared statement @@ -135,6 +148,11 @@ type QuerySeter interface { // // time compare // qs.Filter("created", time.Now()) Filter(string, ...interface{}) QuerySeter + // add raw sql to querySeter. + // for example: + // qs.FilterRaw("user_id IN (SELECT id FROM profile WHERE age>=18)") + // //sql-> WHERE user_id IN (SELECT id FROM profile WHERE age>=18) + FilterRaw(string, string) QuerySeter // add NOT condition to querySeter. // have the same usage as Filter Exclude(string, ...interface{}) QuerySeter @@ -190,6 +208,10 @@ type QuerySeter interface { // Distinct(). // All(&permissions) Distinct() QuerySeter + // set FOR UPDATE to query. + // for example: + // o.QueryTable("user").Filter("uid", uid).ForUpdate().All(&users) + ForUpdate() QuerySeter // return QuerySeter execution result number // for example: // num, err = qs.Filter("profile__age__gt", 28).Count() @@ -374,16 +396,23 @@ type RawSeter interface { type stmtQuerier interface { Close() error Exec(args ...interface{}) (sql.Result, error) + //ExecContext(ctx context.Context, args ...interface{}) (sql.Result, error) Query(args ...interface{}) (*sql.Rows, error) + //QueryContext(args ...interface{}) (*sql.Rows, error) QueryRow(args ...interface{}) *sql.Row + //QueryRowContext(ctx context.Context, args ...interface{}) *sql.Row } // db querier type dbQuerier interface { Prepare(query string) (*sql.Stmt, error) + PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) Exec(query string, args ...interface{}) (sql.Result, error) + ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) Query(query string, args ...interface{}) (*sql.Rows, error) + QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) QueryRow(query string, args ...interface{}) *sql.Row + QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row } // type DB interface { @@ -397,6 +426,7 @@ type dbQuerier interface { // transaction beginner type txer interface { Begin() (*sql.Tx, error) + BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error) } // transaction ending diff --git a/src/vendor/github.com/astaxie/beego/orm/utils.go b/src/vendor/github.com/astaxie/beego/orm/utils.go index 669d47344..783927713 100644 --- a/src/vendor/github.com/astaxie/beego/orm/utils.go +++ b/src/vendor/github.com/astaxie/beego/orm/utils.go @@ -23,6 +23,18 @@ import ( "time" ) +type fn func(string) string + +var ( + nameStrategyMap = map[string]fn{ + defaultNameStrategy: snakeString, + SnakeAcronymNameStrategy: snakeStringWithAcronym, + } + defaultNameStrategy = "snakeString" + SnakeAcronymNameStrategy = "snakeStringWithAcronym" + nameStrategy = defaultNameStrategy +) + // StrTo is the target string type StrTo string @@ -198,7 +210,28 @@ func ToInt64(value interface{}) (d int64) { return } -// snake string, XxYy to xx_yy , XxYY to xx_yy +func snakeStringWithAcronym(s string) string { + data := make([]byte, 0, len(s)*2) + num := len(s) + for i := 0; i < num; i++ { + d := s[i] + before := false + after := false + if i > 0 { + before = s[i-1] >= 'a' && s[i-1] <= 'z' + } + if i+1 < num { + after = s[i+1] >= 'a' && s[i+1] <= 'z' + } + if i > 0 && d >= 'A' && d <= 'Z' && (before || after) { + data = append(data, '_') + } + data = append(data, d) + } + return strings.ToLower(string(data[:])) +} + +// snake string, XxYy to xx_yy , XxYY to xx_y_y func snakeString(s string) string { data := make([]byte, 0, len(s)*2) j := false @@ -216,6 +249,14 @@ func snakeString(s string) string { return strings.ToLower(string(data[:])) } +// SetNameStrategy set different name strategy +func SetNameStrategy(s string) { + if SnakeAcronymNameStrategy != s { + nameStrategy = defaultNameStrategy + } + nameStrategy = s +} + // camel string, xx_yy to XxYy func camelString(s string) string { data := make([]byte, 0, len(s)) diff --git a/src/vendor/github.com/astaxie/beego/parser.go b/src/vendor/github.com/astaxie/beego/parser.go index f787fd5cd..5e6b9111d 100644 --- a/src/vendor/github.com/astaxie/beego/parser.go +++ b/src/vendor/github.com/astaxie/beego/parser.go @@ -35,11 +35,11 @@ import ( "github.com/astaxie/beego/utils" ) -var globalRouterTemplate = `package routers +var globalRouterTemplate = `package {{.routersDir}} import ( "github.com/astaxie/beego" - "github.com/astaxie/beego/context/param" + "github.com/astaxie/beego/context/param"{{.globalimport}} ) func init() { @@ -52,6 +52,22 @@ var ( commentFilename string pkgLastupdate map[string]int64 genInfoList map[string][]ControllerComments + + routerHooks = map[string]int{ + "beego.BeforeStatic": BeforeStatic, + "beego.BeforeRouter": BeforeRouter, + "beego.BeforeExec": BeforeExec, + "beego.AfterExec": AfterExec, + "beego.FinishRouter": FinishRouter, + } + + routerHooksMapping = map[int]string{ + BeforeStatic: "beego.BeforeStatic", + BeforeRouter: "beego.BeforeRouter", + BeforeExec: "beego.BeforeExec", + AfterExec: "beego.AfterExec", + FinishRouter: "beego.FinishRouter", + } ) const commentPrefix = "commentsRouter_" @@ -102,6 +118,20 @@ type parsedComment struct { routerPath string methods []string params map[string]parsedParam + filters []parsedFilter + imports []parsedImport +} + +type parsedImport struct { + importPath string + importAlias string +} + +type parsedFilter struct { + pattern string + pos int + filter string + params []bool } type parsedParam struct { @@ -114,24 +144,69 @@ type parsedParam struct { func parserComments(f *ast.FuncDecl, controllerName, pkgpath string) error { if f.Doc != nil { - parsedComment, err := parseComment(f.Doc.List) + parsedComments, err := parseComment(f.Doc.List) if err != nil { return err } - if parsedComment.routerPath != "" { - key := pkgpath + ":" + controllerName - cc := ControllerComments{} - cc.Method = f.Name.String() - cc.Router = parsedComment.routerPath - cc.AllowHTTPMethods = parsedComment.methods - cc.MethodParams = buildMethodParams(f.Type.Params.List, parsedComment) - genInfoList[key] = append(genInfoList[key], cc) + for _, parsedComment := range parsedComments { + if parsedComment.routerPath != "" { + key := pkgpath + ":" + controllerName + cc := ControllerComments{} + cc.Method = f.Name.String() + cc.Router = parsedComment.routerPath + cc.AllowHTTPMethods = parsedComment.methods + cc.MethodParams = buildMethodParams(f.Type.Params.List, parsedComment) + cc.FilterComments = buildFilters(parsedComment.filters) + cc.ImportComments = buildImports(parsedComment.imports) + genInfoList[key] = append(genInfoList[key], cc) + } } - } return nil } +func buildImports(pis []parsedImport) []*ControllerImportComments { + var importComments []*ControllerImportComments + + for _, pi := range pis { + importComments = append(importComments, &ControllerImportComments{ + ImportPath: pi.importPath, + ImportAlias: pi.importAlias, + }) + } + + return importComments +} + +func buildFilters(pfs []parsedFilter) []*ControllerFilterComments { + var filterComments []*ControllerFilterComments + + for _, pf := range pfs { + var ( + returnOnOutput bool + resetParams bool + ) + + if len(pf.params) >= 1 { + returnOnOutput = pf.params[0] + } + + if len(pf.params) >= 2 { + resetParams = pf.params[1] + } + + filterComments = append(filterComments, &ControllerFilterComments{ + Filter: pf.filter, + Pattern: pf.pattern, + Pos: pf.pos, + ReturnOnOutput: returnOnOutput, + ResetParams: resetParams, + }) + } + + return filterComments +} + func buildMethodParams(funcParams []*ast.Field, pc *parsedComment) []*param.MethodParam { result := make([]*param.MethodParam, 0, len(funcParams)) for _, fparam := range funcParams { @@ -177,26 +252,15 @@ func paramInPath(name, route string) bool { var routeRegex = regexp.MustCompile(`@router\s+(\S+)(?:\s+\[(\S+)\])?`) -func parseComment(lines []*ast.Comment) (pc *parsedComment, err error) { - pc = &parsedComment{} +func parseComment(lines []*ast.Comment) (pcs []*parsedComment, err error) { + pcs = []*parsedComment{} + params := map[string]parsedParam{} + filters := []parsedFilter{} + imports := []parsedImport{} + for _, c := range lines { t := strings.TrimSpace(strings.TrimLeft(c.Text, "//")) - if strings.HasPrefix(t, "@router") { - matches := routeRegex.FindStringSubmatch(t) - if len(matches) == 3 { - pc.routerPath = matches[1] - methods := matches[2] - if methods == "" { - pc.methods = []string{"get"} - //pc.hasGet = true - } else { - pc.methods = strings.Split(methods, ",") - //pc.hasGet = strings.Contains(methods, "get") - } - } else { - return nil, errors.New("Router information is missing") - } - } else if strings.HasPrefix(t, "@Param") { + if strings.HasPrefix(t, "@Param") { pv := getparams(strings.TrimSpace(strings.TrimLeft(t, "@Param"))) if len(pv) < 4 { logs.Error("Invalid @Param format. Needs at least 4 parameters") @@ -217,17 +281,99 @@ func parseComment(lines []*ast.Comment) (pc *parsedComment, err error) { p.defValue = pv[3] p.required, _ = strconv.ParseBool(pv[4]) } - if pc.params == nil { - pc.params = map[string]parsedParam{} + params[funcParamName] = p + } + } + + for _, c := range lines { + t := strings.TrimSpace(strings.TrimLeft(c.Text, "//")) + if strings.HasPrefix(t, "@Import") { + iv := getparams(strings.TrimSpace(strings.TrimLeft(t, "@Import"))) + if len(iv) == 0 || len(iv) > 2 { + logs.Error("Invalid @Import format. Only accepts 1 or 2 parameters") + continue + } + + p := parsedImport{} + p.importPath = iv[0] + + if len(iv) == 2 { + p.importAlias = iv[1] + } + + imports = append(imports, p) + } + } + +filterLoop: + for _, c := range lines { + t := strings.TrimSpace(strings.TrimLeft(c.Text, "//")) + if strings.HasPrefix(t, "@Filter") { + fv := getparams(strings.TrimSpace(strings.TrimLeft(t, "@Filter"))) + if len(fv) < 3 { + logs.Error("Invalid @Filter format. Needs at least 3 parameters") + continue filterLoop + } + + p := parsedFilter{} + p.pattern = fv[0] + posName := fv[1] + if pos, exists := routerHooks[posName]; exists { + p.pos = pos + } else { + logs.Error("Invalid @Filter pos: ", posName) + continue filterLoop + } + + p.filter = fv[2] + fvParams := fv[3:] + for _, fvParam := range fvParams { + switch fvParam { + case "true": + p.params = append(p.params, true) + case "false": + p.params = append(p.params, false) + default: + logs.Error("Invalid @Filter param: ", fvParam) + continue filterLoop + } + } + + filters = append(filters, p) + } + } + + for _, c := range lines { + var pc = &parsedComment{} + pc.params = params + pc.filters = filters + pc.imports = imports + + t := strings.TrimSpace(strings.TrimLeft(c.Text, "//")) + if strings.HasPrefix(t, "@router") { + t := strings.TrimSpace(strings.TrimLeft(c.Text, "//")) + matches := routeRegex.FindStringSubmatch(t) + if len(matches) == 3 { + pc.routerPath = matches[1] + methods := matches[2] + if methods == "" { + pc.methods = []string{"get"} + //pc.hasGet = true + } else { + pc.methods = strings.Split(methods, ",") + //pc.hasGet = strings.Contains(methods, "get") + } + pcs = append(pcs, pc) + } else { + return nil, errors.New("Router information is missing") } - pc.params[funcParamName] = p } } return } // direct copy from bee\g_docs.go -// analisys params return []string +// analysis params return []string // @Param query form string true "The email for login" // [query form string true "The email for login"] func getparams(str string) []string { @@ -266,8 +412,9 @@ func genRouterCode(pkgRealpath string) { os.Mkdir(getRouterDir(pkgRealpath), 0755) logs.Info("generate router from comments") var ( - globalinfo string - sortKey []string + globalinfo string + globalimport string + sortKey []string ) for k := range genInfoList { sortKey = append(sortKey, k) @@ -285,6 +432,7 @@ func genRouterCode(pkgRealpath string) { } allmethod = strings.TrimRight(allmethod, ",") + "}" } + params := "nil" if len(c.Params) > 0 { params = "[]map[string]string{" @@ -295,6 +443,7 @@ func genRouterCode(pkgRealpath string) { } params = strings.TrimRight(params, ",") + "}" } + methodParams := "param.Make(" if len(c.MethodParams) > 0 { lines := make([]string, 0, len(c.MethodParams)) @@ -306,24 +455,72 @@ func genRouterCode(pkgRealpath string) { ",\n " } methodParams += ")" + + imports := "" + if len(c.ImportComments) > 0 { + for _, i := range c.ImportComments { + var s string + if i.ImportAlias != "" { + s = fmt.Sprintf(` + %s "%s"`, i.ImportAlias, i.ImportPath) + } else { + s = fmt.Sprintf(` + "%s"`, i.ImportPath) + } + if !strings.Contains(globalimport, s) { + imports += s + } + } + } + + filters := "" + if len(c.FilterComments) > 0 { + for _, f := range c.FilterComments { + filters += fmt.Sprintf(` &beego.ControllerFilter{ + Pattern: "%s", + Pos: %s, + Filter: %s, + ReturnOnOutput: %v, + ResetParams: %v, + },`, f.Pattern, routerHooksMapping[f.Pos], f.Filter, f.ReturnOnOutput, f.ResetParams) + } + } + + if filters == "" { + filters = "nil" + } else { + filters = fmt.Sprintf(`[]*beego.ControllerFilter{ +%s + }`, filters) + } + + globalimport += imports + globalinfo = globalinfo + ` - beego.GlobalControllerRouter["` + k + `"] = append(beego.GlobalControllerRouter["` + k + `"], - beego.ControllerComments{ - Method: "` + strings.TrimSpace(c.Method) + `", - ` + "Router: `" + c.Router + "`" + `, - AllowHTTPMethods: ` + allmethod + `, - MethodParams: ` + methodParams + `, - Params: ` + params + `}) + beego.GlobalControllerRouter["` + k + `"] = append(beego.GlobalControllerRouter["` + k + `"], + beego.ControllerComments{ + Method: "` + strings.TrimSpace(c.Method) + `", + ` + "Router: `" + c.Router + "`" + `, + AllowHTTPMethods: ` + allmethod + `, + MethodParams: ` + methodParams + `, + Filters: ` + filters + `, + Params: ` + params + `}) ` } } + if globalinfo != "" { f, err := os.Create(filepath.Join(getRouterDir(pkgRealpath), commentFilename)) if err != nil { panic(err) } defer f.Close() - f.WriteString(strings.Replace(globalRouterTemplate, "{{.globalinfo}}", globalinfo, -1)) + + routersDir := AppConfig.DefaultString("routersdir", "routers") + content := strings.Replace(globalRouterTemplate, "{{.globalinfo}}", globalinfo, -1) + content = strings.Replace(content, "{{.routersDir}}", routersDir, -1) + content = strings.Replace(content, "{{.globalimport}}", globalimport, -1) + f.WriteString(content) } } @@ -379,7 +576,8 @@ func getpathTime(pkgRealpath string) (lastupdate int64, err error) { func getRouterDir(pkgRealpath string) string { dir := filepath.Dir(pkgRealpath) for { - d := filepath.Join(dir, "routers") + routersDir := AppConfig.DefaultString("routersdir", "routers") + d := filepath.Join(dir, routersDir) if utils.FileExists(d) { return d } diff --git a/src/vendor/github.com/astaxie/beego/router.go b/src/vendor/github.com/astaxie/beego/router.go index e5a4e80de..3593be4c5 100644 --- a/src/vendor/github.com/astaxie/beego/router.go +++ b/src/vendor/github.com/astaxie/beego/router.go @@ -15,12 +15,12 @@ package beego import ( + "errors" "fmt" "net/http" "path" "path/filepath" "reflect" - "runtime" "strconv" "strings" "sync" @@ -50,28 +50,28 @@ const ( var ( // HTTPMETHOD list the supported http methods. - HTTPMETHOD = map[string]string{ - "GET": "GET", - "POST": "POST", - "PUT": "PUT", - "DELETE": "DELETE", - "PATCH": "PATCH", - "OPTIONS": "OPTIONS", - "HEAD": "HEAD", - "TRACE": "TRACE", - "CONNECT": "CONNECT", - "MKCOL": "MKCOL", - "COPY": "COPY", - "MOVE": "MOVE", - "PROPFIND": "PROPFIND", - "PROPPATCH": "PROPPATCH", - "LOCK": "LOCK", - "UNLOCK": "UNLOCK", + HTTPMETHOD = map[string]bool{ + "GET": true, + "POST": true, + "PUT": true, + "DELETE": true, + "PATCH": true, + "OPTIONS": true, + "HEAD": true, + "TRACE": true, + "CONNECT": true, + "MKCOL": true, + "COPY": true, + "MOVE": true, + "PROPFIND": true, + "PROPPATCH": true, + "LOCK": true, + "UNLOCK": true, } // these beego.Controller's methods shouldn't reflect to AutoRouter exceptMethod = []string{"Init", "Prepare", "Finish", "Render", "RenderString", "RenderBytes", "Redirect", "Abort", "StopRun", "UrlFor", "ServeJSON", "ServeJSONP", - "ServeXML", "Input", "ParseForm", "GetString", "GetStrings", "GetInt", "GetBool", + "ServeYAML", "ServeXML", "Input", "ParseForm", "GetString", "GetStrings", "GetInt", "GetBool", "GetFloat", "GetFile", "SaveToFile", "StartSession", "SetSession", "GetSession", "DelSession", "SessionRegenerateID", "DestroySession", "IsAjax", "GetSecureCookie", "SetSecureCookie", "XsrfToken", "CheckXsrfCookie", "XsrfFormHtml", @@ -117,6 +117,7 @@ type ControllerInfo struct { handler http.Handler runFunction FilterFunc routerType int + initialize func() ControllerInterface methodParams []*param.MethodParam } @@ -132,14 +133,15 @@ type ControllerRegister struct { // NewControllerRegister returns a new ControllerRegister. func NewControllerRegister() *ControllerRegister { - cr := &ControllerRegister{ + return &ControllerRegister{ routers: make(map[string]*Tree), policies: make(map[string]*Tree), + pool: sync.Pool{ + New: func() interface{} { + return beecontext.NewContext() + }, + }, } - cr.pool.New = func() interface{} { - return beecontext.NewContext() - } - return cr } // Add controller handler and pattern rules to ControllerRegister. @@ -169,7 +171,7 @@ func (p *ControllerRegister) addWithMethodParams(pattern string, c ControllerInt } comma := strings.Split(colon[0], ",") for _, m := range comma { - if _, ok := HTTPMETHOD[strings.ToUpper(m)]; m == "*" || ok { + if m == "*" || HTTPMETHOD[strings.ToUpper(m)] { if val := reflectVal.MethodByName(colon[1]); val.IsValid() { methods[strings.ToUpper(m)] = colon[1] } else { @@ -187,15 +189,39 @@ func (p *ControllerRegister) addWithMethodParams(pattern string, c ControllerInt route.methods = methods route.routerType = routerTypeBeego route.controllerType = t + route.initialize = func() ControllerInterface { + vc := reflect.New(route.controllerType) + execController, ok := vc.Interface().(ControllerInterface) + if !ok { + panic("controller is not ControllerInterface") + } + + elemVal := reflect.ValueOf(c).Elem() + elemType := reflect.TypeOf(c).Elem() + execElem := reflect.ValueOf(execController).Elem() + + numOfFields := elemVal.NumField() + for i := 0; i < numOfFields; i++ { + fieldType := elemType.Field(i) + elemField := execElem.FieldByName(fieldType.Name) + if elemField.CanSet() { + fieldVal := elemVal.Field(i) + elemField.Set(fieldVal) + } + } + + return execController + } + route.methodParams = methodParams if len(methods) == 0 { - for _, m := range HTTPMETHOD { + for m := range HTTPMETHOD { p.addToRouter(m, pattern, route) } } else { for k := range methods { if k == "*" { - for _, m := range HTTPMETHOD { + for m := range HTTPMETHOD { p.addToRouter(m, pattern, route) } } else { @@ -252,6 +278,10 @@ func (p *ControllerRegister) Include(cList ...ControllerInterface) { key := t.PkgPath() + ":" + t.Name() if comm, ok := GlobalControllerRouter[key]; ok { for _, a := range comm { + for _, f := range a.Filters { + p.InsertFilter(f.Pattern, f.Pos, f.Filter, f.ReturnOnOutput, f.ResetParams) + } + p.addWithMethodParams(a.Router, c, a.MethodParams, strings.Join(a.AllowHTTPMethods, ",")+":"+a.Method) } } @@ -337,7 +367,7 @@ func (p *ControllerRegister) Any(pattern string, f FilterFunc) { // }) func (p *ControllerRegister) AddMethod(method, pattern string, f FilterFunc) { method = strings.ToUpper(method) - if _, ok := HTTPMETHOD[method]; method != "*" && !ok { + if method != "*" && !HTTPMETHOD[method] { panic("not support http method: " + method) } route := &ControllerInfo{} @@ -346,7 +376,7 @@ func (p *ControllerRegister) AddMethod(method, pattern string, f FilterFunc) { route.runFunction = f methods := make(map[string]string) if method == "*" { - for _, val := range HTTPMETHOD { + for val := range HTTPMETHOD { methods[val] = val } } else { @@ -355,7 +385,7 @@ func (p *ControllerRegister) AddMethod(method, pattern string, f FilterFunc) { route.methods = methods for k := range methods { if k == "*" { - for _, m := range HTTPMETHOD { + for m := range HTTPMETHOD { p.addToRouter(m, pattern, route) } } else { @@ -375,7 +405,7 @@ func (p *ControllerRegister) Handler(pattern string, h http.Handler, options ... pattern = path.Join(pattern, "?:all(.*)") } } - for _, m := range HTTPMETHOD { + for m := range HTTPMETHOD { p.addToRouter(m, pattern, route) } } @@ -410,7 +440,7 @@ func (p *ControllerRegister) AddAutoPrefix(prefix string, c ControllerInterface) patternFix := path.Join(prefix, strings.ToLower(controllerName), strings.ToLower(rt.Method(i).Name)) patternFixInit := path.Join(prefix, controllerName, rt.Method(i).Name) route.pattern = pattern - for _, m := range HTTPMETHOD { + for m := range HTTPMETHOD { p.addToRouter(m, pattern, route) p.addToRouter(m, patternInit, route) p.addToRouter(m, patternFix, route) @@ -449,8 +479,7 @@ func (p *ControllerRegister) InsertFilter(pattern string, pos int, filter Filter // add Filter into func (p *ControllerRegister) insertFilterRouter(pos int, mr *FilterRouter) (err error) { if pos < BeforeStatic || pos > FinishRouter { - err = fmt.Errorf("can not find your filter position") - return + return errors.New("can not find your filter position") } p.enableFilter = true p.filters[pos] = append(p.filters[pos], mr) @@ -480,10 +509,10 @@ func (p *ControllerRegister) URLFor(endpoint string, values ...interface{}) stri } } } - controllName := strings.Join(paths[:len(paths)-1], "/") + controllerName := strings.Join(paths[:len(paths)-1], "/") methodName := paths[len(paths)-1] for m, t := range p.routers { - ok, url := p.geturl(t, "/", controllName, methodName, params, m) + ok, url := p.getURL(t, "/", controllerName, methodName, params, m) if ok { return url } @@ -491,17 +520,17 @@ func (p *ControllerRegister) URLFor(endpoint string, values ...interface{}) stri return "" } -func (p *ControllerRegister) geturl(t *Tree, url, controllName, methodName string, params map[string]string, httpMethod string) (bool, string) { +func (p *ControllerRegister) getURL(t *Tree, url, controllerName, methodName string, params map[string]string, httpMethod string) (bool, string) { for _, subtree := range t.fixrouters { u := path.Join(url, subtree.prefix) - ok, u := p.geturl(subtree, u, controllName, methodName, params, httpMethod) + ok, u := p.getURL(subtree, u, controllerName, methodName, params, httpMethod) if ok { return ok, u } } if t.wildcard != nil { u := path.Join(url, urlPlaceholder) - ok, u := p.geturl(t.wildcard, u, controllName, methodName, params, httpMethod) + ok, u := p.getURL(t.wildcard, u, controllerName, methodName, params, httpMethod) if ok { return ok, u } @@ -509,9 +538,9 @@ func (p *ControllerRegister) geturl(t *Tree, url, controllName, methodName strin for _, l := range t.leaves { if c, ok := l.runObject.(*ControllerInfo); ok { if c.routerType == routerTypeBeego && - strings.HasSuffix(path.Join(c.controllerType.PkgPath(), c.controllerType.Name()), controllName) { + strings.HasSuffix(path.Join(c.controllerType.PkgPath(), c.controllerType.Name()), controllerName) { find := false - if _, ok := HTTPMETHOD[strings.ToUpper(methodName)]; ok { + if HTTPMETHOD[strings.ToUpper(methodName)] { if len(c.methods) == 0 { find = true } else if m, ok := c.methods[strings.ToUpper(methodName)]; ok && m == strings.ToUpper(methodName) { @@ -548,18 +577,18 @@ func (p *ControllerRegister) geturl(t *Tree, url, controllName, methodName strin } } } - canskip := false + canSkip := false for _, v := range l.wildcards { if v == ":" { - canskip = true + canSkip = true continue } if u, ok := params[v]; ok { delete(params, v) url = strings.Replace(url, urlPlaceholder, u, 1) } else { - if canskip { - canskip = false + if canSkip { + canSkip = false continue } return false, "" @@ -568,27 +597,27 @@ func (p *ControllerRegister) geturl(t *Tree, url, controllName, methodName strin return true, url + toURL(params) } var i int - var startreg bool - regurl := "" + var startReg bool + regURL := "" for _, v := range strings.Trim(l.regexps.String(), "^$") { if v == '(' { - startreg = true + startReg = true continue } else if v == ')' { - startreg = false + startReg = false if v, ok := params[l.wildcards[i]]; ok { delete(params, l.wildcards[i]) - regurl = regurl + v + regURL = regURL + v i++ } else { break } - } else if !startreg { - regurl = string(append([]rune(regurl), v)) + } else if !startReg { + regURL = string(append([]rune(regURL), v)) } } - if l.regexps.MatchString(regurl) { - ps := strings.Split(regurl, "/") + if l.regexps.MatchString(regURL) { + ps := strings.Split(regURL, "/") for _, p := range ps { url = strings.Replace(url, urlPlaceholder, p, 1) } @@ -659,8 +688,8 @@ func (p *ControllerRegister) ServeHTTP(rw http.ResponseWriter, r *http.Request) } // filter wrong http method - if _, ok := HTTPMETHOD[r.Method]; !ok { - http.Error(rw, "Method Not Allowed", 405) + if !HTTPMETHOD[r.Method] { + exception("405", context) goto Admin } @@ -749,7 +778,7 @@ func (p *ControllerRegister) ServeHTTP(rw http.ResponseWriter, r *http.Request) runRouter = routerInfo.controllerType methodParams = routerInfo.methodParams method := r.Method - if r.Method == http.MethodPost && context.Input.Query("_method") == http.MethodPost { + if r.Method == http.MethodPost && context.Input.Query("_method") == http.MethodPut { method = http.MethodPut } if r.Method == http.MethodPost && context.Input.Query("_method") == http.MethodDelete { @@ -768,14 +797,20 @@ func (p *ControllerRegister) ServeHTTP(rw http.ResponseWriter, r *http.Request) // also defined runRouter & runMethod from filter if !isRunnable { //Invoke the request handler - vc := reflect.New(runRouter) - execController, ok := vc.Interface().(ControllerInterface) - if !ok { - panic("controller is not ControllerInterface") + var execController ControllerInterface + if routerInfo != nil && routerInfo.initialize != nil { + execController = routerInfo.initialize() + } else { + vc := reflect.New(runRouter) + var ok bool + execController, ok = vc.Interface().(ControllerInterface) + if !ok { + panic("controller is not ControllerInterface") + } } //call the controller init function - execController.Init(context, runRouter.Name(), runMethod, vc.Interface()) + execController.Init(context, runRouter.Name(), runMethod, execController) //call prepare function execController.Prepare() @@ -808,8 +843,11 @@ func (p *ControllerRegister) ServeHTTP(rw http.ResponseWriter, r *http.Request) execController.Patch() case http.MethodOptions: execController.Options() + case http.MethodTrace: + execController.Trace() default: if !execController.HandlerFunc(runMethod) { + vc := reflect.ValueOf(execController) method := vc.MethodByName(runMethod) in := param.ConvertParams(methodParams, method.Type(), context) out := method.Call(in) @@ -846,59 +884,46 @@ func (p *ControllerRegister) ServeHTTP(rw http.ResponseWriter, r *http.Request) Admin: //admin module record QPS + + statusCode := context.ResponseWriter.Status + if statusCode == 0 { + statusCode = 200 + } + + LogAccess(context, &startTime, statusCode) + + timeDur := time.Since(startTime) + context.ResponseWriter.Elapsed = timeDur if BConfig.Listen.EnableAdmin { - timeDur := time.Since(startTime) pattern := "" if routerInfo != nil { pattern = routerInfo.pattern } - statusCode := context.ResponseWriter.Status - if statusCode == 0 { - statusCode = 200 - } + if FilterMonitorFunc(r.Method, r.URL.Path, timeDur, pattern, statusCode) { + routerName := "" if runRouter != nil { - go toolbox.StatisticsMap.AddStatistics(r.Method, r.URL.Path, runRouter.Name(), timeDur) - } else { - go toolbox.StatisticsMap.AddStatistics(r.Method, r.URL.Path, "", timeDur) + routerName = runRouter.Name() } + go toolbox.StatisticsMap.AddStatistics(r.Method, r.URL.Path, routerName, timeDur) } } - if BConfig.RunMode == DEV || BConfig.Log.AccessLogs { - timeDur := time.Since(startTime) - var devInfo string - - statusCode := context.ResponseWriter.Status - if statusCode == 0 { - statusCode = 200 + if BConfig.RunMode == DEV && !BConfig.Log.AccessLogs { + match := map[bool]string{true: "match", false: "nomatch"} + devInfo := fmt.Sprintf("|%15s|%s %3d %s|%13s|%8s|%s %-7s %s %-3s", + context.Input.IP(), + logs.ColorByStatus(statusCode), statusCode, logs.ResetColor(), + timeDur.String(), + match[findRouter], + logs.ColorByMethod(r.Method), r.Method, logs.ResetColor(), + r.URL.Path) + if routerInfo != nil { + devInfo += fmt.Sprintf(" r:%s", routerInfo.pattern) } - iswin := (runtime.GOOS == "windows") - statusColor := logs.ColorByStatus(iswin, statusCode) - methodColor := logs.ColorByMethod(iswin, r.Method) - resetColor := logs.ColorByMethod(iswin, "") - - if findRouter { - if routerInfo != nil { - devInfo = fmt.Sprintf("|%15s|%s %3d %s|%13s|%8s|%s %-7s %s %-3s r:%s", context.Input.IP(), statusColor, statusCode, - resetColor, timeDur.String(), "match", methodColor, r.Method, resetColor, r.URL.Path, - routerInfo.pattern) - } else { - devInfo = fmt.Sprintf("|%15s|%s %3d %s|%13s|%8s|%s %-7s %s %-3s", context.Input.IP(), statusColor, statusCode, resetColor, - timeDur.String(), "match", methodColor, r.Method, resetColor, r.URL.Path) - } - } else { - devInfo = fmt.Sprintf("|%15s|%s %3d %s|%13s|%8s|%s %-7s %s %-3s", context.Input.IP(), statusColor, statusCode, resetColor, - timeDur.String(), "nomatch", methodColor, r.Method, resetColor, r.URL.Path) - } - if iswin { - logs.W32Debug(devInfo) - } else { - logs.Debug(devInfo) - } + logs.Debug(devInfo) } - // Call WriteHeader if status code has been set changed if context.Output.Status != 0 { context.ResponseWriter.WriteHeader(context.Output.Status) @@ -914,7 +939,7 @@ func (p *ControllerRegister) handleParamResponse(context *beecontext.Context, ex context.RenderMethodResult(resultValue) } } - if !context.ResponseWriter.Started && context.Output.Status == 0 { + if !context.ResponseWriter.Started && len(results) > 0 && context.Output.Status == 0 { context.Output.SetStatus(200) } } @@ -945,3 +970,39 @@ func toURL(params map[string]string) string { } return strings.TrimRight(u, "&") } + +// LogAccess logging info HTTP Access +func LogAccess(ctx *beecontext.Context, startTime *time.Time, statusCode int) { + //Skip logging if AccessLogs config is false + if !BConfig.Log.AccessLogs { + return + } + //Skip logging static requests unless EnableStaticLogs config is true + if !BConfig.Log.EnableStaticLogs && DefaultAccessLogFilter.Filter(ctx) { + return + } + var ( + requestTime time.Time + elapsedTime time.Duration + r = ctx.Request + ) + if startTime != nil { + requestTime = *startTime + elapsedTime = time.Since(*startTime) + } + record := &logs.AccessLogRecord{ + RemoteAddr: ctx.Input.IP(), + RequestTime: requestTime, + RequestMethod: r.Method, + Request: fmt.Sprintf("%s %s %s", r.Method, r.RequestURI, r.Proto), + ServerProtocol: r.Proto, + Host: r.Host, + Status: statusCode, + ElapsedTime: elapsedTime, + HTTPReferrer: r.Header.Get("Referer"), + HTTPUserAgent: r.Header.Get("User-Agent"), + RemoteUser: r.Header.Get("Remote-User"), + BodyBytesSent: 0, //@todo this one is missing! + } + logs.AccessLog(record, BConfig.Log.AccessLogsFormat) +} diff --git a/src/vendor/github.com/astaxie/beego/session/redis/sess_redis.go b/src/vendor/github.com/astaxie/beego/session/redis/sess_redis.go index d0424515d..5c382d61e 100644 --- a/src/vendor/github.com/astaxie/beego/session/redis/sess_redis.go +++ b/src/vendor/github.com/astaxie/beego/session/redis/sess_redis.go @@ -14,9 +14,9 @@ // Package redis for session provider // -// depend on github.com/garyburd/redigo/redis +// depend on github.com/gomodule/redigo/redis // -// go install github.com/garyburd/redigo/redis +// go install github.com/gomodule/redigo/redis // // Usage: // import( @@ -24,10 +24,10 @@ // "github.com/astaxie/beego/session" // ) // -// func init() { -// globalSessions, _ = session.NewManager("redis", ``{"cookieName":"gosessionid","gclifetime":3600,"ProviderConfig":"127.0.0.1:7070"}``) -// go globalSessions.GC() -// } +// func init() { +// globalSessions, _ = session.NewManager("redis", ``{"cookieName":"gosessionid","gclifetime":3600,"ProviderConfig":"127.0.0.1:7070"}``) +// go globalSessions.GC() +// } // // more docs: http://beego.me/docs/module/session.md package redis @@ -37,10 +37,11 @@ import ( "strconv" "strings" "sync" + "time" "github.com/astaxie/beego/session" - "github.com/garyburd/redigo/redis" + "github.com/gomodule/redigo/redis" ) var redispder = &Provider{} @@ -118,8 +119,8 @@ type Provider struct { } // SessionInit init redis session -// savepath like redis server addr,pool size,password,dbnum -// e.g. 127.0.0.1:6379,100,astaxie,0 +// savepath like redis server addr,pool size,password,dbnum,IdleTimeout second +// e.g. 127.0.0.1:6379,100,astaxie,0,30 func (rp *Provider) SessionInit(maxlifetime int64, savePath string) error { rp.maxlifetime = maxlifetime configs := strings.Split(savePath, ",") @@ -149,24 +150,39 @@ func (rp *Provider) SessionInit(maxlifetime int64, savePath string) error { } else { rp.dbNum = 0 } - rp.poollist = redis.NewPool(func() (redis.Conn, error) { - c, err := redis.Dial("tcp", rp.savePath) - if err != nil { - return nil, err + var idleTimeout time.Duration = 0 + if len(configs) > 4 { + timeout, err := strconv.Atoi(configs[4]) + if err == nil && timeout > 0 { + idleTimeout = time.Duration(timeout) * time.Second } - if rp.password != "" { - if _, err = c.Do("AUTH", rp.password); err != nil { - c.Close() + } + rp.poollist = &redis.Pool{ + Dial: func() (redis.Conn, error) { + c, err := redis.Dial("tcp", rp.savePath) + if err != nil { return nil, err } - } - _, err = c.Do("SELECT", rp.dbNum) - if err != nil { - c.Close() - return nil, err - } - return c, err - }, rp.poolsize) + if rp.password != "" { + if _, err = c.Do("AUTH", rp.password); err != nil { + c.Close() + return nil, err + } + } + // some redis proxy such as twemproxy is not support select command + if rp.dbNum > 0 { + _, err = c.Do("SELECT", rp.dbNum) + if err != nil { + c.Close() + return nil, err + } + } + return c, err + }, + MaxIdle: rp.poolsize, + } + + rp.poollist.IdleTimeout = idleTimeout return rp.poollist.Get().Err() } diff --git a/src/vendor/github.com/astaxie/beego/session/sess_file.go b/src/vendor/github.com/astaxie/beego/session/sess_file.go index 3ca93d555..db1435223 100644 --- a/src/vendor/github.com/astaxie/beego/session/sess_file.go +++ b/src/vendor/github.com/astaxie/beego/session/sess_file.go @@ -19,8 +19,10 @@ import ( "io/ioutil" "net/http" "os" + "errors" "path" "path/filepath" + "strings" "sync" "time" ) @@ -78,6 +80,8 @@ func (fs *FileSessionStore) SessionID() string { // SessionRelease Write file session to local file with Gob string func (fs *FileSessionStore) SessionRelease(w http.ResponseWriter) { + filepder.lock.Lock() + defer filepder.lock.Unlock() b, err := EncodeGob(fs.values) if err != nil { SLogger.Println(err) @@ -125,6 +129,12 @@ func (fp *FileProvider) SessionInit(maxlifetime int64, savePath string) error { // if file is not exist, create it. // the file path is generated from sid string. func (fp *FileProvider) SessionRead(sid string) (Store, error) { + if strings.ContainsAny(sid, "./") { + return nil, nil + } + if len(sid) < 2 { + return nil, errors.New("length of the sid is less than 2") + } filepder.lock.Lock() defer filepder.lock.Unlock() @@ -164,7 +174,7 @@ func (fp *FileProvider) SessionRead(sid string) (Store, error) { } // SessionExist Check file session exist. -// it checkes the file named from sid exist or not. +// it checks the file named from sid exist or not. func (fp *FileProvider) SessionExist(sid string) bool { filepder.lock.Lock() defer filepder.lock.Unlock() diff --git a/src/vendor/github.com/astaxie/beego/session/sess_utils.go b/src/vendor/github.com/astaxie/beego/session/sess_utils.go index d7db5ba8d..2e3376c71 100644 --- a/src/vendor/github.com/astaxie/beego/session/sess_utils.go +++ b/src/vendor/github.com/astaxie/beego/session/sess_utils.go @@ -149,7 +149,7 @@ func decodeCookie(block cipher.Block, hashKey, name, value string, gcmaxlifetime // 2. Verify MAC. Value is "date|value|mac". parts := bytes.SplitN(b, []byte("|"), 3) if len(parts) != 3 { - return nil, errors.New("Decode: invalid value %v") + return nil, errors.New("Decode: invalid value format") } b = append([]byte(name+"|"), b[:len(b)-len(parts[2])]...) diff --git a/src/vendor/github.com/astaxie/beego/session/session.go b/src/vendor/github.com/astaxie/beego/session/session.go index cf647521a..46a9f1f0d 100644 --- a/src/vendor/github.com/astaxie/beego/session/session.go +++ b/src/vendor/github.com/astaxie/beego/session/session.go @@ -81,6 +81,15 @@ func Register(name string, provide Provider) { provides[name] = provide } +//GetProvider +func GetProvider(name string) (Provider, error) { + provider, ok := provides[name] + if !ok { + return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", name) + } + return provider, nil +} + // ManagerConfig define the session config type ManagerConfig struct { CookieName string `json:"cookieName"` @@ -96,6 +105,7 @@ type ManagerConfig struct { EnableSidInHTTPHeader bool `json:"EnableSidInHTTPHeader"` SessionNameInHTTPHeader string `json:"SessionNameInHTTPHeader"` EnableSidInURLQuery bool `json:"EnableSidInURLQuery"` + SessionIDPrefix string `json:"sessionIDPrefix"` } // Manager contains Provider and its configuration. @@ -153,6 +163,11 @@ func NewManager(provideName string, cf *ManagerConfig) (*Manager, error) { }, nil } +// GetProvider return current manager's provider +func (manager *Manager) GetProvider() Provider { + return manager.provider +} + // getSid retrieves session identifier from HTTP Request. // First try to retrieve id by reading from cookie, session cookie name is configurable, // if not exist, then retrieve id from querying parameters. @@ -331,7 +346,7 @@ func (manager *Manager) sessionID() (string, error) { if n != len(b) || err != nil { return "", fmt.Errorf("Could not successfully read from the system CSPRNG") } - return hex.EncodeToString(b), nil + return manager.config.SessionIDPrefix + hex.EncodeToString(b), nil } // Set cookie with https. diff --git a/src/vendor/github.com/astaxie/beego/staticfile.go b/src/vendor/github.com/astaxie/beego/staticfile.go index bbb2a1fbf..68241a865 100644 --- a/src/vendor/github.com/astaxie/beego/staticfile.go +++ b/src/vendor/github.com/astaxie/beego/staticfile.go @@ -74,7 +74,7 @@ func serverStaticRouter(ctx *context.Context) { if enableCompress { acceptEncoding = context.ParseEncoding(ctx.Request) } - b, n, sch, err := openFile(filePath, fileInfo, acceptEncoding) + b, n, sch, reader, err := openFile(filePath, fileInfo, acceptEncoding) if err != nil { if BConfig.RunMode == DEV { logs.Warn("Can't compress the file:", filePath, err) @@ -89,47 +89,53 @@ func serverStaticRouter(ctx *context.Context) { ctx.Output.Header("Content-Length", strconv.FormatInt(sch.size, 10)) } - http.ServeContent(ctx.ResponseWriter, ctx.Request, filePath, sch.modTime, sch) + http.ServeContent(ctx.ResponseWriter, ctx.Request, filePath, sch.modTime, reader) } type serveContentHolder struct { - *bytes.Reader + data []byte modTime time.Time size int64 encoding string } +type serveContentReader struct { + *bytes.Reader +} + var ( staticFileMap = make(map[string]*serveContentHolder) mapLock sync.RWMutex ) -func openFile(filePath string, fi os.FileInfo, acceptEncoding string) (bool, string, *serveContentHolder, error) { +func openFile(filePath string, fi os.FileInfo, acceptEncoding string) (bool, string, *serveContentHolder, *serveContentReader, error) { mapKey := acceptEncoding + ":" + filePath mapLock.RLock() mapFile := staticFileMap[mapKey] mapLock.RUnlock() if isOk(mapFile, fi) { - return mapFile.encoding != "", mapFile.encoding, mapFile, nil + reader := &serveContentReader{Reader: bytes.NewReader(mapFile.data)} + return mapFile.encoding != "", mapFile.encoding, mapFile, reader, nil } mapLock.Lock() defer mapLock.Unlock() if mapFile = staticFileMap[mapKey]; !isOk(mapFile, fi) { file, err := os.Open(filePath) if err != nil { - return false, "", nil, err + return false, "", nil, nil, err } defer file.Close() var bufferWriter bytes.Buffer _, n, err := context.WriteFile(acceptEncoding, &bufferWriter, file) if err != nil { - return false, "", nil, err + return false, "", nil, nil, err } - mapFile = &serveContentHolder{Reader: bytes.NewReader(bufferWriter.Bytes()), modTime: fi.ModTime(), size: int64(bufferWriter.Len()), encoding: n} + mapFile = &serveContentHolder{data: bufferWriter.Bytes(), modTime: fi.ModTime(), size: int64(bufferWriter.Len()), encoding: n} staticFileMap[mapKey] = mapFile } - return mapFile.encoding != "", mapFile.encoding, mapFile, nil + reader := &serveContentReader{Reader: bytes.NewReader(mapFile.data)} + return mapFile.encoding != "", mapFile.encoding, mapFile, reader, nil } func isOk(s *serveContentHolder, fi os.FileInfo) bool { @@ -172,7 +178,7 @@ func searchFile(ctx *context.Context) (string, os.FileInfo, error) { if !strings.Contains(requestPath, prefix) { continue } - if len(requestPath) > len(prefix) && requestPath[len(prefix)] != '/' { + if prefix != "/" && len(requestPath) > len(prefix) && requestPath[len(prefix)] != '/' { continue } filePath := path.Join(staticDir, requestPath[len(prefix):]) diff --git a/src/vendor/github.com/astaxie/beego/template.go b/src/vendor/github.com/astaxie/beego/template.go index d4859cd70..59875be7b 100644 --- a/src/vendor/github.com/astaxie/beego/template.go +++ b/src/vendor/github.com/astaxie/beego/template.go @@ -20,6 +20,7 @@ import ( "html/template" "io" "io/ioutil" + "net/http" "os" "path/filepath" "regexp" @@ -37,9 +38,10 @@ var ( beeViewPathTemplates = make(map[string]map[string]*template.Template) templatesLock sync.RWMutex // beeTemplateExt stores the template extension which will build - beeTemplateExt = []string{"tpl", "html"} + beeTemplateExt = []string{"tpl", "html", "gohtml"} // beeTemplatePreprocessors stores associations of extension -> preprocessor handler beeTemplateEngines = map[string]templatePreProcessor{} + beeTemplateFS = defaultFSFunc ) // ExecuteTemplate applies the template with name to the specified data object, @@ -181,12 +183,17 @@ func lockViewPaths() { // BuildTemplate will build all template files in a directory. // it makes beego can render any template file in view directory. func BuildTemplate(dir string, files ...string) error { - if _, err := os.Stat(dir); err != nil { + var err error + fs := beeTemplateFS() + f, err := fs.Open(dir) + if err != nil { if os.IsNotExist(err) { return nil } return errors.New("dir open err") } + defer f.Close() + beeTemplates, ok := beeViewPathTemplates[dir] if !ok { panic("Unknown view path: " + dir) @@ -195,11 +202,11 @@ func BuildTemplate(dir string, files ...string) error { root: dir, files: make(map[string][]string), } - err := filepath.Walk(dir, func(path string, f os.FileInfo, err error) error { + err = Walk(fs, dir, func(path string, f os.FileInfo, err error) error { return self.visit(path, f, err) }) if err != nil { - fmt.Printf("filepath.Walk() returned %v\n", err) + fmt.Printf("Walk() returned %v\n", err) return err } buildAllFiles := len(files) == 0 @@ -210,17 +217,18 @@ func BuildTemplate(dir string, files ...string) error { ext := filepath.Ext(file) var t *template.Template if len(ext) == 0 { - t, err = getTemplate(self.root, file, v...) + t, err = getTemplate(self.root, fs, file, v...) } else if fn, ok := beeTemplateEngines[ext[1:]]; ok { t, err = fn(self.root, file, beegoTplFuncMap) } else { - t, err = getTemplate(self.root, file, v...) + t, err = getTemplate(self.root, fs, file, v...) } if err != nil { logs.Error("parse template err:", file, err) - } else { - beeTemplates[file] = t + templatesLock.Unlock() + return err } + beeTemplates[file] = t templatesLock.Unlock() } } @@ -228,20 +236,23 @@ func BuildTemplate(dir string, files ...string) error { return nil } -func getTplDeep(root, file, parent string, t *template.Template) (*template.Template, [][]string, error) { +func getTplDeep(root string, fs http.FileSystem, file string, parent string, t *template.Template) (*template.Template, [][]string, error) { var fileAbsPath string var rParent string - if filepath.HasPrefix(file, "../") { + var err error + if strings.HasPrefix(file, "../") { rParent = filepath.Join(filepath.Dir(parent), file) fileAbsPath = filepath.Join(root, filepath.Dir(parent), file) } else { rParent = file fileAbsPath = filepath.Join(root, file) } - if e := utils.FileExists(fileAbsPath); !e { + f, err := fs.Open(fileAbsPath) + if err != nil { panic("can't find template file:" + file) } - data, err := ioutil.ReadFile(fileAbsPath) + defer f.Close() + data, err := ioutil.ReadAll(f) if err != nil { return nil, [][]string{}, err } @@ -260,7 +271,7 @@ func getTplDeep(root, file, parent string, t *template.Template) (*template.Temp if !HasTemplateExt(m[1]) { continue } - _, _, err = getTplDeep(root, m[1], rParent, t) + _, _, err = getTplDeep(root, fs, m[1], rParent, t) if err != nil { return nil, [][]string{}, err } @@ -269,14 +280,14 @@ func getTplDeep(root, file, parent string, t *template.Template) (*template.Temp return t, allSub, nil } -func getTemplate(root, file string, others ...string) (t *template.Template, err error) { +func getTemplate(root string, fs http.FileSystem, file string, others ...string) (t *template.Template, err error) { t = template.New(file).Delims(BConfig.WebConfig.TemplateLeft, BConfig.WebConfig.TemplateRight).Funcs(beegoTplFuncMap) var subMods [][]string - t, subMods, err = getTplDeep(root, file, "", t) + t, subMods, err = getTplDeep(root, fs, file, "", t) if err != nil { return nil, err } - t, err = _getTemplate(t, root, subMods, others...) + t, err = _getTemplate(t, root, fs, subMods, others...) if err != nil { return nil, err @@ -284,7 +295,7 @@ func getTemplate(root, file string, others ...string) (t *template.Template, err return } -func _getTemplate(t0 *template.Template, root string, subMods [][]string, others ...string) (t *template.Template, err error) { +func _getTemplate(t0 *template.Template, root string, fs http.FileSystem, subMods [][]string, others ...string) (t *template.Template, err error) { t = t0 for _, m := range subMods { if len(m) == 2 { @@ -296,11 +307,11 @@ func _getTemplate(t0 *template.Template, root string, subMods [][]string, others for _, otherFile := range others { if otherFile == m[1] { var subMods1 [][]string - t, subMods1, err = getTplDeep(root, otherFile, "", t) + t, subMods1, err = getTplDeep(root, fs, otherFile, "", t) if err != nil { logs.Trace("template parse file err:", err) } else if len(subMods1) > 0 { - t, err = _getTemplate(t, root, subMods1, others...) + t, err = _getTemplate(t, root, fs, subMods1, others...) } break } @@ -309,8 +320,16 @@ func _getTemplate(t0 *template.Template, root string, subMods [][]string, others for _, otherFile := range others { var data []byte fileAbsPath := filepath.Join(root, otherFile) - data, err = ioutil.ReadFile(fileAbsPath) + f, err := fs.Open(fileAbsPath) if err != nil { + f.Close() + logs.Trace("template file parse error, not success open file:", err) + continue + } + data, err = ioutil.ReadAll(f) + f.Close() + if err != nil { + logs.Trace("template file parse error, not success read file:", err) continue } reg := regexp.MustCompile(BConfig.WebConfig.TemplateLeft + "[ ]*define[ ]+\"([^\"]+)\"") @@ -318,11 +337,14 @@ func _getTemplate(t0 *template.Template, root string, subMods [][]string, others for _, sub := range allSub { if len(sub) == 2 && sub[1] == m[1] { var subMods1 [][]string - t, subMods1, err = getTplDeep(root, otherFile, "", t) + t, subMods1, err = getTplDeep(root, fs, otherFile, "", t) if err != nil { logs.Trace("template parse file err:", err) } else if len(subMods1) > 0 { - t, err = _getTemplate(t, root, subMods1, others...) + t, err = _getTemplate(t, root, fs, subMods1, others...) + if err != nil { + logs.Trace("template parse file err:", err) + } } break } @@ -334,6 +356,17 @@ func _getTemplate(t0 *template.Template, root string, subMods [][]string, others return } +type templateFSFunc func() http.FileSystem + +func defaultFSFunc() http.FileSystem { + return FileSystem{} +} + +// SetTemplateFSFunc set default filesystem function +func SetTemplateFSFunc(fnt templateFSFunc) { + beeTemplateFS = fnt +} + // SetViewsPath sets view directory path in beego application. func SetViewsPath(path string) *App { BConfig.WebConfig.ViewsPath = path diff --git a/src/vendor/github.com/astaxie/beego/templatefunc.go b/src/vendor/github.com/astaxie/beego/templatefunc.go index a104fd241..ba1ec5ebc 100644 --- a/src/vendor/github.com/astaxie/beego/templatefunc.go +++ b/src/vendor/github.com/astaxie/beego/templatefunc.go @@ -17,6 +17,7 @@ package beego import ( "errors" "fmt" + "html" "html/template" "net/url" "reflect" @@ -54,21 +55,21 @@ func Substr(s string, start, length int) string { // HTML2str returns escaping text convert from html. func HTML2str(html string) string { - re, _ := regexp.Compile(`\<[\S\s]+?\>`) + re := regexp.MustCompile(`\<[\S\s]+?\>`) html = re.ReplaceAllStringFunc(html, strings.ToLower) //remove STYLE - re, _ = regexp.Compile(`\`) + re = regexp.MustCompile(`\`) html = re.ReplaceAllString(html, "") //remove SCRIPT - re, _ = regexp.Compile(`\`) + re = regexp.MustCompile(`\`) html = re.ReplaceAllString(html, "") - re, _ = regexp.Compile(`\<[\S\s]+?\>`) + re = regexp.MustCompile(`\<[\S\s]+?\>`) html = re.ReplaceAllString(html, "\n") - re, _ = regexp.Compile(`\s{2,}`) + re = regexp.MustCompile(`\s{2,}`) html = re.ReplaceAllString(html, "\n") return strings.TrimSpace(html) @@ -171,7 +172,7 @@ func GetConfig(returnType, key string, defaultVal interface{}) (value interface{ case "DIY": value, err = AppConfig.DIY(key) default: - err = errors.New("Config keys must be of type String, Bool, Int, Int64, Float, or DIY") + err = errors.New("config keys must be of type String, Bool, Int, Int64, Float, or DIY") } if err != nil { @@ -207,14 +208,12 @@ func Htmlquote(text string) string { '<'&">' */ - text = strings.Replace(text, "&", "&", -1) // Must be done first! - text = strings.Replace(text, "<", "<", -1) - text = strings.Replace(text, ">", ">", -1) - text = strings.Replace(text, "'", "'", -1) - text = strings.Replace(text, "\"", """, -1) - text = strings.Replace(text, "“", "“", -1) - text = strings.Replace(text, "”", "”", -1) - text = strings.Replace(text, " ", " ", -1) + text = html.EscapeString(text) + text = strings.NewReplacer( + `“`, "“", + `”`, "”", + ` `, " ", + ).Replace(text) return strings.TrimSpace(text) } @@ -228,17 +227,7 @@ func Htmlunquote(text string) string { '<\\'&">' */ - // strings.Replace(s, old, new, n) - // 在s字符串中,把old字符串替换为new字符串,n表示替换的次数,小于0表示全部替换 - - text = strings.Replace(text, " ", " ", -1) - text = strings.Replace(text, "”", "”", -1) - text = strings.Replace(text, "“", "“", -1) - text = strings.Replace(text, """, "\"", -1) - text = strings.Replace(text, "'", "'", -1) - text = strings.Replace(text, ">", ">", -1) - text = strings.Replace(text, "<", "<", -1) - text = strings.Replace(text, "&", "&", -1) // Must be done last! + text = html.UnescapeString(text) return strings.TrimSpace(text) } @@ -308,9 +297,21 @@ func parseFormToStruct(form url.Values, objT reflect.Type, objV reflect.Value) e tag = tags[0] } - value := form.Get(tag) - if len(value) == 0 { - continue + formValues := form[tag] + var value string + if len(formValues) == 0 { + defaultValue := fieldT.Tag.Get("default") + if defaultValue != "" { + value = defaultValue + } else { + continue + } + } + if len(formValues) == 1 { + value = formValues[0] + if value == "" { + continue + } } switch fieldT.Type.Kind() { @@ -360,6 +361,8 @@ func parseFormToStruct(form url.Values, objT reflect.Type, objV reflect.Value) e if len(value) >= 25 { value = value[:25] t, err = time.ParseInLocation(time.RFC3339, value, time.Local) + } else if strings.HasSuffix(strings.ToUpper(value), "Z") { + t, err = time.ParseInLocation(time.RFC3339, value, time.Local) } else if len(value) >= 19 { if strings.Contains(value, "T") { value = value[:19] @@ -703,7 +706,7 @@ func ge(arg1, arg2 interface{}) (bool, error) { // MapGet getting value from map by keys // usage: -// Data["m"] = map[string]interface{} { +// Data["m"] = M{ // "a": 1, // "1": map[string]float64{ // "c": 4, diff --git a/src/vendor/github.com/astaxie/beego/toolbox/task.go b/src/vendor/github.com/astaxie/beego/toolbox/task.go index 672717cd9..d13430238 100644 --- a/src/vendor/github.com/astaxie/beego/toolbox/task.go +++ b/src/vendor/github.com/astaxie/beego/toolbox/task.go @@ -20,6 +20,7 @@ import ( "sort" "strconv" "strings" + "sync" "time" ) @@ -32,6 +33,7 @@ type bounds struct { // The bounds for each field. var ( AdminTaskList map[string]Tasker + taskLock sync.Mutex stop chan bool changed chan bool isstart bool @@ -389,6 +391,8 @@ func dayMatches(s *Schedule, t time.Time) bool { // StartTask start all tasks func StartTask() { + taskLock.Lock() + defer taskLock.Unlock() if isstart { //If already started, no need to start another goroutine. return @@ -428,6 +432,9 @@ func run() { continue case <-changed: now = time.Now().Local() + for _, t := range AdminTaskList { + t.SetNext(now) + } continue case <-stop: return @@ -437,6 +444,8 @@ func run() { // StopTask stop all tasks func StopTask() { + taskLock.Lock() + defer taskLock.Unlock() if isstart { isstart = false stop <- true @@ -446,6 +455,9 @@ func StopTask() { // AddTask add task with name func AddTask(taskname string, t Tasker) { + taskLock.Lock() + defer taskLock.Unlock() + t.SetNext(time.Now().Local()) AdminTaskList[taskname] = t if isstart { changed <- true @@ -454,6 +466,8 @@ func AddTask(taskname string, t Tasker) { // DeleteTask delete task with name func DeleteTask(taskname string) { + taskLock.Lock() + defer taskLock.Unlock() delete(AdminTaskList, taskname) if isstart { changed <- true diff --git a/src/vendor/github.com/astaxie/beego/tree.go b/src/vendor/github.com/astaxie/beego/tree.go index 2d6c3fc3e..9e53003bc 100644 --- a/src/vendor/github.com/astaxie/beego/tree.go +++ b/src/vendor/github.com/astaxie/beego/tree.go @@ -28,7 +28,7 @@ var ( ) // Tree has three elements: FixRouter/wildcard/leaves -// fixRouter sotres Fixed Router +// fixRouter stores Fixed Router // wildcard stores params // leaves store the endpoint information type Tree struct { diff --git a/src/vendor/github.com/astaxie/beego/utils/mail.go b/src/vendor/github.com/astaxie/beego/utils/mail.go index e3fa1c909..42b1e4d44 100644 --- a/src/vendor/github.com/astaxie/beego/utils/mail.go +++ b/src/vendor/github.com/astaxie/beego/utils/mail.go @@ -162,7 +162,7 @@ func (e *Email) Bytes() ([]byte, error) { // AttachFile Add attach file to the send mail func (e *Email) AttachFile(args ...string) (a *Attachment, err error) { - if len(args) < 1 && len(args) > 2 { + if len(args) < 1 || len(args) > 2 { // change && to || err = errors.New("Must specify a file name and number of parameters can not exceed at least two") return } @@ -183,7 +183,7 @@ func (e *Email) AttachFile(args ...string) (a *Attachment, err error) { // Attach is used to attach content from an io.Reader to the email. // Parameters include an io.Reader, the desired filename for the attachment, and the Content-Type. func (e *Email) Attach(r io.Reader, filename string, args ...string) (a *Attachment, err error) { - if len(args) < 1 && len(args) > 2 { + if len(args) < 1 || len(args) > 2 { // change && to || err = errors.New("Must specify the file type and number of parameters can not exceed at least two") return } diff --git a/src/vendor/github.com/astaxie/beego/utils/utils.go b/src/vendor/github.com/astaxie/beego/utils/utils.go index ed8857873..3874b803b 100644 --- a/src/vendor/github.com/astaxie/beego/utils/utils.go +++ b/src/vendor/github.com/astaxie/beego/utils/utils.go @@ -3,19 +3,78 @@ package utils import ( "os" "path/filepath" + "regexp" "runtime" + "strconv" "strings" ) // GetGOPATHs returns all paths in GOPATH variable. func GetGOPATHs() []string { gopath := os.Getenv("GOPATH") - if gopath == "" && strings.Compare(runtime.Version(), "go1.8") >= 0 { + if gopath == "" && compareGoVersion(runtime.Version(), "go1.8") >= 0 { gopath = defaultGOPATH() } return filepath.SplitList(gopath) } +func compareGoVersion(a, b string) int { + reg := regexp.MustCompile("^\\d*") + + a = strings.TrimPrefix(a, "go") + b = strings.TrimPrefix(b, "go") + + versionsA := strings.Split(a, ".") + versionsB := strings.Split(b, ".") + + for i := 0; i < len(versionsA) && i < len(versionsB); i++ { + versionA := versionsA[i] + versionB := versionsB[i] + + vA, err := strconv.Atoi(versionA) + if err != nil { + str := reg.FindString(versionA) + if str != "" { + vA, _ = strconv.Atoi(str) + } else { + vA = -1 + } + } + + vB, err := strconv.Atoi(versionB) + if err != nil { + str := reg.FindString(versionB) + if str != "" { + vB, _ = strconv.Atoi(str) + } else { + vB = -1 + } + } + + if vA > vB { + // vA = 12, vB = 8 + return 1 + } else if vA < vB { + // vA = 6, vB = 8 + return -1 + } else if vA == -1 { + // vA = rc1, vB = rc3 + return strings.Compare(versionA, versionB) + } + + // vA = vB = 8 + continue + } + + if len(versionsA) > len(versionsB) { + return 1 + } else if len(versionsA) == len(versionsB) { + return 0 + } + + return -1 +} + func defaultGOPATH() string { env := "HOME" if runtime.GOOS == "windows" { diff --git a/src/vendor/github.com/astaxie/beego/validation/validation.go b/src/vendor/github.com/astaxie/beego/validation/validation.go index ef484fb37..ca1e211fc 100644 --- a/src/vendor/github.com/astaxie/beego/validation/validation.go +++ b/src/vendor/github.com/astaxie/beego/validation/validation.go @@ -112,7 +112,7 @@ type Validation struct { RequiredFirst bool Errors []*Error - ErrorsMap map[string]*Error + ErrorsMap map[string][]*Error } // Clear Clean all ValidationError. @@ -129,7 +129,7 @@ func (v *Validation) HasErrors() bool { // ErrorMap Return the errors mapped by key. // If there are multiple validation errors associated with a single key, the // first one "wins". (Typically the first validation will be the more basic). -func (v *Validation) ErrorMap() map[string]*Error { +func (v *Validation) ErrorMap() map[string][]*Error { return v.ErrorsMap } @@ -245,7 +245,21 @@ func (v *Validation) ZipCode(obj interface{}, key string) *Result { } func (v *Validation) apply(chk Validator, obj interface{}) *Result { - if chk.IsSatisfied(obj) { + if nil == obj { + if chk.IsSatisfied(obj) { + return &Result{Ok: true} + } + } else if reflect.TypeOf(obj).Kind() == reflect.Ptr { + if reflect.ValueOf(obj).IsNil() { + if chk.IsSatisfied(nil) { + return &Result{Ok: true} + } + } else { + if chk.IsSatisfied(reflect.ValueOf(obj).Elem().Interface()) { + return &Result{Ok: true} + } + } + } else if chk.IsSatisfied(obj) { return &Result{Ok: true} } @@ -278,14 +292,35 @@ func (v *Validation) apply(chk Validator, obj interface{}) *Result { } } +// AddError adds independent error message for the provided key +func (v *Validation) AddError(key, message string) { + Name := key + Field := "" + + parts := strings.Split(key, ".") + if len(parts) == 2 { + Field = parts[0] + Name = parts[1] + } + + err := &Error{ + Message: message, + Key: key, + Name: Name, + Field: Field, + } + v.setError(err) +} + func (v *Validation) setError(err *Error) { v.Errors = append(v.Errors, err) if v.ErrorsMap == nil { - v.ErrorsMap = make(map[string]*Error) + v.ErrorsMap = make(map[string][]*Error) } if _, ok := v.ErrorsMap[err.Field]; !ok { - v.ErrorsMap[err.Field] = err + v.ErrorsMap[err.Field] = []*Error{} } + v.ErrorsMap[err.Field] = append(v.ErrorsMap[err.Field], err) } // SetError Set error message for one field in ValidationError @@ -330,13 +365,24 @@ func (v *Validation) Valid(obj interface{}) (b bool, err error) { return } - var hasReuired bool + var hasRequired bool for _, vf := range vfs { if vf.Name == "Required" { - hasReuired = true + hasRequired = true } - if !hasReuired && v.RequiredFirst && len(objV.Field(i).String()) == 0 { + currentField := objV.Field(i).Interface() + if objV.Field(i).Kind() == reflect.Ptr { + if objV.Field(i).IsNil() { + currentField = "" + } else { + currentField = objV.Field(i).Elem().Interface() + } + } + + + chk := Required{""}.IsSatisfied(currentField) + if !hasRequired && v.RequiredFirst && !chk { if _, ok := CanSkipFuncs[vf.Name]; ok { continue } @@ -393,3 +439,9 @@ func (v *Validation) RecursiveValid(objc interface{}) (bool, error) { } return pass, err } + +func (v *Validation) CanSkipAlso(skipFunc string) { + if _, ok := CanSkipFuncs[skipFunc]; !ok { + CanSkipFuncs[skipFunc] = struct{}{} + } +} diff --git a/src/vendor/github.com/astaxie/beego/validation/validators.go b/src/vendor/github.com/astaxie/beego/validation/validators.go index 4dff9c0b4..dc18b11eb 100644 --- a/src/vendor/github.com/astaxie/beego/validation/validators.go +++ b/src/vendor/github.com/astaxie/beego/validation/validators.go @@ -632,7 +632,7 @@ func (b Base64) GetLimitValue() interface{} { } // just for chinese mobile phone number -var mobilePattern = regexp.MustCompile(`^((\+86)|(86))?(1(([35][0-9])|[8][0-9]|[7][06789]|[4][579]))\d{8}$`) +var mobilePattern = regexp.MustCompile(`^((\+86)|(86))?(1(([35][0-9])|[8][0-9]|[7][01356789]|[4][579]))\d{8}$`) // Mobile check struct type Mobile struct { diff --git a/src/vendor/github.com/shiena/ansicolor/.gitignore b/src/vendor/github.com/shiena/ansicolor/.gitignore new file mode 100644 index 000000000..69cec52c4 --- /dev/null +++ b/src/vendor/github.com/shiena/ansicolor/.gitignore @@ -0,0 +1,27 @@ +# Created by http://www.gitignore.io + +### Go ### +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test + diff --git a/src/vendor/github.com/shiena/ansicolor/LICENSE b/src/vendor/github.com/shiena/ansicolor/LICENSE new file mode 100644 index 000000000..e58473ed9 --- /dev/null +++ b/src/vendor/github.com/shiena/ansicolor/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) [2014] [shiena] + +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. diff --git a/src/vendor/github.com/shiena/ansicolor/README.md b/src/vendor/github.com/shiena/ansicolor/README.md new file mode 100644 index 000000000..cf2ae4055 --- /dev/null +++ b/src/vendor/github.com/shiena/ansicolor/README.md @@ -0,0 +1,101 @@ +[![GoDoc](https://godoc.org/github.com/shiena/ansicolor?status.svg)](https://godoc.org/github.com/shiena/ansicolor) + +# ansicolor + +Ansicolor library provides color console in Windows as ANSICON for Golang. + +## Features + +|Escape sequence|Text attributes| +|---------------|----| +|\x1b[0m|All attributes off(color at startup)| +|\x1b[1m|Bold on(enable foreground intensity)| +|\x1b[4m|Underline on| +|\x1b[5m|Blink on(enable background intensity)| +|\x1b[21m|Bold off(disable foreground intensity)| +|\x1b[24m|Underline off| +|\x1b[25m|Blink off(disable background intensity)| + +|Escape sequence|Foreground colors| +|---------------|----| +|\x1b[30m|Black| +|\x1b[31m|Red| +|\x1b[32m|Green| +|\x1b[33m|Yellow| +|\x1b[34m|Blue| +|\x1b[35m|Magenta| +|\x1b[36m|Cyan| +|\x1b[37m|White| +|\x1b[39m|Default(foreground color at startup)| +|\x1b[90m|Light Gray| +|\x1b[91m|Light Red| +|\x1b[92m|Light Green| +|\x1b[93m|Light Yellow| +|\x1b[94m|Light Blue| +|\x1b[95m|Light Magenta| +|\x1b[96m|Light Cyan| +|\x1b[97m|Light White| + +|Escape sequence|Background colors| +|---------------|----| +|\x1b[40m|Black| +|\x1b[41m|Red| +|\x1b[42m|Green| +|\x1b[43m|Yellow| +|\x1b[44m|Blue| +|\x1b[45m|Magenta| +|\x1b[46m|Cyan| +|\x1b[47m|White| +|\x1b[49m|Default(background color at startup)| +|\x1b[100m|Light Gray| +|\x1b[101m|Light Red| +|\x1b[102m|Light Green| +|\x1b[103m|Light Yellow| +|\x1b[104m|Light Blue| +|\x1b[105m|Light Magenta| +|\x1b[106m|Light Cyan| +|\x1b[107m|Light White| + +## Example + +```go +package main + +import ( + "fmt" + "os" + + "github.com/shiena/ansicolor" +) + +func main() { + w := ansicolor.NewAnsiColorWriter(os.Stdout) + text := "%sforeground %sbold%s %sbackground%s\n" + fmt.Fprintf(w, text, "\x1b[31m", "\x1b[1m", "\x1b[21m", "\x1b[41;32m", "\x1b[0m") + fmt.Fprintf(w, text, "\x1b[32m", "\x1b[1m", "\x1b[21m", "\x1b[42;31m", "\x1b[0m") + fmt.Fprintf(w, text, "\x1b[33m", "\x1b[1m", "\x1b[21m", "\x1b[43;34m", "\x1b[0m") + fmt.Fprintf(w, text, "\x1b[34m", "\x1b[1m", "\x1b[21m", "\x1b[44;33m", "\x1b[0m") + fmt.Fprintf(w, text, "\x1b[35m", "\x1b[1m", "\x1b[21m", "\x1b[45;36m", "\x1b[0m") + fmt.Fprintf(w, text, "\x1b[36m", "\x1b[1m", "\x1b[21m", "\x1b[46;35m", "\x1b[0m") + fmt.Fprintf(w, text, "\x1b[37m", "\x1b[1m", "\x1b[21m", "\x1b[47;30m", "\x1b[0m") +} +``` + +![screenshot](https://gist.githubusercontent.com/shiena/a1bada24b525314a7d5e/raw/c763aa7cda6e4fefaccf831e2617adc40b6151c7/main.png) + +## See also: + +- https://github.com/daviddengcn/go-colortext +- https://github.com/adoxa/ansicon +- https://github.com/aslakhellesoy/wac +- https://github.com/wsxiaoys/terminal +- https://github.com/mattn/go-colorable + +## Contributing + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Add some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request + diff --git a/src/vendor/github.com/shiena/ansicolor/ansicolor.go b/src/vendor/github.com/shiena/ansicolor/ansicolor.go new file mode 100644 index 000000000..8551345b8 --- /dev/null +++ b/src/vendor/github.com/shiena/ansicolor/ansicolor.go @@ -0,0 +1,42 @@ +// Copyright 2014 shiena Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +// Package ansicolor provides color console in Windows as ANSICON. +package ansicolor + +import "io" + +type outputMode int + +// DiscardNonColorEscSeq supports the divided color escape sequence. +// But non-color escape sequence is not output. +// Please use the OutputNonColorEscSeq If you want to output a non-color +// escape sequences such as ncurses. However, it does not support the divided +// color escape sequence. +const ( + _ outputMode = iota + DiscardNonColorEscSeq + OutputNonColorEscSeq +) + +// NewAnsiColorWriter creates and initializes a new ansiColorWriter +// using io.Writer w as its initial contents. +// In the console of Windows, which change the foreground and background +// colors of the text by the escape sequence. +// In the console of other systems, which writes to w all text. +func NewAnsiColorWriter(w io.Writer) io.Writer { + return NewModeAnsiColorWriter(w, DiscardNonColorEscSeq) +} + +// NewModeAnsiColorWriter create and initializes a new ansiColorWriter +// by specifying the outputMode. +func NewModeAnsiColorWriter(w io.Writer, mode outputMode) io.Writer { + if _, ok := w.(*ansiColorWriter); !ok { + return &ansiColorWriter{ + w: w, + mode: mode, + } + } + return w +} diff --git a/src/vendor/github.com/shiena/ansicolor/ansicolor_ansi.go b/src/vendor/github.com/shiena/ansicolor/ansicolor_ansi.go new file mode 100644 index 000000000..f4803bbbb --- /dev/null +++ b/src/vendor/github.com/shiena/ansicolor/ansicolor_ansi.go @@ -0,0 +1,18 @@ +// Copyright 2014 shiena Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +// +build !windows + +package ansicolor + +import "io" + +type ansiColorWriter struct { + w io.Writer + mode outputMode +} + +func (cw *ansiColorWriter) Write(p []byte) (int, error) { + return cw.w.Write(p) +} diff --git a/src/vendor/github.com/astaxie/beego/logs/color_windows.go b/src/vendor/github.com/shiena/ansicolor/ansicolor_windows.go similarity index 93% rename from src/vendor/github.com/astaxie/beego/logs/color_windows.go rename to src/vendor/github.com/shiena/ansicolor/ansicolor_windows.go index 4e28f1888..ff2ae0ef3 100644 --- a/src/vendor/github.com/astaxie/beego/logs/color_windows.go +++ b/src/vendor/github.com/shiena/ansicolor/ansicolor_windows.go @@ -1,20 +1,10 @@ -// Copyright 2014 beego Author. All Rights Reserved. -// -// 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. +// Copyright 2014 shiena Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. // +build windows -package logs +package ansicolor import ( "bytes" @@ -24,10 +14,7 @@ import ( "unsafe" ) -type ( - csiState int - parseResult int -) +type csiState int const ( outsideCsiCode csiState = iota @@ -35,6 +22,8 @@ const ( secondCsiCode ) +type parseResult int + const ( noConsole parseResult = iota changedColor @@ -361,7 +350,7 @@ func isParameterChar(b byte) bool { } func (cw *ansiColorWriter) Write(p []byte) (int, error) { - var r, nw, first, last int + r, nw, first, last := 0, 0, 0, 0 if cw.mode != DiscardNonColorEscSeq { cw.state = outsideCsiCode cw.resetBuffer() @@ -420,7 +409,7 @@ func (cw *ansiColorWriter) Write(p []byte) (int, error) { } if cw.mode != DiscardNonColorEscSeq || cw.state == outsideCsiCode { - nw, err = cw.w.Write(p[first:]) + nw, err = cw.w.Write(p[first:len(p)]) r += nw } diff --git a/src/vendor/golang.org/x/crypto/acme/acme.go b/src/vendor/golang.org/x/crypto/acme/acme.go new file mode 100644 index 000000000..00ee95550 --- /dev/null +++ b/src/vendor/golang.org/x/crypto/acme/acme.go @@ -0,0 +1,949 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package acme provides an implementation of the +// Automatic Certificate Management Environment (ACME) spec. +// See https://tools.ietf.org/html/draft-ietf-acme-acme-02 for details. +// +// Most common scenarios will want to use autocert subdirectory instead, +// which provides automatic access to certificates from Let's Encrypt +// and any other ACME-based CA. +// +// This package is a work in progress and makes no API stability promises. +package acme + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/base64" + "encoding/hex" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "io" + "io/ioutil" + "math/big" + "net/http" + "strings" + "sync" + "time" +) + +const ( + // LetsEncryptURL is the Directory endpoint of Let's Encrypt CA. + LetsEncryptURL = "https://acme-v01.api.letsencrypt.org/directory" + + // ALPNProto is the ALPN protocol name used by a CA server when validating + // tls-alpn-01 challenges. + // + // Package users must ensure their servers can negotiate the ACME ALPN in + // order for tls-alpn-01 challenge verifications to succeed. + // See the crypto/tls package's Config.NextProtos field. + ALPNProto = "acme-tls/1" +) + +// idPeACMEIdentifierV1 is the OID for the ACME extension for the TLS-ALPN challenge. +var idPeACMEIdentifierV1 = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 30, 1} + +const ( + maxChainLen = 5 // max depth and breadth of a certificate chain + maxCertSize = 1 << 20 // max size of a certificate, in bytes + + // Max number of collected nonces kept in memory. + // Expect usual peak of 1 or 2. + maxNonces = 100 +) + +// Client is an ACME client. +// The only required field is Key. An example of creating a client with a new key +// is as follows: +// +// key, err := rsa.GenerateKey(rand.Reader, 2048) +// if err != nil { +// log.Fatal(err) +// } +// client := &Client{Key: key} +// +type Client struct { + // Key is the account key used to register with a CA and sign requests. + // Key.Public() must return a *rsa.PublicKey or *ecdsa.PublicKey. + // + // The following algorithms are supported: + // RS256, ES256, ES384 and ES512. + // See RFC7518 for more details about the algorithms. + Key crypto.Signer + + // HTTPClient optionally specifies an HTTP client to use + // instead of http.DefaultClient. + HTTPClient *http.Client + + // DirectoryURL points to the CA directory endpoint. + // If empty, LetsEncryptURL is used. + // Mutating this value after a successful call of Client's Discover method + // will have no effect. + DirectoryURL string + + // RetryBackoff computes the duration after which the nth retry of a failed request + // should occur. The value of n for the first call on failure is 1. + // The values of r and resp are the request and response of the last failed attempt. + // If the returned value is negative or zero, no more retries are done and an error + // is returned to the caller of the original method. + // + // Requests which result in a 4xx client error are not retried, + // except for 400 Bad Request due to "bad nonce" errors and 429 Too Many Requests. + // + // If RetryBackoff is nil, a truncated exponential backoff algorithm + // with the ceiling of 10 seconds is used, where each subsequent retry n + // is done after either ("Retry-After" + jitter) or (2^n seconds + jitter), + // preferring the former if "Retry-After" header is found in the resp. + // The jitter is a random value up to 1 second. + RetryBackoff func(n int, r *http.Request, resp *http.Response) time.Duration + + dirMu sync.Mutex // guards writes to dir + dir *Directory // cached result of Client's Discover method + + noncesMu sync.Mutex + nonces map[string]struct{} // nonces collected from previous responses +} + +// Discover performs ACME server discovery using c.DirectoryURL. +// +// It caches successful result. So, subsequent calls will not result in +// a network round-trip. This also means mutating c.DirectoryURL after successful call +// of this method will have no effect. +func (c *Client) Discover(ctx context.Context) (Directory, error) { + c.dirMu.Lock() + defer c.dirMu.Unlock() + if c.dir != nil { + return *c.dir, nil + } + + res, err := c.get(ctx, c.directoryURL(), wantStatus(http.StatusOK)) + if err != nil { + return Directory{}, err + } + defer res.Body.Close() + c.addNonce(res.Header) + + var v struct { + Reg string `json:"new-reg"` + Authz string `json:"new-authz"` + Cert string `json:"new-cert"` + Revoke string `json:"revoke-cert"` + Meta struct { + Terms string `json:"terms-of-service"` + Website string `json:"website"` + CAA []string `json:"caa-identities"` + } + } + if err := json.NewDecoder(res.Body).Decode(&v); err != nil { + return Directory{}, err + } + c.dir = &Directory{ + RegURL: v.Reg, + AuthzURL: v.Authz, + CertURL: v.Cert, + RevokeURL: v.Revoke, + Terms: v.Meta.Terms, + Website: v.Meta.Website, + CAA: v.Meta.CAA, + } + return *c.dir, nil +} + +func (c *Client) directoryURL() string { + if c.DirectoryURL != "" { + return c.DirectoryURL + } + return LetsEncryptURL +} + +// CreateCert requests a new certificate using the Certificate Signing Request csr encoded in DER format. +// The exp argument indicates the desired certificate validity duration. CA may issue a certificate +// with a different duration. +// If the bundle argument is true, the returned value will also contain the CA (issuer) certificate chain. +// +// In the case where CA server does not provide the issued certificate in the response, +// CreateCert will poll certURL using c.FetchCert, which will result in additional round-trips. +// In such a scenario, the caller can cancel the polling with ctx. +// +// CreateCert returns an error if the CA's response or chain was unreasonably large. +// Callers are encouraged to parse the returned value to ensure the certificate is valid and has the expected features. +func (c *Client) CreateCert(ctx context.Context, csr []byte, exp time.Duration, bundle bool) (der [][]byte, certURL string, err error) { + if _, err := c.Discover(ctx); err != nil { + return nil, "", err + } + + req := struct { + Resource string `json:"resource"` + CSR string `json:"csr"` + NotBefore string `json:"notBefore,omitempty"` + NotAfter string `json:"notAfter,omitempty"` + }{ + Resource: "new-cert", + CSR: base64.RawURLEncoding.EncodeToString(csr), + } + now := timeNow() + req.NotBefore = now.Format(time.RFC3339) + if exp > 0 { + req.NotAfter = now.Add(exp).Format(time.RFC3339) + } + + res, err := c.post(ctx, c.Key, c.dir.CertURL, req, wantStatus(http.StatusCreated)) + if err != nil { + return nil, "", err + } + defer res.Body.Close() + + curl := res.Header.Get("Location") // cert permanent URL + if res.ContentLength == 0 { + // no cert in the body; poll until we get it + cert, err := c.FetchCert(ctx, curl, bundle) + return cert, curl, err + } + // slurp issued cert and CA chain, if requested + cert, err := c.responseCert(ctx, res, bundle) + return cert, curl, err +} + +// FetchCert retrieves already issued certificate from the given url, in DER format. +// It retries the request until the certificate is successfully retrieved, +// context is cancelled by the caller or an error response is received. +// +// The returned value will also contain the CA (issuer) certificate if the bundle argument is true. +// +// FetchCert returns an error if the CA's response or chain was unreasonably large. +// Callers are encouraged to parse the returned value to ensure the certificate is valid +// and has expected features. +func (c *Client) FetchCert(ctx context.Context, url string, bundle bool) ([][]byte, error) { + res, err := c.get(ctx, url, wantStatus(http.StatusOK)) + if err != nil { + return nil, err + } + return c.responseCert(ctx, res, bundle) +} + +// RevokeCert revokes a previously issued certificate cert, provided in DER format. +// +// The key argument, used to sign the request, must be authorized +// to revoke the certificate. It's up to the CA to decide which keys are authorized. +// For instance, the key pair of the certificate may be authorized. +// If the key is nil, c.Key is used instead. +func (c *Client) RevokeCert(ctx context.Context, key crypto.Signer, cert []byte, reason CRLReasonCode) error { + if _, err := c.Discover(ctx); err != nil { + return err + } + + body := &struct { + Resource string `json:"resource"` + Cert string `json:"certificate"` + Reason int `json:"reason"` + }{ + Resource: "revoke-cert", + Cert: base64.RawURLEncoding.EncodeToString(cert), + Reason: int(reason), + } + if key == nil { + key = c.Key + } + res, err := c.post(ctx, key, c.dir.RevokeURL, body, wantStatus(http.StatusOK)) + if err != nil { + return err + } + defer res.Body.Close() + return nil +} + +// AcceptTOS always returns true to indicate the acceptance of a CA's Terms of Service +// during account registration. See Register method of Client for more details. +func AcceptTOS(tosURL string) bool { return true } + +// Register creates a new account registration by following the "new-reg" flow. +// It returns the registered account. The account is not modified. +// +// The registration may require the caller to agree to the CA's Terms of Service (TOS). +// If so, and the account has not indicated the acceptance of the terms (see Account for details), +// Register calls prompt with a TOS URL provided by the CA. Prompt should report +// whether the caller agrees to the terms. To always accept the terms, the caller can use AcceptTOS. +func (c *Client) Register(ctx context.Context, a *Account, prompt func(tosURL string) bool) (*Account, error) { + if _, err := c.Discover(ctx); err != nil { + return nil, err + } + + var err error + if a, err = c.doReg(ctx, c.dir.RegURL, "new-reg", a); err != nil { + return nil, err + } + var accept bool + if a.CurrentTerms != "" && a.CurrentTerms != a.AgreedTerms { + accept = prompt(a.CurrentTerms) + } + if accept { + a.AgreedTerms = a.CurrentTerms + a, err = c.UpdateReg(ctx, a) + } + return a, err +} + +// GetReg retrieves an existing registration. +// The url argument is an Account URI. +func (c *Client) GetReg(ctx context.Context, url string) (*Account, error) { + a, err := c.doReg(ctx, url, "reg", nil) + if err != nil { + return nil, err + } + a.URI = url + return a, nil +} + +// UpdateReg updates an existing registration. +// It returns an updated account copy. The provided account is not modified. +func (c *Client) UpdateReg(ctx context.Context, a *Account) (*Account, error) { + uri := a.URI + a, err := c.doReg(ctx, uri, "reg", a) + if err != nil { + return nil, err + } + a.URI = uri + return a, nil +} + +// Authorize performs the initial step in an authorization flow. +// The caller will then need to choose from and perform a set of returned +// challenges using c.Accept in order to successfully complete authorization. +// +// If an authorization has been previously granted, the CA may return +// a valid authorization (Authorization.Status is StatusValid). If so, the caller +// need not fulfill any challenge and can proceed to requesting a certificate. +func (c *Client) Authorize(ctx context.Context, domain string) (*Authorization, error) { + return c.authorize(ctx, "dns", domain) +} + +// AuthorizeIP is the same as Authorize but requests IP address authorization. +// Clients which successfully obtain such authorization may request to issue +// a certificate for IP addresses. +// +// See the ACME spec extension for more details about IP address identifiers: +// https://tools.ietf.org/html/draft-ietf-acme-ip. +func (c *Client) AuthorizeIP(ctx context.Context, ipaddr string) (*Authorization, error) { + return c.authorize(ctx, "ip", ipaddr) +} + +func (c *Client) authorize(ctx context.Context, typ, val string) (*Authorization, error) { + if _, err := c.Discover(ctx); err != nil { + return nil, err + } + + type authzID struct { + Type string `json:"type"` + Value string `json:"value"` + } + req := struct { + Resource string `json:"resource"` + Identifier authzID `json:"identifier"` + }{ + Resource: "new-authz", + Identifier: authzID{Type: typ, Value: val}, + } + res, err := c.post(ctx, c.Key, c.dir.AuthzURL, req, wantStatus(http.StatusCreated)) + if err != nil { + return nil, err + } + defer res.Body.Close() + + var v wireAuthz + if err := json.NewDecoder(res.Body).Decode(&v); err != nil { + return nil, fmt.Errorf("acme: invalid response: %v", err) + } + if v.Status != StatusPending && v.Status != StatusValid { + return nil, fmt.Errorf("acme: unexpected status: %s", v.Status) + } + return v.authorization(res.Header.Get("Location")), nil +} + +// GetAuthorization retrieves an authorization identified by the given URL. +// +// If a caller needs to poll an authorization until its status is final, +// see the WaitAuthorization method. +func (c *Client) GetAuthorization(ctx context.Context, url string) (*Authorization, error) { + res, err := c.get(ctx, url, wantStatus(http.StatusOK, http.StatusAccepted)) + if err != nil { + return nil, err + } + defer res.Body.Close() + var v wireAuthz + if err := json.NewDecoder(res.Body).Decode(&v); err != nil { + return nil, fmt.Errorf("acme: invalid response: %v", err) + } + return v.authorization(url), nil +} + +// RevokeAuthorization relinquishes an existing authorization identified +// by the given URL. +// The url argument is an Authorization.URI value. +// +// If successful, the caller will be required to obtain a new authorization +// using the Authorize method before being able to request a new certificate +// for the domain associated with the authorization. +// +// It does not revoke existing certificates. +func (c *Client) RevokeAuthorization(ctx context.Context, url string) error { + req := struct { + Resource string `json:"resource"` + Status string `json:"status"` + Delete bool `json:"delete"` + }{ + Resource: "authz", + Status: "deactivated", + Delete: true, + } + res, err := c.post(ctx, c.Key, url, req, wantStatus(http.StatusOK)) + if err != nil { + return err + } + defer res.Body.Close() + return nil +} + +// WaitAuthorization polls an authorization at the given URL +// until it is in one of the final states, StatusValid or StatusInvalid, +// the ACME CA responded with a 4xx error code, or the context is done. +// +// It returns a non-nil Authorization only if its Status is StatusValid. +// In all other cases WaitAuthorization returns an error. +// If the Status is StatusInvalid, the returned error is of type *AuthorizationError. +func (c *Client) WaitAuthorization(ctx context.Context, url string) (*Authorization, error) { + for { + res, err := c.get(ctx, url, wantStatus(http.StatusOK, http.StatusAccepted)) + if err != nil { + return nil, err + } + + var raw wireAuthz + err = json.NewDecoder(res.Body).Decode(&raw) + res.Body.Close() + switch { + case err != nil: + // Skip and retry. + case raw.Status == StatusValid: + return raw.authorization(url), nil + case raw.Status == StatusInvalid: + return nil, raw.error(url) + } + + // Exponential backoff is implemented in c.get above. + // This is just to prevent continuously hitting the CA + // while waiting for a final authorization status. + d := retryAfter(res.Header.Get("Retry-After")) + if d == 0 { + // Given that the fastest challenges TLS-SNI and HTTP-01 + // require a CA to make at least 1 network round trip + // and most likely persist a challenge state, + // this default delay seems reasonable. + d = time.Second + } + t := time.NewTimer(d) + select { + case <-ctx.Done(): + t.Stop() + return nil, ctx.Err() + case <-t.C: + // Retry. + } + } +} + +// GetChallenge retrieves the current status of an challenge. +// +// A client typically polls a challenge status using this method. +func (c *Client) GetChallenge(ctx context.Context, url string) (*Challenge, error) { + res, err := c.get(ctx, url, wantStatus(http.StatusOK, http.StatusAccepted)) + if err != nil { + return nil, err + } + defer res.Body.Close() + v := wireChallenge{URI: url} + if err := json.NewDecoder(res.Body).Decode(&v); err != nil { + return nil, fmt.Errorf("acme: invalid response: %v", err) + } + return v.challenge(), nil +} + +// Accept informs the server that the client accepts one of its challenges +// previously obtained with c.Authorize. +// +// The server will then perform the validation asynchronously. +func (c *Client) Accept(ctx context.Context, chal *Challenge) (*Challenge, error) { + auth, err := keyAuth(c.Key.Public(), chal.Token) + if err != nil { + return nil, err + } + + req := struct { + Resource string `json:"resource"` + Type string `json:"type"` + Auth string `json:"keyAuthorization"` + }{ + Resource: "challenge", + Type: chal.Type, + Auth: auth, + } + res, err := c.post(ctx, c.Key, chal.URI, req, wantStatus( + http.StatusOK, // according to the spec + http.StatusAccepted, // Let's Encrypt: see https://goo.gl/WsJ7VT (acme-divergences.md) + )) + if err != nil { + return nil, err + } + defer res.Body.Close() + + var v wireChallenge + if err := json.NewDecoder(res.Body).Decode(&v); err != nil { + return nil, fmt.Errorf("acme: invalid response: %v", err) + } + return v.challenge(), nil +} + +// DNS01ChallengeRecord returns a DNS record value for a dns-01 challenge response. +// A TXT record containing the returned value must be provisioned under +// "_acme-challenge" name of the domain being validated. +// +// The token argument is a Challenge.Token value. +func (c *Client) DNS01ChallengeRecord(token string) (string, error) { + ka, err := keyAuth(c.Key.Public(), token) + if err != nil { + return "", err + } + b := sha256.Sum256([]byte(ka)) + return base64.RawURLEncoding.EncodeToString(b[:]), nil +} + +// HTTP01ChallengeResponse returns the response for an http-01 challenge. +// Servers should respond with the value to HTTP requests at the URL path +// provided by HTTP01ChallengePath to validate the challenge and prove control +// over a domain name. +// +// The token argument is a Challenge.Token value. +func (c *Client) HTTP01ChallengeResponse(token string) (string, error) { + return keyAuth(c.Key.Public(), token) +} + +// HTTP01ChallengePath returns the URL path at which the response for an http-01 challenge +// should be provided by the servers. +// The response value can be obtained with HTTP01ChallengeResponse. +// +// The token argument is a Challenge.Token value. +func (c *Client) HTTP01ChallengePath(token string) string { + return "/.well-known/acme-challenge/" + token +} + +// TLSSNI01ChallengeCert creates a certificate for TLS-SNI-01 challenge response. +// Servers can present the certificate to validate the challenge and prove control +// over a domain name. +// +// The implementation is incomplete in that the returned value is a single certificate, +// computed only for Z0 of the key authorization. ACME CAs are expected to update +// their implementations to use the newer version, TLS-SNI-02. +// For more details on TLS-SNI-01 see https://tools.ietf.org/html/draft-ietf-acme-acme-01#section-7.3. +// +// The token argument is a Challenge.Token value. +// If a WithKey option is provided, its private part signs the returned cert, +// and the public part is used to specify the signee. +// If no WithKey option is provided, a new ECDSA key is generated using P-256 curve. +// +// The returned certificate is valid for the next 24 hours and must be presented only when +// the server name of the TLS ClientHello matches exactly the returned name value. +func (c *Client) TLSSNI01ChallengeCert(token string, opt ...CertOption) (cert tls.Certificate, name string, err error) { + ka, err := keyAuth(c.Key.Public(), token) + if err != nil { + return tls.Certificate{}, "", err + } + b := sha256.Sum256([]byte(ka)) + h := hex.EncodeToString(b[:]) + name = fmt.Sprintf("%s.%s.acme.invalid", h[:32], h[32:]) + cert, err = tlsChallengeCert([]string{name}, opt) + if err != nil { + return tls.Certificate{}, "", err + } + return cert, name, nil +} + +// TLSSNI02ChallengeCert creates a certificate for TLS-SNI-02 challenge response. +// Servers can present the certificate to validate the challenge and prove control +// over a domain name. For more details on TLS-SNI-02 see +// https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-7.3. +// +// The token argument is a Challenge.Token value. +// If a WithKey option is provided, its private part signs the returned cert, +// and the public part is used to specify the signee. +// If no WithKey option is provided, a new ECDSA key is generated using P-256 curve. +// +// The returned certificate is valid for the next 24 hours and must be presented only when +// the server name in the TLS ClientHello matches exactly the returned name value. +func (c *Client) TLSSNI02ChallengeCert(token string, opt ...CertOption) (cert tls.Certificate, name string, err error) { + b := sha256.Sum256([]byte(token)) + h := hex.EncodeToString(b[:]) + sanA := fmt.Sprintf("%s.%s.token.acme.invalid", h[:32], h[32:]) + + ka, err := keyAuth(c.Key.Public(), token) + if err != nil { + return tls.Certificate{}, "", err + } + b = sha256.Sum256([]byte(ka)) + h = hex.EncodeToString(b[:]) + sanB := fmt.Sprintf("%s.%s.ka.acme.invalid", h[:32], h[32:]) + + cert, err = tlsChallengeCert([]string{sanA, sanB}, opt) + if err != nil { + return tls.Certificate{}, "", err + } + return cert, sanA, nil +} + +// TLSALPN01ChallengeCert creates a certificate for TLS-ALPN-01 challenge response. +// Servers can present the certificate to validate the challenge and prove control +// over a domain name. For more details on TLS-ALPN-01 see +// https://tools.ietf.org/html/draft-shoemaker-acme-tls-alpn-00#section-3 +// +// The token argument is a Challenge.Token value. +// If a WithKey option is provided, its private part signs the returned cert, +// and the public part is used to specify the signee. +// If no WithKey option is provided, a new ECDSA key is generated using P-256 curve. +// +// The returned certificate is valid for the next 24 hours and must be presented only when +// the server name in the TLS ClientHello matches the domain, and the special acme-tls/1 ALPN protocol +// has been specified. +func (c *Client) TLSALPN01ChallengeCert(token, domain string, opt ...CertOption) (cert tls.Certificate, err error) { + ka, err := keyAuth(c.Key.Public(), token) + if err != nil { + return tls.Certificate{}, err + } + shasum := sha256.Sum256([]byte(ka)) + extValue, err := asn1.Marshal(shasum[:]) + if err != nil { + return tls.Certificate{}, err + } + acmeExtension := pkix.Extension{ + Id: idPeACMEIdentifierV1, + Critical: true, + Value: extValue, + } + + tmpl := defaultTLSChallengeCertTemplate() + + var newOpt []CertOption + for _, o := range opt { + switch o := o.(type) { + case *certOptTemplate: + t := *(*x509.Certificate)(o) // shallow copy is ok + tmpl = &t + default: + newOpt = append(newOpt, o) + } + } + tmpl.ExtraExtensions = append(tmpl.ExtraExtensions, acmeExtension) + newOpt = append(newOpt, WithTemplate(tmpl)) + return tlsChallengeCert([]string{domain}, newOpt) +} + +// doReg sends all types of registration requests. +// The type of request is identified by typ argument, which is a "resource" +// in the ACME spec terms. +// +// A non-nil acct argument indicates whether the intention is to mutate data +// of the Account. Only Contact and Agreement of its fields are used +// in such cases. +func (c *Client) doReg(ctx context.Context, url string, typ string, acct *Account) (*Account, error) { + req := struct { + Resource string `json:"resource"` + Contact []string `json:"contact,omitempty"` + Agreement string `json:"agreement,omitempty"` + }{ + Resource: typ, + } + if acct != nil { + req.Contact = acct.Contact + req.Agreement = acct.AgreedTerms + } + res, err := c.post(ctx, c.Key, url, req, wantStatus( + http.StatusOK, // updates and deletes + http.StatusCreated, // new account creation + http.StatusAccepted, // Let's Encrypt divergent implementation + )) + if err != nil { + return nil, err + } + defer res.Body.Close() + + var v struct { + Contact []string + Agreement string + Authorizations string + Certificates string + } + if err := json.NewDecoder(res.Body).Decode(&v); err != nil { + return nil, fmt.Errorf("acme: invalid response: %v", err) + } + var tos string + if v := linkHeader(res.Header, "terms-of-service"); len(v) > 0 { + tos = v[0] + } + var authz string + if v := linkHeader(res.Header, "next"); len(v) > 0 { + authz = v[0] + } + return &Account{ + URI: res.Header.Get("Location"), + Contact: v.Contact, + AgreedTerms: v.Agreement, + CurrentTerms: tos, + Authz: authz, + Authorizations: v.Authorizations, + Certificates: v.Certificates, + }, nil +} + +// popNonce returns a nonce value previously stored with c.addNonce +// or fetches a fresh one from a URL by issuing a HEAD request. +// It first tries c.directoryURL() and then the provided url if the former fails. +func (c *Client) popNonce(ctx context.Context, url string) (string, error) { + c.noncesMu.Lock() + defer c.noncesMu.Unlock() + if len(c.nonces) == 0 { + dirURL := c.directoryURL() + v, err := c.fetchNonce(ctx, dirURL) + if err != nil && url != dirURL { + v, err = c.fetchNonce(ctx, url) + } + return v, err + } + var nonce string + for nonce = range c.nonces { + delete(c.nonces, nonce) + break + } + return nonce, nil +} + +// clearNonces clears any stored nonces +func (c *Client) clearNonces() { + c.noncesMu.Lock() + defer c.noncesMu.Unlock() + c.nonces = make(map[string]struct{}) +} + +// addNonce stores a nonce value found in h (if any) for future use. +func (c *Client) addNonce(h http.Header) { + v := nonceFromHeader(h) + if v == "" { + return + } + c.noncesMu.Lock() + defer c.noncesMu.Unlock() + if len(c.nonces) >= maxNonces { + return + } + if c.nonces == nil { + c.nonces = make(map[string]struct{}) + } + c.nonces[v] = struct{}{} +} + +func (c *Client) fetchNonce(ctx context.Context, url string) (string, error) { + r, err := http.NewRequest("HEAD", url, nil) + if err != nil { + return "", err + } + resp, err := c.doNoRetry(ctx, r) + if err != nil { + return "", err + } + defer resp.Body.Close() + nonce := nonceFromHeader(resp.Header) + if nonce == "" { + if resp.StatusCode > 299 { + return "", responseError(resp) + } + return "", errors.New("acme: nonce not found") + } + return nonce, nil +} + +func nonceFromHeader(h http.Header) string { + return h.Get("Replay-Nonce") +} + +func (c *Client) responseCert(ctx context.Context, res *http.Response, bundle bool) ([][]byte, error) { + b, err := ioutil.ReadAll(io.LimitReader(res.Body, maxCertSize+1)) + if err != nil { + return nil, fmt.Errorf("acme: response stream: %v", err) + } + if len(b) > maxCertSize { + return nil, errors.New("acme: certificate is too big") + } + cert := [][]byte{b} + if !bundle { + return cert, nil + } + + // Append CA chain cert(s). + // At least one is required according to the spec: + // https://tools.ietf.org/html/draft-ietf-acme-acme-03#section-6.3.1 + up := linkHeader(res.Header, "up") + if len(up) == 0 { + return nil, errors.New("acme: rel=up link not found") + } + if len(up) > maxChainLen { + return nil, errors.New("acme: rel=up link is too large") + } + for _, url := range up { + cc, err := c.chainCert(ctx, url, 0) + if err != nil { + return nil, err + } + cert = append(cert, cc...) + } + return cert, nil +} + +// chainCert fetches CA certificate chain recursively by following "up" links. +// Each recursive call increments the depth by 1, resulting in an error +// if the recursion level reaches maxChainLen. +// +// First chainCert call starts with depth of 0. +func (c *Client) chainCert(ctx context.Context, url string, depth int) ([][]byte, error) { + if depth >= maxChainLen { + return nil, errors.New("acme: certificate chain is too deep") + } + + res, err := c.get(ctx, url, wantStatus(http.StatusOK)) + if err != nil { + return nil, err + } + defer res.Body.Close() + b, err := ioutil.ReadAll(io.LimitReader(res.Body, maxCertSize+1)) + if err != nil { + return nil, err + } + if len(b) > maxCertSize { + return nil, errors.New("acme: certificate is too big") + } + chain := [][]byte{b} + + uplink := linkHeader(res.Header, "up") + if len(uplink) > maxChainLen { + return nil, errors.New("acme: certificate chain is too large") + } + for _, up := range uplink { + cc, err := c.chainCert(ctx, up, depth+1) + if err != nil { + return nil, err + } + chain = append(chain, cc...) + } + + return chain, nil +} + +// linkHeader returns URI-Reference values of all Link headers +// with relation-type rel. +// See https://tools.ietf.org/html/rfc5988#section-5 for details. +func linkHeader(h http.Header, rel string) []string { + var links []string + for _, v := range h["Link"] { + parts := strings.Split(v, ";") + for _, p := range parts { + p = strings.TrimSpace(p) + if !strings.HasPrefix(p, "rel=") { + continue + } + if v := strings.Trim(p[4:], `"`); v == rel { + links = append(links, strings.Trim(parts[0], "<>")) + } + } + } + return links +} + +// keyAuth generates a key authorization string for a given token. +func keyAuth(pub crypto.PublicKey, token string) (string, error) { + th, err := JWKThumbprint(pub) + if err != nil { + return "", err + } + return fmt.Sprintf("%s.%s", token, th), nil +} + +// defaultTLSChallengeCertTemplate is a template used to create challenge certs for TLS challenges. +func defaultTLSChallengeCertTemplate() *x509.Certificate { + return &x509.Certificate{ + SerialNumber: big.NewInt(1), + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + } +} + +// tlsChallengeCert creates a temporary certificate for TLS-SNI challenges +// with the given SANs and auto-generated public/private key pair. +// The Subject Common Name is set to the first SAN to aid debugging. +// To create a cert with a custom key pair, specify WithKey option. +func tlsChallengeCert(san []string, opt []CertOption) (tls.Certificate, error) { + var key crypto.Signer + tmpl := defaultTLSChallengeCertTemplate() + for _, o := range opt { + switch o := o.(type) { + case *certOptKey: + if key != nil { + return tls.Certificate{}, errors.New("acme: duplicate key option") + } + key = o.key + case *certOptTemplate: + t := *(*x509.Certificate)(o) // shallow copy is ok + tmpl = &t + default: + // package's fault, if we let this happen: + panic(fmt.Sprintf("unsupported option type %T", o)) + } + } + if key == nil { + var err error + if key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader); err != nil { + return tls.Certificate{}, err + } + } + tmpl.DNSNames = san + if len(san) > 0 { + tmpl.Subject.CommonName = san[0] + } + + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key) + if err != nil { + return tls.Certificate{}, err + } + return tls.Certificate{ + Certificate: [][]byte{der}, + PrivateKey: key, + }, nil +} + +// encodePEM returns b encoded as PEM with block of type typ. +func encodePEM(typ string, b []byte) []byte { + pb := &pem.Block{Type: typ, Bytes: b} + return pem.EncodeToMemory(pb) +} + +// timeNow is useful for testing for fixed current time. +var timeNow = time.Now diff --git a/src/vendor/golang.org/x/crypto/acme/autocert/autocert.go b/src/vendor/golang.org/x/crypto/acme/autocert/autocert.go new file mode 100644 index 000000000..e562609cc --- /dev/null +++ b/src/vendor/golang.org/x/crypto/acme/autocert/autocert.go @@ -0,0 +1,1156 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package autocert provides automatic access to certificates from Let's Encrypt +// and any other ACME-based CA. +// +// This package is a work in progress and makes no API stability promises. +package autocert + +import ( + "bytes" + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "errors" + "fmt" + "io" + mathrand "math/rand" + "net" + "net/http" + "path" + "strings" + "sync" + "time" + + "golang.org/x/crypto/acme" + "golang.org/x/net/idna" +) + +// createCertRetryAfter is how much time to wait before removing a failed state +// entry due to an unsuccessful createCert call. +// This is a variable instead of a const for testing. +// TODO: Consider making it configurable or an exp backoff? +var createCertRetryAfter = time.Minute + +// pseudoRand is safe for concurrent use. +var pseudoRand *lockedMathRand + +func init() { + src := mathrand.NewSource(time.Now().UnixNano()) + pseudoRand = &lockedMathRand{rnd: mathrand.New(src)} +} + +// AcceptTOS is a Manager.Prompt function that always returns true to +// indicate acceptance of the CA's Terms of Service during account +// registration. +func AcceptTOS(tosURL string) bool { return true } + +// HostPolicy specifies which host names the Manager is allowed to respond to. +// It returns a non-nil error if the host should be rejected. +// The returned error is accessible via tls.Conn.Handshake and its callers. +// See Manager's HostPolicy field and GetCertificate method docs for more details. +type HostPolicy func(ctx context.Context, host string) error + +// HostWhitelist returns a policy where only the specified host names are allowed. +// Only exact matches are currently supported. Subdomains, regexp or wildcard +// will not match. +// +// Note that all hosts will be converted to Punycode via idna.Lookup.ToASCII so that +// Manager.GetCertificate can handle the Unicode IDN and mixedcase hosts correctly. +// Invalid hosts will be silently ignored. +func HostWhitelist(hosts ...string) HostPolicy { + whitelist := make(map[string]bool, len(hosts)) + for _, h := range hosts { + if h, err := idna.Lookup.ToASCII(h); err == nil { + whitelist[h] = true + } + } + return func(_ context.Context, host string) error { + if !whitelist[host] { + return fmt.Errorf("acme/autocert: host %q not configured in HostWhitelist", host) + } + return nil + } +} + +// defaultHostPolicy is used when Manager.HostPolicy is not set. +func defaultHostPolicy(context.Context, string) error { + return nil +} + +// Manager is a stateful certificate manager built on top of acme.Client. +// It obtains and refreshes certificates automatically using "tls-alpn-01", +// "tls-sni-01", "tls-sni-02" and "http-01" challenge types, +// as well as providing them to a TLS server via tls.Config. +// +// You must specify a cache implementation, such as DirCache, +// to reuse obtained certificates across program restarts. +// Otherwise your server is very likely to exceed the certificate +// issuer's request rate limits. +type Manager struct { + // Prompt specifies a callback function to conditionally accept a CA's Terms of Service (TOS). + // The registration may require the caller to agree to the CA's TOS. + // If so, Manager calls Prompt with a TOS URL provided by the CA. Prompt should report + // whether the caller agrees to the terms. + // + // To always accept the terms, the callers can use AcceptTOS. + Prompt func(tosURL string) bool + + // Cache optionally stores and retrieves previously-obtained certificates + // and other state. If nil, certs will only be cached for the lifetime of + // the Manager. Multiple Managers can share the same Cache. + // + // Using a persistent Cache, such as DirCache, is strongly recommended. + Cache Cache + + // HostPolicy controls which domains the Manager will attempt + // to retrieve new certificates for. It does not affect cached certs. + // + // If non-nil, HostPolicy is called before requesting a new cert. + // If nil, all hosts are currently allowed. This is not recommended, + // as it opens a potential attack where clients connect to a server + // by IP address and pretend to be asking for an incorrect host name. + // Manager will attempt to obtain a certificate for that host, incorrectly, + // eventually reaching the CA's rate limit for certificate requests + // and making it impossible to obtain actual certificates. + // + // See GetCertificate for more details. + HostPolicy HostPolicy + + // RenewBefore optionally specifies how early certificates should + // be renewed before they expire. + // + // If zero, they're renewed 30 days before expiration. + RenewBefore time.Duration + + // Client is used to perform low-level operations, such as account registration + // and requesting new certificates. + // + // If Client is nil, a zero-value acme.Client is used with acme.LetsEncryptURL + // as directory endpoint. If the Client.Key is nil, a new ECDSA P-256 key is + // generated and, if Cache is not nil, stored in cache. + // + // Mutating the field after the first call of GetCertificate method will have no effect. + Client *acme.Client + + // Email optionally specifies a contact email address. + // This is used by CAs, such as Let's Encrypt, to notify about problems + // with issued certificates. + // + // If the Client's account key is already registered, Email is not used. + Email string + + // ForceRSA used to make the Manager generate RSA certificates. It is now ignored. + // + // Deprecated: the Manager will request the correct type of certificate based + // on what each client supports. + ForceRSA bool + + // ExtraExtensions are used when generating a new CSR (Certificate Request), + // thus allowing customization of the resulting certificate. + // For instance, TLS Feature Extension (RFC 7633) can be used + // to prevent an OCSP downgrade attack. + // + // The field value is passed to crypto/x509.CreateCertificateRequest + // in the template's ExtraExtensions field as is. + ExtraExtensions []pkix.Extension + + clientMu sync.Mutex + client *acme.Client // initialized by acmeClient method + + stateMu sync.Mutex + state map[certKey]*certState + + // renewal tracks the set of domains currently running renewal timers. + renewalMu sync.Mutex + renewal map[certKey]*domainRenewal + + // tokensMu guards the rest of the fields: tryHTTP01, certTokens and httpTokens. + tokensMu sync.RWMutex + // tryHTTP01 indicates whether the Manager should try "http-01" challenge type + // during the authorization flow. + tryHTTP01 bool + // httpTokens contains response body values for http-01 challenges + // and is keyed by the URL path at which a challenge response is expected + // to be provisioned. + // The entries are stored for the duration of the authorization flow. + httpTokens map[string][]byte + // certTokens contains temporary certificates for tls-sni and tls-alpn challenges + // and is keyed by token domain name, which matches server name of ClientHello. + // Keys always have ".acme.invalid" suffix for tls-sni. Otherwise, they are domain names + // for tls-alpn. + // The entries are stored for the duration of the authorization flow. + certTokens map[string]*tls.Certificate + // nowFunc, if not nil, returns the current time. This may be set for + // testing purposes. + nowFunc func() time.Time +} + +// certKey is the key by which certificates are tracked in state, renewal and cache. +type certKey struct { + domain string // without trailing dot + isRSA bool // RSA cert for legacy clients (as opposed to default ECDSA) + isToken bool // tls-based challenge token cert; key type is undefined regardless of isRSA +} + +func (c certKey) String() string { + if c.isToken { + return c.domain + "+token" + } + if c.isRSA { + return c.domain + "+rsa" + } + return c.domain +} + +// TLSConfig creates a new TLS config suitable for net/http.Server servers, +// supporting HTTP/2 and the tls-alpn-01 ACME challenge type. +func (m *Manager) TLSConfig() *tls.Config { + return &tls.Config{ + GetCertificate: m.GetCertificate, + NextProtos: []string{ + "h2", "http/1.1", // enable HTTP/2 + acme.ALPNProto, // enable tls-alpn ACME challenges + }, + } +} + +// GetCertificate implements the tls.Config.GetCertificate hook. +// It provides a TLS certificate for hello.ServerName host, including answering +// tls-alpn-01 and *.acme.invalid (tls-sni-01 and tls-sni-02) challenges. +// All other fields of hello are ignored. +// +// If m.HostPolicy is non-nil, GetCertificate calls the policy before requesting +// a new cert. A non-nil error returned from m.HostPolicy halts TLS negotiation. +// The error is propagated back to the caller of GetCertificate and is user-visible. +// This does not affect cached certs. See HostPolicy field description for more details. +// +// If GetCertificate is used directly, instead of via Manager.TLSConfig, package users will +// also have to add acme.ALPNProto to NextProtos for tls-alpn-01, or use HTTPHandler +// for http-01. (The tls-sni-* challenges have been deprecated by popular ACME providers +// due to security issues in the ecosystem.) +func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { + if m.Prompt == nil { + return nil, errors.New("acme/autocert: Manager.Prompt not set") + } + + name := hello.ServerName + if name == "" { + return nil, errors.New("acme/autocert: missing server name") + } + if !strings.Contains(strings.Trim(name, "."), ".") { + return nil, errors.New("acme/autocert: server name component count invalid") + } + + // Note that this conversion is necessary because some server names in the handshakes + // started by some clients (such as cURL) are not converted to Punycode, which will + // prevent us from obtaining certificates for them. In addition, we should also treat + // example.com and EXAMPLE.COM as equivalent and return the same certificate for them. + // Fortunately, this conversion also helped us deal with this kind of mixedcase problems. + // + // Due to the "σςΣ" problem (see https://unicode.org/faq/idn.html#22), we can't use + // idna.Punycode.ToASCII (or just idna.ToASCII) here. + name, err := idna.Lookup.ToASCII(name) + if err != nil { + return nil, errors.New("acme/autocert: server name contains invalid character") + } + + // In the worst-case scenario, the timeout needs to account for caching, host policy, + // domain ownership verification and certificate issuance. + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + // Check whether this is a token cert requested for TLS-SNI or TLS-ALPN challenge. + if wantsTokenCert(hello) { + m.tokensMu.RLock() + defer m.tokensMu.RUnlock() + // It's ok to use the same token cert key for both tls-sni and tls-alpn + // because there's always at most 1 token cert per on-going domain authorization. + // See m.verify for details. + if cert := m.certTokens[name]; cert != nil { + return cert, nil + } + if cert, err := m.cacheGet(ctx, certKey{domain: name, isToken: true}); err == nil { + return cert, nil + } + // TODO: cache error results? + return nil, fmt.Errorf("acme/autocert: no token cert for %q", name) + } + + // regular domain + ck := certKey{ + domain: strings.TrimSuffix(name, "."), // golang.org/issue/18114 + isRSA: !supportsECDSA(hello), + } + cert, err := m.cert(ctx, ck) + if err == nil { + return cert, nil + } + if err != ErrCacheMiss { + return nil, err + } + + // first-time + if err := m.hostPolicy()(ctx, name); err != nil { + return nil, err + } + cert, err = m.createCert(ctx, ck) + if err != nil { + return nil, err + } + m.cachePut(ctx, ck, cert) + return cert, nil +} + +// wantsTokenCert reports whether a TLS request with SNI is made by a CA server +// for a challenge verification. +func wantsTokenCert(hello *tls.ClientHelloInfo) bool { + // tls-alpn-01 + if len(hello.SupportedProtos) == 1 && hello.SupportedProtos[0] == acme.ALPNProto { + return true + } + // tls-sni-xx + return strings.HasSuffix(hello.ServerName, ".acme.invalid") +} + +func supportsECDSA(hello *tls.ClientHelloInfo) bool { + // The "signature_algorithms" extension, if present, limits the key exchange + // algorithms allowed by the cipher suites. See RFC 5246, section 7.4.1.4.1. + if hello.SignatureSchemes != nil { + ecdsaOK := false + schemeLoop: + for _, scheme := range hello.SignatureSchemes { + const tlsECDSAWithSHA1 tls.SignatureScheme = 0x0203 // constant added in Go 1.10 + switch scheme { + case tlsECDSAWithSHA1, tls.ECDSAWithP256AndSHA256, + tls.ECDSAWithP384AndSHA384, tls.ECDSAWithP521AndSHA512: + ecdsaOK = true + break schemeLoop + } + } + if !ecdsaOK { + return false + } + } + if hello.SupportedCurves != nil { + ecdsaOK := false + for _, curve := range hello.SupportedCurves { + if curve == tls.CurveP256 { + ecdsaOK = true + break + } + } + if !ecdsaOK { + return false + } + } + for _, suite := range hello.CipherSuites { + switch suite { + case tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305: + return true + } + } + return false +} + +// HTTPHandler configures the Manager to provision ACME "http-01" challenge responses. +// It returns an http.Handler that responds to the challenges and must be +// running on port 80. If it receives a request that is not an ACME challenge, +// it delegates the request to the optional fallback handler. +// +// If fallback is nil, the returned handler redirects all GET and HEAD requests +// to the default TLS port 443 with 302 Found status code, preserving the original +// request path and query. It responds with 400 Bad Request to all other HTTP methods. +// The fallback is not protected by the optional HostPolicy. +// +// Because the fallback handler is run with unencrypted port 80 requests, +// the fallback should not serve TLS-only requests. +// +// If HTTPHandler is never called, the Manager will only use the "tls-alpn-01" +// challenge for domain verification. +func (m *Manager) HTTPHandler(fallback http.Handler) http.Handler { + m.tokensMu.Lock() + defer m.tokensMu.Unlock() + m.tryHTTP01 = true + + if fallback == nil { + fallback = http.HandlerFunc(handleHTTPRedirect) + } + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !strings.HasPrefix(r.URL.Path, "/.well-known/acme-challenge/") { + fallback.ServeHTTP(w, r) + return + } + // A reasonable context timeout for cache and host policy only, + // because we don't wait for a new certificate issuance here. + ctx, cancel := context.WithTimeout(r.Context(), time.Minute) + defer cancel() + if err := m.hostPolicy()(ctx, r.Host); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + data, err := m.httpToken(ctx, r.URL.Path) + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + w.Write(data) + }) +} + +func handleHTTPRedirect(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" && r.Method != "HEAD" { + http.Error(w, "Use HTTPS", http.StatusBadRequest) + return + } + target := "https://" + stripPort(r.Host) + r.URL.RequestURI() + http.Redirect(w, r, target, http.StatusFound) +} + +func stripPort(hostport string) string { + host, _, err := net.SplitHostPort(hostport) + if err != nil { + return hostport + } + return net.JoinHostPort(host, "443") +} + +// cert returns an existing certificate either from m.state or cache. +// If a certificate is found in cache but not in m.state, the latter will be filled +// with the cached value. +func (m *Manager) cert(ctx context.Context, ck certKey) (*tls.Certificate, error) { + m.stateMu.Lock() + if s, ok := m.state[ck]; ok { + m.stateMu.Unlock() + s.RLock() + defer s.RUnlock() + return s.tlscert() + } + defer m.stateMu.Unlock() + cert, err := m.cacheGet(ctx, ck) + if err != nil { + return nil, err + } + signer, ok := cert.PrivateKey.(crypto.Signer) + if !ok { + return nil, errors.New("acme/autocert: private key cannot sign") + } + if m.state == nil { + m.state = make(map[certKey]*certState) + } + s := &certState{ + key: signer, + cert: cert.Certificate, + leaf: cert.Leaf, + } + m.state[ck] = s + go m.renew(ck, s.key, s.leaf.NotAfter) + return cert, nil +} + +// cacheGet always returns a valid certificate, or an error otherwise. +// If a cached certificate exists but is not valid, ErrCacheMiss is returned. +func (m *Manager) cacheGet(ctx context.Context, ck certKey) (*tls.Certificate, error) { + if m.Cache == nil { + return nil, ErrCacheMiss + } + data, err := m.Cache.Get(ctx, ck.String()) + if err != nil { + return nil, err + } + + // private + priv, pub := pem.Decode(data) + if priv == nil || !strings.Contains(priv.Type, "PRIVATE") { + return nil, ErrCacheMiss + } + privKey, err := parsePrivateKey(priv.Bytes) + if err != nil { + return nil, err + } + + // public + var pubDER [][]byte + for len(pub) > 0 { + var b *pem.Block + b, pub = pem.Decode(pub) + if b == nil { + break + } + pubDER = append(pubDER, b.Bytes) + } + if len(pub) > 0 { + // Leftover content not consumed by pem.Decode. Corrupt. Ignore. + return nil, ErrCacheMiss + } + + // verify and create TLS cert + leaf, err := validCert(ck, pubDER, privKey, m.now()) + if err != nil { + return nil, ErrCacheMiss + } + tlscert := &tls.Certificate{ + Certificate: pubDER, + PrivateKey: privKey, + Leaf: leaf, + } + return tlscert, nil +} + +func (m *Manager) cachePut(ctx context.Context, ck certKey, tlscert *tls.Certificate) error { + if m.Cache == nil { + return nil + } + + // contains PEM-encoded data + var buf bytes.Buffer + + // private + switch key := tlscert.PrivateKey.(type) { + case *ecdsa.PrivateKey: + if err := encodeECDSAKey(&buf, key); err != nil { + return err + } + case *rsa.PrivateKey: + b := x509.MarshalPKCS1PrivateKey(key) + pb := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: b} + if err := pem.Encode(&buf, pb); err != nil { + return err + } + default: + return errors.New("acme/autocert: unknown private key type") + } + + // public + for _, b := range tlscert.Certificate { + pb := &pem.Block{Type: "CERTIFICATE", Bytes: b} + if err := pem.Encode(&buf, pb); err != nil { + return err + } + } + + return m.Cache.Put(ctx, ck.String(), buf.Bytes()) +} + +func encodeECDSAKey(w io.Writer, key *ecdsa.PrivateKey) error { + b, err := x509.MarshalECPrivateKey(key) + if err != nil { + return err + } + pb := &pem.Block{Type: "EC PRIVATE KEY", Bytes: b} + return pem.Encode(w, pb) +} + +// createCert starts the domain ownership verification and returns a certificate +// for that domain upon success. +// +// If the domain is already being verified, it waits for the existing verification to complete. +// Either way, createCert blocks for the duration of the whole process. +func (m *Manager) createCert(ctx context.Context, ck certKey) (*tls.Certificate, error) { + // TODO: maybe rewrite this whole piece using sync.Once + state, err := m.certState(ck) + if err != nil { + return nil, err + } + // state may exist if another goroutine is already working on it + // in which case just wait for it to finish + if !state.locked { + state.RLock() + defer state.RUnlock() + return state.tlscert() + } + + // We are the first; state is locked. + // Unblock the readers when domain ownership is verified + // and we got the cert or the process failed. + defer state.Unlock() + state.locked = false + + der, leaf, err := m.authorizedCert(ctx, state.key, ck) + if err != nil { + // Remove the failed state after some time, + // making the manager call createCert again on the following TLS hello. + time.AfterFunc(createCertRetryAfter, func() { + defer testDidRemoveState(ck) + m.stateMu.Lock() + defer m.stateMu.Unlock() + // Verify the state hasn't changed and it's still invalid + // before deleting. + s, ok := m.state[ck] + if !ok { + return + } + if _, err := validCert(ck, s.cert, s.key, m.now()); err == nil { + return + } + delete(m.state, ck) + }) + return nil, err + } + state.cert = der + state.leaf = leaf + go m.renew(ck, state.key, state.leaf.NotAfter) + return state.tlscert() +} + +// certState returns a new or existing certState. +// If a new certState is returned, state.exist is false and the state is locked. +// The returned error is non-nil only in the case where a new state could not be created. +func (m *Manager) certState(ck certKey) (*certState, error) { + m.stateMu.Lock() + defer m.stateMu.Unlock() + if m.state == nil { + m.state = make(map[certKey]*certState) + } + // existing state + if state, ok := m.state[ck]; ok { + return state, nil + } + + // new locked state + var ( + err error + key crypto.Signer + ) + if ck.isRSA { + key, err = rsa.GenerateKey(rand.Reader, 2048) + } else { + key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + } + if err != nil { + return nil, err + } + + state := &certState{ + key: key, + locked: true, + } + state.Lock() // will be unlocked by m.certState caller + m.state[ck] = state + return state, nil +} + +// authorizedCert starts the domain ownership verification process and requests a new cert upon success. +// The key argument is the certificate private key. +func (m *Manager) authorizedCert(ctx context.Context, key crypto.Signer, ck certKey) (der [][]byte, leaf *x509.Certificate, err error) { + client, err := m.acmeClient(ctx) + if err != nil { + return nil, nil, err + } + + if err := m.verify(ctx, client, ck.domain); err != nil { + return nil, nil, err + } + csr, err := certRequest(key, ck.domain, m.ExtraExtensions) + if err != nil { + return nil, nil, err + } + der, _, err = client.CreateCert(ctx, csr, 0, true) + if err != nil { + return nil, nil, err + } + leaf, err = validCert(ck, der, key, m.now()) + if err != nil { + return nil, nil, err + } + return der, leaf, nil +} + +// revokePendingAuthz revokes all authorizations idenfied by the elements of uri slice. +// It ignores revocation errors. +func (m *Manager) revokePendingAuthz(ctx context.Context, uri []string) { + client, err := m.acmeClient(ctx) + if err != nil { + return + } + for _, u := range uri { + client.RevokeAuthorization(ctx, u) + } +} + +// verify runs the identifier (domain) authorization flow +// using each applicable ACME challenge type. +func (m *Manager) verify(ctx context.Context, client *acme.Client, domain string) error { + // The list of challenge types we'll try to fulfill + // in this specific order. + challengeTypes := []string{"tls-alpn-01", "tls-sni-02", "tls-sni-01"} + m.tokensMu.RLock() + if m.tryHTTP01 { + challengeTypes = append(challengeTypes, "http-01") + } + m.tokensMu.RUnlock() + + // Keep track of pending authzs and revoke the ones that did not validate. + pendingAuthzs := make(map[string]bool) + defer func() { + var uri []string + for k, pending := range pendingAuthzs { + if pending { + uri = append(uri, k) + } + } + if len(uri) > 0 { + // Use "detached" background context. + // The revocations need not happen in the current verification flow. + go m.revokePendingAuthz(context.Background(), uri) + } + }() + + // errs accumulates challenge failure errors, printed if all fail + errs := make(map[*acme.Challenge]error) + var nextTyp int // challengeType index of the next challenge type to try + for { + // Start domain authorization and get the challenge. + authz, err := client.Authorize(ctx, domain) + if err != nil { + return err + } + // No point in accepting challenges if the authorization status + // is in a final state. + switch authz.Status { + case acme.StatusValid: + return nil // already authorized + case acme.StatusInvalid: + return fmt.Errorf("acme/autocert: invalid authorization %q", authz.URI) + } + + pendingAuthzs[authz.URI] = true + + // Pick the next preferred challenge. + var chal *acme.Challenge + for chal == nil && nextTyp < len(challengeTypes) { + chal = pickChallenge(challengeTypes[nextTyp], authz.Challenges) + nextTyp++ + } + if chal == nil { + errorMsg := fmt.Sprintf("acme/autocert: unable to authorize %q", domain) + for chal, err := range errs { + errorMsg += fmt.Sprintf("; challenge %q failed with error: %v", chal.Type, err) + } + return errors.New(errorMsg) + } + cleanup, err := m.fulfill(ctx, client, chal, domain) + if err != nil { + errs[chal] = err + continue + } + defer cleanup() + if _, err := client.Accept(ctx, chal); err != nil { + errs[chal] = err + continue + } + + // A challenge is fulfilled and accepted: wait for the CA to validate. + if _, err := client.WaitAuthorization(ctx, authz.URI); err != nil { + errs[chal] = err + continue + } + delete(pendingAuthzs, authz.URI) + return nil + } +} + +// fulfill provisions a response to the challenge chal. +// The cleanup is non-nil only if provisioning succeeded. +func (m *Manager) fulfill(ctx context.Context, client *acme.Client, chal *acme.Challenge, domain string) (cleanup func(), err error) { + switch chal.Type { + case "tls-alpn-01": + cert, err := client.TLSALPN01ChallengeCert(chal.Token, domain) + if err != nil { + return nil, err + } + m.putCertToken(ctx, domain, &cert) + return func() { go m.deleteCertToken(domain) }, nil + case "tls-sni-01": + cert, name, err := client.TLSSNI01ChallengeCert(chal.Token) + if err != nil { + return nil, err + } + m.putCertToken(ctx, name, &cert) + return func() { go m.deleteCertToken(name) }, nil + case "tls-sni-02": + cert, name, err := client.TLSSNI02ChallengeCert(chal.Token) + if err != nil { + return nil, err + } + m.putCertToken(ctx, name, &cert) + return func() { go m.deleteCertToken(name) }, nil + case "http-01": + resp, err := client.HTTP01ChallengeResponse(chal.Token) + if err != nil { + return nil, err + } + p := client.HTTP01ChallengePath(chal.Token) + m.putHTTPToken(ctx, p, resp) + return func() { go m.deleteHTTPToken(p) }, nil + } + return nil, fmt.Errorf("acme/autocert: unknown challenge type %q", chal.Type) +} + +func pickChallenge(typ string, chal []*acme.Challenge) *acme.Challenge { + for _, c := range chal { + if c.Type == typ { + return c + } + } + return nil +} + +// putCertToken stores the token certificate with the specified name +// in both m.certTokens map and m.Cache. +func (m *Manager) putCertToken(ctx context.Context, name string, cert *tls.Certificate) { + m.tokensMu.Lock() + defer m.tokensMu.Unlock() + if m.certTokens == nil { + m.certTokens = make(map[string]*tls.Certificate) + } + m.certTokens[name] = cert + m.cachePut(ctx, certKey{domain: name, isToken: true}, cert) +} + +// deleteCertToken removes the token certificate with the specified name +// from both m.certTokens map and m.Cache. +func (m *Manager) deleteCertToken(name string) { + m.tokensMu.Lock() + defer m.tokensMu.Unlock() + delete(m.certTokens, name) + if m.Cache != nil { + ck := certKey{domain: name, isToken: true} + m.Cache.Delete(context.Background(), ck.String()) + } +} + +// httpToken retrieves an existing http-01 token value from an in-memory map +// or the optional cache. +func (m *Manager) httpToken(ctx context.Context, tokenPath string) ([]byte, error) { + m.tokensMu.RLock() + defer m.tokensMu.RUnlock() + if v, ok := m.httpTokens[tokenPath]; ok { + return v, nil + } + if m.Cache == nil { + return nil, fmt.Errorf("acme/autocert: no token at %q", tokenPath) + } + return m.Cache.Get(ctx, httpTokenCacheKey(tokenPath)) +} + +// putHTTPToken stores an http-01 token value using tokenPath as key +// in both in-memory map and the optional Cache. +// +// It ignores any error returned from Cache.Put. +func (m *Manager) putHTTPToken(ctx context.Context, tokenPath, val string) { + m.tokensMu.Lock() + defer m.tokensMu.Unlock() + if m.httpTokens == nil { + m.httpTokens = make(map[string][]byte) + } + b := []byte(val) + m.httpTokens[tokenPath] = b + if m.Cache != nil { + m.Cache.Put(ctx, httpTokenCacheKey(tokenPath), b) + } +} + +// deleteHTTPToken removes an http-01 token value from both in-memory map +// and the optional Cache, ignoring any error returned from the latter. +// +// If m.Cache is non-nil, it blocks until Cache.Delete returns without a timeout. +func (m *Manager) deleteHTTPToken(tokenPath string) { + m.tokensMu.Lock() + defer m.tokensMu.Unlock() + delete(m.httpTokens, tokenPath) + if m.Cache != nil { + m.Cache.Delete(context.Background(), httpTokenCacheKey(tokenPath)) + } +} + +// httpTokenCacheKey returns a key at which an http-01 token value may be stored +// in the Manager's optional Cache. +func httpTokenCacheKey(tokenPath string) string { + return path.Base(tokenPath) + "+http-01" +} + +// renew starts a cert renewal timer loop, one per domain. +// +// The loop is scheduled in two cases: +// - a cert was fetched from cache for the first time (wasn't in m.state) +// - a new cert was created by m.createCert +// +// The key argument is a certificate private key. +// The exp argument is the cert expiration time (NotAfter). +func (m *Manager) renew(ck certKey, key crypto.Signer, exp time.Time) { + m.renewalMu.Lock() + defer m.renewalMu.Unlock() + if m.renewal[ck] != nil { + // another goroutine is already on it + return + } + if m.renewal == nil { + m.renewal = make(map[certKey]*domainRenewal) + } + dr := &domainRenewal{m: m, ck: ck, key: key} + m.renewal[ck] = dr + dr.start(exp) +} + +// stopRenew stops all currently running cert renewal timers. +// The timers are not restarted during the lifetime of the Manager. +func (m *Manager) stopRenew() { + m.renewalMu.Lock() + defer m.renewalMu.Unlock() + for name, dr := range m.renewal { + delete(m.renewal, name) + dr.stop() + } +} + +func (m *Manager) accountKey(ctx context.Context) (crypto.Signer, error) { + const keyName = "acme_account+key" + + // Previous versions of autocert stored the value under a different key. + const legacyKeyName = "acme_account.key" + + genKey := func() (*ecdsa.PrivateKey, error) { + return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + } + + if m.Cache == nil { + return genKey() + } + + data, err := m.Cache.Get(ctx, keyName) + if err == ErrCacheMiss { + data, err = m.Cache.Get(ctx, legacyKeyName) + } + if err == ErrCacheMiss { + key, err := genKey() + if err != nil { + return nil, err + } + var buf bytes.Buffer + if err := encodeECDSAKey(&buf, key); err != nil { + return nil, err + } + if err := m.Cache.Put(ctx, keyName, buf.Bytes()); err != nil { + return nil, err + } + return key, nil + } + if err != nil { + return nil, err + } + + priv, _ := pem.Decode(data) + if priv == nil || !strings.Contains(priv.Type, "PRIVATE") { + return nil, errors.New("acme/autocert: invalid account key found in cache") + } + return parsePrivateKey(priv.Bytes) +} + +func (m *Manager) acmeClient(ctx context.Context) (*acme.Client, error) { + m.clientMu.Lock() + defer m.clientMu.Unlock() + if m.client != nil { + return m.client, nil + } + + client := m.Client + if client == nil { + client = &acme.Client{DirectoryURL: acme.LetsEncryptURL} + } + if client.Key == nil { + var err error + client.Key, err = m.accountKey(ctx) + if err != nil { + return nil, err + } + } + var contact []string + if m.Email != "" { + contact = []string{"mailto:" + m.Email} + } + a := &acme.Account{Contact: contact} + _, err := client.Register(ctx, a, m.Prompt) + if ae, ok := err.(*acme.Error); err == nil || ok && ae.StatusCode == http.StatusConflict { + // conflict indicates the key is already registered + m.client = client + err = nil + } + return m.client, err +} + +func (m *Manager) hostPolicy() HostPolicy { + if m.HostPolicy != nil { + return m.HostPolicy + } + return defaultHostPolicy +} + +func (m *Manager) renewBefore() time.Duration { + if m.RenewBefore > renewJitter { + return m.RenewBefore + } + return 720 * time.Hour // 30 days +} + +func (m *Manager) now() time.Time { + if m.nowFunc != nil { + return m.nowFunc() + } + return time.Now() +} + +// certState is ready when its mutex is unlocked for reading. +type certState struct { + sync.RWMutex + locked bool // locked for read/write + key crypto.Signer // private key for cert + cert [][]byte // DER encoding + leaf *x509.Certificate // parsed cert[0]; always non-nil if cert != nil +} + +// tlscert creates a tls.Certificate from s.key and s.cert. +// Callers should wrap it in s.RLock() and s.RUnlock(). +func (s *certState) tlscert() (*tls.Certificate, error) { + if s.key == nil { + return nil, errors.New("acme/autocert: missing signer") + } + if len(s.cert) == 0 { + return nil, errors.New("acme/autocert: missing certificate") + } + return &tls.Certificate{ + PrivateKey: s.key, + Certificate: s.cert, + Leaf: s.leaf, + }, nil +} + +// certRequest generates a CSR for the given common name cn and optional SANs. +func certRequest(key crypto.Signer, cn string, ext []pkix.Extension, san ...string) ([]byte, error) { + req := &x509.CertificateRequest{ + Subject: pkix.Name{CommonName: cn}, + DNSNames: san, + ExtraExtensions: ext, + } + return x509.CreateCertificateRequest(rand.Reader, req, key) +} + +// Attempt to parse the given private key DER block. OpenSSL 0.9.8 generates +// PKCS#1 private keys by default, while OpenSSL 1.0.0 generates PKCS#8 keys. +// OpenSSL ecparam generates SEC1 EC private keys for ECDSA. We try all three. +// +// Inspired by parsePrivateKey in crypto/tls/tls.go. +func parsePrivateKey(der []byte) (crypto.Signer, error) { + if key, err := x509.ParsePKCS1PrivateKey(der); err == nil { + return key, nil + } + if key, err := x509.ParsePKCS8PrivateKey(der); err == nil { + switch key := key.(type) { + case *rsa.PrivateKey: + return key, nil + case *ecdsa.PrivateKey: + return key, nil + default: + return nil, errors.New("acme/autocert: unknown private key type in PKCS#8 wrapping") + } + } + if key, err := x509.ParseECPrivateKey(der); err == nil { + return key, nil + } + + return nil, errors.New("acme/autocert: failed to parse private key") +} + +// validCert parses a cert chain provided as der argument and verifies the leaf and der[0] +// correspond to the private key, the domain and key type match, and expiration dates +// are valid. It doesn't do any revocation checking. +// +// The returned value is the verified leaf cert. +func validCert(ck certKey, der [][]byte, key crypto.Signer, now time.Time) (leaf *x509.Certificate, err error) { + // parse public part(s) + var n int + for _, b := range der { + n += len(b) + } + pub := make([]byte, n) + n = 0 + for _, b := range der { + n += copy(pub[n:], b) + } + x509Cert, err := x509.ParseCertificates(pub) + if err != nil || len(x509Cert) == 0 { + return nil, errors.New("acme/autocert: no public key found") + } + // verify the leaf is not expired and matches the domain name + leaf = x509Cert[0] + if now.Before(leaf.NotBefore) { + return nil, errors.New("acme/autocert: certificate is not valid yet") + } + if now.After(leaf.NotAfter) { + return nil, errors.New("acme/autocert: expired certificate") + } + if err := leaf.VerifyHostname(ck.domain); err != nil { + return nil, err + } + // ensure the leaf corresponds to the private key and matches the certKey type + switch pub := leaf.PublicKey.(type) { + case *rsa.PublicKey: + prv, ok := key.(*rsa.PrivateKey) + if !ok { + return nil, errors.New("acme/autocert: private key type does not match public key type") + } + if pub.N.Cmp(prv.N) != 0 { + return nil, errors.New("acme/autocert: private key does not match public key") + } + if !ck.isRSA && !ck.isToken { + return nil, errors.New("acme/autocert: key type does not match expected value") + } + case *ecdsa.PublicKey: + prv, ok := key.(*ecdsa.PrivateKey) + if !ok { + return nil, errors.New("acme/autocert: private key type does not match public key type") + } + if pub.X.Cmp(prv.X) != 0 || pub.Y.Cmp(prv.Y) != 0 { + return nil, errors.New("acme/autocert: private key does not match public key") + } + if ck.isRSA && !ck.isToken { + return nil, errors.New("acme/autocert: key type does not match expected value") + } + default: + return nil, errors.New("acme/autocert: unknown public key algorithm") + } + return leaf, nil +} + +type lockedMathRand struct { + sync.Mutex + rnd *mathrand.Rand +} + +func (r *lockedMathRand) int63n(max int64) int64 { + r.Lock() + n := r.rnd.Int63n(max) + r.Unlock() + return n +} + +// For easier testing. +var ( + // Called when a state is removed. + testDidRemoveState = func(certKey) {} +) diff --git a/src/vendor/golang.org/x/crypto/acme/autocert/cache.go b/src/vendor/golang.org/x/crypto/acme/autocert/cache.go new file mode 100644 index 000000000..aa9aa845c --- /dev/null +++ b/src/vendor/golang.org/x/crypto/acme/autocert/cache.go @@ -0,0 +1,130 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package autocert + +import ( + "context" + "errors" + "io/ioutil" + "os" + "path/filepath" +) + +// ErrCacheMiss is returned when a certificate is not found in cache. +var ErrCacheMiss = errors.New("acme/autocert: certificate cache miss") + +// Cache is used by Manager to store and retrieve previously obtained certificates +// and other account data as opaque blobs. +// +// Cache implementations should not rely on the key naming pattern. Keys can +// include any printable ASCII characters, except the following: \/:*?"<>| +type Cache interface { + // Get returns a certificate data for the specified key. + // If there's no such key, Get returns ErrCacheMiss. + Get(ctx context.Context, key string) ([]byte, error) + + // Put stores the data in the cache under the specified key. + // Underlying implementations may use any data storage format, + // as long as the reverse operation, Get, results in the original data. + Put(ctx context.Context, key string, data []byte) error + + // Delete removes a certificate data from the cache under the specified key. + // If there's no such key in the cache, Delete returns nil. + Delete(ctx context.Context, key string) error +} + +// DirCache implements Cache using a directory on the local filesystem. +// If the directory does not exist, it will be created with 0700 permissions. +type DirCache string + +// Get reads a certificate data from the specified file name. +func (d DirCache) Get(ctx context.Context, name string) ([]byte, error) { + name = filepath.Join(string(d), name) + var ( + data []byte + err error + done = make(chan struct{}) + ) + go func() { + data, err = ioutil.ReadFile(name) + close(done) + }() + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-done: + } + if os.IsNotExist(err) { + return nil, ErrCacheMiss + } + return data, err +} + +// Put writes the certificate data to the specified file name. +// The file will be created with 0600 permissions. +func (d DirCache) Put(ctx context.Context, name string, data []byte) error { + if err := os.MkdirAll(string(d), 0700); err != nil { + return err + } + + done := make(chan struct{}) + var err error + go func() { + defer close(done) + var tmp string + if tmp, err = d.writeTempFile(name, data); err != nil { + return + } + select { + case <-ctx.Done(): + // Don't overwrite the file if the context was canceled. + default: + newName := filepath.Join(string(d), name) + err = os.Rename(tmp, newName) + } + }() + select { + case <-ctx.Done(): + return ctx.Err() + case <-done: + } + return err +} + +// Delete removes the specified file name. +func (d DirCache) Delete(ctx context.Context, name string) error { + name = filepath.Join(string(d), name) + var ( + err error + done = make(chan struct{}) + ) + go func() { + err = os.Remove(name) + close(done) + }() + select { + case <-ctx.Done(): + return ctx.Err() + case <-done: + } + if err != nil && !os.IsNotExist(err) { + return err + } + return nil +} + +// writeTempFile writes b to a temporary file, closes the file and returns its path. +func (d DirCache) writeTempFile(prefix string, b []byte) (string, error) { + // TempFile uses 0600 permissions + f, err := ioutil.TempFile(string(d), prefix) + if err != nil { + return "", err + } + if _, err := f.Write(b); err != nil { + f.Close() + return "", err + } + return f.Name(), f.Close() +} diff --git a/src/vendor/golang.org/x/crypto/acme/autocert/listener.go b/src/vendor/golang.org/x/crypto/acme/autocert/listener.go new file mode 100644 index 000000000..1e069818a --- /dev/null +++ b/src/vendor/golang.org/x/crypto/acme/autocert/listener.go @@ -0,0 +1,157 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package autocert + +import ( + "crypto/tls" + "log" + "net" + "os" + "path/filepath" + "runtime" + "time" +) + +// NewListener returns a net.Listener that listens on the standard TLS +// port (443) on all interfaces and returns *tls.Conn connections with +// LetsEncrypt certificates for the provided domain or domains. +// +// It enables one-line HTTPS servers: +// +// log.Fatal(http.Serve(autocert.NewListener("example.com"), handler)) +// +// NewListener is a convenience function for a common configuration. +// More complex or custom configurations can use the autocert.Manager +// type instead. +// +// Use of this function implies acceptance of the LetsEncrypt Terms of +// Service. If domains is not empty, the provided domains are passed +// to HostWhitelist. If domains is empty, the listener will do +// LetsEncrypt challenges for any requested domain, which is not +// recommended. +// +// Certificates are cached in a "golang-autocert" directory under an +// operating system-specific cache or temp directory. This may not +// be suitable for servers spanning multiple machines. +// +// The returned listener uses a *tls.Config that enables HTTP/2, and +// should only be used with servers that support HTTP/2. +// +// The returned Listener also enables TCP keep-alives on the accepted +// connections. The returned *tls.Conn are returned before their TLS +// handshake has completed. +func NewListener(domains ...string) net.Listener { + m := &Manager{ + Prompt: AcceptTOS, + } + if len(domains) > 0 { + m.HostPolicy = HostWhitelist(domains...) + } + dir := cacheDir() + if err := os.MkdirAll(dir, 0700); err != nil { + log.Printf("warning: autocert.NewListener not using a cache: %v", err) + } else { + m.Cache = DirCache(dir) + } + return m.Listener() +} + +// Listener listens on the standard TLS port (443) on all interfaces +// and returns a net.Listener returning *tls.Conn connections. +// +// The returned listener uses a *tls.Config that enables HTTP/2, and +// should only be used with servers that support HTTP/2. +// +// The returned Listener also enables TCP keep-alives on the accepted +// connections. The returned *tls.Conn are returned before their TLS +// handshake has completed. +// +// Unlike NewListener, it is the caller's responsibility to initialize +// the Manager m's Prompt, Cache, HostPolicy, and other desired options. +func (m *Manager) Listener() net.Listener { + ln := &listener{ + m: m, + conf: m.TLSConfig(), + } + ln.tcpListener, ln.tcpListenErr = net.Listen("tcp", ":443") + return ln +} + +type listener struct { + m *Manager + conf *tls.Config + + tcpListener net.Listener + tcpListenErr error +} + +func (ln *listener) Accept() (net.Conn, error) { + if ln.tcpListenErr != nil { + return nil, ln.tcpListenErr + } + conn, err := ln.tcpListener.Accept() + if err != nil { + return nil, err + } + tcpConn := conn.(*net.TCPConn) + + // Because Listener is a convenience function, help out with + // this too. This is not possible for the caller to set once + // we return a *tcp.Conn wrapping an inaccessible net.Conn. + // If callers don't want this, they can do things the manual + // way and tweak as needed. But this is what net/http does + // itself, so copy that. If net/http changes, we can change + // here too. + tcpConn.SetKeepAlive(true) + tcpConn.SetKeepAlivePeriod(3 * time.Minute) + + return tls.Server(tcpConn, ln.conf), nil +} + +func (ln *listener) Addr() net.Addr { + if ln.tcpListener != nil { + return ln.tcpListener.Addr() + } + // net.Listen failed. Return something non-nil in case callers + // call Addr before Accept: + return &net.TCPAddr{IP: net.IP{0, 0, 0, 0}, Port: 443} +} + +func (ln *listener) Close() error { + if ln.tcpListenErr != nil { + return ln.tcpListenErr + } + return ln.tcpListener.Close() +} + +func homeDir() string { + if runtime.GOOS == "windows" { + return os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") + } + if h := os.Getenv("HOME"); h != "" { + return h + } + return "/" +} + +func cacheDir() string { + const base = "golang-autocert" + switch runtime.GOOS { + case "darwin": + return filepath.Join(homeDir(), "Library", "Caches", base) + case "windows": + for _, ev := range []string{"APPDATA", "CSIDL_APPDATA", "TEMP", "TMP"} { + if v := os.Getenv(ev); v != "" { + return filepath.Join(v, base) + } + } + // Worst case: + return filepath.Join(homeDir(), base) + } + if xdg := os.Getenv("XDG_CACHE_HOME"); xdg != "" { + return filepath.Join(xdg, base) + } + return filepath.Join(homeDir(), ".cache", base) +} diff --git a/src/vendor/golang.org/x/crypto/acme/autocert/renewal.go b/src/vendor/golang.org/x/crypto/acme/autocert/renewal.go new file mode 100644 index 000000000..665f870dc --- /dev/null +++ b/src/vendor/golang.org/x/crypto/acme/autocert/renewal.go @@ -0,0 +1,141 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package autocert + +import ( + "context" + "crypto" + "sync" + "time" +) + +// renewJitter is the maximum deviation from Manager.RenewBefore. +const renewJitter = time.Hour + +// domainRenewal tracks the state used by the periodic timers +// renewing a single domain's cert. +type domainRenewal struct { + m *Manager + ck certKey + key crypto.Signer + + timerMu sync.Mutex + timer *time.Timer +} + +// start starts a cert renewal timer at the time +// defined by the certificate expiration time exp. +// +// If the timer is already started, calling start is a noop. +func (dr *domainRenewal) start(exp time.Time) { + dr.timerMu.Lock() + defer dr.timerMu.Unlock() + if dr.timer != nil { + return + } + dr.timer = time.AfterFunc(dr.next(exp), dr.renew) +} + +// stop stops the cert renewal timer. +// If the timer is already stopped, calling stop is a noop. +func (dr *domainRenewal) stop() { + dr.timerMu.Lock() + defer dr.timerMu.Unlock() + if dr.timer == nil { + return + } + dr.timer.Stop() + dr.timer = nil +} + +// renew is called periodically by a timer. +// The first renew call is kicked off by dr.start. +func (dr *domainRenewal) renew() { + dr.timerMu.Lock() + defer dr.timerMu.Unlock() + if dr.timer == nil { + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + // TODO: rotate dr.key at some point? + next, err := dr.do(ctx) + if err != nil { + next = renewJitter / 2 + next += time.Duration(pseudoRand.int63n(int64(next))) + } + dr.timer = time.AfterFunc(next, dr.renew) + testDidRenewLoop(next, err) +} + +// updateState locks and replaces the relevant Manager.state item with the given +// state. It additionally updates dr.key with the given state's key. +func (dr *domainRenewal) updateState(state *certState) { + dr.m.stateMu.Lock() + defer dr.m.stateMu.Unlock() + dr.key = state.key + dr.m.state[dr.ck] = state +} + +// do is similar to Manager.createCert but it doesn't lock a Manager.state item. +// Instead, it requests a new certificate independently and, upon success, +// replaces dr.m.state item with a new one and updates cache for the given domain. +// +// It may lock and update the Manager.state if the expiration date of the currently +// cached cert is far enough in the future. +// +// The returned value is a time interval after which the renewal should occur again. +func (dr *domainRenewal) do(ctx context.Context) (time.Duration, error) { + // a race is likely unavoidable in a distributed environment + // but we try nonetheless + if tlscert, err := dr.m.cacheGet(ctx, dr.ck); err == nil { + next := dr.next(tlscert.Leaf.NotAfter) + if next > dr.m.renewBefore()+renewJitter { + signer, ok := tlscert.PrivateKey.(crypto.Signer) + if ok { + state := &certState{ + key: signer, + cert: tlscert.Certificate, + leaf: tlscert.Leaf, + } + dr.updateState(state) + return next, nil + } + } + } + + der, leaf, err := dr.m.authorizedCert(ctx, dr.key, dr.ck) + if err != nil { + return 0, err + } + state := &certState{ + key: dr.key, + cert: der, + leaf: leaf, + } + tlscert, err := state.tlscert() + if err != nil { + return 0, err + } + if err := dr.m.cachePut(ctx, dr.ck, tlscert); err != nil { + return 0, err + } + dr.updateState(state) + return dr.next(leaf.NotAfter), nil +} + +func (dr *domainRenewal) next(expiry time.Time) time.Duration { + d := expiry.Sub(dr.m.now()) - dr.m.renewBefore() + // add a bit of randomness to renew deadline + n := pseudoRand.int63n(int64(renewJitter)) + d -= time.Duration(n) + if d < 0 { + return 0 + } + return d +} + +var testDidRenewLoop = func(next time.Duration, err error) {} diff --git a/src/vendor/golang.org/x/crypto/acme/http.go b/src/vendor/golang.org/x/crypto/acme/http.go new file mode 100644 index 000000000..a43ce6a5f --- /dev/null +++ b/src/vendor/golang.org/x/crypto/acme/http.go @@ -0,0 +1,281 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package acme + +import ( + "bytes" + "context" + "crypto" + "crypto/rand" + "encoding/json" + "fmt" + "io/ioutil" + "math/big" + "net/http" + "strconv" + "strings" + "time" +) + +// retryTimer encapsulates common logic for retrying unsuccessful requests. +// It is not safe for concurrent use. +type retryTimer struct { + // backoffFn provides backoff delay sequence for retries. + // See Client.RetryBackoff doc comment. + backoffFn func(n int, r *http.Request, res *http.Response) time.Duration + // n is the current retry attempt. + n int +} + +func (t *retryTimer) inc() { + t.n++ +} + +// backoff pauses the current goroutine as described in Client.RetryBackoff. +func (t *retryTimer) backoff(ctx context.Context, r *http.Request, res *http.Response) error { + d := t.backoffFn(t.n, r, res) + if d <= 0 { + return fmt.Errorf("acme: no more retries for %s; tried %d time(s)", r.URL, t.n) + } + wakeup := time.NewTimer(d) + defer wakeup.Stop() + select { + case <-ctx.Done(): + return ctx.Err() + case <-wakeup.C: + return nil + } +} + +func (c *Client) retryTimer() *retryTimer { + f := c.RetryBackoff + if f == nil { + f = defaultBackoff + } + return &retryTimer{backoffFn: f} +} + +// defaultBackoff provides default Client.RetryBackoff implementation +// using a truncated exponential backoff algorithm, +// as described in Client.RetryBackoff. +// +// The n argument is always bounded between 1 and 30. +// The returned value is always greater than 0. +func defaultBackoff(n int, r *http.Request, res *http.Response) time.Duration { + const max = 10 * time.Second + var jitter time.Duration + if x, err := rand.Int(rand.Reader, big.NewInt(1000)); err == nil { + // Set the minimum to 1ms to avoid a case where + // an invalid Retry-After value is parsed into 0 below, + // resulting in the 0 returned value which would unintentionally + // stop the retries. + jitter = (1 + time.Duration(x.Int64())) * time.Millisecond + } + if v, ok := res.Header["Retry-After"]; ok { + return retryAfter(v[0]) + jitter + } + + if n < 1 { + n = 1 + } + if n > 30 { + n = 30 + } + d := time.Duration(1< max { + return max + } + return d +} + +// retryAfter parses a Retry-After HTTP header value, +// trying to convert v into an int (seconds) or use http.ParseTime otherwise. +// It returns zero value if v cannot be parsed. +func retryAfter(v string) time.Duration { + if i, err := strconv.Atoi(v); err == nil { + return time.Duration(i) * time.Second + } + t, err := http.ParseTime(v) + if err != nil { + return 0 + } + return t.Sub(timeNow()) +} + +// resOkay is a function that reports whether the provided response is okay. +// It is expected to keep the response body unread. +type resOkay func(*http.Response) bool + +// wantStatus returns a function which reports whether the code +// matches the status code of a response. +func wantStatus(codes ...int) resOkay { + return func(res *http.Response) bool { + for _, code := range codes { + if code == res.StatusCode { + return true + } + } + return false + } +} + +// get issues an unsigned GET request to the specified URL. +// It returns a non-error value only when ok reports true. +// +// get retries unsuccessful attempts according to c.RetryBackoff +// until the context is done or a non-retriable error is received. +func (c *Client) get(ctx context.Context, url string, ok resOkay) (*http.Response, error) { + retry := c.retryTimer() + for { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + res, err := c.doNoRetry(ctx, req) + switch { + case err != nil: + return nil, err + case ok(res): + return res, nil + case isRetriable(res.StatusCode): + retry.inc() + resErr := responseError(res) + res.Body.Close() + // Ignore the error value from retry.backoff + // and return the one from last retry, as received from the CA. + if retry.backoff(ctx, req, res) != nil { + return nil, resErr + } + default: + defer res.Body.Close() + return nil, responseError(res) + } + } +} + +// post issues a signed POST request in JWS format using the provided key +// to the specified URL. +// It returns a non-error value only when ok reports true. +// +// post retries unsuccessful attempts according to c.RetryBackoff +// until the context is done or a non-retriable error is received. +// It uses postNoRetry to make individual requests. +func (c *Client) post(ctx context.Context, key crypto.Signer, url string, body interface{}, ok resOkay) (*http.Response, error) { + retry := c.retryTimer() + for { + res, req, err := c.postNoRetry(ctx, key, url, body) + if err != nil { + return nil, err + } + if ok(res) { + return res, nil + } + resErr := responseError(res) + res.Body.Close() + switch { + // Check for bad nonce before isRetriable because it may have been returned + // with an unretriable response code such as 400 Bad Request. + case isBadNonce(resErr): + // Consider any previously stored nonce values to be invalid. + c.clearNonces() + case !isRetriable(res.StatusCode): + return nil, resErr + } + retry.inc() + // Ignore the error value from retry.backoff + // and return the one from last retry, as received from the CA. + if err := retry.backoff(ctx, req, res); err != nil { + return nil, resErr + } + } +} + +// postNoRetry signs the body with the given key and POSTs it to the provided url. +// The body argument must be JSON-serializable. +// It is used by c.post to retry unsuccessful attempts. +func (c *Client) postNoRetry(ctx context.Context, key crypto.Signer, url string, body interface{}) (*http.Response, *http.Request, error) { + nonce, err := c.popNonce(ctx, url) + if err != nil { + return nil, nil, err + } + b, err := jwsEncodeJSON(body, key, nonce) + if err != nil { + return nil, nil, err + } + req, err := http.NewRequest("POST", url, bytes.NewReader(b)) + if err != nil { + return nil, nil, err + } + req.Header.Set("Content-Type", "application/jose+json") + res, err := c.doNoRetry(ctx, req) + if err != nil { + return nil, nil, err + } + c.addNonce(res.Header) + return res, req, nil +} + +// doNoRetry issues a request req, replacing its context (if any) with ctx. +func (c *Client) doNoRetry(ctx context.Context, req *http.Request) (*http.Response, error) { + res, err := c.httpClient().Do(req.WithContext(ctx)) + if err != nil { + select { + case <-ctx.Done(): + // Prefer the unadorned context error. + // (The acme package had tests assuming this, previously from ctxhttp's + // behavior, predating net/http supporting contexts natively) + // TODO(bradfitz): reconsider this in the future. But for now this + // requires no test updates. + return nil, ctx.Err() + default: + return nil, err + } + } + return res, nil +} + +func (c *Client) httpClient() *http.Client { + if c.HTTPClient != nil { + return c.HTTPClient + } + return http.DefaultClient +} + +// isBadNonce reports whether err is an ACME "badnonce" error. +func isBadNonce(err error) bool { + // According to the spec badNonce is urn:ietf:params:acme:error:badNonce. + // However, ACME servers in the wild return their versions of the error. + // See https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-5.4 + // and https://github.com/letsencrypt/boulder/blob/0e07eacb/docs/acme-divergences.md#section-66. + ae, ok := err.(*Error) + return ok && strings.HasSuffix(strings.ToLower(ae.ProblemType), ":badnonce") +} + +// isRetriable reports whether a request can be retried +// based on the response status code. +// +// Note that a "bad nonce" error is returned with a non-retriable 400 Bad Request code. +// Callers should parse the response and check with isBadNonce. +func isRetriable(code int) bool { + return code <= 399 || code >= 500 || code == http.StatusTooManyRequests +} + +// responseError creates an error of Error type from resp. +func responseError(resp *http.Response) error { + // don't care if ReadAll returns an error: + // json.Unmarshal will fail in that case anyway + b, _ := ioutil.ReadAll(resp.Body) + e := &wireError{Status: resp.StatusCode} + if err := json.Unmarshal(b, e); err != nil { + // this is not a regular error response: + // populate detail with anything we received, + // e.Status will already contain HTTP response code value + e.Detail = string(b) + if e.Detail == "" { + e.Detail = resp.Status + } + } + return e.error(resp.Header) +} diff --git a/src/vendor/golang.org/x/crypto/acme/jws.go b/src/vendor/golang.org/x/crypto/acme/jws.go new file mode 100644 index 000000000..1093b5039 --- /dev/null +++ b/src/vendor/golang.org/x/crypto/acme/jws.go @@ -0,0 +1,156 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package acme + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + _ "crypto/sha512" // need for EC keys + "encoding/base64" + "encoding/json" + "fmt" + "math/big" +) + +// jwsEncodeJSON signs claimset using provided key and a nonce. +// The result is serialized in JSON format. +// See https://tools.ietf.org/html/rfc7515#section-7. +func jwsEncodeJSON(claimset interface{}, key crypto.Signer, nonce string) ([]byte, error) { + jwk, err := jwkEncode(key.Public()) + if err != nil { + return nil, err + } + alg, sha := jwsHasher(key.Public()) + if alg == "" || !sha.Available() { + return nil, ErrUnsupportedKey + } + phead := fmt.Sprintf(`{"alg":%q,"jwk":%s,"nonce":%q}`, alg, jwk, nonce) + phead = base64.RawURLEncoding.EncodeToString([]byte(phead)) + cs, err := json.Marshal(claimset) + if err != nil { + return nil, err + } + payload := base64.RawURLEncoding.EncodeToString(cs) + hash := sha.New() + hash.Write([]byte(phead + "." + payload)) + sig, err := jwsSign(key, sha, hash.Sum(nil)) + if err != nil { + return nil, err + } + + enc := struct { + Protected string `json:"protected"` + Payload string `json:"payload"` + Sig string `json:"signature"` + }{ + Protected: phead, + Payload: payload, + Sig: base64.RawURLEncoding.EncodeToString(sig), + } + return json.Marshal(&enc) +} + +// jwkEncode encodes public part of an RSA or ECDSA key into a JWK. +// The result is also suitable for creating a JWK thumbprint. +// https://tools.ietf.org/html/rfc7517 +func jwkEncode(pub crypto.PublicKey) (string, error) { + switch pub := pub.(type) { + case *rsa.PublicKey: + // https://tools.ietf.org/html/rfc7518#section-6.3.1 + n := pub.N + e := big.NewInt(int64(pub.E)) + // Field order is important. + // See https://tools.ietf.org/html/rfc7638#section-3.3 for details. + return fmt.Sprintf(`{"e":"%s","kty":"RSA","n":"%s"}`, + base64.RawURLEncoding.EncodeToString(e.Bytes()), + base64.RawURLEncoding.EncodeToString(n.Bytes()), + ), nil + case *ecdsa.PublicKey: + // https://tools.ietf.org/html/rfc7518#section-6.2.1 + p := pub.Curve.Params() + n := p.BitSize / 8 + if p.BitSize%8 != 0 { + n++ + } + x := pub.X.Bytes() + if n > len(x) { + x = append(make([]byte, n-len(x)), x...) + } + y := pub.Y.Bytes() + if n > len(y) { + y = append(make([]byte, n-len(y)), y...) + } + // Field order is important. + // See https://tools.ietf.org/html/rfc7638#section-3.3 for details. + return fmt.Sprintf(`{"crv":"%s","kty":"EC","x":"%s","y":"%s"}`, + p.Name, + base64.RawURLEncoding.EncodeToString(x), + base64.RawURLEncoding.EncodeToString(y), + ), nil + } + return "", ErrUnsupportedKey +} + +// jwsSign signs the digest using the given key. +// The hash is unused for ECDSA keys. +// +// Note: non-stdlib crypto.Signer implementations are expected to return +// the signature in the format as specified in RFC7518. +// See https://tools.ietf.org/html/rfc7518 for more details. +func jwsSign(key crypto.Signer, hash crypto.Hash, digest []byte) ([]byte, error) { + if key, ok := key.(*ecdsa.PrivateKey); ok { + // The key.Sign method of ecdsa returns ASN1-encoded signature. + // So, we use the package Sign function instead + // to get R and S values directly and format the result accordingly. + r, s, err := ecdsa.Sign(rand.Reader, key, digest) + if err != nil { + return nil, err + } + rb, sb := r.Bytes(), s.Bytes() + size := key.Params().BitSize / 8 + if size%8 > 0 { + size++ + } + sig := make([]byte, size*2) + copy(sig[size-len(rb):], rb) + copy(sig[size*2-len(sb):], sb) + return sig, nil + } + return key.Sign(rand.Reader, digest, hash) +} + +// jwsHasher indicates suitable JWS algorithm name and a hash function +// to use for signing a digest with the provided key. +// It returns ("", 0) if the key is not supported. +func jwsHasher(pub crypto.PublicKey) (string, crypto.Hash) { + switch pub := pub.(type) { + case *rsa.PublicKey: + return "RS256", crypto.SHA256 + case *ecdsa.PublicKey: + switch pub.Params().Name { + case "P-256": + return "ES256", crypto.SHA256 + case "P-384": + return "ES384", crypto.SHA384 + case "P-521": + return "ES512", crypto.SHA512 + } + } + return "", 0 +} + +// JWKThumbprint creates a JWK thumbprint out of pub +// as specified in https://tools.ietf.org/html/rfc7638. +func JWKThumbprint(pub crypto.PublicKey) (string, error) { + jwk, err := jwkEncode(pub) + if err != nil { + return "", err + } + b := sha256.Sum256([]byte(jwk)) + return base64.RawURLEncoding.EncodeToString(b[:]), nil +} diff --git a/src/vendor/golang.org/x/crypto/acme/types.go b/src/vendor/golang.org/x/crypto/acme/types.go new file mode 100644 index 000000000..54792c065 --- /dev/null +++ b/src/vendor/golang.org/x/crypto/acme/types.go @@ -0,0 +1,329 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package acme + +import ( + "crypto" + "crypto/x509" + "errors" + "fmt" + "net/http" + "strings" + "time" +) + +// ACME server response statuses used to describe Authorization and Challenge states. +const ( + StatusUnknown = "unknown" + StatusPending = "pending" + StatusProcessing = "processing" + StatusValid = "valid" + StatusInvalid = "invalid" + StatusRevoked = "revoked" +) + +// CRLReasonCode identifies the reason for a certificate revocation. +type CRLReasonCode int + +// CRL reason codes as defined in RFC 5280. +const ( + CRLReasonUnspecified CRLReasonCode = 0 + CRLReasonKeyCompromise CRLReasonCode = 1 + CRLReasonCACompromise CRLReasonCode = 2 + CRLReasonAffiliationChanged CRLReasonCode = 3 + CRLReasonSuperseded CRLReasonCode = 4 + CRLReasonCessationOfOperation CRLReasonCode = 5 + CRLReasonCertificateHold CRLReasonCode = 6 + CRLReasonRemoveFromCRL CRLReasonCode = 8 + CRLReasonPrivilegeWithdrawn CRLReasonCode = 9 + CRLReasonAACompromise CRLReasonCode = 10 +) + +// ErrUnsupportedKey is returned when an unsupported key type is encountered. +var ErrUnsupportedKey = errors.New("acme: unknown key type; only RSA and ECDSA are supported") + +// Error is an ACME error, defined in Problem Details for HTTP APIs doc +// http://tools.ietf.org/html/draft-ietf-appsawg-http-problem. +type Error struct { + // StatusCode is The HTTP status code generated by the origin server. + StatusCode int + // ProblemType is a URI reference that identifies the problem type, + // typically in a "urn:acme:error:xxx" form. + ProblemType string + // Detail is a human-readable explanation specific to this occurrence of the problem. + Detail string + // Header is the original server error response headers. + // It may be nil. + Header http.Header +} + +func (e *Error) Error() string { + return fmt.Sprintf("%d %s: %s", e.StatusCode, e.ProblemType, e.Detail) +} + +// AuthorizationError indicates that an authorization for an identifier +// did not succeed. +// It contains all errors from Challenge items of the failed Authorization. +type AuthorizationError struct { + // URI uniquely identifies the failed Authorization. + URI string + + // Identifier is an AuthzID.Value of the failed Authorization. + Identifier string + + // Errors is a collection of non-nil error values of Challenge items + // of the failed Authorization. + Errors []error +} + +func (a *AuthorizationError) Error() string { + e := make([]string, len(a.Errors)) + for i, err := range a.Errors { + e[i] = err.Error() + } + return fmt.Sprintf("acme: authorization error for %s: %s", a.Identifier, strings.Join(e, "; ")) +} + +// RateLimit reports whether err represents a rate limit error and +// any Retry-After duration returned by the server. +// +// See the following for more details on rate limiting: +// https://tools.ietf.org/html/draft-ietf-acme-acme-05#section-5.6 +func RateLimit(err error) (time.Duration, bool) { + e, ok := err.(*Error) + if !ok { + return 0, false + } + // Some CA implementations may return incorrect values. + // Use case-insensitive comparison. + if !strings.HasSuffix(strings.ToLower(e.ProblemType), ":ratelimited") { + return 0, false + } + if e.Header == nil { + return 0, true + } + return retryAfter(e.Header.Get("Retry-After")), true +} + +// Account is a user account. It is associated with a private key. +type Account struct { + // URI is the account unique ID, which is also a URL used to retrieve + // account data from the CA. + URI string + + // Contact is a slice of contact info used during registration. + Contact []string + + // The terms user has agreed to. + // A value not matching CurrentTerms indicates that the user hasn't agreed + // to the actual Terms of Service of the CA. + AgreedTerms string + + // Actual terms of a CA. + CurrentTerms string + + // Authz is the authorization URL used to initiate a new authz flow. + Authz string + + // Authorizations is a URI from which a list of authorizations + // granted to this account can be fetched via a GET request. + Authorizations string + + // Certificates is a URI from which a list of certificates + // issued for this account can be fetched via a GET request. + Certificates string +} + +// Directory is ACME server discovery data. +type Directory struct { + // RegURL is an account endpoint URL, allowing for creating new + // and modifying existing accounts. + RegURL string + + // AuthzURL is used to initiate Identifier Authorization flow. + AuthzURL string + + // CertURL is a new certificate issuance endpoint URL. + CertURL string + + // RevokeURL is used to initiate a certificate revocation flow. + RevokeURL string + + // Term is a URI identifying the current terms of service. + Terms string + + // Website is an HTTP or HTTPS URL locating a website + // providing more information about the ACME server. + Website string + + // CAA consists of lowercase hostname elements, which the ACME server + // recognises as referring to itself for the purposes of CAA record validation + // as defined in RFC6844. + CAA []string +} + +// Challenge encodes a returned CA challenge. +// Its Error field may be non-nil if the challenge is part of an Authorization +// with StatusInvalid. +type Challenge struct { + // Type is the challenge type, e.g. "http-01", "tls-sni-02", "dns-01". + Type string + + // URI is where a challenge response can be posted to. + URI string + + // Token is a random value that uniquely identifies the challenge. + Token string + + // Status identifies the status of this challenge. + Status string + + // Error indicates the reason for an authorization failure + // when this challenge was used. + // The type of a non-nil value is *Error. + Error error +} + +// Authorization encodes an authorization response. +type Authorization struct { + // URI uniquely identifies a authorization. + URI string + + // Status identifies the status of an authorization. + Status string + + // Identifier is what the account is authorized to represent. + Identifier AuthzID + + // Challenges that the client needs to fulfill in order to prove possession + // of the identifier (for pending authorizations). + // For final authorizations, the challenges that were used. + Challenges []*Challenge + + // A collection of sets of challenges, each of which would be sufficient + // to prove possession of the identifier. + // Clients must complete a set of challenges that covers at least one set. + // Challenges are identified by their indices in the challenges array. + // If this field is empty, the client needs to complete all challenges. + Combinations [][]int +} + +// AuthzID is an identifier that an account is authorized to represent. +type AuthzID struct { + Type string // The type of identifier, e.g. "dns". + Value string // The identifier itself, e.g. "example.org". +} + +// wireAuthz is ACME JSON representation of Authorization objects. +type wireAuthz struct { + Status string + Challenges []wireChallenge + Combinations [][]int + Identifier struct { + Type string + Value string + } +} + +func (z *wireAuthz) authorization(uri string) *Authorization { + a := &Authorization{ + URI: uri, + Status: z.Status, + Identifier: AuthzID{Type: z.Identifier.Type, Value: z.Identifier.Value}, + Combinations: z.Combinations, // shallow copy + Challenges: make([]*Challenge, len(z.Challenges)), + } + for i, v := range z.Challenges { + a.Challenges[i] = v.challenge() + } + return a +} + +func (z *wireAuthz) error(uri string) *AuthorizationError { + err := &AuthorizationError{ + URI: uri, + Identifier: z.Identifier.Value, + } + for _, raw := range z.Challenges { + if raw.Error != nil { + err.Errors = append(err.Errors, raw.Error.error(nil)) + } + } + return err +} + +// wireChallenge is ACME JSON challenge representation. +type wireChallenge struct { + URI string `json:"uri"` + Type string + Token string + Status string + Error *wireError +} + +func (c *wireChallenge) challenge() *Challenge { + v := &Challenge{ + URI: c.URI, + Type: c.Type, + Token: c.Token, + Status: c.Status, + } + if v.Status == "" { + v.Status = StatusPending + } + if c.Error != nil { + v.Error = c.Error.error(nil) + } + return v +} + +// wireError is a subset of fields of the Problem Details object +// as described in https://tools.ietf.org/html/rfc7807#section-3.1. +type wireError struct { + Status int + Type string + Detail string +} + +func (e *wireError) error(h http.Header) *Error { + return &Error{ + StatusCode: e.Status, + ProblemType: e.Type, + Detail: e.Detail, + Header: h, + } +} + +// CertOption is an optional argument type for the TLS ChallengeCert methods for +// customizing a temporary certificate for TLS-based challenges. +type CertOption interface { + privateCertOpt() +} + +// WithKey creates an option holding a private/public key pair. +// The private part signs a certificate, and the public part represents the signee. +func WithKey(key crypto.Signer) CertOption { + return &certOptKey{key} +} + +type certOptKey struct { + key crypto.Signer +} + +func (*certOptKey) privateCertOpt() {} + +// WithTemplate creates an option for specifying a certificate template. +// See x509.CreateCertificate for template usage details. +// +// In TLS ChallengeCert methods, the template is also used as parent, +// resulting in a self-signed certificate. +// The DNSNames field of t is always overwritten for tls-sni challenge certs. +func WithTemplate(t *x509.Certificate) CertOption { + return (*certOptTemplate)(t) +} + +type certOptTemplate x509.Certificate + +func (*certOptTemplate) privateCertOpt() {} diff --git a/src/vendor/modules.txt b/src/vendor/modules.txt index 6e785665c..9ced0922e 100644 --- a/src/vendor/modules.txt +++ b/src/vendor/modules.txt @@ -27,7 +27,7 @@ github.com/aliyun/alibaba-cloud-sdk-go/sdk/utils github.com/aliyun/alibaba-cloud-sdk-go/services/cr # github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a github.com/asaskevich/govalidator -# github.com/astaxie/beego v1.9.0 +# github.com/astaxie/beego v1.12.0 github.com/astaxie/beego github.com/astaxie/beego/cache github.com/astaxie/beego/cache/redis @@ -241,6 +241,8 @@ github.com/pquerna/cachecontrol github.com/pquerna/cachecontrol/cacheobject # github.com/robfig/cron v1.0.0 github.com/robfig/cron +# github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 +github.com/shiena/ansicolor # github.com/sirupsen/logrus v1.4.1 github.com/sirupsen/logrus # github.com/spf13/pflag v1.0.3 @@ -274,6 +276,8 @@ go.mongodb.org/mongo-driver/bson/bsontype go.mongodb.org/mongo-driver/bson/primitive go.mongodb.org/mongo-driver/x/bsonx/bsoncore # golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56 +golang.org/x/crypto/acme +golang.org/x/crypto/acme/autocert golang.org/x/crypto/cast5 golang.org/x/crypto/ed25519 golang.org/x/crypto/ed25519/internal/edwards25519