319 lines
8.1 KiB
Go
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
|
|
}
|