Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

R16 #1

Merged
merged 48 commits into from

14 participants

Maruthavanan Subbaryan Bob Ippolito Kostis Sagonas Huseyin Yilmaz Benoit Chesneau Logan Zhou Prashanth Mundkur KM Dmitry Demeshchuk Anthony Molinaro Sriram Melkote lhft Steve Vinoski Shoji KUMAGAI
Maruthavanan Subbaryan
Owner

No description provided.

Logan Zhou

What if we just make the return value of get_header_value/1 to lower case? If it is a string().

Benoit Chesneau

shouldn't it be the same for keep-alive then?

well the keep-alive value is also a connection header. But OK...

pmundkur and others added some commits
Prashanth Mundkur pmundkur The range-header handling does not implement the following:
http://tools.ietf.org/html/rfc2616#section-14.35.1

  If the last-byte-pos value is absent, or if
  the value is greater than or equal to the current
  length of the entity-body, last-byte-pos is taken
  to be equal to one less than the current length of
  the entity-body in bytes.

Specifically, the 'greater than equal to' case.
fe5e0c5
KM kmwang add ability to handle combined content-length header. 888ceb5
Bob Ippolito etrepum Merge pull request #85 from pmundkur/fix-ranges
Fix a case in handling range headers
e282266
KM kmwang amended get_combined_value. 53ee10b
KM kmwang support parsing quoted string. ede9003
Dmitry Demeshchuk doubleyou Avoid using regular expressions 2def5f1
Dmitry Demeshchuk doubleyou Removed export_all 2ba8d24
Dmitry Demeshchuk doubleyou Merge pull request #88 from doubleyou/handling-combined-header
Handling combined header
20f2f00
Anthony Molinaro djnym Fix for mochiweb_acceptor crash under R15B02
The source is still unclear but R15B02 now will return and emsgsize error
if the received packet is larger than the recvbuf.  This can be tested with
the following (sorry I don't know how to integrate this sort of test into
mochiweb's tests).

-module(mochi_test).

-export([start/0,
         handle_http/1,
         test/1]).

start() ->
  application:start (inets),
  mochiweb_http:start([{port, 5678}, {loop, fun(Req) -> handle_http(Req) end}]).

handle_http(Req) ->
  Req:respond({ 200,
                [ {"Content-Type", "text/html"} ],
                [ "<html><body>Hello</body></html>" ]
              }).

test (Len) ->
  httpc:request (get, {"http://127.0.0.1:5678/",
                 [{"X-Random", [$a || _ <- lists:seq(1,Len)]}]}, [], []).

Once compiled you can run this with

erl -pa ebin -boot start_sasl

Then run with

mochi_test:start().
mochi_test:test(10000).

The result is different with R14B04 and R15B02.  With R15B02 there was
a crash in the mochiweb_acceptor.  This patch deals with that crash.
fedfd11
Sriram Melkote melkote Do not allow backslashes in path (security).
On Windows, it is possible to access arbitrary files by crafting
a GET with unescaped \, like GET /..\..\..\..\..\windows\win.ini

http://www.couchbase.com/issues/browse/MB-7390
977f91c
Sriram Melkote melkote Issue 92: Do not allow backslashes in path (security).
On Windows, it is possible to access arbitrary files by crafting
a GET with unescaped \, like GET /..\..\..\..\..\windows\win.ini

Please also see ouchbase.com/issues/browse/MB-7390
ac2bf2a
Sriram Melkote melkote Merge branch 'master' of git://github.com/melkote/mochiweb 3259a93
Bob Ippolito etrepum Merge pull request #93 from melkote/master
Pull request for issue 92: Do not allow backslashes in path (windows security).
5ee1eeb
Bob Ippolito etrepum Merge pull request #91 from djnym/R15B02_mochiweb_acceptor_crash
Fix for mochiweb_acceptor crash under R15B02
dcd1076
Bob Ippolito etrepum prep changelog for 2.4.0 5ed0946
lhft lhft Added session module for use of secure cookies 1be66c0
lhft lhft Added some tests. still getting errors though 92464d0
lhft lhft Trying new encoding ways 1f1867b
lhft lhft Mochiweb session it's functional now 7aa6d1e
lhft lhft Working on Dymitri d21e5b2
lhft lhft Still workin on Dymitri's suggestions ba86dff
lhft lhft There is only one place to put user or any kind of data now. I don't …
…really understan the security implications of this. There is no term_to_binary in the code now.
ae7aa5d
Steve Vinoski vinoski use tuple modules instead of parameterized modules
Erlang R16, coming soon, will do away with parameterized modules (see Issue
4 under http://www.erlang.org/news/35 for details). Change Mochiweb to use
tuple modules instead, since they will continue to be supported in R16 and
beyond. These changes are backward compatible, so current Mochiweb
applications should require only recompilation to continue working.
23a1d48
Bob Ippolito etrepum Merge pull request #95 from vinoski/drop-param-mods
use tuple modules instead of parameterized modules
2fb6dcc
Bob Ippolito etrepum update CHANGES and bump vsn to 2.4.0 b02ea50
Bob Ippolito etrepum #96 - mochifmt_records regression 22b770e
lhft lhft Working on improvements suggested by doubleyou df5a881
Bob Ippolito etrepum fix mochiweb_request regression #97 5a1b589
Huseyin Yilmaz

I have been trying to run mochiweb template app for the last couple hours and I could not make it run. Than It magically started to work. So I wasn't doing anything wrong after all.

Sorry about that, some regressions were introduced a few days ago in the name of R16 compatibility. Most or all of the kinks should be worked out by now, please report any issues if you run into anything else.

Kostis Sagonas

Dialyzer tells me that some of these specs are invalid. The following diff shows specs that shut off dialyzer warnings:

--- a/src/mochiweb_session.erl
+++ b/src/mochiweb_session.erl
@@ -99,13 +99,13 @@ ensure_binary(B) when is_binary(B) ->
 ensure_binary(L) when is_list(L) ->
     iolist_to_binary(L).
 
--spec encrypt_data(iolist(), iolist()) -> binary().
+-spec encrypt_data(binary(), binary()) -> binary().
 encrypt_data(Data, Key) ->
     IV = crypto:rand_bytes(16),
     Crypt = crypto:aes_cfb_128_encrypt(Key, IV, Data),
     >.
 
--spec decrypt_data(binary(), iolist()) -> binary().
+-spec decrypt_data(binary(), binary()) -> binary().
 decrypt_data(>, Key) ->
     crypto:aes_cfb_128_decrypt(Key, IV, Crypt).
 
@@ -113,8 +113,8 @@ decrypt_data(>, Key) ->
 gen_key(ExpirationTime, ServerKey)->
     crypto:md5_mac(ServerKey, [ExpirationTime]).
 
--spec gen_hmac(iolist(), iolist(), iolist(), iolist()) -> binary().
-gen_hmac(ExpirationTime, Data, SessionKey, Key)->
+-spec gen_hmac(iolist(), binary(), iolist(), binary()) -> binary().
+gen_hmac(ExpirationTime, Data, SessionKey, Key) ->
     crypto:sha_mac(Key, [ExpirationTime, Data, SessionKey]).

Hope this helps.

Thanks!

Maruthavanan Subbaryan marutha merged commit 7f75cfb into from
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jul 28, 2012
  1. Bob Ippolito
Commits on Oct 8, 2012
  1. Prashanth Mundkur

    The range-header handling does not implement the following:

    pmundkur authored
    http://tools.ietf.org/html/rfc2616#section-14.35.1
    
      If the last-byte-pos value is absent, or if
      the value is greater than or equal to the current
      length of the entity-body, last-byte-pos is taken
      to be equal to one less than the current length of
      the entity-body in bytes.
    
    Specifically, the 'greater than equal to' case.
Commits on Oct 9, 2012
  1. KM
Commits on Oct 12, 2012
  1. Bob Ippolito

    Merge pull request #85 from pmundkur/fix-ranges

    etrepum authored
    Fix a case in handling range headers
Commits on Oct 15, 2012
  1. KM

    amended get_combined_value.

    kmwang authored
Commits on Oct 18, 2012
  1. KM

    support parsing quoted string.

    kmwang authored
Commits on Nov 6, 2012
  1. Dmitry Demeshchuk
  2. Dmitry Demeshchuk

    Removed export_all

    doubleyou authored
  3. Dmitry Demeshchuk

    Merge pull request #88 from doubleyou/handling-combined-header

    doubleyou authored
    Handling combined header
Commits on Dec 13, 2012
  1. Anthony Molinaro

    Fix for mochiweb_acceptor crash under R15B02

    djnym authored
    The source is still unclear but R15B02 now will return and emsgsize error
    if the received packet is larger than the recvbuf.  This can be tested with
    the following (sorry I don't know how to integrate this sort of test into
    mochiweb's tests).
    
    -module(mochi_test).
    
    -export([start/0,
             handle_http/1,
             test/1]).
    
    start() ->
      application:start (inets),
      mochiweb_http:start([{port, 5678}, {loop, fun(Req) -> handle_http(Req) end}]).
    
    handle_http(Req) ->
      Req:respond({ 200,
                    [ {"Content-Type", "text/html"} ],
                    [ "<html><body>Hello</body></html>" ]
                  }).
    
    test (Len) ->
      httpc:request (get, {"http://127.0.0.1:5678/",
                     [{"X-Random", [$a || _ <- lists:seq(1,Len)]}]}, [], []).
    
    Once compiled you can run this with
    
    erl -pa ebin -boot start_sasl
    
    Then run with
    
    mochi_test:start().
    mochi_test:test(10000).
    
    The result is different with R14B04 and R15B02.  With R15B02 there was
    a crash in the mochiweb_acceptor.  This patch deals with that crash.
Commits on Dec 14, 2012
  1. Sriram Melkote

    Do not allow backslashes in path (security).

    melkote authored
    On Windows, it is possible to access arbitrary files by crafting
    a GET with unescaped \, like GET /..\..\..\..\..\windows\win.ini
    
    http://www.couchbase.com/issues/browse/MB-7390
  2. Sriram Melkote

    Issue 92: Do not allow backslashes in path (security).

    melkote authored
    On Windows, it is possible to access arbitrary files by crafting
    a GET with unescaped \, like GET /..\..\..\..\..\windows\win.ini
    
    Please also see ouchbase.com/issues/browse/MB-7390
  3. Sriram Melkote
Commits on Dec 15, 2012
  1. Bob Ippolito

    Merge pull request #93 from melkote/master

    etrepum authored
    Pull request for issue 92: Do not allow backslashes in path (windows security).
  2. Bob Ippolito

    Merge pull request #91 from djnym/R15B02_mochiweb_acceptor_crash

    etrepum authored
    Fix for mochiweb_acceptor crash under R15B02
  3. Bob Ippolito

    prep changelog for 2.4.0

    etrepum authored
Commits on Jan 2, 2013
  1. lhft
Commits on Jan 4, 2013
  1. lhft
Commits on Jan 7, 2013
  1. lhft

    Trying new encoding ways

    lhft authored
Commits on Jan 8, 2013
  1. lhft
Commits on Jan 10, 2013
  1. lhft

    Working on Dymitri

    lhft authored
Commits on Jan 11, 2013
  1. lhft
  2. lhft

    There is only one place to put user or any kind of data now. I don't …

    lhft authored
    …really understan the security implications of this. There is no term_to_binary in the code now.
Commits on Jan 23, 2013
  1. Steve Vinoski

    use tuple modules instead of parameterized modules

    vinoski authored
    Erlang R16, coming soon, will do away with parameterized modules (see Issue
    4 under http://www.erlang.org/news/35 for details). Change Mochiweb to use
    tuple modules instead, since they will continue to be supported in R16 and
    beyond. These changes are backward compatible, so current Mochiweb
    applications should require only recompilation to continue working.
  2. Bob Ippolito

    Merge pull request #95 from vinoski/drop-param-mods

    etrepum authored
    use tuple modules instead of parameterized modules
  3. Bob Ippolito
Commits on Jan 24, 2013
  1. Bob Ippolito

    #96 - mochifmt_records regression

    etrepum authored
Commits on Jan 25, 2013
  1. lhft
Commits on Jan 26, 2013
  1. Bob Ippolito
Commits on Jan 27, 2013
  1. lhft

    Fixed the final notes

    lhft authored
Commits on Jan 30, 2013
  1. Bob Ippolito

    tag v2.4.1

    etrepum authored
Commits on Feb 6, 2013
  1. Shoji KUMAGAI
  2. Bob Ippolito

    Merge pull request #100 from shkumagai/feature/fix-mochiweb_response-…

    etrepum authored
    …regression
    
    fix mochiweb_response regression
  3. Bob Ippolito

    update CHANGES, tag v2.4.2

    etrepum authored
Commits on Feb 11, 2013
  1. lhft

    Some formatting applied

    lhft authored
Commits on Feb 20, 2013
  1. Dmitry Demeshchuk

    Merge pull request #94 from lhft/master

    doubleyou authored
    Session module for managing session cookies
Commits on Mar 4, 2013
  1. replace now() with os:timestamp() in acceptor

    Tristan Sloughter authored
  2. Bob Ippolito

    Merge pull request #102 from tsloughter/master

    etrepum authored
    replace now() with os:timestamp() in acceptor
  3. Bob Ippolito
  4. Bob Ippolito
  5. Bob Ippolito
  6. Bob Ippolito
Commits on Mar 6, 2013
  1. Bob Ippolito

    travis R16B

    etrepum authored
Commits on Mar 7, 2013
  1. Bob Ippolito
  2. Bob Ippolito
Commits on Mar 10, 2013
  1. Bob Ippolito

    dialyzer fixes

    etrepum authored
Commits on Mar 15, 2013
  1. Bob Ippolito
Commits on Mar 20, 2013
  1. Bob Ippolito
This page is out of date. Refresh to see the latest.
6 .travis.yml
View
@@ -2,6 +2,6 @@ language: erlang
notifications:
email: false
otp_release:
- - R14B03
- - R14B02
- - R14B01
+ - R15B02
+ - R15B03
+ - R16B
39 CHANGES.md
View
@@ -1,3 +1,42 @@
+Version 2.5.0 released 2013-03-04
+
+* Replace now() with os:timestamp() in acceptor (optimization)
+ https://github.com/mochi/mochiweb/pull/102
+* New mochiweb_session module for managing session cookies.
+ NOTE: this module is only supported on R15B02 and later!
+ https://github.com/mochi/mochiweb/pull/94
+* New mochiweb_base64url module for base64url encoding
+ (URL and Filename safe alphabet, see RFC 4648).
+* Fix rebar.config in mochiwebapp_skel to use {branch, "master"}
+ https://github.com/mochi/mochiweb/issues/105
+
+Version 2.4.2 released 2013-02-05
+
+* Fixed issue in mochiweb_response introduced in v2.4.0
+ https://github.com/mochi/mochiweb/pull/100
+
+Version 2.4.1 released 2013-01-30
+
+* Fixed issue in mochiweb_request introduced in v2.4.0
+ https://github.com/mochi/mochiweb/issues/97
+* Fixed issue in mochifmt_records introduced in v2.4.0
+ https://github.com/mochi/mochiweb/issues/96
+
+Version 2.4.0 released 2013-01-23
+
+* Switch from parameterized modules to explicit tuple module calls for
+ R16 compatibility (#95)
+* Fix for mochiweb_acceptor crash with extra-long HTTP headers under
+ R15B02 (#91)
+* Fix case in handling range headers (#85)
+* Handle combined Content-Length header (#88)
+* Windows security fix for `safe_relative_path`, any path with a
+ backslash on any platform is now considered unsafe (#92)
+
+Version 2.3.2 released 2012-07-27
+
+* Case insensitive match for "Connection: close" (#81)
+
Version 2.3.1 released 2012-03-31
* Fix edoc warnings (#63)
12 src/mochifmt_records.erl
View
@@ -9,11 +9,15 @@
%% M:format("{0.bar}", [#rec{bar=foo}]).
%% foo
--module(mochifmt_records, [Recs]).
+-module(mochifmt_records).
-author('bob@mochimedia.com').
--export([get_value/2]).
+-export([new/1, get_value/3]).
-get_value(Key, Rec) when is_tuple(Rec) and is_atom(element(1, Rec)) ->
+new([{_Rec, RecFields}]=Recs) when is_list(RecFields) ->
+ {?MODULE, Recs}.
+
+get_value(Key, Rec, {?MODULE, Recs})
+ when is_tuple(Rec) and is_atom(element(1, Rec)) ->
try begin
Atom = list_to_existing_atom(Key),
{_, Fields} = proplists:lookup(element(1, Rec), Recs),
@@ -21,7 +25,7 @@ get_value(Key, Rec) when is_tuple(Rec) and is_atom(element(1, Rec)) ->
end
catch error:_ -> mochifmt:get_value(Key, Rec)
end;
-get_value(Key, Args) ->
+get_value(Key, Args, {?MODULE, _Recs}) ->
mochifmt:get_value(Key, Args).
get_rec_index(Atom, [Atom | _], Index) ->
17 src/mochifmt_std.erl
View
@@ -3,23 +3,26 @@
%% @doc Template module for a mochifmt formatter.
--module(mochifmt_std, []).
+-module(mochifmt_std).
-author('bob@mochimedia.com').
--export([format/2, get_value/2, format_field/2, get_field/2, convert_field/2]).
+-export([new/0, format/3, get_value/3, format_field/3, get_field/3, convert_field/3]).
-format(Format, Args) ->
+new() ->
+ {?MODULE}.
+
+format(Format, Args, {?MODULE}=THIS) ->
mochifmt:format(Format, Args, THIS).
-get_field(Key, Args) ->
+get_field(Key, Args, {?MODULE}=THIS) ->
mochifmt:get_field(Key, Args, THIS).
-convert_field(Key, Args) ->
+convert_field(Key, Args, {?MODULE}) ->
mochifmt:convert_field(Key, Args).
-get_value(Key, Args) ->
+get_value(Key, Args, {?MODULE}) ->
mochifmt:get_value(Key, Args).
-format_field(Arg, Format) ->
+format_field(Arg, Format, {?MODULE}=THIS) ->
mochifmt:format_field(Arg, Format, THIS).
%%
2  src/mochiweb.app.src
View
@@ -1,7 +1,7 @@
%% This is generated from src/mochiweb.app.src
{application, mochiweb,
[{description, "MochiMedia Web Server"},
- {vsn, "2.3.1"},
+ {vsn, "2.5.0"},
{modules, []},
{registered, []},
{env, []},
4 src/mochiweb_acceptor.erl
View
@@ -14,10 +14,10 @@ start_link(Server, Listen, Loop) ->
proc_lib:spawn_link(?MODULE, init, [Server, Listen, Loop]).
init(Server, Listen, Loop) ->
- T1 = now(),
+ T1 = os:timestamp(),
case catch mochiweb_socket:accept(Listen) of
{ok, Socket} ->
- gen_server:cast(Server, {accepted, self(), timer:now_diff(now(), T1)}),
+ gen_server:cast(Server, {accepted, self(), timer:now_diff(os:timestamp(), T1)}),
call_loop(Loop, Socket);
{error, closed} ->
exit(normal);
83 src/mochiweb_base64url.erl
View
@@ -0,0 +1,83 @@
+-module(mochiweb_base64url).
+-export([encode/1, decode/1]).
+%% @doc URL and filename safe base64 variant with no padding,
+%% also known as "base64url" per RFC 4648.
+%%
+%% This differs from base64 in the following ways:
+%% '-' is used in place of '+' (62),
+%% '_' is used in place of '/' (63),
+%% padding is implicit rather than explicit ('=').
+
+-spec encode(iolist()) -> binary().
+encode(B) when is_binary(B) ->
+ encode_binary(B);
+encode(L) when is_list(L) ->
+ encode_binary(iolist_to_binary(L)).
+
+-spec decode(iolist()) -> binary().
+decode(B) when is_binary(B) ->
+ decode_binary(B);
+decode(L) when is_list(L) ->
+ decode_binary(iolist_to_binary(L)).
+
+%% Implementation, derived from stdlib base64.erl
+
+%% One-based decode map.
+-define(DECODE_MAP,
+ {bad,bad,bad,bad,bad,bad,bad,bad,ws,ws,bad,bad,ws,bad,bad, %1-15
+ bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad, %16-31
+ ws,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,62,bad,bad, %32-47
+ 52,53,54,55,56,57,58,59,60,61,bad,bad,bad,bad,bad,bad, %48-63
+ bad,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14, %64-79
+ 15,16,17,18,19,20,21,22,23,24,25,bad,bad,bad,bad,63, %80-95
+ bad,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40, %96-111
+ 41,42,43,44,45,46,47,48,49,50,51,bad,bad,bad,bad,bad, %112-127
+ bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,
+ bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,
+ bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,
+ bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,
+ bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,
+ bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,
+ bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,
+ bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad,bad}).
+
+encode_binary(Bin) ->
+ Split = 3*(byte_size(Bin) div 3),
+ <<Main0:Split/binary,Rest/binary>> = Bin,
+ Main = << <<(b64e(C)):8>> || <<C:6>> <= Main0 >>,
+ case Rest of
+ <<A:6,B:6,C:4>> ->
+ <<Main/binary,(b64e(A)):8,(b64e(B)):8,(b64e(C bsl 2)):8>>;
+ <<A:6,B:2>> ->
+ <<Main/binary,(b64e(A)):8,(b64e(B bsl 4)):8>>;
+ <<>> ->
+ Main
+ end.
+
+decode_binary(Bin) ->
+ Main = << <<(b64d(C)):6>> || <<C>> <= Bin,
+ (C =/= $\t andalso C =/= $\s andalso
+ C =/= $\r andalso C =/= $\n) >>,
+ case bit_size(Main) rem 8 of
+ 0 ->
+ Main;
+ N ->
+ Split = byte_size(Main) - 1,
+ <<Result:Split/bytes, _:N>> = Main,
+ Result
+ end.
+
+%% accessors
+
+b64e(X) ->
+ element(X+1,
+ {$A, $B, $C, $D, $E, $F, $G, $H, $I, $J, $K, $L, $M, $N,
+ $O, $P, $Q, $R, $S, $T, $U, $V, $W, $X, $Y, $Z,
+ $a, $b, $c, $d, $e, $f, $g, $h, $i, $j, $k, $l, $m, $n,
+ $o, $p, $q, $r, $s, $t, $u, $v, $w, $x, $y, $z,
+ $0, $1, $2, $3, $4, $5, $6, $7, $8, $9, $-, $_}).
+
+b64d(X) ->
+ b64d_ok(element(X, ?DECODE_MAP)).
+
+b64d_ok(I) when is_integer(I) -> I.
123 src/mochiweb_headers.erl
View
@@ -6,7 +6,7 @@
-module(mochiweb_headers).
-author('bob@mochimedia.com').
-export([empty/0, from_list/1, insert/3, enter/3, get_value/2, lookup/2]).
--export([delete_any/2, get_primary_value/2]).
+-export([delete_any/2, get_primary_value/2, get_combined_value/2]).
-export([default/3, enter_from_list/2, default_from_list/2]).
-export([to_list/1, make/1]).
-export([from_binary/1]).
@@ -112,6 +112,34 @@ get_primary_value(K, T) ->
lists:takewhile(fun (C) -> C =/= $; end, V)
end.
+%% @spec get_combined_value(key(), headers()) -> string() | undefined
+%% @doc Return the value from the given header using a case insensitive search.
+%% If the value of the header is a comma-separated list where holds values
+%% are all identical, the identical value will be returned.
+%% undefined will be returned for keys that are not present or the
+%% values in the list are not the same.
+%%
+%% NOTE: The process isn't designed for a general purpose. If you need
+%% to access all values in the combined header, please refer to
+%% '''tokenize_header_value/1'''.
+%%
+%% Section 4.2 of the RFC 2616 (HTTP 1.1) describes multiple message-header
+%% fields with the same field-name may be present in a message if and only
+%% if the entire field-value for that header field is defined as a
+%% comma-separated list [i.e., #(values)].
+get_combined_value(K, T) ->
+ case get_value(K, T) of
+ undefined ->
+ undefined;
+ V ->
+ case sets:to_list(sets:from_list(tokenize_header_value(V))) of
+ [Val] ->
+ Val;
+ _ ->
+ undefined
+ end
+ end.
+
%% @spec lookup(key(), headers()) -> {value, {key(), string()}} | none
%% @doc Return the case preserved key and value for the given header using
%% a case insensitive search. none will be returned for keys that are
@@ -164,6 +192,49 @@ delete_any(K, T) ->
%% Internal API
+tokenize_header_value(undefined) ->
+ undefined;
+tokenize_header_value(V) ->
+ reversed_tokens(trim_and_reverse(V, false), [], []).
+
+trim_and_reverse([S | Rest], Reversed) when S=:=$ ; S=:=$\n; S=:=$\t ->
+ trim_and_reverse(Rest, Reversed);
+trim_and_reverse(V, false) ->
+ trim_and_reverse(lists:reverse(V), true);
+trim_and_reverse(V, true) ->
+ V.
+
+reversed_tokens([], [], Acc) ->
+ Acc;
+reversed_tokens([], Token, Acc) ->
+ [Token | Acc];
+reversed_tokens("\"" ++ Rest, [], Acc) ->
+ case extract_quoted_string(Rest, []) of
+ {String, NewRest} ->
+ reversed_tokens(NewRest, [], [String | Acc]);
+ undefined ->
+ undefined
+ end;
+reversed_tokens("\"" ++ _Rest, _Token, _Acc) ->
+ undefined;
+reversed_tokens([C | Rest], [], Acc) when C=:=$ ;C=:=$\n;C=:=$\t;C=:=$, ->
+ reversed_tokens(Rest, [], Acc);
+reversed_tokens([C | Rest], Token, Acc) when C=:=$ ;C=:=$\n;C=:=$\t;C=:=$, ->
+ reversed_tokens(Rest, [], [Token | Acc]);
+reversed_tokens([C | Rest], Token, Acc) ->
+ reversed_tokens(Rest, [C | Token], Acc);
+reversed_tokens(_, _, _) ->
+ undefeined.
+
+extract_quoted_string([], _Acc) ->
+ undefined;
+extract_quoted_string("\"\\" ++ Rest, Acc) ->
+ extract_quoted_string(Rest, "\"" ++ Acc);
+extract_quoted_string("\"" ++ Rest, Acc) ->
+ {Acc, Rest};
+extract_quoted_string([C | Rest], Acc) ->
+ extract_quoted_string(Rest, [C | Acc]).
+
expand({array, L}) ->
mochiweb_util:join(lists:reverse(L), ", ");
expand(V) ->
@@ -237,6 +308,37 @@ get_primary_value_test() ->
get_primary_value(<<"baz">>, H)),
ok.
+get_combined_value_test() ->
+ H = make([{hdr, foo}, {baz, <<"wibble,taco">>}, {content_length, "123, 123"},
+ {test, " 123, 123, 123 , 123,123 "},
+ {test2, "456, 123, 123 , 123"},
+ {test3, "123"}, {test4, " 123, "}]),
+ ?assertEqual(
+ "foo",
+ get_combined_value(hdr, H)),
+ ?assertEqual(
+ undefined,
+ get_combined_value(bar, H)),
+ ?assertEqual(
+ undefined,
+ get_combined_value(<<"baz">>, H)),
+ ?assertEqual(
+ "123",
+ get_combined_value(<<"content_length">>, H)),
+ ?assertEqual(
+ "123",
+ get_combined_value(<<"test">>, H)),
+ ?assertEqual(
+ undefined,
+ get_combined_value(<<"test2">>, H)),
+ ?assertEqual(
+ "123",
+ get_combined_value(<<"test3">>, H)),
+ ?assertEqual(
+ "123",
+ get_combined_value(<<"test4">>, H)),
+ ok.
+
set_cookie_test() ->
H = make([{"set-cookie", foo}, {"set-cookie", bar}, {"set-cookie", baz}]),
?assertEqual(
@@ -296,4 +398,23 @@ headers_test() ->
[] = ?MODULE:to_list(?MODULE:from_binary([<<"\r\n\r\n">>])),
ok.
+tokenize_header_value_test() ->
+ ?assertEqual(["a quote in a \"quote\"."],
+ tokenize_header_value("\"a quote in a \\\"quote\\\".\"")),
+ ?assertEqual(["abc"], tokenize_header_value("abc")),
+ ?assertEqual(["abc", "def"], tokenize_header_value("abc def")),
+ ?assertEqual(["abc", "def"], tokenize_header_value("abc , def")),
+ ?assertEqual(["abc", "def"], tokenize_header_value(",abc ,, def,,")),
+ ?assertEqual(["abc def"], tokenize_header_value("\"abc def\" ")),
+ ?assertEqual(["abc, def"], tokenize_header_value("\"abc, def\"")),
+ ?assertEqual(["\\a\\$"], tokenize_header_value("\"\\a\\$\"")),
+ ?assertEqual(["abc def", "foo, bar", "12345", ""],
+ tokenize_header_value("\"abc def\" \"foo, bar\" , 12345, \"\"")),
+ ?assertEqual(undefined,
+ tokenize_header_value(undefined)),
+ ?assertEqual(undefined,
+ tokenize_header_value("umatched quote\"")),
+ ?assertEqual(undefined,
+ tokenize_header_value("\"unmatched quote")).
+
-endif.
18 src/mochiweb_http.erl
View
@@ -66,6 +66,10 @@ request(Socket, Body) ->
{ssl_closed, _} ->
mochiweb_socket:close(Socket),
exit(normal);
+ {tcp_error,_,emsgsize} ->
+ % R15B02 returns this then closes the socket, so close and exit
+ mochiweb_socket:close(Socket),
+ exit(normal);
_Other ->
handle_invalid_request(Socket)
after ?REQUEST_RECV_TIMEOUT ->
@@ -95,6 +99,10 @@ headers(Socket, Request, Headers, Body, HeaderCount) ->
{tcp_closed, _} ->
mochiweb_socket:close(Socket),
exit(normal);
+ {tcp_error,_,emsgsize} ->
+ % R15B02 returns this then closes the socket, so close and exit
+ mochiweb_socket:close(Socket),
+ exit(normal);
_Other ->
handle_invalid_request(Socket, Request, Headers)
after ?HEADERS_RECV_TIMEOUT ->
@@ -171,6 +179,8 @@ range_skip_length(Spec, Size) ->
invalid_range;
{Start, End} when 0 =< Start, Start =< End, End < Size ->
{Start, End - Start + 1};
+ {Start, End} when 0 =< Start, Start =< End, End >= Size ->
+ {Start, Size - Start};
{_OutOfRange, _End} ->
invalid_range
end.
@@ -225,19 +235,23 @@ range_skip_length_test() ->
BodySizeLess1 = BodySize - 1,
?assertEqual({BodySizeLess1, 1},
range_skip_length({BodySize - 1, none}, BodySize)),
+ ?assertEqual({BodySizeLess1, 1},
+ range_skip_length({BodySize - 1, BodySize+5}, BodySize)),
+ ?assertEqual({BodySizeLess1, 1},
+ range_skip_length({BodySize - 1, BodySize}, BodySize)),
%% out of range, return whole thing
?assertEqual({0, BodySize},
range_skip_length({none, BodySize + 1}, BodySize)),
?assertEqual({0, BodySize},
range_skip_length({none, -1}, BodySize)),
+ ?assertEqual({0, BodySize},
+ range_skip_length({0, BodySize + 1}, BodySize)),
%% invalid ranges
?assertEqual(invalid_range,
range_skip_length({-1, 30}, BodySize)),
?assertEqual(invalid_range,
- range_skip_length({0, BodySize + 1}, BodySize)),
- ?assertEqual(invalid_range,
range_skip_length({-1, BodySize + 1}, BodySize)),
?assertEqual(invalid_range,
range_skip_length({BodySize, 40}, BodySize)),
2  src/mochiweb_multipart.erl
View
@@ -128,7 +128,7 @@ default_file_handler_1(Filename, ContentType, Acc) ->
parse_multipart_request(Req, Callback) ->
%% TODO: Support chunked?
- Length = list_to_integer(Req:get_header_value("content-length")),
+ Length = list_to_integer(Req:get_combined_header_value("content-length")),
Boundary = iolist_to_binary(
get_boundary(Req:get_header_value("content-type"))),
Prefix = <<"\r\n--", Boundary/binary>>,
349 src/mochiweb_request.erl
View
@@ -3,7 +3,7 @@
%% @doc MochiWeb HTTP Request abstraction.
--module(mochiweb_request, [Socket, Method, RawPath, Version, Headers]).
+-module(mochiweb_request).
-author('bob@mochimedia.com').
-include_lib("kernel/include/file.hrl").
@@ -11,17 +11,18 @@
-define(QUIP, "Any of you quaids got a smint?").
--export([get_header_value/1, get_primary_header_value/1, get/1, dump/0]).
--export([send/1, recv/1, recv/2, recv_body/0, recv_body/1, stream_body/3]).
--export([start_response/1, start_response_length/1, start_raw_response/1]).
--export([respond/1, ok/1]).
--export([not_found/0, not_found/1]).
--export([parse_post/0, parse_qs/0]).
--export([should_close/0, cleanup/0]).
--export([parse_cookie/0, get_cookie_value/1]).
--export([serve_file/2, serve_file/3]).
--export([accepted_encodings/1]).
--export([accepts_content_type/1, accepted_content_types/1]).
+-export([new/5]).
+-export([get_header_value/2, get_primary_header_value/2, get_combined_header_value/2, get/2, dump/1]).
+-export([send/2, recv/2, recv/3, recv_body/1, recv_body/2, stream_body/4]).
+-export([start_response/2, start_response_length/2, start_raw_response/2]).
+-export([respond/2, ok/2]).
+-export([not_found/1, not_found/2]).
+-export([parse_post/1, parse_qs/1]).
+-export([should_close/1, cleanup/1]).
+-export([parse_cookie/1, get_cookie_value/2]).
+-export([serve_file/3, serve_file/4]).
+-export([accepted_encodings/2]).
+-export([accepts_content_type/2, accepted_content_types/2]).
-define(SAVE_QS, mochiweb_request_qs).
-define(SAVE_PATH, mochiweb_request_path).
@@ -35,6 +36,7 @@
%% @type key() = atom() | string() | binary()
%% @type value() = atom() | string() | binary() | integer()
%% @type headers(). A mochiweb_headers structure.
+%% @type request(). A mochiweb_request parameterized module instance.
%% @type response(). A mochiweb_response parameterized module instance.
%% @type ioheaders() = headers() | [{key(), value()}].
@@ -44,50 +46,58 @@
% Maximum recv_body() length of 1MB
-define(MAX_RECV_BODY, (1024*1024)).
-%% @spec get_header_value(K) -> undefined | Value
+%% @spec new(Socket, Method, RawPath, Version, headers()) -> request()
+%% @doc Create a new request instance.
+new(Socket, Method, RawPath, Version, Headers) ->
+ {?MODULE, [Socket, Method, RawPath, Version, Headers]}.
+
+%% @spec get_header_value(K, request()) -> undefined | Value
%% @doc Get the value of a given request header.
-get_header_value(K) ->
+get_header_value(K, {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}) ->
mochiweb_headers:get_value(K, Headers).
-get_primary_header_value(K) ->
+get_primary_header_value(K, {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}) ->
mochiweb_headers:get_primary_value(K, Headers).
+get_combined_header_value(K, {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}) ->
+ mochiweb_headers:get_combined_value(K, Headers).
+
%% @type field() = socket | scheme | method | raw_path | version | headers | peer | path | body_length | range
-%% @spec get(field()) -> term()
+%% @spec get(field(), request()) -> term()
%% @doc Return the internal representation of the given field. If
%% <code>socket</code> is requested on a HTTPS connection, then
%% an ssl socket will be returned as <code>{ssl, SslSocket}</code>.
%% You can use <code>SslSocket</code> with the <code>ssl</code>
%% application, eg: <code>ssl:peercert(SslSocket)</code>.
-get(socket) ->
+get(socket, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) ->
Socket;
-get(scheme) ->
+get(scheme, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) ->
case mochiweb_socket:type(Socket) of
plain ->
http;
ssl ->
https
end;
-get(method) ->
+get(method, {?MODULE, [_Socket, Method, _RawPath, _Version, _Headers]}) ->
Method;
-get(raw_path) ->
+get(raw_path, {?MODULE, [_Socket, _Method, RawPath, _Version, _Headers]}) ->
RawPath;
-get(version) ->
+get(version, {?MODULE, [_Socket, _Method, _RawPath, Version, _Headers]}) ->
Version;
-get(headers) ->
+get(headers, {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}) ->
Headers;
-get(peer) ->
+get(peer, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
case mochiweb_socket:peername(Socket) of
{ok, {Addr={10, _, _, _}, _Port}} ->
- case get_header_value("x-forwarded-for") of
+ case get_header_value("x-forwarded-for", THIS) of
undefined ->
inet_parse:ntoa(Addr);
Hosts ->
string:strip(lists:last(string:tokens(Hosts, ",")))
end;
{ok, {{127, 0, 0, 1}, _Port}} ->
- case get_header_value("x-forwarded-for") of
+ case get_header_value("x-forwarded-for", THIS) of
undefined ->
"127.0.0.1";
Hosts ->
@@ -98,7 +108,7 @@ get(peer) ->
{error, enotconn} ->
exit(normal)
end;
-get(path) ->
+get(path, {?MODULE, [_Socket, _Method, RawPath, _Version, _Headers]}) ->
case erlang:get(?SAVE_PATH) of
undefined ->
{Path0, _, _} = mochiweb_util:urlsplit_path(RawPath),
@@ -108,35 +118,35 @@ get(path) ->
Cached ->
Cached
end;
-get(body_length) ->
+get(body_length, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
case erlang:get(?SAVE_BODY_LENGTH) of
undefined ->
- BodyLength = body_length(),
+ BodyLength = body_length(THIS),
put(?SAVE_BODY_LENGTH, {cached, BodyLength}),
BodyLength;
{cached, Cached} ->
Cached
end;
-get(range) ->
- case get_header_value(range) of
+get(range, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+ case get_header_value(range, THIS) of
undefined ->
undefined;
RawRange ->
mochiweb_http:parse_range_request(RawRange)
end.
-%% @spec dump() -> {mochiweb_request, [{atom(), term()}]}
+%% @spec dump(request()) -> {mochiweb_request, [{atom(), term()}]}
%% @doc Dump the internal representation to a "human readable" set of terms
%% for debugging/inspection purposes.
-dump() ->
+dump({?MODULE, [_Socket, Method, RawPath, Version, Headers]}) ->
{?MODULE, [{method, Method},
{version, Version},
{raw_path, RawPath},
{headers, mochiweb_headers:to_list(Headers)}]}.
-%% @spec send(iodata()) -> ok
+%% @spec send(iodata(), request()) -> ok
%% @doc Send data over the socket.
-send(Data) ->
+send(Data, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) ->
case mochiweb_socket:send(Socket, Data) of
ok ->
ok;
@@ -144,16 +154,16 @@ send(Data) ->
exit(normal)
end.
-%% @spec recv(integer()) -> binary()
+%% @spec recv(integer(), request()) -> binary()
%% @doc Receive Length bytes from the client as a binary, with the default
%% idle timeout.
-recv(Length) ->
- recv(Length, ?IDLE_TIMEOUT).
+recv(Length, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+ recv(Length, ?IDLE_TIMEOUT, THIS).
-%% @spec recv(integer(), integer()) -> binary()
+%% @spec recv(integer(), integer(), request()) -> binary()
%% @doc Receive Length bytes from the client as a binary, with the given
%% Timeout in msec.
-recv(Length, Timeout) ->
+recv(Length, Timeout, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) ->
case mochiweb_socket:recv(Socket, Length, Timeout) of
{ok, Data} ->
put(?SAVE_RECV, true),
@@ -162,12 +172,12 @@ recv(Length, Timeout) ->
exit(normal)
end.
-%% @spec body_length() -> undefined | chunked | unknown_transfer_encoding | integer()
+%% @spec body_length(request()) -> undefined | chunked | unknown_transfer_encoding | integer()
%% @doc Infer body length from transfer-encoding and content-length headers.
-body_length() ->
- case get_header_value("transfer-encoding") of
+body_length({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+ case get_header_value("transfer-encoding", THIS) of
undefined ->
- case get_header_value("content-length") of
+ case get_combined_header_value("content-length", THIS) of
undefined ->
undefined;
Length ->
@@ -180,16 +190,16 @@ body_length() ->
end.
-%% @spec recv_body() -> binary()
+%% @spec recv_body(request()) -> binary()
%% @doc Receive the body of the HTTP request (defined by Content-Length).
%% Will only receive up to the default max-body length of 1MB.
-recv_body() ->
- recv_body(?MAX_RECV_BODY).
+recv_body({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+ recv_body(?MAX_RECV_BODY, THIS).
-%% @spec recv_body(integer()) -> binary()
+%% @spec recv_body(integer(), request()) -> binary()
%% @doc Receive the body of the HTTP request (defined by Content-Length).
%% Will receive up to MaxBody bytes.
-recv_body(MaxBody) ->
+recv_body(MaxBody, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
case erlang:get(?SAVE_BODY) of
undefined ->
% we could use a sane constant for max chunk size
@@ -203,17 +213,18 @@ recv_body(MaxBody) ->
true ->
{NewLength, [Bin | BinAcc]}
end
- end, {0, []}, MaxBody),
+ end, {0, []}, MaxBody, THIS),
put(?SAVE_BODY, Body),
Body;
Cached -> Cached
end.
-stream_body(MaxChunkSize, ChunkFun, FunState) ->
- stream_body(MaxChunkSize, ChunkFun, FunState, undefined).
+stream_body(MaxChunkSize, ChunkFun, FunState, {?MODULE,[_Socket,_Method,_RawPath,_Version,_Headers]}=THIS) ->
+ stream_body(MaxChunkSize, ChunkFun, FunState, undefined, THIS).
-stream_body(MaxChunkSize, ChunkFun, FunState, MaxBodyLength) ->
- Expect = case get_header_value("expect") of
+stream_body(MaxChunkSize, ChunkFun, FunState, MaxBodyLength,
+ {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+ Expect = case get_header_value("expect", THIS) of
undefined ->
undefined;
Value when is_list(Value) ->
@@ -221,12 +232,12 @@ stream_body(MaxChunkSize, ChunkFun, FunState, MaxBodyLength) ->
end,
case Expect of
"100-continue" ->
- _ = start_raw_response({100, gb_trees:empty()}),
+ _ = start_raw_response({100, gb_trees:empty()}, THIS),
ok;
_Else ->
ok
end,
- case body_length() of
+ case body_length(THIS) of
undefined ->
undefined;
{unknown_transfer_encoding, Unknown} ->
@@ -235,7 +246,7 @@ stream_body(MaxChunkSize, ChunkFun, FunState, MaxBodyLength) ->
% In this case the MaxBody is actually used to
% determine the maximum allowed size of a single
% chunk.
- stream_chunked_body(MaxChunkSize, ChunkFun, FunState);
+ stream_chunked_body(MaxChunkSize, ChunkFun, FunState, THIS);
0 ->
<<>>;
Length when is_integer(Length) ->
@@ -243,60 +254,64 @@ stream_body(MaxChunkSize, ChunkFun, FunState, MaxBodyLength) ->
MaxBodyLength when is_integer(MaxBodyLength), MaxBodyLength < Length ->
exit({body_too_large, content_length});
_ ->
- stream_unchunked_body(Length, ChunkFun, FunState)
+ stream_unchunked_body(Length, ChunkFun, FunState, THIS)
end
end.
-%% @spec start_response({integer(), ioheaders()}) -> response()
+%% @spec start_response({integer(), ioheaders()}, request()) -> response()
%% @doc Start the HTTP response by sending the Code HTTP response and
%% ResponseHeaders. The server will set header defaults such as Server
%% and Date if not present in ResponseHeaders.
-start_response({Code, ResponseHeaders}) ->
+start_response({Code, ResponseHeaders}, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
HResponse = mochiweb_headers:make(ResponseHeaders),
HResponse1 = mochiweb_headers:default_from_list(server_headers(),
HResponse),
- start_raw_response({Code, HResponse1}).
+ start_raw_response({Code, HResponse1}, THIS).
-%% @spec start_raw_response({integer(), headers()}) -> response()
+%% @spec start_raw_response({integer(), headers()}, request()) -> response()
%% @doc Start the HTTP response by sending the Code HTTP response and
%% ResponseHeaders.
-start_raw_response({Code, ResponseHeaders}) ->
+start_raw_response({Code, ResponseHeaders}, {?MODULE, [_Socket, _Method, _RawPath, Version, _Headers]}=THIS) ->
F = fun ({K, V}, Acc) ->
[mochiweb_util:make_io(K), <<": ">>, V, <<"\r\n">> | Acc]
end,
End = lists:foldl(F, [<<"\r\n">>],
mochiweb_headers:to_list(ResponseHeaders)),
- send([make_version(Version), make_code(Code), <<"\r\n">> | End]),
+ send([make_version(Version), make_code(Code), <<"\r\n">> | End], THIS),
mochiweb:new_response({THIS, Code, ResponseHeaders}).
-%% @spec start_response_length({integer(), ioheaders(), integer()}) -> response()
+%% @spec start_response_length({integer(), ioheaders(), integer()}, request()) -> response()
%% @doc Start the HTTP response by sending the Code HTTP response and
%% ResponseHeaders including a Content-Length of Length. The server
%% will set header defaults such as Server
%% and Date if not present in ResponseHeaders.
-start_response_length({Code, ResponseHeaders, Length}) ->
+start_response_length({Code, ResponseHeaders, Length},
+ {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
HResponse = mochiweb_headers:make(ResponseHeaders),
HResponse1 = mochiweb_headers:enter("Content-Length", Length, HResponse),
- start_response({Code, HResponse1}).
+ start_response({Code, HResponse1}, THIS).
-%% @spec respond({integer(), ioheaders(), iodata() | chunked | {file, IoDevice}}) -> response()
+%% @spec respond({integer(), ioheaders(), iodata() | chunked | {file, IoDevice}}, request()) -> response()
%% @doc Start the HTTP response with start_response, and send Body to the
%% client (if the get(method) /= 'HEAD'). The Content-Length header
%% will be set by the Body length, and the server will insert header
%% defaults.
-respond({Code, ResponseHeaders, {file, IoDevice}}) ->
+respond({Code, ResponseHeaders, {file, IoDevice}},
+ {?MODULE, [_Socket, Method, _RawPath, _Version, _Headers]}=THIS) ->
Length = mochiweb_io:iodevice_size(IoDevice),
- Response = start_response_length({Code, ResponseHeaders, Length}),
+ Response = start_response_length({Code, ResponseHeaders, Length}, THIS),
case Method of
'HEAD' ->
ok;
_ ->
- mochiweb_io:iodevice_stream(fun send/1, IoDevice)
+ mochiweb_io:iodevice_stream(
+ fun (Body) -> send(Body, THIS) end,
+ IoDevice)
end,
Response;
-respond({Code, ResponseHeaders, chunked}) ->
+respond({Code, ResponseHeaders, chunked}, {?MODULE, [_Socket, Method, _RawPath, Version, _Headers]}=THIS) ->
HResponse = mochiweb_headers:make(ResponseHeaders),
HResponse1 = case Method of
'HEAD' ->
@@ -317,35 +332,35 @@ respond({Code, ResponseHeaders, chunked}) ->
put(?SAVE_FORCE_CLOSE, true),
HResponse
end,
- start_response({Code, HResponse1});
-respond({Code, ResponseHeaders, Body}) ->
- Response = start_response_length({Code, ResponseHeaders, iolist_size(Body)}),
+ start_response({Code, HResponse1}, THIS);
+respond({Code, ResponseHeaders, Body}, {?MODULE, [_Socket, Method, _RawPath, _Version, _Headers]}=THIS) ->
+ Response = start_response_length({Code, ResponseHeaders, iolist_size(Body)}, THIS),
case Method of
'HEAD' ->
ok;
_ ->
- send(Body)
+ send(Body, THIS)
end,
Response.
-%% @spec not_found() -> response()
+%% @spec not_found(request()) -> response()
%% @doc Alias for <code>not_found([])</code>.
-not_found() ->
- not_found([]).
+not_found({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+ not_found([], THIS).
-%% @spec not_found(ExtraHeaders) -> response()
+%% @spec not_found(ExtraHeaders, request()) -> response()
%% @doc Alias for <code>respond({404, [{"Content-Type", "text/plain"}
%% | ExtraHeaders], &lt;&lt;"Not found."&gt;&gt;})</code>.
-not_found(ExtraHeaders) ->
+not_found(ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
respond({404, [{"Content-Type", "text/plain"} | ExtraHeaders],
- <<"Not found.">>}).
+ <<"Not found.">>}, THIS).
-%% @spec ok({value(), iodata()} | {value(), ioheaders(), iodata() | {file, IoDevice}}) ->
+%% @spec ok({value(), iodata()} | {value(), ioheaders(), iodata() | {file, IoDevice}}, request()) ->
%% response()
%% @doc respond({200, [{"Content-Type", ContentType} | Headers], Body}).
-ok({ContentType, Body}) ->
- ok({ContentType, [], Body});
-ok({ContentType, ResponseHeaders, Body}) ->
+ok({ContentType, Body}, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+ ok({ContentType, [], Body}, THIS);
+ok({ContentType, ResponseHeaders, Body}, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
HResponse = mochiweb_headers:make(ResponseHeaders),
case THIS:get(range) of
X when (X =:= undefined orelse X =:= fail) orelse Body =:= chunked ->
@@ -354,7 +369,7 @@ ok({ContentType, ResponseHeaders, Body}) ->
%% full response.
HResponse1 = mochiweb_headers:enter("Content-Type", ContentType,
HResponse),
- respond({200, HResponse1, Body});
+ respond({200, HResponse1, Body}, THIS);
Ranges ->
{PartList, Size} = range_parts(Body, Ranges),
case PartList of
@@ -363,7 +378,7 @@ ok({ContentType, ResponseHeaders, Body}) ->
ContentType,
HResponse),
%% could be 416, for now we'll just return 200
- respond({200, HResponse1, Body});
+ respond({200, HResponse1, Body}, THIS);
PartList ->
{RangeHeaders, RangeBody} =
mochiweb_multipart:parts_to_body(PartList, ContentType, Size),
@@ -371,33 +386,40 @@ ok({ContentType, ResponseHeaders, Body}) ->
[{"Accept-Ranges", "bytes"} |
RangeHeaders],
HResponse),
- respond({206, HResponse1, RangeBody})
+ respond({206, HResponse1, RangeBody}, THIS)
end
end.
-%% @spec should_close() -> bool()
+%% @spec should_close(request()) -> bool()
%% @doc Return true if the connection must be closed. If false, using
%% Keep-Alive should be safe.
-should_close() ->
+should_close({?MODULE, [_Socket, _Method, _RawPath, Version, _Headers]}=THIS) ->
ForceClose = erlang:get(?SAVE_FORCE_CLOSE) =/= undefined,
DidNotRecv = erlang:get(?SAVE_RECV) =:= undefined,
ForceClose orelse Version < {1, 0}
%% Connection: close
- orelse get_header_value("connection") =:= "close"
+ orelse is_close(get_header_value("connection", THIS))
%% HTTP 1.0 requires Connection: Keep-Alive
orelse (Version =:= {1, 0}
- andalso get_header_value("connection") =/= "Keep-Alive")
+ andalso get_header_value("connection", THIS) =/= "Keep-Alive")
%% unread data left on the socket, can't safely continue
orelse (DidNotRecv
- andalso get_header_value("content-length") =/= undefined
- andalso list_to_integer(get_header_value("content-length")) > 0)
+ andalso get_combined_header_value("content-length", THIS) =/= undefined
+ andalso list_to_integer(get_combined_header_value("content-length", THIS)) > 0)
orelse (DidNotRecv
- andalso get_header_value("transfer-encoding") =:= "chunked").
+ andalso get_header_value("transfer-encoding", THIS) =:= "chunked").
+
+is_close("close") ->
+ true;
+is_close(S=[_C, _L, _O, _S, _E]) ->
+ string:to_lower(S) =:= "close";
+is_close(_) ->
+ false.
-%% @spec cleanup() -> ok
+%% @spec cleanup(request()) -> ok
%% @doc Clean up any junk in the process dictionary, required before continuing
%% a Keep-Alive request.
-cleanup() ->
+cleanup({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}) ->
L = [?SAVE_QS, ?SAVE_PATH, ?SAVE_RECV, ?SAVE_BODY, ?SAVE_BODY_LENGTH,
?SAVE_POST, ?SAVE_COOKIE, ?SAVE_FORCE_CLOSE],
lists:foreach(fun(K) ->
@@ -405,9 +427,9 @@ cleanup() ->
end, L),
ok.
-%% @spec parse_qs() -> [{Key::string(), Value::string()}]
+%% @spec parse_qs(request()) -> [{Key::string(), Value::string()}]
%% @doc Parse the query string of the URL.
-parse_qs() ->
+parse_qs({?MODULE, [_Socket, _Method, RawPath, _Version, _Headers]}) ->
case erlang:get(?SAVE_QS) of
undefined ->
{_, QueryString, _} = mochiweb_util:urlsplit_path(RawPath),
@@ -418,17 +440,17 @@ parse_qs() ->
Cached
end.
-%% @spec get_cookie_value(Key::string) -> string() | undefined
+%% @spec get_cookie_value(Key::string, request()) -> string() | undefined
%% @doc Get the value of the given cookie.
-get_cookie_value(Key) ->
- proplists:get_value(Key, parse_cookie()).
+get_cookie_value(Key, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+ proplists:get_value(Key, parse_cookie(THIS)).
-%% @spec parse_cookie() -> [{Key::string(), Value::string()}]
+%% @spec parse_cookie(request()) -> [{Key::string(), Value::string()}]
%% @doc Parse the cookie header.
-parse_cookie() ->
+parse_cookie({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
case erlang:get(?SAVE_COOKIE) of
undefined ->
- Cookies = case get_header_value("cookie") of
+ Cookies = case get_header_value("cookie", THIS) of
undefined ->
[];
Value ->
@@ -440,17 +462,17 @@ parse_cookie() ->
Cached
end.
-%% @spec parse_post() -> [{Key::string(), Value::string()}]
+%% @spec parse_post(request()) -> [{Key::string(), Value::string()}]
%% @doc Parse an application/x-www-form-urlencoded form POST. This
%% has the side-effect of calling recv_body().
-parse_post() ->
+parse_post({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
case erlang:get(?SAVE_POST) of
undefined ->
- Parsed = case recv_body() of
+ Parsed = case recv_body(THIS) of
undefined ->
[];
Binary ->
- case get_primary_header_value("content-type") of
+ case get_primary_header_value("content-type",THIS) of
"application/x-www-form-urlencoded" ++ _ ->
mochiweb_util:parse_qs(Binary);
_ ->
@@ -463,37 +485,39 @@ parse_post() ->
Cached
end.
-%% @spec stream_chunked_body(integer(), fun(), term()) -> term()
+%% @spec stream_chunked_body(integer(), fun(), term(), request()) -> term()
%% @doc The function is called for each chunk.
%% Used internally by read_chunked_body.
-stream_chunked_body(MaxChunkSize, Fun, FunState) ->
- case read_chunk_length() of
+stream_chunked_body(MaxChunkSize, Fun, FunState,
+ {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+ case read_chunk_length(THIS) of
0 ->
- Fun({0, read_chunk(0)}, FunState);
+ Fun({0, read_chunk(0, THIS)}, FunState);
Length when Length > MaxChunkSize ->
- NewState = read_sub_chunks(Length, MaxChunkSize, Fun, FunState),
- stream_chunked_body(MaxChunkSize, Fun, NewState);
+ NewState = read_sub_chunks(Length, MaxChunkSize, Fun, FunState, THIS),
+ stream_chunked_body(MaxChunkSize, Fun, NewState, THIS);
Length ->
- NewState = Fun({Length, read_chunk(Length)}, FunState),
- stream_chunked_body(MaxChunkSize, Fun, NewState)
+ NewState = Fun({Length, read_chunk(Length, THIS)}, FunState),
+ stream_chunked_body(MaxChunkSize, Fun, NewState, THIS)
end.
-stream_unchunked_body(0, Fun, FunState) ->
+stream_unchunked_body(0, Fun, FunState, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}) ->
Fun({0, <<>>}, FunState);
-stream_unchunked_body(Length, Fun, FunState) when Length > 0 ->
+stream_unchunked_body(Length, Fun, FunState,
+ {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) when Length > 0 ->
PktSize = case Length > ?RECBUF_SIZE of
true ->
?RECBUF_SIZE;
false ->
Length
end,
- Bin = recv(PktSize),
+ Bin = recv(PktSize, THIS),
NewState = Fun({PktSize, Bin}, FunState),
- stream_unchunked_body(Length - PktSize, Fun, NewState).
+ stream_unchunked_body(Length - PktSize, Fun, NewState, THIS).
-%% @spec read_chunk_length() -> integer()
+%% @spec read_chunk_length(request()) -> integer()
%% @doc Read the length of the next HTTP chunk.
-read_chunk_length() ->
+read_chunk_length({?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) ->
ok = mochiweb_socket:setopts(Socket, [{packet, line}]),
case mochiweb_socket:recv(Socket, 0, ?IDLE_TIMEOUT) of
{ok, Header} ->
@@ -507,10 +531,10 @@ read_chunk_length() ->
exit(normal)
end.
-%% @spec read_chunk(integer()) -> Chunk::binary() | [Footer::binary()]
+%% @spec read_chunk(integer(), request()) -> Chunk::binary() | [Footer::binary()]
%% @doc Read in a HTTP chunk of the given length. If Length is 0, then read the
%% HTTP footers (as a list of binaries, since they're nominal).
-read_chunk(0) ->
+read_chunk(0, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) ->
ok = mochiweb_socket:setopts(Socket, [{packet, line}]),
F = fun (F1, Acc) ->
case mochiweb_socket:recv(Socket, 0, ?IDLE_TIMEOUT) of
@@ -526,7 +550,7 @@ read_chunk(0) ->
ok = mochiweb_socket:setopts(Socket, [{packet, raw}]),
put(?SAVE_RECV, true),
Footers;
-read_chunk(Length) ->
+read_chunk(Length, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) ->
case mochiweb_socket:recv(Socket, 2 + Length, ?IDLE_TIMEOUT) of
{ok, <<Chunk:Length/binary, "\r\n">>} ->
Chunk;
@@ -534,32 +558,34 @@ read_chunk(Length) ->
exit(normal)
end.
-read_sub_chunks(Length, MaxChunkSize, Fun, FunState) when Length > MaxChunkSize ->
- Bin = recv(MaxChunkSize),
+read_sub_chunks(Length, MaxChunkSize, Fun, FunState,
+ {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) when Length > MaxChunkSize ->
+ Bin = recv(MaxChunkSize, THIS),
NewState = Fun({size(Bin), Bin}, FunState),
- read_sub_chunks(Length - MaxChunkSize, MaxChunkSize, Fun, NewState);
+ read_sub_chunks(Length - MaxChunkSize, MaxChunkSize, Fun, NewState, THIS);
-read_sub_chunks(Length, _MaxChunkSize, Fun, FunState) ->
- Fun({Length, read_chunk(Length)}, FunState).
+read_sub_chunks(Length, _MaxChunkSize, Fun, FunState,
+ {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+ Fun({Length, read_chunk(Length, THIS)}, FunState).
-%% @spec serve_file(Path, DocRoot) -> Response
+%% @spec serve_file(Path, DocRoot, request()) -> Response
%% @doc Serve a file relative to DocRoot.
-serve_file(Path, DocRoot) ->
- serve_file(Path, DocRoot, []).
+serve_file(Path, DocRoot, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+ serve_file(Path, DocRoot, [], THIS).
-%% @spec serve_file(Path, DocRoot, ExtraHeaders) -> Response
+%% @spec serve_file(Path, DocRoot, ExtraHeaders, request()) -> Response
%% @doc Serve a file relative to DocRoot.
-serve_file(Path, DocRoot, ExtraHeaders) ->
+serve_file(Path, DocRoot, ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
case mochiweb_util:safe_relative_path(Path) of
undefined ->
- not_found(ExtraHeaders);
+ not_found(ExtraHeaders, THIS);
RelPath ->
FullPath = filename:join([DocRoot, RelPath]),
case filelib:is_dir(FullPath) of
true ->
- maybe_redirect(RelPath, FullPath, ExtraHeaders);
+ maybe_redirect(RelPath, FullPath, ExtraHeaders, THIS);
false ->
- maybe_serve_file(FullPath, ExtraHeaders)
+ maybe_serve_file(FullPath, ExtraHeaders, THIS)
end
end.
@@ -569,13 +595,14 @@ serve_file(Path, DocRoot, ExtraHeaders) ->
directory_index(FullPath) ->
filename:join([FullPath, "index.html"]).
-maybe_redirect([], FullPath, ExtraHeaders) ->
- maybe_serve_file(directory_index(FullPath), ExtraHeaders);
+maybe_redirect([], FullPath, ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+ maybe_serve_file(directory_index(FullPath), ExtraHeaders, THIS);
-maybe_redirect(RelPath, FullPath, ExtraHeaders) ->
+maybe_redirect(RelPath, FullPath, ExtraHeaders,
+ {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}=THIS) ->
case string:right(RelPath, 1) of
"/" ->
- maybe_serve_file(directory_index(FullPath), ExtraHeaders);
+ maybe_serve_file(directory_index(FullPath), ExtraHeaders, THIS);
_ ->
Host = mochiweb_headers:get_value("host", Headers),
Location = "http://" ++ Host ++ "/" ++ RelPath ++ "/",
@@ -590,16 +617,16 @@ maybe_redirect(RelPath, FullPath, ExtraHeaders) ->
"<p>The document has moved <a href=\"">>,
Bottom = <<">here</a>.</p></body></html>\n">>,
Body = <<Top/binary, LocationBin/binary, Bottom/binary>>,
- respond({301, MoreHeaders, Body})
+ respond({301, MoreHeaders, Body}, THIS)
end.
-maybe_serve_file(File, ExtraHeaders) ->
+maybe_serve_file(File, ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
case file:read_file_info(File) of
{ok, FileInfo} ->
LastModified = httpd_util:rfc1123_date(FileInfo#file_info.mtime),
- case get_header_value("if-modified-since") of
+ case get_header_value("if-modified-since", THIS) of
LastModified ->
- respond({304, ExtraHeaders, ""});
+ respond({304, ExtraHeaders, ""}, THIS);
_ ->
case file:open(File, [raw, binary]) of
{ok, IoDevice} ->
@@ -607,15 +634,15 @@ maybe_serve_file(File, ExtraHeaders) ->
Res = ok({ContentType,
[{"last-modified", LastModified}
| ExtraHeaders],
- {file, IoDevice}}),
+ {file, IoDevice}}, THIS),
ok = file:close(IoDevice),
Res;
_ ->
- not_found(ExtraHeaders)
+ not_found(ExtraHeaders, THIS)
end
end;
{error, _} ->
- not_found(ExtraHeaders)
+ not_found(ExtraHeaders, THIS)
end.
server_headers() ->
@@ -663,7 +690,7 @@ range_parts(Body0, Ranges) ->
end,
{lists:foldr(F, [], Ranges), Size}.
-%% @spec accepted_encodings([encoding()]) -> [encoding()] | bad_accept_encoding_value
+%% @spec accepted_encodings([encoding()], request()) -> [encoding()] | bad_accept_encoding_value
%% @type encoding() = string().
%%
%% @doc Returns a list of encodings accepted by a request. Encodings that are
@@ -687,8 +714,8 @@ range_parts(Body0, Ranges) ->
%% accepted_encodings(["gzip", "deflate", "identity"]) ->
%% ["deflate", "gzip", "identity"]
%%
-accepted_encodings(SupportedEncodings) ->
- AcceptEncodingHeader = case get_header_value("Accept-Encoding") of
+accepted_encodings(SupportedEncodings, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+ AcceptEncodingHeader = case get_header_value("Accept-Encoding", THIS) of
undefined ->
"";
Value ->
@@ -703,7 +730,7 @@ accepted_encodings(SupportedEncodings) ->
)
end.
-%% @spec accepts_content_type(string() | binary()) -> boolean() | bad_accept_header
+%% @spec accepts_content_type(string() | binary(), request()) -> boolean() | bad_accept_header
%%
%% @doc Determines whether a request accepts a given media type by analyzing its
%% "Accept" header.
@@ -725,9 +752,9 @@ accepted_encodings(SupportedEncodings) ->
%% 5) For an "Accept" header with value "text/*; q=0.0, */*":
%% accepts_content_type("text/plain") -> false
%%
-accepts_content_type(ContentType1) ->
+accepts_content_type(ContentType1, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
ContentType = re:replace(ContentType1, "\\s", "", [global, {return, list}]),
- AcceptHeader = accept_header(),
+ AcceptHeader = accept_header(THIS),
case mochiweb_util:parse_qvalues(AcceptHeader) of
invalid_qvalue_string ->
bad_accept_header;
@@ -748,7 +775,7 @@ accepts_content_type(ContentType1) ->
(not lists:member({SuperType, 0.0}, QList))
end.
-%% @spec accepted_content_types([string() | binary()]) -> [string()] | bad_accept_header
+%% @spec accepted_content_types([string() | binary()], request()) -> [string()] | bad_accept_header
%%
%% @doc Filters which of the given media types this request accepts. This filtering
%% is performed by analyzing the "Accept" header. The returned list is sorted
@@ -774,11 +801,11 @@ accepts_content_type(ContentType1) ->
%% accepts_content_types(["application/json", "text/html"]) ->
%% ["text/html", "application/json"]
%%
-accepted_content_types(Types1) ->
+accepted_content_types(Types1, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
Types = lists:map(
fun(T) -> re:replace(T, "\\s", "", [global, {return, list}]) end,
Types1),
- AcceptHeader = accept_header(),
+ AcceptHeader = accept_header(THIS),
case mochiweb_util:parse_qvalues(AcceptHeader) of
invalid_qvalue_string ->
bad_accept_header;
@@ -814,8 +841,8 @@ accepted_content_types(Types1) ->
[Type || {_Q, Type} <- lists:sort(SortFun, TypesQ)]
end.
-accept_header() ->
- case get_header_value("Accept") of
+accept_header({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+ case get_header_value("Accept", THIS) of
undefined ->
"*/*";
Value ->
30 src/mochiweb_request_tests.erl
View
@@ -149,4 +149,34 @@ accepted_content_types_test() ->
?assertEqual(["application/json", "text/html"],
Req9:accepted_content_types(["text/html", "application/json"])).
+should_close_test() ->
+ F = fun (V, H) ->
+ (mochiweb_request:new(
+ nil, 'GET', "/", V,
+ mochiweb_headers:make(H)
+ )):should_close()
+ end,
+ ?assertEqual(
+ true,
+ F({1, 1}, [{"Connection", "close"}])),
+ ?assertEqual(
+ true,
+ F({1, 0}, [{"Connection", "close"}])),
+ ?assertEqual(
+ true,
+ F({1, 1}, [{"Connection", "ClOSe"}])),
+ ?assertEqual(
+ false,
+ F({1, 1}, [{"Connection", "closer"}])),
+ ?assertEqual(
+ false,
+ F({1, 1}, [])),
+ ?assertEqual(
+ true,
+ F({1, 0}, [])),
+ ?assertEqual(
+ false,
+ F({1, 0}, [{"Connection", "Keep-Alive"}])),
+ ok.
+
-endif.
42 src/mochiweb_response.erl
View
@@ -3,39 +3,47 @@
%% @doc Response abstraction.
--module(mochiweb_response, [Request, Code, Headers]).
+-module(mochiweb_response).
-author('bob@mochimedia.com').
-define(QUIP, "Any of you quaids got a smint?").
--export([get_header_value/1, get/1, dump/0]).
--export([send/1, write_chunk/1]).
+-export([new/3, get_header_value/2, get/2, dump/1]).
+-export([send/2, write_chunk/2]).
-%% @spec get_header_value(string() | atom() | binary()) -> string() | undefined
+%% @type response(). A mochiweb_response parameterized module instance.
+
+%% @spec new(Request, Code, Headers) -> response()
+%% @doc Create a new mochiweb_response instance.
+new(Request, Code, Headers) ->
+ {?MODULE, [Request, Code, Headers]}.
+
+%% @spec get_header_value(string() | atom() | binary(), response()) ->
+%% string() | undefined
%% @doc Get the value of the given response header.
-get_header_value(K) ->
+get_header_value(K, {?MODULE, [_Request, _Code, Headers]}) ->
mochiweb_headers:get_value(K, Headers).
-%% @spec get(request | code | headers) -> term()
+%% @spec get(request | code | headers, response()) -> term()
%% @doc Return the internal representation of the given field.
-get(request) ->
+get(request, {?MODULE, [Request, _Code, _Headers]}) ->
Request;
-get(code) ->
+get(code, {?MODULE, [_Request, Code, _Headers]}) ->
Code;
-get(headers) ->
+get(headers, {?MODULE, [_Request, _Code, Headers]}) ->
Headers.
-%% @spec dump() -> {mochiweb_request, [{atom(), term()}]}
+%% @spec dump(response()) -> {mochiweb_request, [{atom(), term()}]}
%% @doc Dump the internal representation to a "human readable" set of terms
%% for debugging/inspection purposes.
-dump() ->
+dump({?MODULE, [Request, Code, Headers]}) ->
[{request, Request:dump()},
{code, Code},
{headers, mochiweb_headers:to_list(Headers)}].
-%% @spec send(iodata()) -> ok
+%% @spec send(iodata(), response()) -> ok
%% @doc Send data over the socket if the method is not HEAD.
-send(Data) ->
+send(Data, {?MODULE, [Request, _Code, _Headers]}) ->
case Request:get(method) of
'HEAD' ->
ok;
@@ -43,16 +51,16 @@ send(Data) ->
Request:send(Data)
end.
-%% @spec write_chunk(iodata()) -> ok
+%% @spec write_chunk(iodata(), response()) -> ok
%% @doc Write a chunk of a HTTP chunked response. If Data is zero length,
%% then the chunked response will be finished.
-write_chunk(Data) ->
+write_chunk(Data, {?MODULE, [Request, _Code, _Headers]}=THIS) ->
case Request:get(version) of
Version when Version >= {1, 1} ->
Length = iolist_size(Data),
- send([io_lib:format("~.16b\r\n", [Length]), Data, <<"\r\n">>]);
+ send([io_lib:format("~.16b\r\n", [Length]), Data, <<"\r\n">>], THIS);
_ ->
- send(Data)
+ send(Data, THIS)
end.
189 src/mochiweb_session.erl
View
@@ -0,0 +1,189 @@
+%% @author Asier Azkuenaga Batiz <asier@zebixe.com>
+
+%% @doc HTTP Cookie session. Note that the expiration time travels unencrypted
+%% as far as this module is concerned. In order to achieve more security,
+%% it is advised to use https.
+%% Based on the paper
+%% <a href="http://www.cse.msu.edu/~alexliu/publications/Cookie/cookie.pdf">
+%% "A Secure Cookie Protocol"</a>.
+%% This module is only supported on R15B02 and later, the AES CFB mode is not
+%% available in earlier releases of crypto.
+-module(mochiweb_session).
+-export([generate_session_data/4, generate_session_cookie/4,
+ check_session_cookie/4]).
+
+-export_types([expiration_time/0]).
+-type expiration_time() :: integer().
+-type key_fun() :: fun((string()) -> iolist()).
+
+%% TODO: Import this from elsewhere after attribute types refactor.
+-type header() :: {string(), string()}.
+
+%% @doc Generates a secure encrypted binary convining all the parameters. The
+%% expiration time must be a 32-bit integer.
+-spec generate_session_data(
+ ExpirationTime :: expiration_time(),
+ Data :: iolist(),
+ FSessionKey :: key_fun(),
+ ServerKey :: iolist()) -> binary().
+generate_session_data(ExpirationTime, Data, FSessionKey, ServerKey)
+ when is_integer(ExpirationTime), is_function(FSessionKey)->
+ BData = ensure_binary(Data),
+ ExpTime = integer_to_list(ExpirationTime),
+ Key = gen_key(ExpTime, ServerKey),
+ Hmac = gen_hmac(ExpTime, BData, FSessionKey(ExpTime), Key),
+ EData = encrypt_data(BData, Key),
+ mochiweb_base64url:encode(
+ <<ExpirationTime:32/integer, Hmac/binary, EData/binary>>).
+
+%% @doc Convenience wrapper for generate_session_data that returns a
+%% mochiweb cookie with "id" as the key, a max_age of 20000 seconds,
+%% and the current local time as local time.
+-spec generate_session_cookie(
+ ExpirationTime :: expiration_time(),
+ Data :: iolist(),
+ FSessionKey :: key_fun(),
+ ServerKey :: iolist()) -> header().
+generate_session_cookie(ExpirationTime, Data, FSessionKey, ServerKey)
+ when is_integer(ExpirationTime), is_function(FSessionKey)->
+ CookieData = generate_session_data(ExpirationTime, Data,
+ FSessionKey, ServerKey),
+ mochiweb_cookies:cookie("id", CookieData,
+ [{max_age, 20000},
+ {local_time,
+ calendar:universal_time_to_local_time(
+ calendar:universal_time())}]).
+
+%% TODO: This return type is messy to express in the type system.
+-spec check_session_cookie(
+ ECookie :: binary(),
+ ExpirationTime :: string(),
+ FSessionKey :: key_fun(),
+ ServerKey :: iolist()) ->
+ {Success :: boolean(),
+ ExpTimeAndData :: [integer() | binary()]}.
+check_session_cookie(ECookie, ExpirationTime, FSessionKey, ServerKey)
+ when is_binary(ECookie), is_integer(ExpirationTime),
+ is_function(FSessionKey) ->
+ case mochiweb_base64url:decode(ECookie) of
+ <<ExpirationTime1:32/integer, BHmac:20/binary, EData/binary>> ->
+ ETString = integer_to_list(ExpirationTime1),
+ Key = gen_key(ETString, ServerKey),
+ Data = decrypt_data(EData, Key),
+ Hmac2 = gen_hmac(ETString,
+ Data,
+ FSessionKey(ETString),
+ Key),
+ {ExpirationTime1 >= ExpirationTime andalso eq(Hmac2, BHmac),
+ [ExpirationTime1, binary_to_list(Data)]};
+ _ ->
+ {false, []}
+ end;
+check_session_cookie(_ECookie, _ExpirationTime, _FSessionKey, _ServerKey) ->
+ {false, []}.
+
+%% 'Constant' time =:= operator for binary, to mitigate timing attacks.
+-spec eq(binary(), binary()) -> boolean().
+eq(A, B) when is_binary(A) andalso is_binary(B) ->
+ eq(A, B, 0).
+
+eq(<<A, As/binary>>, <<B, Bs/binary>>, Acc) ->
+ eq(As, Bs, Acc bor (A bxor B));
+eq(<<>>, <<>>, 0) ->
+ true;
+eq(_As, _Bs, _Acc) ->
+ false.
+
+-spec ensure_binary(iolist()) -> binary().
+ensure_binary(B) when is_binary(B) ->
+ B;
+ensure_binary(L) when is_list(L) ->
+ iolist_to_binary(L).
+
+-spec encrypt_data(binary(), binary()) -> binary().
+encrypt_data(Data, Key) ->
+ IV = crypto:rand_bytes(16),
+ Crypt = crypto:aes_cfb_128_encrypt(Key, IV, Data),
+ <<IV/binary, Crypt/binary>>.
+
+-spec decrypt_data(binary(), binary()) -> binary().
+decrypt_data(<<IV:16/binary, Crypt/binary>>, Key) ->
+ crypto:aes_cfb_128_decrypt(Key, IV, Crypt).
+
+-spec gen_key(iolist(), iolist()) -> binary().
+gen_key(ExpirationTime, ServerKey)->
+ crypto:md5_mac(ServerKey, [ExpirationTime]).
+
+-spec gen_hmac(iolist(), binary(), iolist(), binary()) -> binary().
+gen_hmac(ExpirationTime, Data, SessionKey, Key) ->
+ crypto:sha_mac(Key, [ExpirationTime, Data, SessionKey]).
+
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+
+generate_check_session_cookie_test_() ->
+ {setup,
+ fun setup_server_key/0,
+ fun generate_check_session_cookie/1}.
+
+setup_server_key() ->
+ crypto:start(),
+ ["adfasdfasfs",30000].
+
+generate_check_session_cookie([ServerKey, TS]) ->
+ Id = fun (A) -> A end,
+ TSFuture = TS + 1000,
+ TSPast = TS - 1,
+ [?_assertEqual(
+ {true, [TSFuture, "alice"]},
+ check_session_cookie(
+ generate_session_data(TSFuture, "alice", Id, ServerKey),
+ TS, Id, ServerKey)),
+ ?_assertEqual(
+ {true, [TSFuture, "alice and"]},
+ check_session_cookie(
+ generate_session_data(TSFuture, "alice and", Id, ServerKey),
+ TS, Id, ServerKey)),
+ ?_assertEqual(
+ {true, [TSFuture, "alice and"]},
+ check_session_cookie(
+ generate_session_data(TSFuture, "alice and", Id, ServerKey),
+ TS, Id,ServerKey)),
+ ?_assertEqual(
+ {true, [TSFuture, "alice and bob"]},
+ check_session_cookie(
+ generate_session_data(TSFuture, "alice and bob",
+ Id, ServerKey),
+ TS, Id, ServerKey)),
+ ?_assertEqual(
+ {true, [TSFuture, "alice jlkjfkjsdfg sdkfjgldsjgl"]},
+ check_session_cookie(
+ generate_session_data(TSFuture, "alice jlkjfkjsdfg sdkfjgldsjgl",
+ Id, ServerKey),
+ TS, Id, ServerKey)),
+ ?_assertEqual(
+ {true, [TSFuture, "alice .'¡'ç+-$%/(&\""]},
+ check_session_cookie(
+ generate_session_data(TSFuture, "alice .'¡'ç+-$%/(&\""
+ ,Id, ServerKey),
+ TS, Id, ServerKey)),
+ ?_assertEqual(
+ {true,[TSFuture,"alice456689875"]},
+ check_session_cookie(
+ generate_session_data(TSFuture, ["alice","456689875"],
+ Id, ServerKey),
+ TS, Id, ServerKey)),
+ ?_assertError(
+ function_clause,
+ check_session_cookie(
+ generate_session_data(TSFuture, {tuple,one},
+ Id, ServerKey),
+ TS, Id,ServerKey)),
+ ?_assertEqual(
+ {false, [TSPast, "bob"]},
+ check_session_cookie(
+ generate_session_data(TSPast, "bob", Id,ServerKey),
+ TS, Id, ServerKey))
+ ].
+-endif.
11 src/mochiweb_util.erl
View
@@ -68,11 +68,17 @@ partition2(_S, _Sep) ->
%% @spec safe_relative_path(string()) -> string() | undefined
%% @doc Return the reduced version of a relative path or undefined if it
%% is not safe. safe relative paths can be joined with an absolute path
-%% and will result in a subdirectory of the absolute path.
+%% and will result in a subdirectory of the absolute path. Safe paths
+%% never contain a backslash character.
safe_relative_path("/" ++ _) ->
undefined;
safe_relative_path(P) ->
- safe_relative_path(P, []).
+ case string:chr(P, $\\) of
+ 0 ->
+ safe_relative_path(P, []);
+ _ ->
+ undefined
+ end.
safe_relative_path("", Acc) ->
case Acc of
@@ -815,6 +821,7 @@ safe_relative_path_test() ->
undefined = safe_relative_path("../foo"),
undefined = safe_relative_path("foo/../.."),
undefined = safe_relative_path("foo//"),
+ undefined = safe_relative_path("foo\\bar"),
ok.
parse_qvalues_test() ->