// Copyright 2017 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package tls

import (
	"errors"
	"fmt"
	"strings"

	"golang.org/x/crypto/cryptobyte"
)

// Fingerprinter is a struct largely for holding options for the FingerprintClientHello func
type Fingerprinter struct {
	// KeepPSK will ensure that the PreSharedKey extension is passed along into the resulting ClientHelloSpec as-is
	KeepPSK bool
	// AllowBluntMimicry will ensure that unknown extensions are
	// passed along into the resulting ClientHelloSpec as-is
	// It will not ensure that the PSK is passed along, if you require that, use KeepPSK
	// WARNING: there could be numerous subtle issues with ClientHelloSpecs
	// that are generated with this flag which could compromise security and/or mimicry
	AllowBluntMimicry bool
	// AlwaysAddPadding will always add a UtlsPaddingExtension with BoringPaddingStyle
	// at the end of the extensions list if it isn't found in the fingerprinted hello.
	// This could be useful in scenarios where the hello you are fingerprinting does not
	// have any padding, but you suspect that other changes you make to the final hello
	// (including things like different SNI lengths) would cause padding to be necessary
	AlwaysAddPadding bool
}

// FingerprintClientHello returns a ClientHelloSpec which is based on the
// ClientHello that is passed in as the data argument
//
// If the ClientHello passed in has extensions that are not recognized or cannot be handled
// it will return a non-nil error and a nil *ClientHelloSpec value
//
// The data should be the full tls record, including the record type/version/length header
// as well as the handshake type/length/version header
// https://tools.ietf.org/html/rfc5246#section-6.2
// https://tools.ietf.org/html/rfc5246#section-7.4
func (f *Fingerprinter) FingerprintClientHello(data []byte) (*ClientHelloSpec, error) {
	clientHelloSpec := &ClientHelloSpec{}
	s := cryptobyte.String(data)

	var contentType uint8
	var recordVersion uint16
	if !s.ReadUint8(&contentType) || // record type
		!s.ReadUint16(&recordVersion) || !s.Skip(2) { // record version and length
		return nil, errors.New("unable to read record type, version, and length")
	}

	if recordType(contentType) != recordTypeHandshake {
		return nil, errors.New("record is not a handshake")
	}

	var handshakeVersion uint16
	var handshakeType uint8

	if !s.ReadUint8(&handshakeType) || !s.Skip(3) || // message type and 3 byte length
		!s.ReadUint16(&handshakeVersion) || !s.Skip(32) { // 32 byte random
		return nil, errors.New("unable to read handshake message type, length, and random")
	}

	if handshakeType != typeClientHello {
		return nil, errors.New("handshake message is not a ClientHello")
	}

	clientHelloSpec.TLSVersMin = recordVersion
	clientHelloSpec.TLSVersMax = handshakeVersion

	var ignoredSessionID cryptobyte.String
	if !s.ReadUint8LengthPrefixed(&ignoredSessionID) {
		return nil, errors.New("unable to read session id")
	}

	var cipherSuitesBytes cryptobyte.String
	if !s.ReadUint16LengthPrefixed(&cipherSuitesBytes) {
		return nil, errors.New("unable to read ciphersuites")
	}
	cipherSuites := []uint16{}
	for !cipherSuitesBytes.Empty() {
		var suite uint16
		if !cipherSuitesBytes.ReadUint16(&suite) {
			return nil, errors.New("unable to read ciphersuite")
		}
		cipherSuites = append(cipherSuites, unGREASEUint16(suite))
	}
	clientHelloSpec.CipherSuites = cipherSuites

	if !readUint8LengthPrefixed(&s, &clientHelloSpec.CompressionMethods) {
		return nil, errors.New("unable to read compression methods")
	}

	if s.Empty() {
		// ClientHello is optionally followed by extension data
		return clientHelloSpec, nil
	}

	var extensions cryptobyte.String
	if !s.ReadUint16LengthPrefixed(&extensions) || !s.Empty() {
		return nil, errors.New("unable to read extensions data")
	}

	for !extensions.Empty() {
		var extension uint16
		var extData cryptobyte.String
		if !extensions.ReadUint16(&extension) ||
			!extensions.ReadUint16LengthPrefixed(&extData) {
			return nil, errors.New("unable to read extension data")
		}

		switch extension {
		case extensionServerName:
			// RFC 6066, Section 3
			var nameList cryptobyte.String
			if !extData.ReadUint16LengthPrefixed(&nameList) || nameList.Empty() {
				return nil, errors.New("unable to read server name extension data")
			}
			var serverName string
			for !nameList.Empty() {
				var nameType uint8
				var serverNameBytes cryptobyte.String
				if !nameList.ReadUint8(&nameType) ||
					!nameList.ReadUint16LengthPrefixed(&serverNameBytes) ||
					serverNameBytes.Empty() {
					return nil, errors.New("unable to read server name extension data")
				}
				if nameType != 0 {
					continue
				}
				if len(serverName) != 0 {
					return nil, errors.New("multiple names of the same name_type in server name extension are prohibited")
				}
				serverName = string(serverNameBytes)
				if strings.HasSuffix(serverName, ".") {
					return nil, errors.New("SNI value may not include a trailing dot")
				}

				clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &SNIExtension{})

			}
		case extensionNextProtoNeg:
			// draft-agl-tls-nextprotoneg-04
			clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &NPNExtension{})

		case extensionStatusRequest:
			// RFC 4366, Section 3.6
			var statusType uint8
			var ignored cryptobyte.String
			if !extData.ReadUint8(&statusType) ||
				!extData.ReadUint16LengthPrefixed(&ignored) ||
				!extData.ReadUint16LengthPrefixed(&ignored) {
				return nil, errors.New("unable to read status request extension data")
			}

			if statusType == statusTypeOCSP {
				clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &StatusRequestExtension{})
			} else {
				return nil, errors.New("status request extension statusType is not statusTypeOCSP")
			}

		case extensionSupportedCurves:
			// RFC 4492, sections 5.1.1 and RFC 8446, Section 4.2.7
			var curvesBytes cryptobyte.String
			if !extData.ReadUint16LengthPrefixed(&curvesBytes) || curvesBytes.Empty() {
				return nil, errors.New("unable to read supported curves extension data")
			}
			curves := []CurveID{}
			for !curvesBytes.Empty() {
				var curve uint16
				if !curvesBytes.ReadUint16(&curve) {
					return nil, errors.New("unable to read supported curves extension data")
				}
				curves = append(curves, CurveID(unGREASEUint16(curve)))
			}
			clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &SupportedCurvesExtension{curves})

		case extensionSupportedPoints:
			// RFC 4492, Section 5.1.2
			supportedPoints := []uint8{}
			if !readUint8LengthPrefixed(&extData, &supportedPoints) ||
				len(supportedPoints) == 0 {
				return nil, errors.New("unable to read supported points extension data")
			}
			clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &SupportedPointsExtension{supportedPoints})

		case extensionSessionTicket:
			// RFC 5077, Section 3.2
			clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &SessionTicketExtension{})

		case extensionSignatureAlgorithms:
			// RFC 5246, Section 7.4.1.4.1
			var sigAndAlgs cryptobyte.String
			if !extData.ReadUint16LengthPrefixed(&sigAndAlgs) || sigAndAlgs.Empty() {
				return nil, errors.New("unable to read signature algorithms extension data")
			}
			supportedSignatureAlgorithms := []SignatureScheme{}
			for !sigAndAlgs.Empty() {
				var sigAndAlg uint16
				if !sigAndAlgs.ReadUint16(&sigAndAlg) {
					return nil, errors.New("unable to read signature algorithms extension data")
				}
				supportedSignatureAlgorithms = append(
					supportedSignatureAlgorithms, SignatureScheme(sigAndAlg))
			}
			clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &SignatureAlgorithmsExtension{supportedSignatureAlgorithms})

		case extensionSignatureAlgorithmsCert:
			// RFC 8446, Section 4.2.3
			if f.AllowBluntMimicry {
				clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &GenericExtension{extension, extData})
			} else {
				return nil, errors.New("unsupported extension SignatureAlgorithmsCert")
			}

		case extensionRenegotiationInfo:
			// RFC 5746, Section 3.2
			clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &RenegotiationInfoExtension{RenegotiateOnceAsClient})

		case extensionALPN:
			// RFC 7301, Section 3.1
			var protoList cryptobyte.String
			if !extData.ReadUint16LengthPrefixed(&protoList) || protoList.Empty() {
				return nil, errors.New("unable to read ALPN extension data")
			}
			alpnProtocols := []string{}
			for !protoList.Empty() {
				var proto cryptobyte.String
				if !protoList.ReadUint8LengthPrefixed(&proto) || proto.Empty() {
					return nil, errors.New("unable to read ALPN extension data")
				}
				alpnProtocols = append(alpnProtocols, string(proto))

			}
			clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &ALPNExtension{alpnProtocols})

		case extensionSCT:
			// RFC 6962, Section 3.3.1
			clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &SCTExtension{})

		case extensionSupportedVersions:
			// RFC 8446, Section 4.2.1
			var versList cryptobyte.String
			if !extData.ReadUint8LengthPrefixed(&versList) || versList.Empty() {
				return nil, errors.New("unable to read supported versions extension data")
			}
			supportedVersions := []uint16{}
			for !versList.Empty() {
				var vers uint16
				if !versList.ReadUint16(&vers) {
					return nil, errors.New("unable to read supported versions extension data")
				}
				supportedVersions = append(supportedVersions, unGREASEUint16(vers))
			}
			clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &SupportedVersionsExtension{supportedVersions})
			// If SupportedVersionsExtension is present, use that instead of record+handshake versions
			clientHelloSpec.TLSVersMin = 0
			clientHelloSpec.TLSVersMax = 0

		case extensionKeyShare:
			// RFC 8446, Section 4.2.8
			var clientShares cryptobyte.String
			if !extData.ReadUint16LengthPrefixed(&clientShares) {
				return nil, errors.New("unable to read key share extension data")
			}
			keyShares := []KeyShare{}
			for !clientShares.Empty() {
				var ks KeyShare
				var group uint16
				if !clientShares.ReadUint16(&group) ||
					!readUint16LengthPrefixed(&clientShares, &ks.Data) ||
					len(ks.Data) == 0 {
					return nil, errors.New("unable to read key share extension data")
				}
				ks.Group = CurveID(unGREASEUint16(group))
				// if not GREASE, key share data will be discarded as it should
				// be generated per connection
				if ks.Group != GREASE_PLACEHOLDER {
					ks.Data = nil
				}
				keyShares = append(keyShares, ks)
			}
			clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &KeyShareExtension{keyShares})

		case extensionPSKModes:
			// RFC 8446, Section 4.2.9
			// TODO: PSK Modes have their own form of GREASE-ing which is not currently implemented
			// the current functionality will NOT re-GREASE/re-randomize these values when using a fingerprinted spec
			// https://github.com/refraction-networking/utls/pull/58#discussion_r522354105
			// https://tools.ietf.org/html/draft-ietf-tls-grease-01#section-2
			pskModes := []uint8{}
			if !readUint8LengthPrefixed(&extData, &pskModes) {
				return nil, errors.New("unable to read PSK extension data")
			}
			clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &PSKKeyExchangeModesExtension{pskModes})

		case utlsExtensionExtendedMasterSecret:
			// https://tools.ietf.org/html/rfc7627
			clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &UtlsExtendedMasterSecretExtension{})

		case utlsExtensionPadding:
			clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &UtlsPaddingExtension{GetPaddingLen: BoringPaddingStyle})

		case utlsExtensionCompressCertificate:
			methods := []CertCompressionAlgo{}
			methodsRaw := new(cryptobyte.String)
			if !extData.ReadUint8LengthPrefixed(methodsRaw) {
				return nil, errors.New("unable to read cert compression algorithms extension data")
			}
			for !methodsRaw.Empty() {
				var method uint16
				if !methodsRaw.ReadUint16(&method) {
					return nil, errors.New("unable to read cert compression algorithms extension data")
				}
				methods = append(methods, CertCompressionAlgo(method))
			}
			clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &UtlsCompressCertExtension{methods})

		case fakeExtensionChannelID:
			clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &FakeChannelIDExtension{})

		case fakeExtensionChannelIDOld:
			clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &FakeChannelIDExtension{true})

		case fakeExtensionTokenBinding:
			var tokenBindingExt FakeTokenBindingExtension
			var keyParameters cryptobyte.String
			if !extData.ReadUint8(&tokenBindingExt.MajorVersion) ||
				!extData.ReadUint8(&tokenBindingExt.MinorVersion) ||
				!extData.ReadUint8LengthPrefixed(&keyParameters) {
				return nil, errors.New("unable to read token binding extension data")
			}
			tokenBindingExt.KeyParameters = keyParameters
			clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &tokenBindingExt)

		case fakeExtensionALPS:
			// Similar to ALPN (RFC 7301, Section 3.1):
			// https://datatracker.ietf.org/doc/html/draft-vvv-tls-alps#section-3
			var protoList cryptobyte.String
			if !extData.ReadUint16LengthPrefixed(&protoList) || protoList.Empty() {
				return nil, errors.New("unable to read ALPS extension data")
			}
			supportedProtocols := []string{}
			for !protoList.Empty() {
				var proto cryptobyte.String
				if !protoList.ReadUint8LengthPrefixed(&proto) || proto.Empty() {
					return nil, errors.New("unable to read ALPS extension data")
				}
				supportedProtocols = append(supportedProtocols, string(proto))
			}
			clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &ApplicationSettingsExtension{supportedProtocols})

		case fakeRecordSizeLimit:
			recordSizeExt := new(FakeRecordSizeLimitExtension)
			if !extData.ReadUint16(&recordSizeExt.Limit) {
				return nil, errors.New("unable to read record size limit extension data")
			}
			clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, recordSizeExt)

		case fakeExtensionDelegatedCredentials:
			//https://datatracker.ietf.org/doc/html/draft-ietf-tls-subcerts-15#section-4.1.1
			var supportedAlgs cryptobyte.String
			if !extData.ReadUint16LengthPrefixed(&supportedAlgs) || supportedAlgs.Empty() {
				return nil, errors.New("unable to read signature algorithms extension data")
			}
			supportedSignatureAlgorithms := []SignatureScheme{}
			for !supportedAlgs.Empty() {
				var sigAndAlg uint16
				if !supportedAlgs.ReadUint16(&sigAndAlg) {
					return nil, errors.New("unable to read signature algorithms extension data")
				}
				supportedSignatureAlgorithms = append(
					supportedSignatureAlgorithms, SignatureScheme(sigAndAlg))
			}
			clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &FakeDelegatedCredentialsExtension{supportedSignatureAlgorithms})

		case extensionPreSharedKey:
			// RFC 8446, Section 4.2.11
			if f.KeepPSK {
				clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &GenericExtension{extension, extData})
			} else {
				return nil, errors.New("unsupported extension PreSharedKey")
			}

		case extensionCookie:
			// RFC 8446, Section 4.2.2
			if f.AllowBluntMimicry {
				clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &GenericExtension{extension, extData})
			} else {
				return nil, errors.New("unsupported extension Cookie")
			}

		case extensionEarlyData:
			// RFC 8446, Section 4.2.10
			if f.AllowBluntMimicry {
				clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &GenericExtension{extension, extData})
			} else {
				return nil, errors.New("unsupported extension EarlyData")
			}

		default:
			if isGREASEUint16(extension) {
				clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &UtlsGREASEExtension{unGREASEUint16(extension), extData})
			} else if f.AllowBluntMimicry {
				clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &GenericExtension{extension, extData})
			} else {
				return nil, fmt.Errorf("unsupported extension %d", extension)
			}

			continue
		}
	}

	if f.AlwaysAddPadding {
		alreadyHasPadding := false
		for _, ext := range clientHelloSpec.Extensions {
			if _, ok := ext.(*UtlsPaddingExtension); ok {
				alreadyHasPadding = true
				break
			}
		}
		if !alreadyHasPadding {
			clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &UtlsPaddingExtension{GetPaddingLen: BoringPaddingStyle})
		}
	}

	return clientHelloSpec, nil
}
