package main import ( "context" "crypto/tls" "fmt" "log" "net" "net/http" "net/http/httputil" "net/url" "strings" "time" "github.com/viki-org/dnscache" ) func main() { c, err := NewConfig() if err != nil { panic(err) } s := &http.Server{ Addr: fmt.Sprintf(":%d", c.Port), Handler: c, } foo := s.ListenAndServe if c.Cert.CRT != "" { foo = func() error { return s.ListenAndServeTLS(c.Cert.CRT, c.Cert.Key) } } log.Printf("listening on %v...", s.Addr) if err := foo(); err != nil { panic(err) } } var resolver = dnscache.New(time.Minute * 500) func (c Config) ServeHTTP(w http.ResponseWriter, r *http.Request) { endpoint := c.endpoint(r) if endpoint.To == "" { http.NotFound(w, r) return } if endpoint.Redir { u, _ := url.Parse(endpoint.To) r.URL = u r.Host = u.Host http.Redirect(w, r, r.URL.String(), http.StatusSeeOther) return } cors(w) if r.Method == http.MethodOptions { w.Header().Set("Content-Length", "0") w.Header().Set("Content-Type", "text/plain") w.WriteHeader(http.StatusOK) return } if r.URL.Scheme == "https" { w.Header().Set("X-Forwarded-Proto", "https") } if c.handleAdmin(w, r) { return } if !c.basicAuth(w, r) { return } u, err := url.Parse(endpoint.To) if err != nil { log.Printf("[%s] %v", c.key(r), err) } var transport http.Transport transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} if ip, _ := resolver.FetchOneString(strings.Split(u.Host, ":")[0]); ip != "" { transport.DialContext = func(ctx context.Context, network string, address string) (net.Conn, error) { if idx := strings.LastIndex(u.Host, ":"); idx > 0 { ip = ip + u.Host[idx:] } return (&net.Dialer{}).DialContext(ctx, network, ip) } } proxy := httputil.NewSingleHostReverseProxy(u) proxy.Transport = redirPurge{ proxyHost: r.Host, targetHost: u.Host, baseTransport: &transport, } proxy.ServeHTTP(w, r) } func cors(w http.ResponseWriter) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Headers", "X-Auth-Token, content-type, Content-Type") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS, TRACE, PATCH, HEAD, DELETE") } func (c Config) key(r *http.Request) string { return strings.Split(r.Host, ".")[0] } func (c Config) handleAdmin(w http.ResponseWriter, r *http.Request) bool { switch c.key(r) { case "_": panic("not impl: list") case "home": panic("not impl: home") } return false } func (c Config) basicAuth(w http.ResponseWriter, r *http.Request) bool { basicAuth := c.endpoint(r).BasicAuth if noAuth := basicAuth == ""; noAuth { return true } u, p, _ := r.BasicAuth() if fmt.Sprintf("%s:%s", u, p) != basicAuth { w.Header().Set("WWW-Authenticate", "Basic") http.Error(w, "unexpected basic auth", http.StatusUnauthorized) return false } return true } func (c Config) endpoint(r *http.Request) Endpoint { key := c.key(r) domain := strings.TrimPrefix(r.Host, key) m, ok := c.Domains[domain] if !ok { return Endpoint{} } return m[c.key(r)] } type redirPurge struct { proxyHost string targetHost string baseTransport http.RoundTripper } func (rp redirPurge) RoundTrip(r *http.Request) (*http.Response, error) { resp, err := rp.baseTransport.RoundTrip(r) if err != nil { return resp, err } if loc := resp.Header.Get("Location"); loc != "" { resp.Header.Set("Location", strings.Replace(loc, rp.targetHost, rp.proxyHost, 1)) } // google floc https://paramdeo.com/blog/opting-your-website-out-of-googles-floc-network resp.Header.Set("Permissions-Policy", "interest-cohort=()") return resp, err }