summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--etc/couchdb/default.ini.tpl.in6
-rw-r--r--src/couchdb/Makefile.am6
-rw-r--r--src/couchdb/couch_external_manager.erl97
-rw-r--r--src/couchdb/couch_external_server.erl80
-rw-r--r--src/couchdb/couch_httpd_external.erl107
5 files changed, 295 insertions, 1 deletions
diff --git a/etc/couchdb/default.ini.tpl.in b/etc/couchdb/default.ini.tpl.in
index 65f6de9d..863ec1f7 100644
--- a/etc/couchdb/default.ini.tpl.in
+++ b/etc/couchdb/default.ini.tpl.in
@@ -21,13 +21,16 @@ level = info
[query_servers]
javascript = %bindir%/%couchjs_command_name% %localdatadir%/server/main.js
+[external]
+action = %bindir%/%couchjs_command_name% %localdatadir%/server/action.js
+
[daemons]
view_manager={couch_view, start_link, []}
+external_manager={couch_external_manager, start_link, []}
db_update_notifier={couch_db_update_notifier_sup, start_link, []}
query_servers={couch_query_servers, start_link, []}
httpd={couch_httpd, start_link, []}
-
[httpd_global_handlers]
/ = {couch_httpd_misc_handlers, handle_welcome_req, <<"Welcome">>}
favicon.ico = {couch_httpd_misc_handlers, handle_favicon_req, "%localdatadir%/www"}
@@ -42,3 +45,4 @@ _restart = {couch_httpd_misc_handlers, handle_restart_req}
[httpd_db_handlers]
_view = {couch_httpd_view, handle_view_req}
_temp_view = {couch_httpd_view, handle_temp_view_req}
+_external = {couch_httpd_external, handle_external_req}
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.