From 17326f38761a518076fd146775aa7f722aff1e5a Mon Sep 17 00:00:00 2001 From: Vladislav Yarmak Date: Tue, 19 May 2020 22:53:13 +0300 Subject: [PATCH] initial code --- .dockerignore | 1 + .gitignore | 2 ++ Dockerfile | 17 +++++++++++ Makefile | 56 ++++++++++++++++++++++++++++++++++ condlog.go | 58 +++++++++++++++++++++++++++++++++++ go.mod | 3 ++ handler.go | 73 ++++++++++++++++++++++++++++++++++++++++++++ logwriter.go | 57 ++++++++++++++++++++++++++++++++++ main.go | 63 ++++++++++++++++++++++++++++++++++++++ utils.go | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 414 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 condlog.go create mode 100644 go.mod create mode 100644 handler.go create mode 100644 logwriter.go create mode 100644 main.go create mode 100644 utils.go 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 +}