package iris import ( "context" "crypto/tls" "net" "net/http" "os" "strconv" "strings" "time" "github.com/kataras/go-errors" "golang.org/x/crypto/acme/autocert" ) var ( errPortAlreadyUsed = errors.New("Port is already used") errRemoveUnix = errors.New("Unexpected error when trying to remove unix socket file. Addr: %s | Trace: %s") errChmod = errors.New("Cannot chmod %#o for %q: %s") errCertKeyMissing = errors.New("You should provide certFile and keyFile for TLS/SSL") errParseTLS = errors.New("Couldn't load TLS, certFile=%q, keyFile=%q. Trace: %s") ) // TCP4 returns a new tcp4 Listener func TCP4(addr string) (net.Listener, error) { return net.Listen("tcp4", ParseHost(addr)) } // TCPKeepAlive returns a new tcp4 keep alive Listener func TCPKeepAlive(addr string) (net.Listener, error) { ln, err := TCP4(addr) if err != nil { return nil, err } return TCPKeepAliveListener{ln.(*net.TCPListener)}, err } // UNIX returns a new unix(file) Listener func UNIX(addr string, mode os.FileMode) (net.Listener, error) { if errOs := os.Remove(addr); errOs != nil && !os.IsNotExist(errOs) { return nil, errRemoveUnix.Format(addr, errOs.Error()) } listener, err := net.Listen("unix", addr) if err != nil { return nil, errPortAlreadyUsed.AppendErr(err) } if err = os.Chmod(addr, mode); err != nil { return nil, errChmod.Format(mode, addr, err.Error()) } return listener, nil } // TLS returns a new TLS Listener func TLS(addr, certFile, keyFile string) (net.Listener, error) { if certFile == "" || keyFile == "" { return nil, errCertKeyMissing } cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { return nil, errParseTLS.Format(certFile, keyFile, err) } return CERT(addr, cert) } // CERT returns a listener which contans tls.Config with the provided certificate, use for ssl func CERT(addr string, cert tls.Certificate) (net.Listener, error) { ln, err := TCP4(addr) if err != nil { return nil, err } tlsConfig := &tls.Config{ Certificates: []tls.Certificate{cert}, PreferServerCipherSuites: true, } return tls.NewListener(ln, tlsConfig), nil } // LETSENCRYPT returns a new Automatic TLS Listener using letsencrypt.org service // receives two parameters, the first is the domain of the server // and the second is optionally, the cache directory, if you skip it then the cache directory is "./certcache" // if you want to disable cache directory then simple give it a value of empty string "" // // does NOT supports localhost domains for testing. // // this is the recommended function to use when you're ready for production state func LETSENCRYPT(addr string, cacheDirOptional ...string) (net.Listener, error) { if portIdx := strings.IndexByte(addr, ':'); portIdx == -1 { addr += ":443" } ln, err := TCP4(addr) if err != nil { return nil, err } cacheDir := "./certcache" if len(cacheDirOptional) > 0 { cacheDir = cacheDirOptional[0] } m := autocert.Manager{ Prompt: autocert.AcceptTOS, } // HostPolicy is missing, if user wants it, then she/he should manually // configure the autocertmanager and use the `iris.Default.Serve` to pass that listener if cacheDir == "" { // then the user passed empty by own will, then I guess she/he doesnt' want any cache directory } else { m.Cache = autocert.DirCache(cacheDir) } tlsConfig := &tls.Config{GetCertificate: m.GetCertificate} tlsLn := tls.NewListener(ln, tlsConfig) return tlsLn, nil } // TCPKeepAliveListener sets TCP keep-alive timeouts on accepted // connections. // Dead TCP connections (e.g. closing laptop mid-download) eventually // go away // It is not used by default if you want to pass a keep alive listener // then just pass the child listener, example: // listener := iris.TCPKeepAliveListener{iris.TCP4(":8080").(*net.TCPListener)} type TCPKeepAliveListener struct { *net.TCPListener } // Accept implements the listener and sets the keep alive period which is 3minutes func (ln TCPKeepAliveListener) Accept() (c net.Conn, err error) { tc, err := ln.AcceptTCP() if err != nil { return } err = tc.SetKeepAlive(true) if err != nil { return } err = tc.SetKeepAlivePeriod(3 * time.Minute) if err != nil { return } return tc, nil } ///TODO: ? // func (ln TCPKeepAliveListener) Close() error { // return nil // } // ParseHost tries to convert a given string to an address which is compatible with net.Listener and server func ParseHost(addr string) string { // check if addr has :port, if not do it +:80 ,we need the hostname for many cases a := addr if a == "" { // check for os environments if oshost := os.Getenv("ADDR"); oshost != "" { a = oshost } else if oshost := os.Getenv("HOST"); oshost != "" { a = oshost } else if oshost := os.Getenv("HOSTNAME"); oshost != "" { a = oshost // check for port also here if osport := os.Getenv("PORT"); osport != "" { a += ":" + osport } } else if osport := os.Getenv("PORT"); osport != "" { a = ":" + osport } else { a = ":http" } } if portIdx := strings.IndexByte(a, ':'); portIdx == 0 { if a[portIdx:] == ":https" { a = DefaultServerHostname + ":443" } else { // if contains only :port ,then the : is the first letter, so we dont have setted a hostname, lets set it a = DefaultServerHostname + a } } /* changed my mind, don't add 80, this will cause problems on unix listeners, and it's not really necessary because we take the port using parsePort if portIdx := strings.IndexByte(a, ':'); portIdx < 0 { // missing port part, add it a = a + ":80" }*/ return a } // ParseHostname receives an addr of form host[:port] and returns the hostname part of it // ex: localhost:8080 will return the `localhost`, mydomain.com:8080 will return the 'mydomain' func ParseHostname(addr string) string { idx := strings.IndexByte(addr, ':') if idx == 0 { // only port, then return 0.0.0.0 return "0.0.0.0" } else if idx > 0 { return addr[0:idx] } // it's already hostname return addr } // ParsePort receives an addr of form host[:port] and returns the port part of it // ex: localhost:8080 will return the `8080`, mydomain.com will return the '80' func ParsePort(addr string) int { if portIdx := strings.IndexByte(addr, ':'); portIdx != -1 { afP := addr[portIdx+1:] p, err := strconv.Atoi(afP) if err == nil { return p } else if afP == "https" { // it's not number, check if it's :https return 443 } } return 80 } const ( // SchemeHTTPS returns "https://" (full) SchemeHTTPS = "https://" // SchemeHTTP returns "http://" (full) SchemeHTTP = "http://" ) // ParseScheme returns the scheme based on the host,addr,domain // Note: the full scheme not just http*,https* *http:// *https:// func ParseScheme(domain string) string { // pure check if strings.HasPrefix(domain, SchemeHTTPS) || ParsePort(domain) == 443 { return SchemeHTTPS } return SchemeHTTP } // ProxyHandler returns a new net/http.Handler which works as 'proxy', maybe doesn't suits you look its code before using that in production var ProxyHandler = func(redirectSchemeAndHost string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // override the handler and redirect all requests to this addr redirectTo := redirectSchemeAndHost fakehost := r.URL.Host path := r.URL.EscapedPath() if strings.Count(fakehost, ".") >= 3 { // propably a subdomain, pure check but doesn't matters don't worry if sufIdx := strings.LastIndexByte(fakehost, '.'); sufIdx > 0 { // check if the last part is a number instead of .com/.gr... // if it's number then it's propably is 0.0.0.0 or 127.0.0.1... so it shouldn' use subdomain if _, err := strconv.Atoi(fakehost[sufIdx+1:]); err != nil { // it's not number then process the try to parse the subdomain redirectScheme := ParseScheme(redirectSchemeAndHost) realHost := strings.Replace(redirectSchemeAndHost, redirectScheme, "", 1) redirectHost := strings.Replace(fakehost, fakehost, realHost, 1) redirectTo = redirectScheme + redirectHost + path http.Redirect(w, r, redirectTo, StatusMovedPermanently) return } } } if path != "/" { redirectTo += path } if redirectTo == r.URL.String() { return } // redirectTo := redirectSchemeAndHost + r.RequestURI http.Redirect(w, r, redirectTo, StatusMovedPermanently) } } // Proxy not really a proxy, it's just // starts a server listening on proxyAddr but redirects all requests to the redirectToSchemeAndHost+$path // nothing special, use it only when you want to start a secondary server which its only work is to redirect from one requested path to another // // returns a close function func Proxy(proxyAddr string, redirectSchemeAndHost string) func(context.Context) error { proxyAddr = ParseHost(proxyAddr) // override the handler and redirect all requests to this addr h := ProxyHandler(redirectSchemeAndHost) prx := New() prx.Adapt(RouterBuilderPolicy(func(RouteRepository, ContextPool) http.Handler { return h })) go prx.Listen(proxyAddr) time.Sleep(150 * time.Millisecond) return prx.Shutdown }