├── .github
└── workflows
│ └── erlang.yml
├── .gitignore
├── .travis.yml
├── LICENSE
├── Makefile
├── README.md
├── THANKS
├── docs
└── http-headers-status-v3.png
├── include
├── webmachine.hrl
├── webmachine_logger.hrl
├── wm_compat.hrl
├── wm_reqdata.hrl
├── wm_reqstate.hrl
└── wm_resource.hrl
├── priv
├── trace
│ ├── http-headers-status-v3.png
│ ├── wmtrace.css
│ └── wmtrace.js
└── www
│ └── index.html
├── rebar.config
├── rebar.config.script
├── rebar3
├── src
├── webmachine.app.src
├── webmachine.erl
├── webmachine_access_log_handler.erl
├── webmachine_app.erl
├── webmachine_decision_core.erl
├── webmachine_deps.erl
├── webmachine_dispatcher.erl
├── webmachine_error.erl
├── webmachine_error_handler.erl
├── webmachine_error_log_handler.erl
├── webmachine_headers.erl
├── webmachine_log.erl
├── webmachine_logger_watcher.erl
├── webmachine_logger_watcher_sup.erl
├── webmachine_mochiweb.erl
├── webmachine_multipart.erl
├── webmachine_perf_log_handler.erl
├── webmachine_request.erl
├── webmachine_resource.erl
├── webmachine_router.erl
├── webmachine_sup.erl
├── webmachine_util.erl
├── wmtrace_resource.erl
└── wrq.erl
└── test
├── decision_core_test.erl
├── etag_test.erl
├── wm_echo_host_header.erl
├── wm_integration_test.erl
└── wm_integration_test_util.erl
/.github/workflows/erlang.yml:
--------------------------------------------------------------------------------
1 | name: Erlang CI
2 |
3 | on:
4 | push:
5 | branches: [ develop ]
6 | pull_request:
7 | branches: [ develop ]
8 |
9 |
10 | jobs:
11 |
12 | build:
13 |
14 | runs-on: ubuntu-latest
15 |
16 | strategy:
17 | fail-fast: false
18 | matrix:
19 | otp:
20 | - "25.1"
21 | - "24.3"
22 | - "22.3"
23 |
24 | container:
25 | image: erlang:${{ matrix.otp }}
26 |
27 | steps:
28 | - uses: lukka/get-cmake@latest
29 | - uses: actions/checkout@v2
30 | - name: Compile
31 | run: ./rebar3 compile
32 | - name: Run xref and dialyzer
33 | run: ./rebar3 do xref, dialyzer
34 | - name: Run eunit
35 | run: ./rebar3 as gha do eunit
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | deps/*
2 | *.beam
3 | .eunit/*
4 | ebin
5 | doc/*
6 | .local_dialyzer_plt
7 | /.rebar
8 | _build
9 | .DS_Store
10 | rebar.lock
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | ---
2 | script: "make all"
3 | language: erlang
4 | sudo: false
5 | install: "true" # don't let travis run get-deps
6 | otp_release:
7 | - 21.0
8 | - 20.0
9 | - 19.1
10 | - 18.3
11 | - 17.5
12 | - R16B03-1
13 | ## These won't work until we have checks for application:ensure_all_started
14 | # - R15B03
15 | # - R14B04
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | REBAR3_URL=https://s3.amazonaws.com/rebar3/rebar3
2 |
3 | # If there is a rebar in the current directory, use it
4 | ifeq ($(wildcard rebar3),rebar3)
5 | REBAR3 = $(CURDIR)/rebar3
6 | endif
7 |
8 | # Fallback to rebar on PATH
9 | REBAR3 ?= $(shell which rebar3)
10 |
11 | # And finally, prep to download rebar if all else fails
12 | ifeq ($(REBAR3),)
13 | REBAR3 = $(CURDIR)/rebar3
14 | endif
15 |
16 |
17 | all: $(REBAR3)
18 | @$(REBAR3) do update, clean, compile, eunit, dialyzer
19 |
20 | $(REBAR3):
21 | curl -Lo rebar3 $(REBAR3_URL) || wget $(REBAR3_URL)
22 | chmod a+x rebar3
23 |
24 | distclean:
25 | @rm -rf ./_build
26 |
27 | edoc:
28 | @$(REBAR3) edoc
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | webmachine
2 | ==========
3 |
4 | This project began at [Basho](http://basho.com), the creators and
5 | maintainers of Riak. Due to the importance of webmachine to the
6 | broader Erlang community, a new organization was formed. Please
7 | contact [@seancribbs](http://github.com/seancribbs) to get involved.
8 |
9 | ### Overview
10 |
11 | [](https://github.com/basho/webmachine/actions)
12 |
13 | Webmachine is an application layer that adds HTTP semantic awareness
14 | on top of the excellent bit-pushing and HTTP syntax-management
15 | provided by mochiweb, and provides a simple and clean way to connect
16 | that to your application's behavior.
17 |
18 | More information is available
19 | [here](https://github.com/webmachine/webmachine/wiki). You can also
20 | read past blog posts about Webmachine
21 | [here](http://basho.com/tag/webmachine/).
22 |
23 | ### Development
24 |
25 | Webmachine is a [rebar3](http://rebar3.org) project.
26 |
27 | Running all tests and dialyzer is as easy as
28 |
29 | ```
30 | make all
31 | ```
32 |
33 | However, if you'd like to run them separately:
34 |
35 | * EUnit: `rebar3 eunit`
36 | * Dialyzer: `rebar3 dialyzer`
37 |
38 |
39 | If you don't have `rebar3`, you should get it. If you don't want to,
40 | it's downloaded as part of `make all`
41 |
42 | ### Quick Start
43 |
44 | A [rebar3](http://rebar3.org) template is provided for users quickly
45 | and easily create a new `webmachine` application.
46 |
47 | ```
48 | $ mkdir -p ~/.config/rebar3/templates
49 | $ git clone https://github.com/webmachine/webmachine-rebar3-template.git ~/.config/rebar3/templates
50 | $ rebar3 new webmachine your_app_here
51 | ```
52 |
53 | Once a new application has been created it can be built and started.
54 |
55 | ```
56 | $ cd your_app_here
57 | $ rebar3 release
58 | $ _build/default/rel/your_app_here/bin/your_app_here console
59 | ```
60 |
61 | The application will be available at [http://localhost:8080](http://localhost:8080).
62 |
63 | To learn more continue reading [here](https://github.com/webmachine/webmachine/wiki).
64 |
65 | ### Contributing
66 |
67 | We encourage contributions to `webmachine` from the community.
68 |
69 | 1) Fork the `webmachine` repository on [Github](https://github.com/webmachine/webmachine).
70 |
71 | 2) Clone your fork or add the remote if you already have a clone of
72 | the repository.
73 |
74 | ```
75 | git clone git@github.com:yourusername/webmachine.git
76 | ```
77 |
78 | or
79 |
80 | ```
81 | git remote add mine git@github.com:yourusername/webmachine.git
82 | ```
83 |
84 | 3) Create a topic branch for your change.
85 |
86 | ```
87 | git checkout -b some-topic-branch
88 | ```
89 |
90 | 4) Make your change and commit. Use a clear and descriptive commit
91 | message, spanning multiple lines if detailed explanation is
92 | needed.
93 |
94 | 5) Push to your fork of the repository and then send a pull-request
95 | through Github.
96 |
97 | ```
98 | git push mine some-topic-branch
99 | ```
100 |
101 | 6) A community maintainer will review your pull request and merge
102 | it into the main repository or send you feedback.
103 |
--------------------------------------------------------------------------------
/THANKS:
--------------------------------------------------------------------------------
1 | The following people have contributed to Webmachine:
2 |
3 | Andy Gross
4 | Justin Sheehy
5 | John Muellerleile
6 | Robert Ahrens
7 | Jeremy Latt
8 | Bryan Fink
9 | Ryan Tilder
10 | Taavi Talvik
11 | Marc Worrell
12 | Seth Falcon
13 | Tuncer Ayaz
14 | Martin Scholl
15 | Paul Mineiro
16 | Dave Smith
17 | Arjan Scherpenisse
18 | Benjamin Black
19 | Anthony Molinaro
20 | Phil Pirozhkov
21 | Rusty Klophaus
22 | Joe DeVivo
23 | Tristan Sloughter
24 |
--------------------------------------------------------------------------------
/docs/http-headers-status-v3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basho/webmachine/6c80fda34258eaca64d7de6af1b364a29bd1fec0/docs/http-headers-status-v3.png
--------------------------------------------------------------------------------
/include/webmachine.hrl:
--------------------------------------------------------------------------------
1 | -include("wm_reqdata.hrl").
2 |
--------------------------------------------------------------------------------
/include/webmachine_logger.hrl:
--------------------------------------------------------------------------------
1 | -record(wm_log_data,
2 | {resource_module :: atom(),
3 | start_time :: tuple(),
4 | method :: atom(),
5 | headers,
6 | peer,
7 | sock,
8 | path :: string(),
9 | version,
10 | response_code,
11 | response_length,
12 | end_time :: undefined | tuple(),
13 | finish_time :: undefined | tuple(),
14 | notes}).
15 | -type wm_log_data() :: #wm_log_data{}.
16 |
17 | -define(EVENT_LOGGER, webmachine_log_event).
18 |
--------------------------------------------------------------------------------
/include/wm_compat.hrl:
--------------------------------------------------------------------------------
1 | -ifndef(deprecate_stacktrace).
2 | -define(STPATTERN(Pattern), Pattern).
3 | -define(STACKTRACE, erlang:get_stacktrace()).
4 | -else.
5 | -define(STPATTERN(Pattern), Pattern:__STACKTRACE).
6 | -define(STACKTRACE, __STACKTRACE).
7 | -endif.
8 |
--------------------------------------------------------------------------------
/include/wm_reqdata.hrl:
--------------------------------------------------------------------------------
1 | %% Reverse Engineering of these types. Right now based on what
2 | %% mochiweb expected, and the assumptions it made from OTP
3 |
4 | %% The following types are clear in erlang:decode_packet/3
5 | %% https://github.com/erlang/otp/blob/86d1fb0865193cce4e308baa6472885a81033f10/erts/preloaded/src/erlang.erl#L537-L632
6 | %% {http_request, Method, Url, Version}
7 | %% Method :: atom() | string()
8 | %% Url :: string()
9 | %% Version :: {integer(), integer()}
10 |
11 | -record(
12 | wm_reqdata,
13 | {
14 | method :: wrq:method(),
15 | scheme :: wrq:scheme(),
16 | version :: {non_neg_integer(), % decode_packet/3
17 | non_neg_integer()},
18 | peer="defined_in_wm_req_srv_init",
19 | sock="defined_in_wm_req_srv_init",
20 | wm_state = defined_on_call,
21 | disp_path=defined_in_load_dispatch_data,
22 | path = "defined_in_create" :: string(),
23 | raw_path :: string(), % mochiweb:uri/1
24 | path_info = orddict:new() :: orddict:orddict(),
25 | path_tokens=defined_in_load_dispatch_data,
26 | app_root="defined_in_load_dispatch_data",
27 | response_code = 500 :: non_neg_integer()
28 | | {non_neg_integer(), string()},
29 | max_recv_body = (1024*(1024*1024)) :: non_neg_integer(),
30 | % Stolen from R13B03 inet_drv.c's TCP_MAX_PACKET_SIZE definition
31 | max_recv_hunk = (64*(1024*1024)) :: non_neg_integer(),
32 | req_cookie = defined_in_create :: [{string(), string()}]
33 | | defined_in_create,
34 | req_qs = defined_in_create :: [{string(), string()}]
35 | | defined_in_create,
36 | req_headers :: webmachine:headers(),
37 | req_body=not_fetched_yet,
38 | resp_redirect = false :: boolean(),
39 | resp_headers = webmachine_headers:empty() :: webmachine:headers(),
40 | resp_body = <<>> :: webmachine:response_body(),
41 | %% follow_request : range responce for range request, normal responce for non-range one
42 | %% ignore_request : normal resopnse for either range reuqest or non-range one
43 | resp_range = follow_request :: follow_request | ignore_request,
44 | host_tokens,
45 | port :: undefined | inet:port_number(),
46 | notes = [] :: list()
47 | }).
48 |
--------------------------------------------------------------------------------
/include/wm_reqstate.hrl:
--------------------------------------------------------------------------------
1 | -record(wm_reqstate, {
2 | socket=undefined,
3 | metadata=orddict:new(),
4 | range=undefined,
5 | peer=undefined,
6 | sock=undefined,
7 | reqdata=undefined,
8 | bodyfetch=undefined,
9 | reqbody=undefined,
10 | log_data=undefined
11 | }).
12 |
--------------------------------------------------------------------------------
/include/wm_resource.hrl:
--------------------------------------------------------------------------------
1 | -record(wm_resource, {module, modstate, trace}).
2 |
--------------------------------------------------------------------------------
/priv/trace/http-headers-status-v3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basho/webmachine/6c80fda34258eaca64d7de6af1b364a29bd1fec0/priv/trace/http-headers-status-v3.png
--------------------------------------------------------------------------------
/priv/trace/wmtrace.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin:0px;
3 | padding:0px;
4 | }
5 |
6 | canvas#v3map {
7 | margin-top:2em;
8 | z-index: 1;
9 | }
10 |
11 | div#sizetest {
12 | width:100%;
13 | }
14 |
15 | div#zoompanel {
16 | height:2em;
17 | position:fixed;
18 | z-index:10;
19 | }
20 |
21 | div#preview {
22 | position:absolute;
23 | display:none;
24 | background:#dddddd;
25 | border:1px solid #999999;
26 | }
27 |
28 | div#preview ul {
29 | padding: 0px 0px 0px 0.5em;
30 | margin: 0px;
31 | list-style: none;
32 | }
33 |
34 | div#infopanel {
35 | z-index:20;
36 | background:#dddddd;
37 | position:fixed;
38 | top:0px;
39 | right:0px;
40 | bottom:0px;
41 | left:75%;
42 | min-width:30em;
43 | padding:5px;
44 | }
45 |
46 | div#infocontrols {
47 | position:absolute;
48 | top:0px;
49 | bottom:0px;
50 | left:-5px;
51 | width:5px;
52 | background:#999999;
53 | cursor:ew-resize;
54 | }
55 |
56 | div#infocontrols div {
57 | position:absolute;
58 | left:-15px;
59 | width:20px;
60 | height:49px;
61 | background:#999999;
62 | cursor:pointer;
63 | }
64 |
65 | div#infocontrols div.selectedtab {
66 | background:#dddddd;
67 | border-top: 1px solid #999999;
68 | border-left: 1px solid #999999;
69 | border-bottom: 1px solid #999999;
70 | }
71 |
72 | div#requesttab {
73 | top:2px;
74 | }
75 |
76 | div#responsetab {
77 | top:54px;
78 | }
79 |
80 | div#decisiontab {
81 | top:106px;
82 | }
83 |
84 | div#requestdetail, div#responsedetail, div#decisiondetail {
85 | height:100%;
86 | }
87 |
88 | div#responsedetail, div#decisiondetail {
89 | display:none;
90 | }
91 |
92 | div#infopanel ul {
93 | list-style:none;
94 | padding-left:0px;
95 | height:5em;
96 | overflow-y:scroll;
97 | }
98 |
99 | pre {
100 | height:40%;
101 | overflow:scroll;
102 | }
103 |
104 | div#responsebody, div#requestbody {
105 | height:70%;
106 | overflow-y:scroll;
107 | }
108 |
--------------------------------------------------------------------------------
/priv/trace/wmtrace.js:
--------------------------------------------------------------------------------
1 | var HIGHLIGHT = '#cc00cc';
2 | var REGULAR = '#666666';
3 |
4 | var cols = {
5 | 'a':173,
6 | 'b':325,
7 | 'c':589,
8 | 'd':797,
9 | 'e':1005,
10 | 'f':1195,
11 | 'g':1402,
12 | 'gg':1515,
13 | 'h':1572,
14 | 'i':1799,
15 | 'j':1893,
16 | 'k':1988,
17 | 'l':2157,
18 | 'll':2346,
19 | 'm':2403,
20 | 'mm':2535,
21 | 'n':2554,
22 | 'o':2649,
23 | 'oo':2781,
24 | 'ooo':2801,
25 | 'p':2894,
26 | 'q':3007
27 | };
28 |
29 | var rows = {
30 | '1':221,
31 | '2':298,
32 | '3':373,
33 | '4':448,
34 | '5':524,
35 | '6':599,
36 | '7':675,
37 | '8':751,
38 | '9':826,
39 | '10':902,
40 | '11':977,
41 | '12':1053,
42 | '13':1129,
43 | '14':1204,
44 | '15':1280,
45 | '16':1355,
46 | '17':1431,
47 | '18':1506,
48 | '19':1583,
49 | '20':1658,
50 | '21':1734,
51 | '22':1809,
52 | '23':1885,
53 | '24':1961,
54 | '25':2036,
55 | '26':2112
56 | };
57 |
58 | var edges = {
59 | 'b14b13':['b14','b13'],
60 |
61 | 'b13b12':['b13','b12'],
62 | 'b13503':['b13','503'],
63 |
64 | 'b12b11':['b12','b11'],
65 | 'b12501':['b12','501'],
66 |
67 | 'b11b10':['b11','b10'],
68 | 'b11414':['b11','414'],
69 |
70 | 'b10b9':['b10','b9'],
71 | 'b10405':['b10','405'],
72 |
73 | 'b9b8':['b9','b8'],
74 | 'b9400':['b9','400'],
75 |
76 | 'b8b7':['b8','b7'],
77 | 'b8401':['b8','401'],
78 |
79 | 'b7b6':['b7','b6'],
80 | 'b7403':['b7','403'],
81 |
82 | 'b6b5':['b6','b5'],
83 | 'b6501':['b6','501a'],
84 |
85 | 'b5b4':['b5','b4'],
86 | 'b5415':['b5','415'],
87 |
88 | 'b4b3':['b4','b3'],
89 | 'b4413':['b4','b4'],
90 |
91 | 'b3c3':['b3','c3'],
92 | 'b3200':['b3','200'],
93 |
94 | 'c3c4':['c3','c4'],
95 | 'c3d4':['c3','d3','d4'],
96 |
97 | 'c4d4':['c4','d4'],
98 | 'c4406':['c4','406'],
99 |
100 | 'd4d5':['d4','d5'],
101 | 'd4e5':['d4','e4','e5'],
102 |
103 | 'd5e5':['d5','e5'],
104 | 'd5406':['d5','d7','406'],
105 |
106 | 'e5e6':['e5','e6'],
107 | 'e5f6':['e5','f5','f6'],
108 |
109 | 'e6f6':['e6','f6'],
110 | 'e6406':['e6','e7','406'],
111 |
112 | 'f6f7':['f6','f7'],
113 | 'f6g7':['f6','g6','g7'],
114 |
115 | 'f7g7':['f7','g7'],
116 | 'f7406':['f7','406'],
117 |
118 | 'g7g8':['g7','g8'],
119 | 'g7h7':['g7','h7'],
120 |
121 | 'g8g9':['g8','g9'],
122 | 'g8h10':['g8','h8','h10'],
123 |
124 | 'g9g11':['g9','g11'],
125 | 'g9h10':['g9','gg9','gg10','h10'],
126 |
127 | 'g11h10':['g11','gg11','gg10','h10'],
128 | 'g11412':['g11','g18','412a'],
129 |
130 | 'h7i7':['h7','i7'],
131 | 'h7412':['h7','412'],
132 |
133 | 'h10h11':['h10','h11'],
134 | 'h10i12':['h10','i10','i12'],
135 |
136 | 'h11h12':['h11','h12'],
137 | 'h11i12':['h11','i11','i12'],
138 |
139 | 'h12i12':['h12','i12'],
140 | 'h12412':['h12','412a'],
141 |
142 | 'i4p3':['i4','i3','p3'],
143 | 'i4301':['i4','301'],
144 |
145 | 'i7i4':['i7','i4'],
146 | 'i7k7':['i7','k7'],
147 |
148 | 'i12l13':['i12','l12','l13'],
149 | 'i12i13':['i12','i13'],
150 |
151 | 'i13k13':['i13','k13'],
152 | 'i13j18':['i13','i17','j17','j18'],
153 |
154 | 'j18412':['j18','412a'],
155 | 'j18304':['j18','304'],
156 |
157 | 'k5l5':['k5','l5'],
158 | 'k5301':['k5','301'],
159 |
160 | 'k7k5':['k7','k5'],
161 | 'k7l7':['k7','l7'],
162 |
163 | 'k13j18':['k13','k17','j17','j18'],
164 | 'k13l13':['k13','l13'],
165 |
166 | 'l5m5':['l5','m5'],
167 | 'l5307':['l5','307'],
168 |
169 | 'l7m7':['l7','m7'],
170 | 'l7404':['l7','l8','404'],
171 |
172 | 'l13l14':['l13','l14'],
173 | 'l13m16':['l13','m13','m16'],
174 |
175 | 'l14l15':['l14','l15'],
176 | 'l14m16':['l14','m14','m16'],
177 |
178 | 'l15l17':['l15','l17'],
179 | 'l15m16':['l15','ll15','ll16','m16'],
180 |
181 | 'l17m16':['l17','ll17','ll16','m16'],
182 | 'l17304':['l17','304'],
183 |
184 | 'm5n5':['m5','n5'],
185 | 'm5410':['m5','m4','410'],
186 |
187 | 'm7n11':['m7','n7','n11'],
188 | 'm7404':['m7','404'],
189 |
190 | 'm16m20':['m16','m20'],
191 | 'm16n16':['m16','n16'],
192 |
193 | 'm20o20':['m20','o20'],
194 | 'm20202':['m20','202'],
195 |
196 | 'n5n11':['n5','n11'],
197 | 'n5410':['n5','410'],
198 |
199 | 'n11p11':['n11','p11'],
200 | 'n11303':['n11','303'],
201 |
202 | 'n16n11':['n16','n11'],
203 | 'n16o16':['n16','o16'],
204 |
205 | 'o14p11':['o14','o11','p11'],
206 | 'o14409':['o14','409a'],
207 |
208 | 'o16o14':['o16','o14'],
209 | 'o16o18':['o16','o18'],
210 |
211 | 'o18200':['o18','200a'],
212 | 'o18300':['o18','oo18','300'],
213 |
214 | 'o20o18':['o20','o18'],
215 | 'o20204':['o20','204'],
216 |
217 | 'p3p11':['p3','p11'],
218 | 'p3409':['p3','409'],
219 |
220 | 'p11o20':['p11','p20','o20'],
221 | 'p11201':['p11','q11','201']
222 | };
223 |
224 | var ends = {
225 | '200': {col:'a', row:'3', width:190},
226 | '200a': {col:'mm', row:'18', width:116},
227 | '201': {col:'q', row:'12', width:154},
228 | '202': {col:'m', row:'21', width:116},
229 | '204': {col:'o', row:'21', width:152},
230 |
231 | '300': {col:'oo', row:'19', width:152},
232 | '301': {col:'k', row:'4', width:154},
233 | '303': {col:'m', row:'11', width:116},
234 | '304': {col:'l', row:'18', width:116},
235 | '307': {col:'l', row:'4', width:154},
236 |
237 | '400': {col:'a', row:'9', width:190},
238 | '401': {col:'a', row:'8', width:190},
239 | '403': {col:'a', row:'7', width:190},
240 | '404': {col:'m', row:'8', width:116},
241 | '405': {col:'a', row:'10', width:190},
242 | '406': {col:'c', row:'7', width:152},
243 | '409': {col:'p', row:'2', width:116},
244 | '409a': {col:'oo', row:'14', width:116},
245 | '410': {col:'n', row:'4', width:116},
246 | '412': {col:'h', row:'6', width:152},
247 | '412a': {col:'h', row:'18', width:152},
248 | '413': {col:'a', row:'4', width:190},
249 | '414': {col:'a', row:'11', width:190},
250 | '415': {col:'a', row:'5', width:190},
251 |
252 | '501a': {col:'a', row:'6', width:190},
253 | '501': {col:'a', row:'12', width:190},
254 | '503': {col:'a', row:'13', width:190}
255 | };
256 |
257 | var canvas;
258 |
259 | function decorateTrace() {
260 | trace[0].x = cols[trace[0].d[0]];
261 | trace[0].y = rows[trace[0].d.slice(1)];
262 | trace[0].previewCalls = previewCalls(trace[0]);
263 |
264 | for (var i = 1; i < trace.length; i++) {
265 | trace[i].x = cols[trace[i].d[0]];
266 | trace[i].y = rows[trace[i].d.slice(1)];
267 | trace[i].previewCalls = previewCalls(trace[i]);
268 |
269 | var path = edges[trace[i-1].d+trace[i].d];
270 | if (path) {
271 | trace[i].path = [path.length-1];
272 | for (var p = 1; p < path.length; p++) {
273 | trace[i].path[p-1] = getSeg(path[p-1], path[p], p == path.length-1);
274 | }
275 | } else {
276 | trace[i].path = [];
277 | }
278 | }
279 |
280 | var path = edges[trace[i-1].d+response.code];
281 | if (path) {
282 | var end = ends[path[path.length-1]];
283 | response.x = cols[end.col];
284 | response.y = rows[end.row];
285 | response.width = end.width;
286 | response.type = 'normal';
287 |
288 | response.path = [path.length-1];
289 | for (var p = 1; p < path.length; p++) {
290 | response.path[p-1] = getSeg(path[p-1], path[p], p == path.length-1);
291 | }
292 | } else {
293 | var ld = trace[trace.length-1];
294 | response.x = ld.x+50;
295 | response.y = ld.y-50;
296 | response.width = 38;
297 | response.type = 'other';
298 |
299 | response.path = [
300 | {x1: ld.x+10, y1: ld.y-10,
301 | x2: ld.x+36, y2: ld.y-36}
302 | ];
303 | }
304 | };
305 |
306 | function previewCalls(dec) {
307 | var prev = '';
308 | for (var i = 0; i < dec.calls.length; i++) {
309 | if (dec.calls[i].output != "wmtrace_not_exported")
310 | prev += '
'+dec.calls[i].module+':'+dec.calls[i]['function']+' ';
311 | }
312 | return prev;
313 | };
314 |
315 | function drawTrace() {
316 | drawDecision(trace[0]);
317 | for (var i = 1; i < trace.length; i++) {
318 | drawPath(trace[i].path);
319 | drawDecision(trace[i]);
320 | }
321 |
322 | drawPath(response.path);
323 | drawResponse();
324 | };
325 |
326 | function drawResponse() {
327 | if (response.type == 'normal') {
328 | var context = canvas.getContext('2d');
329 | context.strokeStyle=HIGHLIGHT;
330 | context.lineWidth=4;
331 |
332 | context.beginPath();
333 | context.rect(response.x-(response.width/2),
334 | response.y-19,
335 | response.width,
336 | 38);
337 | context.stroke();
338 | } else {
339 | var context = canvas.getContext('2d');
340 | context.strokeStyle='#ff0000';
341 | context.lineWidth=4;
342 |
343 | context.beginPath();
344 | context.arc(response.x, response.y, 19,
345 | 0, 2*3.14159, false);
346 | context.stroke();
347 |
348 | }
349 | };
350 |
351 | function drawDecision(dec) {
352 | var context = canvas.getContext('2d');
353 |
354 | if (dec.previewCalls == '')
355 | context.strokeStyle=REGULAR;
356 | else
357 | context.strokeStyle=HIGHLIGHT;
358 | context.lineWidth=4;
359 |
360 | context.beginPath();
361 | context.moveTo(dec.x, dec.y-19);
362 | context.lineTo(dec.x+19, dec.y);
363 | context.lineTo(dec.x, dec.y+19);
364 | context.lineTo(dec.x-19, dec.y);
365 | context.closePath();
366 | context.stroke();
367 | };
368 |
369 | function drawPath(path) {
370 | var context = canvas.getContext('2d');
371 | context.strokeStyle=REGULAR;
372 | context.lineWidth=4;
373 |
374 | context.beginPath();
375 | context.moveTo(path[0].x1, path[0].y1);
376 | for (var p = 0; p < path.length; p++) {
377 | context.lineTo(path[p].x2, path[p].y2);
378 | }
379 | context.stroke();
380 | };
381 |
382 | function getSeg(p1, p2, last) {
383 | var seg = {
384 | x1:cols[p1[0]],
385 | y1:rows[p1.slice(1)]
386 | };
387 | if (ends[p2]) {
388 | seg.x2 = cols[ends[p2].col];
389 | seg.y2 = rows[ends[p2].row];
390 | } else {
391 | seg.x2 = cols[p2[0]];
392 | seg.y2 = rows[p2.slice(1)];
393 | }
394 |
395 | if (seg.x1 == seg.x2) {
396 | if (seg.y1 < seg.y2) {
397 | seg.y1 = seg.y1+19;
398 | if (last) seg.y2 = seg.y2-19;
399 | } else {
400 | seg.y1 = seg.y1-19;
401 | if (last) seg.y2 = seg.y2+19;
402 | }
403 | } else {
404 | //assume seg.y1 == seg.y2
405 | if (seg.x1 < seg.x2) {
406 | seg.x1 = seg.x1+19;
407 | if (last) seg.x2 = seg.x2-(ends[p2] ? (ends[p2].width/2) : 19);
408 | } else {
409 | seg.x1 = seg.x1-19;
410 | if (last) seg.x2 = seg.x2+(ends[p2] ? (ends[p2].width/2) : 19);
411 | }
412 | }
413 | return seg;
414 | };
415 |
416 | function traceDecision(name) {
417 | for (var i = trace.length-1; i >= 0; i--)
418 | if (trace[i].d == name) return trace[i];
419 | };
420 |
421 | var detailPanels = {};
422 | function initDetailPanels() {
423 | var windowWidth = document.getElementById('sizetest').clientWidth;
424 | var infoPanel = document.getElementById('infopanel');
425 | var panelWidth = windowWidth-infoPanel.offsetLeft;
426 |
427 | var panels = {
428 | 'request': document.getElementById('requestdetail'),
429 | 'response': document.getElementById('responsedetail'),
430 | 'decision': document.getElementById('decisiondetail')
431 | };
432 |
433 | var tabs = {
434 | 'request': document.getElementById('requesttab'),
435 | 'response': document.getElementById('responsetab'),
436 | 'decision': document.getElementById('decisiontab')
437 | };
438 |
439 | var decisionId = document.getElementById('decisionid');
440 | var decisionCalls = document.getElementById('decisioncalls');
441 | var callInput = document.getElementById('callinput');
442 | var callOutput = document.getElementById('calloutput');
443 |
444 | var lastUsedPanelWidth = windowWidth-infoPanel.offsetLeft;
445 |
446 | var setPanelWidth = function(width) {
447 | infoPanel.style.left = (windowWidth-width)+'px';
448 | canvas.style.marginRight = (width+20)+'px';
449 | panelWidth = width;
450 | };
451 | setPanelWidth(panelWidth);
452 |
453 | var ensureVisible = function() {
454 | if (windowWidth-infoPanel.offsetLeft < 10)
455 | setPanelWidth(lastUsedPanelWidth);
456 | };
457 |
458 | var decChoices = '';
459 | for (var i = 0; i < trace.length; i++) {
460 | decChoices += ''+trace[i].d+' ';
461 | }
462 | decisionId.innerHTML = decChoices;
463 | decisionId.selectedIndex = -1;
464 |
465 | decisionId.onchange = function() {
466 | detailPanels.setDecision(traceDecision(decisionId.value));
467 | }
468 |
469 | detailPanels.setDecision = function(dec) {
470 | decisionId.value = dec.d;
471 |
472 | var calls = [];
473 | for (var i = 0; i < dec.calls.length; i++) {
474 | calls.push('');
475 | calls.push(dec.calls[i].module+':'+dec.calls[i]['function']);
476 | calls.push(' ');
477 | }
478 | decisionCalls.innerHTML = calls.join('');
479 | decisionCalls.selectedIndex = 0;
480 |
481 | decisionCalls.onchange();
482 | };
483 |
484 | detailPanels.show = function(name) {
485 | for (p in panels) {
486 | if (p == name) {
487 | panels[p].style.display = 'block';
488 | tabs[p].className = 'selectedtab';
489 | }
490 | else {
491 | panels[p].style.display = 'none';
492 | tabs[p].className = '';
493 | }
494 | }
495 | ensureVisible();
496 | };
497 |
498 | detailPanels.hide = function() {
499 | setPanelWidth(0);
500 | }
501 |
502 | decisionCalls.onchange = function() {
503 | var val = decisionCalls.value;
504 | if (val) {
505 | var dec = traceDecision(val.substring(0, val.indexOf('-')));
506 | var call = dec.calls[parseInt(val.substring(val.indexOf('-')+1, val.length))];
507 |
508 | if (call.output != "wmtrace_not_exported") {
509 | callInput.style.color='#000000';
510 | callInput.innerHTML = call.input;
511 | if (call.output != null) {
512 | callOutput.style.color = '#000000';
513 | callOutput.innerHTML = call.output;
514 | } else {
515 | callOutput.style.color = '#ff0000';
516 | callOutput.textContent = 'Error: '+call.module+':'+call['function']+' never returned';
517 | }
518 | } else {
519 | callInput.style.color='#999999';
520 | callInput.textContent = call.module+':'+call['function']+' was not exported';
521 | callOutput.textContent = '';
522 | }
523 | } else {
524 | callInput.textContent = '';
525 | callOutput.textContent = '';
526 | }
527 | };
528 |
529 | var headersList = function(headers) {
530 | var h = '';
531 | for (n in headers) h += ''+n+': '+headers[n];
532 | return h;
533 | };
534 |
535 | document.getElementById('requestmethod').innerHTML = request.method;
536 | document.getElementById('requestpath').innerHTML = request.path;
537 | document.getElementById('requestheaders').innerHTML = headersList(request.headers);
538 | document.getElementById('requestbody').innerHTML = request.body;
539 |
540 | document.getElementById('responsecode').innerHTML = response.code;
541 | document.getElementById('responseheaders').innerHTML = headersList(response.headers);
542 | document.getElementById('responsebody').innerHTML = response.body;
543 |
544 |
545 | var infoControls = document.getElementById('infocontrols');
546 | var md = false;
547 | var dragged = false;
548 | var msoff = 0;
549 | infoControls.onmousedown = function(ev) {
550 | md = true;
551 | dragged = false;
552 | msoff = ev.clientX-infoPanel.offsetLeft;
553 | };
554 |
555 | infoControls.onclick = function(ev) {
556 | if (dragged) {
557 | lastUsedPanelWidth = panelWidth;
558 | }
559 | else if (panelWidth < 10) {
560 | switch(ev.target.id) {
561 | case 'requesttab': detailPanels.show('request'); break;
562 | case 'responsetab': detailPanels.show('response'); break;
563 | case 'decisiontab': detailPanels.show('decision'); break;
564 | default: ensureVisible();
565 | }
566 | } else {
567 | var name = 'none';
568 | switch(ev.target.id) {
569 | case 'requesttab': name = 'request'; break;
570 | case 'responsetab': name = 'response'; break;
571 | case 'decisiontab': name = 'decision'; break;
572 | }
573 |
574 | if (panels[name] && panels[name].style.display != 'block')
575 | detailPanels.show(name);
576 | else
577 | detailPanels.hide();
578 | }
579 |
580 | return false;
581 | };
582 |
583 | document.onmousemove = function(ev) {
584 | if (md) {
585 | dragged = true;
586 | panelWidth = windowWidth-(ev.clientX-msoff);
587 | if (panelWidth < 0) {
588 | panelWidth = 0;
589 | infoPanel.style.left = windowWidth+"px";
590 | }
591 | else if (panelWidth > windowWidth-21) {
592 | panelWidth = windowWidth-21;
593 | infoPanel.style.left = '21px';
594 | }
595 | else
596 | infoPanel.style.left = (ev.clientX-msoff)+"px";
597 |
598 | canvas.style.marginRight = panelWidth+20+"px";
599 | return false;
600 | }
601 | };
602 |
603 | document.onmouseup = function() { md = false; };
604 |
605 | window.onresize = function() {
606 | windowWidth = document.getElementById('sizetest').clientWidth;
607 | infoPanel.style.left = windowWidth-panelWidth+'px';
608 | };
609 | };
610 |
611 | window.onload = function() {
612 | canvas = document.getElementById('v3map');
613 |
614 | initDetailPanels();
615 |
616 | var scale = 0.25;
617 | var coy = canvas.offsetTop;
618 | function findDecision(ev) {
619 | var x = (ev.clientX+window.pageXOffset)/scale;
620 | var y = (ev.clientY+window.pageYOffset-coy)/scale;
621 |
622 | for (var i = trace.length-1; i >= 0; i--) {
623 | if (x >= trace[i].x-19 && x <= trace[i].x+19 &&
624 | y >= trace[i].y-19 && y <= trace[i].y+19)
625 | return trace[i];
626 | }
627 | };
628 |
629 | var preview = document.getElementById('preview');
630 | var previewId = document.getElementById('previewid');
631 | var previewCalls = document.getElementById('previewcalls');
632 | function previewDecision(dec) {
633 | preview.style.left = (dec.x*scale)+'px';
634 | preview.style.top = (dec.y*scale+coy+15)+'px';
635 | preview.style.display = 'block';
636 | previewId.textContent = dec.d;
637 |
638 | previewCalls.innerHTML = dec.previewCalls;
639 | };
640 |
641 | function overResponse(ev) {
642 | var x = (ev.clientX+window.pageXOffset)/scale;
643 | var y = (ev.clientY+window.pageYOffset-coy)/scale;
644 |
645 | return (x >= response.x-(response.width/2)
646 | && x <= response.x+(response.width/2)
647 | && y >= response.y-19 && y <= response.y+19);
648 | };
649 |
650 | decorateTrace();
651 |
652 | var bg = new Image(3138, 2184);
653 |
654 | function drawMap() {
655 | var ctx = canvas.getContext("2d");
656 |
657 | ctx.save();
658 | ctx.scale(1/scale, 1/scale);
659 | ctx.fillStyle = '#ffffff';
660 | ctx.fillRect(0, 0, 3138, 2184);
661 | ctx.restore();
662 |
663 | ctx.drawImage(bg, 0, 0);
664 | drawTrace();
665 | };
666 |
667 | bg.onload = function() {
668 | canvas.getContext("2d").scale(scale, scale);
669 | drawMap(scale);
670 |
671 | canvas.onmousemove = function(ev) {
672 | if (findDecision(ev)) {
673 | canvas.style.cursor = 'pointer';
674 | previewDecision(findDecision(ev));
675 | }
676 | else {
677 | preview.style.display = 'none';
678 | if (overResponse(ev))
679 | canvas.style.cursor = 'pointer';
680 | else
681 | canvas.style.cursor = 'default';
682 | }
683 | };
684 |
685 | canvas.onclick = function(ev) {
686 | var dec = findDecision(ev);
687 | if (dec) {
688 | detailPanels.setDecision(dec);
689 | detailPanels.show('decision');
690 | } else if (overResponse(ev)) {
691 | detailPanels.show('response');
692 | }
693 | };
694 |
695 | document.getElementById('zoomin').onclick = function() {
696 | scale = scale*2;
697 | canvas.getContext("2d").scale(2, 2);
698 | drawMap();
699 | };
700 |
701 | document.getElementById('zoomout').onclick = function() {
702 | scale = scale/2;
703 | canvas.getContext("2d").scale(0.5, 0.5);
704 | drawMap();
705 | };
706 | };
707 |
708 | bg.onerror = function() {
709 | alert('Failed to load background image.');
710 | };
711 |
712 | bg.src = 'static/map.png';
713 | };
714 |
--------------------------------------------------------------------------------
/priv/www/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | It Worked
4 |
5 |
6 | Running.
7 |
8 |
9 |
--------------------------------------------------------------------------------
/rebar.config:
--------------------------------------------------------------------------------
1 | %%-*- mode: erlang -*-
2 | {minimum_otp_vsn, "22.0"}.
3 | {erl_opts, [warnings_as_errors]}.
4 | {cover_enabled, true}.
5 | {edoc_opts, [{preprocess, true}]}.
6 |
7 | {xref_checks, [undefined_function_calls]}.
8 |
9 | {deps, [{mochiweb, {git, "https://github.com/basho/mochiweb.git", {branch, "develop"}}}]}.
10 |
11 | {eunit_opts, [
12 | no_tty,
13 | {report, {eunit_progress, [colored, profile]}}
14 | ]}.
15 |
16 | {profiles,
17 | [{gha, [{erl_opts, [{d, 'GITHUBEXCLUDE'}]}]},
18 | {test,
19 | [{deps, [meck,
20 | {ibrowse, "4.4.0"}
21 | ]},
22 | {erl_opts, [debug_info]}
23 | ]}
24 | ]}.
25 |
--------------------------------------------------------------------------------
/rebar.config.script:
--------------------------------------------------------------------------------
1 | %% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*-
2 | %% ex: ft=erlang ts=4 sw=4 et
3 | OtpVersion = erlang:system_info(otp_release),
4 | Config1 = case hd(OtpVersion) =:= $R andalso OtpVersion < "R15B02" of
5 | true ->
6 | HashDefine = [{d,old_hash}],
7 | case lists:keysearch(erl_opts, 1, CONFIG) of
8 | {value, {erl_opts, Opts}} ->
9 | lists:keyreplace(erl_opts,1,CONFIG,{erl_opts,Opts++HashDefine});
10 | false ->
11 | CONFIG ++ [{erl_opts, HashDefine}]
12 | end;
13 | false -> CONFIG
14 | end,
15 | if
16 | OtpVersion >= "21" ->
17 | case lists:keysearch(erl_opts, 1, Config1) of
18 | {value, {erl_opts, Opts2}} ->
19 | lists:keyreplace(erl_opts, 1, Config1, {erl_opts, [{d, deprecate_stacktrace}|Opts2]});
20 | false ->
21 | [{erl_opts, [{d, deprecate_stacktrace}]}|Config1]
22 | end;
23 | true ->
24 | Config1
25 | end.
26 |
--------------------------------------------------------------------------------
/rebar3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/basho/webmachine/6c80fda34258eaca64d7de6af1b364a29bd1fec0/rebar3
--------------------------------------------------------------------------------
/src/webmachine.app.src:
--------------------------------------------------------------------------------
1 | %%-*- mode: erlang -*-
2 | {application, webmachine,
3 | [
4 | {description, "webmachine"},
5 | {vsn, git},
6 | {modules, []},
7 | {registered, []},
8 | {applications, [kernel,
9 | stdlib,
10 | crypto,
11 | mochiweb]},
12 | {mod, {webmachine_app, []}},
13 | {env,
14 | %% env is THE place for expected defaults. Even if it's just
15 | %% comments, it's worthwhile
16 | [
17 | {log_handlers, []}
18 | ,{error_handler, webmachine_error_handler}
19 | %% error_handler is a module that implements the function render_error/3
20 | ,{rewrite_module, undefined}
21 | %% module that has rewrite/5
22 | ,{server_name, undefined}
23 | %% string() for the "Server" response header
24 | ]},
25 |
26 | {maintainers,["Sean Cribbs", "Joe DeVivo", "Bryan Fink",
27 | "Kelly McLaughlin", "Jared Morrow", "Andy Gross",
28 | "Steve Vinoski"]},
29 | {licenses,["Apache"]},
30 | {links,[{"Github","https://github.com/webmachine/webmachine"}]}
31 | ]}.
32 |
--------------------------------------------------------------------------------
/src/webmachine.erl:
--------------------------------------------------------------------------------
1 | %% @author Justin Sheehy
2 | %% @author Andy Gross
3 | %% @copyright 2007-2014 Basho Technologies
4 | %%
5 | %% Licensed under the Apache License, Version 2.0 (the "License");
6 | %% you may not use this file except in compliance with the License.
7 | %% You may obtain a copy of the License at
8 | %%
9 | %% http://www.apache.org/licenses/LICENSE-2.0
10 | %%
11 | %% Unless required by applicable law or agreed to in writing, software
12 | %% distributed under the License is distributed on an "AS IS" BASIS,
13 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | %% See the License for the specific language governing permissions and
15 | %% limitations under the License.
16 |
17 | -module(webmachine).
18 | -author('Justin Sheehy ').
19 | -author('Andy Gross ').
20 | -export([start/0, stop/0, new_request/2]).
21 |
22 | -type headers() :: webmachine_headers:t().
23 | -type response_body() :: iodata()
24 | | {stream, StreamBody::any()}
25 | | {known_length_stream, non_neg_integer(), StreamBody::any()}
26 | | {stream, non_neg_integer(), fun()} %% TODO: type for fun()
27 | | {writer, WrtieBody::any()}
28 | | {file, IoDevice::any()}.
29 |
30 |
31 | -export_type([headers/0, response_body/0]).
32 |
33 | %% @spec start() -> ok
34 | %% @doc Start the webmachine server.
35 | start() ->
36 | webmachine_deps:ensure(),
37 | ok = ensure_started(crypto),
38 | ok = ensure_started(webmachine).
39 |
40 | ensure_started(App) ->
41 | case application:start(App) of
42 | ok ->
43 | ok;
44 | {error, {already_started, App}} ->
45 | ok;
46 | {error, _} = E ->
47 | E
48 | end.
49 |
50 | %% @spec stop() -> ok
51 | %% @doc Stop the webmachine server.
52 | stop() ->
53 | application:stop(webmachine).
54 |
55 | new_request(mochiweb, MochiReq) ->
56 | webmachine_mochiweb:new_webmachine_req(MochiReq).
57 |
58 | %%
59 | %% TEST
60 | %%
61 | -ifdef(TEST).
62 |
63 | -include_lib("eunit/include/eunit.hrl").
64 |
65 | start_mochiweb() ->
66 | webmachine_util:ensure_all_started(mochiweb).
67 |
68 | start_stop_test() ->
69 | {Res, Apps} = start_mochiweb(),
70 | ?assertEqual(ok, Res),
71 | ?assertEqual(ok, webmachine:start()),
72 | ?assertEqual(ok, webmachine:stop()),
73 | [application:stop(App) || App <- Apps],
74 | ok.
75 |
76 | -endif.
77 |
--------------------------------------------------------------------------------
/src/webmachine_access_log_handler.erl:
--------------------------------------------------------------------------------
1 | %% Copyright (c) 2011-2014 Basho Technologies, Inc. All Rights Reserved.
2 | %%
3 | %% This file is provided to you under the Apache License,
4 | %% Version 2.0 (the "License"); you may not use this file
5 | %% except in compliance with the License. You may obtain
6 | %% a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing,
11 | %% software distributed under the License is distributed on an
12 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
13 | %% KIND, either express or implied. See the License for the
14 | %% specific language governing permissions and limitations
15 | %% under the License.
16 |
17 | %% @doc Default access log handler for webmachine
18 |
19 | -module(webmachine_access_log_handler).
20 |
21 | -behaviour(gen_event).
22 |
23 | %% gen_event callbacks
24 | -export([init/1,
25 | handle_call/2,
26 | handle_event/2,
27 | handle_info/2,
28 | terminate/2,
29 | code_change/3]).
30 |
31 | -include("webmachine_logger.hrl").
32 |
33 | -ifdef(TEST).
34 | -include_lib("eunit/include/eunit.hrl").
35 | -endif.
36 |
37 | -record(state, {hourstamp,
38 | filename,
39 | handle,
40 | request_timing=false :: boolean()}).
41 |
42 | -define(FILENAME, "access.log").
43 |
44 | %% ===================================================================
45 | %% gen_event callbacks
46 | %% ===================================================================
47 |
48 | %% @private
49 | init([BaseDir]) ->
50 | {ok,_} = webmachine_log:defer_refresh(?MODULE),
51 | FileName = filename:join(BaseDir, ?FILENAME),
52 | {Handle, DateHour} = webmachine_log:log_open(FileName),
53 | {ok, #state{filename=FileName, handle=Handle, hourstamp=DateHour}};
54 | init([BaseDir, true]) ->
55 | {ok,_} = webmachine_log:defer_refresh(?MODULE),
56 | FileName = filename:join(BaseDir, ?FILENAME),
57 | {Handle, DateHour} = webmachine_log:log_open(FileName),
58 | {ok, #state{filename=FileName,
59 | handle=Handle,
60 | hourstamp=DateHour,
61 | request_timing=true}};
62 | init([BaseDir, _]) ->
63 | init([BaseDir]).
64 |
65 | %% @private
66 | handle_call({_Label, MRef, get_modules}, State) ->
67 | {ok, {MRef, [?MODULE]}, State};
68 | handle_call({refresh, Time}, State) ->
69 | {NewHour, NewHandle} = webmachine_log:maybe_rotate(?MODULE,
70 | State#state.filename,
71 | State#state.handle,
72 | Time,
73 | State#state.hourstamp),
74 | {ok, ok, State#state{hourstamp=NewHour, handle=NewHandle}};
75 | handle_call(_Request, State) ->
76 | {ok, ok, State}.
77 |
78 | %% @private
79 | handle_event({log_access, LogData}, State) ->
80 | {NewHour, NewHandle} = webmachine_log:maybe_rotate(?MODULE,
81 | State#state.filename,
82 | State#state.handle,
83 | os:timestamp(),
84 | State#state.hourstamp),
85 | NewState = State#state{hourstamp=NewHour, handle=NewHandle},
86 | Msg = format_req(LogData, NewState#state.request_timing),
87 | _ = webmachine_log:log_write(NewState#state.handle, Msg),
88 | {ok, NewState};
89 | handle_event(_Event, State) ->
90 | {ok, State}.
91 |
92 | %% @private
93 | handle_info(_Info, State) ->
94 | {ok, State}.
95 |
96 | %% @private
97 | terminate(_Reason, _State) ->
98 | ok.
99 |
100 | %% @private
101 | code_change(_OldVsn, State, _Extra) ->
102 | {ok, State}.
103 |
104 | %% ===================================================================
105 | %% Internal functions
106 | %% ===================================================================
107 |
108 | format_req(#wm_log_data{method=Method,
109 | headers=Headers,
110 | peer=Peer,
111 | path=Path,
112 | version=Version,
113 | response_code=ResponseCode,
114 | response_length=ResponseLength,
115 | start_time=StartTime,
116 | end_time=EndTime},
117 | RequestTiming) ->
118 | User = "-",
119 | Time = webmachine_log:fmtnow(),
120 | Status = case ResponseCode of
121 | {Code, _ReasonPhrase} when is_integer(Code) ->
122 | integer_to_list(Code);
123 | _ when is_integer(ResponseCode) ->
124 | integer_to_list(ResponseCode);
125 | _ ->
126 | ResponseCode
127 | end,
128 | Length = integer_to_list(ResponseLength),
129 | Referer =
130 | case mochiweb_headers:get_value("Referer", Headers) of
131 | undefined -> "";
132 | R -> R
133 | end,
134 | UserAgent =
135 | case mochiweb_headers:get_value("User-Agent", Headers) of
136 | undefined -> "";
137 | U -> U
138 | end,
139 | case RequestTiming of
140 | true ->
141 | fmt_alog(Time, Peer, User, Method, Path, Version,
142 | Status, Length, Referer, UserAgent, timer:now_diff(EndTime, StartTime));
143 | false ->
144 | fmt_alog(Time, Peer, User, Method, Path, Version,
145 | Status, Length, Referer, UserAgent)
146 | end.
147 |
148 | fmt_alog(Time, Ip, User, Method, Path, Version,
149 | Status, Length, Referer, UserAgent) when is_atom(Method) ->
150 | fmt_alog(Time, Ip, User, atom_to_list(Method), Path, Version,
151 | Status, Length, Referer, UserAgent);
152 | fmt_alog(Time, Ip, User, Method, Path, {VM,Vm},
153 | Status, Length, Referer, UserAgent) ->
154 | [webmachine_log:fmt_ip(Ip), " - ", User, [$\s], Time, [$\s, $"], Method, " ", Path,
155 | " HTTP/", integer_to_list(VM), ".", integer_to_list(Vm), [$",$\s],
156 | Status, [$\s], Length, [$\s,$"], Referer,
157 | [$",$\s,$"], UserAgent, [$",$\n]].
158 |
159 | fmt_alog(Time, Ip, User, Method, Path, Version,
160 | Status, Length, Referer, UserAgent, ReqDuration) when is_atom(Method) ->
161 | fmt_alog(Time, Ip, User, atom_to_list(Method), Path, Version,
162 | Status, Length, Referer, UserAgent, ReqDuration);
163 | fmt_alog(Time, Ip, User, Method, Path, {VM,Vm},
164 | Status, Length, Referer, UserAgent, ReqDuration) ->
165 | [webmachine_log:fmt_ip(Ip), " - ", User, [$\s], Time, [$\s, $"], Method, " ", Path,
166 | " HTTP/", integer_to_list(VM), ".", integer_to_list(Vm), [$",$\s],
167 | Status, [$\s], Length, [$\s,$"], Referer,
168 | [$",$\s,$"], UserAgent, [$",$\s], integer_to_list(ReqDuration), [$\n]].
169 |
170 |
171 | -ifdef(TEST).
172 |
173 | non_standard_method_test() ->
174 | LogData = #wm_log_data{method="FOO",
175 | headers=mochiweb_headers:make([]),
176 | peer={127,0,0,1},
177 | path="/",
178 | version={1,1},
179 | response_code=501,
180 | response_length=1234},
181 | LogEntry = format_req(LogData, false),
182 | ?assert(is_list(LogEntry)),
183 | ok.
184 |
185 | -endif.
186 |
--------------------------------------------------------------------------------
/src/webmachine_app.erl:
--------------------------------------------------------------------------------
1 | %% @author Justin Sheehy
2 | %% @author Andy Gross
3 | %% @copyright 2007-2008 Basho Technologies
4 | %%
5 | %% Licensed under the Apache License, Version 2.0 (the "License");
6 | %% you may not use this file except in compliance with the License.
7 | %% You may obtain a copy of the License at
8 | %%
9 | %% http://www.apache.org/licenses/LICENSE-2.0
10 | %%
11 | %% Unless required by applicable law or agreed to in writing, software
12 | %% distributed under the License is distributed on an "AS IS" BASIS,
13 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | %% See the License for the specific language governing permissions and
15 | %% limitations under the License.
16 |
17 | %% @doc Callbacks for the webmachine application.
18 |
19 | -module(webmachine_app).
20 | -author('Justin Sheehy ').
21 | -author('Andy Gross ').
22 |
23 | -behaviour(application).
24 |
25 | -export([start/2,
26 | stop/1]).
27 |
28 | -include("webmachine_logger.hrl").
29 |
30 | -define(QUIP, "greased slide to failure").
31 |
32 | %% @spec start(_Type, _StartArgs) -> ServerRet
33 | %% @doc application start callback for webmachine.
34 | start(_Type, _StartArgs) ->
35 | webmachine_deps:ensure(),
36 |
37 | %% Populate dynamic defaults on load:
38 | load_default_app_config(),
39 |
40 | {ok, _Pid} = SupLinkRes = webmachine_sup:start_link(),
41 | Handlers = application:get_env(webmachine, log_handlers, []),
42 |
43 | %% handlers failing to start are handled in the handler_watcher
44 | _ = [supervisor:start_child(webmachine_logger_watcher_sup,
45 | [?EVENT_LOGGER, Module, Config]) ||
46 | {Module, Config} <- Handlers],
47 | SupLinkRes.
48 |
49 | %% @spec stop(_State) -> ServerRet
50 | %% @doc application stop callback for webmachine.
51 | stop(_State) ->
52 | ok.
53 |
54 | -spec load_default_app_config() -> ok.
55 | load_default_app_config() ->
56 |
57 | case application:get_env(webmachine, server_name, undefined) of
58 | Name when is_list(Name) ->
59 | ok;
60 | _ ->
61 | set_default_server_name()
62 | end,
63 | ok.
64 |
65 |
66 | set_default_server_name() ->
67 | {mochiweb, _, MochiVersion} =
68 | lists:keyfind(mochiweb, 1, application:loaded_applications()),
69 |
70 | {webmachine, _, WMVersion} =
71 | lists:keyfind(webmachine, 1, application:loaded_applications()),
72 | ServerName =
73 | lists:flatten(
74 | io_lib:format(
75 | "MochiWeb/~s WebMachine/~s (~s)",
76 | [MochiVersion, WMVersion, ?QUIP])),
77 | application:set_env(
78 | webmachine, server_name, ServerName),
79 | ok.
80 |
--------------------------------------------------------------------------------
/src/webmachine_deps.erl:
--------------------------------------------------------------------------------
1 | %% @author Justin Sheehy
2 | %% @author Andy Gross
3 | %% @copyright 2007-2008 Basho Technologies
4 | %%
5 | %% Licensed under the Apache License, Version 2.0 (the "License");
6 | %% you may not use this file except in compliance with the License.
7 | %% You may obtain a copy of the License at
8 | %%
9 | %% http://www.apache.org/licenses/LICENSE-2.0
10 | %%
11 | %% Unless required by applicable law or agreed to in writing, software
12 | %% distributed under the License is distributed on an "AS IS" BASIS,
13 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | %% See the License for the specific language governing permissions and
15 | %% limitations under the License.
16 |
17 | %% @doc Ensure that the relatively-installed dependencies are on the code
18 | %% loading path, and locate resources relative
19 | %% to this application's path.
20 |
21 | -module(webmachine_deps).
22 | -author('Justin Sheehy ').
23 | -author('Andy Gross ').
24 |
25 | -export([ensure/0, ensure/1]).
26 | -export([get_base_dir/0, get_base_dir/1]).
27 | -export([local_path/1, local_path/2]).
28 | -export([deps_on_path/0, new_siblings/1]).
29 |
30 | %% @spec deps_on_path() -> [ProjNameAndVers]
31 | %% @doc List of project dependencies on the path.
32 | deps_on_path() ->
33 | ordsets:from_list([filename:basename(filename:dirname(X)) || X <- code:get_path()]).
34 |
35 | %% @spec new_siblings(Module) -> [Dir]
36 | %% @doc Find new siblings paths relative to Module that aren't already on the
37 | %% code path.
38 | new_siblings(Module) ->
39 | Existing = deps_on_path(),
40 | SiblingEbin = [ X || X <- filelib:wildcard(local_path(["deps", "*", "ebin"], Module)),
41 | filename:basename(filename:dirname(X)) /= %% don't include self
42 | filename:basename(filename:dirname(
43 | filename:dirname(
44 | filename:dirname(X)))) ],
45 | Siblings = [filename:dirname(X) || X <- SiblingEbin,
46 | ordsets:is_element(
47 | filename:basename(filename:dirname(X)),
48 | Existing) =:= false],
49 | lists:filter(fun filelib:is_dir/1,
50 | lists:append([[filename:join([X, "ebin"]),
51 | filename:join([X, "include"])] ||
52 | X <- Siblings])).
53 |
54 |
55 | %% @spec ensure(Module) -> ok
56 | %% @doc Ensure that all ebin and include paths for dependencies
57 | %% of the application for Module are on the code path.
58 | ensure(Module) ->
59 | code:add_paths(new_siblings(Module)),
60 | ok.
61 |
62 | %% @spec ensure() -> ok
63 | %% @doc Ensure that the ebin and include paths for dependencies of
64 | %% this application are on the code path. Equivalent to
65 | %% ensure(?Module).
66 | ensure() ->
67 | ensure(?MODULE).
68 |
69 | %% @spec get_base_dir(Module) -> string()
70 | %% @doc Return the application directory for Module. It assumes Module is in
71 | %% a standard OTP layout application in the ebin or src directory.
72 | get_base_dir(Module) ->
73 | {file, Here} = code:is_loaded(Module),
74 | filename:dirname(filename:dirname(Here)).
75 |
76 | %% @spec get_base_dir() -> string()
77 | %% @doc Return the application directory for this application. Equivalent to
78 | %% get_base_dir(?MODULE).
79 | get_base_dir() ->
80 | get_base_dir(?MODULE).
81 |
82 | %% @spec local_path([string()], Module) -> string()
83 | %% @doc Return an application-relative directory from Module's application.
84 | local_path(Components, Module) ->
85 | filename:join([get_base_dir(Module) | Components]).
86 |
87 | %% @spec local_path(Components) -> string()
88 | %% @doc Return an application-relative directory for this application.
89 | %% Equivalent to local_path(Components, ?MODULE).
90 | local_path(Components) ->
91 | local_path(Components, ?MODULE).
92 |
--------------------------------------------------------------------------------
/src/webmachine_error.erl:
--------------------------------------------------------------------------------
1 | %% Copyright (c) 2011-2013 Basho Technologies, Inc. All Rights Reserved.
2 | %%
3 | %% This file is provided to you under the Apache License,
4 | %% Version 2.0 (the "License"); you may not use this file
5 | %% except in compliance with the License. You may obtain
6 | %% a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing,
11 | %% software distributed under the License is distributed on an
12 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
13 | %% KIND, either express or implied. See the License for the
14 | %% specific language governing permissions and limitations
15 | %% under the License.
16 |
17 | %% @doc Default HTTP error reasons for webmachine error responsesf
18 |
19 | -module(webmachine_error).
20 |
21 | -export([reason/1]).
22 |
23 | -spec reason(pos_integer()) -> string().
24 | reason(400) ->
25 | "Bad Request";
26 | reason(401) ->
27 | "Unauthorized";
28 | reason(402) ->
29 | "Payment Requested";
30 | reason(403) ->
31 | "Forbidden";
32 | reason(404) ->
33 | "Not Found";
34 | reason(405) ->
35 | "Method Not Allowed";
36 | reason(406) ->
37 | "Not Acceptable";
38 | reason(407) ->
39 | "Proxy Authentication Required";
40 | reason(408) ->
41 | "Request Timeout";
42 | reason(409) ->
43 | "Conflict";
44 | reason(410) ->
45 | "Gone";
46 | reason(411) ->
47 | "Length Required";
48 | reason(412) ->
49 | "Precondition Failed";
50 | reason(413) ->
51 | "Request Entity Too Large";
52 | reason(414) ->
53 | "Request-URI Too Long";
54 | reason(415) ->
55 | "Unsupported Media Type";
56 | reason(416) ->
57 | "Request Range Not Satisfiable";
58 | reason(417) ->
59 | "Expectation Failed";
60 | reason(500) ->
61 | "Internal Server Error";
62 | reason(501) ->
63 | "Not Implemented";
64 | reason(502) ->
65 | "Bad Gateway";
66 | reason(503) ->
67 | "Service Unavailable";
68 | reason(504) ->
69 | "Gateway Timeout";
70 | reason(505) ->
71 | "HTTP Version Not Supported";
72 | reason(Code) when Code >= 400, Code < 500 ->
73 | "Client Error";
74 | reason(Code) when Code >= 500 ->
75 | "Server Error".
76 |
--------------------------------------------------------------------------------
/src/webmachine_error_handler.erl:
--------------------------------------------------------------------------------
1 | %% @author Justin Sheehy
2 | %% @author Andy Gross
3 | %% @author Jeremy Latt
4 | %% @copyright 2007-2008 Basho Technologies
5 | %%
6 | %% Licensed under the Apache License, Version 2.0 (the "License");
7 | %% you may not use this file except in compliance with the License.
8 | %% You may obtain a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing, software
13 | %% distributed under the License is distributed on an "AS IS" BASIS,
14 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | %% See the License for the specific language governing permissions and
16 | %% limitations under the License.
17 |
18 |
19 | %% @doc Some fairly minimal error message formatters.
20 |
21 | -module(webmachine_error_handler).
22 | -author('Justin Sheehy ').
23 | -author('Andy Gross ').
24 | -author('Jeremy Latt ').
25 |
26 | -export([render_error/3]).
27 |
28 | render_error(Code, Req, Reason) ->
29 | case webmachine_request:has_response_body(Req) of
30 | {true,_} ->
31 | maybe_log(Code, Req, Reason),
32 | webmachine_request:response_body(Req);
33 | {false,_} ->
34 | render_error_body(Code, webmachine_request:trim_state(Req), Reason)
35 | end.
36 |
37 | render_error_body(404, Req, _Reason) ->
38 | {ok, ReqState} =
39 | webmachine_request:add_response_header("Content-Type", "text/html", Req),
40 | {<<"404 Not Found Not Found The requested document was not found on this server.
mochiweb+webmachine web server ">>, ReqState};
41 |
42 | render_error_body(500, Req, Reason) ->
43 | {ok, ReqState} =
44 | webmachine_request:add_response_header("Content-Type", "text/html", Req),
45 | maybe_log(500, Req, Reason),
46 | STString = io_lib:format("~p", [Reason]),
47 | ErrorStart = "500 Internal Server Error Internal Server Error The server encountered an error while processing this request:",
48 | ErrorEnd = "
mochiweb+webmachine web server ",
49 | ErrorIOList = [ErrorStart,STString,ErrorEnd],
50 | {erlang:iolist_to_binary(ErrorIOList), ReqState};
51 |
52 | render_error_body(501, Req, Reason) ->
53 | {ok, ReqState} =
54 | webmachine_request:add_response_header("Content-Type", "text/html", Req),
55 | {Method,_} = webmachine_request:method(Req),
56 | webmachine_log:log_error(501, Req, Reason),
57 | ErrorStr = io_lib:format("501 Not Implemented "
58 | "Not Implemented "
59 | "The server does not support the ~p method. "
60 | "
mochiweb+webmachine web server"
61 | " ",
62 | [Method]),
63 | {erlang:iolist_to_binary(ErrorStr), ReqState};
64 |
65 | render_error_body(503, Req, Reason) ->
66 | {ok, ReqState} =
67 | webmachine_request:add_response_header("Content-Type", "text/html", Req),
68 | webmachine_log:log_error(503, Req, Reason),
69 | ErrorStr = "503 Service Unavailable "
70 | "Service Unavailable "
71 | "The server is currently unable to handle "
72 | "the request due to a temporary overloading "
73 | "or maintenance of the server. "
74 | "
mochiweb+webmachine web server"
75 | " ",
76 | {list_to_binary(ErrorStr), ReqState};
77 |
78 | render_error_body(Code, Req, Reason) ->
79 | {ok, ReqState} =
80 | webmachine_request:add_response_header("Content-Type", "text/html", Req),
81 | ReasonPhrase = httpd_util:reason_phrase(Code),
82 | Body = ["",
83 | integer_to_list(Code),
84 | " ",
85 | ReasonPhrase,
86 | " ",
87 | ReasonPhrase,
88 | " ",
89 | Reason,
90 | "
mochiweb+webmachine web server "],
91 | {iolist_to_binary(Body), ReqState}.
92 |
93 | maybe_log(_Code, _Req, {error, {exit, normal, _Stack}}) ->
94 | %% webmachine_request did an exit(normal), so suppress this
95 | %% message. This usually happens when a chunked upload is
96 | %% interrupted by network failure.
97 | ok;
98 | maybe_log(Code, Req, Reason) ->
99 | webmachine_log:log_error(Code, Req, Reason).
100 |
--------------------------------------------------------------------------------
/src/webmachine_error_log_handler.erl:
--------------------------------------------------------------------------------
1 | %% Copyright (c) 2011-2014 Basho Technologies, Inc. All Rights Reserved.
2 | %%
3 | %% This file is provided to you under the Apache License,
4 | %% Version 2.0 (the "License"); you may not use this file
5 | %% except in compliance with the License. You may obtain
6 | %% a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing,
11 | %% software distributed under the License is distributed on an
12 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
13 | %% KIND, either express or implied. See the License for the
14 | %% specific language governing permissions and limitations
15 | %% under the License.
16 |
17 | %% @doc Default log handler for webmachine
18 |
19 | -module(webmachine_error_log_handler).
20 |
21 | -behaviour(gen_event).
22 |
23 | %% gen_event callbacks
24 | -export([init/1,
25 | handle_call/2,
26 | handle_event/2,
27 | handle_info/2,
28 | terminate/2,
29 | code_change/3]).
30 |
31 | -include("webmachine_logger.hrl").
32 |
33 | -ifdef(TEST).
34 | -include_lib("eunit/include/eunit.hrl").
35 | -endif.
36 |
37 | -record(state, {hourstamp, filename, handle}).
38 |
39 | -define(FILENAME, "wm_error.log").
40 |
41 | %% ===================================================================
42 | %% gen_event callbacks
43 | %% ===================================================================
44 |
45 | %% @private
46 | init([BaseDir]) ->
47 | {ok,_} = webmachine_log:defer_refresh(?MODULE),
48 | FileName = filename:join(BaseDir, ?FILENAME),
49 | {Handle, DateHour} = webmachine_log:log_open(FileName),
50 | {ok, #state{filename=FileName, handle=Handle, hourstamp=DateHour}}.
51 |
52 | %% @private
53 | handle_call({_Label, MRef, get_modules}, State) ->
54 | {ok, {MRef, [?MODULE]}, State};
55 | handle_call({refresh, Time}, State) ->
56 | {NewHour, NewHandle} = webmachine_log:maybe_rotate(?MODULE,
57 | State#state.filename,
58 | State#state.handle,
59 | Time,
60 | State#state.hourstamp),
61 | {ok, ok, State#state{hourstamp=NewHour, handle=NewHandle}};
62 | handle_call(_Request, State) ->
63 | {ok, ok, State}.
64 |
65 | %% @private
66 | handle_event({log_error, Msg}, State) ->
67 | {NewHour, NewHandle} = webmachine_log:maybe_rotate(?MODULE,
68 | State#state.filename,
69 | State#state.handle,
70 | os:timestamp(),
71 | State#state.hourstamp),
72 | NewState = State#state{hourstamp=NewHour, handle=NewHandle},
73 | FormattedMsg = format_req(error, undefined, undefined, Msg),
74 | _ = webmachine_log:log_write(NewState#state.handle, FormattedMsg),
75 | {ok, NewState};
76 | handle_event({log_error, Code, _Req, _Reason}, State) when Code < 500 ->
77 | {ok, State};
78 | handle_event({log_error, Code, Req, Reason}, State) ->
79 | {NewHour, NewHandle} = webmachine_log:maybe_rotate(?MODULE,
80 | State#state.filename,
81 | State#state.handle,
82 | os:timestamp(),
83 | State#state.hourstamp),
84 | NewState = State#state{hourstamp=NewHour, handle=NewHandle},
85 | Msg = format_req(error, Code, Req, Reason),
86 | _ = webmachine_log:log_write(NewState#state.handle, Msg),
87 | {ok, NewState};
88 | handle_event({log_info, Msg}, State) ->
89 | {NewHour, NewHandle} = webmachine_log:maybe_rotate(?MODULE,
90 | State#state.filename,
91 | State#state.handle,
92 | os:timestamp(),
93 | State#state.hourstamp),
94 | NewState = State#state{hourstamp=NewHour, handle=NewHandle},
95 | FormattedMsg = format_req(info, undefined, undefined, Msg),
96 | _ = webmachine_log:log_write(NewState#state.handle, FormattedMsg),
97 | {ok, NewState};
98 | handle_event(_Event, State) ->
99 | {ok, State}.
100 |
101 | %% @private
102 | handle_info(_Info, State) ->
103 | {ok, State}.
104 |
105 | %% @private
106 | terminate(_Reason, _State) ->
107 | ok.
108 |
109 | %% @private
110 | code_change(_OldVsn, State, _Extra) ->
111 | {ok, State}.
112 |
113 | %% ===================================================================
114 | %% Internal functions
115 | %% ===================================================================
116 |
117 | format_req(info, undefined, _, Msg) ->
118 | ["[info] ", Msg];
119 | format_req(error, undefined, _, Msg) ->
120 | ["[error] ", Msg];
121 | format_req(error, 501, Req, _) ->
122 | {Path, _} = Req:path(),
123 | {Method, _} = Req:method(),
124 | Reason = "Webmachine does not support method ",
125 | ["[error] ", Reason, Method, ": path=", Path, $\n];
126 | format_req(error, 503, Req, _) ->
127 | {Path, _} = Req:path(),
128 | Reason = "Webmachine cannot fulfill the request at this time",
129 | ["[error] ", Reason, ": path=", Path, $\n];
130 | format_req(error, _Code, Req, Reason) ->
131 | {Path, _} = Req:path(),
132 | Str = io_lib:format("~p", [Reason]),
133 | ["[error] path=", Path, $\x20, Str, $\n].
134 |
--------------------------------------------------------------------------------
/src/webmachine_headers.erl:
--------------------------------------------------------------------------------
1 | -module(webmachine_headers).
2 |
3 | -type t() :: gb_trees:tree(name(), value()).
4 | -type name() :: 'Cache-Control' % erlang:decode_packet/3
5 | | 'Connection'
6 | | 'Date'
7 | | 'Pragma'
8 | | 'Transfer-Encoding'
9 | | 'Upgrade'
10 | | 'Via'
11 | | 'Accept'
12 | | 'Accept-Charset'
13 | | 'Accept-Encoding'
14 | | 'Accept-Language'
15 | | 'Authorization'
16 | | 'From'
17 | | 'Host'
18 | | 'If-Modified-Since'
19 | | 'If-Match'
20 | | 'If-None-Match'
21 | | 'If-Range'
22 | | 'If-Unmodified-Since'
23 | | 'Max-Forwards'
24 | | 'Proxy-Authorization'
25 | | 'Range'
26 | | 'Referer'
27 | | 'User-Agent'
28 | | 'Age'
29 | | 'Location'
30 | | 'Proxy-Authenticate'
31 | | 'Public'
32 | | 'Retry-After'
33 | | 'Server'
34 | | 'Vary'
35 | | 'Warning'
36 | |'Www-Authenticate'
37 | | 'Allow'
38 | | 'Content-Base'
39 | | 'Content-Encoding'
40 | | 'Content-Language'
41 | | 'Content-Length'
42 | | 'Content-Location'
43 | | 'Content-Md5'
44 | | 'Content-Range'
45 | | 'Content-Type'
46 | | 'Etag'
47 | | 'Expires'
48 | | 'Last-Modified'
49 | | 'Accept-Ranges'
50 | | 'Set-Cookie'
51 | | 'Set-Cookie2'
52 | | 'X-Forwarded-For'
53 | | 'Cookie'
54 | | 'Keep-Alive'
55 | | 'Proxy-Connection'
56 | | string() | binary().
57 |
58 | -type value() :: string() | binary().
59 | -export_type([t/0, name/0, value/0]).
60 |
61 | -export([
62 | empty/0
63 | ]).
64 |
65 | -spec empty() -> t().
66 | empty() ->
67 | gb_trees:empty().
68 |
--------------------------------------------------------------------------------
/src/webmachine_log.erl:
--------------------------------------------------------------------------------
1 | %% Copyright (c) 2011-2014 Basho Technologies, Inc. All Rights Reserved.
2 | %%
3 | %% This file is provided to you under the Apache License,
4 | %% Version 2.0 (the "License"); you may not use this file
5 | %% except in compliance with the License. You may obtain
6 | %% a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing,
11 | %% software distributed under the License is distributed on an
12 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
13 | %% KIND, either express or implied. See the License for the
14 | %% specific language governing permissions and limitations
15 | %% under the License.
16 |
17 | %% @doc Helper functions for webmachine's default log handlers
18 |
19 | -module(webmachine_log).
20 |
21 | -include("webmachine_logger.hrl").
22 | -include("wm_reqdata.hrl").
23 |
24 | -export([add_handler/2,
25 | call/2,
26 | call/3,
27 | datehour/0,
28 | datehour/1,
29 | defer_refresh/1,
30 | delete_handler/1,
31 | fix_log/2,
32 | fmt_ip/1,
33 | fmtnow/0,
34 | log_access/1,
35 | log_close/3,
36 | log_error/1,
37 | log_error/3,
38 | log_info/1,
39 | log_open/1,
40 | log_open/2,
41 | log_write/2,
42 | maybe_rotate/5,
43 | month/1,
44 | refresh/2,
45 | suffix/1,
46 | zeropad/2,
47 | zone/0]).
48 |
49 | %% @doc Add a handler to receive log events
50 | -type add_handler_result() :: ok | {'EXIT', term()} | term().
51 | -spec add_handler(atom() | {atom(), term()}, term()) -> add_handler_result().
52 | add_handler(Mod, Args) ->
53 | gen_event:add_handler(?EVENT_LOGGER, Mod, Args).
54 |
55 | %% @doc Make a synchronous call directly to a specific event handler
56 | %% module
57 | -type error() :: {error, bad_module} | {'EXIT', term()} | term().
58 | -spec call(atom(), term()) -> term() | error().
59 | call(Mod, Msg) ->
60 | gen_event:call(?EVENT_LOGGER, Mod, Msg).
61 |
62 | %% @doc Make a synchronous call directly to a specific event handler
63 | %% module
64 | -spec call(atom(), term(), timeout()) -> term() | error().
65 | call(Mod, Msg, Timeout) ->
66 | gen_event:call(?EVENT_LOGGER, Mod, Msg, Timeout).
67 |
68 | %% @doc Return a four-tuple containing year, month, day, and hour
69 | %% of the current time.
70 | -type datehour() :: {non_neg_integer(), 1..12, 1..31, 0..23}.
71 | -spec datehour() -> datehour().
72 | datehour() ->
73 | datehour(os:timestamp()).
74 |
75 | %% @doc Return a four-tuple containing year, month, day, and hour
76 | %% of the specified time.
77 | -spec datehour(erlang:timestamp()) -> datehour().
78 | datehour(TS) ->
79 | {{Y, M, D}, {H, _, _}} = calendar:now_to_universal_time(TS),
80 | {Y, M, D, H}.
81 |
82 | %% @doc Defer the refresh of a log file.
83 | -spec defer_refresh(atom()) -> {ok, timer:tref()} | {error, term()}.
84 | defer_refresh(Mod) ->
85 | {_, {_, M, S}} = calendar:universal_time(),
86 | Time = 1000 * (3600 - ((M * 60) + S)),
87 | timer:apply_after(Time, ?MODULE, refresh, [Mod, os:timestamp()]).
88 |
89 | %% @doc Remove a log handler
90 | -type delete_handler_result() :: term() | {error, module_not_found} | {'EXIT', term()}.
91 | -spec delete_handler(atom() | {atom(), term()}) -> delete_handler_result().
92 | delete_handler(Mod) ->
93 | gen_event:delete_handler(?EVENT_LOGGER, Mod, []).
94 |
95 | %% Seek backwards to the last valid log entry
96 | -spec fix_log(file:io_device(), non_neg_integer()) -> ok.
97 | fix_log(_FD, 0) ->
98 | ok;
99 | fix_log(FD, 1) ->
100 | {ok, 0} = file:position(FD, 0),
101 | ok;
102 | fix_log(FD, Location) ->
103 | case file:pread(FD, Location - 1, 1) of
104 | {ok, [$\n | _]} ->
105 | ok;
106 | {ok, _} ->
107 | fix_log(FD, Location - 1)
108 | end.
109 |
110 | %% @doc Format an IP address or host name
111 | -spec fmt_ip(undefined | string() | inet:ip4_address() | inet:ip6_address()) -> string().
112 | fmt_ip(IP) when is_tuple(IP) ->
113 | inet_parse:ntoa(IP);
114 | fmt_ip(undefined) ->
115 | "0.0.0.0";
116 | fmt_ip(HostName) ->
117 | HostName.
118 |
119 | %% @doc Format the current time into a string
120 | -spec fmtnow() -> string().
121 | fmtnow() ->
122 | {{Year, Month, Date}, {Hour, Min, Sec}} = calendar:local_time(),
123 | io_lib:format("[~2..0w/~s/~4..0w:~2..0w:~2..0w:~2..0w ~s]",
124 | [Date,month(Month),Year, Hour, Min, Sec, zone()]).
125 |
126 | %% @doc Notify registered log event handler of an access event.
127 | -spec log_access(wm_log_data()) -> ok.
128 | log_access(#wm_log_data{}=LogData) ->
129 | gen_event:sync_notify(?EVENT_LOGGER, {log_access, LogData}).
130 |
131 | %% @doc Close a log file.
132 | -spec log_close(atom(), string(), file:io_device()) -> ok | {error, term()}.
133 | log_close(Mod, Name, FD) ->
134 | error_logger:info_msg("~p: closing log file: ~s~n", [Mod, Name]),
135 | file:close(FD).
136 |
137 | %% @doc Notify registered log event handler of an error event.
138 | -spec log_error(iolist()) -> ok.
139 | log_error(LogMsg) ->
140 | gen_event:sync_notify(?EVENT_LOGGER, {log_error, LogMsg}).
141 |
142 | %% @doc Notify registered log event handler of an error event.
143 | -spec log_error(pos_integer(), webmachine_request:t(), term()) -> ok.
144 | log_error(Code, Req, Reason) ->
145 | gen_event:sync_notify(?EVENT_LOGGER, {log_error, Code, Req, Reason}).
146 |
147 | %% @doc Notify registered log event handler of an info event.
148 | -spec log_info(iolist()) -> ok.
149 | log_info(LogMsg) ->
150 | gen_event:sync_notify(?EVENT_LOGGER, {log_info, LogMsg}).
151 |
152 | %% @doc Open a new log file for writing
153 | -spec log_open(string()) -> {file:io_device(), datehour()}.
154 | log_open(FileName) ->
155 | DateHour = datehour(),
156 | {log_open(FileName, DateHour), DateHour}.
157 |
158 | %% @doc Open a new log file for writing
159 | -spec log_open(string(), datehour()) -> file:io_device().
160 | log_open(FileName, DateHour) ->
161 | LogName = FileName ++ suffix(DateHour),
162 | error_logger:info_msg("opening log file: ~p~n", [LogName]),
163 | ok = filelib:ensure_dir(LogName),
164 | {ok, FD} = file:open(LogName, [read, write, raw]),
165 | {ok, Location} = file:position(FD, eof),
166 | fix_log(FD, Location),
167 | ok = file:truncate(FD),
168 | FD.
169 |
170 | -spec log_write(file:io_device(), iolist()) -> ok | {error, term()}.
171 | log_write(FD, IoData) ->
172 | file:write(FD, IoData).
173 |
174 | %% @doc Rotate a log file if the hour it represents
175 | %% has passed.
176 | -spec maybe_rotate(atom(), string(), file:io_device(), erlang:timestamp(), datehour()) ->
177 | {datehour(), file:io_device()}.
178 | maybe_rotate(Mod, FileName, Handle, Time, Hour) ->
179 | Rotate = datehour(Time) == Hour,
180 | maybe_rotate(Mod, FileName, Handle, Time, Hour, Rotate).
181 |
182 | -spec maybe_rotate(atom(), string(), file:io_device(), erlang:timestamp(), datehour(), boolean()) ->
183 | {datehour(), file:io_device()}.
184 | maybe_rotate(_Mod, _FileName, Handle, _Time, Hour, true) ->
185 | {Hour, Handle};
186 | maybe_rotate(Mod, FileName, Handle, Time, _Hour, false) ->
187 | NewHour = datehour(Time),
188 | {ok,_} = defer_refresh(Mod),
189 | ok = log_close(Mod, FileName, Handle),
190 | NewHandle = log_open(FileName, NewHour),
191 | {NewHour, NewHandle}.
192 |
193 | %% @doc Convert numeric month value to the abbreviation
194 | -spec month(1..12) -> string().
195 | month(1) ->
196 | "Jan";
197 | month(2) ->
198 | "Feb";
199 | month(3) ->
200 | "Mar";
201 | month(4) ->
202 | "Apr";
203 | month(5) ->
204 | "May";
205 | month(6) ->
206 | "Jun";
207 | month(7) ->
208 | "Jul";
209 | month(8) ->
210 | "Aug";
211 | month(9) ->
212 | "Sep";
213 | month(10) ->
214 | "Oct";
215 | month(11) ->
216 | "Nov";
217 | month(12) ->
218 | "Dec".
219 |
220 | %% @doc Make a synchronous call to instruct a log handler to refresh
221 | %% itself.
222 | -spec refresh(atom(), erlang:timestamp()) -> ok | {error, term()}.
223 | refresh(Mod, Time) ->
224 | call(Mod, {refresh, Time}, infinity).
225 |
226 | -spec suffix(datehour()) -> string().
227 | suffix({Y, M, D, H}) ->
228 | YS = zeropad(Y, 4),
229 | MS = zeropad(M, 2),
230 | DS = zeropad(D, 2),
231 | HS = zeropad(H, 2),
232 | lists:flatten([$., YS, $_, MS, $_, DS, $_, HS]).
233 |
234 | -spec zeropad(integer(), integer()) -> string().
235 | zeropad(Num, MinLength) ->
236 | NumStr = integer_to_list(Num),
237 | zeropad_str(NumStr, MinLength - length(NumStr)).
238 |
239 | -spec zeropad_str(string(), integer()) -> string().
240 | zeropad_str(NumStr, Zeros) when Zeros > 0 ->
241 | zeropad_str([$0 | NumStr], Zeros - 1);
242 | zeropad_str(NumStr, _) ->
243 | NumStr.
244 |
245 | -spec zone() -> string().
246 | zone() ->
247 | Time = erlang:universaltime(),
248 | LocalTime = calendar:universal_time_to_local_time(Time),
249 | DiffSecs = calendar:datetime_to_gregorian_seconds(LocalTime) -
250 | calendar:datetime_to_gregorian_seconds(Time),
251 | zone((DiffSecs/3600)*100).
252 |
253 | %% Ugly reformatting code to get times like +0000 and -1300
254 |
255 | -spec zone(float()) -> string().
256 | zone(Val) when Val < 0 ->
257 | io_lib:format("-~4..0w", [trunc(abs(Val))]);
258 | zone(Val) when Val >= 0 ->
259 | io_lib:format("+~4..0w", [trunc(abs(Val))]).
260 |
--------------------------------------------------------------------------------
/src/webmachine_logger_watcher.erl:
--------------------------------------------------------------------------------
1 | %% Copyright (c) 2011-2012 Basho Technologies, Inc. All Rights Reserved.
2 | %%
3 | %% This file is provided to you under the Apache License,
4 | %% Version 2.0 (the "License"); you may not use this file
5 | %% except in compliance with the License. You may obtain
6 | %% a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing,
11 | %% software distributed under the License is distributed on an
12 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
13 | %% KIND, either express or implied. See the License for the
14 | %% specific language governing permissions and limitations
15 | %% under the License.
16 |
17 | %% @doc A process that does a gen_event:add_sup_handler and attempts to re-add
18 | %% event handlers when they exit.
19 |
20 | %% @private
21 |
22 | -module(webmachine_logger_watcher).
23 |
24 | -behaviour(gen_server).
25 |
26 | -ifdef(TEST).
27 | -include_lib("eunit/include/eunit.hrl").
28 | -endif.
29 |
30 | %% callbacks
31 | -export([init/1,
32 | handle_call/3,
33 | handle_cast/2,
34 | handle_info/2,
35 | terminate/2,
36 | code_change/3]).
37 |
38 | -export([start_link/3,
39 | start/3]).
40 |
41 | -record(state, {module, config, event}).
42 |
43 | start_link(Event, Module, Config) ->
44 | gen_server:start_link(?MODULE, [Event, Module, Config], []).
45 |
46 | start(Event, Module, Config) ->
47 | gen_server:start(?MODULE, [Event, Module, Config], []).
48 |
49 | init([Event, Module, Config]) ->
50 | install_handler(Event, Module, Config),
51 | {ok, #state{event=Event, module=Module, config=Config}}.
52 |
53 | handle_call(_Call, _From, State) ->
54 | {reply, ok, State}.
55 |
56 | handle_cast(_Request, State) ->
57 | {noreply, State}.
58 |
59 | handle_info({gen_event_EXIT, Module, normal}, #state{module=Module} = State) ->
60 | {stop, normal, State};
61 | handle_info({gen_event_EXIT, Module, shutdown}, #state{module=Module} = State) ->
62 | {stop, normal, State};
63 | handle_info({gen_event_EXIT, Module, _Reason}, #state{module=Module,
64 | config=Config, event=Event} = State) ->
65 | install_handler(Event, Module, Config),
66 | {noreply, State};
67 | handle_info(reinstall_handler, #state{module=Module, config=Config, event=Event} = State) ->
68 | install_handler(Event, Module, Config),
69 | {noreply, State};
70 | handle_info(_Info, State) ->
71 | {noreply, State}.
72 |
73 | terminate(_Reason, _State) ->
74 | ok.
75 |
76 | code_change(_OldVsn, State, _Extra) ->
77 | {ok, State}.
78 |
79 | %% internal
80 |
81 | install_handler(Event, Module, Config) ->
82 | case gen_event:add_sup_handler(Event, Module, Config) of
83 | ok ->
84 | ok;
85 | _Error ->
86 | erlang:send_after(5000, self(), reinstall_handler),
87 | ok
88 | end.
89 |
--------------------------------------------------------------------------------
/src/webmachine_logger_watcher_sup.erl:
--------------------------------------------------------------------------------
1 | %% Copyright (c) 2011-2012 Basho Technologies, Inc. All Rights Reserved.
2 | %%
3 | %% This file is provided to you under the Apache License,
4 | %% Version 2.0 (the "License"); you may not use this file
5 | %% except in compliance with the License. You may obtain
6 | %% a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing,
11 | %% software distributed under the License is distributed on an
12 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
13 | %% KIND, either express or implied. See the License for the
14 | %% specific language governing permissions and limitations
15 | %% under the License.
16 |
17 | %% @doc A supervisor for monitoring webmachine_logger_handler_watcher processes.
18 |
19 | %% @private
20 |
21 | -module(webmachine_logger_watcher_sup).
22 |
23 | -behaviour(supervisor).
24 |
25 | %% API
26 | -export([start_link/0]).
27 |
28 | %% Callbacks
29 | -export([init/1]).
30 |
31 | start_link() ->
32 | supervisor:start_link({local, ?MODULE}, ?MODULE, []).
33 |
34 | init([]) ->
35 | {ok, {{simple_one_for_one, 10, 60},
36 | [
37 | {webmachine_logger_watcher, {webmachine_logger_watcher, start_link, []},
38 | transient, 5000, worker, [webmachine_logger_watcher]}
39 | ]}}.
40 |
--------------------------------------------------------------------------------
/src/webmachine_mochiweb.erl:
--------------------------------------------------------------------------------
1 | %% @author Justin Sheehy
2 | %% @author Andy Gross
3 | %% @copyright 2007-2014 Basho Technologies
4 | %%
5 | %% Licensed under the Apache License, Version 2.0 (the "License");
6 | %% you may not use this file except in compliance with the License.
7 | %% You may obtain a copy of the License at
8 | %%
9 | %% http://www.apache.org/licenses/LICENSE-2.0
10 | %%
11 | %% Unless required by applicable law or agreed to in writing, software
12 | %% distributed under the License is distributed on an "AS IS" BASIS,
13 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | %% See the License for the specific language governing permissions and
15 | %% limitations under the License.
16 |
17 | %% @doc Mochiweb interface for webmachine.
18 | -module(webmachine_mochiweb).
19 | -author('Justin Sheehy ').
20 | -author('Andy Gross ').
21 | -export([start/1, stop/0, stop/1, loop/2, new_webmachine_req/1]).
22 |
23 | -include("webmachine_logger.hrl").
24 | -include("wm_reqstate.hrl").
25 | -include("wm_reqdata.hrl").
26 |
27 | -type mochiweb_request() ::
28 | {
29 | mochiweb_request,
30 | [any()]
31 | }.
32 | %% [any()] always looks like this. Basically it should be a record,
33 | %% but is a list inside a tuple. There are more specific types out
34 | %% there I'm sure, but I have better things to do with my time than
35 | %% typespec a module from 2007. I might come back and make these more
36 | %% specific if I encounter anything worth while, but since they can't
37 | %% be included in the spec anyway, it's worthless().
38 |
39 | %% [
40 | %% Socket :: any(),
41 | %% Opts :: any(),
42 | %% Method :: any(),
43 | %% RawPath :: any(),
44 | %% Version :: any(),
45 | %% Headers :: any()
46 | %% ]
47 |
48 | -ifdef(TEST).
49 | -include_lib("eunit/include/eunit.hrl").
50 | -endif.
51 |
52 | %% The `log_dir' option is deprecated, but remove it from the
53 | %% options list if it is present
54 | -define(WM_OPTIONS, [error_handler,
55 | log_dir,
56 | rewrite_module,
57 | resource_module_option]).
58 |
59 | -define (WM_OPTION_DEFAULTS, [{error_handler, webmachine_error_handler}]).
60 |
61 | start(Options) ->
62 | {DispatchList, PName, DGroup, WMOptions, OtherOptions} = get_wm_options(Options),
63 | webmachine_router:init_routes(DGroup, DispatchList),
64 | _ = [application_set_unless_env_or_undef(K, V) || {K, V} <- WMOptions],
65 | MochiName = list_to_atom(to_list(PName) ++ "_mochiweb"),
66 | LoopFun = {?MODULE, loop, [DGroup]},
67 | mochiweb_http:start([{name, MochiName}, {loop, LoopFun} | OtherOptions]).
68 |
69 | stop() ->
70 | {registered_name, PName} = process_info(self(), registered_name),
71 | MochiName = list_to_atom(atom_to_list(PName) ++ "_mochiweb"),
72 | stop(MochiName).
73 |
74 | stop(Name) ->
75 | mochiweb_http:stop(Name).
76 |
77 | -spec loop(mochiweb_request(), any()) -> ok.
78 | loop(MochiReq, Name) ->
79 | case new_webmachine_req(MochiReq) of
80 | {{error, NewRequestError}, ErrorReq} ->
81 | handle_error(500, {error, NewRequestError}, ErrorReq);
82 | Req ->
83 | DispatchList = webmachine_router:get_routes(Name),
84 | HostHeaders = host_headers(Req),
85 | Host = host_from_host_values(HostHeaders),
86 | {Path, _} = webmachine_request:path(Req),
87 | {RD, _} = webmachine_request:get_reqdata(Req),
88 | %% Run the dispatch code, catch any errors...
89 | try webmachine_dispatcher:dispatch(Host, Path, DispatchList, RD) of
90 | {no_dispatch_match, _UnmatchedHost, _UnmatchedPathTokens} ->
91 | handle_error(404, {none, none, []}, Req);
92 | {Mod, ModOpts, HostTokens, Port, PathTokens, Bindings,
93 | AppRoot, StringPath} ->
94 | {ok, XReq1} = webmachine_request:load_dispatch_data(
95 | Bindings,HostTokens,Port,
96 | PathTokens,AppRoot,StringPath,Req),
97 | try
98 | {ok, Resource} = webmachine_resource:wrap(
99 | Mod, ModOpts),
100 | {ok, RS2} = webmachine_request:set_metadata(
101 | 'resource_module',
102 | resource_module(Mod, ModOpts),
103 | XReq1),
104 | webmachine_decision_core:handle_request(Resource, RS2)
105 | catch
106 | error:Error ->
107 | handle_error(500, {error, Error}, Req)
108 | end
109 | catch
110 | Type : Error ->
111 | handle_error(500, {Type, Error}, Req)
112 | end
113 | end.
114 |
115 | -spec new_webmachine_req(mochiweb_request()) ->
116 | {module(),#wm_reqstate{}}
117 | |{{error, term()}, #wm_reqstate{}}.
118 | new_webmachine_req(Request) ->
119 | Method = mochiweb_request:get(method, Request),
120 | Scheme = mochiweb_request:get(scheme, Request),
121 | Version = mochiweb_request:get(version, Request),
122 | {Headers, RawPath} =
123 | case application:get_env(webmachine, rewrite_module) of
124 | {ok, undefined} ->
125 | {
126 | mochiweb_request:get(headers, Request),
127 | mochiweb_request:get(raw_path, Request)
128 | };
129 | {ok, RewriteMod} ->
130 | do_rewrite(RewriteMod,
131 | Method,
132 | Scheme,
133 | Version,
134 | mochiweb_request:get(headers, Request),
135 | mochiweb_request:get(raw_path, Request))
136 | end,
137 | Socket = mochiweb_request:get(socket, Request),
138 |
139 | InitialReqData = wrq:create(Method,Scheme,Version,RawPath,Headers),
140 | InitialLogData = #wm_log_data{start_time=os:timestamp(),
141 | method=Method,
142 | headers=Headers,
143 | path=RawPath,
144 | version=Version,
145 | response_code=404,
146 | response_length=0},
147 |
148 | InitState = #wm_reqstate{socket=Socket,
149 | log_data=InitialLogData,
150 | reqdata=InitialReqData},
151 | InitReq = {webmachine_request,InitState},
152 |
153 | case webmachine_request:get_peer(InitReq) of
154 | {ErrorGetPeer = {error,_}, ErrorGetPeerReqState} ->
155 | % failed to get peer
156 | { ErrorGetPeer, webmachine_request:new (ErrorGetPeerReqState) };
157 | {Peer, _ReqState} ->
158 | case webmachine_request:get_sock(InitReq) of
159 | {ErrorGetSock = {error,_}, ErrorGetSockReqState} ->
160 | LogDataWithPeer = InitialLogData#wm_log_data {peer=Peer},
161 | ReqStateWithSockErr =
162 | ErrorGetSockReqState#wm_reqstate{log_data=LogDataWithPeer},
163 | { ErrorGetSock, webmachine_request:new (ReqStateWithSockErr) };
164 | {Sock, ReqState} ->
165 | ReqData = wrq:set_sock(Sock, wrq:set_peer(Peer, InitialReqData)),
166 | LogData =
167 | InitialLogData#wm_log_data {peer=Peer, sock=Sock},
168 | webmachine_request:new(ReqState#wm_reqstate{log_data=LogData,
169 | reqdata=ReqData})
170 | end
171 | end.
172 |
173 | do_rewrite(RewriteMod, Method, Scheme, Version, Headers, RawPath) ->
174 | case RewriteMod:rewrite(Method, Scheme, Version, Headers, RawPath) of
175 | %% only raw path has been rewritten (older style rewriting)
176 | NewPath when is_list(NewPath) -> {Headers, NewPath};
177 |
178 | %% headers and raw path rewritten (new style rewriting)
179 | {NewHeaders, NewPath} -> {NewHeaders,NewPath}
180 | end.
181 |
182 | handle_error(Code, Error, Req) ->
183 | {ok, ErrorHandler} = application:get_env(webmachine, error_handler),
184 | {ErrorHTML,Req1} =
185 | ErrorHandler:render_error(Code, Req, Error),
186 | {ok,Req2} = webmachine_request:append_to_response_body(ErrorHTML, Req1),
187 | {ok,Req3} = webmachine_request:send_response(Code, Req2),
188 | {LogData,_ReqState4} = webmachine_request:log_data(Req3),
189 | spawn(webmachine_log, log_access, [LogData]),
190 | ok.
191 |
192 | get_wm_option(OptName, {WMOptions, OtherOptions}) ->
193 | {Value, UpdOtherOptions} =
194 | handle_get_option_result(get_option(OptName, OtherOptions), OptName),
195 | {[{OptName, Value} | WMOptions], UpdOtherOptions}.
196 |
197 | handle_get_option_result({undefined, Options}, Name) ->
198 | {proplists:get_value(Name, ?WM_OPTION_DEFAULTS), Options};
199 | handle_get_option_result(GetOptRes, _) ->
200 | GetOptRes.
201 |
202 | get_wm_options(Options) ->
203 | {DispatchList, Options1} = get_option(dispatch, Options),
204 | {Name, Options2} =
205 | case get_option(name, Options1) of
206 | {undefined, Opts2} ->
207 | {webmachine, Opts2};
208 | NRes -> NRes
209 | end,
210 | {DGroup, Options3} =
211 | case get_option(dispatch_group, Options2) of
212 | {undefined, Opts3} ->
213 | {default, Opts3};
214 | RRes -> RRes
215 | end,
216 | {WMOptions, RestOptions} = lists:foldl(fun get_wm_option/2, {[], Options3}, ?WM_OPTIONS),
217 | {DispatchList, Name, DGroup, WMOptions, RestOptions}.
218 |
219 | get_option(Option, Options) ->
220 | case lists:keytake(Option, 1, Options) of
221 | false -> {undefined, Options};
222 | {value, {Option, Value}, NewOptions} -> {Value, NewOptions}
223 | end.
224 |
225 | application_set_unless_env_or_undef(_Var, undefined) ->
226 | ok;
227 | application_set_unless_env_or_undef(Var, Value) ->
228 | application_set_unless_env(webmachine, Var, Value).
229 |
230 | application_set_unless_env(App, Var, Value) ->
231 | Current = application:get_all_env(App),
232 | CurrentKeys = proplists:get_keys(Current),
233 | case lists:member(Var, CurrentKeys) of
234 | true ->
235 | ok;
236 | false ->
237 | application:set_env(App, Var, Value)
238 | end.
239 |
240 | %% X-Forwarded-Host/Server can contain comma-separated values.
241 | %% Reference: https://httpd.apache.org/docs/current/mod/mod_proxy.html#x-headers
242 | %% In that case, we'll take the first as our host, since proxies will append
243 | %% additional values to the original.
244 | host_from_host_values(HostValues) ->
245 | case HostValues of
246 | [] ->
247 | [];
248 | [H|_] ->
249 | case string:tokens(H, ",") of
250 | [FirstHost|_] ->
251 | FirstHost;
252 | [] ->
253 | H
254 | end
255 | end.
256 |
257 | host_headers(Req) ->
258 | [ V || {V,_ReqState} <- [webmachine_request:get_header_value(H, Req)
259 | || H <- ["x-forwarded-host",
260 | "x-forwarded-server",
261 | "host"]],
262 | V /= undefined].
263 |
264 | get_app_env(Key) ->
265 | application:get_env(webmachine, Key).
266 |
267 | %% @private
268 | %% @doc This function is used for cases where it may be desirable to
269 | %% override the value that is set in the request metadata under the
270 | %% `resource_module' key. An example would be a pattern where a set of
271 | %% resource modules shares a lot of common functionality that is
272 | %% contained in a single module and is used as the resource in all
273 | %% dispatch rules and the `ModOpts' are used to specify a smaller
274 | %% set of callbacks for resource specialization.
275 | resource_module(Mod, ModOpts) ->
276 | resource_module(Mod, ModOpts, get_app_env(resource_module_option)).
277 |
278 | resource_module(Mod, _, undefined) ->
279 | Mod;
280 | resource_module(Mod, ModOpts, {ok, OptionVal}) ->
281 | proplists:get_value(OptionVal, ModOpts, Mod).
282 |
283 | to_list(L) when is_list(L) ->
284 | L;
285 | to_list(A) when is_atom(A) ->
286 | atom_to_list(A).
287 |
288 | -ifdef(TEST).
289 |
290 | host_from_host_values_test_() ->
291 | [
292 | {"when a host value is multi-part it resolves the first host correctly",
293 | ?_assertEqual("host1",
294 | host_from_host_values(["host1,host2,host3:443","other", "other1"]))
295 | },
296 | {"when a host value is multi-part it retains the port",
297 | ?_assertEqual("host1:443",
298 | host_from_host_values(["host1:443,host2","other", "other1"]))
299 | },
300 | {"a single host per header is resolved correctly",
301 | ?_assertEqual("host1:80",
302 | host_from_host_values(["host1:80","other", "other1"]))
303 | },
304 | {"a missing host is resolved correctly",
305 | ?_assertEqual([],
306 | host_from_host_values([]))
307 | }
308 | ].
309 |
310 | %[
311 | %{"when a host value is multi-part it resolves the first host correctly",
312 | %?_assertEqual("host1:443",
313 | %host_from_host_values(["host1,host2,host3:443","other", "other1"])) }
314 | %].
315 |
316 | -endif.
317 |
--------------------------------------------------------------------------------
/src/webmachine_multipart.erl:
--------------------------------------------------------------------------------
1 | %% @author Justin Sheehy
2 | %% @author Andy Gross
3 | %% @copyright 2009 Basho Technologies
4 |
5 | %% @doc Utility for parsing multipart form bodies.
6 |
7 | %% Licensed under the Apache License, Version 2.0 (the "License");
8 | %% you may not use this file except in compliance with the License.
9 | %% You may obtain a copy of the License at
10 |
11 | %% http://www.apache.org/licenses/LICENSE-2.0
12 |
13 | %% Unless required by applicable law or agreed to in writing, software
14 | %% distributed under the License is distributed on an "AS IS" BASIS,
15 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | %% See the License for the specific language governing permissions and
17 | %% limitations under the License.
18 |
19 | -module(webmachine_multipart).
20 | -author('Justin Sheehy ').
21 | -author('Andy Gross ').
22 | -export([get_all_parts/2,stream_parts/2, find_boundary/1]).
23 |
24 | % @type incoming_req_body() = binary().
25 | % The request body, in "multipart/form-data" (rfc2388) form,
26 |
27 | % @type boundary() = string().
28 | % The multipart boundary, as taken from the containing message's content-type.
29 |
30 | % @type fpart() = {fpartname(), {[fparam()],[fheader()]}, fcontent()}.
31 | % A single part of a multipart form.
32 |
33 | % @type fpartname() = string().
34 | % The name from the form field of a form part.
35 |
36 | % @type fparam() = {binary(), binary()}.
37 | % A key-value parameter from the content-disposition header in a form part.
38 |
39 | % @type fheader() = {binary(), binary()}.
40 | % A header name and value supplied within a form part.
41 |
42 | % @type fcontent() = binary().
43 | % The body content within a form part.
44 |
45 | % @doc Find the multipart boundary for a request.
46 | % @spec find_boundary(wrq:wm_reqdata()) -> boundary()
47 | find_boundary(ReqData) ->
48 | ContentType = wrq:get_req_header("content-type", ReqData),
49 | string:substr(ContentType, string:str(ContentType, "boundary=")
50 | + length("boundary=")).
51 |
52 | % @doc Turn a multipart form into component parts.
53 | % @spec get_all_parts(incoming_req_body(), boundary()) -> [fpart()]
54 | get_all_parts(Body, Boundary) when is_binary(Body), is_list(Boundary) ->
55 | StreamStruct = send_streamed_body(Body,1024),
56 | getparts1(stream_parts(StreamStruct, Boundary), []).
57 |
58 | % @doc Similar to get_all_parts/2, but for streamed/chunked bodies.
59 | % Takes as input the result of wrq:stream_req_body/2, and provides
60 | % either the atom 'done_parts' when no more parts are available, or
61 | % a tuple with the next part and a function. That function will
62 | % have 0-arity and the same return type as stream_parts/2 itself.
63 | % @spec stream_parts(wm_stream(), boundary()) ->
64 | % 'done_parts' | {fpart(), function()}
65 | stream_parts(StreamStruct, Boundary) ->
66 | stream_form(StreamStruct, "--" ++ Boundary, []).
67 |
68 | stream_form(_, _, [<<"----\n">>|_]) -> done_parts;
69 | stream_form(_, _, [<<"--\n">>|_]) -> done_parts;
70 | stream_form({Hunk, Next}, Boundary, []) ->
71 | stream_form(get_more_data(Next), Boundary, re:split(Hunk, Boundary,[]));
72 | stream_form({Hunk, Next}, Boundary, [<<>>|DQ]) ->
73 | stream_form({Hunk, Next}, Boundary, DQ);
74 | stream_form({Hunk, Next}, Boundary, [H|[T1|T2]]) ->
75 | {make_part(H), fun() ->
76 | stream_form({Hunk, Next}, Boundary, [T1|T2]) end};
77 | stream_form({Hunk, really_done}, Boundary, DQ) ->
78 | DQBin = iolist_to_binary(DQ),
79 | FullHunk = <>,
80 | stream_parts(re:split(FullHunk, Boundary,[]));
81 | stream_form({Hunk, Next}, Boundary, [Single]) ->
82 | FullHunk = <>,
83 | stream_form(get_more_data(Next), Boundary, re:split(FullHunk, Boundary,[])).
84 |
85 | stream_parts([]) -> done_parts;
86 | % browsers are fun, and terminate posts slightly differently from each other:
87 | stream_parts([<<"----\n">>]) -> done_parts;
88 | stream_parts([<<"--\n">>]) -> done_parts;
89 | stream_parts([<<"----\r\n">>]) -> done_parts;
90 | stream_parts([<<"--\r\n">>]) -> done_parts;
91 | stream_parts([<<"--\r\n--\n">>]) -> done_parts;
92 | stream_parts([<<"--\r\n--\r\n">>]) -> done_parts;
93 | stream_parts([H|T]) -> {make_part(H), fun() -> stream_parts(T) end}.
94 |
95 | get_more_data(done) -> {<<"--\n">>, really_done};
96 | get_more_data(Fun) -> Fun().
97 |
98 | make_part(PartData) ->
99 | %% Remove the trailing \r\n
100 | [HeadData, BodyWithCRLF] = re:split(PartData, "\\r\\n\\r\\n", [{parts,2}]),
101 | BodyLen = size(BodyWithCRLF) - 2,
102 | <> = BodyWithCRLF,
103 |
104 | HeadList = [list_to_binary(X) ||
105 | X <- string:tokens(binary_to_list(HeadData), "\r\n")],
106 | {Name, Params, Headers} = make_headers(HeadList),
107 | {Name, {Params,Headers}, Body}.
108 |
109 | make_headers(X) ->
110 | make_headers(X, name_undefined, params_undefined, []).
111 | make_headers([], Name, Params, Headers) -> {Name, Params, Headers};
112 | make_headers([<<>>|HL], Name, Params, Headers) ->
113 | make_headers(HL, Name, Params, Headers);
114 | make_headers(
115 | [<<"Content-Disposition: form-data; ", Names/binary>>|HL],
116 | _, _, Headers) ->
117 | {Name, Params} = extract_names(Names),
118 | make_headers(HL, Name, Params, Headers);
119 | make_headers([H|HL], Name, Params, Headers) ->
120 | make_headers(HL, Name, Params, [cheap_parse_header(H)|Headers]).
121 |
122 | extract_names(NamesString) ->
123 | Params = [{K, V} ||
124 | {K, [<<>>, V, <<>>]} <- [{K0, re:split(V0,"\"",[])} ||
125 | [K0, V0] <- [re:split(N, "=", [{parts, 2}]) ||
126 | N <- re:split(NamesString, "; ", [])]]],
127 | Name = hd([binary_to_list(V) || {<<"name">>,V} <- Params]),
128 | {Name, Params}.
129 |
130 | cheap_parse_header(HeadBin) ->
131 | [K,V] = re:split(HeadBin, ": ", [{parts,2}]),
132 | {K,V}.
133 |
134 | getparts1(done_parts, Acc) ->
135 | lists:reverse(Acc);
136 | getparts1({Part, Streamer}, Acc) ->
137 | getparts1(Streamer(), [Part|Acc]).
138 |
139 | send_streamed_body(Body, Max) ->
140 | HunkLen=8*Max,
141 | case Body of
142 | <> ->
143 | {<>, fun() -> send_streamed_body(Rest,Max) end};
144 | _ ->
145 | {Body, done}
146 | end.
147 |
148 | %%
149 | %% Tests
150 | %%
151 | -ifdef(TEST).
152 | -include_lib("eunit/include/eunit.hrl").
153 |
154 | body_test() ->
155 | Body = <<"------------ae0gL6gL6Ij5KM7Ef1KM7ei4ae0cH2\r\nContent-Disposition: form-data; name=\"Filename\"\r\n\r\ntestfile.txt\r\n------------ae0gL6gL6Ij5KM7Ef1KM7ei4ae0cH2\r\nContent-Disposition: form-data; name=\"Filedata\"; filename=\"testfile.txt\"\r\nContent-Type: application/octet-stream\r\n\r\n%%% The contents of this file are a test,\n%%% do not be alarmed.\n\r\n------------ae0gL6gL6Ij5KM7Ef1KM7ei4ae0cH2\r\nContent-Disposition: form-data; name=\"Upload\"\r\n\r\nSubmit Query\r\n------------ae0gL6gL6Ij5KM7Ef1KM7ei4ae0cH2--">>,
156 | Boundary = "----------ae0gL6gL6Ij5KM7Ef1KM7ei4ae0cH2",
157 | ?assertEqual(
158 | [{"Filename",
159 | {[{<<"name">>,<<"Filename">>}],[]},
160 | <<"testfile.txt">>},
161 | {"Filedata",
162 | {[{<<"name">>,<<"Filedata">>},
163 | {<<"filename">>,<<"testfile.txt">>}],
164 | [{<<"Content-Type">>,<<"application/octet-stream">>}]},
165 | <<"%%% The contents of this file are a test,\n%%% do not be alarmed.\n">>},
166 | {"Upload",{[{<<"name">>,<<"Upload">>}],[]},
167 | <<"Submit Query">>}],
168 | get_all_parts(Body, Boundary)).
169 |
170 | body2_test() ->
171 | Body = <<"-----------------------------89205314411538515011004844897\r\nContent-Disposition: form-data; name=\"Filedata\"; filename=\"akamai.txt\"\r\nContent-Type: text/plain\r\n\r\nCAMBRIDGE, MA - February 18, 2009 - Akamai Technologies, Inc. (NASDAQ: AKAM), the leader in powering rich media, dynamic transactions and enterprise applications online, today announced that its Service & Support organization was awarded top honors for Innovation in Customer Service at the 3rd Annual Stevie Awards for Sales & Customer Service, an international competition recognizing excellence in disciplines that are crucial to business success.\n\n\"We have always set incredibly high standards with respect to the service and support we provide our customers,\" said Sanjay Singh, vice president of Global Service & Support at Akamai. \"Our support team provides highly responsive service around the clock to our global customer base and, as a result, has become an extension of our customers' online businesses. This prestigious award is validation of Akamai's commitment to customer service and technical support.\"\n\nAkamai Service & Support professionals are dedicated to working with customers on a daily basis to fine tune, optimize, and support their Internet initiatives. Akamai's winning submission highlighted the key pillars of its service and support offering, as well as the initiatives established to meet customer requirements for proactive communication, simplification, and faster response times.\n\n\"This year's honorees demonstrate that even in challenging economic times, it's possible for organizations to continue to shine in sales and customer service, the two most important functions in business: acquiring and keeping customers,\" said Michael Gallagher, president of the Stevie Awards.\n\nThe awards are presented by the Stevie Awards, which organizes several of the world's leading business awards shows, including the prestigious American Business Awards. Nicknamed the Stevies for the Greek word \"crowned,\" winners were announced during a gala banquet on Monday, February 9 at Caesars Palace in Las Vegas. Nominated customer service and sales executives from the U.S.A. and several other countries attended. More than 500 entries from companies of all sizes and in virtually every industry were submitted to this year's competition. There are 27 categories for customer service professionals, as well as 41 categories for sales professionals.\n\nDetails about the Stevie Awards for Sales & Customer Service and the list of honorees in all categories are available at www.stevieawards.com/sales. \n\r\n-----------------------------89205314411538515011004844897--\r\n">>,
172 | Boundary = "---------------------------89205314411538515011004844897",
173 | ?assertEqual(
174 | [{"Filedata",
175 | {[{<<"name">>,<<"Filedata">>},
176 | {<<"filename">>,<<"akamai.txt">>}],
177 | [{<<"Content-Type">>,<<"text/plain">>}]},
178 | <<"CAMBRIDGE, MA - February 18, 2009 - Akamai Technologies, Inc. (NASDAQ: AKAM), the leader in powering rich media, dynamic transactions and enterprise applications online, today announced that its Service & Support organization was awarded top honors for Innovation in Customer Service at the 3rd Annual Stevie Awards for Sales & Customer Service, an international competition recognizing excellence in disciplines that are crucial to business success.\n\n\"We have always set incredibly high standards with respect to the service and support we provide our customers,\" said Sanjay Singh, vice president of Global Service & Support at Akamai. \"Our support team provides highly responsive service around the clock to our global customer base and, as a result, has become an extension of our customers' online businesses. This prestigious award is validation of Akamai's commitment to customer service and technical support.\"\n\nAkamai Service & Support professionals are dedicated to working with customers on a daily basis to fine tune, optimize, and support their Internet initiatives. Akamai's winning submission highlighted the key pillars of its service and support offering, as well as the initiatives established to meet customer requirements for proactive communication, simplification, and faster response times.\n\n\"This year's honorees demonstrate that even in challenging economic times, it's possible for organizations to continue to shine in sales and customer service, the two most important functions in business: acquiring and keeping customers,\" said Michael Gallagher, president of the Stevie Awards.\n\nThe awards are presented by the Stevie Awards, which organizes several of the world's leading business awards shows, including the prestigious American Business Awards. Nicknamed the Stevies for the Greek word \"crowned,\" winners were announced during a gala banquet on Monday, February 9 at Caesars Palace in Las Vegas. Nominated customer service and sales executives from the U.S.A. and several other countries attended. More than 500 entries from companies of all sizes and in virtually every industry were submitted to this year's competition. There are 27 categories for customer service professionals, as well as 41 categories for sales professionals.\n\nDetails about the Stevie Awards for Sales & Customer Service and the list of honorees in all categories are available at www.stevieawards.com/sales. \n">>
179 | }],
180 | get_all_parts(Body,Boundary)).
181 |
182 | firefox_test() ->
183 | Body = <<"-----------------------------823378840143542612896544303\r\nContent-Disposition: form-data; name=\"upload-test\"; filename=\"abcdef.txt\"\r\nContent-Type: text/plain\r\n\r\n01234567890123456789012345678901234567890123456789\r\n-----------------------------823378840143542612896544303--\r\n">>,
184 | Boundary = "---------------------------823378840143542612896544303",
185 | ?assertEqual(
186 | [{"upload-test",
187 | {[{<<"name">>,<<"upload-test">>},
188 | {<<"filename">>,<<"abcdef.txt">>}],
189 | [{<<"Content-Type">>,<<"text/plain">>}]},
190 | <<"01234567890123456789012345678901234567890123456789">>}],
191 | get_all_parts(Body,Boundary)).
192 |
193 | chrome_test() ->
194 | Body = <<"------WebKitFormBoundaryIHB9Xyi7ZCNKJusP\r\nContent-Disposition: form-data; name=\"upload-test\"; filename=\"abcdef.txt\"\r\nContent-Type: text/plain\r\n\r\n01234567890123456789012345678901234567890123456789\r\n------WebKitFormBoundaryIHB9Xyi7ZCNKJusP--\r\n">>,
195 | Boundary = "----WebKitFormBoundaryIHB9Xyi7ZCNKJusP",
196 | ?assertEqual(
197 | [{"upload-test",
198 | {[{<<"name">>,<<"upload-test">>},
199 | {<<"filename">>,<<"abcdef.txt">>}],
200 | [{<<"Content-Type">>,<<"text/plain">>}]},
201 | <<"01234567890123456789012345678901234567890123456789">>}],
202 | get_all_parts(Body,Boundary)).
203 |
204 | -endif.
205 |
--------------------------------------------------------------------------------
/src/webmachine_perf_log_handler.erl:
--------------------------------------------------------------------------------
1 | %% Copyright (c) 2011-2014 Basho Technologies, Inc. All Rights Reserved.
2 | %%
3 | %% This file is provided to you under the Apache License,
4 | %% Version 2.0 (the "License"); you may not use this file
5 | %% except in compliance with the License. You may obtain
6 | %% a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing,
11 | %% software distributed under the License is distributed on an
12 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
13 | %% KIND, either express or implied. See the License for the
14 | %% specific language governing permissions and limitations
15 | %% under the License.
16 |
17 | %% @doc Default performance log handler for webmachine
18 |
19 | -module(webmachine_perf_log_handler).
20 |
21 | -behaviour(gen_event).
22 |
23 | %% gen_event callbacks
24 | -export([init/1,
25 | handle_call/2,
26 | handle_event/2,
27 | handle_info/2,
28 | terminate/2,
29 | code_change/3]).
30 |
31 | -include("webmachine_logger.hrl").
32 |
33 | -ifdef(TEST).
34 | -include_lib("eunit/include/eunit.hrl").
35 | -endif.
36 |
37 | -record(state, {hourstamp, filename, handle}).
38 |
39 | -define(FILENAME, "perf.log").
40 |
41 | %% ===================================================================
42 | %% gen_event callbacks
43 | %% ===================================================================
44 |
45 | %% @private
46 | init([BaseDir]) ->
47 | {ok,_} = webmachine_log:defer_refresh(?MODULE),
48 | FileName = filename:join(BaseDir, ?FILENAME),
49 | {Handle, DateHour} = webmachine_log:log_open(FileName),
50 | {ok, #state{filename=FileName, handle=Handle, hourstamp=DateHour}}.
51 |
52 | %% @private
53 | handle_call({_Label, MRef, get_modules}, State) ->
54 | {ok, {MRef, [?MODULE]}, State};
55 | handle_call({refresh, Time}, State) ->
56 | {NewHour, NewHandle} = webmachine_log:maybe_rotate(?MODULE,
57 | State#state.filename,
58 | State#state.handle,
59 | Time,
60 | State#state.hourstamp),
61 | {ok, ok, State#state{hourstamp=NewHour, handle=NewHandle}};
62 | handle_call(_Request, State) ->
63 | {ok, ok, State}.
64 |
65 | %% @private
66 | handle_event({log_access, LogData}, State) ->
67 | {NewHour, NewHandle} = webmachine_log:maybe_rotate(?MODULE,
68 | State#state.filename,
69 | State#state.handle,
70 | os:timestamp(),
71 | State#state.hourstamp),
72 | NewState = State#state{hourstamp=NewHour, handle=NewHandle},
73 | Msg = format_req(LogData),
74 | _ = webmachine_log:log_write(NewState#state.handle, Msg),
75 | {ok, NewState};
76 | handle_event(_Event, State) ->
77 | {ok, State}.
78 |
79 | %% @private
80 | handle_info(_Info, State) ->
81 | {ok, State}.
82 |
83 | %% @private
84 | terminate(_Reason, _State) ->
85 | ok.
86 |
87 | %% @private
88 | code_change(_OldVsn, State, _Extra) ->
89 | {ok, State}.
90 |
91 | %% ===================================================================
92 | %% Internal functions
93 | %% ===================================================================
94 |
95 | format_req(#wm_log_data{resource_module=Mod,
96 | start_time=StartTime,
97 | method=Method,
98 | peer=Peer,
99 | path=Path,
100 | version=Version,
101 | response_code=ResponseCode,
102 | response_length=ResponseLength,
103 | end_time=EndTime,
104 | finish_time=FinishTime}) ->
105 | Time = webmachine_log:fmtnow(),
106 | Status = case ResponseCode of
107 | {Code, _ReasonPhrase} when is_integer(Code) ->
108 | integer_to_list(Code);
109 | _ when is_integer(ResponseCode) ->
110 | integer_to_list(ResponseCode);
111 | _ ->
112 | ResponseCode
113 | end,
114 | Length = integer_to_list(ResponseLength),
115 | TTPD = webmachine_util:now_diff_milliseconds(EndTime, StartTime),
116 | TTPS = webmachine_util:now_diff_milliseconds(FinishTime, EndTime),
117 | fmt_plog(Time, Peer, Method, Path, Version,
118 | Status, Length, atom_to_list(Mod), integer_to_list(TTPD),
119 | integer_to_list(TTPS)).
120 |
121 | fmt_plog(Time, Ip, Method0, Path, {VM,Vm}, Status, Length, Mod, TTPD, TTPS)
122 | when is_atom(Method0) ->
123 | Method = atom_to_list(Method0),
124 | fmt_plog(Time, Ip, Method, Path, {VM,Vm}, Status, Length, Mod, TTPD, TTPS);
125 | fmt_plog(Time, Ip, Method, Path, {VM,Vm}, Status, Length, Mod, TTPD, TTPS) ->
126 | [webmachine_log:fmt_ip(Ip), " - ", [$\s], Time, [$\s, $"], Method, " ", Path,
127 | " HTTP/", integer_to_list(VM), ".", integer_to_list(Vm), [$",$\s],
128 | Status, [$\s], Length, " " , Mod, " ", TTPD, " ", TTPS, $\n].
129 |
130 |
131 | -ifdef(TEST).
132 |
133 | non_standard_method_test() ->
134 | LogData = #wm_log_data{resource_module=foo,
135 | start_time=os:timestamp(),
136 | method="FOO",
137 | peer={127,0,0,1},
138 | path="/",
139 | version={1,1},
140 | response_code=501,
141 | response_length=1234,
142 | end_time=os:timestamp(),
143 | finish_time=os:timestamp()},
144 | LogEntry = format_req(LogData),
145 | ?assert(is_list(LogEntry)),
146 | ok.
147 |
148 | -endif.
149 |
--------------------------------------------------------------------------------
/src/webmachine_resource.erl:
--------------------------------------------------------------------------------
1 | %% @author Justin Sheehy
2 | %% @author Andy Gross
3 | %% @copyright 2007-2014 Basho Technologies
4 | %%
5 | %% Licensed under the Apache License, Version 2.0 (the "License");
6 | %% you may not use this file except in compliance with the License.
7 | %% You may obtain a copy of the License at
8 | %%
9 | %% http://www.apache.org/licenses/LICENSE-2.0
10 | %%
11 | %% Unless required by applicable law or agreed to in writing, software
12 | %% distributed under the License is distributed on an "AS IS" BASIS,
13 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | %% See the License for the specific language governing permissions and
15 | %% limitations under the License.
16 |
17 | -module(webmachine_resource).
18 | -author('Justin Sheehy ').
19 | -author('Andy Gross ').
20 | -export([new/3, wrap/2]).
21 | -export([do/3,log_d/2,stop/1]).
22 |
23 | -include("wm_compat.hrl").
24 | -include("wm_resource.hrl").
25 | -include("wm_reqdata.hrl").
26 | -include("wm_reqstate.hrl").
27 |
28 | -type t() :: #wm_resource{}.
29 | -export_type([t/0]).
30 |
31 | -define(CALLBACK_ARITY, 2).
32 |
33 | %% Suppress Erlang/OTP 21 warnings about the new method to retrieve
34 | %% stacktraces.
35 | -ifdef(OTP_RELEASE).
36 | -compile({nowarn_deprecated_function, [{erlang, get_stacktrace, 0}]}).
37 | -endif.
38 |
39 | new(R_Mod, R_ModState, R_Trace) ->
40 | case erlang:module_loaded(R_Mod) of
41 | false -> code:ensure_loaded(R_Mod);
42 | true -> ok
43 | end,
44 | #wm_resource{
45 | module = R_Mod,
46 | modstate = R_ModState,
47 | trace = R_Trace
48 | }.
49 |
50 | default(service_available) ->
51 | true;
52 | default(resource_exists) ->
53 | true;
54 | default(is_authorized) ->
55 | true;
56 | default(forbidden) ->
57 | false;
58 | default(allow_missing_post) ->
59 | false;
60 | default(malformed_request) ->
61 | false;
62 | default(uri_too_long) ->
63 | false;
64 | default(known_content_type) ->
65 | true;
66 | default(valid_content_headers) ->
67 | true;
68 | default(valid_entity_length) ->
69 | true;
70 | default(options) ->
71 | [];
72 | default(allowed_methods) ->
73 | ['GET', 'HEAD'];
74 | default(known_methods) ->
75 | ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'TRACE', 'CONNECT', 'OPTIONS'];
76 | default(content_types_provided) ->
77 | [{"text/html", to_html}];
78 | default(content_types_accepted) ->
79 | [];
80 | default(delete_resource) ->
81 | false;
82 | default(delete_completed) ->
83 | true;
84 | default(post_is_create) ->
85 | false;
86 | default(create_path) ->
87 | undefined;
88 | default(base_uri) ->
89 | undefined;
90 | default(process_post) ->
91 | false;
92 | default(language_available) ->
93 | true;
94 | default(charsets_provided) ->
95 | no_charset; % this atom causes charset-negotation to short-circuit
96 | % the default setting is needed for non-charset responses such as image/png
97 | % an example of how one might do actual negotiation
98 | % [{"iso-8859-1", fun(X) -> X end}, {"utf-8", make_utf8}];
99 | default(encodings_provided) ->
100 | [{"identity", fun(X) -> X end}];
101 | % this is handy for auto-gzip of GET-only resources:
102 | % [{"identity", fun(X) -> X end}, {"gzip", fun(X) -> zlib:gzip(X) end}];
103 | default(variances) ->
104 | [];
105 | default(is_conflict) ->
106 | false;
107 | default(multiple_choices) ->
108 | false;
109 | default(previously_existed) ->
110 | false;
111 | default(moved_permanently) ->
112 | false;
113 | default(moved_temporarily) ->
114 | false;
115 | default(last_modified) ->
116 | undefined;
117 | default(expires) ->
118 | undefined;
119 | default(generate_etag) ->
120 | undefined;
121 | default(finish_request) ->
122 | true;
123 | default(validate_content_checksum) ->
124 | not_validated;
125 | default(_) ->
126 | no_default.
127 |
128 | -spec wrap(module(), [any()]) ->
129 | {ok, t()} | {stop, bad_init_arg}.
130 | wrap(Mod, Args) ->
131 | case Mod:init(Args) of
132 | {ok, ModState} ->
133 | {ok, webmachine_resource:new(Mod, ModState, false)};
134 | {{trace, Dir}, ModState} ->
135 | {ok, File} = open_log_file(Dir, Mod),
136 | log_decision(File, v3b14),
137 | log_call(File, attempt, Mod, init, Args),
138 | log_call(File, result, Mod, init, {{trace, Dir}, ModState}),
139 | {ok, webmachine_resource:new(Mod, ModState, File)};
140 | _ ->
141 | {stop, bad_init_arg}
142 | end.
143 |
144 | do(#wm_resource{}=Res, Fun, ReqProps) ->
145 | do(Fun, ReqProps, Res);
146 | do(Fun, ReqProps,
147 | #wm_resource{
148 | module=R_Mod,
149 | trace=R_Trace
150 | }=Req)
151 | when is_atom(Fun) andalso is_list(ReqProps) ->
152 | case lists:keyfind(reqstate, 1, ReqProps) of
153 | false -> RState0 = undefined;
154 | {reqstate, RState0} -> ok
155 | end,
156 | put(tmp_reqstate, empty),
157 | {Reply, ReqData, NewModState} = handle_wm_call(Fun,
158 | (RState0#wm_reqstate.reqdata)#wm_reqdata{wm_state=RState0},
159 | Req),
160 | ReqState = case get(tmp_reqstate) of
161 | empty -> RState0;
162 | X -> X
163 | end,
164 | %% Do not need the embedded state anymore
165 | TrimData = ReqData#wm_reqdata{wm_state=undefined},
166 | {Reply,
167 | webmachine_resource:new(R_Mod, NewModState, R_Trace),
168 | ReqState#wm_reqstate{reqdata=TrimData}}.
169 |
170 | handle_wm_call(Fun, ReqData,
171 | #wm_resource{
172 | module=R_Mod,
173 | modstate=R_ModState,
174 | trace=R_Trace
175 | }=Req) ->
176 | case default(Fun) of
177 | no_default ->
178 | resource_call(Fun, ReqData, Req);
179 | Default ->
180 | case erlang:function_exported(R_Mod, Fun, ?CALLBACK_ARITY) of
181 | true ->
182 | resource_call(Fun, ReqData, Req);
183 | false ->
184 | if is_pid(R_Trace) ->
185 | log_call(R_Trace,
186 | not_exported,
187 | R_Mod, Fun, [ReqData, R_ModState]);
188 | true -> ok
189 | end,
190 | {Default, ReqData, R_ModState}
191 | end
192 | end.
193 |
194 | trim_trace([{M,F,[RD = #wm_reqdata{},S],_}|STRest]) ->
195 | TrimState = (RD#wm_reqdata.wm_state)#wm_reqstate{reqdata='REQDATA'},
196 | TrimRD = RD#wm_reqdata{wm_state=TrimState},
197 | [{M,F,[TrimRD,S]}|STRest];
198 | trim_trace(X) -> X.
199 |
200 | resource_call(F, ReqData,
201 | #wm_resource{
202 | module=R_Mod,
203 | modstate=R_ModState,
204 | trace=R_Trace
205 | }) ->
206 | case R_Trace of
207 | false -> nop;
208 | _ -> log_call(R_Trace, attempt, R_Mod, F, [ReqData, R_ModState])
209 | end,
210 | Result = try
211 | %% Note: the argument list must match the definition of CALLBACK_ARITY
212 | apply(R_Mod, F, [ReqData, R_ModState])
213 | catch ?STPATTERN(C:R) ->
214 | Reason = {C, R, trim_trace(?STACKTRACE)},
215 | {{error, Reason}, ReqData, R_ModState}
216 | end,
217 | case R_Trace of
218 | false -> nop;
219 | _ -> log_call(R_Trace, result, R_Mod, F, Result)
220 | end,
221 | Result.
222 |
223 | log_d(#wm_resource{}=Res, DecisionID) ->
224 | log_d(DecisionID, Res);
225 | log_d(DecisionID,
226 | #wm_resource{
227 | trace=R_Trace
228 | }) ->
229 | case R_Trace of
230 | false -> nop;
231 | _ -> log_decision(R_Trace, DecisionID)
232 | end.
233 |
234 | stop(#wm_resource{trace=R_Trace}) -> close_log_file(R_Trace).
235 |
236 | log_call(File, Type, M, F, Data) ->
237 | io:format(File,
238 | "{~p, ~p, ~p,~n ~p}.~n",
239 | [Type, M, F, escape_trace_data(Data)]).
240 |
241 | escape_trace_data(Fun) when is_function(Fun) ->
242 | {'WMTRACE_ESCAPED_FUN',
243 | [erlang:fun_info(Fun, module),
244 | erlang:fun_info(Fun, name),
245 | erlang:fun_info(Fun, arity),
246 | erlang:fun_info(Fun, type)]};
247 | escape_trace_data(Pid) when is_pid(Pid) ->
248 | {'WMTRACE_ESCAPED_PID', pid_to_list(Pid)};
249 | escape_trace_data(Port) when is_port(Port) ->
250 | {'WMTRACE_ESCAPED_PORT', erlang:port_to_list(Port)};
251 | escape_trace_data(List) when is_list(List) ->
252 | escape_trace_list(List, []);
253 | escape_trace_data(R=#wm_reqstate{}) ->
254 | list_to_tuple(
255 | escape_trace_data(
256 | tuple_to_list(R#wm_reqstate{reqdata='WMTRACE_NESTED_REQDATA'})));
257 | escape_trace_data(Tuple) when is_tuple(Tuple) ->
258 | list_to_tuple(escape_trace_data(tuple_to_list(Tuple)));
259 | escape_trace_data(Other) ->
260 | Other.
261 |
262 | escape_trace_list([Head|Tail], Acc) ->
263 | escape_trace_list(Tail, [escape_trace_data(Head)|Acc]);
264 | escape_trace_list([], Acc) ->
265 | %% proper, nil-terminated list
266 | lists:reverse(Acc);
267 | escape_trace_list(Final, Acc) ->
268 | %% non-nil-terminated list, like the dict module uses
269 | lists:reverse(tl(Acc))++[hd(Acc)|escape_trace_data(Final)].
270 |
271 | log_decision(File, DecisionID) ->
272 | io:format(File, "{decision, ~p}.~n", [DecisionID]).
273 |
274 | open_log_file(Dir, Mod) ->
275 | Now = {_,_,US} = os:timestamp(),
276 | {{Y,M,D},{H,I,S}} = calendar:now_to_universal_time(Now),
277 | Filename = io_lib:format(
278 | "~s/~p-~4..0B-~2..0B-~2..0B"
279 | "-~2..0B-~2..0B-~2..0B.~6..0B.wmtrace",
280 | [Dir, Mod, Y, M, D, H, I, S, US]),
281 | file:open(Filename, [write]).
282 |
283 | close_log_file(File) when is_pid(File) ->
284 | file:close(File);
285 | close_log_file(_) ->
286 | ok.
287 |
--------------------------------------------------------------------------------
/src/webmachine_router.erl:
--------------------------------------------------------------------------------
1 | %% @author Kevin A. Smith
2 | %% @copyright 2007-2010 Basho Technologies
3 | %%
4 | %% Licensed under the Apache License, Version 2.0 (the "License");
5 | %% you may not use this file except in compliance with the License.
6 | %% You may obtain a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing, software
11 | %% distributed under the License is distributed on an "AS IS" BASIS,
12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | %% See the License for the specific language governing permissions and
14 | %% limitations under the License.
15 |
16 | %% @doc Module to add and remove dynamic routes to webmachine's routing
17 | %% table. Dynamic routes are not persistent between executions of
18 | %% a webmachine application. They will need to be added to the
19 | %% the table each time webmachine restarts.
20 | -module(webmachine_router).
21 |
22 | -behaviour(gen_server).
23 |
24 | %% API
25 | -export([start_link/0,
26 | add_route/1,
27 | add_route/2,
28 | remove_route/1,
29 | remove_route/2,
30 | remove_resource/1,
31 | remove_resource/2,
32 | get_routes/0,
33 | get_routes/1,
34 | init_routes/1,
35 | init_routes/2
36 | ]).
37 |
38 | %% gen_server callbacks
39 | -export([init/1,
40 | handle_call/3,
41 | handle_cast/2,
42 | handle_info/2,
43 | terminate/2,
44 | code_change/3]).
45 |
46 | %% @type hostmatchterm() = {hostmatch(), [pathmatchterm()]}.
47 | % The dispatch configuration contains a list of these terms, and the
48 | % first one whose host and one pathmatchterm match is used.
49 |
50 | %% @type pathmatchterm() = {[pathterm()], matchmod(), matchopts()}.
51 | % The dispatch configuration contains a list of these terms, and the
52 | % first one whose list of pathterms matches the input path is used.
53 |
54 | %% @type pathterm() = '*' | string() | atom().
55 | % A list of pathterms is matched against a '/'-separated input path.
56 | % The '*' pathterm matches all remaining tokens.
57 | % A string pathterm will match a token of exactly the same string.
58 | % Any atom pathterm other than '*' will match any token and will
59 | % create a binding in the result if a complete match occurs.
60 |
61 | %% @type matchmod() = atom().
62 | % This atom, if present in a successful matchterm, will appear in
63 | % the resulting dispterm. In Webmachine this is used to name the
64 | % resource module that will handle the matching request.
65 |
66 | %% @type matchopts() = [term()].
67 | % This term, if present in a successful matchterm, will appear in
68 | % the resulting dispterm. In Webmachine this is used to provide
69 | % arguments to the resource module handling the matching request.
70 |
71 | -define(SERVER, ?MODULE).
72 |
73 | %% @spec add_route(hostmatchterm() | pathmatchterm()) -> ok
74 | %% @doc Adds a route to webmachine's route table. The route should
75 | %% be the format documented here:
76 | %% http://bitbucket.org/justin/webmachine/wiki/DispatchConfiguration
77 | add_route(Route) ->
78 | add_route(default, Route).
79 |
80 | add_route(Name, Route) ->
81 | gen_server:call(?SERVER, {add_route, Name, Route}, infinity).
82 |
83 | %% @spec remove_route(hostmatchterm() | pathmatchterm()) -> ok
84 | %% @doc Removes a route from webamchine's route table. The route
85 | %% route must be properly formatted
86 | %% @see add_route/2
87 | remove_route(Route) ->
88 | remove_route(default, Route).
89 |
90 | remove_route(Name, Route) ->
91 | gen_server:call(?SERVER, {remove_route, Name, Route}, infinity).
92 |
93 | %% @spec remove_resource(atom()) -> ok
94 | %% @doc Removes all routes for a specific resource module.
95 | remove_resource(Resource) when is_atom(Resource) ->
96 | remove_resource(default, Resource).
97 |
98 | remove_resource(Name, Resource) when is_atom(Resource) ->
99 | gen_server:call(?SERVER, {remove_resource, Name, Resource}, infinity).
100 |
101 | %% @spec get_routes() -> [{[], res, []}]
102 | %% @doc Retrieve a list of routes and resources set in webmachine's
103 | %% route table.
104 | get_routes() ->
105 | get_routes(default).
106 |
107 | get_routes(Name) ->
108 | get_dispatch_list(Name).
109 |
110 | %% @spec init_routes([hostmatchterm() | pathmatchterm()]) -> ok
111 | %% @doc Set the default routes, unless the routing table isn't empty.
112 | init_routes(DefaultRoutes) ->
113 | init_routes(default, DefaultRoutes).
114 |
115 | init_routes(Name, DefaultRoutes) ->
116 | gen_server:call(?SERVER, {init_routes, Name, DefaultRoutes}, infinity).
117 |
118 | %% @spec start_link() -> {ok, pid()} | {error, any()}
119 | %% @doc Starts the webmachine_router gen_server.
120 | start_link() ->
121 | %% We expect to only be called from webmachine_sup
122 | %%
123 | %% Set up the ETS configuration table.
124 | try ets:new(?MODULE, [named_table, public, set, {keypos, 1},
125 | {read_concurrency, true}]) of
126 | _Result ->
127 | ok
128 | catch
129 | error:badarg ->
130 | %% The table already exists, which is fine. The webmachine_router
131 | %% probably crashed and this is a restart.
132 | ok
133 | end,
134 | gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
135 |
136 | %% @private
137 | init([]) ->
138 | {ok, undefined}.
139 |
140 | %% @private
141 | handle_call({remove_resource, Name, Resource}, _From, State) ->
142 | DL = filter_by_resource(Resource, get_dispatch_list(Name)),
143 | {reply, set_dispatch_list(Name, DL), State};
144 |
145 | handle_call({remove_route, Name, Route}, _From, State) ->
146 | DL = [D || D <- get_dispatch_list(Name),
147 | D /= Route],
148 | {reply, set_dispatch_list(Name, DL), State};
149 |
150 | handle_call({add_route, Name, Route}, _From, State) ->
151 | DL = [Route|[D || D <- get_dispatch_list(Name),
152 | D /= Route]],
153 | {reply, set_dispatch_list(Name, DL), State};
154 |
155 | handle_call({init_routes, Name, DefaultRoutes}, _From, State) ->
156 | %% if the table lacks a dispatch_list row, set it
157 | ets:insert_new(?MODULE, {Name, DefaultRoutes}),
158 | {reply, ok, State};
159 |
160 | handle_call(_Request, _From, State) ->
161 | {reply, ignore, State}.
162 |
163 | %% @private
164 | handle_cast(_Msg, State) ->
165 | {noreply, State}.
166 |
167 | %% @private
168 | handle_info(_Info, State) ->
169 | {noreply, State}.
170 |
171 | %% @private
172 | terminate(_Reason, _State) ->
173 | ok.
174 |
175 | %% @private
176 | code_change(_OldVsn, State, _Extra) ->
177 | {ok, State}.
178 |
179 | %% Internal functions
180 |
181 | %% @doc Remove any dispatch rule that directs requests to `Resource'
182 | filter_by_resource(Resource, Dispatch) ->
183 | lists:foldr(filter_by_resource(Resource), [], Dispatch).
184 |
185 | filter_by_resource(Resource) ->
186 | fun({_, R, _}, Acc) when R == Resource -> % basic dispatch
187 | Acc;
188 | ({_, _, R, _}, Acc) when R == Resource -> % guarded dispatch
189 | Acc;
190 | ({Host, Disp}, Acc) -> % host-based dispatch
191 | [{Host, filter_by_resource(Resource, Disp)}|Acc];
192 | (Other, Acc) -> % dispatch not mentioning this resource
193 | [Other|Acc]
194 | end.
195 |
196 | get_dispatch_list(Name) ->
197 | case ets:lookup(?MODULE, Name) of
198 | [{Name, Dispatch}] ->
199 | Dispatch;
200 | [] ->
201 | []
202 | end.
203 |
204 | set_dispatch_list(Name, DispatchList) ->
205 | true = ets:insert(?MODULE, {Name, DispatchList}),
206 | ok.
207 |
208 | %%
209 | %% Tests
210 | %%
211 | -ifdef(TEST).
212 | -include_lib("eunit/include/eunit.hrl").
213 |
214 | webmachine_router_test_() ->
215 | {setup,
216 | fun() ->
217 | {ok, Pid} = webmachine_router:start_link(),
218 | unlink(Pid),
219 | {Pid}
220 | end,
221 | fun({Pid}) ->
222 | exit(Pid, kill),
223 | wait_for_termination(webmachine_router),
224 | %% the test process owns the table, so we clear it between tests
225 | ets:delete(?MODULE)
226 | end,
227 | [{"add_remove_route", fun add_remove_route/0},
228 | {"add_remove_resource", fun add_remove_resource/0},
229 | {"no_dupe_path", fun no_dupe_path/0}
230 | ]}.
231 |
232 | %% Wait until the given registered name cannot be found, to ensure that
233 | %% another test can safely start it again via start_link
234 | wait_for_termination(RegName) ->
235 | IdOrUndefined = whereis(RegName),
236 | case IdOrUndefined of
237 | undefined ->
238 | ok;
239 | _ ->
240 | timer:sleep(100),
241 | wait_for_termination(RegName)
242 | end.
243 |
244 | add_remove_route() ->
245 | PathSpec = {["foo"], foo, []},
246 | webmachine_router:add_route(PathSpec),
247 | ?assertEqual([PathSpec], get_routes()),
248 | webmachine_router:remove_route(PathSpec),
249 | ?assertEqual([], get_routes()),
250 | ok.
251 |
252 | add_remove_resource() ->
253 | PathSpec1 = {["foo"], foo, []},
254 | PathSpec2 = {["bar"], foo, []},
255 | PathSpec3 = {["baz"], bar, []},
256 | PathSpec4 = {["foo"], fun(_) -> true end, foo, []},
257 | PathSpec5 = {["foo"], {webmachine_router, test_guard}, foo, []},
258 | webmachine_router:add_route(PathSpec1),
259 | webmachine_router:add_route(PathSpec2),
260 | webmachine_router:add_route(PathSpec3),
261 | webmachine_router:remove_resource(foo),
262 | ?assertEqual([PathSpec3], get_routes()),
263 | webmachine_router:add_route(PathSpec4),
264 | webmachine_router:remove_resource(foo),
265 | ?assertEqual([PathSpec3], get_routes()),
266 | webmachine_router:add_route(PathSpec5),
267 | webmachine_router:remove_resource(foo),
268 | ?assertEqual([PathSpec3], get_routes()),
269 | webmachine_router:remove_route(PathSpec3),
270 | [begin
271 | PathSpec = {"localhost", [HostPath]},
272 | webmachine_router:add_route(PathSpec),
273 | webmachine_router:remove_resource(foo),
274 | ?assertEqual([{"localhost", []}], get_routes()),
275 | webmachine_router:remove_route({"localhost", []})
276 | end || HostPath <- [PathSpec1, PathSpec4, PathSpec5]],
277 | ok.
278 |
279 | no_dupe_path() ->
280 | PathSpec = {["foo"], foo, []},
281 | webmachine_router:add_route(PathSpec),
282 | webmachine_router:add_route(PathSpec),
283 | ?assertEqual([PathSpec], get_routes()),
284 | ok.
285 |
286 | supervisor_restart_keeps_routes_test() ->
287 | {ok, Pid} = webmachine_router:start_link(),
288 | unlink(Pid),
289 | PathSpec = {["foo"], foo, []},
290 | webmachine_router:add_route(PathSpec),
291 | ?assertEqual([PathSpec], get_routes()),
292 | OldRouter = whereis(webmachine_router),
293 | ?assertEqual(Pid, OldRouter),
294 | exit(whereis(webmachine_router), kill),
295 | timer:sleep(100),
296 | %% Note: This test is currently broken and wasn't actually testing what it
297 | %% was supposed to
298 | NewRouter = undefined,
299 | ?assert(OldRouter /= NewRouter),
300 | ?assertEqual([PathSpec], get_routes()),
301 | exit(Pid, kill),
302 | ets:delete(?MODULE),
303 | ok.
304 |
305 | -endif.
306 |
--------------------------------------------------------------------------------
/src/webmachine_sup.erl:
--------------------------------------------------------------------------------
1 | %% @author Justin Sheehy
2 | %% @author Andy Gross
3 | %% @copyright 2007-2014 Basho Technologies
4 | %%
5 | %% Licensed under the Apache License, Version 2.0 (the "License");
6 | %% you may not use this file except in compliance with the License.
7 | %% You may obtain a copy of the License at
8 | %%
9 | %% http://www.apache.org/licenses/LICENSE-2.0
10 | %%
11 | %% Unless required by applicable law or agreed to in writing, software
12 | %% distributed under the License is distributed on an "AS IS" BASIS,
13 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | %% See the License for the specific language governing permissions and
15 | %% limitations under the License.
16 |
17 | %% @doc Supervisor for the webmachine application.
18 |
19 | -module(webmachine_sup).
20 |
21 | -behaviour(supervisor).
22 |
23 | %% External exports
24 | -export([start_link/0, upgrade/0]).
25 |
26 | %% supervisor callbacks
27 | -export([init/1]).
28 |
29 | -include("webmachine_logger.hrl").
30 |
31 | %% @spec start_link() -> ServerRet
32 | %% @doc API for starting the supervisor.
33 | start_link() ->
34 | supervisor:start_link({local, ?MODULE}, ?MODULE, []).
35 |
36 | %% @spec upgrade() -> ok
37 | %% @doc Add processes if necessary.
38 | upgrade() ->
39 | {ok, {_, Specs}} = init([]),
40 |
41 | Old = sets:from_list(
42 | [Name || {Name, _, _, _} <- supervisor:which_children(?MODULE)]),
43 | New = sets:from_list([Name || {Name, _, _, _, _, _} <- Specs]),
44 | Kill = sets:subtract(Old, New),
45 |
46 | sets:fold(fun (Id, ok) ->
47 | _ = supervisor:terminate_child(?MODULE, Id),
48 | _ = supervisor:delete_child(?MODULE, Id),
49 | ok
50 | end, ok, Kill),
51 |
52 | _ = [supervisor:start_child(?MODULE, Spec) || Spec <- Specs],
53 | ok.
54 |
55 | %% @spec init([]) -> SupervisorTree
56 | %% @doc supervisor callback.
57 | init([]) ->
58 | Router =
59 | {webmachine_router,
60 | {webmachine_router, start_link, []},
61 | permanent, 5000, worker, [webmachine_router]},
62 | LogHandler =
63 | [{webmachine_logger,
64 | {gen_event, start_link, [{local, ?EVENT_LOGGER}]},
65 | permanent, 5000, worker, [dynamic]},
66 | {webmachine_logger_watcher_sup,
67 | {webmachine_logger_watcher_sup, start_link, []},
68 | permanent, 5000, supervisor, [webmachine_logger_watcher_sup]}],
69 | {ok, {{one_for_one, 9, 10}, LogHandler ++ [Router]}}.
70 |
--------------------------------------------------------------------------------
/src/webmachine_util.erl:
--------------------------------------------------------------------------------
1 | %% @author Justin Sheehy
2 | %% @author Andy Gross
3 | %% @copyright 2007-2008 Basho Technologies
4 | %% (guess_mime/1 derived from code copyright 2007 Mochi Media, Inc.)
5 | %%
6 | %% Licensed under the Apache License, Version 2.0 (the "License");
7 | %% you may not use this file except in compliance with the License.
8 | %% You may obtain a copy of the License at
9 | %%
10 | %% http://www.apache.org/licenses/LICENSE-2.0
11 | %%
12 | %% Unless required by applicable law or agreed to in writing, software
13 | %% distributed under the License is distributed on an "AS IS" BASIS,
14 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | %% See the License for the specific language governing permissions and
16 | %% limitations under the License.
17 |
18 | %% @doc Utilities for parsing, quoting, and negotiation.
19 |
20 | -module(webmachine_util).
21 | -export([guess_mime/1]).
22 | -export([convert_request_date/1, compare_ims_dates/2]).
23 | -export([rfc1123_date/1]).
24 | -export([choose_media_type/2, format_content_type/1]).
25 | -export([choose_charset/2]).
26 | -export([choose_encoding/2]).
27 | -export([now_diff_milliseconds/2]).
28 | -export([media_type_to_detail/1,
29 | quoted_string/1,
30 | split_quoted_strings/1]).
31 | -export([parse_range/2]).
32 | -export([ensure_all_started/1]).
33 |
34 | -ifdef(TEST).
35 | -ifdef(EQC).
36 | -include_lib("eqc/include/eqc.hrl").
37 | -endif.
38 | -include_lib("eunit/include/eunit.hrl").
39 | -export([accept_header_to_media_types/1]).
40 | -endif.
41 |
42 | convert_request_date(Date) ->
43 | try
44 | case httpd_util:convert_request_date(Date) of
45 | ReqDate -> ReqDate
46 | end
47 | catch
48 | error:_ -> bad_date
49 | end.
50 |
51 | %% returns true if D1 > D2
52 | compare_ims_dates(D1, D2) ->
53 | GD1 = calendar:datetime_to_gregorian_seconds(D1),
54 | GD2 = calendar:datetime_to_gregorian_seconds(D2),
55 | GD1 > GD2.
56 |
57 | %% @doc Convert tuple style GMT datetime to RFC1123 style one
58 | rfc1123_date({{YYYY, MM, DD}, {Hour, Min, Sec}}) ->
59 | DayNumber = calendar:day_of_the_week({YYYY, MM, DD}),
60 | lists:flatten(io_lib:format("~s, ~2.2.0w ~3.s ~4.4.0w ~2.2.0w:~2.2.0w:~2.2.0w GMT",
61 | [httpd_util:day(DayNumber), DD, httpd_util:month(MM),
62 | YYYY, Hour, Min, Sec])).
63 |
64 | %% @spec guess_mime(string()) -> string()
65 | %% @doc Guess the mime type of a file by the extension of its filename.
66 | guess_mime(File) ->
67 | case filename:extension(File) of
68 | ".bz2" ->
69 | "application/x-bzip2";
70 | ".css" ->
71 | "text/css";
72 | ".eot" ->
73 | "application/vnd.ms-fontobject";
74 | ".gif" ->
75 | "image/gif";
76 | ".gz" ->
77 | "application/x-gzip";
78 | ".htc" ->
79 | "text/x-component";
80 | ".html" ->
81 | "text/html";
82 | ".ico" ->
83 | "image/x-icon";
84 | ".jpeg" ->
85 | "image/jpeg";
86 | ".jpg" ->
87 | "image/jpeg";
88 | ".js" ->
89 | "application/x-javascript";
90 | ".less" ->
91 | "text/css";
92 | ".m4v" ->
93 | "video/mp4";
94 | ".manifest" ->
95 | "text/cache-manifest";
96 | ".mp4" ->
97 | "video/mp4";
98 | ".oga" ->
99 | "audio/ogg";
100 | ".ogg" ->
101 | "audio/ogg";
102 | ".ogv" ->
103 | "video/ogg";
104 | ".otf" ->
105 | "font/opentyp";
106 | ".png" ->
107 | "image/png";
108 | ".svg" ->
109 | "image/svg+xml";
110 | ".svgz" ->
111 | "image/svg+xml";
112 | ".swf" ->
113 | "application/x-shockwave-flash";
114 | ".tar" ->
115 | "application/x-tar";
116 | ".tgz" ->
117 | "application/x-gzip";
118 | ".ttc" ->
119 | "application/x-font-ttf";
120 | ".ttf" ->
121 | "application/x-font-ttf";
122 | ".vcf" ->
123 | "text/x-vcard";
124 | ".webm" ->
125 | "video/web";
126 | ".webp" ->
127 | "image/web";
128 | ".woff" ->
129 | "application/x-font-woff";
130 | ".xhtml" ->
131 | "application/xhtml+xml";
132 | ".xml" ->
133 | "application/xml";
134 | ".zip" ->
135 | "application/zip";
136 | _ ->
137 | "text/plain"
138 | end.
139 |
140 | choose_media_type(Provided,AcceptHead) ->
141 | % Return the Content-Type we will serve for a request.
142 | % If there is no acceptable/available match, return the atom "none".
143 | % AcceptHead is the value of the request's Accept header
144 | % Provided is a list of media types the resource can provide.
145 | % each is either a string e.g. -- "text/html"
146 | % or a string and parameters e.g. -- {"text/html",[{level,1}]}
147 | % (the plain string case with no parameters is much more common)
148 | Requested = accept_header_to_media_types(AcceptHead),
149 | Prov1 = normalize_provided(Provided),
150 | choose_media_type1(Prov1,Requested).
151 | choose_media_type1(_Provided,[]) ->
152 | none;
153 | choose_media_type1(Provided,[H|T]) ->
154 | {_Pri,Type,Params} = H,
155 | case media_match({Type,Params}, Provided) of
156 | [] -> choose_media_type1(Provided,T);
157 | [{CT_T,CT_P}|_] -> format_content_type(CT_T,CT_P)
158 | end.
159 |
160 | media_match(_,[]) -> [];
161 | media_match({"*/*",[]},[H|_]) -> [H];
162 | media_match({Type,Params},Provided) ->
163 | [{T1,P1} || {T1,P1} <- Provided,
164 | media_type_match(Type,T1), media_params_match(Params,P1)].
165 | media_type_match(Req,Prov) ->
166 | case Req of
167 | "*" -> % might as well not break for lame (Gomez) clients
168 | true;
169 | "*/*" ->
170 | true;
171 | Prov ->
172 | true;
173 | _ ->
174 | [R1|R2] = string:tokens(Req,"/"),
175 | [P1,_P2] = string:tokens(Prov,"/"),
176 | case R2 of
177 | ["*"] ->
178 | case R1 of
179 | P1 -> true;
180 | _ -> false
181 | end;
182 | _ -> false
183 | end
184 | end.
185 | media_params_match(Req,Prov) ->
186 | lists:sort(Req) =:= lists:sort(Prov).
187 |
188 | prioritize_media(TyParam) ->
189 | {Type, Params} = TyParam,
190 | prioritize_media(Type,Params,[]).
191 | prioritize_media(Type,Params,Acc) ->
192 | case Params of
193 | [] ->
194 | {1, Type, Acc};
195 | _ ->
196 | [{Tok,Val}|Rest] = Params,
197 | case Tok of
198 | "q" ->
199 | QVal = case Val of
200 | "1" ->
201 | 1;
202 | "0" ->
203 | 0;
204 | [$.|_] ->
205 | %% handle strange FeedBurner Accept
206 | list_to_float([$0|Val]);
207 | _ -> list_to_float(Val)
208 | end,
209 | {QVal, Type, Rest ++ Acc};
210 | _ ->
211 | prioritize_media(Type,Rest,[{Tok,Val}|Acc])
212 | end
213 | end.
214 |
215 | media_type_to_detail(MType) ->
216 | mochiweb_util:parse_header(MType).
217 |
218 | accept_header_to_media_types(HeadVal) ->
219 | % given the value of an accept header, produce an ordered list
220 | % based on the q-values. Results are [{Type,Params}] with the
221 | % head of the list being the highest-priority requested type.
222 | try
223 | lists:reverse(lists:keysort(1,
224 | [prioritize_media(media_type_to_detail(MType)) ||
225 | MType <- [string:strip(X) || X <- string:tokens(HeadVal, ",")]]))
226 | catch _:_ -> []
227 | end.
228 |
229 | normalize_provided(Provided) ->
230 | [normalize_provided1(X) || X <- Provided].
231 | normalize_provided1(Type) when is_list(Type) -> {Type, []};
232 | normalize_provided1({Type,Params}) -> {Type, normalize_media_params(Params)}.
233 |
234 | normalize_media_params(Params) ->
235 | normalize_media_params(Params,[]).
236 |
237 | normalize_media_params([],Acc) ->
238 | Acc;
239 | normalize_media_params([{K,V}|T], Acc) when is_atom(K) ->
240 | normalize_media_params(T,[{atom_to_list(K),V}|Acc]);
241 | normalize_media_params([H|T], Acc) ->
242 | normalize_media_params(T, [H|Acc]).
243 |
244 |
245 | format_content_type(Type) when is_list(Type) ->
246 | Type;
247 | format_content_type({Type,Params}) ->
248 | format_content_type(Type,Params).
249 |
250 | format_content_type(Type,[]) -> Type;
251 | format_content_type(Type,[{K,V}|T]) when is_atom(K) ->
252 | format_content_type(Type, [{atom_to_list(K),V}|T]);
253 | format_content_type(Type,[{K,V}|T]) ->
254 | format_content_type(Type ++ "; " ++ K ++ "=" ++ V, T).
255 |
256 | choose_charset(CSets, AccCharHdr) -> do_choose(CSets, AccCharHdr, "ISO-8859-1").
257 |
258 | choose_encoding(Encs, AccEncHdr) -> do_choose(Encs, AccEncHdr, "identity").
259 |
260 | do_choose(Choices, Header, Default) ->
261 | Accepted = build_conneg_list(string:tokens(Header, ",")),
262 | DefaultPrio = [P || {P,C} <- Accepted, C =:= Default],
263 | StarPrio = [P || {P,C} <- Accepted, C =:= "*"],
264 | DefaultOkay = case DefaultPrio of
265 | [] ->
266 | case StarPrio of
267 | [0.0] -> no;
268 | _ -> yes
269 | end;
270 | [0.0] -> no;
271 | _ -> yes
272 | end,
273 | AnyOkay = case StarPrio of
274 | [] -> no;
275 | [0.0] -> no;
276 | _ -> yes
277 | end,
278 | do_choose(Default, DefaultOkay, AnyOkay, Choices, Accepted).
279 | do_choose(_Default, _DefaultOkay, _AnyOkay, [], []) ->
280 | none;
281 | do_choose(_Default, _DefaultOkay, _AnyOkay, [], _Accepted) ->
282 | none;
283 | do_choose(Default, DefaultOkay, AnyOkay, Choices, []) ->
284 | case AnyOkay of
285 | yes -> hd(Choices);
286 | no ->
287 | case DefaultOkay of
288 | yes ->
289 | case lists:member(Default, Choices) of
290 | true -> Default;
291 | _ -> none
292 | end;
293 | no -> none
294 | end
295 | end;
296 | do_choose(Default, DefaultOkay, AnyOkay, Choices, [AccPair|AccRest]) ->
297 | {Prio, Acc} = AccPair,
298 | case Prio of
299 | 0.0 ->
300 | do_choose(Default, DefaultOkay, AnyOkay,
301 | lists:delete(Acc, Choices), AccRest);
302 | _ ->
303 | LAcc = string:to_lower(Acc),
304 | LChoices = [string:to_lower(X) || X <- Choices],
305 | % doing this a little more work than needed in
306 | % order to be easily insensitive but preserving
307 | case lists:member(LAcc, LChoices) of
308 | true ->
309 | hd([X || X <- Choices,
310 | string:to_lower(X) =:= LAcc]);
311 | false -> do_choose(Default, DefaultOkay, AnyOkay,
312 | Choices, AccRest)
313 | end
314 | end.
315 |
316 | build_conneg_list(AccList) ->
317 | build_conneg_list(AccList, []).
318 | build_conneg_list([], Result) -> lists:reverse(lists:sort(Result));
319 | build_conneg_list([Acc|AccRest], Result) ->
320 | XPair = list_to_tuple([string:strip(X) || X <- string:tokens(Acc, ";")]),
321 | Pair = case XPair of
322 | {Choice, "q=" ++ PrioStr} ->
323 | case PrioStr of
324 | "0" -> {0.0, Choice};
325 | "1" -> {1.0, Choice};
326 | [$.|_] ->
327 | %% handle strange FeedBurner Accept
328 | {list_to_float([$0|PrioStr]), Choice};
329 | _ -> {list_to_float(PrioStr), Choice}
330 | end;
331 | {Choice} ->
332 | {1.0, Choice}
333 | end,
334 | build_conneg_list(AccRest,[Pair|Result]).
335 |
336 |
337 | quoted_string([$" | _Rest] = Str) ->
338 | Str;
339 | quoted_string(Str) ->
340 | escape_quotes(Str, [$"]). % Initialize Acc with opening quote
341 |
342 | escape_quotes([], Acc) ->
343 | lists:reverse([$" | Acc]); % Append final quote
344 | escape_quotes([$\\, Char | Rest], Acc) ->
345 | escape_quotes(Rest, [Char, $\\ | Acc]); % Any quoted char should be skipped
346 | escape_quotes([$" | Rest], Acc) ->
347 | escape_quotes(Rest, [$", $\\ | Acc]); % Unquoted quotes should be escaped
348 | escape_quotes([Char | Rest], Acc) ->
349 | escape_quotes(Rest, [Char | Acc]).
350 |
351 | split_quoted_strings(Str) ->
352 | split_quoted_strings(Str, []).
353 |
354 | split_quoted_strings([], Acc) ->
355 | lists:reverse(Acc);
356 | split_quoted_strings([$" | Rest], Acc) ->
357 | {Str, Cont} = unescape_quoted_string(Rest, []),
358 | split_quoted_strings(Cont, [Str | Acc]);
359 | split_quoted_strings([_Skip | Rest], Acc) ->
360 | split_quoted_strings(Rest, Acc).
361 |
362 | unescape_quoted_string([], Acc) ->
363 | {lists:reverse(Acc), []};
364 | unescape_quoted_string([$\\, Char | Rest], Acc) -> % Any quoted char should be unquoted
365 | unescape_quoted_string(Rest, [Char | Acc]);
366 | unescape_quoted_string([$" | Rest], Acc) -> % Quote indicates end of this string
367 | {lists:reverse(Acc), Rest};
368 | unescape_quoted_string([Char | Rest], Acc) ->
369 | unescape_quoted_string(Rest, [Char | Acc]).
370 |
371 |
372 | %% @type now() = {MegaSecs, Secs, MicroSecs}
373 |
374 | %% This is faster than timer:now_diff() because it does not use bignums.
375 | %% But it returns *milliseconds* (timer:now_diff returns microseconds.)
376 | %% From http://www.erlang.org/ml-archive/erlang-questions/200205/msg00027.html
377 |
378 | %% @doc Compute the difference between two now() tuples, in milliseconds.
379 | %% @spec now_diff_milliseconds(now(), now()) -> integer()
380 | now_diff_milliseconds(undefined, undefined) ->
381 | 0;
382 | now_diff_milliseconds(undefined, T2) ->
383 | now_diff_milliseconds(os:timestamp(), T2);
384 | now_diff_milliseconds({M,S,U}, {M,S1,U1}) ->
385 | ((S-S1) * 1000) + ((U-U1) div 1000);
386 | now_diff_milliseconds({M,S,U}, {M1,S1,U1}) ->
387 | ((M-M1)*1000000+(S-S1))*1000 + ((U-U1) div 1000).
388 |
389 | -spec parse_range(RawRange::string(), ResourceLength::non_neg_integer()) ->
390 | [{Start::non_neg_integer(), End::non_neg_integer()}].
391 | parse_range(RawRange, ResourceLength) when is_list(RawRange) ->
392 | parse_range(mochiweb_http:parse_range_request(RawRange), ResourceLength, []).
393 |
394 | parse_range(fail, _ResourceLength, _Acc) ->
395 | [];
396 | parse_range([], _ResourceLength, Acc) ->
397 | lists:reverse(Acc);
398 | parse_range([Spec | Rest], ResourceLength, Acc) ->
399 | case mochiweb_http:range_skip_length(Spec, ResourceLength) of
400 | invalid_range ->
401 | parse_range(Rest, ResourceLength, Acc);
402 | {Skip, Length} ->
403 | parse_range(Rest, ResourceLength, [{Skip, Skip + Length - 1} | Acc])
404 | end.
405 |
406 | %% On older versions of Erlang, we don't have application:ensure_all_started,
407 | %% so we use this wrapper function to either use the native implementation or
408 | %% our own version, depending on what's available.
409 | -spec ensure_all_started(atom()) -> {ok, [atom()]} | {error, term()}.
410 | ensure_all_started(App) ->
411 | case erlang:function_exported(application, ensure_all_started, 1) of
412 | true ->
413 | application:ensure_all_started(App);
414 | false ->
415 | ensure_all_started(App, [])
416 | end.
417 |
418 | %% Reimplementation of ensure_all_started. NOTE this does not behave the same
419 | %% as the native version in all cases, but as a quick hack it works well
420 | %% enough for our purposes. Eventually I assume we'll drop support for older
421 | %% versions of Erlang and this can be eliminated.
422 | ensure_all_started(App, Apps0) ->
423 | case application:start(App) of
424 | ok ->
425 | {ok, lists:reverse([App | Apps0])};
426 | {error,{already_started,App}} ->
427 | {ok, lists:reverse(Apps0)};
428 | {error,{not_started,BaseApp}} ->
429 | {ok, Apps} = ensure_all_started(BaseApp, Apps0),
430 | ensure_all_started(App, [BaseApp|Apps])
431 | end.
432 |
433 | %%
434 | %% TEST
435 | %%
436 | -ifdef(TEST).
437 |
438 | choose_media_type_test() ->
439 | Provided = "text/html",
440 | ShouldMatch = ["*", "*/*", "text/*", "text/html"],
441 | WantNone = ["foo", "text/xml", "application/*", "foo/bar/baz"],
442 | [ ?assertEqual(Provided, choose_media_type([Provided], I))
443 | || I <- ShouldMatch ],
444 | [ ?assertEqual(none, choose_media_type([Provided], I))
445 | || I <- WantNone ].
446 |
447 | choose_media_type_qval_test() ->
448 | Provided = ["text/html", "image/jpeg"],
449 | HtmlMatch = ["image/jpeg;q=0.5, text/html",
450 | "text/html, image/jpeg; q=0.5",
451 | "text/*; q=0.8, image/*;q=0.7",
452 | "text/*;q=.8, image/*;q=.7"], %% strange FeedBurner format
453 | JpgMatch = ["image/*;q=1, text/html;q=0.9",
454 | "image/png, image/*;q=0.3"],
455 | [ ?assertEqual("text/html", choose_media_type(Provided, I))
456 | || I <- HtmlMatch ],
457 | [ ?assertEqual("image/jpeg", choose_media_type(Provided, I))
458 | || I <- JpgMatch ].
459 |
460 | accept_header_to_media_types_test() ->
461 | Header1 = "text/html,application/xhtml+xml,application/xml,application/x-javascript,*/*;q=0.5",
462 | Header2 = "audio/*; q=0, audio/basic",
463 | OddHeader = "text/html,application/xhtml+xml,application/xml,application/x-javascript,*/*;q=0,5",
464 | Result1 = accept_header_to_media_types(Header1),
465 | Result2 = accept_header_to_media_types(Header2),
466 | Result3 = accept_header_to_media_types(OddHeader),
467 | ExpResult1 = [{1,"application/x-javascript", []},
468 | {1,"application/xml",[]},
469 | {1,"application/xhtml+xml",[]},
470 | {1,"text/html",[]},
471 | {0.5,"*/*",[]}],
472 | ExpResult2 = [{1,"audio/basic",[]},{0,"audio/*",[]}],
473 | ExpResult3 = [{1, "5", []},
474 | {1,"application/x-javascript", []},
475 | {1,"application/xml",[]},
476 | {1,"application/xhtml+xml",[]},
477 | {1,"text/html",[]},
478 | {0,"*/*",[]}],
479 | ?assertEqual(ExpResult1, Result1),
480 | ?assertEqual(ExpResult2, Result2),
481 | ?assertEqual(ExpResult3, Result3).
482 |
483 | media_type_extra_whitespace_test() ->
484 | MType = "application/x-www-form-urlencoded ; charset = utf8",
485 | ?assertEqual({"application/x-www-form-urlencoded",[{"charset","utf8"}]},
486 | webmachine_util:media_type_to_detail(MType)).
487 |
488 | format_content_type_test() ->
489 | Types = ["audio/vnd.wave; codec=31",
490 | "text/x-okie; charset=iso-8859-1; declaration="],
491 | [?assertEqual(Type, format_content_type(
492 | webmachine_util:media_type_to_detail(Type)))
493 | || Type <- Types],
494 | ?assertEqual(hd(Types), format_content_type("audio/vnd.wave", [{codec, "31"}])).
495 |
496 | convert_request_date_test() ->
497 | ?assertMatch({{_,_,_},{_,_,_}},
498 | convert_request_date("Wed, 30 Dec 2009 14:39:02 GMT")),
499 | ?assertMatch(bad_date,
500 | convert_request_date(<<"does not handle binaries">>)).
501 |
502 | compare_ims_dates_test() ->
503 | Late = {{2009,12,30},{14,39,02}},
504 | Early = {{2009,12,30},{13,39,02}},
505 | ?assertEqual(true, compare_ims_dates(Late, Early)),
506 | ?assertEqual(false, compare_ims_dates(Early, Late)).
507 |
508 | rfc1123_date_test() ->
509 | ?assertEqual("Thu, 11 Jul 2013 04:33:19 GMT",
510 | rfc1123_date({{2013, 7, 11}, {4, 33, 19}})).
511 |
512 | guess_mime_test() ->
513 | TextTypes = [".html",".css",".htc",".manifest",".txt"],
514 | AppTypes = [".xhtml",".xml",".js",".swf",".zip",".bz2",
515 | ".gz",".tar",".tgz"],
516 | ImgTypes = [".jpg",".jpeg",".gif",".png",".ico",".svg"],
517 | ?assertEqual([], [ T || T <- TextTypes,
518 | 1 /= string:str(guess_mime("file" ++ T),"text/") ]),
519 | ?assertEqual([], [ T || T <- AppTypes,
520 | 1 /= string:str(guess_mime("file" ++ T),"application/") ]),
521 | ?assertEqual([], [ T || T <- ImgTypes,
522 | 1 /= string:str(guess_mime("file" ++ T),"image/") ]).
523 |
524 |
525 | now_diff_milliseconds_test() ->
526 | Late = {10, 10, 10},
527 | Early1 = {10, 9, 9},
528 | Early2 = {9, 9, 9},
529 | ?assertEqual(1000, now_diff_milliseconds(Late, Early1)),
530 | ?assertEqual(1000001000, now_diff_milliseconds(Late, Early2)).
531 |
532 | parse_range_test() ->
533 | ValidRange = "bytes=1-2",
534 | InvalidRange = "bytes=2-1",
535 | EmptyRange = "bytes=",
536 | UnparsableRange = "bytes=foo",
537 | ?assertEqual([{1,2}], parse_range(ValidRange, 10)),
538 | ?assertEqual([], parse_range(InvalidRange, 10)),
539 | ?assertEqual([], parse_range(EmptyRange, 10)),
540 | ?assertEqual([], parse_range(UnparsableRange, 10)).
541 |
542 | -ifdef(EQC).
543 |
544 | -define(QC_OUT(P),
545 | eqc:on_output(fun(Str, Args) -> io:format(user, Str, Args) end, P)).
546 |
547 | prop_quoted_string() ->
548 | ?FORALL(String0, non_empty(list(oneof([char(), $", [$\\, char()]]))),
549 | begin
550 | String = lists:flatten(String0),
551 |
552 | Quoted = quoted_string(String),
553 | case String of
554 | [$" | _] ->
555 | ?assertEqual(String, Quoted),
556 | true;
557 | _ ->
558 | %% Properties:
559 | %% * strings must begin/end with quote
560 | %% * All other quotes should be escaped
561 | ?assertEqual($", hd(Quoted)),
562 | ?assertEqual($", lists:last(Quoted)),
563 | Partial = lists:reverse(tl(lists:reverse(tl(Quoted)))),
564 | case check_quote(Partial) of
565 | true ->
566 | true;
567 | false ->
568 | io:format(user, "----\n", []),
569 | io:format(user, "In: ~p\n", [[integer_to_list(C) || C <- String]]),
570 | io:format(user, "Out: ~p\n", [[integer_to_list(C) || C <- Quoted]]),
571 | false
572 | end
573 | end
574 | end).
575 |
576 | check_quote([]) ->
577 | true;
578 | check_quote([$\\, _Any | Rest]) ->
579 | check_quote(Rest);
580 | check_quote([$" | _Rest]) ->
581 | false;
582 | check_quote([_Any | Rest]) ->
583 | check_quote(Rest).
584 |
585 | prop_quoted_string_test() ->
586 | ?assert(eqc:quickcheck(?QC_OUT(prop_quoted_string()))).
587 |
588 | -endif. % EQC
589 | -endif. % TEST
590 |
--------------------------------------------------------------------------------
/src/wmtrace_resource.erl:
--------------------------------------------------------------------------------
1 | %% @author Bryan Fink
2 | %% @doc Webmachine trace file interpretter.
3 | -module(wmtrace_resource).
4 |
5 | -export([add_dispatch_rule/2,
6 | remove_dispatch_rules/0]).
7 |
8 | -export([init/1,
9 | resource_exists/2,
10 | content_types_provided/2,
11 | produce_html/2,
12 | produce_javascript/2,
13 | produce_map/2,
14 | produce_css/2]).
15 |
16 | -include("wm_reqdata.hrl").
17 |
18 | -record(ctx, {trace_dir, trace}).
19 |
20 | -define(MAP_EXTERNAL, "static/map.png").
21 | -define(MAP_INTERNAL, "http-headers-status-v3.png").
22 | -define(SCRIPT_EXTERNAL, "static/wmtrace.js").
23 | -define(SCRIPT_INTERNAL, "wmtrace.js").
24 | -define(STYLE_EXTERNAL, "static/wmtrace.css").
25 | -define(STYLE_INTERNAL, "wmtrace.css").
26 |
27 | %%
28 | %% Dispatch Modifiers
29 | %%
30 |
31 | %% @spec add_dispatch_rule(string(), string()) -> ok
32 | %% @doc Add a dispatch rule to point at wmtrace_resource.
33 | %% Example: to serve wmtrace_resource from
34 | %% http://yourhost/dev/wmtrace/
35 | %% with trace files on disk at
36 | %% priv/traces
37 | %% call:
38 | %% add_dispatch_rule("dev/wmtrace", "priv/traces")
39 | add_dispatch_rule(BasePath, TracePath) when is_list(BasePath),
40 | is_list(TracePath) ->
41 | Parts = string:tokens(BasePath, "/"),
42 | webmachine_router:add_route({Parts ++ ['*'], ?MODULE, [{trace_dir, TracePath}]}).
43 |
44 | %% @spec remove_dispatch_rules() -> ok
45 | %% @doc Remove all dispatch rules pointing to wmtrace_resource.
46 | remove_dispatch_rules() ->
47 | webmachine_router:remove_resource(?MODULE).
48 |
49 | %%
50 | %% Resource
51 | %%
52 |
53 | init(Config) ->
54 | {trace_dir, TraceDir} = proplists:lookup(trace_dir, Config),
55 | {trace_dir_exists, true} = {trace_dir_exists, filelib:is_dir(TraceDir)},
56 | {ok, #ctx{trace_dir=TraceDir}}.
57 |
58 | resource_exists(RD, Ctx) ->
59 | case wrq:disp_path(RD) of
60 | [] ->
61 | case lists:reverse(wrq:raw_path(RD)) of
62 | [$/|_] ->
63 | {true, RD, Ctx};
64 | _ ->
65 | {{halt, 303},
66 | wrq:set_resp_header("Location",
67 | wrq:raw_path(RD)++"/",
68 | RD),
69 | Ctx}
70 | end;
71 | ?MAP_EXTERNAL ->
72 | {filelib:is_file(wm_path(?MAP_INTERNAL)), RD, Ctx};
73 | ?SCRIPT_EXTERNAL ->
74 | {filelib:is_file(wm_path(?SCRIPT_INTERNAL)), RD, Ctx};
75 | ?STYLE_EXTERNAL ->
76 | {filelib:is_file(wm_path(?STYLE_INTERNAL)), RD, Ctx};
77 | TraceName ->
78 | TracePath = filename:join([Ctx#ctx.trace_dir, TraceName]),
79 | {filelib:is_file(TracePath), RD, Ctx#ctx{trace=TracePath}}
80 | end.
81 |
82 | wm_path(File) ->
83 | filename:join([code:priv_dir(webmachine), "trace", File]).
84 |
85 | content_types_provided(RD, Ctx) ->
86 | case wrq:disp_path(RD) of
87 | ?MAP_EXTERNAL ->
88 | {[{"image/png", produce_map}], RD, Ctx};
89 | ?SCRIPT_EXTERNAL ->
90 | {[{"text/javascript", produce_javascript}], RD, Ctx};
91 | ?STYLE_EXTERNAL ->
92 | {[{"text/css", produce_css}], RD, Ctx};
93 | _ ->
94 | {[{"text/html", produce_html}], RD, Ctx}
95 | end.
96 |
97 | produce_html(RD, Ctx=#ctx{trace=undefined}) ->
98 | Dir = filename:absname(Ctx#ctx.trace_dir),
99 | Files = lists:reverse(
100 | lists:sort(
101 | filelib:fold_files(Dir,
102 | ".*\.wmtrace",
103 | false,
104 | fun(F, Acc) ->
105 | [filename:basename(F)|Acc]
106 | end,
107 | []))),
108 | {trace_list_html(Dir, Files), RD, Ctx};
109 | produce_html(RD, Ctx) ->
110 | Filename = filename:absname(Ctx#ctx.trace),
111 | {ok, Data} = file:consult(Filename),
112 | {trace_html(Filename, Data), RD, Ctx}.
113 |
114 | trace_list_html(Dir, Files) ->
115 | html([],
116 | [head([],
117 | title([], ["Webmachine Trace List for ",Dir])),
118 | body([],
119 | [h1([], ["Traces in ",Dir]),
120 | ul([],
121 | [ li([], a([{"href", F}], F)) || F <- Files ])
122 | ])
123 | ]).
124 |
125 | trace_html(Filename, Data) ->
126 | {Request, Response, Trace} = encode_trace(Data),
127 | html([],
128 | [head([],
129 | [title([],["Webmachine Trace ",Filename]),
130 | linkblock([{"rel", "stylesheet"},
131 | {"type", "text/css"},
132 | {"href", "static/wmtrace.css"}],
133 | []),
134 | script([{"type", "text/javascript"},
135 | {"src", "static/wmtrace.js"}],
136 | []),
137 | script([{"type", "text/javascript"}],
138 | mochiweb_html:escape(
139 | lists:flatten(
140 | ["var request=",Request,";\n"
141 | "var response=",Response,";\n"
142 | "var trace=",Trace,";"])))
143 | ]),
144 | body([],
145 | [divblock([{"id", "zoompanel"}],
146 | [button([{"id", "zoomout"}], ["zoom out"]),
147 | button([{"id", "zoomin"}], ["zoom in"])
148 | ]),
149 | canvas([{"id", "v3map"},
150 | {"width", "3138"},
151 | {"height", "2184"}],
152 | []),
153 | divblock([{"id", "sizetest"}], []),
154 | divblock([{"id", "preview"}],
155 | [divblock([{"id", "previewid"}],[]),
156 | ul([{"id", "previewcalls"}], [])
157 | ]),
158 | divblock([{"id", "infopanel"}],
159 | [divblock([{"id", "infocontrols"}],
160 | [divblock([{"id", "requesttab"},
161 | {"class", "selectedtab"}],"Q"),
162 | divblock([{"id", "responsetab"}], "R"),
163 | divblock([{"id", "decisiontab"}], "D")
164 | ]),
165 | divblock([{"id", "requestdetail"}],
166 | [divblock([],
167 | [span([{"id", "requestmethod"}], []),
168 | " ",
169 | span([{"id", "requestpath"}], [])]),
170 | ul([{"id", "requestheaders"}], []),
171 | divblock([{"id", "requestbody"}],
172 | [])
173 | ]),
174 | divblock([{"id", "responsedetail"}],
175 | [divblock([{"id", "responsecode"}], []),
176 | ul([{"id", "responseheaders"}], []),
177 | divblock([{"id", "responsebody"}], [])
178 | ]),
179 | divblock([{"id", "decisiondetail"}],
180 | [divblock([],
181 | ["Decision: ",
182 | select([{"id", "decisionid"}], [])
183 | ]),
184 | divblock([],
185 | ["Calls:",
186 | select([{"id", "decisioncalls"}], [])
187 | ]),
188 | divblock([], "Input:"),
189 | pre([{"id", "callinput"}], []),
190 | divblock([], "Output:"),
191 | pre([{"id", "calloutput"}], [])
192 | ])
193 | ])
194 | ])
195 | ]).
196 |
197 | produce_javascript(RD, Ctx) ->
198 | {ok, Script} = file:read_file(wm_path(?SCRIPT_INTERNAL)),
199 | {Script, RD, Ctx}.
200 |
201 | produce_map(RD, Ctx) ->
202 | {ok, Map} = file:read_file(wm_path(?MAP_INTERNAL)),
203 | {Map, RD, Ctx}.
204 |
205 | produce_css(RD, Ctx) ->
206 | {ok, Script} = file:read_file(wm_path(?STYLE_INTERNAL)),
207 | {Script, RD, Ctx}.
208 |
209 | %%
210 | %% Trace Encoding
211 | %%
212 |
213 | encode_trace(Data) ->
214 | {Request, Response, Trace} = aggregate_trace(Data),
215 | {mochijson:encode(encode_request(Request)),
216 | mochijson:encode(encode_response(Response)),
217 | mochijson:encode({array, [ encode_trace_part(P) || P <- Trace ]})}.
218 |
219 | aggregate_trace(RawTrace) ->
220 | {Request, Response, Trace} = lists:foldl(fun aggregate_trace_part/2,
221 | {undefined, 500, []},
222 | RawTrace),
223 | {Request, Response, lists:reverse(Trace)}.
224 |
225 | aggregate_trace_part({decision, Decision}, {Q, R, Acc}) ->
226 | BDN = base_decision_name(Decision),
227 | case Acc of
228 | [{BDN,_}|_] -> {Q, R, Acc}; %% subdecision (ex. v3b13b)
229 | _ ->
230 | {Q, R, [{base_decision_name(Decision), []}|Acc]}
231 | end;
232 | aggregate_trace_part({attempt, Module, Function, Args},
233 | {Q, R, [{Decision,Calls}|Acc]}) ->
234 | {maybe_extract_request(Function, Args, Q),
235 | R, [{Decision,[{Module, Function, Args, wmtrace_null}|Calls]}|Acc]};
236 | aggregate_trace_part({result, Module, Function, Result},
237 | {Q, R, [{Decision,[{Module,Function,Args,_}|Calls]}|Acc]}) ->
238 | {Q, maybe_extract_response(Function, Result, R),
239 | [{Decision,[{Module, Function, Args, Result}|Calls]}|Acc]};
240 | aggregate_trace_part({not_exported, Module, Function, Args},
241 | {Q, R, [{Decision,Calls}|Acc]}) ->
242 | {maybe_extract_request(Function, Args, Q),
243 | maybe_extract_response(Function, Args, R),
244 | [{Decision,[{Module, Function, Args, wmtrace_not_exported}|Calls]}
245 | |Acc]}.
246 |
247 | maybe_extract_request(service_available, [ReqData, _], _) ->
248 | ReqData;
249 | maybe_extract_request(_, _, R) ->
250 | R.
251 |
252 | maybe_extract_response(finish_request, [ReqData,_], _) ->
253 | ReqData;
254 | maybe_extract_response(finish_request, {_, ReqData, _}, _) ->
255 | ReqData;
256 | maybe_extract_response(_, _, R) ->
257 | R.
258 |
259 | base_decision_name(Decision) ->
260 | [$v,$3|D] = atom_to_list(Decision), %% strip 'v3'
261 | case lists:reverse(D) of
262 | [A|RD] when A >= $a, A =< $z ->
263 | lists:reverse(RD); %% strip 'b' off end of some
264 | _ ->
265 | D
266 | end.
267 |
268 | encode_request(ReqData) when is_record(ReqData, wm_reqdata) ->
269 | Method = case wrq:method(ReqData) of
270 | M when is_atom(M) -> atom_to_list(M);
271 | M -> M
272 | end,
273 | {struct, [{"method", Method},
274 | {"path", wrq:raw_path(ReqData)},
275 | {"headers", encode_headers(wrq:req_headers(ReqData))},
276 | {"body", case ReqData#wm_reqdata.req_body of
277 | undefined -> [];
278 | Body when is_atom(Body) ->
279 | atom_to_list(Body);
280 | Body -> lists:flatten(io_lib:format("~s", [Body]))
281 | end}]}.
282 |
283 | encode_response(ReqData) ->
284 | {struct, [{"code", integer_to_list(
285 | wrq:response_code(ReqData))},
286 | {"headers", encode_headers(wrq:resp_headers(ReqData))},
287 | {"body", lists:flatten(io_lib:format("~s", [wrq:resp_body(ReqData)]))}]}.
288 |
289 | encode_headers(Headers) when is_list(Headers) ->
290 | {struct, [ {N, V} || {N, V} <- Headers ]};
291 | encode_headers(Headers) ->
292 | encode_headers(mochiweb_headers:to_list(Headers)).
293 |
294 | encode_trace_part({Decision, Calls}) ->
295 | {struct, [{"d", Decision},
296 | {"calls",
297 | {array, [ {struct,
298 | [{"module", Module},
299 | {"function", Function},
300 | {"input", encode_trace_io(Input)},
301 | {"output", encode_trace_io(Output)}]}
302 | || {Module, Function, Input, Output}
303 | <- lists:reverse(Calls) ]}}]}.
304 |
305 | encode_trace_io(wmtrace_null) -> null;
306 | encode_trace_io(wmtrace_not_exported) -> "wmtrace_not_exported";
307 | encode_trace_io(Data) ->
308 | lists:flatten(io_lib:format("~p", [Data])).
309 |
310 | %%
311 | %% HTML Building
312 | %%
313 |
314 | -define(TAG(T), T(Attrs, Content) ->
315 | tag(??T, Attrs, Content)).
316 |
317 | ?TAG(head).
318 | ?TAG(script).
319 | ?TAG(title).
320 | ?TAG(body).
321 | ?TAG(h1).
322 | ?TAG(ul).
323 | ?TAG(li).
324 | ?TAG(a).
325 | ?TAG(canvas).
326 | ?TAG(select).
327 | ?TAG(pre).
328 | ?TAG(span).
329 | ?TAG(button).
330 |
331 | html(_Attrs, Content) ->
332 | [<<"">>,
333 | <<"">>,
334 | Content,
335 | <<"">>].
336 |
337 | divblock(Attrs, Content) ->
338 | tag("div", Attrs, Content). %% div is a reserved word
339 |
340 | linkblock(Attrs, Content) ->
341 | tag("link", Attrs, Content). %% link is a reserved word
342 |
343 | tag(Name, Attrs, Content) ->
344 | ["<",Name,
345 | [ [" ",K,"=\"",V,"\""] || {K, V} <- Attrs ],
346 | if Content == empty -> "/>";
347 | true ->
348 | [">",
349 | Content,
350 | "",Name,">"]
351 | end].
352 |
353 |
354 | -ifdef(TEST).
355 | -include_lib("eunit/include/eunit.hrl").
356 |
357 | non_standard_method_test() ->
358 | Method = "FOO",
359 | ReqData = wrq:create(Method, http, "/", mochiweb_headers:from_list([])),
360 | {struct, Props} = encode_request(ReqData),
361 | ?assertMatch({"method",Method}, lists:keyfind("method", 1, Props)),
362 | ok.
363 |
364 | -endif.
365 |
--------------------------------------------------------------------------------
/src/wrq.erl:
--------------------------------------------------------------------------------
1 | %% @author Justin Sheehy
2 | %% @copyright 2007-2009 Basho Technologies
3 | %%
4 | %% Licensed under the Apache License, Version 2.0 (the "License");
5 | %% you may not use this file except in compliance with the License.
6 | %% You may obtain a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing, software
11 | %% distributed under the License is distributed on an "AS IS" BASIS,
12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | %% See the License for the specific language governing permissions and
14 | %% limitations under the License.
15 |
16 | -module(wrq).
17 | -author('Justin Sheehy ').
18 |
19 | -export([create/4, create/5,load_dispatch_data/7]).
20 | -export([method/1,scheme/1,version/1,peer/1,disp_path/1,path/1,raw_path/1,
21 | path_info/1,response_code/1,req_cookie/1,req_qs/1,req_headers/1,
22 | req_body/1,stream_req_body/2,resp_redirect/1,resp_headers/1,
23 | resp_body/1,app_root/1,path_tokens/1, host_tokens/1, port/1,
24 | base_uri/1,sock/1]).
25 | -export([path_info/2,get_req_header/2,do_redirect/2,fresh_resp_headers/2,
26 | get_resp_header/2,set_resp_header/3,set_resp_headers/2,
27 | set_disp_path/2,set_req_body/2,set_resp_body/2,set_response_code/2,
28 | merge_resp_headers/2,remove_resp_header/2,
29 | append_to_resp_body/2,append_to_response_body/2, set_resp_range/2,
30 | max_recv_body/1,set_max_recv_body/2,
31 | get_cookie_value/2,get_qs_value/2,get_qs_value/3,set_peer/2,
32 | set_sock/2,add_note/3, get_notes/1]).
33 |
34 | % @type reqdata(). The opaque data type used for req/resp data structures.
35 | -include("wm_reqdata.hrl").
36 | -include("wm_reqstate.hrl").
37 | -type t() :: #wm_reqdata{}.
38 | -export_type([t/0]).
39 |
40 | -type scheme() :: http | https.
41 | -type method() ::'OPTIONS' % from erlang:decode_packet/3
42 | | 'GET'
43 | | 'HEAD'
44 | | 'POST'
45 | | 'PUT'
46 | | 'DELETE'
47 | | 'TRACE'
48 | | string()
49 | | binary ().
50 | -type version() :: {non_neg_integer(), non_neg_integer()}.
51 |
52 | -export_type([scheme/0, method/0, version/0]).
53 |
54 | -spec create(method(),
55 | version(),
56 | string(),
57 | webmachine:headers()) ->
58 | t().
59 | create(Method,Version,RawPath,Headers) ->
60 | create(Method,http,Version,RawPath,Headers).
61 | -spec create(method(),
62 | scheme(),
63 | version(),
64 | string(),
65 | webmachine:headers()) ->
66 | t().
67 | create(Method,Scheme,Version,RawPath,Headers) ->
68 | create(
69 | #wm_reqdata{
70 | method=Method,
71 | scheme=Scheme,
72 | version=Version,
73 | raw_path=RawPath,
74 | req_headers=Headers
75 | }).
76 |
77 | -spec create(t()) -> t().
78 | create(RD = #wm_reqdata{raw_path=RawPath}) ->
79 | {Path, _, _} = mochiweb_util:urlsplit_path(RawPath),
80 | Cookie = case get_req_header("cookie", RD) of
81 | undefined -> [];
82 | Value -> mochiweb_cookies:parse_cookie(Value)
83 | end,
84 | {_, QueryString, _} = mochiweb_util:urlsplit_path(RawPath),
85 | ReqQS = mochiweb_util:parse_qs(QueryString),
86 | RD#wm_reqdata{path=Path,req_cookie=Cookie,req_qs=ReqQS}.
87 |
88 | load_dispatch_data(PathInfo, HostTokens, Port, PathTokens, AppRoot,
89 | DispPath, RD) ->
90 | RD#wm_reqdata{path_info=PathInfo,host_tokens=HostTokens,
91 | port=Port,path_tokens=PathTokens,
92 | app_root=AppRoot,disp_path=DispPath}.
93 |
94 | -spec method(t()) -> method().
95 | method(_RD = #wm_reqdata{method=Method}) -> Method.
96 |
97 | -spec scheme(t()) -> scheme().
98 | scheme(_RD = #wm_reqdata{scheme=Scheme}) -> Scheme.
99 |
100 | -spec version(t()) -> version().
101 | version(_RD = #wm_reqdata{version=Version})
102 | when is_tuple(Version), size(Version) == 2,
103 | is_integer(element(1,Version)), is_integer(element(2,Version)) -> Version.
104 |
105 | peer(_RD = #wm_reqdata{peer=Peer}) when is_list(Peer) -> Peer.
106 |
107 | sock(_RD = #wm_reqdata{sock=Sock}) when is_list(Sock) -> Sock.
108 |
109 | app_root(_RD = #wm_reqdata{app_root=AR}) when is_list(AR) -> AR.
110 |
111 | -spec disp_path(t()) -> string().
112 | disp_path(_RD = #wm_reqdata{disp_path=DP}) when is_list(DP) -> DP.
113 |
114 | -spec path(t()) -> string().
115 | path(_RD = #wm_reqdata{path=Path}) when is_list(Path) -> Path.
116 |
117 | -spec raw_path(t()) -> string().
118 | raw_path(_RD = #wm_reqdata{raw_path=RawPath}) when is_list(RawPath) -> RawPath.
119 |
120 | path_info(_RD = #wm_reqdata{path_info=PathInfo}) -> PathInfo. % dict
121 |
122 | path_tokens(_RD = #wm_reqdata{path_tokens=PathT}) -> PathT. % list of strings
123 |
124 | host_tokens(_RD = #wm_reqdata{host_tokens=HostT}) -> HostT. % list of strings
125 |
126 | -spec port(t()) -> inet:port_number().
127 | port(_RD = #wm_reqdata{port=Port}) -> Port.
128 |
129 | -spec response_code(t()) -> non_neg_integer().
130 | response_code(#wm_reqdata{response_code={C,_ReasonPhrase}})
131 | when is_integer(C) -> C;
132 | response_code(_RD = #wm_reqdata{response_code=C})
133 | when is_integer(C) -> C.
134 |
135 | -spec req_cookie(t()) -> [{string(), string()}].
136 | req_cookie(_RD = #wm_reqdata{req_cookie=C}) when is_list(C) -> C.
137 |
138 | -spec req_qs(t()) -> [{string(), string()}].
139 | req_qs(_RD = #wm_reqdata{req_qs=QS}) when is_list(QS) -> QS.
140 |
141 | -spec req_headers(t()) -> webmachine:headers().
142 | req_headers(_RD = #wm_reqdata{req_headers=ReqH}) -> ReqH.
143 |
144 | req_body(_RD = #wm_reqdata{wm_state=ReqState0,max_recv_body=MRB}) ->
145 | Req = webmachine_request:new(ReqState0),
146 | {ReqResp, ReqState} = webmachine_request:req_body(MRB, Req),
147 | put(tmp_reqstate, ReqState),
148 | maybe_conflict_body(ReqResp).
149 |
150 | stream_req_body(_RD = #wm_reqdata{wm_state=ReqState0}, MaxHunk) ->
151 | Req = webmachine_request:new(ReqState0),
152 | {ReqResp, ReqState} = webmachine_request:stream_req_body(MaxHunk, Req),
153 | put(tmp_reqstate, ReqState),
154 | maybe_conflict_body(ReqResp).
155 |
156 | max_recv_body(_RD = #wm_reqdata{max_recv_body=X}) when is_integer(X) -> X.
157 |
158 | set_max_recv_body(X, RD) when is_integer(X) -> RD#wm_reqdata{max_recv_body=X}.
159 |
160 | maybe_conflict_body(BodyResponse) ->
161 | case BodyResponse of
162 | stream_conflict ->
163 | exit("wrq:req_body and wrq:stream_req_body conflict");
164 | {error, req_body_too_large} ->
165 | exit("request body too large");
166 | _ ->
167 | BodyResponse
168 | end.
169 |
170 | -spec resp_redirect(t()) -> boolean().
171 | resp_redirect(#wm_reqdata{resp_redirect=R}) -> R.
172 |
173 | -spec resp_headers(t()) -> webmachine_headers:headers().
174 | resp_headers(_RD = #wm_reqdata{resp_headers=RespH}) -> RespH. % mochiheaders
175 |
176 | -spec resp_body(t()) -> webmachine:response_body().
177 | resp_body(_RD = #wm_reqdata{resp_body={stream,X}}) -> {stream,X};
178 | resp_body(_RD = #wm_reqdata{resp_body={known_length_stream,X,Y}}) -> {known_length_stream,X,Y};
179 | resp_body(_RD = #wm_reqdata{resp_body={stream,X,Y}}) -> {stream,X,Y};
180 | resp_body(_RD = #wm_reqdata{resp_body={writer,X}}) -> {writer,X};
181 | resp_body(_RD = #wm_reqdata{resp_body=RespB}) when is_binary(RespB) -> RespB;
182 | resp_body(_RD = #wm_reqdata{resp_body=RespB}) -> iolist_to_binary(RespB).
183 |
184 | %% --
185 |
186 | path_info(Key, RD) when is_atom(Key) ->
187 | case orddict:find(Key, path_info(RD)) of
188 | {ok, Value} when is_list(Value); is_integer(Value) ->
189 | Value; % string (for host or path match)
190 | % or integer (for port match)
191 | error -> undefined
192 | end.
193 |
194 | -spec get_req_header(webmachine_headers:name(), t()) ->
195 | undefined | webmachine_headers:value().
196 | get_req_header(HdrName, RD) -> % string->string
197 | mochiweb_headers:get_value(HdrName, req_headers(RD)).
198 |
199 | -spec do_redirect(boolean(), t()) -> t().
200 | do_redirect(Bool, RD) -> RD#wm_reqdata{resp_redirect=Bool}.
201 |
202 | set_peer(P, RD) when is_list(P) -> RD#wm_reqdata{peer=P}. % string
203 |
204 | set_sock(S, RD) when is_list(S) -> RD#wm_reqdata{sock=S}. % string
205 |
206 | set_disp_path(P, RD) when is_list(P) -> RD#wm_reqdata{disp_path=P}. % string
207 |
208 | set_req_body(Body, RD) -> RD#wm_reqdata{req_body=Body}.
209 |
210 | set_resp_body(Body, RD) -> RD#wm_reqdata{resp_body=Body}.
211 |
212 | set_response_code({Code, _ReasonPhrase}=CodeAndReason, RD) when is_integer(Code) ->
213 | RD#wm_reqdata{response_code=CodeAndReason};
214 | set_response_code(Code, RD) when is_integer(Code) ->
215 | RD#wm_reqdata{response_code=Code}.
216 |
217 | get_resp_header(HdrName, _RD=#wm_reqdata{resp_headers=RespH}) ->
218 | mochiweb_headers:get_value(HdrName, RespH).
219 |
220 | set_resp_header(K, V, RD=#wm_reqdata{resp_headers=RespH})
221 | when is_list(K),is_list(V) ->
222 | RD#wm_reqdata{resp_headers=mochiweb_headers:enter(K, V, RespH)}.
223 |
224 | set_resp_headers(Hdrs, RD=#wm_reqdata{resp_headers=RespH}) ->
225 | F = fun({K, V}, Acc) -> mochiweb_headers:enter(K, V, Acc) end,
226 | RD#wm_reqdata{resp_headers=lists:foldl(F, RespH, Hdrs)}.
227 |
228 | fresh_resp_headers(Hdrs, RD) ->
229 | F = fun({K, V}, Acc) -> mochiweb_headers:enter(K, V, Acc) end,
230 | RD#wm_reqdata{resp_headers=lists:foldl(F, mochiweb_headers:empty(), Hdrs)}.
231 |
232 | remove_resp_header(K, RD=#wm_reqdata{resp_headers=RespH}) when is_list(K) ->
233 | RD#wm_reqdata{resp_headers=mochiweb_headers:from_list(
234 | proplists:delete(K,
235 | mochiweb_headers:to_list(RespH)))}.
236 |
237 | merge_resp_headers(Hdrs, RD=#wm_reqdata{resp_headers=RespH}) ->
238 | F = fun({K, V}, Acc) -> mochiweb_headers:insert(K, V, Acc) end,
239 | NewHdrs = lists:foldl(F, RespH, Hdrs),
240 | RD#wm_reqdata{resp_headers=NewHdrs}.
241 |
242 | -spec append_to_resp_body(iolist() | binary(), t()) -> t().
243 | append_to_resp_body(Data, RD) -> append_to_response_body(Data, RD).
244 |
245 | -spec append_to_response_body(iolist() | binary(), t()) -> t().
246 | append_to_response_body(IOList, RD) when is_list(IOList) ->
247 | append_to_response_body(iolist_to_binary(IOList), RD);
248 | append_to_response_body(Data, RD=#wm_reqdata{resp_body=RespB})
249 | when is_binary(Data) ->
250 | Data0 = RespB,
251 | Data1 = <>,
252 | RD#wm_reqdata{resp_body=Data1}.
253 |
254 | -spec set_resp_range(follow_request | ignore_request, t()) -> t().
255 | set_resp_range(RespRange, RD)
256 | when RespRange =:= follow_request orelse RespRange =:= ignore_request ->
257 | RD#wm_reqdata{resp_range = RespRange}.
258 |
259 | -spec get_cookie_value(string(), t()) -> string() | undefined.
260 | get_cookie_value(Key, RD) when is_list(Key) ->
261 | case lists:keyfind(Key, 1, req_cookie(RD)) of
262 | false -> undefined;
263 | {Key, Value} -> Value
264 | end.
265 |
266 | -spec get_qs_value(string(), t()) -> string() | undefined.
267 | get_qs_value(Key, RD) when is_list(Key) -> % string
268 | case lists:keyfind(Key, 1, req_qs(RD)) of
269 | false -> undefined;
270 | {Key, Value} -> Value
271 | end.
272 |
273 | -spec get_qs_value(string(), string(), t()) -> string().
274 | get_qs_value(Key, Default, RD) when is_list(Key) ->
275 | case lists:keyfind(Key, 1, req_qs(RD)) of
276 | false -> Default;
277 | {Key, Value} -> Value
278 | end.
279 |
280 | -spec add_note(any(), any(), t()) -> t().
281 | add_note(K, V, RD) -> RD#wm_reqdata{notes=[{K, V} | RD#wm_reqdata.notes]}.
282 |
283 | -spec get_notes(t()) -> list().
284 | get_notes(RD) -> RD#wm_reqdata.notes.
285 |
286 | -spec base_uri(t()) -> string().
287 | base_uri(RD) ->
288 | Scheme = erlang:atom_to_list(RD#wm_reqdata.scheme),
289 | Host = string:join(RD#wm_reqdata.host_tokens, "."),
290 | PortString = port_string(RD#wm_reqdata.scheme, RD#wm_reqdata.port),
291 | Scheme ++ "://" ++ Host ++ PortString.
292 |
293 | -spec port_string(scheme(), inet:port_number()) -> string().
294 | port_string(http, 80) -> "";
295 | port_string(https, 443) -> "";
296 | port_string(_, Port) ->
297 | ":" ++ erlang:integer_to_list(Port).
298 |
299 | %%
300 | %% Tests
301 | %%
302 |
303 | -ifdef(TEST).
304 | -include_lib("eunit/include/eunit.hrl").
305 |
306 | make_wrq(Method, RawPath, Headers) ->
307 | make_wrq(Method, http, RawPath, Headers).
308 |
309 | make_wrq(Method, Scheme, RawPath, Headers) ->
310 | create(Method, Scheme, {1,1}, RawPath, mochiweb_headers:from_list(Headers)).
311 |
312 | accessor_test() ->
313 | R0 = make_wrq('GET', "/foo?a=1&b=2", [{"Cookie", "foo=bar"}]),
314 | R = set_peer("127.0.0.1", R0),
315 | ?assertEqual('GET', method(R)),
316 | ?assertEqual({1,1}, version(R)),
317 | ?assertEqual("/foo", path(R)),
318 | ?assertEqual("/foo?a=1&b=2", raw_path(R)),
319 | ?assertEqual([{"a", "1"}, {"b", "2"}], req_qs(R)),
320 | ?assertEqual({"1", "2"}, {get_qs_value("a", R), get_qs_value("b", R)}),
321 | ?assertEqual("3", get_qs_value("c", "3", R)),
322 | ?assertEqual([{"foo", "bar"}], req_cookie(R)),
323 | ?assertEqual("bar", get_cookie_value("foo", R)),
324 | ?assertEqual("127.0.0.1", peer(R)).
325 |
326 | simple_dispatch_test() ->
327 | R0 = make_wrq('GET', "/foo?a=1&b=2", [{"Cookie", "foo=bar"}]),
328 | R1 = set_peer("127.0.0.1", R0),
329 | {_, _, HostTokens, Port, PathTokens, Bindings, AppRoot, StringPath} =
330 | webmachine_dispatcher:dispatch("127.0.0.1", "/foo",
331 | [{["foo"], foo_resource, []}], R1),
332 | R = load_dispatch_data(Bindings,
333 | HostTokens,
334 | Port,
335 | PathTokens,
336 | AppRoot,
337 | StringPath,
338 | R1),
339 | ?assertEqual(".", app_root(R)),
340 | ?assertEqual(80, port(R)),
341 | ?assertEqual("http://127.0.0.1", base_uri(R)).
342 |
343 | base_uri_test_() ->
344 | Make_req =
345 | fun(Scheme, Host) ->
346 | R0 = make_wrq('GET', Scheme, "/foo?a=1&b=2",
347 | [{"Cookie", "foo=bar"}]),
348 | R1 = set_peer("127.0.0.1", R0),
349 | DispatchRule = {["foo"], foo_resource, []},
350 | {_, _, HostTokens, Port, PathTokens,
351 | Bindings, AppRoot,StringPath} =
352 | webmachine_dispatcher:dispatch(Host, "/foo", [DispatchRule],
353 | R1),
354 | load_dispatch_data(Bindings,
355 | HostTokens,
356 | Port,
357 | PathTokens,
358 | AppRoot,
359 | StringPath,
360 | R1)
361 | end,
362 | Tests = [{{http, "somewhere.com:8080"}, "http://somewhere.com:8080"},
363 | {{https, "somewhere.com:8080"}, "https://somewhere.com:8080"},
364 |
365 | {{http, "somewhere.com"}, "http://somewhere.com"},
366 | {{https, "somewhere.com"}, "https://somewhere.com"},
367 |
368 | {{http, "somewhere.com:80"}, "http://somewhere.com"},
369 | {{https, "somewhere.com:443"}, "https://somewhere.com"},
370 | {{https, "somewhere.com:80"}, "https://somewhere.com:80"},
371 | {{http, "somewhere.com:443"}, "http://somewhere.com:443"}],
372 | [ ?_assertEqual(Expect, base_uri(Make_req(Scheme, Host)))
373 | || {{Scheme, Host}, Expect} <- Tests ].
374 |
375 | -endif.
376 |
--------------------------------------------------------------------------------
/test/etag_test.erl:
--------------------------------------------------------------------------------
1 | %% @author Justin Sheehy
2 | %% @author Andy Gross
3 | %% @copyright 2007-2010 Basho Technologies
4 | %%
5 | %% Licensed under the Apache License, Version 2.0 (the "License");
6 | %% you may not use this file except in compliance with the License.
7 | %% You may obtain a copy of the License at
8 | %%
9 | %% http://www.apache.org/licenses/LICENSE-2.0
10 | %%
11 | %% Unless required by applicable law or agreed to in writing, software
12 | %% distributed under the License is distributed on an "AS IS" BASIS,
13 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | %% See the License for the specific language governing permissions and
15 | %% limitations under the License.
16 |
17 | -module(etag_test).
18 |
19 | -ifdef(EQC).
20 |
21 | -include_lib("eqc/include/eqc.hrl").
22 | -include_lib("eunit/include/eunit.hrl").
23 | -include("webmachine.hrl").
24 |
25 | -compile(export_all).
26 |
27 | -define(QC_OUT(P),
28 | eqc:on_output(fun(Str, Args) -> io:format(user, Str, Args) end, P)).
29 |
30 | unique(L) ->
31 | lists:reverse(lists:foldl(fun(Elem, Acc) ->
32 | case lists:member(Elem, Acc) of
33 | true ->
34 | Acc;
35 | false ->
36 | [Elem | Acc]
37 | end
38 | end, [], L)).
39 |
40 | etag(Bin) ->
41 | integer_to_list(erlang:crc32(Bin)).
42 |
43 | etag_list([]) ->
44 | "*";
45 | etag_list(Bins) ->
46 | string:join([[$", etag(B), $"] || B <- Bins], ",").
47 |
48 | http_request(_Match, _IfVals, _NewVal, 0) ->
49 | error;
50 | http_request(Match, IfVals, NewVal, Count) ->
51 | case httpc:request(put, {"http://localhost:12000/etagtest/foo",
52 | [{Match, etag_list(IfVals)}],
53 | "binary/octet-stream",
54 | NewVal},
55 | [], []) of
56 | {ok, Result} ->
57 | {ok, Result};
58 | {error, socket_closed_remotely} ->
59 | io:format(user, "Retry!\n", []),
60 | http_request(Match, IfVals, NewVal, Count-1)
61 | end.
62 |
63 | etag_prop() ->
64 | ?LET({AllVals, Match}, {non_empty(list(binary())), oneof(["If-Match", "If-None-Match"])},
65 | ?FORALL({IfVals0, CurVal, NewVal},
66 | {list(oneof(AllVals)), oneof(AllVals), oneof(AllVals)},
67 | begin
68 | ets:insert(?MODULE, [{etag, etag(CurVal)}]),
69 | IfVals = unique(IfVals0),
70 | {ok, Result} = http_request(Match, IfVals, NewVal, 3),
71 | Code = element(2, element(1, Result)),
72 | ExpectedCode =
73 | expected_response_code(Match,
74 | IfVals,
75 | lists:member(CurVal, IfVals)),
76 | equals(ExpectedCode, Code)
77 | end)).
78 |
79 | expected_response_code("If-Match", _, true) ->
80 | 204;
81 | expected_response_code("If-Match", [], false) ->
82 | 204;
83 | expected_response_code("If-Match", _, false) ->
84 | 412;
85 | expected_response_code("If-None-Match", _, true) ->
86 | 412;
87 | expected_response_code("If-None-Match", [], false) ->
88 | 412;
89 | expected_response_code("If-None-Match", _, false) ->
90 | 204.
91 |
92 | etag_test_() ->
93 | Time = 10,
94 | {spawn,
95 | [{setup,
96 | fun setup/0,
97 | fun cleanup/1,
98 | [
99 | {timeout, Time*3,
100 | ?_assert(eqc:quickcheck(eqc:testing_time(Time, ?QC_OUT(etag_prop()))))}
101 | ]}]}.
102 |
103 | %% The EQC tests can periodically fail, however the counter examples it
104 | %% produces are just coincidental. One reduction in particlar (the tuple of the
105 | %% empty list and two empty binaries) is enough of a red herring that it's
106 | %% included here as a sanity check.
107 | etag_regressions_test_() ->
108 | CounterExample1 = [{[], <<>>, <<>>}],
109 | CounterExample2 = [{[<<25,113,71,254>>, <<25,113,71,254>>, <<"?">>],
110 | <<"?">>, <<"r?}">>}],
111 | CounterExample3 = [{[<<19>>, <<70,6,56,181,38,128>>,
112 | <<70,6,56,181,38,128>>, <<19>>, <<19>>, <<19>>,
113 | <<70,6,56,181,38,128>>, <<19>>, <<19>>, <<19>>,
114 | <<19>>, <<70,6,56,181,38,128>>], <<19>>,
115 | <<70,6,56,181,38,128>>}],
116 | {spawn,
117 | [{setup, fun setup/0, fun cleanup/1,
118 | [{"counter example 1",
119 | ?_assert(eqc:check(?QC_OUT(etag_prop()), CounterExample1))},
120 | {"counter example 2",
121 | ?_assert(eqc:check(?QC_OUT(etag_prop()), CounterExample2))},
122 | {"counter example 3",
123 | ?_assert(eqc:check(?QC_OUT(etag_prop()), CounterExample3))}]}]}.
124 |
125 | setup() ->
126 | error_logger:tty(false),
127 | %% Setup ETS table to hold current etag value
128 | ets:new(?MODULE, [named_table, public]),
129 |
130 | %% Spin up webmachine
131 | application:start(inets),
132 | WebConfig = [{ip, "0.0.0.0"}, {port, 12000},
133 | {dispatch, [{["etagtest", '*'], ?MODULE, []}]}],
134 | {ok, Pid0} = webmachine_sup:start_link(),
135 | {ok, Pid1} = webmachine_mochiweb:start(WebConfig),
136 | link(Pid1),
137 | {Pid0, Pid1}.
138 |
139 | cleanup({Pid0, Pid1}) ->
140 | %% clean up
141 | unlink(Pid0),
142 | exit(Pid0, normal),
143 | unlink(Pid1),
144 | exit(Pid1, kill),
145 | application:stop(inets).
146 |
147 | init([]) ->
148 | {ok, undefined}.
149 |
150 | allowed_methods(ReqData, Context) ->
151 | {['PUT'], ReqData, Context}.
152 |
153 | content_types_accepted(ReqData, Context) ->
154 | {[{"binary/octet-stream", on_put}], ReqData, Context}.
155 |
156 | on_put(ReqData, Context) ->
157 | {ok, ReqData, Context}.
158 |
159 | generate_etag(ReqData, Context) ->
160 | case ets:lookup(?MODULE, etag) of
161 | [] ->
162 | {undefined, ReqData, Context};
163 | [{etag, ETag}] ->
164 | {ETag, ReqData, Context}
165 | end.
166 |
167 | -endif.
168 |
--------------------------------------------------------------------------------
/test/wm_echo_host_header.erl:
--------------------------------------------------------------------------------
1 | %%
2 | %%
3 | %% Licensed under the Apache License, Version 2.0 (the "License");
4 | %% you may not use this file except in compliance with the License.
5 | %% You may obtain a copy of the License at
6 | %%
7 | %% http://www.apache.org/licenses/LICENSE-2.0
8 | %%
9 | %% Unless required by applicable law or agreed to in writing, software
10 | %% distributed under the License is distributed on an "AS IS" BASIS,
11 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | %% See the License for the specific language governing permissions and
13 | %% limitations under the License.
14 |
15 | -module(wm_echo_host_header).
16 |
17 | -export([
18 | init/1,
19 | to_html/2,
20 | parse_body/1
21 | ]).
22 |
23 | -include("webmachine.hrl").
24 |
25 | init([]) -> {ok, undefined}.
26 |
27 | to_html(Req, State) ->
28 | HostVal = wrq:get_req_header("host", Req),
29 | HostTokens = string:join(wrq:host_tokens(Req), "."),
30 | Body = "Host\t" ++ HostVal ++
31 | "\nHostTokens\t" ++ HostTokens ++ "\n",
32 | {Body, Req, State}.
33 |
34 | %% Transform the body returned by this resource into a proplist for
35 | %% testing.
36 | parse_body(Body) ->
37 | Lines = re:split(Body, "\n"),
38 | [ erlang:list_to_tuple(re:split(Line, "\t")) || Line <- Lines ].
39 |
--------------------------------------------------------------------------------
/test/wm_integration_test.erl:
--------------------------------------------------------------------------------
1 | %%
2 | %%
3 | %% Licensed under the Apache License, Version 2.0 (the "License");
4 | %% you may not use this file except in compliance with the License.
5 | %% You may obtain a copy of the License at
6 | %%
7 | %% http://www.apache.org/licenses/LICENSE-2.0
8 | %%
9 | %% Unless required by applicable law or agreed to in writing, software
10 | %% distributed under the License is distributed on an "AS IS" BASIS,
11 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | %% See the License for the specific language governing permissions and
13 | %% limitations under the License.
14 | -module(wm_integration_test).
15 | -ifdef(TEST).
16 |
17 | -include_lib("eunit/include/eunit.hrl").
18 | -include("webmachine.hrl").
19 |
20 | integration_test_() ->
21 | {foreach,
22 | %% Setup
23 | fun() ->
24 | ibrowse:start(),
25 | DL = [{["wm_echo_host_header", '*'], wm_echo_host_header, []}],
26 | %% Listen on both ipv4 and ipv6 so we can test both.
27 | Ctx = wm_integration_test_util:start(?MODULE, "::0", DL),
28 | Ctx
29 | end,
30 | %% Cleanup
31 | fun(Ctx) ->
32 | wm_integration_test_util:stop(Ctx)
33 | end,
34 | %% Test functions provided with context from setup
35 | [fun(Ctx) ->
36 | {spawn, {with, Ctx, integration_tests()}}
37 | end]}.
38 |
39 | -ifdef(GITHUBEXCLUDE).
40 |
41 | integration_tests() ->
42 | [fun test_host_header_localhost/1,
43 | fun test_host_header_127/1].
44 |
45 | -else.
46 |
47 | integration_tests() ->
48 | [fun test_host_header_localhost/1,
49 | fun test_host_header_127/1,
50 | fun test_host_header_ipv6/1,
51 | fun test_host_header_ipv6_curl/1].
52 |
53 | -endif.
54 |
55 | test_host_header_localhost(Ctx) ->
56 | ExpectHost = add_port(Ctx, "localhost"),
57 | verify_host_header(Ctx, "localhost", ExpectHost, <<"localhost">>).
58 |
59 | test_host_header_127(Ctx) ->
60 | ExpectHost = add_port(Ctx, "127.0.0.1"),
61 | verify_host_header(Ctx, "127.0.0.1", ExpectHost, <<"127.0.0.1">>).
62 |
63 | -ifndef(GITHUBEXCLUDE).
64 |
65 | test_host_header_ipv6(Ctx) ->
66 | %% Bare ipv6 addresses must be enclosed in square
67 | %% brackets. ibrowse does the right thing in parsing the URL, but
68 | %% does not set the Host header correctly where it adds the bare
69 | %% host rather than the bracketed version.
70 | %%
71 | %% It is likely there are other HTTP clients that will make send
72 | %% such a Host header, it is worth testing that we handle it
73 | %% reasonably.
74 | ExpectHost = add_port(Ctx, "::1"),
75 | ExpectTokens = <<"[", ExpectHost/binary, "]">>,
76 | verify_host_header(Ctx, "[::1]", ExpectHost, ExpectTokens).
77 |
78 | test_host_header_ipv6_curl(Ctx) ->
79 | %% curl has the desired client behavior for ipv6
80 | case os:find_executable("curl") of
81 | false ->
82 | ?debugMsg("curl not found: skipping test_host_header_ipv6_curl");
83 | _ ->
84 | Port = wm_integration_test_util:get_port(Ctx),
85 | P = erlang:integer_to_list(Port),
86 | Cmd = "curl -gs http://[::1]:" ++ P ++ "/wm_echo_host_header",
87 | Got = wm_echo_host_header:parse_body(erlang:list_to_binary(os:cmd(Cmd))),
88 | ?assertEqual(add_port(Ctx, "[::1]"), proplists:get_value(<<"Host">>, Got)),
89 | ?assertEqual(<<"[::1]">>, proplists:get_value(<<"HostTokens">>, Got))
90 | end.
91 |
92 | -endif.
93 |
94 | url(Ctx, Host, Path) ->
95 | Port = erlang:integer_to_list(wm_integration_test_util:get_port(Ctx)),
96 | "http://" ++ Host ++ ":" ++ Port ++ slash(Path).
97 |
98 | slash("/" ++ _Rest = Path) ->
99 | Path;
100 | slash(Path) ->
101 | "/" ++ Path.
102 |
103 | add_port(Ctx, Host) ->
104 | Port = wm_integration_test_util:get_port(Ctx),
105 | erlang:iolist_to_binary([Host, ":", erlang:integer_to_list(Port)]).
106 |
107 | %% TODO: wm crashes if multiple Host headers are sent
108 |
109 | verify_host_header(Ctx, Host, ExpectHostHeader, ExpectHostTokens) ->
110 | URL = url(Ctx, Host, "wm_echo_host_header"),
111 | {ok, Status, _Headers, Body} = ibrowse:send_req(URL, [], get, [], []),
112 | ?assertEqual("200", Status),
113 | Got = wm_echo_host_header:parse_body(Body),
114 | ?assertEqual(ExpectHostHeader, proplists:get_value(<<"Host">>, Got)),
115 | ?assertEqual(ExpectHostTokens, proplists:get_value(<<"HostTokens">>, Got)).
116 |
117 |
118 | -endif.
119 |
--------------------------------------------------------------------------------
/test/wm_integration_test_util.erl:
--------------------------------------------------------------------------------
1 | %% @author Macneil Shonle
2 | %% @copyright 2007-2013 Basho Technologies
3 | %%
4 | %% Licensed under the Apache License, Version 2.0 (the "License");
5 | %% you may not use this file except in compliance with the License.
6 | %% You may obtain a copy of the License at
7 | %%
8 | %% http://www.apache.org/licenses/LICENSE-2.0
9 | %%
10 | %% Unless required by applicable law or agreed to in writing, software
11 | %% distributed under the License is distributed on an "AS IS" BASIS,
12 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | %% See the License for the specific language governing permissions and
14 | %% limitations under the License.
15 |
16 | %% These utility functions can be used to set up Webmachine on an ephemeral
17 | %% port that can be interacted with over HTTP requests. These utilities are
18 | %% useful for writing integration tests, which test all of the Webmachine
19 | %% stack, as a complement to the unit-testing and mocking strategies.
20 | -module(wm_integration_test_util).
21 |
22 | -ifdef(TEST).
23 | -export([start/3, stop/1]).
24 | -export([get_port/1, url/1, url/2]).
25 |
26 | -export([init/1, service_available/2]).
27 |
28 | -include("webmachine.hrl").
29 | -include_lib("eunit/include/eunit.hrl").
30 |
31 | -define(EPHEMERAL_PORT, 0).
32 |
33 | -record(integration_state, {
34 | webmachine_sup,
35 | mochi_serv,
36 | port,
37 | resource_name,
38 | base_url = "http://localhost"
39 | }).
40 |
41 | %% Returns an integration_state record that should be passed to get_port/1 and
42 | %% stop/1. Starts up webmachine and the mochiweb server with the given name,
43 | %% ip, and dispatch list. Communication is set up on an ephemeral port, which
44 | %% can be accessed via get_port/1.
45 | start(Name, IP, DispatchList) ->
46 | cleanup_previous_runs(),
47 | error_logger:tty(false),
48 | application:start(inets),
49 | {ok, WebmachineSup} = webmachine_sup:start_link(),
50 | WebConfig = [{name, Name}, {ip, IP}, {port, ?EPHEMERAL_PORT},
51 | {dispatch, DispatchList}],
52 | {ok, MochiServ} = webmachine_mochiweb:start(WebConfig),
53 | link(MochiServ),
54 | Port = mochiweb_socket_server:get(MochiServ, port),
55 | #integration_state{webmachine_sup=WebmachineSup,
56 | mochi_serv=MochiServ,
57 | port=Port,
58 | resource_name=Name}.
59 |
60 | %% Receives the integration_state record returned by start/3
61 | stop(Context) ->
62 | stop_supervisor(Context#integration_state.webmachine_sup),
63 | MochiServ = Context#integration_state.mochi_serv,
64 | {registered_name, MochiName} = process_info(MochiServ, registered_name),
65 | webmachine_mochiweb:stop(MochiName),
66 | stop_supervisor(MochiServ),
67 | application:stop(inets).
68 |
69 | %% Receives the integration_state record returned by start_webmachine, returns
70 | %% the port to use to communicate with Webmachine over HTTP.
71 | get_port(Context) ->
72 | Context#integration_state.port.
73 |
74 | %% Returns a URL to use for an HTTP request to communicate with Webmachine.
75 | url(Context) ->
76 | Port = get_port(Context),
77 | Name = Context#integration_state.resource_name,
78 | Chars = io_lib:format("http://localhost:~b/~s", [Port, Name]),
79 | lists:flatten(Chars).
80 |
81 | %% Returns a URL extended with the given path to use for an HTTP request to
82 | %% communicate with Webmachine
83 | url(Context, Path) ->
84 | url(Context) ++ "/" ++ Path.
85 |
86 | stop_supervisor(Sup) ->
87 | unlink(Sup),
88 | exit(Sup, kill),
89 | wait_for_pid(Sup).
90 |
91 | %% Wait for a pid to exit -- Copied from riak_kv_test_util.erl
92 | wait_for_pid(Pid) ->
93 | Mref = erlang:monitor(process, Pid),
94 | receive
95 | {'DOWN', Mref, process, _, _} ->
96 | ok
97 | after
98 | 5000 ->
99 | {error, didnotexit, Pid, erlang:process_info(Pid)}
100 | end.
101 |
102 | %% Sometimes the previous clean up didn't work, so we try again
103 | cleanup_previous_runs() ->
104 | RegNames = [webmachine_sup, webmachine_router, webmachine_logger,
105 | webmachine_log_event, webmachine_logger_watcher_sup],
106 | UndefinedsOrPids = [whereis(RegName) || RegName <- RegNames],
107 | [wait_for_pid(Pid) || Pid <- UndefinedsOrPids, Pid /= undefined].
108 |
109 |
110 | %%
111 | %% EXAMPLE TEST CASE
112 | %%
113 | %% init and service_available are simple resource functions for a service that
114 | %% is unavailable (a 503 error)
115 | init([]) ->
116 | {ok, undefined}.
117 |
118 | service_available(ReqData, Context) ->
119 | {false, ReqData, Context}.
120 |
121 | integration_tests() ->
122 | [fun service_unavailable_test/1].
123 |
124 | integration_test_() ->
125 | {foreach,
126 | %% Setup
127 | fun() ->
128 | DL = [{[atom_to_list(?MODULE), '*'], ?MODULE, []}],
129 | Ctx = wm_integration_test_util:start(?MODULE, "0.0.0.0", DL),
130 | Ctx
131 | end,
132 | %% Cleanup
133 | fun(Ctx) ->
134 | wm_integration_test_util:stop(Ctx)
135 | end,
136 | %% Test functions provided with context from setup
137 | [fun(Ctx) ->
138 | {spawn, {with, Ctx, integration_tests()}}
139 | end]}.
140 |
141 | service_unavailable_test(Ctx) ->
142 | URL = wm_integration_test_util:url(Ctx, "foo"),
143 | {ok, Result} = httpc:request(head, {URL, []}, [], []),
144 | ?assertMatch({{"HTTP/1.1", 503, "Service Unavailable"}, _, _}, Result),
145 | ok.
146 |
147 | -endif.
148 |
--------------------------------------------------------------------------------