Skip to content

Instantly share code, notes, and snippets.

@ayanamist
Last active June 14, 2022 02:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ayanamist/a900832af9c8b95b489875a94a13a842 to your computer and use it in GitHub Desktop.
Save ayanamist/a900832af9c8b95b489875a94a13a842 to your computer and use it in GitHub Desktop.
一个特殊的MITM http proxy,将图片、js、css请求劫持并缓存在本地,后续再访问时直接从本地返回,避免一些垃圾网站打不开的问题
package main
import (
"bytes"
"compress/gzip"
"errors"
"flag"
"io"
"io/ioutil"
"log"
"mime"
"net"
"net/http"
"os"
"path/filepath"
"time"
)
var dialer = &net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}
var tr = &http.Transport{DialContext: dialer.DialContext}
var cacheDir string
func cacheable(r http.Request) bool {
if r.Method != http.MethodGet {
return false
}
switch filepath.Ext(r.URL.Path) {
case ".css", ".js", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".woff", ".woff2", ".ttf", ".eot", ".ico":
return true
default:
return false
}
}
func handleFunc(w http.ResponseWriter, r *http.Request) {
log.Printf("%s: %s %+v", r.RemoteAddr, r.Method, r.URL)
isCacheable := cacheable(*r)
p := r.URL.EscapedPath()
if r.URL.RawQuery != "" {
p += "?" + r.URL.RawQuery
}
cachePath := filepath.Join(cacheDir, r.URL.Host, p)
if isCacheable {
f, err := os.Open(cachePath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
log.Printf("%s: %s: cache miss", r.RemoteAddr, cachePath)
} else {
log.Printf("%s: %s: failed to open cache file: %v", r.RemoteAddr, cachePath, err)
}
} else {
log.Printf("%s: %s: cache hit", r.RemoteAddr, cachePath)
defer f.Close()
if mimeType := mime.TypeByExtension(filepath.Ext(cachePath)); mimeType != "" {
w.Header().Set("Content-Type", mimeType)
}
if _, err = io.Copy(w, f); err != nil {
log.Printf("%s: %s: failed to copy cache file: %v", r.RemoteAddr, cachePath, err)
}
return
}
}
resp, err := tr.RoundTrip(r)
if err != nil {
w.WriteHeader(http.StatusBadGateway)
w.Write([]byte(err.Error()))
return
}
wHeader := w.Header()
for k, v := range resp.Header {
wHeader[k] = v
}
w.WriteHeader(resp.StatusCode)
var dst io.Writer = w
buf := &bytes.Buffer{}
if isCacheable {
dst = io.MultiWriter(w, buf)
}
if _, err = io.Copy(dst, resp.Body); err != nil {
log.Printf("%s: failed to copy response body: %v", r.RemoteAddr, err)
} else if isCacheable {
if err := os.MkdirAll(filepath.Dir(cachePath), 0755); err != nil {
log.Printf("%s: failed to create cache directory: %v", r.RemoteAddr, err)
} else {
if resp.Header.Get("Content-Encoding") == "gzip" {
if reader, err := gzip.NewReader(buf); err != nil {
log.Printf("%s: failed to create gzip reader: %v", r.RemoteAddr, err)
} else {
buf2 := &bytes.Buffer{}
if _, err := io.Copy(buf2, reader); err != nil {
log.Printf("%s: failed to copy gzip reader: %v", r.RemoteAddr, err)
}
buf = buf2
}
}
if err := ioutil.WriteFile(cachePath, buf.Bytes(), 0644); err != nil {
log.Printf("%s: failed to write cache file: %v", r.RemoteAddr, err)
}
}
}
}
func main() {
var listenAddr string
const defaultListen = "localhost:8888"
flag.StringVar(&listenAddr, "listen", defaultListen, "address to listen on, default is "+defaultListen)
cacheDir = filepath.Join(os.TempDir(), "cache")
flag.StringVar(&cacheDir, "cache", cacheDir, "directory to cache responses, default is "+cacheDir)
flag.Parse()
log.Printf("try listen on %s cache on %s", listenAddr, cacheDir)
if err := http.ListenAndServe(listenAddr, http.HandlerFunc(handleFunc)); err != nil {
log.Fatalf("failed to listen and serve: %v", err)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment