diff options
| -rw-r--r-- | share/www/script/couch.js | 2 | ||||
| -rw-r--r-- | share/www/script/test/basics.js | 9 | ||||
| -rw-r--r-- | share/www/script/test/batch_save.js | 5 | ||||
| -rw-r--r-- | share/www/script/test/stats.js | 5 | ||||
| -rw-r--r-- | src/couchdb/couch_httpd.erl | 39 | ||||
| -rw-r--r-- | src/couchdb/couch_httpd_auth.erl | 1 | ||||
| -rw-r--r-- | src/couchdb/couch_httpd_db.erl | 14 | ||||
| -rw-r--r-- | src/couchdb/couch_httpd_show.erl | 21 | ||||
| -rw-r--r-- | src/couchdb/couch_rep.erl | 10 | ||||
| -rw-r--r-- | src/couchdb/couch_rep_writer.erl | 3 | ||||
| -rw-r--r-- | src/couchdb/couch_util.erl | 14 | 
11 files changed, 85 insertions, 38 deletions
| diff --git a/share/www/script/couch.js b/share/www/script/couch.js index dbffa7ce..d745b06e 100644 --- a/share/www/script/couch.js +++ b/share/www/script/couch.js @@ -398,6 +398,8 @@ CouchDB.newXhr = function() {  CouchDB.request = function(method, uri, options) {    options = options || {}; +  options.headers = options.headers || {}; +  options.headers["Content-Type"] = options.headers["Content-Type"] || "application/json";    var req = CouchDB.newXhr();    if(uri.substr(0, "http://".length) != "http://") {      uri = CouchDB.urlPrefix + uri diff --git a/share/www/script/test/basics.js b/share/www/script/test/basics.js index 0f9ac44f..6a3ae471 100644 --- a/share/www/script/test/basics.js +++ b/share/www/script/test/basics.js @@ -152,7 +152,8 @@ couchTests.basics = function(debug) {    // test that the POST response has a Location header    var xhr = CouchDB.request("POST", "/test_suite_db", { -    body: JSON.stringify({"foo":"bar"}) +    body: JSON.stringify({"foo":"bar"}), +    headers: {"Content-Type": "application/json"}    });    var resp = JSON.parse(xhr.responseText);    T(resp.ok); @@ -164,6 +165,7 @@ couchTests.basics = function(debug) {    // test that that POST's with an _id aren't overriden with a UUID.    var xhr = CouchDB.request("POST", "/test_suite_db", { +    headers: {"Content-Type": "application/json"},      body: JSON.stringify({"_id": "oppossum", "yar": "matey"})    });    var resp = JSON.parse(xhr.responseText); @@ -202,7 +204,10 @@ couchTests.basics = function(debug) {      result = JSON.parse(xhr.responseText);      T(result.error == "doc_validation"); -    xhr = CouchDB.request("POST", "/test_suite_db/", {body: data}); +    xhr = CouchDB.request("POST", "/test_suite_db/", { +      headers: {"Content-Type": "application/json"}, +      body: data +    });      T(xhr.status == 500);      result = JSON.parse(xhr.responseText);      T(result.error == "doc_validation"); diff --git a/share/www/script/test/batch_save.js b/share/www/script/test/batch_save.js index 1c8a2be9..a1b00192 100644 --- a/share/www/script/test/batch_save.js +++ b/share/www/script/test/batch_save.js @@ -36,7 +36,10 @@ couchTests.batch_save = function(debug) {    // repeat the tests for POST    for(i=0; i < 100; i++) { -    var resp = db.request("POST", db.uri + "?batch=ok", {body: JSON.stringify({a:1})}); +    var resp = db.request("POST", db.uri + "?batch=ok", { +      headers: {"Content-Type": "application/json"}, +      body: JSON.stringify({a:1}) +    });      T(JSON.parse(resp.responseText).ok);    } diff --git a/share/www/script/test/stats.js b/share/www/script/test/stats.js index 23468a37..d2fd6eac 100644 --- a/share/www/script/test/stats.js +++ b/share/www/script/test/stats.js @@ -160,7 +160,10 @@ couchTests.stats = function(debug) {    runTest("couchdb", "database_writes", {      run: function(db) { -      CouchDB.request("POST", "/test_suite_db", {body: '{"a": "1"}'}) +      CouchDB.request("POST", "/test_suite_db", { +        headers: {"Content-Type": "application/json"}, +        body: '{"a": "1"}' +      })      },      test: function(before, after) {        TEquals(before+1, after, "POST'ing new docs increments doc writes."); diff --git a/src/couchdb/couch_httpd.erl b/src/couchdb/couch_httpd.erl index c05fca45..331546eb 100644 --- a/src/couchdb/couch_httpd.erl +++ b/src/couchdb/couch_httpd.erl @@ -25,7 +25,7 @@  -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,last_chunk/1,parse_multipart_request/3]). --export([accepted_encodings/1,handle_request_int/5]). +-export([accepted_encodings/1,handle_request_int/5,validate_referer/1,validate_ctype/2]).  start_link() ->      % read config and register for configuration changes @@ -321,6 +321,34 @@ vhost_global(VhostGlobals, MochiReq) ->      end,      [true] == [true||V <- VhostGlobals, V == Front]. +validate_referer(Req) -> +    Host = host_for_request(Req), +    Referer = header_value(Req, "Referer", fail), +    case Referer of +    fail -> +        throw({bad_request, <<"Referer header required.">>}); +    Referer -> +        {_,RefererHost,_,_,_} = mochiweb_util:urlsplit(Referer), +        if +            RefererHost =:= Host -> ok; +            true -> throw({bad_request, <<"Referer header must match host.">>}) +        end +    end. + +validate_ctype(Req, Ctype) -> +    case couch_httpd:header_value(Req, "Content-Type") of +    undefined -> +        throw({bad_ctype, "Content-Type must be "++Ctype}); +    ReqCtype -> +        % ?LOG_ERROR("Ctype ~p ReqCtype ~p",[Ctype,ReqCtype]), +        case re:split(ReqCtype, ";", [{return, list}]) of +        [Ctype] -> ok; +        [Ctype, _Rest] -> ok; +        _Else -> +            throw({bad_ctype, "Content-Type must be "++Ctype}) +        end +    end. +  % Utilities  partition(Path) -> @@ -367,9 +395,9 @@ qs(#httpd{mochi_req=MochiReq}) ->  path(#httpd{mochi_req=MochiReq}) ->      MochiReq:get(path). -absolute_uri(#httpd{mochi_req=MochiReq}, Path) -> +host_for_request(#httpd{mochi_req=MochiReq}) ->      XHost = couch_config:get("httpd", "x_forwarded_host", "X-Forwarded-Host"), -    Host = case MochiReq:get_header_value(XHost) of +    case MochiReq:get_header_value(XHost) of          undefined ->              case MochiReq:get_header_value("Host") of                  undefined -> @@ -379,7 +407,10 @@ absolute_uri(#httpd{mochi_req=MochiReq}, Path) ->                      Value1              end;          Value -> Value -    end, +    end. + +absolute_uri(#httpd{mochi_req=MochiReq}=Req, Path) -> +    Host = host_for_request(Req),      XSsl = couch_config:get("httpd", "x_forwarded_ssl", "X-Forwarded-Ssl"),      Scheme = case MochiReq:get_header_value(XSsl) of          "on" -> "https"; diff --git a/src/couchdb/couch_httpd_auth.erl b/src/couchdb/couch_httpd_auth.erl index f8c9a346..7023e7f3 100644 --- a/src/couchdb/couch_httpd_auth.erl +++ b/src/couchdb/couch_httpd_auth.erl @@ -251,6 +251,7 @@ ensure_cookie_auth_secret() ->  handle_session_req(#httpd{method='POST', mochi_req=MochiReq}=Req) ->      ReqBody = MochiReq:recv_body(),      Form = case MochiReq:get_primary_header_value("content-type") of +        % content type should be json          "application/x-www-form-urlencoded" ++ _ ->              mochiweb_util:parse_qs(ReqBody);          _ -> diff --git a/src/couchdb/couch_httpd_db.erl b/src/couchdb/couch_httpd_db.erl index 06d56c7b..f1514785 100644 --- a/src/couchdb/couch_httpd_db.erl +++ b/src/couchdb/couch_httpd_db.erl @@ -112,6 +112,7 @@ handle_changes_req(#httpd{path_parts=[_,<<"_changes">>]}=Req, _Db) ->      send_method_not_allowed(Req, "GET,HEAD").  handle_compact_req(#httpd{method='POST',path_parts=[DbName,_,Id|_]}=Req, _Db) -> +    couch_httpd:validate_ctype(Req, "application/json"),      ok = couch_view_compactor:start_compact(DbName, Id),      send_json(Req, 202, {[{ok, true}]}); @@ -195,6 +196,7 @@ db_req(#httpd{method='GET',path_parts=[_DbName]}=Req, Db) ->      send_json(Req, {DbInfo});  db_req(#httpd{method='POST',path_parts=[DbName]}=Req, Db) -> +    couch_httpd:validate_ctype(Req, "application/json"),      Doc = couch_doc:from_json_obj(couch_httpd:json_body(Req)),      Doc2 = case Doc#doc.id of          <<"">> -> @@ -262,6 +264,7 @@ db_req(#httpd{path_parts=[_,<<"_ensure_full_commit">>]}=Req, _Db) ->  db_req(#httpd{method='POST',path_parts=[_,<<"_bulk_docs">>]}=Req, Db) ->      couch_stats_collector:increment({httpd, bulk_requests}), +    couch_httpd:validate_ctype(Req, "application/json"),      {JsonProps} = couch_httpd:json_body_obj(Req),      DocsArray = couch_util:get_value(<<"docs">>, JsonProps),      case couch_httpd:header_value(Req, "X-Couch-Full-Commit") of @@ -323,6 +326,7 @@ db_req(#httpd{path_parts=[_,<<"_bulk_docs">>]}=Req, _Db) ->      send_method_not_allowed(Req, "POST");  db_req(#httpd{method='POST',path_parts=[_,<<"_purge">>]}=Req, Db) -> +    couch_httpd:validate_ctype(Req, "application/json"),      {IdsRevs} = couch_httpd:json_body_obj(Req),      IdsRevs2 = [{Id, couch_doc:parse_revs(Revs)} || {Id, Revs} <- IdsRevs], @@ -367,7 +371,6 @@ db_req(#httpd{method='POST',path_parts=[_,<<"_missing_revs">>]}=Req, Db) ->  db_req(#httpd{path_parts=[_,<<"_missing_revs">>]}=Req, _Db) ->      send_method_not_allowed(Req, "POST"); -  db_req(#httpd{method='POST',path_parts=[_,<<"_revs_diff">>]}=Req, Db) ->      {JsonDocIdRevs} = couch_httpd:json_body_obj(Req),      JsonDocIdRevs2 = @@ -586,14 +589,11 @@ db_doc_req(#httpd{method='GET'}=Req, Db, DocId) ->          end      end; +  db_doc_req(#httpd{method='POST'}=Req, Db, DocId) -> +    couch_httpd:validate_referer(Req),      couch_doc:validate_docid(DocId), -    case couch_httpd:header_value(Req, "Content-Type") of -    "multipart/form-data" ++  _Rest -> -        ok; -    _Else -> -        throw({bad_ctype, <<"Invalid Content-Type header for form upload">>}) -    end, +    couch_httpd:validate_ctype(Req, "multipart/form-data"),      Form = couch_httpd:parse_form(Req),      case proplists:is_defined("_doc", Form) of      true -> diff --git a/src/couchdb/couch_httpd_show.erl b/src/couchdb/couch_httpd_show.erl index c098f5ef..d50ca83a 100644 --- a/src/couchdb/couch_httpd_show.erl +++ b/src/couchdb/couch_httpd_show.erl @@ -145,7 +145,7 @@ send_doc_update_response(Req, Db, DDoc, UpdateName, Doc, DocId) ->              {200, JsonResp}      end, -    JsonResp2 = json_apply_field({<<"code">>, Code}, JsonResp1), +    JsonResp2 = couch_util:json_apply_field({<<"code">>, Code}, JsonResp1),      % todo set location field      couch_httpd_external:send_external_response(Req, JsonResp2). @@ -376,21 +376,6 @@ render_head_for_empty_list(StartListRespFun, Req, Etag, CurrentSeq, null) ->  render_head_for_empty_list(StartListRespFun, Req, Etag, CurrentSeq, TotalRows) ->      StartListRespFun(Req, Etag, TotalRows, null, [], CurrentSeq). - -% Maybe this is in the proplists API -% todo move to couch_util -json_apply_field(H, {L}) -> -    json_apply_field(H, L, []). -json_apply_field({Key, NewValue}, [{Key, _OldVal} | Headers], Acc) -> -    % drop matching keys -    json_apply_field({Key, NewValue}, Headers, Acc); -json_apply_field({Key, NewValue}, [{OtherKey, OtherVal} | Headers], Acc) -> -    % something else is next, leave it alone. -    json_apply_field({Key, NewValue}, Headers, [{OtherKey, OtherVal} | Acc]); -json_apply_field({Key, NewValue}, [], Acc) -> -    % end of list, add ours -    {[{Key, NewValue}|Acc]}. -  apply_etag({ExternalResponse}, CurrentEtag) ->      % Here we embark on the delicate task of replacing or creating the      % headers on the JsonResponse object. We need to control the Etag and @@ -404,8 +389,8 @@ apply_etag({ExternalResponse}, CurrentEtag) ->      JsonHeaders ->          {[case Field of          {<<"headers">>, JsonHeaders} -> % add our headers -            JsonHeadersEtagged = json_apply_field({<<"Etag">>, CurrentEtag}, JsonHeaders), -            JsonHeadersVaried = json_apply_field({<<"Vary">>, <<"Accept">>}, JsonHeadersEtagged), +            JsonHeadersEtagged = couch_util:json_apply_field({<<"Etag">>, CurrentEtag}, JsonHeaders), +            JsonHeadersVaried = couch_util:json_apply_field({<<"Vary">>, <<"Accept">>}, JsonHeadersEtagged),              {<<"headers">>, JsonHeadersVaried};          _ -> % skip non-header fields              Field diff --git a/src/couchdb/couch_rep.erl b/src/couchdb/couch_rep.erl index 96bed646..b741f86f 100644 --- a/src/couchdb/couch_rep.erl +++ b/src/couchdb/couch_rep.erl @@ -653,10 +653,11 @@ commit_to_both(Source, Target, RequiredSeq) ->      end,      {SourceStartTime, TargetStartTime}. -ensure_full_commit(#http_db{} = Target) -> +ensure_full_commit(#http_db{headers = Headers} = Target) ->      Req = Target#http_db{          resource = "_ensure_full_commit", -        method = post +        method = post, +        headers = [{"content-type", "application/json"} | Headers]      },      {ResultProps} = couch_rep_httpc:request(Req),      true = couch_util:get_value(<<"ok">>, ResultProps), @@ -677,11 +678,12 @@ ensure_full_commit(Target) ->          InstanceStartTime      end. -ensure_full_commit(#http_db{} = Source, RequiredSeq) -> +ensure_full_commit(#http_db{headers = Headers} = Source, RequiredSeq) ->      Req = Source#http_db{          resource = "_ensure_full_commit",          method = post, -        qs = [{seq, RequiredSeq}] +        qs = [{seq, RequiredSeq}], +        headers = [{"content-type", "application/json"} | Headers]      },      {ResultProps} = couch_rep_httpc:request(Req),      case couch_util:get_value(<<"ok">>, ResultProps) of diff --git a/src/couchdb/couch_rep_writer.erl b/src/couchdb/couch_rep_writer.erl index cdbbbee0..16fc1902 100644 --- a/src/couchdb/couch_rep_writer.erl +++ b/src/couchdb/couch_rep_writer.erl @@ -76,8 +76,9 @@ write_bulk_docs(#http_db{headers = Headers} = Db, Docs) ->          resource = "_bulk_docs",          method = post,          body = {[{new_edits, false}, {docs, JsonDocs}]}, -        headers = [{"x-couch-full-commit", "false"} | Headers] +        headers = couch_util:proplist_apply_field({"Content-Type", "application/json"}, [{"X-Couch-Full-Commit", "false"} | Headers])      }, +    ?LOG_ERROR("headers ~p",[Request#http_db.headers]),      ErrorsJson = case couch_rep_httpc:request(Request) of      {FailProps} ->          exit({target_error, couch_util:get_value(<<"error">>, FailProps)}); diff --git a/src/couchdb/couch_util.erl b/src/couchdb/couch_util.erl index b334c961..e50ab2fe 100644 --- a/src/couchdb/couch_util.erl +++ b/src/couchdb/couch_util.erl @@ -19,6 +19,7 @@  -export([encodeBase64Url/1, decodeBase64Url/1]).  -export([to_hex/1, parse_term/1, dict_find/3]).  -export([file_read_size/1, get_nested_json_value/2, json_user_ctx/1]). +-export([proplist_apply_field/2, json_apply_field/2]).  -export([to_binary/1, to_integer/1, to_list/1, url_encode/1]).  -export([json_encode/1, json_decode/1]).  -export([verify/2,simple_call/2,shutdown_sync/1]). @@ -143,6 +144,19 @@ get_nested_json_value(Value, []) ->  get_nested_json_value(_NotJSONObj, _) ->      throw({not_found, json_mismatch}). +proplist_apply_field(H, L) -> +    {R} = json_apply_field(H, {L}), +    R. + +json_apply_field(H, {L}) -> +    json_apply_field(H, L, []). +json_apply_field({Key, NewValue}, [{Key, _OldVal} | Headers], Acc) -> +    json_apply_field({Key, NewValue}, Headers, Acc); +json_apply_field({Key, NewValue}, [{OtherKey, OtherVal} | Headers], Acc) -> +    json_apply_field({Key, NewValue}, Headers, [{OtherKey, OtherVal} | Acc]); +json_apply_field({Key, NewValue}, [], Acc) -> +    {[{Key, NewValue}|Acc]}. +  json_user_ctx(#db{name=DbName, user_ctx=Ctx}) ->      {[{<<"db">>, DbName},              {<<"name">>,Ctx#user_ctx.name}, | 
