Quellcode durchsuchen

Added WIP reverse proxy server

TC pushbot 5 vor 4 Jahren
Ursprung
Commit
2b6b311f9e

+ 4 - 4
main.router.go

@@ -73,20 +73,20 @@ func mrouter(h http.Handler) http.Handler {
 			} else {
 				interfaceModule := userinfo.GetInterfaceModules()
 				if len(interfaceModule) == 1 && interfaceModule[0] == "Desktop" {
-					http.Redirect(w, r, "desktop.system", 307)
+					http.Redirect(w, r, "./desktop.system", 307)
 				} else if len(interfaceModule) == 1 {
 					//User with default interface module not desktop
 					modileInfo := moduleHandler.GetModuleInfoByID(interfaceModule[0])
 					http.Redirect(w, r, modileInfo.StartDir, 307)
 				} else if len(interfaceModule) > 1 {
 					//Redirect to module selector
-					http.Redirect(w, r, "SystemAO/boot/interface_selector.html", 307)
+					http.Redirect(w, r, "./SystemAO/boot/interface_selector.html", 307)
 				} else if len(interfaceModule) == 0 {
 					//Redirect to error page
-					http.Redirect(w, r, "SystemAO/boot/no_interfaceing.html", 307)
+					http.Redirect(w, r, "./SystemAO/boot/no_interfaceing.html", 307)
 				} else {
 					//For unknown operations, send it to desktop
-					http.Redirect(w, r, "desktop.system", 307)
+					http.Redirect(w, r, "./desktop.system", 307)
 				}
 			}
 		} else if ((len(r.URL.Path) >= 5 && r.URL.Path[:5] == "/www/") || r.URL.Path == "/www") && *allow_homepage == true {

+ 21 - 0
mod/network/dynamicproxy/dpcore/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018-present tobychui
+
+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.

+ 394 - 0
mod/network/dynamicproxy/dpcore/dpcore.go

@@ -0,0 +1,394 @@
+package dpcore
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"log"
+	"net"
+	"net/http"
+	"net/url"
+	"path/filepath"
+	"strings"
+	"sync"
+	"time"
+)
+
+var onExitFlushLoop func()
+
+const (
+	defaultTimeout = time.Minute * 5
+)
+
+// ReverseProxy is an HTTP Handler that takes an incoming request and
+// sends it to another server, proxying the response back to the
+// client, support http, also support https tunnel using http.hijacker
+type ReverseProxy struct {
+	// Set the timeout of the proxy server, default is 5 minutes
+	Timeout time.Duration
+
+	// Director must be a function which modifies
+	// the request into a new request to be sent
+	// using Transport. Its response is then copied
+	// back to the original client unmodified.
+	// Director must not access the provided Request
+	// after returning.
+	Director func(*http.Request)
+
+	// The transport used to perform proxy requests.
+	// default is http.DefaultTransport.
+	Transport http.RoundTripper
+
+	// FlushInterval specifies the flush interval
+	// to flush to the client while copying the
+	// response body. If zero, no periodic flushing is done.
+	FlushInterval time.Duration
+
+	// ErrorLog specifies an optional logger for errors
+	// that occur when attempting to proxy the request.
+	// If nil, logging goes to os.Stderr via the log package's
+	// standard logger.
+	ErrorLog *log.Logger
+
+	// ModifyResponse is an optional function that
+	// modifies the Response from the backend.
+	// If it returns an error, the proxy returns a StatusBadGateway error.
+	ModifyResponse func(*http.Response) error
+
+	//Prepender is an optional prepend text for URL rewrite
+	//
+	Prepender string
+}
+
+type requestCanceler interface {
+	CancelRequest(req *http.Request)
+}
+
+func NewDynamicProxyCore(target *url.URL, prepender string) *ReverseProxy {
+	targetQuery := target.RawQuery
+	director := func(req *http.Request) {
+		req.URL.Scheme = target.Scheme
+		req.URL.Host = target.Host
+		req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
+
+		// If Host is empty, the Request.Write method uses
+		// the value of URL.Host.
+		// force use URL.Host
+		req.Host = req.URL.Host
+		if targetQuery == "" || req.URL.RawQuery == "" {
+			req.URL.RawQuery = targetQuery + req.URL.RawQuery
+		} else {
+			req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
+		}
+
+		if _, ok := req.Header["User-Agent"]; !ok {
+			req.Header.Set("User-Agent", "")
+		}
+	}
+
+	return &ReverseProxy{
+		Director:  director,
+		Prepender: prepender,
+	}
+}
+
+func singleJoiningSlash(a, b string) string {
+	aslash := strings.HasSuffix(a, "/")
+	bslash := strings.HasPrefix(b, "/")
+	switch {
+	case aslash && bslash:
+		return a + b[1:]
+	case !aslash && !bslash:
+		return a + "/" + b
+	}
+	return a + b
+}
+
+func copyHeader(dst, src http.Header) {
+	for k, vv := range src {
+		for _, v := range vv {
+			dst.Add(k, v)
+		}
+	}
+}
+
+// Hop-by-hop headers. These are removed when sent to the backend.
+// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
+var hopHeaders = []string{
+	//"Connection",
+	"Proxy-Connection", // non-standard but still sent by libcurl and rejected by e.g. google
+	"Keep-Alive",
+	"Proxy-Authenticate",
+	"Proxy-Authorization",
+	"Te",      // canonicalized version of "TE"
+	"Trailer", // not Trailers per URL above; http://www.rfc-editor.org/errata_search.php?eid=4522
+	"Transfer-Encoding",
+	//"Upgrade",
+}
+
+func (p *ReverseProxy) copyResponse(dst io.Writer, src io.Reader) {
+	if p.FlushInterval != 0 {
+		if wf, ok := dst.(writeFlusher); ok {
+			mlw := &maxLatencyWriter{
+				dst:     wf,
+				latency: p.FlushInterval,
+				done:    make(chan bool),
+			}
+
+			go mlw.flushLoop()
+			defer mlw.stop()
+			dst = mlw
+		}
+	}
+
+	io.Copy(dst, src)
+}
+
+type writeFlusher interface {
+	io.Writer
+	http.Flusher
+}
+
+type maxLatencyWriter struct {
+	dst     writeFlusher
+	latency time.Duration
+	mu      sync.Mutex
+	done    chan bool
+}
+
+func (m *maxLatencyWriter) Write(b []byte) (int, error) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	return m.dst.Write(b)
+}
+
+func (m *maxLatencyWriter) flushLoop() {
+	t := time.NewTicker(m.latency)
+	defer t.Stop()
+	for {
+		select {
+		case <-m.done:
+			if onExitFlushLoop != nil {
+				onExitFlushLoop()
+			}
+			return
+		case <-t.C:
+			m.mu.Lock()
+			m.dst.Flush()
+			m.mu.Unlock()
+		}
+	}
+}
+
+func (m *maxLatencyWriter) stop() {
+	m.done <- true
+}
+
+func (p *ReverseProxy) logf(format string, args ...interface{}) {
+	if p.ErrorLog != nil {
+		p.ErrorLog.Printf(format, args...)
+	} else {
+		log.Printf(format, args...)
+	}
+}
+
+func removeHeaders(header http.Header) {
+	// Remove hop-by-hop headers listed in the "Connection" header.
+	if c := header.Get("Connection"); c != "" {
+		for _, f := range strings.Split(c, ",") {
+			if f = strings.TrimSpace(f); f != "" {
+				header.Del(f)
+			}
+		}
+	}
+
+	// Remove hop-by-hop headers
+	for _, h := range hopHeaders {
+		if header.Get(h) != "" {
+			header.Del(h)
+		}
+	}
+
+	if header.Get("A-Upgrade") != "" {
+		header.Set("Upgrade", header.Get("A-Upgrade"))
+		header.Del("A-Upgrade")
+	}
+}
+
+func addXForwardedForHeader(req *http.Request) {
+	if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
+		// If we aren't the first proxy retain prior
+		// X-Forwarded-For information as a comma+space
+		// separated list and fold multiple headers into one.
+		if prior, ok := req.Header["X-Forwarded-For"]; ok {
+			clientIP = strings.Join(prior, ", ") + ", " + clientIP
+		}
+		req.Header.Set("X-Forwarded-For", clientIP)
+	}
+}
+
+func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request) error {
+	transport := p.Transport
+	if transport == nil {
+		transport = http.DefaultTransport
+	}
+
+	outreq := new(http.Request)
+	// Shallow copies of maps, like header
+	*outreq = *req
+
+	if cn, ok := rw.(http.CloseNotifier); ok {
+		if requestCanceler, ok := transport.(requestCanceler); ok {
+			// After the Handler has returned, there is no guarantee
+			// that the channel receives a value, so to make sure
+			reqDone := make(chan struct{})
+			defer close(reqDone)
+			clientGone := cn.CloseNotify()
+
+			go func() {
+				select {
+				case <-clientGone:
+					requestCanceler.CancelRequest(outreq)
+				case <-reqDone:
+				}
+			}()
+		}
+	}
+
+	p.Director(outreq)
+	outreq.Close = false
+
+	// We may modify the header (shallow copied above), so we only copy it.
+	outreq.Header = make(http.Header)
+	copyHeader(outreq.Header, req.Header)
+
+	// Remove hop-by-hop headers listed in the "Connection" header, Remove hop-by-hop headers.
+	removeHeaders(outreq.Header)
+
+	// Add X-Forwarded-For Header.
+	addXForwardedForHeader(outreq)
+
+	res, err := transport.RoundTrip(outreq)
+	if err != nil {
+		p.logf("http: proxy error: %v", err)
+		rw.WriteHeader(http.StatusBadGateway)
+		return err
+	}
+
+	// Remove hop-by-hop headers listed in the "Connection" header of the response, Remove hop-by-hop headers.
+	removeHeaders(res.Header)
+
+	if p.ModifyResponse != nil {
+		if err := p.ModifyResponse(res); err != nil {
+			p.logf("http: proxy error: %v", err)
+			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"))
+		res.Header.Set("Location", filepath.ToSlash(filepath.Join(p.Prepender, res.Header.Get("Location"))))
+	}
+	// Copy header from response to client.
+	copyHeader(rw.Header(), res.Header)
+
+	// The "Trailer" header isn't included in the Transport's response, Build it up from Trailer.
+	if len(res.Trailer) > 0 {
+		trailerKeys := make([]string, 0, len(res.Trailer))
+		for k := range res.Trailer {
+			trailerKeys = append(trailerKeys, k)
+		}
+		rw.Header().Add("Trailer", strings.Join(trailerKeys, ", "))
+	}
+
+	rw.WriteHeader(res.StatusCode)
+	if len(res.Trailer) > 0 {
+		// Force chunking if we saw a response trailer.
+		// This prevents net/http from calculating the length for short
+		// bodies and adding a Content-Length.
+		if fl, ok := rw.(http.Flusher); ok {
+			fl.Flush()
+		}
+	}
+
+	p.copyResponse(rw, res.Body)
+	// close now, instead of defer, to populate res.Trailer
+	res.Body.Close()
+	copyHeader(rw.Header(), res.Trailer)
+
+	return nil
+}
+
+func (p *ReverseProxy) ProxyHTTPS(rw http.ResponseWriter, req *http.Request) error {
+	hij, ok := rw.(http.Hijacker)
+	if !ok {
+		p.logf("http server does not support hijacker")
+		return errors.New("http server does not support hijacker")
+	}
+
+	clientConn, _, err := hij.Hijack()
+	if err != nil {
+		p.logf("http: proxy error: %v", err)
+		return err
+	}
+
+	proxyConn, err := net.Dial("tcp", req.URL.Host)
+	if err != nil {
+		p.logf("http: proxy error: %v", err)
+		return err
+	}
+
+	// The returned net.Conn may have read or write deadlines
+	// already set, depending on the configuration of the
+	// Server, to set or clear those deadlines as needed
+	// we set timeout to 5 minutes
+	deadline := time.Now()
+	if p.Timeout == 0 {
+		deadline = deadline.Add(time.Minute * 5)
+	} else {
+		deadline = deadline.Add(p.Timeout)
+	}
+
+	err = clientConn.SetDeadline(deadline)
+	if err != nil {
+		p.logf("http: proxy error: %v", err)
+		return err
+	}
+
+	err = proxyConn.SetDeadline(deadline)
+	if err != nil {
+		p.logf("http: proxy error: %v", err)
+		return err
+	}
+
+	_, err = clientConn.Write([]byte("HTTP/1.0 200 OK\r\n\r\n"))
+	if err != nil {
+		p.logf("http: proxy error: %v", err)
+		return err
+	}
+
+	go func() {
+		io.Copy(clientConn, proxyConn)
+		clientConn.Close()
+		proxyConn.Close()
+	}()
+
+	io.Copy(proxyConn, clientConn)
+	proxyConn.Close()
+	clientConn.Close()
+
+	return nil
+}
+
+func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) error {
+	if req.Method == "CONNECT" {
+		err := p.ProxyHTTPS(rw, req)
+		return err
+	} else {
+		err := p.ProxyHTTP(rw, req)
+		return err
+	}
+}

+ 76 - 17
mod/network/dynamicproxy/dynamicproxy.go

@@ -1,12 +1,17 @@
 package dynamicproxy
 
 import (
+	"context"
+	"errors"
 	"log"
 	"net/http"
 	"net/url"
 	"strconv"
+	"strings"
 	"sync"
+	"time"
 
+	"imuslab.com/arozos/mod/network/dynamicproxy/dpcore"
 	"imuslab.com/arozos/mod/network/reverseproxy"
 )
 
@@ -15,11 +20,14 @@ import (
 
 */
 type Router struct {
-	ListenPort     int
-	ProxyEndpoints *sync.Map
-	root           *ProxyEndpoint
-	mux            http.Handler
-	useTLS         bool
+	ListenPort        int
+	ProxyEndpoints    *sync.Map
+	SubdomainEndpoint *sync.Map
+	Running           bool
+	Root              *ProxyEndpoint
+	mux               http.Handler
+	useTLS            bool
+	server            *http.Server
 }
 
 type RouterOption struct {
@@ -30,7 +38,14 @@ type ProxyEndpoint struct {
 	Root       string
 	Domain     string
 	RequireTLS bool
-	Proxy      *reverseproxy.ReverseProxy
+	Proxy      *dpcore.ReverseProxy `json:"-"`
+}
+
+type SubdomainEndpoint struct {
+	MatchingDomain string
+	Domain         string
+	RequireTLS     bool
+	Proxy          *reverseproxy.ReverseProxy `json:"-"`
 }
 
 type ProxyHandler struct {
@@ -38,11 +53,15 @@ type ProxyHandler struct {
 }
 
 func NewDynamicProxy(port int) (*Router, error) {
-	newSyncMap := sync.Map{}
+	proxyMap := sync.Map{}
+	domainMap := sync.Map{}
 	thisRouter := Router{
-		ListenPort:     port,
-		ProxyEndpoints: &newSyncMap,
-		useTLS:         false,
+		ListenPort:        port,
+		ProxyEndpoints:    &proxyMap,
+		SubdomainEndpoint: &domainMap,
+		Running:           false,
+		useTLS:            false,
+		server:            nil,
 	}
 
 	thisRouter.mux = &ProxyHandler{
@@ -53,11 +72,41 @@ func NewDynamicProxy(port int) (*Router, error) {
 }
 
 //Start the dynamic routing
-func (router *Router) StartProxyService() {
+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 := http.ListenAndServe(":"+strconv.Itoa(router.ListenPort), router.mux)
+		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
 }
 
 /*
@@ -80,7 +129,7 @@ func (router *Router) AddProxyService(rootname string, domain string, requireTLS
 		return err
 	}
 
-	proxy := reverseproxy.NewReverseProxy(path)
+	proxy := dpcore.NewDynamicProxyCore(path, rootname)
 
 	router.ProxyEndpoints.Store(rootname, &ProxyEndpoint{
 		Root:       rootname,
@@ -88,6 +137,8 @@ func (router *Router) AddProxyService(rootname string, domain string, requireTLS
 		RequireTLS: requireTLS,
 		Proxy:      proxy,
 	})
+
+	log.Println("Adding Proxy Rule: ", rootname+" to "+domain)
 	return nil
 }
 
@@ -111,7 +162,7 @@ func (router *Router) SetRootProxy(proxyLocation string, requireTLS bool) error
 		return err
 	}
 
-	proxy := reverseproxy.NewReverseProxy(path)
+	proxy := dpcore.NewDynamicProxyCore(path, "")
 
 	rootEndpoint := ProxyEndpoint{
 		Root:       "/",
@@ -120,17 +171,25 @@ func (router *Router) SetRootProxy(proxyLocation string, requireTLS bool) error
 		Proxy:      proxy,
 	}
 
-	router.root = &rootEndpoint
+	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)
-	log.Println(targetProxyEndpoint)
 	if targetProxyEndpoint != nil {
 		h.proxyRequest(w, r, targetProxyEndpoint)
 	} else {
-		h.proxyRequest(w, r, h.Parent.root)
+		h.proxyRequest(w, r, h.Parent.Root)
 	}
 }

+ 43 - 4
mod/network/dynamicproxy/proxyRequestHandler.go

@@ -1,7 +1,6 @@
 package dynamicproxy
 
 import (
-	"fmt"
 	"log"
 	"net/http"
 	"net/url"
@@ -23,6 +22,16 @@ func (router *Router) getTargetProxyEndpointFromRequestURI(requestURI string) *P
 	return targetProxyEndpoint
 }
 
+func (router *Router) getSubdomainProxyEndpointFromHostname(hostname string) *SubdomainEndpoint {
+	var targetSubdomainEndpoint *SubdomainEndpoint = nil
+	ep, ok := router.SubdomainEndpoint.Load(hostname)
+	if ok {
+		targetSubdomainEndpoint = ep.(*SubdomainEndpoint)
+	}
+
+	return targetSubdomainEndpoint
+}
+
 func (router *Router) rewriteURL(rooturl string, requestURL string) string {
 	if len(requestURL) > len(rooturl) {
 		return requestURL[len(rooturl):]
@@ -30,9 +39,40 @@ func (router *Router) rewriteURL(rooturl string, requestURL string) string {
 	return ""
 }
 
+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" {
+		//Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
+		r.Header.Set("A-Upgrade", "websocket")
+		wsRedirectionEndpoint := target.Domain
+		if wsRedirectionEndpoint[len(wsRedirectionEndpoint)-1:] != "/" {
+			//Append / to the end of the redirection endpoint if not exists
+			wsRedirectionEndpoint = wsRedirectionEndpoint + "/"
+		}
+		if len(requestURL) > 0 && requestURL[:1] == "/" {
+			//Remove starting / from request URL if exists
+			requestURL = requestURL[1:]
+		}
+		u, _ := url.Parse("ws://" + wsRedirectionEndpoint + requestURL)
+		if target.RequireTLS {
+			u, _ = url.Parse("wss://" + wsRedirectionEndpoint + requestURL)
+		}
+		wspHandler := websocketproxy.NewProxy(u)
+		wspHandler.ServeHTTP(w, r)
+		return
+	}
+
+	r.Host = r.URL.Host
+	err := target.Proxy.ServeHTTP(w, r)
+	if err != nil {
+		log.Println(err.Error())
+	}
+
+}
+
 func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, target *ProxyEndpoint) {
 	rewriteURL := h.Parent.rewriteURL(target.Root, r.RequestURI)
-	fmt.Println("Rewrite URL", rewriteURL)
 	r.URL, _ = url.Parse(rewriteURL)
 	r.Header.Set("X-Forwarded-Host", r.Host)
 	if r.Header["Upgrade"] != nil && r.Header["Upgrade"][0] == "websocket" {
@@ -44,9 +84,8 @@ func (h *ProxyHandler) proxyRequest(w http.ResponseWriter, r *http.Request, targ
 		}
 		u, _ := url.Parse("ws://" + wsRedirectionEndpoint + r.URL.String())
 		if target.RequireTLS {
-			u, _ = url.Parse("wss://localhost:" + wsRedirectionEndpoint + r.URL.String())
+			u, _ = url.Parse("wss://" + wsRedirectionEndpoint + r.URL.String())
 		}
-		fmt.Println("WebSocket opening on: " + "ws://" + wsRedirectionEndpoint + r.URL.String())
 		wspHandler := websocketproxy.NewProxy(u)
 		wspHandler.ServeHTTP(w, r)
 		return

+ 44 - 0
mod/network/dynamicproxy/subdomain.go

@@ -0,0 +1,44 @@
+package dynamicproxy
+
+import (
+	"log"
+	"net/url"
+
+	"imuslab.com/arozos/mod/network/reverseproxy"
+)
+
+/*
+	Add an URL intoa custom subdomain service
+
+*/
+
+func (router *Router) AddSubdomainRoutingService(hostnameWithSubdomain string, domain string, requireTLS bool) error {
+	if domain[len(domain)-1:] == "/" {
+		domain = domain[:len(domain)-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 := reverseproxy.NewReverseProxy(path)
+
+	router.SubdomainEndpoint.Store(hostnameWithSubdomain, &SubdomainEndpoint{
+		MatchingDomain: hostnameWithSubdomain,
+		Domain:         domain,
+		RequireTLS:     requireTLS,
+		Proxy:          proxy,
+	})
+
+	log.Println("Adding Subdomain Rule: ", hostnameWithSubdomain+" to "+domain)
+	return nil
+}

+ 144 - 8
reverseproxy.go

@@ -1,9 +1,13 @@
 package main
 
 import (
+	"encoding/json"
 	"log"
+	"net/http"
 
+	module "imuslab.com/arozos/mod/modules"
 	"imuslab.com/arozos/mod/network/dynamicproxy"
+	prout "imuslab.com/arozos/mod/prouter"
 )
 
 var (
@@ -12,16 +16,148 @@ var (
 
 //Add user customizable reverse proxy
 func ReverseProxtInit() {
-	return
-	dynamicProxyRouter, err := dynamicproxy.NewDynamicProxy(80)
+	dprouter, err := dynamicproxy.NewDynamicProxy(80)
 	if err != nil {
 		log.Println(err.Error())
 		return
 	}
-	dynamicProxyRouter.SetRootProxy("192.168.0.107:8080", false)
-	dynamicProxyRouter.AddProxyService("/loopback", "localhost:8080", false)
-	dynamicProxyRouter.AddProxyService("/imus", "192.168.0.107:8081", false)
-	dynamicProxyRouter.AddProxyService("/hkwtc", "hkwtc.org:9091", false)
-	dynamicProxyRouter.StartProxyService()
-	log.Println("Dynamic Proxy service started")
+
+	dynamicProxyRouter = dprouter
+
+	//Register the module
+	moduleHandler.RegisterModule(module.ModuleInfo{
+		Name:        "Reverse Proxy",
+		Desc:        "Setup reverse proxy to other nearby services",
+		Group:       "System Settings",
+		IconPath:    "SystemAO/reverse_proxy/img/small_icon.png",
+		Version:     "1.0",
+		StartDir:    "SystemAO/reverse_proxy/index.html",
+		SupportFW:   true,
+		InitFWSize:  []int{1080, 580},
+		LaunchFWDir: "SystemAO/reverse_proxy/index.html",
+		SupportEmb:  false,
+	})
+
+	//Register HybridBackup storage restore endpoints
+	router := prout.NewModuleRouter(prout.RouterOption{
+		ModuleName:  "Reverse Proxy",
+		AdminOnly:   false,
+		UserHandler: userHandler,
+		DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
+			sendErrorResponse(w, "Permission Denied")
+		},
+	})
+
+	router.HandleFunc("/system/proxy/enable", ReverseProxyHandleOnOff)
+	router.HandleFunc("/system/proxy/add", ReverseProxyHandleAddEndpoint)
+	router.HandleFunc("/system/proxy/status", ReverseProxyStatus)
+	router.HandleFunc("/system/proxy/list", ReverseProxyList)
+
+	/*
+		dynamicProxyRouter.SetRootProxy("192.168.0.107:8080", false)
+		dynamicProxyRouter.AddSubdomainRoutingService("loopback.localhost", "localhost:8080", false)
+		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")
+
+	if eptype == "vdir" {
+		vdir, err := mv(r, "vdir", true)
+		if err != nil {
+			sendErrorResponse(w, "vdir not defined")
+			return
+		}
+		dynamicProxyRouter.AddProxyService(vdir, endpoint, useTLS)
+
+	} else if eptype == "subd" {
+		subdomain, err := mv(r, "subdomain", true)
+		if err != nil {
+			sendErrorResponse(w, "subdomain not defined")
+			return
+		}
+		dynamicProxyRouter.AddSubdomainRoutingService(subdomain, endpoint, useTLS)
+	} else if eptype == "root" {
+		dynamicProxyRouter.SetRootProxy(endpoint, useTLS)
+	}
+
+	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 {
+		sendErrorResponse(w, "Invalid type given")
+	}
 }

+ 108 - 0
storage.bridge.go

@@ -0,0 +1,108 @@
+package main
+
+import (
+	"errors"
+	"log"
+
+	fs "imuslab.com/arozos/mod/filesystem"
+	storage "imuslab.com/arozos/mod/storage"
+)
+
+/*
+	Storage functions related to bridged FSH
+*/
+
+//Initiate bridged storage pool configs
+func BridgeStoragePoolInit() {
+	bridgeRecords, err := bridgeManager.ReadConfig()
+	if err != nil {
+		log.Println("[ERROR] Fail to read File System Handler bridge config")
+		return
+	}
+
+	for _, bridgeConf := range bridgeRecords {
+		fsh, err := GetFsHandlerByUUID(bridgeConf.FSHUUID)
+		if err != nil {
+			//This fsh is not found. Skip this
+			continue
+		}
+
+		basePool, err := GetStoragePoolByOwner(bridgeConf.SPOwner)
+		if err != nil {
+			//This fsh is not found. Skip this
+			continue
+		}
+
+		err = BridgeFSHandlerToGroup(fsh, basePool)
+		if err != nil {
+			log.Println("Failed to bridge "+fsh.UUID+":/ to "+basePool.Owner, err.Error())
+		}
+		log.Println(fsh.UUID + ":/ bridged to " + basePool.Owner + " Storage Pool")
+	}
+}
+
+func BridgeStoragePoolForGroup(group string) {
+	bridgeRecords, err := bridgeManager.ReadConfig()
+	if err != nil {
+		log.Println("Failed to bridge FSH for group " + group)
+		return
+	}
+
+	for _, bridgeConf := range bridgeRecords {
+		if bridgeConf.SPOwner == group {
+			fsh, err := GetFsHandlerByUUID(bridgeConf.FSHUUID)
+			if err != nil {
+				//This fsh is not found. Skip this
+				continue
+			}
+
+			basePool, err := GetStoragePoolByOwner(bridgeConf.SPOwner)
+			if err != nil {
+				//This fsh is not found. Skip this
+				continue
+			}
+
+			err = BridgeFSHandlerToGroup(fsh, basePool)
+			if err != nil {
+				log.Println("Failed to bridge "+fsh.UUID+":/ to "+basePool.Owner, err.Error())
+			}
+			log.Println(fsh.UUID + ":/ bridged to " + basePool.Owner + " Storage Pool")
+		}
+	}
+}
+
+//Bridge a FSH to a given Storage Pool
+func BridgeFSHandlerToGroup(fsh *fs.FileSystemHandler, sp *storage.StoragePool) error {
+	//Check if the fsh already exists in the basepool
+	for _, thisFSH := range sp.Storages {
+		if thisFSH.UUID == fsh.UUID {
+			return errors.New("Target File System Handler already bridged to this pool")
+		}
+	}
+	sp.Storages = append(sp.Storages, fsh)
+	return nil
+}
+
+func DebridgeFSHandlerFromGroup(fshUUID string, sp *storage.StoragePool) error {
+	isBridged, err := bridgeManager.IsBridgedFSH(fshUUID, sp.Owner)
+	if err != nil || !isBridged {
+		return errors.New("FSH not bridged")
+	}
+
+	newStorageList := []*fs.FileSystemHandler{}
+	fshExists := false
+	for _, fsh := range sp.Storages {
+		if fsh.UUID != fshUUID {
+			newStorageList = append(newStorageList, fsh)
+		} else {
+			fshExists = true
+		}
+	}
+
+	if fshExists {
+		sp.Storages = newStorageList
+		return nil
+	} else {
+		return errors.New("Target File System Handler not found")
+	}
+}

+ 0 - 96
storage.go

@@ -129,102 +129,6 @@ func GroupStoragePoolInit() {
 	StoragePoolEditorInit()
 }
 
-//Initiate bridged storage pool configs
-func BridgeStoragePoolInit() {
-	bridgeRecords, err := bridgeManager.ReadConfig()
-	if err != nil {
-		log.Println("[ERROR] Fail to read File System Handler bridge config")
-		return
-	}
-
-	for _, bridgeConf := range bridgeRecords {
-		fsh, err := GetFsHandlerByUUID(bridgeConf.FSHUUID)
-		if err != nil {
-			//This fsh is not found. Skip this
-			continue
-		}
-
-		basePool, err := GetStoragePoolByOwner(bridgeConf.SPOwner)
-		if err != nil {
-			//This fsh is not found. Skip this
-			continue
-		}
-
-		err = BridgeFSHandlerToGroup(fsh, basePool)
-		if err != nil {
-			log.Println("Failed to bridge "+fsh.UUID+":/ to "+basePool.Owner, err.Error())
-		}
-		log.Println(fsh.UUID + ":/ bridged to " + basePool.Owner + " Storage Pool")
-	}
-}
-
-func BridgeStoragePoolForGroup(group string) {
-	bridgeRecords, err := bridgeManager.ReadConfig()
-	if err != nil {
-		log.Println("Failed to bridge FSH for group " + group)
-		return
-	}
-
-	for _, bridgeConf := range bridgeRecords {
-		if bridgeConf.SPOwner == group {
-			fsh, err := GetFsHandlerByUUID(bridgeConf.FSHUUID)
-			if err != nil {
-				//This fsh is not found. Skip this
-				continue
-			}
-
-			basePool, err := GetStoragePoolByOwner(bridgeConf.SPOwner)
-			if err != nil {
-				//This fsh is not found. Skip this
-				continue
-			}
-
-			err = BridgeFSHandlerToGroup(fsh, basePool)
-			if err != nil {
-				log.Println("Failed to bridge "+fsh.UUID+":/ to "+basePool.Owner, err.Error())
-			}
-			log.Println(fsh.UUID + ":/ bridged to " + basePool.Owner + " Storage Pool")
-		}
-	}
-}
-
-//Bridge a FSH to a given Storage Pool
-func BridgeFSHandlerToGroup(fsh *fs.FileSystemHandler, sp *storage.StoragePool) error {
-	//Check if the fsh already exists in the basepool
-	for _, thisFSH := range sp.Storages {
-		if thisFSH.UUID == fsh.UUID {
-			return errors.New("Target File System Handler already bridged to this pool")
-		}
-	}
-	sp.Storages = append(sp.Storages, fsh)
-	return nil
-}
-
-func DebridgeFSHandlerFromGroup(fshUUID string, sp *storage.StoragePool) error {
-	isBridged, err := bridgeManager.IsBridgedFSH(fshUUID, sp.Owner)
-	if err != nil || !isBridged {
-		return errors.New("FSH not bridged")
-	}
-
-	newStorageList := []*fs.FileSystemHandler{}
-	fshExists := false
-	for _, fsh := range sp.Storages {
-		if fsh.UUID != fshUUID {
-			newStorageList = append(newStorageList, fsh)
-		} else {
-			fshExists = true
-		}
-	}
-
-	if fshExists {
-		sp.Storages = newStorageList
-		return nil
-	} else {
-		return errors.New("Target File System Handler not found")
-	}
-
-}
-
 func LoadStoragePoolForGroup(pg *permission.PermissionGroup) error {
 	expectedConfigPath := "./system/storage/" + pg.Name + ".json"
 	if fileExists(expectedConfigPath) {

+ 6 - 1
system/bridge.json

@@ -1 +1,6 @@
-[]
+[
+ {
+  "FSHUUID": "sh",
+  "SPOwner": "administrator"
+ }
+]

+ 2 - 2
web/SystemAO/file_system/file_explorer.html

@@ -1640,7 +1640,7 @@
                         $("#folderList").append(`
                         <div class="ts card fileObject ${currentTheme}" draggable="true" ondragstart="onFileObjectDragStart(this,event);" ondrop="dropToFolder(event)" ondragover="allowDrop(event)" fileID="${i}" filename="${filename}" filepath="${filepath}" ondblclick="openthis(this,event);" type="folder" style="width:${gridSize}px; display:inline-block !important;vertical-align:top; height:15em; margin-top:0px !important; overflow:hidden;">
                             <div class="image" style="text-align: center;">
-                                <img draggable="true"ondragstart="disableDrag(event);" src="../../../img/desktop/files_icon/${filesIconTheme}/folder.png" style="height: 148px; width: 148px; display: inline-block;">
+                                <img draggable="true"ondragstart="disableDrag(event);" src="../../img/desktop/files_icon/${filesIconTheme}/folder.png" style="height: 148px; width: 148px; display: inline-block;">
                             </div>
                             <div class="content" style="font-size: 12px;">
                                 <div class="header ${currentTheme} ${textclass}" title="${filename}">${displayName} ${shareicon}</div>
@@ -1733,7 +1733,7 @@
                         </div>`);
                     }else if (viewMode == "grid"){
                         //Update the icon path depends on the type of file
-                        var imagePath = "../../../img/desktop/files_icon/" + filesIconTheme + "/" + icon + ".png";
+                        var imagePath = "../../img/desktop/files_icon/" + filesIconTheme + "/" + icon + ".png";
                         var displayName = JSON.parse(JSON.stringify(filename));
                         if (ext == "shortcut"){
                             //This is a shortcut file. Treat it specially

BIN
web/SystemAO/reverse_proxy/img/desktop_icon.png


BIN
web/SystemAO/reverse_proxy/img/small_icon.png


BIN
web/SystemAO/reverse_proxy/img/small_icon.psd


+ 129 - 0
web/SystemAO/reverse_proxy/index.html

@@ -0,0 +1,129 @@
+<!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="stylesheet" href="../../script/semantic/semantic.min.css">
+        <script src="../../script/jquery.min.js"></script>
+        <script src="../../script/ao_module.js"></script>
+        <script src="../../script/semantic/semantic.min.js"></script>
+        <title>Reverse Proxy</title>
+        <style>
+            body{
+                background-color:white;
+            }
+            
+        </style>
+    </head>
+    <body>
+        <br>
+        <div class="ui container">
+            <div class="ui basic segment">
+                <h3 class="ui header">
+                    <i class="exchange icon"></i>
+                    <div class="content">
+                      Reverse Proxy
+                      <div class="sub header">Simple reverse proxy designed for web desktop users</div>
+                    </div>
+                </h3>
+            </div>
+            <div class="ui divider"></div>
+            <div id="serverstatus" class="ui message">
+                <h3 class="ui header">
+                    <i class="power off icon"></i>
+                    <div class="content">
+                      <span id="statusTitle">Offline</span>
+                      <div class="sub header" id="statusText">Reverse proxy server is offline</div>
+                    </div>
+                </h3>
+            </div>
+            <div id="errmsg" class="ui red message" style="display: none;">
+                
+            </div>
+            <button id="startbtn" class="ui green button" onclick="startService();">Start Service</button>
+            <button id="stopbtn" class="ui red disabled button" onclick="stopService();">Stop Service</button>
+            <div class="ui divider"></div>
+            <p>Virtual Directories</p>
+            <table class="ui celled table">
+                <thead>
+                    <tr>
+                        <th>Virtual Directory</th>
+                        <th>Proxy To</th>
+                        <th>Remove</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    <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>
+            <div class="ui divider"></div>
+            <p>Subdomain Proxy</p>
+            <table class="ui celled table">
+                <thead>
+                    <tr>
+                        <th>Subdomain</th>
+                        <th>Proxy To</th>
+                        <th>Remove</th>
+                    </tr>
+                </thead>
+                <tbody>
+                   
+                </tbody>
+            </table>
+            <div class="">
+                <button class="ui blue button" onclick=""><i class="add icon"></i> New Proxy Endpoint</button>
+                <button class="ui button" onclick=""><i class="edit icon"></i> Set Proxy Root</button>
+            </div>
+            <br><br>
+        </div>
+        
+        <script>
+            initRPStaste();
+
+            function initRPStaste(){
+                $.get("../../system/proxy/status", function(data){
+                    if (data.Running == true){
+                        $("#startbtn").addClass("disabled");
+                        $("#stopbtn").removeClass("disabled");
+                        $("#serverstatus").addClass("green");
+                    }else{
+                        $("#startbtn").removeClass("disabled");
+                        $("#stopbtn").addClass("disabled");
+                        $("#statusTitle").text("Offline");
+                        $("#statusText").text("Reverse proxy server is offline");
+                        $("#serverstatus").removeClass("green");
+                    }
+                });
+            }
+
+            function startService(){
+                $.post("../../system/proxy/enable", {enable: true}, function(data){
+                    if (data.error != undefined){
+                        errmsg(data.error);
+                    }
+                    initRPStaste();
+                });
+            }   
+
+            function stopService(){
+                $.post("../../system/proxy/enable", {enable: false}, function(data){
+                    if (data.error != undefined){
+                        errmsg(data.error);
+                    }
+                    initRPStaste();
+                });
+            }
+
+            function errmsg(message){
+                $("#errmsg").html(`<i class="red remove icon"></i> ${message}`);
+                $("#errmsg").slideDown('fast').delay(5000).slideUp('fast');
+            }
+        </script>
+    </body>
+</html>

+ 11 - 11
web/desktop.system

@@ -4,13 +4,13 @@
     <title>ArozOS Desktop</title>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <link rel="stylesheet" href="script/tocas/tocas.css">
-    <link id="fwcss" rel="stylesheet" href="script/ao.css">
-    <link rel="stylesheet" href="SystemAO/desktop/script/jsCalendar/source/jsCalendar.css">
-    <script type="text/javascript" src="script/tocas/tocas.js"></script>
-    <script type="text/javascript" src="script/jquery.min.js"></script>
-    <script type="text/javascript" src="script/ao_module.js"></script>
-    <script type="text/javascript" src="SystemAO/desktop/script/jsCalendar/source/jsCalendar.js"></script>
+    <link rel="stylesheet" href="./script/tocas/tocas.css">
+    <link id="fwcss" rel="stylesheet" href="./script/ao.css">
+    <link rel="stylesheet" href="./SystemAO/desktop/script/jsCalendar/source/jsCalendar.css">
+    <script type="text/javascript" src="./script/tocas/tocas.js"></script>
+    <script type="text/javascript" src="./script/jquery.min.js"></script>
+    <script type="text/javascript" src="./script/ao_module.js"></script>
+    <script type="text/javascript" src="./SystemAO/desktop/script/jsCalendar/source/jsCalendar.js"></script>
     <style>
         body {
             background-repeat: no-repeat;
@@ -1140,7 +1140,7 @@
 
         function initStartupSounds(){
             $.ajax({
-                url: "../../system/desktop/preference",
+                url: "system/desktop/preference",
                 method: "GET",
                 data: {preference: "startup-audio"},
                 success: function(data){
@@ -4145,7 +4145,7 @@
             loggingOut = true;
             if (confirm("Exiting Session. Confirm?")){
                 $.get("system/auth/logout", function() {
-                    window.location.href = "/";
+                    window.location.href = "./";
                 });
             }
             hideAllContextMenus();
@@ -5638,7 +5638,7 @@
 
         function initTheme(targetTheme=undefined){
             if (targetTheme == undefined){
-                $.get("../../system/file_system/preference?key=file_explorer/theme", function(data){
+                $.get("system/file_system/preference?key=file_explorer/theme", function(data){
                     if (data == "darkTheme"){
                         setDarkTheme();
                     }
@@ -5724,7 +5724,7 @@
         //Load the user define theme color to overwrite the default color if exists
         function initUserDefinedThemeColor(){
             $.ajax({
-                url: "../../system/file_system/preference",
+                url: "system/file_system/preference",
                 data: {key: "themecolor"},
                 success: function(data){
                     if (data.error == undefined && data != ""){

+ 2 - 2
web/login.system

@@ -219,7 +219,7 @@
             var magic = $("#magic").val();
             var rmbme = document.getElementById("rmbme").checked;
             $("input").addClass('disabled');
-            $.post("/system/auth/login", {"username": username, "password": magic, "rmbme": rmbme}).done(function(data){
+            $.post("system/auth/login", {"username": username, "password": magic, "rmbme": rmbme}).done(function(data){
                 if (data.error !== undefined){
                     //Something went wrong during the login
                     $("#errmsg").text(data.error);
@@ -228,7 +228,7 @@
                     //Login succeed
                     if (redirectionAddress == ""){
                         //Redirect back to index
-                        window.location.href = "/";
+                        window.location.href = "./";
                     }else{
                         window.location.href = redirectionAddress;
                     }

+ 1 - 1
web/script/ao.css

@@ -1,6 +1,6 @@
 @font-face {
     font-family: 'TaipeiSansTCBeta-Regular';
-    src: url('/script/font/TaipeiSansTCBeta-Regular.ttf');
+    src: url('font/TaipeiSansTCBeta-Regular.ttf');
 }
 
 h1, h2, h3, p, span, div { font-family: 'TaipeiSansTCBeta-Regular'}