Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Caddy2 caddyfile #79

Merged
merged 7 commits into from
Oct 2, 2020
Merged
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 185 additions & 3 deletions forwardproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,23 @@ import (
"net"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"sync"
"time"

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/forwardproxy/httpclient"
"golang.org/x/net/proxy"
)

func init() {
caddy.RegisterModule(Handler{})
httpcaddyfile.RegisterHandlerDirective("forward_proxy", parseCaddyfile)
}

type ProbeResistance struct {
Expand All @@ -46,7 +50,7 @@ type Handler struct {
// port string // port on which chain with forwardproxy is listening on
Hosts caddyhttp.MatchHost `json:"hosts,omitempty"`

ProbeResistance *ProbeResistance
ProbeResistance *ProbeResistance `json:"probe_resistance"`
klzgrad marked this conversation as resolved.
Show resolved Hide resolved

DialTimeout caddy.Duration `json:"dial_timeout,omitempty"` // for initial tcp connection

Expand Down Expand Up @@ -232,7 +236,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
return caddyhttp.Error(http.StatusProxyAuthRequired, authErr)
}

if r.ProtoMajor != 1 && r.ProtoMajor != 2 {
if r.ProtoMajor != 1 && r.ProtoMajor != 2 && r.ProtoMajor != 3 {
klzgrad marked this conversation as resolved.
Show resolved Hide resolved
return caddyhttp.Error(http.StatusHTTPVersionNotSupported,
fmt.Errorf("unsupported HTTP major version: %d", r.ProtoMajor))
}
Expand All @@ -251,7 +255,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht

if r.Method == http.MethodConnect {

if r.ProtoMajor == 2 {
if r.ProtoMajor == 2 || r.ProtoMajor == 3 {
klzgrad marked this conversation as resolved.
Show resolved Hide resolved
if len(r.URL.Scheme) > 0 || len(r.URL.Path) > 0 {
return caddyhttp.Error(http.StatusBadRequest,
fmt.Errorf("CONNECT request has :scheme and/or :path pseudo-header fields"))
Expand All @@ -278,6 +282,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
case 1: // http1: hijack the whole flow
return serveHijack(w, targetConn)
case 2: // http2: keep reading from "request" and writing into same response
fallthrough
case 3:
klzgrad marked this conversation as resolved.
Show resolved Hide resolved
defer r.Body.Close()
wFlusher, ok := w.(http.Flusher)
if !ok {
Expand Down Expand Up @@ -695,8 +701,184 @@ type dialContexter interface {
DialContext(ctx context.Context, network, address string) (net.Conn, error)
}

func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if !d.Next() {
return d.ArgErr()
}
args := d.RemainingArgs()
if len(args) > 0 {
return d.ArgErr()
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
subdirective := d.Val()
args := d.RemainingArgs()
switch subdirective {
case "basicauth":
klzgrad marked this conversation as resolved.
Show resolved Hide resolved
if len(args) != 2 {
return d.ArgErr()
}
if len(args[0]) == 0 {
return d.Err("empty usernames are not allowed")
}
// TODO: Evaluate policy of allowing empty passwords.
if strings.Contains(args[0], ":") {
return d.Err("character ':' in usernames is not allowed")
}
// TODO: Support multiple basicauths.
if h.BasicauthUser != "" || h.BasicauthPass != "" {
return d.Err("Multi-user basicauth is not supported")
}
h.BasicauthUser = args[0]
h.BasicauthPass = args[1]
case "ports":
if len(args) == 0 {
return d.ArgErr()
}
if len(h.WhitelistedPorts) != 0 {
return d.Err("ports subdirective specified twice")
}
h.WhitelistedPorts = make([]int, len(args))
for i, p := range args {
intPort, err := strconv.Atoi(p)
if intPort <= 0 || intPort > 65535 || err != nil {
return d.Err("ports are expected to be space-separated" +
" and in 0-65535 range. Got: " + p)
}
h.WhitelistedPorts[i] = intPort
}
case "hide_ip":
if len(args) != 0 {
return d.ArgErr()
}
h.HideIP = true
case "hide_via":
if len(args) != 0 {
return d.ArgErr()
}
h.HideVia = true
case "probe_resistance":
if len(args) > 1 {
return d.ArgErr()
}
if len(args) == 1 {
lowercaseArg := strings.ToLower(args[0])
if lowercaseArg != args[0] {
log.Println("WARNING: secret domain appears to have uppercase letters in it, which are not visitable")
klzgrad marked this conversation as resolved.
Show resolved Hide resolved
}
h.ProbeResistance = &ProbeResistance{Domain: args[0]}
} else {
h.ProbeResistance = &ProbeResistance{}
}
case "serve_pac":
if len(args) > 1 {
return d.ArgErr()
}
if len(h.PACPath) != 0 {
return d.Err("serve_pac subdirective specified twice")
}
if len(args) == 1 {
h.PACPath = args[0]
if !strings.HasPrefix(h.PACPath, "/") {
h.PACPath = "/" + h.PACPath
}
} else {
h.PACPath = "/proxy.pac"
}
log.Printf("Proxy Auto-Config will be served at %s\n", h.PACPath)
case "response_timeout":
return d.Err("response_timeout not supported yet.")
klzgrad marked this conversation as resolved.
Show resolved Hide resolved
case "dial_timeout":
if len(args) != 1 {
return d.ArgErr()
}
timeout, err := caddy.ParseDuration(args[0])
if err != nil {
return d.ArgErr()
}
if timeout < 0 {
return d.Err("dial_timeout cannot be negative.")
}
h.DialTimeout = caddy.Duration(timeout)
case "upstream":
if len(args) != 1 {
return d.ArgErr()
}
if h.Upstream != "" {
return d.Err("upstream directive specified more than once")
}
h.Upstream = args[0]
case "acl":
for nesting := d.Nesting(); d.NextBlock(nesting); {
aclDirective := d.Val()
args := d.RemainingArgs()
if len(args) == 0 {
return d.ArgErr()
}
var ruleSubjects []string
var err error
aclAllow := false
switch aclDirective {
case "allow":
ruleSubjects = args[:]
aclAllow = true
case "allowfile":
klzgrad marked this conversation as resolved.
Show resolved Hide resolved
if len(args) != 1 {
return d.Err("allowfile accepts a single filename argument")
}
ruleSubjects, err = readLinesFromFile(args[0])
if err != nil {
return err
}
aclAllow = true
case "deny":
ruleSubjects = args[:]
case "denyfile":
klzgrad marked this conversation as resolved.
Show resolved Hide resolved
if len(args) != 1 {
return d.Err("denyfile accepts a single filename argument")
}
ruleSubjects, err = readLinesFromFile(args[0])
if err != nil {
return err
}
default:
return d.Err("expected acl directive: allow/allowfile/deny/denyfile." +
"got: " + aclDirective)
}
ar := ACLRule{Subjects: ruleSubjects, Allow: aclAllow}
h.ACL = append(h.ACL, ar)
}
default:
return d.ArgErr()
}
}
return nil
}

func readLinesFromFile(filename string) ([]string, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()

var hostnames []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
hostnames = append(hostnames, scanner.Text())
}

return hostnames, scanner.Err()
}

func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
var fp Handler
err := fp.UnmarshalCaddyfile(h.Dispenser)
return &fp, err
}

// Interface guards
var (
_ caddy.Provisioner = (*Handler)(nil)
_ caddyhttp.MiddlewareHandler = (*Handler)(nil)
_ caddyfile.Unmarshaler = (*Handler)(nil)
)