package mmtls import ( "bytes" "context" "fmt" "io" "net" "net/http" "net/http/httptrace" "strings" "sync" "time" "xiawan/wx/clientsdk/baseutils" ) var mmHTTPClientCache sync.Map func getMMHTTPClient(mmInfo *MMInfo, timeout time.Duration) *http.Client { key := fmt.Sprintf("%s|%d|%p|%t", mmInfo.ShortHost, int64(timeout/time.Millisecond), mmInfo.Dialer, mmInfo.AllowDirectOnProxyFail) if val, ok := mmHTTPClientCache.Load(key); ok { return val.(*http.Client) } dialer := &net.Dialer{Timeout: timeout, KeepAlive: 30 * time.Second} transport := &http.Transport{ DialContext: dialer.DialContext, ResponseHeaderTimeout: timeout, MaxIdleConns: 128, MaxIdleConnsPerHost: 128, IdleConnTimeout: 90 * time.Second, DisableKeepAlives: false, } if mmInfo.Dialer != nil { transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { type result struct { conn net.Conn err error } resultChan := make(chan result, 1) go func() { conn, err := mmInfo.Dialer.Dial(network, addr) resultChan <- result{conn: conn, err: err} }() select { case res := <-resultChan: if res.err != nil { baseutils.PrintLog(fmt.Sprintf("Proxy dial failed: %s", res.err.Error())) if mmInfo.AllowDirectOnProxyFail { baseutils.PrintLog("Proxy failed, falling back to direct connection") conn, err := dialer.DialContext(ctx, network, addr) if err != nil { return nil, fmt.Errorf("proxy and direct connection both failed: proxy=%v, direct=%v", res.err, err) } return conn, nil } return nil, fmt.Errorf("proxy connection failed and direct fallback is disabled: %w", res.err) } return res.conn, nil case <-time.After(timeout): baseutils.PrintLog("Proxy dial timeout") if mmInfo.AllowDirectOnProxyFail { baseutils.PrintLog("Proxy timeout, falling back to direct connection") conn, err := dialer.DialContext(ctx, network, addr) if err != nil { return nil, fmt.Errorf("proxy timeout and direct connection failed: %w", err) } return conn, nil } return nil, fmt.Errorf("proxy connection timeout and direct fallback is disabled") } } } client := &http.Client{ Transport: transport, Timeout: timeout, } actual, _ := mmHTTPClientCache.LoadOrStore(key, client) return actual.(*http.Client) } // MMHTTPPost mmtls方式发送数据包 func MMHTTPPost(mmInfo *MMInfo, data []byte, tag string) ([]byte, error) { startMs := time.Now().UnixMilli() var connReused bool var connWasIdle bool var connIdleMs int64 var remoteAddr string var connectStartMs int64 var connectDoneMs int64 var gotConnMs int64 var wroteRequestMs int64 var firstByteMs int64 requestURL := "http://" + mmInfo.ShortHost + mmInfo.ShortURL req, err := http.NewRequest("POST", requestURL, bytes.NewReader(data)) if err != nil { return nil, err } req.Header.Add("UserAgent", "MicroMessenger Client") req.Header.Add("Accept", "*/*") req.Header.Add("Cache-Control", "no-cache") req.Header.Add("Connection", "Keep-Alive") req.Header.Add("content-type", "application/octet-stream") req.Header.Add("Upgrade", "mmtls") req.Header.Add("Host", mmInfo.ShortHost) // fmt.Println(mmInfo.ShortHost) //szshort.weixin.qq.com // 获取超时配置 timeout := time.Duration(mmInfo.ShortConnTimeout) * time.Second if timeout <= 0 { timeout = 15 * time.Second } client := getMMHTTPClient(mmInfo, timeout) if strings.Contains(tag, "openwxhb") || strings.Contains(tag, "openunionhb") || strings.Contains(tag, "receivewxhb") || strings.Contains(tag, "unionhb") { trace := &httptrace.ClientTrace{ GotConn: func(info httptrace.GotConnInfo) { gotConnMs = time.Now().UnixMilli() connReused = info.Reused connWasIdle = info.WasIdle connIdleMs = int64(info.IdleTime / time.Millisecond) if info.Conn != nil && info.Conn.RemoteAddr() != nil { remoteAddr = info.Conn.RemoteAddr().String() } }, ConnectStart: func(network, addr string) { connectStartMs = time.Now().UnixMilli() }, ConnectDone: func(network, addr string, err error) { connectDoneMs = time.Now().UnixMilli() }, WroteRequest: func(info httptrace.WroteRequestInfo) { wroteRequestMs = time.Now().UnixMilli() }, GotFirstResponseByte: func() { firstByteMs = time.Now().UnixMilli() }, } req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace)) } resp, err := client.Do(req) if err != nil { baseutils.PrintLog(err.Error()) return nil, err } if strings.Contains(tag, "openwxhb") || strings.Contains(tag, "openunionhb") || strings.Contains(tag, "receivewxhb") || strings.Contains(tag, "unionhb") { fmt.Printf("MMHTTPPost resp tag=%s status=%s connHeader=%s close=%t\n", tag, resp.Status, resp.Header.Get("Connection"), resp.Close) } // 接收响应 defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } endMs := time.Now().UnixMilli() if strings.Contains(tag, "openwxhb") || strings.Contains(tag, "openunionhb") || strings.Contains(tag, "receivewxhb") || strings.Contains(tag, "unionhb") { connectMs := int64(0) if connectStartMs > 0 && connectDoneMs > 0 { connectMs = connectDoneMs - connectStartMs } ttfbMs := int64(0) if firstByteMs > 0 { ttfbMs = firstByteMs - startMs } writeMs := int64(0) if wroteRequestMs > 0 { writeMs = wroteRequestMs - startMs } gotConnDelayMs := int64(0) if gotConnMs > 0 { gotConnDelayMs = gotConnMs - startMs } fmt.Printf("MMHTTPPost trace tag=%s reused=%t wasIdle=%t idle=%dms gotConn=%dms connect=%dms wrote=%dms ttfb=%dms total=%dms\n", tag, connReused, connWasIdle, connIdleMs, gotConnDelayMs, connectMs, writeMs, ttfbMs, endMs-startMs) if remoteAddr != "" { fmt.Printf("MMHTTPPost peer tag=%s remote=%s\n", tag, remoteAddr) } } // 返回响应数据 return body, nil } // MMHTTPPostData mmtls短链接方式发送请求数据包 func MMHTTPPostData(mmInfo *MMInfo, url string, data []byte) ([]byte, error) { startMs := time.Now().UnixMilli() createStartMs := startMs // 创建HttpHandler httpHandler := &HTTPHandler{} httpHandler.URL = url httpHandler.Host = mmInfo.ShortHost httpHandler.MMPkg = data // 创建发送请求项列表 sendItems, err := CreateSendPackItems(mmInfo, httpHandler) createEndMs := time.Now().UnixMilli() if err != nil { return []byte{}, err } // MMTLS-加密要发送的数据 packStartMs := time.Now().UnixMilli() packData, err := MMHTTPPackData(mmInfo, sendItems) packEndMs := time.Now().UnixMilli() if err != nil { return []byte{}, err } // 发送数据 postStartMs := time.Now().UnixMilli() respData, err := MMHTTPPost(mmInfo, packData, url) postEndMs := time.Now().UnixMilli() // 解包响应数据 decodeStartMs := time.Now().UnixMilli() decodeData, err := MMDecodeResponseData(mmInfo, sendItems, respData) decodeEndMs := time.Now().UnixMilli() if err != nil { baseutils.PrintLog(err.Error()) return nil, err } if strings.Contains(url, "openwxhb") || strings.Contains(url, "openunionhb") || strings.Contains(url, "receivewxhb") || strings.Contains(url, "unionhb") { totalMs := decodeEndMs - startMs fmt.Printf("MMHTTPPostData url=%s create=%dms pack=%dms post=%dms decode=%dms total=%dms\n", url, createEndMs-createStartMs, packEndMs-packStartMs, postEndMs-postStartMs, decodeEndMs-decodeStartMs, totalMs) } return decodeData, nil } /* * 纯Http请求 */ func HTTPPost(mmInfo *MMInfo, cgi string, data []byte) ([]byte, error) { requestURL := "http://" + mmInfo.ShortURL + cgi request, err := http.NewRequest("POST", requestURL, bytes.NewReader(data)) if err != nil { return nil, err } request.Header.Add("UserAgent", "MicroMessenger Client") request.Header.Add("Accept", "*/*") request.Header.Add("Cache-Control", "no-cache") request.Header.Add("Connection", "Keep-Alive") request.Header.Add("content-type", "application/octet-stream") // 获取超时配置 timeout := time.Duration(mmInfo.ShortConnTimeout) * time.Second if timeout <= 0 { timeout = 15 * time.Second } // 发送请求 httpTransport := &http.Transport{ Dial: func(network, addr string) (net.Conn, error) { conn, err := net.DialTimeout(network, addr, timeout) if err != nil { return nil, err } conn.SetDeadline(time.Now().Add(timeout)) return conn, nil }, ResponseHeaderTimeout: timeout, } // 如果有代理 if mmInfo.Dialer != nil { httpTransport.Dial = func(network, addr string) (net.Conn, error) { type result struct { conn net.Conn err error } resultChan := make(chan result, 1) go func() { conn, err := mmInfo.Dialer.Dial(network, addr) resultChan <- result{conn: conn, err: err} }() select { case res := <-resultChan: if res.err != nil { if mmInfo.AllowDirectOnProxyFail { baseutils.PrintLog("HTTPPost: Proxy failed, falling back to direct connection") conn, err := net.DialTimeout(network, addr, timeout) if err != nil { return nil, fmt.Errorf("proxy and direct connection both failed: %w", err) } conn.SetDeadline(time.Now().Add(timeout)) return conn, nil } return nil, fmt.Errorf("proxy connection failed: %w", res.err) } if res.conn != nil { res.conn.SetDeadline(time.Now().Add(timeout)) } return res.conn, nil case <-time.After(timeout): if mmInfo.AllowDirectOnProxyFail { baseutils.PrintLog("HTTPPost: Proxy timeout, falling back to direct connection") conn, err := net.DialTimeout(network, addr, timeout) if err != nil { return nil, fmt.Errorf("proxy timeout and direct connection failed: %w", err) } conn.SetDeadline(time.Now().Add(timeout)) return conn, nil } return nil, fmt.Errorf("proxy connection timeout") } } } client := &http.Client{ Transport: httpTransport, Timeout: timeout, } resp, err := client.Do(request) if err != nil { baseutils.PrintLog(err.Error()) return nil, err } // 接收响应 defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } // 返回响应数据 return body, nil }