http/http.go

527 lines
14 KiB
Go
Raw Normal View History

2024-10-15 10:45:04 +08:00
package http
import (
_ "embed"
"net/http"
"reflect"
"strings"
"sync"
"time"
"apigo.cc/gojs"
"apigo.cc/gojs/goja"
"github.com/gorilla/websocket"
"github.com/ssgo/httpclient"
"github.com/ssgo/log"
"github.com/ssgo/u"
)
//go:embed http.ts
var httpTS string
type Http struct {
client *httpclient.ClientPool
baseURL string
globalHeaders map[string]string
}
var defaultHttp = &Http{
client: httpclient.GetClient(60000 * time.Millisecond),
globalHeaders: make(map[string]string),
}
// TODO ws
func init() {
obj := map[string]any{
"new": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
return newClient("HTTP", argsIn, vm)
},
"newH2C": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
return newClient("H2C", argsIn, vm)
},
"get": defaultHttp.Get,
"head": defaultHttp.Head,
"post": defaultHttp.Post,
"put": defaultHttp.Put,
"delete": defaultHttp.Delete,
"do": defaultHttp.Do,
"upload": defaultHttp.Upload,
"download": defaultHttp.Download,
"connect": defaultHttp.Connect,
}
gojs.Register("apigo.cc/gojs/http", gojs.Module{
Object: obj,
TsCode: httpTS,
Desc: "",
Example: "",
})
}
func newClient(portal string, argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(0)
opt := args.Obj(0)
timeout := 60000 * time.Millisecond
if opt != nil {
timeout = time.Duration(opt.Int64("timeout")) * time.Millisecond
}
var client *httpclient.ClientPool
if portal == "H2C" {
client = httpclient.GetClientH2C(timeout)
} else {
client = httpclient.GetClient(timeout)
}
cli := &Http{
client: client,
globalHeaders: make(map[string]string),
}
setConfig(cli, opt)
return vm.ToValue(gojs.MakeMap(cli))
}
func setConfig(cli *Http, opt *gojs.Obj) {
if opt != nil {
if globalHeaders := opt.Map("globalHeaders"); globalHeaders != nil {
for k, v := range globalHeaders {
cli.globalHeaders[k] = u.String(v)
}
}
if baseURL := opt.Str("baseURL"); baseURL != "" {
cli.baseURL = baseURL
}
if downloadPartSize := opt.Int64("downloadPartSize"); downloadPartSize != 0 {
cli.client.DownloadPartSize = downloadPartSize
}
if redirect := opt.Bool("redirect"); redirect {
cli.client.EnableRedirect()
}
}
}
func makeResult(r *httpclient.Result, vm *goja.Runtime) goja.Value {
if r.Error != nil {
panic(vm.NewGoError(r.Error))
}
headers := map[string]string{}
for k, v := range r.Response.Header {
headers[k] = v[0]
}
return vm.ToValue(map[string]any{
"status": r.Response.Status,
"statusCode": r.Response.StatusCode,
"headers": headers,
"_data": r.Bytes(),
"bytes": toBytes,
"string": toString,
"object": toObject,
})
}
func toBytes(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
dataValue := argsIn.This.ToObject(vm).Get("_data")
if _, ok := dataValue.Export().([]byte); ok {
return dataValue
}
return nil
}
func toString(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
dataValue := argsIn.This.ToObject(vm).Get("_data")
if data, ok := dataValue.Export().([]byte); ok {
return vm.ToValue(string(data))
}
return nil
}
func toObject(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
dataValue := argsIn.This.ToObject(vm).Get("_data")
if data, ok := dataValue.Export().([]byte); ok {
obj := u.UnJsonBytes(data, nil)
v := u.FinalValue(reflect.ValueOf(obj))
2024-10-16 11:47:38 +08:00
if v.IsValid() {
return vm.ToValue(v.Interface())
}
2024-10-15 10:45:04 +08:00
}
return nil
}
func (hc *Http) makeURL(url string) string {
if !strings.Contains(url, "://") && hc.baseURL != "" {
if strings.HasSuffix(hc.baseURL, "/") && strings.HasPrefix(url, "/") {
return hc.baseURL + url[1:]
} else if !strings.HasSuffix(hc.baseURL, "/") && !strings.HasPrefix(url, "/") {
return hc.baseURL + "/" + url
}
return hc.baseURL + url
}
return url
}
func (hc *Http) makeHeaderArray(in map[string]any) []string {
out := make([]string, 0)
if hc.globalHeaders != nil {
for k, v := range hc.globalHeaders {
out = append(out, k, v)
}
}
if in != nil {
for k, v := range in {
out = append(out, k, u.String(v))
}
}
return out
}
func (hc *Http) Get(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(1)
return makeResult(hc.client.Get(hc.makeURL(args.Str(0)), hc.makeHeaderArray(args.Map(1))...), vm)
}
func (hc *Http) Head(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(1)
return makeResult(hc.client.Head(hc.makeURL(args.Str(0)), hc.makeHeaderArray(args.Map(1))...), vm)
}
func (hc *Http) Post(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(2)
return makeResult(hc.client.Post(hc.makeURL(args.Str(0)), args.Any(1), hc.makeHeaderArray(args.Map(2))...), vm)
}
func (hc *Http) Put(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(2)
return makeResult(hc.client.Put(hc.makeURL(args.Str(0)), args.Any(1), hc.makeHeaderArray(args.Map(2))...), vm)
}
func (hc *Http) Delete(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(2)
return makeResult(hc.client.Delete(hc.makeURL(args.Str(0)), args.Any(1), hc.makeHeaderArray(args.Map(2))...), vm)
}
func (hc *Http) Do(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(3)
if len(argsIn.Arguments) == 3 {
argsIn.Arguments = append(argsIn.Arguments, vm.ToValue(nil))
}
var r *httpclient.Result
if cb, ok := goja.AssertFunction(argsIn.Arguments[3]); ok {
r = hc.client.ManualDo(hc.makeURL(args.Str(0)), args.Str(1), args.Any(2), hc.makeHeaderArray(args.Map(4))...)
buf := make([]byte, 1024)
for {
n, err := r.Response.Body.Read(buf)
if err != nil {
break
}
_, _ = cb(argsIn.This, vm.ToValue(u.String(buf[0:n])))
}
} else {
r = hc.client.Do(hc.makeURL(args.Str(0)), args.Str(1), args.Any(2), hc.makeHeaderArray(args.Map(4))...)
}
return makeResult(r, vm)
}
func (hc *Http) Upload(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(2)
postData := map[string]string{}
postFiles := map[string]any{}
u.Convert(args.Any(1), &postData)
if len(argsIn.Arguments) > 2 {
u.Convert(args.Any(2), &postFiles)
}
r, _ := hc.client.MPost(hc.makeURL(args.Str(0)), postData, postFiles, hc.makeHeaderArray(args.Map(3))...)
return makeResult(r, vm)
}
func (hc *Http) Download(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(2)
var r *httpclient.Result
var callback goja.Callable
if len(argsIn.Arguments) > 2 {
if cb, ok := goja.AssertFunction(argsIn.Arguments[2]); ok {
callback = cb
}
}
if callback != nil {
r, _ = hc.client.Download(hc.makeURL(args.Str(0)), args.Str(1), func(start, end int64, ok bool, finished, total int64) {
_, _ = callback(argsIn.This, vm.ToValue(finished), vm.ToValue(total))
}, hc.makeHeaderArray(args.Map(3))...)
} else {
r, _ = hc.client.Download(hc.makeURL(args.Str(0)), args.Str(1), nil, hc.makeHeaderArray(args.Map(3))...)
}
return makeResult(r, vm)
}
func (hc *Http) Connect(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(1)
url := hc.makeURL(args.Str(0))
if strings.HasPrefix(url, "http") {
url = strings.Replace(url, "http", "ws", 1)
}
ws := &WS{url: url, this: args.This, running: false, pingStopChan: make(chan bool, 1), closeChan: make(chan bool, 1), logger: args.Logger, headers: make(map[string]string)}
for k, v := range hc.globalHeaders {
ws.headers[k] = v
}
if opt := args.Obj(1); opt != nil {
if headers := opt.Map("headers"); headers != nil {
for k, v := range headers {
ws.headers[k] = u.String(v)
}
}
ws.compress = opt.Bool("compress")
ws.onOpen = opt.Func("onOpen")
ws.onClose = opt.Func("onClose")
ws.onError = opt.Func("onError")
// ws.onPing = opt.Func("onPing")
// ws.onPong = opt.Func("onPong")
ws.onMessage = opt.Func("onMessage")
ws.onJSONMessage = opt.Func("onJSONMessage")
ws.pingInterval = opt.Int64("pingInterval")
ws.reconnectInterval = opt.Int64("reconnectInterval")
if ws.reconnectInterval == 0 && (ws.onMessage != nil || ws.onJSONMessage != nil) {
ws.reconnectInterval = 1000
}
}
// fmt.Println(u.BMagenta("WS"), "start")
if err := ws.connect(vm); err == nil {
if ws.pingInterval > 0 {
// fmt.Println(u.BMagenta("WS"), "start ping")
go func() {
// ws.sleep(time.Duration(ws.pingInterval) * time.Millisecond)
for {
if ws.conn != nil {
// fmt.Println(u.BMagenta("WS"), "ping")
ws.writeLock.Lock()
ws.conn.WriteMessage(websocket.PingMessage, []byte{'P'})
ws.writeLock.Unlock()
}
if !ws.running {
break
}
ws.sleep(time.Duration(ws.pingInterval) * time.Millisecond)
if !ws.running {
break
}
}
// fmt.Println(u.BMagenta("WS"), "stop ping")
ws.pingStopChan <- true
}()
} else {
ws.pingStopChan <- true
}
if ws.onMessage != nil || ws.onJSONMessage != nil {
// fmt.Println(u.BMagenta("WS"), "start onMessage")
go func() {
for {
for {
if ws.conn != nil {
if ws.onJSONMessage != nil {
var obj interface{}
err := ws.conn.ReadJSON(&obj)
if err != nil {
break
}
_, _ = ws.onJSONMessage(ws.this, vm.ToValue(obj))
} else {
typ, buf, err := ws.conn.ReadMessage()
if err != nil {
break
}
if typ == websocket.TextMessage {
_, _ = ws.onMessage(ws.this, vm.ToValue(string(buf)))
} else {
_, _ = ws.onMessage(ws.this, vm.ToValue(buf))
}
}
}
}
// fmt.Println(u.BMagenta("WS"), "stop onMessage")
// 未结束的连接自动重连
if !ws.running {
break
}
ws.sleep(time.Duration(ws.reconnectInterval) * time.Millisecond)
if !ws.running {
break
}
// fmt.Println(u.BMagenta("WS"), "reconnect")
ws.connect(vm)
}
// fmt.Println(u.BMagenta("WS"), "stop onMessage2")
ws.closeChan <- true
}()
} else {
ws.closeChan <- true
}
return vm.ToValue(gojs.MakeMap(ws))
} else {
panic(vm.NewGoError(err))
}
}
type WS struct {
conn *websocket.Conn
running bool
closed bool
closeChan chan bool
pingStopChan chan bool
logger *log.Logger
this goja.Value
writeLock sync.Mutex
url string
headers map[string]string
compress bool
onOpen goja.Callable
onClose goja.Callable
onError goja.Callable
// onPing goja.Callable
// onPong goja.Callable
onMessage goja.Callable
onJSONMessage goja.Callable
pingInterval int64
reconnectInterval int64
}
func (ws *WS) sleep(interval time.Duration) {
if interval < time.Second {
time.Sleep(interval)
} else {
for {
time.Sleep(time.Second)
interval -= time.Second
if !ws.running || interval <= 0 {
break
}
}
}
}
func (ws *WS) error(err goja.Value) {
if ws.onError != nil {
ws.onError(ws.this, err)
}
}
func (ws *WS) connect(vm *goja.Runtime) error {
reqHeader := http.Header{}
for k, v := range ws.headers {
reqHeader.Set(k, v)
}
conn, _, err := websocket.DefaultDialer.Dial(ws.url, reqHeader)
if err != nil {
return err
}
ws.conn = conn
if ws.reconnectInterval > 0 || ws.pingInterval > 0 {
ws.running = true
}
ws.closed = false
if ws.compress {
conn.EnableWriteCompression(true)
}
// if ws.onPing != nil {
// conn.SetPingHandler(func(appData string) error {
// fmt.Println(u.BMagenta("WS"), "onPing")
// _, err := ws.onPing(ws.this, vm.ToValue(appData))
// return err
// })
// return err
// }
// if ws.onPong != nil {
// conn.SetPongHandler(func(appData string) error {
// fmt.Println(u.BMagenta("WS"), "onPong")
// _, err := ws.onPong(ws.this, vm.ToValue(appData))
// return err
// })
// return err
// }
conn.SetCloseHandler(func(code int, text string) error {
// fmt.Println(u.BMagenta("WS"), "onClose")
if ws.onClose != nil {
_, err := ws.onClose(ws.this, vm.ToValue(code), vm.ToValue(text))
return err
}
// 关闭旧连接和接收器
if !ws.closed {
ws.conn.Close()
ws.closed = true
}
// 未结束的连接自动重连
if ws.running {
time.Sleep(time.Duration(ws.reconnectInterval) * time.Millisecond)
// fmt.Println(u.BMagenta("WS"), "reconnect")
return ws.connect(vm)
}
return nil
})
return nil
}
func (ws *WS) Read(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
typ, buf, err := ws.conn.ReadMessage()
if err != nil {
panic(vm.NewGoError(err))
}
if typ == websocket.TextMessage {
return vm.ToValue(string(buf))
} else {
return vm.ToValue(buf)
}
}
func (ws *WS) ReadJSON(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
var obj interface{}
err := ws.conn.ReadJSON(&obj)
if err != nil {
panic(vm.NewGoError(err))
}
return vm.ToValue(obj)
}
func (ws *WS) Write(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(1)
var err error
ws.writeLock.Lock()
if args.Arguments[0].ExportType().Kind() == reflect.String {
err = ws.conn.WriteMessage(websocket.TextMessage, args.Bytes(0))
} else {
err = ws.conn.WriteMessage(websocket.BinaryMessage, args.Bytes(0))
}
ws.writeLock.Unlock()
if err != nil {
panic(vm.NewGoError(err))
}
return nil
}
func (ws *WS) WriteJSON(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(1)
ws.writeLock.Lock()
err := ws.conn.WriteJSON(args.Any(0))
ws.writeLock.Unlock()
if err != nil {
panic(vm.NewGoError(err))
}
return nil
}
func (ws *WS) Close(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
// fmt.Println(u.BMagenta("WS"), "stop")
ws.running = false
if !ws.closed {
ws.closed = true
err := ws.conn.Close()
if err != nil {
panic(vm.NewGoError(err))
}
<-ws.pingStopChan
<-ws.closeChan
}
return nil
}