Improve fetch for exec source

This commit is contained in:
Alex X
2025-12-13 14:00:16 +03:00
parent c5277daa45
commit fb31a251b8
2 changed files with 179 additions and 84 deletions
+71 -11
View File
@@ -12,34 +12,94 @@
- `fetch` - JS-like HTTP requests - `fetch` - JS-like HTTP requests
- `match` - JS-like RegExp queries - `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** **Two way audio for Dahua VTO**
```yaml ```yaml
streams: streams:
dahua_vto: | dahua_vto: |
expr: let host = "admin:password@192.168.1.123"; expr:
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 let host = 'admin:password@192.168.1.123';
? "rtsp://"+host+"/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif" : ""
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** **dom.ru**
You can get credentials via: You can get credentials from https://github.com/ad/domru
- https://github.com/alexmorbo/domru (file `/share/domru/accounts`)
- https://github.com/ad/domru
```yaml ```yaml
streams: streams:
dom_ru: | dom_ru: |
expr: let camera = "99999999"; let token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; let operator = 99; expr:
fetch("https://myhome.novotelecom.ru/rest/v1/forpost/cameras/"+camera+"/video", { let camera = '***';
headers: {Authorization: "Bearer "+token, Operator: operator} 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 }).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** **Parse HLS files from Apple**
Same example in two languages - python and expr. Same example in two languages - python and expr.
+108 -73
View File
@@ -1,40 +1,78 @@
package expr package expr
import ( import (
"bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"net/http/cookiejar"
"net/url"
"regexp" "regexp"
"strings" "strings"
"time"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/expr-lang/expr" "github.com/expr-lang/expr"
"github.com/expr-lang/expr/vm" "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 var rd io.Reader
if method == "" { // method from js fetch
if s, ok := options["method"].(string); ok {
method = s
} else {
method = "GET" 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 { if err != nil {
return nil, err return nil, err
} }
for k, v := range headers { if kv, ok := options["headers"].(map[string]any); ok {
req.Header.Set(k, fmt.Sprintf("%v", v)) req.Header = kvToString(kv)
}
if contentType != "" && req.Header.Get("Content-Type") == "" {
req.Header.Set("Content-Type", contentType)
} }
return req, nil 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) { func regExp(params ...any) (*regexp.Regexp, error) {
exp := params[0].(string) exp := params[0].(string)
if len(params) >= 2 { if len(params) >= 2 {
@@ -49,72 +87,69 @@ func regExp(params ...any) (*regexp.Regexp, error) {
return regexp.Compile(exp) 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) { 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) { func Eval(input string, env any) (any, error) {