-module(wsHttp).
|
|
|
|
-include_lib("eNet/include/eNet.hrl").
|
|
-include("wsCom.hrl").
|
|
|
|
-export([
|
|
start_link/1
|
|
, sendResponse/5
|
|
, sendFile/5
|
|
%% Exported for looping with a fully-qualified module name
|
|
, chunkLoop/1
|
|
, spellHeaders/1
|
|
, splitArgs/1
|
|
, closeOrKeepAlive/2
|
|
, maybeSendContinue/2
|
|
]).
|
|
|
|
%% eNet callback
|
|
-export([newCon/2]).
|
|
|
|
-export([
|
|
init_it/2
|
|
, system_code_change/4
|
|
, system_continue/3
|
|
, system_get_state/1
|
|
, system_terminate/4
|
|
]).
|
|
|
|
newCon(_Sock, WsMod) ->
|
|
?MODULE:start_link(WsMod).
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% genActor start %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
-spec(start_link(atom()) -> {ok, pid()} | ignore | {error, term()}).
|
|
start_link(WsMod) ->
|
|
proc_lib:start_link(?MODULE, init_it, [self(), WsMod], infinity, []).
|
|
|
|
init_it(Parent, Args) ->
|
|
process_flag(trap_exit, true),
|
|
modInit(Parent, Args).
|
|
|
|
-spec system_code_change(term(), module(), undefined | term(), term()) -> {ok, term()}.
|
|
system_code_change(State, _Module, _OldVsn, _Extra) ->
|
|
{ok, State}.
|
|
|
|
-spec system_continue(pid(), [], {module(), atom(), pid(), term()}) -> ok.
|
|
system_continue(_Parent, _Debug, {Parent, State}) ->
|
|
loop(Parent, State).
|
|
|
|
-spec system_get_state(term()) -> {ok, term()}.
|
|
system_get_state(State) ->
|
|
{ok, State}.
|
|
|
|
-spec system_terminate(term(), pid(), [], term()) -> none().
|
|
system_terminate(Reason, _Parent, _Debug, State) ->
|
|
terminate(Reason, State).
|
|
|
|
modInit(Parent, Args) ->
|
|
case init(Args) of
|
|
{ok, State} ->
|
|
proc_lib:init_ack(Parent, {ok, self()}),
|
|
loop(Parent, State);
|
|
{stop, Reason} ->
|
|
proc_lib:init_ack(Parent, {error, Reason}),
|
|
exit(Reason)
|
|
end.
|
|
|
|
loop(Parent, State) ->
|
|
receive
|
|
{system, From, Request} ->
|
|
sys:handle_system_msg(Request, From, Parent, ?MODULE, [], {Parent, State});
|
|
{'EXIT', Parent, Reason} ->
|
|
terminate(Reason, State);
|
|
Msg ->
|
|
case handleMsg(Msg, State) of
|
|
kpS ->
|
|
loop(Parent, State);
|
|
{ok, NewState} ->
|
|
loop(Parent, NewState);
|
|
{stop, Reason} ->
|
|
terminate(Reason, State),
|
|
exit(Reason)
|
|
end
|
|
end.
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% genActor end %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%% ************************************************ API ***************************************************************
|
|
|
|
init(WsMod) ->
|
|
{ok, #wsState{wsMod = WsMod}}.
|
|
|
|
handleMsg({tcp, _Socket, Data}, State) ->
|
|
#wsState{stage = Stage, buffer = Buffer, socket = Socket} = State,
|
|
case wsHttpProtocol:request(Stage, <<Buffer/binary, Data/binary>>, State) of
|
|
{ok, _NewState} = LRet ->
|
|
LRet;
|
|
{done, NewState} ->
|
|
Response = doHandle(NewState),
|
|
#wsState{buffer = Buffer, socket = Socket, temHeader = TemHeader, method = Method} = NewState,
|
|
case doResponse(Response, Socket, TemHeader, Method) of
|
|
keep_alive ->
|
|
handleMsg({tcp, _Socket, Buffer}, newWsState(NewState));
|
|
close ->
|
|
{stop, close}
|
|
end;
|
|
Err ->
|
|
?wsErr("recv the http data error ~p~n", [Err]),
|
|
sendBadRequest(Socket),
|
|
{stop, close}
|
|
end;
|
|
handleMsg({tcp_closed, _Socket}, _State) ->
|
|
{stop, tcp_closed};
|
|
handleMsg({tcp_error, _Socket, Reason}, _State) ->
|
|
?wsErr("the http tcp socket error ~p~n", [Reason]),
|
|
{stop, tcp_error};
|
|
|
|
handleMsg({ssl, _Socket, Data}, State) ->
|
|
#wsState{stage = Stage, buffer = Buffer, socket = Socket} = State,
|
|
case wsHttpProtocol:request(Stage, <<Buffer/binary, Data/binary>>, State) of
|
|
{ok, _NewState} = LRet ->
|
|
LRet;
|
|
{done, NewState} ->
|
|
Response = doHandle(NewState),
|
|
#wsState{buffer = Buffer, temHeader = TemHeader, method = Method} = NewState,
|
|
case doResponse(Response, Socket, TemHeader, Method) of
|
|
keep_alive ->
|
|
handleMsg({tcp, Socket, Buffer}, newWsState(NewState));
|
|
close ->
|
|
{stop, close}
|
|
end;
|
|
Err ->
|
|
?wsErr("recv the http data error ~p~n", [Err]),
|
|
sendBadRequest(Socket),
|
|
{stop, close}
|
|
end;
|
|
handleMsg({ssl_closed, _Socket}, _State) ->
|
|
{stop, ssl_closed};
|
|
handleMsg({ssl_error, _Socket, Reason}, _State) ->
|
|
?wsErr("the http ssl socket error ~p~n", [Reason]),
|
|
{stop, ssl_error};
|
|
handleMsg({?mSockReady, Sock}, _State) ->
|
|
inet:setopts(Sock, [{packet, raw}, {active, true}]),
|
|
{ok, #wsState{socket = Sock}};
|
|
handleMsg({?mSockReady, Sock, SslOpts, SslHSTet}, State) ->
|
|
case wsSslAcceptor:handshake(Sock, SslOpts, SslHSTet) of
|
|
{ok, SslSock} ->
|
|
ssl:setopts(Sock, [{packet, raw}, {active, true}]),
|
|
{ok, State#wsState{socket = SslSock, isSsl = true}};
|
|
_Err ->
|
|
?wsErr("ssl handshake error ~p~n", [_Err]),
|
|
{stop, handshake_error}
|
|
end;
|
|
handleMsg(_Msg, _State) ->
|
|
?wsErr("~p info receive unexpect msg ~p ~n ", [?MODULE, _Msg]),
|
|
kpS.
|
|
|
|
terminate(_Reason, #wsState{socket = Socket} = _State) ->
|
|
wsNet:close(Socket),
|
|
ok.
|
|
|
|
newWsState(WsState) ->
|
|
WsState#wsState{
|
|
stage = reqLine
|
|
, buffer = <<>>
|
|
, wsReq = undefined
|
|
, headerCnt = 0
|
|
, temHeader = []
|
|
, contentLength = undefined
|
|
, temChunked = <<>>
|
|
}.
|
|
|
|
%% @doc Execute the user callback, translating failure into a proper response.
|
|
doHandle(State) ->
|
|
#wsState{wsMod = WsMod, method = Method, path = Path, wsReq = WsReq} = State,
|
|
try WsMod:handle(Method, Path, WsReq) of
|
|
%% {ok,...{file,...}}
|
|
{ok, Headers, {file, Filename}} ->
|
|
{file, 200, Headers, Filename, []};
|
|
{ok, Headers, {file, Filename, Range}} ->
|
|
{file, 200, Headers, Filename, Range};
|
|
%% ok simple
|
|
{ok, Headers, Body} -> {response, 200, Headers, Body};
|
|
{ok, Body} -> {response, 200, [], Body};
|
|
%% Chunk
|
|
{chunk, Headers} -> {chunk, Headers, <<"">>};
|
|
{chunk, Headers, Initial} -> {chunk, Headers, Initial};
|
|
%% File
|
|
{HttpCode, Headers, {file, Filename}} ->
|
|
{file, HttpCode, Headers, Filename, {0, 0}};
|
|
{HttpCode, Headers, {file, Filename, Range}} ->
|
|
{file, HttpCode, Headers, Filename, Range};
|
|
%% Simple
|
|
{HttpCode, Headers, Body} -> {response, HttpCode, Headers, Body};
|
|
{HttpCode, Body} -> {response, HttpCode, [], Body};
|
|
%% Unexpected
|
|
Unexpected ->
|
|
?wsErr("handle return error ~p ~p~n", [WsReq, Unexpected]),
|
|
{response, 500, [], <<"Internal server error">>}
|
|
catch
|
|
throw:{ResponseCode, Headers, Body} when is_integer(ResponseCode) ->
|
|
{response, ResponseCode, Headers, Body};
|
|
throw:Exc:Stacktrace ->
|
|
?wsErr("handle catch throw ~p ~p ~p~n", [WsReq, Exc, Stacktrace]),
|
|
{response, 500, [], <<"Internal server error">>};
|
|
error:Error:Stacktrace ->
|
|
?wsErr("handle catch error ~p ~p ~p~n", [WsReq, Error, Stacktrace]),
|
|
{response, 500, [], <<"Internal server error">>};
|
|
exit:Exit:Stacktrace ->
|
|
?wsErr("handle catch exit ~p ~p ~p~n", [WsReq, Exit, Stacktrace]),
|
|
{response, 500, [], <<"Internal server error">>}
|
|
end.
|
|
|
|
doResponse({response, Code, UserHeaders, Body}, Socket, TemHeader, Method) ->
|
|
Headers = [connection(UserHeaders, TemHeader), contentLength(UserHeaders, Body) | UserHeaders],
|
|
sendResponse(Socket, Method, Code, Headers, Body),
|
|
closeOrKeepAlive(UserHeaders, TemHeader);
|
|
doResponse({chunk, UserHeaders, Initial}, Socket, TemHeader, Method) ->
|
|
ResponseHeaders = [transferEncoding(UserHeaders), connection(UserHeaders, TemHeader) | UserHeaders],
|
|
sendResponse(Socket, Method, 200, ResponseHeaders, <<>>),
|
|
Initial =:= <<"">> orelse sendChunk(Socket, Initial),
|
|
case startChunkLoop(Socket) of
|
|
{error, client_closed} -> client;
|
|
ok -> server
|
|
end,
|
|
close;
|
|
doResponse({file, ResponseCode, UserHeaders, Filename, Range}, Socket, TemHeader, Method) ->
|
|
ResponseHeaders = [connection(UserHeaders, TemHeader) | UserHeaders],
|
|
case wsUtil:fileSize(Filename) of
|
|
{error, _FileError} ->
|
|
sendSrvError(Socket),
|
|
{stop, file_error};
|
|
Size ->
|
|
Ret =
|
|
case wsUtil:normalizeRange(Range, Size) of
|
|
undefined ->
|
|
sendFile(Socket, ResponseCode, [{?CONTENT_LENGTH_HEADER, Size} | ResponseHeaders], Filename, {0, 0});
|
|
{Offset, Length} ->
|
|
ERange = wsUtil:encodeRange({Offset, Length}, Size),
|
|
sendFile(Socket, 206, lists:append(ResponseHeaders, [{?CONTENT_LENGTH_HEADER, Length}, {<<"Content-Range">>, ERange}]), Filename, {Offset, Length});
|
|
invalid_range ->
|
|
ERange = wsUtil:encodeRange(invalid_range, Size),
|
|
sendResponse(Socket, Method, 416, lists:append(ResponseHeaders, [{<<"Content-Length">>, 0}, {<<"Content-Range">>, ERange}]), <<>>),
|
|
{error, range}
|
|
end,
|
|
case Ret of
|
|
ok ->
|
|
closeOrKeepAlive(UserHeaders, TemHeader);
|
|
{error, Reason} ->
|
|
{stop, Reason}
|
|
end
|
|
end.
|
|
|
|
%% @doc Generate a HTTP response and send it to the client.
|
|
sendResponse(Socket, Method, Code, Headers, UserBody) ->
|
|
Body =
|
|
case Method of
|
|
'HEAD' ->
|
|
<<>>;
|
|
_ ->
|
|
case Code of
|
|
304 ->
|
|
<<>>;
|
|
204 ->
|
|
<<>>;
|
|
_ ->
|
|
UserBody
|
|
end
|
|
end,
|
|
|
|
Response = httpResponse(Code, Headers, Body),
|
|
|
|
case wsNet:send(Socket, Response) of
|
|
ok ->
|
|
ok;
|
|
_Err ->
|
|
?wsErr("send_response error ~p~n", [_Err])
|
|
end.
|
|
|
|
%% @doc Send a HTTP response to the client where the body is the
|
|
%% contents of the given file. Assumes correctly set response code
|
|
%% and headers.
|
|
-spec sendFile(Req, Code, Headers, Filename, Range) -> ok when
|
|
Req :: elli:wsReq(),
|
|
Code :: elli:httpCode(),
|
|
Headers :: elli:headers(),
|
|
Filename :: file:filename(),
|
|
Range :: wsUtil:range().
|
|
sendFile(Socket, Code, Headers, Filename, Range) ->
|
|
ResponseHeaders = httpResponse(Code, Headers, <<>>),
|
|
case file:open(Filename, [read, raw, binary]) of
|
|
{ok, Fd} -> doSendFile(Fd, Range, Socket, ResponseHeaders);
|
|
{error, _FileError} = Err ->
|
|
sendSrvError(Socket),
|
|
Err
|
|
end.
|
|
|
|
doSendFile(Fd, {Offset, Length}, Socket, Headers) ->
|
|
try wsNet:send(Socket, Headers) of
|
|
ok ->
|
|
case wsNet:sendfile(Fd, Socket, Offset, Length, []) of
|
|
{ok, _BytesSent} -> ok;
|
|
{error, Closed} = LErr when Closed =:= closed orelse Closed =:= enotconn ->
|
|
?wsErr("send file error"),
|
|
LErr
|
|
end;
|
|
{error, Closed} = LErr when Closed =:= closed orelse Closed =:= enotconn ->
|
|
?wsErr("send file error"),
|
|
LErr
|
|
after
|
|
file:close(Fd)
|
|
end.
|
|
|
|
%% @doc To send a response, we must first have received everything the
|
|
%% client is sending. If this is not the case, {@link send_bad_request/1}
|
|
%% might reset the client connection.
|
|
sendBadRequest(Socket) ->
|
|
sendRescueResponse(Socket, 400, <<"Bad Request">>).
|
|
|
|
sendSrvError(Socket) ->
|
|
sendRescueResponse(Socket, 500, <<"Server Error">>).
|
|
|
|
sendRescueResponse(Socket, Code, Body) ->
|
|
Response = httpResponse(Code, Body),
|
|
wsNet:send(Socket, Response).
|
|
|
|
%% CHUNKED-TRANSFER
|
|
%% @doc The chunk loop is an intermediary between the socket and the
|
|
%% user. We forward anything the user sends until the user sends an
|
|
%% empty response, which signals that the connection should be
|
|
%% closed. When the client closes the socket, the loop exits.
|
|
startChunkLoop(Socket) ->
|
|
%% Set the socket to active so we receive the tcp_closed message
|
|
%% if the client closes the connection
|
|
wsNet:setopts(Socket, [{active, once}]),
|
|
?MODULE:chunkLoop(Socket).
|
|
|
|
chunkLoop(Socket) ->
|
|
receive
|
|
{tcp_closed, Socket} ->
|
|
{error, client_closed};
|
|
{chunk, close} ->
|
|
case wsNet:send(Socket, <<"0\r\n\r\n">>) of
|
|
ok ->
|
|
ok;
|
|
{error, Closed} when Closed =:= closed orelse Closed =:= enotconn ->
|
|
{error, client_closed}
|
|
end;
|
|
{chunk, close, From} ->
|
|
case wsNet:send(Socket, <<"0\r\n\r\n">>) of
|
|
ok ->
|
|
From ! {self(), ok},
|
|
ok;
|
|
{error, Closed} when Closed =:= closed orelse Closed =:= enotconn ->
|
|
From ! {self(), {error, closed}},
|
|
ok
|
|
end;
|
|
{chunk, Data} ->
|
|
sendChunk(Socket, Data),
|
|
?MODULE:chunkLoop(Socket);
|
|
{chunk, Data, From} ->
|
|
case sendChunk(Socket, Data) of
|
|
ok ->
|
|
From ! {self(), ok};
|
|
{error, Closed} when Closed =:= closed orelse Closed =:= enotconn ->
|
|
From ! {self(), {error, closed}}
|
|
end,
|
|
?MODULE:chunkLoop(Socket)
|
|
after 10000 ->
|
|
?MODULE:chunkLoop(Socket)
|
|
end.
|
|
|
|
sendChunk(Socket, Data) ->
|
|
case iolist_size(Data) of
|
|
0 -> ok;
|
|
Size ->
|
|
Response = [integer_to_binary(Size, 16), <<"\r\n">>, Data, <<"\r\n">>],
|
|
wsNet:send(Socket, Response)
|
|
end.
|
|
|
|
maybeSendContinue(Socket, Headers) ->
|
|
% According to RFC2616 section 8.2.3 an origin server must respond with
|
|
% either a "100 Continue" or a final response code when the client
|
|
% headers contains "Expect:100-continue"
|
|
case lists:keyfind(?EXPECT_HEADER, 1, Headers) of
|
|
<<"100-continue">> ->
|
|
Response = httpResponse(100),
|
|
wsNet:send(Socket, Response);
|
|
_Other ->
|
|
ok
|
|
end.
|
|
|
|
httpResponse(Code) ->
|
|
httpResponse(Code, <<>>).
|
|
|
|
httpResponse(Code, Body) ->
|
|
httpResponse(Code, [{?CONTENT_LENGTH_HEADER, size(Body)}], Body).
|
|
|
|
httpResponse(Code, Headers, Body) ->
|
|
[<<"HTTP/1.1 ">>, status(Code), <<"\r\n">>, spellHeaders(Headers), <<"\r\n">>, Body].
|
|
|
|
spellHeaders(Headers) ->
|
|
<<<<(toBinStr(Key))/binary, ": ", (toBinStr(Value))/binary, "\r\n">> || {Key, Value} <- Headers>>.
|
|
|
|
-spec splitArgs(binary()) -> list({binary(), binary() | true}).
|
|
splitArgs(<<>>) -> [];
|
|
splitArgs(Qs) ->
|
|
Tokens = binary:split(Qs, <<"&">>, [global, trim]),
|
|
[case binary:split(Token, <<"=">>) of [Token] -> {Token, true}; [Name, Value] ->
|
|
{Name, Value} end || Token <- Tokens].
|
|
|
|
toBinStr(V) when is_integer(V) -> integer_to_binary(V);
|
|
toBinStr(V) when is_binary(V) -> V;
|
|
toBinStr(V) when is_list(V) -> list_to_binary(V);
|
|
toBinStr(V) when is_atom(V) -> atom_to_binary(V).
|
|
|
|
closeOrKeepAlive(UserHeaders, ReqHeader) ->
|
|
case lists:keyfind(?CONNECTION_HEADER, 1, UserHeaders) of
|
|
{_, <<"Close">>} ->
|
|
close;
|
|
{_, <<"close">>} ->
|
|
close;
|
|
_ ->
|
|
case lists:keyfind(?CONNECTION_HEADER, 1, ReqHeader) of
|
|
{_, <<"Close">>} ->
|
|
close;
|
|
{_, <<"close">>} ->
|
|
close;
|
|
_ ->
|
|
keep_alive
|
|
end
|
|
end.
|
|
|
|
connection(UserHeaders, ReqHeader) ->
|
|
case lists:keyfind(?CONNECTION_HEADER, 1, UserHeaders) of
|
|
false ->
|
|
case lists:keyfind(?CONNECTION_HEADER, 1, ReqHeader) of
|
|
false ->
|
|
{?CONNECTION_HEADER, <<"Keep-Alive">>};
|
|
Tuple ->
|
|
Tuple
|
|
end;
|
|
_ ->
|
|
[]
|
|
end.
|
|
|
|
contentLength(Headers, Body) ->
|
|
case lists:keyfind(?CONTENT_LENGTH_HEADER, Headers) of
|
|
false ->
|
|
{?CONTENT_LENGTH_HEADER, iolist_size(Body)};
|
|
_ ->
|
|
[]
|
|
end.
|
|
|
|
transferEncoding(Headers) ->
|
|
case lists:keyfind(?TRANSFER_ENCODING_HEADER, Headers) of
|
|
false ->
|
|
{?TRANSFER_ENCODING_HEADER, <<"chunked">>};
|
|
_ ->
|
|
[]
|
|
end.
|
|
|
|
%% HTTP STATUS CODES
|
|
status(100) -> <<"100 Continue">>;
|
|
status(101) -> <<"101 Switching Protocols">>;
|
|
status(102) -> <<"102 Processing">>;
|
|
status(200) -> <<"200 OK">>;
|
|
status(201) -> <<"201 Created">>;
|
|
status(202) -> <<"202 Accepted">>;
|
|
status(203) -> <<"203 Non-Authoritative Information">>;
|
|
status(204) -> <<"204 No Content">>;
|
|
status(205) -> <<"205 Reset Content">>;
|
|
status(206) -> <<"206 Partial Content">>;
|
|
status(207) -> <<"207 Multi-Status">>;
|
|
status(226) -> <<"226 IM Used">>;
|
|
status(300) -> <<"300 Multiple Choices">>;
|
|
status(301) -> <<"301 Moved Permanently">>;
|
|
status(302) -> <<"302 Found">>;
|
|
status(303) -> <<"303 See Other">>;
|
|
status(304) -> <<"304 Not Modified">>;
|
|
status(305) -> <<"305 Use Proxy">>;
|
|
status(306) -> <<"306 Switch Proxy">>;
|
|
status(307) -> <<"307 Temporary Redirect">>;
|
|
status(400) -> <<"400 Bad Request">>;
|
|
status(401) -> <<"401 Unauthorized">>;
|
|
status(402) -> <<"402 Payment Required">>;
|
|
status(403) -> <<"403 Forbidden">>;
|
|
status(404) -> <<"404 Not Found">>;
|
|
status(405) -> <<"405 Method Not Allowed">>;
|
|
status(406) -> <<"406 Not Acceptable">>;
|
|
status(407) -> <<"407 Proxy Authentication Required">>;
|
|
status(408) -> <<"408 Request Timeout">>;
|
|
status(409) -> <<"409 Conflict">>;
|
|
status(410) -> <<"410 Gone">>;
|
|
status(411) -> <<"411 Length Required">>;
|
|
status(412) -> <<"412 Precondition Failed">>;
|
|
status(413) -> <<"413 Request Entity Too Large">>;
|
|
status(414) -> <<"414 Request-URI Too Long">>;
|
|
status(415) -> <<"415 Unsupported Media Type">>;
|
|
status(416) -> <<"416 Requested Range Not Satisfiable">>;
|
|
status(417) -> <<"417 Expectation Failed">>;
|
|
status(418) -> <<"418 I'm a teapot">>;
|
|
status(422) -> <<"422 Unprocessable Entity">>;
|
|
status(423) -> <<"423 Locked">>;
|
|
status(424) -> <<"424 Failed Dependency">>;
|
|
status(425) -> <<"425 Unordered Collection">>;
|
|
status(426) -> <<"426 Upgrade Required">>;
|
|
status(428) -> <<"428 Precondition Required">>;
|
|
status(429) -> <<"429 Too Many Requests">>;
|
|
status(431) -> <<"431 Request Header Fields Too Large">>;
|
|
status(500) -> <<"500 Internal Server Error">>;
|
|
status(501) -> <<"501 Not Implemented">>;
|
|
status(502) -> <<"502 Bad Gateway">>;
|
|
status(503) -> <<"503 Service Unavailable">>;
|
|
status(504) -> <<"504 Gateway Timeout">>;
|
|
status(505) -> <<"505 HTTP Version Not Supported">>;
|
|
status(506) -> <<"506 Variant Also Negotiates">>;
|
|
status(507) -> <<"507 Insufficient Storage">>;
|
|
status(510) -> <<"510 Not Extended">>;
|
|
status(511) -> <<"511 Network Authentication Required">>;
|
|
status(I) when is_integer(I), I >= 100, I < 1000 -> <<(integer_to_binary(I))/binary, "Status">>;
|
|
status(B) when is_binary(B) -> B.
|