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

improvement: reduce cyclomatic complexity and add license badge #15

Merged
merged 4 commits into from
Oct 5, 2023
Merged
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
![Go Build Status](https://github.com/gaukas/clienthellod/actions/workflows/go.yml/badge.svg)
[![Go Report Card](https://goreportcard.com/badge/github.com/gaukas/clienthellod)](https://goreportcard.com/report/github.com/gaukas/clienthellod)
[![DeepSource](https://app.deepsource.com/gh/gaukas/clienthellod.svg/?label=active+issues&show_trend=true&token=GugDSBnYAxAF25QNpfyAO5d2)](https://app.deepsource.com/gh/gaukas/clienthellod/)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fgaukas%2Fclienthellod.svg?type=shield&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2Fgaukas%2Fclienthellod?ref=badge_shield&issueType=license)

ClientHello Parser/Resolver as a Service from [tlsfingerprint.io](https://tlsfingerprint.io).

Expand Down Expand Up @@ -148,3 +149,8 @@ A sample Caddyfile is provided below.
}
```

## License

This project is developed and distributed under Apache-2.0 license.

[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fgaukas%2Fclienthellod.svg?type=large&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2Fgaukas%2Fclienthellod?ref=badge_large&issueType=license)
112 changes: 67 additions & 45 deletions clienthello.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,21 @@ func (ch *ClientHello) ParseClientHello() error {
ch.CipherSuites = chs.CipherSuites
ch.CompressionMethods = chs.CompressionMethods

// parse extensions
ch.parseExtensions(chs)

// Call uTLS to parse the raw bytes into ClientHelloMsg
chm := tls.UnmarshalClientHello(ch.raw[5:])
if chm == nil {
return errors.New("failed to parse ClientHello, (*tls.ClientHelloInfo).Unmarshal(): nil")
}
ch.ServerName = chm.ServerName

// In the end parse extra information from raw
return ch.parseExtra()
}

func (ch *ClientHello) parseExtensions(chs *tls.ClientHelloSpec) {
for _, ext := range chs.Extensions {
switch ext := ext.(type) {
case *tls.SupportedCurvesExtension:
Expand Down Expand Up @@ -159,16 +174,6 @@ func (ch *ClientHello) ParseClientHello() error {
}
}
}

// Call uTLS to parse the raw bytes into ClientHelloMsg
chm := tls.UnmarshalClientHello(ch.raw[5:])
if chm == nil {
return errors.New("failed to parse ClientHello, (*tls.ClientHelloInfo).Unmarshal(): nil")
}
ch.ServerName = chm.ServerName

// In the end parse extra information from raw
return ch.parseExtra()
}

// parseExtra parses extra information from raw bytes which couldn't be parsed by uTLS.
Expand Down Expand Up @@ -213,6 +218,22 @@ func (ch *ClientHello) parseExtra() error {
return errors.New("unable to read extensions data")
}

err := ch.parseExtensionsExtra(extensions)
if err != nil {
return fmt.Errorf("failed to parse extensions, parseExtensionsExtra(): %w", err)
}

// sort ch.Extensions and put result to ch.ExtensionsNormalized
ch.ExtensionsNormalized = make([]uint16, len(ch.Extensions))
copy(ch.ExtensionsNormalized, ch.Extensions)
sort.Slice(ch.ExtensionsNormalized, func(i, j int) bool {
return ch.ExtensionsNormalized[i] < ch.ExtensionsNormalized[j]
})

return nil
}

func (ch *ClientHello) parseExtensionsExtra(extensions cryptobyte.String) error {
var extensionIDs []uint16
for !extensions.Empty() {
var extensionID uint16
Expand All @@ -224,47 +245,48 @@ func (ch *ClientHello) parseExtra() error {
return errors.New("unable to read extension data")
}

switch extensionID {
case 16: // ALPN
ch.alpnWithLengths = extensionData
case 51: // keyshare
if !extensionData.Skip(2) {
return errors.New("unable to skip keyshare total length")
extensionID, err := ch.parseExtensionExtra(extensionID, extensionData)
if err != nil {
return fmt.Errorf("failed to parse extension, parseExtensionExtra(): %w", err)
}
extensionIDs = append(extensionIDs, extensionID) // extension ID might need to be overridden by parseExtensionExtra() in case of GREASE
}
ch.Extensions = extensionIDs

return nil
}

func (ch *ClientHello) parseExtensionExtra(extensionID uint16, extensionData cryptobyte.String) (uint16, error) {
switch extensionID {
case 16: // ALPN
ch.alpnWithLengths = extensionData
case 51: // keyshare
if !extensionData.Skip(2) {
return 0, errors.New("unable to skip keyshare total length")
}
for !extensionData.Empty() {
var group uint16
var length uint16
if !extensionData.ReadUint16(&group) || !extensionData.ReadUint16(&length) {
return 0, errors.New("unable to read keyshare group")
}
for !extensionData.Empty() {
var group uint16
var length uint16
if !extensionData.ReadUint16(&group) || !extensionData.ReadUint16(&length) {
return errors.New("unable to read keyshare group")
}
if utils.IsGREASEUint16(group) {
group = tls.GREASE_PLACEHOLDER
}
ch.keyshareGroupsWithLengths = append(ch.keyshareGroupsWithLengths, group)
ch.keyshareGroupsWithLengths = append(ch.keyshareGroupsWithLengths, length)

if !extensionData.Skip(int(length)) {
return errors.New("unable to skip keyshare data")
}
if utils.IsGREASEUint16(group) {
group = tls.GREASE_PLACEHOLDER
}
default:
if utils.IsGREASEUint16(extensionID) {
extensionIDs = append(extensionIDs, tls.GREASE_PLACEHOLDER)
continue
ch.keyshareGroupsWithLengths = append(ch.keyshareGroupsWithLengths, group)
ch.keyshareGroupsWithLengths = append(ch.keyshareGroupsWithLengths, length)

if !extensionData.Skip(int(length)) {
return 0, errors.New("unable to skip keyshare data")
}
}
extensionIDs = append(extensionIDs, extensionID)
default:
if utils.IsGREASEUint16(extensionID) {
return tls.GREASE_PLACEHOLDER, nil
}
}
ch.Extensions = extensionIDs

// sort ch.Extensions and put result to ch.ExtensionsNormalized
ch.ExtensionsNormalized = make([]uint16, len(ch.Extensions))
copy(ch.ExtensionsNormalized, ch.Extensions)
sort.Slice(ch.ExtensionsNormalized, func(i, j int) bool {
return ch.ExtensionsNormalized[i] < ch.ExtensionsNormalized[j]
})

return nil
return extensionID, nil
}

// FingerprintNID calculates fingerprint Numerical ID of ClientHello.
Expand Down