From 133ed1837409296aed944ca7c18962ae9d80be70 Mon Sep 17 00:00:00 2001 From: Toby Allen Date: Sat, 24 Jun 2017 21:54:35 +0100 Subject: [PATCH] Create request_id directive #1590 (#1711) * Create request_id directive #1590 * Address Comments * Fix TestListenerAddrEqual * requestid: Add some tests * Address Comments by tobya * Address Comments --- caddyhttp/caddyhttp.go | 1 + caddyhttp/caddyhttp_test.go | 2 +- caddyhttp/httpserver/middleware.go | 3 + caddyhttp/httpserver/plugin.go | 1 + caddyhttp/httpserver/replacer.go | 3 + caddyhttp/requestid/requestid.go | 34 +++++ caddyhttp/requestid/requestid_test.go | 33 +++++ caddyhttp/requestid/setup.go | 27 ++++ caddyhttp/requestid/setup_test.go | 43 ++++++ vendor/github.com/nu7hatch/gouuid/COPYING | 19 +++ vendor/github.com/nu7hatch/gouuid/uuid.go | 173 ++++++++++++++++++++++ vendor/manifest | 8 + 12 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 caddyhttp/requestid/requestid.go create mode 100644 caddyhttp/requestid/requestid_test.go create mode 100644 caddyhttp/requestid/setup.go create mode 100644 caddyhttp/requestid/setup_test.go create mode 100644 vendor/github.com/nu7hatch/gouuid/COPYING create mode 100644 vendor/github.com/nu7hatch/gouuid/uuid.go diff --git a/caddyhttp/caddyhttp.go b/caddyhttp/caddyhttp.go index 429702a54..99215bdb7 100644 --- a/caddyhttp/caddyhttp.go +++ b/caddyhttp/caddyhttp.go @@ -24,6 +24,7 @@ import ( _ "github.com/mholt/caddy/caddyhttp/proxy" _ "github.com/mholt/caddy/caddyhttp/push" _ "github.com/mholt/caddy/caddyhttp/redirect" + _ "github.com/mholt/caddy/caddyhttp/requestid" _ "github.com/mholt/caddy/caddyhttp/rewrite" _ "github.com/mholt/caddy/caddyhttp/root" _ "github.com/mholt/caddy/caddyhttp/status" diff --git a/caddyhttp/caddyhttp_test.go b/caddyhttp/caddyhttp_test.go index f7417eae7..99ffdbefa 100644 --- a/caddyhttp/caddyhttp_test.go +++ b/caddyhttp/caddyhttp_test.go @@ -11,7 +11,7 @@ import ( // ensure that the standard plugins are in fact plugged in // and registered properly; this is a quick/naive way to do it. func TestStandardPlugins(t *testing.T) { - numStandardPlugins := 31 // importing caddyhttp plugs in this many plugins + numStandardPlugins := 32 // importing caddyhttp plugs in this many plugins s := caddy.DescribePlugins() if got, want := strings.Count(s, "\n"), numStandardPlugins+5; got != want { t.Errorf("Expected all standard plugins to be plugged in, got:\n%s", s) diff --git a/caddyhttp/httpserver/middleware.go b/caddyhttp/httpserver/middleware.go index 52bc8df66..48e45a149 100644 --- a/caddyhttp/httpserver/middleware.go +++ b/caddyhttp/httpserver/middleware.go @@ -205,4 +205,7 @@ const ( // MitmCtxKey is the key for the result of MITM detection MitmCtxKey caddy.CtxKey = "mitm" + + // RequestIDCtxKey is the key for the U4 UUID value + RequestIDCtxKey caddy.CtxKey = "request_id" ) diff --git a/caddyhttp/httpserver/plugin.go b/caddyhttp/httpserver/plugin.go index 0b49135c8..6757542f1 100644 --- a/caddyhttp/httpserver/plugin.go +++ b/caddyhttp/httpserver/plugin.go @@ -443,6 +443,7 @@ var directives = []string{ // services/utilities, or other directives that don't necessarily inject handlers "startup", "shutdown", + "requestid", "realip", // github.com/captncraig/caddy-realip "git", // github.com/abiosoft/caddy-git diff --git a/caddyhttp/httpserver/replacer.go b/caddyhttp/httpserver/replacer.go index 178c175d8..b8a0c496d 100644 --- a/caddyhttp/httpserver/replacer.go +++ b/caddyhttp/httpserver/replacer.go @@ -243,6 +243,9 @@ func (r *replacer) getSubstitution(key string) string { case "{path_escaped}": u, _ := r.request.Context().Value(OriginalURLCtxKey).(url.URL) return url.QueryEscape(u.Path) + case "{request_id}": + reqid, _ := r.request.Context().Value(RequestIDCtxKey).(string) + return reqid case "{rewrite_path}": return r.request.URL.Path case "{rewrite_path_escaped}": diff --git a/caddyhttp/requestid/requestid.go b/caddyhttp/requestid/requestid.go new file mode 100644 index 000000000..40b258d42 --- /dev/null +++ b/caddyhttp/requestid/requestid.go @@ -0,0 +1,34 @@ +package requestid + +import ( + "context" + "log" + "net/http" + + "github.com/mholt/caddy/caddyhttp/httpserver" + uuid "github.com/nu7hatch/gouuid" +) + +// Handler is a middleware handler +type Handler struct { + Next httpserver.Handler +} + +func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { + reqid := UUID() + c := context.WithValue(r.Context(), httpserver.RequestIDCtxKey, reqid) + r = r.WithContext(c) + + return h.Next.ServeHTTP(w, r) +} + +// UUID returns U4 UUID +func UUID() string { + u4, err := uuid.NewV4() + if err != nil { + log.Printf("[ERROR] generating request ID: %v", err) + return "" + } + + return u4.String() +} diff --git a/caddyhttp/requestid/requestid_test.go b/caddyhttp/requestid/requestid_test.go new file mode 100644 index 000000000..77014dfd4 --- /dev/null +++ b/caddyhttp/requestid/requestid_test.go @@ -0,0 +1,33 @@ +package requestid + +import ( + "context" + "net/http" + "testing" + + "github.com/mholt/caddy/caddyhttp/httpserver" +) + +func TestRequestID(t *testing.T) { + request, err := http.NewRequest("GET", "http://localhost/", nil) + if err != nil { + t.Fatal("Could not create HTTP request:", err) + } + + reqid := UUID() + + c := context.WithValue(request.Context(), httpserver.RequestIDCtxKey, reqid) + + request = request.WithContext(c) + + // See caddyhttp/replacer.go + value, _ := request.Context().Value(httpserver.RequestIDCtxKey).(string) + + if value == "" { + t.Fatal("Request ID should not be empty") + } + + if value != reqid { + t.Fatal("Request ID does not match") + } +} diff --git a/caddyhttp/requestid/setup.go b/caddyhttp/requestid/setup.go new file mode 100644 index 000000000..20abf3455 --- /dev/null +++ b/caddyhttp/requestid/setup.go @@ -0,0 +1,27 @@ +package requestid + +import ( + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" +) + +func init() { + caddy.RegisterPlugin("requestid", caddy.Plugin{ + ServerType: "http", + Action: setup, + }) +} + +func setup(c *caddy.Controller) error { + for c.Next() { + if c.NextArg() { + return c.ArgErr() //no arg expected. + } + } + + httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler { + return Handler{Next: next} + }) + + return nil +} diff --git a/caddyhttp/requestid/setup_test.go b/caddyhttp/requestid/setup_test.go new file mode 100644 index 000000000..17cbceee3 --- /dev/null +++ b/caddyhttp/requestid/setup_test.go @@ -0,0 +1,43 @@ +package requestid + +import ( + "testing" + + "github.com/mholt/caddy" + "github.com/mholt/caddy/caddyhttp/httpserver" +) + +func TestSetup(t *testing.T) { + c := caddy.NewTestController("http", `requestid`) + err := setup(c) + if err != nil { + t.Errorf("Expected no errors, got: %v", err) + } + mids := httpserver.GetConfig(c).Middleware() + if len(mids) == 0 { + t.Fatal("Expected middleware, got 0 instead") + } + + handler := mids[0](httpserver.EmptyNext) + myHandler, ok := handler.(Handler) + + if !ok { + t.Fatalf("Expected handler to be type Handler, got: %#v", handler) + } + + if !httpserver.SameNext(myHandler.Next, httpserver.EmptyNext) { + t.Error("'Next' field of handler was not set properly") + } +} + +func TestSetupWithArg(t *testing.T) { + c := caddy.NewTestController("http", `requestid abc`) + err := setup(c) + if err == nil { + t.Errorf("Expected an error, got: %v", err) + } + mids := httpserver.GetConfig(c).Middleware() + if len(mids) != 0 { + t.Fatal("Expected no middleware") + } +} diff --git a/vendor/github.com/nu7hatch/gouuid/COPYING b/vendor/github.com/nu7hatch/gouuid/COPYING new file mode 100644 index 000000000..d7849fd8f --- /dev/null +++ b/vendor/github.com/nu7hatch/gouuid/COPYING @@ -0,0 +1,19 @@ +Copyright (C) 2011 by Krzysztof Kowalik + +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. \ No newline at end of file diff --git a/vendor/github.com/nu7hatch/gouuid/uuid.go b/vendor/github.com/nu7hatch/gouuid/uuid.go new file mode 100644 index 000000000..ac9623b72 --- /dev/null +++ b/vendor/github.com/nu7hatch/gouuid/uuid.go @@ -0,0 +1,173 @@ +// This package provides immutable UUID structs and the functions +// NewV3, NewV4, NewV5 and Parse() for generating versions 3, 4 +// and 5 UUIDs as specified in RFC 4122. +// +// Copyright (C) 2011 by Krzysztof Kowalik +package uuid + +import ( + "crypto/md5" + "crypto/rand" + "crypto/sha1" + "encoding/hex" + "errors" + "fmt" + "hash" + "regexp" +) + +// The UUID reserved variants. +const ( + ReservedNCS byte = 0x80 + ReservedRFC4122 byte = 0x40 + ReservedMicrosoft byte = 0x20 + ReservedFuture byte = 0x00 +) + +// The following standard UUIDs are for use with NewV3() or NewV5(). +var ( + NamespaceDNS, _ = ParseHex("6ba7b810-9dad-11d1-80b4-00c04fd430c8") + NamespaceURL, _ = ParseHex("6ba7b811-9dad-11d1-80b4-00c04fd430c8") + NamespaceOID, _ = ParseHex("6ba7b812-9dad-11d1-80b4-00c04fd430c8") + NamespaceX500, _ = ParseHex("6ba7b814-9dad-11d1-80b4-00c04fd430c8") +) + +// Pattern used to parse hex string representation of the UUID. +// FIXME: do something to consider both brackets at one time, +// current one allows to parse string with only one opening +// or closing bracket. +const hexPattern = "^(urn\\:uuid\\:)?\\{?([a-z0-9]{8})-([a-z0-9]{4})-" + + "([1-5][a-z0-9]{3})-([a-z0-9]{4})-([a-z0-9]{12})\\}?$" + +var re = regexp.MustCompile(hexPattern) + +// A UUID representation compliant with specification in +// RFC 4122 document. +type UUID [16]byte + +// ParseHex creates a UUID object from given hex string +// representation. Function accepts UUID string in following +// formats: +// +// uuid.ParseHex("6ba7b814-9dad-11d1-80b4-00c04fd430c8") +// uuid.ParseHex("{6ba7b814-9dad-11d1-80b4-00c04fd430c8}") +// uuid.ParseHex("urn:uuid:6ba7b814-9dad-11d1-80b4-00c04fd430c8") +// +func ParseHex(s string) (u *UUID, err error) { + md := re.FindStringSubmatch(s) + if md == nil { + err = errors.New("Invalid UUID string") + return + } + hash := md[2] + md[3] + md[4] + md[5] + md[6] + b, err := hex.DecodeString(hash) + if err != nil { + return + } + u = new(UUID) + copy(u[:], b) + return +} + +// Parse creates a UUID object from given bytes slice. +func Parse(b []byte) (u *UUID, err error) { + if len(b) != 16 { + err = errors.New("Given slice is not valid UUID sequence") + return + } + u = new(UUID) + copy(u[:], b) + return +} + +// Generate a UUID based on the MD5 hash of a namespace identifier +// and a name. +func NewV3(ns *UUID, name []byte) (u *UUID, err error) { + if ns == nil { + err = errors.New("Invalid namespace UUID") + return + } + u = new(UUID) + // Set all bits to MD5 hash generated from namespace and name. + u.setBytesFromHash(md5.New(), ns[:], name) + u.setVariant(ReservedRFC4122) + u.setVersion(3) + return +} + +// Generate a random UUID. +func NewV4() (u *UUID, err error) { + u = new(UUID) + // Set all bits to randomly (or pseudo-randomly) chosen values. + _, err = rand.Read(u[:]) + if err != nil { + return + } + u.setVariant(ReservedRFC4122) + u.setVersion(4) + return +} + +// Generate a UUID based on the SHA-1 hash of a namespace identifier +// and a name. +func NewV5(ns *UUID, name []byte) (u *UUID, err error) { + u = new(UUID) + // Set all bits to truncated SHA1 hash generated from namespace + // and name. + u.setBytesFromHash(sha1.New(), ns[:], name) + u.setVariant(ReservedRFC4122) + u.setVersion(5) + return +} + +// Generate a MD5 hash of a namespace and a name, and copy it to the +// UUID slice. +func (u *UUID) setBytesFromHash(hash hash.Hash, ns, name []byte) { + hash.Write(ns[:]) + hash.Write(name) + copy(u[:], hash.Sum([]byte{})[:16]) +} + +// Set the two most significant bits (bits 6 and 7) of the +// clock_seq_hi_and_reserved to zero and one, respectively. +func (u *UUID) setVariant(v byte) { + switch v { + case ReservedNCS: + u[8] = (u[8] | ReservedNCS) & 0xBF + case ReservedRFC4122: + u[8] = (u[8] | ReservedRFC4122) & 0x7F + case ReservedMicrosoft: + u[8] = (u[8] | ReservedMicrosoft) & 0x3F + } +} + +// Variant returns the UUID Variant, which determines the internal +// layout of the UUID. This will be one of the constants: RESERVED_NCS, +// RFC_4122, RESERVED_MICROSOFT, RESERVED_FUTURE. +func (u *UUID) Variant() byte { + if u[8]&ReservedNCS == ReservedNCS { + return ReservedNCS + } else if u[8]&ReservedRFC4122 == ReservedRFC4122 { + return ReservedRFC4122 + } else if u[8]&ReservedMicrosoft == ReservedMicrosoft { + return ReservedMicrosoft + } + return ReservedFuture +} + +// Set the four most significant bits (bits 12 through 15) of the +// time_hi_and_version field to the 4-bit version number. +func (u *UUID) setVersion(v byte) { + u[6] = (u[6] & 0xF) | (v << 4) +} + +// Version returns a version number of the algorithm used to +// generate the UUID sequence. +func (u *UUID) Version() uint { + return uint(u[6] >> 4) +} + +// Returns unparsed version of the generated UUID sequence. +func (u *UUID) String() string { + return fmt.Sprintf("%x-%x-%x-%x-%x", u[0:4], u[4:6], u[6:8], u[8:10], u[10:]) +} diff --git a/vendor/manifest b/vendor/manifest index 438dd3d85..c5774ed33 100644 --- a/vendor/manifest +++ b/vendor/manifest @@ -165,6 +165,14 @@ "branch": "master", "notests": true }, + { + "importpath": "github.com/nu7hatch/gouuid", + "repository": "https://github.com/nu7hatch/gouuid", + "vcs": "git", + "revision": "179d4d0c4d8d407a32af483c2354df1d2c91e6c3", + "branch": "master", + "notests": true + }, { "importpath": "github.com/russross/blackfriday", "repository": "https://github.com/russross/blackfriday",