Skip to content
Closed
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: 4 additions & 2 deletions cmd/pilotctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2969,9 +2969,10 @@ func cmdSetWebhook(args []string) {

// Apply to running daemon (best-effort — daemon may not be running)
applied := false
adminToken := getAdminToken()
d, err := driver.Connect(getSocket())
if err == nil {
_, err = d.SetWebhook(url)
_, err = d.SetWebhook(url, adminToken)
d.Close()
if err == nil {
applied = true
Expand Down Expand Up @@ -3002,9 +3003,10 @@ func cmdClearWebhook() {

// Apply to running daemon (best-effort)
applied := false
adminToken := getAdminToken()
d, err := driver.Connect(getSocket())
if err == nil {
_, err = d.SetWebhook("")
_, err = d.SetWebhook("", adminToken)
d.Close()
if err == nil {
applied = true
Expand Down
27 changes: 26 additions & 1 deletion pkg/daemon/ipc.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package daemon

import (
"context"
"crypto/subtle"
"encoding/binary"
"encoding/json"
"errors"
Expand Down Expand Up @@ -192,6 +193,8 @@ var ErrIPCClosed = errors.New("ipc: connection closed")
// code paths.
var ErrIPCBackpressure = errors.New("ipc: backpressure (client too slow)")

var errSetWebhookAuth = errors.New("ipc: set_webhook requires admin token")

// IPCEnvelopeHeaderSize is the size of the per-message header that sits
// inside the ipcutil length-framed envelope: 1 byte cmd.
const IPCEnvelopeHeaderSize = 1
Expand Down Expand Up @@ -1171,8 +1174,30 @@ func (s *IPCServer) handleRotateKey(conn *ipcConn, reqID uint64) {
}
}

// handleSetWebhook services CmdSetWebhook — admin-token-gated webhook URL change.
// Wire payload: [tokenLen(2)][token...][url...]
//
// On success, replies with CmdSetWebhookOK carrying {"webhook": url}.
// On failure, replies with CmdError.
func (s *IPCServer) handleSetWebhook(conn *ipcConn, reqID uint64, payload []byte) {
url := string(payload) // empty string = clear webhook
// Admin token required: [tokenLen(2)][token...][url...]
if len(payload) < 2 {
s.sendError(conn, reqID, "set_webhook: missing admin token")
return
}
tokenLen := binary.BigEndian.Uint16(payload[0:2])
if len(payload) < 2+int(tokenLen) {
s.sendError(conn, reqID, "set_webhook: truncated admin token")
return
}
if s.daemon.config.AdminToken != "" {
token := string(payload[2 : 2+tokenLen])
if subtle.ConstantTimeCompare([]byte(token), []byte(s.daemon.config.AdminToken)) != 1 {
s.sendError(conn, reqID, "set_webhook: invalid admin token")
return
}
}
url := string(payload[2+tokenLen:]) // empty string = clear webhook
if url != "" {
if err := ValidateWebhookURL(url); err != nil {
s.sendError(conn, reqID, err.Error())
Expand Down
6 changes: 3 additions & 3 deletions pkg/daemon/zz_ipc_simple_handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ func TestHandleSetWebhookEmptyPayloadClearsWebhookReturnsOK(t *testing.T) {
t.Parallel()
_, s := newSimpleHandlerDaemon(t, nil)
ic, client := newIPCTestConn(t)
reply := runHandler(t, client, func() { s.handleSetWebhook(ic, 0, []byte{}) })
reply := runHandler(t, client, func() { s.handleSetWebhook(ic, 0, []byte{0x00, 0x00}) })

if reply[0] != CmdSetWebhookOK {
t.Fatalf("opcode 0x%02X, want CmdSetWebhookOK", reply[0])
Expand All @@ -455,7 +455,7 @@ func TestHandleSetWebhookInvalidURLReturnsError(t *testing.T) {
t.Parallel()
_, s := newSimpleHandlerDaemon(t, nil)
ic, client := newIPCTestConn(t)
reply := runHandler(t, client, func() { s.handleSetWebhook(ic, 0, []byte("ftp://example.com/hook")) })
reply := runHandler(t, client, func() { s.handleSetWebhook(ic, 0, append([]byte{0x00, 0x00}, []byte("ftp://example.com/hook")...)) })
if reply[0] != CmdError {
t.Fatalf("opcode 0x%02X, want CmdError on invalid URL", reply[0])
}
Expand All @@ -466,7 +466,7 @@ func TestHandleSetWebhookValidHTTPSURLAcceptedAndEchoedBack(t *testing.T) {
_, s := newSimpleHandlerDaemon(t, nil)
ic, client := newIPCTestConn(t)
url := "https://example.com/hook"
reply := runHandler(t, client, func() { s.handleSetWebhook(ic, 0, []byte(url)) })
reply := runHandler(t, client, func() { s.handleSetWebhook(ic, 0, append([]byte{0x00, 0x00}, []byte(url)...)) })

if reply[0] != CmdSetWebhookOK {
t.Fatalf("opcode 0x%02X, want CmdSetWebhookOK", reply[0])
Expand Down
6 changes: 3 additions & 3 deletions pkg/daemon/zz_ipc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ func TestHandleSetWebhookEmptyPayloadClearsWebhookAndRepliesOK(t *testing.T) {
s := d.ipc
ic, client := newIPCTestConn(t)

reply := runHandler(t, client, func() { s.handleSetWebhook(ic, 0, nil) })
reply := runHandler(t, client, func() { s.handleSetWebhook(ic, 0, []byte{0x00, 0x00}) })

if reply[0] != CmdSetWebhookOK {
t.Fatalf("reply[0] = 0x%02X, want CmdSetWebhookOK", reply[0])
Expand All @@ -390,7 +390,7 @@ func TestHandleSetWebhookInvalidSchemeSendsError(t *testing.T) {
s := d.ipc
ic, client := newIPCTestConn(t)

reply := runHandler(t, client, func() { s.handleSetWebhook(ic, 0, []byte("ftp://evil.example.com/hook")) })
reply := runHandler(t, client, func() { s.handleSetWebhook(ic, 0, append([]byte{0x00, 0x00}, []byte("ftp://evil.example.com/hook")...)) })
assertErrorReply(t, reply, "scheme")
}

Expand All @@ -402,7 +402,7 @@ func TestHandleSetWebhookValidHTTPSConfiguresAndRepliesOK(t *testing.T) {
s := d.ipc
ic, client := newIPCTestConn(t)

reply := runHandler(t, client, func() { s.handleSetWebhook(ic, 0, []byte("https://example.com/hook")) })
reply := runHandler(t, client, func() { s.handleSetWebhook(ic, 0, append([]byte{0x00, 0x00}, []byte("https://example.com/hook")...)) })

if reply[0] != CmdSetWebhookOK {
t.Fatalf("reply[0] = 0x%02X, want CmdSetWebhookOK", reply[0])
Expand Down
13 changes: 9 additions & 4 deletions pkg/driver/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,11 +323,16 @@ func (d *Driver) SetTags(tags []string) (map[string]interface{}, error) {
}

// SetWebhook sets or clears the daemon's webhook URL at runtime.
// An empty URL disables the webhook.
func (d *Driver) SetWebhook(url string) (map[string]interface{}, error) {
msg := make([]byte, 1+len(url))
// An empty URL disables the webhook. The adminToken must match the
// daemon's configured AdminToken (empty passes through).
// Wire format: [cmd(1)][tokenLen(2)][token...][url...]
func (d *Driver) SetWebhook(url, adminToken string) (map[string]interface{}, error) {
tokenLen := len(adminToken)
msg := make([]byte, 3+tokenLen+len(url))
msg[0] = cmdSetWebhook
copy(msg[1:], url)
binary.BigEndian.PutUint16(msg[1:3], uint16(tokenLen))
copy(msg[3:], adminToken)
copy(msg[3+tokenLen:], url)
return d.jsonRPC(msg, cmdSetWebhookOK, "set_webhook")
}

Expand Down
8 changes: 5 additions & 3 deletions pkg/driver/zz_driver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,7 @@ func TestRegistryWrappersEncodeCorrectly(t *testing.T) {
if _, err := drv.SetTags([]string{"a", "b"}); err != nil {
t.Fatalf("SetTags: %v", err)
}
if _, err := drv.SetWebhook("https://x/y"); err != nil {
if _, err := drv.SetWebhook("https://x/y", ""); err != nil {
t.Fatalf("SetWebhook: %v", err)
}

Expand All @@ -566,8 +566,10 @@ func TestRegistryWrappersEncodeCorrectly(t *testing.T) {
t.Errorf("ResolveHostname host = %q", f[1:])
}
case cmdSetWebhook:
if string(f[1:]) != "https://x/y" {
t.Errorf("SetWebhook url = %q", f[1:])
// wire format: [cmd(1)][tokenLen(2)][token...][url...]
// tokenLen=0, so url starts at f[3:]
if string(f[3:]) != "https://x/y" {
t.Errorf("SetWebhook url = %q", f[3:])
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions tests/zz_ipc_ops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func TestSetWebhookViaIPC(t *testing.T) {
di := env.AddDaemon()

// Set webhook
result, err := di.Driver.SetWebhook("http://localhost:9999/hooks")
result, err := di.Driver.SetWebhook("http://localhost:9999/hooks", "")
if err != nil {
t.Fatalf("set webhook: %v", err)
}
Expand All @@ -169,7 +169,7 @@ func TestSetWebhookViaIPC(t *testing.T) {
}

// Clear webhook
result, err = di.Driver.SetWebhook("")
result, err = di.Driver.SetWebhook("", "")
if err != nil {
t.Fatalf("clear webhook: %v", err)
}
Expand Down
Loading