diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/couchdb/Makefile.am | 6 | ||||
-rw-r--r-- | src/couchdb/couch_external_manager.erl | 97 | ||||
-rw-r--r-- | src/couchdb/couch_external_server.erl | 80 | ||||
-rw-r--r-- | src/couchdb/couch_httpd_external.erl | 107 |
4 files changed, 290 insertions, 0 deletions
diff --git a/src/couchdb/Makefile.am b/src/couchdb/Makefile.am index 76d3f13e..aaa6c67c 100644 --- a/src/couchdb/Makefile.am +++ b/src/couchdb/Makefile.am @@ -48,9 +48,12 @@ source_files = \ couch_db_update_notifier_sup.erl \ couch_doc.erl \ couch_event_sup.erl \ + couch_external_manager.erl \ + couch_external_server.erl \ couch_file.erl \ couch_httpd.erl \ couch_httpd_db.erl \ + couch_httpd_external.erl \ couch_httpd_view.erl \ couch_httpd_misc_handlers.erl \ couch_key_tree.erl \ @@ -79,9 +82,12 @@ compiled_files = \ couch_db_update_notifier_sup.beam \ couch_doc.beam \ couch_event_sup.beam \ + couch_external_manager.beam \ + couch_external_server.beam \ couch_file.beam \ couch_httpd.beam \ couch_httpd_db.beam \ + couch_httpd_external.beam \ couch_httpd_view.beam \ couch_httpd_misc_handlers.beam \ couch_key_tree.beam \ diff --git a/src/couchdb/couch_external_manager.erl b/src/couchdb/couch_external_manager.erl new file mode 100644 index 00000000..d86013eb --- /dev/null +++ b/src/couchdb/couch_external_manager.erl @@ -0,0 +1,97 @@ +% 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_external_manager). +-behaviour(gen_server). + +-export([start_link/0, execute/8, config_change/2]). +-export([init/1, terminate/2, code_change/3, handle_call/3, handle_cast/2, handle_info/2]). + +-include("couch_db.hrl"). + +start_link() -> + gen_server:start_link({local, couch_external_manager}, couch_external_manager, [], []). + +execute(UrlName, Db, Verb, Path, Query, Body, Post, Cookie) -> + Pid = gen_server:call(couch_external_manager, {get, UrlName}), + case Pid of + {error, Reason} -> + Reason; + _ -> + couch_external_server:execute(Pid, Db, Verb, Path, Query, Body, Post, Cookie) + end. + +config_change("external", UrlName) -> + gen_server:call(couch_external_manager, {config, UrlName}). + +% gen_server API + +init([]) -> + Handlers = ets:new(couch_external_manager_handlers, [set, private]), + lists:foreach(fun({UrlName, Command}) -> + {ok, Pid} = couch_external_server:start_link(UrlName, Command), + true = ets:insert(Handlers, {UrlName, Pid}) + end, couch_config:get("external")), + couch_config:register(fun config_change/2), + {ok, Handlers}. + +terminate(_Reason, Handlers) -> + ets:foldl(fun({_UrlName, Pid}, nil) -> + couch_external_server:stop(Pid), + nil + end, nil, Handlers), + ok. + +handle_call({get, UrlName}, _From, Handlers) -> + Resp = case ets:lookup(Handlers, UrlName) of + [{UrlName, Pid}] -> + Pid; + [] -> + Mesg = lists:flatten(io_lib:format("No server configured for ~p.", [UrlName])), + {error, {unknown_external_server, Mesg}} + end, + {reply, Resp, Handlers}; +handle_call({config, UrlName}, _From, Handlers) -> + % A newly added handler and a handler that had it's command + % changed are treated exactly the same. + + % Shutdown the old handler. + case ets:lookup(Handlers, UrlName) of + [{UrlName, Pid}] -> + couch_external_server:stop(Pid); + _ -> + ok + end, + case couch_config:get("external", UrlName, nil) of + % Handler no longer exists + nil -> + ok; + % New handler start up. + Command -> + {ok, NewPid} = couch_external_server:start_link(UrlName, Command), + true = ets:insert(Handlers, {Command, NewPid}) + end, + {reply, ok, Handlers}. + +handle_cast(_Whatever, State) -> + {noreply, State}. + +handle_info({'EXIT', Reason, Pid}, Handlers) -> + ?LOG_DEBUG("External server ~p died. (reason: ~p)", [Pid, Reason]), + % Remove Pid from the handlers table so we don't try closing + % it a second time in terminate/2. + ets:match_delete(Handlers, {'_', Pid}), + {stop, Handlers}. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + diff --git a/src/couchdb/couch_external_server.erl b/src/couchdb/couch_external_server.erl new file mode 100644 index 00000000..afbb729e --- /dev/null +++ b/src/couchdb/couch_external_server.erl @@ -0,0 +1,80 @@ +% 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_external_server). +-behaviour(gen_server). + +-export([start_link/2, stop/1, execute/8]). +-export([init/1, terminate/2, handle_call/3, handle_cast/2, handle_info/2, code_change/3]). + +-define(TIMEOUT, 5000). + +-include("couch_db.hrl"). + +% External API + +start_link(Name, Command) -> + gen_server:start_link(couch_external_server, [Name, Command], []). + +stop(Pid) -> + gen_server:cast(Pid, stop). + +execute(Pid, Db, Verb, Path, Query, Body, Post, Cookie) -> + gen_server:call(Pid, {execute, Db, Verb, Path, Query, Body, Post, Cookie}). + +% Gen Server Handlers + +init([Name, Command]) -> + ?LOG_INFO("Starting process for: ~s", [Name]), + {ok, Pid} = couch_os_process:start_link(Command), + {ok, {Name, Command, Pid}}. + +terminate(_Reason, {Name, _Command, Pid}) -> + ?LOG_INFO("External Process Terminating: ~p: ~p", [Name, Pid]), + couch_os_process:stop(Pid), + ok. + +handle_call({execute, Db, Verb, Path, Query, Body, Post, Cookie}, _From, {Name, Command, Pid}) -> + ?LOG_DEBUG("Query Params ~p",[Query]), + {ok, Info} = couch_db:get_db_info(Db), + Json = {[ + {<<"info">>, {Info}}, + {<<"verb">>, Verb}, + {<<"path">>, Path}, + {<<"query">>, to_json_terms(Query)}, + {<<"body">>, Body}, + {<<"form">>, to_json_terms(Post)}, + {<<"cookie">>, to_json_terms(Cookie)}]}, + {reply, couch_os_process:prompt(Pid, Json), {Name, Command, Pid}}. + +handle_info({'EXIT', Pid, Reason}, {Name, Command, Pid}) -> + ?LOG_INFO("EXTERNAL: Restarting process for ~s (reason: ~w)", [Name, Reason]), + {ok, Pid} = couch_os_process:start_link(Command), + {noreply, {Name, Command, Pid}}. + +handle_cast(stop, State) -> + {stop, normal, State}; +handle_cast(_Whatever, State) -> + {noreply, State}. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +% Internal API + +to_json_terms(Data) -> + to_json_terms(Data, []). +to_json_terms([], Acc) -> + {lists:reverse(Acc)}; +to_json_terms([{Key, Value} | Rest], Acc) -> + to_json_terms(Rest, [{list_to_binary(Key), list_to_binary(Value)} | Acc]). + diff --git a/src/couchdb/couch_httpd_external.erl b/src/couchdb/couch_httpd_external.erl new file mode 100644 index 00000000..7ed3d51a --- /dev/null +++ b/src/couchdb/couch_httpd_external.erl @@ -0,0 +1,107 @@ +% 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]). + +-import(couch_httpd,[send_error/4]). + +-include("couch_db.hrl"). + +-record(extern_resp_args, { + code = 200, + data = <<>>, + ctype = "application/json", + headers = [] +}). + +handle_external_req(#httpd{mochi_req=Req, + method=Verb, + path_parts=[_DbName, _External, UrlName | Path] + }=HttpReq, Db) -> + ReqBody = Req:recv_body(), + ParsedForm = case Req:get_primary_header_value("content-type") of + "application/x-www-form-urlencoded" ++ _ -> + mochiweb_util:parse_qs(ReqBody); + _ -> + [] + end, + Response = couch_external_manager:execute(binary_to_list(UrlName), + Db, Verb, Path, Req:parse_qs(), ReqBody, ParsedForm, + Req:parse_cookie()), + + case Response of + {unknown_external_server, Msg} -> + send_error(HttpReq, 404, <<"external_server_error">>, Msg); + _ -> + send_external_response(Req, Response) + end; +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">>). + +send_external_response(Req, Response) -> + #extern_resp_args{ + code = Code, + data = Data, + ctype = CType, + headers = Headers + } = parse_external_response(Response), + ?LOG_DEBUG("External Response ~p",[Response]), + Resp = Req:respond({Code, + default_or_content_type(CType, Headers), chunked}), + Resp:write_chunk(Data), + Resp:write_chunk(""), + {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}; + {<<"json">>, Value} -> + Args#extern_resp_args{ + data=?JSON_ENCODE(Value), + ctype="application/json"}; + {<<"body">>, Value} -> + Args#extern_resp_args{data=Value, ctype="text/html"}; + {<<"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: ~s = ~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. |