mirror of
https://github.com/aykhans/sarin.git
synced 2026-02-28 06:49:13 +00:00
Introduce structured error types and bump Go/linter versions
Replace ad-hoc fmt.Errorf/errors.New calls with typed error structs across config, sarin, and script packages to enable type-based error handling. Add script-specific error handlers in CLI entry point. Fix variable shadowing bug in Worker for scriptTransformer. Bump Go to 1.25.7 and golangci-lint to v2.8.0.
This commit is contained in:
4
.github/workflows/lint.yaml
vendored
4
.github/workflows/lint.yaml
vendored
@@ -16,8 +16,8 @@ jobs:
|
|||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: 1.25.5
|
go-version: 1.25.7
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v9
|
uses: golangci/golangci-lint-action@v9
|
||||||
with:
|
with:
|
||||||
version: v2.7.2
|
version: v2.8.0
|
||||||
|
|||||||
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -35,7 +35,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "VERSION=$(git describe --tags --always)" >> $GITHUB_ENV
|
echo "VERSION=$(git describe --tags --always)" >> $GITHUB_ENV
|
||||||
echo "GIT_COMMIT=$(git rev-parse HEAD)" >> $GITHUB_ENV
|
echo "GIT_COMMIT=$(git rev-parse HEAD)" >> $GITHUB_ENV
|
||||||
echo "GO_VERSION=1.25.5" >> $GITHUB_ENV
|
echo "GO_VERSION=1.25.7" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
if: github.event_name == 'release' || inputs.build_binaries
|
if: github.event_name == 'release' || inputs.build_binaries
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
ARG GO_VERSION=1.25.5
|
ARG GO_VERSION=1.25.7
|
||||||
|
|
||||||
FROM docker.io/library/golang:${GO_VERSION}-alpine AS builder
|
FROM docker.io/library/golang:${GO_VERSION}-alpine AS builder
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ version: "3"
|
|||||||
|
|
||||||
vars:
|
vars:
|
||||||
BIN_DIR: ./bin
|
BIN_DIR: ./bin
|
||||||
GOLANGCI_LINT_VERSION: v2.7.2
|
GOLANGCI_LINT_VERSION: v2.8.0
|
||||||
GOLANGCI: "{{.BIN_DIR}}/golangci-lint-{{.GOLANGCI_LINT_VERSION}}"
|
GOLANGCI: "{{.BIN_DIR}}/golangci-lint-{{.GOLANGCI_LINT_VERSION}}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
|
|||||||
@@ -55,16 +55,22 @@ func main() {
|
|||||||
*combinedConfig.DryRun,
|
*combinedConfig.DryRun,
|
||||||
combinedConfig.Lua, combinedConfig.Js,
|
combinedConfig.Lua, combinedConfig.Js,
|
||||||
)
|
)
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, config.StyleRed.Render("[ERROR] ")+err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
_ = utilsErr.MustHandle(err,
|
_ = utilsErr.MustHandle(err,
|
||||||
utilsErr.OnType(func(err types.ProxyDialError) error {
|
utilsErr.OnType(func(err types.ProxyDialError) error {
|
||||||
fmt.Fprintln(os.Stderr, config.StyleRed.Render("[PROXY] ")+err.Error())
|
fmt.Fprintln(os.Stderr, config.StyleRed.Render("[PROXY] ")+err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
return nil
|
return nil
|
||||||
}),
|
}),
|
||||||
|
utilsErr.OnSentinel(types.ErrScriptEmpty, func(err error) error {
|
||||||
|
fmt.Fprintln(os.Stderr, config.StyleRed.Render("[SCRIPT] ")+err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
utilsErr.OnType(func(err types.ScriptLoadError) error {
|
||||||
|
fmt.Fprintln(os.Stderr, config.StyleRed.Render("[SCRIPT] ")+err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
srn.Start(ctx)
|
srn.Start(ctx)
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module go.aykhans.me/sarin
|
module go.aykhans.me/sarin
|
||||||
|
|
||||||
go 1.25.5
|
go 1.25.7
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/brianvoe/gofakeit/v7 v7.14.0
|
github.com/brianvoe/gofakeit/v7 v7.14.0
|
||||||
|
|||||||
@@ -638,10 +638,16 @@ func parseConfigFile(configFile types.ConfigFile, maxDepth int) (*Config, error)
|
|||||||
// - Escaped "@": strings starting with "@@" (literal "@" at start)
|
// - Escaped "@": strings starting with "@@" (literal "@" at start)
|
||||||
// - File reference: "@/path/to/file" or "@./relative/path"
|
// - File reference: "@/path/to/file" or "@./relative/path"
|
||||||
// - URL reference: "@http://..." or "@https://..."
|
// - URL reference: "@http://..." or "@https://..."
|
||||||
|
//
|
||||||
|
// It can return the following errors:
|
||||||
|
// - types.ErrScriptEmpty
|
||||||
|
// - types.ErrScriptSourceEmpty
|
||||||
|
// - types.ErrScriptURLNoHost
|
||||||
|
// - types.URLParseError
|
||||||
func validateScriptSource(script string) error {
|
func validateScriptSource(script string) error {
|
||||||
// Empty script is invalid
|
// Empty script is invalid
|
||||||
if script == "" {
|
if script == "" {
|
||||||
return errors.New("script cannot be empty")
|
return types.ErrScriptEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not a file/URL reference - it's an inline script
|
// Not a file/URL reference - it's an inline script
|
||||||
@@ -658,17 +664,17 @@ func validateScriptSource(script string) error {
|
|||||||
source := script[1:] // Remove the @ prefix
|
source := script[1:] // Remove the @ prefix
|
||||||
|
|
||||||
if source == "" {
|
if source == "" {
|
||||||
return errors.New("script source cannot be empty after @")
|
return types.ErrScriptSourceEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's a URL
|
// Check if it's a http(s) URL
|
||||||
if strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") {
|
if strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") {
|
||||||
parsedURL, err := url.Parse(source)
|
parsedURL, err := url.Parse(source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid URL: %w", err)
|
return types.NewURLParseError(source, err)
|
||||||
}
|
}
|
||||||
if parsedURL.Host == "" {
|
if parsedURL.Host == "" {
|
||||||
return errors.New("URL must have a host")
|
return types.ErrScriptURLNoHost
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ func (parser ConfigFileParser) Parse() (*Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fetchFile retrieves file contents from a local path or HTTP/HTTPS URL.
|
// fetchFile retrieves file contents from a local path or HTTP/HTTPS URL.
|
||||||
|
// It can return the following errors:
|
||||||
|
// - types.FileReadError
|
||||||
|
// - types.HTTPFetchError
|
||||||
|
// - types.HTTPStatusError
|
||||||
func fetchFile(ctx context.Context, src string) ([]byte, error) {
|
func fetchFile(ctx context.Context, src string) ([]byte, error) {
|
||||||
if strings.HasPrefix(src, "http://") || strings.HasPrefix(src, "https://") {
|
if strings.HasPrefix(src, "http://") || strings.HasPrefix(src, "https://") {
|
||||||
return fetchHTTP(ctx, src)
|
return fetchHTTP(ctx, src)
|
||||||
@@ -57,25 +61,28 @@ func fetchFile(ctx context.Context, src string) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fetchHTTP downloads file contents from an HTTP/HTTPS URL.
|
// fetchHTTP downloads file contents from an HTTP/HTTPS URL.
|
||||||
|
// It can return the following errors:
|
||||||
|
// - types.HTTPFetchError
|
||||||
|
// - types.HTTPStatusError
|
||||||
func fetchHTTP(ctx context.Context, url string) ([]byte, error) {
|
func fetchHTTP(ctx context.Context, url string) ([]byte, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
return nil, types.NewHTTPFetchError(url, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to fetch file: %w", err)
|
return nil, types.NewHTTPFetchError(url, err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close() //nolint:errcheck
|
defer resp.Body.Close() //nolint:errcheck
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, fmt.Errorf("failed to fetch file: HTTP %d %s", resp.StatusCode, resp.Status)
|
return nil, types.NewHTTPStatusError(url, resp.StatusCode, resp.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := io.ReadAll(resp.Body)
|
data, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
return nil, types.NewHTTPFetchError(url, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return data, nil
|
return data, nil
|
||||||
@@ -83,19 +90,21 @@ func fetchHTTP(ctx context.Context, url string) ([]byte, error) {
|
|||||||
|
|
||||||
// fetchLocal reads file contents from the local filesystem.
|
// fetchLocal reads file contents from the local filesystem.
|
||||||
// It resolves relative paths from the current working directory.
|
// It resolves relative paths from the current working directory.
|
||||||
|
// It can return the following errors:
|
||||||
|
// - types.FileReadError
|
||||||
func fetchLocal(src string) ([]byte, error) {
|
func fetchLocal(src string) ([]byte, error) {
|
||||||
path := src
|
path := src
|
||||||
if !filepath.IsAbs(src) {
|
if !filepath.IsAbs(src) {
|
||||||
pwd, err := os.Getwd()
|
pwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get working directory: %w", err)
|
return nil, types.NewFileReadError(src, err)
|
||||||
}
|
}
|
||||||
path = filepath.Join(pwd, src)
|
path = filepath.Join(pwd, src)
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := os.ReadFile(path) //nolint:gosec
|
data, err := os.ReadFile(path) //nolint:gosec
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read file: %w", err)
|
return nil, types.NewFileReadError(path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return data, nil
|
return data, nil
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"go.aykhans.me/sarin/internal/types"
|
"go.aykhans.me/sarin/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// It can return the following errors:
|
||||||
|
// - types.TemplateParseError
|
||||||
func validateTemplateString(value string, funcMap template.FuncMap) error {
|
func validateTemplateString(value string, funcMap template.FuncMap) error {
|
||||||
if value == "" {
|
if value == "" {
|
||||||
return nil
|
return nil
|
||||||
@@ -15,7 +17,7 @@ func validateTemplateString(value string, funcMap template.FuncMap) error {
|
|||||||
|
|
||||||
_, err := template.New("").Funcs(funcMap).Parse(value)
|
_, err := template.New("").Funcs(funcMap).Parse(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("template parse error: %w", err)
|
return types.NewTemplateParseError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
|
||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -95,6 +94,9 @@ func NewHostClients(
|
|||||||
return []*fasthttp.HostClient{client}, nil
|
return []*fasthttp.HostClient{client}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewProxyDialFunc creates a dial function for the given proxy URL.
|
||||||
|
// It can return the following errors:
|
||||||
|
// - types.ProxyUnsupportedSchemeError
|
||||||
func NewProxyDialFunc(ctx context.Context, proxyURL *url.URL, timeout time.Duration) (fasthttp.DialFunc, error) {
|
func NewProxyDialFunc(ctx context.Context, proxyURL *url.URL, timeout time.Duration) (fasthttp.DialFunc, error) {
|
||||||
var (
|
var (
|
||||||
dialer fasthttp.DialFunc
|
dialer fasthttp.DialFunc
|
||||||
@@ -117,16 +119,14 @@ func NewProxyDialFunc(ctx context.Context, proxyURL *url.URL, timeout time.Durat
|
|||||||
case "https":
|
case "https":
|
||||||
dialer = fasthttpHTTPSDialerDualStackTimeout(proxyURL, timeout)
|
dialer = fasthttpHTTPSDialerDualStackTimeout(proxyURL, timeout)
|
||||||
default:
|
default:
|
||||||
return nil, errors.New("unsupported proxy scheme")
|
return nil, types.NewProxyUnsupportedSchemeError(proxyURL.Scheme)
|
||||||
}
|
|
||||||
|
|
||||||
if dialer == nil {
|
|
||||||
return nil, errors.New("internal error: proxy dialer is nil")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return dialer, nil
|
return dialer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The returned dial function can return the following errors:
|
||||||
|
// - types.ProxyDialError
|
||||||
func fasthttpSocksDialerDualStackTimeout(ctx context.Context, proxyURL *url.URL, timeout time.Duration, resolveLocally bool) (fasthttp.DialFunc, error) {
|
func fasthttpSocksDialerDualStackTimeout(ctx context.Context, proxyURL *url.URL, timeout time.Duration, resolveLocally bool) (fasthttp.DialFunc, error) {
|
||||||
netDialer := &net.Dialer{}
|
netDialer := &net.Dialer{}
|
||||||
|
|
||||||
@@ -147,12 +147,18 @@ func fasthttpSocksDialerDualStackTimeout(ctx context.Context, proxyURL *url.URL,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
proxyStr := proxyURL.String()
|
||||||
|
|
||||||
// Assert to ContextDialer for timeout support
|
// Assert to ContextDialer for timeout support
|
||||||
contextDialer, ok := socksDialer.(proxy.ContextDialer)
|
contextDialer, ok := socksDialer.(proxy.ContextDialer)
|
||||||
if !ok {
|
if !ok {
|
||||||
// Fallback without timeout (should not happen with net.Dialer)
|
// Fallback without timeout (should not happen with net.Dialer)
|
||||||
return func(addr string) (net.Conn, error) {
|
return func(addr string) (net.Conn, error) {
|
||||||
return socksDialer.Dial("tcp", addr)
|
conn, err := socksDialer.Dial("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, types.NewProxyDialError(proxyStr, err)
|
||||||
|
}
|
||||||
|
return conn, nil
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +169,7 @@ func fasthttpSocksDialerDualStackTimeout(ctx context.Context, proxyURL *url.URL,
|
|||||||
if resolveLocally {
|
if resolveLocally {
|
||||||
host, port, err := net.SplitHostPort(addr)
|
host, port, err := net.SplitHostPort(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, types.NewProxyDialError(proxyStr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cap DNS resolution to half the timeout to reserve time for dial
|
// Cap DNS resolution to half the timeout to reserve time for dial
|
||||||
@@ -171,10 +177,10 @@ func fasthttpSocksDialerDualStackTimeout(ctx context.Context, proxyURL *url.URL,
|
|||||||
ips, err := net.DefaultResolver.LookupIP(dnsCtx, "ip", host)
|
ips, err := net.DefaultResolver.LookupIP(dnsCtx, "ip", host)
|
||||||
dnsCancel()
|
dnsCancel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, types.NewProxyDialError(proxyStr, err)
|
||||||
}
|
}
|
||||||
if len(ips) == 0 {
|
if len(ips) == 0 {
|
||||||
return nil, errors.New("no IP addresses found for host: " + host)
|
return nil, types.NewProxyDialError(proxyStr, types.NewProxyResolveError(host))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the first resolved IP
|
// Use the first resolved IP
|
||||||
@@ -184,16 +190,22 @@ func fasthttpSocksDialerDualStackTimeout(ctx context.Context, proxyURL *url.URL,
|
|||||||
// Use remaining time for dial
|
// Use remaining time for dial
|
||||||
remaining := time.Until(deadline)
|
remaining := time.Until(deadline)
|
||||||
if remaining <= 0 {
|
if remaining <= 0 {
|
||||||
return nil, context.DeadlineExceeded
|
return nil, types.NewProxyDialError(proxyStr, context.DeadlineExceeded)
|
||||||
}
|
}
|
||||||
|
|
||||||
dialCtx, dialCancel := context.WithTimeout(ctx, remaining)
|
dialCtx, dialCancel := context.WithTimeout(ctx, remaining)
|
||||||
defer dialCancel()
|
defer dialCancel()
|
||||||
|
|
||||||
return contextDialer.DialContext(dialCtx, "tcp", addr)
|
conn, err := contextDialer.DialContext(dialCtx, "tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, types.NewProxyDialError(proxyStr, err)
|
||||||
|
}
|
||||||
|
return conn, nil
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The returned dial function can return the following errors:
|
||||||
|
// - types.ProxyDialError
|
||||||
func fasthttpHTTPSDialerDualStackTimeout(proxyURL *url.URL, timeout time.Duration) fasthttp.DialFunc {
|
func fasthttpHTTPSDialerDualStackTimeout(proxyURL *url.URL, timeout time.Duration) fasthttp.DialFunc {
|
||||||
proxyAddr := proxyURL.Host
|
proxyAddr := proxyURL.Host
|
||||||
if proxyURL.Port() == "" {
|
if proxyURL.Port() == "" {
|
||||||
@@ -209,24 +221,26 @@ func fasthttpHTTPSDialerDualStackTimeout(proxyURL *url.URL, timeout time.Duratio
|
|||||||
proxyAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(credentials))
|
proxyAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(credentials))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
proxyStr := proxyURL.String()
|
||||||
|
|
||||||
return func(addr string) (net.Conn, error) {
|
return func(addr string) (net.Conn, error) {
|
||||||
// Establish TCP connection to proxy with timeout
|
// Establish TCP connection to proxy with timeout
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
conn, err := fasthttp.DialDualStackTimeout(proxyAddr, timeout)
|
conn, err := fasthttp.DialDualStackTimeout(proxyAddr, timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, types.NewProxyDialError(proxyStr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
remaining := timeout - time.Since(start)
|
remaining := timeout - time.Since(start)
|
||||||
if remaining <= 0 {
|
if remaining <= 0 {
|
||||||
conn.Close() //nolint:errcheck,gosec
|
conn.Close() //nolint:errcheck,gosec
|
||||||
return nil, context.DeadlineExceeded
|
return nil, types.NewProxyDialError(proxyStr, context.DeadlineExceeded)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set deadline for the TLS handshake and CONNECT request
|
// Set deadline for the TLS handshake and CONNECT request
|
||||||
if err := conn.SetDeadline(time.Now().Add(remaining)); err != nil {
|
if err := conn.SetDeadline(time.Now().Add(remaining)); err != nil {
|
||||||
conn.Close() //nolint:errcheck,gosec
|
conn.Close() //nolint:errcheck,gosec
|
||||||
return nil, err
|
return nil, types.NewProxyDialError(proxyStr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upgrade to TLS
|
// Upgrade to TLS
|
||||||
@@ -235,7 +249,7 @@ func fasthttpHTTPSDialerDualStackTimeout(proxyURL *url.URL, timeout time.Duratio
|
|||||||
})
|
})
|
||||||
if err := tlsConn.Handshake(); err != nil {
|
if err := tlsConn.Handshake(); err != nil {
|
||||||
tlsConn.Close() //nolint:errcheck,gosec
|
tlsConn.Close() //nolint:errcheck,gosec
|
||||||
return nil, err
|
return nil, types.NewProxyDialError(proxyStr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build and send CONNECT request
|
// Build and send CONNECT request
|
||||||
@@ -251,7 +265,7 @@ func fasthttpHTTPSDialerDualStackTimeout(proxyURL *url.URL, timeout time.Duratio
|
|||||||
|
|
||||||
if err := connectReq.Write(tlsConn); err != nil {
|
if err := connectReq.Write(tlsConn); err != nil {
|
||||||
tlsConn.Close() //nolint:errcheck,gosec
|
tlsConn.Close() //nolint:errcheck,gosec
|
||||||
return nil, err
|
return nil, types.NewProxyDialError(proxyStr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read response using buffered reader, but return wrapped connection
|
// Read response using buffered reader, but return wrapped connection
|
||||||
@@ -260,19 +274,19 @@ func fasthttpHTTPSDialerDualStackTimeout(proxyURL *url.URL, timeout time.Duratio
|
|||||||
resp, err := http.ReadResponse(bufReader, connectReq)
|
resp, err := http.ReadResponse(bufReader, connectReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlsConn.Close() //nolint:errcheck,gosec
|
tlsConn.Close() //nolint:errcheck,gosec
|
||||||
return nil, err
|
return nil, types.NewProxyDialError(proxyStr, err)
|
||||||
}
|
}
|
||||||
resp.Body.Close() //nolint:errcheck,gosec
|
resp.Body.Close() //nolint:errcheck,gosec
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
tlsConn.Close() //nolint:errcheck,gosec
|
tlsConn.Close() //nolint:errcheck,gosec
|
||||||
return nil, errors.New("proxy CONNECT failed: " + resp.Status)
|
return nil, types.NewProxyDialError(proxyStr, types.NewProxyConnectError(resp.Status))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear deadline for the tunneled connection
|
// Clear deadline for the tunneled connection
|
||||||
if err := tlsConn.SetDeadline(time.Time{}); err != nil {
|
if err := tlsConn.SetDeadline(time.Time{}); err != nil {
|
||||||
tlsConn.Close() //nolint:errcheck,gosec
|
tlsConn.Close() //nolint:errcheck,gosec
|
||||||
return nil, err
|
return nil, types.NewProxyDialError(proxyStr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return wrapped connection that uses the buffered reader
|
// Return wrapped connection that uses the buffered reader
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package sarin
|
package sarin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -10,6 +9,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.aykhans.me/sarin/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CachedFile holds the cached content and metadata of a file.
|
// CachedFile holds the cached content and metadata of a file.
|
||||||
@@ -31,6 +32,10 @@ func NewFileCache(requestTimeout time.Duration) *FileCache {
|
|||||||
|
|
||||||
// GetOrLoad retrieves a file from cache or loads it using the provided source.
|
// GetOrLoad retrieves a file from cache or loads it using the provided source.
|
||||||
// The source can be a local file path or an HTTP/HTTPS URL.
|
// The source can be a local file path or an HTTP/HTTPS URL.
|
||||||
|
// It can return the following errors:
|
||||||
|
// - types.FileReadError
|
||||||
|
// - types.HTTPFetchError
|
||||||
|
// - types.HTTPStatusError
|
||||||
func (fc *FileCache) GetOrLoad(source string) (*CachedFile, error) {
|
func (fc *FileCache) GetOrLoad(source string) (*CachedFile, error) {
|
||||||
if val, ok := fc.cache.Load(source); ok {
|
if val, ok := fc.cache.Load(source); ok {
|
||||||
return val.(*CachedFile), nil
|
return val.(*CachedFile), nil
|
||||||
@@ -59,14 +64,21 @@ func (fc *FileCache) GetOrLoad(source string) (*CachedFile, error) {
|
|||||||
return actual.(*CachedFile), nil
|
return actual.(*CachedFile), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readLocalFile reads a file from the local filesystem and returns its content and filename.
|
||||||
|
// It can return the following errors:
|
||||||
|
// - types.FileReadError
|
||||||
func (fc *FileCache) readLocalFile(filePath string) ([]byte, string, error) {
|
func (fc *FileCache) readLocalFile(filePath string) ([]byte, string, error) {
|
||||||
content, err := os.ReadFile(filePath) //nolint:gosec
|
content, err := os.ReadFile(filePath) //nolint:gosec
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("failed to read file %s: %w", filePath, err)
|
return nil, "", types.NewFileReadError(filePath, err)
|
||||||
}
|
}
|
||||||
return content, filepath.Base(filePath), nil
|
return content, filepath.Base(filePath), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fetchURL downloads file contents from an HTTP/HTTPS URL.
|
||||||
|
// It can return the following errors:
|
||||||
|
// - types.HTTPFetchError
|
||||||
|
// - types.HTTPStatusError
|
||||||
func (fc *FileCache) fetchURL(url string) ([]byte, string, error) {
|
func (fc *FileCache) fetchURL(url string) ([]byte, string, error) {
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Timeout: fc.requestTimeout,
|
Timeout: fc.requestTimeout,
|
||||||
@@ -74,17 +86,17 @@ func (fc *FileCache) fetchURL(url string) ([]byte, string, error) {
|
|||||||
|
|
||||||
resp, err := client.Get(url)
|
resp, err := client.Get(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("failed to fetch URL %s: %w", url, err)
|
return nil, "", types.NewHTTPFetchError(url, err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close() //nolint:errcheck
|
defer resp.Body.Close() //nolint:errcheck
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, "", fmt.Errorf("failed to fetch URL %s: HTTP %d", url, resp.StatusCode)
|
return nil, "", types.NewHTTPStatusError(url, resp.StatusCode, resp.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
content, err := io.ReadAll(resp.Body)
|
content, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", fmt.Errorf("failed to read response body from %s: %w", url, err)
|
return nil, "", types.NewHTTPFetchError(url, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract filename from URL path
|
// Extract filename from URL path
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package sarin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
|
||||||
"maps"
|
"maps"
|
||||||
"math/rand/v2"
|
"math/rand/v2"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -261,12 +260,12 @@ func NewValuesGeneratorFunc(values []string, templateFunctions template.FuncMap)
|
|||||||
for _, generator := range generators {
|
for _, generator := range generators {
|
||||||
rendered, err = generator(nil)
|
rendered, err = generator(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return valuesData{}, fmt.Errorf("values rendering: %w", err)
|
return valuesData{}, types.NewTemplateRenderError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err = godotenv.Unmarshal(rendered)
|
data, err = godotenv.Unmarshal(rendered)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return valuesData{}, fmt.Errorf("values rendering: %w", err)
|
return valuesData{}, types.NewTemplateRenderError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
maps.Copy(result, data)
|
maps.Copy(result, data)
|
||||||
@@ -283,7 +282,7 @@ func createTemplateFunc(value string, templateFunctions template.FuncMap) (func(
|
|||||||
return func(data any) (string, error) {
|
return func(data any) (string, error) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err = tmpl.Execute(&buf, data); err != nil {
|
if err = tmpl.Execute(&buf, data); err != nil {
|
||||||
return "", fmt.Errorf("template rendering: %w", err)
|
return "", types.NewTemplateRenderError(err)
|
||||||
}
|
}
|
||||||
return buf.String(), nil
|
return buf.String(), nil
|
||||||
}, true
|
}, true
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ type sarin struct {
|
|||||||
// NewSarin creates a new sarin instance for load testing.
|
// NewSarin creates a new sarin instance for load testing.
|
||||||
// It can return the following errors:
|
// It can return the following errors:
|
||||||
// - types.ProxyDialError
|
// - types.ProxyDialError
|
||||||
// - script loading errors
|
// - types.ErrScriptEmpty
|
||||||
|
// - types.ScriptLoadError
|
||||||
func NewSarin(
|
func NewSarin(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
methods []string,
|
methods []string,
|
||||||
@@ -216,7 +217,8 @@ func (q sarin) Worker(
|
|||||||
// Scripts are pre-validated in NewSarin, so this should not fail
|
// Scripts are pre-validated in NewSarin, so this should not fail
|
||||||
var scriptTransformer *script.Transformer
|
var scriptTransformer *script.Transformer
|
||||||
if !q.scriptChain.IsEmpty() {
|
if !q.scriptChain.IsEmpty() {
|
||||||
scriptTransformer, err := q.scriptChain.NewTransformer()
|
var err error
|
||||||
|
scriptTransformer, err = q.scriptChain.NewTransformer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package sarin
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
|
||||||
"math/rand/v2"
|
"math/rand/v2"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -12,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/brianvoe/gofakeit/v7"
|
"github.com/brianvoe/gofakeit/v7"
|
||||||
|
"go.aykhans.me/sarin/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewDefaultTemplateFuncMap(randSource rand.Source, fileCache *FileCache) template.FuncMap {
|
func NewDefaultTemplateFuncMap(randSource rand.Source, fileCache *FileCache) template.FuncMap {
|
||||||
@@ -90,7 +90,7 @@ func NewDefaultTemplateFuncMap(randSource rand.Source, fileCache *FileCache) tem
|
|||||||
// {{ file_Base64 "https://example.com/image.png" }}
|
// {{ file_Base64 "https://example.com/image.png" }}
|
||||||
"file_Base64": func(source string) (string, error) {
|
"file_Base64": func(source string) (string, error) {
|
||||||
if fileCache == nil {
|
if fileCache == nil {
|
||||||
return "", errors.New("file cache is not initialized")
|
return "", types.ErrFileCacheNotInitialized
|
||||||
}
|
}
|
||||||
cached, err := fileCache.GetOrLoad(source)
|
cached, err := fileCache.GetOrLoad(source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -582,7 +582,7 @@ func NewDefaultBodyTemplateFuncMap(
|
|||||||
// {{ body_FormData "name" "John" "avatar" "@/path/to/photo.jpg" "doc" "@https://example.com/file.pdf" }}
|
// {{ body_FormData "name" "John" "avatar" "@/path/to/photo.jpg" "doc" "@https://example.com/file.pdf" }}
|
||||||
funcMap["body_FormData"] = func(pairs ...string) (string, error) {
|
funcMap["body_FormData"] = func(pairs ...string) (string, error) {
|
||||||
if len(pairs)%2 != 0 {
|
if len(pairs)%2 != 0 {
|
||||||
return "", errors.New("body_FormData requires an even number of arguments (key-value pairs)")
|
return "", types.ErrFormDataOddArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
var multipartData bytes.Buffer
|
var multipartData bytes.Buffer
|
||||||
@@ -602,7 +602,7 @@ func NewDefaultBodyTemplateFuncMap(
|
|||||||
case strings.HasPrefix(val, "@"):
|
case strings.HasPrefix(val, "@"):
|
||||||
// File (local path or remote URL)
|
// File (local path or remote URL)
|
||||||
if fileCache == nil {
|
if fileCache == nil {
|
||||||
return "", errors.New("file cache is not initialized")
|
return "", types.ErrFileCacheNotInitialized
|
||||||
}
|
}
|
||||||
source := val[1:]
|
source := val[1:]
|
||||||
cached, err := fileCache.GetOrLoad(source)
|
cached, err := fileCache.GetOrLoad(source)
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
package script
|
package script
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
|
"go.aykhans.me/sarin/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Chain holds the loaded script sources and can create engine instances.
|
// Chain holds the loaded script sources and can create engine instances.
|
||||||
@@ -36,6 +35,8 @@ type Transformer struct {
|
|||||||
|
|
||||||
// NewTransformer creates engine instances from the chain's sources.
|
// NewTransformer creates engine instances from the chain's sources.
|
||||||
// Call this once per worker goroutine.
|
// Call this once per worker goroutine.
|
||||||
|
// It can return the following errors:
|
||||||
|
// - types.ScriptChainError
|
||||||
func (c *Chain) NewTransformer() (*Transformer, error) {
|
func (c *Chain) NewTransformer() (*Transformer, error) {
|
||||||
if c.IsEmpty() {
|
if c.IsEmpty() {
|
||||||
return &Transformer{}, nil
|
return &Transformer{}, nil
|
||||||
@@ -51,7 +52,7 @@ func (c *Chain) NewTransformer() (*Transformer, error) {
|
|||||||
engine, err := NewLuaEngine(src.Content)
|
engine, err := NewLuaEngine(src.Content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Close() // Clean up already created engines
|
t.Close() // Clean up already created engines
|
||||||
return nil, fmt.Errorf("lua script[%d]: %w", i, err)
|
return nil, types.NewScriptChainError("lua", i, err)
|
||||||
}
|
}
|
||||||
t.luaEngines = append(t.luaEngines, engine)
|
t.luaEngines = append(t.luaEngines, engine)
|
||||||
}
|
}
|
||||||
@@ -61,7 +62,7 @@ func (c *Chain) NewTransformer() (*Transformer, error) {
|
|||||||
engine, err := NewJsEngine(src.Content)
|
engine, err := NewJsEngine(src.Content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Close() // Clean up already created engines
|
t.Close() // Clean up already created engines
|
||||||
return nil, fmt.Errorf("js script[%d]: %w", i, err)
|
return nil, types.NewScriptChainError("js", i, err)
|
||||||
}
|
}
|
||||||
t.jsEngines = append(t.jsEngines, engine)
|
t.jsEngines = append(t.jsEngines, engine)
|
||||||
}
|
}
|
||||||
@@ -71,18 +72,20 @@ func (c *Chain) NewTransformer() (*Transformer, error) {
|
|||||||
|
|
||||||
// Transform applies all scripts to the request data.
|
// Transform applies all scripts to the request data.
|
||||||
// Lua scripts run first, then JavaScript scripts.
|
// Lua scripts run first, then JavaScript scripts.
|
||||||
|
// It can return the following errors:
|
||||||
|
// - types.ScriptChainError
|
||||||
func (t *Transformer) Transform(req *RequestData) error {
|
func (t *Transformer) Transform(req *RequestData) error {
|
||||||
// Run Lua scripts
|
// Run Lua scripts
|
||||||
for i, engine := range t.luaEngines {
|
for i, engine := range t.luaEngines {
|
||||||
if err := engine.Transform(req); err != nil {
|
if err := engine.Transform(req); err != nil {
|
||||||
return fmt.Errorf("lua script[%d]: %w", i, err)
|
return types.NewScriptChainError("lua", i, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run JS scripts
|
// Run JS scripts
|
||||||
for i, engine := range t.jsEngines {
|
for i, engine := range t.jsEngines {
|
||||||
if err := engine.Transform(req); err != nil {
|
if err := engine.Transform(req); err != nil {
|
||||||
return fmt.Errorf("js script[%d]: %w", i, err)
|
return types.NewScriptChainError("js", i, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ package script
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
|
"go.aykhans.me/sarin/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// JsEngine implements the Engine interface using goja (JavaScript).
|
// JsEngine implements the Engine interface using goja (JavaScript).
|
||||||
@@ -20,27 +20,31 @@ type JsEngine struct {
|
|||||||
// Example JavaScript script:
|
// Example JavaScript script:
|
||||||
//
|
//
|
||||||
// function transform(req) {
|
// function transform(req) {
|
||||||
// req.headers["X-Custom"] = "value";
|
// req.headers["X-Custom"] = ["value"];
|
||||||
// return req;
|
// return req;
|
||||||
// }
|
// }
|
||||||
|
//
|
||||||
|
// It can return the following errors:
|
||||||
|
// - types.ErrScriptTransformMissing
|
||||||
|
// - types.ScriptExecutionError
|
||||||
func NewJsEngine(scriptContent string) (*JsEngine, error) {
|
func NewJsEngine(scriptContent string) (*JsEngine, error) {
|
||||||
vm := goja.New()
|
vm := goja.New()
|
||||||
|
|
||||||
// Execute the script to define the transform function
|
// Execute the script to define the transform function
|
||||||
_, err := vm.RunString(scriptContent)
|
_, err := vm.RunString(scriptContent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to execute JavaScript script: %w", err)
|
return nil, types.NewScriptExecutionError("JavaScript", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the transform function
|
// Get the transform function
|
||||||
transformVal := vm.Get("transform")
|
transformVal := vm.Get("transform")
|
||||||
if transformVal == nil || goja.IsUndefined(transformVal) || goja.IsNull(transformVal) {
|
if transformVal == nil || goja.IsUndefined(transformVal) || goja.IsNull(transformVal) {
|
||||||
return nil, errors.New("script must define a global 'transform' function")
|
return nil, types.ErrScriptTransformMissing
|
||||||
}
|
}
|
||||||
|
|
||||||
transform, ok := goja.AssertFunction(transformVal)
|
transform, ok := goja.AssertFunction(transformVal)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("'transform' must be a function")
|
return nil, types.NewScriptExecutionError("JavaScript", errors.New("'transform' must be a function"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return &JsEngine{
|
return &JsEngine{
|
||||||
@@ -50,6 +54,8 @@ func NewJsEngine(scriptContent string) (*JsEngine, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Transform executes the JavaScript transform function with the given request data.
|
// Transform executes the JavaScript transform function with the given request data.
|
||||||
|
// It can return the following errors:
|
||||||
|
// - types.ScriptExecutionError
|
||||||
func (e *JsEngine) Transform(req *RequestData) error {
|
func (e *JsEngine) Transform(req *RequestData) error {
|
||||||
// Convert RequestData to JavaScript object
|
// Convert RequestData to JavaScript object
|
||||||
reqObj := e.requestDataToObject(req)
|
reqObj := e.requestDataToObject(req)
|
||||||
@@ -57,12 +63,12 @@ func (e *JsEngine) Transform(req *RequestData) error {
|
|||||||
// Call transform(req)
|
// Call transform(req)
|
||||||
result, err := e.transform(goja.Undefined(), reqObj)
|
result, err := e.transform(goja.Undefined(), reqObj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("JavaScript transform error: %w", err)
|
return types.NewScriptExecutionError("JavaScript", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update RequestData from the returned object
|
// Update RequestData from the returned object
|
||||||
if err := e.objectToRequestData(result, req); err != nil {
|
if err := e.objectToRequestData(result, req); err != nil {
|
||||||
return fmt.Errorf("failed to parse transform result: %w", err)
|
return types.NewScriptExecutionError("JavaScript", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -111,12 +117,12 @@ func (e *JsEngine) requestDataToObject(req *RequestData) goja.Value {
|
|||||||
// objectToRequestData updates RequestData from a JavaScript object.
|
// objectToRequestData updates RequestData from a JavaScript object.
|
||||||
func (e *JsEngine) objectToRequestData(val goja.Value, req *RequestData) error {
|
func (e *JsEngine) objectToRequestData(val goja.Value, req *RequestData) error {
|
||||||
if val == nil || goja.IsUndefined(val) || goja.IsNull(val) {
|
if val == nil || goja.IsUndefined(val) || goja.IsNull(val) {
|
||||||
return errors.New("transform function must return an object")
|
return types.ErrScriptTransformReturnObject
|
||||||
}
|
}
|
||||||
|
|
||||||
obj := val.ToObject(e.runtime)
|
obj := val.ToObject(e.runtime)
|
||||||
if obj == nil {
|
if obj == nil {
|
||||||
return errors.New("transform function must return an object")
|
return types.ErrScriptTransformReturnObject
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method
|
// Method
|
||||||
@@ -159,7 +165,7 @@ func (e *JsEngine) objectToRequestData(val goja.Value, req *RequestData) error {
|
|||||||
|
|
||||||
// stringSliceToArray converts a Go []string to a JavaScript array.
|
// stringSliceToArray converts a Go []string to a JavaScript array.
|
||||||
func (e *JsEngine) stringSliceToArray(values []string) *goja.Object {
|
func (e *JsEngine) stringSliceToArray(values []string) *goja.Object {
|
||||||
ifaces := make([]interface{}, len(values))
|
ifaces := make([]any, len(values))
|
||||||
for i, v := range values {
|
for i, v := range values {
|
||||||
ifaces[i] = v
|
ifaces[i] = v
|
||||||
}
|
}
|
||||||
@@ -181,7 +187,7 @@ func (e *JsEngine) objectToStringSliceMap(obj *goja.Object) map[string][]string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's an array
|
// Check if it's an array
|
||||||
if arr, ok := v.Export().([]interface{}); ok {
|
if arr, ok := v.Export().([]any); ok {
|
||||||
var values []string
|
var values []string
|
||||||
for _, item := range arr {
|
for _, item := range arr {
|
||||||
if s, ok := item.(string); ok {
|
if s, ok := item.(string); ok {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package script
|
package script
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
lua "github.com/yuin/gopher-lua"
|
lua "github.com/yuin/gopher-lua"
|
||||||
|
"go.aykhans.me/sarin/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LuaEngine implements the Engine interface using gopher-lua.
|
// LuaEngine implements the Engine interface using gopher-lua.
|
||||||
@@ -20,23 +20,27 @@ type LuaEngine struct {
|
|||||||
// Example Lua script:
|
// Example Lua script:
|
||||||
//
|
//
|
||||||
// function transform(req)
|
// function transform(req)
|
||||||
// req.headers["X-Custom"] = "value"
|
// req.headers["X-Custom"] = {"value"}
|
||||||
// return req
|
// return req
|
||||||
// end
|
// end
|
||||||
|
//
|
||||||
|
// It can return the following errors:
|
||||||
|
// - types.ErrScriptTransformMissing
|
||||||
|
// - types.ScriptExecutionError
|
||||||
func NewLuaEngine(scriptContent string) (*LuaEngine, error) {
|
func NewLuaEngine(scriptContent string) (*LuaEngine, error) {
|
||||||
L := lua.NewState()
|
L := lua.NewState()
|
||||||
|
|
||||||
// Execute the script to define the transform function
|
// Execute the script to define the transform function
|
||||||
if err := L.DoString(scriptContent); err != nil {
|
if err := L.DoString(scriptContent); err != nil {
|
||||||
L.Close()
|
L.Close()
|
||||||
return nil, fmt.Errorf("failed to execute Lua script: %w", err)
|
return nil, types.NewScriptExecutionError("Lua", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the transform function
|
// Get the transform function
|
||||||
transform := L.GetGlobal("transform")
|
transform := L.GetGlobal("transform")
|
||||||
if transform.Type() != lua.LTFunction {
|
if transform.Type() != lua.LTFunction {
|
||||||
L.Close()
|
L.Close()
|
||||||
return nil, errors.New("script must define a global 'transform' function")
|
return nil, types.ErrScriptTransformMissing
|
||||||
}
|
}
|
||||||
|
|
||||||
return &LuaEngine{
|
return &LuaEngine{
|
||||||
@@ -46,6 +50,8 @@ func NewLuaEngine(scriptContent string) (*LuaEngine, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Transform executes the Lua transform function with the given request data.
|
// Transform executes the Lua transform function with the given request data.
|
||||||
|
// It can return the following errors:
|
||||||
|
// - types.ScriptExecutionError
|
||||||
func (e *LuaEngine) Transform(req *RequestData) error {
|
func (e *LuaEngine) Transform(req *RequestData) error {
|
||||||
// Convert RequestData to Lua table
|
// Convert RequestData to Lua table
|
||||||
reqTable := e.requestDataToTable(req)
|
reqTable := e.requestDataToTable(req)
|
||||||
@@ -54,7 +60,7 @@ func (e *LuaEngine) Transform(req *RequestData) error {
|
|||||||
e.state.Push(e.transform)
|
e.state.Push(e.transform)
|
||||||
e.state.Push(reqTable)
|
e.state.Push(reqTable)
|
||||||
if err := e.state.PCall(1, 1, nil); err != nil {
|
if err := e.state.PCall(1, 1, nil); err != nil {
|
||||||
return fmt.Errorf("lua transform error: %w", err)
|
return types.NewScriptExecutionError("Lua", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the result
|
// Get the result
|
||||||
@@ -62,7 +68,7 @@ func (e *LuaEngine) Transform(req *RequestData) error {
|
|||||||
e.state.Pop(1)
|
e.state.Pop(1)
|
||||||
|
|
||||||
if result.Type() != lua.LTTable {
|
if result.Type() != lua.LTTable {
|
||||||
return fmt.Errorf("transform function must return a table, got %s", result.Type())
|
return types.NewScriptExecutionError("Lua", fmt.Errorf("transform function must return a table, got %s", result.Type()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update RequestData from the returned table
|
// Update RequestData from the returned table
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ package script
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.aykhans.me/sarin/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RequestData represents the request data passed to scripts for transformation.
|
// RequestData represents the request data passed to scripts for transformation.
|
||||||
@@ -56,9 +56,13 @@ type Source struct {
|
|||||||
// - Escaped "@": strings starting with "@@" (literal "@" at start, returns string without first @)
|
// - Escaped "@": strings starting with "@@" (literal "@" at start, returns string without first @)
|
||||||
// - File reference: "@/path/to/file" or "@./relative/path"
|
// - File reference: "@/path/to/file" or "@./relative/path"
|
||||||
// - URL reference: "@http://..." or "@https://..."
|
// - URL reference: "@http://..." or "@https://..."
|
||||||
|
//
|
||||||
|
// It can return the following errors:
|
||||||
|
// - types.ErrScriptEmpty
|
||||||
|
// - types.ScriptLoadError
|
||||||
func LoadSource(ctx context.Context, source string, engineType EngineType) (*Source, error) {
|
func LoadSource(ctx context.Context, source string, engineType EngineType) (*Source, error) {
|
||||||
if source == "" {
|
if source == "" {
|
||||||
return nil, errors.New("script source cannot be empty")
|
return nil, types.ErrScriptEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
var content string
|
var content string
|
||||||
@@ -77,7 +81,7 @@ func LoadSource(ctx context.Context, source string, engineType EngineType) (*Sou
|
|||||||
content, err = readFile(ref)
|
content, err = readFile(ref)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to load script from %q: %w", ref, err)
|
return nil, types.NewScriptLoadError(ref, err)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
// Inline script
|
// Inline script
|
||||||
@@ -91,12 +95,15 @@ func LoadSource(ctx context.Context, source string, engineType EngineType) (*Sou
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LoadSources loads multiple script sources.
|
// LoadSources loads multiple script sources.
|
||||||
|
// It can return the following errors:
|
||||||
|
// - types.ErrScriptEmpty
|
||||||
|
// - types.ScriptLoadError
|
||||||
func LoadSources(ctx context.Context, sources []string, engineType EngineType) ([]*Source, error) {
|
func LoadSources(ctx context.Context, sources []string, engineType EngineType) ([]*Source, error) {
|
||||||
loaded := make([]*Source, 0, len(sources))
|
loaded := make([]*Source, 0, len(sources))
|
||||||
for i, src := range sources {
|
for _, src := range sources {
|
||||||
source, err := LoadSource(ctx, src, engineType)
|
source, err := LoadSource(ctx, src, engineType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("script[%d]: %w", i, err)
|
return nil, err
|
||||||
}
|
}
|
||||||
loaded = append(loaded, source)
|
loaded = append(loaded, source)
|
||||||
}
|
}
|
||||||
@@ -106,6 +113,12 @@ func LoadSources(ctx context.Context, sources []string, engineType EngineType) (
|
|||||||
// ValidateScript validates a script source by loading it and checking syntax.
|
// ValidateScript validates a script source by loading it and checking syntax.
|
||||||
// It loads the script (from file/URL/inline), parses it, and verifies
|
// It loads the script (from file/URL/inline), parses it, and verifies
|
||||||
// that a 'transform' function is defined.
|
// that a 'transform' function is defined.
|
||||||
|
// It can return the following errors:
|
||||||
|
// - types.ErrScriptEmpty
|
||||||
|
// - types.ErrScriptTransformMissing
|
||||||
|
// - types.ScriptLoadError
|
||||||
|
// - types.ScriptExecutionError
|
||||||
|
// - types.ScriptUnknownEngineError
|
||||||
func ValidateScript(ctx context.Context, source string, engineType EngineType) error {
|
func ValidateScript(ctx context.Context, source string, engineType EngineType) error {
|
||||||
// Load the script source
|
// Load the script source
|
||||||
src, err := LoadSource(ctx, source, engineType)
|
src, err := LoadSource(ctx, source, engineType)
|
||||||
@@ -121,7 +134,7 @@ func ValidateScript(ctx context.Context, source string, engineType EngineType) e
|
|||||||
case EngineTypeJavaScript:
|
case EngineTypeJavaScript:
|
||||||
engine, err = NewJsEngine(src.Content)
|
engine, err = NewJsEngine(src.Content)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unknown engine type: %s", engineType)
|
return types.NewScriptUnknownEngineError(string(engineType))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -134,56 +147,67 @@ func ValidateScript(ctx context.Context, source string, engineType EngineType) e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ValidateScripts validates multiple script sources.
|
// ValidateScripts validates multiple script sources.
|
||||||
|
// It can return the following errors:
|
||||||
|
// - types.ErrScriptEmpty
|
||||||
|
// - types.ErrScriptTransformMissing
|
||||||
|
// - types.ScriptLoadError
|
||||||
|
// - types.ScriptExecutionError
|
||||||
|
// - types.ScriptUnknownEngineError
|
||||||
func ValidateScripts(ctx context.Context, sources []string, engineType EngineType) error {
|
func ValidateScripts(ctx context.Context, sources []string, engineType EngineType) error {
|
||||||
for i, src := range sources {
|
for _, src := range sources {
|
||||||
if err := ValidateScript(ctx, src, engineType); err != nil {
|
if err := ValidateScript(ctx, src, engineType); err != nil {
|
||||||
return fmt.Errorf("script[%d]: %w", i, err)
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetchURL downloads content from an HTTP/HTTPS URL.
|
// fetchURL downloads content from an HTTP/HTTPS URL.
|
||||||
|
// It can return the following errors:
|
||||||
|
// - types.HTTPFetchError
|
||||||
|
// - types.HTTPStatusError
|
||||||
func fetchURL(ctx context.Context, url string) (string, error) {
|
func fetchURL(ctx context.Context, url string) (string, error) {
|
||||||
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to create request: %w", err)
|
return "", types.NewHTTPFetchError(url, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to fetch: %w", err)
|
return "", types.NewHTTPFetchError(url, err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close() //nolint:errcheck
|
defer resp.Body.Close() //nolint:errcheck
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return "", fmt.Errorf("HTTP %d %s", resp.StatusCode, resp.Status)
|
return "", types.NewHTTPStatusError(url, resp.StatusCode, resp.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := io.ReadAll(resp.Body)
|
data, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to read response: %w", err)
|
return "", types.NewHTTPFetchError(url, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(data), nil
|
return string(data), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// readFile reads content from a local file.
|
// readFile reads content from a local file.
|
||||||
|
// It can return the following errors:
|
||||||
|
// - types.FileReadError
|
||||||
func readFile(path string) (string, error) {
|
func readFile(path string) (string, error) {
|
||||||
if !filepath.IsAbs(path) {
|
if !filepath.IsAbs(path) {
|
||||||
pwd, err := os.Getwd()
|
pwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to get working directory: %w", err)
|
return "", types.NewFileReadError(path, err)
|
||||||
}
|
}
|
||||||
path = filepath.Join(pwd, path)
|
path = filepath.Join(pwd, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := os.ReadFile(path) //nolint:gosec
|
data, err := os.ReadFile(path) //nolint:gosec
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to read file: %w", err)
|
return "", types.NewFileReadError(path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(data), nil
|
return string(data), nil
|
||||||
|
|||||||
@@ -6,16 +6,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
// General
|
|
||||||
ErrNoError = errors.New("no error (internal)")
|
|
||||||
|
|
||||||
// CLI
|
|
||||||
ErrCLINoArgs = errors.New("CLI expects arguments but received none")
|
|
||||||
)
|
|
||||||
|
|
||||||
// ======================================== General ========================================
|
// ======================================== General ========================================
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNoError = errors.New("no error (internal)")
|
||||||
|
)
|
||||||
|
|
||||||
type FieldParseError struct {
|
type FieldParseError struct {
|
||||||
Field string
|
Field string
|
||||||
Value string
|
Value string
|
||||||
@@ -131,8 +127,147 @@ func (e UnmarshalError) Unwrap() error {
|
|||||||
return e.error
|
return e.error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ======================================== General I/O ========================================
|
||||||
|
|
||||||
|
type FileReadError struct {
|
||||||
|
Path string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFileReadError(path string, err error) FileReadError {
|
||||||
|
if err == nil {
|
||||||
|
err = ErrNoError
|
||||||
|
}
|
||||||
|
return FileReadError{path, err}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e FileReadError) Error() string {
|
||||||
|
return fmt.Sprintf("failed to read file %s: %v", e.Path, e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e FileReadError) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTPFetchError struct {
|
||||||
|
URL string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTPFetchError(url string, err error) HTTPFetchError {
|
||||||
|
if err == nil {
|
||||||
|
err = ErrNoError
|
||||||
|
}
|
||||||
|
return HTTPFetchError{url, err}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e HTTPFetchError) Error() string {
|
||||||
|
return fmt.Sprintf("failed to fetch %s: %v", e.URL, e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e HTTPFetchError) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTPStatusError struct {
|
||||||
|
URL string
|
||||||
|
StatusCode int
|
||||||
|
Status string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTPStatusError(url string, statusCode int, status string) HTTPStatusError {
|
||||||
|
return HTTPStatusError{url, statusCode, status}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e HTTPStatusError) Error() string {
|
||||||
|
return fmt.Sprintf("HTTP %d %s (url: %s)", e.StatusCode, e.Status, e.URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
type URLParseError struct {
|
||||||
|
URL string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewURLParseError(url string, err error) URLParseError {
|
||||||
|
if err == nil {
|
||||||
|
err = ErrNoError
|
||||||
|
}
|
||||||
|
return URLParseError{url, err}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e URLParseError) Error() string {
|
||||||
|
return fmt.Sprintf("invalid URL %q: %v", e.URL, e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e URLParseError) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================================== Template ========================================
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrFileCacheNotInitialized = errors.New("file cache is not initialized")
|
||||||
|
ErrFormDataOddArgs = errors.New("body_FormData requires an even number of arguments (key-value pairs)")
|
||||||
|
)
|
||||||
|
|
||||||
|
type TemplateParseError struct {
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTemplateParseError(err error) TemplateParseError {
|
||||||
|
if err == nil {
|
||||||
|
err = ErrNoError
|
||||||
|
}
|
||||||
|
return TemplateParseError{err}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e TemplateParseError) Error() string {
|
||||||
|
return "template parse error: " + e.Err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e TemplateParseError) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
type TemplateRenderError struct {
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTemplateRenderError(err error) TemplateRenderError {
|
||||||
|
if err == nil {
|
||||||
|
err = ErrNoError
|
||||||
|
}
|
||||||
|
return TemplateRenderError{err}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e TemplateRenderError) Error() string {
|
||||||
|
return "template rendering: " + e.Err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e TemplateRenderError) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================================== YAML ========================================
|
||||||
|
|
||||||
|
type YAMLFormatError struct {
|
||||||
|
Detail string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewYAMLFormatError(detail string) YAMLFormatError {
|
||||||
|
return YAMLFormatError{detail}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e YAMLFormatError) Error() string {
|
||||||
|
return e.Detail
|
||||||
|
}
|
||||||
|
|
||||||
// ======================================== CLI ========================================
|
// ======================================== CLI ========================================
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrCLINoArgs = errors.New("CLI expects arguments but received none")
|
||||||
|
)
|
||||||
|
|
||||||
type CLIUnexpectedArgsError struct {
|
type CLIUnexpectedArgsError struct {
|
||||||
Args []string
|
Args []string
|
||||||
}
|
}
|
||||||
@@ -168,6 +303,61 @@ func (e ConfigFileReadError) Unwrap() error {
|
|||||||
|
|
||||||
// ======================================== Proxy ========================================
|
// ======================================== Proxy ========================================
|
||||||
|
|
||||||
|
type ProxyUnsupportedSchemeError struct {
|
||||||
|
Scheme string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProxyUnsupportedSchemeError(scheme string) ProxyUnsupportedSchemeError {
|
||||||
|
return ProxyUnsupportedSchemeError{scheme}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ProxyUnsupportedSchemeError) Error() string {
|
||||||
|
return "unsupported proxy scheme: " + e.Scheme
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProxyParseError struct {
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProxyParseError(err error) ProxyParseError {
|
||||||
|
if err == nil {
|
||||||
|
err = ErrNoError
|
||||||
|
}
|
||||||
|
return ProxyParseError{err}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ProxyParseError) Error() string {
|
||||||
|
return "failed to parse proxy URL: " + e.Err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ProxyParseError) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProxyConnectError struct {
|
||||||
|
Status string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProxyConnectError(status string) ProxyConnectError {
|
||||||
|
return ProxyConnectError{status}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ProxyConnectError) Error() string {
|
||||||
|
return "proxy CONNECT failed: " + e.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProxyResolveError struct {
|
||||||
|
Host string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProxyResolveError(host string) ProxyResolveError {
|
||||||
|
return ProxyResolveError{host}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ProxyResolveError) Error() string {
|
||||||
|
return "no IP addresses found for host: " + e.Host
|
||||||
|
}
|
||||||
|
|
||||||
type ProxyDialError struct {
|
type ProxyDialError struct {
|
||||||
Proxy string
|
Proxy string
|
||||||
Err error
|
Err error
|
||||||
@@ -187,3 +377,86 @@ func (e ProxyDialError) Error() string {
|
|||||||
func (e ProxyDialError) Unwrap() error {
|
func (e ProxyDialError) Unwrap() error {
|
||||||
return e.Err
|
return e.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ======================================== Script ========================================
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrScriptEmpty = errors.New("script cannot be empty")
|
||||||
|
ErrScriptSourceEmpty = errors.New("script source cannot be empty after @")
|
||||||
|
ErrScriptTransformMissing = errors.New("script must define a global 'transform' function")
|
||||||
|
ErrScriptTransformReturnObject = errors.New("transform function must return an object")
|
||||||
|
ErrScriptURLNoHost = errors.New("script URL must have a host")
|
||||||
|
)
|
||||||
|
|
||||||
|
type ScriptLoadError struct {
|
||||||
|
Source string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScriptLoadError(source string, err error) ScriptLoadError {
|
||||||
|
if err == nil {
|
||||||
|
err = ErrNoError
|
||||||
|
}
|
||||||
|
return ScriptLoadError{source, err}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ScriptLoadError) Error() string {
|
||||||
|
return fmt.Sprintf("failed to load script from %q: %v", e.Source, e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ScriptLoadError) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScriptExecutionError struct {
|
||||||
|
EngineType string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScriptExecutionError(engineType string, err error) ScriptExecutionError {
|
||||||
|
if err == nil {
|
||||||
|
err = ErrNoError
|
||||||
|
}
|
||||||
|
return ScriptExecutionError{engineType, err}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ScriptExecutionError) Error() string {
|
||||||
|
return fmt.Sprintf("%s script error: %v", e.EngineType, e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ScriptExecutionError) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScriptChainError struct {
|
||||||
|
EngineType string
|
||||||
|
Index int
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScriptChainError(engineType string, index int, err error) ScriptChainError {
|
||||||
|
if err == nil {
|
||||||
|
err = ErrNoError
|
||||||
|
}
|
||||||
|
return ScriptChainError{engineType, index, err}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ScriptChainError) Error() string {
|
||||||
|
return fmt.Sprintf("%s script[%d]: %v", e.EngineType, e.Index, e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ScriptChainError) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScriptUnknownEngineError struct {
|
||||||
|
EngineType string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScriptUnknownEngineError(engineType string) ScriptUnknownEngineError {
|
||||||
|
return ScriptUnknownEngineError{engineType}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ScriptUnknownEngineError) Error() string {
|
||||||
|
return "unknown engine type: " + e.EngineType
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,6 +16,9 @@ func (proxies *Proxies) Append(proxy ...Proxy) {
|
|||||||
*proxies = append(*proxies, proxy...)
|
*proxies = append(*proxies, proxy...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse parses a raw proxy string and appends it to the list.
|
||||||
|
// It can return the following errors:
|
||||||
|
// - ProxyParseError
|
||||||
func (proxies *Proxies) Parse(rawValue string) error {
|
func (proxies *Proxies) Parse(rawValue string) error {
|
||||||
parsedProxy, err := ParseProxy(rawValue)
|
parsedProxy, err := ParseProxy(rawValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -27,10 +29,13 @@ func (proxies *Proxies) Parse(rawValue string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseProxy parses a raw proxy URL string into a Proxy.
|
||||||
|
// It can return the following errors:
|
||||||
|
// - ProxyParseError
|
||||||
func ParseProxy(rawValue string) (*Proxy, error) {
|
func ParseProxy(rawValue string) (*Proxy, error) {
|
||||||
urlParsed, err := url.Parse(rawValue)
|
urlParsed, err := url.Parse(rawValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse proxy URL: %w", err)
|
return nil, NewProxyParseError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyParsed := Proxy(*urlParsed)
|
proxyParsed := Proxy(*urlParsed)
|
||||||
|
|||||||
Reference in New Issue
Block a user