Skip to content

Instantly share code, notes, and snippets.

@yowu
Last active October 29, 2025 09:20
Show Gist options
  • Select an option

  • Save yowu/f7dc34bd4736a65ff28d to your computer and use it in GitHub Desktop.

Select an option

Save yowu/f7dc34bd4736a65ff28d to your computer and use it in GitHub Desktop.
A simple HTTP proxy by Golang
package main
import (
"flag"
"io"
"log"
"net"
"net/http"
"strings"
)
// 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-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 appendHostToXForwardHeader(header http.Header, host string) {
// If we aren't the first proxy retain prior
// X-Forwarded-For information as a comma+space
// separated list and fold multiple headers into one.
if prior, ok := header["X-Forwarded-For"]; ok {
host = strings.Join(prior, ", ") + ", " + host
}
header.Set("X-Forwarded-For", host)
}
type proxy struct {
}
func (p *proxy) ServeHTTP(wr http.ResponseWriter, req *http.Request) {
log.Println(req.RemoteAddr, " ", req.Method, " ", req.URL)
if req.URL.Scheme != "http" && req.URL.Scheme != "https" {
msg := "unsupported protocal scheme "+req.URL.Scheme
http.Error(wr, msg, http.StatusBadRequest)
log.Println(msg)
return
}
client := &http.Client{}
//http: Request.RequestURI can't be set in client requests.
//http://golang.org/src/pkg/net/http/client.go
req.RequestURI = ""
delHopHeaders(req.Header)
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
appendHostToXForwardHeader(req.Header, clientIP)
}
resp, err := client.Do(req)
if err != nil {
http.Error(wr, "Server Error", http.StatusInternalServerError)
log.Fatal("ServeHTTP:", err)
}
defer resp.Body.Close()
log.Println(req.RemoteAddr, " ", resp.Status)
delHopHeaders(resp.Header)
copyHeader(wr.Header(), resp.Header)
wr.WriteHeader(resp.StatusCode)
io.Copy(wr, resp.Body)
}
func main() {
var addr = flag.String("addr", "127.0.0.1:8080", "The addr of the application.")
flag.Parse()
handler := &proxy{}
log.Println("Starting proxy server on", *addr)
if err := http.ListenAndServe(*addr, handler); err != nil {
log.Fatal("ListenAndServe:", err)
}
}
@carnerito

Copy link
Copy Markdown

Great gist! I have one question. Why do you create a new instance of http.Client for each request? As stated in Golang documentation for http Client, it is concurrent safe and can be used concurrently by multiple goroutines?

Is there some downside of having a global http Client?

@Yapcheekian

Copy link
Copy Markdown
if req.URL.Scheme != "http" && req.URL.Scheme != "https" {
		msg := "unsupported protocal scheme " + req.URL.Scheme
		http.Error(wr, msg, http.StatusBadRequest)
		log.Println(msg)
		return
	}

i think this part is redundant since you are using ListenAndServe

@aep

aep commented Feb 16, 2022

Copy link
Copy Markdown

hopHeaders needs Content-Length i think?

@jkopczyn

jkopczyn commented Jun 6, 2022

Copy link
Copy Markdown

The req.URL.Scheme is nonfunctional - req.URL will always have a blank Scheme, as discussed in golang/go#28940. So this will always error out.

@ppdouble

Copy link
Copy Markdown
VERSION = $(shell git describe --tags --always --dirty)
LDFLAGS=-ldflags "-X main.version=$(VERSION)"
OSARCH=$(shell go env GOHOSTOS)-$(shell go env GOHOSTARCH)

SIMPLEPROXYTOOL=\
	goproxytool-darwin-amd64 \
	goproxytool-linux-amd64 \
	goproxytool-windows-amd64.exe

proxytool: goproxytool-$(OSARCH)

$(SIMPLEPROXYTOOL):
	GOOS=$(word 2,$(subst -, ,$@)) GOARCH=$(word 3,$(subst -, ,$(subst .exe,,$@))) go build $(LDFLAGS) -o $@ ./$<

%-$(VERSION).zip: %.exe
	rm -f $@
	zip $@ $<

%-$(VERSION).zip: %
	rm -f $@
	zip $@ $<

clean:
	rm -f goproxytool-*


release:
	$(foreach bin,$(SIMPLEPROXYTOOL),$(subst .exe,,$(bin))-$(VERSION).zip)

rm:
	$(foreach bin,$(SIMPLEPROXYTOOL),$(subst .exe,,$(bin))-$(VERSION).zip)


build: proxytool $(SIMPLEPROXYTOOL) clean rm

.PHONY: build
all: build

A MakeFile following this link

@dko1905

dko1905 commented Dec 25, 2024

Copy link
Copy Markdown

For anyone looking for an ever better solution, use the std

The Go std contains a helper for creating a simple reverse proxy, net/http/httputil.ReverseProxy:

target := <a URL>

proxy := &ReverseProxy{
	Rewrite: func(r *ProxyRequest) {
		r.SetURL(target)
		r.Out.Host = r.In.Host // if desired
	},
}

requestHandle := func(w.., r..) {
     proxy.ServeHTTP(w, r)
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment