blob: 3431291dc6d4ce4faf7b98023b1f04e17358a9af [file]
/*
* 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,
}
}