iris/addr.go
Gerasimos (Makis) Maropoulos 40b62f2958 gofmt everything, even the vendor.
Former-commit-id: c2bf34c63b5cc6c974ce2b3bcf04bb1c9b77efaf
2017-02-16 03:26:02 +02:00

300 lines
8.9 KiB
Go

package iris
import (
"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() error {
proxyAddr = ParseHost(proxyAddr)
// override the handler and redirect all requests to this addr
h := ProxyHandler(redirectSchemeAndHost)
prx := New(OptionDisableBanner(true))
prx.Adapt(RouterBuilderPolicy(func(RouteRepository, ContextPool) http.Handler {
return h
}))
go prx.Listen(proxyAddr)
time.Sleep(300 * time.Millisecond)
return func() error { return prx.Close() }
}