Files
xk-hy-forward-go/internal/forward/testweb/apilog_parse.go
2026-05-28 17:04:29 +08:00

319 lines
8.1 KiB
Go

package testweb
import (
"encoding/base64"
"fmt"
"os"
"regexp"
"strconv"
"strings"
)
var (
blockInboundRE = regexp.MustCompile(`(?m)^=== 1\. 入站请求`)
blockOutboundRE = regexp.MustCompile(`(?m)^=== 2\. 出站转发`)
blockResponseRE = regexp.MustCompile(`(?m)^=== 3\. 回复`)
)
// ParsedAPILog 解析后的 api-log 三段结构。
type ParsedAPILog struct {
Rel string `json:"rel"`
Meta map[string]string `json:"meta"`
Inbound RequestSection `json:"inbound"`
Outbound RequestSection `json:"outbound"`
Response ResponseSection `json:"response"`
ParseWarnings []string `json:"parseWarnings,omitempty"`
OutboundAbsent bool `json:"outboundAbsent"`
}
// RequestSection 请求段(入站或出站)。
type RequestSection struct {
URL string `json:"url,omitempty"`
Method string `json:"method,omitempty"`
Headers map[string]string `json:"headers"`
BodyRaw string `json:"bodyRaw,omitempty"`
BodyLen int `json:"bodyLen"`
BodyBase64 string `json:"bodyBase64,omitempty"`
BodyBytes []byte `json:"-"`
BodyPlain bool `json:"bodyPlain"`
}
// ResponseSection 上游/客户端回复段。
type ResponseSection struct {
Status int `json:"status,omitempty"`
StatusText string `json:"statusText,omitempty"`
Headers map[string]string `json:"headers"`
Body string `json:"body,omitempty"`
BodyJSON string `json:"bodyJSON,omitempty"`
UpstreamSet bool `json:"upstreamSet"`
}
// ParseAPILogBytes 解析 api-log 文件内容。
func ParseAPILogBytes(rel string, raw []byte) (*ParsedAPILog, error) {
content := string(raw)
p := &ParsedAPILog{Rel: rel}
p.Meta = parseMeta(content)
inboundStart := blockInboundRE.FindStringIndex(content)
outboundStart := blockOutboundRE.FindStringIndex(content)
responseStart := blockResponseRE.FindStringIndex(content)
if inboundStart == nil {
return nil, fmt.Errorf("missing inbound block")
}
var inboundText, outboundText, responseText string
if outboundStart != nil {
inboundText = content[inboundStart[0]:outboundStart[0]]
} else if responseStart != nil {
inboundText = content[inboundStart[0]:responseStart[0]]
} else {
inboundText = content[inboundStart[0]:]
}
if outboundStart != nil {
if responseStart != nil {
outboundText = content[outboundStart[0]:responseStart[0]]
} else {
outboundText = content[outboundStart[0]:]
}
}
if responseStart != nil {
responseText = content[responseStart[0]:]
}
var warns []string
in, w := parseRequestSection(inboundText, true)
p.Inbound = in
warns = append(warns, w...)
if outboundText != "" {
if strings.Contains(outboundText, "(未发起出站转发)") {
p.OutboundAbsent = true
} else {
out, w2 := parseRequestSection(outboundText, false)
p.Outbound = out
warns = append(warns, w2...)
}
} else {
p.OutboundAbsent = true
}
if responseText != "" {
p.Response = parseResponseSection(responseText)
}
p.ParseWarnings = warns
return p, nil
}
// ParseAPILogFile 读取并解析 api-log 文件。
func ParseAPILogFile(root, rel string) (*ParsedAPILog, error) {
path, err := resolveLogRelPath(root, rel)
if err != nil {
return nil, err
}
raw, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return ParseAPILogBytes(rel, raw)
}
func parseMeta(content string) map[string]string {
meta := make(map[string]string)
idx := strings.Index(content, "=== META ===")
if idx < 0 {
return meta
}
end := strings.Index(content[idx:], "\n\n")
section := content[idx:]
if end > 0 {
section = section[:end]
}
for _, line := range strings.Split(section, "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "===") {
continue
}
if k, v, ok := strings.Cut(line, ": "); ok {
meta[k] = v
}
}
return meta
}
func parseRequestSection(section string, inbound bool) (RequestSection, []string) {
var rs RequestSection
rs.Headers = make(map[string]string)
var warns []string
lines := strings.Split(section, "\n")
i := 0
for i < len(lines) && strings.TrimSpace(lines[i]) == "" {
i++
}
for i < len(lines) {
line := lines[i]
if strings.HasPrefix(line, "URL: ") {
rs.URL = strings.TrimSpace(strings.TrimPrefix(line, "URL: "))
if inbound && strings.HasPrefix(rs.URL, "POST ") {
rs.URL = strings.TrimSpace(strings.TrimPrefix(rs.URL, "POST "))
}
i++
continue
}
if strings.HasPrefix(line, "TargetURL: ") {
rs.URL = strings.TrimSpace(strings.TrimPrefix(line, "TargetURL: "))
i++
continue
}
if strings.HasPrefix(line, "Method: ") {
rs.Method = strings.TrimSpace(strings.TrimPrefix(line, "Method: "))
i++
continue
}
if strings.TrimSpace(line) == "Headers:" {
i++
for i < len(lines) {
hl := lines[i]
if hl == "" || strings.HasPrefix(hl, "Body:") || strings.HasPrefix(hl, "===") {
break
}
if k, v, ok := strings.Cut(hl, ": "); ok {
rs.Headers[k] = v
}
i++
}
continue
}
if strings.TrimSpace(line) == "Body:" {
i++
bodyLines, consumed, w := readBodyLines(lines[i:])
warns = append(warns, w...)
i += consumed
applyBodyToSection(&rs, bodyLines)
continue
}
i++
}
return rs, warns
}
func readBodyLines(lines []string) (bodyLines []string, consumed int, warns []string) {
for consumed < len(lines) {
line := lines[consumed]
if strings.HasPrefix(line, "===") {
break
}
bodyLines = append(bodyLines, line)
consumed++
}
return bodyLines, consumed, warns
}
func applyBodyToSection(rs *RequestSection, bodyLines []string) {
text := strings.Join(bodyLines, "\n")
text = strings.TrimSuffix(text, "\n")
if text == "" || text == "(empty)" {
return
}
if strings.HasPrefix(text, "body_len=") {
for _, line := range bodyLines {
if strings.HasPrefix(line, "body_len=") {
if n, err := strconv.Atoi(strings.TrimPrefix(line, "body_len=")); err == nil {
rs.BodyLen = n
}
}
if strings.HasPrefix(line, "body_base64=") {
b64 := strings.TrimPrefix(line, "body_base64=")
rs.BodyBase64 = b64
dec, err := base64.StdEncoding.DecodeString(b64)
if err != nil {
return
}
rs.BodyBytes = dec
rs.BodyLen = len(dec)
}
}
return
}
rs.BodyRaw = text
rs.BodyPlain = true
rs.BodyBytes = []byte(text)
rs.BodyLen = len(rs.BodyBytes)
}
func parseResponseSection(section string) ResponseSection {
var rs ResponseSection
rs.Headers = make(map[string]string)
lines := strings.Split(section, "\n")
i := 0
for i < len(lines) && strings.TrimSpace(lines[i]) == "" {
i++
}
for i < len(lines) {
line := lines[i]
if strings.HasPrefix(line, "UpstreamStatus: ") {
st := strings.TrimSpace(strings.TrimPrefix(line, "UpstreamStatus: "))
if st == "(无上游响应)" {
rs.UpstreamSet = false
} else {
rs.UpstreamSet = true
if n, err := strconv.Atoi(st); err == nil {
rs.Status = n
}
}
i++
continue
}
if strings.TrimSpace(line) == "UpstreamHeaders:" || strings.TrimSpace(line) == "ClientResponseHeaders:" {
i++
for i < len(lines) {
hl := lines[i]
if hl == "" || strings.HasPrefix(hl, "Body:") || strings.HasPrefix(hl, "BodyJSON:") || strings.HasPrefix(hl, "===") {
break
}
if k, v, ok := strings.Cut(hl, ": "); ok {
rs.Headers[k] = v
}
i++
}
continue
}
if strings.TrimSpace(line) == "Body:" {
i++
var bodyLines []string
for i < len(lines) {
if strings.TrimSpace(lines[i]) == "BodyJSON:" {
break
}
if strings.HasPrefix(lines[i], "===") {
break
}
bodyLines = append(bodyLines, lines[i])
i++
}
rs.Body = strings.TrimSuffix(strings.Join(bodyLines, "\n"), "\n")
continue
}
if strings.TrimSpace(line) == "BodyJSON:" {
i++
var jsonLines []string
for i < len(lines) {
if strings.HasPrefix(lines[i], "===") {
break
}
jsonLines = append(jsonLines, lines[i])
i++
}
rs.BodyJSON = strings.TrimSuffix(strings.Join(jsonLines, "\n"), "\n")
continue
}
i++
}
if rs.Status > 0 || rs.Body != "" {
rs.UpstreamSet = true
}
return rs
}