Skip to content

Commit

Permalink
Merge pull request #13 from cased/target-socket
Browse files Browse the repository at this point in the history
Support target unix socket
  • Loading branch information
tnm authored Feb 12, 2025
2 parents 3addb97 + c245989 commit 9ee6cc5
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 11 deletions.
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")
}
}
24 changes: 22 additions & 2 deletions internal/webhook/handler.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package webhook

import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log/slog"
"net"
"net/http"
"strings"
"time"
Expand Down Expand Up @@ -78,10 +80,20 @@ func NewHandler(opts Options) *Handler {
ipValidator = security.NewIPValidator(1*time.Hour, false) // Update IP ranges every hour
}

transport := &http.Transport{}

// Swap out HTTP client transport to use Unix socket
if strings.HasPrefix(opts.TargetURL, "unix://") {
socketPath := strings.TrimPrefix(opts.TargetURL, "unix://")
transport.DialContext = func(ctx context.Context, _, addr string) (net.Conn, error) {
return net.Dial("unix", socketPath)
}
}

return &Handler{
secret: opts.Secret,
targetURL: opts.TargetURL,
httpClient: &http.Client{},
httpClient: &http.Client{Transport: transport},
logger: opts.Logger,
ipValidator: ipValidator,
store: opts.Store,
Expand Down Expand Up @@ -171,7 +183,15 @@ func (h *Handler) Forward(payload []byte, headers http.Header) error {
return nil
}

req, err := http.NewRequest(http.MethodPost, h.targetURL, strings.NewReader(string(payload)))
var targetURL string
// http.NewRequest still needs a valid http URI, make a fake one for unix socket path
if strings.HasPrefix(h.targetURL, "unix://") {
targetURL = "http://127.0.0.1/webhook"
} else {
targetURL = h.targetURL
}

req, err := http.NewRequest(http.MethodPost, targetURL, strings.NewReader(string(payload)))
if err != nil {
return fmt.Errorf("creating request: %w", err)
}
Expand Down
35 changes: 29 additions & 6 deletions tools/dev.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,23 @@ set -e
DEFAULT_SECRET="dev-secret"
DEFAULT_TARGET_PORT=8082
DB_PATH=".cache/hubproxy.db"
UNIX_SOCKET=""

# Parse arguments
while [[ "$#" -gt 0 ]]; do
case $1 in
--target-port) TARGET_PORT="$2"; shift ;;
--unix-socket) UNIX_SOCKET="$2"; shift ;;
--help)
echo "Usage: $0 [--target-port PORT]"
echo "Usage: $0 [--target-port PORT] [--unix-socket SOCKET]"
echo "Starts HubProxy development environment with:"
echo " 1. SQLite database"
echo " 2. Test server (for receiving forwarded webhooks)"
echo " 3. Webhook proxy"
echo ""
echo "Options:"
echo " --target-port Port for test server (default: 8082)"
echo " --unix-socket Unix socket for test server"
exit 0
;;
*) echo "Unknown parameter: $1"; exit 1 ;;
Expand All @@ -42,6 +45,9 @@ cleanup() {
fi
# Kill any existing proxy processes
pkill -f "hubproxy.*--target" || true
if [ -e "$UNIX_SOCKET" ]; then
rm "$UNIX_SOCKET"
fi
}

# Set up cleanup on script exit
Expand All @@ -60,18 +66,35 @@ echo "Using webhook secret: $HUBPROXY_WEBHOOK_SECRET (default: $DEFAULT_SECRET)"

# Start the test server in the background
echo "Starting test server..."
go run internal/cmd/dev/testserver/main.go --port $TARGET_PORT > .cache/testserver.log 2>&1 &
if [ -n "$UNIX_SOCKET" ]; then
go run internal/cmd/dev/testserver/main.go --unix-socket "$UNIX_SOCKET" > .cache/testserver.log 2>&1 &
else
go run internal/cmd/dev/testserver/main.go --port $TARGET_PORT > .cache/testserver.log 2>&1 &
fi
TEST_SERVER_PID=$!

# Wait for test server to be ready
echo "Waiting for test server..."
sleep 2
if [ -n "$UNIX_SOCKET" ]; then
echo "Waiting for Unix socket to be created..."
while [ ! -e "$UNIX_SOCKET" ]; do
sleep 0.1
done
else
echo "Waiting for test server..."
sleep 2
fi

# Start the proxy
echo "Starting webhook proxy..."
if [ -n "$UNIX_SOCKET" ]; then
TARGET_URL="unix://$UNIX_SOCKET"
else
TARGET_URL="http://localhost:$TARGET_PORT"
fi

go run cmd/hubproxy/main.go \
--target "http://localhost:$TARGET_PORT" \
--db sqlite \
--target-url "$TARGET_URL" \
--db-type sqlite \
--db-dsn "$DB_PATH" \
--validate-ip=false \
--log-level debug

0 comments on commit 9ee6cc5

Please sign in to comment.