diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e660fd9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +bin/ diff --git a/.gitignore b/.gitignore index 66fd13c..58bb2f3 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ # Dependency directories (remove the comment below to include it) # vendor/ +bin/ +*.snap diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..706e8d2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM golang AS build + +WORKDIR /go/src/github.com/Snawoot/dumbproxy +COPY . . +RUN CGO_ENABLED=0 go build -a -tags netgo -ldflags '-s -w -extldflags "-static"' +ADD https://curl.haxx.se/ca/cacert.pem /certs.crt +RUN chmod 0644 /certs.crt + +FROM scratch AS arrange +COPY --from=build /go/src/github.com/Snawoot/dumbproxy/dumbproxy / +COPY --from=build /certs.crt /etc/ssl/certs/ca-certificates.crt + +FROM scratch +COPY --from=arrange / / +USER 9999:9999 +EXPOSE 8080/tcp +ENTRYPOINT ["/dumbproxy", "-bind-address", ":8080"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0c9d500 --- /dev/null +++ b/Makefile @@ -0,0 +1,56 @@ +PROGNAME = dumbproxy +OUTSUFFIX = bin/$(PROGNAME) +BUILDOPTS = -a -tags netgo +LDFLAGS = -ldflags '-s -w -extldflags "-static"' + +src = $(wildcard *.go) + +native: bin-native +all: bin-linux-amd64 bin-linux-386 bin-linux-arm \ + bin-freebsd-amd64 bin-freebsd-386 bin-freebsd-arm \ + bin-darwin-amd64 \ + bin-windows-amd64 bin-windows-386 + +bin-native: $(OUTSUFFIX) +bin-linux-amd64: $(OUTSUFFIX).linux-amd64 +bin-linux-386: $(OUTSUFFIX).linux-386 +bin-linux-arm: $(OUTSUFFIX).linux-arm +bin-freebsd-amd64: $(OUTSUFFIX).freebsd-amd64 +bin-freebsd-386: $(OUTSUFFIX).freebsd-386 +bin-freebsd-arm: $(OUTSUFFIX).freebsd-arm +bin-darwin-amd64: $(OUTSUFFIX).darwin-amd64 +bin-windows-amd64: $(OUTSUFFIX).windows-amd64.exe +bin-windows-386: $(OUTSUFFIX).windows-386.exe + +$(OUTSUFFIX): $(src) + CGO_ENABLED=0 go build $(BUILDOPTS) $(LDFLAGS) -o $@ + +$(OUTSUFFIX).linux-amd64: $(src) + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(BUILDOPTS) $(LDFLAGS) -o $@ + +$(OUTSUFFIX).linux-386: $(src) + CGO_ENABLED=0 GOOS=linux GOARCH=386 go build $(BUILDOPTS) $(LDFLAGS) -o $@ + +$(OUTSUFFIX).linux-arm: $(src) + CGO_ENABLED=0 GOOS=linux GOARCH=arm go build $(BUILDOPTS) $(LDFLAGS) -o $@ + +$(OUTSUFFIX).freebsd-amd64: $(src) + CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build $(BUILDOPTS) $(LDFLAGS) -o $@ + +$(OUTSUFFIX).freebsd-386: $(src) + CGO_ENABLED=0 GOOS=freebsd GOARCH=386 go build $(BUILDOPTS) $(LDFLAGS) -o $@ + +$(OUTSUFFIX).freebsd-arm: $(src) + CGO_ENABLED=0 GOOS=freebsd GOARCH=arm go build $(BUILDOPTS) $(LDFLAGS) -o $@ + +$(OUTSUFFIX).darwin-amd64: $(src) + CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build $(BUILDOPTS) $(LDFLAGS) -o $@ + +$(OUTSUFFIX).windows-amd64.exe: $(src) + CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build $(BUILDOPTS) $(LDFLAGS) -o $@ + +$(OUTSUFFIX).windows-386.exe: $(src) + CGO_ENABLED=0 GOOS=windows GOARCH=386 go build $(BUILDOPTS) $(LDFLAGS) -o $@ + +clean: + rm -f bin/* diff --git a/condlog.go b/condlog.go new file mode 100644 index 0000000..cb76d2b --- /dev/null +++ b/condlog.go @@ -0,0 +1,58 @@ +package main + +import ( + "log" + "fmt" +) + +const ( + CRITICAL = 50 + ERROR = 40 + WARNING = 30 + INFO = 20 + DEBUG = 10 + NOTSET = 0 +) + +type CondLogger struct { + logger *log.Logger + verbosity int +} + +func (cl *CondLogger) Log(verb int, format string, v ...interface{}) error { + if verb >= cl.verbosity { + return cl.logger.Output(2, fmt.Sprintf(format, v...)) + } + return nil +} + +func (cl *CondLogger) log(verb int, format string, v ...interface{}) error { + if verb >= cl.verbosity { + return cl.logger.Output(3, fmt.Sprintf(format, v...)) + } + return nil +} + +func (cl *CondLogger) Critical(s string, v ...interface{}) error { + return cl.log(CRITICAL, "CRITICAL " + s, v...) +} + +func (cl *CondLogger) Error(s string, v ...interface{}) error { + return cl.log(ERROR, "ERROR " + s, v...) +} + +func (cl *CondLogger) Warning(s string, v ...interface{}) error { + return cl.log(WARNING, "WARNING " + s, v...) +} + +func (cl *CondLogger) Info(s string, v ...interface{}) error { + return cl.log(INFO, "INFO " + s, v...) +} + +func (cl *CondLogger) Debug(s string, v ...interface{}) error { + return cl.log(DEBUG, "DEBUG " + s, v...) +} + +func NewCondLogger(logger *log.Logger, verbosity int) *CondLogger { + return &CondLogger{verbosity: verbosity, logger: logger} +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..bf15fb6 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/Snawoot/dumbproxy + +go 1.13 diff --git a/handler.go b/handler.go new file mode 100644 index 0000000..d643868 --- /dev/null +++ b/handler.go @@ -0,0 +1,73 @@ +package main + +import ( + "io" + "net" + "fmt" + "net/http" + "strings" +) + +type ProxyHandler struct { + logger *CondLogger + httptransport http.RoundTripper +} + +func NewProxyHandler(logger *CondLogger) *ProxyHandler { + httptransport := &http.Transport{} + return &ProxyHandler{ + logger: logger, + httptransport: httptransport, + } +} + +func (s *ProxyHandler) HandleTunnel(wr http.ResponseWriter, req *http.Request) { + conn, err := net.Dial("tcp", req.RequestURI) + if err != nil { + s.logger.Error("Can't satisfy CONNECT request: %v", err) + http.Error(wr, "Can't satisfy CONNECT request", http.StatusBadGateway) + return + } + defer conn.Close() + + + // Upgrade client connection + localconn, _, err := hijack(wr) + if err != nil { + s.logger.Error("Can't hijack client connection: %v", err) + http.Error(wr, "Can't hijack client connection", http.StatusInternalServerError) + return + } + defer localconn.Close() + + // Inform client connection is built + fmt.Fprintf(localconn, "HTTP/%d.%d 200 OK\r\n\r\n", req.ProtoMajor, req.ProtoMinor) + + proxy(req.Context(), localconn, conn) +} + +func (s *ProxyHandler) HandleRequest(wr http.ResponseWriter, req *http.Request) { + req.RequestURI = "" + resp, err := s.httptransport.RoundTrip(req) + if err != nil { + s.logger.Error("HTTP fetch error: %v", err) + http.Error(wr, "Server Error", http.StatusInternalServerError) + return + } + defer resp.Body.Close() + s.logger.Info("%v %v %v %v", req.RemoteAddr, req.Method, req.URL, resp.Status) + delHopHeaders(resp.Header) + copyHeader(wr.Header(), resp.Header) + wr.WriteHeader(resp.StatusCode) + io.Copy(wr, resp.Body) +} + +func (s *ProxyHandler) ServeHTTP(wr http.ResponseWriter, req *http.Request) { + s.logger.Info("Request: %v %v %v", req.RemoteAddr, req.Method, req.URL) + delHopHeaders(req.Header) + if strings.ToUpper(req.Method) == "CONNECT" { + s.HandleTunnel(wr, req) + } else { + s.HandleRequest(wr, req) + } +} diff --git a/logwriter.go b/logwriter.go new file mode 100644 index 0000000..a880db0 --- /dev/null +++ b/logwriter.go @@ -0,0 +1,57 @@ +package main + +import ( + "io" + "errors" + "time" +) + +const MAX_LOG_QLEN = 128 +const QUEUE_SHUTDOWN_TIMEOUT = 500 * time.Millisecond + +type LogWriter struct { + writer io.Writer + ch chan []byte + done chan struct{} +} + +func (lw *LogWriter) Write(p []byte) (int, error) { + if p == nil { + return 0, errors.New("Can't write nil byte slice") + } + buf := make([]byte, len(p)) + copy(buf, p) + select { + case lw.ch <- buf: + return len(p), nil + default: + return 0, errors.New("Writer queue overflow") + } +} + +func NewLogWriter(writer io.Writer) *LogWriter { + lw := &LogWriter{writer, + make(chan []byte, MAX_LOG_QLEN), + make(chan struct{})} + go lw.loop() + return lw +} + +func (lw *LogWriter) loop() { + for p := range lw.ch { + if p == nil { + break + } + lw.writer.Write(p) + } + lw.done <- struct{}{} +} + +func (lw *LogWriter) Close() { + lw.ch <- nil + timer := time.After(QUEUE_SHUTDOWN_TIMEOUT) + select { + case <-timer: + case <-lw.done: + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..b51e8f8 --- /dev/null +++ b/main.go @@ -0,0 +1,63 @@ +package main + +import ( + "log" + "os" + "fmt" + "flag" + "time" + "net/http" +) + +func perror(msg string) { + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, msg) +} + +func arg_fail(msg string) { + perror(msg) + perror("Usage:") + flag.PrintDefaults() + os.Exit(2) +} + +type CLIArgs struct { + bind_address string + verbosity int + timeout time.Duration +} + + +func parse_args() CLIArgs { + var args CLIArgs + flag.StringVar(&args.bind_address, "bind-address", ":8080", "HTTP proxy listen address") + flag.IntVar(&args.verbosity, "verbosity", 20, "logging verbosity " + + "(10 - debug, 20 - info, 30 - warning, 40 - error, 50 - critical)") + flag.DurationVar(&args.timeout, "timeout", 10 * time.Second, "timeout for network operations") + flag.Parse() + return args +} + +func run() int { + args := parse_args() + + logWriter := NewLogWriter(os.Stderr) + defer logWriter.Close() + + mainLogger := NewCondLogger(log.New(logWriter, "MAIN : ", + log.LstdFlags | log.Lshortfile), + args.verbosity) + proxyLogger := NewCondLogger(log.New(logWriter, "PROXY : ", + log.LstdFlags | log.Lshortfile), + args.verbosity) + mainLogger.Info("Starting proxy server...") + handler := NewProxyHandler(proxyLogger) + err := http.ListenAndServe(args.bind_address, handler) + mainLogger.Critical("Server terminated with a reason: %v", err) + mainLogger.Info("Shutting down...") + return 0 +} + +func main() { + os.Exit(run()) +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..072f80c --- /dev/null +++ b/utils.go @@ -0,0 +1,84 @@ +package main + +import ( + "context" + "net" + "sync" + "io" + "time" + "errors" + "net/http" + "bufio" +) + +func proxy(ctx context.Context, left, right net.Conn) { + wg := sync.WaitGroup{} + cpy := func (dst, src net.Conn) { + defer wg.Done() + io.Copy(dst, src) + dst.Close() + } + wg.Add(2) + go cpy(left, right) + go cpy(right, left) + groupdone := make(chan struct{}, 1) + go func() { + wg.Wait() + groupdone <-struct{}{} + }() + select { + case <-ctx.Done(): + left.Close() + right.Close() + case <-groupdone: + return + } + <-groupdone + return +} + +// Hop-by-hop headers. These are removed when sent to the backend. +// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html +var hopHeaders = []string{ + "Connection", + "Keep-Alive", + "Proxy-Authenticate", + "Proxy-Connection", + "Proxy-Authorization", + "Te", // canonicalized version of "TE" + "Trailers", + "Transfer-Encoding", + "Upgrade", +} + +func copyHeader(dst, src http.Header) { + for k, vv := range src { + for _, v := range vv { + dst.Add(k, v) + } + } +} + +func delHopHeaders(header http.Header) { + for _, h := range hopHeaders { + header.Del(h) + } +} + +func hijack(hijackable interface{}) (net.Conn, *bufio.ReadWriter, error) { + hj, ok := hijackable.(http.Hijacker) + if !ok { + return nil, nil, errors.New("Connection doesn't support hijacking") + } + conn, rw, err := hj.Hijack() + if err != nil { + return nil, nil, err + } + var emptytime time.Time + err = conn.SetDeadline(emptytime) + if err != nil { + conn.Close() + return nil, nil, err + } + return conn, rw, nil +}