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

Make HTTP methods case sensitive, also allow non-standard HTTP methods #65

Merged
merged 7 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from 4 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
77 changes: 57 additions & 20 deletions src/gleam/http.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -29,34 +29,71 @@ pub type Method {
Other(String)
}

// A token is defined as:
//
// token = 1*tchar
//
// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
// / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
// / DIGIT / ALPHA
// ; any VCHAR, except delimiters
//
// (From https://www.rfc-editor.org/rfc/rfc9110.html#name-tokens)
//
// Where DIGIT = %x30-39
// ALPHA = %x41-5A / %x61-7A
// (%xXX is a hexadecimal ASCII value)
//
// (From https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1)
//
fn is_valid_token(s: String) -> Bool {
bit_array.from_string(s)
|> do_is_valid_token(True)
}

fn do_is_valid_token(bytes: BitArray, acc: Bool) {
case bytes, acc {
<<char, rest:bits>>, True -> do_is_valid_token(rest, is_valid_tchar(char))
_, _ -> acc
}
}

@external(erlang, "gleam_http_native", "is_valid_tchar")
@external(javascript, "../gleam_http_native.mjs", "is_valid_tchar")
fn is_valid_tchar(ch: Int) -> Bool

// TODO: check if the a is a valid HTTP method (i.e. it is a token, as per the
// spec) and return Ok(Other(s)) if so.
pub fn parse_method(s) -> Result(Method, Nil) {
case string.lowercase(s) {
"connect" -> Ok(Connect)
"delete" -> Ok(Delete)
"get" -> Ok(Get)
"head" -> Ok(Head)
"options" -> Ok(Options)
"patch" -> Ok(Patch)
"post" -> Ok(Post)
"put" -> Ok(Put)
"trace" -> Ok(Trace)
_ -> Error(Nil)
case s {
"CONNECT" -> Ok(Connect)
"DELETE" -> Ok(Delete)
"GET" -> Ok(Get)
"HEAD" -> Ok(Head)
"OPTIONS" -> Ok(Options)
"PATCH" -> Ok(Patch)
"POST" -> Ok(Post)
"PUT" -> Ok(Put)
"TRACE" -> Ok(Trace)
s ->
case is_valid_token(s) {
True -> Ok(Other(s))
False -> Error(Nil)
}
}
}

pub fn method_to_string(method: Method) -> String {
case method {
Connect -> "connect"
Delete -> "delete"
Get -> "get"
Head -> "head"
Options -> "options"
Patch -> "patch"
Post -> "post"
Put -> "put"
Trace -> "trace"
Connect -> "CONNECT"
Delete -> "DELETE"
Get -> "GET"
Head -> "HEAD"
Options -> "OPTIONS"
Patch -> "PATCH"
Post -> "POST"
Put -> "PUT"
Trace -> "TRACE"
Other(s) -> s
}
}
Expand Down
101 changes: 45 additions & 56 deletions src/gleam_http_native.erl
Original file line number Diff line number Diff line change
@@ -1,17 +1,47 @@
-module(gleam_http_native).
-export([decode_method/1]).
-export([decode_method/1, is_valid_tchar/1]).

is_valid_tchar(X) when is_integer(X) ->
case X of
$! -> true;
$# -> true;
$$ -> true;
$% -> true;
$& -> true;
$' -> true;
$* -> true;
$+ -> true;
$- -> true;
$. -> true;
$^ -> true;
$_ -> true;
$` -> true;
$| -> true;
$~ -> true;
% DIGIT
_ when X >= 16#30 andalso X =< 16#39 -> true;
% ALPHA
_ when (X >= 16#41 andalso X =< 16#5A) orelse (X >= 16#61 andalso X =< 16#7A) -> true;
_ -> false
end;
is_valid_tchar(_) -> false.

is_valid_token([]) -> false;
is_valid_token(<<>>) -> false;
is_valid_token(X) when is_list(X) orelse is_bitstring(X) -> do_is_valid_token(X, true);
is_valid_token(_) -> false.

do_is_valid_token(_, false) -> false;
do_is_valid_token([H|T] = X, true) when is_list(X) -> do_is_valid_token(T, is_valid_tchar(H));
do_is_valid_token(<<H, T/bits>> = X, true) when is_bitstring(X) -> do_is_valid_token(T, is_valid_tchar(H));
do_is_valid_token(_, X) -> X.

normalise_method(X) when is_bitstring(X) -> {ok, {other, X}};
normalise_method(X) when is_list(X) -> {ok, {other, list_to_binary(X)}};
normalise_method(_) -> {error, nil}.

decode_method(Term) ->
case Term of
"connect" -> {ok, connect};
"delete" -> {ok, delete};
"get" -> {ok, get};
"head" -> {ok, head};
"options" -> {ok, options};
"patch" -> {ok, patch};
"post" -> {ok, post};
"put" -> {ok, put};
"trace" -> {ok, trace};
"CONNECT" -> {ok, connect};
"DELETE" -> {ok, delete};
"GET" -> {ok, get};
Expand All @@ -21,24 +51,6 @@ decode_method(Term) ->
"POST" -> {ok, post};
"PUT" -> {ok, put};
"TRACE" -> {ok, trace};
"Connect" -> {ok, connect};
"Delete" -> {ok, delete};
"Get" -> {ok, get};
"Head" -> {ok, head};
"Options" -> {ok, options};
"Patch" -> {ok, patch};
"Post" -> {ok, post};
"Put" -> {ok, put};
"Trace" -> {ok, trace};
'connect' -> {ok, connect};
'delete' -> {ok, delete};
'get' -> {ok, get};
'head' -> {ok, head};
'options' -> {ok, options};
'patch' -> {ok, patch};
'post' -> {ok, post};
'put' -> {ok, put};
'trace' -> {ok, trace};
'CONNECT' -> {ok, connect};
'DELETE' -> {ok, delete};
'GET' -> {ok, get};
Expand All @@ -48,24 +60,6 @@ decode_method(Term) ->
'POST' -> {ok, post};
'PUT' -> {ok, put};
'TRACE' -> {ok, trace};
'Connect' -> {ok, connect};
'Delete' -> {ok, delete};
'Get' -> {ok, get};
'Head' -> {ok, head};
'Options' -> {ok, options};
'Patch' -> {ok, patch};
'Post' -> {ok, post};
'Put' -> {ok, put};
'Trace' -> {ok, trace};
<<"connect">> -> {ok, connect};
<<"delete">> -> {ok, delete};
<<"get">> -> {ok, get};
<<"head">> -> {ok, head};
<<"options">> -> {ok, options};
<<"patch">> -> {ok, patch};
<<"post">> -> {ok, post};
<<"put">> -> {ok, put};
<<"trace">> -> {ok, trace};
<<"CONNECT">> -> {ok, connect};
<<"DELETE">> -> {ok, delete};
<<"GET">> -> {ok, get};
Expand All @@ -75,14 +69,9 @@ decode_method(Term) ->
<<"POST">> -> {ok, post};
<<"PUT">> -> {ok, put};
<<"TRACE">> -> {ok, trace};
<<"Connect">> -> {ok, connect};
<<"Delete">> -> {ok, delete};
<<"Get">> -> {ok, get};
<<"Head">> -> {ok, head};
<<"Options">> -> {ok, options};
<<"Patch">> -> {ok, patch};
<<"Post">> -> {ok, post};
<<"Put">> -> {ok, put};
<<"Trace">> -> {ok, trace};
_ -> {error, nil}
X ->
case is_valid_token(X) of
true -> normalise_method(X);
false -> {error, nil}
end
end.
56 changes: 46 additions & 10 deletions src/gleam_http_native.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,66 @@ import {
Connect,
Options,
Patch,
Other,
} from "./gleam/http.mjs";

/**
@param {number} ch
*/
export function is_valid_tchar(ch) {
try {
switch (ch) {
case '33': return true;
case '35': return true;
case '36': return true;
case '37': return true;
case '38': return true;
case '39': return true;
case '42': return true;
case '43': return true;
case '45': return true;
case '46': return true;
case '94': return true;
case '95': return true;
case '96': return true;
case '124': return true;
case '126': return true;
}
// DIGIT
if (ch >= 0x30 && ch <= 0x39) return true;
// ALPHA
if (ch >= 0x41 && ch <= 0x5A || ch >= 0x61 && ch <= 0x7A) return true;
} catch {}
return false;
}

export function decode_method(value) {
try {
switch (value.toLowerCase()) {
case "get":
switch (value) {
case "GET":
return new Ok(new Get());
case "post":
case "POST":
return new Ok(new Post());
case "head":
case "HEAD":
return new Ok(new Head());
case "put":
case "PUT":
return new Ok(new Put());
case "delete":
case "DELETE":
return new Ok(new Delete());
case "trace":
case "TRACE":
return new Ok(new Trace());
case "connect":
case "CONNECT":
return new Ok(new Connect());
case "options":
case "OPTIONS":
return new Ok(new Options());
case "patch":
case "PATCH":
return new Ok(new Patch());
}
if (typeof value === 'string') {
for (const v of value)
if (!is_valid_tchar(v)) return new Error(undefined);
return new Ok(new Other(value));
}
} catch {}
return new Error(undefined);
}
Loading