1% Licensed under the Apache License, Version 2.0 (the "License"); you may not
2% use this file except in compliance with the License. You may obtain a copy of
3% the License at
4%
5%   http://www.apache.org/licenses/LICENSE-2.0
6%
7% Unless required by applicable law or agreed to in writing, software
8% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10% License for the specific language governing permissions and limitations under
11% the License.
12
13-module(chttpd_misc).
14
15-export([
16    handle_all_dbs_req/1,
17    handle_dbs_info_req/1,
18    handle_favicon_req/1,
19    handle_favicon_req/2,
20    handle_replicate_req/1,
21    handle_reload_query_servers_req/1,
22    handle_task_status_req/1,
23    handle_up_req/1,
24    handle_utils_dir_req/1,
25    handle_utils_dir_req/2,
26    handle_uuids_req/1,
27    handle_welcome_req/1,
28    handle_welcome_req/2
29]).
30
31-include_lib("couch/include/couch_db.hrl").
32-include_lib("couch_mrview/include/couch_mrview.hrl").
33
34-import(chttpd,
35    [send_json/2,send_json/3,send_method_not_allowed/2,
36    send_chunk/2,start_chunked_response/3]).
37
38-define(MAX_DB_NUM_FOR_DBS_INFO, 100).
39
40% httpd global handlers
41
42handle_welcome_req(Req) ->
43    handle_welcome_req(Req, <<"Welcome">>).
44
45handle_welcome_req(#httpd{method='GET'}=Req, WelcomeMessage) ->
46    send_json(Req, {[
47        {couchdb, WelcomeMessage},
48        {version, list_to_binary(couch_server:get_version())},
49        {git_sha, list_to_binary(couch_server:get_git_sha())},
50        {uuid, couch_server:get_uuid()},
51        {features, get_features()}
52        ] ++ case config:get("vendor") of
53        [] ->
54            [];
55        Properties ->
56            [{vendor, {[{?l2b(K), ?l2b(V)} || {K, V} <- Properties]}}]
57        end
58    });
59handle_welcome_req(Req, _) ->
60    send_method_not_allowed(Req, "GET,HEAD").
61
62get_features() ->
63    case clouseau_rpc:connected() of
64        true ->
65            [search | config:features()];
66        false ->
67            config:features()
68    end.
69
70handle_favicon_req(Req) ->
71    handle_favicon_req(Req, get_docroot()).
72
73handle_favicon_req(#httpd{method='GET'}=Req, DocumentRoot) ->
74    {DateNow, TimeNow} = calendar:universal_time(),
75    DaysNow = calendar:date_to_gregorian_days(DateNow),
76    DaysWhenExpires = DaysNow + 365,
77    DateWhenExpires = calendar:gregorian_days_to_date(DaysWhenExpires),
78    CachingHeaders = [
79        %favicon should expire a year from now
80        {"Cache-Control", "public, max-age=31536000"},
81        {"Expires", couch_util:rfc1123_date({DateWhenExpires, TimeNow})}
82    ],
83    chttpd:serve_file(Req, "favicon.ico", DocumentRoot, CachingHeaders);
84handle_favicon_req(Req, _) ->
85    send_method_not_allowed(Req, "GET,HEAD").
86
87handle_utils_dir_req(Req) ->
88    handle_utils_dir_req(Req, get_docroot()).
89
90handle_utils_dir_req(#httpd{method='GET'}=Req, DocumentRoot) ->
91    "/" ++ UrlPath = chttpd:path(Req),
92    case chttpd:partition(UrlPath) of
93    {_ActionKey, "/", RelativePath} ->
94        % GET /_utils/path or GET /_utils/
95        CachingHeaders = [{"Cache-Control", "private, must-revalidate"}],
96        DefaultValues = "child-src 'self' data: blob:; default-src 'self'; img-src 'self' data:; font-src 'self'; "
97            "script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline';",
98        Headers = chttpd_util:maybe_add_csp_header("utils", CachingHeaders, DefaultValues),
99        chttpd:serve_file(Req, RelativePath, DocumentRoot, Headers);
100    {_ActionKey, "", _RelativePath} ->
101        % GET /_utils
102        RedirectPath = chttpd:path(Req) ++ "/",
103        chttpd:send_redirect(Req, RedirectPath)
104    end;
105handle_utils_dir_req(Req, _) ->
106    send_method_not_allowed(Req, "GET,HEAD").
107
108handle_all_dbs_req(#httpd{method='GET'}=Req) ->
109    Args = couch_mrview_http:parse_params(Req, undefined),
110    ShardDbName = config:get("mem3", "shards_db", "_dbs"),
111    %% shard_db is not sharded but mem3:shards treats it as an edge case
112    %% so it can be pushed thru fabric
113    {ok, Info} = fabric:get_db_info(ShardDbName),
114    Etag = couch_httpd:make_etag({Info}),
115    Options = [{user_ctx, Req#httpd.user_ctx}],
116    {ok, Resp} = chttpd:etag_respond(Req, Etag, fun() ->
117        {ok, Resp} = chttpd:start_delayed_json_response(Req, 200, [{"ETag",Etag}]),
118        VAcc = #vacc{req=Req,resp=Resp},
119        fabric:all_docs(ShardDbName, Options, fun all_dbs_callback/2, VAcc, Args)
120    end),
121    case is_record(Resp, vacc) of
122        true -> {ok, Resp#vacc.resp};
123        _ -> {ok, Resp}
124    end;
125handle_all_dbs_req(Req) ->
126    send_method_not_allowed(Req, "GET,HEAD").
127
128all_dbs_callback({meta, _Meta}, #vacc{resp=Resp0}=Acc) ->
129    {ok, Resp1} = chttpd:send_delayed_chunk(Resp0, "["),
130    {ok, Acc#vacc{resp=Resp1}};
131all_dbs_callback({row, Row}, #vacc{resp=Resp0}=Acc) ->
132    Prepend = couch_mrview_http:prepend_val(Acc),
133    case couch_util:get_value(id, Row) of <<"_design", _/binary>> ->
134        {ok, Acc};
135    DbName ->
136        {ok, Resp1} = chttpd:send_delayed_chunk(Resp0, [Prepend, ?JSON_ENCODE(DbName)]),
137        {ok, Acc#vacc{prepend=",", resp=Resp1}}
138    end;
139all_dbs_callback(complete, #vacc{resp=Resp0}=Acc) ->
140    {ok, Resp1} = chttpd:send_delayed_chunk(Resp0, "]"),
141    {ok, Resp2} = chttpd:end_delayed_json_response(Resp1),
142    {ok, Acc#vacc{resp=Resp2}};
143all_dbs_callback({error, Reason}, #vacc{resp=Resp0}=Acc) ->
144    {ok, Resp1} = chttpd:send_delayed_error(Resp0, Reason),
145    {ok, Acc#vacc{resp=Resp1}}.
146
147handle_dbs_info_req(#httpd{method='POST'}=Req) ->
148    chttpd:validate_ctype(Req, "application/json"),
149    Props = chttpd:json_body_obj(Req),
150    Keys = couch_mrview_util:get_view_keys(Props),
151    case Keys of
152        undefined -> throw({bad_request, "`keys` member must exist."});
153        _ -> ok
154    end,
155    MaxNumber = config:get_integer("chttpd",
156        "max_db_number_for_dbs_info_req", ?MAX_DB_NUM_FOR_DBS_INFO),
157    case length(Keys) =< MaxNumber of
158        true -> ok;
159        false -> throw({bad_request, too_many_keys})
160    end,
161    {ok, Resp} = chttpd:start_json_response(Req, 200),
162    send_chunk(Resp, "["),
163    lists:foldl(fun(DbName, AccSeparator) ->
164        case catch fabric:get_db_info(DbName) of
165            {ok, Result} ->
166                Json = ?JSON_ENCODE({[{key, DbName}, {info, {Result}}]}),
167                send_chunk(Resp, AccSeparator ++ Json);
168            _ ->
169                Json = ?JSON_ENCODE({[{key, DbName}, {error, not_found}]}),
170                send_chunk(Resp, AccSeparator ++ Json)
171        end,
172        "," % AccSeparator now has a comma
173    end, "", Keys),
174    send_chunk(Resp, "]"),
175    chttpd:end_json_response(Resp);
176handle_dbs_info_req(Req) ->
177    send_method_not_allowed(Req, "POST").
178
179handle_task_status_req(#httpd{method='GET'}=Req) ->
180    ok = chttpd:verify_is_server_admin(Req),
181    {Replies, _BadNodes} = gen_server:multi_call(couch_task_status, all),
182    Response = lists:flatmap(fun({Node, Tasks}) ->
183        [{[{node,Node} | Task]} || Task <- Tasks]
184    end, Replies),
185    send_json(Req, lists:sort(Response));
186handle_task_status_req(Req) ->
187    send_method_not_allowed(Req, "GET,HEAD").
188
189handle_replicate_req(#httpd{method='POST', user_ctx=Ctx, req_body=PostBody} = Req) ->
190    chttpd:validate_ctype(Req, "application/json"),
191    %% see HACK in chttpd.erl about replication
192    case replicate(PostBody, Ctx) of
193        {ok, {continuous, RepId}} ->
194            send_json(Req, 202, {[{ok, true}, {<<"_local_id">>, RepId}]});
195        {ok, {cancelled, RepId}} ->
196            send_json(Req, 200, {[{ok, true}, {<<"_local_id">>, RepId}]});
197        {ok, {JsonResults}} ->
198            send_json(Req, {[{ok, true} | JsonResults]});
199        {ok, stopped} ->
200            send_json(Req, 200, {[{ok, stopped}]});
201        {error, not_found=Error} ->
202            chttpd:send_error(Req, Error);
203        {error, {_, _}=Error} ->
204            chttpd:send_error(Req, Error);
205        {_, _}=Error ->
206            chttpd:send_error(Req, Error)
207    end;
208handle_replicate_req(Req) ->
209    send_method_not_allowed(Req, "POST").
210
211replicate({Props} = PostBody, Ctx) ->
212    case couch_util:get_value(<<"cancel">>, Props) of
213    true ->
214        cancel_replication(PostBody, Ctx);
215    _ ->
216        Node = choose_node([
217            couch_util:get_value(<<"source">>, Props),
218            couch_util:get_value(<<"target">>, Props)
219        ]),
220        case rpc:call(Node, couch_replicator, replicate, [PostBody, Ctx]) of
221        {badrpc, Reason} ->
222            erlang:error(Reason);
223        Res ->
224            Res
225        end
226    end.
227
228cancel_replication(PostBody, Ctx) ->
229    {Res, _Bad} = rpc:multicall(couch_replicator, replicate, [PostBody, Ctx]),
230    case [X || {ok, {cancelled, _}} = X <- Res] of
231    [Success|_] ->
232        % Report success if at least one node canceled the replication
233        Success;
234    [] ->
235        case lists:usort(Res) of
236        [UniqueReply] ->
237            % Report a universally agreed-upon reply
238            UniqueReply;
239        [] ->
240            {error, badrpc};
241        Else ->
242            % Unclear what to do here -- pick the first error?
243            % Except try ignoring any {error, not_found} responses
244            % because we'll always get two of those
245            hd(Else -- [{error, not_found}])
246        end
247    end.
248
249choose_node(Key) when is_binary(Key) ->
250    Checksum = erlang:crc32(Key),
251    Nodes = lists:sort([node()|erlang:nodes()]),
252    lists:nth(1 + Checksum rem length(Nodes), Nodes);
253choose_node(Key) ->
254    choose_node(term_to_binary(Key)).
255
256handle_reload_query_servers_req(#httpd{method='POST'}=Req) ->
257    chttpd:validate_ctype(Req, "application/json"),
258    ok = couch_proc_manager:reload(),
259    send_json(Req, 200, {[{ok, true}]});
260handle_reload_query_servers_req(Req) ->
261    send_method_not_allowed(Req, "POST").
262
263handle_uuids_req(Req) ->
264    couch_httpd_misc_handlers:handle_uuids_req(Req).
265
266
267handle_up_req(#httpd{method='GET'} = Req) ->
268    case config:get("couchdb", "maintenance_mode") of
269    "true" ->
270        send_json(Req, 404, {[{status, maintenance_mode}]});
271    "nolb" ->
272        send_json(Req, 404, {[{status, nolb}]});
273    _ ->
274        {ok, {Status}} = mem3_seeds:get_status(),
275        case couch_util:get_value(status, Status) of
276            ok ->
277                send_json(Req, 200, {Status});
278            seeding ->
279                send_json(Req, 404, {Status})
280        end
281    end;
282
283handle_up_req(Req) ->
284    send_method_not_allowed(Req, "GET,HEAD").
285
286get_docroot() ->
287    % if the env var isn’t set, let’s not throw an error, but
288    % assume the current working dir is what we want
289    os:getenv("COUCHDB_FAUXTON_DOCROOT", "").
290