Repository navigation
Expand file tree
/
Copy pathcontext.go
More file actions
247 lines (220 loc) · 7.52 KB
/
Copy pathcontext.go
File metadata and controls
247 lines (220 loc) · 7.52 KB
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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
// Copyright 2025 Sylvain Müller. All rights reserved.
// Mount of this source code is governed by a Apache-2.0 license that can be found
// at https://github.com/stdapi/edge/blob/main/LICENSE.txt.
package edge
import (
"io"
"net/http"
"net/netip"
"net/url"
"github.com/stdapi/edge/internal/stringsutil"
)
// Param is a matched pattern parameter.
type Param struct {
Key string
Value string
}
// RequestContext is the read-only view of a request handed to a [RequestMatcher]. It is
// valid during the [RequestMatcher.Match] call and reused once that call returns. It exposes
// the request and nothing of the connection, which serves many requests.
type RequestContext interface {
// Request returns the request being routed.
Request() *http.Request
// Method returns the request method.
Method() string
// Scheme returns the scheme of the request target, http or https.
Scheme() string
// Host returns the hostname of the request target, without its port.
Host() string
// Port returns the port of the request target, always explicit.
Port() string
// Authority returns the request target as host:port.
Authority() string
// RoutingPath returns the canonical form of the path the client sent, which is the form
// the tree matched on. Percent-encoded RFC 3986 unreserved characters are decoded, bytes
// that must be escaped in a path are encoded in place, and every other escape keeps
// uppercase hex and stays distinct from its decoded form, so %2F never matches /. An empty
// path is /, and so is the "*" of an asterisk-form target.
RoutingPath() string
// QueryParams parses the raw query and caches the result.
QueryParams() url.Values
// QueryParam returns the first value for name.
QueryParam(name string) string
// Header returns the first request header value for key.
Header(key string) string
// RemoteAddr returns the address the request was received from, or the zero value when
// it has none.
RemoteAddr() netip.AddrPort
// Protocol returns the protocol the request was received over.
Protocol() Protocol
sealed()
}
// routeCtx is the state of one routing lookup, what the tree walks with, what it captures and
// what the handler reads back. It is pooled per tree, and the HTTP view lives inside it so
// serving a request allocates nothing.
type routeCtx struct {
// w is the client writer a 1xx from the origin is relayed to, nil where none may be.
w http.ResponseWriter
body io.ReadCloser
req *http.Request
sess *Session
// client is the request as the server read it and body is its body, which a handler may
// replace on the request. Only dispatch sets them, since a gRPC call routes each message
// again from the transport while its handler swaps the body.
client *http.Request
params *[]string
skipStack *skipStack
entry *entry
p *Proxy // no reset, and the only thing the state keeps of the proxy
cachedQueries url.Values
// http points back at this state, so a handler is handed &c.http and nothing is
// allocated to serve a request.
http HTTPCtx
relay *infoRelay
// inflight is the trace of a round trip without a relay.
inflight *inflightTrace
// upgraded is the relay that recorded the connection of the last round trip, which a 101
// hands over.
upgraded *infoRelay
// rq is the request in the list of server.requests, until its session takes it over.
rq *request
// grpc borrows the same state through http, a call being a request underneath, so
// serving one allocates nothing either.
grpc GRPCCtx
scheme string
host string
port string
identity string
lookup lookupState
// expectContinue is set when net/http writes a 100 Continue from the first read of the
// request body.
expectContinue bool
}
func (c *routeCtx) reset(sess *Session, r *http.Request, scheme, host, port string) {
c.req = r
c.client = nil
c.body = nil
c.sess = sess
c.scheme = scheme
c.host = host
c.port = port
c.cachedQueries = nil
c.entry = nil
c.identity = ""
c.w = nil
c.expectContinue = false
c.rq = nil
if c.upgraded != nil {
c.upgraded.forget()
c.upgraded = nil
}
c.grpc.reset()
*c.params = (*c.params)[:0]
}
func (c *routeCtx) resetForAdmission(sess *Session, scheme, host, port string) {
c.reset(sess, nil, scheme, host, port)
}
// capturedParams copies the parameters captured by the last lookup, for a handler that
// outlives the pooled state the walk used.
func (c *routeCtx) capturedParams() []Param {
return c.appendParams(nil)
}
// appendParams appends the parameters captured by the last lookup to dst, reusing its storage.
func (c *routeCtx) appendParams(dst []Param) []Param {
if c.entry == nil || len(*c.params) == 0 {
return dst[:0]
}
n := min(len(*c.params), len(c.entry.params))
if cap(dst) < n {
dst = make([]Param, 0, n)
}
dst = dst[:0]
for i := range n {
dst = append(dst, Param{Key: c.entry.params[i], Value: (*c.params)[i]})
}
return dst
}
// routingPath returns the canonical form of the request path used for matching. The request
// is never rewritten to it, since a proxy must put back on the wire exactly what the client
// sent.
func routingPath(r *http.Request) string {
if r == nil || r.URL == nil {
return ""
}
u := r.URL
if u.RawPath == "" {
if u.Path == "" || u.Path == "*" {
// An absolute-form target may have an empty path, and a lookup without one
// reaches the admission of its authority. The asterisk-form has an empty path
// too, RFC 9112 section 3.3.
return "/"
}
// The wire path was the default encoding of Path, which is always canonical.
return stringsutil.EscapePath(u.Path)
}
norm, _ := stringsutil.NormalizeRawPath(u.RawPath, u.Path)
if norm == "" {
// RawPath holds a malformed escape or is not an encoding of Path. Like net/url,
// Path is then the source of truth.
return stringsutil.EscapePath(u.Path)
}
return norm
}
func (c *routeCtx) getQueries() url.Values {
if c.cachedQueries == nil {
if c.req != nil && c.req.URL != nil {
c.cachedQueries = c.req.URL.Query()
} else {
c.cachedQueries = make(url.Values)
}
}
return c.cachedQueries
}
// onlyRequestContext seals the routing state down to [RequestContext] so a [RequestMatcher]
// cannot reach the mutating half of the API.
type onlyRequestContext struct {
c *routeCtx
}
func (o onlyRequestContext) Request() *http.Request { return o.c.req }
func (o onlyRequestContext) Method() string {
if o.c.req == nil {
return ""
}
return o.c.req.Method
}
func (o onlyRequestContext) Scheme() string { return o.c.scheme }
func (o onlyRequestContext) Host() string { return o.c.host }
func (o onlyRequestContext) Port() string { return o.c.port }
func (o onlyRequestContext) Authority() string { return joinHostPort(o.c.host, o.c.port) }
func (o onlyRequestContext) RoutingPath() string { return routingPath(o.c.req) }
func (o onlyRequestContext) QueryParams() url.Values { return o.c.getQueries() }
func (o onlyRequestContext) QueryParam(n string) string {
return o.c.getQueries().Get(n)
}
func (o onlyRequestContext) Header(k string) string {
if o.c.req == nil {
return ""
}
return o.c.req.Header.Get(k)
}
func (o onlyRequestContext) RemoteAddr() netip.AddrPort {
if o.c.req == nil {
return netip.AddrPort{}
}
ap, err := netip.ParseAddrPort(o.c.req.RemoteAddr)
if err != nil {
return netip.AddrPort{}
}
return netip.AddrPortFrom(ap.Addr().Unmap(), ap.Port())
}
func (o onlyRequestContext) Protocol() Protocol {
if o.c.req == nil {
return 0
}
return protoOfRequest(o.c.req)
}
func (o onlyRequestContext) sealed() {}
var (
_ RequestContext = onlyRequestContext{}
_ RequestContext = (*HTTPCtx)(nil)
)