Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
kitos9112 committed Jul 23, 2023
0 parents commit 08d10db
Show file tree
Hide file tree
Showing 9 changed files with 322 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist/
cert.pem
45 changes: 45 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com
before:
hooks:
# You may remove this if you don't use go modules.
- go mod tidy
# you may remove this if you don't need go generate
- go generate ./...
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin

archives:
- format: tar.gz
# this name template makes the OS and Arch compatible with the results of uname.
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
# use zip for windows archives
format_overrides:
- goos: windows
format: zip
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'

# The lines beneath this are called `modelines`. See `:help modeline`
# Feel free to remove those if you don't want/use them.
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
37 changes: 37 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
VERSION = $(shell git describe --tags --always --dirty)
LDFLAGS=-ldflags "-X main.version=$(VERSION)"
OSARCH=$(shell go env GOHOSTOS)-$(shell go env GOHOSTARCH)

SIMPLEPROXYTOOL=\
go-hhtp-proxy-darwin-amd64 \
go-http-proxy-linux-amd64 \
go-http-proxy-windows-amd64.exe

proxytool: go-http-proxy-$(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 go-http-proxy-*


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
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Go HTTP Proxy

Go HTTP Proxy is a simple proxy server written in Go. It provides the ability to proxy HTTP and HTTPS traffic leveraging the HTTP CONNECT Method.

This tool is useful when you need to debug or inspect network requests, or to test your client code in presence of proxies.

## Features

- Supports both HTTP and HTTPS. Forwards HTTPS traffic using the HTTP CONNECT method.
- Provides an option to increase verbosity to log detailed information about the traffic.
- Supports custom TLS certificates (`-cert` and `-key` flags).

## Limitations

- This tool is primarily for debugging and development purposes. It might not be suited for production-level traffic or security demands.
- The proxy does not modify or interfere with the content of the traffic, it merely passes it along. This means it will not remove certain headers or modify request/response bodies.

## Usage

First, download the latest binary for your platform from the GitHub releases page. Then you can run the proxy with:

By default, the proxy will start an HTTP server on port 8888. By default, the proxy will start an HTTP server on port 8888.

```bash
./go-http-proxy --help
-cert string
Path to TLS certificate. If not provided and https is set, a self-signed certificate will be generated and saved to cert.pem in the current directory.
-https
Use HTTPS for proxy
-key string
Path to TLS key. If not provided and https is set, a self-signed certificate will be generated.
-port string
Port to listen on (default "8888")
-v Increase verbosity
```

To start an HTTPS server, use the `-https` flag and optionally provide paths to your certificate and key files.

If `key` and `cert` paths are not passed, the program will generate a self-signed certificate and key with a CNAME and DNS SAN of `localhost` for easy DNS resolution during local tests. Then, you'd either have to add this cert to the `ca-certificates` store on your machine, or avoid verifying the cert in the client.

```bash
./go-http-proxy -https
```
Binary file added go-http-proxy
Binary file not shown.
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/go-http-proxy

go 1.20

require github.com/hauke96/sigolo v1.0.1
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/hauke96/sigolo v1.0.1 h1:o4E1Qv5BPqHRxXfzpakRdIygTUUzpslqkwagaCbr0rg=
github.com/hauke96/sigolo v1.0.1/go.mod h1:HjmtTXJhUyF8xPUnNt9i6oUkx7+Py5NZZiLc2V7khxg=
150 changes: 150 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package main

import (
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"flag"
"io"
"log"
"math/big"
"net"
"net/http"
"os"
"time"
)

var (
verbose = flag.Bool("v", false, "Increase verbosity")
port = flag.String("port", "8888", "Port to listen on")
https = flag.Bool("https", false, "Use HTTPS for proxy")
certificate = flag.String("cert", "", "Path to TLS certificate. If not provided and https is set, a self-signed certificate will be generated and saved to cert.pem in the current directory.")
key = flag.String("key", "", "Path to TLS key. If not provided and https is set, a self-signed certificate will be generated.")
)

func verbosePrintln(logger *log.Logger, msg string) {
if *verbose {
logger.Println(msg)
}
}

func handleTunneling(w http.ResponseWriter, r *http.Request) {
destConn, err := net.DialTimeout("tcp", r.Host, 10*time.Second)
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}

verbosePrintln(log.New(io.Discard, "", log.LstdFlags), "Handling CONNECT request for "+r.Host)

w.WriteHeader(http.StatusOK)
hijacker, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "Hijacking not supported", http.StatusInternalServerError)
return
}
clientConn, _, err := hijacker.Hijack()
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
}
go transfer(destConn, clientConn)
go transfer(clientConn, destConn)
}

func transfer(destination io.WriteCloser, source io.ReadCloser) {
defer destination.Close()
defer source.Close()
_, err := io.Copy(destination, source)
if err != nil && *verbose {
log.Println("Error while transferring data:", err)
}
}

func handleHTTP(w http.ResponseWriter, r *http.Request) {
verbosePrintln(log.New(io.Discard, "", log.LstdFlags), "Handling HTTP request for "+r.URL.String())
resp, err := http.DefaultTransport.RoundTrip(r)
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
defer resp.Body.Close()
copyHeader(w.Header(), resp.Header)
w.WriteHeader(resp.StatusCode)
_, err = io.Copy(w, resp.Body)
if err != nil && *verbose {
log.Println("Error while copying response body:", err)
}
}

func copyHeader(dst, src http.Header) {
for k, vv := range src {
for _, v := range vv {
dst.Add(k, v)
}
}
}


func main() {
flag.Parse()

server := &http.Server{
Addr: ":" + *port,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodConnect {
handleTunneling(w, r)
} else {
handleHTTP(w, r)
}
}),
}

if *https {
var tlsConfig *tls.Config

if *certificate != "" && *key != "" {
cert, err := tls.LoadX509KeyPair(*certificate, *key)
if err != nil {
log.Fatalf("Failed to load certificate and key: %v", err)
}

tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
} else {
// Generate a new self-signed certificate.
priv, _ := rsa.GenerateKey(rand.Reader, 2048)
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: "localhost",
},
DNSNames : []string{"localhost"},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour * 24 * 365), // Valid for one year.
BasicConstraintsValid: true,
}

certDER, _ := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})

// Write the certificate and key to files.
if err := os.WriteFile("cert.pem", certPEM, 0644); err != nil {
log.Fatalf("Failed to write certificate: %v", err)
}

cert, _ := tls.X509KeyPair(certPEM, keyPEM)
tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
}

server.TLSConfig = tlsConfig

log.Println("Starting HTTPS server on", server.Addr)
log.Fatal(server.ListenAndServeTLS("", ""))
} else {
log.Println("Starting HTTP server on", server.Addr)
log.Fatal(server.ListenAndServe())
}
}
38 changes: 38 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package main

import (
"bytes"
"log"
"net/http"
"strings"
"testing"
)

func TestCopyHeader(t *testing.T) {
dst := make(http.Header)
src := make(http.Header)

src.Add("Content-Type", "text/html")
src.Add("Content-Length", "123")

copyHeader(dst, src)

if dst.Get("Content-Type") != "text/html" || dst.Get("Content-Length") != "123" {
t.Fatal("copyHeader did not correctly copy headers from src to dst")
}
}

func TestVerbosePrintln(t *testing.T) {
var buf bytes.Buffer
logger := log.New(&buf, "", log.LstdFlags)

*verbose = true

verbosePrintln(logger, "Test message")

if !strings.Contains(buf.String(), "Test message") {
t.Fatal("verbosePrintln did not correctly output message to log")
}

*verbose = false
}

0 comments on commit 08d10db

Please sign in to comment.