diff --git a/_examples/reusable-sessions/main.go b/_examples/reusable-sessions/main.go index dbc15b6..a3abf87 100644 --- a/_examples/reusable-sessions/main.go +++ b/_examples/reusable-sessions/main.go @@ -5,6 +5,7 @@ package main import ( "context" "fmt" + "os" "time" "github.com/fastly/compute-sdk-go/fsthttp" @@ -14,10 +15,10 @@ func main() { var requests int fsthttp.ServeMany(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { requests++ - fmt.Fprintf(w, "Request %v, Hello, %s!\n", requests, r.RemoteAddr) + fmt.Fprintf(w, "Request %v, Hello, %s (%q, %q)!\n", requests, r.RemoteAddr, os.Getenv("FASTLY_TRACE_ID"), r.RequestID) }, &fsthttp.ServeManyOptions{ - NextTimeout: 10 * time.Second, + NextTimeout: 1 * time.Second, MaxRequests: 100, - MaxLifetime: 10 * time.Second, + MaxLifetime: 5 * time.Second, }) } diff --git a/fsthttp/request.go b/fsthttp/request.go index da7819d..d9e77f9 100644 --- a/fsthttp/request.go +++ b/fsthttp/request.go @@ -81,6 +81,12 @@ type Request struct { // TLSInfo collects TLS metadata for incoming requests received over HTTPS. TLSInfo TLSInfo + // tlsClientCertificateInfo is information about the tls client certificate, if available + clientCertificate *TLSClientCertificateInfo + + // Fingerprint collects fingerprint metadata for incoming requests + fingerprint *Fingerprint + // SendPollInterval determines how often the Send method will check for // completed requests. While polling, the Go runtime is suspended, and all // user code stops execution. A shorter interval will make Send more @@ -111,6 +117,9 @@ type Request struct { // discarded. ManualFramingMode bool + // RequestID is the current Fastly request ID + RequestID string + sent bool // a request may only be sent once abi struct { @@ -165,6 +174,11 @@ func newClientRequest(abiReq *fastly.HTTPRequest, abiReqBody *fastly.HTTPBody) ( return nil, fmt.Errorf("get protocol version: %w", err) } + reqID, err := abiReq.DownstreamRequestID() + if err != nil { + return nil, fmt.Errorf("get request id: %w", err) + } + header := NewHeader() keys := abiReq.GetHeaderNames(RequestLimits.maxHeaderNameLen) for keys.Next() { @@ -214,6 +228,12 @@ func newClientRequest(abiReq *fastly.HTTPRequest, abiReqBody *fastly.HTTPBody) ( if err != nil { return nil, fmt.Errorf("get TLS JA3 MD5: %w", err) } + + tlsInfo.JA4, err = abiReq.DownstreamTLSJA4() + if err != nil { + return nil, fmt.Errorf("get TLS JA4: %w", err) + } + } // Setting the fsthttp.Request Host field to the url.URL Host field is @@ -231,6 +251,7 @@ func newClientRequest(abiReq *fastly.HTTPRequest, abiReqBody *fastly.HTTPBody) ( RemoteAddr: remoteAddr.String(), ServerAddr: serverAddr.String(), TLSInfo: tlsInfo, + RequestID: reqID, }, nil } @@ -342,6 +363,44 @@ func (req *Request) AddCookie(c *Cookie) { } } +// Fingerprint returns a fleshed-out Fingerprint object for the request. +func (req *Request) Fingerprint() (*Fingerprint, error) { + if req.fingerprint != nil { + return req.fingerprint, nil + } + + var err error + + var fingerprint Fingerprint + fingerprint.H2, err = req.abi.req.DownstreamH2Fingerprint() + if err != nil { + if status, ok := fastly.IsFastlyError(err); ok && status != fastly.FastlyStatusNone { + return nil, fmt.Errorf("get H2 fingerprint: %w", err) + } + } + + fingerprint.OH, err = req.abi.req.DownstreamOHFingerprint() + if err != nil { + if status, ok := fastly.IsFastlyError(err); ok && status != fastly.FastlyStatusNone { + return nil, fmt.Errorf("get OH fingerprint: %w", err) + } + } + + fingerprint.DDOSDetected, err = req.abi.req.DownstreamDDOSDetected() + if err != nil { + return nil, fmt.Errorf("get ddos detected: %w", err) + } + + fingerprint.FastlyKeyIsValid, err = req.abi.req.DownstreamFastlyKeyIsValid() + if err != nil { + return nil, fmt.Errorf("get fastly key is valid: %w", err) + } + + req.fingerprint = &fingerprint + + return req.fingerprint, nil +} + // Send the request to the named backend. Requests may only be sent to // backends that have been preconfigured in your service, regardless of // their URL. Once sent, a request cannot be sent again. @@ -945,6 +1004,58 @@ type TLSInfo struct { // JA3MD5 contains the bytes of the JA3 signature of the client TLS request. // See https://www.fastly.com/blog/the-state-of-tls-fingerprinting-whats-working-what-isnt-and-whats-next JA3MD5 []byte + + // JA4 contains the bytes of the JA4 signature of the client TLS request. + // See https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md + JA4 []byte +} + +func (req *Request) TLSClientCertificateInfo() (*TLSClientCertificateInfo, error) { + if req.clientCertificate != nil { + return req.clientCertificate, nil + } + + var err error + var cert TLSClientCertificateInfo + + cert.RawClientCertificate, err = req.abi.req.DownstreamTLSRawCertificate() + if err != nil { + return nil, fmt.Errorf("get TLS raw client certificate: %w", err) + } + + if cert.RawClientCertificate != nil { + cert.ClientCertIsVerified, err = req.abi.req.DownstreamTLSClientCertVerifyResult() + if err != nil { + return nil, fmt.Errorf("get TLS client certificate verify: %w", err) + } + } + + req.clientCertificate = &cert + return req.clientCertificate, nil +} + +type TLSClientCertificateInfo struct { + // RawClientCertificate contains the bytes of the raw client certificate, if one was provided. + RawClientCertificate []byte + + // ClientCertIsVerified is true if the provided client certificate is valid. + ClientCertIsVerified bool +} + +// Fingerprint holds various fingerprints for a request. +type Fingerprint struct { + + // H2 is the HTTP/2 fingerprint of a client request if available + H2 []byte + + // OH is a fingerprint of the client request's original headers + OH []byte + + // DDOSDetected is true if the request was determined to be part of a DDOS attack. + DDOSDetected bool + + // FastlyKeyIsValid is true if the request contains a valid Fastly API token. + FastlyKeyIsValid bool } // DecompressResponseOptions control the auto decompress response behaviour. diff --git a/internal/abi/fastly/hostcalls_noguest.go b/internal/abi/fastly/hostcalls_noguest.go index af88a73..1918f40 100644 --- a/internal/abi/fastly/hostcalls_noguest.go +++ b/internal/abi/fastly/hostcalls_noguest.go @@ -91,6 +91,42 @@ func (r *HTTPRequest) DownstreamTLSJA3MD5() ([]byte, error) { return nil, fmt.Errorf("not implemented") } +func (r *HTTPRequest) DownstreamH2Fingerprint() ([]byte, error) { + return nil, fmt.Errorf("not implemented") +} + +func (r *HTTPRequest) DownstreamRequestID() (string, error) { + return "", fmt.Errorf("not implemented") +} + +func (r *HTTPRequest) DownstreamOHFingerprint() ([]byte, error) { + return nil, fmt.Errorf("not implemented") +} + +func (r *HTTPRequest) DownstreamDDOSDetected() (bool, error) { + return false, fmt.Errorf("not implemented") +} + +func (r *HTTPRequest) DownstreamTLSRawCertificate() ([]byte, error) { + return nil, fmt.Errorf("not implemented") +} + +func (r *HTTPRequest) DownstreamTLSClientCertVerifyResult() (bool, error) { + return false, fmt.Errorf("not implemented") +} + +func (r *HTTPRequest) DownstreamTLSJA4() ([]byte, error) { + return nil, fmt.Errorf("not implemented") +} + +func (r *HTTPRequest) DownstreamComplianceRegion() (string, error) { + return "", fmt.Errorf("not implemented") +} + +func (r *HTTPRequest) DownstreamFastlyKeyIsValid() (bool, error) { + return false, fmt.Errorf("not implemented") +} + func NewHTTPRequest() (*HTTPRequest, error) { return nil, fmt.Errorf("not implemented") } diff --git a/internal/abi/fastly/http_guest.go b/internal/abi/fastly/http_guest.go index bc48012..3b8a38e 100644 --- a/internal/abi/fastly/http_guest.go +++ b/internal/abi/fastly/http_guest.go @@ -1306,35 +1306,151 @@ func (r *HTTPRequest) DownstreamServerIPAddr() (net.IP, error) { return net.IP(buf.AsBytes()), nil } +// witx: +// +// (@interface func (export "downstream_client_h2_fingerprint") +// (param $req $request_handle) +// (param $h2fp_out (@witx pointer (@witx char8))) +// (param $h2fp_max_len (@witx usize)) +// (param $nwritten_out (@witx pointer (@witx usize))) +// (result $err (expected (error $fastly_status))) +// ) +// +//go:wasmimport fastly_http_downstream downstream_client_h2_fingerprint +//go:noescape +func fastlyHTTPDownstreamH2Fingerprint( + req requestHandle, + fingerprintOut prim.Pointer[prim.Char8], + fingerprintMaxLen prim.Usize, + nwrittenOut prim.Pointer[prim.Usize], +) FastlyStatus + +func (r *HTTPRequest) DownstreamH2Fingerprint() ([]byte, error) { + n := DefaultMediumBufLen + for { + buf := prim.NewWriteBuffer(n) + status := fastlyHTTPDownstreamH2Fingerprint( + r.h, + prim.ToPointer(buf.Char8Pointer()), + buf.Cap(), + prim.ToPointer(buf.NPointer()), + ) + if status == FastlyStatusBufLen && buf.NValue() > 0 { + n = int(buf.NValue()) + continue + } + if err := status.toError(); err != nil { + return nil, err + } + return buf.AsBytes(), nil + } +} + +// witx; +// +// (@interface func (export "downstream_client_request_id") +// (param $req $request_handle) +// (param $reqid_out (@witx pointer (@witx char8))) +// (param $reqid_max_len (@witx usize)) +// (param $nwritten_out (@witx pointer (@witx usize))) +// (result $err (expected (error $fastly_status))) +// ) +// +//go:wasmimport fastly_http_downstream downstream_client_request_id +//go:noescape +func fastlyHTTPDownstreamRequestID( + req requestHandle, + requestOut prim.Pointer[prim.Char8], + requestMaxLen prim.Usize, + nwrittenOut prim.Pointer[prim.Usize], +) FastlyStatus + +func (r *HTTPRequest) DownstreamRequestID() (string, error) { + n := DefaultSmallBufLen + for { + buf := prim.NewWriteBuffer(n) + status := fastlyHTTPDownstreamRequestID( + r.h, + prim.ToPointer(buf.Char8Pointer()), + buf.Cap(), + prim.ToPointer(buf.NPointer()), + ) + if status == FastlyStatusBufLen && buf.NValue() > 0 { + n = int(buf.NValue()) + continue + } + if err := status.toError(); err != nil { + return "", err + } + return buf.ToString(), nil + } +} + +// witx: +// +// (@interface func (export "downstream_client_oh_fingerprint") +// (param $req $request_handle) +// (param $ohfp_out (@witx pointer (@witx char8))) +// (param $ohfp_max_len (@witx usize)) +// (param $nwritten_out (@witx pointer (@witx usize))) +// (result $err (expected (error $fastly_status))) +// ) // -// (@interface func (export "downstream_client_h2_fingerprint") -// (param $req $request_handle) -// (param $h2fp_out (@witx pointer (@witx char8))) -// (param $h2fp_max_len (@witx usize)) -// (param $nwritten_out (@witx pointer (@witx usize))) -// (result $err (expected (error $fastly_status))) -// ) -// -// (@interface func (export "downstream_client_request_id") -// (param $req $request_handle) -// (param $reqid_out (@witx pointer (@witx char8))) -// (param $reqid_max_len (@witx usize)) -// (param $nwritten_out (@witx pointer (@witx usize))) -// (result $err (expected (error $fastly_status))) -// ) -// -// (@interface func (export "downstream_client_oh_fingerprint") -// (param $req $request_handle) -// (param $ohfp_out (@witx pointer (@witx char8))) -// (param $ohfp_max_len (@witx usize)) -// (param $nwritten_out (@witx pointer (@witx usize))) -// (result $err (expected (error $fastly_status))) -// ) -// -// (@interface func (export "downstream_client_ddos_detected") -// (param $req $request_handle) -// (result $err (expected $ddos_detected (error $fastly_status))) -// ) +//go:wasmimport fastly_http_downstream downstream_client_oh_fingerprint +//go:noescape +func fastlyHTTPDownstreamOHFingerprint( + req requestHandle, + fingerprintOut prim.Pointer[prim.Char8], + fingerprintMaxLen prim.Usize, + nwrittenOut prim.Pointer[prim.Usize], +) FastlyStatus + +func (r *HTTPRequest) DownstreamOHFingerprint() ([]byte, error) { + n := DefaultSmallBufLen + for { + buf := prim.NewWriteBuffer(n) + status := fastlyHTTPDownstreamOHFingerprint( + r.h, + prim.ToPointer(buf.Char8Pointer()), + buf.Cap(), + prim.ToPointer(buf.NPointer()), + ) + if status == FastlyStatusBufLen && buf.NValue() > 0 { + n = int(buf.NValue()) + continue + } + if err := status.toError(); err != nil { + return nil, err + } + return buf.AsBytes(), nil + } +} + +// witx: +// +// (@interface func (export "downstream_client_ddos_detected") +// (param $req $request_handle) +// (result $err (expected $ddos_detected (error $fastly_status))) +// ) +// +//go:wasmimport fastly_http_downstream downstream_client_ddos_detected +//go:noescape +func fastlyHTTPDownstreamDDOSDetected( + req requestHandle, + detected prim.Pointer[bool], +) FastlyStatus + +func (r *HTTPRequest) DownstreamDDOSDetected() (bool, error) { + var detected bool + if err := fastlyHTTPDownstreamDDOSDetected( + r.h, + prim.ToPointer(&detected), + ).toError(); err != nil { + return false, err + } + + return detected, nil +} // witx: // @@ -1454,19 +1570,71 @@ func (r *HTTPRequest) DownstreamTLSClientHello() ([]byte, error) { } } +// witx: // -// (@interface func (export "downstream_tls_raw_client_certificate") -// (param $req $request_handle) -// (param $raw_client_cert_out (@witx pointer (@witx char8))) -// (param $raw_client_cert_max_len (@witx usize)) -// (param $nwritten_out (@witx pointer (@witx usize))) -// (result $err (expected (error $fastly_status))) -// ) +// (@interface func (export "downstream_tls_raw_client_certificate") +// (param $req $request_handle) +// (param $raw_client_cert_out (@witx pointer (@witx char8))) +// (param $raw_client_cert_max_len (@witx usize)) +// (param $nwritten_out (@witx pointer (@witx usize))) +// (result $err (expected (error $fastly_status))) +// ) // -// (@interface func (export "downstream_tls_client_cert_verify_result") -// (param $req $request_handle) -// (result $err (expected $client_cert_verify_result (error $fastly_status))) -// ) +//go:wasmimport fastly_http_downstream downstream_tls_raw_client_certificate +//go:noescape +func fastlyHTTPReqDownstreamTLSRawCertificate( + req requestHandle, + certOut prim.Pointer[prim.Char8], + certMaxLen prim.Usize, + nwrittenOut prim.Pointer[prim.Usize], +) FastlyStatus + +func (r *HTTPRequest) DownstreamTLSRawCertificate() ([]byte, error) { + n := DefaultLargeBufLen // Longest (~132,000); typically < 2^14; RFC https://datatracker.ietf.org/doc/html/rfc8446#section-4.1.2 + for { + buf := prim.NewWriteBuffer(n) + status := fastlyHTTPReqDownstreamTLSRawCertificate( + r.h, + prim.ToPointer(buf.Char8Pointer()), + buf.Cap(), + prim.ToPointer(buf.NPointer()), + ) + if status == FastlyStatusBufLen && buf.NValue() > 0 { + n = int(buf.NValue()) + continue + } + if err := status.toError(); err != nil { + return nil, err + } + return buf.AsBytes(), nil + } +} + +// witx: +// +// (@interface func (export "downstream_tls_client_cert_verify_result") +// (param $req $request_handle) +// (result $err (expected $client_cert_verify_result (error $fastly_status))) +// ) +// +//go:wasmimport fastly_http_downstream downstream_tls_client_cert_verify_result +//go:noescape +func fastlyHTTPDownstreamTLSClientCertVerifyResult( + req requestHandle, + result prim.Pointer[bool], +) FastlyStatus + +func (r *HTTPRequest) DownstreamTLSClientCertVerifyResult() (bool, error) { + var result bool + if err := fastlyHTTPDownstreamTLSClientCertVerifyResult( + r.h, + prim.ToPointer(&result), + ).toError(); err != nil { + return false, err + } + + return result, nil +} // witx: // @@ -1500,28 +1668,114 @@ func (r *HTTPRequest) DownstreamTLSJA3MD5() ([]byte, error) { return buf.AsBytes(), nil } +// witx: // -// (@interface func (export "downstream_tls_ja4") -// (param $req $request_handle) -// (param $ja4_out (@witx pointer (@witx char8))) -// (param $ja4_max_len (@witx usize)) -// (param $nwritten_out (@witx pointer (@witx usize))) -// (result $err (expected (error $fastly_status))) -// ) -// -// (@interface func (export "downstream_compliance_region") -// (param $req $request_handle) -// (param $region_out (@witx pointer (@witx char8))) -// (param $region_max_len (@witx usize)) -// (param $nwritten_out (@witx pointer (@witx usize))) -// (result $err (expected (error $fastly_status))) -// ) -// -// (@interface func (export "fastly_key_is_valid") -// (param $req $request_handle) -// (result $err (expected $is_valid (error $fastly_status))) -// ) -//) +// (@interface func (export "downstream_tls_ja4") +// (param $req $request_handle) +// (param $ja4_out (@witx pointer (@witx char8))) +// (param $ja4_max_len (@witx usize)) +// (param $nwritten_out (@witx pointer (@witx usize))) +// (result $err (expected (error $fastly_status))) +// ) +// +//go:wasmimport fastly_http_downstream downstream_tls_ja4 +//go:noescape +func fastlyHTTPReqDownstreamTLSJA4( + req requestHandle, + ja4Out prim.Pointer[prim.Char8], + ja4MaxLen prim.Usize, + nwrittenOut prim.Pointer[prim.Usize], +) FastlyStatus + +func (r *HTTPRequest) DownstreamTLSJA4() ([]byte, error) { + n := DefaultSmallBufLen // JA4 hashes should be <64 bytes + for { + buf := prim.NewWriteBuffer(n) + status := fastlyHTTPReqDownstreamTLSJA4( + r.h, + prim.ToPointer(buf.Char8Pointer()), + buf.Cap(), + prim.ToPointer(buf.NPointer()), + ) + if status == FastlyStatusBufLen && buf.NValue() > 0 { + n = int(buf.NValue()) + continue + } + if err := status.toError(); err != nil { + return nil, err + } + return buf.AsBytes(), nil + } +} + +// witx: +// +// (@interface func (export "downstream_compliance_region") +// (param $req $request_handle) +// (param $region_out (@witx pointer (@witx char8))) +// (param $region_max_len (@witx usize)) +// (param $nwritten_out (@witx pointer (@witx usize))) +// (result $err (expected (error $fastly_status))) +// ) +// +//go:wasmimport fastly_http_downstream downstream_compliance_region +//go:noescape +func fastlyHTTPReqDownstreamComplianceRegion( + req requestHandle, + regionOut prim.Pointer[prim.Char8], + regionMaxLen prim.Usize, + nwrittenOut prim.Pointer[prim.Usize], +) FastlyStatus + +// DownstreamComplianceRegion returns the compliance region (US/EU/None) for the request. +func (r *HTTPRequest) DownstreamComplianceRegion() (string, error) { + n := 4 + for { + buf := prim.NewWriteBuffer(n) + status := fastlyHTTPReqDownstreamComplianceRegion( + r.h, + prim.ToPointer(buf.Char8Pointer()), + buf.Cap(), + prim.ToPointer(buf.NPointer()), + ) + if status == FastlyStatusBufLen && buf.NValue() > 0 { + n = int(buf.NValue()) + continue + } + if err := status.toError(); err != nil { + return "", err + } + return buf.ToString(), nil + } +} + +// witx; +// +// (@interface func (export "fastly_key_is_valid") +// (param $req $request_handle) +// (result $err (expected $is_valid (error $fastly_status))) +// ) +// +// ) +// +//go:wasmimport fastly_http_downstream fastly_key_is_valid +//go:noescape +func fastlyHTTPDownstreamFastlyKeyIsValid( + req requestHandle, + valid prim.Pointer[bool], +) FastlyStatus + +func (r *HTTPRequest) DownstreamFastlyKeyIsValid() (bool, error) { + var valid bool + if err := fastlyHTTPDownstreamFastlyKeyIsValid( + r.h, + prim.ToPointer(&valid), + ).toError(); err != nil { + return false, err + } + + return valid, nil +} // (module $fastly_http_body