Skip to content

Commit

Permalink
cmd/forwarder: add pac-server and pac-eval commands
Browse files Browse the repository at this point in the history
- PAC files can be read from: stdin, file path, http(s) URL
- PAC are validated before running
- Eval supports custom DNS the same way proxy does, Server doesn't
- It produces meaningful error messages

You can play with some test files from chromium-libpac.

```
go run ./cmd/forwarder pac-eval -p - https://www.google.com/ https://www.google.com/ https://www.google.com/ < pac/testdata/chromium-libpac/side_effects.js
PROXY sideffect_0
PROXY sideffect_1
PROXY sideffect_2
```

You can cross-check the implementations with:

```
go run ./cmd/forwarder pac-server -p pac/testdata/chromium-libpac/side_effects.js --protocol https &
go run ./cmd/forwarder pac-eval --pac https://localhost:8080 http://www.google.com
```

Fixes #87
  • Loading branch information
mmatczuk committed Oct 27, 2022
1 parent df1d665 commit 011b262
Show file tree
Hide file tree
Showing 5 changed files with 246 additions and 1 deletion.
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ linters-settings:
issues:
exclude:
- "can be `fmt.Stringer`"
- "Error return value of `cmd.MarkFlag.+` is not checked"
- "string `https?` has \\d+ occurrences"
- "Magic number: 0o"
- "`nop.+` is unused"
Expand Down
5 changes: 5 additions & 0 deletions bind/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ func DNSConfig(fs *pflag.FlagSet, cfg *forwarder.DNSConfig) {
"dns-timeout", cfg.Timeout, "timeout for DNS queries if DNS server is specified")
}

func PAC(fs *pflag.FlagSet, pac **url.URL) {
fs.VarP(anyflag.NewValue[*url.URL](*pac, pac, fileurl.ParseFilePathOrURL),
"pac", "p", "local file `path or URL` to PAC content, use \"-\" to read from stdin")
}

func HTTPProxyConfig(fs *pflag.FlagSet, cfg *forwarder.HTTPProxyConfig) {
fs.VarP(anyflag.NewValue[*url.URL](cfg.UpstreamProxy, &cfg.UpstreamProxy, forwarder.ParseProxyURL),
"upstream-proxy", "u", "upstream proxy URL")
Expand Down
116 changes: 116 additions & 0 deletions cmd/forwarder/paceval/paceval.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package paceval

import (
"fmt"
"net"
"net/url"
"os"
"strings"

"github.com/saucelabs/forwarder"
"github.com/saucelabs/forwarder/bind"
"github.com/saucelabs/forwarder/pac"
"github.com/spf13/cobra"
)

type command struct {
pac *url.URL
dnsConfig *forwarder.DNSConfig
httpTransportConfig *forwarder.HTTPTransportConfig
}

func (c *command) RunE(cmd *cobra.Command, args []string) error {
var resolver *net.Resolver
if len(c.dnsConfig.Servers) > 0 {
r, err := forwarder.NewResolver(c.dnsConfig, forwarder.NopLogger)
if err != nil {
return err
}
resolver = r
}
t := forwarder.NewHTTPTransport(c.httpTransportConfig, resolver)

script, err := forwarder.ReadURL(c.pac, t)
if err != nil {
return fmt.Errorf("read PAC file: %w", err)
}
cfg := pac.ProxyResolverConfig{
Script: script,
AlertSink: os.Stderr,
}
pr, err := pac.NewProxyResolver(&cfg, resolver)
if err != nil {
return err
}

w := cmd.OutOrStdout()
for _, arg := range args {
u, err := url.Parse(arg)
if err != nil {
return fmt.Errorf("parse URL: %w", err)
}
proxy, err := pr.FindProxyForURL(u)
if err != nil {
return err
}
fmt.Fprintln(w, proxy)
}

return nil
}

func Command() (cmd *cobra.Command) {
c := command{
pac: &url.URL{Scheme: "file", Path: "pac.js"},
dnsConfig: forwarder.DefaultDNSConfig(),
httpTransportConfig: forwarder.DefaultHTTPTransportConfig(),
}

defer func() {
fs := cmd.Flags()

bind.PAC(fs, &c.pac)
bind.DNSConfig(fs, c.dnsConfig)
bind.HTTPTransportConfig(fs, c.httpTransportConfig)

bind.MarkFlagFilename(cmd, "pac")
}()
return &cobra.Command{
Use: "pac-eval --pac <file|url> [flags] <url>...",
Short: "Evaluate a PAC file for given URLs",
Long: long,
RunE: c.RunE,
Example: example + "\n" + supportedFunctions(),
}
}

const long = `Evaluate a PAC file for given URLs.
The PAC file can be specified as a file path or URL with scheme "file", "http" or "https".
The URLs to evaluate are passed as arguments. The output is a list of proxy strings, one per URL.
The PAC file must contain FindProxyForURL or FindProxyForURLEx and must be valid.
All PAC util functions are supported (see below).
Alerts are written to stderr.
`

const example = ` # Evaluate a PAC file for a URL
forwarder pac-eval --pac pac.js https://www.google.com
# Evaluate a PAC file for multiple URLs
forwarder pac-eval --pac pac.js https://www.google.com https://www.facebook.com
# Evaluate a PAC file for multiple URLs using a PAC file from stdin
cat pac.js | forwarder pac-eval --pac - https://www.google.com https://www.facebook.com
# Evaluate a PAC file for multiple URLs using a PAC file from a URL
forwarder pac-eval --pac https://example.com/pac.js https://www.google.com https://www.facebook.com
`

func supportedFunctions() string {
var sb strings.Builder
sb.WriteString("Supported PAC util functions:")
for _, fn := range pac.SupportedFunctions() {
sb.WriteString("\n ")
sb.WriteString(fn)
}
return sb.String()
}
119 changes: 119 additions & 0 deletions cmd/forwarder/pacserver/pacserver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package pacserver

import (
"context"
"fmt"
"net/http"
"net/url"
"os/signal"
"strings"
"syscall"

"github.com/saucelabs/forwarder"
"github.com/saucelabs/forwarder/bind"
"github.com/saucelabs/forwarder/log"
"github.com/saucelabs/forwarder/log/stdlog"
"github.com/saucelabs/forwarder/pac"
"github.com/spf13/cobra"
)

type command struct {
pac *url.URL
httpTransportConfig *forwarder.HTTPTransportConfig
httpServerConfig *forwarder.HTTPServerConfig
logConfig *log.Config
}

func (c *command) RunE(cmd *cobra.Command, args []string) error {
t := forwarder.NewHTTPTransport(c.httpTransportConfig, nil)

script, err := forwarder.ReadURL(c.pac, t)
if err != nil {
return fmt.Errorf("read PAC file: %w", err)
}
if _, err := pac.NewProxyResolver(&pac.ProxyResolverConfig{Script: script}, nil); err != nil {
return err
}

if f := c.logConfig.File; f != nil {
defer f.Close()
}
logger := stdlog.New(c.logConfig)

s, err := forwarder.NewHTTPServer(c.httpServerConfig, servePAC(script), logger.Named("server"))
if err != nil {
return err
}

ctx := context.Background()
ctx, _ = signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
return s.Run(ctx)
}

func servePAC(script string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/x-ns-proxy-autoconfig")
w.Write([]byte(script)) //nolint:errcheck // ignore it
})
}

func Command() (cmd *cobra.Command) {
c := command{
pac: &url.URL{Scheme: "file", Path: "pac.js"},
httpTransportConfig: forwarder.DefaultHTTPTransportConfig(),
httpServerConfig: forwarder.DefaultHTTPServerConfig(),
logConfig: log.DefaultConfig(),
}

defer func() {
fs := cmd.Flags()

bind.PAC(fs, &c.pac)
bind.HTTPTransportConfig(fs, c.httpTransportConfig)
bind.HTTPServerConfig(fs, c.httpServerConfig, "")
bind.LogConfig(fs, c.logConfig)

bind.MarkFlagFilename(cmd, "pac", "cert-file", "key-file", "log-file")
}()
return &cobra.Command{
Use: "pac-server --pac <file|url> [--protocol <http|https|h2>] [--address <host:port>] [flags]",
Short: "Start HTTP(S) server that serves a PAC file",
Long: long,
RunE: c.RunE,
Example: example + "\n" + supportedFunctions(),
}
}

const long = `Start HTTP(S) server that serves a PAC file.
The PAC file can be specified as a file path or URL with scheme "file", "http" or "https".
The PAC file must contain FindProxyForURL or FindProxyForURLEx and must be valid.
All PAC util functions are supported (see below).
Alerts are ignored.
You can start HTTP, HTTPS or H2 (HTTPS) server.
The server may be protected by basic authentication.
If you start an HTTPS server and you don't provide a certificate, the server will generate a self-signed certificate on startup.
`

const example = ` # Start a HTTP server serving a PAC file
pac-server --pac pac.js --protocol http --address localhost:8080
# Start a HTTPS server serving a PAC file
pac-server --pac pac.js --protocol https --address localhost:80443
# Start a HTTPS server serving a PAC file with custom certificate
pac-server --pac pac.js --protocol https --address localhost:80443 --cert-file cert.pem --key-file key.pem
# Start a HTTPS server serving a PAC file with basic authentication
pac-server --pac pac.js --protocol https --address localhost:80443 --basic-auth user:pass
`

func supportedFunctions() string {
var sb strings.Builder
sb.WriteString("Supported PAC util functions:")
for _, fn := range pac.SupportedFunctions() {
sb.WriteString("\n ")
sb.WriteString(fn)
}
return sb.String()
}
6 changes: 5 additions & 1 deletion cmd/forwarder/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
package main

import (
"github.com/saucelabs/forwarder/cmd/forwarder/paceval"
"github.com/saucelabs/forwarder/cmd/forwarder/pacserver"
"github.com/saucelabs/forwarder/cmd/forwarder/proxy"
"github.com/saucelabs/forwarder/cmd/forwarder/version"
"github.com/spf13/cobra"
Expand All @@ -14,14 +16,16 @@ const envPrefix = "FORWARDER"

func rootCommand() *cobra.Command {
rootCmd := &cobra.Command{
Use: "proxy",
Use: "forwarder",
Short: "A simple flexible forward proxy",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
return bindFlagsToEnv(cmd, envPrefix)
},
}

rootCmd.AddCommand(
paceval.Command(),
pacserver.Command(),
proxy.Command(),
version.Command(),
)
Expand Down

0 comments on commit 011b262

Please sign in to comment.