mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-03 06:07:20 +02:00
Added apache compatible logger
- Rewritten the logger to make it more apache log parser friendly - Fixed uptime not updating after upstream change bug - Added SSO page (wip) - Added log viewer
This commit is contained in:
parent
e410b92e34
commit
8239f4cb53
@ -148,7 +148,7 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/gan/members/authorize", ganManager.HandleMemberAuthorization)
|
||||
authRouter.HandleFunc("/api/gan/members/delete", ganManager.HandleMemberDelete)
|
||||
|
||||
//TCP Proxy
|
||||
//Stream (TCP / UDP) Proxy
|
||||
authRouter.HandleFunc("/api/streamprox/config/add", streamProxyManager.HandleAddProxyConfig)
|
||||
authRouter.HandleFunc("/api/streamprox/config/edit", streamProxyManager.HandleEditProxyConfigs)
|
||||
authRouter.HandleFunc("/api/streamprox/config/list", streamProxyManager.HandleListConfigs)
|
||||
@ -229,12 +229,13 @@ func initAPIs() {
|
||||
authRouter.HandleFunc("/api/info/geoip", HandleGeoIpLookup)
|
||||
authRouter.HandleFunc("/api/conf/export", ExportConfigAsZip)
|
||||
authRouter.HandleFunc("/api/conf/import", ImportConfigFromZip)
|
||||
authRouter.HandleFunc("/api/log/list", LogViewer.HandleListLog)
|
||||
authRouter.HandleFunc("/api/log/read", LogViewer.HandleReadLog)
|
||||
|
||||
//Debug
|
||||
authRouter.HandleFunc("/api/info/pprof", pprof.Index)
|
||||
|
||||
//If you got APIs to add, append them here
|
||||
// get available docker containers
|
||||
|
||||
}
|
||||
|
||||
|
@ -80,7 +80,7 @@ func LoadReverseProxyConfig(configFilepath string) error {
|
||||
return errors.New("not supported proxy type")
|
||||
}
|
||||
|
||||
SystemWideLogger.PrintAndLog("Proxy", thisConfigEndpoint.RootOrMatchingDomain+" -> "+loadbalance.GetUpstreamsAsString(thisConfigEndpoint.ActiveOrigins)+" routing rule loaded", nil)
|
||||
SystemWideLogger.PrintAndLog("proxy-config", thisConfigEndpoint.RootOrMatchingDomain+" -> "+loadbalance.GetUpstreamsAsString(thisConfigEndpoint.ActiveOrigins)+" routing rule loaded", nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
34
src/main.go
34
src/main.go
@ -24,6 +24,7 @@ import (
|
||||
"imuslab.com/zoraxy/mod/ganserv"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/info/logviewer"
|
||||
"imuslab.com/zoraxy/mod/mdns"
|
||||
"imuslab.com/zoraxy/mod/netstat"
|
||||
"imuslab.com/zoraxy/mod/pathrule"
|
||||
@ -52,14 +53,13 @@ var acmeAutoRenewInterval = flag.Int("autorenew", 86400, "ACME auto TLS/SSL cert
|
||||
var enableHighSpeedGeoIPLookup = flag.Bool("fastgeoip", false, "Enable high speed geoip lookup, require 1GB extra memory (Not recommend for low end devices)")
|
||||
var staticWebServerRoot = flag.String("webroot", "./www", "Static web server root folder. Only allow chnage in start paramters")
|
||||
var allowWebFileManager = flag.Bool("webfm", true, "Enable web file manager for static web server root folder")
|
||||
var logOutputToFile = flag.Bool("log", true, "Log terminal output to file")
|
||||
var enableAutoUpdate = flag.Bool("cfgupgrade", true, "Enable auto config upgrade if breaking change is detected")
|
||||
|
||||
var (
|
||||
name = "Zoraxy"
|
||||
version = "3.0.8"
|
||||
nodeUUID = "generic" //System uuid, in uuidv4 format
|
||||
development = true //Set this to false to use embedded web fs
|
||||
development = false //Set this to false to use embedded web fs
|
||||
bootTime = time.Now().Unix()
|
||||
|
||||
/*
|
||||
@ -97,6 +97,7 @@ var (
|
||||
AnalyticLoader *analytic.DataLoader //Data loader for Zoraxy Analytic
|
||||
DockerUXOptimizer *dockerux.UXOptimizer //Docker user experience optimizer, community contribution only
|
||||
SystemWideLogger *logger.Logger //Logger for Zoraxy
|
||||
LogViewer *logviewer.Viewer
|
||||
)
|
||||
|
||||
// Kill signal handler. Do something before the system the core terminate.
|
||||
@ -111,33 +112,34 @@ func SetupCloseHandler() {
|
||||
}
|
||||
|
||||
func ShutdownSeq() {
|
||||
fmt.Println("- Shutting down " + name)
|
||||
fmt.Println("- Closing GeoDB ")
|
||||
SystemWideLogger.Println("Shutting down " + name)
|
||||
SystemWideLogger.Println("Closing GeoDB ")
|
||||
geodbStore.Close()
|
||||
fmt.Println("- Closing Netstats Listener")
|
||||
SystemWideLogger.Println("Closing Netstats Listener")
|
||||
netstatBuffers.Close()
|
||||
fmt.Println("- Closing Statistic Collector")
|
||||
SystemWideLogger.Println("Closing Statistic Collector")
|
||||
statisticCollector.Close()
|
||||
if mdnsTickerStop != nil {
|
||||
fmt.Println("- Stopping mDNS Discoverer (might take a few minutes)")
|
||||
SystemWideLogger.Println("Stopping mDNS Discoverer (might take a few minutes)")
|
||||
// Stop the mdns service
|
||||
mdnsTickerStop <- true
|
||||
}
|
||||
mdnsScanner.Close()
|
||||
fmt.Println("- Shutting down load balancer")
|
||||
SystemWideLogger.Println("Shutting down load balancer")
|
||||
loadBalancer.Close()
|
||||
fmt.Println("- Closing Certificates Auto Renewer")
|
||||
SystemWideLogger.Println("Closing Certificates Auto Renewer")
|
||||
acmeAutoRenewer.Close()
|
||||
//Remove the tmp folder
|
||||
fmt.Println("- Cleaning up tmp files")
|
||||
SystemWideLogger.Println("Cleaning up tmp files")
|
||||
os.RemoveAll("./tmp")
|
||||
|
||||
fmt.Println("- Closing system wide logger")
|
||||
SystemWideLogger.Close()
|
||||
|
||||
//Close database, final
|
||||
fmt.Println("- Stopping system database")
|
||||
//Close database
|
||||
SystemWideLogger.Println("Stopping system database")
|
||||
sysdb.Close()
|
||||
|
||||
//Close logger
|
||||
SystemWideLogger.Println("Closing system wide logger")
|
||||
SystemWideLogger.Close()
|
||||
}
|
||||
|
||||
func main() {
|
||||
@ -154,7 +156,7 @@ func main() {
|
||||
}
|
||||
|
||||
if *enableAutoUpdate {
|
||||
log.Println("[INFO] Checking required config update")
|
||||
fmt.Println("Checking required config update")
|
||||
update.RunConfigUpdate(0, update.GetVersionIntFromVersionNumber(version))
|
||||
}
|
||||
|
||||
|
@ -14,10 +14,10 @@ import (
|
||||
"strings"
|
||||
|
||||
"encoding/hex"
|
||||
"log"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
db "imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
@ -27,6 +27,7 @@ type AuthAgent struct {
|
||||
SessionStore *sessions.CookieStore
|
||||
Database *db.Database
|
||||
LoginRedirectionHandler func(http.ResponseWriter, *http.Request)
|
||||
Logger *logger.Logger
|
||||
}
|
||||
|
||||
type AuthEndpoints struct {
|
||||
@ -37,12 +38,12 @@ type AuthEndpoints struct {
|
||||
Autologin string
|
||||
}
|
||||
|
||||
//Constructor
|
||||
func NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database, allowReg bool, loginRedirectionHandler func(http.ResponseWriter, *http.Request)) *AuthAgent {
|
||||
// Constructor
|
||||
func NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database, allowReg bool, systemLogger *logger.Logger, loginRedirectionHandler func(http.ResponseWriter, *http.Request)) *AuthAgent {
|
||||
store := sessions.NewCookieStore(key)
|
||||
err := sysdb.NewTable("auth")
|
||||
if err != nil {
|
||||
log.Println("Failed to create auth database. Terminating.")
|
||||
systemLogger.Println("Failed to create auth database. Terminating.")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@ -52,13 +53,14 @@ func NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database,
|
||||
SessionStore: store,
|
||||
Database: sysdb,
|
||||
LoginRedirectionHandler: loginRedirectionHandler,
|
||||
Logger: systemLogger,
|
||||
}
|
||||
|
||||
//Return the authAgent
|
||||
return &newAuthAgent
|
||||
}
|
||||
|
||||
func GetSessionKey(sysdb *db.Database) (string, error) {
|
||||
func GetSessionKey(sysdb *db.Database, logger *logger.Logger) (string, error) {
|
||||
sysdb.NewTable("auth")
|
||||
sessionKey := ""
|
||||
if !sysdb.KeyExists("auth", "sessionkey") {
|
||||
@ -66,9 +68,9 @@ func GetSessionKey(sysdb *db.Database) (string, error) {
|
||||
rand.Read(key)
|
||||
sessionKey = string(key)
|
||||
sysdb.Write("auth", "sessionkey", sessionKey)
|
||||
log.Println("[Auth] New authentication session key generated")
|
||||
logger.PrintAndLog("auth", "New authentication session key generated", nil)
|
||||
} else {
|
||||
log.Println("[Auth] Authentication session key loaded from database")
|
||||
logger.PrintAndLog("auth", "Authentication session key loaded from database", nil)
|
||||
err := sysdb.Read("auth", "sessionkey", &sessionKey)
|
||||
if err != nil {
|
||||
return "", errors.New("database read error. Is the database file corrupted?")
|
||||
@ -77,7 +79,7 @@ func GetSessionKey(sysdb *db.Database) (string, error) {
|
||||
return sessionKey, nil
|
||||
}
|
||||
|
||||
//This function will handle an http request and redirect to the given login address if not logged in
|
||||
// This function will handle an http request and redirect to the given login address if not logged in
|
||||
func (a *AuthAgent) HandleCheckAuth(w http.ResponseWriter, r *http.Request, handler func(http.ResponseWriter, *http.Request)) {
|
||||
if a.CheckAuth(r) {
|
||||
//User already logged in
|
||||
@ -88,14 +90,14 @@ func (a *AuthAgent) HandleCheckAuth(w http.ResponseWriter, r *http.Request, hand
|
||||
}
|
||||
}
|
||||
|
||||
//Handle login request, require POST username and password
|
||||
// Handle login request, require POST username and password
|
||||
func (a *AuthAgent) HandleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
//Get username from request using POST mode
|
||||
username, err := utils.PostPara(r, "username")
|
||||
if err != nil {
|
||||
//Username not defined
|
||||
log.Println("[Auth] " + r.RemoteAddr + " trying to login with username: " + username)
|
||||
a.Logger.PrintAndLog("auth", r.RemoteAddr+" trying to login with username: "+username, nil)
|
||||
utils.SendErrorResponse(w, "Username not defined or empty.")
|
||||
return
|
||||
}
|
||||
@ -124,11 +126,11 @@ func (a *AuthAgent) HandleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
a.LoginUserByRequest(w, r, username, rememberme)
|
||||
|
||||
//Print the login message to console
|
||||
log.Println(username + " logged in.")
|
||||
a.Logger.PrintAndLog("auth", username+" logged in.", nil)
|
||||
utils.SendOK(w)
|
||||
} else {
|
||||
//Password incorrect
|
||||
log.Println(username + " login request rejected: " + rejectionReason)
|
||||
a.Logger.PrintAndLog("auth", username+" login request rejected: "+rejectionReason, nil)
|
||||
|
||||
utils.SendErrorResponse(w, rejectionReason)
|
||||
return
|
||||
@ -140,14 +142,14 @@ func (a *AuthAgent) ValidateUsernameAndPassword(username string, password string
|
||||
return succ
|
||||
}
|
||||
|
||||
//validate the username and password, return reasons if the auth failed
|
||||
// validate the username and password, return reasons if the auth failed
|
||||
func (a *AuthAgent) ValidateUsernameAndPasswordWithReason(username string, password string) (bool, string) {
|
||||
hashedPassword := Hash(password)
|
||||
var passwordInDB string
|
||||
err := a.Database.Read("auth", "passhash/"+username, &passwordInDB)
|
||||
if err != nil {
|
||||
//User not found or db exception
|
||||
log.Println("[Auth] " + username + " login with incorrect password")
|
||||
a.Logger.PrintAndLog("auth", username+" login with incorrect password", nil)
|
||||
return false, "Invalid username or password"
|
||||
}
|
||||
|
||||
@ -158,7 +160,7 @@ func (a *AuthAgent) ValidateUsernameAndPasswordWithReason(username string, passw
|
||||
}
|
||||
}
|
||||
|
||||
//Login the user by creating a valid session for this user
|
||||
// Login the user by creating a valid session for this user
|
||||
func (a *AuthAgent) LoginUserByRequest(w http.ResponseWriter, r *http.Request, username string, rememberme bool) {
|
||||
session, _ := a.SessionStore.Get(r, a.SessionName)
|
||||
|
||||
@ -181,11 +183,15 @@ func (a *AuthAgent) LoginUserByRequest(w http.ResponseWriter, r *http.Request, u
|
||||
session.Save(r, w)
|
||||
}
|
||||
|
||||
//Handle logout, reply OK after logged out. WILL NOT DO REDIRECTION
|
||||
// Handle logout, reply OK after logged out. WILL NOT DO REDIRECTION
|
||||
func (a *AuthAgent) HandleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
username, err := a.GetUserName(w, r)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "user not logged in")
|
||||
return
|
||||
}
|
||||
if username != "" {
|
||||
log.Println(username + " logged out.")
|
||||
a.Logger.PrintAndLog("auth", username+" logged out", nil)
|
||||
}
|
||||
// Revoke users authentication
|
||||
err = a.Logout(w, r)
|
||||
@ -194,7 +200,7 @@ func (a *AuthAgent) HandleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
w.Write([]byte("OK"))
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func (a *AuthAgent) Logout(w http.ResponseWriter, r *http.Request) error {
|
||||
@ -208,7 +214,7 @@ func (a *AuthAgent) Logout(w http.ResponseWriter, r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//Get the current session username from request
|
||||
// Get the current session username from request
|
||||
func (a *AuthAgent) GetUserName(w http.ResponseWriter, r *http.Request) (string, error) {
|
||||
if a.CheckAuth(r) {
|
||||
//This user has logged in.
|
||||
@ -220,7 +226,7 @@ func (a *AuthAgent) GetUserName(w http.ResponseWriter, r *http.Request) (string,
|
||||
}
|
||||
}
|
||||
|
||||
//Get the current session user email from request
|
||||
// Get the current session user email from request
|
||||
func (a *AuthAgent) GetUserEmail(w http.ResponseWriter, r *http.Request) (string, error) {
|
||||
if a.CheckAuth(r) {
|
||||
//This user has logged in.
|
||||
@ -239,7 +245,7 @@ func (a *AuthAgent) GetUserEmail(w http.ResponseWriter, r *http.Request) (string
|
||||
}
|
||||
}
|
||||
|
||||
//Check if the user has logged in, return true / false in JSON
|
||||
// Check if the user has logged in, return true / false in JSON
|
||||
func (a *AuthAgent) CheckLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if a.CheckAuth(r) {
|
||||
utils.SendJSONResponse(w, "true")
|
||||
@ -248,7 +254,7 @@ func (a *AuthAgent) CheckLogin(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
//Handle new user register. Require POST username, password, group.
|
||||
// Handle new user register. Require POST username, password, group.
|
||||
func (a *AuthAgent) HandleRegister(w http.ResponseWriter, r *http.Request, callback func(string, string)) {
|
||||
//Get username from request
|
||||
newusername, err := utils.PostPara(r, "username")
|
||||
@ -291,10 +297,10 @@ func (a *AuthAgent) HandleRegister(w http.ResponseWriter, r *http.Request, callb
|
||||
|
||||
//Return to the client with OK
|
||||
utils.SendOK(w)
|
||||
log.Println("[Auth] New user " + newusername + " added to system.")
|
||||
a.Logger.PrintAndLog("auth", "New user "+newusername+" added to system.", nil)
|
||||
}
|
||||
|
||||
//Handle new user register without confirmation email. Require POST username, password, group.
|
||||
// Handle new user register without confirmation email. Require POST username, password, group.
|
||||
func (a *AuthAgent) HandleRegisterWithoutEmail(w http.ResponseWriter, r *http.Request, callback func(string, string)) {
|
||||
//Get username from request
|
||||
newusername, err := utils.PostPara(r, "username")
|
||||
@ -324,10 +330,10 @@ func (a *AuthAgent) HandleRegisterWithoutEmail(w http.ResponseWriter, r *http.Re
|
||||
|
||||
//Return to the client with OK
|
||||
utils.SendOK(w)
|
||||
log.Println("[Auth] Admin account created: " + newusername)
|
||||
a.Logger.PrintAndLog("auth", "Admin account created: "+newusername, nil)
|
||||
}
|
||||
|
||||
//Check authentication from request header's session value
|
||||
// Check authentication from request header's session value
|
||||
func (a *AuthAgent) CheckAuth(r *http.Request) bool {
|
||||
session, err := a.SessionStore.Get(r, a.SessionName)
|
||||
if err != nil {
|
||||
@ -340,8 +346,8 @@ func (a *AuthAgent) CheckAuth(r *http.Request) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
//Handle de-register of users. Require POST username.
|
||||
//THIS FUNCTION WILL NOT CHECK FOR PERMISSION. PLEASE USE WITH PERMISSION HANDLER
|
||||
// Handle de-register of users. Require POST username.
|
||||
// THIS FUNCTION WILL NOT CHECK FOR PERMISSION. PLEASE USE WITH PERMISSION HANDLER
|
||||
func (a *AuthAgent) HandleUnregister(w http.ResponseWriter, r *http.Request) {
|
||||
//Check if the user is logged in
|
||||
if !a.CheckAuth(r) {
|
||||
@ -365,7 +371,7 @@ func (a *AuthAgent) HandleUnregister(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
//Return to the client with OK
|
||||
utils.SendOK(w)
|
||||
log.Println("[Auth] User " + username + " has been removed from the system.")
|
||||
a.Logger.PrintAndLog("auth", "User "+username+" has been removed from the system", nil)
|
||||
}
|
||||
|
||||
func (a *AuthAgent) UnregisterUser(username string) error {
|
||||
@ -381,7 +387,7 @@ func (a *AuthAgent) UnregisterUser(username string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//Get the number of users in the system
|
||||
// Get the number of users in the system
|
||||
func (a *AuthAgent) GetUserCounts() int {
|
||||
entries, _ := a.Database.ListTable("auth")
|
||||
usercount := 0
|
||||
@ -393,12 +399,12 @@ func (a *AuthAgent) GetUserCounts() int {
|
||||
}
|
||||
|
||||
if usercount == 0 {
|
||||
log.Println("There are no user in the database.")
|
||||
a.Logger.PrintAndLog("auth", "There are no user in the database", nil)
|
||||
}
|
||||
return usercount
|
||||
}
|
||||
|
||||
//List all username within the system
|
||||
// List all username within the system
|
||||
func (a *AuthAgent) ListUsers() []string {
|
||||
entries, _ := a.Database.ListTable("auth")
|
||||
results := []string{}
|
||||
@ -411,7 +417,7 @@ func (a *AuthAgent) ListUsers() []string {
|
||||
return results
|
||||
}
|
||||
|
||||
//Check if the given username exists
|
||||
// Check if the given username exists
|
||||
func (a *AuthAgent) UserExists(username string) bool {
|
||||
userpasswordhash := ""
|
||||
err := a.Database.Read("auth", "passhash/"+username, &userpasswordhash)
|
||||
@ -421,7 +427,7 @@ func (a *AuthAgent) UserExists(username string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
//Update the session expire time given the request header.
|
||||
// Update the session expire time given the request header.
|
||||
func (a *AuthAgent) UpdateSessionExpireTime(w http.ResponseWriter, r *http.Request) bool {
|
||||
session, _ := a.SessionStore.Get(r, a.SessionName)
|
||||
if session.Values["authenticated"].(bool) {
|
||||
@ -446,7 +452,7 @@ func (a *AuthAgent) UpdateSessionExpireTime(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
}
|
||||
|
||||
//Create user account
|
||||
// Create user account
|
||||
func (a *AuthAgent) CreateUserAccount(newusername string, password string, email string) error {
|
||||
//Check user already exists
|
||||
if a.UserExists(newusername) {
|
||||
@ -470,7 +476,7 @@ func (a *AuthAgent) CreateUserAccount(newusername string, password string, email
|
||||
return nil
|
||||
}
|
||||
|
||||
//Hash the given raw string into sha512 hash
|
||||
// Hash the given raw string into sha512 hash
|
||||
func Hash(raw string) string {
|
||||
h := sha512.New()
|
||||
h.Write([]byte(raw))
|
||||
|
@ -2,7 +2,7 @@ package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@ -28,7 +28,7 @@ func NewManagedHTTPRouter(option RouterOption) *RouterDef {
|
||||
func (router *RouterDef) HandleFunc(endpoint string, handler func(http.ResponseWriter, *http.Request)) error {
|
||||
//Check if the endpoint already registered
|
||||
if _, exist := router.endpoints[endpoint]; exist {
|
||||
log.Println("WARNING! Duplicated registering of web endpoint: " + endpoint)
|
||||
fmt.Println("WARNING! Duplicated registering of web endpoint: " + endpoint)
|
||||
return errors.New("endpoint register duplicated")
|
||||
}
|
||||
|
||||
|
@ -77,6 +77,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if sep.RequireRateLimit {
|
||||
err := h.handleRateLimitRouting(w, r, sep)
|
||||
if err != nil {
|
||||
h.Parent.Option.Logger.LogHTTPRequest(r, "host", 429)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -85,6 +86,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if sep.RequireBasicAuth {
|
||||
err := h.handleBasicAuthRouting(w, r, sep)
|
||||
if err != nil {
|
||||
h.Parent.Option.Logger.LogHTTPRequest(r, "host", 401)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -101,6 +103,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if potentialProxtEndpoint != nil && !potentialProxtEndpoint.Disabled {
|
||||
//Missing tailing slash. Redirect to target proxy endpoint
|
||||
http.Redirect(w, r, r.RequestURI+"/", http.StatusTemporaryRedirect)
|
||||
h.Parent.Option.Logger.LogHTTPRequest(r, "redirect", 307)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -154,7 +154,7 @@ func (router *Router) StartProxyService() error {
|
||||
selectedUpstream, err := router.loadBalancer.GetRequestUpstreamTarget(w, r, sep.ActiveOrigins, sep.UseStickySession)
|
||||
if err != nil {
|
||||
http.ServeFile(w, r, "./web/hosterror.html")
|
||||
log.Println(err.Error())
|
||||
router.Option.Logger.PrintAndLog("dprouter", "failed to get upstream for hostname", err)
|
||||
router.logRequest(r, false, 404, "vdir-http", r.Host)
|
||||
}
|
||||
selectedUpstream.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
|
||||
@ -195,7 +195,7 @@ func (router *Router) StartProxyService() error {
|
||||
IdleTimeout: 120 * time.Second,
|
||||
}
|
||||
|
||||
log.Println("Starting HTTP-to-HTTPS redirector (port 80)")
|
||||
router.Option.Logger.PrintAndLog("dprouter", "Starting HTTP-to-HTTPS redirector (port 80)", nil)
|
||||
|
||||
//Create a redirection stop channel
|
||||
stopChan := make(chan bool)
|
||||
@ -206,7 +206,7 @@ func (router *Router) StartProxyService() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
httpServer.Shutdown(ctx)
|
||||
log.Println("HTTP to HTTPS redirection listener stopped")
|
||||
router.Option.Logger.PrintAndLog("dprouter", "HTTP to HTTPS redirection listener stopped", nil)
|
||||
}()
|
||||
|
||||
//Start the http server that listens to port 80 and redirect to 443
|
||||
@ -221,10 +221,10 @@ func (router *Router) StartProxyService() error {
|
||||
}
|
||||
|
||||
//Start the TLS server
|
||||
log.Println("Reverse proxy service started in the background (TLS mode)")
|
||||
router.Option.Logger.PrintAndLog("dprouter", "Reverse proxy service started in the background (TLS mode)", nil)
|
||||
go func() {
|
||||
if err := router.server.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("Could not start proxy server: %v\n", err)
|
||||
router.Option.Logger.PrintAndLog("dprouter", "Could not start proxy server", err)
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
@ -232,10 +232,9 @@ func (router *Router) StartProxyService() error {
|
||||
router.tlsListener = nil
|
||||
router.server = &http.Server{Addr: ":" + strconv.Itoa(router.Option.Port), Handler: router.mux}
|
||||
router.Running = true
|
||||
log.Println("Reverse proxy service started in the background (Plain HTTP mode)")
|
||||
router.Option.Logger.PrintAndLog("dprouter", "Reverse proxy service started in the background (Plain HTTP mode)", nil)
|
||||
go func() {
|
||||
router.server.ListenAndServe()
|
||||
//log.Println("[DynamicProxy] " + err.Error())
|
||||
}()
|
||||
}
|
||||
|
||||
|
@ -136,7 +136,7 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
|
||||
if selectedUpstream.RequireTLS {
|
||||
u, _ = url.Parse("wss://" + wsRedirectionEndpoint + requestURL)
|
||||
}
|
||||
h.Parent.logRequest(r, true, 101, "subdomain-websocket", selectedUpstream.OriginIpOrDomain)
|
||||
h.Parent.logRequest(r, true, 101, "host-websocket", selectedUpstream.OriginIpOrDomain)
|
||||
wspHandler := websocketproxy.NewProxy(u, websocketproxy.Options{
|
||||
SkipTLSValidation: selectedUpstream.SkipCertValidations,
|
||||
SkipOriginCheck: selectedUpstream.SkipWebSocketOriginCheck,
|
||||
@ -173,15 +173,15 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
|
||||
if errors.As(err, &dnsError) {
|
||||
http.ServeFile(w, r, "./web/hosterror.html")
|
||||
log.Println(err.Error())
|
||||
h.Parent.logRequest(r, false, 404, "subdomain-http", r.URL.Hostname())
|
||||
h.Parent.logRequest(r, false, 404, "host-http", r.URL.Hostname())
|
||||
} else {
|
||||
http.ServeFile(w, r, "./web/rperror.html")
|
||||
log.Println(err.Error())
|
||||
h.Parent.logRequest(r, false, 521, "subdomain-http", r.URL.Hostname())
|
||||
h.Parent.logRequest(r, false, 521, "host-http", r.URL.Hostname())
|
||||
}
|
||||
}
|
||||
|
||||
h.Parent.logRequest(r, true, 200, "subdomain-http", r.URL.Hostname())
|
||||
h.Parent.logRequest(r, true, 200, "host-http", r.URL.Hostname())
|
||||
}
|
||||
|
||||
// Handle vdir type request
|
||||
@ -249,6 +249,7 @@ func (h *ProxyHandler) vdirRequest(w http.ResponseWriter, r *http.Request, targe
|
||||
|
||||
}
|
||||
|
||||
// This logger collect data for the statistical analysis. For log to file logger, check the Logger and LogHTTPRequest handler
|
||||
func (router *Router) logRequest(r *http.Request, succ bool, statusCode int, forwardType string, target string) {
|
||||
if router.Option.StatisticCollector != nil {
|
||||
go func() {
|
||||
@ -266,4 +267,5 @@ func (router *Router) logRequest(r *http.Request, succ bool, statusCode int, for
|
||||
router.Option.StatisticCollector.RecordRequest(requestInfo)
|
||||
}()
|
||||
}
|
||||
router.Option.Logger.LogHTTPRequest(r, forwardType, statusCode)
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/statistic"
|
||||
"imuslab.com/zoraxy/mod/tlscert"
|
||||
)
|
||||
@ -43,6 +44,7 @@ type RouterOption struct {
|
||||
StatisticCollector *statistic.Collector //Statistic collector for storing stats on incoming visitors
|
||||
WebDirectory string //The static web server directory containing the templates folder
|
||||
LoadBalancer *loadbalance.RouteManager //Load balancer that handle load balancing of proxy target
|
||||
Logger *logger.Logger //Logger for reverse proxy requets
|
||||
}
|
||||
|
||||
/* Router Object */
|
||||
|
@ -13,29 +13,31 @@ import (
|
||||
Zoraxy Logger
|
||||
|
||||
This script is designed to make a managed log for the Zoraxy
|
||||
and replace the ton of log.Println in the system core
|
||||
and replace the ton of log.Println in the system core.
|
||||
The core logger is based in golang's build-in log package
|
||||
*/
|
||||
|
||||
type Logger struct {
|
||||
LogToFile bool //Set enable write to file
|
||||
Prefix string //Prefix for log files
|
||||
LogFolder string //Folder to store the log file
|
||||
CurrentLogFile string //Current writing filename
|
||||
logger *log.Logger
|
||||
file *os.File
|
||||
}
|
||||
|
||||
func NewLogger(logFilePrefix string, logFolder string, logToFile bool) (*Logger, error) {
|
||||
// Create a new logger that log to files
|
||||
func NewLogger(logFilePrefix string, logFolder string) (*Logger, error) {
|
||||
err := os.MkdirAll(logFolder, 0775)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
thisLogger := Logger{
|
||||
LogToFile: logToFile,
|
||||
Prefix: logFilePrefix,
|
||||
LogFolder: logFolder,
|
||||
}
|
||||
|
||||
//Create the log file if not exists
|
||||
logFilePath := thisLogger.getLogFilepath()
|
||||
f, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0755)
|
||||
if err != nil {
|
||||
@ -43,9 +45,26 @@ func NewLogger(logFilePrefix string, logFolder string, logToFile bool) (*Logger,
|
||||
}
|
||||
thisLogger.CurrentLogFile = logFilePath
|
||||
thisLogger.file = f
|
||||
|
||||
//Start the logger
|
||||
logger := log.New(f, "", log.Flags()&^(log.Ldate|log.Ltime))
|
||||
logger.SetFlags(0)
|
||||
logger.SetOutput(f)
|
||||
thisLogger.logger = logger
|
||||
return &thisLogger, nil
|
||||
}
|
||||
|
||||
// Create a fmt logger that only log to STDOUT
|
||||
func NewFmtLogger() (*Logger, error) {
|
||||
return &Logger{
|
||||
Prefix: "",
|
||||
LogFolder: "",
|
||||
CurrentLogFile: "",
|
||||
logger: nil,
|
||||
file: nil,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *Logger) getLogFilepath() string {
|
||||
year, month, _ := time.Now().Date()
|
||||
return filepath.Join(l.LogFolder, l.Prefix+"_"+strconv.Itoa(year)+"-"+strconv.Itoa(int(month))+".log")
|
||||
@ -54,9 +73,8 @@ func (l *Logger) getLogFilepath() string {
|
||||
// PrintAndLog will log the message to file and print the log to STDOUT
|
||||
func (l *Logger) PrintAndLog(title string, message string, originalError error) {
|
||||
go func() {
|
||||
l.Log(title, message, originalError)
|
||||
l.Log(title, message, originalError, true)
|
||||
}()
|
||||
log.Println("[" + title + "] " + message)
|
||||
}
|
||||
|
||||
// Println is a fast snap-in replacement for log.Println
|
||||
@ -64,18 +82,26 @@ func (l *Logger) Println(v ...interface{}) {
|
||||
//Convert the array of interfaces into string
|
||||
message := fmt.Sprint(v...)
|
||||
go func() {
|
||||
l.Log("info", string(message), nil)
|
||||
l.Log("internal", string(message), nil, true)
|
||||
}()
|
||||
log.Println("[INFO] " + string(message))
|
||||
}
|
||||
|
||||
func (l *Logger) Log(title string, errorMessage string, originalError error) {
|
||||
func (l *Logger) Log(title string, errorMessage string, originalError error, copyToSTDOUT bool) {
|
||||
l.ValidateAndUpdateLogFilepath()
|
||||
if l.LogToFile {
|
||||
if l.logger == nil || copyToSTDOUT {
|
||||
//Use STDOUT instead of logger
|
||||
if originalError == nil {
|
||||
l.file.WriteString(time.Now().Format("2006-01-02 15:04:05.000000") + "|" + fmt.Sprintf("%-16s", title) + " [INFO]" + errorMessage + "\n")
|
||||
fmt.Println("[" + time.Now().Format("2006-01-02 15:04:05.000000") + "] [" + title + "] [system:info] " + errorMessage)
|
||||
} else {
|
||||
l.file.WriteString(time.Now().Format("2006-01-02 15:04:05.000000") + "|" + fmt.Sprintf("%-16s", title) + " [ERROR]" + errorMessage + " " + originalError.Error() + "\n")
|
||||
fmt.Println("[" + time.Now().Format("2006-01-02 15:04:05.000000") + "] [" + title + "] [system:error] " + errorMessage + ": " + originalError.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if l.logger != nil {
|
||||
if originalError == nil {
|
||||
l.logger.Println("[" + time.Now().Format("2006-01-02 15:04:05.000000") + "] [" + title + "] [system:info] " + errorMessage)
|
||||
} else {
|
||||
l.logger.Println("[" + time.Now().Format("2006-01-02 15:04:05.000000") + "] [" + title + "] [system:error] " + errorMessage + ": " + originalError.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@ -83,18 +109,28 @@ func (l *Logger) Log(title string, errorMessage string, originalError error) {
|
||||
|
||||
// Validate if the logging target is still valid (detect any months change)
|
||||
func (l *Logger) ValidateAndUpdateLogFilepath() {
|
||||
if l.file == nil {
|
||||
return
|
||||
}
|
||||
expectedCurrentLogFilepath := l.getLogFilepath()
|
||||
if l.CurrentLogFile != expectedCurrentLogFilepath {
|
||||
//Change of month. Update to a new log file
|
||||
l.file.Close()
|
||||
l.file = nil
|
||||
|
||||
//Create a new log file
|
||||
f, err := os.OpenFile(expectedCurrentLogFilepath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0755)
|
||||
if err != nil {
|
||||
log.Println("[Logger] Unable to create new log. Logging to file disabled.")
|
||||
l.LogToFile = false
|
||||
log.Println("Unable to create new log. Logging is disabled: ", err.Error())
|
||||
l.logger = nil
|
||||
return
|
||||
}
|
||||
l.CurrentLogFile = expectedCurrentLogFilepath
|
||||
l.file = f
|
||||
|
||||
//Start a new logger
|
||||
logger := log.New(f, "", log.Default().Flags())
|
||||
l.logger = logger
|
||||
}
|
||||
}
|
||||
|
||||
|
32
src/mod/info/logger/trafficlog.go
Normal file
32
src/mod/info/logger/trafficlog.go
Normal file
@ -0,0 +1,32 @@
|
||||
package logger
|
||||
|
||||
/*
|
||||
Traffic Log
|
||||
|
||||
This script log the traffic of HTTP requests
|
||||
|
||||
*/
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/netutils"
|
||||
)
|
||||
|
||||
// Log HTTP request. Note that this must run in go routine to prevent any blocking
|
||||
// in reverse proxy router
|
||||
func (l *Logger) LogHTTPRequest(r *http.Request, reqclass string, statusCode int) {
|
||||
go func() {
|
||||
l.ValidateAndUpdateLogFilepath()
|
||||
if l.logger == nil || l.file == nil {
|
||||
//logger is not initiated. Do not log http request
|
||||
return
|
||||
}
|
||||
clientIP := netutils.GetRequesterIP(r)
|
||||
requestURI := r.RequestURI
|
||||
statusCodeString := strconv.Itoa(statusCode)
|
||||
//fmt.Println("[" + time.Now().Format("2006-01-02 15:04:05.000000") + "] [router:" + reqclass + "] [client " + clientIP + "] " + r.Method + " " + requestURI + " " + statusCodeString)
|
||||
l.logger.Println("[" + time.Now().Format("2006-01-02 15:04:05.000000") + "] [router:" + reqclass + "] [origin:" + r.URL.Hostname() + "] [client " + clientIP + "] " + r.Method + " " + requestURI + " " + statusCodeString)
|
||||
}()
|
||||
}
|
@ -3,6 +3,7 @@ package logviewer
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
@ -51,13 +52,7 @@ func (v *Viewer) HandleReadLog(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
catergory, err := utils.GetPara(r, "catergory")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid catergory given")
|
||||
return
|
||||
}
|
||||
|
||||
content, err := v.LoadLogFile(strings.TrimSpace(filepath.Base(catergory)), strings.TrimSpace(filepath.Base(filename)))
|
||||
content, err := v.LoadLogFile(strings.TrimSpace(filepath.Base(filename)))
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
@ -106,8 +101,11 @@ func (v *Viewer) ListLogFiles(showFullpath bool) map[string][]*LogFile {
|
||||
return result
|
||||
}
|
||||
|
||||
func (v *Viewer) LoadLogFile(catergory string, filename string) (string, error) {
|
||||
logFilepath := filepath.Join(v.option.RootFolder, catergory, filename)
|
||||
func (v *Viewer) LoadLogFile(filename string) (string, error) {
|
||||
filename = filepath.ToSlash(filename)
|
||||
filename = strings.ReplaceAll(filename, "../", "")
|
||||
logFilepath := filepath.Join(v.option.RootFolder, filename)
|
||||
fmt.Println(logFilepath)
|
||||
if utils.FileExists(logFilepath) {
|
||||
//Load it
|
||||
content, err := os.ReadFile(logFilepath)
|
||||
|
@ -39,7 +39,6 @@ type NetStatBuffers struct {
|
||||
|
||||
// Get a new network statistic buffers
|
||||
func NewNetStatBuffer(recordCount int) (*NetStatBuffers, error) {
|
||||
|
||||
//Flood fill the stats with 0
|
||||
initialStats := []*FlowStat{}
|
||||
for i := 0; i < recordCount; i++ {
|
||||
|
@ -2,16 +2,23 @@ package uptime
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/publicsuffix"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
logModuleName = "uptime-monitor"
|
||||
)
|
||||
|
||||
type Record struct {
|
||||
Timestamp int64
|
||||
ID string
|
||||
@ -42,6 +49,7 @@ type Config struct {
|
||||
Targets []*Target
|
||||
Interval int
|
||||
MaxRecordsStore int
|
||||
Logger *logger.Logger
|
||||
}
|
||||
|
||||
type Monitor struct {
|
||||
@ -64,6 +72,12 @@ func NewUptimeMonitor(config *Config) (*Monitor, error) {
|
||||
Config: config,
|
||||
OnlineStatusLog: map[string][]*Record{},
|
||||
}
|
||||
|
||||
if config.Logger == nil {
|
||||
//Use default fmt to log if logger is not provided
|
||||
config.Logger, _ = logger.NewFmtLogger()
|
||||
}
|
||||
|
||||
//Start the endpoint listener
|
||||
ticker := time.NewTicker(time.Duration(config.Interval) * time.Second)
|
||||
done := make(chan bool)
|
||||
@ -77,7 +91,7 @@ func NewUptimeMonitor(config *Config) (*Monitor, error) {
|
||||
case <-done:
|
||||
return
|
||||
case t := <-ticker.C:
|
||||
log.Println("Uptime updated - ", t.Unix())
|
||||
thisMonitor.Config.Logger.PrintAndLog(logModuleName, "Uptime updated - "+strconv.Itoa(int(t.Unix())), nil)
|
||||
thisMonitor.ExecuteUptimeCheck()
|
||||
}
|
||||
}
|
||||
@ -91,7 +105,7 @@ func (m *Monitor) ExecuteUptimeCheck() {
|
||||
//For each target to check online, do the following
|
||||
var thisRecord Record
|
||||
if target.Protocol == "http" || target.Protocol == "https" {
|
||||
online, laterncy, statusCode := getWebsiteStatusWithLatency(target.URL)
|
||||
online, laterncy, statusCode := m.getWebsiteStatusWithLatency(target.URL)
|
||||
thisRecord = Record{
|
||||
Timestamp: time.Now().Unix(),
|
||||
ID: target.ID,
|
||||
@ -104,7 +118,7 @@ func (m *Monitor) ExecuteUptimeCheck() {
|
||||
}
|
||||
|
||||
} else {
|
||||
log.Println("Unknown protocol: " + target.Protocol + ". Skipping")
|
||||
m.Config.Logger.PrintAndLog(logModuleName, "Unknown protocol: "+target.Protocol, errors.New("unsupported protocol"))
|
||||
continue
|
||||
}
|
||||
|
||||
@ -124,8 +138,6 @@ func (m *Monitor) ExecuteUptimeCheck() {
|
||||
m.OnlineStatusLog[target.ID] = thisRecords
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Write results to db
|
||||
}
|
||||
|
||||
func (m *Monitor) AddTargetToMonitor(target *Target) {
|
||||
@ -201,12 +213,12 @@ func (m *Monitor) HandleUptimeLogRead(w http.ResponseWriter, r *http.Request) {
|
||||
*/
|
||||
|
||||
// Get website stauts with latency given URL, return is conn succ and its latency and status code
|
||||
func getWebsiteStatusWithLatency(url string) (bool, int64, int) {
|
||||
func (m *Monitor) getWebsiteStatusWithLatency(url string) (bool, int64, int) {
|
||||
start := time.Now().UnixNano() / int64(time.Millisecond)
|
||||
statusCode, err := getWebsiteStatus(url)
|
||||
end := time.Now().UnixNano() / int64(time.Millisecond)
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
m.Config.Logger.PrintAndLog(logModuleName, "Ping upstream timeout. Assume offline", err)
|
||||
return false, 0, 0
|
||||
} else {
|
||||
diff := end - start
|
||||
|
@ -5,13 +5,13 @@ import (
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
"imuslab.com/zoraxy/mod/webserv/filemanager"
|
||||
)
|
||||
@ -30,6 +30,7 @@ type WebServerOptions struct {
|
||||
EnableDirectoryListing bool //Enable listing of directory
|
||||
WebRoot string //Folder for stroing the static web folders
|
||||
EnableWebDirManager bool //Enable web file manager to handle files in web directory
|
||||
Logger *logger.Logger //System logger
|
||||
Sysdb *database.Database //Database for storing configs
|
||||
}
|
||||
|
||||
@ -45,13 +46,16 @@ type WebServer struct {
|
||||
|
||||
// NewWebServer creates a new WebServer instance. One instance only
|
||||
func NewWebServer(options *WebServerOptions) *WebServer {
|
||||
if options.Logger == nil {
|
||||
options.Logger, _ = logger.NewFmtLogger()
|
||||
}
|
||||
if !utils.FileExists(options.WebRoot) {
|
||||
//Web root folder not exists. Create one with default templates
|
||||
os.MkdirAll(filepath.Join(options.WebRoot, "html"), 0775)
|
||||
os.MkdirAll(filepath.Join(options.WebRoot, "templates"), 0775)
|
||||
indexTemplate, err := templates.ReadFile("templates/index.html")
|
||||
if err != nil {
|
||||
log.Println("Failed to read static wev server template file: ", err.Error())
|
||||
options.Logger.PrintAndLog("static-webserv", "Failed to read static wev server template file: ", err)
|
||||
} else {
|
||||
os.WriteFile(filepath.Join(options.WebRoot, "html", "index.html"), indexTemplate, 0775)
|
||||
}
|
||||
@ -102,7 +106,7 @@ func (ws *WebServer) RestorePreviousState() {
|
||||
// ChangePort changes the server's port.
|
||||
func (ws *WebServer) ChangePort(port string) error {
|
||||
if IsPortInUse(port) {
|
||||
return errors.New("Selected port is used by another process")
|
||||
return errors.New("selected port is used by another process")
|
||||
}
|
||||
|
||||
if ws.isRunning {
|
||||
@ -119,6 +123,7 @@ func (ws *WebServer) ChangePort(port string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
ws.option.Logger.PrintAndLog("static-webserv", "Listening port updated to "+port, nil)
|
||||
ws.option.Sysdb.Write("webserv", "port", port)
|
||||
|
||||
return nil
|
||||
@ -141,7 +146,7 @@ func (ws *WebServer) Start() error {
|
||||
|
||||
//Check if the port is usable
|
||||
if IsPortInUse(ws.option.Port) {
|
||||
return errors.New("Port already in use or access denied by host OS")
|
||||
return errors.New("port already in use or access denied by host OS")
|
||||
}
|
||||
|
||||
//Dispose the old mux and create a new one
|
||||
@ -159,12 +164,12 @@ func (ws *WebServer) Start() error {
|
||||
go func() {
|
||||
if err := ws.server.ListenAndServe(); err != nil {
|
||||
if err != http.ErrServerClosed {
|
||||
fmt.Printf("Web server error: %v\n", err)
|
||||
ws.option.Logger.PrintAndLog("static-webserv", "Web server failed to start", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
log.Println("Static Web Server started. Listeing on :" + ws.option.Port)
|
||||
ws.option.Logger.PrintAndLog("static-webserv", "Static Web Server started. Listeing on :"+ws.option.Port, nil)
|
||||
ws.isRunning = true
|
||||
ws.option.Sysdb.Write("webserv", "enabled", true)
|
||||
return nil
|
||||
@ -182,7 +187,7 @@ func (ws *WebServer) Stop() error {
|
||||
if err := ws.server.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ws.option.Logger.PrintAndLog("static-webserv", "Static Web Server stopped", nil)
|
||||
ws.isRunning = false
|
||||
ws.option.Sysdb.Write("webserv", "enabled", false)
|
||||
return nil
|
||||
|
@ -98,9 +98,10 @@ func ReverseProxtInit() {
|
||||
WebDirectory: *staticWebServerRoot,
|
||||
AccessController: accessController,
|
||||
LoadBalancer: loadBalancer,
|
||||
Logger: SystemWideLogger,
|
||||
})
|
||||
if err != nil {
|
||||
SystemWideLogger.PrintAndLog("Proxy", "Unable to create dynamic proxy router", err)
|
||||
SystemWideLogger.PrintAndLog("proxy-config", "Unable to create dynamic proxy router", err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -115,7 +116,7 @@ func ReverseProxtInit() {
|
||||
for _, conf := range confs {
|
||||
err := LoadReverseProxyConfig(conf)
|
||||
if err != nil {
|
||||
SystemWideLogger.PrintAndLog("Proxy", "Failed to load config file: "+filepath.Base(conf), err)
|
||||
SystemWideLogger.PrintAndLog("proxy-config", "Failed to load config file: "+filepath.Base(conf), err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -124,7 +125,7 @@ func ReverseProxtInit() {
|
||||
//Root config not set (new deployment?), use internal static web server as root
|
||||
defaultRootRouter, err := GetDefaultRootConfig()
|
||||
if err != nil {
|
||||
SystemWideLogger.PrintAndLog("Proxy", "Failed to generate default root routing", err)
|
||||
SystemWideLogger.PrintAndLog("proxy-config", "Failed to generate default root routing", err)
|
||||
return
|
||||
}
|
||||
dynamicProxyRouter.SetProxyRouteAsRoot(defaultRootRouter)
|
||||
@ -143,8 +144,9 @@ func ReverseProxtInit() {
|
||||
//This must be done in go routine to prevent blocking on system startup
|
||||
uptimeMonitor, _ = uptime.NewUptimeMonitor(&uptime.Config{
|
||||
Targets: GetUptimeTargetsFromReverseProxyRules(dynamicProxyRouter),
|
||||
Interval: 300, //5 minutes
|
||||
MaxRecordsStore: 288, //1 day
|
||||
Interval: 300, //5 minutes
|
||||
MaxRecordsStore: 288, //1 day
|
||||
Logger: SystemWideLogger, //Logger
|
||||
})
|
||||
|
||||
SystemWideLogger.Println("Uptime Monitor background service started")
|
||||
@ -412,7 +414,7 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
//Save the config to file
|
||||
err = SaveReverseProxyConfig(proxyEndpointCreated)
|
||||
if err != nil {
|
||||
SystemWideLogger.PrintAndLog("Proxy", "Unable to save new proxy rule to file", err)
|
||||
SystemWideLogger.PrintAndLog("proxy-config", "Unable to save new proxy rule to file", err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -480,15 +482,6 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
proxyRateLimit = 1000
|
||||
}
|
||||
|
||||
// Bypass WebSocket Origin Check
|
||||
/*
|
||||
strbpwsorg, _ := utils.PostPara(r, "bpwsorg")
|
||||
if strbpwsorg == "" {
|
||||
strbpwsorg = "false"
|
||||
}
|
||||
bypassWebsocketOriginCheck := (strbpwsorg == "true")
|
||||
*/
|
||||
|
||||
//Load the previous basic auth credentials from current proxy rules
|
||||
targetProxyEntry, err := dynamicProxyRouter.LoadProxy(rootNameOrMatchingDomain)
|
||||
if err != nil {
|
||||
@ -498,11 +491,6 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
//Generate a new proxyEndpoint from the new config
|
||||
newProxyEndpoint := dynamicproxy.CopyEndpoint(targetProxyEntry)
|
||||
//TODO: Move these into dedicated module
|
||||
//newProxyEndpoint.Domain = endpoint
|
||||
//newProxyEndpoint.RequireTLS = useTLS
|
||||
//newProxyEndpoint.SkipCertValidations = skipTlsValidation
|
||||
//newProxyEndpoint.SkipWebSocketOriginCheck = bypassWebsocketOriginCheck
|
||||
newProxyEndpoint.BypassGlobalTLS = bypassGlobalTLS
|
||||
newProxyEndpoint.RequireBasicAuth = requireBasicAuth
|
||||
newProxyEndpoint.RequireRateLimit = requireRateLimit
|
||||
@ -521,9 +509,6 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
//Save it to file
|
||||
SaveReverseProxyConfig(newProxyEndpoint)
|
||||
|
||||
//Update uptime monitor
|
||||
UpdateUptimeMonitorTargets()
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
@ -555,7 +540,7 @@ func ReverseProxyHandleAlias(w http.ResponseWriter, r *http.Request) {
|
||||
newAlias := []string{}
|
||||
err = json.Unmarshal([]byte(newAliasJSON), &newAlias)
|
||||
if err != nil {
|
||||
SystemWideLogger.PrintAndLog("Proxy", "Unable to parse new alias list", err)
|
||||
SystemWideLogger.PrintAndLog("proxy-config", "Unable to parse new alias list", err)
|
||||
utils.SendErrorResponse(w, "Invalid alias list given")
|
||||
return
|
||||
}
|
||||
@ -577,7 +562,7 @@ func ReverseProxyHandleAlias(w http.ResponseWriter, r *http.Request) {
|
||||
err = SaveReverseProxyConfig(newProxyEndpoint)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Alias update failed")
|
||||
SystemWideLogger.PrintAndLog("Proxy", "Unable to save alias update", err)
|
||||
SystemWideLogger.PrintAndLog("proxy-config", "Unable to save alias update", err)
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
@ -881,6 +866,10 @@ func ReverseProxyToggleRuleSet(w http.ResponseWriter, r *http.Request) {
|
||||
utils.SendErrorResponse(w, "unable to save updated rule")
|
||||
return
|
||||
}
|
||||
|
||||
//Update uptime monitor
|
||||
UpdateUptimeMonitorTargets()
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
|
26
src/start.go
26
src/start.go
@ -20,6 +20,7 @@ import (
|
||||
"imuslab.com/zoraxy/mod/ganserv"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/info/logviewer"
|
||||
"imuslab.com/zoraxy/mod/mdns"
|
||||
"imuslab.com/zoraxy/mod/netstat"
|
||||
"imuslab.com/zoraxy/mod/pathrule"
|
||||
@ -47,6 +48,18 @@ var (
|
||||
)
|
||||
|
||||
func startupSequence() {
|
||||
//Start a system wide logger and log viewer
|
||||
l, err := logger.NewLogger("zr", "./log")
|
||||
if err == nil {
|
||||
SystemWideLogger = l
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
LogViewer = logviewer.NewLogViewer(&logviewer.ViewerOption{
|
||||
RootFolder: "./log",
|
||||
Extension: ".log",
|
||||
})
|
||||
|
||||
//Create database
|
||||
db, err := database.NewDatabase("sys.db", false)
|
||||
if err != nil {
|
||||
@ -61,11 +74,11 @@ func startupSequence() {
|
||||
os.MkdirAll("./conf/proxy/", 0775)
|
||||
|
||||
//Create an auth agent
|
||||
sessionKey, err := auth.GetSessionKey(sysdb)
|
||||
sessionKey, err := auth.GetSessionKey(sysdb, SystemWideLogger)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
authAgent = auth.NewAuthenticationAgent(name, []byte(sessionKey), sysdb, true, func(w http.ResponseWriter, r *http.Request) {
|
||||
authAgent = auth.NewAuthenticationAgent(name, []byte(sessionKey), sysdb, true, SystemWideLogger, func(w http.ResponseWriter, r *http.Request) {
|
||||
//Not logged in. Redirecting to login page
|
||||
http.Redirect(w, r, ppf("/login.html"), http.StatusTemporaryRedirect)
|
||||
})
|
||||
@ -76,14 +89,6 @@ func startupSequence() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//Create a system wide logger
|
||||
l, err := logger.NewLogger("zr", "./log", *logOutputToFile)
|
||||
if err == nil {
|
||||
SystemWideLogger = l
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//Create a redirection rule table
|
||||
db.NewTable("redirect")
|
||||
redirectAllowRegexp := false
|
||||
@ -134,6 +139,7 @@ func startupSequence() {
|
||||
WebRoot: *staticWebServerRoot,
|
||||
EnableDirectoryListing: true,
|
||||
EnableWebDirManager: *allowWebFileManager,
|
||||
Logger: SystemWideLogger,
|
||||
})
|
||||
//Restore the web server to previous shutdown state
|
||||
staticWebServer.RestorePreviousState()
|
||||
|
@ -109,6 +109,9 @@ func ReverseProxyUpstreamAdd(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
//Update Uptime Monitor
|
||||
UpdateUptimeMonitorTargets()
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
@ -180,6 +183,9 @@ func ReverseProxyUpstreamUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
utils.SendErrorResponse(w, "Failed to save updated upstream config")
|
||||
return
|
||||
}
|
||||
|
||||
//Update Uptime Monitor
|
||||
UpdateUptimeMonitorTargets()
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
@ -270,5 +276,8 @@ func ReverseProxyUpstreamDelete(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
//Update uptime monitor
|
||||
UpdateUptimeMonitorTargets()
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="standardContainer">
|
||||
<div class="ui basic segment">
|
||||
<h2>Service Expose Proxy</h2>
|
||||
<p>Expose your local test-site on the internet with single command</p>
|
||||
<h2>Single-Sign-On</h2>
|
||||
<p>Create and manage accounts with Zoraxy!</p>
|
||||
</div>
|
||||
<div class="ui message">
|
||||
<h4>Work In Progress</h4>
|
@ -120,7 +120,12 @@
|
||||
<!-- Config Tools -->
|
||||
<h3>System Backup & Restore</h3>
|
||||
<p>Options related to system backup, migrate and restore.</p>
|
||||
<button class="ui basic button" onclick="showSideWrapper('snippet/configTools.html');">Open Config Tools</button>
|
||||
<button class="ui basic button" onclick="showSideWrapper('snippet/configTools.html');"><i class="ui green undo icon icon"></i> Open Config Tools</button>
|
||||
<div class="ui divider"></div>
|
||||
<!-- Log Viewer -->
|
||||
<h3>System Log Viewer</h3>
|
||||
<p>View and download Zoraxy log</p>
|
||||
<button class="ui basic button" onclick="launchToolWithSize('snippet/logview.html', 1024, 768);"><i class="ui blue file icon"></i> Open Log Viewer</button>
|
||||
<div class="ui divider"></div>
|
||||
<!-- System Information -->
|
||||
<div id="zoraxyinfo">
|
||||
|
@ -55,21 +55,21 @@
|
||||
<i class="simplistic exchange icon"></i> Stream Proxy
|
||||
</a>
|
||||
<div class="ui divider menudivider">Access & Connections</div>
|
||||
<a class="item" tag="cert">
|
||||
<i class="simplistic lock icon"></i> TLS / SSL certificates
|
||||
</a>
|
||||
<a class="item" tag="redirectset">
|
||||
<i class="simplistic level up alternate icon"></i> Redirection
|
||||
</a>
|
||||
<a class="item" tag="access">
|
||||
<i class="simplistic ban icon"></i> Access Control
|
||||
</a>
|
||||
<div class="ui divider menudivider">Bridging</div>
|
||||
<a class="item" tag="gan">
|
||||
<i class="simplistic globe icon"></i> Global Area Network
|
||||
</a>
|
||||
<a class="item" tag="zgrok">
|
||||
<i class="simplistic podcast icon"></i> Service Expose Proxy
|
||||
<div class="ui divider menudivider">Security</div>
|
||||
<a class="item" tag="cert">
|
||||
<i class="simplistic lock icon"></i> TLS / SSL certificates
|
||||
</a>
|
||||
<a class="item" tag="sso">
|
||||
<i class="simplistic user circle icon"></i> SSO / Oauth
|
||||
</a>
|
||||
<div class="ui divider menudivider">Others</div>
|
||||
<a class="item" tag="webserv">
|
||||
@ -120,8 +120,8 @@
|
||||
<!-- Global Area Networking -->
|
||||
<div id="gan" class="functiontab" target="gan.html"></div>
|
||||
|
||||
<!-- Service Expose Proxy -->
|
||||
<div id="zgrok" class="functiontab" target="zgrok.html"></div>
|
||||
<!-- SSO / Oauth services -->
|
||||
<div id="sso" class="functiontab" target="sso.html"></div>
|
||||
|
||||
<!-- TCP Proxy -->
|
||||
<div id="streamproxy" class="functiontab" target="streamprox.html"></div>
|
||||
|
155
src/web/snippet/logview.html
Normal file
155
src/web/snippet/logview.html
Normal file
@ -0,0 +1,155 @@
|
||||
<!DOCTYPE html>
|
||||
<html ng-app="App">
|
||||
<head>
|
||||
<title>System Logs</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0 user-scalable=no">
|
||||
<link rel="stylesheet" href="../script/semantic/semantic.min.css">
|
||||
<script type="text/javascript" src="../script/jquery-3.6.0.min.js"></script>
|
||||
<script type="text/javascript" src="../script/semantic/semantic.min.js"></script>
|
||||
<style>
|
||||
.clickable{
|
||||
cursor: pointer;
|
||||
}
|
||||
.clickable:hover{
|
||||
opacity: 0.7;
|
||||
}
|
||||
.logfile{
|
||||
padding-left: 1em !important;
|
||||
position: relative;
|
||||
padding-right: 1em !important;
|
||||
}
|
||||
|
||||
.loglist{
|
||||
background-color: rgb(250, 250, 250);
|
||||
}
|
||||
|
||||
.logfile .showing{
|
||||
position: absolute;
|
||||
top: 0.18em;
|
||||
right: 0em;
|
||||
margin-right: -0.4em;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.logfile.active .showing{
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#logrender{
|
||||
width: 100% !important;
|
||||
height: calc(100% - 1.2em);
|
||||
min-height: calc(90vh - 1.2em) !important;
|
||||
border: 0px solid transparent !important;
|
||||
background-color: #252630;
|
||||
color: white;
|
||||
font-family: monospace;
|
||||
overflow-x: scroll !important;
|
||||
white-space: pre;
|
||||
resize: none;
|
||||
scrollbar-width: thin;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
#logrender::selection{
|
||||
background:#3643bb;
|
||||
color:white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<br>
|
||||
<div class="ui container">
|
||||
<div class="ui stackable grid">
|
||||
<div class="four wide column loglist">
|
||||
<h3 class="ui header" style="padding-top: 1em;">
|
||||
<div class="content">
|
||||
Log View
|
||||
<div class="sub header">Check System Log in Real Time</div>
|
||||
</div>
|
||||
</h3>
|
||||
<div class="ui divider"></div>
|
||||
<div id="logList" class="ui accordion">
|
||||
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<small>Notes: Some log files might be huge. Make sure you have checked the log file size before opening</small>
|
||||
</div>
|
||||
<div class="twelve wide column">
|
||||
<textarea id="logrender" spellcheck="false" readonly="true">
|
||||
← Pick a log file from the left menu to start debugging
|
||||
</textarea>
|
||||
<a href="#" onclick="openLogInNewTab();">Open In New Tab</a>
|
||||
<br><br>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
</body>
|
||||
<script>
|
||||
var currentOpenedLogURL = "";
|
||||
|
||||
function openLogInNewTab(){
|
||||
if (currentOpenedLogURL != ""){
|
||||
window.open(currentOpenedLogURL);
|
||||
}
|
||||
}
|
||||
|
||||
function openLog(object, catergory, filename){
|
||||
$(".logfile.active").removeClass('active');
|
||||
$(object).addClass("active");
|
||||
currentOpenedLogURL = "/api/log/read?file=" + filename;
|
||||
$.get(currentOpenedLogURL, function(data){
|
||||
if (data.error !== undefined){
|
||||
alert(data.error);
|
||||
return;
|
||||
}
|
||||
$("#logrender").val(data);
|
||||
});
|
||||
}
|
||||
|
||||
function initLogList(){
|
||||
$("#logList").html("");
|
||||
$.get("/api/log/list", function(data){
|
||||
//console.log(data);
|
||||
for (let [key, value] of Object.entries(data)) {
|
||||
console.log(key, value);
|
||||
value.reverse(); //Default value was from oldest to newest
|
||||
var fileItemList = "";
|
||||
value.forEach(file => {
|
||||
fileItemList += `<div class="item clickable logfile" onclick="openLog(this, '${key}','${file.Filename}');">
|
||||
<i class="file outline icon"></i>
|
||||
<div class="content">
|
||||
${file.Title} (${formatBytes(file.Filesize)})
|
||||
<div class="showing"><i class="green chevron right icon"></i></div>
|
||||
</div>
|
||||
</div>`;
|
||||
})
|
||||
$("#logList").append(`<div class="title">
|
||||
<i class="dropdown icon"></i>
|
||||
${key}
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="ui list">
|
||||
${fileItemList}
|
||||
</div>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
$(".ui.accordion").accordion();
|
||||
});
|
||||
}
|
||||
initLogList();
|
||||
|
||||
|
||||
function formatBytes(x){
|
||||
var units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
let l = 0, n = parseInt(x, 10) || 0;
|
||||
while(n >= 1024 && ++l){
|
||||
n = n/1024;
|
||||
}
|
||||
return(n.toFixed(n < 10 && l > 0 ? 1 : 0) + ' ' + units[l]);
|
||||
}
|
||||
</script>
|
||||
</html>
|
@ -75,7 +75,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui message">
|
||||
<i class="ui blue info circle icon"></i> Round-robin load balancing algorithm will be used for upstreams with same weight. Set weight to 0 for fallback only.
|
||||
<i class="ui blue info circle icon"></i> Weighted random will be used for load-balancing. Set weight to 0 for fallback only.
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui tab basic segment" data-tab="newupstream">
|
||||
|
@ -115,7 +115,7 @@ func UpdateUptimeMonitorTargets() {
|
||||
uptimeMonitor.ExecuteUptimeCheck()
|
||||
}()
|
||||
|
||||
SystemWideLogger.PrintAndLog("Uptime", "Uptime monitor config updated", nil)
|
||||
SystemWideLogger.PrintAndLog("uptime-monitor", "Uptime monitor config updated", nil)
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,8 +125,12 @@ func GetUptimeTargetsFromReverseProxyRules(dp *dynamicproxy.Router) []*uptime.Ta
|
||||
|
||||
UptimeTargets := []*uptime.Target{}
|
||||
for hostid, target := range hosts {
|
||||
for _, origin := range target.ActiveOrigins {
|
||||
|
||||
if target.Disabled {
|
||||
//Skip those proxy rules that is disabled
|
||||
continue
|
||||
}
|
||||
isMultipleUpstreams := len(target.ActiveOrigins) > 1
|
||||
for i, origin := range target.ActiveOrigins {
|
||||
url := "http://" + origin.OriginIpOrDomain
|
||||
protocol := "http"
|
||||
if origin.RequireTLS {
|
||||
@ -135,9 +139,13 @@ func GetUptimeTargetsFromReverseProxyRules(dp *dynamicproxy.Router) []*uptime.Ta
|
||||
}
|
||||
|
||||
//Add the root url
|
||||
hostIdAndName := hostid
|
||||
if isMultipleUpstreams {
|
||||
hostIdAndName = hostIdAndName + " (upstream:" + strconv.Itoa(i) + ")"
|
||||
}
|
||||
UptimeTargets = append(UptimeTargets, &uptime.Target{
|
||||
ID: hostid,
|
||||
Name: hostid,
|
||||
ID: hostIdAndName,
|
||||
Name: hostIdAndName,
|
||||
URL: url,
|
||||
Protocol: protocol,
|
||||
ProxyType: uptime.ProxyType_Host,
|
||||
|
Loading…
x
Reference in New Issue
Block a user