qh

package module
v0.0.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 16, 2025 License: MIT Imports: 21 Imported by: 0

README

qh:// - The Quite Ok HTTP Protocol

qh:// is a simplified HTTP-like protocol. Built on top of QOTP (Quite Ok Transport Protocol), it provides 0-RTT connections, built-in encryption, stream multiplexing, and uses DNS TXT records for key distribution. The protocol uses a compact binary format which eliminates the use of header compression schemes like HPACK or QPACK.

STATUS: Experimental

Comparison

Feature HTTP/1.1 HTTP/2 HTTP/3 QH
Transport TCP TCP UDP (QUIC) UDP (QOTP)
Encryption Optional (TLS) Optional (TLS) Built-in Built-in
0-RTT No With TLS 1.3 Yes Yes
Multiplexing No Yes Yes Yes
Header Format Text Binary (HPACK) Binary (QPACK) Binary (static table, no compression)
Key Distribution CAs CAs CAs DNS TXT
Avg Header Size* 1829 B 1226 B 1171 B 1318 B

*Average total headers (request + response) across 110 test cases. See Benchmark Report for details.

Documentation

Installation

go get github.com/qo-proto/qh

Run example

  • Prerequisites: Go 1.25 or higher
# Terminal 1: Start the server
go run ./examples/server/main.go

# Terminal 2: Run the client
go run ./examples/client/main.go

# Or directly in tmux with a shell script (basic client)
./run-demo-tmux.sh

# Or run concurrent examples (multiplexing)
go run ./examples/server-concurrent/main.go
go run ./examples/client-concurrent/main.go
Keylog Support (for Wireshark Decryption)

QH supports QOTP keylog output for decrypting network traffic in Wireshark. This is useful for debugging and protocol analysis.

Server-side keylog (recommended):

# Build with keylog support
go run -tags keylog ./examples/server/main.go

# The server will create qh_server_keylog.txt automatically

The keylog file format follows the SSLKEYLOGFILE convention with QOTP_SHARED_SECRET entries that can be used with the QOTP Wireshark dissector.

Benchmarks

QH protocol wire format efficiency compared against HTTP/1.1, HTTP/2, and HTTP/3.

Documentation

Overview

Package qh implements the QH (Quite Ok HTTP) protocol.

Index

Constants

View Source
const (
	// 1xx Informational responses
	StatusContinue           = 100
	StatusSwitchingProtocols = 101
	StatusProcessing         = 102
	StatusEarlyHints         = 103

	// 2xx Success responses
	StatusOK              = 200
	StatusCreated         = 201
	StatusAccepted        = 202
	StatusNoContent       = 204
	StatusResetContent    = 205
	StatusPartialContent  = 206
	StatusMultiStatus     = 207
	StatusAlreadyReported = 208
	StatusIMUsed          = 226

	// 3xx Redirection responses
	StatusMultipleChoices   = 300
	StatusMovedPermanently  = 301
	StatusFound             = 302
	StatusSeeOther          = 303
	StatusNotModified       = 304
	StatusUseProxy          = 305
	StatusTemporaryRedirect = 307
	StatusPermanentRedirect = 308

	// 4xx Client Error responses
	StatusBadRequest           = 400
	StatusUnauthorized         = 401
	StatusPaymentRequired      = 402
	StatusForbidden            = 403
	StatusNotFound             = 404
	StatusMethodNotAllowed     = 405
	StatusNotAcceptable        = 406
	StatusProxyAuthRequired    = 407
	StatusRequestTimeout       = 408
	StatusConflict             = 409
	StatusGone                 = 410
	StatusLengthRequired       = 411
	StatusPreconditionFailed   = 412
	StatusPayloadTooLarge      = 413
	StatusURITooLong           = 414
	StatusUnsupportedMediaType = 415
	StatusRangeNotSatisfiable  = 416
	StatusExpectationFailed    = 417
	StatusUnprocessableEntity  = 422
	StatusTooManyRequests      = 429

	// 5xx Server Error responses
	StatusInternalServerError   = 500
	StatusBadGateway            = 502
	StatusServiceUnavailable    = 503
	StatusGatewayTimeout        = 504
	StatusQHVersionNotSupported = 505
)

Status code constants. These mirror standard HTTP status codes and are used in QH responses.

View Source
const (
	// CustomHeader is a special header ID (0) used to indicate custom headers
	CustomHeader byte = 0
)
View Source
const (
	DefaultAcceptEncoding = "zstd, br, gzip"
)
View Source
const (
	// Version is the current QH protocol version number.
	// This value is encoded in the first byte of both requests and responses.
	Version = 0
)

Variables

View Source
var CompactToStatus map[uint8]int

CompactToStatus is the reverse mapping for decoding compact codes to HTTP status. Exported for external consumers (e.g., Wireshark plugin).

View Source
var RequestHeaderStaticTable = map[byte]headerEntry{

	0x01: {"sec-ch-ua-mobile", "?0"},
	0x02: {"sec-ch-ua-platform", "\"Windows\""},
	0x03: {"accept", "*/*"},
	0x04: {"accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"},
	0x05: {"x-requested-with", "XMLHttpRequest"},
	0x06: {"content-type", "application/json; charset=UTF-8"},
	0x07: {"content-type", "text/plain;charset=UTF-8"},
	0x08: {"sec-ch-ua-arch", "\"x86\""},
	0x09: {"sec-ch-ua-bitness", "\"64\""},
	0x0A: {"sec-gpc", "1"},
	0x0B: {"connection", "keep-alive"},
	0x0C: {"accept-language", "en-US,en;q=0.5"},
	0x0D: {"accept-encoding", "gzip, deflate, br, zstd"},
	0x0E: {"content-type", "application/json"},
	0x0F: {"sec-fetch-mode", "cors"},
	0x10: {"content-type", "application/x-www-form-urlencoded"},
	0x11: {"sec-fetch-site", "cross-site"},
	0x12: {"sec-fetch-site", "same-origin"},
	0x13: {"sec-fetch-dest", "script"},
	0x14: {"cache-control", "no-cache"},
	0x15: {"pragma", "no-cache"},
	0x16: {"sec-fetch-dest", "empty"},
	0x17: {"sec-fetch-mode", "no-cors"},
	0x18: {"cache-control", "no-cache, no-store"},
	0x19: {"accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"},
	0x1A: {"upgrade-insecure-requests", "1"},
	0x1B: {"content-type", "text/plain"},
	0x1C: {"content-type", "application/json; charset=utf-8"},
	0x1D: {"accept", "application/json"},
	0x1E: {"sec-purpose", "prefetch;prerender"},
	0x1F: {"accept", "image/avif,image/jxl,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5"},
	0x20: {"sec-fetch-dest", "image"},
	0x21: {"sec-fetch-site", "same-site"},
	0x22: {"sec-purpose", "prefetch"},
	0x23: {"accept", "image/webp,*/*"},
	0x24: {"accept", "application/json, text/plain, */*"},
	0x25: {"accept", "application/json, text/javascript, */*; q=0.01"},
	0x26: {"content-encoding", "gzip"},
	0x27: {"service-worker", "script"},
	0x28: {"content-type", "application/x-www-form-urlencoded; charset=UTF-8"},
	0x29: {"content-type", "application/json+protobuf"},
	0x2A: {"sec-fetch-mode", "same-origin"},
	0x2B: {"access-control-request-method", "POST"},
	0x2C: {"sec-fetch-dest", "style"},
	0x2D: {"accept", "application/signed-exchange;v=b3;q=0.7,*/*;q=0.8"},
	0x2E: {"accept", "text/html"},
	0x2F: {"accept", "application/font-woff2;q=1.0,application/font-woff;q=0.9,*/*;q=0.8"},
	0x30: {"sec-fetch-dest", "font"},
	0x31: {"accept", "text/event-stream"},
	0x32: {"sec-fetch-mode", "navigate"},
	0x33: {"content-length", "0"},
	0x34: {"connection", "Upgrade"},
	0x35: {"upgrade", "websocket"},
	0x36: {"cache-control", "max-age=0"},
	0x37: {"range", "bytes=0-"},
	0x38: {"sec-fetch-dest", "document"},
	0x39: {"sec-fetch-mode", "websocket"},
	0x3A: {"sec-fetch-user", "?1"},
	0x3B: {"access-control-request-method", "GET"},
	0x3C: {"sec-ch-ua-mobile", "?1"},
	0x3D: {"sec-ch-ua-platform", "\"macOS\""},
	0x3E: {"sec-ch-ua-platform", "\"Linux\""},
	0x3F: {"sec-ch-ua-platform", "\"Android\""},
	0x40: {"sec-ch-ua-arch", "\"arm\""},

	0x41: {"user-agent", ""},
	0x42: {"sec-ch-ua-mobile", ""},
	0x43: {"sec-ch-ua-platform", ""},
	0x44: {"sec-ch-ua", ""},
	0x45: {"accept", ""},
	0x46: {"content-type", ""},
	0x47: {"sec-ch-ua-platform-version", ""},
	0x48: {"sec-ch-ua-arch", ""},
	0x49: {"connection", ""},
	0x4A: {"host", ""},
	0x4B: {"sec-gpc", ""},
	0x4C: {"accept-language", ""},
	0x4D: {"sec-fetch-mode", ""},
	0x4E: {"sec-fetch-site", ""},
	0x4F: {"sec-fetch-dest", ""},
	0x50: {"accept-encoding", ""},
	0x51: {"referer", ""},
	0x52: {"authorization", ""},
	0x53: {"origin", ""},
	0x54: {"cookie", ""},
	0x55: {"cache-control", ""},
	0x56: {"pragma", ""},
	0x57: {"sec-purpose", ""},
	0x58: {"content-length", ""},
	0x59: {"prefer", ""},
	0x5A: {"content-encoding", ""},
	0x5B: {"access-control-request-method", ""},
	0x5C: {"access-control-request-headers", ""},
	0x5D: {"range", ""},
	0x5E: {"sec-websocket-version", ""},
	0x5F: {"upgrade", ""},
	0x60: {"if-none-match", ""},
	0x61: {"sec-websocket-extensions", ""},
	0x62: {"sec-websocket-key", ""},
	0x63: {"if-modified-since", ""},
	0x64: {"x-payment", ""},
}

DECODING: Maps request header IDs to entries (check entry.value: empty=Format2, non-empty=Format1)

View Source
var ResponseHeaderStaticTable = map[byte]headerEntry{}/* 190 elements not displayed */

DECODING: Maps response header IDs to entries (check entry.value: empty=Format2, non-empty=Format1)

Functions

func AppendUvarint

func AppendUvarint(buf []byte, v uint64) []byte

func Compress

func Compress(data []byte, encoding Encoding) ([]byte, error)

func DebugRequest added in v0.0.5

func DebugRequest(data []byte) string

DebugRequest returns a human-readable annotated representation of a QH request in wire format. It displays each field with its byte offset, hex values, and decoded description.

func DebugResponse added in v0.0.5

func DebugResponse(data []byte) string

DebugResponse returns a human-readable annotated representation of a QH response in wire format. It displays each field with its byte offset, hex values, and decoded description.

func DecodeStatusCode

func DecodeStatusCode(compact uint8) int

convert compact format to HTTP status code

func Decompress

func Decompress(data []byte, encoding Encoding, maxSize int) ([]byte, error)

func IsRequestComplete

func IsRequestComplete(data []byte) (bool, error)

func IsResponseComplete

func IsResponseComplete(data []byte) (bool, error)

func ReadUvarint

func ReadUvarint(buf []byte, offset int) (uint64, int, error)

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is a QH protocol client that manages connections to QH servers. It supports connection establishment with optional 0-RTT via DNS-based key exchange, automatic response decompression, and redirect handling.

func NewClient

func NewClient(opts ...ClientOption) *Client

NewClient creates a new QH client with the specified options.

func (*Client) Close

func (c *Client) Close() error

Close closes the client connection and releases associated resources. After calling Close, the client should not be used for further requests.

func (*Client) Connect

func (c *Client) Connect(addr string, _ io.Writer) error

Connect establishes a connection to a QH server at the specified address. The address should be in "host:port" format.

Connect performs concurrent DNS lookups to resolve the hostname and optionally retrieve the server's public key from a DNS TXT record (at _qotp.<host>) for 0-RTT connection establishment. If no valid DNS key is found, Connect falls back to a standard in-band key exchange handshake.

func (*Client) DELETE

func (c *Client) DELETE(host, path string, headers map[string]string) (*Response, error)

DELETE performs a DELETE request to the specified host and path. Returns the server's response or an error if the request fails.

func (*Client) GET

func (c *Client) GET(host, path string, headers map[string]string) (*Response, error)

GET performs a GET request to the specified host and path. Returns the server's response or an error if the request fails.

func (*Client) HEAD

func (c *Client) HEAD(host, path string, headers map[string]string) (*Response, error)

HEAD performs a HEAD request to the specified host and path. Returns only headers without a response body.

func (*Client) PATCH

func (c *Client) PATCH(
	host, path string,
	body []byte,
	headers map[string]string,
) (*Response, error)

PATCH performs a PATCH request with the given body to the specified host and path. The Content-Length header is automatically set based on the body size.

func (*Client) POST

func (c *Client) POST(
	host, path string,
	body []byte,
	headers map[string]string,
) (*Response, error)

POST performs a POST request with the given body to the specified host and path. The Content-Length header is automatically set based on the body size.

func (*Client) PUT

func (c *Client) PUT(host, path string, body []byte, headers map[string]string) (*Response, error)

PUT performs a PUT request with the given body to the specified host and path. The Content-Length header is automatically set based on the body size.

func (*Client) Request

func (c *Client) Request(req *Request, redirectCount int) (*Response, error)

Request sends a QH request and returns the response. The redirectCount parameter tracks the number of redirects followed and should typically be 0 for initial requests.

This method handles automatic decompression of responses if the server uses compression and the client advertised support via Accept-Encoding.

type ClientOption added in v0.0.5

type ClientOption func(*Client)

ClientOption is a functional option for configuring a Client.

func WithClientKeyLogWriter added in v0.0.5

func WithClientKeyLogWriter(w io.Writer) ClientOption

func WithMaxRedirects added in v0.0.5

func WithMaxRedirects(limit int) ClientOption

WithMaxRedirects sets the maximum number of redirects to follow. If this limit is exceeded, the request will return an error. Default is 10.

func WithMaxResponseSize added in v0.0.5

func WithMaxResponseSize(size int) ClientOption

WithMaxResponseSize sets the maximum allowed response size in bytes. Responses exceeding this limit will return an error. Default is 50MB.

type Encoding

type Encoding string

Encoding represents a compression encoding type used in Content-Encoding and Accept-Encoding headers.

const (
	Gzip   Encoding = "gzip"
	Brotli Encoding = "br"
	Zstd   Encoding = "zstd"
)

Supported compression encoding constants.

type Handler

type Handler func(*Request) *Response

Handler is a function type that processes QH requests and returns responses. Handlers are registered with the server to handle specific path and method combinations.

type Method

type Method int

Method represents a QH method encoded as an integer for compact wire format. Methods are encoded in 3 bits.

const (
	GET     Method = iota // GET retrieves a resource
	POST                  // POST submits data to be processed
	PUT                   // PUT replaces a resource
	PATCH                 // PATCH partially modifies a resource
	DELETE                // DELETE removes a resource
	HEAD                  // HEAD retrieves headers only
	OPTIONS               // OPTIONS describes communication options
)

QH method constants for use in QH requests. These are encoded as 3-bit values in the wire format.

func (Method) String

func (m Method) String() string

String returns the QH method name as a string (e.g., "GET", "POST").

type Request

type Request struct {
	Method  Method            // QH method (GET, POST, etc.)
	Host    string            // Target hostname
	Path    string            // Request path (e.g., "/api/users")
	Version uint8             // Protocol version number
	Headers map[string]string // Request headers as key-value pairs
	Body    []byte            // Optional request body
}

Request represents a QH protocol request message. It contains the QH method, target host and path, protocol version, headers as key-value pairs, and an optional body.

func ParseRequest

func ParseRequest(data []byte) (*Request, error)

func (*Request) Format

func (r *Request) Format() []byte

Format encodes a QH request into wire format bytes using varint length prefixes.

Wire format structure:

  • 1 byte: Version (2 bits) | Method (3 bits) | Reserved (3 bits)
  • varint: host length, followed by host bytes
  • varint: path length, followed by path bytes
  • varint: headers length, followed by encoded headers
  • varint: body length, followed by body bytes

type Response

type Response struct {
	Version    uint8             // Protocol version number
	StatusCode int               // QH status code
	Headers    map[string]string // Response headers as key-value pairs
	Body       []byte            // Response body content
}

Response represents a QH protocol response message. It contains the protocol version, QH status code, headers, and body.

func JSONResponse

func JSONResponse(statusCode int, body string) *Response

JSONResponse creates an application/json response with the given status code and body. This is a convenience method for returning JSON responses.

func NewResponse

func NewResponse(statusCode int, body []byte, headers map[string]string) *Response

NewResponse creates a new Response with the given status code, body, and headers. Any headers provided will override auto-generated headers.

func ParseResponse

func ParseResponse(data []byte) (*Response, error)

func TextResponse

func TextResponse(statusCode int, body string) *Response

TextResponse creates a text/plain response with the given status code and body. This is a convenience method for returning plain text responses.

func (*Response) Format

func (r *Response) Format() []byte

Format encodes a QH response into wire format bytes using varint length prefixes.

Wire format structure:

  • 1 byte: Version (2 bits) | Compact status code (6 bits)
  • varint: headers length, followed by encoded headers
  • varint: body length, followed by body bytes

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is a QH protocol server that listens for incoming connections and routes requests to registered handlers. It supports automatic response compression and configurable request size limits.

func NewServer

func NewServer(opts ...ServerOption) *Server

NewServer creates a new QH server with the specified options.

func (*Server) Close

func (s *Server) Close() error

Close shuts down the server's listener.

func (*Server) HandleFunc

func (s *Server) HandleFunc(path string, method Method, handler Handler)

HandleFunc registers a handler for a given path and method.

func (*Server) Listen

func (s *Server) Listen(addr string, _ io.Writer, seed ...string) error

func (*Server) Serve

func (s *Server) Serve() error

Serve starts the server's main loop, accepting and handling incoming streams.

type ServerOption added in v0.0.5

type ServerOption func(*Server)

ServerOption is a functional option for configuring a Server.

func WithMaxRequestSize added in v0.0.5

func WithMaxRequestSize(size int) ServerOption

WithMaxRequestSize sets the maximum allowed request size in bytes. Requests exceeding this limit will receive a 413 Payload Too Large response. Default is 10MB.

func WithMinCompressionSize added in v0.0.5

func WithMinCompressionSize(size int) ServerOption

WithMinCompressionSize sets the minimum response body size for compression. Responses smaller than this threshold will not be compressed. Default is 1KB.

func WithSupportedEncodings added in v0.0.5

func WithSupportedEncodings(encodings []Encoding) ServerOption

WithSupportedEncodings sets the compression encodings the server supports. The server will use the first client-preferred encoding that the server also supports. Default is [Zstd, Brotli, Gzip].

Directories

Path Synopsis
examples
client command
server command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL