diff options
| author | Nicholas S. Husin <nsh@golang.org> | 2026-01-22 22:48:46 -0500 |
|---|---|---|
| committer | Nicholas Husin <nsh@golang.org> | 2026-01-23 11:09:53 -0800 |
| commit | ca5ffe0092363f21df2c57b50144ba056f260049 (patch) | |
| tree | e181aa7d9e80504a0173fc8dfc70241a08713db1 /src/net | |
| parent | 4af8ad24ee3b55ccb644680d95e2502e5551ea0b (diff) | |
| download | go-ca5ffe0092363f21df2c57b50144ba056f260049.tar.xz | |
all: update vendored dependencies
This CL does the following:
1. Bundles up golang.org/x/net/internal/httpsfv since h2_bundle.go now
relies on it.
2. Modifies h2_bundle.go import mapping to account for httpsfv package.
3. Updates all vendored dependencies using
golang.org/x/build/cmd/updatestd.
For #75500
Change-Id: Ia2f41ad606092fe20b62f946266190502b146977
Reviewed-on: https://go-review.googlesource.com/c/go/+/738621
Reviewed-by: Nicholas Husin <husin@google.com>
Reviewed-by: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Diffstat (limited to 'src/net')
| -rw-r--r-- | src/net/http/h2_bundle.go | 297 | ||||
| -rw-r--r-- | src/net/http/http.go | 2 | ||||
| -rw-r--r-- | src/net/http/internal/httpsfv/httpsfv.go | 666 |
3 files changed, 921 insertions, 44 deletions
diff --git a/src/net/http/h2_bundle.go b/src/net/http/h2_bundle.go index 5a7420bc7e..7793d1cd5e 100644 --- a/src/net/http/h2_bundle.go +++ b/src/net/http/h2_bundle.go @@ -1,7 +1,7 @@ //go:build !nethttpomithttp2 // Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT. -// $ bundle -o=h2_bundle.go -prefix=http2 -tags=!nethttpomithttp2 -import=golang.org/x/net/internal/httpcommon=net/http/internal/httpcommon golang.org/x/net/http2 +// $ bundle -o=h2_bundle.go -prefix=http2 -tags=!nethttpomithttp2 -import=golang.org/x/net/internal/httpcommon=net/http/internal/httpcommon -import=golang.org/x/net/internal/httpsfv=net/http/internal/httpsfv golang.org/x/net/http2 // Package http2 implements the HTTP/2 protocol. // @@ -36,11 +36,13 @@ import ( "net" "net/http/httptrace" "net/http/internal/httpcommon" + "net/http/internal/httpsfv" "net/textproto" "net/url" "os" "reflect" "runtime" + "slices" "sort" "strconv" "strings" @@ -1031,6 +1033,10 @@ func http2shouldRetryDial(call *http2dialCall, req *Request) bool { return call.ctx.Err() != nil } +func http2clientPriorityDisabled(s *Server) bool { + return s.DisableClientPriority +} + // http2Config is a package-internal version of net/http.HTTP2Config. // // http.HTTP2Config was added in Go 1.24. @@ -1586,33 +1592,36 @@ const http2frameHeaderLen = 9 var http2padZeros = make([]byte, 255) // zeros for padding // A FrameType is a registered frame type as defined in -// https://httpwg.org/specs/rfc7540.html#rfc.section.11.2 +// https://httpwg.org/specs/rfc7540.html#rfc.section.11.2 and other future +// RFCs. type http2FrameType uint8 const ( - http2FrameData http2FrameType = 0x0 - http2FrameHeaders http2FrameType = 0x1 - http2FramePriority http2FrameType = 0x2 - http2FrameRSTStream http2FrameType = 0x3 - http2FrameSettings http2FrameType = 0x4 - http2FramePushPromise http2FrameType = 0x5 - http2FramePing http2FrameType = 0x6 - http2FrameGoAway http2FrameType = 0x7 - http2FrameWindowUpdate http2FrameType = 0x8 - http2FrameContinuation http2FrameType = 0x9 + http2FrameData http2FrameType = 0x0 + http2FrameHeaders http2FrameType = 0x1 + http2FramePriority http2FrameType = 0x2 + http2FrameRSTStream http2FrameType = 0x3 + http2FrameSettings http2FrameType = 0x4 + http2FramePushPromise http2FrameType = 0x5 + http2FramePing http2FrameType = 0x6 + http2FrameGoAway http2FrameType = 0x7 + http2FrameWindowUpdate http2FrameType = 0x8 + http2FrameContinuation http2FrameType = 0x9 + http2FramePriorityUpdate http2FrameType = 0x10 ) var http2frameNames = [...]string{ - http2FrameData: "DATA", - http2FrameHeaders: "HEADERS", - http2FramePriority: "PRIORITY", - http2FrameRSTStream: "RST_STREAM", - http2FrameSettings: "SETTINGS", - http2FramePushPromise: "PUSH_PROMISE", - http2FramePing: "PING", - http2FrameGoAway: "GOAWAY", - http2FrameWindowUpdate: "WINDOW_UPDATE", - http2FrameContinuation: "CONTINUATION", + http2FrameData: "DATA", + http2FrameHeaders: "HEADERS", + http2FramePriority: "PRIORITY", + http2FrameRSTStream: "RST_STREAM", + http2FrameSettings: "SETTINGS", + http2FramePushPromise: "PUSH_PROMISE", + http2FramePing: "PING", + http2FrameGoAway: "GOAWAY", + http2FrameWindowUpdate: "WINDOW_UPDATE", + http2FrameContinuation: "CONTINUATION", + http2FramePriorityUpdate: "PRIORITY_UPDATE", } func (t http2FrameType) String() string { @@ -1688,16 +1697,17 @@ var http2flagName = map[http2FrameType]map[http2Flags]string{ type http2frameParser func(fc *http2frameCache, fh http2FrameHeader, countError func(string), payload []byte) (http2Frame, error) var http2frameParsers = [...]http2frameParser{ - http2FrameData: http2parseDataFrame, - http2FrameHeaders: http2parseHeadersFrame, - http2FramePriority: http2parsePriorityFrame, - http2FrameRSTStream: http2parseRSTStreamFrame, - http2FrameSettings: http2parseSettingsFrame, - http2FramePushPromise: http2parsePushPromise, - http2FramePing: http2parsePingFrame, - http2FrameGoAway: http2parseGoAwayFrame, - http2FrameWindowUpdate: http2parseWindowUpdateFrame, - http2FrameContinuation: http2parseContinuationFrame, + http2FrameData: http2parseDataFrame, + http2FrameHeaders: http2parseHeadersFrame, + http2FramePriority: http2parsePriorityFrame, + http2FrameRSTStream: http2parseRSTStreamFrame, + http2FrameSettings: http2parseSettingsFrame, + http2FramePushPromise: http2parsePushPromise, + http2FramePing: http2parsePingFrame, + http2FrameGoAway: http2parseGoAwayFrame, + http2FrameWindowUpdate: http2parseWindowUpdateFrame, + http2FrameContinuation: http2parseContinuationFrame, + http2FramePriorityUpdate: http2parsePriorityUpdateFrame, } func http2typeFrameParser(t http2FrameType) http2frameParser { @@ -2746,9 +2756,34 @@ type http2PriorityFrame struct { http2PriorityParam } -var http2defaultRFC9218Priority = http2PriorityParam{ - incremental: 0, - urgency: 3, +// defaultRFC9218Priority determines what priority we should use as the default +// value. +// +// According to RFC 9218, by default, streams should be given an urgency of 3 +// and should be non-incremental. However, making streams non-incremental by +// default would be a huge change to our historical behavior where we would +// round-robin writes across streams. When streams are non-incremental, we +// would process streams of the same urgency one-by-one to completion instead. +// +// To avoid such a sudden change which might break some HTTP/2 users, this +// function allows the caller to specify whether they can actually use the +// default value as specified in RFC 9218. If not, this function will return a +// priority value where streams are incremental by default instead: effectively +// a round-robin between stream of the same urgency. +// +// As an example, a server might not be able to use the RFC 9218 default value +// when it's not sure that the client it is serving is aware of RFC 9218. +func http2defaultRFC9218Priority(canUseDefault bool) http2PriorityParam { + if canUseDefault { + return http2PriorityParam{ + urgency: 3, + incremental: 0, + } + } + return http2PriorityParam{ + urgency: 3, + incremental: 1, + } } // Note that HTTP/2 has had two different prioritization schemes, and @@ -2832,6 +2867,74 @@ func (f *http2Framer) WritePriority(streamID uint32, p http2PriorityParam) error return f.endWrite() } +// PriorityUpdateFrame is a PRIORITY_UPDATE frame as described in +// https://www.rfc-editor.org/rfc/rfc9218.html#name-the-priority_update-frame. +type http2PriorityUpdateFrame struct { + http2FrameHeader + Priority string + PrioritizedStreamID uint32 +} + +func http2parseRFC9218Priority(s string, canUseDefault bool) (p http2PriorityParam, ok bool) { + p = http2defaultRFC9218Priority(canUseDefault) + ok = httpsfv.ParseDictionary(s, func(key, val, _ string) { + switch key { + case "u": + if u, ok := httpsfv.ParseInteger(val); ok && u >= 0 && u <= 7 { + p.urgency = uint8(u) + } + case "i": + if i, ok := httpsfv.ParseBoolean(val); ok { + if i { + p.incremental = 1 + } else { + p.incremental = 0 + } + } + } + }) + if !ok { + return http2defaultRFC9218Priority(canUseDefault), ok + } + return p, true +} + +func http2parsePriorityUpdateFrame(_ *http2frameCache, fh http2FrameHeader, countError func(string), payload []byte) (http2Frame, error) { + if fh.StreamID != 0 { + countError("frame_priority_update_non_zero_stream") + return nil, http2connError{http2ErrCodeProtocol, "PRIORITY_UPDATE frame with non-zero stream ID"} + } + if len(payload) < 4 { + countError("frame_priority_update_bad_length") + return nil, http2connError{http2ErrCodeFrameSize, fmt.Sprintf("PRIORITY_UPDATE frame payload size was %d; want at least 4", len(payload))} + } + v := binary.BigEndian.Uint32(payload[:4]) + streamID := v & 0x7fffffff // mask off high bit + if streamID == 0 { + countError("frame_priority_update_prioritizing_zero_stream") + return nil, http2connError{http2ErrCodeProtocol, "PRIORITY_UPDATE frame with prioritized stream ID of zero"} + } + return &http2PriorityUpdateFrame{ + http2FrameHeader: fh, + PrioritizedStreamID: streamID, + Priority: string(payload[4:]), + }, nil +} + +// WritePriorityUpdate writes a PRIORITY_UPDATE frame. +// +// It will perform exactly one Write to the underlying Writer. +// It is the caller's responsibility to not call other Write methods concurrently. +func (f *http2Framer) WritePriorityUpdate(streamID uint32, priority string) error { + if !http2validStreamID(streamID) && !f.AllowIllegalWrites { + return http2errStreamID + } + f.startWrite(http2FramePriorityUpdate, 0, 0) + f.writeUint32(streamID) + f.writeBytes([]byte(priority)) + return f.endWrite() +} + // A RSTStreamFrame allows for abnormal termination of a stream. // See https://httpwg.org/specs/rfc7540.html#rfc.section.6.4 type http2RSTStreamFrame struct { @@ -3113,6 +3216,23 @@ func (mh *http2MetaHeadersFrame) PseudoFields() []hpack.HeaderField { return mh.Fields } +func (mh *http2MetaHeadersFrame) rfc9218Priority(priorityAware bool) (p http2PriorityParam, priorityAwareAfter, hasIntermediary bool) { + var s string + for _, field := range mh.Fields { + if field.Name == "priority" { + s = field.Value + priorityAware = true + } + if slices.Contains([]string{"via", "forwarded", "x-forwarded-for"}, field.Name) { + hasIntermediary = true + } + } + // No need to check for ok. parseRFC9218Priority will return a default + // value if there is no priority field or if the field cannot be parsed. + p, _ = http2parseRFC9218Priority(s, priorityAware && !hasIntermediary) + return p, priorityAware, hasIntermediary +} + func (mh *http2MetaHeadersFrame) checkPseudos() error { var isRequest, isResponse bool pf := mh.PseudoFields() @@ -3619,6 +3739,7 @@ const ( http2SettingMaxFrameSize http2SettingID = 0x5 http2SettingMaxHeaderListSize http2SettingID = 0x6 http2SettingEnableConnectProtocol http2SettingID = 0x8 + http2SettingNoRFC7540Priorities http2SettingID = 0x9 ) var http2settingName = map[http2SettingID]string{ @@ -3629,6 +3750,7 @@ var http2settingName = map[http2SettingID]string{ http2SettingMaxFrameSize: "MAX_FRAME_SIZE", http2SettingMaxHeaderListSize: "MAX_HEADER_LIST_SIZE", http2SettingEnableConnectProtocol: "ENABLE_CONNECT_PROTOCOL", + http2SettingNoRFC7540Priorities: "NO_RFC7540_PRIORITIES", } func (s http2SettingID) String() string { @@ -4454,10 +4576,13 @@ func (s *http2Server) serveConn(c net.Conn, opts *http2ServeConnOpts, newf func( sc.conn.SetWriteDeadline(time.Time{}) } - if s.NewWriteScheduler != nil { + switch { + case s.NewWriteScheduler != nil: sc.writeSched = s.NewWriteScheduler() - } else { + case http2clientPriorityDisabled(http1srv): sc.writeSched = http2newRoundRobinWriteScheduler() + default: + sc.writeSched = http2newPriorityWriteSchedulerRFC9218() } // These start at the RFC-specified defaults. If there is a higher @@ -4630,6 +4755,23 @@ type http2serverConn struct { // Used by startGracefulShutdown. shutdownOnce sync.Once + + // Used for RFC 9218 prioritization. + hasIntermediary bool // connection is done via an intermediary / proxy + priorityAware bool // the client has sent priority signal, meaning that it is aware of it. +} + +func (sc *http2serverConn) writeSchedIgnoresRFC7540() bool { + switch sc.writeSched.(type) { + case *http2priorityWriteSchedulerRFC9218: + return true + case *http2randomWriteScheduler: + return true + case *http2roundRobinWriteScheduler: + return true + default: + return false + } } func (sc *http2serverConn) maxHeaderListSize() uint32 { @@ -4923,6 +5065,9 @@ func (sc *http2serverConn) serve(conf http2http2Config) { if !http2disableExtendedConnectProtocol { settings = append(settings, http2Setting{http2SettingEnableConnectProtocol, 1}) } + if sc.writeSchedIgnoresRFC7540() { + settings = append(settings, http2Setting{http2SettingNoRFC7540Priorities, 1}) + } sc.writeFrame(http2FrameWriteRequest{ write: settings, }) @@ -5611,6 +5756,8 @@ func (sc *http2serverConn) processFrame(f http2Frame) error { // A client cannot push. Thus, servers MUST treat the receipt of a PUSH_PROMISE // frame as a connection error (Section 5.4.1) of type PROTOCOL_ERROR. return sc.countError("push_promise", http2ConnectionError(http2ErrCodeProtocol)) + case *http2PriorityUpdateFrame: + return sc.processPriorityUpdate(f) default: sc.vlogf("http2: server ignoring frame: %v", f.Header()) return nil @@ -5791,6 +5938,10 @@ func (sc *http2serverConn) processSetting(s http2Setting) error { case http2SettingEnableConnectProtocol: // Receipt of this parameter by a server does not // have any impact + case http2SettingNoRFC7540Priorities: + if s.Val > 1 { + return http2ConnectionError(http2ErrCodeProtocol) + } default: // Unknown setting: "An endpoint that receives a SETTINGS // frame with any unknown or unsupported identifier MUST @@ -6061,13 +6212,33 @@ func (sc *http2serverConn) processHeaders(f *http2MetaHeadersFrame) error { if f.StreamEnded() { initialState = http2stateHalfClosedRemote } - st := sc.newStream(id, 0, initialState) + + // We are handling two special cases here: + // 1. When a request is sent via an intermediary, we force priority to be + // u=3,i. This is essentially a round-robin behavior, and is done to ensure + // fairness between, for example, multiple clients using the same proxy. + // 2. Until a client has shown that it is aware of RFC 9218, we make its + // streams non-incremental by default. This is done to preserve the + // historical behavior of handling streams in a round-robin manner, rather + // than one-by-one to completion. + initialPriority := http2defaultRFC9218Priority(sc.priorityAware && !sc.hasIntermediary) + if _, ok := sc.writeSched.(*http2priorityWriteSchedulerRFC9218); ok && !sc.hasIntermediary { + headerPriority, priorityAware, hasIntermediary := f.rfc9218Priority(sc.priorityAware) + initialPriority = headerPriority + sc.hasIntermediary = hasIntermediary + if priorityAware { + sc.priorityAware = true + } + } + st := sc.newStream(id, 0, initialState, initialPriority) if f.HasPriority() { if err := sc.checkPriority(f.StreamID, f.Priority); err != nil { return err } - sc.writeSched.AdjustStream(st.id, f.Priority) + if !sc.writeSchedIgnoresRFC7540() { + sc.writeSched.AdjustStream(st.id, f.Priority) + } } rw, req, err := sc.newWriterAndRequest(st, f) @@ -6108,7 +6279,7 @@ func (sc *http2serverConn) upgradeRequest(req *Request) { sc.serveG.check() id := uint32(1) sc.maxClientStreamID = id - st := sc.newStream(id, 0, http2stateHalfClosedRemote) + st := sc.newStream(id, 0, http2stateHalfClosedRemote, http2defaultRFC9218Priority(sc.priorityAware && !sc.hasIntermediary)) st.reqTrailer = req.Trailer if st.reqTrailer != nil { st.trailer = make(Header) @@ -6173,11 +6344,32 @@ func (sc *http2serverConn) processPriority(f *http2PriorityFrame) error { if err := sc.checkPriority(f.StreamID, f.http2PriorityParam); err != nil { return err } + // We need to avoid calling AdjustStream when using the RFC 9218 write + // scheduler. Otherwise, incremental's zero value in PriorityParam will + // unexpectedly make all streams non-incremental. This causes us to process + // streams one-by-one to completion rather than doing it in a round-robin + // manner (the historical behavior), which might be unexpected to users. + if sc.writeSchedIgnoresRFC7540() { + return nil + } sc.writeSched.AdjustStream(f.StreamID, f.http2PriorityParam) return nil } -func (sc *http2serverConn) newStream(id, pusherID uint32, state http2streamState) *http2stream { +func (sc *http2serverConn) processPriorityUpdate(f *http2PriorityUpdateFrame) error { + sc.priorityAware = true + if _, ok := sc.writeSched.(*http2priorityWriteSchedulerRFC9218); !ok { + return nil + } + p, ok := http2parseRFC9218Priority(f.Priority, sc.priorityAware) + if !ok { + return sc.countError("unparsable_priority_update", http2streamError(f.PrioritizedStreamID, http2ErrCodeProtocol)) + } + sc.writeSched.AdjustStream(f.PrioritizedStreamID, p) + return nil +} + +func (sc *http2serverConn) newStream(id, pusherID uint32, state http2streamState, priority http2PriorityParam) *http2stream { sc.serveG.check() if id == 0 { panic("internal error: cannot create stream with id 0") @@ -6200,7 +6392,7 @@ func (sc *http2serverConn) newStream(id, pusherID uint32, state http2streamState } sc.streams[id] = st - sc.writeSched.OpenStream(st.id, http2OpenStreamOptions{PusherID: pusherID}) + sc.writeSched.OpenStream(st.id, http2OpenStreamOptions{PusherID: pusherID, priority: priority}) if st.isPushed() { sc.curPushedStreams++ } else { @@ -7206,7 +7398,7 @@ func (sc *http2serverConn) startPush(msg *http2startPushRequest) { // transition to "half closed (remote)" after sending the initial HEADERS, but // we start in "half closed (remote)" for simplicity. // See further comments at the definition of stateHalfClosedRemote. - promised := sc.newStream(promisedID, msg.parent.id, http2stateHalfClosedRemote) + promised := sc.newStream(promisedID, msg.parent.id, http2stateHalfClosedRemote, http2defaultRFC9218Priority(sc.priorityAware && !sc.hasIntermediary)) rw, req, err := sc.newWriterAndRequestNoBody(promised, httpcommon.ServerRequestParam{ Method: msg.method, Scheme: msg.url.Scheme, @@ -11452,6 +11644,10 @@ type http2PriorityWriteSchedulerConfig struct { // frames by following HTTP/2 priorities as described in RFC 7540 Section 5.3. // If cfg is nil, default options are used. func http2NewPriorityWriteScheduler(cfg *http2PriorityWriteSchedulerConfig) http2WriteScheduler { + return http2newPriorityWriteSchedulerRFC7540(cfg) +} + +func http2newPriorityWriteSchedulerRFC7540(cfg *http2PriorityWriteSchedulerConfig) http2WriteScheduler { if cfg == nil { // For justification of these defaults, see: // https://docs.google.com/document/d/1oLhNg1skaWD4_DtaoCxdSRN5erEXrH-KnLrMwEpOtFY @@ -11875,6 +12071,15 @@ type http2priorityWriteSchedulerRFC9218 struct { // incremental streams or not, when urgency is the same in a given Pop() // call. prioritizeIncremental bool + + // priorityUpdateBuf is used to buffer the most recent PRIORITY_UPDATE we + // receive per https://www.rfc-editor.org/rfc/rfc9218.html#name-the-priority_update-frame. + priorityUpdateBuf struct { + // streamID being 0 means that the buffer is empty. This is a safe + // assumption as PRIORITY_UPDATE for stream 0 is a PROTOCOL_ERROR. + streamID uint32 + priority http2PriorityParam + } } func http2newPriorityWriteSchedulerRFC9218() http2WriteScheduler { @@ -11888,6 +12093,10 @@ func (ws *http2priorityWriteSchedulerRFC9218) OpenStream(streamID uint32, opt ht if ws.streams[streamID].location != nil { panic(fmt.Errorf("stream %d already opened", streamID)) } + if streamID == ws.priorityUpdateBuf.streamID { + ws.priorityUpdateBuf.streamID = 0 + opt.priority = ws.priorityUpdateBuf.priority + } q := ws.queuePool.get() ws.streams[streamID] = http2streamMetadata{ location: q, @@ -11933,6 +12142,8 @@ func (ws *http2priorityWriteSchedulerRFC9218) AdjustStream(streamID uint32, prio metadata := ws.streams[streamID] q, u, i := metadata.location, metadata.priority.urgency, metadata.priority.incremental if q == nil { + ws.priorityUpdateBuf.streamID = streamID + ws.priorityUpdateBuf.priority = priority return } diff --git a/src/net/http/http.go b/src/net/http/http.go index d346e60646..d3e9a2787a 100644 --- a/src/net/http/http.go +++ b/src/net/http/http.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:generate bundle -o=h2_bundle.go -prefix=http2 -tags=!nethttpomithttp2 -import=golang.org/x/net/internal/httpcommon=net/http/internal/httpcommon golang.org/x/net/http2 +//go:generate bundle -o=h2_bundle.go -prefix=http2 -tags=!nethttpomithttp2 -import=golang.org/x/net/internal/httpcommon=net/http/internal/httpcommon -import=golang.org/x/net/internal/httpsfv=net/http/internal/httpsfv golang.org/x/net/http2 package http diff --git a/src/net/http/internal/httpsfv/httpsfv.go b/src/net/http/internal/httpsfv/httpsfv.go new file mode 100644 index 0000000000..07f07ebf1c --- /dev/null +++ b/src/net/http/internal/httpsfv/httpsfv.go @@ -0,0 +1,666 @@ +// Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT. +//go:generate bundle -o httpsfv.go -prefix= golang.org/x/net/internal/httpsfv + +// Package httpsfv provides functionality for dealing with HTTP Structured +// Field Values. +// + +package httpsfv + +import ( + "slices" + "strconv" + "strings" + "time" + "unicode/utf8" +) + +func isLCAlpha(b byte) bool { + return (b >= 'a' && b <= 'z') +} + +func isAlpha(b byte) bool { + return isLCAlpha(b) || (b >= 'A' && b <= 'Z') +} + +func isDigit(b byte) bool { + return b >= '0' && b <= '9' +} + +func isVChar(b byte) bool { + return b >= 0x21 && b <= 0x7e +} + +func isSP(b byte) bool { + return b == 0x20 +} + +func isTChar(b byte) bool { + if isAlpha(b) || isDigit(b) { + return true + } + return slices.Contains([]byte{'!', '#', '$', '%', '&', '\'', '*', '+', '-', '.', '^', '_', '`', '|', '~'}, b) +} + +func countLeftWhitespace(s string) int { + i := 0 + for _, ch := range []byte(s) { + if ch != ' ' && ch != '\t' { + break + } + i++ + } + return i +} + +// https://www.rfc-editor.org/rfc/rfc4648#section-8. +func decOctetHex(ch1, ch2 byte) (ch byte, ok bool) { + decBase16 := func(in byte) (out byte, ok bool) { + if !isDigit(in) && !(in >= 'a' && in <= 'f') { + return 0, false + } + if isDigit(in) { + return in - '0', true + } + return in - 'a' + 10, true + } + + if ch1, ok = decBase16(ch1); !ok { + return 0, ok + } + if ch2, ok = decBase16(ch2); !ok { + return 0, ok + } + return ch1<<4 | ch2, true +} + +// ParseList parses a list from a given HTTP Structured Field Values. +// +// Given an HTTP SFV string that represents a list, it will call the given +// function using each of the members and parameters contained in the list. +// This allows the caller to extract information out of the list. +// +// This function will return once it encounters the end of the string, or +// something that is not a list. If it cannot consume the entire given +// string, the ok value returned will be false. +// +// https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-a-list. +func ParseList(s string, f func(member, param string)) (ok bool) { + for len(s) != 0 { + var member, param string + if len(s) != 0 && s[0] == '(' { + if member, s, ok = consumeBareInnerList(s, nil); !ok { + return ok + } + } else { + if member, s, ok = consumeBareItem(s); !ok { + return ok + } + } + if param, s, ok = consumeParameter(s, nil); !ok { + return ok + } + if f != nil { + f(member, param) + } + + s = s[countLeftWhitespace(s):] + if len(s) == 0 { + break + } + if s[0] != ',' { + return false + } + s = s[1:] + s = s[countLeftWhitespace(s):] + if len(s) == 0 { + return false + } + } + return true +} + +// consumeBareInnerList consumes an inner list +// (https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-an-inner-list), +// except for the inner list's top-most parameter. +// For example, given `(a;b c;d);e`, it will consume only `(a;b c;d)`. +func consumeBareInnerList(s string, f func(bareItem, param string)) (consumed, rest string, ok bool) { + if len(s) == 0 || s[0] != '(' { + return "", s, false + } + rest = s[1:] + for len(rest) != 0 { + var bareItem, param string + rest = rest[countLeftWhitespace(rest):] + if len(rest) != 0 && rest[0] == ')' { + rest = rest[1:] + break + } + if bareItem, rest, ok = consumeBareItem(rest); !ok { + return "", s, ok + } + if param, rest, ok = consumeParameter(rest, nil); !ok { + return "", s, ok + } + if len(rest) == 0 || (rest[0] != ')' && !isSP(rest[0])) { + return "", s, false + } + if f != nil { + f(bareItem, param) + } + } + return s[:len(s)-len(rest)], rest, true +} + +// ParseBareInnerList parses a bare inner list from a given HTTP Structured +// Field Values. +// +// We define a bare inner list as an inner list +// (https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-an-inner-list), +// without the top-most parameter of the inner list. For example, given the +// inner list `(a;b c;d);e`, the bare inner list would be `(a;b c;d)`. +// +// Given an HTTP SFV string that represents a bare inner list, it will call the +// given function using each of the bare item and parameter within the bare +// inner list. This allows the caller to extract information out of the bare +// inner list. +// +// This function will return once it encounters the end of the bare inner list, +// or something that is not a bare inner list. If it cannot consume the entire +// given string, the ok value returned will be false. +func ParseBareInnerList(s string, f func(bareItem, param string)) (ok bool) { + _, rest, ok := consumeBareInnerList(s, f) + return rest == "" && ok +} + +// https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-an-item. +func consumeItem(s string, f func(bareItem, param string)) (consumed, rest string, ok bool) { + var bareItem, param string + if bareItem, rest, ok = consumeBareItem(s); !ok { + return "", s, ok + } + if param, rest, ok = consumeParameter(rest, nil); !ok { + return "", s, ok + } + if f != nil { + f(bareItem, param) + } + return s[:len(s)-len(rest)], rest, true +} + +// ParseItem parses an item from a given HTTP Structured Field Values. +// +// Given an HTTP SFV string that represents an item, it will call the given +// function once, with the bare item and the parameter of the item. This allows +// the caller to extract information out of the item. +// +// This function will return once it encounters the end of the string, or +// something that is not an item. If it cannot consume the entire given +// string, the ok value returned will be false. +// +// https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-an-item. +func ParseItem(s string, f func(bareItem, param string)) (ok bool) { + _, rest, ok := consumeItem(s, f) + return rest == "" && ok +} + +// ParseDictionary parses a dictionary from a given HTTP Structured Field +// Values. +// +// Given an HTTP SFV string that represents a dictionary, it will call the +// given function using each of the keys, values, and parameters contained in +// the dictionary. This allows the caller to extract information out of the +// dictionary. +// +// This function will return once it encounters the end of the string, or +// something that is not a dictionary. If it cannot consume the entire given +// string, the ok value returned will be false. +// +// https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-a-dictionary. +func ParseDictionary(s string, f func(key, val, param string)) (ok bool) { + for len(s) != 0 { + var key, val, param string + val = "?1" // Default value for empty val is boolean true. + if key, s, ok = consumeKey(s); !ok { + return ok + } + if len(s) != 0 && s[0] == '=' { + s = s[1:] + if len(s) != 0 && s[0] == '(' { + if val, s, ok = consumeBareInnerList(s, nil); !ok { + return ok + } + } else { + if val, s, ok = consumeBareItem(s); !ok { + return ok + } + } + } + if param, s, ok = consumeParameter(s, nil); !ok { + return ok + } + if f != nil { + f(key, val, param) + } + s = s[countLeftWhitespace(s):] + if len(s) == 0 { + break + } + if s[0] == ',' { + s = s[1:] + } + s = s[countLeftWhitespace(s):] + if len(s) == 0 { + return false + } + } + return true +} + +// https://www.rfc-editor.org/rfc/rfc9651.html#parse-param. +func consumeParameter(s string, f func(key, val string)) (consumed, rest string, ok bool) { + rest = s + for len(rest) != 0 { + var key, val string + val = "?1" // Default value for empty val is boolean true. + if rest[0] != ';' { + break + } + rest = rest[1:] + rest = rest[countLeftWhitespace(rest):] + key, rest, ok = consumeKey(rest) + if !ok { + return "", s, ok + } + if len(rest) != 0 && rest[0] == '=' { + rest = rest[1:] + val, rest, ok = consumeBareItem(rest) + if !ok { + return "", s, ok + } + } + if f != nil { + f(key, val) + } + } + return s[:len(s)-len(rest)], rest, true +} + +// ParseParameter parses a parameter from a given HTTP Structured Field Values. +// +// Given an HTTP SFV string that represents a parameter, it will call the given +// function using each of the keys and values contained in the parameter. This +// allows the caller to extract information out of the parameter. +// +// This function will return once it encounters the end of the string, or +// something that is not a parameter. If it cannot consume the entire given +// string, the ok value returned will be false. +// +// https://www.rfc-editor.org/rfc/rfc9651.html#parse-param. +func ParseParameter(s string, f func(key, val string)) (ok bool) { + _, rest, ok := consumeParameter(s, f) + return rest == "" && ok +} + +// https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-a-key. +func consumeKey(s string) (consumed, rest string, ok bool) { + if len(s) == 0 || (!isLCAlpha(s[0]) && s[0] != '*') { + return "", s, false + } + i := 0 + for _, ch := range []byte(s) { + if !isLCAlpha(ch) && !isDigit(ch) && !slices.Contains([]byte("_-.*"), ch) { + break + } + i++ + } + return s[:i], s[i:], true +} + +// https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-an-integer-or-decim. +func consumeIntegerOrDecimal(s string) (consumed, rest string, ok bool) { + var i, signOffset, periodIndex int + var isDecimal bool + if i < len(s) && s[i] == '-' { + i++ + signOffset++ + } + if i >= len(s) { + return "", s, false + } + if !isDigit(s[i]) { + return "", s, false + } + for i < len(s) { + ch := s[i] + if isDigit(ch) { + i++ + continue + } + if !isDecimal && ch == '.' { + if i-signOffset > 12 { + return "", s, false + } + periodIndex = i + isDecimal = true + i++ + continue + } + break + } + if !isDecimal && i-signOffset > 15 { + return "", s, false + } + if isDecimal { + if i-signOffset > 16 { + return "", s, false + } + if s[i-1] == '.' { + return "", s, false + } + if i-periodIndex-1 > 3 { + return "", s, false + } + } + return s[:i], s[i:], true +} + +// ParseInteger parses an integer from a given HTTP Structured Field Values. +// +// The entire HTTP SFV string must consist of a valid integer. It returns the +// parsed integer and an ok boolean value, indicating success or not. +// +// https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-an-integer-or-decim. +func ParseInteger(s string) (parsed int64, ok bool) { + if _, rest, ok := consumeIntegerOrDecimal(s); !ok || rest != "" { + return 0, false + } + if n, err := strconv.ParseInt(s, 10, 64); err == nil { + return n, true + } + return 0, false +} + +// ParseDecimal parses a decimal from a given HTTP Structured Field Values. +// +// The entire HTTP SFV string must consist of a valid decimal. It returns the +// parsed decimal and an ok boolean value, indicating success or not. +// +// https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-an-integer-or-decim. +func ParseDecimal(s string) (parsed float64, ok bool) { + if _, rest, ok := consumeIntegerOrDecimal(s); !ok || rest != "" { + return 0, false + } + if !strings.Contains(s, ".") { + return 0, false + } + if n, err := strconv.ParseFloat(s, 64); err == nil { + return n, true + } + return 0, false +} + +// https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-a-string. +func consumeString(s string) (consumed, rest string, ok bool) { + if len(s) == 0 || s[0] != '"' { + return "", s, false + } + for i := 1; i < len(s); i++ { + switch ch := s[i]; ch { + case '\\': + if i+1 >= len(s) { + return "", s, false + } + i++ + if ch = s[i]; ch != '"' && ch != '\\' { + return "", s, false + } + case '"': + return s[:i+1], s[i+1:], true + default: + if !isVChar(ch) && !isSP(ch) { + return "", s, false + } + } + } + return "", s, false +} + +// ParseString parses a Go string from a given HTTP Structured Field Values. +// +// The entire HTTP SFV string must consist of a valid string. It returns the +// parsed string and an ok boolean value, indicating success or not. +// +// https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-a-string. +func ParseString(s string) (parsed string, ok bool) { + if _, rest, ok := consumeString(s); !ok || rest != "" { + return "", false + } + return s[1 : len(s)-1], true +} + +// https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-a-token +func consumeToken(s string) (consumed, rest string, ok bool) { + if len(s) == 0 || (!isAlpha(s[0]) && s[0] != '*') { + return "", s, false + } + i := 0 + for _, ch := range []byte(s) { + if !isTChar(ch) && !slices.Contains([]byte(":/"), ch) { + break + } + i++ + } + return s[:i], s[i:], true +} + +// ParseToken parses a token from a given HTTP Structured Field Values. +// +// The entire HTTP SFV string must consist of a valid token. It returns the +// parsed token and an ok boolean value, indicating success or not. +// +// https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-a-token +func ParseToken(s string) (parsed string, ok bool) { + if _, rest, ok := consumeToken(s); !ok || rest != "" { + return "", false + } + return s, true +} + +// https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-a-byte-sequence. +func consumeByteSequence(s string) (consumed, rest string, ok bool) { + if len(s) == 0 || s[0] != ':' { + return "", s, false + } + for i := 1; i < len(s); i++ { + if ch := s[i]; ch == ':' { + return s[:i+1], s[i+1:], true + } + if ch := s[i]; !isAlpha(ch) && !isDigit(ch) && !slices.Contains([]byte("+/="), ch) { + return "", s, false + } + } + return "", s, false +} + +// ParseByteSequence parses a byte sequence from a given HTTP Structured Field +// Values. +// +// The entire HTTP SFV string must consist of a valid byte sequence. It returns +// the parsed byte sequence and an ok boolean value, indicating success or not. +// +// https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-a-byte-sequence. +func ParseByteSequence(s string) (parsed []byte, ok bool) { + if _, rest, ok := consumeByteSequence(s); !ok || rest != "" { + return nil, false + } + return []byte(s[1 : len(s)-1]), true +} + +// https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-a-boolean. +func consumeBoolean(s string) (consumed, rest string, ok bool) { + if len(s) >= 2 && (s[:2] == "?0" || s[:2] == "?1") { + return s[:2], s[2:], true + } + return "", s, false +} + +// ParseBoolean parses a boolean from a given HTTP Structured Field Values. +// +// The entire HTTP SFV string must consist of a valid boolean. It returns the +// parsed boolean and an ok boolean value, indicating success or not. +// +// https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-a-boolean. +func ParseBoolean(s string) (parsed bool, ok bool) { + if _, rest, ok := consumeBoolean(s); !ok || rest != "" { + return false, false + } + return s == "?1", true +} + +// https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-a-date. +func consumeDate(s string) (consumed, rest string, ok bool) { + if len(s) == 0 || s[0] != '@' { + return "", s, false + } + if _, rest, ok = consumeIntegerOrDecimal(s[1:]); !ok { + return "", s, ok + } + consumed = s[:len(s)-len(rest)] + if slices.Contains([]byte(consumed), '.') { + return "", s, false + } + return consumed, rest, ok +} + +// ParseDate parses a date from a given HTTP Structured Field Values. +// +// The entire HTTP SFV string must consist of a valid date. It returns the +// parsed date and an ok boolean value, indicating success or not. +// +// https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-a-date. +func ParseDate(s string) (parsed time.Time, ok bool) { + if _, rest, ok := consumeDate(s); !ok || rest != "" { + return time.Time{}, false + } + if n, ok := ParseInteger(s[1:]); !ok { + return time.Time{}, false + } else { + return time.Unix(n, 0), true + } +} + +// https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-a-display-string. +func consumeDisplayString(s string) (consumed, rest string, ok bool) { + // To prevent excessive allocation, especially when input is large, we + // maintain a buffer of 4 bytes to keep track of the last rune we + // encounter. This way, we can validate that the display string conforms to + // UTF-8 without actually building the whole string. + var lastRune [4]byte + var runeLen int + isPartOfValidRune := func(ch byte) bool { + lastRune[runeLen] = ch + runeLen++ + if utf8.FullRune(lastRune[:runeLen]) { + r, s := utf8.DecodeRune(lastRune[:runeLen]) + if r == utf8.RuneError { + return false + } + copy(lastRune[:], lastRune[s:runeLen]) + runeLen -= s + return true + } + return runeLen <= 4 + } + + if len(s) <= 1 || s[:2] != `%"` { + return "", s, false + } + i := 2 + for i < len(s) { + ch := s[i] + if !isVChar(ch) && !isSP(ch) { + return "", s, false + } + switch ch { + case '"': + if runeLen > 0 { + return "", s, false + } + return s[:i+1], s[i+1:], true + case '%': + if i+2 >= len(s) { + return "", s, false + } + if ch, ok = decOctetHex(s[i+1], s[i+2]); !ok { + return "", s, ok + } + if ok = isPartOfValidRune(ch); !ok { + return "", s, ok + } + i += 3 + default: + if ok = isPartOfValidRune(ch); !ok { + return "", s, ok + } + i++ + } + } + return "", s, false +} + +// ParseDisplayString parses a display string from a given HTTP Structured +// Field Values. +// +// The entire HTTP SFV string must consist of a valid display string. It +// returns the parsed display string and an ok boolean value, indicating +// success or not. +// +// https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-a-display-string. +func ParseDisplayString(s string) (parsed string, ok bool) { + if _, rest, ok := consumeDisplayString(s); !ok || rest != "" { + return "", false + } + // consumeDisplayString() already validates that we have a valid display + // string. Therefore, we can just construct the display string, without + // validating it again. + s = s[2 : len(s)-1] + var b strings.Builder + for i := 0; i < len(s); { + if s[i] == '%' { + decoded, _ := decOctetHex(s[i+1], s[i+2]) + b.WriteByte(decoded) + i += 3 + continue + } + b.WriteByte(s[i]) + i++ + } + return b.String(), true +} + +// https://www.rfc-editor.org/rfc/rfc9651.html#parse-bare-item. +func consumeBareItem(s string) (consumed, rest string, ok bool) { + if len(s) == 0 { + return "", s, false + } + ch := s[0] + switch { + case ch == '-' || isDigit(ch): + return consumeIntegerOrDecimal(s) + case ch == '"': + return consumeString(s) + case ch == '*' || isAlpha(ch): + return consumeToken(s) + case ch == ':': + return consumeByteSequence(s) + case ch == '?': + return consumeBoolean(s) + case ch == '@': + return consumeDate(s) + case ch == '%': + return consumeDisplayString(s) + default: + return "", s, false + } +} |
