Files
2026-07-21 16:35:46 +08:00

266 lines
7.3 KiB
Go

package testweb
import (
"encoding/base64"
"encoding/json"
"io"
"io/fs"
"net/http"
"strings"
)
// Deps testweb 依赖。
type Deps struct {
SuperviseTarget string
FileTarget string
APILogRoot string
}
// Register 注册 /t 页面与 API。
func Register(mux *http.ServeMux, deps Deps) {
s := &server{deps: deps}
sub, _ := fs.Sub(webFS, "web")
static := http.FileServer(http.FS(sub))
mux.HandleFunc("/t", s.handlePage)
mux.HandleFunc("/t/", func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/t/static/") {
http.StripPrefix("/t/static/", static).ServeHTTP(w, r)
return
}
if r.URL.Path == "/t" || r.URL.Path == "/t/" {
s.handlePage(w, r)
return
}
if r.URL.Path == "/t/prescription" || r.URL.Path == "/t/prescription/" {
s.handlePrescriptionPage(w, r)
return
}
http.NotFound(w, r)
})
mux.HandleFunc("/t/api/meta", s.handleMeta)
mux.HandleFunc("/t/api/logs", s.handleLogs)
mux.HandleFunc("/t/api/logs/detail", s.handleLogDetail)
mux.HandleFunc("/t/api/logs/export", s.handleLogExport)
mux.HandleFunc("/t/api/prescription/records", s.handlePrescriptionRecords)
mux.HandleFunc("/t/api/prescription/rebuild", s.handlePrescriptionRebuild)
mux.HandleFunc("/t/api/test/connect", s.handleTestConnect)
mux.HandleFunc("/t/api/test/send", s.handleTestSend)
}
type server struct {
deps Deps
}
func (s *server) handlePage(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/t" && r.URL.Path != "/t/" {
http.NotFound(w, r)
return
}
b, err := webFS.ReadFile("web/index.html")
if err != nil {
http.Error(w, "page not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write(b)
}
func (s *server) handleMeta(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, map[string]any{
"superviseTarget": s.deps.SuperviseTarget,
"fileTarget": s.deps.FileTarget,
"apiLogRoot": s.deps.APILogRoot,
})
}
func (s *server) handleLogs(w http.ResponseWriter, _ *http.Request) {
list, err := ListFileLogs(s.deps.APILogRoot)
if err != nil {
writeErr(w, err.Error(), http.StatusInternalServerError)
return
}
if list == nil {
list = []FileLogEntry{}
}
writeJSON(w, list)
}
func (s *server) handleLogDetail(w http.ResponseWriter, r *http.Request) {
rel := r.URL.Query().Get("rel")
parsed, err := ParseAPILogFile(s.deps.APILogRoot, rel)
if err != nil {
writeErr(w, err.Error(), http.StatusBadRequest)
return
}
out := toDetailDTO(parsed)
writeJSON(w, out)
}
type logDetailJSON struct {
Rel string `json:"rel"`
Meta map[string]string `json:"meta"`
Inbound sectionDTO `json:"inbound"`
Outbound sectionDTO `json:"outbound"`
Response ResponseSection `json:"response"`
ParseWarnings []string `json:"parseWarnings,omitempty"`
OutboundAbsent bool `json:"outboundAbsent"`
}
type sectionDTO 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"`
BodyPlain bool `json:"bodyPlain"`
}
func toDetailDTO(p *ParsedAPILog) logDetailJSON {
return logDetailJSON{
Rel: p.Rel,
Meta: p.Meta,
Inbound: sectionDTOFrom(p.Inbound),
Outbound: sectionDTOFrom(p.Outbound),
Response: p.Response,
ParseWarnings: p.ParseWarnings,
OutboundAbsent: p.OutboundAbsent,
}
}
func sectionDTOFrom(sec RequestSection) sectionDTO {
d := sectionDTO{
URL: sec.URL,
Method: sec.Method,
Headers: sec.Headers,
BodyRaw: sec.BodyRaw,
BodyLen: sec.BodyLen,
BodyBase64: sec.BodyBase64,
BodyPlain: sec.BodyPlain,
}
if len(sec.BodyBytes) > 0 && !sec.BodyPlain {
d.BodyBase64 = base64.StdEncoding.EncodeToString(sec.BodyBytes)
}
return d
}
func (s *server) handleLogExport(w http.ResponseWriter, r *http.Request) {
rel := r.URL.Query().Get("rel")
useInbound := r.URL.Query().Get("source") == "inbound"
parsed, err := ParseAPILogFile(s.deps.APILogRoot, rel)
if err != nil {
writeErr(w, err.Error(), http.StatusBadRequest)
return
}
data, err := ToPostmanCollection(parsed, useInbound)
if err != nil {
writeErr(w, err.Error(), http.StatusInternalServerError)
return
}
name := "forward-file-" + strings.ReplaceAll(rel, "/", "_") + ".postman_collection.json"
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Content-Disposition", "attachment; filename=\""+name+"\"")
_, _ = w.Write(data)
}
func (s *server) handleTestConnect(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
Channel string `json:"channel"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErr(w, "invalid json", http.StatusBadRequest)
return
}
ch := strings.ToLower(strings.TrimSpace(req.Channel))
var results []ConnectResult
switch ch {
case "supervise", "28212":
results = append(results, TestConnectivity("supervise", s.deps.SuperviseTarget))
case "file", "28211":
results = append(results, TestConnectivity("file", s.deps.FileTarget))
default:
results = append(results,
TestConnectivity("supervise", s.deps.SuperviseTarget),
TestConnectivity("file", s.deps.FileTarget),
)
}
writeJSON(w, map[string]any{"results": results})
}
func (s *server) handleTestSend(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
body, _ := io.ReadAll(r.Body)
var req struct {
Rel string `json:"rel"`
UseOutbound bool `json:"useOutbound"`
URL string `json:"url"`
Headers map[string]string `json:"headers"`
BodyBase64 string `json:"bodyBase64"`
}
if err := json.Unmarshal(body, &req); err != nil {
writeErr(w, "invalid json", http.StatusBadRequest)
return
}
url := strings.TrimSpace(req.URL)
headers := req.Headers
var payload []byte
if req.Rel != "" {
parsed, err := ParseAPILogFile(s.deps.APILogRoot, req.Rel)
if err != nil {
writeErr(w, err.Error(), http.StatusBadRequest)
return
}
sec := parsed.Outbound
if !req.UseOutbound {
sec = parsed.Inbound
}
if url == "" {
url = sec.URL
}
if len(headers) == 0 {
headers = sec.Headers
}
payload = sec.BodyBytes
}
if req.BodyBase64 != "" {
dec, err := base64.StdEncoding.DecodeString(req.BodyBase64)
if err != nil {
writeErr(w, "invalid bodyBase64", http.StatusBadRequest)
return
}
payload = dec
}
if url == "" {
writeErr(w, "url required", http.StatusBadRequest)
return
}
if headers == nil {
headers = map[string]string{}
}
result := ReplayRequest(url, headers, payload)
writeJSON(w, result)
}
func writeJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(v)
}
func writeErr(w http.ResponseWriter, msg string, code int) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(map[string]string{"error": msg})
}