Files
chis_osi/osi/transport.go
T

205 lines
5.1 KiB
Go
Raw Normal View History

package osi
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"sort"
"strings"
"time"
"golang.org/x/net/proxy"
)
type TransportConfig struct {
Timeout time.Duration
Socks5Proxy string
}
type Transport struct {
timeout time.Duration
dialer proxy.Dialer
}
func NewTransport(config TransportConfig) (*Transport, error) {
timeout := config.Timeout
if timeout <= 0 {
timeout = 20 * time.Second
}
dialer := proxy.Dialer(proxy.Direct)
if strings.TrimSpace(config.Socks5Proxy) != "" {
proxyDialer, err := socks5Dialer(config.Socks5Proxy)
if err != nil {
return nil, err
}
dialer = proxyDialer
}
return &Transport{timeout: timeout, dialer: dialer}, nil
}
func (t *Transport) PostJSON(ctx context.Context, targetURL string, payload any, headers map[string]string) (int, []byte, error) {
body, err := json.Marshal(payload)
if err != nil {
return 0, nil, fmt.Errorf("marshal json payload: %w", err)
}
target, err := url.Parse(targetURL)
if err != nil {
return 0, nil, fmt.Errorf("parse target url: %w", err)
}
if target.Scheme != "http" {
return 0, nil, fmt.Errorf("unsupported target scheme %q", target.Scheme)
}
reqCtx := ctx
cancel := func() {}
if _, ok := ctx.Deadline(); !ok && t.timeout > 0 {
reqCtx, cancel = context.WithTimeout(ctx, t.timeout)
}
defer cancel()
conn, err := dialWithContext(reqCtx, t.dialer, "tcp", target.Host)
if err != nil {
return 0, nil, err
}
defer conn.Close()
if t.timeout > 0 {
_ = conn.SetDeadline(time.Now().Add(t.timeout))
}
if err := writeJSONRequest(conn, target, body, headers); err != nil {
return 0, nil, err
}
resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
if err != nil {
return 0, nil, err
}
defer resp.Body.Close()
raw, err := io.ReadAll(resp.Body)
if err != nil {
return resp.StatusCode, nil, fmt.Errorf("read response body: %w", err)
}
return resp.StatusCode, raw, nil
}
func writeJSONRequest(w io.Writer, target *url.URL, body []byte, headers map[string]string) error {
path := target.RequestURI()
if path == "" {
path = "/"
}
var buf bytes.Buffer
fmt.Fprintf(&buf, "POST %s HTTP/1.1\r\n", path)
writeHeader(&buf, "Host", target.Host)
writeHeader(&buf, "User-Agent", headerValue(headers, "User-Agent", "python-requests/2.32.4"))
writeHeader(&buf, "Accept-Encoding", headerValue(headers, "Accept-Encoding", "identity"))
writeHeader(&buf, "Accept", headerValue(headers, "Accept", "*/*"))
writeHeader(&buf, "Connection", headerValue(headers, "Connection", "keep-alive"))
writeHeader(&buf, "Content-Length", fmt.Sprintf("%d", len(body)))
writeHeader(&buf, "Content-Type", headerValue(headers, "Content-Type", "application/json"))
written := map[string]bool{
"host": true, "user-agent": true, "accept-encoding": true, "accept": true,
"connection": true, "content-length": true, "content-type": true,
}
for _, name := range []string{"orgCode", "deviceSN", "ts", "userName", "password"} {
if value, ok := headers[name]; ok {
writeHeader(&buf, name, value)
written[strings.ToLower(name)] = true
}
}
var rest []string
for name := range headers {
if !written[strings.ToLower(name)] {
rest = append(rest, name)
}
}
sort.Strings(rest)
for _, name := range rest {
writeHeader(&buf, name, headers[name])
}
buf.WriteString("\r\n")
buf.Write(body)
_, err := w.Write(buf.Bytes())
return err
}
func writeHeader(buf *bytes.Buffer, name, value string) {
fmt.Fprintf(buf, "%s: %s\r\n", name, value)
}
func headerValue(headers map[string]string, name, fallback string) string {
if value, ok := headers[name]; ok {
return value
}
return fallback
}
func dialWithContext(ctx context.Context, dialer proxy.Dialer, network, address string) (net.Conn, error) {
if contextDialer, ok := dialer.(proxy.ContextDialer); ok {
return contextDialer.DialContext(ctx, network, address)
}
type result struct {
conn net.Conn
err error
}
ch := make(chan result, 1)
go func() {
conn, err := dialer.Dial(network, address)
ch <- result{conn: conn, err: err}
}()
select {
case <-ctx.Done():
return nil, ctx.Err()
case result := <-ch:
return result.conn, result.err
}
}
func socks5Dialer(rawProxy string) (proxy.Dialer, error) {
proxyURL, err := normalizeSocks5Proxy(rawProxy)
if err != nil {
return nil, err
}
dialer, err := proxy.SOCKS5("tcp", proxyURL.Host, nil, proxy.Direct)
if err != nil {
return nil, fmt.Errorf("create socks5 dialer: %w", err)
}
return dialer, nil
}
func normalizeSocks5Proxy(rawProxy string) (*url.URL, error) {
rawProxy = strings.TrimSpace(rawProxy)
if !strings.Contains(rawProxy, "://") {
rawProxy = "socks5://" + rawProxy
}
proxyURL, err := url.Parse(rawProxy)
if err != nil {
return nil, fmt.Errorf("parse socks5 proxy: %w", err)
}
if proxyURL.Scheme != "socks5" && proxyURL.Scheme != "socks5h" {
return nil, fmt.Errorf("unsupported proxy scheme %q", proxyURL.Scheme)
}
if proxyURL.User != nil {
return nil, fmt.Errorf("socks5 proxy authentication is not supported yet")
}
if _, _, err := net.SplitHostPort(proxyURL.Host); err != nil {
return nil, fmt.Errorf("invalid socks5 proxy host %q: %w", proxyURL.Host, err)
}
return proxyURL, nil
}