diff --git a/tests/test_ext_utils.nim b/tests/test_ext_utils.nim new file mode 100644 index 0000000000..5335109b71 --- /dev/null +++ b/tests/test_ext_utils.nim @@ -0,0 +1,269 @@ +## nim-ws +## Copyright (c) 2021 Status Research & Development GmbH +## Licensed under either of +## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +## * MIT license ([LICENSE-MIT](LICENSE-MIT)) +## at your option. +## This file may not be copied, modified, or distributed except according to +## those terms. + +import + pkg/[asynctest, chronos], + ../ws/ext_utils + +suite "extension parser": + test "single extension": + var app: seq[AppExt] + let res = parseExt("permessage-deflate", app) + check res == true + check app.len == 1 + if app.len == 1: + check app[0].name == "permessage-deflate" + + test "single extension quoted bad syntax": + var app: seq[AppExt] + let res = parseExt("\"zip\"", app) + check res == false + + test "basic extensions no param": + var app: seq[AppExt] + let res = parseExt("permessage-deflate, snappy, bzip", app) + check res == true + check app.len == 3 + if app.len == 3: + check app[0].name == "permessage-deflate" + check app[1].name == "snappy" + check app[2].name == "bzip" + + test "basic extensions no param bad syntax": + var app: seq[AppExt] + let res = parseExt("permessage-deflate, ", app) + check res == false + + test "basic extensions no param with trailing leading whitespaces": + var app: seq[AppExt] + let res = parseExt(" permessage-deflate, snappy, bzip ", app) + check res == true + check app.len == 3 + if app.len == 3: + check app[0].name == "permessage-deflate" + check app[1].name == "snappy" + check app[2].name == "bzip" + + test "basic extension with params": + var app: seq[AppExt] + let res = parseExt("snappy; arg1noval; arg2 = 123; arg3 = \"hello\"", app) + check res == true + check app.len == 1 + if app.len == 1: + check app[0].name == "snappy" + check app[0].params[0].name == "arg1noval" + check app[0].params[0].value == "" + + check app[0].params[1].name == "arg2" + check app[0].params[1].value == "123" + + check app[0].params[2].name == "arg3" + check app[0].params[2].value == "hello" + + test "basic extension with param + fallback": + var app: seq[AppExt] + let res = parseExt("snappy; arg = 123, snappy", app) + check res == true + check app.len == 2 + if app.len == 2: + check app[0].name == "snappy" + check app[1].name == "snappy" + + check app[0].params[0].name == "arg" + check app[0].params[0].value == "123" + + test "extension param no value + fallback": + var app: seq[AppExt] + let res = parseExt("snappy; arg, snappy", app) + check res == true + check app.len == 2 + if app.len == 2: + check app[0].name == "snappy" + check app[1].name == "snappy" + + check app[0].params[0].name == "arg" + check app[0].params[0].value == "" + + test "extension param no value + fallback bad syntax": + var app: seq[AppExt] + let res = parseExt("snappy; arg = , snappy", app) + check res == false + + test "extension param no value + fallback bad syntax": + var app: seq[AppExt] + let res = parseExt("snappy; arg = , snappy", app) + check res == false + + test "extensions bad syntax": + var app: seq[AppExt] + let res = parseExt("snappy; snappy; ", app) + check res == false + + test "extension bad syntax": + var app: seq[AppExt] + let res = parseExt("snappy; ", app) + check res == false + + test "extension param no value bad syntax": + var app: seq[AppExt] + let res = parseExt("snappy; arg = ", app) + check res == false + + test "extension param no value": + var app: seq[AppExt] + let res = parseExt("snappy; arg", app) + check res == true + check app.len == 1 + if app.len == 1: + check app[0].name == "snappy" + check app[0].params[0].name == "arg" + check app[0].params[0].value == "" + + test "extension param not closed quoted value": + var app: seq[AppExt] + let res = parseExt("snappy; arg = \"wwww", app) + check res == false + + test "inlwithasciifilename": + var app: seq[AppExt] + let res = parseExt("inline; filename=\"foo.html\"", app) + check res == true + check app[0].params[0].value == "foo.html" + + test "inlwithfnattach": + var app: seq[AppExt] + let res = parseExt("inline; filename=\"Not an attachment!\"", app) + check res == true + check app[0].params[0].value == "Not an attachment!" + + test "attwithasciifnescapedchar": + var app: seq[AppExt] + let res = parseExt("attachment; filename=\"f\\oo.html\"", app) + check res == true + check app[0].params[0].value == "foo.html" + + test "attwithasciifnescapedquote": + var app: seq[AppExt] + let res = parseExt("attachment; filename=\"\\\"quoting\\\" tested.html\"", app) + check res == true + check app[0].params[0].value == "\"quoting\" tested.html" + + test "attwithquotedsemicolon": + var app: seq[AppExt] + let res = parseExt("attachment; filename=\"Here's a semicolon;.html\"", app) + check res == true + check app[0].params[0].value == "Here's a semicolon;.html" + + test "attwithfilenameandextparamescaped": + var app: seq[AppExt] + let res = parseExt("attachment; foo=\"\\\"\\\\\"", app) + check res == true + check app[0].params[0].value == "\"\\" + + test "attwithasciifilenamenq": + var app: seq[AppExt] + let res = parseExt("attachment; filename=foo.html", app) + check res == true + check app[0].params[0].value == "foo.html" + + test "attemptyparam": + var app: seq[AppExt] + let res = parseExt("attachment; ;filename=foo", app) + check res == false + + test "attwithasciifilenamenqws": + var app: seq[AppExt] + let res = parseExt("attachment; filename=foo bar.html", app) + check res == false + + test "attwithfntokensq": + var app: seq[AppExt] + let res = parseExt("attachment; filename='foo.bar'", app) + check res == true + check app[0].params[0].value == "'foo.bar'" + + test "attfnbrokentoken": + var app: seq[AppExt] + let res = parseExt("attachment; filename=foo[1](2).html", app) + check res == false + + test "attfnbrokentokeniso": + var app: seq[AppExt] + let res = parseExt("attachment; filename=foo-ä.html", app) + check res == false + + test "attfnbrokentokenutf": + var app: seq[AppExt] + let res = parseExt("attachment; filename=foo-ä.html", app) + check res == false + + test "attmissingdisposition": + var app: seq[AppExt] + let res = parseExt("filename=foo.html", app) + check res == false + + test "attmissingdisposition2": + var app: seq[AppExt] + let res = parseExt("x=y; filename=foo.html", app) + check res == false + + test "attmissingdisposition3": + var app: seq[AppExt] + let res = parseExt("\"foo; filename=bar;baz\"; filename=qux", app) + check res == false + + test "attmissingdisposition4": + var app: seq[AppExt] + let res = parseExt("filename=foo.html, filename=bar.html", app) + check res == false + + test "emptydisposition": + var app: seq[AppExt] + let res = parseExt(" ; filename=foo.html", app) + check res == false + + test "doublecolon": + var app: seq[AppExt] + let res = parseExt(": inline; attachment; filename=foo.html", app) + check res == false + + test "attbrokenquotedfn": + var app: seq[AppExt] + let res = parseExt(" attachment; filename=\"foo.html\".txt", app) + check res == false + + test "attbrokenquotedfn2": + var app: seq[AppExt] + let res = parseExt("attachment; filename=\"bar", app) + check res == false + + test "attbrokenquotedfn3": + var app: seq[AppExt] + let res = parseExt("attachment; filename=foo\"bar;baz\"qux", app) + check res == false + + test "attmissingdelim": + var app: seq[AppExt] + let res = parseExt("attachment; foo=foo filename=bar", app) + check res == false + + test "attmissingdelim2": + var app: seq[AppExt] + let res = parseExt("attachment; filename=bar foo=foo", app) + check res == false + + test "attmissingdelim3": + var app: seq[AppExt] + let res = parseExt("attachment filename=bar", app) + check res == false + + test "attreversed": + var app: seq[AppExt] + let res = parseExt("filename=foo.html; attachment", app) + check res == false \ No newline at end of file diff --git a/tests/testcommon.nim b/tests/testcommon.nim index cf71dbce4d..f4e34de740 100644 --- a/tests/testcommon.nim +++ b/tests/testcommon.nim @@ -2,3 +2,4 @@ import ./testframes import ./testutf8 +import ./test_ext_utils diff --git a/ws/ext_utils.nim b/ws/ext_utils.nim new file mode 100644 index 0000000000..3c334ec3da --- /dev/null +++ b/ws/ext_utils.nim @@ -0,0 +1,159 @@ +## nim-ws +## Copyright (c) 2021 Status Research & Development GmbH +## Licensed under either of +## * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +## * MIT license ([LICENSE-MIT](LICENSE-MIT)) +## at your option. +## This file may not be copied, modified, or distributed except according to +## those terms. + +import + std/strutils, + pkg/httputils + +type + ExtParam* = object + name* : string + value*: string + + AppExt* = object + name* : string + params*: seq[ExtParam] + + TokenKind = enum + tkError + tkSemcol + tkComma + tkEqual + tkName + tkQuoted + tkEof + + Lexer = object + pos: int + token: string + tok: TokenKind + +const + WHITES = {' ', '\t'} + LCHAR = {'a'..'z', 'A'..'Z', '-', '_', '0'..'9','.','\''} + SEPARATORS = {'`','~','!','@','#','$','%','^','&','*','(',')','+','=', + '[','{',']','}', ';',':','\'',',','<','.','>','/','?','|'} + QCHAR = WHITES + LCHAR + SEPARATORS + +proc parseName[T: BChar](lex: var Lexer, data: openarray[T]) = + while lex.pos < data.len: + let cc = data[lex.pos] + if cc notin LCHAR: + break + lex.token.add cc + inc lex.pos + +proc parseQuoted[T: BChar](lex: var Lexer, data: openarray[T]) = + while lex.pos < data.len: + let cc = data[lex.pos] + case cc: + of QCHAR: + lex.token.add cc + inc lex.pos + of '\\': + inc lex.pos + if lex.pos >= data.len: + lex.tok = tkError + return + lex.token.add data[lex.pos] + inc lex.pos + of '\"': + inc lex.pos + lex.tok = tkQuoted + return + else: + lex.tok = tkError + return + + lex.tok = tkError + +proc next[T: BChar](lex: var Lexer, data: openarray[T]) = + while lex.pos < data.len: + if data[lex.pos] notin WHITES: + break + inc lex.pos + lex.token.setLen(0) + + if lex.pos >= data.len: + lex.tok = tkEof + return + + let c = data[lex.pos] + case c + of ';': + inc lex.pos + lex.tok = tkSemcol + return + of ',': + inc lex.pos + lex.tok = tkComma + return + of '=': + inc lex.pos + lex.tok = tkEqual + return + of LCHAR: + lex.parseName(data) + lex.tok = tkName + return + of '\"': + inc lex.pos + lex.parseQuoted(data) + return + else: + lex.tok = tkError + return + +proc parseExt*[T: BChar](data: openarray[T], output: var seq[AppExt]): bool = + var lex: Lexer + var ext: AppExt + lex.next(data) + + while lex.tok notin {tkEof, tkError}: + if lex.tok != tkName: + return false + ext.name = system.move(lex.token) + + lex.next(data) + var param: ExtParam + while lex.tok == tkSemCol: + lex.next(data) + if lex.tok in {tkEof, tkError}: + return false + if lex.tok != tkName: + return false + param.name = system.move(lex.token) + lex.next(data) + if lex.tok == tkEqual: + lex.next(data) + if lex.tok notin {tkName, tkQuoted}: + return false + param.value = system.move(lex.token) + lex.next(data) + ext.params.setLen(ext.params.len + 1) + ext.params[^1].name = system.move(param.name) + ext.params[^1].value = system.move(param.value) + + if lex.tok notin {tkSemCol, tkComma, tkEof}: + return false + + output.setLen(output.len + 1) + output[^1].name = system.move(ext.name) + output[^1].params = system.move(ext.params) + + if lex.tok == tkEof: + return true + + if lex.tok == tkComma: + lex.next(data) + if lex.tok != tkName: + return false + continue + + lex.tok != tkError