mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2024-04-21 12:32:08 +00:00
310 lines
7.6 KiB
Go
310 lines
7.6 KiB
Go
// Copyright (C) 2019 Storj Labs, Inc.
|
|
// See LICENSE for copying information.
|
|
|
|
package useragent
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// This parsing implements useragents based on:
|
|
// https://tools.ietf.org/html/rfc7231#section-5.5.3
|
|
//
|
|
// User-Agent = product *( RWS ( product / comment ) )
|
|
//
|
|
// product = token ["/" product-version]
|
|
// product-version = token
|
|
//
|
|
// token = 1*tchar
|
|
//
|
|
// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
|
|
// / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
|
|
// / DIGIT / ALPHA
|
|
// ; any VCHAR, except delimiters
|
|
//
|
|
// quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
|
|
// qdtext = HTAB / SP /%x21 / %x23-5B / %x5D-7E / obs-text
|
|
// obs-text = %x80-FF
|
|
//
|
|
// comment = "(" *( ctext / quoted-pair / comment ) ")"
|
|
// ctext = HTAB / SP / %x21-27 / %x2A-5B / %x5D-7E / obs-text
|
|
//
|
|
// quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
|
|
//
|
|
// delimiters = DQUOTE and "(),/:;<=>?@[\]{}".
|
|
//
|
|
// VCHAR = %x21-7E ; any 7bit ansi character, except space
|
|
|
|
// The code below uses variations of
|
|
// `Mozilla/5.0 (Linux; U; Android 4.4.3;)`
|
|
// as examples.
|
|
|
|
// Entry represents a single item in useragent string.
|
|
type Entry struct {
|
|
Product string
|
|
Version string
|
|
Comment string
|
|
}
|
|
|
|
// ParseEntries parses every entry in useragent string.
|
|
func ParseEntries(data []byte) ([]Entry, error) {
|
|
data = bytes.TrimSpace(data)
|
|
if len(data) == 0 {
|
|
return []Entry{}, nil
|
|
}
|
|
|
|
// Parses the first entry, this must not be a comment.
|
|
// v---------v
|
|
// `Mozilla/5.0 (Linux; U; Android 4.4.3;)`
|
|
e, p, err := parseEntry(data, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if e.Comment != "" {
|
|
return nil, fmt.Errorf("expected product not a comment @%d", 0)
|
|
}
|
|
|
|
entries := []Entry{e}
|
|
|
|
for p < len(data) {
|
|
var ok bool
|
|
// Skip the required whitespace.
|
|
// v-----v
|
|
// `Mozilla/5.0 (Linux; U; Android 4.4.3;)`
|
|
if p, ok = requireWhitespace(data, p); !ok {
|
|
return nil, fmt.Errorf("expected whitespace @%d", p)
|
|
}
|
|
|
|
// Parse any other entries, including comments.
|
|
// v------------------------v
|
|
// `Mozilla/5.0 (Linux; U; Android 4.4.3;)`
|
|
e, p, err = parseEntry(data, p)
|
|
if err != nil {
|
|
return entries, err
|
|
}
|
|
|
|
entries = append(entries, e)
|
|
}
|
|
|
|
return entries, nil
|
|
}
|
|
|
|
// parseEntry parses either a product or a comment.
|
|
//
|
|
// Entry = ( product / comment )
|
|
func parseEntry(data []byte, from int) (e Entry, next int, err error) {
|
|
// We are at the start of a character. Example valid input:
|
|
// `Mozilla/5.0`
|
|
// `(Linux; U; Android 4.4.3;)`
|
|
|
|
var ok bool
|
|
|
|
// This is a comment.
|
|
// v
|
|
// `(Linux; U; Android 4.4.3;)`
|
|
if _, ok = acceptOne(data, from, '('); ok {
|
|
e.Comment, next, err = parseComment(data, from)
|
|
return e, next, err
|
|
}
|
|
|
|
// Otherwise, it must be a product.
|
|
e.Product, e.Version, next, err = parseProduct(data, from)
|
|
return e, next, err
|
|
}
|
|
|
|
// parseProduct parses product with optional version.
|
|
//
|
|
// product = token ["/" product-version]
|
|
// product-version = token
|
|
func parseProduct(data []byte, from int) (product, version string, next int, err error) {
|
|
// Example valid input:
|
|
// `Mozilla`
|
|
// `Mozilla/5.0`
|
|
// `Mozilla/5.0-rc.3`
|
|
|
|
var ok bool
|
|
p := from
|
|
product, p, ok = parseToken(data, p)
|
|
if !ok {
|
|
return "", "", p, fmt.Errorf("expected product token @%d", p)
|
|
}
|
|
|
|
// should we have a version?
|
|
p, ok = acceptOne(data, p, '/')
|
|
if !ok {
|
|
return product, "", p, nil
|
|
}
|
|
|
|
// we must have a version
|
|
version, p, ok = parseToken(data, p)
|
|
if !ok {
|
|
return "", "", from, fmt.Errorf("expected version token @%d", p)
|
|
}
|
|
|
|
return product, version, p, nil
|
|
}
|
|
|
|
// parseToken parses a token.
|
|
//
|
|
// token = 1*tchar
|
|
func parseToken(data []byte, from int) (token string, next int, ok bool) {
|
|
// token is a sequence of token characters.
|
|
// See tchar for the allowed characters.
|
|
next = from
|
|
for ; next < len(data); next++ {
|
|
if !istchar(data[next]) {
|
|
break
|
|
}
|
|
}
|
|
return string(data[from:next]), next, from < next
|
|
}
|
|
|
|
// parseComment parses a potentially comment. This does not allow nesting.
|
|
//
|
|
// comment = "(" *( ctext / quoted-pair / comment ) ")"
|
|
// ctext = HTAB / SP / %x21-27 / %x2A-5B / %x5D-7E / obs-text
|
|
// quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
|
|
// obs-text = %x80-FF
|
|
func parseComment(data []byte, from int) (comment string, next int, err error) {
|
|
// `(Linux; U; Android 4.4.3;)`
|
|
// `(Linux; \(; Android)`
|
|
// `(Linux; \); Android)`
|
|
//
|
|
// RFC also supports nested comments, however we disallow it:
|
|
// `(Linux; (Android) Quoted)`
|
|
|
|
p, ok := acceptOne(data, from, '(')
|
|
if !ok {
|
|
return "", from, fmt.Errorf("expected comment start '(' @%d", p)
|
|
}
|
|
|
|
var out strings.Builder
|
|
for {
|
|
if p >= len(data) {
|
|
return "", from, fmt.Errorf("expected comment char @%d", p)
|
|
}
|
|
if data[p] == ')' {
|
|
break
|
|
}
|
|
// divergence from RFC, disallow nested comments
|
|
if data[p] == '(' {
|
|
return "", 0, fmt.Errorf("nested comments not allowed @%d", p)
|
|
}
|
|
|
|
var b byte
|
|
b, p, err = parseCommentChar(data, p)
|
|
if err != nil {
|
|
return "", from, err
|
|
}
|
|
|
|
_ = out.WriteByte(b)
|
|
}
|
|
|
|
p, ok = acceptOne(data, p, ')')
|
|
if !ok {
|
|
return "", from, fmt.Errorf("expected comment end ')' @%d", p)
|
|
}
|
|
|
|
return out.String(), p, nil
|
|
}
|
|
|
|
// parseCommentChar parses comment character.
|
|
//
|
|
// ctext = HTAB / SP / %x21-27 / %x2A-5B / %x5D-7E / obs-text
|
|
// quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
|
|
// obs-text = %x80-FF
|
|
func parseCommentChar(data []byte, from int) (ch byte, next int, err error) {
|
|
p := from
|
|
b := data[p]
|
|
p++
|
|
|
|
// quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
|
|
if b == '\\' {
|
|
if p >= len(data) {
|
|
return 0, from, fmt.Errorf("unexpected end at @%d", p)
|
|
}
|
|
b = data[p]
|
|
p++
|
|
if !(b == '\t' || b == ' ' || isvchar(b) || isobstext(b)) {
|
|
return 0, from, fmt.Errorf("expected quoted-pair char at @%d", p-1)
|
|
}
|
|
return b, p, nil
|
|
}
|
|
|
|
// ctext = HTAB / SP / %x21-27 / %x2A-5B / %x5D-7E / obs-text
|
|
if b == '\t' || b == ' ' ||
|
|
inrange(b, 0x21, 0x27) ||
|
|
inrange(b, 0x2A, 0x5B) ||
|
|
inrange(b, 0x5D, 0x7E) ||
|
|
isobstext(b) {
|
|
return b, p, nil
|
|
}
|
|
|
|
return 0, from, fmt.Errorf("expected ctext, quoted-pair or comment-char @%d, but got %q", p, string(b))
|
|
}
|
|
|
|
func inrange(b byte, from, to byte) bool {
|
|
return from <= b && b <= to
|
|
}
|
|
|
|
func acceptOne(data []byte, from int, b byte) (next int, ok bool) {
|
|
if from < len(data) {
|
|
next = from
|
|
if data[next] == b {
|
|
next++
|
|
return next, true
|
|
}
|
|
}
|
|
return from, false
|
|
}
|
|
|
|
func requireWhitespace(data []byte, from int) (next int, ok bool) {
|
|
next = from
|
|
for ; next < len(data); next++ {
|
|
p := data[next]
|
|
if p != ' ' && p != '\t' {
|
|
return next, from < next
|
|
}
|
|
}
|
|
return next, true
|
|
}
|
|
|
|
// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
|
|
// / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
|
|
// / DIGIT / ALPHA
|
|
// ; any VCHAR, except delimiters
|
|
func istchar(b byte) bool {
|
|
// DIGIT / ALPHA
|
|
if '0' <= b && b <= '9' || 'a' <= b && b <= 'z' || 'A' <= b && b <= 'Z' {
|
|
return true
|
|
}
|
|
|
|
switch b {
|
|
// the set of symbols allowed
|
|
case '!', '#', '$', '%', '&', '\'', '*', '+', '-', '.', '^', '_', '`', '|', '~':
|
|
return true
|
|
}
|
|
|
|
// otherwise, just verify that it is visible
|
|
return isvchar(b) && !isdelim(b)
|
|
}
|
|
|
|
// isvchar verifies that VCHAR is a US-ASCII visible character.
|
|
func isvchar(b byte) bool {
|
|
return 0x21 <= b && b <= 0x7e // TODO: should this be 7f?
|
|
}
|
|
|
|
func isdelim(b byte) bool {
|
|
switch b {
|
|
case '"', '(', ')', ',', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '{', '}':
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isobstext(b byte) bool {
|
|
return 0x80 <= b && b <= 0xFF
|
|
}
|