Created
May 2, 2014 10:14
-
-
Save YoukouTenhouin/6ae44519afcdc6338e10 to your computer and use it in GitHub Desktop.
Static File Server in Erlang
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-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). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-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