OSDN Git Service

Added net module.
authorgguoss <1536310027@qq.com>
Tue, 22 Aug 2017 02:16:43 +0000 (10:16 +0800)
committergguoss <1536310027@qq.com>
Tue, 22 Aug 2017 02:16:43 +0000 (10:16 +0800)
18 files changed:
Makefile
blockchain/reactor.go
glide.lock
net/http/gzip/gzip.go [new file with mode: 0644]
net/http/gzip/gzip_test.go [new file with mode: 0644]
net/http/httperror/httperror.go [new file with mode: 0644]
net/http/httperror/httperror_test.go [new file with mode: 0644]
net/http/httpjson/context.go [new file with mode: 0644]
net/http/httpjson/context_test.go [new file with mode: 0644]
net/http/httpjson/doc.go [new file with mode: 0644]
net/http/httpjson/handler.go [new file with mode: 0644]
net/http/httpjson/handler_test.go [new file with mode: 0644]
net/http/httpjson/io.go [new file with mode: 0644]
net/http/httpjson/io_test.go [new file with mode: 0644]
net/http/limit/limit.go [new file with mode: 0644]
net/http/reqid/reqid.go [new file with mode: 0644]
net/http/reqid/reqid_test.go [new file with mode: 0644]
net/http/static/static.go [new file with mode: 0644]

index 46cc8eb..5dff4f3 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -20,6 +20,7 @@ copy:
        cp -r vendor/github.com/golang/net vendor/golang.org/x/net
        cp -r vendor/github.com/golang/text vendor/golang.org/x/text
        cp -r vendor/github.com/golang/tools vendor/golang.org/x/tools
+       cp -r vendor/github.com/golang/time vendor/golang.org/x/time
 
 # dist builds binaries for all platforms and packages them for distribution
 dist:
index 782580a..78942bf 100644 (file)
@@ -5,6 +5,7 @@ import (
        "errors"
        "reflect"
     "time"
+       "net/http"
 
        wire "github.com/tendermint/go-wire"
        "github.com/bytom/p2p"
@@ -14,6 +15,10 @@ import (
        cmn "github.com/tendermint/tmlibs/common"
        "github.com/bytom/blockchain/txdb"
        "github.com/bytom/blockchain/account"
+       //"github.com/bytom/net/http/gzip"
+       //"github.com/bytom/net/http/httpjson"
+       //"github.com/bytom/net/http/limit"
+       //"github.com/bytom/net/http/static"
 )
 
 const (
@@ -53,6 +58,7 @@ type BlockchainReactor struct {
        store        *txdb.Store
        accounts         *account.Manager
        pool         *BlockPool
+       mux          *http.ServeMux
        fastSync     bool
        requestsCh   chan BlockRequest
        timeoutsCh   chan string
index 0c91938..583f908 100644 (file)
@@ -146,6 +146,7 @@ imports:
 - name: github.com/golang/net
 - name: github.com/golang/text
 - name: github.com/golang/tools
+- name: github.com/golang/time
 - name: golang.org/x/sys
   version: e62c3de784db939836898e5c19ffd41bece347da
   subpackages:
diff --git a/net/http/gzip/gzip.go b/net/http/gzip/gzip.go
new file mode 100644 (file)
index 0000000..7d84116
--- /dev/null
@@ -0,0 +1,61 @@
+package gzip
+
+import (
+       "bufio"
+       "compress/gzip"
+       "errors"
+       "io"
+       "net"
+       "net/http"
+       "strings"
+       "sync"
+)
+
+var pool = sync.Pool{
+       New: func() interface{} {
+               w, _ := gzip.NewWriterLevel(nil, gzip.BestSpeed) // #nosec
+               return w
+       },
+}
+
+func getWriter(w io.Writer) *gzip.Writer {
+       gz := pool.Get().(*gzip.Writer)
+       gz.Reset(w)
+       return gz
+}
+
+type Handler struct {
+       Handler http.Handler
+}
+
+func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+       w.Header().Add("Vary", "Accept-Encoding")
+       if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
+               h.Handler.ServeHTTP(w, r)
+               return
+       }
+       w.Header().Set("Content-Encoding", "gzip")
+       gz := getWriter(w)
+       w = &responseWriter{gz, w}
+       h.Handler.ServeHTTP(w, r)
+       gz.Close()
+       pool.Put(gz)
+}
+
+type responseWriter struct {
+       w                   io.Writer // w wraps only method Write
+       http.ResponseWriter           // embedded for the other methods
+}
+
+var _ http.ResponseWriter = (*responseWriter)(nil)
+var _ http.Hijacker = (*responseWriter)(nil)
+
+func (w *responseWriter) Write(p []byte) (int, error) { return w.w.Write(p) }
+
+func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
+       h, ok := w.ResponseWriter.(http.Hijacker)
+       if !ok {
+               return nil, nil, errors.New("not a hijacker")
+       }
+       return h.Hijack()
+}
diff --git a/net/http/gzip/gzip_test.go b/net/http/gzip/gzip_test.go
new file mode 100644 (file)
index 0000000..ba1c178
--- /dev/null
@@ -0,0 +1,93 @@
+package gzip
+
+import (
+       "io"
+       "net/http"
+       "net/http/httptest"
+       "testing"
+)
+
+var (
+       small  = []byte(`{"message":"ok"}`)
+       medium = []byte(`{"id":"961458e16018cb60f06b01d303ae0d8e2b3ff98698a1f80b5c6715969644f519","timestamp":"2016-10-04T19:13:23Z","block_id":"181c11b24c7dbdd5ce5e2b9da1b665878a80712dbfd1796613e66305da49ca7c","block_height":2,"position":0,"reference_data":{},"is_local":"yes","inputs":[{"action":"issue","asset_id":"1811eb7d8aebbfc39ea14a2da0ae840e9b447952d6e205756ea5c7ad028bcc97","asset_alias":"t","asset_definition":{},"asset_tags":{},"asset_is_local":"yes","amount":5,"issuance_program":"027b7d75766baa205fd08ca9e18b180c7da3ace70e890cba8c7014c7da5c9ed78e3d9e253cccec8f5151ad696c00c0","reference_data":{},"is_local":"yes"}],"outputs":[{"action":"control","purpose":"receive","position":0,"asset_id":"1811eb7d8aebbfc39ea14a2da0ae840e9b447952d6e205756ea5c7ad028bcc97","asset_alias":"t","asset_definition":{},"asset_tags":{},"asset_is_local":"yes","amount":5,"account_id":"acc0KNY9W8QG0802","account_alias":"t","account_tags":null,"control_program":"766baa20107c8767129a4ae5325371946e44f4ae76448452f722768048bd2d5cf12fc1595151ad696c00c0","reference_data":{},"is_local":"yes"}]}`)
+       large  = []byte(`{"items":[{"id":"0266cf2ed4ff3cb989341a9f9b2c3e7ffcdf2133ee84df9652834ca97a9bfe53","timestamp":"2016-10-04T20:31:02Z","block_id":"c77d0004600ce1de5ac1c815dba6fd3512b396292203e96b56c3e618a8c07113","block_height":8,"position":0,"reference_data":{},"is_local":"yes","inputs":[{"action":"spend","asset_id":"1811eb7d8aebbfc39ea14a2da0ae840e9b447952d6e205756ea5c7ad028bcc97","asset_alias":"t","asset_definition":{},"asset_tags":{},"asset_is_local":"yes","amount":20,"spent_output":{"position":1,"transaction_id":"092de8cc56abcd588919973c2eb5b5a56355e4d4d50910f5bcfa25ca4e2c0124"},"account_id":"acc0KP0F1K9G081A","account_alias":"foo","account_tags":null,"reference_data":{},"is_local":"yes"}],"outputs":[{"action":"control","purpose":"change","position":0,"asset_id":"1811eb7d8aebbfc39ea14a2da0ae840e9b447952d6e205756ea5c7ad028bcc97","asset_alias":"t","asset_definition":{},"asset_tags":{},"asset_is_local":"yes","amount":17,"account_id":"acc0KP0F1K9G081A","account_alias":"foo","account_tags":null,"control_program":"766baa2097abd5c16fab864da84605e5cc2ae96e946949c63470658f3b34a10e8594bc485151ad696c00c0","reference_data":{},"is_local":"yes"},{"action":"retire","position":1,"asset_id":"1811eb7d8aebbfc39ea14a2da0ae840e9b447952d6e205756ea5c7ad028bcc97","asset_alias":"t","asset_definition":{},"asset_tags":{},"asset_is_local":"yes","amount":3,"control_program":"6a","reference_data":{},"is_local":"no"}]},{"id":"712e79e954150750db4e245112429e18de7e67465858074ef79b5a83621d52f7","timestamp":"2016-10-04T20:30:37Z","block_id":"9a2e4a3f8a837935ef7c1d40e0032922fcdbf1e5e152222e7f2b9b5695170b03","block_height":7,"position":0,"reference_data":{},"is_local":"yes","inputs":[{"action":"issue","asset_id":"1811eb7d8aebbfc39ea14a2da0ae840e9b447952d6e205756ea5c7ad028bcc97","asset_alias":"t","asset_definition":{},"asset_tags":{},"asset_is_local":"yes","amount":20,"issuance_program":"027b7d75766baa205fd08ca9e18b180c7da3ace70e890cba8c7014c7da5c9ed78e3d9e253cccec8f5151ad696c00c0","reference_data":{},"is_local":"yes"}],"outputs":[{"action":"retire","position":0,"asset_id":"1811eb7d8aebbfc39ea14a2da0ae840e9b447952d6e205756ea5c7ad028bcc97","asset_alias":"t","asset_definition":{},"asset_tags":{},"asset_is_local":"yes","amount":20,"control_program":"6a","reference_data":{},"is_local":"no"}]},{"id":"092de8cc56abcd588919973c2eb5b5a56355e4d4d50910f5bcfa25ca4e2c0124","timestamp":"2016-10-04T20:30:13Z","block_id":"bf38fe464a50a24e90ecf01b75101c7063c94c7ac6e4ec9d349ced0033c6b233","block_height":6,"position":0,"reference_data":{},"is_local":"yes","inputs":[{"action":"spend","asset_id":"1811eb7d8aebbfc39ea14a2da0ae840e9b447952d6e205756ea5c7ad028bcc97","asset_alias":"t","asset_definition":{},"asset_tags":{},"asset_is_local":"yes","amount":55,"spent_output":{"position":0,"transaction_id":"8c851f25563a33b79a3f30139c88854c607979db437f71b31549c664f6995113"},"account_id":"acc0KNY9W8QG0802","account_alias":"t","account_tags":null,"reference_data":{},"is_local":"yes"}],"outputs":[{"action":"control","purpose":"change","position":0,"asset_id":"1811eb7d8aebbfc39ea14a2da0ae840e9b447952d6e205756ea5c7ad028bcc97","asset_alias":"t","asset_definition":{},"asset_tags":{},"asset_is_local":"yes","amount":35,"account_id":"acc0KNY9W8QG0802","account_alias":"t","account_tags":null,"control_program":"766baa20ae40f6e7509b6a86deab669f36b3a8bd43a4d6128904e97ebabecb81473084a65151ad696c00c0","reference_data":{},"is_local":"yes"},{"action":"control","purpose":"receive","position":1,"asset_id":"1811eb7d8aebbfc39ea14a2da0ae840e9b447952d6e205756ea5c7ad028bcc97","asset_alias":"t","asset_definition":{},"asset_tags":{},"asset_is_local":"yes","amount":20,"account_id":"acc0KP0F1K9G081A","account_alias":"foo","account_tags":null,"control_program":"766baa20a2abff2f62e5a9912bc9776c170f66219077adab278ca683b4583b7a9d6c83b85151ad696c00c0","reference_data":{},"is_local":"yes"}]},{"id":"4e6ad8970866d652f755c32fd04868ecd63d292021b3961ec4a87d9d8cd97ccc","timestamp":"2016-10-04T20:29:26Z","block_id":"033e802edc6d4b22c17679a0a1512dc04448b13dc2b2194c58ec11ac4c4c4cab","block_height":5,"position":0,"reference_data":{},"is_local":"yes","inputs":[{"action":"issue","asset_id":"1811eb7d8aebbfc39ea14a2da0ae840e9b447952d6e205756ea5c7ad028bcc97","asset_alias":"t","asset_definition":{},"asset_tags":{},"asset_is_local":"yes","amount":1,"issuance_program":"027b7d75766baa205fd08ca9e18b180c7da3ace70e890cba8c7014c7da5c9ed78e3d9e253cccec8f5151ad696c00c0","reference_data":{},"is_local":"yes"}],"outputs":[{"action":"control","purpose":"receive","position":0,"asset_id":"1811eb7d8aebbfc39ea14a2da0ae840e9b447952d6e205756ea5c7ad028bcc97","asset_alias":"t","asset_definition":{},"asset_tags":{},"asset_is_local":"yes","amount":1,"account_id":"acc0KP0F1K9G081A","account_alias":"foo","account_tags":null,"control_program":"766baa205dfbb62393e5e6d3b2a0772dd8fc1f865a0c774c44ee7de1bb40b18de5368bf55151ad696c00c0","reference_data":{},"is_local":"yes"}]},{"id":"8599e2feb681b84ae9e8233183c73b8a5bbf7564a9f7061b926f6b7040824608","timestamp":"2016-10-04T20:28:42Z","block_id":"cb6c384181883422e42f87637373593f7821df24723b89fb77f10fef69a73235","block_height":4,"position":0,"reference_data":{},"is_local":"yes","inputs":[{"action":"spend","asset_id":"1811eb7d8aebbfc39ea14a2da0ae840e9b447952d6e205756ea5c7ad028bcc97","asset_alias":"t","asset_definition":{},"asset_tags":{},"asset_is_local":"yes","amount":44,"spent_output":{"position":1,"transaction_id":"8c851f25563a33b79a3f30139c88854c607979db437f71b31549c664f6995113"},"account_id":"acc0KNY9W8QG0802","account_alias":"t","account_tags":null,"reference_data":{},"is_local":"yes"}],"outputs":[{"action":"control","purpose":"change","position":0,"asset_id":"1811eb7d8aebbfc39ea14a2da0ae840e9b447952d6e205756ea5c7ad028bcc97","asset_alias":"t","asset_definition":{},"asset_tags":{},"asset_is_local":"yes","amount":39,"account_id":"acc0KNY9W8QG0802","account_alias":"t","account_tags":null,"control_program":"766baa20d16227a885f913a958ff7e9df91118874133aba484175da7fb5e24b4bf6710315151ad696c00c0","reference_data":{},"is_local":"yes"},{"action":"control","purpose":"receive","position":1,"asset_id":"1811eb7d8aebbfc39ea14a2da0ae840e9b447952d6e205756ea5c7ad028bcc97","asset_alias":"t","asset_definition":{},"asset_tags":{},"asset_is_local":"yes","amount":5,"account_id":"acc0KP0F1K9G081A","account_alias":"foo","account_tags":null,"control_program":"766baa20153e9de41ba01abc10d5e7dbe5c3c222733c653cf8520a984802e7eaf18ba7855151ad696c00c0","reference_data":{},"is_local":"yes"}]},{"id":"8c851f25563a33b79a3f30139c88854c607979db437f71b31549c664f6995113","timestamp":"2016-10-04T20:23:27Z","block_id":"eb2593b3a13b386eab0dcc9f4c8aa5d03e01696d866e06c9048b7786127c163a","block_height":3,"position":0,"reference_data":{},"is_local":"yes","inputs":[{"action":"issue","asset_id":"1811eb7d8aebbfc39ea14a2da0ae840e9b447952d6e205756ea5c7ad028bcc97","asset_alias":"t","asset_definition":{},"asset_tags":{},"asset_is_local":"yes","amount":99,"issuance_program":"027b7d75766baa205fd08ca9e18b180c7da3ace70e890cba8c7014c7da5c9ed78e3d9e253cccec8f5151ad696c00c0","reference_data":{},"is_local":"yes"}],"outputs":[{"action":"control","purpose":"receive","position":0,"asset_id":"1811eb7d8aebbfc39ea14a2da0ae840e9b447952d6e205756ea5c7ad028bcc97","asset_alias":"t","asset_definition":{},"asset_tags":{},"asset_is_local":"yes","amount":55,"account_id":"acc0KNY9W8QG0802","account_alias":"t","account_tags":null,"control_program":"766baa209997e49055a4e9b020c3c2342a632b0977f8020778d3607acceacd5f0f8fc7fe5151ad696c00c0","reference_data":{},"is_local":"yes"},{"action":"control","purpose":"receive","position":1,"asset_id":"1811eb7d8aebbfc39ea14a2da0ae840e9b447952d6e205756ea5c7ad028bcc97","asset_alias":"t","asset_definition":{},"asset_tags":{},"asset_is_local":"yes","amount":44,"account_id":"acc0KNY9W8QG0802","account_alias":"t","account_tags":null,"control_program":"766baa20da29a78752723e4c873e1c46eafc0dfd22041fe2e818ed5584c9b3139fc5363a5151ad696c00c0","reference_data":{},"is_local":"yes"}]},{"id":"961458e16018cb60f06b01d303ae0d8e2b3ff98698a1f80b5c6715969644f519","timestamp":"2016-10-04T19:13:23Z","block_id":"181c11b24c7dbdd5ce5e2b9da1b665878a80712dbfd1796613e66305da49ca7c","block_height":2,"position":0,"reference_data":{},"is_local":"yes","inputs":[{"action":"issue","asset_id":"1811eb7d8aebbfc39ea14a2da0ae840e9b447952d6e205756ea5c7ad028bcc97","asset_alias":"t","asset_definition":{},"asset_tags":{},"asset_is_local":"yes","amount":5,"issuance_program":"027b7d75766baa205fd08ca9e18b180c7da3ace70e890cba8c7014c7da5c9ed78e3d9e253cccec8f5151ad696c00c0","reference_data":{},"is_local":"yes"}],"outputs":[{"action":"control","purpose":"receive","position":0,"asset_id":"1811eb7d8aebbfc39ea14a2da0ae840e9b447952d6e205756ea5c7ad028bcc97","asset_alias":"t","asset_definition":{},"asset_tags":{},"asset_is_local":"yes","amount":5,"account_id":"acc0KNY9W8QG0802","account_alias":"t","account_tags":null,"control_program":"766baa20107c8767129a4ae5325371946e44f4ae76448452f722768048bd2d5cf12fc1595151ad696c00c0","reference_data":{},"is_local":"yes"}]}],"next":{"page_size":0,"timeout":0,"after":"2:0-1","end_time":1475613062958,"type":""},"last_page":true}`)
+)
+
+type noOpWriter struct{ header http.Header }
+
+func (n noOpWriter) Header() http.Header {
+       return n.header
+}
+
+func (n noOpWriter) Write(d []byte) (int, error) {
+       return len(d), nil
+}
+
+func (n noOpWriter) WriteHeader(int) {}
+
+func BenchmarkGzipSmall(b *testing.B) {
+       r, _ := http.NewRequest("GET", "/foo", nil) // #nosec
+       r.Header.Set("accept-encoding", "gzip")
+       h := Handler{http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+               w.Write(small)
+       })}
+       w := noOpWriter{header: http.Header{}}
+
+       for i := 0; i < b.N; i++ {
+               h.ServeHTTP(&w, r)
+       }
+       b.SetBytes(int64(len(small)))
+}
+
+func BenchmarkGzipMedium(b *testing.B) {
+       r, _ := http.NewRequest("GET", "/foo", nil)
+       r.Header.Set("accept-encoding", "gzip")
+       h := Handler{http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+               w.Write(medium)
+       })}
+       w := noOpWriter{header: http.Header{}}
+
+       for i := 0; i < b.N; i++ {
+               h.ServeHTTP(&w, r)
+       }
+       b.SetBytes(int64(len(medium)))
+}
+
+func BenchmarkGzipLarge(b *testing.B) {
+       r, _ := http.NewRequest("GET", "/foo", nil)
+       r.Header.Set("accept-encoding", "gzip")
+       h := Handler{http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+               w.Write(large)
+       })}
+       w := noOpWriter{header: http.Header{}}
+
+       for i := 0; i < b.N; i++ {
+               h.ServeHTTP(&w, r)
+       }
+       b.SetBytes(int64(len(large)))
+}
+
+func TestGzip(t *testing.T) {
+       w := httptest.NewRecorder()
+       r, _ := http.NewRequest("GET", "/foo", nil)
+       r.Header.Set("accept-encoding", "gzip")
+       h := Handler{http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+               io.WriteString(w, "hello, world")
+       })}
+       h.ServeHTTP(w, r)
+       if s := w.HeaderMap.Get("content-encoding"); s != "gzip" {
+               t.Errorf(`w.HeaderMap.Get("content-encoding") = %s want gzip`, s)
+       }
+}
+
+func TestNoGzip(t *testing.T) {
+       w := httptest.NewRecorder()
+       r, _ := http.NewRequest("GET", "/foo", nil)
+       h := Handler{http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+               io.WriteString(w, "hello, world")
+       })}
+       h.ServeHTTP(w, r)
+       if w.HeaderMap.Get("content-encoding") == "gzip" {
+               t.Error("unexpected gzip")
+       }
+}
diff --git a/net/http/httperror/httperror.go b/net/http/httperror/httperror.go
new file mode 100644 (file)
index 0000000..8f09d15
--- /dev/null
@@ -0,0 +1,99 @@
+// Package httperror defines the format for HTTP error responses
+// from Chain services.
+package httperror
+
+import (
+       "context"
+       "net/http"
+
+       "github.com/bytom/errors"
+       "github.com/bytom/log"
+       "github.com/bytom/net/http/httpjson"
+       "github.com/bytom/net/http/reqid"
+)
+
+func init() {
+       log.SkipFunc("chain/net/http/httperror.Formatter.Log")
+       log.SkipFunc("chain/net/http/httperror.Formatter.Write")
+}
+
+// Info contains a set of error codes to send to the user.
+type Info struct {
+       HTTPStatus int    `json:"-"`
+       ChainCode  string `json:"code"`
+       Message    string `json:"message"`
+}
+
+// Response defines the error response for a Chain error.
+type Response struct {
+       Info
+       Detail    string                 `json:"detail,omitempty"`
+       Data      map[string]interface{} `json:"data,omitempty"`
+       Temporary bool                   `json:"temporary"`
+}
+
+// Formatter defines rules for mapping errors to the Chain error
+// response format.
+type Formatter struct {
+       Default     Info
+       IsTemporary func(info Info, err error) bool
+       Errors      map[error]Info
+}
+
+// Format builds an error Response body describing err by consulting
+// the f.Errors lookup table. If no entry is found, it returns f.Default.
+func (f Formatter) Format(err error) (body Response) {
+       root := errors.Root(err)
+       // Some types cannot be used as map keys, for example slices.
+       // If an error's underlying type is one of these, don't panic.
+       // Just treat it like any other missing entry.
+       defer func() {
+               if err := recover(); err != nil {
+                       body = Response{f.Default, "", nil, true}
+               }
+       }()
+       info, ok := f.Errors[root]
+       if !ok {
+               info = f.Default
+       }
+
+       body = Response{
+               Info:      info,
+               Detail:    errors.Detail(err),
+               Data:      errors.Data(err),
+               Temporary: f.IsTemporary(info, err),
+       }
+       return body
+}
+
+// Write writes a json encoded Response to the ResponseWriter.
+// It uses the status code associated with the error.
+//
+// Write may be used as an ErrorWriter in the httpjson package.
+func (f Formatter) Write(ctx context.Context, w http.ResponseWriter, err error) {
+       f.Log(ctx, err)
+       resp := f.Format(err)
+       httpjson.Write(ctx, w, resp.HTTPStatus, resp)
+}
+
+// Log writes a structured log entry to the chain/log logger with
+// information about the error and the HTTP response.
+func (f Formatter) Log(ctx context.Context, err error) {
+       var errorMessage string
+       if err != nil {
+               // strip the stack trace, if there is one
+               errorMessage = err.Error()
+       }
+
+       resp := f.Format(err)
+       keyvals := []interface{}{
+               "status", resp.HTTPStatus,
+               "chaincode", resp.ChainCode,
+               "path", reqid.PathFromContext(ctx),
+               log.KeyError, errorMessage,
+       }
+       if resp.HTTPStatus == 500 {
+               keyvals = append(keyvals, log.KeyStack, errors.Stack(err))
+       }
+       log.Printkv(ctx, keyvals...)
+}
diff --git a/net/http/httperror/httperror_test.go b/net/http/httperror/httperror_test.go
new file mode 100644 (file)
index 0000000..b74771e
--- /dev/null
@@ -0,0 +1,75 @@
+package httperror
+
+import (
+       "bytes"
+       "context"
+       "fmt"
+       "strings"
+       "testing"
+
+       "github.com/bytom/errors"
+       "github.com/bytom/log"
+)
+
+var (
+       errNotFound   = errors.New("not found")
+       testFormatter = Formatter{
+               Default:     Info{500, "CH000", "Internal server error"},
+               IsTemporary: func(Info, error) bool { return false },
+               Errors: map[error]Info{
+                       errNotFound: {400, "CH002", "Not found"},
+               },
+       }
+)
+
+// Dummy error type, to test that Format
+// doesn't panic when it's used as a map key.
+type sliceError []int
+
+func (err sliceError) Error() string { return "slice error" }
+
+func TestInfo(t *testing.T) {
+       cases := []struct {
+               err  error
+               want int
+       }{
+               {nil, 500},
+               {context.Canceled, 500},
+               {errNotFound, 400},
+               {errors.Wrap(errNotFound, "foo"), 400},
+               {sliceError{}, 500},
+               {fmt.Errorf("an error!"), 500},
+       }
+
+       for _, test := range cases {
+               resp := testFormatter.Format(test.err)
+               got := resp.HTTPStatus
+               if got != test.want {
+                       t.Errorf("errInfo(%#v) = %d want %d", test.err, got, test.want)
+               }
+       }
+}
+
+func TestLogSkip(t *testing.T) {
+       var buf bytes.Buffer
+       log.SetOutput(&buf)
+
+       formatter := Formatter{
+               Default:     Info{500, "CH000", "Internal server error"},
+               IsTemporary: func(Info, error) bool { return false },
+               Errors:      map[error]Info{},
+       }
+       formatter.Log(context.Background(), errors.New("an unmapped error"))
+
+       logStr := string(buf.Bytes())
+       if len(logStr) == 0 {
+               t.Error("expected error to be logged")
+       }
+       if strings.Contains(logStr, "at=httperror.go") {
+               t.Errorf("expected httperror stack frames to be skipped but got:\n%s", logStr)
+       }
+       if !strings.Contains(logStr, "status=500") {
+               t.Errorf("expected status code of default error info but got:\n%s", logStr)
+       }
+       t.Log(logStr)
+}
diff --git a/net/http/httpjson/context.go b/net/http/httpjson/context.go
new file mode 100644 (file)
index 0000000..ed3e6ef
--- /dev/null
@@ -0,0 +1,42 @@
+package httpjson
+
+import (
+       "context"
+       "net/http"
+)
+
+// key is an unexported type for keys defined in this package.
+// This prevents collisions with keys defined in other packages.
+type key int
+
+// Keys for HTTP objects in Contexts.
+// They are unexported; clients use Request and ResponseWriter
+// instead of using these keys directly.
+const (
+       reqKey key = iota
+       respKey
+)
+
+// Request returns the HTTP request stored in ctx.
+// If there is none, it panics.
+// The context given to a handler function
+// registered in this package is guaranteed to have
+// a request.
+func Request(ctx context.Context) *http.Request {
+       return ctx.Value(reqKey).(*http.Request)
+}
+
+// ResponseWriter returns the HTTP response writer stored in ctx.
+// If there is none, it panics.
+// The context given to a handler function
+// registered in this package is guaranteed to have
+// a response writer.
+func ResponseWriter(ctx context.Context) http.ResponseWriter {
+       return ctx.Value(respKey).(http.ResponseWriter)
+}
+
+// WithRequest returns a context with an HTTP request stored in it.
+// It is useful for testing.
+func WithRequest(ctx context.Context, req *http.Request) context.Context {
+       return context.WithValue(ctx, reqKey, req)
+}
diff --git a/net/http/httpjson/context_test.go b/net/http/httpjson/context_test.go
new file mode 100644 (file)
index 0000000..a959c7f
--- /dev/null
@@ -0,0 +1,32 @@
+package httpjson
+
+import (
+       "context"
+       "net/http"
+       "net/http/httptest"
+       "testing"
+)
+
+func TestContext(t *testing.T) {
+       wantHead := "bar"
+       wantRespHead := "baz"
+       f := func(ctx context.Context) {
+               if g := Request(ctx).Header.Get("Test-Key"); g != wantHead {
+                       t.Errorf("header = %q want %q", g, wantHead)
+               }
+               ResponseWriter(ctx).Header().Set("Test-Resp-Key", wantRespHead)
+       }
+
+       h, err := Handler(f, nil)
+       if err != nil {
+               t.Fatalf("err = %v", err)
+       }
+
+       resp := httptest.NewRecorder()
+       req, _ := http.NewRequest("GET", "/", nil)
+       req.Header.Set("Test-Key", wantHead)
+       h.ServeHTTP(resp, req)
+       if g := resp.Header().Get("Test-Resp-Key"); g != wantRespHead {
+               t.Errorf("header = %q want %q", g, wantRespHead)
+       }
+}
diff --git a/net/http/httpjson/doc.go b/net/http/httpjson/doc.go
new file mode 100644 (file)
index 0000000..9a0f2da
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+
+Package httpjson creates HTTP handlers to map request
+and response formats onto Go function signatures.
+The request body is decoded as a JSON text
+into an arbitrary value and the function's return value
+is encoded as a JSON text for the response body.
+The function's signature determines the types of the
+input and output values.
+
+For example, the handler for a function with signature
+
+  func(struct{ FavColor, Birthday string })
+
+would read the JSON request body into a variable
+of type struct{ FavColor, Birthday string }, then call
+the function.
+
+The allowed elements of a function signature are:
+
+  parameters:
+  Context (optional)
+  request body (optional)
+
+  return values:
+  response body (optional)
+  error (optional)
+
+All elements are optional.
+Thus, the smallest possible function signature is
+
+  func()
+
+If the function returns a non-nil error,
+the handler will call the error function provided
+in its constructor.
+Otherwise, the handler will write the return value
+as JSON text to the response body.
+If the return type is omitted, the handler will send
+a default response value.
+
+*/
+package httpjson
diff --git a/net/http/httpjson/handler.go b/net/http/httpjson/handler.go
new file mode 100644 (file)
index 0000000..3a6d245
--- /dev/null
@@ -0,0 +1,119 @@
+package httpjson
+
+import (
+       "context"
+       "encoding/json"
+       "errors"
+       "net/http"
+       "reflect"
+)
+
+// ErrorWriter is responsible for writing the provided error value
+// to the response.
+type ErrorWriter func(context.Context, http.ResponseWriter, error)
+
+// DefaultResponse will be sent as the response body
+// when the handler function signature
+// has no return value.
+var DefaultResponse = json.RawMessage(`{"message":"ok"}`)
+
+// handler is an http.Handler that calls a function for each request.
+// It uses the signature of the function to decide how to interpret
+type handler struct {
+       fv      reflect.Value
+       inType  reflect.Type
+       hasCtx  bool
+       errFunc ErrorWriter
+}
+
+// Handler returns an HTTP handler for function f.
+// See the package doc for details on allowed signatures for f.
+// If f returns a non-nil error, the handler will call errFunc.
+func Handler(f interface{}, errFunc ErrorWriter) (http.Handler, error) {
+       fv := reflect.ValueOf(f)
+       hasCtx, inType, err := funcInputType(fv)
+       if err != nil {
+               return nil, err
+       }
+
+       h := &handler{fv, inType, hasCtx, errFunc}
+       return h, nil
+}
+
+func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+       var a []reflect.Value
+       if h.hasCtx {
+               ctx := req.Context()
+               ctx = context.WithValue(ctx, reqKey, req)
+               ctx = context.WithValue(ctx, respKey, w)
+               a = append(a, reflect.ValueOf(ctx))
+       }
+       if h.inType != nil {
+               inPtr := reflect.New(h.inType)
+               err := Read(req.Context(), req.Body, inPtr.Interface())
+               if err != nil {
+                       h.errFunc(req.Context(), w, err)
+                       return
+               }
+               a = append(a, inPtr.Elem())
+       }
+       rv := h.fv.Call(a)
+
+       var (
+               res interface{}
+               err error
+       )
+       switch n := len(rv); {
+       case n == 0:
+               res = &DefaultResponse
+       case n == 1 && !h.fv.Type().Out(0).Implements(errorType):
+               res = rv[0].Interface()
+       case n == 1 && h.fv.Type().Out(0).Implements(errorType):
+               // out param is of type error; its value can still be nil
+               res = &DefaultResponse
+               err, _ = rv[0].Interface().(error)
+       case n == 2:
+               res = rv[0].Interface()
+               err, _ = rv[1].Interface().(error)
+       }
+       if err != nil {
+               h.errFunc(req.Context(), w, err)
+               return
+       }
+
+       Write(req.Context(), w, 200, res)
+}
+
+var (
+       errorType   = reflect.TypeOf((*error)(nil)).Elem()
+       contextType = reflect.TypeOf((*context.Context)(nil)).Elem()
+)
+
+func funcInputType(fv reflect.Value) (hasCtx bool, t reflect.Type, err error) {
+       ft := fv.Type()
+       if ft.Kind() != reflect.Func || ft.IsVariadic() {
+               return false, nil, errors.New("need nonvariadic func in " + ft.String())
+       }
+
+       off := 0 // or 1 with context
+       hasCtx = ft.NumIn() >= 1 && ft.In(0).Implements(contextType)
+       if hasCtx {
+               off = 1
+       }
+
+       if ft.NumIn() > off+1 {
+               return false, nil, errors.New("too many params in " + ft.String())
+       }
+
+       if ft.NumIn() == off+1 {
+               t = ft.In(ft.NumIn() - 1)
+       }
+
+       if n := ft.NumOut(); n == 2 && !ft.Out(1).Implements(errorType) {
+               return false, nil, errors.New("second return value must be error in " + ft.String())
+       } else if n > 2 {
+               return false, nil, errors.New("need at most two return values in " + ft.String())
+       }
+
+       return hasCtx, t, nil
+}
diff --git a/net/http/httpjson/handler_test.go b/net/http/httpjson/handler_test.go
new file mode 100644 (file)
index 0000000..e7b0063
--- /dev/null
@@ -0,0 +1,144 @@
+package httpjson
+
+import (
+       "context"
+       "net/http"
+       "net/http/httptest"
+       "reflect"
+       "strings"
+       "testing"
+       "testing/iotest"
+
+       "github.com/bytom/errors"
+)
+
+func TestHandler(t *testing.T) {
+       errX := errors.New("x")
+
+       cases := []struct {
+               rawQuery string
+               input    string
+               output   string
+               f        interface{}
+               wantErr  error
+       }{
+               {"", ``, `{"message":"ok"}`, func() {}, nil},
+               {"", ``, `1`, func() int { return 1 }, nil},
+               {"", ``, `{"message":"ok"}`, func() error { return nil }, nil},
+               {"", ``, ``, func() error { return errX }, errX},
+               {"", ``, `1`, func() (int, error) { return 1, nil }, nil},
+               {"", ``, ``, func() (int, error) { return 0, errX }, errX},
+               {"", `1`, `1`, func(i int) int { return i }, nil},
+               {"", `1`, `1`, func(i *int) int { return *i }, nil},
+               {"", `"foo"`, `"foo"`, func(s string) string { return s }, nil},
+               {"", `{"x":1}`, `1`, func(x struct{ X int }) int { return x.X }, nil},
+               {"", `{"x":1}`, `1`, func(x *struct{ X int }) int { return x.X }, nil},
+               {"", ``, `1`, func(ctx context.Context) int { return ctx.Value("k").(int) }, nil},
+       }
+
+       for _, test := range cases {
+               var gotErr error
+               errFunc := func(ctx context.Context, w http.ResponseWriter, err error) {
+                       gotErr = err
+               }
+               h, err := Handler(test.f, errFunc)
+               if err != nil {
+                       t.Errorf("Handler(%v) got err %v", test.f, err)
+                       continue
+               }
+
+               resp := httptest.NewRecorder()
+               req, _ := http.NewRequest("GET", "/", strings.NewReader(test.input))
+               req.URL.RawQuery = test.rawQuery
+               ctx := context.WithValue(context.Background(), "k", 1)
+               h.ServeHTTP(resp, req.WithContext(ctx))
+               if resp.Code != 200 {
+                       t.Errorf("%T response code = %d want 200", test.f, resp.Code)
+               }
+               got := strings.TrimSpace(resp.Body.String())
+               if got != test.output {
+                       t.Errorf("%T response body = %#q want %#q", test.f, got, test.output)
+               }
+               if gotErr != test.wantErr {
+                       t.Errorf("%T err = %v want %v", test.f, gotErr, test.wantErr)
+               }
+       }
+}
+
+func TestReadErr(t *testing.T) {
+       var gotErr error
+       errFunc := func(ctx context.Context, w http.ResponseWriter, err error) {
+               gotErr = errors.Root(err)
+       }
+       h, _ := Handler(func(int) {}, errFunc)
+
+       resp := httptest.NewRecorder()
+       body := iotest.OneByteReader(iotest.TimeoutReader(strings.NewReader("123456")))
+       req, _ := http.NewRequest("GET", "/", body)
+       h.ServeHTTP(resp, req)
+       if got := resp.Body.Len(); got != 0 {
+               t.Errorf("len(response) = %d want 0", got)
+       }
+       wantErr := ErrBadRequest
+       if gotErr != wantErr {
+               t.Errorf("err = %v want %v", gotErr, wantErr)
+       }
+}
+
+func TestFuncInputTypeError(t *testing.T) {
+       cases := []interface{}{
+               0,
+               "foo",
+               func() (int, int) { return 0, 0 },
+               func(string, int) {},
+               func() (int, int, error) { return 0, 0, nil },
+       }
+
+       for _, testf := range cases {
+               _, _, err := funcInputType(reflect.ValueOf(testf))
+               if err == nil {
+                       t.Errorf("funcInputType(%T) want error", testf)
+               }
+
+               _, err = Handler(testf, nil)
+               if err == nil {
+                       t.Errorf("funcInputType(%T) want error", testf)
+               }
+       }
+}
+
+var (
+       intType    = reflect.TypeOf(0)
+       intpType   = reflect.TypeOf((*int)(nil))
+       stringType = reflect.TypeOf("")
+)
+
+func TestFuncInputTypeOk(t *testing.T) {
+       cases := []struct {
+               f       interface{}
+               wantCtx bool
+               wantT   reflect.Type
+       }{
+               {func() {}, false, nil},
+               {func() int { return 0 }, false, nil},
+               {func() error { return nil }, false, nil},
+               {func() (int, error) { return 0, nil }, false, nil},
+               {func(int) {}, false, intType},
+               {func(*int) {}, false, intpType},
+               {func(context.Context) {}, true, nil},
+               {func(string) {}, false, stringType}, // req body is string
+       }
+
+       for _, test := range cases {
+               gotCtx, gotT, err := funcInputType(reflect.ValueOf(test.f))
+               if err != nil {
+                       t.Errorf("funcInputType(%T) got error: %v", test.f, err)
+               }
+               if gotCtx != test.wantCtx {
+                       t.Errorf("funcInputType(%T) context = %v want %v", test.f, gotCtx, test.wantCtx)
+               }
+               if gotT != test.wantT {
+                       t.Errorf("funcInputType(%T) = %v want %v", test.f, gotT, test.wantT)
+               }
+       }
+}
diff --git a/net/http/httpjson/io.go b/net/http/httpjson/io.go
new file mode 100644 (file)
index 0000000..7cc39ac
--- /dev/null
@@ -0,0 +1,57 @@
+package httpjson
+
+import (
+       "context"
+       "encoding/json"
+       "io"
+       "net/http"
+       "reflect"
+
+       "github.com/bytom/errors"
+       "github.com/bytom/log"
+)
+
+// ErrBadRequest indicates the user supplied malformed JSON input,
+// possibly including a datatype that doesn't match what we expected.
+var ErrBadRequest = errors.New("httpjson: bad request")
+
+// Read decodes a single JSON text from r into v.
+// The only error it returns is ErrBadRequest
+// (wrapped with the original error message as context).
+func Read(ctx context.Context, r io.Reader, v interface{}) error {
+       dec := json.NewDecoder(r)
+       dec.UseNumber()
+       err := dec.Decode(v)
+       if err != nil {
+               detail := errors.Detail(err)
+               if detail == "" {
+                       detail = "check request parameters for missing and/or incorrect values"
+               }
+               return errors.WithDetail(ErrBadRequest, err.Error()+": "+detail)
+       }
+       return err
+}
+
+// Write sets the Content-Type header field to indicate
+// JSON data, writes the header using status,
+// then writes v to w.
+// It logs any error encountered during the write.
+func Write(ctx context.Context, w http.ResponseWriter, status int, v interface{}) {
+       w.Header().Set("Content-Type", "application/json; charset=utf-8")
+       w.WriteHeader(status)
+
+       err := json.NewEncoder(w).Encode(Array(v))
+       if err != nil {
+               log.Error(ctx, err)
+       }
+}
+
+// Array returns an empty JSON array if v is a nil slice,
+// so that it renders as "[]" rather than "null".
+// Otherwise, it returns v.
+func Array(v interface{}) interface{} {
+       if rv := reflect.ValueOf(v); rv.Kind() == reflect.Slice && rv.IsNil() {
+               v = []struct{}{}
+       }
+       return v
+}
diff --git a/net/http/httpjson/io_test.go b/net/http/httpjson/io_test.go
new file mode 100644 (file)
index 0000000..3219893
--- /dev/null
@@ -0,0 +1,58 @@
+package httpjson
+
+import (
+       "bytes"
+       "context"
+       "errors"
+       "net/http/httptest"
+       "os"
+       "strings"
+       "testing"
+
+       "github.com/bytom/log"
+)
+
+func TestWriteArray(t *testing.T) {
+       examples := []struct {
+               in   []int
+               want string
+       }{
+               {nil, "[]"},
+               {[]int{}, "[]"},
+               {make([]int, 0), "[]"},
+       }
+
+       for _, ex := range examples {
+               rec := httptest.NewRecorder()
+               Write(context.Background(), rec, 200, ex.in)
+               got := strings.TrimSpace(rec.Body.String())
+               if got != ex.want {
+                       t.Errorf("Write(%v) = %v want %v", ex.in, got, ex.want)
+               }
+       }
+}
+
+func TestWriteErr(t *testing.T) {
+       var buf bytes.Buffer
+       log.SetOutput(&buf)
+       defer log.SetOutput(os.Stderr)
+
+       want := "test-error"
+
+       ctx := context.Background()
+       resp := &errResponse{httptest.NewRecorder(), errors.New(want)}
+       Write(ctx, resp, 200, "ok")
+       got := buf.String()
+       if !strings.Contains(got, want) {
+               t.Errorf("log = %v; should contain %q", got, want)
+       }
+}
+
+type errResponse struct {
+       *httptest.ResponseRecorder
+       err error
+}
+
+func (r *errResponse) Write([]byte) (int, error) {
+       return 0, r.err
+}
diff --git a/net/http/limit/limit.go b/net/http/limit/limit.go
new file mode 100644 (file)
index 0000000..00de20d
--- /dev/null
@@ -0,0 +1,74 @@
+package limit
+
+import (
+       "net/http"
+       "sync"
+
+       "golang.org/x/time/rate"
+)
+
+type BucketLimiter struct {
+       freq  rate.Limit
+       burst int
+
+       bucketMu sync.Mutex // protects the following
+       buckets  map[string]*rate.Limiter
+}
+
+func NewBucketLimiter(freq, burst int) *BucketLimiter {
+       return &BucketLimiter{
+               freq:    rate.Limit(freq),
+               burst:   burst,
+               buckets: make(map[string]*rate.Limiter),
+       }
+}
+
+func (b *BucketLimiter) Allow(id string) bool {
+       return b.bucket(id).Allow()
+}
+
+func (b *BucketLimiter) bucket(id string) *rate.Limiter {
+       b.bucketMu.Lock()
+       bucket, ok := b.buckets[id]
+       if !ok {
+               bucket = rate.NewLimiter(b.freq, b.burst)
+               b.buckets[id] = bucket
+       }
+       b.bucketMu.Unlock()
+       return bucket
+}
+
+type handler struct {
+       next    http.Handler
+       limited http.Handler
+       f       func(*http.Request) string
+
+       limiter *BucketLimiter
+}
+
+func Handler(next, limited http.Handler, freq, burst int, f func(*http.Request) string) http.Handler {
+       return &handler{
+               next:    next,
+               limited: limited,
+               f:       f,
+               limiter: NewBucketLimiter(freq, burst),
+       }
+}
+
+func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+       id := h.f(r)
+       if !h.limiter.Allow(id) {
+               h.limited.ServeHTTP(w, r)
+               return
+       }
+       h.next.ServeHTTP(w, r)
+}
+
+func RemoteAddrID(r *http.Request) string {
+       return r.RemoteAddr
+}
+
+func AuthUserID(r *http.Request) string {
+       user, _, _ := r.BasicAuth()
+       return user
+}
diff --git a/net/http/reqid/reqid.go b/net/http/reqid/reqid.go
new file mode 100644 (file)
index 0000000..cf655c2
--- /dev/null
@@ -0,0 +1,121 @@
+// Package reqid creates request IDs and stores them in Contexts.
+package reqid
+
+import (
+       "context"
+       "crypto/rand"
+       "encoding/hex"
+       "net/http"
+
+       "github.com/bytom/log"
+)
+
+// key is an unexported type for keys defined in this package.
+// This prevents collisions with keys defined in other packages.
+type key int
+
+const (
+       // reqIDKey is the key for request IDs in Contexts.  It is
+       // unexported; clients use NewContext and FromContext
+       // instead of using this key directly.
+       reqIDKey key = iota
+       // subReqIDKey is the key for sub-request IDs in Contexts.  It is
+       // unexported; clients use NewSubContext and FromSubContext
+       // instead of using this key directly.
+       subReqIDKey
+       // coreIDKey is the key for Chain-Core-ID request header field values.
+       // It is only for statistics; don't use it for authorization.
+       coreIDKey
+       // pathKey is the key for the request path being handled.
+       pathKey
+)
+
+// New generates a random request ID.
+func New() string {
+       // Given n IDs of length b bits, the probability that there will be a collision is bounded by
+       // the number of pairs of IDs multiplied by the probability that any pair might collide:
+       // p ≤ n(n - 1)/2 * 1/(2^b)
+       //
+       // We assume an upper bound of 1000 req/sec, which means that in a week there will be
+       // n = 1000 * 604800 requests. If l = 10, b = 8*10, then p ≤ 1.512e-7, which is a suitably
+       // low probability.
+       l := 10
+       b := make([]byte, l)
+       _, err := rand.Read(b)
+       if err != nil {
+               log.Printf(context.Background(), "error making reqID")
+       }
+       return hex.EncodeToString(b)
+}
+
+// NewContext returns a new Context that carries reqid.
+// It also adds a log prefix to print the request ID using
+// package chain/log.
+func NewContext(ctx context.Context, reqid string) context.Context {
+       ctx = context.WithValue(ctx, reqIDKey, reqid)
+       ctx = log.AddPrefixkv(ctx, "reqid", reqid)
+       return ctx
+}
+
+// FromContext returns the request ID stored in ctx,
+// if any.
+func FromContext(ctx context.Context) string {
+       reqID, _ := ctx.Value(reqIDKey).(string)
+       return reqID
+}
+
+// CoreIDFromContext returns the Chain-Core-ID stored in ctx,
+// or the empty string.
+func CoreIDFromContext(ctx context.Context) string {
+       id, _ := ctx.Value(coreIDKey).(string)
+       return id
+}
+
+// PathFromContext returns the HTTP path stored in ctx,
+// or the empty string.
+func PathFromContext(ctx context.Context) string {
+       path, _ := ctx.Value(pathKey).(string)
+       return path
+}
+
+// NewSubContext returns a new Context that carries subreqid
+// It also adds a log prefix to print the sub-request ID using
+// package chain/log.
+func NewSubContext(ctx context.Context, reqid string) context.Context {
+       ctx = context.WithValue(ctx, subReqIDKey, reqid)
+       ctx = log.AddPrefixkv(ctx, "subreqid", reqid)
+       return ctx
+}
+
+// FromSubContext returns the sub-request ID stored in ctx,
+// if any.
+func FromSubContext(ctx context.Context) string {
+       subReqID, _ := ctx.Value(subReqIDKey).(string)
+       return subReqID
+}
+
+func Handler(handler http.Handler) http.Handler {
+       return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+               ctx := req.Context()
+               // TODO(kr): take half of request ID from the client
+               id := New()
+               ctx = NewContext(ctx, id)
+               ctx = context.WithValue(ctx, pathKey, req.URL.Path)
+               if coreID := req.Header.Get("Chain-Core-ID"); coreID != "" {
+                       ctx = context.WithValue(ctx, coreIDKey, coreID)
+                       ctx = log.AddPrefixkv(ctx, "coreid", coreID)
+               }
+
+               defer func() {
+                       if err := recover(); err != nil {
+                               log.Printkv(ctx,
+                                       "message", "panic",
+                                       "remote-addr", req.RemoteAddr,
+                                       "error", err,
+                               )
+                       }
+               }()
+               w.Header().Add("Chain-Request-Id", id)
+               handler.ServeHTTP(w, req.WithContext(ctx))
+       })
+}
diff --git a/net/http/reqid/reqid_test.go b/net/http/reqid/reqid_test.go
new file mode 100644 (file)
index 0000000..7be59fd
--- /dev/null
@@ -0,0 +1,25 @@
+package reqid
+
+import (
+       "bytes"
+       "context"
+       "os"
+       "strings"
+       "testing"
+
+       "github.com/bytom/log"
+)
+
+func TestPrintkvRequestID(t *testing.T) {
+       buf := new(bytes.Buffer)
+       log.SetOutput(buf)
+       defer log.SetOutput(os.Stdout)
+
+       log.Printkv(NewContext(context.Background(), "example-request-id"))
+
+       got := buf.String()
+       want := "reqid=example-request-id"
+       if !strings.Contains(got, want) {
+               t.Errorf("Result did not contain string:\ngot:  %s\nwant: %s", got, want)
+       }
+}
diff --git a/net/http/static/static.go b/net/http/static/static.go
new file mode 100644 (file)
index 0000000..5577e55
--- /dev/null
@@ -0,0 +1,46 @@
+// Package static provides a handler for serving static assets from an in-memory
+// map.
+package static
+
+import (
+       "net/http"
+       "strings"
+       "time"
+)
+
+// use start time as a conservative bound for last-modified
+var lastMod = time.Now()
+
+type Handler struct {
+       Assets map[string]string
+
+       // Index is the name of an entry in Assets that should be used if the request
+       // path is empty (equivalent to requesting "/"). This is analogous to index
+       // documents commonly used in webservers. If Index is empty, it will be
+       // ignored.
+       Index string
+
+       // Default is the name of an entry in Assets that should be used if the
+       // the requested path does not exist in Assets. This is useful for
+       // delivering a common document (usually a frontend application script) that
+       // handles URL-based state on the client side. If Default is empty, it will be
+       // ignored.
+       Default string
+}
+
+func (h Handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
+       output, ok := h.Assets[r.URL.Path]
+       if !ok && r.URL.Path == "" && h.Index != "" {
+               output = h.Assets[h.Index]
+       } else if !ok && h.Default != "" {
+               output = h.Assets[h.Default]
+       } else if !ok {
+               http.NotFound(rw, r)
+               return
+       }
+
+       // Some autogenerated documentation uses frames, e.g. Javadoc
+       rw.Header().Set("X-Frame-Options", "SAMEORIGIN")
+
+       http.ServeContent(rw, r, r.URL.Path, lastMod, strings.NewReader(output))
+}