2022-08-15 06:16:22 +00:00
|
|
|
package redirector
|
2022-08-06 20:19:12 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/tls"
|
|
|
|
"crypto/x509"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
log "github.com/sirupsen/logrus"
|
2022-08-14 07:42:49 +00:00
|
|
|
"net"
|
2022-08-06 20:19:12 +00:00
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"runtime"
|
2022-08-15 06:16:22 +00:00
|
|
|
"strings"
|
2022-08-06 20:19:12 +00:00
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
ErrHttpsRedirect = errors.New("unexpected forced https redirect")
|
|
|
|
ErrCertExpired = errors.New("certificate is expired")
|
|
|
|
)
|
|
|
|
|
2022-08-15 06:16:22 +00:00
|
|
|
func (r *Redirector) checkHttp(scheme string) ServerCheck {
|
|
|
|
return func(server *Server, logFields log.Fields) (bool, error) {
|
|
|
|
return r.checkHttpScheme(server, scheme, logFields)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-06 20:19:12 +00:00
|
|
|
// checkHttp checks a URL for validity, and checks redirects
|
2022-08-15 06:16:22 +00:00
|
|
|
func (r *Redirector) checkHttpScheme(server *Server, scheme string, logFields log.Fields) (bool, error) {
|
2022-08-06 20:19:12 +00:00
|
|
|
u := &url.URL{
|
|
|
|
Scheme: "http",
|
|
|
|
Host: server.Host,
|
|
|
|
Path: server.Path,
|
|
|
|
}
|
|
|
|
|
|
|
|
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
|
|
|
|
|
|
|
req.Header.Set("User-Agent", "ArmbianRouter/1.0 (Go "+runtime.Version()+")")
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
|
|
|
res, err := checkClient.Do(req)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
|
|
|
logFields["responseCode"] = res.StatusCode
|
|
|
|
|
|
|
|
if res.StatusCode == http.StatusOK || res.StatusCode == http.StatusMovedPermanently || res.StatusCode == http.StatusFound || res.StatusCode == http.StatusNotFound {
|
|
|
|
if res.StatusCode == http.StatusMovedPermanently || res.StatusCode == http.StatusFound {
|
|
|
|
location := res.Header.Get("Location")
|
|
|
|
|
|
|
|
logFields["url"] = location
|
|
|
|
|
2022-08-15 06:16:22 +00:00
|
|
|
switch u.Scheme {
|
|
|
|
case "http":
|
|
|
|
res, err := r.checkRedirect(u.Scheme, location)
|
2022-08-06 20:19:12 +00:00
|
|
|
|
|
|
|
if !res || err != nil {
|
2022-08-15 06:16:22 +00:00
|
|
|
// If we don't support http, we remove it from supported protocols
|
|
|
|
server.Protocols = server.Protocols.Remove("http")
|
|
|
|
} else {
|
|
|
|
// Otherwise, we verify https support
|
|
|
|
r.checkProtocol(server, "https")
|
2022-08-06 20:19:12 +00:00
|
|
|
}
|
2022-08-15 06:16:22 +00:00
|
|
|
case "https":
|
|
|
|
// We don't want to allow downgrading, so this is an error.
|
|
|
|
return r.checkRedirect(u.Scheme, location)
|
2022-08-06 20:19:12 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
logFields["cause"] = fmt.Sprintf("Unexpected http status %d", res.StatusCode)
|
|
|
|
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
|
2022-08-15 06:16:22 +00:00
|
|
|
func (r *Redirector) checkProtocol(server *Server, scheme string) {
|
|
|
|
res, err := r.checkHttpScheme(server, scheme, log.Fields{})
|
|
|
|
|
|
|
|
if !res || err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if !server.Protocols.Contains(scheme) {
|
|
|
|
server.Protocols = server.Protocols.Append(scheme)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-06 20:19:12 +00:00
|
|
|
// checkRedirect parses a location header response and checks the scheme
|
2022-08-15 06:16:22 +00:00
|
|
|
func (r *Redirector) checkRedirect(originatingScheme, locationHeader string) (bool, error) {
|
2022-08-06 20:19:12 +00:00
|
|
|
newUrl, err := url.Parse(locationHeader)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if newUrl.Scheme == "https" {
|
|
|
|
return false, ErrHttpsRedirect
|
2022-08-15 06:16:22 +00:00
|
|
|
} else if originatingScheme == "https" && newUrl.Scheme == "https" {
|
|
|
|
return false, ErrHttpsRedirect
|
2022-08-06 20:19:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// checkTLS checks tls certificates from a host, ensures they're valid, and not expired.
|
2022-08-15 06:16:22 +00:00
|
|
|
func (r *Redirector) checkTLS(server *Server, logFields log.Fields) (bool, error) {
|
|
|
|
var host, port string
|
|
|
|
var err error
|
|
|
|
|
|
|
|
if strings.Contains(server.Host, ":") {
|
|
|
|
host, port, err = net.SplitHostPort(server.Host)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
host = server.Host
|
|
|
|
}
|
|
|
|
|
|
|
|
log.WithFields(log.Fields{
|
|
|
|
"server": server.Host,
|
|
|
|
"host": host,
|
|
|
|
"port": port,
|
|
|
|
}).Info("Checking TLS server")
|
2022-08-14 07:42:49 +00:00
|
|
|
|
|
|
|
if port == "" {
|
|
|
|
port = "443"
|
|
|
|
}
|
|
|
|
|
2022-08-15 06:16:22 +00:00
|
|
|
conn, err := tls.Dial("tcp", host+":"+port, &tls.Config{
|
|
|
|
RootCAs: r.config.RootCAs,
|
|
|
|
})
|
2022-08-06 20:19:12 +00:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
|
|
|
defer conn.Close()
|
|
|
|
|
|
|
|
err = conn.VerifyHostname(server.Host)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
|
|
|
state := conn.ConnectionState()
|
|
|
|
|
2022-08-15 06:16:22 +00:00
|
|
|
peerPool := x509.NewCertPool()
|
|
|
|
|
|
|
|
for _, intermediate := range state.PeerCertificates {
|
|
|
|
if !intermediate.IsCA {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
peerPool.AddCert(intermediate)
|
|
|
|
}
|
|
|
|
|
2022-08-06 20:19:12 +00:00
|
|
|
opts := x509.VerifyOptions{
|
2022-08-15 06:16:22 +00:00
|
|
|
Roots: r.config.RootCAs,
|
|
|
|
Intermediates: peerPool,
|
|
|
|
CurrentTime: time.Now(),
|
2022-08-06 20:19:12 +00:00
|
|
|
}
|
|
|
|
|
2022-08-15 06:16:22 +00:00
|
|
|
// We want only the leaf certificate, as this will verify up the chain for us.
|
|
|
|
cert := state.PeerCertificates[0]
|
|
|
|
|
|
|
|
if _, err := cert.Verify(opts); err != nil {
|
|
|
|
logFields["peerCert"] = cert.Subject.String()
|
|
|
|
|
|
|
|
if authErr, ok := err.(x509.UnknownAuthorityError); ok {
|
|
|
|
logFields["authCert"] = authErr.Cert.Subject.String()
|
|
|
|
logFields["ca"] = authErr.Cert.Issuer
|
2022-08-06 20:19:12 +00:00
|
|
|
}
|
2022-08-15 06:16:22 +00:00
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if now.Before(cert.NotBefore) || now.After(cert.NotAfter) {
|
|
|
|
logFields["peerCert"] = cert.Subject.String()
|
|
|
|
return false, err
|
2022-08-06 20:19:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, chain := range state.VerifiedChains {
|
|
|
|
for _, cert := range chain {
|
|
|
|
if now.Before(cert.NotBefore) || now.After(cert.NotAfter) {
|
|
|
|
logFields["cert"] = cert.Subject.String()
|
|
|
|
return false, ErrCertExpired
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-15 06:16:22 +00:00
|
|
|
// If https is valid, append it
|
|
|
|
if !server.Protocols.Contains("https") {
|
|
|
|
server.Protocols = server.Protocols.Append("https")
|
|
|
|
}
|
|
|
|
|
2022-08-06 20:19:12 +00:00
|
|
|
return true, nil
|
|
|
|
}
|