diff --git a/http.go b/http.go index cb6bd84..fdfe244 100644 --- a/http.go +++ b/http.go @@ -51,9 +51,9 @@ type HTTP struct { WebhookVerification *webhookVerification `json:"webhook_verification,omitempty"` - RequestHeader *httpHeaders `json:"request_header,omitempty"` + RequestHeader *httpRequestHeaders `json:"request_header,omitempty"` - ResponseHeader *httpHeaders `json:"header,omitempty"` + ResponseHeader *httpResponseHeaders `json:"header,omitempty"` l *zap.Logger } @@ -154,28 +154,20 @@ func (t *HTTP) provisionOpts(ctx caddy.Context) error { t.opts = append(t.opts, t.WebhookVerification.opt) } - if t.WebhookVerification != nil { - err := t.WebhookVerification.Provision(ctx) - if err != nil { - return fmt.Errorf("provisioning webhook_verification: %v", err) - } - t.opts = append(t.opts, t.WebhookVerification.WebhookVerificationOption) - } - if t.RequestHeader != nil { - requestHeaderOpts, err := t.RequestHeader.provisionRequestHeaders() + err := t.RequestHeader.Provision(ctx) if err != nil { return fmt.Errorf("provisioning request_header: %v", err) } - t.opts = append(t.opts, requestHeaderOpts...) + t.opts = append(t.opts, t.RequestHeader.Opts...) } if t.ResponseHeader != nil { - responseHeaderOpts, err := t.ResponseHeader.provisionResponseHeaders() + err := t.ResponseHeader.Provision(ctx) if err != nil { return fmt.Errorf("provisioning header: %v", err) } - t.opts = append(t.opts, responseHeaderOpts...) + t.opts = append(t.opts, t.ResponseHeader.Opts...) } return nil @@ -458,8 +450,8 @@ func (t *HTTP) unmarshalWebhookVerification(d *caddyfile.Dispenser) error { } func (t *HTTP) unmarshalRequestHeader(d *caddyfile.Dispenser) error { - requestHeader := httpHeaders{} - err := requestHeader.unmarshalHeaders(d) + requestHeader := httpRequestHeaders{} + err := requestHeader.UnmarshalCaddyfile(d) if err != nil { return d.Errf(`parsing request_header %w`, err) } @@ -470,8 +462,8 @@ func (t *HTTP) unmarshalRequestHeader(d *caddyfile.Dispenser) error { } func (t *HTTP) unmarshalResponseHeader(d *caddyfile.Dispenser) error { - responseHeader := httpHeaders{} - err := responseHeader.unmarshalHeaders(d) + responseHeader := httpResponseHeaders{} + err := responseHeader.UnmarshalCaddyfile(d) if err != nil { return d.Errf(`parsing header %w`, err) } diff --git a/http_headers.go b/http_headers.go index 4b8d72e..14dceeb 100644 --- a/http_headers.go +++ b/http_headers.go @@ -10,39 +10,14 @@ import ( ) type httpHeaders struct { - opts []config.HTTPEndpointOption + Opts []config.HTTPEndpointOption Added map[string]string `json:"added,omitempty"` Removed []string `json:"removed,omitempty"` } -func (h *httpHeaders) provisionRequestHeaders() ([]config.HTTPEndpointOption, error) { - - for name, value := range h.Added { - h.opts = append(h.opts, config.WithRequestHeader(name, value)) - } - - for _, name := range h.Removed { - h.opts = append(h.opts, config.WithRemoveRequestHeader(name)) - } - - return h.opts, nil -} - -func (h *httpHeaders) provisionResponseHeaders() ([]config.HTTPEndpointOption, error) { - - for name, value := range h.Added { - h.opts = append(h.opts, config.WithResponseHeader(name, value)) - } - - for _, name := range h.Removed { - h.opts = append(h.opts, config.WithRemoveResponseHeader(name)) - } - - return h.opts, nil -} - -func (h *httpHeaders) doReplace(repl *caddy.Replacer) error { +func (h *httpHeaders) doReplace() { + repl := caddy.NewReplacer() replacedAddedHeaders := make(map[string]string, len(h.Added)) @@ -66,10 +41,9 @@ func (h *httpHeaders) doReplace(repl *caddy.Replacer) error { h.Removed = replacedRemovedHeaders - return nil } -func (h *httpHeaders) unmarshalHeaders(d *caddyfile.Dispenser) error { +func (h *httpHeaders) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for d.Next() { // first see if headers are in the initial line var hasArgs bool @@ -77,9 +51,10 @@ func (h *httpHeaders) unmarshalHeaders(d *caddyfile.Dispenser) error { hasArgs = true field := d.Val() var value string - if !d.AllArgs(&value) { - d.ArgErr() // Additional arg Would be replacement if ngrok handled that + if d.CountRemainingArgs() > 1 { + return d.ArgErr() // Additional arg Would be replacement if ngrok handled that } + d.Args(&value) err := h.applyHeaderOp( field, value, @@ -102,9 +77,10 @@ func (h *httpHeaders) unmarshalHeaders(d *caddyfile.Dispenser) error { field = strings.TrimSuffix(field, ":") var value string - if !d.AllArgs(&value) { - d.ArgErr() // Additional arg Would be replacement if ngrok handled that + if d.CountRemainingArgs() > 1 { + return d.ArgErr() // Additional arg Would be replacement if ngrok handled that } + d.Args(&value) err := h.applyHeaderOp( field, value, diff --git a/http_headers_test.go b/http_headers_test.go deleted file mode 100644 index 6ab6330..0000000 --- a/http_headers_test.go +++ /dev/null @@ -1,122 +0,0 @@ -package ngroklistener - -import ( - "reflect" - "testing" - - "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" -) - -func TestHTTPHeaders(t *testing.T) { - class := "HTTPHeaders" - - tests := []struct { - name string - input string - shouldErr bool - expected httpHeaders - }{ - { - name: "absent", - input: `{}`, - shouldErr: false, - expected: httpHeaders{Added: nil, Removed: nil}, - }, - { - name: "add header inline", - input: `header foo bar`, - shouldErr: false, - expected: httpHeaders{Added: map[string]string{"foo": "bar"}, Removed: nil}, - }, - { - name: "remove header inline", - input: `header -baz`, - shouldErr: false, - expected: httpHeaders{Added: nil, Removed: []string{"baz"}}, - }, - { - name: "add headers inline", - input: ` - header foo bar - header spam eggs - `, - shouldErr: false, - expected: httpHeaders{Added: map[string]string{"foo": "bar", "spam": "eggs"}, Removed: nil}, - }, - { - name: "remove headers inline", - input: ` - header -qas - header -wex - `, - shouldErr: false, - expected: httpHeaders{Added: nil, Removed: []string{"qas", "wex"}}, - }, - { - name: "add header block", - input: `header { - foo bar - }`, - shouldErr: false, - expected: httpHeaders{Added: map[string]string{"foo": "bar"}, Removed: nil}, - }, - { - name: "remove header block", - input: `header { - -baz - }`, - shouldErr: false, - expected: httpHeaders{Added: nil, Removed: []string{"baz"}}, - }, - { - name: "add headers block", - input: `header { - foo bar - spam eggs - }`, - shouldErr: false, - expected: httpHeaders{Added: map[string]string{"foo": "bar", "spam": "eggs"}, Removed: nil}, - }, - { - name: "remove headers block", - input: `header { - -qas - -wex - }`, - shouldErr: false, - expected: httpHeaders{Added: nil, Removed: []string{"qas", "wex"}}, - }, - { - name: "add and remove headers mixed", - input: `header { - -wex - foo bar - - } - header spam eggs - header -qas`, - shouldErr: false, - expected: httpHeaders{Added: map[string]string{"foo": "bar", "spam": "eggs"}, Removed: []string{"wex", "qas"}}, - }, - } - - for i, test := range tests { - d := caddyfile.NewTestDispenser(test.input) - httpHeaders := httpHeaders{} - err := httpHeaders.unmarshalHeaders(d) - - if test.shouldErr { - if err == nil { - t.Errorf("Test %v (%v) %v: Expected error but found nil", class, i, test.name) - } - } else { - if err != nil { - t.Errorf("Test %v (%v) %v: Expected no error but found error: %v", class, i, test.name, err) - } else if !reflect.DeepEqual(test.expected.Added, httpHeaders.Added) { - t.Errorf("Test %v (%v) %v: Created Headers (\n%#v\n) does not match expected (\n%#v\n)", class, i, test.name, httpHeaders.Added, test.expected.Added) - } else if !reflect.DeepEqual(test.expected.Removed, httpHeaders.Removed) { - t.Errorf("Test %v (%v) %v: Created Headers (\n%#v\n) does not match expected (\n%#v\n)", class, i, test.name, httpHeaders.Removed, test.expected.Removed) - } - } - } -} diff --git a/http_request_headers.go b/http_request_headers.go new file mode 100644 index 0000000..5c5a567 --- /dev/null +++ b/http_request_headers.go @@ -0,0 +1,24 @@ +package ngroklistener + +import ( + "github.com/caddyserver/caddy/v2" + "golang.ngrok.com/ngrok/config" +) + +type httpRequestHeaders struct { + httpHeaders +} + +func (h *httpRequestHeaders) Provision(caddy.Context) error { + h.doReplace() + + for name, value := range h.Added { + h.Opts = append(h.Opts, config.WithRequestHeader(name, value)) + } + + for _, name := range h.Removed { + h.Opts = append(h.Opts, config.WithRemoveRequestHeader(name)) + } + + return nil +} diff --git a/http_request_headers_test.go b/http_request_headers_test.go new file mode 100644 index 0000000..ca0cf32 --- /dev/null +++ b/http_request_headers_test.go @@ -0,0 +1,174 @@ +package ngroklistener + +import ( + "testing" + + "github.com/stretchr/testify/require" + "golang.ngrok.com/ngrok/config" +) + +func TestHTTPRequestHeaders(t *testing.T) { + cases := genericNgrokTestCases[*httpRequestHeaders]{ + { + name: "absent", + caddyInput: `{}`, + expectConfig: func(t *testing.T, actual *httpRequestHeaders) { + require.Nil(t, actual.Opts) + }, + expectedOptsFunc: func(t *testing.T, actual *httpRequestHeaders) { + config.HTTPEndpoint() + }, + }, + { + name: "add header inline", + caddyInput: `header foo bar`, + expectConfig: func(t *testing.T, actual *httpRequestHeaders) { + require.Contains(t, actual.Added, "foo") + require.Equal(t, actual.Added["foo"], "bar") + }, + expectedOptsFunc: func(t *testing.T, actual *httpRequestHeaders) { + config.HTTPEndpoint( + config.WithRequestHeader("foo", "bar"), + ) + }, + }, + { + name: "remove header inline", + caddyInput: `header -baz`, + expectConfig: func(t *testing.T, actual *httpRequestHeaders) { + require.ElementsMatch(t, actual.Removed, []string{"baz"}) + }, + expectedOptsFunc: func(t *testing.T, actual *httpRequestHeaders) { + config.HTTPEndpoint( + config.WithRemoveRequestHeader("baz"), + ) + }, + }, + { + name: "add headers inline", + caddyInput: ` + header foo bar + header spam eggs + `, + expectConfig: func(t *testing.T, actual *httpRequestHeaders) { + require.Contains(t, actual.Added, "foo") + require.Equal(t, actual.Added["foo"], "bar") + require.Contains(t, actual.Added, "spam") + require.Equal(t, actual.Added["spam"], "eggs") + }, + expectedOptsFunc: func(t *testing.T, actual *httpRequestHeaders) { + config.HTTPEndpoint( + config.WithRequestHeader("foo", "bar"), + config.WithRequestHeader("spam", "eggs"), + ) + }, + }, + { + name: "remove headers inline", + caddyInput: ` + header -qas + header -wex + `, + expectConfig: func(t *testing.T, actual *httpRequestHeaders) { + require.ElementsMatch(t, actual.Removed, []string{"qas", "wex"}) + }, + expectedOptsFunc: func(t *testing.T, actual *httpRequestHeaders) { + config.HTTPEndpoint( + config.WithRemoveRequestHeader("qas"), + config.WithRemoveRequestHeader("wex"), + ) + }, + }, + { + name: "add header block", + caddyInput: `header { + foo bar + }`, + expectConfig: func(t *testing.T, actual *httpRequestHeaders) { + require.Contains(t, actual.Added, "foo") + require.Equal(t, actual.Added["foo"], "bar") + }, + expectedOptsFunc: func(t *testing.T, actual *httpRequestHeaders) { + config.HTTPEndpoint( + config.WithRequestHeader("foo", "bar"), + ) + }, + }, + { + name: "remove header block", + caddyInput: `header { + -baz + }`, + expectConfig: func(t *testing.T, actual *httpRequestHeaders) { + require.ElementsMatch(t, actual.Removed, []string{"baz"}) + }, + expectedOptsFunc: func(t *testing.T, actual *httpRequestHeaders) { + config.HTTPEndpoint( + config.WithRemoveRequestHeader("baz"), + ) + }, + }, + { + name: "add headers block", + caddyInput: `header { + foo bar + spam eggs + }`, + expectConfig: func(t *testing.T, actual *httpRequestHeaders) { + require.Contains(t, actual.Added, "foo") + require.Equal(t, actual.Added["foo"], "bar") + require.Contains(t, actual.Added, "spam") + require.Equal(t, actual.Added["spam"], "eggs") + }, + expectedOptsFunc: func(t *testing.T, actual *httpRequestHeaders) { + config.HTTPEndpoint( + config.WithRequestHeader("foo", "bar"), + config.WithRequestHeader("spam", "eggs"), + ) + }, + }, + { + name: "remove headers block", + caddyInput: `header { + -qas + -wex + }`, + expectConfig: func(t *testing.T, actual *httpRequestHeaders) { + require.ElementsMatch(t, actual.Removed, []string{"qas", "wex"}) + }, + expectedOptsFunc: func(t *testing.T, actual *httpRequestHeaders) { + config.HTTPEndpoint( + config.WithRemoveRequestHeader("qas"), + config.WithRemoveRequestHeader("wex"), + ) + }, + }, + { + name: "add and remove headers mixed", + caddyInput: `header { + -wex + foo bar + + } + header spam eggs + header -qas`, + expectConfig: func(t *testing.T, actual *httpRequestHeaders) { + require.Contains(t, actual.Added, "foo") + require.Equal(t, actual.Added["foo"], "bar") + require.Contains(t, actual.Added, "spam") + require.Equal(t, actual.Added["spam"], "eggs") + require.ElementsMatch(t, actual.Removed, []string{"qas", "wex"}) + }, + expectedOptsFunc: func(t *testing.T, actual *httpRequestHeaders) { + config.HTTPEndpoint( + config.WithRequestHeader("foo", "bar"), + config.WithRequestHeader("spam", "eggs"), + config.WithRemoveRequestHeader("qas"), + config.WithRemoveRequestHeader("wex"), + ) + }, + }, + } + + cases.runAll(t) +} diff --git a/http_response_headers.go b/http_response_headers.go new file mode 100644 index 0000000..56e28da --- /dev/null +++ b/http_response_headers.go @@ -0,0 +1,24 @@ +package ngroklistener + +import ( + "github.com/caddyserver/caddy/v2" + "golang.ngrok.com/ngrok/config" +) + +type httpResponseHeaders struct { + httpHeaders +} + +func (h *httpResponseHeaders) Provision(caddy.Context) error { + h.doReplace() + + for name, value := range h.Added { + h.Opts = append(h.Opts, config.WithResponseHeader(name, value)) + } + + for _, name := range h.Removed { + h.Opts = append(h.Opts, config.WithRemoveResponseHeader(name)) + } + + return nil +} diff --git a/http_response_headers_test.go b/http_response_headers_test.go new file mode 100644 index 0000000..51cada1 --- /dev/null +++ b/http_response_headers_test.go @@ -0,0 +1,174 @@ +package ngroklistener + +import ( + "testing" + + "github.com/stretchr/testify/require" + "golang.ngrok.com/ngrok/config" +) + +func TestHTTPResponseHeaders(t *testing.T) { + cases := genericNgrokTestCases[*httpResponseHeaders]{ + { + name: "absent", + caddyInput: `{}`, + expectConfig: func(t *testing.T, actual *httpResponseHeaders) { + require.Nil(t, actual.Opts) + }, + expectedOptsFunc: func(t *testing.T, actual *httpResponseHeaders) { + config.HTTPEndpoint() + }, + }, + { + name: "add header inline", + caddyInput: `header foo bar`, + expectConfig: func(t *testing.T, actual *httpResponseHeaders) { + require.Contains(t, actual.Added, "foo") + require.Equal(t, actual.Added["foo"], "bar") + }, + expectedOptsFunc: func(t *testing.T, actual *httpResponseHeaders) { + config.HTTPEndpoint( + config.WithResponseHeader("foo", "bar"), + ) + }, + }, + { + name: "remove header inline", + caddyInput: `header -baz`, + expectConfig: func(t *testing.T, actual *httpResponseHeaders) { + require.ElementsMatch(t, actual.Removed, []string{"baz"}) + }, + expectedOptsFunc: func(t *testing.T, actual *httpResponseHeaders) { + config.HTTPEndpoint( + config.WithRemoveResponseHeader("baz"), + ) + }, + }, + { + name: "add headers inline", + caddyInput: ` + header foo bar + header spam eggs + `, + expectConfig: func(t *testing.T, actual *httpResponseHeaders) { + require.Contains(t, actual.Added, "foo") + require.Equal(t, actual.Added["foo"], "bar") + require.Contains(t, actual.Added, "spam") + require.Equal(t, actual.Added["spam"], "eggs") + }, + expectedOptsFunc: func(t *testing.T, actual *httpResponseHeaders) { + config.HTTPEndpoint( + config.WithResponseHeader("foo", "bar"), + config.WithResponseHeader("spam", "eggs"), + ) + }, + }, + { + name: "remove headers inline", + caddyInput: ` + header -qas + header -wex + `, + expectConfig: func(t *testing.T, actual *httpResponseHeaders) { + require.ElementsMatch(t, actual.Removed, []string{"qas", "wex"}) + }, + expectedOptsFunc: func(t *testing.T, actual *httpResponseHeaders) { + config.HTTPEndpoint( + config.WithRemoveResponseHeader("qas"), + config.WithRemoveResponseHeader("wex"), + ) + }, + }, + { + name: "add header block", + caddyInput: `header { + foo bar + }`, + expectConfig: func(t *testing.T, actual *httpResponseHeaders) { + require.Contains(t, actual.Added, "foo") + require.Equal(t, actual.Added["foo"], "bar") + }, + expectedOptsFunc: func(t *testing.T, actual *httpResponseHeaders) { + config.HTTPEndpoint( + config.WithResponseHeader("foo", "bar"), + ) + }, + }, + { + name: "remove header block", + caddyInput: `header { + -baz + }`, + expectConfig: func(t *testing.T, actual *httpResponseHeaders) { + require.ElementsMatch(t, actual.Removed, []string{"baz"}) + }, + expectedOptsFunc: func(t *testing.T, actual *httpResponseHeaders) { + config.HTTPEndpoint( + config.WithRemoveResponseHeader("baz"), + ) + }, + }, + { + name: "add headers block", + caddyInput: `header { + foo bar + spam eggs + }`, + expectConfig: func(t *testing.T, actual *httpResponseHeaders) { + require.Contains(t, actual.Added, "foo") + require.Equal(t, actual.Added["foo"], "bar") + require.Contains(t, actual.Added, "spam") + require.Equal(t, actual.Added["spam"], "eggs") + }, + expectedOptsFunc: func(t *testing.T, actual *httpResponseHeaders) { + config.HTTPEndpoint( + config.WithResponseHeader("foo", "bar"), + config.WithResponseHeader("spam", "eggs"), + ) + }, + }, + { + name: "remove headers block", + caddyInput: `header { + -qas + -wex + }`, + expectConfig: func(t *testing.T, actual *httpResponseHeaders) { + require.ElementsMatch(t, actual.Removed, []string{"qas", "wex"}) + }, + expectedOptsFunc: func(t *testing.T, actual *httpResponseHeaders) { + config.HTTPEndpoint( + config.WithRemoveResponseHeader("qas"), + config.WithRemoveResponseHeader("wex"), + ) + }, + }, + { + name: "add and remove headers mixed", + caddyInput: `header { + -wex + foo bar + + } + header spam eggs + header -qas`, + expectConfig: func(t *testing.T, actual *httpResponseHeaders) { + require.Contains(t, actual.Added, "foo") + require.Equal(t, actual.Added["foo"], "bar") + require.Contains(t, actual.Added, "spam") + require.Equal(t, actual.Added["spam"], "eggs") + require.ElementsMatch(t, actual.Removed, []string{"qas", "wex"}) + }, + expectedOptsFunc: func(t *testing.T, actual *httpResponseHeaders) { + config.HTTPEndpoint( + config.WithResponseHeader("foo", "bar"), + config.WithResponseHeader("spam", "eggs"), + config.WithRemoveResponseHeader("qas"), + config.WithRemoveResponseHeader("wex"), + ) + }, + }, + } + + cases.runAll(t) +}