Build a Golang HTTP Proxy from Scratch: Full Code and Explanation
This article walks through the concepts of forward and reverse proxies, explains HTTP and HTTPS proxy protocols, and provides a complete, step‑by‑step Golang implementation with code samples and diagrams for building a functional HTTP proxy server.
This article introduces how to implement an HTTP proxy server using Go, covering both the theoretical background of proxy types and the practical code needed to run one.
Proxy Basics
A proxy acts as an intermediary that forwards requests from a client to a target server and returns the response. Forward proxies serve client requests, while reverse proxies serve server requests. Different protocols are used for each type, such as HTTP, HTTPS, SOCKS4/5 for forward proxies and TCP, UDP, HTTP, HTTPS for reverse proxies.
HTTP Proxy Overview
In a forward HTTP proxy the client sends a full URL in the request line and uses the Proxy-Connection header instead of Connection. This allows the proxy to identify the target server and remain compatible with older HTTP/1.0 proxies that do not understand the standard Connection header.
// Direct connection
GET / HTTP/1.1
Host: staight.github.io
Connection: keep-alive
// HTTP proxy request
GET http://staight.github.io/ HTTP/1.1
Host: staight.github.io
Proxy-Connection: keep-aliveThe full URL is required because the proxy must know which server to contact, and the Proxy-Connection header prevents connection‑handling issues with legacy proxies.
HTTPS Proxy
HTTPS proxies use the CONNECT method. The client establishes a tunnel to the target server, after which all traffic is encrypted and the proxy simply forwards raw TCP data.
CONNECT staight.github.io:443 HTTP/1.1
Host: staight.github.io:443
Proxy-Connection: keep-aliveUnlike HTTP, the request line does not contain a full URL because the proxy does not need to parse the encrypted payload.
Code Implementation
The program creates a TCP listener on port 8080, accepts connections, and handles each in a separate goroutine.
// tcp listener on port 8080
l, err := net.Listen("tcp", ":8080")
if err != nil {
log.Panic(err)
}
for {
client, err := l.Accept()
if err != nil {
log.Panic(err)
}
go handle(client)
}Inside handle, the client request is read into a buffer, the method and URL are parsed, and the target address is derived. For CONNECT (HTTPS) the address is built from the scheme and opaque part; otherwise the host is used, defaulting to port 80 when omitted.
var method, URL, address string
fmt.Sscanf(string(b[:bytes.IndexByte(b[:], '
')]) , "%s%s", &method, &URL)
hostPortURL, err := url.Parse(URL)
if err != nil { log.Println(err); return }
if method == "CONNECT" {
address = hostPortURL.Scheme + ":" + hostPortURL.Opaque
} else {
address = hostPortURL.Host
if strings.Index(hostPortURL.Host, ":") == -1 {
address = hostPortURL.Host + ":80"
}
}The proxy then dials the target server. For HTTPS it sends a "200 Connection established" response to the client before tunneling data; for HTTP it forwards the original request bytes.
server, err := net.Dial("tcp", address)
if err != nil { log.Println(err); return }
if method == "CONNECT" {
fmt.Fprint(client, "HTTP/1.1 200 Connection established
")
} else {
server.Write(b[:n])
}
// bidirectional copy
go io.Copy(server, client)
io.Copy(client, server)The full source code is provided below, followed by screenshots of the proxy running.
package main
import (
"bytes"
"fmt"
"io"
"log"
"net"
"net/url"
"strings"
)
func main() {
l, err := net.Listen("tcp", ":8080")
if err != nil { log.Panic(err) }
for {
client, err := l.Accept()
if err != nil { log.Panic(err) }
go handle(client)
}
}
func handle(client net.Conn) {
if client == nil { return }
defer client.Close()
log.Printf("remote addr: %v
", client.RemoteAddr())
var b [1024]byte
n, err := client.Read(b[:])
if err != nil { log.Println(err); return }
var method, URL, address string
fmt.Sscanf(string(b[:bytes.IndexByte(b[:], '
')]), "%s%s", &method, &URL)
hostPortURL, err := url.Parse(URL)
if err != nil { log.Println(err); return }
if method == "CONNECT" {
address = hostPortURL.Scheme + ":" + hostPortURL.Opaque
} else {
address = hostPortURL.Host
if strings.Index(hostPortURL.Host, ":") == -1 {
address = hostPortURL.Host + ":80"
}
}
server, err := net.Dial("tcp", address)
if err != nil { log.Println(err); return }
if method == "CONNECT" {
fmt.Fprint(client, "HTTP/1.1 200 Connection established
")
} else {
server.Write(b[:n])
}
go io.Copy(server, client)
io.Copy(client, server)
}Reference: https://blog.csdn.net/weixin_43507410/article/details/124839308
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Go Development Architecture Practice
Daily sharing of Golang-related technical articles, practical resources, language news, tutorials, real-world projects, and more. Looking forward to growing together. Let's go!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
