From fb31a251b83ebfae85a0d4307ee0e415c1d0fc04 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sat, 13 Dec 2025 14:00:16 +0300 Subject: [PATCH] Improve fetch for exec source --- internal/expr/README.md | 82 +++++++++++++++--- pkg/expr/expr.go | 181 ++++++++++++++++++++++++---------------- 2 files changed, 179 insertions(+), 84 deletions(-) diff --git a/internal/expr/README.md b/internal/expr/README.md index 35e3c612..db5a27d4 100644 --- a/internal/expr/README.md +++ b/internal/expr/README.md @@ -12,34 +12,94 @@ - `fetch` - JS-like HTTP requests - `match` - JS-like RegExp queries -## Examples +## Fetch examples + +Multiple fetch requests are executed within a single session. They share the same cookie. + +**HTTP GET** + +```js +var r = fetch('https://example.org/products.json'); +``` + +**HTTP POST JSON** + +```js +var r = fetch('https://example.org/post', { + method: 'POST', + // Content-Type: application/json will be set automatically + json: {username: 'example'} +}); +``` + +**HTTP POST Form** + +```js +var r = fetch('https://example.org/post', { + method: 'POST', + // Content-Type: application/x-www-form-urlencoded will be set automatically + data: {username: 'example', password: 'password'} +}); +``` + +## Script examples **Two way audio for Dahua VTO** ```yaml streams: dahua_vto: | - expr: let host = "admin:password@192.168.1.123"; - fetch("http://"+host+"/cgi-bin/configManager.cgi?action=setConfig&Encode[0].MainFormat[0].Audio.Compression=G.711A&Encode[0].MainFormat[0].Audio.Frequency=8000").ok - ? "rtsp://"+host+"/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif" : "" + expr: + let host = 'admin:password@192.168.1.123'; + + var r = fetch('http://' + host + '/cgi-bin/configManager.cgi?action=setConfig&Encode[0].MainFormat[0].Audio.Compression=G.711A&Encode[0].MainFormat[0].Audio.Frequency=8000'); + + 'rtsp://' + host + '/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif' ``` **dom.ru** -You can get credentials via: - -- https://github.com/alexmorbo/domru (file `/share/domru/accounts`) -- https://github.com/ad/domru +You can get credentials from https://github.com/ad/domru ```yaml streams: dom_ru: | - expr: let camera = "99999999"; let token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; let operator = 99; - fetch("https://myhome.novotelecom.ru/rest/v1/forpost/cameras/"+camera+"/video", { - headers: {Authorization: "Bearer "+token, Operator: operator} + expr: + let camera = '***'; + let token = '***'; + let operator = '***'; + + fetch('https://myhome.proptech.ru/rest/v1/forpost/cameras/' + camera + '/video', { + headers: { + 'Authorization': 'Bearer ' + token, + 'User-Agent': 'Google sdkgphone64x8664 | Android 14 | erth | 8.26.0 (82600010) | 0 | 0 | 0', + 'Operator': operator + } }).json().data.URL ``` +**dom.ufanet.ru** + +```yaml +streams: + ufanet_ru: | + expr: + let username = '***'; + let password = '***'; + let cameraid = '***'; + + let r1 = fetch('https://ucams.ufanet.ru/api/internal/login/', { + method: 'POST', + data: {username: username, password: password} + }); + let r2 = fetch('https://ucams.ufanet.ru/api/v0/cameras/this/?lang=ru', { + method: 'POST', + json: {'fields': ['token_l', 'server'], 'token_l_ttl': 3600, 'numbers': [cameraid]}, + }).json().results[0]; + + 'rtsp://' + r2.server.domain + '/' + r2.number + '?token=' + r2.token_l +``` + **Parse HLS files from Apple** Same example in two languages - python and expr. diff --git a/pkg/expr/expr.go b/pkg/expr/expr.go index 4a8a663c..e82551be 100644 --- a/pkg/expr/expr.go +++ b/pkg/expr/expr.go @@ -1,40 +1,78 @@ package expr import ( + "bytes" "encoding/json" "fmt" "io" "net/http" + "net/http/cookiejar" + "net/url" "regexp" "strings" + "time" - "github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/expr-lang/expr" "github.com/expr-lang/expr/vm" ) -func newRequest(method, url string, headers map[string]any, body string) (*http.Request, error) { +func newRequest(rawURL string, options map[string]any) (*http.Request, error) { + var method, contentType string var rd io.Reader - if method == "" { + // method from js fetch + if s, ok := options["method"].(string); ok { + method = s + } else { method = "GET" } - if body != "" { - rd = strings.NewReader(body) + + // params key from python requests + if kv, ok := options["params"].(map[string]any); ok { + rawURL += "?" + url.Values(kvToString(kv)).Encode() } - req, err := http.NewRequest(method, url, rd) + // json key from python requests + // data key from python requests + // body key from js fetch + if v, ok := options["json"]; ok { + b, err := json.Marshal(v) + if err != nil { + return nil, err + } + contentType = "application/json" + rd = bytes.NewReader(b) + } else if kv, ok := options["data"].(map[string]any); ok { + contentType = "application/x-www-form-urlencoded" + rd = strings.NewReader(url.Values(kvToString(kv)).Encode()) + } else if s, ok := options["body"].(string); ok { + rd = strings.NewReader(s) + } + + req, err := http.NewRequest(method, rawURL, rd) if err != nil { return nil, err } - for k, v := range headers { - req.Header.Set(k, fmt.Sprintf("%v", v)) + if kv, ok := options["headers"].(map[string]any); ok { + req.Header = kvToString(kv) + } + + if contentType != "" && req.Header.Get("Content-Type") == "" { + req.Header.Set("Content-Type", contentType) } return req, nil } +func kvToString(kv map[string]any) map[string][]string { + dst := make(map[string][]string, len(kv)) + for k, v := range kv { + dst[k] = []string{fmt.Sprintf("%v", v)} + } + return dst +} + func regExp(params ...any) (*regexp.Regexp, error) { exp := params[0].(string) if len(params) >= 2 { @@ -49,72 +87,69 @@ func regExp(params ...any) (*regexp.Regexp, error) { return regexp.Compile(exp) } -var Options = []expr.Option{ - expr.Function( - "fetch", - func(params ...any) (any, error) { - var req *http.Request - var err error - - url := params[0].(string) - - if len(params) == 2 { - options := params[1].(map[string]any) - method, _ := options["method"].(string) - headers, _ := options["headers"].(map[string]any) - body, _ := options["body"].(string) - req, err = newRequest(method, url, headers, body) - } else { - req, err = http.NewRequest("GET", url, nil) - } - - if err != nil { - return nil, err - } - - res, err := tcp.Do(req) - if err != nil { - return nil, err - } - - b, _ := io.ReadAll(res.Body) - - return map[string]any{ - "ok": res.StatusCode < 400, - "status": res.Status, - "text": string(b), - "json": func() (v any) { - _ = json.Unmarshal(b, &v) - return - }, - }, nil - }, - //new(func(url string) map[string]any), - //new(func(url string, options map[string]any) map[string]any), - ), - expr.Function( - "match", - func(params ...any) (any, error) { - re, err := regExp(params[1:]...) - if err != nil { - return nil, err - } - str := params[0].(string) - return re.FindStringSubmatch(str), nil - }, - //new(func(str, expr string) []string), - //new(func(str, expr, flags string) []string), - ), - expr.Function( - "RegExp", - func(params ...any) (any, error) { - return regExp(params) - }, - ), -} - func Compile(input string) (*vm.Program, error) { - return expr.Compile(input, Options...) + // support http sessions + jar, _ := cookiejar.New(nil) + client := http.Client{ + Jar: jar, + Timeout: 5 * time.Second, + } + + return expr.Compile( + input, + expr.Function( + "fetch", + func(params ...any) (any, error) { + var req *http.Request + var err error + + rawURL := params[0].(string) + + if len(params) == 2 { + options := params[1].(map[string]any) + req, err = newRequest(rawURL, options) + } else { + req, err = http.NewRequest("GET", rawURL, nil) + } + + if err != nil { + return nil, err + } + + res, err := client.Do(req) + if err != nil { + return nil, err + } + + b, _ := io.ReadAll(res.Body) + + return map[string]any{ + "ok": res.StatusCode < 400, + "status": res.Status, + "text": string(b), + "json": func() (v any) { + _ = json.Unmarshal(b, &v) + return + }, + }, nil + }, + //new(func(url string) map[string]any), + //new(func(url string, options map[string]any) map[string]any), + ), + expr.Function( + "match", + func(params ...any) (any, error) { + re, err := regExp(params[1:]...) + if err != nil { + return nil, err + } + str := params[0].(string) + return re.FindStringSubmatch(str), nil + }, + //new(func(str, expr string) []string), + //new(func(str, expr, flags string) []string), + ), + ) } func Eval(input string, env any) (any, error) {