Browse Source

Create httpsify.go

Mohammed Al Ashaal 8 years ago
parent
commit
49dd6e48b8
1 changed files with 167 additions and 0 deletions
  1. 167 0
      httpsify.go

+ 167 - 0
httpsify.go

@@ -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)
+	})
+}