Skip to content

Commit

Permalink
Merge branch 'master' into issue-453
Browse files Browse the repository at this point in the history
  • Loading branch information
ErikPelli authored Dec 18, 2024
2 parents b13a546 + 3840d13 commit d4745fa
Show file tree
Hide file tree
Showing 70 changed files with 1,161 additions and 652 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ jobs:
runs-on: ubuntu-latest
steps:

- name: Set up Go 1.13
- name: Set up Go 1.20
uses: actions/setup-go@v1
with:
go-version: 1.13
go-version: 1.20
id: go

- name: Check out code into the Go module directory
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ a `ReqCondition` accepting only requests directed to "www.reddit.com".

`DoFunc` will receive a function that will preprocess the request. We can change the request, or
return a response. If the time is between 8:00am and 17:00pm, we will reject the request, and
return a precanned text response saying "do not waste your time".
return a pre-canned text response saying "do not waste your time".

See additional examples in the examples directory.

Expand Down Expand Up @@ -135,13 +135,14 @@ proxy.OnResponse(Some RespConditions).Do(YourRespHandlerFunc())
For example:

```go
// This rejects the HTTPS request to *.reddit.com during HTTP CONNECT phase
proxy.OnRequest(goproxy.ReqHostMatches(regexp.MustCompile("reddit.*:443$"))).HandleConnect(goproxy.RejectConnect)
// This rejects the HTTPS request to *.reddit.com during HTTP CONNECT phase.
// Reddit URL check is case-insensitive, so the block will work also if the user types something like rEdDit.com.
proxy.OnRequest(goproxy.ReqHostMatches(regexp.MustCompile("(?i)reddit.*:443$"))).HandleConnect(goproxy.AlwaysReject)

// This will NOT reject the HTTPS request with URL ending with gif, due to the fact that proxy
// only got the URL.Hostname and URL.Port during the HTTP CONNECT phase if the scheme is HTTPS, which is
// quiet common these days.
proxy.OnRequest(goproxy.UrlMatches(regexp.MustCompile(`.*gif$`))).HandleConnect(goproxy.RejectConnect)
proxy.OnRequest(goproxy.UrlMatches(regexp.MustCompile(`.*gif$`))).HandleConnect(goproxy.AlwaysReject)

// The correct way to manipulate the HTTP request using URL.Path as condition is:
proxy.OnRequest(goproxy.UrlMatches(regexp.MustCompile(`.*gif$`))).Do(YourReqHandlerFunc())
Expand Down
18 changes: 12 additions & 6 deletions certs.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,21 @@ import (
"crypto/x509"
)

var GoproxyCa tls.Certificate

func init() {
if goproxyCaErr != nil {
panic("Error parsing builtin CA " + goproxyCaErr.Error())
}
// When we included the embedded certificate inside this file, we made
// sure that it was valid.
// If there is an error here, this is a really exceptional case that requires
// a panic. It should NEVER happen!
var err error
GoproxyCa, err = tls.X509KeyPair(CA_CERT, CA_KEY)
if err != nil {
panic("Error parsing builtin CA: " + err.Error())
}

if GoproxyCa.Leaf, err = x509.ParseCertificate(GoproxyCa.Certificate[0]); err != nil {
panic("Error parsing builtin CA " + err.Error())
panic("Error parsing builtin CA leaf: " + err.Error())
}
}

Expand Down Expand Up @@ -107,5 +115,3 @@ pmcjjocD/UCCSuHgbAYNNnO/JdhnSylz1tIg26I+2iLNyeTKIepSNlsBxnkLmqM1
cj/azKBaT04IOMLaN8xfSqitJYSraWMVNgGJM5vfcVaivZnNh0lZBv+qu6YkdM88
4/avCJ8IutT+FcMM+GbGazOm5ALWqUyhrnbLGc4CQMPfe7Il6NxwcrOxT8w=
-----END RSA PRIVATE KEY-----`)

var GoproxyCa, goproxyCaErr = tls.X509KeyPair(CA_CERT, CA_KEY)
7 changes: 6 additions & 1 deletion counterecryptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"crypto/aes"
"crypto/cipher"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
Expand All @@ -26,8 +27,12 @@ func NewCounterEncryptorRandFromKey(key interface{}, seed []byte) (r CounterEncr
if keyBytes, err = x509.MarshalECPrivateKey(key); err != nil {
return
}
case ed25519.PrivateKey:
if keyBytes, err = x509.MarshalPKCS8PrivateKey(key); err != nil {
return
}
default:
err = errors.New("only RSA and ECDSA keys supported")
err = errors.New("only RSA, ED25519 and ECDSA keys supported")
return
}
h := sha256.New()
Expand Down
39 changes: 27 additions & 12 deletions dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package goproxy

import (
"bytes"
"io/ioutil"
"io"
"net"
"net/http"
"regexp"
Expand Down Expand Up @@ -97,15 +97,22 @@ func ReqHostIs(hosts ...string) ReqConditionFunc {
}
}

var localHostIpv4 = regexp.MustCompile(`127\.0\.0\.\d+`)

// IsLocalHost checks whether the destination host is explicitly local host
// (buggy, there can be IPv6 addresses it doesn't catch)
// IsLocalHost checks whether the destination host is localhost.
var IsLocalHost ReqConditionFunc = func(req *http.Request, ctx *ProxyCtx) bool {
return req.URL.Host == "::1" ||
req.URL.Host == "0:0:0:0:0:0:0:1" ||
localHostIpv4.MatchString(req.URL.Host) ||
req.URL.Host == "localhost"
h := req.URL.Hostname()
if h == "localhost" {
return true
}
if ip := net.ParseIP(h); ip != nil {
return ip.IsLoopback()
}

// In case of IPv6 without a port number Hostname() sometimes returns the invalid value.
if ip := net.ParseIP(req.URL.Host); ip != nil {
return ip.IsLoopback()
}

return false
}

// UrlMatches returns a ReqCondition testing whether the destination URL
Expand All @@ -119,8 +126,9 @@ func UrlMatches(re *regexp.Regexp) ReqConditionFunc {

// DstHostIs returns a ReqCondition testing wether the host in the request url is the given string
func DstHostIs(host string) ReqConditionFunc {
host = strings.ToLower(host)
return func(req *http.Request, ctx *ProxyCtx) bool {
return req.URL.Host == host
return strings.ToLower(req.URL.Host) == host
}
}

Expand Down Expand Up @@ -181,6 +189,7 @@ func StatusCodeIs(codes ...int) RespCondition {
// You will use the ReqProxyConds struct to register a ReqHandler, that would filter
// the request, only if all the given ReqCondition matched.
// Typical usage:
//
// proxy.OnRequest(UrlIs("example.com/foo"),UrlMatches(regexp.MustParse(`.*\.exampl.\com\./.*`)).Do(...)
func (proxy *ProxyHttpServer) OnRequest(conds ...ReqCondition) *ReqProxyConds {
return &ReqProxyConds{proxy, conds}
Expand All @@ -201,6 +210,7 @@ func (pcond *ReqProxyConds) DoFunc(f func(req *http.Request, ctx *ProxyCtx) (*ht
// ReqProxyConds.Do will register the ReqHandler on the proxy,
// the ReqHandler will handle the HTTP request if all the conditions
// aggregated in the ReqProxyConds are met. Typical usage:
//
// proxy.OnRequest().Do(handler) // will call handler.Handle(req,ctx) on every request to the proxy
// proxy.OnRequest(cond1,cond2).Do(handler)
// // given request to the proxy, will test if cond1.HandleReq(req,ctx) && cond2.HandleReq(req,ctx) are true
Expand All @@ -227,6 +237,7 @@ func (pcond *ReqProxyConds) Do(h ReqHandler) {
// connection.
// The ConnectAction struct contains possible tlsConfig that will be used for eavesdropping. If nil, the proxy
// will use the default tls configuration.
//
// proxy.OnRequest().HandleConnect(goproxy.AlwaysReject) // rejects all CONNECT requests
func (pcond *ReqProxyConds) HandleConnect(h HttpsHandler) {
pcond.proxy.httpsHandlers = append(pcond.proxy.httpsHandlers,
Expand All @@ -242,6 +253,7 @@ func (pcond *ReqProxyConds) HandleConnect(h HttpsHandler) {

// HandleConnectFunc is equivalent to HandleConnect,
// for example, accepting CONNECT request if they contain a password in header
//
// io.WriteString(h,password)
// passHash := h.Sum(nil)
// proxy.OnRequest().HandleConnectFunc(func(host string, ctx *ProxyCtx) (*ConnectAction, string) {
Expand Down Expand Up @@ -302,6 +314,7 @@ func (pcond *ProxyConds) Do(h RespHandler) {
}

// OnResponse is used when adding a response-filter to the HTTP proxy, usual pattern is
//
// proxy.OnResponse(cond1,cond2).Do(handler) // handler.Handle(resp,ctx) will be used
// // if cond1.HandleResp(resp) && cond2.HandleResp(resp)
func (proxy *ProxyHttpServer) OnResponse(conds ...RespCondition) *ProxyConds {
Expand All @@ -310,13 +323,15 @@ func (proxy *ProxyHttpServer) OnResponse(conds ...RespCondition) *ProxyConds {

// AlwaysMitm is a HttpsHandler that always eavesdrop https connections, for example to
// eavesdrop all https connections to www.google.com, we can use
//
// proxy.OnRequest(goproxy.ReqHostIs("www.google.com")).HandleConnect(goproxy.AlwaysMitm)
var AlwaysMitm FuncHttpsHandler = func(host string, ctx *ProxyCtx) (*ConnectAction, string) {
return MitmConnect, host
}

// AlwaysReject is a HttpsHandler that drops any CONNECT request, for example, this code will disallow
// connections to hosts on any other port than 443
//
// proxy.OnRequest(goproxy.Not(goproxy.ReqHostMatches(regexp.MustCompile(":443$"))).
// HandleConnect(goproxy.AlwaysReject)
var AlwaysReject FuncHttpsHandler = func(host string, ctx *ProxyCtx) (*ConnectAction, string) {
Expand All @@ -328,14 +343,14 @@ var AlwaysReject FuncHttpsHandler = func(host string, ctx *ProxyCtx) (*ConnectAc
// and will replace the body of the original response with the resulting byte array.
func HandleBytes(f func(b []byte, ctx *ProxyCtx) []byte) RespHandler {
return FuncRespHandler(func(resp *http.Response, ctx *ProxyCtx) *http.Response {
b, err := ioutil.ReadAll(resp.Body)
b, err := io.ReadAll(resp.Body)
if err != nil {
ctx.Warnf("Cannot read response %s", err)
return resp
}
resp.Body.Close()

resp.Body = ioutil.NopCloser(bytes.NewBuffer(f(b, ctx)))
resp.Body = io.NopCloser(bytes.NewBuffer(f(b, ctx)))
return resp
})
}
47 changes: 47 additions & 0 deletions dispatcher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package goproxy

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

func TestIsLocalHost(t *testing.T) {
hosts := []string{
"localhost",
"127.0.0.1",
"127.0.0.7",
"::ffff:127.0.0.1",
"::ffff:127.0.0.7",
"::1",
"0:0:0:0:0:0:0:1",
}
ports := []string{
"",
"80",
"443",
}

for _, host := range hosts {
for _, port := range ports {
if port == "" && strings.HasPrefix(host, "::ffff:") {
continue
}

addr := host
if port != "" {
addr = net.JoinHostPort(host, port)
}
t.Run(addr, func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "http://"+addr, http.NoBody)
if err != nil {
t.Fatal(err)
}
if !IsLocalHost(req, nil) {
t.Fatal("expected true")
}
})
}
}
}
4 changes: 2 additions & 2 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Package goproxy provides a customizable HTTP proxy,
supporting hijacking HTTPS connection.
The intent of the proxy, is to be usable with reasonable amount of traffic
yet, customizable and programable.
yet, customizable and programmable.
The proxy itself is simply an `net/http` handler.
Expand Down Expand Up @@ -63,7 +63,7 @@ Finally, we have convenience function to throw a quick response
return goproxy.NewResponse(ctx.Req, goproxy.ContentTypeText, http.StatusForbidden, "Can't see response with X-GoProxy header!")
})
we close the body of the original repsonse, and return a new 403 response with a short message.
we close the body of the original response, and return a new 403 response with a short message.
Example use cases:
Expand Down
14 changes: 7 additions & 7 deletions examples/goproxy-basic/README.md → examples/base/README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
# Simple HTTP Proxy

`goproxy-basic` starts an HTTP proxy on :8080. It only handles explicit CONNECT
requests.
This example contains a base HTTP proxy server that listens on port :8080.
It only handles explicit CONNECT requests.

Start it in one shell:

```sh
goproxy-basic -v
go build
base -v
```

Fetch goproxy homepage in another:

Fetch a website using the proxy:
```sh
http_proxy=http://127.0.0.1:8080 wget -O - \
http://ripper234.com/p/introducing-goproxy-light-http-proxy/
```

The homepage HTML content should be displayed in the console. The proxy should
have logged the request being processed:
The homepage HTML content should be displayed in the console.
The proxy should have logged the request being processed:

```sh
2015/04/09 18:19:17 [001] INFO: Got request /p/introducing-goproxy-light-http-proxy/ ripper234.com GET http://ripper234.com/p/introducing-goproxy-light-http-proxy/
Expand Down
5 changes: 3 additions & 2 deletions examples/goproxy-basic/main.go → examples/base/main.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package main

import (
"github.com/elazarl/goproxy"
"log"
"flag"
"log"
"net/http"

"github.com/elazarl/goproxy"
)

func main() {
Expand Down
17 changes: 17 additions & 0 deletions examples/cascadeproxy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# CascadeProxy

`CascadeProxy` is an example that shows an aggregator server that forwards
the requests to another proxy server (end proxy).

Diagram:
```
client --> middle proxy --> end proxy --> internet
```

This example starts both proxy servers using goproxy, the middle one
listens on port `8081`, and the end one on port `8082`.

The middle proxy must be an HTTP server, since we use goproxy that
expose only it.
The end proxy can be any type of proxy supported by Go, including SOCKS5,
there is a comment in the part where you can put its address.
Loading

0 comments on commit d4745fa

Please sign in to comment.