Updates v2.6 experimental build

+ Basic auth
+ Support TLS verification skip (for self signed certs)
+ Added trend analysis
+ Added referer and file type analysis
+ Added cert expire day display
+ Moved subdomain proxy logic to dpcore
This commit is contained in:
Toby Chui 2023-05-27 11:12:34 +08:00
parent edd19e2b30
commit 5952a1b55f
18 changed files with 792 additions and 145 deletions

3
.gitignore vendored
View File

@ -27,4 +27,5 @@ src/conf/*
src/ReverseProxy_*_*
src/Zoraxy_*_*
src/certs/*
src/rules/*
src/rules/*
src/README.md

View File

@ -1,7 +1,9 @@
package main
import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"log"
@ -41,21 +43,41 @@ func handleListCertificate(w http.ResponseWriter, r *http.Request) {
type CertInfo struct {
Domain string
LastModifiedDate string
ExpireDate string
}
results := []*CertInfo{}
for _, filename := range filenames {
fileInfo, err := os.Stat(filepath.Join(tlsCertManager.CertStore, filename+".crt"))
certFilepath := filepath.Join(tlsCertManager.CertStore, filename+".crt")
//keyFilepath := filepath.Join(tlsCertManager.CertStore, filename+".key")
fileInfo, err := os.Stat(certFilepath)
if err != nil {
utils.SendErrorResponse(w, "invalid domain certificate discovered: "+filename)
return
}
modifiedTime := fileInfo.ModTime().Format("2006-01-02 15:04:05")
certExpireTime := "Unknown"
certBtyes, err := os.ReadFile(certFilepath)
if err != nil {
//Unable to load this file
continue
} else {
//Cert loaded. Check its expire time
block, _ := pem.Decode(certBtyes)
if block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err == nil {
certExpireTime = cert.NotAfter.Format("2006-01-02 15:04:05")
}
}
}
thisCertInfo := CertInfo{
Domain: filename,
LastModifiedDate: modifiedTime,
ExpireDate: certExpireTime,
}
results = append(results, &thisCertInfo)

View File

@ -8,6 +8,7 @@ import (
"path/filepath"
"strings"
"imuslab.com/zoraxy/mod/dynamicproxy"
"imuslab.com/zoraxy/mod/utils"
)
@ -19,23 +20,22 @@ import (
*/
type Record struct {
ProxyType string
Rootname string
ProxyTarget string
UseTLS bool
ProxyType string
Rootname string
ProxyTarget string
UseTLS bool
SkipTlsValidation bool
RequireBasicAuth bool
BasicAuthCredentials []*dynamicproxy.BasicAuthCredentials
}
func SaveReverseProxyConfig(ptype string, rootname string, proxyTarget string, useTLS bool) error {
func SaveReverseProxyConfig(proxyConfigRecord *Record) error {
//TODO: Make this accept new def types
os.MkdirAll("conf", 0775)
filename := getFilenameFromRootName(rootname)
filename := getFilenameFromRootName(proxyConfigRecord.Rootname)
//Generate record
thisRecord := Record{
ProxyType: ptype,
Rootname: rootname,
ProxyTarget: proxyTarget,
UseTLS: useTLS,
}
thisRecord := proxyConfigRecord
//Write to file
js, _ := json.MarshalIndent(thisRecord, "", " ")
@ -67,7 +67,6 @@ func LoadReverseProxyConfig(filename string) (*Record, error) {
}
//Unmarshal the content into config
err = json.Unmarshal(configContent, &thisRecord)
if err != nil {
return &thisRecord, err

View File

@ -38,7 +38,7 @@ var ztAuthToken = flag.String("ztauth", "", "ZeroTier authtoken for the local no
var ztAPIPort = flag.Int("ztport", 9993, "ZeroTier controller API port")
var (
name = "Zoraxy"
version = "2.5"
version = "2.6"
nodeUUID = "generic"
development = false //Set this to false to use embedded web fs

View File

@ -12,9 +12,19 @@ import (
Server.go
Main server for dynamic proxy core
Routing Handler Priority (High to Low)
- Blacklist
- Whitelist
- Redirectable
- Subdomain Routing
- Vitrual Directory Routing
*/
func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
/*
General Access Check
*/
//Check if this ip is in blacklist
clientIpAddr := geodb.GetRequesterIP(r)
if h.Parent.Option.GeodbStore.IsBlacklisted(clientIpAddr) {
@ -30,6 +40,9 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
/*
Redirection Routing
*/
//Check if this is a redirection url
if h.Parent.Option.RedirectRuleTable.IsRedirectable(r) {
statusCode := h.Parent.Option.RedirectRuleTable.HandleRedirect(w, r)
@ -53,21 +66,37 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
domainOnly = hostPath[0]
}
/*
Subdomain Routing
*/
if strings.Contains(r.Host, ".") {
//This might be a subdomain. See if there are any subdomain proxy router for this
//Remove the port if any
sep := h.Parent.getSubdomainProxyEndpointFromHostname(domainOnly)
if sep != nil {
if sep.RequireBasicAuth {
err := h.handleBasicAuthRouting(w, r, sep)
if err != nil {
return
}
}
h.subdomainRequest(w, r, sep)
return
}
}
/*
Virtual Directory Routing
*/
//Clean up the request URI
proxyingPath := strings.TrimSpace(r.RequestURI)
targetProxyEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(proxyingPath)
if targetProxyEndpoint != nil {
if targetProxyEndpoint.RequireBasicAuth {
err := h.handleBasicAuthRouting(w, r, targetProxyEndpoint)
if err != nil {
return
}
}
h.proxyRequest(w, r, targetProxyEndpoint)
} else if !strings.HasSuffix(proxyingPath, "/") {
potentialProxtEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(proxyingPath + "/")
@ -75,11 +104,12 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if potentialProxtEndpoint != nil {
//Missing tailing slash. Redirect to target proxy endpoint
http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
//h.proxyRequest(w, r, potentialProxtEndpoint)
} else {
//Passthrough the request to root
h.proxyRequest(w, r, h.Parent.Root)
}
} else {
//No routing rules found. Route to root.
h.proxyRequest(w, r, h.Parent.Root)
}
}

View File

@ -0,0 +1,47 @@
package dynamicproxy
import (
"errors"
"net/http"
"imuslab.com/zoraxy/mod/auth"
)
/*
BasicAuth.go
This file handles the basic auth on proxy endpoints
if RequireBasicAuth is set to true
*/
func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error {
proxyType := "vdir-auth"
if pe.ProxyType == ProxyType_Subdomain {
proxyType = "subd-auth"
}
u, p, ok := r.BasicAuth()
if !ok {
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
w.WriteHeader(401)
return errors.New("unauthorized")
}
//Check for the credentials to see if there is one matching
hashedPassword := auth.Hash(p)
matchingFound := false
for _, cred := range pe.BasicAuthCredentials {
if u == cred.Username && hashedPassword == cred.PasswordHash {
matchingFound = true
break
}
}
if !matchingFound {
h.logRequest(r, false, 401, proxyType, pe.Domain)
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
w.WriteHeader(401)
return errors.New("unauthorized")
}
return nil
}

View File

@ -71,7 +71,7 @@ type requestCanceler interface {
CancelRequest(req *http.Request)
}
func NewDynamicProxyCore(target *url.URL, prepender string) *ReverseProxy {
func NewDynamicProxyCore(target *url.URL, prepender string, ignoreTLSVerification bool) *ReverseProxy {
targetQuery := target.RawQuery
director := func(req *http.Request) {
req.URL.Scheme = target.Scheme
@ -95,7 +95,12 @@ func NewDynamicProxyCore(target *url.URL, prepender string) *ReverseProxy {
thisTransporter.(*http.Transport).MaxIdleConnsPerHost = 3000
thisTransporter.(*http.Transport).IdleConnTimeout = 10 * time.Second
thisTransporter.(*http.Transport).MaxConnsPerHost = 0
thisTransporter.(*http.Transport).DisableCompression = true
//thisTransporter.(*http.Transport).DisableCompression = true
if ignoreTLSVerification {
//Ignore TLS certificate validation error
thisTransporter.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true
}
return &ReverseProxy{
Director: director,
@ -278,9 +283,6 @@ func addXForwardedForHeader(req *http.Request) {
func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr *ResponseRewriteRuleSet) error {
transport := p.Transport
if transport == nil {
transport = http.DefaultTransport
}
outreq := new(http.Request)
// Shallow copies of maps, like header

View File

@ -5,7 +5,6 @@ import (
"crypto/tls"
"errors"
"log"
"net"
"net/http"
"net/url"
"strconv"
@ -14,57 +13,11 @@ import (
"time"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/reverseproxy"
"imuslab.com/zoraxy/mod/statistic"
"imuslab.com/zoraxy/mod/tlscert"
)
/*
Zoraxy Dynamic Proxy
Zoraxy Dynamic Proxy
*/
type RouterOption struct {
Port int
UseTls bool
ForceHttpsRedirect bool
TlsManager *tlscert.Manager
RedirectRuleTable *redirection.RuleTable
GeodbStore *geodb.Store
StatisticCollector *statistic.Collector
}
type Router struct {
Option *RouterOption
ProxyEndpoints *sync.Map
SubdomainEndpoint *sync.Map
Running bool
Root *ProxyEndpoint
mux http.Handler
server *http.Server
tlsListener net.Listener
routingRules []*RoutingRule
tlsRedirectStop chan bool
}
type ProxyEndpoint struct {
Root string
Domain string
RequireTLS bool
Proxy *dpcore.ReverseProxy `json:"-"`
}
type SubdomainEndpoint struct {
MatchingDomain string
Domain string
RequireTLS bool
Proxy *reverseproxy.ReverseProxy `json:"-"`
}
type ProxyHandler struct {
Parent *Router
}
func NewDynamicProxy(option RouterOption) (*Router, error) {
proxyMap := sync.Map{}
@ -250,8 +203,8 @@ func (router *Router) IsProxiedSubdomain(r *http.Request) bool {
/*
Add an URL into a custom proxy services
*/
func (router *Router) AddVirtualDirectoryProxyService(rootname string, domain string, requireTLS bool) error {
func (router *Router) AddVirtualDirectoryProxyService(options *VdirOptions) error {
domain := options.Domain
if domain[len(domain)-1:] == "/" {
domain = domain[:len(domain)-1]
}
@ -263,7 +216,7 @@ func (router *Router) AddVirtualDirectoryProxyService(rootname string, domain st
*/
webProxyEndpoint := domain
if requireTLS {
if options.RequireTLS {
webProxyEndpoint = "https://" + webProxyEndpoint
} else {
webProxyEndpoint = "http://" + webProxyEndpoint
@ -274,18 +227,22 @@ func (router *Router) AddVirtualDirectoryProxyService(rootname string, domain st
return err
}
proxy := dpcore.NewDynamicProxyCore(path, rootname)
proxy := dpcore.NewDynamicProxyCore(path, options.RootName, options.SkipCertValidations)
endpointObject := ProxyEndpoint{
Root: rootname,
Domain: domain,
RequireTLS: requireTLS,
Proxy: proxy,
ProxyType: ProxyType_Vdir,
RootOrMatchingDomain: options.RootName,
Domain: domain,
RequireTLS: options.RequireTLS,
SkipCertValidations: options.SkipCertValidations,
RequireBasicAuth: options.RequireBasicAuth,
BasicAuthCredentials: options.BasicAuthCredentials,
Proxy: proxy,
}
router.ProxyEndpoints.Store(rootname, &endpointObject)
router.ProxyEndpoints.Store(options.RootName, &endpointObject)
log.Println("Adding Proxy Rule: ", rootname+" to "+domain)
log.Println("Adding Proxy Rule: ", options.RootName+" to "+domain)
return nil
}
@ -307,13 +264,14 @@ func (router *Router) RemoveProxy(ptype string, key string) error {
/*
Add an default router for the proxy server
*/
func (router *Router) SetRootProxy(proxyLocation string, requireTLS bool) error {
func (router *Router) SetRootProxy(options *RootOptions) error {
proxyLocation := options.ProxyLocation
if proxyLocation[len(proxyLocation)-1:] == "/" {
proxyLocation = proxyLocation[:len(proxyLocation)-1]
}
webProxyEndpoint := proxyLocation
if requireTLS {
if options.RequireTLS {
webProxyEndpoint = "https://" + webProxyEndpoint
} else {
webProxyEndpoint = "http://" + webProxyEndpoint
@ -324,13 +282,17 @@ func (router *Router) SetRootProxy(proxyLocation string, requireTLS bool) error
return err
}
proxy := dpcore.NewDynamicProxyCore(path, "")
proxy := dpcore.NewDynamicProxyCore(path, "", options.SkipCertValidations)
rootEndpoint := ProxyEndpoint{
Root: "/",
Domain: proxyLocation,
RequireTLS: requireTLS,
Proxy: proxy,
ProxyType: ProxyType_Vdir,
RootOrMatchingDomain: "/",
Domain: proxyLocation,
RequireTLS: options.RequireTLS,
SkipCertValidations: options.SkipCertValidations,
RequireBasicAuth: options.RequireBasicAuth,
BasicAuthCredentials: options.BasicAuthCredentials,
Proxy: proxy,
}
router.Root = &rootEndpoint
@ -338,14 +300,14 @@ func (router *Router) SetRootProxy(proxyLocation string, requireTLS bool) error
}
// Helpers to export the syncmap for easier processing
func (r *Router) GetSDProxyEndpointsAsMap() map[string]*SubdomainEndpoint {
m := make(map[string]*SubdomainEndpoint)
func (r *Router) GetSDProxyEndpointsAsMap() map[string]*ProxyEndpoint {
m := make(map[string]*ProxyEndpoint)
r.SubdomainEndpoint.Range(func(key, value interface{}) bool {
k, ok := key.(string)
if !ok {
return true
}
v, ok := value.(*SubdomainEndpoint)
v, ok := value.(*ProxyEndpoint)
if !ok {
return true
}

View File

@ -28,23 +28,34 @@ func (router *Router) getTargetProxyEndpointFromRequestURI(requestURI string) *P
return targetProxyEndpoint
}
func (router *Router) getSubdomainProxyEndpointFromHostname(hostname string) *SubdomainEndpoint {
var targetSubdomainEndpoint *SubdomainEndpoint = nil
func (router *Router) getSubdomainProxyEndpointFromHostname(hostname string) *ProxyEndpoint {
var targetSubdomainEndpoint *ProxyEndpoint = nil
ep, ok := router.SubdomainEndpoint.Load(hostname)
if ok {
targetSubdomainEndpoint = ep.(*SubdomainEndpoint)
targetSubdomainEndpoint = ep.(*ProxyEndpoint)
}
return targetSubdomainEndpoint
}
// Clearn URL Path (without the http:// part) replaces // in a URL to /
func (router *Router) clearnURL(targetUrlOPath string) string {
return strings.ReplaceAll(targetUrlOPath, "//", "/")
}
// Rewrite URL rewrite the prefix part of a virtual directory URL with /
func (router *Router) rewriteURL(rooturl string, requestURL string) string {
rewrittenURL := requestURL
rewrittenURL = strings.TrimPrefix(rewrittenURL, strings.TrimSuffix(rooturl, "/"))
if strings.Contains(rewrittenURL, "//") {
rewrittenURL = router.clearnURL(rewrittenURL)
}
return rewrittenURL
}
func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request, target *SubdomainEndpoint) {
// Handle subdomain request
func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
r.Header.Set("X-Forwarded-Host", r.Host)
requestURL := r.URL.String()
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
@ -69,8 +80,20 @@ func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request,
return
}
r.Host = r.URL.Host
err := target.Proxy.ServeHTTP(w, r)
originalHostHeader := r.Host
if r.URL != nil {
r.Host = r.URL.Host
} else {
//Fallback when the upstream proxy screw something up in the header
r.URL, _ = url.Parse(originalHostHeader)
}
err := target.Proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: target.Domain,
OriginalHost: originalHostHeader,
UseTLS: target.RequireTLS,
PathPrefix: "",
})
var dnsError *net.DNSError
if err != nil {
if errors.As(err, &dnsError) {
@ -87,8 +110,9 @@ func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request,
h.logRequest(r, true, 200, "subdomain-http", target.Domain)
}
// Handle vdir type request
func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
rewriteURL := h.Parent.rewriteURL(target.Root, r.RequestURI)
rewriteURL := h.Parent.rewriteURL(target.RootOrMatchingDomain, r.RequestURI)
r.URL, _ = url.Parse(rewriteURL)
r.Header.Set("X-Forwarded-Host", r.Host)
@ -110,12 +134,18 @@ func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, targ
}
originalHostHeader := r.Host
r.Host = r.URL.Host
if r.URL != nil {
r.Host = r.URL.Host
} else {
//Fallback when the upstream proxy screw something up in the header
r.URL, _ = url.Parse(originalHostHeader)
}
err := target.Proxy.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: target.Domain,
OriginalHost: originalHostHeader,
UseTLS: target.RequireTLS,
PathPrefix: target.Root,
PathPrefix: target.RootOrMatchingDomain,
})
var dnsError *net.DNSError

View File

@ -4,7 +4,7 @@ import (
"log"
"net/url"
"imuslab.com/zoraxy/mod/reverseproxy"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
)
/*
@ -12,13 +12,14 @@ import (
*/
func (router *Router) AddSubdomainRoutingService(hostnameWithSubdomain string, domain string, requireTLS bool) error {
func (router *Router) AddSubdomainRoutingService(options *SubdOptions) error {
domain := options.Domain
if domain[len(domain)-1:] == "/" {
domain = domain[:len(domain)-1]
}
webProxyEndpoint := domain
if requireTLS {
if options.RequireTLS {
webProxyEndpoint = "https://" + webProxyEndpoint
} else {
webProxyEndpoint = "http://" + webProxyEndpoint
@ -30,15 +31,18 @@ func (router *Router) AddSubdomainRoutingService(hostnameWithSubdomain string, d
return err
}
proxy := reverseproxy.NewReverseProxy(path)
proxy := dpcore.NewDynamicProxyCore(path, "", options.SkipCertValidations)
router.SubdomainEndpoint.Store(hostnameWithSubdomain, &SubdomainEndpoint{
MatchingDomain: hostnameWithSubdomain,
Domain: domain,
RequireTLS: requireTLS,
Proxy: proxy,
router.SubdomainEndpoint.Store(options.MatchingDomain, &ProxyEndpoint{
RootOrMatchingDomain: options.MatchingDomain,
Domain: domain,
RequireTLS: options.RequireTLS,
Proxy: proxy,
SkipCertValidations: options.SkipCertValidations,
RequireBasicAuth: options.RequireBasicAuth,
BasicAuthCredentials: options.BasicAuthCredentials,
})
log.Println("Adding Subdomain Rule: ", hostnameWithSubdomain+" to "+domain)
log.Println("Adding Subdomain Rule: ", options.MatchingDomain+" to "+domain)
return nil
}

View File

@ -0,0 +1,112 @@
package dynamicproxy
import (
"net"
"net/http"
"sync"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/statistic"
"imuslab.com/zoraxy/mod/tlscert"
)
const (
ProxyType_Subdomain = 0
ProxyType_Vdir = 1
)
type ProxyHandler struct {
Parent *Router
}
type RouterOption struct {
Port int
UseTls bool
ForceHttpsRedirect bool
TlsManager *tlscert.Manager
RedirectRuleTable *redirection.RuleTable
GeodbStore *geodb.Store
StatisticCollector *statistic.Collector
}
type Router struct {
Option *RouterOption
ProxyEndpoints *sync.Map
SubdomainEndpoint *sync.Map
Running bool
Root *ProxyEndpoint
mux http.Handler
server *http.Server
tlsListener net.Listener
routingRules []*RoutingRule
tlsRedirectStop chan bool
}
// Auth credential for basic auth on certain endpoints
type BasicAuthCredentials struct {
Username string
PasswordHash string
}
// Auth credential for basic auth on certain endpoints
type BasicAuthUnhashedCredentials struct {
Username string
Password string
}
// A proxy endpoint record
type ProxyEndpoint struct {
ProxyType int //The type of this proxy, see const def
RootOrMatchingDomain string //Root for vdir or Matching domain for subd
Domain string //Domain or IP to proxy to
RequireTLS bool //Target domain require TLS
SkipCertValidations bool //Set to true to accept self signed certs
RequireBasicAuth bool //Set to true to request basic auth before proxy
BasicAuthCredentials []*BasicAuthCredentials `json:"-"`
Proxy *dpcore.ReverseProxy `json:"-"`
}
type RootOptions struct {
ProxyLocation string
RequireTLS bool
SkipCertValidations bool
RequireBasicAuth bool
BasicAuthCredentials []*BasicAuthCredentials
}
type VdirOptions struct {
RootName string
Domain string
RequireTLS bool
SkipCertValidations bool
RequireBasicAuth bool
BasicAuthCredentials []*BasicAuthCredentials
}
type SubdOptions struct {
MatchingDomain string
Domain string
RequireTLS bool
SkipCertValidations bool
RequireBasicAuth bool
BasicAuthCredentials []*BasicAuthCredentials
}
/*
type ProxyEndpoint struct {
Root string
Domain string
RequireTLS bool
Proxy *reverseproxy.ReverseProxy `json:"-"`
}
type SubdomainEndpoint struct {
MatchingDomain string
Domain string
RequireTLS bool
Proxy *reverseproxy.ReverseProxy `json:"-"`
}
*/

View File

@ -10,6 +10,7 @@ import (
"strings"
"time"
"imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/dynamicproxy"
"imuslab.com/zoraxy/mod/uptime"
"imuslab.com/zoraxy/mod/utils"
@ -71,24 +72,33 @@ func ReverseProxtInit() {
}
if record.ProxyType == "root" {
dynamicProxyRouter.SetRootProxy(record.ProxyTarget, record.UseTLS)
dynamicProxyRouter.SetRootProxy(&dynamicproxy.RootOptions{
ProxyLocation: record.ProxyTarget,
RequireTLS: record.UseTLS,
})
} else if record.ProxyType == "subd" {
dynamicProxyRouter.AddSubdomainRoutingService(record.Rootname, record.ProxyTarget, record.UseTLS)
dynamicProxyRouter.AddSubdomainRoutingService(&dynamicproxy.SubdOptions{
MatchingDomain: record.Rootname,
Domain: record.ProxyTarget,
RequireTLS: record.UseTLS,
SkipCertValidations: record.SkipTlsValidation,
RequireBasicAuth: record.RequireBasicAuth,
BasicAuthCredentials: record.BasicAuthCredentials,
})
} else if record.ProxyType == "vdir" {
dynamicProxyRouter.AddVirtualDirectoryProxyService(record.Rootname, record.ProxyTarget, record.UseTLS)
dynamicProxyRouter.AddVirtualDirectoryProxyService(&dynamicproxy.VdirOptions{
RootName: record.Rootname,
Domain: record.ProxyTarget,
RequireTLS: record.UseTLS,
SkipCertValidations: record.SkipTlsValidation,
RequireBasicAuth: record.RequireBasicAuth,
BasicAuthCredentials: record.BasicAuthCredentials,
})
} else {
log.Println("Unsupported endpoint type: " + record.ProxyType + ". Skipping " + filepath.Base(conf))
}
}
/*
dynamicProxyRouter.SetRootProxy("192.168.0.107:8080", false)
dynamicProxyRouter.AddSubdomainRoutingService("aroz.localhost", "192.168.0.107:8080/private/AOB/", false)
dynamicProxyRouter.AddSubdomainRoutingService("loopback.localhost", "localhost:8080", false)
dynamicProxyRouter.AddSubdomainRoutingService("git.localhost", "mc.alanyeung.co:3000", false)
dynamicProxyRouter.AddVirtualDirectoryProxyService("/git/server/", "mc.alanyeung.co:3000", false)
*/
//Start Service
//Not sure why but delay must be added if you have another
//reverse proxy server in front of this service
@ -111,7 +121,6 @@ func ReverseProxtInit() {
}
func ReverseProxyHandleOnOff(w http.ResponseWriter, r *http.Request) {
enable, _ := utils.PostPara(r, "enable") //Support root, vdir and subd
if enable == "true" {
err := dynamicProxyRouter.StartProxyService()
@ -157,6 +166,49 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
}
useTLS := (tls == "true")
stv, _ := utils.PostPara(r, "tlsval")
if stv == "" {
stv = "false"
}
skipTlsValidation := (stv == "true")
rba, _ := utils.PostPara(r, "bauth")
if rba == "" {
rba = "false"
}
requireBasicAuth := (rba == "true")
//Prase the basic auth to correct structure
cred, _ := utils.PostPara(r, "cred")
basicAuthCredentials := []*dynamicproxy.BasicAuthCredentials{}
if requireBasicAuth {
preProcessCredentials := []*dynamicproxy.BasicAuthUnhashedCredentials{}
err = json.Unmarshal([]byte(cred), &preProcessCredentials)
if err != nil {
utils.SendErrorResponse(w, "invalid user credentials")
return
}
//Check if there are empty password credentials
for _, credObj := range preProcessCredentials {
if strings.TrimSpace(credObj.Password) == "" {
utils.SendErrorResponse(w, credObj.Username+" has empty password")
return
}
}
//Convert and hash the passwords
for _, credObj := range preProcessCredentials {
basicAuthCredentials = append(basicAuthCredentials, &dynamicproxy.BasicAuthCredentials{
Username: credObj.Username,
PasswordHash: auth.Hash(credObj.Password),
})
}
}
rootname := ""
if eptype == "vdir" {
vdir, err := utils.PostPara(r, "rootname")
@ -170,7 +222,16 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
vdir = "/" + vdir
}
rootname = vdir
dynamicProxyRouter.AddVirtualDirectoryProxyService(vdir, endpoint, useTLS)
thisOption := dynamicproxy.VdirOptions{
RootName: vdir,
Domain: endpoint,
RequireTLS: useTLS,
SkipCertValidations: skipTlsValidation,
RequireBasicAuth: requireBasicAuth,
BasicAuthCredentials: basicAuthCredentials,
}
dynamicProxyRouter.AddVirtualDirectoryProxyService(&thisOption)
} else if eptype == "subd" {
subdomain, err := utils.PostPara(r, "rootname")
@ -179,10 +240,22 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
return
}
rootname = subdomain
dynamicProxyRouter.AddSubdomainRoutingService(subdomain, endpoint, useTLS)
thisOption := dynamicproxy.SubdOptions{
MatchingDomain: subdomain,
Domain: endpoint,
RequireTLS: useTLS,
SkipCertValidations: skipTlsValidation,
RequireBasicAuth: requireBasicAuth,
BasicAuthCredentials: basicAuthCredentials,
}
dynamicProxyRouter.AddSubdomainRoutingService(&thisOption)
} else if eptype == "root" {
rootname = "root"
dynamicProxyRouter.SetRootProxy(endpoint, useTLS)
thisOption := dynamicproxy.RootOptions{
ProxyLocation: endpoint,
RequireTLS: useTLS,
}
dynamicProxyRouter.SetRootProxy(&thisOption)
} else {
//Invalid eptype
utils.SendErrorResponse(w, "Invalid endpoint type")
@ -190,7 +263,16 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
}
//Save it
SaveReverseProxyConfig(eptype, rootname, endpoint, useTLS)
thisProxyConfigRecord := Record{
ProxyType: eptype,
Rootname: rootname,
ProxyTarget: endpoint,
UseTLS: useTLS,
SkipTlsValidation: skipTlsValidation,
RequireBasicAuth: requireBasicAuth,
BasicAuthCredentials: basicAuthCredentials,
}
SaveReverseProxyConfig(&thisProxyConfigRecord)
//Update utm if exists
if uptimeMonitor != nil {
@ -255,14 +337,14 @@ func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
js, _ := json.Marshal(results)
utils.SendJSONResponse(w, string(js))
} else if eptype == "subd" {
results := []*dynamicproxy.SubdomainEndpoint{}
results := []*dynamicproxy.ProxyEndpoint{}
dynamicProxyRouter.SubdomainEndpoint.Range(func(key, value interface{}) bool {
results = append(results, value.(*dynamicproxy.SubdomainEndpoint))
results = append(results, value.(*dynamicproxy.ProxyEndpoint))
return true
})
sort.Slice(results, func(i, j int) bool {
return results[i].MatchingDomain < results[j].MatchingDomain
return results[i].RootOrMatchingDomain < results[j].RootOrMatchingDomain
})
js, _ := json.Marshal(results)

View File

@ -86,10 +86,6 @@ func startupSequence() {
panic(err)
}
if err != nil {
panic(err)
}
//Create a netstat buffer
netstatBuffers, err = netstat.NewNetStatBuffer(300)
if err != nil {

View File

@ -58,6 +58,7 @@
<thead>
<tr><th>Domain</th>
<th>Last Update</th>
<th>Expire At</th>
<th class="no-sort">Remove</th>
</tr></thead>
<tbody id="certifiedDomainList">
@ -108,6 +109,7 @@
$("#certifiedDomainList").append(`<tr>
<td>${entry.Domain}</td>
<td>${entry.LastModifiedDate}</td>
<td>${entry.ExpireDate}</td>
<td><button title="Delete key-pair" class="ui mini basic red icon button" onclick="deleteCertificate('${entry.Domain}');"><i class="ui red trash icon"></i></button></td>
</tr>`);
})

View File

@ -20,7 +20,6 @@
<div class="field">
<label>Subdomain Matching Keyword / Virtual Directory Name</label>
<input type="text" id="rootname" placeholder="s1.mydomain.com">
</div>
<div class="field">
<label>IP Address or Domain Name with port</label>
@ -33,6 +32,58 @@
<label>Proxy Target require TLS Connection <br><small>(i.e. Your proxy target starts with https://)</small></label>
</div>
</div>
<!-- Advance configs -->
<div class="ui basic segment" style="background-color: #f7f7f7; border-radius: 1em;">
<div id="advanceProxyRules" class="ui fluid accordion">
<div class="title">
<i class="dropdown icon"></i>
Advance Settings
</div>
<div class="content">
<p></p>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="skipTLSValidation">
<label>Ignore TLS/SSL Verification Error<br><small>E.g. self-signed, expired certificate (Not Recommended)</small></label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="requireBasicAuth">
<label>Require Basic Auth<br><small>Require client to login in order to view the page</small></label>
</div>
</div>
<div id="basicAuthCredentials" class="field">
<p>Enter the username and password for allowing them to access this proxy endpoint</p>
<table class="ui very basic celled table">
<thead>
<tr>
<th>Username</th>
<th>Password</th>
<th>Remove</th>
</tr></thead>
<tbody id="basicAuthCredentialTable">
<tr>
<td colspan="3"><i class="ui green circle check icon"></i> No Entered Credential</td>
</tr>
</tbody>
</table>
<div class="three small fields credentialEntry">
<div class="field">
<input id="basicAuthCredUsername" type="text" placeholder="Username" autocomplete="off">
</div>
<div class="field">
<input id="basicAuthCredPassword" type="password" placeholder="Password" autocomplete="off">
</div>
<div class="field">
<button class="ui basic button" onclick="addCredentials();"><i class="blue add icon"></i> Add Credential</button>
</div>
</div>
</div>
</div>
</div>
</div>
<br>
<button class="ui basic button" onclick="newProxyEndpoint();"><i class="blue add icon"></i> Create Endpoint</button>
<br><br>
</div>
@ -63,12 +114,16 @@
</div>
</div>
<script>
$("#advanceProxyRules").accordion();
//New Proxy Endpoint
function newProxyEndpoint(){
var type = $("#ptype").val();
var rootname = $("#rootname").val();
var proxyDomain = $("#proxyDomain").val();
var useTLS = $("#reqTls")[0].checked;
var skipTLSValidation = $("#skipTLSValidation")[0].checked;
var requireBasicAuth = $("#requireBasicAuth")[0].checked;
if (type === "vdir") {
if (!rootname.startsWith("/")) {
@ -101,7 +156,15 @@
//Create the endpoint by calling add
$.ajax({
url: "/api/proxy/add",
data: {type: type, rootname: rootname, tls: useTLS, ep: proxyDomain},
data: {
type: type,
rootname: rootname,
tls: useTLS,
ep: proxyDomain,
tlsval: skipTLSValidation,
bauth: requireBasicAuth,
cred: JSON.stringify(credentials),
},
success: function(data){
if (data.error != undefined){
msgbox(data.error, false, 5000);
@ -114,6 +177,8 @@
//Clear old data
$("#rootname").val("");
$("#proxyDomain").val("");
credentials = [];
updateTable();
}
}
});
@ -152,6 +217,83 @@
}
function toggleBasicAuth() {
var basicAuthDiv = document.getElementById('basicAuthOnly');
if ($("#requireBasicAuth").parent().checkbox("is checked")) {
$("#basicAuthCredentials").removeClass("disabled");
} else {
$("#basicAuthCredentials").addClass("disabled");
}
}
$("#requireBasicAuth").on('change', toggleBasicAuth);
toggleBasicAuth();
/*
Credential Managements
*/
let credentials = []; // Global variable to store credentials
function addCredentials() {
// Retrieve the username and password input values
var username = $('#basicAuthCredUsername').val();
var password = $('#basicAuthCredPassword').val();
if(username == "" || password == ""){
msgbox("Username or password cannot be empty", false, 5000);
return;
}
// Create a new credential object
var credential = {
username: username,
password: password
};
// Add the credential to the global credentials array
credentials.push(credential);
// Clear the input fields
$('#basicAuthCredUsername').val('');
$('#basicAuthCredPassword').val('');
// Update the table body with the credentials
updateTable();
}
function updateTable() {
var tableBody = $('#basicAuthCredentialTable');
tableBody.empty();
if (credentials.length === 0) {
tableBody.append('<tr><td colspan="3"><i class="ui green circle check icon"></i> No Entered Credential</td></tr>');
} else {
for (var i = 0; i < credentials.length; i++) {
var credential = credentials[i];
var username = credential.username;
var password = credential.password.replace(/./g, '*'); // Replace each character with '*'
var row = '<tr>' +
'<td>' + username + '</td>' +
'<td>' + password + '</td>' +
'<td><button class="ui basic button" onclick="removeCredential(' + i + ');"><i class="red remove icon"></i> Remove</button></td>' +
'</tr>';
tableBody.append(row);
}
}
}
function removeCredential(index) {
// Remove the credential from the credentials array
credentials.splice(index, 1);
// Update the table body
updateTable();
}
//Check if a string is a valid subdomain
function isSubdomainDomain(str) {
const regex = /^(localhost|[a-z0-9]+([\-.]{1}[a-z0-9]+)*\.[a-z]{2,}|[a-z0-9]+([\-.]{1}[a-z0-9]+)*\.[a-z]{2,}\.)$/i;

View File

@ -157,6 +157,42 @@
</div>
</div>
</div>
<div class="ui divider"></div>
<div class="ui stackable grid">
<div class="eight wide column">
<h3>Request File Types</h3>
<p>The file types being served by this proxy</p>
<div>
<canvas id="stats_filetype"></canvas>
</div>
</div>
<div class="eight wide column">
<h3>Referring Sites</h3>
<p>The Top 100 sources of traffic according to referer header</p>
<div>
<div style="height: 500px; overflow-y: auto;">
<table class="ui unstackable striped celled table">
<thead>
<tr>
<th class="no-sort">Referer</th>
<th class="no-sort">Requests</th>
</tr></thead>
<tbody id="stats_RefererTable">
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="ui divider"></div>
<div class="ui basic segment" id="trendGraphs">
<h3>Visitor Trend Analysis</h3>
<p>Request trends in the selected time range</p>
<div>
<canvas id="requestTrends"></canvas>
</div>
</div>
</div>
<!-- <button class="ui icon right floated basic button" onclick="initStatisticSummery();"><i class="green refresh icon"></i> Refresh</button> -->
<br><br>
@ -177,7 +213,6 @@
//Two dates are given and they are not identical
loadStatisticByRange(startdate, enddate);
console.log(startdate, enddate);
}
}
@ -219,6 +254,15 @@
//Render user agent analysis
renderUserAgentCharts(data.UserAgent);
//Render file type by analysising request URL paths
renderFileTypeGraph(data.RequestURL);
//Render Referer header
renderRefererTable(data.Referer);
//Hide the trend graphs
$("#trendGraphs").hide();
});
}
initStatisticSummery();
@ -259,7 +303,16 @@
//Render user agent analysis
renderUserAgentCharts(data.Summary.UserAgent);
//Render file type by analysising request URL paths
renderFileTypeGraph(data.Summary.RequestURL);
//Render Referer header
renderRefererTable(data.Summary.Referer);
//Render the trend graph
$("#trendGraphs").show();
renderTrendGraph(data.Records);
});
}
@ -313,6 +366,155 @@
$("#statsRangeEnd").val("");
}
function renderRefererTable(refererList){
const sortedEntries = Object.entries(refererList).sort(([, valueA], [, valueB]) => valueB - valueA);
console.log(sortedEntries);
$("#stats_RefererTable").html("");
let endStop = 100;
if (sortedEntries.length < 100){
endStop = sortedEntries.length;
}
for (var i = 0; i < endStop; i++) {
let referer = (decodeURIComponent(sortedEntries[i][0])).replace(/(<([^>]+)>)/ig,"");
if (sortedEntries[i][0] == ""){
//Root
referer = `<span style="color: #b5b5b5;">(<i class="eye slash outline icon"></i> Unknown or Hidden)</span>`;
}
$("#stats_RefererTable").append(`<tr>
<td>${referer}</td>
<td>${sortedEntries[i][1]}</td>
</tr>`);
}
}
function renderFileTypeGraph(requestURLs){
//Create the device chart
let fileExtensions = {};
for (const [url, count] of Object.entries(requestURLs)) {
let filename = url.split("/").pop();
let ext = "";
if (filename == ""){
//Loading from a folder
ext = "Folder path"
}else{
if (filename.includes(".")){
ext = filename.split(".").pop();
}else{
ext = "API call"
}
}
if (fileExtensions[ext] != undefined){
fileExtensions[ext] = fileExtensions[ext] + count;
}else{
//First time this ext show up
fileExtensions[ext] = count;
}
}
//Convert the key-value pairs to array for graph render
let fileTypes = [];
let fileCounts = [];
let colors = [];
for (const [ftype, count] of Object.entries(fileExtensions)) {
fileTypes.push(ftype);
fileCounts.push(count);
colors.push(generateColorFromHash(ftype));
}
let filetypeChart = new Chart(document.getElementById("stats_filetype"), {
type: 'pie',
data: {
labels: fileTypes,
datasets: [{
data: fileCounts,
backgroundColor: colors,
hoverBackgroundColor: colors,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
}
});
statisticCharts.push(filetypeChart);
}
function renderTrendGraph(dailySummary){
// Get the canvas element
const canvas = document.getElementById('requestTrends');
//Generate the X axis labels
let datesLabel = [];
let succData = [];
let errorData = [];
let totalData = [];
for (var i = 0; i < dailySummary.length; i++){
let thisDayData = dailySummary[i];
datesLabel.push("Day " + i);
succData.push(thisDayData.ValidRequest);
errorData.push(thisDayData.ErrorRequest);
totalData.push(thisDayData.TotalRequest);
}
// Create the chart
let TrendChart = new Chart(canvas, {
type: 'line',
data: {
labels: datesLabel,
datasets: [
{
label: 'All Requests',
data: totalData,
borderColor: '#7d99f7',
backgroundColor: 'rgba(125, 153, 247, 0.4)',
fill: false
},
{
label: 'Success Requests',
data: succData,
borderColor: '#6dad7c',
backgroundColor: "rgba(109, 173, 124, 0.4)",
fill: true
},
{
label: 'Error Requests',
data: errorData,
borderColor: '#de7373',
backgroundColor: "rgba(222, 115, 115, 0.4)",
fill: true
},
]
},
options: {
responsive: true,
maintainAspectRatio: true,
title: {
display: true,
//text: 'Line Chart Example'
},
scales: {
x: {
display: true,
title: {
display: false,
text: 'Time'
}
},
y: {
display: true,
title: {
display: true,
text: 'Request Counts'
}
}
}
}
});
statisticCharts.push(TrendChart);
}
function renderUserAgentCharts(userAgentsEntries){
let userAgents = Object.keys(userAgentsEntries);
let requestCounts = Object.values(userAgentsEntries);

View File

@ -9,7 +9,9 @@
<tr>
<th>Matching Domain</th>
<th>Proxy To</th>
<th class="no-sort">Remove</th>
<th>TLS/SSL Verification</th>
<th>Basic Auth</th>
<th class="no-sort" style="min-width: 7.2em;">Actions</th>
</tr>
</thead>
<tbody id="subdList">
@ -36,12 +38,17 @@
data.forEach(subd => {
let tlsIcon = "";
if (subd.RequireTLS){
tlsIcon = `<i class="lock icon"></i>`;
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
}
$("#subdList").append(`<tr>
<td data-label="">${subd.MatchingDomain}</td>
<td data-label="">${subd.RootOrMatchingDomain}</td>
<td data-label="">${subd.Domain} ${tlsIcon}</td>
<td class="center aligned" data-label=""><button class="ui circular mini red basic icon button" onclick='deleteEndpoint("subd","${subd.MatchingDomain}")'><i class="trash icon"></i></button></td>
<td data-label="">${!subd.SkipCertValidations?`<i class="ui green check icon"></i>`:`<i class="ui yellow exclamation circle icon" title="TLS/SSL Verification will be skipped on this host"></i>`}</td>
<td data-label="">${subd.RequireBasicAuth?`<i class="ui green check icon"></i>`:`<i class="ui grey remove icon"></i>`}</td>
<td class="center aligned" data-label="">
<button class="ui circular mini basic icon button" onclick='editEndpoint("subd","${subd.RootOrMatchingDomain}")'><i class="edit icon"></i></button>
<button class="ui circular mini red basic icon button" onclick='deleteEndpoint("subd","${subd.RootOrMatchingDomain}")'><i class="trash icon"></i></button>
</td>
</tr>`);
});
}

View File

@ -9,7 +9,9 @@
<tr>
<th>Virtual Directory</th>
<th>Proxy To</th>
<th class="no-sort">Remove</th>
<th>TLS/SSL Verification</th>
<th>Basic Auth</th>
<th class="no-sort" style="min-width: 7.2em;">Actions</th>
</tr>
</thead>
<tbody id="vdirList">
@ -39,12 +41,17 @@
data.forEach(vdir => {
let tlsIcon = "";
if (vdir.RequireTLS){
tlsIcon = `<i title="TLS mode" class="lock icon"></i>`;
tlsIcon = `<i class="green lock icon" title="TLS Mode"></i>`;
}
$("#vdirList").append(`<tr>
<td data-label="">${vdir.Root}</td>
<td data-label="">${vdir.RootOrMatchingDomain}</td>
<td data-label="">${vdir.Domain} ${tlsIcon}</td>
<td class="center aligned" data-label=""><button class="ui circular mini red basic icon button" onclick='deleteEndpoint("vdir","${vdir.Root}")'><i class="trash icon"></i></button></td>
<td data-label="">${!subd.SkipCertValidations?`<i class="ui green check icon"></i>`:`<i class="ui yellow exclamation circle icon" title="TLS/SSL Verification will be skipped on this host"></i>`}</td>
<td data-label="">${subd.RequireBasicAuth?`<i class="ui green check icon"></i>`:`<i class="ui grey remove icon"></i>`}</td>
<td class="center aligned" data-label="">
<button class="ui circular mini basic icon button" onclick='editEndpoint("vdir","${vdir.RootOrMatchingDomain}")'><i class="edit icon"></i></button>
<button class="ui circular mini red basic icon button" onclick='deleteEndpoint("vdir","${vdir.RootOrMatchingDomain}")'><i class="trash icon"></i></button>
</td>
</tr>`);
});
}