├── .gitignore ├── ext ├── html │ ├── cp1255.txt │ ├── cp1255.html │ ├── html_test.go │ └── html.go ├── image │ └── image.go └── auth │ ├── basic.go │ └── basic_test.go ├── test_data ├── baby.jpg ├── panda.png └── football.png ├── examples ├── goproxy-jquery-version │ ├── jquery1.html │ ├── jquery2.html │ ├── README.md │ ├── main.go │ ├── jquery_test.go │ ├── jquery_homepage.html │ └── php_man.html ├── goproxy-basic │ ├── main.go │ └── README.md ├── goproxy-customca │ ├── main.go │ └── cert.go ├── goproxy-no-reddit-at-worktime │ ├── README.md │ └── noreddit.go ├── goproxy-sokeepalive │ └── sokeepalive.go ├── goproxy-upside-down-ternet │ └── main.go ├── goproxy-sslstrip │ └── sslstrip.go ├── goproxy-httpdump │ ├── README.md │ └── httpdump.go ├── goproxy-transparent │ ├── proxy.sh │ ├── README.md │ └── transparent.go ├── goproxy-stats │ ├── README.md │ └── main.go ├── goproxy-eavesdropper │ └── main.go └── goproxy-yui-minify │ └── yui.go ├── certs ├── openssl-gen.sh └── openssl.cnf ├── transport ├── util.go ├── roundtripper.go └── transport.go ├── all.bash ├── responses.go ├── LICENSE ├── counterecryptor.go ├── chunked.go ├── ca.pem ├── signer.go ├── signer_test.go ├── regretable ├── regretreader.go └── regretreader_test.go ├── actions.go ├── key.pem ├── counterecryptor_test.go ├── doc.go ├── ctx.go ├── README.md ├── certs.go ├── proxy.go ├── https.go └── dispatcher.go /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | *.swp 3 | -------------------------------------------------------------------------------- /ext/html/cp1255.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heixiaoma/goproxy/master/ext/html/cp1255.txt -------------------------------------------------------------------------------- /test_data/baby.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heixiaoma/goproxy/master/test_data/baby.jpg -------------------------------------------------------------------------------- /test_data/panda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heixiaoma/goproxy/master/test_data/panda.png -------------------------------------------------------------------------------- /ext/html/cp1255.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heixiaoma/goproxy/master/ext/html/cp1255.html -------------------------------------------------------------------------------- /test_data/football.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heixiaoma/goproxy/master/test_data/football.png -------------------------------------------------------------------------------- /examples/goproxy-jquery-version/jquery1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/goproxy-jquery-version/jquery2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /certs/openssl-gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | # generate CA's key 4 | openssl genrsa -aes256 -passout pass:1 -out ca.key.pem 4096 5 | openssl rsa -passin pass:1 -in ca.key.pem -out ca.key.pem.tmp 6 | mv ca.key.pem.tmp ca.key.pem 7 | 8 | openssl req -config openssl.cnf -key ca.key.pem -new -x509 -days 7300 -sha256 -extensions v3_ca -out ca.pem 9 | -------------------------------------------------------------------------------- /transport/util.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type badStringError struct { 9 | what string 10 | str string 11 | } 12 | 13 | func (e *badStringError) Error() string { return fmt.Sprintf("%s %q", e.what, e.str) } 14 | 15 | func hasPort(s string) bool { return strings.LastIndex(s, ":") > strings.LastIndex(s, "]") } 16 | -------------------------------------------------------------------------------- /all.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | go test || exit 4 | for action in $@; do go $action; done 5 | 6 | mkdir -p bin 7 | find regretable examples/* ext/* -maxdepth 0 -type d | while read d; do 8 | (cd $d 9 | go build -o ../../bin/$(basename $d) 10 | find *_test.go -maxdepth 0 2>/dev/null|while read f;do 11 | for action in $@; do go $action; done 12 | go test 13 | break 14 | done) 15 | done 16 | -------------------------------------------------------------------------------- /examples/goproxy-basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "github.com/heixiaoma/goproxy" 6 | "log" 7 | "net/http" 8 | ) 9 | 10 | func main() { 11 | verbose := flag.Bool("v", false, "should every proxy request be logged to stdout") 12 | addr := flag.String("addr", ":8080", "proxy listen address") 13 | flag.Parse() 14 | proxy := goproxy.NewProxyHttpServer() 15 | proxy.Verbose = *verbose 16 | log.Fatal(http.ListenAndServe(*addr, proxy)) 17 | } 18 | -------------------------------------------------------------------------------- /examples/goproxy-customca/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/heixiaoma/goproxy" 9 | ) 10 | 11 | func main() { 12 | verbose := flag.Bool("v", false, "should every proxy request be logged to stdout") 13 | addr := flag.String("addr", ":8080", "proxy listen address") 14 | flag.Parse() 15 | setCA(caCert, caKey) 16 | proxy := goproxy.NewProxyHttpServer() 17 | proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) 18 | proxy.Verbose = *verbose 19 | log.Fatal(http.ListenAndServe(*addr, proxy)) 20 | } 21 | -------------------------------------------------------------------------------- /examples/goproxy-no-reddit-at-worktime/README.md: -------------------------------------------------------------------------------- 1 | # Request Filtering 2 | 3 | `goproxy-no-reddit-at-work` starts an HTTP proxy on :8080. It denies requests 4 | to "www.reddit.com" made between 8am to 5pm inclusive, local time. 5 | 6 | Start it in one shell: 7 | 8 | ```sh 9 | $ goproxy-no-reddit-at-work 10 | ``` 11 | 12 | Fetch reddit in another: 13 | 14 | ```sh 15 | $ http_proxy=http://127.0.0.1:8080 wget -O - http://www.reddit.com 16 | --2015-04-11 16:59:01-- http://www.reddit.com/ 17 | Connecting to 127.0.0.1:8080... connected. 18 | Proxy request sent, awaiting response... 403 Forbidden 19 | 2015-04-11 16:59:01 ERROR 403: Forbidden. 20 | ``` 21 | 22 | -------------------------------------------------------------------------------- /examples/goproxy-sokeepalive/sokeepalive.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "github.com/heixiaoma/goproxy" 6 | "log" 7 | "net" 8 | "net/http" 9 | ) 10 | 11 | func main() { 12 | verbose := flag.Bool("v", false, "should every proxy request be logged to stdout") 13 | addr := flag.String("addr", ":8080", "proxy listen address") 14 | flag.Parse() 15 | proxy := goproxy.NewProxyHttpServer() 16 | proxy.Tr.Dial = func(network, addr string) (c net.Conn, err error) { 17 | c, err = net.Dial(network, addr) 18 | if c, ok := c.(*net.TCPConn); err == nil && ok { 19 | c.SetKeepAlive(true) 20 | } 21 | return 22 | } 23 | proxy.Verbose = *verbose 24 | log.Fatal(http.ListenAndServe(*addr, proxy)) 25 | } 26 | -------------------------------------------------------------------------------- /examples/goproxy-upside-down-ternet/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/heixiaoma/goproxy" 5 | "github.com/heixiaoma/goproxy/ext/image" 6 | "image" 7 | "log" 8 | "net/http" 9 | ) 10 | 11 | func main() { 12 | proxy := goproxy.NewProxyHttpServer() 13 | proxy.OnResponse().Do(goproxy_image.HandleImage(func(img image.Image, ctx *goproxy.ProxyCtx) image.Image { 14 | dx, dy := img.Bounds().Dx(), img.Bounds().Dy() 15 | 16 | nimg := image.NewRGBA(img.Bounds()) 17 | for i := 0; i < dx; i++ { 18 | for j := 0; j <= dy; j++ { 19 | nimg.Set(i, j, img.At(i, dy-j-1)) 20 | } 21 | } 22 | return nimg 23 | })) 24 | proxy.Verbose = true 25 | log.Fatal(http.ListenAndServe(":8080", proxy)) 26 | } 27 | -------------------------------------------------------------------------------- /examples/goproxy-sslstrip/sslstrip.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "github.com/heixiaoma/goproxy" 6 | "log" 7 | "net/http" 8 | ) 9 | 10 | func main() { 11 | verbose := flag.Bool("v", false, "should every proxy request be logged to stdout") 12 | addr := flag.String("addr", ":8080", "proxy listen address") 13 | flag.Parse() 14 | proxy := goproxy.NewProxyHttpServer() 15 | proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm) 16 | proxy.OnRequest().DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { 17 | if req.URL.Scheme == "https" { 18 | req.URL.Scheme = "http" 19 | } 20 | return req, nil 21 | }) 22 | proxy.Verbose = *verbose 23 | log.Fatal(http.ListenAndServe(*addr, proxy)) 24 | } 25 | -------------------------------------------------------------------------------- /examples/goproxy-no-reddit-at-worktime/noreddit.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/heixiaoma/goproxy" 5 | "log" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | func main() { 11 | proxy := goproxy.NewProxyHttpServer() 12 | proxy.OnRequest(goproxy.DstHostIs("www.reddit.com")).DoFunc( 13 | func(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { 14 | h, _, _ := time.Now().Clock() 15 | if h >= 8 && h <= 17 { 16 | return r, goproxy.NewResponse(r, 17 | goproxy.ContentTypeText, http.StatusForbidden, 18 | "Don't waste your time!") 19 | } else { 20 | ctx.Warnf("clock: %d, you can waste your time...", h) 21 | } 22 | return r, nil 23 | }) 24 | log.Fatalln(http.ListenAndServe(":8080", proxy)) 25 | } 26 | -------------------------------------------------------------------------------- /transport/roundtripper.go: -------------------------------------------------------------------------------- 1 | package transport 2 | import "net/http" 3 | type RoundTripper interface { 4 | // RoundTrip executes a single HTTP transaction, returning 5 | // the Response for the request req. RoundTrip should not 6 | // attempt to interpret the response. In particular, 7 | // RoundTrip must return err == nil if it obtained a response, 8 | // regardless of the response's HTTP status code. A non-nil 9 | // err should be reserved for failure to obtain a response. 10 | // Similarly, RoundTrip should not attempt to handle 11 | // higher-level protocol details such as redirects, 12 | // authentication, or cookies. 13 | // 14 | // RoundTrip should not modify the request, except for 15 | // consuming the Body. The request's URL and Header fields 16 | // are guaranteed to be initialized. 17 | RoundTrip(*http.Request) (*http.Response, error) 18 | DetailedRoundTrip(*http.Request) (*RoundTripDetails, *http.Response, error) 19 | } 20 | -------------------------------------------------------------------------------- /examples/goproxy-basic/README.md: -------------------------------------------------------------------------------- 1 | # Simple HTTP Proxy 2 | 3 | `goproxy-basic` starts an HTTP proxy on :8080. It only handles explicit CONNECT 4 | requests. 5 | 6 | Start it in one shell: 7 | 8 | ```sh 9 | goproxy-basic -v 10 | ``` 11 | 12 | Fetch goproxy homepage in another: 13 | 14 | ```sh 15 | http_proxy=http://127.0.0.1:8080 wget -O - \ 16 | http://ripper234.com/p/introducing-goproxy-light-http-proxy/ 17 | ``` 18 | 19 | The homepage HTML content should be displayed in the console. The proxy should 20 | have logged the request being processed: 21 | 22 | ```sh 23 | 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/ 24 | 2015/04/09 18:19:17 [001] INFO: Sending request GET http://ripper234.com/p/introducing-goproxy-light-http-proxy/ 25 | 2015/04/09 18:19:18 [001] INFO: Received response 200 OK 26 | 2015/04/09 18:19:18 [001] INFO: Copying response to client 200 OK [200] 27 | 2015/04/09 18:19:18 [001] INFO: Copied 44333 bytes to client error= 28 | ``` 29 | 30 | -------------------------------------------------------------------------------- /examples/goproxy-httpdump/README.md: -------------------------------------------------------------------------------- 1 | # Trace HTTP Requests and Responses 2 | 3 | `goproxy-httpdump` starts an HTTP proxy on :8080. It handles explicit CONNECT 4 | requests and traces them in a "db" directory created in the proxy working 5 | directory. Each request type and headers are logged in a "log" file, while 6 | their bodies are dumped in files prefixed with the request session identifier. 7 | 8 | Additionally, the example demonstrates how to: 9 | - Log information asynchronously (see HttpLogger) 10 | - Allow the proxy to be stopped manually while ensuring all pending requests 11 | have been processed (in this case, logged). 12 | 13 | Start it in one shell: 14 | 15 | ```sh 16 | goproxy-httpdump 17 | ``` 18 | 19 | Fetch goproxy homepage in another: 20 | 21 | ```sh 22 | http_proxy=http://127.0.0.1:8080 wget -O - \ 23 | http://ripper234.com/p/introducing-goproxy-light-http-proxy/ 24 | ``` 25 | 26 | A "db" directory should have appeared where you started the proxy, containing 27 | two files: 28 | - log: the request/response traces 29 | - 1\_resp: the first response body 30 | 31 | -------------------------------------------------------------------------------- /examples/goproxy-jquery-version/README.md: -------------------------------------------------------------------------------- 1 | # Content Analysis 2 | 3 | `goproxy-jquery-version` starts an HTTP proxy on :8080. It checks HTML 4 | responses, looks for scripts referencing jQuery library and emits warnings if 5 | different versions of the library are being used for a given host. 6 | 7 | Start it in one shell: 8 | 9 | ```sh 10 | goproxy-jquery-version 11 | ``` 12 | 13 | Fetch goproxy homepage in another: 14 | 15 | ```sh 16 | http_proxy=http://127.0.0.1:8080 wget -O - \ 17 | http://ripper234.com/p/introducing-goproxy-light-http-proxy/ 18 | ``` 19 | 20 | Goproxy homepage uses jQuery and a mix of plugins. First the proxy reports the 21 | first use of jQuery it detects for the domain. Then, because the regular 22 | expression matching the jQuery sources is imprecise, it reports a mismatch with 23 | a plugin reference: 24 | 25 | ```sh 26 | 2015/04/11 11:23:02 [001] WARN: ripper234.com uses //ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js 27 | 2015/04/11 11:23:02 [001] WARN: In http://ripper234.com/p/introducing-goproxy-light-http-proxy/, \ 28 | Contradicting jqueries //ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js \ 29 | http://ripper234.wpengine.netdna-cdn.com/wp-content/plugins/wp-ajax-edit-comments/js/jquery.colorbox.min.js?ver=5.0.36 30 | ``` 31 | 32 | -------------------------------------------------------------------------------- /certs/openssl.cnf: -------------------------------------------------------------------------------- 1 | [ ca ] 2 | default_ca = CA_default 3 | [ CA_default ] 4 | default_md = sha256 5 | [ v3_ca ] 6 | subjectKeyIdentifier=hash 7 | authorityKeyIdentifier=keyid:always,issuer 8 | basicConstraints = critical,CA:true 9 | [ req ] 10 | distinguished_name = req_distinguished_name 11 | [ req_distinguished_name ] 12 | countryName = Country Name (2 letter code) 13 | countryName_default = IL 14 | countryName_min = 2 15 | countryName_max = 2 16 | 17 | stateOrProvinceName = State or Province Name (full name) 18 | stateOrProvinceName_default = Center 19 | 20 | localityName = Locality Name (eg, city) 21 | localityName_default = Lod 22 | 23 | 0.organizationName = Organization Name (eg, company) 24 | 0.organizationName_default = GoProxy 25 | 26 | # we can do this but it is not needed normally :-) 27 | #1.organizationName = Second Organization Name (eg, company) 28 | #1.organizationName_default = World Wide Web Pty Ltd 29 | 30 | organizationalUnitName = Organizational Unit Name (eg, section) 31 | organizationalUnitName_default = GoProxy 32 | 33 | commonName = Common Name (e.g. server FQDN or YOUR name) 34 | commonName_default = goproxy.github.io 35 | commonName_max = 64 36 | 37 | emailAddress = Email Address 38 | emailAddress_default = elazarl@gmail.com 39 | emailAddress_max = 64 40 | -------------------------------------------------------------------------------- /examples/goproxy-transparent/proxy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # goproxy IP 3 | GOPROXY_SERVER="10.10.10.1" 4 | # goproxy port 5 | GOPROXY_PORT="3129" 6 | GOPROXY_PORT_TLS="3128" 7 | # DO NOT MODIFY BELOW 8 | # Load IPTABLES modules for NAT and IP conntrack support 9 | modprobe ip_conntrack 10 | modprobe ip_conntrack_ftp 11 | echo 1 > /proc/sys/net/ipv4/ip_forward 12 | echo 2 > /proc/sys/net/ipv4/conf/all/rp_filter 13 | 14 | # Clean old firewall 15 | iptables -t nat -F 16 | iptables -t nat -X 17 | iptables -t mangle -F 18 | iptables -t mangle -X 19 | 20 | # Write new rules 21 | iptables -t nat -A PREROUTING -s $GOPROXY_SERVER -p tcp --dport $GOPROXY_PORT -j ACCEPT 22 | iptables -t nat -A PREROUTING -s $GOPROXY_SERVER -p tcp --dport $GOPROXY_PORT_TLS -j ACCEPT 23 | iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination $GOPROXY_SERVER:$GOPROXY_PORT 24 | iptables -t nat -A PREROUTING -p tcp --dport 443 -j DNAT --to-destination $GOPROXY_SERVER:$GOPROXY_PORT_TLS 25 | # The following line supports using goproxy as an explicit proxy in addition 26 | iptables -t nat -A PREROUTING -p tcp --dport 8080 -j DNAT --to-destination $GOPROXY_SERVER:$GOPROXY_PORT 27 | iptables -t nat -A POSTROUTING -j MASQUERADE 28 | iptables -t mangle -A PREROUTING -p tcp --dport $GOPROXY_PORT -j DROP 29 | iptables -t mangle -A PREROUTING -p tcp --dport $GOPROXY_PORT_TLS -j DROP 30 | -------------------------------------------------------------------------------- /responses.go: -------------------------------------------------------------------------------- 1 | package goproxy 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net/http" 7 | ) 8 | 9 | // Will generate a valid http response to the given request the response will have 10 | // the given contentType, and http status. 11 | // Typical usage, refuse to process requests to local addresses: 12 | // 13 | // proxy.OnRequest(IsLocalHost()).DoFunc(func(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request,*http.Response) { 14 | // return nil,NewResponse(r,goproxy.ContentTypeHtml,http.StatusUnauthorized, 15 | // `Can't use proxy for local addresses`) 16 | // }) 17 | func NewResponse(r *http.Request, contentType string, status int, body string) *http.Response { 18 | resp := &http.Response{} 19 | resp.Request = r 20 | resp.TransferEncoding = r.TransferEncoding 21 | resp.Header = make(http.Header) 22 | resp.Header.Add("Content-Type", contentType) 23 | resp.StatusCode = status 24 | buf := bytes.NewBufferString(body) 25 | resp.ContentLength = int64(buf.Len()) 26 | resp.Body = ioutil.NopCloser(buf) 27 | return resp 28 | } 29 | 30 | const ( 31 | ContentTypeText = "text/plain" 32 | ContentTypeHtml = "text/html" 33 | ) 34 | 35 | // Alias for NewResponse(r,ContentTypeText,http.StatusAccepted,text) 36 | func TextResponse(r *http.Request, text string) *http.Response { 37 | return NewResponse(r, ContentTypeText, http.StatusAccepted, text) 38 | } 39 | -------------------------------------------------------------------------------- /examples/goproxy-stats/README.md: -------------------------------------------------------------------------------- 1 | # Gather Browsing Statistics 2 | 3 | `goproxy-stats` starts an HTTP proxy on :8080, counts the bytes received for 4 | web resources and prints the cumulative sum per URL every 20 seconds. 5 | 6 | Start it in one shell: 7 | 8 | ```sh 9 | goproxy-stats 10 | ``` 11 | 12 | Fetch goproxy homepage in another: 13 | 14 | ```sh 15 | mkdir tmp 16 | cd tmp 17 | http_proxy=http://127.0.0.1:8080 wget -r -l 1 -H \ 18 | http://ripper234.com/p/introducing-goproxy-light-http-proxy/ 19 | ``` 20 | 21 | Stop it after a moment. `goproxy-stats` should eventually print: 22 | ```sh 23 | listening on :8080 24 | statistics 25 | http://www.telerik.com/fiddler -> 84335 26 | http://msmvps.com/robots.txt -> 157 27 | http://eli.thegreenplace.net/robots.txt -> 294 28 | http://www.phdcomics.com/robots.txt -> 211 29 | http://resharper.blogspot.com/robots.txt -> 221 30 | http://idanz.blogli.co.il/robots.txt -> 271 31 | http://ripper234.com/p/introducing-goproxy-light-http-proxy/ -> 44407 32 | http://live.gnome.org/robots.txt -> 298 33 | http://ponetium.wordpress.com/robots.txt -> 178 34 | http://pilaheleg.blogli.co.il/robots.txt -> 321 35 | http://pilaheleg.wordpress.com/robots.txt -> 178 36 | http://blogli.co.il/ -> 9165 37 | http://nimrod-code.org/robots.txt -> 289 38 | http://www.joelonsoftware.com/robots.txt -> 1245 39 | http://top-performance.blogspot.com/robots.txt -> 227 40 | http://ooc-lang.org/robots.txt -> 345 41 | http://blogs.jetbrains.com/robots.txt -> 293 42 | ``` 43 | 44 | -------------------------------------------------------------------------------- /examples/goproxy-stats/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/heixiaoma/goproxy" 6 | "github.com/heixiaoma/goproxy/ext/html" 7 | "io" 8 | "log" 9 | . "net/http" 10 | "time" 11 | ) 12 | 13 | type Count struct { 14 | Id string 15 | Count int64 16 | } 17 | type CountReadCloser struct { 18 | Id string 19 | R io.ReadCloser 20 | ch chan<- Count 21 | nr int64 22 | } 23 | 24 | func (c *CountReadCloser) Read(b []byte) (n int, err error) { 25 | n, err = c.R.Read(b) 26 | c.nr += int64(n) 27 | return 28 | } 29 | func (c CountReadCloser) Close() error { 30 | c.ch <- Count{c.Id, c.nr} 31 | return c.R.Close() 32 | } 33 | 34 | func main() { 35 | proxy := goproxy.NewProxyHttpServer() 36 | timer := make(chan bool) 37 | ch := make(chan Count, 10) 38 | go func() { 39 | for { 40 | time.Sleep(20 * time.Second) 41 | timer <- true 42 | } 43 | }() 44 | go func() { 45 | m := make(map[string]int64) 46 | for { 47 | select { 48 | case c := <-ch: 49 | m[c.Id] = m[c.Id] + c.Count 50 | case <-timer: 51 | fmt.Printf("statistics\n") 52 | for k, v := range m { 53 | fmt.Printf("%s -> %d\n", k, v) 54 | } 55 | } 56 | } 57 | }() 58 | 59 | // IsWebRelatedText filters on html/javascript/css resources 60 | proxy.OnResponse(goproxy_html.IsWebRelatedText).DoFunc(func(resp *Response, ctx *goproxy.ProxyCtx) *Response { 61 | resp.Body = &CountReadCloser{ctx.Req.URL.String(), resp.Body, ch, 0} 62 | return resp 63 | }) 64 | fmt.Printf("listening on :8080\n") 65 | log.Fatal(ListenAndServe(":8080", proxy)) 66 | } 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Elazar Leibovich. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Elazar Leibovich. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /counterecryptor.go: -------------------------------------------------------------------------------- 1 | package goproxy 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/rsa" 7 | "crypto/sha256" 8 | "crypto/x509" 9 | "errors" 10 | ) 11 | 12 | type CounterEncryptorRand struct { 13 | cipher cipher.Block 14 | counter []byte 15 | rand []byte 16 | ix int 17 | } 18 | 19 | func NewCounterEncryptorRandFromKey(key interface{}, seed []byte) (r CounterEncryptorRand, err error) { 20 | var keyBytes []byte 21 | switch key := key.(type) { 22 | case *rsa.PrivateKey: 23 | keyBytes = x509.MarshalPKCS1PrivateKey(key) 24 | default: 25 | err = errors.New("only RSA keys supported") 26 | return 27 | } 28 | h := sha256.New() 29 | if r.cipher, err = aes.NewCipher(h.Sum(keyBytes)[:aes.BlockSize]); err != nil { 30 | return 31 | } 32 | r.counter = make([]byte, r.cipher.BlockSize()) 33 | if seed != nil { 34 | copy(r.counter, h.Sum(seed)[:r.cipher.BlockSize()]) 35 | } 36 | r.rand = make([]byte, r.cipher.BlockSize()) 37 | r.ix = len(r.rand) 38 | return 39 | } 40 | 41 | func (c *CounterEncryptorRand) Seed(b []byte) { 42 | if len(b) != len(c.counter) { 43 | panic("SetCounter: wrong counter size") 44 | } 45 | copy(c.counter, b) 46 | } 47 | 48 | func (c *CounterEncryptorRand) refill() { 49 | c.cipher.Encrypt(c.rand, c.counter) 50 | for i := 0; i < len(c.counter); i++ { 51 | if c.counter[i]++; c.counter[i] != 0 { 52 | break 53 | } 54 | } 55 | c.ix = 0 56 | } 57 | 58 | func (c *CounterEncryptorRand) Read(b []byte) (n int, err error) { 59 | if c.ix == len(c.rand) { 60 | c.refill() 61 | } 62 | if n = len(c.rand) - c.ix; n > len(b) { 63 | n = len(b) 64 | } 65 | copy(b, c.rand[c.ix:c.ix+n]) 66 | c.ix += n 67 | return 68 | } 69 | -------------------------------------------------------------------------------- /ext/html/html_test.go: -------------------------------------------------------------------------------- 1 | package goproxy_html_test 2 | 3 | import ( 4 | "github.com/heixiaoma/goproxy" 5 | "github.com/heixiaoma/goproxy/ext/html" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httptest" 9 | "net/url" 10 | "testing" 11 | ) 12 | 13 | type ConstantServer int 14 | 15 | func (s ConstantServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { 16 | w.Header().Set("Content-Type", "text/plain; charset=iso-8859-8") 17 | //w.Header().Set("Content-Type","text/plain; charset=cp-1255") 18 | w.Write([]byte{0xe3, 0xf3}) 19 | } 20 | 21 | func TestCharset(t *testing.T) { 22 | s := httptest.NewServer(ConstantServer(1)) 23 | defer s.Close() 24 | 25 | ch := make(chan string, 2) 26 | proxy := goproxy.NewProxyHttpServer() 27 | proxy.OnResponse().Do(goproxy_html.HandleString( 28 | func(s string, ctx *goproxy.ProxyCtx) string { 29 | ch <- s 30 | return s 31 | })) 32 | proxyServer := httptest.NewServer(proxy) 33 | defer proxyServer.Close() 34 | 35 | proxyUrl, _ := url.Parse(proxyServer.URL) 36 | client := &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyUrl)}} 37 | 38 | resp, err := client.Get(s.URL + "/cp1255.txt") 39 | if err != nil { 40 | t.Fatal("GET:", err) 41 | } 42 | b, err := ioutil.ReadAll(resp.Body) 43 | if err != nil { 44 | t.Fatal("readAll:", err) 45 | } 46 | resp.Body.Close() 47 | 48 | inHandleString := "" 49 | select { 50 | case inHandleString = <-ch: 51 | default: 52 | } 53 | 54 | if len(b) != 2 || b[0] != 0xe3 || b[1] != 0xf3 { 55 | t.Error("Did not translate back to 0xe3,0xf3, instead", b) 56 | } 57 | if inHandleString != "דף" { 58 | t.Error("HandleString did not convert DALET & PEH SOFIT (דף) from ISO-8859-8 to utf-8, got", []byte(inHandleString)) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/goproxy-transparent/README.md: -------------------------------------------------------------------------------- 1 | # Transparent Proxy 2 | 3 | This transparent example in goproxy is meant to show how to transparenty proxy and hijack all http and https connections while doing a man-in-the-middle to the TLS session. It requires that goproxy sees all the packets traversing out to the internet. Linux iptables rules deal with changing the source/destination IPs to act transparently, but you do need to setup your network configuration so that goproxy is a mandatory stop on the outgoing route. Primarily you can do this by placing the proxy inline. goproxy does not have any WCCP support itself; patches welcome. 4 | 5 | ## Why not explicit? 6 | 7 | Transparent proxies are more difficult to maintain and setup from a server side, but they require no configuration on the client(s) which could be in unmanaged systems or systems that don't support a proxy configuration. See the [eavesdropper example](https://github.com/elazarl/goproxy/blob/master/examples/goproxy-eavesdropper/main.go) if you want to see an explicit proxy example. 8 | 9 | ## Potential Issues 10 | 11 | Support for very old clients using HTTPS will fail. Clients need to send the SNI value in the TLS ClientHello which most modern clients do these days, but old clients will break. 12 | 13 | If you're routing table allows for it, an explicit http request to goproxy will cause it to fail in an endless loop since it will try to request resources from itself repeatedly. This could be solved in the goproxy code by looking up the hostnames, but it adds a delay that is much easier/faster to handle on the routing side. 14 | 15 | ## Routing Rules 16 | 17 | Example routing rules are included in [proxy.sh](https://github.com/elazarl/goproxy/blob/master/examples/goproxy-transparent/proxy.sh) but are best when setup using your distribution's configuration. 18 | -------------------------------------------------------------------------------- /examples/goproxy-eavesdropper/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "log" 7 | "net" 8 | "net/http" 9 | "regexp" 10 | 11 | "github.com/heixiaoma/goproxy" 12 | ) 13 | 14 | func orPanic(err error) { 15 | if err != nil { 16 | panic(err) 17 | } 18 | } 19 | 20 | func main() { 21 | proxy := goproxy.NewProxyHttpServer() 22 | proxy.OnRequest(goproxy.ReqHostMatches(regexp.MustCompile("^.*baidu.com$"))). 23 | HandleConnect(goproxy.AlwaysReject) 24 | proxy.OnRequest(goproxy.ReqHostMatches(regexp.MustCompile("^.*$"))). 25 | HandleConnect(goproxy.AlwaysMitm) 26 | // enable curl -p for all hosts on port 80 27 | proxy.OnRequest(goproxy.ReqHostMatches(regexp.MustCompile("^.*:80$"))). 28 | HijackConnect(func(req *http.Request, client net.Conn, ctx *goproxy.ProxyCtx) { 29 | defer func() { 30 | if e := recover(); e != nil { 31 | ctx.Logf("error connecting to remote: %v", e) 32 | client.Write([]byte("HTTP/1.1 500 Cannot reach destination\r\n\r\n")) 33 | } 34 | client.Close() 35 | }() 36 | clientBuf := bufio.NewReadWriter(bufio.NewReader(client), bufio.NewWriter(client)) 37 | remote, err := net.Dial("tcp", req.URL.Host) 38 | orPanic(err) 39 | remoteBuf := bufio.NewReadWriter(bufio.NewReader(remote), bufio.NewWriter(remote)) 40 | for { 41 | req, err := http.ReadRequest(clientBuf.Reader) 42 | orPanic(err) 43 | orPanic(req.Write(remoteBuf)) 44 | orPanic(remoteBuf.Flush()) 45 | resp, err := http.ReadResponse(remoteBuf.Reader, req) 46 | orPanic(err) 47 | orPanic(resp.Write(clientBuf.Writer)) 48 | orPanic(clientBuf.Flush()) 49 | } 50 | }) 51 | verbose := flag.Bool("v", false, "should every proxy request be logged to stdout") 52 | addr := flag.String("addr", ":8080", "proxy listen address") 53 | flag.Parse() 54 | proxy.Verbose = *verbose 55 | log.Fatal(http.ListenAndServe(*addr, proxy)) 56 | } 57 | -------------------------------------------------------------------------------- /chunked.go: -------------------------------------------------------------------------------- 1 | // Taken from $GOROOT/src/pkg/net/http/chunked 2 | // needed to write https responses to client. 3 | package goproxy 4 | 5 | import ( 6 | "io" 7 | "strconv" 8 | ) 9 | 10 | // newChunkedWriter returns a new chunkedWriter that translates writes into HTTP 11 | // "chunked" format before writing them to w. Closing the returned chunkedWriter 12 | // sends the final 0-length chunk that marks the end of the stream. 13 | // 14 | // newChunkedWriter is not needed by normal applications. The http 15 | // package adds chunking automatically if handlers don't set a 16 | // Content-Length header. Using newChunkedWriter inside a handler 17 | // would result in double chunking or chunking with a Content-Length 18 | // length, both of which are wrong. 19 | func newChunkedWriter(w io.Writer) io.WriteCloser { 20 | return &chunkedWriter{w} 21 | } 22 | 23 | // Writing to chunkedWriter translates to writing in HTTP chunked Transfer 24 | // Encoding wire format to the underlying Wire chunkedWriter. 25 | type chunkedWriter struct { 26 | Wire io.Writer 27 | } 28 | 29 | // Write the contents of data as one chunk to Wire. 30 | // NOTE: Note that the corresponding chunk-writing procedure in Conn.Write has 31 | // a bug since it does not check for success of io.WriteString 32 | func (cw *chunkedWriter) Write(data []byte) (n int, err error) { 33 | 34 | // Don't send 0-length data. It looks like EOF for chunked encoding. 35 | if len(data) == 0 { 36 | return 0, nil 37 | } 38 | 39 | head := strconv.FormatInt(int64(len(data)), 16) + "\r\n" 40 | 41 | if _, err = io.WriteString(cw.Wire, head); err != nil { 42 | return 0, err 43 | } 44 | if n, err = cw.Wire.Write(data); err != nil { 45 | return 46 | } 47 | if n != len(data) { 48 | err = io.ErrShortWrite 49 | return 50 | } 51 | _, err = io.WriteString(cw.Wire, "\r\n") 52 | 53 | return 54 | } 55 | 56 | func (cw *chunkedWriter) Close() error { 57 | _, err := io.WriteString(cw.Wire, "0\r\n") 58 | return err 59 | } 60 | -------------------------------------------------------------------------------- /examples/goproxy-jquery-version/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/heixiaoma/goproxy" 5 | "github.com/heixiaoma/goproxy/ext/html" 6 | "log" 7 | "net/http" 8 | "regexp" 9 | ) 10 | 11 | var ( 12 | // who said we can't parse HTML with regexp? 13 | scriptMatcher = regexp.MustCompile(`(?i:]*\ssrc=["']([^"']*)["'])`) 15 | ) 16 | 17 | // findScripts returns all sources of HTML script tags found in input text. 18 | func findScriptSrc(html string) []string { 19 | srcs := make([]string, 0) 20 | matches := scriptMatcher.FindAllStringIndex(html, -1) 21 | for _, match := range matches { 22 | // -1 to capture the whitespace at the end of the script tag 23 | srcMatch := srcAttrMatcher.FindStringSubmatch(html[match[1]-1:]) 24 | if srcMatch != nil { 25 | srcs = append(srcs, srcMatch[1]) 26 | } 27 | } 28 | return srcs 29 | } 30 | 31 | // NewJQueryVersionProxy creates a proxy checking responses HTML content, looks 32 | // for scripts referencing jQuery library and emits warnings if different 33 | // versions of the library are being used for a given host. 34 | func NewJqueryVersionProxy() *goproxy.ProxyHttpServer { 35 | proxy := goproxy.NewProxyHttpServer() 36 | m := make(map[string]string) 37 | jqueryMatcher := regexp.MustCompile(`(?i:jquery\.)`) 38 | proxy.OnResponse(goproxy_html.IsHtml).Do(goproxy_html.HandleString( 39 | func(s string, ctx *goproxy.ProxyCtx) string { 40 | for _, src := range findScriptSrc(s) { 41 | if !jqueryMatcher.MatchString(src) { 42 | continue 43 | } 44 | prev, ok := m[ctx.Req.Host] 45 | if ok { 46 | if prev != src { 47 | ctx.Warnf("In %v, Contradicting jqueries %v %v", 48 | ctx.Req.URL, prev, src) 49 | break 50 | } 51 | } else { 52 | ctx.Warnf("%s uses jquery %s", ctx.Req.Host, src) 53 | m[ctx.Req.Host] = src 54 | } 55 | } 56 | return s 57 | })) 58 | return proxy 59 | } 60 | 61 | func main() { 62 | proxy := NewJqueryVersionProxy() 63 | log.Fatal(http.ListenAndServe(":8080", proxy)) 64 | } 65 | -------------------------------------------------------------------------------- /ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIF9DCCA9ygAwIBAgIJAODqYUwoVjJkMA0GCSqGSIb3DQEBCwUAMIGOMQswCQYD 3 | VQQGEwJJTDEPMA0GA1UECAwGQ2VudGVyMQwwCgYDVQQHDANMb2QxEDAOBgNVBAoM 4 | B0dvUHJveHkxEDAOBgNVBAsMB0dvUHJveHkxGjAYBgNVBAMMEWdvcHJveHkuZ2l0 5 | aHViLmlvMSAwHgYJKoZIhvcNAQkBFhFlbGF6YXJsQGdtYWlsLmNvbTAeFw0xNzA0 6 | MDUyMDAwMTBaFw0zNzAzMzEyMDAwMTBaMIGOMQswCQYDVQQGEwJJTDEPMA0GA1UE 7 | CAwGQ2VudGVyMQwwCgYDVQQHDANMb2QxEDAOBgNVBAoMB0dvUHJveHkxEDAOBgNV 8 | BAsMB0dvUHJveHkxGjAYBgNVBAMMEWdvcHJveHkuZ2l0aHViLmlvMSAwHgYJKoZI 9 | hvcNAQkBFhFlbGF6YXJsQGdtYWlsLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIP 10 | ADCCAgoCggIBAJ4Qy+H6hhoY1s0QRcvIhxrjSHaO/RbaFj3rwqcnpOgFq07gRdI9 11 | 3c0TFKQJHpgv6feLRhEvX/YllFYu4J35lM9ZcYY4qlKFuStcX8Jm8fqpgtmAMBzP 12 | sqtqDi8M9RQGKENzU9IFOnCV7SAeh45scMuI3wz8wrjBcH7zquHkvqUSYZz035t9 13 | V6WTrHyTEvT4w+lFOVN2bA/6DAIxrjBiF6DhoJqnha0SZtDfv77XpwGG3EhA/qoh 14 | hiYrDruYK7zJdESQL44LwzMPupVigqalfv+YHfQjbhT951IVurW2NJgRyBE62dLr 15 | lHYdtT9tCTCrd+KJNMJ+jp9hAjdIu1Br/kifU4F4+4ZLMR9Ueji0GkkPKsYdyMnq 16 | j0p0PogyvP1l4qmboPImMYtaoFuYmMYlebgC9LN10bL91K4+jLt0I1YntEzrqgJo 17 | WsJztYDw543NzSy5W+/cq4XRYgtq1b0RWwuUiswezmMoeyHZ8BQJe2xMjAOllASD 18 | fqa8OK3WABHJpy4zUrnUBiMuPITzD/FuDx4C5IwwlC68gHAZblNqpBZCX0nFCtKj 19 | YOcI2So5HbQ2OC8QF+zGVuduHUSok4hSy2BBfZ1pfvziqBeetWJwFvapGB44nIHh 20 | WKNKvqOxLNIy7e+TGRiWOomrAWM18VSR9LZbBxpJK7PLSzWqYJYTRCZHAgMBAAGj 21 | UzBRMB0GA1UdDgQWBBR4uDD9Y6x7iUoHO+32ioOcw1ICZTAfBgNVHSMEGDAWgBR4 22 | uDD9Y6x7iUoHO+32ioOcw1ICZTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEB 23 | CwUAA4ICAQAaCEupzGGqcdh+L7BzhX7zyd7yzAKUoLxFrxaZY34Xyj3lcx1XoK6F 24 | AqsH2JM25GixgadzhNt92JP7vzoWeHZtLfstrPS638Y1zZi6toy4E49viYjFk5J0 25 | C6ZcFC04VYWWx6z0HwJuAS08tZ37JuFXpJGfXJOjZCQyxse0Lg0tuKLMeXDCk2Y3 26 | Ba0noeuNyHRoWXXPyiUoeApkVCU5gIsyiJSWOjhJ5hpJG06rQNfNYexgKrrraEin 27 | o0jmEMtJMx5TtD83hSnLCnFGBBq5lkE7jgXME1KsbIE3lJZzRX1mQwUK8CJDYxye 28 | i6M/dzSvy0SsPvz8fTAlprXRtWWtJQmxgWENp3Dv+0Pmux/l+ilk7KA4sMXGhsfr 29 | bvTOeWl1/uoFTPYiWR/ww7QEPLq23yDFY04Q7Un0qjIk8ExvaY8lCkXMgc8i7sGY 30 | VfvOYb0zm67EfAQl3TW8Ky5fl5CcxpVCD360Bzi6hwjYixa3qEeBggOixFQBFWft 31 | 8wrkKTHpOQXjn4sDPtet8imm9UYEtzWrFX6T9MFYkBR0/yye0FIh9+YPiTA6WB86 32 | NCNwK5Yl6HuvF97CIH5CdgO+5C7KifUtqTOL8pQKbNwy0S3sNYvB+njGvRpR7pKV 33 | BUnFpB/Atptqr4CUlTXrc5IPLAqAfmwk5IKcwy3EXUbruf9Dwz69YA== 34 | -----END CERTIFICATE----- 35 | -------------------------------------------------------------------------------- /signer.go: -------------------------------------------------------------------------------- 1 | package goproxy 2 | 3 | import ( 4 | "crypto/rsa" 5 | "crypto/sha1" 6 | "crypto/tls" 7 | "crypto/x509" 8 | "crypto/x509/pkix" 9 | "math/big" 10 | "net" 11 | "runtime" 12 | "sort" 13 | "time" 14 | ) 15 | 16 | func hashSorted(lst []string) []byte { 17 | c := make([]string, len(lst)) 18 | copy(c, lst) 19 | sort.Strings(c) 20 | h := sha1.New() 21 | for _, s := range c { 22 | h.Write([]byte(s + ",")) 23 | } 24 | return h.Sum(nil) 25 | } 26 | 27 | func hashSortedBigInt(lst []string) *big.Int { 28 | rv := new(big.Int) 29 | rv.SetBytes(hashSorted(lst)) 30 | return rv 31 | } 32 | 33 | var goproxySignerVersion = ":goroxy1" 34 | 35 | func signHost(ca tls.Certificate, hosts []string) (cert tls.Certificate, err error) { 36 | var x509ca *x509.Certificate 37 | 38 | // Use the provided ca and not the global GoproxyCa for certificate generation. 39 | if x509ca, err = x509.ParseCertificate(ca.Certificate[0]); err != nil { 40 | return 41 | } 42 | start := time.Unix(0, 0) 43 | end, err := time.Parse("2006-01-02", "2049-12-31") 44 | if err != nil { 45 | panic(err) 46 | } 47 | hash := hashSorted(append(hosts, goproxySignerVersion, ":"+runtime.Version())) 48 | serial := new(big.Int) 49 | serial.SetBytes(hash) 50 | template := x509.Certificate{ 51 | // TODO(elazar): instead of this ugly hack, just encode the certificate and hash the binary form. 52 | SerialNumber: serial, 53 | Issuer: x509ca.Subject, 54 | Subject: pkix.Name{ 55 | Organization: []string{"GoProxy untrusted MITM proxy Inc"}, 56 | }, 57 | NotBefore: start, 58 | NotAfter: end, 59 | 60 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 61 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 62 | BasicConstraintsValid: true, 63 | } 64 | for _, h := range hosts { 65 | if ip := net.ParseIP(h); ip != nil { 66 | template.IPAddresses = append(template.IPAddresses, ip) 67 | } else { 68 | template.DNSNames = append(template.DNSNames, h) 69 | } 70 | } 71 | var csprng CounterEncryptorRand 72 | if csprng, err = NewCounterEncryptorRandFromKey(ca.PrivateKey, hash); err != nil { 73 | return 74 | } 75 | var certpriv *rsa.PrivateKey 76 | if certpriv, err = rsa.GenerateKey(&csprng, 1024); err != nil { 77 | return 78 | } 79 | var derBytes []byte 80 | if derBytes, err = x509.CreateCertificate(&csprng, &template, x509ca, &certpriv.PublicKey, ca.PrivateKey); err != nil { 81 | return 82 | } 83 | return tls.Certificate{ 84 | Certificate: [][]byte{derBytes, ca.Certificate[0]}, 85 | PrivateKey: certpriv, 86 | }, nil 87 | } 88 | -------------------------------------------------------------------------------- /ext/image/image.go: -------------------------------------------------------------------------------- 1 | package goproxy_image 2 | 3 | import ( 4 | "bytes" 5 | . "github.com/heixiaoma/goproxy" 6 | "github.com/heixiaoma/goproxy/regretable" 7 | "image" 8 | _ "image/gif" 9 | "image/jpeg" 10 | "image/png" 11 | "io/ioutil" 12 | "net/http" 13 | ) 14 | 15 | var RespIsImage = ContentTypeIs("image/gif", 16 | "image/jpeg", 17 | "image/pjpeg", 18 | "application/octet-stream", 19 | "image/png") 20 | 21 | // "image/tiff" tiff support is in external package, and rarely used, so we omitted it 22 | 23 | func HandleImage(f func(img image.Image, ctx *ProxyCtx) image.Image) RespHandler { 24 | return FuncRespHandler(func(resp *http.Response, ctx *ProxyCtx) *http.Response { 25 | if !RespIsImage.HandleResp(resp, ctx) { 26 | return resp 27 | } 28 | if resp.StatusCode != 200 { 29 | // we might get 304 - not modified response without data 30 | return resp 31 | } 32 | contentType := resp.Header.Get("Content-Type") 33 | 34 | const kb = 1024 35 | regret := regretable.NewRegretableReaderCloserSize(resp.Body, 16*kb) 36 | resp.Body = regret 37 | img, imgType, err := image.Decode(resp.Body) 38 | if err != nil { 39 | regret.Regret() 40 | ctx.Warnf("%s: %s", ctx.Req.Method+" "+ctx.Req.URL.String()+" Image from "+ctx.Req.RequestURI+"content type"+ 41 | contentType+"cannot be decoded returning original image", err) 42 | return resp 43 | } 44 | result := f(img, ctx) 45 | buf := bytes.NewBuffer([]byte{}) 46 | switch contentType { 47 | // No gif image encoder in go - convert to png 48 | case "image/gif", "image/png": 49 | if err := png.Encode(buf, result); err != nil { 50 | ctx.Warnf("Cannot encode image, returning orig %v %v", ctx.Req.URL.String(), err) 51 | return resp 52 | } 53 | resp.Header.Set("Content-Type", "image/png") 54 | case "image/jpeg", "image/pjpeg": 55 | if err := jpeg.Encode(buf, result, nil); err != nil { 56 | ctx.Warnf("Cannot encode image, returning orig %v %v", ctx.Req.URL.String(), err) 57 | return resp 58 | } 59 | case "application/octet-stream": 60 | switch imgType { 61 | case "jpeg": 62 | if err := jpeg.Encode(buf, result, nil); err != nil { 63 | ctx.Warnf("Cannot encode image as jpeg, returning orig %v %v", ctx.Req.URL.String(), err) 64 | return resp 65 | } 66 | case "png", "gif": 67 | if err := png.Encode(buf, result); err != nil { 68 | ctx.Warnf("Cannot encode image as png, returning orig %v %v", ctx.Req.URL.String(), err) 69 | return resp 70 | } 71 | } 72 | default: 73 | panic("unhandlable type" + contentType) 74 | } 75 | resp.Body = ioutil.NopCloser(buf) 76 | return resp 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /signer_test.go: -------------------------------------------------------------------------------- 1 | package goproxy 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httptest" 9 | "os" 10 | "os/exec" 11 | "strings" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func orFatal(msg string, err error, t *testing.T) { 17 | if err != nil { 18 | t.Fatal(msg, err) 19 | } 20 | } 21 | 22 | type ConstantHanlder string 23 | 24 | func (h ConstantHanlder) ServeHTTP(w http.ResponseWriter, r *http.Request) { 25 | w.Write([]byte(h)) 26 | } 27 | 28 | func getBrowser(args []string) string { 29 | for i, arg := range args { 30 | if arg == "-browser" && i+1 < len(arg) { 31 | return args[i+1] 32 | } 33 | if strings.HasPrefix(arg, "-browser=") { 34 | return arg[len("-browser="):] 35 | } 36 | } 37 | return "" 38 | } 39 | 40 | func TestSingerTls(t *testing.T) { 41 | cert, err := signHost(GoproxyCa, []string{"example.com", "1.1.1.1", "localhost"}) 42 | orFatal("singHost", err, t) 43 | cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) 44 | orFatal("ParseCertificate", err, t) 45 | expected := "key verifies with Go" 46 | server := httptest.NewUnstartedServer(ConstantHanlder(expected)) 47 | defer server.Close() 48 | server.TLS = &tls.Config{Certificates: []tls.Certificate{cert, GoproxyCa}} 49 | server.TLS.BuildNameToCertificate() 50 | server.StartTLS() 51 | certpool := x509.NewCertPool() 52 | certpool.AddCert(GoproxyCa.Leaf) 53 | tr := &http.Transport{ 54 | TLSClientConfig: &tls.Config{RootCAs: certpool}, 55 | } 56 | asLocalhost := strings.Replace(server.URL, "127.0.0.1", "localhost", -1) 57 | req, err := http.NewRequest("GET", asLocalhost, nil) 58 | orFatal("NewRequest", err, t) 59 | resp, err := tr.RoundTrip(req) 60 | orFatal("RoundTrip", err, t) 61 | txt, err := ioutil.ReadAll(resp.Body) 62 | orFatal("ioutil.ReadAll", err, t) 63 | if string(txt) != expected { 64 | t.Errorf("Expected '%s' got '%s'", expected, string(txt)) 65 | } 66 | browser := getBrowser(os.Args) 67 | if browser != "" { 68 | exec.Command(browser, asLocalhost).Run() 69 | time.Sleep(10 * time.Second) 70 | } 71 | } 72 | 73 | func TestSingerX509(t *testing.T) { 74 | cert, err := signHost(GoproxyCa, []string{"example.com", "1.1.1.1", "localhost"}) 75 | orFatal("singHost", err, t) 76 | cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) 77 | orFatal("ParseCertificate", err, t) 78 | certpool := x509.NewCertPool() 79 | certpool.AddCert(GoproxyCa.Leaf) 80 | orFatal("VerifyHostname", cert.Leaf.VerifyHostname("example.com"), t) 81 | orFatal("CheckSignatureFrom", cert.Leaf.CheckSignatureFrom(GoproxyCa.Leaf), t) 82 | _, err = cert.Leaf.Verify(x509.VerifyOptions{ 83 | DNSName: "example.com", 84 | Roots: certpool, 85 | }) 86 | orFatal("Verify", err, t) 87 | } 88 | -------------------------------------------------------------------------------- /regretable/regretreader.go: -------------------------------------------------------------------------------- 1 | package regretable 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | // A RegretableReader will allow you to read from a reader, and then 8 | // to "regret" reading it, and push back everything you've read. 9 | // For example, 10 | // rb := NewRegretableReader(bytes.NewBuffer([]byte{1,2,3})) 11 | // var b = make([]byte,1) 12 | // rb.Read(b) // b[0] = 1 13 | // rb.Regret() 14 | // ioutil.ReadAll(rb.Read) // returns []byte{1,2,3},nil 15 | type RegretableReader struct { 16 | reader io.Reader 17 | overflow bool 18 | r, w int 19 | buf []byte 20 | } 21 | 22 | var defaultBufferSize = 500 23 | 24 | // Same as RegretableReader, but allows closing the underlying reader 25 | type RegretableReaderCloser struct { 26 | RegretableReader 27 | c io.Closer 28 | } 29 | 30 | // Closes the underlying readCloser, you cannot regret after closing the stream 31 | func (rbc *RegretableReaderCloser) Close() error { 32 | return rbc.c.Close() 33 | } 34 | 35 | // initialize a RegretableReaderCloser with underlying readCloser rc 36 | func NewRegretableReaderCloser(rc io.ReadCloser) *RegretableReaderCloser { 37 | return &RegretableReaderCloser{*NewRegretableReader(rc), rc} 38 | } 39 | 40 | // initialize a RegretableReaderCloser with underlying readCloser rc 41 | func NewRegretableReaderCloserSize(rc io.ReadCloser, size int) *RegretableReaderCloser { 42 | return &RegretableReaderCloser{*NewRegretableReaderSize(rc, size), rc} 43 | } 44 | 45 | // The next read from the RegretableReader will be as if the underlying reader 46 | // was never read (or from the last point forget is called). 47 | func (rb *RegretableReader) Regret() { 48 | if rb.overflow { 49 | panic("regretting after overflow makes no sense") 50 | } 51 | rb.r = 0 52 | } 53 | 54 | // Will "forget" everything read so far. 55 | // rb := NewRegretableReader(bytes.NewBuffer([]byte{1,2,3})) 56 | // var b = make([]byte,1) 57 | // rb.Read(b) // b[0] = 1 58 | // rb.Forget() 59 | // rb.Read(b) // b[0] = 2 60 | // rb.Regret() 61 | // ioutil.ReadAll(rb.Read) // returns []byte{2,3},nil 62 | func (rb *RegretableReader) Forget() { 63 | if rb.overflow { 64 | panic("forgetting after overflow makes no sense") 65 | } 66 | rb.r = 0 67 | rb.w = 0 68 | } 69 | 70 | // initialize a RegretableReader with underlying reader r, whose buffer is size bytes long 71 | func NewRegretableReaderSize(r io.Reader, size int) *RegretableReader { 72 | return &RegretableReader{reader: r, buf: make([]byte, size) } 73 | } 74 | 75 | // initialize a RegretableReader with underlying reader r 76 | func NewRegretableReader(r io.Reader) *RegretableReader { 77 | return NewRegretableReaderSize(r, defaultBufferSize) 78 | } 79 | 80 | // reads from the underlying reader. Will buffer all input until Regret is called. 81 | func (rb *RegretableReader) Read(p []byte) (n int, err error) { 82 | if rb.overflow { 83 | return rb.reader.Read(p) 84 | } 85 | if rb.r < rb.w { 86 | n = copy(p, rb.buf[rb.r:rb.w]) 87 | rb.r += n 88 | return 89 | } 90 | n, err = rb.reader.Read(p) 91 | bn := copy(rb.buf[rb.w:], p[:n]) 92 | rb.w, rb.r = rb.w + bn, rb.w + n 93 | if bn < n { 94 | rb.overflow = true 95 | } 96 | return 97 | } 98 | -------------------------------------------------------------------------------- /actions.go: -------------------------------------------------------------------------------- 1 | package goproxy 2 | 3 | import "net/http" 4 | 5 | // ReqHandler will "tamper" with the request coming to the proxy server 6 | // If Handle returns req,nil the proxy will send the returned request 7 | // to the destination server. If it returns nil,resp the proxy will 8 | // skip sending any requests, and will simply return the response `resp` 9 | // to the client. 10 | type ReqHandler interface { 11 | Handle(req *http.Request, ctx *ProxyCtx) (*http.Request, *http.Response) 12 | } 13 | 14 | // A wrapper that would convert a function to a ReqHandler interface type 15 | type FuncReqHandler func(req *http.Request, ctx *ProxyCtx) (*http.Request, *http.Response) 16 | 17 | // FuncReqHandler.Handle(req,ctx) <=> FuncReqHandler(req,ctx) 18 | func (f FuncReqHandler) Handle(req *http.Request, ctx *ProxyCtx) (*http.Request, *http.Response) { 19 | return f(req, ctx) 20 | } 21 | 22 | // after the proxy have sent the request to the destination server, it will 23 | // "filter" the response through the RespHandlers it has. 24 | // The proxy server will send to the client the response returned by the RespHandler. 25 | // In case of error, resp will be nil, and ctx.RoundTrip.Error will contain the error 26 | type RespHandler interface { 27 | Handle(resp *http.Response, ctx *ProxyCtx) *http.Response 28 | } 29 | 30 | // A wrapper that would convert a function to a RespHandler interface type 31 | type FuncRespHandler func(resp *http.Response, ctx *ProxyCtx) *http.Response 32 | 33 | // FuncRespHandler.Handle(req,ctx) <=> FuncRespHandler(req,ctx) 34 | func (f FuncRespHandler) Handle(resp *http.Response, ctx *ProxyCtx) *http.Response { 35 | return f(resp, ctx) 36 | } 37 | 38 | // When a client send a CONNECT request to a host, the request is filtered through 39 | // all the HttpsHandlers the proxy has, and if one returns true, the connection is 40 | // sniffed using Man in the Middle attack. 41 | // That is, the proxy will create a TLS connection with the client, another TLS 42 | // connection with the destination the client wished to connect to, and would 43 | // send back and forth all messages from the server to the client and vice versa. 44 | // The request and responses sent in this Man In the Middle channel are filtered 45 | // through the usual flow (request and response filtered through the ReqHandlers 46 | // and RespHandlers) 47 | type HttpsHandler interface { 48 | HandleConnect(req string, ctx *ProxyCtx) (*ConnectAction, string) 49 | } 50 | 51 | // A wrapper that would convert a function to a HttpsHandler interface type 52 | type FuncHttpsHandler func(host string, ctx *ProxyCtx) (*ConnectAction, string) 53 | 54 | // FuncHttpsHandler should implement the RespHandler interface 55 | func (f FuncHttpsHandler) HandleConnect(host string, ctx *ProxyCtx) (*ConnectAction, string) { 56 | return f(host, ctx) 57 | } 58 | 59 | // OnDone is called when a http request complete including https 60 | type DoneHandler interface { 61 | Handle(ctx *ProxyCtx) 62 | } 63 | 64 | // A wrapper that would convert a function to a DoneHandler interface type 65 | type FuncDoneHandler func(ctx *ProxyCtx) 66 | 67 | // FuncDoneHandler.Handle(req,ctx) <=> FuncDoneHandler(req,ctx) 68 | func (f FuncDoneHandler) Handle(ctx *ProxyCtx) { 69 | f(ctx) 70 | return 71 | } 72 | -------------------------------------------------------------------------------- /ext/auth/basic.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "io/ioutil" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/heixiaoma/goproxy" 11 | ) 12 | 13 | var unauthorizedMsg = []byte("407 Proxy Authentication Required") 14 | 15 | func BasicUnauthorized(req *http.Request, realm string) *http.Response { 16 | // TODO(elazar): verify realm is well formed 17 | return &http.Response{ 18 | StatusCode: 407, 19 | ProtoMajor: 1, 20 | ProtoMinor: 1, 21 | Request: req, 22 | Header: http.Header{"Proxy-Authenticate": []string{"Basic realm=" + realm}}, 23 | Body: ioutil.NopCloser(bytes.NewBuffer(unauthorizedMsg)), 24 | ContentLength: int64(len(unauthorizedMsg)), 25 | } 26 | } 27 | 28 | var proxyAuthorizationHeader = "Proxy-Authorization" 29 | 30 | func auth(proxy *goproxy.ProxyHttpServer, ctx *goproxy.ProxyCtx, f func(user, passwd string) string) bool { 31 | authheader := strings.SplitN(ctx.Req.Header.Get(proxyAuthorizationHeader), " ", 2) 32 | ctx.Req.Header.Del(proxyAuthorizationHeader) 33 | if len(authheader) != 2 || authheader[0] != "Basic" { 34 | return false 35 | } 36 | userpassraw, err := base64.StdEncoding.DecodeString(authheader[1]) 37 | if err != nil { 38 | return false 39 | } 40 | userpass := strings.SplitN(string(userpassraw), ":", 2) 41 | if len(userpass) != 2 { 42 | return false 43 | } 44 | 45 | // auth 46 | proxyURL := f(userpass[0], userpass[1]) 47 | 48 | // invalid login 49 | if proxyURL == "" { 50 | return false 51 | } 52 | 53 | // set ctx username/password 54 | ctx.Username = userpass[0] 55 | ctx.Password = userpass[1] 56 | 57 | // set ctx proxy for http 58 | ctx.ProxyURL = proxyURL 59 | 60 | // set https proxy 61 | if proxy != nil { 62 | ctx.ConnectDial = proxy.NewConnectDialToProxy("http://" + proxyURL) 63 | } 64 | return true 65 | } 66 | 67 | // Basic returns a basic HTTP authentication handler for requests 68 | // 69 | // You probably want to use auth.ProxyBasic(proxy) to enable authentication for all proxy activities 70 | func Basic(realm string, f func(user, passwd string) string) goproxy.ReqHandler { 71 | return goproxy.FuncReqHandler(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { 72 | if !auth(nil, ctx, f) { 73 | return nil, BasicUnauthorized(req, realm) 74 | } 75 | return req, nil 76 | }) 77 | } 78 | 79 | // BasicConnect returns a basic HTTP authentication handler for CONNECT requests 80 | // 81 | // You probably want to use auth.ProxyBasic(proxy) to enable authentication for all proxy activities 82 | func BasicConnect(proxy *goproxy.ProxyHttpServer, realm string, f func(user, passwd string) string) goproxy.HttpsHandler { 83 | return goproxy.FuncHttpsHandler(func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) { 84 | if !auth(proxy, ctx, f) { 85 | ctx.Resp = BasicUnauthorized(ctx.Req, realm) 86 | return goproxy.RejectConnect, host 87 | } 88 | return goproxy.OkConnect, host 89 | }) 90 | } 91 | 92 | // ProxyBasic will force HTTP authentication before any request to the proxy is processed 93 | func ProxyBasic(proxy *goproxy.ProxyHttpServer, realm string, f func(user, passwd string) string) { 94 | proxy.OnRequest().Do(Basic(realm, f)) 95 | proxy.OnRequest().HandleConnect(BasicConnect(proxy, realm, f)) 96 | } 97 | -------------------------------------------------------------------------------- /key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJKAIBAAKCAgEAnhDL4fqGGhjWzRBFy8iHGuNIdo79FtoWPevCpyek6AWrTuBF 3 | 0j3dzRMUpAkemC/p94tGES9f9iWUVi7gnfmUz1lxhjiqUoW5K1xfwmbx+qmC2YAw 4 | HM+yq2oOLwz1FAYoQ3NT0gU6cJXtIB6Hjmxwy4jfDPzCuMFwfvOq4eS+pRJhnPTf 5 | m31XpZOsfJMS9PjD6UU5U3ZsD/oMAjGuMGIXoOGgmqeFrRJm0N+/vtenAYbcSED+ 6 | qiGGJisOu5grvMl0RJAvjgvDMw+6lWKCpqV+/5gd9CNuFP3nUhW6tbY0mBHIETrZ 7 | 0uuUdh21P20JMKt34ok0wn6On2ECN0i7UGv+SJ9TgXj7hksxH1R6OLQaSQ8qxh3I 8 | yeqPSnQ+iDK8/WXiqZug8iYxi1qgW5iYxiV5uAL0s3XRsv3Urj6Mu3QjVie0TOuq 9 | AmhawnO1gPDnjc3NLLlb79yrhdFiC2rVvRFbC5SKzB7OYyh7IdnwFAl7bEyMA6WU 10 | BIN+prw4rdYAEcmnLjNSudQGIy48hPMP8W4PHgLkjDCULryAcBluU2qkFkJfScUK 11 | 0qNg5wjZKjkdtDY4LxAX7MZW524dRKiTiFLLYEF9nWl+/OKoF561YnAW9qkYHjic 12 | geFYo0q+o7Es0jLt75MZGJY6iasBYzXxVJH0tlsHGkkrs8tLNapglhNEJkcCAwEA 13 | AQKCAgAwSuNvxHHqUUJ3XoxkiXy1u1EtX9x1eeYnvvs2xMb+WJURQTYz2NEGUdkR 14 | kPO2/ZSXHAcpQvcnpi2e8y2PNmy/uQ0VPATVt6NuWweqxncR5W5j82U/uDlXY8y3 15 | lVbfak4s5XRri0tikHvlP06dNgZ0OPok5qi7d+Zd8yZ3Y8LXfjkykiIrSG1Z2jdt 16 | zCWTkNmSUKMGG/1CGFxI41Lb12xuq+C8v4f469Fb6bCUpyCQN9rffHQSGLH6wVb7 17 | +68JO+d49zCATpmx5RFViMZwEcouXxRvvc9pPHXLP3ZPBD8nYu9kTD220mEGgWcZ 18 | 3L9dDlZPcSocbjw295WMvHz2QjhrDrb8gXwdpoRyuyofqgCyNxSnEC5M13SjOxtf 19 | pjGzjTqh0kDlKXg2/eTkd9xIHjVhFYiHIEeITM/lHCfWwBCYxViuuF7pSRPzTe8U 20 | C440b62qZSPMjVoquaMg+qx0n9fKSo6n1FIKHypv3Kue2G0WhDeK6u0U288vQ1t4 21 | Ood3Qa13gZ+9hwDLbM/AoBfVBDlP/tpAwa7AIIU1ZRDNbZr7emFdctx9B6kLINv3 22 | 4PDOGM2xrjOuACSGMq8Zcu7LBz35PpIZtviJOeKNwUd8/xHjWC6W0itgfJb5I1Nm 23 | V6Vj368pGlJx6Se26lvXwyyrc9pSw6jSAwARBeU4YkNWpi4i6QKCAQEA0T7u3P/9 24 | jZJSnDN1o2PXymDrJulE61yguhc/QSmLccEPZe7or06/DmEhhKuCbv+1MswKDeag 25 | /1JdFPGhL2+4G/f/9BK3BJPdcOZSz7K6Ty8AMMBf8AehKTcSBqwkJWcbEvpHpKJ6 26 | eDqn1B6brXTNKMT6fEEXCuZJGPBpNidyLv/xXDcN7kCOo3nGYKfB5OhFpNiL63tw 27 | +LntU56WESZwEqr8Pf80uFvsyXQK3a5q5HhIQtxl6tqQuPlNjsDBvCqj0x72mmaJ 28 | ZVsVWlv7khUrCwAXz7Y8K7mKKBd2ekF5hSbryfJsxFyvEaWUPhnJpTKV85lAS+tt 29 | FQuIp9TvKYlRQwKCAQEAwWJN8jysapdhi67jO0HtYOEl9wwnF4w6XtiOYtllkMmC 30 | 06/e9h7RsRyWPMdu3qRDPUYFaVDy6+dpUDSQ0+E2Ot6AHtVyvjeUTIL651mFIo/7 31 | OSUCEc+HRo3SfPXdPhSQ2thNTxl6y9XcFacuvbthgr70KXbvC4k6IEmdpf/0Kgs9 32 | 7QTZCG26HDrEZ2q9yMRlRaL2SRD+7Y2xra7gB+cQGFj6yn0Wd/07er49RqMXidQf 33 | KR2oYfev2BDtHXoSZFfhFGHlOdLvWRh90D4qZf4vQ+g/EIMgcNSoxjvph1EShmKt 34 | sjhTHtoHuu+XmEQvIewk2oCI+JvofBkcnpFrVvUUrQKCAQAaTIufETmgCo0BfuJB 35 | N/JOSGIl0NnNryWwXe2gVgVltbsmt6FdL0uKFiEtWJUbOF5g1Q5Kcvs3O/XhBQGa 36 | QbNlKIVt+tAv7hm97+Tmn/MUsraWagdk1sCluns0hXxBizT27KgGhDlaVRz05yfv 37 | 5CdJAYDuDwxDXXBAhy7iFJEgYSDH00+X61tCJrMNQOh4ycy/DEyBu1EWod+3S85W 38 | t3sMjZsIe8P3i+4137Th6eMbdha2+JaCrxfTd9oMoCN5b+6JQXIDM/H+4DTN15PF 39 | 540yY7+aZrAnWrmHknNcqFAKsTqfdi2/fFqwoBwCtiEG91WreU6AfEWIiJuTZIru 40 | sIibAoIBAAqIwlo5t+KukF+9jR9DPh0S5rCIdvCvcNaN0WPNF91FPN0vLWQW1bFi 41 | L0TsUDvMkuUZlV3hTPpQxsnZszH3iK64RB5p3jBCcs+gKu7DT59MXJEGVRCHT4Um 42 | YJryAbVKBYIGWl++sZO8+JotWzx2op8uq7o+glMMjKAJoo7SXIiVyC/LHc95urOi 43 | 9+PySphPKn0anXPpexmRqGYfqpCDo7rPzgmNutWac80B4/CfHb8iUPg6Z1u+1FNe 44 | yKvcZHgW2Wn00znNJcCitufLGyAnMofudND/c5rx2qfBx7zZS7sKUQ/uRYjes6EZ 45 | QBbJUA/2/yLv8YYpaAaqj4aLwV8hRpkCggEBAIh3e25tr3avCdGgtCxS7Y1blQ2c 46 | ue4erZKmFP1u8wTNHQ03T6sECZbnIfEywRD/esHpclfF3kYAKDRqIP4K905Rb0iH 47 | 759ZWt2iCbqZznf50XTvptdmjm5KxvouJzScnQ52gIV6L+QrCKIPelLBEIqCJREh 48 | pmcjjocD/UCCSuHgbAYNNnO/JdhnSylz1tIg26I+2iLNyeTKIepSNlsBxnkLmqM1 49 | cj/azKBaT04IOMLaN8xfSqitJYSraWMVNgGJM5vfcVaivZnNh0lZBv+qu6YkdM88 50 | 4/avCJ8IutT+FcMM+GbGazOm5ALWqUyhrnbLGc4CQMPfe7Il6NxwcrOxT8w= 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /counterecryptor_test.go: -------------------------------------------------------------------------------- 1 | package goproxy_test 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rsa" 6 | "encoding/binary" 7 | "github.com/heixiaoma/goproxy" 8 | "io" 9 | "math" 10 | "math/rand" 11 | "testing" 12 | ) 13 | 14 | type RandSeedReader struct { 15 | r rand.Rand 16 | } 17 | 18 | func (r *RandSeedReader) Read(b []byte) (n int, err error) { 19 | for i := range b { 20 | b[i] = byte(r.r.Int() & 0xFF) 21 | } 22 | return len(b), nil 23 | } 24 | 25 | func TestCounterEncDifferentConsecutive(t *testing.T) { 26 | k, err := rsa.GenerateKey(&RandSeedReader{*rand.New(rand.NewSource(0xFF43109))}, 128) 27 | fatalOnErr(err, "rsa.GenerateKey", t) 28 | c, err := goproxy.NewCounterEncryptorRandFromKey(k, []byte("the quick brown fox run over the lazy dog")) 29 | fatalOnErr(err, "NewCounterEncryptorRandFromKey", t) 30 | for i := 0; i < 100*1000; i++ { 31 | var a, b int64 32 | binary.Read(&c, binary.BigEndian, &a) 33 | binary.Read(&c, binary.BigEndian, &b) 34 | if a == b { 35 | t.Fatal("two consecutive equal int64", a, b) 36 | } 37 | } 38 | } 39 | 40 | func TestCounterEncIdenticalStreams(t *testing.T) { 41 | k, err := rsa.GenerateKey(&RandSeedReader{*rand.New(rand.NewSource(0xFF43109))}, 128) 42 | fatalOnErr(err, "rsa.GenerateKey", t) 43 | c1, err := goproxy.NewCounterEncryptorRandFromKey(k, []byte("the quick brown fox run over the lazy dog")) 44 | fatalOnErr(err, "NewCounterEncryptorRandFromKey", t) 45 | c2, err := goproxy.NewCounterEncryptorRandFromKey(k, []byte("the quick brown fox run over the lazy dog")) 46 | fatalOnErr(err, "NewCounterEncryptorRandFromKey", t) 47 | nout := 1000 48 | out1, out2 := make([]byte, nout), make([]byte, nout) 49 | io.ReadFull(&c1, out1) 50 | tmp := out2[:] 51 | rand.Seed(0xFF43109) 52 | for len(tmp) > 0 { 53 | n := 1 + rand.Intn(256) 54 | if n > len(tmp) { 55 | n = len(tmp) 56 | } 57 | n, err := c2.Read(tmp[:n]) 58 | fatalOnErr(err, "CounterEncryptorRand.Read", t) 59 | tmp = tmp[n:] 60 | } 61 | if !bytes.Equal(out1, out2) { 62 | t.Error("identical CSPRNG does not produce the same output") 63 | } 64 | } 65 | 66 | func stddev(data []int) float64 { 67 | var sum, sum_sqr float64 = 0, 0 68 | for _, h := range data { 69 | sum += float64(h) 70 | sum_sqr += float64(h) * float64(h) 71 | } 72 | n := float64(len(data)) 73 | variance := (sum_sqr - ((sum * sum) / n)) / (n - 1) 74 | return math.Sqrt(variance) 75 | } 76 | 77 | func TestCounterEncStreamHistogram(t *testing.T) { 78 | k, err := rsa.GenerateKey(&RandSeedReader{*rand.New(rand.NewSource(0xFF43109))}, 128) 79 | fatalOnErr(err, "rsa.GenerateKey", t) 80 | c, err := goproxy.NewCounterEncryptorRandFromKey(k, []byte("the quick brown fox run over the lazy dog")) 81 | fatalOnErr(err, "NewCounterEncryptorRandFromKey", t) 82 | nout := 100 * 1000 83 | out := make([]byte, nout) 84 | io.ReadFull(&c, out) 85 | refhist := make([]int, 256) 86 | for i := 0; i < nout; i++ { 87 | refhist[rand.Intn(256)]++ 88 | } 89 | hist := make([]int, 256) 90 | for _, b := range out { 91 | hist[int(b)]++ 92 | } 93 | refstddev, stddev := stddev(refhist), stddev(hist) 94 | // due to lack of time, I guestimate 95 | t.Logf("ref:%v - act:%v = %v", refstddev, stddev, math.Abs(refstddev-stddev)) 96 | if math.Abs(refstddev-stddev) >= 1 { 97 | t.Errorf("stddev of ref histogram different than regular PRNG: %v %v", refstddev, stddev) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /ext/html/html.go: -------------------------------------------------------------------------------- 1 | // extension to goproxy that will allow you to easily filter web browser related content. 2 | package goproxy_html 3 | 4 | import ( 5 | "bytes" 6 | "errors" 7 | "io" 8 | "io/ioutil" 9 | "net/http" 10 | "strings" 11 | 12 | "github.com/heixiaoma/goproxy" 13 | "github.com/rogpeppe/go-charset/charset" 14 | _ "github.com/rogpeppe/go-charset/data" 15 | ) 16 | 17 | var IsHtml goproxy.RespCondition = goproxy.ContentTypeIs("text/html") 18 | 19 | var IsCss goproxy.RespCondition = goproxy.ContentTypeIs("text/css") 20 | 21 | var IsJavaScript goproxy.RespCondition = goproxy.ContentTypeIs("text/javascript", 22 | "application/javascript") 23 | 24 | var IsJson goproxy.RespCondition = goproxy.ContentTypeIs("text/json") 25 | 26 | var IsXml goproxy.RespCondition = goproxy.ContentTypeIs("text/xml") 27 | 28 | var IsWebRelatedText goproxy.RespCondition = goproxy.ContentTypeIs("text/html", 29 | "text/css", 30 | "text/javascript", "application/javascript", 31 | "text/xml", 32 | "text/json") 33 | 34 | // HandleString will receive a function that filters a string, and will convert the 35 | // request body to a utf8 string, according to the charset specified in the Content-Type 36 | // header. 37 | // guessing Html charset encoding from the tags is not yet implemented. 38 | func HandleString(f func(s string, ctx *goproxy.ProxyCtx) string) goproxy.RespHandler { 39 | return HandleStringReader(func(r io.Reader, ctx *goproxy.ProxyCtx) io.Reader { 40 | b, err := ioutil.ReadAll(r) 41 | if err != nil { 42 | ctx.Warnf("Cannot read string from resp body: %v", err) 43 | return r 44 | } 45 | return bytes.NewBufferString(f(string(b), ctx)) 46 | }) 47 | } 48 | 49 | // Will receive an input stream which would convert the response to utf-8 50 | // The given function must close the reader r, in order to close the response body. 51 | func HandleStringReader(f func(r io.Reader, ctx *goproxy.ProxyCtx) io.Reader) goproxy.RespHandler { 52 | return goproxy.FuncRespHandler(func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response { 53 | if ctx.Error != nil { 54 | return nil 55 | } 56 | charsetName := ctx.Charset() 57 | if charsetName == "" { 58 | charsetName = "utf-8" 59 | } 60 | 61 | if strings.ToLower(charsetName) != "utf-8" { 62 | r, err := charset.NewReader(charsetName, resp.Body) 63 | if err != nil { 64 | ctx.Warnf("Cannot convert from %v to utf-8: %v", charsetName, err) 65 | return resp 66 | } 67 | tr, err := charset.TranslatorTo(charsetName) 68 | if err != nil { 69 | ctx.Warnf("Can't translate to %v from utf-8: %v", charsetName, err) 70 | return resp 71 | } 72 | if err != nil { 73 | ctx.Warnf("Cannot translate to %v: %v", charsetName, err) 74 | return resp 75 | } 76 | newr := charset.NewTranslatingReader(f(r, ctx), tr) 77 | resp.Body = &readFirstCloseBoth{ioutil.NopCloser(newr), resp.Body} 78 | } else { 79 | //no translation is needed, already at utf-8 80 | resp.Body = &readFirstCloseBoth{ioutil.NopCloser(f(resp.Body, ctx)), resp.Body} 81 | } 82 | return resp 83 | }) 84 | } 85 | 86 | type readFirstCloseBoth struct { 87 | r io.ReadCloser 88 | c io.Closer 89 | } 90 | 91 | func (rfcb *readFirstCloseBoth) Read(b []byte) (nr int, err error) { 92 | return rfcb.r.Read(b) 93 | } 94 | func (rfcb *readFirstCloseBoth) Close() error { 95 | err1 := rfcb.r.Close() 96 | err2 := rfcb.c.Close() 97 | if err1 != nil && err2 != nil { 98 | return errors.New(err1.Error() + ", " + err2.Error()) 99 | } 100 | if err1 != nil { 101 | return err1 102 | } 103 | return err2 104 | } 105 | -------------------------------------------------------------------------------- /examples/goproxy-yui-minify/yui.go: -------------------------------------------------------------------------------- 1 | // This example would minify standalone Javascript files (identified by their content type) 2 | // using the command line utility YUI compressor http://yui.github.io/yuicompressor/ 3 | // Example usage: 4 | // 5 | // ./yui -java /usr/local/bin/java -yuicompressor ~/Downloads/yuicompressor-2.4.8.jar 6 | // $ curl -vx localhost:8080 http://golang.org/lib/godoc/godocs.js 7 | // (function(){function g(){var u=$("#search");if(u.length===0){return}function t(){if(.... 8 | // $ curl http://golang.org/lib/godoc/godocs.js | head -n 3 9 | // // Copyright 2012 The Go Authors. All rights reserved. 10 | // // Use of this source code is governed by a BSD-style 11 | // // license that can be found in the LICENSE file. 12 | package main 13 | 14 | import ( 15 | "flag" 16 | "io" 17 | "io/ioutil" 18 | "log" 19 | "net/http" 20 | "os" 21 | "os/exec" 22 | "path" 23 | "strings" 24 | 25 | "github.com/heixiaoma/goproxy" 26 | ) 27 | 28 | func main() { 29 | verbose := flag.Bool("v", false, "should every proxy request be logged to stdout") 30 | addr := flag.String("addr", ":8080", "proxy listen address") 31 | java := flag.String("javapath", "java", "where the Java executable is located") 32 | yuicompressor := flag.String("yuicompressor", "", "where the yuicompressor is located, assumed to be in CWD") 33 | yuicompressordir := flag.String("yuicompressordir", ".", "a folder to search yuicompressor in, will be ignored if yuicompressor is set") 34 | flag.Parse() 35 | if *yuicompressor == "" { 36 | files, err := ioutil.ReadDir(*yuicompressordir) 37 | if err != nil { 38 | log.Fatal("Cannot find yuicompressor jar") 39 | } 40 | for _, file := range files { 41 | if strings.HasPrefix(file.Name(), "yuicompressor") && strings.HasSuffix(file.Name(), ".jar") { 42 | c := path.Join(*yuicompressordir, file.Name()) 43 | yuicompressor = &c 44 | break 45 | } 46 | } 47 | } 48 | if *yuicompressor == "" { 49 | log.Fatal("Can't find yuicompressor jar, searched yuicompressor*.jar in dir ", *yuicompressordir) 50 | } 51 | if _, err := os.Stat(*yuicompressor); os.IsNotExist(err) { 52 | log.Fatal("Can't find yuicompressor jar specified ", *yuicompressor) 53 | } 54 | proxy := goproxy.NewProxyHttpServer() 55 | proxy.Verbose = *verbose 56 | proxy.OnResponse().DoFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response { 57 | contentType := resp.Header.Get("Content-Type") 58 | if contentType == "application/javascript" || contentType == "application/x-javascript" { 59 | // in real code, response should be streamed as well 60 | var err error 61 | cmd := exec.Command(*java, "-jar", *yuicompressor, "--type", "js") 62 | cmd.Stdin = resp.Body 63 | resp.Body, err = cmd.StdoutPipe() 64 | if err != nil { 65 | ctx.Warnf("Cannot minify content in %v: %v", ctx.Req.URL, err) 66 | return goproxy.TextResponse(ctx.Req, "Error getting stdout pipe") 67 | } 68 | stderr, err := cmd.StderrPipe() 69 | if err != nil { 70 | ctx.Logf("Error obtaining stderr from yuicompress: %s", err) 71 | return goproxy.TextResponse(ctx.Req, "Error getting stderr pipe") 72 | } 73 | if err := cmd.Start(); err != nil { 74 | ctx.Warnf("Cannot minify content in %v: %v", ctx.Req.URL, err) 75 | } 76 | go func() { 77 | defer stderr.Close() 78 | const kb = 1024 79 | msg, err := ioutil.ReadAll(&io.LimitedReader{stderr, 50 * kb}) 80 | if len(msg) != 0 { 81 | ctx.Logf("Error executing yuicompress: %s", string(msg)) 82 | } 83 | if err != nil { 84 | ctx.Logf("Error reading stderr from yuicompress: %s", string(msg)) 85 | } 86 | }() 87 | } 88 | return resp 89 | }) 90 | log.Fatal(http.ListenAndServe(*addr, proxy)) 91 | } 92 | -------------------------------------------------------------------------------- /examples/goproxy-jquery-version/jquery_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "net/http/httptest" 9 | "net/url" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | func equal(u, v []string) bool { 15 | if len(u) != len(v) { 16 | return false 17 | } 18 | for i, _ := range u { 19 | if u[i] != v[i] { 20 | return false 21 | } 22 | } 23 | return true 24 | } 25 | 26 | func readFile(fname string, t *testing.T) string { 27 | b, err := ioutil.ReadFile(fname) 28 | if err != nil { 29 | t.Fatal("readFile", err) 30 | } 31 | return string(b) 32 | } 33 | 34 | func TestDefectiveScriptParser(t *testing.T) { 35 | if l := len(findScriptSrc(` 36 | 37 | 38 | 39 | 45 | 46 | 47 | `)); l != 0 { 48 | t.Fail() 49 | } 50 | urls := findScriptSrc(readFile("w3schools.html", t)) 51 | if !equal(urls, []string{"http://partner.googleadservices.com/gampad/google_service.js", 52 | "//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit"}) { 53 | t.Error("w3schools.html", "src scripts are not recognized", urls) 54 | } 55 | urls = findScriptSrc(readFile("jquery_homepage.html", t)) 56 | if !equal(urls, []string{"http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js", 57 | "http://code.jquery.com/jquery-1.4.2.min.js", 58 | "http://static.jquery.com/files/rocker/scripts/custom.js", 59 | "http://static.jquery.com/donate/donate.js"}) { 60 | t.Error("jquery_homepage.html", "src scripts are not recognized", urls) 61 | } 62 | } 63 | 64 | func proxyWithLog() (*http.Client, *bytes.Buffer) { 65 | proxy := NewJqueryVersionProxy() 66 | proxyServer := httptest.NewServer(proxy) 67 | buf := new(bytes.Buffer) 68 | proxy.Logger = log.New(buf, "", 0) 69 | proxyUrl, _ := url.Parse(proxyServer.URL) 70 | tr := &http.Transport{Proxy: http.ProxyURL(proxyUrl)} 71 | client := &http.Client{Transport: tr} 72 | return client, buf 73 | } 74 | 75 | func get(t *testing.T, server *httptest.Server, client *http.Client, url string) { 76 | resp, err := client.Get(server.URL + url) 77 | if err != nil { 78 | t.Fatal("cannot get proxy", err) 79 | } 80 | ioutil.ReadAll(resp.Body) 81 | resp.Body.Close() 82 | } 83 | 84 | func TestProxyServiceTwoVersions(t *testing.T) { 85 | var fs = httptest.NewServer(http.FileServer(http.Dir("."))) 86 | defer fs.Close() 87 | 88 | client, buf := proxyWithLog() 89 | 90 | get(t, fs, client, "/w3schools.html") 91 | get(t, fs, client, "/php_man.html") 92 | if buf.String() != "" && 93 | !strings.Contains(buf.String(), " uses jquery ") { 94 | t.Error("shouldn't warn on a single URL", buf.String()) 95 | } 96 | get(t, fs, client, "/jquery1.html") 97 | warnings := buf.String() 98 | if !strings.Contains(warnings, "http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js") || 99 | !strings.Contains(warnings, "jquery.1.4.js") || 100 | !strings.Contains(warnings, "Contradicting") { 101 | t.Error("contradicting jquery versions (php_man.html, w3schools.html) does not issue warning", warnings) 102 | } 103 | } 104 | 105 | func TestProxyService(t *testing.T) { 106 | var fs = httptest.NewServer(http.FileServer(http.Dir("."))) 107 | defer fs.Close() 108 | 109 | client, buf := proxyWithLog() 110 | 111 | get(t, fs, client, "/jquery_homepage.html") 112 | warnings := buf.String() 113 | if !strings.Contains(warnings, "http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js") || 114 | !strings.Contains(warnings, "http://code.jquery.com/jquery-1.4.2.min.js") || 115 | !strings.Contains(warnings, "Contradicting") { 116 | t.Error("contradicting jquery versions does not issue warning") 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /examples/goproxy-customca/cert.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | 7 | "github.com/heixiaoma/goproxy" 8 | ) 9 | 10 | var caCert = []byte(`-----BEGIN CERTIFICATE----- 11 | MIIDkzCCAnugAwIBAgIJAKe/ZGdfcHdPMA0GCSqGSIb3DQEBCwUAMGAxCzAJBgNV 12 | BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX 13 | aWRnaXRzIFB0eSBMdGQxGTAXBgNVBAMMEGRlbW8gZm9yIGdvcHJveHkwHhcNMTYw 14 | OTI3MTQzNzQ3WhcNMTkwOTI3MTQzNzQ3WjBgMQswCQYDVQQGEwJBVTETMBEGA1UE 15 | CAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRk 16 | MRkwFwYDVQQDDBBkZW1vIGZvciBnb3Byb3h5MIIBIjANBgkqhkiG9w0BAQEFAAOC 17 | AQ8AMIIBCgKCAQEA2+W48YZoch72zj0a+ZlyFVY2q2MWmqsEY9f/u53fAeTxvPE6 18 | 1/DnqsydnA3FnGvxw9Dz0oZO6xG+PZvp+lhN07NZbuXK1nie8IpxCa342axpu4C0 19 | 69lZwxikpGyJO4IL5ywp/qfb5a2DxPTAyQOQ8ROAaydoEmktRp25yicnQ2yeZW// 20 | 1SIQxt7gRxQIGmuOQ/Gqr/XN/z2cZdbGJVRUvQXk7N6NhQiCX1zlmp1hzUW9jwC+ 21 | JEKKF1XVpQbc94Bo5supxhkKJ70CREPy8TH9mAUcQUZQRohnPvvt/lKneYAGhjHK 22 | vhpajwlbMMSocVXFvY7o/IqIE/+ZUeQTs1SUwQIDAQABo1AwTjAdBgNVHQ4EFgQU 23 | GnlWcIbfsWJW7GId+6xZIK8YlFEwHwYDVR0jBBgwFoAUGnlWcIbfsWJW7GId+6xZ 24 | IK8YlFEwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAoFUjSD15rKlY 25 | xudzyVlr6n0fRNhITkiZMX3JlFOvtHNYif8RfK4TH/oHNBTmle69AgixjMgy8GGd 26 | H90prytGQ5zCs1tKcCFsN5gRSgdAkc2PpRFOK6u8HwOITV5lV7sjucsddXJcOJbQ 27 | 4fyVe47V9TTxI+A7lRnUP2HYTR1Bd0R/IgRAH57d1ZHs7omHIuQ+Ea8ph2ppXMnP 28 | DXVOlZ9zfczSnPnQoomqULOU9Fq2ycyi8Y/ROtAHP6O7wCFbYHXhxojdaHSdhkcd 29 | troTflFMD2/4O6MtBKbHxSmEG6H0FBYz5xUZhZq7WUH24V3xYsfge29/lOCd5/Xf 30 | A+j0RJc/lQ== 31 | -----END CERTIFICATE-----`) 32 | 33 | var caKey = []byte(`-----BEGIN RSA PRIVATE KEY----- 34 | MIIEowIBAAKCAQEA2+W48YZoch72zj0a+ZlyFVY2q2MWmqsEY9f/u53fAeTxvPE6 35 | 1/DnqsydnA3FnGvxw9Dz0oZO6xG+PZvp+lhN07NZbuXK1nie8IpxCa342axpu4C0 36 | 69lZwxikpGyJO4IL5ywp/qfb5a2DxPTAyQOQ8ROAaydoEmktRp25yicnQ2yeZW// 37 | 1SIQxt7gRxQIGmuOQ/Gqr/XN/z2cZdbGJVRUvQXk7N6NhQiCX1zlmp1hzUW9jwC+ 38 | JEKKF1XVpQbc94Bo5supxhkKJ70CREPy8TH9mAUcQUZQRohnPvvt/lKneYAGhjHK 39 | vhpajwlbMMSocVXFvY7o/IqIE/+ZUeQTs1SUwQIDAQABAoIBAHK94ww8W0G5QIWL 40 | Qwkc9XeGvg4eLUxVknva2Ll4fkZJxY4WveKx9OCd1lv4n7WoacYIwUGIDaQBZShW 41 | s/eKnkmqGy+PvpC87gqL4sHvQpuqqJ1LYpxylLEFqduWOuGPUVC2Lc+QnWCycsCS 42 | CgqZzsbMq0S+kkKRGSvw32JJneZCzqLgLNssQNVk+Gm6SI3s4jJsGPesjhnvoPaa 43 | xZK14uFpltaA05GSTDaQeZJFEdnnb3f/eNPc2xMEfi0S2ZlJ6Q92WJEOepAetDlR 44 | cRFi004bNyTb4Bphg8s4+9Cti5is199aFkGCRDWxeqEnc6aMY3Ezu9Qg3uttLVUd 45 | uy830GUCgYEA7qS0X+9UH1R02L3aoANyADVbFt2ZpUwQGauw9WM92pH52xeHAw1S 46 | ohus6FI3OC8xQq2CN525tGLUbFDZnNZ3YQHqFsfgevfnTs1//gbKXomitev0oFKh 47 | VT+WYS4lkgYtPlXzhdGuk32q99T/wIocAguvCUY3PiA7yBz93ReyausCgYEA6+P8 48 | bugMqT8qjoiz1q/YCfxsw9bAGWjlVqme2xmp256AKtxvCf1BPsToAaJU3nFi3vkw 49 | ICLxUWAYoMBODJ3YnbOsIZOavdXZwYHv54JqwqFealC3DG0Du6fZYZdiY8pK+E6m 50 | 3fiYzP1WoVK5tU4bH8ibuIQvpcI8j7Gy0cV6/AMCgYBHl7fZNAZro72uLD7DVGVF 51 | 9LvP/0kR0uDdoqli5JPw12w6szM40i1hHqZfyBJy042WsFDpeHL2z9Nkb1jpeVm1 52 | C4r7rJkGqwqElJf6UHUzqVzb8N6hnkhyN7JYkyyIQzwdgFGfaslRzBiXYxoa3BQM 53 | 9Q5c3OjDxY3JuhDa3DoVYwKBgDNqrWJLSD832oHZAEIicBe1IswJKjQfriWWsV6W 54 | mHSbdtpg0/88aZVR/DQm+xLFakSp0jifBTS0momngRu06Dtvp2xmLQuF6oIIXY97 55 | 2ON1owvPbibSOEcWDgb8pWCU/oRjOHIXts6vxctCKeKAFN93raGphm0+Ck9T72NU 56 | BTubAoGBAMEhI/Wy9wAETuXwN84AhmPdQsyCyp37YKt2ZKaqu37x9v2iL8JTbPEz 57 | pdBzkA2Gc0Wdb6ekIzRrTsJQl+c/0m9byFHsRsxXW2HnezfOFX1H4qAmF6KWP0ub 58 | M8aIn6Rab4sNPSrvKGrU6rFpv/6M33eegzldVnV9ku6uPJI1fFTC 59 | -----END RSA PRIVATE KEY-----`) 60 | 61 | func setCA(caCert, caKey []byte) error { 62 | goproxyCa, err := tls.X509KeyPair(caCert, caKey) 63 | if err != nil { 64 | return err 65 | } 66 | if goproxyCa.Leaf, err = x509.ParseCertificate(goproxyCa.Certificate[0]); err != nil { 67 | return err 68 | } 69 | goproxy.GoproxyCa = goproxyCa 70 | goproxy.OkConnect = &goproxy.ConnectAction{Action: goproxy.ConnectAccept, TLSConfig: goproxy.TLSConfigFromCA(&goproxyCa)} 71 | goproxy.MitmConnect = &goproxy.ConnectAction{Action: goproxy.ConnectMitm, TLSConfig: goproxy.TLSConfigFromCA(&goproxyCa)} 72 | goproxy.HTTPMitmConnect = &goproxy.ConnectAction{Action: goproxy.ConnectHTTPMitm, TLSConfig: goproxy.TLSConfigFromCA(&goproxyCa)} 73 | goproxy.RejectConnect = &goproxy.ConnectAction{Action: goproxy.ConnectReject, TLSConfig: goproxy.TLSConfigFromCA(&goproxyCa)} 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package goproxy provides a customizable HTTP proxy, 3 | supporting hijacking HTTPS connection. 4 | 5 | The intent of the proxy, is to be usable with reasonable amount of traffic 6 | yet, customizable and programable. 7 | 8 | The proxy itself is simply an `net/http` handler. 9 | 10 | Typical usage is 11 | 12 | proxy := goproxy.NewProxyHttpServer() 13 | proxy.OnRequest(..conditions..).Do(..requesthandler..) 14 | proxy.OnRequest(..conditions..).DoFunc(..requesthandlerFunction..) 15 | proxy.OnResponse(..conditions..).Do(..responesHandler..) 16 | proxy.OnResponse(..conditions..).DoFunc(..responesHandlerFunction..) 17 | http.ListenAndServe(":8080", proxy) 18 | 19 | Adding a header to each request 20 | 21 | proxy.OnRequest().DoFunc(func(r *http.Request,ctx *goproxy.ProxyCtx) (*http.Request, *http.Response){ 22 | r.Header.Set("X-GoProxy","1") 23 | return r, nil 24 | }) 25 | 26 | # Note that the function is called before the proxy sends the request to the server 27 | 28 | For printing the content type of all incoming responses 29 | 30 | proxy.OnResponse().DoFunc(func(r *http.Response, ctx *goproxy.ProxyCtx)*http.Response{ 31 | println(ctx.Req.Host,"->",r.Header.Get("Content-Type")) 32 | return r 33 | }) 34 | 35 | note that we used the ProxyCtx context variable here. It contains the request 36 | and the response (Req and Resp, Resp is nil if unavailable) of this specific client 37 | interaction with the proxy. 38 | 39 | To print the content type of all responses from a certain url, we'll add a 40 | ReqCondition to the OnResponse function: 41 | 42 | proxy.OnResponse(goproxy.UrlIs("golang.org/pkg")).DoFunc(func(r *http.Response, ctx *goproxy.ProxyCtx)*http.Response{ 43 | println(ctx.Req.Host,"->",r.Header.Get("Content-Type")) 44 | return r 45 | }) 46 | 47 | We can write the condition ourselves, conditions can be set on request and on response 48 | 49 | var random = ReqConditionFunc(func(r *http.Request) bool { 50 | return rand.Intn(1) == 0 51 | }) 52 | var hasGoProxyHeader = RespConditionFunc(func(resp *http.Response,req *http.Request)bool { 53 | return resp.Header.Get("X-GoProxy") != "" 54 | }) 55 | 56 | Caution! If you give a RespCondition to the OnRequest function, you'll get a run time panic! It doesn't 57 | make sense to read the response, if you still haven't got it! 58 | 59 | Finally, we have convenience function to throw a quick response 60 | 61 | proxy.OnResponse(hasGoProxyHeader).DoFunc(func(r*http.Response,ctx *goproxy.ProxyCtx)*http.Response { 62 | r.Body.Close() 63 | return goproxy.ForbiddenTextResponse(ctx.Req,"Can't see response with X-GoProxy header!") 64 | }) 65 | 66 | we close the body of the original repsonse, and return a new 403 response with a short message. 67 | 68 | Example use cases: 69 | 70 | 1. https://github.com/heixiaoma/goproxy/tree/master/examples/goproxy-avgsize 71 | 72 | To measure the average size of an Html served in your site. One can ask 73 | all the QA team to access the website by a proxy, and the proxy will 74 | measure the average size of all text/html responses from your host. 75 | 76 | 2. [not yet implemented] 77 | 78 | All requests to your web servers should be directed through the proxy, 79 | when the proxy will detect html pieces sent as a response to AJAX 80 | request, it'll send a warning email. 81 | 82 | 3. https://github.com/heixiaoma/goproxy/blob/master/examples/goproxy-httpdump/ 83 | 84 | Generate a real traffic to your website by real users using through 85 | proxy. Record the traffic, and try it again for more real load testing. 86 | 87 | 4. https://github.com/heixiaoma/goproxy/tree/master/examples/goproxy-no-reddit-at-worktime 88 | 89 | Will allow browsing to reddit.com between 8:00am and 17:00pm 90 | 91 | 5. https://github.com/heixiaoma/goproxy/tree/master/examples/goproxy-jquery-version 92 | 93 | Will warn if multiple versions of jquery are used in the same domain. 94 | 95 | 6. https://github.com/heixiaoma/goproxy/blob/master/examples/goproxy-upside-down-ternet/ 96 | 97 | Modifies image files in an HTTP response via goproxy's image extension found in ext/. 98 | */ 99 | package goproxy 100 | -------------------------------------------------------------------------------- /ctx.go: -------------------------------------------------------------------------------- 1 | package goproxy 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | "regexp" 7 | "sync/atomic" 8 | ) 9 | 10 | // ProxyCtx is the Proxy context, contains useful information about every request. It is passed to 11 | // every user function. Also used as a logger. 12 | type ProxyCtx struct { 13 | // Will contain the client request from the proxy 14 | Req *http.Request 15 | // Will contain the remote server's response (if available. nil if the request wasn't send yet) 16 | Resp *http.Response 17 | RoundTripper RoundTripper 18 | ConnectDial func(network string, addr string) (net.Conn, error) 19 | ProxyURL string 20 | // will contain the recent error that occured while trying to send receive or parse traffic 21 | Error error 22 | // A handle for the user to keep data in the context, from the call of ReqHandler to the 23 | // call of RespHandler 24 | UserData interface{} 25 | // Will connect a request to a response 26 | Session int64 27 | proxy *ProxyHttpServer 28 | // Auth 29 | Username string 30 | Password string 31 | // Bandwidth 32 | Inbounce int64 33 | Outbounce int64 34 | } 35 | 36 | type RoundTripper interface { 37 | RoundTrip(req *http.Request, ctx *ProxyCtx) (*http.Response, error) 38 | } 39 | 40 | type RoundTripperFunc func(req *http.Request, ctx *ProxyCtx) (*http.Response, error) 41 | 42 | func (f RoundTripperFunc) RoundTrip(req *http.Request, ctx *ProxyCtx) (*http.Response, error) { 43 | return f(req, ctx) 44 | } 45 | 46 | func (ctx *ProxyCtx) RoundTrip(req *http.Request) (*http.Response, error) { 47 | if ctx.RoundTripper != nil { 48 | return ctx.RoundTripper.RoundTrip(req, ctx) 49 | } 50 | return ctx.proxy.Tr.RoundTrip(req) 51 | } 52 | 53 | func (ctx *ProxyCtx) connectDial(network, addr string) (net.Conn, error) { 54 | if ctx.ConnectDial != nil { 55 | return ctx.ConnectDial(network, addr) 56 | } 57 | if ctx.proxy.ConnectDial != nil { 58 | return ctx.proxy.ConnectDial(network, addr) 59 | } 60 | return ctx.proxy.dial(network, addr) 61 | } 62 | 63 | func (ctx *ProxyCtx) printf(msg string, argv ...interface{}) { 64 | ctx.proxy.Logger.Printf("[%03d] "+msg+"\n", append([]interface{}{ctx.Session & 0xFF}, argv...)...) 65 | } 66 | 67 | // Logf prints a message to the proxy's log. Should be used in a ProxyHttpServer's filter 68 | // This message will be printed only if the Verbose field of the ProxyHttpServer is set to true 69 | // 70 | // proxy.OnRequest().DoFunc(func(r *http.Request,ctx *goproxy.ProxyCtx) (*http.Request, *http.Response){ 71 | // nr := atomic.AddInt32(&counter,1) 72 | // ctx.Printf("So far %d requests",nr) 73 | // return r, nil 74 | // }) 75 | func (ctx *ProxyCtx) Logf(msg string, argv ...interface{}) { 76 | if ctx.proxy.Verbose { 77 | ctx.printf("INFO: "+msg, argv...) 78 | } 79 | } 80 | 81 | // Warnf prints a message to the proxy's log. Should be used in a ProxyHttpServer's filter 82 | // This message will always be printed. 83 | // 84 | // proxy.OnRequest().DoFunc(func(r *http.Request,ctx *goproxy.ProxyCtx) (*http.Request, *http.Response){ 85 | // f,err := os.OpenFile(cachedContent) 86 | // if err != nil { 87 | // ctx.Warnf("error open file %v: %v",cachedContent,err) 88 | // return r, nil 89 | // } 90 | // return r, nil 91 | // }) 92 | func (ctx *ProxyCtx) Warnf(msg string, argv ...interface{}) { 93 | ctx.printf("WARN: "+msg, argv...) 94 | } 95 | 96 | var charsetFinder = regexp.MustCompile("charset=([^ ;]*)") 97 | 98 | // Will try to infer the character set of the request from the headers. 99 | // Returns the empty string if we don't know which character set it used. 100 | // Currently it will look for charset= in the Content-Type header of the request. 101 | func (ctx *ProxyCtx) Charset() string { 102 | charsets := charsetFinder.FindStringSubmatch(ctx.Resp.Header.Get("Content-Type")) 103 | if charsets == nil { 104 | return "" 105 | } 106 | return charsets[1] 107 | } 108 | 109 | // AddBandwidth set ctx Inbounce and Outbounce 110 | func (ctx *ProxyCtx) AddBandwidth(written int64, isInbounce bool) { 111 | if isInbounce { 112 | atomic.AddInt64(&ctx.Inbounce, written) 113 | return 114 | } 115 | atomic.AddInt64(&ctx.Outbounce, written) 116 | } 117 | -------------------------------------------------------------------------------- /regretable/regretreader_test.go: -------------------------------------------------------------------------------- 1 | package regretable_test 2 | 3 | import ( 4 | "bytes" 5 | . "github.com/heixiaoma/goproxy/regretable" 6 | "io" 7 | "io/ioutil" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestRegretableReader(t *testing.T) { 13 | buf := new(bytes.Buffer) 14 | mb := NewRegretableReader(buf) 15 | word := "12345678" 16 | buf.WriteString(word) 17 | 18 | fivebytes := make([]byte, 5) 19 | mb.Read(fivebytes) 20 | mb.Regret() 21 | 22 | s, _ := ioutil.ReadAll(mb) 23 | if string(s) != word { 24 | t.Errorf("Uncommited read is gone, [%d,%d] actual '%v' expected '%v'\n", len(s), len(word), string(s), word) 25 | } 26 | } 27 | 28 | func TestRegretableEmptyRead(t *testing.T) { 29 | buf := new(bytes.Buffer) 30 | mb := NewRegretableReader(buf) 31 | word := "12345678" 32 | buf.WriteString(word) 33 | 34 | zero := make([]byte, 0) 35 | mb.Read(zero) 36 | mb.Regret() 37 | 38 | s, err := ioutil.ReadAll(mb) 39 | if string(s) != word { 40 | t.Error("Uncommited read is gone, actual:", string(s), "expected:", word, "err:", err) 41 | } 42 | } 43 | 44 | func TestRegretableAlsoEmptyRead(t *testing.T) { 45 | buf := new(bytes.Buffer) 46 | mb := NewRegretableReader(buf) 47 | word := "12345678" 48 | buf.WriteString(word) 49 | 50 | one := make([]byte, 1) 51 | zero := make([]byte, 0) 52 | five := make([]byte, 5) 53 | mb.Read(one) 54 | mb.Read(zero) 55 | mb.Read(five) 56 | mb.Regret() 57 | 58 | s, _ := ioutil.ReadAll(mb) 59 | if string(s) != word { 60 | t.Error("Uncommited read is gone", string(s), "expected", word) 61 | } 62 | } 63 | 64 | func TestRegretableRegretBeforeRead(t *testing.T) { 65 | buf := new(bytes.Buffer) 66 | mb := NewRegretableReader(buf) 67 | word := "12345678" 68 | buf.WriteString(word) 69 | 70 | five := make([]byte, 5) 71 | mb.Regret() 72 | mb.Read(five) 73 | 74 | s, err := ioutil.ReadAll(mb) 75 | if string(s) != "678" { 76 | t.Error("Uncommited read is gone", string(s), len(string(s)), "expected", "678", len("678"), "err:", err) 77 | } 78 | } 79 | 80 | func TestRegretableFullRead(t *testing.T) { 81 | buf := new(bytes.Buffer) 82 | mb := NewRegretableReader(buf) 83 | word := "12345678" 84 | buf.WriteString(word) 85 | 86 | twenty := make([]byte, 20) 87 | mb.Read(twenty) 88 | mb.Regret() 89 | 90 | s, _ := ioutil.ReadAll(mb) 91 | if string(s) != word { 92 | t.Error("Uncommited read is gone", string(s), len(string(s)), "expected", word, len(word)) 93 | } 94 | } 95 | 96 | func assertEqual(t *testing.T, expected, actual string) { 97 | if expected != actual { 98 | t.Fatal("Expected", expected, "actual", actual) 99 | } 100 | } 101 | 102 | func assertReadAll(t *testing.T, r io.Reader) string { 103 | s, err := ioutil.ReadAll(r) 104 | if err != nil { 105 | t.Fatal("error when reading", err) 106 | } 107 | return string(s) 108 | } 109 | 110 | func TestRegretableRegretTwice(t *testing.T) { 111 | buf := new(bytes.Buffer) 112 | mb := NewRegretableReader(buf) 113 | word := "12345678" 114 | buf.WriteString(word) 115 | 116 | assertEqual(t, word, assertReadAll(t, mb)) 117 | mb.Regret() 118 | assertEqual(t, word, assertReadAll(t, mb)) 119 | mb.Regret() 120 | assertEqual(t, word, assertReadAll(t, mb)) 121 | } 122 | 123 | type CloseCounter struct { 124 | r io.Reader 125 | closed int 126 | } 127 | 128 | func (cc *CloseCounter) Read(b []byte) (int, error) { 129 | return cc.r.Read(b) 130 | } 131 | 132 | func (cc *CloseCounter) Close() error { 133 | cc.closed++ 134 | return nil 135 | } 136 | 137 | func assert(t *testing.T, b bool, msg string) { 138 | if !b { 139 | t.Errorf("Assertion Error: %s", msg) 140 | } 141 | } 142 | 143 | func TestRegretableCloserSizeRegrets(t *testing.T) { 144 | defer func() { 145 | if r := recover(); r == nil || !strings.Contains(r.(string), "regret") { 146 | t.Error("Did not panic when regretting overread buffer:", r) 147 | } 148 | }() 149 | buf := new(bytes.Buffer) 150 | buf.WriteString("123456") 151 | mb := NewRegretableReaderCloserSize(ioutil.NopCloser(buf), 3) 152 | mb.Read(make([]byte, 4)) 153 | mb.Regret() 154 | } 155 | 156 | func TestRegretableCloserRegretsClose(t *testing.T) { 157 | buf := new(bytes.Buffer) 158 | cc := &CloseCounter{buf, 0} 159 | mb := NewRegretableReaderCloser(cc) 160 | word := "12345678" 161 | buf.WriteString(word) 162 | 163 | mb.Read([]byte{0}) 164 | mb.Close() 165 | if cc.closed != 1 { 166 | t.Error("RegretableReaderCloser ignores Close") 167 | } 168 | mb.Regret() 169 | mb.Close() 170 | if cc.closed != 2 { 171 | t.Error("RegretableReaderCloser does ignore Close after regret") 172 | } 173 | // TODO(elazar): return an error if client issues Close more than once after regret 174 | } 175 | -------------------------------------------------------------------------------- /examples/goproxy-transparent/transparent.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "flag" 7 | "fmt" 8 | "log" 9 | "net" 10 | "net/http" 11 | "net/url" 12 | "regexp" 13 | 14 | "github.com/heixiaoma/goproxy" 15 | "github.com/inconshreveable/go-vhost" 16 | ) 17 | 18 | func orPanic(err error) { 19 | if err != nil { 20 | panic(err) 21 | } 22 | } 23 | 24 | func main() { 25 | verbose := flag.Bool("v", true, "should every proxy request be logged to stdout") 26 | http_addr := flag.String("httpaddr", ":3129", "proxy http listen address") 27 | https_addr := flag.String("httpsaddr", ":3128", "proxy https listen address") 28 | flag.Parse() 29 | 30 | proxy := goproxy.NewProxyHttpServer() 31 | proxy.Verbose = *verbose 32 | if proxy.Verbose { 33 | log.Printf("Server starting up! - configured to listen on http interface %s and https interface %s", *http_addr, *https_addr) 34 | } 35 | 36 | proxy.NonproxyHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 37 | if req.Host == "" { 38 | fmt.Fprintln(w, "Cannot handle requests without Host header, e.g., HTTP 1.0") 39 | return 40 | } 41 | req.URL.Scheme = "http" 42 | req.URL.Host = req.Host 43 | proxy.ServeHTTP(w, req) 44 | }) 45 | proxy.OnRequest(goproxy.ReqHostMatches(regexp.MustCompile("^.*$"))). 46 | HandleConnect(goproxy.AlwaysMitm) 47 | proxy.OnRequest(goproxy.ReqHostMatches(regexp.MustCompile("^.*:80$"))). 48 | HijackConnect(func(req *http.Request, client net.Conn, ctx *goproxy.ProxyCtx) { 49 | defer func() { 50 | if e := recover(); e != nil { 51 | ctx.Logf("error connecting to remote: %v", e) 52 | client.Write([]byte("HTTP/1.1 500 Cannot reach destination\r\n\r\n")) 53 | } 54 | client.Close() 55 | }() 56 | clientBuf := bufio.NewReadWriter(bufio.NewReader(client), bufio.NewWriter(client)) 57 | remote, err := connectDial(proxy, "tcp", req.URL.Host) 58 | orPanic(err) 59 | remoteBuf := bufio.NewReadWriter(bufio.NewReader(remote), bufio.NewWriter(remote)) 60 | for { 61 | req, err := http.ReadRequest(clientBuf.Reader) 62 | orPanic(err) 63 | orPanic(req.Write(remoteBuf)) 64 | orPanic(remoteBuf.Flush()) 65 | resp, err := http.ReadResponse(remoteBuf.Reader, req) 66 | orPanic(err) 67 | orPanic(resp.Write(clientBuf.Writer)) 68 | orPanic(clientBuf.Flush()) 69 | } 70 | }) 71 | 72 | go func() { 73 | log.Fatalln(http.ListenAndServe(*http_addr, proxy)) 74 | }() 75 | 76 | // listen to the TLS ClientHello but make it a CONNECT request instead 77 | ln, err := net.Listen("tcp", *https_addr) 78 | if err != nil { 79 | log.Fatalf("Error listening for https connections - %v", err) 80 | } 81 | for { 82 | c, err := ln.Accept() 83 | if err != nil { 84 | log.Printf("Error accepting new connection - %v", err) 85 | continue 86 | } 87 | go func(c net.Conn) { 88 | tlsConn, err := vhost.TLS(c) 89 | if err != nil { 90 | log.Printf("Error accepting new connection - %v", err) 91 | } 92 | if tlsConn.Host() == "" { 93 | log.Printf("Cannot support non-SNI enabled clients") 94 | return 95 | } 96 | connectReq := &http.Request{ 97 | Method: "CONNECT", 98 | URL: &url.URL{ 99 | Opaque: tlsConn.Host(), 100 | Host: net.JoinHostPort(tlsConn.Host(), "443"), 101 | }, 102 | Host: tlsConn.Host(), 103 | Header: make(http.Header), 104 | } 105 | resp := dumbResponseWriter{tlsConn} 106 | proxy.ServeHTTP(resp, connectReq) 107 | }(c) 108 | } 109 | } 110 | 111 | // copied/converted from https.go 112 | func dial(proxy *goproxy.ProxyHttpServer, network, addr string) (c net.Conn, err error) { 113 | if proxy.Tr.Dial != nil { 114 | return proxy.Tr.Dial(network, addr) 115 | } 116 | return net.Dial(network, addr) 117 | } 118 | 119 | // copied/converted from https.go 120 | func connectDial(proxy *goproxy.ProxyHttpServer, network, addr string) (c net.Conn, err error) { 121 | if proxy.ConnectDial == nil { 122 | return dial(proxy, network, addr) 123 | } 124 | return proxy.ConnectDial(network, addr) 125 | } 126 | 127 | type dumbResponseWriter struct { 128 | net.Conn 129 | } 130 | 131 | func (dumb dumbResponseWriter) Header() http.Header { 132 | panic("Header() should not be called on this ResponseWriter") 133 | } 134 | 135 | func (dumb dumbResponseWriter) Write(buf []byte) (int, error) { 136 | if bytes.Equal(buf, []byte("HTTP/1.0 200 OK\r\n\r\n")) { 137 | return len(buf), nil // throw away the HTTP OK response from the faux CONNECT request 138 | } 139 | return dumb.Conn.Write(buf) 140 | } 141 | 142 | func (dumb dumbResponseWriter) WriteHeader(code int) { 143 | panic("WriteHeader() should not be called on this ResponseWriter") 144 | } 145 | 146 | func (dumb dumbResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 147 | return dumb, bufio.NewReadWriter(bufio.NewReader(dumb), bufio.NewWriter(dumb)), nil 148 | } 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | [![GoDoc](https://godoc.org/github.com/elazarl/goproxy?status.svg)](https://godoc.org/github.com/elazarl/goproxy) 4 | [![Join the chat at https://gitter.im/elazarl/goproxy](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/elazarl/goproxy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | 6 | Package goproxy provides a customizable HTTP proxy library for Go (golang), 7 | 8 | It supports regular HTTP proxy, HTTPS through CONNECT, and "hijacking" HTTPS 9 | connection using "Man in the Middle" style attack. 10 | 11 | The intent of the proxy, is to be usable with reasonable amount of traffic 12 | yet, customizable and programmable. 13 | 14 | The proxy itself is simply a `net/http` handler. 15 | 16 | In order to use goproxy, one should set their browser to use goproxy as an HTTP 17 | proxy. Here is how you do that [in Chrome](https://support.google.com/chrome/answer/96815?hl=en) 18 | and [in Firefox](http://www.wikihow.com/Enter-Proxy-Settings-in-Firefox). 19 | 20 | For example, the URL you should use as proxy when running `./bin/basic` is 21 | `localhost:8080`, as this is the default binding for the basic proxy. 22 | 23 | ## Mailing List 24 | 25 | New features would be discussed on the [mailing list](https://groups.google.com/forum/#!forum/goproxy-dev) 26 | before their development. 27 | 28 | ## Latest Stable Release 29 | 30 | Get the latest goproxy from `gopkg.in/elazarl/goproxy.v1`. 31 | 32 | # Why not Fiddler2? 33 | 34 | Fiddler is an excellent software with similar intent. However, Fiddler is not 35 | as customable as goproxy intend to be. The main difference is, Fiddler is not 36 | intended to be used as a real proxy. 37 | 38 | A possible use case that suits goproxy but 39 | not Fiddler, is, gathering statistics on page load times for a certain website over a week. 40 | With goproxy you could ask all your users to set their proxy to a dedicated machine running a 41 | goproxy server. Fiddler is a GUI app not designed to be ran like a server for multiple users. 42 | 43 | # A taste of goproxy 44 | 45 | To get a taste of `goproxy`, a basic HTTP/HTTPS transparent proxy 46 | 47 | ```go 48 | package main 49 | 50 | import ( 51 | "github.com/elazarl/goproxy" 52 | "log" 53 | "net/http" 54 | ) 55 | 56 | func main() { 57 | proxy := goproxy.NewProxyHttpServer() 58 | proxy.Verbose = true 59 | log.Fatal(http.ListenAndServe(":8080", proxy)) 60 | } 61 | ``` 62 | 63 | This line will add `X-GoProxy: yxorPoG-X` header to all requests sent through the proxy 64 | 65 | ```go 66 | proxy.OnRequest().DoFunc( 67 | func(r *http.Request,ctx *goproxy.ProxyCtx)(*http.Request,*http.Response) { 68 | r.Header.Set("X-GoProxy","yxorPoG-X") 69 | return r,nil 70 | }) 71 | ``` 72 | 73 | `DoFunc` will process all incoming requests to the proxy. It will add a header to the request 74 | and return it. The proxy will send the modified request. 75 | 76 | Note that we returned nil value as the response. Have we returned a response, goproxy would 77 | have discarded the request and sent the new response to the client. 78 | 79 | In order to refuse connections to reddit at work time 80 | 81 | ```go 82 | proxy.OnRequest(goproxy.DstHostIs("www.reddit.com")).DoFunc( 83 | func(r *http.Request,ctx *goproxy.ProxyCtx)(*http.Request,*http.Response) { 84 | if h,_,_ := time.Now().Clock(); h >= 8 && h <= 17 { 85 | return r,goproxy.NewResponse(r, 86 | goproxy.ContentTypeText,http.StatusForbidden, 87 | "Don't waste your time!") 88 | } 89 | return r,nil 90 | }) 91 | ``` 92 | 93 | `DstHostIs` returns a `ReqCondition`, that is a function receiving a `Request` and returning a boolean 94 | we will only process requests that matches the condition. `DstHostIs("www.reddit.com")` will return 95 | a `ReqCondition` accepting only requests directed to "www.reddit.com". 96 | 97 | `DoFunc` will receive a function that will preprocess the request. We can change the request, or 98 | return a response. If the time is between 8:00am and 17:00pm, we will neglect the request, and 99 | return a precanned text response saying "do not waste your time". 100 | 101 | See additional examples in the examples directory. 102 | 103 | # What's New 104 | 105 | 1. Ability to `Hijack` CONNECT requests. See 106 | [the eavesdropper example](https://github.com/elazarl/goproxy/blob/master/examples/goproxy-eavesdropper/main.go#L27) 107 | 2. Transparent proxy support for http/https including MITM certificate generation for TLS. See the [transparent example.](https://github.com/elazarl/goproxy/tree/master/examples/goproxy-transparent) 108 | 109 | # License 110 | 111 | I put the software temporarily under the Go-compatible BSD license, 112 | if this prevents someone from using the software, do let me know and I'll consider changing it. 113 | 114 | At any rate, user feedback is very important for me, so I'll be delighted to know if you're using this package. 115 | 116 | # Beta Software 117 | 118 | I've received a positive feedback from a few people who use goproxy in production settings. 119 | I believe it is good enough for usage. 120 | 121 | I'll try to keep reasonable backwards compatibility. In case of a major API change, 122 | I'll change the import path. 123 | -------------------------------------------------------------------------------- /ext/auth/basic_test.go: -------------------------------------------------------------------------------- 1 | package auth_test 2 | 3 | import ( 4 | "encoding/base64" 5 | "io" 6 | "io/ioutil" 7 | "net" 8 | "net/http" 9 | "net/http/httptest" 10 | "net/url" 11 | "os" 12 | "os/exec" 13 | "os/signal" 14 | "sync/atomic" 15 | "testing" 16 | 17 | "github.com/heixiaoma/goproxy" 18 | "github.com/heixiaoma/goproxy/ext/auth" 19 | ) 20 | 21 | type ConstantHanlder string 22 | 23 | func (h ConstantHanlder) ServeHTTP(w http.ResponseWriter, r *http.Request) { 24 | io.WriteString(w, string(h)) 25 | } 26 | 27 | func oneShotProxy(proxy *goproxy.ProxyHttpServer) (client *http.Client, s *httptest.Server) { 28 | s = httptest.NewServer(proxy) 29 | 30 | proxyUrl, _ := url.Parse(s.URL) 31 | tr := &http.Transport{Proxy: http.ProxyURL(proxyUrl)} 32 | client = &http.Client{Transport: tr} 33 | return 34 | } 35 | 36 | func times(n int, s string) string { 37 | r := make([]byte, 0, n*len(s)) 38 | for i := 0; i < n; i++ { 39 | r = append(r, s...) 40 | } 41 | return string(r) 42 | } 43 | 44 | func TestBasicConnectAuthWithCurl(t *testing.T) { 45 | expected := ":c>" 46 | background := httptest.NewTLSServer(ConstantHanlder(expected)) 47 | defer background.Close() 48 | proxy := goproxy.NewProxyHttpServer() 49 | proxy.OnRequest().HandleConnect(auth.BasicConnect("my_realm", func(user, passwd string) bool { 50 | return user == "user" && passwd == "open sesame" 51 | })) 52 | _, proxyserver := oneShotProxy(proxy) 53 | defer proxyserver.Close() 54 | 55 | cmd := exec.Command("curl", 56 | "--silent", "--show-error", "--insecure", 57 | "-x", proxyserver.URL, 58 | "-U", "user:open sesame", 59 | "-p", 60 | "--url", background.URL+"/[1-3]", 61 | ) 62 | out, err := cmd.CombinedOutput() // if curl got error, it'll show up in stderr 63 | if err != nil { 64 | t.Fatal(err, string(out)) 65 | } 66 | finalexpected := times(3, expected) 67 | if string(out) != finalexpected { 68 | t.Error("Expected", finalexpected, "got", string(out)) 69 | } 70 | } 71 | 72 | func TestBasicAuthWithCurl(t *testing.T) { 73 | expected := ":c>" 74 | background := httptest.NewServer(ConstantHanlder(expected)) 75 | defer background.Close() 76 | proxy := goproxy.NewProxyHttpServer() 77 | proxy.OnRequest().Do(auth.Basic("my_realm", func(user, passwd string) bool { 78 | return user == "user" && passwd == "open sesame" 79 | })) 80 | _, proxyserver := oneShotProxy(proxy) 81 | defer proxyserver.Close() 82 | 83 | cmd := exec.Command("curl", 84 | "--silent", "--show-error", 85 | "-x", proxyserver.URL, 86 | "-U", "user:open sesame", 87 | "--url", background.URL+"/[1-3]", 88 | ) 89 | out, err := cmd.CombinedOutput() // if curl got error, it'll show up in stderr 90 | if err != nil { 91 | t.Fatal(err, string(out)) 92 | } 93 | finalexpected := times(3, expected) 94 | if string(out) != finalexpected { 95 | t.Error("Expected", finalexpected, "got", string(out)) 96 | } 97 | } 98 | 99 | func TestBasicAuth(t *testing.T) { 100 | expected := "hello" 101 | background := httptest.NewServer(ConstantHanlder(expected)) 102 | defer background.Close() 103 | proxy := goproxy.NewProxyHttpServer() 104 | proxy.OnRequest().Do(auth.Basic("my_realm", func(user, passwd string) bool { 105 | return user == "user" && passwd == "open sesame" 106 | })) 107 | client, proxyserver := oneShotProxy(proxy) 108 | defer proxyserver.Close() 109 | 110 | // without auth 111 | resp, err := client.Get(background.URL) 112 | if err != nil { 113 | t.Fatal(err) 114 | } 115 | if resp.Header.Get("Proxy-Authenticate") != "Basic realm=my_realm" { 116 | t.Error("Expected Proxy-Authenticate header got", resp.Header.Get("Proxy-Authenticate")) 117 | } 118 | if resp.StatusCode != 407 { 119 | t.Error("Expected status 407 Proxy Authentication Required, got", resp.Status) 120 | } 121 | 122 | // with auth 123 | req, err := http.NewRequest("GET", background.URL, nil) 124 | if err != nil { 125 | t.Fatal(err) 126 | } 127 | req.Header.Set("Proxy-Authorization", 128 | "Basic "+base64.StdEncoding.EncodeToString([]byte("user:open sesame"))) 129 | resp, err = client.Do(req) 130 | if err != nil { 131 | t.Fatal(err) 132 | } 133 | if resp.StatusCode != 200 { 134 | t.Error("Expected status 200 OK, got", resp.Status) 135 | } 136 | msg, err := ioutil.ReadAll(resp.Body) 137 | if err != nil { 138 | t.Fatal(err) 139 | } 140 | if string(msg) != "hello" { 141 | t.Errorf("Expected '%s', actual '%s'", expected, string(msg)) 142 | } 143 | } 144 | 145 | func TestWithBrowser(t *testing.T) { 146 | // an easy way to check if auth works with webserver 147 | // to test, run with 148 | // $ go test -run TestWithBrowser -- server 149 | // configure a browser to use the printed proxy address, use the proxy 150 | // and exit with Ctrl-C. It will throw error if your haven't acutally used the proxy 151 | if os.Args[len(os.Args)-1] != "server" { 152 | return 153 | } 154 | proxy := goproxy.NewProxyHttpServer() 155 | println("proxy localhost port 8082") 156 | access := int32(0) 157 | proxy.OnRequest().Do(auth.Basic("my_realm", func(user, passwd string) bool { 158 | atomic.AddInt32(&access, 1) 159 | return user == "user" && passwd == "1234" 160 | })) 161 | l, err := net.Listen("tcp", "localhost:8082") 162 | if err != nil { 163 | t.Fatal(err) 164 | } 165 | ch := make(chan os.Signal) 166 | signal.Notify(ch, os.Interrupt) 167 | go func() { 168 | <-ch 169 | l.Close() 170 | }() 171 | http.Serve(l, proxy) 172 | if access <= 0 { 173 | t.Error("No one accessed the proxy") 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /certs.go: -------------------------------------------------------------------------------- 1 | package goproxy 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | ) 7 | 8 | func init() { 9 | if goproxyCaErr != nil { 10 | panic("Error parsing builtin CA " + goproxyCaErr.Error()) 11 | } 12 | var err error 13 | if GoproxyCa.Leaf, err = x509.ParseCertificate(GoproxyCa.Certificate[0]); err != nil { 14 | panic("Error parsing builtin CA " + err.Error()) 15 | } 16 | } 17 | 18 | var tlsClientSkipVerify = &tls.Config{InsecureSkipVerify: true} 19 | 20 | var defaultTLSConfig = &tls.Config{ 21 | InsecureSkipVerify: true, 22 | } 23 | 24 | var CA_CERT = []byte(`-----BEGIN CERTIFICATE----- 25 | MIIF9DCCA9ygAwIBAgIJAODqYUwoVjJkMA0GCSqGSIb3DQEBCwUAMIGOMQswCQYD 26 | VQQGEwJJTDEPMA0GA1UECAwGQ2VudGVyMQwwCgYDVQQHDANMb2QxEDAOBgNVBAoM 27 | B0dvUHJveHkxEDAOBgNVBAsMB0dvUHJveHkxGjAYBgNVBAMMEWdvcHJveHkuZ2l0 28 | aHViLmlvMSAwHgYJKoZIhvcNAQkBFhFlbGF6YXJsQGdtYWlsLmNvbTAeFw0xNzA0 29 | MDUyMDAwMTBaFw0zNzAzMzEyMDAwMTBaMIGOMQswCQYDVQQGEwJJTDEPMA0GA1UE 30 | CAwGQ2VudGVyMQwwCgYDVQQHDANMb2QxEDAOBgNVBAoMB0dvUHJveHkxEDAOBgNV 31 | BAsMB0dvUHJveHkxGjAYBgNVBAMMEWdvcHJveHkuZ2l0aHViLmlvMSAwHgYJKoZI 32 | hvcNAQkBFhFlbGF6YXJsQGdtYWlsLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIP 33 | ADCCAgoCggIBAJ4Qy+H6hhoY1s0QRcvIhxrjSHaO/RbaFj3rwqcnpOgFq07gRdI9 34 | 3c0TFKQJHpgv6feLRhEvX/YllFYu4J35lM9ZcYY4qlKFuStcX8Jm8fqpgtmAMBzP 35 | sqtqDi8M9RQGKENzU9IFOnCV7SAeh45scMuI3wz8wrjBcH7zquHkvqUSYZz035t9 36 | V6WTrHyTEvT4w+lFOVN2bA/6DAIxrjBiF6DhoJqnha0SZtDfv77XpwGG3EhA/qoh 37 | hiYrDruYK7zJdESQL44LwzMPupVigqalfv+YHfQjbhT951IVurW2NJgRyBE62dLr 38 | lHYdtT9tCTCrd+KJNMJ+jp9hAjdIu1Br/kifU4F4+4ZLMR9Ueji0GkkPKsYdyMnq 39 | j0p0PogyvP1l4qmboPImMYtaoFuYmMYlebgC9LN10bL91K4+jLt0I1YntEzrqgJo 40 | WsJztYDw543NzSy5W+/cq4XRYgtq1b0RWwuUiswezmMoeyHZ8BQJe2xMjAOllASD 41 | fqa8OK3WABHJpy4zUrnUBiMuPITzD/FuDx4C5IwwlC68gHAZblNqpBZCX0nFCtKj 42 | YOcI2So5HbQ2OC8QF+zGVuduHUSok4hSy2BBfZ1pfvziqBeetWJwFvapGB44nIHh 43 | WKNKvqOxLNIy7e+TGRiWOomrAWM18VSR9LZbBxpJK7PLSzWqYJYTRCZHAgMBAAGj 44 | UzBRMB0GA1UdDgQWBBR4uDD9Y6x7iUoHO+32ioOcw1ICZTAfBgNVHSMEGDAWgBR4 45 | uDD9Y6x7iUoHO+32ioOcw1ICZTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEB 46 | CwUAA4ICAQAaCEupzGGqcdh+L7BzhX7zyd7yzAKUoLxFrxaZY34Xyj3lcx1XoK6F 47 | AqsH2JM25GixgadzhNt92JP7vzoWeHZtLfstrPS638Y1zZi6toy4E49viYjFk5J0 48 | C6ZcFC04VYWWx6z0HwJuAS08tZ37JuFXpJGfXJOjZCQyxse0Lg0tuKLMeXDCk2Y3 49 | Ba0noeuNyHRoWXXPyiUoeApkVCU5gIsyiJSWOjhJ5hpJG06rQNfNYexgKrrraEin 50 | o0jmEMtJMx5TtD83hSnLCnFGBBq5lkE7jgXME1KsbIE3lJZzRX1mQwUK8CJDYxye 51 | i6M/dzSvy0SsPvz8fTAlprXRtWWtJQmxgWENp3Dv+0Pmux/l+ilk7KA4sMXGhsfr 52 | bvTOeWl1/uoFTPYiWR/ww7QEPLq23yDFY04Q7Un0qjIk8ExvaY8lCkXMgc8i7sGY 53 | VfvOYb0zm67EfAQl3TW8Ky5fl5CcxpVCD360Bzi6hwjYixa3qEeBggOixFQBFWft 54 | 8wrkKTHpOQXjn4sDPtet8imm9UYEtzWrFX6T9MFYkBR0/yye0FIh9+YPiTA6WB86 55 | NCNwK5Yl6HuvF97CIH5CdgO+5C7KifUtqTOL8pQKbNwy0S3sNYvB+njGvRpR7pKV 56 | BUnFpB/Atptqr4CUlTXrc5IPLAqAfmwk5IKcwy3EXUbruf9Dwz69YA== 57 | -----END CERTIFICATE-----`) 58 | 59 | var CA_KEY = []byte(`-----BEGIN RSA PRIVATE KEY----- 60 | MIIJKAIBAAKCAgEAnhDL4fqGGhjWzRBFy8iHGuNIdo79FtoWPevCpyek6AWrTuBF 61 | 0j3dzRMUpAkemC/p94tGES9f9iWUVi7gnfmUz1lxhjiqUoW5K1xfwmbx+qmC2YAw 62 | HM+yq2oOLwz1FAYoQ3NT0gU6cJXtIB6Hjmxwy4jfDPzCuMFwfvOq4eS+pRJhnPTf 63 | m31XpZOsfJMS9PjD6UU5U3ZsD/oMAjGuMGIXoOGgmqeFrRJm0N+/vtenAYbcSED+ 64 | qiGGJisOu5grvMl0RJAvjgvDMw+6lWKCpqV+/5gd9CNuFP3nUhW6tbY0mBHIETrZ 65 | 0uuUdh21P20JMKt34ok0wn6On2ECN0i7UGv+SJ9TgXj7hksxH1R6OLQaSQ8qxh3I 66 | yeqPSnQ+iDK8/WXiqZug8iYxi1qgW5iYxiV5uAL0s3XRsv3Urj6Mu3QjVie0TOuq 67 | AmhawnO1gPDnjc3NLLlb79yrhdFiC2rVvRFbC5SKzB7OYyh7IdnwFAl7bEyMA6WU 68 | BIN+prw4rdYAEcmnLjNSudQGIy48hPMP8W4PHgLkjDCULryAcBluU2qkFkJfScUK 69 | 0qNg5wjZKjkdtDY4LxAX7MZW524dRKiTiFLLYEF9nWl+/OKoF561YnAW9qkYHjic 70 | geFYo0q+o7Es0jLt75MZGJY6iasBYzXxVJH0tlsHGkkrs8tLNapglhNEJkcCAwEA 71 | AQKCAgAwSuNvxHHqUUJ3XoxkiXy1u1EtX9x1eeYnvvs2xMb+WJURQTYz2NEGUdkR 72 | kPO2/ZSXHAcpQvcnpi2e8y2PNmy/uQ0VPATVt6NuWweqxncR5W5j82U/uDlXY8y3 73 | lVbfak4s5XRri0tikHvlP06dNgZ0OPok5qi7d+Zd8yZ3Y8LXfjkykiIrSG1Z2jdt 74 | zCWTkNmSUKMGG/1CGFxI41Lb12xuq+C8v4f469Fb6bCUpyCQN9rffHQSGLH6wVb7 75 | +68JO+d49zCATpmx5RFViMZwEcouXxRvvc9pPHXLP3ZPBD8nYu9kTD220mEGgWcZ 76 | 3L9dDlZPcSocbjw295WMvHz2QjhrDrb8gXwdpoRyuyofqgCyNxSnEC5M13SjOxtf 77 | pjGzjTqh0kDlKXg2/eTkd9xIHjVhFYiHIEeITM/lHCfWwBCYxViuuF7pSRPzTe8U 78 | C440b62qZSPMjVoquaMg+qx0n9fKSo6n1FIKHypv3Kue2G0WhDeK6u0U288vQ1t4 79 | Ood3Qa13gZ+9hwDLbM/AoBfVBDlP/tpAwa7AIIU1ZRDNbZr7emFdctx9B6kLINv3 80 | 4PDOGM2xrjOuACSGMq8Zcu7LBz35PpIZtviJOeKNwUd8/xHjWC6W0itgfJb5I1Nm 81 | V6Vj368pGlJx6Se26lvXwyyrc9pSw6jSAwARBeU4YkNWpi4i6QKCAQEA0T7u3P/9 82 | jZJSnDN1o2PXymDrJulE61yguhc/QSmLccEPZe7or06/DmEhhKuCbv+1MswKDeag 83 | /1JdFPGhL2+4G/f/9BK3BJPdcOZSz7K6Ty8AMMBf8AehKTcSBqwkJWcbEvpHpKJ6 84 | eDqn1B6brXTNKMT6fEEXCuZJGPBpNidyLv/xXDcN7kCOo3nGYKfB5OhFpNiL63tw 85 | +LntU56WESZwEqr8Pf80uFvsyXQK3a5q5HhIQtxl6tqQuPlNjsDBvCqj0x72mmaJ 86 | ZVsVWlv7khUrCwAXz7Y8K7mKKBd2ekF5hSbryfJsxFyvEaWUPhnJpTKV85lAS+tt 87 | FQuIp9TvKYlRQwKCAQEAwWJN8jysapdhi67jO0HtYOEl9wwnF4w6XtiOYtllkMmC 88 | 06/e9h7RsRyWPMdu3qRDPUYFaVDy6+dpUDSQ0+E2Ot6AHtVyvjeUTIL651mFIo/7 89 | OSUCEc+HRo3SfPXdPhSQ2thNTxl6y9XcFacuvbthgr70KXbvC4k6IEmdpf/0Kgs9 90 | 7QTZCG26HDrEZ2q9yMRlRaL2SRD+7Y2xra7gB+cQGFj6yn0Wd/07er49RqMXidQf 91 | KR2oYfev2BDtHXoSZFfhFGHlOdLvWRh90D4qZf4vQ+g/EIMgcNSoxjvph1EShmKt 92 | sjhTHtoHuu+XmEQvIewk2oCI+JvofBkcnpFrVvUUrQKCAQAaTIufETmgCo0BfuJB 93 | N/JOSGIl0NnNryWwXe2gVgVltbsmt6FdL0uKFiEtWJUbOF5g1Q5Kcvs3O/XhBQGa 94 | QbNlKIVt+tAv7hm97+Tmn/MUsraWagdk1sCluns0hXxBizT27KgGhDlaVRz05yfv 95 | 5CdJAYDuDwxDXXBAhy7iFJEgYSDH00+X61tCJrMNQOh4ycy/DEyBu1EWod+3S85W 96 | t3sMjZsIe8P3i+4137Th6eMbdha2+JaCrxfTd9oMoCN5b+6JQXIDM/H+4DTN15PF 97 | 540yY7+aZrAnWrmHknNcqFAKsTqfdi2/fFqwoBwCtiEG91WreU6AfEWIiJuTZIru 98 | sIibAoIBAAqIwlo5t+KukF+9jR9DPh0S5rCIdvCvcNaN0WPNF91FPN0vLWQW1bFi 99 | L0TsUDvMkuUZlV3hTPpQxsnZszH3iK64RB5p3jBCcs+gKu7DT59MXJEGVRCHT4Um 100 | YJryAbVKBYIGWl++sZO8+JotWzx2op8uq7o+glMMjKAJoo7SXIiVyC/LHc95urOi 101 | 9+PySphPKn0anXPpexmRqGYfqpCDo7rPzgmNutWac80B4/CfHb8iUPg6Z1u+1FNe 102 | yKvcZHgW2Wn00znNJcCitufLGyAnMofudND/c5rx2qfBx7zZS7sKUQ/uRYjes6EZ 103 | QBbJUA/2/yLv8YYpaAaqj4aLwV8hRpkCggEBAIh3e25tr3avCdGgtCxS7Y1blQ2c 104 | ue4erZKmFP1u8wTNHQ03T6sECZbnIfEywRD/esHpclfF3kYAKDRqIP4K905Rb0iH 105 | 759ZWt2iCbqZznf50XTvptdmjm5KxvouJzScnQ52gIV6L+QrCKIPelLBEIqCJREh 106 | pmcjjocD/UCCSuHgbAYNNnO/JdhnSylz1tIg26I+2iLNyeTKIepSNlsBxnkLmqM1 107 | cj/azKBaT04IOMLaN8xfSqitJYSraWMVNgGJM5vfcVaivZnNh0lZBv+qu6YkdM88 108 | 4/avCJ8IutT+FcMM+GbGazOm5ALWqUyhrnbLGc4CQMPfe7Il6NxwcrOxT8w= 109 | -----END RSA PRIVATE KEY-----`) 110 | 111 | var GoproxyCa, goproxyCaErr = tls.X509KeyPair(CA_CERT, CA_KEY) 112 | -------------------------------------------------------------------------------- /proxy.go: -------------------------------------------------------------------------------- 1 | package goproxy 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "log" 7 | "net" 8 | "net/http" 9 | "net/http/httputil" 10 | "os" 11 | "regexp" 12 | "sync/atomic" 13 | ) 14 | 15 | // The basic proxy type. Implements http.Handler. 16 | type ProxyHttpServer struct { 17 | // session variable must be aligned in i386 18 | // see http://golang.org/src/pkg/sync/atomic/doc.go#L41 19 | sess int64 20 | // setting Verbose to true will log information on each request sent to the proxy 21 | Verbose bool 22 | Logger *log.Logger 23 | NonproxyHandler http.Handler 24 | reqHandlers []ReqHandler 25 | respHandlers []RespHandler 26 | httpsHandlers []HttpsHandler 27 | doneHandlers []DoneHandler 28 | Tr *http.Transport 29 | // ConnectDial will be used to create TCP connections for CONNECT requests 30 | // if nil Tr.Dial will be used 31 | ConnectDial func(network string, addr string) (net.Conn, error) 32 | } 33 | 34 | var hasPort = regexp.MustCompile(`:\d+$`) 35 | 36 | func copyHeaders(dst, src http.Header) { 37 | for k, _ := range dst { 38 | dst.Del(k) 39 | } 40 | for k, vs := range src { 41 | for _, v := range vs { 42 | dst.Add(k, v) 43 | } 44 | } 45 | } 46 | 47 | func isEof(r *bufio.Reader) bool { 48 | _, err := r.Peek(1) 49 | if err == io.EOF { 50 | return true 51 | } 52 | return false 53 | } 54 | 55 | func (proxy *ProxyHttpServer) filterRequest(r *http.Request, ctx *ProxyCtx) (req *http.Request, resp *http.Response) { 56 | req = r 57 | for _, h := range proxy.reqHandlers { 58 | req, resp = h.Handle(r, ctx) 59 | // non-nil resp means the handler decided to skip sending the request 60 | // and return canned response instead. 61 | if resp != nil { 62 | break 63 | } 64 | } 65 | return 66 | } 67 | func (proxy *ProxyHttpServer) filterResponse(respOrig *http.Response, ctx *ProxyCtx) (resp *http.Response) { 68 | resp = respOrig 69 | for _, h := range proxy.respHandlers { 70 | ctx.Resp = resp 71 | resp = h.Handle(resp, ctx) 72 | } 73 | return 74 | } 75 | func (proxy *ProxyHttpServer) onDone(ctx *ProxyCtx) { 76 | for _, h := range proxy.doneHandlers { 77 | h.Handle(ctx) 78 | } 79 | return 80 | } 81 | 82 | func removeProxyHeaders(ctx *ProxyCtx, r *http.Request) { 83 | r.RequestURI = "" // this must be reset when serving a request with the client 84 | ctx.Logf("Sending request %v %v", r.Method, r.URL.String()) 85 | // If no Accept-Encoding header exists, Transport will add the headers it can accept 86 | // and would wrap the response body with the relevant reader. 87 | r.Header.Del("Accept-Encoding") 88 | // curl can add that, see 89 | // https://jdebp.eu./FGA/web-proxy-connection-header.html 90 | r.Header.Del("Proxy-Connection") 91 | r.Header.Del("Proxy-Authenticate") 92 | r.Header.Del("Proxy-Authorization") 93 | // Connection, Authenticate and Authorization are single hop Header: 94 | // http://www.w3.org/Protocols/rfc2616/rfc2616.txt 95 | // 14.10 Connection 96 | // The Connection general-header field allows the sender to specify 97 | // options that are desired for that particular connection and MUST NOT 98 | // be communicated by proxies over further connections. 99 | r.Header.Del("Connection") 100 | } 101 | 102 | // Standard net/http function. Shouldn't be used directly, http.Serve will use it. 103 | func (proxy *ProxyHttpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { 104 | //r.Header["X-Forwarded-For"] = w.RemoteAddr() 105 | if r.Method == "CONNECT" { 106 | proxy.handleHttps(w, r) 107 | } else { 108 | ctx := &ProxyCtx{Req: r, Session: atomic.AddInt64(&proxy.sess, 1), proxy: proxy} 109 | 110 | var err error 111 | ctx.Logf("Got request %v %v %v %v", r.URL.Path, r.Host, r.Method, r.URL.String()) 112 | if !r.URL.IsAbs() { 113 | proxy.NonproxyHandler.ServeHTTP(w, r) 114 | return 115 | } 116 | r, resp := proxy.filterRequest(r, ctx) 117 | 118 | // bandwidth 119 | var dumpData []byte 120 | if r != nil { 121 | dumpData, _ = httputil.DumpRequest(r, true) 122 | ctx.AddBandwidth(int64(len(dumpData)), true) 123 | } 124 | 125 | if resp == nil { 126 | removeProxyHeaders(ctx, r) 127 | resp, err = ctx.RoundTrip(r) 128 | if err != nil { 129 | ctx.Error = err 130 | resp = proxy.filterResponse(nil, ctx) 131 | if resp == nil { 132 | ctx.Logf("error read response %v %v:", r.URL.Host, err.Error()) 133 | http.Error(w, err.Error(), 500) 134 | 135 | proxy.onDone(ctx) 136 | return 137 | } 138 | } 139 | ctx.Logf("Received response %v", resp.Status) 140 | } 141 | origBody := resp.Body 142 | resp = proxy.filterResponse(resp, ctx) 143 | defer origBody.Close() 144 | ctx.Logf("Copying response to client %v [%d]", resp.Status, resp.StatusCode) 145 | // http.ResponseWriter will take care of filling the correct response length 146 | // Setting it now, might impose wrong value, contradicting the actual new 147 | // body the user returned. 148 | // We keep the original body to remove the header only if things changed. 149 | // This will prevent problems with HEAD requests where there's no body, yet, 150 | // the Content-Length header should be set. 151 | if origBody != resp.Body { 152 | resp.Header.Del("Content-Length") 153 | } 154 | copyHeaders(w.Header(), resp.Header) 155 | w.WriteHeader(resp.StatusCode) 156 | 157 | // bandwidth 158 | dumpData, _ = httputil.DumpResponse(resp, true) 159 | ctx.AddBandwidth(int64(len(dumpData)), false) 160 | 161 | nr, err := io.Copy(w, resp.Body) 162 | if err := resp.Body.Close(); err != nil { 163 | ctx.Warnf("Can't close response body %v", err) 164 | } 165 | ctx.Logf("Copied %v bytes to client error=%v", nr, err) 166 | 167 | proxy.onDone(ctx) 168 | } 169 | } 170 | 171 | // New proxy server, logs to StdErr by default 172 | func NewProxyHttpServer() *ProxyHttpServer { 173 | proxy := ProxyHttpServer{ 174 | Logger: log.New(os.Stderr, "", log.LstdFlags), 175 | reqHandlers: []ReqHandler{}, 176 | respHandlers: []RespHandler{}, 177 | httpsHandlers: []HttpsHandler{}, 178 | NonproxyHandler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 179 | http.Error(w, "This is a proxy server. Does not respond to non-proxy requests.", 500) 180 | }), 181 | Tr: &http.Transport{TLSClientConfig: tlsClientSkipVerify, 182 | Proxy: http.ProxyFromEnvironment}, 183 | } 184 | proxy.ConnectDial = dialerFromEnv(&proxy) 185 | return &proxy 186 | } 187 | -------------------------------------------------------------------------------- /examples/goproxy-httpdump/httpdump.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net" 10 | "net/http" 11 | "net/http/httputil" 12 | "os" 13 | "os/signal" 14 | "path" 15 | "sync" 16 | "time" 17 | 18 | "github.com/heixiaoma/goproxy" 19 | "github.com/heixiaoma/goproxy/transport" 20 | ) 21 | 22 | type FileStream struct { 23 | path string 24 | f *os.File 25 | } 26 | 27 | func NewFileStream(path string) *FileStream { 28 | return &FileStream{path, nil} 29 | } 30 | 31 | func (fs *FileStream) Write(b []byte) (nr int, err error) { 32 | if fs.f == nil { 33 | fs.f, err = os.Create(fs.path) 34 | if err != nil { 35 | return 0, err 36 | } 37 | } 38 | return fs.f.Write(b) 39 | } 40 | 41 | func (fs *FileStream) Close() error { 42 | fmt.Println("Close", fs.path) 43 | if fs.f == nil { 44 | return errors.New("FileStream was never written into") 45 | } 46 | return fs.f.Close() 47 | } 48 | 49 | type Meta struct { 50 | req *http.Request 51 | resp *http.Response 52 | err error 53 | t time.Time 54 | sess int64 55 | bodyPath string 56 | from string 57 | } 58 | 59 | func fprintf(nr *int64, err *error, w io.Writer, pat string, a ...interface{}) { 60 | if *err != nil { 61 | return 62 | } 63 | var n int 64 | n, *err = fmt.Fprintf(w, pat, a...) 65 | *nr += int64(n) 66 | } 67 | 68 | func write(nr *int64, err *error, w io.Writer, b []byte) { 69 | if *err != nil { 70 | return 71 | } 72 | var n int 73 | n, *err = w.Write(b) 74 | *nr += int64(n) 75 | } 76 | 77 | func (m *Meta) WriteTo(w io.Writer) (nr int64, err error) { 78 | if m.req != nil { 79 | fprintf(&nr, &err, w, "Type: request\r\n") 80 | } else if m.resp != nil { 81 | fprintf(&nr, &err, w, "Type: response\r\n") 82 | } 83 | fprintf(&nr, &err, w, "ReceivedAt: %v\r\n", m.t) 84 | fprintf(&nr, &err, w, "Session: %d\r\n", m.sess) 85 | fprintf(&nr, &err, w, "From: %v\r\n", m.from) 86 | if m.err != nil { 87 | // note the empty response 88 | fprintf(&nr, &err, w, "Error: %v\r\n\r\n\r\n\r\n", m.err) 89 | } else if m.req != nil { 90 | fprintf(&nr, &err, w, "\r\n") 91 | buf, err2 := httputil.DumpRequest(m.req, false) 92 | if err2 != nil { 93 | return nr, err2 94 | } 95 | write(&nr, &err, w, buf) 96 | } else if m.resp != nil { 97 | fprintf(&nr, &err, w, "\r\n") 98 | buf, err2 := httputil.DumpResponse(m.resp, false) 99 | if err2 != nil { 100 | return nr, err2 101 | } 102 | write(&nr, &err, w, buf) 103 | } 104 | return 105 | } 106 | 107 | // HttpLogger is an asynchronous HTTP request/response logger. It traces 108 | // requests and responses headers in a "log" file in logger directory and dumps 109 | // their bodies in files prefixed with the session identifiers. 110 | // Close it to ensure pending items are correctly logged. 111 | type HttpLogger struct { 112 | path string 113 | c chan *Meta 114 | errch chan error 115 | } 116 | 117 | func NewLogger(basepath string) (*HttpLogger, error) { 118 | f, err := os.Create(path.Join(basepath, "log")) 119 | if err != nil { 120 | return nil, err 121 | } 122 | logger := &HttpLogger{basepath, make(chan *Meta), make(chan error)} 123 | go func() { 124 | for m := range logger.c { 125 | if _, err := m.WriteTo(f); err != nil { 126 | log.Println("Can't write meta", err) 127 | } 128 | } 129 | logger.errch <- f.Close() 130 | }() 131 | return logger, nil 132 | } 133 | 134 | func (logger *HttpLogger) LogResp(resp *http.Response, ctx *goproxy.ProxyCtx) { 135 | body := path.Join(logger.path, fmt.Sprintf("%d_resp", ctx.Session)) 136 | from := "" 137 | if ctx.UserData != nil { 138 | from = ctx.UserData.(*transport.RoundTripDetails).TCPAddr.String() 139 | } 140 | if resp == nil { 141 | resp = emptyResp 142 | } else { 143 | resp.Body = NewTeeReadCloser(resp.Body, NewFileStream(body)) 144 | } 145 | logger.LogMeta(&Meta{ 146 | resp: resp, 147 | err: ctx.Error, 148 | t: time.Now(), 149 | sess: ctx.Session, 150 | from: from}) 151 | } 152 | 153 | var emptyResp = &http.Response{} 154 | var emptyReq = &http.Request{} 155 | 156 | func (logger *HttpLogger) LogReq(req *http.Request, ctx *goproxy.ProxyCtx) { 157 | body := path.Join(logger.path, fmt.Sprintf("%d_req", ctx.Session)) 158 | if req == nil { 159 | req = emptyReq 160 | } else { 161 | req.Body = NewTeeReadCloser(req.Body, NewFileStream(body)) 162 | } 163 | logger.LogMeta(&Meta{ 164 | req: req, 165 | err: ctx.Error, 166 | t: time.Now(), 167 | sess: ctx.Session, 168 | from: req.RemoteAddr}) 169 | } 170 | 171 | func (logger *HttpLogger) LogMeta(m *Meta) { 172 | logger.c <- m 173 | } 174 | 175 | func (logger *HttpLogger) Close() error { 176 | close(logger.c) 177 | return <-logger.errch 178 | } 179 | 180 | // TeeReadCloser extends io.TeeReader by allowing reader and writer to be 181 | // closed. 182 | type TeeReadCloser struct { 183 | r io.Reader 184 | w io.WriteCloser 185 | c io.Closer 186 | } 187 | 188 | func NewTeeReadCloser(r io.ReadCloser, w io.WriteCloser) io.ReadCloser { 189 | return &TeeReadCloser{io.TeeReader(r, w), w, r} 190 | } 191 | 192 | func (t *TeeReadCloser) Read(b []byte) (int, error) { 193 | return t.r.Read(b) 194 | } 195 | 196 | // Close attempts to close the reader and write. It returns an error if both 197 | // failed to Close. 198 | func (t *TeeReadCloser) Close() error { 199 | err1 := t.c.Close() 200 | err2 := t.w.Close() 201 | if err1 != nil { 202 | return err1 203 | } 204 | return err2 205 | } 206 | 207 | // stoppableListener serves stoppableConn and tracks their lifetime to notify 208 | // when it is safe to terminate the application. 209 | type stoppableListener struct { 210 | net.Listener 211 | sync.WaitGroup 212 | } 213 | 214 | type stoppableConn struct { 215 | net.Conn 216 | wg *sync.WaitGroup 217 | } 218 | 219 | func newStoppableListener(l net.Listener) *stoppableListener { 220 | return &stoppableListener{l, sync.WaitGroup{}} 221 | } 222 | 223 | func (sl *stoppableListener) Accept() (net.Conn, error) { 224 | c, err := sl.Listener.Accept() 225 | if err != nil { 226 | return c, err 227 | } 228 | sl.Add(1) 229 | return &stoppableConn{c, &sl.WaitGroup}, nil 230 | } 231 | 232 | func (sc *stoppableConn) Close() error { 233 | sc.wg.Done() 234 | return sc.Conn.Close() 235 | } 236 | 237 | func main() { 238 | verbose := flag.Bool("v", false, "should every proxy request be logged to stdout") 239 | addr := flag.String("l", ":8080", "on which address should the proxy listen") 240 | flag.Parse() 241 | proxy := goproxy.NewProxyHttpServer() 242 | proxy.Verbose = *verbose 243 | if err := os.MkdirAll("db", 0755); err != nil { 244 | log.Fatal("Can't create dir", err) 245 | } 246 | logger, err := NewLogger("db") 247 | if err != nil { 248 | log.Fatal("can't open log file", err) 249 | } 250 | tr := transport.Transport{Proxy: transport.ProxyFromEnvironment} 251 | // For every incoming request, override the RoundTripper to extract 252 | // connection information. Store it is session context log it after 253 | // handling the response. 254 | proxy.OnRequest().DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { 255 | ctx.RoundTripper = goproxy.RoundTripperFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (resp *http.Response, err error) { 256 | ctx.UserData, resp, err = tr.DetailedRoundTrip(req) 257 | return 258 | }) 259 | logger.LogReq(req, ctx) 260 | return req, nil 261 | }) 262 | proxy.OnResponse().DoFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response { 263 | logger.LogResp(resp, ctx) 264 | return resp 265 | }) 266 | l, err := net.Listen("tcp", *addr) 267 | if err != nil { 268 | log.Fatal("listen:", err) 269 | } 270 | sl := newStoppableListener(l) 271 | ch := make(chan os.Signal) 272 | signal.Notify(ch, os.Interrupt) 273 | go func() { 274 | <-ch 275 | log.Println("Got SIGINT exiting") 276 | sl.Add(1) 277 | sl.Close() 278 | logger.Close() 279 | sl.Done() 280 | }() 281 | log.Println("Starting Proxy") 282 | http.Serve(sl, proxy) 283 | sl.Wait() 284 | log.Println("All connections closed - exit") 285 | } 286 | -------------------------------------------------------------------------------- /examples/goproxy-jquery-version/jquery_homepage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | jQuery: The Write Less, Do More, JavaScript Library 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 18 | 19 |
20 | 31 |
32 | 33 |
34 | 42 |
43 | 44 | 45 | 46 |
47 | 48 |
49 | 50 |
51 |

jQuery is a new kind of JavaScript Library.

52 |

jQuery is a fast and concise JavaScript Library that simplifies HTML document traversing, event handling, animating, and Ajax interactions for rapid web development. jQuery is designed to change the way that you write JavaScript.

53 |
    54 |
  • Lightweight Footprint 55 |
    56 | 57 |

    About 31KB in size (Minified and Gzipped)

    58 |
    59 |
  • 60 |
  • CSS3 Compliant 61 |
    62 |

    Supports CSS 1-3 selectors and more!

    63 |
    64 | 65 |
  • 66 |
  • Cross-browser 67 |
    68 |

    IE 6.0+, FF 3.6+, Safari 5.0+, Opera, Chrome

    69 |
    70 |
  • 71 |
72 |
73 | 74 |
75 |

Grab the latest version!

76 |
77 |
78 | Choose your compression level: 79 |
80 | 81 | jquery-1.7.2.min.js 82 | 83 | 84 | jquery-1.7.2.js 85 | 86 |
87 | 88 |

Current Release: v1.7.2

89 |
90 |
91 | 97 |
98 | 99 |
100 |

Who's using jQuery?

101 | 115 |
116 | 117 | 118 |
119 | 120 |
121 |

Learn jQuery Now!

122 |

What does jQuery code look like? Here's the quick and dirty:

123 |
124 |
$("p.neat").addClass("ohmy").show("slow");
125 | Run Code 126 | 127 |

Congratulations! You just ran a snippet of jQuery code. Wasn't that easy? There's lots of example code throughout the documentation on this site. Be sure to give all the code a test run to see what happens.

128 |
129 |
130 | 131 | 132 | 133 |
134 |

jQuery Resources

135 | 136 |
137 |

Getting Started With jQuery

138 | 145 |
146 |
147 |

Developer Resources

148 | 155 |
156 |
157 | 158 |
159 | 160 |
161 |

Books About jQuery

162 | 163 | 197 | 198 |
199 | 200 | 201 | 202 |
203 | 204 | 205 | 224 |
225 | 226 | 232 | 233 | 234 | -------------------------------------------------------------------------------- /https.go: -------------------------------------------------------------------------------- 1 | package goproxy 2 | 3 | import ( 4 | "bufio" 5 | "crypto/tls" 6 | "errors" 7 | "io" 8 | "io/ioutil" 9 | "net" 10 | "net/http" 11 | "net/url" 12 | "os" 13 | "regexp" 14 | "strconv" 15 | "strings" 16 | "sync" 17 | "sync/atomic" 18 | ) 19 | 20 | type ConnectActionLiteral int 21 | 22 | const ( 23 | ConnectAccept = iota 24 | ConnectReject 25 | ConnectMitm 26 | ConnectHijack 27 | ConnectHTTPMitm 28 | ConnectProxyAuthHijack 29 | ) 30 | 31 | var ( 32 | OkConnect = &ConnectAction{Action: ConnectAccept, TLSConfig: TLSConfigFromCA(&GoproxyCa)} 33 | MitmConnect = &ConnectAction{Action: ConnectMitm, TLSConfig: TLSConfigFromCA(&GoproxyCa)} 34 | HTTPMitmConnect = &ConnectAction{Action: ConnectHTTPMitm, TLSConfig: TLSConfigFromCA(&GoproxyCa)} 35 | RejectConnect = &ConnectAction{Action: ConnectReject, TLSConfig: TLSConfigFromCA(&GoproxyCa)} 36 | httpsRegexp = regexp.MustCompile(`^https:\/\/`) 37 | ) 38 | 39 | type ConnectAction struct { 40 | Action ConnectActionLiteral 41 | Hijack func(req *http.Request, client net.Conn, ctx *ProxyCtx) 42 | TLSConfig func(host string, ctx *ProxyCtx) (*tls.Config, error) 43 | } 44 | 45 | func stripPort(s string) string { 46 | ix := strings.IndexRune(s, ':') 47 | if ix == -1 { 48 | return s 49 | } 50 | return s[:ix] 51 | } 52 | 53 | func (proxy *ProxyHttpServer) dial(network, addr string) (c net.Conn, err error) { 54 | if proxy.Tr.Dial != nil { 55 | return proxy.Tr.Dial(network, addr) 56 | } 57 | return net.Dial(network, addr) 58 | } 59 | 60 | func (proxy *ProxyHttpServer) connectDial(network, addr string) (c net.Conn, err error) { 61 | if proxy.ConnectDial == nil { 62 | return proxy.dial(network, addr) 63 | } 64 | return proxy.ConnectDial(network, addr) 65 | } 66 | 67 | func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request) { 68 | ctx := &ProxyCtx{Req: r, Session: atomic.AddInt64(&proxy.sess, 1), proxy: proxy} 69 | 70 | hij, ok := w.(http.Hijacker) 71 | if !ok { 72 | panic("httpserver does not support hijacking") 73 | } 74 | 75 | proxyClient, _, e := hij.Hijack() 76 | if e != nil { 77 | panic("Cannot hijack connection " + e.Error()) 78 | } 79 | 80 | ctx.Logf("Running %d CONNECT handlers", len(proxy.httpsHandlers)) 81 | todo, host := OkConnect, r.URL.Host 82 | for i, h := range proxy.httpsHandlers { 83 | newtodo, newhost := h.HandleConnect(host, ctx) 84 | 85 | // If found a result, break the loop immediately 86 | if newtodo != nil { 87 | todo, host = newtodo, newhost 88 | ctx.Logf("on %dth handler: %v %s", i, todo, host) 89 | break 90 | } 91 | } 92 | switch todo.Action { 93 | case ConnectAccept: 94 | if !hasPort.MatchString(host) { 95 | host += ":80" 96 | } 97 | targetSiteCon, err := ctx.connectDial("tcp", host) 98 | if err != nil { 99 | httpError(proxyClient, ctx, err) 100 | return 101 | } 102 | ctx.Logf("Accepting CONNECT to %s", host) 103 | proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n")) 104 | 105 | targetTCP, targetOK := targetSiteCon.(*net.TCPConn) 106 | proxyClientTCP, clientOK := proxyClient.(*net.TCPConn) 107 | 108 | // start a goroutine to handle connections 109 | go func() { 110 | if targetOK && clientOK { 111 | var wg sync.WaitGroup 112 | wg.Add(2) 113 | go copyAndClose(ctx, targetTCP, proxyClientTCP, true, &wg) 114 | go copyAndClose(ctx, proxyClientTCP, targetTCP, false, &wg) 115 | wg.Wait() 116 | 117 | proxy.onDone(ctx) 118 | } else { 119 | var wg sync.WaitGroup 120 | wg.Add(2) 121 | go copyOrWarn(ctx, targetSiteCon, proxyClient, true, &wg) 122 | go copyOrWarn(ctx, proxyClient, targetSiteCon, false, &wg) 123 | wg.Wait() 124 | proxyClient.Close() 125 | targetSiteCon.Close() 126 | 127 | proxy.onDone(ctx) 128 | } 129 | }() 130 | 131 | case ConnectHijack: 132 | ctx.Logf("Hijacking CONNECT to %s", host) 133 | proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n")) 134 | todo.Hijack(r, proxyClient, ctx) 135 | case ConnectHTTPMitm: 136 | proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n")) 137 | ctx.Logf("Assuming CONNECT is plain HTTP tunneling, mitm proxying it") 138 | targetSiteCon, err := proxy.connectDial("tcp", host) 139 | if err != nil { 140 | ctx.Warnf("Error dialing to %s: %s", host, err.Error()) 141 | return 142 | } 143 | for { 144 | client := bufio.NewReader(proxyClient) 145 | remote := bufio.NewReader(targetSiteCon) 146 | req, err := http.ReadRequest(client) 147 | if err != nil && err != io.EOF { 148 | ctx.Warnf("cannot read request of MITM HTTP client: %+#v", err) 149 | } 150 | if err != nil { 151 | return 152 | } 153 | req, resp := proxy.filterRequest(req, ctx) 154 | if resp == nil { 155 | if err := req.Write(targetSiteCon); err != nil { 156 | httpError(proxyClient, ctx, err) 157 | return 158 | } 159 | resp, err = http.ReadResponse(remote, req) 160 | if err != nil { 161 | httpError(proxyClient, ctx, err) 162 | return 163 | } 164 | defer resp.Body.Close() 165 | } 166 | resp = proxy.filterResponse(resp, ctx) 167 | if err := resp.Write(proxyClient); err != nil { 168 | httpError(proxyClient, ctx, err) 169 | return 170 | } 171 | } 172 | case ConnectMitm: 173 | proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n")) 174 | ctx.Logf("Assuming CONNECT is TLS, mitm proxying it") 175 | // this goes in a separate goroutine, so that the net/http server won't think we're 176 | // still handling the request even after hijacking the connection. Those HTTP CONNECT 177 | // request can take forever, and the server will be stuck when "closed". 178 | // TODO: Allow Server.Close() mechanism to shut down this connection as nicely as possible 179 | tlsConfig := defaultTLSConfig 180 | if todo.TLSConfig != nil { 181 | var err error 182 | tlsConfig, err = todo.TLSConfig(host, ctx) 183 | if err != nil { 184 | httpError(proxyClient, ctx, err) 185 | return 186 | } 187 | } 188 | go func() { 189 | //TODO: cache connections to the remote website 190 | rawClientTls := tls.Server(proxyClient, tlsConfig) 191 | if err := rawClientTls.Handshake(); err != nil { 192 | ctx.Warnf("Cannot handshake client %v %v", r.Host, err) 193 | return 194 | } 195 | defer rawClientTls.Close() 196 | clientTlsReader := bufio.NewReader(rawClientTls) 197 | for !isEof(clientTlsReader) { 198 | req, err := http.ReadRequest(clientTlsReader) 199 | var ctx = &ProxyCtx{Req: req, Session: atomic.AddInt64(&proxy.sess, 1), proxy: proxy} 200 | if err != nil && err != io.EOF { 201 | return 202 | } 203 | if err != nil { 204 | ctx.Warnf("Cannot read TLS request from mitm'd client %v %v", r.Host, err) 205 | return 206 | } 207 | req.RemoteAddr = r.RemoteAddr // since we're converting the request, need to carry over the original connecting IP as well 208 | ctx.Logf("req %v", r.Host) 209 | 210 | if !httpsRegexp.MatchString(req.URL.String()) { 211 | req.URL, err = url.Parse("https://" + r.Host + req.URL.String()) 212 | } 213 | 214 | // Bug fix which goproxy fails to provide request 215 | // information URL in the context when does HTTPS MITM 216 | ctx.Req = req 217 | 218 | req, resp := proxy.filterRequest(req, ctx) 219 | if resp == nil { 220 | if err != nil { 221 | ctx.Warnf("Illegal URL %s", "https://"+r.Host+req.URL.Path) 222 | return 223 | } 224 | removeProxyHeaders(ctx, req) 225 | resp, err = ctx.RoundTrip(req) 226 | if err != nil { 227 | ctx.Warnf("Cannot read TLS response from mitm'd server %v", err) 228 | return 229 | } 230 | ctx.Logf("resp %v", resp.Status) 231 | } 232 | resp = proxy.filterResponse(resp, ctx) 233 | defer resp.Body.Close() 234 | 235 | text := resp.Status 236 | statusCode := strconv.Itoa(resp.StatusCode) + " " 237 | if strings.HasPrefix(text, statusCode) { 238 | text = text[len(statusCode):] 239 | } 240 | // always use 1.1 to support chunked encoding 241 | if _, err := io.WriteString(rawClientTls, "HTTP/1.1"+" "+statusCode+text+"\r\n"); err != nil { 242 | ctx.Warnf("Cannot write TLS response HTTP status from mitm'd client: %v", err) 243 | return 244 | } 245 | // Since we don't know the length of resp, return chunked encoded response 246 | // TODO: use a more reasonable scheme 247 | resp.Header.Del("Content-Length") 248 | resp.Header.Set("Transfer-Encoding", "chunked") 249 | // Force connection close otherwise chrome will keep CONNECT tunnel open forever 250 | resp.Header.Set("Connection", "close") 251 | if err := resp.Header.Write(rawClientTls); err != nil { 252 | ctx.Warnf("Cannot write TLS response header from mitm'd client: %v", err) 253 | return 254 | } 255 | if _, err = io.WriteString(rawClientTls, "\r\n"); err != nil { 256 | ctx.Warnf("Cannot write TLS response header end from mitm'd client: %v", err) 257 | return 258 | } 259 | chunked := newChunkedWriter(rawClientTls) 260 | if _, err := io.Copy(chunked, resp.Body); err != nil { 261 | ctx.Warnf("Cannot write TLS response body from mitm'd client: %v", err) 262 | return 263 | } 264 | if err := chunked.Close(); err != nil { 265 | ctx.Warnf("Cannot write TLS chunked EOF from mitm'd client: %v", err) 266 | return 267 | } 268 | if _, err = io.WriteString(rawClientTls, "\r\n"); err != nil { 269 | ctx.Warnf("Cannot write TLS response chunked trailer from mitm'd client: %v", err) 270 | return 271 | } 272 | } 273 | ctx.Logf("Exiting on EOF") 274 | }() 275 | case ConnectProxyAuthHijack: 276 | proxyClient.Write([]byte("HTTP/1.1 407 Proxy Authentication Required\r\n")) 277 | todo.Hijack(r, proxyClient, ctx) 278 | case ConnectReject: 279 | if ctx.Resp != nil { 280 | if err := ctx.Resp.Write(proxyClient); err != nil { 281 | ctx.Warnf("Cannot write response that reject http CONNECT: %v", err) 282 | } 283 | } 284 | proxyClient.Close() 285 | } 286 | } 287 | 288 | func httpError(w io.WriteCloser, ctx *ProxyCtx, err error) { 289 | if _, err := io.WriteString(w, "HTTP/1.1 502 Bad Gateway\r\n\r\n"); err != nil { 290 | ctx.Warnf("Error responding to client: %s", err) 291 | } 292 | if err := w.Close(); err != nil { 293 | ctx.Warnf("Error closing client connection: %s", err) 294 | } 295 | } 296 | 297 | func copyOrWarn(ctx *ProxyCtx, dst io.Writer, src io.Reader, isInbounce bool, wg *sync.WaitGroup) { 298 | written, err := io.Copy(dst, src) 299 | if err != nil { 300 | ctx.Warnf("Error copying to client: %s", err) 301 | } 302 | 303 | ctx.AddBandwidth(written, isInbounce) 304 | 305 | wg.Done() 306 | } 307 | 308 | func copyAndClose(ctx *ProxyCtx, dst, src *net.TCPConn, isInbounce bool, wg *sync.WaitGroup) { 309 | written, err := io.Copy(dst, src) 310 | if err != nil { 311 | ctx.Warnf("Error copying to client: %s", err) 312 | } 313 | 314 | ctx.AddBandwidth(written, isInbounce) 315 | 316 | err = dst.CloseWrite() 317 | err = src.CloseRead() 318 | 319 | wg.Done() 320 | } 321 | 322 | func dialerFromEnv(proxy *ProxyHttpServer) func(network, addr string) (net.Conn, error) { 323 | https_proxy := os.Getenv("HTTPS_PROXY") 324 | if https_proxy == "" { 325 | https_proxy = os.Getenv("https_proxy") 326 | } 327 | if https_proxy == "" { 328 | return nil 329 | } 330 | return proxy.NewConnectDialToProxy(https_proxy) 331 | } 332 | 333 | func (proxy *ProxyHttpServer) NewConnectDialToProxy(https_proxy string) func(network, addr string) (net.Conn, error) { 334 | u, err := url.Parse(https_proxy) 335 | if err != nil { 336 | return nil 337 | } 338 | if u.Scheme == "" || u.Scheme == "http" { 339 | if strings.IndexRune(u.Host, ':') == -1 { 340 | u.Host += ":80" 341 | } 342 | return func(network, addr string) (net.Conn, error) { 343 | connectReq := &http.Request{ 344 | Method: "CONNECT", 345 | URL: &url.URL{Opaque: addr}, 346 | Host: addr, 347 | Header: make(http.Header), 348 | } 349 | c, err := proxy.dial(network, u.Host) 350 | if err != nil { 351 | return nil, err 352 | } 353 | connectReq.Write(c) 354 | // Read response. 355 | // Okay to use and discard buffered reader here, because 356 | // TLS server will not speak until spoken to. 357 | br := bufio.NewReader(c) 358 | resp, err := http.ReadResponse(br, connectReq) 359 | if err != nil { 360 | c.Close() 361 | return nil, err 362 | } 363 | defer resp.Body.Close() 364 | if resp.StatusCode != 200 { 365 | resp, err := ioutil.ReadAll(resp.Body) 366 | if err != nil { 367 | return nil, err 368 | } 369 | c.Close() 370 | return nil, errors.New("proxy refused connection" + string(resp)) 371 | } 372 | return c, nil 373 | } 374 | } 375 | if u.Scheme == "https" { 376 | if strings.IndexRune(u.Host, ':') == -1 { 377 | u.Host += ":443" 378 | } 379 | return func(network, addr string) (net.Conn, error) { 380 | c, err := proxy.dial(network, u.Host) 381 | if err != nil { 382 | return nil, err 383 | } 384 | c = tls.Client(c, proxy.Tr.TLSClientConfig) 385 | connectReq := &http.Request{ 386 | Method: "CONNECT", 387 | URL: &url.URL{Opaque: addr}, 388 | Host: addr, 389 | Header: make(http.Header), 390 | } 391 | connectReq.Write(c) 392 | // Read response. 393 | // Okay to use and discard buffered reader here, because 394 | // TLS server will not speak until spoken to. 395 | br := bufio.NewReader(c) 396 | resp, err := http.ReadResponse(br, connectReq) 397 | if err != nil { 398 | c.Close() 399 | return nil, err 400 | } 401 | defer resp.Body.Close() 402 | if resp.StatusCode != 200 { 403 | body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 500)) 404 | if err != nil { 405 | return nil, err 406 | } 407 | c.Close() 408 | return nil, errors.New("proxy refused connection" + string(body)) 409 | } 410 | return c, nil 411 | } 412 | } 413 | return nil 414 | } 415 | 416 | func TLSConfigFromCA(ca *tls.Certificate) func(host string, ctx *ProxyCtx) (*tls.Config, error) { 417 | return func(host string, ctx *ProxyCtx) (*tls.Config, error) { 418 | config := *defaultTLSConfig 419 | ctx.Logf("signing for %s", stripPort(host)) 420 | cert, err := signHost(*ca, []string{stripPort(host)}) 421 | if err != nil { 422 | ctx.Warnf("Cannot sign host certificate with provided CA: %s", err) 423 | return nil, err 424 | } 425 | config.Certificates = append(config.Certificates, cert) 426 | return &config, nil 427 | } 428 | } 429 | -------------------------------------------------------------------------------- /dispatcher.go: -------------------------------------------------------------------------------- 1 | package goproxy 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net" 7 | "net/http" 8 | "regexp" 9 | "strings" 10 | ) 11 | 12 | // ReqCondition.HandleReq will decide whether or not to use the ReqHandler on an HTTP request 13 | // before sending it to the remote server 14 | type ReqCondition interface { 15 | RespCondition 16 | HandleReq(req *http.Request, ctx *ProxyCtx) bool 17 | } 18 | 19 | // RespCondition.HandleReq will decide whether or not to use the RespHandler on an HTTP response 20 | // before sending it to the proxy client. Note that resp might be nil, in case there was an 21 | // error sending the request. 22 | type RespCondition interface { 23 | HandleResp(resp *http.Response, ctx *ProxyCtx) bool 24 | } 25 | 26 | // ReqConditionFunc.HandleReq(req,ctx) <=> ReqConditionFunc(req,ctx) 27 | type ReqConditionFunc func(req *http.Request, ctx *ProxyCtx) bool 28 | 29 | // RespConditionFunc.HandleResp(resp,ctx) <=> RespConditionFunc(resp,ctx) 30 | type RespConditionFunc func(resp *http.Response, ctx *ProxyCtx) bool 31 | 32 | func (c ReqConditionFunc) HandleReq(req *http.Request, ctx *ProxyCtx) bool { 33 | return c(req, ctx) 34 | } 35 | 36 | // ReqConditionFunc cannot test responses. It only satisfies RespCondition interface so that 37 | // to be usable as RespCondition. 38 | func (c ReqConditionFunc) HandleResp(resp *http.Response, ctx *ProxyCtx) bool { 39 | return c(ctx.Req, ctx) 40 | } 41 | 42 | func (c RespConditionFunc) HandleResp(resp *http.Response, ctx *ProxyCtx) bool { 43 | return c(resp, ctx) 44 | } 45 | 46 | // UrlHasPrefix returns a ReqCondition checking wether the destination URL the proxy client has requested 47 | // has the given prefix, with or without the host. 48 | // For example UrlHasPrefix("host/x") will match requests of the form 'GET host/x', and will match 49 | // requests to url 'http://host/x' 50 | func UrlHasPrefix(prefix string) ReqConditionFunc { 51 | return func(req *http.Request, ctx *ProxyCtx) bool { 52 | return strings.HasPrefix(req.URL.Path, prefix) || 53 | strings.HasPrefix(req.URL.Host+req.URL.Path, prefix) || 54 | strings.HasPrefix(req.URL.Scheme+req.URL.Host+req.URL.Path, prefix) 55 | } 56 | } 57 | 58 | // UrlIs returns a ReqCondition, testing whether or not the request URL is one of the given strings 59 | // with or without the host prefix. 60 | // UrlIs("google.com/","foo") will match requests 'GET /' to 'google.com', requests `'GET google.com/' to 61 | // any host, and requests of the form 'GET foo'. 62 | func UrlIs(urls ...string) ReqConditionFunc { 63 | urlSet := make(map[string]bool) 64 | for _, u := range urls { 65 | urlSet[u] = true 66 | } 67 | return func(req *http.Request, ctx *ProxyCtx) bool { 68 | _, pathOk := urlSet[req.URL.Path] 69 | _, hostAndOk := urlSet[req.URL.Host+req.URL.Path] 70 | return pathOk || hostAndOk 71 | } 72 | } 73 | 74 | // ReqHostMatches returns a ReqCondition, testing whether the host to which the request was directed to matches 75 | // any of the given regular expressions. 76 | func ReqHostMatches(regexps ...*regexp.Regexp) ReqConditionFunc { 77 | return func(req *http.Request, ctx *ProxyCtx) bool { 78 | for _, re := range regexps { 79 | if re.MatchString(req.Host) { 80 | return true 81 | } 82 | } 83 | return false 84 | } 85 | } 86 | 87 | // ReqHostIs returns a ReqCondition, testing whether the host to which the request is directed to equal 88 | // to one of the given strings 89 | func ReqHostIs(hosts ...string) ReqConditionFunc { 90 | hostSet := make(map[string]bool) 91 | for _, h := range hosts { 92 | hostSet[h] = true 93 | } 94 | return func(req *http.Request, ctx *ProxyCtx) bool { 95 | _, ok := hostSet[req.URL.Host] 96 | return ok 97 | } 98 | } 99 | 100 | var localHostIpv4 = regexp.MustCompile(`127\.0\.0\.\d+`) 101 | 102 | // IsLocalHost checks whether the destination host is explicitly local host 103 | // (buggy, there can be IPv6 addresses it doesn't catch) 104 | var IsLocalHost ReqConditionFunc = func(req *http.Request, ctx *ProxyCtx) bool { 105 | return req.URL.Host == "::1" || 106 | req.URL.Host == "0:0:0:0:0:0:0:1" || 107 | localHostIpv4.MatchString(req.URL.Host) || 108 | req.URL.Host == "localhost" 109 | } 110 | 111 | // UrlMatches returns a ReqCondition testing whether the destination URL 112 | // of the request matches the given regexp, with or without prefix 113 | func UrlMatches(re *regexp.Regexp) ReqConditionFunc { 114 | return func(req *http.Request, ctx *ProxyCtx) bool { 115 | return re.MatchString(req.URL.Path) || 116 | re.MatchString(req.URL.Host+req.URL.Path) 117 | } 118 | } 119 | 120 | // DstHostIs returns a ReqCondition testing wether the host in the request url is the given string 121 | func DstHostIs(host string) ReqConditionFunc { 122 | return func(req *http.Request, ctx *ProxyCtx) bool { 123 | return req.URL.Host == host 124 | } 125 | } 126 | 127 | // SrcIpIs returns a ReqCondition testing whether the source IP of the request is one of the given strings 128 | func SrcIpIs(ips ...string) ReqCondition { 129 | return ReqConditionFunc(func(req *http.Request, ctx *ProxyCtx) bool { 130 | for _, ip := range ips { 131 | if strings.HasPrefix(req.RemoteAddr, ip+":") { 132 | return true 133 | } 134 | } 135 | return false 136 | }) 137 | } 138 | 139 | // Not returns a ReqCondition negating the given ReqCondition 140 | func Not(r ReqCondition) ReqConditionFunc { 141 | return func(req *http.Request, ctx *ProxyCtx) bool { 142 | return !r.HandleReq(req, ctx) 143 | } 144 | } 145 | 146 | // ContentTypeIs returns a RespCondition testing whether the HTTP response has Content-Type header equal 147 | // to one of the given strings. 148 | func ContentTypeIs(typ string, types ...string) RespCondition { 149 | types = append(types, typ) 150 | return RespConditionFunc(func(resp *http.Response, ctx *ProxyCtx) bool { 151 | if resp == nil { 152 | return false 153 | } 154 | contentType := resp.Header.Get("Content-Type") 155 | for _, typ := range types { 156 | if contentType == typ || strings.HasPrefix(contentType, typ+";") { 157 | return true 158 | } 159 | } 160 | return false 161 | }) 162 | } 163 | 164 | // ProxyHttpServer.OnRequest Will return a temporary ReqProxyConds struct, aggregating the given condtions. 165 | // You will use the ReqProxyConds struct to register a ReqHandler, that would filter 166 | // the request, only if all the given ReqCondition matched. 167 | // Typical usage: 168 | // proxy.OnRequest(UrlIs("example.com/foo"),UrlMatches(regexp.MustParse(`.*\.exampl.\com\./.*`)).Do(...) 169 | func (proxy *ProxyHttpServer) OnRequest(conds ...ReqCondition) *ReqProxyConds { 170 | return &ReqProxyConds{proxy, conds} 171 | } 172 | 173 | // ReqProxyConds aggregate ReqConditions for a ProxyHttpServer. Upon calling Do, it will register a ReqHandler that would 174 | // handle the request if all conditions on the HTTP request are met. 175 | type ReqProxyConds struct { 176 | proxy *ProxyHttpServer 177 | reqConds []ReqCondition 178 | } 179 | 180 | // DoFunc is equivalent to proxy.OnRequest().Do(FuncReqHandler(f)) 181 | func (pcond *ReqProxyConds) DoFunc(f func(req *http.Request, ctx *ProxyCtx) (*http.Request, *http.Response)) { 182 | pcond.Do(FuncReqHandler(f)) 183 | } 184 | 185 | // ReqProxyConds.Do will register the ReqHandler on the proxy, 186 | // the ReqHandler will handle the HTTP request if all the conditions 187 | // aggregated in the ReqProxyConds are met. Typical usage: 188 | // proxy.OnRequest().Do(handler) // will call handler.Handle(req,ctx) on every request to the proxy 189 | // proxy.OnRequest(cond1,cond2).Do(handler) 190 | // // given request to the proxy, will test if cond1.HandleReq(req,ctx) && cond2.HandleReq(req,ctx) are true 191 | // // if they are, will call handler.Handle(req,ctx) 192 | func (pcond *ReqProxyConds) Do(h ReqHandler) { 193 | pcond.proxy.reqHandlers = append(pcond.proxy.reqHandlers, 194 | FuncReqHandler(func(r *http.Request, ctx *ProxyCtx) (*http.Request, *http.Response) { 195 | for _, cond := range pcond.reqConds { 196 | if !cond.HandleReq(r, ctx) { 197 | return r, nil 198 | } 199 | } 200 | return h.Handle(r, ctx) 201 | })) 202 | } 203 | 204 | // HandleConnect is used when proxy receives an HTTP CONNECT request, 205 | // it'll then use the HttpsHandler to determine what should it 206 | // do with this request. The handler returns a ConnectAction struct, the Action field in the ConnectAction 207 | // struct returned will determine what to do with this request. ConnectAccept will simply accept the request 208 | // forwarding all bytes from the client to the remote host, ConnectReject will close the connection with the 209 | // client, and ConnectMitm, will assume the underlying connection is an HTTPS connection, and will use Man 210 | // in the Middle attack to eavesdrop the connection. All regular handler will be active on this eavesdropped 211 | // connection. 212 | // The ConnectAction struct contains possible tlsConfig that will be used for eavesdropping. If nil, the proxy 213 | // will use the default tls configuration. 214 | // proxy.OnRequest().HandleConnect(goproxy.AlwaysReject) // rejects all CONNECT requests 215 | func (pcond *ReqProxyConds) HandleConnect(h HttpsHandler) { 216 | pcond.proxy.httpsHandlers = append(pcond.proxy.httpsHandlers, 217 | FuncHttpsHandler(func(host string, ctx *ProxyCtx) (*ConnectAction, string) { 218 | for _, cond := range pcond.reqConds { 219 | if !cond.HandleReq(ctx.Req, ctx) { 220 | return nil, "" 221 | } 222 | } 223 | return h.HandleConnect(host, ctx) 224 | })) 225 | } 226 | 227 | // HandleConnectFunc is equivalent to HandleConnect, 228 | // for example, accepting CONNECT request if they contain a password in header 229 | // io.WriteString(h,password) 230 | // passHash := h.Sum(nil) 231 | // proxy.OnRequest().HandleConnectFunc(func(host string, ctx *ProxyCtx) (*ConnectAction, string) { 232 | // c := sha1.New() 233 | // io.WriteString(c,ctx.Req.Header.Get("X-GoProxy-Auth")) 234 | // if c.Sum(nil) == passHash { 235 | // return OkConnect, host 236 | // } 237 | // return RejectConnect, host 238 | // }) 239 | func (pcond *ReqProxyConds) HandleConnectFunc(f func(host string, ctx *ProxyCtx) (*ConnectAction, string)) { 240 | pcond.HandleConnect(FuncHttpsHandler(f)) 241 | } 242 | 243 | func (pcond *ReqProxyConds) HijackConnect(f func(req *http.Request, client net.Conn, ctx *ProxyCtx)) { 244 | pcond.proxy.httpsHandlers = append(pcond.proxy.httpsHandlers, 245 | FuncHttpsHandler(func(host string, ctx *ProxyCtx) (*ConnectAction, string) { 246 | for _, cond := range pcond.reqConds { 247 | if !cond.HandleReq(ctx.Req, ctx) { 248 | return nil, "" 249 | } 250 | } 251 | return &ConnectAction{Action: ConnectHijack, Hijack: f}, host 252 | })) 253 | } 254 | 255 | // ProxyConds is used to aggregate RespConditions for a ProxyHttpServer. 256 | // Upon calling ProxyConds.Do, it will register a RespHandler that would 257 | // handle the HTTP response from remote server if all conditions on the HTTP response are met. 258 | type ProxyConds struct { 259 | proxy *ProxyHttpServer 260 | reqConds []ReqCondition 261 | respCond []RespCondition 262 | } 263 | 264 | // ProxyConds.DoFunc is equivalent to proxy.OnResponse().Do(FuncRespHandler(f)) 265 | func (pcond *ProxyConds) DoFunc(f func(resp *http.Response, ctx *ProxyCtx) *http.Response) { 266 | pcond.Do(FuncRespHandler(f)) 267 | } 268 | 269 | // ProxyConds.Do will register the RespHandler on the proxy, h.Handle(resp,ctx) will be called on every 270 | // request that matches the conditions aggregated in pcond. 271 | func (pcond *ProxyConds) Do(h RespHandler) { 272 | pcond.proxy.respHandlers = append(pcond.proxy.respHandlers, 273 | FuncRespHandler(func(resp *http.Response, ctx *ProxyCtx) *http.Response { 274 | for _, cond := range pcond.reqConds { 275 | if !cond.HandleReq(ctx.Req, ctx) { 276 | return resp 277 | } 278 | } 279 | for _, cond := range pcond.respCond { 280 | if !cond.HandleResp(resp, ctx) { 281 | return resp 282 | } 283 | } 284 | return h.Handle(resp, ctx) 285 | })) 286 | } 287 | 288 | // OnResponse is used when adding a response-filter to the HTTP proxy, usual pattern is 289 | // proxy.OnResponse(cond1,cond2).Do(handler) // handler.Handle(resp,ctx) will be used 290 | // // if cond1.HandleResp(resp) && cond2.HandleResp(resp) 291 | func (proxy *ProxyHttpServer) OnResponse(conds ...RespCondition) *ProxyConds { 292 | return &ProxyConds{proxy, make([]ReqCondition, 0), conds} 293 | } 294 | 295 | // AlwaysMitm is a HttpsHandler that always eavesdrop https connections, for example to 296 | // eavesdrop all https connections to www.google.com, we can use 297 | // proxy.OnRequest(goproxy.ReqHostIs("www.google.com")).HandleConnect(goproxy.AlwaysMitm) 298 | var AlwaysMitm FuncHttpsHandler = func(host string, ctx *ProxyCtx) (*ConnectAction, string) { 299 | return MitmConnect, host 300 | } 301 | 302 | // AlwaysReject is a HttpsHandler that drops any CONNECT request, for example, this code will disallow 303 | // connections to hosts on any other port than 443 304 | // proxy.OnRequest(goproxy.Not(goproxy.ReqHostMatches(regexp.MustCompile(":443$"))). 305 | // HandleConnect(goproxy.AlwaysReject) 306 | var AlwaysReject FuncHttpsHandler = func(host string, ctx *ProxyCtx) (*ConnectAction, string) { 307 | return RejectConnect, host 308 | } 309 | 310 | // HandleBytes will return a RespHandler that read the entire body of the request 311 | // to a byte array in memory, would run the user supplied f function on the byte arra, 312 | // and will replace the body of the original response with the resulting byte array. 313 | func HandleBytes(f func(b []byte, ctx *ProxyCtx) []byte) RespHandler { 314 | return FuncRespHandler(func(resp *http.Response, ctx *ProxyCtx) *http.Response { 315 | b, err := ioutil.ReadAll(resp.Body) 316 | if err != nil { 317 | ctx.Warnf("Cannot read response %s", err) 318 | return resp 319 | } 320 | resp.Body.Close() 321 | 322 | resp.Body = ioutil.NopCloser(bytes.NewBuffer(f(b, ctx))) 323 | return resp 324 | }) 325 | } 326 | 327 | // DoneProxyConds has empty conditions now 328 | type DoneProxyConds struct { 329 | proxy *ProxyHttpServer 330 | } 331 | 332 | // OnDone is called when a http request complete including https 333 | func (proxy *ProxyHttpServer) OnDone() *DoneProxyConds { 334 | return &DoneProxyConds{proxy} 335 | } 336 | 337 | // DoFunc is equivalent to proxy.OnDone().Do(FuncDoneHandler(f)) 338 | func (pcond *DoneProxyConds) DoFunc(f func(ctx *ProxyCtx)) { 339 | pcond.Do(FuncDoneHandler(f)) 340 | } 341 | 342 | // DoneProxyConds.Do will register the DoneHandler on the proxy 343 | func (pcond *DoneProxyConds) Do(h DoneHandler) { 344 | pcond.proxy.doneHandlers = append(pcond.proxy.doneHandlers, 345 | FuncDoneHandler(func(ctx *ProxyCtx) { 346 | h.Handle(ctx) 347 | return 348 | })) 349 | } 350 | -------------------------------------------------------------------------------- /examples/goproxy-jquery-version/php_man.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | PHP: PHP Manual - Manual 6 | 11 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 66 | 67 | 68 | 69 | 70 |
71 | 73 |
74 | downloads | 75 | documentation | 76 | faq | 77 | getting help | 78 | mailing lists | 79 | licenses | 80 | wiki | 81 | reporting bugs | 82 | php.net sites | 83 | links | 84 | conferences | 85 | my php.net 86 |
87 |
88 | 89 |
90 |
91 |

92 | 93 | search for 94 | 95 | 96 | in the 97 | 112 | 115 | 116 |

117 |
118 |
119 | 120 |
121 |
122 | 123 | 126 | 127 |
128 |
129 | 130 |
131 | 132 | 133 | 134 |   135 | 136 |
137 | [edit] Last updated: Fri, 23 Mar 2012 138 |
139 |
140 |

view this page in

154 | 155 |
156 |
157 |
158 | 159 | 160 |
161 |

PHP Manual

162 | 163 | 164 | 165 |
166 |
167 | 168 |
by:
169 | 170 | Mehdi Achour 171 | 172 |
173 | 174 | 175 |
176 | 177 | Friedhelm Betz 178 | 179 |
180 | 181 | 182 |
183 | 184 | Antony Dovgal 185 | 186 |
187 | 188 | 189 |
190 | 191 | Nuno Lopes 192 | 193 |
194 | 195 | 196 |
197 | 198 | Hannes Magnusson 199 | 200 |
201 | 202 | 203 |
204 | 205 | Georg Richter 206 | 207 |
208 | 209 | 210 |
211 | 212 | Damien Seguy 213 | 214 |
215 | 216 | 217 |
218 | 219 | Jakub Vrana 220 | 221 |
222 | 223 | 224 | 225 |
226 | 227 | 228 | And several others 229 | 230 | 231 |
232 | 233 |
234 |
2012-03-23
235 | 236 |
237 |
Edited By: 238 | 239 | Philip Olson 240 | 241 |
242 | 243 |
244 | 245 | 249 | 250 | 251 | 252 |
253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 |


280 |
281 |
282 | add a note add a note 283 | User Contributed Notes 284 | PHP Manual 285 |
286 |
There are no user contributed notes for this page.

287 |
288 |
 
289 |
290 | 291 |
292 | show source | 293 | credits | 294 | stats | 295 | sitemap | 296 | contact | 297 | advertising | 298 | mirror sites 299 |
300 | 301 | 313 | 322 | 323 | -------------------------------------------------------------------------------- /transport/transport.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // HTTP client implementation. See RFC 2616. 6 | // 7 | // This is the low-level Transport implementation of RoundTripper. 8 | // The high-level interface is in client.go. 9 | 10 | // This file is DEPRECATED and keep solely for backward compatibility. 11 | 12 | package transport 13 | 14 | import ( 15 | "net/http" 16 | "bufio" 17 | "compress/gzip" 18 | "crypto/tls" 19 | "encoding/base64" 20 | "errors" 21 | "fmt" 22 | "io" 23 | "io/ioutil" 24 | "log" 25 | "net" 26 | "net/url" 27 | "os" 28 | "strings" 29 | "sync" 30 | ) 31 | 32 | // DefaultTransport is the default implementation of Transport and is 33 | // used by DefaultClient. It establishes a new network connection for 34 | // each call to Do and uses HTTP proxies as directed by the 35 | // $HTTP_PROXY and $NO_PROXY (or $http_proxy and $no_proxy) 36 | // environment variables. 37 | var DefaultTransport RoundTripper = &Transport{Proxy: ProxyFromEnvironment} 38 | 39 | // DefaultMaxIdleConnsPerHost is the default value of Transport's 40 | // MaxIdleConnsPerHost. 41 | const DefaultMaxIdleConnsPerHost = 2 42 | 43 | // Transport is an implementation of RoundTripper that supports http, 44 | // https, and http proxies (for either http or https with CONNECT). 45 | // Transport can also cache connections for future re-use. 46 | type Transport struct { 47 | lk sync.Mutex 48 | idleConn map[string][]*persistConn 49 | altProto map[string]RoundTripper // nil or map of URI scheme => RoundTripper 50 | 51 | // TODO: tunable on global max cached connections 52 | // TODO: tunable on timeout on cached connections 53 | // TODO: optional pipelining 54 | 55 | // Proxy specifies a function to return a proxy for a given 56 | // Request. If the function returns a non-nil error, the 57 | // request is aborted with the provided error. 58 | // If Proxy is nil or returns a nil *URL, no proxy is used. 59 | Proxy func(*http.Request) (*url.URL, error) 60 | 61 | // Dial specifies the dial function for creating TCP 62 | // connections. 63 | // If Dial is nil, net.Dial is used. 64 | Dial func(net, addr string) (c net.Conn, err error) 65 | 66 | // TLSClientConfig specifies the TLS configuration to use with 67 | // tls.Client. If nil, the default configuration is used. 68 | TLSClientConfig *tls.Config 69 | 70 | DisableKeepAlives bool 71 | DisableCompression bool 72 | 73 | // MaxIdleConnsPerHost, if non-zero, controls the maximum idle 74 | // (keep-alive) to keep to keep per-host. If zero, 75 | // DefaultMaxIdleConnsPerHost is used. 76 | MaxIdleConnsPerHost int 77 | } 78 | 79 | // ProxyFromEnvironment returns the URL of the proxy to use for a 80 | // given request, as indicated by the environment variables 81 | // $HTTP_PROXY and $NO_PROXY (or $http_proxy and $no_proxy). 82 | // An error is returned if the proxy environment is invalid. 83 | // A nil URL and nil error are returned if no proxy is defined in the 84 | // environment, or a proxy should not be used for the given request. 85 | func ProxyFromEnvironment(req *http.Request) (*url.URL, error) { 86 | proxy := getenvEitherCase("HTTP_PROXY") 87 | if proxy == "" { 88 | return nil, nil 89 | } 90 | if !useProxy(canonicalAddr(req.URL)) { 91 | return nil, nil 92 | } 93 | proxyURL, err := url.Parse(proxy) 94 | if err != nil || proxyURL.Scheme == "" { 95 | if u, err := url.Parse("http://" + proxy); err == nil { 96 | proxyURL = u 97 | err = nil 98 | } 99 | } 100 | if err != nil { 101 | return nil, fmt.Errorf("invalid proxy address %q: %v", proxy, err) 102 | } 103 | return proxyURL, nil 104 | } 105 | 106 | // ProxyURL returns a proxy function (for use in a Transport) 107 | // that always returns the same URL. 108 | func ProxyURL(fixedURL *url.URL) func(*http.Request) (*url.URL, error) { 109 | return func(*http.Request) (*url.URL, error) { 110 | return fixedURL, nil 111 | } 112 | } 113 | 114 | // transportRequest is a wrapper around a *Request that adds 115 | // optional extra headers to write. 116 | type transportRequest struct { 117 | *http.Request // original request, not to be mutated 118 | extra http.Header // extra headers to write, or nil 119 | } 120 | 121 | func (tr *transportRequest) extraHeaders() http.Header { 122 | if tr.extra == nil { 123 | tr.extra = make(http.Header) 124 | } 125 | return tr.extra 126 | } 127 | 128 | type RoundTripDetails struct { 129 | Host string 130 | TCPAddr *net.TCPAddr 131 | IsProxy bool 132 | Error error 133 | } 134 | 135 | func (t *Transport) DetailedRoundTrip(req *http.Request) (details *RoundTripDetails, resp *http.Response, err error) { 136 | if req.URL == nil { 137 | return nil, nil, errors.New("http: nil Request.URL") 138 | } 139 | if req.Header == nil { 140 | return nil, nil, errors.New("http: nil Request.Header") 141 | } 142 | if req.URL.Scheme != "http" && req.URL.Scheme != "https" { 143 | t.lk.Lock() 144 | var rt RoundTripper 145 | if t.altProto != nil { 146 | rt = t.altProto[req.URL.Scheme] 147 | } 148 | t.lk.Unlock() 149 | if rt == nil { 150 | return nil, nil, &badStringError{"unsupported protocol scheme", req.URL.Scheme} 151 | } 152 | return rt.DetailedRoundTrip(req) 153 | } 154 | treq := &transportRequest{Request: req} 155 | cm, err := t.connectMethodForRequest(treq) 156 | if err != nil { 157 | return nil, nil, err 158 | } 159 | 160 | // Get the cached or newly-created connection to either the 161 | // host (for http or https), the http proxy, or the http proxy 162 | // pre-CONNECTed to https server. In any case, we'll be ready 163 | // to send it requests. 164 | pconn, err := t.getConn(cm) 165 | if err != nil { 166 | return nil, nil, err 167 | } 168 | 169 | resp, err = pconn.roundTrip(treq) 170 | return &RoundTripDetails{pconn.host, pconn.ip, pconn.isProxy, err}, resp, err 171 | } 172 | 173 | // RoundTrip implements the RoundTripper interface. 174 | func (t *Transport) RoundTrip(req *http.Request) (resp *http.Response, err error) { 175 | _, resp, err = t.DetailedRoundTrip(req) 176 | return 177 | } 178 | 179 | // RegisterProtocol registers a new protocol with scheme. 180 | // The Transport will pass requests using the given scheme to rt. 181 | // It is rt's responsibility to simulate HTTP request semantics. 182 | // 183 | // RegisterProtocol can be used by other packages to provide 184 | // implementations of protocol schemes like "ftp" or "file". 185 | func (t *Transport) RegisterProtocol(scheme string, rt RoundTripper) { 186 | if scheme == "http" || scheme == "https" { 187 | panic("protocol " + scheme + " already registered") 188 | } 189 | t.lk.Lock() 190 | defer t.lk.Unlock() 191 | if t.altProto == nil { 192 | t.altProto = make(map[string]RoundTripper) 193 | } 194 | if _, exists := t.altProto[scheme]; exists { 195 | panic("protocol " + scheme + " already registered") 196 | } 197 | t.altProto[scheme] = rt 198 | } 199 | 200 | // CloseIdleConnections closes any connections which were previously 201 | // connected from previous requests but are now sitting idle in 202 | // a "keep-alive" state. It does not interrupt any connections currently 203 | // in use. 204 | func (t *Transport) CloseIdleConnections() { 205 | t.lk.Lock() 206 | defer t.lk.Unlock() 207 | if t.idleConn == nil { 208 | return 209 | } 210 | for _, conns := range t.idleConn { 211 | for _, pconn := range conns { 212 | pconn.close() 213 | } 214 | } 215 | t.idleConn = make(map[string][]*persistConn) 216 | } 217 | 218 | // 219 | // Private implementation past this point. 220 | // 221 | 222 | func getenvEitherCase(k string) string { 223 | if v := os.Getenv(strings.ToUpper(k)); v != "" { 224 | return v 225 | } 226 | return os.Getenv(strings.ToLower(k)) 227 | } 228 | 229 | func (t *Transport) connectMethodForRequest(treq *transportRequest) (*connectMethod, error) { 230 | cm := &connectMethod{ 231 | targetScheme: treq.URL.Scheme, 232 | targetAddr: canonicalAddr(treq.URL), 233 | } 234 | if t.Proxy != nil { 235 | var err error 236 | cm.proxyURL, err = t.Proxy(treq.Request) 237 | if err != nil { 238 | return nil, err 239 | } 240 | } 241 | return cm, nil 242 | } 243 | 244 | // proxyAuth returns the Proxy-Authorization header to set 245 | // on requests, if applicable. 246 | func (cm *connectMethod) proxyAuth() string { 247 | if cm.proxyURL == nil { 248 | return "" 249 | } 250 | if u := cm.proxyURL.User; u != nil { 251 | return "Basic " + base64.URLEncoding.EncodeToString([]byte(u.String())) 252 | } 253 | return "" 254 | } 255 | 256 | // putIdleConn adds pconn to the list of idle persistent connections awaiting 257 | // a new request. 258 | // If pconn is no longer needed or not in a good state, putIdleConn 259 | // returns false. 260 | func (t *Transport) putIdleConn(pconn *persistConn) bool { 261 | t.lk.Lock() 262 | defer t.lk.Unlock() 263 | if t.DisableKeepAlives || t.MaxIdleConnsPerHost < 0 { 264 | pconn.close() 265 | return false 266 | } 267 | if pconn.isBroken() { 268 | return false 269 | } 270 | key := pconn.cacheKey 271 | max := t.MaxIdleConnsPerHost 272 | if max == 0 { 273 | max = DefaultMaxIdleConnsPerHost 274 | } 275 | if len(t.idleConn[key]) >= max { 276 | pconn.close() 277 | return false 278 | } 279 | t.idleConn[key] = append(t.idleConn[key], pconn) 280 | return true 281 | } 282 | 283 | func (t *Transport) getIdleConn(cm *connectMethod) (pconn *persistConn) { 284 | t.lk.Lock() 285 | defer t.lk.Unlock() 286 | if t.idleConn == nil { 287 | t.idleConn = make(map[string][]*persistConn) 288 | } 289 | key := cm.String() 290 | for { 291 | pconns, ok := t.idleConn[key] 292 | if !ok { 293 | return nil 294 | } 295 | if len(pconns) == 1 { 296 | pconn = pconns[0] 297 | delete(t.idleConn, key) 298 | } else { 299 | // 2 or more cached connections; pop last 300 | // TODO: queue? 301 | pconn = pconns[len(pconns)-1] 302 | t.idleConn[key] = pconns[0 : len(pconns)-1] 303 | } 304 | if !pconn.isBroken() { 305 | return 306 | } 307 | } 308 | return 309 | } 310 | 311 | func (t *Transport) dial(network, addr string) (c net.Conn, raddr string, ip *net.TCPAddr, err error) { 312 | if t.Dial != nil { 313 | ip, err = net.ResolveTCPAddr("tcp", addr) 314 | if err!=nil { 315 | return 316 | } 317 | c, err = t.Dial(network, addr) 318 | raddr = addr 319 | return 320 | } 321 | addri, err := net.ResolveTCPAddr("tcp", addr) 322 | if err!=nil { 323 | return 324 | } 325 | c, err = net.DialTCP("tcp", nil, addri) 326 | raddr = addr 327 | ip = addri 328 | return 329 | } 330 | 331 | // getConn dials and creates a new persistConn to the target as 332 | // specified in the connectMethod. This includes doing a proxy CONNECT 333 | // and/or setting up TLS. If this doesn't return an error, the persistConn 334 | // is ready to write requests to. 335 | func (t *Transport) getConn(cm *connectMethod) (*persistConn, error) { 336 | if pc := t.getIdleConn(cm); pc != nil { 337 | return pc, nil 338 | } 339 | 340 | conn, raddr, ip, err := t.dial("tcp", cm.addr()) 341 | if err != nil { 342 | if cm.proxyURL != nil { 343 | err = fmt.Errorf("http: error connecting to proxy %s: %v", cm.proxyURL, err) 344 | } 345 | return nil, err 346 | } 347 | 348 | pa := cm.proxyAuth() 349 | 350 | pconn := &persistConn{ 351 | t: t, 352 | cacheKey: cm.String(), 353 | conn: conn, 354 | reqch: make(chan requestAndChan, 50), 355 | host: raddr, 356 | ip: ip, 357 | } 358 | 359 | switch { 360 | case cm.proxyURL == nil: 361 | // Do nothing. 362 | case cm.targetScheme == "http": 363 | pconn.isProxy = true 364 | if pa != "" { 365 | pconn.mutateHeaderFunc = func(h http.Header) { 366 | h.Set("Proxy-Authorization", pa) 367 | } 368 | } 369 | case cm.targetScheme == "https": 370 | connectReq := &http.Request{ 371 | Method: "CONNECT", 372 | URL: &url.URL{Opaque: cm.targetAddr}, 373 | Host: cm.targetAddr, 374 | Header: make(http.Header), 375 | } 376 | if pa != "" { 377 | connectReq.Header.Set("Proxy-Authorization", pa) 378 | } 379 | connectReq.Write(conn) 380 | 381 | // Read response. 382 | // Okay to use and discard buffered reader here, because 383 | // TLS server will not speak until spoken to. 384 | br := bufio.NewReader(conn) 385 | resp, err := http.ReadResponse(br, connectReq) 386 | if err != nil { 387 | conn.Close() 388 | return nil, err 389 | } 390 | if resp.StatusCode != 200 { 391 | f := strings.SplitN(resp.Status, " ", 2) 392 | conn.Close() 393 | return nil, errors.New(f[1]) 394 | } 395 | } 396 | 397 | if cm.targetScheme == "https" { 398 | // Initiate TLS and check remote host name against certificate. 399 | conn = tls.Client(conn, t.TLSClientConfig) 400 | if err = conn.(*tls.Conn).Handshake(); err != nil { 401 | return nil, err 402 | } 403 | if t.TLSClientConfig == nil || !t.TLSClientConfig.InsecureSkipVerify { 404 | if err = conn.(*tls.Conn).VerifyHostname(cm.tlsHost()); err != nil { 405 | return nil, err 406 | } 407 | } 408 | pconn.conn = conn 409 | } 410 | 411 | pconn.br = bufio.NewReader(pconn.conn) 412 | pconn.bw = bufio.NewWriter(pconn.conn) 413 | go pconn.readLoop() 414 | return pconn, nil 415 | } 416 | 417 | // useProxy returns true if requests to addr should use a proxy, 418 | // according to the NO_PROXY or no_proxy environment variable. 419 | // addr is always a canonicalAddr with a host and port. 420 | func useProxy(addr string) bool { 421 | if len(addr) == 0 { 422 | return true 423 | } 424 | host, _, err := net.SplitHostPort(addr) 425 | if err != nil { 426 | return false 427 | } 428 | if host == "localhost" { 429 | return false 430 | } 431 | if ip := net.ParseIP(host); ip != nil { 432 | if ip.IsLoopback() { 433 | return false 434 | } 435 | } 436 | 437 | no_proxy := getenvEitherCase("NO_PROXY") 438 | if no_proxy == "*" { 439 | return false 440 | } 441 | 442 | addr = strings.ToLower(strings.TrimSpace(addr)) 443 | if hasPort(addr) { 444 | addr = addr[:strings.LastIndex(addr, ":")] 445 | } 446 | 447 | for _, p := range strings.Split(no_proxy, ",") { 448 | p = strings.ToLower(strings.TrimSpace(p)) 449 | if len(p) == 0 { 450 | continue 451 | } 452 | if hasPort(p) { 453 | p = p[:strings.LastIndex(p, ":")] 454 | } 455 | if addr == p || (p[0] == '.' && (strings.HasSuffix(addr, p) || addr == p[1:])) { 456 | return false 457 | } 458 | } 459 | return true 460 | } 461 | 462 | // connectMethod is the map key (in its String form) for keeping persistent 463 | // TCP connections alive for subsequent HTTP requests. 464 | // 465 | // A connect method may be of the following types: 466 | // 467 | // Cache key form Description 468 | // ----------------- ------------------------- 469 | // ||http|foo.com http directly to server, no proxy 470 | // ||https|foo.com https directly to server, no proxy 471 | // http://proxy.com|https|foo.com http to proxy, then CONNECT to foo.com 472 | // http://proxy.com|http http to proxy, http to anywhere after that 473 | // 474 | // Note: no support to https to the proxy yet. 475 | // 476 | type connectMethod struct { 477 | proxyURL *url.URL // nil for no proxy, else full proxy URL 478 | targetScheme string // "http" or "https" 479 | targetAddr string // Not used if proxy + http targetScheme (4th example in table) 480 | } 481 | 482 | func (ck *connectMethod) String() string { 483 | proxyStr := "" 484 | if ck.proxyURL != nil { 485 | proxyStr = ck.proxyURL.String() 486 | } 487 | return strings.Join([]string{proxyStr, ck.targetScheme, ck.targetAddr}, "|") 488 | } 489 | 490 | // addr returns the first hop "host:port" to which we need to TCP connect. 491 | func (cm *connectMethod) addr() string { 492 | if cm.proxyURL != nil { 493 | return canonicalAddr(cm.proxyURL) 494 | } 495 | return cm.targetAddr 496 | } 497 | 498 | // tlsHost returns the host name to match against the peer's 499 | // TLS certificate. 500 | func (cm *connectMethod) tlsHost() string { 501 | h := cm.targetAddr 502 | if hasPort(h) { 503 | h = h[:strings.LastIndex(h, ":")] 504 | } 505 | return h 506 | } 507 | 508 | // persistConn wraps a connection, usually a persistent one 509 | // (but may be used for non-keep-alive requests as well) 510 | type persistConn struct { 511 | t *Transport 512 | cacheKey string // its connectMethod.String() 513 | conn net.Conn 514 | br *bufio.Reader // from conn 515 | bw *bufio.Writer // to conn 516 | reqch chan requestAndChan // written by roundTrip(); read by readLoop() 517 | isProxy bool 518 | 519 | // mutateHeaderFunc is an optional func to modify extra 520 | // headers on each outbound request before it's written. (the 521 | // original Request given to RoundTrip is not modified) 522 | mutateHeaderFunc func(http.Header) 523 | 524 | lk sync.Mutex // guards numExpectedResponses and broken 525 | numExpectedResponses int 526 | broken bool // an error has happened on this connection; marked broken so it's not reused. 527 | 528 | host string 529 | ip *net.TCPAddr 530 | } 531 | 532 | func (pc *persistConn) isBroken() bool { 533 | pc.lk.Lock() 534 | defer pc.lk.Unlock() 535 | return pc.broken 536 | } 537 | 538 | var remoteSideClosedFunc func(error) bool // or nil to use default 539 | 540 | func remoteSideClosed(err error) bool { 541 | if err == io.EOF { 542 | return true 543 | } 544 | if remoteSideClosedFunc != nil { 545 | return remoteSideClosedFunc(err) 546 | } 547 | return false 548 | } 549 | 550 | func (pc *persistConn) readLoop() { 551 | alive := true 552 | var lastbody io.ReadCloser // last response body, if any, read on this connection 553 | 554 | for alive { 555 | pb, err := pc.br.Peek(1) 556 | 557 | pc.lk.Lock() 558 | if pc.numExpectedResponses == 0 { 559 | pc.closeLocked() 560 | pc.lk.Unlock() 561 | if len(pb) > 0 { 562 | log.Printf("Unsolicited response received on idle HTTP channel starting with %q; err=%v", 563 | string(pb), err) 564 | } 565 | return 566 | } 567 | pc.lk.Unlock() 568 | 569 | rc := <-pc.reqch 570 | 571 | // Advance past the previous response's body, if the 572 | // caller hasn't done so. 573 | if lastbody != nil { 574 | lastbody.Close() // assumed idempotent 575 | lastbody = nil 576 | } 577 | resp, err := http.ReadResponse(pc.br, rc.req) 578 | 579 | if err != nil { 580 | pc.close() 581 | } else { 582 | hasBody := rc.req.Method != "HEAD" && resp.ContentLength != 0 583 | if rc.addedGzip && hasBody && resp.Header.Get("Content-Encoding") == "gzip" { 584 | resp.Header.Del("Content-Encoding") 585 | resp.Header.Del("Content-Length") 586 | resp.ContentLength = -1 587 | gzReader, zerr := gzip.NewReader(resp.Body) 588 | if zerr != nil { 589 | pc.close() 590 | err = zerr 591 | } else { 592 | resp.Body = &readFirstCloseBoth{&discardOnCloseReadCloser{gzReader}, resp.Body} 593 | } 594 | } 595 | resp.Body = &bodyEOFSignal{body: resp.Body} 596 | } 597 | 598 | if err != nil || resp.Close || rc.req.Close { 599 | alive = false 600 | } 601 | 602 | hasBody := resp != nil && resp.ContentLength != 0 603 | var waitForBodyRead chan bool 604 | if alive { 605 | if hasBody { 606 | lastbody = resp.Body 607 | waitForBodyRead = make(chan bool) 608 | resp.Body.(*bodyEOFSignal).fn = func() { 609 | if !pc.t.putIdleConn(pc) { 610 | alive = false 611 | } 612 | waitForBodyRead <- true 613 | } 614 | } else { 615 | // When there's no response body, we immediately 616 | // reuse the TCP connection (putIdleConn), but 617 | // we need to prevent ClientConn.Read from 618 | // closing the Response.Body on the next 619 | // loop, otherwise it might close the body 620 | // before the client code has had a chance to 621 | // read it (even though it'll just be 0, EOF). 622 | lastbody = nil 623 | 624 | if !pc.t.putIdleConn(pc) { 625 | alive = false 626 | } 627 | } 628 | } 629 | 630 | rc.ch <- responseAndError{resp, err} 631 | 632 | // Wait for the just-returned response body to be fully consumed 633 | // before we race and peek on the underlying bufio reader. 634 | if waitForBodyRead != nil { 635 | <-waitForBodyRead 636 | } 637 | } 638 | } 639 | 640 | type responseAndError struct { 641 | res *http.Response 642 | err error 643 | } 644 | 645 | type requestAndChan struct { 646 | req *http.Request 647 | ch chan responseAndError 648 | 649 | // did the Transport (as opposed to the client code) add an 650 | // Accept-Encoding gzip header? only if it we set it do 651 | // we transparently decode the gzip. 652 | addedGzip bool 653 | } 654 | 655 | func (pc *persistConn) roundTrip(req *transportRequest) (resp *http.Response, err error) { 656 | if pc.mutateHeaderFunc != nil { 657 | panic("mutateHeaderFunc not supported in modified Transport") 658 | pc.mutateHeaderFunc(req.extraHeaders()) 659 | } 660 | 661 | // Ask for a compressed version if the caller didn't set their 662 | // own value for Accept-Encoding. We only attempted to 663 | // uncompress the gzip stream if we were the layer that 664 | // requested it. 665 | requestedGzip := false 666 | if !pc.t.DisableCompression && req.Header.Get("Accept-Encoding") == "" { 667 | // Request gzip only, not deflate. Deflate is ambiguous and 668 | // not as universally supported anyway. 669 | // See: http://www.gzip.org/zlib/zlib_faq.html#faq38 670 | requestedGzip = true 671 | req.extraHeaders().Set("Accept-Encoding", "gzip") 672 | } 673 | 674 | pc.lk.Lock() 675 | pc.numExpectedResponses++ 676 | pc.lk.Unlock() 677 | 678 | // orig: err = req.Request.write(pc.bw, pc.isProxy, req.extra) 679 | if pc.isProxy { 680 | err = req.Request.WriteProxy(pc.bw) 681 | } else { 682 | err = req.Request.Write(pc.bw) 683 | } 684 | if err != nil { 685 | pc.close() 686 | return 687 | } 688 | pc.bw.Flush() 689 | 690 | ch := make(chan responseAndError, 1) 691 | pc.reqch <- requestAndChan{req.Request, ch, requestedGzip} 692 | re := <-ch 693 | pc.lk.Lock() 694 | pc.numExpectedResponses-- 695 | pc.lk.Unlock() 696 | 697 | return re.res, re.err 698 | } 699 | 700 | func (pc *persistConn) close() { 701 | pc.lk.Lock() 702 | defer pc.lk.Unlock() 703 | pc.closeLocked() 704 | } 705 | 706 | func (pc *persistConn) closeLocked() { 707 | pc.broken = true 708 | pc.conn.Close() 709 | pc.mutateHeaderFunc = nil 710 | } 711 | 712 | var portMap = map[string]string{ 713 | "http": "80", 714 | "https": "443", 715 | } 716 | 717 | // canonicalAddr returns url.Host but always with a ":port" suffix 718 | func canonicalAddr(url *url.URL) string { 719 | addr := url.Host 720 | if !hasPort(addr) { 721 | return addr + ":" + portMap[url.Scheme] 722 | } 723 | return addr 724 | } 725 | 726 | func responseIsKeepAlive(res *http.Response) bool { 727 | // TODO: implement. for now just always shutting down the connection. 728 | return false 729 | } 730 | 731 | // bodyEOFSignal wraps a ReadCloser but runs fn (if non-nil) at most 732 | // once, right before the final Read() or Close() call returns, but after 733 | // EOF has been seen. 734 | type bodyEOFSignal struct { 735 | body io.ReadCloser 736 | fn func() 737 | isClosed bool 738 | } 739 | 740 | func (es *bodyEOFSignal) Read(p []byte) (n int, err error) { 741 | n, err = es.body.Read(p) 742 | if es.isClosed && n > 0 { 743 | panic("http: unexpected bodyEOFSignal Read after Close; see issue 1725") 744 | } 745 | if err == io.EOF && es.fn != nil { 746 | es.fn() 747 | es.fn = nil 748 | } 749 | return 750 | } 751 | 752 | func (es *bodyEOFSignal) Close() (err error) { 753 | if es.isClosed { 754 | return nil 755 | } 756 | es.isClosed = true 757 | err = es.body.Close() 758 | if err == nil && es.fn != nil { 759 | es.fn() 760 | es.fn = nil 761 | } 762 | return 763 | } 764 | 765 | type readFirstCloseBoth struct { 766 | io.ReadCloser 767 | io.Closer 768 | } 769 | 770 | func (r *readFirstCloseBoth) Close() error { 771 | if err := r.ReadCloser.Close(); err != nil { 772 | r.Closer.Close() 773 | return err 774 | } 775 | if err := r.Closer.Close(); err != nil { 776 | return err 777 | } 778 | return nil 779 | } 780 | 781 | // discardOnCloseReadCloser consumes all its input on Close. 782 | type discardOnCloseReadCloser struct { 783 | io.ReadCloser 784 | } 785 | 786 | func (d *discardOnCloseReadCloser) Close() error { 787 | io.Copy(ioutil.Discard, d.ReadCloser) // ignore errors; likely invalid or already closed 788 | return d.ReadCloser.Close() 789 | } 790 | --------------------------------------------------------------------------------