httpsify.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. package main
  2. import (
  3. "crypto/tls"
  4. "flag"
  5. "fmt"
  6. "io"
  7. "log"
  8. "net"
  9. "net/http"
  10. "net/http/httputil"
  11. "net/url"
  12. "regexp"
  13. "strings"
  14. "github.com/gorilla/handlers"
  15. "github.com/tdewolff/minify"
  16. "github.com/tdewolff/minify/css"
  17. "github.com/tdewolff/minify/html"
  18. "github.com/tdewolff/minify/js"
  19. "github.com/tdewolff/minify/json"
  20. "github.com/tdewolff/minify/svg"
  21. "github.com/tdewolff/minify/xml"
  22. "golang.org/x/crypto/acme/autocert"
  23. )
  24. var (
  25. // CMD options
  26. listen = flag.String("listen", ":443", "the local listen address")
  27. domains = flag.String("domains", "", "a comma separated strings of domain[->[ip]:port]")
  28. backend = flag.String("backend", ":80", "the default backend to be used")
  29. sslCacheDir = flag.String("ssl-cache-dir", "./httpsify-ssl-cache", "the cache directory to cache generated ssl certs")
  30. gzip = flag.Int("gzip", 0, "gzip compression level [0-9]")
  31. mnfy = flag.Bool("minify", true, "whether to minify the output or not")
  32. // internal vars
  33. domain_backend = map[string]string{}
  34. whitelisted = []string{}
  35. )
  36. func main() {
  37. flag.Parse()
  38. if *domains == "" {
  39. flag.Usage()
  40. fmt.Println(`Example(template): httpsify -domains "example.org,api.example.org->localhost:366, api2.example.org->:367"`)
  41. fmt.Println(`Example(real-life1): httpsify -domains "www.site.com,apiv1.site.com->:8080,apiv2.site.com->:8081" -minify=true -gzip=9`)
  42. fmt.Println(`Example(real-life2): httpsify -domains "www.site.com,site.com" -backend=:8080 -minify=true -gzip=0`)
  43. return
  44. }
  45. for _, zone := range strings.Split(*domains, ",") {
  46. parts := strings.SplitN(zone, "->", 2)
  47. if len(parts) < 2 {
  48. parts = append(parts, *backend)
  49. }
  50. parts[1] = fixUrl(parts[1])
  51. domain_backend[parts[0]] = parts[1]
  52. whitelisted = append(whitelisted, parts[0])
  53. }
  54. minifier := minify.New()
  55. if *mnfy {
  56. minifier.AddFunc("text/css", css.Minify)
  57. minifier.AddFunc("text/html", html.Minify)
  58. minifier.AddFunc("image/svg+xml", svg.Minify)
  59. minifier.AddFuncRegexp(regexp.MustCompile("[/+]javascript$"), js.Minify)
  60. minifier.AddFuncRegexp(regexp.MustCompile("[/+]json$"), json.Minify)
  61. minifier.AddFuncRegexp(regexp.MustCompile("[/+]xml$"), xml.Minify)
  62. }
  63. m := autocert.Manager{
  64. Prompt: autocert.AcceptTOS,
  65. HostPolicy: autocert.HostWhitelist(whitelisted...),
  66. Cache: autocert.DirCache(*sslCacheDir),
  67. }
  68. h := handlers.CompressHandlerLevel(
  69. minifier.Middleware(handler()),
  70. *gzip,
  71. )
  72. s := &http.Server{
  73. Addr: *listen,
  74. Handler: h,
  75. TLSConfig: &tls.Config{GetCertificate: m.GetCertificate},
  76. }
  77. log.Fatal(s.ListenAndServeTLS("", ""))
  78. }
  79. // fix the specified url
  80. // this function will make sure that "http://" already exists,
  81. // also it will make sure that it has a hostname .
  82. func fixUrl(u string) string {
  83. u = strings.TrimPrefix(strings.TrimSpace(u), "https://")
  84. if strings.Index(u, ":") == 0 {
  85. u = "localhost" + u
  86. }
  87. if !strings.HasPrefix(u, "ws://") && !strings.HasPrefix(u, "http://") {
  88. u = "http://" + u
  89. }
  90. u = strings.TrimRight(u, "/")
  91. return u
  92. }
  93. // the proxy handler
  94. func handler() http.Handler {
  95. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  96. r.Host = strings.SplitN(r.Host, ":", 2)[0]
  97. if _, found := domain_backend[r.Host]; !found {
  98. http.Error(w, r.Host+": not found", http.StatusNotImplemented)
  99. return
  100. }
  101. r.Header["X-Forwarded-Proto"] = []string{"https"}
  102. r.Header["X-Forwarded-For"] = append(r.Header["X-Forwarded-For"], strings.SplitN(r.RemoteAddr, ":", 2)[0])
  103. u, _ := url.Parse(domain_backend[r.Host] + "/" + strings.TrimLeft(r.URL.RequestURI(), "/"))
  104. if strings.ToLower(r.Header.Get("Upgrade")) == "websocket" {
  105. NewWebsocketReverseProxy(u).ServeHTTP(w, r)
  106. return
  107. } else {
  108. proxy := httputil.NewSingleHostReverseProxy(u)
  109. defaultDirector := proxy.Director
  110. proxy.Director = func(req *http.Request) {
  111. defaultDirector(req)
  112. req.Host = r.Host
  113. req.URL = u
  114. }
  115. proxy.ServeHTTP(w, r)
  116. return
  117. }
  118. })
  119. }
  120. // the websocket proxy handler
  121. func NewWebsocketReverseProxy(u *url.URL) http.Handler {
  122. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  123. backConn, err := net.Dial("tcp", u.Host)
  124. if err != nil {
  125. http.Error(w, err.Error(), http.StatusInternalServerError)
  126. return
  127. }
  128. defer backConn.Close()
  129. hj, ok := w.(http.Hijacker)
  130. if !ok {
  131. http.Error(w, "webserver doesn't support hijacking", http.StatusInternalServerError)
  132. return
  133. }
  134. clientConn, _, err := hj.Hijack()
  135. if err != nil {
  136. http.Error(w, err.Error(), http.StatusInternalServerError)
  137. return
  138. }
  139. defer clientConn.Close()
  140. message := r.Method + " " + r.URL.RequestURI() + " " + r.Proto + "\n"
  141. message += "Host: " + r.Host + "\n"
  142. for k, vals := range r.Header {
  143. for _, v := range vals {
  144. message += k + ": " + v + "\n"
  145. }
  146. }
  147. message += "\n"
  148. go io.Copy(backConn, io.MultiReader(strings.NewReader(message), r.Body, clientConn))
  149. io.Copy(clientConn, backConn)
  150. })
  151. }