Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • ai3/go-common
1 result
Show changes
Commits on Source (25)
......@@ -9,6 +9,7 @@ import (
"fmt"
"log"
"math/rand"
"mime"
"net/http"
"net/url"
"os"
......@@ -158,7 +159,7 @@ func (b *balancedBackend) Call(ctx context.Context, shard, path string, req, res
defer httpResp.Body.Close() // nolint
// Decode the response, unless the 'resp' output is nil.
if httpResp.Header.Get("Content-Type") != "application/json" {
if ct, _, _ := mime.ParseMediaType(httpResp.Header.Get("Content-Type")); ct != "application/json" {
return errors.New("not a JSON response")
}
if resp == nil {
......
......@@ -4,28 +4,25 @@ go 1.11
require (
contrib.go.opencensus.io/exporter/zipkin v0.1.2
github.com/NYTimes/gziphandler v1.1.1
github.com/amoghe/go-crypt v0.0.0-20191109212615-b2ff80594b7f
github.com/bbrks/wrap/v2 v2.5.0
github.com/cenkalti/backoff/v4 v4.1.0
github.com/coreos/go-systemd/v22 v22.2.0
github.com/cenkalti/backoff/v4 v4.1.1
github.com/coreos/go-systemd/v22 v22.3.2
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594
github.com/go-asn1-ber/asn1-ber v1.5.3
github.com/go-ldap/ldap/v3 v3.2.4
github.com/go-ldap/ldap/v3 v3.4.1
github.com/gofrs/flock v0.8.0 // indirect
github.com/google/go-cmp v0.5.5
github.com/google/go-cmp v0.5.6
github.com/gorilla/handlers v1.5.1
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40
github.com/miscreant/miscreant.go v0.0.0-20200214223636-26d376326b75
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/openzipkin/zipkin-go v0.2.5
github.com/pierrec/lz4 v2.0.5+incompatible // indirect
github.com/prometheus/client_golang v1.9.0
github.com/prometheus/client_golang v1.11.0
github.com/russross/blackfriday/v2 v2.1.0
github.com/theckman/go-flock v0.8.0
github.com/theckman/go-flock v0.8.1
github.com/tstranex/u2f v1.0.0
go.opencensus.io v0.23.0
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 // indirect
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
)
This diff is collapsed.
package serverutil
import (
"compress/gzip"
"context"
"crypto/tls"
"fmt"
......@@ -16,12 +17,25 @@ import (
"time"
"git.autistici.org/ai3/go-common/tracing"
"github.com/NYTimes/gziphandler"
"github.com/coreos/go-systemd/v22/daemon"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var gracefulShutdownTimeout = 3 * time.Second
var (
gracefulShutdownTimeout = 3 * time.Second
gzipLevel = gzip.BestSpeed
gzipMinSize = 1300
gzipContentTypes = []string{
"application/json",
"application/javascript",
"text/html",
"text/plain",
"text/css",
}
)
// ServerConfig stores common HTTP/HTTPS server configuration parameters.
type ServerConfig struct {
......@@ -29,20 +43,23 @@ type ServerConfig struct {
MaxInflightRequests int `yaml:"max_inflight_requests"`
RequestTimeoutSecs int `yaml:"request_timeout"`
TrustedForwarders []string `yaml:"trusted_forwarders"`
// TODO: switch do disable_compression (flip default) later.
EnableCompression bool `yaml:"enable_compression"`
}
func (config *ServerConfig) buildHTTPServer(h http.Handler) (*http.Server, error) {
func (config *ServerConfig) buildHTTPHandler(h http.Handler) (http.Handler, *tls.Config, error) {
var tlsConfig *tls.Config
var err error
if config != nil {
if config.TLS != nil {
tlsConfig, err = config.TLS.TLSConfig()
if err != nil {
return nil, err
return nil, nil, err
}
h, err = config.TLS.TLSAuthWrapper(h)
if err != nil {
return nil, err
return nil, nil, err
}
}
......@@ -51,7 +68,7 @@ func (config *ServerConfig) buildHTTPServer(h http.Handler) (*http.Server, error
if len(config.TrustedForwarders) > 0 {
h, err = newProxyHeaders(h, config.TrustedForwarders)
if err != nil {
return nil, err
return nil, nil, err
}
}
......@@ -68,16 +85,23 @@ func (config *ServerConfig) buildHTTPServer(h http.Handler) (*http.Server, error
}
}
// These are not meant to be external-facing servers, so we
// can be generous with the timeouts to keep the number of
// reconnections low.
return &http.Server{
Handler: addDefaultHandlers(h),
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 600 * time.Second,
TLSConfig: tlsConfig,
}, nil
// Add all the default handlers (health, monitoring, etc).
h = addDefaultHandlers(h)
// Optionally enable compression.
if config.EnableCompression {
gzwrap, err := gziphandler.GzipHandlerWithOpts(
gziphandler.CompressionLevel(gzipLevel),
gziphandler.MinSize(gzipMinSize),
gziphandler.ContentTypes(gzipContentTypes),
)
if err != nil {
return nil, nil, err
}
h = gzwrap(h)
}
return h, tlsConfig, nil
}
// Serve HTTP(S) content on the specified address. If config.TLS is
......@@ -91,12 +115,22 @@ func Serve(h http.Handler, config *ServerConfig, addr string) error {
// debugging endpoints).
h = tracing.WrapHandler(h, guessEndpointName(addr))
// Create the HTTP server.
srv, err := config.buildHTTPServer(h)
// Create the top-level HTTP handler with all our additions.
hh, tlsConfig, err := config.buildHTTPHandler(h)
if err != nil {
return err
}
// These are not meant to be external-facing servers, so we
// can be generous with the timeouts to keep the number of
// reconnections low.
srv := &http.Server{
Handler: hh,
ReadHeaderTimeout: 30 * time.Second,
IdleTimeout: 600 * time.Second,
TLSConfig: tlsConfig,
}
// Create the net.Listener first, so we can detect
// initialization-time errors safely.
l, err := net.Listen("tcp", addr)
......
package serverutil
import (
"context"
"crypto/rand"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"git.autistici.org/ai3/go-common/clientutil"
)
type TestRequest struct {
Data []string `json:"data"`
}
type TestObject struct {
Name string `json:"name"`
Host string `json:"host"`
Timestamp time.Time `json:"timestamp"`
PubKey []byte `json:"pubkey"`
}
type TestResponse struct {
Objects []*TestObject `json:"objects"`
}
func fastRandomBytes(n int) []byte {
b := make([]byte, n)
rand.Read(b) // nolint: errcheck
return b
}
func makeTestHandler() http.HandlerFunc {
// Generate a large-ish random response.
var resp TestResponse
now := time.Now()
n := 256
resp.Objects = make([]*TestObject, 0, n)
for i := 0; i < n; i++ {
resp.Objects = append(resp.Objects, &TestObject{
Name: fmt.Sprintf("test-object-%06d", i+1),
Host: "host-452-ff-bb",
Timestamp: now,
PubKey: fastRandomBytes(256),
})
}
return func(w http.ResponseWriter, httpReq *http.Request) {
var req TestRequest
if !DecodeJSONRequest(w, httpReq, &req) {
return
}
EncodeJSONResponse(w, &resp)
}
}
const apiPath = "/api/v1/random"
func makeTestRequest() *TestRequest {
var req TestRequest
n := 256
req.Data = make([]string, 0, n)
for i := 0; i < n; i++ {
req.Data = append(req.Data, fmt.Sprintf("data-item-%06d", i))
}
return &req
}
func makeSingleRequest(backend clientutil.Backend, req *TestRequest) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var resp TestResponse
return backend.Call(ctx, "", apiPath, &req, &resp)
}
func runHTTPTest(t *testing.T, config *ServerConfig) {
mux := http.NewServeMux()
mux.HandleFunc(apiPath, makeTestHandler())
h, _, err := config.buildHTTPHandler(mux)
if err != nil {
t.Fatal(err)
}
srv := httptest.NewServer(h)
defer srv.Close()
backend, err := clientutil.NewBackend(&clientutil.BackendConfig{
URL: srv.URL,
})
if err != nil {
t.Fatalf("NewBackend() error: %v", err)
}
defer backend.Close()
if err := makeSingleRequest(backend, makeTestRequest()); err != nil {
t.Fatal(err)
}
}
func TestHTTP(t *testing.T) {
runHTTPTest(t, &ServerConfig{})
}
func TestHTTP_Compression(t *testing.T) {
runHTTPTest(t, &ServerConfig{
EnableCompression: true,
})
}
func BenchmarkLoad(b *testing.B) {
mux := http.NewServeMux()
mux.HandleFunc(apiPath, makeTestHandler())
config := &ServerConfig{
EnableCompression: true,
}
h, _, _ := config.buildHTTPHandler(mux)
srv := httptest.NewServer(h)
defer srv.Close()
backend, err := clientutil.NewBackend(&clientutil.BackendConfig{
URL: srv.URL,
})
if err != nil {
b.Fatalf("NewBackend() error: %v", err)
}
defer backend.Close()
req := makeTestRequest()
// Run clients.
b.SetParallelism(100)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
makeSingleRequest(backend, req) // nolint: errcheck
}
})
}
......@@ -3,6 +3,7 @@ package serverutil
import (
"encoding/json"
"log"
"mime"
"net/http"
)
......@@ -14,7 +15,7 @@ func DecodeJSONRequest(w http.ResponseWriter, r *http.Request, obj interface{})
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return false
}
if r.Header.Get("Content-Type") != "application/json" {
if ct, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type")); ct != "application/json" {
http.Error(w, "Need JSON request", http.StatusBadRequest)
return false
}
......@@ -29,19 +30,15 @@ func DecodeJSONRequest(w http.ResponseWriter, r *http.Request, obj interface{})
// EncodeJSONResponse writes an application/json response to w.
func EncodeJSONResponse(w http.ResponseWriter, obj interface{}) {
data, err := json.Marshal(obj)
if err != nil {
log.Printf("JSON serialization error: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Expires", "-1")
w.Header().Set("X-Content-Type-Options", "nosniff")
if _, err = w.Write(data); err != nil {
log.Printf("error writing response: %v", err)
err := json.NewEncoder(w).Encode(obj)
if err != nil {
log.Printf("error writing JSON response: %v", err)
// Too late to return an error to the client now.
}
}