summaryrefslogtreecommitdiff
path: root/internal/ui/format.go
blob: 8038d42f682dbfd3a1314985271872f6e6eba3d3 (plain)
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
}