diff --git a/common.go b/common.go new file mode 100644 index 0000000..270e181 --- /dev/null +++ b/common.go @@ -0,0 +1,185 @@ +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 +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..0d1970e --- /dev/null +++ b/config.go @@ -0,0 +1,76 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" +) + +type Record struct { + ProxyType string + Rootname string + ProxyTarget string + UseTLS bool +} + +func SaveReverseProxyConfig(ptype string, rootname string, proxyTarget string, useTLS bool) error { + os.MkdirAll("conf", 0775) + filename := getFilenameFromRootName(rootname) + + //Generate record + thisRecord := Record{ + ProxyType: ptype, + Rootname: rootname, + ProxyTarget: proxyTarget, + UseTLS: useTLS, + } + + //Write to file + js, _ := json.MarshalIndent(thisRecord, "", " ") + return ioutil.WriteFile(filepath.Join("conf", filename), js, 0775) +} + +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)) + if err != nil { + log.Println(err.Error()) + return err + } + } + + //File already gone + return nil +} + +//Return ptype, rootname and proxyTarget, error if any +func LoadReverseProxyConfig(filename string) (*Record, error) { + thisRecord := Record{} + configContent, err := ioutil.ReadFile(filename) + if err != nil { + return &thisRecord, err + } + + //Unmarshal the content into config + + err = json.Unmarshal(configContent, &thisRecord) + if err != nil { + return &thisRecord, err + } + + //Return it + return &thisRecord, nil +} + +func getFilenameFromRootName(rootname string) string { + //Generate a filename for this rootname + filename := strings.ReplaceAll(rootname, ".", "_") + filename = strings.ReplaceAll(filename, "/", "-") + filename = filename + ".config" + return filename +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8257426 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..11d5fd5 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..923232f --- /dev/null +++ b/main.go @@ -0,0 +1,75 @@ +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) + } + +} diff --git a/mod/aroz/aroz.go b/mod/aroz/aroz.go new file mode 100644 index 0000000..2dc3236 --- /dev/null +++ b/mod/aroz/aroz.go @@ -0,0 +1,70 @@ +package aroz + +import ( + "encoding/json" + "flag" + "fmt" + "net/http" + "net/url" + "os" +) + +type ArozHandler struct { + Port string + restfulEndpoint string +} + +//Information required for registering this subservice to arozos +type ServiceInfo struct { + Name string //Name of this module. e.g. "Audio" + Desc string //Description for this module + Group string //Group of the module, e.g. "system" / "media" etc + IconPath string //Module icon image path e.g. "Audio/img/function_icon.png" + Version string //Version of the module. Format: [0-9]*.[0-9][0-9].[0-9] + StartDir string //Default starting dir, e.g. "Audio/index.html" + SupportFW bool //Support floatWindow. If yes, floatWindow dir will be loaded + LaunchFWDir string //This link will be launched instead of 'StartDir' if fw mode + SupportEmb bool //Support embedded mode + LaunchEmb string //This link will be launched instead of StartDir / Fw if a file is opened with this module + InitFWSize []int //Floatwindow init size. [0] => Width, [1] => Height + InitEmbSize []int //Embedded mode init size. [0] => Width, [1] => Height + SupportedExt []string //Supported File Extensions. e.g. ".mp3", ".flac", ".wav" +} + +//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") + //Parse the flags + flag.Parse() + if *infoRequestMode == true { + //Information request mode + jsonString, _ := json.Marshal(info) + fmt.Println(string(jsonString)) + os.Exit(0) + } + return &ArozHandler{ + Port: *port, + restfulEndpoint: *restful, + } +} + +//Get the username and resources access token from the request, return username, token +func (a *ArozHandler) GetUserInfoFromRequest(w http.ResponseWriter, r *http.Request) (string, string) { + username := r.Header.Get("aouser") + token := r.Header.Get("aotoken") + + return username, token +} + +func (a *ArozHandler) RequestGatewayInterface(token string, script string) (*http.Response, error) { + resp, err := http.PostForm(a.restfulEndpoint, + url.Values{"token": {token}, "script": {script}}) + if err != nil { + // handle error + return nil, err + } + + return resp, nil +} diff --git a/mod/aroz/doc.txt b/mod/aroz/doc.txt new file mode 100644 index 0000000..346c515 Binary files /dev/null and b/mod/aroz/doc.txt differ diff --git a/mod/database/database.go b/mod/database/database.go new file mode 100644 index 0000000..efbdf70 --- /dev/null +++ b/mod/database/database.go @@ -0,0 +1,240 @@ +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" + "log" + "sync" + + "github.com/boltdb/bolt" +) + +type Database struct { + Db *bolt.DB + Tables sync.Map + ReadOnly bool +} + +func NewDatabase(dbfile string, readOnlyMode bool) (*Database, error) { + db, err := bolt.Open(dbfile, 0600, nil) + log.Println("Key-value Database Service Started: " + dbfile) + + tableMap := sync.Map{} + //Build the table list from database + err = db.View(func(tx *bolt.Tx) error { + return tx.ForEach(func(name []byte, _ *bolt.Bucket) error { + tableMap.Store(string(name), "") + return nil + }) + }) + + return &Database{ + Db: db, + Tables: tableMap, + ReadOnly: readOnlyMode, + }, 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) { + results := []string{} + + d.Tables.Range(func(tableName, v interface{}) bool { + entries, err := d.ListTable(tableName.(string)) + if err != nil { + log.Println("Reading table " + tableName.(string) + " failed: " + err.Error()) + return false + } + for _, keypairs := range entries { + results = append(results, string(keypairs[0])+":"+string(keypairs[1])+"\n") + } + return true + }) + + return results, nil +} + +//Create a new table +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 := tx.CreateBucketIfNotExists([]byte(tableName)) + if err != nil { + return err + } + return nil + }) + + d.Tables.Store(tableName, "") + return err +} + +//Check is table exists +func (d *Database) TableExists(tableName string) bool { + if _, ok := d.Tables.Load(tableName); ok { + return true + } + return false +} + +//Drop the given table +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 := tx.DeleteBucket([]byte(tableName)) + if err != nil { + return err + } + return nil + }) + 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 { + return errors.New("Operation rejected in ReadOnly mode") + } + + jsonString, err := json.Marshal(value) + if err != nil { + return err + } + err = d.Db.Update(func(tx *bolt.Tx) error { + _, err := tx.CreateBucketIfNotExists([]byte(tableName)) + b := tx.Bucket([]byte(tableName)) + err = b.Put([]byte(key), jsonString) + return err + }) + 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 { + b := tx.Bucket([]byte(tableName)) + v := b.Get([]byte(key)) + json.Unmarshal(v, &assignee) + return nil + }) + return err +} + +func (d *Database) KeyExists(tableName string, key string) bool { + resultIsNil := false + err := d.Db.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(tableName)) + v := b.Get([]byte(key)) + if v == nil { + resultIsNil = true + } + return nil + }) + + if err != nil { + return false + } else { + if resultIsNil { + return false + } else { + return true + } + } +} + +/* + 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 { + return errors.New("Operation rejected in ReadOnly mode") + } + + err := d.Db.Update(func(tx *bolt.Tx) error { + tx.Bucket([]byte(tableName)).Delete([]byte(key)) + return nil + }) + + 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) { + var results [][][]byte + err := d.Db.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(tableName)) + c := b.Cursor() + + for k, v := c.First(); k != nil; k, v = c.Next() { + results = append(results, [][]byte{k, v}) + } + return nil + }) + return results, err +} + +func (d *Database) Close() { + d.Db.Close() + return +} diff --git a/mod/database/doc.txt b/mod/database/doc.txt new file mode 100644 index 0000000..a1cf3fc --- /dev/null +++ b/mod/database/doc.txt @@ -0,0 +1,44 @@ + +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); + diff --git a/mod/dynamicproxy/dpcore/LICENSE b/mod/dynamicproxy/dpcore/LICENSE new file mode 100644 index 0000000..3e055f0 --- /dev/null +++ b/mod/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. diff --git a/mod/dynamicproxy/dpcore/dpcore.go b/mod/dynamicproxy/dpcore/dpcore.go new file mode 100644 index 0000000..af0f7a8 --- /dev/null +++ b/mod/dynamicproxy/dpcore/dpcore.go @@ -0,0 +1,416 @@ +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 + + Verbal bool +} + +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, + Verbal: false, + } +} + +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 { + if p.Verbal { + 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 { + if p.Verbal { + 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 { + if p.Verbal { + p.logf("http: proxy error: %v", err) + } + return err + } + + proxyConn, err := net.Dial("tcp", req.URL.Host) + if err != nil { + if p.Verbal { + 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 { + if p.Verbal { + p.logf("http: proxy error: %v", err) + } + return err + } + + err = proxyConn.SetDeadline(deadline) + if err != nil { + if p.Verbal { + 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 { + if p.Verbal { + 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 + } +} diff --git a/mod/dynamicproxy/dynamicproxy.go b/mod/dynamicproxy/dynamicproxy.go new file mode 100644 index 0000000..555283c --- /dev/null +++ b/mod/dynamicproxy/dynamicproxy.go @@ -0,0 +1,216 @@ +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) + } +} diff --git a/mod/dynamicproxy/proxyRequestHandler.go b/mod/dynamicproxy/proxyRequestHandler.go new file mode 100644 index 0000000..a4397dc --- /dev/null +++ b/mod/dynamicproxy/proxyRequestHandler.go @@ -0,0 +1,99 @@ +package dynamicproxy + +import ( + "log" + "net/http" + "net/url" + + "imuslab.com/arozos/ReverseProxy/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 { + thisProxyEndpoint := value.(*ProxyEndpoint) + targetProxyEndpoint = thisProxyEndpoint + } + return true + }) + + 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):] + } + 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) + r.URL, _ = url.Parse(rewriteURL) + r.Header.Set("X-Forwarded-Host", r.Host) + 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:] != "/" { + wsRedirectionEndpoint = wsRedirectionEndpoint + "/" + } + u, _ := url.Parse("ws://" + wsRedirectionEndpoint + r.URL.String()) + if target.RequireTLS { + u, _ = url.Parse("wss://" + wsRedirectionEndpoint + r.URL.String()) + } + 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()) + } +} diff --git a/mod/dynamicproxy/subdomain.go b/mod/dynamicproxy/subdomain.go new file mode 100644 index 0000000..7682fbc --- /dev/null +++ b/mod/dynamicproxy/subdomain.go @@ -0,0 +1,44 @@ +package dynamicproxy + +import ( + "log" + "net/url" + + "imuslab.com/arozos/ReverseProxy/mod/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 +} diff --git a/mod/reverseproxy/LICENSE b/mod/reverseproxy/LICENSE new file mode 100644 index 0000000..3e055f0 --- /dev/null +++ b/mod/reverseproxy/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. diff --git a/mod/reverseproxy/README.md b/mod/reverseproxy/README.md new file mode 100644 index 0000000..278675e --- /dev/null +++ b/mod/reverseproxy/README.md @@ -0,0 +1,68 @@ +# Introduction +A minimalist proxy library for go, inspired by `net/http/httputil` and add support for HTTPS using HTTP Tunnel + +Support cancels an in-flight request by closing it's connection + +# Installation +```sh +go get github.com/cssivision/reverseproxy +``` + +# Usage + +## A simple proxy +```go +package main + +import ( + "net/http" + "net/url" + "github.com/cssivision/reverseproxy" +) + +func main() { + http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path, err := url.Parse("https://github.com") + if err != nil { + panic(err) + return + } + proxy := reverseproxy.NewReverseProxy(path) + proxy.ServeHTTP(w, r) + })) +} +``` + +## Use as a proxy server + +To use proxy server, you should set browser to use the proxy server as an HTTP proxy. + +```go +package main + +import ( + "net/http" + "net/url" + "github.com/cssivision/reverseproxy" +) + +func main() { + http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path, err := url.Parse("http://" + r.Host) + if err != nil { + panic(err) + return + } + + proxy := reverseproxy.NewReverseProxy(path) + proxy.ServeHTTP(w, r) + + // Specific for HTTP and HTTPS + // if r.Method == "CONNECT" { + // proxy.ProxyHTTPS(w, r) + // } else { + // proxy.ProxyHTTP(w, r) + // } + })) +} +``` diff --git a/mod/reverseproxy/reverse.go b/mod/reverseproxy/reverse.go new file mode 100644 index 0000000..7d7e0db --- /dev/null +++ b/mod/reverseproxy/reverse.go @@ -0,0 +1,405 @@ +package reverseproxy + +import ( + "errors" + "io" + "log" + "net" + "net/http" + "net/url" + "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 + + Verbal bool +} + +type requestCanceler interface { + CancelRequest(req *http.Request) +} + +// NewReverseProxy returns a new ReverseProxy that routes +// URLs to the scheme, host, and base path provided in target. If the +// target's path is "/base" and the incoming request was for "/dir", +// the target request will be for /base/dir. if the target's query is a=10 +// and the incoming request's query is b=100, the target's request's query +// will be a=10&b=100. +// NewReverseProxy does not rewrite the Host header. +// To rewrite Host headers, use ReverseProxy directly with a custom +// Director policy. +func NewReverseProxy(target *url.URL) *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, Verbal: false} +} + +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 { + if p.Verbal { + 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 { + if p.Verbal { + p.logf("http: proxy error: %v", err) + } + rw.WriteHeader(http.StatusBadGateway) + return err + } + } + + // 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 { + if p.Verbal { + p.logf("http: proxy error: %v", err) + } + return err + } + + proxyConn, err := net.Dial("tcp", req.URL.Host) + if err != nil { + if p.Verbal { + 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 { + if p.Verbal { + p.logf("http: proxy error: %v", err) + } + return err + } + + err = proxyConn.SetDeadline(deadline) + if err != nil { + if p.Verbal { + 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 { + if p.Verbal { + 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 + } +} diff --git a/mod/websocketproxy/LICENSE.md b/mod/websocketproxy/LICENSE.md new file mode 100644 index 0000000..f0a2a7c --- /dev/null +++ b/mod/websocketproxy/LICENSE.md @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 Koding, Inc. + +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. diff --git a/mod/websocketproxy/README.md b/mod/websocketproxy/README.md new file mode 100644 index 0000000..526bb43 --- /dev/null +++ b/mod/websocketproxy/README.md @@ -0,0 +1,54 @@ +# WebsocketProxy [![GoDoc](https://godoc.org/github.com/koding/websocketproxy?status.svg)](https://godoc.org/github.com/koding/websocketproxy) [![Build Status](https://travis-ci.org/koding/websocketproxy.svg)](https://travis-ci.org/koding/websocketproxy) + +WebsocketProxy is an http.Handler interface build on top of +[gorilla/websocket](https://github.com/gorilla/websocket) that you can plug +into your existing Go webserver to provide WebSocket reverse proxy. + +## Install + +```bash +go get github.com/koding/websocketproxy +``` + +## Example + +Below is a simple server that proxies to the given backend URL + +```go +package main + +import ( + "flag" + "net/http" + "net/url" + + "github.com/koding/websocketproxy" +) + +var ( + flagBackend = flag.String("backend", "", "Backend URL for proxying") +) + +func main() { + u, err := url.Parse(*flagBackend) + if err != nil { + log.Fatalln(err) + } + + err = http.ListenAndServe(":80", websocketproxy.NewProxy(u)) + if err != nil { + log.Fatalln(err) + } +} +``` + +Save it as `proxy.go` and run as: + +```bash +go run proxy.go -backend ws://example.com:3000 +``` + +Now all incoming WebSocket requests coming to this server will be proxied to +`ws://example.com:3000` + + diff --git a/mod/websocketproxy/websocketproxy.go b/mod/websocketproxy/websocketproxy.go new file mode 100644 index 0000000..18c1859 --- /dev/null +++ b/mod/websocketproxy/websocketproxy.go @@ -0,0 +1,239 @@ +// Package websocketproxy is a reverse proxy for WebSocket connections. +package websocketproxy + +import ( + "fmt" + "io" + "log" + "net" + "net/http" + "net/url" + "strings" + + "github.com/gorilla/websocket" +) + +var ( + // DefaultUpgrader specifies the parameters for upgrading an HTTP + // connection to a WebSocket connection. + DefaultUpgrader = &websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + } + + // DefaultDialer is a dialer with all fields set to the default zero values. + DefaultDialer = websocket.DefaultDialer +) + +// WebsocketProxy is an HTTP Handler that takes an incoming WebSocket +// connection and proxies it to another server. +type WebsocketProxy struct { + // Director, if non-nil, is a function that may copy additional request + // headers from the incoming WebSocket connection into the output headers + // which will be forwarded to another server. + Director func(incoming *http.Request, out http.Header) + + // Backend returns the backend URL which the proxy uses to reverse proxy + // the incoming WebSocket connection. Request is the initial incoming and + // unmodified request. + Backend func(*http.Request) *url.URL + + // Upgrader specifies the parameters for upgrading a incoming HTTP + // connection to a WebSocket connection. If nil, DefaultUpgrader is used. + Upgrader *websocket.Upgrader + + // Dialer contains options for connecting to the backend WebSocket server. + // If nil, DefaultDialer is used. + Dialer *websocket.Dialer + + Verbal bool +} + +// ProxyHandler returns a new http.Handler interface that reverse proxies the +// request to the given target. +func ProxyHandler(target *url.URL) http.Handler { return NewProxy(target) } + +// NewProxy returns a new Websocket reverse proxy that rewrites the +// URL's to the scheme, host and base path provider in target. +func NewProxy(target *url.URL) *WebsocketProxy { + backend := func(r *http.Request) *url.URL { + // Shallow copy + u := *target + u.Fragment = r.URL.Fragment + u.Path = r.URL.Path + u.RawQuery = r.URL.RawQuery + return &u + } + return &WebsocketProxy{Backend: backend, Verbal: false} +} + +// ServeHTTP implements the http.Handler that proxies WebSocket connections. +func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + if w.Backend == nil { + log.Println("websocketproxy: backend function is not defined") + http.Error(rw, "internal server error (code: 1)", http.StatusInternalServerError) + return + } + + backendURL := w.Backend(req) + if backendURL == nil { + log.Println("websocketproxy: backend URL is nil") + http.Error(rw, "internal server error (code: 2)", http.StatusInternalServerError) + return + } + + dialer := w.Dialer + if w.Dialer == nil { + dialer = DefaultDialer + } + + // Pass headers from the incoming request to the dialer to forward them to + // the final destinations. + requestHeader := http.Header{} + if origin := req.Header.Get("Origin"); origin != "" { + requestHeader.Add("Origin", origin) + } + for _, prot := range req.Header[http.CanonicalHeaderKey("Sec-WebSocket-Protocol")] { + requestHeader.Add("Sec-WebSocket-Protocol", prot) + } + for _, cookie := range req.Header[http.CanonicalHeaderKey("Cookie")] { + requestHeader.Add("Cookie", cookie) + } + if req.Host != "" { + requestHeader.Set("Host", req.Host) + } + + // Pass X-Forwarded-For headers too, code below is a part of + // httputil.ReverseProxy. See http://en.wikipedia.org/wiki/X-Forwarded-For + // for more information + // TODO: use RFC7239 http://tools.ietf.org/html/rfc7239 + 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 + } + requestHeader.Set("X-Forwarded-For", clientIP) + } + + // Set the originating protocol of the incoming HTTP request. The SSL might + // be terminated on our site and because we doing proxy adding this would + // be helpful for applications on the backend. + requestHeader.Set("X-Forwarded-Proto", "http") + if req.TLS != nil { + requestHeader.Set("X-Forwarded-Proto", "https") + } + + // Enable the director to copy any additional headers it desires for + // forwarding to the remote server. + if w.Director != nil { + w.Director(req, requestHeader) + } + + // Connect to the backend URL, also pass the headers we get from the requst + // together with the Forwarded headers we prepared above. + // TODO: support multiplexing on the same backend connection instead of + // opening a new TCP connection time for each request. This should be + // optional: + // http://tools.ietf.org/html/draft-ietf-hybi-websocket-multiplexing-01 + connBackend, resp, err := dialer.Dial(backendURL.String(), requestHeader) + if err != nil { + log.Printf("websocketproxy: couldn't dial to remote backend url %s", err) + if resp != nil { + // If the WebSocket handshake fails, ErrBadHandshake is returned + // along with a non-nil *http.Response so that callers can handle + // redirects, authentication, etcetera. + if err := copyResponse(rw, resp); err != nil { + log.Printf("websocketproxy: couldn't write response after failed remote backend handshake: %s", err) + } + } else { + http.Error(rw, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable) + } + return + } + defer connBackend.Close() + + upgrader := w.Upgrader + if w.Upgrader == nil { + upgrader = DefaultUpgrader + } + + // Only pass those headers to the upgrader. + upgradeHeader := http.Header{} + if hdr := resp.Header.Get("Sec-Websocket-Protocol"); hdr != "" { + upgradeHeader.Set("Sec-Websocket-Protocol", hdr) + } + if hdr := resp.Header.Get("Set-Cookie"); hdr != "" { + upgradeHeader.Set("Set-Cookie", hdr) + } + + // Now upgrade the existing incoming request to a WebSocket connection. + // Also pass the header that we gathered from the Dial handshake. + connPub, err := upgrader.Upgrade(rw, req, upgradeHeader) + if err != nil { + log.Printf("websocketproxy: couldn't upgrade %s", err) + return + } + defer connPub.Close() + + errClient := make(chan error, 1) + errBackend := make(chan error, 1) + replicateWebsocketConn := func(dst, src *websocket.Conn, errc chan error) { + for { + msgType, msg, err := src.ReadMessage() + if err != nil { + m := websocket.FormatCloseMessage(websocket.CloseNormalClosure, fmt.Sprintf("%v", err)) + if e, ok := err.(*websocket.CloseError); ok { + if e.Code != websocket.CloseNoStatusReceived { + m = websocket.FormatCloseMessage(e.Code, e.Text) + } + } + errc <- err + dst.WriteMessage(websocket.CloseMessage, m) + break + } + err = dst.WriteMessage(msgType, msg) + if err != nil { + errc <- err + break + } + } + } + + go replicateWebsocketConn(connPub, connBackend, errClient) + go replicateWebsocketConn(connBackend, connPub, errBackend) + + var message string + select { + case err = <-errClient: + message = "websocketproxy: Error when copying from backend to client: %v" + case err = <-errBackend: + message = "websocketproxy: Error when copying from client to backend: %v" + + } + if e, ok := err.(*websocket.CloseError); !ok || e.Code == websocket.CloseAbnormalClosure { + if w.Verbal { + //Only print message on verbal mode + log.Printf(message, err) + } + + } +} + +func copyHeader(dst, src http.Header) { + for k, vv := range src { + for _, v := range vv { + dst.Add(k, v) + } + } +} + +func copyResponse(rw http.ResponseWriter, resp *http.Response) error { + copyHeader(rw.Header(), resp.Header) + rw.WriteHeader(resp.StatusCode) + defer resp.Body.Close() + + _, err := io.Copy(rw, resp.Body) + return err +} diff --git a/mod/websocketproxy/websocketproxy_test.go b/mod/websocketproxy/websocketproxy_test.go new file mode 100644 index 0000000..b90e02b --- /dev/null +++ b/mod/websocketproxy/websocketproxy_test.go @@ -0,0 +1,130 @@ +package websocketproxy + +import ( + "log" + "net/http" + "net/url" + "testing" + "time" + + "github.com/gorilla/websocket" +) + +var ( + serverURL = "ws://127.0.0.1:7777" + backendURL = "ws://127.0.0.1:8888" +) + +func TestProxy(t *testing.T) { + // websocket proxy + supportedSubProtocols := []string{"test-protocol"} + upgrader := &websocket.Upgrader{ + ReadBufferSize: 4096, + WriteBufferSize: 4096, + CheckOrigin: func(r *http.Request) bool { + return true + }, + Subprotocols: supportedSubProtocols, + } + + u, _ := url.Parse(backendURL) + proxy := NewProxy(u) + proxy.Upgrader = upgrader + + mux := http.NewServeMux() + mux.Handle("/proxy", proxy) + go func() { + if err := http.ListenAndServe(":7777", mux); err != nil { + t.Fatal("ListenAndServe: ", err) + } + }() + + time.Sleep(time.Millisecond * 100) + + // backend echo server + go func() { + mux2 := http.NewServeMux() + mux2.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + // Don't upgrade if original host header isn't preserved + if r.Host != "127.0.0.1:7777" { + log.Printf("Host header set incorrectly. Expecting 127.0.0.1:7777 got %s", r.Host) + return + } + + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Println(err) + return + } + + messageType, p, err := conn.ReadMessage() + if err != nil { + return + } + + if err = conn.WriteMessage(messageType, p); err != nil { + return + } + }) + + err := http.ListenAndServe(":8888", mux2) + if err != nil { + t.Fatal("ListenAndServe: ", err) + } + }() + + time.Sleep(time.Millisecond * 100) + + // let's us define two subprotocols, only one is supported by the server + clientSubProtocols := []string{"test-protocol", "test-notsupported"} + h := http.Header{} + for _, subprot := range clientSubProtocols { + h.Add("Sec-WebSocket-Protocol", subprot) + } + + // frontend server, dial now our proxy, which will reverse proxy our + // message to the backend websocket server. + conn, resp, err := websocket.DefaultDialer.Dial(serverURL+"/proxy", h) + if err != nil { + t.Fatal(err) + } + + // check if the server really accepted only the first one + in := func(desired string) bool { + for _, prot := range resp.Header[http.CanonicalHeaderKey("Sec-WebSocket-Protocol")] { + if desired == prot { + return true + } + } + return false + } + + if !in("test-protocol") { + t.Error("test-protocol should be available") + } + + if in("test-notsupported") { + t.Error("test-notsupported should be not recevied from the server.") + } + + // now write a message and send it to the backend server (which goes trough + // proxy..) + msg := "hello kite" + err = conn.WriteMessage(websocket.TextMessage, []byte(msg)) + if err != nil { + t.Error(err) + } + + messageType, p, err := conn.ReadMessage() + if err != nil { + t.Error(err) + } + + if messageType != websocket.TextMessage { + t.Error("incoming message type is not Text") + } + + if msg != string(p) { + t.Errorf("expecting: %s, got: %s", msg, string(p)) + } +} diff --git a/reverseproxy.go b/reverseproxy.go new file mode 100644 index 0000000..c94405c --- /dev/null +++ b/reverseproxy.go @@ -0,0 +1,201 @@ +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") + } +}