pax_global_header00006660000000000000000000000064145033322740014515gustar00rootroot0000000000000052 comment=897f22f6720cf369547d1303888b46b5a00e00b4 mochiweb-3.2.1/000077500000000000000000000000001450333227400133155ustar00rootroot00000000000000mochiweb-3.2.1/.editorconfig000066400000000000000000000005501450333227400157720ustar00rootroot00000000000000# EditorConfig file: http://EditorConfig.org # top-most EditorConfig file root = true # Unix-style newlines with a newline ending every file [*] end_of_line = lf insert_final_newline = true charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true # 4 space indentation [*.{erl,src,hrl}] indent_style = space indent_size = 4 tab_width = 8 mochiweb-3.2.1/.github/000077500000000000000000000000001450333227400146555ustar00rootroot00000000000000mochiweb-3.2.1/.github/workflows/000077500000000000000000000000001450333227400167125ustar00rootroot00000000000000mochiweb-3.2.1/.github/workflows/ci.yml000066400000000000000000000011501450333227400200250ustar00rootroot00000000000000on: push: branches: - main pull_request: jobs: test: name: test ${{matrix.otp}} on ${{matrix.os}} runs-on: ${{matrix.os}} container: image: erlang:${{matrix.otp}} strategy: fail-fast: false matrix: os: - ubuntu-latest otp: - "25.3.0.0" - "24.3.4.10" - "23.3.4.18" - "22.3.4.26" - "21.3.8.24" - "20.3.8.26" - "19.3.6.13" - "18.3.4.11" steps: - uses: actions/checkout@v3 - run: make test - run: make edoc - run: REBAR=rebar make test mochiweb-3.2.1/.github/workflows/release.yml000066400000000000000000000005631450333227400210610ustar00rootroot00000000000000on: push: tags: - '*' jobs: publish: if: "github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')" runs-on: ubuntu-latest steps: - name: Check out uses: actions/checkout@v3 - name: Publish to Hex.pm uses: erlangpack/github-action@v3 env: HEX_API_KEY: ${{ secrets.HEX_API_KEY }} mochiweb-3.2.1/.gitignore000066400000000000000000000001751450333227400153100ustar00rootroot00000000000000/ebin /doc /_test /.eunit /docs .DS_Store /TEST-*.xml /deps /.rebar *.swp *.beam *.dump rebar.lock /_build /rebar3.crashdump mochiweb-3.2.1/CHANGES.md000066400000000000000000000272621450333227400147200ustar00rootroot00000000000000Version 3.2.1 released 2023-09-22 * mochinum:digits/1: fix handling of -0.0 for OTP-26.1/27.0 https://github.com/mochi/mochiweb/pull/260 Version 3.2.0 released 2023-08-31 * Add new mochiweb_request:is_closed/1 function https://github.com/mochi/mochiweb/pull/258 Version 3.1.2 released 2023-04-20 * Fix rebar edoc settings https://github.com/mochi/mochiweb/pull/257 Version 3.1.1 released 2022-10-11 * OTP 25 added to test matrix https://github.com/mochi/mochiweb/pull/251 * Fix for chunk length parsing edge case https://github.com/mochi/mochiweb/pull/249 Version 3.1.0 released 2022-08-13 * Leading and trailing whitespace in header values are now trimmed for better RFC 7230 compliance. https://github.com/mochi/mochiweb/pull/247 https://github.com/mochi/mochiweb/pull/248 Version 3.0.0 released 2022-05-09 * rebar3 is now the preferred build tool (finally) https://github.com/mochi/mochiweb/pull/241 https://github.com/mochi/mochiweb/pull/243 * Minimum OTP version is now 18, which allows us to remove a number of backwards compatibility hacks while still supporting almost 7 years of Erlang releases. https://github.com/mochi/mochiweb/pull/239 * Crashing client processes now exit with reason `{shutdown, Error}`. This ensures processes linked to the connection process are also cleaned up. If exit `normal` was caught in a request loop callback, for example in a `try ... catch exit:normal ...` expression, that expression might have to be updated to handle the `{shutdown, Error}` error reason. https://github.com/mochi/mochiweb/pull/238 https://github.com/mochi/mochiweb/pull/242 Version 2.22.0 released 2021-08-23 * Renamed master branch to main * Add unquote_path/1 for separate '+' encoding https://github.com/mochi/mochiweb/pull/231 Version 2.21.0 released 2021-06-06 * Upgrade crypto functions to support OTP 23 https://github.com/mochi/mochiweb/pull/231 * Switch from Travis to GitHub Actions for testing https://github.com/mochi/mochiweb/pull/232 Version 2.20.1 released 2020-02-03 * Removed deprecated metadata from .app file Version 2.20.0 released 2019-07-14 * Expand testing matrix to include Erlang/OTP 22.0 and Erlang/OTP 21.3 * Add support for SameSite=none in cookies https://github.com/mochi/mochiweb/pull/225 * Fix parsing of certain unquoted cookie values https://github.com/mochi/mochiweb/pull/212 Version 2.19.0 released 2019-01-17 * Fix warning in 21.2.3 and crash on incompatible releases (21.2, 21.2.1, 21.2.2 have a SSL bug) https://github.com/mochi/mochiweb/pull/210 * Erlang/OTP 21 compatibility https://github.com/mochi/mochiweb/pull/198 https://github.com/mochi/mochiweb/pull/204 * New `{buffer, Buffer}` socket server option https://github.com/mochi/mochiweb/pull/208 * New `{format, map}` option for mochijson2:decode/2 https://github.com/mochi/mochiweb/pull/206 * No longer crash when a socket is closed server-side https://github.com/mochi/mochiweb/pull/205 * Support for SameSite cookie setting https://github.com/mochi/mochiweb/pull/203 Version 2.18.0 released 2018-05-12 * Add the 100.64.0.0/10 private IP shared address range https://github.com/mochi/mochiweb/pull/193 Version 2.17.0 released 2017-08-12 * Fix deprecation warnings for Erlang/OTP 20.0 https://github.com/mochi/mochiweb/pull/186 * Updated mochiweb_html singleton tag heuristic for HTML5 https://github.com/mochi/mochiweb/pull/190 * Send 400 Bad Request if request line exceeds recbuf (regression fix) https://github.com/mochi/mochiweb/pull/191 Version 2.16.0 released 2016-12-19 * Added support for encoding maps to mochijson2 (where available) https://github.com/mochi/mochiweb/pull/184 * Added missing RFC1918 address spaces to the allowed x-forwarded-for header https://github.com/mochi/mochiweb/pull/183 Version 2.15.1 released 2016-06-24 * Fixed deprecation warnings in Erlang/OTP 19.0 https://github.com/mochi/mochiweb/pull/177 Version 2.15.0 released 2016-05-08 * mochiweb_request now normalizes paths such that duplicate slashes are discarded (and thus all path segments except the last are non-empty). https://github.com/mochi/mochiweb/pull/173 Version 2.14.0 released 2016-04-11 * mochiweb_html now requires a letter to begin a HTML tag https://github.com/mochi/mochiweb/pull/171 Version 2.13.2 released 2016-03-18 * Allow mochijson2 to handle code points that xmerl_ucs considered invalid https://github.com/mochi/mochiweb/issues/168 Version 2.13.1 released 2016-03-13 * Fix mochiweb_html regression parsing invalid charref sequences https://github.com/mochi/mochiweb/issues/167 Version 2.13.0 released 2016-02-08 * Support parsing of UTF-16 surrogate pairs encoded as character references in mochiweb_html https://github.com/mochi/mochiweb/issues/164 * Avoid swallowing messages that are not related to the socket during request parsing https://github.com/mochi/mochiweb/pull/161 * Ensure correct ordering of Set-Cookie headers: first in, first out https://github.com/mochi/mochiweb/issues/162 * Improve response times by caching a formatted date once per second for the response headers with a mochiweb_clock service https://github.com/mochi/mochiweb/pull/158 Version 2.12.2 released 2015-02-21 * Close connections quietly when setopts fails with a closed socket. https://github.com/mochi/mochiweb/pull/152 Version 2.12.1 released 2015-02-01 * Fix active_socket accounting https://github.com/mochi/mochiweb/issues/149 * Added full MIT license preludes to each source file to make it easier for mochiweb's code to be used piecemeal https://github.com/mochi/mochiweb/pull/148 Version 2.12.0 released 2015-01-16 * Send "Connection: close" header when the server is going to close a Keep-Alive connection, usually due to unread data from the client https://github.com/mochi/mochiweb/issues/146 Version 2.11.2 released 2015-01-16 * Fix regression introduced in #147 https://github.com/mochi/mochiweb/pull/147 Version 2.11.1 released 2015-01-16 * Accept range end position which exceededs the resource size https://github.com/mochi/mochiweb/pull/147 Version 2.11.0 released 2015-01-12 * Perform SSL handshake after releasing acceptor back into the pool, and slow accept rate when file descriptors are not available, to mitigate a potential DoS attack. Adds new mochiweb_socket functions transport_accept/1 and finish_accept/1 which should be used in preference to the now deprecated accept/1 function. https://github.com/mochi/mochiweb/issues/138 Version 2.10.1 released 2015-01-11 * Fixes issue with SSL and mochiweb_websocket. Note that mochiweb_websocket is still experimental and the API is subject to change in future versions. https://github.com/mochi/mochiweb/pull/144 Version 2.10.0 released 2014-12-17 * Added new `recbuf` option to mochiweb_http to allow the receive buffer to be configured. https://github.com/mochi/mochiweb/pull/134 Version 2.9.2 released 2014-10-16 * Add timeouts to SSL connect to prevent DoS by opening a connection and not doing anything. https://github.com/mochi/mochiweb/pull/140 * Prevent using ECDH cipher in R16B because it is broken https://github.com/mochi/mochiweb/pull/140 * For default SSL connections, remove usage of sslv3 and not-so-secure ciphers. https://github.com/mochi/mochiweb/pull/140 Version 2.9.1 released 2014-09-29 * Fix Makefile rule for building docs https://github.com/mochi/mochiweb/issues/135 * Minimize gen_tcp:send calls to optimize performance. https://github.com/mochi/mochiweb/pull/137 Version 2.9.0 released 2014-06-24 * Increased timeout in test suite for FreeBSD https://github.com/mochi/mochiweb/pull/121 * Updated rebar to v2.5.0 and fixed associated build issues https://github.com/mochi/mochiweb/issues/131 Version 2.8.0 released 2014-01-01 * Websocket support https://github.com/mochi/mochiweb/pull/120 * Force files named "crossdomain.xml" to have MIME type text/x-cross-domain-policy. https://github.com/mochi/mochiweb/pull/118 Version 2.7.0 released 2013-08-01 * Fix 0-length range responses https://github.com/mochi/mochiweb/pull/87 * Add support for all possible `erlang:decode_packet/3` responses, previously these would just crash. https://github.com/mochi/mochiweb/pull/114 * Makefile fixed to make `make test` work before `make all` https://github.com/mochi/mochiweb/pull/116 * Usage of the crypto module made R16B01+ compatible https://github.com/mochi/mochiweb/pull/115 * Build fixed for R16B01 https://github.com/mochi/mochiweb/pull/112 * `mochiweb_socket_server:stop/1` is now a synchronous call instead of an asynchronous cast * `mochiweb_html:parse_tokens/1` (and `parse/1`) will now create a html element to wrap documents that have a HTML5 doctype (``) but no html element https://github.com/mochi/mochiweb/issues/110 Version 2.6.0 released 2013-04-15 * Enable R15B gen_tcp workaround only on R15B https://github.com/mochi/mochiweb/pull/107 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) * Fix mochiweb_html handling of invalid charref sequences (unescaped &) (#69). * Add a manual garbage collection between requests to avoid worst case behavior on keep-alive sockets. * Fix dst cookie bug (#73) * Removed unnecessary template_dir option, see https://github.com/basho/rebar/issues/203 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. mochiweb-3.2.1/LICENSE000066400000000000000000000020771450333227400143300ustar00rootroot00000000000000This is the MIT license. Copyright (c) 2007 Mochi Media, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. mochiweb-3.2.1/Makefile000066400000000000000000000003551450333227400147600ustar00rootroot00000000000000REBAR?=rebar3 .PHONY: all edoc test clean build all: build build: @$(REBAR) get-deps # rebar2 compatibility, it's no-op on rebar3 @$(REBAR) compile test: build @$(REBAR) eunit edoc: build @$(REBAR) edoc clean: @$(REBAR) clean mochiweb-3.2.1/README.md000066400000000000000000000035631450333227400146030ustar00rootroot00000000000000# MochiWeb MochiWeb is an Erlang library for building lightweight HTTP servers. ## Overview MochiWeb provides a lightweight and fast solution for building HTTP servers in Erlang. The library provides features for building robust and scalable HTTP servers. ## Getting Started Before you can use MochiWeb, you'll need to have [Erlang OTP](https://www.erlang.org/) installed. Once you have Erlang OTP installed, you can download the latest version of MochiWeb from the [GitHub repository](https://github.com/mochi/mochiweb). For a MochiWeb project, first obtain a copy of MochiWeb using Git by running the command. ```erlang-repl $ git clone git://github.com/mochi/mochiweb.git. ``` To create a project. ```erlang-repl $ cd mochiweb $ make app PROJECT=exampleName ``` You can now start the project with. ```erlang-repl $ cd ../exampleName/ $ make $ ./start-dev.sh ``` You can access the app by navigating to http://localhost:8080 in your browser. For an example, view the `example_project` in the `examples/` folder. ## Benefits * Lightweight: MochiWeb is designed to be lightweight and fast, making it ideal for use in resource-constrained environments. * Easy to use: MochiWeb provides a simple and intuitive API that makes it easy to get started with building HTTP servers. * Robust: MochiWeb provides a comprehensive set of features for building robust and scalable HTTP servers. ## Documentations and Resources [Information about Rebar (Erlang build tool)](https://github.com/erlang/rebar3) [Mailing list](https://groups.google.com/group/mochiweb/) ## OTP 21.2, 21.2.1, 21.2.2 warning OTP 21.2 (up to and including 21.2.2) introduced an SSL regression that makes these releases unsafe to use. See [ERL-830](https://bugs.erlang.org/browse/ERL-830). This issue was resolved in OTP 21.2.3. ## Contributing MochiWeb is an open-source project and welcomes contributions from the community. mochiweb-3.2.1/erlang_ls.config000066400000000000000000000001451450333227400164520ustar00rootroot00000000000000apps_dirs: - "_build/default/lib/*" include_dirs: - "_build/default/lib/*/include" - "include" mochiweb-3.2.1/examples/000077500000000000000000000000001450333227400151335ustar00rootroot00000000000000mochiweb-3.2.1/examples/example_project/000077500000000000000000000000001450333227400203145ustar00rootroot00000000000000mochiweb-3.2.1/examples/example_project/.gitignore000066400000000000000000000001531450333227400223030ustar00rootroot00000000000000/ebin /doc /_test /.eunit /docs .DS_Store /TEST-*.xml /deps /.rebar *.swp *.beam *.dump _build/ rebar.lock mochiweb-3.2.1/examples/example_project/Makefile000066400000000000000000000003231450333227400217520ustar00rootroot00000000000000REBAR?=rebar3 .PHONY: all edoc test clean app build: @$(REBAR) get-deps # rebar2 compatibility, it's no-op on rebar3 @$(REBAR) compile test: @$(REBAR) eunit edoc: @$(REBAR) edoc clean: @$(REBAR) clean mochiweb-3.2.1/examples/example_project/bench.sh000077500000000000000000000004661450333227400217400ustar00rootroot00000000000000#!/bin/sh # workaround for rebar mustache template bug DEFAULT_PORT=8080 HOST=${HOST:-127.0.0.1} PORT=${PORT:-${DEFAULT_PORT}} BENCH_RUN="siege -q -c400 -r100 -b http://$HOST:$PORT/hello_world" sleep 120 echo "" echo "" for i in `seq 1 10`; do echo "Running test #$i:" $BENCH_RUN sleep 90 done mochiweb-3.2.1/examples/example_project/priv/000077500000000000000000000000001450333227400212745ustar00rootroot00000000000000mochiweb-3.2.1/examples/example_project/priv/www/000077500000000000000000000000001450333227400221205ustar00rootroot00000000000000mochiweb-3.2.1/examples/example_project/priv/www/index.html000066400000000000000000000001371450333227400241160ustar00rootroot00000000000000 It Worked example_project running. mochiweb-3.2.1/examples/example_project/rebar.config000066400000000000000000000003471450333227400226020ustar00rootroot00000000000000%% -*- erlang -*- {erl_opts, [debug_info]}. {deps, [ {mochiweb, ".*", {git, "https://github.com/mochi/mochiweb.git", {branch, "main"}}}]}. {cover_enabled, true}. {eunit_opts, [verbose, {report,{eunit_surefire,[{dir,"."}]}}]}. mochiweb-3.2.1/examples/example_project/src/000077500000000000000000000000001450333227400211035ustar00rootroot00000000000000mochiweb-3.2.1/examples/example_project/src/example_project.app.src000066400000000000000000000003531450333227400255550ustar00rootroot00000000000000%% -*- erlang -*- {application, example_project, [{description, "example_project"}, {vsn, "0.1"}, {modules, []}, {registered, []}, {mod, {'example_project_app', []}}, {env, []}, {applications, [kernel, stdlib, crypto]}]}. mochiweb-3.2.1/examples/example_project/src/example_project.erl000066400000000000000000000012411450333227400247660ustar00rootroot00000000000000%% @author Mochi Media %% @copyright 2010 Mochi Media %% @doc example_project. -module(example_project). -author("Mochi Media "). -export([start/0, stop/0]). ensure_started(App) -> case application:start(App) of ok -> ok; {error, {already_started, App}} -> ok end. %% @spec start() -> ok %% @doc Start the example_project server. start() -> example_project_deps:ensure(), ensure_started(crypto), application:start(example_project). %% @spec stop() -> ok %% @doc Stop the example_project server. stop() -> application:stop(example_project). mochiweb-3.2.1/examples/example_project/src/example_project_app.erl000066400000000000000000000011451450333227400256310ustar00rootroot00000000000000%% @author Mochi Media %% @copyright example_project Mochi Media %% @doc Callbacks for the example_project application. -module(example_project_app). -author("Mochi Media "). -behaviour(application). -export([start/2,stop/1]). %% @spec start(_Type, _StartArgs) -> ServerRet %% @doc application start callback for example_project. start(_Type, _StartArgs) -> example_project_deps:ensure(), example_project_sup:start_link(). %% @spec stop(_State) -> ServerRet %% @doc application stop callback for example_project. stop(_State) -> ok. mochiweb-3.2.1/examples/example_project/src/example_project_deps.erl000066400000000000000000000061201450333227400260020ustar00rootroot00000000000000%% @author Mochi Media %% @copyright 2010 Mochi Media %% @doc Ensure that the relatively-installed dependencies are on the code %% loading path, and locate resources relative %% to this application's path. -module(example_project_deps). -author("Mochi Media "). -export([ensure/0, ensure/1]). -export([get_base_dir/0, get_base_dir/1]). -export([local_path/1, local_path/2]). -export([deps_on_path/0, new_siblings/1]). %% @spec deps_on_path() -> [ProjNameAndVers] %% @doc List of project dependencies on the path. deps_on_path() -> F = fun (X, Acc) -> ProjDir = filename:dirname(X), case {filename:basename(X), filename:basename(filename:dirname(ProjDir))} of {"ebin", "deps"} -> [filename:basename(ProjDir) | Acc]; _ -> Acc end end, ordsets:from_list(lists:foldl(F, [], code:get_path())). %% @spec new_siblings(Module) -> [Dir] %% @doc Find new siblings paths relative to Module that aren't already on the %% code path. new_siblings(Module) -> Existing = deps_on_path(), SiblingEbin = filelib:wildcard(local_path(["deps", "*", "ebin"], Module)), Siblings = [filename:dirname(X) || X <- SiblingEbin, ordsets:is_element( filename:basename(filename:dirname(X)), Existing) =:= false], lists:filter(fun filelib:is_dir/1, lists:append([[filename:join([X, "ebin"]), filename:join([X, "include"])] || X <- Siblings])). %% @spec ensure(Module) -> ok %% @doc Ensure that all ebin and include paths for dependencies %% of the application for Module are on the code path. ensure(Module) -> code:add_paths(new_siblings(Module)), code:clash(), ok. %% @spec ensure() -> ok %% @doc Ensure that the ebin and include paths for dependencies of %% this application are on the code path. Equivalent to %% ensure(?Module). ensure() -> ensure(?MODULE). %% @spec get_base_dir(Module) -> string() %% @doc Return the application directory for Module. It assumes Module is in %% a standard OTP layout application in the ebin or src directory. get_base_dir(Module) -> {file, Here} = code:is_loaded(Module), filename:dirname(filename:dirname(Here)). %% @spec get_base_dir() -> string() %% @doc Return the application directory for this application. Equivalent to %% get_base_dir(?MODULE). get_base_dir() -> get_base_dir(?MODULE). %% @spec local_path([string()], Module) -> string() %% @doc Return an application-relative directory from Module's application. local_path(Components, Module) -> filename:join([get_base_dir(Module) | Components]). %% @spec local_path(Components) -> string() %% @doc Return an application-relative directory for this application. %% Equivalent to local_path(Components, ?MODULE). local_path(Components) -> local_path(Components, ?MODULE). mochiweb-3.2.1/examples/example_project/src/example_project_sup.erl000066400000000000000000000030721450333227400256610ustar00rootroot00000000000000%% @author Mochi Media %% @copyright 2010 Mochi Media %% @doc Supervisor for the example_project application. -module(example_project_sup). -author("Mochi Media "). -behaviour(supervisor). %% External exports -export([start_link/0, upgrade/0]). %% supervisor callbacks -export([init/1]). %% @spec start_link() -> ServerRet %% @doc API for starting the supervisor. start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). %% @spec upgrade() -> ok %% @doc Add processes if necessary. upgrade() -> {ok, {_, Specs}} = init([]), Old = sets:from_list( [Name || {Name, _, _, _} <- supervisor:which_children(?MODULE)]), New = sets:from_list([Name || {Name, _, _, _, _, _} <- Specs]), Kill = sets:subtract(Old, New), sets:fold(fun (Id, ok) -> supervisor:terminate_child(?MODULE, Id), supervisor:delete_child(?MODULE, Id), ok end, ok, Kill), [supervisor:start_child(?MODULE, Spec) || Spec <- Specs], ok. %% @spec init([]) -> SupervisorTree %% @doc supervisor callback. init([]) -> Web = web_specs(example_project_web, 8080), Processes = [Web], Strategy = {one_for_one, 10, 10}, {ok, {Strategy, lists:flatten(Processes)}}. web_specs(Mod, Port) -> WebConfig = [{ip, {0,0,0,0}}, {port, Port}, {docroot, example_project_deps:local_path(["priv", "www"])}], {Mod, {Mod, start, [WebConfig]}, permanent, 5000, worker, dynamic}. mochiweb-3.2.1/examples/example_project/src/example_project_web.erl000066400000000000000000000042421450333227400256270ustar00rootroot00000000000000%% @author Mochi Media %% @copyright 2010 Mochi Media %% @doc Web server for example_project. -module('example_project_web'). -author("Mochi Media "). -export([loop/2, start/1, stop/0]). %% External API start(Options) -> {DocRoot, Options1} = get_option(docroot, Options), Loop = fun (Req) -> (?MODULE):loop(Req, DocRoot) end, mochiweb_http:start([{name, ?MODULE}, {loop, Loop} | Options1]). stop() -> mochiweb_http:stop(?MODULE). %% OTP 21 is the first to define OTP_RELEASE and the first to support %% EEP-0047 direct stack trace capture. -ifdef(OTP_RELEASE). -if((?OTP_RELEASE) >= 21). -define(HAS_DIRECT_STACKTRACE, true). -endif. -endif. -ifdef(HAS_DIRECT_STACKTRACE). - define ( CAPTURE_EXC_PRE ( Type , What , Trace ) , Type : What : Trace ) . -define(CAPTURE_EXC_GET(Trace), Trace). -else. -define(CAPTURE_EXC_PRE(Type, What, Trace), Type:What). -define(CAPTURE_EXC_GET(Trace), erlang:get_stacktrace()). -endif. loop ( Req , DocRoot ) -> "/" ++ Path = mochiweb_request : get ( path , Req ) , try case mochiweb_request : get ( method , Req ) of Method when Method =:= 'GET' ; Method =:= 'HEAD' -> case Path of "hello_world" -> mochiweb_request : respond ( { 200 , [ { "Content-Type" , "text/plain" } ] , "Hello world!\n" } , Req ) ; _ -> mochiweb_request : serve_file ( Path , DocRoot , Req ) end ; 'POST' -> case Path of _ -> mochiweb_request : not_found ( Req ) end ; _ -> mochiweb_request : respond ( { 501 , [ ] , [ ] } , Req ) end catch ? CAPTURE_EXC_PRE ( Type , What , Trace ) -> Report = [ "web request failed" , { path , Path } , { type , Type } , { what , What } , { trace , ? CAPTURE_EXC_GET ( Trace ) } ] , error_logger : error_report ( Report ) , mochiweb_request : respond ( { 500 , [ { "Content-Type" , "text/plain" } ] , "request failed, sorry\n" } , Req ) end . %% Internal API get_option(Option, Options) -> {proplists:get_value(Option, Options), proplists:delete(Option, Options)}. %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). you_should_write_a_test() -> ?assertEqual("No, but I will!", "Have you written any tests?"), ok. -endif. mochiweb-3.2.1/examples/example_project/start-dev.sh000077500000000000000000000003561450333227400225700ustar00rootroot00000000000000#!/bin/sh exec erl \ -pa _build/default/lib/example_project/ebin \ -pa _build/default/lib/mochiweb/ebin \ -pa ebin deps/*/ebin \ -boot start_sasl \ -sname example_project_dev \ -s example_project \ -s reloader mochiweb-3.2.1/examples/hmac_api/000077500000000000000000000000001450333227400166745ustar00rootroot00000000000000mochiweb-3.2.1/examples/hmac_api/README000066400000000000000000000204571450333227400175640ustar00rootroot00000000000000Introduction ------------ 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 key pair 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: 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 = mochiweb_request:get(path, Req), ... 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) mochiweb-3.2.1/examples/hmac_api/hmac_api.hrl000066400000000000000000000036441450333227400211530ustar00rootroot00000000000000-author("Hypernumbers Ltd "). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% %%% %%% 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 }). mochiweb-3.2.1/examples/hmac_api/hmac_api_client.erl000066400000000000000000000023641450333227400225040ustar00rootroot00000000000000-module(hmac_api_client). -export([ fire/0 ]). -include("hmac_api.hrl"). -author("Hypernumbers Ltd "). 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}, [], []). mochiweb-3.2.1/examples/hmac_api/hmac_api_lib.erl000066400000000000000000000351701450333227400217750ustar00rootroot00000000000000-module(hmac_api_lib). -include("hmac_api.hrl"). -include_lib("eunit/include/eunit.hrl"). -author("Hypernumbers Ltd "). %%% 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 argument %%% 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, get_api_keypair/0, sign/5]). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% %%% %%% API %%% %%% %%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% authorize_request(Req) -> Method = mochiweb_request:get(method, Req), Path = mochiweb_request:get(path, Req), Headers = normalise(mochiweb_headers:to_list(mochiweb_request:get(headers, Req))), 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 = "[ * | \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/jo" "hnsmith/?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/di" "ctionary/" ++ "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:b" "ongwater\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&zbis" "h=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&ZBis" "h=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&ZBis" "h=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&ZBis" "h=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}]}. mochiweb-3.2.1/examples/https/000077500000000000000000000000001450333227400162755ustar00rootroot00000000000000mochiweb-3.2.1/examples/https/https_store.erl000066400000000000000000000071361450333227400213660ustar00rootroot00000000000000%% 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([dispatch/1, loop/1, start/0, stop/0]). -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 mochiweb_request:get(method, Req) of 'GET' -> get_resource(Req); 'PUT' -> put_resource(Req); 'DELETE' -> delete_resource(Req); _ -> Headers = [{"Allow", "GET,PUT,DELETE"}], mochiweb_request:respond({405, Headers, "405 Method Not Allowed\r\n"}, Req) end. get_resource(Req) -> Path = mochiweb_request:get(path, Req), case ets:lookup(?MODULE, Path) of [{Path, #resource{type = Type, data = Data}}] -> mochiweb_request:ok({Type, Data}, Req); [] -> mochiweb_request:respond({404, [], "404 Not Found\r\n"}, Req) end. put_resource(Req) -> ContentType = case mochiweb_request:get_header_value("Content-Type", Req) of undefined -> "application/octet-stream"; S -> S end, Resource = #resource{type = ContentType, data = mochiweb_request:recv_body(Req)}, http_store ! {self(), {put, mochiweb_request:get(path, Req), Resource}}, Pid = whereis(http_store), receive {Pid, created} -> mochiweb_request:respond({201, [], "201 Created\r\n"}, Req); {Pid, updated} -> mochiweb_request:respond({200, [], "200 OK\r\n"}, Req) end. delete_resource(Req) -> http_store ! {self(), {delete, mochiweb_request:get(path, Req)}}, Pid = whereis(http_store), receive {Pid, ok} -> mochiweb_request:respond({200, [], "200 OK\r\n"}, Req) 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). mochiweb-3.2.1/examples/https/server_cert.pem000066400000000000000000000021671450333227400213310ustar00rootroot00000000000000-----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----- mochiweb-3.2.1/examples/https/server_key.pem000066400000000000000000000032171450333227400211610ustar00rootroot00000000000000-----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----- mochiweb-3.2.1/examples/keepalive/000077500000000000000000000000001450333227400171005ustar00rootroot00000000000000mochiweb-3.2.1/examples/keepalive/keepalive.erl000066400000000000000000000062421450333227400215550ustar00rootroot00000000000000-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([loop/1, start/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 = mochiweb_request:get(path, Req), 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", [mochiweb_request:get(path, Req)]), Reentry(Req). ok(Req, Response) -> mochiweb_request:ok({_ContentType = "text/plain", _Headers = [], Response}, Req). mochiweb-3.2.1/examples/websocket/000077500000000000000000000000001450333227400171215ustar00rootroot00000000000000mochiweb-3.2.1/examples/websocket/index.html000066400000000000000000000027541450333227400211260ustar00rootroot00000000000000 Websockets With Mochiweb Demo

Mochiweb websocket demo

  State:

Protip: open your javascript error console, just in case..


mochiweb-3.2.1/examples/websocket/websocket.erl000066400000000000000000000125261450333227400216210ustar00rootroot00000000000000-module(websocket). %% To run: erlc websocket.erl && erl -pa ../../ebin -s websocket %% The MIT License (MIT) %% Copyright (c) 2012 Zadane.pl sp. z o.o. %% Permission is hereby granted, free of charge, to any person obtaining a copy %% of this software and associated documentation files (the "Software"), to deal %% in the Software without restriction, including without limitation the rights %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell %% copies of the Software, and to permit persons to whom the Software is %% furnished to do so, subject to the following conditions: %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN %% THE SOFTWARE. -export([start/0, start_link/0, ws_loop/3, loop/2]). -export([broadcast_server/1]). %% %% Mochiweb websocket example %% %% [1]: At first you have to start HTTP server which will listen for HTTP %% requests and eventually upgrade connection to websocket %% [2]: Attempt to upgrade connection to websocket. %% Function mochiweb_websocket:upgrade_connection/2: %% * first argument is mochiweb_request %% * second is M:F which will handle further websocket messages. %% Function return two funs: %% * ReentryWs/1 - use it to enter to messages handling loop %% (in this example ws_loop/3) %% * ReplyChannel/1 - use to send messages to client. May be passed to %% other processes %% [3]: Example of sending message to client %% [4]: State that will be passed to message handling loop %% [5]: Pass control to messages handling loop. From this moment each message %% received from client can be handled... %% [6]: ...here as Payload. State is variable intended for holding your custom %% state. ReplyChannel is the same function as in [3]. %% Notice! Payload is list of messages received from client. Websocket %% framing mechanism concatenates messages which are sent one after another %% in short time. %% [7]: Print payload received from client and send it back %% [8]: Message handling function must return new state value start() -> spawn( fun () -> application:start(sasl), start_link(), receive stop -> ok end end). start_link() -> %% [1] io:format("Listening at http://127.0.0.1:8080/~n"), Broadcaster = spawn_link(?MODULE, broadcast_server, [dict:new()]), mochiweb_http:start_link([ {name, client_access}, {loop, {?MODULE, loop, [Broadcaster]}}, {port, 8080} ]). ws_loop(Payload, Broadcaster, _ReplyChannel) -> %% [6] %% [7] io:format("Received data: ~p~n", [Payload]), Received = list_to_binary(Payload), Broadcaster ! {broadcast, self(), Received}, %% [8] Broadcaster. loop(Req, Broadcaster) -> H = mochiweb_request:get_header_value("Upgrade", Req), loop(Req, Broadcaster, H =/= undefined andalso string:to_lower(H) =:= "websocket"). loop(Req, _Broadcaster, false) -> mochiweb_request:serve_file("index.html", "./", Req); loop(Req, Broadcaster, true) -> {ReentryWs, ReplyChannel} = mochiweb_websocket:upgrade_connection( Req, fun ?MODULE:ws_loop/3), %% [3] Broadcaster ! {register, self(), ReplyChannel}, %% [4] %% [5] ReentryWs(Broadcaster). %% This server keeps track of connected pids broadcast_server(Pids) -> Pids1 = receive {register, Pid, Channel} -> broadcast_register(Pid, Channel, Pids); {broadcast, Pid, Message} -> broadcast_sendall(Pid, Message, Pids); {'DOWN', MRef, process, Pid, _Reason} -> broadcast_down(Pid, MRef, Pids); Msg -> io:format("Unknown message: ~p~n", [Msg]), Pids end, erlang:hibernate(?MODULE, broadcast_server, [Pids1]). broadcast_register(Pid, Channel, Pids) -> MRef = erlang:monitor(process, Pid), broadcast_sendall( Pid, "connected", dict:store(Pid, {Channel, MRef}, Pids)). broadcast_down(Pid, MRef, Pids) -> Pids1 = case dict:find(Pid, Pids) of {ok, {_, MRef}} -> dict:erase(Pid, Pids); _ -> Pids end, broadcast_sendall(Pid, "disconnected", Pids1). broadcast_sendall(Pid, Msg, Pids) -> M = iolist_to_binary([pid_to_list(Pid), ": ", Msg]), dict:fold( fun (K, {Reply, MRef}, Acc) -> try begin Reply(M), dict:store(K, {Reply, MRef}, Acc) end catch _:_ -> Acc end end, dict:new(), Pids). mochiweb-3.2.1/include/000077500000000000000000000000001450333227400147405ustar00rootroot00000000000000mochiweb-3.2.1/include/internal.hrl000066400000000000000000000000361450333227400172620ustar00rootroot00000000000000 -define(RECBUF_SIZE, 8192). mochiweb-3.2.1/rebar.config000066400000000000000000000015121450333227400155760ustar00rootroot00000000000000% -*- mode: erlang -*- {erl_opts, [debug_info, {platform_define, "^(18|19|20|21|22)", new_crypto_unavailable}, {platform_define, "^(18|19|20)", ssl_handshake_unavailable}, {platform_define, "^21-", otp_21}]}. {cover_enabled, true}. {eunit_opts, [verbose, {report,{eunit_surefire,[{dir,"."}]}}]}. {edoc_opts, [{preprocess, true}]}. {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]}]}. mochiweb-3.2.1/scripts/000077500000000000000000000000001450333227400150045ustar00rootroot00000000000000mochiweb-3.2.1/scripts/entities.erl000077500000000000000000000026211450333227400173400ustar00rootroot00000000000000#!/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, <> = 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. mochiweb-3.2.1/src/000077500000000000000000000000001450333227400141045ustar00rootroot00000000000000mochiweb-3.2.1/src/mochifmt.erl000066400000000000000000000371341450333227400164260ustar00rootroot00000000000000%% @author Bob Ippolito %% @copyright 2008 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc String Formatting for Erlang, inspired by Python 2.6 %% (PEP 3101). %% -module(mochifmt). -author('bob@mochimedia.com'). -export([convert_field/2, format/2, format_field/2, get_field/2, get_value/2]). -export([format/3, format_field/3, get_field/3, tokenize/1]). -export([bformat/2, bformat/3]). -export([f/2, f/3]). -record(conversion, {length, precision, ctype, align, fill_char, sign}). %% @spec tokenize(S::string()) -> tokens() %% @doc Tokenize a format string into mochifmt's internal format. tokenize(S) -> {?MODULE, tokenize(S, "", [])}. %% @spec convert_field(Arg, Conversion::conversion()) -> term() %% @doc Process Arg according to the given explicit conversion specifier. convert_field(Arg, "") -> Arg; convert_field(Arg, "r") -> repr(Arg); convert_field(Arg, "s") -> str(Arg). %% @spec get_value(Key::string(), Args::args()) -> term() %% @doc Get the Key from Args. If Args is a tuple then convert Key to %% an integer and get element(1 + Key, Args). If Args is a list and Key %% can be parsed as an integer then use lists:nth(1 + Key, Args), %% otherwise try and look for Key in Args as a proplist, converting %% Key to an atom or binary if necessary. get_value(Key, Args) when is_tuple(Args) -> element(1 + list_to_integer(Key), Args); get_value(Key, Args) when is_list(Args) -> try lists:nth(1 + list_to_integer(Key), Args) catch error:_ -> {_K, V} = proplist_lookup(Key, Args), V end. %% @spec get_field(Key::string(), Args) -> term() %% @doc Consecutively call get_value/2 on parts of Key delimited by ".", %% replacing Args with the result of the previous get_value. This %% is used to implement formats such as {0.0}. get_field(Key, Args) -> get_field(Key, Args, ?MODULE). %% @spec get_field(Key::string(), Args, Module) -> term() %% @doc Consecutively call Module:get_value/2 on parts of Key delimited by ".", %% replacing Args with the result of the previous get_value. This %% is used to implement formats such as {0.0}. get_field(Key, Args, Module) -> {Name, Next} = lists:splitwith(fun (C) -> C =/= $. end, Key), Res = mod_get_value(Name, Args, Module), case Next of "" -> Res; "." ++ S1 -> get_field(S1, Res, Module) end. mod_get_value(Name, Args, Module) -> try tuple_apply(Module, get_value, [Name, Args]) catch error:undef -> get_value(Name, Args) end. tuple_apply(Module, F, Args) when is_atom(Module) -> erlang:apply(Module, F, Args); tuple_apply(Module, F, Args) when is_tuple(Module), is_atom(element(1, Module)) -> erlang:apply(element(1, Module), F, Args ++ [Module]). %% @spec format(Format::string(), Args) -> iolist() %% @doc Format Args with Format. format(Format, Args) -> format(Format, Args, ?MODULE). %% @spec format(Format::string(), Args, Module) -> iolist() %% @doc Format Args with Format using Module. format({?MODULE, Parts}, Args, Module) -> format2(Parts, Args, Module, []); format(S, Args, Module) -> format(tokenize(S), Args, Module). %% @spec format_field(Arg, Format) -> iolist() %% @doc Format Arg with Format. format_field(Arg, Format) -> format_field(Arg, Format, ?MODULE). %% @spec format_field(Arg, Format, _Module) -> iolist() %% @doc Format Arg with Format. format_field(Arg, Format, _Module) -> F = default_ctype(Arg, parse_std_conversion(Format)), fix_padding(fix_sign(convert2(Arg, F), F), F). %% @spec f(Format::string(), Args) -> string() %% @doc Format Args with Format and return a string(). f(Format, Args) -> f(Format, Args, ?MODULE). %% @spec f(Format::string(), Args, Module) -> string() %% @doc Format Args with Format using Module and return a string(). f(Format, Args, Module) -> case lists:member(${, Format) of true -> binary_to_list(bformat(Format, Args, Module)); false -> Format end. %% @spec bformat(Format::string(), Args) -> binary() %% @doc Format Args with Format and return a binary(). bformat(Format, Args) -> iolist_to_binary(format(Format, Args)). %% @spec bformat(Format::string(), Args, Module) -> binary() %% @doc Format Args with Format using Module and return a binary(). bformat(Format, Args, Module) -> iolist_to_binary(format(Format, Args, Module)). %% Internal API add_raw("", Acc) -> Acc; add_raw(S, Acc) -> [{raw, lists:reverse(S)} | Acc]. tokenize([], S, Acc) -> lists:reverse(add_raw(S, Acc)); tokenize("{{" ++ Rest, S, Acc) -> tokenize(Rest, "{" ++ S, Acc); tokenize("{" ++ Rest, S, Acc) -> {Format, Rest1} = tokenize_format(Rest), tokenize(Rest1, "", [{format, make_format(Format)} | add_raw(S, Acc)]); tokenize("}}" ++ Rest, S, Acc) -> tokenize(Rest, "}" ++ S, Acc); tokenize([C | Rest], S, Acc) -> tokenize(Rest, [C | S], Acc). tokenize_format(S) -> tokenize_format(S, 1, []). tokenize_format("}" ++ Rest, 1, Acc) -> {lists:reverse(Acc), Rest}; tokenize_format("}" ++ Rest, N, Acc) -> tokenize_format(Rest, N - 1, "}" ++ Acc); tokenize_format("{" ++ Rest, N, Acc) -> tokenize_format(Rest, 1 + N, "{" ++ Acc); tokenize_format([C | Rest], N, Acc) -> tokenize_format(Rest, N, [C | Acc]). make_format(S) -> {Name0, Spec} = case lists:splitwith(fun (C) -> C =/= $: end, S) of {_, ""} -> {S, ""}; {SN, ":" ++ SS} -> {SN, SS} end, {Name, Transform} = case lists:splitwith(fun (C) -> C =/= $! end, Name0) of {_, ""} -> {Name0, ""}; {TN, "!" ++ TT} -> {TN, TT} end, {Name, Transform, Spec}. proplist_lookup(S, P) -> A = try list_to_existing_atom(S) catch error:_ -> make_ref() end, B = try list_to_binary(S) catch error:_ -> make_ref() end, proplist_lookup2({S, A, B}, P). proplist_lookup2({KS, KA, KB}, [{K, V} | _]) when KS =:= K orelse KA =:= K orelse KB =:= K -> {K, V}; proplist_lookup2(Keys, [_ | Rest]) -> proplist_lookup2(Keys, Rest). format2([], _Args, _Module, Acc) -> lists:reverse(Acc); format2([{raw, S} | Rest], Args, Module, Acc) -> format2(Rest, Args, Module, [S | Acc]); format2([{format, {Key, Convert, Format0}} | Rest], Args, Module, Acc) -> Format = f(Format0, Args, Module), V = case Module of ?MODULE -> V0 = get_field(Key, Args), V1 = convert_field(V0, Convert), format_field(V1, Format); _ -> V0 = try tuple_apply(Module, get_field, [Key, Args]) catch error:undef -> get_field(Key, Args, Module) end, V1 = try tuple_apply(Module, convert_field, [V0, Convert]) catch error:undef -> convert_field(V0, Convert) end, try tuple_apply(Module, format_field, [V1, Format]) catch error:undef -> format_field(V1, Format, Module) end end, format2(Rest, Args, Module, [V | Acc]). default_ctype(_Arg, C = #conversion{ctype = N}) when N =/= undefined -> C; default_ctype(Arg, C) when is_integer(Arg) -> C#conversion{ctype = decimal}; default_ctype(Arg, C) when is_float(Arg) -> C#conversion{ctype = general}; default_ctype(_Arg, C) -> C#conversion{ctype = string}. fix_padding(Arg, #conversion{length = undefined}) -> Arg; fix_padding(Arg, F = #conversion{length = Length, fill_char = Fill0, align = Align0, ctype = Type}) -> Padding = Length - iolist_size(Arg), Fill = case Fill0 of undefined -> $\s; _ -> Fill0 end, Align = case Align0 of undefined -> case Type of string -> left; _ -> right end; _ -> Align0 end, case Padding > 0 of true -> do_padding(Arg, Padding, Fill, Align, F); false -> Arg end. do_padding(Arg, Padding, Fill, right, _F) -> [lists:duplicate(Padding, Fill), Arg]; do_padding(Arg, Padding, Fill, center, _F) -> LPadding = lists:duplicate(Padding div 2, Fill), RPadding = case Padding band 1 of 1 -> [Fill | LPadding]; _ -> LPadding end, [LPadding, Arg, RPadding]; do_padding([$- | Arg], Padding, Fill, sign_right, _F) -> [[$- | lists:duplicate(Padding, Fill)], Arg]; do_padding(Arg, Padding, Fill, sign_right, #conversion{sign = $-}) -> [lists:duplicate(Padding, Fill), Arg]; do_padding([S | Arg], Padding, Fill, sign_right, #conversion{sign = S}) -> [[S | lists:duplicate(Padding, Fill)], Arg]; do_padding(Arg, Padding, Fill, sign_right, #conversion{sign = undefined}) -> [lists:duplicate(Padding, Fill), Arg]; do_padding(Arg, Padding, Fill, left, _F) -> [Arg | lists:duplicate(Padding, Fill)]. fix_sign(Arg, #conversion{sign = $+}) when Arg >= 0 -> [$+, Arg]; fix_sign(Arg, #conversion{sign = $\s}) when Arg >= 0 -> [$\s, Arg]; fix_sign(Arg, _F) -> Arg. ctype($%) -> percent; ctype($s) -> string; ctype($b) -> bin; ctype($o) -> oct; ctype($X) -> upper_hex; ctype($x) -> hex; ctype($c) -> char; ctype($d) -> decimal; ctype($g) -> general; ctype($f) -> fixed; ctype($e) -> exp. align($<) -> left; align($>) -> right; align($^) -> center; align($=) -> sign_right. convert2(Arg, F = #conversion{ctype = percent}) -> [convert2(1.0e+2 * Arg, F#conversion{ctype = fixed}), $%]; convert2(Arg, #conversion{ctype = string}) -> str(Arg); convert2(Arg, #conversion{ctype = bin}) -> erlang:integer_to_list(Arg, 2); convert2(Arg, #conversion{ctype = oct}) -> erlang:integer_to_list(Arg, 8); convert2(Arg, #conversion{ctype = upper_hex}) -> erlang:integer_to_list(Arg, 16); convert2(Arg, #conversion{ctype = hex}) -> string:to_lower(erlang:integer_to_list(Arg, 16)); convert2(Arg, #conversion{ctype = char}) when Arg < 128 -> [Arg]; convert2(Arg, #conversion{ctype = char}) -> xmerl_ucs:to_utf8(Arg); convert2(Arg, #conversion{ctype = decimal}) -> integer_to_list(Arg); convert2(Arg, #conversion{ctype = general, precision = undefined}) -> try mochinum:digits(Arg) catch error:undef -> io_lib:format("~g", [Arg]) end; convert2(Arg, #conversion{ctype = fixed, precision = undefined}) -> io_lib:format("~f", [Arg]); convert2(Arg, #conversion{ctype = exp, precision = undefined}) -> io_lib:format("~e", [Arg]); convert2(Arg, #conversion{ctype = general, precision = P}) -> io_lib:format("~." ++ integer_to_list(P) ++ "g", [Arg]); convert2(Arg, #conversion{ctype = fixed, precision = P}) -> io_lib:format("~." ++ integer_to_list(P) ++ "f", [Arg]); convert2(Arg, #conversion{ctype = exp, precision = P}) -> io_lib:format("~." ++ integer_to_list(P) ++ "e", [Arg]). str(A) when is_atom(A) -> atom_to_list(A); str(I) when is_integer(I) -> integer_to_list(I); str(F) when is_float(F) -> try mochinum:digits(F) catch error:undef -> io_lib:format("~g", [F]) end; str(L) when is_list(L) -> L; str(B) when is_binary(B) -> B; str(P) -> repr(P). repr(P) when is_float(P) -> try mochinum:digits(P) catch error:undef -> float_to_list(P) end; repr(P) -> io_lib:format("~p", [P]). parse_std_conversion(S) -> parse_std_conversion(S, #conversion{}). parse_std_conversion("", Acc) -> Acc; parse_std_conversion([Fill, Align | Spec], Acc) when Align =:= $< orelse Align =:= $> orelse Align =:= $= orelse Align =:= $^ -> parse_std_conversion(Spec, Acc#conversion{fill_char = Fill, align = align(Align)}); parse_std_conversion([Align | Spec], Acc) when Align =:= $< orelse Align =:= $> orelse Align =:= $= orelse Align =:= $^ -> parse_std_conversion(Spec, Acc#conversion{align = align(Align)}); parse_std_conversion([Sign | Spec], Acc) when Sign =:= $+ orelse Sign =:= $- orelse Sign =:= $\s -> parse_std_conversion(Spec, Acc#conversion{sign = Sign}); parse_std_conversion("0" ++ Spec, Acc) -> Align = case Acc#conversion.align of undefined -> sign_right; A -> A end, parse_std_conversion(Spec, Acc#conversion{fill_char = $0, align = Align}); parse_std_conversion(Spec = [D | _], Acc) when D >= $0 andalso D =< $9 -> {W, Spec1} = lists:splitwith(fun (C) -> C >= $0 andalso C =< $9 end, Spec), parse_std_conversion(Spec1, Acc#conversion{length = list_to_integer(W)}); parse_std_conversion([$. | Spec], Acc) -> case lists:splitwith(fun (C) -> C >= $0 andalso C =< $9 end, Spec) of {"", Spec1} -> parse_std_conversion(Spec1, Acc); {P, Spec1} -> parse_std_conversion(Spec1, Acc#conversion{precision = list_to_integer(P)}) end; parse_std_conversion([Type], Acc) -> parse_std_conversion("", Acc#conversion{ctype = ctype(Type)}). %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). tokenize_test() -> {?MODULE, [{raw, "ABC"}]} = tokenize("ABC"), {?MODULE, [{format, {"0", "", ""}}]} = tokenize("{0}"), {?MODULE, [{raw, "ABC"}, {format, {"1", "", ""}}, {raw, "DEF"}]} = tokenize("ABC{1}DEF"), ok. format_test() -> <<" -4">> = bformat("{0:4}", [-4]), <<" 4">> = bformat("{0:4}", [4]), <<" 4">> = bformat("{0:{0}}", [4]), <<"4 ">> = bformat("{0:4}", ["4"]), <<"4 ">> = bformat("{0:{0}}", ["4"]), <<"1.2yoDEF">> = bformat("{2}{0}{1}{3}", {yo, "DE", 1.19999999999999995559, <<"F">>}), <<"cafebabe">> = bformat("{0:x}", {3405691582}), <<"CAFEBABE">> = bformat("{0:X}", {3405691582}), <<"CAFEBABE">> = bformat("{0:X}", {3405691582}), <<"755">> = bformat("{0:o}", {493}), <<"a">> = bformat("{0:c}", {97}), %% Horizontal ellipsis <<226, 128, 166>> = bformat("{0:c}", {8230}), <<"11">> = bformat("{0:b}", {3}), <<"11">> = bformat("{0:b}", [3]), <<"11">> = bformat("{three:b}", [{three, 3}]), <<"11">> = bformat("{three:b}", [{"three", 3}]), <<"11">> = bformat("{three:b}", [{<<"three">>, 3}]), <<"\"foo\"">> = bformat("{0!r}", {"foo"}), <<"2008-5-4">> = bformat("{0.0}-{0.1}-{0.2}", {{2008, 5, 4}}), <<"2008-05-04">> = bformat("{0.0:04}-{0.1:02}-{0.2:02}", {{2008, 5, 4}}), <<"foo6bar-6">> = bformat("foo{1}{0}-{1}", {bar, 6}), <<"-'atom test'-">> = bformat("-{arg!r}-", [{arg, 'atom test'}]), <<"2008-05-04">> = bformat("{0.0:0{1.0}}-{0.1:0{1.1}}-{0.2:0{1.2}}", {{2008, 5, 4}, {4, 2, 2}}), ok. std_test() -> M = mochifmt_std:new(), <<"01">> = bformat("{0}{1}", [0, 1], M), ok. records_test() -> M = mochifmt_records:new([{conversion, record_info(fields, conversion)}]), R = #conversion{length = long, precision = hard, sign = peace}, long = mochifmt_records:get_value("length", R, M), hard = mochifmt_records:get_value("precision", R, M), peace = mochifmt_records:get_value("sign", R, M), <<"long hard">> = bformat("{length} {precision}", R, M), <<"long hard">> = bformat("{0.length} {0.precision}", [R], M), ok. -endif. mochiweb-3.2.1/src/mochifmt_records.erl000066400000000000000000000041511450333227400201400ustar00rootroot00000000000000%% @author Bob Ippolito %% @copyright 2008 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc Formatter that understands records. %% %% Usage: %% %% 1> M = mochifmt_records:new([{rec, record_info(fields, rec)}]), %% M:format("{0.bar}", [#rec{bar=foo}]). %% foo -module(mochifmt_records). -author('bob@mochimedia.com'). -export([new/1, get_value/3]). 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), element(get_rec_index(Atom, Fields, 2), Rec) end catch error:_ -> mochifmt:get_value(Key, Rec) end; get_value(Key, Args, {?MODULE, _Recs}) -> mochifmt:get_value(Key, Args). get_rec_index(Atom, [Atom | _], Index) -> Index; get_rec_index(Atom, [_ | Rest], Index) -> get_rec_index(Atom, Rest, 1 + Index). %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -endif. mochiweb-3.2.1/src/mochifmt_std.erl000066400000000000000000000035041450333227400172720ustar00rootroot00000000000000%% @author Bob Ippolito %% @copyright 2008 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc Template module for a mochifmt formatter. -module(mochifmt_std). -author('bob@mochimedia.com'). -export([new/0, format/3, get_value/3, format_field/3, get_field/3, convert_field/3]). new() -> {?MODULE}. format(Format, Args, {?MODULE}=THIS) -> mochifmt:format(Format, Args, THIS). get_field(Key, Args, {?MODULE}=THIS) -> mochifmt:get_field(Key, Args, THIS). convert_field(Key, Args, {?MODULE}) -> mochifmt:convert_field(Key, Args). get_value(Key, Args, {?MODULE}) -> mochifmt:get_value(Key, Args). format_field(Arg, Format, {?MODULE}=THIS) -> mochifmt:format_field(Arg, Format, THIS). %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -endif. mochiweb-3.2.1/src/mochiglobal.erl000066400000000000000000000075101450333227400170730ustar00rootroot00000000000000%% @author Bob Ippolito %% @copyright 2010 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc Abuse module constant pools as a "read-only shared heap" (since erts 5.6) %% [1]. -module(mochiglobal). -author("Bob Ippolito "). -export([get/1, get/2, put/2, delete/1]). -spec get(atom()) -> any() | undefined. %% @equiv get(K, undefined) get(K) -> get(K, undefined). -spec get(atom(), T) -> any() | T. %% @doc Get the term for K or return Default. get(K, Default) -> get(K, Default, key_to_module(K)). get(_K, Default, Mod) -> try Mod:term() catch error:undef -> Default end. -spec put(atom(), any()) -> ok. %% @doc Store term V at K, replaces an existing term if present. put(K, V) -> put(K, V, key_to_module(K)). put(_K, V, Mod) -> Bin = compile(Mod, V), code:purge(Mod), {module, Mod} = code:load_binary(Mod, atom_to_list(Mod) ++ ".erl", Bin), ok. -spec delete(atom()) -> boolean(). %% @doc Delete term stored at K, no-op if non-existent. delete(K) -> delete(K, key_to_module(K)). delete(_K, Mod) -> code:purge(Mod), code:delete(Mod). -spec key_to_module(atom()) -> atom(). key_to_module(K) -> list_to_atom("mochiglobal:" ++ atom_to_list(K)). -spec compile(atom(), any()) -> binary(). compile(Module, T) -> {ok, Module, Bin} = compile:forms(forms(Module, T), [verbose, report_errors]), Bin. -spec forms(atom(), any()) -> [erl_syntax:syntaxTree()]. forms(Module, T) -> [erl_syntax:revert(X) || X <- term_to_abstract(Module, term, T)]. -spec term_to_abstract(atom(), atom(), any()) -> [erl_syntax:syntaxTree()]. term_to_abstract(Module, Getter, T) -> [%% -module(Module). erl_syntax:attribute( erl_syntax:atom(module), [erl_syntax:atom(Module)]), %% -export([Getter/0]). erl_syntax:attribute( erl_syntax:atom(export), [erl_syntax:list( [erl_syntax:arity_qualifier( erl_syntax:atom(Getter), erl_syntax:integer(0))])]), %% Getter() -> T. erl_syntax:function( erl_syntax:atom(Getter), [erl_syntax:clause([], none, [erl_syntax:abstract(T)])])]. %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). get_put_delete_test() -> K = '$$test$$mochiglobal', delete(K), ?assertEqual( bar, get(K, bar)), try ?MODULE:put(K, baz), ?assertEqual( baz, get(K, bar)), ?MODULE:put(K, wibble), ?assertEqual( wibble, ?MODULE:get(K)) after delete(K) end, ?assertEqual( bar, get(K, bar)), ?assertEqual( undefined, ?MODULE:get(K)), ok. -endif. mochiweb-3.2.1/src/mochihex.erl000066400000000000000000000060661450333227400164240ustar00rootroot00000000000000%% @author Bob Ippolito %% @copyright 2006 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc Utilities for working with hexadecimal strings. -module(mochihex). -author('bob@mochimedia.com'). -export([to_hex/1, to_bin/1, to_int/1, dehex/1, hexdigit/1]). %% @spec to_hex(integer | iolist()) -> string() %% @doc Convert an iolist to a hexadecimal string. to_hex(0) -> "0"; to_hex(I) when is_integer(I), I > 0 -> to_hex_int(I, []); to_hex(B) -> to_hex(iolist_to_binary(B), []). %% @spec to_bin(string()) -> binary() %% @doc Convert a hexadecimal string to a binary. to_bin(L) -> to_bin(L, []). %% @spec to_int(string()) -> integer() %% @doc Convert a hexadecimal string to an integer. to_int(L) -> erlang:list_to_integer(L, 16). %% @spec dehex(char()) -> integer() %% @doc Convert a hex digit to its integer value. dehex(C) when C >= $0, C =< $9 -> C - $0; dehex(C) when C >= $a, C =< $f -> C - $a + 10; dehex(C) when C >= $A, C =< $F -> C - $A + 10. %% @spec hexdigit(integer()) -> char() %% @doc Convert an integer less than 16 to a hex digit. hexdigit(C) when C >= 0, C =< 9 -> C + $0; hexdigit(C) when C =< 15 -> C + $a - 10. %% Internal API to_hex(<<>>, Acc) -> lists:reverse(Acc); to_hex(<>, Acc) -> to_hex(Rest, [hexdigit(C2), hexdigit(C1) | Acc]). to_hex_int(0, Acc) -> Acc; to_hex_int(I, Acc) -> to_hex_int(I bsr 4, [hexdigit(I band 15) | Acc]). to_bin([], Acc) -> iolist_to_binary(lists:reverse(Acc)); to_bin([C1, C2 | Rest], Acc) -> to_bin(Rest, [(dehex(C1) bsl 4) bor dehex(C2) | Acc]). %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). to_hex_test() -> "ff000ff1" = to_hex([255, 0, 15, 241]), "ff000ff1" = to_hex(16#ff000ff1), "0" = to_hex(16#0), ok. to_bin_test() -> <<255, 0, 15, 241>> = to_bin("ff000ff1"), <<255, 0, 10, 161>> = to_bin("Ff000aA1"), ok. to_int_test() -> 16#ff000ff1 = to_int("ff000ff1"), 16#ff000aa1 = to_int("FF000Aa1"), 16#0 = to_int("0"), ok. -endif. mochiweb-3.2.1/src/mochijson.erl000066400000000000000000000473231450333227400166120ustar00rootroot00000000000000%% @author Bob Ippolito %% @copyright 2006 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc Yet another JSON (RFC 4627) library for Erlang. -module(mochijson). -author('bob@mochimedia.com'). -export([encoder/1, encode/1]). -export([decoder/1, decode/1]). -export([binary_encoder/1, binary_encode/1]). -export([binary_decoder/1, binary_decode/1]). % This is a macro to placate syntax highlighters.. -define(Q, $\"). -define(ADV_COL(S, N), S#decoder{column=N+S#decoder.column}). -define(INC_COL(S), S#decoder{column=1+S#decoder.column}). -define(INC_LINE(S), S#decoder{column=1, line=1+S#decoder.line}). %% @type json_string() = atom | string() | binary() %% @type json_number() = integer() | float() %% @type json_array() = {array, [json_term()]} %% @type json_object() = {struct, [{json_string(), json_term()}]} %% @type json_term() = json_string() | json_number() | json_array() | %% json_object() %% @type encoding() = utf8 | unicode %% @type encoder_option() = {input_encoding, encoding()} | %% {handler, function()} %% @type decoder_option() = {input_encoding, encoding()} | %% {object_hook, function()} %% @type bjson_string() = binary() %% @type bjson_number() = integer() | float() %% @type bjson_array() = [bjson_term()] %% @type bjson_object() = {struct, [{bjson_string(), bjson_term()}]} %% @type bjson_term() = bjson_string() | bjson_number() | bjson_array() | %% bjson_object() %% @type binary_encoder_option() = {handler, function()} %% @type binary_decoder_option() = {object_hook, function()} -record(encoder, {input_encoding=unicode, handler=null}). -record(decoder, {input_encoding=utf8, object_hook=null, line=1, column=1, state=null}). %% @spec encoder([encoder_option()]) -> function() %% @doc Create an encoder/1 with the given options. encoder(Options) -> State = parse_encoder_options(Options, #encoder{}), fun (O) -> json_encode(O, State) end. %% @spec encode(json_term()) -> iolist() %% @doc Encode the given as JSON to an iolist. encode(Any) -> json_encode(Any, #encoder{}). %% @spec decoder([decoder_option()]) -> function() %% @doc Create a decoder/1 with the given options. decoder(Options) -> State = parse_decoder_options(Options, #decoder{}), fun (O) -> json_decode(O, State) end. %% @spec decode(iolist()) -> json_term() %% @doc Decode the given iolist to Erlang terms. decode(S) -> json_decode(S, #decoder{}). %% @spec binary_decoder([binary_decoder_option()]) -> function() %% @doc Create a binary_decoder/1 with the given options. binary_decoder(Options) -> mochijson2:decoder(Options). %% @spec binary_encoder([binary_encoder_option()]) -> function() %% @doc Create a binary_encoder/1 with the given options. binary_encoder(Options) -> mochijson2:encoder(Options). %% @spec binary_encode(bjson_term()) -> iolist() %% @doc Encode the given as JSON to an iolist, using lists for arrays and %% binaries for strings. binary_encode(Any) -> mochijson2:encode(Any). %% @spec binary_decode(iolist()) -> bjson_term() %% @doc Decode the given iolist to Erlang terms, using lists for arrays and %% binaries for strings. binary_decode(S) -> mochijson2:decode(S). %% Internal API parse_encoder_options([], State) -> State; parse_encoder_options([{input_encoding, Encoding} | Rest], State) -> parse_encoder_options(Rest, State#encoder{input_encoding=Encoding}); parse_encoder_options([{handler, Handler} | Rest], State) -> parse_encoder_options(Rest, State#encoder{handler=Handler}). parse_decoder_options([], State) -> State; parse_decoder_options([{input_encoding, Encoding} | Rest], State) -> parse_decoder_options(Rest, State#decoder{input_encoding=Encoding}); parse_decoder_options([{object_hook, Hook} | Rest], State) -> parse_decoder_options(Rest, State#decoder{object_hook=Hook}). json_encode(true, _State) -> "true"; json_encode(false, _State) -> "false"; json_encode(null, _State) -> "null"; json_encode(I, _State) when is_integer(I) -> integer_to_list(I); json_encode(F, _State) when is_float(F) -> mochinum:digits(F); json_encode(L, State) when is_list(L); is_binary(L); is_atom(L) -> json_encode_string(L, State); json_encode({array, Props}, State) when is_list(Props) -> json_encode_array(Props, State); json_encode({struct, Props}, State) when is_list(Props) -> json_encode_proplist(Props, State); json_encode(Bad, #encoder{handler=null}) -> exit({json_encode, {bad_term, Bad}}); json_encode(Bad, State=#encoder{handler=Handler}) -> json_encode(Handler(Bad), State). json_encode_array([], _State) -> "[]"; json_encode_array(L, State) -> F = fun (O, Acc) -> [$,, json_encode(O, State) | Acc] end, [$, | Acc1] = lists:foldl(F, "[", L), lists:reverse([$\] | Acc1]). json_encode_proplist([], _State) -> "{}"; json_encode_proplist(Props, State) -> F = fun ({K, V}, Acc) -> KS = case K of K when is_atom(K) -> json_encode_string_utf8(atom_to_list(K)); K when is_integer(K) -> json_encode_string(integer_to_list(K), State); K when is_list(K); is_binary(K) -> json_encode_string(K, State) end, VS = json_encode(V, State), [$,, VS, $:, KS | Acc] end, [$, | Acc1] = lists:foldl(F, "{", Props), lists:reverse([$\} | Acc1]). json_encode_string(A, _State) when is_atom(A) -> json_encode_string_unicode(xmerl_ucs:from_utf8(atom_to_list(A))); json_encode_string(B, _State) when is_binary(B) -> json_encode_string_unicode(xmerl_ucs:from_utf8(B)); json_encode_string(S, #encoder{input_encoding=utf8}) -> json_encode_string_utf8(S); json_encode_string(S, #encoder{input_encoding=unicode}) -> json_encode_string_unicode(S). json_encode_string_utf8(S) -> [?Q | json_encode_string_utf8_1(S)]. json_encode_string_utf8_1([C | Cs]) when C >= 0, C =< 16#7f -> NewC = case C of $\\ -> "\\\\"; ?Q -> "\\\""; _ when C >= $\s, C < 16#7f -> C; $\t -> "\\t"; $\n -> "\\n"; $\r -> "\\r"; $\f -> "\\f"; $\b -> "\\b"; _ when C >= 0, C =< 16#7f -> unihex(C); _ -> exit({json_encode, {bad_char, C}}) end, [NewC | json_encode_string_utf8_1(Cs)]; json_encode_string_utf8_1(All=[C | _]) when C >= 16#80, C =< 16#10FFFF -> [?Q | Rest] = json_encode_string_unicode(xmerl_ucs:from_utf8(All)), Rest; json_encode_string_utf8_1([]) -> "\"". json_encode_string_unicode(S) -> [?Q | json_encode_string_unicode_1(S)]. json_encode_string_unicode_1([C | Cs]) -> NewC = case C of $\\ -> "\\\\"; ?Q -> "\\\""; _ when C >= $\s, C < 16#7f -> C; $\t -> "\\t"; $\n -> "\\n"; $\r -> "\\r"; $\f -> "\\f"; $\b -> "\\b"; _ when C >= 0, C =< 16#10FFFF -> unihex(C); _ -> exit({json_encode, {bad_char, C}}) end, [NewC | json_encode_string_unicode_1(Cs)]; json_encode_string_unicode_1([]) -> "\"". dehex(C) when C >= $0, C =< $9 -> C - $0; dehex(C) when C >= $a, C =< $f -> C - $a + 10; dehex(C) when C >= $A, C =< $F -> C - $A + 10. hexdigit(C) when C >= 0, C =< 9 -> C + $0; hexdigit(C) when C =< 15 -> C + $a - 10. unihex(C) when C < 16#10000 -> <> = <>, Digits = [hexdigit(D) || D <- [D3, D2, D1, D0]], [$\\, $u | Digits]; unihex(C) when C =< 16#10FFFF -> N = C - 16#10000, S1 = 16#d800 bor ((N bsr 10) band 16#3ff), S2 = 16#dc00 bor (N band 16#3ff), [unihex(S1), unihex(S2)]. json_decode(B, S) when is_binary(B) -> json_decode(binary_to_list(B), S); json_decode(L, S) -> {Res, L1, S1} = decode1(L, S), {eof, [], _} = tokenize(L1, S1#decoder{state=trim}), Res. decode1(L, S=#decoder{state=null}) -> case tokenize(L, S#decoder{state=any}) of {{const, C}, L1, S1} -> {C, L1, S1}; {start_array, L1, S1} -> decode_array(L1, S1#decoder{state=any}, []); {start_object, L1, S1} -> decode_object(L1, S1#decoder{state=key}, []) end. make_object(V, #decoder{object_hook=null}) -> V; make_object(V, #decoder{object_hook=Hook}) -> Hook(V). decode_object(L, S=#decoder{state=key}, Acc) -> case tokenize(L, S) of {end_object, Rest, S1} -> V = make_object({struct, lists:reverse(Acc)}, S1), {V, Rest, S1#decoder{state=null}}; {{const, K}, Rest, S1} when is_list(K) -> {colon, L2, S2} = tokenize(Rest, S1), {V, L3, S3} = decode1(L2, S2#decoder{state=null}), decode_object(L3, S3#decoder{state=comma}, [{K, V} | Acc]) end; decode_object(L, S=#decoder{state=comma}, Acc) -> case tokenize(L, S) of {end_object, Rest, S1} -> V = make_object({struct, lists:reverse(Acc)}, S1), {V, Rest, S1#decoder{state=null}}; {comma, Rest, S1} -> decode_object(Rest, S1#decoder{state=key}, Acc) end. decode_array(L, S=#decoder{state=any}, Acc) -> case tokenize(L, S) of {end_array, Rest, S1} -> {{array, lists:reverse(Acc)}, Rest, S1#decoder{state=null}}; {start_array, Rest, S1} -> {Array, Rest1, S2} = decode_array(Rest, S1#decoder{state=any}, []), decode_array(Rest1, S2#decoder{state=comma}, [Array | Acc]); {start_object, Rest, S1} -> {Array, Rest1, S2} = decode_object(Rest, S1#decoder{state=key}, []), decode_array(Rest1, S2#decoder{state=comma}, [Array | Acc]); {{const, Const}, Rest, S1} -> decode_array(Rest, S1#decoder{state=comma}, [Const | Acc]) end; decode_array(L, S=#decoder{state=comma}, Acc) -> case tokenize(L, S) of {end_array, Rest, S1} -> {{array, lists:reverse(Acc)}, Rest, S1#decoder{state=null}}; {comma, Rest, S1} -> decode_array(Rest, S1#decoder{state=any}, Acc) end. tokenize_string(IoList=[C | _], S=#decoder{input_encoding=utf8}, Acc) when is_list(C); is_binary(C); C >= 16#7f -> List = xmerl_ucs:from_utf8(iolist_to_binary(IoList)), tokenize_string(List, S#decoder{input_encoding=unicode}, Acc); tokenize_string("\"" ++ Rest, S, Acc) -> {lists:reverse(Acc), Rest, ?INC_COL(S)}; tokenize_string("\\\"" ++ Rest, S, Acc) -> tokenize_string(Rest, ?ADV_COL(S, 2), [$\" | Acc]); tokenize_string("\\\\" ++ Rest, S, Acc) -> tokenize_string(Rest, ?ADV_COL(S, 2), [$\\ | Acc]); tokenize_string("\\/" ++ Rest, S, Acc) -> tokenize_string(Rest, ?ADV_COL(S, 2), [$/ | Acc]); tokenize_string("\\b" ++ Rest, S, Acc) -> tokenize_string(Rest, ?ADV_COL(S, 2), [$\b | Acc]); tokenize_string("\\f" ++ Rest, S, Acc) -> tokenize_string(Rest, ?ADV_COL(S, 2), [$\f | Acc]); tokenize_string("\\n" ++ Rest, S, Acc) -> tokenize_string(Rest, ?ADV_COL(S, 2), [$\n | Acc]); tokenize_string("\\r" ++ Rest, S, Acc) -> tokenize_string(Rest, ?ADV_COL(S, 2), [$\r | Acc]); tokenize_string("\\t" ++ Rest, S, Acc) -> tokenize_string(Rest, ?ADV_COL(S, 2), [$\t | Acc]); tokenize_string([$\\, $u, C3, C2, C1, C0 | Rest], S, Acc) -> % coalesce UTF-16 surrogate pair? C = dehex(C0) bor (dehex(C1) bsl 4) bor (dehex(C2) bsl 8) bor (dehex(C3) bsl 12), tokenize_string(Rest, ?ADV_COL(S, 6), [C | Acc]); tokenize_string([C | Rest], S, Acc) when C >= $\s; C < 16#10FFFF -> tokenize_string(Rest, ?ADV_COL(S, 1), [C | Acc]). tokenize_number(IoList=[C | _], Mode, S=#decoder{input_encoding=utf8}, Acc) when is_list(C); is_binary(C); C >= 16#7f -> List = xmerl_ucs:from_utf8(iolist_to_binary(IoList)), tokenize_number(List, Mode, S#decoder{input_encoding=unicode}, Acc); tokenize_number([$- | Rest], sign, S, []) -> tokenize_number(Rest, int, ?INC_COL(S), [$-]); tokenize_number(Rest, sign, S, []) -> tokenize_number(Rest, int, S, []); tokenize_number([$0 | Rest], int, S, Acc) -> tokenize_number(Rest, frac, ?INC_COL(S), [$0 | Acc]); tokenize_number([C | Rest], int, S, Acc) when C >= $1, C =< $9 -> tokenize_number(Rest, int1, ?INC_COL(S), [C | Acc]); tokenize_number([C | Rest], int1, S, Acc) when C >= $0, C =< $9 -> tokenize_number(Rest, int1, ?INC_COL(S), [C | Acc]); tokenize_number(Rest, int1, S, Acc) -> tokenize_number(Rest, frac, S, Acc); tokenize_number([$., C | Rest], frac, S, Acc) when C >= $0, C =< $9 -> tokenize_number(Rest, frac1, ?ADV_COL(S, 2), [C, $. | Acc]); tokenize_number([E | Rest], frac, S, Acc) when E == $e; E == $E -> tokenize_number(Rest, esign, ?INC_COL(S), [$e, $0, $. | Acc]); tokenize_number(Rest, frac, S, Acc) -> {{int, lists:reverse(Acc)}, Rest, S}; tokenize_number([C | Rest], frac1, S, Acc) when C >= $0, C =< $9 -> tokenize_number(Rest, frac1, ?INC_COL(S), [C | Acc]); tokenize_number([E | Rest], frac1, S, Acc) when E == $e; E == $E -> tokenize_number(Rest, esign, ?INC_COL(S), [$e | Acc]); tokenize_number(Rest, frac1, S, Acc) -> {{float, lists:reverse(Acc)}, Rest, S}; tokenize_number([C | Rest], esign, S, Acc) when C == $-; C == $+ -> tokenize_number(Rest, eint, ?INC_COL(S), [C | Acc]); tokenize_number(Rest, esign, S, Acc) -> tokenize_number(Rest, eint, S, Acc); tokenize_number([C | Rest], eint, S, Acc) when C >= $0, C =< $9 -> tokenize_number(Rest, eint1, ?INC_COL(S), [C | Acc]); tokenize_number([C | Rest], eint1, S, Acc) when C >= $0, C =< $9 -> tokenize_number(Rest, eint1, ?INC_COL(S), [C | Acc]); tokenize_number(Rest, eint1, S, Acc) -> {{float, lists:reverse(Acc)}, Rest, S}. tokenize([], S=#decoder{state=trim}) -> {eof, [], S}; tokenize([L | Rest], S) when is_list(L) -> tokenize(L ++ Rest, S); tokenize([B | Rest], S) when is_binary(B) -> tokenize(xmerl_ucs:from_utf8(B) ++ Rest, S); tokenize("\r\n" ++ Rest, S) -> tokenize(Rest, ?INC_LINE(S)); tokenize("\n" ++ Rest, S) -> tokenize(Rest, ?INC_LINE(S)); tokenize([C | Rest], S) when C == $\s; C == $\t -> tokenize(Rest, ?INC_COL(S)); tokenize("{" ++ Rest, S) -> {start_object, Rest, ?INC_COL(S)}; tokenize("}" ++ Rest, S) -> {end_object, Rest, ?INC_COL(S)}; tokenize("[" ++ Rest, S) -> {start_array, Rest, ?INC_COL(S)}; tokenize("]" ++ Rest, S) -> {end_array, Rest, ?INC_COL(S)}; tokenize("," ++ Rest, S) -> {comma, Rest, ?INC_COL(S)}; tokenize(":" ++ Rest, S) -> {colon, Rest, ?INC_COL(S)}; tokenize("null" ++ Rest, S) -> {{const, null}, Rest, ?ADV_COL(S, 4)}; tokenize("true" ++ Rest, S) -> {{const, true}, Rest, ?ADV_COL(S, 4)}; tokenize("false" ++ Rest, S) -> {{const, false}, Rest, ?ADV_COL(S, 5)}; tokenize("\"" ++ Rest, S) -> {String, Rest1, S1} = tokenize_string(Rest, ?INC_COL(S), []), {{const, String}, Rest1, S1}; tokenize(L=[C | _], S) when C >= $0, C =< $9; C == $- -> case tokenize_number(L, sign, S, []) of {{int, Int}, Rest, S1} -> {{const, list_to_integer(Int)}, Rest, S1}; {{float, Float}, Rest, S1} -> {{const, list_to_float(Float)}, Rest, S1} end. %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). %% testing constructs borrowed from the Yaws JSON implementation. %% Create an object from a list of Key/Value pairs. obj_new() -> {struct, []}. is_obj({struct, Props}) -> F = fun ({K, _}) when is_list(K) -> true; (_) -> false end, lists:all(F, Props). obj_from_list(Props) -> Obj = {struct, Props}, case is_obj(Obj) of true -> Obj; false -> exit(json_bad_object) end. %% Test for equivalence of Erlang terms. %% Due to arbitrary order of construction, equivalent objects might %% compare unequal as erlang terms, so we need to carefully recurse %% through aggregates (tuples and objects). equiv({struct, Props1}, {struct, Props2}) -> equiv_object(Props1, Props2); equiv({array, L1}, {array, L2}) -> equiv_list(L1, L2); equiv(N1, N2) when is_number(N1), is_number(N2) -> N1 == N2; equiv(S1, S2) when is_list(S1), is_list(S2) -> S1 == S2; equiv(true, true) -> true; equiv(false, false) -> true; equiv(null, null) -> true. %% Object representation and traversal order is unknown. %% Use the sledgehammer and sort property lists. equiv_object(Props1, Props2) -> L1 = lists:keysort(1, Props1), L2 = lists:keysort(1, Props2), Pairs = lists:zip(L1, L2), true = lists:all(fun({{K1, V1}, {K2, V2}}) -> equiv(K1, K2) and equiv(V1, V2) end, Pairs). %% Recursively compare tuple elements for equivalence. equiv_list([], []) -> true; equiv_list([V1 | L1], [V2 | L2]) -> equiv(V1, V2) andalso equiv_list(L1, L2). e2j_vec_test() -> test_one(e2j_test_vec(utf8), 1). issue33_test() -> %% http://code.google.com/p/mochiweb/issues/detail?id=33 Js = {struct, [{"key", [194, 163]}]}, Encoder = encoder([{input_encoding, utf8}]), "{\"key\":\"\\u00a3\"}" = lists:flatten(Encoder(Js)). test_one([], _N) -> %% io:format("~p tests passed~n", [N-1]), ok; test_one([{E, J} | Rest], N) -> %% io:format("[~p] ~p ~p~n", [N, E, J]), true = equiv(E, decode(J)), true = equiv(E, decode(encode(E))), test_one(Rest, 1+N). e2j_test_vec(utf8) -> [ {1, "1"}, {3.1416, "3.14160"}, % text representation may truncate, trail zeroes {-1, "-1"}, {-3.1416, "-3.14160"}, {12.0e10, "1.20000e+11"}, {1.234E+10, "1.23400e+10"}, {-1.234E-10, "-1.23400e-10"}, {10.0, "1.0e+01"}, {123.456, "1.23456E+2"}, {10.0, "1e1"}, {"foo", "\"foo\""}, {"foo" ++ [5] ++ "bar", "\"foo\\u0005bar\""}, {"", "\"\""}, {"\"", "\"\\\"\""}, {"\n\n\n", "\"\\n\\n\\n\""}, {"\\", "\"\\\\\""}, {"\" \b\f\r\n\t\"", "\"\\\" \\b\\f\\r\\n\\t\\\"\""}, {obj_new(), "{}"}, {obj_from_list([{"foo", "bar"}]), "{\"foo\":\"bar\"}"}, {obj_from_list([{"foo", "bar"}, {"baz", 123}]), "{\"foo\":\"bar\",\"baz\":123}"}, {{array, []}, "[]"}, {{array, [{array, []}]}, "[[]]"}, {{array, [1, "foo"]}, "[1,\"foo\"]"}, % json array in a json object {obj_from_list([{"foo", {array, [123]}}]), "{\"foo\":[123]}"}, % json object in a json object {obj_from_list([{"foo", obj_from_list([{"bar", true}])}]), "{\"foo\":{\"bar\":true}}"}, % fold evaluation order {obj_from_list([{"foo", {array, []}}, {"bar", obj_from_list([{"baz", true}])}, {"alice", "bob"}]), "{\"foo\":[],\"bar\":{\"baz\":true},\"alice\":\"bob\"}"}, % json object in a json array {{array, [-123, "foo", obj_from_list([{"bar", {array, []}}]), null]}, "[-123,\"foo\",{\"bar\":[]},null]"} ]. -endif. mochiweb-3.2.1/src/mochijson2.erl000066400000000000000000001034731450333227400166730ustar00rootroot00000000000000%% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @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): %%
    %%
  • {"key": "value"} -> %% {struct, [{<<"key">>, <<"value">>}]}
  • %%
  • ["array", 123, 12.34, true, false, null] -> %% [<<"array">>, 123, 12.34, true, false, null] %%
  • %%
%%
    %%
  • Strings in JSON decode to UTF-8 binaries in Erlang
  • %%
  • Objects decode to {struct, PropList}
  • %%
  • Numbers decode to integer or float
  • %%
  • true, false, null decode to their respective terms.
  • %%
%% The encoder will accept the same format that the decoder will produce, %% but will also allow additional cases for leniency: %%
    %%
  • atoms other than true, false, null will be considered UTF-8 %% strings (even as a proplist key) %%
  • %%
  • {json, IoList} will insert IoList directly into the output %% with no validation %%
  • %%
  • {array, Array} will be encoded as Array %% (legacy mochijson style) %%
  • %%
  • 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 %%
  • %%
-module(mochijson2). -author('bob@mochimedia.com'). -export([encoder/1, encode/1]). -export([decoder/1, decode/1, decode/2]). %% 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}). -define(INC_COL(S), S#decoder{offset=1+S#decoder.offset, column=1+S#decoder.column}). -define(INC_LINE(S), S#decoder{offset=1+S#decoder.offset, column=1, line=1+S#decoder.line}). -define(INC_CHAR(S, C), case C of $\n -> S#decoder{column=1, line=1+S#decoder.line, offset=1+S#decoder.offset}; _ -> S#decoder{column=1+S#decoder.column, offset=1+S#decoder.offset} end). -define(IS_WHITESPACE(C), (C =:= $\s orelse C =:= $\t orelse C =:= $\r orelse C =:= $\n)). %% @type json_string() = atom | binary() %% @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_eep18_object() | json_iolist() -record(encoder, {handler=null, utf8=false}). -record(decoder, {object_hook=null, offset=0, line=1, column=1, state=null}). %% @spec encoder([encoder_option()]) -> function() %% @doc Create an encoder/1 with the given options. %% @type encoder_option() = handler_option() | utf8_option() %% @type utf8_option() = boolean(). Emit unicode as utf8 (default - false) encoder(Options) -> State = parse_encoder_options(Options, #encoder{}), fun (O) -> json_encode(O, State) end. %% @spec encode(json_term()) -> iolist() %% @doc Encode the given as JSON to an iolist. encode(Any) -> json_encode(Any, #encoder{}). %% @spec decoder([decoder_option()]) -> function() %% @doc Create a decoder/1 with the given options. decoder(Options) -> State = parse_decoder_options(Options, #decoder{}), fun (O) -> json_decode(O, State) end. %% @spec decode(iolist(), [{format, proplist | eep18 | struct | map}]) -> 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()]}, %% map 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) -> json_decode(S, #decoder{}). %% Internal API parse_encoder_options([], State) -> State; parse_encoder_options([{handler, Handler} | Rest], State) -> parse_encoder_options(Rest, State#encoder{handler=Handler}); parse_encoder_options([{utf8, Switch} | Rest], State) -> parse_encoder_options(Rest, State#encoder{utf8=Switch}). 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([{format, map} | Rest], State) -> Hook = make_object_hook_for_map(), 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}). make_object_hook_for_map() -> fun ({struct, P}) -> maps:from_list(P) end. json_encode(true, _State) -> <<"true">>; json_encode(false, _State) -> <<"false">>; json_encode(null, _State) -> <<"null">>; json_encode(I, _State) when is_integer(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([{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(M, State) when is_map(M) -> json_encode_map(M, State); json_encode({json, IoList}, _State) -> IoList; json_encode(Bad, #encoder{handler=null}) -> exit({json_encode, {bad_term, Bad}}); json_encode(Bad, State=#encoder{handler=Handler}) -> json_encode(Handler(Bad), State). json_encode_array([], _State) -> <<"[]">>; json_encode_array(L, State) -> F = fun (O, Acc) -> [$,, json_encode(O, State) | Acc] end, [$, | Acc1] = lists:foldl(F, "[", L), lists:reverse([$\] | Acc1]). json_encode_proplist([], _State) -> <<"{}">>; json_encode_proplist(Props, State) -> F = fun ({K, V}, Acc) -> KS = json_encode_string(K, State), VS = json_encode(V, State), [$,, VS, $:, KS | Acc] end, [$, | Acc1] = lists:foldl(F, "{", Props), lists:reverse([$\} | Acc1]). json_encode_map(Map, _State) when map_size(Map) =:= 0 -> <<"{}">>; json_encode_map(Map, State) -> F = fun(K, V, Acc) -> KS = json_encode_string(K, State), VS = json_encode(V, State), [$,, VS, $:, KS | Acc] end, [$, | Acc1] = maps:fold(F, "{", Map), lists:reverse([$\} | Acc1]). json_encode_string(A, State) when is_atom(A) -> json_encode_string(atom_to_binary(A, latin1), State); json_encode_string(B, State) when is_binary(B) -> case json_bin_is_safe(B) of true -> [?Q, B, ?Q]; false -> json_encode_string_unicode(unicode:characters_to_list(B), State, [?Q]) end; json_encode_string(I, _State) when is_integer(I) -> [?Q, integer_to_list(I), ?Q]; json_encode_string(L, State) when is_list(L) -> case json_string_is_safe(L) of true -> [?Q, L, ?Q]; false -> json_encode_string_unicode(L, State, [?Q]) end. json_string_is_safe([]) -> true; json_string_is_safe([C | Rest]) -> case C of ?Q -> false; $\\ -> false; $\b -> false; $\f -> false; $\n -> false; $\r -> false; $\t -> false; C when C >= 0, C < $\s; C >= 16#7f, C =< 16#10FFFF -> false; C when C < 16#7f -> json_string_is_safe(Rest); _ -> exit({json_encode, {bad_char, C}}) end. json_bin_is_safe(<<>>) -> true; json_bin_is_safe(<>) -> case C of ?Q -> false; $\\ -> false; $\b -> false; $\f -> false; $\n -> false; $\r -> false; $\t -> false; C when C >= 0, C < $\s; C >= 16#7f -> false; C when C < 16#7f -> json_bin_is_safe(Rest) end. json_encode_string_unicode([], _State, Acc) -> lists:reverse([$\" | Acc]); json_encode_string_unicode([C | Cs], State, Acc) -> Acc1 = case C of ?Q -> [?Q, $\\ | Acc]; %% Escaping solidus is only useful when trying to protect %% against "" injection attacks which are only %% possible when JSON is inserted into a HTML document %% in-line. mochijson2 does not protect you from this, so %% if you do insert directly into HTML then you need to %% uncomment the following case or escape the output of encode. %% %% $/ -> %% [$/, $\\ | Acc]; %% $\\ -> [$\\, $\\ | Acc]; $\b -> [$b, $\\ | Acc]; $\f -> [$f, $\\ | Acc]; $\n -> [$n, $\\ | Acc]; $\r -> [$r, $\\ | Acc]; $\t -> [$t, $\\ | Acc]; C when C >= 0, C < $\s -> [unihex(C) | Acc]; C when C >= 16#7f, C =< 16#10FFFF, State#encoder.utf8 -> [unicode:characters_to_binary([C]) | Acc]; C when C >= 16#7f, C =< 16#10FFFF, not State#encoder.utf8 -> [unihex(C) | Acc]; C when C < 16#7f -> [C | Acc]; _ -> %% json_string_is_safe guarantees that this branch is dead exit({json_encode, {bad_char, C}}) end, json_encode_string_unicode(Cs, State, Acc1). hexdigit(C) when C >= 0, C =< 9 -> C + $0; hexdigit(C) when C =< 15 -> C + $a - 10. unihex(C) when C < 16#10000 -> <> = <>, Digits = [hexdigit(D) || D <- [D3, D2, D1, D0]], [$\\, $u | Digits]; unihex(C) when C =< 16#10FFFF -> N = C - 16#10000, S1 = 16#d800 bor ((N bsr 10) band 16#3ff), S2 = 16#dc00 bor (N band 16#3ff), [unihex(S1), unihex(S2)]. json_decode(L, S) when is_list(L) -> json_decode(iolist_to_binary(L), S); json_decode(B, S) -> {Res, S1} = decode1(B, S), {eof, _} = tokenize(B, S1#decoder{state=trim}), Res. decode1(B, S=#decoder{state=null}) -> case tokenize(B, S#decoder{state=any}) of {{const, C}, S1} -> {C, S1}; {start_array, S1} -> decode_array(B, S1); {start_object, S1} -> decode_object(B, S1) end. 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). decode_object(B, S) -> decode_object(B, S#decoder{state=key}, []). decode_object(B, S=#decoder{state=key}, Acc) -> case tokenize(B, S) of {end_object, S1} -> V = make_object({struct, lists:reverse(Acc)}, S1), {V, S1#decoder{state=null}}; {{const, K}, S1} -> {colon, S2} = tokenize(B, S1), {V, S3} = decode1(B, S2#decoder{state=null}), decode_object(B, S3#decoder{state=comma}, [{K, V} | Acc]) end; decode_object(B, S=#decoder{state=comma}, Acc) -> case tokenize(B, S) of {end_object, S1} -> V = make_object({struct, lists:reverse(Acc)}, S1), {V, S1#decoder{state=null}}; {comma, S1} -> decode_object(B, S1#decoder{state=key}, Acc) end. decode_array(B, S) -> decode_array(B, S#decoder{state=any}, []). decode_array(B, S=#decoder{state=any}, Acc) -> case tokenize(B, S) of {end_array, S1} -> {lists:reverse(Acc), S1#decoder{state=null}}; {start_array, S1} -> {Array, S2} = decode_array(B, S1), decode_array(B, S2#decoder{state=comma}, [Array | Acc]); {start_object, S1} -> {Array, S2} = decode_object(B, S1), decode_array(B, S2#decoder{state=comma}, [Array | Acc]); {{const, Const}, S1} -> decode_array(B, S1#decoder{state=comma}, [Const | Acc]) end; decode_array(B, S=#decoder{state=comma}, Acc) -> case tokenize(B, S) of {end_array, S1} -> {lists:reverse(Acc), S1#decoder{state=null}}; {comma, S1} -> decode_array(B, S1#decoder{state=any}, Acc) end. tokenize_string(B, S=#decoder{offset=O}) -> case tokenize_string_fast(B, O) of {escape, O1} -> Length = O1 - O, S1 = ?ADV_COL(S, Length), <<_:O/binary, Head:Length/binary, _/binary>> = B, tokenize_string(B, S1, lists:reverse(binary_to_list(Head))); O1 -> Length = O1 - O, <<_:O/binary, String:Length/binary, ?Q, _/binary>> = B, {{const, String}, ?ADV_COL(S, Length + 1)} end. tokenize_string_fast(B, O) -> case B of <<_:O/binary, ?Q, _/binary>> -> O; <<_:O/binary, $\\, _/binary>> -> {escape, O}; <<_:O/binary, C1, _/binary>> when C1 < 128 -> tokenize_string_fast(B, 1 + O); <<_:O/binary, C1, C2, _/binary>> when C1 >= 194, C1 =< 223, C2 >= 128, C2 =< 191 -> tokenize_string_fast(B, 2 + O); <<_:O/binary, C1, C2, C3, _/binary>> when C1 >= 224, C1 =< 239, C2 >= 128, C2 =< 191, C3 >= 128, C3 =< 191 -> tokenize_string_fast(B, 3 + O); <<_: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_fast(B, 4 + O); _ -> throw(invalid_utf8) end. tokenize_string(B, S=#decoder{offset=O}, Acc) -> case B of <<_:O/binary, ?Q, _/binary>> -> {{const, iolist_to_binary(lists:reverse(Acc))}, ?INC_COL(S)}; <<_:O/binary, "\\\"", _/binary>> -> tokenize_string(B, ?ADV_COL(S, 2), [$\" | Acc]); <<_:O/binary, "\\\\", _/binary>> -> tokenize_string(B, ?ADV_COL(S, 2), [$\\ | Acc]); <<_:O/binary, "\\/", _/binary>> -> tokenize_string(B, ?ADV_COL(S, 2), [$/ | Acc]); <<_:O/binary, "\\b", _/binary>> -> tokenize_string(B, ?ADV_COL(S, 2), [$\b | Acc]); <<_:O/binary, "\\f", _/binary>> -> tokenize_string(B, ?ADV_COL(S, 2), [$\f | Acc]); <<_:O/binary, "\\n", _/binary>> -> tokenize_string(B, ?ADV_COL(S, 2), [$\n | Acc]); <<_:O/binary, "\\r", _/binary>> -> tokenize_string(B, ?ADV_COL(S, 2), [$\r | Acc]); <<_:O/binary, "\\t", _/binary>> -> tokenize_string(B, ?ADV_COL(S, 2), [$\t | Acc]); <<_:O/binary, "\\u", C3, C2, C1, C0, Rest/binary>> -> C = erlang:list_to_integer([C3, C2, C1, C0], 16), if C > 16#D7FF, C < 16#DC00 -> %% coalesce UTF-16 surrogate pair <<"\\u", D3, D2, D1, D0, _/binary>> = Rest, D = erlang:list_to_integer([D3,D2,D1,D0], 16), Acc1 = [unicode:characters_to_binary( <>, utf16) | Acc], tokenize_string(B, ?ADV_COL(S, 12), Acc1); true -> Acc1 = [unicode:characters_to_binary([C]) | Acc], tokenize_string(B, ?ADV_COL(S, 6), Acc1) end; <<_: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) -> case tokenize_number(B, sign, S, []) of {{int, Int}, S1} -> {{const, list_to_integer(Int)}, S1}; {{float, Float}, S1} -> {{const, list_to_float(Float)}, S1} end. tokenize_number(B, sign, S=#decoder{offset=O}, []) -> case B of <<_:O/binary, $-, _/binary>> -> tokenize_number(B, int, ?INC_COL(S), [$-]); _ -> tokenize_number(B, int, S, []) end; tokenize_number(B, int, S=#decoder{offset=O}, Acc) -> case B of <<_:O/binary, $0, _/binary>> -> tokenize_number(B, frac, ?INC_COL(S), [$0 | Acc]); <<_:O/binary, C, _/binary>> when C >= $1 andalso C =< $9 -> tokenize_number(B, int1, ?INC_COL(S), [C | Acc]) end; tokenize_number(B, int1, S=#decoder{offset=O}, Acc) -> case B of <<_:O/binary, C, _/binary>> when C >= $0 andalso C =< $9 -> tokenize_number(B, int1, ?INC_COL(S), [C | Acc]); _ -> tokenize_number(B, frac, S, Acc) end; tokenize_number(B, frac, S=#decoder{offset=O}, Acc) -> case B of <<_:O/binary, $., C, _/binary>> when C >= $0, C =< $9 -> tokenize_number(B, frac1, ?ADV_COL(S, 2), [C, $. | Acc]); <<_:O/binary, E, _/binary>> when E =:= $e orelse E =:= $E -> tokenize_number(B, esign, ?INC_COL(S), [$e, $0, $. | Acc]); _ -> {{int, lists:reverse(Acc)}, S} end; tokenize_number(B, frac1, S=#decoder{offset=O}, Acc) -> case B of <<_:O/binary, C, _/binary>> when C >= $0 andalso C =< $9 -> tokenize_number(B, frac1, ?INC_COL(S), [C | Acc]); <<_:O/binary, E, _/binary>> when E =:= $e orelse E =:= $E -> tokenize_number(B, esign, ?INC_COL(S), [$e | Acc]); _ -> {{float, lists:reverse(Acc)}, S} end; tokenize_number(B, esign, S=#decoder{offset=O}, Acc) -> case B of <<_:O/binary, C, _/binary>> when C =:= $- orelse C=:= $+ -> tokenize_number(B, eint, ?INC_COL(S), [C | Acc]); _ -> tokenize_number(B, eint, S, Acc) end; tokenize_number(B, eint, S=#decoder{offset=O}, Acc) -> case B of <<_:O/binary, C, _/binary>> when C >= $0 andalso C =< $9 -> tokenize_number(B, eint1, ?INC_COL(S), [C | Acc]) end; tokenize_number(B, eint1, S=#decoder{offset=O}, Acc) -> case B of <<_:O/binary, C, _/binary>> when C >= $0 andalso C =< $9 -> tokenize_number(B, eint1, ?INC_COL(S), [C | Acc]); _ -> {{float, lists:reverse(Acc)}, S} end. tokenize(B, S=#decoder{offset=O}) -> case B of <<_:O/binary, C, _/binary>> when ?IS_WHITESPACE(C) -> tokenize(B, ?INC_CHAR(S, C)); <<_:O/binary, "{", _/binary>> -> {start_object, ?INC_COL(S)}; <<_:O/binary, "}", _/binary>> -> {end_object, ?INC_COL(S)}; <<_:O/binary, "[", _/binary>> -> {start_array, ?INC_COL(S)}; <<_:O/binary, "]", _/binary>> -> {end_array, ?INC_COL(S)}; <<_:O/binary, ",", _/binary>> -> {comma, ?INC_COL(S)}; <<_:O/binary, ":", _/binary>> -> {colon, ?INC_COL(S)}; <<_:O/binary, "null", _/binary>> -> {{const, null}, ?ADV_COL(S, 4)}; <<_:O/binary, "true", _/binary>> -> {{const, true}, ?ADV_COL(S, 4)}; <<_:O/binary, "false", _/binary>> -> {{const, false}, ?ADV_COL(S, 5)}; <<_:O/binary, "\"", _/binary>> -> tokenize_string(B, ?INC_COL(S)); <<_:O/binary, C, _/binary>> when (C >= $0 andalso C =< $9) orelse C =:= $- -> tokenize_number(B, S); <<_:O/binary>> -> trim = S#decoder.state, {eof, S} end. %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). %% testing constructs borrowed from the Yaws JSON implementation. %% Create an object from a list of Key/Value pairs. obj_new() -> {struct, []}. is_obj({struct, Props}) -> F = fun ({K, _}) when is_binary(K) -> true end, lists:all(F, Props). obj_from_list(Props) -> Obj = {struct, Props}, ?assert(is_obj(Obj)), Obj. %% Test for equivalence of Erlang terms. %% Due to arbitrary order of construction, equivalent objects might %% compare unequal as erlang terms, so we need to carefully recurse %% through aggregates (tuples and objects). equiv({struct, Props1}, {struct, Props2}) -> equiv_object(Props1, Props2); equiv(L1, L2) when is_list(L1), is_list(L2) -> equiv_list(L1, L2); equiv(N1, N2) when is_number(N1), is_number(N2) -> N1 == N2; equiv(B1, B2) when is_binary(B1), is_binary(B2) -> B1 == B2; equiv(A, A) when A =:= true orelse A =:= false orelse A =:= null -> true. %% Object representation and traversal order is unknown. %% Use the sledgehammer and sort property lists. equiv_object(Props1, Props2) -> L1 = lists:keysort(1, Props1), L2 = lists:keysort(1, Props2), Pairs = lists:zip(L1, L2), true = lists:all(fun({{K1, V1}, {K2, V2}}) -> equiv(K1, K2) and equiv(V1, V2) end, Pairs). %% Recursively compare tuple elements for equivalence. equiv_list([], []) -> true; equiv_list([V1 | L1], [V2 | L2]) -> equiv(V1, V2) andalso equiv_list(L1, L2). decode_test() -> [1199344435545.0, 1] = decode(<<"[1199344435545.0,1]">>), <<16#F0,16#9D,16#9C,16#95>> = decode([34,"\\ud835","\\udf15",34]). e2j_vec_test() -> test_one(e2j_test_vec(utf8), 1). test_one([], _N) -> %% io:format("~p tests passed~n", [N-1]), ok; test_one([{E, J} | Rest], N) -> %% io:format("[~p] ~p ~p~n", [N, E, J]), true = equiv(E, decode(J)), true = equiv(E, decode(encode(E))), test_one(Rest, 1+N). e2j_test_vec(utf8) -> [ {1, "1"}, {3.1416, "3.14160"}, %% text representation may truncate, trail zeroes {-1, "-1"}, {-3.1416, "-3.14160"}, {12.0e10, "1.20000e+11"}, {1.234E+10, "1.23400e+10"}, {-1.234E-10, "-1.23400e-10"}, {10.0, "1.0e+01"}, {123.456, "1.23456E+2"}, {10.0, "1e1"}, {<<"foo">>, "\"foo\""}, {<<"foo", 5, "bar">>, "\"foo\\u0005bar\""}, {<<"">>, "\"\""}, {<<"\n\n\n">>, "\"\\n\\n\\n\""}, {<<"\" \b\f\r\n\t\"">>, "\"\\\" \\b\\f\\r\\n\\t\\\"\""}, {obj_new(), "{}"}, {obj_from_list([{<<"foo">>, <<"bar">>}]), "{\"foo\":\"bar\"}"}, {obj_from_list([{<<"foo">>, <<"bar">>}, {<<"baz">>, 123}]), "{\"foo\":\"bar\",\"baz\":123}"}, {[], "[]"}, {[[]], "[[]]"}, {[1, <<"foo">>], "[1,\"foo\"]"}, %% json array in a json object {obj_from_list([{<<"foo">>, [123]}]), "{\"foo\":[123]}"}, %% json object in a json object {obj_from_list([{<<"foo">>, obj_from_list([{<<"bar">>, true}])}]), "{\"foo\":{\"bar\":true}}"}, %% fold evaluation order {obj_from_list([{<<"foo">>, []}, {<<"bar">>, obj_from_list([{<<"baz">>, true}])}, {<<"alice">>, <<"bob">>}]), "{\"foo\":[],\"bar\":{\"baz\":true},\"alice\":\"bob\"}"}, %% json object in a json array {[-123, <<"foo">>, obj_from_list([{<<"bar">>, []}]), null], "[-123,\"foo\",{\"bar\":[]},null]"} ]. %% test utf8 encoding encoder_utf8_test() -> %% safe conversion case (default) <<"\"\\u0001\\u0442\\u0435\\u0441\\u0442\"">> = iolist_to_binary(encode(<<1,"\321\202\320\265\321\201\321\202">>)), %% raw utf8 output (optional) Enc = mochijson2:encoder([{utf8, true}]), <<34,"\\u0001",209,130,208,181,209,129,209,130,34>> = iolist_to_binary(Enc(<<1,"\321\202\320\265\321\201\321\202">>)). input_validation_test() -> Good = [ {16#00A3, <>}, %% pound {16#20AC, <>}, %% euro {16#10196, <>} %% denarius ], lists:foreach(fun({CodePoint, UTF8}) -> Expect = unicode:characters_to_binary([CodePoint]), Expect = decode(UTF8) end, Good), Bad = [ %% 2nd, 3rd, or 4th byte of a multi-byte sequence w/o leading byte <>, %% missing continuations, last byte in each should be 80-BF <>, <>, <>, %% we don't support code points > 10FFFF per RFC 3629 <>, %% escape characters trigger a different code path <> ], lists:foreach( fun(X) -> ok = try decode(X) catch invalid_utf8 -> ok end, %% could be {ucs,{bad_utf8_character_code}} or %% {json_encode,{bad_char,_}} {'EXIT', _} = (catch encode(X)) end, Bad). inline_json_test() -> ?assertEqual(<<"\"iodata iodata\"">>, iolist_to_binary( encode({json, [<<"\"iodata">>, " iodata\""]}))), ?assertEqual({struct, [{<<"key">>, <<"iodata iodata">>}]}, decode( encode({struct, [{key, {json, [<<"\"iodata">>, " iodata\""]}}]}))), ok. big_unicode_test() -> UTF8Seq = unicode:characters_to_binary([16#0001d120]), ?assertEqual( <<"\"\\ud834\\udd20\"">>, iolist_to_binary(encode(UTF8Seq))), ?assertEqual( UTF8Seq, decode(iolist_to_binary(encode(UTF8Seq)))), ok. custom_decoder_test() -> ?assertEqual( {struct, [{<<"key">>, <<"value">>}]}, (decoder([]))("{\"key\": \"value\"}")), F = fun ({struct, [{<<"key">>, <<"value">>}]}) -> win end, ?assertEqual( win, (decoder([{object_hook, F}]))("{\"key\": \"value\"}")), ok. atom_test() -> %% JSON native atoms [begin ?assertEqual(A, decode(atom_to_list(A))), ?assertEqual(iolist_to_binary(atom_to_list(A)), iolist_to_binary(encode(A))) end || A <- [true, false, null]], %% Atom to string ?assertEqual( <<"\"foo\"">>, iolist_to_binary(encode(foo))), ?assertEqual( <<"\"\\ud834\\udd20\"">>, iolist_to_binary( encode( binary_to_atom( unicode:characters_to_binary([16#0001d120]), latin1)))), ok. key_encode_test() -> %% Some forms are accepted as keys that would not be strings in other %% cases ?assertEqual( <<"{\"foo\":1}">>, iolist_to_binary(encode({struct, [{foo, 1}]}))), ?assertEqual( <<"{\"foo\":1}">>, iolist_to_binary(encode({struct, [{<<"foo">>, 1}]}))), ?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( encode({struct, [{[16#0001d120], 1}]}))), ?assertEqual( <<"{\"1\":1}">>, iolist_to_binary(encode({struct, [{1, 1}]}))), ok. unsafe_chars_test() -> Chars = "\"\\\b\f\n\r\t", [begin ?assertEqual(false, json_string_is_safe([C])), ?assertEqual(false, json_bin_is_safe(<>)), ?assertEqual(<>, decode(encode(<>))) end || C <- Chars], ?assertEqual( false, json_string_is_safe([16#0001d120])), ?assertEqual( false, json_bin_is_safe(unicode:characters_to_binary([16#0001d120]))), ?assertEqual( [16#0001d120], unicode:characters_to_list( decode( encode( binary_to_atom( unicode:characters_to_binary([16#0001d120]), latin1))))), ?assertEqual( false, json_string_is_safe([16#10ffff])), ?assertEqual( false, json_bin_is_safe(unicode:characters_to_binary([16#10ffff]))), %% solidus can be escaped but isn't unsafe by default ?assertEqual( <<"/">>, decode(<<"\"\\/\"">>)), ok. int_test() -> ?assertEqual(0, decode("0")), ?assertEqual(1, decode("1")), ?assertEqual(11, decode("11")), ok. 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,{x,y}}}}, catch encode({x,y})), F = fun ({x,y}) -> [] end, ?assertEqual( <<"[]">>, 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}]]. array_test() -> A = [<<"hello">>], ?assertEqual(A, decode(encode({array, A}))). bad_char_test() -> ?assertEqual( {'EXIT', {json_encode, {bad_char, 16#110000}}}, catch json_string_is_safe([16#110000])). utf8_roundtrip_test_() -> %% These are the boundary cases for UTF8 encoding Codepoints = [%% 7 bits -> 1 byte 16#00, 16#7f, %% 11 bits -> 2 bytes 16#080, 16#07ff, %% 16 bits -> 3 bytes 16#0800, 16#ffff, 16#d7ff, 16#e000, %% 21 bits -> 4 bytes 16#010000, 16#10ffff], UTF8 = unicode:characters_to_binary(Codepoints), Encode = encoder([{utf8, true}]), [{"roundtrip escaped", ?_assertEqual(UTF8, decode(encode(UTF8)))}, {"roundtrip utf8", ?_assertEqual(UTF8, decode(Encode(UTF8)))}]. utf8_non_character_test_() -> S = unicode:characters_to_binary([16#ffff, 16#fffe]), [{"roundtrip escaped", ?_assertEqual(S, decode(encode(S)))}, {"roundtrip utf8", ?_assertEqual(S, decode((encoder([{utf8, true}]))(S)))}]. decode_map_test() -> Json = "{\"var1\": 3, \"var2\": {\"var3\": 7}}", M = #{<<"var1">> => 3,<<"var2">> => #{<<"var3">> => 7}}, ?assertEqual(M, decode(Json, [{format, map}])). encode_map_test() -> M = <<"{\"a\":1,\"b\":{\"c\":2}}">>, ?assertEqual(M, iolist_to_binary(encode(#{a => 1, b => #{ c => 2}}))). encode_empty_map_test() -> ?assertEqual(<<"{}">>, encode(#{})). -endif. mochiweb-3.2.1/src/mochilists.erl000066400000000000000000000074121450333227400167720ustar00rootroot00000000000000%% @copyright Copyright (c) 2010 Mochi Media, Inc. %% @author David Reid %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc Utility functions for dealing with proplists. -module(mochilists). -author("David Reid "). -export([get_value/2, get_value/3, is_defined/2, set_default/2, set_defaults/2]). %% @spec set_default({Key::term(), Value::term()}, Proplist::list()) -> list() %% %% @doc Return new Proplist with {Key, Value} set if not is_defined(Key, Proplist). set_default({Key, Value}, Proplist) -> case is_defined(Key, Proplist) of true -> Proplist; false -> [{Key, Value} | Proplist] end. %% @spec set_defaults([{Key::term(), Value::term()}], Proplist::list()) -> list() %% %% @doc Return new Proplist with {Key, Value} set if not is_defined(Key, Proplist). set_defaults(DefaultProps, Proplist) -> lists:foldl(fun set_default/2, Proplist, DefaultProps). %% @spec is_defined(Key::term(), Proplist::list()) -> bool() %% %% @doc Returns true if Propist contains at least one entry associated %% with Key, otherwise false is returned. is_defined(Key, Proplist) -> lists:keyfind(Key, 1, Proplist) =/= false. %% @spec get_value(Key::term(), Proplist::list()) -> term() | undefined %% %% @doc Return the value of Key or undefined get_value(Key, Proplist) -> get_value(Key, Proplist, undefined). %% @spec get_value(Key::term(), Proplist::list(), Default::term()) -> term() %% %% @doc Return the value of Key or Default get_value(_Key, [], Default) -> Default; get_value(Key, Proplist, Default) -> case lists:keyfind(Key, 1, Proplist) of false -> Default; {Key, Value} -> Value end. %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). set_defaults_test() -> ?assertEqual( [{k, v}], set_defaults([{k, v}], [])), ?assertEqual( [{k, v}], set_defaults([{k, vee}], [{k, v}])), ?assertEqual( lists:sort([{kay, vee}, {k, v}]), lists:sort(set_defaults([{k, vee}, {kay, vee}], [{k, v}]))), ok. set_default_test() -> ?assertEqual( [{k, v}], set_default({k, v}, [])), ?assertEqual( [{k, v}], set_default({k, vee}, [{k, v}])), ok. get_value_test() -> ?assertEqual( undefined, get_value(foo, [])), ?assertEqual( undefined, get_value(foo, [{bar, baz}])), ?assertEqual( bar, get_value(foo, [{foo, bar}])), ?assertEqual( default, get_value(foo, [], default)), ?assertEqual( default, get_value(foo, [{bar, baz}], default)), ?assertEqual( bar, get_value(foo, [{foo, bar}], default)), ok. -endif. mochiweb-3.2.1/src/mochilogfile2.erl000066400000000000000000000114121450333227400173320ustar00rootroot00000000000000%% @author Bob Ippolito %% @copyright 2010 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc Write newline delimited log files, ensuring that if a truncated %% entry is found on log open then it is fixed before writing. Uses %% delayed writes and raw files for performance. -module(mochilogfile2). -author('bob@mochimedia.com'). -export([open/1, write/2, close/1, name/1]). %% @spec open(Name) -> Handle %% @doc Open the log file Name, creating or appending as necessary. All data %% at the end of the file will be truncated until a newline is found, to %% ensure that all records are complete. open(Name) -> {ok, FD} = file:open(Name, [raw, read, write, delayed_write, binary]), fix_log(FD), {?MODULE, Name, FD}. %% @spec name(Handle) -> string() %% @doc Return the path of the log file. name({?MODULE, Name, _FD}) -> Name. %% @spec write(Handle, IoData) -> ok %% @doc Write IoData to the log file referenced by Handle. write({?MODULE, _Name, FD}, IoData) -> ok = file:write(FD, [IoData, $\n]), ok. %% @spec close(Handle) -> ok %% @doc Close the log file referenced by Handle. close({?MODULE, _Name, FD}) -> ok = file:sync(FD), ok = file:close(FD), ok. fix_log(FD) -> {ok, Location} = file:position(FD, eof), Seek = find_last_newline(FD, Location), {ok, Seek} = file:position(FD, Seek), ok = file:truncate(FD), ok. %% Seek backwards to the last valid log entry find_last_newline(_FD, N) when N =< 1 -> 0; find_last_newline(FD, Location) -> case file:pread(FD, Location - 1, 1) of {ok, <<$\n>>} -> Location; {ok, _} -> find_last_newline(FD, Location - 1) end. %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). name_test() -> D = mochitemp:mkdtemp(), FileName = filename:join(D, "open_close_test.log"), H = open(FileName), ?assertEqual( FileName, name(H)), close(H), file:delete(FileName), file:del_dir(D), ok. open_close_test() -> D = mochitemp:mkdtemp(), FileName = filename:join(D, "open_close_test.log"), OpenClose = fun () -> H = open(FileName), ?assertEqual( true, filelib:is_file(FileName)), ok = close(H), ?assertEqual( {ok, <<>>}, file:read_file(FileName)), ok end, OpenClose(), OpenClose(), file:delete(FileName), file:del_dir(D), ok. write_test() -> D = mochitemp:mkdtemp(), FileName = filename:join(D, "write_test.log"), F = fun () -> H = open(FileName), write(H, "test line"), close(H), ok end, F(), ?assertEqual( {ok, <<"test line\n">>}, file:read_file(FileName)), F(), ?assertEqual( {ok, <<"test line\ntest line\n">>}, file:read_file(FileName)), file:delete(FileName), file:del_dir(D), ok. fix_log_test() -> D = mochitemp:mkdtemp(), FileName = filename:join(D, "write_test.log"), file:write_file(FileName, <<"first line good\nsecond line bad">>), F = fun () -> H = open(FileName), write(H, "test line"), close(H), ok end, F(), ?assertEqual( {ok, <<"first line good\ntest line\n">>}, file:read_file(FileName)), file:write_file(FileName, <<"first line bad">>), F(), ?assertEqual( {ok, <<"test line\n">>}, file:read_file(FileName)), F(), ?assertEqual( {ok, <<"test line\ntest line\n">>}, file:read_file(FileName)), ok. -endif. mochiweb-3.2.1/src/mochinum.erl000066400000000000000000000266121450333227400164360ustar00rootroot00000000000000%% @copyright 2007 Mochi Media, Inc. %% @author Bob Ippolito %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc Useful numeric algorithms for floats that cover some deficiencies %% in the math module. More interesting is digits/1, which implements %% the algorithm from: %% http://www.cs.indiana.edu/~burger/fp/index.html %% See also "Printing Floating-Point Numbers Quickly and Accurately" %% in Proceedings of the SIGPLAN '96 Conference on Programming Language %% Design and Implementation. -module(mochinum). -author("Bob Ippolito "). -export([digits/1, frexp/1, int_pow/2, int_ceil/1]). %% IEEE 754 Float exponent bias -define(FLOAT_BIAS, 1022). -define(MIN_EXP, -1074). -define(BIG_POW, 4503599627370496). %% External API %% @spec digits(number()) -> string() %% @doc Returns a string that accurately represents the given integer or float %% using a conservative amount of digits. Great for generating %% human-readable output, or compact ASCII serializations for floats. digits(N) when is_integer(N) -> integer_to_list(N); digits(Float) when Float == 0.0 -> "0.0"; digits(Float) -> {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]; _ -> R end. %% @spec frexp(F::float()) -> {Frac::float(), Exp::float()} %% @doc Return the fractional and exponent part of an IEEE 754 double, %% equivalent to the libc function of the same name. %% F = Frac * pow(2, Exp). frexp(F) -> frexp1(unpack(F)). %% @spec int_pow(X::integer(), N::integer()) -> Y::integer() %% @doc Moderately efficient way to exponentiate integers. %% int_pow(10, 2) = 100. int_pow(_X, 0) -> 1; int_pow(X, N) when N > 0 -> int_pow(X, N, 1). %% @spec int_ceil(F::float()) -> integer() %% @doc Return the ceiling of F as an integer. The ceiling is defined as %% F when F == trunc(F); %% trunc(F) when F < 0; %% trunc(F) + 1 when F > 0. int_ceil(X) -> T = trunc(X), case (X - T) of Pos when Pos > 0 -> T + 1; _ -> T end. %% Internal API int_pow(X, N, R) when N < 2 -> R * X; int_pow(X, N, R) -> int_pow(X * X, N bsr 1, case N band 1 of 1 -> R * X; 0 -> R end). insert_decimal(0, S) -> "0." ++ S; insert_decimal(Place, S) when Place > 0 -> L = length(S), case Place - L of 0 -> S ++ ".0"; N when N < 0 -> {S0, S1} = lists:split(L + N, S), S0 ++ "." ++ S1; N when N < 6 -> %% More places than digits S ++ lists:duplicate(N, $0) ++ ".0"; _ -> insert_decimal_exp(Place, S) end; insert_decimal(Place, S) when Place > -6 -> "0." ++ lists:duplicate(abs(Place), $0) ++ S; insert_decimal(Place, S) -> insert_decimal_exp(Place, S). insert_decimal_exp(Place, S) -> [C | S0] = S, S1 = case S0 of [] -> "0"; _ -> S0 end, Exp = case Place < 0 of true -> "e-"; false -> "e+" end, [C] ++ "." ++ S1 ++ Exp ++ integer_to_list(abs(Place - 1)). digits1(Float, Exp, Frac) -> Round = ((Frac band 1) =:= 0), case Exp >= 0 of true -> BExp = 1 bsl Exp, case (Frac =/= ?BIG_POW) of true -> scale((Frac * BExp * 2), 2, BExp, BExp, Round, Round, Float); false -> scale((Frac * BExp * 4), 4, (BExp * 2), BExp, Round, Round, Float) end; false -> case (Exp =:= ?MIN_EXP) orelse (Frac =/= ?BIG_POW) of true -> scale((Frac * 2), 1 bsl (1 - Exp), 1, 1, Round, Round, Float); false -> scale((Frac * 4), 1 bsl (2 - Exp), 2, 1, Round, Round, Float) end end. scale(R, S, MPlus, MMinus, LowOk, HighOk, Float) -> Est = int_ceil(math:log10(abs(Float)) - 1.0e-10), %% Note that the scheme implementation uses a 326 element look-up table %% for int_pow(10, N) where we do not. case Est >= 0 of true -> fixup(R, S * int_pow(10, Est), MPlus, MMinus, Est, LowOk, HighOk); false -> Scale = int_pow(10, -Est), fixup(R * Scale, S, MPlus * Scale, MMinus * Scale, Est, LowOk, HighOk) end. fixup(R, S, MPlus, MMinus, K, LowOk, HighOk) -> TooLow = case HighOk of true -> (R + MPlus) >= S; false -> (R + MPlus) > S end, case TooLow of true -> [(K + 1) | generate(R, S, MPlus, MMinus, LowOk, HighOk)]; false -> [K | generate(R * 10, S, MPlus * 10, MMinus * 10, LowOk, HighOk)] end. generate(R0, S, MPlus, MMinus, LowOk, HighOk) -> D = R0 div S, R = R0 rem S, TC1 = case LowOk of true -> R =< MMinus; false -> R < MMinus end, TC2 = case HighOk of true -> (R + MPlus) >= S; false -> (R + MPlus) > S end, case TC1 of false -> case TC2 of false -> [D | generate(R * 10, S, MPlus * 10, MMinus * 10, LowOk, HighOk)]; true -> [D + 1] end; true -> case TC2 of false -> [D]; true -> case R * 2 < S of true -> [D]; false -> [D + 1] end end end. unpack(Float) -> <> = <>, {Sign, Exp, Frac}. frexp1({_Sign, 0, 0}) -> {0.0, 0}; frexp1({Sign, 0, Frac}) -> Exp = log2floor(Frac), <> = <>, {Frac1, -(?FLOAT_BIAS) - 52 + Exp}; frexp1({Sign, Exp, Frac}) -> <> = <>, {Frac1, Exp - ?FLOAT_BIAS}. log2floor(Int) -> log2floor(Int, 0). log2floor(0, N) -> N; 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 %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). int_ceil_test() -> ?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() -> ?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() -> ?assertEqual("0", digits(0)), ?assertEqual("0.0", digits(0.0)), ?assertEqual("0.0", digits(-0.0)), ?assertEqual("1.0", digits(1.0)), ?assertEqual("-1.0", digits(-1.0)), ?assertEqual("0.1", digits(0.1)), ?assertEqual("0.01", digits(0.01)), ?assertEqual("0.001", digits(0.001)), ?assertEqual("1.0e+6", digits(1000000.0)), ?assertEqual("0.5", digits(0.5)), ?assertEqual("4503599627370496.0", digits(4503599627370496.0)), %% small denormalized number %% 4.94065645841246544177e-324 =:= 5.0e-324 <> = <<0,0,0,0,0,0,0,1>>, ?assertEqual("5.0e-324", digits(SmallDenorm)), ?assertEqual(SmallDenorm, list_to_float(digits(SmallDenorm))), %% large denormalized number %% 2.22507385850720088902e-308 <> = <<0,15,255,255,255,255,255,255>>, ?assertEqual("2.225073858507201e-308", digits(BigDenorm)), ?assertEqual(BigDenorm, list_to_float(digits(BigDenorm))), %% small normalized number %% 2.22507385850720138309e-308 <> = <<0,16,0,0,0,0,0,0>>, ?assertEqual("2.2250738585072014e-308", digits(SmallNorm)), ?assertEqual(SmallNorm, list_to_float(digits(SmallNorm))), %% large normalized number %% 1.79769313486231570815e+308 <> = <<127,239,255,255,255,255,255,255>>, ?assertEqual("1.7976931348623157e+308", 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 ?assertEqual({0.0, 0}, frexp(0.0)), %% one ?assertEqual({0.5, 1}, frexp(1.0)), %% negative one ?assertEqual({-0.5, 1}, frexp(-1.0)), %% small denormalized number %% 4.94065645841246544177e-324 <> = <<0,0,0,0,0,0,0,1>>, ?assertEqual({0.5, -1073}, frexp(SmallDenorm)), %% large denormalized number %% 2.22507385850720088902e-308 <> = <<0,15,255,255,255,255,255,255>>, ?assertEqual( {0.99999999999999978, -1022}, frexp(BigDenorm)), %% small normalized number %% 2.22507385850720138309e-308 <> = <<0,16,0,0,0,0,0,0>>, ?assertEqual({0.5, -1021}, frexp(SmallNorm)), %% large normalized number %% 1.79769313486231570815e+308 <> = <<127,239,255,255,255,255,255,255>>, ?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. mochiweb-3.2.1/src/mochitemp.erl000066400000000000000000000211101450333227400165700ustar00rootroot00000000000000%% @author Bob Ippolito %% @copyright 2010 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc Create temporary files and directories. Requires crypto to be started. -module(mochitemp). -export([gettempdir/0]). -export([mkdtemp/0, mkdtemp/3]). -export([rmtempdir/1]). %% -export([mkstemp/4]). -define(SAFE_CHARS, {$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, $_}). -define(TMP_MAX, 10000). -include_lib("kernel/include/file.hrl"). %% TODO: An ugly wrapper over the mktemp tool with open_port and sadness? %% We can't implement this race-free in Erlang without the ability %% to issue O_CREAT|O_EXCL. I suppose we could hack something with %% mkdtemp, del_dir, open. %% mkstemp(Suffix, Prefix, Dir, Options) -> %% ok. rmtempdir(Dir) -> case file:del_dir(Dir) of {error, eexist} -> ok = rmtempdirfiles(Dir), ok = file:del_dir(Dir); ok -> ok end. rmtempdirfiles(Dir) -> {ok, Files} = file:list_dir(Dir), ok = rmtempdirfiles(Dir, Files). rmtempdirfiles(_Dir, []) -> ok; rmtempdirfiles(Dir, [Basename | Rest]) -> Path = filename:join([Dir, Basename]), case filelib:is_dir(Path) of true -> ok = rmtempdir(Path); false -> ok = file:delete(Path) end, rmtempdirfiles(Dir, Rest). mkdtemp() -> mkdtemp("", "tmp", gettempdir()). mkdtemp(Suffix, Prefix, Dir) -> mkdtemp_n(rngpath_fun(Suffix, Prefix, Dir), ?TMP_MAX). mkdtemp_n(RngPath, 1) -> make_dir(RngPath()); mkdtemp_n(RngPath, N) -> try make_dir(RngPath()) catch throw:{error, eexist} -> mkdtemp_n(RngPath, N - 1) end. make_dir(Path) -> case file:make_dir(Path) of ok -> ok; E={error, eexist} -> throw(E) end, %% Small window for a race condition here because dir is created 777 ok = file:write_file_info(Path, #file_info{mode=8#0700}), Path. rngpath_fun(Prefix, Suffix, Dir) -> fun () -> filename:join([Dir, Prefix ++ rngchars(6) ++ Suffix]) end. rngchars(0) -> ""; rngchars(N) -> [rngchar() | rngchars(N - 1)]. rngchar() -> rngchar(mochiweb_util:rand_uniform(0, tuple_size(?SAFE_CHARS))). rngchar(C) -> element(1 + C, ?SAFE_CHARS). %% @spec gettempdir() -> string() %% @doc Get a usable temporary directory using the first of these that is a directory: %% $TMPDIR, $TMP, $TEMP, "/tmp", "/var/tmp", "/usr/tmp", ".". gettempdir() -> gettempdir(gettempdir_checks(), fun normalize_dir/1). gettempdir_checks() -> [{fun os:getenv/1, ["TMPDIR", "TMP", "TEMP"]}, {fun gettempdir_identity/1, ["/tmp", "/var/tmp", "/usr/tmp"]}, {fun gettempdir_cwd/1, [cwd]}]. gettempdir_identity(L) -> L. gettempdir_cwd(cwd) -> {ok, L} = file:get_cwd(), L. gettempdir([{_F, []} | RestF], Normalize) -> gettempdir(RestF, Normalize); gettempdir([{F, [L | RestL]} | RestF], Normalize) -> case Normalize(F(L)) of false -> gettempdir([{F, RestL} | RestF], Normalize); Dir -> Dir end. normalize_dir(False) when False =:= false orelse False =:= "" -> %% Erlang doesn't have an unsetenv, wtf. false; normalize_dir(L) -> Dir = filename:absname(L), case filelib:is_dir(Dir) of false -> false; true -> Dir end. %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). pushenv(L) -> [{K, os:getenv(K)} || K <- L]. popenv(L) -> F = fun ({K, false}) -> %% Erlang doesn't have an unsetenv, wtf. os:putenv(K, ""); ({K, V}) -> os:putenv(K, V) end, lists:foreach(F, L). gettempdir_fallback_test() -> ?assertEqual( "/", gettempdir([{fun gettempdir_identity/1, ["/--not-here--/"]}, {fun gettempdir_identity/1, ["/"]}], fun normalize_dir/1)), ?assertEqual( "/", %% simulate a true os:getenv unset env gettempdir([{fun gettempdir_identity/1, [false]}, {fun gettempdir_identity/1, ["/"]}], fun normalize_dir/1)), ok. gettempdir_identity_test() -> ?assertEqual( "/", gettempdir([{fun gettempdir_identity/1, ["/"]}], fun normalize_dir/1)), ok. gettempdir_cwd_test() -> {ok, Cwd} = file:get_cwd(), ?assertEqual( normalize_dir(Cwd), gettempdir([{fun gettempdir_cwd/1, [cwd]}], fun normalize_dir/1)), ok. rngchars_test() -> crypto:start(), ?assertEqual( "", rngchars(0)), ?assertEqual( 10, length(rngchars(10))), ok. rngchar_test() -> ?assertEqual( $a, rngchar(0)), ?assertEqual( $A, rngchar(26)), ?assertEqual( $_, rngchar(62)), ok. mkdtemp_n_failonce_test() -> crypto:start(), D = mkdtemp(), Path = filename:join([D, "testdir"]), %% Toggle the existence of a dir so that it fails %% the first time and succeeds the second. F = fun () -> case filelib:is_dir(Path) of true -> file:del_dir(Path); false -> file:make_dir(Path) end, Path end, try %% Fails the first time ?assertThrow( {error, eexist}, mkdtemp_n(F, 1)), %% Reset state file:del_dir(Path), %% Succeeds the second time ?assertEqual( Path, mkdtemp_n(F, 2)) after rmtempdir(D) end, ok. mkdtemp_n_fail_test() -> {ok, Cwd} = file:get_cwd(), ?assertThrow( {error, eexist}, mkdtemp_n(fun () -> Cwd end, 1)), ?assertThrow( {error, eexist}, mkdtemp_n(fun () -> Cwd end, 2)), ok. make_dir_fail_test() -> {ok, Cwd} = file:get_cwd(), ?assertThrow( {error, eexist}, make_dir(Cwd)), ok. mkdtemp_test() -> crypto:start(), D = mkdtemp(), ?assertEqual( true, filelib:is_dir(D)), ?assertEqual( ok, file:del_dir(D)), ok. rmtempdir_test() -> crypto:start(), D1 = mkdtemp(), ?assertEqual( true, filelib:is_dir(D1)), ?assertEqual( ok, rmtempdir(D1)), D2 = mkdtemp(), ?assertEqual( true, filelib:is_dir(D2)), ok = file:write_file(filename:join([D2, "foo"]), <<"bytes">>), D3 = mkdtemp("suffix", "prefix", D2), ?assertEqual( true, filelib:is_dir(D3)), ok = file:write_file(filename:join([D3, "foo"]), <<"bytes">>), ?assertEqual( ok, rmtempdir(D2)), ?assertEqual( {error, enoent}, file:consult(D3)), ?assertEqual( {error, enoent}, file:consult(D2)), ok. gettempdir_env_test() -> Env = pushenv(["TMPDIR", "TEMP", "TMP"]), FalseEnv = [{"TMPDIR", false}, {"TEMP", false}, {"TMP", false}], try popenv(FalseEnv), popenv([{"TMPDIR", "/"}]), ?assertEqual( "/", os:getenv("TMPDIR")), ?assertEqual( "/", gettempdir()), {ok, Cwd} = file:get_cwd(), popenv(FalseEnv), popenv([{"TMP", Cwd}]), ?assertEqual( normalize_dir(Cwd), gettempdir()) after popenv(Env) end, ok. -endif. mochiweb-3.2.1/src/mochiutf8.erl000066400000000000000000000305111450333227400165160ustar00rootroot00000000000000%% @copyright 2010 Mochi Media, Inc. %% @author Bob Ippolito %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc Algorithm to convert any binary to a valid UTF-8 sequence by ignoring %% invalid bytes. -module(mochiutf8). -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 -type unichar_low() :: 0..16#d7ff. -type unichar_high() :: 16#e000..16#10ffff. -type unichar() :: unichar_low() | unichar_high(). -spec codepoint_to_bytes(unichar()) -> binary(). %% @doc Convert a unicode codepoint to UTF-8 bytes. codepoint_to_bytes(C) when (C >= 16#00 andalso C =< 16#7f) -> %% U+0000 - U+007F - 7 bits <>; codepoint_to_bytes(C) when (C >= 16#080 andalso C =< 16#07FF) -> %% U+0080 - U+07FF - 11 bits <<0:5, B1:5, B0:6>> = <>, <<2#110:3, B1:5, 2#10:2, B0:6>>; codepoint_to_bytes(C) when (C >= 16#0800 andalso C =< 16#FFFF) andalso (C < 16#D800 orelse C > 16#DFFF) -> %% U+0800 - U+FFFF - 16 bits (excluding UTC-16 surrogate code points) <> = <>, <<2#1110:4, B2:4, 2#10:2, B1:6, 2#10:2, B0:6>>; codepoint_to_bytes(C) when (C >= 16#010000 andalso C =< 16#10FFFF) -> %% U+10000 - U+10FFFF - 21 bits <<0:3, B3:3, B2:6, B1:6, B0:6>> = <>, <<2#11110:5, B3:3, 2#10:2, B2:6, 2#10:2, B1:6, 2#10:2, B0:6>>. -spec codepoints_to_bytes([unichar()]) -> binary(). %% @doc Convert a list of codepoints to a UTF-8 binary. codepoints_to_bytes(L) -> <<<<(codepoint_to_bytes(C))/binary>> || C <- L>>. -spec read_codepoint(binary()) -> {unichar(), binary(), binary()}. read_codepoint(Bin = <<2#0:1, C:7, Rest/binary>>) -> %% U+0000 - U+007F - 7 bits <> = Bin, {C, B, Rest}; read_codepoint(Bin = <<2#110:3, B1:5, 2#10:2, B0:6, Rest/binary>>) -> %% U+0080 - U+07FF - 11 bits case <> of <> when C >= 16#80 -> <> = Bin, {C, B, Rest} end; read_codepoint(Bin = <<2#1110:4, B2:4, 2#10:2, B1:6, 2#10:2, B0:6, Rest/binary>>) -> %% U+0800 - U+FFFF - 16 bits (excluding UTC-16 surrogate code points) case <> of <> when (C >= 16#0800 andalso C =< 16#FFFF) andalso (C < 16#D800 orelse C > 16#DFFF) -> <> = Bin, {C, B, Rest} end; read_codepoint(Bin = <<2#11110:5, B3:3, 2#10:2, B2:6, 2#10:2, B1:6, 2#10:2, B0:6, Rest/binary>>) -> %% U+10000 - U+10FFFF - 21 bits case <> of <> when (C >= 16#010000 andalso C =< 16#10FFFF) -> <> = Bin, {C, B, Rest} end. -spec codepoint_foldl(fun((unichar(), _) -> _), _, binary()) -> _. codepoint_foldl(F, Acc, <<>>) when is_function(F, 2) -> Acc; codepoint_foldl(F, Acc, Bin) -> {C, _, Rest} = read_codepoint(Bin), codepoint_foldl(F, F(C, Acc), Rest). -spec bytes_foldl(fun((binary(), _) -> _), _, binary()) -> _. bytes_foldl(F, Acc, <<>>) when is_function(F, 2) -> Acc; bytes_foldl(F, Acc, Bin) -> {_, B, Rest} = read_codepoint(Bin), bytes_foldl(F, F(B, Acc), Rest). -spec bytes_to_codepoints(binary()) -> [unichar()]. bytes_to_codepoints(B) -> lists:reverse(codepoint_foldl(fun (C, Acc) -> [C | Acc] end, [], B)). -spec len(binary()) -> non_neg_integer(). len(<<>>) -> 0; len(B) -> {_, _, Rest} = read_codepoint(B), 1 + len(Rest). -spec valid_utf8_bytes(B::binary()) -> binary(). %% @doc Return only the bytes in B that represent valid UTF-8. Uses %% the following recursive algorithm: skip one byte if B does not %% follow UTF-8 syntax (a 1-4 byte encoding of some number), %% skip sequence of 2-4 bytes if it represents an overlong encoding %% or bad code point (surrogate U+D800 - U+DFFF or > U+10FFFF). valid_utf8_bytes(B) when is_binary(B) -> binary_skip_bytes(B, invalid_utf8_indexes(B)). %% Internal API -spec binary_skip_bytes(binary(), [non_neg_integer()]) -> binary(). %% @doc Return B, but skipping the 0-based indexes in L. binary_skip_bytes(B, []) -> B; binary_skip_bytes(B, L) -> binary_skip_bytes(B, L, 0, []). %% @private -spec binary_skip_bytes(binary(), [non_neg_integer()], non_neg_integer(), iolist()) -> binary(). binary_skip_bytes(B, [], _N, Acc) -> iolist_to_binary(lists:reverse([B | Acc])); binary_skip_bytes(<<_, RestB/binary>>, [N | RestL], N, Acc) -> binary_skip_bytes(RestB, RestL, 1 + N, Acc); binary_skip_bytes(<>, L, N, Acc) -> binary_skip_bytes(RestB, L, 1 + N, [C | Acc]). -spec invalid_utf8_indexes(binary()) -> [non_neg_integer()]. %% @doc Return the 0-based indexes in B that are not valid UTF-8. invalid_utf8_indexes(B) -> invalid_utf8_indexes(B, 0, []). %% @private. -spec invalid_utf8_indexes(binary(), non_neg_integer(), [non_neg_integer()]) -> [non_neg_integer()]. invalid_utf8_indexes(<>, N, Acc) when C < 16#80 -> %% U+0000 - U+007F - 7 bits invalid_utf8_indexes(Rest, 1 + N, Acc); invalid_utf8_indexes(<>, N, Acc) when C1 band 16#E0 =:= 16#C0, 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 -> %% Overlong encoding. invalid_utf8_indexes(Rest, 2 + N, [1 + N, N | Acc]); _ -> %% Upper bound U+07FF does not need to be checked invalid_utf8_indexes(Rest, 2 + N, Acc) end; invalid_utf8_indexes(<>, N, Acc) when C1 band 16#F0 =:= 16#E0, C2 band 16#C0 =:= 16#80, 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. 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) end; invalid_utf8_indexes(<>, N, Acc) when C1 band 16#F8 =:= 16#F0, C2 band 16#C0 =:= 16#80, C3 band 16#C0 =:= 16#80, C4 band 16#C0 =:= 16#80 -> %% 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) end; invalid_utf8_indexes(<<_, Rest/binary>>, N, Acc) -> %% Invalid char invalid_utf8_indexes(Rest, 1 + N, [N | Acc]); invalid_utf8_indexes(<<>>, _N, Acc) -> lists:reverse(Acc). %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). binary_skip_bytes_test() -> ?assertEqual(<<"foo">>, binary_skip_bytes(<<"foo">>, [])), ?assertEqual(<<"foobar">>, binary_skip_bytes(<<"foo bar">>, [3])), ?assertEqual(<<"foo">>, binary_skip_bytes(<<"foo bar">>, [3, 4, 5, 6])), ?assertEqual(<<"oo bar">>, binary_skip_bytes(<<"foo bar">>, [0])), ok. invalid_utf8_indexes_test() -> ?assertEqual( [], invalid_utf8_indexes(<<"unicode snowman for you: ", 226, 152, 131>>)), ?assertEqual( [0], invalid_utf8_indexes(<<128>>)), ?assertEqual( [57,59,60,64,66,67], invalid_utf8_indexes(<<"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; (", 167, 65, 170, 186, 73, 83, 80, 166, 87, 186, 217, 41, 41>>)), ok. codepoint_to_bytes_test() -> %% U+0000 - U+007F - 7 bits %% U+0080 - U+07FF - 11 bits %% U+0800 - U+FFFF - 16 bits (excluding UTC-16 surrogate code points) %% U+10000 - U+10FFFF - 21 bits ?assertEqual( <<"a">>, codepoint_to_bytes($a)), ?assertEqual( <<16#c2, 16#80>>, codepoint_to_bytes(16#80)), ?assertEqual( <<16#df, 16#bf>>, codepoint_to_bytes(16#07ff)), ?assertEqual( <<16#ef, 16#bf, 16#bf>>, codepoint_to_bytes(16#ffff)), ?assertEqual( <<16#f4, 16#8f, 16#bf, 16#bf>>, codepoint_to_bytes(16#10ffff)), ok. bytes_foldl_test() -> ?assertEqual( <<"abc">>, bytes_foldl(fun (B, Acc) -> <> end, <<>>, <<"abc">>)), ?assertEqual( <<"abc", 226, 152, 131, 228, 184, 173, 194, 133, 244,143,191,191>>, bytes_foldl(fun (B, Acc) -> <> end, <<>>, <<"abc", 226, 152, 131, 228, 184, 173, 194, 133, 244,143,191,191>>)), ok. bytes_to_codepoints_test() -> ?assertEqual( "abc" ++ [16#2603, 16#4e2d, 16#85, 16#10ffff], bytes_to_codepoints(<<"abc", 226, 152, 131, 228, 184, 173, 194, 133, 244,143,191,191>>)), ok. codepoint_foldl_test() -> ?assertEqual( "cba", codepoint_foldl(fun (C, Acc) -> [C | Acc] end, [], <<"abc">>)), ?assertEqual( [16#10ffff, 16#85, 16#4e2d, 16#2603 | "cba"], codepoint_foldl(fun (C, Acc) -> [C | Acc] end, [], <<"abc", 226, 152, 131, 228, 184, 173, 194, 133, 244,143,191,191>>)), ok. len_test() -> ?assertEqual( 29, len(<<"unicode snowman for you: ", 226, 152, 131, 228, 184, 173, 194, 133, 244, 143, 191, 191>>)), ok. codepoints_to_bytes_test() -> ?assertEqual( iolist_to_binary(lists:map(fun codepoint_to_bytes/1, lists:seq(1, 1000))), codepoints_to_bytes(lists:seq(1, 1000))), ok. valid_utf8_bytes_test() -> ?assertEqual( <<"invalid U+11ffff: ">>, valid_utf8_bytes(<<"invalid U+11ffff: ", 244, 159, 191, 191>>)), ?assertEqual( <<"U+10ffff: ", 244, 143, 191, 191>>, valid_utf8_bytes(<<"U+10ffff: ", 244, 143, 191, 191>>)), ?assertEqual( <<"overlong 2-byte encoding (a): ">>, valid_utf8_bytes(<<"overlong 2-byte encoding (a): ", 2#11000001, 2#10100001>>)), ?assertEqual( <<"overlong 2-byte encoding (!): ">>, valid_utf8_bytes(<<"overlong 2-byte encoding (!): ", 2#11000000, 2#10100001>>)), ?assertEqual( <<"mu: ", 194, 181>>, valid_utf8_bytes(<<"mu: ", 194, 181>>)), ?assertEqual( <<"bad coding bytes: ">>, valid_utf8_bytes(<<"bad coding bytes: ", 2#10011111, 2#10111111, 2#11111111>>)), ?assertEqual( <<"low surrogate (unpaired): ">>, valid_utf8_bytes(<<"low surrogate (unpaired): ", 237, 176, 128>>)), ?assertEqual( <<"high surrogate (unpaired): ">>, valid_utf8_bytes(<<"high surrogate (unpaired): ", 237, 191, 191>>)), ?assertEqual( <<"unicode snowman for you: ", 226, 152, 131>>, valid_utf8_bytes(<<"unicode snowman for you: ", 226, 152, 131>>)), ?assertEqual( <<"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; (AISPW))">>, valid_utf8_bytes(<<"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; (", 167, 65, 170, 186, 73, 83, 80, 166, 87, 186, 217, 41, 41>>)), ok. -endif. mochiweb-3.2.1/src/mochiweb.app.src000066400000000000000000000005711450333227400171740ustar00rootroot00000000000000%% This is generated from src/mochiweb.app.src {application, mochiweb, [{description, "MochiMedia Web Server"}, {vsn, "3.2.1"}, {modules, []}, {registered, []}, {env, []}, {applications, [kernel, stdlib, crypto, inets, ssl, xmerl, compiler, syntax_tools]}, {licenses, ["MIT"]}, {links, [{"Github", "https://github.com/mochi/mochiweb"}]} ] }. mochiweb-3.2.1/src/mochiweb.erl000066400000000000000000000073511450333227400164130ustar00rootroot00000000000000%% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc Start and stop the MochiWeb server. -module(mochiweb). -author('bob@mochimedia.com'). -export([new_request/1, new_response/1]). -export([all_loaded/0, all_loaded/1, reload/0]). -export([ensure_started/1]). reload() -> [c:l(Module) || Module <- all_loaded()]. all_loaded() -> all_loaded(filename:dirname(code:which(?MODULE))). all_loaded(Base) when is_atom(Base) -> []; all_loaded(Base) -> FullBase = Base ++ "/", F = fun ({_Module, Loaded}, Acc) when is_atom(Loaded) -> Acc; ({Module, Loaded}, Acc) -> case lists:prefix(FullBase, Loaded) of true -> [Module | Acc]; false -> Acc end end, lists:foldl(F, [], code:all_loaded()). %% See the erlang:decode_packet/3 docs for the full type -spec uri(HttpUri :: term()) -> string(). uri({abs_path, Uri}) -> Uri; %% TODO: %% This makes it hard to implement certain kinds of proxies with mochiweb, %% perhaps a field could be added to the mochiweb_request record to preserve %% this information in raw_path. uri({absoluteURI, _Protocol, _Host, _Port, Uri}) -> Uri; %% From http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2 uri('*') -> "*"; %% Erlang decode_packet will return this for requests like `CONNECT host:port` uri({scheme, Hostname, Port}) -> Hostname ++ ":" ++ Port; uri(HttpString) when is_list(HttpString) -> HttpString. %% @spec new_request( {Socket, Request, Headers} %% | {Socket, Opts, Request, Headers} ) -> MochiWebRequest %% @doc Return a mochiweb_request data structure. new_request({Socket, {Method, HttpUri, Version}, Headers}) -> new_request({Socket, [], {Method, HttpUri, Version}, Headers}); new_request({Socket, Opts, {Method, HttpUri, Version}, Headers}) -> mochiweb_request:new(Socket, Opts, Method, uri(HttpUri), Version, mochiweb_headers:make(Headers)). %% @spec new_response({Request, integer(), Headers}) -> MochiWebResponse %% @doc Return a mochiweb_response data structure. new_response({Request, Code, Headers}) -> mochiweb_response:new(Request, Code, mochiweb_headers:make(Headers)). %% @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 -> ok; {error, {already_started, App}} -> ok end. mochiweb-3.2.1/src/mochiweb_acceptor.erl000066400000000000000000000054201450333227400202660ustar00rootroot00000000000000%% @author Bob Ippolito %% @copyright 2010 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc MochiWeb acceptor. -module(mochiweb_acceptor). -author('bob@mochimedia.com'). -include("internal.hrl"). -export([init/4, start_link/3, start_link/4]). -define(EMFILE_SLEEP_MSEC, 100). start_link(Server, Listen, Loop) -> start_link(Server, Listen, Loop, []). start_link(Server, Listen, Loop, Opts) -> proc_lib:spawn_link(?MODULE, init, [Server, Listen, Loop, Opts]). do_accept(Server, Listen) -> T1 = os:timestamp(), case mochiweb_socket:transport_accept(Listen) of {ok, Socket} -> gen_server:cast(Server, {accepted, self(), timer:now_diff(os:timestamp(), T1)}), mochiweb_socket:finish_accept(Socket); Other -> Other end. init(Server, Listen, Loop, Opts) -> case catch do_accept(Server, Listen) of {ok, Socket} -> call_loop(Loop, Socket, Opts); {error, Err} when Err =:= closed orelse Err =:= esslaccept orelse Err =:= timeout -> exit({shutdown, Err}); Other -> %% Mitigate out of file descriptor scenario by sleeping for a %% short time to slow error rate case Other of {error, emfile} -> receive after ?EMFILE_SLEEP_MSEC -> ok end; _ -> ok end, error_logger:error_report([{application, mochiweb}, "Accept failed error", lists:flatten(io_lib:format("~p", [Other]))]), exit({error, accept_failed}) end. call_loop({M, F}, Socket, Opts) when is_atom(M) -> M:F(Socket, Opts); call_loop({M, F, [A1]}, Socket, Opts) when is_atom(M) -> M:F(Socket, Opts, A1); call_loop({M, F, A}, Socket, Opts) when is_atom(M) -> erlang:apply(M, F, [Socket, Opts | A]); call_loop(Loop, Socket, Opts) -> Loop(Socket, Opts). mochiweb-3.2.1/src/mochiweb_base64url.erl000066400000000000000000000104041450333227400202730ustar00rootroot00000000000000%% @author Bob Ippolito %% @copyright 2013 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. -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()) -> 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()) -> 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), <> = Bin, Main = << <<(b64e(C)):8>> || <> <= Main0 >>, case Rest of <> -> <
>; <> -> <
>; <<>> -> Main end. decode_binary(Bin) -> Main = << <<(b64d(C)):6>> || <> <= 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, <> = 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. mochiweb-3.2.1/src/mochiweb_charref.erl000066400000000000000000002062341450333227400201060ustar00rootroot00000000000000%% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc Converts HTML 5 charrefs and entities to codepoints (or lists of code points). -module(mochiweb_charref). -export([charref/1]). %% External API. %% @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 -> try erlang:list_to_integer(L, 16) catch error:badarg -> undefined end; charref([$# | L]) -> try list_to_integer(L) catch error:badarg -> undefined end; charref(L) -> entity(L). %% Internal API. %% [2011-10-14] Generated from: %% http://www.w3.org/TR/html5/named-character-references.html entity("AElig") -> 16#000C6; entity("AMP") -> 16#00026; entity("Aacute") -> 16#000C1; entity("Abreve") -> 16#00102; entity("Acirc") -> 16#000C2; entity("Acy") -> 16#00410; entity("Afr") -> 16#1D504; entity("Agrave") -> 16#000C0; entity("Alpha") -> 16#00391; entity("Amacr") -> 16#00100; entity("And") -> 16#02A53; entity("Aogon") -> 16#00104; entity("Aopf") -> 16#1D538; entity("ApplyFunction") -> 16#02061; entity("Aring") -> 16#000C5; entity("Ascr") -> 16#1D49C; entity("Assign") -> 16#02254; entity("Atilde") -> 16#000C3; entity("Auml") -> 16#000C4; entity("Backslash") -> 16#02216; entity("Barv") -> 16#02AE7; entity("Barwed") -> 16#02306; entity("Bcy") -> 16#00411; entity("Because") -> 16#02235; entity("Bernoullis") -> 16#0212C; entity("Beta") -> 16#00392; entity("Bfr") -> 16#1D505; entity("Bopf") -> 16#1D539; entity("Breve") -> 16#002D8; entity("Bscr") -> 16#0212C; entity("Bumpeq") -> 16#0224E; entity("CHcy") -> 16#00427; entity("COPY") -> 16#000A9; entity("Cacute") -> 16#00106; entity("Cap") -> 16#022D2; entity("CapitalDifferentialD") -> 16#02145; entity("Cayleys") -> 16#0212D; entity("Ccaron") -> 16#0010C; entity("Ccedil") -> 16#000C7; entity("Ccirc") -> 16#00108; entity("Cconint") -> 16#02230; entity("Cdot") -> 16#0010A; entity("Cedilla") -> 16#000B8; entity("CenterDot") -> 16#000B7; entity("Cfr") -> 16#0212D; entity("Chi") -> 16#003A7; entity("CircleDot") -> 16#02299; entity("CircleMinus") -> 16#02296; entity("CirclePlus") -> 16#02295; entity("CircleTimes") -> 16#02297; entity("ClockwiseContourIntegral") -> 16#02232; entity("CloseCurlyDoubleQuote") -> 16#0201D; entity("CloseCurlyQuote") -> 16#02019; entity("Colon") -> 16#02237; entity("Colone") -> 16#02A74; entity("Congruent") -> 16#02261; entity("Conint") -> 16#0222F; entity("ContourIntegral") -> 16#0222E; entity("Copf") -> 16#02102; entity("Coproduct") -> 16#02210; entity("CounterClockwiseContourIntegral") -> 16#02233; entity("Cross") -> 16#02A2F; entity("Cscr") -> 16#1D49E; entity("Cup") -> 16#022D3; entity("CupCap") -> 16#0224D; entity("DD") -> 16#02145; entity("DDotrahd") -> 16#02911; entity("DJcy") -> 16#00402; entity("DScy") -> 16#00405; entity("DZcy") -> 16#0040F; entity("Dagger") -> 16#02021; entity("Darr") -> 16#021A1; entity("Dashv") -> 16#02AE4; entity("Dcaron") -> 16#0010E; entity("Dcy") -> 16#00414; entity("Del") -> 16#02207; entity("Delta") -> 16#00394; entity("Dfr") -> 16#1D507; entity("DiacriticalAcute") -> 16#000B4; entity("DiacriticalDot") -> 16#002D9; entity("DiacriticalDoubleAcute") -> 16#002DD; entity("DiacriticalGrave") -> 16#00060; entity("DiacriticalTilde") -> 16#002DC; entity("Diamond") -> 16#022C4; entity("DifferentialD") -> 16#02146; entity("Dopf") -> 16#1D53B; entity("Dot") -> 16#000A8; entity("DotDot") -> 16#020DC; entity("DotEqual") -> 16#02250; entity("DoubleContourIntegral") -> 16#0222F; entity("DoubleDot") -> 16#000A8; entity("DoubleDownArrow") -> 16#021D3; entity("DoubleLeftArrow") -> 16#021D0; entity("DoubleLeftRightArrow") -> 16#021D4; entity("DoubleLeftTee") -> 16#02AE4; entity("DoubleLongLeftArrow") -> 16#027F8; entity("DoubleLongLeftRightArrow") -> 16#027FA; entity("DoubleLongRightArrow") -> 16#027F9; entity("DoubleRightArrow") -> 16#021D2; entity("DoubleRightTee") -> 16#022A8; entity("DoubleUpArrow") -> 16#021D1; entity("DoubleUpDownArrow") -> 16#021D5; entity("DoubleVerticalBar") -> 16#02225; entity("DownArrow") -> 16#02193; entity("DownArrowBar") -> 16#02913; entity("DownArrowUpArrow") -> 16#021F5; entity("DownBreve") -> 16#00311; entity("DownLeftRightVector") -> 16#02950; entity("DownLeftTeeVector") -> 16#0295E; entity("DownLeftVector") -> 16#021BD; entity("DownLeftVectorBar") -> 16#02956; entity("DownRightTeeVector") -> 16#0295F; entity("DownRightVector") -> 16#021C1; entity("DownRightVectorBar") -> 16#02957; entity("DownTee") -> 16#022A4; entity("DownTeeArrow") -> 16#021A7; entity("Downarrow") -> 16#021D3; entity("Dscr") -> 16#1D49F; entity("Dstrok") -> 16#00110; entity("ENG") -> 16#0014A; entity("ETH") -> 16#000D0; entity("Eacute") -> 16#000C9; entity("Ecaron") -> 16#0011A; entity("Ecirc") -> 16#000CA; entity("Ecy") -> 16#0042D; entity("Edot") -> 16#00116; entity("Efr") -> 16#1D508; entity("Egrave") -> 16#000C8; entity("Element") -> 16#02208; entity("Emacr") -> 16#00112; entity("EmptySmallSquare") -> 16#025FB; entity("EmptyVerySmallSquare") -> 16#025AB; entity("Eogon") -> 16#00118; entity("Eopf") -> 16#1D53C; entity("Epsilon") -> 16#00395; entity("Equal") -> 16#02A75; entity("EqualTilde") -> 16#02242; entity("Equilibrium") -> 16#021CC; entity("Escr") -> 16#02130; entity("Esim") -> 16#02A73; entity("Eta") -> 16#00397; entity("Euml") -> 16#000CB; entity("Exists") -> 16#02203; entity("ExponentialE") -> 16#02147; entity("Fcy") -> 16#00424; entity("Ffr") -> 16#1D509; entity("FilledSmallSquare") -> 16#025FC; entity("FilledVerySmallSquare") -> 16#025AA; entity("Fopf") -> 16#1D53D; entity("ForAll") -> 16#02200; entity("Fouriertrf") -> 16#02131; entity("Fscr") -> 16#02131; entity("GJcy") -> 16#00403; entity("GT") -> 16#0003E; entity("Gamma") -> 16#00393; entity("Gammad") -> 16#003DC; entity("Gbreve") -> 16#0011E; entity("Gcedil") -> 16#00122; entity("Gcirc") -> 16#0011C; entity("Gcy") -> 16#00413; entity("Gdot") -> 16#00120; entity("Gfr") -> 16#1D50A; entity("Gg") -> 16#022D9; entity("Gopf") -> 16#1D53E; entity("GreaterEqual") -> 16#02265; entity("GreaterEqualLess") -> 16#022DB; entity("GreaterFullEqual") -> 16#02267; entity("GreaterGreater") -> 16#02AA2; entity("GreaterLess") -> 16#02277; entity("GreaterSlantEqual") -> 16#02A7E; entity("GreaterTilde") -> 16#02273; entity("Gscr") -> 16#1D4A2; entity("Gt") -> 16#0226B; entity("HARDcy") -> 16#0042A; entity("Hacek") -> 16#002C7; entity("Hat") -> 16#0005E; entity("Hcirc") -> 16#00124; entity("Hfr") -> 16#0210C; entity("HilbertSpace") -> 16#0210B; entity("Hopf") -> 16#0210D; entity("HorizontalLine") -> 16#02500; entity("Hscr") -> 16#0210B; entity("Hstrok") -> 16#00126; entity("HumpDownHump") -> 16#0224E; entity("HumpEqual") -> 16#0224F; entity("IEcy") -> 16#00415; entity("IJlig") -> 16#00132; entity("IOcy") -> 16#00401; entity("Iacute") -> 16#000CD; entity("Icirc") -> 16#000CE; entity("Icy") -> 16#00418; entity("Idot") -> 16#00130; entity("Ifr") -> 16#02111; entity("Igrave") -> 16#000CC; entity("Im") -> 16#02111; entity("Imacr") -> 16#0012A; entity("ImaginaryI") -> 16#02148; entity("Implies") -> 16#021D2; entity("Int") -> 16#0222C; entity("Integral") -> 16#0222B; entity("Intersection") -> 16#022C2; entity("InvisibleComma") -> 16#02063; entity("InvisibleTimes") -> 16#02062; entity("Iogon") -> 16#0012E; entity("Iopf") -> 16#1D540; entity("Iota") -> 16#00399; entity("Iscr") -> 16#02110; entity("Itilde") -> 16#00128; entity("Iukcy") -> 16#00406; entity("Iuml") -> 16#000CF; entity("Jcirc") -> 16#00134; entity("Jcy") -> 16#00419; entity("Jfr") -> 16#1D50D; entity("Jopf") -> 16#1D541; entity("Jscr") -> 16#1D4A5; entity("Jsercy") -> 16#00408; entity("Jukcy") -> 16#00404; entity("KHcy") -> 16#00425; entity("KJcy") -> 16#0040C; entity("Kappa") -> 16#0039A; entity("Kcedil") -> 16#00136; entity("Kcy") -> 16#0041A; entity("Kfr") -> 16#1D50E; entity("Kopf") -> 16#1D542; entity("Kscr") -> 16#1D4A6; entity("LJcy") -> 16#00409; entity("LT") -> 16#0003C; entity("Lacute") -> 16#00139; entity("Lambda") -> 16#0039B; entity("Lang") -> 16#027EA; entity("Laplacetrf") -> 16#02112; entity("Larr") -> 16#0219E; entity("Lcaron") -> 16#0013D; entity("Lcedil") -> 16#0013B; entity("Lcy") -> 16#0041B; entity("LeftAngleBracket") -> 16#027E8; entity("LeftArrow") -> 16#02190; entity("LeftArrowBar") -> 16#021E4; entity("LeftArrowRightArrow") -> 16#021C6; entity("LeftCeiling") -> 16#02308; entity("LeftDoubleBracket") -> 16#027E6; entity("LeftDownTeeVector") -> 16#02961; entity("LeftDownVector") -> 16#021C3; entity("LeftDownVectorBar") -> 16#02959; entity("LeftFloor") -> 16#0230A; entity("LeftRightArrow") -> 16#02194; entity("LeftRightVector") -> 16#0294E; entity("LeftTee") -> 16#022A3; entity("LeftTeeArrow") -> 16#021A4; entity("LeftTeeVector") -> 16#0295A; entity("LeftTriangle") -> 16#022B2; entity("LeftTriangleBar") -> 16#029CF; entity("LeftTriangleEqual") -> 16#022B4; entity("LeftUpDownVector") -> 16#02951; entity("LeftUpTeeVector") -> 16#02960; entity("LeftUpVector") -> 16#021BF; entity("LeftUpVectorBar") -> 16#02958; entity("LeftVector") -> 16#021BC; entity("LeftVectorBar") -> 16#02952; entity("Leftarrow") -> 16#021D0; entity("Leftrightarrow") -> 16#021D4; entity("LessEqualGreater") -> 16#022DA; entity("LessFullEqual") -> 16#02266; entity("LessGreater") -> 16#02276; entity("LessLess") -> 16#02AA1; entity("LessSlantEqual") -> 16#02A7D; entity("LessTilde") -> 16#02272; entity("Lfr") -> 16#1D50F; entity("Ll") -> 16#022D8; entity("Lleftarrow") -> 16#021DA; entity("Lmidot") -> 16#0013F; entity("LongLeftArrow") -> 16#027F5; entity("LongLeftRightArrow") -> 16#027F7; entity("LongRightArrow") -> 16#027F6; entity("Longleftarrow") -> 16#027F8; entity("Longleftrightarrow") -> 16#027FA; entity("Longrightarrow") -> 16#027F9; entity("Lopf") -> 16#1D543; entity("LowerLeftArrow") -> 16#02199; entity("LowerRightArrow") -> 16#02198; entity("Lscr") -> 16#02112; entity("Lsh") -> 16#021B0; entity("Lstrok") -> 16#00141; entity("Lt") -> 16#0226A; entity("Map") -> 16#02905; entity("Mcy") -> 16#0041C; entity("MediumSpace") -> 16#0205F; entity("Mellintrf") -> 16#02133; entity("Mfr") -> 16#1D510; entity("MinusPlus") -> 16#02213; entity("Mopf") -> 16#1D544; entity("Mscr") -> 16#02133; entity("Mu") -> 16#0039C; entity("NJcy") -> 16#0040A; entity("Nacute") -> 16#00143; entity("Ncaron") -> 16#00147; entity("Ncedil") -> 16#00145; entity("Ncy") -> 16#0041D; entity("NegativeMediumSpace") -> 16#0200B; entity("NegativeThickSpace") -> 16#0200B; entity("NegativeThinSpace") -> 16#0200B; entity("NegativeVeryThinSpace") -> 16#0200B; entity("NestedGreaterGreater") -> 16#0226B; entity("NestedLessLess") -> 16#0226A; entity("NewLine") -> 16#0000A; entity("Nfr") -> 16#1D511; entity("NoBreak") -> 16#02060; entity("NonBreakingSpace") -> 16#000A0; entity("Nopf") -> 16#02115; entity("Not") -> 16#02AEC; entity("NotCongruent") -> 16#02262; entity("NotCupCap") -> 16#0226D; entity("NotDoubleVerticalBar") -> 16#02226; entity("NotElement") -> 16#02209; entity("NotEqual") -> 16#02260; entity("NotEqualTilde") -> [16#02242, 16#00338]; entity("NotExists") -> 16#02204; entity("NotGreater") -> 16#0226F; entity("NotGreaterEqual") -> 16#02271; entity("NotGreaterFullEqual") -> [16#02267, 16#00338]; entity("NotGreaterGreater") -> [16#0226B, 16#00338]; entity("NotGreaterLess") -> 16#02279; entity("NotGreaterSlantEqual") -> [16#02A7E, 16#00338]; entity("NotGreaterTilde") -> 16#02275; entity("NotHumpDownHump") -> [16#0224E, 16#00338]; entity("NotHumpEqual") -> [16#0224F, 16#00338]; entity("NotLeftTriangle") -> 16#022EA; entity("NotLeftTriangleBar") -> [16#029CF, 16#00338]; entity("NotLeftTriangleEqual") -> 16#022EC; entity("NotLess") -> 16#0226E; entity("NotLessEqual") -> 16#02270; entity("NotLessGreater") -> 16#02278; entity("NotLessLess") -> [16#0226A, 16#00338]; entity("NotLessSlantEqual") -> [16#02A7D, 16#00338]; entity("NotLessTilde") -> 16#02274; entity("NotNestedGreaterGreater") -> [16#02AA2, 16#00338]; entity("NotNestedLessLess") -> [16#02AA1, 16#00338]; entity("NotPrecedes") -> 16#02280; entity("NotPrecedesEqual") -> [16#02AAF, 16#00338]; entity("NotPrecedesSlantEqual") -> 16#022E0; entity("NotReverseElement") -> 16#0220C; entity("NotRightTriangle") -> 16#022EB; entity("NotRightTriangleBar") -> [16#029D0, 16#00338]; entity("NotRightTriangleEqual") -> 16#022ED; entity("NotSquareSubset") -> [16#0228F, 16#00338]; entity("NotSquareSubsetEqual") -> 16#022E2; entity("NotSquareSuperset") -> [16#02290, 16#00338]; entity("NotSquareSupersetEqual") -> 16#022E3; entity("NotSubset") -> [16#02282, 16#020D2]; entity("NotSubsetEqual") -> 16#02288; entity("NotSucceeds") -> 16#02281; entity("NotSucceedsEqual") -> [16#02AB0, 16#00338]; entity("NotSucceedsSlantEqual") -> 16#022E1; entity("NotSucceedsTilde") -> [16#0227F, 16#00338]; entity("NotSuperset") -> [16#02283, 16#020D2]; entity("NotSupersetEqual") -> 16#02289; entity("NotTilde") -> 16#02241; entity("NotTildeEqual") -> 16#02244; entity("NotTildeFullEqual") -> 16#02247; entity("NotTildeTilde") -> 16#02249; entity("NotVerticalBar") -> 16#02224; entity("Nscr") -> 16#1D4A9; entity("Ntilde") -> 16#000D1; entity("Nu") -> 16#0039D; entity("OElig") -> 16#00152; entity("Oacute") -> 16#000D3; entity("Ocirc") -> 16#000D4; entity("Ocy") -> 16#0041E; entity("Odblac") -> 16#00150; entity("Ofr") -> 16#1D512; entity("Ograve") -> 16#000D2; entity("Omacr") -> 16#0014C; entity("Omega") -> 16#003A9; entity("Omicron") -> 16#0039F; entity("Oopf") -> 16#1D546; entity("OpenCurlyDoubleQuote") -> 16#0201C; entity("OpenCurlyQuote") -> 16#02018; entity("Or") -> 16#02A54; entity("Oscr") -> 16#1D4AA; entity("Oslash") -> 16#000D8; entity("Otilde") -> 16#000D5; entity("Otimes") -> 16#02A37; entity("Ouml") -> 16#000D6; entity("OverBar") -> 16#0203E; entity("OverBrace") -> 16#023DE; entity("OverBracket") -> 16#023B4; entity("OverParenthesis") -> 16#023DC; entity("PartialD") -> 16#02202; entity("Pcy") -> 16#0041F; entity("Pfr") -> 16#1D513; entity("Phi") -> 16#003A6; entity("Pi") -> 16#003A0; entity("PlusMinus") -> 16#000B1; entity("Poincareplane") -> 16#0210C; entity("Popf") -> 16#02119; entity("Pr") -> 16#02ABB; entity("Precedes") -> 16#0227A; entity("PrecedesEqual") -> 16#02AAF; entity("PrecedesSlantEqual") -> 16#0227C; entity("PrecedesTilde") -> 16#0227E; entity("Prime") -> 16#02033; entity("Product") -> 16#0220F; entity("Proportion") -> 16#02237; entity("Proportional") -> 16#0221D; entity("Pscr") -> 16#1D4AB; entity("Psi") -> 16#003A8; entity("QUOT") -> 16#00022; entity("Qfr") -> 16#1D514; entity("Qopf") -> 16#0211A; entity("Qscr") -> 16#1D4AC; entity("RBarr") -> 16#02910; entity("REG") -> 16#000AE; entity("Racute") -> 16#00154; entity("Rang") -> 16#027EB; entity("Rarr") -> 16#021A0; entity("Rarrtl") -> 16#02916; entity("Rcaron") -> 16#00158; entity("Rcedil") -> 16#00156; entity("Rcy") -> 16#00420; entity("Re") -> 16#0211C; entity("ReverseElement") -> 16#0220B; entity("ReverseEquilibrium") -> 16#021CB; entity("ReverseUpEquilibrium") -> 16#0296F; entity("Rfr") -> 16#0211C; entity("Rho") -> 16#003A1; entity("RightAngleBracket") -> 16#027E9; entity("RightArrow") -> 16#02192; entity("RightArrowBar") -> 16#021E5; entity("RightArrowLeftArrow") -> 16#021C4; entity("RightCeiling") -> 16#02309; entity("RightDoubleBracket") -> 16#027E7; entity("RightDownTeeVector") -> 16#0295D; entity("RightDownVector") -> 16#021C2; entity("RightDownVectorBar") -> 16#02955; entity("RightFloor") -> 16#0230B; entity("RightTee") -> 16#022A2; entity("RightTeeArrow") -> 16#021A6; entity("RightTeeVector") -> 16#0295B; entity("RightTriangle") -> 16#022B3; entity("RightTriangleBar") -> 16#029D0; entity("RightTriangleEqual") -> 16#022B5; entity("RightUpDownVector") -> 16#0294F; entity("RightUpTeeVector") -> 16#0295C; entity("RightUpVector") -> 16#021BE; entity("RightUpVectorBar") -> 16#02954; entity("RightVector") -> 16#021C0; entity("RightVectorBar") -> 16#02953; entity("Rightarrow") -> 16#021D2; entity("Ropf") -> 16#0211D; entity("RoundImplies") -> 16#02970; entity("Rrightarrow") -> 16#021DB; entity("Rscr") -> 16#0211B; entity("Rsh") -> 16#021B1; entity("RuleDelayed") -> 16#029F4; entity("SHCHcy") -> 16#00429; entity("SHcy") -> 16#00428; entity("SOFTcy") -> 16#0042C; entity("Sacute") -> 16#0015A; entity("Sc") -> 16#02ABC; entity("Scaron") -> 16#00160; entity("Scedil") -> 16#0015E; entity("Scirc") -> 16#0015C; entity("Scy") -> 16#00421; entity("Sfr") -> 16#1D516; entity("ShortDownArrow") -> 16#02193; entity("ShortLeftArrow") -> 16#02190; entity("ShortRightArrow") -> 16#02192; entity("ShortUpArrow") -> 16#02191; entity("Sigma") -> 16#003A3; entity("SmallCircle") -> 16#02218; entity("Sopf") -> 16#1D54A; entity("Sqrt") -> 16#0221A; entity("Square") -> 16#025A1; entity("SquareIntersection") -> 16#02293; entity("SquareSubset") -> 16#0228F; entity("SquareSubsetEqual") -> 16#02291; entity("SquareSuperset") -> 16#02290; entity("SquareSupersetEqual") -> 16#02292; entity("SquareUnion") -> 16#02294; entity("Sscr") -> 16#1D4AE; entity("Star") -> 16#022C6; entity("Sub") -> 16#022D0; entity("Subset") -> 16#022D0; entity("SubsetEqual") -> 16#02286; entity("Succeeds") -> 16#0227B; entity("SucceedsEqual") -> 16#02AB0; entity("SucceedsSlantEqual") -> 16#0227D; entity("SucceedsTilde") -> 16#0227F; entity("SuchThat") -> 16#0220B; entity("Sum") -> 16#02211; entity("Sup") -> 16#022D1; entity("Superset") -> 16#02283; entity("SupersetEqual") -> 16#02287; entity("Supset") -> 16#022D1; entity("THORN") -> 16#000DE; entity("TRADE") -> 16#02122; entity("TSHcy") -> 16#0040B; entity("TScy") -> 16#00426; entity("Tab") -> 16#00009; entity("Tau") -> 16#003A4; entity("Tcaron") -> 16#00164; entity("Tcedil") -> 16#00162; entity("Tcy") -> 16#00422; entity("Tfr") -> 16#1D517; entity("Therefore") -> 16#02234; entity("Theta") -> 16#00398; entity("ThickSpace") -> [16#0205F, 16#0200A]; entity("ThinSpace") -> 16#02009; entity("Tilde") -> 16#0223C; entity("TildeEqual") -> 16#02243; entity("TildeFullEqual") -> 16#02245; entity("TildeTilde") -> 16#02248; entity("Topf") -> 16#1D54B; entity("TripleDot") -> 16#020DB; entity("Tscr") -> 16#1D4AF; entity("Tstrok") -> 16#00166; entity("Uacute") -> 16#000DA; entity("Uarr") -> 16#0219F; entity("Uarrocir") -> 16#02949; entity("Ubrcy") -> 16#0040E; entity("Ubreve") -> 16#0016C; entity("Ucirc") -> 16#000DB; entity("Ucy") -> 16#00423; entity("Udblac") -> 16#00170; entity("Ufr") -> 16#1D518; entity("Ugrave") -> 16#000D9; entity("Umacr") -> 16#0016A; entity("UnderBar") -> 16#0005F; entity("UnderBrace") -> 16#023DF; entity("UnderBracket") -> 16#023B5; entity("UnderParenthesis") -> 16#023DD; entity("Union") -> 16#022C3; entity("UnionPlus") -> 16#0228E; entity("Uogon") -> 16#00172; entity("Uopf") -> 16#1D54C; entity("UpArrow") -> 16#02191; entity("UpArrowBar") -> 16#02912; entity("UpArrowDownArrow") -> 16#021C5; entity("UpDownArrow") -> 16#02195; entity("UpEquilibrium") -> 16#0296E; entity("UpTee") -> 16#022A5; entity("UpTeeArrow") -> 16#021A5; entity("Uparrow") -> 16#021D1; entity("Updownarrow") -> 16#021D5; entity("UpperLeftArrow") -> 16#02196; entity("UpperRightArrow") -> 16#02197; entity("Upsi") -> 16#003D2; entity("Upsilon") -> 16#003A5; entity("Uring") -> 16#0016E; entity("Uscr") -> 16#1D4B0; entity("Utilde") -> 16#00168; entity("Uuml") -> 16#000DC; entity("VDash") -> 16#022AB; entity("Vbar") -> 16#02AEB; entity("Vcy") -> 16#00412; entity("Vdash") -> 16#022A9; entity("Vdashl") -> 16#02AE6; entity("Vee") -> 16#022C1; entity("Verbar") -> 16#02016; entity("Vert") -> 16#02016; entity("VerticalBar") -> 16#02223; entity("VerticalLine") -> 16#0007C; entity("VerticalSeparator") -> 16#02758; entity("VerticalTilde") -> 16#02240; entity("VeryThinSpace") -> 16#0200A; entity("Vfr") -> 16#1D519; entity("Vopf") -> 16#1D54D; entity("Vscr") -> 16#1D4B1; entity("Vvdash") -> 16#022AA; entity("Wcirc") -> 16#00174; entity("Wedge") -> 16#022C0; entity("Wfr") -> 16#1D51A; entity("Wopf") -> 16#1D54E; entity("Wscr") -> 16#1D4B2; entity("Xfr") -> 16#1D51B; entity("Xi") -> 16#0039E; entity("Xopf") -> 16#1D54F; entity("Xscr") -> 16#1D4B3; entity("YAcy") -> 16#0042F; entity("YIcy") -> 16#00407; entity("YUcy") -> 16#0042E; entity("Yacute") -> 16#000DD; entity("Ycirc") -> 16#00176; entity("Ycy") -> 16#0042B; entity("Yfr") -> 16#1D51C; entity("Yopf") -> 16#1D550; entity("Yscr") -> 16#1D4B4; entity("Yuml") -> 16#00178; entity("ZHcy") -> 16#00416; entity("Zacute") -> 16#00179; entity("Zcaron") -> 16#0017D; entity("Zcy") -> 16#00417; entity("Zdot") -> 16#0017B; entity("ZeroWidthSpace") -> 16#0200B; entity("Zeta") -> 16#00396; entity("Zfr") -> 16#02128; entity("Zopf") -> 16#02124; entity("Zscr") -> 16#1D4B5; entity("aacute") -> 16#000E1; entity("abreve") -> 16#00103; entity("ac") -> 16#0223E; entity("acE") -> [16#0223E, 16#00333]; entity("acd") -> 16#0223F; entity("acirc") -> 16#000E2; entity("acute") -> 16#000B4; entity("acy") -> 16#00430; entity("aelig") -> 16#000E6; entity("af") -> 16#02061; entity("afr") -> 16#1D51E; entity("agrave") -> 16#000E0; entity("alefsym") -> 16#02135; entity("aleph") -> 16#02135; entity("alpha") -> 16#003B1; entity("amacr") -> 16#00101; entity("amalg") -> 16#02A3F; entity("amp") -> 16#00026; entity("and") -> 16#02227; entity("andand") -> 16#02A55; entity("andd") -> 16#02A5C; entity("andslope") -> 16#02A58; entity("andv") -> 16#02A5A; entity("ang") -> 16#02220; entity("ange") -> 16#029A4; entity("angle") -> 16#02220; entity("angmsd") -> 16#02221; entity("angmsdaa") -> 16#029A8; entity("angmsdab") -> 16#029A9; entity("angmsdac") -> 16#029AA; entity("angmsdad") -> 16#029AB; entity("angmsdae") -> 16#029AC; entity("angmsdaf") -> 16#029AD; entity("angmsdag") -> 16#029AE; entity("angmsdah") -> 16#029AF; entity("angrt") -> 16#0221F; entity("angrtvb") -> 16#022BE; entity("angrtvbd") -> 16#0299D; entity("angsph") -> 16#02222; entity("angst") -> 16#000C5; entity("angzarr") -> 16#0237C; entity("aogon") -> 16#00105; entity("aopf") -> 16#1D552; entity("ap") -> 16#02248; entity("apE") -> 16#02A70; entity("apacir") -> 16#02A6F; entity("ape") -> 16#0224A; entity("apid") -> 16#0224B; entity("apos") -> 16#00027; entity("approx") -> 16#02248; entity("approxeq") -> 16#0224A; entity("aring") -> 16#000E5; entity("ascr") -> 16#1D4B6; entity("ast") -> 16#0002A; entity("asymp") -> 16#02248; entity("asympeq") -> 16#0224D; entity("atilde") -> 16#000E3; entity("auml") -> 16#000E4; entity("awconint") -> 16#02233; entity("awint") -> 16#02A11; entity("bNot") -> 16#02AED; entity("backcong") -> 16#0224C; entity("backepsilon") -> 16#003F6; entity("backprime") -> 16#02035; entity("backsim") -> 16#0223D; entity("backsimeq") -> 16#022CD; entity("barvee") -> 16#022BD; entity("barwed") -> 16#02305; entity("barwedge") -> 16#02305; entity("bbrk") -> 16#023B5; entity("bbrktbrk") -> 16#023B6; entity("bcong") -> 16#0224C; entity("bcy") -> 16#00431; entity("bdquo") -> 16#0201E; entity("becaus") -> 16#02235; entity("because") -> 16#02235; entity("bemptyv") -> 16#029B0; entity("bepsi") -> 16#003F6; entity("bernou") -> 16#0212C; entity("beta") -> 16#003B2; entity("beth") -> 16#02136; entity("between") -> 16#0226C; entity("bfr") -> 16#1D51F; entity("bigcap") -> 16#022C2; entity("bigcirc") -> 16#025EF; entity("bigcup") -> 16#022C3; entity("bigodot") -> 16#02A00; entity("bigoplus") -> 16#02A01; entity("bigotimes") -> 16#02A02; entity("bigsqcup") -> 16#02A06; entity("bigstar") -> 16#02605; entity("bigtriangledown") -> 16#025BD; entity("bigtriangleup") -> 16#025B3; entity("biguplus") -> 16#02A04; entity("bigvee") -> 16#022C1; entity("bigwedge") -> 16#022C0; entity("bkarow") -> 16#0290D; entity("blacklozenge") -> 16#029EB; entity("blacksquare") -> 16#025AA; entity("blacktriangle") -> 16#025B4; entity("blacktriangledown") -> 16#025BE; entity("blacktriangleleft") -> 16#025C2; entity("blacktriangleright") -> 16#025B8; entity("blank") -> 16#02423; entity("blk12") -> 16#02592; entity("blk14") -> 16#02591; entity("blk34") -> 16#02593; entity("block") -> 16#02588; entity("bne") -> [16#0003D, 16#020E5]; entity("bnequiv") -> [16#02261, 16#020E5]; entity("bnot") -> 16#02310; entity("bopf") -> 16#1D553; entity("bot") -> 16#022A5; entity("bottom") -> 16#022A5; entity("bowtie") -> 16#022C8; entity("boxDL") -> 16#02557; entity("boxDR") -> 16#02554; entity("boxDl") -> 16#02556; entity("boxDr") -> 16#02553; entity("boxH") -> 16#02550; entity("boxHD") -> 16#02566; entity("boxHU") -> 16#02569; entity("boxHd") -> 16#02564; entity("boxHu") -> 16#02567; entity("boxUL") -> 16#0255D; entity("boxUR") -> 16#0255A; entity("boxUl") -> 16#0255C; entity("boxUr") -> 16#02559; entity("boxV") -> 16#02551; entity("boxVH") -> 16#0256C; entity("boxVL") -> 16#02563; entity("boxVR") -> 16#02560; entity("boxVh") -> 16#0256B; entity("boxVl") -> 16#02562; entity("boxVr") -> 16#0255F; entity("boxbox") -> 16#029C9; entity("boxdL") -> 16#02555; entity("boxdR") -> 16#02552; entity("boxdl") -> 16#02510; entity("boxdr") -> 16#0250C; entity("boxh") -> 16#02500; entity("boxhD") -> 16#02565; entity("boxhU") -> 16#02568; entity("boxhd") -> 16#0252C; entity("boxhu") -> 16#02534; entity("boxminus") -> 16#0229F; entity("boxplus") -> 16#0229E; entity("boxtimes") -> 16#022A0; entity("boxuL") -> 16#0255B; entity("boxuR") -> 16#02558; entity("boxul") -> 16#02518; entity("boxur") -> 16#02514; entity("boxv") -> 16#02502; entity("boxvH") -> 16#0256A; entity("boxvL") -> 16#02561; entity("boxvR") -> 16#0255E; entity("boxvh") -> 16#0253C; entity("boxvl") -> 16#02524; entity("boxvr") -> 16#0251C; entity("bprime") -> 16#02035; entity("breve") -> 16#002D8; entity("brvbar") -> 16#000A6; entity("bscr") -> 16#1D4B7; entity("bsemi") -> 16#0204F; entity("bsim") -> 16#0223D; entity("bsime") -> 16#022CD; entity("bsol") -> 16#0005C; entity("bsolb") -> 16#029C5; entity("bsolhsub") -> 16#027C8; entity("bull") -> 16#02022; entity("bullet") -> 16#02022; entity("bump") -> 16#0224E; entity("bumpE") -> 16#02AAE; entity("bumpe") -> 16#0224F; entity("bumpeq") -> 16#0224F; entity("cacute") -> 16#00107; entity("cap") -> 16#02229; entity("capand") -> 16#02A44; entity("capbrcup") -> 16#02A49; entity("capcap") -> 16#02A4B; entity("capcup") -> 16#02A47; entity("capdot") -> 16#02A40; entity("caps") -> [16#02229, 16#0FE00]; entity("caret") -> 16#02041; entity("caron") -> 16#002C7; entity("ccaps") -> 16#02A4D; entity("ccaron") -> 16#0010D; entity("ccedil") -> 16#000E7; entity("ccirc") -> 16#00109; entity("ccups") -> 16#02A4C; entity("ccupssm") -> 16#02A50; entity("cdot") -> 16#0010B; entity("cedil") -> 16#000B8; entity("cemptyv") -> 16#029B2; entity("cent") -> 16#000A2; entity("centerdot") -> 16#000B7; entity("cfr") -> 16#1D520; entity("chcy") -> 16#00447; entity("check") -> 16#02713; entity("checkmark") -> 16#02713; entity("chi") -> 16#003C7; entity("cir") -> 16#025CB; entity("cirE") -> 16#029C3; entity("circ") -> 16#002C6; entity("circeq") -> 16#02257; entity("circlearrowleft") -> 16#021BA; entity("circlearrowright") -> 16#021BB; entity("circledR") -> 16#000AE; entity("circledS") -> 16#024C8; entity("circledast") -> 16#0229B; entity("circledcirc") -> 16#0229A; entity("circleddash") -> 16#0229D; entity("cire") -> 16#02257; entity("cirfnint") -> 16#02A10; entity("cirmid") -> 16#02AEF; entity("cirscir") -> 16#029C2; entity("clubs") -> 16#02663; entity("clubsuit") -> 16#02663; entity("colon") -> 16#0003A; entity("colone") -> 16#02254; entity("coloneq") -> 16#02254; entity("comma") -> 16#0002C; entity("commat") -> 16#00040; entity("comp") -> 16#02201; entity("compfn") -> 16#02218; entity("complement") -> 16#02201; entity("complexes") -> 16#02102; entity("cong") -> 16#02245; entity("congdot") -> 16#02A6D; entity("conint") -> 16#0222E; entity("copf") -> 16#1D554; entity("coprod") -> 16#02210; entity("copy") -> 16#000A9; entity("copysr") -> 16#02117; entity("crarr") -> 16#021B5; entity("cross") -> 16#02717; entity("cscr") -> 16#1D4B8; entity("csub") -> 16#02ACF; entity("csube") -> 16#02AD1; entity("csup") -> 16#02AD0; entity("csupe") -> 16#02AD2; entity("ctdot") -> 16#022EF; entity("cudarrl") -> 16#02938; entity("cudarrr") -> 16#02935; entity("cuepr") -> 16#022DE; entity("cuesc") -> 16#022DF; entity("cularr") -> 16#021B6; entity("cularrp") -> 16#0293D; entity("cup") -> 16#0222A; entity("cupbrcap") -> 16#02A48; entity("cupcap") -> 16#02A46; entity("cupcup") -> 16#02A4A; entity("cupdot") -> 16#0228D; entity("cupor") -> 16#02A45; entity("cups") -> [16#0222A, 16#0FE00]; entity("curarr") -> 16#021B7; entity("curarrm") -> 16#0293C; entity("curlyeqprec") -> 16#022DE; entity("curlyeqsucc") -> 16#022DF; entity("curlyvee") -> 16#022CE; entity("curlywedge") -> 16#022CF; entity("curren") -> 16#000A4; entity("curvearrowleft") -> 16#021B6; entity("curvearrowright") -> 16#021B7; entity("cuvee") -> 16#022CE; entity("cuwed") -> 16#022CF; entity("cwconint") -> 16#02232; entity("cwint") -> 16#02231; entity("cylcty") -> 16#0232D; entity("dArr") -> 16#021D3; entity("dHar") -> 16#02965; entity("dagger") -> 16#02020; entity("daleth") -> 16#02138; entity("darr") -> 16#02193; entity("dash") -> 16#02010; entity("dashv") -> 16#022A3; entity("dbkarow") -> 16#0290F; entity("dblac") -> 16#002DD; entity("dcaron") -> 16#0010F; entity("dcy") -> 16#00434; entity("dd") -> 16#02146; entity("ddagger") -> 16#02021; entity("ddarr") -> 16#021CA; entity("ddotseq") -> 16#02A77; entity("deg") -> 16#000B0; entity("delta") -> 16#003B4; entity("demptyv") -> 16#029B1; entity("dfisht") -> 16#0297F; entity("dfr") -> 16#1D521; entity("dharl") -> 16#021C3; entity("dharr") -> 16#021C2; entity("diam") -> 16#022C4; entity("diamond") -> 16#022C4; entity("diamondsuit") -> 16#02666; entity("diams") -> 16#02666; entity("die") -> 16#000A8; entity("digamma") -> 16#003DD; entity("disin") -> 16#022F2; entity("div") -> 16#000F7; entity("divide") -> 16#000F7; entity("divideontimes") -> 16#022C7; entity("divonx") -> 16#022C7; entity("djcy") -> 16#00452; entity("dlcorn") -> 16#0231E; entity("dlcrop") -> 16#0230D; entity("dollar") -> 16#00024; entity("dopf") -> 16#1D555; entity("dot") -> 16#002D9; entity("doteq") -> 16#02250; entity("doteqdot") -> 16#02251; entity("dotminus") -> 16#02238; entity("dotplus") -> 16#02214; entity("dotsquare") -> 16#022A1; entity("doublebarwedge") -> 16#02306; entity("downarrow") -> 16#02193; entity("downdownarrows") -> 16#021CA; entity("downharpoonleft") -> 16#021C3; entity("downharpoonright") -> 16#021C2; entity("drbkarow") -> 16#02910; entity("drcorn") -> 16#0231F; entity("drcrop") -> 16#0230C; entity("dscr") -> 16#1D4B9; entity("dscy") -> 16#00455; entity("dsol") -> 16#029F6; entity("dstrok") -> 16#00111; entity("dtdot") -> 16#022F1; entity("dtri") -> 16#025BF; entity("dtrif") -> 16#025BE; entity("duarr") -> 16#021F5; entity("duhar") -> 16#0296F; entity("dwangle") -> 16#029A6; entity("dzcy") -> 16#0045F; entity("dzigrarr") -> 16#027FF; entity("eDDot") -> 16#02A77; entity("eDot") -> 16#02251; entity("eacute") -> 16#000E9; entity("easter") -> 16#02A6E; entity("ecaron") -> 16#0011B; entity("ecir") -> 16#02256; entity("ecirc") -> 16#000EA; entity("ecolon") -> 16#02255; entity("ecy") -> 16#0044D; entity("edot") -> 16#00117; entity("ee") -> 16#02147; entity("efDot") -> 16#02252; entity("efr") -> 16#1D522; entity("eg") -> 16#02A9A; entity("egrave") -> 16#000E8; entity("egs") -> 16#02A96; entity("egsdot") -> 16#02A98; entity("el") -> 16#02A99; entity("elinters") -> 16#023E7; entity("ell") -> 16#02113; entity("els") -> 16#02A95; entity("elsdot") -> 16#02A97; entity("emacr") -> 16#00113; entity("empty") -> 16#02205; entity("emptyset") -> 16#02205; entity("emptyv") -> 16#02205; entity("emsp") -> 16#02003; entity("emsp13") -> 16#02004; entity("emsp14") -> 16#02005; entity("eng") -> 16#0014B; entity("ensp") -> 16#02002; entity("eogon") -> 16#00119; entity("eopf") -> 16#1D556; entity("epar") -> 16#022D5; entity("eparsl") -> 16#029E3; entity("eplus") -> 16#02A71; entity("epsi") -> 16#003B5; entity("epsilon") -> 16#003B5; entity("epsiv") -> 16#003F5; entity("eqcirc") -> 16#02256; entity("eqcolon") -> 16#02255; entity("eqsim") -> 16#02242; entity("eqslantgtr") -> 16#02A96; entity("eqslantless") -> 16#02A95; entity("equals") -> 16#0003D; entity("equest") -> 16#0225F; entity("equiv") -> 16#02261; entity("equivDD") -> 16#02A78; entity("eqvparsl") -> 16#029E5; entity("erDot") -> 16#02253; entity("erarr") -> 16#02971; entity("escr") -> 16#0212F; entity("esdot") -> 16#02250; entity("esim") -> 16#02242; entity("eta") -> 16#003B7; entity("eth") -> 16#000F0; entity("euml") -> 16#000EB; entity("euro") -> 16#020AC; entity("excl") -> 16#00021; entity("exist") -> 16#02203; entity("expectation") -> 16#02130; entity("exponentiale") -> 16#02147; entity("fallingdotseq") -> 16#02252; entity("fcy") -> 16#00444; entity("female") -> 16#02640; entity("ffilig") -> 16#0FB03; entity("fflig") -> 16#0FB00; entity("ffllig") -> 16#0FB04; entity("ffr") -> 16#1D523; entity("filig") -> 16#0FB01; entity("fjlig") -> [16#00066, 16#0006A]; entity("flat") -> 16#0266D; entity("fllig") -> 16#0FB02; entity("fltns") -> 16#025B1; entity("fnof") -> 16#00192; entity("fopf") -> 16#1D557; entity("forall") -> 16#02200; entity("fork") -> 16#022D4; entity("forkv") -> 16#02AD9; entity("fpartint") -> 16#02A0D; entity("frac12") -> 16#000BD; entity("frac13") -> 16#02153; entity("frac14") -> 16#000BC; entity("frac15") -> 16#02155; entity("frac16") -> 16#02159; entity("frac18") -> 16#0215B; entity("frac23") -> 16#02154; entity("frac25") -> 16#02156; entity("frac34") -> 16#000BE; entity("frac35") -> 16#02157; entity("frac38") -> 16#0215C; entity("frac45") -> 16#02158; entity("frac56") -> 16#0215A; entity("frac58") -> 16#0215D; entity("frac78") -> 16#0215E; entity("frasl") -> 16#02044; entity("frown") -> 16#02322; entity("fscr") -> 16#1D4BB; entity("gE") -> 16#02267; entity("gEl") -> 16#02A8C; entity("gacute") -> 16#001F5; entity("gamma") -> 16#003B3; entity("gammad") -> 16#003DD; entity("gap") -> 16#02A86; entity("gbreve") -> 16#0011F; entity("gcirc") -> 16#0011D; entity("gcy") -> 16#00433; entity("gdot") -> 16#00121; entity("ge") -> 16#02265; entity("gel") -> 16#022DB; entity("geq") -> 16#02265; entity("geqq") -> 16#02267; entity("geqslant") -> 16#02A7E; entity("ges") -> 16#02A7E; entity("gescc") -> 16#02AA9; entity("gesdot") -> 16#02A80; entity("gesdoto") -> 16#02A82; entity("gesdotol") -> 16#02A84; entity("gesl") -> [16#022DB, 16#0FE00]; entity("gesles") -> 16#02A94; entity("gfr") -> 16#1D524; entity("gg") -> 16#0226B; entity("ggg") -> 16#022D9; entity("gimel") -> 16#02137; entity("gjcy") -> 16#00453; entity("gl") -> 16#02277; entity("glE") -> 16#02A92; entity("gla") -> 16#02AA5; entity("glj") -> 16#02AA4; entity("gnE") -> 16#02269; entity("gnap") -> 16#02A8A; entity("gnapprox") -> 16#02A8A; entity("gne") -> 16#02A88; entity("gneq") -> 16#02A88; entity("gneqq") -> 16#02269; entity("gnsim") -> 16#022E7; entity("gopf") -> 16#1D558; entity("grave") -> 16#00060; entity("gscr") -> 16#0210A; entity("gsim") -> 16#02273; entity("gsime") -> 16#02A8E; entity("gsiml") -> 16#02A90; entity("gt") -> 16#0003E; entity("gtcc") -> 16#02AA7; entity("gtcir") -> 16#02A7A; entity("gtdot") -> 16#022D7; entity("gtlPar") -> 16#02995; entity("gtquest") -> 16#02A7C; entity("gtrapprox") -> 16#02A86; entity("gtrarr") -> 16#02978; entity("gtrdot") -> 16#022D7; entity("gtreqless") -> 16#022DB; entity("gtreqqless") -> 16#02A8C; entity("gtrless") -> 16#02277; entity("gtrsim") -> 16#02273; entity("gvertneqq") -> [16#02269, 16#0FE00]; entity("gvnE") -> [16#02269, 16#0FE00]; entity("hArr") -> 16#021D4; entity("hairsp") -> 16#0200A; entity("half") -> 16#000BD; entity("hamilt") -> 16#0210B; entity("hardcy") -> 16#0044A; entity("harr") -> 16#02194; entity("harrcir") -> 16#02948; entity("harrw") -> 16#021AD; entity("hbar") -> 16#0210F; entity("hcirc") -> 16#00125; entity("hearts") -> 16#02665; entity("heartsuit") -> 16#02665; entity("hellip") -> 16#02026; entity("hercon") -> 16#022B9; entity("hfr") -> 16#1D525; entity("hksearow") -> 16#02925; entity("hkswarow") -> 16#02926; entity("hoarr") -> 16#021FF; entity("homtht") -> 16#0223B; entity("hookleftarrow") -> 16#021A9; entity("hookrightarrow") -> 16#021AA; entity("hopf") -> 16#1D559; entity("horbar") -> 16#02015; entity("hscr") -> 16#1D4BD; entity("hslash") -> 16#0210F; entity("hstrok") -> 16#00127; entity("hybull") -> 16#02043; entity("hyphen") -> 16#02010; entity("iacute") -> 16#000ED; entity("ic") -> 16#02063; entity("icirc") -> 16#000EE; entity("icy") -> 16#00438; entity("iecy") -> 16#00435; entity("iexcl") -> 16#000A1; entity("iff") -> 16#021D4; entity("ifr") -> 16#1D526; entity("igrave") -> 16#000EC; entity("ii") -> 16#02148; entity("iiiint") -> 16#02A0C; entity("iiint") -> 16#0222D; entity("iinfin") -> 16#029DC; entity("iiota") -> 16#02129; entity("ijlig") -> 16#00133; entity("imacr") -> 16#0012B; entity("image") -> 16#02111; entity("imagline") -> 16#02110; entity("imagpart") -> 16#02111; entity("imath") -> 16#00131; entity("imof") -> 16#022B7; entity("imped") -> 16#001B5; entity("in") -> 16#02208; entity("incare") -> 16#02105; entity("infin") -> 16#0221E; entity("infintie") -> 16#029DD; entity("inodot") -> 16#00131; entity("int") -> 16#0222B; entity("intcal") -> 16#022BA; entity("integers") -> 16#02124; entity("intercal") -> 16#022BA; entity("intlarhk") -> 16#02A17; entity("intprod") -> 16#02A3C; entity("iocy") -> 16#00451; entity("iogon") -> 16#0012F; entity("iopf") -> 16#1D55A; entity("iota") -> 16#003B9; entity("iprod") -> 16#02A3C; entity("iquest") -> 16#000BF; entity("iscr") -> 16#1D4BE; entity("isin") -> 16#02208; entity("isinE") -> 16#022F9; entity("isindot") -> 16#022F5; entity("isins") -> 16#022F4; entity("isinsv") -> 16#022F3; entity("isinv") -> 16#02208; entity("it") -> 16#02062; entity("itilde") -> 16#00129; entity("iukcy") -> 16#00456; entity("iuml") -> 16#000EF; entity("jcirc") -> 16#00135; entity("jcy") -> 16#00439; entity("jfr") -> 16#1D527; entity("jmath") -> 16#00237; entity("jopf") -> 16#1D55B; entity("jscr") -> 16#1D4BF; entity("jsercy") -> 16#00458; entity("jukcy") -> 16#00454; entity("kappa") -> 16#003BA; entity("kappav") -> 16#003F0; entity("kcedil") -> 16#00137; entity("kcy") -> 16#0043A; entity("kfr") -> 16#1D528; entity("kgreen") -> 16#00138; entity("khcy") -> 16#00445; entity("kjcy") -> 16#0045C; entity("kopf") -> 16#1D55C; entity("kscr") -> 16#1D4C0; entity("lAarr") -> 16#021DA; entity("lArr") -> 16#021D0; entity("lAtail") -> 16#0291B; entity("lBarr") -> 16#0290E; entity("lE") -> 16#02266; entity("lEg") -> 16#02A8B; entity("lHar") -> 16#02962; entity("lacute") -> 16#0013A; entity("laemptyv") -> 16#029B4; entity("lagran") -> 16#02112; entity("lambda") -> 16#003BB; entity("lang") -> 16#027E8; entity("langd") -> 16#02991; entity("langle") -> 16#027E8; entity("lap") -> 16#02A85; entity("laquo") -> 16#000AB; entity("larr") -> 16#02190; entity("larrb") -> 16#021E4; entity("larrbfs") -> 16#0291F; entity("larrfs") -> 16#0291D; entity("larrhk") -> 16#021A9; entity("larrlp") -> 16#021AB; entity("larrpl") -> 16#02939; entity("larrsim") -> 16#02973; entity("larrtl") -> 16#021A2; entity("lat") -> 16#02AAB; entity("latail") -> 16#02919; entity("late") -> 16#02AAD; entity("lates") -> [16#02AAD, 16#0FE00]; entity("lbarr") -> 16#0290C; entity("lbbrk") -> 16#02772; entity("lbrace") -> 16#0007B; entity("lbrack") -> 16#0005B; entity("lbrke") -> 16#0298B; entity("lbrksld") -> 16#0298F; entity("lbrkslu") -> 16#0298D; entity("lcaron") -> 16#0013E; entity("lcedil") -> 16#0013C; entity("lceil") -> 16#02308; entity("lcub") -> 16#0007B; entity("lcy") -> 16#0043B; entity("ldca") -> 16#02936; entity("ldquo") -> 16#0201C; entity("ldquor") -> 16#0201E; entity("ldrdhar") -> 16#02967; entity("ldrushar") -> 16#0294B; entity("ldsh") -> 16#021B2; entity("le") -> 16#02264; entity("leftarrow") -> 16#02190; entity("leftarrowtail") -> 16#021A2; entity("leftharpoondown") -> 16#021BD; entity("leftharpoonup") -> 16#021BC; entity("leftleftarrows") -> 16#021C7; entity("leftrightarrow") -> 16#02194; entity("leftrightarrows") -> 16#021C6; entity("leftrightharpoons") -> 16#021CB; entity("leftrightsquigarrow") -> 16#021AD; entity("leftthreetimes") -> 16#022CB; entity("leg") -> 16#022DA; entity("leq") -> 16#02264; entity("leqq") -> 16#02266; entity("leqslant") -> 16#02A7D; entity("les") -> 16#02A7D; entity("lescc") -> 16#02AA8; entity("lesdot") -> 16#02A7F; entity("lesdoto") -> 16#02A81; entity("lesdotor") -> 16#02A83; entity("lesg") -> [16#022DA, 16#0FE00]; entity("lesges") -> 16#02A93; entity("lessapprox") -> 16#02A85; entity("lessdot") -> 16#022D6; entity("lesseqgtr") -> 16#022DA; entity("lesseqqgtr") -> 16#02A8B; entity("lessgtr") -> 16#02276; entity("lesssim") -> 16#02272; entity("lfisht") -> 16#0297C; entity("lfloor") -> 16#0230A; entity("lfr") -> 16#1D529; entity("lg") -> 16#02276; entity("lgE") -> 16#02A91; entity("lhard") -> 16#021BD; entity("lharu") -> 16#021BC; entity("lharul") -> 16#0296A; entity("lhblk") -> 16#02584; entity("ljcy") -> 16#00459; entity("ll") -> 16#0226A; entity("llarr") -> 16#021C7; entity("llcorner") -> 16#0231E; entity("llhard") -> 16#0296B; entity("lltri") -> 16#025FA; entity("lmidot") -> 16#00140; entity("lmoust") -> 16#023B0; entity("lmoustache") -> 16#023B0; entity("lnE") -> 16#02268; entity("lnap") -> 16#02A89; entity("lnapprox") -> 16#02A89; entity("lne") -> 16#02A87; entity("lneq") -> 16#02A87; entity("lneqq") -> 16#02268; entity("lnsim") -> 16#022E6; entity("loang") -> 16#027EC; entity("loarr") -> 16#021FD; entity("lobrk") -> 16#027E6; entity("longleftarrow") -> 16#027F5; entity("longleftrightarrow") -> 16#027F7; entity("longmapsto") -> 16#027FC; entity("longrightarrow") -> 16#027F6; entity("looparrowleft") -> 16#021AB; entity("looparrowright") -> 16#021AC; entity("lopar") -> 16#02985; entity("lopf") -> 16#1D55D; entity("loplus") -> 16#02A2D; entity("lotimes") -> 16#02A34; entity("lowast") -> 16#02217; entity("lowbar") -> 16#0005F; entity("loz") -> 16#025CA; entity("lozenge") -> 16#025CA; entity("lozf") -> 16#029EB; entity("lpar") -> 16#00028; entity("lparlt") -> 16#02993; entity("lrarr") -> 16#021C6; entity("lrcorner") -> 16#0231F; entity("lrhar") -> 16#021CB; entity("lrhard") -> 16#0296D; entity("lrm") -> 16#0200E; entity("lrtri") -> 16#022BF; entity("lsaquo") -> 16#02039; entity("lscr") -> 16#1D4C1; entity("lsh") -> 16#021B0; entity("lsim") -> 16#02272; entity("lsime") -> 16#02A8D; entity("lsimg") -> 16#02A8F; entity("lsqb") -> 16#0005B; entity("lsquo") -> 16#02018; entity("lsquor") -> 16#0201A; entity("lstrok") -> 16#00142; entity("lt") -> 16#0003C; entity("ltcc") -> 16#02AA6; entity("ltcir") -> 16#02A79; entity("ltdot") -> 16#022D6; entity("lthree") -> 16#022CB; entity("ltimes") -> 16#022C9; entity("ltlarr") -> 16#02976; entity("ltquest") -> 16#02A7B; entity("ltrPar") -> 16#02996; entity("ltri") -> 16#025C3; entity("ltrie") -> 16#022B4; entity("ltrif") -> 16#025C2; entity("lurdshar") -> 16#0294A; entity("luruhar") -> 16#02966; entity("lvertneqq") -> [16#02268, 16#0FE00]; entity("lvnE") -> [16#02268, 16#0FE00]; entity("mDDot") -> 16#0223A; entity("macr") -> 16#000AF; entity("male") -> 16#02642; entity("malt") -> 16#02720; entity("maltese") -> 16#02720; entity("map") -> 16#021A6; entity("mapsto") -> 16#021A6; entity("mapstodown") -> 16#021A7; entity("mapstoleft") -> 16#021A4; entity("mapstoup") -> 16#021A5; entity("marker") -> 16#025AE; entity("mcomma") -> 16#02A29; entity("mcy") -> 16#0043C; entity("mdash") -> 16#02014; entity("measuredangle") -> 16#02221; entity("mfr") -> 16#1D52A; entity("mho") -> 16#02127; entity("micro") -> 16#000B5; entity("mid") -> 16#02223; entity("midast") -> 16#0002A; entity("midcir") -> 16#02AF0; entity("middot") -> 16#000B7; entity("minus") -> 16#02212; entity("minusb") -> 16#0229F; entity("minusd") -> 16#02238; entity("minusdu") -> 16#02A2A; entity("mlcp") -> 16#02ADB; entity("mldr") -> 16#02026; entity("mnplus") -> 16#02213; entity("models") -> 16#022A7; entity("mopf") -> 16#1D55E; entity("mp") -> 16#02213; entity("mscr") -> 16#1D4C2; entity("mstpos") -> 16#0223E; entity("mu") -> 16#003BC; entity("multimap") -> 16#022B8; entity("mumap") -> 16#022B8; entity("nGg") -> [16#022D9, 16#00338]; entity("nGt") -> [16#0226B, 16#020D2]; entity("nGtv") -> [16#0226B, 16#00338]; entity("nLeftarrow") -> 16#021CD; entity("nLeftrightarrow") -> 16#021CE; entity("nLl") -> [16#022D8, 16#00338]; entity("nLt") -> [16#0226A, 16#020D2]; entity("nLtv") -> [16#0226A, 16#00338]; entity("nRightarrow") -> 16#021CF; entity("nVDash") -> 16#022AF; entity("nVdash") -> 16#022AE; entity("nabla") -> 16#02207; entity("nacute") -> 16#00144; entity("nang") -> [16#02220, 16#020D2]; entity("nap") -> 16#02249; entity("napE") -> [16#02A70, 16#00338]; entity("napid") -> [16#0224B, 16#00338]; entity("napos") -> 16#00149; entity("napprox") -> 16#02249; entity("natur") -> 16#0266E; entity("natural") -> 16#0266E; entity("naturals") -> 16#02115; entity("nbsp") -> 16#000A0; entity("nbump") -> [16#0224E, 16#00338]; entity("nbumpe") -> [16#0224F, 16#00338]; entity("ncap") -> 16#02A43; entity("ncaron") -> 16#00148; entity("ncedil") -> 16#00146; entity("ncong") -> 16#02247; entity("ncongdot") -> [16#02A6D, 16#00338]; entity("ncup") -> 16#02A42; entity("ncy") -> 16#0043D; entity("ndash") -> 16#02013; entity("ne") -> 16#02260; entity("neArr") -> 16#021D7; entity("nearhk") -> 16#02924; entity("nearr") -> 16#02197; entity("nearrow") -> 16#02197; entity("nedot") -> [16#02250, 16#00338]; entity("nequiv") -> 16#02262; entity("nesear") -> 16#02928; entity("nesim") -> [16#02242, 16#00338]; entity("nexist") -> 16#02204; entity("nexists") -> 16#02204; entity("nfr") -> 16#1D52B; entity("ngE") -> [16#02267, 16#00338]; entity("nge") -> 16#02271; entity("ngeq") -> 16#02271; entity("ngeqq") -> [16#02267, 16#00338]; entity("ngeqslant") -> [16#02A7E, 16#00338]; entity("nges") -> [16#02A7E, 16#00338]; entity("ngsim") -> 16#02275; entity("ngt") -> 16#0226F; entity("ngtr") -> 16#0226F; entity("nhArr") -> 16#021CE; entity("nharr") -> 16#021AE; entity("nhpar") -> 16#02AF2; entity("ni") -> 16#0220B; entity("nis") -> 16#022FC; entity("nisd") -> 16#022FA; entity("niv") -> 16#0220B; entity("njcy") -> 16#0045A; entity("nlArr") -> 16#021CD; entity("nlE") -> [16#02266, 16#00338]; entity("nlarr") -> 16#0219A; entity("nldr") -> 16#02025; entity("nle") -> 16#02270; entity("nleftarrow") -> 16#0219A; entity("nleftrightarrow") -> 16#021AE; entity("nleq") -> 16#02270; entity("nleqq") -> [16#02266, 16#00338]; entity("nleqslant") -> [16#02A7D, 16#00338]; entity("nles") -> [16#02A7D, 16#00338]; entity("nless") -> 16#0226E; entity("nlsim") -> 16#02274; entity("nlt") -> 16#0226E; entity("nltri") -> 16#022EA; entity("nltrie") -> 16#022EC; entity("nmid") -> 16#02224; entity("nopf") -> 16#1D55F; entity("not") -> 16#000AC; entity("notin") -> 16#02209; entity("notinE") -> [16#022F9, 16#00338]; entity("notindot") -> [16#022F5, 16#00338]; entity("notinva") -> 16#02209; entity("notinvb") -> 16#022F7; entity("notinvc") -> 16#022F6; entity("notni") -> 16#0220C; entity("notniva") -> 16#0220C; entity("notnivb") -> 16#022FE; entity("notnivc") -> 16#022FD; entity("npar") -> 16#02226; entity("nparallel") -> 16#02226; entity("nparsl") -> [16#02AFD, 16#020E5]; entity("npart") -> [16#02202, 16#00338]; entity("npolint") -> 16#02A14; entity("npr") -> 16#02280; entity("nprcue") -> 16#022E0; entity("npre") -> [16#02AAF, 16#00338]; entity("nprec") -> 16#02280; entity("npreceq") -> [16#02AAF, 16#00338]; entity("nrArr") -> 16#021CF; entity("nrarr") -> 16#0219B; entity("nrarrc") -> [16#02933, 16#00338]; entity("nrarrw") -> [16#0219D, 16#00338]; entity("nrightarrow") -> 16#0219B; entity("nrtri") -> 16#022EB; entity("nrtrie") -> 16#022ED; entity("nsc") -> 16#02281; entity("nsccue") -> 16#022E1; entity("nsce") -> [16#02AB0, 16#00338]; entity("nscr") -> 16#1D4C3; entity("nshortmid") -> 16#02224; entity("nshortparallel") -> 16#02226; entity("nsim") -> 16#02241; entity("nsime") -> 16#02244; entity("nsimeq") -> 16#02244; entity("nsmid") -> 16#02224; entity("nspar") -> 16#02226; entity("nsqsube") -> 16#022E2; entity("nsqsupe") -> 16#022E3; entity("nsub") -> 16#02284; entity("nsubE") -> [16#02AC5, 16#00338]; entity("nsube") -> 16#02288; entity("nsubset") -> [16#02282, 16#020D2]; entity("nsubseteq") -> 16#02288; entity("nsubseteqq") -> [16#02AC5, 16#00338]; entity("nsucc") -> 16#02281; entity("nsucceq") -> [16#02AB0, 16#00338]; entity("nsup") -> 16#02285; entity("nsupE") -> [16#02AC6, 16#00338]; entity("nsupe") -> 16#02289; entity("nsupset") -> [16#02283, 16#020D2]; entity("nsupseteq") -> 16#02289; entity("nsupseteqq") -> [16#02AC6, 16#00338]; entity("ntgl") -> 16#02279; entity("ntilde") -> 16#000F1; entity("ntlg") -> 16#02278; entity("ntriangleleft") -> 16#022EA; entity("ntrianglelefteq") -> 16#022EC; entity("ntriangleright") -> 16#022EB; entity("ntrianglerighteq") -> 16#022ED; entity("nu") -> 16#003BD; entity("num") -> 16#00023; entity("numero") -> 16#02116; entity("numsp") -> 16#02007; entity("nvDash") -> 16#022AD; entity("nvHarr") -> 16#02904; entity("nvap") -> [16#0224D, 16#020D2]; entity("nvdash") -> 16#022AC; entity("nvge") -> [16#02265, 16#020D2]; entity("nvgt") -> [16#0003E, 16#020D2]; entity("nvinfin") -> 16#029DE; entity("nvlArr") -> 16#02902; entity("nvle") -> [16#02264, 16#020D2]; entity("nvlt") -> [16#0003C, 16#020D2]; entity("nvltrie") -> [16#022B4, 16#020D2]; entity("nvrArr") -> 16#02903; entity("nvrtrie") -> [16#022B5, 16#020D2]; entity("nvsim") -> [16#0223C, 16#020D2]; entity("nwArr") -> 16#021D6; entity("nwarhk") -> 16#02923; entity("nwarr") -> 16#02196; entity("nwarrow") -> 16#02196; entity("nwnear") -> 16#02927; entity("oS") -> 16#024C8; entity("oacute") -> 16#000F3; entity("oast") -> 16#0229B; entity("ocir") -> 16#0229A; entity("ocirc") -> 16#000F4; entity("ocy") -> 16#0043E; entity("odash") -> 16#0229D; entity("odblac") -> 16#00151; entity("odiv") -> 16#02A38; entity("odot") -> 16#02299; entity("odsold") -> 16#029BC; entity("oelig") -> 16#00153; entity("ofcir") -> 16#029BF; entity("ofr") -> 16#1D52C; entity("ogon") -> 16#002DB; entity("ograve") -> 16#000F2; entity("ogt") -> 16#029C1; entity("ohbar") -> 16#029B5; entity("ohm") -> 16#003A9; entity("oint") -> 16#0222E; entity("olarr") -> 16#021BA; entity("olcir") -> 16#029BE; entity("olcross") -> 16#029BB; entity("oline") -> 16#0203E; entity("olt") -> 16#029C0; entity("omacr") -> 16#0014D; entity("omega") -> 16#003C9; entity("omicron") -> 16#003BF; entity("omid") -> 16#029B6; entity("ominus") -> 16#02296; entity("oopf") -> 16#1D560; entity("opar") -> 16#029B7; entity("operp") -> 16#029B9; entity("oplus") -> 16#02295; entity("or") -> 16#02228; entity("orarr") -> 16#021BB; entity("ord") -> 16#02A5D; entity("order") -> 16#02134; entity("orderof") -> 16#02134; entity("ordf") -> 16#000AA; entity("ordm") -> 16#000BA; entity("origof") -> 16#022B6; entity("oror") -> 16#02A56; entity("orslope") -> 16#02A57; entity("orv") -> 16#02A5B; entity("oscr") -> 16#02134; entity("oslash") -> 16#000F8; entity("osol") -> 16#02298; entity("otilde") -> 16#000F5; entity("otimes") -> 16#02297; entity("otimesas") -> 16#02A36; entity("ouml") -> 16#000F6; entity("ovbar") -> 16#0233D; entity("par") -> 16#02225; entity("para") -> 16#000B6; entity("parallel") -> 16#02225; entity("parsim") -> 16#02AF3; entity("parsl") -> 16#02AFD; entity("part") -> 16#02202; entity("pcy") -> 16#0043F; entity("percnt") -> 16#00025; entity("period") -> 16#0002E; entity("permil") -> 16#02030; entity("perp") -> 16#022A5; entity("pertenk") -> 16#02031; entity("pfr") -> 16#1D52D; entity("phi") -> 16#003C6; entity("phiv") -> 16#003D5; entity("phmmat") -> 16#02133; entity("phone") -> 16#0260E; entity("pi") -> 16#003C0; entity("pitchfork") -> 16#022D4; entity("piv") -> 16#003D6; entity("planck") -> 16#0210F; entity("planckh") -> 16#0210E; entity("plankv") -> 16#0210F; entity("plus") -> 16#0002B; entity("plusacir") -> 16#02A23; entity("plusb") -> 16#0229E; entity("pluscir") -> 16#02A22; entity("plusdo") -> 16#02214; entity("plusdu") -> 16#02A25; entity("pluse") -> 16#02A72; entity("plusmn") -> 16#000B1; entity("plussim") -> 16#02A26; entity("plustwo") -> 16#02A27; entity("pm") -> 16#000B1; entity("pointint") -> 16#02A15; entity("popf") -> 16#1D561; entity("pound") -> 16#000A3; entity("pr") -> 16#0227A; entity("prE") -> 16#02AB3; entity("prap") -> 16#02AB7; entity("prcue") -> 16#0227C; entity("pre") -> 16#02AAF; entity("prec") -> 16#0227A; entity("precapprox") -> 16#02AB7; entity("preccurlyeq") -> 16#0227C; entity("preceq") -> 16#02AAF; entity("precnapprox") -> 16#02AB9; entity("precneqq") -> 16#02AB5; entity("precnsim") -> 16#022E8; entity("precsim") -> 16#0227E; entity("prime") -> 16#02032; entity("primes") -> 16#02119; entity("prnE") -> 16#02AB5; entity("prnap") -> 16#02AB9; entity("prnsim") -> 16#022E8; entity("prod") -> 16#0220F; entity("profalar") -> 16#0232E; entity("profline") -> 16#02312; entity("profsurf") -> 16#02313; entity("prop") -> 16#0221D; entity("propto") -> 16#0221D; entity("prsim") -> 16#0227E; entity("prurel") -> 16#022B0; entity("pscr") -> 16#1D4C5; entity("psi") -> 16#003C8; entity("puncsp") -> 16#02008; entity("qfr") -> 16#1D52E; entity("qint") -> 16#02A0C; entity("qopf") -> 16#1D562; entity("qprime") -> 16#02057; entity("qscr") -> 16#1D4C6; entity("quaternions") -> 16#0210D; entity("quatint") -> 16#02A16; entity("quest") -> 16#0003F; entity("questeq") -> 16#0225F; entity("quot") -> 16#00022; entity("rAarr") -> 16#021DB; entity("rArr") -> 16#021D2; entity("rAtail") -> 16#0291C; entity("rBarr") -> 16#0290F; entity("rHar") -> 16#02964; entity("race") -> [16#0223D, 16#00331]; entity("racute") -> 16#00155; entity("radic") -> 16#0221A; entity("raemptyv") -> 16#029B3; entity("rang") -> 16#027E9; entity("rangd") -> 16#02992; entity("range") -> 16#029A5; entity("rangle") -> 16#027E9; entity("raquo") -> 16#000BB; entity("rarr") -> 16#02192; entity("rarrap") -> 16#02975; entity("rarrb") -> 16#021E5; entity("rarrbfs") -> 16#02920; entity("rarrc") -> 16#02933; entity("rarrfs") -> 16#0291E; entity("rarrhk") -> 16#021AA; entity("rarrlp") -> 16#021AC; entity("rarrpl") -> 16#02945; entity("rarrsim") -> 16#02974; entity("rarrtl") -> 16#021A3; entity("rarrw") -> 16#0219D; entity("ratail") -> 16#0291A; entity("ratio") -> 16#02236; entity("rationals") -> 16#0211A; entity("rbarr") -> 16#0290D; entity("rbbrk") -> 16#02773; entity("rbrace") -> 16#0007D; entity("rbrack") -> 16#0005D; entity("rbrke") -> 16#0298C; entity("rbrksld") -> 16#0298E; entity("rbrkslu") -> 16#02990; entity("rcaron") -> 16#00159; entity("rcedil") -> 16#00157; entity("rceil") -> 16#02309; entity("rcub") -> 16#0007D; entity("rcy") -> 16#00440; entity("rdca") -> 16#02937; entity("rdldhar") -> 16#02969; entity("rdquo") -> 16#0201D; entity("rdquor") -> 16#0201D; entity("rdsh") -> 16#021B3; entity("real") -> 16#0211C; entity("realine") -> 16#0211B; entity("realpart") -> 16#0211C; entity("reals") -> 16#0211D; entity("rect") -> 16#025AD; entity("reg") -> 16#000AE; entity("rfisht") -> 16#0297D; entity("rfloor") -> 16#0230B; entity("rfr") -> 16#1D52F; entity("rhard") -> 16#021C1; entity("rharu") -> 16#021C0; entity("rharul") -> 16#0296C; entity("rho") -> 16#003C1; entity("rhov") -> 16#003F1; entity("rightarrow") -> 16#02192; entity("rightarrowtail") -> 16#021A3; entity("rightharpoondown") -> 16#021C1; entity("rightharpoonup") -> 16#021C0; entity("rightleftarrows") -> 16#021C4; entity("rightleftharpoons") -> 16#021CC; entity("rightrightarrows") -> 16#021C9; entity("rightsquigarrow") -> 16#0219D; entity("rightthreetimes") -> 16#022CC; entity("ring") -> 16#002DA; entity("risingdotseq") -> 16#02253; entity("rlarr") -> 16#021C4; entity("rlhar") -> 16#021CC; entity("rlm") -> 16#0200F; entity("rmoust") -> 16#023B1; entity("rmoustache") -> 16#023B1; entity("rnmid") -> 16#02AEE; entity("roang") -> 16#027ED; entity("roarr") -> 16#021FE; entity("robrk") -> 16#027E7; entity("ropar") -> 16#02986; entity("ropf") -> 16#1D563; entity("roplus") -> 16#02A2E; entity("rotimes") -> 16#02A35; entity("rpar") -> 16#00029; entity("rpargt") -> 16#02994; entity("rppolint") -> 16#02A12; entity("rrarr") -> 16#021C9; entity("rsaquo") -> 16#0203A; entity("rscr") -> 16#1D4C7; entity("rsh") -> 16#021B1; entity("rsqb") -> 16#0005D; entity("rsquo") -> 16#02019; entity("rsquor") -> 16#02019; entity("rthree") -> 16#022CC; entity("rtimes") -> 16#022CA; entity("rtri") -> 16#025B9; entity("rtrie") -> 16#022B5; entity("rtrif") -> 16#025B8; entity("rtriltri") -> 16#029CE; entity("ruluhar") -> 16#02968; entity("rx") -> 16#0211E; entity("sacute") -> 16#0015B; entity("sbquo") -> 16#0201A; entity("sc") -> 16#0227B; entity("scE") -> 16#02AB4; entity("scap") -> 16#02AB8; entity("scaron") -> 16#00161; entity("sccue") -> 16#0227D; entity("sce") -> 16#02AB0; entity("scedil") -> 16#0015F; entity("scirc") -> 16#0015D; entity("scnE") -> 16#02AB6; entity("scnap") -> 16#02ABA; entity("scnsim") -> 16#022E9; entity("scpolint") -> 16#02A13; entity("scsim") -> 16#0227F; entity("scy") -> 16#00441; entity("sdot") -> 16#022C5; entity("sdotb") -> 16#022A1; entity("sdote") -> 16#02A66; entity("seArr") -> 16#021D8; entity("searhk") -> 16#02925; entity("searr") -> 16#02198; entity("searrow") -> 16#02198; entity("sect") -> 16#000A7; entity("semi") -> 16#0003B; entity("seswar") -> 16#02929; entity("setminus") -> 16#02216; entity("setmn") -> 16#02216; entity("sext") -> 16#02736; entity("sfr") -> 16#1D530; entity("sfrown") -> 16#02322; entity("sharp") -> 16#0266F; entity("shchcy") -> 16#00449; entity("shcy") -> 16#00448; entity("shortmid") -> 16#02223; entity("shortparallel") -> 16#02225; entity("shy") -> 16#000AD; entity("sigma") -> 16#003C3; entity("sigmaf") -> 16#003C2; entity("sigmav") -> 16#003C2; entity("sim") -> 16#0223C; entity("simdot") -> 16#02A6A; entity("sime") -> 16#02243; entity("simeq") -> 16#02243; entity("simg") -> 16#02A9E; entity("simgE") -> 16#02AA0; entity("siml") -> 16#02A9D; entity("simlE") -> 16#02A9F; entity("simne") -> 16#02246; entity("simplus") -> 16#02A24; entity("simrarr") -> 16#02972; entity("slarr") -> 16#02190; entity("smallsetminus") -> 16#02216; entity("smashp") -> 16#02A33; entity("smeparsl") -> 16#029E4; entity("smid") -> 16#02223; entity("smile") -> 16#02323; entity("smt") -> 16#02AAA; entity("smte") -> 16#02AAC; entity("smtes") -> [16#02AAC, 16#0FE00]; entity("softcy") -> 16#0044C; entity("sol") -> 16#0002F; entity("solb") -> 16#029C4; entity("solbar") -> 16#0233F; entity("sopf") -> 16#1D564; entity("spades") -> 16#02660; entity("spadesuit") -> 16#02660; entity("spar") -> 16#02225; entity("sqcap") -> 16#02293; entity("sqcaps") -> [16#02293, 16#0FE00]; entity("sqcup") -> 16#02294; entity("sqcups") -> [16#02294, 16#0FE00]; entity("sqsub") -> 16#0228F; entity("sqsube") -> 16#02291; entity("sqsubset") -> 16#0228F; entity("sqsubseteq") -> 16#02291; entity("sqsup") -> 16#02290; entity("sqsupe") -> 16#02292; entity("sqsupset") -> 16#02290; entity("sqsupseteq") -> 16#02292; entity("squ") -> 16#025A1; entity("square") -> 16#025A1; entity("squarf") -> 16#025AA; entity("squf") -> 16#025AA; entity("srarr") -> 16#02192; entity("sscr") -> 16#1D4C8; entity("ssetmn") -> 16#02216; entity("ssmile") -> 16#02323; entity("sstarf") -> 16#022C6; entity("star") -> 16#02606; entity("starf") -> 16#02605; entity("straightepsilon") -> 16#003F5; entity("straightphi") -> 16#003D5; entity("strns") -> 16#000AF; entity("sub") -> 16#02282; entity("subE") -> 16#02AC5; entity("subdot") -> 16#02ABD; entity("sube") -> 16#02286; entity("subedot") -> 16#02AC3; entity("submult") -> 16#02AC1; entity("subnE") -> 16#02ACB; entity("subne") -> 16#0228A; entity("subplus") -> 16#02ABF; entity("subrarr") -> 16#02979; entity("subset") -> 16#02282; entity("subseteq") -> 16#02286; entity("subseteqq") -> 16#02AC5; entity("subsetneq") -> 16#0228A; entity("subsetneqq") -> 16#02ACB; entity("subsim") -> 16#02AC7; entity("subsub") -> 16#02AD5; entity("subsup") -> 16#02AD3; entity("succ") -> 16#0227B; entity("succapprox") -> 16#02AB8; entity("succcurlyeq") -> 16#0227D; entity("succeq") -> 16#02AB0; entity("succnapprox") -> 16#02ABA; entity("succneqq") -> 16#02AB6; entity("succnsim") -> 16#022E9; entity("succsim") -> 16#0227F; entity("sum") -> 16#02211; entity("sung") -> 16#0266A; entity("sup") -> 16#02283; entity("sup1") -> 16#000B9; entity("sup2") -> 16#000B2; entity("sup3") -> 16#000B3; entity("supE") -> 16#02AC6; entity("supdot") -> 16#02ABE; entity("supdsub") -> 16#02AD8; entity("supe") -> 16#02287; entity("supedot") -> 16#02AC4; entity("suphsol") -> 16#027C9; entity("suphsub") -> 16#02AD7; entity("suplarr") -> 16#0297B; entity("supmult") -> 16#02AC2; entity("supnE") -> 16#02ACC; entity("supne") -> 16#0228B; entity("supplus") -> 16#02AC0; entity("supset") -> 16#02283; entity("supseteq") -> 16#02287; entity("supseteqq") -> 16#02AC6; entity("supsetneq") -> 16#0228B; entity("supsetneqq") -> 16#02ACC; entity("supsim") -> 16#02AC8; entity("supsub") -> 16#02AD4; entity("supsup") -> 16#02AD6; entity("swArr") -> 16#021D9; entity("swarhk") -> 16#02926; entity("swarr") -> 16#02199; entity("swarrow") -> 16#02199; entity("swnwar") -> 16#0292A; entity("szlig") -> 16#000DF; entity("target") -> 16#02316; entity("tau") -> 16#003C4; entity("tbrk") -> 16#023B4; entity("tcaron") -> 16#00165; entity("tcedil") -> 16#00163; entity("tcy") -> 16#00442; entity("tdot") -> 16#020DB; entity("telrec") -> 16#02315; entity("tfr") -> 16#1D531; entity("there4") -> 16#02234; entity("therefore") -> 16#02234; entity("theta") -> 16#003B8; entity("thetasym") -> 16#003D1; entity("thetav") -> 16#003D1; entity("thickapprox") -> 16#02248; entity("thicksim") -> 16#0223C; entity("thinsp") -> 16#02009; entity("thkap") -> 16#02248; entity("thksim") -> 16#0223C; entity("thorn") -> 16#000FE; entity("tilde") -> 16#002DC; entity("times") -> 16#000D7; entity("timesb") -> 16#022A0; entity("timesbar") -> 16#02A31; entity("timesd") -> 16#02A30; entity("tint") -> 16#0222D; entity("toea") -> 16#02928; entity("top") -> 16#022A4; entity("topbot") -> 16#02336; entity("topcir") -> 16#02AF1; entity("topf") -> 16#1D565; entity("topfork") -> 16#02ADA; entity("tosa") -> 16#02929; entity("tprime") -> 16#02034; entity("trade") -> 16#02122; entity("triangle") -> 16#025B5; entity("triangledown") -> 16#025BF; entity("triangleleft") -> 16#025C3; entity("trianglelefteq") -> 16#022B4; entity("triangleq") -> 16#0225C; entity("triangleright") -> 16#025B9; entity("trianglerighteq") -> 16#022B5; entity("tridot") -> 16#025EC; entity("trie") -> 16#0225C; entity("triminus") -> 16#02A3A; entity("triplus") -> 16#02A39; entity("trisb") -> 16#029CD; entity("tritime") -> 16#02A3B; entity("trpezium") -> 16#023E2; entity("tscr") -> 16#1D4C9; entity("tscy") -> 16#00446; entity("tshcy") -> 16#0045B; entity("tstrok") -> 16#00167; entity("twixt") -> 16#0226C; entity("twoheadleftarrow") -> 16#0219E; entity("twoheadrightarrow") -> 16#021A0; entity("uArr") -> 16#021D1; entity("uHar") -> 16#02963; entity("uacute") -> 16#000FA; entity("uarr") -> 16#02191; entity("ubrcy") -> 16#0045E; entity("ubreve") -> 16#0016D; entity("ucirc") -> 16#000FB; entity("ucy") -> 16#00443; entity("udarr") -> 16#021C5; entity("udblac") -> 16#00171; entity("udhar") -> 16#0296E; entity("ufisht") -> 16#0297E; entity("ufr") -> 16#1D532; entity("ugrave") -> 16#000F9; entity("uharl") -> 16#021BF; entity("uharr") -> 16#021BE; entity("uhblk") -> 16#02580; entity("ulcorn") -> 16#0231C; entity("ulcorner") -> 16#0231C; entity("ulcrop") -> 16#0230F; entity("ultri") -> 16#025F8; entity("umacr") -> 16#0016B; entity("uml") -> 16#000A8; entity("uogon") -> 16#00173; entity("uopf") -> 16#1D566; entity("uparrow") -> 16#02191; entity("updownarrow") -> 16#02195; entity("upharpoonleft") -> 16#021BF; entity("upharpoonright") -> 16#021BE; entity("uplus") -> 16#0228E; entity("upsi") -> 16#003C5; entity("upsih") -> 16#003D2; entity("upsilon") -> 16#003C5; entity("upuparrows") -> 16#021C8; entity("urcorn") -> 16#0231D; entity("urcorner") -> 16#0231D; entity("urcrop") -> 16#0230E; entity("uring") -> 16#0016F; entity("urtri") -> 16#025F9; entity("uscr") -> 16#1D4CA; entity("utdot") -> 16#022F0; entity("utilde") -> 16#00169; entity("utri") -> 16#025B5; entity("utrif") -> 16#025B4; entity("uuarr") -> 16#021C8; entity("uuml") -> 16#000FC; entity("uwangle") -> 16#029A7; entity("vArr") -> 16#021D5; entity("vBar") -> 16#02AE8; entity("vBarv") -> 16#02AE9; entity("vDash") -> 16#022A8; entity("vangrt") -> 16#0299C; entity("varepsilon") -> 16#003F5; entity("varkappa") -> 16#003F0; entity("varnothing") -> 16#02205; entity("varphi") -> 16#003D5; entity("varpi") -> 16#003D6; entity("varpropto") -> 16#0221D; entity("varr") -> 16#02195; entity("varrho") -> 16#003F1; entity("varsigma") -> 16#003C2; entity("varsubsetneq") -> [16#0228A, 16#0FE00]; entity("varsubsetneqq") -> [16#02ACB, 16#0FE00]; entity("varsupsetneq") -> [16#0228B, 16#0FE00]; entity("varsupsetneqq") -> [16#02ACC, 16#0FE00]; entity("vartheta") -> 16#003D1; entity("vartriangleleft") -> 16#022B2; entity("vartriangleright") -> 16#022B3; entity("vcy") -> 16#00432; entity("vdash") -> 16#022A2; entity("vee") -> 16#02228; entity("veebar") -> 16#022BB; entity("veeeq") -> 16#0225A; entity("vellip") -> 16#022EE; entity("verbar") -> 16#0007C; entity("vert") -> 16#0007C; entity("vfr") -> 16#1D533; entity("vltri") -> 16#022B2; entity("vnsub") -> [16#02282, 16#020D2]; entity("vnsup") -> [16#02283, 16#020D2]; entity("vopf") -> 16#1D567; entity("vprop") -> 16#0221D; entity("vrtri") -> 16#022B3; entity("vscr") -> 16#1D4CB; entity("vsubnE") -> [16#02ACB, 16#0FE00]; entity("vsubne") -> [16#0228A, 16#0FE00]; entity("vsupnE") -> [16#02ACC, 16#0FE00]; entity("vsupne") -> [16#0228B, 16#0FE00]; entity("vzigzag") -> 16#0299A; entity("wcirc") -> 16#00175; entity("wedbar") -> 16#02A5F; entity("wedge") -> 16#02227; entity("wedgeq") -> 16#02259; entity("weierp") -> 16#02118; entity("wfr") -> 16#1D534; entity("wopf") -> 16#1D568; entity("wp") -> 16#02118; entity("wr") -> 16#02240; entity("wreath") -> 16#02240; entity("wscr") -> 16#1D4CC; entity("xcap") -> 16#022C2; entity("xcirc") -> 16#025EF; entity("xcup") -> 16#022C3; entity("xdtri") -> 16#025BD; entity("xfr") -> 16#1D535; entity("xhArr") -> 16#027FA; entity("xharr") -> 16#027F7; entity("xi") -> 16#003BE; entity("xlArr") -> 16#027F8; entity("xlarr") -> 16#027F5; entity("xmap") -> 16#027FC; entity("xnis") -> 16#022FB; entity("xodot") -> 16#02A00; entity("xopf") -> 16#1D569; entity("xoplus") -> 16#02A01; entity("xotime") -> 16#02A02; entity("xrArr") -> 16#027F9; entity("xrarr") -> 16#027F6; entity("xscr") -> 16#1D4CD; entity("xsqcup") -> 16#02A06; entity("xuplus") -> 16#02A04; entity("xutri") -> 16#025B3; entity("xvee") -> 16#022C1; entity("xwedge") -> 16#022C0; entity("yacute") -> 16#000FD; entity("yacy") -> 16#0044F; entity("ycirc") -> 16#00177; entity("ycy") -> 16#0044B; entity("yen") -> 16#000A5; entity("yfr") -> 16#1D536; entity("yicy") -> 16#00457; entity("yopf") -> 16#1D56A; entity("yscr") -> 16#1D4CE; entity("yucy") -> 16#0044E; entity("yuml") -> 16#000FF; entity("zacute") -> 16#0017A; entity("zcaron") -> 16#0017E; entity("zcy") -> 16#00437; entity("zdot") -> 16#0017C; entity("zeetrf") -> 16#02128; entity("zeta") -> 16#003B6; entity("zfr") -> 16#1D537; entity("zhcy") -> 16#00436; entity("zigrarr") -> 16#021DD; entity("zopf") -> 16#1D56B; entity("zscr") -> 16#1D4CF; entity("zwj") -> 16#0200D; entity("zwnj") -> 16#0200C; entity(_) -> undefined. %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). exhaustive_entity_test() -> T = mochiweb_cover:clause_lookup_table(?MODULE, entity), [?assertEqual(V, entity(K)) || {K, V} <- T]. charref_test() -> 1234 = charref("#1234"), 255 = charref("#xfF"), 255 = charref(<<"#XFf">>), 38 = charref("amp"), 38 = charref(<<"amp">>), undefined = charref("not_an_entity"), undefined = charref("#not_an_entity"), undefined = charref("#xnot_an_entity"), ok. -endif. mochiweb-3.2.1/src/mochiweb_clock.erl000066400000000000000000000056411450333227400175660ustar00rootroot00000000000000%% Copyright (c) 2011-2014, Loïc Hoguin %% Copyright (c) 2015, Robert Kowalski %% %% Permission to use, copy, modify, and/or distribute this software for any %% purpose with or without fee is hereby granted, provided that the above %% copyright notice and this permission notice appear in all copies. %% %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. %% While a gen_server process runs in the background to update %% the cache of formatted dates every second, all API calls are %% local and directly read from the ETS cache table, providing %% fast time and date computations. -module(mochiweb_clock). -behaviour(gen_server). %% API. -export([start_link/0]). -export([start/0]). -export([stop/0]). -export([rfc1123/0]). %% gen_server. -export([init/1]). -export([handle_call/3]). -export([handle_cast/2]). -export([handle_info/2]). -export([terminate/2]). -export([code_change/3]). -record(state, {}). %% API. -spec start_link() -> {ok, pid()}. start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). -spec start() -> {ok, pid()}. start() -> gen_server:start({local, ?MODULE}, ?MODULE, [], []). -spec stop() -> stopped. stop() -> gen_server:call(?MODULE, stop). -spec rfc1123() -> string(). rfc1123() -> case ets:lookup(?MODULE, rfc1123) of [{rfc1123, Date}] -> Date; [] -> "" end. %% gen_server. -spec init([]) -> {ok, #state{}}. init([]) -> ?MODULE = ets:new(?MODULE, [named_table, protected, {read_concurrency, true}]), handle_info(update_date, #state{}), timer:send_interval(1000, update_date), {ok, #state{}}. -type from() :: {pid(), term()}. -spec handle_call (stop, from(), State) -> {stop, normal, stopped, State} when State::#state{}. handle_call(stop, _From, State) -> {stop, normal, stopped, State}; handle_call(_Request, _From, State) -> {reply, ignored, State}. -spec handle_cast(_, State) -> {noreply, State} when State::#state{}. handle_cast(_Msg, State) -> {noreply, State}. -spec handle_info(any(), State) -> {noreply, State} when State::#state{}. handle_info(update_date, State) -> Date = httpd_util:rfc1123_date(), ets:insert(?MODULE, {rfc1123, Date}), {noreply, State}; handle_info(_Info, State) -> {noreply, State}. -spec terminate(_, _) -> ok. terminate(_Reason, _State) -> ok. -spec code_change(_, State, _) -> {ok, State} when State::#state{}. code_change(_OldVsn, State, _Extra) -> {ok, State}. mochiweb-3.2.1/src/mochiweb_cookies.erl000066400000000000000000000320031450333227400201170ustar00rootroot00000000000000%% @author Emad El-Haraty %% @copyright 2007 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc HTTP Cookie parsing and generating (RFC 2109, RFC 2965). -module(mochiweb_cookies). -export([parse_cookie/1, cookie/3, cookie/2]). -define(QUOTE, $\"). -define(IS_WHITESPACE(C), (C =:= $\s orelse C =:= $\t orelse C =:= $\r orelse C =:= $\n)). %% RFC 2616 separators (called tspecials in RFC 2068) -define(IS_SEPARATOR(C), (C < 32 orelse C =:= $\s orelse C =:= $\t orelse C =:= $( orelse C =:= $) orelse C =:= $< orelse C =:= $> orelse C =:= $@ orelse C =:= $, orelse C =:= $; orelse C =:= $: orelse C =:= $\\ orelse C =:= $\" orelse C =:= $/ orelse C =:= $[ orelse C =:= $] orelse C =:= $? orelse C =:= $= orelse C =:= ${ orelse C =:= $})). %% RFC 6265 cookie value allowed characters %% cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E %% ; US-ASCII characters excluding CTLs, %% ; whitespace DQUOTE, comma, semicolon, %% ; and backslash -define(IS_COOKIE_OCTET(C), (C =:= 16#21 orelse (C >= 16#23 andalso C =< 16#2B) orelse (C >= 16#2D andalso C =< 16#3A) orelse (C >= 16#3C andalso C =< 16#5B) orelse (C >= 16#5D andalso C =< 16#7E) )). %% @type proplist() = [{Key::string(), Value::string()}]. %% @type header() = {Name::string(), Value::string()}. %% @type int_seconds() = integer(). %% @spec cookie(Key::string(), Value::string()) -> header() %% @doc Short-hand for cookie(Key, Value, []). cookie(Key, Value) -> cookie(Key, Value, []). %% @spec cookie(Key::string(), Value::string(), Options::[Option]) -> header() %% where Option = {max_age, int_seconds()} | {local_time, {date(), time()}} %% | {domain, string()} | {path, string()} %% | {secure, true | false} | {http_only, true | false} %% | {same_site, lax | strict | none} %% %% @doc Generate a Set-Cookie header field tuple. cookie(Key, Value, Options) -> Cookie = [any_to_list(Key), "=", quote(Value), "; Version=1"], %% Set-Cookie: %% Comment, Domain, Max-Age, Path, Secure, Version %% Set-Cookie2: %% Comment, CommentURL, Discard, Domain, Max-Age, Path, Port, Secure, %% Version ExpiresPart = case proplists:get_value(max_age, Options) of undefined -> ""; RawAge -> When = case proplists:get_value(local_time, Options) of undefined -> calendar:local_time(); LocalTime -> LocalTime end, Age = case RawAge < 0 of true -> 0; false -> RawAge end, ["; Expires=", age_to_cookie_date(Age, When), "; Max-Age=", quote(Age)] end, SecurePart = case proplists:get_value(secure, Options) of true -> "; Secure"; _ -> "" end, DomainPart = case proplists:get_value(domain, Options) of undefined -> ""; Domain -> ["; Domain=", quote(Domain)] end, PathPart = case proplists:get_value(path, Options) of undefined -> ""; Path -> ["; Path=", quote(Path)] end, HttpOnlyPart = case proplists:get_value(http_only, Options) of true -> "; HttpOnly"; _ -> "" end, SameSitePart = case proplists:get_value(same_site, Options) of undefined -> ""; lax -> "; SameSite=Lax"; strict -> "; SameSite=Strict"; none -> "; SameSite=None" end, CookieParts = [Cookie, ExpiresPart, SecurePart, DomainPart, PathPart, HttpOnlyPart, SameSitePart], {"Set-Cookie", lists:flatten(CookieParts)}. %% Every major browser incorrectly handles quoted strings in a %% different and (worse) incompatible manner. Instead of wasting time %% writing redundant code for each browser, we restrict cookies to %% only contain characters that browsers handle compatibly. %% %% By replacing the definition of quote with this, we generate %% RFC-compliant cookies: %% %% quote(V) -> %% Fun = fun(?QUOTE, Acc) -> [$\\, ?QUOTE | Acc]; %% (Ch, Acc) -> [Ch | Acc] %% end, %% [?QUOTE | lists:foldr(Fun, [?QUOTE], V)]. %% Convert to a string and raise an error if quoting is required. quote(V0) -> V = any_to_list(V0), lists:all(fun(Ch) -> Ch =:= $/ orelse not ?IS_SEPARATOR(Ch) end, V) orelse erlang:error({cookie_quoting_required, V}), V. %% Return a date in the form of: Wdy, DD-Mon-YYYY HH:MM:SS GMT %% See also: rfc2109: 10.1.2 rfc2109_cookie_expires_date(LocalTime) -> {{YYYY,MM,DD},{Hour,Min,Sec}} = case calendar:local_time_to_universal_time_dst(LocalTime) of [] -> {Date, {Hour1, Min1, Sec1}} = LocalTime, LocalTime2 = {Date, {Hour1 + 1, Min1, Sec1}}, case calendar:local_time_to_universal_time_dst(LocalTime2) of [Gmt] -> Gmt; [_,Gmt] -> Gmt end; [Gmt] -> Gmt; [_,Gmt] -> Gmt end, DayNumber = calendar:day_of_the_week({YYYY,MM,DD}), lists:flatten( io_lib:format("~s, ~2.2.0w-~3.s-~4.4.0w ~2.2.0w:~2.2.0w:~2.2.0w GMT", [httpd_util:day(DayNumber),DD,httpd_util:month(MM),YYYY,Hour,Min,Sec])). add_seconds(Secs, LocalTime) -> Greg = calendar:datetime_to_gregorian_seconds(LocalTime), calendar:gregorian_seconds_to_datetime(Greg + Secs). age_to_cookie_date(Age, LocalTime) -> rfc2109_cookie_expires_date(add_seconds(Age, LocalTime)). %% @spec parse_cookie(string()) -> [{K::string(), V::string()}] %% @doc Parse the contents of a Cookie header field, ignoring cookie %% attributes, and return a simple property list. parse_cookie("") -> []; parse_cookie(Cookie) -> parse_cookie(Cookie, []). %% Internal API parse_cookie([], Acc) -> lists:reverse(Acc); parse_cookie(String, Acc) -> {{Token, Value}, Rest} = read_pair(String), Acc1 = case Token of "" -> Acc; "$" ++ _ -> Acc; _ -> [{Token, Value} | Acc] end, parse_cookie(Rest, Acc1). read_pair(String) -> {Token, Rest} = read_token(skip_whitespace(String)), {Value, Rest1} = read_value(skip_whitespace(Rest)), {{Token, Value}, skip_past_separator(Rest1)}. read_value([$= | Value]) -> Value1 = skip_whitespace(Value), case Value1 of [?QUOTE | _] -> read_quoted(Value1); _ -> read_value_(Value1) end; read_value(String) -> {"", String}. read_value_(String) -> F = fun (C) -> ?IS_COOKIE_OCTET(C) end, lists:splitwith(F, String). read_quoted([?QUOTE | String]) -> read_quoted(String, []). read_quoted([], Acc) -> {lists:reverse(Acc), []}; read_quoted([?QUOTE | Rest], Acc) -> {lists:reverse(Acc), Rest}; read_quoted([$\\, Any | Rest], Acc) -> read_quoted(Rest, [Any | Acc]); read_quoted([C | Rest], Acc) -> read_quoted(Rest, [C | Acc]). skip_whitespace(String) -> F = fun (C) -> ?IS_WHITESPACE(C) end, lists:dropwhile(F, String). read_token(String) -> F = fun (C) -> not ?IS_SEPARATOR(C) end, lists:splitwith(F, String). skip_past_separator([]) -> []; skip_past_separator([$; | Rest]) -> Rest; skip_past_separator([$, | Rest]) -> Rest; skip_past_separator([_ | Rest]) -> skip_past_separator(Rest). any_to_list(V) when is_list(V) -> V; any_to_list(V) when is_atom(V) -> atom_to_list(V); any_to_list(V) when is_binary(V) -> binary_to_list(V); any_to_list(V) when is_integer(V) -> integer_to_list(V). %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). quote_test() -> %% ?assertError eunit macro is not compatible with coverage module try quote(":wq") catch error:{cookie_quoting_required, ":wq"} -> ok end, ?assertEqual( "foo", quote(foo)), ok. parse_cookie_test() -> %% RFC example C1 = "$Version=\"1\"; Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\"; Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\"; Shipping=\"FedEx\"; $Path=\"/acme\"", ?assertEqual( [{"Customer","WILE_E_COYOTE"}, {"Part_Number","Rocket_Launcher_0001"}, {"Shipping","FedEx"}], parse_cookie(C1)), %% Potential edge cases ?assertEqual( [{"foo", "x"}], parse_cookie("foo=\"\\x\"")), ?assertEqual( [], parse_cookie("=")), ?assertEqual( [{"foo", ""}, {"bar", ""}], parse_cookie(" foo ; bar ")), ?assertEqual( [{"foo", ""}, {"bar", ""}], parse_cookie("foo=;bar=")), ?assertEqual( [{"foo", "\";"}, {"bar", ""}], parse_cookie("foo = \"\\\";\";bar ")), ?assertEqual( [{"foo", "\";bar"}], parse_cookie("foo=\"\\\";bar")), ?assertEqual( [], parse_cookie([])), ?assertEqual( [{"foo", "bar"}, {"baz", "wibble"}], parse_cookie("foo=bar , baz=wibble ")), ?assertEqual( [{"foo", "base64=="}, {"bar", "base64="}], parse_cookie("foo=\"base64==\";bar=\"base64=\"")), ?assertEqual( [{"foo", "base64=="}, {"bar", "base64="}], parse_cookie("foo=base64==;bar=base64=")), ok. domain_test() -> ?assertEqual( {"Set-Cookie", "Customer=WILE_E_COYOTE; " "Version=1; " "Domain=acme.com; " "HttpOnly"}, cookie("Customer", "WILE_E_COYOTE", [{http_only, true}, {domain, "acme.com"}])), ok. local_time_test() -> {"Set-Cookie", S} = cookie("Customer", "WILE_E_COYOTE", [{max_age, 111}, {secure, true}]), ?assertMatch( ["Customer=WILE_E_COYOTE", " Version=1", " Expires=" ++ _, " Max-Age=111", " Secure"], string:tokens(S, ";")), ok. cookie_test() -> C1 = {"Set-Cookie", "Customer=WILE_E_COYOTE; " "Version=1; " "Path=/acme"}, C1 = cookie("Customer", "WILE_E_COYOTE", [{path, "/acme"}]), C1 = cookie("Customer", "WILE_E_COYOTE", [{path, "/acme"}, {badoption, "negatory"}]), C1 = cookie('Customer', 'WILE_E_COYOTE', [{path, '/acme'}]), C1 = cookie(<<"Customer">>, <<"WILE_E_COYOTE">>, [{path, <<"/acme">>}]), {"Set-Cookie","=NoKey; Version=1"} = cookie("", "NoKey", []), {"Set-Cookie","=NoKey; Version=1"} = cookie("", "NoKey"), LocalTime = calendar:universal_time_to_local_time({{2007, 5, 15}, {13, 45, 33}}), C2 = {"Set-Cookie", "Customer=WILE_E_COYOTE; " "Version=1; " "Expires=Tue, 15-May-2007 13:45:33 GMT; " "Max-Age=0"}, C2 = cookie("Customer", "WILE_E_COYOTE", [{max_age, -111}, {local_time, LocalTime}]), C3 = {"Set-Cookie", "Customer=WILE_E_COYOTE; " "Version=1; " "Expires=Wed, 16-May-2007 13:45:50 GMT; " "Max-Age=86417"}, C3 = cookie("Customer", "WILE_E_COYOTE", [{max_age, 86417}, {local_time, LocalTime}]), % test various values for SameSite % % unset default to nothing C4 = {"Set-Cookie","i=test123; Version=1"}, C4 = cookie("i", "test123", []), C5 = {"Set-Cookie","i=test123; Version=1; SameSite=Strict"}, C5 = cookie("i", "test123", [ {same_site, strict}]), C6 = {"Set-Cookie","i=test123; Version=1; SameSite=Lax"}, C6 = cookie("i", "test123", [ {same_site, lax}]), C7 = {"Set-Cookie","i=test123; Version=1; SameSite=None"}, C7 = cookie("i", "test123", [ {same_site, none}]), ok. -endif. mochiweb-3.2.1/src/mochiweb_cover.erl000066400000000000000000000057471450333227400176200ustar00rootroot00000000000000%% @author Bob Ippolito %% @copyright 2010 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc Workarounds for various cover deficiencies. -module(mochiweb_cover). -export([get_beam/1, get_abstract_code/1, get_clauses/2, clause_lookup_table/1]). -export([clause_lookup_table/2]). %% Internal get_beam(Module) -> {Module, Beam, _Path} = code:get_object_code(Module), Beam. get_abstract_code(Beam) -> {ok, {_Module, [{abstract_code, {raw_abstract_v1, L}}]}} = beam_lib:chunks(Beam, [abstract_code]), L. get_clauses(Function, Code) -> [L] = [Clauses || {function, _, FName, _, Clauses} <- Code, FName =:= Function], L. clause_lookup_table(Module, Function) -> clause_lookup_table( get_clauses(Function, get_abstract_code(get_beam(Module)))). clause_lookup_table(Clauses) -> lists:foldr(fun clause_fold/2, [], Clauses). clause_fold({clause, _, [InTerm], _Guards=[], [OutTerm]}, Acc) -> try [{erl_parse:normalise(InTerm), erl_parse:normalise(OutTerm)} | Acc] catch error:_ -> Acc end; clause_fold(_, Acc) -> Acc. %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). foo_table(a) -> b; foo_table("a") -> <<"b">>; foo_table(123) -> {4, 3, 2}; foo_table([list]) -> []; foo_table([list1, list2]) -> [list1, list2, list3]; foo_table(ignored) -> some, code, ignored; foo_table(Var) -> Var. foo_table_test() -> T = clause_lookup_table(?MODULE, foo_table), [?assertEqual(V, foo_table(K)) || {K, V} <- T]. clause_lookup_table_test() -> ?assertEqual(b, foo_table(a)), ?assertEqual(ignored, foo_table(ignored)), ?assertEqual('Var', foo_table('Var')), ?assertEqual( [{a, b}, {"a", <<"b">>}, {123, {4, 3, 2}}, {[list], []}, {[list1, list2], [list1, list2, list3]}], clause_lookup_table(?MODULE, foo_table)). -endif. mochiweb-3.2.1/src/mochiweb_echo.erl000066400000000000000000000036701450333227400174110ustar00rootroot00000000000000%% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc Simple and stupid echo server to demo mochiweb_socket_server. -module(mochiweb_echo). -author('bob@mochimedia.com'). -export([start/0, stop/0, loop/1]). stop() -> mochiweb_socket_server:stop(?MODULE). start() -> mochiweb_socket_server:start([{link, false} | options()]). options() -> [{name, ?MODULE}, {port, 6789}, {ip, "127.0.0.1"}, {max, 1}, {loop, {?MODULE, loop}}]. loop(Socket) -> case mochiweb_socket:recv(Socket, 0, 30000) of {ok, Data} -> case mochiweb_socket:send(Socket, Data) of ok -> loop(Socket); _ -> exit(normal) end; _Other -> exit(normal) end. %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -endif. mochiweb-3.2.1/src/mochiweb_headers.erl000066400000000000000000000402341450333227400201030ustar00rootroot00000000000000%% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc Case preserving (but case insensitive) HTTP Header dictionary. -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, 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]). %% @type headers(). %% @type key() = atom() | binary() | string(). %% @type value() = atom() | binary() | string() | integer(). %% @spec empty() -> headers() %% @doc Create an empty headers structure. empty() -> gb_trees:empty(). %% @spec make(headers() | [{key(), value()}]) -> headers() %% @doc Construct a headers() from the given list. make(L) when is_list(L) -> from_list(L); %% assume a non-list is already mochiweb_headers. make(T) -> T. %% @spec from_binary(iolist()) -> headers() %% @doc Transforms a raw HTTP header into a mochiweb headers structure. %% %% The given raw HTTP header can be one of the following: %% %% 1) A string or a binary representing a full HTTP header ending with %% double CRLF. %% Examples: %% ``` %% "Content-Length: 47\r\nContent-Type: text/plain\r\n\r\n" %% <<"Content-Length: 47\r\nContent-Type: text/plain\r\n\r\n">>''' %% %% 2) A list of binaries or strings where each element represents a raw %% HTTP header line ending with a single CRLF. %% Examples: %% ``` %% [<<"Content-Length: 47\r\n">>, <<"Content-Type: text/plain\r\n">>] %% ["Content-Length: 47\r\n", "Content-Type: text/plain\r\n"] %% ["Content-Length: 47\r\n", <<"Content-Type: text/plain\r\n">>]''' %% from_binary(RawHttpHeader) when is_binary(RawHttpHeader) -> from_binary(RawHttpHeader, []); from_binary(RawHttpHeaderList) -> from_binary(list_to_binary([RawHttpHeaderList, "\r\n"])). from_binary(RawHttpHeader, Acc) -> case erlang:decode_packet(httph, RawHttpHeader, []) of {ok, {http_header, _, H, _, V}, Rest} -> from_binary(Rest, [{H, V} | Acc]); _ -> make(Acc) end. %% @spec from_list([{key(), value()}]) -> headers() %% @doc Construct a headers() from the given list. from_list(List) -> lists:foldl(fun ({K, V}, T) -> insert(K, V, T) end, empty(), List). %% @spec enter_from_list([{key(), value()}], headers()) -> headers() %% @doc Insert pairs into the headers, replace any values for existing keys. enter_from_list(List, T) -> lists:foldl(fun ({K, V}, T1) -> enter(K, V, T1) end, T, List). %% @spec default_from_list([{key(), value()}], headers()) -> headers() %% @doc Insert pairs into the headers for keys that do not already exist. default_from_list(List, T) -> lists:foldl(fun ({K, V}, T1) -> default(K, V, T1) end, T, List). %% @spec to_list(headers()) -> [{key(), string()}] %% @doc Return the contents of the headers. The keys will be the exact key %% that was first inserted (e.g. may be an atom or binary, case is %% preserved). to_list(T) -> F = fun ({K, {array, L}}, Acc) -> L1 = lists:reverse(L), lists:foldl(fun (V, Acc1) -> [{K, V} | Acc1] end, Acc, L1); (Pair, Acc) -> [Pair | Acc] end, lists:reverse(lists:foldl(F, [], gb_trees:values(T))). %% @spec get_value(key(), headers()) -> string() | undefined %% @doc Return the value of the given header using a case insensitive search. %% undefined will be returned for keys that are not present. get_value(K, T) -> case lookup(K, T) of {value, {_, V}} -> expand(V); none -> undefined end. %% @spec get_primary_value(key(), headers()) -> string() | undefined %% @doc Return the value of the given header up to the first semicolon using %% a case insensitive search. undefined will be returned for keys %% that are not present. get_primary_value(K, T) -> case get_value(K, T) of undefined -> undefined; V -> 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 %% not present. lookup(K, T) -> case gb_trees:lookup(normalize(K), T) of {value, {K0, V}} -> {value, {K0, expand(V)}}; none -> none end. %% @spec default(key(), value(), headers()) -> headers() %% @doc Insert the pair into the headers if it does not already exist. default(K, V, T) -> K1 = normalize(K), V1 = trim_leading_and_trailing_ws(any_to_list(V)), try gb_trees:insert(K1, {K, V1}, T) catch error:{key_exists, _} -> T end. %% @spec enter(key(), value(), headers()) -> headers() %% @doc Insert the pair into the headers, replacing any pre-existing key. enter(K, V, T) -> K1 = normalize(K), V1 = trim_leading_and_trailing_ws(any_to_list(V)), gb_trees:enter(K1, {K, V1}, T). %% @spec insert(key(), value(), headers()) -> headers() %% @doc Insert the pair into the headers, merging with any pre-existing key. %% A merge is done with Value = V0 ++ ", " ++ V1. insert(K, V, T) -> K1 = normalize(K), V1 = trim_leading_and_trailing_ws(any_to_list(V)), try gb_trees:insert(K1, {K, V1}, T) catch error:{key_exists, _} -> {K0, V0} = gb_trees:get(K1, T), V2 = merge(K1, V1, V0), gb_trees:update(K1, {K0, V2}, T) end. %% @spec delete_any(key(), headers()) -> headers() %% @doc Delete the header corresponding to key if it is present. delete_any(K, T) -> K1 = normalize(K), gb_trees:delete_any(K1, T). %% Internal API tokenize_header_value(undefined) -> undefined; tokenize_header_value(V) -> reversed_tokens(trim_and_reverse(V, false), [], []). trim_leading_and_trailing_ws(S) -> trim_and_reverse(trim_and_reverse(S, false), 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(_, _, _) -> undefined. 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) -> V. merge("set-cookie", V1, {array, L}) -> {array, [V1 | L]}; merge("set-cookie", V1, V0) -> {array, [V1, V0]}; merge(_, V1, V0) -> V0 ++ ", " ++ V1. normalize(K) when is_list(K) -> string:to_lower(K); normalize(K) when is_atom(K) -> normalize(atom_to_list(K)); normalize(K) when is_binary(K) -> normalize(binary_to_list(K)). any_to_list(V) when is_list(V) -> V; any_to_list(V) when is_atom(V) -> atom_to_list(V); any_to_list(V) when is_binary(V) -> binary_to_list(V); any_to_list(V) when is_integer(V) -> integer_to_list(V). %% %% Tests. %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). make_test() -> Identity = make([{hdr, foo}]), ?assertEqual( Identity, make(Identity)). enter_from_list_test() -> H = make([{hdr, foo}]), ?assertEqual( [{baz, "wibble"}, {hdr, "foo"}], to_list(enter_from_list([{baz, wibble}], H))), ?assertEqual( [{hdr, "bar"}], to_list(enter_from_list([{hdr, bar}], H))), ok. default_from_list_test() -> H = make([{hdr, foo}]), ?assertEqual( [{baz, "wibble"}, {hdr, "foo"}], to_list(default_from_list([{baz, wibble}], H))), ?assertEqual( [{hdr, "foo"}], to_list(default_from_list([{hdr, bar}], H))), ok. get_primary_value_test() -> H = make([{hdr, foo}, {baz, <<"wibble;taco">>}]), ?assertEqual( "foo", get_primary_value(hdr, H)), ?assertEqual( undefined, get_primary_value(bar, H)), ?assertEqual( "wibble", 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( [{"set-cookie", "foo"}, {"set-cookie", "bar"}, {"set-cookie", "baz"}], to_list(H)), ok. whitespace_headers_test() -> %% Check RFC 7230 whitespace compliance H = ?MODULE:make([{"X-Auth-Roles", " test, test2,test3, test4, test5 , test6 "}]), ?assertEqual( [{"X-Auth-Roles", "test, test2,test3, test4, test5 , test6"}], to_list(H)). headers_test() -> H = ?MODULE:make([{hdr, foo}, {"Hdr", "bar"}, {'Hdr', 2}]), [{hdr, "foo, bar, 2"}] = ?MODULE:to_list(H), H1 = ?MODULE:insert(taco, grande, H), [{hdr, "foo, bar, 2"}, {taco, "grande"}] = ?MODULE:to_list(H1), H2 = ?MODULE:make([{"Set-Cookie", "foo"}]), [{"Set-Cookie", "foo"}] = ?MODULE:to_list(H2), H3 = ?MODULE:insert("Set-Cookie", "bar", H2), [{"Set-Cookie", "foo"}, {"Set-Cookie", "bar"}] = ?MODULE:to_list(H3), "foo, bar" = ?MODULE:get_value("set-cookie", H3), {value, {"Set-Cookie", "foo, bar"}} = ?MODULE:lookup("set-cookie", H3), undefined = ?MODULE:get_value("shibby", H3), none = ?MODULE:lookup("shibby", H3), H4 = ?MODULE:insert("content-type", "application/x-www-form-urlencoded; charset=utf8", H3), "application/x-www-form-urlencoded" = ?MODULE:get_primary_value( "content-type", H4), H4 = ?MODULE:delete_any("nonexistent-header", H4), H3 = ?MODULE:delete_any("content-type", H4), HB = <<"Content-Length: 47\r\nContent-Type: text/plain\r\n\r\n">>, H_HB = ?MODULE:from_binary(HB), H_HB = ?MODULE:from_binary(binary_to_list(HB)), "47" = ?MODULE:get_value("Content-Length", H_HB), "text/plain" = ?MODULE:get_value("Content-Type", H_HB), L_H_HB = ?MODULE:to_list(H_HB), 2 = length(L_H_HB), true = lists:member({'Content-Length', "47"}, L_H_HB), true = lists:member({'Content-Type', "text/plain"}, L_H_HB), HL = [ <<"Content-Length: 47\r\n">>, <<"Content-Type: text/plain\r\n">> ], HL2 = [ "Content-Length: 47\r\n", <<"Content-Type: text/plain\r\n">> ], HL3 = [ <<"Content-Length: 47\r\n">>, "Content-Type: text/plain\r\n" ], H_HL = ?MODULE:from_binary(HL), H_HL = ?MODULE:from_binary(HL2), H_HL = ?MODULE:from_binary(HL3), "47" = ?MODULE:get_value("Content-Length", H_HL), "text/plain" = ?MODULE:get_value("Content-Type", H_HL), L_H_HL = ?MODULE:to_list(H_HL), 2 = length(L_H_HL), true = lists:member({'Content-Length', "47"}, L_H_HL), true = lists:member({'Content-Type', "text/plain"}, L_H_HL), [] = ?MODULE:to_list(?MODULE:from_binary(<<>>)), [] = ?MODULE:to_list(?MODULE:from_binary(<<"">>)), [] = ?MODULE:to_list(?MODULE:from_binary(<<"\r\n">>)), [] = ?MODULE:to_list(?MODULE:from_binary(<<"\r\n\r\n">>)), [] = ?MODULE:to_list(?MODULE:from_binary("")), [] = ?MODULE:to_list(?MODULE:from_binary([<<>>])), [] = ?MODULE:to_list(?MODULE:from_binary([<<"">>])), [] = ?MODULE:to_list(?MODULE:from_binary([<<"\r\n">>])), [] = ?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. mochiweb-3.2.1/src/mochiweb_html.erl000066400000000000000000000731761450333227400174470ustar00rootroot00000000000000%% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc Loosely tokenizes and generates parse trees for HTML 4. -module(mochiweb_html). -export([tokens/1, parse/1, parse_tokens/1, to_tokens/1, escape/1, escape_attr/1, to_html/1]). -ifdef(TEST). -export([destack/1, destack/2, is_singleton/1]). -endif. %% This is a macro to placate syntax highlighters.. -define(QUOTE, $\"). %% $\" -define(SQUOTE, $\'). %% $\' -define(ADV_COL(S, N), S#decoder{column=N+S#decoder.column, offset=N+S#decoder.offset}). -define(INC_COL(S), S#decoder{column=1+S#decoder.column, offset=1+S#decoder.offset}). -define(INC_LINE(S), S#decoder{column=1, line=1+S#decoder.line, offset=1+S#decoder.offset}). -define(INC_CHAR(S, C), case C of $\n -> S#decoder{column=1, line=1+S#decoder.line, offset=1+S#decoder.offset}; _ -> S#decoder{column=1+S#decoder.column, offset=1+S#decoder.offset} end). -define(IS_WHITESPACE(C), (C =:= $\s orelse C =:= $\t orelse C =:= $\r orelse C =:= $\n)). -define(IS_LETTER(C), ((C >= $A andalso C =< $Z) orelse (C >= $a andalso C =< $z))). -define(IS_LITERAL_SAFE(C), ((C >= $A andalso C =< $Z) orelse (C >= $a andalso C =< $z) orelse (C >= $0 andalso C =< $9))). -define(PROBABLE_CLOSE(C), (C =:= $> orelse ?IS_WHITESPACE(C))). -record(decoder, {line=1, column=1, offset=0}). %% @type html_node() = {string(), [html_attr()], [html_node() | string()]} %% @type html_attr() = {string(), string()} %% @type html_token() = html_data() | start_tag() | end_tag() | inline_html() | html_comment() | html_doctype() %% @type html_data() = {data, string(), Whitespace::boolean()} %% @type start_tag() = {start_tag, Name, [html_attr()], Singleton::boolean()} %% @type end_tag() = {end_tag, Name} %% @type html_comment() = {comment, Comment} %% @type html_doctype() = {doctype, [Doctype]} %% @type inline_html() = {'=', iolist()} %% External API. %% @spec parse(string() | binary()) -> html_node() %% @doc tokenize and then transform the token stream into a HTML tree. parse(Input) -> parse_tokens(tokens(Input)). %% @spec parse_tokens([html_token()]) -> html_node() %% @doc Transform the output of tokens(Doc) into a HTML tree. parse_tokens(Tokens) when is_list(Tokens) -> %% Skip over doctype, processing instructions [{start_tag, Tag, Attrs, false} | Rest] = find_document(Tokens, normal), {Tree, _} = tree(Rest, [norm({Tag, Attrs})]), Tree. find_document(Tokens=[{start_tag, _Tag, _Attrs, false} | _Rest], Mode) -> maybe_add_html_tag(Tokens, Mode); find_document([{doctype, [<<"html">>]} | Rest], _Mode) -> find_document(Rest, html5); find_document([_T | Rest], Mode) -> find_document(Rest, Mode); find_document([], _Mode) -> []. maybe_add_html_tag(Tokens=[{start_tag, Tag, _Attrs, false} | _], html5) when Tag =/= <<"html">> -> [{start_tag, <<"html">>, [], false} | Tokens]; maybe_add_html_tag(Tokens, _Mode) -> Tokens. %% @spec tokens(StringOrBinary) -> [html_token()] %% @doc Transform the input UTF-8 HTML into a token stream. tokens(Input) -> tokens(iolist_to_binary(Input), #decoder{}, []). %% @spec to_tokens(html_node()) -> [html_token()] %% @doc Convert a html_node() tree to a list of tokens. to_tokens({Tag0}) -> to_tokens({Tag0, [], []}); to_tokens(T={'=', _}) -> [T]; to_tokens(T={doctype, _}) -> [T]; to_tokens(T={comment, _}) -> [T]; to_tokens({Tag0, Acc}) -> %% This is only allowed in sub-tags: {p, [{"class", "foo"}]} to_tokens({Tag0, [], Acc}); to_tokens({Tag0, Attrs, Acc}) -> Tag = to_tag(Tag0), case is_singleton(Tag) of true -> to_tokens([], [{start_tag, Tag, Attrs, true}]); false -> to_tokens([{Tag, Acc}], [{start_tag, Tag, Attrs, false}]) end. %% @spec to_html([html_token()] | html_node()) -> iolist() %% @doc Convert a list of html_token() to a HTML document. to_html(Node) when is_tuple(Node) -> to_html(to_tokens(Node)); to_html(Tokens) when is_list(Tokens) -> to_html(Tokens, []). %% @spec escape(string() | atom() | binary()) -> binary() %% @doc Escape a string such that it's safe for HTML (amp; lt; gt;). escape(B) when is_binary(B) -> escape(binary_to_list(B), []); escape(A) when is_atom(A) -> escape(atom_to_list(A), []); escape(S) when is_list(S) -> escape(S, []). %% @spec escape_attr(string() | binary() | atom() | integer() | float()) -> binary() %% @doc Escape a string such that it's safe for HTML attrs %% (amp; lt; gt; quot;). escape_attr(B) when is_binary(B) -> escape_attr(binary_to_list(B), []); escape_attr(A) when is_atom(A) -> escape_attr(atom_to_list(A), []); escape_attr(S) when is_list(S) -> escape_attr(S, []); escape_attr(I) when is_integer(I) -> escape_attr(integer_to_list(I), []); escape_attr(F) when is_float(F) -> escape_attr(mochinum:digits(F), []). to_html([], Acc) -> lists:reverse(Acc); to_html([{'=', Content} | Rest], Acc) -> to_html(Rest, [Content | Acc]); to_html([{pi, Bin} | Rest], Acc) -> Open = [<<">, Bin, <<"?>">>], to_html(Rest, [Open | Acc]); to_html([{pi, Tag, Attrs} | Rest], Acc) -> Open = [<<">, Tag, attrs_to_html(Attrs, []), <<"?>">>], to_html(Rest, [Open | Acc]); to_html([{comment, Comment} | Rest], Acc) -> to_html(Rest, [[<<"">>] | Acc]); to_html([{doctype, Parts} | Rest], Acc) -> Inside = doctype_to_html(Parts, Acc), to_html(Rest, [[<<">, Inside, <<">">>] | Acc]); to_html([{data, Data, _Whitespace} | Rest], Acc) -> to_html(Rest, [escape(Data) | Acc]); to_html([{start_tag, Tag, Attrs, Singleton} | Rest], Acc) -> Open = [<<"<">>, Tag, attrs_to_html(Attrs, []), case Singleton of true -> <<" />">>; false -> <<">">> end], to_html(Rest, [Open | Acc]); to_html([{end_tag, Tag} | Rest], Acc) -> to_html(Rest, [[<<">, Tag, <<">">>] | Acc]). doctype_to_html([], Acc) -> lists:reverse(Acc); doctype_to_html([Word | Rest], Acc) -> case lists:all(fun (C) -> ?IS_LITERAL_SAFE(C) end, binary_to_list(iolist_to_binary(Word))) of true -> doctype_to_html(Rest, [[<<" ">>, Word] | Acc]); false -> doctype_to_html(Rest, [[<<" \"">>, escape_attr(Word), ?QUOTE] | Acc]) end. attrs_to_html([], Acc) -> lists:reverse(Acc); attrs_to_html([{K, V} | Rest], Acc) -> attrs_to_html(Rest, [[<<" ">>, escape(K), <<"=\"">>, escape_attr(V), <<"\"">>] | Acc]). escape([], Acc) -> list_to_binary(lists:reverse(Acc)); escape("<" ++ Rest, Acc) -> escape(Rest, lists:reverse("<", Acc)); escape(">" ++ Rest, Acc) -> escape(Rest, lists:reverse(">", Acc)); escape("&" ++ Rest, Acc) -> escape(Rest, lists:reverse("&", Acc)); escape([C | Rest], Acc) -> escape(Rest, [C | Acc]). escape_attr([], Acc) -> list_to_binary(lists:reverse(Acc)); escape_attr("<" ++ Rest, Acc) -> escape_attr(Rest, lists:reverse("<", Acc)); escape_attr(">" ++ Rest, Acc) -> escape_attr(Rest, lists:reverse(">", Acc)); escape_attr("&" ++ Rest, Acc) -> escape_attr(Rest, lists:reverse("&", Acc)); escape_attr([?QUOTE | Rest], Acc) -> escape_attr(Rest, lists:reverse(""", Acc)); escape_attr([C | Rest], Acc) -> escape_attr(Rest, [C | Acc]). to_tag(A) when is_atom(A) -> norm(atom_to_list(A)); to_tag(L) -> norm(L). to_tokens([], Acc) -> lists:reverse(Acc); to_tokens([{Tag, []} | Rest], Acc) -> to_tokens(Rest, [{end_tag, to_tag(Tag)} | Acc]); to_tokens([{Tag0, [{T0} | R1]} | Rest], Acc) -> %% Allow {br} to_tokens([{Tag0, [{T0, [], []} | R1]} | Rest], Acc); to_tokens([{Tag0, [T0={'=', _C0} | R1]} | Rest], Acc) -> %% Allow {'=', iolist()} to_tokens([{Tag0, R1} | Rest], [T0 | Acc]); to_tokens([{Tag0, [T0={comment, _C0} | R1]} | Rest], Acc) -> %% Allow {comment, iolist()} to_tokens([{Tag0, R1} | Rest], [T0 | Acc]); to_tokens([{Tag0, [T0={pi, _S0} | R1]} | Rest], Acc) -> %% Allow {pi, binary()} to_tokens([{Tag0, R1} | Rest], [T0 | Acc]); to_tokens([{Tag0, [T0={pi, _S0, _A0} | R1]} | Rest], Acc) -> %% Allow {pi, binary(), list()} to_tokens([{Tag0, R1} | Rest], [T0 | Acc]); to_tokens([{Tag0, [{T0, A0=[{_, _} | _]} | R1]} | Rest], Acc) -> %% Allow {p, [{"class", "foo"}]} to_tokens([{Tag0, [{T0, A0, []} | R1]} | Rest], Acc); to_tokens([{Tag0, [{T0, C0} | R1]} | Rest], Acc) -> %% Allow {p, "content"} and {p, <<"content">>} to_tokens([{Tag0, [{T0, [], C0} | R1]} | Rest], Acc); to_tokens([{Tag0, [{T0, A1, C0} | R1]} | Rest], Acc) when is_binary(C0) -> %% Allow {"p", [{"class", "foo"}], <<"content">>} to_tokens([{Tag0, [{T0, A1, binary_to_list(C0)} | R1]} | Rest], Acc); to_tokens([{Tag0, [{T0, A1, C0=[C | _]} | R1]} | Rest], Acc) when is_integer(C) -> %% Allow {"p", [{"class", "foo"}], "content"} to_tokens([{Tag0, [{T0, A1, [C0]} | R1]} | Rest], Acc); to_tokens([{Tag0, [{T0, A1, C1} | R1]} | Rest], Acc) -> %% Native {"p", [{"class", "foo"}], ["content"]} Tag = to_tag(Tag0), T1 = to_tag(T0), case is_singleton(norm(T1)) of true -> to_tokens([{Tag, R1} | Rest], [{start_tag, T1, A1, true} | Acc]); false -> to_tokens([{T1, C1}, {Tag, R1} | Rest], [{start_tag, T1, A1, false} | Acc]) end; to_tokens([{Tag0, [L | R1]} | Rest], Acc) when is_list(L) -> %% List text Tag = to_tag(Tag0), to_tokens([{Tag, R1} | Rest], [{data, iolist_to_binary(L), false} | Acc]); to_tokens([{Tag0, [B | R1]} | Rest], Acc) when is_binary(B) -> %% Binary text Tag = to_tag(Tag0), to_tokens([{Tag, R1} | Rest], [{data, B, false} | Acc]). tokens(B, S=#decoder{offset=O}, Acc) -> case B of <<_:O/binary>> -> lists:reverse(Acc); _ -> {Tag, S1} = tokenize(B, S), case parse_flag(Tag) of script -> {Tag2, S2} = tokenize_script(B, S1), tokens(B, S2, [Tag2, Tag | Acc]); textarea -> {Tag2, S2} = tokenize_textarea(B, S1), tokens(B, S2, [Tag2, Tag | Acc]); none -> tokens(B, S1, [Tag | Acc]) end end. parse_flag({start_tag, B, _, false}) -> case string:to_lower(binary_to_list(B)) of "script" -> script; "textarea" -> textarea; _ -> none end; parse_flag(_) -> none. tokenize(B, S=#decoder{offset=O}) -> case B of <<_:O/binary, "", _/binary>> -> Len = O - Start, <<_:Start/binary, Raw:Len/binary, _/binary>> = Bin, {{comment, Raw}, ?ADV_COL(S, 3)}; <<_:O/binary, C, _/binary>> -> tokenize_comment(Bin, ?INC_CHAR(S, C), Start); <<_:Start/binary, Raw/binary>> -> {{comment, Raw}, S} end. tokenize_script(Bin, S=#decoder{offset=O}) -> tokenize_script(Bin, S, O). tokenize_script(Bin, S=#decoder{offset=O}, Start) -> case Bin of %% Just a look-ahead, we want the end_tag separately <<_:O/binary, $<, $/, SS, CC, RR, II, PP, TT, ZZ, _/binary>> when (SS =:= $s orelse SS =:= $S) andalso (CC =:= $c orelse CC =:= $C) andalso (RR =:= $r orelse RR =:= $R) andalso (II =:= $i orelse II =:= $I) andalso (PP =:= $p orelse PP =:= $P) andalso (TT=:= $t orelse TT =:= $T) andalso ?PROBABLE_CLOSE(ZZ) -> Len = O - Start, <<_:Start/binary, Raw:Len/binary, _/binary>> = Bin, {{data, Raw, false}, S}; <<_:O/binary, C, _/binary>> -> tokenize_script(Bin, ?INC_CHAR(S, C), Start); <<_:Start/binary, Raw/binary>> -> {{data, Raw, false}, S} end. tokenize_textarea(Bin, S=#decoder{offset=O}) -> tokenize_textarea(Bin, S, O). tokenize_textarea(Bin, S=#decoder{offset=O}, Start) -> case Bin of %% Just a look-ahead, we want the end_tag separately <<_:O/binary, $<, $/, TT, EE, XX, TT2, AA, RR, EE2, AA2, ZZ, _/binary>> when (TT =:= $t orelse TT =:= $T) andalso (EE =:= $e orelse EE =:= $E) andalso (XX =:= $x orelse XX =:= $X) andalso (TT2 =:= $t orelse TT2 =:= $T) andalso (AA =:= $a orelse AA =:= $A) andalso (RR =:= $r orelse RR =:= $R) andalso (EE2 =:= $e orelse EE2 =:= $E) andalso (AA2 =:= $a orelse AA2 =:= $A) andalso ?PROBABLE_CLOSE(ZZ) -> Len = O - Start, <<_:Start/binary, Raw:Len/binary, _/binary>> = Bin, {{data, Raw, false}, S}; <<_:O/binary, C, _/binary>> -> tokenize_textarea(Bin, ?INC_CHAR(S, C), Start); <<_:Start/binary, Raw/binary>> -> {{data, Raw, false}, S} end. mochiweb-3.2.1/src/mochiweb_http.erl000066400000000000000000000261611450333227400174520ustar00rootroot00000000000000%% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc HTTP server. -module(mochiweb_http). -author('bob@mochimedia.com'). -export([start/1, start_link/1, stop/0, stop/1]). -export([loop/3]). -export([after_response/2, reentry/1]). -export([parse_range_request/1, range_skip_length/2]). -define(REQUEST_RECV_TIMEOUT, 300000). %% timeout waiting for request line -define(HEADERS_RECV_TIMEOUT, 30000). %% timeout waiting for headers -define(MAX_HEADERS, 1000). -define(DEFAULTS, [{name, ?MODULE}, {port, 8888}]). parse_options(Options) -> {loop, HttpLoop} = proplists:lookup(loop, Options), Loop = {?MODULE, loop, [HttpLoop]}, Options1 = [{loop, Loop} | proplists:delete(loop, Options)], mochilists:set_defaults(?DEFAULTS, Options1). stop() -> mochiweb_socket_server:stop(?MODULE). stop(Name) -> mochiweb_socket_server:stop(Name). %% @spec start(Options) -> ServerRet %% Options = [option()] %% Option = {name, atom()} | {ip, string() | tuple()} | {backlog, integer()} %% | {nodelay, boolean()} | {acceptor_pool_size, integer()} %% | {ssl, boolean()} | {profile_fun, undefined | (Props) -> ok} %% | {link, false} | {recbuf, undefined | non_negative_integer()} %% @doc Start a mochiweb server. %% profile_fun is used to profile accept timing. %% After each accept, if defined, profile_fun is called with a proplist of a subset of the mochiweb_socket_server state and timing information. %% The proplist is as follows: [{name, Name}, {port, Port}, {active_sockets, ActiveSockets}, {timing, Timing}]. %% @end start(Options) -> ok = ensure_started(mochiweb_clock), mochiweb_socket_server:start(parse_options(Options)). start_link(Options) -> ok = ensure_started(mochiweb_clock), mochiweb_socket_server:start_link(parse_options(Options)). ensure_started(M) when is_atom(M) -> case M:start() of {ok, _Pid} -> ok; {error, {already_started, _Pid}} -> ok end. loop(Socket, Opts, Body) -> ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, http}])), request(Socket, Opts, Body). request(Socket, Opts, Body) -> ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{active, once}])), receive {Protocol, _, {http_request, Method, Path, Version}} when Protocol == http orelse Protocol == ssl -> ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, httph}])), headers(Socket, Opts, {Method, Path, Version}, [], Body, 0); {Protocol, _, {http_error, "\r\n"}} when Protocol == http orelse Protocol == ssl -> request(Socket, Opts, Body); {Protocol, _, {http_error, "\n"}} when Protocol == http orelse Protocol == ssl -> request(Socket, Opts, Body); {tcp_closed = Error, _} -> mochiweb_socket:close(Socket), exit({shutdown, Error}); {tcp_error, _, emsgsize} -> handle_invalid_request(Socket, Opts); {ssl_closed = Error, _} -> mochiweb_socket:close(Socket), exit({shutdown, Error}) after ?REQUEST_RECV_TIMEOUT -> mochiweb_socket:close(Socket), exit({shutdown, request_recv_timeout}) end. reentry(Body) -> fun (Req) -> (?MODULE):after_response(Body, Req) end. headers(Socket, Opts, Request, Headers, _Body, ?MAX_HEADERS) -> %% Too many headers sent, bad request. ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, raw}])), handle_invalid_request(Socket, Opts, Request, Headers); headers(Socket, Opts, Request, Headers, Body, HeaderCount) -> ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{active, once}])), receive {Protocol, _, http_eoh} when Protocol == http orelse Protocol == ssl -> Req = new_request(Socket, Opts, Request, Headers), call_body(Body, Req), (?MODULE):after_response(Body, Req); {Protocol, _, {http_header, _, Name, _, Value}} when Protocol == http orelse Protocol == ssl -> headers(Socket, Opts, Request, [{Name, Value} | Headers], Body, 1 + HeaderCount); {tcp_closed = Error, _} -> mochiweb_socket:close(Socket), exit({shutdown, Error}); {tcp_error, _, emsgsize} -> handle_invalid_request(Socket, Opts, Request, Headers) after ?HEADERS_RECV_TIMEOUT -> mochiweb_socket:close(Socket), exit({shutdown, headers_recv_timeout}) end. call_body({M, F, A}, Req) when is_atom(M) -> erlang:apply(M, F, [Req | A]); call_body({M, F}, Req) when is_atom(M) -> M:F(Req); call_body(Body, Req) -> Body(Req). -spec handle_invalid_request(term(), term()) -> no_return(). handle_invalid_request(Socket, Opts) -> handle_invalid_request(Socket, Opts, {'GET', {abs_path, "/"}, {0, 9}}, []). -spec handle_invalid_request(term(), term(), term(), term()) -> no_return(). handle_invalid_request(Socket, Opts, Request, RevHeaders) -> {ReqM, _} = Req = new_request(Socket, Opts, Request, RevHeaders), ReqM:respond({400, [], []}, Req), mochiweb_socket:close(Socket), exit({shutdown, invalid_request}). new_request(Socket, Opts, Request, RevHeaders) -> ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, raw}])), mochiweb:new_request({Socket, Opts, Request, lists:reverse(RevHeaders)}). after_response(Body, {ReqM, _} = Req) -> Socket = ReqM:get(socket, Req), case ReqM:should_close(Req) of true -> mochiweb_socket:close(Socket), exit({shutdown, should_close}); false -> ReqM:cleanup(Req), erlang:garbage_collect(), (?MODULE):loop(Socket, mochiweb_request:get(opts, Req), Body) end. parse_range_request(RawRange) when is_list(RawRange) -> try "bytes=" ++ RangeString = RawRange, RangeTokens = [string:strip(R) || R <- string:tokens(RangeString, ",")], Ranges = [R || R <- RangeTokens, string:len(R) > 0], [parse_range_request_1(V1) || V1 <- Ranges] catch _:_ -> fail end. parse_range_request_1("-" ++ V) -> {none, list_to_integer(V)}; parse_range_request_1(R) -> case string:tokens(R, "-") of [S1, S2] -> {list_to_integer(S1), list_to_integer(S2)}; [S] -> {list_to_integer(S), none} end. range_skip_length(Spec, Size) -> case Spec of {none, R} when R =< Size, R >= 0 -> {Size - R, R}; {none, _OutOfRange} -> {0, Size}; {R, none} when R >= 0, R < Size -> {R, Size - R}; {_OutOfRange, none} -> invalid_range; {Start, End} when Start >= 0, Start < Size, Start =< End -> {Start, erlang:min(End + 1, Size) - Start}; {_InvalidStart, _InvalidEnd} -> invalid_range end. %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). range_test() -> %% valid, single ranges ?assertEqual([{20, 30}], (parse_range_request("bytes=20-30"))), ?assertEqual([{20, none}], (parse_range_request("bytes=20-"))), ?assertEqual([{none, 20}], (parse_range_request("bytes=-20"))), %% trivial single range ?assertEqual([{0, none}], (parse_range_request("bytes=0-"))), %% invalid, single ranges ?assertEqual(fail, (parse_range_request(""))), ?assertEqual(fail, (parse_range_request("garbage"))), ?assertEqual(fail, (parse_range_request("bytes=-20-30"))), %% valid, multiple range ?assertEqual([{20, 30}, {50, 100}, {110, 200}], (parse_range_request("bytes=20-30,50-100,110-200"))), ?assertEqual([{20, none}, {50, 100}, {none, 200}], (parse_range_request("bytes=20-,50-100,-200"))), %% valid, multiple range with whitespace ?assertEqual([{20, 30}, {50, 100}, {110, 200}], (parse_range_request("bytes=20-30, 50-100 , 110-200"))), %% valid, multiple range with extra commas ?assertEqual([{20, 30}, {50, 100}, {110, 200}], (parse_range_request("bytes=20-30,,50-100,110-200"))), ?assertEqual([{20, 30}, {50, 100}, {110, 200}], (parse_range_request("bytes=20-30, ,50-100,,,110-200"))), %% no ranges ?assertEqual([], (parse_range_request("bytes="))), ok. range_skip_length_test() -> Body = <<"012345678901234567890123456789012345678901234" "567890123456789">>, BodySize = byte_size(Body), %% 60 BodySize = 60, %% these values assume BodySize =:= 60 ?assertEqual({1, 9}, (range_skip_length({1, 9}, BodySize))), %% 1-9 ?assertEqual({10, 10}, (range_skip_length({10, 19}, BodySize))), %% 10-19 ?assertEqual({40, 20}, (range_skip_length({none, 20}, BodySize))), %% -20 ?assertEqual({30, 30}, (range_skip_length({30, none}, BodySize))), %% 30- %% valid edge cases for range_skip_length ?assertEqual({BodySize, 0}, (range_skip_length({none, 0}, BodySize))), ?assertEqual({0, BodySize}, (range_skip_length({none, BodySize}, BodySize))), ?assertEqual({0, BodySize}, (range_skip_length({0, none}, BodySize))), ?assertEqual({0, BodySize}, (range_skip_length({0, BodySize + 1}, BodySize))), 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({-1, BodySize + 1}, BodySize))), ?assertEqual(invalid_range, (range_skip_length({BodySize, 40}, BodySize))), ?assertEqual(invalid_range, (range_skip_length({-1, none}, BodySize))), ?assertEqual(invalid_range, (range_skip_length({BodySize, none}, BodySize))), ?assertEqual(invalid_range, (range_skip_length({BodySize + 1, BodySize + 5}, BodySize))), ok. -endif. mochiweb-3.2.1/src/mochiweb_io.erl000066400000000000000000000042121450333227400170730ustar00rootroot00000000000000%% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc Utilities for dealing with IO devices (open files). -module(mochiweb_io). -author('bob@mochimedia.com'). -export([iodevice_stream/3, iodevice_stream/2]). -export([iodevice_foldl/4, iodevice_foldl/3]). -export([iodevice_size/1]). -define(READ_SIZE, 8192). iodevice_foldl(F, Acc, IoDevice) -> iodevice_foldl(F, Acc, IoDevice, ?READ_SIZE). iodevice_foldl(F, Acc, IoDevice, BufferSize) -> case file:read(IoDevice, BufferSize) of eof -> Acc; {ok, Data} -> iodevice_foldl(F, F(Data, Acc), IoDevice, BufferSize) end. iodevice_stream(Callback, IoDevice) -> iodevice_stream(Callback, IoDevice, ?READ_SIZE). iodevice_stream(Callback, IoDevice, BufferSize) -> F = fun (Data, ok) -> Callback(Data) end, ok = iodevice_foldl(F, ok, IoDevice, BufferSize). iodevice_size(IoDevice) -> {ok, Size} = file:position(IoDevice, eof), {ok, 0} = file:position(IoDevice, bof), Size. %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -endif. mochiweb-3.2.1/src/mochiweb_mime.erl000066400000000000000000000265001450333227400174170ustar00rootroot00000000000000%% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc Gives a good MIME type guess based on file extension. -module(mochiweb_mime). -author('bob@mochimedia.com'). -export([from_extension/1]). %% @spec from_extension(S::string()) -> string() | undefined %% @doc Given a filename extension (e.g. ".html") return a guess for the MIME %% type such as "text/html". Will return the atom undefined if no good %% guess is available. from_extension(".stl") -> "application/SLA"; from_extension(".stp") -> "application/STEP"; from_extension(".step") -> "application/STEP"; from_extension(".dwg") -> "application/acad"; from_extension(".ez") -> "application/andrew-inset"; from_extension(".ccad") -> "application/clariscad"; from_extension(".drw") -> "application/drafting"; from_extension(".tsp") -> "application/dsptype"; from_extension(".dxf") -> "application/dxf"; from_extension(".xls") -> "application/excel"; from_extension(".unv") -> "application/i-deas"; from_extension(".jar") -> "application/java-archive"; from_extension(".hqx") -> "application/mac-binhex40"; from_extension(".cpt") -> "application/mac-compactpro"; from_extension(".pot") -> "application/vnd.ms-powerpoint"; from_extension(".ppt") -> "application/vnd.ms-powerpoint"; from_extension(".dms") -> "application/octet-stream"; from_extension(".lha") -> "application/octet-stream"; from_extension(".lzh") -> "application/octet-stream"; from_extension(".oda") -> "application/oda"; from_extension(".ogg") -> "application/ogg"; from_extension(".ogm") -> "application/ogg"; from_extension(".pdf") -> "application/pdf"; from_extension(".pgp") -> "application/pgp"; from_extension(".ai") -> "application/postscript"; from_extension(".eps") -> "application/postscript"; from_extension(".ps") -> "application/postscript"; from_extension(".prt") -> "application/pro_eng"; from_extension(".rtf") -> "application/rtf"; from_extension(".smi") -> "application/smil"; from_extension(".smil") -> "application/smil"; from_extension(".sol") -> "application/solids"; from_extension(".vda") -> "application/vda"; from_extension(".xlm") -> "application/vnd.ms-excel"; from_extension(".cod") -> "application/vnd.rim.cod"; from_extension(".pgn") -> "application/x-chess-pgn"; from_extension(".cpio") -> "application/x-cpio"; from_extension(".csh") -> "application/x-csh"; from_extension(".deb") -> "application/x-debian-package"; from_extension(".dcr") -> "application/x-director"; from_extension(".dir") -> "application/x-director"; from_extension(".dxr") -> "application/x-director"; from_extension(".gz") -> "application/x-gzip"; from_extension(".hdf") -> "application/x-hdf"; from_extension(".ipx") -> "application/x-ipix"; from_extension(".ips") -> "application/x-ipscript"; from_extension(".js") -> "application/x-javascript"; from_extension(".skd") -> "application/x-koan"; from_extension(".skm") -> "application/x-koan"; from_extension(".skp") -> "application/x-koan"; from_extension(".skt") -> "application/x-koan"; from_extension(".latex") -> "application/x-latex"; from_extension(".lsp") -> "application/x-lisp"; from_extension(".scm") -> "application/x-lotusscreencam"; from_extension(".mif") -> "application/x-mif"; from_extension(".com") -> "application/x-msdos-program"; from_extension(".exe") -> "application/octet-stream"; from_extension(".cdf") -> "application/x-netcdf"; from_extension(".nc") -> "application/x-netcdf"; from_extension(".pl") -> "application/x-perl"; from_extension(".pm") -> "application/x-perl"; from_extension(".rar") -> "application/x-rar-compressed"; from_extension(".sh") -> "application/x-sh"; from_extension(".shar") -> "application/x-shar"; from_extension(".swf") -> "application/x-shockwave-flash"; from_extension(".sit") -> "application/x-stuffit"; from_extension(".sv4cpio") -> "application/x-sv4cpio"; from_extension(".sv4crc") -> "application/x-sv4crc"; from_extension(".tar.gz") -> "application/x-tar-gz"; from_extension(".tgz") -> "application/x-tar-gz"; from_extension(".tar") -> "application/x-tar"; from_extension(".tcl") -> "application/x-tcl"; from_extension(".texi") -> "application/x-texinfo"; from_extension(".texinfo") -> "application/x-texinfo"; from_extension(".man") -> "application/x-troff-man"; from_extension(".me") -> "application/x-troff-me"; from_extension(".ms") -> "application/x-troff-ms"; from_extension(".roff") -> "application/x-troff"; from_extension(".t") -> "application/x-troff"; from_extension(".tr") -> "application/x-troff"; from_extension(".ustar") -> "application/x-ustar"; from_extension(".src") -> "application/x-wais-source"; from_extension(".zip") -> "application/zip"; from_extension(".tsi") -> "audio/TSP-audio"; from_extension(".au") -> "audio/basic"; from_extension(".snd") -> "audio/basic"; from_extension(".kar") -> "audio/midi"; from_extension(".mid") -> "audio/midi"; from_extension(".midi") -> "audio/midi"; from_extension(".mp2") -> "audio/mpeg"; from_extension(".mp3") -> "audio/mpeg"; from_extension(".mpga") -> "audio/mpeg"; from_extension(".aif") -> "audio/x-aiff"; from_extension(".aifc") -> "audio/x-aiff"; from_extension(".aiff") -> "audio/x-aiff"; from_extension(".m3u") -> "audio/x-mpegurl"; from_extension(".wax") -> "audio/x-ms-wax"; from_extension(".wma") -> "audio/x-ms-wma"; from_extension(".rpm") -> "audio/x-pn-realaudio-plugin"; from_extension(".ram") -> "audio/x-pn-realaudio"; from_extension(".rm") -> "audio/x-pn-realaudio"; from_extension(".ra") -> "audio/x-realaudio"; from_extension(".wav") -> "audio/x-wav"; from_extension(".pdb") -> "chemical/x-pdb"; from_extension(".ras") -> "image/cmu-raster"; from_extension(".gif") -> "image/gif"; from_extension(".ief") -> "image/ief"; from_extension(".jpe") -> "image/jpeg"; from_extension(".jpeg") -> "image/jpeg"; from_extension(".jpg") -> "image/jpeg"; from_extension(".jp2") -> "image/jp2"; from_extension(".png") -> "image/png"; from_extension(".tif") -> "image/tiff"; from_extension(".tiff") -> "image/tiff"; from_extension(".pnm") -> "image/x-portable-anymap"; from_extension(".pbm") -> "image/x-portable-bitmap"; from_extension(".pgm") -> "image/x-portable-graymap"; from_extension(".ppm") -> "image/x-portable-pixmap"; from_extension(".rgb") -> "image/x-rgb"; from_extension(".xbm") -> "image/x-xbitmap"; from_extension(".xwd") -> "image/x-xwindowdump"; from_extension(".iges") -> "model/iges"; from_extension(".igs") -> "model/iges"; from_extension(".mesh") -> "model/mesh"; from_extension(".") -> ""; from_extension(".msh") -> "model/mesh"; from_extension(".silo") -> "model/mesh"; from_extension(".vrml") -> "model/vrml"; from_extension(".wrl") -> "model/vrml"; from_extension(".css") -> "text/css"; from_extension(".htm") -> "text/html"; from_extension(".html") -> "text/html"; from_extension(".asc") -> "text/plain"; from_extension(".c") -> "text/plain"; from_extension(".cc") -> "text/plain"; from_extension(".f90") -> "text/plain"; from_extension(".f") -> "text/plain"; from_extension(".hh") -> "text/plain"; from_extension(".m") -> "text/plain"; from_extension(".txt") -> "text/plain"; from_extension(".rtx") -> "text/richtext"; from_extension(".sgm") -> "text/sgml"; from_extension(".sgml") -> "text/sgml"; from_extension(".tsv") -> "text/tab-separated-values"; from_extension(".jad") -> "text/vnd.sun.j2me.app-descriptor"; from_extension(".etx") -> "text/x-setext"; from_extension(".xml") -> "application/xml"; from_extension(".dl") -> "video/dl"; from_extension(".fli") -> "video/fli"; from_extension(".flv") -> "video/x-flv"; from_extension(".gl") -> "video/gl"; from_extension(".mp4") -> "video/mp4"; from_extension(".mpe") -> "video/mpeg"; from_extension(".mpeg") -> "video/mpeg"; from_extension(".mpg") -> "video/mpeg"; from_extension(".mov") -> "video/quicktime"; from_extension(".qt") -> "video/quicktime"; from_extension(".viv") -> "video/vnd.vivo"; from_extension(".vivo") -> "video/vnd.vivo"; from_extension(".asf") -> "video/x-ms-asf"; from_extension(".asx") -> "video/x-ms-asx"; from_extension(".wmv") -> "video/x-ms-wmv"; from_extension(".wmx") -> "video/x-ms-wmx"; from_extension(".wvx") -> "video/x-ms-wvx"; from_extension(".avi") -> "video/x-msvideo"; from_extension(".movie") -> "video/x-sgi-movie"; from_extension(".mime") -> "www/mime"; from_extension(".ice") -> "x-conference/x-cooltalk"; from_extension(".vrm") -> "x-world/x-vrml"; from_extension(".spx") -> "audio/ogg"; from_extension(".xhtml") -> "application/xhtml+xml"; from_extension(".bz2") -> "application/x-bzip2"; from_extension(".doc") -> "application/msword"; from_extension(".z") -> "application/x-compress"; from_extension(".ico") -> "image/x-icon"; from_extension(".bmp") -> "image/bmp"; from_extension(".m4a") -> "audio/mpeg"; from_extension(".csv") -> "text/csv"; from_extension(".eot") -> "application/vnd.ms-fontobject"; from_extension(".m4v") -> "video/mp4"; from_extension(".svg") -> "image/svg+xml"; from_extension(".svgz") -> "image/svg+xml"; from_extension(".ttc") -> "application/x-font-ttf"; from_extension(".ttf") -> "application/x-font-ttf"; from_extension(".vcf") -> "text/x-vcard"; from_extension(".webm") -> "video/web"; from_extension(".webp") -> "image/web"; from_extension(".woff") -> "application/x-font-woff"; from_extension(".otf") -> "font/opentype"; from_extension(_) -> undefined. %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). exhaustive_from_extension_test() -> T = mochiweb_cover:clause_lookup_table(?MODULE, from_extension), [?assertEqual(V, from_extension(K)) || {K, V} <- T]. from_extension_test() -> ?assertEqual("text/html", from_extension(".html")), ?assertEqual(undefined, from_extension("")), ?assertEqual(undefined, from_extension(".wtf")), ok. -endif. mochiweb-3.2.1/src/mochiweb_multipart.erl000066400000000000000000001000011450333227400204760ustar00rootroot00000000000000%% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc Utilities for parsing multipart/form-data. -module(mochiweb_multipart). -author('bob@mochimedia.com'). -export([parse_form/1, parse_form/2]). -export([parse_multipart_request/2]). -export([parts_to_body/3, parts_to_multipart_body/4]). -export([default_file_handler/2]). -define(CHUNKSIZE, 4096). -record(mp, {state, boundary, length, buffer, callback, req}). %% TODO: DOCUMENT THIS MODULE. %% @type key() = atom() | string() | binary(). %% @type value() = atom() | iolist() | integer(). %% @type header() = {key(), value()}. %% @type bodypart() = {Start::integer(), End::integer(), Body::iolist()}. %% @type formfile() = {Name::string(), ContentType::string(), Content::binary()}. %% @type request(). %% @type file_handler() = (Filename::string(), ContentType::string()) -> file_handler_callback(). %% @type file_handler_callback() = (binary() | eof) -> file_handler_callback() | term(). %% @spec parts_to_body([bodypart()], ContentType::string(), %% Size::integer()) -> {[header()], iolist()} %% @doc Return {[header()], iolist()} representing the body for the given %% parts, may be a single part or multipart. parts_to_body([{Start, End, Body}], ContentType, Size) -> HeaderList = [{"Content-Type", ContentType}, {"Content-Range", ["bytes ", mochiweb_util:make_io(Start), "-", mochiweb_util:make_io(End), "/", mochiweb_util:make_io(Size)]}], {HeaderList, Body}; parts_to_body(BodyList, ContentType, Size) when is_list(BodyList) -> parts_to_multipart_body(BodyList, ContentType, Size, mochihex:to_hex(crypto:strong_rand_bytes(8))). %% @spec parts_to_multipart_body([bodypart()], ContentType::string(), %% Size::integer(), Boundary::string()) -> %% {[header()], iolist()} %% @doc Return {[header()], iolist()} representing the body for the given %% parts, always a multipart response. parts_to_multipart_body(BodyList, ContentType, Size, Boundary) -> HeaderList = [{"Content-Type", ["multipart/byteranges; ", "boundary=", Boundary]}], MultiPartBody = multipart_body(BodyList, ContentType, Boundary, Size), {HeaderList, MultiPartBody}. %% @spec multipart_body([bodypart()], ContentType::string(), %% Boundary::string(), Size::integer()) -> iolist() %% @doc Return the representation of a multipart body for the given [bodypart()]. multipart_body([], _ContentType, Boundary, _Size) -> ["--", Boundary, "--\r\n"]; multipart_body([{Start, End, Body} | BodyList], ContentType, Boundary, Size) -> ["--", Boundary, "\r\n", "Content-Type: ", ContentType, "\r\n", "Content-Range: ", "bytes ", mochiweb_util:make_io(Start), "-", mochiweb_util:make_io(End), "/", mochiweb_util:make_io(Size), "\r\n\r\n", Body, "\r\n" | multipart_body(BodyList, ContentType, Boundary, Size)]. %% @spec parse_form(request()) -> [{string(), string() | formfile()}] %% @doc Parse a multipart form from the given request using the in-memory %% default_file_handler/2. parse_form(Req) -> parse_form(Req, fun default_file_handler/2). %% @spec parse_form(request(), F::file_handler()) -> [{string(), string() | term()}] %% @doc Parse a multipart form from the given request using the given file_handler(). parse_form(Req, FileHandler) -> Callback = fun (Next) -> parse_form_outer(Next, FileHandler, []) end, {_, _, Res} = parse_multipart_request(Req, Callback), Res. parse_form_outer(eof, _, Acc) -> lists:reverse(Acc); parse_form_outer({headers, H}, FileHandler, State) -> {"form-data", H1} = proplists:get_value("content-disposition", H), Name = proplists:get_value("name", H1), Filename = proplists:get_value("filename", H1), case Filename of undefined -> fun (Next) -> parse_form_value(Next, {Name, []}, FileHandler, State) end; _ -> ContentType = proplists:get_value("content-type", H), Handler = FileHandler(Filename, ContentType), fun (Next) -> parse_form_file(Next, {Name, Handler}, FileHandler, State) end end. parse_form_value(body_end, {Name, Acc}, FileHandler, State) -> Value = binary_to_list(iolist_to_binary(lists:reverse(Acc))), State1 = [{Name, Value} | State], fun (Next) -> parse_form_outer(Next, FileHandler, State1) end; parse_form_value({body, Data}, {Name, Acc}, FileHandler, State) -> Acc1 = [Data | Acc], fun (Next) -> parse_form_value(Next, {Name, Acc1}, FileHandler, State) end. parse_form_file(body_end, {Name, Handler}, FileHandler, State) -> Value = Handler(eof), State1 = [{Name, Value} | State], fun (Next) -> parse_form_outer(Next, FileHandler, State1) end; parse_form_file({body, Data}, {Name, Handler}, FileHandler, State) -> H1 = Handler(Data), fun (Next) -> parse_form_file(Next, {Name, H1}, FileHandler, State) end. default_file_handler(Filename, ContentType) -> default_file_handler_1(Filename, ContentType, []). default_file_handler_1(Filename, ContentType, Acc) -> fun (eof) -> Value = iolist_to_binary(lists:reverse(Acc)), {Filename, ContentType, Value}; (Next) -> default_file_handler_1(Filename, ContentType, [Next | Acc]) end. parse_multipart_request({ReqM, _} = Req, Callback) -> %% TODO: Support chunked? Length = list_to_integer(ReqM:get_combined_header_value("content-length", Req)), Boundary = iolist_to_binary(get_boundary(ReqM:get_header_value("content-type", Req))), Prefix = <<"\r\n--", Boundary/binary>>, BS = byte_size(Boundary), Chunk = read_chunk(Req, Length), Length1 = Length - byte_size(Chunk), <<"--", Boundary:BS/binary, "\r\n", Rest/binary>> = Chunk, feed_mp(headers, flash_multipart_hack(#mp{boundary = Prefix, length = Length1, buffer = Rest, callback = Callback, req = Req})). parse_headers(<<>>) -> []; parse_headers(Binary) -> parse_headers(Binary, []). parse_headers(Binary, Acc) -> case find_in_binary(<<"\r\n">>, Binary) of {exact, N} -> <> = Binary, parse_headers(Rest, [split_header(Line) | Acc]); not_found -> lists:reverse([split_header(Binary) | Acc]) end. split_header(Line) -> {Name, [$: | Value]} = lists:splitwith(fun (C) -> C =/= $: end, binary_to_list(Line)), {string:to_lower(string:strip(Name)), mochiweb_util:parse_header(Value)}. read_chunk({ReqM, _} = Req, Length) when Length > 0 -> case Length of Length when Length < (?CHUNKSIZE) -> ReqM:recv(Length, Req); _ -> ReqM:recv(?CHUNKSIZE, Req) end. read_more(State = #mp{length = Length, buffer = Buffer, req = Req}) -> Data = read_chunk(Req, Length), Buffer1 = <>, flash_multipart_hack(State#mp{length = Length - byte_size(Data), buffer = Buffer1}). flash_multipart_hack(State = #mp{length = 0, buffer = Buffer, boundary = Prefix}) -> %% http://code.google.com/p/mochiweb/issues/detail?id=22 %% Flash doesn't terminate multipart with \r\n properly so we fix it up here PrefixSize = size(Prefix), case size(Buffer) - (2 + PrefixSize) of Seek when Seek >= 0 -> case Buffer of <<_:Seek/binary, Prefix:PrefixSize/binary, "--">> -> Buffer1 = <>, State#mp{buffer = Buffer1}; _ -> State end; _ -> State end; flash_multipart_hack(State) -> State. feed_mp(headers, State = #mp{buffer = Buffer, callback = Callback}) -> {State1, P} = case find_in_binary(<<"\r\n\r\n">>, Buffer) of {exact, N} -> {State, N}; _ -> S1 = read_more(State), %% Assume headers must be less than ?CHUNKSIZE {exact, N} = find_in_binary(<<"\r\n\r\n">>, S1#mp.buffer), {S1, N} end, <> = State1#mp.buffer, NextCallback = Callback({headers, parse_headers(Headers)}), feed_mp(body, State1#mp{buffer = Rest, callback = NextCallback}); feed_mp(body, State = #mp{boundary = Prefix, buffer = Buffer, callback = Callback}) -> Boundary = find_boundary(Prefix, Buffer), case Boundary of {end_boundary, Start, Skip} -> <> = Buffer, C1 = Callback({body, Data}), C2 = C1(body_end), {State#mp.length, Rest, C2(eof)}; {next_boundary, Start, Skip} -> <> = Buffer, C1 = Callback({body, Data}), feed_mp(headers, State#mp{callback = C1(body_end), buffer = Rest}); {maybe, Start} -> <> = Buffer, feed_mp(body, read_more(State#mp{callback = Callback({body, Data}), buffer = Rest})); not_found -> {Data, Rest} = {Buffer, <<>>}, feed_mp(body, read_more(State#mp{callback = Callback({body, Data}), buffer = Rest})) end. get_boundary(ContentType) -> {"multipart/form-data", Opts} = mochiweb_util:parse_header(ContentType), case proplists:get_value("boundary", Opts) of S when is_list(S) -> S end. %% @spec find_in_binary(Pattern::binary(), Data::binary()) -> %% {exact, N} | {partial, N, K} | not_found %% @doc Searches for the given pattern in the given binary. find_in_binary(P, Data) when size(P) > 0 -> PS = size(P), DS = size(Data), case DS - PS of Last when Last < 0 -> partial_find(P, Data, 0, DS); Last -> case binary:match(Data, P) of {Pos, _} -> {exact, Pos}; nomatch -> partial_find(P, Data, Last + 1, PS - 1) end end. partial_find(_B, _D, _N, 0) -> not_found; partial_find(B, D, N, K) -> <> = B, case D of <<_Skip:N/binary, B1:K/binary>> -> {partial, N, K}; _ -> partial_find(B, D, 1 + N, K - 1) end. find_boundary(Prefix, Data) -> case find_in_binary(Prefix, Data) of {exact, Skip} -> PrefixSkip = Skip + size(Prefix), case Data of <<_:PrefixSkip/binary, "\r\n", _/binary>> -> {next_boundary, Skip, size(Prefix) + 2}; <<_:PrefixSkip/binary, "--\r\n", _/binary>> -> {end_boundary, Skip, size(Prefix) + 4}; _ when size(Data) < PrefixSkip + 4 -> %% Underflow {maybe, Skip}; _ -> %% False positive not_found end; {partial, Skip, Length} when Skip + Length =:= size(Data) -> %% Underflow {maybe, Skip}; _ -> not_found end. %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). ssl_cert_opts() -> {ok, Cwd} = file:get_cwd(), CertDir = filename:join( case filename:basename(Cwd) of %% rebar2 compatibility ".eunit" -> [".."]; _ -> [] end ++ ["support", "test-materials"]), CertFile = filename:join(CertDir, "test_ssl_cert.pem"), KeyFile = filename:join(CertDir, "test_ssl_key.pem"), [{certfile, CertFile}, {keyfile, KeyFile}]. with_socket_server(Transport, ServerFun, ClientFun) -> ServerOpts0 = [{ip, "127.0.0.1"}, {port, 0}, {loop, ServerFun}], ServerOpts = case Transport of plain -> ServerOpts0; ssl -> ServerOpts0 ++ [{ssl, true}, {ssl_opts, ssl_cert_opts()}] end, {ok, Server} = mochiweb_socket_server:start_link(ServerOpts), Port = mochiweb_socket_server:get(Server, port), ClientOpts = [binary, {active, false}], {ok, Client} = case Transport of plain -> gen_tcp:connect("127.0.0.1", Port, ClientOpts); ssl -> ClientOpts1 = mochiweb_test_util:ssl_client_opts(ClientOpts), {ok, SslSocket} = ssl:connect("127.0.0.1", Port, ClientOpts1), {ok, {ssl, SslSocket}} end, Res = (catch ClientFun(Client)), mochiweb_socket_server:stop(Server), Res. fake_request(Socket, ContentType, Length) -> mochiweb_request:new(Socket, 'POST', "/multipart", {1, 1}, mochiweb_headers:make([{"content-type", ContentType}, {"content-length", Length}])). test_callback({body, <<>>}, Rest = [body_end | _]) -> %% When expecting the body_end we might get an empty binary fun (Next) -> test_callback(Next, Rest) end; test_callback({body, Got}, [{body, Expect} | Rest]) when Got =/= Expect -> %% Partial response GotSize = size(Got), <> = Expect, fun (Next) -> test_callback(Next, [{body, Expect1} | Rest]) end; test_callback(Got, [Expect | Rest]) -> ?assertEqual(Got, Expect), case Rest of [] -> ok; _ -> fun (Next) -> test_callback(Next, Rest) end end. parse3_http_test() -> parse3(plain). parse3_https_test() -> parse3(ssl). parse3(Transport) -> ContentType = "multipart/form-data; boundary=---------------" "------------7386909285754635891697677882", BinContent = <<"-----------------------------7386909285754635" "891697677882\r\nContent-Disposition: " "form-data; name=\"hidden\"\r\n\r\nmultipart " "message\r\n-----------------------------73869" "09285754635891697677882\r\nContent-Dispositio" "n: form-data; name=\"file\"; filename=\"test_" "file.txt\"\r\nContent-Type: text/plain\r\n\r\n" "Woo multiline text file\n\nLa la la\r\n------" "-----------------------7386909285754635891697" "677882--\r\n">>, Expect = [{headers, [{"content-disposition", {"form-data", [{"name", "hidden"}]}}]}, {body, <<"multipart message">>}, body_end, {headers, [{"content-disposition", {"form-data", [{"name", "file"}, {"filename", "test_file.txt"}]}}, {"content-type", {"text/plain", []}}]}, {body, <<"Woo multiline text file\n\nLa la la">>}, body_end, eof], TestCallback = fun (Next) -> test_callback(Next, Expect) end, ServerFun = fun (Socket, _Opts) -> ok = mochiweb_socket:send(Socket, BinContent), exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, byte_size(BinContent)), Res = parse_multipart_request(Req, TestCallback), {0, <<>>, ok} = Res, ok end, ok = with_socket_server(Transport, ServerFun, ClientFun), ok. parse2_http_test() -> parse2(plain). parse2_https_test() -> parse2(ssl). parse2(Transport) -> ContentType = "multipart/form-data; boundary=---------------" "------------6072231407570234361599764024", BinContent = <<"-----------------------------6072231407570234" "361599764024\r\nContent-Disposition: " "form-data; name=\"hidden\"\r\n\r\nmultipart " "message\r\n-----------------------------60722" "31407570234361599764024\r\nContent-Dispositio" "n: form-data; name=\"file\"; filename=\"\"\r\n" "Content-Type: application/octet-stream\r\n\r\n\r\n" "-----------------------------6072231407570234" "361599764024--\r\n">>, Expect = [{headers, [{"content-disposition", {"form-data", [{"name", "hidden"}]}}]}, {body, <<"multipart message">>}, body_end, {headers, [{"content-disposition", {"form-data", [{"name", "file"}, {"filename", ""}]}}, {"content-type", {"application/octet-stream", []}}]}, {body, <<>>}, body_end, eof], TestCallback = fun (Next) -> test_callback(Next, Expect) end, ServerFun = fun (Socket, _Opts) -> ok = mochiweb_socket:send(Socket, BinContent), exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, byte_size(BinContent)), Res = parse_multipart_request(Req, TestCallback), {0, <<>>, ok} = Res, ok end, ok = with_socket_server(Transport, ServerFun, ClientFun), ok. parse_form_http_test() -> do_parse_form(plain). parse_form_https_test() -> do_parse_form(ssl). do_parse_form(Transport) -> ContentType = "multipart/form-data; boundary=AaB03x", "AaB03x" = get_boundary(ContentType), Content = mochiweb_util:join(["--AaB03x", "Content-Disposition: form-data; name=\"submit" "-name\"", "", "Larry", "--AaB03x", "Content-Disposition: form-data; name=\"files\";" ++ "filename=\"file1.txt\"", "Content-Type: text/plain", "", "... contents of file1.txt ...", "--AaB03x--", ""], "\r\n"), BinContent = iolist_to_binary(Content), ServerFun = fun (Socket, _Opts) -> ok = mochiweb_socket:send(Socket, BinContent), exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, byte_size(BinContent)), Res = parse_form(Req), [{"submit-name", "Larry"}, {"files", {"file1.txt", {"text/plain", []}, <<"... contents of file1.txt ...">>}}] = Res, ok end, ok = with_socket_server(Transport, ServerFun, ClientFun), ok. parse_http_test() -> do_parse(plain). parse_https_test() -> do_parse(ssl). do_parse(Transport) -> ContentType = "multipart/form-data; boundary=AaB03x", "AaB03x" = get_boundary(ContentType), Content = mochiweb_util:join(["--AaB03x", "Content-Disposition: form-data; name=\"submit" "-name\"", "", "Larry", "--AaB03x", "Content-Disposition: form-data; name=\"files\";" ++ "filename=\"file1.txt\"", "Content-Type: text/plain", "", "... contents of file1.txt ...", "--AaB03x--", ""], "\r\n"), BinContent = iolist_to_binary(Content), Expect = [{headers, [{"content-disposition", {"form-data", [{"name", "submit-name"}]}}]}, {body, <<"Larry">>}, body_end, {headers, [{"content-disposition", {"form-data", [{"name", "files"}, {"filename", "file1.txt"}]}}, {"content-type", {"text/plain", []}}]}, {body, <<"... contents of file1.txt ...">>}, body_end, eof], TestCallback = fun (Next) -> test_callback(Next, Expect) end, ServerFun = fun (Socket, _Opts) -> ok = mochiweb_socket:send(Socket, BinContent), exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, byte_size(BinContent)), Res = parse_multipart_request(Req, TestCallback), {0, <<>>, ok} = Res, ok end, ok = with_socket_server(Transport, ServerFun, ClientFun), ok. parse_partial_body_boundary_http_test() -> parse_partial_body_boundary(plain). parse_partial_body_boundary_https_test() -> parse_partial_body_boundary(ssl). parse_partial_body_boundary(Transport) -> Boundary = string:copies("$", 2048), ContentType = "multipart/form-data; boundary=" ++ Boundary, ?assertEqual(Boundary, (get_boundary(ContentType))), Content = mochiweb_util:join(["--" ++ Boundary, "Content-Disposition: form-data; name=\"submit" "-name\"", "", "Larry", "--" ++ Boundary, "Content-Disposition: form-data; name=\"files\";" ++ "filename=\"file1.txt\"", "Content-Type: text/plain", "", "... contents of file1.txt ...", "--" ++ Boundary ++ "--", ""], "\r\n"), BinContent = iolist_to_binary(Content), Expect = [{headers, [{"content-disposition", {"form-data", [{"name", "submit-name"}]}}]}, {body, <<"Larry">>}, body_end, {headers, [{"content-disposition", {"form-data", [{"name", "files"}, {"filename", "file1.txt"}]}}, {"content-type", {"text/plain", []}}]}, {body, <<"... contents of file1.txt ...">>}, body_end, eof], TestCallback = fun (Next) -> test_callback(Next, Expect) end, ServerFun = fun (Socket, _Opts) -> ok = mochiweb_socket:send(Socket, BinContent), exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, byte_size(BinContent)), Res = parse_multipart_request(Req, TestCallback), {0, <<>>, ok} = Res, ok end, ok = with_socket_server(Transport, ServerFun, ClientFun), ok. parse_large_header_http_test() -> parse_large_header(plain). parse_large_header_https_test() -> parse_large_header(ssl). parse_large_header(Transport) -> ContentType = "multipart/form-data; boundary=AaB03x", "AaB03x" = get_boundary(ContentType), Content = mochiweb_util:join(["--AaB03x", "Content-Disposition: form-data; name=\"submit" "-name\"", "", "Larry", "--AaB03x", "Content-Disposition: form-data; name=\"files\";" ++ "filename=\"file1.txt\"", "Content-Type: text/plain", "x-large-header: " ++ string:copies("%", 4096), "", "... contents of file1.txt ...", "--AaB03x--", ""], "\r\n"), BinContent = iolist_to_binary(Content), Expect = [{headers, [{"content-disposition", {"form-data", [{"name", "submit-name"}]}}]}, {body, <<"Larry">>}, body_end, {headers, [{"content-disposition", {"form-data", [{"name", "files"}, {"filename", "file1.txt"}]}}, {"content-type", {"text/plain", []}}, {"x-large-header", {string:copies("%", 4096), []}}]}, {body, <<"... contents of file1.txt ...">>}, body_end, eof], TestCallback = fun (Next) -> test_callback(Next, Expect) end, ServerFun = fun (Socket, _Opts) -> ok = mochiweb_socket:send(Socket, BinContent), exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, byte_size(BinContent)), Res = parse_multipart_request(Req, TestCallback), {0, <<>>, ok} = Res, ok end, ok = with_socket_server(Transport, ServerFun, ClientFun), ok. find_boundary_test() -> B = <<"\r\n--X">>, {next_boundary, 0, 7} = find_boundary(B, <<"\r\n--X\r\nRest">>), {next_boundary, 1, 7} = find_boundary(B, <<"!\r\n--X\r\nRest">>), {end_boundary, 0, 9} = find_boundary(B, <<"\r\n--X--\r\nRest">>), {end_boundary, 1, 9} = find_boundary(B, <<"!\r\n--X--\r\nRest">>), not_found = find_boundary(B, <<"--X\r\nRest">>), {maybe, 0} = find_boundary(B, <<"\r\n--X\r">>), {maybe, 1} = find_boundary(B, <<"!\r\n--X\r">>), P = <<"\r\n-----------------------------160374543510" "82272548568224146">>, B0 = <<55, 212, 131, 77, 206, 23, 216, 198, 35, 87, 252, 118, 252, 8, 25, 211, 132, 229, 182, 42, 29, 188, 62, 175, 247, 243, 4, 4, 0, 59, 13, 10, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 49, 54, 48, 51, 55, 52, 53, 52, 51, 53, 49>>, {maybe, 30} = find_boundary(P, B0), not_found = find_boundary(B, <<"\r\n--XJOPKE">>), ok. find_in_binary_test() -> {exact, 0} = find_in_binary(<<"foo">>, <<"foobarbaz">>), {exact, 1} = find_in_binary(<<"oo">>, <<"foobarbaz">>), {exact, 8} = find_in_binary(<<"z">>, <<"foobarbaz">>), not_found = find_in_binary(<<"q">>, <<"foobarbaz">>), {partial, 7, 2} = find_in_binary(<<"azul">>, <<"foobarbaz">>), {exact, 0} = find_in_binary(<<"foobarbaz">>, <<"foobarbaz">>), {partial, 0, 3} = find_in_binary(<<"foobar">>, <<"foo">>), {partial, 1, 3} = find_in_binary(<<"foobar">>, <<"afoo">>), ok. flash_parse_http_test() -> flash_parse(plain). flash_parse_https_test() -> flash_parse(ssl). flash_parse(Transport) -> ContentType = "multipart/form-data; boundary=----------ei4GI" "3GI3Ij5Ef1ae0KM7Ij5ei4Ij5", "----------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5" = get_boundary(ContentType), BinContent = <<"------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5\r\n" "Content-Disposition: form-data; name=\"Filena" "me\"\r\n\r\nhello.txt\r\n------------ei4GI3GI" "3Ij5Ef1ae0KM7Ij5ei4Ij5\r\nContent-Disposition" ": form-data; name=\"success_action_status\"\r\n\r\n" "201\r\n------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei" "4Ij5\r\nContent-Disposition: form-data; " "name=\"file\"; filename=\"hello.txt\"\r\nCont" "ent-Type: application/octet-stream\r\n\r\nhel" "lo\n\r\n------------ei4GI3GI3Ij5Ef1ae0KM7Ij5e" "i4Ij5\r\nContent-Disposition: form-data; " "name=\"Upload\"\r\n\r\nSubmit Query\r\n------" "------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5--">>, Expect = [{headers, [{"content-disposition", {"form-data", [{"name", "Filename"}]}}]}, {body, <<"hello.txt">>}, body_end, {headers, [{"content-disposition", {"form-data", [{"name", "success_action_status"}]}}]}, {body, <<"201">>}, body_end, {headers, [{"content-disposition", {"form-data", [{"name", "file"}, {"filename", "hello.txt"}]}}, {"content-type", {"application/octet-stream", []}}]}, {body, <<"hello\n">>}, body_end, {headers, [{"content-disposition", {"form-data", [{"name", "Upload"}]}}]}, {body, <<"Submit Query">>}, body_end, eof], TestCallback = fun (Next) -> test_callback(Next, Expect) end, ServerFun = fun (Socket, _Opts) -> ok = mochiweb_socket:send(Socket, BinContent), exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, byte_size(BinContent)), Res = parse_multipart_request(Req, TestCallback), {0, <<>>, ok} = Res, ok end, ok = with_socket_server(Transport, ServerFun, ClientFun), ok. flash_parse2_http_test() -> flash_parse2(plain). flash_parse2_https_test() -> flash_parse2(ssl). flash_parse2(Transport) -> ContentType = "multipart/form-data; boundary=----------ei4GI" "3GI3Ij5Ef1ae0KM7Ij5ei4Ij5", "----------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5" = get_boundary(ContentType), Chunk = iolist_to_binary(string:copies("%", 4096)), BinContent = <<"------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5\r\n" "Content-Disposition: form-data; name=\"Filena" "me\"\r\n\r\nhello.txt\r\n------------ei4GI3GI" "3Ij5Ef1ae0KM7Ij5ei4Ij5\r\nContent-Disposition" ": form-data; name=\"success_action_status\"\r\n\r\n" "201\r\n------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei" "4Ij5\r\nContent-Disposition: form-data; " "name=\"file\"; filename=\"hello.txt\"\r\nCont" "ent-Type: application/octet-stream\r\n\r\n", Chunk/binary, "\r\n------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij" "5\r\nContent-Disposition: form-data; " "name=\"Upload\"\r\n\r\nSubmit Query\r\n------" "------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5--">>, Expect = [{headers, [{"content-disposition", {"form-data", [{"name", "Filename"}]}}]}, {body, <<"hello.txt">>}, body_end, {headers, [{"content-disposition", {"form-data", [{"name", "success_action_status"}]}}]}, {body, <<"201">>}, body_end, {headers, [{"content-disposition", {"form-data", [{"name", "file"}, {"filename", "hello.txt"}]}}, {"content-type", {"application/octet-stream", []}}]}, {body, Chunk}, body_end, {headers, [{"content-disposition", {"form-data", [{"name", "Upload"}]}}]}, {body, <<"Submit Query">>}, body_end, eof], TestCallback = fun (Next) -> test_callback(Next, Expect) end, ServerFun = fun (Socket, _Opts) -> ok = mochiweb_socket:send(Socket, BinContent), exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, byte_size(BinContent)), Res = parse_multipart_request(Req, TestCallback), {0, <<>>, ok} = Res, ok end, ok = with_socket_server(Transport, ServerFun, ClientFun), ok. parse_headers_test() -> ?assertEqual([], (parse_headers(<<>>))). flash_multipart_hack_test() -> Buffer = <<"prefix-">>, Prefix = <<"prefix">>, State = #mp{length = 0, buffer = Buffer, boundary = Prefix}, ?assertEqual(State, (flash_multipart_hack(State))). parts_to_body_single_test() -> {HL, B} = parts_to_body([{0, 5, <<"01234">>}], "text/plain", 10), [{"Content-Range", Range}, {"Content-Type", Type}] = lists:sort(HL), ?assertEqual(<<"bytes 0-5/10">>, (iolist_to_binary(Range))), ?assertEqual(<<"text/plain">>, (iolist_to_binary(Type))), ?assertEqual(<<"01234">>, (iolist_to_binary(B))), ok. parts_to_body_multi_test() -> {[{"Content-Type", Type}], _B} = parts_to_body([{0, 5, <<"01234">>}, {5, 10, <<"56789">>}], "text/plain", 10), ?assertMatch(<<"multipart/byteranges; boundary=", _/binary>>, (iolist_to_binary(Type))), ok. parts_to_multipart_body_test() -> {[{"Content-Type", V}], B} = parts_to_multipart_body([{0, 5, <<"01234">>}, {5, 10, <<"56789">>}], "text/plain", 10, "BOUNDARY"), MB = multipart_body([{0, 5, <<"01234">>}, {5, 10, <<"56789">>}], "text/plain", "BOUNDARY", 10), ?assertEqual(<<"multipart/byteranges; boundary=BOUNDARY">>, (iolist_to_binary(V))), ?assertEqual((iolist_to_binary(MB)), (iolist_to_binary(B))), ok. multipart_body_test() -> ?assertEqual(<<"--BOUNDARY--\r\n">>, (iolist_to_binary(multipart_body([], "text/plain", "BOUNDARY", 0)))), ?assertEqual(<<"--BOUNDARY\r\nContent-Type: text/plain\r\nCon" "tent-Range: bytes 0-5/10\r\n\r\n01234\r\n--BO" "UNDARY\r\nContent-Type: text/plain\r\nContent" "-Range: bytes 5-10/10\r\n\r\n56789\r\n--BOUND" "ARY--\r\n">>, (iolist_to_binary(multipart_body([{0, 5, <<"01234">>}, {5, 10, <<"56789">>}], "text/plain", "BOUNDARY", 10)))), ok. %% @todo Move somewhere more appropriate than in the test suite multipart_parsing_benchmark_test() -> run_multipart_parsing_benchmark(1). run_multipart_parsing_benchmark(0) -> ok; run_multipart_parsing_benchmark(N) -> multipart_parsing_benchmark(), run_multipart_parsing_benchmark(N - 1). multipart_parsing_benchmark() -> ContentType = "multipart/form-data; boundary=----------ei4GI" "3GI3Ij5Ef1ae0KM7Ij5ei4Ij5", Chunk = binary:copy(<<"This Is_%Some=Quite0Long4String2Used9For7Benc" "hmarKing.5">>, 102400), BinContent = <<"------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5\r\n" "Content-Disposition: form-data; name=\"Filena" "me\"\r\n\r\nhello.txt\r\n------------ei4GI3GI" "3Ij5Ef1ae0KM7Ij5ei4Ij5\r\nContent-Disposition" ": form-data; name=\"success_action_status\"\r\n\r\n" "201\r\n------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei" "4Ij5\r\nContent-Disposition: form-data; " "name=\"file\"; filename=\"hello.txt\"\r\nCont" "ent-Type: application/octet-stream\r\n\r\n", Chunk/binary, "\r\n------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij" "5\r\nContent-Disposition: form-data; " "name=\"Upload\"\r\n\r\nSubmit Query\r\n------" "------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5--">>, Expect = [{headers, [{"content-disposition", {"form-data", [{"name", "Filename"}]}}]}, {body, <<"hello.txt">>}, body_end, {headers, [{"content-disposition", {"form-data", [{"name", "success_action_status"}]}}]}, {body, <<"201">>}, body_end, {headers, [{"content-disposition", {"form-data", [{"name", "file"}, {"filename", "hello.txt"}]}}, {"content-type", {"application/octet-stream", []}}]}, {body, Chunk}, body_end, {headers, [{"content-disposition", {"form-data", [{"name", "Upload"}]}}]}, {body, <<"Submit Query">>}, body_end, eof], TestCallback = fun (Next) -> test_callback(Next, Expect) end, ServerFun = fun (Socket, _Opts) -> ok = mochiweb_socket:send(Socket, BinContent), exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, byte_size(BinContent)), Res = parse_multipart_request(Req, TestCallback), {0, <<>>, ok} = Res, ok end, ok = with_socket_server(plain, ServerFun, ClientFun), ok. -endif. mochiweb-3.2.1/src/mochiweb_request.erl000066400000000000000000001124761450333227400201700ustar00rootroot00000000000000%% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc MochiWeb HTTP Request abstraction. -module(mochiweb_request). -author('bob@mochimedia.com'). -include_lib("kernel/include/file.hrl"). -include("internal.hrl"). -define(QUIP, "Any of you quaids got a smint?"). -export([new/5, new/6]). -export([dump/1, get/2, get_combined_header_value/2, get_header_value/2, get_primary_header_value/2]). -export([recv/2, recv/3, recv_body/1, recv_body/2, send/2, stream_body/4, stream_body/5]). -export([start_raw_response/2, start_response/2, start_response_length/2]). -export([ok/2, respond/2]). -export([not_found/1, not_found/2]). -export([parse_post/1, parse_qs/1]). -export([cleanup/1, should_close/1]). -export([get_cookie_value/2, parse_cookie/1]). -export([serve_file/3, serve_file/4]). -export([accepted_encodings/2]). -export([accepted_content_types/2, accepts_content_type/2]). -export([is_closed/1]). -define(SAVE_QS, mochiweb_request_qs). -define(SAVE_PATH, mochiweb_request_path). -define(SAVE_RECV, mochiweb_request_recv). -define(SAVE_BODY, mochiweb_request_body). -define(SAVE_BODY_LENGTH, mochiweb_request_body_length). -define(SAVE_POST, mochiweb_request_post). -define(SAVE_COOKIE, mochiweb_request_cookie). -define(SAVE_FORCE_CLOSE, mochiweb_request_force_close). %% @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()}]. % 5 minute default idle timeout -define(IDLE_TIMEOUT, 300000). % Maximum recv_body() length of 1MB -define(MAX_RECV_BODY, 1024 * 1024). %% @spec new(Socket, Method, RawPath, Version, headers()) -> request() %% @doc Create a new request instance. new(Socket, Method, RawPath, Version, Headers) -> new(Socket, [], Method, RawPath, Version, Headers). %% @spec new(Socket, Opts, Method, RawPath, Version, headers()) -> request() %% @doc Create a new request instance. new(Socket, Opts, Method, RawPath, Version, Headers) -> {?MODULE, [Socket, Opts, 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, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, Headers]}) -> mochiweb_headers:get_value(K, Headers). get_primary_header_value(K, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, Headers]}) -> mochiweb_headers:get_primary_value(K, Headers). get_combined_header_value(K, {?MODULE, [_Socket, _Opts, _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(), request()) -> term() %% @doc Return the internal representation of the given field. If %% socket is requested on a HTTPS connection, then %% an ssl socket will be returned as {ssl, SslSocket}. %% You can use SslSocket with the ssl %% application, eg: ssl:peercert(SslSocket). get(socket, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) -> Socket; get(scheme, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) -> case mochiweb_socket:type(Socket) of plain -> http; ssl -> https end; get(method, {?MODULE, [_Socket, _Opts, Method, _RawPath, _Version, _Headers]}) -> Method; get(raw_path, {?MODULE, [_Socket, _Opts, _Method, RawPath, _Version, _Headers]}) -> RawPath; get(version, {?MODULE, [_Socket, _Opts, _Method, _RawPath, Version, _Headers]}) -> Version; get(headers, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, Headers]}) -> Headers; get(peer, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> case mochiweb_socket:peername(Socket) of {ok, {Addr = {10, _, _, _}, _Port}} -> case get_header_value("x-forwarded-for", THIS) of undefined -> inet_parse:ntoa(Addr); Hosts -> string:strip(lists:last(string:tokens(Hosts, ","))) end; %% Copied this syntax from webmachine contributor Steve Vinoski {ok, {Addr = {172, Second, _, _}, _Port}} when Second > 15 andalso Second < 32 -> case get_header_value("x-forwarded-for", THIS) of undefined -> inet_parse:ntoa(Addr); Hosts -> string:strip(lists:last(string:tokens(Hosts, ","))) end; %% According to RFC 6598, contributor Gerald Xv {ok, {Addr = {100, Second, _, _}, _Port}} when Second > 63 andalso Second < 128 -> case get_header_value("x-forwarded-for", THIS) of undefined -> inet_parse:ntoa(Addr); Hosts -> string:strip(lists:last(string:tokens(Hosts, ","))) end; {ok, {Addr = {192, 168, _, _}, _Port}} -> 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", THIS) of undefined -> "127.0.0.1"; Hosts -> string:strip(lists:last(string:tokens(Hosts, ","))) end; {ok, {Addr, _Port}} -> inet_parse:ntoa(Addr); {error, enotconn = Error} -> exit({shutdown, Error}) end; get(path, {?MODULE, [_Socket, _Opts, _Method, RawPath, _Version, _Headers]}) -> case erlang:get(?SAVE_PATH) of undefined -> {Path0, _, _} = mochiweb_util:urlsplit_path(RawPath), Path = mochiweb_util:normalize_path(mochiweb_util:unquote(Path0)), put(?SAVE_PATH, Path), Path; Cached -> Cached end; get(body_length, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> case erlang:get(?SAVE_BODY_LENGTH) of undefined -> BodyLength = body_length(THIS), put(?SAVE_BODY_LENGTH, {cached, BodyLength}), BodyLength; {cached, Cached} -> Cached end; get(range, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> case get_header_value(range, THIS) of undefined -> undefined; RawRange -> mochiweb_http:parse_range_request(RawRange) end; get(opts, {?MODULE, [_Socket, Opts, _Method, _RawPath, _Version, _Headers]}) -> Opts. %% @spec dump(request()) -> {mochiweb_request, [{atom(), term()}]} %% @doc Dump the internal representation to a "human readable" set of terms %% for debugging/inspection purposes. dump({?MODULE, [_Socket, Opts, Method, RawPath, Version, Headers]}) -> {?MODULE, [{method, Method}, {version, Version}, {raw_path, RawPath}, {opts, Opts}, {headers, mochiweb_headers:to_list(Headers)}]}. %% @spec send(iodata(), request()) -> ok %% @doc Send data over the socket. send(Data, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) -> case mochiweb_socket:send(Socket, Data) of ok -> ok; _ -> exit({shutdown, send_error}) end. %% @spec recv(integer(), request()) -> binary() %% @doc Receive Length bytes from the client as a binary, with the default %% idle timeout. recv(Length, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> recv(Length, ?IDLE_TIMEOUT, THIS). %% @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, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) -> case mochiweb_socket:recv(Socket, Length, Timeout) of {ok, Data} -> put(?SAVE_RECV, true), Data; _ -> exit({shutdown, recv_error}) end. %% @spec body_length(request()) -> undefined | chunked | unknown_transfer_encoding | integer() %% @doc Infer body length from transfer-encoding and content-length headers. body_length({?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> case get_header_value("transfer-encoding", THIS) of undefined -> case get_combined_header_value("content-length", THIS) of undefined -> undefined; Length -> list_to_integer(Length) end; "chunked" -> chunked; Unknown -> {unknown_transfer_encoding, Unknown} end. %% @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({?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> recv_body(?MAX_RECV_BODY, THIS). %% @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, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> case erlang:get(?SAVE_BODY) of undefined -> % we could use a sane constant for max chunk size Body = stream_body(?MAX_RECV_BODY, fun ({0, _ChunkedFooter}, {_LengthAcc, BinAcc}) -> iolist_to_binary(lists:reverse(BinAcc)); ({Length, Bin}, {LengthAcc, BinAcc}) -> NewLength = Length + LengthAcc, if NewLength > MaxBody -> exit({body_too_large, chunked}); true -> {NewLength, [Bin | BinAcc]} end end, {0, []}, MaxBody, THIS), put(?SAVE_BODY, Body), Body; Cached -> Cached end. stream_body(MaxChunkSize, ChunkFun, FunState, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> stream_body(MaxChunkSize, ChunkFun, FunState, undefined, THIS). stream_body(MaxChunkSize, ChunkFun, FunState, MaxBodyLength, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> Expect = case get_header_value("expect", THIS) of undefined -> undefined; Value when is_list(Value) -> string:to_lower(Value) end, case Expect of "100-continue" -> _ = start_raw_response({100, gb_trees:empty()}, THIS), ok; _Else -> ok end, case body_length(THIS) of undefined -> undefined; {unknown_transfer_encoding, Unknown} -> exit({unknown_transfer_encoding, Unknown}); chunked -> % In this case the MaxBody is actually used to % determine the maximum allowed size of a single % chunk. stream_chunked_body(MaxChunkSize, ChunkFun, FunState, THIS); 0 -> <<>>; Length when is_integer(Length) -> case MaxBodyLength of MaxBodyLength when is_integer(MaxBodyLength), MaxBodyLength < Length -> exit({body_too_large, content_length}); _ -> stream_unchunked_body(MaxChunkSize, Length, ChunkFun, FunState, THIS) end end. %% @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}, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> start_raw_response({Code, ResponseHeaders}, THIS). %% @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}, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> {Header, Response} = format_response_header({Code, ResponseHeaders}, THIS), send(Header, THIS), 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}, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> HResponse = mochiweb_headers:make(ResponseHeaders), HResponse1 = mochiweb_headers:enter("Content-Length", Length, HResponse), start_response({Code, HResponse1}, THIS). %% @spec format_response_header({integer(), ioheaders()} | {integer(), ioheaders(), integer()}, request()) -> iolist() %% @doc Format the HTTP response header, including the Code HTTP response and %% ResponseHeaders including an optional Content-Length of Length. The server %% will set header defaults such as Server %% and Date if not present in ResponseHeaders. format_response_header({Code, ResponseHeaders}, {?MODULE, [_Socket, _Opts, _Method, _RawPath, Version, _Headers]} = THIS) -> HResponse = mochiweb_headers:make(ResponseHeaders), HResponse1 = mochiweb_headers:default_from_list(server_headers(), HResponse), HResponse2 = case should_close(THIS) of true -> mochiweb_headers:enter("Connection", "close", HResponse1); false -> HResponse1 end, End = [[mochiweb_util:make_io(K), <<": ">>, V, <<"\r\n">>] || {K, V} <- mochiweb_headers:to_list(HResponse2)], Response = mochiweb:new_response({THIS, Code, HResponse2}), {[make_version(Version), make_code(Code), <<"\r\n">>, End, <<"\r\n">>], Response}; format_response_header({Code, ResponseHeaders, Length}, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> HResponse = mochiweb_headers:make(ResponseHeaders), HResponse1 = mochiweb_headers:enter("Content-Length", Length, HResponse), format_response_header({Code, HResponse1}, THIS). %% @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}}, {?MODULE, [_Socket, _Opts, Method, _RawPath, _Version, _Headers]} = THIS) -> Length = mochiweb_io:iodevice_size(IoDevice), Response = start_response_length({Code, ResponseHeaders, Length}, THIS), case Method of 'HEAD' -> ok; _ -> mochiweb_io:iodevice_stream(fun (Body) -> send(Body, THIS) end, IoDevice) end, Response; respond({Code, ResponseHeaders, chunked}, {?MODULE, [_Socket, _Opts, Method, _RawPath, Version, _Headers]} = THIS) -> HResponse = mochiweb_headers:make(ResponseHeaders), HResponse1 = case Method of 'HEAD' -> %% This is what Google does, http://www.google.com/ %% is chunked but HEAD gets Content-Length: 0. %% The RFC is ambiguous so emulating Google is smart. mochiweb_headers:enter("Content-Length", "0", HResponse); _ when Version >= {1, 1} -> %% Only use chunked encoding for HTTP/1.1 mochiweb_headers:enter("Transfer-Encoding", "chunked", HResponse); _ -> %% For pre-1.1 clients we send the data as-is %% without a Content-Length header and without %% chunk delimiters. Since the end of the document %% is now ambiguous we must force a close. put(?SAVE_FORCE_CLOSE, true), HResponse end, start_response({Code, HResponse1}, THIS); respond({Code, ResponseHeaders, Body}, {?MODULE, [_Socket, _Opts, Method, _RawPath, _Version, _Headers]} = THIS) -> {Header, Response} = format_response_header({Code, ResponseHeaders, iolist_size(Body)}, THIS), case Method of 'HEAD' -> send(Header, THIS); _ -> send([Header, Body], THIS) end, Response. %% @spec not_found(request()) -> response() %% @doc Alias for not_found([]). not_found({?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> not_found([], THIS). %% @spec not_found(ExtraHeaders, request()) -> response() %% @doc Alias for respond({404, [{"Content-Type", "text/plain"} %% | ExtraHeaders], <<"Not found.">>}). not_found(ExtraHeaders, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> respond({404, [{"Content-Type", "text/plain"} | ExtraHeaders], <<"Not found.">>}, THIS). %% @spec ok({value(), iodata()} | {value(), ioheaders(), iodata() | {file, IoDevice}}, request()) -> %% response() %% @doc respond({200, [{"Content-Type", ContentType} | Headers], Body}). ok({ContentType, Body}, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> ok({ContentType, [], Body}, THIS); ok({ContentType, ResponseHeaders, Body}, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> HResponse = mochiweb_headers:make(ResponseHeaders), case get(range, THIS) of X when (X =:= undefined orelse X =:= fail) orelse Body =:= chunked -> %% http://code.google.com/p/mochiweb/issues/detail?id=54 %% Range header not supported when chunked, return 200 and provide %% full response. HResponse1 = mochiweb_headers:enter("Content-Type", ContentType, HResponse), respond({200, HResponse1, Body}, THIS); Ranges -> {PartList, Size} = range_parts(Body, Ranges), case PartList of [] -> %% no valid ranges HResponse1 = mochiweb_headers:enter("Content-Type", ContentType, HResponse), %% could be 416, for now we'll just return 200 respond({200, HResponse1, Body}, THIS); PartList -> {RangeHeaders, RangeBody} = mochiweb_multipart:parts_to_body(PartList, ContentType, Size), HResponse1 = mochiweb_headers:enter_from_list([{"Accept-Ranges", "bytes"} | RangeHeaders], HResponse), respond({206, HResponse1, RangeBody}, THIS) end end. %% @spec should_close(request()) -> bool() %% @doc Return true if the connection must be closed. If false, using %% Keep-Alive should be safe. should_close({?MODULE, [_Socket, _Opts, _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 is_close(get_header_value("connection", THIS)) %% HTTP 1.0 requires Connection: Keep-Alive orelse Version =:= {1, 0} andalso get_header_value("connection", THIS) =/= "Keep-Alive" %% unread data left on the socket, can't safely continue orelse DidNotRecv 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", THIS) =:= "chunked". is_close("close") -> true; is_close(S = [_C, _L, _O, _S, _E]) -> string:to_lower(S) =:= "close"; is_close(_) -> false. %% @spec cleanup(request()) -> ok %% @doc Clean up any junk in the process dictionary, required before continuing %% a Keep-Alive request. cleanup({?MODULE, [_Socket, _Opts, _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) -> erase(K) end, L), ok. %% @spec parse_qs(request()) -> [{Key::string(), Value::string()}] %% @doc Parse the query string of the URL. parse_qs({?MODULE, [_Socket, _Opts, _Method, RawPath, _Version, _Headers]}) -> case erlang:get(?SAVE_QS) of undefined -> {_, QueryString, _} = mochiweb_util:urlsplit_path(RawPath), Parsed = mochiweb_util:parse_qs(QueryString), put(?SAVE_QS, Parsed), Parsed; Cached -> Cached end. %% @spec get_cookie_value(Key::string, request()) -> string() | undefined %% @doc Get the value of the given cookie. get_cookie_value(Key, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> proplists:get_value(Key, parse_cookie(THIS)). %% @spec parse_cookie(request()) -> [{Key::string(), Value::string()}] %% @doc Parse the cookie header. parse_cookie({?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> case erlang:get(?SAVE_COOKIE) of undefined -> Cookies = case get_header_value("cookie", THIS) of undefined -> []; Value -> mochiweb_cookies:parse_cookie(Value) end, put(?SAVE_COOKIE, Cookies), Cookies; Cached -> Cached end. %% @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({?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> case erlang:get(?SAVE_POST) of undefined -> Parsed = case recv_body(THIS) of undefined -> []; Binary -> case get_primary_header_value("content-type", THIS) of "application/x-www-form-urlencoded" ++ _ -> mochiweb_util:parse_qs(Binary); _ -> [] end end, put(?SAVE_POST, Parsed), Parsed; Cached -> Cached end. %% @spec stream_chunked_body(integer(), fun(), term(), request()) -> term() %% @doc The function is called for each chunk. %% Used internally by stream_body. stream_chunked_body(MaxChunkSize, Fun, FunState, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> case read_chunk_length(THIS) of 0 -> Fun({0, read_chunk(0, THIS)}, FunState); Length when Length > MaxChunkSize -> NewState = read_sub_chunks(Length, MaxChunkSize, Fun, FunState, THIS), stream_chunked_body(MaxChunkSize, Fun, NewState, THIS); Length -> NewState = Fun({Length, read_chunk(Length, THIS)}, FunState), stream_chunked_body(MaxChunkSize, Fun, NewState, THIS) end. stream_unchunked_body(_MaxChunkSize, 0, Fun, FunState, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) -> Fun({0, <<>>}, FunState); stream_unchunked_body(MaxChunkSize, Length, Fun, FunState, {?MODULE, [_Socket, Opts, _Method, _RawPath, _Version, _Headers]} = THIS) when Length > 0 -> RecBuf = case mochilists:get_value(recbuf, Opts, ?RECBUF_SIZE) of undefined -> %os controlled buffer size MaxChunkSize; Val -> Val end, PktSize = min(Length, RecBuf), Bin = recv(PktSize, THIS), NewState = Fun({PktSize, Bin}, FunState), stream_unchunked_body(MaxChunkSize, Length - PktSize, Fun, NewState, THIS). %% @spec read_chunk_length(request()) -> integer() %% @doc Read the length of the next HTTP chunk. read_chunk_length({?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) -> ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, line}])), case mochiweb_socket:recv(Socket, 0, ?IDLE_TIMEOUT) of {ok, Header} -> ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, raw}])), Splitter = fun (C) -> C =/= $\r andalso C =/= $\n andalso C =/= $\s end, {Hex, _Rest} = lists:splitwith(Splitter, binary_to_list(Header)), mochihex:to_int(Hex); _ -> exit({shutdown, read_chunk_length_recv_error}) end. %% @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, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) -> ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, line}])), F = fun (F1, Acc) -> case mochiweb_socket:recv(Socket, 0, ?IDLE_TIMEOUT) of {ok, <<"\r\n">>} -> Acc; {ok, Footer} -> F1(F1, [Footer | Acc]); _ -> exit({shutdown, read_chunk_recv_error}) end end, Footers = F(F, []), ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, raw}])), put(?SAVE_RECV, true), Footers; read_chunk(Length, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) -> case mochiweb_socket:recv(Socket, 2 + Length, ?IDLE_TIMEOUT) of {ok, <>} -> Chunk; _ -> exit({shutdown, read_chunk_recv_error}) end. read_sub_chunks(Length, MaxChunkSize, Fun, FunState, {?MODULE, [_Socket, _Opts, _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, THIS); read_sub_chunks(Length, _MaxChunkSize, Fun, FunState, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> Fun({Length, read_chunk(Length, THIS)}, FunState). %% @spec serve_file(Path, DocRoot, request()) -> Response %% @doc Serve a file relative to DocRoot. serve_file(Path, DocRoot, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> serve_file(Path, DocRoot, [], THIS). %% @spec serve_file(Path, DocRoot, ExtraHeaders, request()) -> Response %% @doc Serve a file relative to DocRoot. serve_file(Path, DocRoot, ExtraHeaders, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> case mochiweb_util:safe_relative_path(Path) of undefined -> not_found(ExtraHeaders, THIS); RelPath -> FullPath = filename:join([DocRoot, RelPath]), case filelib:is_dir(FullPath) of true -> maybe_redirect(RelPath, FullPath, ExtraHeaders, THIS); false -> maybe_serve_file(FullPath, ExtraHeaders, THIS) end end. %% Internal API %% This has the same effect as the DirectoryIndex directive in httpd directory_index(FullPath) -> filename:join([FullPath, "index.html"]). maybe_redirect([], FullPath, ExtraHeaders, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> maybe_serve_file(directory_index(FullPath), ExtraHeaders, THIS); maybe_redirect(RelPath, FullPath, ExtraHeaders, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, Headers]} = THIS) -> case string:right(RelPath, 1) of "/" -> maybe_serve_file(directory_index(FullPath), ExtraHeaders, THIS); _ -> Host = mochiweb_headers:get_value("host", Headers), Location = "http://" ++ Host ++ "/" ++ RelPath ++ "/", LocationBin = list_to_binary(Location), MoreHeaders = [{"Location", Location}, {"Content-Type", "text/html"} | ExtraHeaders], Top = <<"301 " "Moved Permanently

Mov" "ed Permanently

The document has " "moved >, Bottom = <<">here.

\n">>, Body = <>, respond({301, MoreHeaders, Body}, THIS) end. maybe_serve_file(File, ExtraHeaders, {?MODULE, [_Socket, _Opts, _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", THIS) of LastModified -> respond({304, ExtraHeaders, ""}, THIS); _ -> case file:open(File, [raw, binary]) of {ok, IoDevice} -> ContentType = mochiweb_util:guess_mime(File), Res = ok({ContentType, [{"last-modified", LastModified} | ExtraHeaders], {file, IoDevice}}, THIS), ok = file:close(IoDevice), Res; _ -> not_found(ExtraHeaders, THIS) end end; {error, _} -> not_found(ExtraHeaders, THIS) end. server_headers() -> [{"Server", "MochiWeb/1.0 (" ++ (?QUIP) ++ ")"}, {"Date", mochiweb_clock:rfc1123()}]. make_code(X) when is_integer(X) -> [integer_to_list(X), [" " | httpd_util:reason_phrase(X)]]; make_code(Io) when is_list(Io); is_binary(Io) -> Io. make_version({1, 0}) -> <<"HTTP/1.0 ">>; make_version(_) -> <<"HTTP/1.1 ">>. range_parts({file, IoDevice}, Ranges) -> Size = mochiweb_io:iodevice_size(IoDevice), F = fun (Spec, Acc) -> case mochiweb_http:range_skip_length(Spec, Size) of invalid_range -> Acc; V -> [V | Acc] end end, LocNums = lists:foldr(F, [], Ranges), {ok, Data} = file:pread(IoDevice, LocNums), Bodies = lists:zipwith(fun ({Skip, Length}, PartialBody) -> case Length of 0 -> {Skip, Skip, <<>>}; _ -> {Skip, Skip + Length - 1, PartialBody} end end, LocNums, Data), {Bodies, Size}; range_parts(Body0, Ranges) -> Body = iolist_to_binary(Body0), Size = size(Body), F = fun (Spec, Acc) -> case mochiweb_http:range_skip_length(Spec, Size) of invalid_range -> Acc; {Skip, Length} -> <<_:Skip/binary, PartialBody:Length/binary, _/binary>> = Body, [{Skip, Skip + Length - 1, PartialBody} | Acc] end end, {lists:foldr(F, [], Ranges), Size}. %% @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 %% not supported by the server will not be included in the return list. %% This list is computed from the "Accept-Encoding" header and %% its elements are ordered, descendingly, according to their Q values. %% %% Section 14.3 of the RFC 2616 (HTTP 1.1) describes the "Accept-Encoding" %% header and the process of determining which server supported encodings %% can be used for encoding the body for the request's response. %% %% Examples %% %% 1) For a missing "Accept-Encoding" header: %% accepted_encodings(["gzip", "identity"]) -> ["identity"] %% %% 2) For an "Accept-Encoding" header with value "gzip, deflate": %% accepted_encodings(["gzip", "identity"]) -> ["gzip", "identity"] %% %% 3) For an "Accept-Encoding" header with value "gzip;q=0.5, deflate": %% accepted_encodings(["gzip", "deflate", "identity"]) -> %% ["deflate", "gzip", "identity"] %% accepted_encodings(SupportedEncodings, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> AcceptEncodingHeader = case get_header_value("Accept-Encoding", THIS) of undefined -> ""; Value -> Value end, case mochiweb_util:parse_qvalues(AcceptEncodingHeader) of invalid_qvalue_string -> bad_accept_encoding_value; QList -> mochiweb_util:pick_accepted_encodings(QList, SupportedEncodings, "identity") end. %% @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. %% %% Examples %% %% 1) For a missing "Accept" header: %% accepts_content_type("application/json") -> true %% %% 2) For an "Accept" header with value "text/plain, application/*": %% accepts_content_type("application/json") -> true %% %% 3) For an "Accept" header with value "text/plain, */*; q=0.0": %% accepts_content_type("application/json") -> false %% %% 4) For an "Accept" header with value "text/plain; q=0.5, */*; q=0.1": %% accepts_content_type("application/json") -> true %% %% 5) For an "Accept" header with value "text/*; q=0.0, */*": %% accepts_content_type("text/plain") -> false %% accepts_content_type(ContentType1, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> ContentType = re:replace(ContentType1, "\\s", "", [global, {return, list}]), AcceptHeader = accept_header(THIS), case mochiweb_util:parse_qvalues(AcceptHeader) of invalid_qvalue_string -> bad_accept_header; QList -> [MainType, _SubType] = string:tokens(ContentType, "/"), SuperType = MainType ++ "/*", lists:any(fun ({"*/*", Q}) when Q > 0.0 -> true; ({Type, Q}) when Q > 0.0 -> Type =:= ContentType orelse Type =:= SuperType; (_) -> false end, QList) andalso not lists:member({ContentType, 0.0}, QList) andalso not lists:member({SuperType, 0.0}, QList) end. %% @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 %% according to the preferences specified in the "Accept" header (higher Q values %% first). If two or more types have the same preference (Q value), they're order %% in the returned list is the same as they're order in the input list. %% %% Examples %% %% 1) For a missing "Accept" header: %% accepted_content_types(["text/html", "application/json"]) -> %% ["text/html", "application/json"] %% %% 2) For an "Accept" header with value "text/html, application/*": %% accepted_content_types(["application/json", "text/html"]) -> %% ["application/json", "text/html"] %% %% 3) For an "Accept" header with value "text/html, */*; q=0.0": %% accepted_content_types(["text/html", "application/json"]) -> %% ["text/html"] %% %% 4) For an "Accept" header with value "text/html; q=0.5, */*; q=0.1": %% accepts_content_types(["application/json", "text/html"]) -> %% ["text/html", "application/json"] %% accepted_content_types(Types1, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> Types = [accepted_content_types_1(V1) || V1 <- Types1], AcceptHeader = accept_header(THIS), case mochiweb_util:parse_qvalues(AcceptHeader) of invalid_qvalue_string -> bad_accept_header; QList -> TypesQ = lists:foldr(fun (T, Acc) -> case proplists:get_value(T, QList) of undefined -> [MainType, _SubType] = string:tokens(T, "/"), case proplists:get_value(MainType ++ "/*", QList) of undefined -> case proplists:get_value("*/*", QList) of Q when is_float(Q), Q > 0.0 -> [{Q, T} | Acc]; _ -> Acc end; Q when Q > 0.0 -> [{Q, T} | Acc]; _ -> Acc end; Q when Q > 0.0 -> [{Q, T} | Acc]; _ -> Acc end end, [], Types), % Note: Stable sort. If 2 types have the same Q value we leave them in the % same order as in the input list. SortFun = fun ({Q1, _}, {Q2, _}) -> Q1 >= Q2 end, [Type || {_Q, Type} <- lists:sort(SortFun, TypesQ)] end. accepted_content_types_1(T) -> re:replace(T, "\\s", "", [global, {return, list}]). accept_header({?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]} = THIS) -> case get_header_value("Accept", THIS) of undefined -> "*/*"; Value -> Value end. %% @spec is_closed(request()) -> true | false | undefined %% @doc Check if a request connection is closing or already closed. This may be %% useful when processing long running request callbacks, when the client %% disconnects after a short timeout. This function works on Linux, NetBSD, %% OpenBSD, FreeBSD and MacOS. On other operating systems, like Windows for %% instance, it will return undefined. is_closed({?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) -> mochiweb_socket:is_closed(Socket). mochiweb-3.2.1/src/mochiweb_response.erl000066400000000000000000000065351450333227400203340ustar00rootroot00000000000000%% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc Response abstraction. -module(mochiweb_response). -author('bob@mochimedia.com'). -define(QUIP, "Any of you quaids got a smint?"). -export([dump/1, get/2, get_header_value/2, new/3]). -export([send/2, write_chunk/2]). %% @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, {?MODULE, [_Request, _Code, Headers]}) -> mochiweb_headers:get_value(K, Headers). %% @spec get(request | code | headers, response()) -> term() %% @doc Return the internal representation of the given field. get(request, {?MODULE, [Request, _Code, _Headers]}) -> Request; get(code, {?MODULE, [_Request, Code, _Headers]}) -> Code; get(headers, {?MODULE, [_Request, _Code, Headers]}) -> Headers. %% @spec dump(response()) -> {mochiweb_request, [{atom(), term()}]} %% @doc Dump the internal representation to a "human readable" set of terms %% for debugging/inspection purposes. dump({?MODULE, [{ReqM, _} = Request, Code, Headers]}) -> [{request, ReqM:dump(Request)}, {code, Code}, {headers, mochiweb_headers:to_list(Headers)}]. %% @spec send(iodata(), response()) -> ok %% @doc Send data over the socket if the method is not HEAD. send(Data, {?MODULE, [{ReqM, _} = Request, _Code, _Headers]}) -> case ReqM:get(method, Request) of 'HEAD' -> ok; _ -> ReqM:send(Data, Request) end. %% @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, {?MODULE, [{ReqM, _} = Request, _Code, _Headers]} = THIS) -> case ReqM:get(version, Request) of Version when Version >= {1, 1} -> Length = iolist_size(Data), send([io_lib:format("~.16b\r\n", [Length]), Data, <<"\r\n">>], THIS); _ -> send(Data, THIS) end. %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -endif. mochiweb-3.2.1/src/mochiweb_session.erl000066400000000000000000000215661450333227400201620ustar00rootroot00000000000000%% @author Asier Azkuenaga Batiz %% @copyright 2013 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @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 Secure Cookie Protocol". %% 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 combining 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( <>). %% @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 <> -> 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(<>, <>, 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). -ifdef(new_crypto_unavailable). -spec encrypt_data(binary(), binary()) -> binary(). encrypt_data(Data, Key) -> IV = crypto:strong_rand_bytes(16), Crypt = crypto:block_encrypt(aes_cfb128, Key, IV, Data), <>. -spec decrypt_data(binary(), binary()) -> binary(). decrypt_data(<>, Key) -> crypto:block_decrypt(aes_cfb128, Key, IV, Crypt). -spec gen_key(iolist(), iolist()) -> binary(). gen_key(ExpirationTime, ServerKey) -> crypto:hmac(md5, ServerKey, [ExpirationTime]). -spec gen_hmac(iolist(), binary(), iolist(), binary()) -> binary(). gen_hmac(ExpirationTime, Data, SessionKey, Key) -> crypto:hmac(sha, Key, [ExpirationTime, Data, SessionKey]). -else. % new crypto available (OTP 23+) -spec encrypt_data(binary(), binary()) -> binary(). encrypt_data(Data, Key) -> IV = crypto:strong_rand_bytes(16), Crypt = crypto:crypto_one_time(aes_128_cfb128, Key, IV, Data, true), <>. -spec decrypt_data(binary(), binary()) -> binary(). decrypt_data(<>, Key) -> crypto:crypto_one_time(aes_128_cfb128, Key, IV, Crypt, false). -spec gen_key(iolist(), iolist()) -> binary(). gen_key(ExpirationTime, ServerKey) -> crypto:mac(hmac, md5, ServerKey, [ExpirationTime]). -spec gen_hmac(iolist(), binary(), iolist(), binary()) -> binary(). gen_hmac(ExpirationTime, Data, SessionKey, Key) -> crypto:mac(hmac, sha, Key, [ExpirationTime, Data, SessionKey]). -endif. -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). generate_check_session_cookie_test_() -> {setup, fun setup_server_key/0, fun generate_check_session_cookie/1}. setup_server_key() -> crypto:start(), ["adfasdfasfs",30000]. generate_check_session_cookie([ServerKey, TS]) -> Id = fun (A) -> A end, TSFuture = TS + 1000, TSPast = TS - 1, [?_assertEqual( {true, [TSFuture, "alice"]}, check_session_cookie( generate_session_data(TSFuture, "alice", Id, ServerKey), TS, Id, ServerKey)), ?_assertEqual( {true, [TSFuture, "alice and"]}, check_session_cookie( generate_session_data(TSFuture, "alice and", Id, ServerKey), TS, Id, ServerKey)), ?_assertEqual( {true, [TSFuture, "alice and"]}, check_session_cookie( generate_session_data(TSFuture, "alice and", Id, ServerKey), TS, Id,ServerKey)), ?_assertEqual( {true, [TSFuture, "alice and bob"]}, check_session_cookie( generate_session_data(TSFuture, "alice and bob", Id, ServerKey), TS, Id, ServerKey)), ?_assertEqual( {true, [TSFuture, "alice jlkjfkjsdfg sdkfjgldsjgl"]}, check_session_cookie( generate_session_data(TSFuture, "alice jlkjfkjsdfg sdkfjgldsjgl", Id, ServerKey), TS, Id, ServerKey)), ?_assertEqual( {true, [TSFuture, "alice .'¡'ç+-$%/(&\""]}, check_session_cookie( generate_session_data(TSFuture, "alice .'¡'ç+-$%/(&\"" ,Id, ServerKey), TS, Id, ServerKey)), ?_assertEqual( {true,[TSFuture,"alice456689875"]}, check_session_cookie( generate_session_data(TSFuture, ["alice","456689875"], Id, ServerKey), TS, Id, ServerKey)), ?_assertError( function_clause, check_session_cookie( generate_session_data(TSFuture, {tuple,one}, Id, ServerKey), TS, Id,ServerKey)), ?_assertEqual( {false, [TSPast, "bob"]}, check_session_cookie( generate_session_data(TSPast, "bob", Id,ServerKey), TS, Id, ServerKey)) ]. -endif. mochiweb-3.2.1/src/mochiweb_socket.erl000066400000000000000000000206031450333227400177560ustar00rootroot00000000000000%% @copyright 2010 Mochi Media, Inc. %% @doc MochiWeb socket - wrapper for plain and ssl sockets. -module(mochiweb_socket). -export([listen/4, accept/1, transport_accept/1, finish_accept/1, recv/3, send/2, close/1, port/1, peername/1, setopts/2, getopts/2, type/1, exit_if_closed/1, is_closed/1]). -define(ACCEPT_TIMEOUT, 2000). -define(SSL_TIMEOUT, 10000). -define(SSL_HANDSHAKE_TIMEOUT, 20000). listen(Ssl, Port, Opts, SslOpts) -> case Ssl of true -> Opts1 = add_safe_protocol_versions(Opts), Opts2 = add_unbroken_ciphers_default(Opts1 ++ SslOpts), case ssl:listen(Port, Opts2) of {ok, ListenSocket} -> {ok, {ssl, ListenSocket}}; {error, _} = Err -> Err end; false -> gen_tcp:listen(Port, Opts) end. -ifdef(new_crypto_unavailable). add_unbroken_ciphers_default(Opts) -> Default = filter_unsecure_cipher_suites(ssl:cipher_suites()), Ciphers = filter_broken_cipher_suites(proplists:get_value(ciphers, Opts, Default)), [{ciphers, Ciphers} | proplists:delete(ciphers, Opts)]. %% Filter old map style cipher suites filter_unsecure_cipher_suites(Ciphers) -> lists:filter(fun ({_,des_cbc,_}) -> false; ({_,_,md5}) -> false; (_) -> true end, Ciphers). -else. add_unbroken_ciphers_default(Opts) -> %% add_safe_protocol_versions/1 must have been called to ensure a {versions, _} tuple is present Versions = proplists:get_value(versions, Opts), CipherSuites = lists:append([ssl:cipher_suites(all, Version) || Version <- Versions]), Default = filter_unsecure_cipher_suites(CipherSuites), Ciphers = filter_broken_cipher_suites(proplists:get_value(ciphers, Opts, Default)), [{ciphers, Ciphers} | proplists:delete(ciphers, Opts)]. %% Filter new map style cipher suites filter_unsecure_cipher_suites(Ciphers) -> ssl:filter_cipher_suites(Ciphers, [ {key_exchange, fun(des_cbc) -> false; (_) -> true end}, {mac, fun(md5) -> false; (_) -> true end} ]). -endif. filter_broken_cipher_suites(Ciphers) -> case proplists:get_value(ssl_app, ssl:versions()) of "5.3" ++ _ -> lists:filter(fun(Suite) -> string:left(atom_to_list(element(1, Suite)), 4) =/= "ecdh" end, Ciphers); _ -> Ciphers end. add_safe_protocol_versions(Opts) -> case proplists:is_defined(versions, Opts) of true -> Opts; false -> Versions = filter_unsafe_protcol_versions(proplists:get_value(available, ssl:versions())), [{versions, Versions} | Opts] end. filter_unsafe_protcol_versions(Versions) -> lists:filter(fun (sslv3) -> false; (_) -> true end, Versions). %% Provided for backwards compatibility only accept(ListenSocket) -> case transport_accept(ListenSocket) of {ok, Socket} -> finish_accept(Socket); {error, _} = Err -> Err end. transport_accept({ssl, ListenSocket}) -> case ssl:transport_accept(ListenSocket, ?SSL_TIMEOUT) of {ok, Socket} -> {ok, {ssl, Socket}}; {error, _} = Err -> Err end; transport_accept(ListenSocket) -> gen_tcp:accept(ListenSocket, ?ACCEPT_TIMEOUT). -ifdef(ssl_handshake_unavailable). finish_accept({ssl, Socket}) -> case ssl:ssl_accept(Socket, ?SSL_HANDSHAKE_TIMEOUT) of ok -> {ok, {ssl, Socket}}; {error, _} = Err -> Err end; finish_accept(Socket) -> {ok, Socket}. -else. finish_accept({ssl, Socket}) -> case ssl:handshake(Socket, ?SSL_HANDSHAKE_TIMEOUT) of {ok, SslSocket} -> {ok, {ssl, SslSocket}}; {error, _} = Err -> Err end; finish_accept(Socket) -> {ok, Socket}. -endif. recv({ssl, Socket}, Length, Timeout) -> ssl:recv(Socket, Length, Timeout); recv(Socket, Length, Timeout) -> gen_tcp:recv(Socket, Length, Timeout). send({ssl, Socket}, Data) -> ssl:send(Socket, Data); send(Socket, Data) -> gen_tcp:send(Socket, Data). close({ssl, Socket}) -> ssl:close(Socket); close(Socket) -> gen_tcp:close(Socket). port({ssl, Socket}) -> case ssl:sockname(Socket) of {ok, {_, Port}} -> {ok, Port}; {error, _} = Err -> Err end; port(Socket) -> inet:port(Socket). peername({ssl, Socket}) -> ssl:peername(Socket); peername(Socket) -> inet:peername(Socket). setopts({ssl, Socket}, Opts) -> ssl:setopts(Socket, Opts); setopts(Socket, Opts) -> inet:setopts(Socket, Opts). getopts({ssl, Socket}, Opts) -> ssl:getopts(Socket, Opts); getopts(Socket, Opts) -> inet:getopts(Socket, Opts). type({ssl, _}) -> ssl; type(_) -> plain. exit_if_closed({error, closed = Error}) -> exit({shutdown, Error}); exit_if_closed({error, einval = Error}) -> exit({shutdown, Error}); exit_if_closed(Res) -> Res. %% @doc Check if the socket is closing or already closed. This function works %% with passive mode sockets on Linux, OpenBSD, NetBSD, FreeBSD and MacOS. On %% unsupported OS-es, like Windows, it returns undefined. is_closed(Socket) -> OsType = os:type(), case tcp_info_opt(OsType) of {raw, _, _, _} = InfoOpt -> case getopts(Socket, [InfoOpt]) of {ok, [{raw, _, _, <>}]} -> tcp_is_closed(State, OsType); {ok, []} -> undefined; {error, einval} -> % Already cleaned up true; {error, _} -> undefined end; undefined -> undefined end. % All OS-es have the tcpi_state (uint8) as first member of tcp_info struct tcp_info_opt({unix, linux}) -> %% netinet/in.h %% IPPROTO_TCP = 6 %% %% netinet/tcp.h %% #define TCP_INFO 11 %% {raw, 6, 11, 1}; tcp_info_opt({unix, darwin}) -> %% netinet/in.h %% #define IPPROTO_TCP 6 %% %% netinet/tcp.h %% #define TCP_CONNECTION_INFO 0x106 %% {raw, 6, 16#106, 1}; tcp_info_opt({unix, freebsd}) -> %% sys/netinet/in.h %% #define IPPROTO_TCP 6 %% %% sys/netinet/tcp.h %% #define TCP_INFO 32 %% {raw, 6, 32, 1}; tcp_info_opt({unix, netbsd}) -> %% sys/netinet/in.h %% #define IPPROTO_TCP 6 %% %% sys/netinet/tcp.h %% #define TCP_INFO 9 {raw, 6, 9, 1}; tcp_info_opt({unix, openbsd}) -> %% sys/netinet/in.h %% #define IPPROTO_TCP 6 %% %% sys/netinet/tcp.h %% #define TCP_INFO 0x09 {raw, 6, 16#09, 1}; tcp_info_opt({_, _}) -> undefined. tcp_is_closed(State, {unix, linux}) -> %% netinet/tcp.h %% enum %% { %% TCP_ESTABLISHED = 1, %% TCP_SYN_SENT, %% TCP_SYN_RECV, %% TCP_FIN_WAIT1, %% TCP_FIN_WAIT2, %% TCP_TIME_WAIT, %% TCP_CLOSE, %% TCP_CLOSE_WAIT, %% TCP_LAST_ACK, %% TCP_LISTEN, %% TCP_CLOSING %% } %% lists:member(State, [4, 5, 6, 7, 8, 9, 11]); tcp_is_closed(State, {unix, Type}) when Type =:= darwin; Type =:= freebsd; Type =:= netbsd; Type =:= openbsd -> %% tcp_fsm.h states are the same on macos, freebsd, netbsd and openbsd %% %% netinet/tcp_fsm.h %% #define TCPS_CLOSED 0 /* closed */ %% #define TCPS_LISTEN 1 /* listening for connection */ %% #define TCPS_SYN_SENT 2 /* active, have sent syn */ %% #define TCPS_SYN_RECEIVED 3 /* have send and received syn */ %% #define TCPS_ESTABLISHED 4 /* established */ %% #define TCPS_CLOSE_WAIT 5 /* rcvd fin, waiting for close */ %% #define TCPS_FIN_WAIT_1 6 /* have closed, sent fin */ %% #define TCPS_CLOSING 7 /* closed xchd FIN; await FIN ACK */ %% #define TCPS_LAST_ACK 8 /* had fin and close; await FIN ACK */ %% #define TCPS_FIN_WAIT_2 9 /* have closed, fin is acked */ %% #define TCPS_TIME_WAIT 10 /* in 2*msl quiet wait after close */ %% lists:member(State, [0, 5, 6, 7, 8, 9, 10]). mochiweb-3.2.1/src/mochiweb_socket_server.erl000066400000000000000000000423021450333227400213440ustar00rootroot00000000000000%% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. %% @doc MochiWeb socket server. -module(mochiweb_socket_server). -author('bob@mochimedia.com'). -behaviour(gen_server). -include("internal.hrl"). -export([start/1, start_link/1, stop/1]). -export([init/1, handle_call/3, handle_cast/2, terminate/2, code_change/3, handle_info/2]). -export([get/2, set/3]). -record(mochiweb_socket_server, {port, loop, name=undefined, max=2048, ip=any, listen=null, nodelay=false, recbuf=?RECBUF_SIZE, buffer=undefined, backlog=128, active_sockets=0, acceptor_pool_size=16, ssl=false, ssl_opts=[{ssl_imp, new}], acceptor_pool=sets:new(), profile_fun=undefined}). -define(is_old_state(State), not is_record(State, mochiweb_socket_server)). start_link(Options) -> start_server(start_link, parse_options(Options)). start(Options) -> case lists:keytake(link, 1, Options) of {value, {_Key, false}, Options1} -> start_server(start, parse_options(Options1)); _ -> %% TODO: https://github.com/mochi/mochiweb/issues/58 %% [X] Phase 1: Add new APIs (Sep 2011) %% [_] Phase 2: Add deprecation warning %% [_] Phase 3: Change default to {link, false} and ignore link %% [_] Phase 4: Add deprecation warning for {link, _} option %% [_] Phase 5: Remove support for {link, _} option start_link(Options) end. get(Name, Property) -> gen_server:call(Name, {get, Property}). set(Name, profile_fun, Fun) -> gen_server:cast(Name, {set, profile_fun, Fun}); set(Name, Property, _Value) -> error_logger:info_msg("?MODULE:set for ~p with ~p not implemented~n", [Name, Property]). stop(Name) when is_atom(Name) orelse is_pid(Name) -> gen_server:call(Name, stop); stop({Scope, Name}) when Scope =:= local orelse Scope =:= global -> stop(Name); stop(Options) -> State = parse_options(Options), stop(State#mochiweb_socket_server.name). %% Internal API parse_options(State=#mochiweb_socket_server{}) -> State; parse_options(Options) -> parse_options(Options, #mochiweb_socket_server{}). parse_options([], State=#mochiweb_socket_server{acceptor_pool_size=PoolSize, max=Max}) -> case Max < PoolSize of true -> error_logger:info_report([{warning, "max is set lower than acceptor_pool_size"}, {max, Max}, {acceptor_pool_size, PoolSize}]); false -> ok end, State; parse_options([{name, L} | Rest], State) when is_list(L) -> Name = {local, list_to_atom(L)}, parse_options(Rest, State#mochiweb_socket_server{name=Name}); parse_options([{name, A} | Rest], State) when A =:= undefined -> parse_options(Rest, State#mochiweb_socket_server{name=A}); parse_options([{name, A} | Rest], State) when is_atom(A) -> Name = {local, A}, parse_options(Rest, State#mochiweb_socket_server{name=Name}); parse_options([{name, Name} | Rest], State) -> parse_options(Rest, State#mochiweb_socket_server{name=Name}); parse_options([{port, L} | Rest], State) when is_list(L) -> Port = list_to_integer(L), parse_options(Rest, State#mochiweb_socket_server{port=Port}); parse_options([{port, Port} | Rest], State) -> parse_options(Rest, State#mochiweb_socket_server{port=Port}); parse_options([{ip, Ip} | Rest], State) -> ParsedIp = case Ip of any -> any; Ip when is_tuple(Ip) -> Ip; Ip when is_list(Ip) -> {ok, IpTuple} = inet_parse:address(Ip), IpTuple end, parse_options(Rest, State#mochiweb_socket_server{ip=ParsedIp}); parse_options([{loop, Loop} | Rest], State) -> parse_options(Rest, State#mochiweb_socket_server{loop=Loop}); parse_options([{backlog, Backlog} | Rest], State) -> parse_options(Rest, State#mochiweb_socket_server{backlog=Backlog}); parse_options([{nodelay, NoDelay} | Rest], State) -> parse_options(Rest, State#mochiweb_socket_server{nodelay=NoDelay}); parse_options([{recbuf, RecBuf} | Rest], State) when is_integer(RecBuf) orelse RecBuf == undefined -> %% XXX: `recbuf' value which is passed to `gen_tcp' %% and value reported by `inet:getopts(P, [recbuf])' may %% differ. They depends on underlying OS. From linux mans: %% %% The kernel doubles this value (to allow space for %% bookkeeping overhead) when it is set using setsockopt(2), %% and this doubled value is returned by getsockopt(2). %% %% See: man 7 socket | grep SO_RCVBUF %% %% In case undefined is passed instead of the default buffer %% size ?RECBUF_SIZE, no size is set and the OS can control it dynamically parse_options(Rest, State#mochiweb_socket_server{recbuf=RecBuf}); parse_options([{buffer, Buffer} | Rest], State) when is_integer(Buffer) orelse Buffer == undefined -> %% `buffer` sets Erlang's userland socket buffer size. The size of this %% buffer affects the maximum URL path that can be parsed. URL sizes that %% are larger than this plus the size of the HTTP verb and some whitespace %% will result in an `emsgsize` TCP error. %% %% If this value is not set Erlang sets it to 1460 which might be too low. parse_options(Rest, State#mochiweb_socket_server{buffer=Buffer}); parse_options([{acceptor_pool_size, Max} | Rest], State) -> MaxInt = ensure_int(Max), parse_options(Rest, State#mochiweb_socket_server{acceptor_pool_size=MaxInt}); parse_options([{max, Max} | Rest], State) -> MaxInt = ensure_int(Max), parse_options(Rest, State#mochiweb_socket_server{max=MaxInt}); parse_options([{ssl, Ssl} | Rest], State) when is_boolean(Ssl) -> parse_options(Rest, State#mochiweb_socket_server{ssl=Ssl}); parse_options([{ssl_opts, SslOpts} | Rest], State) when is_list(SslOpts) -> SslOpts1 = [{ssl_imp, new} | proplists:delete(ssl_imp, SslOpts)], parse_options(Rest, State#mochiweb_socket_server{ssl_opts=SslOpts1}); parse_options([{profile_fun, ProfileFun} | Rest], State) when is_function(ProfileFun) -> parse_options(Rest, State#mochiweb_socket_server{profile_fun=ProfileFun}). start_server(F, State=#mochiweb_socket_server{ssl=Ssl, name=Name}) -> ok = prep_ssl(Ssl), case Name of undefined -> gen_server:F(?MODULE, State, []); _ -> gen_server:F(Name, ?MODULE, State, []) end. -ifdef(otp_21). check_ssl_compatibility() -> case lists:keyfind(ssl, 1, application:loaded_applications()) of {_, _, V} when V =:= "9.1" orelse V =:= "9.1.1" -> {error, "ssl-" ++ V ++ " (OTP 21.2 to 21.2.2) has a regression and is not safe to use with mochiweb. See https://bugs.erlang.org/browse/ERL-830"}; _ -> ok end. -else. check_ssl_compatibility() -> ok. -endif. prep_ssl(true) -> ok = mochiweb:ensure_started(crypto), ok = mochiweb:ensure_started(asn1), ok = mochiweb:ensure_started(public_key), ok = mochiweb:ensure_started(ssl), ok = check_ssl_compatibility(), ok; prep_ssl(false) -> ok. ensure_int(N) when is_integer(N) -> N; ensure_int(S) when is_list(S) -> list_to_integer(S). ipv6_supported() -> case (catch inet:getaddr("localhost", inet6)) of {ok, _Addr} -> true; {error, _} -> false end. init(State=#mochiweb_socket_server{ip=Ip, port=Port, backlog=Backlog, nodelay=NoDelay, recbuf=RecBuf, buffer=Buffer}) -> process_flag(trap_exit, true), BaseOpts = [binary, {reuseaddr, true}, {packet, 0}, {backlog, Backlog}, {exit_on_close, false}, {active, false}, {nodelay, NoDelay}], Opts = case Ip of any -> case ipv6_supported() of % IPv4, and IPv6 if supported true -> [inet, inet6 | BaseOpts]; _ -> BaseOpts end; {_, _, _, _} -> % IPv4 [inet, {ip, Ip} | BaseOpts]; {_, _, _, _, _, _, _, _} -> % IPv6 [inet6, {ip, Ip} | BaseOpts] end, OptsBuf = set_buffer_opts(RecBuf, Buffer, Opts), listen(Port, OptsBuf, State). set_buffer_opts(undefined, undefined, Opts) -> % If recbuf is undefined, user space buffer is set to the default 1460 % value. That unexpectedly break the {packet, http} parser and any URL % lines longer than 1460 would error out with emsgsize. So when recbuf is % undefined, use previous value of recbuf for buffer in order to keep older % code from breaking. [{buffer, ?RECBUF_SIZE} | Opts]; set_buffer_opts(RecBuf, undefined, Opts) -> [{recbuf, RecBuf} | Opts]; set_buffer_opts(undefined, Buffer, Opts) -> [{buffer, Buffer} | Opts]; set_buffer_opts(RecBuf, Buffer, Opts) -> % Note: order matters, recbuf will override buffer unless buffer value % comes first, except on older versions of Erlang (ex. 17.0) where it works % exactly the opposite. [{buffer, Buffer}, {recbuf, RecBuf} | Opts]. new_acceptor_pool(State=#mochiweb_socket_server{acceptor_pool_size=Size}) -> lists:foldl(fun (_, S) -> new_acceptor(S) end, State, lists:seq(1, Size)). new_acceptor(State=#mochiweb_socket_server{acceptor_pool=Pool, recbuf=RecBuf, loop=Loop, listen=Listen}) -> LoopOpts = [{recbuf, RecBuf}], Pid = mochiweb_acceptor:start_link(self(), Listen, Loop, LoopOpts), State#mochiweb_socket_server{ acceptor_pool=sets:add_element(Pid, Pool)}. listen(Port, Opts, State=#mochiweb_socket_server{ssl=Ssl, ssl_opts=SslOpts}) -> case mochiweb_socket:listen(Ssl, Port, Opts, SslOpts) of {ok, Listen} -> {ok, ListenPort} = mochiweb_socket:port(Listen), {ok, new_acceptor_pool(State#mochiweb_socket_server{ listen=Listen, port=ListenPort})}; {error, Reason} -> {stop, Reason} end. do_get(port, #mochiweb_socket_server{port=Port}) -> Port; do_get(waiting_acceptors, #mochiweb_socket_server{acceptor_pool=Pool}) -> sets:size(Pool); do_get(active_sockets, #mochiweb_socket_server{active_sockets=ActiveSockets}) -> ActiveSockets. state_to_proplist(#mochiweb_socket_server{name=Name, port=Port, active_sockets=ActiveSockets}) -> [{name, Name}, {port, Port}, {active_sockets, ActiveSockets}]. upgrade_state(State = #mochiweb_socket_server{}) -> State; upgrade_state({mochiweb_socket_server, Port, Loop, Name, Max, IP, Listen, NoDelay, Backlog, ActiveSockets, AcceptorPoolSize, SSL, SSL_opts, AcceptorPool}) -> #mochiweb_socket_server{port=Port, loop=Loop, name=Name, max=Max, ip=IP, listen=Listen, nodelay=NoDelay, backlog=Backlog, active_sockets=ActiveSockets, acceptor_pool_size=AcceptorPoolSize, ssl=SSL, ssl_opts=SSL_opts, acceptor_pool=AcceptorPool}. handle_call(Req, From, State) when ?is_old_state(State) -> handle_call(Req, From, upgrade_state(State)); handle_call({get, Property}, _From, State) -> Res = do_get(Property, State), {reply, Res, State}; handle_call(stop, _From, State) -> {stop, normal, ok, State}; handle_call(_Message, _From, State) -> Res = error, {reply, Res, State}. handle_cast(Req, State) when ?is_old_state(State) -> handle_cast(Req, upgrade_state(State)); handle_cast({accepted, Pid, Timing}, State=#mochiweb_socket_server{active_sockets=ActiveSockets}) -> State1 = State#mochiweb_socket_server{active_sockets=1 + ActiveSockets}, case State#mochiweb_socket_server.profile_fun of undefined -> undefined; F when is_function(F) -> catch F([{timing, Timing} | state_to_proplist(State1)]) end, {noreply, recycle_acceptor(Pid, State1)}; handle_cast({set, profile_fun, ProfileFun}, State) -> State1 = case ProfileFun of ProfileFun when is_function(ProfileFun); ProfileFun =:= undefined -> State#mochiweb_socket_server{profile_fun=ProfileFun}; _ -> State end, {noreply, State1}. terminate(Reason, State) when ?is_old_state(State) -> terminate(Reason, upgrade_state(State)); terminate(_Reason, #mochiweb_socket_server{listen=Listen}) -> mochiweb_socket:close(Listen). code_change(_OldVsn, State, _Extra) -> State. recycle_acceptor(Pid, State=#mochiweb_socket_server{ acceptor_pool=Pool, acceptor_pool_size=PoolSize, max=Max, active_sockets=ActiveSockets}) -> %% A socket is considered to be active from immediately after it %% has been accepted (see the {accepted, Pid, Timing} cast above). %% This function will be called when an acceptor is transitioning %% to an active socket, or when either type of Pid dies. An acceptor %% Pid will always be in the acceptor_pool set, and an active socket %% will be in that set during the transition but not afterwards. Pool1 = sets:del_element(Pid, Pool), NewSize = sets:size(Pool1), ActiveSockets1 = case NewSize =:= sets:size(Pool) of %% Pid has died and it is not in the acceptor set, %% it must be an active socket. true -> max(0, ActiveSockets - 1); false -> ActiveSockets end, State1 = State#mochiweb_socket_server{ acceptor_pool=Pool1, active_sockets=ActiveSockets1}, %% Spawn a new acceptor only if it will not overrun the maximum socket %% count or the maximum pool size. case NewSize + ActiveSockets1 < Max andalso NewSize < PoolSize of true -> new_acceptor(State1); false -> State1 end. handle_info(Msg, State) when ?is_old_state(State) -> handle_info(Msg, upgrade_state(State)); handle_info({'EXIT', Pid, normal}, State) -> {noreply, recycle_acceptor(Pid, State)}; handle_info({'EXIT', Pid, {shutdown, _Error}}, State) -> {noreply, recycle_acceptor(Pid, State)}; handle_info({'EXIT', Pid, Reason}, State=#mochiweb_socket_server{acceptor_pool=Pool}) -> case sets:is_element(Pid, Pool) of true -> %% If there was an unexpected error accepting, log and sleep. error_logger:error_report({?MODULE, ?LINE, {acceptor_error, Reason}}), timer:sleep(100); false -> ok end, {noreply, recycle_acceptor(Pid, State)}; % this is what release_handler needs to get a list of modules, % since our supervisor modules list is set to 'dynamic' % see sasl-2.1.9.2/src/release_handler_1.erl get_dynamic_mods handle_info({From, Tag, get_modules}, State = #mochiweb_socket_server{name={local,Mod}}) -> From ! {element(2,Tag), [Mod]}, {noreply, State}; % If for some reason we can't get the module name, send empty list to avoid release_handler timeout: handle_info({From, Tag, get_modules}, State) -> error_logger:info_msg("mochiweb_socket_server replying to dynamic modules request as '[]'~n",[]), From ! {element(2,Tag), []}, {noreply, State}; handle_info(Info, State) -> error_logger:info_report([{'INFO', Info}, {'State', State}]), {noreply, State}. %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). upgrade_state_test() -> OldState = {mochiweb_socket_server, port, loop, name, max, ip, listen, nodelay, backlog, active_sockets, acceptor_pool_size, ssl, ssl_opts, acceptor_pool}, State = upgrade_state(OldState), CmpState = #mochiweb_socket_server{port=port, loop=loop, name=name, max=max, ip=ip, listen=listen, nodelay=nodelay, backlog=backlog, active_sockets=active_sockets, acceptor_pool_size=acceptor_pool_size, ssl=ssl, ssl_opts=ssl_opts, acceptor_pool=acceptor_pool, profile_fun=undefined}, ?assertEqual(CmpState, State). set_buffer_opts_test() -> ?assertEqual([{buffer, 8192}], set_buffer_opts(undefined, undefined, [])), ?assertEqual([{recbuf, 5}], set_buffer_opts(5, undefined, [])), ?assertEqual([{buffer, 6}], set_buffer_opts(undefined, 6, [])), ?assertEqual([{buffer, 6}, {recbuf, 5}], set_buffer_opts(5, 6, [])). -endif. mochiweb-3.2.1/src/mochiweb_util.erl000066400000000000000000001064131450333227400174470ustar00rootroot00000000000000%% @author Bob Ippolito %% @copyright 2007 Mochi Media, Inc. %% @doc Utilities for parsing and quoting. -module(mochiweb_util). -author('bob@mochimedia.com'). -export([join/2, quote_plus/1, urlencode/1, parse_qs/1, unquote/1, unquote_path/1]). -export([path_split/1]). -export([urlsplit/1, urlsplit_path/1, urlunsplit/1, urlunsplit_path/1]). -export([guess_mime/1, parse_header/1]). -export([shell_quote/1, cmd/1, cmd_string/1, cmd_port/2, cmd_status/1, cmd_status/2]). -export([record_to_proplist/2, record_to_proplist/3]). -export([safe_relative_path/1, partition/2]). -export([parse_qvalues/1, pick_accepted_encodings/3]). -export([make_io/1]). -export([normalize_path/1]). -export([rand_uniform/2]). -define(PERCENT, 37). % $\% -define(FULLSTOP, 46). % $\. -define(IS_HEX(C), ((C >= $0 andalso C =< $9) orelse (C >= $a andalso C =< $f) orelse (C >= $A andalso C =< $F))). -define(QS_SAFE(C), ((C >= $a andalso C =< $z) orelse (C >= $A andalso C =< $Z) orelse (C >= $0 andalso C =< $9) orelse (C =:= ?FULLSTOP orelse C =:= $- orelse C =:= $~ orelse C =:= $_))). hexdigit(C) when C < 10 -> $0 + C; hexdigit(C) when C < 16 -> $A + (C - 10). unhexdigit(C) when C >= $0, C =< $9 -> C - $0; unhexdigit(C) when C >= $a, C =< $f -> C - $a + 10; unhexdigit(C) when C >= $A, C =< $F -> C - $A + 10. unhexdigit(Hi, Lo) -> unhexdigit(Lo) bor (unhexdigit(Hi) bsl 4). %% @spec partition(String, Sep) -> {String, [], []} | {Prefix, Sep, Postfix} %% @doc Inspired by Python 2.5's str.partition: %% partition("foo/bar", "/") = {"foo", "/", "bar"}, %% partition("foo", "/") = {"foo", "", ""}. partition(String, Sep) -> case partition(String, Sep, []) of undefined -> {String, "", ""}; Result -> Result end. partition("", _Sep, _Acc) -> undefined; partition(S, Sep, Acc) -> case partition2(S, Sep) of undefined -> [C | Rest] = S, partition(Rest, Sep, [C | Acc]); Rest -> {lists:reverse(Acc), Sep, Rest} end. partition2(Rest, "") -> Rest; partition2([C | R1], [C | R2]) -> partition2(R1, R2); partition2(_S, _Sep) -> undefined. %% @spec safe_relative_path(string()) -> string() | undefined %% @doc Return the reduced version of a relative path or undefined if it %% is not safe. safe relative paths can be joined with an absolute path %% and will result in a subdirectory of the absolute path. Safe paths %% never contain a backslash character. safe_relative_path("/" ++ _) -> undefined; safe_relative_path(P) -> case string:chr(P, $\\) of 0 -> safe_relative_path(P, []); _ -> undefined end. safe_relative_path("", Acc) -> case Acc of [] -> ""; _ -> string:join(lists:reverse(Acc), "/") end; safe_relative_path(P, Acc) -> case partition(P, "/") of {"", "/", _} -> %% /foo or foo//bar undefined; {"..", _, _} when Acc =:= [] -> undefined; {"..", _, Rest} -> safe_relative_path(Rest, tl(Acc)); {Part, "/", ""} -> safe_relative_path("", ["", Part | Acc]); {Part, _, Rest} -> safe_relative_path(Rest, [Part | Acc]) end. %% @spec shell_quote(string()) -> string() %% @doc Quote a string according to UNIX shell quoting rules, returns a string %% surrounded by double quotes. shell_quote(L) -> shell_quote(L, [$\"]). %% @spec cmd_port([string()], Options) -> port() %% @doc open_port({spawn, mochiweb_util:cmd_string(Argv)}, Options). cmd_port(Argv, Options) -> open_port({spawn, cmd_string(Argv)}, Options). %% @spec cmd([string()]) -> string() %% @doc os:cmd(cmd_string(Argv)). cmd(Argv) -> os:cmd(cmd_string(Argv)). %% @spec cmd_string([string()]) -> string() %% @doc Create a shell quoted command string from a list of arguments. cmd_string(Argv) -> string:join([shell_quote(X) || X <- Argv], " "). %% @spec cmd_status([string()]) -> {ExitStatus::integer(), Stdout::binary()} %% @doc Accumulate the output and exit status from the given application, %% will be spawned with cmd_port/2. cmd_status(Argv) -> cmd_status(Argv, []). %% @spec cmd_status([string()], [atom()]) -> {ExitStatus::integer(), Stdout::binary()} %% @doc Accumulate the output and exit status from the given application, %% will be spawned with cmd_port/2. cmd_status(Argv, Options) -> Port = cmd_port(Argv, [exit_status, stderr_to_stdout, use_stdio, binary | Options]), try cmd_loop(Port, []) after catch port_close(Port) end. %% @spec cmd_loop(port(), list()) -> {ExitStatus::integer(), Stdout::binary()} %% @doc Accumulate the output and exit status from a port. cmd_loop(Port, Acc) -> receive {Port, {exit_status, Status}} -> {Status, iolist_to_binary(lists:reverse(Acc))}; {Port, {data, Data}} -> cmd_loop(Port, [Data | Acc]) end. %% @spec join([iolist()], iolist()) -> iolist() %% @doc Join a list of strings or binaries together with the given separator %% string or char or binary. The output is flattened, but may be an %% iolist() instead of a string() if any of the inputs are binary(). join([], _Separator) -> []; join([S], _Separator) -> lists:flatten(S); join(Strings, Separator) -> lists:flatten(revjoin(lists:reverse(Strings), Separator, [])). revjoin([], _Separator, Acc) -> Acc; revjoin([S | Rest], Separator, []) -> revjoin(Rest, Separator, [S]); revjoin([S | Rest], Separator, Acc) -> revjoin(Rest, Separator, [S, Separator | Acc]). %% @spec quote_plus(atom() | integer() | float() | string() | binary()) -> string() %% @doc URL safe encoding of the given term. quote_plus(Atom) when is_atom(Atom) -> quote_plus(atom_to_list(Atom)); quote_plus(Int) when is_integer(Int) -> quote_plus(integer_to_list(Int)); quote_plus(Binary) when is_binary(Binary) -> quote_plus(binary_to_list(Binary)); quote_plus(Float) when is_float(Float) -> quote_plus(mochinum:digits(Float)); quote_plus(String) -> quote_plus(String, []). quote_plus([], Acc) -> lists:reverse(Acc); quote_plus([C | Rest], Acc) when ?QS_SAFE(C) -> quote_plus(Rest, [C | Acc]); quote_plus([$\s | Rest], Acc) -> quote_plus(Rest, [$+ | Acc]); quote_plus([C | Rest], Acc) -> <> = <>, quote_plus(Rest, [hexdigit(Lo), hexdigit(Hi), ?PERCENT | Acc]). %% @spec urlencode([{Key, Value}]) -> string() %% @doc URL encode the property list. urlencode(Props) -> Pairs = lists:foldr( fun ({K, V}, Acc) -> [quote_plus(K) ++ "=" ++ quote_plus(V) | Acc] end, [], Props), string:join(Pairs, "&"). %% @spec parse_qs(string() | binary()) -> [{Key, Value}] %% @doc Parse a query string or application/x-www-form-urlencoded. parse_qs(Binary) when is_binary(Binary) -> parse_qs(binary_to_list(Binary)); parse_qs(String) -> parse_qs(String, []). parse_qs([], Acc) -> lists:reverse(Acc); parse_qs(String, Acc) -> {Key, Rest} = parse_qs_key(String), {Value, Rest1} = parse_qs_value(Rest), parse_qs(Rest1, [{Key, Value} | Acc]). parse_qs_key(String) -> parse_qs_key(String, []). parse_qs_key([], Acc) -> {qs_revdecode(Acc), ""}; parse_qs_key([$= | Rest], Acc) -> {qs_revdecode(Acc), Rest}; parse_qs_key(Rest=[$; | _], Acc) -> {qs_revdecode(Acc), Rest}; parse_qs_key(Rest=[$& | _], Acc) -> {qs_revdecode(Acc), Rest}; parse_qs_key([C | Rest], Acc) -> parse_qs_key(Rest, [C | Acc]). parse_qs_value(String) -> parse_qs_value(String, []). parse_qs_value([], Acc) -> {qs_revdecode(Acc), ""}; parse_qs_value([$; | Rest], Acc) -> {qs_revdecode(Acc), Rest}; parse_qs_value([$& | Rest], Acc) -> {qs_revdecode(Acc), Rest}; parse_qs_value([C | Rest], Acc) -> parse_qs_value(Rest, [C | Acc]). %% @spec unquote(string() | binary()) -> string() %% @doc Unquote a URL encoded string. unquote(Binary) when is_binary(Binary) -> unquote(binary_to_list(Binary)); unquote(String) -> qs_revdecode(lists:reverse(String)). qs_revdecode(S) -> qs_revdecode(S, []). qs_revdecode([], Acc) -> Acc; qs_revdecode([$+ | Rest], Acc) -> qs_revdecode(Rest, [$\s | Acc]); qs_revdecode([Lo, Hi, ?PERCENT | Rest], Acc) when ?IS_HEX(Lo), ?IS_HEX(Hi) -> qs_revdecode(Rest, [(unhexdigit(Hi, Lo)) | Acc]); qs_revdecode([C | Rest], Acc) -> qs_revdecode(Rest, [C | Acc]). %% @spec unquote_path(string() | binary()) -> string() %% @doc Unquote a URL encoded string, does not encode + into space. unquote_path(Binary) when is_binary(Binary) -> unquote_path(binary_to_list(Binary)); unquote_path(String) -> qs_revdecode_path(lists:reverse(String)). qs_revdecode_path(S) -> qs_revdecode_path(S, []). qs_revdecode_path([], Acc) -> Acc; qs_revdecode_path([Lo, Hi, ?PERCENT | Rest], Acc) when ?IS_HEX(Lo), ?IS_HEX(Hi) -> qs_revdecode_path(Rest, [(unhexdigit(Hi, Lo)) | Acc]); qs_revdecode_path([C | Rest], Acc) -> qs_revdecode_path(Rest, [C | Acc]). %% @spec urlsplit(Url) -> {Scheme, Netloc, Path, Query, Fragment} %% @doc Return a 5-tuple, does not expand % escapes. Only supports HTTP style %% URLs. urlsplit(Url) -> {Scheme, Url1} = urlsplit_scheme(Url), {Netloc, Url2} = urlsplit_netloc(Url1), {Path, Query, Fragment} = urlsplit_path(Url2), {Scheme, Netloc, Path, Query, Fragment}. urlsplit_scheme(Url) -> case urlsplit_scheme(Url, []) of no_scheme -> {"", Url}; Res -> Res end. urlsplit_scheme([C | Rest], Acc) when ((C >= $a andalso C =< $z) orelse (C >= $A andalso C =< $Z) orelse (C >= $0 andalso C =< $9) orelse C =:= $+ orelse C =:= $- orelse C =:= $.) -> urlsplit_scheme(Rest, [C | Acc]); urlsplit_scheme([$: | Rest], Acc=[_ | _]) -> {string:to_lower(lists:reverse(Acc)), Rest}; urlsplit_scheme(_Rest, _Acc) -> no_scheme. urlsplit_netloc("//" ++ Rest) -> urlsplit_netloc(Rest, []); urlsplit_netloc(Path) -> {"", Path}. urlsplit_netloc("", Acc) -> {lists:reverse(Acc), ""}; urlsplit_netloc(Rest=[C | _], Acc) when C =:= $/; C =:= $?; C =:= $# -> {lists:reverse(Acc), Rest}; urlsplit_netloc([C | Rest], Acc) -> urlsplit_netloc(Rest, [C | Acc]). %% @spec path_split(string()) -> {Part, Rest} %% @doc Split a path starting from the left, as in URL traversal. %% path_split("foo/bar") = {"foo", "bar"}, %% path_split("/foo/bar") = {"", "foo/bar"}. path_split(S) -> path_split(S, []). path_split("", Acc) -> {lists:reverse(Acc), ""}; path_split("/" ++ Rest, Acc) -> {lists:reverse(Acc), Rest}; path_split([C | Rest], Acc) -> path_split(Rest, [C | Acc]). %% @spec urlunsplit({Scheme, Netloc, Path, Query, Fragment}) -> string() %% @doc Assemble a URL from the 5-tuple. Path must be absolute. urlunsplit({Scheme, Netloc, Path, Query, Fragment}) -> lists:flatten([case Scheme of "" -> ""; _ -> [Scheme, "://"] end, Netloc, urlunsplit_path({Path, Query, Fragment})]). %% @spec urlunsplit_path({Path, Query, Fragment}) -> string() %% @doc Assemble a URL path from the 3-tuple. urlunsplit_path({Path, Query, Fragment}) -> lists:flatten([Path, case Query of "" -> ""; _ -> [$? | Query] end, case Fragment of "" -> ""; _ -> [$# | Fragment] end]). %% @spec urlsplit_path(Url) -> {Path, Query, Fragment} %% @doc Return a 3-tuple, does not expand % escapes. Only supports HTTP style %% paths. urlsplit_path(Path) -> urlsplit_path(Path, []). urlsplit_path("", Acc) -> {lists:reverse(Acc), "", ""}; urlsplit_path("?" ++ Rest, Acc) -> {Query, Fragment} = urlsplit_query(Rest), {lists:reverse(Acc), Query, Fragment}; urlsplit_path("#" ++ Rest, Acc) -> {lists:reverse(Acc), "", Rest}; urlsplit_path([C | Rest], Acc) -> urlsplit_path(Rest, [C | Acc]). urlsplit_query(Query) -> urlsplit_query(Query, []). urlsplit_query("", Acc) -> {lists:reverse(Acc), ""}; urlsplit_query("#" ++ Rest, Acc) -> {lists:reverse(Acc), Rest}; urlsplit_query([C | Rest], Acc) -> urlsplit_query(Rest, [C | Acc]). extension(Name) -> case filename:extension(Name) of "" -> Name; Ext -> Ext end. %% @spec guess_mime(string()) -> string() %% @doc Guess the mime type of a file by the extension of its filename. guess_mime(File) -> case filename:basename(File) of "crossdomain.xml" -> "text/x-cross-domain-policy"; Name -> case mochiweb_mime:from_extension(extension(Name)) of undefined -> "text/plain"; Mime -> Mime end end. %% @spec parse_header(string()) -> {Type, [{K, V}]} %% @doc Parse a Content-Type like header, return the main Content-Type %% and a property list of options. parse_header(String) -> %% TODO: This is exactly as broken as Python's cgi module. %% Should parse properly like mochiweb_cookies. [Type | Parts] = [string:strip(S) || S <- string:tokens(String, ";")], F = fun (S, Acc) -> case lists:splitwith(fun (C) -> C =/= $= end, S) of {"", _} -> %% Skip anything with no name Acc; {_, ""} -> %% Skip anything with no value Acc; {Name, [$\= | Value]} -> [{string:to_lower(string:strip(Name)), unquote_header(string:strip(Value))} | Acc] end end, {string:to_lower(Type), lists:foldr(F, [], Parts)}. unquote_header("\"" ++ Rest) -> unquote_header(Rest, []); unquote_header(S) -> S. unquote_header("", Acc) -> lists:reverse(Acc); unquote_header("\"", Acc) -> lists:reverse(Acc); unquote_header([$\\, C | Rest], Acc) -> unquote_header(Rest, [C | Acc]); unquote_header([C | Rest], Acc) -> unquote_header(Rest, [C | Acc]). %% @spec record_to_proplist(Record, Fields) -> proplist() %% @doc calls record_to_proplist/3 with a default TypeKey of '__record' record_to_proplist(Record, Fields) -> record_to_proplist(Record, Fields, '__record'). %% @spec record_to_proplist(Record, Fields, TypeKey) -> proplist() %% @doc Return a proplist of the given Record with each field in the %% Fields list set as a key with the corresponding value in the Record. %% TypeKey is the key that is used to store the record type %% Fields should be obtained by calling record_info(fields, record_type) %% where record_type is the record type of Record record_to_proplist(Record, Fields, TypeKey) when tuple_size(Record) - 1 =:= length(Fields) -> lists:zip([TypeKey | Fields], tuple_to_list(Record)). shell_quote([], Acc) -> lists:reverse([$\" | Acc]); shell_quote([C | Rest], Acc) when C =:= $\" orelse C =:= $\` orelse C =:= $\\ orelse C =:= $\$ -> shell_quote(Rest, [C, $\\ | Acc]); shell_quote([C | Rest], Acc) -> shell_quote(Rest, [C | Acc]). %% @spec parse_qvalues(string()) -> [qvalue()] | invalid_qvalue_string %% @type qvalue() = {media_type() | encoding() , float()}. %% @type media_type() = string(). %% @type encoding() = string(). %% %% @doc Parses a list (given as a string) of elements with Q values associated %% to them. Elements are separated by commas and each element is separated %% from its Q value by a semicolon. Q values are optional but when missing %% the value of an element is considered as 1.0. A Q value is always in the %% range [0.0, 1.0]. A Q value list is used for example as the value of the %% HTTP "Accept" and "Accept-Encoding" headers. %% %% Q values are described in section 2.9 of the RFC 2616 (HTTP 1.1). %% %% Example: %% %% parse_qvalues("gzip; q=0.5, deflate, identity;q=0.0") -> %% [{"gzip", 0.5}, {"deflate", 1.0}, {"identity", 0.0}] %% parse_qvalues(QValuesStr) -> try lists:map( fun(Pair) -> [Type | Params] = string:tokens(Pair, ";"), NormParams = normalize_media_params(Params), {Q, NonQParams} = extract_q(NormParams), {string:join([string:strip(Type) | NonQParams], ";"), Q} end, string:tokens(string:to_lower(QValuesStr), ",") ) catch _Type:_Error -> invalid_qvalue_string end. normalize_media_params(Params) -> {ok, Re} = re:compile("\\s"), normalize_media_params(Re, Params, []). normalize_media_params(_Re, [], Acc) -> lists:reverse(Acc); normalize_media_params(Re, [Param | Rest], Acc) -> NormParam = re:replace(Param, Re, "", [global, {return, list}]), normalize_media_params(Re, Rest, [NormParam | Acc]). extract_q(NormParams) -> {ok, KVRe} = re:compile("^([^=]+)=([^=]+)$"), {ok, QRe} = re:compile("^((?:0|1)(?:\\.\\d{1,3})?)$"), extract_q(KVRe, QRe, NormParams, []). extract_q(_KVRe, _QRe, [], Acc) -> {1.0, lists:reverse(Acc)}; extract_q(KVRe, QRe, [Param | Rest], Acc) -> case re:run(Param, KVRe, [{capture, [1, 2], list}]) of {match, [Name, Value]} -> case Name of "q" -> {match, [Q]} = re:run(Value, QRe, [{capture, [1], list}]), QVal = case Q of "0" -> 0.0; "1" -> 1.0; Else -> list_to_float(Else) end, case QVal < 0.0 orelse QVal > 1.0 of false -> {QVal, lists:reverse(Acc) ++ Rest} end; _ -> extract_q(KVRe, QRe, Rest, [Param | Acc]) end end. %% @spec pick_accepted_encodings([qvalue()], [encoding()], encoding()) -> %% [encoding()] %% %% @doc Determines which encodings specified in the given Q values list are %% valid according to a list of supported encodings and a default encoding. %% %% The returned list of encodings is sorted, descendingly, according to the %% Q values of the given list. The last element of this list is the given %% default encoding unless this encoding is explicitly or implicitily %% marked with a Q value of 0.0 in the given Q values list. %% Note: encodings with the same Q value are kept in the same order as %% found in the input Q values list. %% %% This encoding picking process is described in section 14.3 of the %% RFC 2616 (HTTP 1.1). %% %% Example: %% %% pick_accepted_encodings( %% [{"gzip", 0.5}, {"deflate", 1.0}], %% ["gzip", "identity"], %% "identity" %% ) -> %% ["gzip", "identity"] %% pick_accepted_encodings(AcceptedEncs, SupportedEncs, DefaultEnc) -> SortedQList = lists:reverse( lists:sort(fun({_, Q1}, {_, Q2}) -> Q1 < Q2 end, AcceptedEncs) ), {Accepted, Refused} = lists:foldr( fun({E, Q}, {A, R}) -> case Q > 0.0 of true -> {[E | A], R}; false -> {A, [E | R]} end end, {[], []}, SortedQList ), Refused1 = lists:foldr( fun(Enc, Acc) -> case Enc of "*" -> lists:subtract(SupportedEncs, Accepted) ++ Acc; _ -> [Enc | Acc] end end, [], Refused ), Accepted1 = lists:foldr( fun(Enc, Acc) -> case Enc of "*" -> lists:subtract(SupportedEncs, Accepted ++ Refused1) ++ Acc; _ -> [Enc | Acc] end end, [], Accepted ), Accepted2 = case lists:member(DefaultEnc, Accepted1) of true -> Accepted1; false -> Accepted1 ++ [DefaultEnc] end, [E || E <- Accepted2, lists:member(E, SupportedEncs), not lists:member(E, Refused1)]. make_io(Atom) when is_atom(Atom) -> atom_to_list(Atom); make_io(Integer) when is_integer(Integer) -> integer_to_list(Integer); make_io(Io) when is_list(Io); is_binary(Io) -> Io. %% @spec normalize_path(string()) -> string() %% @doc Remove duplicate slashes from an uri path ("//foo///bar////" becomes %% "/foo/bar/"). %% Per RFC 3986, all but the last path segment must be non-empty. normalize_path(Path) -> normalize_path(Path, []). normalize_path([], Acc) -> lists:reverse(Acc); normalize_path("/" ++ Path, "/" ++ _ = Acc) -> normalize_path(Path, Acc); normalize_path([C|Path], Acc) -> normalize_path(Path, [C|Acc]). rand_uniform(Start, End) -> Start + rand:uniform(End - Start) - 1. %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). make_io_test() -> ?assertEqual( <<"atom">>, iolist_to_binary(make_io(atom))), ?assertEqual( <<"20">>, iolist_to_binary(make_io(20))), ?assertEqual( <<"list">>, iolist_to_binary(make_io("list"))), ?assertEqual( <<"binary">>, iolist_to_binary(make_io(<<"binary">>))), ok. -record(test_record, {field1=f1, field2=f2}). record_to_proplist_test() -> ?assertEqual( [{'__record', test_record}, {field1, f1}, {field2, f2}], record_to_proplist(#test_record{}, record_info(fields, test_record))), ?assertEqual( [{'typekey', test_record}, {field1, f1}, {field2, f2}], record_to_proplist(#test_record{}, record_info(fields, test_record), typekey)), ok. shell_quote_test() -> ?assertEqual( "\"foo \\$bar\\\"\\`' baz\"", shell_quote("foo $bar\"`' baz")), ok. cmd_port_test_spool(Port, Acc) -> receive {Port, eof} -> Acc; {Port, {data, {eol, Data}}} -> cmd_port_test_spool(Port, ["\n", Data | Acc]); {Port, Unknown} -> throw({unknown, Unknown}) after 1000 -> throw(timeout) end. cmd_port_test() -> Port = cmd_port(["echo", "$bling$ `word`!"], [eof, stream, {line, 4096}]), Res = try lists:append(lists:reverse(cmd_port_test_spool(Port, []))) after catch port_close(Port) end, self() ! {Port, wtf}, try cmd_port_test_spool(Port, []) catch throw:{unknown, wtf} -> ok end, try cmd_port_test_spool(Port, []) catch throw:timeout -> ok end, ?assertEqual( "$bling$ `word`!\n", Res). cmd_test() -> ?assertEqual( "$bling$ `word`!\n", cmd(["echo", "$bling$ `word`!"])), ok. cmd_string_test() -> ?assertEqual( "\"echo\" \"\\$bling\\$ \\`word\\`!\"", cmd_string(["echo", "$bling$ `word`!"])), ok. cmd_status_test() -> ?assertEqual( {0, <<"$bling$ `word`!\n">>}, cmd_status(["echo", "$bling$ `word`!"])), ok. parse_header_test() -> ?assertEqual( {"multipart/form-data", [{"boundary", "AaB03x"}]}, parse_header("multipart/form-data; boundary=AaB03x")), %% This tests (currently) intentionally broken behavior ?assertEqual( {"multipart/form-data", [{"b", ""}, {"cgi", "is"}, {"broken", "true\"e"}]}, parse_header("multipart/form-data;b=;cgi=\"i\\s;broken=true\"e;=z;z")), ok. guess_mime_test() -> ?assertEqual("text/plain", guess_mime("")), ?assertEqual("text/plain", guess_mime(".text")), ?assertEqual("application/zip", guess_mime(".zip")), ?assertEqual("application/zip", guess_mime("x.zip")), ?assertEqual("text/html", guess_mime("x.html")), ?assertEqual("application/xhtml+xml", guess_mime("x.xhtml")), ?assertEqual("text/x-cross-domain-policy", guess_mime("crossdomain.xml")), ?assertEqual("text/x-cross-domain-policy", guess_mime("www/crossdomain.xml")), ok. path_split_test() -> {"", "foo/bar"} = path_split("/foo/bar"), {"foo", "bar"} = path_split("foo/bar"), {"bar", ""} = path_split("bar"), ok. urlsplit_test() -> {"", "", "/foo", "", "bar?baz"} = urlsplit("/foo#bar?baz"), {"http", "host:port", "/foo", "", "bar?baz"} = urlsplit("http://host:port/foo#bar?baz"), {"http", "host", "", "", ""} = urlsplit("http://host"), {"", "", "/wiki/Category:Fruit", "", ""} = urlsplit("/wiki/Category:Fruit"), ok. urlsplit_path_test() -> {"/foo/bar", "", ""} = urlsplit_path("/foo/bar"), {"/foo", "baz", ""} = urlsplit_path("/foo?baz"), {"/foo", "", "bar?baz"} = urlsplit_path("/foo#bar?baz"), {"/foo", "", "bar?baz#wibble"} = urlsplit_path("/foo#bar?baz#wibble"), {"/foo", "bar", "baz"} = urlsplit_path("/foo?bar#baz"), {"/foo", "bar?baz", "baz"} = urlsplit_path("/foo?bar?baz#baz"), ok. urlunsplit_test() -> "/foo#bar?baz" = urlunsplit({"", "", "/foo", "", "bar?baz"}), "http://host:port/foo#bar?baz" = urlunsplit({"http", "host:port", "/foo", "", "bar?baz"}), ok. urlunsplit_path_test() -> "/foo/bar" = urlunsplit_path({"/foo/bar", "", ""}), "/foo?baz" = urlunsplit_path({"/foo", "baz", ""}), "/foo#bar?baz" = urlunsplit_path({"/foo", "", "bar?baz"}), "/foo#bar?baz#wibble" = urlunsplit_path({"/foo", "", "bar?baz#wibble"}), "/foo?bar#baz" = urlunsplit_path({"/foo", "bar", "baz"}), "/foo?bar?baz#baz" = urlunsplit_path({"/foo", "bar?baz", "baz"}), ok. join_test() -> ?assertEqual("foo,bar,baz", join(["foo", "bar", "baz"], $,)), ?assertEqual("foo,bar,baz", join(["foo", "bar", "baz"], ",")), ?assertEqual("foo bar", join([["foo", " bar"]], ",")), ?assertEqual("foo bar,baz", join([["foo", " bar"], "baz"], ",")), ?assertEqual("foo", join(["foo"], ",")), ?assertEqual("foobarbaz", join(["foo", "bar", "baz"], "")), ?assertEqual("foo" ++ [<<>>] ++ "bar" ++ [<<>>] ++ "baz", join(["foo", "bar", "baz"], <<>>)), ?assertEqual("foobar" ++ [<<"baz">>], join(["foo", "bar", <<"baz">>], "")), ?assertEqual("", join([], "any")), ok. quote_plus_test() -> "foo" = quote_plus(foo), "1" = quote_plus(1), "1.1" = quote_plus(1.1), "foo" = quote_plus("foo"), "foo+bar" = quote_plus("foo bar"), "foo%0A" = quote_plus("foo\n"), "foo%0A" = quote_plus("foo\n"), "foo%3B%26%3D" = quote_plus("foo;&="), "foo%3B%26%3D" = quote_plus(<<"foo;&=">>), ok. unquote_test() -> ?assertEqual("foo bar", unquote("foo+bar")), ?assertEqual("foo bar", unquote("foo%20bar")), ?assertEqual("foo\r\n", unquote("foo%0D%0A")), ?assertEqual("foo\r\n", unquote(<<"foo%0D%0A">>)), ok. urlencode_test() -> "foo=bar&baz=wibble+%0D%0A&z=1" = urlencode([{foo, "bar"}, {"baz", "wibble \r\n"}, {z, 1}]), ok. parse_qs_test() -> ?assertEqual( [{"foo", "bar"}, {"baz", "wibble \r\n"}, {"z", "1"}], parse_qs("foo=bar&baz=wibble+%0D%0a&z=1")), ?assertEqual( [{"", "bar"}, {"baz", "wibble \r\n"}, {"z", ""}], parse_qs("=bar&baz=wibble+%0D%0a&z=")), ?assertEqual( [{"foo", "bar"}, {"baz", "wibble \r\n"}, {"z", "1"}], parse_qs(<<"foo=bar&baz=wibble+%0D%0a&z=1">>)), ?assertEqual( [], parse_qs("")), ?assertEqual( [{"foo", ""}, {"bar", ""}, {"baz", ""}], parse_qs("foo;bar&baz")), ok. partition_test() -> {"foo", "", ""} = partition("foo", "/"), {"foo", "/", "bar"} = partition("foo/bar", "/"), {"foo", "/", ""} = partition("foo/", "/"), {"", "/", "bar"} = partition("/bar", "/"), {"f", "oo/ba", "r"} = partition("foo/bar", "oo/ba"), ok. safe_relative_path_test() -> "foo" = safe_relative_path("foo"), "foo/" = safe_relative_path("foo/"), "foo" = safe_relative_path("foo/bar/.."), "bar" = safe_relative_path("foo/../bar"), "bar/" = safe_relative_path("foo/../bar/"), "" = safe_relative_path("foo/.."), "" = safe_relative_path("foo/../"), undefined = safe_relative_path("/foo"), undefined = safe_relative_path("../foo"), undefined = safe_relative_path("foo/../.."), undefined = safe_relative_path("foo//"), undefined = safe_relative_path("foo\\bar"), ok. parse_qvalues_test() -> [] = parse_qvalues(""), [{"identity", 0.0}] = parse_qvalues("identity;q=0"), [{"identity", 0.0}] = parse_qvalues("identity ;q=0"), [{"identity", 0.0}] = parse_qvalues(" identity; q =0 "), [{"identity", 0.0}] = parse_qvalues("identity ; q = 0"), [{"identity", 0.0}] = parse_qvalues("identity ; q= 0.0"), [{"gzip", 1.0}, {"deflate", 1.0}, {"identity", 0.0}] = parse_qvalues( "gzip,deflate,identity;q=0.0" ), [{"deflate", 1.0}, {"gzip", 1.0}, {"identity", 0.0}] = parse_qvalues( "deflate,gzip,identity;q=0.0" ), [{"gzip", 1.0}, {"deflate", 1.0}, {"gzip", 1.0}, {"identity", 0.0}] = parse_qvalues("gzip,deflate,gzip,identity;q=0"), [{"gzip", 1.0}, {"deflate", 1.0}, {"identity", 0.0}] = parse_qvalues( "gzip, deflate , identity; q=0.0" ), [{"gzip", 1.0}, {"deflate", 1.0}, {"identity", 0.0}] = parse_qvalues( "gzip; q=1, deflate;q=1.0, identity;q=0.0" ), [{"gzip", 0.5}, {"deflate", 1.0}, {"identity", 0.0}] = parse_qvalues( "gzip; q=0.5, deflate;q=1.0, identity;q=0" ), [{"gzip", 0.5}, {"deflate", 1.0}, {"identity", 0.0}] = parse_qvalues( "gzip; q=0.5, deflate , identity;q=0.0" ), [{"gzip", 0.5}, {"deflate", 0.8}, {"identity", 0.0}] = parse_qvalues( "gzip; q=0.5, deflate;q=0.8, identity;q=0.0" ), [{"gzip", 0.5}, {"deflate", 1.0}, {"identity", 1.0}] = parse_qvalues( "gzip; q=0.5,deflate,identity" ), [{"gzip", 0.5}, {"deflate", 1.0}, {"identity", 1.0}, {"identity", 1.0}] = parse_qvalues("gzip; q=0.5,deflate,identity, identity "), [{"text/html;level=1", 1.0}, {"text/plain", 0.5}] = parse_qvalues("text/html;level=1, text/plain;q=0.5"), [{"text/html;level=1", 0.3}, {"text/plain", 1.0}] = parse_qvalues("text/html;level=1;q=0.3, text/plain"), [{"text/html;level=1", 0.3}, {"text/plain", 1.0}] = parse_qvalues("text/html; level = 1; q = 0.3, text/plain"), [{"text/html;level=1", 0.3}, {"text/plain", 1.0}] = parse_qvalues("text/html;q=0.3;level=1, text/plain"), invalid_qvalue_string = parse_qvalues("gzip; q=1.1, deflate"), invalid_qvalue_string = parse_qvalues("gzip; q=0.5, deflate;q=2"), invalid_qvalue_string = parse_qvalues("gzip, deflate;q=AB"), invalid_qvalue_string = parse_qvalues("gzip; q=2.1, deflate"), invalid_qvalue_string = parse_qvalues("gzip; q=0.1234, deflate"), invalid_qvalue_string = parse_qvalues("text/html;level=1;q=0.3, text/html;level"), ok. pick_accepted_encodings_test() -> ["identity"] = pick_accepted_encodings( [], ["gzip", "identity"], "identity" ), ["gzip", "identity"] = pick_accepted_encodings( [{"gzip", 1.0}], ["gzip", "identity"], "identity" ), ["identity"] = pick_accepted_encodings( [{"gzip", 0.0}], ["gzip", "identity"], "identity" ), ["gzip", "identity"] = pick_accepted_encodings( [{"gzip", 1.0}, {"deflate", 1.0}], ["gzip", "identity"], "identity" ), ["gzip", "identity"] = pick_accepted_encodings( [{"gzip", 0.5}, {"deflate", 1.0}], ["gzip", "identity"], "identity" ), ["identity"] = pick_accepted_encodings( [{"gzip", 0.0}, {"deflate", 0.0}], ["gzip", "identity"], "identity" ), ["gzip"] = pick_accepted_encodings( [{"gzip", 1.0}, {"deflate", 1.0}, {"identity", 0.0}], ["gzip", "identity"], "identity" ), ["gzip", "deflate", "identity"] = pick_accepted_encodings( [{"gzip", 1.0}, {"deflate", 1.0}], ["gzip", "deflate", "identity"], "identity" ), ["gzip", "deflate"] = pick_accepted_encodings( [{"gzip", 1.0}, {"deflate", 1.0}, {"identity", 0.0}], ["gzip", "deflate", "identity"], "identity" ), ["deflate", "gzip", "identity"] = pick_accepted_encodings( [{"gzip", 0.2}, {"deflate", 1.0}], ["gzip", "deflate", "identity"], "identity" ), ["deflate", "deflate", "gzip", "identity"] = pick_accepted_encodings( [{"gzip", 0.2}, {"deflate", 1.0}, {"deflate", 1.0}], ["gzip", "deflate", "identity"], "identity" ), ["deflate", "gzip", "gzip", "identity"] = pick_accepted_encodings( [{"gzip", 0.2}, {"deflate", 1.0}, {"gzip", 1.0}], ["gzip", "deflate", "identity"], "identity" ), ["gzip", "deflate", "gzip", "identity"] = pick_accepted_encodings( [{"gzip", 0.2}, {"deflate", 0.9}, {"gzip", 1.0}], ["gzip", "deflate", "identity"], "identity" ), [] = pick_accepted_encodings( [{"*", 0.0}], ["gzip", "deflate", "identity"], "identity" ), ["gzip", "deflate", "identity"] = pick_accepted_encodings( [{"*", 1.0}], ["gzip", "deflate", "identity"], "identity" ), ["gzip", "deflate", "identity"] = pick_accepted_encodings( [{"*", 0.6}], ["gzip", "deflate", "identity"], "identity" ), ["gzip"] = pick_accepted_encodings( [{"gzip", 1.0}, {"*", 0.0}], ["gzip", "deflate", "identity"], "identity" ), ["gzip", "deflate"] = pick_accepted_encodings( [{"gzip", 1.0}, {"deflate", 0.6}, {"*", 0.0}], ["gzip", "deflate", "identity"], "identity" ), ["deflate", "gzip"] = pick_accepted_encodings( [{"gzip", 0.5}, {"deflate", 1.0}, {"*", 0.0}], ["gzip", "deflate", "identity"], "identity" ), ["gzip", "identity"] = pick_accepted_encodings( [{"deflate", 0.0}, {"*", 1.0}], ["gzip", "deflate", "identity"], "identity" ), ["gzip", "identity"] = pick_accepted_encodings( [{"*", 1.0}, {"deflate", 0.0}], ["gzip", "deflate", "identity"], "identity" ), ok. normalize_path_test() -> "" = normalize_path(""), "/" = normalize_path("/"), "/" = normalize_path("//"), "/" = normalize_path("///"), "foo" = normalize_path("foo"), "/foo" = normalize_path("/foo"), "/foo" = normalize_path("//foo"), "/foo" = normalize_path("///foo"), "foo/" = normalize_path("foo/"), "foo/" = normalize_path("foo//"), "foo/" = normalize_path("foo///"), "foo/bar" = normalize_path("foo/bar"), "foo/bar" = normalize_path("foo//bar"), "foo/bar" = normalize_path("foo///bar"), "foo/bar" = normalize_path("foo////bar"), "/foo/bar" = normalize_path("/foo/bar"), "/foo/bar" = normalize_path("/foo////bar"), "/foo/bar" = normalize_path("////foo/bar"), "/foo/bar" = normalize_path("////foo///bar"), "/foo/bar" = normalize_path("////foo////bar"), "/foo/bar/" = normalize_path("/foo/bar/"), "/foo/bar/" = normalize_path("////foo/bar/"), "/foo/bar/" = normalize_path("/foo////bar/"), "/foo/bar/" = normalize_path("/foo/bar////"), "/foo/bar/" = normalize_path("///foo////bar/"), "/foo/bar/" = normalize_path("////foo/bar////"), "/foo/bar/" = normalize_path("/foo///bar////"), "/foo/bar/" = normalize_path("////foo///bar////"), ok. -endif. mochiweb-3.2.1/src/mochiweb_websocket.erl000066400000000000000000000220461450333227400204570ustar00rootroot00000000000000-module(mochiweb_websocket). -author('lukasz.lalik@zadane.pl'). %% The MIT License (MIT) %% Copyright (c) 2012 Zadane.pl sp. z o.o. %% Permission is hereby granted, free of charge, to any person obtaining a copy %% of this software and associated documentation files (the "Software"), to deal %% in the Software without restriction, including without limitation the rights %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell %% copies of the Software, and to permit persons to whom the Software is %% furnished to do so, subject to the following conditions: %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN %% THE SOFTWARE. %% @doc Websockets module for Mochiweb. Based on Misultin websockets module. -export([loop/5, request/5, upgrade_connection/2]). -export([send/3]). -ifdef(TEST). -export([hixie_handshake/7, make_handshake/1, parse_hixie_frames/2, parse_hybi_frames/3]). -endif. loop(Socket, Body, State, WsVersion, ReplyChannel) -> ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, 0}, {active, once}])), proc_lib:hibernate(?MODULE, request, [Socket, Body, State, WsVersion, ReplyChannel]). request(Socket, Body, State, WsVersion, ReplyChannel) -> receive {tcp_closed = Error, _} -> mochiweb_socket:close(Socket), exit({shutdown, Error}); {ssl_closed = Error, _} -> mochiweb_socket:close(Socket), exit({shutdown, Error}); {tcp_error, _, Error} -> mochiweb_socket:close(Socket), exit({shutdown, {tcp_error, Error}}); {Proto, _, WsFrames} when Proto =:= tcp orelse Proto =:= ssl -> case parse_frames(WsVersion, WsFrames, Socket) of close -> mochiweb_socket:close(Socket), exit({shutdown, websocket_parse_frames_close}); error -> mochiweb_socket:close(Socket), exit({shutdown, websocket_parse_frames_error}); Payload -> NewState = call_body(Body, Payload, State, ReplyChannel), loop(Socket, Body, NewState, WsVersion, ReplyChannel) end; _ -> mochiweb_socket:close(Socket), exit({shutdown, websocket_request_error}) end. call_body({M, F, A}, Payload, State, ReplyChannel) -> erlang:apply(M, F, [Payload, State, ReplyChannel | A]); call_body({M, F}, Payload, State, ReplyChannel) -> M:F(Payload, State, ReplyChannel); call_body(Body, Payload, State, ReplyChannel) -> Body(Payload, State, ReplyChannel). send(Socket, Payload, hybi) -> Prefix = <<1:1, 0:3, 1:4, (payload_length(iolist_size(Payload)))/binary>>, mochiweb_socket:send(Socket, [Prefix, Payload]); send(Socket, Payload, hixie) -> mochiweb_socket:send(Socket, [0, Payload, 255]). upgrade_connection({ReqM, _} = Req, Body) -> case make_handshake(Req) of {Version, Response} -> ReqM:respond(Response, Req), Socket = ReqM:get(socket, Req), ReplyChannel = fun (Payload) -> (?MODULE):send(Socket, Payload, Version) end, Reentry = fun (State) -> (?MODULE):loop(Socket, Body, State, Version, ReplyChannel) end, {Reentry, ReplyChannel}; _ -> mochiweb_socket:close(ReqM:get(socket, Req)), exit({shutdown, websocket_handshake_error}) end. make_handshake({ReqM, _} = Req) -> SecKey = ReqM:get_header_value("sec-websocket-key", Req), Sec1Key = ReqM:get_header_value("Sec-WebSocket-Key1", Req), Sec2Key = ReqM:get_header_value("Sec-WebSocket-Key2", Req), Origin = ReqM:get_header_value(origin, Req), if SecKey =/= undefined -> hybi_handshake(SecKey); Sec1Key =/= undefined andalso Sec2Key =/= undefined -> Host = ReqM:get_header_value("Host", Req), Path = ReqM:get(path, Req), Body = ReqM:recv(8, Req), Scheme = scheme(Req), hixie_handshake(Scheme, Host, Path, Sec1Key, Sec2Key, Body, Origin); true -> error end. hybi_handshake(SecKey) -> BinKey = list_to_binary(SecKey), Bin = <>, Challenge = base64:encode(crypto:hash(sha, Bin)), Response = {101, [{"Connection", "Upgrade"}, {"Upgrade", "websocket"}, {"Sec-Websocket-Accept", Challenge}], ""}, {hybi, Response}. scheme(Req) -> case mochiweb_request:get(scheme, Req) of http -> "ws://"; https -> "wss://" end. hixie_handshake(Scheme, Host, Path, Key1, Key2, Body, Origin) -> Ikey1 = [D || D <- Key1, $0 =< D, D =< $9], Ikey2 = [D || D <- Key2, $0 =< D, D =< $9], Blank1 = length([D || D <- Key1, D =:= 32]), Blank2 = length([D || D <- Key2, D =:= 32]), Part1 = erlang:list_to_integer(Ikey1) div Blank1, Part2 = erlang:list_to_integer(Ikey2) div Blank2, Ckey = <>, Challenge = erlang:md5(Ckey), Location = lists:concat([Scheme, Host, Path]), Response = {101, [{"Upgrade", "WebSocket"}, {"Connection", "Upgrade"}, {"Sec-WebSocket-Origin", Origin}, {"Sec-WebSocket-Location", Location}], Challenge}, {hixie, Response}. parse_frames(hybi, Frames, Socket) -> try parse_hybi_frames(Socket, Frames, []) of Parsed -> process_frames(Parsed, []) catch _:_ -> error end; parse_frames(hixie, Frames, _Socket) -> try parse_hixie_frames(Frames, []) of Payload -> Payload catch _:_ -> error end. %% %% Websockets internal functions for RFC6455 and hybi draft %% process_frames([], Acc) -> lists:reverse(Acc); process_frames([{Opcode, Payload} | Rest], Acc) -> case Opcode of 8 -> close; _ -> process_frames(Rest, [Payload | Acc]) end. parse_hybi_frames(_, <<>>, Acc) -> lists:reverse(Acc); parse_hybi_frames(S, <<_Fin:1, _Rsv:3, Opcode:4, _Mask:1, PayloadLen:7, MaskKey:4/binary, Payload:PayloadLen/binary-unit:8, Rest/binary>>, Acc) when PayloadLen < 126 -> Payload2 = hybi_unmask(Payload, MaskKey, <<>>), parse_hybi_frames(S, Rest, [{Opcode, Payload2} | Acc]); parse_hybi_frames(S, <<_Fin:1, _Rsv:3, Opcode:4, _Mask:1, 126:7, PayloadLen:16, MaskKey:4/binary, Payload:PayloadLen/binary-unit:8, Rest/binary>>, Acc) -> Payload2 = hybi_unmask(Payload, MaskKey, <<>>), parse_hybi_frames(S, Rest, [{Opcode, Payload2} | Acc]); parse_hybi_frames(Socket, <<_Fin:1, _Rsv:3, _Opcode:4, _Mask:1, 126:7, _PayloadLen:16, _MaskKey:4/binary, _/binary-unit:8>> = PartFrame, Acc) -> ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, 0}, {active, once}])), receive {tcp_closed = Error, _} -> mochiweb_socket:close(Socket), exit({shutdown, Error}); {ssl_closed = Error, _} -> mochiweb_socket:close(Socket), exit({shutdown, Error}); {tcp_error, _, Error} -> mochiweb_socket:close(Socket), exit({shutdown, {tcp_error, Error}}); {Proto, _, Continuation} when Proto =:= tcp orelse Proto =:= ssl -> parse_hybi_frames(Socket, <>, Acc); _ -> mochiweb_socket:close(Socket), exit({shutdown, parse_hybi_frames_error}) after 5000 -> mochiweb_socket:close(Socket), exit({shutdown, parse_hybi_frames_timeout}) end; parse_hybi_frames(S, <<_Fin:1, _Rsv:3, Opcode:4, _Mask:1, 127:7, 0:1, PayloadLen:63, MaskKey:4/binary, Payload:PayloadLen/binary-unit:8, Rest/binary>>, Acc) -> Payload2 = hybi_unmask(Payload, MaskKey, <<>>), parse_hybi_frames(S, Rest, [{Opcode, Payload2} | Acc]). %% Unmasks RFC 6455 message hybi_unmask(<>, MaskKey, Acc) -> <> = MaskKey, hybi_unmask(Rest, MaskKey, <>); hybi_unmask(<>, MaskKey, Acc) -> <> = MaskKey, <>; hybi_unmask(<>, MaskKey, Acc) -> <> = MaskKey, <>; hybi_unmask(<>, MaskKey, Acc) -> <> = MaskKey, <>; hybi_unmask(<<>>, _MaskKey, Acc) -> Acc. payload_length(N) -> case N of N when N =< 125 -> <>; N when N =< 65535 -> <<126, N:16>>; N when N =< 9223372036854775807 -> <<127, N:64>> end. %% %% Websockets internal functions for hixie-76 websocket version %% parse_hixie_frames(<<>>, Frames) -> lists:reverse(Frames); parse_hixie_frames(<<0, T/binary>>, Frames) -> {Frame, Rest} = parse_hixie(T, <<>>), parse_hixie_frames(Rest, [Frame | Frames]). parse_hixie(<<255, Rest/binary>>, Buffer) -> {Buffer, Rest}; parse_hixie(<>, Buffer) -> parse_hixie(T, <>). mochiweb-3.2.1/src/reloader.erl000066400000000000000000000134161450333227400164120ustar00rootroot00000000000000%% @author Matthew Dempsky %% @copyright 2007 Mochi Media, Inc. %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% @doc Erlang module for automatically reloading modified modules %% during development. -module(reloader). -author("Matthew Dempsky "). -include_lib("kernel/include/file.hrl"). -behaviour(gen_server). -export([start/0, start_link/0]). -export([stop/0]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([all_changed/0]). -export([is_changed/1]). -export([reload_modules/1]). -record(state, {last, tref}). %% External API %% @spec start() -> ServerRet %% @doc Start the reloader. start() -> gen_server:start({local, ?MODULE}, ?MODULE, [], []). %% @spec start_link() -> ServerRet %% @doc Start the reloader. start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). %% @spec stop() -> ok %% @doc Stop the reloader. stop() -> gen_server:call(?MODULE, stop). %% gen_server callbacks %% @spec init([]) -> {ok, State} %% @doc gen_server init, opens the server in an initial state. init([]) -> {ok, TRef} = timer:send_interval(timer:seconds(1), doit), {ok, #state{last = stamp(), tref = TRef}}. %% @spec handle_call(Args, From, State) -> tuple() %% @doc gen_server callback. handle_call(stop, _From, State) -> {stop, shutdown, stopped, State}; handle_call(_Req, _From, State) -> {reply, {error, badrequest}, State}. %% @spec handle_cast(Cast, State) -> tuple() %% @doc gen_server callback. handle_cast(_Req, State) -> {noreply, State}. %% @spec handle_info(Info, State) -> tuple() %% @doc gen_server callback. handle_info(doit, State) -> Now = stamp(), _ = doit(State#state.last, Now), {noreply, State#state{last = Now}}; handle_info(_Info, State) -> {noreply, State}. %% @spec terminate(Reason, State) -> ok %% @doc gen_server termination callback. terminate(_Reason, State) -> {ok, cancel} = timer:cancel(State#state.tref), ok. %% @spec code_change(_OldVsn, State, _Extra) -> State %% @doc gen_server code_change callback (trivial). code_change(_Vsn, State, _Extra) -> {ok, State}. %% @spec reload_modules([atom()]) -> [{module, atom()} | {error, term()}] %% @doc code:purge/1 and code:load_file/1 the given list of modules in order, %% return the results of code:load_file/1. reload_modules(Modules) -> [begin code:purge(M), code:load_file(M) end || M <- Modules]. %% @spec all_changed() -> [atom()] %% @doc Return a list of beam modules that have changed. all_changed() -> [M || {M, Fn} <- code:all_loaded(), is_list(Fn), is_changed(M)]. %% @spec is_changed(atom()) -> boolean() %% @doc true if the loaded module is a beam with a vsn attribute %% and does not match the on-disk beam file, returns false otherwise. is_changed(M) -> try module_vsn(M:module_info()) =/= module_vsn(code:get_object_code(M)) catch _:_ -> false end. %% Internal API module_vsn({M, Beam, _Fn}) -> {ok, {M, Vsn}} = beam_lib:version(Beam), Vsn; module_vsn(L) when is_list(L) -> {_, Attrs} = lists:keyfind(attributes, 1, L), {_, Vsn} = lists:keyfind(vsn, 1, Attrs), Vsn. doit(From, To) -> [case file:read_file_info(Filename) of {ok, #file_info{mtime = Mtime}} when Mtime >= From, Mtime < To -> reload(Module); {ok, _} -> unmodified; {error, enoent} -> %% The Erlang compiler deletes existing .beam files if %% recompiling fails. Maybe it's worth spitting out a %% warning here, but I'd want to limit it to just once. gone; {error, Reason} -> io:format("Error reading ~s's file info: ~p~n", [Filename, Reason]), error end || {Module, Filename} <- code:all_loaded(), is_list(Filename)]. reload(Module) -> io:format("Reloading ~p ...", [Module]), code:purge(Module), case code:load_file(Module) of {module, Module} -> io:format(" ok.~n"), case erlang:function_exported(Module, test, 0) of true -> io:format(" - Calling ~p:test() ...", [Module]), case catch Module:test() of ok -> io:format(" ok.~n"), reload; Reason -> io:format(" fail: ~p.~n", [Reason]), reload_but_test_failed end; false -> reload end; {error, Reason} -> io:format(" fail: ~p.~n", [Reason]), error end. stamp() -> erlang:localtime(). %% %% Tests %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -endif. mochiweb-3.2.1/support/000077500000000000000000000000001450333227400150315ustar00rootroot00000000000000mochiweb-3.2.1/support/test-materials/000077500000000000000000000000001450333227400177675ustar00rootroot00000000000000mochiweb-3.2.1/support/test-materials/test_ssl_cert.pem000066400000000000000000000021671450333227400233550ustar00rootroot00000000000000-----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----- mochiweb-3.2.1/support/test-materials/test_ssl_key.pem000066400000000000000000000032171450333227400232050ustar00rootroot00000000000000-----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----- mochiweb-3.2.1/test/000077500000000000000000000000001450333227400142745ustar00rootroot00000000000000mochiweb-3.2.1/test/mochiweb_base64url_tests.erl000066400000000000000000000012531450333227400217070ustar00rootroot00000000000000-module(mochiweb_base64url_tests). -include_lib("eunit/include/eunit.hrl"). id(X) -> ?assertEqual( X, mochiweb_base64url:decode(mochiweb_base64url:encode(X))), ?assertEqual( X, mochiweb_base64url:decode( binary_to_list(mochiweb_base64url:encode(binary_to_list(X))))). random_binary(Short,Long) -> << <<(rand:uniform(256) - 1)>> || _ <- lists:seq(1, Short + rand:uniform(1 + Long - Short) - 1) >>. empty_test() -> id(<<>>). onechar_test() -> [id(<>) || C <- lists:seq(0,255)], ok. nchar_test() -> %% 1000 tests of 2-6 char strings [id(B) || _ <- lists:seq(1,1000), B <- [random_binary(2, 6)]], ok. mochiweb-3.2.1/test/mochiweb_html_tests.erl000066400000000000000000000631001450333227400210430ustar00rootroot00000000000000-module(mochiweb_html_tests). -include_lib("eunit/include/eunit.hrl"). to_html_test() -> ?assertEqual( <<"hey!

what's up

sucka
RAW!">>, iolist_to_binary( mochiweb_html:to_html({html, [], [{<<"head">>, [], [{title, <<"hey!">>}]}, {body, [], [{p, [{class, foo}], [<<"what's">>, <<" up">>, {br}]}, {'div', <<"sucka">>}, {'=', <<"RAW!">>}, {comment, <<" comment! ">>}]}]}))), ?assertEqual( <<"">>, iolist_to_binary( mochiweb_html:to_html({doctype, [<<"html">>, <<"PUBLIC">>, <<"-//W3C//DTD XHTML 1.0 Transitional//EN">>, <<"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">>]}))), ?assertEqual( <<"">>, iolist_to_binary( mochiweb_html:to_html({<<"html">>,[], [{pi, <<"xml:namespace">>, [{<<"prefix">>,<<"o">>}, {<<"ns">>,<<"urn:schemas-microsoft-com:office:office">>}]}]}))), ok. escape_test() -> ?assertEqual( <<"&quot;\"word ><<up!&quot;">>, mochiweb_html:escape(<<""\"word ><>)), ?assertEqual( <<"&quot;\"word ><<up!&quot;">>, mochiweb_html:escape(""\"word ><>, mochiweb_html:escape('"\"word >< ?assertEqual( <<"&quot;"word ><<up!&quot;">>, mochiweb_html:escape_attr(<<""\"word ><>)), ?assertEqual( <<"&quot;"word ><<up!&quot;">>, mochiweb_html:escape_attr(""\"word ><>, mochiweb_html:escape_attr('"\"word ><>, mochiweb_html:escape_attr(12345)), ?assertEqual( <<"1.5">>, mochiweb_html:escape_attr(1.5)), ok. tokens_test() -> ?assertEqual( [{start_tag, <<"foo">>, [{<<"bar">>, <<"baz">>}, {<<"wibble">>, <<"wibble">>}, {<<"alice">>, <<"bob">>}], true}], mochiweb_html:tokens(<<"">>)), ?assertEqual( [{start_tag, <<"foo">>, [{<<"bar">>, <<"baz">>}, {<<"wibble">>, <<"wibble">>}, {<<"alice">>, <<"bob">>}], true}], mochiweb_html:tokens(<<"">>)), ?assertEqual( [{comment, <<"[if lt IE 7]>\n\n>}], mochiweb_html:tokens(<<"">>)), ?assertEqual( [{start_tag, <<"script">>, [{<<"type">>, <<"text/javascript">>}], false}, {data, <<" A= B <= C ">>, false}, {end_tag, <<"script">>}], mochiweb_html:tokens(<<"">>)), ?assertEqual( [{start_tag, <<"script">>, [{<<"type">>, <<"text/javascript">>}], false}, {data, <<" A= B <= C ">>, false}, {end_tag, <<"script">>}], mochiweb_html:tokens(<<"">>)), ?assertEqual( [{start_tag, <<"script">>, [{<<"type">>, <<"text/javascript">>}], false}, {data, <<" A= B <= C ">>, false}, {end_tag, <<"script">>}], mochiweb_html:tokens(<<"">>)), ?assertEqual( [{start_tag, <<"script">>, [{<<"type">>, <<"text/javascript">>}], false}, {data, <<" A= B <= C ">>, false}, {end_tag, <<"script">>}], mochiweb_html:tokens(<<"">>)), ?assertEqual( [{start_tag, <<"textarea">>, [], false}, {data, <<"">>, false}, {end_tag, <<"textarea">>}], mochiweb_html:tokens(<<"">>)), ?assertEqual( [{start_tag, <<"textarea">>, [], false}, {data, <<"">>, false}], mochiweb_html:tokens(<<"