1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
|
package ui
import (
"bytes"
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
"github.com/shiv/internal/store"
)
// PathOf extracts the path+query portion from a full URL string.
func PathOf(rawURL string) string {
for _, prefix := range []string{"https://", "http://"} {
if strings.HasPrefix(rawURL, prefix) {
rest := rawURL[len(prefix):]
if slash := strings.Index(rest, "/"); slash >= 0 {
return rest[slash:]
}
return "/"
}
}
return rawURL
}
// PrettyJSON re-indents JSON, returning the original bytes on any error.
func PrettyJSON(body []byte) []byte {
if len(body) == 0 {
return body
}
var buf bytes.Buffer
if err := json.Indent(&buf, body, "", " "); err != nil {
return body
}
return buf.Bytes()
}
// hostHeader returns the value to use for the Host header in a reconstructed
// raw request. Default ports (80 for HTTP, 443 for HTTPS) are omitted per
// RFC 7230 §5.4. Non-standard ports are always included.
func hostHeader(tx store.Transaction) string {
if tx.Port == 0 || (tx.TLS && tx.Port == 443) || (!tx.TLS && tx.Port == 80) {
return tx.Host
}
return tx.Host + ":" + strconv.Itoa(tx.Port)
}
// FormatStoreRequest serialises a Transaction into a raw HTTP/1.1 request string
// suitable for display in the UI or sending via Repeater/Intruder.
//
// The protocol is always written as HTTP/1.1 regardless of what was captured —
func FormatStoreRequest(tx store.Transaction) string {
proto := tx.Proto
if proto == "" {
proto = "HTTP/1.1"
}
var b strings.Builder
fmt.Fprintf(&b, "%s %s %s\r\n", tx.Method, PathOf(tx.URL), proto)
fmt.Fprintf(&b, "Host: %s\r\n", hostHeader(tx))
keys := sortedKeys(tx.ReqHeaders)
for _, k := range keys {
// Skip Host — we already wrote a canonical one above.
if strings.EqualFold(k, "host") {
continue
}
for _, v := range tx.ReqHeaders[k] {
fmt.Fprintf(&b, "%s: %s\r\n", k, v)
}
}
b.WriteString("\r\n")
if len(tx.ReqBody) > 0 {
b.Write(PrettyJSON(tx.ReqBody))
}
return b.String()
}
// FormatStoreResponse serialises a Transaction into a raw HTTP response string
// suitable for display in the UI.
//
// The store already holds decompressed body bytes — we must NOT decompress again.
func FormatStoreResponse(tx store.Transaction) string {
var b strings.Builder
// Always render as HTTP/1.1; H2 pseudo-headers are not meaningful in raw text.
const proto = "HTTP/1.1"
fmt.Fprintf(&b, "%s %d\r\n", proto, tx.StatusCode)
keys := sortedKeys(tx.RespHeaders)
for _, k := range keys {
for _, v := range tx.RespHeaders[k] {
fmt.Fprintf(&b, "%s: %s\r\n", k, v)
}
}
b.WriteString("\r\n")
if len(tx.RespBody) > 0 {
// tx.RespBody is already decompressed by the proxy before storage.
// Do NOT call Decompress here — the body is plain bytes.
body := tx.RespBody
ct := tx.RespHeaders.Get("Content-Type")
if strings.Contains(ct, "application/json") {
b.Write(PrettyJSON(body))
} else {
b.Write(body)
}
}
return b.String()
}
func sortedKeys(h map[string][]string) []string {
keys := make([]string, 0, len(h))
for k := range h {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
|