summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES6
-rw-r--r--Makefile.am2
-rw-r--r--bin/Makefile.am1
-rw-r--r--bin/couchdb.tpl.in2
-rw-r--r--configure.ac1
-rw-r--r--configure.ac.orig303
-rw-r--r--etc/couchdb/default.ini.tpl.in11
-rw-r--r--etc/couchdb/local.ini4
-rw-r--r--share/Makefile.am4
-rw-r--r--share/www/script/couch.js61
-rw-r--r--share/www/script/couch_tests.js4
-rw-r--r--share/www/script/jquery.couch.js35
-rw-r--r--share/www/script/oauth.js511
-rw-r--r--share/www/script/sha1.js202
-rw-r--r--share/www/script/test/changes.js4
-rw-r--r--share/www/script/test/cookie_auth.js162
-rw-r--r--share/www/script/test/oauth.js166
-rw-r--r--share/www/script/test/security_validation.js12
-rw-r--r--share/www/script/test/show_documents.js2
-rw-r--r--src/couchdb/Makefile.am4
-rw-r--r--src/couchdb/couch_db.hrl3
-rw-r--r--src/couchdb/couch_httpd.erl134
-rw-r--r--src/couchdb/couch_httpd_auth.erl507
-rw-r--r--src/couchdb/couch_httpd_external.erl7
-rw-r--r--src/couchdb/couch_httpd_misc_handlers.erl27
-rw-r--r--src/couchdb/couch_httpd_oauth.erl173
-rw-r--r--src/couchdb/couch_httpd_show.erl28
-rw-r--r--src/couchdb/couch_httpd_view.erl2
-rw-r--r--src/couchdb/couch_rep.erl86
-rw-r--r--src/couchdb/couch_util.erl46
-rw-r--r--src/erlang-oauth/Makefile.am47
-rw-r--r--src/erlang-oauth/oauth.app20
-rw-r--r--src/erlang-oauth/oauth.erl107
-rw-r--r--src/erlang-oauth/oauth_hmac_sha1.erl11
-rw-r--r--src/erlang-oauth/oauth_http.erl22
-rw-r--r--src/erlang-oauth/oauth_plaintext.erl10
-rw-r--r--src/erlang-oauth/oauth_rsa_sha1.erl30
-rw-r--r--src/erlang-oauth/oauth_unix.erl16
-rw-r--r--src/erlang-oauth/oauth_uri.erl88
-rw-r--r--src/mochiweb/mochiweb_cookies.erl11
-rwxr-xr-xtest/runner.sh2
-rw-r--r--utils/Makefile.am1
42 files changed, 2704 insertions, 171 deletions
diff --git a/CHANGES b/CHANGES
index 906b6f15..18f13e46 100644
--- a/CHANGES
+++ b/CHANGES
@@ -11,6 +11,12 @@ Build and System Integration:
* Changed `couchdb` script configuration options.
* Added default.d and local.d configuration directories to load sequence.
+HTTP Interface:
+
+ * Added optional cookie-based authentication handler.
+ * Added optional two-legged OAuth authentication handler.
+
+
Version 0.9.1
-------------
diff --git a/Makefile.am b/Makefile.am
index f41fca5d..6ea817c7 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -10,7 +10,7 @@
## License for the specific language governing permissions and limitations under
## the License.
-SUBDIRS = bin etc src/couchdb src/ibrowse src/mochiweb share test var utils
+SUBDIRS = bin etc src/couchdb src/erlang-oauth src/ibrowse src/mochiweb share test var utils
localdoc_DATA = AUTHORS.gz BUGS.gz CHANGES.gz NEWS.gz README.gz THANKS.gz
diff --git a/bin/Makefile.am b/bin/Makefile.am
index 991ceef4..4f7b451e 100644
--- a/bin/Makefile.am
+++ b/bin/Makefile.am
@@ -31,6 +31,7 @@ couchdb: couchdb.tpl
-e "s|%couchdbebindir%|couch-@version@/ebin|g" \
-e "s|%mochiwebebindir%|mochiweb-r97/ebin|g" \
-e "s|%ibrowseebindir%|ibrowse-1.4.1/ebin|g" \
+ -e "s|%oauthebindir%|erlang-oauth/ebin|g" \
-e "s|%defaultini%|default.ini|g" \
-e "s|%localini%|local.ini|g" \
-e "s|%localconfdir%|@localconfdir@|g" \
diff --git a/bin/couchdb.tpl.in b/bin/couchdb.tpl.in
index 033acb97..6f22495c 100644
--- a/bin/couchdb.tpl.in
+++ b/bin/couchdb.tpl.in
@@ -219,7 +219,9 @@ start_couchdb () {
-pa %localerlanglibdir%/%couchdbebindir% \
%localerlanglibdir%/%mochiwebebindir% \
%localerlanglibdir%/%ibrowseebindir% \
+ %localerlanglibdir%/%oauthebindir% \
-eval \"application:load(ibrowse)\" \
+ -eval \"application:load(oauth)\" \
-eval \"application:load(crypto)\" \
-eval \"application:load(couch)\" \
-eval \"crypto:start()\" \
diff --git a/configure.ac b/configure.ac
index a0ced9f3..35fd3de1 100644
--- a/configure.ac
+++ b/configure.ac
@@ -289,6 +289,7 @@ AC_CONFIG_FILES([share/Makefile])
AC_CONFIG_FILES([src/couchdb/couch.app.tpl])
AC_CONFIG_FILES([src/couchdb/Makefile])
AC_CONFIG_FILES([src/couchdb/priv/Makefile])
+AC_CONFIG_FILES([src/erlang-oauth/Makefile])
AC_CONFIG_FILES([src/ibrowse/Makefile])
AC_CONFIG_FILES([src/mochiweb/Makefile])
AC_CONFIG_FILES([test/Makefile])
diff --git a/configure.ac.orig b/configure.ac.orig
new file mode 100644
index 00000000..a0ced9f3
--- /dev/null
+++ b/configure.ac.orig
@@ -0,0 +1,303 @@
+dnl Licensed under the Apache License, Version 2.0 (the "License"); you may not
+dnl use this file except in compliance with the License. dnl You may obtain a
+dnl copy of the License at
+dnl
+dnl http://www.apache.org/licenses/LICENSE-2.0
+dnl
+dnl Unless required by applicable law or agreed to in writing, software
+dnl distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+dnl WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+dnl License for the specific language governing permissions and limitations
+dnl under the License.
+
+m4_include([m4/ac_check_icu.m4])
+
+AC_INIT([LOCAL_PACKAGE_NAME], [LOCAL_VERSION], [], [LOCAL_PACKAGE_TARNAME])
+
+AC_PREREQ([2.59])
+
+AC_CONFIG_SRCDIR([CHANGES])
+AC_CONFIG_AUX_DIR([build-aux])
+
+AM_CONFIG_HEADER([config.h])
+
+AM_INIT_AUTOMAKE([1.6.3 foreign])
+
+AC_GNU_SOURCE
+AC_ENABLE_SHARED
+AC_DISABLE_STATIC
+
+AC_PROG_CC
+AC_PROG_LIBTOOL
+AC_PROG_LN_S
+
+AC_MSG_CHECKING([for pthread_create in -lpthread])
+
+original_LIBS="$LIBS"
+LIBS="-lpthread $original_LIBS"
+
+AC_TRY_LINK([#include<pthread.h>],
+ [pthread_create((void *)0, (void *)0, (void *)0, (void *)0)],
+ [pthread=yes], [pthread=no])
+
+if test x${pthread} = xyes; then
+ AC_MSG_RESULT([yes])
+else
+ LIBS="$original_LIBS"
+ AC_MSG_RESULT([no])
+fi
+
+AC_ARG_WITH([erlang], [AC_HELP_STRING([--with-erlang=PATH],
+ [set PATH to the Erlang include directory])], [
+ ERLANG_FLAGS="-I$withval"
+], [
+ ERLANG_FLAGS="-I${libdir}/erlang/usr/include"
+ ERLANG_FLAGS="$ERLANG_FLAGS -I/usr/lib/erlang/usr/include"
+ ERLANG_FLAGS="$ERLANG_FLAGS -I/usr/local/lib/erlang/usr/include"
+ ERLANG_FLAGS="$ERLANG_FLAGS -I/opt/local/lib/erlang/usr/include"
+])
+
+AC_ARG_WITH([js-include], [AC_HELP_STRING([--with-js-include=PATH],
+ [set PATH to the SpiderMonkey include directory])], [
+ JS_INCLUDE="$withval"
+ JS_FLAGS="-I$JS_INCLUDE"
+], [
+ JS_FLAGS="-I/usr/include"
+ JS_FLAGS="$JS_FLAGS -I/usr/include/js"
+ JS_FLAGS="$JS_FLAGS -I/usr/include/mozjs"
+ JS_FLAGS="$JS_FLAGS -I/usr/local/include"
+ JS_FLAGS="$JS_FLAGS -I/opt/local/include"
+ JS_FLAGS="$JS_FLAGS -I/usr/local/include/js"
+ JS_FLAGS="$JS_FLAGS -I/opt/local/include/js"
+])
+
+AC_ARG_WITH([js-lib], [AC_HELP_STRING([--with-js-lib=PATH],
+ [set PATH to the SpiderMonkey library directory])],
+ [JS_LIB_FLAGS="-L$withval"], [])
+
+AC_ARG_VAR([ERLC_FLAGS], [general flags to prepend to ERLC_FLAGS])
+AC_ARG_VAR([FLAGS], [general flags to prepend to LDFLAGS and CPPFLAGS])
+
+LIB_FLAGS="$JS_LIB_FLAGS -L/usr/local/lib -L/opt/local/lib"
+LIBS="$LIB_FLAGS $LIBS"
+# XP_UNIX required for jsapi.h and has been tested to work on Linux and Darwin.
+FLAGS="$LIB_FLAGS $ERLANG_FLAGS $JS_FLAGS -DXP_UNIX $FLAGS"
+CPPFLAGS="$FLAGS $CPPFLAGS"
+# manually linking libm is requred for FreeBSD 7.0
+LDFLAGS="$FLAGS -lm $LDFLAGS"
+
+AC_CHECK_LIB([mozjs], [JS_NewContext], [], [
+ AC_CHECK_LIB([js], [JS_NewContext], [], [
+ AC_MSG_ERROR([Could not find the js library.
+
+Is the Mozilla SpiderMonkey library installed?])])])
+
+AC_CHECK_HEADER([jsapi.h], [], [
+ AC_CHECK_HEADER([js/jsapi.h],
+ [
+ CPPFLAGS="$CPPFLAGS -I$JS_INCLUDE/js"
+ ],
+ [
+ AC_MSG_ERROR([Could not find the jsapi header.
+
+Are the Mozilla SpiderMonkey headers installed?])
+ ])])
+
+AC_LANG_PUSH(C)
+OLD_CFLAGS="$CFLAGS"
+CFLAGS="-Werror-implicit-function-declaration"
+AC_COMPILE_IFELSE(
+ [AC_LANG_PROGRAM(
+ [[#include <jsapi.h>]],
+ [[JS_SetOperationCallback(0, 0);]]
+ )],
+ AC_DEFINE([USE_JS_SETOPCB], [], [Use new JS_SetOperationCallback])
+)
+CFLAGS="$OLD_CFLAGS"
+AC_LANG_POP(C)
+
+AC_CHECK_ICU([3])
+
+ICU_LOCAL_CFLAGS=`$ICU_CONFIG --cppflags-searchpath`
+ICU_LOCAL_LDFLAGS=`$ICU_CONFIG --ldflags-searchpath`
+
+AC_SUBST(ICU_CONFIG)
+AC_SUBST(ICU_LOCAL_CFLAGS)
+AC_SUBST(ICU_LOCAL_LDFLAGS)
+
+AC_CHECK_CURL([7.15.5])
+AC_SUBST(CURL_CFLAGS)
+AC_SUBST(CURL_LIBS)
+
+case "$(uname -s)" in
+ Linux)
+ LIBS="$LIBS -lcrypt"
+ CPPFLAGS="-D_XOPEN_SOURCE $CPPFLAGS"
+ ;;
+ FreeBSD)
+ LIBS="$LIBS -lcrypt"
+ ;;
+ OpenBSD)
+ LIBS="$LIBS -lcrypto"
+ ;;
+esac
+
+AC_PATH_PROG([ERL], [erl])
+
+if test x${ERL} = x; then
+ AC_MSG_ERROR([Could not find the `erl' executable. Is Erlang installed?])
+fi
+
+erlang_version_error="The installed Erlang version is less than 5.6.0 (R12B)."
+
+version="`${ERL} -version 2>&1 | ${SED} "s/[[^0-9]]/ /g"`"
+
+if test `echo $version | ${AWK} "{print \\$1}"` -lt 5; then
+ AC_MSG_ERROR([$erlang_version_error])
+fi
+
+if test `echo $version | ${AWK} "{print \\$2}"` -lt 6; then
+ AC_MSG_ERROR([$erlang_version_error])
+fi
+
+AC_PATH_PROG([ERLC], [erlc])
+
+if test x${ERLC} = x; then
+ AC_MSG_ERROR([Could not find the `erlc' executable. Is Erlang installed?])
+fi
+
+AC_CHECK_HEADER([erl_driver.h], [], [
+ AC_MSG_ERROR([Could not find the `erl_driver.h' header.
+
+Are the Erlang headers installed? Use the `--with-erlang' option to specify the
+path to the Erlang include directory.])])
+
+AC_PATH_PROG([HELP2MAN_EXECUTABLE], [help2man])
+if test x${HELP2MAN_EXECUTABLE} = x; then
+ AC_MSG_WARN([You will be unable to regenerate any man pages.])
+fi
+
+use_init=yes
+use_launchd=yes
+
+AC_ARG_ENABLE([init], [AC_HELP_STRING([--disable-init],
+ [don't install init script where applicable])], [
+ use_init=$enableval
+], [])
+
+AC_ARG_ENABLE([launchd], [AC_HELP_STRING([--disable-launchd],
+ [don't install launchd configuration where applicable])], [
+ use_launchd=$enableval
+], [])
+
+init_enabled=false
+launchd_enabled=false
+
+if test "$use_init" = "yes"; then
+ AC_MSG_CHECKING(location of init directory)
+ if test -d /etc/rc.d; then
+ init_enabled=true
+ AC_SUBST([initdir], ['${sysconfdir}/rc.d'])
+ AC_MSG_RESULT(${initdir})
+ else
+ if test -d /etc/init.d; then
+ init_enabled=true
+ AC_SUBST([initdir], ['${sysconfdir}/init.d'])
+ AC_MSG_RESULT(${initdir})
+ else
+ AC_MSG_RESULT(not found)
+ fi
+ fi
+fi
+
+if test "$use_launchd" = "yes"; then
+ AC_MSG_CHECKING(location of launchd directory)
+ if test -d /Library/LaunchDaemons; then
+ init_enabled=false
+ launchd_enabled=true
+ AC_SUBST([launchddir], ['${prefix}/Library/LaunchDaemons'])
+ AC_MSG_RESULT(${launchddir})
+ else
+ AC_MSG_RESULT(not found)
+ fi
+fi
+
+AC_ARG_VAR([ERL], [path to the `erl' executable])
+AC_ARG_VAR([ERLC], [path to the `erlc' executable])
+AC_ARG_VAR([HELP2MAN_EXECUTABLE], [path to the `help2man' program])
+
+if test -n "$HELP2MAN_EXECUTABLE"; then
+ help2man_enabled=true
+else
+ if test -f "$srcdir/bin/couchdb.1" -a -f "$srcdir/bin/couchjs.1"; then
+ help2man_enabled=true
+ else
+ help2man_enabled=false
+ fi
+fi
+
+AM_CONDITIONAL([INIT], [test x${init_enabled} = xtrue])
+AM_CONDITIONAL([LAUNCHD], [test x${launchd_enabled} = xtrue])
+AM_CONDITIONAL([HELP2MAN], [test x${help2man_enabled} = xtrue])
+
+AC_SUBST([package_author_name], ["LOCAL_PACKAGE_AUTHOR_NAME"])
+AC_SUBST([package_author_address], ["LOCAL_PACKAGE_AUTHOR_ADDRESS"])
+AC_SUBST([package_identifier], ["LOCAL_PACKAGE_IDENTIFIER"])
+AC_SUBST([package_tarname], ["LOCAL_PACKAGE_TARNAME"])
+AC_SUBST([package_name], ["LOCAL_PACKAGE_NAME"])
+
+AC_SUBST([version], ["LOCAL_VERSION"])
+AC_SUBST([version_major], ["LOCAL_VERSION_MAJOR"])
+AC_SUBST([version_minor], ["LOCAL_VERSION_MINOR"])
+AC_SUBST([version_revision], ["LOCAL_VERSION_REVISION"])
+AC_SUBST([version_stage], ["LOCAL_VERSION_STAGE"])
+AC_SUBST([version_release], ["LOCAL_VERSION_RELEASE"])
+
+AC_SUBST([bug_uri], ["LOCAL_BUG_URI"])
+
+AC_SUBST([localconfdir], [${sysconfdir}/${package_identifier}])
+AC_SUBST([localdatadir], [${datadir}/${package_identifier}])
+AC_SUBST([localdocdir], [${datadir}/doc/${package_identifier}])
+AC_SUBST([locallibdir], [${libdir}/${package_identifier}])
+AC_SUBST([localstatelibdir], [${localstatedir}/lib/${package_identifier}])
+AC_SUBST([localstatelogdir], [${localstatedir}/log/${package_identifier}])
+AC_SUBST([localstaterundir], [${localstatedir}/run/${package_identifier}])
+AC_SUBST([locallibbindir], [${locallibdir}/bin])
+AC_SUBST([localerlanglibdir], [${locallibdir}/erlang/lib])
+
+# fix for older autotools that don't define "abs_top_YYY" by default
+AC_SUBST(abs_top_srcdir)
+AC_SUBST(abs_top_builddir)
+
+AC_REVISION([LOCAL_VERSION])
+
+AC_CONFIG_FILES([Makefile])
+AC_CONFIG_FILES([bin/couchjs.tpl])
+AC_CONFIG_FILES([bin/couchdb.tpl])
+AC_CONFIG_FILES([bin/Makefile])
+AC_CONFIG_FILES([etc/couchdb/Makefile])
+AC_CONFIG_FILES([etc/couchdb/default.ini.tpl])
+AC_CONFIG_FILES([etc/default/Makefile])
+AC_CONFIG_FILES([etc/init/couchdb.tpl])
+AC_CONFIG_FILES([etc/init/Makefile])
+AC_CONFIG_FILES([etc/launchd/org.apache.couchdb.plist.tpl])
+AC_CONFIG_FILES([etc/launchd/Makefile])
+AC_CONFIG_FILES([etc/logrotate.d/couchdb.tpl])
+AC_CONFIG_FILES([etc/logrotate.d/Makefile])
+AC_CONFIG_FILES([etc/Makefile])
+AC_CONFIG_FILES([share/Makefile])
+AC_CONFIG_FILES([src/couchdb/couch.app.tpl])
+AC_CONFIG_FILES([src/couchdb/Makefile])
+AC_CONFIG_FILES([src/couchdb/priv/Makefile])
+AC_CONFIG_FILES([src/ibrowse/Makefile])
+AC_CONFIG_FILES([src/mochiweb/Makefile])
+AC_CONFIG_FILES([test/Makefile])
+AC_CONFIG_FILES([utils/Makefile])
+AC_CONFIG_FILES([var/Makefile])
+
+AC_OUTPUT
+
+echo
+echo "You have configured Apache CouchDB, time to relax."
+echo
+echo "Run \`make && sudo make install' to install."
diff --git a/etc/couchdb/default.ini.tpl.in b/etc/couchdb/default.ini.tpl.in
index 77a09fcb..68373691 100644
--- a/etc/couchdb/default.ini.tpl.in
+++ b/etc/couchdb/default.ini.tpl.in
@@ -16,7 +16,7 @@ batch_save_interval = 1000 ; milliseconds after which to save batches
[httpd]
port = 5984
bind_address = 127.0.0.1
-authentication_handler = {couch_httpd, default_authentication_handler}
+authentication_handlers = {couch_httpd_oauth, oauth_authentication_handler}, {couch_httpd_auth, default_authentication_handler}
default_handler = {couch_httpd_db, handle_request}
WWW-Authenticate = Basic realm="administrator"
@@ -24,6 +24,11 @@ WWW-Authenticate = Basic realm="administrator"
file = %localstatelogdir%/couch.log
level = info
+[couch_httpd_auth]
+authentication_db = users
+secret = replace this with a real secret in your local.ini file
+require_valid_user = false
+
[query_servers]
javascript = %bindir%/%couchjs_command_name% %localdatadir%/server/main.js
@@ -62,7 +67,9 @@ _restart = {couch_httpd_misc_handlers, handle_restart_req}
_stats = {couch_httpd_stats_handlers, handle_stats_req}
_log = {couch_httpd_misc_handlers, handle_log_req}
_sleep = {couch_httpd_misc_handlers, handle_sleep_req}
-_whoami = {couch_httpd_misc_handlers, handle_whoami_req}
+_session = {couch_httpd_auth, handle_session_req}
+_oauth = {couch_httpd_oauth, handle_oauth_req}
+_user = {couch_httpd_auth, handle_user_req}
[httpd_db_handlers]
_view_cleanup = {couch_httpd_db, handle_view_cleanup_req}
diff --git a/etc/couchdb/local.ini b/etc/couchdb/local.ini
index f2dbc4fb..c7027415 100644
--- a/etc/couchdb/local.ini
+++ b/etc/couchdb/local.ini
@@ -14,6 +14,10 @@
[log]
;level = debug
+[couch_httpd_auth]
+;secret = replace this with a real secret
+
+
[update_notification]
;unique notifier name=/full/path/to/exe -with "cmd line arg"
diff --git a/share/Makefile.am b/share/Makefile.am
index 4e584703..8071525f 100644
--- a/share/Makefile.am
+++ b/share/Makefile.am
@@ -93,6 +93,8 @@ nobase_dist_localdata_DATA = \
www/script/jquery.resizer.js \
www/script/jquery.suggest.js \
www/script/json2.js \
+ www/script/oauth.js \
+ www/script/sha1.js \
www/script/test/basics.js \
www/script/test/delayed_commits.js \
www/script/test/all_docs.js \
@@ -138,6 +140,8 @@ nobase_dist_localdata_DATA = \
www/script/test/purge.js \
www/script/test/config.js \
www/script/test/security_validation.js \
+ www/script/test/cookie_auth.js \
+ www/script/test/oauth.js \
www/script/test/stats.js \
www/script/test/changes.js \
www/script/test/lorem.txt \
diff --git a/share/www/script/couch.js b/share/www/script/couch.js
index bd1d49dd..f354f52a 100644
--- a/share/www/script/couch.js
+++ b/share/www/script/couch.js
@@ -299,6 +299,67 @@ function CouchDB(name, httpHeaders) {
// Use this from callers to check HTTP status or header values of requests.
CouchDB.last_req = null;
+CouchDB.login = function(username, password) {
+ CouchDB.last_req = CouchDB.request("POST", "/_session", {
+ headers: {"Content-Type": "application/x-www-form-urlencoded",
+ "X-CouchDB-WWW-Authenticate": "Cookie"},
+ body: "username=" + encodeURIComponent(username) + "&password=" + encodeURIComponent(password)
+ });
+ return JSON.parse(CouchDB.last_req.responseText);
+}
+
+CouchDB.logout = function() {
+ CouchDB.last_req = CouchDB.request("DELETE", "/_session", {
+ headers: {"Content-Type": "application/x-www-form-urlencoded",
+ "X-CouchDB-WWW-Authenticate": "Cookie"}
+ });
+ return JSON.parse(CouchDB.last_req.responseText);
+}
+
+CouchDB.createUser = function(username, password, email, roles, basicAuth) {
+ var roles_str = ""
+ if (roles) {
+ for (var i=0; i< roles.length; i++) {
+ roles_str += "&roles=" + encodeURIComponent(roles[i]);
+ }
+ }
+ var headers = {"Content-Type": "application/x-www-form-urlencoded"};
+ if (!basicAuth) {
+ headers['X-CouchDB-WWW-Authenticate'] = 'Cookie';
+ }
+
+ CouchDB.last_req = CouchDB.request("POST", "/_user/", {
+ headers: headers,
+ body: "username=" + encodeURIComponent(username) + "&password=" + encodeURIComponent(password)
+ + "&email="+ encodeURIComponent(email)+ roles_str
+
+ });
+ return JSON.parse(CouchDB.last_req.responseText);
+}
+
+CouchDB.updateUser = function(username, email, roles, password, old_password) {
+ var roles_str = ""
+ if (roles) {
+ for (var i=0; i< roles.length; i++) {
+ roles_str += "&roles=" + encodeURIComponent(roles[i]);
+ }
+ }
+
+ var body = "email="+ encodeURIComponent(email)+ roles_str;
+
+ if (typeof(password) != "undefined" && password)
+ body += "&password=" + password;
+
+ if (typeof(old_password) != "undefined" && old_password)
+ body += "&old_password=" + old_password;
+
+ CouchDB.last_req = CouchDB.request("PUT", "/_user/"+encodeURIComponent(username), {
+ headers: {"Content-Type": "application/x-www-form-urlencoded",
+ "X-CouchDB-WWW-Authenticate": "Cookie"},
+ body: body
+ });
+ return JSON.parse(CouchDB.last_req.responseText);
+}
CouchDB.allDbs = function() {
CouchDB.last_req = CouchDB.request("GET", "/_all_dbs");
diff --git a/share/www/script/couch_tests.js b/share/www/script/couch_tests.js
index 5ac0f51e..91e95b11 100644
--- a/share/www/script/couch_tests.js
+++ b/share/www/script/couch_tests.js
@@ -73,6 +73,10 @@ loadTest("purge.js");
loadTest("config.js");
loadTest("form_submit.js");
loadTest("security_validation.js");
+loadTest("cookie_auth.js");
+loadScript("script/sha1.js");
+loadScript("script/oauth.js");
+loadTest("oauth.js");
loadTest("stats.js");
loadTest("rev_stemming.js");
diff --git a/share/www/script/jquery.couch.js b/share/www/script/jquery.couch.js
index 73d064c4..54e32617 100644
--- a/share/www/script/jquery.couch.js
+++ b/share/www/script/jquery.couch.js
@@ -61,6 +61,41 @@
);
},
+ // TODO make login/logout and db.login/db.logout DRY
+ login: function(options) {
+ options = options || {};
+ $.ajax({
+ type: "POST", url: "/_login", dataType: "json",
+ data: {username: options.username, password: options.password},
+ complete: function(req) {
+ var resp = $.httpData(req, "json");
+ if (req.status == 200) {
+ if (options.success) options.success(resp);
+ } else if (options.error) {
+ options.error(req.status, resp.error, resp.reason);
+ } else {
+ alert("An error occurred logging in: " + resp.reason);
+ }
+ }
+ });
+ },
+ logout: function(options) {
+ options = options || {};
+ $.ajax({
+ type: "POST", url: "/_logout", dataType: "json",
+ complete: function(req) {
+ var resp = $.httpData(req, "json");
+ if (req.status == 200) {
+ if (options.success) options.success(resp);
+ } else if (options.error) {
+ options.error(req.status, resp.error, resp.reason);
+ } else {
+ alert("An error occurred logging out: " + resp.reason);
+ }
+ }
+ });
+ },
+
db: function(name) {
return {
name: name,
diff --git a/share/www/script/oauth.js b/share/www/script/oauth.js
new file mode 100644
index 00000000..aa0019d5
--- /dev/null
+++ b/share/www/script/oauth.js
@@ -0,0 +1,511 @@
+/*
+ * Copyright 2008 Netflix, Inc.
+ *
+ * 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.
+ */
+
+/* Here's some JavaScript software for implementing OAuth.
+
+ This isn't as useful as you might hope. OAuth is based around
+ allowing tools and websites to talk to each other. However,
+ JavaScript running in web browsers is hampered by security
+ restrictions that prevent code running on one website from
+ accessing data stored or served on another.
+
+ Before you start hacking, make sure you understand the limitations
+ posed by cross-domain XMLHttpRequest.
+
+ On the bright side, some platforms use JavaScript as their
+ language, but enable the programmer to access other web sites.
+ Examples include Google Gadgets, and Microsoft Vista Sidebar.
+ For those platforms, this library should come in handy.
+*/
+
+// The HMAC-SHA1 signature method calls b64_hmac_sha1, defined by
+// http://pajhome.org.uk/crypt/md5/sha1.js
+
+/* An OAuth message is represented as an object like this:
+ {method: "GET", action: "http://server.com/path", parameters: ...}
+
+ The parameters may be either a map {name: value, name2: value2}
+ or an Array of name-value pairs [[name, value], [name2, value2]].
+ The latter representation is more powerful: it supports parameters
+ in a specific sequence, or several parameters with the same name;
+ for example [["a", 1], ["b", 2], ["a", 3]].
+
+ Parameter names and values are NOT percent-encoded in an object.
+ They must be encoded before transmission and decoded after reception.
+ For example, this message object:
+ {method: "GET", action: "http://server/path", parameters: {p: "x y"}}
+ ... can be transmitted as an HTTP request that begins:
+ GET /path?p=x%20y HTTP/1.0
+ (This isn't a valid OAuth request, since it lacks a signature etc.)
+ Note that the object "x y" is transmitted as x%20y. To encode
+ parameters, you can call OAuth.addToURL, OAuth.formEncode or
+ OAuth.getAuthorization.
+
+ This message object model harmonizes with the browser object model for
+ input elements of an form, whose value property isn't percent encoded.
+ The browser encodes each value before transmitting it. For example,
+ see consumer.setInputs in example/consumer.js.
+ */
+var OAuth; if (OAuth == null) OAuth = {};
+
+OAuth.setProperties = function setProperties(into, from) {
+ if (into != null && from != null) {
+ for (var key in from) {
+ into[key] = from[key];
+ }
+ }
+ return into;
+}
+
+OAuth.setProperties(OAuth, // utility functions
+{
+ percentEncode: function percentEncode(s) {
+ if (s == null) {
+ return "";
+ }
+ if (s instanceof Array) {
+ var e = "";
+ for (var i = 0; i < s.length; ++s) {
+ if (e != "") e += '&';
+ e += percentEncode(s[i]);
+ }
+ return e;
+ }
+ s = encodeURIComponent(s);
+ // Now replace the values which encodeURIComponent doesn't do
+ // encodeURIComponent ignores: - _ . ! ~ * ' ( )
+ // OAuth dictates the only ones you can ignore are: - _ . ~
+ // Source: http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Functions:encodeURIComponent
+ s = s.replace(/\!/g, "%21");
+ s = s.replace(/\*/g, "%2A");
+ s = s.replace(/\'/g, "%27");
+ s = s.replace(/\(/g, "%28");
+ s = s.replace(/\)/g, "%29");
+ return s;
+ }
+,
+ decodePercent: function decodePercent(s) {
+ if (s != null) {
+ // Handle application/x-www-form-urlencoded, which is defined by
+ // http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1
+ s = s.replace(/\+/g, " ");
+ }
+ return decodeURIComponent(s);
+ }
+,
+ /** Convert the given parameters to an Array of name-value pairs. */
+ getParameterList: function getParameterList(parameters) {
+ if (parameters == null) {
+ return [];
+ }
+ if (typeof parameters != "object") {
+ return decodeForm(parameters + "");
+ }
+ if (parameters instanceof Array) {
+ return parameters;
+ }
+ var list = [];
+ for (var p in parameters) {
+ list.push([p, parameters[p]]);
+ }
+ return list;
+ }
+,
+ /** Convert the given parameters to a map from name to value. */
+ getParameterMap: function getParameterMap(parameters) {
+ if (parameters == null) {
+ return {};
+ }
+ if (typeof parameters != "object") {
+ return getParameterMap(decodeForm(parameters + ""));
+ }
+ if (parameters instanceof Array) {
+ var map = {};
+ for (var p = 0; p < parameters.length; ++p) {
+ var key = parameters[p][0];
+ if (map[key] === undefined) { // first value wins
+ map[key] = parameters[p][1];
+ }
+ }
+ return map;
+ }
+ return parameters;
+ }
+,
+ getParameter: function getParameter(parameters, name) {
+ if (parameters instanceof Array) {
+ for (var p = 0; p < parameters.length; ++p) {
+ if (parameters[p][0] == name) {
+ return parameters[p][1]; // first value wins
+ }
+ }
+ } else {
+ return OAuth.getParameterMap(parameters)[name];
+ }
+ return null;
+ }
+,
+ formEncode: function formEncode(parameters) {
+ var form = "";
+ var list = OAuth.getParameterList(parameters);
+ for (var p = 0; p < list.length; ++p) {
+ var value = list[p][1];
+ if (value == null) value = "";
+ if (form != "") form += '&';
+ form += OAuth.percentEncode(list[p][0])
+ +'='+ OAuth.percentEncode(value);
+ }
+ return form;
+ }
+,
+ decodeForm: function decodeForm(form) {
+ var list = [];
+ var nvps = form.split('&');
+ for (var n = 0; n < nvps.length; ++n) {
+ var nvp = nvps[n];
+ if (nvp == "") {
+ continue;
+ }
+ var equals = nvp.indexOf('=');
+ var name;
+ var value;
+ if (equals < 0) {
+ name = OAuth.decodePercent(nvp);
+ value = null;
+ } else {
+ name = OAuth.decodePercent(nvp.substring(0, equals));
+ value = OAuth.decodePercent(nvp.substring(equals + 1));
+ }
+ list.push([name, value]);
+ }
+ return list;
+ }
+,
+ setParameter: function setParameter(message, name, value) {
+ var parameters = message.parameters;
+ if (parameters instanceof Array) {
+ for (var p = 0; p < parameters.length; ++p) {
+ if (parameters[p][0] == name) {
+ if (value === undefined) {
+ parameters.splice(p, 1);
+ } else {
+ parameters[p][1] = value;
+ value = undefined;
+ }
+ }
+ }
+ if (value !== undefined) {
+ parameters.push([name, value]);
+ }
+ } else {
+ parameters = OAuth.getParameterMap(parameters);
+ parameters[name] = value;
+ message.parameters = parameters;
+ }
+ }
+,
+ setParameters: function setParameters(message, parameters) {
+ var list = OAuth.getParameterList(parameters);
+ for (var i = 0; i < list.length; ++i) {
+ OAuth.setParameter(message, list[i][0], list[i][1]);
+ }
+ }
+,
+ /** Fill in parameters to help construct a request message.
+ This function doesn't fill in every parameter.
+ The accessor object should be like:
+ {consumerKey:'foo', consumerSecret:'bar', accessorSecret:'nurn', token:'krelm', tokenSecret:'blah'}
+ The accessorSecret property is optional.
+ */
+ completeRequest: function completeRequest(message, accessor) {
+ if (message.method == null) {
+ message.method = "GET";
+ }
+ var map = OAuth.getParameterMap(message.parameters);
+ if (map.oauth_consumer_key == null) {
+ OAuth.setParameter(message, "oauth_consumer_key", accessor.consumerKey || "");
+ }
+ if (map.oauth_token == null && accessor.token != null) {
+ OAuth.setParameter(message, "oauth_token", accessor.token);
+ }
+ if (map.oauth_version == null) {
+ OAuth.setParameter(message, "oauth_version", "1.0");
+ }
+ if (map.oauth_timestamp == null) {
+ OAuth.setParameter(message, "oauth_timestamp", OAuth.timestamp());
+ }
+ if (map.oauth_nonce == null) {
+ OAuth.setParameter(message, "oauth_nonce", OAuth.nonce(6));
+ }
+ OAuth.SignatureMethod.sign(message, accessor);
+ }
+,
+ setTimestampAndNonce: function setTimestampAndNonce(message) {
+ OAuth.setParameter(message, "oauth_timestamp", OAuth.timestamp());
+ OAuth.setParameter(message, "oauth_nonce", OAuth.nonce(6));
+ }
+,
+ addToURL: function addToURL(url, parameters) {
+ newURL = url;
+ if (parameters != null) {
+ var toAdd = OAuth.formEncode(parameters);
+ if (toAdd.length > 0) {
+ var q = url.indexOf('?');
+ if (q < 0) newURL += '?';
+ else newURL += '&';
+ newURL += toAdd;
+ }
+ }
+ return newURL;
+ }
+,
+ /** Construct the value of the Authorization header for an HTTP request. */
+ getAuthorizationHeader: function getAuthorizationHeader(realm, parameters) {
+ var header = 'OAuth realm="' + OAuth.percentEncode(realm) + '"';
+ var list = OAuth.getParameterList(parameters);
+ for (var p = 0; p < list.length; ++p) {
+ var parameter = list[p];
+ var name = parameter[0];
+ if (name.indexOf("oauth_") == 0) {
+ header += ',' + OAuth.percentEncode(name) + '="' + OAuth.percentEncode(parameter[1]) + '"';
+ }
+ }
+ return header;
+ }
+,
+ timestamp: function timestamp() {
+ var d = new Date();
+ return Math.floor(d.getTime()/1000);
+ }
+,
+ nonce: function nonce(length) {
+ var chars = OAuth.nonce.CHARS;
+ var result = "";
+ for (var i = 0; i < length; ++i) {
+ var rnum = Math.floor(Math.random() * chars.length);
+ result += chars.substring(rnum, rnum+1);
+ }
+ return result;
+ }
+});
+
+OAuth.nonce.CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
+
+/** Define a constructor function,
+ without causing trouble to anyone who was using it as a namespace.
+ That is, if parent[name] already existed and had properties,
+ copy those properties into the new constructor.
+ */
+OAuth.declareClass = function declareClass(parent, name, newConstructor) {
+ var previous = parent[name];
+ parent[name] = newConstructor;
+ if (newConstructor != null && previous != null) {
+ for (var key in previous) {
+ if (key != "prototype") {
+ newConstructor[key] = previous[key];
+ }
+ }
+ }
+ return newConstructor;
+}
+
+/** An abstract algorithm for signing messages. */
+OAuth.declareClass(OAuth, "SignatureMethod", function OAuthSignatureMethod(){});
+
+OAuth.setProperties(OAuth.SignatureMethod.prototype, // instance members
+{
+ /** Add a signature to the message. */
+ sign: function sign(message) {
+ var baseString = OAuth.SignatureMethod.getBaseString(message);
+ var signature = this.getSignature(baseString);
+ OAuth.setParameter(message, "oauth_signature", signature);
+ return signature; // just in case someone's interested
+ }
+,
+ /** Set the key string for signing. */
+ initialize: function initialize(name, accessor) {
+ var consumerSecret;
+ if (accessor.accessorSecret != null
+ && name.length > 9
+ && name.substring(name.length-9) == "-Accessor")
+ {
+ consumerSecret = accessor.accessorSecret;
+ } else {
+ consumerSecret = accessor.consumerSecret;
+ }
+ this.key = OAuth.percentEncode(consumerSecret)
+ +"&"+ OAuth.percentEncode(accessor.tokenSecret);
+ }
+});
+
+/* SignatureMethod expects an accessor object to be like this:
+ {tokenSecret: "lakjsdflkj...", consumerSecret: "QOUEWRI..", accessorSecret: "xcmvzc..."}
+ The accessorSecret property is optional.
+ */
+// Class members:
+OAuth.setProperties(OAuth.SignatureMethod, // class members
+{
+ sign: function sign(message, accessor) {
+ var name = OAuth.getParameterMap(message.parameters).oauth_signature_method;
+ if (name == null || name == "") {
+ name = "HMAC-SHA1";
+ OAuth.setParameter(message, "oauth_signature_method", name);
+ }
+ OAuth.SignatureMethod.newMethod(name, accessor).sign(message);
+ }
+,
+ /** Instantiate a SignatureMethod for the given method name. */
+ newMethod: function newMethod(name, accessor) {
+ var impl = OAuth.SignatureMethod.REGISTERED[name];
+ if (impl != null) {
+ var method = new impl();
+ method.initialize(name, accessor);
+ return method;
+ }
+ var err = new Error("signature_method_rejected");
+ var acceptable = "";
+ for (var r in OAuth.SignatureMethod.REGISTERED) {
+ if (acceptable != "") acceptable += '&';
+ acceptable += OAuth.percentEncode(r);
+ }
+ err.oauth_acceptable_signature_methods = acceptable;
+ throw err;
+ }
+,
+ /** A map from signature method name to constructor. */
+ REGISTERED : {}
+,
+ /** Subsequently, the given constructor will be used for the named methods.
+ The constructor will be called with no parameters.
+ The resulting object should usually implement getSignature(baseString).
+ You can easily define such a constructor by calling makeSubclass, below.
+ */
+ registerMethodClass: function registerMethodClass(names, classConstructor) {
+ for (var n = 0; n < names.length; ++n) {
+ OAuth.SignatureMethod.REGISTERED[names[n]] = classConstructor;
+ }
+ }
+,
+ /** Create a subclass of OAuth.SignatureMethod, with the given getSignature function. */
+ makeSubclass: function makeSubclass(getSignatureFunction) {
+ var superClass = OAuth.SignatureMethod;
+ var subClass = function() {
+ superClass.call(this);
+ };
+ subClass.prototype = new superClass();
+ // Delete instance variables from prototype:
+ // delete subclass.prototype... There aren't any.
+ subClass.prototype.getSignature = getSignatureFunction;
+ subClass.prototype.constructor = subClass;
+ return subClass;
+ }
+,
+ getBaseString: function getBaseString(message) {
+ var URL = message.action;
+ var q = URL.indexOf('?');
+ var parameters;
+ if (q < 0) {
+ parameters = message.parameters;
+ } else {
+ // Combine the URL query string with the other parameters:
+ parameters = OAuth.decodeForm(URL.substring(q + 1));
+ var toAdd = OAuth.getParameterList(message.parameters);
+ for (var a = 0; a < toAdd.length; ++a) {
+ parameters.push(toAdd[a]);
+ }
+ }
+ return OAuth.percentEncode(message.method.toUpperCase())
+ +'&'+ OAuth.percentEncode(OAuth.SignatureMethod.normalizeUrl(URL))
+ +'&'+ OAuth.percentEncode(OAuth.SignatureMethod.normalizeParameters(parameters));
+ }
+,
+ normalizeUrl: function normalizeUrl(url) {
+ var uri = OAuth.SignatureMethod.parseUri(url);
+ var scheme = uri.protocol.toLowerCase();
+ var authority = uri.authority.toLowerCase();
+ var dropPort = (scheme == "http" && uri.port == 80)
+ || (scheme == "https" && uri.port == 443);
+ if (dropPort) {
+ // find the last : in the authority
+ var index = authority.lastIndexOf(":");
+ if (index >= 0) {
+ authority = authority.substring(0, index);
+ }
+ }
+ var path = uri.path;
+ if (!path) {
+ path = "/"; // conforms to RFC 2616 section 3.2.2
+ }
+ // we know that there is no query and no fragment here.
+ return scheme + "://" + authority + path;
+ }
+,
+ parseUri: function parseUri (str) {
+ /* This function was adapted from parseUri 1.2.1
+ http://stevenlevithan.com/demo/parseuri/js/assets/parseuri.js
+ */
+ var o = {key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
+ parser: {strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/ }};
+ var m = o.parser.strict.exec(str);
+ var uri = {};
+ var i = 14;
+ while (i--) uri[o.key[i]] = m[i] || "";
+ return uri;
+ }
+,
+ normalizeParameters: function normalizeParameters(parameters) {
+ if (parameters == null) {
+ return "";
+ }
+ var list = OAuth.getParameterList(parameters);
+ var sortable = [];
+ for (var p = 0; p < list.length; ++p) {
+ var nvp = list[p];
+ if (nvp[0] != "oauth_signature") {
+ sortable.push([ OAuth.percentEncode(nvp[0])
+ + " " // because it comes before any character that can appear in a percentEncoded string.
+ + OAuth.percentEncode(nvp[1])
+ , nvp]);
+ }
+ }
+ sortable.sort(function(a,b) {
+ if (a[0] < b[0]) return -1;
+ if (a[0] > b[0]) return 1;
+ return 0;
+ });
+ var sorted = [];
+ for (var s = 0; s < sortable.length; ++s) {
+ sorted.push(sortable[s][1]);
+ }
+ return OAuth.formEncode(sorted);
+ }
+});
+
+OAuth.SignatureMethod.registerMethodClass(["PLAINTEXT", "PLAINTEXT-Accessor"],
+ OAuth.SignatureMethod.makeSubclass(
+ function getSignature(baseString) {
+ return this.key;
+ }
+ ));
+
+OAuth.SignatureMethod.registerMethodClass(["HMAC-SHA1", "HMAC-SHA1-Accessor"],
+ OAuth.SignatureMethod.makeSubclass(
+ function getSignature(baseString) {
+ b64pad = '=';
+ var signature = b64_hmac_sha1(this.key, baseString);
+ return signature;
+ }
+ ));
diff --git a/share/www/script/sha1.js b/share/www/script/sha1.js
new file mode 100644
index 00000000..1b559823
--- /dev/null
+++ b/share/www/script/sha1.js
@@ -0,0 +1,202 @@
+/*
+ * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined
+ * in FIPS PUB 180-1
+ * Version 2.1a Copyright Paul Johnston 2000 - 2002.
+ * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
+ * Distributed under the BSD License
+ * See http://pajhome.org.uk/crypt/md5 for details.
+ */
+
+/*
+ * Configurable variables. You may need to tweak these to be compatible with
+ * the server-side, but the defaults work in most cases.
+ */
+var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
+var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */
+var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */
+
+/*
+ * These are the functions you'll usually want to call
+ * They take string arguments and return either hex or base-64 encoded strings
+ */
+function hex_sha1(s){return binb2hex(core_sha1(str2binb(s),s.length * chrsz));}
+function b64_sha1(s){return binb2b64(core_sha1(str2binb(s),s.length * chrsz));}
+function str_sha1(s){return binb2str(core_sha1(str2binb(s),s.length * chrsz));}
+function hex_hmac_sha1(key, data){ return binb2hex(core_hmac_sha1(key, data));}
+function b64_hmac_sha1(key, data){ return binb2b64(core_hmac_sha1(key, data));}
+function str_hmac_sha1(key, data){ return binb2str(core_hmac_sha1(key, data));}
+
+/*
+ * Perform a simple self-test to see if the VM is working
+ */
+function sha1_vm_test()
+{
+ return hex_sha1("abc") == "a9993e364706816aba3e25717850c26c9cd0d89d";
+}
+
+/*
+ * Calculate the SHA-1 of an array of big-endian words, and a bit length
+ */
+function core_sha1(x, len)
+{
+ /* append padding */
+ x[len >> 5] |= 0x80 << (24 - len % 32);
+ x[((len + 64 >> 9) << 4) + 15] = len;
+
+ var w = Array(80);
+ var a = 1732584193;
+ var b = -271733879;
+ var c = -1732584194;
+ var d = 271733878;
+ var e = -1009589776;
+
+ for(var i = 0; i < x.length; i += 16)
+ {
+ var olda = a;
+ var oldb = b;
+ var oldc = c;
+ var oldd = d;
+ var olde = e;
+
+ for(var j = 0; j < 80; j++)
+ {
+ if(j < 16) w[j] = x[i + j];
+ else w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1);
+ var t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)),
+ safe_add(safe_add(e, w[j]), sha1_kt(j)));
+ e = d;
+ d = c;
+ c = rol(b, 30);
+ b = a;
+ a = t;
+ }
+
+ a = safe_add(a, olda);
+ b = safe_add(b, oldb);
+ c = safe_add(c, oldc);
+ d = safe_add(d, oldd);
+ e = safe_add(e, olde);
+ }
+ return Array(a, b, c, d, e);
+
+}
+
+/*
+ * Perform the appropriate triplet combination function for the current
+ * iteration
+ */
+function sha1_ft(t, b, c, d)
+{
+ if(t < 20) return (b & c) | ((~b) & d);
+ if(t < 40) return b ^ c ^ d;
+ if(t < 60) return (b & c) | (b & d) | (c & d);
+ return b ^ c ^ d;
+}
+
+/*
+ * Determine the appropriate additive constant for the current iteration
+ */
+function sha1_kt(t)
+{
+ return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 :
+ (t < 60) ? -1894007588 : -899497514;
+}
+
+/*
+ * Calculate the HMAC-SHA1 of a key and some data
+ */
+function core_hmac_sha1(key, data)
+{
+ var bkey = str2binb(key);
+ if(bkey.length > 16) bkey = core_sha1(bkey, key.length * chrsz);
+
+ var ipad = Array(16), opad = Array(16);
+ for(var i = 0; i < 16; i++)
+ {
+ ipad[i] = bkey[i] ^ 0x36363636;
+ opad[i] = bkey[i] ^ 0x5C5C5C5C;
+ }
+
+ var hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * chrsz);
+ return core_sha1(opad.concat(hash), 512 + 160);
+}
+
+/*
+ * Add integers, wrapping at 2^32. This uses 16-bit operations internally
+ * to work around bugs in some JS interpreters.
+ */
+function safe_add(x, y)
+{
+ var lsw = (x & 0xFFFF) + (y & 0xFFFF);
+ var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
+ return (msw << 16) | (lsw & 0xFFFF);
+}
+
+/*
+ * Bitwise rotate a 32-bit number to the left.
+ */
+function rol(num, cnt)
+{
+ return (num << cnt) | (num >>> (32 - cnt));
+}
+
+/*
+ * Convert an 8-bit or 16-bit string to an array of big-endian words
+ * In 8-bit function, characters >255 have their hi-byte silently ignored.
+ */
+function str2binb(str)
+{
+ var bin = Array();
+ var mask = (1 << chrsz) - 1;
+ for(var i = 0; i < str.length * chrsz; i += chrsz)
+ bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (32 - chrsz - i%32);
+ return bin;
+}
+
+/*
+ * Convert an array of big-endian words to a string
+ */
+function binb2str(bin)
+{
+ var str = "";
+ var mask = (1 << chrsz) - 1;
+ for(var i = 0; i < bin.length * 32; i += chrsz)
+ str += String.fromCharCode((bin[i>>5] >>> (32 - chrsz - i%32)) & mask);
+ return str;
+}
+
+/*
+ * Convert an array of big-endian words to a hex string.
+ */
+function binb2hex(binarray)
+{
+ var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
+ var str = "";
+ for(var i = 0; i < binarray.length * 4; i++)
+ {
+ str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) +
+ hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF);
+ }
+ return str;
+}
+
+/*
+ * Convert an array of big-endian words to a base-64 string
+ */
+function binb2b64(binarray)
+{
+ var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+ var str = "";
+ for(var i = 0; i < binarray.length * 4; i += 3)
+ {
+ var triplet = (((binarray[i >> 2] >> 8 * (3 - i %4)) & 0xFF) << 16)
+ | (((binarray[i+1 >> 2] >> 8 * (3 - (i+1)%4)) & 0xFF) << 8 )
+ | ((binarray[i+2 >> 2] >> 8 * (3 - (i+2)%4)) & 0xFF);
+ for(var j = 0; j < 4; j++)
+ {
+ if(i * 8 + j * 6 > binarray.length * 32) str += b64pad;
+ else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F);
+ }
+ }
+ return str;
+}
diff --git a/share/www/script/test/changes.js b/share/www/script/test/changes.js
index 1800ca82..d8abcc71 100644
--- a/share/www/script/test/changes.js
+++ b/share/www/script/test/changes.js
@@ -195,8 +195,8 @@ couchTests.changes = function(debug) {
// test for userCtx
run_on_modified_server(
[{section: "httpd",
- key: "authentication_handler",
- value: "{couch_httpd, special_test_authentication_handler}"},
+ key: "authentication_handlers",
+ value: "{couch_httpd_auth, special_test_authentication_handler}"},
{section:"httpd",
key: "WWW-Authenticate",
value: "X-Couch-Test-Auth"}],
diff --git a/share/www/script/test/cookie_auth.js b/share/www/script/test/cookie_auth.js
new file mode 100644
index 00000000..63ab21b9
--- /dev/null
+++ b/share/www/script/test/cookie_auth.js
@@ -0,0 +1,162 @@
+// 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.
+
+couchTests.cookie_auth = function(debug) {
+ // This tests cookie-based authentication.
+
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ if (debug) debugger;
+
+ // Simple secret key generator
+ function generateSecret(length) {
+ var secret = '';
+ for (var i=0; i<length; i++) {
+ secret += String.fromCharCode(Math.floor(Math.random() * 256));
+ }
+ return secret;
+ }
+ // this function will be called on the modified server
+ var testFun = function () {
+ try {
+ // try using an invalid cookie
+ var usersDb = new CouchDB("test_suite_users");
+ usersDb.deleteDb();
+ usersDb.createDb();
+
+ var password = "3.141592653589";
+
+ // Create a user
+ T(usersDb.save({
+ _id: "a1",
+ salt: "123",
+ password_sha: "8da1CtkFvb58LWrnup5chgdZVUs=",
+ username: "Jason Davies",
+ author: "Jason Davies",
+ type: "user",
+ roles: ["_admin"]
+ }).ok);
+
+ var validationDoc = {
+ _id : "_design/validate",
+ validate_doc_update: "(" + (function (newDoc, oldDoc, userCtx) {
+ // docs should have an author field.
+ if (!newDoc._deleted && !newDoc.author) {
+ throw {forbidden:
+ "Documents must have an author field"};
+ }
+ if (oldDoc && oldDoc.author != userCtx.name) {
+ throw {unauthorized:
+ "You are not the author of this document. You jerk."+userCtx.name};
+ }
+ }).toString() + ")"
+ };
+
+ T(db.save(validationDoc).ok);
+
+
+
+ T(CouchDB.login('Jason Davies', password).ok);
+ // update the credentials document
+ var doc = usersDb.open("a1");
+ doc.foo=2;
+ T(usersDb.save(doc).ok);
+
+ // Save a document that's missing an author field.
+ try {
+ // db has a validation function
+ db.save({foo:1});
+ T(false && "Can't get here. Should have thrown an error 2");
+ } catch (e) {
+ T(e.error == "forbidden");
+ T(db.last_req.status == 403);
+ }
+
+ // TODO should login() throw an exception here?
+ T(!CouchDB.login('Jason Davies', "2.71828").ok);
+ T(!CouchDB.login('Robert Allen Zimmerman', 'd00d').ok);
+
+ // test redirect
+ xhr = CouchDB.request("POST", "/_session?next=/", {
+ headers: {"Content-Type": "application/x-www-form-urlencoded"},
+ body: "username=Jason%20Davies&password="+encodeURIComponent(password)
+ });
+ // should this be a redirect code instead of 200?
+ T(xhr.status == 200);
+
+ usersDb.deleteDb();
+ // test user creation
+ T(CouchDB.createUser("test", "testpassword", "test@somemail.com", ['read', 'write']).ok);
+
+ // make sure we create a unique user
+ T(!CouchDB.createUser("test", "testpassword2", "test2@somemail.com", ['read', 'write']).ok);
+
+ // test login
+ T(CouchDB.login("test", "testpassword").ok);
+ T(!CouchDB.login('test', "testpassword2").ok);
+
+ // test update user without changing password
+ T(CouchDB.updateUser("test", "test2@somemail.com").ok);
+ result = usersDb.view("_auth/users", {key: "test"});
+ T(result.rows[0].value['email'] == "test2@somemail.com");
+
+
+ // test changing password
+ result = usersDb.view("_auth/users", {key: "test"});
+ T(CouchDB.updateUser("test", "test2@somemail.com", [], "testpassword2", "testpassword").ok);
+ result1 = usersDb.view("_auth/users", {key: "test"});
+ T(result.rows[0].value['password_sha'] != result1.rows[0].value['password_sha']);
+
+
+ // test changing password with passing old password
+ T(!CouchDB.updateUser("test", "test2@somemail.com", [], "testpassword2").ok);
+
+ // test changing password whith bad old password
+ T(!CouchDB.updateUser("test", "test2@somemail.com", [], "testpassword2", "badpasswword").ok);
+
+ // Only admins can change roles
+ T(!CouchDB.updateUser("test", "test2@somemail.com", ['read', 'write']).ok);
+
+ T(CouchDB.logout().ok);
+
+ T(CouchDB.updateUser("test", "test2@somemail.com").ok);
+ result = usersDb.view("_auth/users", {key: "test"});
+ T(result.rows[0].value['email'] == "test2@somemail.com");
+
+ // test changing password, we don't need to set old password when we are admin
+ result = usersDb.view("_auth/users", {key: "test"});
+ T(CouchDB.updateUser("test", "test2@somemail.com", [], "testpassword3").ok);
+ result1 = usersDb.view("_auth/users", {key: "test"});
+ T(result.rows[0].value['password_sha'] != result1.rows[0].value['password_sha']);
+
+ // Only admins can change roles
+ T(CouchDB.updateUser("test", "test2@somemail.com", ['read']).ok);
+
+ } finally {
+ // Make sure we erase any auth cookies so we don't affect other tests
+ T(CouchDB.logout().ok);
+ }
+ };
+
+ run_on_modified_server(
+ [{section: "httpd",
+ key: "authentication_handlers",
+ value: "{couch_httpd_auth, cookie_authentication_handler}"},
+ {section: "couch_httpd_auth",
+ key: "secret", value: generateSecret(64)},
+ {section: "couch_httpd_auth",
+ key: "authentication_db", value: "test_suite_users"}],
+ testFun
+ );
+
+};
diff --git a/share/www/script/test/oauth.js b/share/www/script/test/oauth.js
new file mode 100644
index 00000000..fb4ab818
--- /dev/null
+++ b/share/www/script/test/oauth.js
@@ -0,0 +1,166 @@
+// 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.
+
+couchTests.oauth = function(debug) {
+ // This tests OAuth authentication.
+
+ var authorization_url = "/_oauth/authorize";
+
+ var db = new CouchDB("test_suite_db");
+ db.deleteDb();
+ db.createDb();
+ if (debug) debugger;
+
+ var dbA = new CouchDB("test_suite_db_a");
+ var dbB = new CouchDB("test_suite_db_b");
+ dbA.deleteDb();
+ dbA.createDb();
+ dbB.deleteDb();
+ dbB.createDb();
+
+ // Simple secret key generator
+ function generateSecret(length) {
+ var secret = '';
+ for (var i=0; i<length; i++) {
+ secret += String.fromCharCode(Math.floor(Math.random() * 256));
+ }
+ return secret;
+ }
+
+ function oauthRequest(path, message, accessor, method) {
+ message.action = path;
+ message.method = method || 'GET';
+ OAuth.SignatureMethod.sign(message, accessor);
+ var parameters = message.parameters;
+ if (method == "POST" || method == "GET") {
+ if (method == "GET") {
+ return CouchDB.request("GET", OAuth.addToURL(path, parameters));
+ } else {
+ return CouchDB.request("POST", path, {
+ headers: {"Content-Type": "application/x-www-form-urlencoded"},
+ body: OAuth.formEncode(parameters)
+ });
+ }
+ } else {
+ return CouchDB.request("GET", path, {
+ headers: {Authorization: OAuth.getAuthorizationHeader('', parameters)}
+ });
+ }
+ }
+
+ var consumerSecret = generateSecret(64);
+ var tokenSecret = generateSecret(64);
+
+ var host = CouchDB.host;
+ var dbPair = {
+ source: {
+ url: "http://" + host + "/test_suite_db_a",
+ auth: {
+ oauth: {
+ consumer_key: "key",
+ consumer_secret: consumerSecret,
+ token_secret: tokenSecret,
+ token: "foo"
+ }
+ }
+ },
+ target: "http://" + host + "/test_suite_db_b"
+ };
+
+ // this function will be called on the modified server
+ var testFun = function () {
+ try {
+ var usersDb = new CouchDB("test_suite_users");
+ usersDb.deleteDb();
+ usersDb.createDb();
+
+ // Create a user
+ T(CouchDB.createUser("jason", "testpassword", "test@somemail.com", ['test'], true).ok);
+
+ var accessor = {
+ consumerSecret: consumerSecret,
+ tokenSecret: tokenSecret
+ };
+
+ var signatureMethods = ["PLAINTEXT", "HMAC-SHA1"];
+ var consumerKeys = {key: 200, nonexistent_key: 400};
+
+ for (var i=0; i<signatureMethods.length; i++) {
+ for (var consumerKey in consumerKeys) {
+ var expectedCode = consumerKeys[consumerKey];
+ var message = {
+ parameters: {
+ oauth_signature_method: signatureMethods[i],
+ oauth_consumer_key: consumerKey,
+ oauth_token: "foo",
+ oauth_token_secret: tokenSecret,
+ oauth_version: "1.0"
+ }
+ };
+
+ // Get request token via Authorization header
+ xhr = oauthRequest("http://" + host + "/_oauth/request_token", message, accessor);
+ T(xhr.status == expectedCode);
+
+ // GET request token via query parameters
+ xhr = oauthRequest("http://" + host + "/_oauth/request_token", message, accessor, "GET");
+ T(xhr.status == expectedCode);
+
+ responseMessage = OAuth.decodeForm(xhr.responseText);
+
+ // Obtaining User Authorization
+ //Only needed for 3-legged OAuth
+ //xhr = CouchDB.request("GET", authorization_url + '?oauth_token=' + responseMessage.oauth_token);
+ //T(xhr.status == expectedCode);
+
+ xhr = oauthRequest("http://" + host + "/_session", message, accessor);
+ T(xhr.status == expectedCode);
+ if (xhr.status == expectedCode == 200) {
+ data = JSON.parse(xhr.responseText);
+ T(data.name == "jason");
+ T(data.roles[0] == "test");
+ }
+
+ xhr = oauthRequest("http://" + host + "/_session?foo=bar", message, accessor);
+ T(xhr.status == expectedCode);
+
+ // Replication
+ var result = CouchDB.replicate(dbPair.source, dbPair.target);
+ T(result.ok);
+ }
+ }
+
+ } finally {
+ }
+ };
+
+ run_on_modified_server(
+ [{section: "httpd",
+ key: "WWW-Authenticate", value: 'Basic realm="administrator",OAuth'},
+ {section: "couch_httpd_auth",
+ key: "secret", value: generateSecret(64)},
+ {section: "couch_httpd_auth",
+ key: "authentication_db", value: "test_suite_users"},
+ {section: "oauth_consumer_secrets",
+ key: "key", value: consumerSecret},
+ {section: "oauth_token_users",
+ key: "foo", value: "jason"},
+ {section: "oauth_token_secrets",
+ key: "foo", value: tokenSecret},
+ {section: "couch_httpd_oauth",
+ key: "authorization_url", value: authorization_url},
+ {section: "httpd",
+ key: "authentication_handlers",
+ value: "{couch_httpd_oauth, oauth_authentication_handler}, {couch_httpd_auth, default_authentication_handler}"}],
+ testFun
+ );
+};
diff --git a/share/www/script/test/security_validation.js b/share/www/script/test/security_validation.js
index 10553e5e..52f895bd 100644
--- a/share/www/script/test/security_validation.js
+++ b/share/www/script/test/security_validation.js
@@ -39,14 +39,14 @@ couchTests.security_validation = function(debug) {
run_on_modified_server(
[{section: "httpd",
- key: "authentication_handler",
- value: "{couch_httpd, special_test_authentication_handler}"},
+ key: "authentication_handlers",
+ value: "{couch_httpd_auth, special_test_authentication_handler}"},
{section:"httpd",
key: "WWW-Authenticate",
value: "X-Couch-Test-Auth"}],
function () {
- // try saving document usin the wrong credentials
+ // try saving document using the wrong credentials
var wrongPasswordDb = new CouchDB("test_suite_db",
{"WWW-Authenticate": "X-Couch-Test-Auth Damien Katz:foo"}
);
@@ -59,8 +59,8 @@ couchTests.security_validation = function(debug) {
T(wrongPasswordDb.last_req.status == 401);
}
- // test force_login=true.
- var resp = wrongPasswordDb.request("GET", "/_whoami?force_login=true");
+ // test force basic login
+ var resp = wrongPasswordDb.request("GET", "/_session?basic=true");
var err = JSON.parse(resp.responseText);
T(err.error == "unauthorized");
T(resp.status == 401);
@@ -104,7 +104,7 @@ couchTests.security_validation = function(debug) {
T(userDb.save(designDoc).ok);
// test the _whoami endpoint
- var resp = userDb.request("GET", "/_whoami");
+ var resp = userDb.request("GET", "/_session");
var user = JSON.parse(resp.responseText)
T(user.name == "Damien Katz");
// test that the roles are listed properly
diff --git a/share/www/script/test/show_documents.js b/share/www/script/test/show_documents.js
index a1138596..c87cfb0b 100644
--- a/share/www/script/test/show_documents.js
+++ b/share/www/script/test/show_documents.js
@@ -335,7 +335,7 @@ couchTests.show_documents = function(debug) {
var doc2 = {_id:"foo", a:2};
db.save(doc1);
- //create the conflict with a all_or_nothing bulk docs request
+ // create the conflict with an all_or_nothing bulk docs request
var docs = [doc2];
db.bulkSave(docs, {all_or_nothing:true});
diff --git a/src/couchdb/Makefile.am b/src/couchdb/Makefile.am
index fce89f3c..ec707d64 100644
--- a/src/couchdb/Makefile.am
+++ b/src/couchdb/Makefile.am
@@ -61,6 +61,8 @@ source_files = \
couch_file.erl \
couch_httpd.erl \
couch_httpd_db.erl \
+ couch_httpd_auth.erl \
+ couch_httpd_oauth.erl \
couch_httpd_external.erl \
couch_httpd_show.erl \
couch_httpd_view.erl \
@@ -106,6 +108,8 @@ compiled_files = \
couch_file.beam \
couch_httpd.beam \
couch_httpd_db.beam \
+ couch_httpd_auth.beam \
+ couch_httpd_oauth.beam \
couch_httpd_external.beam \
couch_httpd_show.beam \
couch_httpd_view.beam \
diff --git a/src/couchdb/couch_db.hrl b/src/couchdb/couch_db.hrl
index 4eda42e6..d4e0fbd5 100644
--- a/src/couchdb/couch_db.hrl
+++ b/src/couchdb/couch_db.hrl
@@ -67,7 +67,8 @@
db_url_handlers,
user_ctx,
req_body = undefined,
- design_url_handlers
+ design_url_handlers,
+ auth
}).
diff --git a/src/couchdb/couch_httpd.erl b/src/couchdb/couch_httpd.erl
index 616ada8c..766d18d0 100644
--- a/src/couchdb/couch_httpd.erl
+++ b/src/couchdb/couch_httpd.erl
@@ -23,8 +23,6 @@
-export([start_json_response/2, start_json_response/3, end_json_response/1]).
-export([send_response/4,send_method_not_allowed/2,send_error/4, send_redirect/2,send_chunked_error/2]).
-export([send_json/2,send_json/3,send_json/4]).
--export([default_authentication_handler/1,special_test_authentication_handler/1]).
--export([null_authentication_handler/1]).
start_link() ->
% read config and register for configuration changes
@@ -111,6 +109,9 @@ make_arity_2_fun(SpecStr) ->
fun(Arg1, Arg2) -> apply(Mod, Fun, [Arg1, Arg2]) end
end.
+% SpecStr is "{my_module, my_fun}, {my_module2, my_fun2}"
+make_arity_1_fun_list(SpecStr) ->
+ [make_arity_1_fun(FunSpecStr) || FunSpecStr <- re:split(SpecStr, "(?<=})\\s*,\\s*(?={)", [{return, list}])].
stop() ->
mochiweb_http:stop(?MODULE).
@@ -119,8 +120,8 @@ stop() ->
handle_request(MochiReq, DefaultFun,
UrlHandlers, DbUrlHandlers, DesignUrlHandlers) ->
Begin = now(),
- AuthenticationFun = make_arity_1_fun(
- couch_config:get("httpd", "authentication_handler")),
+ AuthenticationFuns = make_arity_1_fun_list(
+ couch_config:get("httpd", "authentication_handlers")),
% for the path, use the raw path with the query string and fragment
% removed, but URL quoting left intact
RawUri = MochiReq:get(raw_path),
@@ -171,11 +172,25 @@ handle_request(MochiReq, DefaultFun,
{ok, Resp} =
try
- HandlerFun(HttpReq#httpd{user_ctx=AuthenticationFun(HttpReq)})
+ % Try authentication handlers in order until one returns a result
+ case lists:foldl(fun(_Fun, #httpd{user_ctx=#user_ctx{}}=Req) -> Req;
+ (Fun, #httpd{}=Req) -> Fun(Req);
+ (_Fun, Response) -> Response
+ end, HttpReq, AuthenticationFuns) of
+ #httpd{user_ctx=#user_ctx{}}=Req -> HandlerFun(Req);
+ #httpd{}=Req ->
+ case couch_config:get("couch_httpd_auth", "require_valid_user", "false") of
+ "true" ->
+ throw({unauthorized, <<"Authentication required.">>});
+ _ ->
+ HandlerFun(Req#httpd{user_ctx=#user_ctx{}})
+ end;
+ Response -> Response
+ end
catch
throw:Error ->
- % ?LOG_DEBUG("Minor error in HTTP request: ~p",[Error]),
- % ?LOG_DEBUG("Stacktrace: ~p",[erlang:get_stacktrace()]),
+ ?LOG_DEBUG("Minor error in HTTP request: ~p",[Error]),
+ ?LOG_DEBUG("Stacktrace: ~p",[erlang:get_stacktrace()]),
send_error(HttpReq, Error);
error:badarg ->
?LOG_ERROR("Badarg error in HTTP request",[]),
@@ -205,48 +220,6 @@ handle_request(MochiReq, DefaultFun,
increment_method_stats(Method) ->
couch_stats_collector:increment({httpd_request_methods, Method}).
-special_test_authentication_handler(Req) ->
- case header_value(Req, "WWW-Authenticate") of
- "X-Couch-Test-Auth " ++ NamePass ->
- % NamePass is a colon separated string: "joe schmoe:a password".
- {ok, [Name, Pass]} = regexp:split(NamePass, ":"),
- case {Name, Pass} of
- {"Jan Lehnardt", "apple"} -> ok;
- {"Christopher Lenz", "dog food"} -> ok;
- {"Noah Slater", "biggiesmalls endian"} -> ok;
- {"Chris Anderson", "mp3"} -> ok;
- {"Damien Katz", "pecan pie"} -> ok;
- {_, _} ->
- throw({unauthorized, <<"Name or password is incorrect.">>})
- end,
- #user_ctx{name=?l2b(Name)};
- _ ->
- % No X-Couch-Test-Auth credentials sent, give admin access so the
- % previous authentication can be restored after the test
- #user_ctx{roles=[<<"_admin">>]}
- end.
-
-default_authentication_handler(Req) ->
- case basic_username_pw(Req) of
- {User, Pass} ->
- case couch_server:is_admin(User, Pass) of
- true ->
- #user_ctx{name=?l2b(User), roles=[<<"_admin">>]};
- false ->
- throw({unauthorized, <<"Name or password is incorrect.">>})
- end;
- nil ->
- case couch_server:has_admins() of
- true ->
- #user_ctx{};
- false ->
- % if no admins, then everyone is admin! Yay, admin party!
- #user_ctx{roles=[<<"_admin">>]}
- end
- end.
-
-null_authentication_handler(_Req) ->
- #user_ctx{roles=[<<"_admin">>]}.
% Utilities
@@ -265,8 +238,9 @@ header_value(#httpd{mochi_req=MochiReq}, Key, Default) ->
primary_header_value(#httpd{mochi_req=MochiReq}, Key) ->
MochiReq:get_primary_header_value(Key).
-serve_file(#httpd{mochi_req=MochiReq}, RelativePath, DocumentRoot) ->
- {ok, MochiReq:serve_file(RelativePath, DocumentRoot, server_header())}.
+serve_file(#httpd{mochi_req=MochiReq}=Req, RelativePath, DocumentRoot) ->
+ {ok, MochiReq:serve_file(RelativePath, DocumentRoot,
+ server_header() ++ couch_httpd_auth:cookie_auth_header(Req, []))}.
qs_value(Req, Key) ->
qs_value(Req, Key, undefined).
@@ -307,11 +281,16 @@ recv_chunked(#httpd{mochi_req=MochiReq}, MaxChunkSize, ChunkFun, InitState) ->
% called with Length == 0 on the last time.
MochiReq:stream_body(MaxChunkSize, ChunkFun, InitState).
-body(#httpd{mochi_req=MochiReq}) ->
- % Maximum size of document PUT request body (4GB)
- MaxSize = list_to_integer(
- couch_config:get("couchdb", "max_document_size", "4294967296")),
- MochiReq:recv_body(MaxSize).
+body(#httpd{mochi_req=MochiReq, req_body=ReqBody}) ->
+ case ReqBody of
+ undefined ->
+ % Maximum size of document PUT request body (4GB)
+ MaxSize = list_to_integer(
+ couch_config:get("couchdb", "max_document_size", "4294967296")),
+ MochiReq:recv_body(MaxSize);
+ _Else ->
+ ReqBody
+ end.
json_body(Httpd) ->
?JSON_DECODE(body(Httpd)).
@@ -357,37 +336,21 @@ verify_is_server_admin(#httpd{user_ctx=#user_ctx{roles=Roles}}) ->
-basic_username_pw(Req) ->
- case header_value(Req, "Authorization") of
- "Basic " ++ Base64Value ->
- case string:tokens(?b2l(couch_util:decodeBase64(Base64Value)),":") of
- [User, Pass] ->
- {User, Pass};
- [User] ->
- {User, ""};
- _ ->
- nil
- end;
- _ ->
- nil
- end.
-
-
-start_chunked_response(#httpd{mochi_req=MochiReq}, Code, Headers) ->
+start_chunked_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers) ->
couch_stats_collector:increment({httpd_status_codes, Code}),
- {ok, MochiReq:respond({Code, Headers ++ server_header(), chunked})}.
+ {ok, MochiReq:respond({Code, Headers ++ server_header() ++ couch_httpd_auth:cookie_auth_header(Req, Headers), chunked})}.
send_chunk(Resp, Data) ->
Resp:write_chunk(Data),
{ok, Resp}.
-send_response(#httpd{mochi_req=MochiReq}, Code, Headers, Body) ->
+send_response(#httpd{mochi_req=MochiReq}=Req, Code, Headers, Body) ->
couch_stats_collector:increment({httpd_status_codes, Code}),
if Code >= 400 ->
?LOG_DEBUG("httpd ~p error response:~n ~s", [Code, Body]);
true -> ok
end,
- {ok, MochiReq:respond({Code, Headers ++ server_header(), Body})}.
+ {ok, MochiReq:respond({Code, Headers ++ server_header() ++ couch_httpd_auth:cookie_auth_header(Req, Headers), Body})}.
send_method_not_allowed(Req, Methods) ->
send_response(Req, 405, [{"Allow", Methods}], <<>>).
@@ -508,17 +471,22 @@ error_info(Error) ->
send_error(_Req, {already_sent, Resp, _Error}) ->
{ok, Resp};
-send_error(Req, Error) ->
+send_error(#httpd{mochi_req=MochiReq}=Req, Error) ->
{Code, ErrorStr, ReasonStr} = error_info(Error),
- if Code == 401 ->
- case couch_config:get("httpd", "WWW-Authenticate", nil) of
- nil ->
- Headers = [];
+ Headers = if Code == 401 ->
+ case MochiReq:get_header_value("X-CouchDB-WWW-Authenticate") of
+ undefined ->
+ case couch_config:get("httpd", "WWW-Authenticate", nil) of
+ nil ->
+ [];
+ Type ->
+ [{"WWW-Authenticate", Type}]
+ end;
Type ->
- Headers = [{"WWW-Authenticate", Type}]
+ [{"WWW-Authenticate", Type}]
end;
true ->
- Headers = []
+ []
end,
send_error(Req, Code, Headers, ErrorStr, ReasonStr).
diff --git a/src/couchdb/couch_httpd_auth.erl b/src/couchdb/couch_httpd_auth.erl
new file mode 100644
index 00000000..29a50eb5
--- /dev/null
+++ b/src/couchdb/couch_httpd_auth.erl
@@ -0,0 +1,507 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_httpd_auth).
+-include("couch_db.hrl").
+
+-export([default_authentication_handler/1,special_test_authentication_handler/1]).
+-export([cookie_authentication_handler/1]).
+-export([null_authentication_handler/1]).
+-export([cookie_auth_header/2]).
+-export([handle_session_req/1]).
+-export([handle_user_req/1]).
+-export([ensure_users_db_exists/1, get_user/2]).
+
+-import(couch_httpd, [header_value/2, send_json/2,send_json/4, send_method_not_allowed/2]).
+
+special_test_authentication_handler(Req) ->
+ case header_value(Req, "WWW-Authenticate") of
+ "X-Couch-Test-Auth " ++ NamePass ->
+ % NamePass is a colon separated string: "joe schmoe:a password".
+ [Name, Pass] = re:split(NamePass, ":", [{return, list}]),
+ case {Name, Pass} of
+ {"Jan Lehnardt", "apple"} -> ok;
+ {"Christopher Lenz", "dog food"} -> ok;
+ {"Noah Slater", "biggiesmalls endian"} -> ok;
+ {"Chris Anderson", "mp3"} -> ok;
+ {"Damien Katz", "pecan pie"} -> ok;
+ {_, _} ->
+ throw({unauthorized, <<"Name or password is incorrect.">>})
+ end,
+ Req#httpd{user_ctx=#user_ctx{name=?l2b(Name)}};
+ _ ->
+ % No X-Couch-Test-Auth credentials sent, give admin access so the
+ % previous authentication can be restored after the test
+ Req#httpd{user_ctx=#user_ctx{roles=[<<"_admin">>]}}
+ end.
+
+basic_username_pw(Req) ->
+ case header_value(Req, "Authorization") of
+ "Basic " ++ Base64Value ->
+ case string:tokens(?b2l(couch_util:decodeBase64(Base64Value)),":") of
+ [User, Pass] ->
+ {User, Pass};
+ [User] ->
+ {User, ""};
+ _ ->
+ nil
+ end;
+ _ ->
+ nil
+ end.
+
+default_authentication_handler(Req) ->
+ case basic_username_pw(Req) of
+ {User, Pass} ->
+ case couch_server:is_admin(User, Pass) of
+ true ->
+ Req#httpd{user_ctx=#user_ctx{name=?l2b(User), roles=[<<"_admin">>]}};
+ false ->
+ throw({unauthorized, <<"Name or password is incorrect.">>})
+ end;
+ nil ->
+ case couch_server:has_admins() of
+ true ->
+ Req;
+ false ->
+ case couch_config:get("couch_httpd_auth", "require_valid_user", "false") of
+ "true" -> Req;
+ % If no admins, and no user required, then everyone is admin!
+ % Yay, admin party!
+ _ -> Req#httpd{user_ctx=#user_ctx{roles=[<<"_admin">>]}}
+ end
+ end
+ end.
+
+null_authentication_handler(Req) ->
+ Req#httpd{user_ctx=#user_ctx{roles=[<<"_admin">>]}}.
+
+% Cookie auth handler using per-node user db
+cookie_authentication_handler(Req) ->
+ DbName = couch_config:get("couch_httpd_auth", "authentication_db"),
+ case cookie_auth_user(Req, ?l2b(DbName)) of
+ % Fall back to default authentication handler
+ nil -> default_authentication_handler(Req);
+ Req2 -> Req2
+ end.
+
+% Cookie auth handler using per-db user db
+% cookie_authentication_handler(#httpd{path_parts=Path}=Req) ->
+% case Path of
+% [DbName|_] ->
+% case cookie_auth_user(Req, DbName) of
+% nil -> default_authentication_handler(Req);
+% Req2 -> Req2
+% end;
+% _Else ->
+% % Fall back to default authentication handler
+% default_authentication_handler(Req)
+% end.
+
+% maybe we can use hovercraft to simplify running this view query
+get_user(Db, UserName) ->
+ DesignId = <<"_design/_auth">>,
+ ViewName = <<"users">>,
+ % if the design doc or the view doesn't exist, then make it
+ ensure_users_view_exists(Db, DesignId, ViewName),
+
+ case (catch couch_view:get_map_view(Db, DesignId, ViewName, nil)) of
+ {ok, View, _Group} ->
+ FoldlFun = fun
+ ({{Key, _DocId}, Value}, _, nil) when Key == UserName -> {ok, Value};
+ (_, _, Acc) -> {stop, Acc}
+ end,
+ case couch_view:fold(View, {UserName, nil}, fwd, FoldlFun, nil) of
+ {ok, {Result}} -> Result;
+ _Else -> nil
+ end;
+ {not_found, _Reason} ->
+ nil
+ % case (catch couch_view:get_reduce_view(Db, DesignId, ViewName, nil)) of
+ % {ok, _ReduceView, _Group} ->
+ % not_implemented;
+ % {not_found, _Reason} ->
+ % nil
+ % end
+ end.
+
+ensure_users_db_exists(DbName) ->
+ case couch_db:open(DbName, [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]) of
+ {ok, Db} ->
+ couch_db:close(Db),
+ ok;
+ _Error ->
+ ?LOG_ERROR("Create the db ~p", [DbName]),
+ {ok, Db} = couch_db:create(DbName, [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]),
+ ?LOG_ERROR("Created the db ~p", [DbName]),
+ couch_db:close(Db),
+ ok
+ end.
+
+ensure_users_view_exists(Db, DDocId, VName) ->
+ try couch_httpd_db:couch_doc_open(Db, DDocId, nil, []) of
+ _Foo -> ok
+ catch
+ _:Error ->
+ ?LOG_ERROR("create the design document ~p : ~p", [DDocId, Error]),
+ % create the design document
+ {ok, AuthDesign} = auth_design_doc(DDocId, VName),
+ {ok, _Rev} = couch_db:update_doc(Db, AuthDesign, []),
+ ?LOG_ERROR("created the design document", []),
+ ok
+ end.
+
+auth_design_doc(DocId, VName) ->
+ DocProps = [
+ {<<"_id">>, DocId},
+ {<<"language">>,<<"javascript">>},
+ {<<"views">>,
+ {[{VName,
+ {[{<<"map">>,
+ <<"function (doc) {\n if (doc.type == \"user\") {\n emit(doc.username, doc);\n}\n}">>
+ }]}
+ }]}
+ }],
+ {ok, couch_doc:from_json_obj({DocProps})}.
+
+
+user_doc(DocId, Username, UserSalt, PasswordHash, Email, Active, Roles) ->
+ user_doc(DocId, Username, UserSalt, PasswordHash, Email, Active, Roles, nil).
+user_doc(DocId, Username, UserSalt, PasswordHash, Email, Active, Roles, Rev) ->
+ DocProps = [
+ {<<"_id">>, DocId},
+ {<<"type">>, <<"user">>},
+ {<<"username">>, Username},
+ {<<"password_sha">>, PasswordHash},
+ {<<"salt">>, UserSalt},
+ {<<"email">>, Email},
+ {<<"active">>, Active},
+ {<<"roles">>, Roles}],
+ DocProps1 = case Rev of
+ nil -> DocProps;
+ _Rev ->
+ [{<<"_rev">>, Rev}] ++ DocProps
+ end,
+ {ok, couch_doc:from_json_obj({DocProps1})}.
+
+cookie_auth_user(_Req, undefined) -> nil;
+cookie_auth_user(#httpd{mochi_req=MochiReq}=Req, DbName) ->
+ case MochiReq:get_cookie_value("AuthSession") of
+ undefined -> nil;
+ [] -> nil;
+ Cookie ->
+ case couch_db:open(DbName, [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]) of
+ {ok, Db} ->
+ try
+ AuthSession = couch_util:decodeBase64Url(Cookie),
+ [User, TimeStr | HashParts] = string:tokens(?b2l(AuthSession), ":"),
+ % Verify expiry and hash
+ {NowMS, NowS, _} = erlang:now(),
+ CurrentTime = NowMS * 1000000 + NowS,
+ case couch_config:get("couch_httpd_auth", "secret", nil) of
+ nil -> nil;
+ SecretStr ->
+ Secret = ?l2b(SecretStr),
+ case get_user(Db, ?l2b(User)) of
+ nil -> nil;
+ Result ->
+ UserSalt = proplists:get_value(<<"salt">>, Result, <<"">>),
+ FullSecret = <<Secret/binary, UserSalt/binary>>,
+ ExpectedHash = crypto:sha_mac(FullSecret, User ++ ":" ++ TimeStr),
+ Hash = ?l2b(string:join(HashParts, ":")),
+ Timeout = to_int(couch_config:get("couch_httpd_auth", "timeout", 600)),
+ ?LOG_DEBUG("timeout ~p", [Timeout]),
+ case (catch erlang:list_to_integer(TimeStr, 16)) of
+ TimeStamp when CurrentTime < TimeStamp + Timeout
+ andalso ExpectedHash == Hash ->
+ TimeLeft = TimeStamp + Timeout - CurrentTime,
+ ?LOG_DEBUG("Successful cookie auth as: ~p", [User]),
+ Req#httpd{user_ctx=#user_ctx{
+ name=?l2b(User),
+ roles=proplists:get_value(<<"roles">>, Result, [])
+ }, auth={FullSecret, TimeLeft < Timeout*0.9}};
+ _Else ->
+ nil
+ end
+ end
+ end
+ after
+ couch_db:close(Db)
+ end;
+ _Else ->
+ nil
+ end
+ end.
+
+cookie_auth_header(#httpd{user_ctx=#user_ctx{name=null}}, _Headers) -> [];
+cookie_auth_header(#httpd{user_ctx=#user_ctx{name=User}, auth={Secret, true}}, Headers) ->
+ % Note: we only set the AuthSession cookie if:
+ % * a valid AuthSession cookie has been received
+ % * we are outside a 10% timeout window
+ % * and if an AuthSession cookie hasn't already been set e.g. by a login
+ % or logout handler.
+ % The login and logout handlers need to set the AuthSession cookie
+ % themselves.
+ case proplists:get_value("Set-Cookie", Headers) of
+ undefined -> [];
+ Cookie ->
+ case proplists:get_value("AuthSession",
+ mochiweb_cookies:parse_cookie(Cookie), undefined) of
+ undefined ->
+ {NowMS, NowS, _} = erlang:now(),
+ TimeStamp = NowMS * 1000000 + NowS,
+ [cookie_auth_cookie(?b2l(User), Secret, TimeStamp)];
+ _Else -> []
+ end
+ end;
+cookie_auth_header(_Req, _Headers) -> [].
+
+cookie_auth_cookie(User, Secret, TimeStamp) ->
+ SessionData = User ++ ":" ++ erlang:integer_to_list(TimeStamp, 16),
+ Hash = crypto:sha_mac(Secret, SessionData),
+ mochiweb_cookies:cookie("AuthSession",
+ couch_util:encodeBase64Url(SessionData ++ ":" ++ ?b2l(Hash)),
+ [{path, "/"}, {http_only, true}]). % TODO add {secure, true} when SSL is detected
+
+% Login handler with user db
+handle_login_req(#httpd{method='POST', mochi_req=MochiReq}=Req, #db{}=Db) ->
+ ReqBody = MochiReq:recv_body(),
+ Form = case MochiReq:get_primary_header_value("content-type") of
+ "application/x-www-form-urlencoded" ++ _ ->
+ mochiweb_util:parse_qs(ReqBody);
+ _ ->
+ []
+ end,
+ UserName = ?l2b(proplists:get_value("username", Form, "")),
+ Password = ?l2b(proplists:get_value("password", Form, "")),
+ User = case get_user(Db, UserName) of
+ nil -> [];
+ Result -> Result
+ end,
+ UserSalt = proplists:get_value(<<"salt">>, User, <<>>),
+ PasswordHash = couch_util:encodeBase64(crypto:sha(<<UserSalt/binary, Password/binary>>)),
+ case proplists:get_value(<<"password_sha">>, User, nil) of
+ ExpectedHash when ExpectedHash == PasswordHash ->
+ Secret = ?l2b(couch_config:get("couch_httpd_auth", "secret", nil)),
+ {NowMS, NowS, _} = erlang:now(),
+ CurrentTime = NowMS * 1000000 + NowS,
+ Cookie = cookie_auth_cookie(?b2l(UserName), <<Secret/binary, UserSalt/binary>>, CurrentTime),
+ {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of
+ nil ->
+ {200, [Cookie]};
+ Redirect ->
+ {302, [Cookie, {"Location", couch_httpd:absolute_uri(Req, Redirect)}]}
+ end,
+ send_json(Req#httpd{req_body=ReqBody}, Code, Headers,
+ {[{ok, true}]});
+ _Else ->
+ throw({unauthorized, <<"Name or password is incorrect.">>})
+ end.
+
+% Session Handler
+
+handle_session_req(#httpd{method='POST'}=Req) ->
+ % login
+ DbName = couch_config:get("couch_httpd_auth", "authentication_db"),
+ case couch_db:open(?l2b(DbName), [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]) of
+ {ok, Db} -> handle_login_req(Req, Db)
+ end;
+handle_session_req(#httpd{method='GET', user_ctx=UserCtx}=Req) ->
+ % whoami
+ Name = UserCtx#user_ctx.name,
+ Roles = UserCtx#user_ctx.roles,
+ ForceLogin = couch_httpd:qs_value(Req, "basic", "false"),
+ case {Name, ForceLogin} of
+ {null, "true"} ->
+ throw({unauthorized, <<"Please login.">>});
+ _False -> ok
+ end,
+ send_json(Req, {[
+ {ok, true},
+ {name, Name},
+ {roles, Roles}
+ ]});
+handle_session_req(#httpd{method='DELETE'}=Req) ->
+ % logout
+ Cookie = mochiweb_cookies:cookie("AuthSession", "", [{path, "/"}, {http_only, true}]),
+ {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of
+ nil ->
+ {200, [Cookie]};
+ Redirect ->
+ {302, [Cookie, {"Location", couch_httpd:absolute_uri(Req, Redirect)}]}
+ end,
+ send_json(Req, Code, Headers, {[{ok, true}]});
+handle_session_req(Req) ->
+ send_method_not_allowed(Req, "GET,HEAD,POST,DELETE").
+
+create_user_req(#httpd{method='POST', mochi_req=MochiReq}=Req, Db) ->
+ ReqBody = MochiReq:recv_body(),
+ Form = case MochiReq:get_primary_header_value("content-type") of
+ "application/x-www-form-urlencoded" ++ _ ->
+ ?LOG_INFO("body parsed ~p", [mochiweb_util:parse_qs(ReqBody)]),
+ mochiweb_util:parse_qs(ReqBody);
+ _ ->
+ []
+ end,
+ Roles = proplists:get_all_values("roles", Form),
+ UserName = ?l2b(proplists:get_value("username", Form, "")),
+ Password = ?l2b(proplists:get_value("password", Form, "")),
+ Email = ?l2b(proplists:get_value("email", Form, "")),
+ Active = couch_httpd_view:parse_bool_param(proplists:get_value("active", Form, "true")),
+ case get_user(Db, UserName) of
+ nil ->
+ Roles1 = case Roles of
+ [] -> Roles;
+ _ ->
+ ok = couch_httpd:verify_is_server_admin(Req),
+ [?l2b(R) || R <- Roles]
+ end,
+
+ UserSalt = couch_util:new_uuid(),
+ PasswordHash = couch_util:encodeBase64(crypto:sha(<<UserSalt/binary, Password/binary>>)),
+ DocId = couch_util:new_uuid(),
+ {ok, UserDoc} = user_doc(DocId, UserName, UserSalt, PasswordHash, Email, Active, Roles1),
+ {ok, _Rev} = couch_db:update_doc(Db, UserDoc, []),
+ ?LOG_DEBUG("User ~s (~s) with password, ~s created.", [?b2l(UserName), ?b2l(DocId), ?b2l(Password)]),
+ {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of
+ nil ->
+ {200, []};
+ Redirect ->
+ {302, [{"Location", couch_httpd:absolute_uri(Req, Redirect)}]}
+ end,
+ send_json(Req, Code, Headers, {[{ok, true}]});
+ _Result ->
+ ?LOG_DEBUG("Can't create ~s: already exists", [?b2l(UserName)]),
+ throw({forbidden, <<"User already exists.">>})
+ end.
+
+update_user_req(#httpd{method='PUT', mochi_req=MochiReq, user_ctx=UserCtx}=Req, Db, UserName) ->
+ Name = UserCtx#user_ctx.name,
+ UserRoles = UserCtx#user_ctx.roles,
+ case User = get_user(Db, UserName) of
+ nil ->
+ throw({not_found, <<"User don't exist">>});
+ _Result ->
+ ReqBody = MochiReq:recv_body(),
+ Form = case MochiReq:get_primary_header_value("content-type") of
+ "application/x-www-form-urlencoded" ++ _ ->
+ mochiweb_util:parse_qs(ReqBody);
+ _ ->
+ []
+ end,
+ Roles = proplists:get_all_values("roles", Form),
+ Password = ?l2b(proplists:get_value("password", Form, "")),
+ Email = ?l2b(proplists:get_value("email", Form, "")),
+ Active = couch_httpd_view:parse_bool_param(proplists:get_value("active", Form, "true")),
+ OldPassword = proplists:get_value("old_password", Form, ""),
+ OldPassword1 = ?l2b(OldPassword),
+ UserSalt = proplists:get_value(<<"salt">>, User, <<>>),
+ OldRev = proplists:get_value(<<"_rev">>, User, <<>>),
+ DocId = proplists:get_value(<<"_id">>, User, <<>>),
+ CurrentPasswordHash = proplists:get_value(<<"password_sha">>, User, nil),
+
+
+ Roles1 = case Roles of
+ [] -> Roles;
+ _ ->
+ ok = couch_httpd:verify_is_server_admin(Req),
+ [?l2b(R) || R <- Roles]
+ end,
+
+ PasswordHash = case lists:member(<<"_admin">>, UserRoles) of
+ true ->
+ Hash = case Password of
+ <<>> -> CurrentPasswordHash;
+ _Else ->
+ H = couch_util:encodeBase64(crypto:sha(<<UserSalt/binary, Password/binary>>)),
+ H
+ end,
+ Hash;
+ false when Name == UserName ->
+ %% for user we test old password before allowing change
+ Hash = case Password of
+ <<>> ->
+ CurrentPasswordHash;
+ _P when length(OldPassword) == 0 ->
+ throw({forbidden, <<"Old password is incorrect.">>});
+ _Else ->
+ OldPasswordHash = couch_util:encodeBase64(crypto:sha(<<UserSalt/binary, OldPassword1/binary>>)),
+ ?LOG_DEBUG("~p == ~p", [CurrentPasswordHash, OldPasswordHash]),
+ Hash1 = case CurrentPasswordHash of
+ ExpectedHash when ExpectedHash == OldPasswordHash ->
+ H = couch_util:encodeBase64(crypto:sha(<<UserSalt/binary, Password/binary>>)),
+ H;
+ _ ->
+ throw({forbidden, <<"Old password is incorrect.">>})
+ end,
+ Hash1
+ end,
+ Hash;
+ _ ->
+ throw({forbidden, <<"You aren't allowed to change this password.">>})
+ end,
+ {ok, UserDoc} = user_doc(DocId, UserName, UserSalt, PasswordHash, Email, Active, Roles1, OldRev),
+ {ok, _Rev} = couch_db:update_doc(Db, UserDoc, []),
+ ?LOG_DEBUG("User ~s (~s)updated.", [?b2l(UserName), ?b2l(DocId)]),
+ {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of
+ nil -> {200, []};
+ Redirect ->
+ {302, [{"Location", couch_httpd:absolute_uri(Req, Redirect)}]}
+ end,
+ send_json(Req, Code, Headers, {[{ok, true}]})
+ end.
+
+handle_user_req(#httpd{method='POST'}=Req) ->
+ DbName = couch_config:get("couch_httpd_auth", "authentication_db"),
+ ensure_users_db_exists(?l2b(DbName)),
+ case couch_db:open(?l2b(DbName), [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]) of
+ {ok, Db} -> create_user_req(Req, Db)
+ end;
+handle_user_req(#httpd{method='PUT', path_parts=[_]}=_Req) ->
+ throw({bad_request, <<"Username is missing">>});
+handle_user_req(#httpd{method='PUT', path_parts=[_, UserName]}=Req) ->
+ DbName = couch_config:get("couch_httpd_auth", "authentication_db"),
+ ensure_users_db_exists(?l2b(DbName)),
+ case couch_db:open(?l2b(DbName), [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]) of
+ {ok, Db} -> update_user_req(Req, Db, UserName)
+ end;
+handle_user_req(Req) ->
+ send_method_not_allowed(Req, "GET,HEAD,POST,PUT,DELETE").
+
+to_int(Value) when is_binary(Value) ->
+ to_int(?b2l(Value));
+to_int(Value) when is_list(Value) ->
+ erlang:list_to_integer(Value);
+to_int(Value) when is_integer(Value) ->
+ Value.
+
+% % Login handler
+% handle_login_req(#httpd{method='POST'}=Req) ->
+% DbName = couch_config:get("couch_httpd_auth", "authentication_db"),
+% case couch_db:open(?l2b(DbName), [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]) of
+% {ok, Db} -> handle_login_req(Req, Db)
+% end;
+% handle_login_req(Req) ->
+% send_method_not_allowed(Req, "POST").
+%
+% % Logout handler
+% handle_logout_req(#httpd{method='POST'}=Req) ->
+% Cookie = mochiweb_cookies:cookie("AuthSession", "", [{path, "/"}, {http_only, true}]),
+% {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of
+% nil ->
+% {200, [Cookie]};
+% Redirect ->
+% {302, [Cookie, {"Location", couch_httpd:absolute_uri(Req, Redirect)}]}
+% end,
+% send_json(Req, Code, Headers, {[{ok, true}]});
+% handle_logout_req(Req) ->
+% send_method_not_allowed(Req, "POST").
diff --git a/src/couchdb/couch_httpd_external.erl b/src/couchdb/couch_httpd_external.erl
index 4ff01d9e..d6fa945f 100644
--- a/src/couchdb/couch_httpd_external.erl
+++ b/src/couchdb/couch_httpd_external.erl
@@ -57,7 +57,8 @@ process_external_req(HttpReq, Db, Name) ->
json_req_obj(#httpd{mochi_req=Req,
method=Verb,
path_parts=Path,
- req_body=ReqBody
+ req_body=ReqBody,
+ user_ctx=#user_ctx{name=UserName, roles=UserRoles}
}, Db) ->
Body = case ReqBody of
undefined -> Req:recv_body();
@@ -69,6 +70,7 @@ json_req_obj(#httpd{mochi_req=Req,
_ ->
[]
end,
+ UserCtx = {[{<<"name">>, UserName}, {<<"roles">>, UserRoles}]},
Headers = Req:get(headers),
Hlist = mochiweb_headers:to_list(Headers),
{ok, Info} = couch_db:get_db_info(Db),
@@ -80,7 +82,8 @@ json_req_obj(#httpd{mochi_req=Req,
{<<"headers">>, to_json_terms(Hlist)},
{<<"body">>, Body},
{<<"form">>, to_json_terms(ParsedForm)},
- {<<"cookie">>, to_json_terms(Req:parse_cookie())}]}.
+ {<<"cookie">>, to_json_terms(Req:parse_cookie())},
+ {<<"userCtx">>, UserCtx}]}.
to_json_terms(Data) ->
to_json_terms(Data, []).
diff --git a/src/couchdb/couch_httpd_misc_handlers.erl b/src/couchdb/couch_httpd_misc_handlers.erl
index b811f05d..eea353bd 100644
--- a/src/couchdb/couch_httpd_misc_handlers.erl
+++ b/src/couchdb/couch_httpd_misc_handlers.erl
@@ -15,7 +15,7 @@
-export([handle_welcome_req/2,handle_favicon_req/2,handle_utils_dir_req/2,
handle_all_dbs_req/1,handle_replicate_req/1,handle_restart_req/1,
handle_uuids_req/1,handle_config_req/1,handle_log_req/1,
- handle_task_status_req/1,handle_sleep_req/1,handle_whoami_req/1]).
+ handle_task_status_req/1,handle_sleep_req/1]).
-export([increment_update_seq_req/2]).
@@ -88,11 +88,13 @@ fix_db_url(UrlBin) ->
get_rep_endpoint(_Req, {Props}) ->
Url = proplists:get_value(<<"url">>, Props),
{BinHeaders} = proplists:get_value(<<"headers">>, Props, {[]}),
- {remote, fix_db_url(Url), [{?b2l(K),?b2l(V)} || {K,V} <- BinHeaders]};
+ Auth = proplists:get_value(<<"auth">>, Props, undefined),
+ ?LOG_DEBUG("AUTH ~p", [Auth]),
+ {remote, fix_db_url(Url), [{?b2l(K),?b2l(V)} || {K,V} <- BinHeaders], Auth};
get_rep_endpoint(_Req, <<"http://",_/binary>>=Url) ->
- {remote, fix_db_url(Url), []};
+ {remote, fix_db_url(Url), [], []};
get_rep_endpoint(_Req, <<"https://",_/binary>>=Url) ->
- {remote, fix_db_url(Url), []};
+ {remote, fix_db_url(Url), [], []};
get_rep_endpoint(#httpd{user_ctx=UserCtx}, <<DbName/binary>>) ->
{local, DbName, UserCtx}.
@@ -218,20 +220,3 @@ handle_log_req(Req) ->
send_method_not_allowed(Req, "GET").
-% whoami handler
-handle_whoami_req(#httpd{method='GET', user_ctx=UserCtx}=Req) ->
- Name = UserCtx#user_ctx.name,
- Roles = UserCtx#user_ctx.roles,
- ForceLogin = couch_httpd:qs_value(Req, "force_login", "false"),
- case {Name, ForceLogin} of
- {null, "true"} ->
- throw({unauthorized, <<"Please login.">>});
- _False -> ok
- end,
- send_json(Req, {[
- {ok, true},
- {name, Name},
- {roles, Roles}
- ]});
-handle_whoami_req(Req) ->
- send_method_not_allowed(Req, "GET").
diff --git a/src/couchdb/couch_httpd_oauth.erl b/src/couchdb/couch_httpd_oauth.erl
new file mode 100644
index 00000000..f542a989
--- /dev/null
+++ b/src/couchdb/couch_httpd_oauth.erl
@@ -0,0 +1,173 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(couch_httpd_oauth).
+-include("couch_db.hrl").
+
+-export([oauth_authentication_handler/1, handle_oauth_req/1, consumer_lookup/2]).
+
+% OAuth auth handler using per-node user db
+oauth_authentication_handler(#httpd{method=Method}=Req) ->
+ serve_oauth(Req, fun(URL, Params, Consumer, Signature) ->
+ AccessToken = proplists:get_value("oauth_token", Params),
+ TokenSecret = couch_config:get("oauth_token_secrets", AccessToken),
+ case oauth:verify(Signature, atom_to_list(Method), URL, Params, Consumer, TokenSecret) of
+ true ->
+ set_user_ctx(Req, AccessToken);
+ false ->
+ Req
+ end
+ end, true).
+
+% Look up the consumer key and get the roles to give the consumer
+set_user_ctx(Req, AccessToken) ->
+ DbName = couch_config:get("couch_httpd_auth", "authentication_db"),
+ couch_httpd_auth:ensure_users_db_exists(?l2b(DbName)),
+ case couch_db:open(?l2b(DbName), [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]) of
+ {ok, Db} ->
+ Name = ?l2b(couch_config:get("oauth_token_users", AccessToken)),
+ case couch_httpd_auth:get_user(Db, Name) of
+ nil -> Req;
+ User ->
+ Roles = proplists:get_value(<<"roles">>, User, []),
+ Req#httpd{user_ctx=#user_ctx{name=Name, roles=Roles}}
+ end;
+ _Else->
+ Req
+ end.
+
+% OAuth request_token
+handle_oauth_req(#httpd{path_parts=[_OAuth, <<"request_token">>], method=Method}=Req) ->
+ serve_oauth(Req, fun(URL, Params, Consumer, Signature) ->
+ AccessToken = proplists:get_value("oauth_token", Params),
+ TokenSecret = couch_config:get("oauth_token_secrets", AccessToken),
+ case oauth:verify(Signature, atom_to_list(Method), URL, Params, Consumer, TokenSecret) of
+ true ->
+ ok(Req, <<"oauth_token=requestkey&oauth_token_secret=requestsecret">>);
+ false ->
+ invalid_signature(Req)
+ end
+ end, false);
+handle_oauth_req(#httpd{path_parts=[_OAuth, <<"authorize">>]}=Req) ->
+ {ok, serve_oauth_authorize(Req)};
+handle_oauth_req(#httpd{path_parts=[_OAuth, <<"access_token">>], method='GET'}=Req) ->
+ serve_oauth(Req, fun(URL, Params, Consumer, Signature) ->
+ case oauth:token(Params) of
+ "requestkey" ->
+ case oauth:verify(Signature, "GET", URL, Params, Consumer, "requestsecret") of
+ true ->
+ ok(Req, <<"oauth_token=accesskey&oauth_token_secret=accesssecret">>);
+ false ->
+ invalid_signature(Req)
+ end;
+ _ ->
+ couch_httpd:send_error(Req, 400, <<"invalid_token">>, <<"Invalid OAuth token.">>)
+ end
+ end, false);
+handle_oauth_req(#httpd{path_parts=[_OAuth, <<"access_token">>]}=Req) ->
+ couch_httpd:send_method_not_allowed(Req, "GET").
+
+invalid_signature(Req) ->
+ couch_httpd:send_error(Req, 400, <<"invalid_signature">>, <<"Invalid signature value.">>).
+
+% This needs to be protected i.e. force user to login using HTTP Basic Auth or form-based login.
+serve_oauth_authorize(#httpd{method=Method}=Req) ->
+ case Method of
+ 'GET' ->
+ % Confirm with the User that they want to authenticate the Consumer
+ serve_oauth(Req, fun(URL, Params, Consumer, Signature) ->
+ AccessToken = proplists:get_value("oauth_token", Params),
+ TokenSecret = couch_config:get("oauth_token_secrets", AccessToken),
+ case oauth:verify(Signature, "GET", URL, Params, Consumer, TokenSecret) of
+ true ->
+ ok(Req, <<"oauth_token=requestkey&oauth_token_secret=requestsecret">>);
+ false ->
+ invalid_signature(Req)
+ end
+ end, false);
+ 'POST' ->
+ % If the User has confirmed, we direct the User back to the Consumer with a verification code
+ serve_oauth(Req, fun(URL, Params, Consumer, Signature) ->
+ AccessToken = proplists:get_value("oauth_token", Params),
+ TokenSecret = couch_config:get("oauth_token_secrets", AccessToken),
+ case oauth:verify(Signature, "POST", URL, Params, Consumer, TokenSecret) of
+ true ->
+ %redirect(oauth_callback, oauth_token, oauth_verifier),
+ ok(Req, <<"oauth_token=requestkey&oauth_token_secret=requestsecret">>);
+ false ->
+ invalid_signature(Req)
+ end
+ end, false);
+ _ ->
+ couch_httpd:send_method_not_allowed(Req, "GET,POST")
+ end.
+
+serve_oauth(#httpd{mochi_req=MochiReq}=Req, Fun, FailSilently) ->
+ % 1. In the HTTP Authorization header as defined in OAuth HTTP Authorization Scheme.
+ % 2. As the HTTP POST request body with a content-type of application/x-www-form-urlencoded.
+ % 3. Added to the URLs in the query part (as defined by [RFC3986] section 3).
+ AuthHeader = case MochiReq:get_header_value("authorization") of
+ undefined ->
+ "";
+ Else ->
+ [Head | Tail] = re:split(Else, "\\s", [{parts, 2}, {return, list}]),
+ case [string:to_lower(Head) | Tail] of
+ ["oauth", Rest] -> Rest;
+ _ -> ""
+ end
+ end,
+ HeaderParams = oauth_uri:params_from_header_string(AuthHeader),
+ %Realm = proplists:get_value("realm", HeaderParams),
+ Params = proplists:delete("realm", HeaderParams) ++ MochiReq:parse_qs(),
+ ?LOG_DEBUG("OAuth Params: ~p", [Params]),
+ case proplists:get_value("oauth_version", Params, "1.0") of
+ "1.0" ->
+ case proplists:get_value("oauth_consumer_key", Params, undefined) of
+ undefined ->
+ case FailSilently of
+ true -> Req;
+ false -> couch_httpd:send_error(Req, 400, <<"invalid_consumer">>, <<"Invalid consumer.">>)
+ end;
+ ConsumerKey ->
+ SigMethod = proplists:get_value("oauth_signature_method", Params),
+ case consumer_lookup(ConsumerKey, SigMethod) of
+ none ->
+ couch_httpd:send_error(Req, 400, <<"invalid_consumer">>, <<"Invalid consumer (key or signature method).">>);
+ Consumer ->
+ Signature = proplists:get_value("oauth_signature", Params),
+ URL = couch_httpd:absolute_uri(Req, MochiReq:get(path)),
+ Fun(URL, proplists:delete("oauth_signature", Params),
+ Consumer, Signature)
+ end
+ end;
+ _ ->
+ couch_httpd:send_error(Req, 400, <<"invalid_oauth_version">>, <<"Invalid OAuth version.">>)
+ end.
+
+consumer_lookup(Key, MethodStr) ->
+ SignatureMethod = case MethodStr of
+ "PLAINTEXT" -> plaintext;
+ "HMAC-SHA1" -> hmac_sha1;
+ %"RSA-SHA1" -> rsa_sha1;
+ _Else -> undefined
+ end,
+ case SignatureMethod of
+ undefined -> none;
+ _SupportedMethod ->
+ case couch_config:get("oauth_consumer_secrets", Key, undefined) of
+ undefined -> none;
+ Secret -> {Key, Secret, SignatureMethod}
+ end
+ end.
+
+ok(#httpd{mochi_req=MochiReq}, Body) ->
+ {ok, MochiReq:respond({200, [], Body})}.
diff --git a/src/couchdb/couch_httpd_show.erl b/src/couchdb/couch_httpd_show.erl
index 40f0dc11..1428e612 100644
--- a/src/couchdb/couch_httpd_show.erl
+++ b/src/couchdb/couch_httpd_show.erl
@@ -30,7 +30,6 @@ handle_doc_show_req(#httpd{
handle_doc_show(Req, DesignName, ShowName, DocId, Db);
handle_doc_show_req(#httpd{
- method='GET',
path_parts=[_DbName, _Design, DesignName, _Show, ShowName]
}=Req, Db) ->
handle_doc_show(Req, DesignName, ShowName, nil, Db);
@@ -39,7 +38,7 @@ handle_doc_show_req(#httpd{method='GET'}=Req, _Db) ->
send_error(Req, 404, <<"show_error">>, <<"Invalid path.">>);
handle_doc_show_req(Req, _Db) ->
- send_method_not_allowed(Req, "GET,HEAD").
+ send_method_not_allowed(Req, "GET,POST,HEAD").
handle_doc_show(Req, DesignName, ShowName, DocId, Db) ->
DesignId = <<"_design/", DesignName/binary>>,
@@ -112,7 +111,7 @@ send_view_list_response(Lang, ListSrc, ViewName, DesignId, Req, Db, Keys) ->
end.
-output_map_list(#httpd{mochi_req=MReq}=Req, Lang, ListSrc, View, Group, Db, QueryArgs, nil) ->
+output_map_list(#httpd{mochi_req=MReq, user_ctx=UserCtx}=Req, Lang, ListSrc, View, Group, Db, QueryArgs, nil) ->
#view_query_args{
limit = Limit,
direction = Dir,
@@ -125,7 +124,7 @@ output_map_list(#httpd{mochi_req=MReq}=Req, Lang, ListSrc, View, Group, Db, Quer
Headers = MReq:get(headers),
Hlist = mochiweb_headers:to_list(Headers),
Accept = proplists:get_value('Accept', Hlist),
- CurrentEtag = couch_httpd_view:view_group_etag(Group, {Lang, ListSrc, Accept}),
+ CurrentEtag = couch_httpd_view:view_group_etag(Group, {Lang, ListSrc, Accept, UserCtx}),
couch_httpd:etag_respond(Req, CurrentEtag, fun() ->
% get the os process here
% pass it into the view fold with closures
@@ -145,7 +144,7 @@ output_map_list(#httpd{mochi_req=MReq}=Req, Lang, ListSrc, View, Group, Db, Quer
finish_list(Req, QueryServer, CurrentEtag, FoldResult, StartListRespFun, RowCount)
end);
-output_map_list(#httpd{mochi_req=MReq}=Req, Lang, ListSrc, View, Group, Db, QueryArgs, Keys) ->
+output_map_list(#httpd{mochi_req=MReq, user_ctx=UserCtx}=Req, Lang, ListSrc, View, Group, Db, QueryArgs, Keys) ->
#view_query_args{
limit = Limit,
direction = Dir,
@@ -156,7 +155,7 @@ output_map_list(#httpd{mochi_req=MReq}=Req, Lang, ListSrc, View, Group, Db, Quer
Headers = MReq:get(headers),
Hlist = mochiweb_headers:to_list(Headers),
Accept = proplists:get_value('Accept', Hlist),
- CurrentEtag = couch_httpd_view:view_group_etag(Group, {Lang, ListSrc, Accept}),
+ CurrentEtag = couch_httpd_view:view_group_etag(Group, {Lang, ListSrc, Accept, UserCtx}),
couch_httpd:etag_respond(Req, CurrentEtag, fun() ->
% get the os process here
% pass it into the view fold with closures
@@ -239,7 +238,7 @@ send_non_empty_chunk(Resp, Chunk) ->
_ -> send_chunk(Resp, Chunk)
end.
-output_reduce_list(#httpd{mochi_req=MReq}=Req, Lang, ListSrc, View, Group, Db, QueryArgs, nil) ->
+output_reduce_list(#httpd{mochi_req=MReq, user_ctx=UserCtx}=Req, Lang, ListSrc, View, Group, Db, QueryArgs, nil) ->
#view_query_args{
limit = Limit,
direction = Dir,
@@ -256,7 +255,7 @@ output_reduce_list(#httpd{mochi_req=MReq}=Req, Lang, ListSrc, View, Group, Db, Q
Headers = MReq:get(headers),
Hlist = mochiweb_headers:to_list(Headers),
Accept = proplists:get_value('Accept', Hlist),
- CurrentEtag = couch_httpd_view:view_group_etag(Group, {Lang, ListSrc, Accept}),
+ CurrentEtag = couch_httpd_view:view_group_etag(Group, {Lang, ListSrc, Accept, UserCtx}),
couch_httpd:etag_respond(Req, CurrentEtag, fun() ->
StartListRespFun = make_reduce_start_resp_fun(QueryServer, Req, Db, CurrentEtag),
SendListRowFun = make_reduce_send_row_fun(QueryServer, Db),
@@ -274,7 +273,7 @@ output_reduce_list(#httpd{mochi_req=MReq}=Req, Lang, ListSrc, View, Group, Db, Q
finish_list(Req, QueryServer, CurrentEtag, FoldResult, StartListRespFun, null)
end);
-output_reduce_list(#httpd{mochi_req=MReq}=Req, Lang, ListSrc, View, Group, Db, QueryArgs, Keys) ->
+output_reduce_list(#httpd{mochi_req=MReq, user_ctx=UserCtx}=Req, Lang, ListSrc, View, Group, Db, QueryArgs, Keys) ->
#view_query_args{
limit = Limit,
direction = Dir,
@@ -289,7 +288,7 @@ output_reduce_list(#httpd{mochi_req=MReq}=Req, Lang, ListSrc, View, Group, Db, Q
Headers = MReq:get(headers),
Hlist = mochiweb_headers:to_list(Headers),
Accept = proplists:get_value('Accept', Hlist),
- CurrentEtag = couch_httpd_view:view_group_etag(Group, {Lang, ListSrc, Accept, Keys}),
+ CurrentEtag = couch_httpd_view:view_group_etag(Group, {Lang, ListSrc, Accept, UserCtx, Keys}),
couch_httpd:etag_respond(Req, CurrentEtag, fun() ->
StartListRespFun = make_reduce_start_resp_fun(QueryServer, Req, Db, CurrentEtag),
@@ -332,17 +331,18 @@ finish_list(Req, QueryServer, Etag, FoldResult, StartFun, TotalRows) ->
end,
send_chunk(Resp, []).
+
render_head_for_empty_list(StartListRespFun, Req, Etag, null) ->
StartListRespFun(Req, Etag, []); % for reduce
render_head_for_empty_list(StartListRespFun, Req, Etag, TotalRows) ->
StartListRespFun(Req, Etag, TotalRows, null, []).
-send_doc_show_response(Lang, ShowSrc, DocId, nil, #httpd{mochi_req=MReq}=Req, Db) ->
+send_doc_show_response(Lang, ShowSrc, DocId, nil, #httpd{mochi_req=MReq, user_ctx=UserCtx}=Req, Db) ->
% compute etag with no doc
Headers = MReq:get(headers),
Hlist = mochiweb_headers:to_list(Headers),
Accept = proplists:get_value('Accept', Hlist),
- CurrentEtag = couch_httpd:make_etag({Lang, ShowSrc, nil, Accept}),
+ CurrentEtag = couch_httpd:make_etag({Lang, ShowSrc, nil, Accept, UserCtx}),
couch_httpd:etag_respond(Req, CurrentEtag, fun() ->
[<<"resp">>, ExternalResp] = couch_query_servers:render_doc_show(Lang, ShowSrc,
DocId, nil, Req, Db),
@@ -350,12 +350,12 @@ send_doc_show_response(Lang, ShowSrc, DocId, nil, #httpd{mochi_req=MReq}=Req, Db
couch_httpd_external:send_external_response(Req, JsonResp)
end);
-send_doc_show_response(Lang, ShowSrc, DocId, #doc{revs=Revs}=Doc, #httpd{mochi_req=MReq}=Req, Db) ->
+send_doc_show_response(Lang, ShowSrc, DocId, #doc{revs=Revs}=Doc, #httpd{mochi_req=MReq, user_ctx=UserCtx}=Req, Db) ->
% calculate the etag
Headers = MReq:get(headers),
Hlist = mochiweb_headers:to_list(Headers),
Accept = proplists:get_value('Accept', Hlist),
- CurrentEtag = couch_httpd:make_etag({Lang, ShowSrc, Revs, Accept}),
+ CurrentEtag = couch_httpd:make_etag({Lang, ShowSrc, Revs, Accept, UserCtx}),
% We know our etag now
couch_httpd:etag_respond(Req, CurrentEtag, fun() ->
[<<"resp">>, ExternalResp] = couch_query_servers:render_doc_show(Lang, ShowSrc,
diff --git a/src/couchdb/couch_httpd_view.erl b/src/couchdb/couch_httpd_view.erl
index dc1b853d..2aa7d626 100644
--- a/src/couchdb/couch_httpd_view.erl
+++ b/src/couchdb/couch_httpd_view.erl
@@ -18,7 +18,7 @@
-export([get_stale_type/1, get_reduce_type/1, parse_view_params/3]).
-export([make_view_fold_fun/6, finish_view_fold/3, view_row_obj/3]).
-export([view_group_etag/1, view_group_etag/2, make_reduce_fold_funs/5]).
--export([design_doc_view/5]).
+-export([design_doc_view/5, parse_bool_param/1]).
-import(couch_httpd,
[send_json/2,send_json/3,send_json/4,send_method_not_allowed/2,send_chunk/2,
diff --git a/src/couchdb/couch_rep.erl b/src/couchdb/couch_rep.erl
index 71aa0693..b63a2610 100644
--- a/src/couchdb/couch_rep.erl
+++ b/src/couchdb/couch_rep.erl
@@ -84,7 +84,8 @@ replicate(Source, Target) ->
-record(http_db, {
uri,
- headers
+ headers,
+ oauth
}).
@@ -496,8 +497,8 @@ make_att_stub_receiver(Url, Headers, Name, Type, Length, Retries, Pause) ->
end.
-open_db({remote, Url, Headers})->
- {ok, #http_db{uri=?b2l(Url), headers=Headers}, Url};
+open_db({remote, Url, Headers, Auth})->
+ {ok, #http_db{uri=?b2l(Url), headers=Headers, oauth=Auth}, Url};
open_db({local, DbName, UserCtx})->
case couch_db:open(DbName, [{user_ctx, UserCtx}]) of
{ok, Db} -> {ok, Db, DbName};
@@ -600,19 +601,38 @@ do_checkpoint(Source, Target, Context, NewSeqNum, Stats) ->
end.
-do_http_request(Url, Action, Headers) ->
- do_http_request(Url, Action, Headers, []).
-
-do_http_request(Url, Action, Headers, JsonBody) ->
- do_http_request(Url, Action, Headers, JsonBody, 10, 1000).
+do_http_request(Url, Action, Headers, Auth) ->
+ do_http_request(Url, Action, Headers, Auth, []).
+
+do_http_request(Url, Action, Headers, Auth, JsonBody) ->
+ Headers0 = case Auth of
+ {Props} ->
+ % Add OAuth header
+ {OAuth} = proplists:get_value(<<"oauth">>, Props),
+ ConsumerKey = ?b2l(proplists:get_value(<<"consumer_key">>, OAuth)),
+ Token = ?b2l(proplists:get_value(<<"token">>, OAuth)),
+ TokenSecret = ?b2l(proplists:get_value(<<"token_secret">>, OAuth)),
+ ConsumerSecret = ?b2l(proplists:get_value(<<"consumer_secret">>, OAuth)),
+ Consumer = {ConsumerKey, ConsumerSecret, hmac_sha1},
+ Method = case Action of
+ get -> "GET";
+ post -> "POST";
+ put -> "PUT"
+ end,
+ Params = oauth:signed_params(Method, Url, [], Consumer, Token, TokenSecret),
+ [{"Authorization", "OAuth " ++ oauth_uri:params_to_header_string(Params)} | Headers];
+ _Else ->
+ Headers
+ end,
+ do_http_request0(Url, Action, Headers0, JsonBody, 10, 1000).
-do_http_request(Url, Action, Headers, Body, Retries, Pause) when is_binary(Url) ->
- do_http_request(?b2l(Url), Action, Headers, Body, Retries, Pause);
-do_http_request(Url, Action, _Headers, _JsonBody, 0, _Pause) ->
+do_http_request0(Url, Action, Headers, Body, Retries, Pause) when is_binary(Url) ->
+ do_http_request0(?b2l(Url), Action, Headers, Body, Retries, Pause);
+do_http_request0(Url, Action, _Headers, _JsonBody, 0, _Pause) ->
?LOG_ERROR("couch_rep HTTP ~p request failed after 10 retries: ~s",
[Action, Url]),
exit({http_request_failed, ?l2b(["failed to replicate ", Url])});
-do_http_request(Url, Action, Headers, JsonBody, Retries, Pause) ->
+do_http_request0(Url, Action, Headers, JsonBody, Retries, Pause) ->
?LOG_DEBUG("couch_rep HTTP ~p request: ~s", [Action, Url]),
Body =
case JsonBody of
@@ -638,7 +658,7 @@ do_http_request(Url, Action, Headers, JsonBody, Retries, Pause) ->
ResponseCode >= 300, ResponseCode < 400 ->
RedirectUrl = mochiweb_headers:get_value("Location",
mochiweb_headers:make(ResponseHeaders)),
- do_http_request(RedirectUrl, Action, Headers, JsonBody, Retries-1,
+ do_http_request0(RedirectUrl, Action, Headers, JsonBody, Retries-1,
Pause);
ResponseCode >= 400, ResponseCode < 500 ->
?JSON_DECODE(ResponseBody);
@@ -646,18 +666,18 @@ do_http_request(Url, Action, Headers, JsonBody, Retries, Pause) ->
?LOG_INFO("retrying couch_rep HTTP ~p request in ~p seconds " ++
"due to 500 error: ~s", [Action, Pause/1000, Url]),
timer:sleep(Pause),
- do_http_request(Url, Action, Headers, JsonBody, Retries - 1, 2*Pause)
+ do_http_request0(Url, Action, Headers, JsonBody, Retries - 1, 2*Pause)
end;
{error, Reason} ->
?LOG_INFO("retrying couch_rep HTTP ~p request in ~p seconds due to " ++
"{error, ~p}: ~s", [Action, Pause/1000, Reason, Url]),
timer:sleep(Pause),
- do_http_request(Url, Action, Headers, JsonBody, Retries - 1, 2*Pause)
+ do_http_request0(Url, Action, Headers, JsonBody, Retries - 1, 2*Pause)
end.
-ensure_full_commit(#http_db{uri=DbUrl, headers=Headers}) ->
+ensure_full_commit(#http_db{uri=DbUrl, headers=Headers, oauth=OAuth}) ->
{ResultProps} = do_http_request(DbUrl ++ "_ensure_full_commit", post,
- Headers, true),
+ Headers, OAuth, true),
true = proplists:get_value(<<"ok">>, ResultProps),
{ok, proplists:get_value(<<"instance_start_time">>, ResultProps)};
ensure_full_commit(Db) ->
@@ -687,16 +707,16 @@ enum_docs_since(Pid, DbSource, DbTarget, {StartSeq, RevsCount}) ->
-get_db_info(#http_db{uri=DbUrl, headers=Headers}) ->
- {DbProps} = do_http_request(DbUrl, get, Headers),
+get_db_info(#http_db{uri=DbUrl, headers=Headers, oauth=OAuth}) ->
+ {DbProps} = do_http_request(DbUrl, get, Headers, OAuth),
{ok, [{list_to_atom(?b2l(K)), V} || {K,V} <- DbProps]};
get_db_info(Db) ->
couch_db:get_db_info(Db).
-get_doc_info_list(#http_db{uri=DbUrl, headers=Headers}, StartSeq) ->
+get_doc_info_list(#http_db{uri=DbUrl, headers=Headers, oauth=OAuth}, StartSeq) ->
Url = DbUrl ++ "_all_docs_by_seq?limit=100&startkey="
++ integer_to_list(StartSeq),
- {Results} = do_http_request(Url, get, Headers),
+ {Results} = do_http_request(Url, get, Headers, OAuth),
lists:map(fun({RowInfoList}) ->
{RowValueProps} = proplists:get_value(<<"value">>, RowInfoList),
Seq = proplists:get_value(<<"key">>, RowInfoList),
@@ -719,9 +739,9 @@ get_doc_info_list(DbSource, StartSeq) ->
end, {0, []}),
lists:reverse(DocInfoList).
-get_missing_revs(#http_db{uri=DbUrl, headers=Headers}, DocIdRevsList) ->
+get_missing_revs(#http_db{uri=DbUrl, headers=Headers, oauth=OAuth}, DocIdRevsList) ->
DocIdRevsList2 = [{Id, couch_doc:rev_to_strs(Revs)} || {Id, Revs} <- DocIdRevsList],
- {ResponseMembers} = do_http_request(DbUrl ++ "_missing_revs", post, Headers,
+ {ResponseMembers} = do_http_request(DbUrl ++ "_missing_revs", post, Headers, OAuth,
{DocIdRevsList2}),
{DocMissingRevsList} = proplists:get_value(<<"missing_revs">>, ResponseMembers),
DocMissingRevsList2 = [{Id, couch_doc:parse_revs(MissingRevStrs)} || {Id, MissingRevStrs} <- DocMissingRevsList],
@@ -730,9 +750,9 @@ get_missing_revs(Db, DocId) ->
couch_db:get_missing_revs(Db, DocId).
-open_doc(#http_db{uri=DbUrl, headers=Headers}, DocId, Options) ->
+open_doc(#http_db{uri=DbUrl, headers=Headers, oauth=OAuth}, DocId, Options) ->
[] = Options,
- case do_http_request(DbUrl ++ couch_util:url_encode(DocId), get, Headers) of
+ case do_http_request(DbUrl ++ couch_util:url_encode(DocId), get, Headers, OAuth) of
{[{<<"error">>, ErrId}, {<<"reason">>, Reason}]} ->
{couch_util:to_existing_atom(ErrId), Reason};
Doc ->
@@ -741,7 +761,7 @@ open_doc(#http_db{uri=DbUrl, headers=Headers}, DocId, Options) ->
open_doc(Db, DocId, Options) ->
couch_db:open_doc(Db, DocId, Options).
-open_doc_revs(#http_db{uri=DbUrl, headers=Headers} = DbS, DocId, Revs0,
+open_doc_revs(#http_db{uri=DbUrl, headers=Headers, oauth=OAuth} = DbS, DocId, Revs0,
[latest]) ->
Revs = couch_doc:rev_to_strs(Revs0),
BaseUrl = DbUrl ++ couch_util:url_encode(DocId) ++ "?revs=true&latest=true",
@@ -752,18 +772,18 @@ open_doc_revs(#http_db{uri=DbUrl, headers=Headers} = DbS, DocId, Revs0,
JsonResults = case length(Revs) > MaxN of
false ->
Url = ?l2b(BaseUrl ++ "&open_revs=" ++ ?JSON_ENCODE(Revs)),
- do_http_request(Url, get, Headers);
+ do_http_request(Url, get, Headers, OAuth);
true ->
{_, Rest, Acc} = lists:foldl(
fun(Rev, {Count, RevsAcc, AccResults}) when Count =:= MaxN ->
QSRevs = ?JSON_ENCODE(lists:reverse(RevsAcc)),
Url = ?l2b(BaseUrl ++ "&open_revs=" ++ QSRevs),
- {1, [Rev], AccResults++do_http_request(Url, get, Headers)};
+ {1, [Rev], AccResults++do_http_request(Url, get, Headers, OAuth)};
(Rev, {Count, RevsAcc, AccResults}) ->
{Count+1, [Rev|RevsAcc], AccResults}
end, {0, [], []}, Revs),
Acc ++ do_http_request(?l2b(BaseUrl ++ "&open_revs=" ++
- ?JSON_ENCODE(lists:reverse(Rest))), get, Headers)
+ ?JSON_ENCODE(lists:reverse(Rest))), get, Headers, OAuth)
end,
Results =
@@ -825,10 +845,10 @@ binary_memory(Pid) ->
lists:foldl(fun({_Id, Size, _NRefs}, Acc) -> Size+Acc end,
0, element(2,process_info(Pid, binary))).
-update_doc(#http_db{uri=DbUrl, headers=Headers}, #doc{id=DocId}=Doc, Options) ->
+update_doc(#http_db{uri=DbUrl, headers=Headers, oauth=OAuth}, #doc{id=DocId}=Doc, Options) ->
[] = Options,
Url = DbUrl ++ couch_util:url_encode(DocId),
- {ResponseMembers} = do_http_request(Url, put, Headers,
+ {ResponseMembers} = do_http_request(Url, put, Headers, OAuth,
couch_doc:to_json_obj(Doc, [attachments])),
Rev = proplists:get_value(<<"rev">>, ResponseMembers),
{ok, couch_doc:parse_rev(Rev)};
@@ -837,10 +857,10 @@ update_doc(Db, Doc, Options) ->
update_docs(_, [], _, _) ->
{ok, []};
-update_docs(#http_db{uri=DbUrl, headers=Headers}, Docs, [], replicated_changes) ->
+update_docs(#http_db{uri=DbUrl, headers=Headers, oauth=OAuth}, Docs, [], replicated_changes) ->
JsonDocs = [couch_doc:to_json_obj(Doc, [revs,attachments]) || Doc <- Docs],
ErrorsJson =
- do_http_request(DbUrl ++ "_bulk_docs", post, Headers,
+ do_http_request(DbUrl ++ "_bulk_docs", post, Headers, OAuth,
{[{new_edits, false}, {docs, JsonDocs}]}),
ErrorsList =
lists:map(
diff --git a/src/couchdb/couch_util.erl b/src/couchdb/couch_util.erl
index 71c6aea9..e31bd0e7 100644
--- a/src/couchdb/couch_util.erl
+++ b/src/couchdb/couch_util.erl
@@ -16,7 +16,8 @@
-export([should_flush/0, should_flush/1, to_existing_atom/1]).
-export([new_uuid/0, rand32/0, implode/2, collate/2, collate/3]).
-export([abs_pathname/1,abs_pathname/2, trim/1, ascii_lower/1]).
--export([encodeBase64/1, decodeBase64/1, to_hex/1,parse_term/1,dict_find/3]).
+-export([encodeBase64/1, decodeBase64/1, encodeBase64Url/1, decodeBase64Url/1,
+ to_hex/1,parse_term/1, dict_find/3]).
-export([file_read_size/1, get_nested_json_value/2, json_user_ctx/1]).
-export([to_binary/1, to_list/1, url_encode/1]).
@@ -259,6 +260,23 @@ encodeBase64(<<B:1/binary>>, Acc) ->
encodeBase64(<<>>, Acc) ->
Acc.
+encodeBase64Url(Bs) when list(Bs) ->
+ encodeBase64Url(list_to_binary(Bs), <<>>);
+encodeBase64Url(Bs) ->
+ encodeBase64Url(Bs, <<>>).
+
+encodeBase64Url(<<B:3/binary, Bs/binary>>, Acc) ->
+ <<C1:6, C2:6, C3:6, C4:6>> = B,
+ encodeBase64Url(Bs, <<Acc/binary, (encUrl(C1)), (encUrl(C2)), (encUrl(C3)), (encUrl(C4))>>);
+encodeBase64Url(<<B:2/binary>>, Acc) ->
+ <<C1:6, C2:6, C3:6, _:6>> = <<B/binary, 0>>,
+ <<Acc/binary, (encUrl(C1)), (encUrl(C2)), (encUrl(C3))>>;
+encodeBase64Url(<<B:1/binary>>, Acc) ->
+ <<C1:6, C2:6, _:12>> = <<B/binary, 0, 0>>,
+ <<Acc/binary, (encUrl(C1)), (encUrl(C2))>>;
+encodeBase64Url(<<>>, Acc) ->
+ Acc.
+
%%
%% decodeBase64(BinaryChars) -> Binary
%%
@@ -279,6 +297,23 @@ decode1(<<C1, C2, C3, C4, Cs/binary>>, Acc) ->
decode1(<<>>, Acc) ->
Acc.
+decodeBase64Url(Cs) when is_list(Cs)->
+ decodeBase64Url(list_to_binary(Cs));
+decodeBase64Url(Cs) ->
+ decode1Url(Cs, <<>>).
+
+decode1Url(<<C1, C2>>, Acc) ->
+ <<B1, _:16>> = <<(decUrl(C1)):6, (decUrl(C2)):6, 0:12>>,
+ <<Acc/binary, B1>>;
+decode1Url(<<C1, C2, C3>>, Acc) ->
+ <<B1, B2, _:8>> = <<(decUrl(C1)):6, (decUrl(C2)):6, (decUrl(C3)):6, (decUrl(0)):6>>,
+ <<Acc/binary, B1, B2>>;
+decode1Url(<<C1, C2, C3, C4, Cs/binary>>, Acc) ->
+ Bin = <<Acc/binary, (decUrl(C1)):6, (decUrl(C2)):6, (decUrl(C3)):6, (decUrl(C4)):6>>,
+ decode1Url(Cs, Bin);
+decode1Url(<<>>, Acc) ->
+ Acc.
+
%% enc/1 and dec/1
%%
%% Mapping: 0-25 -> A-Z, 26-51 -> a-z, 52-61 -> 0-9, 62 -> +, 63 -> /
@@ -289,6 +324,15 @@ enc(C) ->
dec(C) ->
62*?st(C,43) + ?st(C,47) + (C-59)*?st(C,48) - 69*?st(C,65) - 6*?st(C,97).
+%% encUrl/1 and decUrl/1
+%%
+%% Mapping: 0-25 -> A-Z, 26-51 -> a-z, 52-61 -> 0-9, 62 -> -, 63 -> _
+%%
+encUrl(C) ->
+ 65 + C + 6*?st(C,26) - 75*?st(C,52) -13*?st(C,62) + 49*?st(C,63).
+
+decUrl(C) ->
+ 62*?st(C,45) + (C-58)*?st(C,48) - 69*?st(C,65) + 33*?st(C,95) - 39*?st(C,97).
dict_find(Key, Dict, DefaultValue) ->
case dict:find(Key, Dict) of
diff --git a/src/erlang-oauth/Makefile.am b/src/erlang-oauth/Makefile.am
new file mode 100644
index 00000000..e20bf2b1
--- /dev/null
+++ b/src/erlang-oauth/Makefile.am
@@ -0,0 +1,47 @@
+## 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.
+
+oauthebindir = $(localerlanglibdir)/erlang-oauth/ebin
+
+oauth_file_collection = \
+ oauth.erl \
+ oauth_hmac_sha1.erl \
+ oauth_http.erl \
+ oauth_plaintext.erl \
+ oauth_rsa_sha1.erl \
+ oauth_unix.erl \
+ oauth_uri.erl
+
+oauthebin_static_file = oauth.app
+
+oauthebin_make_generated_file_list = \
+ oauth.beam \
+ oauth_hmac_sha1.beam \
+ oauth_http.beam \
+ oauth_plaintext.beam \
+ oauth_rsa_sha1.beam \
+ oauth_unix.beam \
+ oauth_uri.beam
+
+oauthebin_DATA = \
+ $(oauthebin_static_file) \
+ $(oauthebin_make_generated_file_list)
+
+EXTRA_DIST = \
+ $(oauth_file_collection) \
+ $(oauthebin_static_file)
+
+CLEANFILES = \
+ $(oauthebin_make_generated_file_list)
+
+%.beam: %.erl
+ $(ERLC) $(ERLC_FLAGS) $<
diff --git a/src/erlang-oauth/oauth.app b/src/erlang-oauth/oauth.app
new file mode 100644
index 00000000..6357b9b0
--- /dev/null
+++ b/src/erlang-oauth/oauth.app
@@ -0,0 +1,20 @@
+{application, oauth, [
+ {description, "Erlang OAuth implementation"},
+ {vsn, "dev"},
+ {modules, [
+ oauth,
+ oauth_hmac_sha1,
+ oauth_http,
+ oauth_plaintext,
+ oauth_rsa_sha1,
+ oauth_unix,
+ oauth_uri
+ ]},
+ {registered, []},
+ {applications, [
+ kernel,
+ stdlib,
+ crypto,
+ inets
+ ]}
+]}.
diff --git a/src/erlang-oauth/oauth.erl b/src/erlang-oauth/oauth.erl
new file mode 100644
index 00000000..866655c9
--- /dev/null
+++ b/src/erlang-oauth/oauth.erl
@@ -0,0 +1,107 @@
+-module(oauth).
+
+-export(
+ [ get/5
+ , header/1
+ , post/5
+ , signature/5
+ , signature_base_string/3
+ , signed_params/6
+ , token/1
+ , token_secret/1
+ , uri/2
+ , verify/6
+ ]).
+
+
+get(URL, ExtraParams, Consumer, Token, TokenSecret) ->
+ SignedParams = signed_params("GET", URL, ExtraParams, Consumer, Token, TokenSecret),
+ oauth_http:get(uri(URL, SignedParams)).
+
+post(URL, ExtraParams, Consumer, Token, TokenSecret) ->
+ SignedParams = signed_params("POST", URL, ExtraParams, Consumer, Token, TokenSecret),
+ oauth_http:post(URL, oauth_uri:params_to_string(SignedParams)).
+
+uri(Base, []) ->
+ Base;
+uri(Base, Params) ->
+ lists:concat([Base, "?", oauth_uri:params_to_string(Params)]).
+
+header(Params) ->
+ {"Authorization", "OAuth " ++ oauth_uri:params_to_header_string(Params)}.
+
+token(Params) ->
+ proplists:get_value("oauth_token", Params).
+
+token_secret(Params) ->
+ proplists:get_value("oauth_token_secret", Params).
+
+verify(Signature, HttpMethod, URL, Params, Consumer, TokenSecret) ->
+ case signature_method(Consumer) of
+ plaintext ->
+ oauth_plaintext:verify(Signature, consumer_secret(Consumer), TokenSecret);
+ hmac_sha1 ->
+ BaseString = signature_base_string(HttpMethod, URL, Params),
+ oauth_hmac_sha1:verify(Signature, BaseString, consumer_secret(Consumer), TokenSecret);
+ rsa_sha1 ->
+ BaseString = signature_base_string(HttpMethod, URL, Params),
+ oauth_rsa_sha1:verify(Signature, BaseString, consumer_secret(Consumer))
+ end.
+
+signed_params(HttpMethod, URL, ExtraParams, Consumer, Token, TokenSecret) ->
+ Params = token_param(Token, params(Consumer, ExtraParams)),
+ [{"oauth_signature", signature(HttpMethod, URL, Params, Consumer, TokenSecret)}|Params].
+
+signature(HttpMethod, URL, Params, Consumer, TokenSecret) ->
+ case signature_method(Consumer) of
+ plaintext ->
+ oauth_plaintext:signature(consumer_secret(Consumer), TokenSecret);
+ hmac_sha1 ->
+ BaseString = signature_base_string(HttpMethod, URL, Params),
+ oauth_hmac_sha1:signature(BaseString, consumer_secret(Consumer), TokenSecret);
+ rsa_sha1 ->
+ BaseString = signature_base_string(HttpMethod, URL, Params),
+ oauth_rsa_sha1:signature(BaseString, consumer_secret(Consumer))
+ end.
+
+signature_base_string(HttpMethod, URL, Params) ->
+ NormalizedURL = oauth_uri:normalize(URL),
+ NormalizedParams = oauth_uri:params_to_string(lists:sort(Params)),
+ oauth_uri:calate("&", [HttpMethod, NormalizedURL, NormalizedParams]).
+
+token_param("", Params) ->
+ Params;
+token_param(Token, Params) ->
+ [{"oauth_token", Token}|Params].
+
+params(Consumer, Params) ->
+ Nonce = base64:encode_to_string(crypto:rand_bytes(32)), % cf. ruby-oauth
+ params(Consumer, oauth_unix:timestamp(), Nonce, Params).
+
+params(Consumer, Timestamp, Nonce, Params) ->
+ [ {"oauth_version", "1.0"}
+ , {"oauth_nonce", Nonce}
+ , {"oauth_timestamp", integer_to_list(Timestamp)}
+ , {"oauth_signature_method", signature_method_string(Consumer)}
+ , {"oauth_consumer_key", consumer_key(Consumer)}
+ | Params
+ ].
+
+signature_method_string(Consumer) ->
+ case signature_method(Consumer) of
+ plaintext ->
+ "PLAINTEXT";
+ hmac_sha1 ->
+ "HMAC-SHA1";
+ rsa_sha1 ->
+ "RSA-SHA1"
+ end.
+
+signature_method(_Consumer={_, _, Method}) ->
+ Method.
+
+consumer_secret(_Consumer={_, Secret, _}) ->
+ Secret.
+
+consumer_key(_Consumer={Key, _, _}) ->
+ Key.
diff --git a/src/erlang-oauth/oauth_hmac_sha1.erl b/src/erlang-oauth/oauth_hmac_sha1.erl
new file mode 100644
index 00000000..69064edd
--- /dev/null
+++ b/src/erlang-oauth/oauth_hmac_sha1.erl
@@ -0,0 +1,11 @@
+-module(oauth_hmac_sha1).
+
+-export([signature/3, verify/4]).
+
+
+signature(BaseString, CS, TS) ->
+ Key = oauth_uri:calate("&", [CS, TS]),
+ base64:encode_to_string(crypto:sha_mac(Key, BaseString)).
+
+verify(Signature, BaseString, CS, TS) ->
+ Signature =:= signature(BaseString, CS, TS).
diff --git a/src/erlang-oauth/oauth_http.erl b/src/erlang-oauth/oauth_http.erl
new file mode 100644
index 00000000..bf5a4bac
--- /dev/null
+++ b/src/erlang-oauth/oauth_http.erl
@@ -0,0 +1,22 @@
+-module(oauth_http).
+
+-export([get/1, post/2, response_params/1, response_body/1, response_code/1]).
+
+
+get(URL) ->
+ request(get, {URL, []}).
+
+post(URL, Data) ->
+ request(post, {URL, [], "application/x-www-form-urlencoded", Data}).
+
+request(Method, Request) ->
+ http:request(Method, Request, [{autoredirect, false}], []).
+
+response_params(Response) ->
+ oauth_uri:params_from_string(response_body(Response)).
+
+response_body({{_, _, _}, _, Body}) ->
+ Body.
+
+response_code({{_, Code, _}, _, _}) ->
+ Code.
diff --git a/src/erlang-oauth/oauth_plaintext.erl b/src/erlang-oauth/oauth_plaintext.erl
new file mode 100644
index 00000000..d8085e02
--- /dev/null
+++ b/src/erlang-oauth/oauth_plaintext.erl
@@ -0,0 +1,10 @@
+-module(oauth_plaintext).
+
+-export([signature/2, verify/3]).
+
+
+signature(CS, TS) ->
+ oauth_uri:calate("&", [CS, TS]).
+
+verify(Signature, CS, TS) ->
+ Signature =:= signature(CS, TS).
diff --git a/src/erlang-oauth/oauth_rsa_sha1.erl b/src/erlang-oauth/oauth_rsa_sha1.erl
new file mode 100644
index 00000000..6f4828e0
--- /dev/null
+++ b/src/erlang-oauth/oauth_rsa_sha1.erl
@@ -0,0 +1,30 @@
+-module(oauth_rsa_sha1).
+
+-export([signature/2, verify/3]).
+
+-include_lib("public_key/include/public_key.hrl").
+
+
+signature(BaseString, PrivateKeyPath) ->
+ {ok, [Info]} = public_key:pem_to_der(PrivateKeyPath),
+ {ok, PrivateKey} = public_key:decode_private_key(Info),
+ base64:encode_to_string(public_key:sign(list_to_binary(BaseString), PrivateKey)).
+
+verify(Signature, BaseString, PublicKey) ->
+ public_key:verify_signature(to_binary(BaseString), sha, base64:decode(Signature), public_key(PublicKey)).
+
+to_binary(Term) when is_list(Term) ->
+ list_to_binary(Term);
+to_binary(Term) when is_binary(Term) ->
+ Term.
+
+public_key(Path) when is_list(Path) ->
+ {ok, [{cert, DerCert, not_encrypted}]} = public_key:pem_to_der(Path),
+ {ok, Cert} = public_key:pkix_decode_cert(DerCert, otp),
+ public_key(Cert);
+public_key(#'OTPCertificate'{tbsCertificate=Cert}) ->
+ public_key(Cert);
+public_key(#'OTPTBSCertificate'{subjectPublicKeyInfo=Info}) ->
+ public_key(Info);
+public_key(#'OTPSubjectPublicKeyInfo'{subjectPublicKey=Key}) ->
+ Key.
diff --git a/src/erlang-oauth/oauth_unix.erl b/src/erlang-oauth/oauth_unix.erl
new file mode 100644
index 00000000..73ca3143
--- /dev/null
+++ b/src/erlang-oauth/oauth_unix.erl
@@ -0,0 +1,16 @@
+-module(oauth_unix).
+
+-export([timestamp/0]).
+
+
+timestamp() ->
+ timestamp(calendar:universal_time()).
+
+timestamp(DateTime) ->
+ seconds(DateTime) - epoch().
+
+epoch() ->
+ seconds({{1970,1,1},{00,00,00}}).
+
+seconds(DateTime) ->
+ calendar:datetime_to_gregorian_seconds(DateTime).
diff --git a/src/erlang-oauth/oauth_uri.erl b/src/erlang-oauth/oauth_uri.erl
new file mode 100644
index 00000000..fb27ae72
--- /dev/null
+++ b/src/erlang-oauth/oauth_uri.erl
@@ -0,0 +1,88 @@
+-module(oauth_uri).
+
+-export([normalize/1, calate/2, encode/1]).
+-export([params_from_string/1, params_to_string/1,
+ params_from_header_string/1, params_to_header_string/1]).
+
+-import(lists, [concat/1]).
+
+-define(is_uppercase_alpha(C), C >= $A, C =< $Z).
+-define(is_lowercase_alpha(C), C >= $a, C =< $z).
+-define(is_alpha(C), ?is_uppercase_alpha(C); ?is_lowercase_alpha(C)).
+-define(is_digit(C), C >= $0, C =< $9).
+-define(is_alphanumeric(C), ?is_alpha(C); ?is_digit(C)).
+-define(is_unreserved(C), ?is_alphanumeric(C); C =:= $-; C =:= $_; C =:= $.; C =:= $~).
+-define(is_hex(C), ?is_digit(C); C >= $A, C =< $F).
+
+
+normalize(URI) ->
+ case http_uri:parse(URI) of
+ {Scheme, UserInfo, Host, Port, Path, _Query} ->
+ normalize(Scheme, UserInfo, string:to_lower(Host), Port, [Path]);
+ Else ->
+ Else
+ end.
+
+normalize(http, UserInfo, Host, 80, Acc) ->
+ normalize(http, UserInfo, [Host|Acc]);
+normalize(https, UserInfo, Host, 443, Acc) ->
+ normalize(https, UserInfo, [Host|Acc]);
+normalize(Scheme, UserInfo, Host, Port, Acc) ->
+ normalize(Scheme, UserInfo, [Host, ":", Port|Acc]).
+
+normalize(Scheme, [], Acc) ->
+ concat([Scheme, "://"|Acc]);
+normalize(Scheme, UserInfo, Acc) ->
+ concat([Scheme, "://", UserInfo, "@"|Acc]).
+
+params_to_header_string(Params) ->
+ intercalate(", ", [concat([encode(K), "=\"", encode(V), "\""]) || {K, V} <- Params]).
+
+params_from_header_string(String) ->
+ [param_from_header_string(Param) || Param <- re:split(String, ",\\s*", [{return, list}]), Param =/= ""].
+
+param_from_header_string(Param) ->
+ [Key, QuotedValue] = string:tokens(Param, "="),
+ Value = string:substr(QuotedValue, 2, length(QuotedValue) - 2),
+ {decode(Key), decode(Value)}.
+
+params_from_string(Params) ->
+ [param_from_string(Param) || Param <- string:tokens(Params, "&")].
+
+param_from_string(Param) ->
+ list_to_tuple([decode(Value) || Value <- string:tokens(Param, "=")]).
+
+params_to_string(Params) ->
+ intercalate("&", [calate("=", [K, V]) || {K, V} <- Params]).
+
+calate(Sep, Xs) ->
+ intercalate(Sep, [encode(X) || X <- Xs]).
+
+intercalate(Sep, Xs) ->
+ concat(intersperse(Sep, Xs)).
+
+intersperse(_, []) -> [];
+intersperse(_, [X]) -> [X];
+intersperse(Sep, [X|Xs]) ->
+ [X, Sep|intersperse(Sep, Xs)].
+
+decode(Chars) ->
+ decode(Chars, []).
+
+decode([], Decoded) ->
+ lists:reverse(Decoded);
+decode([$%,A,B|Etc], Decoded) when ?is_hex(A), ?is_hex(B) ->
+ decode(Etc, [erlang:list_to_integer([A,B], 16)|Decoded]);
+decode([C|Etc], Decoded) when ?is_unreserved(C) ->
+ decode(Etc, [C|Decoded]).
+
+encode(Chars) ->
+ encode(Chars, []).
+
+encode([], Encoded) ->
+ lists:flatten(lists:reverse(Encoded));
+encode([C|Etc], Encoded) when ?is_unreserved(C) ->
+ encode(Etc, [C|Encoded]);
+encode([C|Etc], Encoded) ->
+ Value = io_lib:format("%~2.1.0s", [erlang:integer_to_list(C, 16)]),
+ encode(Etc, [Value|Encoded]).
diff --git a/src/mochiweb/mochiweb_cookies.erl b/src/mochiweb/mochiweb_cookies.erl
index b9da37b4..61711ff0 100644
--- a/src/mochiweb/mochiweb_cookies.erl
+++ b/src/mochiweb/mochiweb_cookies.erl
@@ -32,7 +32,7 @@ cookie(Key, Value) ->
%% @spec cookie(Key::string(), Value::string(), Options::[Option]) -> header()
%% where Option = {max_age, integer()} | {local_time, {date(), time()}}
%% | {domain, string()} | {path, string()}
-%% | {secure, true | false}
+%% | {secure, true | false} | {http_only, true | false}
%%
%% @doc Generate a Set-Cookie header field tuple.
cookie(Key, Value, Options) ->
@@ -83,7 +83,14 @@ cookie(Key, Value, Options) ->
Path ->
["; Path=", quote(Path)]
end,
- CookieParts = [Cookie, ExpiresPart, SecurePart, DomainPart, PathPart],
+ HttpOnlyPart =
+ case proplists:get_value(http_only, Options) of
+ true ->
+ "; HttpOnly";
+ _ ->
+ ""
+ end,
+ CookieParts = [Cookie, ExpiresPart, SecurePart, DomainPart, PathPart, HttpOnlyPart],
{"Set-Cookie", lists:flatten(CookieParts)}.
diff --git a/test/runner.sh b/test/runner.sh
index 7890a259..e1936b7e 100755
--- a/test/runner.sh
+++ b/test/runner.sh
@@ -19,5 +19,5 @@ erl -noshell -pa ../src/couchdb -pa ../src/mochiweb -eval "runner:run()"
cat ../share/www/script/couch.js \
../share/www/script/couch_test_runner.js \
../share/www/script/couch_tests.js \
- ../share/www/script/test/* test.js \
+ ../share/www/script/test/*.js test.js \
| ../src/couchdb/couchjs -
diff --git a/utils/Makefile.am b/utils/Makefile.am
index c4bc0ff2..6ac49511 100644
--- a/utils/Makefile.am
+++ b/utils/Makefile.am
@@ -25,6 +25,7 @@ run: ../bin/couchdb.tpl
-e "s|%mochiwebebindir%|mochiweb|g" \
-e "s|%couchdbebindir%|couchdb|g" \
-e "s|%ibrowseebindir%|ibrowse|g" \
+ -e "s|%oauthebindir%|erlang-oauth|g" \
-e "s|%defaultini%|default_dev.ini|g" \
-e "s|%localini%|local_dev.ini|g" \
-e "s|%localerlanglibdir%|$(abs_top_srcdir)/src|g" \