Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

update basho fork from main mochiweb repo

  • Loading branch information...
commit eadb14b94df088deca8eeec3655b660a534e8e79 2 parents 50695ad + e6d1870
@vinoski vinoski authored
Showing with 5,052 additions and 700 deletions.
  1. +9 −2 .gitignore
  2. +7 −0 .travis.yml
  3. +29 −0 CHANGES.md
  4. +19 −7 Makefile
  5. +16 −0 README
  6. +0 −42 ebin/mochiweb.app
  7. +206 −0 examples/hmac_api/README
  8. +43 −0 examples/hmac_api/hmac_api.hrl
  9. +34 −0 examples/hmac_api/hmac_api_client.erl
  10. +435 −0 examples/hmac_api/hmac_api_lib.erl
  11. +146 −0 examples/https/https_store.erl
  12. +19 −0 examples/https/server_cert.pem
  13. +27 −0 examples/https/server_key.pem
  14. +81 −0 examples/keepalive/keepalive.erl
  15. BIN  rebar
  16. +15 −3 rebar.config
  17. +45 −0 scripts/entities.erl
  18. +10 −14 scripts/new_mochiweb.erl
  19. +1 −1  src/mochifmt.erl
  20. +1 −1  src/mochifmt_records.erl
  21. +1 −1  src/mochifmt_std.erl
  22. +2 −2 src/mochiglobal.erl
  23. +1 −1  src/mochihex.erl
  24. +1 −1  src/mochijson.erl
  25. +131 −22 src/mochijson2.erl
  26. +1 −1  src/mochilists.erl
  27. +4 −4 src/mochilogfile2.erl
  28. +51 −28 src/mochinum.erl
  29. +2 −1  src/mochitemp.erl
  30. +15 −14 src/mochiutf8.erl
  31. +9 −0 src/mochiweb.app.src
  32. +19 −27 src/mochiweb.erl
  33. +4 −2 src/mochiweb_acceptor.erl
  34. +2,131 −256 src/mochiweb_charref.erl
  35. +20 −5 src/mochiweb_cookies.erl
  36. +1 −1  src/mochiweb_cover.erl
  37. +9 −6 src/mochiweb_echo.erl
  38. +3 −3 src/mochiweb_headers.erl
  39. +216 −11 src/mochiweb_html.erl
  40. +52 −78 src/mochiweb_http.erl
  41. +1 −4 src/mochiweb_io.erl
  42. +360 −39 src/mochiweb_mime.erl
  43. +64 −16 src/mochiweb_multipart.erl
  44. +141 −22 src/mochiweb_request.erl
  45. +152 −0 src/mochiweb_request_tests.erl
  46. +1 −1  src/mochiweb_response.erl
  47. +129 −53 src/mochiweb_socket_server.erl
  48. +68 −29 src/mochiweb_util.erl
  49. +2 −2 src/reloader.erl
  50. +5 −0 support/templates/.gitignore
  51. +22 −0 support/templates/mochiwebapp.template
  52. +8 −0 support/templates/mochiwebapp_skel/priv/www/index.html
  53. +7 −0 support/templates/mochiwebapp_skel/rebar.config
  54. +9 −0 support/templates/mochiwebapp_skel/src/mochiapp.app.src
  55. +30 −0 support/templates/mochiwebapp_skel/src/mochiapp.erl
  56. +22 −0 support/templates/mochiwebapp_skel/src/mochiapp_app.erl
  57. +84 −0 support/templates/mochiwebapp_skel/src/mochiapp_deps.erl
  58. +56 −0 support/templates/mochiwebapp_skel/src/mochiapp_sup.erl
  59. +69 −0 support/templates/mochiwebapp_skel/src/mochiapp_web.erl
  60. +6 −0 support/templates/mochiwebapp_skel/start-dev.sh
View
11 .gitignore
@@ -1,2 +1,9 @@
-ebin
-.eunit
+/ebin
+/doc
+/_test
+/.eunit
+/docs
+.DS_Store
+/TEST-*.xml
+/deps
+*.swp
View
7 .travis.yml
@@ -0,0 +1,7 @@
+language: erlang
+notifications:
+ email: false
+otp_release:
+ - R14B03
+ - R14B02
+ - R14B01
View
29 CHANGES.md
@@ -0,0 +1,29 @@
+Version 2.3.0 released 2011-10-14
+
+* Handle ssl_closed message in mochiweb_http (#59)
+* Added support for new MIME types (otf, eot, m4v, svg, svgz, ttc, ttf,
+ vcf, webm, webp, woff) (#61)
+* Updated mochiweb_charref to support all HTML5 entities. Note that
+ if you are using this module directly, the spec has changed to return
+ `[integer()]` for some entities. (#64)
+
+Version 2.2.1 released 2011-08-31
+
+* Removed `mochiweb_skel` module from the pre-rebar era
+
+Version 2.2.0 released 2011-08-29
+
+* Added new `mochiweb_http:start_link/1` and
+ `mochiweb_socket_server:start_link/1` APIs to explicitly start linked
+ servers. Also added `{link, false}` option to the `start/1` variants
+ to explicitly start unlinked. This is in expectation that we will
+ eventually change the default behavior of `start/1` to be unlinked as you
+ would expect it to. See https://github.com/mochi/mochiweb/issues/58 for
+ discussion.
+
+Version 2.1.0 released 2011-08-29
+
+* Added new `mochijson2:decode/2` with `{format, struct | proplist | eep18}`
+ options for easy decoding to various proplist formats. Also added encoding
+ support for eep18 style objects.
+
View
26 Makefile
@@ -1,16 +1,28 @@
-all: compile
-compile:
- ./rebar compile
+PREFIX:=../
+DEST:=$(PREFIX)$(PROJECT)
+
+REBAR=./rebar
+
+all:
+ @$(REBAR) get-deps compile
edoc:
- ./rebar doc
+ @$(REBAR) doc
test:
- ./rebar eunit
+ @rm -rf .eunit
+ @mkdir -p .eunit
+ @$(REBAR) skip_deps=true eunit
clean:
- ./rebar clean
+ @$(REBAR) clean
+
+build_plt:
+ @$(REBAR) build-plt
dialyzer:
- ./rebar analyze
+ @$(REBAR) dialyze
+
+app:
+ @$(REBAR) create template=mochiwebapp dest=$(DEST) appid=$(PROJECT)
View
16 README
@@ -1 +1,17 @@
MochiWeb is an Erlang library for building lightweight HTTP servers.
+
+The latest version of MochiWeb is available at http://github.com/mochi/mochiweb
+
+The mailing list for MochiWeb is at http://groups.google.com/group/mochiweb/
+
+R12B compatibility:
+The master of MochiWeb is tested with R14A and later. A branch compatible
+with R12B is maintained separately at http://github.com/lemenkov/mochiweb
+The R12B branch of that repository is mirrored in the official repository
+occasionally for convenience.
+
+To create a new mochiweb using project:
+ make app PROJECT=project_name
+
+To create a new mochiweb using project in a specific directory:
+ make app PROJECT=project_name PREFIX=$HOME/projects/
View
42 ebin/mochiweb.app
@@ -1,42 +0,0 @@
-{application, mochiweb,
- [{description, "MochiMedia Web Server"},
- {vsn, "1.7.1"},
- {modules, [
- mochifmt,
- mochiglobal,
- mochihex,
- mochijson,
- mochijson2,
- mochilists,
- mochilogfile2,
- mochinum,
- mochitemp,
- mochiutf8,
- mochiweb,
- mochiweb_acceptor,
- mochiweb_app,
- mochiweb_charref,
- mochiweb_cookies,
- mochiweb_cover,
- mochiweb_echo,
- mochiweb_headers,
- mochiweb_io,
- mochiweb_html,
- mochiweb_http,
- mochiweb_mime,
- mochiweb_multipart,
- mochiweb_request,
- mochiweb_response,
- mochiweb_skel,
- mochiweb_socket,
- mochiweb_socket_server,
- mochiweb_sup,
- mochiweb_util,
- reloader,
- mochifmt_std,
- mochifmt_records
- ]},
- {registered, []},
- {mod, {mochiweb_app, []}},
- {env, []},
- {applications, [kernel, stdlib]}]}.
View
206 examples/hmac_api/README
@@ -0,0 +1,206 @@
+Introduction
+------------
+
+This example shows how to make an Amazon-style HMAC authentication system for an API with mochiweb.
+
+Purpose
+-------
+
+The purpose of this example is to:
+* make it easy to implement an API in mochiweb
+ - using a proven approach so that 'amateurs' don't have to reinvent crypto
+* make it easy to generate client libraries for that API so that client-side implementers can:
+ - reuse closely related code examples
+ - build compatibility unit tests instead of fiddling around debugging their library against live implementations of the system
+
+Scope
+-----
+
+The scope of this document is:
+* a description of the client-server exchange
+* a reference implementation of
+ - the server-side implementation of the exchange
+ - the client-side implementation of the exchange
+* developing a custom implementation of an API
+* deploying that implementation to new client-side users to build their client libraries
+
+Contents
+--------
+
+Subsequent sections of this document are:
+* the client-server exchange
+* the reference implementation in this example
+* building a custom implementation
+* deploying a custom implementation
+
+The Client-Server Exchange
+--------------------------
+
+OVERVIEW
+
+This section describes the client-server exchange for an Amazon-style API authentication schema. It has the following characteristics:
+* based on a public key/private key
+* used to authenticate non-SSL api requests
+* not a full once-use schema and is vulnerable to replay attacks within a short time window
+
+TYPE OF API
+
+The api described in this document is:
+* suitable for machine-machine communication
+
+The api described in this document is NOT:
+* an implementation of 2-legged OAUTH
+ - see https://github.com/tim/erlang-oauth
+* an implementation of 3-legged OAUTH
+
+It is not suitable for use in applications where an end user has to log into a service and piggy-back on top of a keypair security system.
+
+THE CLIENT LIBRARY HERE IS **NOT** AN AMAZON CLIENT LIBRARY. AMAZON DOES FUNKY STUFF WITH HOSTNAMES AND PUSHES THEM ONTO THE URL IN CANONICALIZATION! THE CLIENT LIBRARY IS AMAZON-A-LIKE ENOUGH TO USE THE AMAZON DOCOS TO BUILD A TEST SUITE.
+
+STEP 1
+
+The client is issued with a pair of keys, one public, one private, for example:
+* public: "bcaa49f2a4f7d4f92ac36c8bf66d5bb6"
+* private: "92bc93d6b8aaec1cde772f903e06daf5"
+
+In the Amazon docs these are referred to as:
+* AWSAccessKeyId (public)
+* AWSSecretAccessKey (private)
+
+These can be generated by the function:
+hmac_api_lib:get_api_keypair/0
+
+This function returns cryptographically strong random numbers using the openSSL crypto library under the covers.
+
+The public key is used as a declaration of identity, "I am bcaa49..."
+
+The private key is never passed over the wire and is used to construct the same hash on both the client- and the server-side.
+
+STEP 2
+
+The client prepares their request:
+* url
+* time of request
+* action (GET, POST, etc)
+* type of request (application/json, etc)
+* contents of request
+* etc, etc
+
+These components are then turned into a string called the canonical form.
+
+The HTTP protocol is permissive; it treats different requests as if they were the same. For instance it doesn't care about the order in which headers are sent, and allows the same header to contain multiple values as a list or be specified multiple times as a key-value pair.
+
+Intermediate machines between the client and server MAY pack and repack the HTTP request as long as they don't alter its meaning in a narrow sense. This means that the format of the HTTP request is not guaranteed to be maintained.
+
+The canonical form simply ensures that all the valid ways of making the same request are represented by the same string - irrespective of how this is done.
+
+The canonical form handles POST bodies and query parameters and silently discards anchors in URL's.
+
+A hash of this string is made with the private key.
+
+STEP 3
+
+The client makes the request to the server:
+* the signature is included in the request in the standard HTTPAuthorization header. (As the Amazon documentation points out this is infelicitous as it is being used for Authentication not Authorization, but hey!).
+
+The Authorization header constructed has the form:
+<schema name><space><public key><colon><signature>
+
+An Amazon one looks like:
+Authorization: AWS 0PN5J17HBGZHT7JJ3X82:frJIUN8DYpKDtOLCwo//yllqDzg=
+ --- -------------------- ----------------------------
+ sch public key signature
+
+The HTTP request is made.
+
+STEP 4
+
+The request is processed:
+* the server receives the request
+* the server constructs the canonical form from the attributes of the request:
+ - url
+ - date header
+ - action (GET, POST, etc)
+ - content type of request (application/json, etc)
+ - some custom headers
+ - etc, etc
+* the server takes the client's public key from the HTTPAuthorization header and looks up the client's private key
+* the server signs the canonical form with the private key
+* the server compares:
+ - the signature in the request to the signature it has just generated
+ - the time encoded in the request with the server time
+* the request is accepted or denied
+
+The time comparison is 'fuzzy'. Different server's clocks will be out of sync to a degree, the request may have acquired a time from an intermediate machine along the way, etc, etc. Normally a 'clock skew' time is allowed - in Amazon's case this is 15 minutes.
+
+NOTA BENE: THIS CLOCK SKEW TIME ALLOWS FOR REPLAY ATTACKS WHERE A BAD GUY SIMPLY CAPTURES AND REPLAYS TRAFFIC.
+
+EXTENSION
+
+It is possible to extend this schema to prevent replay attacks. The server issues a nonce token (a random string) which is included in the signature. When the server authorizes the request it stores the token and prevents any request with that token (ie a replay) being authorized again.
+
+The client receives its next nonce token in the response to a successful request.
+
+The Reference Implementation In This Example
+--------------------------------------------
+
+The reference implementation used in this example is that described in the Amazon documentation here:
+http://docs.amazonwebservices.com/AmazonS3/latest/dev/index.html?RESTAuthentication.html
+
+The try out the reference implementation:
+* create a new mochiweb project as per the mochiweb README
+ - make app PROJECT=project_name
+* copy hmac_api_lib.erl and hmac_api_client.erl into project_name/src
+* copy hmac_api.hrl into project_name/include
+* edit project_name_web.erl and add a call to hmac_api_lib:authorize_request/1
+
+authorize/request/1 should be called in the loop of project_name_web.erl as per:
+
+ loop(Req, DocRoot) ->
+ Auth = hmac_api_lib:authorize_request(Req),
+ io:format("Auth is ~p~n", [Auth]),
+ "/" ++ Path = Req:get(path),
+ ...
+
+When this is done you are ready to test the api:
+* run 'make' in project_name/ to build the Erlang
+* start the web server with 'start-dev.sh' in project_name/ (this will also open an Erlang shell to the Erlang VM)
+
+To test the api run this command in the Erlang shell:
+* hmac_api_client:fire().
+
+The reference implementation uses 5 constants defined in hmac_api.hrl.
+* schema
+* headerprefix
+* dateheader
+* publickey
+* privatekey
+
+Building A Custom Implementation
+--------------------------------
+
+The simplest custom implementation is to simply take the existing code and change the values of the following constants:
+* schema
+* headerprefix
+* dateheader
+
+If the API is to be used 'as is', please use the values which are commented out in hmac_api.hrl. This will make easier for software developers to work out which version of which client-side libraries they can use.
+
+Client libraries written in other languages than Erlang can reemployment the test suite in hmac_api_lib.erl.
+
+More sophisticated changes will involve changes to the canonicalization functions.
+
+Use of a generic schema should make reuse of client libraries easier across different platforms.
+
+If you develop an ‘as-is’ client-side library in another language please consider submitting its code to this example.
+
+Deploying A Custom Implementation
+---------------------------------
+
+When deploying a custom implementation, the server-side code should be released with unit tests so the client-side developer can easily build a robust client.
+
+In addition to that you will need to specify:
+* description of how the API works:
+ - ie the acceptable methods and urls
+ - custom headers and their usage (if appropriate)
+
View
43 examples/hmac_api/hmac_api.hrl
@@ -0,0 +1,43 @@
+-author("Hypernumbers Ltd <gordon@hypernumbers.com>").
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% %%%
+%%% Reference values for testing against Amazon documents %%%
+%%% %%%
+%%% These need to be changed in production! %%%
+%%% %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+-define(schema, "AWS").
+%% defines the prefix for headers to be included in the signature
+-define(headerprefix, "x-amz-").
+%% defines the date header
+-define(dateheader, "x-amz-date").
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% %%%
+%%% Default values for defining a generic API %%%
+%%% %%%
+%%% Only change these if you alter the canonicalisation %%%
+%%% %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%-define(schema, "MOCHIAPI").
+%%-define(headerprefix, "x-mochiapi-").
+%%-define(dateheader, "x-mochiapi-date").
+
+%% a couple of keys for testing
+%% these are taken from the document
+%% % http://docs.amazonwebservices.com/AmazonS3/latest/dev/index.html?RESTAuthentication.html
+%% they are not valid keys!
+-define(publickey, "0PN5J17HBGZHT7JJ3X82").
+-define(privatekey, "uV3F3YluFJax1cknvbcGwgjvx4QpvB+leU8dUj2o").
+
+
+-record(hmac_signature,
+ {
+ method,
+ contentmd5,
+ contenttype,
+ date,
+ headers,
+ resource
+ }).
View
34 examples/hmac_api/hmac_api_client.erl
@@ -0,0 +1,34 @@
+-module(hmac_api_client).
+
+-export([
+ fire/0
+ ]).
+
+-include("hmac_api.hrl").
+-author("Hypernumbers Ltd <gordon@hypernumbers.com>").
+
+fire() ->
+ URL = "http://127.0.0.1:8080/some/page/yeah/",
+ %% Dates SHOULD conform to Section 3.3 of RFC2616
+ %% the examples from the RFC are:
+ %% Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123
+ %% Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036
+ %% Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format
+
+ %% Dates can be conveniently generated using dh_date.erl
+ %% https://github.com/daleharvey/dh_date
+ %% which is largely compatible with
+ %% http://uk.php.net/date
+
+ %% You MIGHT find it convenient to insist on times in UTC only
+ %% as it reduces the errors caused by summer time and other
+ %% conversion issues
+ Method = post,
+ Headers = [{"content-type", "application/json"},
+ {"date", "Sun, 10 Jul 2011 05:07:19"}],
+ ContentType = "application/json",
+ Body = "blah",
+ HTTPAuthHeader = hmac_api_lib:sign(?privatekey, Method, URL,
+ Headers, ContentType),
+ httpc:request(Method, {URL, [HTTPAuthHeader | Headers],
+ ContentType, Body}, [], []).
View
435 examples/hmac_api/hmac_api_lib.erl
@@ -0,0 +1,435 @@
+-module(hmac_api_lib).
+
+-include("hmac_api.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+-author("Hypernumbers Ltd <gordon@hypernumbers.com>").
+
+%%% this library supports the hmac_sha api on both the client-side
+%%% AND the server-side
+%%%
+%%% sign/5 is used client-side to sign a request
+%%% - it returns an HTTPAuthorization header
+%%%
+%%% authorize_request/1 takes a mochiweb Request as an arguement
+%%% and checks that the request matches the signature
+%%%
+%%% get_api_keypair/0 creates a pair of public/private keys
+%%%
+%%% THIS LIB DOESN'T IMPLEMENT THE AMAZON API IT ONLY IMPLEMENTS
+%%% ENOUGH OF IT TO GENERATE A TEST SUITE.
+%%%
+%%% THE AMAZON API MUNGES HOSTNAME AND PATHS IN A CUSTOM WAY
+%%% THIS IMPLEMENTATION DOESN'T
+-export([
+ authorize_request/1,
+ sign/5,
+ get_api_keypair/0
+ ]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% %%%
+%%% API %%%
+%%% %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+authorize_request(Req) ->
+ Method = Req:get(method),
+ Path = Req:get(path),
+ Headers = normalise(mochiweb_headers:to_list(Req:get(headers))),
+ ContentMD5 = get_header(Headers, "content-md5"),
+ ContentType = get_header(Headers, "content-type"),
+ Date = get_header(Headers, "date"),
+ IncAuth = get_header(Headers, "authorization"),
+ {_Schema, _PublicKey, _Sig} = breakout(IncAuth),
+ %% normally you would use the public key to look up the private key
+ PrivateKey = ?privatekey,
+ Signature = #hmac_signature{method = Method,
+ contentmd5 = ContentMD5,
+ contenttype = ContentType,
+ date = Date,
+ headers = Headers,
+ resource = Path},
+ Signed = sign_data(PrivateKey, Signature),
+ {_, AuthHeader} = make_HTTPAuth_header(Signed),
+ case AuthHeader of
+ IncAuth -> "match";
+ _ -> "no_match"
+ end.
+
+sign(PrivateKey, Method, URL, Headers, ContentType) ->
+ Headers2 = normalise(Headers),
+ ContentMD5 = get_header(Headers2, "content-md5"),
+ Date = get_header(Headers2, "date"),
+ Signature = #hmac_signature{method = Method,
+ contentmd5 = ContentMD5,
+ contenttype = ContentType,
+ date = Date,
+ headers = Headers,
+ resource = URL},
+ SignedSig = sign_data(PrivateKey, Signature),
+ make_HTTPAuth_header(SignedSig).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% %%%
+%%% Internal Functions %%%
+%%% %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+breakout(Header) ->
+ [Schema, Tail] = string:tokens(Header, " "),
+ [PublicKey, Signature] = string:tokens(Tail, ":"),
+ {Schema, PublicKey, Signature}.
+
+get_api_keypair() ->
+ Public = mochihex:to_hex(binary_to_list(crypto:strong_rand_bytes(16))),
+ Private = mochihex:to_hex(binary_to_list(crypto:strong_rand_bytes(16))),
+ {Public, Private}.
+
+make_HTTPAuth_header(Signature) ->
+ {"Authorization", ?schema ++ " "
+ ++ ?publickey ++ ":" ++ Signature}.
+
+make_signature_string(#hmac_signature{} = S) ->
+ Date = get_date(S#hmac_signature.headers, S#hmac_signature.date),
+ string:to_upper(atom_to_list(S#hmac_signature.method)) ++ "\n"
+ ++ S#hmac_signature.contentmd5 ++ "\n"
+ ++ S#hmac_signature.contenttype ++ "\n"
+ ++ Date ++ "\n"
+ ++ canonicalise_headers(S#hmac_signature.headers)
+ ++ canonicalise_resource(S#hmac_signature.resource).
+
+sign_data(PrivateKey, #hmac_signature{} = Signature) ->
+ Str = make_signature_string(Signature),
+ sign2(PrivateKey, Str).
+
+%% this fn is the entry point for a unit test which is why it is broken out...
+%% if yer encryption and utf8 and base45 doo-dahs don't work then
+%% yer Donald is well and truly Ducked so ye may as weel test it...
+sign2(PrivateKey, Str) ->
+ Sign = xmerl_ucs:to_utf8(Str),
+ binary_to_list(base64:encode(crypto:sha_mac(PrivateKey, Sign))).
+
+canonicalise_headers([]) -> "\n";
+canonicalise_headers(List) when is_list(List) ->
+ List2 = [{string:to_lower(K), V} || {K, V} <- lists:sort(List)],
+ c_headers2(consolidate(List2, []), []).
+
+c_headers2([], Acc) -> string:join(Acc, "\n") ++ "\n";
+c_headers2([{?headerprefix ++ Rest, Key} | T], Acc) ->
+ Hd = string:strip(?headerprefix ++ Rest) ++ ":" ++ string:strip(Key),
+ c_headers2(T, [Hd | Acc]);
+c_headers2([_H | T], Acc) -> c_headers2(T, Acc).
+
+consolidate([H | []], Acc) -> [H | Acc];
+consolidate([{H, K1}, {H, K2} | Rest], Acc) ->
+ consolidate([{H, join(K1, K2)} | Rest], Acc);
+consolidate([{H1, K1}, {H2, K2} | Rest], Acc) ->
+ consolidate([{rectify(H2), rectify(K2)} | Rest], [{H1, K1} | Acc]).
+
+join(A, B) -> string:strip(A) ++ ";" ++ string:strip(B).
+
+%% removes line spacing as per RFC 2616 Section 4.2
+rectify(String) ->
+ Re = "[\x20* | \t*]+",
+ re:replace(String, Re, " ", [{return, list}, global]).
+
+canonicalise_resource("http://" ++ Rest) -> c_res2(Rest);
+canonicalise_resource("https://" ++ Rest) -> c_res2(Rest);
+canonicalise_resource(X) -> c_res3(X).
+
+c_res2(Rest) ->
+ N = string:str(Rest, "/"),
+ {_, Tail} = lists:split(N, Rest),
+ c_res3("/" ++ Tail).
+
+c_res3(Tail) ->
+ URL = case string:str(Tail, "#") of
+ 0 -> Tail;
+ N -> {U, _Anchor} = lists:split(N, Tail),
+ U
+ end,
+ U3 = case string:str(URL, "?") of
+ 0 -> URL;
+ N2 -> {U2, Q} = lists:split(N2, URL),
+ U2 ++ canonicalise_query(Q)
+ end,
+ string:to_lower(U3).
+
+canonicalise_query(List) ->
+ List1 = string:to_lower(List),
+ List2 = string:tokens(List1, "&"),
+ string:join(lists:sort(List2), "&").
+
+%% if there's a header date take it and ditch the date
+get_date([], Date) -> Date;
+get_date([{K, _V} | T], Date) -> case string:to_lower(K) of
+ ?dateheader -> [];
+ _ -> get_date(T, Date)
+ end.
+
+normalise(List) -> norm2(List, []).
+
+norm2([], Acc) -> Acc;
+norm2([{K, V} | T], Acc) when is_atom(K) ->
+ norm2(T, [{string:to_lower(atom_to_list(K)), V} | Acc]);
+norm2([H | T], Acc) -> norm2(T, [H | Acc]).
+
+get_header(Headers, Type) ->
+ case lists:keyfind(Type, 1, Headers) of
+ false -> [];
+ {_K, V} -> V
+ end.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% %%%
+%%% Unit Tests %%%
+%%% %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+ % taken from Amazon docs
+%% http://docs.amazonwebservices.com/AmazonS3/latest/dev/index.html?RESTAuthentication.html
+hash_test1(_) ->
+ Sig = "DELETE\n\n\n\nx-amz-date:Tue, 27 Mar 2007 21:20:26 +0000\n/johnsmith/photos/puppy.jpg",
+ Key = ?privatekey,
+ Hash = sign2(Key, Sig),
+ Expected = "k3nL7gH3+PadhTEVn5Ip83xlYzk=",
+ ?assertEqual(Expected, Hash).
+
+%% taken from Amazon docs
+%% http://docs.amazonwebservices.com/AmazonS3/latest/dev/index.html?RESTAuthentication.html
+hash_test2(_) ->
+ Sig = "GET\n\n\nTue, 27 Mar 2007 19:44:46 +0000\n/johnsmith/?acl",
+ Key = "uV3F3YluFJax1cknvbcGwgjvx4QpvB+leU8dUj2o",
+ Hash = sign2(Key, Sig),
+ Expected = "thdUi9VAkzhkniLj96JIrOPGi0g=",
+ ?assertEqual(Expected, Hash).
+
+%% taken from Amazon docs
+%% http://docs.amazonwebservices.com/AmazonS3/latest/dev/index.html?RESTAuthentication.html
+hash_test3(_) ->
+ Sig = "GET\n\n\nWed, 28 Mar 2007 01:49:49 +0000\n/dictionary/"
+ ++ "fran%C3%A7ais/pr%c3%a9f%c3%a8re",
+ Key = "uV3F3YluFJax1cknvbcGwgjvx4QpvB+leU8dUj2o",
+ Hash = sign2(Key, Sig),
+ Expected = "dxhSBHoI6eVSPcXJqEghlUzZMnY=",
+ ?assertEqual(Expected, Hash).
+
+signature_test1(_) ->
+ URL = "http://example.com:90/tongs/ya/bas",
+ Method = post,
+ ContentMD5 = "",
+ ContentType = "",
+ Date = "Sun, 10 Jul 2011 05:07:19 UTC",
+ Headers = [],
+ Signature = #hmac_signature{method = Method,
+ contentmd5 = ContentMD5,
+ contenttype = ContentType,
+ date = Date,
+ headers = Headers,
+ resource = URL},
+ Sig = make_signature_string(Signature),
+ Expected = "POST\n\n\nSun, 10 Jul 2011 05:07:19 UTC\n\n/tongs/ya/bas",
+ ?assertEqual(Expected, Sig).
+
+signature_test2(_) ->
+ URL = "http://example.com:90/tongs/ya/bas",
+ Method = get,
+ ContentMD5 = "",
+ ContentType = "",
+ Date = "Sun, 10 Jul 2011 05:07:19 UTC",
+ Headers = [{"x-amz-acl", "public-read"}],
+ Signature = #hmac_signature{method = Method,
+ contentmd5 = ContentMD5,
+ contenttype = ContentType,
+ date = Date,
+ headers = Headers,
+ resource = URL},
+ Sig = make_signature_string(Signature),
+ Expected = "GET\n\n\nSun, 10 Jul 2011 05:07:19 UTC\nx-amz-acl:public-read\n/tongs/ya/bas",
+ ?assertEqual(Expected, Sig).
+
+signature_test3(_) ->
+ URL = "http://example.com:90/tongs/ya/bas",
+ Method = get,
+ ContentMD5 = "",
+ ContentType = "",
+ Date = "Sun, 10 Jul 2011 05:07:19 UTC",
+ Headers = [{"x-amz-acl", "public-read"},
+ {"yantze", "blast-off"},
+ {"x-amz-doobie", "bongwater"},
+ {"x-amz-acl", "public-write"}],
+ Signature = #hmac_signature{method = Method,
+ contentmd5 = ContentMD5,
+ contenttype = ContentType,
+ date = Date,
+ headers = Headers,
+ resource = URL},
+ Sig = make_signature_string(Signature),
+ Expected = "GET\n\n\nSun, 10 Jul 2011 05:07:19 UTC\nx-amz-acl:public-read;public-write\nx-amz-doobie:bongwater\n/tongs/ya/bas",
+ ?assertEqual(Expected, Sig).
+
+signature_test4(_) ->
+ URL = "http://example.com:90/tongs/ya/bas",
+ Method = get,
+ ContentMD5 = "",
+ ContentType = "",
+ Date = "Sun, 10 Jul 2011 05:07:19 UTC",
+ Headers = [{"x-amz-acl", "public-read"},
+ {"yantze", "blast-off"},
+ {"x-amz-doobie oobie \t boobie ", "bongwater"},
+ {"x-amz-acl", "public-write"}],
+ Signature = #hmac_signature{method = Method,
+ contentmd5 = ContentMD5,
+ contenttype = ContentType,
+ date = Date,
+ headers = Headers,
+ resource = URL},
+ Sig = make_signature_string(Signature),
+ Expected = "GET\n\n\nSun, 10 Jul 2011 05:07:19 UTC\nx-amz-acl:public-read;public-write\nx-amz-doobie oobie boobie:bongwater\n/tongs/ya/bas",
+ ?assertEqual(Expected, Sig).
+
+signature_test5(_) ->
+ URL = "http://example.com:90/tongs/ya/bas",
+ Method = get,
+ ContentMD5 = "",
+ ContentType = "",
+ Date = "Sun, 10 Jul 2011 05:07:19 UTC",
+ Headers = [{"x-amz-acl", "public-Read"},
+ {"yantze", "Blast-Off"},
+ {"x-amz-doobie Oobie \t boobie ", "bongwater"},
+ {"x-amz-acl", "public-write"}],
+ Signature = #hmac_signature{method = Method,
+ contentmd5 = ContentMD5,
+ contenttype = ContentType,
+ date = Date,
+ headers = Headers,
+ resource = URL},
+ Sig = make_signature_string(Signature),
+ Expected = "GET\n\n\nSun, 10 Jul 2011 05:07:19 UTC\nx-amz-acl:public-Read;public-write\nx-amz-doobie oobie boobie:bongwater\n/tongs/ya/bas",
+ ?assertEqual(Expected, Sig).
+
+signature_test6(_) ->
+ URL = "http://example.com:90/tongs/ya/bas/?andy&zbish=bash&bosh=burp",
+ Method = get,
+ ContentMD5 = "",
+ ContentType = "",
+ Date = "Sun, 10 Jul 2011 05:07:19 UTC",
+ Headers = [],
+ Signature = #hmac_signature{method = Method,
+ contentmd5 = ContentMD5,
+ contenttype = ContentType,
+ date = Date,
+ headers = Headers,
+ resource = URL},
+ Sig = make_signature_string(Signature),
+ Expected = "GET\n\n\nSun, 10 Jul 2011 05:07:19 UTC\n\n"
+ ++ "/tongs/ya/bas/?andy&bosh=burp&zbish=bash",
+ ?assertEqual(Expected, Sig).
+
+signature_test7(_) ->
+ URL = "http://exAMPLE.Com:90/tONgs/ya/bas/?ANdy&ZBish=Bash&bOsh=burp",
+ Method = get,
+ ContentMD5 = "",
+ ContentType = "",
+ Date = "Sun, 10 Jul 2011 05:07:19 UTC",
+ Headers = [],
+ Signature = #hmac_signature{method = Method,
+ contentmd5 = ContentMD5,
+ contenttype = ContentType,
+ date = Date,
+ headers = Headers,
+ resource = URL},
+ Sig = make_signature_string(Signature),
+ Expected = "GET\n\n\nSun, 10 Jul 2011 05:07:19 UTC\n\n"
+ ++"/tongs/ya/bas/?andy&bosh=burp&zbish=bash",
+ ?assertEqual(Expected, Sig).
+
+signature_test8(_) ->
+ URL = "http://exAMPLE.Com:90/tONgs/ya/bas/?ANdy&ZBish=Bash&bOsh=burp",
+ Method = get,
+ ContentMD5 = "",
+ ContentType = "",
+ Date = "",
+ Headers = [{"x-aMz-daTe", "Tue, 27 Mar 2007 21:20:26 +0000"}],
+ Signature = #hmac_signature{method = Method,
+ contentmd5 = ContentMD5,
+ contenttype = ContentType,
+ date = Date,
+ headers = Headers,
+ resource = URL},
+ Sig = make_signature_string(Signature),
+ Expected = "GET\n\n\n\n"
+ ++"x-amz-date:Tue, 27 Mar 2007 21:20:26 +0000\n"
+ ++"/tongs/ya/bas/?andy&bosh=burp&zbish=bash",
+ ?assertEqual(Expected, Sig).
+
+signature_test9(_) ->
+ URL = "http://exAMPLE.Com:90/tONgs/ya/bas/?ANdy&ZBish=Bash&bOsh=burp",
+ Method = get,
+ ContentMD5 = "",
+ ContentType = "",
+ Date = "Sun, 10 Jul 2011 05:07:19 UTC",
+ Headers = [{"x-amz-date", "Tue, 27 Mar 2007 21:20:26 +0000"}],
+ Signature = #hmac_signature{method = Method,
+ contentmd5 = ContentMD5,
+ contenttype = ContentType,
+ date = Date,
+ headers = Headers,
+ resource = URL},
+ Sig = make_signature_string(Signature),
+ Expected = "GET\n\n\n\n"
+ ++"x-amz-date:Tue, 27 Mar 2007 21:20:26 +0000\n"
+ ++"/tongs/ya/bas/?andy&bosh=burp&zbish=bash",
+ ?assertEqual(Expected, Sig).
+
+amazon_test1(_) ->
+ URL = "http://exAMPLE.Com:90/johnsmith/photos/puppy.jpg",
+ Method = delete,
+ ContentMD5 = "",
+ ContentType = "",
+ Date = "",
+ Headers = [{"x-amz-date", "Tue, 27 Mar 2007 21:20:26 +0000"}],
+ Signature = #hmac_signature{method = Method,
+ contentmd5 = ContentMD5,
+ contenttype = ContentType,
+ date = Date,
+ headers = Headers,
+ resource = URL},
+ Sig = sign_data(?privatekey, Signature),
+ Expected = "k3nL7gH3+PadhTEVn5Ip83xlYzk=",
+ ?assertEqual(Expected, Sig).
+
+unit_test_() ->
+ Setup = fun() -> ok end,
+ Cleanup = fun(_) -> ok end,
+
+ Series1 = [
+ fun hash_test1/1,
+ fun hash_test2/1,
+ fun hash_test3/1
+ ],
+
+ Series2 = [
+ fun signature_test1/1,
+ fun signature_test2/1,
+ fun signature_test3/1,
+ fun signature_test4/1,
+ fun signature_test5/1,
+ fun signature_test6/1,
+ fun signature_test7/1,
+ fun signature_test8/1,
+ fun signature_test9/1
+ ],
+
+ Series3 = [
+ fun amazon_test1/1
+ ],
+
+ {setup, Setup, Cleanup, [
+ {with, [], Series1},
+ {with, [], Series2},
+ {with, [], Series3}
+ ]}.
View
146 examples/https/https_store.erl
@@ -0,0 +1,146 @@
+
+%% Trivial web storage app. It's available over both HTTP (port 8442)
+%% and HTTPS (port 8443). You use a PUT to store items, a GET to
+%% retrieve them and DELETE to delete them. The HTTP POST method is
+%% invalid for this application. Example (using HTTPS transport):
+%%
+%% $ curl -k --verbose https://localhost:8443/flintstones
+%% ...
+%% 404 Not Found
+%% ...
+%% $ echo -e "Fred\nWilma\nBarney" |
+%% curl -k --verbose https://localhost:8443/flintstones \
+%% -X PUT -H "Content-Type: text/plain" --data-binary @-
+%% ...
+%% 201 Created
+%% ...
+%% $ curl -k --verbose https://localhost:8443/flintstones
+%% ...
+%% Fred
+%% Wilma
+%% Barney
+%% ...
+%% $ curl -k --verbose https://localhost:8443/flintstones -X DELETE
+%% ...
+%% 200 OK
+%% ...
+%% $ curl -k --verbose https://localhost:8443/flintstones
+%% ...
+%% 404 Not Found
+%% ...
+%%
+%% All submitted data is stored in memory (in an ets table). Could be
+%% useful for ad-hoc testing.
+
+-module(https_store).
+
+-export([start/0,
+ stop/0,
+ dispatch/1,
+ loop/1
+ ]).
+
+-define(HTTP_OPTS, [
+ {loop, {?MODULE, dispatch}},
+ {port, 8442},
+ {name, http_8442}
+ ]).
+
+-define(HTTPS_OPTS, [
+ {loop, {?MODULE, dispatch}},
+ {port, 8443},
+ {name, https_8443},
+ {ssl, true},
+ {ssl_opts, [
+ {certfile, "server_cert.pem"},
+ {keyfile, "server_key.pem"}]}
+ ]).
+
+-record(sd, {http, https}).
+-record(resource, {type, data}).
+
+start() ->
+ {ok, Http} = mochiweb_http:start(?HTTP_OPTS),
+ {ok, Https} = mochiweb_http:start(?HTTPS_OPTS),
+ SD = #sd{http=Http, https=Https},
+ Pid = spawn_link(fun() ->
+ ets:new(?MODULE, [named_table]),
+ loop(SD)
+ end),
+ register(http_store, Pid),
+ ok.
+
+stop() ->
+ http_store ! stop,
+ ok.
+
+dispatch(Req) ->
+ case Req:get(method) of
+ 'GET' ->
+ get_resource(Req);
+ 'PUT' ->
+ put_resource(Req);
+ 'DELETE' ->
+ delete_resource(Req);
+ _ ->
+ Headers = [{"Allow", "GET,PUT,DELETE"}],
+ Req:respond({405, Headers, "405 Method Not Allowed\r\n"})
+ end.
+
+get_resource(Req) ->
+ Path = Req:get(path),
+ case ets:lookup(?MODULE, Path) of
+ [{Path, #resource{type=Type, data=Data}}] ->
+ Req:ok({Type, Data});
+ [] ->
+ Req:respond({404, [], "404 Not Found\r\n"})
+ end.
+
+put_resource(Req) ->
+ ContentType = case Req:get_header_value("Content-Type") of
+ undefined ->
+ "application/octet-stream";
+ S ->
+ S
+ end,
+ Resource = #resource{type=ContentType, data=Req:recv_body()},
+ http_store ! {self(), {put, Req:get(path), Resource}},
+ Pid = whereis(http_store),
+ receive
+ {Pid, created} ->
+ Req:respond({201, [], "201 Created\r\n"});
+ {Pid, updated} ->
+ Req:respond({200, [], "200 OK\r\n"})
+ end.
+
+delete_resource(Req) ->
+ http_store ! {self(), {delete, Req:get(path)}},
+ Pid = whereis(http_store),
+ receive
+ {Pid, ok} ->
+ Req:respond({200, [], "200 OK\r\n"})
+ end.
+
+loop(#sd{http=Http, https=Https} = SD) ->
+ receive
+ stop ->
+ ok = mochiweb_http:stop(Http),
+ ok = mochiweb_http:stop(Https),
+ exit(normal);
+ {From, {put, Key, Val}} ->
+ Exists = ets:member(?MODULE, Key),
+ ets:insert(?MODULE, {Key, Val}),
+ case Exists of
+ true ->
+ From ! {self(), updated};
+ false ->
+ From ! {self(), created}
+ end;
+ {From, {delete, Key}} ->
+ ets:delete(?MODULE, Key),
+ From ! {self(), ok};
+ _ ->
+ ignore
+ end,
+ ?MODULE:loop(SD).
+
View
19 examples/https/server_cert.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDIDCCAgigAwIBAgIJAJLkNZzERPIUMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV
+BAMTCWxvY2FsaG9zdDAeFw0xMDAzMTgxOTM5MThaFw0yMDAzMTUxOTM5MThaMBQx
+EjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
+ggEBAJeUCOZxbmtngF4S5lXckjSDLc+8C+XjMBYBPyy5eKdJY20AQ1s9/hhp3ulI
+8pAvl+xVo4wQ+iBSvOzcy248Q+Xi6+zjceF7UNRgoYPgtJjKhdwcHV3mvFFrS/fp
+9ggoAChaJQWDO1OCfUgTWXImhkw+vcDR11OVMAJ/h73dqzJPI9mfq44PTTHfYtgr
+v4LAQAOlhXIAa2B+a6PlF6sqDqJaW5jLTcERjsBwnRhUGi7JevQzkejujX/vdA+N
+jRBjKH/KLU5h3Q7wUchvIez0PXWVTCnZjpA9aR4m7YV05nKQfxtGd71czYDYk+j8
+hd005jetT4ir7JkAWValBybJVksCAwEAAaN1MHMwHQYDVR0OBBYEFJl9s51SnjJt
+V/wgKWqV5Q6jnv1ZMEQGA1UdIwQ9MDuAFJl9s51SnjJtV/wgKWqV5Q6jnv1ZoRik
+FjAUMRIwEAYDVQQDEwlsb2NhbGhvc3SCCQCS5DWcxETyFDAMBgNVHRMEBTADAQH/
+MA0GCSqGSIb3DQEBBQUAA4IBAQB2ldLeLCc+lxK5i0EZquLamMBJwDIjGpT0JMP9
+b4XQOK2JABIu54BQIZhwcjk3FDJz/uOW5vm8k1kYni8FCjNZAaRZzCUfiUYTbTKL
+Rq9LuIAODyP2dnTqyKaQOOJHvrx9MRZ3XVecXPS0Tib4aO57vCaAbIkmhtYpTWmw
+e3t8CAIDVtgvjR6Se0a1JA4LktR7hBu22tDImvCSJn1nVAaHpani6iPBPPdMuMsP
+TBoeQfj8VpqBUjCStqJGa8ytjDFX73YaxV2mgrtGwPNme1x3YNRR11yTu7tksyMO
+GrmgxNriqYRchBhNEf72AKF0LR1ByKwfbDB9rIsV00HtCgOp
+-----END CERTIFICATE-----
View
27 examples/https/server_key.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAl5QI5nFua2eAXhLmVdySNIMtz7wL5eMwFgE/LLl4p0ljbQBD
+Wz3+GGne6UjykC+X7FWjjBD6IFK87NzLbjxD5eLr7ONx4XtQ1GChg+C0mMqF3Bwd
+Xea8UWtL9+n2CCgAKFolBYM7U4J9SBNZciaGTD69wNHXU5UwAn+Hvd2rMk8j2Z+r
+jg9NMd9i2Cu/gsBAA6WFcgBrYH5ro+UXqyoOolpbmMtNwRGOwHCdGFQaLsl69DOR
+6O6Nf+90D42NEGMof8otTmHdDvBRyG8h7PQ9dZVMKdmOkD1pHibthXTmcpB/G0Z3
+vVzNgNiT6PyF3TTmN61PiKvsmQBZVqUHJslWSwIDAQABAoIBACI8Ky5xHDFh9RpK
+Rn/KC7OUlTpADKflgizWJ0Cgu2F9L9mkn5HyFHvLHa+u7CootbWJOiEejH/UcBtH
+WyMQtX0snYCpdkUpJv5wvMoebGu+AjHOn8tfm9T/2O6rhwgckLyMb6QpGbMo28b1
+p9QiY17BJPZx7qJQJcHKsAvwDwSThlb7MFmWf42LYWlzybpeYQvwpd+UY4I0WXLu
+/dqJIS9Npq+5Y5vbo2kAEAssb2hSCvhCfHmwFdKmBzlvgOn4qxgZ1iHQgfKI6Z3Y
+J0573ZgOVTuacn+lewtdg5AaHFcl/zIYEr9SNqRoPNGbPliuv6k6N2EYcufWL5lR
+sCmmmHECgYEAxm+7OpepGr++K3+O1e1MUhD7vSPkKJrCzNtUxbOi2NWj3FFUSPRU
+adWhuxvUnZgTcgM1+KuQ0fB2VmxXe9IDcrSFS7PKFGtd2kMs/5mBw4UgDZkOQh+q
+kDiBEV3HYYJWRq0w3NQ/9Iy1jxxdENHtGmG9aqamHxNtuO608wGW2S8CgYEAw4yG
+ZyAic0Q/U9V2OHI0MLxLCzuQz17C2wRT1+hBywNZuil5YeTuIt2I46jro6mJmWI2
+fH4S/geSZzg2RNOIZ28+aK79ab2jWBmMnvFCvaru+odAuser4N9pfAlHZvY0pT+S
+1zYX3f44ygiio+oosabLC5nWI0zB2gG8pwaJlaUCgYEAgr7poRB+ZlaCCY0RYtjo
+mYYBKD02vp5BzdKSB3V1zeLuBWM84pjB6b3Nw0fyDig+X7fH3uHEGN+USRs3hSj6
+BqD01s1OT6fyfbYXNw5A1r+nP+5h26Wbr0zblcKxdQj4qbbBZC8hOJNhqTqqA0Qe
+MmzF7jiBaiZV/Cyj4x1f9BcCgYEAhjL6SeuTuOctTqs/5pz5lDikh6DpUGcH8qaV
+o6aRAHHcMhYkZzpk8yh1uUdD7516APmVyvn6rrsjjhLVq4ZAJjwB6HWvE9JBN0TR
+bILF+sREHUqU8Zn2Ku0nxyfXCKIOnxlx/J/y4TaGYqBqfXNFWiXNUrjQbIlQv/xR
+K48g/MECgYBZdQlYbMSDmfPCC5cxkdjrkmAl0EgV051PWAi4wR+hLxIMRjHBvAk7
+IweobkFvT4TICulgroLkYcSa5eOZGxB/DHqcQCbWj3reFV0VpzmTDoFKG54sqBRl
+vVntGt0pfA40fF17VoS7riAdHF53ippTtsovHEsg5tq5NrBl5uKm2g==
+-----END RSA PRIVATE KEY-----
View
81 examples/keepalive/keepalive.erl
@@ -0,0 +1,81 @@
+-module(keepalive).
+
+%% your web app can push data to clients using a technique called comet long
+%% polling. browsers make a request and your server waits to send a
+%% response until data is available. see wikipedia for a better explanation:
+%% http://en.wikipedia.org/wiki/Comet_(programming)#Ajax_with_long_polling
+%%
+%% since the majority of your http handlers will be idle at any given moment,
+%% you might consider making them hibernate while they wait for more data from
+%% another process. however, since the execution stack is discarded when a
+%% process hibernates, the handler would usually terminate after your response
+%% code runs. this means http keep alives wouldn't work; the handler process
+%% would terminate after each response and close its socket rather than
+%% returning to the big @mochiweb_http@ loop and processing another request.
+%%
+%% however, if mochiweb exposes a continuation that encapsulates the return to
+%% the top of the big loop in @mochiweb_http@, we can call that after the
+%% response. if you do that then control flow returns to the proper place,
+%% and keep alives work like they would if you hadn't hibernated.
+
+-export([ start/1, loop/1
+ ]).
+
+%% internal export (so hibernate can reach it)
+-export([ resume/3
+ ]).
+
+-define(LOOP, {?MODULE, loop}).
+
+start(Options = [{port, _Port}]) ->
+ mochiweb_http:start([{name, ?MODULE}, {loop, ?LOOP} | Options]).
+
+loop(Req) ->
+ Path = Req:get(path),
+ case string:tokens(Path, "/") of
+ ["longpoll" | RestOfPath] ->
+ %% the "reentry" is a continuation -- what @mochiweb_http@
+ %% needs to do to start its loop back at the top
+ Reentry = mochiweb_http:reentry(?LOOP),
+
+ %% here we could send a message to some other process and hope
+ %% to get an interesting message back after a while. for
+ %% simplicity let's just send ourselves a message after a few
+ %% seconds
+ erlang:send_after(2000, self(), "honk honk"),
+
+ %% since we expect to wait for a long time before getting a
+ %% reply, let's hibernate. memory usage will be minimized, so
+ %% we won't be wasting memory just sitting in a @receive@
+ proc_lib:hibernate(?MODULE, resume, [Req, RestOfPath, Reentry]),
+
+ %% we'll never reach this point, and this function @loop/1@
+ %% won't ever return control to @mochiweb_http@. luckily
+ %% @resume/3@ will take care of that.
+ io:format("not gonna happen~n", []);
+
+ _ ->
+ ok(Req, io_lib:format("some other page: ~p", [Path]))
+ end,
+
+ io:format("restarting loop normally in ~p~n", [Path]),
+ ok.
+
+%% this is the function that's called when a message arrives.
+resume(Req, RestOfPath, Reentry) ->
+ receive
+ Msg ->
+ Text = io_lib:format("wake up message: ~p~nrest of path: ~p", [Msg, RestOfPath]),
+ ok(Req, Text)
+ end,
+
+ %% if we didn't call @Reentry@ here then the function would finish and the
+ %% process would exit. calling @Reentry@ takes care of returning control
+ %% to @mochiweb_http@
+ io:format("reentering loop via continuation in ~p~n", [Req:get(path)]),
+ Reentry(Req).
+
+ok(Req, Response) ->
+ Req:ok({_ContentType = "text/plain",
+ _Headers = [],
+ Response}).
View
BIN  rebar
Binary file not shown
View
18 rebar.config
@@ -1,4 +1,16 @@
-{erl_opts, [debug_info, fail_on_warning]}.
+% -*- mode: erlang -*-
+{erl_opts, [debug_info]}.
{cover_enabled, true}.
-
-
+{eunit_opts, [verbose, {report,{eunit_surefire,[{dir,"."}]}}]}.
+{template_dir, "support/templates/"}.
+{dialyzer_opts, [{warnings, [no_return,
+ no_unused,
+ no_improper_lists,
+ no_fun_app,
+ no_match,
+ no_opaque,
+ no_fail_call,
+ error_handling,
+ race_conditions,
+ behaviours,
+ unmatched_returns]}]}.
View
45 scripts/entities.erl
@@ -0,0 +1,45 @@
+#!/usr/bin/env escript
+%% -*- mode: erlang -*-
+-export([main/1]).
+
+%% @doc Script used to generate mochiweb_charref.erl table.
+
+main(_) ->
+ application:start(inets),
+ code:add_patha("ebin"),
+ {ok, {_, _, HTML}} = httpc:request("http://www.w3.org/TR/html5/named-character-references.html"),
+ print(lists:sort(search(mochiweb_html:parse(HTML)))).
+
+print([F | T]) ->
+ io:put_chars([clause(F), ";\n"]),
+ print(T);
+print([]) ->
+ io:put_chars(["entity(_) -> undefined.\n"]),
+ ok.
+
+clause({Title, [Codepoint]}) ->
+ ["entity(\"", Title, "\") -> 16#", Codepoint];
+clause({Title, [First | Rest]}) ->
+ ["entity(\"", Title, "\") -> [16#", First,
+ [[", 16#", Codepoint] || Codepoint <- Rest],
+ "]"].
+
+
+search(Elem) ->
+ search(Elem, []).
+
+search({<<"tr">>, [{<<"id">>, <<"entity-", _/binary>>} | _], Children}, Acc) ->
+ %% HTML5 charrefs can have more than one code point(!)
+ [{<<"td">>, _, [{<<"code">>, _, [TitleSemi]}]},
+ {<<"td">>, [], [RawCPs]} | _] = Children,
+ L = byte_size(TitleSemi) - 1,
+ <<Title:L/binary, $;>> = TitleSemi,
+ {match, Matches} = re:run(RawCPs, "(?:\\s*U\\+)([a-fA-F0-9]+)",
+ [{capture, all, binary}, global]),
+ [{Title, [CP || [_, CP] <- Matches]} | Acc];
+search({Tag, Attrs, [H | T]}, Acc) ->
+ search({Tag, Attrs, T}, search(H, Acc));
+search({_Tag, _Attrs, []}, Acc) ->
+ Acc;
+search(<<_/binary>>, Acc) ->
+ Acc.
View
24 scripts/new_mochiweb.erl
@@ -4,24 +4,20 @@
%% External API
-main([Name]) ->
- main([Name, "."]);
-main([Name, Dest]) ->
- ensure(),
- DestDir = filename:absname(Dest),
- ok = mochiweb_skel:skelcopy(DestDir, Name);
main(_) ->
usage().
%% Internal API
-ensure() ->
- code:add_patha(filename:join(filename:dirname(escript:script_name()),
- "../ebin")).
-
usage() ->
- io:format("usage: ~s name [destdir]~n",
- [filename:basename(escript:script_name())]),
+ io:format(
+ "new_mochiweb.erl has been replaced by a rebar template!\n"
+ "\n"
+ "To create a new mochiweb using project:\n"
+ " make app PROJECT=project_name\n"
+ "\n"
+ "To create a new mochiweb using project in a specific directory:\n"
+ " make app PROJECT=project_name PREFIX=$HOME/projects/\n"
+ "\n"
+ ),
halt(1).
-
-
View
2  src/mochifmt.erl
@@ -369,8 +369,8 @@ parse_std_conversion([Type], Acc) ->
%%
%% Tests
%%
--include_lib("eunit/include/eunit.hrl").
-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
tokenize_test() ->
{?MODULE, [{raw, "ABC"}]} = tokenize("ABC"),
View
2  src/mochifmt_records.erl
@@ -33,6 +33,6 @@ get_rec_index(Atom, [_ | Rest], Index) ->
%%
%% Tests
%%
--include_lib("eunit/include/eunit.hrl").
-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
-endif.
View
2  src/mochifmt_std.erl
@@ -25,6 +25,6 @@ format_field(Arg, Format) ->
%%
%% Tests
%%
--include_lib("eunit/include/eunit.hrl").
-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
-endif.
View
4 src/mochiglobal.erl
@@ -30,7 +30,7 @@ put(K, V) ->
put(_K, V, Mod) ->
Bin = compile(Mod, V),
code:purge(Mod),
- code:load_binary(Mod, atom_to_list(Mod) ++ ".erl", Bin),
+ {module, Mod} = code:load_binary(Mod, atom_to_list(Mod) ++ ".erl", Bin),
ok.
-spec delete(atom()) -> boolean().
@@ -77,8 +77,8 @@ term_to_abstract(Module, Getter, T) ->
%%
%% Tests
%%
--include_lib("eunit/include/eunit.hrl").
-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
get_put_delete_test() ->
K = '$$test$$mochiglobal',
delete(K),
View
2  src/mochihex.erl
@@ -68,8 +68,8 @@ to_bin([C1, C2 | Rest], Acc) ->
%%
%% Tests
%%
--include_lib("eunit/include/eunit.hrl").
-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
to_hex_test() ->
"ff000ff1" = to_hex([255, 0, 15, 241]),
View
2  src/mochijson.erl
@@ -406,8 +406,8 @@ tokenize(L=[C | _], S) when C >= $0, C =< $9; C == $- ->
%%
%% Tests
%%
--include_lib("eunit/include/eunit.hrl").
-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
%% testing constructs borrowed from the Yaws JSON implementation.
View
153 src/mochijson2.erl
@@ -4,13 +4,45 @@
%% @doc Yet another JSON (RFC 4627) library for Erlang. mochijson2 works
%% with binaries as strings, arrays as lists (without an {array, _})
%% wrapper and it only knows how to decode UTF-8 (and ASCII).
+%%
+%% JSON terms are decoded as follows (javascript -> erlang):
+%% <ul>
+%% <li>{"key": "value"} ->
+%% {struct, [{&lt;&lt;"key">>, &lt;&lt;"value">>}]}</li>
+%% <li>["array", 123, 12.34, true, false, null] ->
+%% [&lt;&lt;"array">>, 123, 12.34, true, false, null]
+%% </li>
+%% </ul>
+%% <ul>
+%% <li>Strings in JSON decode to UTF-8 binaries in Erlang</li>
+%% <li>Objects decode to {struct, PropList}</li>
+%% <li>Numbers decode to integer or float</li>
+%% <li>true, false, null decode to their respective terms.</li>
+%% </ul>
+%% The encoder will accept the same format that the decoder will produce,
+%% but will also allow additional cases for leniency:
+%% <ul>
+%% <li>atoms other than true, false, null will be considered UTF-8
+%% strings (even as a proplist key)
+%% </li>
+%% <li>{json, IoList} will insert IoList directly into the output
+%% with no validation
+%% </li>
+%% <li>{array, Array} will be encoded as Array
+%% (legacy mochijson style)
+%% </li>
+%% <li>A non-empty raw proplist will be encoded as an object as long
+%% as the first pair does not have an atom key of json, struct,
+%% or array
+%% </li>
+%% </ul>
-module(mochijson2).
-author('bob@mochimedia.com').
-export([encoder/1, encode/1]).
--export([decoder/1, decode/1]).
+-export([decoder/1, decode/1, decode/2]).
-% This is a macro to placate syntax highlighters..
+%% This is a macro to placate syntax highlighters..
-define(Q, $\").
-define(ADV_COL(S, N), S#decoder{offset=N+S#decoder.offset,
column=N+S#decoder.column}).
@@ -38,9 +70,10 @@
%% @type json_number() = integer() | float()
%% @type json_array() = [json_term()]
%% @type json_object() = {struct, [{json_string(), json_term()}]}
+%% @type json_eep18_object() = {[{json_string(), json_term()}]}
%% @type json_iolist() = {json, iolist()}
%% @type json_term() = json_string() | json_number() | json_array() |
-%% json_object() | json_iolist()
+%% json_object() | json_eep18_object() | json_iolist()
-record(encoder, {handler=null,
utf8=false}).
@@ -70,6 +103,14 @@ decoder(Options) ->
State = parse_decoder_options(Options, #decoder{}),
fun (O) -> json_decode(O, State) end.
+%% @spec decode(iolist(), [{format, proplist | eep18 | struct}]) -> json_term()
+%% @doc Decode the given iolist to Erlang terms using the given object format
+%% for decoding, where proplist returns JSON objects as [{binary(), json_term()}]
+%% proplists, eep18 returns JSON objects as {[binary(), json_term()]}, and struct
+%% returns them as-is.
+decode(S, Options) ->
+ json_decode(S, parse_decoder_options(Options, #decoder{})).
+
%% @spec decode(iolist()) -> json_term()
%% @doc Decode the given iolist to Erlang terms.
decode(S) ->
@@ -87,7 +128,10 @@ parse_encoder_options([{utf8, Switch} | Rest], State) ->
parse_decoder_options([], State) ->
State;
parse_decoder_options([{object_hook, Hook} | Rest], State) ->
- parse_decoder_options(Rest, State#decoder{object_hook=Hook}).
+ parse_decoder_options(Rest, State#decoder{object_hook=Hook});
+parse_decoder_options([{format, Format} | Rest], State)
+ when Format =:= struct orelse Format =:= eep18 orelse Format =:= proplist ->
+ parse_decoder_options(Rest, State#decoder{object_hook=Format}).
json_encode(true, _State) ->
<<"true">>;
@@ -95,19 +139,26 @@ json_encode(false, _State) ->
<<"false">>;
json_encode(null, _State) ->
<<"null">>;
-json_encode(I, _State) when is_integer(I) andalso I >= -2147483648 andalso I =< 2147483647 ->
- %% Anything outside of 32-bit integers should be encoded as a float
- integer_to_list(I);
json_encode(I, _State) when is_integer(I) ->
- mochinum:digits(float(I));
+ integer_to_list(I);
json_encode(F, _State) when is_float(F) ->
mochinum:digits(F);
json_encode(S, State) when is_binary(S); is_atom(S) ->
json_encode_string(S, State);
-json_encode(Array, State) when is_list(Array) ->
- json_encode_array(Array, State);
+json_encode([{K, _}|_] = Props, State) when (K =/= struct andalso
+ K =/= array andalso
+ K =/= json) ->
+ json_encode_proplist(Props, State);
json_encode({struct, Props}, State) when is_list(Props) ->
json_encode_proplist(Props, State);
+json_encode({Props}, State) when is_list(Props) ->
+ json_encode_proplist(Props, State);
+json_encode({}, State) ->
+ json_encode_proplist([], State);
+json_encode(Array, State) when is_list(Array) ->
+ json_encode_array(Array, State);
+json_encode({array, Array}, State) when is_list(Array) ->
+ json_encode_array(Array, State);
json_encode({json, IoList}, _State) ->
IoList;
json_encode(Bad, #encoder{handler=null}) ->
@@ -283,8 +334,12 @@ decode1(B, S=#decoder{state=null}) ->
decode_object(B, S1)
end.
-make_object(V, #decoder{object_hook=null}) ->
+make_object(V, #decoder{object_hook=N}) when N =:= null orelse N =:= struct ->
V;
+make_object({struct, P}, #decoder{object_hook=eep18}) ->
+ {P};
+make_object({struct, P}, #decoder{object_hook=proplist}) ->
+ P;
make_object(V, #decoder{object_hook=Hook}) ->
Hook(V).
@@ -405,8 +460,22 @@ tokenize_string(B, S=#decoder{offset=O}, Acc) ->
Acc1 = lists:reverse(xmerl_ucs:to_utf8(C), Acc),
tokenize_string(B, ?ADV_COL(S, 6), Acc1)
end;
- <<_:O/binary, C, _/binary>> ->
- tokenize_string(B, ?INC_CHAR(S, C), [C | Acc])
+ <<_:O/binary, C1, _/binary>> when C1 < 128 ->
+ tokenize_string(B, ?INC_CHAR(S, C1), [C1 | Acc]);
+ <<_:O/binary, C1, C2, _/binary>> when C1 >= 194, C1 =< 223,
+ C2 >= 128, C2 =< 191 ->
+ tokenize_string(B, ?ADV_COL(S, 2), [C2, C1 | Acc]);
+ <<_:O/binary, C1, C2, C3, _/binary>> when C1 >= 224, C1 =< 239,
+ C2 >= 128, C2 =< 191,
+ C3 >= 128, C3 =< 191 ->
+ tokenize_string(B, ?ADV_COL(S, 3), [C3, C2, C1 | Acc]);
+ <<_:O/binary, C1, C2, C3, C4, _/binary>> when C1 >= 240, C1 =< 244,
+ C2 >= 128, C2 =< 191,
+ C3 >= 128, C3 =< 191,
+ C4 >= 128, C4 =< 191 ->
+ tokenize_string(B, ?ADV_COL(S, 4), [C4, C3, C2, C1 | Acc]);
+ _ ->
+ throw(invalid_utf8)
end.
tokenize_number(B, S) ->
@@ -510,8 +579,8 @@ tokenize(B, S=#decoder{offset=O}) ->
%%
%% Tests
%%
--include_lib("eunit/include/eunit.hrl").
-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
%% testing constructs borrowed from the Yaws JSON implementation.
@@ -651,7 +720,9 @@ input_validation_test() ->
<<?Q, 16#E0, 16#80,16#7F, ?Q>>,
<<?Q, 16#F0, 16#80, 16#80, 16#7F, ?Q>>,
%% we don't support code points > 10FFFF per RFC 3629
- <<?Q, 16#F5, 16#80, 16#80, 16#80, ?Q>>
+ <<?Q, 16#F5, 16#80, 16#80, 16#80, ?Q>>,
+ %% escape characters trigger a different code path
+ <<?Q, $\\, $\n, 16#80, ?Q>>
],
lists:foreach(
fun(X) ->
@@ -719,6 +790,15 @@ key_encode_test() ->
?assertEqual(
<<"{\"foo\":1}">>,
iolist_to_binary(encode({struct, [{"foo", 1}]}))),
+ ?assertEqual(
+ <<"{\"foo\":1}">>,
+ iolist_to_binary(encode([{foo, 1}]))),
+ ?assertEqual(
+ <<"{\"foo\":1}">>,
+ iolist_to_binary(encode([{<<"foo">>, 1}]))),
+ ?assertEqual(
+ <<"{\"foo\":1}">>,
+ iolist_to_binary(encode([{"foo", 1}]))),
?assertEqual(
<<"{\"\\ud834\\udd20\":1}">>,
iolist_to_binary(
@@ -764,19 +844,48 @@ int_test() ->
?assertEqual(11, decode("11")),
ok.
-float_fallback_test() ->
- ?assertEqual(<<"-2147483649.0">>, iolist_to_binary(encode(-2147483649))),
- ?assertEqual(<<"2147483648.0">>, iolist_to_binary(encode(2147483648))),
+large_int_test() ->
+ ?assertEqual(<<"-2147483649214748364921474836492147483649">>,
+ iolist_to_binary(encode(-2147483649214748364921474836492147483649))),
+ ?assertEqual(<<"2147483649214748364921474836492147483649">>,
+ iolist_to_binary(encode(2147483649214748364921474836492147483649))),
+ ok.
+
+float_test() ->
+ ?assertEqual(<<"-2147483649.0">>, iolist_to_binary(encode(-2147483649.0))),
+ ?assertEqual(<<"2147483648.0">>, iolist_to_binary(encode(2147483648.0))),
ok.
handler_test() ->
?assertEqual(
- {'EXIT',{json_encode,{bad_term,{}}}},
- catch encode({})),
- F = fun ({}) -> [] end,
+ {'EXIT',{json_encode,{bad_term,{x,y}}}},
+ catch encode({x,y})),
+ F = fun ({x,y}) -> [] end,
?assertEqual(
<<"[]">>,
- iolist_to_binary((encoder([{handler, F}]))({}))),
+ iolist_to_binary((encoder([{handler, F}]))({x, y}))),
ok.
+encode_empty_test_() ->
+ [{A, ?_assertEqual(<<"{}">>, iolist_to_binary(encode(B)))}
+ || {A, B} <- [{"eep18 {}", {}},
+ {"eep18 {[]}", {[]}},
+ {"{struct, []}", {struct, []}}]].
+
+encode_test_() ->
+ P = [{<<"k">>, <<"v">>}],
+ JSON = iolist_to_binary(encode({struct, P})),
+ [{atom_to_list(F),
+ ?_assertEqual(JSON, iolist_to_binary(encode(decode(JSON, [{format, F}]))))}
+ || F <- [struct, eep18, proplist]].
+
+format_test_() ->
+ P = [{<<"k">>, <<"v">>}],
+ JSON = iolist_to_binary(encode({struct, P})),
+ [{atom_to_list(F),
+ ?_assertEqual(A, decode(JSON, [{format, F}]))}
+ || {F, A} <- [{struct, {struct, P}},
+ {eep18, {P}},
+ {proplist, P}]].
+
-endif.
View
2  src/mochilists.erl
@@ -55,8 +55,8 @@ get_value(Key, Proplist, Default) ->
%%
%% Tests
%%
--include_lib("eunit/include/eunit.hrl").
-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
set_defaults_test() ->
?assertEqual(
View
8 src/mochilogfile2.erl
@@ -48,17 +48,17 @@ find_last_newline(_FD, N) when N =< 1 ->
0;
find_last_newline(FD, Location) ->
case file:pread(FD, Location - 1, 1) of
- {ok, <<$\n>>} ->
+ {ok, <<$\n>>} ->
Location;
- {ok, _} ->
- find_last_newline(FD, Location - 1)
+ {ok, _} ->
+ find_last_newline(FD, Location - 1)
end.
%%
%% Tests
%%
--include_lib("eunit/include/eunit.hrl").
-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
name_test() ->
D = mochitemp:mkdtemp(),
FileName = filename:join(D, "open_close_test.log"),
View
79 src/mochinum.erl
@@ -29,11 +29,10 @@ digits(N) when is_integer(N) ->
digits(0.0) ->
"0.0";
digits(Float) ->
- {Frac, Exp} = frexp(Float),
- Exp1 = Exp - 53,
- Frac1 = trunc(abs(Frac) * (1 bsl 53)),
- [Place | Digits] = digits1(Float, Exp1, Frac1),
- R = insert_decimal(Place, [$0 + D || D <- Digits]),
+ {Frac1, Exp1} = frexp_int(Float),
+ [Place0 | Digits0] = digits1(Float, Exp1, Frac1),
+ {Place, Digits} = transform_digits(Place0, Digits0),
+ R = insert_decimal(Place, Digits),
case Float < 0 of
true ->
[$- | R];
@@ -64,7 +63,6 @@ int_pow(X, N) when N > 0 ->
int_ceil(X) ->
T = trunc(X),
case (X - T) of
- Neg when Neg < 0 -> T;
Pos when Pos > 0 -> T + 1;
_ -> T
end.
@@ -228,28 +226,42 @@ log2floor(Int, N) ->
log2floor(Int bsr 1, 1 + N).
+transform_digits(Place, [0 | Rest]) ->
+ transform_digits(Place, Rest);
+transform_digits(Place, Digits) ->
+ {Place, [$0 + D || D <- Digits]}.
+
+
+frexp_int(F) ->
+ case unpack(F) of
+ {_Sign, 0, Frac} ->
+ {Frac, ?MIN_EXP};
+ {_Sign, Exp, Frac} ->
+ {Frac + (1 bsl 52), Exp - 53 - ?FLOAT_BIAS}
+ end.
+
%%
%% Tests
%%
--include_lib("eunit/include/eunit.hrl").
-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
int_ceil_test() ->
- 1 = int_ceil(0.0001),
- 0 = int_ceil(0.0),
- 1 = int_ceil(0.99),
- 1 = int_ceil(1.0),
- -1 = int_ceil(-1.5),
- -2 = int_ceil(-2.0),
+ ?assertEqual(1, int_ceil(0.0001)),
+ ?assertEqual(0, int_ceil(0.0)),
+ ?assertEqual(1, int_ceil(0.99)),
+ ?assertEqual(1, int_ceil(1.0)),
+ ?assertEqual(-1, int_ceil(-1.5)),
+ ?assertEqual(-2, int_ceil(-2.0)),
ok.
int_pow_test() ->
- 1 = int_pow(1, 1),
- 1 = int_pow(1, 0),
- 1 = int_pow(10, 0),
- 10 = int_pow(10, 1),
- 100 = int_pow(10, 2),
- 1000 = int_pow(10, 3),
+ ?assertEqual(1, int_pow(1, 1)),
+ ?assertEqual(1, int_pow(1, 0)),
+ ?assertEqual(1, int_pow(10, 0)),
+ ?assertEqual(10, int_pow(10, 1)),
+ ?assertEqual(100, int_pow(10, 2)),
+ ?assertEqual(1000, int_pow(10, 3)),
ok.
digits_test() ->
@@ -274,9 +286,9 @@ digits_test() ->
?assertEqual("4503599627370496.0",
digits(4503599627370496.0)),
%% small denormalized number
- %% 4.94065645841246544177e-324
+ %% 4.94065645841246544177e-324 =:= 5.0e-324
<<SmallDenorm/float>> = <<0,0,0,0,0,0,0,1>>,
- ?assertEqual("4.9406564584124654e-324",
+ ?assertEqual("5.0e-324",
digits(SmallDenorm)),
?assertEqual(SmallDenorm,
list_to_float(digits(SmallDenorm))),
@@ -301,31 +313,42 @@ digits_test() ->
digits(LargeNorm)),
?assertEqual(LargeNorm,
list_to_float(digits(LargeNorm))),
+ %% issue #10 - mochinum:frexp(math:pow(2, -1074)).
+ ?assertEqual("5.0e-324",
+ digits(math:pow(2, -1074))),
ok.
frexp_test() ->
%% zero
- {0.0, 0} = frexp(0.0),
+ ?assertEqual({0.0, 0}, frexp(0.0)),
%% one
- {0.5, 1} = frexp(1.0),
+ ?assertEqual({0.5, 1}, frexp(1.0)),
%% negative one
- {-0.5, 1} = frexp(-1.0),
+ ?assertEqual({-0.5, 1}, frexp(-1.0)),
%% small denormalized number
%% 4.94065645841246544177e-324
<<SmallDenorm/float>> = <<0,0,0,0,0,0,0,1>>,
- {0.5, -1073} = frexp(SmallDenorm),
+ ?assertEqual({0.5, -1073}, frexp(SmallDenorm)),
%% large denormalized number
%% 2.22507385850720088902e-308
<<BigDenorm/float>> = <<0,15,255,255,255,255,255,255>>,
- {0.99999999999999978, -1022} = frexp(BigDenorm),
+ ?assertEqual(
+ {0.99999999999999978, -1022},
+ frexp(BigDenorm)),
%% small normalized number
%% 2.22507385850720138309e-308
<<SmallNorm/float>> = <<0,16,0,0,0,0,0,0>>,
- {0.5, -1021} = frexp(SmallNorm),
+ ?assertEqual({0.5, -1021}, frexp(SmallNorm)),
%% large normalized number
%% 1.79769313486231570815e+308
<<LargeNorm/float>> = <<127,239,255,255,255,255,255,255>>,
- {0.99999999999999989, 1024} = frexp(LargeNorm),
+ ?assertEqual(
+ {0.99999999999999989, 1024},
+ frexp(LargeNorm)),
+ %% issue #10 - mochinum:frexp(math:pow(2, -1074)).
+ ?assertEqual(
+ {0.5, -1073},
+ frexp(math:pow(2, -1074))),
ok.
-endif.
View
3  src/mochitemp.erl
@@ -135,8 +135,9 @@ normalize_dir(L) ->
%%
%% Tests
%%
--include_lib("eunit/include/eunit.hrl").
-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+
pushenv(L) ->
[{K, os:getenv(K)} || K <- L].
popenv(L) ->
View
29 src/mochiutf8.erl
@@ -5,8 +5,9 @@
%% invalid bytes.
-module(mochiutf8).
--export([valid_utf8_bytes/1, codepoint_to_bytes/1, bytes_to_codepoints/1]).
--export([bytes_foldl/3, codepoint_foldl/3, read_codepoint/1, len/1]).
+-export([valid_utf8_bytes/1, codepoint_to_bytes/1, codepoints_to_bytes/1]).
+-export([bytes_to_codepoints/1, bytes_foldl/3, codepoint_foldl/3]).
+-export([read_codepoint/1, len/1]).
%% External API
@@ -148,7 +149,7 @@ invalid_utf8_indexes(<<C1, C2, Rest/binary>>, N, Acc)
C2 band 16#C0 =:= 16#80 ->
%% U+0080 - U+07FF - 11 bits
case ((C1 band 16#1F) bsl 6) bor (C2 band 16#3F) of
- C when C < 16#80 ->
+ C when C < 16#80 ->
%% Overlong encoding.
invalid_utf8_indexes(Rest, 2 + N, [1 + N, N | Acc]);
_ ->
@@ -161,13 +162,13 @@ invalid_utf8_indexes(<<C1, C2, C3, Rest/binary>>, N, Acc)
C3 band 16#C0 =:= 16#80 ->
%% U+0800 - U+FFFF - 16 bits
case ((((C1 band 16#0F) bsl 6) bor (C2 band 16#3F)) bsl 6) bor
- (C3 band 16#3F) of
- C when (C < 16#800) orelse (C >= 16#D800 andalso C =< 16#DFFF) ->
- %% Overlong encoding or surrogate.
+ (C3 band 16#3F) of
+ C when (C < 16#800) orelse (C >= 16#D800 andalso C =< 16#DFFF) ->
+ %% Overlong encoding or surrogate.
invalid_utf8_indexes(Rest, 3 + N, [2 + N, 1 + N, N | Acc]);
- _ ->
+ _ ->
%% Upper bound U+FFFF does not need to be checked
- invalid_utf8_indexes(Rest, 3 + N, Acc)
+ invalid_utf8_indexes(Rest, 3 + N, Acc)
end;
invalid_utf8_indexes(<<C1, C2, C3, C4, Rest/binary>>, N, Acc)
when C1 band 16#F8 =:= 16#F0,
@@ -177,11 +178,11 @@ invalid_utf8_indexes(<<C1, C2, C3, C4, Rest/binary>>, N, Acc)
%% U+10000 - U+10FFFF - 21 bits
case ((((((C1 band 16#0F) bsl 6) bor (C2 band 16#3F)) bsl 6) bor
(C3 band 16#3F)) bsl 6) bor (C4 band 16#3F) of
- C when (C < 16#10000) orelse (C > 16#10FFFF) ->
- %% Overlong encoding or invalid code point.
- invalid_utf8_indexes(Rest, 4 + N, [3 + N, 2 + N, 1 + N, N | Acc]);
- _ ->
- invalid_utf8_indexes(Rest, 4 + N, Acc)
+ C when (C < 16#10000) orelse (C > 16#10FFFF) ->
+ %% Overlong encoding or invalid code point.
+ invalid_utf8_indexes(Rest, 4 + N, [3 + N, 2 + N, 1 + N, N | Acc]);
+ _ ->
+ invalid_utf8_indexes(Rest, 4 + N, Acc)
end;
invalid_utf8_indexes(<<_, Rest/binary>>, N, Acc) ->
%% Invalid char
@@ -192,8 +193,8 @@ invalid_utf8_indexes(<<>>, _N, Acc) ->
%%
%% Tests
%%
--include_lib("eunit/include/eunit.hrl").
-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
binary_skip_bytes_test() ->
?assertEqual(<<"foo">>,
View
9 src/mochiweb.app.src
@@ -0,0 +1,9 @@
+%% This is generated from src/mochiweb.app.src
+{application, mochiweb,
+ [{description, "MochiMedia Web Server"},
+ {vsn, "2.3.0"},
+ {modules, []},
+ {registered, []},
+ {env, []},
+ {applications, [kernel, stdlib, crypto, inets, ssl, xmerl,
+ compiler, syntax_tools]}]}.
View
46 src/mochiweb.erl
@@ -6,22 +6,9 @@
-module(mochiweb).
-author('bob@mochimedia.com').
--export([start/0, stop/0]).
-export([new_request/1, new_response/1]).
-export([all_loaded/0, all_loaded/1, reload/0]).
-
-%% @spec start() -> ok
-%% @doc Start the MochiWeb server.
-start() ->
- ensure_started(crypto),
- application:start(mochiweb).
-
-%% @spec stop() -> ok
-%% @doc Stop the MochiWeb server.
-stop() ->
- Res = application:stop(mochiweb),
- application:stop(crypto),
- Res.
+-export([ensure_started/1]).
reload() ->
[c:l(Module) || Module <- all_loaded()].
@@ -78,8 +65,8 @@ new_response({Request, Code, Headers}) ->
Code,
mochiweb_headers:make(Headers)).
-%% Internal API
-
+%% @spec ensure_started(App::atom()) -> ok
+%% @doc Start the given App if it has not been started already.
ensure_started(App) ->
case application:start(App) of
ok ->
@@ -88,12 +75,11 @@ ensure_started(App) ->
ok
end.
-
%%
%% Tests
%%
--include_lib("eunit/include/eunit.hrl").
-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
-record(treq, {path, body= <<>>, xreply= <<>>}).
@@ -112,7 +98,7 @@ with_server(Transport, ServerFun, ClientFun) ->
ssl ->
ServerOpts0 ++ [{ssl, true}, {ssl_opts, ssl_cert_opts()}]
end,
- {ok, Server} = mochiweb_http:start(ServerOpts),
+ {ok, Server} = mochiweb_http:start_link(ServerOpts),
Port = mochiweb_socket_server:get(Server, port),
Res = (catch ClientFun(Transport, Port)),
mochiweb_http:stop(Server),
@@ -123,6 +109,8 @@ request_test() ->
"/foo/bar/baz wibble quux" = R:get(path),
ok.
+-define(LARGE_TIMEOUT, 60).
+
single_http_GET_test() ->
do_GET(plain, 1).
@@ -135,11 +123,13 @@ multiple_http_GET_test() ->
multiple_https_GET_test() ->
do_GET(ssl, 3).
-hundred_http_GET_test() ->
- do_GET(plain, 100).
+hundred_http_GET_test_() -> % note the underscore
+ {timeout, ?LARGE_TIMEOUT,
+ fun() -> ?assertEqual(ok, do_GET(plain,100)) end}.
-hundred_https_GET_test() ->
- do_GET(ssl, 100).
+hundred_https_GET_test_() -> % note the underscore
+ {timeout, ?LARGE_TIMEOUT,
+ fun() -> ?assertEqual(ok, do_GET(ssl,100)) end}.
single_128_http_POST_test() ->
do_POST(plain, 128, 1).
@@ -165,11 +155,13 @@ multiple_100k_http_POST_test() ->
multiple_100K_https_POST_test() ->
do_POST(ssl, 102400, 3).
-hundred_128_http_POST_test() ->
- do_POST(plain, 128, 100).
+hundred_128_http_POST_test_() -> % note the underscore
+ {timeout, ?LARGE_TIMEOUT,
+ fun() -> ?assertEqual(ok, do_POST(plain, 128, 100)) end}.
-hundred_128_https_POST_test() ->
- do_POST(ssl, 128, 100).
+hundred_128_https_POST_test_() -> % note the underscore
+ {timeout, ?LARGE_TIMEOUT,
+ fun() -> ?assertEqual(ok, do_POST(ssl, 128, 100)) end}.
do_GET(Transport, Times) ->
PathPrefix = "/whatever/",
View
6 src/mochiweb_acceptor.erl
@@ -22,7 +22,7 @@ init(Server, Listen, Loop) ->
{error, closed} ->
exit(normal);
{error, timeout} ->
- exit(normal);
+ init(Server, Listen, Loop);
{error, esslaccept} ->
exit(normal);
Other ->
@@ -35,6 +35,8 @@ init(Server, Listen, Loop) ->
call_loop({M, F}, Socket) ->
M:F(Socket);
+call_loop({M, F, [A1]}, Socket) ->
+ M:F(Socket, A1);
call_loop({M, F, A}, Socket) ->
erlang:apply(M, F, [Socket | A]);
call_loop(Loop, Socket) ->
@@ -43,6 +45,6 @@ call_loop(Loop, Socket) ->
%%
%% Tests
%%
--include_lib("eunit/include/eunit.hrl").
-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
-endif.
View
2,387 src/mochiweb_charref.erl
@@ -1,17 +1,17 @@
%% @author Bob Ippolito <bob@mochimedia.com>
%% @copyright 2007 Mochi Media, Inc.
-%% @doc Converts HTML 4 charrefs and entities to codepoints.
+%% @doc Converts HTML 5 charrefs and entities to codepoints (or lists of code points).
-module(mochiweb_charref).
-export([charref/1]).
%% External API.
-%% @spec charref(S) -> integer() | undefined
%% @doc Convert a decimal charref, hex charref, or html entity to a unicode
%% codepoint, or return undefined on failure.
%% The input should not include an ampersand or semicolon.
%% charref("#38") = 38, charref("#x26") = 38, charref("amp") = 38.
+-spec charref(binary() | string()) -> integer() | [integer()] | undefined.
charref(B) when is_binary(B) ->
charref(binary_to_list(B));
charref([$#, C | L]) when C =:= $x orelse C =:= $X ->
@@ -29,266 +29,2141 @@ charref(L) ->
%% Internal API.
-entity("nbsp") -> 160;
-entity("iexcl") -> 161;
-entity("cent") -> 162;
-entity("pound") -> 163;
-entity("curren") -> 164;
-entity("yen") -> 165;
-entity("brvbar") -> 166;
-entity("sect") -> 167;
-entity("uml") -> 168;
-entity("copy") -> 169;
-entity("ordf") -> 170;
-entity("laquo") -> 171;