├── .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 | [![Erlang CI Actions Status](https://github.com/basho/webmachine/workflows/Erlang%20CI/badge.svg)](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 += ''; 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(''); 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 | ""] 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 | --------------------------------------------------------------------------------