123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167 |
- package main
- import (
- "crypto/tls"
- "flag"
- "fmt"
- "io"
- "log"
- "net"
- "net/http"
- "net/http/httputil"
- "net/url"
- "regexp"
- "strings"
- "github.com/gorilla/handlers"
- "github.com/tdewolff/minify"
- "github.com/tdewolff/minify/css"
- "github.com/tdewolff/minify/html"
- "github.com/tdewolff/minify/js"
- "github.com/tdewolff/minify/json"
- "github.com/tdewolff/minify/svg"
- "github.com/tdewolff/minify/xml"
- "golang.org/x/crypto/acme/autocert"
- )
- var (
- // CMD options
- listen = flag.String("listen", ":443", "the local listen address")
- domains = flag.String("domains", "", "a comma separated strings of domain[->[ip]:port]")
- backend = flag.String("backend", ":80", "the default backend to be used")
- sslCacheDir = flag.String("ssl-cache-dir", "./httpsify-ssl-cache", "the cache directory to cache generated ssl certs")
- gzip = flag.Int("gzip", 0, "gzip compression level [0-9]")
- mnfy = flag.Bool("minify", true, "whether to minify the output or not")
- // internal vars
- domain_backend = map[string]string{}
- whitelisted = []string{}
- )
- func main() {
- flag.Parse()
- if *domains == "" {
- flag.Usage()
- fmt.Println(`Example(template): httpsify -domains "example.org,api.example.org->localhost:366, api2.example.org->:367"`)
- fmt.Println(`Example(real-life1): httpsify -domains "www.site.com,apiv1.site.com->:8080,apiv2.site.com->:8081" -minify=true -gzip=9`)
- fmt.Println(`Example(real-life2): httpsify -domains "www.site.com,site.com" -backend=:8080 -minify=true -gzip=0`)
- return
- }
- for _, zone := range strings.Split(*domains, ",") {
- parts := strings.SplitN(zone, "->", 2)
- if len(parts) < 2 {
- parts = append(parts, *backend)
- }
- parts[1] = fixUrl(parts[1])
- domain_backend[parts[0]] = parts[1]
- whitelisted = append(whitelisted, parts[0])
- }
- minifier := minify.New()
- if *mnfy {
- minifier.AddFunc("text/css", css.Minify)
- minifier.AddFunc("text/html", html.Minify)
- minifier.AddFunc("image/svg+xml", svg.Minify)
- minifier.AddFuncRegexp(regexp.MustCompile("[/+]javascript$"), js.Minify)
- minifier.AddFuncRegexp(regexp.MustCompile("[/+]json$"), json.Minify)
- minifier.AddFuncRegexp(regexp.MustCompile("[/+]xml$"), xml.Minify)
- }
- m := autocert.Manager{
- Prompt: autocert.AcceptTOS,
- HostPolicy: autocert.HostWhitelist(whitelisted...),
- Cache: autocert.DirCache(*sslCacheDir),
- }
- h := handlers.CompressHandlerLevel(
- minifier.Middleware(handler()),
- *gzip,
- )
- s := &http.Server{
- Addr: *listen,
- Handler: h,
- TLSConfig: &tls.Config{GetCertificate: m.GetCertificate},
- }
- log.Fatal(s.ListenAndServeTLS("", ""))
- }
- // fix the specified url
- // this function will make sure that "http://" already exists,
- // also it will make sure that it has a hostname .
- func fixUrl(u string) string {
- u = strings.TrimPrefix(strings.TrimSpace(u), "https://")
- if strings.Index(u, ":") == 0 {
- u = "localhost" + u
- }
- if !strings.HasPrefix(u, "ws://") && !strings.HasPrefix(u, "http://") {
- u = "http://" + u
- }
- u = strings.TrimRight(u, "/")
- return u
- }
- // the proxy handler
- func handler() http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- r.Host = strings.SplitN(r.Host, ":", 2)[0]
- if _, found := domain_backend[r.Host]; !found {
- http.Error(w, r.Host+": not found", http.StatusNotImplemented)
- return
- }
- r.Header["X-Forwarded-Proto"] = []string{"https"}
- r.Header["X-Forwarded-For"] = append(r.Header["X-Forwarded-For"], strings.SplitN(r.RemoteAddr, ":", 2)[0])
- u, _ := url.Parse(domain_backend[r.Host] + "/" + strings.TrimLeft(r.URL.RequestURI(), "/"))
- if strings.ToLower(r.Header.Get("Upgrade")) == "websocket" {
- NewWebsocketReverseProxy(u).ServeHTTP(w, r)
- return
- } else {
- proxy := httputil.NewSingleHostReverseProxy(u)
- defaultDirector := proxy.Director
- proxy.Director = func(req *http.Request) {
- defaultDirector(req)
- req.Host = r.Host
- req.URL = u
- }
- proxy.ServeHTTP(w, r)
- return
- }
- })
- }
- // the websocket proxy handler
- func NewWebsocketReverseProxy(u *url.URL) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- backConn, err := net.Dial("tcp", u.Host)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- defer backConn.Close()
- hj, ok := w.(http.Hijacker)
- if !ok {
- http.Error(w, "webserver doesn't support hijacking", http.StatusInternalServerError)
- return
- }
- clientConn, _, err := hj.Hijack()
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- defer clientConn.Close()
- message := r.Method + " " + r.URL.RequestURI() + " " + r.Proto + "\n"
- message += "Host: " + r.Host + "\n"
- for k, vals := range r.Header {
- for _, v := range vals {
- message += k + ": " + v + "\n"
- }
- }
- message += "\n"
- go io.Copy(backConn, io.MultiReader(strings.NewReader(message), r.Body, clientConn))
- io.Copy(clientConn, backConn)
- })
- }
|