mirror of
https://github.com/Luzifer/nginx-sso.git
synced 2024-12-30 01:31:18 +00:00
597 lines
16 KiB
Go
597 lines
16 KiB
Go
package ochttp
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/net/http2"
|
|
|
|
"go.opencensus.io/stats/view"
|
|
"go.opencensus.io/trace"
|
|
)
|
|
|
|
func httpHandler(statusCode, respSize int) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(statusCode)
|
|
body := make([]byte, respSize)
|
|
w.Write(body)
|
|
})
|
|
}
|
|
|
|
func updateMean(mean float64, sample, count int) float64 {
|
|
if count == 1 {
|
|
return float64(sample)
|
|
}
|
|
return mean + (float64(sample)-mean)/float64(count)
|
|
}
|
|
|
|
func TestHandlerStatsCollection(t *testing.T) {
|
|
if err := view.Register(DefaultServerViews...); err != nil {
|
|
t.Fatalf("Failed to register ochttp.DefaultServerViews error: %v", err)
|
|
}
|
|
|
|
views := []string{
|
|
"opencensus.io/http/server/request_count",
|
|
"opencensus.io/http/server/latency",
|
|
"opencensus.io/http/server/request_bytes",
|
|
"opencensus.io/http/server/response_bytes",
|
|
}
|
|
|
|
// TODO: test latency measurements?
|
|
tests := []struct {
|
|
name, method, target string
|
|
count, statusCode, reqSize, respSize int
|
|
}{
|
|
{"get 200", "GET", "http://opencensus.io/request/one", 10, 200, 512, 512},
|
|
{"post 503", "POST", "http://opencensus.io/request/two", 5, 503, 1024, 16384},
|
|
{"no body 302", "GET", "http://opencensus.io/request/three", 2, 302, 0, 0},
|
|
}
|
|
totalCount, meanReqSize, meanRespSize := 0, 0.0, 0.0
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
body := bytes.NewBuffer(make([]byte, test.reqSize))
|
|
r := httptest.NewRequest(test.method, test.target, body)
|
|
w := httptest.NewRecorder()
|
|
mux := http.NewServeMux()
|
|
mux.Handle("/request/", httpHandler(test.statusCode, test.respSize))
|
|
h := &Handler{
|
|
Handler: mux,
|
|
StartOptions: trace.StartOptions{
|
|
Sampler: trace.NeverSample(),
|
|
},
|
|
}
|
|
for i := 0; i < test.count; i++ {
|
|
h.ServeHTTP(w, r)
|
|
totalCount++
|
|
// Distributions do not track sum directly, we must
|
|
// mimic their behaviour to avoid rounding failures.
|
|
meanReqSize = updateMean(meanReqSize, test.reqSize, totalCount)
|
|
meanRespSize = updateMean(meanRespSize, test.respSize, totalCount)
|
|
}
|
|
})
|
|
}
|
|
|
|
for _, viewName := range views {
|
|
v := view.Find(viewName)
|
|
if v == nil {
|
|
t.Errorf("view not found %q", viewName)
|
|
continue
|
|
}
|
|
rows, err := view.RetrieveData(viewName)
|
|
if err != nil {
|
|
t.Error(err)
|
|
continue
|
|
}
|
|
if got, want := len(rows), 1; got != want {
|
|
t.Errorf("len(%q) = %d; want %d", viewName, got, want)
|
|
continue
|
|
}
|
|
data := rows[0].Data
|
|
|
|
var count int
|
|
var sum float64
|
|
switch data := data.(type) {
|
|
case *view.CountData:
|
|
count = int(data.Value)
|
|
case *view.DistributionData:
|
|
count = int(data.Count)
|
|
sum = data.Sum()
|
|
default:
|
|
t.Errorf("Unknown data type: %v", data)
|
|
continue
|
|
}
|
|
|
|
if got, want := count, totalCount; got != want {
|
|
t.Fatalf("%s = %d; want %d", viewName, got, want)
|
|
}
|
|
|
|
// We can only check sum for distribution views.
|
|
switch viewName {
|
|
case "opencensus.io/http/server/request_bytes":
|
|
if got, want := sum, meanReqSize*float64(totalCount); got != want {
|
|
t.Fatalf("%s = %g; want %g", viewName, got, want)
|
|
}
|
|
case "opencensus.io/http/server/response_bytes":
|
|
if got, want := sum, meanRespSize*float64(totalCount); got != want {
|
|
t.Fatalf("%s = %g; want %g", viewName, got, want)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type testResponseWriterHijacker struct {
|
|
httptest.ResponseRecorder
|
|
}
|
|
|
|
func (trw *testResponseWriterHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
func TestUnitTestHandlerProxiesHijack(t *testing.T) {
|
|
tests := []struct {
|
|
w http.ResponseWriter
|
|
hasHijack bool
|
|
}{
|
|
{httptest.NewRecorder(), false},
|
|
{nil, false},
|
|
{new(testResponseWriterHijacker), true},
|
|
}
|
|
|
|
for i, tt := range tests {
|
|
tw := &trackingResponseWriter{writer: tt.w}
|
|
w := tw.wrappedResponseWriter()
|
|
_, ttHijacker := w.(http.Hijacker)
|
|
if want, have := tt.hasHijack, ttHijacker; want != have {
|
|
t.Errorf("#%d Hijack got %t, want %t", i, have, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Integration test with net/http to ensure that our Handler proxies to its
|
|
// response the call to (http.Hijack).Hijacker() and that that successfully
|
|
// passes with HTTP/1.1 connections. See Issue #642
|
|
func TestHandlerProxiesHijack_HTTP1(t *testing.T) {
|
|
cst := httptest.NewServer(&Handler{
|
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var writeMsg func(string)
|
|
defer func() {
|
|
err := recover()
|
|
writeMsg(fmt.Sprintf("Proto=%s\npanic=%v", r.Proto, err != nil))
|
|
}()
|
|
conn, _, _ := w.(http.Hijacker).Hijack()
|
|
writeMsg = func(msg string) {
|
|
fmt.Fprintf(conn, "%s 200\nContentLength: %d", r.Proto, len(msg))
|
|
fmt.Fprintf(conn, "\r\n\r\n%s", msg)
|
|
conn.Close()
|
|
}
|
|
}),
|
|
})
|
|
defer cst.Close()
|
|
|
|
testCases := []struct {
|
|
name string
|
|
tr *http.Transport
|
|
want string
|
|
}{
|
|
{
|
|
name: "http1-transport",
|
|
tr: new(http.Transport),
|
|
want: "Proto=HTTP/1.1\npanic=false",
|
|
},
|
|
{
|
|
name: "http2-transport",
|
|
tr: func() *http.Transport {
|
|
tr := new(http.Transport)
|
|
http2.ConfigureTransport(tr)
|
|
return tr
|
|
}(),
|
|
want: "Proto=HTTP/1.1\npanic=false",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
c := &http.Client{Transport: &Transport{Base: tc.tr}}
|
|
res, err := c.Get(cst.URL)
|
|
if err != nil {
|
|
t.Errorf("(%s) unexpected error %v", tc.name, err)
|
|
continue
|
|
}
|
|
blob, _ := ioutil.ReadAll(res.Body)
|
|
res.Body.Close()
|
|
if g, w := string(blob), tc.want; g != w {
|
|
t.Errorf("(%s) got = %q; want = %q", tc.name, g, w)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Integration test with net/http, x/net/http2 to ensure that our Handler proxies
|
|
// to its response the call to (http.Hijack).Hijacker() and that that crashes
|
|
// since http.Hijacker and HTTP/2.0 connections are incompatible, but the
|
|
// detection is only at runtime and ensure that we can stream and flush to the
|
|
// connection even after invoking Hijack(). See Issue #642.
|
|
func TestHandlerProxiesHijack_HTTP2(t *testing.T) {
|
|
cst := httptest.NewUnstartedServer(&Handler{
|
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if _, ok := w.(http.Hijacker); ok {
|
|
conn, _, err := w.(http.Hijacker).Hijack()
|
|
if conn != nil {
|
|
data := fmt.Sprintf("Surprisingly got the Hijacker() Proto: %s", r.Proto)
|
|
fmt.Fprintf(conn, "%s 200\nContent-Length:%d\r\n\r\n%s", r.Proto, len(data), data)
|
|
conn.Close()
|
|
return
|
|
}
|
|
|
|
switch {
|
|
case err == nil:
|
|
fmt.Fprintf(w, "Unexpectedly did not encounter an error!")
|
|
default:
|
|
fmt.Fprintf(w, "Unexpected error: %v", err)
|
|
case strings.Contains(err.(error).Error(), "Hijack"):
|
|
// Confirmed HTTP/2.0, let's stream to it
|
|
for i := 0; i < 5; i++ {
|
|
fmt.Fprintf(w, "%d\n", i)
|
|
w.(http.Flusher).Flush()
|
|
}
|
|
}
|
|
} else {
|
|
// Confirmed HTTP/2.0, let's stream to it
|
|
for i := 0; i < 5; i++ {
|
|
fmt.Fprintf(w, "%d\n", i)
|
|
w.(http.Flusher).Flush()
|
|
}
|
|
}
|
|
}),
|
|
})
|
|
cst.TLS = &tls.Config{NextProtos: []string{"h2"}}
|
|
cst.StartTLS()
|
|
defer cst.Close()
|
|
|
|
if wantPrefix := "https://"; !strings.HasPrefix(cst.URL, wantPrefix) {
|
|
t.Fatalf("URL got = %q wantPrefix = %q", cst.URL, wantPrefix)
|
|
}
|
|
|
|
tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
|
|
http2.ConfigureTransport(tr)
|
|
c := &http.Client{Transport: tr}
|
|
res, err := c.Get(cst.URL)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error %v", err)
|
|
}
|
|
blob, _ := ioutil.ReadAll(res.Body)
|
|
res.Body.Close()
|
|
if g, w := string(blob), "0\n1\n2\n3\n4\n"; g != w {
|
|
t.Errorf("got = %q; want = %q", g, w)
|
|
}
|
|
}
|
|
|
|
func TestEnsureTrackingResponseWriterSetsStatusCode(t *testing.T) {
|
|
// Ensure that the trackingResponseWriter always sets the spanStatus on ending the span.
|
|
// Because we can only examine the Status after exporting, this test roundtrips a
|
|
// couple of requests and then later examines the exported spans.
|
|
// See Issue #700.
|
|
exporter := &spanExporter{cur: make(chan *trace.SpanData, 1)}
|
|
trace.RegisterExporter(exporter)
|
|
defer trace.UnregisterExporter(exporter)
|
|
|
|
tests := []struct {
|
|
res *http.Response
|
|
want trace.Status
|
|
}{
|
|
{res: &http.Response{StatusCode: 200}, want: trace.Status{Code: trace.StatusCodeOK, Message: `OK`}},
|
|
{res: &http.Response{StatusCode: 500}, want: trace.Status{Code: trace.StatusCodeUnknown, Message: `UNKNOWN`}},
|
|
{res: &http.Response{StatusCode: 403}, want: trace.Status{Code: trace.StatusCodePermissionDenied, Message: `PERMISSION_DENIED`}},
|
|
{res: &http.Response{StatusCode: 401}, want: trace.Status{Code: trace.StatusCodeUnauthenticated, Message: `UNAUTHENTICATED`}},
|
|
{res: &http.Response{StatusCode: 429}, want: trace.Status{Code: trace.StatusCodeResourceExhausted, Message: `RESOURCE_EXHAUSTED`}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.want.Message, func(t *testing.T) {
|
|
ctx := context.Background()
|
|
prc, pwc := io.Pipe()
|
|
go func() {
|
|
pwc.Write([]byte("Foo"))
|
|
pwc.Close()
|
|
}()
|
|
inRes := tt.res
|
|
inRes.Body = prc
|
|
tr := &traceTransport{
|
|
base: &testResponseTransport{res: inRes},
|
|
formatSpanName: spanNameFromURL,
|
|
startOptions: trace.StartOptions{
|
|
Sampler: trace.AlwaysSample(),
|
|
},
|
|
}
|
|
req, err := http.NewRequest("POST", "https://example.org", bytes.NewReader([]byte("testing")))
|
|
if err != nil {
|
|
t.Fatalf("NewRequest error: %v", err)
|
|
}
|
|
req = req.WithContext(ctx)
|
|
res, err := tr.RoundTrip(req)
|
|
if err != nil {
|
|
t.Fatalf("RoundTrip error: %v", err)
|
|
}
|
|
_, _ = ioutil.ReadAll(res.Body)
|
|
res.Body.Close()
|
|
|
|
cur := <-exporter.cur
|
|
if got, want := cur.Status, tt.want; got != want {
|
|
t.Fatalf("SpanData:\ngot = (%#v)\nwant = (%#v)", got, want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type spanExporter struct {
|
|
sync.Mutex
|
|
cur chan *trace.SpanData
|
|
}
|
|
|
|
var _ trace.Exporter = (*spanExporter)(nil)
|
|
|
|
func (se *spanExporter) ExportSpan(sd *trace.SpanData) {
|
|
se.Lock()
|
|
se.cur <- sd
|
|
se.Unlock()
|
|
}
|
|
|
|
type testResponseTransport struct {
|
|
res *http.Response
|
|
}
|
|
|
|
var _ http.RoundTripper = (*testResponseTransport)(nil)
|
|
|
|
func (rb *testResponseTransport) RoundTrip(*http.Request) (*http.Response, error) {
|
|
return rb.res, nil
|
|
}
|
|
|
|
func TestHandlerImplementsHTTPPusher(t *testing.T) {
|
|
cst := setupAndStartServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
pusher, ok := w.(http.Pusher)
|
|
if !ok {
|
|
w.Write([]byte("false"))
|
|
return
|
|
}
|
|
err := pusher.Push("/static.css", &http.PushOptions{
|
|
Method: "GET",
|
|
Header: http.Header{"Accept-Encoding": r.Header["Accept-Encoding"]},
|
|
})
|
|
if err != nil && false {
|
|
// TODO: (@odeke-em) consult with Go stdlib for why trying
|
|
// to configure even an HTTP/2 server and HTTP/2 transport
|
|
// still return http.ErrNotSupported even without using ochttp.Handler.
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
w.Write([]byte("true"))
|
|
}), asHTTP2)
|
|
defer cst.Close()
|
|
|
|
tests := []struct {
|
|
rt http.RoundTripper
|
|
wantBody string
|
|
}{
|
|
{
|
|
rt: h1Transport(),
|
|
wantBody: "false",
|
|
},
|
|
{
|
|
rt: h2Transport(),
|
|
wantBody: "true",
|
|
},
|
|
{
|
|
rt: &Transport{Base: h1Transport()},
|
|
wantBody: "false",
|
|
},
|
|
{
|
|
rt: &Transport{Base: h2Transport()},
|
|
wantBody: "true",
|
|
},
|
|
}
|
|
|
|
for i, tt := range tests {
|
|
c := &http.Client{Transport: &Transport{Base: tt.rt}}
|
|
res, err := c.Get(cst.URL)
|
|
if err != nil {
|
|
t.Errorf("#%d: Unexpected error %v", i, err)
|
|
continue
|
|
}
|
|
body, _ := ioutil.ReadAll(res.Body)
|
|
_ = res.Body.Close()
|
|
if g, w := string(body), tt.wantBody; g != w {
|
|
t.Errorf("#%d: got = %q; want = %q", i, g, w)
|
|
}
|
|
}
|
|
}
|
|
|
|
const (
|
|
isNil = "isNil"
|
|
hang = "hang"
|
|
ended = "ended"
|
|
nonNotifier = "nonNotifier"
|
|
|
|
asHTTP1 = false
|
|
asHTTP2 = true
|
|
)
|
|
|
|
func setupAndStartServer(hf func(http.ResponseWriter, *http.Request), isHTTP2 bool) *httptest.Server {
|
|
cst := httptest.NewUnstartedServer(&Handler{
|
|
Handler: http.HandlerFunc(hf),
|
|
})
|
|
if isHTTP2 {
|
|
http2.ConfigureServer(cst.Config, new(http2.Server))
|
|
cst.TLS = cst.Config.TLSConfig
|
|
cst.StartTLS()
|
|
} else {
|
|
cst.Start()
|
|
}
|
|
|
|
return cst
|
|
}
|
|
|
|
func insecureTLS() *tls.Config { return &tls.Config{InsecureSkipVerify: true} }
|
|
func h1Transport() *http.Transport { return &http.Transport{TLSClientConfig: insecureTLS()} }
|
|
func h2Transport() *http.Transport {
|
|
tr := &http.Transport{TLSClientConfig: insecureTLS()}
|
|
http2.ConfigureTransport(tr)
|
|
return tr
|
|
}
|
|
|
|
type concurrentBuffer struct {
|
|
sync.RWMutex
|
|
bw *bytes.Buffer
|
|
}
|
|
|
|
func (cw *concurrentBuffer) Write(b []byte) (int, error) {
|
|
cw.Lock()
|
|
defer cw.Unlock()
|
|
|
|
return cw.bw.Write(b)
|
|
}
|
|
|
|
func (cw *concurrentBuffer) String() string {
|
|
cw.Lock()
|
|
defer cw.Unlock()
|
|
|
|
return cw.bw.String()
|
|
}
|
|
|
|
func handleCloseNotify(outLog io.Writer) http.HandlerFunc {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
cn, ok := w.(http.CloseNotifier)
|
|
if !ok {
|
|
fmt.Fprintln(outLog, nonNotifier)
|
|
return
|
|
}
|
|
ch := cn.CloseNotify()
|
|
if ch == nil {
|
|
fmt.Fprintln(outLog, isNil)
|
|
return
|
|
}
|
|
|
|
<-ch
|
|
fmt.Fprintln(outLog, ended)
|
|
})
|
|
}
|
|
|
|
func TestHandlerImplementsHTTPCloseNotify(t *testing.T) {
|
|
http1Log := &concurrentBuffer{bw: new(bytes.Buffer)}
|
|
http1Server := setupAndStartServer(handleCloseNotify(http1Log), asHTTP1)
|
|
http2Log := &concurrentBuffer{bw: new(bytes.Buffer)}
|
|
http2Server := setupAndStartServer(handleCloseNotify(http2Log), asHTTP2)
|
|
|
|
defer http1Server.Close()
|
|
defer http2Server.Close()
|
|
|
|
tests := []struct {
|
|
url string
|
|
want string
|
|
}{
|
|
{url: http1Server.URL, want: nonNotifier},
|
|
{url: http2Server.URL, want: ended},
|
|
}
|
|
|
|
transports := []struct {
|
|
name string
|
|
rt http.RoundTripper
|
|
}{
|
|
{name: "http2+ochttp", rt: &Transport{Base: h2Transport()}},
|
|
{name: "http1+ochttp", rt: &Transport{Base: h1Transport()}},
|
|
{name: "http1-ochttp", rt: h1Transport()},
|
|
{name: "http2-ochttp", rt: h2Transport()},
|
|
}
|
|
|
|
// Each transport invokes one of two server types, either HTTP/1 or HTTP/2
|
|
for _, trc := range transports {
|
|
// Try out all the transport combinations
|
|
for i, tt := range tests {
|
|
req, err := http.NewRequest("GET", tt.url, nil)
|
|
if err != nil {
|
|
t.Errorf("#%d: Unexpected error making request: %v", i, err)
|
|
continue
|
|
}
|
|
|
|
// Using a timeout to ensure that the request is cancelled and the server
|
|
// if its handler implements CloseNotify will see this as the client leaving.
|
|
ctx, cancel := context.WithTimeout(context.Background(), 80*time.Millisecond)
|
|
defer cancel()
|
|
req = req.WithContext(ctx)
|
|
|
|
client := &http.Client{Transport: trc.rt}
|
|
res, err := client.Do(req)
|
|
if err != nil && !strings.Contains(err.Error(), "context deadline exceeded") {
|
|
t.Errorf("#%d: %sClient Unexpected error %v", i, trc.name, err)
|
|
continue
|
|
}
|
|
if res != nil && res.Body != nil {
|
|
io.CopyN(ioutil.Discard, res.Body, 5)
|
|
_ = res.Body.Close()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Wait for a couple of milliseconds for the GoAway frames to be properly propagated
|
|
<-time.After(200 * time.Millisecond)
|
|
|
|
wantHTTP1Log := strings.Repeat("ended\n", len(transports))
|
|
wantHTTP2Log := strings.Repeat("ended\n", len(transports))
|
|
if g, w := http1Log.String(), wantHTTP1Log; g != w {
|
|
t.Errorf("HTTP1Log got\n\t%q\nwant\n\t%q", g, w)
|
|
}
|
|
if g, w := http2Log.String(), wantHTTP2Log; g != w {
|
|
t.Errorf("HTTP2Log got\n\t%q\nwant\n\t%q", g, w)
|
|
}
|
|
}
|
|
|
|
func TestIgnoreHealthz(t *testing.T) {
|
|
var spans int
|
|
|
|
ts := httptest.NewServer(&Handler{
|
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
span := trace.FromContext(r.Context())
|
|
if span != nil {
|
|
spans++
|
|
}
|
|
fmt.Fprint(w, "ok")
|
|
}),
|
|
StartOptions: trace.StartOptions{
|
|
Sampler: trace.AlwaysSample(),
|
|
},
|
|
})
|
|
defer ts.Close()
|
|
|
|
client := &http.Client{}
|
|
|
|
for _, path := range []string{"/healthz", "/_ah/health"} {
|
|
resp, err := client.Get(ts.URL + path)
|
|
if err != nil {
|
|
t.Fatalf("Cannot GET %q: %v", path, err)
|
|
}
|
|
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("Cannot read body for %q: %v", path, err)
|
|
}
|
|
|
|
if got, want := string(b), "ok"; got != want {
|
|
t.Fatalf("Body for %q = %q; want %q", path, got, want)
|
|
}
|
|
resp.Body.Close()
|
|
}
|
|
|
|
if spans > 0 {
|
|
t.Errorf("Got %v spans; want no spans", spans)
|
|
}
|
|
}
|