// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

//go:build !plan9

package spdy

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"io"
	"net/http"
	"sync"

	"go.uber.org/zap"
)

const (
	SYN_STREAM ControlFrameType = 1 // https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt section 2.6.1
	SYN_REPLY  ControlFrameType = 2 // https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt section 2.6.2
	SYN_PING   ControlFrameType = 6 // https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt section 2.6.5
)

// spdyFrame is a parsed SPDY frame as defined in
// https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt
// A SPDY frame can be either a control frame or a data frame.
type spdyFrame struct {
	Raw []byte // full frame as raw bytes

	// Common frame fields:
	Ctrl    bool   // true if this is a SPDY control frame
	Payload []byte // payload as raw bytes

	// Control frame fields:
	Version uint16 // SPDY protocol version
	Type    ControlFrameType

	// Data frame fields:
	// StreamID is the id of the steam to which this data frame belongs.
	// SPDY allows transmitting multiple data streams concurrently.
	StreamID uint32
}

// Type of an SPDY control frame.
type ControlFrameType uint16

// Parse parses bytes into spdyFrame.
// If the bytes don't contain a full frame, return false.
//
// Control frame structure:
//
//	 +----------------------------------+
//	|C| Version(15bits) | Type(16bits) |
//	+----------------------------------+
//	| Flags (8)  |  Length (24 bits)   |
//	+----------------------------------+
//	|               Data               |
//	+----------------------------------+
//
// Data frame structure:
//
//	+----------------------------------+
//	|C|       Stream-ID (31bits)       |
//	+----------------------------------+
//	| Flags (8)  |  Length (24 bits)   |
//	+----------------------------------+
//	|               Data               |
//	+----------------------------------+
//
// https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt
func (sf *spdyFrame) Parse(b []byte, log *zap.SugaredLogger) (ok bool, _ error) {
	const (
		spdyHeaderLength = 8
	)
	have := len(b)
	if have < spdyHeaderLength { // input does not contain full frame
		return false, nil
	}

	if !isSPDYFrameHeader(b) {
		return false, fmt.Errorf("bytes %v do not seem to contain SPDY frames. Ensure that you are using a SPDY based client to 'kubectl exec'.", b)
	}

	payloadLength := readInt24(b[5:8])
	frameLength := payloadLength + spdyHeaderLength
	if have < frameLength { // input does not contain full frame
		return false, nil
	}

	frame := b[:frameLength:frameLength] // enforce frameLength capacity

	sf.Raw = frame
	sf.Payload = frame[spdyHeaderLength:frameLength]

	sf.Ctrl = hasControlBitSet(frame)

	if !sf.Ctrl { // data frame
		sf.StreamID = dataFrameStreamID(frame)
		return true, nil
	}

	sf.Version = controlFrameVersion(frame)
	sf.Type = controlFrameType(frame)
	return true, nil
}

// parseHeaders retrieves any headers from this spdyFrame.
func (sf *spdyFrame) parseHeaders(z *zlibReader, log *zap.SugaredLogger) (http.Header, error) {
	if !sf.Ctrl {
		return nil, fmt.Errorf("[unexpected] parseHeaders called for a frame that is not a control frame")
	}
	const (
		// +------------------------------------+
		// |X|           Stream-ID (31bits)     |
		// +------------------------------------+
		// |X| Associated-To-Stream-ID (31bits) |
		// +------------------------------------+
		// | Pri|Unused | Slot |                |
		// +-------------------+                |
		synStreamPayloadLengthBeforeHeaders = 10

		// +------------------------------------+
		// |X|           Stream-ID (31bits)     |
		//+------------------------------------+
		synReplyPayloadLengthBeforeHeaders = 4

		// +----------------------------------|
		// |            32-bit ID             |
		// +----------------------------------+
		pingPayloadLength = 4
	)

	switch sf.Type {
	case SYN_STREAM:
		if len(sf.Payload) < synStreamPayloadLengthBeforeHeaders {
			return nil, fmt.Errorf("SYN_STREAM frame too short: %v", len(sf.Payload))
		}
		z.Set(sf.Payload[synStreamPayloadLengthBeforeHeaders:])
		return parseHeaders(z, log)
	case SYN_REPLY:
		if len(sf.Payload) < synReplyPayloadLengthBeforeHeaders {
			return nil, fmt.Errorf("SYN_REPLY frame too short: %v", len(sf.Payload))
		}
		if len(sf.Payload) == synReplyPayloadLengthBeforeHeaders {
			return nil, nil // no headers
		}
		z.Set(sf.Payload[synReplyPayloadLengthBeforeHeaders:])
		return parseHeaders(z, log)
	case SYN_PING:
		if len(sf.Payload) != pingPayloadLength {
			return nil, fmt.Errorf("PING frame with unexpected length %v", len(sf.Payload))
		}
		return nil, nil // ping frame has no headers

	default:
		log.Infof("[unexpected] unknown control frame type %v", sf.Type)
	}
	return nil, nil
}

// parseHeaders expects to be passed a reader that contains a compressed SPDY control
// frame Name/Value Header Block with 0 or more headers:
//
// | Number of Name/Value pairs (int32) |   <+
// +------------------------------------+    |
// |     Length of name (int32)         |    | This section is the "Name/Value
// +------------------------------------+    | Header Block", and is compressed.
// |           Name (string)            |    |
// +------------------------------------+    |
// |     Length of value  (int32)       |    |
// +------------------------------------+    |
// |          Value   (string)          |    |
// +------------------------------------+    |
// |           (repeats)                |   <+
//
// It extracts the headers and returns them as http.Header. By doing that it
// also advances the provided reader past the headers block.
// See also https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt section 2.6.10
func parseHeaders(decompressor io.Reader, log *zap.SugaredLogger) (http.Header, error) {
	buf := bufPool.Get().(*bytes.Buffer)
	defer bufPool.Put(buf)
	buf.Reset()

	// readUint32 reads the next 4 decompressed bytes from the decompressor
	// as a uint32.
	readUint32 := func() (uint32, error) {
		const uint32Length = 4
		if _, err := io.CopyN(buf, decompressor, uint32Length); err != nil { // decompress
			return 0, fmt.Errorf("error decompressing bytes: %w", err)
		}
		return binary.BigEndian.Uint32(buf.Next(uint32Length)), nil // return as uint32
	}

	// readLenBytes decompresses and returns as bytes the next 'Name' or 'Value'
	// field from SPDY Name/Value header block. decompressor must be at
	// 'Length of name'/'Length of value' field.
	readLenBytes := func() ([]byte, error) {
		xLen, err := readUint32() // length of field to read
		if err != nil {
			return nil, err
		}
		if _, err := io.CopyN(buf, decompressor, int64(xLen)); err != nil { // decompress
			return nil, err
		}
		return buf.Next(int(xLen)), nil
	}

	numHeaders, err := readUint32()
	if err != nil {
		return nil, fmt.Errorf("error determining num headers: %v", err)
	}
	h := make(http.Header, numHeaders)
	for range numHeaders {
		name, err := readLenBytes()
		if err != nil {
			return nil, err
		}
		ns := string(name)
		if _, ok := h[ns]; ok {
			return nil, fmt.Errorf("invalid data: duplicate header %q", ns)
		}
		val, err := readLenBytes()
		if err != nil {
			return nil, fmt.Errorf("error reading header data: %w", err)
		}
		for v := range bytes.SplitSeq(val, headerSep) {
			h.Add(ns, string(v))
		}
	}
	return h, nil
}

// isSPDYFrame validates that the input bytes start with a valid SPDY frame
// header.
func isSPDYFrameHeader(f []byte) bool {
	if hasControlBitSet(f) {
		// If this is a control frame, version and type must be set.
		return controlFrameVersion(f) != uint16(0) && uint16(controlFrameType(f)) != uint16(0)
	}
	// If this is a data frame, stream ID must be set.
	return dataFrameStreamID(f) != uint32(0)
}

// spdyDataFrameStreamID returns stream ID for an SPDY data frame passed as the
// input data slice. StreaID is contained within bits [0-31) of a data frame
// header.
func dataFrameStreamID(frame []byte) uint32 {
	return binary.BigEndian.Uint32(frame[0:4]) & 0x7f
}

// controlFrameType returns the type of a SPDY control frame.
// See https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt section 2.6
func controlFrameType(f []byte) ControlFrameType {
	return ControlFrameType(binary.BigEndian.Uint16(f[2:4]))
}

// spdyControlFrameVersion returns SPDY version extracted from input bytes that
// must be a SPDY control frame.
func controlFrameVersion(frame []byte) uint16 {
	bs := binary.BigEndian.Uint16(frame[0:2]) // first 16 bits
	return bs & 0x7f                          // discard control bit
}

// hasControlBitSet returns true if the passsed bytes have SPDY control bit set.
// SPDY frames can be either control frames or data frames. A control frame has
// control bit set to 1 and a data frame has it set to 0.
func hasControlBitSet(frame []byte) bool {
	return frame[0]&0x80 == 128 // 0x80
}

var bufPool = sync.Pool{
	New: func() any {
		return new(bytes.Buffer)
	},
}

// Headers in SPDY header name/value block are separated by a 0 byte.
// https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt section 2.6.10
var headerSep = []byte{0}

func readInt24(b []byte) int {
	_ = b[2] // bounds check hint to compiler; see golang.org/issue/14808
	return int(b[0])<<16 | int(b[1])<<8 | int(b[2])
}
