Skip to content

Instantly share code, notes, and snippets.

@MosakujiHokuto
Created May 2, 2014 10:14
Show Gist options
  • Save MosakujiHokuto/6ae44519afcdc6338e10 to your computer and use it in GitHub Desktop.
Save MosakujiHokuto/6ae44519afcdc6338e10 to your computer and use it in GitHub Desktop.
Static File Server in Erlang
-module(http_parser).
-export([run/1,continue/2]).
-define(PATH_RESERVED_CHARS,"!*'();:@&=+$,/?%#[]").
is_ctl(C) ->
(((0 =< C) and (8 >= C))
or ((11 =< C) and (31 >= C))
or (C == 127)).
is_sep(C) ->
lists:member(C,"()<>@,;:\\\"/[]?={}\s\t").
allowed_in_path(C) ->
((($A =< C) and ($Z >= C))
or (($a =< C) and ($z >= C))
or (($0 =< C) and ($9 >= C))
or ((C == $.))
or lists:member(C,?PATH_RESERVED_CHARS)).
allowed_in_field(C) ->
(not (is_ctl(C) or is_sep(C))).
allowed_in_value(C) ->
(not is_ctl(C)).
do_parse(start,_,Input,_) ->
case Input of
"\r\n" ++ Rest ->
do_parse(start,none,Rest,none);
"\s" ++ Rest ->
do_parse(start,none,Rest,none);
[C|Rest] when ($A =< C) and ($Z >= C) ->
do_parse(on_method,[C],Rest,none);
[] ->
{continue,{start,none,none}}
end;
do_parse(on_method,Parsed,Input,_) ->
case Input of
"\s" ++ Rest ->
Method = lists:reverse(Parsed),
case Method of
"GET" ->
M_Atom = get,
do_parse(before_path,none,Rest,{M_Atom});
"POST" ->
M_Atom = post,
do_parse(before_path,none,Rest,{M_Atom});
"HEAD" ->
M_Atom = head,
do_parse(before_path,none,Rest,{M_Atom});
"PUT" ->
M_Atom = put,
do_parse(before_path,none,Rest,{M_Atom});
"DELETE" ->
M_Atom = delete,
do_parse(before_path,none,Rest,{M_Atom});
_ ->
throw(http_405)
end;
[C|Rest] when ($A =< C) and ($Z >= C) ->
do_parse(on_method,[C|Parsed],Rest,none);
[] ->
{continue,{on_method,Parsed,none}}
end;
do_parse(before_path,_,Input,{Method}) ->
case Input of
"\s" ++ Rest ->
do_parse(before_path,none,Rest,{Method});
[C|Rest] ->
case allowed_in_path(C) of
true ->
do_parse(on_path,[C],Rest,{Method});
false ->
throw(http_400)
end;
[] ->
{continue,{before_path,none,{Method}}}
end;
do_parse(on_path,Parsed,Input,{Method}) ->
case Input of
"\s" ++ Rest ->
Path = lists:reverse(Parsed),
do_parse(httpver,none,Rest,{Method,Path});
[C|Rest] ->
case allowed_in_path(C) of
true ->
do_parse(on_path,[C|Parsed],Rest,{Method});
false ->
throw(http_400)
end;
[] ->
{continue,{on_path,Parsed,{Method}}}
end;
do_parse(httpver,_,Input,{Method,Path}) ->
case Input of
"\s" ++ Rest ->
do_parse(before_httpver,none,Rest,{Method,Path});
"HTTP/" ++ [Major,$.,Minor|Rest] ->
HTTPVer = list_to_float([Major,$.,Minor]),
do_parse(before_header,none,Rest,{Method,Path,HTTPVer,dict:new()});
[] ->
{continue,{before_httpver,none,{Method,Path}}}
end;
do_parse(before_header,_,Input,Request) ->
case Input of
"\r\n\r\n" ++ _ ->
{ok,Request};
"\r\n" ++ Rest ->
do_parse(before_header,none,Rest,Request);
[C|Rest] ->
case allowed_in_field(C) of
true ->
do_parse(header_field,[C],Rest,Request);
false ->
throw(http_400)
end;
[] ->
{continue,{before_header,none,Request}}
end;
do_parse(header_field,Parsed,Input,Request) ->
case Input of
"\s" ++ Rest ->
do_parse(ws_before_sep,Parsed,Rest,Request);
":" ++ Rest ->
do_parse(before_header_value,lists:reverse(Parsed),Rest,Request);
[C|Rest] ->
case allowed_in_field(C) of
true ->
do_parse(header_field,[C|Parsed],Rest,Request);
false ->
throw(http_400)
end;
[] ->
{continue,{header_field,Parsed,Request}}
end;
do_parse(ws_before_sep,Parsed,Input,Request) ->
case Input of
"\s" ++ Rest ->
do_parse(ws_before_sep,Parsed,Rest,Request);
":" ++ Rest ->
do_parse(before_header_value,lists:reverse(Parsed),Rest,Request);
[] ->
{continue,{ws_before,sep,Parsed,Request}};
_ ->
throw(http_400)
end;
do_parse(before_header_value,Field,Input,Request) ->
case Input of
"\s" ++ Rest ->
do_parse(before_header_value,Field,Rest,Request);
[C|Rest] ->
case allowed_in_value(C) of
true ->
do_parse(header_value,{Field,[C]},Rest,Request);
false ->
throw(http_400)
end;
[] ->
{continue,{before_header_value,Field,Request}}
end;
do_parse(header_value,{Field,Parsed},Input,
{Method,Path,HTTPVer,Headers}) ->
case Input of
"\r\n\r\n" ++ _ ->
{ok,{Method,Path,HTTPVer,dict:append(Field,lists:reverse(Parsed),Headers)}};
"\r\n" ++ Rest ->
do_parse(before_header,none,Rest,
{Method,Path,HTTPVer,dict:append(Field,lists:reverse(Parsed),Headers)});
"\s" ++ Rest ->
do_parse(header_value,{Field,Parsed},Rest,
{Method,Path,HTTPVer,Headers});
[C|Rest] ->
case allowed_in_value(C) of
true ->
do_parse(header_value,{Field,[C|Parsed]},Rest,
{Method,Path,HTTPVer,Headers});
false ->
throw(http_400)
end;
[] ->
{continue,{header_value,{Field,Parsed},
{Method,Path,HTTPVer,Headers}}}
end.
run(Input) ->
do_parse(start,none,Input,none).
continue(Input,{Status,Parsed,Request}) ->
do_parse(Status,Parsed,Input,Request).
-module(statichttp).
-export([run/1,worker/2]).
-define(RES405,"<html><body>405 Method Not Allowed</body></html>").
-define(RES404,"<html><body>404 Not Found</body></html>").
-define(RES403,"<html><body>403 Forbidden</body><html>").
-define(RES400,"<html><body>400 Bad Request</body></html>").
run(Port) ->
{ok,LSock} = gen_tcp:listen(Port,[binary,{active,false},{packet,0},
{reuseaddr,true},{backlog,100}]),
spawn_link(?MODULE,worker,[self(),LSock]),
process_flag(trap_exit,true),
server_loop(LSock,1).
server_loop(LSock,1000) ->
receive
{'EXIT',_,_} ->
server_loop(LSock,999)
end;
server_loop(LSock,CAlive) ->
receive
new_worker ->
spawn_link(?MODULE,worker,[self(),LSock]),
server_loop(LSock,CAlive+1);
{'EXIT',_,_} ->
server_loop(LSock,CAlive-1)
end.
worker(Server,LSock) ->
case gen_tcp:accept(LSock) of
{ok,CSock} ->
Server ! new_worker,
receive_loop(CSock);
_ ->
worker(Server,LSock)
end.
receive_loop(CSock) ->
case gen_tcp:recv(CSock,0) of
{ok,Pack} ->
List = binary_to_list(Pack),
try http_parser:run(List) of
{ok,Request} ->
response(CSock,Request);
{continue,Data} ->
receive_loop(CSock,4096 - size(Pack),Data)
catch
throw:http_405 ->
gen_tcp:send(CSock,["HTTP/1.1 405 Method Not Allowed\r\n",
"Server: ErlangStatic\r\n",
"Connection: Close\r\n",
"Content-Length: ",integer_to_list(length(?RES405)),"\r\n",
"Content-Type: text/html\r\n\r\n",
?RES405]),
gen_tcp:close(CSock),
http_405;
throw:http_400 ->
gen_tcp:send(CSock,["HTTP/1.1 400 Bad Request\r\n",
"Server: ErlangStatic\r\n",
"Connection: Close\r\n",
"Content-Length: ",integer_to_list(length(?RES400)),"\r\n",
"Content-Type: text/html\r\n\r\n",
?RES400]),
gen_tcp:close(CSock),
http_400
end;
{error,closed} ->
closed;
Other ->
Other
end.
receive_loop(CSock,Bytes,_) when Bytes =< 0 ->
gen_tcp:send(CSock,["HTTP/1.1 400 Bad Request\r\n",
"Server: ErlangStatic\r\n"
"Connection: Close\r\n",
"Content-Length: ",integer_to_list(length(?RES400)),"\r\n",
"Content-Type: text/html\r\n\r\n"
?RES400]),
gen_tcp:close(CSock),
http_400;
receive_loop(CSock,Bytes,Data) ->
case gen_tcp:recv(CSock,Bytes) of
{ok,Pack} ->
List = binary_to_list(Pack),
try http_parser:continue(List,Data) of
{ok,Request} ->
response(CSock,Request);
{continue,Data} ->
receive_loop(CSock,Bytes - size(Pack),Data)
catch
throw:http_405 ->
gen_tcp:send(CSock,["HTTP/1.1 405 Method Not Allowed\r\n",
"Server: ErlangStatic\r\n",
"Connection: Close\r\n",
"Content-Length: ",integer_to_list(length(?RES405)),"\r\n",
"Content-Type: text/html\r\n\r\n",
?RES405]),
gen_tcp:close(CSock),
http_405;
throw:http_400 ->
gen_tcp:send(CSock,["HTTP/1.1 400 Bad Request\r\n",
"Server: ErlangStatic\r\n",
"Connection: Close\r\n",
"Content-Length: ",integer_to_list(length(?RES400)),"\r\n",
"Content-Type: text/html\r\n\r\n",
?RES400]),
gen_tcp:close(CSock),
http_400
end;
{error,closed} ->
closed;
Other ->
Other
end.
response(CSock,{Method,Path,_,Headers}) ->
case Method of
get ->
response(get,CSock,Path,Headers);
_ ->
gen_tcp:send(CSock,["HTTP/1.1 405 Method Not Allowed\r\n",
"Server: ErlangStatic\r\n",
"Connection: Close\r\n",
"Content-Length: ",integer_to_list(length(?RES405)),"\r\n",
"Content-Type: text/html\r\n\r\n",
?RES405]),
gen_tcp:close(CSock),
http_405
end.
response(get,CSock,Path,Headers) ->
case string:str(Path,"..") of
0 ->
NPath = case Path of
"/" ->
"index.html";
"/" ++ Rest ->
Rest;
Other ->
Other
end,
response(path_ok,CSock,NPath,Headers);
_ ->
gen_tcp:send(CSock,["HTTP/1.1 403 Forbidden\r\n",
"Server: ErlangStatic\r\n",
"Connection: Close\r\n",
"Content-Length: ",integer_to_list(length(?RES403)),"\r\n",
"Content-Type: text/html\r\n\r\n",
?RES403]),
gen_tcp:close(CSock),
http_403
end;
response(path_ok,CSock,Path,Headers) ->
FileSize = filelib:file_size(Path),
case FileSize of
0 ->
gen_tcp:send(CSock,["HTTP/1.1 404 Not Found",
"Server: ErlangStatic\r\n",
"Connection: Close\r\n",
"Content-Length: ",integer_to_list(length(?RES404)),"\r\n",
"Content-Type: text/html\r\n\r\n",
?RES404]),
gen_tcp:close(CSock),
http_404;
Other ->
response(exist,CSock,Path,Other,Headers)
end.
response(exist,CSock,Path,FileSize,Headers) ->
case filelib:is_regular(Path) of
true ->
response(all_ok,CSock,Path,FileSize,Headers);
false ->
gen_tcp:send(CSock,["HTTP/1.1 403 Forbidden\r\n",
"Server: ErlangStatic\r\n",
"Connection: Close\r\n",
"Content-Length: ",integer_to_list(length(?RES403)),"\r\n",
"Content-Type: text/html\r\n\r\n",
?RES403]),
gen_tcp:close(CSock),
http_403
end;
response(all_ok,CSock,Path,FileSize,Headers) ->
case gen_tcp:send(CSock,["HTTP/1.1 200 OK\r\n",
"Server: ErlangStatic\r\n",
"Connection: Close\r\n",
"Content-Length: ",integer_to_list(FileSize),"\r\n",
"Content-Type: text/html\r\n\r\n"]) of
ok ->
case file:sendfile(Path,CSock) of
{ok,_} ->
KeepAlive = (try dict:fetch("Connection",Headers) of
"Keep-Alive" ->
true;
_ ->
false
catch
_:_ ->
false
end),
if
KeepAlive ->
receive_loop(CSock);
true ->
gen_tcp:close(CSock),
ok
end;
{error,closed} ->
closed;
Other ->
Other
end;
{error,closed} ->
closed;
Other ->
Other
end.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment