Added Zoraxy experimental
39
.gitignore
vendored
@ -1,15 +1,30 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
# ---> Go
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
src/*.o
|
||||
src/*.a
|
||||
src/*.so
|
||||
src/*.exe
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
# Folders
|
||||
src/_obj
|
||||
src/_test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
src/*.cgo1.go
|
||||
src/*.cgo2.c
|
||||
src/_cgo_defun.c
|
||||
src/_cgo_gotypes.go
|
||||
src/_cgo_export.*
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
src/_testmain.go
|
||||
|
||||
src/*.exe
|
||||
src/*.test
|
||||
src/*.prof
|
||||
|
||||
src/sys.db
|
||||
src/src/sys.db.lock
|
||||
src/conf/*
|
||||
src/ReverseProxy_*_*
|
||||
src/Zoraxy_*_*
|
||||
src/certs/*
|
||||
src/rules/*
|
21
LICENSE
@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Toby Chui
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
131
README.md
@ -1,2 +1,129 @@
|
||||
# NoobyRP
|
||||
A very simple to use reverse proxy server written in Go
|
||||

|
||||
# Zoraxy
|
||||
|
||||
General purpose request (reverse) proxy and forwarding tool for low power devices. Now written in Go!
|
||||
|
||||
### Features
|
||||
|
||||
- Simple to use interface with detail in-system instructions
|
||||
|
||||
- Reverse Proxy
|
||||
|
||||
- Subdomain Reverse Proxy
|
||||
|
||||
- Virtual Directory Reverse Proxy
|
||||
|
||||
- Redirection Rules
|
||||
|
||||
- TLS / SSL setup and deploy
|
||||
|
||||
- Blacklist by country or IP address (single IP, CIDR or wildcard for beginners :D)
|
||||
|
||||
- (More features work in progress)
|
||||
|
||||
## Usage
|
||||
|
||||
Zoraxy provide basic authentication system for standalone mode. To use it in standalone mode, follow the instruction below for your desired deployment platform.
|
||||
|
||||
### Standalone Mode
|
||||
|
||||
Standalone mode is the default mode for Zoraxy. This allow single account to manage your reverse proxy server just like a home router. This mode is suitable for new owners for homelab or makers start growing their web services into multiple servers.
|
||||
|
||||
#### Linux
|
||||
|
||||
```bash
|
||||
//Download the latest zoraxy binary and web.tar.gz from the Release page
|
||||
sudo chmod 775 ./zoraxy web.tar.gz
|
||||
sudo ./zoraxy -port=:8000
|
||||
```
|
||||
|
||||
#### Windows
|
||||
|
||||
Download the binary executable and web.tar.gz, put them into the same folder and double click the binary file to start it.
|
||||
|
||||
#### Raspberry Pi
|
||||
|
||||
The installation method is same as Linux. If you are using Raspberry Pi 4 or newer models, pick the arm64 release. For older version of Pis, use the arm (armv6) version instead.
|
||||
|
||||
#### Other ARM SBCs or Android phone with Termux
|
||||
|
||||
The installation method is same as Linux. For other ARM SBCs, please refer to your SBC's CPU architecture and pick the one that is suitable for your device.
|
||||
|
||||
### External Permission Managment Mode
|
||||
|
||||
If you already have a up-stream reverse proxy server in place with permission management, you can use Zoraxy in noauth mode. To enable no-auth mode, start Zoraxy with the following flag
|
||||
|
||||
```bash
|
||||
./zoraxy -noauth=true
|
||||
```
|
||||
|
||||
*Note: For security reaons, you should only enable no-auth if you are running Zoraxy in a trusted environment or with another authentication management proxy in front.*
|
||||
|
||||
#### Use with ArozOS
|
||||
|
||||
[ArozOS ](https://arozos.com)subservice is a build in permission managed reverse proxy server. To use zoraxy with arozos, connect to your arozos host via ssh and use the following command to install zoraxy
|
||||
|
||||
```bash
|
||||
# cd into your arozos subservice folder. Sometime it is under ~/arozos/src/subservice
|
||||
cd ~/arozos/subservices
|
||||
mkdir zoraxy
|
||||
cd ./zoraxy
|
||||
|
||||
# Download the release binary from Github release
|
||||
wget {binary executable link from release page}
|
||||
wget {web.tar.gz link from release page}
|
||||
|
||||
# Set permission. Change this if required
|
||||
sudo chmod 775 -R ./
|
||||
|
||||
# Start zoraxy to see if the downloaded arch is correct. If yes, you should
|
||||
# see it start unzipping
|
||||
./zoraxy
|
||||
|
||||
# After the unzip done, press Ctrl + C to kill it
|
||||
# Rename it to valid arozos subservice binary format
|
||||
mv ./zoraxy zoraxy_linux_amd64
|
||||
|
||||
# If you are using SBCs with different CPU arch
|
||||
mv ./zoraxy zoraxy_linux_arm
|
||||
mv ./zoraxy zoraxy_linux_arm64
|
||||
|
||||
# Restart arozos
|
||||
sudo systemctl restart arozos
|
||||
```
|
||||
|
||||
To start the module, go to System Settings > Modules > Subservice and enable it in the menu. You should be able to see a new module named "Zoraxy" pop up in the start menu.
|
||||
|
||||
## Build from Source
|
||||
|
||||
*Requirement: Go 1.17 or above*
|
||||
|
||||
```bash
|
||||
git clone https://github.com/tobychui/zoraxy
|
||||
cd ./zoraxy/src
|
||||
go mod tidy
|
||||
go build
|
||||
|
||||
./zoraxy
|
||||
```
|
||||
|
||||
### Forward Modes
|
||||
|
||||
#### Proxy Modes
|
||||
|
||||
There are two mode in the ReverseProxy Subservice
|
||||
|
||||
1. vdir mode (Virtual Dirctories)
|
||||
2. subd mode (Subdomain Proxying Mode)
|
||||
|
||||
Vdir mode proxy web request based on the virtual directories given in the request URL. For example, when configured to redirect /example to example.com, any visits to {your_domain}/example will be proxied to example.com.
|
||||
|
||||
Subd mode proxy web request based on sub-domain exists in the request URL. For example, when configured to redirect example.localhost to example.com, any visits that includes example.localhost (e.g. example.localhost/page1) will be proxied to example.com (e.g. example.com/page1)
|
||||
|
||||
#### Root Proxy
|
||||
|
||||
Root proxy is the main proxy destination where if all proxy root name did not match, the request will be proxied to this request. If you are working with ArozOS system in default configuration, you can set this to localhost:8080 for any unknown request to be handled by the host ArozOS system
|
||||
|
||||
## License
|
||||
|
||||
To be decided (Currently: All Right Reserved)
|
||||
|
185
common.go
@ -1,185 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
Basic Response Functions
|
||||
|
||||
Send response with ease
|
||||
*/
|
||||
//Send text response with given w and message as string
|
||||
func sendTextResponse(w http.ResponseWriter, msg string) {
|
||||
w.Write([]byte(msg))
|
||||
}
|
||||
|
||||
//Send JSON response, with an extra json header
|
||||
func sendJSONResponse(w http.ResponseWriter, json string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(json))
|
||||
}
|
||||
|
||||
func sendErrorResponse(w http.ResponseWriter, errMsg string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte("{\"error\":\"" + errMsg + "\"}"))
|
||||
}
|
||||
|
||||
func sendOK(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte("\"OK\""))
|
||||
}
|
||||
|
||||
/*
|
||||
The paramter move function (mv)
|
||||
|
||||
You can find similar things in the PHP version of ArOZ Online Beta. You need to pass in
|
||||
r (HTTP Request Object)
|
||||
getParamter (string, aka $_GET['This string])
|
||||
|
||||
Will return
|
||||
Paramter string (if any)
|
||||
Error (if error)
|
||||
|
||||
*/
|
||||
func mv(r *http.Request, getParamter string, postMode bool) (string, error) {
|
||||
if postMode == false {
|
||||
//Access the paramter via GET
|
||||
keys, ok := r.URL.Query()[getParamter]
|
||||
|
||||
if !ok || len(keys[0]) < 1 {
|
||||
//log.Println("Url Param " + getParamter +" is missing")
|
||||
return "", errors.New("GET paramter " + getParamter + " not found or it is empty")
|
||||
}
|
||||
|
||||
// Query()["key"] will return an array of items,
|
||||
// we only want the single item.
|
||||
key := keys[0]
|
||||
return string(key), nil
|
||||
} else {
|
||||
//Access the parameter via POST
|
||||
r.ParseForm()
|
||||
x := r.Form.Get(getParamter)
|
||||
if len(x) == 0 || x == "" {
|
||||
return "", errors.New("POST paramter " + getParamter + " not found or it is empty")
|
||||
}
|
||||
return string(x), nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func stringInSlice(a string, list []string) bool {
|
||||
for _, b := range list {
|
||||
if b == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func fileExists(filename string) bool {
|
||||
_, err := os.Stat(filename)
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func IsDir(path string) bool {
|
||||
if fileExists(path) == false {
|
||||
return false
|
||||
}
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return false
|
||||
}
|
||||
switch mode := fi.Mode(); {
|
||||
case mode.IsDir():
|
||||
return true
|
||||
case mode.IsRegular():
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func inArray(arr []string, str string) bool {
|
||||
for _, a := range arr {
|
||||
if a == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func timeToString(targetTime time.Time) string {
|
||||
return targetTime.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
func IntToString(number int) string {
|
||||
return strconv.Itoa(number)
|
||||
}
|
||||
|
||||
func StringToInt(number string) (int, error) {
|
||||
return strconv.Atoi(number)
|
||||
}
|
||||
|
||||
func StringToInt64(number string) (int64, error) {
|
||||
i, err := strconv.ParseInt(number, 10, 64)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func Int64ToString(number int64) string {
|
||||
convedNumber := strconv.FormatInt(number, 10)
|
||||
return convedNumber
|
||||
}
|
||||
|
||||
func GetUnixTime() int64 {
|
||||
return time.Now().Unix()
|
||||
}
|
||||
|
||||
func LoadImageAsBase64(filepath string) (string, error) {
|
||||
if !fileExists(filepath) {
|
||||
return "", errors.New("File not exists")
|
||||
}
|
||||
f, _ := os.Open(filepath)
|
||||
reader := bufio.NewReader(f)
|
||||
content, _ := ioutil.ReadAll(reader)
|
||||
encoded := base64.StdEncoding.EncodeToString(content)
|
||||
return string(encoded), nil
|
||||
}
|
||||
|
||||
//Get the IP address of the current authentication user
|
||||
func getUserIPAddr(w http.ResponseWriter, r *http.Request) {
|
||||
requestPort, _ := mv(r, "port", false)
|
||||
showPort := false
|
||||
if requestPort == "true" {
|
||||
//Show port as well
|
||||
showPort = true
|
||||
}
|
||||
IPAddress := r.Header.Get("X-Real-Ip")
|
||||
if IPAddress == "" {
|
||||
IPAddress = r.Header.Get("X-Forwarded-For")
|
||||
}
|
||||
if IPAddress == "" {
|
||||
IPAddress = r.RemoteAddr
|
||||
}
|
||||
if !showPort {
|
||||
IPAddress = IPAddress[:strings.LastIndex(IPAddress, ":")]
|
||||
|
||||
}
|
||||
w.Write([]byte(IPAddress))
|
||||
return
|
||||
}
|
9
go.mod
@ -1,9 +0,0 @@
|
||||
module imuslab.com/arozos/ReverseProxy
|
||||
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/boltdb/bolt v1.3.1
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 // indirect
|
||||
)
|
6
go.sum
@ -1,6 +0,0 @@
|
||||
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
|
||||
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 h1:xrCZDmdtoloIiooiA9q0OQb9r8HejIHYoHGhGCe1pGg=
|
||||
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
BIN
img/title.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
img/title.psd
Normal file
75
main.go
@ -1,75 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"imuslab.com/arozos/ReverseProxy/mod/aroz"
|
||||
"imuslab.com/arozos/ReverseProxy/mod/database"
|
||||
)
|
||||
|
||||
var (
|
||||
handler *aroz.ArozHandler
|
||||
sysdb *database.Database
|
||||
)
|
||||
|
||||
//Kill signal handler. Do something before the system the core terminate.
|
||||
func SetupCloseHandler() {
|
||||
c := make(chan os.Signal, 2)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-c
|
||||
log.Println("\r- Shutting down demo module.")
|
||||
//Do other things like close database or opened files
|
||||
sysdb.Close()
|
||||
|
||||
os.Exit(0)
|
||||
}()
|
||||
}
|
||||
|
||||
func main() {
|
||||
//Start the aoModule pipeline (which will parse the flags as well). Pass in the module launch information
|
||||
handler = aroz.HandleFlagParse(aroz.ServiceInfo{
|
||||
Name: "ReverseProxy",
|
||||
Desc: "Basic reverse proxy listener",
|
||||
Group: "Network",
|
||||
IconPath: "reverseproxy/img/small_icon.png",
|
||||
Version: "0.1",
|
||||
StartDir: "reverseproxy/index.html",
|
||||
SupportFW: true,
|
||||
LaunchFWDir: "reverseproxy/index.html",
|
||||
SupportEmb: false,
|
||||
InitFWSize: []int{1080, 580},
|
||||
})
|
||||
|
||||
//Register the standard web services urls
|
||||
fs := http.FileServer(http.Dir("./web"))
|
||||
|
||||
http.Handle("/", fs)
|
||||
|
||||
SetupCloseHandler()
|
||||
|
||||
//Create database
|
||||
db, err := database.NewDatabase("sys.db", false)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
sysdb = db
|
||||
|
||||
//Start the reverse proxy server in go routine
|
||||
go func() {
|
||||
ReverseProxtInit()
|
||||
}()
|
||||
|
||||
//Any log println will be shown in the core system via STDOUT redirection. But not STDIN.
|
||||
log.Println("ReverseProxy started. Listening on " + handler.Port)
|
||||
err = http.ListenAndServe(handler.Port, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
|
||||
package database // import "imuslab.com/arozos/mod/database"
|
||||
|
||||
|
||||
TYPES
|
||||
|
||||
type Database struct {
|
||||
Db *bolt.DB
|
||||
ReadOnly bool
|
||||
}
|
||||
|
||||
func NewDatabase(dbfile string, readOnlyMode bool) (*Database, error)
|
||||
|
||||
func (d *Database) Close()
|
||||
|
||||
func (d *Database) Delete(tableName string, key string) error
|
||||
Delete a value from the database table given tablename and key
|
||||
|
||||
err := sysdb.Delete("MyTable", "username/message");
|
||||
|
||||
func (d *Database) DropTable(tableName string) error
|
||||
|
||||
func (d *Database) KeyExists(tableName string, key string) bool
|
||||
|
||||
func (d *Database) ListTable(tableName string) ([][][]byte, error)
|
||||
|
||||
func (d *Database) NewTable(tableName string) error
|
||||
|
||||
func (d *Database) Read(tableName string, key string, assignee interface{}) error
|
||||
|
||||
func (d *Database) UpdateReadWriteMode(readOnly bool)
|
||||
|
||||
func (d *Database) Write(tableName string, key string, value interface{}) error
|
||||
Write to database with given tablename and key. Example Usage: type demo
|
||||
struct{
|
||||
|
||||
content string
|
||||
|
||||
} thisDemo := demo{
|
||||
|
||||
content: "Hello World",
|
||||
|
||||
} err := sysdb.Write("MyTable", "username/message",thisDemo);
|
||||
|
@ -1,216 +0,0 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"imuslab.com/arozos/ReverseProxy/mod/dynamicproxy/dpcore"
|
||||
"imuslab.com/arozos/ReverseProxy/mod/reverseproxy"
|
||||
)
|
||||
|
||||
/*
|
||||
Allow users to setup manual proxying for specific path
|
||||
|
||||
*/
|
||||
type Router struct {
|
||||
ListenPort int
|
||||
ProxyEndpoints *sync.Map
|
||||
SubdomainEndpoint *sync.Map
|
||||
Running bool
|
||||
Root *ProxyEndpoint
|
||||
mux http.Handler
|
||||
useTLS bool
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
type RouterOption struct {
|
||||
Port int
|
||||
}
|
||||
|
||||
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(port int) (*Router, error) {
|
||||
proxyMap := sync.Map{}
|
||||
domainMap := sync.Map{}
|
||||
thisRouter := Router{
|
||||
ListenPort: port,
|
||||
ProxyEndpoints: &proxyMap,
|
||||
SubdomainEndpoint: &domainMap,
|
||||
Running: false,
|
||||
useTLS: false,
|
||||
server: nil,
|
||||
}
|
||||
|
||||
thisRouter.mux = &ProxyHandler{
|
||||
Parent: &thisRouter,
|
||||
}
|
||||
|
||||
return &thisRouter, nil
|
||||
}
|
||||
|
||||
//Start the dynamic routing
|
||||
func (router *Router) StartProxyService() error {
|
||||
//Create a new server object
|
||||
if router.server != nil {
|
||||
return errors.New("Reverse proxy server already running")
|
||||
}
|
||||
|
||||
if router.Root == nil {
|
||||
return errors.New("Reverse proxy router root not set")
|
||||
}
|
||||
|
||||
router.server = &http.Server{Addr: ":" + strconv.Itoa(router.ListenPort), Handler: router.mux}
|
||||
router.Running = true
|
||||
go func() {
|
||||
err := router.server.ListenAndServe()
|
||||
log.Println("[DynamicProxy] " + err.Error())
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (router *Router) StopProxyService() error {
|
||||
if router.server == nil {
|
||||
return errors.New("Reverse proxy server already stopped")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
err := router.server.Shutdown(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//Discard the server object
|
||||
router.server = nil
|
||||
router.Running = false
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
Add an URL into a custom proxy services
|
||||
*/
|
||||
func (router *Router) AddVirtualDirectoryProxyService(rootname string, domain string, requireTLS bool) error {
|
||||
if domain[len(domain)-1:] == "/" {
|
||||
domain = domain[:len(domain)-1]
|
||||
}
|
||||
|
||||
if rootname[len(rootname)-1:] == "/" {
|
||||
rootname = rootname[:len(rootname)-1]
|
||||
}
|
||||
|
||||
webProxyEndpoint := domain
|
||||
if requireTLS {
|
||||
webProxyEndpoint = "https://" + webProxyEndpoint
|
||||
} else {
|
||||
webProxyEndpoint = "http://" + webProxyEndpoint
|
||||
}
|
||||
//Create a new proxy agent for this root
|
||||
path, err := url.Parse(webProxyEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
proxy := dpcore.NewDynamicProxyCore(path, rootname)
|
||||
|
||||
endpointObject := ProxyEndpoint{
|
||||
Root: rootname,
|
||||
Domain: domain,
|
||||
RequireTLS: requireTLS,
|
||||
Proxy: proxy,
|
||||
}
|
||||
|
||||
router.ProxyEndpoints.Store(rootname, &endpointObject)
|
||||
|
||||
log.Println("Adding Proxy Rule: ", rootname+" to "+domain)
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
Remove routing from RP
|
||||
|
||||
*/
|
||||
func (router *Router) RemoveProxy(ptype string, key string) error {
|
||||
if ptype == "vdir" {
|
||||
router.ProxyEndpoints.Delete(key)
|
||||
return nil
|
||||
} else if ptype == "subd" {
|
||||
router.SubdomainEndpoint.Delete(key)
|
||||
return nil
|
||||
}
|
||||
return errors.New("invalid ptype")
|
||||
}
|
||||
|
||||
/*
|
||||
Add an default router for the proxy server
|
||||
*/
|
||||
func (router *Router) SetRootProxy(proxyLocation string, requireTLS bool) error {
|
||||
if proxyLocation[len(proxyLocation)-1:] == "/" {
|
||||
proxyLocation = proxyLocation[:len(proxyLocation)-1]
|
||||
}
|
||||
|
||||
webProxyEndpoint := proxyLocation
|
||||
if requireTLS {
|
||||
webProxyEndpoint = "https://" + webProxyEndpoint
|
||||
} else {
|
||||
webProxyEndpoint = "http://" + webProxyEndpoint
|
||||
}
|
||||
//Create a new proxy agent for this root
|
||||
path, err := url.Parse(webProxyEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
proxy := dpcore.NewDynamicProxyCore(path, "")
|
||||
|
||||
rootEndpoint := ProxyEndpoint{
|
||||
Root: "/",
|
||||
Domain: proxyLocation,
|
||||
RequireTLS: requireTLS,
|
||||
Proxy: proxy,
|
||||
}
|
||||
|
||||
router.Root = &rootEndpoint
|
||||
return nil
|
||||
}
|
||||
|
||||
//Do all the main routing in here
|
||||
func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.Contains(r.Host, ".") {
|
||||
//This might be a subdomain. See if there are any subdomain proxy router for this
|
||||
sep := h.Parent.getSubdomainProxyEndpointFromHostname(r.Host)
|
||||
if sep != nil {
|
||||
h.subdomainRequest(w, r, sep)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
targetProxyEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(r.RequestURI)
|
||||
if targetProxyEndpoint != nil {
|
||||
h.proxyRequest(w, r, targetProxyEndpoint)
|
||||
} else {
|
||||
h.proxyRequest(w, r, h.Parent.Root)
|
||||
}
|
||||
}
|
201
reverseproxy.go
@ -1,201 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"imuslab.com/arozos/ReverseProxy/mod/dynamicproxy"
|
||||
)
|
||||
|
||||
var (
|
||||
dynamicProxyRouter *dynamicproxy.Router
|
||||
)
|
||||
|
||||
//Add user customizable reverse proxy
|
||||
func ReverseProxtInit() {
|
||||
dprouter, err := dynamicproxy.NewDynamicProxy(80)
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
dynamicProxyRouter = dprouter
|
||||
|
||||
http.HandleFunc("/enable", ReverseProxyHandleOnOff)
|
||||
http.HandleFunc("/add", ReverseProxyHandleAddEndpoint)
|
||||
http.HandleFunc("/status", ReverseProxyStatus)
|
||||
http.HandleFunc("/list", ReverseProxyList)
|
||||
http.HandleFunc("/del", DeleteProxyEndpoint)
|
||||
|
||||
//Load all conf from files
|
||||
confs, _ := filepath.Glob("./conf/*.config")
|
||||
for _, conf := range confs {
|
||||
record, err := LoadReverseProxyConfig(conf)
|
||||
if err != nil {
|
||||
log.Println("Failed to load "+filepath.Base(conf), err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if record.ProxyType == "root" {
|
||||
dynamicProxyRouter.SetRootProxy(record.ProxyTarget, record.UseTLS)
|
||||
} else if record.ProxyType == "subd" {
|
||||
dynamicProxyRouter.AddSubdomainRoutingService(record.Rootname, record.ProxyTarget, record.UseTLS)
|
||||
} else if record.ProxyType == "vdir" {
|
||||
dynamicProxyRouter.AddVirtualDirectoryProxyService(record.Rootname, record.ProxyTarget, record.UseTLS)
|
||||
} 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
|
||||
dynamicProxyRouter.StartProxyService()
|
||||
|
||||
/*
|
||||
go func() {
|
||||
time.Sleep(10 * time.Second)
|
||||
dynamicProxyRouter.StopProxyService()
|
||||
fmt.Println("Proxy stopped")
|
||||
}()
|
||||
*/
|
||||
log.Println("Dynamic Proxy service started")
|
||||
|
||||
}
|
||||
|
||||
func ReverseProxyHandleOnOff(w http.ResponseWriter, r *http.Request) {
|
||||
enable, _ := mv(r, "enable", true) //Support root, vdir and subd
|
||||
if enable == "true" {
|
||||
err := dynamicProxyRouter.StartProxyService()
|
||||
if err != nil {
|
||||
sendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := dynamicProxyRouter.StopProxyService()
|
||||
if err != nil {
|
||||
sendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
sendOK(w)
|
||||
}
|
||||
|
||||
func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
eptype, err := mv(r, "type", true) //Support root, vdir and subd
|
||||
if err != nil {
|
||||
sendErrorResponse(w, "type not defined")
|
||||
return
|
||||
}
|
||||
|
||||
endpoint, err := mv(r, "ep", true)
|
||||
if err != nil {
|
||||
sendErrorResponse(w, "endpoint not defined")
|
||||
return
|
||||
}
|
||||
|
||||
tls, _ := mv(r, "tls", true)
|
||||
if tls == "" {
|
||||
tls = "false"
|
||||
}
|
||||
|
||||
useTLS := (tls == "true")
|
||||
rootname := ""
|
||||
if eptype == "vdir" {
|
||||
vdir, err := mv(r, "rootname", true)
|
||||
if err != nil {
|
||||
sendErrorResponse(w, "vdir not defined")
|
||||
return
|
||||
}
|
||||
rootname = vdir
|
||||
dynamicProxyRouter.AddVirtualDirectoryProxyService(vdir, endpoint, useTLS)
|
||||
|
||||
} else if eptype == "subd" {
|
||||
subdomain, err := mv(r, "rootname", true)
|
||||
if err != nil {
|
||||
sendErrorResponse(w, "subdomain not defined")
|
||||
return
|
||||
}
|
||||
rootname = subdomain
|
||||
dynamicProxyRouter.AddSubdomainRoutingService(subdomain, endpoint, useTLS)
|
||||
} else if eptype == "root" {
|
||||
rootname = "root"
|
||||
dynamicProxyRouter.SetRootProxy(endpoint, useTLS)
|
||||
} else {
|
||||
//Invalid eptype
|
||||
sendErrorResponse(w, "Invalid endpoint type")
|
||||
return
|
||||
}
|
||||
|
||||
//Save it
|
||||
SaveReverseProxyConfig(eptype, rootname, endpoint, useTLS)
|
||||
|
||||
sendOK(w)
|
||||
|
||||
}
|
||||
|
||||
func DeleteProxyEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
ep, err := mv(r, "ep", true)
|
||||
if err != nil {
|
||||
sendErrorResponse(w, "Invalid ep given")
|
||||
}
|
||||
|
||||
ptype, err := mv(r, "ptype", true)
|
||||
if err != nil {
|
||||
sendErrorResponse(w, "Invalid ptype given")
|
||||
}
|
||||
|
||||
err = dynamicProxyRouter.RemoveProxy(ptype, ep)
|
||||
if err != nil {
|
||||
sendErrorResponse(w, err.Error())
|
||||
}
|
||||
|
||||
RemoveReverseProxyConfig(ep)
|
||||
sendOK(w)
|
||||
}
|
||||
|
||||
func ReverseProxyStatus(w http.ResponseWriter, r *http.Request) {
|
||||
js, _ := json.Marshal(dynamicProxyRouter)
|
||||
sendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
|
||||
eptype, err := mv(r, "type", true) //Support root, vdir and subd
|
||||
if err != nil {
|
||||
sendErrorResponse(w, "type not defined")
|
||||
return
|
||||
}
|
||||
|
||||
if eptype == "vdir" {
|
||||
results := []*dynamicproxy.ProxyEndpoint{}
|
||||
dynamicProxyRouter.ProxyEndpoints.Range(func(key, value interface{}) bool {
|
||||
results = append(results, value.(*dynamicproxy.ProxyEndpoint))
|
||||
return true
|
||||
})
|
||||
|
||||
js, _ := json.Marshal(results)
|
||||
sendJSONResponse(w, string(js))
|
||||
} else if eptype == "subd" {
|
||||
results := []*dynamicproxy.SubdomainEndpoint{}
|
||||
dynamicProxyRouter.SubdomainEndpoint.Range(func(key, value interface{}) bool {
|
||||
results = append(results, value.(*dynamicproxy.SubdomainEndpoint))
|
||||
return true
|
||||
})
|
||||
js, _ := json.Marshal(results)
|
||||
sendJSONResponse(w, string(js))
|
||||
} else if eptype == "root" {
|
||||
js, _ := json.Marshal(dynamicProxyRouter.Root)
|
||||
sendJSONResponse(w, string(js))
|
||||
} else {
|
||||
sendErrorResponse(w, "Invalid type given")
|
||||
}
|
||||
}
|
54
src/Makefile
Normal file
@ -0,0 +1,54 @@
|
||||
# PLATFORMS := darwin/amd64 darwin/arm64 freebsd/amd64 linux/386 linux/amd64 linux/arm linux/arm64 linux/mipsle windows/386 windows/amd64 windows/arm windows/arm64
|
||||
PLATFORMS := darwin/amd64 darwin/arm64 linux/amd64 linux/arm linux/arm64 linux/mipsle linux/riscv64 windows/amd64 windows/arm64
|
||||
temp = $(subst /, ,$@)
|
||||
os = $(word 1, $(temp))
|
||||
arch = $(word 2, $(temp))
|
||||
|
||||
all: web.tar.gz $(PLATFORMS) fixwindows zoraxy_file_checksum.sha1
|
||||
|
||||
binary: $(PLATFORMS)
|
||||
|
||||
hash: zoraxy_file_checksum.sha1
|
||||
|
||||
web: web.tar.gz
|
||||
|
||||
clean:
|
||||
rm -f zoraxy_*_*
|
||||
rm -f web.tar.gz
|
||||
|
||||
$(PLATFORMS):
|
||||
@echo "Building $(os)/$(arch)"
|
||||
GOROOT_FINAL=Git/ GOOS=$(os) GOARCH=$(arch) GOARM=6 go build -o './dist/zoraxy_$(os)_$(arch)' -ldflags "-s -w" -trimpath
|
||||
|
||||
|
||||
fixwindows:
|
||||
-mv ./dist/zoraxy_windows_amd64 ./dist/zoraxy_windows_amd64.exe
|
||||
-mv ./dist/zoraxy_windows_arm64 ./dist/zoraxy_windows_arm64.exe
|
||||
|
||||
web.tar.gz:
|
||||
|
||||
@echo "Removing old build resources, if exists"
|
||||
-rm -rf ./dist/
|
||||
-mkdir ./dist/
|
||||
|
||||
@echo "Moving subfolders to build folder"
|
||||
-cp -r ./web ./dist/web/
|
||||
-cp -r ./system ./dist/system/
|
||||
|
||||
@ echo "Remove sensitive information"
|
||||
-rm -rf ./dist/certs/
|
||||
-rm -rf ./dist/conf/
|
||||
-rm -rf ./dist/rules/
|
||||
|
||||
|
||||
@echo "Creating tarball for all required files"
|
||||
-rm ./dist/web.tar.gz
|
||||
-cd ./dist/ && tar -czf ./web.tar.gz system/ web/
|
||||
|
||||
@echo "Clearing up unzipped folder structures"
|
||||
-rm -rf "./dist/web"
|
||||
-rm -rf "./dist/system"
|
||||
|
||||
zoraxy_file_checksum.sha1:
|
||||
@echo "Generating the checksum, if sha1sum installed"
|
||||
-sha1sum ./dist/web.tar.gz > ./dist/zoraxy_file_checksum.sha1
|
136
src/README.md
Normal file
@ -0,0 +1,136 @@
|
||||
# Zoraxy
|
||||
|
||||
General purpose request (reverse) proxy and forwarding tool for low power devices. Now written in Go!
|
||||
|
||||
### Features
|
||||
|
||||
- Simple to use interface with detail in-system instructions
|
||||
|
||||
- Reverse Proxy
|
||||
|
||||
- Subdomain Reverse Proxy
|
||||
|
||||
- Virtual Directory Reverse Proxy
|
||||
|
||||
- Redirection Rules
|
||||
|
||||
- TLS / SSL setup and deploy
|
||||
|
||||
- Blacklist by country or IP address (single IP, CIDR or wildcard for beginners :D)
|
||||
|
||||
- (More features work in progress)
|
||||
|
||||
## Usage
|
||||
|
||||
Zoraxy provide basic authentication system for standalone mode. To use it in standalone mode, follow the instruction below for your desired deployment platform.
|
||||
|
||||
### Standalone Mode
|
||||
|
||||
Standalone mode is the default mode for Zoraxy. This allow single account to manage your reverse proxy server just like a home router. This mode is suitable for new owners for homelab or makers start growing their web services into multiple servers.
|
||||
|
||||
#### Linux
|
||||
|
||||
```bash
|
||||
//Download the latest zoraxy binary and web.tar.gz from the Release page
|
||||
sudo chmod 775 ./zoraxy web.tar.gz
|
||||
sudo ./zoraxy -port=:8000
|
||||
```
|
||||
|
||||
#### Windows
|
||||
|
||||
Download the binary executable and web.tar.gz, put them into the same folder and double click the binary file to start it.
|
||||
|
||||
#### Raspberry Pi
|
||||
|
||||
The installation method is same as Linux. If you are using Raspberry Pi 4 or newer models, pick the arm64 release. For older version of Pis, use the arm (armv6) version instead.
|
||||
|
||||
#### Other ARM SBCs or Android phone with Termux
|
||||
|
||||
The installation method is same as Linux. For other ARM SBCs, please refer to your SBC's CPU architecture and pick the one that is suitable for your device.
|
||||
|
||||
### External Permission Managment Mode
|
||||
|
||||
If you already have a up-stream reverse proxy server in place with permission management, you can use Zoraxy in noauth mode. To enable no-auth mode, start Zoraxy with the following flag
|
||||
|
||||
```bash
|
||||
./zoraxy -noauth=true
|
||||
```
|
||||
|
||||
*Note: For security reaons, you should only enable no-auth if you are running Zoraxy in a trusted environment or with another authentication management proxy in front.*
|
||||
|
||||
#### Use with ArozOS
|
||||
|
||||
[ArozOS ](https://arozos.com)subservice is a build in permission managed reverse proxy server. To use zoraxy with arozos, connect to your arozos host via ssh and use the following command to install zoraxy
|
||||
|
||||
```bash
|
||||
# cd into your arozos subservice folder. Sometime it is under ~/arozos/src/subservice
|
||||
cd ~/arozos/subservices
|
||||
mkdir zoraxy
|
||||
cd ./zoraxy
|
||||
|
||||
# Download the release binary from Github release
|
||||
wget {binary executable link from release page}
|
||||
wget {web.tar.gz link from release page}
|
||||
|
||||
# Set permission. Change this if required
|
||||
sudo chmod 775 -R ./
|
||||
|
||||
# Start zoraxy to see if the downloaded arch is correct. If yes, you should
|
||||
# see it start unzipping
|
||||
./zoraxy
|
||||
|
||||
# After the unzip done, press Ctrl + C to kill it
|
||||
# Rename it to valid arozos subservice binary format
|
||||
mv ./zoraxy zoraxy_linux_amd64
|
||||
|
||||
# If you are using SBCs with different CPU arch
|
||||
mv ./zoraxy zoraxy_linux_arm
|
||||
mv ./zoraxy zoraxy_linux_arm64
|
||||
|
||||
# Restart arozos
|
||||
sudo systemctl restart arozos
|
||||
|
||||
|
||||
```
|
||||
|
||||
To start the module, go to System Settings > Modules > Subservice and enable it in the menu. You should be able to see a new module named "Zoraxy" pop up in the start menu.
|
||||
|
||||
|
||||
|
||||
## Build from Source
|
||||
|
||||
*Requirement: Go 1.17 or above*
|
||||
|
||||
```bash
|
||||
git clone https://github.com/tobychui/zoraxy
|
||||
cd ./zoraxy/src
|
||||
go mod tidy
|
||||
go build
|
||||
|
||||
./zoraxy
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Forward Modes
|
||||
|
||||
#### Proxy Modes
|
||||
|
||||
There are two mode in the ReverseProxy Subservice
|
||||
|
||||
1. vdir mode (Virtual Dirctories)
|
||||
2. subd mode (Subdomain Proxying Mode)
|
||||
|
||||
Vdir mode proxy web request based on the virtual directories given in the request URL. For example, when configured to redirect /example to example.com, any visits to {your_domain}/example will be proxied to example.com.
|
||||
|
||||
Subd mode proxy web request based on sub-domain exists in the request URL. For example, when configured to redirect example.localhost to example.com, any visits that includes example.localhost (e.g. example.localhost/page1) will be proxied to example.com (e.g. example.com/page1)
|
||||
|
||||
#### Root Proxy
|
||||
|
||||
Root proxy is the main proxy destination where if all proxy root name did not match, the request will be proxied to this request. If you are working with ArozOS system in default configuration, you can set this to localhost:8080 for any unknown request to be handled by the host ArozOS system
|
||||
|
||||
|
||||
|
||||
## License
|
||||
|
||||
To be decided (Currently: All Right Reserved)
|
153
src/api.go
Normal file
@ -0,0 +1,153 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
/*
|
||||
API.go
|
||||
|
||||
This file contains all the API called by the web management interface
|
||||
|
||||
*/
|
||||
|
||||
func initAPIs() {
|
||||
requireAuth := !(*noauth || handler.IsUsingExternalPermissionManager())
|
||||
authRouter := auth.NewManagedHTTPRouter(auth.RouterOption{
|
||||
AuthAgent: authAgent,
|
||||
RequireAuth: requireAuth,
|
||||
DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "401 - Unauthorized", http.StatusUnauthorized)
|
||||
},
|
||||
})
|
||||
|
||||
//Register the standard web services urls
|
||||
fs := http.FileServer(http.Dir("./web"))
|
||||
if requireAuth {
|
||||
//Add a layer of middleware for auth control
|
||||
authHandler := AuthFsHandler(fs)
|
||||
http.Handle("/", authHandler)
|
||||
} else {
|
||||
http.Handle("/", fs)
|
||||
}
|
||||
|
||||
//Authentication APIs
|
||||
registerAuthAPIs(requireAuth)
|
||||
|
||||
//Reverse proxy
|
||||
authRouter.HandleFunc("/api/proxy/enable", ReverseProxyHandleOnOff)
|
||||
authRouter.HandleFunc("/api/proxy/add", ReverseProxyHandleAddEndpoint)
|
||||
authRouter.HandleFunc("/api/proxy/status", ReverseProxyStatus)
|
||||
authRouter.HandleFunc("/api/proxy/list", ReverseProxyList)
|
||||
authRouter.HandleFunc("/api/proxy/del", DeleteProxyEndpoint)
|
||||
authRouter.HandleFunc("/api/proxy/setIncoming", HandleIncomingPortSet)
|
||||
authRouter.HandleFunc("/api/proxy/useHttpsRedirect", HandleUpdateHttpsRedirect)
|
||||
authRouter.HandleFunc("/api/proxy/requestIsProxied", HandleManagementProxyCheck)
|
||||
|
||||
//TLS / SSL config
|
||||
authRouter.HandleFunc("/api/cert/tls", handleToggleTLSProxy)
|
||||
authRouter.HandleFunc("/api/cert/upload", handleCertUpload)
|
||||
authRouter.HandleFunc("/api/cert/list", handleListCertificate)
|
||||
authRouter.HandleFunc("/api/cert/checkDefault", handleDefaultCertCheck)
|
||||
authRouter.HandleFunc("/api/cert/delete", handleCertRemove)
|
||||
|
||||
//Redirection config
|
||||
authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules)
|
||||
authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule)
|
||||
authRouter.HandleFunc("/api/redirect/delete", handleDeleteRedirectionRule)
|
||||
|
||||
//Blacklist APIs
|
||||
authRouter.HandleFunc("/api/blacklist/list", handleListBlacklisted)
|
||||
authRouter.HandleFunc("/api/blacklist/country/add", handleCountryBlacklistAdd)
|
||||
authRouter.HandleFunc("/api/blacklist/country/remove", handleCountryBlacklistRemove)
|
||||
authRouter.HandleFunc("/api/blacklist/ip/add", handleIpBlacklistAdd)
|
||||
authRouter.HandleFunc("/api/blacklist/ip/remove", handleIpBlacklistRemove)
|
||||
authRouter.HandleFunc("/api/blacklist/enable", handleBlacklistEnable)
|
||||
|
||||
//Statistic API
|
||||
authRouter.HandleFunc("/api/stats/summary", statisticCollector.HandleTodayStatLoad)
|
||||
|
||||
//Upnp
|
||||
authRouter.HandleFunc("/api/upnp/discover", handleUpnpDiscover)
|
||||
//If you got APIs to add, append them here
|
||||
}
|
||||
|
||||
//Function to renders Auth related APIs
|
||||
func registerAuthAPIs(requireAuth bool) {
|
||||
//Auth APIs
|
||||
http.HandleFunc("/api/auth/login", authAgent.HandleLogin)
|
||||
http.HandleFunc("/api/auth/logout", authAgent.HandleLogout)
|
||||
http.HandleFunc("/api/auth/checkLogin", func(w http.ResponseWriter, r *http.Request) {
|
||||
if requireAuth {
|
||||
authAgent.CheckLogin(w, r)
|
||||
} else {
|
||||
utils.SendJSONResponse(w, "true")
|
||||
}
|
||||
})
|
||||
http.HandleFunc("/api/auth/username", func(w http.ResponseWriter, r *http.Request) {
|
||||
username, err := authAgent.GetUserName(w, r)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(username)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
})
|
||||
http.HandleFunc("/api/auth/userCount", func(w http.ResponseWriter, r *http.Request) {
|
||||
uc := authAgent.GetUserCounts()
|
||||
js, _ := json.Marshal(uc)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
})
|
||||
http.HandleFunc("/api/auth/register", func(w http.ResponseWriter, r *http.Request) {
|
||||
if authAgent.GetUserCounts() == 0 {
|
||||
//Allow register root admin
|
||||
authAgent.HandleRegisterWithoutEmail(w, r, func(username, reserved string) {
|
||||
|
||||
})
|
||||
} else {
|
||||
//This function is disabled
|
||||
utils.SendErrorResponse(w, "Root management account already exists")
|
||||
}
|
||||
})
|
||||
http.HandleFunc("/api/auth/changePassword", func(w http.ResponseWriter, r *http.Request) {
|
||||
username, err := authAgent.GetUserName(w, r)
|
||||
if err != nil {
|
||||
http.Error(w, "401 - Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
oldPassword, err := utils.PostPara(r, "oldPassword")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "empty current password")
|
||||
return
|
||||
}
|
||||
newPassword, err := utils.PostPara(r, "newPassword")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "empty new password")
|
||||
return
|
||||
}
|
||||
confirmPassword, _ := utils.PostPara(r, "confirmPassword")
|
||||
|
||||
if newPassword != confirmPassword {
|
||||
utils.SendErrorResponse(w, "confirm password not match")
|
||||
return
|
||||
}
|
||||
|
||||
//Check if the old password correct
|
||||
oldPasswordCorrect, _ := authAgent.ValidateUsernameAndPasswordWithReason(username, oldPassword)
|
||||
if !oldPasswordCorrect {
|
||||
utils.SendErrorResponse(w, "Invalid current password given")
|
||||
return
|
||||
}
|
||||
|
||||
//Change the password of the root user
|
||||
authAgent.UnregisterUser(username)
|
||||
authAgent.CreateUserAccount(username, newPassword, "")
|
||||
})
|
||||
|
||||
}
|
102
src/blacklist.go
Normal file
@ -0,0 +1,102 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
/*
|
||||
blacklist.go
|
||||
|
||||
This script file is added to extend the
|
||||
reverse proxy function to include
|
||||
banning a specific IP address or country code
|
||||
*/
|
||||
|
||||
//List a of blacklisted ip address or country code
|
||||
func handleListBlacklisted(w http.ResponseWriter, r *http.Request) {
|
||||
bltype, err := utils.GetPara(r, "type")
|
||||
if err != nil {
|
||||
bltype = "country"
|
||||
}
|
||||
|
||||
resulst := []string{}
|
||||
if bltype == "country" {
|
||||
resulst = geodbStore.GetAllBlacklistedCountryCode()
|
||||
} else if bltype == "ip" {
|
||||
resulst = geodbStore.GetAllBlacklistedIp()
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(resulst)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
|
||||
}
|
||||
|
||||
func handleCountryBlacklistAdd(w http.ResponseWriter, r *http.Request) {
|
||||
countryCode, err := utils.PostPara(r, "cc")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid or empty country code")
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.AddCountryCodeToBlackList(countryCode)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func handleCountryBlacklistRemove(w http.ResponseWriter, r *http.Request) {
|
||||
countryCode, err := utils.PostPara(r, "cc")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid or empty country code")
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.RemoveCountryCodeFromBlackList(countryCode)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func handleIpBlacklistAdd(w http.ResponseWriter, r *http.Request) {
|
||||
ipAddr, err := utils.PostPara(r, "ip")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid or empty ip address")
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.AddIPToBlackList(ipAddr)
|
||||
}
|
||||
|
||||
func handleIpBlacklistRemove(w http.ResponseWriter, r *http.Request) {
|
||||
ipAddr, err := utils.PostPara(r, "ip")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid or empty ip address")
|
||||
return
|
||||
}
|
||||
|
||||
geodbStore.RemoveIPFromBlackList(ipAddr)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func handleBlacklistEnable(w http.ResponseWriter, r *http.Request) {
|
||||
enable, err := utils.PostPara(r, "enable")
|
||||
if err != nil {
|
||||
//Return the current enabled state
|
||||
currentEnabled := geodbStore.Enabled
|
||||
js, _ := json.Marshal(currentEnabled)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
if enable == "true" {
|
||||
geodbStore.ToggleBlacklist(true)
|
||||
} else if enable == "false" {
|
||||
geodbStore.ToggleBlacklist(false)
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "invalid enable state: only true and false is accepted")
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
}
|
189
src/cert.go
Normal file
@ -0,0 +1,189 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
// Check if the default certificates is correctly setup
|
||||
func handleDefaultCertCheck(w http.ResponseWriter, r *http.Request) {
|
||||
type CheckResult struct {
|
||||
DefaultPubExists bool
|
||||
DefaultPriExists bool
|
||||
}
|
||||
|
||||
pub, pri := tlsCertManager.DefaultCertExistsSep()
|
||||
js, _ := json.Marshal(CheckResult{
|
||||
pub,
|
||||
pri,
|
||||
})
|
||||
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
// Return a list of domains where the certificates covers
|
||||
func handleListCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
filenames, err := tlsCertManager.ListCertDomains()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
showDate, _ := utils.GetPara(r, "date")
|
||||
if showDate == "true" {
|
||||
type CertInfo struct {
|
||||
Domain string
|
||||
LastModifiedDate string
|
||||
}
|
||||
|
||||
results := []*CertInfo{}
|
||||
|
||||
for _, filename := range filenames {
|
||||
fileInfo, err := os.Stat(filepath.Join(tlsCertManager.CertStore, filename+".crt"))
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid domain certificate discovered: "+filename)
|
||||
return
|
||||
}
|
||||
modifiedTime := fileInfo.ModTime().Format("2006-01-02 15:04:05")
|
||||
|
||||
thisCertInfo := CertInfo{
|
||||
Domain: filename,
|
||||
LastModifiedDate: modifiedTime,
|
||||
}
|
||||
|
||||
results = append(results, &thisCertInfo)
|
||||
}
|
||||
|
||||
js, _ := json.Marshal(results)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(js)
|
||||
} else {
|
||||
response, err := json.Marshal(filenames)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(response)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Handle front-end toggling TLS mode
|
||||
func handleToggleTLSProxy(w http.ResponseWriter, r *http.Request) {
|
||||
currentTlsSetting := false
|
||||
if sysdb.KeyExists("settings", "usetls") {
|
||||
sysdb.Read("settings", "usetls", ¤tTlsSetting)
|
||||
}
|
||||
|
||||
newState, err := utils.PostPara(r, "set")
|
||||
if err != nil {
|
||||
//No setting. Get the current status
|
||||
js, _ := json.Marshal(currentTlsSetting)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
if newState == "true" {
|
||||
sysdb.Write("settings", "usetls", true)
|
||||
log.Println("Enabling TLS mode on reverse proxy")
|
||||
dynamicProxyRouter.UpdateTLSSetting(true)
|
||||
} else if newState == "false" {
|
||||
sysdb.Write("settings", "usetls", false)
|
||||
log.Println("Disabling TLS mode on reverse proxy")
|
||||
dynamicProxyRouter.UpdateTLSSetting(false)
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "invalid state given. Only support true or false")
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Handle upload of the certificate
|
||||
func handleCertUpload(w http.ResponseWriter, r *http.Request) {
|
||||
// check if request method is POST
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// get the key type
|
||||
keytype, err := utils.GetPara(r, "ktype")
|
||||
overWriteFilename := ""
|
||||
if err != nil {
|
||||
http.Error(w, "Not defined key type (pub / pri)", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// get the domain
|
||||
domain, err := utils.GetPara(r, "domain")
|
||||
if err != nil {
|
||||
//Assume localhost
|
||||
domain = "default"
|
||||
}
|
||||
|
||||
if keytype == "pub" {
|
||||
overWriteFilename = domain + ".crt"
|
||||
} else if keytype == "pri" {
|
||||
overWriteFilename = domain + ".key"
|
||||
} else {
|
||||
http.Error(w, "Not supported keytype: "+keytype, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// parse multipart form data
|
||||
err = r.ParseMultipartForm(10 << 20) // 10 MB
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to parse form data", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// get file from form data
|
||||
file, _, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get file", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// create file in upload directory
|
||||
os.MkdirAll("./certs", 0775)
|
||||
f, err := os.Create(filepath.Join("./certs", overWriteFilename))
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to create file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// copy file contents to destination file
|
||||
_, err = io.Copy(f, file)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to save file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// send response
|
||||
fmt.Fprintln(w, "File upload successful!")
|
||||
}
|
||||
|
||||
// Handle cert remove
|
||||
func handleCertRemove(w http.ResponseWriter, r *http.Request) {
|
||||
domain, err := utils.PostPara(r, "domain")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid domain given")
|
||||
return
|
||||
}
|
||||
err = tlsCertManager.RemoveCert(domain)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
}
|
||||
}
|
@ -7,6 +7,8 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
type Record struct {
|
||||
@ -35,9 +37,10 @@ func SaveReverseProxyConfig(ptype string, rootname string, proxyTarget string, u
|
||||
|
||||
func RemoveReverseProxyConfig(rootname string) error {
|
||||
filename := getFilenameFromRootName(rootname)
|
||||
log.Println("Config Removed: ", filepath.Join("conf", filename))
|
||||
if fileExists(filepath.Join("conf", filename)) {
|
||||
err := os.Remove(filepath.Join("conf", filename))
|
||||
removePendingFile := strings.ReplaceAll(filepath.Join("conf", filename), "\\", "/")
|
||||
log.Println("Config Removed: ", removePendingFile)
|
||||
if utils.FileExists(removePendingFile) {
|
||||
err := os.Remove(removePendingFile)
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
return err
|
||||
@ -48,7 +51,7 @@ func RemoveReverseProxyConfig(rootname string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//Return ptype, rootname and proxyTarget, error if any
|
||||
// Return ptype, rootname and proxyTarget, error if any
|
||||
func LoadReverseProxyConfig(filename string) (*Record, error) {
|
||||
thisRecord := Record{}
|
||||
configContent, err := ioutil.ReadFile(filename)
|
39
src/geoip.go
Normal file
@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/oschwald/geoip2-golang"
|
||||
)
|
||||
|
||||
func getCountryCodeFromRequest(r *http.Request) string {
|
||||
countryCode := ""
|
||||
|
||||
// Get the IP address of the user from the request headers
|
||||
ipAddress := r.Header.Get("X-Forwarded-For")
|
||||
if ipAddress == "" {
|
||||
ipAddress = strings.Split(r.RemoteAddr, ":")[0]
|
||||
}
|
||||
|
||||
// Open the GeoIP database
|
||||
db, err := geoip2.Open("./system/GeoIP2-Country.mmdb")
|
||||
if err != nil {
|
||||
// Handle the error
|
||||
return countryCode
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Look up the country code for the IP address
|
||||
record, err := db.Country(net.ParseIP(ipAddress))
|
||||
if err != nil {
|
||||
// Handle the error
|
||||
return countryCode
|
||||
}
|
||||
|
||||
// Get the ISO country code from the record
|
||||
countryCode = record.Country.IsoCode
|
||||
|
||||
return countryCode
|
||||
}
|
12
src/go.mod
Normal file
@ -0,0 +1,12 @@
|
||||
module imuslab.com/zoraxy
|
||||
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/boltdb/bolt v1.3.1
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/oschwald/geoip2-golang v1.8.0
|
||||
gitlab.com/NebulousLabs/go-upnp v0.0.0-20211002182029-11da932010b6
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
)
|
46
src/go.sum
Normal file
@ -0,0 +1,46 @@
|
||||
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
|
||||
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/oschwald/geoip2-golang v1.8.0 h1:KfjYB8ojCEn/QLqsDU0AzrJ3R5Qa9vFlx3z6SLNcKTs=
|
||||
github.com/oschwald/geoip2-golang v1.8.0/go.mod h1:R7bRvYjOeaoenAp9sKRS8GX5bJWcZ0laWO5+DauEktw=
|
||||
github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg=
|
||||
github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYxHV1dLIxBuyOsyYjHK0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.3/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40 h1:dizWJqTWjwyD8KGcMOwgrkqu1JIkofYgKkmDeNE7oAs=
|
||||
gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40/go.mod h1:rOnSnoRyxMI3fe/7KIbVcsHRGxe30OONv8dEgo+vCfA=
|
||||
gitlab.com/NebulousLabs/go-upnp v0.0.0-20211002182029-11da932010b6 h1:WKij6HF8ECp9E7K0E44dew9NrRDGiNR5u4EFsXnJUx4=
|
||||
gitlab.com/NebulousLabs/go-upnp v0.0.0-20211002182029-11da932010b6/go.mod h1:vhrHTGDh4YR7wK8Z+kRJ+x8SF/6RUM3Vb64Si5FD0L8=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1 h1:4qWs8cYYH6PoEFy4dfhDFgoMGkwAcETd+MmPdCPMzUc=
|
||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220804214406-8e32c043e418/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
184
src/main.go
Normal file
@ -0,0 +1,184 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/aroz"
|
||||
"imuslab.com/zoraxy/mod/auth"
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
"imuslab.com/zoraxy/mod/statistic"
|
||||
"imuslab.com/zoraxy/mod/tlscert"
|
||||
"imuslab.com/zoraxy/mod/upnp"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
//General flags
|
||||
var noauth = flag.Bool("noauth", false, "Disable authentication for management interface")
|
||||
var showver = flag.Bool("version", false, "Show version of this server")
|
||||
|
||||
var (
|
||||
name = "Zoraxy"
|
||||
version = "2.1"
|
||||
|
||||
handler *aroz.ArozHandler
|
||||
sysdb *database.Database
|
||||
authAgent *auth.AuthAgent
|
||||
tlsCertManager *tlscert.Manager
|
||||
redirectTable *redirection.RuleTable
|
||||
geodbStore *geodb.Store
|
||||
statisticCollector *statistic.Collector
|
||||
upnpClient *upnp.UPnPClient
|
||||
)
|
||||
|
||||
// Kill signal handler. Do something before the system the core terminate.
|
||||
func SetupCloseHandler() {
|
||||
c := make(chan os.Signal, 2)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-c
|
||||
log.Println("\r- Shutting down " + name)
|
||||
geodbStore.Close()
|
||||
statisticCollector.Close()
|
||||
|
||||
//Close database, final
|
||||
sysdb.Close()
|
||||
os.Exit(0)
|
||||
}()
|
||||
}
|
||||
|
||||
func main() {
|
||||
//Start the aoModule pipeline (which will parse the flags as well). Pass in the module launch information
|
||||
handler = aroz.HandleFlagParse(aroz.ServiceInfo{
|
||||
Name: name,
|
||||
Desc: "Dynamic Reverse Proxy Server",
|
||||
Group: "Network",
|
||||
IconPath: "Zoraxy/img/small_icon.png",
|
||||
Version: version,
|
||||
StartDir: "Zoraxy/index.html",
|
||||
SupportFW: true,
|
||||
LaunchFWDir: "Zoraxy/index.html",
|
||||
SupportEmb: false,
|
||||
InitFWSize: []int{1080, 580},
|
||||
})
|
||||
|
||||
if *showver {
|
||||
fmt.Println(name + " - Version " + version)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
SetupCloseHandler()
|
||||
|
||||
//Check if all required files are here
|
||||
ValidateSystemFiles()
|
||||
|
||||
//Create database
|
||||
db, err := database.NewDatabase("sys.db", false)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
sysdb = db
|
||||
//Create tables for the database
|
||||
sysdb.NewTable("settings")
|
||||
|
||||
//Create an auth agent
|
||||
sessionKey, err := auth.GetSessionKey(sysdb)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
authAgent = auth.NewAuthenticationAgent(name, []byte(sessionKey), sysdb, true, func(w http.ResponseWriter, r *http.Request) {
|
||||
//Not logged in. Redirecting to login page
|
||||
http.Redirect(w, r, "/login.html", http.StatusTemporaryRedirect)
|
||||
})
|
||||
|
||||
//Create a TLS certificate manager
|
||||
tlsCertManager, err = tlscert.NewManager("./certs")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//Create a redirection rule table
|
||||
redirectTable, err = redirection.NewRuleTable("./rules")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//Create a geodb store
|
||||
geodbStore, err = geodb.NewGeoDb(sysdb, "./system/GeoLite2-Country.mmdb")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//Create a statistic collector
|
||||
statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{
|
||||
Database: sysdb,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//Create a upnp client
|
||||
err = initUpnp()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//Initiate management interface APIs
|
||||
initAPIs()
|
||||
|
||||
//Start the reverse proxy server in go routine
|
||||
go func() {
|
||||
ReverseProxtInit()
|
||||
}()
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
//Any log println will be shown in the core system via STDOUT redirection. But not STDIN.
|
||||
log.Println("ReverseProxy started. Visit control panel at http://localhost" + handler.Port)
|
||||
err = http.ListenAndServe(handler.Port, nil)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//Unzip web.tar.gz if file exists
|
||||
func ValidateSystemFiles() error {
|
||||
if !utils.FileExists("./web") || !utils.FileExists("./system") {
|
||||
//Check if the web.tar.gz exists
|
||||
if utils.FileExists("./web.tar.gz") {
|
||||
//Unzip the file
|
||||
f, err := os.Open("./web.tar.gz")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = utils.ExtractTarGzipByStream(filepath.Clean("./"), f, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//Delete the web.tar.gz
|
||||
os.Remove("./web.tar.gz")
|
||||
} else {
|
||||
return errors.New("system files not found")
|
||||
}
|
||||
}
|
||||
return errors.New("system files not found or corrupted")
|
||||
|
||||
}
|
@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
//To be used with arozos system
|
||||
type ArozHandler struct {
|
||||
Port string
|
||||
restfulEndpoint string
|
||||
@ -33,14 +34,14 @@ type ServiceInfo struct {
|
||||
|
||||
//This function will request the required flag from the startup paramters and parse it to the need of the arozos.
|
||||
func HandleFlagParse(info ServiceInfo) *ArozHandler {
|
||||
var infoRequestMode = flag.Bool("info", false, "Show information about this subservice")
|
||||
var port = flag.String("port", ":8000", "The default listening endpoint for this subservice")
|
||||
var restful = flag.String("rpt", "http://localhost:8080/api/ajgi/interface", "The RESTFUL Endpoint of the parent")
|
||||
var infoRequestMode = flag.Bool("info", false, "Show information about this program in JSON")
|
||||
var port = flag.String("port", ":8000", "Management web interface listening port")
|
||||
var restful = flag.String("rpt", "", "Reserved")
|
||||
//Parse the flags
|
||||
flag.Parse()
|
||||
if *infoRequestMode == true {
|
||||
if *infoRequestMode {
|
||||
//Information request mode
|
||||
jsonString, _ := json.Marshal(info)
|
||||
jsonString, _ := json.MarshalIndent(info, "", " ")
|
||||
fmt.Println(string(jsonString))
|
||||
os.Exit(0)
|
||||
}
|
||||
@ -58,6 +59,11 @@ func (a *ArozHandler) GetUserInfoFromRequest(w http.ResponseWriter, r *http.Requ
|
||||
return username, token
|
||||
}
|
||||
|
||||
func (a *ArozHandler) IsUsingExternalPermissionManager() bool {
|
||||
return !(a.restfulEndpoint == "")
|
||||
}
|
||||
|
||||
//Request gateway interface for advance permission sandbox control
|
||||
func (a *ArozHandler) RequestGatewayInterface(token string, script string) (*http.Response, error) {
|
||||
resp, err := http.PostForm(a.restfulEndpoint,
|
||||
url.Values{"token": {token}, "script": {script}})
|
478
src/mod/auth/auth.go
Normal file
@ -0,0 +1,478 @@
|
||||
package auth
|
||||
|
||||
/*
|
||||
|
||||
author: tobychui
|
||||
*/
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha512"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"strings"
|
||||
|
||||
"encoding/hex"
|
||||
"log"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
db "imuslab.com/zoraxy/mod/database"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
type AuthAgent struct {
|
||||
//Session related
|
||||
SessionName string
|
||||
SessionStore *sessions.CookieStore
|
||||
Database *db.Database
|
||||
LoginRedirectionHandler func(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
type AuthEndpoints struct {
|
||||
Login string
|
||||
Logout string
|
||||
Register string
|
||||
CheckLoggedIn string
|
||||
Autologin string
|
||||
}
|
||||
|
||||
//Constructor
|
||||
func NewAuthenticationAgent(sessionName string, key []byte, sysdb *db.Database, allowReg bool, 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.")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//Create a new AuthAgent object
|
||||
newAuthAgent := AuthAgent{
|
||||
SessionName: sessionName,
|
||||
SessionStore: store,
|
||||
Database: sysdb,
|
||||
LoginRedirectionHandler: loginRedirectionHandler,
|
||||
}
|
||||
|
||||
//Return the authAgent
|
||||
return &newAuthAgent
|
||||
}
|
||||
|
||||
func GetSessionKey(sysdb *db.Database) (string, error) {
|
||||
sysdb.NewTable("auth")
|
||||
sessionKey := ""
|
||||
if !sysdb.KeyExists("auth", "sessionkey") {
|
||||
key := make([]byte, 32)
|
||||
rand.Read(key)
|
||||
sessionKey = string(key)
|
||||
sysdb.Write("auth", "sessionkey", sessionKey)
|
||||
log.Println("[Auth] New authentication session key generated")
|
||||
} else {
|
||||
log.Println("[Auth] Authentication session key loaded from database")
|
||||
err := sysdb.Read("auth", "sessionkey", &sessionKey)
|
||||
if err != nil {
|
||||
return "", errors.New("database read error. Is the database file corrupted?")
|
||||
}
|
||||
}
|
||||
return sessionKey, nil
|
||||
}
|
||||
|
||||
//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
|
||||
handler(w, r)
|
||||
} else {
|
||||
//User not logged in
|
||||
a.LoginRedirectionHandler(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
//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)
|
||||
utils.SendErrorResponse(w, "Username not defined or empty.")
|
||||
return
|
||||
}
|
||||
|
||||
//Get password from request using POST mode
|
||||
password, err := utils.PostPara(r, "password")
|
||||
if err != nil {
|
||||
//Password not defined
|
||||
utils.SendErrorResponse(w, "Password not defined or empty.")
|
||||
return
|
||||
}
|
||||
|
||||
//Get rememberme settings
|
||||
rememberme := false
|
||||
rmbme, _ := utils.PostPara(r, "rmbme")
|
||||
if rmbme == "true" {
|
||||
rememberme = true
|
||||
}
|
||||
|
||||
//Check the database and see if this user is in the database
|
||||
passwordCorrect, rejectionReason := a.ValidateUsernameAndPasswordWithReason(username, password)
|
||||
//The database contain this user information. Check its password if it is correct
|
||||
if passwordCorrect {
|
||||
//Password correct
|
||||
// Set user as authenticated
|
||||
a.LoginUserByRequest(w, r, username, rememberme)
|
||||
|
||||
//Print the login message to console
|
||||
log.Println(username + " logged in.")
|
||||
utils.SendOK(w)
|
||||
} else {
|
||||
//Password incorrect
|
||||
log.Println(username + " login request rejected: " + rejectionReason)
|
||||
|
||||
utils.SendErrorResponse(w, rejectionReason)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AuthAgent) ValidateUsernameAndPassword(username string, password string) bool {
|
||||
succ, _ := a.ValidateUsernameAndPasswordWithReason(username, password)
|
||||
return succ
|
||||
}
|
||||
|
||||
//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")
|
||||
return false, "Invalid username or password"
|
||||
}
|
||||
|
||||
if passwordInDB == hashedPassword {
|
||||
return true, ""
|
||||
} else {
|
||||
return false, "Invalid username or password"
|
||||
}
|
||||
}
|
||||
|
||||
//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)
|
||||
|
||||
session.Values["authenticated"] = true
|
||||
session.Values["username"] = username
|
||||
session.Values["rememberMe"] = rememberme
|
||||
|
||||
//Check if remember me is clicked. If yes, set the maxage to 1 week.
|
||||
if rememberme {
|
||||
session.Options = &sessions.Options{
|
||||
MaxAge: 3600 * 24 * 7, //One week
|
||||
Path: "/",
|
||||
}
|
||||
} else {
|
||||
session.Options = &sessions.Options{
|
||||
MaxAge: 3600 * 1, //One hour
|
||||
Path: "/",
|
||||
}
|
||||
}
|
||||
session.Save(r, w)
|
||||
}
|
||||
|
||||
//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 username != "" {
|
||||
log.Println(username + " logged out.")
|
||||
}
|
||||
// Revoke users authentication
|
||||
err = a.Logout(w, r)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Logout failed")
|
||||
return
|
||||
}
|
||||
|
||||
w.Write([]byte("OK"))
|
||||
}
|
||||
|
||||
func (a *AuthAgent) Logout(w http.ResponseWriter, r *http.Request) error {
|
||||
session, err := a.SessionStore.Get(r, a.SessionName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
session.Values["authenticated"] = false
|
||||
session.Values["username"] = nil
|
||||
session.Save(r, w)
|
||||
return nil
|
||||
}
|
||||
|
||||
//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.
|
||||
session, _ := a.SessionStore.Get(r, a.SessionName)
|
||||
return session.Values["username"].(string), nil
|
||||
} else {
|
||||
//This user has not logged in.
|
||||
return "", errors.New("user not logged in")
|
||||
}
|
||||
}
|
||||
|
||||
//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.
|
||||
session, _ := a.SessionStore.Get(r, a.SessionName)
|
||||
username := session.Values["username"].(string)
|
||||
userEmail := ""
|
||||
err := a.Database.Read("auth", "email/"+username, &userEmail)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return userEmail, nil
|
||||
} else {
|
||||
//This user has not logged in.
|
||||
return "", errors.New("user not logged in")
|
||||
}
|
||||
}
|
||||
|
||||
//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")
|
||||
} else {
|
||||
utils.SendJSONResponse(w, "false")
|
||||
}
|
||||
}
|
||||
|
||||
//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")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Missing 'username' paramter")
|
||||
return
|
||||
}
|
||||
|
||||
//Get password from request
|
||||
password, err := utils.PostPara(r, "password")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Missing 'password' paramter")
|
||||
return
|
||||
}
|
||||
|
||||
//Get email from request
|
||||
email, err := utils.PostPara(r, "email")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Missing 'email' paramter")
|
||||
return
|
||||
}
|
||||
|
||||
_, err = mail.ParseAddress(email)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Invalid or malformed email")
|
||||
return
|
||||
}
|
||||
|
||||
//Ok to proceed create this user
|
||||
err = a.CreateUserAccount(newusername, password, email)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Do callback if exists
|
||||
if callback != nil {
|
||||
callback(newusername, email)
|
||||
}
|
||||
|
||||
//Return to the client with OK
|
||||
utils.SendOK(w)
|
||||
log.Println("[Auth] New user " + newusername + " added to system.")
|
||||
}
|
||||
|
||||
//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")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Missing 'username' paramter")
|
||||
return
|
||||
}
|
||||
|
||||
//Get password from request
|
||||
password, err := utils.PostPara(r, "password")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Missing 'password' paramter")
|
||||
return
|
||||
}
|
||||
|
||||
//Ok to proceed create this user
|
||||
err = a.CreateUserAccount(newusername, password, "")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Do callback if exists
|
||||
if callback != nil {
|
||||
callback(newusername, "")
|
||||
}
|
||||
|
||||
//Return to the client with OK
|
||||
utils.SendOK(w)
|
||||
log.Println("[Auth] Admin account created: " + newusername)
|
||||
}
|
||||
|
||||
//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 {
|
||||
return false
|
||||
}
|
||||
// Check if user is authenticated
|
||||
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
//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) {
|
||||
//This user has not logged in
|
||||
utils.SendErrorResponse(w, "Login required to remove user from the system.")
|
||||
return
|
||||
}
|
||||
|
||||
//Get username from request
|
||||
username, err := utils.PostPara(r, "username")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Missing 'username' paramter")
|
||||
return
|
||||
}
|
||||
|
||||
err = a.UnregisterUser(username)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
//Return to the client with OK
|
||||
utils.SendOK(w)
|
||||
log.Println("[Auth] User " + username + " has been removed from the system.")
|
||||
}
|
||||
|
||||
func (a *AuthAgent) UnregisterUser(username string) error {
|
||||
//Check if the user exists in the system database.
|
||||
if !a.Database.KeyExists("auth", "passhash/"+username) {
|
||||
//This user do not exists.
|
||||
return errors.New("this user does not exists")
|
||||
}
|
||||
|
||||
//OK! Remove the user from the database
|
||||
a.Database.Delete("auth", "passhash/"+username)
|
||||
a.Database.Delete("auth", "email/"+username)
|
||||
return nil
|
||||
}
|
||||
|
||||
//Get the number of users in the system
|
||||
func (a *AuthAgent) GetUserCounts() int {
|
||||
entries, _ := a.Database.ListTable("auth")
|
||||
usercount := 0
|
||||
for _, keypairs := range entries {
|
||||
if strings.Contains(string(keypairs[0]), "passhash/") {
|
||||
//This is a user registry
|
||||
usercount++
|
||||
}
|
||||
}
|
||||
|
||||
if usercount == 0 {
|
||||
log.Println("There are no user in the database.")
|
||||
}
|
||||
return usercount
|
||||
}
|
||||
|
||||
//List all username within the system
|
||||
func (a *AuthAgent) ListUsers() []string {
|
||||
entries, _ := a.Database.ListTable("auth")
|
||||
results := []string{}
|
||||
for _, keypairs := range entries {
|
||||
if strings.Contains(string(keypairs[0]), "passhash/") {
|
||||
username := strings.Split(string(keypairs[0]), "/")[1]
|
||||
results = append(results, username)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
//Check if the given username exists
|
||||
func (a *AuthAgent) UserExists(username string) bool {
|
||||
userpasswordhash := ""
|
||||
err := a.Database.Read("auth", "passhash/"+username, &userpasswordhash)
|
||||
if err != nil || userpasswordhash == "" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
//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) {
|
||||
//User authenticated. Extend its expire time
|
||||
rememberme := session.Values["rememberMe"].(bool)
|
||||
//Extend the session expire time
|
||||
if rememberme {
|
||||
session.Options = &sessions.Options{
|
||||
MaxAge: 3600 * 24 * 7, //One week
|
||||
Path: "/",
|
||||
}
|
||||
} else {
|
||||
session.Options = &sessions.Options{
|
||||
MaxAge: 3600 * 1, //One hour
|
||||
Path: "/",
|
||||
}
|
||||
}
|
||||
session.Save(r, w)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
//Create user account
|
||||
func (a *AuthAgent) CreateUserAccount(newusername string, password string, email string) error {
|
||||
//Check user already exists
|
||||
if a.UserExists(newusername) {
|
||||
return errors.New("user with same name already exists")
|
||||
}
|
||||
|
||||
key := newusername
|
||||
hashedPassword := Hash(password)
|
||||
err := a.Database.Write("auth", "passhash/"+key, hashedPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if email != "" {
|
||||
err = a.Database.Write("auth", "email/"+key, email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//Hash the given raw string into sha512 hash
|
||||
func Hash(raw string) string {
|
||||
h := sha512.New()
|
||||
h.Write([]byte(raw))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
53
src/mod/auth/router.go
Normal file
@ -0,0 +1,53 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type RouterOption struct {
|
||||
AuthAgent *AuthAgent
|
||||
RequireAuth bool //This router require authentication
|
||||
DeniedHandler func(http.ResponseWriter, *http.Request) //Things to do when request is rejected
|
||||
|
||||
}
|
||||
|
||||
type RouterDef struct {
|
||||
option RouterOption
|
||||
endpoints map[string]func(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
func NewManagedHTTPRouter(option RouterOption) *RouterDef {
|
||||
return &RouterDef{
|
||||
option: option,
|
||||
endpoints: map[string]func(http.ResponseWriter, *http.Request){},
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
return errors.New("endpoint register duplicated")
|
||||
}
|
||||
|
||||
authAgent := router.option.AuthAgent
|
||||
|
||||
//OK. Register handler
|
||||
http.HandleFunc(endpoint, func(w http.ResponseWriter, r *http.Request) {
|
||||
//Check authentication of the user
|
||||
if router.option.RequireAuth {
|
||||
authAgent.HandleCheckAuth(w, r, func(w http.ResponseWriter, r *http.Request) {
|
||||
handler(w, r)
|
||||
})
|
||||
} else {
|
||||
handler(w, r)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
router.endpoints[endpoint] = handler
|
||||
|
||||
return nil
|
||||
}
|
120
src/mod/database/database.go
Normal file
@ -0,0 +1,120 @@
|
||||
package database
|
||||
|
||||
/*
|
||||
ArOZ Online Database Access Module
|
||||
author: tobychui
|
||||
|
||||
This is an improved Object oriented base solution to the original
|
||||
aroz online database script.
|
||||
*/
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
Db interface{} //This will be nil on openwrt and *bolt.DB in the rest of the systems
|
||||
Tables sync.Map
|
||||
ReadOnly bool
|
||||
}
|
||||
|
||||
func NewDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
|
||||
return newDatabase(dbfile, readOnlyMode)
|
||||
}
|
||||
|
||||
/*
|
||||
Create / Drop a table
|
||||
Usage:
|
||||
err := sysdb.NewTable("MyTable")
|
||||
err := sysdb.DropTable("MyTable")
|
||||
*/
|
||||
|
||||
func (d *Database) UpdateReadWriteMode(readOnly bool) {
|
||||
d.ReadOnly = readOnly
|
||||
}
|
||||
|
||||
//Dump the whole db into a log file
|
||||
func (d *Database) Dump(filename string) ([]string, error) {
|
||||
return d.dump(filename)
|
||||
}
|
||||
|
||||
//Create a new table
|
||||
func (d *Database) NewTable(tableName string) error {
|
||||
return d.newTable(tableName)
|
||||
}
|
||||
|
||||
//Check is table exists
|
||||
func (d *Database) TableExists(tableName string) bool {
|
||||
return d.tableExists(tableName)
|
||||
}
|
||||
|
||||
//Drop the given table
|
||||
func (d *Database) DropTable(tableName string) error {
|
||||
return d.dropTable(tableName)
|
||||
}
|
||||
|
||||
/*
|
||||
Write to database with given tablename and key. Example Usage:
|
||||
type demo struct{
|
||||
content string
|
||||
}
|
||||
thisDemo := demo{
|
||||
content: "Hello World",
|
||||
}
|
||||
err := sysdb.Write("MyTable", "username/message",thisDemo);
|
||||
*/
|
||||
func (d *Database) Write(tableName string, key string, value interface{}) error {
|
||||
return d.write(tableName, key, value)
|
||||
}
|
||||
|
||||
/*
|
||||
Read from database and assign the content to a given datatype. Example Usage:
|
||||
|
||||
type demo struct{
|
||||
content string
|
||||
}
|
||||
thisDemo := new(demo)
|
||||
err := sysdb.Read("MyTable", "username/message",&thisDemo);
|
||||
*/
|
||||
|
||||
func (d *Database) Read(tableName string, key string, assignee interface{}) error {
|
||||
return d.read(tableName, key, assignee)
|
||||
}
|
||||
|
||||
func (d *Database) KeyExists(tableName string, key string) bool {
|
||||
return d.keyExists(tableName, key)
|
||||
}
|
||||
|
||||
/*
|
||||
Delete a value from the database table given tablename and key
|
||||
|
||||
err := sysdb.Delete("MyTable", "username/message");
|
||||
*/
|
||||
func (d *Database) Delete(tableName string, key string) error {
|
||||
return d.delete(tableName, key)
|
||||
}
|
||||
|
||||
/*
|
||||
//List table example usage
|
||||
//Assume the value is stored as a struct named "groupstruct"
|
||||
|
||||
entries, err := sysdb.ListTable("test")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, keypairs := range entries{
|
||||
log.Println(string(keypairs[0]))
|
||||
group := new(groupstruct)
|
||||
json.Unmarshal(keypairs[1], &group)
|
||||
log.Println(group);
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
func (d *Database) ListTable(tableName string) ([][][]byte, error) {
|
||||
return d.listTable(tableName)
|
||||
}
|
||||
|
||||
func (d *Database) Close() {
|
||||
d.close()
|
||||
}
|
@ -1,13 +1,8 @@
|
||||
//go:build !mipsle && !riscv64
|
||||
// +build !mipsle,!riscv64
|
||||
|
||||
package database
|
||||
|
||||
/*
|
||||
ArOZ Online Database Access Module
|
||||
author: tobychui
|
||||
|
||||
This is an improved Object oriented base solution to the original
|
||||
aroz online database script.
|
||||
*/
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@ -17,15 +12,11 @@ import (
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
Db *bolt.DB
|
||||
Tables sync.Map
|
||||
ReadOnly bool
|
||||
}
|
||||
|
||||
func NewDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
|
||||
func newDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
|
||||
db, err := bolt.Open(dbfile, 0600, nil)
|
||||
log.Println("Key-value Database Service Started: " + dbfile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tableMap := sync.Map{}
|
||||
//Build the table list from database
|
||||
@ -43,19 +34,8 @@ func NewDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
|
||||
}, err
|
||||
}
|
||||
|
||||
/*
|
||||
Create / Drop a table
|
||||
Usage:
|
||||
err := sysdb.NewTable("MyTable")
|
||||
err := sysdb.DropTable("MyTable")
|
||||
*/
|
||||
|
||||
func (d *Database) UpdateReadWriteMode(readOnly bool) {
|
||||
d.ReadOnly = readOnly
|
||||
}
|
||||
|
||||
//Dump the whole db into a log file
|
||||
func (d *Database) Dump(filename string) ([]string, error) {
|
||||
func (d *Database) dump(filename string) ([]string, error) {
|
||||
results := []string{}
|
||||
|
||||
d.Tables.Range(func(tableName, v interface{}) bool {
|
||||
@ -74,12 +54,12 @@ func (d *Database) Dump(filename string) ([]string, error) {
|
||||
}
|
||||
|
||||
//Create a new table
|
||||
func (d *Database) NewTable(tableName string) error {
|
||||
func (d *Database) newTable(tableName string) error {
|
||||
if d.ReadOnly == true {
|
||||
return errors.New("Operation rejected in ReadOnly mode")
|
||||
}
|
||||
|
||||
err := d.Db.Update(func(tx *bolt.Tx) error {
|
||||
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
|
||||
_, err := tx.CreateBucketIfNotExists([]byte(tableName))
|
||||
if err != nil {
|
||||
return err
|
||||
@ -92,7 +72,7 @@ func (d *Database) NewTable(tableName string) error {
|
||||
}
|
||||
|
||||
//Check is table exists
|
||||
func (d *Database) TableExists(tableName string) bool {
|
||||
func (d *Database) tableExists(tableName string) bool {
|
||||
if _, ok := d.Tables.Load(tableName); ok {
|
||||
return true
|
||||
}
|
||||
@ -100,12 +80,12 @@ func (d *Database) TableExists(tableName string) bool {
|
||||
}
|
||||
|
||||
//Drop the given table
|
||||
func (d *Database) DropTable(tableName string) error {
|
||||
func (d *Database) dropTable(tableName string) error {
|
||||
if d.ReadOnly == true {
|
||||
return errors.New("Operation rejected in ReadOnly mode")
|
||||
}
|
||||
|
||||
err := d.Db.Update(func(tx *bolt.Tx) error {
|
||||
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
|
||||
err := tx.DeleteBucket([]byte(tableName))
|
||||
if err != nil {
|
||||
return err
|
||||
@ -115,18 +95,9 @@ func (d *Database) DropTable(tableName string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
/*
|
||||
Write to database with given tablename and key. Example Usage:
|
||||
type demo struct{
|
||||
content string
|
||||
}
|
||||
thisDemo := demo{
|
||||
content: "Hello World",
|
||||
}
|
||||
err := sysdb.Write("MyTable", "username/message",thisDemo);
|
||||
*/
|
||||
func (d *Database) Write(tableName string, key string, value interface{}) error {
|
||||
if d.ReadOnly == true {
|
||||
//Write to table
|
||||
func (d *Database) write(tableName string, key string, value interface{}) error {
|
||||
if d.ReadOnly {
|
||||
return errors.New("Operation rejected in ReadOnly mode")
|
||||
}
|
||||
|
||||
@ -134,8 +105,11 @@ func (d *Database) Write(tableName string, key string, value interface{}) error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = d.Db.Update(func(tx *bolt.Tx) error {
|
||||
err = d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
|
||||
_, err := tx.CreateBucketIfNotExists([]byte(tableName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b := tx.Bucket([]byte(tableName))
|
||||
err = b.Put([]byte(key), jsonString)
|
||||
return err
|
||||
@ -143,18 +117,8 @@ func (d *Database) Write(tableName string, key string, value interface{}) error
|
||||
return err
|
||||
}
|
||||
|
||||
/*
|
||||
Read from database and assign the content to a given datatype. Example Usage:
|
||||
|
||||
type demo struct{
|
||||
content string
|
||||
}
|
||||
thisDemo := new(demo)
|
||||
err := sysdb.Read("MyTable", "username/message",&thisDemo);
|
||||
*/
|
||||
|
||||
func (d *Database) Read(tableName string, key string, assignee interface{}) error {
|
||||
err := d.Db.View(func(tx *bolt.Tx) error {
|
||||
func (d *Database) read(tableName string, key string, assignee interface{}) error {
|
||||
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(tableName))
|
||||
v := b.Get([]byte(key))
|
||||
json.Unmarshal(v, &assignee)
|
||||
@ -163,9 +127,14 @@ func (d *Database) Read(tableName string, key string, assignee interface{}) erro
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Database) KeyExists(tableName string, key string) bool {
|
||||
func (d *Database) keyExists(tableName string, key string) bool {
|
||||
resultIsNil := false
|
||||
err := d.Db.View(func(tx *bolt.Tx) error {
|
||||
if !d.TableExists(tableName) {
|
||||
//Table not exists. Do not proceed accessing key
|
||||
log.Println("[DB] ERROR: Requesting key from table that didn't exist!!!")
|
||||
return false
|
||||
}
|
||||
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(tableName))
|
||||
v := b.Get([]byte(key))
|
||||
if v == nil {
|
||||
@ -185,17 +154,12 @@ func (d *Database) KeyExists(tableName string, key string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Delete a value from the database table given tablename and key
|
||||
|
||||
err := sysdb.Delete("MyTable", "username/message");
|
||||
*/
|
||||
func (d *Database) Delete(tableName string, key string) error {
|
||||
if d.ReadOnly == true {
|
||||
func (d *Database) delete(tableName string, key string) error {
|
||||
if d.ReadOnly {
|
||||
return errors.New("Operation rejected in ReadOnly mode")
|
||||
}
|
||||
|
||||
err := d.Db.Update(func(tx *bolt.Tx) error {
|
||||
err := d.Db.(*bolt.DB).Update(func(tx *bolt.Tx) error {
|
||||
tx.Bucket([]byte(tableName)).Delete([]byte(key))
|
||||
return nil
|
||||
})
|
||||
@ -203,26 +167,9 @@ func (d *Database) Delete(tableName string, key string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
/*
|
||||
//List table example usage
|
||||
//Assume the value is stored as a struct named "groupstruct"
|
||||
|
||||
entries, err := sysdb.ListTable("test")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, keypairs := range entries{
|
||||
log.Println(string(keypairs[0]))
|
||||
group := new(groupstruct)
|
||||
json.Unmarshal(keypairs[1], &group)
|
||||
log.Println(group);
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
func (d *Database) ListTable(tableName string) ([][][]byte, error) {
|
||||
func (d *Database) listTable(tableName string) ([][][]byte, error) {
|
||||
var results [][][]byte
|
||||
err := d.Db.View(func(tx *bolt.Tx) error {
|
||||
err := d.Db.(*bolt.DB).View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(tableName))
|
||||
c := b.Cursor()
|
||||
|
||||
@ -234,7 +181,6 @@ func (d *Database) ListTable(tableName string) ([][][]byte, error) {
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (d *Database) Close() {
|
||||
d.Db.Close()
|
||||
return
|
||||
func (d *Database) close() {
|
||||
d.Db.(*bolt.DB).Close()
|
||||
}
|
208
src/mod/database/database_openwrt.go
Normal file
@ -0,0 +1,208 @@
|
||||
//go:build mipsle || riscv64
|
||||
// +build mipsle riscv64
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func newDatabase(dbfile string, readOnlyMode bool) (*Database, error) {
|
||||
dbRootPath := filepath.ToSlash(filepath.Clean(dbfile))
|
||||
dbRootPath = "fsdb/" + dbRootPath
|
||||
err := os.MkdirAll(dbRootPath, 0755)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tableMap := sync.Map{}
|
||||
//build the table list from file system
|
||||
files, err := filepath.Glob(filepath.Join(dbRootPath, "/*"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if isDirectory(file) {
|
||||
tableMap.Store(filepath.Base(file), "")
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Filesystem Emulated Key-value Database Service Started: " + dbRootPath)
|
||||
return &Database{
|
||||
Db: dbRootPath,
|
||||
Tables: tableMap,
|
||||
ReadOnly: readOnlyMode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Database) dump(filename string) ([]string, error) {
|
||||
//Get all file objects from root
|
||||
rootfiles, err := filepath.Glob(filepath.Join(d.Db.(string), "/*"))
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
//Filter out the folders
|
||||
rootFolders := []string{}
|
||||
for _, file := range rootfiles {
|
||||
if !isDirectory(file) {
|
||||
rootFolders = append(rootFolders, filepath.Base(file))
|
||||
}
|
||||
}
|
||||
|
||||
return rootFolders, nil
|
||||
}
|
||||
|
||||
func (d *Database) newTable(tableName string) error {
|
||||
if d.ReadOnly {
|
||||
return errors.New("Operation rejected in ReadOnly mode")
|
||||
}
|
||||
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
|
||||
if !fileExists(tablePath) {
|
||||
return os.MkdirAll(tablePath, 0755)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Database) tableExists(tableName string) bool {
|
||||
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
|
||||
if _, err := os.Stat(tablePath); errors.Is(err, os.ErrNotExist) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !isDirectory(tablePath) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (d *Database) dropTable(tableName string) error {
|
||||
if d.ReadOnly {
|
||||
return errors.New("Operation rejected in ReadOnly mode")
|
||||
}
|
||||
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
|
||||
if d.tableExists(tableName) {
|
||||
return os.RemoveAll(tablePath)
|
||||
} else {
|
||||
return errors.New("table not exists")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (d *Database) write(tableName string, key string, value interface{}) error {
|
||||
if d.ReadOnly {
|
||||
return errors.New("Operation rejected in ReadOnly mode")
|
||||
}
|
||||
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
|
||||
js, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key = strings.ReplaceAll(key, "/", "-SLASH_SIGN-")
|
||||
|
||||
return os.WriteFile(filepath.Join(tablePath, key+".entry"), js, 0755)
|
||||
}
|
||||
|
||||
func (d *Database) read(tableName string, key string, assignee interface{}) error {
|
||||
if !d.keyExists(tableName, key) {
|
||||
return errors.New("key not exists")
|
||||
}
|
||||
|
||||
key = strings.ReplaceAll(key, "/", "-SLASH_SIGN-")
|
||||
|
||||
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
|
||||
entryPath := filepath.Join(tablePath, key+".entry")
|
||||
content, err := os.ReadFile(entryPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(content, &assignee)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Database) keyExists(tableName string, key string) bool {
|
||||
key = strings.ReplaceAll(key, "/", "-SLASH_SIGN-")
|
||||
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
|
||||
entryPath := filepath.Join(tablePath, key+".entry")
|
||||
return fileExists(entryPath)
|
||||
}
|
||||
|
||||
func (d *Database) delete(tableName string, key string) error {
|
||||
if d.ReadOnly {
|
||||
return errors.New("Operation rejected in ReadOnly mode")
|
||||
}
|
||||
if !d.keyExists(tableName, key) {
|
||||
return errors.New("key not exists")
|
||||
}
|
||||
key = strings.ReplaceAll(key, "/", "-SLASH_SIGN-")
|
||||
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
|
||||
entryPath := filepath.Join(tablePath, key+".entry")
|
||||
|
||||
return os.Remove(entryPath)
|
||||
}
|
||||
|
||||
func (d *Database) listTable(tableName string) ([][][]byte, error) {
|
||||
if !d.tableExists(tableName) {
|
||||
return [][][]byte{}, errors.New("table not exists")
|
||||
}
|
||||
tablePath := filepath.Join(d.Db.(string), filepath.Base(tableName))
|
||||
entries, err := filepath.Glob(filepath.Join(tablePath, "/*.entry"))
|
||||
if err != nil {
|
||||
return [][][]byte{}, err
|
||||
}
|
||||
|
||||
var results [][][]byte = [][][]byte{}
|
||||
for _, entry := range entries {
|
||||
if !isDirectory(entry) {
|
||||
//Read it
|
||||
key := filepath.Base(entry)
|
||||
key = strings.TrimSuffix(key, filepath.Ext(key))
|
||||
key = strings.ReplaceAll(key, "-SLASH_SIGN-", "/")
|
||||
|
||||
bkey := []byte(key)
|
||||
bval := []byte("")
|
||||
c, err := os.ReadFile(entry)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
bval = c
|
||||
results = append(results, [][]byte{bkey, bval})
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (d *Database) close() {
|
||||
//Nothing to close as it is file system
|
||||
}
|
||||
|
||||
func isDirectory(path string) bool {
|
||||
fileInfo, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return fileInfo.IsDir()
|
||||
}
|
||||
|
||||
func fileExists(name string) bool {
|
||||
_, err := os.Stat(name)
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
67
src/mod/dynamicproxy/Server.go
Normal file
@ -0,0 +1,67 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
)
|
||||
|
||||
/*
|
||||
Server.go
|
||||
|
||||
Main server for dynamic proxy core
|
||||
*/
|
||||
|
||||
func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
//Check if this ip is in blacklist
|
||||
clientIpAddr := geodb.GetRequesterIP(r)
|
||||
if h.Parent.Option.GeodbStore.IsBlacklisted(clientIpAddr) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
template, err := os.ReadFile("./web/forbidden.html")
|
||||
if err != nil {
|
||||
w.Write([]byte("403 - Forbidden"))
|
||||
} else {
|
||||
w.Write(template)
|
||||
}
|
||||
h.logRequest(r, false, 403, "blacklist")
|
||||
return
|
||||
}
|
||||
|
||||
//Check if this is a redirection url
|
||||
if h.Parent.Option.RedirectRuleTable.IsRedirectable(r) {
|
||||
statusCode := h.Parent.Option.RedirectRuleTable.HandleRedirect(w, r)
|
||||
h.logRequest(r, statusCode != 500, statusCode, "redirect")
|
||||
return
|
||||
}
|
||||
|
||||
//Extract request host to see if it is virtual directory or subdomain
|
||||
domainOnly := r.Host
|
||||
if strings.Contains(r.Host, ":") {
|
||||
hostPath := strings.Split(r.Host, ":")
|
||||
domainOnly = hostPath[0]
|
||||
}
|
||||
|
||||
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 {
|
||||
h.subdomainRequest(w, r, sep)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//Clean up the request URI
|
||||
proxyingPath := strings.TrimSpace(r.RequestURI)
|
||||
|
||||
targetProxyEndpoint := h.Parent.getTargetProxyEndpointFromRequestURI(proxyingPath)
|
||||
if targetProxyEndpoint != nil {
|
||||
h.proxyRequest(w, r, targetProxyEndpoint)
|
||||
} else {
|
||||
h.proxyRequest(w, r, h.Parent.Root)
|
||||
}
|
||||
}
|
23
src/mod/dynamicproxy/domainsniff/domainsniff.go
Normal file
@ -0,0 +1,23 @@
|
||||
package domainsniff
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
//Check if the domain is reachable and return err if not reachable
|
||||
func DomainReachableWithError(domain string) error {
|
||||
timeout := 1 * time.Second
|
||||
conn, err := net.DialTimeout("tcp", domain, timeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
//Check if domain reachable
|
||||
func DomainReachable(domain string) bool {
|
||||
return DomainReachableWithError(domain) == nil
|
||||
}
|
@ -2,7 +2,6 @@ package dpcore
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
@ -277,7 +276,7 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request) erro
|
||||
p.logf("http: proxy error: %v", err)
|
||||
}
|
||||
|
||||
rw.WriteHeader(http.StatusBadGateway)
|
||||
//rw.WriteHeader(http.StatusBadGateway)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -290,15 +289,14 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request) erro
|
||||
p.logf("http: proxy error: %v", err)
|
||||
}
|
||||
|
||||
rw.WriteHeader(http.StatusBadGateway)
|
||||
//rw.WriteHeader(http.StatusBadGateway)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
//Custom header rewriter functions
|
||||
if res.Header.Get("Location") != "" {
|
||||
//Custom redirection fto this rproxy relative path
|
||||
fmt.Println(res.Header.Get("Location"))
|
||||
//Custom redirection to this rproxy relative path
|
||||
res.Header.Set("Location", filepath.ToSlash(filepath.Join(p.Prepender, res.Header.Get("Location"))))
|
||||
}
|
||||
// Copy header from response to client.
|
310
src/mod/dynamicproxy/dynamicproxy.go
Normal file
@ -0,0 +1,310 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"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
|
||||
*/
|
||||
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
|
||||
}
|
||||
|
||||
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{}
|
||||
domainMap := sync.Map{}
|
||||
thisRouter := Router{
|
||||
Option: &option,
|
||||
ProxyEndpoints: &proxyMap,
|
||||
SubdomainEndpoint: &domainMap,
|
||||
Running: false,
|
||||
server: nil,
|
||||
}
|
||||
|
||||
thisRouter.mux = &ProxyHandler{
|
||||
Parent: &thisRouter,
|
||||
}
|
||||
|
||||
return &thisRouter, nil
|
||||
}
|
||||
|
||||
// Update TLS setting in runtime. Will restart the proxy server
|
||||
// if it is already running in the background
|
||||
func (router *Router) UpdateTLSSetting(tlsEnabled bool) {
|
||||
router.Option.UseTls = tlsEnabled
|
||||
router.Restart()
|
||||
}
|
||||
|
||||
// Update https redirect, which will require updates
|
||||
func (router *Router) UpdateHttpToHttpsRedirectSetting(useRedirect bool) {
|
||||
router.Option.ForceHttpsRedirect = useRedirect
|
||||
router.Restart()
|
||||
}
|
||||
|
||||
// Start the dynamic routing
|
||||
func (router *Router) StartProxyService() error {
|
||||
//Create a new server object
|
||||
if router.server != nil {
|
||||
return errors.New("Reverse proxy server already running")
|
||||
}
|
||||
|
||||
if router.Root == nil {
|
||||
return errors.New("Reverse proxy router root not set")
|
||||
}
|
||||
|
||||
config := &tls.Config{
|
||||
GetCertificate: router.Option.TlsManager.GetCert,
|
||||
}
|
||||
|
||||
if router.Option.UseTls {
|
||||
//Serve with TLS mode
|
||||
ln, err := tls.Listen("tcp", ":"+strconv.Itoa(router.Option.Port), config)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return err
|
||||
}
|
||||
router.tlsListener = ln
|
||||
router.server = &http.Server{Addr: ":" + strconv.Itoa(router.Option.Port), Handler: router.mux}
|
||||
router.Running = true
|
||||
|
||||
if router.Option.Port == 443 && router.Option.ForceHttpsRedirect {
|
||||
//Add a 80 to 443 redirector
|
||||
httpServer := &http.Server{
|
||||
Addr: ":80",
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusTemporaryRedirect)
|
||||
}),
|
||||
ReadTimeout: 3 * time.Second,
|
||||
WriteTimeout: 3 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
}
|
||||
|
||||
log.Println("Starting HTTP-to-HTTPS redirector (port 80)")
|
||||
go func() {
|
||||
//Start another router to check if the router.server is killed. If yes, kill this server as well
|
||||
go func() {
|
||||
for router.server != nil {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
httpServer.Shutdown(ctx)
|
||||
log.Println(":80 to :433 redirection listener stopped")
|
||||
}()
|
||||
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("Could not start server: %v\n", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
log.Println("Reverse proxy service started in the background (TLS mode)")
|
||||
go func() {
|
||||
if err := router.server.Serve(ln); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("Could not start server: %v\n", err)
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
//Serve with non TLS mode
|
||||
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)")
|
||||
go func() {
|
||||
router.server.ListenAndServe()
|
||||
//log.Println("[DynamicProxy] " + err.Error())
|
||||
}()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (router *Router) StopProxyService() error {
|
||||
if router.server == nil {
|
||||
return errors.New("Reverse proxy server already stopped")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
err := router.server.Shutdown(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if router.tlsListener != nil {
|
||||
router.tlsListener.Close()
|
||||
}
|
||||
|
||||
//Discard the server object
|
||||
router.tlsListener = nil
|
||||
router.server = nil
|
||||
router.Running = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// Restart the current router if it is running.
|
||||
// Startup the server if it is not running initially
|
||||
func (router *Router) Restart() error {
|
||||
//Stop the router if it is already running
|
||||
if router.Running {
|
||||
err := router.StopProxyService()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
//Start the server
|
||||
err := router.StartProxyService()
|
||||
return err
|
||||
}
|
||||
|
||||
/*
|
||||
Check if a given request is accessed via a proxied subdomain
|
||||
*/
|
||||
|
||||
func (router *Router) IsProxiedSubdomain(r *http.Request) bool {
|
||||
hostname := r.Header.Get("X-Forwarded-Host")
|
||||
if hostname == "" {
|
||||
hostname = r.Host
|
||||
}
|
||||
hostname = strings.Split(hostname, ":")[0]
|
||||
subdEndpoint := router.getSubdomainProxyEndpointFromHostname(hostname)
|
||||
return subdEndpoint != nil
|
||||
}
|
||||
|
||||
/*
|
||||
Add an URL into a custom proxy services
|
||||
*/
|
||||
func (router *Router) AddVirtualDirectoryProxyService(rootname string, domain string, requireTLS bool) error {
|
||||
if domain[len(domain)-1:] == "/" {
|
||||
domain = domain[:len(domain)-1]
|
||||
}
|
||||
|
||||
if rootname[len(rootname)-1:] == "/" {
|
||||
rootname = rootname[:len(rootname)-1]
|
||||
}
|
||||
|
||||
webProxyEndpoint := domain
|
||||
if requireTLS {
|
||||
webProxyEndpoint = "https://" + webProxyEndpoint
|
||||
} else {
|
||||
webProxyEndpoint = "http://" + webProxyEndpoint
|
||||
}
|
||||
//Create a new proxy agent for this root
|
||||
path, err := url.Parse(webProxyEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
proxy := dpcore.NewDynamicProxyCore(path, rootname)
|
||||
|
||||
endpointObject := ProxyEndpoint{
|
||||
Root: rootname,
|
||||
Domain: domain,
|
||||
RequireTLS: requireTLS,
|
||||
Proxy: proxy,
|
||||
}
|
||||
|
||||
router.ProxyEndpoints.Store(rootname, &endpointObject)
|
||||
|
||||
log.Println("Adding Proxy Rule: ", rootname+" to "+domain)
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
Remove routing from RP
|
||||
*/
|
||||
func (router *Router) RemoveProxy(ptype string, key string) error {
|
||||
//fmt.Println(ptype, key)
|
||||
if ptype == "vdir" {
|
||||
router.ProxyEndpoints.Delete(key)
|
||||
return nil
|
||||
} else if ptype == "subd" {
|
||||
router.SubdomainEndpoint.Delete(key)
|
||||
return nil
|
||||
}
|
||||
return errors.New("invalid ptype")
|
||||
}
|
||||
|
||||
/*
|
||||
Add an default router for the proxy server
|
||||
*/
|
||||
func (router *Router) SetRootProxy(proxyLocation string, requireTLS bool) error {
|
||||
if proxyLocation[len(proxyLocation)-1:] == "/" {
|
||||
proxyLocation = proxyLocation[:len(proxyLocation)-1]
|
||||
}
|
||||
|
||||
webProxyEndpoint := proxyLocation
|
||||
if requireTLS {
|
||||
webProxyEndpoint = "https://" + webProxyEndpoint
|
||||
} else {
|
||||
webProxyEndpoint = "http://" + webProxyEndpoint
|
||||
}
|
||||
//Create a new proxy agent for this root
|
||||
path, err := url.Parse(webProxyEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
proxy := dpcore.NewDynamicProxyCore(path, "")
|
||||
|
||||
rootEndpoint := ProxyEndpoint{
|
||||
Root: "/",
|
||||
Domain: proxyLocation,
|
||||
RequireTLS: requireTLS,
|
||||
Proxy: proxy,
|
||||
}
|
||||
|
||||
router.Root = &rootEndpoint
|
||||
return nil
|
||||
}
|
@ -1,21 +1,32 @@
|
||||
package dynamicproxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/arozos/ReverseProxy/mod/websocketproxy"
|
||||
"imuslab.com/zoraxy/mod/geodb"
|
||||
"imuslab.com/zoraxy/mod/statistic"
|
||||
"imuslab.com/zoraxy/mod/websocketproxy"
|
||||
)
|
||||
|
||||
func (router *Router) getTargetProxyEndpointFromRequestURI(requestURI string) *ProxyEndpoint {
|
||||
var targetProxyEndpoint *ProxyEndpoint = nil
|
||||
router.ProxyEndpoints.Range(func(key, value interface{}) bool {
|
||||
rootname := key.(string)
|
||||
if len(requestURI) >= len(rootname) && requestURI[:len(rootname)] == rootname {
|
||||
if strings.HasPrefix(requestURI, rootname) {
|
||||
thisProxyEndpoint := value.(*ProxyEndpoint)
|
||||
targetProxyEndpoint = thisProxyEndpoint
|
||||
}
|
||||
/*
|
||||
if len(requestURI) >= len(rootname) && requestURI[:len(rootname)] == rootname {
|
||||
thisProxyEndpoint := value.(*ProxyEndpoint)
|
||||
targetProxyEndpoint = thisProxyEndpoint
|
||||
}
|
||||
*/
|
||||
return true
|
||||
})
|
||||
|
||||
@ -42,7 +53,7 @@ func (router *Router) rewriteURL(rooturl string, requestURL string) string {
|
||||
func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request, target *SubdomainEndpoint) {
|
||||
r.Header.Set("X-Forwarded-Host", r.Host)
|
||||
requestURL := r.URL.String()
|
||||
if r.Header["Upgrade"] != nil && r.Header["Upgrade"][0] == "websocket" {
|
||||
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
|
||||
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
|
||||
r.Header.Set("A-Upgrade", "websocket")
|
||||
wsRedirectionEndpoint := target.Domain
|
||||
@ -58,6 +69,7 @@ func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request,
|
||||
if target.RequireTLS {
|
||||
u, _ = url.Parse("wss://" + wsRedirectionEndpoint + requestURL)
|
||||
}
|
||||
h.logRequest(r, true, 101, "subdomain-websocket")
|
||||
wspHandler := websocketproxy.NewProxy(u)
|
||||
wspHandler.ServeHTTP(w, r)
|
||||
return
|
||||
@ -65,17 +77,27 @@ func (h *ProxyHandler) subdomainRequest(w http.ResponseWriter, r *http.Request,
|
||||
|
||||
r.Host = r.URL.Host
|
||||
err := target.Proxy.ServeHTTP(w, r)
|
||||
var dnsError *net.DNSError
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
if errors.As(err, &dnsError) {
|
||||
http.ServeFile(w, r, "./web/hosterror.html")
|
||||
log.Println(err.Error())
|
||||
h.logRequest(r, false, 404, "subdomain-http")
|
||||
} else {
|
||||
http.ServeFile(w, r, "./web/rperror.html")
|
||||
log.Println(err.Error())
|
||||
h.logRequest(r, false, 521, "subdomain-http")
|
||||
}
|
||||
}
|
||||
|
||||
h.logRequest(r, true, 200, "subdomain-http")
|
||||
}
|
||||
|
||||
func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
|
||||
rewriteURL := h.Parent.rewriteURL(target.Root, r.RequestURI)
|
||||
r.URL, _ = url.Parse(rewriteURL)
|
||||
r.Header.Set("X-Forwarded-Host", r.Host)
|
||||
if r.Header["Upgrade"] != nil && r.Header["Upgrade"][0] == "websocket" {
|
||||
if r.Header["Upgrade"] != nil && strings.ToLower(r.Header["Upgrade"][0]) == "websocket" {
|
||||
//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
|
||||
r.Header.Set("A-Upgrade", "websocket")
|
||||
wsRedirectionEndpoint := target.Domain
|
||||
@ -86,6 +108,7 @@ func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, targ
|
||||
if target.RequireTLS {
|
||||
u, _ = url.Parse("wss://" + wsRedirectionEndpoint + r.URL.String())
|
||||
}
|
||||
h.logRequest(r, true, 101, "vdir-websocket")
|
||||
wspHandler := websocketproxy.NewProxy(u)
|
||||
wspHandler.ServeHTTP(w, r)
|
||||
return
|
||||
@ -93,7 +116,34 @@ func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, targ
|
||||
|
||||
r.Host = r.URL.Host
|
||||
err := target.Proxy.ServeHTTP(w, r)
|
||||
var dnsError *net.DNSError
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
if errors.As(err, &dnsError) {
|
||||
http.ServeFile(w, r, "./web/hosterror.html")
|
||||
log.Println(err.Error())
|
||||
h.logRequest(r, false, 404, "vdir-http")
|
||||
} else {
|
||||
http.ServeFile(w, r, "./web/rperror.html")
|
||||
log.Println(err.Error())
|
||||
h.logRequest(r, false, 521, "vdir-http")
|
||||
}
|
||||
}
|
||||
h.logRequest(r, true, 200, "vdir-http")
|
||||
|
||||
}
|
||||
|
||||
func (h *ProxyHandler) logRequest(r *http.Request, succ bool, statusCode int, forwardType string) {
|
||||
if h.Parent.Option.StatisticCollector != nil {
|
||||
go func() {
|
||||
requestInfo := statistic.RequestInfo{
|
||||
IpAddr: geodb.GetRequesterIP(r),
|
||||
RequestOriginalCountryISOCode: h.Parent.Option.GeodbStore.GetRequesterCountryISOCode(r),
|
||||
Succ: succ,
|
||||
StatusCode: statusCode,
|
||||
ForwardType: forwardType,
|
||||
}
|
||||
h.Parent.Option.StatisticCollector.RecordRequest(requestInfo)
|
||||
}()
|
||||
|
||||
}
|
||||
}
|
53
src/mod/dynamicproxy/redirection/handler.go
Normal file
@ -0,0 +1,53 @@
|
||||
package redirection
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
handler.go
|
||||
|
||||
This script store the handlers use for handling
|
||||
redirection request
|
||||
*/
|
||||
|
||||
//Check if a request URL is a redirectable URI
|
||||
func (t *RuleTable) IsRedirectable(r *http.Request) bool {
|
||||
requestPath := r.Host + r.URL.Path
|
||||
rr := t.MatchRedirectRule(requestPath)
|
||||
return rr != nil
|
||||
}
|
||||
|
||||
//Handle the redirect request, return after calling this function to prevent
|
||||
//multiple write to the response writer
|
||||
//Return the status code of the redirection handling
|
||||
func (t *RuleTable) HandleRedirect(w http.ResponseWriter, r *http.Request) int {
|
||||
requestPath := r.Host + r.URL.Path
|
||||
rr := t.MatchRedirectRule(requestPath)
|
||||
if rr != nil {
|
||||
redirectTarget := rr.TargetURL
|
||||
//Always pad a / at the back of the target URL
|
||||
if redirectTarget[len(redirectTarget)-1:] != "/" {
|
||||
redirectTarget += "/"
|
||||
}
|
||||
if rr.ForwardChildpath {
|
||||
//Remove the first / in the path
|
||||
redirectTarget += r.URL.Path[1:] + "?" + r.URL.RawQuery
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(redirectTarget, "http://") && !strings.HasPrefix(redirectTarget, "https://") {
|
||||
redirectTarget = "http://" + redirectTarget
|
||||
}
|
||||
|
||||
http.Redirect(w, r, redirectTarget, rr.StatusCode)
|
||||
return rr.StatusCode
|
||||
} else {
|
||||
//Invalid usage
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("500 - Internal Server Error"))
|
||||
log.Println("Target request URL do not have matching redirect rule. Check with IsRedirectable before calling HandleRedirect!")
|
||||
return 500
|
||||
}
|
||||
}
|
162
src/mod/dynamicproxy/redirection/redirection.go
Normal file
@ -0,0 +1,162 @@
|
||||
package redirection
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
type RuleTable struct {
|
||||
configPath string //The location where the redirection rules is stored
|
||||
rules sync.Map //Store the redirection rules for this reverse proxy instance
|
||||
}
|
||||
|
||||
type RedirectRules struct {
|
||||
RedirectURL string //The matching URL to redirect
|
||||
TargetURL string //The destination redirection url
|
||||
ForwardChildpath bool //Also redirect the pathname
|
||||
StatusCode int //Status Code for redirection
|
||||
}
|
||||
|
||||
func NewRuleTable(configPath string) (*RuleTable, error) {
|
||||
thisRuleTable := RuleTable{
|
||||
rules: sync.Map{},
|
||||
configPath: configPath,
|
||||
}
|
||||
//Load all the rules from the config path
|
||||
if !utils.FileExists(configPath) {
|
||||
os.MkdirAll(configPath, 0775)
|
||||
}
|
||||
|
||||
// Load all the *.json from the configPath
|
||||
files, err := filepath.Glob(filepath.Join(configPath, "*.json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse the json content into RedirectRules
|
||||
var rules []*RedirectRules
|
||||
for _, file := range files {
|
||||
b, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
thisRule := RedirectRules{}
|
||||
|
||||
err = json.Unmarshal(b, &thisRule)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
rules = append(rules, &thisRule)
|
||||
}
|
||||
|
||||
//Map the rules into the sync map
|
||||
for _, rule := range rules {
|
||||
log.Println("Redirection rule added: " + rule.RedirectURL + " -> " + rule.TargetURL)
|
||||
thisRuleTable.rules.Store(rule.RedirectURL, rule)
|
||||
}
|
||||
|
||||
return &thisRuleTable, nil
|
||||
}
|
||||
|
||||
func (t *RuleTable) AddRedirectRule(redirectURL string, destURL string, forwardPathname bool, statusCode int) error {
|
||||
// Create a new RedirectRules object with the given parameters
|
||||
newRule := &RedirectRules{
|
||||
RedirectURL: redirectURL,
|
||||
TargetURL: destURL,
|
||||
ForwardChildpath: forwardPathname,
|
||||
StatusCode: statusCode,
|
||||
}
|
||||
|
||||
// Convert the redirectURL to a valid filename by replacing "/" with "-" and "." with "_"
|
||||
filename := strings.ReplaceAll(strings.ReplaceAll(redirectURL, "/", "-"), ".", "_") + ".json"
|
||||
|
||||
// Create the full file path by joining the t.configPath with the filename
|
||||
filepath := path.Join(t.configPath, filename)
|
||||
|
||||
// Create a new file for writing the JSON data
|
||||
file, err := os.Create(filepath)
|
||||
if err != nil {
|
||||
log.Printf("Error creating file %s: %s", filepath, err)
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Encode the RedirectRules object to JSON and write it to the file
|
||||
err = json.NewEncoder(file).Encode(newRule)
|
||||
if err != nil {
|
||||
log.Printf("Error encoding JSON to file %s: %s", filepath, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Store the RedirectRules object in the sync.Map
|
||||
t.rules.Store(redirectURL, newRule)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *RuleTable) DeleteRedirectRule(redirectURL string) error {
|
||||
// Convert the redirectURL to a valid filename by replacing "/" with "-" and "." with "_"
|
||||
filename := strings.ReplaceAll(strings.ReplaceAll(redirectURL, "/", "-"), ".", "_") + ".json"
|
||||
|
||||
// Create the full file path by joining the t.configPath with the filename
|
||||
filepath := path.Join(t.configPath, filename)
|
||||
|
||||
// Check if the file exists
|
||||
if _, err := os.Stat(filepath); os.IsNotExist(err) {
|
||||
return nil // File doesn't exist, nothing to delete
|
||||
}
|
||||
|
||||
// Delete the file
|
||||
if err := os.Remove(filepath); err != nil {
|
||||
log.Printf("Error deleting file %s: %s", filepath, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete the key-value pair from the sync.Map
|
||||
t.rules.Delete(redirectURL)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get a list of all the redirection rules
|
||||
func (t *RuleTable) GetAllRedirectRules() []*RedirectRules {
|
||||
rules := []*RedirectRules{}
|
||||
t.rules.Range(func(key, value interface{}) bool {
|
||||
r, ok := value.(*RedirectRules)
|
||||
if ok {
|
||||
rules = append(rules, r)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return rules
|
||||
}
|
||||
|
||||
// Check if a given request URL matched any of the redirection rule
|
||||
func (t *RuleTable) MatchRedirectRule(requestedURL string) *RedirectRules {
|
||||
// Iterate through all the keys in the rules map
|
||||
var targetRedirectionRule *RedirectRules = nil
|
||||
var maxMatch int = 0
|
||||
|
||||
t.rules.Range(func(key interface{}, value interface{}) bool {
|
||||
// Check if the requested URL starts with the key as a prefix
|
||||
if strings.HasPrefix(requestedURL, key.(string)) {
|
||||
// This request URL matched the domain
|
||||
if len(key.(string)) > maxMatch {
|
||||
maxMatch = len(key.(string))
|
||||
targetRedirectionRule = value.(*RedirectRules)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return targetRedirectionRule
|
||||
}
|
@ -4,7 +4,7 @@ import (
|
||||
"log"
|
||||
"net/url"
|
||||
|
||||
"imuslab.com/arozos/ReverseProxy/mod/reverseproxy"
|
||||
"imuslab.com/zoraxy/mod/reverseproxy"
|
||||
)
|
||||
|
||||
/*
|
244
src/mod/geodb/geodb.go
Normal file
@ -0,0 +1,244 @@
|
||||
package geodb
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/oschwald/geoip2-golang"
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
)
|
||||
|
||||
type Store struct {
|
||||
Enabled bool
|
||||
geodb *geoip2.Reader
|
||||
sysdb *database.Database
|
||||
}
|
||||
|
||||
type CountryInfo struct {
|
||||
CountryIsoCode string
|
||||
ContinetCode string
|
||||
}
|
||||
|
||||
func NewGeoDb(sysdb *database.Database, dbfile string) (*Store, error) {
|
||||
db, err := geoip2.Open(dbfile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = sysdb.NewTable("blacklist-cn")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = sysdb.NewTable("blacklist-ip")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = sysdb.NewTable("blacklist")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blacklistEnabled := false
|
||||
sysdb.Read("blacklist", "enabled", &blacklistEnabled)
|
||||
|
||||
return &Store{
|
||||
Enabled: blacklistEnabled,
|
||||
geodb: db,
|
||||
sysdb: sysdb,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) ToggleBlacklist(enabled bool) {
|
||||
s.sysdb.Write("blacklist", "enabled", enabled)
|
||||
s.Enabled = enabled
|
||||
}
|
||||
|
||||
func (s *Store) ResolveCountryCodeFromIP(ipstring string) (*CountryInfo, error) {
|
||||
// If you are using strings that may be invalid, check that ip is not nil
|
||||
ip := net.ParseIP(ipstring)
|
||||
record, err := s.geodb.City(ip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &CountryInfo{
|
||||
record.Country.IsoCode,
|
||||
record.Continent.Code,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) Close() {
|
||||
s.geodb.Close()
|
||||
}
|
||||
|
||||
func (s *Store) AddCountryCodeToBlackList(countryCode string) {
|
||||
countryCode = strings.ToLower(countryCode)
|
||||
s.sysdb.Write("blacklist-cn", countryCode, true)
|
||||
}
|
||||
|
||||
func (s *Store) RemoveCountryCodeFromBlackList(countryCode string) {
|
||||
countryCode = strings.ToLower(countryCode)
|
||||
s.sysdb.Delete("blacklist-cn", countryCode)
|
||||
}
|
||||
|
||||
func (s *Store) IsCountryCodeBlacklisted(countryCode string) bool {
|
||||
countryCode = strings.ToLower(countryCode)
|
||||
var isBlacklisted bool = false
|
||||
s.sysdb.Read("blacklist-cn", countryCode, &isBlacklisted)
|
||||
return isBlacklisted
|
||||
}
|
||||
|
||||
func (s *Store) GetAllBlacklistedCountryCode() []string {
|
||||
bannedCountryCodes := []string{}
|
||||
entries, err := s.sysdb.ListTable("blacklist-cn")
|
||||
if err != nil {
|
||||
return bannedCountryCodes
|
||||
}
|
||||
for _, keypairs := range entries {
|
||||
ip := string(keypairs[0])
|
||||
bannedCountryCodes = append(bannedCountryCodes, ip)
|
||||
}
|
||||
|
||||
return bannedCountryCodes
|
||||
}
|
||||
|
||||
func (s *Store) AddIPToBlackList(ipAddr string) {
|
||||
s.sysdb.Write("blacklist-ip", ipAddr, true)
|
||||
}
|
||||
|
||||
func (s *Store) RemoveIPFromBlackList(ipAddr string) {
|
||||
s.sysdb.Delete("blacklist-ip", ipAddr)
|
||||
}
|
||||
|
||||
func (s *Store) IsIPBlacklisted(ipAddr string) bool {
|
||||
var isBlacklisted bool = false
|
||||
s.sysdb.Read("blacklist-ip", ipAddr, &isBlacklisted)
|
||||
if isBlacklisted {
|
||||
return true
|
||||
}
|
||||
|
||||
//Check for IP wildcard and CIRD rules
|
||||
AllBlacklistedIps := s.GetAllBlacklistedIp()
|
||||
for _, blacklistRule := range AllBlacklistedIps {
|
||||
wildcardMatch := MatchIpWildcard(ipAddr, blacklistRule)
|
||||
if wildcardMatch {
|
||||
return true
|
||||
}
|
||||
|
||||
cidrMatch := MatchIpCIDR(ipAddr, blacklistRule)
|
||||
if cidrMatch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Store) GetAllBlacklistedIp() []string {
|
||||
bannedIps := []string{}
|
||||
entries, err := s.sysdb.ListTable("blacklist-ip")
|
||||
if err != nil {
|
||||
return bannedIps
|
||||
}
|
||||
|
||||
for _, keypairs := range entries {
|
||||
ip := string(keypairs[0])
|
||||
bannedIps = append(bannedIps, ip)
|
||||
}
|
||||
|
||||
return bannedIps
|
||||
}
|
||||
|
||||
//Check if a IP address is blacklisted, in either country or IP blacklist
|
||||
func (s *Store) IsBlacklisted(ipAddr string) bool {
|
||||
if !s.Enabled {
|
||||
//Blacklist not enabled. Always return false
|
||||
return false
|
||||
}
|
||||
|
||||
if ipAddr == "" {
|
||||
//Unable to get the target IP address
|
||||
return false
|
||||
}
|
||||
|
||||
countryCode, err := s.ResolveCountryCodeFromIP(ipAddr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if s.IsCountryCodeBlacklisted(countryCode.CountryIsoCode) {
|
||||
return true
|
||||
}
|
||||
|
||||
if s.IsIPBlacklisted(ipAddr) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Store) GetRequesterCountryISOCode(r *http.Request) string {
|
||||
ipAddr := GetRequesterIP(r)
|
||||
if ipAddr == "" {
|
||||
return ""
|
||||
}
|
||||
countryCode, err := s.ResolveCountryCodeFromIP(ipAddr)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return countryCode.CountryIsoCode
|
||||
}
|
||||
|
||||
//Utilities function
|
||||
func GetRequesterIP(r *http.Request) string {
|
||||
ip := r.Header.Get("X-Forwarded-For")
|
||||
if ip == "" {
|
||||
ip = r.Header.Get("X-Real-IP")
|
||||
if ip == "" {
|
||||
ip = strings.Split(r.RemoteAddr, ":")[0]
|
||||
}
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
||||
//Match the IP address with a wildcard string
|
||||
func MatchIpWildcard(ipAddress, wildcard string) bool {
|
||||
// Split IP address and wildcard into octets
|
||||
ipOctets := strings.Split(ipAddress, ".")
|
||||
wildcardOctets := strings.Split(wildcard, ".")
|
||||
|
||||
// Check that both have 4 octets
|
||||
if len(ipOctets) != 4 || len(wildcardOctets) != 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check each octet to see if it matches the wildcard or is an exact match
|
||||
for i := 0; i < 4; i++ {
|
||||
if wildcardOctets[i] == "*" {
|
||||
continue
|
||||
}
|
||||
if ipOctets[i] != wildcardOctets[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
//Match ip address with CIDR
|
||||
func MatchIpCIDR(ip string, cidr string) bool {
|
||||
// parse the CIDR string
|
||||
_, cidrnet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// parse the IP address
|
||||
ipAddr := net.ParseIP(ip)
|
||||
|
||||
// check if the IP address is within the CIDR range
|
||||
return cidrnet.Contains(ipAddr)
|
||||
}
|
@ -276,7 +276,7 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request) erro
|
||||
p.logf("http: proxy error: %v", err)
|
||||
}
|
||||
|
||||
rw.WriteHeader(http.StatusBadGateway)
|
||||
//rw.WriteHeader(http.StatusBadGateway)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -288,7 +288,7 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request) erro
|
||||
if p.Verbal {
|
||||
p.logf("http: proxy error: %v", err)
|
||||
}
|
||||
rw.WriteHeader(http.StatusBadGateway)
|
||||
//rw.WriteHeader(http.StatusBadGateway)
|
||||
return err
|
||||
}
|
||||
}
|
76
src/mod/statistic/handler.go
Normal file
@ -0,0 +1,76 @@
|
||||
package statistic
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
/*
|
||||
Handler.go
|
||||
|
||||
This script handles incoming request for loading the statistic of the day
|
||||
|
||||
*/
|
||||
|
||||
func (c *Collector) HandleTodayStatLoad(w http.ResponseWriter, r *http.Request) {
|
||||
type DailySummaryExport struct {
|
||||
TotalRequest int64 //Total request of the day
|
||||
ErrorRequest int64 //Invalid request of the day, including error or not found
|
||||
ValidRequest int64 //Valid request of the day
|
||||
|
||||
ForwardTypes map[string]int
|
||||
RequestOrigin map[string]int
|
||||
RequestClientIp map[string]int
|
||||
}
|
||||
|
||||
fast, err := utils.GetPara(r, "fast")
|
||||
if err != nil {
|
||||
fast = "false"
|
||||
}
|
||||
d := c.DailySummary
|
||||
if fast == "true" {
|
||||
//Only return the counter
|
||||
exported := DailySummaryExport{
|
||||
TotalRequest: d.TotalRequest,
|
||||
ErrorRequest: d.ErrorRequest,
|
||||
ValidRequest: d.ValidRequest,
|
||||
}
|
||||
js, _ := json.Marshal(exported)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
//Return everything
|
||||
exported := DailySummaryExport{
|
||||
TotalRequest: d.TotalRequest,
|
||||
ErrorRequest: d.ErrorRequest,
|
||||
ValidRequest: d.ValidRequest,
|
||||
ForwardTypes: make(map[string]int),
|
||||
RequestOrigin: make(map[string]int),
|
||||
RequestClientIp: make(map[string]int),
|
||||
}
|
||||
|
||||
// Export ForwardTypes sync.Map
|
||||
d.ForwardTypes.Range(func(key, value interface{}) bool {
|
||||
exported.ForwardTypes[key.(string)] = value.(int)
|
||||
return true
|
||||
})
|
||||
|
||||
// Export RequestOrigin sync.Map
|
||||
d.RequestOrigin.Range(func(key, value interface{}) bool {
|
||||
exported.RequestOrigin[key.(string)] = value.(int)
|
||||
return true
|
||||
})
|
||||
|
||||
// Export RequestClientIp sync.Map
|
||||
d.RequestClientIp.Range(func(key, value interface{}) bool {
|
||||
exported.RequestClientIp[key.(string)] = value.(int)
|
||||
return true
|
||||
})
|
||||
|
||||
js, _ := json.Marshal(exported)
|
||||
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
}
|
184
src/mod/statistic/statistic.go
Normal file
@ -0,0 +1,184 @@
|
||||
package statistic
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/database"
|
||||
)
|
||||
|
||||
/*
|
||||
Statistic Package
|
||||
|
||||
This packet is designed to collection information
|
||||
and store them for future analysis
|
||||
*/
|
||||
|
||||
//Faststat, a interval summary for all collected data and avoid
|
||||
//looping through every data everytime a overview is needed
|
||||
type DailySummary struct {
|
||||
TotalRequest int64 //Total request of the day
|
||||
ErrorRequest int64 //Invalid request of the day, including error or not found
|
||||
ValidRequest int64 //Valid request of the day
|
||||
//Type counters
|
||||
ForwardTypes *sync.Map //Map that hold the forward types
|
||||
RequestOrigin *sync.Map //Map that hold [country ISO code]: visitor counter
|
||||
RequestClientIp *sync.Map //Map that hold all unique request IPs
|
||||
}
|
||||
|
||||
type RequestInfo struct {
|
||||
IpAddr string
|
||||
RequestOriginalCountryISOCode string
|
||||
Succ bool
|
||||
StatusCode int
|
||||
ForwardType string
|
||||
}
|
||||
|
||||
type CollectorOption struct {
|
||||
Database *database.Database
|
||||
}
|
||||
|
||||
type Collector struct {
|
||||
rtdataStopChan chan bool
|
||||
DailySummary *DailySummary
|
||||
Option *CollectorOption
|
||||
}
|
||||
|
||||
func NewStatisticCollector(option CollectorOption) (*Collector, error) {
|
||||
option.Database.NewTable("stats")
|
||||
|
||||
//Create the collector object
|
||||
thisCollector := Collector{
|
||||
DailySummary: newDailySummary(),
|
||||
Option: &option,
|
||||
}
|
||||
|
||||
//Load the stat if exists for today
|
||||
//This will exists if the program was forcefully restarted
|
||||
year, month, day := time.Now().Date()
|
||||
summary := thisCollector.LoadSummaryOfDay(year, month, day)
|
||||
if summary != nil {
|
||||
thisCollector.DailySummary = summary
|
||||
}
|
||||
|
||||
//Schedule the realtime statistic clearing at midnight everyday
|
||||
rtstatStopChan := thisCollector.ScheduleResetRealtimeStats()
|
||||
thisCollector.rtdataStopChan = rtstatStopChan
|
||||
|
||||
return &thisCollector, nil
|
||||
}
|
||||
|
||||
//Write the current in-memory summary to database file
|
||||
func (c *Collector) SaveSummaryOfDay() {
|
||||
//When it is called in 0:00am, make sure it is stored as yesterday key
|
||||
t := time.Now().Add(-30 * time.Second)
|
||||
summaryKey := t.Format("02_01_2006")
|
||||
c.Option.Database.Write("stats", summaryKey, c.DailySummary)
|
||||
}
|
||||
|
||||
//Load the summary of a day given
|
||||
func (c *Collector) LoadSummaryOfDay(year int, month time.Month, day int) *DailySummary {
|
||||
date := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.Local)
|
||||
summaryKey := date.Format("02_01_2006")
|
||||
var targetSummary = newDailySummary()
|
||||
c.Option.Database.Read("stats", summaryKey, &targetSummary)
|
||||
return targetSummary
|
||||
}
|
||||
|
||||
//This function gives the current slot in the 288- 5 minutes interval of the day
|
||||
func (c *Collector) GetCurrentRealtimeStatIntervalId() int {
|
||||
now := time.Now()
|
||||
startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local).Unix()
|
||||
secondsSinceStartOfDay := now.Unix() - startOfDay
|
||||
interval := secondsSinceStartOfDay / (5 * 60)
|
||||
return int(interval)
|
||||
}
|
||||
|
||||
func (c *Collector) Close() {
|
||||
//Stop the ticker
|
||||
c.rtdataStopChan <- true
|
||||
|
||||
//Write the buffered data into database
|
||||
c.SaveSummaryOfDay()
|
||||
|
||||
}
|
||||
|
||||
//Main function to record all the inbound traffics
|
||||
//Note that this function run in go routine and might have concurrent R/W issue
|
||||
//Please make sure there is no racing paramters in this function
|
||||
func (c *Collector) RecordRequest(ri RequestInfo) {
|
||||
go func() {
|
||||
c.DailySummary.TotalRequest++
|
||||
if ri.Succ {
|
||||
c.DailySummary.ValidRequest++
|
||||
} else {
|
||||
c.DailySummary.ErrorRequest++
|
||||
}
|
||||
|
||||
//Store the request info into correct types of maps
|
||||
ft, ok := c.DailySummary.ForwardTypes.Load(ri.ForwardType)
|
||||
if !ok {
|
||||
c.DailySummary.ForwardTypes.Store(ri.ForwardType, 1)
|
||||
} else {
|
||||
c.DailySummary.ForwardTypes.Store(ri.ForwardType, ft.(int)+1)
|
||||
}
|
||||
|
||||
originISO := strings.ToLower(ri.RequestOriginalCountryISOCode)
|
||||
fo, ok := c.DailySummary.RequestOrigin.Load(originISO)
|
||||
if !ok {
|
||||
c.DailySummary.RequestOrigin.Store(originISO, 1)
|
||||
} else {
|
||||
c.DailySummary.RequestOrigin.Store(originISO, fo.(int)+1)
|
||||
}
|
||||
|
||||
fi, ok := c.DailySummary.RequestClientIp.Load(ri.IpAddr)
|
||||
if !ok {
|
||||
c.DailySummary.RequestClientIp.Store(ri.IpAddr, 1)
|
||||
} else {
|
||||
c.DailySummary.RequestClientIp.Store(ri.IpAddr, fi.(int)+1)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
//nightly task
|
||||
func (c *Collector) ScheduleResetRealtimeStats() chan bool {
|
||||
doneCh := make(chan bool)
|
||||
|
||||
go func() {
|
||||
defer close(doneCh)
|
||||
|
||||
for {
|
||||
// calculate duration until next midnight
|
||||
now := time.Now()
|
||||
|
||||
// Get midnight of the next day in the local time zone
|
||||
midnight := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, now.Location())
|
||||
|
||||
// Calculate the duration until midnight
|
||||
duration := midnight.Sub(now)
|
||||
select {
|
||||
case <-time.After(duration):
|
||||
// store daily summary to database and reset summary
|
||||
c.SaveSummaryOfDay()
|
||||
c.DailySummary = newDailySummary()
|
||||
case <-doneCh:
|
||||
// stop the routine
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return doneCh
|
||||
}
|
||||
|
||||
func newDailySummary() *DailySummary {
|
||||
return &DailySummary{
|
||||
TotalRequest: 0,
|
||||
ErrorRequest: 0,
|
||||
ValidRequest: 0,
|
||||
ForwardTypes: &sync.Map{},
|
||||
RequestOrigin: &sync.Map{},
|
||||
RequestClientIp: &sync.Map{},
|
||||
}
|
||||
}
|
60
src/mod/tlscert/helper.go
Normal file
@ -0,0 +1,60 @@
|
||||
package tlscert
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//This remove the certificates in the list where either the
|
||||
//public key or the private key is missing
|
||||
func getCertPairs(certFiles []string) []string {
|
||||
crtMap := make(map[string]bool)
|
||||
keyMap := make(map[string]bool)
|
||||
|
||||
for _, filename := range certFiles {
|
||||
if filepath.Ext(filename) == ".crt" {
|
||||
crtMap[strings.TrimSuffix(filename, ".crt")] = true
|
||||
} else if filepath.Ext(filename) == ".key" {
|
||||
keyMap[strings.TrimSuffix(filename, ".key")] = true
|
||||
}
|
||||
}
|
||||
|
||||
var result []string
|
||||
for domain := range crtMap {
|
||||
if keyMap[domain] {
|
||||
result = append(result, domain)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
//Get the cloest subdomain certificate from a list of domains
|
||||
func matchClosestDomainCertificate(subdomain string, domains []string) string {
|
||||
var matchingDomain string = ""
|
||||
maxLength := 0
|
||||
|
||||
for _, domain := range domains {
|
||||
if strings.HasSuffix(subdomain, "."+domain) && len(domain) > maxLength {
|
||||
matchingDomain = domain
|
||||
maxLength = len(domain)
|
||||
}
|
||||
}
|
||||
|
||||
return matchingDomain
|
||||
}
|
||||
|
||||
//Check if a requesting domain is a subdomain of a given domain
|
||||
func isSubdomain(subdomain, domain string) bool {
|
||||
subdomainParts := strings.Split(subdomain, ".")
|
||||
domainParts := strings.Split(domain, ".")
|
||||
if len(subdomainParts) < len(domainParts) {
|
||||
return false
|
||||
}
|
||||
for i := range domainParts {
|
||||
if subdomainParts[len(subdomainParts)-1-i] != domainParts[len(domainParts)-1-i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
172
src/mod/tlscert/tlscert.go
Normal file
@ -0,0 +1,172 @@
|
||||
package tlscert
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
CertStore string
|
||||
verbal bool
|
||||
}
|
||||
|
||||
func NewManager(certStore string) (*Manager, error) {
|
||||
if !utils.FileExists(certStore) {
|
||||
os.MkdirAll(certStore, 0775)
|
||||
}
|
||||
|
||||
thisManager := Manager{
|
||||
CertStore: certStore,
|
||||
verbal: true,
|
||||
}
|
||||
|
||||
return &thisManager, nil
|
||||
}
|
||||
|
||||
func (m *Manager) ListCertDomains() ([]string, error) {
|
||||
filenames, err := m.ListCerts()
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
//Remove certificates where there are missing public key or private key
|
||||
filenames = getCertPairs(filenames)
|
||||
|
||||
return filenames, nil
|
||||
}
|
||||
|
||||
func (m *Manager) ListCerts() ([]string, error) {
|
||||
certs, err := ioutil.ReadDir(m.CertStore)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
filenames := make([]string, 0, len(certs))
|
||||
for _, cert := range certs {
|
||||
if !cert.IsDir() {
|
||||
filenames = append(filenames, cert.Name())
|
||||
}
|
||||
}
|
||||
|
||||
return filenames, nil
|
||||
}
|
||||
|
||||
func (m *Manager) GetCert(helloInfo *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
//Check if the domain corrisponding cert exists
|
||||
pubKey := "./system/localhost.crt"
|
||||
priKey := "./system/localhost.key"
|
||||
|
||||
if utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".crt")) && utils.FileExists(filepath.Join(m.CertStore, helloInfo.ServerName+".key")) {
|
||||
pubKey = filepath.Join(m.CertStore, helloInfo.ServerName+".crt")
|
||||
priKey = filepath.Join(m.CertStore, helloInfo.ServerName+".key")
|
||||
|
||||
} else {
|
||||
domainCerts, _ := m.ListCertDomains()
|
||||
cloestDomainCert := matchClosestDomainCertificate(helloInfo.ServerName, domainCerts)
|
||||
if cloestDomainCert != "" {
|
||||
//There is a matching parent domain for this subdomain. Use this instead.
|
||||
pubKey = filepath.Join(m.CertStore, cloestDomainCert+".crt")
|
||||
priKey = filepath.Join(m.CertStore, cloestDomainCert+".key")
|
||||
} else if m.DefaultCertExists() {
|
||||
//Use default.crt and default.key
|
||||
pubKey = filepath.Join(m.CertStore, "default.crt")
|
||||
priKey = filepath.Join(m.CertStore, "default.key")
|
||||
if m.verbal {
|
||||
log.Println("No matching certificate found. Serving with default")
|
||||
}
|
||||
} else {
|
||||
if m.verbal {
|
||||
log.Println("Matching certificate not found. Serving with default. Requesting server name: ", helloInfo.ServerName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Load the cert and serve it
|
||||
cer, err := tls.LoadX509KeyPair(pubKey, priKey)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &cer, nil
|
||||
}
|
||||
|
||||
//Check if both the default cert public key and private key exists
|
||||
func (m *Manager) DefaultCertExists() bool {
|
||||
return utils.FileExists(filepath.Join(m.CertStore, "default.crt")) && utils.FileExists(filepath.Join(m.CertStore, "default.key"))
|
||||
}
|
||||
|
||||
//Check if the default cert exists returning seperate results for pubkey and prikey
|
||||
func (m *Manager) DefaultCertExistsSep() (bool, bool) {
|
||||
return utils.FileExists(filepath.Join(m.CertStore, "default.crt")), utils.FileExists(filepath.Join(m.CertStore, "default.key"))
|
||||
}
|
||||
|
||||
//Delete the cert if exists
|
||||
func (m *Manager) RemoveCert(domain string) error {
|
||||
pubKey := filepath.Join(m.CertStore, domain+".crt")
|
||||
priKey := filepath.Join(m.CertStore, domain+".key")
|
||||
if utils.FileExists(pubKey) {
|
||||
err := os.Remove(pubKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if utils.FileExists(priKey) {
|
||||
err := os.Remove(priKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//Check if the given file is a valid TLS file
|
||||
func IsValidTLSFile(file io.Reader) bool {
|
||||
// Read the contents of the uploaded file
|
||||
contents, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
// Handle the error
|
||||
return false
|
||||
}
|
||||
|
||||
// Parse the contents of the file as a PEM-encoded certificate or key
|
||||
block, _ := pem.Decode(contents)
|
||||
if block == nil {
|
||||
// The file is not a valid PEM-encoded certificate or key
|
||||
return false
|
||||
}
|
||||
|
||||
// Parse the certificate or key
|
||||
if strings.Contains(block.Type, "CERTIFICATE") {
|
||||
// The file contains a certificate
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
// Handle the error
|
||||
return false
|
||||
}
|
||||
// Check if the certificate is a valid TLS/SSL certificate
|
||||
return cert.IsCA == false && cert.KeyUsage&x509.KeyUsageDigitalSignature != 0 && cert.KeyUsage&x509.KeyUsageKeyEncipherment != 0
|
||||
} else if strings.Contains(block.Type, "PRIVATE KEY") {
|
||||
// The file contains a private key
|
||||
_, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
// Handle the error
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
127
src/mod/upnp/upnp.go
Normal file
@ -0,0 +1,127 @@
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitlab.com/NebulousLabs/go-upnp"
|
||||
)
|
||||
|
||||
/*
|
||||
uPNP Module
|
||||
|
||||
This module handles uPNP Connections to the gateway router and create a port forward entry
|
||||
for the host system at the given port (set with -port paramter)
|
||||
*/
|
||||
|
||||
type UPnPClient struct {
|
||||
Connection *upnp.IGD //UPnP conenction object
|
||||
ExternalIP string //Storage of external IP address
|
||||
RequiredPorts []int //All the required ports will be recored
|
||||
PolicyNames sync.Map //Name for the required port nubmer
|
||||
}
|
||||
|
||||
func NewUPNPClient() (*UPnPClient, error) {
|
||||
//Create uPNP forwarding in the NAT router
|
||||
log.Println("Discovering UPnP router in Local Area Network...")
|
||||
d, err := upnp.Discover()
|
||||
if err != nil {
|
||||
return &UPnPClient{}, err
|
||||
}
|
||||
|
||||
// discover external IP
|
||||
ip, err := d.ExternalIP()
|
||||
if err != nil {
|
||||
return &UPnPClient{}, err
|
||||
}
|
||||
|
||||
//Create the final obejcts
|
||||
newUPnPObject := &UPnPClient{
|
||||
Connection: d,
|
||||
ExternalIP: ip,
|
||||
RequiredPorts: []int{},
|
||||
}
|
||||
|
||||
return newUPnPObject, nil
|
||||
}
|
||||
|
||||
func (u *UPnPClient) ForwardPort(portNumber int, ruleName string) error {
|
||||
log.Println("UPnP forwarding new port: ", portNumber, "for "+ruleName+" service")
|
||||
|
||||
//Check if port already forwarded
|
||||
_, ok := u.PolicyNames.Load(portNumber)
|
||||
if ok {
|
||||
//Port already forward. Ignore this request
|
||||
return errors.New("Port already forwarded")
|
||||
}
|
||||
|
||||
// forward a port
|
||||
err := u.Connection.Forward(uint16(portNumber), ruleName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.RequiredPorts = append(u.RequiredPorts, portNumber)
|
||||
u.PolicyNames.Store(portNumber, ruleName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UPnPClient) ClosePort(portNumber int) error {
|
||||
//Check if port is opened
|
||||
portOpened := false
|
||||
newRequiredPort := []int{}
|
||||
for _, thisPort := range u.RequiredPorts {
|
||||
if thisPort != portNumber {
|
||||
newRequiredPort = append(newRequiredPort, thisPort)
|
||||
} else {
|
||||
portOpened = true
|
||||
}
|
||||
}
|
||||
|
||||
if portOpened {
|
||||
//Update the port list
|
||||
u.RequiredPorts = newRequiredPort
|
||||
|
||||
// Close the port
|
||||
log.Println("Closing UPnP Port Forward: ", portNumber)
|
||||
err := u.Connection.Clear(uint16(portNumber))
|
||||
|
||||
//Delete the name registry
|
||||
u.PolicyNames.Delete(portNumber)
|
||||
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Renew forward rules, prevent router lease time from flushing the Upnp config
|
||||
func (u *UPnPClient) RenewForwardRules() {
|
||||
portsToRenew := u.RequiredPorts
|
||||
for _, thisPort := range portsToRenew {
|
||||
ruleName, ok := u.PolicyNames.Load(thisPort)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
u.ClosePort(thisPort)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
u.ForwardPort(thisPort, ruleName.(string))
|
||||
}
|
||||
log.Println("UPnP Port Forward rule renew completed")
|
||||
}
|
||||
|
||||
func (u *UPnPClient) Close() {
|
||||
//Shutdown the default UPnP Object
|
||||
if u != nil {
|
||||
for _, portNumber := range u.RequiredPorts {
|
||||
err := u.Connection.Clear(uint16(portNumber))
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
16
src/mod/utils/conv.go
Normal file
@ -0,0 +1,16 @@
|
||||
package utils
|
||||
|
||||
import "strconv"
|
||||
|
||||
func StringToInt64(number string) (int64, error) {
|
||||
i, err := strconv.ParseInt(number, 10, 64)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func Int64ToString(number int64) string {
|
||||
convedNumber := strconv.FormatInt(number, 10)
|
||||
return convedNumber
|
||||
}
|
19
src/mod/utils/template.go
Normal file
@ -0,0 +1,19 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
/*
|
||||
Web Template Generator
|
||||
|
||||
This is the main system core module that perform function similar to what PHP did.
|
||||
To replace part of the content of any file, use {{paramter}} to replace it.
|
||||
|
||||
|
||||
*/
|
||||
|
||||
func SendHTMLResponse(w http.ResponseWriter, msg string) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(msg))
|
||||
}
|
230
src/mod/utils/utils.go
Normal file
@ -0,0 +1,230 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
Common
|
||||
|
||||
Some commonly used functions in ArozOS
|
||||
|
||||
*/
|
||||
|
||||
// Response related
|
||||
func SendTextResponse(w http.ResponseWriter, msg string) {
|
||||
w.Write([]byte(msg))
|
||||
}
|
||||
|
||||
// Send JSON response, with an extra json header
|
||||
func SendJSONResponse(w http.ResponseWriter, json string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(json))
|
||||
}
|
||||
|
||||
func SendErrorResponse(w http.ResponseWriter, errMsg string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte("{\"error\":\"" + errMsg + "\"}"))
|
||||
}
|
||||
|
||||
func SendOK(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte("\"OK\""))
|
||||
}
|
||||
|
||||
/*
|
||||
The paramter move function (mv)
|
||||
|
||||
You can find similar things in the PHP version of ArOZ Online Beta. You need to pass in
|
||||
r (HTTP Request Object)
|
||||
getParamter (string, aka $_GET['This string])
|
||||
|
||||
Will return
|
||||
Paramter string (if any)
|
||||
Error (if error)
|
||||
|
||||
*/
|
||||
/*
|
||||
func Mv(r *http.Request, getParamter string, postMode bool) (string, error) {
|
||||
if postMode == false {
|
||||
//Access the paramter via GET
|
||||
keys, ok := r.URL.Query()[getParamter]
|
||||
|
||||
if !ok || len(keys[0]) < 1 {
|
||||
//log.Println("Url Param " + getParamter +" is missing")
|
||||
return "", errors.New("GET paramter " + getParamter + " not found or it is empty")
|
||||
}
|
||||
|
||||
// Query()["key"] will return an array of items,
|
||||
// we only want the single item.
|
||||
key := keys[0]
|
||||
return string(key), nil
|
||||
} else {
|
||||
//Access the parameter via POST
|
||||
r.ParseForm()
|
||||
x := r.Form.Get(getParamter)
|
||||
if len(x) == 0 || x == "" {
|
||||
return "", errors.New("POST paramter " + getParamter + " not found or it is empty")
|
||||
}
|
||||
return string(x), nil
|
||||
}
|
||||
|
||||
}
|
||||
*/
|
||||
|
||||
// Get GET parameter
|
||||
func GetPara(r *http.Request, key string) (string, error) {
|
||||
keys, ok := r.URL.Query()[key]
|
||||
if !ok || len(keys[0]) < 1 {
|
||||
return "", errors.New("invalid " + key + " given")
|
||||
} else {
|
||||
return keys[0], nil
|
||||
}
|
||||
}
|
||||
|
||||
// Get POST paramter
|
||||
func PostPara(r *http.Request, key string) (string, error) {
|
||||
r.ParseForm()
|
||||
x := r.Form.Get(key)
|
||||
if x == "" {
|
||||
return "", errors.New("invalid " + key + " given")
|
||||
} else {
|
||||
return x, nil
|
||||
}
|
||||
}
|
||||
|
||||
func FileExists(filename string) bool {
|
||||
_, err := os.Stat(filename)
|
||||
if os.IsNotExist(err) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func IsDir(path string) bool {
|
||||
if FileExists(path) == false {
|
||||
return false
|
||||
}
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return false
|
||||
}
|
||||
switch mode := fi.Mode(); {
|
||||
case mode.IsDir():
|
||||
return true
|
||||
case mode.IsRegular():
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TimeToString(targetTime time.Time) string {
|
||||
return targetTime.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
func LoadImageAsBase64(filepath string) (string, error) {
|
||||
if !FileExists(filepath) {
|
||||
return "", errors.New("File not exists")
|
||||
}
|
||||
f, _ := os.Open(filepath)
|
||||
reader := bufio.NewReader(f)
|
||||
content, _ := io.ReadAll(reader)
|
||||
encoded := base64.StdEncoding.EncodeToString(content)
|
||||
return string(encoded), nil
|
||||
}
|
||||
|
||||
// Use for redirections
|
||||
func ConstructRelativePathFromRequestURL(requestURI string, redirectionLocation string) string {
|
||||
if strings.Count(requestURI, "/") == 1 {
|
||||
//Already root level
|
||||
return redirectionLocation
|
||||
}
|
||||
for i := 0; i < strings.Count(requestURI, "/")-1; i++ {
|
||||
redirectionLocation = "../" + redirectionLocation
|
||||
}
|
||||
|
||||
return redirectionLocation
|
||||
}
|
||||
|
||||
// Check if given string in a given slice
|
||||
func StringInArray(arr []string, str string) bool {
|
||||
for _, a := range arr {
|
||||
if a == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func StringInArrayIgnoreCase(arr []string, str string) bool {
|
||||
smallArray := []string{}
|
||||
for _, item := range arr {
|
||||
smallArray = append(smallArray, strings.ToLower(item))
|
||||
}
|
||||
|
||||
return StringInArray(smallArray, strings.ToLower(str))
|
||||
}
|
||||
|
||||
func ExtractTarGzipByStream(basedir string, gzipStream io.Reader, onErrorResumeNext bool) error {
|
||||
uncompressedStream, err := gzip.NewReader(gzipStream)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tarReader := tar.NewReader(uncompressedStream)
|
||||
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
err := os.Mkdir(header.Name, 0755)
|
||||
if err != nil {
|
||||
if !onErrorResumeNext {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
case tar.TypeReg:
|
||||
outFile, err := os.Create(filepath.Join(basedir, header.Name))
|
||||
if err != nil {
|
||||
if !onErrorResumeNext {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = io.Copy(outFile, tarReader)
|
||||
if err != nil {
|
||||
if !onErrorResumeNext {
|
||||
return err
|
||||
}
|
||||
}
|
||||
outFile.Close()
|
||||
|
||||
default:
|
||||
//Unknown filetype, continue
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
75
src/redirect.go
Normal file
@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
/*
|
||||
Redirect.go
|
||||
|
||||
This script handle all the http handlers
|
||||
related to redirection function in the reverse proxy
|
||||
*/
|
||||
|
||||
func handleListRedirectionRules(w http.ResponseWriter, r *http.Request) {
|
||||
rules := redirectTable.GetAllRedirectRules()
|
||||
js, _ := json.Marshal(rules)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
func handleAddRedirectionRule(w http.ResponseWriter, r *http.Request) {
|
||||
redirectUrl, err := utils.PostPara(r, "redirectUrl")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "redirect url cannot be empty")
|
||||
return
|
||||
}
|
||||
destUrl, err := utils.PostPara(r, "destUrl")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "destination url cannot be empty")
|
||||
}
|
||||
|
||||
forwardChildpath, err := utils.PostPara(r, "forwardChildpath")
|
||||
if err != nil {
|
||||
//Assume true
|
||||
forwardChildpath = "true"
|
||||
}
|
||||
|
||||
redirectTypeString, err := utils.PostPara(r, "redirectType")
|
||||
if err != nil {
|
||||
redirectTypeString = "307"
|
||||
}
|
||||
|
||||
redirectionStatusCode, err := strconv.Atoi(redirectTypeString)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid status code number")
|
||||
return
|
||||
}
|
||||
|
||||
err = redirectTable.AddRedirectRule(redirectUrl, destUrl, forwardChildpath == "true", redirectionStatusCode)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func handleDeleteRedirectionRule(w http.ResponseWriter, r *http.Request) {
|
||||
redirectUrl, err := utils.PostPara(r, "redirectUrl")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "redirect url cannot be empty")
|
||||
return
|
||||
}
|
||||
|
||||
err = redirectTable.DeleteRedirectRule(redirectUrl)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
314
src/reverseproxy.go
Normal file
@ -0,0 +1,314 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
dynamicProxyRouter *dynamicproxy.Router
|
||||
)
|
||||
|
||||
// Add user customizable reverse proxy
|
||||
func ReverseProxtInit() {
|
||||
inboundPort := 80
|
||||
if sysdb.KeyExists("settings", "inbound") {
|
||||
sysdb.Read("settings", "inbound", &inboundPort)
|
||||
log.Println("Serving inbound port ", inboundPort)
|
||||
} else {
|
||||
log.Println("Inbound port not set. Using default (80)")
|
||||
}
|
||||
|
||||
useTls := false
|
||||
sysdb.Read("settings", "usetls", &useTls)
|
||||
if useTls {
|
||||
log.Println("TLS mode enabled. Serving proxxy request with TLS")
|
||||
} else {
|
||||
log.Println("TLS mode disabled. Serving proxy request with plain http")
|
||||
}
|
||||
|
||||
forceHttpsRedirect := false
|
||||
sysdb.Read("settings", "redirect", &forceHttpsRedirect)
|
||||
if forceHttpsRedirect {
|
||||
log.Println("Force HTTPS mode enabled")
|
||||
} else {
|
||||
log.Println("Force HTTPS mode disabled")
|
||||
}
|
||||
|
||||
dprouter, err := dynamicproxy.NewDynamicProxy(dynamicproxy.RouterOption{
|
||||
Port: inboundPort,
|
||||
UseTls: useTls,
|
||||
ForceHttpsRedirect: forceHttpsRedirect,
|
||||
TlsManager: tlsCertManager,
|
||||
RedirectRuleTable: redirectTable,
|
||||
GeodbStore: geodbStore,
|
||||
StatisticCollector: statisticCollector,
|
||||
})
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
dynamicProxyRouter = dprouter
|
||||
|
||||
//Load all conf from files
|
||||
confs, _ := filepath.Glob("./conf/*.config")
|
||||
for _, conf := range confs {
|
||||
record, err := LoadReverseProxyConfig(conf)
|
||||
if err != nil {
|
||||
log.Println("Failed to load "+filepath.Base(conf), err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if record.ProxyType == "root" {
|
||||
dynamicProxyRouter.SetRootProxy(record.ProxyTarget, record.UseTLS)
|
||||
} else if record.ProxyType == "subd" {
|
||||
dynamicProxyRouter.AddSubdomainRoutingService(record.Rootname, record.ProxyTarget, record.UseTLS)
|
||||
} else if record.ProxyType == "vdir" {
|
||||
dynamicProxyRouter.AddVirtualDirectoryProxyService(record.Rootname, record.ProxyTarget, record.UseTLS)
|
||||
} 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
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
dynamicProxyRouter.StartProxyService()
|
||||
log.Println("Dynamic Reverse Proxy service started")
|
||||
|
||||
}
|
||||
|
||||
func ReverseProxyHandleOnOff(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
enable, _ := utils.PostPara(r, "enable") //Support root, vdir and subd
|
||||
if enable == "true" {
|
||||
err := dynamicProxyRouter.StartProxyService()
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
//Check if it is loopback
|
||||
if dynamicProxyRouter.IsProxiedSubdomain(r) {
|
||||
//Loopback routing. Turning it off will make the user lost control
|
||||
//of the whole system. Do not allow shutdown
|
||||
utils.SendErrorResponse(w, "Unable to shutdown in loopback rp mode. Remove proxy rules for management interface and retry.")
|
||||
return
|
||||
}
|
||||
|
||||
err := dynamicProxyRouter.StopProxyService()
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
eptype, err := utils.PostPara(r, "type") //Support root, vdir and subd
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "type not defined")
|
||||
return
|
||||
}
|
||||
|
||||
endpoint, err := utils.PostPara(r, "ep")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "endpoint not defined")
|
||||
return
|
||||
}
|
||||
|
||||
tls, _ := utils.PostPara(r, "tls")
|
||||
if tls == "" {
|
||||
tls = "false"
|
||||
}
|
||||
|
||||
useTLS := (tls == "true")
|
||||
rootname := ""
|
||||
if eptype == "vdir" {
|
||||
vdir, err := utils.PostPara(r, "rootname")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "vdir not defined")
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(vdir, "/") {
|
||||
vdir = "/" + vdir
|
||||
}
|
||||
rootname = vdir
|
||||
dynamicProxyRouter.AddVirtualDirectoryProxyService(vdir, endpoint, useTLS)
|
||||
|
||||
} else if eptype == "subd" {
|
||||
subdomain, err := utils.PostPara(r, "rootname")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "subdomain not defined")
|
||||
return
|
||||
}
|
||||
rootname = subdomain
|
||||
dynamicProxyRouter.AddSubdomainRoutingService(subdomain, endpoint, useTLS)
|
||||
} else if eptype == "root" {
|
||||
rootname = "root"
|
||||
dynamicProxyRouter.SetRootProxy(endpoint, useTLS)
|
||||
} else {
|
||||
//Invalid eptype
|
||||
utils.SendErrorResponse(w, "Invalid endpoint type")
|
||||
return
|
||||
}
|
||||
|
||||
//Save it
|
||||
SaveReverseProxyConfig(eptype, rootname, endpoint, useTLS)
|
||||
|
||||
utils.SendOK(w)
|
||||
|
||||
}
|
||||
|
||||
func DeleteProxyEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
ep, err := utils.GetPara(r, "ep")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Invalid ep given")
|
||||
}
|
||||
|
||||
ptype, err := utils.PostPara(r, "ptype")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Invalid ptype given")
|
||||
}
|
||||
|
||||
err = dynamicProxyRouter.RemoveProxy(ptype, ep)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
}
|
||||
|
||||
RemoveReverseProxyConfig(ep)
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func ReverseProxyStatus(w http.ResponseWriter, r *http.Request) {
|
||||
js, _ := json.Marshal(dynamicProxyRouter)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
func ReverseProxyList(w http.ResponseWriter, r *http.Request) {
|
||||
eptype, err := utils.PostPara(r, "type") //Support root, vdir and subd
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "type not defined")
|
||||
return
|
||||
}
|
||||
|
||||
if eptype == "vdir" {
|
||||
results := []*dynamicproxy.ProxyEndpoint{}
|
||||
dynamicProxyRouter.ProxyEndpoints.Range(func(key, value interface{}) bool {
|
||||
results = append(results, value.(*dynamicproxy.ProxyEndpoint))
|
||||
return true
|
||||
})
|
||||
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].Domain < results[j].Domain
|
||||
})
|
||||
|
||||
js, _ := json.Marshal(results)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else if eptype == "subd" {
|
||||
results := []*dynamicproxy.SubdomainEndpoint{}
|
||||
dynamicProxyRouter.SubdomainEndpoint.Range(func(key, value interface{}) bool {
|
||||
results = append(results, value.(*dynamicproxy.SubdomainEndpoint))
|
||||
return true
|
||||
})
|
||||
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
return results[i].MatchingDomain < results[j].MatchingDomain
|
||||
})
|
||||
|
||||
js, _ := json.Marshal(results)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else if eptype == "root" {
|
||||
js, _ := json.Marshal(dynamicProxyRouter.Root)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "Invalid type given")
|
||||
}
|
||||
}
|
||||
|
||||
// Handle https redirect
|
||||
func HandleUpdateHttpsRedirect(w http.ResponseWriter, r *http.Request) {
|
||||
useRedirect, err := utils.GetPara(r, "set")
|
||||
if err != nil {
|
||||
currentRedirectToHttps := false
|
||||
//Load the current status
|
||||
err = sysdb.Read("settings", "redirect", ¤tRedirectToHttps)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
js, _ := json.Marshal(currentRedirectToHttps)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
if useRedirect == "true" {
|
||||
sysdb.Write("settings", "redirect", true)
|
||||
log.Println("Updating force HTTPS redirection to true")
|
||||
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(true)
|
||||
} else if useRedirect == "false" {
|
||||
sysdb.Write("settings", "redirect", false)
|
||||
log.Println("Updating force HTTPS redirection to false")
|
||||
dynamicProxyRouter.UpdateHttpToHttpsRedirectSetting(false)
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
}
|
||||
|
||||
//Handle checking if the current user is accessing via the reverse proxied interface
|
||||
//Of the management interface.
|
||||
func HandleManagementProxyCheck(w http.ResponseWriter, r *http.Request) {
|
||||
isProxied := dynamicProxyRouter.IsProxiedSubdomain(r)
|
||||
js, _ := json.Marshal(isProxied)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
|
||||
// Handle incoming port set. Change the current proxy incoming port
|
||||
func HandleIncomingPortSet(w http.ResponseWriter, r *http.Request) {
|
||||
newIncomingPort, err := utils.PostPara(r, "incoming")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid incoming port given")
|
||||
return
|
||||
}
|
||||
|
||||
newIncomingPortInt, err := strconv.Atoi(newIncomingPort)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid incoming port given")
|
||||
return
|
||||
}
|
||||
|
||||
//Stop and change the setting of the reverse proxy service
|
||||
if dynamicProxyRouter.Running {
|
||||
dynamicProxyRouter.StopProxyService()
|
||||
dynamicProxyRouter.Option.Port = newIncomingPortInt
|
||||
dynamicProxyRouter.StartProxyService()
|
||||
} else {
|
||||
//Only change setting but not starting the proxy service
|
||||
dynamicProxyRouter.Option.Port = newIncomingPortInt
|
||||
}
|
||||
|
||||
sysdb.Write("settings", "inbound", newIncomingPortInt)
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
32
src/router.go
Normal file
@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
router.go
|
||||
|
||||
This script holds the static resources router
|
||||
for the reverse proxy service
|
||||
*/
|
||||
|
||||
func AuthFsHandler(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Allow access to /script/*, /img/pubic/* and /login.html without authentication
|
||||
if strings.HasPrefix(r.URL.Path, "/script/") || strings.HasPrefix(r.URL.Path, "/img/public/") || r.URL.Path == "/login.html" || r.URL.Path == "/favicon.png" {
|
||||
handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// check authentication
|
||||
if !authAgent.CheckAuth(r) {
|
||||
http.Redirect(w, r, "/login.html", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
|
||||
//Authenticated
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
BIN
src/system/GeoLite2-Country.mmdb
Normal file
17
src/system/gomod-license.csv
Normal file
@ -0,0 +1,17 @@
|
||||
github.com/boltdb/bolt,https://github.com/boltdb/bolt/blob/v1.3.1/LICENSE,MIT
|
||||
github.com/gorilla/securecookie,https://github.com/gorilla/securecookie/blob/v1.1.1/LICENSE,BSD-3-Clause
|
||||
github.com/gorilla/sessions,https://github.com/gorilla/sessions/blob/v1.2.1/LICENSE,BSD-3-Clause
|
||||
github.com/gorilla/websocket,https://github.com/gorilla/websocket/blob/v1.4.2/LICENSE,BSD-2-Clause
|
||||
github.com/oschwald/geoip2-golang,https://github.com/oschwald/geoip2-golang/blob/v1.8.0/LICENSE,ISC
|
||||
github.com/oschwald/maxminddb-golang,https://github.com/oschwald/maxminddb-golang/blob/v1.10.0/LICENSE,ISC
|
||||
gitlab.com/NebulousLabs/fastrand,https://gitlab.com/NebulousLabs/fastrand/blob/603482d69e40/LICENSE,MIT
|
||||
gitlab.com/NebulousLabs/go-upnp,https://gitlab.com/NebulousLabs/go-upnp/blob/11da932010b6/LICENSE,MIT
|
||||
gitlab.com/NebulousLabs/go-upnp/goupnp,https://gitlab.com/NebulousLabs/go-upnp/blob/11da932010b6/goupnp\LICENSE,BSD-2-Clause
|
||||
golang.org/x/crypto/blake2b,https://cs.opensource.google/go/x/crypto/+/0c34fe9e:LICENSE,BSD-3-Clause
|
||||
golang.org/x/net/html,https://cs.opensource.google/go/x/net/+/afb366fc:LICENSE,BSD-3-Clause
|
||||
golang.org/x/sys,https://cs.opensource.google/go/x/sys/+/v0.6.0:LICENSE,BSD-3-Clause
|
||||
golang.org/x/text,https://cs.opensource.google/go/x/text/+/v0.3.6:LICENSE,BSD-3-Clause
|
||||
imuslab.com/zoraxy,Unknown,MIT
|
||||
imuslab.com/zoraxy/mod/dynamicproxy/dpcore,Unknown,MIT
|
||||
imuslab.com/zoraxy/mod/reverseproxy,Unknown,MIT
|
||||
imuslab.com/zoraxy/mod/websocketproxy,Unknown,MIT
|
|
34
src/system/localhost.crt
Normal file
@ -0,0 +1,34 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIF8TCCA9mgAwIBAgIUavNWjB6rlfRLpeXJ9TXb2FVrENYwDQYJKoZIhvcNAQEL
|
||||
BQAwbjELMAkGA1UEBhMCR0wxEjAQBgNVBAgMCU1pbGt5IFdheTEOMAwGA1UEBwwF
|
||||
RWFydGgxEDAOBgNVBAoMB2ltdXNsYWIxDzANBgNVBAsMBkFyb3pPUzEYMBYGA1UE
|
||||
AwwPd3d3LmltdXNsYWIuY29tMB4XDTIxMDkxNzA4NTkyNFoXDTQ5MDIwMTA4NTky
|
||||
NFowbjELMAkGA1UEBhMCR0wxEjAQBgNVBAgMCU1pbGt5IFdheTEOMAwGA1UEBwwF
|
||||
RWFydGgxEDAOBgNVBAoMB2ltdXNsYWIxDzANBgNVBAsMBkFyb3pPUzEYMBYGA1UE
|
||||
AwwPd3d3LmltdXNsYWIuY29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC
|
||||
AgEAsBpf9ufRYOfdKft+51EibpqhA9yw6YstxL5BNselx3ETVnu7vYRIlH0ypgPN
|
||||
nKguZ+BcN4mJFjQ36N4VpN7ySVfOCSCZz7lPvPfLib9iukBodBYQNAzMkKcLjyoY
|
||||
gS8MD99cqe7s48k4JKp6b2WOmn2OtVZIS7AKZvVsRJNblhy7C3LkLnASKF0jb/ia
|
||||
MGRAE+QV/zznvGg9FhNgQWWUil2Oesx3elj4KwlcHNX+c9pZz6yVgJrerj0s94OD
|
||||
EuueiqAFOWsZrpp754ffC45PbeTNiflQ1B3aqkTtl5bL88ESgwMdtb1JGWN5HIS1
|
||||
Tq2d/3PgqbtvUEhggaFDbe0OxG2V33HqEfeG3BpZpYhCB3I7FPpRC/Tp8PACY13N
|
||||
HYB9P5hRU/DnINhHjMCLKxHsolhiphWuxSuNIIojRL62zj7JwjnBgcghQzVFJ4O4
|
||||
TBfeMDadLII3ndDtsmR1dIba7fg+CWWdv4Zs0XGqHOaiHNclc7BhJF8SgiQxjxjm
|
||||
Fh1ZsJm3LxPsw/iCl7ILE7+1aBQlBjEj0yBvMttkEDhRbILxXFPMALG/qakPvW9O
|
||||
7WWClAc03ei/JFdq2camuY62/Tf1HB+TSpGWYH+cSIqsu3V5u29jmdZjrjnuM7Fz
|
||||
GEjNSCsrMhSLYLkMJmrDGdFQBB31x24o9IXtyrfKZiwxMlUCAwEAAaOBhjCBgzAL
|
||||
BgNVHQ8EBAMCBeAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwQAYDVR0RBDkwN4IBKoIQ
|
||||
aW11c2xhYi5pbnRlcm5hbIISbm90LmZvci5wcm9kdWN0aW9uggxkZXYudXNlLm9u
|
||||
bHkwHQYDVR0OBBYEFISIH/rn8RX1hcNf4rQajJR7FEdMMA0GCSqGSIb3DQEBCwUA
|
||||
A4ICAQBVldF/qjWyGJ5TiZMiXly/So9zR3Xq7O1qayqYxb5SxvhZYCtsFVrAl6Ux
|
||||
5bTZ0XQagjck2VouHOG6s98DpaslWFw9N8ADAmljQ8WL1hT5Ij1LXs2sF0FqttFf
|
||||
YgoT5BOjnHZGlN+FgzAkdF91cYrfZwLm63jvAQtIHwjMSeymy2Fq8gdEZxagYuwG
|
||||
gLkZxw1YG+gP778CKHT2Ff232kH+5up460aGLHLvg+xHQIWBt2FNGdv68u57hWxh
|
||||
XXji4/DewQ0RdJW1JdpSg4npebDNiXpo9pKY/SxU056raOtPA94U/h12cHVkszT7
|
||||
IxdFC2PszAblbSZhHKGE0C6SbATsqvK4gz6e4h7HWVuPPNWpPW2BNjvyenpijV/E
|
||||
YsSe6F7uQE/I/iHp9VMcjWuwItqed9yKDeOfDH4+pidowbSJQ97xYfZge36ZEUHC
|
||||
2ZdQsR0qS+t2h0KlEDN7FNxai3ikSB1bs2AjtU67ofGtoIz/HD70TT6zHKhISZgI
|
||||
w/4/SY7Hd+P+AWSdJwo+ycZYZlXajqh/cxVJ0zVBr5vKC9KnJ+IjnQ/q7CLcxM4W
|
||||
aAFC1jakdPz7qO+xNVLQRf8lVnPJNtI88OrlL4n02JlLS/QUSwELXFW0bOKP33jm
|
||||
PIbPdeP8k0XVe9wlI7MzUQC8pCt+gQ77awTt83Nxp9Xdn1Zbqw==
|
||||
-----END CERTIFICATE-----
|
52
src/system/localhost.key
Normal file
@ -0,0 +1,52 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCwGl/259Fg590p
|
||||
+37nUSJumqED3LDpiy3EvkE2x6XHcRNWe7u9hEiUfTKmA82cqC5n4Fw3iYkWNDfo
|
||||
3hWk3vJJV84JIJnPuU+898uJv2K6QGh0FhA0DMyQpwuPKhiBLwwP31yp7uzjyTgk
|
||||
qnpvZY6afY61VkhLsApm9WxEk1uWHLsLcuQucBIoXSNv+JowZEAT5BX/POe8aD0W
|
||||
E2BBZZSKXY56zHd6WPgrCVwc1f5z2lnPrJWAmt6uPSz3g4MS656KoAU5axmumnvn
|
||||
h98Ljk9t5M2J+VDUHdqqRO2XlsvzwRKDAx21vUkZY3kchLVOrZ3/c+Cpu29QSGCB
|
||||
oUNt7Q7EbZXfceoR94bcGlmliEIHcjsU+lEL9Onw8AJjXc0dgH0/mFFT8Ocg2EeM
|
||||
wIsrEeyiWGKmFa7FK40giiNEvrbOPsnCOcGByCFDNUUng7hMF94wNp0sgjed0O2y
|
||||
ZHV0htrt+D4JZZ2/hmzRcaoc5qIc1yVzsGEkXxKCJDGPGOYWHVmwmbcvE+zD+IKX
|
||||
sgsTv7VoFCUGMSPTIG8y22QQOFFsgvFcU8wAsb+pqQ+9b07tZYKUBzTd6L8kV2rZ
|
||||
xqa5jrb9N/UcH5NKkZZgf5xIiqy7dXm7b2OZ1mOuOe4zsXMYSM1IKysyFItguQwm
|
||||
asMZ0VAEHfXHbij0he3Kt8pmLDEyVQIDAQABAoICAATmtwUILqujyGQCu+V0PKEX
|
||||
bKPO4J2fYga3xNjhdZu3afJePztnEx4O3foA4RgbFi+N7wMcsNQNYAD7LV8JVXT1
|
||||
HKbkYWOGpNF9lAyhZv4IDOAuPQU11fuwqoGxij0OMie+77VLEQzF7OoYVJAFI5Lp
|
||||
K6+gVyLEI4X6DqlZ8JKc+he3euJP/DFjZjkXkjMGl0H2dyZDa6+ytwCGSYeIbDnt
|
||||
oKmKR0kAcOfBuu6ShiJzUUyWYRLTPJ9c1IOPBXbhV+hDy+FtOanCYvBut6Z6r3s/
|
||||
gvj0F2vP6OYURQiTCdoe5YT/8TO9sOsj+Zrxlpo5+svBTd9reA2j9gulkVrd3itN
|
||||
c2Ee7fyuyrCRnEcKoT6BI8/LqH5eWGQKKS9WhOz26VkrorcYYZN3g4ayv+MiiSIm
|
||||
jeo/kAWCqT5ylvlw2gaCbPjB4kbx7yMI/myjgF0R4+aNQaHpXa2qqEORitGx40M7
|
||||
T1V2JIxnsa83TBwumunkYC2pX7bNS0a1VuCNxUafJRKEcvKhWmiRHaWddZn46G8N
|
||||
E56qFzSaLbkd+J71jso9llK5joGIQTt2pbKUdV9LIm5Nsbtp2VgF9URIw5RZFftx
|
||||
PfSm9XM9DtWuxheO4gNwAuOvtaOxztNMvSkQzhTOggSRpt15hFd7CeBrpK43feAH
|
||||
b2pMequB8MHpUieyxlwBAoIBAQC5IRbaKx+fSEbYeIySUwbN8GCJQl+wmvc1gqCC
|
||||
DflEQqxTvCGBB5SLHHurTT0ubhXkvbrtuS5f4IC+htzKSuwlqn3lS0aOXtFP2tT6
|
||||
D9iiMxLxIId5l6dD+PjMWtQcWc8wUQ7+ieRgxybDqiCWMyTbvNgwlkcIbRxmcqyN
|
||||
4/LmmgzTnr5CH0DC/J7xpUJuX9LPVb4ZvBYjz5X++Yb7pCa+kXp0Z6yU48bG3sRe
|
||||
yiUKp3Z4vDoOkMLHTPvTQLG81rQuJnBUw2uLWM0kg1AwteZcQ/gH1ilVbJzMBnKm
|
||||
mtuJWtoPnM2zIhCsURngmBN+qxOb5kchMSvPzAQBCw7HBjWpAoIBAQDzhLQO434G
|
||||
XhyDcdkdMRbDZ8Q8PqtOloAbczMuPGgwHV7rVe/BvnJS7HDDebwlJBD8nhGvgBrp
|
||||
CsjNGHjSQC7ydUa8dP4Aw/46izdR8DsAwqGZq+tZhkY5CS88QpflUT5rftW0RObn
|
||||
Cb/gDzdxHy35/scSICxa2HwcZnqXqfEwnbjkxFwBYFSt6hRiwNhDhd6ZxKa6gt56
|
||||
DS9uIxt1IhKgXZfIw1Vo0mHHFLsB7czGZ0O24ya31Es0bUWGgWIcxvKw6MqKhFWw
|
||||
ncCakVg278UYUm/zt6Dcrn3XYnK7Pr944AiKO21PMQhG7Rb+OVwxgjMhk7/BCt+k
|
||||
sPR1Dct5pqrNAoIBAAl2jYp9ZdJoiWaLUvQv1ks0nFqnz+hhI33SvY2oVTOODO0C
|
||||
0tubnZY20IODITt8WRYmNKXuL1arTSlwD10v0z5hpqnP3T1tz1k7oGNf5/zyi2dT
|
||||
+FjYza4FzgH0Kp+AX7zih9evCMOBqpOZ4KyM1Ld+wbZKGDtwCGGcPwHJwyLSgRFY
|
||||
LfWHT3IoI5/KiMjHkSkUAvGh0afm9o3gB2xZibl4CkBlBEdgFUsZHASUZKxUvxOQ
|
||||
247fC3XQk5bK2csDVpZ9VISgsKCg22ugYrr6sVnKB6Wu5tH9CU7MjZPCmrI8uKTP
|
||||
qRwdA6krRB1c6LIy4H+5l600rD6k+Rdsj0bRJHECggEAeBXSrRzmAsHaEb/MryaL
|
||||
8SR0krjcxU5WMjMm5AAJ6OAy9J5WMxZ1TgsmuF6Jt08HyWsxkXf8zTryNqGAwz2/
|
||||
aPUIQtr2fu4nqjsItrFeh0tzYVJ0JpueeXXcAz1bpkvgGiZbwB/SNdCK/DTExFX5
|
||||
2DQZewi+lrX2zhKDFdNKCw1cJgPm0w7r8y9hiilK/FFBqlZdWdA7Ybiq0Qci/Som
|
||||
QUqmFOyua5iDeybv6U2ZE6XMsJ1ndHON+naAOIoJFePNvguuBYyorQW9+vr9o2mt
|
||||
qgbNCkRdYTXy/ImhxlB1H2hrDa+sgcbOLBuyoP8sRYXNLRutDccM7iwNAMQiuQTF
|
||||
aQKCAQEAiKPwUodT6LNu4lrSbsDAYIqWwlfM0wwUhudT5UTVHSYI3ap0QOiEuzOl
|
||||
IJVdx+vx7rQW7l+JIL6s4shA7mzpzuTVlhRuDuGZx0qQLP7INVpCLzIEbYGI2dL7
|
||||
WLhJd4eYKltJ+BG7S51tq9/6rVcUDn5DKzyGNyeGhOnaYkk+eTm483+vpOP2/ITi
|
||||
cbVv3mx4qE7zMPIxIufm+c8RonadJzYiq1uMk8t0TrcW/B9RTly/Y96kamjyU5b0
|
||||
OcLdRcx3ppKAxHD9AvwAR6SiuNLfNjM9KZM40zM5goMrCJJzwgb7UGeMuw2z7L9F
|
||||
+iSj2pW0Rbdy7oOcFRF/iM2GwFYc1Q==
|
||||
-----END PRIVATE KEY-----
|
210
src/upnp.go
Normal file
@ -0,0 +1,210 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"imuslab.com/zoraxy/mod/upnp"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
var upnpEnabled = false
|
||||
var preforwardMap map[int]string
|
||||
|
||||
func initUpnp() error {
|
||||
go func() {
|
||||
//Let UPnP discovery run in background
|
||||
var err error
|
||||
upnpClient, err = upnp.NewUPNPClient()
|
||||
if err != nil {
|
||||
log.Println("UPnP router discover error: ", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if upnpEnabled {
|
||||
//Forward all the ports
|
||||
for port, policyName := range preforwardMap {
|
||||
upnpClient.ForwardPort(port, policyName)
|
||||
log.Println("Upnp forwarding ", port, " for "+policyName)
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
//Check if the upnp was enabled
|
||||
sysdb.NewTable("upnp")
|
||||
sysdb.Read("upnp", "enabled", &upnpEnabled)
|
||||
|
||||
//Load all the ports from database
|
||||
portsMap := map[int]string{}
|
||||
sysdb.Read("upnp", "portmap", &portsMap)
|
||||
preforwardMap = portsMap
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleUpnpDiscover(w http.ResponseWriter, r *http.Request) {
|
||||
restart, err := utils.PostPara(r, "restart")
|
||||
if err != nil {
|
||||
type UpnpInfo struct {
|
||||
ExternalIp string
|
||||
RouterIp string
|
||||
}
|
||||
|
||||
if upnpClient == nil {
|
||||
utils.SendErrorResponse(w, "No UPnP router discovered")
|
||||
return
|
||||
}
|
||||
|
||||
parsedUrl, _ := url.Parse(upnpClient.Connection.Location())
|
||||
ipWithPort := parsedUrl.Host
|
||||
|
||||
result := UpnpInfo{
|
||||
ExternalIp: upnpClient.ExternalIP,
|
||||
RouterIp: ipWithPort,
|
||||
}
|
||||
|
||||
//Show if there is a upnpclient
|
||||
js, _ := json.Marshal(result)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
if restart == "true" {
|
||||
//Close the upnp client if exists
|
||||
if upnpClient != nil {
|
||||
saveForwardingPortsToDatabase()
|
||||
upnpClient.Close()
|
||||
}
|
||||
|
||||
//Restart a new one
|
||||
initUpnp()
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleToggleUPnP(w http.ResponseWriter, r *http.Request) {
|
||||
newMode, err := utils.PostPara(r, "mode")
|
||||
if err != nil {
|
||||
//Send the current mode to client side
|
||||
js, _ := json.Marshal(upnpEnabled)
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
} else {
|
||||
if newMode == "true" {
|
||||
upnpEnabled = true
|
||||
sysdb.Read("upnp", "enabled", true)
|
||||
|
||||
log.Println("UPnP Enabled. Forwarding all required ports")
|
||||
//Mount all Upnp requests from preforward Map
|
||||
for port, policyName := range preforwardMap {
|
||||
upnpClient.ForwardPort(port, policyName)
|
||||
log.Println("Upnp forwarding ", port, " for "+policyName)
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
}
|
||||
|
||||
utils.SendOK(w)
|
||||
return
|
||||
|
||||
} else if newMode == "false" {
|
||||
upnpEnabled = false
|
||||
sysdb.Read("upnp", "enabled", false)
|
||||
log.Println("UPnP disabled. Closing all forwarded ports")
|
||||
//Save the current forwarded ports
|
||||
saveForwardingPortsToDatabase()
|
||||
|
||||
//Unmount all Upnp request
|
||||
for _, port := range upnpClient.RequiredPorts {
|
||||
upnpClient.ClosePort(port)
|
||||
log.Println("UPnP port closed: ", port)
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
}
|
||||
|
||||
//done
|
||||
utils.SendOK(w)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func filterRFC2141(input string) string {
|
||||
rfc2141 := regexp.MustCompile(`^[\w\-.!~*'()]*(\%[\da-fA-F]{2}[\w\-.!~*'()]*)*$`)
|
||||
var result []rune
|
||||
for _, char := range input {
|
||||
if char <= 127 && rfc2141.MatchString(string(char)) {
|
||||
result = append(result, char)
|
||||
}
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func handleAddUpnpPort(w http.ResponseWriter, r *http.Request) {
|
||||
portString, err := utils.PostPara(r, "port")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid port given")
|
||||
return
|
||||
}
|
||||
|
||||
portNumber, err := strconv.Atoi(portString)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid port given")
|
||||
return
|
||||
}
|
||||
|
||||
policyName, err := utils.PostPara(r, "name")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid policy name")
|
||||
return
|
||||
}
|
||||
|
||||
policyName = filterRFC2141(policyName)
|
||||
|
||||
err = upnpClient.ForwardPort(portNumber, policyName)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
saveForwardingPortsToDatabase()
|
||||
|
||||
utils.SendOK(w)
|
||||
}
|
||||
|
||||
func handleRemoveUpnpPort(w http.ResponseWriter, r *http.Request) {
|
||||
portString, err := utils.PostPara(r, "port")
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid port given")
|
||||
return
|
||||
}
|
||||
|
||||
portNumber, err := strconv.Atoi(portString)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "invalid port given")
|
||||
return
|
||||
}
|
||||
|
||||
saveForwardingPortsToDatabase()
|
||||
|
||||
upnpClient.ClosePort(portNumber)
|
||||
}
|
||||
|
||||
func saveForwardingPortsToDatabase() {
|
||||
//Move the sync map to map[int]string
|
||||
m := make(map[int]string)
|
||||
upnpClient.PolicyNames.Range(func(key, value interface{}) bool {
|
||||
if k, ok := key.(int); ok {
|
||||
if v, ok := value.(string); ok {
|
||||
m[k] = v
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
preforwardMap = m
|
||||
sysdb.Write("upnp", "portmap", &preforwardMap)
|
||||
|
||||
}
|
599
src/web/components/blacklist.html
Normal file
@ -0,0 +1,599 @@
|
||||
|
||||
<h3><i class="ui ban icon"></i> Blacklist</h3>
|
||||
<p>Setup blacklist based on estimated IP geographic location or IP address</p>
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui toggle checkbox">
|
||||
<input type="checkbox" id="enableBlacklist">
|
||||
<label>Enable Blacklist</label>
|
||||
</div>
|
||||
<div id="toggleSucc" style="float: right; display:none; color: #2abd4d;" >
|
||||
<i class="ui green checkmark icon"></i> Setting Saved
|
||||
</div>
|
||||
<div class="ui message">
|
||||
<i class="info circle icon"></i> Blacklist function require complex checking logic to validate each incoming request. Not recommend enabling this feature on servers with low end hardware.
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<h4>Country Blacklist</h4>
|
||||
<p><i class="yellow exclamation triangle icon"></i>
|
||||
This will block all requests from the selected country. The requester's location is estimated from their IP address and may not be 100% accurate.</p>
|
||||
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>Select Country</label>
|
||||
<div id="countrySelector" class="ui fluid search selection dropdown">
|
||||
<input type="hidden" name="country">
|
||||
<i class="dropdown icon"></i>
|
||||
<div class="default text">Select Country</div>
|
||||
<div class="menu">
|
||||
<div class="item" data-value="af"><i class="af flag"></i>Afghanistan</div>
|
||||
<div class="item" data-value="ax"><i class="ax flag"></i>Aland Islands</div>
|
||||
<div class="item" data-value="al"><i class="al flag"></i>Albania</div>
|
||||
<div class="item" data-value="dz"><i class="dz flag"></i>Algeria</div>
|
||||
<div class="item" data-value="as"><i class="as flag"></i>American Samoa</div>
|
||||
<div class="item" data-value="ad"><i class="ad flag"></i>Andorra</div>
|
||||
<div class="item" data-value="ao"><i class="ao flag"></i>Angola</div>
|
||||
<div class="item" data-value="ai"><i class="ai flag"></i>Anguilla</div>
|
||||
<div class="item" data-value="ag"><i class="ag flag"></i>Antigua</div>
|
||||
<div class="item" data-value="ar"><i class="ar flag"></i>Argentina</div>
|
||||
<div class="item" data-value="am"><i class="am flag"></i>Armenia</div>
|
||||
<div class="item" data-value="aw"><i class="aw flag"></i>Aruba</div>
|
||||
<div class="item" data-value="au"><i class="au flag"></i>Australia</div>
|
||||
<div class="item" data-value="at"><i class="at flag"></i>Austria</div>
|
||||
<div class="item" data-value="az"><i class="az flag"></i>Azerbaijan</div>
|
||||
<div class="item" data-value="bs"><i class="bs flag"></i>Bahamas</div>
|
||||
<div class="item" data-value="bh"><i class="bh flag"></i>Bahrain</div>
|
||||
<div class="item" data-value="bd"><i class="bd flag"></i>Bangladesh</div>
|
||||
<div class="item" data-value="bb"><i class="bb flag"></i>Barbados</div>
|
||||
<div class="item" data-value="by"><i class="by flag"></i>Belarus</div>
|
||||
<div class="item" data-value="be"><i class="be flag"></i>Belgium</div>
|
||||
<div class="item" data-value="bz"><i class="bz flag"></i>Belize</div>
|
||||
<div class="item" data-value="bj"><i class="bj flag"></i>Benin</div>
|
||||
<div class="item" data-value="bm"><i class="bm flag"></i>Bermuda</div>
|
||||
<div class="item" data-value="bt"><i class="bt flag"></i>Bhutan</div>
|
||||
<div class="item" data-value="bo"><i class="bo flag"></i>Bolivia</div>
|
||||
<div class="item" data-value="ba"><i class="ba flag"></i>Bosnia</div>
|
||||
<div class="item" data-value="bw"><i class="bw flag"></i>Botswana</div>
|
||||
<div class="item" data-value="bv"><i class="bv flag"></i>Bouvet Island</div>
|
||||
<div class="item" data-value="br"><i class="br flag"></i>Brazil</div>
|
||||
<div class="item" data-value="vg"><i class="vg flag"></i>British Virgin Islands</div>
|
||||
<div class="item" data-value="bn"><i class="bn flag"></i>Brunei</div>
|
||||
<div class="item" data-value="bg"><i class="bg flag"></i>Bulgaria</div>
|
||||
<div class="item" data-value="bf"><i class="bf flag"></i>Burkina Faso</div>
|
||||
<div class="item" data-value="mm"><i class="mm flag"></i>Burma</div>
|
||||
<div class="item" data-value="bi"><i class="bi flag"></i>Burundi</div>
|
||||
<div class="item" data-value="tc"><i class="tc flag"></i>Caicos Islands</div>
|
||||
<div class="item" data-value="kh"><i class="kh flag"></i>Cambodia</div>
|
||||
<div class="item" data-value="cm"><i class="cm flag"></i>Cameroon</div>
|
||||
<div class="item" data-value="ca"><i class="ca flag"></i>Canada</div>
|
||||
<div class="item" data-value="cv"><i class="cv flag"></i>Cape Verde</div>
|
||||
<div class="item" data-value="ky"><i class="ky flag"></i>Cayman Islands</div>
|
||||
<div class="item" data-value="cf"><i class="cf flag"></i>Central African Republic</div>
|
||||
<div class="item" data-value="td"><i class="td flag"></i>Chad</div>
|
||||
<div class="item" data-value="cl"><i class="cl flag"></i>Chile</div>
|
||||
<div class="item" data-value="cn"><i class="cn flag"></i>China</div>
|
||||
<div class="item" data-value="cx"><i class="cx flag"></i>Christmas Island</div>
|
||||
<div class="item" data-value="cc"><i class="cc flag"></i>Cocos Islands</div>
|
||||
<div class="item" data-value="co"><i class="co flag"></i>Colombia</div>
|
||||
<div class="item" data-value="km"><i class="km flag"></i>Comoros</div>
|
||||
<div class="item" data-value="cg"><i class="cg flag"></i>Congo Brazzaville</div>
|
||||
<div class="item" data-value="cd"><i class="cd flag"></i>Congo</div>
|
||||
<div class="item" data-value="ck"><i class="ck flag"></i>Cook Islands</div>
|
||||
<div class="item" data-value="cr"><i class="cr flag"></i>Costa Rica</div>
|
||||
<div class="item" data-value="ci"><i class="ci flag"></i>Cote Divoire</div>
|
||||
<div class="item" data-value="hr"><i class="hr flag"></i>Croatia</div>
|
||||
<div class="item" data-value="cu"><i class="cu flag"></i>Cuba</div>
|
||||
<div class="item" data-value="cy"><i class="cy flag"></i>Cyprus</div>
|
||||
<div class="item" data-value="cz"><i class="cz flag"></i>Czech Republic</div>
|
||||
<div class="item" data-value="dk"><i class="dk flag"></i>Denmark</div>
|
||||
<div class="item" data-value="dj"><i class="dj flag"></i>Djibouti</div>
|
||||
<div class="item" data-value="dm"><i class="dm flag"></i>Dominica</div>
|
||||
<div class="item" data-value="do"><i class="do flag"></i>Dominican Republic</div>
|
||||
<div class="item" data-value="ec"><i class="ec flag"></i>Ecuador</div>
|
||||
<div class="item" data-value="eg"><i class="eg flag"></i>Egypt</div>
|
||||
<div class="item" data-value="sv"><i class="sv flag"></i>El Salvador</div>
|
||||
<div class="item" data-value="gb"><i class="gb flag"></i>England</div>
|
||||
<div class="item" data-value="gq"><i class="gq flag"></i>Equatorial Guinea</div>
|
||||
<div class="item" data-value="er"><i class="er flag"></i>Eritrea</div>
|
||||
<div class="item" data-value="ee"><i class="ee flag"></i>Estonia</div>
|
||||
<div class="item" data-value="et"><i class="et flag"></i>Ethiopia</div>
|
||||
<div class="item" data-value="eu"><i class="eu flag"></i>European Union</div>
|
||||
<div class="item" data-value="fk"><i class="fk flag"></i>Falkland Islands</div>
|
||||
<div class="item" data-value="fo"><i class="fo flag"></i>Faroe Islands</div>
|
||||
<div class="item" data-value="fj"><i class="fj flag"></i>Fiji</div>
|
||||
<div class="item" data-value="fi"><i class="fi flag"></i>Finland</div>
|
||||
<div class="item" data-value="fr"><i class="fr flag"></i>France</div>
|
||||
<div class="item" data-value="gf"><i class="gf flag"></i>French Guiana</div>
|
||||
<div class="item" data-value="pf"><i class="pf flag"></i>French Polynesia</div>
|
||||
<div class="item" data-value="tf"><i class="tf flag"></i>French Territories</div>
|
||||
<div class="item" data-value="ga"><i class="ga flag"></i>Gabon</div>
|
||||
<div class="item" data-value="gm"><i class="gm flag"></i>Gambia</div>
|
||||
<div class="item" data-value="ge"><i class="ge flag"></i>Georgia</div>
|
||||
<div class="item" data-value="de"><i class="de flag"></i>Germany</div>
|
||||
<div class="item" data-value="gh"><i class="gh flag"></i>Ghana</div>
|
||||
<div class="item" data-value="gi"><i class="gi flag"></i>Gibraltar</div>
|
||||
<div class="item" data-value="gr"><i class="gr flag"></i>Greece</div>
|
||||
<div class="item" data-value="gl"><i class="gl flag"></i>Greenland</div>
|
||||
<div class="item" data-value="gd"><i class="gd flag"></i>Grenada</div>
|
||||
<div class="item" data-value="gp"><i class="gp flag"></i>Guadeloupe</div>
|
||||
<div class="item" data-value="gu"><i class="gu flag"></i>Guam</div>
|
||||
<div class="item" data-value="gt"><i class="gt flag"></i>Guatemala</div>
|
||||
<div class="item" data-value="gw"><i class="gw flag"></i>Guinea-Bissau</div>
|
||||
<div class="item" data-value="gn"><i class="gn flag"></i>Guinea</div>
|
||||
<div class="item" data-value="gy"><i class="gy flag"></i>Guyana</div>
|
||||
<div class="item" data-value="ht"><i class="ht flag"></i>Haiti</div>
|
||||
<div class="item" data-value="hm"><i class="hm flag"></i>Heard Island</div>
|
||||
<div class="item" data-value="hn"><i class="hn flag"></i>Honduras</div>
|
||||
<div class="item" data-value="hk"><i class="hk flag"></i>Hong Kong</div>
|
||||
<div class="item" data-value="hu"><i class="hu flag"></i>Hungary</div>
|
||||
<div class="item" data-value="is"><i class="is flag"></i>Iceland</div>
|
||||
<div class="item" data-value="in"><i class="in flag"></i>India</div>
|
||||
<div class="item" data-value="io"><i class="io flag"></i>Indian Ocean Territory</div>
|
||||
<div class="item" data-value="id"><i class="id flag"></i>Indonesia</div>
|
||||
<div class="item" data-value="ir"><i class="ir flag"></i>Iran</div>
|
||||
<div class="item" data-value="iq"><i class="iq flag"></i>Iraq</div>
|
||||
<div class="item" data-value="ie"><i class="ie flag"></i>Ireland</div>
|
||||
<div class="item" data-value="il"><i class="il flag"></i>Israel</div>
|
||||
<div class="item" data-value="it"><i class="it flag"></i>Italy</div>
|
||||
<div class="item" data-value="jm"><i class="jm flag"></i>Jamaica</div>
|
||||
<div class="item" data-value="jp"><i class="jp flag"></i>Japan</div>
|
||||
<div class="item" data-value="jo"><i class="jo flag"></i>Jordan</div>
|
||||
<div class="item" data-value="kz"><i class="kz flag"></i>Kazakhstan</div>
|
||||
<div class="item" data-value="ke"><i class="ke flag"></i>Kenya</div>
|
||||
<div class="item" data-value="ki"><i class="ki flag"></i>Kiribati</div>
|
||||
<div class="item" data-value="kw"><i class="kw flag"></i>Kuwait</div>
|
||||
<div class="item" data-value="kg"><i class="kg flag"></i>Kyrgyzstan</div>
|
||||
<div class="item" data-value="la"><i class="la flag"></i>Laos</div>
|
||||
<div class="item" data-value="lv"><i class="lv flag"></i>Latvia</div>
|
||||
<div class="item" data-value="lb"><i class="lb flag"></i>Lebanon</div>
|
||||
<div class="item" data-value="ls"><i class="ls flag"></i>Lesotho</div>
|
||||
<div class="item" data-value="lr"><i class="lr flag"></i>Liberia</div>
|
||||
<div class="item" data-value="ly"><i class="ly flag"></i>Libya</div>
|
||||
<div class="item" data-value="li"><i class="li flag"></i>Liechtenstein</div>
|
||||
<div class="item" data-value="lt"><i class="lt flag"></i>Lithuania</div>
|
||||
<div class="item" data-value="lu"><i class="lu flag"></i>Luxembourg</div>
|
||||
<div class="item" data-value="mo"><i class="mo flag"></i>Macau</div>
|
||||
<div class="item" data-value="mk"><i class="mk flag"></i>Macedonia</div>
|
||||
<div class="item" data-value="mg"><i class="mg flag"></i>Madagascar</div>
|
||||
<div class="item" data-value="mw"><i class="mw flag"></i>Malawi</div>
|
||||
<div class="item" data-value="my"><i class="my flag"></i>Malaysia</div>
|
||||
<div class="item" data-value="mv"><i class="mv flag"></i>Maldives</div>
|
||||
<div class="item" data-value="ml"><i class="ml flag"></i>Mali</div>
|
||||
<div class="item" data-value="mt"><i class="mt flag"></i>Malta</div>
|
||||
<div class="item" data-value="mh"><i class="mh flag"></i>Marshall Islands</div>
|
||||
<div class="item" data-value="mq"><i class="mq flag"></i>Martinique</div>
|
||||
<div class="item" data-value="mr"><i class="mr flag"></i>Mauritania</div>
|
||||
<div class="item" data-value="mu"><i class="mu flag"></i>Mauritius</div>
|
||||
<div class="item" data-value="yt"><i class="yt flag"></i>Mayotte</div>
|
||||
<div class="item" data-value="mx"><i class="mx flag"></i>Mexico</div>
|
||||
<div class="item" data-value="fm"><i class="fm flag"></i>Micronesia</div>
|
||||
<div class="item" data-value="md"><i class="md flag"></i>Moldova</div>
|
||||
<div class="item" data-value="mc"><i class="mc flag"></i>Monaco</div>
|
||||
<div class="item" data-value="mn"><i class="mn flag"></i>Mongolia</div>
|
||||
<div class="item" data-value="me"><i class="me flag"></i>Montenegro</div>
|
||||
<div class="item" data-value="ms"><i class="ms flag"></i>Montserrat</div>
|
||||
<div class="item" data-value="ma"><i class="ma flag"></i>Morocco</div>
|
||||
<div class="item" data-value="mz"><i class="mz flag"></i>Mozambique</div>
|
||||
<div class="item" data-value="na"><i class="na flag"></i>Namibia</div>
|
||||
<div class="item" data-value="nr"><i class="nr flag"></i>Nauru</div>
|
||||
<div class="item" data-value="np"><i class="np flag"></i>Nepal</div>
|
||||
<div class="item" data-value="an"><i class="an flag"></i>Netherlands Antilles</div>
|
||||
<div class="item" data-value="nl"><i class="nl flag"></i>Netherlands</div>
|
||||
<div class="item" data-value="nc"><i class="nc flag"></i>New Caledonia</div>
|
||||
<div class="item" data-value="pg"><i class="pg flag"></i>New Guinea</div>
|
||||
<div class="item" data-value="nz"><i class="nz flag"></i>New Zealand</div>
|
||||
<div class="item" data-value="ni"><i class="ni flag"></i>Nicaragua</div>
|
||||
<div class="item" data-value="ne"><i class="ne flag"></i>Niger</div>
|
||||
<div class="item" data-value="ng"><i class="ng flag"></i>Nigeria</div>
|
||||
<div class="item" data-value="nu"><i class="nu flag"></i>Niue</div>
|
||||
<div class="item" data-value="nf"><i class="nf flag"></i>Norfolk Island</div>
|
||||
<div class="item" data-value="kp"><i class="kp flag"></i>North Korea</div>
|
||||
<div class="item" data-value="mp"><i class="mp flag"></i>Northern Mariana Islands</div>
|
||||
<div class="item" data-value="no"><i class="no flag"></i>Norway</div>
|
||||
<div class="item" data-value="om"><i class="om flag"></i>Oman</div>
|
||||
<div class="item" data-value="pk"><i class="pk flag"></i>Pakistan</div>
|
||||
<div class="item" data-value="pw"><i class="pw flag"></i>Palau</div>
|
||||
<div class="item" data-value="ps"><i class="ps flag"></i>Palestine</div>
|
||||
<div class="item" data-value="pa"><i class="pa flag"></i>Panama</div>
|
||||
<div class="item" data-value="py"><i class="py flag"></i>Paraguay</div>
|
||||
<div class="item" data-value="pe"><i class="pe flag"></i>Peru</div>
|
||||
<div class="item" data-value="ph"><i class="ph flag"></i>Philippines</div>
|
||||
<div class="item" data-value="pn"><i class="pn flag"></i>Pitcairn Islands</div>
|
||||
<div class="item" data-value="pl"><i class="pl flag"></i>Poland</div>
|
||||
<div class="item" data-value="pt"><i class="pt flag"></i>Portugal</div>
|
||||
<div class="item" data-value="pr"><i class="pr flag"></i>Puerto Rico</div>
|
||||
<div class="item" data-value="qa"><i class="qa flag"></i>Qatar</div>
|
||||
<div class="item" data-value="re"><i class="re flag"></i>Reunion</div>
|
||||
<div class="item" data-value="ro"><i class="ro flag"></i>Romania</div>
|
||||
<div class="item" data-value="ru"><i class="ru flag"></i>Russia</div>
|
||||
<div class="item" data-value="rw"><i class="rw flag"></i>Rwanda</div>
|
||||
<div class="item" data-value="sh"><i class="sh flag"></i>Saint Helena</div>
|
||||
<div class="item" data-value="kn"><i class="kn flag"></i>Saint Kitts and Nevis</div>
|
||||
<div class="item" data-value="lc"><i class="lc flag"></i>Saint Lucia</div>
|
||||
<div class="item" data-value="pm"><i class="pm flag"></i>Saint Pierre</div>
|
||||
<div class="item" data-value="vc"><i class="vc flag"></i>Saint Vincent</div>
|
||||
<div class="item" data-value="ws"><i class="ws flag"></i>Samoa</div>
|
||||
<div class="item" data-value="sm"><i class="sm flag"></i>San Marino</div>
|
||||
<div class="item" data-value="gs"><i class="gs flag"></i>Sandwich Islands</div>
|
||||
<div class="item" data-value="st"><i class="st flag"></i>Sao Tome</div>
|
||||
<div class="item" data-value="sa"><i class="sa flag"></i>Saudi Arabia</div>
|
||||
<div class="item" data-value="sn"><i class="sn flag"></i>Senegal</div>
|
||||
<div class="item" data-value="cs"><i class="cs flag"></i>Serbia</div>
|
||||
<div class="item" data-value="rs"><i class="rs flag"></i>Serbia</div>
|
||||
<div class="item" data-value="sc"><i class="sc flag"></i>Seychelles</div>
|
||||
<div class="item" data-value="sl"><i class="sl flag"></i>Sierra Leone</div>
|
||||
<div class="item" data-value="sg"><i class="sg flag"></i>Singapore</div>
|
||||
<div class="item" data-value="sk"><i class="sk flag"></i>Slovakia</div>
|
||||
<div class="item" data-value="si"><i class="si flag"></i>Slovenia</div>
|
||||
<div class="item" data-value="sb"><i class="sb flag"></i>Solomon Islands</div>
|
||||
<div class="item" data-value="so"><i class="so flag"></i>Somalia</div>
|
||||
<div class="item" data-value="za"><i class="za flag"></i>South Africa</div>
|
||||
<div class="item" data-value="kr"><i class="kr flag"></i>South Korea</div>
|
||||
<div class="item" data-value="es"><i class="es flag"></i>Spain</div>
|
||||
<div class="item" data-value="lk"><i class="lk flag"></i>Sri Lanka</div>
|
||||
<div class="item" data-value="sd"><i class="sd flag"></i>Sudan</div>
|
||||
<div class="item" data-value="sr"><i class="sr flag"></i>Suriname</div>
|
||||
<div class="item" data-value="sj"><i class="sj flag"></i>Svalbard</div>
|
||||
<div class="item" data-value="sz"><i class="sz flag"></i>Swaziland</div>
|
||||
<div class="item" data-value="se"><i class="se flag"></i>Sweden</div>
|
||||
<div class="item" data-value="ch"><i class="ch flag"></i>Switzerland</div>
|
||||
<div class="item" data-value="sy"><i class="sy flag"></i>Syria</div>
|
||||
<div class="item" data-value="tw"><i class="tw flag"></i>Taiwan</div>
|
||||
<div class="item" data-value="tj"><i class="tj flag"></i>Tajikistan</div>
|
||||
<div class="item" data-value="tz"><i class="tz flag"></i>Tanzania</div>
|
||||
<div class="item" data-value="th"><i class="th flag"></i>Thailand</div>
|
||||
<div class="item" data-value="tl"><i class="tl flag"></i>Timorleste</div>
|
||||
<div class="item" data-value="tg"><i class="tg flag"></i>Togo</div>
|
||||
<div class="item" data-value="tk"><i class="tk flag"></i>Tokelau</div>
|
||||
<div class="item" data-value="to"><i class="to flag"></i>Tonga</div>
|
||||
<div class="item" data-value="tt"><i class="tt flag"></i>Trinidad</div>
|
||||
<div class="item" data-value="tn"><i class="tn flag"></i>Tunisia</div>
|
||||
<div class="item" data-value="tr"><i class="tr flag"></i>Turkey</div>
|
||||
<div class="item" data-value="tm"><i class="tm flag"></i>Turkmenistan</div>
|
||||
<div class="item" data-value="tv"><i class="tv flag"></i>Tuvalu</div>
|
||||
<div class="item" data-value="ug"><i class="ug flag"></i>Uganda</div>
|
||||
<div class="item" data-value="ua"><i class="ua flag"></i>Ukraine</div>
|
||||
<div class="item" data-value="ae"><i class="ae flag"></i>United Arab Emirates</div>
|
||||
<div class="item" data-value="us"><i class="us flag"></i>United States</div>
|
||||
<div class="item" data-value="uy"><i class="uy flag"></i>Uruguay</div>
|
||||
<div class="item" data-value="um"><i class="um flag"></i>Us Minor Islands</div>
|
||||
<div class="item" data-value="vi"><i class="vi flag"></i>Us Virgin Islands</div>
|
||||
<div class="item" data-value="uz"><i class="uz flag"></i>Uzbekistan</div>
|
||||
<div class="item" data-value="vu"><i class="vu flag"></i>Vanuatu</div>
|
||||
<div class="item" data-value="va"><i class="va flag"></i>Vatican City</div>
|
||||
<div class="item" data-value="ve"><i class="ve flag"></i>Venezuela</div>
|
||||
<div class="item" data-value="vn"><i class="vn flag"></i>Vietnam</div>
|
||||
<div class="item" data-value="wf"><i class="wf flag"></i>Wallis and Futuna</div>
|
||||
<div class="item" data-value="eh"><i class="eh flag"></i>Western Sahara</div>
|
||||
<div class="item" data-value="ye"><i class="ye flag"></i>Yemen</div>
|
||||
<div class="item" data-value="zm"><i class="zm flag"></i>Zambia</div>
|
||||
<div class="item" data-value="zw"><i class="zw flag"></i>Zimbabwe</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="ui basic red button" id="ban-btn" onclick="addCountryToBlacklist();"><i class="ui red ban icon"></i> Blacklist Country</button>
|
||||
</div>
|
||||
<table class="ui unstackable basic celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ISO Code</th>
|
||||
<th>Remove</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="banned-list">
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="ui divider"></div>
|
||||
|
||||
<h4>IP Blacklist</h4>
|
||||
<p>Black a certain IP or IP range</p>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>IP Address</label>
|
||||
<input id="ipAddressInput" type="text" placeholder="IP Address">
|
||||
</div>
|
||||
<button id="addIpButton" onclick="addIpBlacklist();" class="ui basic red icon button">
|
||||
<i class="ban icon"></i> Blacklist IP
|
||||
</button>
|
||||
</div>
|
||||
<div class="ui message">
|
||||
<i class="ui info circle icon"></i> IP Address support the following formats
|
||||
<div class="ui bulleted list">
|
||||
<div class="item">Fixed IP Address (e.g. 192.128.4.100)</div>
|
||||
<div class="item">IP Wildcard (e.g. 172.164.*.*)</div>
|
||||
<div class="item">CIDR String (e.g. 128.32.0.1/16)</div>
|
||||
</div>
|
||||
</div>
|
||||
<table class="ui unstackable basic celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>IP Address</th>
|
||||
<th>Remove</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="blacklistIpTable">
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<script>
|
||||
$(".dropdown").dropdown();
|
||||
|
||||
function filterCountries(codesToShow) {
|
||||
// get all items in the dropdown
|
||||
const items = document.querySelectorAll('.ui.fluid.search.selection.dropdown .menu .item');
|
||||
// loop through all items
|
||||
items.forEach(item => {
|
||||
// get the value of the item (i.e. the country code)
|
||||
const code = item.dataset.value;
|
||||
// if the code is in the array of codes to show, show the item
|
||||
if (codesToShow.includes(code)) {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
// otherwise, hide the item
|
||||
else {
|
||||
item.style.display = 'block';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//Get the country name from the dropdown
|
||||
function getCountryName(countryCode) {
|
||||
return $('#countrySelector .item[data-value="' + countryCode.toLowerCase() + '"]').text().trim();
|
||||
}
|
||||
|
||||
function initBannedCountryList(){
|
||||
$.get("/api/blacklist/list?type=country", function(data) {
|
||||
let bannedListHtml = '';
|
||||
data.forEach((countryCode) => {
|
||||
bannedListHtml += `
|
||||
<tr>
|
||||
<td><i class="${countryCode} flag"></i> ${getCountryName(countryCode)} (${countryCode.toUpperCase()})</td>
|
||||
<td><button class="ui red basic mini icon button" onclick="removeFromBannedList('${countryCode}')"><i class="trash icon"></i></button></td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
$('#banned-list').html(bannedListHtml);
|
||||
filterCountries(data);
|
||||
if (data.length === 0) {
|
||||
$('#banned-list').append(`
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<i class="green check circle icon"></i>
|
||||
There are no blacklisted countries
|
||||
</td>
|
||||
</tr>
|
||||
`);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
initBannedCountryList();
|
||||
|
||||
function addCountryToBlacklist() {
|
||||
var countryCode = $("#countrySelector").dropdown("get value").toLowerCase();
|
||||
$('#countrySelector').dropdown('clear');
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/api/blacklist/country/add",
|
||||
data: { cc: countryCode },
|
||||
success: function(response) {
|
||||
if (response.error != undefined){
|
||||
alert(response.error);
|
||||
}
|
||||
initBannedCountryList();
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
// handle error response
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeFromBannedList(countryCode){
|
||||
if (confirm("Confirm removing " + getCountryName(countryCode) + " from blacklist?")){
|
||||
countryCode = countryCode.toLowerCase();
|
||||
$.ajax({
|
||||
url: "/api/blacklist/country/remove",
|
||||
method: "POST",
|
||||
data: { cc: countryCode },
|
||||
success: function(response) {
|
||||
if (response.error != undefined){
|
||||
alert(response.error);
|
||||
}
|
||||
initBannedCountryList();
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error("Error removing country from blacklist: " + error);
|
||||
// Handle error response
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function initIpBanTable(){
|
||||
$.get('/api/blacklist/list?type=ip', function(data) {
|
||||
$('#blacklistIpTable').html("");
|
||||
if (data.length === 0) {
|
||||
$('#blacklistIpTable').append(`
|
||||
<tr>
|
||||
<td colspan="2"><i class="green check circle icon"></i>There are no blacklisted IP addresses</td>
|
||||
</tr>
|
||||
`);
|
||||
} else {
|
||||
$.each(data, function(index, ip) {
|
||||
let icon = "globe icon";
|
||||
if (isLAN(ip)){
|
||||
icon = "desktop icon";
|
||||
}else if (isHomeAddr(ip)){
|
||||
icon = "home icon";
|
||||
}
|
||||
$('#blacklistIpTable').append(`
|
||||
<tr>
|
||||
<td><i class="${icon}"></i> ${ip}</td>
|
||||
<td><button class="ui icon basic mini red button" onclick="removeIpBlacklist('${ip}');"><i class="trash alternate icon"></i></button></td>
|
||||
</tr>
|
||||
`);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
initIpBanTable();
|
||||
|
||||
//Check if a input is a valid IP address, wildcard of a IP address or a CIDR string
|
||||
function isValidIpFilter(input) {
|
||||
// Check if input is a valid IP address
|
||||
const isValidIp = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])$/.test(input);
|
||||
|
||||
if (isValidIp) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if input is a wildcard IP address
|
||||
const isValidWildcardIp = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])\.([01]?[0-9]?[0-9]|\*|\*\/[0-9]|[01]?[0-9]?[0-9]-[01]?[0-9]?[0-9]|\[\d+,\d+\])$/.test(input);
|
||||
|
||||
if (isValidWildcardIp) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if input is a valid CIDR address string
|
||||
const isValidCidr = /^([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\.([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\/([0-9]|[1-2][0-9]|3[0-2])$/.test(input);
|
||||
|
||||
if (isValidCidr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Input is not a valid IP address, wildcard IP address, or CIDR address string
|
||||
return false;
|
||||
}
|
||||
|
||||
$("#ipAddressInput").on("input", function() {
|
||||
$(this).val($(this).val().trim());
|
||||
var ipAddress = $(this).val();
|
||||
if (!isValidIpFilter(ipAddress)) {
|
||||
$(this).parent().addClass("error");
|
||||
} else {
|
||||
$(this).parent().removeClass("error");
|
||||
}
|
||||
});
|
||||
|
||||
function isLAN(ipAddress) {
|
||||
function ip2long(ipAddress) {
|
||||
// Convert the IP address to a 32-bit integer
|
||||
const parts = ipAddress.split(".");
|
||||
return (
|
||||
(parseInt(parts[0]) << 24) |
|
||||
(parseInt(parts[1]) << 16) |
|
||||
(parseInt(parts[2]) << 8) |
|
||||
parseInt(parts[3])
|
||||
);
|
||||
}
|
||||
|
||||
// Define the LAN IP address ranges
|
||||
const LAN_RANGES = [
|
||||
{ start: "10.0.0.0", end: "10.255.255.255" },
|
||||
{ start: "172.16.0.0", end: "172.31.255.255" },
|
||||
{ start: "192.168.0.0", end: "192.168.255.255" }
|
||||
];
|
||||
|
||||
// Check if the IP address is within any of the LAN ranges
|
||||
for (let i = 0; i < LAN_RANGES.length; i++) {
|
||||
const rangeStart = ip2long(LAN_RANGES[i].start);
|
||||
const rangeEnd = ip2long(LAN_RANGES[i].end);
|
||||
const ipAddressLong = ip2long(ipAddress);
|
||||
|
||||
if (ipAddressLong >= rangeStart && ipAddressLong <= rangeEnd) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function isHomeAddr(ipAddress) {
|
||||
const specialIpAddresses = ['0.0.0.0', '127.0.0.1', '::1'];
|
||||
return specialIpAddresses.includes(ipAddress);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function addIpBlacklist(){
|
||||
let targetIp = $("#ipAddressInput").val().trim();
|
||||
if (targetIp == ""){
|
||||
alert("IP address is empty")
|
||||
return
|
||||
}
|
||||
if (!isValidIpFilter(targetIp)){
|
||||
if (!confirm("This doesn't seems like a valid IP address. Continue anyway?")){
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: "/api/blacklist/ip/add",
|
||||
type: "POST",
|
||||
data: {ip: targetIp.toLowerCase()},
|
||||
success: function(response) {
|
||||
if (response.error !== undefined) {
|
||||
alert(response.error);
|
||||
} else {
|
||||
initIpBanTable();
|
||||
}
|
||||
|
||||
$("#ipAddressInput").val("");
|
||||
$("#ipAddressInput").parent().remvoeClass("error");
|
||||
},
|
||||
error: function() {
|
||||
alert("Failed to add IP address to blacklist.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeIpBlacklist(ipaddr){
|
||||
if (confirm("Confirm remove blacklist for " + ipaddr + " ?")){
|
||||
$.ajax({
|
||||
url: "/api/blacklist/ip/remove",
|
||||
type: "POST",
|
||||
data: {ip: ipaddr.toLowerCase()},
|
||||
success: function(response) {
|
||||
if (response.error !== undefined) {
|
||||
alert(response.error);
|
||||
} else {
|
||||
initIpBanTable();
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert("Failed to remove IP address from blacklist.");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//function to check for blacklist enable
|
||||
function enableBlacklist() {
|
||||
var isChecked = $('#enableBlacklist').is(':checked');
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/api/blacklist/enable',
|
||||
data: { enable: isChecked },
|
||||
success: function(data){
|
||||
$("#toggleSucc").stop().finish().fadeIn("fast").delay(3000).fadeOut("fast");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initBlacklistEnableState(){
|
||||
$.get('/api/blacklist/enable', function(data){
|
||||
if (data == true){
|
||||
$('#enableBlacklist').parent().checkbox("set checked");
|
||||
}
|
||||
|
||||
//Register on change event
|
||||
$("#enableBlacklist").on("change", function(){
|
||||
enableBlacklist();
|
||||
})
|
||||
});
|
||||
}
|
||||
initBlacklistEnableState();
|
||||
|
||||
|
||||
|
||||
</script>
|
279
src/web/components/cert.html
Normal file
@ -0,0 +1,279 @@
|
||||
|
||||
<h3><i class="ui lock icon"></i> TLS / SSL Certificates</h3>
|
||||
<p>Setup TLS cert for different domains of your reverse proxy server names</p>
|
||||
<div class="ui divider"></div>
|
||||
<h4>Default Certificates</h4>
|
||||
<small>When there are no matching certificate for the requested server name, reverse proxy router will always fallback to this one.<br>Note that you need both of them uploaded for it to fallback properly</small></p>
|
||||
<table class="ui very basic celled table">
|
||||
<thead>
|
||||
<tr><th>Key Type</th>
|
||||
<th>Exists</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><i class="globe icon"></i> Default Public Key</td>
|
||||
<td id="pubkeyExists"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><i class="lock icon"></i> Default Private Key</td>
|
||||
<td id="prikeyExists"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button class="ui button" onclick="uploadPublicKey();"><i class="globe icon"></i> Upload Public Key</button>
|
||||
<button class="ui black button" onclick="uploadPrivateKey();"><i class="lock icon"></i> Upload Private Key</button>
|
||||
<div class="ui divider"></div>
|
||||
<h4>Sub-domain Certificates</h4>
|
||||
<p>Provide certificates for multiple domains reverse proxy</p>
|
||||
<div class="ui fluid form">
|
||||
<div class="three fields">
|
||||
<div class="field">
|
||||
<label>Server Name (Domain)</label>
|
||||
<input type="text" id="certdomain" placeholder="example.com / blog.example.com">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Public Key</label>
|
||||
<input type="file" id="pubkeySelector" onchange="handleFileSelect(event, 'pub')">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Private Key</label>
|
||||
<input type="file" id="prikeySelector" onchange="handleFileSelect(event, 'pri')">
|
||||
</div>
|
||||
</div>
|
||||
<button class="ui teal button" onclick="handleDomainUploadByKeypress();"><i class="ui upload icon"></i> Upload</button>
|
||||
</div>
|
||||
<div id="certUploadSuccMsg" class="ui green message" style="display:none;">
|
||||
<i class="ui checkmark icon"></i> Certificate for domain <span id="certUploadingDomain"></span> uploaded.
|
||||
</div>
|
||||
<br>
|
||||
<div>
|
||||
<table class="ui sortable unstackable celled table">
|
||||
<thead>
|
||||
<tr><th>Domain</th>
|
||||
<th>Last Update</th>
|
||||
<th class="no-sort">Remove</th>
|
||||
</tr></thead>
|
||||
<tbody id="certifiedDomainList">
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<button class="ui green basic button" onclick="initManagedDomainCertificateList();"><i class="refresh icon"></i> Refresh List</button>
|
||||
</div>
|
||||
<div class="ui message">
|
||||
<h4><i class="info circle icon"></i> Sub-domain Certificates</h4>
|
||||
If you have 3rd or even 4th level subdomains like <code>blog.example.com</code> or <code>en.blog.example.com</code> ,
|
||||
depending on your certificates coverage, you might need to setup them one by one (i.e. having two seperate certificate for <code>a.example.com</code> and <code>b.example.com</code>).<br>
|
||||
If you have a wildcard certificate that covers <code>*.example.com</code>, you can just enter <code>example.com</code> as server name in the form below to add a certificate.
|
||||
</div>
|
||||
<script>
|
||||
var uploadPendingPublicKey = undefined;
|
||||
var uploadPendingPrivateKey = undefined;
|
||||
|
||||
//Delete the certificate by its domain
|
||||
function deleteCertificate(domain){
|
||||
if (confirm("Confirm delete certificate for " + domain + " ?")){
|
||||
$.ajax({
|
||||
url: "/api/cert/delete",
|
||||
method: "POST",
|
||||
data: {domain: domain},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
alert(data.error);
|
||||
}else{
|
||||
initManagedDomainCertificateList();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//List the stored certificates
|
||||
function initManagedDomainCertificateList(){
|
||||
$("#certifiedDomainList").html("");
|
||||
$.get("/api/cert/list?date=true", function(data){
|
||||
if (data.error != undefined){
|
||||
alert(data.error);
|
||||
}else{
|
||||
data.forEach(entry => {
|
||||
$("#certifiedDomainList").append(`<tr>
|
||||
<td>${entry.Domain}</td>
|
||||
<td>${entry.LastModifiedDate}</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>`);
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
initManagedDomainCertificateList();
|
||||
|
||||
function handleDomainUploadByKeypress(){
|
||||
handleDomainKeysUpload(function(){
|
||||
$("#certUploadingDomain").text($("#certdomain").val().trim());
|
||||
//After uploaded, reset the file selector
|
||||
document.getElementById('pubkeySelector').value = '';
|
||||
document.getElementById('prikeySelector').value = '';
|
||||
document.getElementById('certdomain').value = '';
|
||||
|
||||
uploadPendingPublicKey = undefined;
|
||||
uploadPendingPrivateKey = undefined;
|
||||
|
||||
//Show succ
|
||||
$("#certUploadSuccMsg").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
|
||||
initManagedDomainCertificateList();
|
||||
});
|
||||
}
|
||||
//Handle domain keys upload
|
||||
function handleDomainKeysUpload(callback=undefined){
|
||||
let domain = $("#certdomain").val();
|
||||
if (domain.trim() == ""){
|
||||
alert("Missing domain.");
|
||||
return;
|
||||
}
|
||||
if (uploadPendingPublicKey && uploadPendingPrivateKey && typeof uploadPendingPublicKey === 'object' && typeof uploadPendingPrivateKey === 'object') {
|
||||
const publicKeyForm = new FormData();
|
||||
publicKeyForm.append('file', uploadPendingPublicKey, 'publicKey');
|
||||
|
||||
const privateKeyForm = new FormData();
|
||||
privateKeyForm.append('file', uploadPendingPrivateKey, 'privateKey');
|
||||
|
||||
const publicKeyRequest = new XMLHttpRequest();
|
||||
publicKeyRequest.open('POST', '/api/cert/upload?ktype=pub&domain=' + domain);
|
||||
publicKeyRequest.onreadystatechange = function() {
|
||||
if (publicKeyRequest.readyState === XMLHttpRequest.DONE) {
|
||||
if (publicKeyRequest.status !== 200) {
|
||||
alert('Error uploading public key: ' + publicKeyRequest.statusText);
|
||||
}
|
||||
|
||||
if (callback != undefined){
|
||||
callback();
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
publicKeyRequest.send(publicKeyForm);
|
||||
|
||||
const privateKeyRequest = new XMLHttpRequest();
|
||||
privateKeyRequest.open('POST', '/api/cert/upload?ktype=pri&domain=' + domain);
|
||||
privateKeyRequest.onreadystatechange = function() {
|
||||
if (privateKeyRequest.readyState === XMLHttpRequest.DONE) {
|
||||
if (privateKeyRequest.status !== 200) {
|
||||
alert('Error uploading private key: ' + privateKeyRequest.statusText);
|
||||
}
|
||||
if (callback != undefined){
|
||||
callback();
|
||||
}
|
||||
}
|
||||
};
|
||||
privateKeyRequest.send(privateKeyForm);
|
||||
} else {
|
||||
alert('One or both of the files is missing or not a file object');
|
||||
}
|
||||
}
|
||||
|
||||
//Handlers for selecting domain based key pairs
|
||||
//ktype = {"pub" / "pri"}
|
||||
function handleFileSelect(event, ktype="pub") {
|
||||
const file = event.target.files[0];
|
||||
//const fileNameInput = document.getElementById('selected-file-name');
|
||||
if (ktype == "pub"){
|
||||
uploadPendingPublicKey = file;
|
||||
}else if (ktype == "pri"){
|
||||
uploadPendingPrivateKey = file;
|
||||
}
|
||||
|
||||
|
||||
//fileNameInput.value = file.name;
|
||||
}
|
||||
|
||||
//Check if the default keypairs exists
|
||||
function initDefaultKeypairCheck(){
|
||||
$.get("/api/cert/checkDefault", function(data){
|
||||
let tick = `<i class="ui green checkmark icon"></i>`;
|
||||
let cross = `<i class="ui red times icon"></i>`;
|
||||
$("#pubkeyExists").html(data.DefaultPubExists?tick:cross);
|
||||
$("#prikeyExists").html(data.DefaultPriExists?tick:cross);
|
||||
});
|
||||
}
|
||||
initDefaultKeypairCheck();
|
||||
|
||||
function uploadPrivateKey(){
|
||||
// create file input element
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
|
||||
// add change listener to file input
|
||||
input.addEventListener('change', () => {
|
||||
// create form data object
|
||||
const formData = new FormData();
|
||||
|
||||
// add selected file to form data
|
||||
formData.append('file', input.files[0]);
|
||||
|
||||
// send form data to server
|
||||
fetch('/api/cert/upload?ktype=pri', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
initDefaultKeypairCheck();
|
||||
if (response.ok) {
|
||||
alert('File upload successful!');
|
||||
} else {
|
||||
response.text().then(text => {
|
||||
alert(text);
|
||||
});
|
||||
//console.log(response.text());
|
||||
//alert('File upload failed!');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('An error occurred while uploading the file.');
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
|
||||
// click file input to open file selector
|
||||
input.click();
|
||||
}
|
||||
|
||||
function uploadPublicKey() {
|
||||
// create file input element
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
|
||||
// add change listener to file input
|
||||
input.addEventListener('change', () => {
|
||||
// create form data object
|
||||
const formData = new FormData();
|
||||
|
||||
// add selected file to form data
|
||||
formData.append('file', input.files[0]);
|
||||
|
||||
// send form data to server
|
||||
fetch('/api/cert/upload?ktype=pub', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
alert('File upload successful!');
|
||||
initDefaultKeypairCheck();
|
||||
} else {
|
||||
response.text().then(text => {
|
||||
alert(text);
|
||||
});
|
||||
//console.log(response.text());
|
||||
//alert('File upload failed!');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('An error occurred while uploading the file.');
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
|
||||
// click file input to open file selector
|
||||
input.click();
|
||||
}
|
||||
</script>
|
160
src/web/components/redirection.html
Normal file
@ -0,0 +1,160 @@
|
||||
<h3><i class="level up alternate icon"></i> Redirection Rules</h3>
|
||||
<p>Add exception case for redirecting any matching URLs</p>
|
||||
<div class="ui basic segment">
|
||||
<div style="width: 100%; overflow-x: auto;">
|
||||
<table class="ui sortable unstackable celled table" >
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Redirection URL</th>
|
||||
<th class="no-sort"></th>
|
||||
<th>Destination URL</th>
|
||||
<th class="no-sort">Copy Pathname</th>
|
||||
<th class="no-sort">Status Code</th>
|
||||
<th class="no-sort">Remove</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="redirectionRuleList">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="ui green message" id="delRuleSucc" style="display:none;">
|
||||
<i class="ui green checkmark icon"></i> Redirection Rule Deleted
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<p>Add path redirection to your domain</p>
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>Redirection URL</label>
|
||||
<input type="text" id="rurl" name="redirection-url" placeholder="Redirection URL">
|
||||
<small><i class="ui circle info icon"></i> Any matching prefix of the request URL will be redirected to the destination URL, e.g. redirect.example.com</small>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Destination URL</label>
|
||||
<input type="text" name="destination-url" placeholder="Destination URL">
|
||||
<small><i class="ui circle info icon"></i> The target URL request being redirected to, e.g. dest.example.com/mysite</small>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="forward-childpath" tabindex="0" class="hidden" checked>
|
||||
<label>Forward Pathname</label>
|
||||
</div>
|
||||
<div class="ui message">
|
||||
<p>Append the current pathname after the redirect destination</p>
|
||||
<i class="check square outline icon"></i> old.example.com<b>/blog?post=13</b> <i class="long arrow alternate right icon" style="margin-left: 1em;"></i> new.example.com<b>/blog?post=13</b> <br>
|
||||
<i class="square outline icon"></i> Disabled old.example.com<b>/blog?post=13</b> <i class="long arrow alternate right icon" style="margin-left: 1em;"></i> new.example.com
|
||||
</div>
|
||||
</div>
|
||||
<div class="grouped fields">
|
||||
<label>Redirection Status Code</label>
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
<input type="radio" name="redirect-type" value="307" checked>
|
||||
<label>Temporary Redirect <br><small>Status Code: 307</small></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui radio checkbox">
|
||||
<input type="radio" name="redirect-type" value="301">
|
||||
<label>Moved Permanently <br><small>Status Code: 301</small></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="ui teal button" onclick="addRules();"><i class="ui plus icon"></i> Add Redirection Rule</button>
|
||||
<div class="ui green message" id="ruleAddSucc" style="display:none;">
|
||||
<i class="ui green checkmark icon"></i> Redirection Rules Added
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$(".checkbox").checkbox();
|
||||
|
||||
function resetForm() {
|
||||
document.getElementById("rurl").value = "";
|
||||
document.getElementsByName("destination-url")[0].value = "";
|
||||
document.getElementsByName("forward-childpath")[0].checked = true;
|
||||
}
|
||||
|
||||
function addRules(){
|
||||
let redirectUrl = document.querySelector('input[name="redirection-url"]').value;
|
||||
let destUrl = document.querySelector('input[name="destination-url"]').value;
|
||||
let forwardChildpath = document.querySelector('input[name="forward-childpath"]').checked;
|
||||
let redirectType = document.querySelector('input[name="redirect-type"]:checked').value;
|
||||
|
||||
$.ajax({
|
||||
url: "/api/redirect/add",
|
||||
method: "POST",
|
||||
data: {
|
||||
redirectUrl: redirectUrl,
|
||||
destUrl: destUrl,
|
||||
forwardChildpath: forwardChildpath,
|
||||
redirectType: parseInt(redirectType),
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
alert(data.error);
|
||||
}else{
|
||||
$("#ruleAddSucc").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
|
||||
}
|
||||
initRedirectionRuleList();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteRule(obj){
|
||||
let targetURL = $(obj).attr("rurl");
|
||||
targetURL = JSON.parse(decodeURIComponent(targetURL));
|
||||
if (confirm("Confirm remove redirection from " + targetURL + " ?")){
|
||||
$.ajax({
|
||||
url: "/api/redirect/delete",
|
||||
method: "POST",
|
||||
data: {
|
||||
redirectUrl: targetURL,
|
||||
},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
alert(data.error);
|
||||
}else{
|
||||
$("#delRuleSucc").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
|
||||
}
|
||||
initRedirectionRuleList();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function initRedirectionRuleList(){
|
||||
$("#redirectionRuleList").html("");
|
||||
$.get("/api/redirect/list", function(data){
|
||||
data.forEach(function(entry){
|
||||
$("#redirectionRuleList").append(`<tr>
|
||||
<td>${entry.RedirectURL} </td>
|
||||
<td style="text-align: center;"><i class="blue arrow right icon"></i></td>
|
||||
<td>${entry.TargetURL}</td>
|
||||
<td>${entry.ForwardChildpath?"<i class='ui green checkmark icon'></i>":"<i class='ui red remove icon'></i>"}</td>
|
||||
<td>${entry.StatusCode==307?"Temporary Redirect (307)":"Moved Permanently (301)"}</td>
|
||||
<td><button onclick="deleteRule(this);" rurl="${encodeURIComponent(JSON.stringify(entry.RedirectURL))}" title="Delete redirection rule" class="ui mini red icon basic button"><i class="trash icon"></i></button></td>
|
||||
</tr>`);
|
||||
});
|
||||
|
||||
if (data.length == 0){
|
||||
$("#redirectionRuleList").append(`<tr colspan="4"><td><i class="checkmark icon"></i> No redirection rule</td></tr>`);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
initRedirectionRuleList();
|
||||
|
||||
$("#rurl").on('change', (event) => {
|
||||
const value = event.target.value.trim().replace(/^(https?:\/\/)/, '');
|
||||
event.target.value = value;
|
||||
});
|
||||
|
||||
</script>
|
61
src/web/components/rproot.html
Normal file
@ -0,0 +1,61 @@
|
||||
<h3><i class="ui home icon"></i> Set Proxy Root</h3>
|
||||
<p>For all routing not found in the proxy rule, will be redirected to the proxy root server.</p>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>Proxy Root</label>
|
||||
<input type="text" id="proxyRoot">
|
||||
<small>E.g. localhost:8080</small>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="rootReqTLS">
|
||||
<label>Root require TLS Connection <br><small>(i.e. Your proxy target starts with https://)</small></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<button class="ui teal button" onclick="setProxyRoot()"><i class="home icon" ></i> Set Proxy Root</button>
|
||||
<div class="ui green message" id="ProxyRootUpdate" style="display:none">
|
||||
<i class="ui checkmark icon"></i> Proxy Root Updated
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function initRootInfo(){
|
||||
$.get("/api/proxy/list?type=root", function(data){
|
||||
if (data == null){
|
||||
|
||||
}else{
|
||||
$("#proxyRoot").val(data.Domain);
|
||||
}
|
||||
});
|
||||
}
|
||||
initRootInfo();
|
||||
|
||||
function setProxyRoot(){
|
||||
var newpr = $("#proxyRoot").val();
|
||||
if (newpr.trim() == ""){
|
||||
$("#proxyRoot").parent().addClass('error');
|
||||
return
|
||||
}else{
|
||||
$("#proxyRoot").parent().removeClass('error');
|
||||
}
|
||||
|
||||
var rootReqTls = $("#rootReqTLS")[0].checked;
|
||||
|
||||
//Create the endpoint by calling add
|
||||
$.ajax({
|
||||
url: "/api/proxy/add",
|
||||
data: {"type": "root", tls: rootReqTls, ep: newpr},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
alert(data.error);
|
||||
}else{
|
||||
//OK
|
||||
initRootInfo();
|
||||
$("#ProxyRootUpdate").stop().slideDown('fast').delay(3000).slideUp('fast');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
</script>
|
100
src/web/components/rules.html
Normal file
@ -0,0 +1,100 @@
|
||||
<h3><i class="ui exchange icon"></i> New Proxy Endpoint</h3>
|
||||
<p>You can create a proxy endpoing by subdomain or virtual directories</p>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>Proxy Type</label>
|
||||
<div class="ui selection dropdown">
|
||||
<input type="hidden" id="ptype" value="subd">
|
||||
<i class="dropdown icon"></i>
|
||||
<div class="default text">Proxy Type</div>
|
||||
<div class="menu">
|
||||
<div class="item" data-value="subd">Sub-domain</div>
|
||||
<div class="item" data-value="vdir">Virtual Directory</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Subdomain Matching Keyword / Virtual Directory Name</label>
|
||||
<input type="text" id="rootname" placeholder="s1.mydomain.com">
|
||||
<div class="ui message">
|
||||
Example of subdomain matching keyword:<br>
|
||||
<code>s1.arozos.com</code> <br>(Any access starting with s1.arozos.com will be proxy to the IP address below)<br>
|
||||
Example of virtual directory name: <br>
|
||||
<code>/s1/home</code> <br>(Any access to {this_server}/s1/ will be proxy to the IP address below)
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>IP Address or Domain Name with port</label>
|
||||
<input type="text" id="proxyDomain">
|
||||
<small>E.g. 192.168.0.101:8000 or example.com</small>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="reqTls">
|
||||
<label>Proxy Target require TLS Connection <br><small>(i.e. Your proxy target starts with https://)</small></label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="ui teal button" onclick="newProxyEndpoint();">Create Proxy Endpoint</button>
|
||||
</div>
|
||||
<div class="ui green message" id="proxyaddSucc" style="display:none">
|
||||
<i class="ui checkmark icon"></i> Proxy Endpoint Added
|
||||
</div>
|
||||
|
||||
<script>
|
||||
//New Proxy Endpoint
|
||||
function newProxyEndpoint(){
|
||||
var type = $("#ptype").val();
|
||||
var rootname = $("#rootname").val();
|
||||
var proxyDomain = $("#proxyDomain").val();
|
||||
var useTLS = $("#reqTls")[0].checked;
|
||||
|
||||
if (rootname.trim() == ""){
|
||||
$("#rootname").parent().addClass("error");
|
||||
return
|
||||
}else{
|
||||
$("#rootname").parent().removeClass("error");
|
||||
}
|
||||
|
||||
if (proxyDomain.trim() == ""){
|
||||
$("#proxyDomain").parent().addClass("error");
|
||||
return
|
||||
}else{
|
||||
$("#proxyDomain").parent().removeClass("error");
|
||||
}
|
||||
|
||||
//Create the endpoint by calling add
|
||||
$.ajax({
|
||||
url: "/api/proxy/add",
|
||||
data: {type: type, rootname: rootname, tls: useTLS, ep: proxyDomain},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
alert(data.error);
|
||||
}else{
|
||||
//OK
|
||||
listVdirs();
|
||||
listSubd();
|
||||
$("#proxyaddSucc").stop().slideDown('fast').delay(3000).slideUp('fast');
|
||||
|
||||
//Clear old data
|
||||
$("#rootname").val("");
|
||||
$("#proxyDomain").val("");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
//Generic functions for delete rp endpoints
|
||||
function deleteEndpoint(ptype, epoint){
|
||||
if (confirm("Confirm remove proxy for :" + epoint + " (type: " + ptype + ")?")){
|
||||
$.ajax({
|
||||
url: "/api/proxy/del",
|
||||
data: {ep: epoint, ptype: ptype},
|
||||
success: function(){
|
||||
listVdirs();
|
||||
listSubd();
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
277
src/web/components/status.html
Normal file
@ -0,0 +1,277 @@
|
||||
<div class="ui stackable four column grid">
|
||||
<div class="column">
|
||||
<div id="serverstatus" class="ui green statustab inverted segment">
|
||||
<h4 class="ui header">
|
||||
<i class="power off icon"></i>
|
||||
<div class="content">
|
||||
<span id="statusTitle">Offline</span>
|
||||
<div style="color: white;" class="sub header" id="statusText">Reverse proxy server is offline</div>
|
||||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div id="connections" class="ui statustab summary segment">
|
||||
<h4 class="ui header">
|
||||
<i class="exchange icon"></i>
|
||||
<div class="content">
|
||||
<span id="summaryTotalCount"></span> <small>Req. Today</small>
|
||||
<div class="sub header" style="margin-top: 0.4em;">
|
||||
<i class="green circle check icon"></i> <span id="summarySuccCount"></span>
|
||||
/ <i class="red red exclamation circle icon"></i> <span id="summaryErrCount"></span>
|
||||
</div>
|
||||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div id="connections" class="ui yellow statustab inverted segment">
|
||||
<h4 class="ui header">
|
||||
<i class="exchange icon"></i>
|
||||
<div class="content">
|
||||
<span></span>
|
||||
<div class="sub header"></div>
|
||||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div id="connections" class="ui pink statustab inverted segment">
|
||||
<h4 class="ui header">
|
||||
<i class="exchange icon"></i>
|
||||
<div class="content">
|
||||
<span></span>
|
||||
<div class="sub header"></div>
|
||||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui divider"></div>
|
||||
<p>Inbound Port (Port to be proxied)</p>
|
||||
<div class="ui action fluid input">
|
||||
<input type="text" id="incomingPort" placeholder="Incoming Port" value="80">
|
||||
<button class="ui button" onclick="handlePortChange();">Apply</button>
|
||||
</div>
|
||||
<br>
|
||||
<div id="tls" class="ui toggle checkbox">
|
||||
<input type="checkbox">
|
||||
<label>Use TLS to serve proxy request</label>
|
||||
</div>
|
||||
<br>
|
||||
<div id="redirect" class="ui toggle checkbox" style="margin-top: 0.6em;">
|
||||
<input type="checkbox">
|
||||
<label>Force redirect HTTP request to HTTPS<br>
|
||||
<small>(Only apply when listening port is 443)</small></label>
|
||||
</div>
|
||||
<br>
|
||||
<div id="portUpdateSucc" class="ui green message" style="display:none;">
|
||||
<i class="ui green checkmark icon"></i> Setting Updated
|
||||
</div>
|
||||
<Br>
|
||||
<button id="startbtn" class="ui teal button" onclick="startService();">Start Service</button>
|
||||
<button id="stopbtn" class="ui red disabled button" onclick="stopService();">Stop Service</button>
|
||||
<div id="rploopbackWarning" class="ui segment" style="display:none;">
|
||||
<b><i class="yellow warning icon"></i> Loopback Routing Warning</b><br>
|
||||
<small>This management interface is a loopback proxied service. <br>If you want to shutdown the reverse proxy server, please remove the proxy rule for the management interface and refresh.</small>
|
||||
</div>
|
||||
<div id="statusErrmsg" class="ui red message" style="display: none;"></div>
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui two column stackable grid">
|
||||
<div class="column">
|
||||
<p>Visitor Counts</p>
|
||||
<table class="ui basic celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Country ISO Code</th>
|
||||
<th>Visitor Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- insert table rows here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="column">
|
||||
<p>Proxy Request Types</p>
|
||||
<table class="ui basic celled table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Proxy Type</th>
|
||||
<th>Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- insert table rows here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<button class="ui basic green button"><i class="refresh icon"></i> Refresh</button>
|
||||
<script>
|
||||
let loopbackProxiedInterface = false;
|
||||
//Initial the start stop button if this is reverse proxied
|
||||
$.get("/api/proxy/requestIsProxied", function(data){
|
||||
if (data == true){
|
||||
//This management interface is reverse proxied by itself
|
||||
//do not allow turning off the proxy
|
||||
$("#stopbtn").addClass("disabled");
|
||||
loopbackProxiedInterface = true;
|
||||
$("#rploopbackWarning").show();
|
||||
}
|
||||
});
|
||||
|
||||
//Get the latest server status from proxy server
|
||||
function initRPStaste(){
|
||||
$.get("/api/proxy/status", function(data){
|
||||
if (data.Running == true){
|
||||
$("#startbtn").addClass("disabled");
|
||||
if (!loopbackProxiedInterface){
|
||||
$("#stopbtn").removeClass("disabled");
|
||||
}
|
||||
$("#serverstatus").addClass("green");
|
||||
$("#statusTitle").text("Online");
|
||||
$("#statusText").text("Serving request on port: " + data.Option.Port);
|
||||
}else{
|
||||
$("#startbtn").removeClass("disabled");
|
||||
$("#stopbtn").addClass("disabled");
|
||||
$("#statusTitle").text("Offline");
|
||||
$("#statusText").text("Reverse proxy server is offline");
|
||||
$("#serverstatus").removeClass("green");
|
||||
}
|
||||
$("#incomingPort").val(data.Option.Port);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function abbreviateNumber(value) {
|
||||
var newValue = value;
|
||||
var suffixes = ["", "K", "M", "B", "T"];
|
||||
var suffixNum = 0;
|
||||
while (newValue >= 1000 && suffixNum < suffixes.length - 1) {
|
||||
newValue /= 1000;
|
||||
suffixNum++;
|
||||
}
|
||||
if (value > 1000){
|
||||
newValue = newValue.toFixed(2);
|
||||
}
|
||||
|
||||
return newValue + suffixes[suffixNum];
|
||||
}
|
||||
|
||||
|
||||
function getDailySummary(){
|
||||
$.get("/api/stats/summary?fast=true", function(data){
|
||||
console.log(data);
|
||||
|
||||
$("#summaryTotalCount").text(abbreviateNumber(data.TotalRequest));
|
||||
$("#summarySuccCount").text(abbreviateNumber(data.ValidRequest));
|
||||
$("#summaryErrCount").text(abbreviateNumber(data.ErrorRequest));
|
||||
});
|
||||
}
|
||||
setInterval(function(){
|
||||
getDailySummary();
|
||||
}, 10000);
|
||||
getDailySummary();
|
||||
|
||||
//Start and stop service button
|
||||
function startService(){
|
||||
$.post("/api/proxy/enable", {enable: true}, function(data){
|
||||
if (data.error != undefined){
|
||||
statusErrmsg(data.error);
|
||||
}
|
||||
initRPStaste();
|
||||
});
|
||||
}
|
||||
|
||||
function stopService(){
|
||||
$.post("/api/proxy/enable", {enable: false}, function(data){
|
||||
if (data.error != undefined){
|
||||
statusErrmsg(data.error);
|
||||
}
|
||||
initRPStaste();
|
||||
});
|
||||
}
|
||||
|
||||
//Show error message
|
||||
function statusErrmsg(message){
|
||||
$("#statusErrmsg").html(`<i class="red remove icon"></i> ${message}`);
|
||||
$("#statusErrmsg").slideDown('fast').delay(5000).slideUp('fast');
|
||||
}
|
||||
|
||||
function handlePortChange(){
|
||||
var newPortValue = $("#incomingPort").val();
|
||||
if (isNaN(newPortValue - 1)){
|
||||
alert("Invalid incoming port value");
|
||||
return;
|
||||
}
|
||||
|
||||
$.post("/api/proxy/setIncoming", {incoming: newPortValue}, function(data){
|
||||
if (data.error != undefined){
|
||||
statusErrmsg(data.error);
|
||||
}
|
||||
$("#portUpdateSucc").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
|
||||
initRPStaste();
|
||||
});
|
||||
}
|
||||
|
||||
function initHTTPtoHTTPSRedirectSetting(){
|
||||
$.get("/api/proxy/useHttpsRedirect", function(data){
|
||||
if (data == true){
|
||||
$("#redirect").checkbox("set checked");
|
||||
}
|
||||
|
||||
//Initiate the input listener on the checkbox
|
||||
$("#redirect").find("input").on("change", function(){
|
||||
let thisValue = $("#redirect").checkbox("is checked");
|
||||
$.ajax({
|
||||
url: "/api/proxy/useHttpsRedirect",
|
||||
data: {set: thisValue},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
alert(data.error);
|
||||
}else{
|
||||
//Updated
|
||||
$("#portUpdateSucc").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
|
||||
initRPStaste();
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
initHTTPtoHTTPSRedirectSetting();
|
||||
|
||||
function initTlsSetting(){
|
||||
$.get("/api/cert/tls", function(data){
|
||||
if (data == true){
|
||||
$("#tls").checkbox("set checked");
|
||||
}
|
||||
|
||||
//Initiate the input listener on the checkbox
|
||||
$("#tls").find("input").on("change", function(){
|
||||
let thisValue = $("#tls").checkbox("is checked");
|
||||
$.ajax({
|
||||
url: "/api/cert/tls",
|
||||
data: {set: thisValue},
|
||||
success: function(data){
|
||||
if (data.error != undefined){
|
||||
alert(data.error);
|
||||
}else{
|
||||
//Updated
|
||||
$("#portUpdateSucc").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
|
||||
initRPStaste();
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
})
|
||||
|
||||
}
|
||||
initTlsSetting();
|
||||
|
||||
</script>
|
42
src/web/components/subd.html
Normal file
@ -0,0 +1,42 @@
|
||||
<table class="ui celled sortable unstackable compact table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Matching Domain</th>
|
||||
<th>Proxy To</th>
|
||||
<th class="no-sort">Remove</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="subdList">
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<button class="ui icon green basic button" onclick="listSubd();"><i class="refresh icon"></i> Refresh</button>
|
||||
<script>
|
||||
listSubd();
|
||||
function listSubd(){
|
||||
$("#subdList").html(``);
|
||||
$.get("/api/proxy/list?type=subd", function(data){
|
||||
if (data.error !== undefined){
|
||||
$("#subdList").append(`<tr>
|
||||
<td data-label="" colspan="3"><i class="remove icon"></i> ${data.error}</td>
|
||||
</tr>`);
|
||||
}else if (data.length == 0){
|
||||
$("#subdList").append(`<tr>
|
||||
<td data-label="" colspan="3"><i class="checkmark icon"></i> No Subdomain Proxy Record</td>
|
||||
</tr>`);
|
||||
}else{
|
||||
data.forEach(subd => {
|
||||
let tlsIcon = "";
|
||||
if (subd.RequireTLS){
|
||||
tlsIcon = `<i class="lock icon"></i>`;
|
||||
}
|
||||
$("#subdList").append(`<tr>
|
||||
<td data-label="">${subd.MatchingDomain}</td>
|
||||
<td data-label="">${subd.Domain} ${tlsIcon}</td>
|
||||
<td data-label=""><button class="ui circular mini red basic button" onclick='deleteEndpoint("subd","${subd.MatchingDomain}")'><i class="remove icon"></i> Remove Subdomain</button></td>
|
||||
</tr>`);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
71
src/web/components/upnp.html
Normal file
@ -0,0 +1,71 @@
|
||||
|
||||
<h3><i class="blue exchange icon"></i> Port Forward</h3>
|
||||
<p>Port forward using UPnP protocol</p>
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui message">
|
||||
<h4><i class="ui loading spinner icon"></i> Checking Upnp State</h4>
|
||||
<p><i class="ui info circle icon"></i> If you are hosting this server under a home router which you have no access to, you can try port forward your services port and expose them to the internet via Upnp protocol.
|
||||
Note that not all router support this function, sometime this might be disabled by your ISP or administrator.</p>
|
||||
|
||||
<button style="position: absolute; right: 0.6em; top: 0.6em;" class="ui circular basic icon button"><i class="ui green refresh icon"></i></button>
|
||||
</div>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<div class="ui toggle checkbox">
|
||||
<input type="checkbox" name="upnp">
|
||||
<label>Enable UPnP</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Port to Forward</label>
|
||||
<div class="ui input">
|
||||
<input type="number" min="1" max="65535" name="forwardPort" placeholder="Forwardable Port">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Rule Name</label>
|
||||
<div class="ui input">
|
||||
<input type="text" name="ruleName" placeholder="Rule Name">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<button class="ui teal button" type="button" name="addRule"><i class="ui add icon"></i> Add Rule</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui basic segment">
|
||||
<p>Forwarded Ports</p>
|
||||
<table class="ui basic table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Port Forwarded</th>
|
||||
<th>Name of Rule</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>80</td>
|
||||
<td>HTTP</td>
|
||||
<td><button class="ui button negative" type="button" name="deleteRule">Delete</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>22</td>
|
||||
<td>SSH</td>
|
||||
<td><button class="ui button negative" type="button" name="deleteRule">Delete</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
//Get value of the form
|
||||
function getFormValues() {
|
||||
var formValues = {};
|
||||
formValues.upnp = $('input[name="upnp"]').prop('checked');
|
||||
formValues.forwardPort = $('input[name="forwardPort"]').val();
|
||||
formValues.ruleName = $('input[name="ruleName"]').val();
|
||||
return formValues;
|
||||
}
|
||||
|
||||
</script>
|
207
src/web/components/utils.html
Normal file
@ -0,0 +1,207 @@
|
||||
<h3><i class="paperclip icon"></i> Utilities</h3>
|
||||
<p>You might find these tools helpful when setting up your gateway server</p>
|
||||
<div class="ui divider"></div>
|
||||
<div class="selfauthOnly">
|
||||
<h3><i class="ui user icon"></i> Account Management</h3>
|
||||
<p>Functions to help management the current account</p>
|
||||
<div class="ui basic segment">
|
||||
<h5><i class="chevron down icon"></i> Change Password</h5>
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>Current Password</label>
|
||||
<input type="password" name="oldPassword" placeholder="Current Password">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>New Password</label>
|
||||
<input type="password" name="newPassword" placeholder="New Password">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Confirm New Password</label>
|
||||
<input type="password" name="confirmNewPassword" placeholder="Confirm New Password">
|
||||
</div>
|
||||
<button class="ui teal button" onclick="changePassword()"><i class="ui key icon"></i> Change Password</button>
|
||||
</div>
|
||||
|
||||
<div id="passwordChangeSuccMsg" class="ui green message" style="display:none;">
|
||||
<i class="ui circle checkmark green icon "></i> Password Updated
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
|
||||
<h3><i class="ui code icon"></i> IP Address Converter</h3>
|
||||
<p>No experience with CIDR notations? Here are some tools you can use to make setting up easier.</p>
|
||||
<div class="ui basic segment">
|
||||
<h5><i class="chevron down icon"></i> IP Range to CIDR Conversion</h5>
|
||||
<div class="ui message">
|
||||
<i class="info circle icon"></i> Note that the CIDR generated here covers additional IP address before or after the given range. If you need more details settings, please use CIDR with a smaller range and add additional IPs for detail range adjustment.
|
||||
</div>
|
||||
<div class="ui input">
|
||||
<input type="text" placeholder="Start IP" id="startIpInput">
|
||||
</div>
|
||||
<div class="ui input">
|
||||
<input type="text" placeholder="End IP" id="endIpInput">
|
||||
</div>
|
||||
<br>
|
||||
<button style="margin-top: 0.6em;" class="ui button" onclick="convertToCIDR()">Convert</button>
|
||||
<p>Results: <div id="cidrOutput">N/A</div></p>
|
||||
</div>
|
||||
|
||||
<div class="ui basic segment">
|
||||
<h5><i class="chevron down icon"></i> CIDR to IP Range Conversion</h5>
|
||||
<div class="ui action input">
|
||||
<input type="text" placeholder="CIDR" id="cidrInput">
|
||||
<button class="ui button" onclick="convertToIPRange()">Convert</button>
|
||||
</div>
|
||||
<p>Results: <div id="ipRangeOutput">N/A</div></p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
/*
|
||||
Account Password utilities
|
||||
*/
|
||||
|
||||
$.get("/api/auth/userCount", function(data){
|
||||
if (data == 0){
|
||||
//Using external auth manager. Hide options
|
||||
$(".selfauthOnly").hide();
|
||||
}
|
||||
})
|
||||
|
||||
function changePassword() {
|
||||
const oldPassword = document.getElementsByName('oldPassword')[0].value;
|
||||
const newPassword = document.getElementsByName('newPassword')[0].value;
|
||||
const confirmNewPassword = document.getElementsByName('confirmNewPassword')[0].value;
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/api/auth/changePassword",
|
||||
data: {
|
||||
oldPassword: oldPassword,
|
||||
newPassword: newPassword,
|
||||
confirmPassword: confirmNewPassword,
|
||||
},
|
||||
success: function (data) {
|
||||
if (data.error != undefined){
|
||||
alert(data.error);
|
||||
}else{
|
||||
$("#passwordChangeSuccMsg").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
|
||||
$('[name="oldPassword"]').val('');
|
||||
$('[name="newPassword"]').val('');
|
||||
$('[name="confirmNewPassword"]').val('');
|
||||
}
|
||||
},
|
||||
error: function (xhr, status, error) {
|
||||
alert("Error changing password: " + error);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
IP Address Utilities
|
||||
*/
|
||||
//events handler
|
||||
function convertToCIDR() {
|
||||
const startIp = document.getElementById('startIpInput').value.trim();
|
||||
const endIp = document.getElementById('endIpInput').value.trim();
|
||||
const cidrOutput = document.getElementById('cidrOutput');
|
||||
const cidr = ipRangeToCIDR(startIp, endIp);
|
||||
const ipRange = cidrToRange(cidr);
|
||||
cidrOutput.innerHTML = `CIDR: ${cidr} <br> (Cover range: ${ipRange[0]} to ${ipRange[1]})`;
|
||||
}
|
||||
|
||||
// CIDR to IP Range Conversion
|
||||
function convertToIPRange() {
|
||||
const cidr = document.getElementById('cidrInput').value.trim();
|
||||
const ipRangeOutput = document.getElementById('ipRangeOutput');
|
||||
const ipRange = cidrToRange(cidr);
|
||||
ipRangeOutput.innerHTML = `Start IP: ${ipRange[0]}<br>End IP: ${ipRange[1]}`;
|
||||
}
|
||||
|
||||
//Ip conversion function
|
||||
function cidrToRange(cidr) {
|
||||
var range = [2];
|
||||
cidr = cidr.split('/');
|
||||
var cidr_1 = parseInt(cidr[1])
|
||||
range[0] = long2ip((ip2long(cidr[0])) & ((-1 << (32 - cidr_1))));
|
||||
start = ip2long(range[0])
|
||||
range[1] = long2ip( start + Math.pow(2, (32 - cidr_1)) - 1);
|
||||
return range;
|
||||
}
|
||||
|
||||
function ipRangeToCIDR(ipStart, ipEnd) {
|
||||
var start = ip2long(ipStart);
|
||||
var end = ip2long(ipEnd);
|
||||
var cidr = 32;
|
||||
|
||||
while (start != end) {
|
||||
start >>= 1;
|
||||
end >>= 1;
|
||||
cidr--;
|
||||
}
|
||||
|
||||
return ipStart + '/' + cidr;
|
||||
}
|
||||
|
||||
function ip2long (argIP) {
|
||||
// discuss at: https://locutus.io/php/ip2long/
|
||||
// original by: Waldo Malqui Silva (https://waldo.malqui.info)
|
||||
// improved by: Victor
|
||||
// revised by: fearphage (https://my.opera.com/fearphage/)
|
||||
// revised by: Theriault (https://github.com/Theriault)
|
||||
// estarget: es2015
|
||||
// example 1: ip2long('192.0.34.166')
|
||||
// returns 1: 3221234342
|
||||
// example 2: ip2long('0.0xABCDEF')
|
||||
// returns 2: 11259375
|
||||
// example 3: ip2long('255.255.255.256')
|
||||
// returns 3: false
|
||||
let i = 0
|
||||
// PHP allows decimal, octal, and hexadecimal IP components.
|
||||
// PHP allows between 1 (e.g. 127) to 4 (e.g 127.0.0.1) components.
|
||||
const pattern = new RegExp([
|
||||
'^([1-9]\\d*|0[0-7]*|0x[\\da-f]+)',
|
||||
'(?:\\.([1-9]\\d*|0[0-7]*|0x[\\da-f]+))?',
|
||||
'(?:\\.([1-9]\\d*|0[0-7]*|0x[\\da-f]+))?',
|
||||
'(?:\\.([1-9]\\d*|0[0-7]*|0x[\\da-f]+))?$'
|
||||
].join(''), 'i')
|
||||
argIP = argIP.match(pattern) // Verify argIP format.
|
||||
if (!argIP) {
|
||||
// Invalid format.
|
||||
return false
|
||||
}
|
||||
// Reuse argIP variable for component counter.
|
||||
argIP[0] = 0
|
||||
for (i = 1; i < 5; i += 1) {
|
||||
argIP[0] += !!((argIP[i] || '').length)
|
||||
argIP[i] = parseInt(argIP[i]) || 0
|
||||
}
|
||||
// Continue to use argIP for overflow values.
|
||||
// PHP does not allow any component to overflow.
|
||||
argIP.push(256, 256, 256, 256)
|
||||
// Recalculate overflow of last component supplied to make up for missing components.
|
||||
argIP[4 + argIP[0]] *= Math.pow(256, 4 - argIP[0])
|
||||
if (argIP[1] >= argIP[5] ||
|
||||
argIP[2] >= argIP[6] ||
|
||||
argIP[3] >= argIP[7] ||
|
||||
argIP[4] >= argIP[8]) {
|
||||
return false
|
||||
}
|
||||
return argIP[1] * (argIP[0] === 1 || 16777216) +
|
||||
argIP[2] * (argIP[0] <= 2 || 65536) +
|
||||
argIP[3] * (argIP[0] <= 3 || 256) +
|
||||
argIP[4] * 1
|
||||
}
|
||||
|
||||
function long2ip (ip) {
|
||||
// discuss at: https://locutus.io/php/long2ip/
|
||||
// original by: Waldo Malqui Silva (https://fayr.us/waldo/)
|
||||
// example 1: long2ip( 3221234342 )
|
||||
// returns 1: '192.0.34.166'
|
||||
if (!isFinite(ip)) {
|
||||
return false
|
||||
}
|
||||
return [ip >>> 24 & 0xFF, ip >>> 16 & 0xFF, ip >>> 8 & 0xFF, ip & 0xFF].join('.')
|
||||
}
|
||||
</script>
|
48
src/web/components/vdir.html
Normal file
@ -0,0 +1,48 @@
|
||||
<table class="ui celled sortable unstackable compact table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Virtual Directory</th>
|
||||
<th>Proxy To</th>
|
||||
<th class="no-sort">Remove</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="vdirList">
|
||||
<tr>
|
||||
<td data-label="">test</td>
|
||||
<td data-label="">test</td>
|
||||
<td data-label=""><button class="ui circular mini red basic button"><i class="remove icon"></i> Remove Proxy</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button class="ui icon green basic button" onclick="listVdirs();"><i class="refresh icon"></i> Refresh</button>
|
||||
<script>
|
||||
//Virtual directories functions
|
||||
listVdirs();
|
||||
function listVdirs(){
|
||||
$("#vdirList").html(``);
|
||||
$.get("/api/proxy/list?type=vdir", function(data){
|
||||
if (data.error !== undefined){
|
||||
$("#vdirList").append(`<tr>
|
||||
<td data-label="" colspan="3"><i class="remove icon"></i> ${data.error}</td>
|
||||
</tr>`);
|
||||
}else if (data.length == 0){
|
||||
$("#vdirList").append(`<tr>
|
||||
<td data-label="" colspan="3"><i class="checkmark icon"></i> No Virtual Directory Record</td>
|
||||
</tr>`);
|
||||
}else{
|
||||
data.forEach(vdir => {
|
||||
let tlsIcon = "";
|
||||
if (vdir.RequireTLS){
|
||||
tlsIcon = `<i title="TLS mode" class="lock icon"></i>`;
|
||||
}
|
||||
$("#vdirList").append(`<tr>
|
||||
<td data-label="">${vdir.Root}</td>
|
||||
<td data-label="">${vdir.Domain} ${tlsIcon}</td>
|
||||
<td data-label=""><button class="ui circular mini red basic button" onclick='deleteEndpoint("vdir","${vdir.Root}")'><i class="remove icon"></i> Remove Virtual Directory</button></td>
|
||||
</tr>`);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
BIN
src/web/favicon.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
55
src/web/forbidden.html
Normal file
@ -0,0 +1,55 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0 user-scalable=no">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.5.0/semantic.min.css">
|
||||
<script type="text/javascript" src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
|
||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.5.0/semantic.min.js
|
||||
"></script>
|
||||
<title>Forbidden</title>
|
||||
<style>
|
||||
#msg{
|
||||
position: absolute;
|
||||
top: calc(50% - 150px);
|
||||
left: calc(50% - 250px);
|
||||
width: 500px;
|
||||
height: 300px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#footer{
|
||||
position: fixed;
|
||||
padding: 2em;
|
||||
padding-left: 5em;
|
||||
padding-right: 5em;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
small{
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="msg">
|
||||
<h1 style="font-size: 6em; margin-bottom: 0px;"><i class="red ban icon"></i></h1>
|
||||
<div>
|
||||
<h3 style="margin-top: 1em;">403 - Forbidden</h3>
|
||||
<div class="ui divider"></div>
|
||||
<p>You do not have permission to view this directory or page. <br>
|
||||
This might be caused by the site admin has blacklisted your country or IP address</p>
|
||||
<div class="ui divider"></div>
|
||||
<div style="text-align: left;">
|
||||
<small>Request time: <span id="reqtime"></span></small><br>
|
||||
<small id="reqURLDisplay">Request URI: <span id="requrl"></span></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$("#reqtime").text(new Date().toLocaleString(undefined, {year: 'numeric', month: '2-digit', day: '2-digit', weekday:"long", hour: '2-digit', hour12: false, minute:'2-digit', second:'2-digit'}));
|
||||
$("#requrl").text(window.location.href);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
157
src/web/hosterror.html
Normal file
@ -0,0 +1,157 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1"/>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="theme-color" content="#4b75ff">
|
||||
<link rel="icon" type="image/png" href="img/small_icon.png"/>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.css">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@300;400;500;700;900&display=swap" rel="stylesheet">
|
||||
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.js"></script>
|
||||
<title>404 - Host Not Found</title>
|
||||
<style>
|
||||
h1, h2, h3, h4, h5, p, a, span{
|
||||
font-family: 'Noto Sans TC', sans-serif;
|
||||
font-weight: 300;
|
||||
color: rgb(88, 88, 88)
|
||||
}
|
||||
|
||||
.diagram{
|
||||
background-color: #ebebeb;
|
||||
box-shadow:
|
||||
inset 0px 11px 8px -10px #CCC,
|
||||
inset 0px -11px 8px -10px #CCC;
|
||||
padding-bottom: 2em;
|
||||
}
|
||||
|
||||
.diagramHeader{
|
||||
margin-top: 0.2em;
|
||||
}
|
||||
|
||||
@media (max-width:512px) {
|
||||
.widescreenOnly{
|
||||
display: none !important;
|
||||
|
||||
}
|
||||
|
||||
.four.wide.column:not(.widescreenOnly){
|
||||
width: 50% !important;
|
||||
}
|
||||
|
||||
.ui.grid{
|
||||
justify-content: center !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<br><br>
|
||||
<div class="ui container">
|
||||
<h1 style="font-size: 4rem;">Error 404</h1>
|
||||
<p style="font-size: 2rem; margin-bottom: 0.4em;">Target Host Not Found</p>
|
||||
<small id="timestamp"></small>
|
||||
</div>
|
||||
<br><br>
|
||||
</div>
|
||||
<div class="diagram">
|
||||
<div class="ui text container">
|
||||
<div class="ui grid">
|
||||
<div class="four wide column widescreenOnly" align="center">
|
||||
<svg version="1.1" id="client_svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="100%" height="" viewBox="0 0 200 200" enable-background="new 0 0 200 200" xml:space="preserve">
|
||||
<path fill="#C9CACA" d="M184.795,143.037c0,9.941-8.059,18-18,18H33.494c-9.941,0-18-8.059-18-18V44.952c0-9.941,8.059-18,18-18
|
||||
h133.301c9.941,0,18,8.059,18,18V143.037z"/>
|
||||
<circle fill="#FFFFFF" cx="37.39" cy="50.88" r="6.998"/>
|
||||
<circle fill="#FFFFFF" cx="54.115" cy="50.88" r="6.998"/>
|
||||
<path fill="#FFFFFF" d="M167.188,50.88c0,3.865-3.133,6.998-6.998,6.998H72.379c-3.865,0-6.998-3.133-6.998-6.998l0,0
|
||||
c0-3.865,3.133-6.998,6.998-6.998h87.811C164.055,43.882,167.188,47.015,167.188,50.88L167.188,50.88z"/>
|
||||
<rect x="31.296" y="66.907" fill="#FFFFFF" width="132.279" height="77.878"/>
|
||||
<circle fill="#9BCA3E" cx="96.754" cy="144.785" r="37.574"/>
|
||||
<polyline fill="none" stroke="#FFFFFF" stroke-width="8" stroke-miterlimit="10" points="108.497,133.047 93.373,153.814
|
||||
82.989,143.204 "/>
|
||||
</svg>
|
||||
<small>You</small>
|
||||
<h2 class="diagramHeader">Browser</h2>
|
||||
<p style="font-weight: 500; color: #9bca3e;">Working</p>
|
||||
</div>
|
||||
<div class="two wide column widescreenOnly" style="margin-top: 8em; text-align: center;">
|
||||
<i class="ui big grey exchange alternate icon" style="color:rgb(167, 167, 167) !important;"></i>
|
||||
</div>
|
||||
<div class="four wide column widescreenOnly" align="center">
|
||||
<svg version="1.1" id="cloud_svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="100%" height="" viewBox="0 0 200 200" enable-background="new 0 0 200 200" xml:space="preserve">
|
||||
<ellipse fill="#9FA0A0" cx="46.979" cy="108.234" rx="25.399" ry="25.139"/>
|
||||
<circle fill="#9FA0A0" cx="109.407" cy="100.066" r="50.314"/>
|
||||
<circle fill="#9FA0A0" cx="22.733" cy="129.949" r="19.798"/>
|
||||
<circle fill="#9FA0A0" cx="172.635" cy="125.337" r="24.785"/>
|
||||
<path fill="#9FA0A0" d="M193.514,133.318c0,9.28-7.522,16.803-16.803,16.803H28.223c-9.281,0-16.803-7.522-16.803-16.803l0,0
|
||||
c0-9.28,7.522-16.804,16.803-16.804h148.488C185.991,116.515,193.514,124.038,193.514,133.318L193.514,133.318z"/>
|
||||
<circle fill="#9BCA3D" cx="100" cy="149.572" r="38.267"/>
|
||||
<polyline fill="none" stroke="#FFFFFF" stroke-width="8" stroke-miterlimit="10" points="113.408,136.402 95.954,160.369
|
||||
83.971,148.123 "/>
|
||||
</svg>
|
||||
|
||||
<small>Gateway Node</small>
|
||||
<h2 class="diagramHeader">Reverse Proxy</h2>
|
||||
<p style="font-weight: 500; color: #9bca3e;">Working</p>
|
||||
</div>
|
||||
<div class="two wide column widescreenOnly" style="margin-top: 8em; text-align: center;">
|
||||
<i class="ui big grey exchange alternate icon" style="color:rgb(167, 167, 167) !important;"></i>
|
||||
</div>
|
||||
<div class="four wide column" align="center">
|
||||
<svg version="1.1" id="host_svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="100%" height="" viewBox="0 0 200 200" enable-background="new 0 0 200 200" xml:space="preserve">
|
||||
<path fill="#999999" d="M168.484,113.413c0,9.941,3.317,46.324-6.624,46.324H35.359c-9.941,0-5.873-39.118-5.715-46.324
|
||||
l17.053-50.909c1.928-9.879,8.059-18,18-18h69.419c9.941,0,15.464,7.746,18,18L168.484,113.413z"/>
|
||||
<rect x="38.068" y="118.152" fill="#FFFFFF" width="122.573" height="34.312"/>
|
||||
<circle fill="#BD2426" cx="141.566" cy="135.873" r="8.014"/>
|
||||
<circle fill="#BD2426" cx="99.354" cy="152.464" r="36.343"/>
|
||||
<line fill="none" stroke="#FFFFFF" stroke-width="6" stroke-miterlimit="10" x1="90.5" y1="144.125" x2="107.594" y2="161.946"/>
|
||||
<line fill="none" stroke="#FFFFFF" stroke-width="6" stroke-miterlimit="10" x1="90.5" y1="161.946" x2="107.594" y2="144.79"/>
|
||||
</svg>
|
||||
<small id="host"></small>
|
||||
<h2 class="diagramHeader">Host</h2>
|
||||
<p style="font-weight: 500; color: #bd2426;">Not Found</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<br>
|
||||
<div class="ui container">
|
||||
<div class="ui stackable grid">
|
||||
<div class="eight wide column">
|
||||
<h1>What happend?</h1>
|
||||
<p>The reverse proxy target domain is not found.<br>For more information, see the error message on the reverse proxy terminal.</p>
|
||||
</div>
|
||||
<div class="eight wide column">
|
||||
<h1>What can I do?</h1>
|
||||
<h5 style="font-weight: 500;">If you are a visitor of this website: </h5>
|
||||
<p>Please try again in a few minutes</p>
|
||||
<h5 style="font-weight: 500;">If you are the owner of this website:</h5>
|
||||
<div class="ui bulleted list">
|
||||
<div class="item">Check if the target web server is online</div>
|
||||
<div class="item">Visit the Reverse Proxy management interface to correct any setting errors</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui container" style="color: grey; font-size: 90%">
|
||||
<p>Reverse Proxy by imuslab, Licensed under MIT</p>
|
||||
</div>
|
||||
<br><br>
|
||||
|
||||
<script>
|
||||
$("#timestamp").text(new Date());
|
||||
$("#host").text(location.href);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
1632
src/web/img/client.ai
Normal file
16
src/web/img/client.svg
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="200px" height="200px" viewBox="0 0 200 200" enable-background="new 0 0 200 200" xml:space="preserve">
|
||||
<path fill="#C9CACA" d="M184.795,143.037c0,9.941-8.059,18-18,18H33.494c-9.941,0-18-8.059-18-18V44.952c0-9.941,8.059-18,18-18
|
||||
h133.301c9.941,0,18,8.059,18,18V143.037z"/>
|
||||
<circle fill="#FFFFFF" cx="37.39" cy="50.88" r="6.998"/>
|
||||
<circle fill="#FFFFFF" cx="54.115" cy="50.88" r="6.998"/>
|
||||
<path fill="#FFFFFF" d="M167.188,50.88c0,3.865-3.133,6.998-6.998,6.998H72.379c-3.865,0-6.998-3.133-6.998-6.998l0,0
|
||||
c0-3.865,3.133-6.998,6.998-6.998h87.811C164.055,43.882,167.188,47.015,167.188,50.88L167.188,50.88z"/>
|
||||
<rect x="31.296" y="66.907" fill="#FFFFFF" width="132.279" height="77.878"/>
|
||||
<circle fill="#9BCA3E" cx="96.754" cy="144.785" r="37.574"/>
|
||||
<polyline fill="none" stroke="#FFFFFF" stroke-width="8" stroke-miterlimit="10" points="108.497,133.047 93.373,153.814
|
||||
82.989,143.204 "/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
1712
src/web/img/cloud.ai
Normal file
15
src/web/img/cloud.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="200px" height="200px" viewBox="0 0 200 200" enable-background="new 0 0 200 200" xml:space="preserve">
|
||||
<ellipse fill="#9FA0A0" cx="46.979" cy="108.234" rx="25.399" ry="25.139"/>
|
||||
<circle fill="#9FA0A0" cx="109.407" cy="100.066" r="50.314"/>
|
||||
<circle fill="#9FA0A0" cx="22.733" cy="129.949" r="19.798"/>
|
||||
<circle fill="#9FA0A0" cx="172.635" cy="125.337" r="24.785"/>
|
||||
<path fill="#9FA0A0" d="M193.514,133.318c0,9.28-7.522,16.803-16.803,16.803H28.223c-9.281,0-16.803-7.522-16.803-16.803l0,0
|
||||
c0-9.28,7.522-16.804,16.803-16.804h148.488C185.991,116.515,193.514,124.038,193.514,133.318L193.514,133.318z"/>
|
||||
<circle fill="#9BCA3D" cx="100" cy="149.572" r="38.267"/>
|
||||
<polyline fill="none" stroke="#FFFFFF" stroke-width="8" stroke-miterlimit="10" points="113.408,136.402 95.954,160.369
|
||||
83.971,148.123 "/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/web/img/desktop_icon.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
1671
src/web/img/host.ai
Normal file
13
src/web/img/host.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="200px" height="200px" viewBox="0 0 200 200" enable-background="new 0 0 200 200" xml:space="preserve">
|
||||
<path fill="#999999" d="M168.484,113.413c0,9.941,3.317,46.324-6.624,46.324H35.359c-9.941,0-5.873-39.118-5.715-46.324
|
||||
l17.053-50.909c1.928-9.879,8.059-18,18-18h69.419c9.941,0,15.464,7.746,18,18L168.484,113.413z"/>
|
||||
<rect x="38.068" y="118.152" fill="#FFFFFF" width="122.573" height="34.312"/>
|
||||
<circle fill="#BD2426" cx="141.566" cy="135.873" r="8.014"/>
|
||||
<circle fill="#BD2426" cx="99.354" cy="152.464" r="36.343"/>
|
||||
<line fill="none" stroke="#FFFFFF" stroke-width="6" stroke-miterlimit="10" x1="90.5" y1="144.125" x2="107.594" y2="161.946"/>
|
||||
<line fill="none" stroke="#FFFFFF" stroke-width="6" stroke-miterlimit="10" x1="90.5" y1="161.946" x2="107.594" y2="144.79"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
2137
src/web/img/logo.ai
Normal file
BIN
src/web/img/logo.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
36
src/web/img/logo.svg
Normal file
@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="600px" height="200px" viewBox="0 0 600 200" enable-background="new 0 0 600 200" xml:space="preserve">
|
||||
<g>
|
||||
<path fill="#213040" d="M138.761,47.403l-16.064,17.87c9.504,8.549,15.48,20.94,15.48,34.728c0,13.785-5.976,26.179-15.48,34.726
|
||||
l16.063,17.871c14.393-12.945,23.445-31.717,23.445-52.597C162.206,79.115,153.155,60.351,138.761,47.403z"/>
|
||||
<path fill="#213040" d="M44.198,152.596l16.064-17.869c-9.503-8.547-15.48-20.941-15.48-34.726c0-13.79,5.978-26.179,15.48-34.728
|
||||
l-16.063-17.87C29.807,60.351,20.753,79.115,20.753,100C20.753,120.881,29.807,139.652,44.198,152.596z"/>
|
||||
</g>
|
||||
<polygon fill="#A9D1F3" points="106.581,38.326 91.48,56.48 76.38,38.326 "/>
|
||||
<polygon fill="#A9D1F3" points="106.581,143.52 91.48,161.674 76.379,143.52 "/>
|
||||
<circle fill="#A9D1F3" cx="91.48" cy="100" r="22.422"/>
|
||||
<g>
|
||||
<path d="M194.194,132.898l43.232-66.846h-39.238V54.539h56.155v8.224l-43.233,66.729h43.703v11.629h-60.619V132.898z"/>
|
||||
<path d="M263.038,108.814c0-21.499,14.45-33.951,30.544-33.951c15.977,0,30.31,12.452,30.31,33.951
|
||||
c0,21.498-14.333,33.951-30.31,33.951C277.488,142.766,263.038,130.313,263.038,108.814z M310.029,108.814
|
||||
c0-13.627-6.344-22.791-16.447-22.791c-10.221,0-16.564,9.164-16.564,22.791c0,13.744,6.344,22.674,16.564,22.674
|
||||
C303.686,131.488,310.029,122.559,310.029,108.814z"/>
|
||||
<path d="M339.869,76.391h11.042l1.176,11.629h0.234c4.582-8.223,11.396-13.156,18.444-13.156c3.173,0,5.169,0.471,7.166,1.293
|
||||
l-2.35,11.863c-2.349-0.704-3.877-1.057-6.578-1.057c-5.287,0-11.632,3.643-15.626,13.981v40.177h-13.509V76.391z"/>
|
||||
<path d="M380.868,123.969c0-13.98,11.748-21.146,38.649-24.082c-0.115-7.402-2.819-13.98-12.334-13.98
|
||||
c-6.813,0-13.158,3.056-18.68,6.578l-5.052-9.162c6.696-4.23,15.742-8.459,26.08-8.459c16.096,0,23.497,10.104,23.497,27.374
|
||||
v38.884h-11.044l-1.058-7.4h-0.469c-5.875,5.051-12.806,9.045-20.56,9.045C388.739,142.766,380.868,135.365,380.868,123.969z
|
||||
M419.518,124.322V108.58c-19.147,2.23-25.61,7.166-25.61,14.332c0,6.461,4.348,9.047,10.104,9.047
|
||||
C409.649,131.959,414.231,129.256,419.518,124.322z"/>
|
||||
<path d="M464.63,107.405l-19.383-31.015h14.686l7.636,13.039c1.996,3.643,3.995,7.285,6.109,10.927h0.587
|
||||
c1.645-3.642,3.406-7.284,5.287-10.927l6.813-13.039h14.099l-19.386,32.424l20.795,32.307h-14.685l-8.459-13.744
|
||||
c-2.115-3.76-4.346-7.754-6.697-11.396h-0.586c-1.997,3.643-3.995,7.52-5.992,11.396l-7.518,13.744h-14.098L464.63,107.405z"/>
|
||||
<path d="M508.096,166.85l2.586-10.574c1.176,0.354,3.054,0.939,4.815,0.939c6.932,0,11.045-5.168,13.394-12.1l1.41-4.463
|
||||
l-25.611-64.262h13.746l11.865,33.363c1.996,5.758,3.993,12.1,5.991,18.209h0.587c1.645-5.992,3.406-12.334,5.053-18.209
|
||||
l10.456-33.363h13.038l-23.73,68.607c-5.051,13.863-11.865,23.143-25.375,23.143C512.914,168.141,510.329,167.672,508.096,166.85z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.0 KiB |
BIN
src/web/img/public/bg.png
Normal file
After Width: | Height: | Size: 4.5 MiB |
1786
src/web/img/public/icon.ai
Normal file
BIN
src/web/img/public/icon.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
15
src/web/img/public/icon.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
|
||||
<g>
|
||||
<path fill="#213040" d="M366.358,143.646l-37.493,41.708c22.181,19.953,36.132,48.874,36.132,81.057s-13.952,61.104-36.133,81.057
|
||||
l37.493,41.708c33.594-30.219,54.725-74.022,54.725-122.765C421.082,217.667,399.952,173.865,366.358,143.646z"/>
|
||||
<path fill="#213040" d="M145.64,389.175l37.494-41.707c-22.181-19.953-36.133-48.874-36.133-81.058s13.951-61.104,36.133-81.058
|
||||
l-37.493-41.707c-33.594,30.219-54.725,74.021-54.725,122.765C90.916,315.152,112.047,358.956,145.64,389.175z"/>
|
||||
</g>
|
||||
<polygon fill="#A9D1F3" points="291.247,122.458 256,164.833 220.753,122.458 "/>
|
||||
<polygon fill="#A9D1F3" points="291.247,367.987 256,410.362 220.752,367.987 "/>
|
||||
<circle fill="#A9D1F3" cx="256" cy="266.41" r="52.333"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
2037
src/web/img/public/logo.ai
Normal file
BIN
src/web/img/public/logo.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
73
src/web/img/public/logo.svg
Normal file
@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="600px" height="200px" viewBox="0 0 600 200" enable-background="new 0 0 600 200" xml:space="preserve">
|
||||
<g>
|
||||
<path fill="#213040" d="M464.798,122.75c0-4.639,3.26-7.434,7.405-7.434c2.018,0,3.614,0.854,4.611,1.805l-1.374,1.437
|
||||
c-0.865-0.737-1.885-1.281-3.192-1.281c-2.816,0-4.789,2.058-4.789,5.415c0,3.378,1.84,5.474,4.723,5.474
|
||||
c1.485,0,2.615-0.602,3.591-1.533l1.397,1.437c-1.33,1.339-2.971,2.076-5.1,2.076C467.969,130.145,464.798,127.447,464.798,122.75z
|
||||
"/>
|
||||
<path fill="#213040" d="M480.032,115.568h2.572v12.383h6.917v1.922h-9.489V115.568z"/>
|
||||
<path fill="#213040" d="M491.542,123.739v-8.171h2.572v8.308c0,3.241,1.463,4.289,3.503,4.289c2.062,0,3.569-1.048,3.569-4.289
|
||||
v-8.308h2.483v8.171c0,4.658-2.438,6.405-6.053,6.405S491.542,128.397,491.542,123.739z"/>
|
||||
<path fill="#213040" d="M506.489,128.029l1.507-1.553c1.176,1.009,2.771,1.688,4.368,1.688c1.951,0,3.037-0.815,3.037-2.019
|
||||
c0-1.301-1.086-1.708-2.594-2.29l-2.262-0.854c-1.596-0.582-3.325-1.649-3.325-3.746c0-2.251,2.239-3.939,5.365-3.939
|
||||
c1.929,0,3.725,0.698,4.922,1.805l-1.308,1.437c-1.021-0.796-2.173-1.281-3.614-1.281c-1.663,0-2.727,0.719-2.727,1.824
|
||||
c0,1.242,1.308,1.689,2.615,2.174l2.24,0.835c1.95,0.718,3.325,1.767,3.325,3.862c0,2.29-2.173,4.173-5.742,4.173
|
||||
C510.059,130.145,507.975,129.349,506.489,128.029z"/>
|
||||
<path fill="#213040" d="M523.94,117.471h-4.767v-1.902h12.172v1.902h-4.789v12.402h-2.616V117.471z"/>
|
||||
<path fill="#213040" d="M534.187,115.568h9.645v1.902h-7.072v3.979h5.986v1.902h-5.986v4.6h7.316v1.922h-9.889V115.568z"/>
|
||||
<path fill="#213040" d="M552.724,124.108h-2.66v5.765h-2.572v-14.305h5.344c3.37,0,5.897,1.048,5.897,4.153
|
||||
c0,2.213-1.353,3.533-3.415,4.096l3.991,6.056h-2.904L552.724,124.108z M552.524,122.304c2.35,0,3.658-0.854,3.658-2.582
|
||||
c0-1.727-1.309-2.329-3.658-2.329h-2.461v4.911H552.524z"/>
|
||||
<path fill="#213040" d="M464.798,146.035c0-4.639,3.304-7.434,7.649-7.434c2.306,0,3.857,0.893,4.833,1.805l-1.374,1.437
|
||||
c-0.82-0.699-1.818-1.281-3.393-1.281c-3.037,0-5.055,2.058-5.055,5.415c0,3.378,1.796,5.474,5.188,5.474
|
||||
c0.998,0,1.974-0.271,2.527-0.699v-3.338h-3.215v-1.863h5.565v6.191c-1.086,0.951-2.927,1.688-5.144,1.688
|
||||
C468.013,153.43,464.798,150.732,464.798,146.035z"/>
|
||||
<path fill="#213040" d="M484.379,138.854h2.971l5.409,14.305h-2.727l-1.375-4.057h-5.676l-1.374,4.057h-2.639L484.379,138.854z
|
||||
M488.037,147.316l-0.644-1.922c-0.532-1.553-1.02-3.145-1.529-4.755h-0.089c-0.488,1.63-0.998,3.202-1.53,4.755l-0.643,1.922
|
||||
H488.037z"/>
|
||||
<path fill="#213040" d="M496.708,140.756h-4.767v-1.902h12.172v1.902h-4.789v12.402h-2.616V140.756z"/>
|
||||
<path fill="#213040" d="M506.954,138.854h9.645v1.902h-7.072v3.979h5.986v1.902h-5.986v4.6h7.316v1.922h-9.889V138.854z"/>
|
||||
<path fill="#213040" d="M518.619,138.854h2.639l1.529,7.434c0.289,1.533,0.577,3.048,0.887,4.601h0.089
|
||||
c0.333-1.553,0.731-3.086,1.108-4.601l2.085-7.434h2.283l2.106,7.434c0.377,1.515,0.731,3.048,1.108,4.601h0.11
|
||||
c0.289-1.553,0.555-3.086,0.82-4.601l1.553-7.434h2.461l-3.215,14.305h-3.171l-2.194-7.938c-0.267-1.126-0.532-2.193-0.754-3.28
|
||||
h-0.089c-0.244,1.087-0.51,2.154-0.776,3.28l-2.15,7.938h-3.126L518.619,138.854z"/>
|
||||
<path fill="#213040" d="M543.345,138.854h2.971l5.409,14.305h-2.727l-1.375-4.057h-5.676l-1.374,4.057h-2.639L543.345,138.854z
|
||||
M547.003,147.316l-0.644-1.922c-0.532-1.553-1.02-3.145-1.529-4.755h-0.089c-0.488,1.63-0.998,3.202-1.53,4.755l-0.643,1.922
|
||||
H547.003z"/>
|
||||
<path fill="#213040" d="M556.184,147.763l-4.899-8.909h2.749l1.885,3.805c0.51,1.067,0.976,2.076,1.529,3.144h0.089
|
||||
c0.532-1.067,1.064-2.076,1.552-3.144l1.907-3.805h2.683l-4.922,8.909v5.396h-2.572V147.763z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path fill="#213040" d="M87.513,84.572l-9.7,10.791c5.739,5.162,9.348,12.645,9.348,20.971c0,8.324-3.609,15.809-9.348,20.971
|
||||
l9.7,10.791c8.691-7.818,14.158-19.152,14.158-31.762C101.671,103.723,96.205,92.391,87.513,84.572z"/>
|
||||
<path fill="#213040" d="M30.41,148.094l9.701-10.789c-5.739-5.162-9.348-12.646-9.348-20.971c0-8.327,3.609-15.809,9.348-20.971
|
||||
l-9.7-10.791c-8.691,7.818-14.158,19.15-14.158,31.762C16.252,128.943,21.719,140.277,30.41,148.094z"/>
|
||||
</g>
|
||||
<polygon fill="#A9D1F3" points="68.081,79.09 58.962,90.053 49.843,79.09 "/>
|
||||
<polygon fill="#A9D1F3" points="68.081,142.613 58.962,153.576 49.843,142.613 "/>
|
||||
<circle fill="#A9D1F3" cx="58.962" cy="116.333" r="13.54"/>
|
||||
<g>
|
||||
<path d="M120.229,141.305l35.696-55.193h-32.398v-9.506h46.366v6.79l-35.696,55.097h36.084v9.603h-50.052V141.305z"/>
|
||||
<path d="M177.072,121.42c0-17.752,11.931-28.033,25.22-28.033c13.192,0,25.026,10.281,25.026,28.033
|
||||
c0,17.751-11.834,28.033-25.026,28.033C189.002,149.453,177.072,139.171,177.072,121.42z M215.872,121.42
|
||||
c0-11.252-5.238-18.818-13.58-18.818c-8.439,0-13.677,7.566-13.677,18.818c0,11.349,5.238,18.721,13.677,18.721
|
||||
C210.634,140.141,215.872,132.769,215.872,121.42z"/>
|
||||
<path d="M240.509,94.647h9.117l0.971,9.603h0.193c3.783-6.79,9.409-10.863,15.229-10.863c2.619,0,4.268,0.388,5.917,1.067
|
||||
l-1.94,9.796c-1.939-0.582-3.201-0.873-5.432-0.873c-4.365,0-9.604,3.008-12.901,11.544v33.174h-11.154V94.647z"/>
|
||||
<path d="M274.361,133.933c0-11.543,9.7-17.46,31.913-19.885c-0.096-6.111-2.328-11.543-10.184-11.543
|
||||
c-5.626,0-10.864,2.522-15.424,5.432l-4.171-7.566c5.529-3.492,12.998-6.984,21.534-6.984c13.29,0,19.401,8.342,19.401,22.602
|
||||
v32.106h-9.119l-0.873-6.11h-0.387c-4.852,4.17-10.574,7.469-16.976,7.469C280.86,149.453,274.361,143.342,274.361,133.933z
|
||||
M306.273,134.224v-12.998c-15.81,1.843-21.146,5.917-21.146,11.834c0,5.335,3.59,7.47,8.343,7.47
|
||||
C298.125,140.529,301.908,138.298,306.273,134.224z"/>
|
||||
<path d="M343.521,120.256l-16.004-25.608h12.125l6.305,10.767c1.648,3.008,3.299,6.015,5.045,9.021h0.484
|
||||
c1.357-3.007,2.813-6.014,4.365-9.021l5.625-10.767h11.641l-16.006,26.772l17.17,26.675h-12.125l-6.984-11.349
|
||||
c-1.746-3.104-3.588-6.402-5.529-9.409h-0.484c-1.648,3.007-3.299,6.208-4.947,9.409l-6.207,11.349h-11.641L343.521,120.256z"/>
|
||||
<path d="M379.41,169.338l2.135-8.73c0.971,0.291,2.521,0.775,3.977,0.775c5.723,0,9.119-4.268,11.059-9.99l1.164-3.686
|
||||
l-21.146-53.06h11.35l9.797,27.548c1.648,4.754,3.297,9.991,4.947,15.035h0.484c1.357-4.947,2.813-10.185,4.172-15.035
|
||||
l8.633-27.548h10.766l-19.594,56.648c-4.17,11.446-9.797,19.108-20.951,19.108C383.389,170.404,381.254,170.018,379.41,169.338z"/>
|
||||
</g>
|
||||
<line fill="none" stroke="#595757" stroke-width="2" stroke-miterlimit="10" x1="450.003" y1="75" x2="450.003" y2="164.666"/>
|
||||
</svg>
|
After Width: | Height: | Size: 6.5 KiB |
1537
src/web/img/public/logo_font.ai
Normal file
BIN
src/web/img/small_icon.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
src/web/img/small_icon.psd
Normal file
221
src/web/index.html
Normal file
@ -0,0 +1,221 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1"/>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="theme-color" content="#4b75ff">
|
||||
<link rel="icon" type="image/png" href="./favicon.png" />
|
||||
<title>Control Panel | Zoraxy</title>
|
||||
<link rel="stylesheet" href="script/semantic/semantic.min.css">
|
||||
<script src="script/jquery-3.6.0.min.js"></script>
|
||||
<script src="../script/ao_module.js"></script>
|
||||
<script src="script/semantic/semantic.min.js"></script>
|
||||
<script src="script/tablesort.js"></script>
|
||||
<link rel="stylesheet" href="main.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="menubar">
|
||||
<div class="item">
|
||||
<img class="logo" src="img/logo.svg">
|
||||
</div>
|
||||
|
||||
<div class="ui right floated buttons" style="padding-top: 2px;">
|
||||
<button class="ui basic icon button" onclick="logout();"><i class="sign-out icon"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wrapper">
|
||||
<div class="toolbar">
|
||||
<div id="mainmenu" class="ui secondary vertical pointing menu">
|
||||
<a class="item active" tag="status">
|
||||
<i class="blue info circle icon"></i>Status
|
||||
</a>
|
||||
<a class="item" tag="vdir">
|
||||
<i class="yellow folder icon"></i> Virtual Directory
|
||||
</a>
|
||||
<a class="item" tag="subd">
|
||||
<i class="teal sitemap icon"></i> Subdomain Proxy
|
||||
</a>
|
||||
<a class="item" tag="rules">
|
||||
<i class="brown plus square icon"></i> Create Proxy Rules
|
||||
</a>
|
||||
<a class="item" tag="setroot">
|
||||
<i class="home icon"></i> Set Proxy Root
|
||||
</a>
|
||||
<div class="ui divider menudivider">Access & Connections</div>
|
||||
<a class="item" tag="cert">
|
||||
<i class="orange lock icon"></i> TLS / SSL certificate
|
||||
</a>
|
||||
<a class="item" tag="redirectset">
|
||||
<i class="violet level up alternate icon"></i> Redirection
|
||||
</a>
|
||||
<a class="item" tag="blacklist">
|
||||
<i class="red ban icon"></i> Blacklist
|
||||
</a>
|
||||
<a class="item" tag="upnp">
|
||||
<i class="blue exchange icon"></i> Port Forward
|
||||
</a>
|
||||
<div class="ui divider menudivider">Bridging</div>
|
||||
<a class="item" tag="">
|
||||
<i class="remove icon"></i> TCP Proxy
|
||||
</a>
|
||||
<a class="item" tag="">
|
||||
<i class="remove icon"></i> HTTP over Websocket
|
||||
</a>
|
||||
<div class="ui divider menudivider">Others</div>
|
||||
<a class="item" tag="">
|
||||
<i class="remove icon"></i> Uptime Monitor
|
||||
</a>
|
||||
<a class="item" tag="">
|
||||
<i class="remove icon"></i> Network Tools
|
||||
</a>
|
||||
<a class="item" tag="utils">
|
||||
<i class="grey paperclip icon"></i> Utilities
|
||||
</a>
|
||||
<!-- Add more components here -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="contentWindow">
|
||||
<!-- Status Tab -->
|
||||
<div id="status" class="functiontab" target="status.html" style="display: block ;">
|
||||
<br><br><div class="ui active centered inline loader"></div>
|
||||
</div>
|
||||
|
||||
<!-- Virtual Directory Tab -->
|
||||
<div id="vdir" class="functiontab" target="vdir.html"></div>
|
||||
|
||||
<!-- Subdomain Proxy -->
|
||||
<div id="subd" class="functiontab" target="subd.html"></div>
|
||||
|
||||
<!-- Create Rules -->
|
||||
<div id="rules" class="functiontab" target="rules.html"></div>
|
||||
|
||||
<!-- Set proxy root -->
|
||||
<div id="setroot" class="functiontab" target="rproot.html"></div>
|
||||
|
||||
<!-- Set TLS cert -->
|
||||
<div id="cert" class="functiontab" target="cert.html"></div>
|
||||
|
||||
<!-- Redirections -->
|
||||
<div id="redirectset" class="functiontab" target="redirection.html"></div>
|
||||
|
||||
<!-- Blacklist -->
|
||||
<div id="blacklist" class="functiontab" target="blacklist.html"></div>
|
||||
|
||||
<!-- UPnP based port fowarding -->
|
||||
<div id="upnp" class="functiontab" target="upnp.html"></div>
|
||||
|
||||
<!-- Utilities -->
|
||||
<div id="utils" class="functiontab" target="utils.html"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br><br>
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui container" style="color: grey; font-size: 90%">
|
||||
<p>CopyRight Zoraxy project and its author, 2022 - <span class="year"></span></p>
|
||||
</div>
|
||||
|
||||
<br><br>
|
||||
<script>
|
||||
$(".year").text(new Date().getFullYear());
|
||||
/*
|
||||
Loader function
|
||||
|
||||
Load all the components view from the
|
||||
components/ directory into their corrisponding divs
|
||||
*/
|
||||
let loadingComponents = 0;
|
||||
function initTabs(callback=undefined){
|
||||
$('.functiontab').each(function(){
|
||||
let loadTarget = $(this).attr("target");
|
||||
if (loadTarget != undefined){
|
||||
$(this).load("./components/" + loadTarget, function(){
|
||||
loadingComponents--;
|
||||
});
|
||||
loadingComponents++;
|
||||
}else{
|
||||
$(this).html(`<p>Unable to load components for this tab</p>`);
|
||||
}
|
||||
})
|
||||
if (callback != undefined){
|
||||
waitInit(callback);
|
||||
}
|
||||
}
|
||||
|
||||
function waitInit(callback = undefined, retryCount = 0){
|
||||
if (loadingComponents > 0 && retryCount < 5){
|
||||
setTimeout(function(){
|
||||
waitInit(callback, retryCount++);
|
||||
}, 300);
|
||||
}else if (loadingComponents == 0){
|
||||
callback();
|
||||
}else{
|
||||
alert("Missing component. Please check if your installation is complete.")
|
||||
}
|
||||
}
|
||||
|
||||
initTabs(function(){
|
||||
initRPStaste();
|
||||
|
||||
if (window.location.hash.length > 1){
|
||||
let tabID = window.location.hash.substr(1);
|
||||
openTabById(tabID);
|
||||
}else{
|
||||
openTabById("status");
|
||||
}
|
||||
$(".ui.dropdown").dropdown();
|
||||
$(".ui.checkbox").checkbox();
|
||||
|
||||
//Click on the current tab
|
||||
$("#mainmenu").find(".item").each(function(){
|
||||
$(this).on("click", function(event){
|
||||
let tabid = $(this).attr("tag");
|
||||
openTabById(tabid);
|
||||
});
|
||||
});
|
||||
|
||||
//Initialize all table that is sortable
|
||||
$('table').tablesort();
|
||||
});
|
||||
|
||||
function logout() {
|
||||
$.get("/api/auth/logout", function(response) {
|
||||
if (response === "OK") {
|
||||
window.location.href = "/";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getTabButtonById(targetTabId){
|
||||
let targetTabBtn = undefined;
|
||||
$("#mainmenu").find(".item").each(function(){
|
||||
let tabid = $(this).attr("tag");
|
||||
|
||||
if (tabid == targetTabId){
|
||||
targetTabBtn = $(this);
|
||||
}
|
||||
});
|
||||
|
||||
return targetTabBtn;
|
||||
}
|
||||
|
||||
//Select and open a tab by its tag id
|
||||
function openTabById(tabID){
|
||||
let targetBtn = getTabButtonById(tabID);
|
||||
if (targetBtn == undefined){
|
||||
alert("Invalid tabid given");
|
||||
return;
|
||||
}
|
||||
$("#mainmenu").find(".item").removeClass("active");
|
||||
$(targetBtn).addClass("active");
|
||||
$(".functiontab").hide();
|
||||
$("#" + tabID).fadeIn('fast');
|
||||
$('html,body').animate({scrollTop: 0}, 'fast');
|
||||
window.location.hash = tabID;
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
262
src/web/login.html
Normal file
@ -0,0 +1,262 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="robots" content="noindex" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/png" href="./favicon.png" />
|
||||
<title>Login | Zoraxy</title>
|
||||
<link rel="stylesheet" href="script/semantic/semantic.min.css">
|
||||
<script type="application/javascript" src="script/jquery-3.6.0.min.js"></script>
|
||||
<script type="application/javascript" src="script/semantic/semantic.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
background: rgb(245,245,245);
|
||||
background: linear-gradient(28deg, rgba(245,245,245,1) 63%, rgba(255,255,255,1) 100%);
|
||||
}
|
||||
|
||||
.background{
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
opacity: 0.8;
|
||||
z-index: -99;
|
||||
background-image: url("img/public/bg.png");
|
||||
background-size: auto 100%;
|
||||
background-position: right top;
|
||||
background-repeat: no-repeat;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
form {
|
||||
margin:auto;
|
||||
}
|
||||
|
||||
#loginForm{
|
||||
height: 100%;
|
||||
background-color: white;
|
||||
width: 25em;
|
||||
margin-left: 10em;
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
@media all and (max-width: 550px) {
|
||||
/* CSS rules here for screens lower than 750px */
|
||||
#loginForm{
|
||||
width: calc(100% - 4em);
|
||||
margin-left: 2em;
|
||||
}
|
||||
}
|
||||
|
||||
#errmsg{
|
||||
color: #9f3a38;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 0.4em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.registerOnly{
|
||||
display:none;
|
||||
}
|
||||
|
||||
.ui.fluid.button.registerOnly{
|
||||
display:none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="background"></div>
|
||||
<div id="loginForm" class="ui middle aligned center aligned grid">
|
||||
<div class="column">
|
||||
<form class="ui large form">
|
||||
<div class="ui basic segment">
|
||||
<img class="ui fluid image" src="img/public/logo.svg" style="pointer-events:none;">
|
||||
<p class="registerOnly">Account Setup</p>
|
||||
<div class="field">
|
||||
<div class="ui left icon input">
|
||||
<i class="user icon"></i>
|
||||
<input id="username" type="text" name="username" placeholder="Username">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui left icon input">
|
||||
<i class="lock icon"></i>
|
||||
<input id="magic" type="password" name="password" placeholder="Password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field registerOnly">
|
||||
<div class="ui left icon input">
|
||||
<i class="lock icon"></i>
|
||||
<input id="repeatMagic" type="password" name="passwordconfirm" placeholder="Confirm Password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field loginOnly" style="text-align: left;">
|
||||
<div class="ui checkbox">
|
||||
<input id="rmbme" type="checkbox" tabindex="0" class="hidden">
|
||||
<label>Remember Me</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="loginbtn" class="ui fluid basic blue button loginOnly">Login</div>
|
||||
<div id="regsiterbtn" class="ui fluid basic blue button registerOnly">Create</div>
|
||||
<div id="errmsg"></div>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<small>Proudly powered by Zoraxy</small>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
var redirectionAddress = "/";
|
||||
var loginAddress = "/api/auth/login";
|
||||
$(".checkbox").checkbox();
|
||||
$(document).ready(function(){
|
||||
var currentdate = new Date();
|
||||
var datetime = currentdate.getDate() + "/"
|
||||
+ (currentdate.getMonth()+1) + "/"
|
||||
+ currentdate.getFullYear() + " "
|
||||
+ currentdate.getHours() + ":"
|
||||
+ currentdate.getMinutes() + ":"
|
||||
+ currentdate.getSeconds();
|
||||
$("#requestTime").text(datetime);
|
||||
|
||||
//Check if this is a new system
|
||||
$.get("/api/auth/userCount", function(data){
|
||||
if (data == 0){
|
||||
//Allow user creation
|
||||
$(".loginOnly").hide();
|
||||
$(".registerOnly").show();
|
||||
}
|
||||
});
|
||||
//Check if the user already logged in
|
||||
$.get("/api/auth/checkLogin",function(data){
|
||||
try{
|
||||
if (data === true || data.trim() == "true"){
|
||||
//User already logged in. Redirect to target page.
|
||||
if (redirectionAddress == ""){
|
||||
//Redirect back to index
|
||||
window.location.href = "/";
|
||||
}else{
|
||||
console.log(data);
|
||||
//window.location.href = redirectionAddress;
|
||||
}
|
||||
}
|
||||
}catch(ex){
|
||||
//Assume not logged in
|
||||
console.log(data);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function updateYear() {
|
||||
const year = new Date().getFullYear();
|
||||
const elements = document.getElementsByClassName("year");
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
elements[i].textContent = year;
|
||||
}
|
||||
}
|
||||
updateYear();
|
||||
|
||||
//Event handlers for buttons
|
||||
$("#loginbtn").on("click",function(){
|
||||
login();
|
||||
});
|
||||
|
||||
$("input").on("keydown",function(event){
|
||||
if (event.keyCode === 13) {
|
||||
event.preventDefault();
|
||||
if ($(this).attr("id") == "magic"){
|
||||
login();
|
||||
}else{
|
||||
//Fuocus to password field
|
||||
$("#magic").focus();
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
$("#regsiterbtn").on("click", function(event){
|
||||
var username = $("#username").val();
|
||||
var magic = $("#magic").val();
|
||||
var repeatMagic = $("#repeatMagic").val();
|
||||
|
||||
if (magic !== repeatMagic) {
|
||||
alert("Password does not match");
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: "/api/auth/register",
|
||||
method: "POST",
|
||||
data: {
|
||||
username: username,
|
||||
password: magic
|
||||
},
|
||||
success: function(data) {
|
||||
if (data.error != undefined){
|
||||
alert(data.error);
|
||||
}else{
|
||||
//Register success. Refresh page
|
||||
window.location.reload();
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error("Error registering user:", error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
//Login system with the given username and password
|
||||
function login(){
|
||||
var username = $("#username").val();
|
||||
var magic = $("#magic").val();
|
||||
var rmbme = document.getElementById("rmbme").checked;
|
||||
$("#errmsg").stop().finish().slideUp("fast");
|
||||
$("input").addClass('disabled');
|
||||
$.post(loginAddress, {"username": username, "password": magic, "rmbme": rmbme}).done(function(data){
|
||||
if (data.error !== undefined){
|
||||
//Something went wrong during the login
|
||||
$("#errmsg").html(`<i class="red remove icon"></i> ${data.error}`);
|
||||
$("#errmsg").stop().finish().slideDown('fast');
|
||||
}else if(data.redirect !== undefined){
|
||||
//LDAP Related Code
|
||||
window.location.href = data.redirect;
|
||||
}else{
|
||||
//Login succeed
|
||||
if (redirectionAddress == ""){
|
||||
//Redirect back to index
|
||||
window.location.href = "./";
|
||||
}else{
|
||||
window.location.href = redirectionAddress;
|
||||
}
|
||||
}
|
||||
$("input").removeClass('disabled');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function get(name){
|
||||
if(name=(new RegExp('[?&]'+encodeURIComponent(name)+'=([^&]*)')).exec(location.search))
|
||||
return decodeURIComponent(name[1]);
|
||||
}
|
||||
|
||||
$(".thisyear").text(new Date().getFullYear());
|
||||
|
||||
function updateRenderElements(){
|
||||
if (window.innerHeight < 520){
|
||||
$(".bottombar").hide();
|
||||
}else{
|
||||
$(".bottombar").show();
|
||||
}
|
||||
}
|
||||
updateRenderElements();
|
||||
$(window).on("resize", function(){
|
||||
updateRenderElements();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
113
src/web/main.css
Normal file
@ -0,0 +1,113 @@
|
||||
/*
|
||||
index.html style overwrite
|
||||
*/
|
||||
body{
|
||||
background-color:white;
|
||||
}
|
||||
|
||||
.functiontab{
|
||||
display:none;
|
||||
}
|
||||
|
||||
.menubar{
|
||||
width: 100%;
|
||||
padding: 0.4em;
|
||||
padding-left: 1.2em;
|
||||
padding-right: 1.2em;
|
||||
background-color: #f5f5f5;
|
||||
margin-bottom: 1em;
|
||||
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
box-shadow: 0px 1px 5px 0px rgba(38,38,38,0.26);
|
||||
}
|
||||
|
||||
.menubar .logo{
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.menubar .item{
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.wrapper{
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 4.6em;
|
||||
}
|
||||
|
||||
.toolbar{
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.contentWindow{
|
||||
padding: 1em;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 750px) {
|
||||
.toolbar {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
#mainmenu{
|
||||
width: calc(100% - 1em);
|
||||
}
|
||||
|
||||
.contentWindow{
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.menudivider{
|
||||
font-size: 0.8em !important;
|
||||
color: #9c9c9c !important;
|
||||
padding-left: 0.6em;
|
||||
}
|
||||
|
||||
/*
|
||||
Global rules overwrite
|
||||
*/
|
||||
|
||||
.ui.teal.button{
|
||||
background-color: #7fbbc5 !important;
|
||||
}
|
||||
|
||||
.ui.red.button:not(.basic){
|
||||
background-color: #c78e70 !important;
|
||||
}
|
||||
|
||||
/*
|
||||
Status style overwrite
|
||||
*/
|
||||
#serverstatus.green{
|
||||
background-color: #7fbbc5 !important;
|
||||
}
|
||||
|
||||
#serverstatus:not(.green){
|
||||
background-color: #5f5e5c !important;
|
||||
}
|
||||
|
||||
.statustab{
|
||||
min-height: 5em;
|
||||
}
|
||||
|
||||
#summaryTotalCount{
|
||||
font-size: 1.6em;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.statustab.summary{
|
||||
background-color: #f1f1f1 !important;
|
||||
border: 0px solid transparent !important;
|
||||
}
|
||||
|
||||
.statustab.summary span, .statustab.summary i{
|
||||
color: rgb(37, 37, 37);
|
||||
}
|
158
src/web/rperror.html
Normal file
@ -0,0 +1,158 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1"/>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="theme-color" content="#4b75ff">
|
||||
<link rel="icon" type="image/png" href="img/small_icon.png"/>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.css">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@300;400;500;700;900&display=swap" rel="stylesheet">
|
||||
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.2/semantic.min.js"></script>
|
||||
<title>521 - Web server is down</title>
|
||||
<style>
|
||||
h1, h2, h3, h4, h5, p, a, span{
|
||||
font-family: 'Noto Sans TC', sans-serif;
|
||||
font-weight: 300;
|
||||
color: rgb(88, 88, 88)
|
||||
}
|
||||
|
||||
.diagram{
|
||||
background-color: #ebebeb;
|
||||
box-shadow:
|
||||
inset 0px 11px 8px -10px #CCC,
|
||||
inset 0px -11px 8px -10px #CCC;
|
||||
padding-bottom: 2em;
|
||||
}
|
||||
|
||||
.diagramHeader{
|
||||
margin-top: 0.2em;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width:512px) {
|
||||
.widescreenOnly{
|
||||
display: none !important;
|
||||
|
||||
}
|
||||
|
||||
.four.wide.column:not(.widescreenOnly){
|
||||
width: 50% !important;
|
||||
}
|
||||
|
||||
.ui.grid{
|
||||
justify-content: center !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<br><br>
|
||||
<div class="ui container">
|
||||
<h1 style="font-size: 4rem;">Error 521</h1>
|
||||
<p style="font-size: 2rem; margin-bottom: 0.4em;">Web server is down</p>
|
||||
<small id="timestamp"></small>
|
||||
</div>
|
||||
<br><br>
|
||||
</div>
|
||||
<div class="diagram">
|
||||
<div class="ui text container">
|
||||
<div class="ui grid">
|
||||
<div class="four wide column widescreenOnly" align="center">
|
||||
<svg version="1.1" id="client_svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="100%" height="" viewBox="0 0 200 200" enable-background="new 0 0 200 200" xml:space="preserve">
|
||||
<path fill="#C9CACA" d="M184.795,143.037c0,9.941-8.059,18-18,18H33.494c-9.941,0-18-8.059-18-18V44.952c0-9.941,8.059-18,18-18
|
||||
h133.301c9.941,0,18,8.059,18,18V143.037z"/>
|
||||
<circle fill="#FFFFFF" cx="37.39" cy="50.88" r="6.998"/>
|
||||
<circle fill="#FFFFFF" cx="54.115" cy="50.88" r="6.998"/>
|
||||
<path fill="#FFFFFF" d="M167.188,50.88c0,3.865-3.133,6.998-6.998,6.998H72.379c-3.865,0-6.998-3.133-6.998-6.998l0,0
|
||||
c0-3.865,3.133-6.998,6.998-6.998h87.811C164.055,43.882,167.188,47.015,167.188,50.88L167.188,50.88z"/>
|
||||
<rect x="31.296" y="66.907" fill="#FFFFFF" width="132.279" height="77.878"/>
|
||||
<circle fill="#9BCA3E" cx="96.754" cy="144.785" r="37.574"/>
|
||||
<polyline fill="none" stroke="#FFFFFF" stroke-width="8" stroke-miterlimit="10" points="108.497,133.047 93.373,153.814
|
||||
82.989,143.204 "/>
|
||||
</svg>
|
||||
<small>You</small>
|
||||
<h2 class="diagramHeader">Browser</h2>
|
||||
<p style="font-weight: 500; color: #9bca3e;">Working</p>
|
||||
</div>
|
||||
<div class="two wide column widescreenOnly" style="margin-top: 8em; text-align: center;">
|
||||
<i class="ui big grey exchange alternate icon" style="color:rgb(167, 167, 167) !important;"></i>
|
||||
</div>
|
||||
<div class="four wide column widescreenOnly" align="center">
|
||||
<svg version="1.1" id="cloud_svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="100%" height="" viewBox="0 0 200 200" enable-background="new 0 0 200 200" xml:space="preserve">
|
||||
<ellipse fill="#9FA0A0" cx="46.979" cy="108.234" rx="25.399" ry="25.139"/>
|
||||
<circle fill="#9FA0A0" cx="109.407" cy="100.066" r="50.314"/>
|
||||
<circle fill="#9FA0A0" cx="22.733" cy="129.949" r="19.798"/>
|
||||
<circle fill="#9FA0A0" cx="172.635" cy="125.337" r="24.785"/>
|
||||
<path fill="#9FA0A0" d="M193.514,133.318c0,9.28-7.522,16.803-16.803,16.803H28.223c-9.281,0-16.803-7.522-16.803-16.803l0,0
|
||||
c0-9.28,7.522-16.804,16.803-16.804h148.488C185.991,116.515,193.514,124.038,193.514,133.318L193.514,133.318z"/>
|
||||
<circle fill="#9BCA3D" cx="100" cy="149.572" r="38.267"/>
|
||||
<polyline fill="none" stroke="#FFFFFF" stroke-width="8" stroke-miterlimit="10" points="113.408,136.402 95.954,160.369
|
||||
83.971,148.123 "/>
|
||||
</svg>
|
||||
|
||||
<small>Gateway Node</small>
|
||||
<h2 class="diagramHeader">Reverse Proxy</h2>
|
||||
<p style="font-weight: 500; color: #9bca3e;">Working</p>
|
||||
</div>
|
||||
<div class="two wide column widescreenOnly" style="margin-top: 8em; text-align: center;">
|
||||
<i class="ui big grey exchange alternate icon" style="color:rgb(167, 167, 167) !important;"></i>
|
||||
</div>
|
||||
<div class="four wide column" align="center">
|
||||
<svg version="1.1" id="host_svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="100%" height="" viewBox="0 0 200 200" enable-background="new 0 0 200 200" xml:space="preserve">
|
||||
<path fill="#999999" d="M168.484,113.413c0,9.941,3.317,46.324-6.624,46.324H35.359c-9.941,0-5.873-39.118-5.715-46.324
|
||||
l17.053-50.909c1.928-9.879,8.059-18,18-18h69.419c9.941,0,15.464,7.746,18,18L168.484,113.413z"/>
|
||||
<rect x="38.068" y="118.152" fill="#FFFFFF" width="122.573" height="34.312"/>
|
||||
<circle fill="#BD2426" cx="141.566" cy="135.873" r="8.014"/>
|
||||
<circle fill="#BD2426" cx="99.354" cy="152.464" r="36.343"/>
|
||||
<line fill="none" stroke="#FFFFFF" stroke-width="6" stroke-miterlimit="10" x1="90.5" y1="144.125" x2="107.594" y2="161.946"/>
|
||||
<line fill="none" stroke="#FFFFFF" stroke-width="6" stroke-miterlimit="10" x1="90.5" y1="161.946" x2="107.594" y2="144.79"/>
|
||||
</svg>
|
||||
<small id="host"></small>
|
||||
<h2 class="diagramHeader">Host</h2>
|
||||
<p style="font-weight: 500; color: #bd2426;">Error</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<br>
|
||||
<div class="ui container">
|
||||
<div class="ui stackable grid">
|
||||
<div class="eight wide column">
|
||||
<h1>What happend?</h1>
|
||||
<p>The web server reported a bad gateway error.<br>For more information, see the error message on the reverse proxy terminal.</p>
|
||||
</div>
|
||||
<div class="eight wide column">
|
||||
<h1>What can I do?</h1>
|
||||
<h5 style="font-weight: 500;">If you are a visitor of this website: </h5>
|
||||
<p>Please try again in a few minutes</p>
|
||||
<h5 style="font-weight: 500;">If you are the owner of this website:</h5>
|
||||
<div class="ui bulleted list">
|
||||
<div class="item">Check if the target web server is online</div>
|
||||
<div class="item">Visit the Reverse Proxy management interface to correct any setting errors</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui container" style="color: grey; font-size: 90%">
|
||||
<p>Reverse Proxy by imuslab, Licensed under MIT</p>
|
||||
</div>
|
||||
<br><br>
|
||||
|
||||
<script>
|
||||
$("#timestamp").text(new Date());
|
||||
$("#host").text(location.href);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|