Port envd from e2b with internalized shared packages and Connect RPC
- Copy envd source from e2b-dev/infra, internalize shared dependencies
into envd/internal/shared/ (keys, filesystem, id, smap, utils)
- Switch from gRPC to Connect RPC for all envd services
- Update module paths to git.omukk.dev/wrenn/{sandbox,sandbox/envd}
- Add proto specs (process, filesystem) with buf-based code generation
- Implement full envd: process exec, filesystem ops, port forwarding,
cgroup management, MMDS integration, and HTTP API
- Update main module dependencies (firecracker SDK, pgx, goose, etc.)
- Remove placeholder .gitkeep files replaced by real implementations
This commit is contained in:
227
envd/internal/api/encoding.go
Normal file
227
envd/internal/api/encoding.go
Normal file
@ -0,0 +1,227 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// EncodingGzip is the gzip content encoding.
|
||||
EncodingGzip = "gzip"
|
||||
// EncodingIdentity means no encoding (passthrough).
|
||||
EncodingIdentity = "identity"
|
||||
// EncodingWildcard means any encoding is acceptable.
|
||||
EncodingWildcard = "*"
|
||||
)
|
||||
|
||||
// SupportedEncodings lists the content encodings supported for file transfer.
|
||||
// The order matters - encodings are checked in order of preference.
|
||||
var SupportedEncodings = []string{
|
||||
EncodingGzip,
|
||||
}
|
||||
|
||||
// encodingWithQuality holds an encoding name and its quality value.
|
||||
type encodingWithQuality struct {
|
||||
encoding string
|
||||
quality float64
|
||||
}
|
||||
|
||||
// isSupportedEncoding checks if the given encoding is in the supported list.
|
||||
// Per RFC 7231, content-coding values are case-insensitive.
|
||||
func isSupportedEncoding(encoding string) bool {
|
||||
return slices.Contains(SupportedEncodings, strings.ToLower(encoding))
|
||||
}
|
||||
|
||||
// parseEncodingWithQuality parses an encoding value and extracts the quality.
|
||||
// Returns the encoding name (lowercased) and quality value (default 1.0 if not specified).
|
||||
// Per RFC 7231, content-coding values are case-insensitive.
|
||||
func parseEncodingWithQuality(value string) encodingWithQuality {
|
||||
value = strings.TrimSpace(value)
|
||||
quality := 1.0
|
||||
|
||||
if idx := strings.Index(value, ";"); idx != -1 {
|
||||
params := value[idx+1:]
|
||||
value = strings.TrimSpace(value[:idx])
|
||||
|
||||
// Parse q=X.X parameter
|
||||
for param := range strings.SplitSeq(params, ";") {
|
||||
param = strings.TrimSpace(param)
|
||||
if strings.HasPrefix(strings.ToLower(param), "q=") {
|
||||
if q, err := strconv.ParseFloat(param[2:], 64); err == nil {
|
||||
quality = q
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize encoding to lowercase per RFC 7231
|
||||
return encodingWithQuality{encoding: strings.ToLower(value), quality: quality}
|
||||
}
|
||||
|
||||
// parseEncoding extracts the encoding name from a header value, stripping quality.
|
||||
func parseEncoding(value string) string {
|
||||
return parseEncodingWithQuality(value).encoding
|
||||
}
|
||||
|
||||
// parseContentEncoding parses the Content-Encoding header and returns the encoding.
|
||||
// Returns an error if an unsupported encoding is specified.
|
||||
// If no Content-Encoding header is present, returns empty string.
|
||||
func parseContentEncoding(r *http.Request) (string, error) {
|
||||
header := r.Header.Get("Content-Encoding")
|
||||
if header == "" {
|
||||
return EncodingIdentity, nil
|
||||
}
|
||||
|
||||
encoding := parseEncoding(header)
|
||||
|
||||
if encoding == EncodingIdentity {
|
||||
return EncodingIdentity, nil
|
||||
}
|
||||
|
||||
if !isSupportedEncoding(encoding) {
|
||||
return "", fmt.Errorf("unsupported Content-Encoding: %s, supported: %v", header, SupportedEncodings)
|
||||
}
|
||||
|
||||
return encoding, nil
|
||||
}
|
||||
|
||||
// parseAcceptEncodingHeader parses the Accept-Encoding header and returns
|
||||
// the parsed encodings along with the identity rejection state.
|
||||
// Per RFC 7231 Section 5.3.4, identity is acceptable unless excluded by
|
||||
// "identity;q=0" or "*;q=0" without a more specific entry for identity with q>0.
|
||||
func parseAcceptEncodingHeader(header string) ([]encodingWithQuality, bool) {
|
||||
if header == "" {
|
||||
return nil, false // identity not rejected when header is empty
|
||||
}
|
||||
|
||||
// Parse all encodings with their quality values
|
||||
var encodings []encodingWithQuality
|
||||
for value := range strings.SplitSeq(header, ",") {
|
||||
eq := parseEncodingWithQuality(value)
|
||||
encodings = append(encodings, eq)
|
||||
}
|
||||
|
||||
// Check if identity is rejected per RFC 7231 Section 5.3.4:
|
||||
// identity is acceptable unless excluded by "identity;q=0" or "*;q=0"
|
||||
// without a more specific entry for identity with q>0.
|
||||
identityRejected := false
|
||||
identityExplicitlyAccepted := false
|
||||
wildcardRejected := false
|
||||
|
||||
for _, eq := range encodings {
|
||||
switch eq.encoding {
|
||||
case EncodingIdentity:
|
||||
if eq.quality == 0 {
|
||||
identityRejected = true
|
||||
} else {
|
||||
identityExplicitlyAccepted = true
|
||||
}
|
||||
case EncodingWildcard:
|
||||
if eq.quality == 0 {
|
||||
wildcardRejected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if wildcardRejected && !identityExplicitlyAccepted {
|
||||
identityRejected = true
|
||||
}
|
||||
|
||||
return encodings, identityRejected
|
||||
}
|
||||
|
||||
// isIdentityAcceptable checks if identity encoding is acceptable based on the
|
||||
// Accept-Encoding header. Per RFC 7231 section 5.3.4, identity is always
|
||||
// implicitly acceptable unless explicitly rejected with q=0.
|
||||
func isIdentityAcceptable(r *http.Request) bool {
|
||||
header := r.Header.Get("Accept-Encoding")
|
||||
_, identityRejected := parseAcceptEncodingHeader(header)
|
||||
|
||||
return !identityRejected
|
||||
}
|
||||
|
||||
// parseAcceptEncoding parses the Accept-Encoding header and returns the best
|
||||
// supported encoding based on quality values. Per RFC 7231 section 5.3.4,
|
||||
// identity is always implicitly acceptable unless explicitly rejected with q=0.
|
||||
// If no Accept-Encoding header is present, returns empty string (identity).
|
||||
func parseAcceptEncoding(r *http.Request) (string, error) {
|
||||
header := r.Header.Get("Accept-Encoding")
|
||||
if header == "" {
|
||||
return EncodingIdentity, nil
|
||||
}
|
||||
|
||||
encodings, identityRejected := parseAcceptEncodingHeader(header)
|
||||
|
||||
// Sort by quality value (highest first)
|
||||
sort.Slice(encodings, func(i, j int) bool {
|
||||
return encodings[i].quality > encodings[j].quality
|
||||
})
|
||||
|
||||
// Find the best supported encoding
|
||||
for _, eq := range encodings {
|
||||
// Skip encodings with q=0 (explicitly rejected)
|
||||
if eq.quality == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if eq.encoding == EncodingIdentity {
|
||||
return EncodingIdentity, nil
|
||||
}
|
||||
|
||||
// Wildcard means any encoding is acceptable - return a supported encoding if identity is rejected
|
||||
if eq.encoding == EncodingWildcard {
|
||||
if identityRejected && len(SupportedEncodings) > 0 {
|
||||
return SupportedEncodings[0], nil
|
||||
}
|
||||
|
||||
return EncodingIdentity, nil
|
||||
}
|
||||
|
||||
if isSupportedEncoding(eq.encoding) {
|
||||
return eq.encoding, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Per RFC 7231, identity is implicitly acceptable unless rejected
|
||||
if !identityRejected {
|
||||
return EncodingIdentity, nil
|
||||
}
|
||||
|
||||
// Identity rejected and no supported encodings found
|
||||
return "", fmt.Errorf("no acceptable encoding found, supported: %v", SupportedEncodings)
|
||||
}
|
||||
|
||||
// getDecompressedBody returns a reader that decompresses the request body based on
|
||||
// Content-Encoding header. Returns the original body if no encoding is specified.
|
||||
// Returns an error if an unsupported encoding is specified.
|
||||
// The caller is responsible for closing both the returned ReadCloser and the
|
||||
// original request body (r.Body) separately.
|
||||
func getDecompressedBody(r *http.Request) (io.ReadCloser, error) {
|
||||
encoding, err := parseContentEncoding(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if encoding == EncodingIdentity {
|
||||
return r.Body, nil
|
||||
}
|
||||
|
||||
switch encoding {
|
||||
case EncodingGzip:
|
||||
gzReader, err := gzip.NewReader(r.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create gzip reader: %w", err)
|
||||
}
|
||||
|
||||
return gzReader, nil
|
||||
default:
|
||||
// This shouldn't happen if isSupportedEncoding is correct
|
||||
return nil, fmt.Errorf("encoding %s is supported but not implemented", encoding)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user