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