2017-02-14 04:54:11 +01:00
|
|
|
package iris
|
|
|
|
|
|
|
|
import (
|
2017-02-17 03:46:33 +01:00
|
|
|
"context"
|
2017-02-14 04:54:11 +01:00
|
|
|
"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
|
|
|
|
}
|
|
|
|
|
2017-02-16 02:26:02 +01:00
|
|
|
///TODO: ?
|
2017-02-14 04:54:11 +01:00
|
|
|
// 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
|
2017-02-17 03:46:33 +01:00
|
|
|
func Proxy(proxyAddr string, redirectSchemeAndHost string) func(context.Context) error {
|
2017-02-14 04:54:11 +01:00
|
|
|
proxyAddr = ParseHost(proxyAddr)
|
|
|
|
|
|
|
|
// override the handler and redirect all requests to this addr
|
|
|
|
h := ProxyHandler(redirectSchemeAndHost)
|
2017-02-17 03:46:33 +01:00
|
|
|
prx := New()
|
2017-02-17 01:55:26 +01:00
|
|
|
|
2017-02-14 04:54:11 +01:00
|
|
|
prx.Adapt(RouterBuilderPolicy(func(RouteRepository, ContextPool) http.Handler {
|
|
|
|
return h
|
|
|
|
}))
|
|
|
|
|
|
|
|
go prx.Listen(proxyAddr)
|
2017-02-17 01:55:26 +01:00
|
|
|
time.Sleep(150 * time.Millisecond)
|
2017-02-14 04:54:11 +01:00
|
|
|
|
2017-02-17 03:46:33 +01:00
|
|
|
return prx.Shutdown
|
2017-02-14 04:54:11 +01:00
|
|
|
}
|