Files
CaIon 29509776ef feat(jsplugin): let driver hooks read upstream data with utils.fetch
utils.fetch is a synchronous, read-only GET or HEAD to the channel Base URL
host or the plugin's allowedHosts, through the channel proxy used for
polling. It is available in buildSubmitRequest, the query and batch build
and parse hooks, and buildContentRequest; every other hook throws. Each hook
call gets at most 4 fetches, 15 s per fetch, 30 s in total and 1 MiB per
body. Redirects are returned, not followed. responseType "bytes" returns an
ArrayBuffer for binary data such as a media file header.

While a fetch waits, the runtime gives up its execution slot and the
watchdog pauses. BuildContentRequest now receives the request context, and
the media proxy shares the plugin request header rules.
2026-10-10 13:39:55 +08:00

70 lines
2.3 KiB
Go

package jsplugin
import (
"fmt"
"net"
"net/url"
"strings"
"golang.org/x/net/http/httpguts"
)
// ValidateRequestURL prevents plugins from directing a channel credential to
// hosts other than the configured base URL or an administrator-approved host.
func ValidateRequestURL(requestURL, baseURL string, allowedHosts []string) error {
request, err := url.Parse(requestURL)
if err != nil || request.Scheme == "" || request.Host == "" {
return fmt.Errorf("plugin request URL must be absolute")
}
base, err := url.Parse(baseURL)
if err != nil || base.Host == "" {
return fmt.Errorf("channel base URL is invalid")
}
requestHost := canonicalHost(request)
if requestHost == canonicalHost(base) {
return nil
}
for _, allowed := range allowedHosts {
// Parse with the request scheme so "host:443" matches an https request
// the same way an explicit default port on the request URL does.
allowedURL, parseErr := url.Parse(request.Scheme + "://" + strings.TrimSpace(allowed))
if parseErr == nil && requestHost == canonicalHost(allowedURL) {
return nil
}
}
return fmt.Errorf("plugin request host %q is not allowed", request.Host)
}
// ValidateRequestHeaders rejects plugin-chosen headers that the host
// transport owns or that change how a request is framed or proxied, and
// bounds their number and size.
func ValidateRequestHeaders(headers map[string]string) error {
if len(headers) > 64 {
return fmt.Errorf("at most 64 request headers are allowed")
}
for name, value := range headers {
name = strings.TrimSpace(name)
if !httpguts.ValidHeaderFieldName(name) {
return fmt.Errorf("request header name %q is invalid", name)
}
if !httpguts.ValidHeaderFieldValue(value) || len(value) > 8192 {
return fmt.Errorf("request header %q has an invalid or oversized value", name)
}
switch strings.ToLower(name) {
case "host", "content-length", "accept-encoding", "connection", "proxy-connection", "keep-alive",
"proxy-authorization", "te", "trailer", "transfer-encoding", "upgrade":
return fmt.Errorf("request header %q is not allowed", name)
}
}
return nil
}
func canonicalHost(value *url.URL) string {
host := strings.ToLower(value.Hostname())
port := value.Port()
if port == "" || port == "80" && value.Scheme == "http" || port == "443" && value.Scheme == "https" {
return host
}
return net.JoinHostPort(host, port)
}