Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

R16 #1

Merged
merged 48 commits into from
@marutha
Owner

No description provided.

@Xorcerer

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

@benoitc

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
@pmundkur 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
@kmwang kmwang add ability to handle combined content-length header. 888ceb5
@etrepum etrepum Merge pull request #85 from pmundkur/fix-ranges
Fix a case in handling range headers
e282266
@kmwang kmwang amended get_combined_value. 53ee10b
@kmwang kmwang support parsing quoted string. ede9003
@doubleyou doubleyou Avoid using regular expressions 2def5f1
@doubleyou doubleyou Removed export_all 2ba8d24
@doubleyou doubleyou Merge pull request #88 from doubleyou/handling-combined-header
Handling combined header
20f2f00
@djnym 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
@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
@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
@melkote melkote Merge branch 'master' of git://github.com/melkote/mochiweb 3259a93
@etrepum etrepum Merge pull request #93 from melkote/master
Pull request for issue 92: Do not allow backslashes in path (windows security).
5ee1eeb
@etrepum etrepum Merge pull request #91 from djnym/R15B02_mochiweb_acceptor_crash
Fix for mochiweb_acceptor crash under R15B02
dcd1076
@etrepum 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
@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
@etrepum etrepum Merge pull request #95 from vinoski/drop-param-mods
use tuple modules instead of parameterized modules
2fb6dcc
@etrepum etrepum update CHANGES and bump vsn to 2.4.0 b02ea50
@etrepum etrepum #96 - mochifmt_records regression 22b770e
@lhft lhft Working on improvements suggested by doubleyou df5a881
@etrepum etrepum fix mochiweb_request regression #97 5a1b589
@huseyinyilmaz

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

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!

@marutha marutha merged commit 7f75cfb into marutha:master
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jul 28, 2012
  1. @etrepum
Commits on Oct 8, 2012
  1. @pmundkur

    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. @kmwang
Commits on Oct 12, 2012
  1. @etrepum

    Merge pull request #85 from pmundkur/fix-ranges

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

    amended get_combined_value.

    kmwang authored
Commits on Oct 18, 2012
  1. @kmwang

    support parsing quoted string.

    kmwang authored
Commits on Nov 6, 2012
  1. @doubleyou
  2. @doubleyou

    Removed export_all

    doubleyou authored
  3. @doubleyou

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

    doubleyou authored
    Handling combined header
Commits on Dec 13, 2012
  1. @djnym

    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. @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. @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. @melkote
Commits on Dec 15, 2012
  1. @etrepum

    Merge pull request #93 from melkote/master

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

    Merge pull request #91 from djnym/R15B02_mochiweb_acceptor_crash

    etrepum authored
    Fix for mochiweb_acceptor crash under R15B02
  3. @etrepum

    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. @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. @etrepum

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

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

    #96 - mochifmt_records regression

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

    Fixed the final notes

    lhft authored
Commits on Jan 30, 2013
  1. @etrepum

    tag v2.4.1

    etrepum authored
Commits on Feb 6, 2013
  1. @shkumagai
  2. @etrepum

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

    etrepum authored
    …regression
    
    fix mochiweb_response regression
  3. @etrepum

    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. @doubleyou

    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. @etrepum

    Merge pull request #102 from tsloughter/master

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

    travis R16B

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

    dialyzer fixes

    etrepum authored
Commits on Mar 15, 2013
  1. @etrepum
Commits on Mar 20, 2013
  1. @etrepum
This page is out of date. Refresh to see the latest.
View
6 .travis.yml
@@ -2,6 +2,6 @@ language: erlang
notifications:
email: false
otp_release:
- - R14B03
- - R14B02
- - R14B01
+ - R15B02
+ - R15B03
+ - R16B
View
39 CHANGES.md
@@ -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)
View
12 src/mochifmt_records.erl
@@ -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) ->
View
17 src/mochifmt_std.erl
@@ -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).
%%
View
2  src/mochiweb.app.src
@@ -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, []},
View
4 src/mochiweb_acceptor.erl
@@ -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);
View
83 src/mochiweb_base64url.erl
@@ -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.
View
123 src/mochiweb_headers.erl
@@ -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.
View
18 src/mochiweb_http.erl
@@ -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)),
View
2  src/mochiweb_multipart.erl
@@ -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>>,
View
349 src/mochiweb_request.erl
@@ -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 ->
View
30 src/mochiweb_request_tests.erl
@@ -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.
View
42 src/mochiweb_response.erl
@@ -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.
View
189 src/mochiweb_session.erl
@@ -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").