Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

client changes required for chunked responses #30

Draft
wants to merge 3 commits into
base: caw/add-chunked-support
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 180 additions & 0 deletions bhttp.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ohttp

import (
"bufio"
"bytes"
"errors"
"fmt"
Expand Down Expand Up @@ -594,3 +595,182 @@ func UnmarshalBinaryResponse(data []byte) (*http.Response, error) {
func CreateBinaryResponse(resp *http.Response) BinaryResponse {
return BinaryResponse(*resp)
}

func ConvertBhttpResponse(resp *http.Response) (*http.Response, error) {
body := bufio.NewReader(resp.Body)
return ParseBhttpResponse(body)
}

func ParseBhttpResponse(body *bufio.Reader) (*http.Response, error) {
frame, err := Read(body)
if err != nil {
return nil, fmt.Errorf("unable to read bhttp framing indicator: %s", err)
}

switch frame {
case 1:
return ParseKnownLengthBhttpResponse(body)
case 3:
return ParseIndeterminateBhttpResponse(body)
default:
return nil, fmt.Errorf("bad response framing indicator: %d", frame)
}
}

func ParseKnownLengthBhttpResponse(body *bufio.Reader) (*http.Response, error) {
data, err := io.ReadAll(body)
if err != nil {
return nil, fmt.Errorf("unable to read the known-length bhttp data: %s", err)
}

// TODO: clean up switching between indeterminate and known-length parsing
return UnmarshalBinaryResponse(append([]byte{0x03}, data...))
}

func ParseIndeterminateBhttpResponse(body *bufio.Reader) (*http.Response, error) {
controlData, err := parseIndeterminateResponseControlData(body)
if err != nil {
return nil, fmt.Errorf("unable to parse response control data: %s", err)
}

headers, err := parseIndeterminateFieldSection(body)
if err != nil {
return nil, fmt.Errorf("could not parse bhttp field section: %s", err)
}

new_body, err := setupIndeterminateContentBody(body)
if err != nil {
return nil, fmt.Errorf("unable to construct intdeterminate body parser")
}

return &http.Response{
StatusCode: controlData.statusCode,
Header: headers,
Body: new_body,
}, nil
}

func parseIndeterminateResponseControlData(body *bufio.Reader) (responseControlData, error) {
statusCode, err := Read(body)
if err != nil {
return responseControlData{}, fmt.Errorf("unable to read response status code: %s", err)
}

if statusCode <= 199 || statusCode >= 600 {
return responseControlData{}, fmt.Errorf("informational responses are unsupported")
}

return responseControlData{
statusCode: int(statusCode),
}, nil
}

func parseIndeterminateFieldSection(body *bufio.Reader) (http.Header, error) {
headers := make(http.Header)

for {
// Name Length (i) = 1
nameLength, err := Read(body)

// The headers are truncated
if err == io.EOF {
return headers, nil
}

if err != nil && err != io.EOF {
return nil, fmt.Errorf("unable to read response status code: %s", err)
}

// Content Terminator (i) = 0
if nameLength == 0 {
break
}

name := make([]byte, nameLength)
n, err := io.ReadFull(body, name)
if n != int(nameLength) || err != nil {
return nil, fmt.Errorf("unable to read header name: %s", err)
}

// Value Length (i) = 1
valueLength, err := Read(body)
if err != nil {
return nil, fmt.Errorf("unable to read header value length: %s", err)
}

value := make([]byte, valueLength)
n, err = io.ReadFull(body, value)
if n != int(valueLength) || err != nil {
return nil, fmt.Errorf("unable to read header value: %s", err)
}

headers.Add(string(name), string(value))
}

return headers, nil
}

func setupIndeterminateContentBody(body *bufio.Reader) (io.ReadCloser, error) {
return NewBhttpBody(body)
}

type BhttpBodyReader struct {
inner *bufio.Reader
buffer *bytes.Buffer
}

var _ io.ReadCloser = (*BhttpBodyReader)(nil)

func NewBhttpBody(inner *bufio.Reader) (BhttpBodyReader, error) {
buffer := bytes.NewBuffer([]byte{})
return BhttpBodyReader{
inner,
buffer,
}, nil
}

func (b BhttpBodyReader) Read(buf []byte) (int, error) {
// if the buffer has not been fully read, passthrough reads
if b.buffer.Len() != 0 {
return b.buffer.Read(buf)
}

len, err := b.readNextChunk()
if err != nil {
return b.buffer.Read(buf)
}

// We are done parsing the body:
if len == 0 && err == io.EOF {
return 0, io.EOF
}

return 0, err
}

func (b BhttpBodyReader) readNextChunk() (int, error) {
length, err := Read(b.inner)

if length == 0 {
return int(length), io.EOF
}

if err != nil {
return int(length), err
}

chunk := make([]byte, length)

n, err := io.ReadFull(b.inner, chunk)
if n != int(length) || err != nil {
return int(length), fmt.Errorf("unable to read chunk of length %d: %s", length, err)
}

b.buffer.Write(chunk)

return int(length), nil
}

func (b BhttpBodyReader) Close() error {
return nil
}
102 changes: 99 additions & 3 deletions client.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package ohttp

import (
"bufio"
"bytes"
"crypto/rand"
"encoding/binary"
"fmt"
"io"

"github.com/cloudflare/circl/hpke"
"github.com/cloudflare/circl/kem"
Expand Down Expand Up @@ -111,6 +114,99 @@ func (c Client) EncapsulateRequest(request []byte) (EncapsulatedRequest, Encapsu
}, nil
}

type ChunkedHpkeReader struct {
rc *EncapsulatedResponseContext
inner *bufio.Reader
buffer *bytes.Buffer
}

func NewChunkedHpkeReader(requestContext EncapsulatedRequestContext, body *bufio.Reader) (*ChunkedHpkeReader, error) {
_, _, AEAD := requestContext.suite.Params()

// Nonce is Nk
responseNonceLen := max(int(AEAD.KeySize()), 12)
responseNonce := make([]byte, responseNonceLen)
n, err := io.ReadFull(body, responseNonce)

if n != responseNonceLen || err != nil {
return nil, fmt.Errorf("unable to read response nonce: %s", err)
}

rc, err := requestContext.Prepare(EncapsulatedResponseHeader{responseNonce: responseNonce})
if err != nil {
return nil, fmt.Errorf("unable to create response decapsulation context: %s", err)
}

return &ChunkedHpkeReader{
rc: rc,
inner: body,
buffer: bytes.NewBuffer([]byte{}),
}, nil
}

func (r ChunkedHpkeReader) Read(buf []byte) (int, error) {
// if the buffer has not been fully read, passthrough reads
if r.buffer.Len() != 0 {
return r.buffer.Read(buf)
}

len, err := r.readNextChunk()
if err != nil {
return r.buffer.Read(buf)
}

// We are done parsing the body:
if len == 0 {
return 0, io.EOF
}

return 0, err
}

func (r ChunkedHpkeReader) readNextChunk() (int, error) {
len, err := Read(r.inner)
length := int(len)

if err != nil {
return length, nil
}

var chunk []byte

// read the chunk to the end
if length == 0 {
finalChunk, err := io.ReadAll(r.inner)
if err != nil {
return 0, fmt.Errorf("unable to read final chunk: %s", err)
}

chunk, err = r.rc.DecapsulateFinalResponseChunk(EncapsulatedResponseChunk{raw: finalChunk})
if err != nil {
return 0, fmt.Errorf("unable to decapsulate final chunk: %s", err)
}
} else {
// We have a normal, length-delimited chunk
encappedChunk := make([]byte, len)
n, err := io.ReadFull(r.inner, encappedChunk)
if n != length || err != nil {
return 0, fmt.Errorf("unable to read chunk: %s, len=%d", err, n)
}

chunk, err = r.rc.DecapsulateResponseChunk(EncapsulatedResponseChunk{raw: encappedChunk})
if err != nil {
return 0, fmt.Errorf("unable to read chunk length: %s", err)
}
}

r.buffer.Write(chunk)

return length, nil
}

func (r ChunkedHpkeReader) Close() error {
return nil
}

func (c *ChunkedClient) Prepare() (EncapsulatedRequestHeader, EncapsulatedRequestContext, error) {
return c.header, EncapsulatedRequestContext{
responseLabel: []byte(c.responseLabel),
Expand All @@ -131,13 +227,13 @@ func (c *EncapsulatedRequestContext) EncapsulateRequestChunk(requestChunk []byte
}, nil
}

func (c *EncapsulatedRequestContext) EncapsulateFinalRequestChunk(requestChunk []byte) (EncapsulatedRequestChunk, error) {
func (c *EncapsulatedRequestContext) EncapsulateFinalRequestChunk(requestChunk []byte) (EncapsulatedFinalRequestChunk, error) {
ct, err := c.context.Seal(requestChunk, []byte("final"))
if err != nil {
return EncapsulatedRequestChunk{}, err
return EncapsulatedFinalRequestChunk{}, err
}

return EncapsulatedRequestChunk{
return EncapsulatedFinalRequestChunk{
ct: ct,
}, nil
}
Expand Down
2 changes: 1 addition & 1 deletion gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ func (s *GatewayRequestContext) DecapsulateRequestChunk(requestChunk Encapsulate
return s.opener.Open(requestChunk.ct, nil)
}

func (s *GatewayRequestContext) DecapsulateFinalRequestChunk(requestChunk EncapsulatedRequestChunk) ([]byte, error) {
func (s *GatewayRequestContext) DecapsulateFinalRequestChunk(requestChunk EncapsulatedFinalRequestChunk) ([]byte, error) {
return s.opener.Open(requestChunk.ct, []byte("final"))
}

Expand Down
15 changes: 15 additions & 0 deletions messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ type EncapsulatedRequestChunk struct {
ct []byte
}

type EncapsulatedFinalRequestChunk struct {
ct []byte
}

// Non-Final Request Chunk {
// Length (i) = 1..,
// HPKE-Protected Chunk (..),
Expand All @@ -122,6 +126,17 @@ func (r EncapsulatedRequestChunk) Marshal() []byte {
return b.BytesOrPanic()
}

// Final Request Chunk Indicator (i) = 0,
// HPKE-Protected Final Chunk (..),
func (r EncapsulatedFinalRequestChunk) Marshal() []byte {
b := cryptobyte.NewBuilder(nil)

b.AddBytes([]byte{0})
b.AddBytes(r.ct)

return b.BytesOrPanic()
}

type EncapsulatedRequestContext struct {
responseLabel []byte
enc []byte
Expand Down
Loading