Skip to content

Commit

Permalink
Merge branch 'main' into db-uri
Browse files Browse the repository at this point in the history
  • Loading branch information
josh committed Feb 12, 2025
2 parents 0ee62ab + 9ee6cc5 commit 5a51ee7
Show file tree
Hide file tree
Showing 8 changed files with 303 additions and 30 deletions.
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,19 @@ Replays all webhook events within a specified time range.
- The webhook payload is preserved exactly as it was in the original event
- Range replay has a default limit of 100 events (can be overridden with `limit` parameter)

### Prometheus Metrics

```
GET /metrics
```

Exposes Prometheus metrics endpoint for monitoring the application's performance and behavior.

The metrics endpoint provides standard Go metrics including:
- Webhook events counts for IP blocks, signature errors, stored and forwarded counts
- HTTP request counts and errors
- Go runtime metrics (memory usage, garbage collection, goroutines)

## Configuration

HubProxy can be configured using either command-line flags or a YAML configuration file, with sensitive values like secrets being managed through environment variables. When both configuration methods are used, command-line flags take precedence over the configuration file.
Expand Down Expand Up @@ -522,7 +535,7 @@ hubproxy --config config.yaml
Most configuration options can also be set via command-line flags:

- `--config`: Path to config file (optional)
- `--target`: Target URL to forward webhooks to
- `--target-url`: Target URL to forward webhooks to
- `--log-level`: Log level (debug, info, warn, error)
- `--validate-ip`: Validate that requests come from GitHub IPs
- `--ts-authkey`: Tailscale auth key for tsnet
Expand Down
34 changes: 15 additions & 19 deletions cmd/hubproxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"hubproxy/internal/webhook"
"log/slog"

"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"tailscale.com/tsnet"
Expand Down Expand Up @@ -46,15 +47,6 @@ and your target services.`,
}
}

// Always validate target URL
targetURL := viper.GetString("target-url")
if targetURL == "" {
return fmt.Errorf("target URL is required")
}
if _, err := url.Parse(targetURL); err != nil {
return fmt.Errorf("invalid target URL: %w", err)
}

// Skip server startup in test mode
if viper.GetBool("test-mode") {
return nil
Expand Down Expand Up @@ -106,15 +98,18 @@ func run() error {
}
logger.Info("using webhook secret from environment", "secret", secret)

// Validate flags
if viper.GetString("target-url") == "" {
return fmt.Errorf("target URL is required")
}

// Parse target URL
targetURL, err := url.Parse(viper.GetString("target-url"))
if err != nil {
return fmt.Errorf("invalid target URL: %w", err)
// Get target URL if provided
targetURL := viper.GetString("target-url")
if targetURL != "" {
// Parse target URL
parsedURL, err := url.Parse(targetURL)
if err != nil {
return fmt.Errorf("invalid target URL: %w", err)
}
targetURL = parsedURL.String()
logger.Info("forwarding webhooks to target URL", "url", targetURL)
} else {
logger.Info("running in log-only mode (no target URL specified)")
}

// Initialize storage if DB URI is provided
Expand All @@ -136,7 +131,7 @@ func run() error {
// Create webhook handler
webhookHandler := webhook.NewHandler(webhook.Options{
Secret: viper.GetString("webhook-secret"),
TargetURL: targetURL.String(),
TargetURL: targetURL,
Logger: logger,
Store: store,
ValidateIP: viper.GetBool("validate-ip"),
Expand All @@ -152,6 +147,7 @@ func run() error {
mux.Handle("/api/stats", http.HandlerFunc(apiHandler.GetStats))
mux.Handle("/api/events/", http.HandlerFunc(apiHandler.ReplayEvent)) // Handle replay endpoint
mux.Handle("/api/replay", http.HandlerFunc(apiHandler.ReplayRange)) // Handle range replay
mux.Handle("/metrics", promhttp.Handler()) // Add Prometheus metrics endpoint

// Start server
var srv *http.Server
Expand Down
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/jackc/pgx/v5 v5.7.2
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.24
github.com/prometheus/client_golang v1.20.5
github.com/spf13/cobra v1.8.1
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.10.0
Expand All @@ -35,7 +36,9 @@ require (
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect
github.com/aws/smithy-go v1.19.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.13.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/coder/websocket v1.8.12 // indirect
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
Expand Down Expand Up @@ -75,10 +78,14 @@ require (
github.com/miekg/dns v1.1.58 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus-community/pro-bing v0.4.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/safchain/ethtool v0.3.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
Expand Down Expand Up @@ -115,6 +122,7 @@ require (
golang.org/x/tools v0.29.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
google.golang.org/protobuf v1.35.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 // indirect
Expand Down
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,12 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGz
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U=
github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk=
github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso=
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
Expand Down Expand Up @@ -133,6 +137,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
Expand Down Expand Up @@ -172,10 +178,14 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4=
github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
Expand Down
46 changes: 43 additions & 3 deletions internal/cmd/dev/testserver/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"io"
"log"
"net"
"net/http"
"os"
"time"
Expand All @@ -12,24 +13,31 @@ import (
)

var port string
var socketPath string

func newRootCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "testserver",
Short: "Test server for HubProxy",
Long: "A simple HTTP server that logs incoming requests for testing HubProxy",
RunE: func(cmd *cobra.Command, args []string) error {
return run()
if socketPath != "" {
return runUnixSocket(socketPath)
} else if port != "" {
return runPort(port)
}
return fmt.Errorf("no port or socket specified")
},
}

flags := cmd.Flags()
flags.StringVarP(&port, "port", "p", "8082", "Port to listen on")
flags.StringVarP(&port, "port", "p", "", "Port to listen on")
flags.StringVarP(&socketPath, "unix-socket", "s", "", "Unix socket to listen on")

return cmd
}

func run() error {
func installHTTPHandler() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
log.Printf("Received %s request from %s", r.Method, r.RemoteAddr)
log.Printf("Headers: %v", r.Header)
Expand All @@ -45,6 +53,10 @@ func run() error {
log.Printf("Body: %s", string(body))
w.WriteHeader(http.StatusOK)
})
}

func runPort(port string) error {
installHTTPHandler()

srv := &http.Server{
Addr: ":" + port,
Expand All @@ -58,6 +70,34 @@ func run() error {
return srv.ListenAndServe()
}

func runUnixSocket(socketPath string) error {
if err := os.Remove(socketPath); err != nil && !os.IsNotExist(err) {
return err
}

installHTTPHandler()

srv := &http.Server{
Handler: http.DefaultServeMux,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}

listener, err := net.Listen("unix", socketPath)
if err != nil {
return err
}
defer listener.Close()

if err := os.Chmod(socketPath, 0666); err != nil {
return err
}

log.Printf("Test server listening on unix socket: %s", socketPath)
return srv.Serve(listener)
}

func main() {
if err := newRootCmd().Execute(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
Expand Down
109 changes: 109 additions & 0 deletions internal/integration/webhook_test.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package integration

import (
"bufio"
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"

"log/slog"

Expand Down Expand Up @@ -125,3 +128,109 @@ func TestWebhookIntegration(t *testing.T) {
assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode)
})
}

func TestWebhookUnixSocket(t *testing.T) {
// Test configuration
secret := "test-secret"
payload := []byte(`{"action": "test", "repository": {"full_name": "test/repo"}}`)
socketPath := "/tmp/hubproxy-test.sock"

// Initialize test database
store := SetupTestDB(t)
defer store.Close()

// Clean up socket file if it exists
os.Remove(socketPath)

// Create Unix socket listener
listener, err := net.Listen("unix", socketPath)
require.NoError(t, err)
defer listener.Close()
defer os.Remove(socketPath)

// Channel to receive forwarded request data
receivedCh := make(chan []byte, 1)

// Start Unix socket server
go func() {
conn, errAccept := listener.Accept()
if errAccept != nil {
t.Logf("Error accepting connection: %v", errAccept)
return
}
defer conn.Close()

// Read HTTP request
bufReader := bufio.NewReader(conn)
req, errRead := http.ReadRequest(bufReader)
if errRead != nil {
t.Logf("Error reading request: %v", errRead)
return
}

// Verify headers
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
assert.Equal(t, "push", req.Header.Get("X-GitHub-Event"))

// Read body
body, errBody := io.ReadAll(req.Body)
if errBody != nil {
t.Logf("Error reading body: %v", errBody)
return
}

// Send response
resp := http.Response{
StatusCode: http.StatusOK,
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(http.Header),
}
resp.Write(conn)

// Send received data to channel
receivedCh <- body
}()

// Create webhook handler with Unix socket target
handler := webhook.NewHandler(webhook.Options{
Secret: secret,
TargetURL: "unix://" + socketPath,
Logger: slog.New(slog.NewJSONHandler(os.Stdout, nil)),
Store: store,
})

// Create test server with webhook handler
server := httptest.NewServer(handler)
defer server.Close()

// Calculate signature
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
signature := "sha256=" + hex.EncodeToString(mac.Sum(nil))

// Create request
req, err := http.NewRequest("POST", server.URL, bytes.NewReader(payload))
require.NoError(t, err)

// Set headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-GitHub-Event", "push")
req.Header.Set("X-GitHub-Delivery", "test-delivery-id")
req.Header.Set("X-Hub-Signature-256", signature)

// Send request
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()

assert.Equal(t, http.StatusOK, resp.StatusCode)

// Wait for forwarded request
select {
case receivedData := <-receivedCh:
assert.Equal(t, payload, receivedData)
case <-time.After(5 * time.Second):
t.Fatal("Timeout waiting for forwarded request")
}
}
Loading

0 comments on commit 5a51ee7

Please sign in to comment.