| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package triple_protocol |
| |
| import ( |
| "net/http" |
| "strconv" |
| "strings" |
| "sync" |
| "time" |
| ) |
| |
| import ( |
| "github.com/dubbogo/gost/log/logger" |
| |
| "github.com/quic-go/quic-go/http3" |
| ) |
| |
| import ( |
| "dubbo.apache.org/dubbo-go/v3/common/constant" |
| ) |
| |
| // AltSvcInfo represents cached alternative service information |
| // This struct stores the parsed information from Alt-Svc HTTP headers |
| // according to RFC 7838 (HTTP Alternative Services) |
| type AltSvcInfo struct { |
| // protocol specifies the alternative protocol to use |
| // Examples: "h3" for HTTP/3, "h2" for HTTP/2, "h3-29" for HTTP/3 draft 29 |
| Protocol string |
| |
| // host specifies the alternative hostname (optional) |
| // If empty, use the same host as the original request |
| // Example: "example.com" or "" (same host) |
| Host string |
| |
| // port specifies the alternative port number (optional) |
| // If empty, use the same port as the original request |
| // Example: "443" or "" (same port) |
| Port string |
| |
| // expires specifies when this alternative service information becomes invalid |
| // After this time, the cached information should be discarded and |
| // a new Alt-Svc header should be requested from the server |
| // |
| // This field is parsed from the "ma" (max-age) parameter in Alt-Svc headers: |
| // - Alt-Svc: h3=":443"; ma=86400 -> expires = now + 86400 seconds |
| // - Alt-Svc: h3=":443"; ma=3600 -> expires = now + 3600 seconds |
| // |
| // If no "ma" parameter is provided, a default expiration time is used |
| // to prevent stale cache entries from being used indefinitely |
| Expires time.Time |
| } |
| |
| // AltSvcCache manages cached alternative service information |
| type AltSvcCache struct { |
| mu sync.RWMutex |
| cache map[string]*AltSvcInfo |
| } |
| |
| // NewAltSvcCache creates a new Alt-Svc cache |
| func NewAltSvcCache() *AltSvcCache { |
| return &AltSvcCache{ |
| cache: make(map[string]*AltSvcInfo), |
| } |
| } |
| |
| // Get retrieves cached alternative service information for a host |
| func (c *AltSvcCache) Get(host string) *AltSvcInfo { |
| c.mu.RLock() |
| defer c.mu.RUnlock() |
| |
| altSvc, exists := c.cache[host] |
| // Return nil if the cached information doesn't exist or has expired |
| if !exists || altSvc.Expires.Before(time.Now()) { |
| return nil |
| } |
| |
| return altSvc |
| } |
| |
| // Set stores alternative service information for a host |
| func (c *AltSvcCache) Set(host string, altSvc *AltSvcInfo) { |
| c.mu.Lock() |
| defer c.mu.Unlock() |
| c.cache[host] = altSvc |
| } |
| |
| // UpdateFromHeaders updates the cache based on Alt-Svc headers from an HTTP response |
| func (c *AltSvcCache) UpdateFromHeaders(host string, headers http.Header) { |
| altSvcHeader := headers.Get("Alt-Svc") |
| if altSvcHeader == "" { |
| return |
| } |
| |
| // Parse Alt-Svc header according to RFC 7838 |
| // Example: "h3=\":443\"; ma=86400, h3-29=\":443\"; ma=86400" |
| altSvcs := ParseAltSvcHeader(altSvcHeader) |
| if len(altSvcs) == 0 { |
| return |
| } |
| |
| // Prefer HTTP/3 over HTTP/2 |
| var preferredAltSvc *AltSvcInfo |
| for _, altSvc := range altSvcs { |
| if altSvc.Protocol == constant.AltSvcProtocolH3 { |
| preferredAltSvc = altSvc |
| break |
| } else if altSvc.Protocol == constant.AltSvcProtocolH2 && preferredAltSvc == nil { |
| preferredAltSvc = altSvc |
| } |
| } |
| |
| if preferredAltSvc != nil { |
| c.Set(host, preferredAltSvc) |
| logger.Debugf("[Triple][Negotiation] updated alt-svc cache for %s, protocol=%s", host, preferredAltSvc.Protocol) |
| } |
| } |
| |
| // ParseAltSvcHeader parses the Alt-Svc header according to RFC 7838 |
| func ParseAltSvcHeader(altSvcHeader string) []*AltSvcInfo { |
| var altSvcs []*AltSvcInfo |
| |
| // Split by comma to get individual alternative services |
| parts := strings.Split(altSvcHeader, ",") |
| for _, part := range parts { |
| part = strings.TrimSpace(part) |
| if part == "" { |
| continue |
| } |
| |
| // Parse protocol and authority |
| // Format: protocol="authority"; ma=max_age |
| // Example: h3=":443"; ma=86400 |
| altSvc := parseAltSvcPart(part) |
| if altSvc != nil { |
| altSvcs = append(altSvcs, altSvc) |
| } |
| } |
| |
| return altSvcs |
| } |
| |
| // parseAltSvcPart parses a single alternative service part |
| func parseAltSvcPart(part string) *AltSvcInfo { |
| // Find the protocol (before the first '=') |
| eqIndex := strings.Index(part, "=") |
| if eqIndex == -1 { |
| return nil |
| } |
| |
| protocol := strings.TrimSpace(part[:eqIndex]) |
| if protocol != constant.AltSvcProtocolH3 && protocol != constant.AltSvcProtocolH2 { |
| return nil |
| } |
| |
| // Find the authority (between quotes) |
| quoteStart := strings.Index(part, "\"") |
| if quoteStart == -1 { |
| return nil |
| } |
| |
| quoteEnd := strings.LastIndex(part, "\"") |
| if quoteEnd == -1 || quoteEnd <= quoteStart { |
| return nil |
| } |
| |
| authority := part[quoteStart+1 : quoteEnd] |
| |
| // Parse authority to get host and port |
| host, port := parseAuthority(authority) |
| |
| // Parse max_age if present |
| // The "ma" parameter specifies how long this alternative service is valid |
| // Example: ma=86400 means valid for 86400 seconds (24 hours) |
| maxAge := 24 * time.Hour // Default to 24 hours if no ma parameter is provided |
| if maIndex := strings.Index(part, "ma="); maIndex != -1 { |
| maPart := part[maIndex+3:] |
| if semicolonIndex := strings.Index(maPart, ";"); semicolonIndex != -1 { |
| maPart = maPart[:semicolonIndex] |
| } |
| if age, err := strconv.Atoi(strings.TrimSpace(maPart)); err == nil && age > 0 { |
| maxAge = time.Duration(age) * time.Second |
| } |
| } |
| |
| return &AltSvcInfo{ |
| Protocol: protocol, |
| Host: host, |
| Port: port, |
| Expires: time.Now().Add(maxAge), // Calculate expiration time based on max-age |
| } |
| } |
| |
| // parseAuthority parses the authority part of an alternative service |
| func parseAuthority(authority string) (host, port string) { |
| // Authority format can be: |
| // - ":port" (host is the same as original) |
| // - "host:port" |
| // - "host" (port is the same as original) |
| |
| if strings.HasPrefix(authority, ":") { |
| // Just port specified |
| port = authority[1:] |
| return "", port |
| } |
| |
| if colonIndex := strings.Index(authority, ":"); colonIndex != -1 { |
| // Both host and port specified |
| host = authority[:colonIndex] |
| port = authority[colonIndex+1:] |
| return host, port |
| } |
| |
| // Just host specified |
| host = authority |
| return host, "" |
| } |
| |
| // AltSvcHandler wraps an http.Handler to automatically add Alt-Svc headers |
| // for non-HTTP/3 requests, enabling HTTP Alternative Services discovery |
| type AltSvcHandler struct { |
| handler http.Handler |
| http3Server *http3.Server |
| negotiation bool // Whether to enable HTTP/3 negotiation via Alt-Svc headers |
| } |
| |
| // ServeHTTP implements http.Handler interface |
| func (h *AltSvcHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| // Add Alt-Svc header for non-HTTP/3 requests to advertise HTTP/3 availability |
| // Only add Alt-Svc headers if negotiation is enabled |
| if h.negotiation && r.ProtoMajor < 3 { |
| if err := h.http3Server.SetQUICHeaders(w.Header()); err != nil { |
| logger.Warnf("[Triple][Negotiation] failed to set QUIC headers for %s, err=%v", r.URL.String(), err) |
| } |
| } |
| |
| // Call the wrapped handler |
| h.handler.ServeHTTP(w, r) |
| } |
| |
| // NewAltSvcHandler creates a new AltSvcHandler that wraps the given handler |
| func NewAltSvcHandler(handler http.Handler, http3Server *http3.Server, negotiation bool) *AltSvcHandler { |
| return &AltSvcHandler{ |
| handler: handler, |
| http3Server: http3Server, |
| negotiation: negotiation, |
| } |
| } |