handlers.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. package scheduler
  2. import (
  3. "encoding/json"
  4. "net/http"
  5. "os"
  6. "path/filepath"
  7. "strconv"
  8. "strings"
  9. "time"
  10. "imuslab.com/arozos/mod/utils"
  11. )
  12. // List all the jobs related to the given user
  13. func (a *Scheduler) HandleListJobs(w http.ResponseWriter, r *http.Request) {
  14. userinfo, err := a.options.UserHandler.GetUserInfoFromRequest(w, r)
  15. if err != nil {
  16. utils.SendErrorResponse(w, "User not logged in")
  17. return
  18. }
  19. //Get username from user info
  20. username := userinfo.Username
  21. //Check if the user request list all
  22. listAll := false
  23. la, _ := utils.GetPara(r, "listall")
  24. if la == "true" && userinfo.IsAdmin() {
  25. listAll = true
  26. }
  27. //Find the scheduled task that belongs to this user
  28. userCreatedJobs := []*Job{}
  29. for _, thisJob := range a.jobs {
  30. if listAll {
  31. //List all the user jobs.
  32. userCreatedJobs = append(userCreatedJobs, thisJob)
  33. } else {
  34. //Only list user's job
  35. if thisJob.Creator == username {
  36. userCreatedJobs = append(userCreatedJobs, thisJob)
  37. }
  38. }
  39. }
  40. //Return the values as json
  41. js, _ := json.Marshal(userCreatedJobs)
  42. utils.SendJSONResponse(w, string(js))
  43. }
  44. func (a *Scheduler) HandleAddJob(w http.ResponseWriter, r *http.Request) {
  45. userinfo, err := a.options.UserHandler.GetUserInfoFromRequest(w, r)
  46. if err != nil {
  47. utils.SendErrorResponse(w, "User not logged in")
  48. return
  49. }
  50. // Check if the user has permission to create cron jobs
  51. if !userinfo.CanCreateCronJob() {
  52. utils.SendErrorResponse(w, "Permission Denied: your account does not have cron job creation permission")
  53. return
  54. }
  55. //Get required paramaters
  56. taskName, err := utils.PostPara(r, "name")
  57. if err != nil {
  58. utils.SendErrorResponse(w, "Invalid task name")
  59. return
  60. }
  61. //Check taskname length valid
  62. if len(taskName) > 32 {
  63. utils.SendErrorResponse(w, "Task name must be shorter than 32 characters")
  64. return
  65. }
  66. //Check if the name already existsed
  67. for _, runningJob := range a.jobs {
  68. if runningJob.Name == taskName {
  69. utils.SendErrorResponse(w, "Task Name already occupied")
  70. return
  71. }
  72. }
  73. scriptpath, err := utils.PostPara(r, "path")
  74. if err != nil {
  75. utils.SendErrorResponse(w, "Invalid script path")
  76. return
  77. }
  78. //Can be empty
  79. jobDescription, _ := utils.PostPara(r, "desc")
  80. fsh, err := userinfo.GetFileSystemHandlerFromVirtualPath(scriptpath)
  81. if err != nil {
  82. utils.SendErrorResponse(w, err.Error())
  83. return
  84. }
  85. fshAbs := fsh.FileSystemAbstraction
  86. realScriptPath, err := fshAbs.VirtualPathToRealPath(scriptpath, userinfo.Username)
  87. if err != nil {
  88. utils.SendErrorResponse(w, err.Error())
  89. return
  90. }
  91. //Check if the file exists
  92. if !fshAbs.FileExists(realScriptPath) {
  93. utils.SendErrorResponse(w, "script file not exists")
  94. return
  95. }
  96. interval := int64(86400) //default 1 day in seconds
  97. intervalString, err := utils.PostPara(r, "interval")
  98. if err != nil {
  99. //Default 1 day
  100. } else {
  101. //Parse the intervalString into int
  102. intervalInt, err := strconv.ParseInt(intervalString, 10, 64)
  103. if err != nil {
  104. //Failed to parse interval to int
  105. utils.SendErrorResponse(w, "invalid interval")
  106. return
  107. }
  108. interval = intervalInt
  109. }
  110. baseUnixTime := time.Now().Unix()
  111. baseTimeString, err := utils.PostPara(r, "base")
  112. if err != nil {
  113. //Use curent timestamp as base
  114. } else {
  115. baseTimeInt, err := strconv.Atoi(baseTimeString)
  116. if err != nil {
  117. //Failed to parse interval to int
  118. utils.SendErrorResponse(w, "Invalid Base Time")
  119. return
  120. }
  121. baseUnixTime = int64(baseTimeInt)
  122. }
  123. //Create a new job
  124. newJob := Job{
  125. Name: taskName,
  126. Creator: userinfo.Username,
  127. Description: jobDescription,
  128. ExecutionInterval: int64(interval),
  129. BaseTime: alignBaseTime(baseUnixTime),
  130. ScriptVpath: scriptpath,
  131. FshID: fsh.UUID,
  132. }
  133. //Write current job lists to file
  134. a.jobs = append(a.jobs, &newJob)
  135. a.saveJobsToCronFile()
  136. //OK
  137. utils.SendOK(w)
  138. }
  139. func (a *Scheduler) HandleJobRemoval(w http.ResponseWriter, r *http.Request) {
  140. userinfo, err := a.options.UserHandler.GetUserInfoFromRequest(w, r)
  141. if err != nil {
  142. utils.SendErrorResponse(w, "User not logged in")
  143. return
  144. }
  145. //Get required paramaters
  146. taskName, err := utils.PostPara(r, "name")
  147. if err != nil {
  148. utils.SendErrorResponse(w, "Invalid task name")
  149. return
  150. }
  151. //Check if Job exists
  152. if !a.JobExists(taskName) {
  153. //Job with that name not exists
  154. utils.SendErrorResponse(w, "Job not exists")
  155. return
  156. }
  157. targetJob := a.GetScheduledJobByName(taskName)
  158. //Job exists. Check if the job is created by the user.
  159. //User can only remove job created by himself or all job is he is admin
  160. allowRemove := false
  161. if !userinfo.IsAdmin() && targetJob.Creator == userinfo.Username {
  162. allowRemove = true
  163. } else if userinfo.IsAdmin() {
  164. allowRemove = true
  165. }
  166. if !allowRemove {
  167. utils.SendErrorResponse(w, "Permission Denied")
  168. return
  169. }
  170. //Ok. Remove Job from the list
  171. a.RemoveJobFromScheduleList(taskName)
  172. //Write current job lists to file
  173. a.saveJobsToCronFile()
  174. utils.SendOK(w)
  175. }
  176. // HandleCheckPermission returns whether the current user has cron job creation permission
  177. func (a *Scheduler) HandleCheckPermission(w http.ResponseWriter, r *http.Request) {
  178. userinfo, err := a.options.UserHandler.GetUserInfoFromRequest(w, r)
  179. if err != nil {
  180. utils.SendErrorResponse(w, "User not logged in")
  181. return
  182. }
  183. type permResult struct {
  184. CanCreate bool
  185. }
  186. js, _ := json.Marshal(permResult{CanCreate: userinfo.CanCreateCronJob()})
  187. utils.SendJSONResponse(w, string(js))
  188. }
  189. // HandleAppRegisterJob allows a webapp to register a cron job on behalf of the current user.
  190. //
  191. // The script lives inside the webapp's own folder (next to init.agi), NOT in user storage.
  192. // POST params:
  193. //
  194. // appname – module folder name, e.g. "MyApp" (required)
  195. // taskname – unique task identifier, max 32 chars (required)
  196. // scriptname – filename relative to the app folder, default "cron.agi"
  197. // interval – execution interval in seconds, default 86400 (1 day)
  198. // base – base unix timestamp for interval alignment, default now
  199. // desc – optional description
  200. func (a *Scheduler) HandleAppRegisterJob(w http.ResponseWriter, r *http.Request) {
  201. userinfo, err := a.options.UserHandler.GetUserInfoFromRequest(w, r)
  202. if err != nil {
  203. utils.SendErrorResponse(w, "User not logged in")
  204. return
  205. }
  206. // Permission check
  207. if !userinfo.CanCreateCronJob() {
  208. utils.SendErrorResponse(w, "Permission Denied: your account does not have cron job creation permission")
  209. return
  210. }
  211. appName, err := utils.PostPara(r, "appname")
  212. if err != nil || appName == "" {
  213. utils.SendErrorResponse(w, "Invalid app name")
  214. return
  215. }
  216. taskName, err := utils.PostPara(r, "taskname")
  217. if err != nil || taskName == "" {
  218. utils.SendErrorResponse(w, "Invalid task name")
  219. return
  220. }
  221. if len(taskName) > 32 {
  222. utils.SendErrorResponse(w, "Task name must be shorter than 32 characters")
  223. return
  224. }
  225. // Check name uniqueness
  226. for _, runningJob := range a.jobs {
  227. if runningJob.Name == taskName {
  228. utils.SendErrorResponse(w, "Task name already occupied")
  229. return
  230. }
  231. }
  232. // Script filename relative to the app folder, default cron.agi
  233. scriptName, _ := utils.PostPara(r, "scriptname")
  234. if scriptName == "" {
  235. scriptName = "cron.agi"
  236. }
  237. // Security: reject any path that tries to escape the app folder
  238. if strings.Contains(scriptName, "..") || strings.Contains(scriptName, "/") {
  239. utils.SendErrorResponse(w, "Invalid script name: must be a plain filename inside the app folder")
  240. return
  241. }
  242. // Resolve and verify the real path on disk
  243. relPath := appName + "/" + scriptName
  244. realScriptPath := filepath.Join(WebRootBase, filepath.FromSlash(relPath))
  245. if _, statErr := os.Stat(realScriptPath); os.IsNotExist(statErr) {
  246. utils.SendErrorResponse(w, "Script not found in app folder: "+relPath)
  247. return
  248. }
  249. jobDescription, _ := utils.PostPara(r, "desc")
  250. interval := int64(86400)
  251. if intervalString, ierr := utils.PostPara(r, "interval"); ierr == nil {
  252. intervalInt, perr := strconv.ParseInt(intervalString, 10, 64)
  253. if perr != nil {
  254. utils.SendErrorResponse(w, "Invalid interval")
  255. return
  256. }
  257. interval = intervalInt
  258. }
  259. baseUnixTime := time.Now().Unix()
  260. if baseTimeString, berr := utils.PostPara(r, "base"); berr == nil {
  261. baseTimeInt, perr := strconv.Atoi(baseTimeString)
  262. if perr != nil {
  263. utils.SendErrorResponse(w, "Invalid base time")
  264. return
  265. }
  266. baseUnixTime = int64(baseTimeInt)
  267. }
  268. newJob := Job{
  269. Name: taskName,
  270. Creator: userinfo.Username,
  271. Description: jobDescription,
  272. ExecutionInterval: interval,
  273. BaseTime: alignBaseTime(baseUnixTime),
  274. ScriptVpath: relPath, // e.g. "MyApp/cron.agi"
  275. FshID: WebRootFshID,
  276. AppName: appName,
  277. }
  278. a.jobs = append(a.jobs, &newJob)
  279. a.saveJobsToCronFile()
  280. utils.SendOK(w)
  281. }
  282. // HandleAppCheckJob checks whether a specific app job is registered for the current user
  283. // GET params: appname, taskname
  284. func (a *Scheduler) HandleAppCheckJob(w http.ResponseWriter, r *http.Request) {
  285. userinfo, err := a.options.UserHandler.GetUserInfoFromRequest(w, r)
  286. if err != nil {
  287. utils.SendErrorResponse(w, "User not logged in")
  288. return
  289. }
  290. appName, _ := utils.GetPara(r, "appname")
  291. taskName, _ := utils.GetPara(r, "taskname")
  292. registered := a.AppJobExists(appName, userinfo.Username, taskName)
  293. type checkResult struct {
  294. Registered bool
  295. }
  296. js, _ := json.Marshal(checkResult{Registered: registered})
  297. utils.SendJSONResponse(w, string(js))
  298. }
  299. // HandleAppUnregisterJob removes a cron job registered by a specific app for the current user
  300. // POST params: appname, taskname
  301. func (a *Scheduler) HandleAppUnregisterJob(w http.ResponseWriter, r *http.Request) {
  302. userinfo, err := a.options.UserHandler.GetUserInfoFromRequest(w, r)
  303. if err != nil {
  304. utils.SendErrorResponse(w, "User not logged in")
  305. return
  306. }
  307. appName, err := utils.PostPara(r, "appname")
  308. if err != nil || appName == "" {
  309. utils.SendErrorResponse(w, "Invalid app name")
  310. return
  311. }
  312. taskName, err := utils.PostPara(r, "taskname")
  313. if err != nil || taskName == "" {
  314. utils.SendErrorResponse(w, "Invalid task name")
  315. return
  316. }
  317. // Find and verify ownership
  318. targetJob := a.GetScheduledJobByName(taskName)
  319. if targetJob == nil {
  320. utils.SendErrorResponse(w, "Job not found")
  321. return
  322. }
  323. if targetJob.AppName != appName {
  324. utils.SendErrorResponse(w, "Job not registered by this app")
  325. return
  326. }
  327. if targetJob.Creator != userinfo.Username && !userinfo.IsAdmin() {
  328. utils.SendErrorResponse(w, "Permission denied")
  329. return
  330. }
  331. a.RemoveJobFromScheduleList(taskName)
  332. a.saveJobsToCronFile()
  333. utils.SendOK(w)
  334. }