agi.http.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. package agi
  2. import (
  3. "bytes"
  4. "encoding/base64"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "net/http"
  10. "net/url"
  11. "os"
  12. "path/filepath"
  13. "strings"
  14. "time"
  15. "github.com/robertkrimen/otto"
  16. "imuslab.com/arozos/mod/agi/static"
  17. "imuslab.com/arozos/mod/info/logger"
  18. )
  19. /*
  20. AJGI HTTP Request Library
  21. This is a library for allowing AGI script to make HTTP Request from the VM
  22. Returning either the head or the body of the request
  23. In addition to the classic helpers (get / post / head / download / getb64 /
  24. getCode / redirect), the library exposes a curl-like http.request(options)
  25. function that lets a script pick the method, set arbitrary request headers,
  26. send a raw / JSON / url-encoded form / binary (base64) body, use HTTP basic
  27. auth, set a timeout and control redirect following, then read back the
  28. status code, response headers and body of the reply.
  29. Author: tobychui
  30. */
  31. // httpRequestOptions mirrors the JS options object passed to http.request. It
  32. // covers the most common parameters curl supports (method, headers, request
  33. // body in several shapes, basic auth, timeout and redirect handling).
  34. type httpRequestOptions struct {
  35. URL string `json:"url"` //Target URL (required)
  36. Method string `json:"method"` //HTTP method, default GET
  37. Headers map[string]string `json:"headers"` //Request headers to set
  38. Body string `json:"body"` //Raw text request body
  39. BodyBase64 string `json:"bodyBase64"` //Binary request body, base64 encoded
  40. Form map[string]string `json:"form"` //application/x-www-form-urlencoded body
  41. JSON json.RawMessage `json:"json"` //JSON request body (sets Content-Type)
  42. ContentType string `json:"contentType"` //Override the Content-Type header
  43. Username string `json:"username"` //HTTP basic auth username
  44. Password string `json:"password"` //HTTP basic auth password
  45. Timeout float64 `json:"timeout"` //Timeout in seconds (0 = no timeout)
  46. FollowRedirect *bool `json:"followRedirect"` //Follow 3xx redirects (default true)
  47. ResponseType string `json:"responseType"` //"text" (default) or "base64" for binary
  48. }
  49. // httpResponse is the object returned by http.request describing the reply.
  50. type httpResponse struct {
  51. Ok bool `json:"ok"` //True when the status code is in the 2xx range
  52. Status int `json:"status"` //HTTP status code
  53. StatusText string `json:"statusText"` //HTTP status line text
  54. Headers map[string][]string `json:"headers"` //Response headers
  55. Body string `json:"body"` //Response body (text, or base64 when responseType is "base64")
  56. Error string `json:"error"` //Non-empty when the request could not be completed
  57. }
  58. func (g *Gateway) HTTPLibRegister() {
  59. err := g.RegisterLib("http", g.injectHTTPFunctions)
  60. if err != nil {
  61. logger.PrintAndLog("Agi", fmt.Sprint(err), nil)
  62. os.Exit(1)
  63. }
  64. }
  65. // buildHTTPRequestBody resolves the request body and default Content-Type from
  66. // the given options. Body precedence is: bodyBase64 > form > json > body.
  67. func buildHTTPRequestBody(opt httpRequestOptions) (io.Reader, string, error) {
  68. switch {
  69. case opt.BodyBase64 != "":
  70. raw, err := base64.StdEncoding.DecodeString(opt.BodyBase64)
  71. if err != nil {
  72. return nil, "", errors.New("invalid bodyBase64: " + err.Error())
  73. }
  74. return bytes.NewReader(raw), "application/octet-stream", nil
  75. case len(opt.Form) > 0:
  76. values := url.Values{}
  77. for k, v := range opt.Form {
  78. values.Set(k, v)
  79. }
  80. return strings.NewReader(values.Encode()), "application/x-www-form-urlencoded", nil
  81. case len(opt.JSON) > 0:
  82. return bytes.NewReader(opt.JSON), "application/json", nil
  83. case opt.Body != "":
  84. return strings.NewReader(opt.Body), "", nil
  85. default:
  86. return nil, "", nil
  87. }
  88. }
  89. // doHTTPRequest builds and executes an HTTP request from the given options and
  90. // returns a populated httpResponse. Any transport-level failure is reported via
  91. // the Error field rather than as a Go error so scripts always get an object.
  92. func doHTTPRequest(opt httpRequestOptions) httpResponse {
  93. if strings.TrimSpace(opt.URL) == "" {
  94. return httpResponse{Error: "missing request url"}
  95. }
  96. method := strings.ToUpper(strings.TrimSpace(opt.Method))
  97. if method == "" {
  98. method = "GET"
  99. }
  100. body, defaultContentType, err := buildHTTPRequestBody(opt)
  101. if err != nil {
  102. return httpResponse{Error: err.Error()}
  103. }
  104. req, err := http.NewRequest(method, opt.URL, body)
  105. if err != nil {
  106. return httpResponse{Error: err.Error()}
  107. }
  108. //Apply a default Content-Type for the chosen body, then let explicit
  109. //headers / the contentType option override it.
  110. if defaultContentType != "" {
  111. req.Header.Set("Content-Type", defaultContentType)
  112. }
  113. req.Header.Set("User-Agent", "arozos-http-client/1.1")
  114. for k, v := range opt.Headers {
  115. req.Header.Set(k, v)
  116. }
  117. if strings.TrimSpace(opt.ContentType) != "" {
  118. req.Header.Set("Content-Type", opt.ContentType)
  119. }
  120. if opt.Username != "" || opt.Password != "" {
  121. req.SetBasicAuth(opt.Username, opt.Password)
  122. }
  123. client := &http.Client{}
  124. if opt.Timeout > 0 {
  125. client.Timeout = time.Duration(opt.Timeout * float64(time.Second))
  126. }
  127. if opt.FollowRedirect != nil && !*opt.FollowRedirect {
  128. client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
  129. return http.ErrUseLastResponse
  130. }
  131. }
  132. resp, err := client.Do(req)
  133. if err != nil {
  134. return httpResponse{Error: err.Error()}
  135. }
  136. defer resp.Body.Close()
  137. bodyContent, err := io.ReadAll(resp.Body)
  138. if err != nil {
  139. return httpResponse{Error: err.Error()}
  140. }
  141. bodyString := string(bodyContent)
  142. if strings.ToLower(strings.TrimSpace(opt.ResponseType)) == "base64" {
  143. bodyString = base64.StdEncoding.EncodeToString(bodyContent)
  144. }
  145. return httpResponse{
  146. Ok: resp.StatusCode >= 200 && resp.StatusCode < 300,
  147. Status: resp.StatusCode,
  148. StatusText: resp.Status,
  149. Headers: resp.Header,
  150. Body: bodyString,
  151. }
  152. }
  153. func (g *Gateway) injectHTTPFunctions(payload *static.AgiLibInjectionPayload) {
  154. vm := payload.VM
  155. u := payload.User
  156. //scriptFsh := payload.ScriptFsh
  157. //scriptPath := payload.ScriptPath
  158. w := payload.Writer
  159. //r := payload.Request
  160. //_http_request(optionsJSON) => response object as JSON string. This is the
  161. //curl-like entry point backing http.request and all method helpers.
  162. vm.Set("_http_request", func(call otto.FunctionCall) otto.Value {
  163. opt := httpRequestOptions{}
  164. optJSON := getOttoStringArg(call, 0)
  165. if s := strings.TrimSpace(optJSON); s != "" && s != "undefined" && s != "null" {
  166. if err := json.Unmarshal([]byte(optJSON), &opt); err != nil {
  167. out, _ := json.Marshal(httpResponse{Error: "invalid request options: " + err.Error()})
  168. rv, _ := vm.ToValue(string(out))
  169. return rv
  170. }
  171. }
  172. resp := doHTTPRequest(opt)
  173. out, _ := json.Marshal(resp)
  174. rv, _ := vm.ToValue(string(out))
  175. return rv
  176. })
  177. vm.Set("_http_get", func(call otto.FunctionCall) otto.Value {
  178. //Get URL from function variable
  179. url, err := call.Argument(0).ToString()
  180. if err != nil {
  181. return otto.NullValue()
  182. }
  183. //Get respond of the url
  184. res, err := http.Get(url)
  185. if err != nil {
  186. return otto.NullValue()
  187. }
  188. bodyContent, err := io.ReadAll(res.Body)
  189. if err != nil {
  190. return otto.NullValue()
  191. }
  192. returnValue, err := vm.ToValue(string(bodyContent))
  193. if err != nil {
  194. return otto.NullValue()
  195. }
  196. return returnValue
  197. })
  198. vm.Set("_http_post", func(call otto.FunctionCall) otto.Value {
  199. //Get URL from function paramter
  200. url, err := call.Argument(0).ToString()
  201. if err != nil {
  202. return otto.NullValue()
  203. }
  204. //Get JSON content from 2nd paramter
  205. sendWithPayload := true
  206. jsonContent, err := call.Argument(1).ToString()
  207. if err != nil {
  208. //Disable the payload send
  209. sendWithPayload = false
  210. }
  211. //Create the request
  212. var req *http.Request
  213. if sendWithPayload {
  214. req, _ = http.NewRequest("POST", url, bytes.NewBuffer([]byte(jsonContent)))
  215. } else {
  216. req, _ = http.NewRequest("POST", url, bytes.NewBuffer([]byte("")))
  217. }
  218. req.Header.Set("Content-Type", "application/json")
  219. req.Header.Set("User-Agent", "arozos-http-client/1.1")
  220. //Send the request
  221. client := &http.Client{}
  222. resp, err := client.Do(req)
  223. if err != nil {
  224. logger.PrintAndLog("Agi", fmt.Sprint(err), nil)
  225. return otto.NullValue()
  226. }
  227. defer resp.Body.Close()
  228. bodyContent, err := io.ReadAll(resp.Body)
  229. if err != nil {
  230. return otto.NullValue()
  231. }
  232. returnValue, _ := vm.ToValue(string(bodyContent))
  233. return returnValue
  234. })
  235. vm.Set("_http_head", func(call otto.FunctionCall) otto.Value {
  236. //Get URL from function paramter
  237. url, err := call.Argument(0).ToString()
  238. if err != nil {
  239. return otto.NullValue()
  240. }
  241. //Request the url
  242. resp, err := http.Get(url)
  243. if err != nil {
  244. return otto.NullValue()
  245. }
  246. headerKey, err := call.Argument(1).ToString()
  247. if err != nil || headerKey == "undefined" {
  248. //No headkey set. Return the whole header as JSON
  249. js, _ := json.Marshal(resp.Header)
  250. returnValue, _ := vm.ToValue(string(js))
  251. return returnValue
  252. } else {
  253. //headerkey is set. Return if exists
  254. possibleValue := resp.Header.Get(headerKey)
  255. js, _ := json.Marshal(possibleValue)
  256. returnValue, _ := vm.ToValue(string(js))
  257. return returnValue
  258. }
  259. })
  260. //Get target status code for response
  261. vm.Set("_http_code", func(call otto.FunctionCall) otto.Value {
  262. //Get URL from function paramter
  263. url, err := call.Argument(0).ToString()
  264. if err != nil {
  265. return otto.FalseValue()
  266. }
  267. req, err := http.NewRequest("GET", url, nil)
  268. if err != nil {
  269. g.RaiseError(err)
  270. return otto.FalseValue()
  271. }
  272. payload := ""
  273. client := new(http.Client)
  274. client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
  275. //Redirection. Return the target location as well
  276. dest, _ := req.Response.Location()
  277. payload = dest.String()
  278. return errors.New("Redirect")
  279. }
  280. response, err := client.Do(req)
  281. if err != nil {
  282. return otto.FalseValue()
  283. }
  284. defer client.CloseIdleConnections()
  285. vm.Run(`var _location = "` + payload + `";`)
  286. value, _ := otto.ToValue(response.StatusCode)
  287. return value
  288. })
  289. vm.Set("_http_download", func(call otto.FunctionCall) otto.Value {
  290. //Get URL from function paramter
  291. downloadURL, err := call.Argument(0).ToString()
  292. if err != nil {
  293. return otto.FalseValue()
  294. }
  295. decodedURL, _ := url.QueryUnescape(downloadURL)
  296. //Get download desintation from paramter
  297. vpath, err := call.Argument(1).ToString()
  298. if err != nil {
  299. return otto.FalseValue()
  300. }
  301. //Optional: filename paramter
  302. filename, err := call.Argument(2).ToString()
  303. if err != nil || filename == "undefined" {
  304. //Extract the filename from the url instead
  305. filename = filepath.Base(decodedURL)
  306. }
  307. //Check user acess permission
  308. if !u.CanWrite(vpath) {
  309. g.RaiseError(errors.New("Permission Denied"))
  310. return otto.FalseValue()
  311. }
  312. //Convert the vpath to realpath. Check if it exists
  313. fsh, rpath, err := static.VirtualPathToRealPath(vpath, u)
  314. if err != nil {
  315. return otto.FalseValue()
  316. }
  317. if !fsh.FileSystemAbstraction.FileExists(rpath) || !fsh.FileSystemAbstraction.IsDir(rpath) {
  318. g.RaiseError(errors.New(vpath + " is a file not a directory."))
  319. return otto.FalseValue()
  320. }
  321. downloadDest := filepath.Join(rpath, filename)
  322. //Ok. Download the file
  323. resp, err := http.Get(decodedURL)
  324. if err != nil {
  325. return otto.FalseValue()
  326. }
  327. defer resp.Body.Close()
  328. // Create the file
  329. err = fsh.FileSystemAbstraction.WriteStream(downloadDest, resp.Body, 0775)
  330. if err != nil {
  331. return otto.FalseValue()
  332. }
  333. return otto.TrueValue()
  334. })
  335. vm.Set("_http_getb64", func(call otto.FunctionCall) otto.Value {
  336. //Get URL from function variable and return bytes as base64
  337. url, err := call.Argument(0).ToString()
  338. if err != nil {
  339. return otto.NullValue()
  340. }
  341. //Get respond of the url
  342. res, err := http.Get(url)
  343. if err != nil {
  344. return otto.NullValue()
  345. }
  346. bodyContent, err := io.ReadAll(res.Body)
  347. if err != nil {
  348. return otto.NullValue()
  349. }
  350. sEnc := base64.StdEncoding.EncodeToString(bodyContent)
  351. r, err := otto.ToValue(string(sEnc))
  352. if err != nil {
  353. logger.PrintAndLog("Agi", err.Error(), nil)
  354. return otto.NullValue()
  355. }
  356. return r
  357. })
  358. vm.Set("_http_redirect", func(call otto.FunctionCall) otto.Value {
  359. //Redirect the current request to another url
  360. targetUrl, err := call.Argument(0).ToString()
  361. if err != nil {
  362. return otto.NullValue()
  363. }
  364. statusCode, err := call.Argument(1).ToInteger()
  365. if err != nil {
  366. //Default: Temporary redirect
  367. statusCode = 307
  368. }
  369. w.Header().Set("Location", targetUrl)
  370. w.WriteHeader(int(statusCode))
  371. return otto.TrueValue()
  372. })
  373. //Wrap all the native code function into an http class
  374. vm.Run(`
  375. var http = {};
  376. //http.request(options) => response object {ok, status, statusText, headers, body, error}
  377. //options: {url, method, headers, body, bodyBase64, form, json, contentType,
  378. // username, password, timeout, followRedirect, responseType}
  379. http.request = function(options){
  380. return JSON.parse(_http_request(JSON.stringify(options || {})));
  381. };
  382. //Classic helpers. http.get / http.post now accept optional headers.
  383. http.get = function(url, headers){
  384. if (typeof headers == "undefined"){
  385. //Backward-compatible fast path: returns body string (or null on error)
  386. return _http_get(url);
  387. }
  388. return http.request({url: url, method: "GET", headers: headers}).body;
  389. };
  390. http.post = function(url, body, headers, contentType){
  391. if (typeof headers == "undefined" && typeof contentType == "undefined"){
  392. //Backward-compatible fast path: JSON body, returns body string
  393. return _http_post(url, body);
  394. }
  395. return http.request({
  396. url: url, method: "POST", body: body,
  397. headers: headers, contentType: contentType
  398. }).body;
  399. };
  400. //Method + body-shape convenience helpers built on http.request.
  401. http.put = function(url, body, headers, contentType){
  402. return http.request({url: url, method: "PUT", body: body, headers: headers, contentType: contentType});
  403. };
  404. http.patch = function(url, body, headers, contentType){
  405. return http.request({url: url, method: "PATCH", body: body, headers: headers, contentType: contentType});
  406. };
  407. http.delete = function(url, headers){
  408. return http.request({url: url, method: "DELETE", headers: headers});
  409. };
  410. http.postForm = function(url, form, headers){
  411. return http.request({url: url, method: "POST", form: form, headers: headers});
  412. };
  413. http.postJSON = function(url, obj, headers){
  414. return http.request({url: url, method: "POST", json: obj, headers: headers});
  415. };
  416. http.head = _http_head;
  417. http.download = _http_download;
  418. http.getb64 = _http_getb64;
  419. http.getCode = _http_code;
  420. http.redirect = function(t, c){
  421. if (typeof(c) == "undefined"){
  422. c = 307;
  423. }
  424. _http_redirect(t,c);
  425. };
  426. `)
  427. }