blob: f8e47a2cb8eeb4f441f022ecb199fe1f409cd83a [file] [log] [blame]
% 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(chttpd_misc).
-export([
handle_all_dbs_req/1,
handle_dbs_info_req/1,
handle_deleted_dbs_req/1,
handle_favicon_req/1,
handle_favicon_req/2,
handle_replicate_req/1,
handle_reload_query_servers_req/1,
handle_task_status_req/1,
handle_up_req/1,
handle_utils_dir_req/1,
handle_utils_dir_req/2,
handle_uuids_req/1,
handle_welcome_req/1,
handle_welcome_req/2
]).
-include_lib("couch/include/couch_db.hrl").
-include_lib("couch_views/include/couch_views.hrl").
-import(chttpd,
[send_json/2,send_json/3,send_json/4,send_method_not_allowed/2,
send_chunk/2,start_chunked_response/3]).
-define(MAX_DB_NUM_FOR_DBS_INFO, 100).
% httpd global handlers
handle_welcome_req(Req) ->
handle_welcome_req(Req, <<"Welcome">>).
handle_welcome_req(#httpd{method='GET'}=Req, WelcomeMessage) ->
send_json(Req, {[
{couchdb, WelcomeMessage},
{version, list_to_binary(couch_server:get_version())},
{git_sha, list_to_binary(couch_server:get_git_sha())},
{uuid, couch_server:get_uuid()},
{features, get_features()}
] ++ case config:get("vendor") of
[] ->
[];
Properties ->
[{vendor, {[{?l2b(K), ?l2b(V)} || {K, V} <- Properties]}}]
end
});
handle_welcome_req(Req, _) ->
send_method_not_allowed(Req, "GET,HEAD").
get_features() ->
config:features().
handle_favicon_req(Req) ->
handle_favicon_req(Req, get_docroot()).
handle_favicon_req(#httpd{method='GET'}=Req, DocumentRoot) ->
{DateNow, TimeNow} = calendar:universal_time(),
DaysNow = calendar:date_to_gregorian_days(DateNow),
DaysWhenExpires = DaysNow + 365,
DateWhenExpires = calendar:gregorian_days_to_date(DaysWhenExpires),
CachingHeaders = [
%favicon should expire a year from now
{"Cache-Control", "public, max-age=31536000"},
{"Expires", couch_util:rfc1123_date({DateWhenExpires, TimeNow})}
],
chttpd:serve_file(Req, "favicon.ico", DocumentRoot, CachingHeaders);
handle_favicon_req(Req, _) ->
send_method_not_allowed(Req, "GET,HEAD").
handle_utils_dir_req(Req) ->
handle_utils_dir_req(Req, get_docroot()).
handle_utils_dir_req(#httpd{method='GET'}=Req, DocumentRoot) ->
"/" ++ UrlPath = chttpd:path(Req),
case chttpd:partition(UrlPath) of
{_ActionKey, "/", RelativePath} ->
% GET /_utils/path or GET /_utils/
CachingHeaders = [{"Cache-Control", "private, must-revalidate"}],
EnableCsp = config:get("csp", "enable", "true"),
Headers = maybe_add_csp_headers(CachingHeaders, EnableCsp),
chttpd:serve_file(Req, RelativePath, DocumentRoot, Headers);
{_ActionKey, "", _RelativePath} ->
% GET /_utils
RedirectPath = chttpd:path(Req) ++ "/",
chttpd:send_redirect(Req, RedirectPath)
end;
handle_utils_dir_req(Req, _) ->
send_method_not_allowed(Req, "GET,HEAD").
maybe_add_csp_headers(Headers, "true") ->
DefaultValues = "child-src 'self' data: blob:; default-src 'self'; img-src 'self' data:; font-src 'self'; "
"script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline';",
Value = config:get("csp", "header_value", DefaultValues),
[{"Content-Security-Policy", Value} | Headers];
maybe_add_csp_headers(Headers, _) ->
Headers.
handle_all_dbs_req(#httpd{method='GET'}=Req) ->
#mrargs{
start_key = StartKey,
end_key = EndKey,
direction = Dir,
limit = Limit,
skip = Skip
} = couch_views_http_util:parse_params(Req, undefined),
Options = [
{start_key, StartKey},
{end_key, EndKey},
{dir, Dir},
{limit, Limit},
{skip, Skip}
],
{ok, Resp} = chttpd:start_delayed_json_response(Req, 200, []),
Callback = fun all_dbs_callback/2,
Acc = #vacc{req=Req,resp=Resp},
{ok, Acc1} = fabric2_db:list_dbs(Callback, Acc, Options),
{ok, Acc1#vacc.resp};
handle_all_dbs_req(Req) ->
send_method_not_allowed(Req, "GET,HEAD").
all_dbs_callback({meta, _Meta}, #vacc{resp=Resp0}=Acc) ->
{ok, Resp1} = chttpd:send_delayed_chunk(Resp0, "["),
{ok, Acc#vacc{resp=Resp1}};
all_dbs_callback({row, Row}, #vacc{resp=Resp0}=Acc) ->
Prepend = couch_views_http_util:prepend_val(Acc),
DbName = couch_util:get_value(id, Row),
{ok, Resp1} = chttpd:send_delayed_chunk(Resp0, [Prepend, ?JSON_ENCODE(DbName)]),
{ok, Acc#vacc{prepend=",", resp=Resp1}};
all_dbs_callback(complete, #vacc{resp=Resp0}=Acc) ->
{ok, Resp1} = chttpd:send_delayed_chunk(Resp0, "]"),
{ok, Resp2} = chttpd:end_delayed_json_response(Resp1),
{ok, Acc#vacc{resp=Resp2}};
all_dbs_callback({error, Reason}, #vacc{resp=Resp0}=Acc) ->
{ok, Resp1} = chttpd:send_delayed_error(Resp0, Reason),
{ok, Acc#vacc{resp=Resp1}}.
handle_dbs_info_req(#httpd{method = 'GET'} = Req) ->
ok = chttpd:verify_is_server_admin(Req),
send_db_infos(Req, list_dbs_info);
handle_dbs_info_req(#httpd{method='POST', user_ctx=UserCtx}=Req) ->
chttpd:validate_ctype(Req, "application/json"),
Props = chttpd:json_body_obj(Req),
Keys = couch_views_util:get_view_keys(Props),
case Keys of
undefined -> throw({bad_request, "`keys` member must exist."});
_ -> ok
end,
MaxNumber = config:get_integer("chttpd",
"max_db_number_for_dbs_info_req", ?MAX_DB_NUM_FOR_DBS_INFO),
case length(Keys) =< MaxNumber of
true -> ok;
false -> throw({bad_request, too_many_keys})
end,
{ok, Resp} = chttpd:start_json_response(Req, 200),
send_chunk(Resp, "["),
lists:foldl(fun(DbName, AccSeparator) ->
try
{ok, Db} = fabric2_db:open(DbName, [{user_ctx, UserCtx}]),
{ok, Info} = fabric2_db:get_db_info(Db),
Json = ?JSON_ENCODE({[{key, DbName}, {info, {Info}}]}),
send_chunk(Resp, AccSeparator ++ Json)
catch error:database_does_not_exist ->
ErrJson = ?JSON_ENCODE({[{key, DbName}, {error, not_found}]}),
send_chunk(Resp, AccSeparator ++ ErrJson)
end,
"," % AccSeparator now has a comma
end, "", Keys),
send_chunk(Resp, "]"),
chttpd:end_json_response(Resp);
handle_dbs_info_req(Req) ->
send_method_not_allowed(Req, "GET,HEAD,POST").
handle_deleted_dbs_req(#httpd{method='GET', path_parts=[_]}=Req) ->
ok = chttpd:verify_is_server_admin(Req),
send_db_infos(Req, list_deleted_dbs_info);
handle_deleted_dbs_req(#httpd{method='POST', user_ctx=Ctx, path_parts=[_]}=Req) ->
couch_httpd:verify_is_server_admin(Req),
chttpd:validate_ctype(Req, "application/json"),
GetJSON = fun(Key, Props, Default) ->
case couch_util:get_value(Key, Props) of
undefined when Default == error ->
Fmt = "POST body must include `~s` parameter.",
Msg = io_lib:format(Fmt, [Key]),
throw({bad_request, iolist_to_binary(Msg)});
undefined ->
Default;
Value ->
Value
end
end,
{BodyProps} = chttpd:json_body_obj(Req),
{UndeleteProps} = GetJSON(<<"undelete">>, BodyProps, error),
DbName = GetJSON(<<"source">>, UndeleteProps, error),
TimeStamp = GetJSON(<<"timestamp">>, UndeleteProps, error),
TgtDbName = GetJSON(<<"target">>, UndeleteProps, DbName),
case fabric2_db:undelete(DbName, TgtDbName, TimeStamp, [{user_ctx, Ctx}]) of
ok ->
send_json(Req, 200, {[{ok, true}]});
{error, file_exists} ->
chttpd:send_error(Req, file_exists);
{error, not_found} ->
chttpd:send_error(Req, not_found);
Error ->
throw(Error)
end;
handle_deleted_dbs_req(#httpd{path_parts = PP}=Req) when length(PP) == 1 ->
send_method_not_allowed(Req, "GET,HEAD,POST");
handle_deleted_dbs_req(#httpd{method='DELETE', user_ctx=Ctx, path_parts=[_, DbName]}=Req) ->
couch_httpd:verify_is_server_admin(Req),
TS = case ?JSON_DECODE(couch_httpd:qs_value(Req, "timestamp", "null")) of
null ->
throw({bad_request, "`timestamp` parameter is not provided."});
TS0 ->
TS0
end,
case fabric2_db:delete(DbName, [{user_ctx, Ctx}, {deleted_at, TS}]) of
ok ->
send_json(Req, 200, {[{ok, true}]});
{error, not_found} ->
chttpd:send_error(Req, not_found);
Error ->
throw(Error)
end;
handle_deleted_dbs_req(#httpd{path_parts = PP}=Req) when length(PP) == 2 ->
send_method_not_allowed(Req, "HEAD,DELETE");
handle_deleted_dbs_req(Req) ->
chttpd:send_error(Req, not_found).
send_db_infos(Req, ListFunctionName) ->
#mrargs{
start_key = StartKey,
end_key = EndKey,
direction = Dir,
limit = Limit,
skip = Skip
} = couch_views_http_util:parse_params(Req, undefined),
Options = [
{start_key, StartKey},
{end_key, EndKey},
{dir, Dir},
{limit, Limit},
{skip, Skip}
],
% TODO: Figure out if we can't calculate a valid
% ETag for this request. \xFFmetadataVersion won't
% work as we don't bump versions on size changes
{ok, Resp1} = chttpd:start_delayed_json_response(Req, 200, []),
Callback = fun dbs_info_callback/2,
Acc = #vacc{req = Req, resp = Resp1},
{ok, Resp2} = fabric2_db:ListFunctionName(Callback, Acc, Options),
case is_record(Resp2, vacc) of
true -> {ok, Resp2#vacc.resp};
_ -> {ok, Resp2}
end.
dbs_info_callback({meta, _Meta}, #vacc{resp = Resp0} = Acc) ->
{ok, Resp1} = chttpd:send_delayed_chunk(Resp0, "["),
{ok, Acc#vacc{resp = Resp1}};
dbs_info_callback({row, Props}, #vacc{resp = Resp0} = Acc) ->
Prepend = couch_views_http_util:prepend_val(Acc),
Chunk = [Prepend, ?JSON_ENCODE({Props})],
{ok, Resp1} = chttpd:send_delayed_chunk(Resp0, Chunk),
{ok, Acc#vacc{prepend = ",", resp = Resp1}};
dbs_info_callback(complete, #vacc{resp = Resp0} = Acc) ->
{ok, Resp1} = chttpd:send_delayed_chunk(Resp0, "]"),
{ok, Resp2} = chttpd:end_delayed_json_response(Resp1),
{ok, Acc#vacc{resp = Resp2}};
dbs_info_callback({error, Reason}, #vacc{resp = Resp0} = Acc) ->
{ok, Resp1} = chttpd:send_delayed_error(Resp0, Reason),
{ok, Acc#vacc{resp = Resp1}}.
handle_task_status_req(#httpd{method='GET'}=Req) ->
ok = chttpd:verify_is_server_admin(Req),
ActiveTasks = fabric2_active_tasks:get_active_tasks(),
send_json(Req, ActiveTasks);
handle_task_status_req(Req) ->
send_method_not_allowed(Req, "GET,HEAD").
handle_replicate_req(#httpd{method='POST', user_ctx=Ctx, req_body=PostBody} = Req) ->
chttpd:validate_ctype(Req, "application/json"),
%% see HACK in chttpd.erl about replication
case couch_replicator:replicate(PostBody, Ctx) of
{ok, {continuous, RepId}} ->
send_json(Req, 202, {[{ok, true}, {<<"_local_id">>, RepId}]});
{ok, {cancelled, RepId}} ->
send_json(Req, 200, {[{ok, true}, {<<"_local_id">>, RepId}]});
{ok, #{} = JsonResults} ->
send_json(Req, maps:merge(#{<<"ok">> => true}, JsonResults));
{ok, stopped} ->
send_json(Req, 200, {[{ok, stopped}]});
{error, not_found=Error} ->
chttpd:send_error(Req, Error);
{error, #{<<"error">> := Err, <<"reason">> := Reason}} when
is_binary(Err), is_binary(Reason) ->
% Safe to use binary_to_atom since this is only built
% from couch_replicator_jobs:error_info/1
chttpd:send_error(Req, {binary_to_atom(Err, utf8), Reason});
{error, {_, _}=Error} ->
chttpd:send_error(Req, Error);
{_, _}=Error ->
chttpd:send_error(Req, Error)
end;
handle_replicate_req(Req) ->
send_method_not_allowed(Req, "POST").
handle_reload_query_servers_req(#httpd{method='POST'}=Req) ->
chttpd:validate_ctype(Req, "application/json"),
ok = couch_proc_manager:reload(),
send_json(Req, 200, {[{ok, true}]});
handle_reload_query_servers_req(Req) ->
send_method_not_allowed(Req, "POST").
handle_uuids_req(#httpd{method='GET'}=Req) ->
Max = config:get_integer("uuids", "max_count", 1000),
Count = try list_to_integer(couch_httpd:qs_value(Req, "count", "1")) of
N when N > Max ->
throw({bad_request, <<"count parameter too large">>});
N when N < 0 ->
throw({bad_request, <<"count must be a positive integer">>});
N -> N
catch
error:badarg ->
throw({bad_request, <<"count must be a positive integer">>})
end,
UUIDs = [couch_uuids:new() || _ <- lists:seq(1, Count)],
Etag = couch_httpd:make_etag(UUIDs),
couch_httpd:etag_respond(Req, Etag, fun() ->
CacheBustingHeaders = [
{"Date", couch_util:rfc1123_date()},
{"Cache-Control", "no-cache"},
% Past date, ON PURPOSE!
{"Expires", "Mon, 01 Jan 1990 00:00:00 GMT"},
{"Pragma", "no-cache"},
{"ETag", Etag}
],
send_json(Req, 200, CacheBustingHeaders, {[{<<"uuids">>, UUIDs}]})
end);
handle_uuids_req(Req) ->
send_method_not_allowed(Req, "GET").
handle_up_req(#httpd{method='GET'} = Req) ->
case config:get("couchdb", "maintenance_mode") of
"true" ->
send_json(Req, 404, {[{status, maintenance_mode}]});
"nolb" ->
send_json(Req, 404, {[{status, nolb}]});
_ ->
try
fabric2_db:list_dbs([{limit, 0}]),
send_json(Req, 200, {[{status, ok}]})
catch error:{timeout, _} ->
send_json(Req, 404, {[{status, backend_unavailable}]})
end
end;
handle_up_req(Req) ->
send_method_not_allowed(Req, "GET,HEAD").
get_docroot() ->
% if the env var isn’t set, let’s not throw an error, but
% assume the current working dir is what we want
os:getenv("COUCHDB_FAUXTON_DOCROOT", "").