subservice.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  1. package subservice
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "io/ioutil"
  6. "log"
  7. "net/http"
  8. "net/url"
  9. "os"
  10. "os/exec"
  11. "path/filepath"
  12. "runtime"
  13. "sort"
  14. "strconv"
  15. "strings"
  16. "time"
  17. modules "imuslab.com/arozos/mod/modules"
  18. "imuslab.com/arozos/mod/network/reverseproxy"
  19. "imuslab.com/arozos/mod/network/websocketproxy"
  20. user "imuslab.com/arozos/mod/user"
  21. )
  22. /*
  23. ArOZ Online System - Dynamic Subsystem loading services
  24. author: tobychui
  25. This module load in ArOZ Online Subservice using authorized reverse proxy channel.
  26. Please see the demo subservice module for more information on implementing a subservice module.
  27. */
  28. type SubService struct {
  29. Port int //Port that this subservice use
  30. ServiceDir string //The directory where the service is located
  31. Path string //Path that this subservice is located
  32. RpEndpoint string //Reverse Proxy Endpoint
  33. ProxyHandler *reverseproxy.ReverseProxy //Reverse Proxy Object
  34. Info modules.ModuleInfo //Module information for this subservice
  35. Process *exec.Cmd //The CMD runtime object of the process
  36. }
  37. type SubServiceRouter struct {
  38. ReservePaths []string
  39. RunningSubService []SubService
  40. BasePort int
  41. listenPort int
  42. userHandler *user.UserHandler
  43. moduleHandler *modules.ModuleHandler
  44. }
  45. func NewSubServiceRouter(ReservePaths []string, basePort int, userHandler *user.UserHandler, moduleHandler *modules.ModuleHandler, parentPort int) *SubServiceRouter {
  46. return &SubServiceRouter{
  47. ReservePaths: ReservePaths,
  48. RunningSubService: []SubService{},
  49. BasePort: basePort,
  50. listenPort: parentPort,
  51. userHandler: userHandler,
  52. moduleHandler: moduleHandler,
  53. }
  54. }
  55. //Load and start all the subservices inside this rootpath
  56. func (sr *SubServiceRouter) LoadSubservicesFromRootPath(rootpath string) {
  57. scanningPath := filepath.ToSlash(filepath.Clean(rootpath)) + "/*"
  58. subservices, _ := filepath.Glob(scanningPath)
  59. for _, servicePath := range subservices {
  60. if !fileExists(servicePath + "/.disabled") {
  61. //Only enable module with no suspended config file
  62. err := sr.Launch(servicePath, true)
  63. if err != nil {
  64. log.Println(err)
  65. }
  66. }
  67. }
  68. }
  69. func (sr *SubServiceRouter) Launch(servicePath string, startupMode bool) error {
  70. //Get the executable name from its path
  71. servicePath = filepath.ToSlash(servicePath)
  72. binaryname := filepath.Base(servicePath)
  73. serviceRoot := filepath.Base(servicePath)
  74. binaryExecPath := filepath.ToSlash(binaryname)
  75. if runtime.GOOS == "windows" {
  76. binaryExecPath = binaryExecPath + ".exe"
  77. } else {
  78. binaryExecPath = binaryExecPath + "_" + runtime.GOOS + "_" + runtime.GOARCH
  79. }
  80. //Check if startscript exists. If no, try to launch the binaries
  81. if fileExists(servicePath + "/.startscript") {
  82. //Launch from start.bat or start.sh
  83. if !(fileExists(servicePath+"/start.sh") || fileExists(servicePath+"/start.bat")) {
  84. log.Println("Failed to load subservice: " + serviceRoot + ", .startscript flag is TRUE but no start script found")
  85. return errors.New("Failed to load subservice")
  86. }
  87. startScriptName := "start.sh"
  88. if runtime.GOOS == "windows" {
  89. startScriptName = "start.bat"
  90. }
  91. binaryExecPath = startScriptName
  92. } else {
  93. //No startscript defined. Start from binary files if exists
  94. if runtime.GOOS == "windows" && !fileExists(servicePath+"/"+binaryExecPath) {
  95. if startupMode {
  96. log.Println("Failed to load subservice: "+serviceRoot, " File not exists "+servicePath+"/"+binaryExecPath+". Skipping this service")
  97. return errors.New("Failed to load subservice")
  98. } else {
  99. return errors.New("Failed to load subservice")
  100. }
  101. } else if runtime.GOOS == "linux" {
  102. //Check if service installed using which
  103. cmd := exec.Command("which", serviceRoot)
  104. searchResults, _ := cmd.CombinedOutput()
  105. if len(strings.TrimSpace(string(searchResults))) == 0 {
  106. //This is not installed. Check if it exists as a binary (aka ./myservice)
  107. if !fileExists(servicePath + "/" + binaryExecPath) {
  108. if startupMode {
  109. log.Println("Package not installed. " + serviceRoot)
  110. return errors.New("Failed to load subservice: Package not installed")
  111. } else {
  112. return errors.New("Package not installed.")
  113. }
  114. }
  115. }
  116. } else if runtime.GOOS == "darwin" {
  117. //Skip the whereis approach that linux use
  118. if !fileExists(servicePath + "/" + binaryExecPath) {
  119. log.Println("Failed to load subservice: "+serviceRoot, " File not exists "+servicePath+"/"+binaryExecPath+". Skipping this service")
  120. return errors.New("Failed to load subservice")
  121. }
  122. }
  123. }
  124. //Check if the suspend file exists. If yes, clear it
  125. if fileExists(servicePath + "/.disabled") {
  126. os.Remove(servicePath + "/.disabled")
  127. }
  128. //Check if there are config files that replace the -info tag. If yes, use it instead.
  129. out := []byte{}
  130. if fileExists(servicePath + "/moduleInfo.json") {
  131. launchConfig, err := ioutil.ReadFile(servicePath + "/moduleInfo.json")
  132. if err != nil {
  133. if startupMode {
  134. log.Fatal("Failed to read moduleInfo.json: "+binaryname, err)
  135. } else {
  136. return errors.New("Failed to read moduleInfo.json: " + binaryname)
  137. }
  138. }
  139. out = launchConfig
  140. } else {
  141. infocmd := exec.Command(servicePath+"/"+binaryExecPath, "-info")
  142. launchConfig, err := infocmd.CombinedOutput()
  143. if err != nil {
  144. log.Println("*Subservice* startup flag -info return no JSON string and moduleInfo.json does not exists.")
  145. if startupMode {
  146. log.Fatal("Unable to start service: "+binaryname, err)
  147. } else {
  148. return errors.New("Unable to start service: " + binaryname)
  149. }
  150. }
  151. out = launchConfig
  152. }
  153. //Clean the module info and append it into the module list
  154. serviceLaunchInfo := strings.TrimSpace(string(out))
  155. thisModuleInfo := modules.ModuleInfo{}
  156. err := json.Unmarshal([]byte(serviceLaunchInfo), &thisModuleInfo)
  157. if err != nil {
  158. if startupMode {
  159. log.Fatal("Failed to load subservice: "+serviceRoot+"\n", err.Error())
  160. } else {
  161. return errors.New("Failed to load subservice: " + serviceRoot)
  162. }
  163. }
  164. var thisSubService SubService
  165. if fileExists(servicePath + "/.noproxy") {
  166. //Adaptive mode. This is designed for modules that do not designed with ArOZ Online in mind.
  167. //Ignore proxy setup and startup the application
  168. absolutePath, _ := filepath.Abs(servicePath + "/" + binaryExecPath)
  169. if fileExists(servicePath + "/.startscript") {
  170. initPath := servicePath + "/start.sh"
  171. if runtime.GOOS == "windows" {
  172. initPath = servicePath + "/start.bat"
  173. }
  174. if !fileExists(initPath) {
  175. if startupMode {
  176. log.Fatal("start.sh not found. Unable to startup service " + serviceRoot)
  177. } else {
  178. return errors.New("start.sh not found. Unable to startup service " + serviceRoot)
  179. }
  180. }
  181. absolutePath, _ = filepath.Abs(initPath)
  182. }
  183. cmd := exec.Command(absolutePath)
  184. cmd.Stdout = os.Stdout
  185. cmd.Stderr = os.Stderr
  186. cmd.Dir = filepath.ToSlash(servicePath + "/")
  187. //Spawn a new go routine to run this subservice
  188. go func(cmdObject *exec.Cmd) {
  189. if err := cmd.Start(); err != nil {
  190. panic(err)
  191. }
  192. }(cmd)
  193. //Create the servie object
  194. thisSubService = SubService{
  195. Path: binaryExecPath,
  196. Info: thisModuleInfo,
  197. ServiceDir: serviceRoot,
  198. Process: cmd,
  199. }
  200. log.Println("[Subservice] Starting service " + serviceRoot + " in compatibility mode.")
  201. } else {
  202. //Create a proxy for this service
  203. //Get proxy endpoint from startDir dir
  204. rProxyEndpoint := filepath.Dir(thisModuleInfo.StartDir)
  205. //Check if this path is reversed
  206. if stringInSlice(rProxyEndpoint, sr.ReservePaths) || rProxyEndpoint == "" {
  207. if startupMode {
  208. log.Fatal(serviceRoot + " service try to request system reserve path as Reverse Proxy endpoint.")
  209. } else {
  210. return errors.New(serviceRoot + " service try to request system reserve path as Reverse Proxy endpoint.")
  211. }
  212. }
  213. //Assign a port for this subservice
  214. thisServicePort := sr.GetNextUsablePort()
  215. //Run the subservice with the given port
  216. absolutePath, _ := filepath.Abs(servicePath + "/" + binaryExecPath)
  217. if fileExists(servicePath + "/.startscript") {
  218. initPath := servicePath + "/start.sh"
  219. if runtime.GOOS == "windows" {
  220. initPath = servicePath + "/start.bat"
  221. }
  222. if !fileExists(initPath) {
  223. if startupMode {
  224. log.Fatal("start.sh not found. Unable to startup service " + serviceRoot)
  225. } else {
  226. return errors.New(serviceRoot + "start.sh not found. Unable to startup service " + serviceRoot)
  227. }
  228. }
  229. absolutePath, _ = filepath.Abs(initPath)
  230. }
  231. servicePort := ":" + intToString(thisServicePort)
  232. if fileExists(filepath.Join(servicePath, "/.intport")) {
  233. servicePort = intToString(thisServicePort)
  234. }
  235. cmd := exec.Command(absolutePath, "-port", servicePort, "-rpt", "http://localhost:"+intToString(sr.listenPort)+"/api/ajgi/interface")
  236. cmd.Stdout = os.Stdout
  237. cmd.Stderr = os.Stderr
  238. cmd.Dir = filepath.ToSlash(servicePath + "/")
  239. //log.Println(cmd.Dir,binaryExecPath)
  240. //Spawn a new go routine to run this subservice
  241. go func(cmdObject *exec.Cmd) {
  242. if err := cmd.Start(); err != nil {
  243. panic(err)
  244. }
  245. }(cmd)
  246. //Create a subservice object for this subservice
  247. thisSubService = SubService{
  248. Port: thisServicePort,
  249. Path: binaryExecPath,
  250. ServiceDir: serviceRoot,
  251. RpEndpoint: rProxyEndpoint,
  252. Info: thisModuleInfo,
  253. Process: cmd,
  254. }
  255. //Create a new proxy object
  256. path, _ := url.Parse("http://localhost:" + intToString(thisServicePort))
  257. proxy := reverseproxy.NewReverseProxy(path)
  258. thisSubService.ProxyHandler = proxy
  259. }
  260. //Append this subservice into the list
  261. sr.RunningSubService = append(sr.RunningSubService, thisSubService)
  262. //Append this module into the loaded module list
  263. sr.moduleHandler.LoadedModule = append(sr.moduleHandler.LoadedModule, thisModuleInfo)
  264. return nil
  265. }
  266. func (sr *SubServiceRouter) HandleListing(w http.ResponseWriter, r *http.Request) {
  267. //List all subservice running in the background
  268. type visableInfo struct {
  269. Port int
  270. ServiceDir string
  271. Path string
  272. RpEndpoint string
  273. ProcessID int
  274. Info modules.ModuleInfo
  275. }
  276. type disabledServiceInfo struct {
  277. ServiceDir string
  278. Path string
  279. }
  280. enabled := []visableInfo{}
  281. disabled := []disabledServiceInfo{}
  282. for _, thisSubservice := range sr.RunningSubService {
  283. enabled = append(enabled, visableInfo{
  284. Port: thisSubservice.Port,
  285. Path: thisSubservice.Path,
  286. ServiceDir: thisSubservice.ServiceDir,
  287. RpEndpoint: thisSubservice.RpEndpoint,
  288. ProcessID: thisSubservice.Process.Process.Pid,
  289. Info: thisSubservice.Info,
  290. })
  291. }
  292. disabledModules, _ := filepath.Glob("subservice/*/.disabled")
  293. for _, modFile := range disabledModules {
  294. thisdsi := new(disabledServiceInfo)
  295. thisdsi.ServiceDir = filepath.Base(filepath.Dir(modFile))
  296. thisdsi.Path = filepath.Base(filepath.Dir(modFile))
  297. if runtime.GOOS == "windows" {
  298. thisdsi.Path = thisdsi.Path + ".exe"
  299. }
  300. disabled = append(disabled, *thisdsi)
  301. }
  302. jsonString, err := json.Marshal(struct {
  303. Enabled []visableInfo
  304. Disabled []disabledServiceInfo
  305. }{
  306. Enabled: enabled,
  307. Disabled: disabled,
  308. })
  309. if err != nil {
  310. log.Println(err)
  311. }
  312. sendJSONResponse(w, string(jsonString))
  313. }
  314. //Kill the subservice that is currently running
  315. func (sr *SubServiceRouter) HandleKillSubService(w http.ResponseWriter, r *http.Request) {
  316. userinfo, _ := sr.userHandler.GetUserInfoFromRequest(w, r)
  317. //Require admin permission
  318. if !userinfo.IsAdmin() {
  319. sendErrorResponse(w, "Permission denied")
  320. return
  321. }
  322. //OK. Get paramters
  323. serviceDir, _ := mv(r, "serviceDir", true)
  324. //moduleName, _ := mv(r, "moduleName", true)
  325. err := sr.KillSubService(serviceDir)
  326. if err != nil {
  327. sendErrorResponse(w, err.Error())
  328. } else {
  329. sendOK(w)
  330. }
  331. }
  332. func (sr *SubServiceRouter) HandleStartSubService(w http.ResponseWriter, r *http.Request) {
  333. userinfo, _ := sr.userHandler.GetUserInfoFromRequest(w, r)
  334. //Require admin permission
  335. if !userinfo.IsAdmin() {
  336. sendErrorResponse(w, "Permission denied")
  337. return
  338. }
  339. //OK. Get which dir to start
  340. serviceDir, _ := mv(r, "serviceDir", true)
  341. err := sr.StartSubService(serviceDir)
  342. if err != nil {
  343. sendErrorResponse(w, err.Error())
  344. } else {
  345. sendOK(w)
  346. }
  347. }
  348. //Check if the user has permission to access such proxy module
  349. func (sr *SubServiceRouter) CheckUserPermissionOnSubservice(ss *SubService, u *user.User) bool {
  350. moduleName := ss.Info.Name
  351. return u.GetModuleAccessPermission(moduleName)
  352. }
  353. //Check if the target is reverse proxy. If yes, return the proxy handler and the rewritten url in string
  354. func (sr *SubServiceRouter) CheckIfReverseProxyPath(r *http.Request) (bool, *reverseproxy.ReverseProxy, string, *SubService) {
  355. requestURL := r.URL.Path
  356. for _, subservice := range sr.RunningSubService {
  357. thisServiceProxyEP := subservice.RpEndpoint
  358. if thisServiceProxyEP != "" {
  359. if len(requestURL) > len(thisServiceProxyEP)+1 && requestURL[1:len(thisServiceProxyEP)+1] == thisServiceProxyEP {
  360. //This is a proxy path. Generate the rewrite URL
  361. //Get all GET paramters from URL
  362. values := r.URL.Query()
  363. counter := 0
  364. parsedGetTail := ""
  365. for k, v := range values {
  366. if counter == 0 {
  367. parsedGetTail = "?" + k + "=" + url.QueryEscape(v[0])
  368. } else {
  369. parsedGetTail = parsedGetTail + "&" + k + "=" + url.QueryEscape(v[0])
  370. }
  371. counter++
  372. }
  373. return true, subservice.ProxyHandler, requestURL[len(thisServiceProxyEP)+1:] + parsedGetTail, &subservice
  374. }
  375. }
  376. }
  377. return false, nil, "", &SubService{}
  378. }
  379. func (sr *SubServiceRouter) Close() {
  380. //Handle shutdown of subprocesses. Kill all of them
  381. for _, subservice := range sr.RunningSubService {
  382. cmd := subservice.Process
  383. if cmd != nil {
  384. if runtime.GOOS == "windows" {
  385. //Force kill with the power of CMD
  386. kill := exec.Command("TASKKILL", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid))
  387. //kill.Stderr = os.Stderr
  388. //kill.Stdout = os.Stdout
  389. kill.Run()
  390. } else {
  391. //Send sigkill to process
  392. cmd.Process.Kill()
  393. }
  394. }
  395. }
  396. }
  397. func (sr *SubServiceRouter) KillSubService(serviceDir string) error {
  398. //Remove them from the system
  399. ssi := -1
  400. moduleName := ""
  401. for i, ss := range sr.RunningSubService {
  402. if ss.ServiceDir == serviceDir {
  403. ssi = i
  404. moduleName = ss.Info.Name
  405. //Kill the module cmd
  406. cmd := ss.Process
  407. if cmd != nil {
  408. if runtime.GOOS == "windows" {
  409. //Force kill with the power of CMD
  410. kill := exec.Command("TASKKILL", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid))
  411. kill.Run()
  412. } else {
  413. err := cmd.Process.Kill()
  414. if err != nil {
  415. return err
  416. }
  417. }
  418. }
  419. //Write a suspended file into the module
  420. ioutil.WriteFile("subservice/"+ss.ServiceDir+"/.disabled", []byte(""), 0755)
  421. }
  422. }
  423. //Pop this service from running Subservice
  424. if ssi != -1 {
  425. i := ssi
  426. copy(sr.RunningSubService[i:], sr.RunningSubService[i+1:])
  427. sr.RunningSubService = sr.RunningSubService[:len(sr.RunningSubService)-1]
  428. }
  429. //Pop the related module from the loadedModule list
  430. mi := -1
  431. for i, m := range sr.moduleHandler.LoadedModule {
  432. if m.Name == moduleName {
  433. mi = i
  434. }
  435. }
  436. if mi != -1 {
  437. i := mi
  438. copy(sr.moduleHandler.LoadedModule[i:], sr.moduleHandler.LoadedModule[i+1:])
  439. sr.moduleHandler.LoadedModule = sr.moduleHandler.LoadedModule[:len(sr.moduleHandler.LoadedModule)-1]
  440. }
  441. return nil
  442. }
  443. func (sr *SubServiceRouter) StartSubService(serviceDir string) error {
  444. if fileExists("subservice/" + serviceDir) {
  445. err := sr.Launch("subservice/"+serviceDir, false)
  446. if err != nil {
  447. return err
  448. }
  449. } else {
  450. return errors.New("Subservice directory not exists.")
  451. }
  452. //Sort the list
  453. sort.Slice(sr.moduleHandler.LoadedModule, func(i, j int) bool {
  454. return sr.moduleHandler.LoadedModule[i].Name < sr.moduleHandler.LoadedModule[j].Name
  455. })
  456. sort.Slice(sr.RunningSubService, func(i, j int) bool {
  457. return sr.RunningSubService[i].Info.Name < sr.RunningSubService[j].Info.Name
  458. })
  459. return nil
  460. }
  461. //Get a list of subservice roots in realpath
  462. func (sr *SubServiceRouter) GetSubserviceRoot() []string {
  463. subserviceRoots := []string{}
  464. for _, subService := range sr.RunningSubService {
  465. subserviceRoots = append(subserviceRoots, subService.Path)
  466. }
  467. return subserviceRoots
  468. }
  469. //Scan and get the next avaible port for subservice from its basePort
  470. func (sr *SubServiceRouter) GetNextUsablePort() int {
  471. basePort := sr.BasePort
  472. for sr.CheckIfPortInUse(basePort) {
  473. basePort++
  474. }
  475. return basePort
  476. }
  477. func (sr *SubServiceRouter) CheckIfPortInUse(port int) bool {
  478. for _, service := range sr.RunningSubService {
  479. if service.Port == port {
  480. return true
  481. }
  482. }
  483. return false
  484. }
  485. func (sr *SubServiceRouter) HandleRoutingRequest(w http.ResponseWriter, r *http.Request, proxy *reverseproxy.ReverseProxy, subserviceObject *SubService, rewriteURL string) {
  486. u, _ := sr.userHandler.GetUserInfoFromRequest(w, r)
  487. if !sr.CheckUserPermissionOnSubservice(subserviceObject, u) {
  488. //Permission denied
  489. http.NotFound(w, r)
  490. return
  491. }
  492. //Perform reverse proxy serving
  493. r.URL, _ = url.Parse(rewriteURL)
  494. token, _ := sr.userHandler.GetAuthAgent().NewTokenFromRequest(w, r)
  495. r.Header.Set("aouser", u.Username)
  496. r.Header.Set("aotoken", token)
  497. r.Header.Set("X-Forwarded-Host", r.Host)
  498. if r.Header["Upgrade"] != nil && r.Header["Upgrade"][0] == "websocket" {
  499. //Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
  500. r.Header.Set("A-Upgrade", "websocket")
  501. u, _ := url.Parse("ws://localhost:" + strconv.Itoa(subserviceObject.Port) + r.URL.String())
  502. wspHandler := websocketproxy.NewProxy(u)
  503. wspHandler.ServeHTTP(w, r)
  504. return
  505. }
  506. r.Host = r.URL.Host
  507. err := proxy.ServeHTTP(w, r)
  508. if err != nil {
  509. //Check if it is cancelling events.
  510. if !strings.Contains(err.Error(), "cancel") {
  511. log.Println(subserviceObject.Info.Name + " IS NOT RESPONDING!")
  512. sr.RestartSubService(subserviceObject)
  513. }
  514. }
  515. }
  516. //Handle fail start over when the remote target is not responding
  517. func (sr *SubServiceRouter) RestartSubService(ss *SubService) {
  518. go func(ss *SubService) {
  519. //Kill the original subservice
  520. sr.KillSubService(ss.ServiceDir)
  521. log.Println("RESTARTING SUBSERVICE " + ss.Info.Name + " IN 10 SECOUNDS")
  522. time.Sleep(10000 * time.Millisecond)
  523. sr.StartSubService(ss.ServiceDir)
  524. log.Println("SUBSERVICE " + ss.Info.Name + " RESTARTED")
  525. }(ss)
  526. }