| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495 |
- package agi
- import (
- "bytes"
- "encoding/base64"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "os"
- "path/filepath"
- "strings"
- "time"
- "github.com/robertkrimen/otto"
- "imuslab.com/arozos/mod/agi/static"
- "imuslab.com/arozos/mod/info/logger"
- )
- /*
- AJGI HTTP Request Library
- This is a library for allowing AGI script to make HTTP Request from the VM
- Returning either the head or the body of the request
- In addition to the classic helpers (get / post / head / download / getb64 /
- getCode / redirect), the library exposes a curl-like http.request(options)
- function that lets a script pick the method, set arbitrary request headers,
- send a raw / JSON / url-encoded form / binary (base64) body, use HTTP basic
- auth, set a timeout and control redirect following, then read back the
- status code, response headers and body of the reply.
- Author: tobychui
- */
- // httpRequestOptions mirrors the JS options object passed to http.request. It
- // covers the most common parameters curl supports (method, headers, request
- // body in several shapes, basic auth, timeout and redirect handling).
- type httpRequestOptions struct {
- URL string `json:"url"` //Target URL (required)
- Method string `json:"method"` //HTTP method, default GET
- Headers map[string]string `json:"headers"` //Request headers to set
- Body string `json:"body"` //Raw text request body
- BodyBase64 string `json:"bodyBase64"` //Binary request body, base64 encoded
- Form map[string]string `json:"form"` //application/x-www-form-urlencoded body
- JSON json.RawMessage `json:"json"` //JSON request body (sets Content-Type)
- ContentType string `json:"contentType"` //Override the Content-Type header
- Username string `json:"username"` //HTTP basic auth username
- Password string `json:"password"` //HTTP basic auth password
- Timeout float64 `json:"timeout"` //Timeout in seconds (0 = no timeout)
- FollowRedirect *bool `json:"followRedirect"` //Follow 3xx redirects (default true)
- ResponseType string `json:"responseType"` //"text" (default) or "base64" for binary
- }
- // httpResponse is the object returned by http.request describing the reply.
- type httpResponse struct {
- Ok bool `json:"ok"` //True when the status code is in the 2xx range
- Status int `json:"status"` //HTTP status code
- StatusText string `json:"statusText"` //HTTP status line text
- Headers map[string][]string `json:"headers"` //Response headers
- Body string `json:"body"` //Response body (text, or base64 when responseType is "base64")
- Error string `json:"error"` //Non-empty when the request could not be completed
- }
- func (g *Gateway) HTTPLibRegister() {
- err := g.RegisterLib("http", g.injectHTTPFunctions)
- if err != nil {
- logger.PrintAndLog("Agi", fmt.Sprint(err), nil)
- os.Exit(1)
- }
- }
- // buildHTTPRequestBody resolves the request body and default Content-Type from
- // the given options. Body precedence is: bodyBase64 > form > json > body.
- func buildHTTPRequestBody(opt httpRequestOptions) (io.Reader, string, error) {
- switch {
- case opt.BodyBase64 != "":
- raw, err := base64.StdEncoding.DecodeString(opt.BodyBase64)
- if err != nil {
- return nil, "", errors.New("invalid bodyBase64: " + err.Error())
- }
- return bytes.NewReader(raw), "application/octet-stream", nil
- case len(opt.Form) > 0:
- values := url.Values{}
- for k, v := range opt.Form {
- values.Set(k, v)
- }
- return strings.NewReader(values.Encode()), "application/x-www-form-urlencoded", nil
- case len(opt.JSON) > 0:
- return bytes.NewReader(opt.JSON), "application/json", nil
- case opt.Body != "":
- return strings.NewReader(opt.Body), "", nil
- default:
- return nil, "", nil
- }
- }
- // doHTTPRequest builds and executes an HTTP request from the given options and
- // returns a populated httpResponse. Any transport-level failure is reported via
- // the Error field rather than as a Go error so scripts always get an object.
- func doHTTPRequest(opt httpRequestOptions) httpResponse {
- if strings.TrimSpace(opt.URL) == "" {
- return httpResponse{Error: "missing request url"}
- }
- method := strings.ToUpper(strings.TrimSpace(opt.Method))
- if method == "" {
- method = "GET"
- }
- body, defaultContentType, err := buildHTTPRequestBody(opt)
- if err != nil {
- return httpResponse{Error: err.Error()}
- }
- req, err := http.NewRequest(method, opt.URL, body)
- if err != nil {
- return httpResponse{Error: err.Error()}
- }
- //Apply a default Content-Type for the chosen body, then let explicit
- //headers / the contentType option override it.
- if defaultContentType != "" {
- req.Header.Set("Content-Type", defaultContentType)
- }
- req.Header.Set("User-Agent", "arozos-http-client/1.1")
- for k, v := range opt.Headers {
- req.Header.Set(k, v)
- }
- if strings.TrimSpace(opt.ContentType) != "" {
- req.Header.Set("Content-Type", opt.ContentType)
- }
- if opt.Username != "" || opt.Password != "" {
- req.SetBasicAuth(opt.Username, opt.Password)
- }
- client := &http.Client{}
- if opt.Timeout > 0 {
- client.Timeout = time.Duration(opt.Timeout * float64(time.Second))
- }
- if opt.FollowRedirect != nil && !*opt.FollowRedirect {
- client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
- return http.ErrUseLastResponse
- }
- }
- resp, err := client.Do(req)
- if err != nil {
- return httpResponse{Error: err.Error()}
- }
- defer resp.Body.Close()
- bodyContent, err := io.ReadAll(resp.Body)
- if err != nil {
- return httpResponse{Error: err.Error()}
- }
- bodyString := string(bodyContent)
- if strings.ToLower(strings.TrimSpace(opt.ResponseType)) == "base64" {
- bodyString = base64.StdEncoding.EncodeToString(bodyContent)
- }
- return httpResponse{
- Ok: resp.StatusCode >= 200 && resp.StatusCode < 300,
- Status: resp.StatusCode,
- StatusText: resp.Status,
- Headers: resp.Header,
- Body: bodyString,
- }
- }
- func (g *Gateway) injectHTTPFunctions(payload *static.AgiLibInjectionPayload) {
- vm := payload.VM
- u := payload.User
- //scriptFsh := payload.ScriptFsh
- //scriptPath := payload.ScriptPath
- w := payload.Writer
- //r := payload.Request
- //_http_request(optionsJSON) => response object as JSON string. This is the
- //curl-like entry point backing http.request and all method helpers.
- vm.Set("_http_request", func(call otto.FunctionCall) otto.Value {
- opt := httpRequestOptions{}
- optJSON := getOttoStringArg(call, 0)
- if s := strings.TrimSpace(optJSON); s != "" && s != "undefined" && s != "null" {
- if err := json.Unmarshal([]byte(optJSON), &opt); err != nil {
- out, _ := json.Marshal(httpResponse{Error: "invalid request options: " + err.Error()})
- rv, _ := vm.ToValue(string(out))
- return rv
- }
- }
- resp := doHTTPRequest(opt)
- out, _ := json.Marshal(resp)
- rv, _ := vm.ToValue(string(out))
- return rv
- })
- vm.Set("_http_get", func(call otto.FunctionCall) otto.Value {
- //Get URL from function variable
- url, err := call.Argument(0).ToString()
- if err != nil {
- return otto.NullValue()
- }
- //Get respond of the url
- res, err := http.Get(url)
- if err != nil {
- return otto.NullValue()
- }
- bodyContent, err := io.ReadAll(res.Body)
- if err != nil {
- return otto.NullValue()
- }
- returnValue, err := vm.ToValue(string(bodyContent))
- if err != nil {
- return otto.NullValue()
- }
- return returnValue
- })
- vm.Set("_http_post", func(call otto.FunctionCall) otto.Value {
- //Get URL from function paramter
- url, err := call.Argument(0).ToString()
- if err != nil {
- return otto.NullValue()
- }
- //Get JSON content from 2nd paramter
- sendWithPayload := true
- jsonContent, err := call.Argument(1).ToString()
- if err != nil {
- //Disable the payload send
- sendWithPayload = false
- }
- //Create the request
- var req *http.Request
- if sendWithPayload {
- req, _ = http.NewRequest("POST", url, bytes.NewBuffer([]byte(jsonContent)))
- } else {
- req, _ = http.NewRequest("POST", url, bytes.NewBuffer([]byte("")))
- }
- req.Header.Set("Content-Type", "application/json")
- req.Header.Set("User-Agent", "arozos-http-client/1.1")
- //Send the request
- client := &http.Client{}
- resp, err := client.Do(req)
- if err != nil {
- logger.PrintAndLog("Agi", fmt.Sprint(err), nil)
- return otto.NullValue()
- }
- defer resp.Body.Close()
- bodyContent, err := io.ReadAll(resp.Body)
- if err != nil {
- return otto.NullValue()
- }
- returnValue, _ := vm.ToValue(string(bodyContent))
- return returnValue
- })
- vm.Set("_http_head", func(call otto.FunctionCall) otto.Value {
- //Get URL from function paramter
- url, err := call.Argument(0).ToString()
- if err != nil {
- return otto.NullValue()
- }
- //Request the url
- resp, err := http.Get(url)
- if err != nil {
- return otto.NullValue()
- }
- headerKey, err := call.Argument(1).ToString()
- if err != nil || headerKey == "undefined" {
- //No headkey set. Return the whole header as JSON
- js, _ := json.Marshal(resp.Header)
- returnValue, _ := vm.ToValue(string(js))
- return returnValue
- } else {
- //headerkey is set. Return if exists
- possibleValue := resp.Header.Get(headerKey)
- js, _ := json.Marshal(possibleValue)
- returnValue, _ := vm.ToValue(string(js))
- return returnValue
- }
- })
- //Get target status code for response
- vm.Set("_http_code", func(call otto.FunctionCall) otto.Value {
- //Get URL from function paramter
- url, err := call.Argument(0).ToString()
- if err != nil {
- return otto.FalseValue()
- }
- req, err := http.NewRequest("GET", url, nil)
- if err != nil {
- g.RaiseError(err)
- return otto.FalseValue()
- }
- payload := ""
- client := new(http.Client)
- client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
- //Redirection. Return the target location as well
- dest, _ := req.Response.Location()
- payload = dest.String()
- return errors.New("Redirect")
- }
- response, err := client.Do(req)
- if err != nil {
- return otto.FalseValue()
- }
- defer client.CloseIdleConnections()
- vm.Run(`var _location = "` + payload + `";`)
- value, _ := otto.ToValue(response.StatusCode)
- return value
- })
- vm.Set("_http_download", func(call otto.FunctionCall) otto.Value {
- //Get URL from function paramter
- downloadURL, err := call.Argument(0).ToString()
- if err != nil {
- return otto.FalseValue()
- }
- decodedURL, _ := url.QueryUnescape(downloadURL)
- //Get download desintation from paramter
- vpath, err := call.Argument(1).ToString()
- if err != nil {
- return otto.FalseValue()
- }
- //Optional: filename paramter
- filename, err := call.Argument(2).ToString()
- if err != nil || filename == "undefined" {
- //Extract the filename from the url instead
- filename = filepath.Base(decodedURL)
- }
- //Check user acess permission
- if !u.CanWrite(vpath) {
- g.RaiseError(errors.New("Permission Denied"))
- return otto.FalseValue()
- }
- //Convert the vpath to realpath. Check if it exists
- fsh, rpath, err := static.VirtualPathToRealPath(vpath, u)
- if err != nil {
- return otto.FalseValue()
- }
- if !fsh.FileSystemAbstraction.FileExists(rpath) || !fsh.FileSystemAbstraction.IsDir(rpath) {
- g.RaiseError(errors.New(vpath + " is a file not a directory."))
- return otto.FalseValue()
- }
- downloadDest := filepath.Join(rpath, filename)
- //Ok. Download the file
- resp, err := http.Get(decodedURL)
- if err != nil {
- return otto.FalseValue()
- }
- defer resp.Body.Close()
- // Create the file
- err = fsh.FileSystemAbstraction.WriteStream(downloadDest, resp.Body, 0775)
- if err != nil {
- return otto.FalseValue()
- }
- return otto.TrueValue()
- })
- vm.Set("_http_getb64", func(call otto.FunctionCall) otto.Value {
- //Get URL from function variable and return bytes as base64
- url, err := call.Argument(0).ToString()
- if err != nil {
- return otto.NullValue()
- }
- //Get respond of the url
- res, err := http.Get(url)
- if err != nil {
- return otto.NullValue()
- }
- bodyContent, err := io.ReadAll(res.Body)
- if err != nil {
- return otto.NullValue()
- }
- sEnc := base64.StdEncoding.EncodeToString(bodyContent)
- r, err := otto.ToValue(string(sEnc))
- if err != nil {
- logger.PrintAndLog("Agi", err.Error(), nil)
- return otto.NullValue()
- }
- return r
- })
- vm.Set("_http_redirect", func(call otto.FunctionCall) otto.Value {
- //Redirect the current request to another url
- targetUrl, err := call.Argument(0).ToString()
- if err != nil {
- return otto.NullValue()
- }
- statusCode, err := call.Argument(1).ToInteger()
- if err != nil {
- //Default: Temporary redirect
- statusCode = 307
- }
- w.Header().Set("Location", targetUrl)
- w.WriteHeader(int(statusCode))
- return otto.TrueValue()
- })
- //Wrap all the native code function into an http class
- vm.Run(`
- var http = {};
- //http.request(options) => response object {ok, status, statusText, headers, body, error}
- //options: {url, method, headers, body, bodyBase64, form, json, contentType,
- // username, password, timeout, followRedirect, responseType}
- http.request = function(options){
- return JSON.parse(_http_request(JSON.stringify(options || {})));
- };
- //Classic helpers. http.get / http.post now accept optional headers.
- http.get = function(url, headers){
- if (typeof headers == "undefined"){
- //Backward-compatible fast path: returns body string (or null on error)
- return _http_get(url);
- }
- return http.request({url: url, method: "GET", headers: headers}).body;
- };
- http.post = function(url, body, headers, contentType){
- if (typeof headers == "undefined" && typeof contentType == "undefined"){
- //Backward-compatible fast path: JSON body, returns body string
- return _http_post(url, body);
- }
- return http.request({
- url: url, method: "POST", body: body,
- headers: headers, contentType: contentType
- }).body;
- };
- //Method + body-shape convenience helpers built on http.request.
- http.put = function(url, body, headers, contentType){
- return http.request({url: url, method: "PUT", body: body, headers: headers, contentType: contentType});
- };
- http.patch = function(url, body, headers, contentType){
- return http.request({url: url, method: "PATCH", body: body, headers: headers, contentType: contentType});
- };
- http.delete = function(url, headers){
- return http.request({url: url, method: "DELETE", headers: headers});
- };
- http.postForm = function(url, form, headers){
- return http.request({url: url, method: "POST", form: form, headers: headers});
- };
- http.postJSON = function(url, obj, headers){
- return http.request({url: url, method: "POST", json: obj, headers: headers});
- };
- http.head = _http_head;
- http.download = _http_download;
- http.getb64 = _http_getb64;
- http.getCode = _http_code;
- http.redirect = function(t, c){
- if (typeof(c) == "undefined"){
- c = 307;
- }
- _http_redirect(t,c);
- };
- `)
- }
|