| % Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| % use this file except in compliance with the License. You may obtain a copy of |
| % the License at |
| % |
| % http://www.apache.org/licenses/LICENSE-2.0 |
| % |
| % Unless required by applicable law or agreed to in writing, software |
| % distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| % WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| % License for the specific language governing permissions and limitations under |
| % the License. |
| |
| -module(couch_httpd_external). |
| |
| -export([handle_external_req/2, handle_external_req/3]). |
| -export([send_external_response/2, json_req_obj/2]). |
| -export([default_or_content_type/2, parse_external_response/1]). |
| |
| -import(couch_httpd,[send_error/4]). |
| |
| -include("couch_db.hrl"). |
| |
| % handle_external_req/2 |
| % for the old type of config usage: |
| % _external = {couch_httpd_external, handle_external_req} |
| % with urls like |
| % /db/_external/action/design/name |
| handle_external_req(#httpd{ |
| path_parts=[_DbName, _External, UrlName | _Path] |
| }=HttpReq, Db) -> |
| process_external_req(HttpReq, Db, UrlName); |
| handle_external_req(#httpd{path_parts=[_, _]}=Req, _Db) -> |
| send_error(Req, 404, <<"external_server_error">>, <<"No server name specified.">>); |
| handle_external_req(Req, _) -> |
| send_error(Req, 404, <<"external_server_error">>, <<"Broken assumption">>). |
| |
| % handle_external_req/3 |
| % for this type of config usage: |
| % _action = {couch_httpd_external, handle_external_req, <<"action">>} |
| % with urls like |
| % /db/_action/design/name |
| handle_external_req(HttpReq, Db, Name) -> |
| process_external_req(HttpReq, Db, Name). |
| |
| process_external_req(HttpReq, Db, Name) -> |
| |
| Response = couch_external_manager:execute(binary_to_list(Name), |
| json_req_obj(HttpReq, Db)), |
| |
| case Response of |
| {unknown_external_server, Msg} -> |
| send_error(HttpReq, 404, <<"external_server_error">>, Msg); |
| _ -> |
| send_external_response(HttpReq, Response) |
| end. |
| |
| json_req_obj(#httpd{mochi_req=Req, |
| method=Verb, |
| path_parts=Path, |
| req_body=ReqBody |
| }, Db) -> |
| Body = case ReqBody of |
| undefined -> Req:recv_body(); |
| Else -> Else |
| end, |
| ParsedForm = case Req:get_primary_header_value("content-type") of |
| "application/x-www-form-urlencoded" ++ _ -> |
| mochiweb_util:parse_qs(Body); |
| _ -> |
| [] |
| end, |
| Headers = Req:get(headers), |
| Hlist = mochiweb_headers:to_list(Headers), |
| {ok, Info} = couch_db:get_db_info(Db), |
| % add headers... |
| {[{<<"info">>, {Info}}, |
| {<<"verb">>, Verb}, |
| {<<"path">>, Path}, |
| {<<"query">>, to_json_terms(Req:parse_qs())}, |
| {<<"headers">>, to_json_terms(Hlist)}, |
| {<<"body">>, Body}, |
| {<<"form">>, to_json_terms(ParsedForm)}, |
| {<<"cookie">>, to_json_terms(Req:parse_cookie())}, |
| {<<"userCtx">>, couch_util:json_user_ctx(Db)}]}. |
| |
| to_json_terms(Data) -> |
| to_json_terms(Data, []). |
| to_json_terms([], Acc) -> |
| {lists:reverse(Acc)}; |
| to_json_terms([{Key, Value} | Rest], Acc) when is_atom(Key) -> |
| to_json_terms(Rest, [{list_to_binary(atom_to_list(Key)), list_to_binary(Value)} | Acc]); |
| to_json_terms([{Key, Value} | Rest], Acc) -> |
| to_json_terms(Rest, [{list_to_binary(Key), list_to_binary(Value)} | Acc]). |
| |
| |
| send_external_response(#httpd{mochi_req=MochiReq}, Response) -> |
| #extern_resp_args{ |
| code = Code, |
| data = Data, |
| ctype = CType, |
| headers = Headers |
| } = parse_external_response(Response), |
| Resp = MochiReq:respond({Code, |
| default_or_content_type(CType, Headers ++ couch_httpd:server_header()), Data}), |
| {ok, Resp}. |
| |
| parse_external_response({Response}) -> |
| lists:foldl(fun({Key,Value}, Args) -> |
| case {Key, Value} of |
| {"", _} -> |
| Args; |
| {<<"code">>, Value} -> |
| Args#extern_resp_args{code=Value}; |
| {<<"stop">>, true} -> |
| Args#extern_resp_args{stop=true}; |
| {<<"json">>, Value} -> |
| Args#extern_resp_args{ |
| data=?JSON_ENCODE(Value), |
| ctype="application/json"}; |
| {<<"body">>, Value} -> |
| Args#extern_resp_args{data=Value, ctype="text/html; charset=utf-8"}; |
| {<<"base64">>, Value} -> |
| Args#extern_resp_args{ |
| data=couch_util:decodeBase64(Value), |
| ctype="application/binary" |
| }; |
| {<<"headers">>, {Headers}} -> |
| NewHeaders = lists:map(fun({Header, HVal}) -> |
| {binary_to_list(Header), binary_to_list(HVal)} |
| end, Headers), |
| Args#extern_resp_args{headers=NewHeaders}; |
| _ -> % unknown key |
| Msg = lists:flatten(io_lib:format("Invalid data from external server: ~p", [{Key, Value}])), |
| throw({external_response_error, Msg}) |
| end |
| end, #extern_resp_args{}, Response). |
| |
| default_or_content_type(DefaultContentType, Headers) -> |
| {ContentType, OtherHeaders} = lists:partition( |
| fun({HeaderName, _}) -> |
| HeaderName == "Content-Type" |
| end, Headers), |
| |
| % XXX: What happens if we were passed multiple content types? We add another? |
| case ContentType of |
| [{"Content-Type", SetContentType}] -> |
| TrueContentType = SetContentType; |
| _Else -> |
| TrueContentType = DefaultContentType |
| end, |
| |
| HeadersWithContentType = lists:append(OtherHeaders, [{"Content-Type", TrueContentType}]), |
| HeadersWithContentType. |