Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(upstream) toxic for http headers #215

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,12 @@ Closes connection when transmitted data exceeded limit.

- `bytes`: number of bytes it should transmit before connection is closed

#### http_request_headers

Modifies http request headers. This toxic only has effect when the direction equals `upstream`. The most common use case would be modifying the Host header when using reverse proxies.

- `headers`: a key value map with the headers to set.

### HTTP API

All communication with the Toxiproxy daemon from the client happens through the
Expand Down
56 changes: 56 additions & 0 deletions toxics/http_request_headers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package toxics

import (
"bufio"
"bytes"
"io"
"net/http"
"strings"

"github.com/Shopify/toxiproxy/stream"
)

// HttpToxic modifies requests headers (upstream) for http requests. Not to be used with direction = downstream
type HttpToxic struct {
Headers map[string]string `json:"headers"`
}

func (t *HttpToxic) modifyRequest(request *http.Request) {
// Add all headers to request. Host is derived from the url if we dont set it explicitly.
for k, v := range t.Headers {
if strings.EqualFold("Host", k) {
request.Host = v
} else {
request.Header.Set(k, v)
}
}
}

func (t *HttpToxic) Pipe(stub *ToxicStub) {
buffer := bytes.NewBuffer(make([]byte, 0, 32*1024))
writer := stream.NewChanWriter(stub.Output)
reader := stream.NewChanReader(stub.Input)
reader.SetInterrupt(stub.Interrupt)
for {
tee := io.TeeReader(reader, buffer)
req, err := http.ReadRequest(bufio.NewReader(tee))
if err == stream.ErrInterrupted {
buffer.WriteTo(writer)
return
} else if err == io.EOF {
stub.Close()
return
}
if err != nil {
buffer.WriteTo(writer)
} else {
t.modifyRequest(req)
req.Write(writer)
}
buffer.Reset()
}
}

func init() {
Register("http_request_headers", new(HttpToxic))
}
79 changes: 79 additions & 0 deletions toxics/http_request_headers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package toxics_test

import (
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"strings"
"testing"

"github.com/Shopify/toxiproxy/toxics"
)

func echoRequestHeaders(w http.ResponseWriter, r *http.Request) {
headersMap := map[string]string{}

for k, v := range r.Header {
// headers can contain multiple elements. for the purposes of this test we pick the 1st
headersMap[k] = v[0]
}

mapAsJson, _ := json.Marshal(headersMap)
w.Write([]byte(mapAsJson))
}

func TestToxicAddsHTTPHeaders(t *testing.T) {
http.HandleFunc("/", echoRequestHeaders)

ln, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatal("Failed to create TCP server", err)
}

go http.Serve(ln, nil)
defer ln.Close()

proxy := NewTestProxy("test", ln.Addr().String())
proxy.Start()
defer proxy.Stop()

resp, err := http.Get("http://" + proxy.Listen)
if err != nil {
t.Error("Failed to connect to proxy", err)
}

body, err := ioutil.ReadAll(resp.Body)

AssertDoesNotContainHeader(t, string(body), "Foo", "Bar")
AssertDoesNotContainHeader(t, string(body), "Lorem", "Ipsum")

proxy.Toxics.AddToxicJson(ToxicToJson(t, "", "http_request_headers", "upstream", &toxics.HttpToxic{Headers: map[string]string{"Foo": "Bar", "Lorem": "Ipsum"}}))

resp, err = http.Get("http://" + proxy.Listen)
if err != nil {
t.Error("Failed to connect to proxy", err)
}

body, err = ioutil.ReadAll(resp.Body)

AssertContainsHeader(t, string(body), "Foo", "Bar")
AssertContainsHeader(t, string(body), "Lorem", "Ipsum")
}

func AssertDoesNotContainHeader(t *testing.T, body string, headerKey string, headerValue string) {
containsHeader := strings.Contains(string(body), fmt.Sprintf(`"%s":"%s"`, headerKey, headerValue))

if containsHeader {
t.Errorf("Unexpected header found. Header=%s", headerKey)
}
}

func AssertContainsHeader(t *testing.T, body string, headerKey string, headerValue string) {
containsHeader := strings.Contains(string(body), fmt.Sprintf(`"%s":"%s"`, headerKey, headerValue))

if !containsHeader {
t.Errorf("Expected header not found. Header=%s", headerKey)
}
}