├── .github ├── release.yml └── workflows │ └── ci.yaml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── go.mod ├── go.sum ├── memhttp.go ├── memhttp_test.go ├── memhttptest ├── example_test.go └── memhttptest.go ├── option.go └── testcert.go /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | authors: 6 | - dependabot 7 | categories: 8 | - title: Enhancements 9 | labels: 10 | - enhancement 11 | - title: Bugfixes 12 | labels: 13 | - bug 14 | - title: Other changes 15 | labels: 16 | - "*" 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: [main] 5 | tags: ['v*'] 6 | pull_request: 7 | branches: [main] 8 | schedule: 9 | - cron: '15 22 * * *' 10 | workflow_dispatch: {} # support manual runs 11 | permissions: 12 | contents: read 13 | jobs: 14 | ci: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | go-version: [stable, oldstable] 19 | steps: 20 | - name: Checkout Code 21 | uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 1 24 | - name: Install Go 25 | uses: actions/setup-go@v4 26 | with: 27 | go-version: ${{ matrix.go-version }} 28 | - name: Test 29 | run: make test 30 | - name: Lint 31 | # Often, lint & gofmt guidelines depend on the Go version. To prevent 32 | # conflicting guidance, run only on the most recent supported version. 33 | if: matrix.go-version == 'stable' 34 | run: make lint 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.out 2 | .tmp/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Akshay Shah 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # See https://tech.davis-hansson.com/p/make/ 2 | SHELL := bash 3 | .DELETE_ON_ERROR: 4 | .SHELLFLAGS := -eu -o pipefail -c 5 | .DEFAULT_GOAL := all 6 | MAKEFLAGS += --warn-undefined-variables 7 | MAKEFLAGS += --no-builtin-rules 8 | MAKEFLAGS += --no-print-directory 9 | GO ?= go 10 | BIN := .tmp/bin 11 | 12 | .PHONY: help 13 | help: ## Describe useful make targets 14 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "%-30s %s\n", $$1, $$2}' 15 | 16 | .PHONY: all 17 | all: ## Build, test, and lint (default) 18 | $(MAKE) test 19 | $(MAKE) lint 20 | 21 | .PHONY: test 22 | test: build ## Run unit tests 23 | $(GO) test -vet=off -race -cover ./... 24 | 25 | .PHONY: build 26 | build: ## Build all packages 27 | $(GO) build ./... 28 | 29 | .PHONY: lint 30 | lint: $(BIN)/gofmt $(BIN)/staticcheck ## Lint Go 31 | test -z "$$($(BIN)/gofmt -s -l . | tee /dev/stderr)" 32 | $(GO) vet ./... 33 | $(BIN)/staticcheck ./... 34 | 35 | .PHONY: lintfix 36 | lintfix: $(BIN)/gofmt ## Automatically fix some lint errors 37 | $(BIN)/gofmt -s -w . 38 | 39 | .PHONY: upgrade 40 | upgrade: ## Upgrade dependencies 41 | go get -u -t ./... && go mod tidy -v 42 | 43 | .PHONY: clean 44 | clean: ## Remove intermediate artifacts 45 | rm -rf .tmp 46 | 47 | $(BIN)/gofmt: 48 | @mkdir -p $(@D) 49 | $(GO) build -o $(@) cmd/gofmt 50 | 51 | $(BIN)/staticcheck: 52 | @mkdir -p $(@D) 53 | GOBIN=$(abspath $(@D)) $(GO) install honnef.co/go/tools/cmd/staticcheck@latest 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | memhttp 2 | ======= 3 | 4 | [![Build](https://github.com/akshayjshah/memhttp/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/akshayjshah/memhttp/actions/workflows/ci.yaml) 5 | [![Report Card](https://goreportcard.com/badge/go.akshayshah.org/memhttp)](https://goreportcard.com/report/go.akshayshah.org/memhttp) 6 | [![GoDoc](https://pkg.go.dev/badge/go.akshayshah.org/memhttp.svg)](https://pkg.go.dev/go.akshayshah.org/memhttp) 7 | 8 | 9 | `memhttp` provides a full `net/http` server and client that communicate over 10 | in-memory pipes rather than the network. This is often useful in tests, where 11 | you want to avoid localhost networking but don't want to stub out all the 12 | complexity of HTTP. 13 | 14 | Occasionally, it's also useful in production code: if you're planning to split a 15 | monolithic application into microservices, you can first use `memhttp` to 16 | simulate the split. This allows you to rewrite local function calls as HTTP 17 | calls (complete with serialization, compression, and middleware) while 18 | retaining the ability to quickly change service boundaries. 19 | 20 | In particular, `memhttp` pairs well with [`connect-go`][connect-go] RPC 21 | servers. 22 | 23 | ## Installation 24 | 25 | ``` 26 | go get go.akshayshah.org/memhttp 27 | ``` 28 | 29 | ## Usage 30 | 31 | In-memory HTTP is most common in tests, so most users will be best served by 32 | the `memhttptest` subpackage: 33 | 34 | ```go 35 | func TestServer(t *testing.T) { 36 | hello := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 37 | io.WriteString(w, "Hello, world!") 38 | }) 39 | // The server starts automatically, and it shuts down gracefully when the 40 | // test ends. Startup and shutdown errors fail the test. 41 | // 42 | // By default, servers and clients use TLS and support HTTP/2. 43 | srv := memhttptest.New(t, hello) 44 | res, err := srv.Client().Get(srv.URL()) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | if res.StatusCode != http.StatusOK { 49 | t.Error(res.Status) 50 | } 51 | } 52 | ``` 53 | 54 | ## Status: Unstable 55 | 56 | This module is unstable, with a stable release expected before the end of 2023. 57 | It supports the [two most recent major releases][go-support-policy] of Go. 58 | 59 | Within those parameters, `memhttp` follows semantic versioning. 60 | 61 | ## Legal 62 | 63 | Offered under the [MIT license][license]. 64 | 65 | [go-support-policy]: https://golang.org/doc/devel/release#policy 66 | [license]: https://github.com/akshayjshah/memhttp/blob/main/LICENSE 67 | [connect-go]: https://github.com/bufbuild/connect-go 68 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go.akshayshah.org/memhttp 2 | 3 | go 1.19 4 | 5 | require go.akshayshah.org/attest v1.0.2 6 | 7 | require github.com/google/go-cmp v0.5.9 // indirect 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 2 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 3 | go.akshayshah.org/attest v1.0.2 h1:qOv9PXCG2mwnph3g0I3yZj0rLAwLyUITs8nhxP+wS44= 4 | go.akshayshah.org/attest v1.0.2/go.mod h1:PnWzcW5j9dkyGwTlBmUsYpPnHG0AUPrs1RQ+HrldWO0= 5 | -------------------------------------------------------------------------------- /memhttp.go: -------------------------------------------------------------------------------- 1 | // Package memhttp provides an in-memory HTTP server and client. For 2 | // testing-specific adapters, see the memhttptest subpackage. 3 | package memhttp 4 | 5 | import ( 6 | "context" 7 | "crypto/tls" 8 | "crypto/x509" 9 | "errors" 10 | "fmt" 11 | "net" 12 | "net/http" 13 | "sync" 14 | "time" 15 | ) 16 | 17 | // Server is a net/http server that uses in-memory pipes instead of TCP. By 18 | // default, it has TLS enabled and supports HTTP/2. It otherwise uses the same 19 | // configuration as the zero value of [http.Server]. 20 | type Server struct { 21 | server *http.Server 22 | listener *memoryListener 23 | certificate *x509.Certificate // for client 24 | url string 25 | disableHTTP2 bool 26 | serveErr chan error 27 | cleanupContext func() (context.Context, context.CancelFunc) 28 | } 29 | 30 | // New constructs and starts a Server. 31 | func New(handler http.Handler, opts ...Option) (*Server, error) { 32 | var cfg config 33 | WithCleanupTimeout(5 * time.Second).apply(&cfg) 34 | for _, opt := range opts { 35 | opt.apply(&cfg) 36 | } 37 | mlis := &memoryListener{ 38 | conns: make(chan net.Conn), 39 | closed: make(chan struct{}), 40 | } 41 | var lis net.Listener = mlis 42 | server := &http.Server{Handler: handler} 43 | 44 | var clientCert *x509.Certificate 45 | if !cfg.DisableTLS { 46 | srvCert, err := tls.X509KeyPair(_cert, _key) 47 | if err != nil { 48 | return nil, fmt.Errorf("create x509 key pair: %v", err) 49 | } 50 | protos := []string{"h2"} 51 | if cfg.DisableHTTP2 { 52 | protos = []string{"http/1.1"} 53 | } 54 | server.TLSConfig = &tls.Config{ 55 | NextProtos: protos, 56 | Certificates: []tls.Certificate{srvCert}, 57 | } 58 | clientCert, err = x509.ParseCertificate(server.TLSConfig.Certificates[0].Certificate[0]) 59 | if err != nil { 60 | return nil, fmt.Errorf("parse x509 certificate: %v", err) 61 | } 62 | lis = tls.NewListener(mlis, server.TLSConfig) 63 | } 64 | 65 | serveErr := make(chan error, 1) 66 | go func() { 67 | serveErr <- server.Serve(lis) 68 | }() 69 | 70 | scheme := "https://" 71 | if cfg.DisableTLS { 72 | scheme = "http://" 73 | } 74 | return &Server{ 75 | server: server, 76 | listener: mlis, 77 | certificate: clientCert, 78 | url: scheme + mlis.Addr().String(), 79 | disableHTTP2: cfg.DisableHTTP2, 80 | serveErr: serveErr, 81 | cleanupContext: cfg.CleanupContext, 82 | }, nil 83 | } 84 | 85 | // Transport returns an [http.Transport] configured to use in-memory pipes 86 | // rather than TCP, disable automatic compression, trust the server's TLS 87 | // certificate (if any), and use HTTP/2 (if the server supports it). 88 | // 89 | // Callers may reconfigure the returned Transport without affecting other 90 | // transports or clients. 91 | func (s *Server) Transport() *http.Transport { 92 | transport := &http.Transport{ 93 | DialContext: s.listener.DialContext, 94 | DisableCompression: true, 95 | } 96 | if s.certificate != nil { 97 | pool := x509.NewCertPool() 98 | pool.AddCert(s.certificate) 99 | transport.TLSClientConfig = &tls.Config{RootCAs: pool} 100 | transport.ForceAttemptHTTP2 = !s.disableHTTP2 101 | } 102 | return transport 103 | } 104 | 105 | // Client returns an [http.Client] configured to use in-memory pipes rather 106 | // than TCP, disable automatic compression, trust the server's TLS certificate 107 | // (if any), and use HTTP/2 (if the server supports it). 108 | // 109 | // Callers may reconfigure the returned client without affecting other clients. 110 | func (s *Server) Client() *http.Client { 111 | return &http.Client{Transport: s.Transport()} 112 | } 113 | 114 | // URL returns the server's URL. 115 | func (s *Server) URL() string { 116 | return s.url 117 | } 118 | 119 | // Close immediately shuts down the server. To shut down the server without 120 | // interrupting in-flight requests, use Shutdown. 121 | func (s *Server) Close() error { 122 | if err := s.server.Close(); err != nil { 123 | return err 124 | } 125 | return s.listenErr() 126 | } 127 | 128 | // Shutdown gracefully shuts down the server, without interrupting any active 129 | // connections. See [http.Server.Shutdown] for details. 130 | func (s *Server) Shutdown(ctx context.Context) error { 131 | if err := s.server.Shutdown(ctx); err != nil { 132 | return err 133 | } 134 | return s.listenErr() 135 | } 136 | 137 | // Cleanup calls Shutdown with a five second timeout. To customize the timeout, 138 | // use WithCleanupTimeout. 139 | // 140 | // Cleanup is primarily intended for use in tests. If you find yourself using 141 | // it, you may want to use the memhttptest package instead. 142 | func (s *Server) Cleanup() error { 143 | ctx, cancel := s.cleanupContext() 144 | defer cancel() 145 | return s.Shutdown(ctx) 146 | } 147 | 148 | // RegisterOnShutdown registers a function to call on Shutdown. It's often used 149 | // to cleanly shut down connections that have been hijacked. See 150 | // [http.Server.RegisterOnShutdown] for details. 151 | func (s *Server) RegisterOnShutdown(f func()) { 152 | s.server.RegisterOnShutdown(f) 153 | } 154 | 155 | func (s *Server) listenErr() error { 156 | if err := <-s.serveErr; err != nil && !errors.Is(err, http.ErrServerClosed) { 157 | return err 158 | } 159 | return nil 160 | } 161 | 162 | type memoryListener struct { 163 | conns chan net.Conn 164 | once sync.Once 165 | closed chan struct{} 166 | } 167 | 168 | // Accept implements net.Listener. 169 | func (l *memoryListener) Accept() (net.Conn, error) { 170 | select { 171 | case conn := <-l.conns: 172 | return conn, nil 173 | case <-l.closed: 174 | return nil, errors.New("listener closed") 175 | } 176 | } 177 | 178 | // Close implements net.Listener. 179 | func (l *memoryListener) Close() error { 180 | l.once.Do(func() { 181 | close(l.closed) 182 | }) 183 | return nil 184 | } 185 | 186 | // Addr implements net.Listener. 187 | func (l *memoryListener) Addr() net.Addr { 188 | return &memoryAddr{} 189 | } 190 | 191 | // DialContext is the type expected by http.Transport.DialContext. 192 | func (l *memoryListener) DialContext(ctx context.Context, network, addr string) (net.Conn, error) { 193 | select { 194 | case <-l.closed: 195 | return nil, errors.New("listener closed") 196 | default: 197 | } 198 | server, client := net.Pipe() 199 | l.conns <- server 200 | return client, nil 201 | } 202 | 203 | type memoryAddr struct{} 204 | 205 | // Network implements net.Addr. 206 | func (*memoryAddr) Network() string { return "memory" } 207 | 208 | // String implements io.Stringer, returning a value that matches the 209 | // certificates used by net/http/httptest. 210 | func (*memoryAddr) String() string { return "example.com" } 211 | -------------------------------------------------------------------------------- /memhttp_test.go: -------------------------------------------------------------------------------- 1 | package memhttp_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "strings" 9 | "sync" 10 | "testing" 11 | "time" 12 | 13 | "go.akshayshah.org/attest" 14 | "go.akshayshah.org/memhttp" 15 | "go.akshayshah.org/memhttp/memhttptest" 16 | ) 17 | 18 | const greeting = "hello world" 19 | 20 | type greeter struct{} 21 | 22 | func (h *greeter) ServeHTTP(w http.ResponseWriter, r *http.Request) { 23 | w.Write([]byte(greeting)) 24 | } 25 | 26 | func TestServer(t *testing.T) { 27 | t.Parallel() 28 | tests := []struct { 29 | name string 30 | opts []memhttp.Option 31 | }{ 32 | {"default", nil}, 33 | {"plaintext", []memhttp.Option{memhttp.WithoutTLS()}}, 34 | {"http1", []memhttp.Option{memhttp.WithoutHTTP2()}}, 35 | } 36 | for _, tt := range tests { 37 | tt := tt 38 | t.Run(tt.name, func(t *testing.T) { 39 | t.Parallel() 40 | const concurrency = 100 41 | srv := memhttptest.New(t, &greeter{}, tt.opts...) 42 | var wg sync.WaitGroup 43 | start := make(chan struct{}) 44 | for i := 0; i < concurrency; i++ { 45 | wg.Add(1) 46 | go func() { 47 | defer wg.Done() 48 | client := srv.Client() 49 | req, err := http.NewRequestWithContext( 50 | context.Background(), 51 | http.MethodGet, 52 | srv.URL(), 53 | strings.NewReader(""), 54 | ) 55 | attest.Ok(t, err) 56 | <-start 57 | res, err := client.Do(req) 58 | attest.Ok(t, err) 59 | attest.Equal(t, res.StatusCode, http.StatusOK, attest.Continue()) 60 | body, err := io.ReadAll(res.Body) 61 | attest.Ok(t, err) 62 | attest.Equal(t, string(body), greeting) 63 | }() 64 | } 65 | close(start) 66 | wg.Wait() 67 | }) 68 | } 69 | } 70 | 71 | func TestRegisterOnShutdown(t *testing.T) { 72 | t.Parallel() 73 | srv, err := memhttp.New(&greeter{}) 74 | attest.Ok(t, err) 75 | done := make(chan struct{}) 76 | srv.RegisterOnShutdown(func() { 77 | close(done) 78 | }) 79 | attest.Ok(t, srv.Shutdown(context.Background())) 80 | select { 81 | case <-done: 82 | case <-time.After(5 * time.Second): 83 | t.Error("OnShutdown hook didn't fire") 84 | } 85 | } 86 | 87 | func Example() { 88 | hello := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 89 | io.WriteString(w, "Hello, world!") 90 | }) 91 | srv, err := memhttp.New(hello) 92 | if err != nil { 93 | panic(err) 94 | } 95 | defer srv.Close() 96 | res, err := srv.Client().Get(srv.URL()) 97 | if err != nil { 98 | panic(err) 99 | } 100 | fmt.Println(res.Status) 101 | // Output: 102 | // 200 OK 103 | } 104 | 105 | func ExampleServer_Transport() { 106 | hello := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 107 | io.WriteString(w, "Hello, world!") 108 | }) 109 | srv, err := memhttp.New(hello) 110 | if err != nil { 111 | panic(err) 112 | } 113 | defer srv.Close() 114 | transport := srv.Transport() 115 | transport.IdleConnTimeout = 10 * time.Second 116 | client := &http.Client{Transport: transport} 117 | res, err := client.Get(srv.URL()) 118 | if err != nil { 119 | panic(err) 120 | } 121 | fmt.Println(res.Status) 122 | // Output: 123 | // 200 OK 124 | } 125 | 126 | func ExampleServer_Client() { 127 | hello := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 128 | io.WriteString(w, "Hello, world!") 129 | }) 130 | srv, err := memhttp.New(hello) 131 | if err != nil { 132 | panic(err) 133 | } 134 | defer srv.Close() 135 | client := srv.Client() 136 | client.Timeout = 10 * time.Second 137 | res, err := client.Get(srv.URL()) 138 | if err != nil { 139 | panic(err) 140 | } 141 | fmt.Println(res.Status) 142 | // Output: 143 | // 200 OK 144 | } 145 | 146 | func ExampleServer_Shutdown() { 147 | hello := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 148 | io.WriteString(w, "Hello, world!") 149 | }) 150 | srv, err := memhttp.New(hello) 151 | if err != nil { 152 | panic(err) 153 | } 154 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 155 | defer cancel() 156 | if err := srv.Shutdown(ctx); err != nil { 157 | panic(err) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /memhttptest/example_test.go: -------------------------------------------------------------------------------- 1 | package memhttptest_test 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "testing" 7 | 8 | "go.akshayshah.org/memhttp/memhttptest" 9 | ) 10 | 11 | func Example() { 12 | // Typically, you'd get a *testing.T from your unit test. 13 | _ = func(t *testing.T) { 14 | hello := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 | io.WriteString(w, "Hello, world!") 16 | }) 17 | // The server is already running, and it automatically shuts down 18 | // gracefully when the test ends. 19 | srv := memhttptest.New(t, hello) 20 | res, err := srv.Client().Get(srv.URL()) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | if res.StatusCode != http.StatusOK { 25 | t.Error(res.Status) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /memhttptest/memhttptest.go: -------------------------------------------------------------------------------- 1 | // Package memhttptest adapts the basic memhttp server to be more convenient for 2 | // tests. 3 | package memhttptest 4 | 5 | import ( 6 | "log" 7 | "net/http" 8 | "testing" 9 | 10 | "go.akshayshah.org/memhttp" 11 | ) 12 | 13 | // New constructs a [memhttp.Server] with defaults suitable for tests: it logs 14 | // runtime errors to the provided testing.TB, and it automatically shuts down 15 | // the server when the test completes. Startup and shutdown errors fail the 16 | // test. 17 | // 18 | // To customize the server, use any [memhttp.Option]. In particular, it may be 19 | // necessary to customize the shutdown timeout with 20 | // [memhttp.WithCleanupTimeout]. 21 | func New(tb testing.TB, h http.Handler, opts ...memhttp.Option) *memhttp.Server { 22 | tb.Helper() 23 | logger := log.New(&tbWriter{tb}, "" /* prefix */, log.Lshortfile) 24 | s, err := memhttp.New( 25 | h, 26 | memhttp.WithErrorLog(logger), 27 | memhttp.WithOptions(opts...), 28 | ) 29 | if err != nil { 30 | tb.Fatalf("start in-memory HTTP server: %v", err) 31 | } 32 | tb.Cleanup(func() { 33 | if err := s.Cleanup(); err != nil { 34 | tb.Error(err) 35 | } 36 | }) 37 | return s 38 | } 39 | 40 | type tbWriter struct { 41 | tb testing.TB 42 | } 43 | 44 | func (w *tbWriter) Write(bs []byte) (int, error) { 45 | w.tb.Log(string(bs)) 46 | return len(bs), nil 47 | } 48 | -------------------------------------------------------------------------------- /option.go: -------------------------------------------------------------------------------- 1 | package memhttp 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "time" 7 | ) 8 | 9 | type config struct { 10 | DisableTLS bool 11 | DisableHTTP2 bool 12 | CleanupContext func() (context.Context, context.CancelFunc) 13 | ErrorLog *log.Logger 14 | } 15 | 16 | // An Option configures a Server. 17 | type Option interface { 18 | apply(*config) 19 | } 20 | 21 | type optionFunc func(*config) 22 | 23 | func (f optionFunc) apply(cfg *config) { f(cfg) } 24 | 25 | // WithoutTLS disables TLS on the server and client. 26 | func WithoutTLS() Option { 27 | return optionFunc(func(cfg *config) { 28 | cfg.DisableTLS = true 29 | }) 30 | } 31 | 32 | // WithoutHTTP2 disables HTTP/2 on the server and client. 33 | func WithoutHTTP2() Option { 34 | return optionFunc(func(cfg *config) { 35 | cfg.DisableHTTP2 = true 36 | }) 37 | } 38 | 39 | // WithOptions composes multiple Options into one. 40 | func WithOptions(opts ...Option) Option { 41 | return optionFunc(func(cfg *config) { 42 | for _, opt := range opts { 43 | opt.apply(cfg) 44 | } 45 | }) 46 | } 47 | 48 | // WithCleanupTimeout customizes the default five-second timeout for the 49 | // server's Cleanup method. It's most useful with the memhttptest subpackage. 50 | func WithCleanupTimeout(d time.Duration) Option { 51 | return optionFunc(func(cfg *config) { 52 | cfg.CleanupContext = func() (context.Context, context.CancelFunc) { 53 | return context.WithTimeout(context.Background(), d) 54 | } 55 | }) 56 | } 57 | 58 | // WithErrorLog sets [http.Server.ErrorLog]. 59 | func WithErrorLog(l *log.Logger) Option { 60 | return optionFunc(func(cfg *config) { 61 | cfg.ErrorLog = l 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /testcert.go: -------------------------------------------------------------------------------- 1 | package memhttp 2 | 3 | import "strings" 4 | 5 | // _cert is a PEM-encoded TLS cert with SAN IPs "127.0.0.1" and "[::1]", 6 | // expiring at Jan 29 16:00:00 2084 GMT. 7 | // 8 | // It's copied from net/http/internal/testcert. 9 | var _cert = []byte(`-----BEGIN CERTIFICATE----- 10 | MIIDOTCCAiGgAwIBAgIQSRJrEpBGFc7tNb1fb5pKFzANBgkqhkiG9w0BAQsFADAS 11 | MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw 12 | MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A 13 | MIIBCgKCAQEA6Gba5tHV1dAKouAaXO3/ebDUU4rvwCUg/CNaJ2PT5xLD4N1Vcb8r 14 | bFSW2HXKq+MPfVdwIKR/1DczEoAGf/JWQTW7EgzlXrCd3rlajEX2D73faWJekD0U 15 | aUgz5vtrTXZ90BQL7WvRICd7FlEZ6FPOcPlumiyNmzUqtwGhO+9ad1W5BqJaRI6P 16 | YfouNkwR6Na4TzSj5BrqUfP0FwDizKSJ0XXmh8g8G9mtwxOSN3Ru1QFc61Xyeluk 17 | POGKBV/q6RBNklTNe0gI8usUMlYyoC7ytppNMW7X2vodAelSu25jgx2anj9fDVZu 18 | h7AXF5+4nJS4AAt0n1lNY7nGSsdZas8PbQIDAQABo4GIMIGFMA4GA1UdDwEB/wQE 19 | AwICpDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud 20 | DgQWBBStsdjh3/JCXXYlQryOrL4Sh7BW5TAuBgNVHREEJzAlggtleGFtcGxlLmNv 21 | bYcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG9w0BAQsFAAOCAQEAxWGI 22 | 5NhpF3nwwy/4yB4i/CwwSpLrWUa70NyhvprUBC50PxiXav1TeDzwzLx/o5HyNwsv 23 | cxv3HdkLW59i/0SlJSrNnWdfZ19oTcS+6PtLoVyISgtyN6DpkKpdG1cOkW3Cy2P2 24 | +tK/tKHRP1Y/Ra0RiDpOAmqn0gCOFGz8+lqDIor/T7MTpibL3IxqWfPrvfVRHL3B 25 | grw/ZQTTIVjjh4JBSW3WyWgNo/ikC1lrVxzl4iPUGptxT36Cr7Zk2Bsg0XqwbOvK 26 | 5d+NTDREkSnUbie4GeutujmX3Dsx88UiV6UY/4lHJa6I5leHUNOHahRbpbWeOfs/ 27 | WkBKOclmOV2xlTVuPw== 28 | -----END CERTIFICATE-----`) 29 | 30 | // _key is the private key for _cert. 31 | var _key = []byte(testingKey(`-----BEGIN RSA TESTING KEY----- 32 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDoZtrm0dXV0Aqi 33 | 4Bpc7f95sNRTiu/AJSD8I1onY9PnEsPg3VVxvytsVJbYdcqr4w99V3AgpH/UNzMS 34 | gAZ/8lZBNbsSDOVesJ3euVqMRfYPvd9pYl6QPRRpSDPm+2tNdn3QFAvta9EgJ3sW 35 | URnoU85w+W6aLI2bNSq3AaE771p3VbkGolpEjo9h+i42TBHo1rhPNKPkGupR8/QX 36 | AOLMpInRdeaHyDwb2a3DE5I3dG7VAVzrVfJ6W6Q84YoFX+rpEE2SVM17SAjy6xQy 37 | VjKgLvK2mk0xbtfa+h0B6VK7bmODHZqeP18NVm6HsBcXn7iclLgAC3SfWU1jucZK 38 | x1lqzw9tAgMBAAECggEABWzxS1Y2wckblnXY57Z+sl6YdmLV+gxj2r8Qib7g4ZIk 39 | lIlWR1OJNfw7kU4eryib4fc6nOh6O4AWZyYqAK6tqNQSS/eVG0LQTLTTEldHyVJL 40 | dvBe+MsUQOj4nTndZW+QvFzbcm2D8lY5n2nBSxU5ypVoKZ1EqQzytFcLZpTN7d89 41 | EPj0qDyrV4NZlWAwL1AygCwnlwhMQjXEalVF1ylXwU3QzyZ/6MgvF6d3SSUlh+sq 42 | XefuyigXw484cQQgbzopv6niMOmGP3of+yV4JQqUSb3IDmmT68XjGd2Dkxl4iPki 43 | 6ZwXf3CCi+c+i/zVEcufgZ3SLf8D99kUGE7v7fZ6AQKBgQD1ZX3RAla9hIhxCf+O 44 | 3D+I1j2LMrdjAh0ZKKqwMR4JnHX3mjQI6LwqIctPWTU8wYFECSh9klEclSdCa64s 45 | uI/GNpcqPXejd0cAAdqHEEeG5sHMDt0oFSurL4lyud0GtZvwlzLuwEweuDtvT9cJ 46 | Wfvl86uyO36IW8JdvUprYDctrQKBgQDycZ697qutBieZlGkHpnYWUAeImVA878sJ 47 | w44NuXHvMxBPz+lbJGAg8Cn8fcxNAPqHIraK+kx3po8cZGQywKHUWsxi23ozHoxo 48 | +bGqeQb9U661TnfdDspIXia+xilZt3mm5BPzOUuRqlh4Y9SOBpSWRmEhyw76w4ZP 49 | OPxjWYAgwQKBgA/FehSYxeJgRjSdo+MWnK66tjHgDJE8bYpUZsP0JC4R9DL5oiaA 50 | brd2fI6Y+SbyeNBallObt8LSgzdtnEAbjIH8uDJqyOmknNePRvAvR6mP4xyuR+Bv 51 | m+Lgp0DMWTw5J9CKpydZDItc49T/mJ5tPhdFVd+am0NAQnmr1MCZ6nHxAoGABS3Y 52 | LkaC9FdFUUqSU8+Chkd/YbOkuyiENdkvl6t2e52jo5DVc1T7mLiIrRQi4SI8N9bN 53 | /3oJWCT+uaSLX2ouCtNFunblzWHBrhxnZzTeqVq4SLc8aESAnbslKL4i8/+vYZlN 54 | s8xtiNcSvL+lMsOBORSXzpj/4Ot8WwTkn1qyGgECgYBKNTypzAHeLE6yVadFp3nQ 55 | Ckq9yzvP/ib05rvgbvrne00YeOxqJ9gtTrzgh7koqJyX1L4NwdkEza4ilDWpucn0 56 | xiUZS4SoaJq6ZvcBYS62Yr1t8n09iG47YL8ibgtmH3L+svaotvpVxVK+d7BLevA/ 57 | ZboOWVe3icTy64BT3OQhmg== 58 | -----END RSA TESTING KEY-----`)) 59 | 60 | // testingKey prevents false positives from key scanners. 61 | func testingKey(s string) string { 62 | return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") 63 | } 64 | --------------------------------------------------------------------------------