You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by be...@apache.org on 2021/06/24 14:46:12 UTC
[couchdb] branch main updated: Reformat src files with erlfmt
(#3568)
This is an automated email from the ASF dual-hosted git repository.
bessbd pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/couchdb.git
The following commit(s) were added to refs/heads/main by this push:
new 15cbb55 Reformat src files with erlfmt (#3568)
15cbb55 is described below
commit 15cbb5502503c6e4d786bf0a85b7c225ccd14f71
Author: Bessenyei Balázs Donát <be...@apache.org>
AuthorDate: Thu Jun 24 16:46:03 2021 +0200
Reformat src files with erlfmt (#3568)
---
.gitignore | 1 +
Makefile | 16 +-
Makefile.win | 16 +-
README-DEV.rst | 14 +
configure | 59 +
dev/__init__.py | 5 +
dev/format_all.py | 80 +
dev/format_check.py | 64 +
dev/format_lib.py | 42 +
src/aegis/src/aegis.erl | 44 +-
src/aegis/src/aegis_app.erl | 3 -
src/aegis/src/aegis_key_manager.erl | 8 +-
src/aegis/src/aegis_keywrap.erl | 87 +-
src/aegis/src/aegis_noop_key_manager.erl | 5 -
src/aegis/src/aegis_server.erl | 94 +-
src/aegis/src/aegis_sup.erl | 3 -
src/chttpd/src/chttpd.erl | 1202 +++++----
src/chttpd/src/chttpd_auth.erl | 20 +-
src/chttpd/src/chttpd_auth_cache.erl | 196 +-
src/chttpd/src/chttpd_auth_request.erl | 117 +-
src/chttpd/src/chttpd_changes.erl | 611 +++--
src/chttpd/src/chttpd_cors.erl | 279 +-
src/chttpd/src/chttpd_db.erl | 2674 +++++++++++---------
src/chttpd/src/chttpd_epi.erl | 2 -
src/chttpd/src/chttpd_external.erl | 162 +-
src/chttpd/src/chttpd_handlers.erl | 31 +-
src/chttpd/src/chttpd_httpd_handlers.erl | 272 +-
src/chttpd/src/chttpd_misc.erl | 215 +-
src/chttpd/src/chttpd_node.erl | 196 +-
src/chttpd/src/chttpd_plugin.erl | 7 +-
src/chttpd/src/chttpd_prefer_header.erl | 27 +-
src/chttpd/src/chttpd_show.erl | 92 +-
src/chttpd/src/chttpd_stats.erl | 49 +-
src/chttpd/src/chttpd_sup.erl | 42 +-
src/chttpd/src/chttpd_test_util.erl | 1 -
src/chttpd/src/chttpd_util.erl | 79 +-
src/chttpd/src/chttpd_view.erl | 242 +-
src/chttpd/src/chttpd_xframe_options.erl | 48 +-
src/couch/src/couch.erl | 25 +-
src/couch/src/couch_att.erl | 388 ++-
src/couch/src/couch_auth_cache.erl | 56 +-
src/couch/src/couch_base32.erl | 167 +-
src/couch/src/couch_db_epi.erl | 1 -
src/couch/src/couch_debug.erl | 146 +-
src/couch/src/couch_doc.erl | 452 ++--
src/couch/src/couch_drv.erl | 38 +-
src/couch/src/couch_ejson_compare.erl | 87 +-
src/couch/src/couch_ejson_size.erl | 57 +-
src/couch/src/couch_flags.erl | 27 +-
src/couch/src/couch_flags_config.erl | 126 +-
src/couch/src/couch_hotp.erl | 11 +-
src/couch/src/couch_httpd.erl | 777 +++---
src/couch/src/couch_httpd_auth.erl | 681 +++--
src/couch/src/couch_httpd_external.erl | 160 +-
src/couch/src/couch_httpd_multipart.erl | 353 +--
src/couch/src/couch_httpd_vhost.erl | 238 +-
src/couch/src/couch_io_logger.erl | 12 +-
src/couch/src/couch_key_tree.erl | 295 ++-
src/couch/src/couch_native_process.erl | 299 ++-
src/couch/src/couch_os_process.erl | 171 +-
src/couch/src/couch_partition.erl | 43 +-
src/couch/src/couch_passwords.erl | 151 +-
src/couch/src/couch_primary_sup.erl | 21 +-
src/couch/src/couch_proc_manager.erl | 321 ++-
src/couch/src/couch_query_servers.erl | 589 +++--
src/couch/src/couch_rand.erl | 3 -
src/couch/src/couch_secondary_sup.erl | 49 +-
src/couch/src/couch_server.erl | 37 +-
src/couch/src/couch_sup.erl | 137 +-
src/couch/src/couch_totp.erl | 13 +-
src/couch/src/couch_util.erl | 375 +--
src/couch/src/couch_uuids.erl | 21 +-
src/couch/src/couch_work_queue.erl | 113 +-
src/couch/src/test_request.erl | 1 -
src/couch/src/test_util.erl | 112 +-
src/couch_epi/src/couch_epi.erl | 88 +-
src/couch_epi/src/couch_epi_codechange_monitor.erl | 10 +-
src/couch_epi/src/couch_epi_codegen.erl | 45 +-
src/couch_epi/src/couch_epi_data.erl | 11 +-
src/couch_epi/src/couch_epi_data_gen.erl | 173 +-
src/couch_epi/src/couch_epi_functions.erl | 10 +-
src/couch_epi/src/couch_epi_functions_gen.erl | 330 ++-
src/couch_epi/src/couch_epi_module_keeper.erl | 31 +-
src/couch_epi/src/couch_epi_plugin.erl | 190 +-
src/couch_epi/src/couch_epi_sup.erl | 59 +-
src/couch_epi/src/couch_epi_util.erl | 2 +-
src/couch_eval/src/couch_eval.erl | 43 +-
.../src/couch_expiring_cache.erl | 47 +-
.../src/couch_expiring_cache_fdb.erl | 82 +-
.../src/couch_expiring_cache_server.erl | 28 +-
src/couch_jobs/src/couch_jobs.erl | 140 +-
src/couch_jobs/src/couch_jobs_activity_monitor.erl | 122 +-
.../src/couch_jobs_activity_monitor_sup.erl | 16 +-
src/couch_jobs/src/couch_jobs_app.erl | 4 -
src/couch_jobs/src/couch_jobs_fdb.erl | 232 +-
src/couch_jobs/src/couch_jobs_notifier.erl | 247 +-
src/couch_jobs/src/couch_jobs_notifier_sup.erl | 16 +-
src/couch_jobs/src/couch_jobs_pending.erl | 16 +-
src/couch_jobs/src/couch_jobs_server.erl | 74 +-
src/couch_jobs/src/couch_jobs_sup.erl | 4 -
src/couch_jobs/src/couch_jobs_type_monitor.erl | 16 +-
src/couch_jobs/src/couch_jobs_util.erl | 28 +-
src/couch_js/src/couch_js.erl | 32 +-
src/couch_js/src/couch_js_app.erl | 7 +-
src/couch_js/src/couch_js_io_logger.erl | 12 +-
src/couch_js/src/couch_js_native_process.erl | 340 +--
src/couch_js/src/couch_js_os_process.erl | 197 +-
src/couch_js/src/couch_js_proc_manager.erl | 329 ++-
src/couch_js/src/couch_js_query_servers.erl | 510 ++--
src/couch_js/src/couch_js_sup.erl | 4 -
src/couch_lib/src/couch_lib_parse.erl | 40 +-
src/couch_log/src/couch_log.erl | 11 -
src/couch_log/src/couch_log_app.erl | 1 -
src/couch_log/src/couch_log_config.erl | 49 +-
src/couch_log/src/couch_log_config_dyn.erl | 2 -
src/couch_log/src/couch_log_error_logger_h.erl | 9 -
src/couch_log/src/couch_log_formatter.erl | 287 +--
src/couch_log/src/couch_log_monitor.erl | 13 +-
src/couch_log/src/couch_log_server.erl | 27 +-
src/couch_log/src/couch_log_sup.erl | 12 +-
src/couch_log/src/couch_log_trunc_io.erl | 873 ++++---
src/couch_log/src/couch_log_trunc_io_fmt.erl | 433 ++--
src/couch_log/src/couch_log_util.erl | 170 +-
src/couch_log/src/couch_log_writer.erl | 24 +-
src/couch_log/src/couch_log_writer_file.erl | 11 -
src/couch_log/src/couch_log_writer_journald.erl | 24 +-
src/couch_log/src/couch_log_writer_stderr.erl | 5 -
src/couch_log/src/couch_log_writer_syslog.erl | 169 +-
src/couch_prometheus/src/couch_prometheus_http.erl | 54 +-
.../src/couch_prometheus_server.erl | 73 +-
src/couch_prometheus/src/couch_prometheus_sup.erl | 3 +-
src/couch_prometheus/src/couch_prometheus_util.erl | 71 +-
src/couch_replicator/src/couch_replicator.erl | 217 +-
.../src/couch_replicator_api_wrap.erl | 967 +++----
src/couch_replicator/src/couch_replicator_auth.erl | 11 -
.../src/couch_replicator_auth_noop.erl | 8 -
.../src/couch_replicator_auth_session.erl | 259 +-
.../src/couch_replicator_changes_reader.erl | 137 +-
.../src/couch_replicator_connection.erl | 171 +-
src/couch_replicator/src/couch_replicator_docs.erl | 205 +-
src/couch_replicator/src/couch_replicator_epi.erl | 10 -
.../src/couch_replicator_fabric2_plugin.erl | 7 +-
.../src/couch_replicator_filters.erl | 128 +-
.../src/couch_replicator_httpc.erl | 365 +--
.../src/couch_replicator_httpc_pool.erl | 94 +-
.../src/couch_replicator_httpd.erl | 104 +-
src/couch_replicator/src/couch_replicator_ids.erl | 277 +-
src/couch_replicator/src/couch_replicator_job.erl | 904 +++----
.../src/couch_replicator_job_server.erl | 106 +-
src/couch_replicator/src/couch_replicator_jobs.erl | 45 +-
.../src/couch_replicator_parse.erl | 811 +++---
.../src/couch_replicator_rate_limiter.erl | 69 +-
.../src/couch_replicator_rate_limiter_tables.erl | 12 +-
.../src/couch_replicator_stats.erl | 34 +-
src/couch_replicator/src/couch_replicator_sup.erl | 21 +-
.../src/couch_replicator_utils.erl | 144 +-
.../src/couch_replicator_worker.erl | 570 +++--
src/couch_replicator/src/json_stream_parse.erl | 391 ++-
src/couch_stats/src/couch_stats.erl | 14 +-
src/couch_stats/src/couch_stats_aggregator.erl | 46 +-
src/couch_stats/src/couch_stats_httpd.erl | 60 +-
.../src/couch_stats_process_tracker.erl | 6 +-
src/couch_stats/src/couch_stats_sup.erl | 16 +-
src/couch_tests/src/couch_tests.erl | 69 +-
src/couch_tests/src/couch_tests_combinatorics.erl | 5 +-
src/couch_views/src/couch_views.erl | 185 +-
src/couch_views/src/couch_views_app.erl | 5 -
src/couch_views/src/couch_views_batch.erl | 43 +-
src/couch_views/src/couch_views_batch_impl.erl | 115 +-
src/couch_views/src/couch_views_ddoc.erl | 16 +-
src/couch_views/src/couch_views_encoding.erl | 59 +-
src/couch_views/src/couch_views_epi.erl | 10 -
src/couch_views/src/couch_views_fabric2_plugin.erl | 5 +-
src/couch_views/src/couch_views_fdb.erl | 102 +-
src/couch_views/src/couch_views_http.erl | 242 +-
src/couch_views/src/couch_views_http_util.erl | 273 +-
src/couch_views/src/couch_views_indexer.erl | 456 ++--
src/couch_views/src/couch_views_jobs.erl | 20 +-
src/couch_views/src/couch_views_plugin.erl | 8 +-
src/couch_views/src/couch_views_reader.erl | 191 +-
src/couch_views/src/couch_views_server.erl | 23 +-
src/couch_views/src/couch_views_sup.erl | 30 +-
src/couch_views/src/couch_views_trees.erl | 433 ++--
src/couch_views/src/couch_views_updater.erl | 103 +-
src/couch_views/src/couch_views_util.erl | 218 +-
src/couch_views/src/couch_views_validate.erl | 361 +--
src/ctrace/src/ctrace.erl | 132 +-
src/ctrace/src/ctrace_config.erl | 117 +-
src/ctrace/src/ctrace_dsl.erl | 58 +-
src/ctrace/src/ctrace_sup.erl | 2 +-
src/ebtree/src/ebtree.erl | 1153 +++++----
src/fabric/src/fabric2_active_tasks.erl | 39 +-
src/fabric/src/fabric2_app.erl | 3 -
src/fabric/src/fabric2_db.erl | 1722 +++++++------
src/fabric/src/fabric2_db_expiration.erl | 91 +-
src/fabric/src/fabric2_db_plugin.erl | 26 +-
src/fabric/src/fabric2_events.erl | 42 +-
src/fabric/src/fabric2_fdb.erl | 1282 +++++-----
src/fabric/src/fabric2_index.erl | 77 +-
src/fabric/src/fabric2_node_types.erl | 6 +-
src/fabric/src/fabric2_server.erl | 150 +-
src/fabric/src/fabric2_sup.erl | 3 -
src/fabric/src/fabric2_txids.erl | 50 +-
src/fabric/src/fabric2_users_db.erl | 172 +-
src/fabric/src/fabric2_util.erl | 195 +-
src/jwtf/src/jwtf.erl | 83 +-
src/jwtf/src/jwtf_keystore.erl | 35 +-
src/jwtf/src/jwtf_sup.erl | 2 +-
src/mango/src/mango_crud.erl | 56 +-
src/mango/src/mango_cursor.erl | 139 +-
src/mango/src/mango_cursor_special.erl | 4 +-
src/mango/src/mango_cursor_view.erl | 125 +-
src/mango/src/mango_doc.erl | 394 +--
src/mango/src/mango_epi.erl | 2 +-
src/mango/src/mango_error.erl | 54 +-
src/mango/src/mango_eval.erl | 97 +-
src/mango/src/mango_execution_stats.erl | 20 +-
src/mango/src/mango_fields.erl | 30 +-
src/mango/src/mango_httpd.erl | 199 +-
src/mango/src/mango_httpd_handlers.erl | 15 +-
src/mango/src/mango_idx.erl | 216 +-
src/mango/src/mango_idx_special.erl | 33 +-
src/mango/src/mango_idx_view.erl | 212 +-
src/mango/src/mango_json.erl | 11 +-
src/mango/src/mango_json_bookmark.erl | 25 +-
src/mango/src/mango_opts.erl | 35 +-
src/mango/src/mango_plugin.erl | 3 -
src/mango/src/mango_selector.erl | 639 +++--
src/mango/src/mango_selector_text.erl | 191 +-
src/mango/src/mango_sort.erl | 7 -
src/mango/src/mango_sup.erl | 3 +-
src/mango/src/mango_util.erl | 140 +-
232 files changed, 19614 insertions(+), 17171 deletions(-)
diff --git a/.gitignore b/.gitignore
index 7192941..ba6dac9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -53,6 +53,7 @@ src/mango/src/mango_cursor_text.nocompile
src/docs/
src/emilio/
src/erlfdb/
+src/erlfmt/
src/ets_lru/
src/excoveralls/
src/fauxton/
diff --git a/Makefile b/Makefile
index fc90117..adac5d1 100644
--- a/Makefile
+++ b/Makefile
@@ -17,6 +17,7 @@
include version.mk
REBAR?=$(shell echo `pwd`/bin/rebar)
+ERLFMT?=$(shell echo `pwd`/bin/erlfmt)
# Handle the following scenarios:
# 1. When building from a tarball, use version.mk.
@@ -160,6 +161,7 @@ endif
.PHONY: check
check: all
@$(MAKE) emilio
+ @$(MAKE) erlfmt-check
@$(MAKE) eunit
@$(MAKE) elixir-suite
@$(MAKE) exunit
@@ -209,6 +211,12 @@ soak-eunit: couch
emilio:
@bin/emilio -c emilio.config src/ | bin/warnings_in_scope -s 3 || exit 0
+erlfmt-check:
+ ERLFMT_PATH=$(ERLFMT) python3 dev/format_check.py
+
+erlfmt-format:
+ ERLFMT_PATH=$(ERLFMT) python3 dev/format_all.py
+
.venv/bin/black:
@python3 -m venv .venv
@.venv/bin/pip3 install black || touch .venv/bin/black
@@ -219,16 +227,16 @@ python-black: .venv/bin/black
echo "Python formatter not supported on Python < 3.6; check results on a newer platform"
@python3 -c "import sys; exit(1 if sys.version_info >= (3,6) else 0)" || \
LC_ALL=C.UTF-8 LANG=C.UTF-8 .venv/bin/black --check \
- --exclude="build/|buck-out/|dist/|_build/|\.git/|\.hg/|\.mypy_cache/|\.nox/|\.tox/|\.venv/|src/rebar/pr2relnotes.py|src/fauxton" \
- build-aux/*.py dev/run src/mango/test/*.py src/docs/src/conf.py src/docs/ext/*.py .
+ --exclude="build/|buck-out/|dist/|_build/|\.git/|\.hg/|\.mypy_cache/|\.nox/|\.tox/|\.venv/|src/erlfmt|src/rebar/pr2relnotes.py|src/fauxton" \
+ build-aux/*.py dev/run dev/format_*.py src/mango/test/*.py src/docs/src/conf.py src/docs/ext/*.py .
python-black-update: .venv/bin/black
@python3 -c "import sys; exit(1 if sys.version_info < (3,6) else 0)" || \
echo "Python formatter not supported on Python < 3.6; check results on a newer platform"
@python3 -c "import sys; exit(1 if sys.version_info >= (3,6) else 0)" || \
LC_ALL=C.UTF-8 LANG=C.UTF-8 .venv/bin/black \
- --exclude="build/|buck-out/|dist/|_build/|\.git/|\.hg/|\.mypy_cache/|\.nox/|\.tox/|\.venv/|src/rebar/pr2relnotes.py|src/fauxton" \
- build-aux/*.py dev/run src/mango/test/*.py src/docs/src/conf.py src/docs/ext/*.py .
+ --exclude="build/|buck-out/|dist/|_build/|\.git/|\.hg/|\.mypy_cache/|\.nox/|\.tox/|\.venv/|src/erlfmt|src/rebar/pr2relnotes.py|src/fauxton" \
+ build-aux/*.py dev/run dev/format_*.py src/mango/test/*.py src/docs/src/conf.py src/docs/ext/*.py .
.PHONY: elixir
elixir: export MIX_ENV=integration
diff --git a/Makefile.win b/Makefile.win
index 5240da3..4add8e4 100644
--- a/Makefile.win
+++ b/Makefile.win
@@ -18,6 +18,7 @@ include version.mk
SHELL=cmd.exe
REBAR=bin\rebar.cmd
+ERLFMT=bin/erlfmt
MAKE=make -f Makefile.win
# REBAR?=$(shell where rebar.cmd)
@@ -132,6 +133,7 @@ fauxton: share\www
# target: check - Test everything
check: all python-black
@$(MAKE) emilio
+ @$(MAKE) erlfmt-check
@$(MAKE) eunit
@$(MAKE) mango-test
@$(MAKE) elixir
@@ -176,6 +178,12 @@ just-eunit:
emilio:
@bin\emilio -c emilio.config src\ | python.exe bin\warnings_in_scope -s 3 || exit 0
+erlfmt-check:
+ ERLFMT_PATH=bin\erlfmt python3 dev\format_check.py
+
+erlfmt-format:
+ ERLFMT_PATH=bin\erlfmt python3 dev\format_all.py
+
.venv/bin/black:
@python.exe -m venv .venv
@.venv\Scripts\pip3.exe install black || copy /b .venv\Scripts\black.exe +,,
@@ -186,16 +194,16 @@ python-black: .venv/bin/black
echo 'Python formatter not supported on Python < 3.6; check results on a newer platform'
@python.exe -c "import sys; exit(1 if sys.version_info >= (3,6) else 0)" || \
.venv\Scripts\black.exe --check \
- --exclude="build/|buck-out/|dist/|_build/|\.git/|\.hg/|\.mypy_cache/|\.nox/|\.tox/|\.venv/|src/rebar/pr2relnotes.py|src/fauxton" \
- build-aux dev\run src\mango\test src\docs\src\conf.py src\docs\ext .
+ --exclude="build/|buck-out/|dist/|_build/|\.git/|\.hg/|\.mypy_cache/|\.nox/|\.tox/|\.venv/|src/erlfmt|src/rebar/pr2relnotes.py|src/fauxton" \
+ build-aux dev\run dev\format_*.py src\mango\test src\docs\src\conf.py src\docs\ext .
python-black-update: .venv/bin/black
@python.exe -c "import sys; exit(1 if sys.version_info < (3,6) else 0)" || \
echo 'Python formatter not supported on Python < 3.6; check results on a newer platform'
@python.exe -c "import sys; exit(1 if sys.version_info >= (3,6) else 0)" || \
.venv\Scripts\black.exe \
- --exclude="build/|buck-out/|dist/|_build/|\.git/|\.hg/|\.mypy_cache/|\.nox/|\.tox/|\.venv/|src/rebar/pr2relnotes.py|src/fauxton" \
- build-aux dev\run src\mango\test src\docs\src\conf.py src\docs\ext .
+ --exclude="build/|buck-out/|dist/|_build/|\.git/|\.hg/|\.mypy_cache/|\.nox/|\.tox/|\.venv/|src/erlfmt|src/rebar/pr2relnotes.py|src/fauxton" \
+ build-aux dev\run dev\format_*.py src\mango\test src\docs\src\conf.py src\docs\ext .
.PHONY: elixir
elixir: export MIX_ENV=integration
diff --git a/README-DEV.rst b/README-DEV.rst
index f4031b7..eb271c1 100644
--- a/README-DEV.rst
+++ b/README-DEV.rst
@@ -127,6 +127,20 @@ ignore their build and avoid any issues with their dependencies.
See ``./configure --help`` for more information.
+Developing
+----------
+
+Formatting
+~~~~~~~~~~
+
+The ``erl`` files in ``src`` are formatted using erlfmt_. The checks are run
+for every PR in the CI. To run the checks locally, run ``make erlfmt-check``.
+To format the ``erl`` files in ``src``, run ``make erlfmt-format``.
+To use ``erlfmt`` for specific files only, use the executable ``bin/erlfmt``
+that is installed by ``configure``.
+
+.. _erlfmt: https://github.com/WhatsApp/erlfmt
+
Testing
-------
diff --git a/configure b/configure
index 07f02e8..adceffb 100755
--- a/configure
+++ b/configure
@@ -53,6 +53,8 @@ Options:
--spidermonkey-version VSN specify the version of SpiderMonkey to use (defaults to $SM_VSN)
--skip-deps do not update erlang dependencies
--rebar=PATH use rebar by specified path (version >=2.6.0 && <3.0 required)
+ --rebar3=PATH use rebar3 by specified path
+ --erlfmt=PATH use erlfmt by specified path
EOF
}
@@ -135,6 +137,28 @@ parse_opts() {
fi
;;
+ --rebar3)
+ if [ -x "$2" ]; then
+ eval REBAR3=$2
+ shift 2
+ continue
+ else
+ printf 'ERROR: "--rebar3" requires valid path to executable.\n' >&2
+ exit 1
+ fi
+ ;;
+
+ --erlfmt)
+ if [ -x "$2" ]; then
+ eval ERLFMT=$2
+ shift 2
+ continue
+ else
+ printf 'ERROR: "--erlfmt" requires valid path to executable.\n' >&2
+ exit 1
+ fi
+ ;;
+
--user|-u)
if [ -n "$2" ]; then
eval COUCHDB_USER=$2
@@ -265,6 +289,31 @@ install_local_rebar() {
fi
}
+install_local_rebar3() {
+ if [ ! -x "${rootdir}/bin/rebar3" ]; then
+ if [ ! -d "${rootdir}/src/rebar3" ]; then
+ git clone --depth 1 https://github.com/erlang/rebar3.git ${rootdir}/src/rebar3
+ fi
+ cd src/rebar3
+ ./bootstrap
+ mv ${rootdir}/src/rebar3/rebar3 ${rootdir}/bin/rebar3
+ cd ../..
+ fi
+}
+
+install_local_erlfmt() {
+ if [ ! -x "${rootdir}/bin/erlfmt" ]; then
+ if [ ! -d "${rootdir}/src/erlfmt" ]; then
+ git clone --depth 1 https://github.com/WhatsApp/erlfmt.git ${rootdir}/src/erlfmt
+ fi
+ cd "${rootdir}"/src/erlfmt
+ ${REBAR3} as release escriptize
+ mv ${rootdir}/src/erlfmt/_build/release/bin/erlfmt ${rootdir}/bin/erlfmt
+ ${REBAR3} clean
+ cd ../..
+ fi
+}
+
install_local_emilio() {
if [ ! -x "${rootdir}/bin/emilio" ]; then
if [ ! -d "${rootdir}/src/emilio" ]; then
@@ -282,6 +331,16 @@ if [ -z "${REBAR}" ]; then
REBAR=${rootdir}/bin/rebar
fi
+if [ -z "${REBAR3}" ]; then
+ install_local_rebar3
+ REBAR3=${rootdir}/bin/rebar3
+fi
+
+if [ -z "${ERLFMT}" ]; then
+ install_local_erlfmt
+ ERLFMT=${rootdir}/bin/erlfmt
+fi
+
install_local_emilio
# only update dependencies, when we are not in a release tarball
diff --git a/dev/__init__.py b/dev/__init__.py
new file mode 100644
index 0000000..47f814f
--- /dev/null
+++ b/dev/__init__.py
@@ -0,0 +1,5 @@
+# For relative imports to work in Python 3.6
+import os
+import sys
+
+sys.path.append(os.path.dirname(os.path.realpath(__file__)))
diff --git a/dev/format_all.py b/dev/format_all.py
new file mode 100644
index 0000000..cf42fdc
--- /dev/null
+++ b/dev/format_all.py
@@ -0,0 +1,80 @@
+#!/usr/bin/env python3
+#
+# 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.
+
+"""Erlang formatter for CouchDB
+Warning: this file needs to run from the CouchDB repo root.
+USAGE: ERLFMT_PATH=<path_to_erlfmt> python3 dev/format_all.py
+"""
+
+import os
+import subprocess
+
+from format_lib import get_source_paths
+
+
+def get_hashes():
+ hashes = {}
+ for item in get_source_paths():
+ if item["is_source_path"]:
+ beam_path = f"{item['dirname']}/ebin/{item['filename']}.beam"
+ hashes[item["raw_path"]] = subprocess.run(
+ ["md5sum", beam_path], encoding="utf-8", capture_output=True
+ ).stdout
+ else:
+ # command = ["erl",
+ # "-eval",
+ # "{ok, _, Binary} = compile:file(\"" + item['raw_path'] +
+ # "\", [binary, no_line_info, deterministic])," +
+ # "erlang:display(crypto:hash(md5, Binary)), halt().",
+ # "-noshell"]
+ # hashes[item['raw_path']] = subprocess.run(command, encoding="utf-8",
+ # capture_output=True).stdout
+ pass
+ return hashes
+
+
+if __name__ == "__main__":
+ print("Cleaning...")
+ subprocess.run(["make", "clean"], encoding="utf-8", stdout=subprocess.PIPE)
+ print("Compiling...")
+ subprocess.run(
+ ["bin/rebar", "compile"],
+ encoding="utf-8",
+ stdout=subprocess.PIPE,
+ env={"ERL_OPTS": "no_line_info"},
+ )
+ os.chdir("src")
+ print("Getting previous hashes...")
+ prev = get_hashes()
+ for key in prev.keys():
+ subprocess.run(
+ [os.environ["ERLFMT_PATH"], "-w", key],
+ encoding="utf-8",
+ stdout=subprocess.PIPE,
+ )
+ os.chdir("..")
+ subprocess.run(
+ ["bin/rebar", "compile"],
+ encoding="utf-8",
+ stdout=subprocess.PIPE,
+ env={"ERL_OPTS": "no_line_info"},
+ )
+ os.chdir("src")
+ print("Getting post hashes...")
+ post = get_hashes()
+ if prev == post:
+ print("Hashes match")
+ else:
+ print("Hash mismatch")
+ print("Diff: ", set(prev.items()) ^ set(post.items()))
diff --git a/dev/format_check.py b/dev/format_check.py
new file mode 100644
index 0000000..de09951
--- /dev/null
+++ b/dev/format_check.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+#
+# 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.
+
+"""Erlang formatter for CouchDB
+Warning: this file needs to run from the CouchDB repo root.
+USAGE: ERLFMT_PATH=<path_to_erlfmt> python3 dev/format_check.py
+"""
+
+import os
+import subprocess
+import sys
+
+from format_lib import get_source_paths
+
+FILTERED_LINES = [
+ "Checking formatting...",
+ "[warn] Code style issues found in the above file(s). Forgot to run erlfmt?",
+ "",
+]
+
+if __name__ == "__main__":
+ os.chdir("src")
+ failed_checks = 0
+ for item in get_source_paths():
+ if item["is_source_path"]:
+ run_result = subprocess.run(
+ [
+ os.environ["ERLFMT_PATH"],
+ "-c",
+ "--verbose",
+ # We have some long lines and erlfmt doesn't forcefully wrap
+ # them all. We should decrease this over time
+ "--print-width=167",
+ item["raw_path"],
+ ],
+ encoding="utf-8",
+ capture_output=True,
+ )
+ if run_result.returncode != 0:
+ # erlfmt sometimes returns a non-zero status code with no
+ # actual errors. This is a workaround
+ stderr_lines = [
+ line
+ for line in run_result.stderr.split("\n")
+ if line not in FILTERED_LINES
+ and not line.startswith("Formatting ")
+ and not line.startswith("[warn] ")
+ ]
+ if len(stderr_lines) > 0:
+ print("\n".join(stderr_lines), file=sys.stderr)
+ failed_checks += 1
+ os.chdir("..")
+ sys.exit(failed_checks)
diff --git a/dev/format_lib.py b/dev/format_lib.py
new file mode 100644
index 0000000..2693114
--- /dev/null
+++ b/dev/format_lib.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python3
+#
+# 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.
+
+"""Erlang formatter lib for CouchDB
+Warning: this file is not meant to be executed manually
+"""
+
+import pathlib
+import re
+import subprocess
+
+
+def get_source_paths():
+ for item in subprocess.run(
+ ["git", "ls-files"], encoding="utf-8", capture_output=True
+ ).stdout.split("\n"):
+ item_path = pathlib.Path(item)
+ if item_path.suffix != ".erl":
+ continue
+
+ regex_result = re.search(r"([^/]+?)/src/([^/]+?).erl", item)
+ result_dict = {
+ "raw_path": item,
+ "item_path": item_path,
+ "is_source_path": regex_result is not None,
+ }
+ if result_dict["is_source_path"]:
+ result_dict.update(
+ {"dirname": regex_result.group(1), "filename": regex_result.group(2)}
+ )
+ yield result_dict
diff --git a/src/aegis/src/aegis.erl b/src/aegis/src/aegis.erl
index f2da69f..fe96e85 100644
--- a/src/aegis/src/aegis.erl
+++ b/src/aegis/src/aegis.erl
@@ -13,10 +13,8 @@
-module(aegis).
-include_lib("fabric/include/fabric2.hrl").
-
-define(WRAPPED_KEY, {?DB_AEGIS, 1}).
-
-export([
key_manager/0,
init_db/2,
@@ -32,56 +30,52 @@
key_manager() ->
?AEGIS_KEY_MANAGER.
-
init_db(#{} = Db, Options) ->
Db#{
is_encrypted => aegis_server:init_db(Db, Options)
}.
-
open_db(#{} = Db) ->
Db#{
is_encrypted => aegis_server:open_db(Db)
}.
-
get_db_info(#{is_encrypted := IsEncrypted} = Db) ->
- KeyManagerInfo = case erlang:function_exported(?AEGIS_KEY_MANAGER, get_db_info, 1) of
- true ->
- ?AEGIS_KEY_MANAGER:get_db_info(Db);
- false ->
- []
- end,
+ KeyManagerInfo =
+ case erlang:function_exported(?AEGIS_KEY_MANAGER, get_db_info, 1) of
+ true ->
+ ?AEGIS_KEY_MANAGER:get_db_info(Db);
+ false ->
+ []
+ end,
[{enabled, IsEncrypted}, {key_manager, {KeyManagerInfo}}].
-
encrypt(#{} = _Db, _Key, <<>>) ->
<<>>;
-
encrypt(#{is_encrypted := false}, _Key, Value) when is_binary(Value) ->
Value;
-
-encrypt(#{is_encrypted := true} = Db, Key, Value)
- when is_binary(Key), is_binary(Value) ->
+encrypt(#{is_encrypted := true} = Db, Key, Value) when
+ is_binary(Key), is_binary(Value)
+->
aegis_server:encrypt(Db, Key, Value).
-
decrypt(#{} = Db, Rows) when is_list(Rows) ->
- lists:map(fun({Key, Value}) ->
- {Key, decrypt(Db, Key, Value)}
- end, Rows).
+ lists:map(
+ fun({Key, Value}) ->
+ {Key, decrypt(Db, Key, Value)}
+ end,
+ Rows
+ ).
decrypt(#{} = _Db, _Key, <<>>) ->
<<>>;
-
decrypt(#{is_encrypted := false}, _Key, Value) when is_binary(Value) ->
Value;
-
-decrypt(#{is_encrypted := true} = Db, Key, Value)
- when is_binary(Key), is_binary(Value) ->
+decrypt(#{is_encrypted := true} = Db, Key, Value) when
+ is_binary(Key), is_binary(Value)
+->
aegis_server:decrypt(Db, Key, Value).
-
wrap_fold_fun(Db, Fun) when is_function(Fun, 2) ->
fun({Key, Value}, Acc) ->
Fun({Key, decrypt(Db, Key, Value)}, Acc)
diff --git a/src/aegis/src/aegis_app.erl b/src/aegis/src/aegis_app.erl
index 4a5a11f..c52e512 100644
--- a/src/aegis/src/aegis_app.erl
+++ b/src/aegis/src/aegis_app.erl
@@ -14,13 +14,10 @@
-behaviour(application).
-
-export([start/2, stop/1]).
-
start(_StartType, _StartArgs) ->
aegis_sup:start_link().
-
stop(_State) ->
ok.
diff --git a/src/aegis/src/aegis_key_manager.erl b/src/aegis/src/aegis_key_manager.erl
index 4426c4f..d35f78a 100644
--- a/src/aegis/src/aegis_key_manager.erl
+++ b/src/aegis/src/aegis_key_manager.erl
@@ -12,19 +12,15 @@
-module(aegis_key_manager).
-
-
-callback init_db(
Db :: #{},
- DbOptions :: list()) -> {ok, binary()} | false.
-
+ DbOptions :: list()
+) -> {ok, binary()} | false.
-callback open_db(Db :: #{}) -> {ok, binary()} | false.
-
-callback get_db_info(Db :: #{}) -> list().
-
-optional_callbacks([
get_db_info/1
]).
diff --git a/src/aegis/src/aegis_keywrap.erl b/src/aegis/src/aegis_keywrap.erl
index 58c7668..3e9a9d8 100644
--- a/src/aegis/src/aegis_keywrap.erl
+++ b/src/aegis/src/aegis_keywrap.erl
@@ -21,8 +21,9 @@
-define(ICV1, 16#A6A6A6A6A6A6A6A6).
-spec key_wrap(WrappingKey :: binary(), KeyToWrap :: binary()) -> binary().
-key_wrap(WrappingKey, KeyToWrap)
- when is_binary(WrappingKey), bit_size(KeyToWrap) rem 64 == 0 ->
+key_wrap(WrappingKey, KeyToWrap) when
+ is_binary(WrappingKey), bit_size(KeyToWrap) rem 64 == 0
+->
N = bit_size(KeyToWrap) div 64,
wrap(WrappingKey, <<?ICV1:64>>, KeyToWrap, 1, 6 * N).
@@ -33,10 +34,10 @@ wrap(WrappingKey, A, R, T, End) ->
<<MSB_B:64, LSB_B:64>> = ?aes_ecb_encrypt(WrappingKey, <<A/binary, R1:64>>),
wrap(WrappingKey, <<(MSB_B bxor T):64>>, <<Rest/binary, LSB_B:64>>, T + 1, End).
-
-spec key_unwrap(WrappingKey :: binary(), KeyToUnwrap :: binary()) -> binary() | fail.
-key_unwrap(WrappingKey, KeyToUnwrap)
- when is_binary(WrappingKey), bit_size(KeyToUnwrap) rem 64 == 0 ->
+key_unwrap(WrappingKey, KeyToUnwrap) when
+ is_binary(WrappingKey), bit_size(KeyToUnwrap) rem 64 == 0
+->
N = (bit_size(KeyToUnwrap) div 64),
<<A:64, R/binary>> = KeyToUnwrap,
case unwrap(WrappingKey, <<A:64>>, R, 6 * (N - 1)) of
@@ -50,48 +51,66 @@ unwrap(_WrappingKey, A, R, 0) ->
<<A/binary, R/binary>>;
unwrap(WrappingKey, <<A:64>>, R, T) ->
RestSize = bit_size(R) - 64,
- <<Rest:RestSize, R2: 64>> = R,
+ <<Rest:RestSize, R2:64>> = R,
<<MSB_B:64, LSB_B:64>> = ?aes_ecb_decrypt(WrappingKey, <<(A bxor T):64, R2:64>>),
unwrap(WrappingKey, <<MSB_B:64>>, <<LSB_B:64, Rest:RestSize>>, T - 1).
-
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
wrap_test_() ->
[
- %% 128 KEK / 128 DATA
- test_wrap_unwrap(<<16#000102030405060708090A0B0C0D0E0F:128>>,
- <<16#00112233445566778899AABBCCDDEEFF:128>>,
- <<16#1FA68B0A8112B447AEF34BD8FB5A7B829D3E862371D2CFE5:192>>),
- %% 192 KEK / 128 DATA
- test_wrap_unwrap(<<16#000102030405060708090A0B0C0D0E0F1011121314151617:192>>,
- <<16#00112233445566778899AABBCCDDEEFF:128>>,
- <<16#96778B25AE6CA435F92B5B97C050AED2468AB8A17AD84E5D:192>>),
- %% 256 KEK / 128 DATA
- test_wrap_unwrap(<<16#000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F:256>>,
- <<16#00112233445566778899AABBCCDDEEFF:128>>,
- <<16#64E8C3F9CE0F5BA263E9777905818A2A93C8191E7D6E8AE7:192>>),
- %% 192 KEK / 192 DATA
- test_wrap_unwrap(<<16#000102030405060708090A0B0C0D0E0F1011121314151617:192>>,
- <<16#00112233445566778899AABBCCDDEEFF0001020304050607:192>>,
- <<16#031D33264E15D33268F24EC260743EDCE1C6C7DDEE725A936BA814915C6762D2:256>>),
- %% 256 KEK / 192 DATA
- test_wrap_unwrap(<<16#000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F:256>>,
- <<16#00112233445566778899AABBCCDDEEFF0001020304050607:192>>,
- <<16#A8F9BC1612C68B3FF6E6F4FBE30E71E4769C8B80A32CB8958CD5D17D6B254DA1:256>>),
- %% 256 KEK / 256 DATA
- test_wrap_unwrap(<<16#000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F:256>>,
- <<16#00112233445566778899AABBCCDDEEFF000102030405060708090A0B0C0D0E0F:256>>,
- <<16#28C9F404C4B810F4CBCCB35CFB87F8263F5786E2D80ED326CBC7F0E71A99F43BFB988B9B7A02DD21:320>>)].
+ %% 128 KEK / 128 DATA
+ test_wrap_unwrap(
+ <<16#000102030405060708090A0B0C0D0E0F:128>>,
+ <<16#00112233445566778899AABBCCDDEEFF:128>>,
+ <<16#1FA68B0A8112B447AEF34BD8FB5A7B829D3E862371D2CFE5:192>>
+ ),
+ %% 192 KEK / 128 DATA
+ test_wrap_unwrap(
+ <<16#000102030405060708090A0B0C0D0E0F1011121314151617:192>>,
+ <<16#00112233445566778899AABBCCDDEEFF:128>>,
+ <<16#96778B25AE6CA435F92B5B97C050AED2468AB8A17AD84E5D:192>>
+ ),
+ %% 256 KEK / 128 DATA
+ test_wrap_unwrap(
+ <<16#000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F:256>>,
+ <<16#00112233445566778899AABBCCDDEEFF:128>>,
+ <<16#64E8C3F9CE0F5BA263E9777905818A2A93C8191E7D6E8AE7:192>>
+ ),
+ %% 192 KEK / 192 DATA
+ test_wrap_unwrap(
+ <<16#000102030405060708090A0B0C0D0E0F1011121314151617:192>>,
+ <<16#00112233445566778899AABBCCDDEEFF0001020304050607:192>>,
+ <<16#031D33264E15D33268F24EC260743EDCE1C6C7DDEE725A936BA814915C6762D2:256>>
+ ),
+ %% 256 KEK / 192 DATA
+ test_wrap_unwrap(
+ <<16#000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F:256>>,
+ <<16#00112233445566778899AABBCCDDEEFF0001020304050607:192>>,
+ <<16#A8F9BC1612C68B3FF6E6F4FBE30E71E4769C8B80A32CB8958CD5D17D6B254DA1:256>>
+ ),
+ %% 256 KEK / 256 DATA
+ test_wrap_unwrap(
+ <<16#000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F:256>>,
+ <<16#00112233445566778899AABBCCDDEEFF000102030405060708090A0B0C0D0E0F:256>>,
+ <<
+ 16#28C9F404C4B810F4CBCCB35CFB87F8263F5786E2D80ED326CBC7F0E71A99F43BFB988B9B7A02DD21:320
+ >>
+ )
+ ].
test_wrap_unwrap(WrappingKey, KeyToWrap, ExpectedWrappedKey) ->
- [?_assertEqual(ExpectedWrappedKey, key_wrap(WrappingKey, KeyToWrap)),
- ?_assertEqual(KeyToWrap, key_unwrap(WrappingKey, key_wrap(WrappingKey, KeyToWrap)))].
+ [
+ ?_assertEqual(ExpectedWrappedKey, key_wrap(WrappingKey, KeyToWrap)),
+ ?_assertEqual(KeyToWrap, key_unwrap(WrappingKey, key_wrap(WrappingKey, KeyToWrap)))
+ ].
fail_test() ->
KEK = <<16#000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F:256>>,
- CipherText = <<16#28C9F404C4B810F4CBCCB35CFB87F8263F5786E2D80ED326CBC7F0E71A99F43BFB988B9B7A02DD20:320>>,
+ CipherText = <<
+ 16#28C9F404C4B810F4CBCCB35CFB87F8263F5786E2D80ED326CBC7F0E71A99F43BFB988B9B7A02DD20:320
+ >>,
?assertEqual(fail, key_unwrap(KEK, CipherText)).
-endif.
diff --git a/src/aegis/src/aegis_noop_key_manager.erl b/src/aegis/src/aegis_noop_key_manager.erl
index 2b61f1d..95732a3 100644
--- a/src/aegis/src/aegis_noop_key_manager.erl
+++ b/src/aegis/src/aegis_noop_key_manager.erl
@@ -12,20 +12,15 @@
-module(aegis_noop_key_manager).
-
-behaviour(aegis_key_manager).
-
-export([
init_db/2,
open_db/1
]).
-
-
init_db(#{} = _Db, _Options) ->
false.
-
open_db(#{} = _Db) ->
false.
diff --git a/src/aegis/src/aegis_server.erl b/src/aegis/src/aegis_server.erl
index 508e453..efc99b4 100644
--- a/src/aegis/src/aegis_server.erl
+++ b/src/aegis/src/aegis_server.erl
@@ -16,11 +16,9 @@
-vsn(1).
-
-include("aegis.hrl").
-include_lib("kernel/include/logger.hrl").
-
%% aegis_server API
-export([
start_link/0,
@@ -40,8 +38,6 @@
code_change/3
]).
-
-
-define(KEY_CHECK, aegis_key_check).
-define(INIT_TIMEOUT, 60000).
-define(TIMEOUT, 10000).
@@ -49,16 +45,14 @@
-define(CACHE_MAX_AGE_SEC, 1800).
-define(CACHE_EXPIRATION_CHECK_SEC, 10).
-define(LAST_ACCESSED_INACTIVITY_SEC, 10).
--define(CACHE_DELETION_GRACE_SEC, 5). % Keep in cache after expiration
-
+% Keep in cache after expiration
+-define(CACHE_DELETION_GRACE_SEC, 5).
-record(entry, {uuid, encryption_key, counter, last_accessed, expires_at}).
-
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
-
-spec init_db(Db :: #{}, Options :: list()) -> boolean().
init_db(#{uuid := UUID} = Db, Options) ->
sensitive(fun() ->
@@ -71,7 +65,6 @@ init_db(#{uuid := UUID} = Db, Options) ->
end
end).
-
-spec open_db(Db :: #{}) -> boolean().
open_db(#{} = Db) ->
sensitive(fun() ->
@@ -83,7 +76,6 @@ open_db(#{} = Db) ->
end
end).
-
-spec encrypt(Db :: #{}, Key :: binary(), Value :: binary()) -> binary().
encrypt(#{} = Db, Key, Value) when is_binary(Key), is_binary(Value) ->
#{
@@ -95,7 +87,7 @@ encrypt(#{} = Db, Key, Value) when is_binary(Key), is_binary(Value) ->
case gen_server:call(?MODULE, {encrypt, Db, Key, Value}) of
CipherText when is_binary(CipherText) ->
CipherText;
- {error, {_Tag, {_C_FileName,_LineNumber}, _Desc} = Reason} ->
+ {error, {_Tag, {_C_FileName, _LineNumber}, _Desc} = Reason} ->
?LOG_ERROR(#{what => encrypt_failure, details => Reason}),
couch_log:error("aegis encryption failure: ~p ", [Reason]),
erlang:error(decryption_failed);
@@ -109,7 +101,6 @@ encrypt(#{} = Db, Key, Value) when is_binary(Key), is_binary(Value) ->
end)
end.
-
-spec decrypt(Db :: #{}, Key :: binary(), Value :: binary()) -> binary().
decrypt(#{} = Db, Key, Value) when is_binary(Key), is_binary(Value) ->
#{
@@ -121,7 +112,7 @@ decrypt(#{} = Db, Key, Value) when is_binary(Key), is_binary(Value) ->
case gen_server:call(?MODULE, {decrypt, Db, Key, Value}) of
PlainText when is_binary(PlainText) ->
PlainText;
- {error, {_Tag, {_C_FileName,_LineNumber}, _Desc} = Reason} ->
+ {error, {_Tag, {_C_FileName, _LineNumber}, _Desc} = Reason} ->
?LOG_ERROR(#{what => decrypt_failure, details => Reason}),
couch_log:error("aegis decryption failure: ~p ", [Reason]),
erlang:error(decryption_failed);
@@ -135,14 +126,15 @@ decrypt(#{} = Db, Key, Value) when is_binary(Key), is_binary(Value) ->
end)
end.
-
%% gen_server functions
init([]) ->
process_flag(sensitive, true),
Cache = ets:new(?MODULE, [set, private, {keypos, #entry.uuid}]),
- ByAccess = ets:new(?MODULE,
- [ordered_set, private, {keypos, #entry.counter}]),
+ ByAccess = ets:new(
+ ?MODULE,
+ [ordered_set, private, {keypos, #entry.counter}]
+ ),
ets:new(?KEY_CHECK, [named_table, protected, {read_concurrency, true}]),
erlang:send_after(0, self(), maybe_remove_expired),
@@ -154,11 +146,9 @@ init([]) ->
},
{ok, St, ?INIT_TIMEOUT}.
-
terminate(_Reason, _St) ->
ok.
-
handle_call({insert_key, UUID, DbKey}, _From, #{cache := Cache} = St) ->
case ets:lookup(Cache, UUID) of
[#entry{uuid = UUID} = Entry] ->
@@ -168,44 +158,34 @@ handle_call({insert_key, UUID, DbKey}, _From, #{cache := Cache} = St) ->
end,
NewSt = insert(St, UUID, DbKey),
{reply, ok, NewSt, ?TIMEOUT};
-
handle_call({encrypt, Db, Key, Value}, From, St) ->
handle_crypto_call(fun do_encrypt/4, Db, Key, Value, From, St);
-
handle_call({decrypt, Db, Key, Value}, From, St) ->
handle_crypto_call(fun do_decrypt/4, Db, Key, Value, From, St);
-
handle_call(_Msg, _From, St) ->
{noreply, St}.
-
handle_cast({accessed, UUID}, St) ->
NewSt = bump_last_accessed(St, UUID),
{noreply, NewSt};
-
-
handle_cast(_Msg, St) ->
{noreply, St}.
-
handle_info(maybe_remove_expired, St) ->
remove_expired_entries(St),
CheckInterval = erlang:convert_time_unit(
- expiration_check_interval(), second, millisecond),
+ expiration_check_interval(), second, millisecond
+ ),
erlang:send_after(CheckInterval, self(), maybe_remove_expired),
{noreply, St};
-
handle_info(_Msg, St) ->
{noreply, St}.
-
code_change(_OldVsn, St, _Extra) ->
{ok, St}.
-
%% private functions
-
handle_crypto_call(DoCryptoOp, Db, Key, Value, From, St) ->
#{uuid := UUID} = Db,
case lookup(St, UUID) of
@@ -214,9 +194,7 @@ handle_crypto_call(DoCryptoOp, Db, Key, Value, From, St) ->
{ok, DbKey} ->
erlang:spawn(fun() ->
process_flag(sensitive, true),
- try
- DoCryptoOp(DbKey, Db, Key, Value)
- of
+ try DoCryptoOp(DbKey, Db, Key, Value) of
Resp ->
gen_server:reply(From, Resp)
catch
@@ -227,7 +205,6 @@ handle_crypto_call(DoCryptoOp, Db, Key, Value, From, St) ->
{noreply, St, ?TIMEOUT}
end.
-
do_open_db(#{uuid := UUID} = Db) ->
case ?AEGIS_KEY_MANAGER:open_db(Db) of
{ok, DbKey} ->
@@ -237,20 +214,19 @@ do_open_db(#{uuid := UUID} = Db) ->
false
end.
-
do_encrypt(DbKey, #{uuid := UUID}, Key, Value) ->
EncryptionKey = crypto:strong_rand_bytes(32),
<<WrappedKey:320>> = aegis_keywrap:key_wrap(DbKey, EncryptionKey),
{CipherText, <<CipherTag:128>>} =
?aes_gcm_encrypt(
- EncryptionKey,
- <<0:96>>,
- <<UUID/binary, 0:8, Key/binary>>,
- Value),
+ EncryptionKey,
+ <<0:96>>,
+ <<UUID/binary, 0:8, Key/binary>>,
+ Value
+ ),
<<1:8, WrappedKey:320, CipherTag:128, CipherText/binary>>.
-
do_decrypt(DbKey, #{uuid := UUID}, Key, Value) ->
case Value of
<<1:8, WrappedKey:320, CipherTag:128, CipherText/binary>> ->
@@ -259,21 +235,22 @@ do_decrypt(DbKey, #{uuid := UUID}, Key, Value) ->
erlang:error(decryption_failed);
DecryptionKey ->
Decrypted =
- ?aes_gcm_decrypt(
- DecryptionKey,
- <<0:96>>,
- <<UUID/binary, 0:8, Key/binary>>,
- CipherText,
- <<CipherTag:128>>),
- if Decrypted /= error -> Decrypted; true ->
- erlang:error(decryption_failed)
+ ?aes_gcm_decrypt(
+ DecryptionKey,
+ <<0:96>>,
+ <<UUID/binary, 0:8, Key/binary>>,
+ CipherText,
+ <<CipherTag:128>>
+ ),
+ if
+ Decrypted /= error -> Decrypted;
+ true -> erlang:error(decryption_failed)
end
end;
_ ->
erlang:error(not_ciphertext)
end.
-
is_key_fresh(UUID) ->
Now = fabric2_util:now(sec),
@@ -282,7 +259,6 @@ is_key_fresh(UUID) ->
_ -> false
end.
-
%% cache functions
insert(St, UUID, DbKey) ->
@@ -321,7 +297,6 @@ insert(St, UUID, DbKey) ->
St#{counter := Counter + 1}.
-
lookup(#{cache := Cache}, UUID) ->
case ets:lookup(Cache, UUID) of
[#entry{uuid = UUID, encryption_key = DbKey} = Entry] ->
@@ -331,7 +306,6 @@ lookup(#{cache := Cache}, UUID) ->
{error, not_found}
end.
-
delete(St, #entry{uuid = UUID} = Entry) ->
#{
cache := Cache,
@@ -342,7 +316,6 @@ delete(St, #entry{uuid = UUID} = Entry) ->
true = ets:delete_object(Cache, Entry),
true = ets:delete_object(ByAccess, Entry).
-
maybe_bump_last_accessed(#entry{last_accessed = LastAccessed} = Entry) ->
case fabric2_util:now(sec) > LastAccessed + ?LAST_ACCESSED_INACTIVITY_SEC of
true ->
@@ -351,7 +324,6 @@ maybe_bump_last_accessed(#entry{last_accessed = LastAccessed} = Entry) ->
ok
end.
-
bump_last_accessed(St, UUID) ->
#{
cache := Cache,
@@ -359,7 +331,6 @@ bump_last_accessed(St, UUID) ->
counter := Counter
} = St,
-
[#entry{counter = OldCounter} = Entry0] = ets:lookup(Cache, UUID),
Entry = Entry0#entry{
@@ -374,7 +345,6 @@ bump_last_accessed(St, UUID) ->
St#{counter := Counter + 1}.
-
remove_expired_entries(St) ->
#{
cache := Cache,
@@ -393,25 +363,21 @@ remove_expired_entries(St) ->
Count = ets:select_delete(Cache, CacheExpired),
Count = ets:select_delete(ByAccess, CacheExpired).
-
-
max_age() ->
config:get_integer("aegis", "cache_max_age_sec", ?CACHE_MAX_AGE_SEC).
-
expiration_check_interval() ->
config:get_integer(
- "aegis", "cache_expiration_check_sec", ?CACHE_EXPIRATION_CHECK_SEC).
-
+ "aegis", "cache_expiration_check_sec", ?CACHE_EXPIRATION_CHECK_SEC
+ ).
cache_limit() ->
config:get_integer("aegis", "cache_limit", ?CACHE_LIMIT).
-
cache_deletion_grace() ->
config:get_integer(
- "aegis", "cache_deletion_grace_sec", ?CACHE_DELETION_GRACE_SEC).
-
+ "aegis", "cache_deletion_grace_sec", ?CACHE_DELETION_GRACE_SEC
+ ).
sensitive(Fun) when is_function(Fun, 0) ->
OldValue = process_flag(sensitive, true),
diff --git a/src/aegis/src/aegis_sup.erl b/src/aegis/src/aegis_sup.erl
index 6d3ee83..4d7e2c4 100644
--- a/src/aegis/src/aegis_sup.erl
+++ b/src/aegis/src/aegis_sup.erl
@@ -16,7 +16,6 @@
-vsn(1).
-
-export([
start_link/0
]).
@@ -25,11 +24,9 @@
init/1
]).
-
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
-
init([]) ->
Flags = #{
strategy => one_for_one,
diff --git a/src/chttpd/src/chttpd.erl b/src/chttpd/src/chttpd.erl
index e232ff4..1f32e82 100644
--- a/src/chttpd/src/chttpd.erl
+++ b/src/chttpd/src/chttpd.erl
@@ -18,30 +18,66 @@
-include_lib("chttpd/include/chttpd.hrl").
-include_lib("kernel/include/logger.hrl").
--export([start_link/0, start_link/1, start_link/2,
- stop/0, handle_request/1, handle_request_int/1,
- primary_header_value/2, header_value/2, header_value/3, qs_value/2,
- qs_value/3, qs/1, qs_json_value/3, path/1, absolute_uri/2, body_length/1,
- verify_is_server_admin/1, unquote/1, quote/1, recv/2, recv_chunked/4,
- error_info/1, parse_form/1, json_body/1, json_body_obj/1, body/1,
- doc_etag/1, make_etag/1, etag_respond/3, etag_match/2,
- partition/1, serve_file/3, serve_file/4,
- server_header/0, start_chunked_response/3,send_chunk/2,last_chunk/1,
- start_response_length/4, send/2, start_json_response/2,
- start_json_response/3, end_json_response/1, send_response/4,
+-export([
+ start_link/0, start_link/1, start_link/2,
+ stop/0,
+ handle_request/1,
+ handle_request_int/1,
+ primary_header_value/2,
+ header_value/2, header_value/3,
+ qs_value/2,
+ qs_value/3,
+ qs/1,
+ qs_json_value/3,
+ path/1,
+ absolute_uri/2,
+ body_length/1,
+ verify_is_server_admin/1,
+ unquote/1,
+ quote/1,
+ recv/2,
+ recv_chunked/4,
+ error_info/1,
+ parse_form/1,
+ json_body/1,
+ json_body_obj/1,
+ body/1,
+ doc_etag/1,
+ make_etag/1,
+ etag_respond/3,
+ etag_match/2,
+ partition/1,
+ serve_file/3, serve_file/4,
+ server_header/0,
+ start_chunked_response/3,
+ send_chunk/2,
+ last_chunk/1,
+ start_response_length/4,
+ send/2,
+ start_json_response/2,
+ start_json_response/3,
+ end_json_response/1,
+ send_response/4,
send_response_no_cors/4,
- send_method_not_allowed/2, send_error/2, send_error/4, send_redirect/2,
- send_chunked_error/2, send_json/2,send_json/3,send_json/4,
- validate_ctype/2]).
+ send_method_not_allowed/2,
+ send_error/2, send_error/4,
+ send_redirect/2,
+ send_chunked_error/2,
+ send_json/2, send_json/3, send_json/4,
+ validate_ctype/2
+]).
-export([authenticate_request/3]).
--export([start_delayed_json_response/2, start_delayed_json_response/3,
- start_delayed_json_response/4,
+-export([
+ start_delayed_json_response/2, start_delayed_json_response/3, start_delayed_json_response/4,
start_delayed_chunked_response/3, start_delayed_chunked_response/4,
- send_delayed_chunk/2, send_delayed_last_chunk/1,
- send_delayed_error/2, end_delayed_json_response/1,
- get_delayed_req/1]).
+ send_delayed_chunk/2,
+ send_delayed_last_chunk/1,
+ send_delayed_error/2,
+ end_delayed_json_response/1,
+ get_delayed_req/1
+]).
-export([
chunked_response_buffer_size/0,
@@ -54,8 +90,8 @@
code,
headers,
chunks,
- resp=nil,
- buffer_response=false
+ resp = nil,
+ buffer_response = false
}).
-define(DEFAULT_SERVER_OPTIONS, "[{recbuf, undefined}]").
@@ -66,23 +102,28 @@ start_link() ->
start_link(http) ->
Port = config:get("chttpd", "port", "5984"),
start_link(?MODULE, [{port, Port}]);
-
start_link(https) ->
Port = config:get("ssl", "port", "6984"),
{ok, Ciphers} = couch_util:parse_term(config:get("ssl", "ciphers", "undefined")),
{ok, Versions} = couch_util:parse_term(config:get("ssl", "tls_versions", "undefined")),
- {ok, SecureRenegotiate} = couch_util:parse_term(config:get("ssl", "secure_renegotiate", "undefined")),
+ {ok, SecureRenegotiate} = couch_util:parse_term(
+ config:get("ssl", "secure_renegotiate", "undefined")
+ ),
ServerOpts0 =
- [{cacertfile, config:get("ssl", "cacert_file", undefined)},
- {keyfile, config:get("ssl", "key_file", undefined)},
- {certfile, config:get("ssl", "cert_file", undefined)},
- {password, config:get("ssl", "password", undefined)},
- {secure_renegotiate, SecureRenegotiate},
- {versions, Versions},
- {ciphers, Ciphers}],
-
- case (couch_util:get_value(keyfile, ServerOpts0) == undefined orelse
- couch_util:get_value(certfile, ServerOpts0) == undefined) of
+ [
+ {cacertfile, config:get("ssl", "cacert_file", undefined)},
+ {keyfile, config:get("ssl", "key_file", undefined)},
+ {certfile, config:get("ssl", "cert_file", undefined)},
+ {password, config:get("ssl", "password", undefined)},
+ {secure_renegotiate, SecureRenegotiate},
+ {versions, Versions},
+ {ciphers, Ciphers}
+ ],
+
+ case
+ (couch_util:get_value(keyfile, ServerOpts0) == undefined orelse
+ couch_util:get_value(certfile, ServerOpts0) == undefined)
+ of
true ->
io:format("SSL enabled but PEM certificates are missing.", []),
throw({error, missing_certs});
@@ -90,41 +131,53 @@ start_link(https) ->
ok
end,
- ServerOpts = [Opt || {_, V}=Opt <- ServerOpts0, V /= undefined],
-
- ClientOpts = case config:get("ssl", "verify_ssl_certificates", "false") of
- "false" ->
- [];
- "true" ->
- FailIfNoPeerCert = case config:get("ssl", "fail_if_no_peer_cert", "false") of
- "false" -> false;
- "true" -> true
- end,
- [{depth, list_to_integer(config:get("ssl",
- "ssl_certificate_max_depth", "1"))},
- {fail_if_no_peer_cert, FailIfNoPeerCert},
- {verify, verify_peer}] ++
- case config:get("ssl", "verify_fun", undefined) of
- undefined -> [];
- SpecStr ->
- [{verify_fun, couch_httpd:make_arity_3_fun(SpecStr)}]
- end
- end,
+ ServerOpts = [Opt || {_, V} = Opt <- ServerOpts0, V /= undefined],
+
+ ClientOpts =
+ case config:get("ssl", "verify_ssl_certificates", "false") of
+ "false" ->
+ [];
+ "true" ->
+ FailIfNoPeerCert =
+ case config:get("ssl", "fail_if_no_peer_cert", "false") of
+ "false" -> false;
+ "true" -> true
+ end,
+ [
+ {depth,
+ list_to_integer(
+ config:get(
+ "ssl",
+ "ssl_certificate_max_depth",
+ "1"
+ )
+ )},
+ {fail_if_no_peer_cert, FailIfNoPeerCert},
+ {verify, verify_peer}
+ ] ++
+ case config:get("ssl", "verify_fun", undefined) of
+ undefined -> [];
+ SpecStr -> [{verify_fun, couch_httpd:make_arity_3_fun(SpecStr)}]
+ end
+ end,
SslOpts = ServerOpts ++ ClientOpts,
Options0 =
- [{port, Port},
- {ssl, true},
- {ssl_opts, SslOpts}],
+ [
+ {port, Port},
+ {ssl, true},
+ {ssl_opts, SslOpts}
+ ],
CustomServerOpts = get_server_options("httpsd"),
Options = merge_server_options(Options0, CustomServerOpts),
start_link(https, Options).
start_link(Name, Options) ->
- IP = case config:get("chttpd", "bind_address", "any") of
- "any" -> any;
- Else -> Else
- end,
+ IP =
+ case config:get("chttpd", "bind_address", "any") of
+ "any" -> any;
+ Else -> Else
+ end,
ok = couch_httpd:validate_bind_address(IP),
% Ensure uuid is set so that concurrent replications
@@ -135,19 +188,21 @@ start_link(Name, Options) ->
set_auth_handlers(),
- Options1 = Options ++ [
- {loop, fun ?MODULE:handle_request/1},
- {name, Name},
- {ip, IP}
- ],
+ Options1 =
+ Options ++
+ [
+ {loop, fun ?MODULE:handle_request/1},
+ {name, Name},
+ {ip, IP}
+ ],
ServerOpts = get_server_options("chttpd"),
Options2 = merge_server_options(Options1, ServerOpts),
case mochiweb_http:start(Options2) of
- {ok, Pid} ->
- {ok, Pid};
- {error, Reason} ->
- io:format("Failure to start Mochiweb: ~s~n", [Reason]),
- {error, Reason}
+ {ok, Pid} ->
+ {ok, Pid};
+ {error, Reason} ->
+ io:format("Failure to start Mochiweb: ~s~n", [Reason]),
+ {error, Reason}
end.
get_server_options(Module) ->
@@ -174,8 +229,11 @@ handle_request(MochiReq0) ->
handle_request_int(MochiReq) ->
Begin = os:timestamp(),
- SocketOptsCfg = config:get("chttpd",
- "socket_options", ?DEFAULT_SOCKET_OPTIONS),
+ SocketOptsCfg = config:get(
+ "chttpd",
+ "socket_options",
+ ?DEFAULT_SOCKET_OPTIONS
+ ),
{ok, SocketOpts} = couch_util:parse_term(SocketOptsCfg),
ok = mochiweb_socket:setopts(MochiReq:get(socket), SocketOpts),
@@ -185,55 +243,73 @@ handle_request_int(MochiReq) ->
{"/" ++ Path, _, _} = mochiweb_util:urlsplit_path(RawUri),
% get requested path
- RequestedPath = case MochiReq:get_header_value("x-couchdb-vhost-path") of
- undefined ->
- case MochiReq:get_header_value("x-couchdb-requested-path") of
- undefined -> RawUri;
- R -> R
- end;
- P -> P
- end,
+ RequestedPath =
+ case MochiReq:get_header_value("x-couchdb-vhost-path") of
+ undefined ->
+ case MochiReq:get_header_value("x-couchdb-requested-path") of
+ undefined -> RawUri;
+ R -> R
+ end;
+ P ->
+ P
+ end,
Peer = MochiReq:get(peer),
Method1 =
- case MochiReq:get(method) of
- % already an atom
- Meth when is_atom(Meth) -> Meth;
-
- % Non standard HTTP verbs aren't atoms (COPY, MOVE etc) so convert when
- % possible (if any module references the atom, then it's existing).
- Meth -> couch_util:to_existing_atom(Meth)
- end,
+ case MochiReq:get(method) of
+ % already an atom
+ Meth when is_atom(Meth) -> Meth;
+ % Non standard HTTP verbs aren't atoms (COPY, MOVE etc) so convert when
+ % possible (if any module references the atom, then it's existing).
+ Meth -> couch_util:to_existing_atom(Meth)
+ end,
increment_method_stats(Method1),
% allow broken HTTP clients to fake a full method vocabulary with an X-HTTP-METHOD-OVERRIDE header
MethodOverride = MochiReq:get_primary_header_value("X-HTTP-Method-Override"),
- Method2 = case lists:member(MethodOverride, ["GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT", "COPY"]) of
- true ->
- couch_log:notice("MethodOverride: ~s (real method was ~s)", [MethodOverride, Method1]),
- case Method1 of
- 'POST' ->
- ?LOG_NOTICE(#{
- what => http_method_override,
- result => ok,
- new_method => MethodOverride
- }),
- couch_util:to_existing_atom(MethodOverride);
- _ ->
- % Ignore X-HTTP-Method-Override when the original verb isn't POST.
- % I'd like to send a 406 error to the client, but that'd require a nasty refactor.
- % throw({not_acceptable, <<"X-HTTP-Method-Override may only be used with POST requests.">>})
- Method1
- end;
- _ -> Method1
- end,
+ Method2 =
+ case
+ lists:member(MethodOverride, [
+ "GET",
+ "HEAD",
+ "POST",
+ "PUT",
+ "DELETE",
+ "TRACE",
+ "CONNECT",
+ "COPY"
+ ])
+ of
+ true ->
+ couch_log:notice("MethodOverride: ~s (real method was ~s)", [
+ MethodOverride,
+ Method1
+ ]),
+ case Method1 of
+ 'POST' ->
+ ?LOG_NOTICE(#{
+ what => http_method_override,
+ result => ok,
+ new_method => MethodOverride
+ }),
+ couch_util:to_existing_atom(MethodOverride);
+ _ ->
+ % Ignore X-HTTP-Method-Override when the original verb isn't POST.
+ % I'd like to send a 406 error to the client, but that'd require a nasty refactor.
+ % throw({not_acceptable, <<"X-HTTP-Method-Override may only be used with POST requests.">>})
+ Method1
+ end;
+ _ ->
+ Method1
+ end,
% alias HEAD to GET as mochiweb takes care of stripping the body
- Method = case Method2 of
- 'HEAD' -> 'GET';
- Other -> Other
- end,
+ Method =
+ case Method2 of
+ 'HEAD' -> 'GET';
+ Other -> Other
+ end,
Nonce = couch_util:to_hex(crypto:strong_rand_bytes(5)),
logger:set_process_metadata(#{request_id => Nonce}),
@@ -245,10 +321,14 @@ handle_request_int(MochiReq) ->
original_method = Method1,
nonce = Nonce,
method = Method,
- path_parts = [list_to_binary(chttpd:unquote(Part))
- || Part <- string:tokens(Path, "/")],
- requested_path_parts = [?l2b(unquote(Part))
- || Part <- string:tokens(RequestedPath, "/")]
+ path_parts = [
+ list_to_binary(chttpd:unquote(Part))
+ || Part <- string:tokens(Path, "/")
+ ],
+ requested_path_parts = [
+ ?l2b(unquote(Part))
+ || Part <- string:tokens(RequestedPath, "/")
+ ]
},
% put small token on heap to keep requests synced to backend calls
@@ -260,12 +340,13 @@ handle_request_int(MochiReq) ->
maybe_trace_fdb(MochiReq:get_header_value("x-couchdb-fdb-trace")),
- {HttpReq2, Response} = case before_request(HttpReq0) of
- {ok, HttpReq1} ->
- process_request(HttpReq1);
- {error, Response0} ->
- {HttpReq0, Response0}
- end,
+ {HttpReq2, Response} =
+ case before_request(HttpReq0) of
+ {ok, HttpReq1} ->
+ process_request(HttpReq1);
+ {error, Response0} ->
+ {HttpReq0, Response0}
+ end,
{Status, Code, Reason, Resp} = split_response(Response),
@@ -296,17 +377,19 @@ before_request(HttpReq) ->
{ok, HttpReq1} = chttpd_plugin:before_request(HttpReq),
chttpd_stats:init(HttpReq1),
{ok, HttpReq1}
- catch Tag:Error:Stack ->
- {error, catch_error(HttpReq, Tag, Error, Stack)}
+ catch
+ Tag:Error:Stack ->
+ {error, catch_error(HttpReq, Tag, Error, Stack)}
end.
after_request(HttpReq, HttpResp0) ->
{ok, HttpResp1} =
try
chttpd_plugin:after_request(HttpReq, HttpResp0)
- catch _Tag:Error:Stack ->
- send_error(HttpReq, {Error, nil, Stack}),
- {ok, HttpResp0#httpd_resp{status = aborted}}
+ catch
+ _Tag:Error:Stack ->
+ send_error(HttpReq, {Error, nil, Stack}),
+ {ok, HttpResp0#httpd_resp{status = aborted}}
end,
HttpResp2 = update_stats(HttpReq, HttpResp1),
chttpd_stats:report(HttpResp2),
@@ -317,7 +400,7 @@ process_request(#httpd{mochi_req = MochiReq} = HttpReq) ->
HandlerKey =
case HttpReq#httpd.path_parts of
[] -> <<>>;
- [Key|_] -> ?l2b(quote(Key))
+ [Key | _] -> ?l2b(quote(Key))
end,
RawUri = MochiReq:get(raw_path),
@@ -327,31 +410,37 @@ process_request(#httpd{mochi_req = MochiReq} = HttpReq) ->
check_request_uri_length(RawUri),
check_url_encoding(RawUri),
case chttpd_cors:maybe_handle_preflight_request(HttpReq) of
- not_preflight ->
- case chttpd_auth:authenticate(HttpReq, fun authenticate_request/1) of
- #httpd{} = Req ->
- handle_req_after_auth(HandlerKey, Req);
+ not_preflight ->
+ case chttpd_auth:authenticate(HttpReq, fun authenticate_request/1) of
+ #httpd{} = Req ->
+ handle_req_after_auth(HandlerKey, Req);
+ Response ->
+ {HttpReq, Response}
+ end;
Response ->
{HttpReq, Response}
- end;
- Response ->
- {HttpReq, Response}
end
- catch Tag:Error:Stack ->
- {HttpReq, catch_error(HttpReq, Tag, Error, Stack)}
+ catch
+ Tag:Error:Stack ->
+ {HttpReq, catch_error(HttpReq, Tag, Error, Stack)}
end.
handle_req_after_auth(HandlerKey, HttpReq) ->
#httpd{user_ctx = #user_ctx{name = User}} = HttpReq,
ctrace:tag(#{user => User}),
try
- HandlerFun = chttpd_handlers:url_handler(HandlerKey,
- fun chttpd_db:handle_request/1),
- AuthorizedReq = chttpd_auth:authorize(possibly_hack(HttpReq),
- fun chttpd_auth_request:authorize_request/1),
+ HandlerFun = chttpd_handlers:url_handler(
+ HandlerKey,
+ fun chttpd_db:handle_request/1
+ ),
+ AuthorizedReq = chttpd_auth:authorize(
+ possibly_hack(HttpReq),
+ fun chttpd_auth_request:authorize_request/1
+ ),
{AuthorizedReq, HandlerFun(AuthorizedReq)}
- catch Tag:Error:Stack ->
- {HttpReq, catch_error(HttpReq, Tag, Error, Stack)}
+ catch
+ Tag:Error:Stack ->
+ {HttpReq, catch_error(HttpReq, Tag, Error, Stack)}
end.
catch_error(_HttpReq, throw, {http_head_abort, Resp}, _Stack) ->
@@ -377,7 +466,8 @@ catch_error(HttpReq, exit, {mochiweb_recv_error, E}, _Stack) ->
Peer,
Method,
MochiReq:get(raw_path),
- E]),
+ E
+ ]),
exit(normal);
catch_error(HttpReq, exit, {uri_too_long, _}, _Stack) ->
send_error(HttpReq, request_uri_too_long);
@@ -397,12 +487,13 @@ catch_error(HttpReq, Tag, Error, Stack) ->
% TODO improve logging and metrics collection for client disconnects
case {Tag, Error, Stack} of
{exit, normal, [{mochiweb_request, send, _, _} | _]} ->
- exit(normal); % Client disconnect (R15+)
+ % Client disconnect (R15+)
+ exit(normal);
_Else ->
send_error(HttpReq, {Error, nil, Stack})
end.
-split_response({ok, #delayed_resp{resp=Resp}}) ->
+split_response({ok, #delayed_resp{resp = Resp}}) ->
{ok, Resp:get(code), undefined, Resp};
split_response({ok, Resp}) ->
{ok, Resp:get(code), undefined, Resp};
@@ -443,38 +534,50 @@ maybe_log(#httpd{} = HttpReq, #httpd_resp{should_log = true} = HttpResp) ->
% - client port
% - timers: connection, request, time to first byte, ...
% - response size
- %
- ?LOG_NOTICE(#{
- method => Method,
- path => RawUri,
- code => Code,
- user => User,
- % req_size => MochiReq:get(body_length),
- src => #{ip4 => Peer},
- duration => RequestTime
- }, #{domain => [chttpd_access_log]}),
- couch_log:notice("~s ~s ~s ~s ~s ~B ~p ~B", [Host, Peer, User,
- Method, RawUri, Code, Status, RequestTime]);
+ %
+ ?LOG_NOTICE(
+ #{
+ method => Method,
+ path => RawUri,
+ code => Code,
+ user => User,
+ % req_size => MochiReq:get(body_length),
+ src => #{ip4 => Peer},
+ duration => RequestTime
+ },
+ #{domain => [chttpd_access_log]}
+ ),
+ couch_log:notice("~s ~s ~s ~s ~s ~B ~p ~B", [
+ Host,
+ Peer,
+ User,
+ Method,
+ RawUri,
+ Code,
+ Status,
+ RequestTime
+ ]);
maybe_log(_HttpReq, #httpd_resp{should_log = false}) ->
ok.
-
%% HACK: replication currently handles two forms of input, #db{} style
%% and #http_db style. We need a third that makes use of fabric. #db{}
%% works fine for replicating the dbs and nodes database because they
%% aren't sharded. So for now when a local db is specified as the source or
%% the target, it's hacked to make it a full url and treated as a remote.
-possibly_hack(#httpd{path_parts=[<<"_replicate">>]}=Req) ->
+possibly_hack(#httpd{path_parts = [<<"_replicate">>]} = Req) ->
{Props0} = chttpd:json_body_obj(Req),
Props1 = fix_uri(Req, Props0, <<"source">>),
Props2 = fix_uri(Req, Props1, <<"target">>),
- Req#httpd{req_body={Props2}};
+ Req#httpd{req_body = {Props2}};
possibly_hack(Req) ->
Req.
check_request_uri_length(Uri) ->
- check_request_uri_length(Uri,
- chttpd_util:get_chttpd_config("max_uri_length")).
+ check_request_uri_length(
+ Uri,
+ chttpd_util:get_chttpd_config("max_uri_length")
+ ).
check_request_uri_length(_Uri, undefined) ->
ok;
@@ -497,24 +600,24 @@ check_url_encoding([_ | Rest]) ->
fix_uri(Req, Props, Type) ->
case replication_uri(Type, Props) of
- undefined ->
- Props;
- Uri0 ->
- case is_http(Uri0) of
- true ->
+ undefined ->
Props;
- false ->
- Uri = make_uri(Req, quote(Uri0)),
- [{Type,Uri}|proplists:delete(Type,Props)]
- end
+ Uri0 ->
+ case is_http(Uri0) of
+ true ->
+ Props;
+ false ->
+ Uri = make_uri(Req, quote(Uri0)),
+ [{Type, Uri} | proplists:delete(Type, Props)]
+ end
end.
replication_uri(Type, PostProps) ->
case couch_util:get_value(Type, PostProps) of
- {Props} ->
- couch_util:get_value(<<"url">>, Props);
- Else ->
- Else
+ {Props} ->
+ couch_util:get_value(<<"url">>, Props);
+ Else ->
+ Else
end.
is_http(<<"http://", _/binary>>) ->
@@ -526,13 +629,19 @@ is_http(_) ->
make_uri(Req, Raw) ->
Port = integer_to_list(mochiweb_socket_server:get(chttpd, port)),
- Url = list_to_binary(["http://", config:get("httpd", "bind_address"),
- ":", Port, "/", Raw]),
+ Url = list_to_binary([
+ "http://",
+ config:get("httpd", "bind_address"),
+ ":",
+ Port,
+ "/",
+ Raw
+ ]),
Headers = [
- {<<"authorization">>, ?l2b(header_value(Req,"authorization",""))},
+ {<<"authorization">>, ?l2b(header_value(Req, "authorization", ""))},
{<<"cookie">>, ?l2b(extract_cookie(Req))}
],
- {[{<<"url">>,Url}, {<<"headers">>,{Headers}}]}.
+ {[{<<"url">>, Url}, {<<"headers">>, {Headers}}]}.
extract_cookie(#httpd{mochi_req = MochiReq}) ->
case MochiReq:get_cookie_value("AuthSession") of
@@ -543,6 +652,7 @@ extract_cookie(#httpd{mochi_req = MochiReq}) ->
end.
%%% end hack
+%% erlfmt-ignore
set_auth_handlers() ->
AuthenticationDefault = "{chttpd_auth, cookie_authentication_handler},
{chttpd_auth, default_authentication_handler}",
@@ -577,20 +687,21 @@ authenticate_request(Req) ->
authenticate_request(#httpd{} = Req0, AuthModule, AuthFuns) ->
Req = Req0#httpd{
auth_module = AuthModule,
- authentication_handlers = AuthFuns},
+ authentication_handlers = AuthFuns
+ },
authenticate_request(Req, AuthFuns).
% Try authentication handlers in order until one returns a result
-authenticate_request(#httpd{user_ctx=#user_ctx{}} = Req, _AuthFuns) ->
+authenticate_request(#httpd{user_ctx = #user_ctx{}} = Req, _AuthFuns) ->
Req;
-authenticate_request(#httpd{} = Req, [{Name, AuthFun}|Rest]) ->
+authenticate_request(#httpd{} = Req, [{Name, AuthFun} | Rest]) ->
authenticate_request(maybe_set_handler(AuthFun(Req), Name), Rest);
-authenticate_request(#httpd{} = Req, [AuthFun|Rest]) ->
+authenticate_request(#httpd{} = Req, [AuthFun | Rest]) ->
authenticate_request(AuthFun(Req), Rest);
authenticate_request(Response, _AuthFuns) ->
Response.
-maybe_set_handler(#httpd{user_ctx=#user_ctx{} = UserCtx} = Req, Name) ->
+maybe_set_handler(#httpd{user_ctx = #user_ctx{} = UserCtx} = Req, Name) ->
Req#httpd{user_ctx = UserCtx#user_ctx{handler = Name}};
maybe_set_handler(Else, _) ->
Else.
@@ -603,16 +714,16 @@ increment_method_stats(Method) ->
partition(Path) ->
mochiweb_util:partition(Path, "/").
-header_value(#httpd{mochi_req=MochiReq}, Key) ->
+header_value(#httpd{mochi_req = MochiReq}, Key) ->
MochiReq:get_header_value(Key).
-header_value(#httpd{mochi_req=MochiReq}, Key, Default) ->
+header_value(#httpd{mochi_req = MochiReq}, Key, Default) ->
case MochiReq:get_header_value(Key) of
- undefined -> Default;
- Value -> Value
+ undefined -> Default;
+ Value -> Value
end.
-primary_header_value(#httpd{mochi_req=MochiReq}, Key) ->
+primary_header_value(#httpd{mochi_req = MochiReq}, Key) ->
MochiReq:get_primary_header_value(Key).
serve_file(Req, RelativePath, DocumentRoot) ->
@@ -640,44 +751,52 @@ qs(#httpd{mochi_req = MochiReq, qs = undefined}) ->
qs(#httpd{qs = QS}) ->
QS.
-path(#httpd{mochi_req=MochiReq}) ->
+path(#httpd{mochi_req = MochiReq}) ->
MochiReq:get(path).
-absolute_uri(#httpd{mochi_req=MochiReq, absolute_uri = undefined}, Path) ->
+absolute_uri(#httpd{mochi_req = MochiReq, absolute_uri = undefined}, Path) ->
XHost = chttpd_util:get_chttpd_config(
- "x_forwarded_host", "X-Forwarded-Host"),
- Host = case MochiReq:get_header_value(XHost) of
- undefined ->
- case MochiReq:get_header_value("Host") of
- undefined ->
- {ok, {Address, Port}} = case MochiReq:get(socket) of
- {ssl, SslSocket} -> ssl:sockname(SslSocket);
- Socket -> inet:sockname(Socket)
- end,
- inet_parse:ntoa(Address) ++ ":" ++ integer_to_list(Port);
- Value1 ->
- Value1
- end;
- Value -> Value
- end,
+ "x_forwarded_host", "X-Forwarded-Host"
+ ),
+ Host =
+ case MochiReq:get_header_value(XHost) of
+ undefined ->
+ case MochiReq:get_header_value("Host") of
+ undefined ->
+ {ok, {Address, Port}} =
+ case MochiReq:get(socket) of
+ {ssl, SslSocket} -> ssl:sockname(SslSocket);
+ Socket -> inet:sockname(Socket)
+ end,
+ inet_parse:ntoa(Address) ++ ":" ++ integer_to_list(Port);
+ Value1 ->
+ Value1
+ end;
+ Value ->
+ Value
+ end,
XSsl = chttpd_util:get_chttpd_config("x_forwarded_ssl", "X-Forwarded-Ssl"),
- Scheme = case MochiReq:get_header_value(XSsl) of
- "on" -> "https";
- _ ->
- XProto = chttpd_util:get_chttpd_config(
- "x_forwarded_proto", "X-Forwarded-Proto"),
- case MochiReq:get_header_value(XProto) of
- % Restrict to "https" and "http" schemes only
- "https" -> "https";
- _ ->
- case MochiReq:get(scheme) of
- https ->
- "https";
- http ->
- "http"
- end
- end
- end,
+ Scheme =
+ case MochiReq:get_header_value(XSsl) of
+ "on" ->
+ "https";
+ _ ->
+ XProto = chttpd_util:get_chttpd_config(
+ "x_forwarded_proto", "X-Forwarded-Proto"
+ ),
+ case MochiReq:get_header_value(XProto) of
+ % Restrict to "https" and "http" schemes only
+ "https" ->
+ "https";
+ _ ->
+ case MochiReq:get(scheme) of
+ https ->
+ "https";
+ http ->
+ "http"
+ end
+ end
+ end,
Scheme ++ "://" ++ Host ++ Path;
absolute_uri(#httpd{absolute_uri = URI}, Path) ->
URI ++ Path.
@@ -688,27 +807,28 @@ unquote(UrlEncodedString) ->
quote(UrlDecodedString) ->
mochiweb_util:quote_plus(UrlDecodedString).
-parse_form(#httpd{mochi_req=MochiReq}) ->
+parse_form(#httpd{mochi_req = MochiReq}) ->
mochiweb_multipart:parse_form(MochiReq).
-recv(#httpd{mochi_req=MochiReq}, Len) ->
+recv(#httpd{mochi_req = MochiReq}, Len) ->
MochiReq:recv(Len).
-recv_chunked(#httpd{mochi_req=MochiReq}, MaxChunkSize, ChunkFun, InitState) ->
+recv_chunked(#httpd{mochi_req = MochiReq}, MaxChunkSize, ChunkFun, InitState) ->
% Fun is called once with each chunk
% Fun({Length, Binary}, State)
% called with Length == 0 on the last time.
MochiReq:stream_body(MaxChunkSize, ChunkFun, InitState).
-body_length(#httpd{mochi_req=MochiReq}) ->
+body_length(#httpd{mochi_req = MochiReq}) ->
MochiReq:get(body_length).
-body(#httpd{mochi_req=MochiReq, req_body=ReqBody}) ->
+body(#httpd{mochi_req = MochiReq, req_body = ReqBody}) ->
case ReqBody of
undefined ->
% Maximum size of document PUT request body (4GB)
MaxSize = chttpd_util:get_chttpd_config_integer(
- "max_http_request_size", 4294967296),
+ "max_http_request_size", 4294967296
+ ),
Begin = os:timestamp(),
try
MochiReq:recv_body(MaxSize)
@@ -723,38 +843,35 @@ body(#httpd{mochi_req=MochiReq, req_body=ReqBody}) ->
validate_ctype(Req, Ctype) ->
couch_httpd:validate_ctype(Req, Ctype).
-json_body(#httpd{req_body=undefined} = Httpd) ->
+json_body(#httpd{req_body = undefined} = Httpd) ->
case body(Httpd) of
undefined ->
throw({bad_request, "Missing request body"});
Body ->
?JSON_DECODE(maybe_decompress(Httpd, Body))
end;
-
-json_body(#httpd{req_body=ReqBody}) ->
+json_body(#httpd{req_body = ReqBody}) ->
ReqBody.
json_body_obj(Httpd) ->
case json_body(Httpd) of
{Props} -> {Props};
- _Else ->
- throw({bad_request, "Request body must be a JSON object"})
+ _Else -> throw({bad_request, "Request body must be a JSON object"})
end.
-
-doc_etag(#doc{id=Id, body=Body, revs={Start, [DiskRev|_]}}) ->
+doc_etag(#doc{id = Id, body = Body, revs = {Start, [DiskRev | _]}}) ->
couch_httpd:doc_etag(Id, Body, {Start, DiskRev}).
make_etag(Term) ->
<<SigInt:128/integer>> = couch_hash:md5_hash(term_to_binary(Term)),
- list_to_binary(io_lib:format("\"~.36B\"",[SigInt])).
+ list_to_binary(io_lib:format("\"~.36B\"", [SigInt])).
etag_match(Req, CurrentEtag) when is_binary(CurrentEtag) ->
etag_match(Req, binary_to_list(CurrentEtag));
-
etag_match(Req, CurrentEtag) ->
EtagsToMatch0 = string:tokens(
- chttpd:header_value(Req, "If-None-Match", ""), ", "),
+ chttpd:header_value(Req, "If-None-Match", ""), ", "
+ ),
EtagsToMatch = lists:map(fun strip_weak_prefix/1, EtagsToMatch0),
lists:member(CurrentEtag, EtagsToMatch).
@@ -765,27 +882,27 @@ strip_weak_prefix(Etag) ->
etag_respond(Req, CurrentEtag, RespFun) ->
case etag_match(Req, CurrentEtag) of
- true ->
- % the client has this in their cache.
- Headers = [{"ETag", CurrentEtag}],
- chttpd:send_response(Req, 304, Headers, <<>>);
- false ->
- % Run the function.
- RespFun()
+ true ->
+ % the client has this in their cache.
+ Headers = [{"ETag", CurrentEtag}],
+ chttpd:send_response(Req, 304, Headers, <<>>);
+ false ->
+ % Run the function.
+ RespFun()
end.
-verify_is_server_admin(#httpd{user_ctx=#user_ctx{roles=Roles}}) ->
+verify_is_server_admin(#httpd{user_ctx = #user_ctx{roles = Roles}}) ->
case lists:member(<<"_admin">>, Roles) of
- true -> ok;
- false -> throw({unauthorized, <<"You are not a server admin.">>})
+ true -> ok;
+ false -> throw({unauthorized, <<"You are not a server admin.">>})
end.
-start_response_length(#httpd{mochi_req=MochiReq}=Req, Code, Headers0, Length) ->
+start_response_length(#httpd{mochi_req = MochiReq} = Req, Code, Headers0, Length) ->
Headers1 = basic_headers(Req, Headers0),
Resp = handle_response(Req, Code, Headers1, Length, start_response_length),
case MochiReq:get(method) of
- 'HEAD' -> throw({http_head_abort, Resp});
- _ -> ok
+ 'HEAD' -> throw({http_head_abort, Resp});
+ _ -> ok
end,
{ok, Resp}.
@@ -793,12 +910,12 @@ send(Resp, Data) ->
Resp:send(Data),
{ok, Resp}.
-start_chunked_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers0) ->
+start_chunked_response(#httpd{mochi_req = MochiReq} = Req, Code, Headers0) ->
Headers1 = basic_headers(Req, Headers0),
Resp = handle_response(Req, Code, Headers1, chunked, respond),
case MochiReq:get(method) of
- 'HEAD' -> throw({http_head_abort, Resp});
- _ -> ok
+ 'HEAD' -> throw({http_head_abort, Resp});
+ _ -> ok
end,
{ok, Resp}.
@@ -806,7 +923,8 @@ send_chunk({remote, _Pid, _Ref} = Resp, Data) ->
couch_httpd:send_chunk(Resp, Data);
send_chunk(Resp, Data) ->
case iolist_size(Data) of
- 0 -> ok; % do nothing
+ % do nothing
+ 0 -> ok;
_ -> Resp:write_chunk(Data)
end,
{ok, Resp}.
@@ -824,8 +942,14 @@ send_response_no_cors(Req, Code, Headers0, Body) ->
couch_httpd:send_response_no_cors(Req, Code, Headers1, Body).
send_method_not_allowed(Req, Methods) ->
- send_error(Req, 405, [{"Allow", Methods}], <<"method_not_allowed">>,
- ?l2b("Only " ++ Methods ++ " allowed"), []).
+ send_error(
+ Req,
+ 405,
+ [{"Allow", Methods}],
+ <<"method_not_allowed">>,
+ ?l2b("Only " ++ Methods ++ " allowed"),
+ []
+ ).
send_json(Req, Value) ->
send_json(Req, 200, Value).
@@ -847,15 +971,12 @@ start_json_response(Req, Code, Headers0) ->
end_json_response(Resp) ->
couch_httpd:end_json_response(Resp).
-
start_delayed_json_response(Req, Code) ->
start_delayed_json_response(Req, Code, []).
-
start_delayed_json_response(Req, Code, Headers) ->
start_delayed_json_response(Req, Code, Headers, "").
-
start_delayed_json_response(Req, Code, Headers, FirstChunk) ->
{ok, #delayed_resp{
start_fun = fun start_json_response/3,
@@ -863,13 +984,12 @@ start_delayed_json_response(Req, Code, Headers, FirstChunk) ->
code = Code,
headers = Headers,
chunks = [FirstChunk],
- buffer_response = buffer_response(Req)}}.
-
+ buffer_response = buffer_response(Req)
+ }}.
start_delayed_chunked_response(Req, Code, Headers) ->
start_delayed_chunked_response(Req, Code, Headers, "").
-
start_delayed_chunked_response(Req, Code, Headers, FirstChunk) ->
{ok, #delayed_resp{
start_fun = fun start_chunked_response/3,
@@ -877,34 +997,30 @@ start_delayed_chunked_response(Req, Code, Headers, FirstChunk) ->
code = Code,
headers = Headers,
chunks = [FirstChunk],
- buffer_response = buffer_response(Req)}}.
+ buffer_response = buffer_response(Req)
+ }}.
-
-send_delayed_chunk(#delayed_resp{buffer_response=false}=DelayedResp, Chunk) ->
- {ok, #delayed_resp{resp=Resp}=DelayedResp1} =
+send_delayed_chunk(#delayed_resp{buffer_response = false} = DelayedResp, Chunk) ->
+ {ok, #delayed_resp{resp = Resp} = DelayedResp1} =
start_delayed_response(DelayedResp),
{ok, Resp} = send_chunk(Resp, Chunk),
{ok, DelayedResp1};
-
-send_delayed_chunk(#delayed_resp{buffer_response=true}=DelayedResp, Chunk) ->
+send_delayed_chunk(#delayed_resp{buffer_response = true} = DelayedResp, Chunk) ->
#delayed_resp{chunks = Chunks} = DelayedResp,
{ok, DelayedResp#delayed_resp{chunks = [Chunk | Chunks]}}.
-
send_delayed_last_chunk(Req) ->
send_delayed_chunk(Req, []).
-
-send_delayed_error(#delayed_resp{req=Req,resp=nil}=DelayedResp, Reason) ->
+send_delayed_error(#delayed_resp{req = Req, resp = nil} = DelayedResp, Reason) ->
{Code, ErrorStr, ReasonStr} = error_info(Reason),
{ok, Resp} = send_error(Req, Code, ErrorStr, ReasonStr),
- {ok, DelayedResp#delayed_resp{resp=Resp}};
-send_delayed_error(#delayed_resp{resp=Resp, req=Req}, Reason) ->
+ {ok, DelayedResp#delayed_resp{resp = Resp}};
+send_delayed_error(#delayed_resp{resp = Resp, req = Req}, Reason) ->
update_timeout_stats(Reason, Req),
log_error_with_stack_trace(Reason),
throw({http_abort, Resp, Reason}).
-
close_delayed_json_object(Resp, Buffer, Terminator, 0) ->
% Use a separate chunk to close the streamed array to maintain strict
% compatibility with earlier versions. See COUCHDB-2724
@@ -913,13 +1029,11 @@ close_delayed_json_object(Resp, Buffer, Terminator, 0) ->
close_delayed_json_object(Resp, Buffer, Terminator, _Threshold) ->
send_delayed_chunk(Resp, [Buffer | Terminator]).
-
-end_delayed_json_response(#delayed_resp{buffer_response=false}=DelayedResp) ->
- {ok, #delayed_resp{resp=Resp}} =
+end_delayed_json_response(#delayed_resp{buffer_response = false} = DelayedResp) ->
+ {ok, #delayed_resp{resp = Resp}} =
start_delayed_response(DelayedResp),
end_json_response(Resp);
-
-end_delayed_json_response(#delayed_resp{buffer_response=true}=DelayedResp) ->
+end_delayed_json_response(#delayed_resp{buffer_response = true} = DelayedResp) ->
#delayed_resp{
start_fun = StartFun,
req = Req,
@@ -928,36 +1042,37 @@ end_delayed_json_response(#delayed_resp{buffer_response=true}=DelayedResp) ->
chunks = Chunks
} = DelayedResp,
{ok, Resp} = StartFun(Req, Code, Headers),
- lists:foreach(fun
- ([]) -> ok;
- (Chunk) -> send_chunk(Resp, Chunk)
- end, lists:reverse(Chunks)),
+ lists:foreach(
+ fun
+ ([]) -> ok;
+ (Chunk) -> send_chunk(Resp, Chunk)
+ end,
+ lists:reverse(Chunks)
+ ),
end_json_response(Resp).
-
-get_delayed_req(#delayed_resp{req=#httpd{mochi_req=MochiReq}}) ->
+get_delayed_req(#delayed_resp{req = #httpd{mochi_req = MochiReq}}) ->
MochiReq;
get_delayed_req(Resp) ->
Resp:get(request).
-start_delayed_response(#delayed_resp{resp=nil}=DelayedResp) ->
+start_delayed_response(#delayed_resp{resp = nil} = DelayedResp) ->
#delayed_resp{
- start_fun=StartFun,
- req=Req,
- code=Code,
- headers=Headers,
- chunks=[FirstChunk]
- }=DelayedResp,
+ start_fun = StartFun,
+ req = Req,
+ code = Code,
+ headers = Headers,
+ chunks = [FirstChunk]
+ } = DelayedResp,
{ok, Resp} = StartFun(Req, Code, Headers),
case FirstChunk of
"" -> ok;
_ -> {ok, Resp} = send_chunk(Resp, FirstChunk)
end,
- {ok, DelayedResp#delayed_resp{resp=Resp}};
-start_delayed_response(#delayed_resp{}=DelayedResp) ->
+ {ok, DelayedResp#delayed_resp{resp = Resp}};
+start_delayed_response(#delayed_resp{} = DelayedResp) ->
{ok, DelayedResp}.
-
buffer_response(Req) ->
case chttpd:qs_value(Req, "buffer_response") of
"false" ->
@@ -968,9 +1083,8 @@ buffer_response(Req) ->
config:get_boolean("chttpd", "buffer_response", false)
end.
-
error_info({erlfdb_error, ErrorCode}) ->
- ErrorDesc = erlfdb:get_error_string(ErrorCode),
+ ErrorDesc = erlfdb:get_error_string(ErrorCode),
Reason = ?l2b(io_lib:format("code: ~B, desc: ~s", [ErrorCode, ErrorDesc])),
{500, erlfdb_error, Reason};
error_info({Error, Reason}) when is_list(Reason) ->
@@ -1001,8 +1115,10 @@ error_info({conflict, _}) ->
{409, <<"conflict">>, <<"Document update conflict.">>};
error_info({partition_overflow, DocId}) ->
Descr = <<
- "Partition limit exceeded due to update on '", DocId/binary, "'"
- >>,
+ "Partition limit exceeded due to update on '",
+ DocId/binary,
+ "'"
+ >>,
{403, <<"partition_overflow">>, Descr};
error_info({{not_found, missing}, {_, _}}) ->
{409, <<"not_found">>, <<"missing_rev">>};
@@ -1013,8 +1129,10 @@ error_info({forbidden, Msg}) ->
error_info({unauthorized, Msg}) ->
{401, <<"unauthorized">>, Msg};
error_info(file_exists) ->
- {412, <<"file_exists">>, <<"The database could not be "
- "created, the file already exists.">>};
+ {412, <<"file_exists">>, <<
+ "The database could not be "
+ "created, the file already exists."
+ >>};
error_info({error, {nodedown, Reason}}) ->
{412, <<"nodedown">>, Reason};
error_info({maintenance_mode, Node}) ->
@@ -1030,16 +1148,17 @@ error_info({bad_ctype, Reason}) ->
error_info(requested_range_not_satisfiable) ->
{416, <<"requested_range_not_satisfiable">>, <<"Requested range not satisfiable">>};
error_info({error, {illegal_database_name, Name}}) ->
- Message = <<"Name: '", Name/binary, "'. Only lowercase characters (a-z), ",
- "digits (0-9), and any of the characters _, $, (, ), +, -, and / ",
- "are allowed. Must begin with a letter.">>,
+ Message =
+ <<"Name: '", Name/binary, "'. Only lowercase characters (a-z), ",
+ "digits (0-9), and any of the characters _, $, (, ), +, -, and / ",
+ "are allowed. Must begin with a letter.">>,
{400, <<"illegal_database_name">>, Message};
error_info({illegal_docid, Reason}) ->
{400, <<"illegal_docid">>, Reason};
error_info({illegal_partition, Reason}) ->
{400, <<"illegal_partition">>, Reason};
-error_info({_DocID,{illegal_docid,DocID}}) ->
- {400, <<"illegal_docid">>,DocID};
+error_info({_DocID, {illegal_docid, DocID}}) ->
+ {400, <<"illegal_docid">>, DocID};
error_info({error, {database_name_too_long, DbName}}) ->
{400, <<"database_name_too_long">>,
<<"At least one path segment of `", DbName/binary, "` is too long.">>};
@@ -1060,18 +1179,24 @@ error_info({request_entity_too_large, {bulk_get, Max}}) when is_integer(Max) ->
error_info({request_entity_too_large, DocID}) ->
{413, <<"document_too_large">>, DocID};
error_info({error, security_migration_updates_disabled}) ->
- {503, <<"security_migration">>, <<"Updates to security docs are disabled during "
- "security migration.">>};
+ {503, <<"security_migration">>, <<
+ "Updates to security docs are disabled during "
+ "security migration."
+ >>};
error_info(all_workers_died) ->
- {503, <<"service unvailable">>, <<"Nodes are unable to service this "
- "request due to overloading or maintenance mode.">>};
+ {503, <<"service unvailable">>, <<
+ "Nodes are unable to service this "
+ "request due to overloading or maintenance mode."
+ >>};
error_info(not_implemented) ->
{501, <<"not_implemented">>, <<"this feature is not yet implemented">>};
error_info({disabled, Reason}) ->
{501, <<"disabled">>, Reason};
error_info(timeout) ->
- {500, <<"timeout">>, <<"The request could not be processed in a reasonable"
- " amount of time.">>};
+ {500, <<"timeout">>, <<
+ "The request could not be processed in a reasonable"
+ " amount of time."
+ >>};
error_info(decryption_failed) ->
{500, <<"decryption_failed">>, <<"Decryption failed">>};
error_info(not_ciphertext) ->
@@ -1079,8 +1204,12 @@ error_info(not_ciphertext) ->
error_info({service_unavailable, Reason}) ->
{503, <<"service unavailable">>, Reason};
error_info({unknown_eval_api_language, Language}) ->
- {400, <<"unknown_eval_api_language">>, <<"unsupported language in design"
- " doc: `", Language/binary, "`">>};
+ {400, <<"unknown_eval_api_language">>, <<
+ "unsupported language in design"
+ " doc: `",
+ Language/binary,
+ "`"
+ >>};
error_info({timeout, _Reason}) ->
error_info(timeout);
error_info({Error, null}) ->
@@ -1106,61 +1235,83 @@ maybe_handle_error(Error) ->
{500, <<"unknown_error">>, couch_util:to_binary(Error)}
end.
-
-error_headers(#httpd{mochi_req=MochiReq}=Req, 401=Code, ErrorStr, ReasonStr) ->
+error_headers(#httpd{mochi_req = MochiReq} = Req, 401 = Code, ErrorStr, ReasonStr) ->
% this is where the basic auth popup is triggered
case MochiReq:get_header_value("X-CouchDB-WWW-Authenticate") of
- undefined ->
- case chttpd_util:get_chttpd_config("WWW-Authenticate") of
undefined ->
- % If the client is a browser and the basic auth popup isn't turned on
- % redirect to the session page.
- case ErrorStr of
- <<"unauthorized">> ->
- case chttpd_util:get_chttpd_auth_config(
- "authentication_redirect", "/_utils/session.html") of
- undefined -> {Code, []};
- AuthRedirect ->
- case chttpd_util:get_chttpd_auth_config_boolean(
- "require_valid_user", false) of
- true ->
- % send the browser popup header no matter what if we are require_valid_user
- {Code, [{"WWW-Authenticate", "Basic realm=\"server\""}]};
- false ->
- case MochiReq:accepts_content_type("application/json") of
- true ->
- {Code, []};
- false ->
- case MochiReq:accepts_content_type("text/html") of
- true ->
- % Redirect to the path the user requested, not
- % the one that is used internally.
- UrlReturnRaw = case MochiReq:get_header_value("x-couchdb-vhost-path") of
+ case chttpd_util:get_chttpd_config("WWW-Authenticate") of
+ undefined ->
+ % If the client is a browser and the basic auth popup isn't turned on
+ % redirect to the session page.
+ case ErrorStr of
+ <<"unauthorized">> ->
+ case
+ chttpd_util:get_chttpd_auth_config(
+ "authentication_redirect", "/_utils/session.html"
+ )
+ of
undefined ->
- MochiReq:get(path);
- VHostPath ->
- VHostPath
- end,
- RedirectLocation = lists:flatten([
- AuthRedirect,
- "?return=", couch_util:url_encode(UrlReturnRaw),
- "&reason=", couch_util:url_encode(ReasonStr)
- ]),
- {302, [{"Location", absolute_uri(Req, RedirectLocation)}]};
- false ->
- {Code, []}
- end
- end
- end
- end;
- _Else ->
- {Code, []}
+ {Code, []};
+ AuthRedirect ->
+ case
+ chttpd_util:get_chttpd_auth_config_boolean(
+ "require_valid_user", false
+ )
+ of
+ true ->
+ % send the browser popup header no matter what if we are require_valid_user
+ {Code, [{"WWW-Authenticate", "Basic realm=\"server\""}]};
+ false ->
+ case
+ MochiReq:accepts_content_type("application/json")
+ of
+ true ->
+ {Code, []};
+ false ->
+ case
+ MochiReq:accepts_content_type("text/html")
+ of
+ true ->
+ % Redirect to the path the user requested, not
+ % the one that is used internally.
+ UrlReturnRaw =
+ case
+ MochiReq:get_header_value(
+ "x-couchdb-vhost-path"
+ )
+ of
+ undefined ->
+ MochiReq:get(path);
+ VHostPath ->
+ VHostPath
+ end,
+ RedirectLocation = lists:flatten([
+ AuthRedirect,
+ "?return=",
+ couch_util:url_encode(UrlReturnRaw),
+ "&reason=",
+ couch_util:url_encode(ReasonStr)
+ ]),
+ {302, [
+ {"Location",
+ absolute_uri(
+ Req, RedirectLocation
+ )}
+ ]};
+ false ->
+ {Code, []}
+ end
+ end
+ end
+ end;
+ _Else ->
+ {Code, []}
+ end;
+ Type ->
+ {Code, [{"WWW-Authenticate", Type}]}
end;
Type ->
{Code, [{"WWW-Authenticate", Type}]}
- end;
- Type ->
- {Code, [{"WWW-Authenticate", Type}]}
end;
error_headers(_, Code, _, _) ->
{Code, []}.
@@ -1177,18 +1328,32 @@ send_error(#httpd{} = Req, Code, ErrorStr, ReasonStr) ->
send_error(Req, Code, [], ErrorStr, ReasonStr, []).
send_error(Req, Code, Headers, ErrorStr, ReasonStr, []) ->
- Return = send_json(Req, Code, Headers,
- {[{<<"error">>, ErrorStr},
- {<<"reason">>, ReasonStr}]}),
+ Return = send_json(
+ Req,
+ Code,
+ Headers,
+ {[
+ {<<"error">>, ErrorStr},
+ {<<"reason">>, ReasonStr}
+ ]}
+ ),
span_error(Code, ErrorStr, ReasonStr, []),
Return;
send_error(Req, Code, Headers, ErrorStr, ReasonStr, Stack) ->
log_error_with_stack_trace({ErrorStr, ReasonStr, Stack}),
- Return = send_json(Req, Code, [stack_trace_id(Stack) | Headers],
- {[{<<"error">>, ErrorStr},
- {<<"reason">>, ReasonStr} |
- case Stack of [] -> []; _ -> [{<<"ref">>, stack_hash(Stack)}] end
- ]}),
+ Return = send_json(
+ Req,
+ Code,
+ [stack_trace_id(Stack) | Headers],
+ {[
+ {<<"error">>, ErrorStr},
+ {<<"reason">>, ReasonStr}
+ | case Stack of
+ [] -> [];
+ _ -> [{<<"ref">>, stack_hash(Stack)}]
+ end
+ ]}
+ ),
span_error(Code, ErrorStr, ReasonStr, Stack),
Return.
@@ -1199,20 +1364,27 @@ update_timeout_stats(timeout, #httpd{requested_path_parts = PathParts}) ->
update_timeout_stats(_, _) ->
ok.
-update_timeout_stats([_, <<"_partition">>, _, <<"_design">>, _,
- <<"_view">> | _]) ->
+update_timeout_stats([
+ _,
+ <<"_partition">>,
+ _,
+ <<"_design">>,
+ _,
+ <<"_view">>
+ | _
+]) ->
couch_stats:increment_counter([couchdb, httpd, partition_view_timeouts]);
-update_timeout_stats([_, <<"_partition">>, _, <<"_find">>| _]) ->
+update_timeout_stats([_, <<"_partition">>, _, <<"_find">> | _]) ->
couch_stats:increment_counter([couchdb, httpd, partition_find_timeouts]);
-update_timeout_stats([_, <<"_partition">>, _, <<"_explain">>| _]) ->
+update_timeout_stats([_, <<"_partition">>, _, <<"_explain">> | _]) ->
couch_stats:increment_counter([couchdb, httpd, partition_explain_timeouts]);
update_timeout_stats([_, <<"_partition">>, _, <<"_all_docs">> | _]) ->
couch_stats:increment_counter([couchdb, httpd, partition_all_docs_timeouts]);
update_timeout_stats([_, <<"_design">>, _, <<"_view">> | _]) ->
couch_stats:increment_counter([couchdb, httpd, view_timeouts]);
-update_timeout_stats([_, <<"_find">>| _]) ->
+update_timeout_stats([_, <<"_find">> | _]) ->
couch_stats:increment_counter([couchdb, httpd, find_timeouts]);
-update_timeout_stats([_, <<"_explain">>| _]) ->
+update_timeout_stats([_, <<"_explain">> | _]) ->
couch_stats:increment_counter([couchdb, httpd, explain_timeouts]);
update_timeout_stats([_, <<"_all_docs">> | _]) ->
couch_stats:increment_counter([couchdb, httpd, all_docs_timeouts]);
@@ -1223,17 +1395,21 @@ update_timeout_stats(_) ->
send_chunked_error(Resp, {_Error, {[{<<"body">>, Reason}]}}) ->
send_chunk(Resp, Reason),
send_chunk(Resp, []);
-
send_chunked_error(Resp, Error) ->
Stack = json_stack(Error),
log_error_with_stack_trace(Error),
{Code, ErrorStr, ReasonStr} = error_info(Error),
- JsonError = {[{<<"code">>, Code},
- {<<"error">>, ErrorStr},
- {<<"reason">>, ReasonStr} |
- case Stack of [] -> []; _ -> [{<<"ref">>, stack_hash(Stack)}] end
- ]},
- send_chunk(Resp, ?l2b([$\n,?JSON_ENCODE(JsonError),$\n])),
+ JsonError =
+ {[
+ {<<"code">>, Code},
+ {<<"error">>, ErrorStr},
+ {<<"reason">>, ReasonStr}
+ | case Stack of
+ [] -> [];
+ _ -> [{<<"ref">>, stack_hash(Stack)}]
+ end
+ ]},
+ send_chunk(Resp, ?l2b([$\n, ?JSON_ENCODE(JsonError), $\n])),
send_chunk(Resp, []).
send_redirect(Req, Path) ->
@@ -1261,32 +1437,43 @@ json_stack({_Error, _Reason, Stack}) when is_list(Stack) ->
json_stack(_) ->
[].
-json_stack_item({M,F,A}) ->
+json_stack_item({M, F, A}) ->
list_to_binary(io_lib:format("~s:~s/~B", [M, F, json_stack_arity(A)]));
-json_stack_item({M,F,A,L}) ->
+json_stack_item({M, F, A, L}) ->
case proplists:get_value(line, L) of
- undefined -> json_stack_item({M,F,A});
- Line -> list_to_binary(io_lib:format("~s:~s/~B L~B",
- [M, F, json_stack_arity(A), Line]))
+ undefined ->
+ json_stack_item({M, F, A});
+ Line ->
+ list_to_binary(
+ io_lib:format(
+ "~s:~s/~B L~B",
+ [M, F, json_stack_arity(A), Line]
+ )
+ )
end;
json_stack_item(_) ->
<<"bad entry in stacktrace">>.
json_stack_arity(A) ->
- if is_integer(A) -> A; is_list(A) -> length(A); true -> 0 end.
+ if
+ is_integer(A) -> A;
+ is_list(A) -> length(A);
+ true -> 0
+ end.
maybe_decompress(Httpd, Body) ->
case header_value(Httpd, "Content-Encoding", "identity") of
- "gzip" ->
- try
- zlib:gunzip(Body)
- catch error:data_error ->
- throw({bad_request, "Request body is not properly gzipped."})
- end;
- "identity" ->
- Body;
- Else ->
- throw({bad_ctype, [Else, " is not a supported content encoding."]})
+ "gzip" ->
+ try
+ zlib:gunzip(Body)
+ catch
+ error:data_error ->
+ throw({bad_request, "Request body is not properly gzipped."})
+ end;
+ "identity" ->
+ Body;
+ Else ->
+ throw({bad_ctype, [Else, " is not a supported content encoding."]})
end.
log_error_with_stack_trace({bad_request, _, _}) ->
@@ -1299,8 +1486,16 @@ log_error_with_stack_trace({Error, Reason, Stack}) ->
hash => stack_hash(Stack),
stacktrace => Stack
}),
- EFmt = if is_binary(Error) -> "~s"; true -> "~w" end,
- RFmt = if is_binary(Reason) -> "~s"; true -> "~w" end,
+ EFmt =
+ if
+ is_binary(Error) -> "~s";
+ true -> "~w"
+ end,
+ RFmt =
+ if
+ is_binary(Reason) -> "~s";
+ true -> "~w"
+ end,
Fmt = "req_err(~w) " ++ EFmt ++ " : " ++ RFmt ++ "~n ~p",
couch_log:error(Fmt, [stack_hash(Stack), Error, Reason, Stack]);
log_error_with_stack_trace(_) ->
@@ -1325,9 +1520,10 @@ chunked_response_buffer_size() ->
chttpd_util:get_chttpd_config_integer("chunked_response_buffer", 1490).
basic_headers(Req, Headers0) ->
- Headers = Headers0
- ++ server_header()
- ++ couch_httpd_auth:cookie_auth_header(Req, Headers0),
+ Headers =
+ Headers0 ++
+ server_header() ++
+ couch_httpd_auth:cookie_auth_header(Req, Headers0),
Headers1 = chttpd_cors:headers(Req, Headers),
Headers2 = chttpd_xframe_options:header(Req, Headers1),
Headers3 = [reqid(), timing() | Headers2],
@@ -1377,26 +1573,33 @@ start_span(Req) ->
path_parts = PathParts
} = Req,
{OperationName, ExtraTags} = get_action(Req),
- Path = case PathParts of
- [] -> <<"">>;
- [_ | _] -> filename:join(PathParts)
- end,
+ Path =
+ case PathParts of
+ [] -> <<"">>;
+ [_ | _] -> filename:join(PathParts)
+ end,
{IsExternalSpan, RootOptions} = root_span_options(MochiReq),
- Tags = maps:merge(#{
- peer => Peer,
- 'http.method' => Method,
- nonce => Nonce,
- 'http.url' => MochiReq:get(raw_path),
- path_parts => Path,
- 'span.kind' => <<"server">>,
- component => <<"couchdb.chttpd">>,
- external => IsExternalSpan
- }, ExtraTags),
-
- ctrace:start_span(OperationName, [
- {tags, Tags},
- {time, Begin}
- ] ++ RootOptions).
+ Tags = maps:merge(
+ #{
+ peer => Peer,
+ 'http.method' => Method,
+ nonce => Nonce,
+ 'http.url' => MochiReq:get(raw_path),
+ path_parts => Path,
+ 'span.kind' => <<"server">>,
+ component => <<"couchdb.chttpd">>,
+ external => IsExternalSpan
+ },
+ ExtraTags
+ ),
+
+ ctrace:start_span(
+ OperationName,
+ [
+ {tags, Tags},
+ {time, Begin}
+ ] ++ RootOptions
+ ).
root_span_options(MochiReq) ->
case get_trace_headers(MochiReq) of
@@ -1420,8 +1623,9 @@ parse_span_id(Hex) ->
to_int(Hex, N) when length(Hex) =:= N ->
try
list_to_integer(Hex, 16)
- catch error:badarg ->
- undefined
+ catch
+ error:badarg ->
+ undefined
end.
get_trace_headers(MochiReq) ->
@@ -1442,20 +1646,21 @@ get_trace_headers(MochiReq) ->
];
_ ->
[undefined, undefined, undefined]
- end
+ end
end.
get_action(#httpd{} = Req) ->
try
chttpd_handlers:handler_info(Req)
- catch Tag:Error ->
- ?LOG_ERROR(#{
- what => tracing_configuration_failure,
- tag => Tag,
- details => Error
- }),
- couch_log:error("Cannot set tracing action ~p:~p", [Tag, Error]),
- {undefined, #{}}
+ catch
+ Tag:Error ->
+ ?LOG_ERROR(#{
+ what => tracing_configuration_failure,
+ tag => Tag,
+ details => Error
+ }),
+ couch_log:error("Cannot set tracing action ~p:~p", [Tag, Error]),
+ {undefined, #{}}
end.
span_ok(#httpd_resp{code = Code}) ->
@@ -1501,52 +1706,83 @@ check_url_encoding_pass_test_() ->
check_url_encoding_fail_test_() ->
[
- ?_assertThrow({bad_request, invalid_url_encoding},
- check_url_encoding("/dbname%")),
- ?_assertThrow({bad_request, invalid_url_encoding},
- check_url_encoding("/dbname/doc_id%")),
- ?_assertThrow({bad_request, invalid_url_encoding},
- check_url_encoding("/dbname/doc_id%?rev=1-abcdefgh")),
- ?_assertThrow({bad_request, invalid_url_encoding},
- check_url_encoding("/dbname%2")),
- ?_assertThrow({bad_request, invalid_url_encoding},
- check_url_encoding("/dbname/doc_id%2")),
- ?_assertThrow({bad_request, invalid_url_encoding},
- check_url_encoding("/user%2Fdbname%")),
- ?_assertThrow({bad_request, invalid_url_encoding},
- check_url_encoding("/user%2Fdbname/doc_id%")),
- ?_assertThrow({bad_request, invalid_url_encoding},
- check_url_encoding("%")),
- ?_assertThrow({bad_request, invalid_url_encoding},
- check_url_encoding("/%")),
- ?_assertThrow({bad_request, invalid_url_encoding},
- check_url_encoding("/%2")),
- ?_assertThrow({bad_request, invalid_url_encoding},
- check_url_encoding("/dbname%2%3A")),
- ?_assertThrow({bad_request, invalid_url_encoding},
- check_url_encoding("/dbname%%3Ae")),
- ?_assertThrow({bad_request, invalid_url_encoding},
- check_url_encoding("/dbname%2g")),
- ?_assertThrow({bad_request, invalid_url_encoding},
- check_url_encoding("/dbname%g2"))
+ ?_assertThrow(
+ {bad_request, invalid_url_encoding},
+ check_url_encoding("/dbname%")
+ ),
+ ?_assertThrow(
+ {bad_request, invalid_url_encoding},
+ check_url_encoding("/dbname/doc_id%")
+ ),
+ ?_assertThrow(
+ {bad_request, invalid_url_encoding},
+ check_url_encoding("/dbname/doc_id%?rev=1-abcdefgh")
+ ),
+ ?_assertThrow(
+ {bad_request, invalid_url_encoding},
+ check_url_encoding("/dbname%2")
+ ),
+ ?_assertThrow(
+ {bad_request, invalid_url_encoding},
+ check_url_encoding("/dbname/doc_id%2")
+ ),
+ ?_assertThrow(
+ {bad_request, invalid_url_encoding},
+ check_url_encoding("/user%2Fdbname%")
+ ),
+ ?_assertThrow(
+ {bad_request, invalid_url_encoding},
+ check_url_encoding("/user%2Fdbname/doc_id%")
+ ),
+ ?_assertThrow(
+ {bad_request, invalid_url_encoding},
+ check_url_encoding("%")
+ ),
+ ?_assertThrow(
+ {bad_request, invalid_url_encoding},
+ check_url_encoding("/%")
+ ),
+ ?_assertThrow(
+ {bad_request, invalid_url_encoding},
+ check_url_encoding("/%2")
+ ),
+ ?_assertThrow(
+ {bad_request, invalid_url_encoding},
+ check_url_encoding("/dbname%2%3A")
+ ),
+ ?_assertThrow(
+ {bad_request, invalid_url_encoding},
+ check_url_encoding("/dbname%%3Ae")
+ ),
+ ?_assertThrow(
+ {bad_request, invalid_url_encoding},
+ check_url_encoding("/dbname%2g")
+ ),
+ ?_assertThrow(
+ {bad_request, invalid_url_encoding},
+ check_url_encoding("/dbname%g2")
+ )
].
log_format_test() ->
?assertEqual(
"127.0.0.1:15984 127.0.0.1 undefined "
"GET /_cluster_setup 201 ok 10000",
- test_log_request("/_cluster_setup", undefined)),
+ test_log_request("/_cluster_setup", undefined)
+ ),
?assertEqual(
"127.0.0.1:15984 127.0.0.1 user_foo "
"GET /_all_dbs 201 ok 10000",
- test_log_request("/_all_dbs", #user_ctx{name = <<"user_foo">>})),
+ test_log_request("/_all_dbs", #user_ctx{name = <<"user_foo">>})
+ ),
%% Utf8Name = unicode:characters_to_binary(Something),
- Utf8User = <<227,130,136,227,129,134,227,129,147,227,129,157>>,
+ Utf8User = <<227, 130, 136, 227, 129, 134, 227, 129, 147, 227, 129, 157>>,
?assertEqual(
"127.0.0.1:15984 127.0.0.1 %E3%82%88%E3%81%86%E3%81%93%E3%81%9D "
"GET /_all_dbs 201 ok 10000",
- test_log_request("/_all_dbs", #user_ctx{name = Utf8User})),
+ test_log_request("/_all_dbs", #user_ctx{name = Utf8User})
+ ),
ok.
test_log_request(RawPath, UserCtx) ->
@@ -1554,14 +1790,14 @@ test_log_request(RawPath, UserCtx) ->
MochiReq = mochiweb_request:new(socket, [], 'POST', RawPath, version, Headers),
Req = #httpd{
mochi_req = MochiReq,
- begin_ts = {1458,588713,124003},
+ begin_ts = {1458, 588713, 124003},
original_method = 'GET',
peer = "127.0.0.1",
nonce = "nonce",
user_ctx = UserCtx
},
Resp = #httpd_resp{
- end_ts = {1458,588723,124303},
+ end_ts = {1458, 588723, 124303},
code = 201,
status = ok
},
@@ -1575,14 +1811,20 @@ test_log_request(RawPath, UserCtx) ->
handle_req_after_auth_test() ->
Headers = mochiweb_headers:make([{"HOST", "127.0.0.1:15984"}]),
- MochiReq = mochiweb_request:new(socket, [], 'PUT', "/newdb", version,
- Headers),
+ MochiReq = mochiweb_request:new(
+ socket,
+ [],
+ 'PUT',
+ "/newdb",
+ version,
+ Headers
+ ),
UserCtx = #user_ctx{name = <<"retain_user">>},
Roles = [<<"_reader">>],
AuthorizedCtx = #user_ctx{name = <<"retain_user">>, roles = Roles},
Req = #httpd{
mochi_req = MochiReq,
- begin_ts = {1458,588713,124003},
+ begin_ts = {1458, 588713, 124003},
original_method = 'PUT',
peer = "127.0.0.1",
nonce = "nonce",
@@ -1592,18 +1834,22 @@ handle_req_after_auth_test() ->
ok = meck:new(chttpd_handlers, [passthrough]),
ok = meck:new(chttpd_auth, [passthrough]),
ok = meck:expect(chttpd_handlers, url_handler, fun(_Key, _Fun) ->
- fun(_Req) -> handled_authorized_req end
+ fun(_Req) -> handled_authorized_req end
end),
ok = meck:expect(chttpd_auth, authorize, fun(_Req, _Fun) ->
AuthorizedReq
end),
- ?assertEqual({AuthorizedReq, handled_authorized_req},
- handle_req_after_auth(foo_key, Req)),
+ ?assertEqual(
+ {AuthorizedReq, handled_authorized_req},
+ handle_req_after_auth(foo_key, Req)
+ ),
ok = meck:expect(chttpd_auth, authorize, fun(_Req, _Fun) ->
meck:exception(throw, {http_abort, resp, some_reason})
end),
- ?assertEqual({Req, {aborted, resp, some_reason}},
- handle_req_after_auth(foo_key, Req)),
+ ?assertEqual(
+ {Req, {aborted, resp, some_reason}},
+ handle_req_after_auth(foo_key, Req)
+ ),
ok = meck:unload(chttpd_handlers),
ok = meck:unload(chttpd_auth).
diff --git a/src/chttpd/src/chttpd_auth.erl b/src/chttpd/src/chttpd_auth.erl
index ffae781..20b5a05 100644
--- a/src/chttpd/src/chttpd_auth.erl
+++ b/src/chttpd/src/chttpd_auth.erl
@@ -27,7 +27,6 @@
-define(SERVICE_ID, chttpd_auth).
-
%% ------------------------------------------------------------------
%% API Function Definitions
%% ------------------------------------------------------------------
@@ -38,7 +37,6 @@ authenticate(HttpReq, Default) ->
authorize(HttpReq, Default) ->
maybe_handle(authorize, [HttpReq], Default).
-
%% ------------------------------------------------------------------
%% Default callbacks
%% ------------------------------------------------------------------
@@ -55,17 +53,20 @@ proxy_authentication_handler(Req) ->
jwt_authentication_handler(Req) ->
couch_httpd_auth:jwt_authentication_handler(Req).
-party_mode_handler(#httpd{method='POST', path_parts=[<<"_session">>]} = Req) ->
+party_mode_handler(#httpd{method = 'POST', path_parts = [<<"_session">>]} = Req) ->
% See #1947 - users should always be able to attempt a login
- Req#httpd{user_ctx=#user_ctx{}};
-party_mode_handler(#httpd{path_parts=[<<"_up">>]} = Req) ->
+ Req#httpd{user_ctx = #user_ctx{}};
+party_mode_handler(#httpd{path_parts = [<<"_up">>]} = Req) ->
RequireValidUser = config:get_boolean("chttpd", "require_valid_user", false),
- RequireValidUserExceptUp = config:get_boolean("chttpd", "require_valid_user_except_for_up", false),
+ RequireValidUserExceptUp = config:get_boolean(
+ "chttpd", "require_valid_user_except_for_up", false
+ ),
require_valid_user(Req, RequireValidUser andalso not RequireValidUserExceptUp);
-
party_mode_handler(Req) ->
RequireValidUser = config:get_boolean("chttpd", "require_valid_user", false),
- RequireValidUserExceptUp = config:get_boolean("chttpd", "require_valid_user_except_for_up", false),
+ RequireValidUserExceptUp = config:get_boolean(
+ "chttpd", "require_valid_user_except_for_up", false
+ ),
require_valid_user(Req, RequireValidUser orelse RequireValidUserExceptUp).
require_valid_user(_Req, true) ->
@@ -75,13 +76,12 @@ require_valid_user(Req, false) ->
[] ->
Req#httpd{user_ctx = ?ADMIN_USER};
_ ->
- Req#httpd{user_ctx=#user_ctx{}}
+ Req#httpd{user_ctx = #user_ctx{}}
end.
handle_session_req(Req) ->
couch_httpd_auth:handle_session_req(Req, chttpd_auth_cache).
-
%% ------------------------------------------------------------------
%% Internal Function Definitions
%% ------------------------------------------------------------------
diff --git a/src/chttpd/src/chttpd_auth_cache.erl b/src/chttpd/src/chttpd_auth_cache.erl
index 88ffb7a..e0e8aed 100644
--- a/src/chttpd/src/chttpd_auth_cache.erl
+++ b/src/chttpd/src/chttpd_auth_cache.erl
@@ -15,8 +15,14 @@
-behaviour(config_listener).
-export([start_link/0, get_user_creds/2, update_user_creds/3]).
--export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
- code_change/3]).
+-export([
+ init/1,
+ handle_call/3,
+ handle_cast/2,
+ handle_info/2,
+ terminate/2,
+ code_change/3
+]).
-export([listen_for_changes/1, changes_callback/2]).
-export([handle_config_change/5, handle_config_terminate/3]).
@@ -29,7 +35,7 @@
-record(state, {
changes_pid,
- last_seq="0"
+ last_seq = "0"
}).
%% public functions
@@ -40,18 +46,21 @@ start_link() ->
get_user_creds(Req, UserName) when is_list(UserName) ->
get_user_creds(Req, ?l2b(UserName));
get_user_creds(_Req, UserName) when is_binary(UserName) ->
- Resp = case couch_auth_cache:get_admin(UserName) of
- nil ->
- get_from_cache(UserName);
- Props ->
- case get_from_cache(UserName) of
- nil ->
- Props;
- UserProps when is_list(UserProps) ->
- couch_auth_cache:add_roles(Props,
- couch_util:get_value(<<"roles">>, UserProps))
- end
- end,
+ Resp =
+ case couch_auth_cache:get_admin(UserName) of
+ nil ->
+ get_from_cache(UserName);
+ Props ->
+ case get_from_cache(UserName) of
+ nil ->
+ Props;
+ UserProps when is_list(UserProps) ->
+ couch_auth_cache:add_roles(
+ Props,
+ couch_util:get_value(<<"roles">>, UserProps)
+ )
+ end
+ end,
maybe_validate_user_creds(Resp).
update_user_creds(_Req, UserDoc, _Ctx) ->
@@ -128,35 +137,38 @@ handle_call(reinit_cache, _From, State) ->
self() ! {start_listener, 0},
{reply, ok, State#state{changes_pid = undefined}};
-
handle_call(_Call, _From, State) ->
{noreply, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
-handle_info({'DOWN', _, _, Pid, Reason}, #state{changes_pid=Pid} = State) ->
- Seq = case Reason of
- {seq, EndSeq} ->
- EndSeq;
- {database_does_not_exist, _} ->
- ?LOG_NOTICE(#{
- what => changes_listener_died,
- reason => database_does_not_exist,
- details => "create the _users database to silence this notice"
- }),
- couch_log:notice("~p changes listener died because the _users database does not exist. Create the database to silence this notice.", [?MODULE]),
- 0;
- _ ->
- ?LOG_NOTICE(#{
- what => changes_listener_died,
- reason => Reason
- }),
- couch_log:notice("~p changes listener died ~r", [?MODULE, Reason]),
- 0
- end,
+handle_info({'DOWN', _, _, Pid, Reason}, #state{changes_pid = Pid} = State) ->
+ Seq =
+ case Reason of
+ {seq, EndSeq} ->
+ EndSeq;
+ {database_does_not_exist, _} ->
+ ?LOG_NOTICE(#{
+ what => changes_listener_died,
+ reason => database_does_not_exist,
+ details => "create the _users database to silence this notice"
+ }),
+ couch_log:notice(
+ "~p changes listener died because the _users database does not exist. Create the database to silence this notice.",
+ [?MODULE]
+ ),
+ 0;
+ _ ->
+ ?LOG_NOTICE(#{
+ what => changes_listener_died,
+ reason => Reason
+ }),
+ couch_log:notice("~p changes listener died ~r", [?MODULE, Reason]),
+ 0
+ end,
erlang:send_after(5000, self(), {start_listener, Seq}),
- {noreply, State#state{last_seq=Seq}};
+ {noreply, State#state{last_seq = Seq}};
handle_info({start_listener, Seq}, State) ->
{noreply, State#state{changes_pid = spawn_changes(Seq)}};
handle_info(restart_config_listener, State) ->
@@ -170,7 +182,7 @@ terminate(_Reason, #state{changes_pid = Pid}) when is_pid(Pid) ->
terminate(_Reason, _State) ->
ok.
-code_change(_OldVsn, #state{}=State, _Extra) ->
+code_change(_OldVsn, #state{} = State, _Extra) ->
{ok, State}.
%% private functions
@@ -214,7 +226,6 @@ changes_callback({timeout, _ResponseType}, Acc) ->
changes_callback({error, _}, EndSeq) ->
exit({seq, EndSeq}).
-
handle_config_change("chttpd_auth", "authentication_db", _DbName, _, _) ->
{ok, gen_server:call(?MODULE, reinit_cache, infinity)};
handle_config_change(_, _, _, _, _) ->
@@ -226,36 +237,35 @@ handle_config_terminate(_Server, _Reason, _State) ->
Dst = whereis(?MODULE),
erlang:send_after(?RELISTEN_DELAY, Dst, restart_config_listener).
-
load_user_from_db(UserName) ->
{ok, Db} = fabric2_db:open(dbname(), [?ADMIN_CTX]),
try fabric2_db:open_doc(Db, docid(UserName), [conflicts]) of
- {ok, Doc} ->
- {Props} = couch_doc:to_json_obj(Doc, []),
- Props;
- _Else ->
- ?LOG_DEBUG(#{
- what => missing_user_document,
- user => UserName
- }),
- couch_log:debug("no record of user ~s", [UserName]),
- nil
- catch error:database_does_not_exist ->
- nil
+ {ok, Doc} ->
+ {Props} = couch_doc:to_json_obj(Doc, []),
+ Props;
+ _Else ->
+ ?LOG_DEBUG(#{
+ what => missing_user_document,
+ user => UserName
+ }),
+ couch_log:debug("no record of user ~s", [UserName]),
+ nil
+ catch
+ error:database_does_not_exist ->
+ nil
end.
-
ensure_auth_db() ->
try
fabric2_db:open(dbname(), [?ADMIN_CTX])
- catch error:database_does_not_exist ->
- case fabric2_db:create(dbname(), [?ADMIN_CTX]) of
- {ok, _} -> ok;
- {error, file_exists} -> ok
- end
+ catch
+ error:database_does_not_exist ->
+ case fabric2_db:create(dbname(), [?ADMIN_CTX]) of
+ {ok, _} -> ok;
+ {error, file_exists} -> ok
+ end
end.
-
dbname() ->
DbNameStr = config:get("chttpd_auth", "authentication_db", "_users"),
iolist_to_binary(DbNameStr).
@@ -268,30 +278,37 @@ username(<<"org.couchdb.user:", UserName/binary>>) ->
ensure_auth_ddoc_exists(Db, DDocId) ->
case fabric2_db:open_doc(Db, DDocId) of
- {not_found, _Reason} ->
- {ok, AuthDesign} = couch_auth_cache:auth_design_doc(DDocId),
- update_doc_ignoring_conflict(Db, AuthDesign);
- {ok, Doc} ->
- {Props} = couch_doc:to_json_obj(Doc, []),
- case couch_util:get_value(<<"validate_doc_update">>, Props, []) of
- ?AUTH_DB_DOC_VALIDATE_FUNCTION ->
- ok;
- _ ->
- Props1 = lists:keyreplace(<<"validate_doc_update">>, 1, Props,
- {<<"validate_doc_update">>,
- ?AUTH_DB_DOC_VALIDATE_FUNCTION}),
- NewDoc = couch_doc:from_json_obj({Props1}),
- update_doc_ignoring_conflict(Db, NewDoc)
- end;
- {error, Reason} ->
- ?LOG_NOTICE(#{
- what => ensure_auth_ddoc_exists_failure,
- db => dbname(),
- docid => DDocId,
- details => Reason
- }),
- couch_log:notice("Failed to ensure auth ddoc ~s/~s exists for reason: ~p", [dbname(), DDocId, Reason]),
- ok
+ {not_found, _Reason} ->
+ {ok, AuthDesign} = couch_auth_cache:auth_design_doc(DDocId),
+ update_doc_ignoring_conflict(Db, AuthDesign);
+ {ok, Doc} ->
+ {Props} = couch_doc:to_json_obj(Doc, []),
+ case couch_util:get_value(<<"validate_doc_update">>, Props, []) of
+ ?AUTH_DB_DOC_VALIDATE_FUNCTION ->
+ ok;
+ _ ->
+ Props1 = lists:keyreplace(
+ <<"validate_doc_update">>,
+ 1,
+ Props,
+ {<<"validate_doc_update">>, ?AUTH_DB_DOC_VALIDATE_FUNCTION}
+ ),
+ NewDoc = couch_doc:from_json_obj({Props1}),
+ update_doc_ignoring_conflict(Db, NewDoc)
+ end;
+ {error, Reason} ->
+ ?LOG_NOTICE(#{
+ what => ensure_auth_ddoc_exists_failure,
+ db => dbname(),
+ docid => DDocId,
+ details => Reason
+ }),
+ couch_log:notice("Failed to ensure auth ddoc ~s/~s exists for reason: ~p", [
+ dbname(),
+ DDocId,
+ Reason
+ ]),
+ ok
end,
ok.
@@ -308,15 +325,18 @@ maybe_validate_user_creds(nil) ->
% throws if UserCreds includes a _conflicts member
% returns UserCreds otherwise
maybe_validate_user_creds(UserCreds) ->
- AllowConflictedUserDocs = config:get_boolean("chttpd_auth", "allow_conflicted_user_docs", false),
+ AllowConflictedUserDocs = config:get_boolean(
+ "chttpd_auth", "allow_conflicted_user_docs", false
+ ),
case {couch_util:get_value(<<"_conflicts">>, UserCreds), AllowConflictedUserDocs} of
{undefined, _} ->
{ok, UserCreds, nil};
{_, true} ->
{ok, UserCreds, nil};
{_ConflictList, false} ->
- throw({unauthorized,
- <<"User document conflicts must be resolved before the document",
- " is used for authentication purposes.">>
- })
+ throw(
+ {unauthorized,
+ <<"User document conflicts must be resolved before the document",
+ " is used for authentication purposes.">>}
+ )
end.
diff --git a/src/chttpd/src/chttpd_auth_request.erl b/src/chttpd/src/chttpd_auth_request.erl
index 3f6f976..93275a1 100644
--- a/src/chttpd/src/chttpd_auth_request.erl
+++ b/src/chttpd/src/chttpd_auth_request.erl
@@ -14,104 +14,103 @@
-export([authorize_request/1]).
-include_lib("couch/include/couch_db.hrl").
-authorize_request(#httpd{auth=Auth, user_ctx=Ctx} = Req) ->
+authorize_request(#httpd{auth = Auth, user_ctx = Ctx} = Req) ->
try
- authorize_request_int(Req)
+ authorize_request_int(Req)
catch
- throw:{forbidden, Msg} ->
- case {Auth, Ctx} of
- {{cookie_auth_failed, {Error, Reason}}, _} ->
- throw({forbidden, {Error, Reason}});
- {_, #user_ctx{name=null}} ->
- throw({unauthorized, Msg});
- {_, _} ->
- throw({forbidden, Msg})
- end
+ throw:{forbidden, Msg} ->
+ case {Auth, Ctx} of
+ {{cookie_auth_failed, {Error, Reason}}, _} ->
+ throw({forbidden, {Error, Reason}});
+ {_, #user_ctx{name = null}} ->
+ throw({unauthorized, Msg});
+ {_, _} ->
+ throw({forbidden, Msg})
+ end
end.
-authorize_request_int(#httpd{path_parts=[]}=Req) ->
+authorize_request_int(#httpd{path_parts = []} = Req) ->
Req;
-authorize_request_int(#httpd{path_parts=[<<"favicon.ico">>|_]}=Req) ->
+authorize_request_int(#httpd{path_parts = [<<"favicon.ico">> | _]} = Req) ->
Req;
-authorize_request_int(#httpd{path_parts=[<<"_all_dbs">>|_]}=Req) ->
- case config:get_boolean("chttpd", "admin_only_all_dbs", true) of
- true -> require_admin(Req);
- false -> Req
- end;
-authorize_request_int(#httpd{path_parts=[<<"_dbs_info">>|_]}=Req) ->
+authorize_request_int(#httpd{path_parts = [<<"_all_dbs">> | _]} = Req) ->
+ case config:get_boolean("chttpd", "admin_only_all_dbs", true) of
+ true -> require_admin(Req);
+ false -> Req
+ end;
+authorize_request_int(#httpd{path_parts = [<<"_dbs_info">> | _]} = Req) ->
Req;
-authorize_request_int(#httpd{path_parts=[<<"_replicator">>], method='PUT'}=Req) ->
+authorize_request_int(#httpd{path_parts = [<<"_replicator">>], method = 'PUT'} = Req) ->
require_admin(Req);
-authorize_request_int(#httpd{path_parts=[<<"_replicator">>], method='DELETE'}=Req) ->
+authorize_request_int(#httpd{path_parts = [<<"_replicator">>], method = 'DELETE'} = Req) ->
require_admin(Req);
-authorize_request_int(#httpd{path_parts=[<<"_replicator">>,<<"_all_docs">>|_]}=Req) ->
+authorize_request_int(#httpd{path_parts = [<<"_replicator">>, <<"_all_docs">> | _]} = Req) ->
require_admin(Req);
-authorize_request_int(#httpd{path_parts=[<<"_replicator">>,<<"_changes">>|_]}=Req) ->
+authorize_request_int(#httpd{path_parts = [<<"_replicator">>, <<"_changes">> | _]} = Req) ->
require_admin(Req);
-authorize_request_int(#httpd{path_parts=[<<"_replicator">>|_]}=Req) ->
+authorize_request_int(#httpd{path_parts = [<<"_replicator">> | _]} = Req) ->
db_authorization_check(Req);
-authorize_request_int(#httpd{path_parts=[<<"_reshard">>|_]}=Req) ->
+authorize_request_int(#httpd{path_parts = [<<"_reshard">> | _]} = Req) ->
require_admin(Req);
-authorize_request_int(#httpd{path_parts=[<<"_users">>], method='PUT'}=Req) ->
+authorize_request_int(#httpd{path_parts = [<<"_users">>], method = 'PUT'} = Req) ->
require_admin(Req);
-authorize_request_int(#httpd{path_parts=[<<"_users">>], method='DELETE'}=Req) ->
+authorize_request_int(#httpd{path_parts = [<<"_users">>], method = 'DELETE'} = Req) ->
require_admin(Req);
-authorize_request_int(#httpd{path_parts=[<<"_users">>,<<"_all_docs">>|_]}=Req) ->
+authorize_request_int(#httpd{path_parts = [<<"_users">>, <<"_all_docs">> | _]} = Req) ->
require_admin(Req);
-authorize_request_int(#httpd{path_parts=[<<"_users">>,<<"_changes">>|_]}=Req) ->
+authorize_request_int(#httpd{path_parts = [<<"_users">>, <<"_changes">> | _]} = Req) ->
require_admin(Req);
-authorize_request_int(#httpd{path_parts=[<<"_users">>|_]}=Req) ->
+authorize_request_int(#httpd{path_parts = [<<"_users">> | _]} = Req) ->
db_authorization_check(Req);
-authorize_request_int(#httpd{path_parts=[<<"_", _/binary>>|_]}=Req) ->
+authorize_request_int(#httpd{path_parts = [<<"_", _/binary>> | _]} = Req) ->
server_authorization_check(Req);
-authorize_request_int(#httpd{path_parts=[_DbName], method='PUT'}=Req) ->
+authorize_request_int(#httpd{path_parts = [_DbName], method = 'PUT'} = Req) ->
require_admin(Req);
-authorize_request_int(#httpd{path_parts=[_DbName], method='DELETE'}=Req) ->
+authorize_request_int(#httpd{path_parts = [_DbName], method = 'DELETE'} = Req) ->
require_admin(Req);
-authorize_request_int(#httpd{path_parts=[_DbName, <<"_compact">>|_]}=Req) ->
+authorize_request_int(#httpd{path_parts = [_DbName, <<"_compact">> | _]} = Req) ->
require_db_admin(Req);
-authorize_request_int(#httpd{path_parts=[_DbName, <<"_view_cleanup">>]}=Req) ->
+authorize_request_int(#httpd{path_parts = [_DbName, <<"_view_cleanup">>]} = Req) ->
require_db_admin(Req);
-authorize_request_int(#httpd{path_parts=[_DbName, <<"_sync_shards">>]}=Req) ->
+authorize_request_int(#httpd{path_parts = [_DbName, <<"_sync_shards">>]} = Req) ->
require_admin(Req);
-authorize_request_int(#httpd{path_parts=[_DbName, <<"_purge">>]}=Req) ->
+authorize_request_int(#httpd{path_parts = [_DbName, <<"_purge">>]} = Req) ->
require_admin(Req);
-authorize_request_int(#httpd{path_parts=[_DbName, <<"_purged_infos_limit">>]}=Req) ->
+authorize_request_int(#httpd{path_parts = [_DbName, <<"_purged_infos_limit">>]} = Req) ->
require_admin(Req);
-authorize_request_int(#httpd{path_parts=[_DbName|_]}=Req) ->
+authorize_request_int(#httpd{path_parts = [_DbName | _]} = Req) ->
db_authorization_check(Req).
-
-server_authorization_check(#httpd{path_parts=[<<"_up">>]}=Req) ->
+server_authorization_check(#httpd{path_parts = [<<"_up">>]} = Req) ->
Req;
-server_authorization_check(#httpd{path_parts=[<<"_uuids">>]}=Req) ->
+server_authorization_check(#httpd{path_parts = [<<"_uuids">>]} = Req) ->
Req;
-server_authorization_check(#httpd{path_parts=[<<"_session">>]}=Req) ->
+server_authorization_check(#httpd{path_parts = [<<"_session">>]} = Req) ->
Req;
-server_authorization_check(#httpd{path_parts=[<<"_replicate">>]}=Req) ->
+server_authorization_check(#httpd{path_parts = [<<"_replicate">>]} = Req) ->
Req;
-server_authorization_check(#httpd{path_parts=[<<"_stats">>]}=Req) ->
+server_authorization_check(#httpd{path_parts = [<<"_stats">>]} = Req) ->
Req;
-server_authorization_check(#httpd{path_parts=[<<"_active_tasks">>]}=Req) ->
+server_authorization_check(#httpd{path_parts = [<<"_active_tasks">>]} = Req) ->
Req;
-server_authorization_check(#httpd{path_parts=[<<"_dbs_info">>]}=Req) ->
+server_authorization_check(#httpd{path_parts = [<<"_dbs_info">>]} = Req) ->
Req;
-server_authorization_check(#httpd{method=Method, path_parts=[<<"_utils">>|_]}=Req)
- when Method =:= 'HEAD' orelse Method =:= 'GET' ->
+server_authorization_check(#httpd{method = Method, path_parts = [<<"_utils">> | _]} = Req) when
+ Method =:= 'HEAD' orelse Method =:= 'GET'
+->
Req;
-server_authorization_check(#httpd{path_parts=[<<"_node">>,_ , <<"_stats">>|_]}=Req) ->
+server_authorization_check(#httpd{path_parts = [<<"_node">>, _, <<"_stats">> | _]} = Req) ->
require_metrics(Req);
-server_authorization_check(#httpd{path_parts=[<<"_node">>,_ , <<"_system">>|_]}=Req) ->
+server_authorization_check(#httpd{path_parts = [<<"_node">>, _, <<"_system">> | _]} = Req) ->
require_metrics(Req);
-server_authorization_check(#httpd{path_parts=[<<"_", _/binary>>|_]}=Req) ->
+server_authorization_check(#httpd{path_parts = [<<"_", _/binary>> | _]} = Req) ->
require_admin(Req).
-db_authorization_check(#httpd{path_parts=[_DbName|_]}=Req) ->
+db_authorization_check(#httpd{path_parts = [_DbName | _]} = Req) ->
% Db authorization checks are performed in fabric before every FDB operation
Req.
-
-require_metrics(#httpd{user_ctx=#user_ctx{roles=UserRoles}}=Req) ->
+require_metrics(#httpd{user_ctx = #user_ctx{roles = UserRoles}} = Req) ->
IsAdmin = lists:member(<<"_admin">>, UserRoles),
IsMetrics = lists:member(<<"_metrics">>, UserRoles),
case {IsAdmin, IsMetrics} of
@@ -124,15 +123,15 @@ require_admin(Req) ->
ok = couch_httpd:verify_is_server_admin(Req),
Req.
-require_db_admin(#httpd{path_parts=[DbName|_],user_ctx=Ctx}=Req) ->
+require_db_admin(#httpd{path_parts = [DbName | _], user_ctx = Ctx} = Req) ->
{ok, Db} = fabric2_db:open(DbName, [{user_ctx, Ctx}]),
Sec = fabric2_db:get_security(Db),
- case is_db_admin(Ctx,Sec) of
+ case is_db_admin(Ctx, Sec) of
true -> Req;
- false -> throw({unauthorized, <<"You are not a server or db admin.">>})
+ false -> throw({unauthorized, <<"You are not a server or db admin.">>})
end.
-is_db_admin(#user_ctx{name=UserName,roles=UserRoles}, {Security}) ->
+is_db_admin(#user_ctx{name = UserName, roles = UserRoles}, {Security}) ->
{Admins} = couch_util:get_value(<<"admins">>, Security, {[]}),
Names = couch_util:get_value(<<"names">>, Admins, []),
Roles = couch_util:get_value(<<"roles">>, Admins, []),
diff --git a/src/chttpd/src/chttpd_changes.erl b/src/chttpd/src/chttpd_changes.erl
index 29ead3d..0d3ce39 100644
--- a/src/chttpd/src/chttpd_changes.erl
+++ b/src/chttpd/src/chttpd_changes.erl
@@ -65,64 +65,82 @@ handle_db_changes(Args0, Req, Db0) ->
DbName = fabric2_db:name(Db0),
StartListenerFun = fun() ->
fabric2_events:link_listener(
- ?MODULE, handle_db_event, self(), [{dbname, DbName}]
- )
+ ?MODULE, handle_db_event, self(), [{dbname, DbName}]
+ )
end,
Start = fun() ->
- StartSeq = case Dir =:= rev orelse Since =:= now of
- true -> fabric2_db:get_update_seq(Db0);
- false -> Since
- end,
+ StartSeq =
+ case Dir =:= rev orelse Since =:= now of
+ true -> fabric2_db:get_update_seq(Db0);
+ false -> Since
+ end,
{Db0, StartSeq}
end,
% begin timer to deal with heartbeat when filter function fails
case Args#changes_args.heartbeat of
- undefined ->
- erlang:erase(last_changes_heartbeat);
- Val when is_integer(Val); Val =:= true ->
- put(last_changes_heartbeat, os:timestamp())
+ undefined ->
+ erlang:erase(last_changes_heartbeat);
+ Val when is_integer(Val); Val =:= true ->
+ put(last_changes_heartbeat, os:timestamp())
end,
case lists:member(Feed, ["continuous", "longpoll", "eventsource"]) of
- true ->
- fun(CallbackAcc) ->
- {Callback, UserAcc} = get_callback_acc(CallbackAcc),
- {ok, Listener} = StartListenerFun(),
-
- {Db, StartSeq} = Start(),
- UserAcc2 = start_sending_changes(Callback, UserAcc),
- {Timeout, TimeoutFun} = get_changes_timeout(Args, Callback),
- Acc0 = build_acc(Args, Callback, UserAcc2, Db, StartSeq,
- <<"">>, Timeout, TimeoutFun),
- try
- keep_sending_changes(
- Args#changes_args{dir=fwd},
- Acc0,
- true)
- after
- fabric2_events:stop_listener(Listener),
- get_rest_updated(ok) % clean out any remaining update messages
+ true ->
+ fun(CallbackAcc) ->
+ {Callback, UserAcc} = get_callback_acc(CallbackAcc),
+ {ok, Listener} = StartListenerFun(),
+
+ {Db, StartSeq} = Start(),
+ UserAcc2 = start_sending_changes(Callback, UserAcc),
+ {Timeout, TimeoutFun} = get_changes_timeout(Args, Callback),
+ Acc0 = build_acc(
+ Args,
+ Callback,
+ UserAcc2,
+ Db,
+ StartSeq,
+ <<"">>,
+ Timeout,
+ TimeoutFun
+ ),
+ try
+ keep_sending_changes(
+ Args#changes_args{dir = fwd},
+ Acc0,
+ true
+ )
+ after
+ fabric2_events:stop_listener(Listener),
+ % clean out any remaining update messages
+ get_rest_updated(ok)
+ end
+ end;
+ false ->
+ fun(CallbackAcc) ->
+ {Callback, UserAcc} = get_callback_acc(CallbackAcc),
+ UserAcc2 = start_sending_changes(Callback, UserAcc),
+ {Timeout, TimeoutFun} = get_changes_timeout(Args, Callback),
+ {Db, StartSeq} = Start(),
+ Acc0 = build_acc(
+ Args#changes_args{feed = "normal"},
+ Callback,
+ UserAcc2,
+ Db,
+ StartSeq,
+ <<>>,
+ Timeout,
+ TimeoutFun
+ ),
+ {ok, #changes_acc{seq = LastSeq, user_acc = UserAcc3}} =
+ send_changes(
+ Acc0,
+ Dir,
+ true
+ ),
+ end_sending_changes(Callback, UserAcc3, LastSeq)
end
- end;
- false ->
- fun(CallbackAcc) ->
- {Callback, UserAcc} = get_callback_acc(CallbackAcc),
- UserAcc2 = start_sending_changes(Callback, UserAcc),
- {Timeout, TimeoutFun} = get_changes_timeout(Args, Callback),
- {Db, StartSeq} = Start(),
- Acc0 = build_acc(Args#changes_args{feed="normal"}, Callback,
- UserAcc2, Db, StartSeq, <<>>,
- Timeout, TimeoutFun),
- {ok, #changes_acc{seq = LastSeq, user_acc = UserAcc3}} =
- send_changes(
- Acc0,
- Dir,
- true),
- end_sending_changes(Callback, UserAcc3, LastSeq)
- end
end.
-
handle_db_event(_DbName, updated, Parent) ->
Parent ! updated,
{ok, Parent};
@@ -132,7 +150,6 @@ handle_db_event(_DbName, deleted, Parent) ->
handle_db_event(_DbName, _Event, Parent) ->
{ok, Parent}.
-
handle_view_event(_DbName, Msg, {Parent, DDocId}) ->
case Msg of
{index_commit, DDocId} ->
@@ -149,20 +166,20 @@ get_callback_acc({Callback, _UserAcc} = Pair) when is_function(Callback, 2) ->
get_callback_acc(Callback) when is_function(Callback, 1) ->
{fun(Ev, _) -> Callback(Ev) end, ok}.
-
configure_filter(Filter, _Style, _Req, _Db) when is_tuple(Filter) ->
% Filter has already been configured
Filter;
configure_filter("_doc_ids", Style, Req, _Db) ->
{doc_ids, Style, get_doc_ids(Req)};
configure_filter("_selector", Style, Req, _Db) ->
- {selector, Style, get_selector_and_fields(Req)};
+ {selector, Style, get_selector_and_fields(Req)};
configure_filter("_design", Style, _Req, _Db) ->
{design_docs, Style};
configure_filter("_view", Style, Req, Db) ->
ViewName = get_view_qs(Req),
- if ViewName /= "" -> ok; true ->
- throw({bad_request, "`view` filter parameter is not provided."})
+ if
+ ViewName /= "" -> ok;
+ true -> throw({bad_request, "`view` filter parameter is not provided."})
end,
ViewNameParts = string:tokens(ViewName, "/"),
case [?l2b(couch_httpd:unquote(Part)) || Part <- ViewNameParts] of
@@ -200,7 +217,6 @@ configure_filter(FilterName, Style, Req, Db) ->
throw({bad_request, Msg})
end.
-
filter(Db, Change, {default, Style}) ->
apply_style(Db, Change, Style);
filter(Db, Change, {doc_ids, Style, DocIds}) ->
@@ -212,8 +228,10 @@ filter(Db, Change, {doc_ids, Style, DocIds}) ->
end;
filter(Db, Change, {selector, Style, {Selector, _Fields}}) ->
Docs = open_revs(Db, Change, Style),
- Passes = [mango_selector:match(Selector, couch_doc:to_json_obj(Doc, []))
- || Doc <- Docs],
+ Passes = [
+ mango_selector:match(Selector, couch_doc:to_json_obj(Doc, []))
+ || Doc <- Docs
+ ],
filter_revs(Passes, Docs);
filter(Db, Change, {design_docs, Style}) ->
case maps:get(id, Change) of
@@ -227,17 +245,17 @@ filter(Db, Change, {view, Style, DDoc, VName}) ->
{ok, Passes} = couch_query_servers:filter_view(DDoc, VName, Docs),
filter_revs(Passes, Docs);
filter(Db, Change, {custom, Style, Req0, DDoc, FName}) ->
- Req = case Req0 of
- {json_req, _} -> Req0;
- #httpd{} -> {json_req, chttpd_external:json_req_obj(Req0, Db)}
- end,
+ Req =
+ case Req0 of
+ {json_req, _} -> Req0;
+ #httpd{} -> {json_req, chttpd_external:json_req_obj(Req0, Db)}
+ end,
Docs = open_revs(Db, Change, Style),
{ok, Passes} = couch_query_servers:filter_docs(Req, Db, DDoc, FName, Docs),
filter_revs(Passes, Docs);
filter(Db, Change, Filter) ->
erlang:error({filter_error, Db, Change, Filter}).
-
get_view_qs({json_req, {Props}}) ->
{Query} = couch_util:get_value(<<"query">>, Props, {[]}),
binary_to_list(couch_util:get_value(<<"view">>, Query, ""));
@@ -246,42 +264,43 @@ get_view_qs(Req) ->
get_doc_ids({json_req, {Props}}) ->
check_docids(couch_util:get_value(<<"doc_ids">>, Props));
-get_doc_ids(#httpd{method='POST'}=Req) ->
+get_doc_ids(#httpd{method = 'POST'} = Req) ->
couch_httpd:validate_ctype(Req, "application/json"),
{Props} = couch_httpd:json_body_obj(Req),
check_docids(couch_util:get_value(<<"doc_ids">>, Props));
-get_doc_ids(#httpd{method='GET'}=Req) ->
+get_doc_ids(#httpd{method = 'GET'} = Req) ->
DocIds = ?JSON_DECODE(couch_httpd:qs_value(Req, "doc_ids", "null")),
check_docids(DocIds);
get_doc_ids(_) ->
throw({bad_request, no_doc_ids_provided}).
-
get_selector_and_fields({json_req, {Props}}) ->
Selector = check_selector(couch_util:get_value(<<"selector">>, Props)),
Fields = check_fields(couch_util:get_value(<<"fields">>, Props, nil)),
{Selector, Fields};
-get_selector_and_fields(#httpd{method='POST'}=Req) ->
+get_selector_and_fields(#httpd{method = 'POST'} = Req) ->
couch_httpd:validate_ctype(Req, "application/json"),
- get_selector_and_fields({json_req, couch_httpd:json_body_obj(Req)});
+ get_selector_and_fields({json_req, couch_httpd:json_body_obj(Req)});
get_selector_and_fields(_) ->
throw({bad_request, "Selector must be specified in POST payload"}).
-
check_docids(DocIds) when is_list(DocIds) ->
- lists:foreach(fun
- (DocId) when not is_binary(DocId) ->
- Msg = "`doc_ids` filter parameter is not a list of doc ids.",
- throw({bad_request, Msg});
- (_) -> ok
- end, DocIds),
+ lists:foreach(
+ fun
+ (DocId) when not is_binary(DocId) ->
+ Msg = "`doc_ids` filter parameter is not a list of doc ids.",
+ throw({bad_request, Msg});
+ (_) ->
+ ok
+ end,
+ DocIds
+ ),
DocIds;
check_docids(_) ->
Msg = "`doc_ids` filter parameter is not a list of doc ids.",
throw({bad_request, Msg}).
-
-check_selector(Selector={_}) ->
+check_selector(Selector = {_}) ->
try
mango_selector:normalize(Selector)
catch
@@ -292,7 +311,6 @@ check_selector(Selector={_}) ->
check_selector(_Selector) ->
throw({bad_request, "Selector error: expected a JSON object"}).
-
check_fields(nil) ->
nil;
check_fields(Fields) when is_list(Fields) ->
@@ -307,18 +325,15 @@ check_fields(Fields) when is_list(Fields) ->
check_fields(_Fields) ->
throw({bad_request, "Selector error: fields must be JSON array"}).
-
open_ddoc(Db, DDocId) ->
case fabric2_db:open_doc(Db, DDocId, [ejson_body, ?ADMIN_CTX]) of
{ok, _} = Resp -> Resp;
Else -> throw(Else)
end.
-
-check_member_exists(#doc{body={Props}}, Path) ->
+check_member_exists(#doc{body = {Props}}, Path) ->
couch_util:get_nested_json_value({Props}, Path).
-
apply_style(_Db, Change, main_only) ->
#{rev_id := RevId} = Change,
[{[{<<"rev">>, couch_doc:rev_to_str(RevId)}]}];
@@ -326,18 +341,20 @@ apply_style(Db, Change, all_docs) ->
% We have to fetch all revs for this row
#{id := DocId} = Change,
{ok, Resps} = fabric2_db:open_doc_revs(Db, DocId, all, [deleted]),
- lists:flatmap(fun(Resp) ->
- case Resp of
- {ok, #doc{revs = {Pos, [Rev | _]}}} ->
- [{[{<<"rev">>, couch_doc:rev_to_str({Pos, Rev})}]}];
- _ ->
- []
- end
- end, Resps);
+ lists:flatmap(
+ fun(Resp) ->
+ case Resp of
+ {ok, #doc{revs = {Pos, [Rev | _]}}} ->
+ [{[{<<"rev">>, couch_doc:rev_to_str({Pos, Rev})}]}];
+ _ ->
+ []
+ end
+ end,
+ Resps
+ );
apply_style(Db, Change, Style) ->
erlang:error({changes_apply_style, Db, Change, Style}).
-
open_revs(Db, Change, Style) ->
#{id := DocId} = Change,
Options = [deleted, conflicts],
@@ -350,22 +367,24 @@ open_revs(Db, Change, Style) ->
{ok, Docs} = fabric2_db:open_doc_revs(Db, DocId, all, Options),
[Doc || {ok, Doc} <- Docs]
end
- catch _:_ ->
- % We didn't log this before, should we now?
- []
+ catch
+ _:_ ->
+ % We didn't log this before, should we now?
+ []
end.
-
filter_revs(Passes, Docs) ->
- lists:flatmap(fun
- ({true, #doc{revs={RevPos, [RevId | _]}}}) ->
- RevStr = couch_doc:rev_to_str({RevPos, RevId}),
- Change = {[{<<"rev">>, RevStr}]},
- [Change];
- (_) ->
- []
- end, lists:zip(Passes, Docs)).
-
+ lists:flatmap(
+ fun
+ ({true, #doc{revs = {RevPos, [RevId | _]}}}) ->
+ RevStr = couch_doc:rev_to_str({RevPos, RevId}),
+ Change = {[{<<"rev">>, RevStr}]},
+ [Change];
+ (_) ->
+ []
+ end,
+ lists:zip(Passes, Docs)
+ ).
get_changes_timeout(Args, Callback) ->
#changes_args{
@@ -374,24 +393,24 @@ get_changes_timeout(Args, Callback) ->
feed = ResponseType
} = Args,
DefaultTimeout = chttpd_util:get_chttpd_config_integer(
- "changes_timeout", 60000),
+ "changes_timeout", 60000
+ ),
case Heartbeat of
- undefined ->
- case Timeout of
undefined ->
- {DefaultTimeout, fun(UserAcc) -> {stop, UserAcc} end};
- infinity ->
- {infinity, fun(UserAcc) -> {stop, UserAcc} end};
+ case Timeout of
+ undefined ->
+ {DefaultTimeout, fun(UserAcc) -> {stop, UserAcc} end};
+ infinity ->
+ {infinity, fun(UserAcc) -> {stop, UserAcc} end};
+ _ ->
+ {lists:min([DefaultTimeout, Timeout]), fun(UserAcc) -> {stop, UserAcc} end}
+ end;
+ true ->
+ {DefaultTimeout, fun(UserAcc) -> Callback({timeout, ResponseType}, UserAcc) end};
_ ->
- {lists:min([DefaultTimeout, Timeout]),
- fun(UserAcc) -> {stop, UserAcc} end}
- end;
- true ->
- {DefaultTimeout,
- fun(UserAcc) -> Callback({timeout, ResponseType}, UserAcc) end};
- _ ->
- {lists:min([DefaultTimeout, Heartbeat]),
- fun(UserAcc) -> Callback({timeout, ResponseType}, UserAcc) end}
+ {lists:min([DefaultTimeout, Heartbeat]), fun(UserAcc) ->
+ Callback({timeout, ResponseType}, UserAcc)
+ end}
end.
start_sending_changes(Callback, UserAcc) ->
@@ -421,8 +440,8 @@ build_acc(Args, Callback, UserAcc, Db, StartSeq, Prepend, Timeout, TimeoutFun) -
conflicts = Conflicts,
timeout = Timeout,
timeout_fun = TimeoutFun,
- aggregation_results=[],
- aggregation_kvs=[]
+ aggregation_results = [],
+ aggregation_kvs = []
}.
send_changes(Acc, Dir, FirstRound) ->
@@ -440,37 +459,43 @@ send_changes(Acc, Dir, FirstRound) ->
fabric2_db:fold_changes(Db, StartSeq, DbEnumFun, Acc, Opts)
end.
-
can_optimize(true, {doc_ids, _Style, DocIds}) ->
- MaxDocIds = config:get_integer("couchdb",
- "changes_doc_ids_optimization_threshold", 100),
- if length(DocIds) =< MaxDocIds ->
- {true, fun send_changes_doc_ids/6};
- true ->
- false
+ MaxDocIds = config:get_integer(
+ "couchdb",
+ "changes_doc_ids_optimization_threshold",
+ 100
+ ),
+ if
+ length(DocIds) =< MaxDocIds ->
+ {true, fun send_changes_doc_ids/6};
+ true ->
+ false
end;
can_optimize(true, {design_docs, _Style}) ->
{true, fun send_changes_design_docs/6};
can_optimize(_, _) ->
false.
-
send_changes_doc_ids(Db, StartSeq, Dir, Fun, Acc0, {doc_ids, _Style, DocIds}) ->
Results = fabric2_db:get_full_doc_infos(Db, DocIds),
- FullInfos = lists:foldl(fun
- (#full_doc_info{}=FDI, Acc) -> [FDI | Acc];
- (not_found, Acc) -> Acc
- end, [], Results),
+ FullInfos = lists:foldl(
+ fun
+ (#full_doc_info{} = FDI, Acc) -> [FDI | Acc];
+ (not_found, Acc) -> Acc
+ end,
+ [],
+ Results
+ ),
send_lookup_changes(FullInfos, StartSeq, Dir, Db, Fun, Acc0).
-
send_changes_design_docs(Db, StartSeq, Dir, Fun, Acc0, {design_docs, _Style}) ->
FoldFun = fun(FDI, Acc) ->
case FDI of
{row, Row} ->
DocId = proplists:get_value(id, Row),
{ok, [fabric2_db:get_full_doc_info(Db, DocId) | Acc]};
- _ -> {ok, Acc}
+ _ ->
+ {ok, Acc}
end
end,
Opts = [
@@ -481,61 +506,73 @@ send_changes_design_docs(Db, StartSeq, Dir, Fun, Acc0, {design_docs, _Style}) ->
{ok, FullInfos} = fabric2_db:fold_docs(Db, FoldFun, [], Opts),
send_lookup_changes(FullInfos, StartSeq, Dir, Db, Fun, Acc0).
-
send_lookup_changes(FullDocInfos, StartSeq, Dir, Db, Fun, Acc0) ->
- FoldFun = case Dir of
- fwd -> fun lists:foldl/3;
- rev -> fun lists:foldr/3
- end,
- GreaterFun = case Dir of
- fwd -> fun(A, B) -> A > B end;
- rev -> fun(A, B) -> A =< B end
- end,
- DocInfos = lists:foldl(fun(FDI, Acc) ->
- DI = couch_doc:to_doc_info(FDI),
- case GreaterFun(DI#doc_info.high_seq, StartSeq) of
- true -> [DI | Acc];
- false -> Acc
- end
- end, [], FullDocInfos),
- SortedDocInfos = lists:keysort(#doc_info.high_seq, DocInfos),
- FinalAcc = try
- FoldFun(fun(DocInfo, Acc) ->
- % Kinda gross that we're munging this back to a map
- % that will then have to re-read and rebuild the FDI
- % for all_docs style. But c'est la vie.
- #doc_info{
- id = DocId,
- high_seq = Seq,
- revs = [#rev_info{rev = Rev, deleted = Deleted} | _]
- } = DocInfo,
- Change = #{
- id => DocId,
- sequence => Seq,
- rev_id => Rev,
- deleted => Deleted
- },
- case Fun(Change, Acc) of
- {ok, NewAcc} ->
- NewAcc;
- {stop, NewAcc} ->
- throw({stop, NewAcc})
+ FoldFun =
+ case Dir of
+ fwd -> fun lists:foldl/3;
+ rev -> fun lists:foldr/3
+ end,
+ GreaterFun =
+ case Dir of
+ fwd -> fun(A, B) -> A > B end;
+ rev -> fun(A, B) -> A =< B end
+ end,
+ DocInfos = lists:foldl(
+ fun(FDI, Acc) ->
+ DI = couch_doc:to_doc_info(FDI),
+ case GreaterFun(DI#doc_info.high_seq, StartSeq) of
+ true -> [DI | Acc];
+ false -> Acc
end
- end, Acc0, SortedDocInfos)
- catch
- {stop, Acc} -> Acc
- end,
+ end,
+ [],
+ FullDocInfos
+ ),
+ SortedDocInfos = lists:keysort(#doc_info.high_seq, DocInfos),
+ FinalAcc =
+ try
+ FoldFun(
+ fun(DocInfo, Acc) ->
+ % Kinda gross that we're munging this back to a map
+ % that will then have to re-read and rebuild the FDI
+ % for all_docs style. But c'est la vie.
+ #doc_info{
+ id = DocId,
+ high_seq = Seq,
+ revs = [#rev_info{rev = Rev, deleted = Deleted} | _]
+ } = DocInfo,
+ Change = #{
+ id => DocId,
+ sequence => Seq,
+ rev_id => Rev,
+ deleted => Deleted
+ },
+ case Fun(Change, Acc) of
+ {ok, NewAcc} ->
+ NewAcc;
+ {stop, NewAcc} ->
+ throw({stop, NewAcc})
+ end
+ end,
+ Acc0,
+ SortedDocInfos
+ )
+ catch
+ {stop, Acc} -> Acc
+ end,
case Dir of
fwd ->
- FinalAcc0 = case element(1, FinalAcc) of
- changes_acc -> % we came here via couch_http or internal call
- FinalAcc#changes_acc{seq = fabric2_db:get_update_seq(Db)}
- end,
+ FinalAcc0 =
+ case element(1, FinalAcc) of
+ % we came here via couch_http or internal call
+ changes_acc ->
+ FinalAcc#changes_acc{seq = fabric2_db:get_update_seq(Db)}
+ end,
{ok, FinalAcc0};
- rev -> {ok, FinalAcc}
+ rev ->
+ {ok, FinalAcc}
end.
-
keep_sending_changes(Args, Acc0, FirstRound) ->
#changes_args{
feed = ResponseType,
@@ -546,39 +583,50 @@ keep_sending_changes(Args, Acc0, FirstRound) ->
{ok, ChangesAcc} = send_changes(Acc0, fwd, FirstRound),
#changes_acc{
- db = Db, callback = Callback,
- timeout = Timeout, timeout_fun = TimeoutFun, seq = EndSeq,
- prepend = Prepend2, user_acc = UserAcc2, limit = NewLimit
+ db = Db,
+ callback = Callback,
+ timeout = Timeout,
+ timeout_fun = TimeoutFun,
+ seq = EndSeq,
+ prepend = Prepend2,
+ user_acc = UserAcc2,
+ limit = NewLimit
} = maybe_upgrade_changes_acc(ChangesAcc),
- if Limit > NewLimit, ResponseType == "longpoll" ->
- end_sending_changes(Callback, UserAcc2, EndSeq);
- true ->
- {Go, UserAcc3} = notify_waiting_for_updates(Callback, UserAcc2),
- if Go /= ok -> end_sending_changes(Callback, UserAcc3, EndSeq); true ->
- case wait_updated(Timeout, TimeoutFun, UserAcc3) of
- {updated, UserAcc4} ->
- UserCtx = fabric2_db:get_user_ctx(Db),
- DbOptions1 = [{user_ctx, UserCtx} | DbOptions],
- case fabric2_db:open(fabric2_db:name(Db), DbOptions1) of
- {ok, Db2} ->
- ?MODULE:keep_sending_changes(
- Args#changes_args{limit=NewLimit},
- ChangesAcc#changes_acc{
- db = Db2,
- user_acc = UserAcc4,
- seq = EndSeq,
- prepend = Prepend2,
- timeout = Timeout,
- timeout_fun = TimeoutFun},
- false);
- _Else ->
- end_sending_changes(Callback, UserAcc3, EndSeq)
- end;
- {stop, UserAcc4} ->
- end_sending_changes(Callback, UserAcc4, EndSeq)
+ if
+ Limit > NewLimit, ResponseType == "longpoll" ->
+ end_sending_changes(Callback, UserAcc2, EndSeq);
+ true ->
+ {Go, UserAcc3} = notify_waiting_for_updates(Callback, UserAcc2),
+ if
+ Go /= ok ->
+ end_sending_changes(Callback, UserAcc3, EndSeq);
+ true ->
+ case wait_updated(Timeout, TimeoutFun, UserAcc3) of
+ {updated, UserAcc4} ->
+ UserCtx = fabric2_db:get_user_ctx(Db),
+ DbOptions1 = [{user_ctx, UserCtx} | DbOptions],
+ case fabric2_db:open(fabric2_db:name(Db), DbOptions1) of
+ {ok, Db2} ->
+ ?MODULE:keep_sending_changes(
+ Args#changes_args{limit = NewLimit},
+ ChangesAcc#changes_acc{
+ db = Db2,
+ user_acc = UserAcc4,
+ seq = EndSeq,
+ prepend = Prepend2,
+ timeout = Timeout,
+ timeout_fun = TimeoutFun
+ },
+ false
+ );
+ _Else ->
+ end_sending_changes(Callback, UserAcc3, EndSeq)
+ end;
+ {stop, UserAcc4} ->
+ end_sending_changes(Callback, UserAcc4, EndSeq)
+ end
end
- end
end.
notify_waiting_for_updates(Callback, UserAcc) ->
@@ -600,65 +648,71 @@ changes_enumerator(Change, Acc) ->
Results0 = filter(Db, Change, Filter),
Results = [Result || Result <- Results0, Result /= null],
Seq = maps:get(sequence, Change),
- Go = if (Limit =< 1) andalso Results =/= [] -> stop; true -> ok end,
- case Results of
- [] ->
- {Done, UserAcc2} = maybe_heartbeat(Timeout, TimeoutFun, UserAcc),
- case Done of
- stop ->
- {stop, Acc#changes_acc{seq = Seq, user_acc = UserAcc2}};
- ok ->
- {Go, Acc#changes_acc{seq = Seq, user_acc = UserAcc2}}
- end;
- _ ->
- ChangesRow = changes_row(Results, Change, Acc),
- {UserGo, UserAcc2} = Callback({change, ChangesRow}, UserAcc),
- RealGo = case UserGo of
- ok -> Go;
- stop -> stop
+ Go =
+ if
+ (Limit =< 1) andalso Results =/= [] -> stop;
+ true -> ok
end,
- reset_heartbeat(),
- {RealGo, Acc#changes_acc{
- seq = Seq,
- user_acc = UserAcc2,
- limit = Limit - 1
- }}
+ case Results of
+ [] ->
+ {Done, UserAcc2} = maybe_heartbeat(Timeout, TimeoutFun, UserAcc),
+ case Done of
+ stop ->
+ {stop, Acc#changes_acc{seq = Seq, user_acc = UserAcc2}};
+ ok ->
+ {Go, Acc#changes_acc{seq = Seq, user_acc = UserAcc2}}
+ end;
+ _ ->
+ ChangesRow = changes_row(Results, Change, Acc),
+ {UserGo, UserAcc2} = Callback({change, ChangesRow}, UserAcc),
+ RealGo =
+ case UserGo of
+ ok -> Go;
+ stop -> stop
+ end,
+ reset_heartbeat(),
+ {RealGo, Acc#changes_acc{
+ seq = Seq,
+ user_acc = UserAcc2,
+ limit = Limit - 1
+ }}
end.
-
changes_row(Results, Change, Acc) ->
#{
id := Id,
sequence := Seq,
deleted := Del
} = Change,
- {[
- {<<"seq">>, Seq},
- {<<"id">>, Id},
- {<<"changes">>, Results}
- ] ++ deleted_item(Del) ++ maybe_get_changes_doc(Change, Acc)}.
+ {
+ [
+ {<<"seq">>, Seq},
+ {<<"id">>, Id},
+ {<<"changes">>, Results}
+ ] ++ deleted_item(Del) ++ maybe_get_changes_doc(Change, Acc)
+ }.
-maybe_get_changes_doc(Value, #changes_acc{include_docs=true}=Acc) ->
+maybe_get_changes_doc(Value, #changes_acc{include_docs = true} = Acc) ->
#changes_acc{
db = Db,
doc_options = DocOpts0,
conflicts = Conflicts,
filter = Filter
} = Acc,
- OpenOpts = case Conflicts of
- true -> [deleted, conflicts];
- false -> [deleted]
- end,
- DocOpts1 = case Conflicts of
- true -> [conflicts | DocOpts0];
- false -> DocOpts0
- end,
+ OpenOpts =
+ case Conflicts of
+ true -> [deleted, conflicts];
+ false -> [deleted]
+ end,
+ DocOpts1 =
+ case Conflicts of
+ true -> [conflicts | DocOpts0];
+ false -> DocOpts0
+ end,
load_doc(Db, Value, OpenOpts, DocOpts1, Filter);
-
maybe_get_changes_doc(_Value, _Acc) ->
[].
-
load_doc(Db, Value, Opts, DocOpts, Filter) ->
case load_doc(Db, Value, Opts) of
null ->
@@ -667,7 +721,6 @@ load_doc(Db, Value, Opts, DocOpts, Filter) ->
[{doc, doc_to_json(Doc, DocOpts, Filter)}]
end.
-
load_doc(Db, Change, Opts) ->
#{
id := Id,
@@ -680,68 +733,66 @@ load_doc(Db, Change, Opts) ->
null
end.
-
-doc_to_json(Doc, DocOpts, {selector, _Style, {_Selector, Fields}})
- when Fields =/= nil ->
+doc_to_json(Doc, DocOpts, {selector, _Style, {_Selector, Fields}}) when
+ Fields =/= nil
+->
mango_fields:extract(couch_doc:to_json_obj(Doc, DocOpts), Fields);
doc_to_json(Doc, DocOpts, _Filter) ->
couch_doc:to_json_obj(Doc, DocOpts).
-
deleted_item(true) -> [{<<"deleted">>, true}];
deleted_item(_) -> [].
% waits for a updated msg, if there are multiple msgs, collects them.
wait_updated(Timeout, TimeoutFun, UserAcc) ->
receive
- updated ->
- get_rest_updated(UserAcc);
- deleted ->
- {stop, UserAcc}
+ updated ->
+ get_rest_updated(UserAcc);
+ deleted ->
+ {stop, UserAcc}
after Timeout ->
{Go, UserAcc2} = TimeoutFun(UserAcc),
case Go of
- ok ->
- ?MODULE:wait_updated(Timeout, TimeoutFun, UserAcc2);
- stop ->
- {stop, UserAcc2}
+ ok ->
+ ?MODULE:wait_updated(Timeout, TimeoutFun, UserAcc2);
+ stop ->
+ {stop, UserAcc2}
end
end.
get_rest_updated(UserAcc) ->
receive
- updated ->
- get_rest_updated(UserAcc)
+ updated ->
+ get_rest_updated(UserAcc)
after 0 ->
{updated, UserAcc}
end.
reset_heartbeat() ->
case get(last_changes_heartbeat) of
- undefined ->
- ok;
- _ ->
- put(last_changes_heartbeat, os:timestamp())
+ undefined ->
+ ok;
+ _ ->
+ put(last_changes_heartbeat, os:timestamp())
end.
maybe_heartbeat(Timeout, TimeoutFun, Acc) ->
Before = get(last_changes_heartbeat),
case Before of
- undefined ->
- {ok, Acc};
- _ ->
- Now = os:timestamp(),
- case timer:now_diff(Now, Before) div 1000 >= Timeout of
- true ->
- {StopOrGo, Acc2} = TimeoutFun(Acc),
- put(last_changes_heartbeat, Now),
- {StopOrGo, Acc2};
- false ->
- {ok, Acc}
- end
+ undefined ->
+ {ok, Acc};
+ _ ->
+ Now = os:timestamp(),
+ case timer:now_diff(Now, Before) div 1000 >= Timeout of
+ true ->
+ {StopOrGo, Acc2} = TimeoutFun(Acc),
+ put(last_changes_heartbeat, Now),
+ {StopOrGo, Acc2};
+ false ->
+ {ok, Acc}
+ end
end.
-
maybe_upgrade_changes_acc(#changes_acc{} = Acc) ->
Acc;
maybe_upgrade_changes_acc(Acc) when tuple_size(Acc) == 19 ->
diff --git a/src/chttpd/src/chttpd_cors.erl b/src/chttpd/src/chttpd_cors.erl
index a2cf167..e907e71 100644
--- a/src/chttpd/src/chttpd_cors.erl
+++ b/src/chttpd/src/chttpd_cors.erl
@@ -12,7 +12,6 @@
-module(chttpd_cors).
-
-export([
maybe_handle_preflight_request/1,
maybe_handle_preflight_request/2,
@@ -24,15 +23,13 @@
get_cors_config/1
]).
-
-include_lib("couch/include/couch_db.hrl").
-include_lib("chttpd/include/chttpd_cors.hrl").
-include_lib("kernel/include/logger.hrl").
-
%% http://www.w3.org/TR/cors/#resource-preflight-requests
-maybe_handle_preflight_request(#httpd{method=Method}) when Method /= 'OPTIONS' ->
+maybe_handle_preflight_request(#httpd{method = Method}) when Method /= 'OPTIONS' ->
not_preflight;
maybe_handle_preflight_request(Req) ->
case maybe_handle_preflight_request(Req, get_cors_config(Req)) of
@@ -42,8 +39,7 @@ maybe_handle_preflight_request(Req) ->
chttpd:send_response_no_cors(Req, 204, PreflightHeaders, <<>>)
end.
-
-maybe_handle_preflight_request(#httpd{}=Req, Config) ->
+maybe_handle_preflight_request(#httpd{} = Req, Config) ->
case is_cors_enabled(Config) of
true ->
case preflight_request(Req, Config) of
@@ -68,7 +64,6 @@ maybe_handle_preflight_request(#httpd{}=Req, Config) ->
not_preflight
end.
-
preflight_request(Req, Config) ->
case get_origin(Req) of
undefined ->
@@ -103,70 +98,85 @@ preflight_request(Req, Config) ->
end
end.
-
handle_preflight_request(Req, Config, Origin) ->
case chttpd:header_value(Req, "Access-Control-Request-Method") of
- undefined ->
- %% If there is no Access-Control-Request-Method header
- %% or if parsing failed, do not set any additional headers
- %% and terminate this set of steps. The request is outside
- %% the scope of this specification.
- %% http://www.w3.org/TR/cors/#resource-preflight-requests
- not_preflight;
- Method ->
- SupportedMethods = get_origin_config(Config, Origin,
- <<"allow_methods">>, ?SUPPORTED_METHODS),
-
- SupportedHeaders = get_origin_config(Config, Origin,
- <<"allow_headers">>, ?SUPPORTED_HEADERS),
-
-
- %% get max age
- MaxAge = couch_util:get_value(<<"max_age">>, Config,
- ?CORS_DEFAULT_MAX_AGE),
-
- PreflightHeaders0 = maybe_add_credentials(Config, Origin, [
- {"Access-Control-Allow-Origin", binary_to_list(Origin)},
- {"Access-Control-Max-Age", MaxAge},
- {"Access-Control-Allow-Methods",
- string:join(SupportedMethods, ", ")}]),
-
- case lists:member(Method, SupportedMethods) of
- true ->
- %% method ok , check headers
- AccessHeaders = chttpd:header_value(Req,
- "Access-Control-Request-Headers"),
- {FinalReqHeaders, ReqHeaders} = case AccessHeaders of
- undefined -> {"", []};
- "" -> {"", []};
- Headers ->
- %% transform header list in something we
- %% could check. make sure everything is a
- %% list
- RH = [to_lower(H)
- || H <- split_headers(Headers)],
- {Headers, RH}
- end,
- %% check if headers are supported
- case ReqHeaders -- SupportedHeaders of
- [] ->
- PreflightHeaders = PreflightHeaders0 ++
- [{"Access-Control-Allow-Headers",
- FinalReqHeaders}],
- {ok, PreflightHeaders};
- _ ->
- not_preflight
- end;
- false ->
- %% If method is not a case-sensitive match for any of
- %% the values in list of methods do not set any additional
- %% headers and terminate this set of steps.
+ undefined ->
+ %% If there is no Access-Control-Request-Method header
+ %% or if parsing failed, do not set any additional headers
+ %% and terminate this set of steps. The request is outside
+ %% the scope of this specification.
%% http://www.w3.org/TR/cors/#resource-preflight-requests
- not_preflight
- end
+ not_preflight;
+ Method ->
+ SupportedMethods = get_origin_config(
+ Config,
+ Origin,
+ <<"allow_methods">>,
+ ?SUPPORTED_METHODS
+ ),
+
+ SupportedHeaders = get_origin_config(
+ Config,
+ Origin,
+ <<"allow_headers">>,
+ ?SUPPORTED_HEADERS
+ ),
+
+ %% get max age
+ MaxAge = couch_util:get_value(
+ <<"max_age">>,
+ Config,
+ ?CORS_DEFAULT_MAX_AGE
+ ),
+
+ PreflightHeaders0 = maybe_add_credentials(Config, Origin, [
+ {"Access-Control-Allow-Origin", binary_to_list(Origin)},
+ {"Access-Control-Max-Age", MaxAge},
+ {"Access-Control-Allow-Methods", string:join(SupportedMethods, ", ")}
+ ]),
+
+ case lists:member(Method, SupportedMethods) of
+ true ->
+ %% method ok , check headers
+ AccessHeaders = chttpd:header_value(
+ Req,
+ "Access-Control-Request-Headers"
+ ),
+ {FinalReqHeaders, ReqHeaders} =
+ case AccessHeaders of
+ undefined ->
+ {"", []};
+ "" ->
+ {"", []};
+ Headers ->
+ %% transform header list in something we
+ %% could check. make sure everything is a
+ %% list
+ RH = [
+ to_lower(H)
+ || H <- split_headers(Headers)
+ ],
+ {Headers, RH}
+ end,
+ %% check if headers are supported
+ case ReqHeaders -- SupportedHeaders of
+ [] ->
+ PreflightHeaders =
+ PreflightHeaders0 ++
+ [{"Access-Control-Allow-Headers", FinalReqHeaders}],
+ {ok, PreflightHeaders};
+ _ ->
+ not_preflight
+ end;
+ false ->
+ %% If method is not a case-sensitive match for any of
+ %% the values in list of methods do not set any additional
+ %% headers and terminate this set of steps.
+ %% http://www.w3.org/TR/cors/#resource-preflight-requests
+ not_preflight
+ end
end.
-
headers(Req, RequestHeaders) ->
case get_origin(Req) of
undefined ->
@@ -179,7 +189,6 @@ headers(Req, RequestHeaders) ->
headers(Req, RequestHeaders, Origin, get_cors_config(Req))
end.
-
headers(_Req, RequestHeaders, undefined, _Config) ->
RequestHeaders;
headers(Req, RequestHeaders, Origin, Config) when is_list(Origin) ->
@@ -190,13 +199,13 @@ headers(Req, RequestHeaders, Origin, Config) ->
AcceptedOrigins = get_accepted_origins(Req, Config),
CorsHeaders = handle_headers(Config, Origin, AcceptedOrigins),
ExposedCouchHeaders = couch_util:get_value(
- <<"exposed_headers">>, Config, ?COUCH_HEADERS),
+ <<"exposed_headers">>, Config, ?COUCH_HEADERS
+ ),
maybe_apply_headers(CorsHeaders, RequestHeaders, ExposedCouchHeaders);
false ->
RequestHeaders
end.
-
maybe_apply_headers([], RequestHeaders, _ExposedCouchHeaders) ->
RequestHeaders;
maybe_apply_headers(CorsHeaders, RequestHeaders, ExposedCouchHeaders) ->
@@ -207,67 +216,64 @@ maybe_apply_headers(CorsHeaders, RequestHeaders, ExposedCouchHeaders) ->
%% need to be exposed.
%% return: RequestHeaders ++ CorsHeaders ++ ACEH
- ExposedHeaders0 = simple_headers([K || {K,_V} <- RequestHeaders]),
+ ExposedHeaders0 = simple_headers([K || {K, _V} <- RequestHeaders]),
%% If Content-Type is not in ExposedHeaders, and the Content-Type
%% is not a member of ?SIMPLE_CONTENT_TYPE_VALUES, then add it
%% into the list of ExposedHeaders
ContentType = proplists:get_value("content-type", ExposedHeaders0),
- IncludeContentType = case ContentType of
- undefined ->
- false;
- _ ->
- lists:member(string:to_lower(ContentType), ?SIMPLE_CONTENT_TYPE_VALUES)
+ IncludeContentType =
+ case ContentType of
+ undefined ->
+ false;
+ _ ->
+ lists:member(string:to_lower(ContentType), ?SIMPLE_CONTENT_TYPE_VALUES)
end,
- ExposedHeaders = case IncludeContentType of
- false ->
- ["content-type" | lists:delete("content-type", ExposedHeaders0)];
- true ->
- ExposedHeaders0
+ ExposedHeaders =
+ case IncludeContentType of
+ false ->
+ ["content-type" | lists:delete("content-type", ExposedHeaders0)];
+ true ->
+ ExposedHeaders0
end,
%% ExposedCouchHeaders may get added later, so expose them by default
- ACEH = [{"Access-Control-Expose-Headers",
- string:join(ExposedHeaders ++ ExposedCouchHeaders, ", ")}],
+ ACEH = [
+ {"Access-Control-Expose-Headers", string:join(ExposedHeaders ++ ExposedCouchHeaders, ", ")}
+ ],
CorsHeaders ++ RequestHeaders ++ ACEH.
-
simple_headers(Headers) ->
LCHeaders = [to_lower(H) || H <- Headers],
lists:filter(fun(H) -> lists:member(H, ?SIMPLE_HEADERS) end, LCHeaders).
-
to_lower(String) when is_binary(String) ->
to_lower(?b2l(String));
to_lower(String) ->
string:to_lower(String).
-
handle_headers(_Config, _Origin, []) ->
[];
handle_headers(Config, Origin, AcceptedOrigins) ->
AcceptAll = lists:member(<<"*">>, AcceptedOrigins),
case AcceptAll orelse lists:member(Origin, AcceptedOrigins) of
- true ->
- make_cors_header(Config, Origin);
- false ->
- %% If the value of the Origin header is not a
- %% case-sensitive match for any of the values
- %% in list of origins, do not set any additional
- %% headers and terminate this set of steps.
- %% http://www.w3.org/TR/cors/#resource-requests
- []
+ true ->
+ make_cors_header(Config, Origin);
+ false ->
+ %% If the value of the Origin header is not a
+ %% case-sensitive match for any of the values
+ %% in list of origins, do not set any additional
+ %% headers and terminate this set of steps.
+ %% http://www.w3.org/TR/cors/#resource-requests
+ []
end.
-
make_cors_header(Config, Origin) ->
Headers = [{"Access-Control-Allow-Origin", binary_to_list(Origin)}],
maybe_add_credentials(Config, Origin, Headers).
-
%% util
-
maybe_add_credentials(Config, Origin, Headers) ->
case allow_credentials(Config, Origin) of
false ->
@@ -276,13 +282,15 @@ maybe_add_credentials(Config, Origin, Headers) ->
Headers ++ [{"Access-Control-Allow-Credentials", "true"}]
end.
-
allow_credentials(_Config, <<"*">>) ->
false;
allow_credentials(Config, Origin) ->
- get_origin_config(Config, Origin, <<"allow_credentials">>,
- ?CORS_DEFAULT_ALLOW_CREDENTIALS).
-
+ get_origin_config(
+ Config,
+ Origin,
+ <<"allow_credentials">>,
+ ?CORS_DEFAULT_ALLOW_CREDENTIALS
+ ).
get_cors_config(#httpd{cors_config = undefined, mochi_req = MochiReq}) ->
Host = couch_httpd_vhost:host(MochiReq),
@@ -290,24 +298,27 @@ get_cors_config(#httpd{cors_config = undefined, mochi_req = MochiReq}) ->
EnableCors = chttpd_util:get_chttpd_config_boolean("enable_cors", false),
AllowCredentials = cors_config(Host, "credentials", "false") =:= "true",
- AllowHeaders = case cors_config(Host, "headers", undefined) of
- undefined ->
- ?SUPPORTED_HEADERS;
- AllowHeaders0 ->
- [to_lower(H) || H <- split_list(AllowHeaders0)]
- end,
- AllowMethods = case cors_config(Host, "methods", undefined) of
- undefined ->
- ?SUPPORTED_METHODS;
- AllowMethods0 ->
- split_list(AllowMethods0)
- end,
- ExposedHeaders = case cors_config(Host, "exposed_headers", undefined) of
- undefined ->
- ?COUCH_HEADERS;
- ExposedHeaders0 ->
- [to_lower(H) || H <- split_list(ExposedHeaders0)]
- end,
+ AllowHeaders =
+ case cors_config(Host, "headers", undefined) of
+ undefined ->
+ ?SUPPORTED_HEADERS;
+ AllowHeaders0 ->
+ [to_lower(H) || H <- split_list(AllowHeaders0)]
+ end,
+ AllowMethods =
+ case cors_config(Host, "methods", undefined) of
+ undefined ->
+ ?SUPPORTED_METHODS;
+ AllowMethods0 ->
+ split_list(AllowMethods0)
+ end,
+ ExposedHeaders =
+ case cors_config(Host, "exposed_headers", undefined) of
+ undefined ->
+ ?COUCH_HEADERS;
+ ExposedHeaders0 ->
+ [to_lower(H) || H <- split_list(ExposedHeaders0)]
+ end,
MaxAge = cors_config(Host, "max_age", ?CORS_DEFAULT_MAX_AGE),
Origins0 = binary_split_list(cors_config(Host, "origins", [])),
Origins = [{O, {[]}} || O <- Origins0],
@@ -323,25 +334,24 @@ get_cors_config(#httpd{cors_config = undefined, mochi_req = MochiReq}) ->
get_cors_config(#httpd{cors_config = Config}) ->
Config.
-
cors_config(Host, Key, Default) ->
- config:get(cors_section(Host), Key,
- config:get("cors", Key, Default)).
-
+ config:get(
+ cors_section(Host),
+ Key,
+ config:get("cors", Key, Default)
+ ).
cors_section(HostValue) ->
HostPort = maybe_strip_scheme(HostValue),
Host = hd(string:tokens(HostPort, ":")),
"cors:" ++ Host.
-
maybe_strip_scheme(Host) ->
case string:str(Host, "://") of
0 -> Host;
N -> string:substr(Host, N + 3)
end.
-
is_cors_enabled(Config) ->
case get(disable_couch_httpd_cors) of
undefined ->
@@ -351,7 +361,6 @@ is_cors_enabled(Config) ->
end,
couch_util:get_value(<<"enable_cors">>, Config, false).
-
%% Get a list of {Origin, OriginConfig} tuples
%% ie: get_origin_configs(Config) ->
%% [
@@ -369,7 +378,6 @@ get_origin_configs(Config) ->
{Origins} = couch_util:get_value(<<"origins">>, Config, {[]}),
Origins.
-
%% Get config for an individual Origin
%% ie: get_origin_config(Config, <<"http://foo.com">>) ->
%% [
@@ -381,15 +389,16 @@ get_origin_config(Config, Origin) ->
{OriginConfig} = couch_util:get_value(Origin, OriginConfigs, {[]}),
OriginConfig.
-
%% Get config of a single key for an individual Origin
%% ie: get_origin_config(Config, <<"http://foo.com">>, <<"allow_methods">>, [])
%% [<<"POST">>]
get_origin_config(Config, Origin, Key, Default) ->
OriginConfig = get_origin_config(Config, Origin),
- couch_util:get_value(Key, OriginConfig,
- couch_util:get_value(Key, Config, Default)).
-
+ couch_util:get_value(
+ Key,
+ OriginConfig,
+ couch_util:get_value(Key, Config, Default)
+ ).
get_origin(Req) ->
case chttpd:header_value(Req, "Origin") of
@@ -399,18 +408,14 @@ get_origin(Req) ->
?l2b(Origin)
end.
-
get_accepted_origins(_Req, Config) ->
- lists:map(fun({K,_V}) -> K end, get_origin_configs(Config)).
-
+ lists:map(fun({K, _V}) -> K end, get_origin_configs(Config)).
split_list(S) ->
re:split(S, "\\s*,\\s*", [trim, {return, list}]).
-
binary_split_list(S) ->
[list_to_binary(E) || E <- split_list(S)].
-
split_headers(H) ->
- re:split(H, ",\\s*", [{return,list}, trim]).
+ re:split(H, ",\\s*", [{return, list}, trim]).
diff --git a/src/chttpd/src/chttpd_db.erl b/src/chttpd/src/chttpd_db.erl
index 4a7b631..df609bb 100644
--- a/src/chttpd/src/chttpd_db.erl
+++ b/src/chttpd/src/chttpd_db.erl
@@ -18,17 +18,34 @@
-include_lib("couch_views/include/couch_views.hrl").
-include_lib("kernel/include/logger.hrl").
--export([handle_request/1, handle_compact_req/2, handle_design_req/2,
- db_req/2, couch_doc_open/4,handle_changes_req/2,
+-export([
+ handle_request/1,
+ handle_compact_req/2,
+ handle_design_req/2,
+ db_req/2,
+ couch_doc_open/4,
+ handle_changes_req/2,
update_doc_result_to_json/1, update_doc_result_to_json/2,
- handle_design_info_req/3, handle_view_cleanup_req/2,
- update_doc/4, http_code_from_status/1]).
-
--import(chttpd,
- [send_json/2,send_json/3,send_json/4,send_method_not_allowed/2,
- start_json_response/2,send_chunk/2,end_json_response/1,
- start_chunked_response/3, absolute_uri/2, send/2,
- start_response_length/4]).
+ handle_design_info_req/3,
+ handle_view_cleanup_req/2,
+ update_doc/4,
+ http_code_from_status/1
+]).
+
+-import(
+ chttpd,
+ [
+ send_json/2, send_json/3, send_json/4,
+ send_method_not_allowed/2,
+ start_json_response/2,
+ send_chunk/2,
+ end_json_response/1,
+ start_chunked_response/3,
+ absolute_uri/2,
+ send/2,
+ start_response_length/4
+ ]
+).
-record(doc_query_args, {
options = [],
@@ -51,99 +68,110 @@
include_docs
}).
--define(IS_ALL_DOCS(T), (
- T == <<"_all_docs">>
- orelse T == <<"_local_docs">>
- orelse T == <<"_design_docs">>)).
+-define(IS_ALL_DOCS(T),
+ (T == <<"_all_docs">> orelse
+ T == <<"_local_docs">> orelse
+ T == <<"_design_docs">>)
+).
--define(IS_MANGO(T), (
- T == <<"_index">>
- orelse T == <<"_find">>
- orelse T == <<"_explain">>)).
+-define(IS_MANGO(T),
+ (T == <<"_index">> orelse
+ T == <<"_find">> orelse
+ T == <<"_explain">>)
+).
% Database request handlers
-handle_request(#httpd{path_parts=[DbName|RestParts],method=Method}=Req)->
+handle_request(#httpd{path_parts = [DbName | RestParts], method = Method} = Req) ->
case {Method, RestParts} of
- {'PUT', []} ->
- create_db_req(Req, DbName);
- {'DELETE', []} ->
- % if we get ?rev=... the user is using a faulty script where the
- % document id is empty by accident. Let them recover safely.
- case chttpd:qs_value(Req, "rev", false) of
- false -> delete_db_req(Req, DbName);
- _Rev -> throw({bad_request,
- "You tried to DELETE a database with a ?=rev parameter. "
- ++ "Did you mean to DELETE a document instead?"})
- end;
- {_, []} ->
- do_db_req(Req, fun db_req/2);
- {_, [SecondPart|_]} ->
- Handler = chttpd_handlers:db_handler(SecondPart, fun db_req/2),
- do_db_req(Req, Handler)
+ {'PUT', []} ->
+ create_db_req(Req, DbName);
+ {'DELETE', []} ->
+ % if we get ?rev=... the user is using a faulty script where the
+ % document id is empty by accident. Let them recover safely.
+ case chttpd:qs_value(Req, "rev", false) of
+ false ->
+ delete_db_req(Req, DbName);
+ _Rev ->
+ throw(
+ {bad_request,
+ "You tried to DELETE a database with a ?=rev parameter. " ++
+ "Did you mean to DELETE a document instead?"}
+ )
+ end;
+ {_, []} ->
+ do_db_req(Req, fun db_req/2);
+ {_, [SecondPart | _]} ->
+ Handler = chttpd_handlers:db_handler(SecondPart, fun db_req/2),
+ do_db_req(Req, Handler)
end.
-handle_changes_req(#httpd{method='POST'}=Req, Db) ->
+handle_changes_req(#httpd{method = 'POST'} = Req, Db) ->
chttpd:validate_ctype(Req, "application/json"),
fabric2_fdb:transactional(Db, fun(TxDb) ->
handle_changes_req_tx(Req, TxDb)
end);
-handle_changes_req(#httpd{method='GET'}=Req, Db) ->
+handle_changes_req(#httpd{method = 'GET'} = Req, Db) ->
fabric2_fdb:transactional(Db, fun(TxDb) ->
handle_changes_req_tx(Req, TxDb)
end);
-handle_changes_req(#httpd{path_parts=[_,<<"_changes">>]}=Req, _Db) ->
+handle_changes_req(#httpd{path_parts = [_, <<"_changes">>]} = Req, _Db) ->
send_method_not_allowed(Req, "GET,POST,HEAD").
-handle_changes_req_tx(#httpd{}=Req, Db) ->
+handle_changes_req_tx(#httpd{} = Req, Db) ->
ChangesArgs = parse_changes_query(Req),
ChangesFun = chttpd_changes:handle_db_changes(ChangesArgs, Req, Db),
Max = chttpd:chunked_response_buffer_size(),
case ChangesArgs#changes_args.feed of
- "normal" ->
- Acc0 = #cacc{
- feed = normal,
- mochi = Req,
- threshold = Max
- },
- ChangesFun({fun changes_callback/2, Acc0});
- Feed when Feed =:= "continuous"; Feed =:= "longpoll"; Feed =:= "eventsource" ->
- couch_stats:increment_counter([couchdb, httpd, clients_requesting_changes]),
- Acc0 = #cacc{
- feed = list_to_atom(Feed),
- mochi = Req,
- threshold = Max,
- include_docs = ChangesArgs#changes_args.include_docs
- },
- try
- ChangesFun({fun changes_callback/2, Acc0})
- after
- couch_stats:decrement_counter([couchdb, httpd, clients_requesting_changes])
- end;
- _ ->
- Msg = <<"Supported `feed` types: normal, continuous, live, longpoll, eventsource">>,
- throw({bad_request, Msg})
+ "normal" ->
+ Acc0 = #cacc{
+ feed = normal,
+ mochi = Req,
+ threshold = Max
+ },
+ ChangesFun({fun changes_callback/2, Acc0});
+ Feed when Feed =:= "continuous"; Feed =:= "longpoll"; Feed =:= "eventsource" ->
+ couch_stats:increment_counter([couchdb, httpd, clients_requesting_changes]),
+ Acc0 = #cacc{
+ feed = list_to_atom(Feed),
+ mochi = Req,
+ threshold = Max,
+ include_docs = ChangesArgs#changes_args.include_docs
+ },
+ try
+ ChangesFun({fun changes_callback/2, Acc0})
+ after
+ couch_stats:decrement_counter([couchdb, httpd, clients_requesting_changes])
+ end;
+ _ ->
+ Msg = <<"Supported `feed` types: normal, continuous, live, longpoll, eventsource">>,
+ throw({bad_request, Msg})
end.
% callbacks for continuous feed (newline-delimited JSON Objects)
changes_callback(start, #cacc{feed = continuous} = Acc) ->
{ok, Resp} = chttpd:start_delayed_json_response(Acc#cacc.mochi, 200),
{ok, Acc#cacc{mochi = Resp, responding = true}};
-changes_callback({change, Change}, #cacc{feed = continuous,
- include_docs = IncludeDocs} = Acc) ->
+changes_callback(
+ {change, Change},
+ #cacc{
+ feed = continuous,
+ include_docs = IncludeDocs
+ } = Acc
+) ->
incr_stats_changes_feed(IncludeDocs),
Data = [?JSON_ENCODE(Change) | "\n"],
Len = iolist_size(Data),
maybe_flush_changes_feed(Acc, Data, Len);
changes_callback({stop, EndSeq, Pending}, #cacc{feed = continuous} = Acc) ->
#cacc{mochi = Resp, buffer = Buf} = Acc,
- Row = {[
- {<<"last_seq">>, EndSeq},
- {<<"pending">>, Pending}
- ]},
+ Row =
+ {[
+ {<<"last_seq">>, EndSeq},
+ {<<"pending">>, Pending}
+ ]},
Data = [Buf, ?JSON_ENCODE(Row) | "\n"],
{ok, Resp1} = chttpd:send_delayed_chunk(Resp, Data),
chttpd:end_delayed_json_response(Resp1);
-
% callbacks for eventsource feed (newline-delimited eventsource Objects)
changes_callback(start, #cacc{feed = eventsource} = Acc) ->
#cacc{mochi = Req} = Acc,
@@ -153,13 +181,18 @@ changes_callback(start, #cacc{feed = eventsource} = Acc) ->
],
{ok, Resp} = chttpd:start_delayed_json_response(Req, 200, Headers),
{ok, Acc#cacc{mochi = Resp, responding = true}};
-changes_callback({change, {ChangeProp}=Change},
- #cacc{feed = eventsource, include_docs = IncludeDocs} = Acc) ->
+changes_callback(
+ {change, {ChangeProp} = Change},
+ #cacc{feed = eventsource, include_docs = IncludeDocs} = Acc
+) ->
incr_stats_changes_feed(IncludeDocs),
Seq = proplists:get_value(seq, ChangeProp),
Chunk = [
- "data: ", ?JSON_ENCODE(Change),
- "\n", "id: ", ?JSON_ENCODE(Seq),
+ "data: ",
+ ?JSON_ENCODE(Change),
+ "\n",
+ "id: ",
+ ?JSON_ENCODE(Seq),
"\n\n"
],
Len = iolist_size(Chunk),
@@ -173,7 +206,6 @@ changes_callback({stop, _EndSeq}, #cacc{feed = eventsource} = Acc) ->
#cacc{mochi = Resp, buffer = Buf} = Acc,
{ok, Resp1} = chttpd:send_delayed_chunk(Resp, Buf),
chttpd:end_delayed_json_response(Resp1);
-
% callbacks for longpoll and normal (single JSON Object)
changes_callback(start, #cacc{feed = normal} = Acc) ->
#cacc{mochi = Req} = Acc,
@@ -201,7 +233,6 @@ changes_callback({stop, EndSeq, Pending}, Acc) ->
],
{ok, Resp1} = chttpd:close_delayed_json_object(Resp, Buf, Terminator, Max),
chttpd:end_delayed_json_response(Resp1);
-
changes_callback(waiting_for_updates, #cacc{buffer = []} = Acc) ->
#cacc{mochi = Resp, chunks_sent = ChunksSent} = Acc,
case ChunksSent > 0 of
@@ -222,10 +253,11 @@ changes_callback(waiting_for_updates, Acc) ->
}};
changes_callback({timeout, ResponseType}, Acc) ->
#cacc{mochi = Resp, chunks_sent = ChunksSent} = Acc,
- Chunk = case ResponseType of
- "eventsource" -> "event: heartbeat\ndata: \n\n";
- _ -> "\n"
- end,
+ Chunk =
+ case ResponseType of
+ "eventsource" -> "event: heartbeat\ndata: \n\n";
+ _ -> "\n"
+ end,
{ok, Resp1} = chttpd:send_delayed_chunk(Resp, Chunk),
{ok, Acc#cacc{mochi = Resp1, chunks_sent = ChunksSent + 1}};
changes_callback({error, Reason}, #cacc{mochi = #httpd{}} = Acc) ->
@@ -237,11 +269,12 @@ changes_callback({error, Reason}, #cacc{feed = normal, responding = false} = Acc
changes_callback({error, Reason}, Acc) ->
chttpd:send_delayed_error(Acc#cacc.mochi, Reason).
-maybe_flush_changes_feed(#cacc{bufsize=Size, threshold=Max} = Acc, Data, Len)
- when Size > 0 andalso (Size + Len) > Max ->
+maybe_flush_changes_feed(#cacc{bufsize = Size, threshold = Max} = Acc, Data, Len) when
+ Size > 0 andalso (Size + Len) > Max
+->
#cacc{buffer = Buffer, mochi = Resp} = Acc,
{ok, R1} = chttpd:send_delayed_chunk(Resp, Buffer),
- {ok, Acc#cacc{prepend = ",\r\n", buffer = Data, bufsize=Len, mochi = R1}};
+ {ok, Acc#cacc{prepend = ",\r\n", buffer = Data, bufsize = Len, mochi = R1}};
maybe_flush_changes_feed(Acc0, Data, Len) ->
#cacc{buffer = Buf, bufsize = Size, chunks_sent = ChunksSent} = Acc0,
Acc = Acc0#cacc{
@@ -254,18 +287,18 @@ maybe_flush_changes_feed(Acc0, Data, Len) ->
incr_stats_changes_feed(IncludeDocs) ->
chttpd_stats:incr_rows(),
- if not IncludeDocs -> ok; true ->
- chttpd_stats:incr_reads()
+ if
+ not IncludeDocs -> ok;
+ true -> chttpd_stats:incr_reads()
end.
% Return the same response as if a compaction succeeded even though _compaction
% isn't a valid operation in CouchDB >= 4.x anymore. This is mostly to not
% break existing user script which maybe periodically call this endpoint. In
% the future this endpoint will return a 410 response then it will be removed.
-handle_compact_req(#httpd{method='POST'}=Req, _Db) ->
+handle_compact_req(#httpd{method = 'POST'} = Req, _Db) ->
chttpd:validate_ctype(Req, "application/json"),
send_json(Req, 202, {[{ok, true}]});
-
handle_compact_req(Req, _Db) ->
send_method_not_allowed(Req, "POST").
@@ -273,35 +306,40 @@ handle_view_cleanup_req(Req, Db) ->
ok = fabric2_index:cleanup(Db),
send_json(Req, 202, {[{ok, true}]}).
-handle_design_req(#httpd{
- path_parts=[_DbName, _Design, Name, <<"_",_/binary>> = Action | _Rest]
- }=Req, Db) ->
+handle_design_req(
+ #httpd{
+ path_parts = [_DbName, _Design, Name, <<"_", _/binary>> = Action | _Rest]
+ } = Req,
+ Db
+) ->
case fabric2_db:open_doc(Db, <<"_design/", Name/binary>>) of
- {ok, DDoc} ->
- Handler = chttpd_handlers:design_handler(Action, fun bad_action_req/3),
- Handler(Req, Db, DDoc);
- Error ->
- throw(Error)
+ {ok, DDoc} ->
+ Handler = chttpd_handlers:design_handler(Action, fun bad_action_req/3),
+ Handler(Req, Db, DDoc);
+ Error ->
+ throw(Error)
end;
-
handle_design_req(Req, Db) ->
db_req(Req, Db).
-bad_action_req(#httpd{path_parts=[_, _, Name|FileNameParts]}=Req, Db, _DDoc) ->
- db_attachment_req(Req, Db, <<"_design/",Name/binary>>, FileNameParts).
+bad_action_req(#httpd{path_parts = [_, _, Name | FileNameParts]} = Req, Db, _DDoc) ->
+ db_attachment_req(Req, Db, <<"_design/", Name/binary>>, FileNameParts).
-handle_design_info_req(#httpd{method='GET'}=Req, Db, #doc{} = DDoc) ->
+handle_design_info_req(#httpd{method = 'GET'} = Req, Db, #doc{} = DDoc) ->
[_, _, Name, _] = Req#httpd.path_parts,
{ok, GroupInfoList} = couch_views:get_info(Db, DDoc),
- send_json(Req, 200, {[
- {name, Name},
- {view_index, {GroupInfoList}}
- ]});
-
+ send_json(
+ Req,
+ 200,
+ {[
+ {name, Name},
+ {view_index, {GroupInfoList}}
+ ]}
+ );
handle_design_info_req(Req, _Db, _DDoc) ->
send_method_not_allowed(Req, "GET").
-create_db_req(#httpd{user_ctx=Ctx}=Req, DbName) ->
+create_db_req(#httpd{user_ctx = Ctx} = Req, DbName) ->
couch_httpd:verify_is_server_admin(Req),
DocUrl = absolute_uri(Req, "/" ++ couch_util:url_encode(DbName)),
case fabric2_db:create(DbName, [{user_ctx, Ctx}]) of
@@ -313,7 +351,7 @@ create_db_req(#httpd{user_ctx=Ctx}=Req, DbName) ->
throw(Error)
end.
-delete_db_req(#httpd{user_ctx=Ctx}=Req, DbName) ->
+delete_db_req(#httpd{user_ctx = Ctx} = Req, DbName) ->
couch_httpd:verify_is_server_admin(Req),
case fabric2_db:delete(DbName, [{user_ctx, Ctx}]) of
ok ->
@@ -322,174 +360,201 @@ delete_db_req(#httpd{user_ctx=Ctx}=Req, DbName) ->
throw(Error)
end.
-do_db_req(#httpd{path_parts=[DbName|_], user_ctx=Ctx}=Req, Fun) ->
+do_db_req(#httpd{path_parts = [DbName | _], user_ctx = Ctx} = Req, Fun) ->
Options = [{user_ctx, Ctx}, {interactive, true}],
{ok, Db} = fabric2_db:open(DbName, Options),
Fun(Req, Db).
-db_req(#httpd{method='GET',path_parts=[_DbName]}=Req, Db) ->
+db_req(#httpd{method = 'GET', path_parts = [_DbName]} = Req, Db) ->
% measure the time required to generate the etag, see if it's worth it
T0 = os:timestamp(),
{ok, DbInfo} = fabric2_db:get_db_info(Db),
DeltaT = timer:now_diff(os:timestamp(), T0) / 1000,
couch_stats:update_histogram([couchdb, dbinfo], DeltaT),
send_json(Req, {DbInfo});
-
-db_req(#httpd{method='POST', path_parts=[DbName]}=Req, Db) ->
+db_req(#httpd{method = 'POST', path_parts = [DbName]} = Req, Db) ->
chttpd:validate_ctype(Req, "application/json"),
Doc0 = chttpd:json_body(Req),
Doc1 = couch_doc:from_json_obj_validate(Doc0, fabric2_db:name(Db)),
validate_attachment_names(Doc1),
- Doc2 = case Doc1#doc.id of
- <<"">> ->
- Doc1#doc{id=couch_uuids:new(), revs={0, []}};
- _ ->
- Doc1
- end,
+ Doc2 =
+ case Doc1#doc.id of
+ <<"">> ->
+ Doc1#doc{id = couch_uuids:new(), revs = {0, []}};
+ _ ->
+ Doc1
+ end,
Doc3 = read_att_data(Doc2),
DocId = Doc3#doc.id,
case chttpd:qs_value(Req, "batch") of
- "ok" ->
- % async_batching
- spawn(fun() ->
- case catch(fabric2_db:update_doc(Db, Doc3, [])) of
- {ok, _} ->
- chttpd_stats:incr_writes(),
- ok;
- {accepted, _} ->
- chttpd_stats:incr_writes(),
- ok;
- Error ->
- ?LOG_DEBUG(#{
- what => async_update_error,
- db => DbName,
- docid => DocId,
- details => Error
- }),
- couch_log:debug("Batch doc error (~s): ~p",[DocId, Error])
+ "ok" ->
+ % async_batching
+ spawn(fun() ->
+ case catch (fabric2_db:update_doc(Db, Doc3, [])) of
+ {ok, _} ->
+ chttpd_stats:incr_writes(),
+ ok;
+ {accepted, _} ->
+ chttpd_stats:incr_writes(),
+ ok;
+ Error ->
+ ?LOG_DEBUG(#{
+ what => async_update_error,
+ db => DbName,
+ docid => DocId,
+ details => Error
+ }),
+ couch_log:debug("Batch doc error (~s): ~p", [DocId, Error])
end
end),
- send_json(Req, 202, [], {[
- {ok, true},
- {id, DocId}
- ]});
- _Normal ->
- % normal
- DocUrl = absolute_uri(Req, [$/, couch_util:url_encode(DbName),
- $/, couch_util:url_encode(DocId)]),
- case fabric2_db:update_doc(Db, Doc3, []) of
- {ok, NewRev} ->
- chttpd_stats:incr_writes(),
- HttpCode = 201;
- {accepted, NewRev} ->
- chttpd_stats:incr_writes(),
- HttpCode = 202
- end,
- send_json(Req, HttpCode, [{"Location", DocUrl}], {[
- {ok, true},
- {id, DocId},
- {rev, couch_doc:rev_to_str(NewRev)}
- ]})
+ send_json(
+ Req,
+ 202,
+ [],
+ {[
+ {ok, true},
+ {id, DocId}
+ ]}
+ );
+ _Normal ->
+ % normal
+ DocUrl = absolute_uri(Req, [
+ $/,
+ couch_util:url_encode(DbName),
+ $/,
+ couch_util:url_encode(DocId)
+ ]),
+ case fabric2_db:update_doc(Db, Doc3, []) of
+ {ok, NewRev} ->
+ chttpd_stats:incr_writes(),
+ HttpCode = 201;
+ {accepted, NewRev} ->
+ chttpd_stats:incr_writes(),
+ HttpCode = 202
+ end,
+ send_json(
+ Req,
+ HttpCode,
+ [{"Location", DocUrl}],
+ {[
+ {ok, true},
+ {id, DocId},
+ {rev, couch_doc:rev_to_str(NewRev)}
+ ]}
+ )
end;
-
-db_req(#httpd{path_parts=[_DbName]}=Req, _Db) ->
+db_req(#httpd{path_parts = [_DbName]} = Req, _Db) ->
send_method_not_allowed(Req, "DELETE,GET,HEAD,POST");
-
-db_req(#httpd{method='POST', path_parts=[_DbName, <<"_ensure_full_commit">>]
- }=Req, Db) ->
+db_req(#httpd{method = 'POST', path_parts = [_DbName, <<"_ensure_full_commit">>]} = Req, Db) ->
chttpd:validate_ctype(Req, "application/json"),
#{db_prefix := <<_/binary>>} = Db,
- send_json(Req, 201, {[
- {ok, true},
- {instance_start_time, <<"0">>}
- ]});
-
-db_req(#httpd{path_parts=[_,<<"_ensure_full_commit">>]}=Req, _Db) ->
+ send_json(
+ Req,
+ 201,
+ {[
+ {ok, true},
+ {instance_start_time, <<"0">>}
+ ]}
+ );
+db_req(#httpd{path_parts = [_, <<"_ensure_full_commit">>]} = Req, _Db) ->
send_method_not_allowed(Req, "POST");
-
-db_req(#httpd{method='POST',path_parts=[_,<<"_bulk_docs">>]}=Req, Db) ->
+db_req(#httpd{method = 'POST', path_parts = [_, <<"_bulk_docs">>]} = Req, Db) ->
couch_stats:increment_counter([couchdb, httpd, bulk_requests]),
chttpd:validate_ctype(Req, "application/json"),
{JsonProps} = chttpd:json_body_obj(Req),
- DocsArray = case couch_util:get_value(<<"docs">>, JsonProps) of
- undefined ->
- throw({bad_request, <<"POST body must include `docs` parameter.">>});
- DocsArray0 when not is_list(DocsArray0) ->
- throw({bad_request, <<"`docs` parameter must be an array.">>});
- DocsArray0 ->
- DocsArray0
- end,
+ DocsArray =
+ case couch_util:get_value(<<"docs">>, JsonProps) of
+ undefined ->
+ throw({bad_request, <<"POST body must include `docs` parameter.">>});
+ DocsArray0 when not is_list(DocsArray0) ->
+ throw({bad_request, <<"`docs` parameter must be an array.">>});
+ DocsArray0 ->
+ DocsArray0
+ end,
MaxDocs = config:get_integer("couchdb", "max_bulk_docs_count", 10000),
case length(DocsArray) =< MaxDocs of
true -> ok;
false -> throw({request_entity_too_large, {bulk_docs, MaxDocs}})
end,
couch_stats:update_histogram([couchdb, httpd, bulk_docs], length(DocsArray)),
- Options = case chttpd:header_value(Req, "X-Couch-Full-Commit") of
- "true" ->
- [full_commit];
- "false" ->
- [delay_commit];
- _ ->
- []
- end,
+ Options =
+ case chttpd:header_value(Req, "X-Couch-Full-Commit") of
+ "true" ->
+ [full_commit];
+ "false" ->
+ [delay_commit];
+ _ ->
+ []
+ end,
DbName = fabric2_db:name(Db),
- Docs = lists:map(fun(JsonObj) ->
- Doc = couch_doc:from_json_obj_validate(JsonObj, DbName),
- validate_attachment_names(Doc),
- case Doc#doc.id of
- <<>> -> Doc#doc{id = couch_uuids:new()};
- _ -> Doc
- end
- end, DocsArray),
- case couch_util:get_value(<<"new_edits">>, JsonProps, true) of
- true ->
- Options2 =
- case couch_util:get_value(<<"all_or_nothing">>, JsonProps) of
- true -> [all_or_nothing|Options];
- _ -> Options
+ Docs = lists:map(
+ fun(JsonObj) ->
+ Doc = couch_doc:from_json_obj_validate(JsonObj, DbName),
+ validate_attachment_names(Doc),
+ case Doc#doc.id of
+ <<>> -> Doc#doc{id = couch_uuids:new()};
+ _ -> Doc
+ end
end,
- case fabric2_db:update_docs(Db, Docs, Options2) of
- {ok, Results} ->
- % output the results
- chttpd_stats:incr_writes(length(Results)),
- DocResults = lists:zipwith(fun update_doc_result_to_json/2,
- Docs, Results),
- send_json(Req, 201, DocResults);
- {accepted, Results} ->
- % output the results
- chttpd_stats:incr_writes(length(Results)),
- DocResults = lists:zipwith(fun update_doc_result_to_json/2,
- Docs, Results),
- send_json(Req, 202, DocResults);
- {aborted, Errors} ->
- ErrorsJson =
- lists:map(fun update_doc_result_to_json/1, Errors),
- send_json(Req, 417, ErrorsJson)
- end;
- false ->
- case fabric2_db:update_docs(Db, Docs, [replicated_changes|Options]) of
- {ok, Errors} ->
- chttpd_stats:incr_writes(length(Docs)),
- ErrorsJson = lists:map(fun update_doc_result_to_json/1, Errors),
- send_json(Req, 201, ErrorsJson);
- {accepted, Errors} ->
- chttpd_stats:incr_writes(length(Docs)),
- ErrorsJson = lists:map(fun update_doc_result_to_json/1, Errors),
- send_json(Req, 202, ErrorsJson)
- end;
- _ ->
- throw({bad_request, <<"`new_edits` parameter must be a boolean.">>})
+ DocsArray
+ ),
+ case couch_util:get_value(<<"new_edits">>, JsonProps, true) of
+ true ->
+ Options2 =
+ case couch_util:get_value(<<"all_or_nothing">>, JsonProps) of
+ true -> [all_or_nothing | Options];
+ _ -> Options
+ end,
+ case fabric2_db:update_docs(Db, Docs, Options2) of
+ {ok, Results} ->
+ % output the results
+ chttpd_stats:incr_writes(length(Results)),
+ DocResults = lists:zipwith(
+ fun update_doc_result_to_json/2,
+ Docs,
+ Results
+ ),
+ send_json(Req, 201, DocResults);
+ {accepted, Results} ->
+ % output the results
+ chttpd_stats:incr_writes(length(Results)),
+ DocResults = lists:zipwith(
+ fun update_doc_result_to_json/2,
+ Docs,
+ Results
+ ),
+ send_json(Req, 202, DocResults);
+ {aborted, Errors} ->
+ ErrorsJson =
+ lists:map(fun update_doc_result_to_json/1, Errors),
+ send_json(Req, 417, ErrorsJson)
+ end;
+ false ->
+ case fabric2_db:update_docs(Db, Docs, [replicated_changes | Options]) of
+ {ok, Errors} ->
+ chttpd_stats:incr_writes(length(Docs)),
+ ErrorsJson = lists:map(fun update_doc_result_to_json/1, Errors),
+ send_json(Req, 201, ErrorsJson);
+ {accepted, Errors} ->
+ chttpd_stats:incr_writes(length(Docs)),
+ ErrorsJson = lists:map(fun update_doc_result_to_json/1, Errors),
+ send_json(Req, 202, ErrorsJson)
+ end;
+ _ ->
+ throw({bad_request, <<"`new_edits` parameter must be a boolean.">>})
end;
-
-db_req(#httpd{path_parts=[_,<<"_bulk_docs">>]}=Req, _Db) ->
+db_req(#httpd{path_parts = [_, <<"_bulk_docs">>]} = Req, _Db) ->
send_method_not_allowed(Req, "POST");
-
-
-db_req(#httpd{method='POST', path_parts=[_, <<"_bulk_get">>],
- mochi_req=MochiReq}=Req, Db) ->
+db_req(
+ #httpd{
+ method = 'POST',
+ path_parts = [_, <<"_bulk_get">>],
+ mochi_req = MochiReq
+ } = Req,
+ Db
+) ->
couch_stats:increment_counter([couchdb, httpd, bulk_requests]),
couch_httpd:validate_ctype(Req, "application/json"),
{JsonProps} = chttpd:json_body_obj(Req),
@@ -505,7 +570,7 @@ db_req(#httpd{method='POST', path_parts=[_, <<"_bulk_get">>],
#doc_query_args{
options = Options
} = bulk_get_parse_doc_query(Req),
- AcceptJson = MochiReq:accepts_content_type("application/json"),
+ AcceptJson = MochiReq:accepts_content_type("application/json"),
AcceptMixedMp = MochiReq:accepts_content_type("multipart/mixed"),
AcceptRelatedMp = MochiReq:accepts_content_type("multipart/related"),
AcceptMp = not AcceptJson andalso (AcceptMixedMp orelse AcceptRelatedMp),
@@ -513,116 +578,145 @@ db_req(#httpd{method='POST', path_parts=[_, <<"_bulk_get">>],
false ->
{ok, Resp} = start_json_response(Req, 200),
send_chunk(Resp, <<"{\"results\": [">>),
- lists:foldl(fun(Doc, Sep) ->
- {DocId, Results, Options1} = bulk_get_open_doc_revs(Db, Doc,
- Options),
- bulk_get_send_docs_json(Resp, DocId, Results, Options1, Sep),
- <<",">>
- end, <<"">>, Docs),
+ lists:foldl(
+ fun(Doc, Sep) ->
+ {DocId, Results, Options1} = bulk_get_open_doc_revs(
+ Db,
+ Doc,
+ Options
+ ),
+ bulk_get_send_docs_json(Resp, DocId, Results, Options1, Sep),
+ <<",">>
+ end,
+ <<"">>,
+ Docs
+ ),
send_chunk(Resp, <<"]}">>),
end_json_response(Resp);
true ->
OuterBoundary = bulk_get_multipart_boundary(),
- MpType = case AcceptMixedMp of
- true ->
- "multipart/mixed";
- _ ->
- "multipart/related"
- end,
- CType = {"Content-Type", MpType ++ "; boundary=\"" ++
- ?b2l(OuterBoundary) ++ "\""},
+ MpType =
+ case AcceptMixedMp of
+ true ->
+ "multipart/mixed";
+ _ ->
+ "multipart/related"
+ end,
+ CType =
+ {
+ "Content-Type",
+ MpType ++
+ "; boundary=\"" ++
+ ?b2l(OuterBoundary) ++
+ "\""
+ },
{ok, Resp} = start_chunked_response(Req, 200, [CType]),
- lists:foldl(fun(Doc, _Pre) ->
- case bulk_get_open_doc_revs(Db, Doc, Options) of
- {_, {ok, []}, _Options1} ->
- ok;
- {_, {ok, Results}, Options1} ->
- send_docs_multipart_bulk_get(Results, Options1,
- OuterBoundary, Resp);
- {DocId, {error, {RevId, Error, Reason}}, _Options1} ->
- Json = ?JSON_ENCODE({[
- {<<"id">>, DocId},
- {<<"rev">>, RevId},
- {<<"error">>, Error},
- {<<"reason">>, Reason}
- ]}),
- couch_httpd:send_chunk(Resp,[
- <<"\r\n--", OuterBoundary/binary>>,
- <<"\r\nContent-Type: application/json; error=\"true\"\r\n\r\n">>,
- Json
- ])
- end
- end, <<"">>, Docs),
+ lists:foldl(
+ fun(Doc, _Pre) ->
+ case bulk_get_open_doc_revs(Db, Doc, Options) of
+ {_, {ok, []}, _Options1} ->
+ ok;
+ {_, {ok, Results}, Options1} ->
+ send_docs_multipart_bulk_get(
+ Results,
+ Options1,
+ OuterBoundary,
+ Resp
+ );
+ {DocId, {error, {RevId, Error, Reason}}, _Options1} ->
+ Json = ?JSON_ENCODE(
+ {[
+ {<<"id">>, DocId},
+ {<<"rev">>, RevId},
+ {<<"error">>, Error},
+ {<<"reason">>, Reason}
+ ]}
+ ),
+ couch_httpd:send_chunk(Resp, [
+ <<"\r\n--", OuterBoundary/binary>>,
+ <<"\r\nContent-Type: application/json; error=\"true\"\r\n\r\n">>,
+ Json
+ ])
+ end
+ end,
+ <<"">>,
+ Docs
+ ),
case Docs of
[] ->
ok;
_ ->
- couch_httpd:send_chunk(Resp, <<"\r\n", "--", OuterBoundary/binary, "--\r\n">>)
+ couch_httpd:send_chunk(
+ Resp, <<"\r\n", "--", OuterBoundary/binary, "--\r\n">>
+ )
end,
couch_httpd:last_chunk(Resp)
end
end;
-db_req(#httpd{path_parts=[_, <<"_bulk_get">>]}=Req, _Db) ->
+db_req(#httpd{path_parts = [_, <<"_bulk_get">>]} = Req, _Db) ->
send_method_not_allowed(Req, "POST");
-
-db_req(#httpd{method='GET',path_parts=[_,OP]}=Req, Db) when ?IS_ALL_DOCS(OP) ->
+db_req(#httpd{method = 'GET', path_parts = [_, OP]} = Req, Db) when ?IS_ALL_DOCS(OP) ->
case chttpd:qs_json_value(Req, "keys", nil) of
- Keys when is_list(Keys) ->
- all_docs_view(Req, Db, Keys, OP);
- nil ->
- all_docs_view(Req, Db, undefined, OP);
- _ ->
- throw({bad_request, "`keys` parameter must be an array."})
+ Keys when is_list(Keys) ->
+ all_docs_view(Req, Db, Keys, OP);
+ nil ->
+ all_docs_view(Req, Db, undefined, OP);
+ _ ->
+ throw({bad_request, "`keys` parameter must be an array."})
end;
-
-db_req(#httpd{method='POST',
- path_parts=[_, OP, <<"queries">>]}=Req, Db) when ?IS_ALL_DOCS(OP) ->
+db_req(
+ #httpd{
+ method = 'POST',
+ path_parts = [_, OP, <<"queries">>]
+ } = Req,
+ Db
+) when ?IS_ALL_DOCS(OP) ->
Props = chttpd:json_body_obj(Req),
case couch_views_util:get_view_queries(Props) of
undefined ->
- throw({bad_request,
- <<"POST body must include `queries` parameter.">>});
+ throw({bad_request, <<"POST body must include `queries` parameter.">>});
Queries ->
multi_all_docs_view(Req, Db, OP, Queries)
end;
-
-db_req(#httpd{path_parts=[_, OP, <<"queries">>]}=Req,
- _Db) when ?IS_ALL_DOCS(OP) ->
+db_req(
+ #httpd{path_parts = [_, OP, <<"queries">>]} = Req,
+ _Db
+) when ?IS_ALL_DOCS(OP) ->
send_method_not_allowed(Req, "POST");
-
-db_req(#httpd{method='POST',path_parts=[_,OP]}=Req, Db) when ?IS_ALL_DOCS(OP) ->
+db_req(#httpd{method = 'POST', path_parts = [_, OP]} = Req, Db) when ?IS_ALL_DOCS(OP) ->
chttpd:validate_ctype(Req, "application/json"),
{Fields} = chttpd:json_body_obj(Req),
case couch_util:get_value(<<"keys">>, Fields, nil) of
- Keys when is_list(Keys) ->
- all_docs_view(Req, Db, Keys, OP);
- nil ->
- all_docs_view(Req, Db, undefined, OP);
- _ ->
- throw({bad_request, "`keys` body member must be an array."})
+ Keys when is_list(Keys) ->
+ all_docs_view(Req, Db, Keys, OP);
+ nil ->
+ all_docs_view(Req, Db, undefined, OP);
+ _ ->
+ throw({bad_request, "`keys` body member must be an array."})
end;
-
-db_req(#httpd{path_parts=[_,OP]}=Req, _Db) when ?IS_ALL_DOCS(OP) ->
+db_req(#httpd{path_parts = [_, OP]} = Req, _Db) when ?IS_ALL_DOCS(OP) ->
send_method_not_allowed(Req, "GET,HEAD,POST");
-
-db_req(#httpd{method='POST',path_parts=[_,<<"_missing_revs">>]}=Req, Db) ->
+db_req(#httpd{method = 'POST', path_parts = [_, <<"_missing_revs">>]} = Req, Db) ->
chttpd:validate_ctype(Req, "application/json"),
{JsonDocIdRevs} = chttpd:json_body_obj(Req),
case fabric2_db:get_missing_revs(Db, JsonDocIdRevs) of
{error, Reason} ->
chttpd:send_error(Req, Reason);
{ok, Results} ->
- Results2 = [{Id, couch_doc:revs_to_strs(Revs)} ||
- {Id, Revs, _} <- Results],
- send_json(Req, {[
- {missing_revs, {Results2}}
- ]})
+ Results2 = [
+ {Id, couch_doc:revs_to_strs(Revs)}
+ || {Id, Revs, _} <- Results
+ ],
+ send_json(
+ Req,
+ {[
+ {missing_revs, {Results2}}
+ ]}
+ )
end;
-
-db_req(#httpd{path_parts=[_,<<"_missing_revs">>]}=Req, _Db) ->
+db_req(#httpd{path_parts = [_, <<"_missing_revs">>]} = Req, _Db) ->
send_method_not_allowed(Req, "POST");
-
-db_req(#httpd{method='POST',path_parts=[_,<<"_revs_diff">>]}=Req, Db) ->
+db_req(#httpd{method = 'POST', path_parts = [_, <<"_revs_diff">>]} = Req, Db) ->
chttpd:validate_ctype(Req, "application/json"),
{JsonDocIdRevs} = chttpd:json_body_obj(Req),
case fabric2_db:get_missing_revs(Db, JsonDocIdRevs) of
@@ -630,23 +724,28 @@ db_req(#httpd{method='POST',path_parts=[_,<<"_revs_diff">>]}=Req, Db) ->
chttpd:send_error(Req, Reason);
{ok, Results} ->
Results2 =
- lists:map(fun({Id, MissingRevs, PossibleAncestors}) ->
- {Id,
- {[{missing, couch_doc:revs_to_strs(MissingRevs)}] ++
- if PossibleAncestors == [] ->
- [];
- true ->
- [{possible_ancestors,
- couch_doc:revs_to_strs(PossibleAncestors)}]
- end}}
- end, Results),
+ lists:map(
+ fun({Id, MissingRevs, PossibleAncestors}) ->
+ {Id, {
+ [{missing, couch_doc:revs_to_strs(MissingRevs)}] ++
+ if
+ PossibleAncestors == [] ->
+ [];
+ true ->
+ [
+ {possible_ancestors,
+ couch_doc:revs_to_strs(PossibleAncestors)}
+ ]
+ end
+ }}
+ end,
+ Results
+ ),
send_json(Req, {Results2})
end;
-
-db_req(#httpd{path_parts=[_,<<"_revs_diff">>]}=Req, _Db) ->
+db_req(#httpd{path_parts = [_, <<"_revs_diff">>]} = Req, _Db) ->
send_method_not_allowed(Req, "POST");
-
-db_req(#httpd{method = 'PUT',path_parts = [_, <<"_security">>]} = Req, Db) ->
+db_req(#httpd{method = 'PUT', path_parts = [_, <<"_security">>]} = Req, Db) ->
validate_security_can_be_edited(fabric2_db:name(Db)),
SecObj = chttpd:json_body(Req),
case fabric2_db:set_security(Db, SecObj) of
@@ -655,56 +754,48 @@ db_req(#httpd{method = 'PUT',path_parts = [_, <<"_security">>]} = Req, Db) ->
Else ->
throw(Else)
end;
-
-db_req(#httpd{method='GET',path_parts=[_,<<"_security">>]}=Req, Db) ->
+db_req(#httpd{method = 'GET', path_parts = [_, <<"_security">>]} = Req, Db) ->
send_json(Req, fabric2_db:get_security(Db));
-
-db_req(#httpd{path_parts=[_,<<"_security">>]}=Req, _Db) ->
+db_req(#httpd{path_parts = [_, <<"_security">>]} = Req, _Db) ->
send_method_not_allowed(Req, "PUT,GET");
-
-db_req(#httpd{method='PUT',path_parts=[_,<<"_revs_limit">>]}=Req, Db) ->
+db_req(#httpd{method = 'PUT', path_parts = [_, <<"_revs_limit">>]} = Req, Db) ->
Limit = chttpd:json_body(Req),
ok = fabric2_db:set_revs_limit(Db, Limit),
send_json(Req, {[{<<"ok">>, true}]});
-
-db_req(#httpd{method='GET',path_parts=[_,<<"_revs_limit">>]}=Req, Db) ->
+db_req(#httpd{method = 'GET', path_parts = [_, <<"_revs_limit">>]} = Req, Db) ->
send_json(Req, fabric2_db:get_revs_limit(Db));
-
-db_req(#httpd{path_parts=[_,<<"_revs_limit">>]}=Req, _Db) ->
+db_req(#httpd{path_parts = [_, <<"_revs_limit">>]} = Req, _Db) ->
send_method_not_allowed(Req, "PUT,GET");
-
% Special case to enable using an unencoded slash in the URL of design docs,
% as slashes in document IDs must otherwise be URL encoded.
-db_req(#httpd{method='GET', mochi_req=MochiReq, path_parts=[_DbName, <<"_design/", _/binary>> | _]}=Req, _Db) ->
+db_req(
+ #httpd{
+ method = 'GET',
+ mochi_req = MochiReq,
+ path_parts = [_DbName, <<"_design/", _/binary>> | _]
+ } = Req,
+ _Db
+) ->
[Head | Tail] = re:split(MochiReq:get(raw_path), "_design%2F", [{return, list}, caseless]),
chttpd:send_redirect(Req, Head ++ "_design/" ++ Tail);
-
-db_req(#httpd{path_parts=[_DbName,<<"_design">>,Name]}=Req, Db) ->
- db_doc_req(Req, Db, <<"_design/",Name/binary>>);
-
-db_req(#httpd{path_parts=[_DbName,<<"_design">>,Name|FileNameParts]}=Req, Db) ->
- db_attachment_req(Req, Db, <<"_design/",Name/binary>>, FileNameParts);
-
-
+db_req(#httpd{path_parts = [_DbName, <<"_design">>, Name]} = Req, Db) ->
+ db_doc_req(Req, Db, <<"_design/", Name/binary>>);
+db_req(#httpd{path_parts = [_DbName, <<"_design">>, Name | FileNameParts]} = Req, Db) ->
+ db_attachment_req(Req, Db, <<"_design/", Name/binary>>, FileNameParts);
% Special case to allow for accessing local documents without %2F
% encoding the docid. Throws out requests that don't have the second
% path part or that specify an attachment name.
-db_req(#httpd{path_parts=[_DbName, <<"_local">>]}, _Db) ->
+db_req(#httpd{path_parts = [_DbName, <<"_local">>]}, _Db) ->
throw({bad_request, <<"Invalid _local document id.">>});
-
-db_req(#httpd{path_parts=[_DbName, <<"_local/">>]}, _Db) ->
+db_req(#httpd{path_parts = [_DbName, <<"_local/">>]}, _Db) ->
throw({bad_request, <<"Invalid _local document id.">>});
-
-db_req(#httpd{path_parts=[_DbName, <<"_local">>, Name]}=Req, Db) ->
+db_req(#httpd{path_parts = [_DbName, <<"_local">>, Name]} = Req, Db) ->
db_doc_req(Req, Db, <<"_local/", Name/binary>>);
-
-db_req(#httpd{path_parts=[_DbName, <<"_local">> | _Rest]}, _Db) ->
+db_req(#httpd{path_parts = [_DbName, <<"_local">> | _Rest]}, _Db) ->
throw({bad_request, <<"_local documents do not accept attachments.">>});
-
-db_req(#httpd{path_parts=[_, DocId]}=Req, Db) ->
+db_req(#httpd{path_parts = [_, DocId]} = Req, Db) ->
db_doc_req(Req, Db, DocId);
-
-db_req(#httpd{path_parts=[_, DocId | FileNameParts]}=Req, Db) ->
+db_req(#httpd{path_parts = [_, DocId | FileNameParts]} = Req, Db) ->
db_attachment_req(Req, Db, DocId, FileNameParts).
multi_all_docs_view(Req, Db, OP, Queries) ->
@@ -716,9 +807,8 @@ multi_all_docs_view(Req, Db, OP, Queries) ->
paginate_multi_all_docs_view(Req, Db, OP, Args, Queries)
end.
-
stream_multi_all_docs_view(Req, Db, OP, Args0, Queries) ->
- Args1 = Args0#mrargs{view_type=map},
+ Args1 = Args0#mrargs{view_type = map},
ArgQueries = chttpd_view:parse_queries(Req, Args1, Queries, fun(QArgs) ->
set_namespace(OP, QArgs)
end),
@@ -732,20 +822,23 @@ stream_multi_all_docs_view(Req, Db, OP, Args0, Queries) ->
threshold = Max,
prepend = "\r\n"
},
- VAcc1 = lists:foldl(fun
- (#mrargs{keys = undefined} = ArgsIn, Acc0) ->
- send_all_docs(Db, ArgsIn, Acc0);
- (#mrargs{keys = Keys} = ArgsIn, Acc0) when is_list(Keys) ->
- Acc1 = send_all_docs_keys(Db, ArgsIn, Acc0),
- {ok, Acc2} = view_cb(complete, Acc1),
- Acc2
- end, VAcc0, ArgQueries),
+ VAcc1 = lists:foldl(
+ fun
+ (#mrargs{keys = undefined} = ArgsIn, Acc0) ->
+ send_all_docs(Db, ArgsIn, Acc0);
+ (#mrargs{keys = Keys} = ArgsIn, Acc0) when is_list(Keys) ->
+ Acc1 = send_all_docs_keys(Db, ArgsIn, Acc0),
+ {ok, Acc2} = view_cb(complete, Acc1),
+ Acc2
+ end,
+ VAcc0,
+ ArgQueries
+ ),
{ok, Resp1} = chttpd:send_delayed_chunk(VAcc1#vacc.resp, "\r\n]}"),
chttpd:end_delayed_json_response(Resp1).
-
paginate_multi_all_docs_view(Req, Db, OP, Args0, Queries) ->
- Args1 = Args0#mrargs{view_type=map},
+ Args1 = Args0#mrargs{view_type = map},
ArgQueries = chttpd_view:parse_queries(Req, Args1, Queries, fun(QArgs) ->
set_namespace(OP, QArgs)
end),
@@ -757,13 +850,17 @@ paginate_multi_all_docs_view(Req, Db, OP, Args0, Queries) ->
UpdateSeq = fabric2_db:get_update_seq(Db),
EtagTerm = {Parts, UpdateSeq, Args0},
Response = couch_views_http:paginated(
- Req, EtagTerm, PageSize, ArgQueries, KeyFun,
+ Req,
+ EtagTerm,
+ PageSize,
+ ArgQueries,
+ KeyFun,
fun(Args) ->
all_docs_paginated_cb(Db, Args)
- end),
+ end
+ ),
chttpd:send_json(Req, Response).
-
all_docs_view(Req, Db, Keys, OP) ->
Args = couch_views_http:parse_body_and_query(Req, Keys),
case couch_views_util:is_paginated(Args) of
@@ -774,7 +871,7 @@ all_docs_view(Req, Db, Keys, OP) ->
end.
stream_all_docs_view(Req, Db, Args0, OP) ->
- Args1 = Args0#mrargs{view_type=map},
+ Args1 = Args0#mrargs{view_type = map},
Args2 = couch_views_util:validate_args(Args1),
Args3 = set_namespace(OP, Args2),
Max = chttpd:chunked_response_buffer_size(),
@@ -793,9 +890,8 @@ stream_all_docs_view(Req, Db, Args0, OP) ->
{ok, VAcc2#vacc.resp}
end.
-
paginate_all_docs_view(Req, Db, Args0, OP) ->
- Args1 = Args0#mrargs{view_type=map},
+ Args1 = Args0#mrargs{view_type = map},
Args2 = chttpd_view:validate_args(Req, Args1),
Args3 = set_namespace(OP, Args2),
KeyFun = fun({Props}) ->
@@ -805,167 +901,180 @@ paginate_all_docs_view(Req, Db, Args0, OP) ->
UpdateSeq = fabric2_db:get_update_seq(Db),
EtagTerm = {Parts, UpdateSeq, Args3},
Response = couch_views_http:paginated(
- Req, EtagTerm, Args3, KeyFun,
+ Req,
+ EtagTerm,
+ Args3,
+ KeyFun,
fun(Args) ->
all_docs_paginated_cb(Db, Args)
- end),
+ end
+ ),
chttpd:send_json(Req, Response).
-
all_docs_paginated_cb(Db, Args) ->
- #vacc{meta=MetaMap, buffer=Items} = case Args#mrargs.keys of
- undefined ->
- send_all_docs(Db, Args, #vacc{paginated=true});
- Keys when is_list(Keys) ->
- send_all_docs_keys(Db, Args, #vacc{paginated=true})
- end,
+ #vacc{meta = MetaMap, buffer = Items} =
+ case Args#mrargs.keys of
+ undefined ->
+ send_all_docs(Db, Args, #vacc{paginated = true});
+ Keys when is_list(Keys) ->
+ send_all_docs_keys(Db, Args, #vacc{paginated = true})
+ end,
{MetaMap, Items}.
-
send_all_docs(Db, #mrargs{keys = undefined} = Args, VAcc0) ->
Opts0 = fabric2_util:all_docs_view_opts(Args),
NS = couch_util:get_value(namespace, Opts0),
- FoldFun = case NS of
- <<"_all_docs">> -> fold_docs;
- <<"_design">> -> fold_design_docs;
- <<"_local">> -> fold_local_docs
- end,
- Opts = case couch_views_util:is_paginated(Args) of
- false ->
- Opts0 ++ [{restart_tx, true}];
- true ->
- Opts0
- end,
+ FoldFun =
+ case NS of
+ <<"_all_docs">> -> fold_docs;
+ <<"_design">> -> fold_design_docs;
+ <<"_local">> -> fold_local_docs
+ end,
+ Opts =
+ case couch_views_util:is_paginated(Args) of
+ false ->
+ Opts0 ++ [{restart_tx, true}];
+ true ->
+ Opts0
+ end,
ViewCb = fun view_cb/2,
Acc = {iter, Db, Args, VAcc0},
{ok, {iter, _, _, VAcc1}} = fabric2_db:FoldFun(Db, ViewCb, Acc, Opts),
VAcc1.
-
send_all_docs_keys(Db, #mrargs{} = Args, VAcc0) ->
Keys = apply_args_to_keylist(Args, Args#mrargs.keys),
NS = couch_util:get_value(namespace, Args#mrargs.extra),
TotalRows = fabric2_db:get_doc_count(Db, NS),
- Meta = case Args#mrargs.update_seq of
- true ->
- UpdateSeq = fabric2_db:get_update_seq(Db),
- [{update_seq, UpdateSeq}];
- false ->
- []
- end ++ [{total, TotalRows}, {offset, null}],
+ Meta =
+ case Args#mrargs.update_seq of
+ true ->
+ UpdateSeq = fabric2_db:get_update_seq(Db),
+ [{update_seq, UpdateSeq}];
+ false ->
+ []
+ end ++ [{total, TotalRows}, {offset, null}],
{ok, VAcc1} = view_cb({meta, Meta}, VAcc0),
- DocOpts = case Args#mrargs.conflicts of
- true -> [conflicts | Args#mrargs.doc_options];
- _ -> Args#mrargs.doc_options
- end,
+ DocOpts =
+ case Args#mrargs.conflicts of
+ true -> [conflicts | Args#mrargs.doc_options];
+ _ -> Args#mrargs.doc_options
+ end,
IncludeDocs = Args#mrargs.include_docs,
OpenOpts = [deleted | DocOpts],
CB = fun(DocId, Doc, Acc) ->
- Row0 = case Doc of
- {not_found, missing} ->
- #view_row{key = DocId};
- {ok, #doc{deleted = true, revs = Revs}} ->
- {RevPos, [RevId | _]} = Revs,
- Value = {[
- {rev, couch_doc:rev_to_str({RevPos, RevId})},
- {deleted, true}
- ]},
- DocValue = if not IncludeDocs -> undefined; true ->
- null
- end,
- #view_row{
- key = DocId,
- id = DocId,
- value = Value,
- doc = DocValue
- };
- {ok, #doc{revs = Revs} = Doc0} ->
- {RevPos, [RevId | _]} = Revs,
- Value = {[
- {rev, couch_doc:rev_to_str({RevPos, RevId})}
- ]},
- DocValue = if not IncludeDocs -> undefined; true ->
- couch_doc:to_json_obj(Doc0, DocOpts)
- end,
- #view_row{
- key = DocId,
- id = DocId,
- value = Value,
- doc = DocValue
- }
- end,
+ Row0 =
+ case Doc of
+ {not_found, missing} ->
+ #view_row{key = DocId};
+ {ok, #doc{deleted = true, revs = Revs}} ->
+ {RevPos, [RevId | _]} = Revs,
+ Value =
+ {[
+ {rev, couch_doc:rev_to_str({RevPos, RevId})},
+ {deleted, true}
+ ]},
+ DocValue =
+ if
+ not IncludeDocs -> undefined;
+ true -> null
+ end,
+ #view_row{
+ key = DocId,
+ id = DocId,
+ value = Value,
+ doc = DocValue
+ };
+ {ok, #doc{revs = Revs} = Doc0} ->
+ {RevPos, [RevId | _]} = Revs,
+ Value =
+ {[
+ {rev, couch_doc:rev_to_str({RevPos, RevId})}
+ ]},
+ DocValue =
+ if
+ not IncludeDocs -> undefined;
+ true -> couch_doc:to_json_obj(Doc0, DocOpts)
+ end,
+ #view_row{
+ key = DocId,
+ id = DocId,
+ value = Value,
+ doc = DocValue
+ }
+ end,
Row1 = couch_views_http:transform_row(Row0),
view_cb(Row1, Acc)
end,
{ok, VAcc2} = fabric2_db:fold_docs(Db, Keys, CB, VAcc1, OpenOpts),
VAcc2.
-
apply_args_to_keylist(Args, Keys0) ->
- Keys1 = case Args#mrargs.direction of
- fwd -> Keys0;
- _ -> lists:reverse(Keys0)
- end,
- Keys2 = case Args#mrargs.skip < length(Keys1) of
- true -> lists:nthtail(Args#mrargs.skip, Keys1);
- false -> []
- end,
+ Keys1 =
+ case Args#mrargs.direction of
+ fwd -> Keys0;
+ _ -> lists:reverse(Keys0)
+ end,
+ Keys2 =
+ case Args#mrargs.skip < length(Keys1) of
+ true -> lists:nthtail(Args#mrargs.skip, Keys1);
+ false -> []
+ end,
case Args#mrargs.limit < length(Keys2) of
true -> lists:sublist(Keys2, Args#mrargs.limit);
false -> Keys2
end.
-
view_cb({row, Row}, {iter, Db, Args, VAcc}) ->
- NewRow = case lists:keymember(doc, 1, Row) of
- true ->
- chttpd_stats:incr_reads(),
- Row;
- false when Args#mrargs.include_docs ->
- {id, DocId} = lists:keyfind(id, 1, Row),
- chttpd_stats:incr_reads(),
- DocOpts = case Args#mrargs.conflicts of
- true -> [conflicts | Args#mrargs.doc_options];
- _ -> Args#mrargs.doc_options
- end,
- OpenOpts = [deleted | DocOpts],
- DocMember = case fabric2_db:open_doc(Db, DocId, OpenOpts) of
- {not_found, missing} ->
- [];
- {ok, #doc{deleted = true}} ->
- [{doc, null}];
- {ok, #doc{} = Doc} ->
- [{doc, couch_doc:to_json_obj(Doc, DocOpts)}]
- end,
- Row ++ DocMember;
- _ ->
- Row
- end,
+ NewRow =
+ case lists:keymember(doc, 1, Row) of
+ true ->
+ chttpd_stats:incr_reads(),
+ Row;
+ false when Args#mrargs.include_docs ->
+ {id, DocId} = lists:keyfind(id, 1, Row),
+ chttpd_stats:incr_reads(),
+ DocOpts =
+ case Args#mrargs.conflicts of
+ true -> [conflicts | Args#mrargs.doc_options];
+ _ -> Args#mrargs.doc_options
+ end,
+ OpenOpts = [deleted | DocOpts],
+ DocMember =
+ case fabric2_db:open_doc(Db, DocId, OpenOpts) of
+ {not_found, missing} ->
+ [];
+ {ok, #doc{deleted = true}} ->
+ [{doc, null}];
+ {ok, #doc{} = Doc} ->
+ [{doc, couch_doc:to_json_obj(Doc, DocOpts)}]
+ end,
+ Row ++ DocMember;
+ _ ->
+ Row
+ end,
chttpd_stats:incr_rows(),
{Go, NewVAcc} = couch_views_http:view_cb({row, NewRow}, VAcc),
{Go, {iter, Db, Args, NewVAcc}};
-
view_cb(Msg, {iter, Db, Args, VAcc}) ->
{Go, NewVAcc} = couch_views_http:view_cb(Msg, VAcc),
{Go, {iter, Db, Args, NewVAcc}};
-
view_cb(Msg, Acc) ->
couch_views_http:view_cb(Msg, Acc).
-db_doc_req(#httpd{method='DELETE'}=Req, Db, DocId) ->
+db_doc_req(#httpd{method = 'DELETE'} = Req, Db, DocId) ->
% check for the existence of the doc to handle the 404 case.
couch_doc_open(Db, DocId, nil, []),
case chttpd:qs_value(Req, "rev") of
- undefined ->
- Body = {[{<<"_deleted">>,true}]};
- Rev ->
- Body = {[{<<"_rev">>, ?l2b(Rev)},{<<"_deleted">>,true}]}
+ undefined ->
+ Body = {[{<<"_deleted">>, true}]};
+ Rev ->
+ Body = {[{<<"_rev">>, ?l2b(Rev)}, {<<"_deleted">>, true}]}
end,
Doc = couch_doc_from_req(Req, Db, DocId, Body),
send_updated_doc(Req, Db, DocId, Doc);
-
-db_doc_req(#httpd{method='GET', mochi_req=MochiReq}=Req, Db, DocId) ->
+db_doc_req(#httpd{method = 'GET', mochi_req = MochiReq} = Req, Db, DocId) ->
#doc_query_args{
rev = Rev,
open_revs = Revs,
@@ -973,306 +1082,382 @@ db_doc_req(#httpd{method='GET', mochi_req=MochiReq}=Req, Db, DocId) ->
atts_since = AttsSince
} = parse_doc_query(Req),
case Revs of
- [] ->
- Options2 =
- if AttsSince /= nil ->
- [{atts_since, AttsSince}, attachments | Options];
- true -> Options
- end,
- Doc = couch_doc_open(Db, DocId, Rev, Options2),
- send_doc(Req, Doc, Options2);
- _ ->
- case fabric2_db:open_doc_revs(Db, DocId, Revs, Options) of
- {ok, []} when Revs == all ->
- chttpd:send_error(Req, {not_found, missing});
- {ok, Results} ->
- chttpd_stats:incr_reads(length(Results)),
- case MochiReq:accepts_content_type("multipart/mixed") of
- false ->
- {ok, Resp} = start_json_response(Req, 200),
- send_chunk(Resp, "["),
- % We loop through the docs. The first time through the separator
- % is whitespace, then a comma on subsequent iterations.
- lists:foldl(
- fun(Result, AccSeparator) ->
- case Result of
- {ok, Doc} ->
- JsonDoc = couch_doc:to_json_obj(Doc, Options),
- Json = ?JSON_ENCODE({[{ok, JsonDoc}]}),
- send_chunk(Resp, AccSeparator ++ Json);
- {{not_found, missing}, RevId} ->
- RevStr = couch_doc:rev_to_str(RevId),
- Json = ?JSON_ENCODE({[{<<"missing">>, RevStr}]}),
- send_chunk(Resp, AccSeparator ++ Json)
- end,
- "," % AccSeparator now has a comma
- end,
- "", Results),
- send_chunk(Resp, "]"),
- end_json_response(Resp);
- true ->
- send_docs_multipart(Req, Results, Options)
- end;
- {error, Error} ->
- chttpd:send_error(Req, Error)
- end
+ [] ->
+ Options2 =
+ if
+ AttsSince /= nil ->
+ [{atts_since, AttsSince}, attachments | Options];
+ true ->
+ Options
+ end,
+ Doc = couch_doc_open(Db, DocId, Rev, Options2),
+ send_doc(Req, Doc, Options2);
+ _ ->
+ case fabric2_db:open_doc_revs(Db, DocId, Revs, Options) of
+ {ok, []} when Revs == all ->
+ chttpd:send_error(Req, {not_found, missing});
+ {ok, Results} ->
+ chttpd_stats:incr_reads(length(Results)),
+ case MochiReq:accepts_content_type("multipart/mixed") of
+ false ->
+ {ok, Resp} = start_json_response(Req, 200),
+ send_chunk(Resp, "["),
+ % We loop through the docs. The first time through the separator
+ % is whitespace, then a comma on subsequent iterations.
+ lists:foldl(
+ fun(Result, AccSeparator) ->
+ case Result of
+ {ok, Doc} ->
+ JsonDoc = couch_doc:to_json_obj(Doc, Options),
+ Json = ?JSON_ENCODE({[{ok, JsonDoc}]}),
+ send_chunk(Resp, AccSeparator ++ Json);
+ {{not_found, missing}, RevId} ->
+ RevStr = couch_doc:rev_to_str(RevId),
+ Json = ?JSON_ENCODE({[{<<"missing">>, RevStr}]}),
+ send_chunk(Resp, AccSeparator ++ Json)
+ end,
+ % AccSeparator now has a comma
+ ","
+ end,
+ "",
+ Results
+ ),
+ send_chunk(Resp, "]"),
+ end_json_response(Resp);
+ true ->
+ send_docs_multipart(Req, Results, Options)
+ end;
+ {error, Error} ->
+ chttpd:send_error(Req, Error)
+ end
end;
-
-db_doc_req(#httpd{method='POST'}=Req, Db, DocId) ->
+db_doc_req(#httpd{method = 'POST'} = Req, Db, DocId) ->
couch_httpd:validate_referer(Req),
fabric2_db:validate_docid(DocId),
chttpd:validate_ctype(Req, "multipart/form-data"),
Form = couch_httpd:parse_form(Req),
case proplists:is_defined("_doc", Form) of
- true ->
- Json = ?JSON_DECODE(couch_util:get_value("_doc", Form)),
- Doc = couch_doc_from_req(Req, Db, DocId, Json);
- false ->
- Rev = couch_doc:parse_rev(list_to_binary(couch_util:get_value("_rev", Form))),
- Doc = case fabric2_db:open_doc_revs(Db, DocId, [Rev], []) of
- {ok, [{ok, Doc0}]} ->
- chttpd_stats:incr_reads(),
- Doc0;
- {error, Error} ->
- throw(Error)
- end
+ true ->
+ Json = ?JSON_DECODE(couch_util:get_value("_doc", Form)),
+ Doc = couch_doc_from_req(Req, Db, DocId, Json);
+ false ->
+ Rev = couch_doc:parse_rev(list_to_binary(couch_util:get_value("_rev", Form))),
+ Doc =
+ case fabric2_db:open_doc_revs(Db, DocId, [Rev], []) of
+ {ok, [{ok, Doc0}]} ->
+ chttpd_stats:incr_reads(),
+ Doc0;
+ {error, Error} ->
+ throw(Error)
+ end
end,
UpdatedAtts = [
couch_att:new([
{name, validate_attachment_name(Name)},
{type, list_to_binary(ContentType)},
{data, Content}
- ]) ||
- {Name, {ContentType, _}, Content} <-
- proplists:get_all_values("_attachments", Form)
+ ])
+ || {Name, {ContentType, _}, Content} <-
+ proplists:get_all_values("_attachments", Form)
],
- #doc{atts=OldAtts} = Doc,
+ #doc{atts = OldAtts} = Doc,
OldAtts2 = lists:flatmap(
fun(Att) ->
OldName = couch_att:fetch(name, Att),
case [1 || A <- UpdatedAtts, couch_att:fetch(name, A) == OldName] of
- [] -> [Att]; % the attachment wasn't in the UpdatedAtts, return it
- _ -> [] % the attachment was in the UpdatedAtts, drop it
+ % the attachment wasn't in the UpdatedAtts, return it
+ [] -> [Att];
+ % the attachment was in the UpdatedAtts, drop it
+ _ -> []
end
- end, OldAtts),
+ end,
+ OldAtts
+ ),
NewDoc = Doc#doc{
atts = UpdatedAtts ++ OldAtts2
},
NewDoc1 = read_att_data(NewDoc),
case fabric2_db:update_doc(Db, NewDoc1, []) of
- {ok, NewRev} ->
- chttpd_stats:incr_writes(),
- HttpCode = 201;
- {accepted, NewRev} ->
- chttpd_stats:incr_writes(),
- HttpCode = 202
+ {ok, NewRev} ->
+ chttpd_stats:incr_writes(),
+ HttpCode = 201;
+ {accepted, NewRev} ->
+ chttpd_stats:incr_writes(),
+ HttpCode = 202
end,
- send_json(Req, HttpCode, [{"ETag", "\"" ++ ?b2l(couch_doc:rev_to_str(NewRev)) ++ "\""}], {[
- {ok, true},
- {id, DocId},
- {rev, couch_doc:rev_to_str(NewRev)}
- ]});
-
-db_doc_req(#httpd{method='PUT'}=Req, Db, DocId) ->
+ send_json(
+ Req,
+ HttpCode,
+ [{"ETag", "\"" ++ ?b2l(couch_doc:rev_to_str(NewRev)) ++ "\""}],
+ {[
+ {ok, true},
+ {id, DocId},
+ {rev, couch_doc:rev_to_str(NewRev)}
+ ]}
+ );
+db_doc_req(#httpd{method = 'PUT'} = Req, Db, DocId) ->
#doc_query_args{
update_type = UpdateType
} = parse_doc_query(Req),
DbName = fabric2_db:name(Db),
fabric2_db:validate_docid(DocId),
- Loc = absolute_uri(Req, [$/, couch_util:url_encode(DbName),
- $/, couch_util:url_encode(DocId)]),
+ Loc = absolute_uri(Req, [
+ $/,
+ couch_util:url_encode(DbName),
+ $/,
+ couch_util:url_encode(DocId)
+ ]),
RespHeaders = [{"Location", Loc}],
case couch_util:to_list(couch_httpd:header_value(Req, "Content-Type")) of
- ("multipart/related;" ++ _) = ContentType ->
- couch_httpd:check_max_request_length(Req),
- couch_httpd_multipart:num_mp_writers(1),
- {ok, Doc0, WaitFun, Parser} = couch_doc:doc_from_multi_part_stream(ContentType,
- fun() -> receive_request_data(Req) end),
- Doc = couch_doc_from_req(Req, Db, DocId, Doc0),
- try
- Result = send_updated_doc(Req, Db, DocId, Doc, RespHeaders, UpdateType),
- WaitFun(),
- Result
- catch throw:Err ->
- % Document rejected by a validate_doc_update function.
- couch_httpd_multipart:abort_multipart_stream(Parser),
- throw(Err)
- end;
- _Else ->
- case chttpd:qs_value(Req, "batch") of
- "ok" ->
- % batch
- Doc0 = couch_doc_from_req(Req, Db, DocId, chttpd:json_body(Req)),
- Doc = read_att_data(Doc0),
- spawn(fun() ->
- case catch(fabric2_db:update_doc(Db, Doc, [])) of
- {ok, _} ->
- chttpd_stats:incr_writes(),
- ok;
- {accepted, _} ->
- chttpd_stats:incr_writes(),
- ok;
- Error ->
- ?LOG_NOTICE(#{
- what => async_update_error,
- db => DbName,
- docid => DocId,
- details => Error
- }),
- couch_log:notice("Batch doc error (~s): ~p",[DocId, Error])
- end
- end),
- send_json(Req, 202, [], {[
- {ok, true},
- {id, DocId}
- ]});
- _Normal ->
- % normal
- Body = chttpd:json_body(Req),
- Doc = couch_doc_from_req(Req, Db, DocId, Body),
- send_updated_doc(Req, Db, DocId, Doc, RespHeaders, UpdateType)
- end
+ ("multipart/related;" ++ _) = ContentType ->
+ couch_httpd:check_max_request_length(Req),
+ couch_httpd_multipart:num_mp_writers(1),
+ {ok, Doc0, WaitFun, Parser} = couch_doc:doc_from_multi_part_stream(
+ ContentType,
+ fun() -> receive_request_data(Req) end
+ ),
+ Doc = couch_doc_from_req(Req, Db, DocId, Doc0),
+ try
+ Result = send_updated_doc(Req, Db, DocId, Doc, RespHeaders, UpdateType),
+ WaitFun(),
+ Result
+ catch
+ throw:Err ->
+ % Document rejected by a validate_doc_update function.
+ couch_httpd_multipart:abort_multipart_stream(Parser),
+ throw(Err)
+ end;
+ _Else ->
+ case chttpd:qs_value(Req, "batch") of
+ "ok" ->
+ % batch
+ Doc0 = couch_doc_from_req(Req, Db, DocId, chttpd:json_body(Req)),
+ Doc = read_att_data(Doc0),
+ spawn(fun() ->
+ case catch (fabric2_db:update_doc(Db, Doc, [])) of
+ {ok, _} ->
+ chttpd_stats:incr_writes(),
+ ok;
+ {accepted, _} ->
+ chttpd_stats:incr_writes(),
+ ok;
+ Error ->
+ ?LOG_NOTICE(#{
+ what => async_update_error,
+ db => DbName,
+ docid => DocId,
+ details => Error
+ }),
+ couch_log:notice("Batch doc error (~s): ~p", [DocId, Error])
+ end
+ end),
+ send_json(
+ Req,
+ 202,
+ [],
+ {[
+ {ok, true},
+ {id, DocId}
+ ]}
+ );
+ _Normal ->
+ % normal
+ Body = chttpd:json_body(Req),
+ Doc = couch_doc_from_req(Req, Db, DocId, Body),
+ send_updated_doc(Req, Db, DocId, Doc, RespHeaders, UpdateType)
+ end
end;
-
-db_doc_req(#httpd{method='COPY'}=Req, Db, SourceDocId) ->
+db_doc_req(#httpd{method = 'COPY'} = Req, Db, SourceDocId) ->
SourceRev =
- case extract_header_rev(Req, chttpd:qs_value(Req, "rev")) of
- missing_rev -> nil;
- Rev -> Rev
- end,
+ case extract_header_rev(Req, chttpd:qs_value(Req, "rev")) of
+ missing_rev -> nil;
+ Rev -> Rev
+ end,
{TargetDocId0, TargetRevs} = chttpd_util:parse_copy_destination_header(Req),
TargetDocId = list_to_binary(mochiweb_util:unquote(TargetDocId0)),
% open old doc
Doc = couch_doc_open(Db, SourceDocId, SourceRev, []),
% save new doc
- case fabric2_db:update_doc(Db,
- Doc#doc{id=TargetDocId, revs=TargetRevs}, []) of
- {ok, NewTargetRev} ->
- chttpd_stats:incr_writes(),
- HttpCode = 201;
- {accepted, NewTargetRev} ->
- chttpd_stats:incr_writes(),
- HttpCode = 202
+ case
+ fabric2_db:update_doc(
+ Db,
+ Doc#doc{id = TargetDocId, revs = TargetRevs},
+ []
+ )
+ of
+ {ok, NewTargetRev} ->
+ chttpd_stats:incr_writes(),
+ HttpCode = 201;
+ {accepted, NewTargetRev} ->
+ chttpd_stats:incr_writes(),
+ HttpCode = 202
end,
% respond
DbName = fabric2_db:name(Db),
{PartRes} = update_doc_result_to_json(TargetDocId, {ok, NewTargetRev}),
- Loc = absolute_uri(Req, "/" ++ couch_util:url_encode(DbName) ++ "/" ++ couch_util:url_encode(TargetDocId)),
- send_json(Req, HttpCode,
- [{"Location", Loc},
- {"ETag", "\"" ++ ?b2l(couch_doc:rev_to_str(NewTargetRev)) ++ "\""}],
- {PartRes});
-
+ Loc = absolute_uri(
+ Req, "/" ++ couch_util:url_encode(DbName) ++ "/" ++ couch_util:url_encode(TargetDocId)
+ ),
+ send_json(
+ Req,
+ HttpCode,
+ [
+ {"Location", Loc},
+ {"ETag", "\"" ++ ?b2l(couch_doc:rev_to_str(NewTargetRev)) ++ "\""}
+ ],
+ {PartRes}
+ );
db_doc_req(Req, _Db, _DocId) ->
send_method_not_allowed(Req, "DELETE,GET,HEAD,POST,PUT,COPY").
send_doc(Req, Doc, Options) ->
case Doc#doc.meta of
- [] ->
- DiskEtag = couch_httpd:doc_etag(Doc),
- % output etag only when we have no meta
- chttpd:etag_respond(Req, DiskEtag, fun() ->
- send_doc_efficiently(Req, Doc, [{"ETag", DiskEtag}], Options)
- end);
- _ ->
- send_doc_efficiently(Req, Doc, [], Options)
+ [] ->
+ DiskEtag = couch_httpd:doc_etag(Doc),
+ % output etag only when we have no meta
+ chttpd:etag_respond(Req, DiskEtag, fun() ->
+ send_doc_efficiently(Req, Doc, [{"ETag", DiskEtag}], Options)
+ end);
+ _ ->
+ send_doc_efficiently(Req, Doc, [], Options)
end.
-send_doc_efficiently(Req, #doc{atts=[]}=Doc, Headers, Options) ->
- send_json(Req, 200, Headers, couch_doc:to_json_obj(Doc, Options));
-send_doc_efficiently(#httpd{mochi_req=MochiReq}=Req, #doc{atts=Atts}=Doc, Headers, Options) ->
+send_doc_efficiently(Req, #doc{atts = []} = Doc, Headers, Options) ->
+ send_json(Req, 200, Headers, couch_doc:to_json_obj(Doc, Options));
+send_doc_efficiently(#httpd{mochi_req = MochiReq} = Req, #doc{atts = Atts} = Doc, Headers, Options) ->
case lists:member(attachments, Options) of
- true ->
- Refs = monitor_attachments(Atts),
- try
- case MochiReq:accepts_content_type("multipart/related") of
- false ->
- send_json(Req, 200, Headers, couch_doc:to_json_obj(Doc, Options));
true ->
- Boundary = couch_uuids:random(),
- JsonBytes = ?JSON_ENCODE(couch_doc:to_json_obj(Doc,
- [attachments, follows, att_encoding_info | Options])),
- {ContentType, Len} = couch_doc:len_doc_to_multi_part_stream(
- Boundary,JsonBytes, Atts, true),
- CType = {"Content-Type", ContentType},
- {ok, Resp} = start_response_length(Req, 200, [CType|Headers], Len),
- couch_doc:doc_to_multi_part_stream(Boundary,JsonBytes,Atts,
- fun(Data) -> couch_httpd:send(Resp, Data) end, true)
- end
- after
- demonitor_refs(Refs)
- end;
- false ->
- send_json(Req, 200, Headers, couch_doc:to_json_obj(Doc, Options))
+ Refs = monitor_attachments(Atts),
+ try
+ case MochiReq:accepts_content_type("multipart/related") of
+ false ->
+ send_json(Req, 200, Headers, couch_doc:to_json_obj(Doc, Options));
+ true ->
+ Boundary = couch_uuids:random(),
+ JsonBytes = ?JSON_ENCODE(
+ couch_doc:to_json_obj(
+ Doc,
+ [attachments, follows, att_encoding_info | Options]
+ )
+ ),
+ {ContentType, Len} = couch_doc:len_doc_to_multi_part_stream(
+ Boundary, JsonBytes, Atts, true
+ ),
+ CType = {"Content-Type", ContentType},
+ {ok, Resp} = start_response_length(Req, 200, [CType | Headers], Len),
+ couch_doc:doc_to_multi_part_stream(
+ Boundary,
+ JsonBytes,
+ Atts,
+ fun(Data) -> couch_httpd:send(Resp, Data) end,
+ true
+ )
+ end
+ after
+ demonitor_refs(Refs)
+ end;
+ false ->
+ send_json(Req, 200, Headers, couch_doc:to_json_obj(Doc, Options))
end.
send_docs_multipart_bulk_get(Results, Options0, OuterBoundary, Resp) ->
InnerBoundary = bulk_get_multipart_boundary(),
Options = [attachments, follows, att_encoding_info | Options0],
lists:foreach(
- fun({ok, #doc{id=Id, revs=Revs, atts=Atts}=Doc}) ->
- Refs = monitor_attachments(Doc#doc.atts),
- try
- JsonBytes = ?JSON_ENCODE(couch_doc:to_json_obj(Doc, Options)),
- couch_httpd:send_chunk(Resp, <<"\r\n--", OuterBoundary/binary>>),
- case Atts of
- [] ->
- couch_httpd:send_chunk(Resp, <<"\r\nContent-Type: application/json\r\n\r\n">>);
- _ ->
- lists:foreach(fun(Header) -> couch_httpd:send_chunk(Resp, Header) end,
- bulk_get_multipart_headers(Revs, Id, InnerBoundary))
- end,
- couch_doc:doc_to_multi_part_stream(InnerBoundary, JsonBytes, Atts,
- fun(Data) -> couch_httpd:send_chunk(Resp, Data)
- end, true)
- after
- demonitor_refs(Refs)
- end;
- ({{not_found, missing}, RevId}) ->
- RevStr = couch_doc:rev_to_str(RevId),
- Json = ?JSON_ENCODE({[{<<"rev">>, RevStr},
- {<<"error">>, <<"not_found">>},
- {<<"reason">>, <<"missing">>}]}),
- couch_httpd:send_chunk(Resp,
- [<<"\r\n--", OuterBoundary/binary>>,
- <<"\r\nContent-Type: application/json; error=\"true\"\r\n\r\n">>,
- Json])
- end, Results).
+ fun
+ ({ok, #doc{id = Id, revs = Revs, atts = Atts} = Doc}) ->
+ Refs = monitor_attachments(Doc#doc.atts),
+ try
+ JsonBytes = ?JSON_ENCODE(couch_doc:to_json_obj(Doc, Options)),
+ couch_httpd:send_chunk(Resp, <<"\r\n--", OuterBoundary/binary>>),
+ case Atts of
+ [] ->
+ couch_httpd:send_chunk(
+ Resp, <<"\r\nContent-Type: application/json\r\n\r\n">>
+ );
+ _ ->
+ lists:foreach(
+ fun(Header) -> couch_httpd:send_chunk(Resp, Header) end,
+ bulk_get_multipart_headers(Revs, Id, InnerBoundary)
+ )
+ end,
+ couch_doc:doc_to_multi_part_stream(
+ InnerBoundary,
+ JsonBytes,
+ Atts,
+ fun(Data) -> couch_httpd:send_chunk(Resp, Data) end,
+ true
+ )
+ after
+ demonitor_refs(Refs)
+ end;
+ ({{not_found, missing}, RevId}) ->
+ RevStr = couch_doc:rev_to_str(RevId),
+ Json = ?JSON_ENCODE(
+ {[
+ {<<"rev">>, RevStr},
+ {<<"error">>, <<"not_found">>},
+ {<<"reason">>, <<"missing">>}
+ ]}
+ ),
+ couch_httpd:send_chunk(
+ Resp,
+ [
+ <<"\r\n--", OuterBoundary/binary>>,
+ <<"\r\nContent-Type: application/json; error=\"true\"\r\n\r\n">>,
+ Json
+ ]
+ )
+ end,
+ Results
+ ).
send_docs_multipart(Req, Results, Options1) ->
OuterBoundary = couch_uuids:random(),
InnerBoundary = couch_uuids:random(),
Options = [attachments, follows, att_encoding_info | Options1],
- CType = {"Content-Type",
- "multipart/mixed; boundary=\"" ++ ?b2l(OuterBoundary) ++ "\""},
+ CType = {"Content-Type", "multipart/mixed; boundary=\"" ++ ?b2l(OuterBoundary) ++ "\""},
{ok, Resp} = start_chunked_response(Req, 200, [CType]),
chttpd:send_chunk(Resp, <<"--", OuterBoundary/binary>>),
lists:foreach(
- fun({ok, #doc{atts=Atts}=Doc}) ->
- Refs = monitor_attachments(Doc#doc.atts),
- try
- JsonBytes = ?JSON_ENCODE(couch_doc:to_json_obj(Doc, Options)),
- {ContentType, _Len} = couch_doc:len_doc_to_multi_part_stream(
- InnerBoundary, JsonBytes, Atts, true),
- chttpd:send_chunk(Resp, <<"\r\nContent-Type: ",
- ContentType/binary, "\r\n\r\n">>),
- couch_doc:doc_to_multi_part_stream(InnerBoundary, JsonBytes, Atts,
- fun(Data) -> chttpd:send_chunk(Resp, Data)
- end, true),
- chttpd:send_chunk(Resp, <<"\r\n--", OuterBoundary/binary>>)
- after
- demonitor_refs(Refs)
- end;
- ({{not_found, missing}, RevId}) ->
- RevStr = couch_doc:rev_to_str(RevId),
- Json = ?JSON_ENCODE({[{<<"missing">>, RevStr}]}),
- chttpd:send_chunk(Resp,
- [<<"\r\nContent-Type: application/json; error=\"true\"\r\n\r\n">>,
- Json,
- <<"\r\n--", OuterBoundary/binary>>])
- end, Results),
+ fun
+ ({ok, #doc{atts = Atts} = Doc}) ->
+ Refs = monitor_attachments(Doc#doc.atts),
+ try
+ JsonBytes = ?JSON_ENCODE(couch_doc:to_json_obj(Doc, Options)),
+ {ContentType, _Len} = couch_doc:len_doc_to_multi_part_stream(
+ InnerBoundary, JsonBytes, Atts, true
+ ),
+ chttpd:send_chunk(
+ Resp, <<"\r\nContent-Type: ", ContentType/binary, "\r\n\r\n">>
+ ),
+ couch_doc:doc_to_multi_part_stream(
+ InnerBoundary,
+ JsonBytes,
+ Atts,
+ fun(Data) -> chttpd:send_chunk(Resp, Data) end,
+ true
+ ),
+ chttpd:send_chunk(Resp, <<"\r\n--", OuterBoundary/binary>>)
+ after
+ demonitor_refs(Refs)
+ end;
+ ({{not_found, missing}, RevId}) ->
+ RevStr = couch_doc:rev_to_str(RevId),
+ Json = ?JSON_ENCODE({[{<<"missing">>, RevStr}]}),
+ chttpd:send_chunk(
+ Resp,
+ [
+ <<"\r\nContent-Type: application/json; error=\"true\"\r\n\r\n">>,
+ Json,
+ <<"\r\n--", OuterBoundary/binary>>
+ ]
+ )
+ end,
+ Results
+ ),
chttpd:send_chunk(Resp, <<"--">>),
chttpd:last_chunk(Resp).
@@ -1281,7 +1466,7 @@ bulk_get_multipart_headers({0, []}, Id, Boundary) ->
<<"\r\nX-Doc-Id: ", Id/binary>>,
<<"\r\nContent-Type: multipart/related; boundary=", Boundary/binary, "\r\n\r\n">>
];
-bulk_get_multipart_headers({Start, [FirstRevId|_]}, Id, Boundary) ->
+bulk_get_multipart_headers({Start, [FirstRevId | _]}, Id, Boundary) ->
RevStr = couch_doc:rev_to_str({Start, FirstRevId}),
[
<<"\r\nX-Doc-Id: ", Id/binary>>,
@@ -1302,9 +1487,17 @@ receive_request_data(Req, Len) when Len == chunked ->
self() ! {chunk, Ref, Binary}
end,
couch_httpd:recv_chunked(Req, 4096, ChunkFun, ok),
- GetChunk = fun GC() -> receive {chunk, Ref, Binary} -> {Binary, GC} end end,
- {receive {chunk, Ref, Binary} -> Binary end, GetChunk};
-
+ GetChunk = fun GC() ->
+ receive
+ {chunk, Ref, Binary} -> {Binary, GC}
+ end
+ end,
+ {
+ receive
+ {chunk, Ref, Binary} -> Binary
+ end,
+ GetChunk
+ };
receive_request_data(Req, LenLeft) when LenLeft > 0 ->
Len = erlang:min(4096, LenLeft),
Data = chttpd:recv(Req, Len),
@@ -1313,11 +1506,15 @@ receive_request_data(_Req, _) ->
throw(<<"expected more data">>).
update_doc_result_to_json({{Id, Rev}, Error}) ->
- {_Code, Err, Msg} = chttpd:error_info(Error),
- {[{id, Id}, {rev, couch_doc:rev_to_str(Rev)},
- {error, Err}, {reason, Msg}]}.
-
-update_doc_result_to_json(#doc{id=DocId}, Result) ->
+ {_Code, Err, Msg} = chttpd:error_info(Error),
+ {[
+ {id, Id},
+ {rev, couch_doc:rev_to_str(Rev)},
+ {error, Err},
+ {reason, Msg}
+ ]}.
+
+update_doc_result_to_json(#doc{id = DocId}, Result) ->
update_doc_result_to_json(DocId, Result);
update_doc_result_to_json(DocId, {ok, NewRev}) ->
{[{ok, true}, {id, DocId}, {rev, couch_doc:rev_to_str(NewRev)}]};
@@ -1335,19 +1532,29 @@ send_updated_doc(Req, Db, DocId, Json) ->
send_updated_doc(Req, Db, DocId, Doc, Headers) ->
send_updated_doc(Req, Db, DocId, Doc, Headers, interactive_edit).
-send_updated_doc(#httpd{} = Req, Db, DocId, #doc{deleted=Deleted}=Doc,
- Headers, UpdateType) ->
+send_updated_doc(
+ #httpd{} = Req,
+ Db,
+ DocId,
+ #doc{deleted = Deleted} = Doc,
+ Headers,
+ UpdateType
+) ->
Options =
case couch_httpd:header_value(Req, "X-Couch-Full-Commit") of
- "true" ->
- [full_commit, UpdateType];
- "false" ->
- [delay_commit, UpdateType];
- _ ->
- [UpdateType]
+ "true" ->
+ [full_commit, UpdateType];
+ "false" ->
+ [delay_commit, UpdateType];
+ _ ->
+ [UpdateType]
end,
- {Status, {etag, Etag}, Body} = update_doc(Db, DocId,
- #doc{deleted=Deleted}=Doc, Options),
+ {Status, {etag, Etag}, Body} = update_doc(
+ Db,
+ DocId,
+ #doc{deleted = Deleted} = Doc,
+ Options
+ ),
HttpCode = http_code_from_status(Status),
ResponseHeaders = [{"ETag", Etag} | Headers],
send_json(Req, HttpCode, ResponseHeaders, Body).
@@ -1362,341 +1569,422 @@ http_code_from_status(Status) ->
200
end.
-update_doc(Db, DocId, #doc{deleted=Deleted, body=DocBody}=Doc0, Options) ->
+update_doc(Db, DocId, #doc{deleted = Deleted, body = DocBody} = Doc0, Options) ->
Doc = read_att_data(Doc0),
case fabric2_db:update_doc(Db, Doc, Options) of
- {ok, NewRev} ->
- Accepted = false;
- {accepted, NewRev} ->
- Accepted = true
+ {ok, NewRev} ->
+ Accepted = false;
+ {accepted, NewRev} ->
+ Accepted = true
end,
Etag = couch_httpd:doc_etag(DocId, DocBody, NewRev),
- Status = case {Accepted, Deleted} of
- {true, _} ->
- accepted;
- {false, true} ->
- ok;
- {false, false} ->
- created
- end,
+ Status =
+ case {Accepted, Deleted} of
+ {true, _} ->
+ accepted;
+ {false, true} ->
+ ok;
+ {false, false} ->
+ created
+ end,
NewRevStr = couch_doc:rev_to_str(NewRev),
Body = {[{ok, true}, {id, DocId}, {rev, NewRevStr}]},
{Status, {etag, Etag}, Body}.
-couch_doc_from_req(Req, _Db, DocId, #doc{revs=Revs} = Doc) ->
+couch_doc_from_req(Req, _Db, DocId, #doc{revs = Revs} = Doc) ->
validate_attachment_names(Doc),
- Rev = case chttpd:qs_value(Req, "rev") of
- undefined ->
- undefined;
- QSRev ->
- couch_doc:parse_rev(QSRev)
- end,
+ Rev =
+ case chttpd:qs_value(Req, "rev") of
+ undefined ->
+ undefined;
+ QSRev ->
+ couch_doc:parse_rev(QSRev)
+ end,
Revs2 =
- case Revs of
- {Start, [RevId|_]} ->
- if Rev /= undefined andalso Rev /= {Start, RevId} ->
- throw({bad_request, "Document rev from request body and query "
- "string have different values"});
- true ->
- case extract_header_rev(Req, {Start, RevId}) of
- missing_rev -> {0, []};
- _ -> Revs
- end
- end;
- _ ->
- case extract_header_rev(Req, Rev) of
- missing_rev -> {0, []};
- {Pos, RevId2} -> {Pos, [RevId2]}
- end
- end,
- Doc#doc{id=DocId, revs=Revs2};
+ case Revs of
+ {Start, [RevId | _]} ->
+ if
+ Rev /= undefined andalso Rev /= {Start, RevId} ->
+ throw(
+ {bad_request,
+ "Document rev from request body and query "
+ "string have different values"}
+ );
+ true ->
+ case extract_header_rev(Req, {Start, RevId}) of
+ missing_rev -> {0, []};
+ _ -> Revs
+ end
+ end;
+ _ ->
+ case extract_header_rev(Req, Rev) of
+ missing_rev -> {0, []};
+ {Pos, RevId2} -> {Pos, [RevId2]}
+ end
+ end,
+ Doc#doc{id = DocId, revs = Revs2};
couch_doc_from_req(Req, Db, DocId, Json) ->
Doc = couch_doc:from_json_obj_validate(Json, fabric2_db:name(Db)),
couch_doc_from_req(Req, Db, DocId, Doc).
-
% Useful for debugging
% couch_doc_open(Db, DocId) ->
% couch_doc_open(Db, DocId, nil, []).
couch_doc_open(Db, DocId, Rev, Options) ->
case Rev of
- nil -> % open most recent rev
- case fabric2_db:open_doc(Db, DocId, Options) of
- {ok, Doc} ->
- chttpd_stats:incr_reads(),
- Doc;
- Error ->
- throw(Error)
- end;
- _ -> % open a specific rev (deletions come back as stubs)
- case fabric2_db:open_doc_revs(Db, DocId, [Rev], Options) of
- {ok, [{ok, Doc}]} ->
- chttpd_stats:incr_reads(),
- Doc;
- {ok, [{{not_found, missing}, Rev}]} ->
- throw(not_found);
- {ok, [Else]} ->
- throw(Else);
- {error, Error} ->
- throw(Error)
- end
- end.
+ % open most recent rev
+ nil ->
+ case fabric2_db:open_doc(Db, DocId, Options) of
+ {ok, Doc} ->
+ chttpd_stats:incr_reads(),
+ Doc;
+ Error ->
+ throw(Error)
+ end;
+ % open a specific rev (deletions come back as stubs)
+ _ ->
+ case fabric2_db:open_doc_revs(Db, DocId, [Rev], Options) of
+ {ok, [{ok, Doc}]} ->
+ chttpd_stats:incr_reads(),
+ Doc;
+ {ok, [{{not_found, missing}, Rev}]} ->
+ throw(not_found);
+ {ok, [Else]} ->
+ throw(Else);
+ {error, Error} ->
+ throw(Error)
+ end
+ end.
% Attachment request handlers
-db_attachment_req(#httpd{method='GET',mochi_req=MochiReq}=Req, Db, DocId, FileNameParts) ->
- FileName = list_to_binary(mochiweb_util:join(lists:map(fun binary_to_list/1,
- FileNameParts),"/")),
+db_attachment_req(#httpd{method = 'GET', mochi_req = MochiReq} = Req, Db, DocId, FileNameParts) ->
+ FileName = list_to_binary(
+ mochiweb_util:join(
+ lists:map(
+ fun binary_to_list/1,
+ FileNameParts
+ ),
+ "/"
+ )
+ ),
#doc_query_args{
- rev=Rev,
- options=Options
+ rev = Rev,
+ options = Options
} = parse_doc_query(Req),
#doc{
- atts=Atts
+ atts = Atts
} = Doc = couch_doc_open(Db, DocId, Rev, Options),
case [A || A <- Atts, couch_att:fetch(name, A) == FileName] of
- [] ->
- throw({not_found, "Document is missing attachment"});
- [Att] ->
- [Type, Enc, DiskLen, AttLen, Md5] = couch_att:fetch([type, encoding, disk_len, att_len, md5], Att),
- Refs = monitor_attachments(Att),
- try
- Etag = case Md5 of
- <<>> -> chttpd:doc_etag(Doc);
- _ -> "\"" ++ ?b2l(base64:encode(Md5)) ++ "\""
- end,
- ReqAcceptsAttEnc = lists:member(
- atom_to_list(Enc),
- couch_httpd:accepted_encodings(Req)
- ),
- Headers = [
- {"ETag", Etag},
- {"Cache-Control", "must-revalidate"},
- {"Content-Type", binary_to_list(Type)}
- ] ++ case ReqAcceptsAttEnc of
- true when Enc =/= identity ->
- % RFC 2616 says that the 'identify' encoding should not be used in
- % the Content-Encoding header
- [{"Content-Encoding", atom_to_list(Enc)}];
- _ ->
- []
- end ++ case Enc of
- identity ->
- [{"Accept-Ranges", "bytes"}];
- _ ->
- [{"Accept-Ranges", "none"}]
- end,
- Len = case {Enc, ReqAcceptsAttEnc} of
- {identity, _} ->
- % stored and served in identity form
- DiskLen;
- {_, false} when DiskLen =/= AttLen ->
- % Stored encoded, but client doesn't accept the encoding we used,
- % so we need to decode on the fly. DiskLen is the identity length
- % of the attachment.
- DiskLen;
- {_, true} ->
- % Stored and served encoded. AttLen is the encoded length.
- AttLen;
- _ ->
- % We received an encoded attachment and stored it as such, so we
- % don't know the identity length. The client doesn't accept the
- % encoding, and since we cannot serve a correct Content-Length
- % header we'll fall back to a chunked response.
- undefined
- end,
- AttFun = case ReqAcceptsAttEnc of
- false ->
- fun couch_att:foldl_decode/3;
- true ->
- fun couch_att:foldl/3
- end,
- chttpd:etag_respond(
- Req,
- Etag,
- fun() ->
- case Len of
- undefined ->
- {ok, Resp} = start_chunked_response(Req, 200, Headers),
- AttFun(Att, fun(Seg, _) -> send_chunk(Resp, Seg) end, {ok, Resp}),
- couch_httpd:last_chunk(Resp);
- _ ->
- Ranges = parse_ranges(MochiReq:get(range), Len),
- case {Enc, Ranges} of
- {identity, [{From, To}]} ->
- Headers1 = [{"Content-Range", make_content_range(From, To, Len)}]
- ++ Headers,
- {ok, Resp} = start_response_length(Req, 206, Headers1, To - From + 1),
- couch_att:range_foldl(Att, From, To + 1,
- fun(Seg, _) -> send(Resp, Seg) end, {ok, Resp});
- {identity, Ranges} when is_list(Ranges) andalso length(Ranges) < 10 ->
- send_ranges_multipart(Req, Type, Len, Att, Ranges);
+ [] ->
+ throw({not_found, "Document is missing attachment"});
+ [Att] ->
+ [Type, Enc, DiskLen, AttLen, Md5] = couch_att:fetch(
+ [type, encoding, disk_len, att_len, md5], Att
+ ),
+ Refs = monitor_attachments(Att),
+ try
+ Etag =
+ case Md5 of
+ <<>> -> chttpd:doc_etag(Doc);
+ _ -> "\"" ++ ?b2l(base64:encode(Md5)) ++ "\""
+ end,
+ ReqAcceptsAttEnc = lists:member(
+ atom_to_list(Enc),
+ couch_httpd:accepted_encodings(Req)
+ ),
+ Headers =
+ [
+ {"ETag", Etag},
+ {"Cache-Control", "must-revalidate"},
+ {"Content-Type", binary_to_list(Type)}
+ ] ++
+ case ReqAcceptsAttEnc of
+ true when Enc =/= identity ->
+ % RFC 2616 says that the 'identify' encoding should not be used in
+ % the Content-Encoding header
+ [{"Content-Encoding", atom_to_list(Enc)}];
+ _ ->
+ []
+ end ++
+ case Enc of
+ identity ->
+ [{"Accept-Ranges", "bytes"}];
+ _ ->
+ [{"Accept-Ranges", "none"}]
+ end,
+ Len =
+ case {Enc, ReqAcceptsAttEnc} of
+ {identity, _} ->
+ % stored and served in identity form
+ DiskLen;
+ {_, false} when DiskLen =/= AttLen ->
+ % Stored encoded, but client doesn't accept the encoding we used,
+ % so we need to decode on the fly. DiskLen is the identity length
+ % of the attachment.
+ DiskLen;
+ {_, true} ->
+ % Stored and served encoded. AttLen is the encoded length.
+ AttLen;
_ ->
- Headers1 = Headers ++
- if Enc =:= identity orelse ReqAcceptsAttEnc =:= true ->
- [{"Content-MD5", base64:encode(couch_att:fetch(md5, Att))}];
- true ->
- []
- end,
- {ok, Resp} = start_response_length(Req, 200, Headers1, Len),
- AttFun(Att, fun(Seg, _) -> send(Resp, Seg) end, {ok, Resp})
+ % We received an encoded attachment and stored it as such, so we
+ % don't know the identity length. The client doesn't accept the
+ % encoding, and since we cannot serve a correct Content-Length
+ % header we'll fall back to a chunked response.
+ undefined
+ end,
+ AttFun =
+ case ReqAcceptsAttEnc of
+ false ->
+ fun couch_att:foldl_decode/3;
+ true ->
+ fun couch_att:foldl/3
+ end,
+ chttpd:etag_respond(
+ Req,
+ Etag,
+ fun() ->
+ case Len of
+ undefined ->
+ {ok, Resp} = start_chunked_response(Req, 200, Headers),
+ AttFun(Att, fun(Seg, _) -> send_chunk(Resp, Seg) end, {ok, Resp}),
+ couch_httpd:last_chunk(Resp);
+ _ ->
+ Ranges = parse_ranges(MochiReq:get(range), Len),
+ case {Enc, Ranges} of
+ {identity, [{From, To}]} ->
+ Headers1 =
+ [{"Content-Range", make_content_range(From, To, Len)}] ++
+ Headers,
+ {ok, Resp} = start_response_length(
+ Req, 206, Headers1, To - From + 1
+ ),
+ couch_att:range_foldl(
+ Att,
+ From,
+ To + 1,
+ fun(Seg, _) -> send(Resp, Seg) end,
+ {ok, Resp}
+ );
+ {identity, Ranges} when
+ is_list(Ranges) andalso length(Ranges) < 10
+ ->
+ send_ranges_multipart(Req, Type, Len, Att, Ranges);
+ _ ->
+ Headers1 =
+ Headers ++
+ if
+ Enc =:= identity orelse
+ ReqAcceptsAttEnc =:= true ->
+ [
+ {"Content-MD5",
+ base64:encode(
+ couch_att:fetch(md5, Att)
+ )}
+ ];
+ true ->
+ []
+ end,
+ {ok, Resp} = start_response_length(Req, 200, Headers1, Len),
+ AttFun(Att, fun(Seg, _) -> send(Resp, Seg) end, {ok, Resp})
+ end
+ end
end
- end
+ )
+ after
+ demonitor_refs(Refs)
end
- )
- after
- demonitor_refs(Refs)
- end
end;
-
-
-db_attachment_req(#httpd{method=Method}=Req, Db, DocId, FileNameParts)
- when (Method == 'PUT') or (Method == 'DELETE') ->
+db_attachment_req(#httpd{method = Method} = Req, Db, DocId, FileNameParts) when
+ (Method == 'PUT') or (Method == 'DELETE')
+->
#httpd{
mochi_req = MochiReq
} = Req,
FileName = validate_attachment_name(
- mochiweb_util:join(
- lists:map(fun binary_to_list/1,
- FileNameParts),"/")),
+ mochiweb_util:join(
+ lists:map(
+ fun binary_to_list/1,
+ FileNameParts
+ ),
+ "/"
+ )
+ ),
- NewAtt = case Method of
- 'DELETE' ->
- [];
- _ ->
- MimeType = case chttpd:header_value(Req,"Content-Type") of
- % We could throw an error here or guess by the FileName.
- % Currently, just giving it a default.
- undefined -> <<"application/octet-stream">>;
- CType -> list_to_binary(CType)
- end,
- Data = case chttpd:body_length(Req) of
- undefined ->
- <<"">>;
- {unknown_transfer_encoding, Unknown} ->
- exit({unknown_transfer_encoding, Unknown});
- chunked ->
- fun(MaxChunkSize, ChunkFun, InitState) ->
- chttpd:recv_chunked(
- Req, MaxChunkSize, ChunkFun, InitState
- )
- end;
- 0 ->
- <<"">>;
- Length when is_integer(Length) ->
- Expect = case chttpd:header_value(Req, "expect") of
+ NewAtt =
+ case Method of
+ 'DELETE' ->
+ [];
+ _ ->
+ MimeType =
+ case chttpd:header_value(Req, "Content-Type") of
+ % We could throw an error here or guess by the FileName.
+ % Currently, just giving it a default.
+ undefined -> <<"application/octet-stream">>;
+ CType -> list_to_binary(CType)
+ end,
+ Data =
+ case chttpd:body_length(Req) of
undefined ->
- undefined;
- Value when is_list(Value) ->
- string:to_lower(Value)
+ <<"">>;
+ {unknown_transfer_encoding, Unknown} ->
+ exit({unknown_transfer_encoding, Unknown});
+ chunked ->
+ fun(MaxChunkSize, ChunkFun, InitState) ->
+ chttpd:recv_chunked(
+ Req, MaxChunkSize, ChunkFun, InitState
+ )
+ end;
+ 0 ->
+ <<"">>;
+ Length when is_integer(Length) ->
+ Expect =
+ case chttpd:header_value(Req, "expect") of
+ undefined ->
+ undefined;
+ Value when is_list(Value) ->
+ string:to_lower(Value)
+ end,
+ case Expect of
+ "100-continue" ->
+ MochiReq:start_raw_response({100, gb_trees:empty()});
+ _Else ->
+ ok
+ end,
+ fun() -> chttpd:recv(Req, 0) end;
+ Length ->
+ exit({length_not_integer, Length})
end,
- case Expect of
- "100-continue" ->
- MochiReq:start_raw_response({100, gb_trees:empty()});
- _Else ->
- ok
+ ContentLen =
+ case couch_httpd:header_value(Req, "Content-Length") of
+ undefined -> undefined;
+ CL -> list_to_integer(CL)
end,
- fun() -> chttpd:recv(Req, 0) end;
- Length ->
- exit({length_not_integer, Length})
- end,
- ContentLen = case couch_httpd:header_value(Req,"Content-Length") of
- undefined -> undefined;
- CL -> list_to_integer(CL)
- end,
- ContentEnc = string:to_lower(string:strip(
- couch_httpd:header_value(Req, "Content-Encoding", "identity")
- )),
- Encoding = case ContentEnc of
- "identity" ->
- identity;
- "gzip" ->
- gzip;
- _ ->
- throw({
- bad_ctype,
- "Only gzip and identity content-encodings are supported"
- })
- end,
- [couch_att:new([
- {name, FileName},
- {type, MimeType},
- {data, Data},
- {att_len, ContentLen},
- {md5, get_md5_header(Req)},
- {encoding, Encoding}
- ])]
- end,
+ ContentEnc = string:to_lower(
+ string:strip(
+ couch_httpd:header_value(Req, "Content-Encoding", "identity")
+ )
+ ),
+ Encoding =
+ case ContentEnc of
+ "identity" ->
+ identity;
+ "gzip" ->
+ gzip;
+ _ ->
+ throw({
+ bad_ctype,
+ "Only gzip and identity content-encodings are supported"
+ })
+ end,
+ [
+ couch_att:new([
+ {name, FileName},
+ {type, MimeType},
+ {data, Data},
+ {att_len, ContentLen},
+ {md5, get_md5_header(Req)},
+ {encoding, Encoding}
+ ])
+ ]
+ end,
- Doc = case extract_header_rev(Req, chttpd:qs_value(Req, "rev")) of
- missing_rev -> % make the new doc
- if Method =/= 'DELETE' -> ok; true ->
- % check for the existence of the doc to handle the 404 case.
- couch_doc_open(Db, DocId, nil, [])
- end,
- fabric2_db:validate_docid(DocId),
- #doc{id=DocId};
- Rev ->
- case fabric2_db:open_doc_revs(Db, DocId, [Rev], []) of
- {ok, [{ok, Doc0}]} ->
- chttpd_stats:incr_reads(),
- Doc0;
- {ok, [Error]} ->
- throw(Error);
- {error, Error} ->
- throw(Error)
- end
- end,
+ Doc =
+ case extract_header_rev(Req, chttpd:qs_value(Req, "rev")) of
+ % make the new doc
+ missing_rev ->
+ if
+ Method =/= 'DELETE' ->
+ ok;
+ true ->
+ % check for the existence of the doc to handle the 404 case.
+ couch_doc_open(Db, DocId, nil, [])
+ end,
+ fabric2_db:validate_docid(DocId),
+ #doc{id = DocId};
+ Rev ->
+ case fabric2_db:open_doc_revs(Db, DocId, [Rev], []) of
+ {ok, [{ok, Doc0}]} ->
+ chttpd_stats:incr_reads(),
+ Doc0;
+ {ok, [Error]} ->
+ throw(Error);
+ {error, Error} ->
+ throw(Error)
+ end
+ end,
- #doc{atts=Atts} = Doc,
+ #doc{atts = Atts} = Doc,
DocEdited0 = Doc#doc{
atts = NewAtt ++ [A || A <- Atts, couch_att:fetch(name, A) /= FileName]
},
DocEdited = read_att_data(DocEdited0),
case fabric2_db:update_doc(Db, DocEdited, []) of
- {ok, UpdatedRev} ->
- chttpd_stats:incr_writes(),
- HttpCode = 201;
- {accepted, UpdatedRev} ->
- chttpd_stats:incr_writes(),
- HttpCode = 202
+ {ok, UpdatedRev} ->
+ chttpd_stats:incr_writes(),
+ HttpCode = 201;
+ {accepted, UpdatedRev} ->
+ chttpd_stats:incr_writes(),
+ HttpCode = 202
end,
erlang:put(mochiweb_request_recv, true),
DbName = fabric2_db:name(Db),
- {Status, Headers} = case Method of
- 'DELETE' ->
- {200, []};
- _ ->
- {HttpCode, [{"Location", absolute_uri(Req, [$/, DbName, $/, couch_util:url_encode(DocId), $/,
- couch_util:url_encode(FileName)])}]}
+ {Status, Headers} =
+ case Method of
+ 'DELETE' ->
+ {200, []};
+ _ ->
+ {HttpCode, [
+ {"Location",
+ absolute_uri(Req, [
+ $/,
+ DbName,
+ $/,
+ couch_util:url_encode(DocId),
+ $/,
+ couch_util:url_encode(FileName)
+ ])}
+ ]}
end,
- send_json(Req,Status, Headers, {[
- {ok, true},
- {id, DocId},
- {rev, couch_doc:rev_to_str(UpdatedRev)}
- ]});
-
+ send_json(
+ Req,
+ Status,
+ Headers,
+ {[
+ {ok, true},
+ {id, DocId},
+ {rev, couch_doc:rev_to_str(UpdatedRev)}
+ ]}
+ );
db_attachment_req(Req, _Db, _DocId, _FileNameParts) ->
send_method_not_allowed(Req, "DELETE,GET,HEAD,PUT").
send_ranges_multipart(Req, ContentType, Len, Att, Ranges) ->
Boundary = couch_uuids:random(),
- CType = {"Content-Type",
- "multipart/byteranges; boundary=\"" ++ ?b2l(Boundary) ++ "\""},
+ CType = {"Content-Type", "multipart/byteranges; boundary=\"" ++ ?b2l(Boundary) ++ "\""},
{ok, Resp} = start_chunked_response(Req, 206, [CType]),
couch_httpd:send_chunk(Resp, <<"--", Boundary/binary>>),
- lists:foreach(fun({From, To}) ->
- ContentRange = make_content_range(From, To, Len),
- couch_httpd:send_chunk(Resp,
- <<"\r\nContent-Type: ", ContentType/binary, "\r\n",
- "Content-Range: ", ContentRange/binary, "\r\n",
- "\r\n">>),
- couch_att:range_foldl(Att, From, To + 1,
- fun(Seg, _) -> send_chunk(Resp, Seg) end, {ok, Resp}),
- couch_httpd:send_chunk(Resp, <<"\r\n--", Boundary/binary>>)
- end, Ranges),
+ lists:foreach(
+ fun({From, To}) ->
+ ContentRange = make_content_range(From, To, Len),
+ couch_httpd:send_chunk(
+ Resp,
+ <<"\r\nContent-Type: ", ContentType/binary, "\r\n", "Content-Range: ",
+ ContentRange/binary, "\r\n", "\r\n">>
+ ),
+ couch_att:range_foldl(
+ Att,
+ From,
+ To + 1,
+ fun(Seg, _) -> send_chunk(Resp, Seg) end,
+ {ok, Resp}
+ ),
+ couch_httpd:send_chunk(Resp, <<"\r\n--", Boundary/binary>>)
+ end,
+ Ranges
+ ),
couch_httpd:send_chunk(Resp, <<"--">>),
couch_httpd:last_chunk(Resp),
{ok, Resp}.
@@ -1710,18 +1998,21 @@ parse_ranges(Ranges, Len) ->
parse_ranges([], _Len, Acc) ->
lists:reverse(Acc);
-parse_ranges([{0, none}|_], _Len, _Acc) ->
+parse_ranges([{0, none} | _], _Len, _Acc) ->
undefined;
-parse_ranges([{From, To}|_], _Len, _Acc) when is_integer(From) andalso is_integer(To) andalso To < From ->
+parse_ranges([{From, To} | _], _Len, _Acc) when
+ is_integer(From) andalso is_integer(To) andalso To < From
+->
throw(requested_range_not_satisfiable);
-parse_ranges([{From, To}|Rest], Len, Acc)
- when is_integer(To) andalso To >= Len ->
- parse_ranges([{From, Len-1}] ++ Rest, Len, Acc);
-parse_ranges([{none, To}|Rest], Len, Acc) ->
+parse_ranges([{From, To} | Rest], Len, Acc) when
+ is_integer(To) andalso To >= Len
+->
+ parse_ranges([{From, Len - 1}] ++ Rest, Len, Acc);
+parse_ranges([{none, To} | Rest], Len, Acc) ->
parse_ranges([{Len - To, Len - 1}] ++ Rest, Len, Acc);
-parse_ranges([{From, none}|Rest], Len, Acc) ->
+parse_ranges([{From, none} | Rest], Len, Acc) ->
parse_ranges([{From, Len - 1}] ++ Rest, Len, Acc);
-parse_ranges([{From,To}|Rest], Len, Acc) ->
+parse_ranges([{From, To} | Rest], Len, Acc) ->
parse_ranges(Rest, Len, [{From, To}] ++ Acc).
make_content_range(From, To, Len) ->
@@ -1754,116 +2045,130 @@ parse_doc_query({Key, Value}, Args) ->
case {Key, Value} of
{"attachments", "true"} ->
Options = [attachments | Args#doc_query_args.options],
- Args#doc_query_args{options=Options};
+ Args#doc_query_args{options = Options};
{"meta", "true"} ->
Options = [revs_info, conflicts, deleted_conflicts | Args#doc_query_args.options],
- Args#doc_query_args{options=Options};
+ Args#doc_query_args{options = Options};
{"revs", "true"} ->
Options = [revs | Args#doc_query_args.options],
- Args#doc_query_args{options=Options};
+ Args#doc_query_args{options = Options};
{"local_seq", "true"} ->
Options = [local_seq | Args#doc_query_args.options],
- Args#doc_query_args{options=Options};
+ Args#doc_query_args{options = Options};
{"revs_info", "true"} ->
Options = [revs_info | Args#doc_query_args.options],
- Args#doc_query_args{options=Options};
+ Args#doc_query_args{options = Options};
{"conflicts", "true"} ->
Options = [conflicts | Args#doc_query_args.options],
- Args#doc_query_args{options=Options};
+ Args#doc_query_args{options = Options};
{"deleted", "true"} ->
Options = [deleted | Args#doc_query_args.options],
- Args#doc_query_args{options=Options};
+ Args#doc_query_args{options = Options};
{"deleted_conflicts", "true"} ->
Options = [deleted_conflicts | Args#doc_query_args.options],
- Args#doc_query_args{options=Options};
+ Args#doc_query_args{options = Options};
{"rev", Rev} ->
- Args#doc_query_args{rev=couch_doc:parse_rev(Rev)};
+ Args#doc_query_args{rev = couch_doc:parse_rev(Rev)};
{"open_revs", "all"} ->
- Args#doc_query_args{open_revs=all};
+ Args#doc_query_args{open_revs = all};
{"open_revs", RevsJsonStr} ->
JsonArray = ?JSON_DECODE(RevsJsonStr),
- Args#doc_query_args{open_revs=couch_doc:parse_revs(JsonArray)};
+ Args#doc_query_args{open_revs = couch_doc:parse_revs(JsonArray)};
{"latest", "true"} ->
Options = [latest | Args#doc_query_args.options],
- Args#doc_query_args{options=Options};
+ Args#doc_query_args{options = Options};
{"atts_since", RevsJsonStr} ->
JsonArray = ?JSON_DECODE(RevsJsonStr),
Args#doc_query_args{atts_since = couch_doc:parse_revs(JsonArray)};
{"new_edits", "false"} ->
- Args#doc_query_args{update_type=replicated_changes};
+ Args#doc_query_args{update_type = replicated_changes};
{"new_edits", "true"} ->
- Args#doc_query_args{update_type=interactive_edit};
+ Args#doc_query_args{update_type = interactive_edit};
{"att_encoding_info", "true"} ->
Options = [att_encoding_info | Args#doc_query_args.options],
- Args#doc_query_args{options=Options};
+ Args#doc_query_args{options = Options};
{"r", R} ->
- Options = [{r,R} | Args#doc_query_args.options],
- Args#doc_query_args{options=Options};
+ Options = [{r, R} | Args#doc_query_args.options],
+ Args#doc_query_args{options = Options};
{"w", W} ->
- Options = [{w,W} | Args#doc_query_args.options],
- Args#doc_query_args{options=Options};
- _Else -> % unknown key value pair, ignore.
+ Options = [{w, W} | Args#doc_query_args.options],
+ Args#doc_query_args{options = Options};
+ % unknown key value pair, ignore.
+ _Else ->
Args
end.
parse_changes_query(Req) ->
erlang:erase(changes_seq_interval),
- ChangesArgs = lists:foldl(fun({Key, Value}, Args) ->
- case {string:to_lower(Key), Value} of
- {"feed", "live"} ->
- %% sugar for continuous
- Args#changes_args{feed="continuous"};
- {"feed", _} ->
- Args#changes_args{feed=Value};
- {"descending", "true"} ->
- Args#changes_args{dir=rev};
- {"since", _} ->
- Args#changes_args{since=parse_since_seq(Value)};
- {"last-event-id", _} ->
- Args#changes_args{since=Value};
- {"limit", _} ->
- Args#changes_args{limit=list_to_integer(Value)};
- {"style", _} ->
- Args#changes_args{style=list_to_existing_atom(Value)};
- {"heartbeat", "true"} ->
- Args#changes_args{heartbeat=true};
- {"heartbeat", _} ->
- try list_to_integer(Value) of
- HeartbeatInteger when HeartbeatInteger > 0 ->
- Args#changes_args{heartbeat=HeartbeatInteger};
- _ ->
- throw({bad_request, <<"The heartbeat value should be a positive integer (in milliseconds).">>})
- catch error:badarg ->
- throw({bad_request, <<"Invalid heartbeat value. Expecting a positive integer value (in milliseconds).">>})
- end;
- {"timeout", _} ->
- Args#changes_args{timeout=list_to_integer(Value)};
- {"include_docs", "true"} ->
- Args#changes_args{include_docs=true};
- {"conflicts", "true"} ->
- Args#changes_args{conflicts=true};
- {"attachments", "true"} ->
- Options = [attachments | Args#changes_args.doc_options],
- Args#changes_args{doc_options=Options};
- {"att_encoding_info", "true"} ->
- Options = [att_encoding_info | Args#changes_args.doc_options],
- Args#changes_args{doc_options=Options};
- {"filter", _} ->
- Args#changes_args{filter=Value};
- {"seq_interval", _} ->
- try list_to_integer(Value) of
- V when V > 0 ->
- erlang:put(changes_seq_interval, V),
- Args;
- _ ->
- throw({bad_request, invalid_seq_interval})
- catch error:badarg ->
- throw({bad_request, invalid_seq_interval})
- end;
- _Else -> % unknown key value pair, ignore.
- Args
- end
- end, #changes_args{}, chttpd:qs(Req)),
+ ChangesArgs = lists:foldl(
+ fun({Key, Value}, Args) ->
+ case {string:to_lower(Key), Value} of
+ {"feed", "live"} ->
+ %% sugar for continuous
+ Args#changes_args{feed = "continuous"};
+ {"feed", _} ->
+ Args#changes_args{feed = Value};
+ {"descending", "true"} ->
+ Args#changes_args{dir = rev};
+ {"since", _} ->
+ Args#changes_args{since = parse_since_seq(Value)};
+ {"last-event-id", _} ->
+ Args#changes_args{since = Value};
+ {"limit", _} ->
+ Args#changes_args{limit = list_to_integer(Value)};
+ {"style", _} ->
+ Args#changes_args{style = list_to_existing_atom(Value)};
+ {"heartbeat", "true"} ->
+ Args#changes_args{heartbeat = true};
+ {"heartbeat", _} ->
+ try list_to_integer(Value) of
+ HeartbeatInteger when HeartbeatInteger > 0 ->
+ Args#changes_args{heartbeat = HeartbeatInteger};
+ _ ->
+ throw(
+ {bad_request,
+ <<"The heartbeat value should be a positive integer (in milliseconds).">>}
+ )
+ catch
+ error:badarg ->
+ throw(
+ {bad_request,
+ <<"Invalid heartbeat value. Expecting a positive integer value (in milliseconds).">>}
+ )
+ end;
+ {"timeout", _} ->
+ Args#changes_args{timeout = list_to_integer(Value)};
+ {"include_docs", "true"} ->
+ Args#changes_args{include_docs = true};
+ {"conflicts", "true"} ->
+ Args#changes_args{conflicts = true};
+ {"attachments", "true"} ->
+ Options = [attachments | Args#changes_args.doc_options],
+ Args#changes_args{doc_options = Options};
+ {"att_encoding_info", "true"} ->
+ Options = [att_encoding_info | Args#changes_args.doc_options],
+ Args#changes_args{doc_options = Options};
+ {"filter", _} ->
+ Args#changes_args{filter = Value};
+ {"seq_interval", _} ->
+ try list_to_integer(Value) of
+ V when V > 0 ->
+ erlang:put(changes_seq_interval, V),
+ Args;
+ _ ->
+ throw({bad_request, invalid_seq_interval})
+ catch
+ error:badarg ->
+ throw({bad_request, invalid_seq_interval})
+ end;
+ % unknown key value pair, ignore.
+ _Else ->
+ Args
+ end
+ end,
+ #changes_args{},
+ chttpd:qs(Req)
+ ),
%% if it's an EventSource request with a Last-event-ID header
%% that should override the `since` query string, since it's
%% probably the browser reconnecting.
@@ -1873,19 +2178,16 @@ parse_changes_query(Req) ->
undefined ->
ChangesArgs;
Value ->
- ChangesArgs#changes_args{since=Value}
+ ChangesArgs#changes_args{since = Value}
end;
_ ->
ChangesArgs
end.
-
parse_since_seq(<<"now">>) ->
now;
-
parse_since_seq(Seq) when is_binary(Seq), size(Seq) > 30 ->
throw({bad_request, url_encoded_since_seq});
-
parse_since_seq(Seq) when is_binary(Seq), size(Seq) > 2 ->
% We have implicitly allowed the since seq to either be
% JSON encoded or a "raw" string. Here we just remove the
@@ -1895,51 +2197,54 @@ parse_since_seq(Seq) when is_binary(Seq), size(Seq) > 2 ->
<<"\"", S:SeqSize/binary, "\"">> -> S;
S -> S
end;
-
parse_since_seq(Seq) when is_binary(Seq) ->
Seq;
-
parse_since_seq(Seq) when is_list(Seq) ->
parse_since_seq(iolist_to_binary(Seq)).
-
-extract_header_rev(Req, ExplicitRev) when is_binary(ExplicitRev) or is_list(ExplicitRev)->
+extract_header_rev(Req, ExplicitRev) when is_binary(ExplicitRev) or is_list(ExplicitRev) ->
extract_header_rev(Req, couch_doc:parse_rev(ExplicitRev));
extract_header_rev(Req, ExplicitRev) ->
- Etag = case chttpd:header_value(Req, "If-Match") of
- undefined -> undefined;
- Value -> couch_doc:parse_rev(string:strip(Value, both, $"))
- end,
+ Etag =
+ case chttpd:header_value(Req, "If-Match") of
+ undefined -> undefined;
+ Value -> couch_doc:parse_rev(string:strip(Value, both, $"))
+ end,
case {ExplicitRev, Etag} of
- {undefined, undefined} -> missing_rev;
- {_, undefined} -> ExplicitRev;
- {undefined, _} -> Etag;
- _ when ExplicitRev == Etag -> Etag;
- _ ->
- throw({bad_request, "Document rev and etag have different values"})
+ {undefined, undefined} -> missing_rev;
+ {_, undefined} -> ExplicitRev;
+ {undefined, _} -> Etag;
+ _ when ExplicitRev == Etag -> Etag;
+ _ -> throw({bad_request, "Document rev and etag have different values"})
end.
validate_security_can_be_edited(DbName) ->
UserDbName = ?l2b(config:get("chttpd_auth", "authentication_db", "_users")),
- CanEditUserSecurityObject = config:get("couchdb","users_db_security_editable","false"),
- case {DbName,CanEditUserSecurityObject} of
- {UserDbName,"false"} ->
+ CanEditUserSecurityObject = config:get("couchdb", "users_db_security_editable", "false"),
+ case {DbName, CanEditUserSecurityObject} of
+ {UserDbName, "false"} ->
Msg = "You can't edit the security object of the user database.",
throw({forbidden, Msg});
- {_,_} -> ok
+ {_, _} ->
+ ok
end.
validate_attachment_names(Doc) ->
- lists:foreach(fun(Att) ->
- Name = couch_att:fetch(name, Att),
- validate_attachment_name(Name)
- end, Doc#doc.atts).
+ lists:foreach(
+ fun(Att) ->
+ Name = couch_att:fetch(name, Att),
+ validate_attachment_name(Name)
+ end,
+ Doc#doc.atts
+ ).
validate_attachment_name(Name) when is_list(Name) ->
validate_attachment_name(list_to_binary(Name));
-validate_attachment_name(<<"_",Rest/binary>>) ->
- throw({bad_request, <<"Attachment name '_", Rest/binary,
- "' starts with prohibited character '_'">>});
+validate_attachment_name(<<"_", Rest/binary>>) ->
+ throw(
+ {bad_request,
+ <<"Attachment name '_", Rest/binary, "' starts with prohibited character '_'">>}
+ );
validate_attachment_name(Name) ->
case couch_util:validate_utf8(Name) of
true -> Name;
@@ -1948,30 +2253,33 @@ validate_attachment_name(Name) ->
-spec monitor_attachments(couch_att:att() | [couch_att:att()]) -> [reference()].
monitor_attachments(Atts) when is_list(Atts) ->
- lists:foldl(fun(Att, Monitors) ->
- case couch_att:fetch(data, Att) of
- {Fd, _} ->
- [monitor(process, Fd) | Monitors];
- {loc, _, _, _} ->
- Monitors;
- stub ->
- Monitors;
- Else ->
- ?LOG_ERROR(#{
- what => malformed_attachment_data,
- attachment => Att
- }),
- couch_log:error("~p from couch_att:fetch(data, ~p)", [Else, Att]),
- Monitors
- end
- end, [], Atts);
+ lists:foldl(
+ fun(Att, Monitors) ->
+ case couch_att:fetch(data, Att) of
+ {Fd, _} ->
+ [monitor(process, Fd) | Monitors];
+ {loc, _, _, _} ->
+ Monitors;
+ stub ->
+ Monitors;
+ Else ->
+ ?LOG_ERROR(#{
+ what => malformed_attachment_data,
+ attachment => Att
+ }),
+ couch_log:error("~p from couch_att:fetch(data, ~p)", [Else, Att]),
+ Monitors
+ end
+ end,
+ [],
+ Atts
+ );
monitor_attachments(Att) ->
monitor_attachments([Att]).
demonitor_refs(Refs) when is_list(Refs) ->
[demonitor(Ref) || Ref <- Refs].
-
set_namespace(<<"_local_docs">>, Args) ->
set_namespace(<<"_local">>, Args);
set_namespace(<<"_design_docs">>, Args) ->
@@ -1979,25 +2287,27 @@ set_namespace(<<"_design_docs">>, Args) ->
set_namespace(NS, #mrargs{} = Args) ->
couch_views_util:set_extra(Args, namespace, NS).
-
%% /db/_bulk_get stuff
bulk_get_parse_doc_query(Req) ->
- lists:foldl(fun({Key, Value}, Args) ->
- ok = validate_query_param(Key),
- parse_doc_query({Key, Value}, Args)
- end, #doc_query_args{}, chttpd:qs(Req)).
-
+ lists:foldl(
+ fun({Key, Value}, Args) ->
+ ok = validate_query_param(Key),
+ parse_doc_query({Key, Value}, Args)
+ end,
+ #doc_query_args{},
+ chttpd:qs(Req)
+ ).
-validate_query_param("open_revs"=Key) ->
+validate_query_param("open_revs" = Key) ->
throw_bad_query_param(Key);
-validate_query_param("new_edits"=Key) ->
+validate_query_param("new_edits" = Key) ->
throw_bad_query_param(Key);
-validate_query_param("w"=Key) ->
+validate_query_param("w" = Key) ->
throw_bad_query_param(Key);
-validate_query_param("rev"=Key) ->
+validate_query_param("rev" = Key) ->
throw_bad_query_param(Key);
-validate_query_param("atts_since"=Key) ->
+validate_query_param("atts_since" = Key) ->
throw_bad_query_param(Key);
validate_query_param(_) ->
ok.
@@ -2008,11 +2318,9 @@ throw_bad_query_param(Key) when is_binary(Key) ->
Msg = <<"\"", Key/binary, "\" query parameter is not acceptable">>,
throw({bad_request, Msg}).
-
bulk_get_open_doc_revs(Db, {Props}, Options) ->
bulk_get_open_doc_revs1(Db, Props, Options, {}).
-
bulk_get_open_doc_revs1(Db, Props, Options, {}) ->
case couch_util:get_value(<<"id">>, Props) of
undefined ->
@@ -2022,8 +2330,9 @@ bulk_get_open_doc_revs1(Db, Props, Options, {}) ->
try
fabric2_db:validate_docid(DocId),
bulk_get_open_doc_revs1(Db, Props, Options, {DocId})
- catch throw:{Error, Reason} ->
- {DocId, {error, {null, Error, Reason}}, Options}
+ catch
+ throw:{Error, Reason} ->
+ {DocId, {error, {null, Error, Reason}}, Options}
end
end;
bulk_get_open_doc_revs1(Db, Props, Options, {DocId}) ->
@@ -2032,10 +2341,8 @@ bulk_get_open_doc_revs1(Db, Props, Options, {DocId}) ->
case parse_field(<<"rev">>, RevStr) of
{error, {RevStr, Error, Reason}} ->
{DocId, {error, {RevStr, Error, Reason}}, Options};
-
{ok, undefined} ->
bulk_get_open_doc_revs1(Db, Props, Options, {DocId, all});
-
{ok, Rev} ->
bulk_get_open_doc_revs1(Db, Props, Options, {DocId, [Rev]})
end;
@@ -2045,10 +2352,8 @@ bulk_get_open_doc_revs1(Db, Props, Options, {DocId, Revs}) ->
case parse_field(<<"atts_since">>, AttsSinceStr) of
{error, {BadAttsSinceRev, Error, Reason}} ->
{DocId, {error, {BadAttsSinceRev, Error, Reason}}, Options};
-
{ok, []} ->
bulk_get_open_doc_revs1(Db, Props, Options, {DocId, Revs, Options});
-
{ok, RevList} ->
Options1 = [{atts_since, RevList}, attachments | Options],
bulk_get_open_doc_revs1(Db, Props, Options, {DocId, Revs, Options1})
@@ -2066,7 +2371,6 @@ bulk_get_open_doc_revs1(Db, Props, _, {DocId, Revs, Options}) ->
{DocId, Else, Options}
end.
-
parse_field(<<"rev">>, undefined) ->
{ok, undefined};
parse_field(<<"rev">>, Value) ->
@@ -2074,7 +2378,7 @@ parse_field(<<"rev">>, Value) ->
Rev = couch_doc:parse_rev(Value),
{ok, Rev}
catch
- throw:{bad_request=Error, Reason} ->
+ throw:{bad_request = Error, Reason} ->
{error, {Value, Error, Reason}}
end;
parse_field(<<"atts_since">>, undefined) ->
@@ -2086,18 +2390,16 @@ parse_field(<<"atts_since">>, Value) when is_list(Value) ->
parse_field(<<"atts_since">>, Value) ->
{error, {Value, bad_request, <<"att_since value must be array of revs.">>}}.
-
parse_atts_since([], Acc) ->
{ok, lists:reverse(Acc)};
parse_atts_since([RevStr | Rest], Acc) ->
case parse_field(<<"rev">>, RevStr) of
{ok, Rev} ->
parse_atts_since(Rest, [Rev | Acc]);
- {error, _}=Error ->
+ {error, _} = Error ->
Error
end.
-
bulk_get_send_docs_json(Resp, DocId, Results, Options, Sep) ->
Id = ?JSON_ENCODE(DocId),
send_chunk(Resp, [Sep, <<"{\"id\": ">>, Id, <<", \"docs\": [">>]),
@@ -2109,26 +2411,36 @@ bulk_get_send_docs_json1(Resp, DocId, {error, {Rev, Error, Reason}}, _) ->
bulk_get_send_docs_json1(_Resp, _DocId, {ok, []}, _) ->
ok;
bulk_get_send_docs_json1(Resp, DocId, {ok, Docs}, Options) ->
- lists:foldl(fun(Result, AccSeparator) ->
- case Result of
- {ok, Doc} ->
- JsonDoc = couch_doc:to_json_obj(Doc, Options),
- Json = ?JSON_ENCODE({[{ok, JsonDoc}]}),
- send_chunk(Resp, [AccSeparator, Json]);
- {{Error, Reason}, RevId} ->
- RevStr = couch_doc:rev_to_str(RevId),
- Json = bulk_get_json_error(DocId, RevStr, Error, Reason),
- send_chunk(Resp, [AccSeparator, Json])
+ lists:foldl(
+ fun(Result, AccSeparator) ->
+ case Result of
+ {ok, Doc} ->
+ JsonDoc = couch_doc:to_json_obj(Doc, Options),
+ Json = ?JSON_ENCODE({[{ok, JsonDoc}]}),
+ send_chunk(Resp, [AccSeparator, Json]);
+ {{Error, Reason}, RevId} ->
+ RevStr = couch_doc:rev_to_str(RevId),
+ Json = bulk_get_json_error(DocId, RevStr, Error, Reason),
+ send_chunk(Resp, [AccSeparator, Json])
+ end,
+ <<",">>
end,
- <<",">>
- end, <<"">>, Docs).
+ <<"">>,
+ Docs
+ ).
bulk_get_json_error(DocId, Rev, Error, Reason) ->
- ?JSON_ENCODE({[{error, {[{<<"id">>, DocId},
- {<<"rev">>, Rev},
- {<<"error">>, Error},
- {<<"reason">>, Reason}]}}]}).
-
+ ?JSON_ENCODE(
+ {[
+ {error,
+ {[
+ {<<"id">>, DocId},
+ {<<"rev">>, Rev},
+ {<<"error">>, Error},
+ {<<"reason">>, Reason}
+ ]}}
+ ]}
+ ).
read_att_data(#doc{} = Doc) ->
#doc{atts = Atts} = Doc,
diff --git a/src/chttpd/src/chttpd_epi.erl b/src/chttpd/src/chttpd_epi.erl
index ffbd87a..5536c9e 100644
--- a/src/chttpd/src/chttpd_epi.erl
+++ b/src/chttpd/src/chttpd_epi.erl
@@ -10,7 +10,6 @@
% License for the specific language governing permissions and limitations under
% the License.
-
-module(chttpd_epi).
-behaviour(couch_epi_plugin).
@@ -33,7 +32,6 @@ providers() ->
{chttpd_handlers, chttpd_httpd_handlers}
].
-
services() ->
[
{chttpd_auth, chttpd_auth},
diff --git a/src/chttpd/src/chttpd_external.erl b/src/chttpd/src/chttpd_external.erl
index c0a2a9f..3beaeec 100644
--- a/src/chttpd/src/chttpd_external.erl
+++ b/src/chttpd/src/chttpd_external.erl
@@ -18,7 +18,7 @@
-export([json_req_obj_fields/0, json_req_obj/2, json_req_obj/3, json_req_obj/4]).
-export([default_or_content_type/2, parse_external_response/1]).
--import(chttpd,[send_error/4]).
+-import(chttpd, [send_error/4]).
-include_lib("couch/include/couch_db.hrl").
@@ -33,9 +33,23 @@ json_req_obj(Req, Db, DocId, Fields) when is_list(Fields) ->
{[{Field, json_req_obj_field(Field, Req, Db, DocId)} || Field <- Fields]}.
json_req_obj_fields() ->
- [<<"info">>, <<"uuid">>, <<"id">>, <<"method">>, <<"requested_path">>,
- <<"path">>, <<"raw_path">>, <<"query">>, <<"headers">>, <<"body">>,
- <<"peer">>, <<"form">>, <<"cookie">>, <<"userCtx">>, <<"secObj">>].
+ [
+ <<"info">>,
+ <<"uuid">>,
+ <<"id">>,
+ <<"method">>,
+ <<"requested_path">>,
+ <<"path">>,
+ <<"raw_path">>,
+ <<"query">>,
+ <<"headers">>,
+ <<"body">>,
+ <<"peer">>,
+ <<"form">>,
+ <<"cookie">>,
+ <<"userCtx">>,
+ <<"secObj">>
+ ].
json_req_obj_field(<<"info">>, #httpd{}, Db, _DocId) ->
{ok, Info} = fabric2_db:get_db_info(Db),
@@ -44,49 +58,53 @@ json_req_obj_field(<<"uuid">>, #httpd{}, _Db, _DocId) ->
couch_uuids:new();
json_req_obj_field(<<"id">>, #httpd{}, _Db, DocId) ->
DocId;
-json_req_obj_field(<<"method">>, #httpd{method=Method}, _Db, _DocId) ->
+json_req_obj_field(<<"method">>, #httpd{method = Method}, _Db, _DocId) ->
Method;
-json_req_obj_field(<<"requested_path">>, #httpd{requested_path_parts=Path}, _Db, _DocId) ->
+json_req_obj_field(<<"requested_path">>, #httpd{requested_path_parts = Path}, _Db, _DocId) ->
Path;
-json_req_obj_field(<<"path">>, #httpd{path_parts=Path}, _Db, _DocId) ->
+json_req_obj_field(<<"path">>, #httpd{path_parts = Path}, _Db, _DocId) ->
Path;
-json_req_obj_field(<<"raw_path">>, #httpd{mochi_req=Req}, _Db, _DocId) ->
+json_req_obj_field(<<"raw_path">>, #httpd{mochi_req = Req}, _Db, _DocId) ->
?l2b(Req:get(raw_path));
-json_req_obj_field(<<"query">>, #httpd{mochi_req=Req}, _Db, _DocId) ->
+json_req_obj_field(<<"query">>, #httpd{mochi_req = Req}, _Db, _DocId) ->
json_query_keys(to_json_terms(Req:parse_qs()));
-json_req_obj_field(<<"headers">>, #httpd{mochi_req=Req}, _Db, _DocId) ->
+json_req_obj_field(<<"headers">>, #httpd{mochi_req = Req}, _Db, _DocId) ->
Headers = Req:get(headers),
Hlist = mochiweb_headers:to_list(Headers),
to_json_terms(Hlist);
-json_req_obj_field(<<"body">>, #httpd{req_body=undefined, mochi_req=Req}, _Db, _DocId) ->
+json_req_obj_field(<<"body">>, #httpd{req_body = undefined, mochi_req = Req}, _Db, _DocId) ->
MaxSize = chttpd_util:get_chttpd_config_integer(
- "max_http_request_size", 4294967296),
+ "max_http_request_size", 4294967296
+ ),
try
Req:recv_body(MaxSize)
- catch exit:normal ->
- exit({bad_request, <<"Invalid request body">>})
+ catch
+ exit:normal ->
+ exit({bad_request, <<"Invalid request body">>})
end;
-json_req_obj_field(<<"body">>, #httpd{req_body=Body}, _Db, _DocId) ->
+json_req_obj_field(<<"body">>, #httpd{req_body = Body}, _Db, _DocId) ->
Body;
-json_req_obj_field(<<"peer">>, #httpd{mochi_req=Req}, _Db, _DocId) ->
+json_req_obj_field(<<"peer">>, #httpd{mochi_req = Req}, _Db, _DocId) ->
?l2b(Req:get(peer));
-json_req_obj_field(<<"form">>, #httpd{mochi_req=Req, method=Method}=HttpReq, Db, DocId) ->
+json_req_obj_field(<<"form">>, #httpd{mochi_req = Req, method = Method} = HttpReq, Db, DocId) ->
Body = json_req_obj_field(<<"body">>, HttpReq, Db, DocId),
- ParsedForm = case Req:get_primary_header_value("content-type") of
- "application/x-www-form-urlencoded" ++ _ when Method =:= 'POST' orelse Method =:= 'PUT' ->
- mochiweb_util:parse_qs(Body);
- _ ->
- []
- end,
+ ParsedForm =
+ case Req:get_primary_header_value("content-type") of
+ "application/x-www-form-urlencoded" ++ _ when
+ Method =:= 'POST' orelse Method =:= 'PUT'
+ ->
+ mochiweb_util:parse_qs(Body);
+ _ ->
+ []
+ end,
to_json_terms(ParsedForm);
-json_req_obj_field(<<"cookie">>, #httpd{mochi_req=Req}, _Db, _DocId) ->
+json_req_obj_field(<<"cookie">>, #httpd{mochi_req = Req}, _Db, _DocId) ->
to_json_terms(Req:parse_cookie());
json_req_obj_field(<<"userCtx">>, #httpd{}, Db, _DocId) ->
json_user_ctx(Db);
json_req_obj_field(<<"secObj">>, #httpd{user_ctx = #user_ctx{}}, Db, _DocId) ->
fabric2_db:get_security(Db).
-
json_user_ctx(Db) ->
Ctx = fabric2_db:get_user_ctx(Db),
{[
@@ -95,7 +113,6 @@ json_user_ctx(Db) ->
{<<"roles">>, Ctx#user_ctx.roles}
]}.
-
to_json_terms(Data) ->
to_json_terms(Data, []).
to_json_terms([], Acc) ->
@@ -110,15 +127,15 @@ json_query_keys({Json}) ->
json_query_keys([], Acc) ->
{lists:reverse(Acc)};
json_query_keys([{<<"startkey">>, Value} | Rest], Acc) ->
- json_query_keys(Rest, [{<<"startkey">>, ?JSON_DECODE(Value)}|Acc]);
+ json_query_keys(Rest, [{<<"startkey">>, ?JSON_DECODE(Value)} | Acc]);
json_query_keys([{<<"endkey">>, Value} | Rest], Acc) ->
- json_query_keys(Rest, [{<<"endkey">>, ?JSON_DECODE(Value)}|Acc]);
+ json_query_keys(Rest, [{<<"endkey">>, ?JSON_DECODE(Value)} | Acc]);
json_query_keys([{<<"key">>, Value} | Rest], Acc) ->
- json_query_keys(Rest, [{<<"key">>, ?JSON_DECODE(Value)}|Acc]);
+ json_query_keys(Rest, [{<<"key">>, ?JSON_DECODE(Value)} | Acc]);
json_query_keys([{<<"descending">>, Value} | Rest], Acc) ->
- json_query_keys(Rest, [{<<"descending">>, ?JSON_DECODE(Value)}|Acc]);
+ json_query_keys(Rest, [{<<"descending">>, ?JSON_DECODE(Value)} | Acc]);
json_query_keys([Term | Rest], Acc) ->
- json_query_keys(Rest, [Term|Acc]).
+ json_query_keys(Rest, [Term | Acc]).
send_external_response(Req, Response) ->
#extern_resp_args{
@@ -130,48 +147,59 @@ send_external_response(Req, Response) ->
} = parse_external_response(Response),
Headers1 = default_or_content_type(CType, Headers0),
case Json of
- nil ->
- chttpd:send_response(Req, Code, Headers1, Data);
- Json ->
- chttpd:send_json(Req, Code, Headers1, Json)
+ nil ->
+ chttpd:send_response(Req, Code, Headers1, Data);
+ Json ->
+ chttpd:send_json(Req, Code, Headers1, Json)
end.
parse_external_response({Response}) ->
- lists:foldl(fun({Key,Value}, Args) ->
- case {Key, Value} of
- {"", _} ->
- Args;
- {<<"code">>, Value} ->
- Args#extern_resp_args{code=Value};
- {<<"stop">>, true} ->
- Args#extern_resp_args{stop=true};
- {<<"json">>, Value} ->
- Args#extern_resp_args{
- json=Value,
- ctype="application/json"};
- {<<"body">>, Value} ->
- Args#extern_resp_args{data=Value, ctype="text/html; charset=utf-8"};
- {<<"base64">>, Value} ->
- Args#extern_resp_args{
- data=base64:decode(Value),
- ctype="application/binary"
- };
- {<<"headers">>, {Headers}} ->
- NewHeaders = lists:map(fun({Header, HVal}) ->
- {couch_util:to_list(Header), couch_util:to_list(HVal)}
- end, Headers),
- Args#extern_resp_args{headers=NewHeaders};
- _ -> % unknown key
- Msg = lists:flatten(io_lib:format("Invalid data from external server: ~p", [{Key, Value}])),
- throw({external_response_error, Msg})
+ lists:foldl(
+ fun({Key, Value}, Args) ->
+ case {Key, Value} of
+ {"", _} ->
+ Args;
+ {<<"code">>, Value} ->
+ Args#extern_resp_args{code = Value};
+ {<<"stop">>, true} ->
+ Args#extern_resp_args{stop = true};
+ {<<"json">>, Value} ->
+ Args#extern_resp_args{
+ json = Value,
+ ctype = "application/json"
+ };
+ {<<"body">>, Value} ->
+ Args#extern_resp_args{data = Value, ctype = "text/html; charset=utf-8"};
+ {<<"base64">>, Value} ->
+ Args#extern_resp_args{
+ data = base64:decode(Value),
+ ctype = "application/binary"
+ };
+ {<<"headers">>, {Headers}} ->
+ NewHeaders = lists:map(
+ fun({Header, HVal}) ->
+ {couch_util:to_list(Header), couch_util:to_list(HVal)}
+ end,
+ Headers
+ ),
+ Args#extern_resp_args{headers = NewHeaders};
+ % unknown key
+ _ ->
+ Msg = lists:flatten(
+ io_lib:format("Invalid data from external server: ~p", [{Key, Value}])
+ ),
+ throw({external_response_error, Msg})
end
- end, #extern_resp_args{}, Response).
+ end,
+ #extern_resp_args{},
+ Response
+ ).
default_or_content_type(DefaultContentType, Headers) ->
IsContentType = fun({X, _}) -> string:to_lower(X) == "content-type" end,
case lists:any(IsContentType, Headers) of
- false ->
- [{"Content-Type", DefaultContentType} | Headers];
- true ->
- Headers
+ false ->
+ [{"Content-Type", DefaultContentType} | Headers];
+ true ->
+ Headers
end.
diff --git a/src/chttpd/src/chttpd_handlers.erl b/src/chttpd/src/chttpd_handlers.erl
index d46875d..7906388 100644
--- a/src/chttpd/src/chttpd_handlers.erl
+++ b/src/chttpd/src/chttpd_handlers.erl
@@ -45,21 +45,22 @@ handler_info(HttpReq) ->
Default = {'unknown.unknown', #{}},
try
select(collect(handler_info, [Method, PathParts, HttpReq]), Default)
- catch Type:Reason:Stack ->
- ?LOG_ERROR(#{
- what => handler_info_failure,
- result => Type,
- details => Reason,
- stack => Stack
- }),
- couch_log:error("~s :: handler_info failure for ~p : ~p:~p :: ~p", [
+ catch
+ Type:Reason:Stack ->
+ ?LOG_ERROR(#{
+ what => handler_info_failure,
+ result => Type,
+ details => Reason,
+ stack => Stack
+ }),
+ couch_log:error("~s :: handler_info failure for ~p : ~p:~p :: ~p", [
?MODULE,
get(nonce),
Type,
Reason,
Stack
]),
- Default
+ Default
end.
%% ------------------------------------------------------------------
@@ -84,9 +85,9 @@ select(Handlers, _Default) ->
do_select([], Acc) ->
Acc;
-do_select([{override, Handler}|_], _Acc) ->
+do_select([{override, Handler} | _], _Acc) ->
[Handler];
-do_select([{default, _}|Rest], Acc) ->
+do_select([{default, _} | Rest], Acc) ->
do_select(Rest, Acc);
do_select([Handler], Acc) ->
[Handler | Acc];
@@ -100,14 +101,16 @@ select_override_test() ->
?assertEqual(selected, select([{override, selected}, foo], default)),
?assertEqual(selected, select([foo, {override, selected}], default)),
?assertEqual(selected, select([{override, selected}, {override, bar}], default)),
- ?assertError({badmatch,[bar, foo]}, select([foo, bar], default)).
+ ?assertError({badmatch, [bar, foo]}, select([foo, bar], default)).
select_default_override_test() ->
?assertEqual(selected, select([{default, new_default}, selected], old_default)),
?assertEqual(selected, select([selected, {default, new_default}], old_default)),
?assertEqual(selected, select([{default, selected}], old_default)),
?assertEqual(selected, select([], selected)),
- ?assertEqual(selected,
- select([{default, new_default}, {override, selected}, bar], old_default)).
+ ?assertEqual(
+ selected,
+ select([{default, new_default}, {override, selected}, bar], old_default)
+ ).
-endif.
diff --git a/src/chttpd/src/chttpd_httpd_handlers.erl b/src/chttpd/src/chttpd_httpd_handlers.erl
index e5374b1..c8a399c 100644
--- a/src/chttpd/src/chttpd_httpd_handlers.erl
+++ b/src/chttpd/src/chttpd_httpd_handlers.erl
@@ -22,135 +22,108 @@
not_implemented/1
]).
-
-include_lib("couch/include/couch_db.hrl").
-
-url_handler(<<>>) -> fun chttpd_misc:handle_welcome_req/1;
-url_handler(<<"favicon.ico">>) -> fun chttpd_misc:handle_favicon_req/1;
-url_handler(<<"_utils">>) -> fun chttpd_misc:handle_utils_dir_req/1;
-url_handler(<<"_all_dbs">>) -> fun chttpd_misc:handle_all_dbs_req/1;
-url_handler(<<"_deleted_dbs">>) -> fun chttpd_misc:handle_deleted_dbs_req/1;
-url_handler(<<"_dbs_info">>) -> fun chttpd_misc:handle_dbs_info_req/1;
-url_handler(<<"_active_tasks">>) -> fun chttpd_misc:handle_task_status_req/1;
-url_handler(<<"_scheduler">>) -> fun couch_replicator_httpd:handle_scheduler_req/1;
-url_handler(<<"_node">>) -> fun chttpd_node:handle_node_req/1;
+url_handler(<<>>) -> fun chttpd_misc:handle_welcome_req/1;
+url_handler(<<"favicon.ico">>) -> fun chttpd_misc:handle_favicon_req/1;
+url_handler(<<"_utils">>) -> fun chttpd_misc:handle_utils_dir_req/1;
+url_handler(<<"_all_dbs">>) -> fun chttpd_misc:handle_all_dbs_req/1;
+url_handler(<<"_deleted_dbs">>) -> fun chttpd_misc:handle_deleted_dbs_req/1;
+url_handler(<<"_dbs_info">>) -> fun chttpd_misc:handle_dbs_info_req/1;
+url_handler(<<"_active_tasks">>) -> fun chttpd_misc:handle_task_status_req/1;
+url_handler(<<"_scheduler">>) -> fun couch_replicator_httpd:handle_scheduler_req/1;
+url_handler(<<"_node">>) -> fun chttpd_node:handle_node_req/1;
url_handler(<<"_reload_query_servers">>) -> fun chttpd_misc:handle_reload_query_servers_req/1;
-url_handler(<<"_replicate">>) -> fun chttpd_misc:handle_replicate_req/1;
-url_handler(<<"_uuids">>) -> fun chttpd_misc:handle_uuids_req/1;
-url_handler(<<"_session">>) -> fun chttpd_auth:handle_session_req/1;
-url_handler(<<"_up">>) -> fun chttpd_misc:handle_up_req/1;
-url_handler(<<"_membership">>) -> fun ?MODULE:not_supported/1;
-url_handler(<<"_reshard">>) -> fun ?MODULE:not_supported/1;
-url_handler(<<"_db_updates">>) -> fun ?MODULE:not_implemented/1;
-url_handler(<<"_cluster_setup">>) -> fun ?MODULE:not_implemented/1;
+url_handler(<<"_replicate">>) -> fun chttpd_misc:handle_replicate_req/1;
+url_handler(<<"_uuids">>) -> fun chttpd_misc:handle_uuids_req/1;
+url_handler(<<"_session">>) -> fun chttpd_auth:handle_session_req/1;
+url_handler(<<"_up">>) -> fun chttpd_misc:handle_up_req/1;
+url_handler(<<"_membership">>) -> fun ?MODULE:not_supported/1;
+url_handler(<<"_reshard">>) -> fun ?MODULE:not_supported/1;
+url_handler(<<"_db_updates">>) -> fun ?MODULE:not_implemented/1;
+url_handler(<<"_cluster_setup">>) -> fun ?MODULE:not_implemented/1;
url_handler(_) -> no_match.
db_handler(<<"_view_cleanup">>) -> fun chttpd_db:handle_view_cleanup_req/2;
-db_handler(<<"_compact">>) -> fun chttpd_db:handle_compact_req/2;
-db_handler(<<"_design">>) -> fun chttpd_db:handle_design_req/2;
-db_handler(<<"_partition">>) -> fun ?MODULE:not_implemented/2;
-db_handler(<<"_temp_view">>) -> fun ?MODULE:not_supported/2;
-db_handler(<<"_changes">>) -> fun chttpd_db:handle_changes_req/2;
-db_handler(<<"_purge">>) -> fun ?MODULE:not_implemented/2;
+db_handler(<<"_compact">>) -> fun chttpd_db:handle_compact_req/2;
+db_handler(<<"_design">>) -> fun chttpd_db:handle_design_req/2;
+db_handler(<<"_partition">>) -> fun ?MODULE:not_implemented/2;
+db_handler(<<"_temp_view">>) -> fun ?MODULE:not_supported/2;
+db_handler(<<"_changes">>) -> fun chttpd_db:handle_changes_req/2;
+db_handler(<<"_purge">>) -> fun ?MODULE:not_implemented/2;
db_handler(<<"_purged_infos_limit">>) -> fun ?MODULE:not_implemented/2;
-db_handler(<<"_shards">>) -> fun ?MODULE:not_supported/2;
-db_handler(<<"_sync_shards">>) -> fun ?MODULE:not_supported/2;
+db_handler(<<"_shards">>) -> fun ?MODULE:not_supported/2;
+db_handler(<<"_sync_shards">>) -> fun ?MODULE:not_supported/2;
db_handler(_) -> no_match.
-design_handler(<<"_view">>) -> fun chttpd_view:handle_view_req/3;
-design_handler(<<"_show">>) -> fun ?MODULE:not_supported/3;
-design_handler(<<"_list">>) -> fun ?MODULE:not_supported/3;
-design_handler(<<"_update">>) -> fun chttpd_show:handle_doc_update_req/3;
-design_handler(<<"_info">>) -> fun chttpd_db:handle_design_info_req/3;
+design_handler(<<"_view">>) -> fun chttpd_view:handle_view_req/3;
+design_handler(<<"_show">>) -> fun ?MODULE:not_supported/3;
+design_handler(<<"_list">>) -> fun ?MODULE:not_supported/3;
+design_handler(<<"_update">>) -> fun chttpd_show:handle_doc_update_req/3;
+design_handler(<<"_info">>) -> fun chttpd_db:handle_design_info_req/3;
design_handler(<<"_rewrite">>) -> fun ?MODULE:not_supported/3;
design_handler(_) -> no_match.
-
handler_info('GET', [], _) ->
{'welcome_message.read', #{}};
-
handler_info('GET', [<<"_active_tasks">>], _) ->
{'active_tasks.read', #{}};
-
handler_info('GET', [<<"_all_dbs">>], _) ->
{'all_dbs.read', #{}};
-
handler_info('GET', [<<"_deleted_dbs">>], _) ->
{'account-deleted-dbs.read', #{}};
-
handler_info('POST', [<<"_deleted_dbs">>], _) ->
{'account-deleted-dbs.undelete', #{}};
-
handler_info('DELETE', [<<"_deleted_dbs">>, Db], _) ->
{'account-deleted-dbs.delete', #{'db.name' => Db}};
-
handler_info('POST', [<<"_dbs_info">>], _) ->
{'dbs_info.read', #{}};
-
handler_info('GET', [<<"_node">>, <<"_local">>], _) ->
{'node.name.read', #{}};
-
handler_info(Method, [<<"_node">>, <<"_local">> | Rest], HttpReq) ->
handler_info(Method, [<<"_node">>, node() | Rest], HttpReq);
-
handler_info('GET', [<<"_node">>, Node, <<"_config">>], _) ->
{'node.config.all.read', #{node => Node}};
-
handler_info('GET', [<<"_node">>, Node, <<"_config">>, Section], _) ->
{'node.config.section.read', #{node => Node, 'config.section' => Section}};
-
handler_info('GET', [<<"_node">>, Node, <<"_config">>, Section, Key], _) ->
{'node.config.key.read', #{
node => Node,
'config.section' => Section,
'config.key' => Key
}};
-
handler_info('PUT', [<<"_node">>, Node, <<"_config">>, Section, Key], _) ->
{'node.config.key.write', #{
node => Node,
'config.section' => Section,
'config.key' => Key
}};
-
handler_info('DELETE', [<<"_node">>, Node, <<"_config">>, Section, Key], _) ->
{'node.config.key.delete', #{
node => Node,
'config.section' => Section,
'config.key' => Key
}};
-
handler_info('GET', [<<"_node">>, Node, <<"_stats">> | Path], _) ->
{'node.stats.read', #{node => Node, 'stat.path' => Path}};
-
handler_info('GET', [<<"_node">>, Node, <<"_system">>], _) ->
{'node.system.read', #{node => Node}};
-
handler_info('POST', [<<"_node">>, Node, <<"_restart">>], _) ->
{'node.restart.execute', #{node => Node}};
-
handler_info('POST', [<<"_reload_query_servers">>], _) ->
{'query_servers.reload', #{}};
-
handler_info('POST', [<<"_replicate">>], _) ->
{'replication.create', #{}};
-
handler_info('GET', [<<"_scheduler">>, <<"jobs">>], _) ->
{'replication.jobs.read', #{}};
-
handler_info('GET', [<<"_scheduler">>, <<"jobs">>, JobId], _) ->
{'replication.job.read', #{'job.id' => JobId}};
-
handler_info('GET', [<<"_scheduler">>, <<"docs">>], _) ->
{'replication.docs.read', #{'db.name' => <<"_replicator">>}};
-
handler_info('GET', [<<"_scheduler">>, <<"docs">>, Db], _) ->
{'replication.docs.read', #{'db.name' => Db}};
-
handler_info('GET', [<<"_scheduler">>, <<"docs">>, Db, DocId], _) ->
{'replication.doc.read', #{'db.name' => Db, 'doc.id' => DocId}};
-
handler_info('GET', [<<"_scheduler">>, <<"docs">> | Path], _) ->
case lists:splitwith(fun(Elem) -> Elem /= <<"_replicator">> end, Path) of
{_, [<<"_replicator">>]} ->
@@ -165,111 +138,87 @@ handler_info('GET', [<<"_scheduler">>, <<"docs">> | Path], _) ->
_ ->
no_match
end;
-
handler_info('GET', [<<"_session">>], _) ->
{'session.read', #{}};
-
handler_info('POST', [<<"_session">>], _) ->
{'session.create', #{}};
-
handler_info('DELETE', [<<"_session">>], _) ->
{'session.delete', #{}};
-
handler_info('GET', [<<"_up">>], _) ->
{'health.read', #{}};
-
handler_info('GET', [<<"_utils">> | Path], _) ->
{'utils.read', #{'file.path' => filename:join(Path)}};
-
handler_info('GET', [<<"_uuids">>], _) ->
{'uuids.read', #{}};
-
handler_info('GET', [<<"favicon.ico">>], _) ->
{'favicon.ico.read', #{}};
-
-
-handler_info(Method, [<<"_", _/binary>> = Part| Rest], Req) ->
+handler_info(Method, [<<"_", _/binary>> = Part | Rest], Req) ->
% Maybe bail here so that we don't trample over a
% different url_handler plugin. However, we continue
% on for known system databases.
- DbName = case Part of
- <<"_dbs">> -> '_dbs';
- <<"_metadata">> -> '_metadata';
- <<"_nodes">> -> '_nodes';
- <<"_replicator">> -> '_replicator';
- <<"_users">> -> '_users';
- _ -> no_match
- end,
- if DbName == no_match -> no_match; true ->
- handler_info(Method, [DbName | Rest], Req)
+ DbName =
+ case Part of
+ <<"_dbs">> -> '_dbs';
+ <<"_metadata">> -> '_metadata';
+ <<"_nodes">> -> '_nodes';
+ <<"_replicator">> -> '_replicator';
+ <<"_users">> -> '_users';
+ _ -> no_match
+ end,
+ if
+ DbName == no_match -> no_match;
+ true -> handler_info(Method, [DbName | Rest], Req)
end;
-
handler_info('GET', [Db], _) ->
{'db.info.read', #{'db.name' => Db}};
-
handler_info('PUT', [Db], _) ->
{'db.create', #{'db.name' => Db}};
-
handler_info('POST', [Db], _) ->
{'db.doc.write', #{'db.name' => Db}};
-
handler_info('DELETE', [Db], _) ->
{'db.delete', #{'db.name' => Db}};
-
handler_info(M, [Db, <<"_all_docs">>], _) when M == 'GET'; M == 'POST' ->
{'db.all_docs.read', #{'db.name' => Db}};
-
handler_info('POST', [Db, <<"_all_docs">>, <<"queries">>], _) ->
{'db.all_docs.read', #{'db.name' => Db, multi => true}};
-
handler_info('POST', [Db, <<"_bulk_docs">>], _) ->
{'db.docs.write', #{'db.name' => Db, bulk => true}};
-
handler_info('POST', [Db, <<"_bulk_get">>], _) ->
{'db.docs.read', #{'db.name' => Db, bulk => true}};
-
handler_info('GET', [Db, <<"_changes">>], _) ->
{'db.changes.read', #{'db.name' => Db}};
-
handler_info('POST', [Db, <<"_changes">>], _) ->
{'db.changes.read', #{'db.name' => Db}};
-
handler_info('POST', [Db, <<"_compact">>], _) ->
{'db.compact.execute', #{'db.name' => Db}};
-
handler_info('GET', [Db, <<"_design">>, Name], _) ->
{'db.design.doc.read', #{'db.name' => Db, 'design.id' => Name}};
-
handler_info('POST', [Db, <<"_design">>, Name], _) ->
{'db.design.doc.write', #{'db.name' => Db, 'design.id' => Name}};
-
handler_info('PUT', [Db, <<"_design">>, Name], _) ->
{'db.design.doc.write', #{'db.name' => Db, 'design.id' => Name}};
-
handler_info('COPY', [Db, <<"_design">>, Name], Req) ->
{'db.design.doc.write', #{
'db.name' => Db,
'design.id' => get_copy_destination(Req),
'copy.source.doc.id' => <<"_design/", Name/binary>>
}};
-
handler_info('DELETE', [Db, <<"_design">>, Name], _) ->
{'db.design.doc.delete', #{'db.name' => Db, 'design.id' => Name}};
-
handler_info('GET', [Db, <<"_design">>, Name, <<"_info">>], _) ->
{'db.design.info.read', #{'db.name' => Db, 'design.id' => Name}};
-
-handler_info(M, [Db, <<"_design">>, Name, <<"_list">>, List, View], _)
- when M == 'GET'; M == 'POST', M == 'OPTIONS' ->
+handler_info(M, [Db, <<"_design">>, Name, <<"_list">>, List, View], _) when
+ M == 'GET'; M == 'POST', M == 'OPTIONS'
+->
{'db.design.list.read', #{
'db.name' => Db,
'design.id' => Name,
'design.list.name' => List,
'design.view.name' => View
}};
-
-handler_info(M, [Db, <<"_design">>, Name, <<"_list">>, List, Design, View], _)
- when M == 'GET'; M == 'POST', M == 'OPTIONS' ->
+handler_info(M, [Db, <<"_design">>, Name, <<"_list">>, List, Design, View], _) when
+ M == 'GET'; M == 'POST', M == 'OPTIONS'
+->
{'db.design.list.read', #{
'db.name' => Db,
'design.id' => Name,
@@ -277,14 +226,12 @@ handler_info(M, [Db, <<"_design">>, Name, <<"_list">>, List, Design, View], _)
'design.view.source.id' => Design,
'design.view.name' => View
}};
-
handler_info(_, [Db, <<"_design">>, Name, <<"_rewrite">> | Path], _) ->
{'db.design.rewrite.execute', #{
'db.name' => Db,
'design.id' => Name,
'rewrite.path' => filename:join(Path)
}};
-
handler_info(_, [Db, <<"_design">>, Name, <<"_show">>, Show, DocId], _) ->
{'db.design.show.execute', #{
'db.name' => Db,
@@ -292,242 +239,203 @@ handler_info(_, [Db, <<"_design">>, Name, <<"_show">>, Show, DocId], _) ->
'design.show.name' => Show,
'design.show.doc.id' => DocId
}};
-
handler_info(_, [Db, <<"_design">>, Name, <<"_update">>, Update | Rest], _) ->
BaseTags = #{
'db.name' => Db,
'design.id' => Name,
'design.update.name' => Update
},
- Tags = case Rest of
- [] ->
- BaseTags;
- _ ->
- DocId = filename:join(Rest),
- maps:put('design.update.doc.id', DocId, BaseTags)
- end,
+ Tags =
+ case Rest of
+ [] ->
+ BaseTags;
+ _ ->
+ DocId = filename:join(Rest),
+ maps:put('design.update.doc.id', DocId, BaseTags)
+ end,
{'db.design.update.execute', Tags};
-
handler_info('POST', [Db, <<"_design">>, Name, <<"_view">>, View, <<"queries">>], _) ->
{'db.design.view.multi.read', #{
'db.name' => Db,
'design.id' => Name,
'design.view.name' => View
}};
-
-handler_info(M, [Db, <<"_design">>, Name, <<"_view">>, View], _)
- when M == 'GET'; M == 'POST' ->
+handler_info(M, [Db, <<"_design">>, Name, <<"_view">>, View], _) when
+ M == 'GET'; M == 'POST'
+->
{'db.design.view.read', #{
'db.name' => Db,
'design.id' => Name,
'design.view.name' => View
}};
-
handler_info(_, [_Db, <<"_design">>, _Name, <<"_", _/binary>> | _], _) ->
% Bail here so that we don't treat a plugin
% design handler in place of a design attachment
no_match;
-
handler_info('GET', [Db, <<"_design">>, Name | Path], _) ->
{'db.design.doc.attachment.read', #{
'db.name' => Db,
'design.id' => Name,
'attachment.name' => filename:join(Path)
}};
-
handler_info('PUT', [Db, <<"_design">>, Name | Path], _) ->
{'db.design.doc.attachment.write', #{
'db.name' => Db,
'design.id' => Name,
'attachment.name' => filename:join(Path)
}};
-
handler_info('DELETE', [Db, <<"_design">>, Name | Path], _) ->
{'db.design.doc.attachment.delete', #{
'db.name' => Db,
'design.id' => Name,
'attachment.name' => filename:join(Path)
}};
-
handler_info(_, [Db, <<"_design/", Name/binary>> | Rest], Req) ->
% Recurse if someone sent us `_design%2Fname`
chttpd_handlers:handler_info(Req#httpd{
path_parts = [Db, <<"_design">>, Name | Rest]
});
-
handler_info(M, [Db, <<"_design_docs">>], _) when M == 'GET'; M == 'POST' ->
{'db.design_docs.read', #{'db.name' => Db}};
-
handler_info('POST', [Db, <<"_design_docs">>, <<"queries">>], _) ->
{'db.design_docs.read', #{'db.name' => Db, multi => true}};
-
handler_info('POST', [Db, <<"_ensure_full_commit">>], _) ->
{'db.ensure_full_commit.execute', #{'db.name' => Db}};
-
handler_info('GET', [Db, <<"_local">>, Name], _) ->
{'db.local.doc.read', #{'db.name' => Db, 'local.id' => Name}};
-
handler_info('POST', [Db, <<"_local">>, Name], _) ->
{'db.local.doc.write', #{'db.name' => Db, 'local.id' => Name}};
-
handler_info('PUT', [Db, <<"_local">>, Name], _) ->
{'db.local.doc.write', #{'db.name' => Db, 'local.id' => Name}};
-
handler_info('COPY', [Db, <<"_local">>, Name], Req) ->
{'db.local.doc.write', #{
'db.name' => Db,
'local.id' => get_copy_destination(Req),
'copy.source.doc.id' => <<"_local/", Name/binary>>
}};
-
handler_info('DELETE', [Db, <<"_local">>, Name], _) ->
{'db.local.doc.delete', #{'db.name' => Db, 'local.id' => Name}};
-
handler_info(_, [Db, <<"_local">>, Name | _Path], _) ->
{'db.local.doc.invalid_attachment_req', #{
'db.name' => Db,
'local.id' => Name
}};
-
handler_info(M, [Db, <<"_local_docs">>], _) when M == 'GET'; M == 'POST' ->
{'db.local_docs.read', #{'db.name' => Db}};
-
handler_info('POST', [Db, <<"_local_docs">>, <<"queries">>], _) ->
{'db.local_docs.read', #{'db.name' => Db, multi => true}};
-
handler_info('POST', [Db, <<"_missing_revs">>], _) ->
{'db.docs.missing_revs.execute', #{'db.name' => Db}};
-
handler_info('GET', [Db, <<"_partition">>, Partition], _) ->
{'db.partition.info.read', #{'db.name' => Db, partition => Partition}};
-
handler_info(_, [Db, <<"_partition">>, Partition | Rest], Req) ->
- NewPath = case Rest of
- [<<"_all_docs">> | _] ->
- [Db | Rest];
- [<<"_index">> | _] ->
- [Db | Rest];
- [<<"_find">> | _] ->
- [Db | Rest];
- [<<"_explain">> | _] ->
- [Db | Rest];
- [<<"_design">>, _Name, <<"_", _/binary>> | _] ->
- [Db | Rest];
- _ ->
- no_match
- end,
- if NewPath == no_match -> no_match; true ->
- {OpName, Tags} = chttpd_handlers:handler_info(Req#httpd{
- path_parts = NewPath
- }),
- NewOpName = case atom_to_list(OpName) of
- "db." ++ Name -> list_to_atom("db.partition." ++ Name);
- Else -> list_to_atom(Else ++ ".partition")
+ NewPath =
+ case Rest of
+ [<<"_all_docs">> | _] ->
+ [Db | Rest];
+ [<<"_index">> | _] ->
+ [Db | Rest];
+ [<<"_find">> | _] ->
+ [Db | Rest];
+ [<<"_explain">> | _] ->
+ [Db | Rest];
+ [<<"_design">>, _Name, <<"_", _/binary>> | _] ->
+ [Db | Rest];
+ _ ->
+ no_match
end,
- {NewOpName, maps:put(partition, Partition, Tags)}
+ if
+ NewPath == no_match ->
+ no_match;
+ true ->
+ {OpName, Tags} = chttpd_handlers:handler_info(Req#httpd{
+ path_parts = NewPath
+ }),
+ NewOpName =
+ case atom_to_list(OpName) of
+ "db." ++ Name -> list_to_atom("db.partition." ++ Name);
+ Else -> list_to_atom(Else ++ ".partition")
+ end,
+ {NewOpName, maps:put(partition, Partition, Tags)}
end;
-
handler_info('POST', [Db, <<"_purge">>], _) ->
{'db.docs.purge', #{'db.name' => Db}};
-
handler_info('GET', [Db, <<"_purged_infos_limit">>], _) ->
{'db.purged_infos_limit.read', #{'db.name' => Db}};
-
handler_info('PUT', [Db, <<"_purged_infos_limit">>], _) ->
{'db.purged_infos_limit.write', #{'db.name' => Db}};
-
handler_info('POST', [Db, <<"_revs_diff">>], _) ->
{'db.docs.revs_diff.execute', #{'db.name' => Db}};
-
handler_info('GET', [Db, <<"_revs_limit">>], _) ->
{'db.revs_limit.read', #{'db.name' => Db}};
-
handler_info('PUT', [Db, <<"_revs_limit">>], _) ->
{'db.revs_limit.write', #{'db.name' => Db}};
-
handler_info('GET', [Db, <<"_security">>], _) ->
{'db.security.read', #{'db.name' => Db}};
-
handler_info('PUT', [Db, <<"_security">>], _) ->
{'db.security.write', #{'db.name' => Db}};
-
handler_info(_, [Db, <<"_view_cleanup">>], _) ->
{'views.cleanup.execute', #{'db.name' => Db}};
-
handler_info(_, [_Db, <<"_", _/binary>> | _], _) ->
% Bail here for other possible db_handleres
no_match;
-
handler_info('GET', [Db, DocId], _) ->
{'db.doc.read', #{'db.name' => Db, 'doc.id' => DocId}};
-
handler_info('POST', [Db, DocId], _) ->
{'db.doc.write', #{'db.name' => Db, 'design.id' => DocId}};
-
handler_info('PUT', [Db, DocId], _) ->
{'db.doc.write', #{'db.name' => Db, 'design.id' => DocId}};
-
handler_info('COPY', [Db, DocId], Req) ->
{'db.doc.write', #{
'db.name' => Db,
'doc.id' => get_copy_destination(Req),
'copy.source.doc.id' => DocId
}};
-
handler_info('DELETE', [Db, DocId], _) ->
{'db.doc.delete', #{'db.name' => Db, 'doc.id' => DocId}};
-
handler_info('GET', [Db, DocId | Path], _) ->
{'db.doc.attachment.read', #{
'db.name' => Db,
'doc.id' => DocId,
'attachment.name' => filename:join(Path)
}};
-
handler_info('PUT', [Db, DocId | Path], _) ->
{'db.doc.attachment.write', #{
'db.name' => Db,
'doc.id' => DocId,
'attachment.name' => filename:join(Path)
}};
-
handler_info('DELETE', [Db, DocId | Path], _) ->
{'db.doc.attachment.delete', #{
'db.name' => Db,
'doc.id' => DocId,
'attachment.name' => filename:join(Path)
}};
-
handler_info(_, _, _) ->
no_match.
-
get_copy_destination(Req) ->
try
{DocIdStr, _} = chttpd_util:parse_copy_destination_header(Req),
list_to_binary(mochiweb_util:unquote(DocIdStr))
- catch _:_ ->
- unknown
+ catch
+ _:_ ->
+ unknown
end.
-
not_supported(#httpd{} = Req, Db, _DDoc) ->
not_supported(Req, Db).
-
not_supported(#httpd{} = Req, _Db) ->
not_supported(Req).
-
not_supported(#httpd{} = Req) ->
Msg = <<"resource is not supported in CouchDB >= 4.x">>,
chttpd:send_error(Req, 410, gone, Msg).
-
not_implemented(#httpd{} = Req, _Db) ->
not_implemented(Req).
-
not_implemented(#httpd{} = Req) ->
Msg = <<"resource is not implemented">>,
chttpd:send_error(Req, 501, not_implemented, Msg).
diff --git a/src/chttpd/src/chttpd_misc.erl b/src/chttpd/src/chttpd_misc.erl
index f8e47a2..5ed7e60 100644
--- a/src/chttpd/src/chttpd_misc.erl
+++ b/src/chttpd/src/chttpd_misc.erl
@@ -32,9 +32,15 @@
-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]).
+-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).
@@ -43,19 +49,21 @@
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(#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").
@@ -66,7 +74,7 @@ get_features() ->
handle_favicon_req(Req) ->
handle_favicon_req(Req, get_docroot()).
-handle_favicon_req(#httpd{method='GET'}=Req, DocumentRoot) ->
+handle_favicon_req(#httpd{method = 'GET'} = Req, DocumentRoot) ->
{DateNow, TimeNow} = calendar:universal_time(),
DaysNow = calendar:date_to_gregorian_days(DateNow),
DaysWhenExpires = DaysNow + 365,
@@ -83,32 +91,33 @@ handle_favicon_req(Req, _) ->
handle_utils_dir_req(Req) ->
handle_utils_dir_req(Req, get_docroot()).
-handle_utils_dir_req(#httpd{method='GET'}=Req, DocumentRoot) ->
+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)
+ {_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';",
+ 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) ->
+handle_all_dbs_req(#httpd{method = 'GET'} = Req) ->
#mrargs{
start_key = StartKey,
end_key = EndKey,
@@ -127,32 +136,32 @@ handle_all_dbs_req(#httpd{method='GET'}=Req) ->
{ok, Resp} = chttpd:start_delayed_json_response(Req, 200, []),
Callback = fun all_dbs_callback/2,
- Acc = #vacc{req=Req,resp=Resp},
+ 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) ->
+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) ->
+ {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, 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, 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}}.
+ {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) ->
+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),
@@ -160,35 +169,44 @@ handle_dbs_info_req(#httpd{method='POST', user_ctx=UserCtx}=Req) ->
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),
+ 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)
+ 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,
- "," % AccSeparator now has a comma
- end, "", Keys),
+ "",
+ 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) ->
+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) ->
+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) ->
@@ -218,16 +236,17 @@ handle_deleted_dbs_req(#httpd{method='POST', user_ctx=Ctx, path_parts=[_]}=Req)
Error ->
throw(Error)
end;
-handle_deleted_dbs_req(#httpd{path_parts = PP}=Req) when length(PP) == 1 ->
+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) ->
+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,
+ 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}]});
@@ -236,7 +255,7 @@ handle_deleted_dbs_req(#httpd{method='DELETE', user_ctx=Ctx, path_parts=[_, DbNa
Error ->
throw(Error)
end;
-handle_deleted_dbs_req(#httpd{path_parts = PP}=Req) when length(PP) == 2 ->
+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).
@@ -287,14 +306,14 @@ 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) ->
+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) ->
+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
@@ -306,41 +325,43 @@ handle_replicate_req(#httpd{method='POST', user_ctx=Ctx, req_body=PostBody} = Re
send_json(Req, maps:merge(#{<<"ok">> => true}, JsonResults));
{ok, stopped} ->
send_json(Req, 200, {[{ok, stopped}]});
- {error, not_found=Error} ->
+ {error, not_found = Error} ->
chttpd:send_error(Req, Error);
{error, #{<<"error">> := Err, <<"reason">> := Reason}} when
- is_binary(Err), is_binary(Reason) ->
+ 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} ->
+ {error, {_, _} = Error} ->
chttpd:send_error(Req, Error);
- {_, _}=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) ->
+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) ->
+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,
+ 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() ->
@@ -357,21 +378,21 @@ handle_uuids_req(#httpd{method='GET'}=Req) ->
handle_uuids_req(Req) ->
send_method_not_allowed(Req, "GET").
-handle_up_req(#httpd{method='GET'} = Req) ->
+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
+ "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").
diff --git a/src/chttpd/src/chttpd_node.erl b/src/chttpd/src/chttpd_node.erl
index 54e0e48..e35781f 100644
--- a/src/chttpd/src/chttpd_node.erl
+++ b/src/chttpd/src/chttpd_node.erl
@@ -21,52 +21,70 @@
-include_lib("couch/include/couch_db.hrl").
--import(chttpd,
- [send_json/2,send_json/3,send_method_not_allowed/2,
- send_chunk/2,start_chunked_response/3]).
+-import(
+ chttpd,
+ [
+ send_json/2, send_json/3,
+ send_method_not_allowed/2,
+ send_chunk/2,
+ start_chunked_response/3
+ ]
+).
% Node-specific request handler (_config and _stats)
% Support _local meaning this node
-handle_node_req(#httpd{path_parts=[_, <<"_local">>]}=Req) ->
+handle_node_req(#httpd{path_parts = [_, <<"_local">>]} = Req) ->
send_json(Req, 200, {[{name, node()}]});
-handle_node_req(#httpd{path_parts=[A, <<"_local">>|Rest]}=Req) ->
- handle_node_req(Req#httpd{path_parts=[A, node()] ++ Rest});
+handle_node_req(#httpd{path_parts = [A, <<"_local">> | Rest]} = Req) ->
+ handle_node_req(Req#httpd{path_parts = [A, node()] ++ Rest});
% GET /_node/$node/_config
-handle_node_req(#httpd{method='GET', path_parts=[_, Node, <<"_config">>]}=Req) ->
- Grouped = lists:foldl(fun({{Section, Key}, Value}, Acc) ->
- case dict:is_key(Section, Acc) of
- true ->
- dict:append(Section, {list_to_binary(Key), list_to_binary(Value)}, Acc);
- false ->
- dict:store(Section, [{list_to_binary(Key), list_to_binary(Value)}], Acc)
- end
- end, dict:new(), call_node(Node, config, all, [])),
- KVs = dict:fold(fun(Section, Values, Acc) ->
- [{list_to_binary(Section), {Values}} | Acc]
- end, [], Grouped),
+handle_node_req(#httpd{method = 'GET', path_parts = [_, Node, <<"_config">>]} = Req) ->
+ Grouped = lists:foldl(
+ fun({{Section, Key}, Value}, Acc) ->
+ case dict:is_key(Section, Acc) of
+ true ->
+ dict:append(Section, {list_to_binary(Key), list_to_binary(Value)}, Acc);
+ false ->
+ dict:store(Section, [{list_to_binary(Key), list_to_binary(Value)}], Acc)
+ end
+ end,
+ dict:new(),
+ call_node(Node, config, all, [])
+ ),
+ KVs = dict:fold(
+ fun(Section, Values, Acc) ->
+ [{list_to_binary(Section), {Values}} | Acc]
+ end,
+ [],
+ Grouped
+ ),
send_json(Req, 200, {KVs});
-handle_node_req(#httpd{path_parts=[_, _Node, <<"_config">>]}=Req) ->
+handle_node_req(#httpd{path_parts = [_, _Node, <<"_config">>]} = Req) ->
send_method_not_allowed(Req, "GET");
% POST /_node/$node/_config/_reload - Flushes unpersisted config values from RAM
-handle_node_req(#httpd{method='POST', path_parts=[_, Node, <<"_config">>, <<"_reload">>]}=Req) ->
+handle_node_req(
+ #httpd{method = 'POST', path_parts = [_, Node, <<"_config">>, <<"_reload">>]} = Req
+) ->
case call_node(Node, config, reload, []) of
ok ->
send_json(Req, 200, {[{ok, true}]});
{error, Reason} ->
chttpd:send_error(Req, {bad_request, Reason})
end;
-handle_node_req(#httpd{path_parts=[_, _Node, <<"_config">>, <<"_reload">>]}=Req) ->
+handle_node_req(#httpd{path_parts = [_, _Node, <<"_config">>, <<"_reload">>]} = Req) ->
send_method_not_allowed(Req, "POST");
% GET /_node/$node/_config/Section
-handle_node_req(#httpd{method='GET', path_parts=[_, Node, <<"_config">>, Section]}=Req) ->
- KVs = [{list_to_binary(Key), list_to_binary(Value)}
- || {Key, Value} <- call_node(Node, config, get, [Section])],
+handle_node_req(#httpd{method = 'GET', path_parts = [_, Node, <<"_config">>, Section]} = Req) ->
+ KVs = [
+ {list_to_binary(Key), list_to_binary(Value)}
+ || {Key, Value} <- call_node(Node, config, get, [Section])
+ ],
send_json(Req, 200, {KVs});
-handle_node_req(#httpd{path_parts=[_, _Node, <<"_config">>, _Section]}=Req) ->
+handle_node_req(#httpd{path_parts = [_, _Node, <<"_config">>, _Section]} = Req) ->
send_method_not_allowed(Req, "GET");
% PUT /_node/$node/_config/Section/Key
% "value"
-handle_node_req(#httpd{method='PUT', path_parts=[_, Node, <<"_config">>, Section, Key]}=Req) ->
+handle_node_req(#httpd{method = 'PUT', path_parts = [_, Node, <<"_config">>, Section, Key]} = Req) ->
couch_util:check_config_blacklist(Section),
Value = couch_util:trim(chttpd:json_body(Req)),
Persist = chttpd:header_value(Req, "X-Couch-Persist") /= "false",
@@ -80,34 +98,36 @@ handle_node_req(#httpd{method='PUT', path_parts=[_, Node, <<"_config">>, Section
chttpd:send_error(Req, {bad_request, Reason})
end;
% GET /_node/$node/_config/Section/Key
-handle_node_req(#httpd{method='GET', path_parts=[_, Node, <<"_config">>, Section, Key]}=Req) ->
+handle_node_req(#httpd{method = 'GET', path_parts = [_, Node, <<"_config">>, Section, Key]} = Req) ->
case call_node(Node, config, get, [Section, Key, undefined]) of
- undefined ->
- throw({not_found, unknown_config_value});
- Value ->
- send_json(Req, 200, list_to_binary(Value))
+ undefined ->
+ throw({not_found, unknown_config_value});
+ Value ->
+ send_json(Req, 200, list_to_binary(Value))
end;
% DELETE /_node/$node/_config/Section/Key
-handle_node_req(#httpd{method='DELETE',path_parts=[_, Node, <<"_config">>, Section, Key]}=Req) ->
+handle_node_req(
+ #httpd{method = 'DELETE', path_parts = [_, Node, <<"_config">>, Section, Key]} = Req
+) ->
couch_util:check_config_blacklist(Section),
Persist = chttpd:header_value(Req, "X-Couch-Persist") /= "false",
case call_node(Node, config, get, [Section, Key, undefined]) of
- undefined ->
- throw({not_found, unknown_config_value});
- OldValue ->
- case call_node(Node, config, delete, [Section, Key, Persist]) of
- ok ->
- send_json(Req, 200, list_to_binary(OldValue));
- {error, Reason} ->
- chttpd:send_error(Req, {bad_request, Reason})
- end
+ undefined ->
+ throw({not_found, unknown_config_value});
+ OldValue ->
+ case call_node(Node, config, delete, [Section, Key, Persist]) of
+ ok ->
+ send_json(Req, 200, list_to_binary(OldValue));
+ {error, Reason} ->
+ chttpd:send_error(Req, {bad_request, Reason})
+ end
end;
-handle_node_req(#httpd{path_parts=[_, _Node, <<"_config">>, _Section, _Key]}=Req) ->
+handle_node_req(#httpd{path_parts = [_, _Node, <<"_config">>, _Section, _Key]} = Req) ->
send_method_not_allowed(Req, "GET,PUT,DELETE");
-handle_node_req(#httpd{path_parts=[_, _Node, <<"_config">>, _Section, _Key | _]}=Req) ->
+handle_node_req(#httpd{path_parts = [_, _Node, <<"_config">>, _Section, _Key | _]} = Req) ->
chttpd:send_error(Req, not_found);
% GET /_node/$node/_stats
-handle_node_req(#httpd{method='GET', path_parts=[_, Node, <<"_stats">> | Path]}=Req) ->
+handle_node_req(#httpd{method = 'GET', path_parts = [_, Node, <<"_stats">> | Path]} = Req) ->
flush(Node, Req),
Stats0 = call_node(Node, couch_stats, fetch, []),
Stats = couch_stats_httpd:transform_stats(Stats0),
@@ -115,44 +135,45 @@ handle_node_req(#httpd{method='GET', path_parts=[_, Node, <<"_stats">> | Path]}=
EJSON0 = couch_stats_httpd:to_ejson(Nested),
EJSON1 = couch_stats_httpd:extract_path(Path, EJSON0),
chttpd:send_json(Req, EJSON1);
-handle_node_req(#httpd{path_parts=[_, _Node, <<"_stats">>]}=Req) ->
+handle_node_req(#httpd{path_parts = [_, _Node, <<"_stats">>]} = Req) ->
send_method_not_allowed(Req, "GET");
-handle_node_req(#httpd{method='GET', path_parts=[_, Node, <<"_prometheus">>]}=Req) ->
+handle_node_req(#httpd{method = 'GET', path_parts = [_, Node, <<"_prometheus">>]} = Req) ->
Metrics = call_node(Node, couch_prometheus_server, scrape, []),
Version = call_node(Node, couch_prometheus_server, version, []),
- Type = "text/plain; version=" ++ Version,
+ Type = "text/plain; version=" ++ Version,
Header = [{<<"Content-Type">>, ?l2b(Type)}],
chttpd:send_response(Req, 200, Header, Metrics);
-handle_node_req(#httpd{path_parts=[_, _Node, <<"_prometheus">>]}=Req) ->
+handle_node_req(#httpd{path_parts = [_, _Node, <<"_prometheus">>]} = Req) ->
send_method_not_allowed(Req, "GET");
% GET /_node/$node/_system
-handle_node_req(#httpd{method='GET', path_parts=[_, Node, <<"_system">>]}=Req) ->
+handle_node_req(#httpd{method = 'GET', path_parts = [_, Node, <<"_system">>]} = Req) ->
Stats = call_node(Node, chttpd_node, get_stats, []),
EJSON = couch_stats_httpd:to_ejson(Stats),
send_json(Req, EJSON);
-handle_node_req(#httpd{path_parts=[_, _Node, <<"_system">>]}=Req) ->
+handle_node_req(#httpd{path_parts = [_, _Node, <<"_system">>]} = Req) ->
send_method_not_allowed(Req, "GET");
% POST /_node/$node/_restart
-handle_node_req(#httpd{method='POST', path_parts=[_, Node, <<"_restart">>]}=Req) ->
+handle_node_req(#httpd{method = 'POST', path_parts = [_, Node, <<"_restart">>]} = Req) ->
call_node(Node, init, restart, []),
send_json(Req, 200, {[{ok, true}]});
-handle_node_req(#httpd{path_parts=[_, _Node, <<"_restart">>]}=Req) ->
+handle_node_req(#httpd{path_parts = [_, _Node, <<"_restart">>]} = Req) ->
send_method_not_allowed(Req, "POST");
-handle_node_req(#httpd{path_parts=[_, _Node | _PathParts]}=Req) ->
+handle_node_req(#httpd{path_parts = [_, _Node | _PathParts]} = Req) ->
% Local (backend) dbs are not support any more
chttpd_httpd_handlers:not_supported(Req);
-handle_node_req(#httpd{path_parts=[_]}=Req) ->
+handle_node_req(#httpd{path_parts = [_]} = Req) ->
chttpd:send_error(Req, {bad_request, <<"Incomplete path to _node request">>});
handle_node_req(Req) ->
chttpd:send_error(Req, not_found).
call_node(Node0, Mod, Fun, Args) when is_binary(Node0) ->
- Node1 = try
- list_to_existing_atom(?b2l(Node0))
- catch
- error:badarg ->
- throw({not_found, <<"no such node: ", Node0/binary>>})
- end,
+ Node1 =
+ try
+ list_to_existing_atom(?b2l(Node0))
+ catch
+ error:badarg ->
+ throw({not_found, <<"no such node: ", Node0/binary>>})
+ end,
call_node(Node1, Mod, Fun, Args);
call_node(Node, Mod, Fun, Args) when is_atom(Node) ->
case rpc:call(Node, Mod, Fun, Args) of
@@ -172,10 +193,25 @@ flush(Node, Req) ->
end.
get_stats() ->
- Other = erlang:memory(system) - lists:sum([X || {_,X} <-
- erlang:memory([atom, code, binary, ets])]),
- Memory = [{other, Other} | erlang:memory([atom, atom_used, processes,
- processes_used, binary, code, ets])],
+ Other =
+ erlang:memory(system) -
+ lists:sum([
+ X
+ || {_, X} <-
+ erlang:memory([atom, code, binary, ets])
+ ]),
+ Memory = [
+ {other, Other}
+ | erlang:memory([
+ atom,
+ atom_used,
+ processes,
+ processes_used,
+ binary,
... 54318 lines suppressed ...