mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-10-12 04:55:18 +00:00
95 lines
2.6 KiB
Go
95 lines
2.6 KiB
Go
package hooks
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// maxResponseBytes caps a worker response so a misbehaving peer cannot exhaust memory.
|
|
const maxResponseBytes = 32 << 20
|
|
|
|
// Minimal HTTP/1.0 client over a raw TCP socket. Linking net/http adds ~3 MB to
|
|
// every hook binary; hooks only speak plain JSON to the local worker. HTTP/1.0
|
|
// makes the server skip chunked encoding and close the connection, so the body
|
|
// is simply everything after the header block.
|
|
type httpResponse struct {
|
|
StatusText string
|
|
Body []byte
|
|
Status int
|
|
}
|
|
|
|
func httpDo(ctx context.Context, timeout time.Duration, method string, port int, path string, body []byte) (*httpResponse, error) {
|
|
ctx, cancel := context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
|
|
addr := net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
|
|
var d net.Dialer
|
|
conn, err := d.DialContext(ctx, "tcp", addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = conn.Close() }()
|
|
|
|
if dl, ok := ctx.Deadline(); ok {
|
|
_ = conn.SetDeadline(dl)
|
|
}
|
|
stop := context.AfterFunc(ctx, func() { _ = conn.Close() })
|
|
defer stop()
|
|
|
|
var req bytes.Buffer
|
|
fmt.Fprintf(&req, "%s %s HTTP/1.0\r\nHost: %s\r\n", method, path, addr)
|
|
if body != nil {
|
|
fmt.Fprintf(&req, "Content-Type: application/json\r\nContent-Length: %d\r\n", len(body))
|
|
}
|
|
req.WriteString("\r\n")
|
|
req.Write(body)
|
|
if _, err := conn.Write(req.Bytes()); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
raw, err := io.ReadAll(io.LimitReader(bufio.NewReader(conn), maxResponseBytes))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return parseHTTPResponse(raw)
|
|
}
|
|
|
|
func parseHTTPResponse(raw []byte) (*httpResponse, error) {
|
|
head, body, found := bytes.Cut(raw, []byte("\r\n\r\n"))
|
|
if !found {
|
|
return nil, errors.New("malformed http response")
|
|
}
|
|
statusLine, _, _ := strings.Cut(string(head), "\r\n")
|
|
_, rest, ok := strings.Cut(statusLine, " ")
|
|
if !ok {
|
|
return nil, fmt.Errorf("malformed status line %q", statusLine)
|
|
}
|
|
codeStr, _, _ := strings.Cut(rest, " ")
|
|
code, err := strconv.Atoi(codeStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("malformed status line %q", statusLine)
|
|
}
|
|
return &httpResponse{Status: code, StatusText: rest, Body: body}, nil
|
|
}
|
|
|
|
// GETBody fetches path from the worker within timeout and returns the raw body.
|
|
// Statuses of 400 and above are returned as errors.
|
|
func GETBody(ctx context.Context, timeout time.Duration, port int, path string) ([]byte, error) {
|
|
resp, err := httpDo(ctx, timeout, "GET", port, path, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.Status >= 400 {
|
|
return nil, fmt.Errorf("request failed: %s", resp.StatusText)
|
|
}
|
|
return resp.Body, nil
|
|
}
|