%%% -*- coding: utf-8; Mode: erlang; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- %%% ex: set softtabstop=4 tabstop=4 shiftwidth=4 expandtab fileencoding=utf-8: %%% %%%------------------------------------------------------------------------ %%% @doc %%% ==Elli CloudI HTTP Handler== %%% @end %%% %%% BSD LICENSE %%% %%% Copyright (c) 2013-2014, Michael Truog %%% All rights reserved. %%% %%% Redistribution and use in source and binary forms, with or without %%% modification, are permitted provided that the following conditions are met: %%% %%% * Redistributions of source code must retain the above copyright %%% notice, this list of conditions and the following disclaimer. %%% * Redistributions in binary form must reproduce the above copyright %%% notice, this list of conditions and the following disclaimer in %%% the documentation and/or other materials provided with the %%% distribution. %%% * All advertising materials mentioning features or use of this %%% software must display the following acknowledgment: %%% This product includes software developed by Michael Truog %%% * The name of the author may not be used to endorse or promote %%% products derived from this software without specific prior %%% written permission %%% %%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND %%% CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, %%% INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES %%% OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE %%% DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR %%% CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, %%% SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, %%% BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR %%% SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS %%% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, %%% WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING %%% NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE %%% OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH %%% DAMAGE. %%% %%% @author Michael Truog %%% @copyright 2013-2014 Michael Truog %%% @version 1.3.3 {@date} {@time} %%%------------------------------------------------------------------------ -module(cloudi_http_elli_handler). -author('mjtruog [at] gmail (dot) com'). %-behaviour(elli_handler). %% external interface %% elli_http_handler callbacks -export([handle/2, handle_event/3]). -include_lib("cloudi_core/include/cloudi_logger.hrl"). -include_lib("cloudi_core/include/cloudi_service_children.hrl"). -include("cloudi_http_elli_handler.hrl"). -include_lib("elli/include/elli.hrl"). %%%------------------------------------------------------------------------ %%% External interface functions %%%------------------------------------------------------------------------ %%%------------------------------------------------------------------------ %%% Callback functions from elli_handler %%%------------------------------------------------------------------------ %% Reply with a normal response. 'ok' can be used instead of '200' %% to signal success. handle(Req, #elli_state{dispatcher = Dispatcher, context = Context, output_type = OutputType, default_content_type = DefaultContentType, use_host_prefix = UseHostPrefix, use_method_suffix = UseMethodSuffix}) -> RequestStartMicroSec = ?LOG_WARN_APPLY(fun request_time_start/0, []), Method = elli_request:method(Req), HeadersIncoming0 = elli_request:headers(Req), [PathRaw | _] = binary:split(elli_request:raw_path(Req), [<<"?">>]), HostRaw = case lists:keyfind(<<"Host">>, 1, HeadersIncoming0) of {<<"Host">>, H} -> H; false -> undefined end, QsVals = elli_request:query_str(Req), Body = elli_request:body(Req), NameIncoming = if UseHostPrefix =:= false; HostRaw =:= undefined -> erlang:binary_to_list(PathRaw); true -> erlang:binary_to_list(<>) end, HeadersIncomingN = [{<<"url-path">>, PathRaw} | HeadersIncoming0], NameOutgoing = if UseMethodSuffix =:= false -> NameIncoming; Method =:= 'GET' -> NameIncoming ++ "/get"; Method =:= 'POST' -> NameIncoming ++ "/post"; Method =:= 'PUT' -> NameIncoming ++ "/put"; Method =:= 'DELETE' -> NameIncoming ++ "/delete"; Method =:= 'HEAD' -> NameIncoming ++ "/head"; Method =:= 'TRACE' -> NameIncoming ++ "/trace"; Method =:= 'OPTIONS' -> NameIncoming ++ "/options"%; %Method =:= 'CONNECT' -> % NameIncoming ++ "/connect" end, RequestBinary = if Method =:= 'GET' -> if (OutputType =:= external) orelse (OutputType =:= binary) orelse (OutputType =:= list) -> get_query_string_external(cow_qs:parse_qs(QsVals)); OutputType =:= internal -> cow_qs:parse_qs(QsVals) end; Method =:= 'POST'; Method =:= 'PUT' -> % do not pass type information along with the request! % make sure to encourage good design that provides % one type per name (path) case header_content_type(HeadersIncomingN) of <<"application/zip">> -> zlib:unzip(Body); _ -> Body end; true -> <<>> end, RequestInfo = if (OutputType =:= external) orelse (OutputType =:= binary) -> headers_external_incoming(HeadersIncomingN); (OutputType =:= internal) orelse (OutputType =:= list) -> HeadersIncomingN end, Request = if (OutputType =:= external) orelse (OutputType =:= internal) orelse (OutputType =:= binary) -> RequestBinary; OutputType =:= list -> erlang:binary_to_list(RequestBinary) end, case send_sync_minimal(Dispatcher, Context, NameOutgoing, RequestInfo, Request, self()) of {ok, ResponseInfo, Response} -> HeadersOutgoing = headers_external_outgoing(ResponseInfo), {HttpCode, _, _} = Result = return_response(NameIncoming, HeadersOutgoing, Response, OutputType, DefaultContentType), ?LOG_TRACE_APPLY(fun request_time_end_success/5, [HttpCode, Method, NameIncoming, NameOutgoing, RequestStartMicroSec]), Result; {error, timeout} -> HttpCode = 504, ?LOG_WARN_APPLY(fun request_time_end_error/5, [HttpCode, Method, NameIncoming, RequestStartMicroSec, timeout]), {HttpCode, [], <<>>} end. %% Handle request events, like request completed, exception %% thrown, client timeout, etc. Must return 'ok'. handle_event(request_throw, Error, _Config) -> ?LOG_ERROR("request_throw: ~p", [Error]), ok; handle_event(request_error, Error, _Config) -> ?LOG_ERROR("request_error: ~p", [Error]), ok; handle_event(request_exit, Error, _Config) -> ?LOG_ERROR("request_exit: ~p", [Error]), ok; handle_event(_Event, _Args, _Config) -> ok. %%%------------------------------------------------------------------------ %%% Private functions %%%------------------------------------------------------------------------ header_content_type(Headers) -> case lists:keyfind(<<"Content-Type">>, 1, Headers) of false -> <<>>; {<<"Content-Type">>, Value} -> hd(binary:split(Value, <<",">>)) end. % format for external services, http headers passed as key-value pairs headers_external_incoming(L) -> erlang:iolist_to_binary(lists:reverse(headers_external_incoming([], L))). headers_external_incoming(Result, []) -> Result; headers_external_incoming(Result, [{K, V} | L]) when is_binary(K) -> headers_external_incoming([[K, 0, V, 0] | Result], L). headers_external_outgoing(<<>>) -> []; headers_external_outgoing([] = ResponseInfo) -> ResponseInfo; headers_external_outgoing([{_, _} | _] = ResponseInfo) -> ResponseInfo; headers_external_outgoing(ResponseInfo) when is_binary(ResponseInfo) -> headers_external_outgoing(binary:split(ResponseInfo, <<0>>, [global]), []). headers_external_outgoing([<<>>], Result) -> lists:reverse(Result); headers_external_outgoing([K, V | L], Result) -> headers_external_outgoing(L, [{K, V} | Result]). get_query_string_external([]) -> <<>>; get_query_string_external(QsVals) -> erlang:iolist_to_binary(lists:reverse(lists:foldr(fun({K, V}, L) -> if V =:= true -> [[K, 0, <<"true">>, 0] | L]; V =:= false -> [[K, 0, <<"false">>, 0] | L]; true -> [[K, 0, V, 0] | L] end end, [], QsVals))). request_time_start() -> uuid:get_v1_time(os). request_time_end_success(HttpCode, Method, NameIncoming, NameOutgoing, RequestStartMicroSec) -> ?LOG_TRACE("~w ~s ~s (to ~s) ~p ms", [HttpCode, Method, NameIncoming, NameOutgoing, (uuid:get_v1_time(os) - RequestStartMicroSec) / 1000.0]). request_time_end_error(HttpCode, Method, NameIncoming, RequestStartMicroSec, Reason) -> ?LOG_WARN("~w ~s ~s ~p ms: ~p", [HttpCode, Method, NameIncoming, (uuid:get_v1_time(os) - RequestStartMicroSec) / 1000.0, Reason]). return_response(NameIncoming, HeadersOutgoing, Response, OutputType, DefaultContentType) -> ResponseBinary = if (((OutputType =:= external) orelse (OutputType =:= internal)) andalso (is_binary(Response) orelse is_list(Response))) orelse ((OutputType =:= binary) andalso is_binary(Response)) -> Response; (OutputType =:= list) andalso is_list(Response) -> erlang:list_to_binary(Response) end, FileName = cloudi_string:afterr($/, NameIncoming, input), ResponseHeadersOutgoing = if HeadersOutgoing =/= [] -> HeadersOutgoing; DefaultContentType =/= undefined -> [{<<"content-type">>, DefaultContentType}]; true -> Extension = filename:extension(FileName), if Extension == [] -> [{<<"content-type">>, <<"text/html">>}]; true -> case cloudi_response_info: lookup_content_type(binary, Extension) of error -> [{<<"content-disposition">>, erlang:iolist_to_binary([ "attachment; filename=\"", filename:basename(NameIncoming), "\""])}, {<<"content-type">>, <<"application/octet-stream">>}]; {ok, {request, ContentType}} -> [{<<"content-type">>, ContentType}]; {ok, {attachment, ContentType}} -> [{<<"content-disposition">>, erlang:iolist_to_binary([ "attachment; filename=\"", filename:basename(NameIncoming), "\""])}, {<<"content-type">>, ContentType}] end end end, HttpCode = 200, {HttpCode, ResponseHeadersOutgoing, ResponseBinary}.