Skip to content

Commit 4824e6d

Browse files
committed
feat: reimplement req builder
1 parent f95acdd commit 4824e6d

File tree

6 files changed

+266
-281
lines changed

6 files changed

+266
-281
lines changed

pkg/render/helpers.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package render
22

3-
import "strings"
3+
import (
4+
"fmt"
5+
"net"
6+
"strconv"
7+
"strings"
8+
)
49

510
type CustomBuilder struct {
611
strings.Builder
@@ -10,3 +15,32 @@ type CustomBuilder struct {
1015
func (b *CustomBuilder) PadAdd(s string) {
1116
b.WriteString(" " + s + "\n")
1217
}
18+
19+
// ParseBackend parses "<HOST/IP>:<PORT>" where HOST may be a hostname,
20+
// IPv4, or IPv6 (possibly unbracketed). Returns host and port separately.
21+
func ParseBackend(s string) (host, port string, err error) {
22+
// Try the standard parser first (works for "host:port" and "[v6]:port").
23+
host, port, err = net.SplitHostPort(s)
24+
if err != nil {
25+
// Fallback: split at the last colon. This handles unbracketed IPv6 like "fe80::1%eth0:8080".
26+
i := strings.LastIndex(s, ":")
27+
if i == -1 {
28+
return "", "", fmt.Errorf("missing port in %q", s)
29+
}
30+
host = s[:i]
31+
port = s[i+1:]
32+
if host == "" {
33+
return "", "", fmt.Errorf("empty host in %q", s)
34+
}
35+
if _, e := strconv.Atoi(port); e != nil {
36+
return "", "", fmt.Errorf("invalid port %q: %v", port, e)
37+
}
38+
}
39+
40+
// If it is an IPv6, surrond it with []
41+
if strings.Contains(host, ":") && !strings.Contains(host, "[") && !strings.Contains(host, "]") {
42+
host = fmt.Sprintf("[%s]", host)
43+
}
44+
45+
return host, port, nil
46+
}

pkg/render/httpmodel.go

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
package render
22

33
import (
4+
"fmt"
5+
"net"
6+
"sort"
7+
"strings"
8+
49
"github.com/aorith/varnishlog-parser/vsl"
510
"github.com/aorith/varnishlog-parser/vsl/tag"
611
)
712

813
type HTTPRequest struct {
914
method string
1015
host string
16+
port string
1117
url string
1218
headers []Header
1319
}
@@ -19,17 +25,21 @@ type Header struct {
1925

2026
type Backend struct {
2127
host string
22-
port int
28+
port string
29+
}
30+
31+
func NewBackend(host string, port string) *Backend {
32+
return &Backend{host: host, port: port}
2333
}
2434

2535
// NewHTTPRequest constructs an HTTPRequest from a Varnish transaction.
2636
// Returns nil if the transaction type is session.
2737
//
2838
// If fromResponse is true, fields are extracted from the response; otherwise, from the request.
2939
// If received is true, initial (received) headers are used; otherwise, headers after VCL processing.
30-
func NewHTTPRequest(tx *vsl.Transaction, fromResponse, received bool) *HTTPRequest {
40+
func NewHTTPRequest(tx *vsl.Transaction, fromResponse, received bool) (*HTTPRequest, error) {
3141
if tx.Type() == vsl.TxTypeSession {
32-
return nil
42+
return nil, fmt.Errorf("cannot create an http request from a transaction of type session")
3343
}
3444

3545
var headers vsl.Headers
@@ -40,12 +50,24 @@ func NewHTTPRequest(tx *vsl.Transaction, fromResponse, received bool) *HTTPReque
4050
}
4151

4252
host := headers.Get("host", received)
53+
port := ""
54+
if strings.Contains(host, ":") {
55+
var err error
56+
host, port, err = ParseBackend(headers.Get("host", received))
57+
if err != nil {
58+
return nil, err
59+
}
60+
}
61+
4362
httpHeaders := []Header{}
4463
for name, h := range headers {
4564
if name == vsl.HdrNameHost {
4665
continue
4766
}
4867
for _, v := range h.Values(received) {
68+
if v.State() == vsl.HdrStateDeleted {
69+
continue
70+
}
4971
httpHeaders = append(httpHeaders, Header{name: name, value: v.Value()})
5072
}
5173
}
@@ -60,10 +82,74 @@ func NewHTTPRequest(tx *vsl.Transaction, fromResponse, received bool) *HTTPReque
6082
url = tx.RecordValueByTag(tag.BereqURL, received)
6183
}
6284

85+
sort.Slice(httpHeaders, func(i, j int) bool {
86+
return httpHeaders[i].name < httpHeaders[j].name
87+
})
88+
6389
return &HTTPRequest{
6490
method: method,
6591
host: host,
92+
port: port,
6693
url: url,
6794
headers: httpHeaders,
95+
}, nil
96+
}
97+
98+
// CurlCommand generates a new curl command as a string
99+
// scheme can be "auto", "http://" or "https://"
100+
func (r *HTTPRequest) CurlCommand(scheme string, backend *Backend) string {
101+
var s strings.Builder
102+
103+
// Parse scheme
104+
switch scheme {
105+
case "auto":
106+
if r.port == "443" {
107+
scheme = "https://"
108+
} else {
109+
// default to http for 80, empty, or any other port
110+
scheme = "http://"
111+
}
112+
case "http://", "https://":
113+
// keep as-is
114+
default:
115+
return "invalid scheme: " + scheme
116+
}
117+
118+
// Build host URL, append port only when provided
119+
hostURL := r.host
120+
if r.port != "" {
121+
hostURL = net.JoinHostPort(r.host, r.port)
122+
}
123+
124+
// Initial command
125+
s.WriteString(fmt.Sprintf(`curl "%s%s%s"`+" \\\n", scheme, hostURL, r.url))
126+
127+
switch r.method {
128+
case "POST", "PUT", "PATCH":
129+
s.WriteString(" -X " + r.method + " \\\n")
130+
s.WriteString(" -d '<body-unavailable>' \\\n")
131+
default:
132+
s.WriteString(" -X " + r.method + " \\\n")
133+
}
134+
135+
// Headers
136+
for _, h := range r.headers {
137+
if h.name == vsl.HdrNameHost {
138+
continue
139+
}
140+
hdrVal := strings.ReplaceAll(h.value, `"`, `\"`)
141+
s.WriteString(fmt.Sprintf(` -H "%s: %s"`+" \\\n", h.name, hdrVal))
142+
}
143+
144+
// Default parameters
145+
s.WriteString(" -qsv -k -o /dev/null")
146+
147+
// Connect-to
148+
// --connect-to HOST1:PORT1:HOST2:PORT2
149+
// when you would connect to HOST1:PORT1, actually connect to HOST2:PORT2
150+
if backend != nil {
151+
s.WriteString(fmt.Sprintf(" \\\n "+`--connect-to "%s:%s:%s"`, hostURL, backend.host, backend.port))
68152
}
153+
154+
return s.String()
69155
}

pkg/server/routes.go

Lines changed: 9 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -72,45 +72,21 @@ func (s *vlogServer) registerRoutes() http.Handler {
7272
return
7373
}
7474

75-
var rt int
76-
rtValue := r.Form.Get("sendTo")
77-
switch rtValue {
78-
case "domain":
79-
rt = content.SendToDomain
80-
case "backend":
81-
rt = content.SendToBackend
82-
case "localhost":
83-
rt = content.SendToLocalhost
75+
connectTo := r.Form.Get("connectTo")
76+
switch connectTo {
8477
case "custom":
85-
rt = content.SendToCustom
78+
connectTo = r.Form.Get("custom")
79+
case "backend":
80+
connectTo = r.Form.Get("transactionBackend")
8681
}
8782

8883
f := content.ReqBuilderForm{
89-
TXID: r.Form.Get("transaction"),
90-
HTTPS: r.Form.Get("https") == "on",
91-
OriginalHeaders: r.Form.Get("headerType") == "original",
92-
OriginalURL: r.Form.Get("urlType") == "original",
93-
ResolveTo: rt,
94-
CustomResolve: r.Form.Get("customResolve"),
95-
}
96-
97-
if f.ResolveTo == content.SendToBackend {
98-
f.CustomResolve = r.Form.Get("transactionBackend")
99-
}
100-
101-
// Find the tx which should still be present in the "New Parse" textarea
102-
tx, ok := txsSet.TransactionsMap()[f.TXID]
103-
if !ok {
104-
err = partials.ErrorMsg(fmt.Errorf(`Transaction %q not found. Did you reset the "New Parse" textarea?`, f.TXID)).Render(context.Background(), w)
105-
if err != nil {
106-
w.WriteHeader(http.StatusInternalServerError)
107-
fmt.Fprintln(w, err)
108-
return
109-
}
110-
return
84+
Scheme: r.Form.Get("scheme"),
85+
Received: r.Form.Get("headers") == "received", // Use the received method/url and headers
86+
ConnectTo: connectTo,
11187
}
11288

113-
err = content.ReqBuild(txsSet, tx, f).Render(context.Background(), w)
89+
err = content.ReqBuild(txsSet, f).Render(context.Background(), w)
11490
if err != nil {
11591
w.WriteHeader(http.StatusInternalServerError)
11692
fmt.Fprintln(w, err)

0 commit comments

Comments
 (0)