Skip to content

Commit

Permalink
Feature: Allow overwriting the Host header for all request.
Browse files Browse the repository at this point in the history
This is to work around situations where DNS isn't available (yet) but Gotify is behind a reverse-proxy which is routing traffic based on Hostnames. For these scenarios, we can simply use the static IP as the Endpoint and overwrite the `Host` header to what the reverse proxy requires to correctly route the request.
  • Loading branch information
LukasKnuth committed Jul 12, 2024
1 parent e3890c7 commit 2f6b791
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 6 deletions.
24 changes: 22 additions & 2 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,32 @@ type AuthedGotifyClient struct {
auth runtime.ClientAuthInfoWriter
}

func NewAuthedClient(endpoint string, username string, password string) (*AuthedGotifyClient, error) {
type OverwriteHostTransport struct {
Host string
Next http.RoundTripper
}

func (hot *OverwriteHostTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req.Host = hot.Host
return hot.Next.RoundTrip(req)
}

func wrapWithHost(host string, wrap http.RoundTripper) http.RoundTripper {
return &OverwriteHostTransport{Host: host, Next: wrap}
}

func NewAuthedClient(endpoint string, username string, password string, host *string) (*AuthedGotifyClient, error) {
url, err := url.Parse(endpoint)
if err != nil {
return nil, err
}
client := gotify.NewClient(url, &http.Client{})

transport := http.DefaultTransport
if host != nil {
transport = wrapWithHost(*host, transport)
}

client := gotify.NewClient(url, &http.Client{Transport: transport})
auth := auth.BasicAuth(username, password)

return &AuthedGotifyClient{client: client, auth: auth}, nil
Expand Down
66 changes: 66 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package main

import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/gotify/go-api-client/v2/client/application"
)

const (
testHost = "my.coolapp.local"
// the httptest.NewServer always listens on this address

Check failure on line 15 in client_test.go

View workflow job for this annotation

GitHub Actions / Build

Comment should end in a period (godot)

Check failure on line 15 in client_test.go

View workflow job for this annotation

GitHub Actions / Build

Comment should end in a period (godot)
localHost = "127.0.0.1"
)

func TestClientHostOverwrite(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.Host != testHost {
t.Errorf("Expected \"Host\" to be %q, got %q", testHost, req.Host)
}

// Return valid response for test endpoint
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, "[]")
}))
defer ts.Close()

host := testHost
client, err := NewAuthedClient(ts.URL, "test", "test", &host)
if err != nil {
t.Fatalf("Could not construct client: %v", err.Error())
}

params := application.NewGetAppsParams()
_, err = client.client.Application.GetApps(params, client.auth)
if err != nil {
t.Fatalf("Error during test request: %v", err.Error())
}
}

func TestClientHostDefault(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if !strings.HasPrefix(req.Host, localHost) {
t.Errorf("Expected \"Host\" to start with %q, got %q", localHost, req.Host)
}

// Return valid response for test endpoint
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, "[]")
}))
defer ts.Close()

client, err := NewAuthedClient(ts.URL, "test", "test", nil)
if err != nil {
t.Fatalf("Could not construct client: %v", err.Error())
}

params := application.NewGetAppsParams()
_, err = client.client.Application.GetApps(params, client.auth)
if err != nil {
t.Fatalf("Error during test request: %v", err.Error())
}
}
8 changes: 8 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,18 @@ For each client that wants to read messages from Gotify, you want a new `gotify_
## Example Usage

```terraform
# Required configuration
provider "gotify" {
endpoint = "http://my.gotify.local" # or GOTIFY_ENDPOINT
username = "admin" # or GOTIFY_USERNAME
password = "admin" # or GOTIFY_PASSWORD
}
# When Gotify is behind a reverse proxy and DNS isn't setup yet
provider "gotify" {
endpoint = "http://192.168.1.4" # public, static IP of deployment
host_header = "my.gotify.local" # Host header expected by reverse proxy
}
```

<!-- schema generated by tfplugindocs -->
Expand All @@ -29,5 +36,6 @@ provider "gotify" {
### Optional

- `endpoint` (String) Endpoint with Protocol to send requests to.
- `host_header` (String) This is useful when Gotify is deployed behind a reverse proxy and this provider is used in your infrastructure setup where DNS might not be available yet. You can then set the endpoint to an IP address and the Host to what your reverse Proxy expects.
- `password` (String, Sensitive) The Password to authenticate against the server. Gotify's default "admin" user has "admin" as their password.
- `username` (String) The Username to authenticate against the server. Gotify has a default "admin" user
8 changes: 8 additions & 0 deletions examples/provider/provider.tf
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Required configuration
provider "gotify" {
endpoint = "http://my.gotify.local" # or GOTIFY_ENDPOINT
username = "admin" # or GOTIFY_USERNAME
password = "admin" # or GOTIFY_PASSWORD
}

# When Gotify is behind a reverse proxy and DNS isn't setup yet
provider "gotify" {
endpoint = "http://192.168.1.4" # public, static IP of deployment
host_header = "my.gotify.local" # Host header expected by reverse proxy
}

14 changes: 10 additions & 4 deletions provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ type GotifyProvider struct {

// Map Terraform HCL schema to Go types.
type GotifyProviderModel struct {
Endpoint types.String `tfsdk:"endpoint"`
Username types.String `tfsdk:"username"`
Password types.String `tfsdk:"password"`
Endpoint types.String `tfsdk:"endpoint"`
HostHeader types.String `tfsdk:"host_header"`
Username types.String `tfsdk:"username"`
Password types.String `tfsdk:"password"`
}

func (p *GotifyProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) {
Expand Down Expand Up @@ -52,6 +53,11 @@ func (p *GotifyProvider) Schema(_ context.Context, _ provider.SchemaRequest, res
Sensitive: true,
Description: "The Password to authenticate against the server. Gotify's default \"admin\" user has \"admin\" as their password.",
},
"host_header": schema.StringAttribute{
Optional: true,
Description: "Allows overwriting the Host header in all HTTP requests made to the Gotify REST API.",
MarkdownDescription: "This is useful when Gotify is deployed behind a reverse proxy and this provider is used in your infrastructure setup where DNS might not be available yet. You can then set the endpoint to an IP address and the Host to what your reverse Proxy expects.",
},
},
}
}
Expand Down Expand Up @@ -107,7 +113,7 @@ func (p *GotifyProvider) Configure(ctx context.Context, req provider.ConfigureRe
return
}

client, err := NewAuthedClient(endpoint, username, password)
client, err := NewAuthedClient(endpoint, username, password, model.HostHeader.ValueStringPointer())
if err != nil {
resp.Diagnostics.AddError(
"Failed while constructing client", err.Error(),
Expand Down

0 comments on commit 2f6b791

Please sign in to comment.