subservice.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. package main
  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. "github.com/cssivision/reverseproxy"
  17. )
  18. /*
  19. ArOZ Online System - Dynamic Subsystem loading services
  20. This module load in ArOZ Online Subservice using authorized reverse proxy channel.
  21. Please see the demo subservice module for more information on implementing a subservice module.
  22. */
  23. var reservePaths = []string{
  24. "web",
  25. "system",
  26. "SystemAO",
  27. "img",
  28. "STDIN",
  29. "STDOUT",
  30. "STDERR",
  31. "COM",
  32. "ws",
  33. }
  34. type subService struct {
  35. Port int //Port that this subservice use
  36. ServiceDir string //The directory where the service is located
  37. Path string //Path that this subservice is located
  38. RpEndpoint string //Reverse Proxy Endpoint
  39. ProxyHandler *reverseproxy.ReverseProxy //Reverse Proxy Object
  40. Info moduleInfo //Module information for this subservice
  41. Process *exec.Cmd //The CMD runtime object of the process
  42. }
  43. func SubserviceInit() {
  44. //If subservice is disabled, do not register endpoints
  45. if *disable_subservices {
  46. return
  47. }
  48. //Register url endpoints
  49. http.HandleFunc("/system/subservice/list", system_subservice_handleListing)
  50. http.HandleFunc("/system/subservice/kill", system_subservice_killSubservice)
  51. http.HandleFunc("/system/subservice/start", system_subservice_startSubservice)
  52. //Scan and load all subservice modules
  53. subservices, _ := filepath.Glob("./subservice/*")
  54. for _, servicePath := range subservices {
  55. if !fileExists(servicePath + "/.disabled") {
  56. //Only enable module with no suspended config file
  57. system_subservice_launch(servicePath, true)
  58. }
  59. }
  60. }
  61. //Launch a given subservice with given service path
  62. func system_subservice_launch(servicePath string, startupMode bool) error {
  63. //Get the executable name from its path
  64. binaryname := filepath.Base(servicePath)
  65. serviceRoot := filepath.Base(servicePath)
  66. binaryExecPath := filepath.ToSlash(binaryname)
  67. if runtime.GOOS == "windows" {
  68. binaryExecPath = binaryExecPath + ".exe"
  69. } else if runtime.GOOS == "linux" {
  70. if runtime.GOARCH == "arm" {
  71. binaryExecPath = binaryExecPath + "_linux_arm"
  72. } else if runtime.GOARCH == "arm64" {
  73. binaryExecPath = binaryExecPath + "_linux_arm64"
  74. } else if runtime.GOARCH == "386" {
  75. binaryExecPath = binaryExecPath + "_linux_386"
  76. } else if runtime.GOARCH == "amd64" {
  77. binaryExecPath = binaryExecPath + "_linux_amd64"
  78. }
  79. }
  80. if runtime.GOOS == "windows" && !fileExists(servicePath+"/"+binaryExecPath) {
  81. if startupMode {
  82. log.Println("Failed to load subservice: "+serviceRoot, " File not exists "+servicePath+"/"+binaryExecPath+". Skipping this service")
  83. return errors.New("Failed to load subservice")
  84. } else {
  85. return errors.New("Failed to load subservice")
  86. }
  87. } else if runtime.GOOS == "linux" {
  88. //Check if service installed using whereis
  89. cmd := exec.Command("whereis", serviceRoot)
  90. searchResults, err := cmd.CombinedOutput()
  91. if err != nil {
  92. if startupMode {
  93. log.Println("Failed to load subservice: " + serviceRoot)
  94. return errors.New("Failed to load subservice: " + err.Error())
  95. } else {
  96. return errors.New("Failed to load subservice: " + err.Error())
  97. }
  98. }
  99. searchResultsString := strings.TrimSpace(string(searchResults))
  100. whereIsInfo := strings.Split(searchResultsString, ":")
  101. if whereIsInfo[1] == "" {
  102. //This is not installed. Check if it exists as a binary (aka ./myservice)
  103. if !fileExists(servicePath + "/" + binaryExecPath) {
  104. if startupMode {
  105. log.Println("Package not installed. " + serviceRoot)
  106. return errors.New("Failed to load subservice: Package not installed")
  107. } else {
  108. return errors.New("Package not installed.")
  109. }
  110. }
  111. }
  112. }
  113. //Check if the suspend file exists. If yes, clear it
  114. if fileExists(servicePath + "/.disabled") {
  115. os.Remove(servicePath + "/.disabled")
  116. }
  117. //Check if there are config files that replace the -info tag. If yes, use it instead.
  118. out := []byte{}
  119. if fileExists(servicePath + "/moduleInfo.json") {
  120. launchConfig, err := ioutil.ReadFile(servicePath + "/moduleInfo.json")
  121. if err != nil {
  122. if startupMode {
  123. log.Fatal("Failed to read moduleInfo.json: "+binaryname, err)
  124. } else {
  125. return errors.New("Failed to read moduleInfo.json: " + binaryname)
  126. }
  127. }
  128. out = launchConfig
  129. } else {
  130. infocmd := exec.Command(servicePath+"/"+binaryExecPath, "-info")
  131. launchConfig, err := infocmd.CombinedOutput()
  132. if err != nil {
  133. if startupMode {
  134. log.Fatal("Unable to start service: "+binaryname, err)
  135. } else {
  136. return errors.New("Unable to start service: " + binaryname)
  137. }
  138. }
  139. out = launchConfig
  140. }
  141. //Clean the module info and append it into the module list
  142. serviceLaunchInfo := strings.TrimSpace(string(out))
  143. thisModuleInfo := new(moduleInfo)
  144. err := json.Unmarshal([]byte(serviceLaunchInfo), &thisModuleInfo)
  145. if err != nil {
  146. if startupMode {
  147. log.Fatal("Failed to load subservice: "+serviceRoot+"\n", err.Error())
  148. } else {
  149. return errors.New("Failed to load subservice: " + serviceRoot)
  150. }
  151. }
  152. thisSubService := new(subService)
  153. if fileExists(servicePath + "/.noproxy") {
  154. //Adaptive mode. This is designed for modules that do not designed with ArOZ Online in mind.
  155. //Ignore proxy setup and startup the application
  156. absolutePath, _ := filepath.Abs(servicePath + "/" + binaryExecPath)
  157. if fileExists(servicePath + "/.startscript") {
  158. initPath := servicePath + "/start.sh"
  159. if runtime.GOOS == "windows" {
  160. initPath = servicePath + "/start.bat"
  161. }
  162. if !fileExists(initPath) {
  163. if startupMode {
  164. log.Fatal("start.sh not found. Unable to startup service " + serviceRoot)
  165. } else {
  166. return errors.New("start.sh not found. Unable to startup service " + serviceRoot)
  167. }
  168. }
  169. absolutePath, _ = filepath.Abs(initPath)
  170. }
  171. cmd := exec.Command(absolutePath)
  172. cmd.Stdout = os.Stdout
  173. cmd.Stderr = os.Stderr
  174. cmd.Dir = filepath.ToSlash(servicePath + "/")
  175. //Spawn a new go routine to run this subservice
  176. go func(cmdObject *exec.Cmd) {
  177. if err := cmd.Start(); err != nil {
  178. panic(err)
  179. }
  180. }(cmd)
  181. //Create the servie object
  182. thisSubService.Path = binaryExecPath
  183. thisSubService.Info = *thisModuleInfo
  184. thisSubService.ServiceDir = serviceRoot
  185. thisSubService.Process = cmd
  186. log.Println("[Subservice] Starting service " + serviceRoot + " in compatibility mode.")
  187. } else {
  188. //Create a proxy for this service
  189. //Get proxy endpoint from startDir dir
  190. rProxyEndpoint := filepath.Dir(thisModuleInfo.StartDir)
  191. //Check if this path is reversed
  192. if stringInSlice(rProxyEndpoint, reservePaths) || rProxyEndpoint == "" {
  193. if startupMode {
  194. log.Fatal(serviceRoot + " service try to request system reserve path as Reverse Proxy endpoint.")
  195. } else {
  196. return errors.New(serviceRoot + " service try to request system reserve path as Reverse Proxy endpoint.")
  197. }
  198. }
  199. //Assign a port for this subservice
  200. thisServicePort := subservice_getNextUsablePort()
  201. //Run the subservice with the given port
  202. absolutePath, _ := filepath.Abs(servicePath + "/" + binaryExecPath)
  203. if fileExists(servicePath + "/.startscript") {
  204. initPath := servicePath + "/start.sh"
  205. if runtime.GOOS == "windows" {
  206. initPath = servicePath + "/start.bat"
  207. }
  208. if !fileExists(initPath) {
  209. if startupMode {
  210. log.Fatal("start.sh not found. Unable to startup service " + serviceRoot)
  211. } else {
  212. return errors.New(serviceRoot + "start.sh not found. Unable to startup service " + serviceRoot)
  213. }
  214. }
  215. absolutePath, _ = filepath.Abs(initPath)
  216. }
  217. cmd := exec.Command(absolutePath, "-port", ":"+IntToString(thisServicePort))
  218. cmd.Stdout = os.Stdout
  219. cmd.Stderr = os.Stderr
  220. cmd.Dir = filepath.ToSlash(servicePath + "/")
  221. //log.Println(cmd.Dir,binaryExecPath)
  222. //Spawn a new go routine to run this subservice
  223. go func(cmdObject *exec.Cmd) {
  224. if err := cmd.Start(); err != nil {
  225. panic(err)
  226. }
  227. }(cmd)
  228. //Create a subservice object for this subservice
  229. thisSubService.Port = thisServicePort
  230. thisSubService.Path = binaryExecPath
  231. thisSubService.ServiceDir = serviceRoot
  232. thisSubService.RpEndpoint = rProxyEndpoint
  233. thisSubService.Info = *thisModuleInfo
  234. thisSubService.Process = cmd
  235. //Create a new proxy object
  236. path, _ := url.Parse("http://localhost:" + IntToString(thisServicePort))
  237. proxy := reverseproxy.NewReverseProxy(path)
  238. thisSubService.ProxyHandler = proxy
  239. }
  240. //Append this subservice into the list
  241. runningSubServices = append(runningSubServices, *thisSubService)
  242. //Append this module into the loaded module list
  243. loadedModule = append(loadedModule, *thisModuleInfo)
  244. return nil
  245. }
  246. //Check if the target is reverse proxy. If yes, return the proxy handler and the rewritten url in string
  247. func system_subservice_checkIfReverseProxyPath(r *http.Request) (bool, *reverseproxy.ReverseProxy, string) {
  248. requestURL := r.URL.Path
  249. for _, subservice := range runningSubServices {
  250. thisServiceProxyEP := subservice.RpEndpoint
  251. if thisServiceProxyEP != "" {
  252. if len(requestURL) > len(thisServiceProxyEP)+1 && requestURL[1:len(thisServiceProxyEP)+1] == thisServiceProxyEP {
  253. //This is a proxy path. Generate the rewrite URL
  254. //Get all GET paramters from URL
  255. values := r.URL.Query()
  256. counter := 0
  257. parsedGetTail := ""
  258. for k, v := range values {
  259. if counter == 0 {
  260. parsedGetTail = "?" + k + "=" + url.QueryEscape(v[0])
  261. } else {
  262. parsedGetTail = parsedGetTail + "&" + k + "=" + url.QueryEscape(v[0])
  263. }
  264. counter++
  265. }
  266. return true, subservice.ProxyHandler, requestURL[len(thisServiceProxyEP)+1:] + parsedGetTail
  267. }
  268. }
  269. }
  270. return false, nil, ""
  271. }
  272. //Stop all the subprocess correctly
  273. func system_subservice_handleShutdown() {
  274. //Handle shutdown of subprocesses. Kill all of them
  275. for _, subservice := range runningSubServices {
  276. cmd := subservice.Process
  277. if cmd != nil {
  278. if runtime.GOOS == "windows" {
  279. //Force kill with the power of CMD
  280. kill := exec.Command("TASKKILL", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid))
  281. //kill.Stderr = os.Stderr
  282. //kill.Stdout = os.Stdout
  283. kill.Run()
  284. } else {
  285. //Send sigkill to process
  286. cmd.Process.Kill()
  287. }
  288. }
  289. }
  290. }
  291. func system_subservice_handleListing(w http.ResponseWriter, r *http.Request) {
  292. //List all subservice running in the background
  293. if !authAgent.CheckAuth(r) {
  294. sendErrorResponse(w, "User not logged in")
  295. return
  296. }
  297. type visableInfo struct {
  298. Port int
  299. ServiceDir string
  300. Path string
  301. RpEndpoint string
  302. ProcessID int
  303. Info moduleInfo
  304. }
  305. type disabledServiceInfo struct {
  306. ServiceDir string
  307. Path string
  308. }
  309. enabled := []visableInfo{}
  310. disabled := []disabledServiceInfo{}
  311. for _, thisSubservice := range runningSubServices {
  312. enabled = append(enabled, visableInfo{
  313. Port: thisSubservice.Port,
  314. Path: thisSubservice.Path,
  315. ServiceDir: thisSubservice.ServiceDir,
  316. RpEndpoint: thisSubservice.RpEndpoint,
  317. ProcessID: thisSubservice.Process.Process.Pid,
  318. Info: thisSubservice.Info,
  319. })
  320. }
  321. disabledModules, _ := filepath.Glob("subservice/*/.disabled")
  322. for _, modFile := range disabledModules {
  323. thisdsi := new(disabledServiceInfo)
  324. thisdsi.ServiceDir = filepath.Base(filepath.Dir(modFile))
  325. thisdsi.Path = filepath.Base(filepath.Dir(modFile))
  326. if runtime.GOOS == "windows" {
  327. thisdsi.Path = thisdsi.Path + ".exe"
  328. }
  329. disabled = append(disabled, *thisdsi)
  330. }
  331. jsonString, err := json.Marshal(struct {
  332. Enabled []visableInfo
  333. Disabled []disabledServiceInfo
  334. }{
  335. Enabled: enabled,
  336. Disabled: disabled,
  337. })
  338. if err != nil {
  339. log.Println(err)
  340. }
  341. sendJSONResponse(w, string(jsonString))
  342. }
  343. //Kill the subservice that is currently running
  344. func system_subservice_killSubservice(w http.ResponseWriter, r *http.Request) {
  345. //Check if user has logged in
  346. if !authAgent.CheckAuth(r) {
  347. sendErrorResponse(w, "User not logged in")
  348. return
  349. }
  350. userinfo, _ := userHandler.GetUserInfoFromRequest(w, r)
  351. //Require admin permission
  352. if !userinfo.IsAdmin() {
  353. sendErrorResponse(w, "Permission denied")
  354. return
  355. }
  356. //OK. Get paramters
  357. serviceDir, _ := mv(r, "serviceDir", true)
  358. moduleName, _ := mv(r, "moduleName", true)
  359. //Remove them from the system
  360. ssi := -1
  361. for i, ss := range runningSubServices {
  362. if ss.ServiceDir == serviceDir {
  363. ssi = i
  364. //Kill the module cmd
  365. cmd := ss.Process
  366. if cmd != nil {
  367. if runtime.GOOS == "windows" {
  368. //Force kill with the power of CMD
  369. kill := exec.Command("TASKKILL", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid))
  370. kill.Run()
  371. } else {
  372. err := cmd.Process.Kill()
  373. if err != nil {
  374. sendErrorResponse(w, err.Error())
  375. return
  376. }
  377. }
  378. }
  379. //Write a suspended file into the module
  380. ioutil.WriteFile("subservice/"+ss.ServiceDir+"/.disabled", []byte(""), 0755)
  381. }
  382. }
  383. //Pop this service from running Subservice
  384. if ssi != -1 {
  385. i := ssi
  386. copy(runningSubServices[i:], runningSubServices[i+1:])
  387. runningSubServices = runningSubServices[:len(runningSubServices)-1]
  388. }
  389. //Pop the related module from the loadedModule list
  390. mi := -1
  391. for i, m := range loadedModule {
  392. if m.Name == moduleName {
  393. mi = i
  394. }
  395. }
  396. if mi != -1 {
  397. i := mi
  398. copy(loadedModule[i:], loadedModule[i+1:])
  399. loadedModule = loadedModule[:len(loadedModule)-1]
  400. }
  401. sendOK(w)
  402. }
  403. func system_subservice_startSubservice(w http.ResponseWriter, r *http.Request) {
  404. //Check if user has logged in
  405. if !authAgent.CheckAuth(r) {
  406. sendErrorResponse(w, "User not logged in")
  407. return
  408. }
  409. userinfo, _ := userHandler.GetUserInfoFromRequest(w, r)
  410. //Require admin permission
  411. if !userinfo.IsAdmin() {
  412. sendErrorResponse(w, "Permission denied")
  413. return
  414. }
  415. //OK. Get which dir to start
  416. serviceDir, _ := mv(r, "serviceDir", true)
  417. if fileExists("subservice/" + serviceDir) {
  418. err := system_subservice_launch("subservice/"+serviceDir, false)
  419. if err != nil {
  420. sendErrorResponse(w, err.Error())
  421. return
  422. }
  423. } else {
  424. sendErrorResponse(w, "Subservice directory not exists.")
  425. }
  426. //Sort the list
  427. sort.Slice(loadedModule, func(i, j int) bool {
  428. return loadedModule[i].Name < loadedModule[j].Name
  429. })
  430. sort.Slice(runningSubServices, func(i, j int) bool {
  431. return runningSubServices[i].Info.Name < runningSubServices[j].Info.Name
  432. })
  433. sendOK(w)
  434. }
  435. //Get a list of subservice roots in realpath
  436. func subservice_getSubserviceRoots() []string{
  437. subserviceRoots := []string{}
  438. for _, subService := range runningSubServices{
  439. subserviceRoots = append(subserviceRoots, subService.Path)
  440. }
  441. return subserviceRoots
  442. }
  443. //Scan and get the next avaible port for subservice from its basePort
  444. func subservice_getNextUsablePort() int {
  445. basePort := subserviceBasePort
  446. for subservice_checkIfPortInUse(basePort) {
  447. basePort++
  448. }
  449. return basePort
  450. }
  451. func subservice_checkIfPortInUse(port int) bool {
  452. for _, service := range runningSubServices {
  453. if service.Port == port {
  454. return true
  455. }
  456. }
  457. return false
  458. }