pax_global_header 0000666 0000000 0000000 00000000064 14503332274 0014515 g ustar 00root root 0000000 0000000 52 comment=897f22f6720cf369547d1303888b46b5a00e00b4
mochiweb-3.2.1/ 0000775 0000000 0000000 00000000000 14503332274 0013315 5 ustar 00root root 0000000 0000000 mochiweb-3.2.1/.editorconfig 0000664 0000000 0000000 00000000550 14503332274 0015772 0 ustar 00root root 0000000 0000000 # 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/ 0000775 0000000 0000000 00000000000 14503332274 0014655 5 ustar 00root root 0000000 0000000 mochiweb-3.2.1/.github/workflows/ 0000775 0000000 0000000 00000000000 14503332274 0016712 5 ustar 00root root 0000000 0000000 mochiweb-3.2.1/.github/workflows/ci.yml 0000664 0000000 0000000 00000001150 14503332274 0020025 0 ustar 00root root 0000000 0000000 on:
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.yml 0000664 0000000 0000000 00000000563 14503332274 0021061 0 ustar 00root root 0000000 0000000 on:
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/.gitignore 0000664 0000000 0000000 00000000175 14503332274 0015310 0 ustar 00root root 0000000 0000000 /ebin
/doc
/_test
/.eunit
/docs
.DS_Store
/TEST-*.xml
/deps
/.rebar
*.swp
*.beam
*.dump
rebar.lock
/_build
/rebar3.crashdump
mochiweb-3.2.1/CHANGES.md 0000664 0000000 0000000 00000027262 14503332274 0014720 0 ustar 00root root 0000000 0000000 Version 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/LICENSE 0000664 0000000 0000000 00000002077 14503332274 0014330 0 ustar 00root root 0000000 0000000 This 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/Makefile 0000664 0000000 0000000 00000000355 14503332274 0014760 0 ustar 00root root 0000000 0000000 REBAR?=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.md 0000664 0000000 0000000 00000003563 14503332274 0014603 0 ustar 00root root 0000000 0000000 # 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.config 0000664 0000000 0000000 00000000145 14503332274 0016452 0 ustar 00root root 0000000 0000000 apps_dirs:
- "_build/default/lib/*"
include_dirs:
- "_build/default/lib/*/include"
- "include"
mochiweb-3.2.1/examples/ 0000775 0000000 0000000 00000000000 14503332274 0015133 5 ustar 00root root 0000000 0000000 mochiweb-3.2.1/examples/example_project/ 0000775 0000000 0000000 00000000000 14503332274 0020314 5 ustar 00root root 0000000 0000000 mochiweb-3.2.1/examples/example_project/.gitignore 0000664 0000000 0000000 00000000153 14503332274 0022303 0 ustar 00root root 0000000 0000000 /ebin
/doc
/_test
/.eunit
/docs
.DS_Store
/TEST-*.xml
/deps
/.rebar
*.swp
*.beam
*.dump
_build/
rebar.lock
mochiweb-3.2.1/examples/example_project/Makefile 0000664 0000000 0000000 00000000323 14503332274 0021752 0 ustar 00root root 0000000 0000000 REBAR?=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.sh 0000775 0000000 0000000 00000000466 14503332274 0021740 0 ustar 00root root 0000000 0000000 #!/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/ 0000775 0000000 0000000 00000000000 14503332274 0021274 5 ustar 00root root 0000000 0000000 mochiweb-3.2.1/examples/example_project/priv/www/ 0000775 0000000 0000000 00000000000 14503332274 0022120 5 ustar 00root root 0000000 0000000 mochiweb-3.2.1/examples/example_project/priv/www/index.html 0000664 0000000 0000000 00000000137 14503332274 0024116 0 ustar 00root root 0000000 0000000
It Worked
example_project running.
mochiweb-3.2.1/examples/example_project/rebar.config 0000664 0000000 0000000 00000000347 14503332274 0022602 0 ustar 00root root 0000000 0000000 %% -*- 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/ 0000775 0000000 0000000 00000000000 14503332274 0021103 5 ustar 00root root 0000000 0000000 mochiweb-3.2.1/examples/example_project/src/example_project.app.src 0000664 0000000 0000000 00000000353 14503332274 0025555 0 ustar 00root root 0000000 0000000 %% -*- 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.erl 0000664 0000000 0000000 00000001241 14503332274 0024766 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000001145 14503332274 0025631 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000006120 14503332274 0026002 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000003072 14503332274 0025661 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000004242 14503332274 0025627 0 ustar 00root root 0000000 0000000 %% @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.sh 0000775 0000000 0000000 00000000356 14503332274 0022570 0 ustar 00root root 0000000 0000000 #!/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/ 0000775 0000000 0000000 00000000000 14503332274 0016674 5 ustar 00root root 0000000 0000000 mochiweb-3.2.1/examples/hmac_api/README 0000664 0000000 0000000 00000020457 14503332274 0017564 0 ustar 00root root 0000000 0000000 Introduction
------------
This example shows how to make an Amazon-style HMAC authentication system for an API with mochiweb.
Purpose
-------
The purpose of this example is to:
* make it easy to implement an API in mochiweb
- using a proven approach so that 'amateurs' don't have to reinvent crypto
* make it easy to generate client libraries for that API so that client-side implementers can:
- reuse closely related code examples
- build compatibility unit tests instead of fiddling around debugging their library against live implementations of the system
Scope
-----
The scope of this document is:
* a description of the client-server exchange
* a reference implementation of
- the server-side implementation of the exchange
- the client-side implementation of the exchange
* developing a custom implementation of an API
* deploying that implementation to new client-side users to build their client libraries
Contents
--------
Subsequent sections of this document are:
* the client-server exchange
* the reference implementation in this example
* building a custom implementation
* deploying a custom implementation
The Client-Server Exchange
--------------------------
OVERVIEW
This section describes the client-server exchange for an Amazon-style API authentication schema. It has the following characteristics:
* based on a public key/private key
* used to authenticate non-SSL api requests
* not a full once-use schema and is vulnerable to replay attacks within a short time window
TYPE OF API
The api described in this document is:
* suitable for machine-machine communication
The api described in this document is NOT:
* an implementation of 2-legged OAUTH
- see https://github.com/tim/erlang-oauth
* an implementation of 3-legged OAUTH
It is not suitable for use in applications where an end user has to log into a service and piggy-back on top of a 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.hrl 0000664 0000000 0000000 00000003644 14503332274 0021153 0 ustar 00root root 0000000 0000000 -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.erl 0000664 0000000 0000000 00000002364 14503332274 0022504 0 ustar 00root root 0000000 0000000 -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.erl 0000664 0000000 0000000 00000035170 14503332274 0021775 0 ustar 00root root 0000000 0000000 -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/ 0000775 0000000 0000000 00000000000 14503332274 0016275 5 ustar 00root root 0000000 0000000 mochiweb-3.2.1/examples/https/https_store.erl 0000664 0000000 0000000 00000007136 14503332274 0021366 0 ustar 00root root 0000000 0000000 %% 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.pem 0000664 0000000 0000000 00000002167 14503332274 0021331 0 ustar 00root root 0000000 0000000 -----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.pem 0000664 0000000 0000000 00000003217 14503332274 0021161 0 ustar 00root root 0000000 0000000 -----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/ 0000775 0000000 0000000 00000000000 14503332274 0017100 5 ustar 00root root 0000000 0000000 mochiweb-3.2.1/examples/keepalive/keepalive.erl 0000664 0000000 0000000 00000006242 14503332274 0021555 0 ustar 00root root 0000000 0000000 -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/ 0000775 0000000 0000000 00000000000 14503332274 0017121 5 ustar 00root root 0000000 0000000 mochiweb-3.2.1/examples/websocket/index.html 0000664 0000000 0000000 00000002754 14503332274 0021126 0 ustar 00root root 0000000 0000000
Websockets With Mochiweb Demo
Mochiweb websocket demo
Connect
State:
Protip: open your javascript error console, just in case..
mochiweb-3.2.1/examples/websocket/websocket.erl 0000664 0000000 0000000 00000012526 14503332274 0021621 0 ustar 00root root 0000000 0000000 -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/ 0000775 0000000 0000000 00000000000 14503332274 0014740 5 ustar 00root root 0000000 0000000 mochiweb-3.2.1/include/internal.hrl 0000664 0000000 0000000 00000000036 14503332274 0017262 0 ustar 00root root 0000000 0000000
-define(RECBUF_SIZE, 8192).
mochiweb-3.2.1/rebar.config 0000664 0000000 0000000 00000001512 14503332274 0015576 0 ustar 00root root 0000000 0000000 % -*- 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/ 0000775 0000000 0000000 00000000000 14503332274 0015004 5 ustar 00root root 0000000 0000000 mochiweb-3.2.1/scripts/entities.erl 0000775 0000000 0000000 00000002621 14503332274 0017340 0 ustar 00root root 0000000 0000000 #!/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/ 0000775 0000000 0000000 00000000000 14503332274 0014104 5 ustar 00root root 0000000 0000000 mochiweb-3.2.1/src/mochifmt.erl 0000664 0000000 0000000 00000037134 14503332274 0016426 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000004151 14503332274 0020140 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000003504 14503332274 0017272 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000007510 14503332274 0017073 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000006066 14503332274 0016424 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000047323 14503332274 0016612 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000103473 14503332274 0016673 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000007412 14503332274 0016772 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000011412 14503332274 0017332 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000026612 14503332274 0016436 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000021110 14503332274 0016570 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000030511 14503332274 0016516 0 ustar 00root root 0000000 0000000 %% @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.src 0000664 0000000 0000000 00000000571 14503332274 0017174 0 ustar 00root root 0000000 0000000 %% 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.erl 0000664 0000000 0000000 00000007351 14503332274 0016413 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000005420 14503332274 0020266 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000010404 14503332274 0020273 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000206234 14503332274 0020106 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000005641 14503332274 0017566 0 ustar 00root root 0000000 0000000 %% 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.erl 0000664 0000000 0000000 00000032003 14503332274 0020117 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000005747 14503332274 0017620 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000003670 14503332274 0017411 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000040234 14503332274 0020103 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000073176 14503332274 0017447 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000026161 14503332274 0017452 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000004212 14503332274 0017073 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000026500 14503332274 0017417 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000100001 14503332274 0020476 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000112476 14503332274 0020170 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000006535 14503332274 0020334 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000021566 14503332274 0020162 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000020603 14503332274 0017756 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000042302 14503332274 0021344 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000106413 14503332274 0017447 0 ustar 00root root 0000000 0000000 %% @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.erl 0000664 0000000 0000000 00000022046 14503332274 0020457 0 ustar 00root root 0000000 0000000 -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.erl 0000664 0000000 0000000 00000013416 14503332274 0016412 0 ustar 00root root 0000000 0000000 %% @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/ 0000775 0000000 0000000 00000000000 14503332274 0015031 5 ustar 00root root 0000000 0000000 mochiweb-3.2.1/support/test-materials/ 0000775 0000000 0000000 00000000000 14503332274 0017767 5 ustar 00root root 0000000 0000000 mochiweb-3.2.1/support/test-materials/test_ssl_cert.pem 0000664 0000000 0000000 00000002167 14503332274 0023355 0 ustar 00root root 0000000 0000000 -----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.pem 0000664 0000000 0000000 00000003217 14503332274 0023205 0 ustar 00root root 0000000 0000000 -----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/ 0000775 0000000 0000000 00000000000 14503332274 0014274 5 ustar 00root root 0000000 0000000 mochiweb-3.2.1/test/mochiweb_base64url_tests.erl 0000664 0000000 0000000 00000001253 14503332274 0021707 0 ustar 00root root 0000000 0000000 -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.erl 0000664 0000000 0000000 00000063100 14503332274 0021043 0 ustar 00root root 0000000 0000000 -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(
<<""\"word ><<up!"">>,
mochiweb_html:escape(<<""\"word ><>)),
?assertEqual(
<<""\"word ><<up!"">>,
mochiweb_html:escape(""\"word ><>,
mochiweb_html:escape('"\"word ><
?assertEqual(
<<"""word ><<up!"">>,
mochiweb_html:escape_attr(<<""\"word ><>)),
?assertEqual(
<<"""word ><<up!"">>,
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(<<"