|
@@ -0,0 +1,167 @@
|
|
|
+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)
|
|
|
+ })
|
|
|
+}
|