Updated a lot of stuffs

+ Added comments for whitelist
+ Added automatic cert pick for multi-host certs (SNI)
+ Renamed .crt to .pem for cert store
+ Added best-fit selection for wildcard matching rules
+ Added x-proxy-by header
+ Added X-real-Ip header
+ Added Development Mode (Cache-Control: no-store)
+ Updated utm timeout to 10 seconds instead of 90
This commit is contained in:
Toby Chui
2024-02-16 15:44:09 +08:00
parent 174efc9080
commit e980bc847b
41 changed files with 1056 additions and 531 deletions

View File

@@ -163,7 +163,7 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email
// Each certificate comes back with the cert bytes, the bytes of the client's
// private key, and a certificate URL.
err = os.WriteFile("./conf/certs/"+certificateName+".crt", certificates.Certificate, 0777)
err = os.WriteFile("./conf/certs/"+certificateName+".pem", certificates.Certificate, 0777)
if err != nil {
log.Println(err)
return false, err

View File

@@ -1,8 +1,6 @@
package dynamicproxy
import (
_ "embed"
"errors"
"net/http"
"net/url"
"os"
@@ -25,11 +23,6 @@ import (
- Vitrual Directory Routing
*/
var (
//go:embed tld.json
rawTldMap []byte
)
func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
/*
Special Routing Rules, bypass most of the limitations
@@ -52,10 +45,12 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
//Inject debug headers
w.Header().Set("x-proxy-by", "zoraxy/"+h.Parent.Option.HostVersion)
/*
General Access Check
*/
respWritten := h.handleAccessRouting(w, r)
if respWritten {
return
@@ -81,6 +76,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
/*
Host Routing
*/
sep := h.Parent.getProxyEndpointFromHostname(domainOnly)
if sep != nil && !sep.Disabled {
if sep.RequireBasicAuth {
@@ -235,44 +231,3 @@ func (h *ProxyHandler) handleAccessRouting(w http.ResponseWriter, r *http.Reques
return false
}
// Return if the given host is already topped (e.g. example.com or example.co.uk) instead of
// a host with subdomain (e.g. test.example.com)
func (h *ProxyHandler) isTopLevelRedirectableDomain(requestHost string) bool {
parts := strings.Split(requestHost, ".")
if len(parts) > 2 {
//Cases where strange tld is used like .co.uk or .com.hk
_, ok := h.Parent.tldMap[strings.Join(parts[1:], ".")]
if ok {
//Already topped
return true
}
} else {
//Already topped
return true
}
return false
}
// GetTopLevelRedirectableDomain returns the toppest level of domain
// that is redirectable. E.g. a.b.c.example.co.uk will return example.co.uk
func (h *ProxyHandler) getTopLevelRedirectableDomain(unsetSubdomainHost string) (string, error) {
parts := strings.Split(unsetSubdomainHost, ".")
if h.isTopLevelRedirectableDomain(unsetSubdomainHost) {
//Already topped
return "", errors.New("already at top level domain")
}
for i := 0; i < len(parts); i++ {
possibleTld := parts[i:]
_, ok := h.Parent.tldMap[strings.Join(possibleTld, ".")]
if ok {
//This is tld length
tld := strings.Join(parts[i-1:], ".")
return "//" + tld, nil
}
}
return "", errors.New("unsupported top level domain given")
}

View File

@@ -60,6 +60,7 @@ type ResponseRewriteRuleSet struct {
ProxyDomain string
OriginalHost string
UseTLS bool
NoCache bool
PathPrefix string //Vdir prefix for root, / will be rewrite to this
}
@@ -243,7 +244,7 @@ func (p *ReverseProxy) logf(format string, args ...interface{}) {
}
}
func removeHeaders(header http.Header) {
func removeHeaders(header http.Header, noCache bool) {
// Remove hop-by-hop headers listed in the "Connection" header.
if c := header.Get("Connection"); c != "" {
for _, f := range strings.Split(c, ",") {
@@ -260,9 +261,16 @@ func removeHeaders(header http.Header) {
}
}
if header.Get("A-Upgrade") != "" {
header.Set("Upgrade", header.Get("A-Upgrade"))
header.Del("A-Upgrade")
//Restore the Upgrade header if any
if header.Get("Zr-Origin-Upgrade") != "" {
header.Set("Upgrade", header.Get("Zr-Origin-Upgrade"))
header.Del("Zr-Origin-Upgrade")
}
//Disable cache if nocache is set
if noCache {
header.Del("Cache-Control")
header.Set("Cache-Control", "no-store")
}
}
@@ -281,6 +289,11 @@ func addXForwardedForHeader(req *http.Request) {
req.Header.Set("X-Forwarded-Proto", "http")
}
if req.Header.Get("X-Real-Ip") == "" {
//Not exists. Fill it in with client IP
req.Header.Set("X-Real-Ip", clientIP)
}
}
}
@@ -323,7 +336,7 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
copyHeader(outreq.Header, req.Header)
// Remove hop-by-hop headers listed in the "Connection" header, Remove hop-by-hop headers.
removeHeaders(outreq.Header)
removeHeaders(outreq.Header, rrr.NoCache)
// Add X-Forwarded-For Header.
addXForwardedForHeader(outreq)
@@ -339,7 +352,7 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
}
// Remove hop-by-hop headers listed in the "Connection" header of the response, Remove hop-by-hop headers.
removeHeaders(res.Header)
removeHeaders(res.Header, rrr.NoCache)
if p.ModifyResponse != nil {
if err := p.ModifyResponse(res); err != nil {

View File

@@ -35,12 +35,6 @@ func NewDynamicProxy(option RouterOption) (*Router, error) {
Parent: &thisRouter,
}
//Prase the tld map for tld redirection in main router
//See Server.go declarations
if len(rawTldMap) > 0 {
json.Unmarshal(rawTldMap, &thisRouter.tldMap)
}
return &thisRouter, nil
}
@@ -74,12 +68,12 @@ func (router *Router) UpdateHttpToHttpsRedirectSetting(useRedirect bool) {
func (router *Router) StartProxyService() error {
//Create a new server object
if router.server != nil {
return errors.New("Reverse proxy server already running")
return errors.New("reverse proxy server already running")
}
//Check if root route is set
if router.Root == nil {
return errors.New("Reverse proxy router root not set")
return errors.New("reverse proxy router root not set")
}
minVersion := tls.VersionTLS10
@@ -92,16 +86,6 @@ func (router *Router) StartProxyService() error {
}
if router.Option.UseTls {
/*
//Serve with TLS mode
ln, err := tls.Listen("tcp", ":"+strconv.Itoa(router.Option.Port), config)
if err != nil {
log.Println(err)
router.Running = false
return err
}
router.tlsListener = ln
*/
router.server = &http.Server{
Addr: ":" + strconv.Itoa(router.Option.Port),
Handler: router.mux,
@@ -216,7 +200,7 @@ func (router *Router) StartProxyService() error {
func (router *Router) StopProxyService() error {
if router.server == nil {
return errors.New("Reverse proxy server already stopped")
return errors.New("reverse proxy server already stopped")
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
@@ -251,6 +235,7 @@ func (router *Router) Restart() error {
return err
}
time.Sleep(300 * time.Millisecond)
// Start the server
err = router.StartProxyService()
if err != nil {

View File

@@ -7,7 +7,13 @@ import (
)
/*
Endpoint Functions
endpoint.go
author: tobychui
This script handle the proxy endpoint object actions
so proxyEndpoint can be handled like a proper oop object
Most of the functions are implemented in dynamicproxy.go
*/
// Get virtual directory handler from given URI
@@ -87,3 +93,16 @@ func (ep *ProxyEndpoint) Clone() *ProxyEndpoint {
json.Unmarshal(js, &clonedProxyEndpoint)
return &clonedProxyEndpoint
}
// Remove this proxy endpoint from running proxy endpoint list
func (ep *ProxyEndpoint) Remove() error {
ep.parent.ProxyEndpoints.Delete(ep.RootOrMatchingDomain)
return nil
}
// Write changes to runtime without respawning the proxy handler
// use prepare -> remove -> add if you change anything in the endpoint
// that effects the proxy routing src / dest
func (ep *ProxyEndpoint) UpdateToRuntime() {
ep.parent.ProxyEndpoints.Store(ep.RootOrMatchingDomain, ep)
}

View File

@@ -7,6 +7,7 @@ import (
"net/http"
"net/url"
"path/filepath"
"sort"
"strings"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
@@ -37,6 +38,7 @@ func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoi
}
//No hit. Try with wildcard
matchProxyEndpoints := []*ProxyEndpoint{}
router.ProxyEndpoints.Range(func(k, v interface{}) bool {
ep := v.(*ProxyEndpoint)
match, err := filepath.Match(ep.RootOrMatchingDomain, hostname)
@@ -45,12 +47,24 @@ func (router *Router) getProxyEndpointFromHostname(hostname string) *ProxyEndpoi
return true
}
if match {
targetSubdomainEndpoint = ep
return false
//targetSubdomainEndpoint = ep
matchProxyEndpoints = append(matchProxyEndpoints, ep)
return true
}
return true
})
if len(matchProxyEndpoints) == 1 {
//Only 1 match
return matchProxyEndpoints[0]
} else if len(matchProxyEndpoints) > 1 {
//More than one match. Get the best match one
sort.Slice(matchProxyEndpoints, func(i, j int) bool {
return matchProxyEndpoints[i].RootOrMatchingDomain < matchProxyEndpoints[j].RootOrMatchingDomain
})
return matchProxyEndpoints[0]
}
return targetSubdomainEndpoint
}
@@ -77,7 +91,7 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
requestURL := r.URL.String()
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
r.Header.Set("A-Upgrade", "websocket")
r.Header.Set("Zr-Origin-Upgrade", "websocket")
wsRedirectionEndpoint := target.Domain
if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
//Append / to the end of the redirection endpoint if not exists
@@ -109,6 +123,7 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
ProxyDomain: target.Domain,
OriginalHost: originalHostHeader,
UseTLS: target.RequireTLS,
NoCache: h.Parent.Option.NoCache,
PathPrefix: "",
})
@@ -137,7 +152,7 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
r.Header.Set("X-Forwarded-Server", "zoraxy-"+h.Parent.Option.HostUUID)
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
r.Header.Set("A-Upgrade", "websocket")
r.Header.Set("Zr-Origin-Upgrade", "websocket")
wsRedirectionEndpoint := target.Domain
if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
wsRedirectionEndpoint = wsRedirectionEndpoint + "/"

View File

@@ -97,3 +97,13 @@ func (router *Router) SetProxyRouteAsRoot(endpoint *ProxyEndpoint) error {
router.Root = endpoint
return nil
}
// ProxyEndpoint remove provide global access by key
func (router *Router) RemoveProxyEndpointByRootname(rootnameOrMatchingDomain string) error {
targetEpt, err := router.LoadProxy(rootnameOrMatchingDomain)
if err != nil {
return err
}
return targetEpt.Remove()
}

View File

@@ -25,9 +25,11 @@ type ProxyHandler struct {
type RouterOption struct {
HostUUID string //The UUID of Zoraxy, use for heading mod
HostVersion string //The version of Zoraxy, use for heading mod
Port int //Incoming port
UseTls bool //Use TLS to serve incoming requsts
ForceTLSLatest bool //Force TLS1.2 or above
NoCache bool //Force set Cache-Control: no-store
ListenOnPort80 bool //Enable port 80 http listener
ForceHttpsRedirect bool //Force redirection of http to https endpoint
TlsManager *tlscert.Manager

View File

@@ -1,6 +1,9 @@
package geodb
import "strings"
import (
"encoding/json"
"strings"
)
/*
Whitelist.go
@@ -8,11 +11,29 @@ import "strings"
This script handles whitelist related functions
*/
const (
EntryType_CountryCode int = 0
EntryType_IP int = 1
)
type WhitelistEntry struct {
EntryType int //Entry type of whitelist, Country Code or IP
CC string //ISO Country Code
IP string //IP address or range
Comment string //Comment for this entry
}
//Geo Whitelist
func (s *Store) AddCountryCodeToWhitelist(countryCode string) {
func (s *Store) AddCountryCodeToWhitelist(countryCode string, comment string) {
countryCode = strings.ToLower(countryCode)
s.sysdb.Write("whitelist-cn", countryCode, true)
entry := WhitelistEntry{
EntryType: EntryType_CountryCode,
CC: countryCode,
Comment: comment,
}
s.sysdb.Write("whitelist-cn", countryCode, entry)
}
func (s *Store) RemoveCountryCodeFromWhitelist(countryCode string) {
@@ -22,20 +43,19 @@ func (s *Store) RemoveCountryCodeFromWhitelist(countryCode string) {
func (s *Store) IsCountryCodeWhitelisted(countryCode string) bool {
countryCode = strings.ToLower(countryCode)
var isWhitelisted bool = false
s.sysdb.Read("whitelist-cn", countryCode, &isWhitelisted)
return isWhitelisted
return s.sysdb.KeyExists("whitelist-cn", countryCode)
}
func (s *Store) GetAllWhitelistedCountryCode() []string {
whitelistedCountryCode := []string{}
func (s *Store) GetAllWhitelistedCountryCode() []*WhitelistEntry {
whitelistedCountryCode := []*WhitelistEntry{}
entries, err := s.sysdb.ListTable("whitelist-cn")
if err != nil {
return whitelistedCountryCode
}
for _, keypairs := range entries {
ip := string(keypairs[0])
whitelistedCountryCode = append(whitelistedCountryCode, ip)
thisWhitelistEntry := WhitelistEntry{}
json.Unmarshal(keypairs[1], &thisWhitelistEntry)
whitelistedCountryCode = append(whitelistedCountryCode, &thisWhitelistEntry)
}
return whitelistedCountryCode
@@ -43,8 +63,14 @@ func (s *Store) GetAllWhitelistedCountryCode() []string {
//IP Whitelist
func (s *Store) AddIPToWhiteList(ipAddr string) {
s.sysdb.Write("whitelist-ip", ipAddr, true)
func (s *Store) AddIPToWhiteList(ipAddr string, comment string) {
thisIpEntry := WhitelistEntry{
EntryType: EntryType_IP,
IP: ipAddr,
Comment: comment,
}
s.sysdb.Write("whitelist-ip", ipAddr, thisIpEntry)
}
func (s *Store) RemoveIPFromWhiteList(ipAddr string) {
@@ -52,14 +78,14 @@ func (s *Store) RemoveIPFromWhiteList(ipAddr string) {
}
func (s *Store) IsIPWhitelisted(ipAddr string) bool {
var isWhitelisted bool = false
s.sysdb.Read("whitelist-ip", ipAddr, &isWhitelisted)
isWhitelisted := s.sysdb.KeyExists("whitelist-ip", ipAddr)
if isWhitelisted {
//single IP whitelist entry
return true
}
//Check for IP wildcard and CIRD rules
AllWhitelistedIps := s.GetAllWhitelistedIp()
AllWhitelistedIps := s.GetAllWhitelistedIpAsStringSlice()
for _, whitelistRules := range AllWhitelistedIps {
wildcardMatch := MatchIpWildcard(ipAddr, whitelistRules)
if wildcardMatch {
@@ -75,17 +101,29 @@ func (s *Store) IsIPWhitelisted(ipAddr string) bool {
return false
}
func (s *Store) GetAllWhitelistedIp() []string {
whitelistedIp := []string{}
func (s *Store) GetAllWhitelistedIp() []*WhitelistEntry {
whitelistedIp := []*WhitelistEntry{}
entries, err := s.sysdb.ListTable("whitelist-ip")
if err != nil {
return whitelistedIp
}
for _, keypairs := range entries {
ip := string(keypairs[0])
whitelistedIp = append(whitelistedIp, ip)
//ip := string(keypairs[0])
thisEntry := WhitelistEntry{}
json.Unmarshal(keypairs[1], &thisEntry)
whitelistedIp = append(whitelistedIp, &thisEntry)
}
return whitelistedIp
}
func (s *Store) GetAllWhitelistedIpAsStringSlice() []string {
allWhitelistedIPs := []string{}
entries := s.GetAllWhitelistedIp()
for _, entry := range entries {
allWhitelistedIPs = append(allWhitelistedIPs, entry.IP)
}
return allWhitelistedIPs
}

View File

@@ -211,9 +211,9 @@ func removeHeaders(header http.Header) {
}
}
if header.Get("A-Upgrade") != "" {
header.Set("Upgrade", header.Get("A-Upgrade"))
header.Del("A-Upgrade")
if header.Get("Zr-Origin-Upgrade") != "" {
header.Set("Upgrade", header.Get("Zr-Origin-Upgrade"))
header.Del("Zr-Origin-Upgrade")
}
}

View File

@@ -82,7 +82,7 @@ func (m *Manager) HandleHttpByInstanceId(instanceId string, w http.ResponseWrite
requestURL := r.URL.String()
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
r.Header.Set("A-Upgrade", "websocket")
r.Header.Set("Zr-Origin-Upgrade", "websocket")
requestURL = strings.TrimPrefix(requestURL, "/")
u, _ := url.Parse("ws://127.0.0.1:" + strconv.Itoa(targetInstance.AssignedPort) + "/" + requestURL)
wspHandler := websocketproxy.NewProxy(u, false)

View File

@@ -5,22 +5,22 @@ import (
"strings"
)
//This remove the certificates in the list where either the
//public key or the private key is missing
// This remove the certificates in the list where either the
// public key or the private key is missing
func getCertPairs(certFiles []string) []string {
crtMap := make(map[string]bool)
pemMap := make(map[string]bool)
keyMap := make(map[string]bool)
for _, filename := range certFiles {
if filepath.Ext(filename) == ".crt" {
crtMap[strings.TrimSuffix(filename, ".crt")] = true
if filepath.Ext(filename) == ".pem" {
pemMap[strings.TrimSuffix(filename, ".pem")] = true
} else if filepath.Ext(filename) == ".key" {
keyMap[strings.TrimSuffix(filename, ".key")] = true
}
}
var result []string
for domain := range crtMap {
for domain := range pemMap {
if keyMap[domain] {
result = append(result, domain)
}
@@ -29,7 +29,7 @@ func getCertPairs(certFiles []string) []string {
return result
}
//Get the cloest subdomain certificate from a list of domains
// Get the cloest subdomain certificate from a list of domains
func matchClosestDomainCertificate(subdomain string, domains []string) string {
var matchingDomain string = ""
maxLength := 0
@@ -43,18 +43,3 @@ func matchClosestDomainCertificate(subdomain string, domains []string) string {
return matchingDomain
}
//Check if a requesting domain is a subdomain of a given domain
func isSubdomain(subdomain, domain string) bool {
subdomainParts := strings.Split(subdomain, ".")
domainParts := strings.Split(domain, ".")
if len(subdomainParts) < len(domainParts) {
return false
}
for i := range domainParts {
if subdomainParts[len(subdomainParts)-1-i] != domainParts[len(domainParts)-1-i] {
return false
}
}
return true
}

View File

@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDuTCCAqCgAwIBAgIBADANBgkqhkiG9w0BAQ0FADB2MQswCQYDVQQGEwJoazES
MBAGA1UECAwJSG9uZyBLb25nMRQwEgYDVQQKDAtpbXVzbGFiLmNvbTEZMBcGA1UE
AwwQWm9yYXh5IFNlbGYtaG9zdDEQMA4GA1UEBwwHSU1VU0xBQjEQMA4GA1UECwwH
SU1VU0xBQjAeFw0yMzA1MjcxMDQyNDJaFw0zODA1MjgxMDQyNDJaMHYxCzAJBgNV
BAYTAmhrMRIwEAYDVQQIDAlIb25nIEtvbmcxFDASBgNVBAoMC2ltdXNsYWIuY29t
MRkwFwYDVQQDDBBab3JheHkgU2VsZi1ob3N0MRAwDgYDVQQHDAdJTVVTTEFCMRAw
DgYDVQQLDAdJTVVTTEFCMIIBIzANBgkqhkiG9w0BAQEFAAOCARAAMIIBCwKCAQIA
xav3Qq4DBooHsGW9m+r0dgjI832grX2c0Z6MJQQoE7B6wfpUI0OyfRugTXyXoiRZ
gLxuROgiCUmp8FaLbl7RsvbImMbCPo3D/RbCT1aJCNXLZ0a7yvcDYc6woQW4nUyk
ohHfT2otcu+OYS6aYRZuXGsKTAqPSwEXRMtr89wkPgZPsrCD27LFHBOmIcVABDvF
KRuiwHWSHhFfU5n1AZLyYeYoLNQ9fZPvzPpkMD+HMKi4MMwr/vLE0DwU5jSfVFq+
cd68zVihp9N/T77yah5EIH9CYm4m8Acs4bfL8DALxnaSN3KmGw6J35rOXrJvJLdh
t42PDROmQrXN8uG8wGkBiBkCAwEAAaNQME4wHQYDVR0OBBYEFLhXihE+1K6MoL0P
Nx5htfuSatpiMB8GA1UdIwQYMBaAFLhXihE+1K6MoL0PNx5htfuSatpiMAwGA1Ud
EwQFMAMBAf8wDQYJKoZIhvcNAQENBQADggECAMCn0ed1bfLefGvoQJV/q+X9p61U
HunSFJAAhp0N2Q3tq/zjIu0kJX7N0JBciEw2c0ZmqJIqR8V8Im/h/4XuuOR+53hg
opOSPo39ww7mpxyBlQm63v1nXcNQcvw4U0JqXQ4Kyv8cgX7DIuyjRWHQpc5+6joy
L5Nz5hzQbgpnPdHQEMorfnm8q6bWg/291IAV3ZA9Z6T5gn4YuyjeUdDczQtpT6nu
1iTNPqtO6R3aeTVT+OSJT9sH2MHfDAsf371HBM6MzM/5QBc/62Bgau7NUjNKeSEA
EtUBil8wBHwT7vOtqbyNk5FHEfoCpYsQtP7AtEo10izKCQpDXPftfiJefkOY
-----END CERTIFICATE-----

View File

@@ -6,7 +6,6 @@ import (
"embed"
"encoding/pem"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
@@ -15,12 +14,19 @@ import (
"imuslab.com/zoraxy/mod/utils"
)
type Manager struct {
CertStore string
verbal bool
type CertCache struct {
Cert *x509.Certificate
PubKey string
PriKey string
}
//go:embed localhost.crt localhost.key
type Manager struct {
CertStore string //Path where all the certs are stored
LoadedCerts []*CertCache //A list of loaded certs
verbal bool
}
//go:embed localhost.pem localhost.key
var buildinCertStore embed.FS
func NewManager(certStore string, verbal bool) (*Manager, error) {
@@ -28,14 +34,99 @@ func NewManager(certStore string, verbal bool) (*Manager, error) {
os.MkdirAll(certStore, 0775)
}
pubKey := "./tmp/localhost.pem"
priKey := "./tmp/localhost.key"
//Check if this is initial setup
if !utils.FileExists(pubKey) {
buildInPubKey, _ := buildinCertStore.ReadFile(filepath.Base(pubKey))
os.WriteFile(pubKey, buildInPubKey, 0775)
}
if !utils.FileExists(priKey) {
buildInPriKey, _ := buildinCertStore.ReadFile(filepath.Base(priKey))
os.WriteFile(priKey, buildInPriKey, 0775)
}
thisManager := Manager{
CertStore: certStore,
verbal: verbal,
CertStore: certStore,
LoadedCerts: []*CertCache{},
verbal: verbal,
}
err := thisManager.UpdateLoadedCertList()
if err != nil {
return nil, err
}
return &thisManager, nil
}
// Update domain mapping from file
func (m *Manager) UpdateLoadedCertList() error {
//Get a list of certificates from file
domainList, err := m.ListCertDomains()
if err != nil {
return err
}
//Load each of the certificates into memory
certList := []*CertCache{}
for _, certname := range domainList {
//Read their certificate into memory
pubKey := filepath.Join(m.CertStore, certname+".pem")
priKey := filepath.Join(m.CertStore, certname+".key")
certificate, err := tls.LoadX509KeyPair(pubKey, priKey)
if err != nil {
log.Println("Certificate loaded failed: " + certname)
continue
}
for _, thisCert := range certificate.Certificate {
loadedCert, err := x509.ParseCertificate(thisCert)
if err != nil {
//Error pasring cert, skip this byte segment
continue
}
thisCacheEntry := CertCache{
Cert: loadedCert,
PubKey: pubKey,
PriKey: priKey,
}
certList = append(certList, &thisCacheEntry)
}
}
//Replace runtime cert array
m.LoadedCerts = certList
return nil
}
// Match cert by CN
func (m *Manager) CertMatchExists(serverName string) bool {
for _, certCacheEntry := range m.LoadedCerts {
if certCacheEntry.Cert.VerifyHostname(serverName) == nil || certCacheEntry.Cert.Issuer.CommonName == serverName {
return true
}
}
return false
}
// Get cert entry by matching server name, return pubKey and priKey if found
// check with CertMatchExists before calling to the load function
func (m *Manager) GetCertByX509CNHostname(serverName string) (string, string) {
for _, certCacheEntry := range m.LoadedCerts {
if certCacheEntry.Cert.VerifyHostname(serverName) == nil || certCacheEntry.Cert.Issuer.CommonName == serverName {
return certCacheEntry.PubKey, certCacheEntry.PriKey
}
}
return "", ""
}
// Return a list of domains by filename
func (m *Manager) ListCertDomains() ([]string, error) {
filenames, err := m.ListCerts()
if err != nil {
@@ -48,8 +139,9 @@ func (m *Manager) ListCertDomains() ([]string, error) {
return filenames, nil
}
// Return a list of cert files (public and private keys)
func (m *Manager) ListCerts() ([]string, error) {
certs, err := ioutil.ReadDir(m.CertStore)
certs, err := os.ReadDir(m.CertStore)
if err != nil {
return []string{}, err
}
@@ -64,44 +156,52 @@ func (m *Manager) ListCerts() ([]string, error) {
return filenames, nil
}
// Get a certificate from disk where its certificate matches with the helloinfo
func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, error) {
//Check if the domain corrisponding cert exists
pubKey := "./tmp/localhost.crt"
pubKey := "./tmp/localhost.pem"
priKey := "./tmp/localhost.key"
//Check if this is initial setup
if !utils.FileExists(pubKey) {
buildInPubKey, _ := buildinCertStore.ReadFile(filepath.Base(pubKey))
os.WriteFile(pubKey, buildInPubKey, 0775)
}
if !utils.FileExists(priKey) {
buildInPriKey, _ := buildinCertStore.ReadFile(filepath.Base(priKey))
os.WriteFile(priKey, buildInPriKey, 0775)
}
if utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".crt")) && utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".key")) {
pubKey = filepath.Join(m.CertStore, helloInfo.ServerName+".crt")
if utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".pem")) && utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".key")) {
//Direct hit
pubKey = filepath.Join(m.CertStore, helloInfo.ServerName+".pem")
priKey = filepath.Join(m.CertStore, helloInfo.ServerName+".key")
} else if m.CertMatchExists(helloInfo.ServerName) {
//Use x509
pubKey, priKey = m.GetCertByX509CNHostname(helloInfo.ServerName)
} else {
domainCerts, _ := m.ListCertDomains()
cloestDomainCert := matchClosestDomainCertificate(helloInfo.ServerName, domainCerts)
if cloestDomainCert != "" {
//There is a matching parent domain for this subdomain. Use this instead.
pubKey = filepath.Join(m.CertStore, cloestDomainCert+".crt")
priKey = filepath.Join(m.CertStore, cloestDomainCert+".key")
} else if m.DefaultCertExists() {
//Use default.crt and default.key
pubKey = filepath.Join(m.CertStore, "default.crt")
//Fallback to legacy method of matching certificates
/*
domainCerts, _ := m.ListCertDomains()
cloestDomainCert := matchClosestDomainCertificate(helloInfo.ServerName, domainCerts)
if cloestDomainCert != "" {
//There is a matching parent domain for this subdomain. Use this instead.
pubKey = filepath.Join(m.CertStore, cloestDomainCert+".pem")
priKey = filepath.Join(m.CertStore, cloestDomainCert+".key")
} else if m.DefaultCertExists() {
//Use default.pem and default.key
pubKey = filepath.Join(m.CertStore, "default.pem")
priKey = filepath.Join(m.CertStore, "default.key")
if m.verbal {
log.Println("No matching certificate found. Serving with default")
}
} else {
if m.verbal {
log.Println("Matching certificate not found. Serving with build-in certificate. Requesting server name: ", helloInfo.ServerName)
}
}*/
if m.DefaultCertExists() {
//Use default.pem and default.key
pubKey = filepath.Join(m.CertStore, "default.pem")
priKey = filepath.Join(m.CertStore, "default.key")
if m.verbal {
log.Println("No matching certificate found. Serving with default")
}
//if m.verbal {
// log.Println("No matching certificate found. Serving with default")
//}
} else {
if m.verbal {
log.Println("Matching certificate not found. Serving with build-in certificate. Requesting server name: ", helloInfo.ServerName)
}
//if m.verbal {
// log.Println("Matching certificate not found. Serving with build-in certificate. Requesting server name: ", helloInfo.ServerName)
//}
}
}
@@ -117,17 +217,17 @@ func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, err
// Check if both the default cert public key and private key exists
func (m *Manager) DefaultCertExists() bool {
return utils.FileExists(filepath.Join(m.CertStore, "default.crt")) && utils.FileExists(filepath.Join(m.CertStore, "default.key"))
return utils.FileExists(filepath.Join(m.CertStore, "default.pem")) && utils.FileExists(filepath.Join(m.CertStore, "default.key"))
}
// Check if the default cert exists returning seperate results for pubkey and prikey
func (m *Manager) DefaultCertExistsSep() (bool, bool) {
return utils.FileExists(filepath.Join(m.CertStore, "default.crt")), utils.FileExists(filepath.Join(m.CertStore, "default.key"))
return utils.FileExists(filepath.Join(m.CertStore, "default.pem")), utils.FileExists(filepath.Join(m.CertStore, "default.key"))
}
// Delete the cert if exists
func (m *Manager) RemoveCert(domain string) error {
pubKey := filepath.Join(m.CertStore, domain+".crt")
pubKey := filepath.Join(m.CertStore, domain+".pem")
priKey := filepath.Join(m.CertStore, domain+".key")
if utils.FileExists(pubKey) {
err := os.Remove(pubKey)
@@ -143,6 +243,9 @@ func (m *Manager) RemoveCert(domain string) error {
}
}
//Update the cert list
m.UpdateLoadedCertList()
return nil
}
@@ -171,15 +274,11 @@ func IsValidTLSFile(file io.Reader) bool {
return false
}
// Check if the certificate is a valid TLS/SSL certificate
return cert.IsCA == false && cert.KeyUsage&x509.KeyUsageDigitalSignature != 0 && cert.KeyUsage&x509.KeyUsageKeyEncipherment != 0
return !cert.IsCA && cert.KeyUsage&x509.KeyUsageDigitalSignature != 0 && cert.KeyUsage&x509.KeyUsageKeyEncipherment != 0
} else if strings.Contains(block.Type, "PRIVATE KEY") {
// The file contains a private key
_, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
// Handle the error
return false
}
return true
return err == nil
} else {
return false
}

View File

@@ -217,7 +217,11 @@ func getWebsiteStatusWithLatency(url string) (bool, int64, int) {
}
func getWebsiteStatus(url string) (int, error) {
resp, err := http.Get(url)
client := http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Get(url)
if err != nil {
//Try replace the http with https and vise versa
rewriteURL := ""
@@ -227,7 +231,7 @@ func getWebsiteStatus(url string) (int, error) {
rewriteURL = strings.ReplaceAll(url, "http://", "https://")
}
resp, err = http.Get(rewriteURL)
resp, err = client.Get(rewriteURL)
if err != nil {
if strings.Contains(err.Error(), "http: server gave HTTP response to HTTPS client") {
//Invalid downstream reverse proxy settings, but it is online

View File

@@ -72,6 +72,7 @@ func (ws *WebServer) HandlePortChange(w http.ResponseWriter, r *http.Request) {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendOK(w)
}