2024-04-15 17:10:06 +09:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"crypto/tls"
|
|
|
|
"encoding/base64"
|
|
|
|
"encoding/json"
|
2024-04-15 23:58:32 +09:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
2024-04-15 17:10:06 +09:00
|
|
|
"net"
|
|
|
|
"net/http"
|
2024-04-15 23:58:32 +09:00
|
|
|
"reflect"
|
2024-04-15 17:10:06 +09:00
|
|
|
"strings"
|
2024-04-15 23:58:32 +09:00
|
|
|
"sync"
|
2024-04-15 17:10:06 +09:00
|
|
|
"time"
|
|
|
|
|
2024-04-15 23:58:32 +09:00
|
|
|
"github.com/FloatTech/ttl"
|
2024-04-15 17:10:06 +09:00
|
|
|
"github.com/fumiama/terasu"
|
2024-04-15 23:58:32 +09:00
|
|
|
"golang.org/x/net/http2"
|
2024-04-15 17:10:06 +09:00
|
|
|
)
|
|
|
|
|
|
|
|
var dialer = net.Dialer{
|
|
|
|
Timeout: time.Minute,
|
|
|
|
}
|
|
|
|
|
2024-04-15 23:58:32 +09:00
|
|
|
var lookupTable = ttl.NewCache[string, []string](time.Hour)
|
|
|
|
|
|
|
|
type comandyClient http.Client
|
|
|
|
|
|
|
|
var cli = comandyClient(http.Client{
|
|
|
|
Transport: &http2.Transport{
|
|
|
|
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
|
|
|
|
if dialer.Timeout != 0 {
|
|
|
|
var cancel context.CancelFunc
|
|
|
|
ctx, cancel = context.WithTimeout(ctx, dialer.Timeout)
|
|
|
|
defer cancel()
|
2024-04-15 17:10:06 +09:00
|
|
|
}
|
2024-04-15 23:58:32 +09:00
|
|
|
|
|
|
|
if !dialer.Deadline.IsZero() {
|
|
|
|
var cancel context.CancelFunc
|
|
|
|
ctx, cancel = context.WithDeadline(ctx, dialer.Deadline)
|
|
|
|
defer cancel()
|
|
|
|
}
|
|
|
|
|
|
|
|
host, port, err := net.SplitHostPort(addr)
|
2024-04-15 17:10:06 +09:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2024-04-15 23:58:32 +09:00
|
|
|
addrs := lookupTable.Get(host)
|
|
|
|
if len(addrs) == 0 {
|
|
|
|
addrs, err = resolver.LookupHost(ctx, host)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
lookupTable.Set(host, addrs)
|
|
|
|
}
|
|
|
|
if len(addr) == 0 {
|
|
|
|
return nil, errors.New("empty host addr")
|
|
|
|
}
|
|
|
|
var tlsConn *tls.Conn
|
|
|
|
for _, a := range addrs {
|
|
|
|
if strings.Contains(a, ":") {
|
|
|
|
a = "[" + a + "]:" + port
|
|
|
|
} else {
|
|
|
|
a += ":" + port
|
|
|
|
}
|
|
|
|
conn, err := dialer.DialContext(ctx, network, a)
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
2024-04-16 15:27:06 +09:00
|
|
|
tlsConn = tls.Client(conn, cfg)
|
|
|
|
err = terasu.Use(tlsConn).HandshakeContext(ctx)
|
2024-04-15 23:58:32 +09:00
|
|
|
if err == nil {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
_ = tlsConn.Close()
|
|
|
|
}
|
|
|
|
return tlsConn, nil
|
2024-04-15 17:10:06 +09:00
|
|
|
},
|
|
|
|
},
|
2024-04-15 23:58:32 +09:00
|
|
|
})
|
2024-04-15 17:10:06 +09:00
|
|
|
|
|
|
|
type capsule struct {
|
|
|
|
C int `json:"code,omitempty"`
|
|
|
|
M string `json:"method,omitempty"`
|
|
|
|
U string `json:"url,omitempty"`
|
|
|
|
H map[string]any `json:"headers,omitempty"`
|
|
|
|
D string `json:"data,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *capsule) printerr(err error) string {
|
|
|
|
buf := strings.Builder{}
|
|
|
|
r.C = http.StatusInternalServerError
|
|
|
|
r.D = base64.StdEncoding.EncodeToString(stringToBytes(err.Error()))
|
|
|
|
_ = json.NewEncoder(&buf).Encode(r)
|
|
|
|
return buf.String()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *capsule) printstrerr(err string) string {
|
|
|
|
buf := strings.Builder{}
|
|
|
|
r.C = http.StatusInternalServerError
|
|
|
|
r.D = base64.StdEncoding.EncodeToString(stringToBytes(err))
|
|
|
|
_ = json.NewEncoder(&buf).Encode(r)
|
|
|
|
return buf.String()
|
|
|
|
}
|
2024-04-15 23:58:32 +09:00
|
|
|
|
2024-04-16 15:27:06 +09:00
|
|
|
func (cli *comandyClient) request(para string) (ret string) {
|
2024-04-15 23:58:32 +09:00
|
|
|
r := capsule{}
|
2024-04-16 15:27:06 +09:00
|
|
|
defer func() {
|
|
|
|
err := recover()
|
|
|
|
if err != nil {
|
|
|
|
ret = r.printstrerr(fmt.Sprint())
|
|
|
|
}
|
|
|
|
}()
|
2024-04-15 23:58:32 +09:00
|
|
|
wg := sync.WaitGroup{}
|
|
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
|
|
defer wg.Done()
|
|
|
|
_ = canUseIPv6.Get()
|
|
|
|
}()
|
|
|
|
err := json.Unmarshal(stringToBytes(para), &r)
|
|
|
|
if err != nil {
|
|
|
|
return r.printerr(err)
|
|
|
|
}
|
|
|
|
if r.U == "" || !strings.HasPrefix(r.U, "https://") {
|
|
|
|
return r.printstrerr("invalid url '" + r.U + "'")
|
|
|
|
}
|
|
|
|
if r.M != "GET" && r.M != "POST" && r.M != "DELETE" {
|
|
|
|
return r.printstrerr("invalid method '" + r.U + "'")
|
|
|
|
}
|
|
|
|
var body io.Reader
|
|
|
|
if len(r.D) > 0 {
|
|
|
|
body = strings.NewReader(r.D)
|
|
|
|
}
|
|
|
|
req, err := http.NewRequest(r.M, r.U, body)
|
|
|
|
if err != nil {
|
|
|
|
return r.printerr(err)
|
|
|
|
}
|
|
|
|
for k, vs := range r.H {
|
|
|
|
lk := strings.ToLower(k)
|
|
|
|
if strings.HasPrefix(lk, "x-") {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
switch x := vs.(type) {
|
|
|
|
case string:
|
|
|
|
req.Header.Add(k, x)
|
|
|
|
case []string:
|
|
|
|
for _, v := range x {
|
|
|
|
req.Header.Add(k, v)
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
return r.printstrerr("unsupported H type " + reflect.ValueOf(x).Type().Name())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
resp, err := (*http.Client)(cli).Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return r.printerr(err)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
sb := strings.Builder{}
|
|
|
|
enc := base64.NewEncoder(base64.StdEncoding, &sb)
|
|
|
|
_, err = io.Copy(enc, resp.Body)
|
|
|
|
_ = enc.Close()
|
|
|
|
if err != nil {
|
|
|
|
return r.printerr(err)
|
|
|
|
}
|
|
|
|
r.C = resp.StatusCode
|
|
|
|
r.H = make(map[string]any, len(resp.Header)*2)
|
|
|
|
for k, vs := range resp.Header {
|
|
|
|
if len(vs) == 1 {
|
|
|
|
r.H[k] = vs[0]
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
r.H[k] = vs
|
|
|
|
}
|
|
|
|
r.D = sb.String()
|
|
|
|
outbuf := strings.Builder{}
|
|
|
|
err = json.NewEncoder(&outbuf).Encode(&r)
|
|
|
|
if err != nil {
|
|
|
|
return r.printerr(err)
|
|
|
|
}
|
|
|
|
return outbuf.String()
|
|
|
|
}
|