123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586 |
- package subservice
- import (
- "encoding/json"
- "errors"
- "io/ioutil"
- "log"
- "net/http"
- "net/url"
- "os"
- "os/exec"
- "path/filepath"
- "runtime"
- "sort"
- "strconv"
- "strings"
- "time"
- modules "imuslab.com/arozos/mod/modules"
- "imuslab.com/arozos/mod/network/reverseproxy"
- "imuslab.com/arozos/mod/network/websocketproxy"
- user "imuslab.com/arozos/mod/user"
- )
- /*
- ArOZ Online System - Dynamic Subsystem loading services
- author: tobychui
- This module load in ArOZ Online Subservice using authorized reverse proxy channel.
- Please see the demo subservice module for more information on implementing a subservice module.
- */
- type SubService struct {
- Port int //Port that this subservice use
- ServiceDir string //The directory where the service is located
- Path string //Path that this subservice is located
- RpEndpoint string //Reverse Proxy Endpoint
- ProxyHandler *reverseproxy.ReverseProxy //Reverse Proxy Object
- Info modules.ModuleInfo //Module information for this subservice
- Process *exec.Cmd //The CMD runtime object of the process
- }
- type SubServiceRouter struct {
- ReservePaths []string
- RunningSubService []SubService
- BasePort int
- listenPort int
- userHandler *user.UserHandler
- moduleHandler *modules.ModuleHandler
- }
- func NewSubServiceRouter(ReservePaths []string, basePort int, userHandler *user.UserHandler, moduleHandler *modules.ModuleHandler, parentPort int) *SubServiceRouter {
- return &SubServiceRouter{
- ReservePaths: ReservePaths,
- RunningSubService: []SubService{},
- BasePort: basePort,
- listenPort: parentPort,
- userHandler: userHandler,
- moduleHandler: moduleHandler,
- }
- }
- //Load and start all the subservices inside this rootpath
- func (sr *SubServiceRouter) LoadSubservicesFromRootPath(rootpath string) {
- scanningPath := filepath.ToSlash(filepath.Clean(rootpath)) + "/*"
- subservices, _ := filepath.Glob(scanningPath)
- for _, servicePath := range subservices {
- if !fileExists(servicePath + "/.disabled") {
- //Only enable module with no suspended config file
- err := sr.Launch(servicePath, true)
- if err != nil {
- log.Println(err)
- }
- }
- }
- }
- func (sr *SubServiceRouter) Launch(servicePath string, startupMode bool) error {
- //Get the executable name from its path
- binaryname := filepath.Base(servicePath)
- serviceRoot := filepath.Base(servicePath)
- binaryExecPath := filepath.ToSlash(binaryname)
- if runtime.GOOS == "windows" {
- binaryExecPath = binaryExecPath + ".exe"
- } else {
- binaryExecPath = binaryExecPath + "_" + runtime.GOOS + "_" + runtime.GOARCH
- }
- if runtime.GOOS == "windows" && !fileExists(servicePath+"/"+binaryExecPath) {
- if startupMode {
- log.Println("Failed to load subservice: "+serviceRoot, " File not exists "+servicePath+"/"+binaryExecPath+". Skipping this service")
- return errors.New("Failed to load subservice")
- } else {
- return errors.New("Failed to load subservice")
- }
- } else if runtime.GOOS == "linux" {
- //Check if service installed using which
- cmd := exec.Command("which", serviceRoot)
- searchResults, _ := cmd.CombinedOutput()
- if len(strings.TrimSpace(string(searchResults))) == 0 {
- //This is not installed. Check if it exists as a binary (aka ./myservice)
- if !fileExists(servicePath + "/" + binaryExecPath) {
- if startupMode {
- log.Println("Package not installed. " + serviceRoot)
- return errors.New("Failed to load subservice: Package not installed")
- } else {
- return errors.New("Package not installed.")
- }
- }
- }
- } else if runtime.GOOS == "darwin" {
- //Skip the whereis approach that linux use
- if !fileExists(servicePath + "/" + binaryExecPath) {
- log.Println("Failed to load subservice: "+serviceRoot, " File not exists "+servicePath+"/"+binaryExecPath+". Skipping this service")
- return errors.New("Failed to load subservice")
- }
- }
- //Check if the suspend file exists. If yes, clear it
- if fileExists(servicePath + "/.disabled") {
- os.Remove(servicePath + "/.disabled")
- }
- //Check if there are config files that replace the -info tag. If yes, use it instead.
- out := []byte{}
- if fileExists(servicePath + "/moduleInfo.json") {
- launchConfig, err := ioutil.ReadFile(servicePath + "/moduleInfo.json")
- if err != nil {
- if startupMode {
- log.Fatal("Failed to read moduleInfo.json: "+binaryname, err)
- } else {
- return errors.New("Failed to read moduleInfo.json: " + binaryname)
- }
- }
- out = launchConfig
- } else {
- infocmd := exec.Command(servicePath+"/"+binaryExecPath, "-info")
- launchConfig, err := infocmd.CombinedOutput()
- if err != nil {
- log.Println("*Subservice* startup flag -info return no JSON string and moduleInfo.json does not exists.")
- if startupMode {
- log.Fatal("Unable to start service: "+binaryname, err)
- } else {
- return errors.New("Unable to start service: " + binaryname)
- }
- }
- out = launchConfig
- }
- //Clean the module info and append it into the module list
- serviceLaunchInfo := strings.TrimSpace(string(out))
- thisModuleInfo := modules.ModuleInfo{}
- err := json.Unmarshal([]byte(serviceLaunchInfo), &thisModuleInfo)
- if err != nil {
- if startupMode {
- log.Fatal("Failed to load subservice: "+serviceRoot+"\n", err.Error())
- } else {
- return errors.New("Failed to load subservice: " + serviceRoot)
- }
- }
- var thisSubService SubService
- if fileExists(servicePath + "/.noproxy") {
- //Adaptive mode. This is designed for modules that do not designed with ArOZ Online in mind.
- //Ignore proxy setup and startup the application
- absolutePath, _ := filepath.Abs(servicePath + "/" + binaryExecPath)
- if fileExists(servicePath + "/.startscript") {
- initPath := servicePath + "/start.sh"
- if runtime.GOOS == "windows" {
- initPath = servicePath + "/start.bat"
- }
- if !fileExists(initPath) {
- if startupMode {
- log.Fatal("start.sh not found. Unable to startup service " + serviceRoot)
- } else {
- return errors.New("start.sh not found. Unable to startup service " + serviceRoot)
- }
- }
- absolutePath, _ = filepath.Abs(initPath)
- }
- cmd := exec.Command(absolutePath)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- cmd.Dir = filepath.ToSlash(servicePath + "/")
- //Spawn a new go routine to run this subservice
- go func(cmdObject *exec.Cmd) {
- if err := cmd.Start(); err != nil {
- panic(err)
- }
- }(cmd)
- //Create the servie object
- thisSubService = SubService{
- Path: binaryExecPath,
- Info: thisModuleInfo,
- ServiceDir: serviceRoot,
- Process: cmd,
- }
- log.Println("[Subservice] Starting service " + serviceRoot + " in compatibility mode.")
- } else {
- //Create a proxy for this service
- //Get proxy endpoint from startDir dir
- rProxyEndpoint := filepath.Dir(thisModuleInfo.StartDir)
- //Check if this path is reversed
- if stringInSlice(rProxyEndpoint, sr.ReservePaths) || rProxyEndpoint == "" {
- if startupMode {
- log.Fatal(serviceRoot + " service try to request system reserve path as Reverse Proxy endpoint.")
- } else {
- return errors.New(serviceRoot + " service try to request system reserve path as Reverse Proxy endpoint.")
- }
- }
- //Assign a port for this subservice
- thisServicePort := sr.GetNextUsablePort()
- //Run the subservice with the given port
- absolutePath, _ := filepath.Abs(servicePath + "/" + binaryExecPath)
- if fileExists(servicePath + "/.startscript") {
- initPath := servicePath + "/start.sh"
- if runtime.GOOS == "windows" {
- initPath = servicePath + "/start.bat"
- }
- if !fileExists(initPath) {
- if startupMode {
- log.Fatal("start.sh not found. Unable to startup service " + serviceRoot)
- } else {
- return errors.New(serviceRoot + "start.sh not found. Unable to startup service " + serviceRoot)
- }
- }
- absolutePath, _ = filepath.Abs(initPath)
- }
- servicePort := ":" + intToString(thisServicePort)
- if fileExists(filepath.Join(servicePath, "/.intport")) {
- servicePort = intToString(thisServicePort)
- }
- cmd := exec.Command(absolutePath, "-port", servicePort, "-rpt", "http://localhost:"+intToString(sr.listenPort)+"/api/ajgi/interface")
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- cmd.Dir = filepath.ToSlash(servicePath + "/")
- //log.Println(cmd.Dir,binaryExecPath)
- //Spawn a new go routine to run this subservice
- go func(cmdObject *exec.Cmd) {
- if err := cmd.Start(); err != nil {
- panic(err)
- }
- }(cmd)
- //Create a subservice object for this subservice
- thisSubService = SubService{
- Port: thisServicePort,
- Path: binaryExecPath,
- ServiceDir: serviceRoot,
- RpEndpoint: rProxyEndpoint,
- Info: thisModuleInfo,
- Process: cmd,
- }
- //Create a new proxy object
- path, _ := url.Parse("http://localhost:" + intToString(thisServicePort))
- proxy := reverseproxy.NewReverseProxy(path)
- thisSubService.ProxyHandler = proxy
- }
- //Append this subservice into the list
- sr.RunningSubService = append(sr.RunningSubService, thisSubService)
- //Append this module into the loaded module list
- sr.moduleHandler.LoadedModule = append(sr.moduleHandler.LoadedModule, thisModuleInfo)
- return nil
- }
- func (sr *SubServiceRouter) HandleListing(w http.ResponseWriter, r *http.Request) {
- //List all subservice running in the background
- type visableInfo struct {
- Port int
- ServiceDir string
- Path string
- RpEndpoint string
- ProcessID int
- Info modules.ModuleInfo
- }
- type disabledServiceInfo struct {
- ServiceDir string
- Path string
- }
- enabled := []visableInfo{}
- disabled := []disabledServiceInfo{}
- for _, thisSubservice := range sr.RunningSubService {
- enabled = append(enabled, visableInfo{
- Port: thisSubservice.Port,
- Path: thisSubservice.Path,
- ServiceDir: thisSubservice.ServiceDir,
- RpEndpoint: thisSubservice.RpEndpoint,
- ProcessID: thisSubservice.Process.Process.Pid,
- Info: thisSubservice.Info,
- })
- }
- disabledModules, _ := filepath.Glob("subservice/*/.disabled")
- for _, modFile := range disabledModules {
- thisdsi := new(disabledServiceInfo)
- thisdsi.ServiceDir = filepath.Base(filepath.Dir(modFile))
- thisdsi.Path = filepath.Base(filepath.Dir(modFile))
- if runtime.GOOS == "windows" {
- thisdsi.Path = thisdsi.Path + ".exe"
- }
- disabled = append(disabled, *thisdsi)
- }
- jsonString, err := json.Marshal(struct {
- Enabled []visableInfo
- Disabled []disabledServiceInfo
- }{
- Enabled: enabled,
- Disabled: disabled,
- })
- if err != nil {
- log.Println(err)
- }
- sendJSONResponse(w, string(jsonString))
- }
- //Kill the subservice that is currently running
- func (sr *SubServiceRouter) HandleKillSubService(w http.ResponseWriter, r *http.Request) {
- userinfo, _ := sr.userHandler.GetUserInfoFromRequest(w, r)
- //Require admin permission
- if !userinfo.IsAdmin() {
- sendErrorResponse(w, "Permission denied")
- return
- }
- //OK. Get paramters
- serviceDir, _ := mv(r, "serviceDir", true)
- //moduleName, _ := mv(r, "moduleName", true)
- err := sr.KillSubService(serviceDir)
- if err != nil {
- sendErrorResponse(w, err.Error())
- } else {
- sendOK(w)
- }
- }
- func (sr *SubServiceRouter) HandleStartSubService(w http.ResponseWriter, r *http.Request) {
- userinfo, _ := sr.userHandler.GetUserInfoFromRequest(w, r)
- //Require admin permission
- if !userinfo.IsAdmin() {
- sendErrorResponse(w, "Permission denied")
- return
- }
- //OK. Get which dir to start
- serviceDir, _ := mv(r, "serviceDir", true)
- err := sr.StartSubService(serviceDir)
- if err != nil {
- sendErrorResponse(w, err.Error())
- } else {
- sendOK(w)
- }
- }
- //Check if the user has permission to access such proxy module
- func (sr *SubServiceRouter) CheckUserPermissionOnSubservice(ss *SubService, u *user.User) bool {
- moduleName := ss.Info.Name
- return u.GetModuleAccessPermission(moduleName)
- }
- //Check if the target is reverse proxy. If yes, return the proxy handler and the rewritten url in string
- func (sr *SubServiceRouter) CheckIfReverseProxyPath(r *http.Request) (bool, *reverseproxy.ReverseProxy, string, *SubService) {
- requestURL := r.URL.Path
- for _, subservice := range sr.RunningSubService {
- thisServiceProxyEP := subservice.RpEndpoint
- if thisServiceProxyEP != "" {
- if len(requestURL) > len(thisServiceProxyEP)+1 && requestURL[1:len(thisServiceProxyEP)+1] == thisServiceProxyEP {
- //This is a proxy path. Generate the rewrite URL
- //Get all GET paramters from URL
- values := r.URL.Query()
- counter := 0
- parsedGetTail := ""
- for k, v := range values {
- if counter == 0 {
- parsedGetTail = "?" + k + "=" + url.QueryEscape(v[0])
- } else {
- parsedGetTail = parsedGetTail + "&" + k + "=" + url.QueryEscape(v[0])
- }
- counter++
- }
- return true, subservice.ProxyHandler, requestURL[len(thisServiceProxyEP)+1:] + parsedGetTail, &subservice
- }
- }
- }
- return false, nil, "", &SubService{}
- }
- func (sr *SubServiceRouter) Close() {
- //Handle shutdown of subprocesses. Kill all of them
- for _, subservice := range sr.RunningSubService {
- cmd := subservice.Process
- if cmd != nil {
- if runtime.GOOS == "windows" {
- //Force kill with the power of CMD
- kill := exec.Command("TASKKILL", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid))
- //kill.Stderr = os.Stderr
- //kill.Stdout = os.Stdout
- kill.Run()
- } else {
- //Send sigkill to process
- cmd.Process.Kill()
- }
- }
- }
- }
- func (sr *SubServiceRouter) KillSubService(serviceDir string) error {
- //Remove them from the system
- ssi := -1
- moduleName := ""
- for i, ss := range sr.RunningSubService {
- if ss.ServiceDir == serviceDir {
- ssi = i
- moduleName = ss.Info.Name
- //Kill the module cmd
- cmd := ss.Process
- if cmd != nil {
- if runtime.GOOS == "windows" {
- //Force kill with the power of CMD
- kill := exec.Command("TASKKILL", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid))
- kill.Run()
- } else {
- err := cmd.Process.Kill()
- if err != nil {
- return err
- }
- }
- }
- //Write a suspended file into the module
- ioutil.WriteFile("subservice/"+ss.ServiceDir+"/.disabled", []byte(""), 0755)
- }
- }
- //Pop this service from running Subservice
- if ssi != -1 {
- i := ssi
- copy(sr.RunningSubService[i:], sr.RunningSubService[i+1:])
- sr.RunningSubService = sr.RunningSubService[:len(sr.RunningSubService)-1]
- }
- //Pop the related module from the loadedModule list
- mi := -1
- for i, m := range sr.moduleHandler.LoadedModule {
- if m.Name == moduleName {
- mi = i
- }
- }
- if mi != -1 {
- i := mi
- copy(sr.moduleHandler.LoadedModule[i:], sr.moduleHandler.LoadedModule[i+1:])
- sr.moduleHandler.LoadedModule = sr.moduleHandler.LoadedModule[:len(sr.moduleHandler.LoadedModule)-1]
- }
- return nil
- }
- func (sr *SubServiceRouter) StartSubService(serviceDir string) error {
- if fileExists("subservice/" + serviceDir) {
- err := sr.Launch("subservice/"+serviceDir, false)
- if err != nil {
- return err
- }
- } else {
- return errors.New("Subservice directory not exists.")
- }
- //Sort the list
- sort.Slice(sr.moduleHandler.LoadedModule, func(i, j int) bool {
- return sr.moduleHandler.LoadedModule[i].Name < sr.moduleHandler.LoadedModule[j].Name
- })
- sort.Slice(sr.RunningSubService, func(i, j int) bool {
- return sr.RunningSubService[i].Info.Name < sr.RunningSubService[j].Info.Name
- })
- return nil
- }
- //Get a list of subservice roots in realpath
- func (sr *SubServiceRouter) GetSubserviceRoot() []string {
- subserviceRoots := []string{}
- for _, subService := range sr.RunningSubService {
- subserviceRoots = append(subserviceRoots, subService.Path)
- }
- return subserviceRoots
- }
- //Scan and get the next avaible port for subservice from its basePort
- func (sr *SubServiceRouter) GetNextUsablePort() int {
- basePort := sr.BasePort
- for sr.CheckIfPortInUse(basePort) {
- basePort++
- }
- return basePort
- }
- func (sr *SubServiceRouter) CheckIfPortInUse(port int) bool {
- for _, service := range sr.RunningSubService {
- if service.Port == port {
- return true
- }
- }
- return false
- }
- func (sr *SubServiceRouter) HandleRoutingRequest(w http.ResponseWriter, r *http.Request, proxy *reverseproxy.ReverseProxy, subserviceObject *SubService, rewriteURL string) {
- u, _ := sr.userHandler.GetUserInfoFromRequest(w, r)
- if !sr.CheckUserPermissionOnSubservice(subserviceObject, u) {
- //Permission denied
- http.NotFound(w, r)
- return
- }
- //Perform reverse proxy serving
- r.URL, _ = url.Parse(rewriteURL)
- token, _ := sr.userHandler.GetAuthAgent().NewTokenFromRequest(w, r)
- r.Header.Set("aouser", u.Username)
- r.Header.Set("aotoken", token)
- 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")
- u, _ := url.Parse("ws://localhost:" + strconv.Itoa(subserviceObject.Port) + r.URL.String())
- wspHandler := websocketproxy.NewProxy(u)
- wspHandler.ServeHTTP(w, r)
- return
- }
- r.Host = r.URL.Host
- err := proxy.ServeHTTP(w, r)
- if err != nil {
- //Check if it is cancelling events.
- if !strings.Contains(err.Error(), "cancel") {
- log.Println(subserviceObject.Info.Name + " IS NOT RESPONDING!")
- sr.RestartSubService(subserviceObject)
- }
- }
- }
- //Handle fail start over when the remote target is not responding
- func (sr *SubServiceRouter) RestartSubService(ss *SubService) {
- go func(ss *SubService) {
- //Kill the original subservice
- sr.KillSubService(ss.ServiceDir)
- log.Println("RESTARTING SUBSERVICE " + ss.Info.Name + " IN 10 SECOUNDS")
- time.Sleep(10000 * time.Millisecond)
- sr.StartSubService(ss.ServiceDir)
- log.Println("SUBSERVICE " + ss.Info.Name + " RESTARTED")
- }(ss)
- }
|