Improve fetch for exec source
This commit is contained in:
+71
-11
@@ -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
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user