├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── LICENSE ├── Makefile ├── README.asciidoc ├── doc └── src │ └── manual │ ├── cow_cookie.asciidoc │ ├── cow_cookie.cookie.asciidoc │ ├── cow_cookie.parse_cookie.asciidoc │ ├── cow_cookie.parse_set_cookie.asciidoc │ ├── cow_cookie.setcookie.asciidoc │ └── cowlib_app.asciidoc ├── ebin └── cowlib.app ├── erlang.mk ├── include ├── cow_inline.hrl └── cow_parse.hrl └── src ├── cow_base64url.erl ├── cow_cookie.erl ├── cow_date.erl ├── cow_deflate.erl ├── cow_hpack.erl ├── cow_hpack_common.hrl ├── cow_hpack_dec_huffman_lookup.hrl ├── cow_http.erl ├── cow_http1.erl ├── cow_http2.erl ├── cow_http2_machine.erl ├── cow_http3.erl ├── cow_http3_machine.erl ├── cow_http_hd.erl ├── cow_http_struct_hd.erl ├── cow_http_te.erl ├── cow_iolists.erl ├── cow_link.erl ├── cow_mimetypes.erl ├── cow_mimetypes.erl.src ├── cow_multipart.erl ├── cow_qpack.erl ├── cow_qs.erl ├── cow_spdy.erl ├── cow_spdy.hrl ├── cow_sse.erl ├── cow_uri.erl ├── cow_uri_template.erl └── cow_ws.erl /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | ## Use workflows from ninenines/ci.erlang.mk to test Cowlib. 2 | 3 | name: Check Cowlib 4 | 5 | on: 6 | push: 7 | branches: 8 | - master 9 | pull_request: 10 | schedule: 11 | ## Every Monday at 2am. 12 | - cron: 0 2 * * 1 13 | 14 | env: 15 | CI_ERLANG_MK: 1 16 | 17 | jobs: 18 | cleanup-master: 19 | name: Cleanup master build 20 | runs-on: ubuntu-latest 21 | steps: 22 | 23 | - name: Cleanup master build if necessary 24 | if: ${{ github.event_name == 'schedule' }} 25 | run: | 26 | gh extension install actions/gh-actions-cache 27 | gh actions-cache delete Linux-X64-Erlang-master -R $REPO --confirm || true 28 | gh actions-cache delete macOS-X64-Erlang-master -R $REPO --confirm || true 29 | env: 30 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | REPO: ${{ github.repository }} 32 | 33 | check: 34 | name: Cowlib 35 | needs: cleanup-master 36 | uses: ninenines/ci.erlang.mk/.github/workflows/ci.yaml@master 37 | 38 | # The perfs tests are nice to run but typically not 39 | # important. So we run them after we are done with the other 40 | # test suites. At this point we know that Erlang was built 41 | # so we can just use the latest version. 42 | 43 | perfs: 44 | name: Run performance tests 45 | needs: check 46 | runs-on: 'ubuntu-latest' 47 | if: ${{ !cancelled() }} 48 | steps: 49 | 50 | - name: Checkout repository 51 | uses: actions/checkout@v4 52 | 53 | - name: Output latest Erlang/OTP version 54 | id: latest_version 55 | run: | 56 | { 57 | echo "latest<> "$GITHUB_OUTPUT" 61 | 62 | - name: Restore CI cache 63 | uses: actions/cache/restore@v4 64 | with: 65 | path: | 66 | ~/erlang/ 67 | key: ${{ runner.os }}-${{ runner.arch }}-Erlang-${{ steps.latest_version.outputs.latest }} 68 | 69 | - name: Run perfs 70 | run: make perfs LATEST_ERLANG_OTP=1 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cowlib.plt 2 | .erlang.mk 3 | _rel 4 | cowlib.d 5 | deps 6 | doc/man3 7 | doc/man7 8 | doc/markdown 9 | ebin/*.beam 10 | ebin/test 11 | examples/*/ebin 12 | logs 13 | relx 14 | test/*.beam 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2025, Loïc Hoguin 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # See LICENSE for licensing information. 2 | 3 | PROJECT = cowlib 4 | PROJECT_DESCRIPTION = Support library for manipulating Web protocols. 5 | PROJECT_VERSION = 2.15.0 6 | 7 | # Options. 8 | 9 | #ERLC_OPTS += +bin_opt_info 10 | DIALYZER_OPTS = -Werror_handling -Wunmatched_returns 11 | 12 | # Dependencies. 13 | 14 | LOCAL_DEPS = crypto 15 | 16 | DOC_DEPS = asciideck 17 | 18 | TEST_DEPS = $(if $(CI_ERLANG_MK),ci.erlang.mk) base32 horse proper jsx \ 19 | decimal structured-header-tests uritemplate-tests 20 | dep_base32 = git https://github.com/dnsimple/base32_erlang main 21 | dep_horse = git https://github.com/ninenines/horse.git master 22 | dep_jsx = git https://github.com/talentdeficit/jsx v2.10.0 23 | dep_decimal = git https://github.com/egobrain/decimal 0.6.2 24 | dep_structured-header-tests = git https://github.com/httpwg/structured-header-tests faed1f92942abd4fb5d61b1f9f0dc359f499f1d7 25 | dep_uritemplate-tests = git https://github.com/uri-templates/uritemplate-test master 26 | 27 | # CI configuration. 28 | 29 | dep_ci.erlang.mk = git https://github.com/ninenines/ci.erlang.mk master 30 | DEP_EARLY_PLUGINS = ci.erlang.mk 31 | 32 | AUTO_CI_OTP ?= OTP-LATEST-24+ 33 | AUTO_CI_WINDOWS ?= OTP-LATEST-24+ 34 | 35 | # Hex configuration. 36 | 37 | define HEX_TARBALL_EXTRA_METADATA 38 | #{ 39 | licenses => [<<"ISC">>], 40 | links => #{ 41 | <<"Function reference">> => <<"https://ninenines.eu/docs/en/cowlib/2.15/manual/">>, 42 | <<"GitHub">> => <<"https://github.com/ninenines/cowlib">>, 43 | <<"Sponsor">> => <<"https://github.com/sponsors/essen">> 44 | } 45 | } 46 | endef 47 | 48 | # Standard targets. 49 | 50 | include erlang.mk 51 | 52 | # Always rebuild from scratch in CI because OTP-25.0+ can't use the older build. 53 | 54 | ci-setup:: distclean-deps 55 | -$(verbose) rm -rf $(ERLANG_MK_TMP)/rebar 56 | 57 | # Compile options. 58 | 59 | TEST_ERLC_OPTS += +'{parse_transform, eunit_autoexport}' +'{parse_transform, horse_autoexport}' 60 | 61 | # Mimetypes module generator. 62 | 63 | GEN_URL = http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types 64 | GEN_SRC = src/cow_mimetypes.erl.src 65 | GEN_OUT = src/cow_mimetypes.erl 66 | 67 | .PHONY: gen 68 | 69 | gen: 70 | $(gen_verbose) cat $(GEN_SRC) \ 71 | | head -n `grep -n "%% GENERATED" $(GEN_SRC) | cut -d : -f 1` \ 72 | > $(GEN_OUT) 73 | $(gen_verbose) wget -qO - $(GEN_URL) \ 74 | | grep -v ^# \ 75 | | awk '{for (i=2; i<=NF; i++) if ($$i != "") { \ 76 | split($$1, a, "/"); \ 77 | print "all_ext(<<\"" $$i "\">>) -> {<<\"" \ 78 | a[1] "\">>, <<\"" a[2] "\">>, []};"}}' \ 79 | | sort \ 80 | | uniq -w 25 \ 81 | >> $(GEN_OUT) 82 | $(gen_verbose) cat $(GEN_SRC) \ 83 | | tail -n +`grep -n "%% GENERATED" $(GEN_SRC) | cut -d : -f 1` \ 84 | >> $(GEN_OUT) 85 | 86 | # Performance testing. 87 | 88 | ifeq ($(MAKECMDGOALS),perfs) 89 | .NOTPARALLEL: 90 | endif 91 | 92 | .PHONY: perfs 93 | 94 | perfs: test-build 95 | $(gen_verbose) erl -noshell -pa ebin -eval 'horse:app_perf($(PROJECT)), erlang:halt().' 96 | 97 | # Prepare for the release. 98 | 99 | prepare_tag: 100 | $(verbose) $(warning Hex metadata: $(HEX_TARBALL_EXTRA_METADATA)) 101 | $(verbose) echo 102 | $(verbose) echo -n "Most recent tag: " 103 | $(verbose) git tag --sort taggerdate | tail -n1 104 | $(verbose) git verify-tag `git tag --sort taggerdate | tail -n1` 105 | $(verbose) echo -n "MAKEFILE: " 106 | $(verbose) grep -m1 PROJECT_VERSION Makefile 107 | $(verbose) echo -n "APP: " 108 | $(verbose) grep -m1 vsn ebin/$(PROJECT).app | sed 's/ //g' 109 | $(verbose) echo 110 | $(verbose) echo -n "LICENSE: " ; head -n1 LICENSE 111 | $(verbose) echo 112 | $(verbose) echo "Dependencies:" 113 | $(verbose) grep ^DEPS Makefile || echo "DEPS =" 114 | $(verbose) grep ^dep_ Makefile || true 115 | -------------------------------------------------------------------------------- /README.asciidoc: -------------------------------------------------------------------------------- 1 | = Cowlib 2 | 3 | Cowlib is a support library for manipulating Web protocols. 4 | 5 | == Goals 6 | 7 | Cowlib provides libraries for parsing and building messages 8 | for various Web protocols, including HTTP/1.1, HTTP/2 and 9 | Websocket. 10 | 11 | It is optimized for completeness rather than speed. No value 12 | is ignored, they are all returned. 13 | 14 | == Support 15 | 16 | * Official IRC Channel: #ninenines on irc.freenode.net 17 | * https://ninenines.eu/services[Commercial Support] 18 | * https://github.com/sponsors/essen[Sponsor me!] 19 | -------------------------------------------------------------------------------- /doc/src/manual/cow_cookie.asciidoc: -------------------------------------------------------------------------------- 1 | = cow_cookie(3) 2 | 3 | == Name 4 | 5 | cow_cookie - Cookies 6 | 7 | == Description 8 | 9 | The module `cow_cookie` provides functions for parsing 10 | and manipulating cookie headers. 11 | 12 | == Exports 13 | 14 | * link:man:cow_cookie:parse_cookie(3)[cow_cookie:parse_cookie(3)] - Parse a cookie header 15 | * link:man:cow_cookie:parse_set_cookie(3)[cow_cookie:parse_set_cookie(3)] - Parse a set-cookie header 16 | * link:man:cow_cookie:cookie(3)[cow_cookie:cookie(3)] - Generate a cookie header 17 | * link:man:cow_cookie:setcookie(3)[cow_cookie:setcookie(3)] - Generate a set-cookie header 18 | 19 | == Types 20 | 21 | === cookie_attrs() 22 | 23 | [source,erlang] 24 | ---- 25 | cookie_attrs() :: #{ 26 | expires => calendar:datetime(), 27 | max_age => calendar:datetime(), 28 | domain => binary(), 29 | path => binary(), 30 | secure => true, 31 | http_only => true, 32 | same_site => default | none | strict | lax 33 | } 34 | ---- 35 | 36 | Cookie attributes parsed from the set-cookie header. 37 | The attributes must be passed as-is to a cookie store 38 | engine for processing, along with the cookie name and value. 39 | More information about the attributes can be found in 40 | https://tools.ietf.org/html/rfc6265[RFC 6265]. 41 | 42 | === cookie_opts() 43 | 44 | [source,erlang] 45 | ---- 46 | cookie_opts() :: #{ 47 | domain => binary(), 48 | http_only => boolean(), 49 | max_age => non_neg_integer(), 50 | path => binary(), 51 | same_site => default | none | strict | lax, 52 | secure => boolean() 53 | } 54 | ---- 55 | 56 | Options for the set-cookie header. They are added to the 57 | header as attributes. More information about the options 58 | can be found in https://tools.ietf.org/html/rfc6265[RFC 6265]. 59 | 60 | The following options are defined: 61 | 62 | domain:: 63 | 64 | Hosts to which the cookie will be sent. By default it will 65 | only be sent to the origin server. 66 | 67 | http_only:: 68 | 69 | Whether the cookie should be restricted to HTTP requests, or 70 | it should also be exposed to other APIs, for example Javascript. 71 | By default there are no restrictions. 72 | 73 | max_age:: 74 | 75 | Maximum lifetime of the cookie, in seconds. By default the 76 | cookie is kept for the duration of the session. 77 | 78 | path:: 79 | 80 | Path to which the cookie will be sent. By default it will 81 | be sent to the current "directory" of the effective request URI. 82 | 83 | same_site:: 84 | 85 | Whether the cookie should be sent along with cross-site 86 | requests. This attribute is currently non-standard but is in 87 | the process of being standardized. Please refer to the 88 | https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7[RFC 6265 (bis) draft] 89 | for details. 90 | + 91 | The default value for this attribute may vary depending on 92 | user agent and configuration. Browsers are known to be more 93 | strict over TCP compared to TLS. 94 | 95 | secure:: 96 | 97 | Whether the cookie should be sent only on secure channels 98 | (for example TLS). Note that this does not guarantee the 99 | integrity of the cookie, only its confidentiality during 100 | transfer. By default there are no restrictions. 101 | 102 | == Changelog 103 | 104 | * *2.12*: The `same_site` attribute and option may now be 105 | set to `default`. 106 | * *2.10*: The `same_site` attribute and option may now be 107 | set to `none`. 108 | * *2.9*: The `cookie_attrs` type was added. 109 | * *1.0*: Module introduced. 110 | 111 | == See also 112 | 113 | link:man:cowlib(7)[cowlib(7)], 114 | https://tools.ietf.org/html/rfc6265[RFC 6265] 115 | -------------------------------------------------------------------------------- /doc/src/manual/cow_cookie.cookie.asciidoc: -------------------------------------------------------------------------------- 1 | = cow_cookie:cookie(3) 2 | 3 | == Name 4 | 5 | cow_cookie:cookie - Generate a cookie header 6 | 7 | == Description 8 | 9 | [source,erlang] 10 | ---- 11 | cookie(Cookies) -> iolist() 12 | 13 | Cookies :: [{Name :: iodata(), Value :: iodata()}] 14 | ---- 15 | 16 | Generate a cookie header. 17 | 18 | == Arguments 19 | 20 | Cookies:: 21 | 22 | A list of pairs of cookie name and value. 23 | 24 | == Return value 25 | 26 | An iolist with the generated cookie header value. 27 | 28 | == Changelog 29 | 30 | * *2.9*: Function introduced. 31 | 32 | == Examples 33 | 34 | .Generate a cookie header 35 | [source,erlang] 36 | ---- 37 | Cookie = cow_cookie:cookie([{<<"sessionid">>, ID}]). 38 | ---- 39 | 40 | == See also 41 | 42 | link:man:cow_cookie(3)[cow_cookie(3)], 43 | link:man:cow_cookie:parse_cookie(3)[cow_cookie:parse_cookie(3)], 44 | link:man:cow_cookie:parse_set_cookie(3)[cow_cookie:parse_set_cookie(3)], 45 | link:man:cow_cookie:setcookie(3)[cow_cookie:setcookie(3)] 46 | -------------------------------------------------------------------------------- /doc/src/manual/cow_cookie.parse_cookie.asciidoc: -------------------------------------------------------------------------------- 1 | = cow_cookie:parse_cookie(3) 2 | 3 | == Name 4 | 5 | cow_cookie:parse_cookie - Parse a cookie header 6 | 7 | == Description 8 | 9 | [source,erlang] 10 | ---- 11 | parse_cookie(Cookie :: binary()) 12 | -> [{binary(), binary()}] 13 | ---- 14 | 15 | Parse a cookie header. 16 | 17 | == Arguments 18 | 19 | Cookie:: 20 | 21 | The cookie header value. 22 | 23 | == Return value 24 | 25 | A list of cookie name/value pairs is returned on success. 26 | 27 | An exception is thrown in the event of a parse error. 28 | 29 | == Changelog 30 | 31 | * *2.9*: Fixes to the parser may lead to potential incompatibilities. 32 | A cookie name starting with `$` is no longer ignored. 33 | A cookie without a `=` will be parsed as the value of 34 | the cookie named `<<>>` (empty name). 35 | * *1.0*: Function introduced. 36 | 37 | == Examples 38 | 39 | .Parse a cookie header 40 | [source,erlang] 41 | ---- 42 | Cookies = cow_cookie:parse_cookie(CookieHd). 43 | ---- 44 | 45 | == See also 46 | 47 | link:man:cow_cookie(3)[cow_cookie(3)], 48 | link:man:cow_cookie:parse_set_cookie(3)[cow_cookie:parse_set_cookie(3)], 49 | link:man:cow_cookie:cookie(3)[cow_cookie:cookie(3)], 50 | link:man:cow_cookie:setcookie(3)[cow_cookie:setcookie(3)] 51 | -------------------------------------------------------------------------------- /doc/src/manual/cow_cookie.parse_set_cookie.asciidoc: -------------------------------------------------------------------------------- 1 | = cow_cookie:parse_set_cookie(3) 2 | 3 | == Name 4 | 5 | cow_cookie:parse_set_cookie - Parse a set-cookie header 6 | 7 | == Description 8 | 9 | [source,erlang] 10 | ---- 11 | parse_set_cookie(SetCookie :: binary()) 12 | -> {ok, Name, Value, Attrs} | ignore 13 | 14 | Name :: binary() 15 | Value :: binary() 16 | Attrs :: cow_cookie:cookie_attrs() 17 | ---- 18 | 19 | Parse a set-cookie header. 20 | 21 | == Arguments 22 | 23 | SetCookie:: 24 | 25 | The set-cookie header value. 26 | 27 | == Return value 28 | 29 | An `ok` tuple with the cookie name, value and attributes 30 | is returned on success. 31 | 32 | An atom `ignore` is returned when the cookie has both 33 | an empty name and an empty value, and must be ignored. 34 | 35 | == Changelog 36 | 37 | * *2.9*: Function introduced. 38 | 39 | == Examples 40 | 41 | .Parse a cookie header 42 | [source,erlang] 43 | ---- 44 | case cow_cookie:parse_set_cookie(SetCookieHd) of 45 | {ok, Name, Value, Attrs} -> 46 | cookie_engine_set_cookie(Name, Value, Attrs); 47 | ignore -> 48 | do_nothing() 49 | end. 50 | ---- 51 | 52 | == See also 53 | 54 | link:man:cow_cookie(3)[cow_cookie(3)], 55 | link:man:cow_cookie:parse_cookie(3)[cow_cookie:parse_cookie(3)], 56 | link:man:cow_cookie:cookie(3)[cow_cookie:cookie(3)], 57 | link:man:cow_cookie:setcookie(3)[cow_cookie:setcookie(3)] 58 | -------------------------------------------------------------------------------- /doc/src/manual/cow_cookie.setcookie.asciidoc: -------------------------------------------------------------------------------- 1 | = cow_cookie:setcookie(3) 2 | 3 | == Name 4 | 5 | cow_cookie:setcookie - Generate a set-cookie header 6 | 7 | == Description 8 | 9 | [source,erlang] 10 | ---- 11 | setcookie(Name :: iodata(), 12 | Value :: iodata(), 13 | Opts :: cow_cookie:cookie_opts()) 14 | -> iolist() 15 | ---- 16 | 17 | Generate a set-cookie header. 18 | 19 | == Arguments 20 | 21 | Name:: 22 | 23 | Cookie name. 24 | 25 | Value:: 26 | 27 | Cookie value. 28 | 29 | Opts:: 30 | 31 | Options added to the set-cookie header as attributes. 32 | 33 | == Return value 34 | 35 | An iolist with the generated set-cookie header value. 36 | 37 | == Changelog 38 | 39 | * *2.12*: The `Version` attribute is no longer generated. 40 | * *1.0*: Function introduced. 41 | 42 | == Examples 43 | 44 | .Generate a set-cookie header 45 | [source,erlang] 46 | ---- 47 | SetCookie = cow_cookie:setcookie(<<"sessionid">>, ID, #{ 48 | http_only => true, 49 | secure => true 50 | }). 51 | ---- 52 | 53 | == See also 54 | 55 | link:man:cow_cookie(3)[cow_cookie(3)], 56 | link:man:cow_cookie:parse_cookie(3)[cow_cookie:parse_cookie(3)], 57 | link:man:cow_cookie:parse_set_cookie(3)[cow_cookie:parse_set_cookie(3)], 58 | link:man:cow_cookie:cookie(3)[cow_cookie:cookie(3)] 59 | -------------------------------------------------------------------------------- /doc/src/manual/cowlib_app.asciidoc: -------------------------------------------------------------------------------- 1 | = cowlib(7) 2 | 3 | == Name 4 | 5 | cowlib - Support library for manipulating Web protocols 6 | 7 | == Description 8 | 9 | Cowlib provides libraries for parsing and building messages 10 | for various Web protocols, including HTTP/1.1, HTTP/2 and 11 | Websocket. 12 | 13 | It is optimized for completeness rather than speed. No value 14 | is ignored, they are all returned. 15 | 16 | == Modules 17 | 18 | * link:man:cow_cookie(3)[cow_cookie(3)] - Cookies 19 | 20 | == Dependencies 21 | 22 | * crypto - Crypto functions 23 | 24 | All these applications must be started before the `cowlib` 25 | application. To start Cowlib and all dependencies at once: 26 | 27 | [source,erlang] 28 | ---- 29 | {ok, _} = application:ensure_all_started(cowlib). 30 | ---- 31 | 32 | == Environment 33 | 34 | The `cowlib` application does not define any application 35 | environment configuration parameters. 36 | 37 | == See also 38 | 39 | link:man:cowboy(7)[cowboy(7)], 40 | link:man:gun(7)[gun(7)] 41 | -------------------------------------------------------------------------------- /ebin/cowlib.app: -------------------------------------------------------------------------------- 1 | {application, 'cowlib', [ 2 | {description, "Support library for manipulating Web protocols."}, 3 | {vsn, "2.15.0"}, 4 | {modules, ['cow_base64url','cow_cookie','cow_date','cow_deflate','cow_hpack','cow_http','cow_http1','cow_http2','cow_http2_machine','cow_http3','cow_http3_machine','cow_http_hd','cow_http_struct_hd','cow_http_te','cow_iolists','cow_link','cow_mimetypes','cow_multipart','cow_qpack','cow_qs','cow_spdy','cow_sse','cow_uri','cow_uri_template','cow_ws']}, 5 | {registered, []}, 6 | {applications, [kernel,stdlib,crypto]}, 7 | {optional_applications, []}, 8 | {env, []} 9 | ]}. -------------------------------------------------------------------------------- /include/cow_parse.hrl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) Loïc Hoguin 2 | %% 3 | %% Permission to use, copy, modify, and/or distribute this software for any 4 | %% purpose with or without fee is hereby granted, provided that the above 5 | %% copyright notice and this permission notice appear in all copies. 6 | %% 7 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | -ifndef(COW_PARSE_HRL). 16 | -define(COW_PARSE_HRL, 1). 17 | 18 | -define(IS_ALPHA(C), 19 | (C =:= $a) or (C =:= $b) or (C =:= $c) or (C =:= $d) or (C =:= $e) or 20 | (C =:= $f) or (C =:= $g) or (C =:= $h) or (C =:= $i) or (C =:= $j) or 21 | (C =:= $k) or (C =:= $l) or (C =:= $m) or (C =:= $n) or (C =:= $o) or 22 | (C =:= $p) or (C =:= $q) or (C =:= $r) or (C =:= $s) or (C =:= $t) or 23 | (C =:= $u) or (C =:= $v) or (C =:= $w) or (C =:= $x) or (C =:= $y) or 24 | (C =:= $z) or 25 | (C =:= $A) or (C =:= $B) or (C =:= $C) or (C =:= $D) or (C =:= $E) or 26 | (C =:= $F) or (C =:= $G) or (C =:= $H) or (C =:= $I) or (C =:= $J) or 27 | (C =:= $K) or (C =:= $L) or (C =:= $M) or (C =:= $N) or (C =:= $O) or 28 | (C =:= $P) or (C =:= $Q) or (C =:= $R) or (C =:= $S) or (C =:= $T) or 29 | (C =:= $U) or (C =:= $V) or (C =:= $W) or (C =:= $X) or (C =:= $Y) or 30 | (C =:= $Z) 31 | ). 32 | 33 | -define(IS_ALPHANUM(C), ?IS_ALPHA(C) or ?IS_DIGIT(C)). 34 | -define(IS_CHAR(C), C > 0, C < 128). 35 | 36 | -define(IS_DIGIT(C), 37 | (C =:= $0) or (C =:= $1) or (C =:= $2) or (C =:= $3) or (C =:= $4) or 38 | (C =:= $5) or (C =:= $6) or (C =:= $7) or (C =:= $8) or (C =:= $9)). 39 | 40 | -define(IS_ETAGC(C), C =:= 16#21; C >= 16#23, C =/= 16#7f). 41 | 42 | -define(IS_HEX(C), 43 | ?IS_DIGIT(C) or 44 | (C =:= $a) or (C =:= $b) or (C =:= $c) or 45 | (C =:= $d) or (C =:= $e) or (C =:= $f) or 46 | (C =:= $A) or (C =:= $B) or (C =:= $C) or 47 | (C =:= $D) or (C =:= $E) or (C =:= $F)). 48 | 49 | -define(IS_LHEX(C), 50 | ?IS_DIGIT(C) or 51 | (C =:= $a) or (C =:= $b) or (C =:= $c) or 52 | (C =:= $d) or (C =:= $e) or (C =:= $f)). 53 | 54 | -define(IS_TOKEN(C), 55 | ?IS_ALPHA(C) or ?IS_DIGIT(C) or 56 | (C =:= $!) or (C =:= $#) or (C =:= $$) or (C =:= $%) or (C =:= $&) or 57 | (C =:= $') or (C =:= $*) or (C =:= $+) or (C =:= $-) or (C =:= $.) or 58 | (C =:= $^) or (C =:= $_) or (C =:= $`) or (C =:= $|) or (C =:= $~)). 59 | 60 | -define(IS_TOKEN68(C), 61 | ?IS_ALPHA(C) or ?IS_DIGIT(C) or 62 | (C =:= $-) or (C =:= $.) or (C =:= $_) or 63 | (C =:= $~) or (C =:= $+) or (C =:= $/)). 64 | 65 | -define(IS_URI_UNRESERVED(C), 66 | ?IS_ALPHA(C) or ?IS_DIGIT(C) or 67 | (C =:= $-) or (C =:= $.) or (C =:= $_) or (C =:= $~)). 68 | 69 | -define(IS_URI_GEN_DELIMS(C), 70 | (C =:= $:) or (C =:= $/) or (C =:= $?) or (C =:= $#) or 71 | (C =:= $[) or (C =:= $]) or (C =:= $@)). 72 | 73 | -define(IS_URI_SUB_DELIMS(C), 74 | (C =:= $!) or (C =:= $$) or (C =:= $&) or (C =:= $') or 75 | (C =:= $() or (C =:= $)) or (C =:= $*) or (C =:= $+) or 76 | (C =:= $,) or (C =:= $;) or (C =:= $=)). 77 | 78 | -define(IS_VCHAR(C), C =:= $\t; C > 31, C < 127). 79 | -define(IS_VCHAR_OBS(C), C =:= $\t; C > 31, C =/= 127). 80 | -define(IS_WS(C), (C =:= $\s) or (C =:= $\t)). 81 | -define(IS_WS_COMMA(C), ?IS_WS(C) or (C =:= $,)). 82 | 83 | -endif. 84 | -------------------------------------------------------------------------------- /src/cow_base64url.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) Loïc Hoguin 2 | %% 3 | %% Permission to use, copy, modify, and/or distribute this software for any 4 | %% purpose with or without fee is hereby granted, provided that the above 5 | %% copyright notice and this permission notice appear in all copies. 6 | %% 7 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | %% This module implements "base64url" following the algorithm 16 | %% found in Appendix C of RFC7515. The option #{padding => false} 17 | %% must be given to reproduce this variant exactly. The default 18 | %% will leave the padding characters. 19 | -module(cow_base64url). 20 | 21 | -export([decode/1]). 22 | -export([decode/2]). 23 | -export([encode/1]). 24 | -export([encode/2]). 25 | 26 | -ifdef(TEST). 27 | -include_lib("proper/include/proper.hrl"). 28 | -endif. 29 | 30 | decode(Enc) -> 31 | decode(Enc, #{}). 32 | 33 | decode(Enc0, Opts) -> 34 | Enc1 = << << case C of 35 | $- -> $+; 36 | $_ -> $/; 37 | _ -> C 38 | end >> || << C >> <= Enc0 >>, 39 | Enc = case Opts of 40 | #{padding := false} -> 41 | case byte_size(Enc1) rem 4 of 42 | 0 -> Enc1; 43 | 2 -> << Enc1/binary, "==" >>; 44 | 3 -> << Enc1/binary, "=" >> 45 | end; 46 | _ -> 47 | Enc1 48 | end, 49 | base64:decode(Enc). 50 | 51 | encode(Dec) -> 52 | encode(Dec, #{}). 53 | 54 | encode(Dec, Opts) -> 55 | encode(base64:encode(Dec), Opts, <<>>). 56 | 57 | encode(<<$+, R/bits>>, Opts, Acc) -> encode(R, Opts, <>); 58 | encode(<<$/, R/bits>>, Opts, Acc) -> encode(R, Opts, <>); 59 | encode(<<$=, _/bits>>, #{padding := false}, Acc) -> Acc; 60 | encode(<>, Opts, Acc) -> encode(R, Opts, <>); 61 | encode(<<>>, _, Acc) -> Acc. 62 | 63 | -ifdef(TEST). 64 | 65 | rfc7515_test() -> 66 | Dec = <<3,236,255,224,193>>, 67 | Enc = <<"A-z_4ME">>, 68 | Pad = <<"A-z_4ME=">>, 69 | Dec = decode(<>), 70 | Dec = decode(Enc, #{padding => false}), 71 | Pad = encode(Dec), 72 | Enc = encode(Dec, #{padding => false}), 73 | ok. 74 | 75 | prop_identity() -> 76 | ?FORALL(B, binary(), B =:= decode(encode(B))). 77 | 78 | prop_identity_no_padding() -> 79 | ?FORALL(B, binary(), B =:= decode(encode(B, #{padding => false}), #{padding => false})). 80 | 81 | -endif. 82 | -------------------------------------------------------------------------------- /src/cow_cookie.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) Loïc Hoguin 2 | %% 3 | %% Permission to use, copy, modify, and/or distribute this software for any 4 | %% purpose with or without fee is hereby granted, provided that the above 5 | %% copyright notice and this permission notice appear in all copies. 6 | %% 7 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | -module(cow_cookie). 16 | 17 | -export([parse_cookie/1]). 18 | -export([parse_set_cookie/1]). 19 | -export([cookie/1]). 20 | -export([setcookie/3]). 21 | 22 | -type cookie_attrs() :: #{ 23 | expires => calendar:datetime(), 24 | max_age => calendar:datetime(), 25 | domain => binary(), 26 | path => binary(), 27 | secure => true, 28 | http_only => true, 29 | same_site => default | none | strict | lax 30 | }. 31 | -export_type([cookie_attrs/0]). 32 | 33 | -type cookie_opts() :: #{ 34 | domain => binary(), 35 | http_only => boolean(), 36 | max_age => non_neg_integer(), 37 | path => binary(), 38 | same_site => default | none | strict | lax, 39 | secure => boolean() 40 | }. 41 | -export_type([cookie_opts/0]). 42 | 43 | -include("cow_inline.hrl"). 44 | 45 | %% Cookie header. 46 | 47 | -spec parse_cookie(binary()) -> [{binary(), binary()}]. 48 | parse_cookie(Cookie) -> 49 | parse_cookie(Cookie, []). 50 | 51 | parse_cookie(<<>>, Acc) -> 52 | lists:reverse(Acc); 53 | parse_cookie(<< $\s, Rest/binary >>, Acc) -> 54 | parse_cookie(Rest, Acc); 55 | parse_cookie(<< $\t, Rest/binary >>, Acc) -> 56 | parse_cookie(Rest, Acc); 57 | parse_cookie(<< $,, Rest/binary >>, Acc) -> 58 | parse_cookie(Rest, Acc); 59 | parse_cookie(<< $;, Rest/binary >>, Acc) -> 60 | parse_cookie(Rest, Acc); 61 | parse_cookie(Cookie, Acc) -> 62 | parse_cookie_name(Cookie, Acc, <<>>). 63 | 64 | parse_cookie_name(<<>>, Acc, Name) -> 65 | lists:reverse([{<<>>, parse_cookie_trim(Name)}|Acc]); 66 | parse_cookie_name(<< $=, _/binary >>, _, <<>>) -> 67 | error(badarg); 68 | parse_cookie_name(<< $=, Rest/binary >>, Acc, Name) -> 69 | parse_cookie_value(Rest, Acc, Name, <<>>); 70 | parse_cookie_name(<< $,, _/binary >>, _, _) -> 71 | error(badarg); 72 | parse_cookie_name(<< $;, Rest/binary >>, Acc, Name) -> 73 | parse_cookie(Rest, [{<<>>, parse_cookie_trim(Name)}|Acc]); 74 | parse_cookie_name(<< $\t, _/binary >>, _, _) -> 75 | error(badarg); 76 | parse_cookie_name(<< $\r, _/binary >>, _, _) -> 77 | error(badarg); 78 | parse_cookie_name(<< $\n, _/binary >>, _, _) -> 79 | error(badarg); 80 | parse_cookie_name(<< $\013, _/binary >>, _, _) -> 81 | error(badarg); 82 | parse_cookie_name(<< $\014, _/binary >>, _, _) -> 83 | error(badarg); 84 | parse_cookie_name(<< C, Rest/binary >>, Acc, Name) -> 85 | parse_cookie_name(Rest, Acc, << Name/binary, C >>). 86 | 87 | parse_cookie_value(<<>>, Acc, Name, Value) -> 88 | lists:reverse([{Name, parse_cookie_trim(Value)}|Acc]); 89 | parse_cookie_value(<< $;, Rest/binary >>, Acc, Name, Value) -> 90 | parse_cookie(Rest, [{Name, parse_cookie_trim(Value)}|Acc]); 91 | parse_cookie_value(<< $\t, _/binary >>, _, _, _) -> 92 | error(badarg); 93 | parse_cookie_value(<< $\r, _/binary >>, _, _, _) -> 94 | error(badarg); 95 | parse_cookie_value(<< $\n, _/binary >>, _, _, _) -> 96 | error(badarg); 97 | parse_cookie_value(<< $\013, _/binary >>, _, _, _) -> 98 | error(badarg); 99 | parse_cookie_value(<< $\014, _/binary >>, _, _, _) -> 100 | error(badarg); 101 | parse_cookie_value(<< C, Rest/binary >>, Acc, Name, Value) -> 102 | parse_cookie_value(Rest, Acc, Name, << Value/binary, C >>). 103 | 104 | parse_cookie_trim(Value = <<>>) -> 105 | Value; 106 | parse_cookie_trim(Value) -> 107 | case binary:last(Value) of 108 | $\s -> 109 | Size = byte_size(Value) - 1, 110 | << Value2:Size/binary, _ >> = Value, 111 | parse_cookie_trim(Value2); 112 | _ -> 113 | Value 114 | end. 115 | 116 | -ifdef(TEST). 117 | parse_cookie_test_() -> 118 | %% {Value, Result}. 119 | Tests = [ 120 | {<<"name=value; name2=value2">>, [ 121 | {<<"name">>, <<"value">>}, 122 | {<<"name2">>, <<"value2">>} 123 | ]}, 124 | %% Space in value. 125 | {<<"foo=Thu Jul 11 2013 15:38:43 GMT+0400 (MSK)">>, 126 | [{<<"foo">>, <<"Thu Jul 11 2013 15:38:43 GMT+0400 (MSK)">>}]}, 127 | %% Comma in value. Google Analytics sets that kind of cookies. 128 | {<<"refk=sOUZDzq2w2; sk=B602064E0139D842D620C7569640DBB4C81C45080651" 129 | "9CC124EF794863E10E80; __utma=64249653.825741573.1380181332.1400" 130 | "015657.1400019557.703; __utmb=64249653.1.10.1400019557; __utmc=" 131 | "64249653; __utmz=64249653.1400019557.703.13.utmcsr=bluesky.chic" 132 | "agotribune.com|utmccn=(referral)|utmcmd=referral|utmcct=/origin" 133 | "als/chi-12-indispensable-digital-tools-bsi,0,0.storygallery">>, [ 134 | {<<"refk">>, <<"sOUZDzq2w2">>}, 135 | {<<"sk">>, <<"B602064E0139D842D620C7569640DBB4C81C45080651" 136 | "9CC124EF794863E10E80">>}, 137 | {<<"__utma">>, <<"64249653.825741573.1380181332.1400" 138 | "015657.1400019557.703">>}, 139 | {<<"__utmb">>, <<"64249653.1.10.1400019557">>}, 140 | {<<"__utmc">>, <<"64249653">>}, 141 | {<<"__utmz">>, <<"64249653.1400019557.703.13.utmcsr=bluesky.chic" 142 | "agotribune.com|utmccn=(referral)|utmcmd=referral|utmcct=/origin" 143 | "als/chi-12-indispensable-digital-tools-bsi,0,0.storygallery">>} 144 | ]}, 145 | %% Potential edge cases (initially from Mochiweb). 146 | {<<"foo=\\x">>, [{<<"foo">>, <<"\\x">>}]}, 147 | {<<"foo=;bar=">>, [{<<"foo">>, <<>>}, {<<"bar">>, <<>>}]}, 148 | {<<"foo=\\\";;bar=good ">>, 149 | [{<<"foo">>, <<"\\\"">>}, {<<"bar">>, <<"good">>}]}, 150 | {<<"foo=\"\\\";bar=good">>, 151 | [{<<"foo">>, <<"\"\\\"">>}, {<<"bar">>, <<"good">>}]}, 152 | {<<>>, []}, %% Flash player. 153 | {<<"foo=bar , baz=wibble ">>, [{<<"foo">>, <<"bar , baz=wibble">>}]}, 154 | %% Technically invalid, but seen in the wild 155 | {<<"foo">>, [{<<>>, <<"foo">>}]}, 156 | {<<"foo ">>, [{<<>>, <<"foo">>}]}, 157 | {<<"foo;">>, [{<<>>, <<"foo">>}]}, 158 | {<<"bar;foo=1">>, [{<<>>, <<"bar">>}, {<<"foo">>, <<"1">>}]} 159 | ], 160 | [{V, fun() -> R = parse_cookie(V) end} || {V, R} <- Tests]. 161 | 162 | parse_cookie_error_test_() -> 163 | %% Value. 164 | Tests = [ 165 | <<"=">> 166 | ], 167 | [{V, fun() -> {'EXIT', {badarg, _}} = (catch parse_cookie(V)) end} || V <- Tests]. 168 | -endif. 169 | 170 | %% Set-Cookie header. 171 | 172 | -spec parse_set_cookie(binary()) 173 | -> {ok, binary(), binary(), cookie_attrs()} 174 | | ignore. 175 | parse_set_cookie(SetCookie) -> 176 | case has_non_ws_ctl(SetCookie) of 177 | true -> 178 | ignore; 179 | false -> 180 | {NameValuePair, UnparsedAttrs} = take_until_semicolon(SetCookie, <<>>), 181 | {Name, Value} = case binary:split(NameValuePair, <<$=>>) of 182 | [Value0] -> {<<>>, trim(Value0)}; 183 | [Name0, Value0] -> {trim(Name0), trim(Value0)} 184 | end, 185 | case {Name, Value} of 186 | {<<>>, <<>>} -> 187 | ignore; 188 | _ -> 189 | Attrs = parse_set_cookie_attrs(UnparsedAttrs, #{}), 190 | {ok, Name, Value, Attrs} 191 | end 192 | end. 193 | 194 | has_non_ws_ctl(<<>>) -> 195 | false; 196 | has_non_ws_ctl(<>) -> 197 | if 198 | C =< 16#08 -> true; 199 | C >= 16#0A, C =< 16#1F -> true; 200 | C =:= 16#7F -> true; 201 | true -> has_non_ws_ctl(R) 202 | end. 203 | 204 | parse_set_cookie_attrs(<<>>, Attrs) -> 205 | Attrs; 206 | parse_set_cookie_attrs(<<$;,Rest0/bits>>, Attrs) -> 207 | {Av, Rest} = take_until_semicolon(Rest0, <<>>), 208 | {Name, Value} = case binary:split(Av, <<$=>>) of 209 | [Name0] -> {trim(Name0), <<>>}; 210 | [Name0, Value0] -> {trim(Name0), trim(Value0)} 211 | end, 212 | if 213 | byte_size(Value) > 1024 -> 214 | parse_set_cookie_attrs(Rest, Attrs); 215 | true -> 216 | case parse_set_cookie_attr(?LOWER(Name), Value) of 217 | {ok, AttrName, AttrValue} -> 218 | parse_set_cookie_attrs(Rest, Attrs#{AttrName => AttrValue}); 219 | {ignore, AttrName} -> 220 | parse_set_cookie_attrs(Rest, maps:remove(AttrName, Attrs)); 221 | ignore -> 222 | parse_set_cookie_attrs(Rest, Attrs) 223 | end 224 | end. 225 | 226 | take_until_semicolon(Rest = <<$;,_/bits>>, Acc) -> {Acc, Rest}; 227 | take_until_semicolon(<>, Acc) -> take_until_semicolon(R, <>); 228 | take_until_semicolon(<<>>, Acc) -> {Acc, <<>>}. 229 | 230 | trim(String) -> 231 | string:trim(String, both, [$\s, $\t]). 232 | 233 | parse_set_cookie_attr(<<"expires">>, Value) -> 234 | try cow_date:parse_date(Value) of 235 | DateTime -> 236 | {ok, expires, DateTime} 237 | catch _:_ -> 238 | ignore 239 | end; 240 | parse_set_cookie_attr(<<"max-age">>, Value) -> 241 | try binary_to_integer(Value) of 242 | MaxAge when MaxAge =< 0 -> 243 | %% Year 0 corresponds to 1 BC. 244 | {ok, max_age, {{0, 1, 1}, {0, 0, 0}}}; 245 | MaxAge -> 246 | CurrentTime = erlang:universaltime(), 247 | {ok, max_age, calendar:gregorian_seconds_to_datetime( 248 | calendar:datetime_to_gregorian_seconds(CurrentTime) + MaxAge)} 249 | catch _:_ -> 250 | ignore 251 | end; 252 | parse_set_cookie_attr(<<"domain">>, Value) -> 253 | case Value of 254 | <<>> -> 255 | ignore; 256 | <<".",Rest/bits>> -> 257 | {ok, domain, ?LOWER(Rest)}; 258 | _ -> 259 | {ok, domain, ?LOWER(Value)} 260 | end; 261 | parse_set_cookie_attr(<<"path">>, Value) -> 262 | case Value of 263 | <<"/",_/bits>> -> 264 | {ok, path, Value}; 265 | %% When the path is not absolute, or the path is empty, the default-path will be used. 266 | %% Note that the default-path is also used when there are no path attributes, 267 | %% so we are simply ignoring the attribute here. 268 | _ -> 269 | {ignore, path} 270 | end; 271 | parse_set_cookie_attr(<<"secure">>, _) -> 272 | {ok, secure, true}; 273 | parse_set_cookie_attr(<<"httponly">>, _) -> 274 | {ok, http_only, true}; 275 | parse_set_cookie_attr(<<"samesite">>, Value) -> 276 | case ?LOWER(Value) of 277 | <<"none">> -> 278 | {ok, same_site, none}; 279 | <<"strict">> -> 280 | {ok, same_site, strict}; 281 | <<"lax">> -> 282 | {ok, same_site, lax}; 283 | %% Unknown values and lack of value are equivalent. 284 | _ -> 285 | {ok, same_site, default} 286 | end; 287 | parse_set_cookie_attr(_, _) -> 288 | ignore. 289 | 290 | -ifdef(TEST). 291 | parse_set_cookie_test_() -> 292 | Tests = [ 293 | {<<"a=b">>, {ok, <<"a">>, <<"b">>, #{}}}, 294 | {<<"a=b; Secure">>, {ok, <<"a">>, <<"b">>, #{secure => true}}}, 295 | {<<"a=b; HttpOnly">>, {ok, <<"a">>, <<"b">>, #{http_only => true}}}, 296 | {<<"a=b; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Expires=Wed, 21 Oct 2015 07:29:00 GMT">>, 297 | {ok, <<"a">>, <<"b">>, #{expires => {{2015,10,21},{7,29,0}}}}}, 298 | {<<"a=b; Max-Age=999; Max-Age=0">>, 299 | {ok, <<"a">>, <<"b">>, #{max_age => {{0,1,1},{0,0,0}}}}}, 300 | {<<"a=b; Domain=example.org; Domain=foo.example.org">>, 301 | {ok, <<"a">>, <<"b">>, #{domain => <<"foo.example.org">>}}}, 302 | {<<"a=b; Path=/path/to/resource; Path=/">>, 303 | {ok, <<"a">>, <<"b">>, #{path => <<"/">>}}}, 304 | {<<"a=b; SameSite=UnknownValue">>, {ok, <<"a">>, <<"b">>, #{same_site => default}}}, 305 | {<<"a=b; SameSite=None">>, {ok, <<"a">>, <<"b">>, #{same_site => none}}}, 306 | {<<"a=b; SameSite=Lax">>, {ok, <<"a">>, <<"b">>, #{same_site => lax}}}, 307 | {<<"a=b; SameSite=Strict">>, {ok, <<"a">>, <<"b">>, #{same_site => strict}}}, 308 | {<<"a=b; SameSite=Lax; SameSite=Strict">>, 309 | {ok, <<"a">>, <<"b">>, #{same_site => strict}}} 310 | ], 311 | [{SetCookie, fun() -> Res = parse_set_cookie(SetCookie) end} 312 | || {SetCookie, Res} <- Tests]. 313 | -endif. 314 | 315 | %% Build a cookie header. 316 | 317 | -spec cookie([{iodata(), iodata()}]) -> iolist(). 318 | cookie([]) -> 319 | []; 320 | cookie([{<<>>, Value}]) -> 321 | [Value]; 322 | cookie([{Name, Value}]) -> 323 | [Name, $=, Value]; 324 | cookie([{<<>>, Value}|Tail]) -> 325 | [Value, $;, $\s|cookie(Tail)]; 326 | cookie([{Name, Value}|Tail]) -> 327 | [Name, $=, Value, $;, $\s|cookie(Tail)]. 328 | 329 | -ifdef(TEST). 330 | cookie_test_() -> 331 | Tests = [ 332 | {[], <<>>}, 333 | {[{<<"a">>, <<"b">>}], <<"a=b">>}, 334 | {[{<<"a">>, <<"b">>}, {<<"c">>, <<"d">>}], <<"a=b; c=d">>}, 335 | {[{<<>>, <<"b">>}, {<<"c">>, <<"d">>}], <<"b; c=d">>}, 336 | {[{<<"a">>, <<"b">>}, {<<>>, <<"d">>}], <<"a=b; d">>} 337 | ], 338 | [{Res, fun() -> Res = iolist_to_binary(cookie(Cookies)) end} 339 | || {Cookies, Res} <- Tests]. 340 | -endif. 341 | 342 | %% Convert a cookie name, value and options to its iodata form. 343 | %% 344 | %% Initially from Mochiweb: 345 | %% * Copyright 2007 Mochi Media, Inc. 346 | %% Initial binary implementation: 347 | %% * Copyright 2011 Thomas Burdick 348 | %% 349 | %% @todo Rename the function to set_cookie eventually. 350 | 351 | -spec setcookie(iodata(), iodata(), cookie_opts()) -> iolist(). 352 | setcookie(Name, Value, Opts) -> 353 | nomatch = binary:match(iolist_to_binary(Name), [<<$=>>, <<$,>>, <<$;>>, 354 | <<$\s>>, <<$\t>>, <<$\r>>, <<$\n>>, <<$\013>>, <<$\014>>]), 355 | nomatch = binary:match(iolist_to_binary(Value), [<<$,>>, <<$;>>, 356 | <<$\s>>, <<$\t>>, <<$\r>>, <<$\n>>, <<$\013>>, <<$\014>>]), 357 | [Name, <<"=">>, Value, attributes(maps:to_list(Opts))]. 358 | 359 | attributes([]) -> []; 360 | attributes([{domain, Domain}|Tail]) -> [<<"; Domain=">>, Domain|attributes(Tail)]; 361 | attributes([{http_only, false}|Tail]) -> attributes(Tail); 362 | attributes([{http_only, true}|Tail]) -> [<<"; HttpOnly">>|attributes(Tail)]; 363 | %% MSIE requires an Expires date in the past to delete a cookie. 364 | attributes([{max_age, 0}|Tail]) -> 365 | [<<"; Expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0">>|attributes(Tail)]; 366 | attributes([{max_age, MaxAge}|Tail]) when is_integer(MaxAge), MaxAge > 0 -> 367 | Secs = calendar:datetime_to_gregorian_seconds(calendar:universal_time()), 368 | Expires = cow_date:rfc2109(calendar:gregorian_seconds_to_datetime(Secs + MaxAge)), 369 | [<<"; Expires=">>, Expires, <<"; Max-Age=">>, integer_to_list(MaxAge)|attributes(Tail)]; 370 | attributes([Opt={max_age, _}|_]) -> 371 | error({badarg, Opt}); 372 | attributes([{path, Path}|Tail]) -> [<<"; Path=">>, Path|attributes(Tail)]; 373 | attributes([{secure, false}|Tail]) -> attributes(Tail); 374 | attributes([{secure, true}|Tail]) -> [<<"; Secure">>|attributes(Tail)]; 375 | attributes([{same_site, default}|Tail]) -> attributes(Tail); 376 | attributes([{same_site, none}|Tail]) -> [<<"; SameSite=None">>|attributes(Tail)]; 377 | attributes([{same_site, lax}|Tail]) -> [<<"; SameSite=Lax">>|attributes(Tail)]; 378 | attributes([{same_site, strict}|Tail]) -> [<<"; SameSite=Strict">>|attributes(Tail)]; 379 | %% Skip unknown options. 380 | attributes([_|Tail]) -> attributes(Tail). 381 | 382 | -ifdef(TEST). 383 | setcookie_test_() -> 384 | %% {Name, Value, Opts, Result} 385 | Tests = [ 386 | {<<"Customer">>, <<"WILE_E_COYOTE">>, 387 | #{http_only => true, domain => <<"acme.com">>}, 388 | <<"Customer=WILE_E_COYOTE; " 389 | "Domain=acme.com; HttpOnly">>}, 390 | {<<"Customer">>, <<"WILE_E_COYOTE">>, 391 | #{path => <<"/acme">>}, 392 | <<"Customer=WILE_E_COYOTE; Path=/acme">>}, 393 | {<<"Customer">>, <<"WILE_E_COYOTE">>, 394 | #{secure => true}, 395 | <<"Customer=WILE_E_COYOTE; Secure">>}, 396 | {<<"Customer">>, <<"WILE_E_COYOTE">>, 397 | #{secure => false, http_only => false}, 398 | <<"Customer=WILE_E_COYOTE">>}, 399 | {<<"Customer">>, <<"WILE_E_COYOTE">>, 400 | #{same_site => default}, 401 | <<"Customer=WILE_E_COYOTE">>}, 402 | {<<"Customer">>, <<"WILE_E_COYOTE">>, 403 | #{same_site => none}, 404 | <<"Customer=WILE_E_COYOTE; SameSite=None">>}, 405 | {<<"Customer">>, <<"WILE_E_COYOTE">>, 406 | #{same_site => lax}, 407 | <<"Customer=WILE_E_COYOTE; SameSite=Lax">>}, 408 | {<<"Customer">>, <<"WILE_E_COYOTE">>, 409 | #{same_site => strict}, 410 | <<"Customer=WILE_E_COYOTE; SameSite=Strict">>}, 411 | {<<"Customer">>, <<"WILE_E_COYOTE">>, 412 | #{path => <<"/acme">>, badoption => <<"negatory">>}, 413 | <<"Customer=WILE_E_COYOTE; Path=/acme">>} 414 | ], 415 | [{R, fun() -> R = iolist_to_binary(setcookie(N, V, O)) end} 416 | || {N, V, O, R} <- Tests]. 417 | 418 | setcookie_max_age_test() -> 419 | F = fun(N, V, O) -> 420 | binary:split(iolist_to_binary( 421 | setcookie(N, V, O)), <<";">>, [global]) 422 | end, 423 | [<<"Customer=WILE_E_COYOTE">>, 424 | <<" Expires=", _/binary>>, 425 | <<" Max-Age=111">>, 426 | <<" Secure">>] = F(<<"Customer">>, <<"WILE_E_COYOTE">>, 427 | #{max_age => 111, secure => true}), 428 | case catch F(<<"Customer">>, <<"WILE_E_COYOTE">>, #{max_age => -111}) of 429 | {'EXIT', {{badarg, {max_age, -111}}, _}} -> ok 430 | end, 431 | [<<"Customer=WILE_E_COYOTE">>, 432 | <<" Expires=", _/binary>>, 433 | <<" Max-Age=86417">>] = F(<<"Customer">>, <<"WILE_E_COYOTE">>, 434 | #{max_age => 86417}), 435 | ok. 436 | 437 | setcookie_failures_test_() -> 438 | F = fun(N, V) -> 439 | try setcookie(N, V, #{}) of 440 | _ -> 441 | false 442 | catch _:_ -> 443 | true 444 | end 445 | end, 446 | Tests = [ 447 | {<<"Na=me">>, <<"Value">>}, 448 | {<<"Name;">>, <<"Value">>}, 449 | {<<"\r\name">>, <<"Value">>}, 450 | {<<"Name">>, <<"Value;">>}, 451 | {<<"Name">>, <<"\value">>} 452 | ], 453 | [{iolist_to_binary(io_lib:format("{~p, ~p} failure", [N, V])), 454 | fun() -> true = F(N, V) end} 455 | || {N, V} <- Tests]. 456 | -endif. 457 | -------------------------------------------------------------------------------- /src/cow_date.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) Loïc Hoguin 2 | %% 3 | %% Permission to use, copy, modify, and/or distribute this software for any 4 | %% purpose with or without fee is hereby granted, provided that the above 5 | %% copyright notice and this permission notice appear in all copies. 6 | %% 7 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | -module(cow_date). 16 | 17 | -export([parse_date/1]). 18 | -export([rfc1123/1]). 19 | -export([rfc2109/1]). 20 | -export([rfc7231/1]). 21 | 22 | -ifdef(TEST). 23 | -include_lib("proper/include/proper.hrl"). 24 | -endif. 25 | 26 | %% @doc Parse the HTTP date (IMF-fixdate, rfc850, asctime). 27 | 28 | -define(DIGITS(A, B), ((A - $0) * 10 + (B - $0))). 29 | -define(DIGITS(A, B, C, D), ((A - $0) * 1000 + (B - $0) * 100 + (C - $0) * 10 + (D - $0))). 30 | 31 | -spec parse_date(binary()) -> calendar:datetime(). 32 | parse_date(DateBin) -> 33 | Date = {{_, _, D}, {H, M, S}} = http_date(DateBin), 34 | true = D >= 0 andalso D =< 31, 35 | true = H >= 0 andalso H =< 23, 36 | true = M >= 0 andalso M =< 59, 37 | true = S >= 0 andalso S =< 60, %% Leap second. 38 | Date. 39 | 40 | http_date(<<"Mon, ", D1, D2, " ", R/bits >>) -> fixdate(R, ?DIGITS(D1, D2)); 41 | http_date(<<"Tue, ", D1, D2, " ", R/bits >>) -> fixdate(R, ?DIGITS(D1, D2)); 42 | http_date(<<"Wed, ", D1, D2, " ", R/bits >>) -> fixdate(R, ?DIGITS(D1, D2)); 43 | http_date(<<"Thu, ", D1, D2, " ", R/bits >>) -> fixdate(R, ?DIGITS(D1, D2)); 44 | http_date(<<"Fri, ", D1, D2, " ", R/bits >>) -> fixdate(R, ?DIGITS(D1, D2)); 45 | http_date(<<"Sat, ", D1, D2, " ", R/bits >>) -> fixdate(R, ?DIGITS(D1, D2)); 46 | http_date(<<"Sun, ", D1, D2, " ", R/bits >>) -> fixdate(R, ?DIGITS(D1, D2)); 47 | http_date(<<"Monday, ", D1, D2, "-", R/bits >>) -> rfc850_date(R, ?DIGITS(D1, D2)); 48 | http_date(<<"Tuesday, ", D1, D2, "-", R/bits >>) -> rfc850_date(R, ?DIGITS(D1, D2)); 49 | http_date(<<"Wednesday, ", D1, D2, "-", R/bits >>) -> rfc850_date(R, ?DIGITS(D1, D2)); 50 | http_date(<<"Thursday, ", D1, D2, "-", R/bits >>) -> rfc850_date(R, ?DIGITS(D1, D2)); 51 | http_date(<<"Friday, ", D1, D2, "-", R/bits >>) -> rfc850_date(R, ?DIGITS(D1, D2)); 52 | http_date(<<"Saturday, ", D1, D2, "-", R/bits >>) -> rfc850_date(R, ?DIGITS(D1, D2)); 53 | http_date(<<"Sunday, ", D1, D2, "-", R/bits >>) -> rfc850_date(R, ?DIGITS(D1, D2)); 54 | http_date(<<"Mon ", R/bits >>) -> asctime_date(R); 55 | http_date(<<"Tue ", R/bits >>) -> asctime_date(R); 56 | http_date(<<"Wed ", R/bits >>) -> asctime_date(R); 57 | http_date(<<"Thu ", R/bits >>) -> asctime_date(R); 58 | http_date(<<"Fri ", R/bits >>) -> asctime_date(R); 59 | http_date(<<"Sat ", R/bits >>) -> asctime_date(R); 60 | http_date(<<"Sun ", R/bits >>) -> asctime_date(R). 61 | 62 | fixdate(<<"Jan ", Y1, Y2, Y3, Y4, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) -> 63 | {{?DIGITS(Y1, Y2, Y3, Y4), 1, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 64 | fixdate(<<"Feb ", Y1, Y2, Y3, Y4, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) -> 65 | {{?DIGITS(Y1, Y2, Y3, Y4), 2, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 66 | fixdate(<<"Mar ", Y1, Y2, Y3, Y4, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) -> 67 | {{?DIGITS(Y1, Y2, Y3, Y4), 3, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 68 | fixdate(<<"Apr ", Y1, Y2, Y3, Y4, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) -> 69 | {{?DIGITS(Y1, Y2, Y3, Y4), 4, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 70 | fixdate(<<"May ", Y1, Y2, Y3, Y4, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) -> 71 | {{?DIGITS(Y1, Y2, Y3, Y4), 5, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 72 | fixdate(<<"Jun ", Y1, Y2, Y3, Y4, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) -> 73 | {{?DIGITS(Y1, Y2, Y3, Y4), 6, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 74 | fixdate(<<"Jul ", Y1, Y2, Y3, Y4, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) -> 75 | {{?DIGITS(Y1, Y2, Y3, Y4), 7, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 76 | fixdate(<<"Aug ", Y1, Y2, Y3, Y4, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) -> 77 | {{?DIGITS(Y1, Y2, Y3, Y4), 8, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 78 | fixdate(<<"Sep ", Y1, Y2, Y3, Y4, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) -> 79 | {{?DIGITS(Y1, Y2, Y3, Y4), 9, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 80 | fixdate(<<"Oct ", Y1, Y2, Y3, Y4, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) -> 81 | {{?DIGITS(Y1, Y2, Y3, Y4), 10, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 82 | fixdate(<<"Nov ", Y1, Y2, Y3, Y4, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) -> 83 | {{?DIGITS(Y1, Y2, Y3, Y4), 11, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 84 | fixdate(<<"Dec ", Y1, Y2, Y3, Y4, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) -> 85 | {{?DIGITS(Y1, Y2, Y3, Y4), 12, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}. 86 | 87 | rfc850_date(<<"Jan-", Y1, Y2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) -> 88 | {{rfc850_year(?DIGITS(Y1, Y2)), 1, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 89 | rfc850_date(<<"Feb-", Y1, Y2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) -> 90 | {{rfc850_year(?DIGITS(Y1, Y2)), 2, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 91 | rfc850_date(<<"Mar-", Y1, Y2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) -> 92 | {{rfc850_year(?DIGITS(Y1, Y2)), 3, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 93 | rfc850_date(<<"Apr-", Y1, Y2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) -> 94 | {{rfc850_year(?DIGITS(Y1, Y2)), 4, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 95 | rfc850_date(<<"May-", Y1, Y2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) -> 96 | {{rfc850_year(?DIGITS(Y1, Y2)), 5, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 97 | rfc850_date(<<"Jun-", Y1, Y2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) -> 98 | {{rfc850_year(?DIGITS(Y1, Y2)), 6, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 99 | rfc850_date(<<"Jul-", Y1, Y2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) -> 100 | {{rfc850_year(?DIGITS(Y1, Y2)), 7, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 101 | rfc850_date(<<"Aug-", Y1, Y2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) -> 102 | {{rfc850_year(?DIGITS(Y1, Y2)), 8, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 103 | rfc850_date(<<"Sep-", Y1, Y2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) -> 104 | {{rfc850_year(?DIGITS(Y1, Y2)), 9, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 105 | rfc850_date(<<"Oct-", Y1, Y2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) -> 106 | {{rfc850_year(?DIGITS(Y1, Y2)), 10, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 107 | rfc850_date(<<"Nov-", Y1, Y2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) -> 108 | {{rfc850_year(?DIGITS(Y1, Y2)), 11, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 109 | rfc850_date(<<"Dec-", Y1, Y2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) -> 110 | {{rfc850_year(?DIGITS(Y1, Y2)), 12, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}. 111 | 112 | rfc850_year(Y) when Y > 50 -> Y + 1900; 113 | rfc850_year(Y) -> Y + 2000. 114 | 115 | asctime_date(<<"Jan ", D1, D2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " ", Y1, Y2, Y3, Y4 >>) -> 116 | {{?DIGITS(Y1, Y2, Y3, Y4), 1, asctime_day(D1, D2)}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 117 | asctime_date(<<"Feb ", D1, D2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " ", Y1, Y2, Y3, Y4 >>) -> 118 | {{?DIGITS(Y1, Y2, Y3, Y4), 2, asctime_day(D1, D2)}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 119 | asctime_date(<<"Mar ", D1, D2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " ", Y1, Y2, Y3, Y4 >>) -> 120 | {{?DIGITS(Y1, Y2, Y3, Y4), 3, asctime_day(D1, D2)}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 121 | asctime_date(<<"Apr ", D1, D2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " ", Y1, Y2, Y3, Y4 >>) -> 122 | {{?DIGITS(Y1, Y2, Y3, Y4), 4, asctime_day(D1, D2)}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 123 | asctime_date(<<"May ", D1, D2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " ", Y1, Y2, Y3, Y4 >>) -> 124 | {{?DIGITS(Y1, Y2, Y3, Y4), 5, asctime_day(D1, D2)}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 125 | asctime_date(<<"Jun ", D1, D2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " ", Y1, Y2, Y3, Y4 >>) -> 126 | {{?DIGITS(Y1, Y2, Y3, Y4), 6, asctime_day(D1, D2)}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 127 | asctime_date(<<"Jul ", D1, D2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " ", Y1, Y2, Y3, Y4 >>) -> 128 | {{?DIGITS(Y1, Y2, Y3, Y4), 7, asctime_day(D1, D2)}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 129 | asctime_date(<<"Aug ", D1, D2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " ", Y1, Y2, Y3, Y4 >>) -> 130 | {{?DIGITS(Y1, Y2, Y3, Y4), 8, asctime_day(D1, D2)}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 131 | asctime_date(<<"Sep ", D1, D2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " ", Y1, Y2, Y3, Y4 >>) -> 132 | {{?DIGITS(Y1, Y2, Y3, Y4), 9, asctime_day(D1, D2)}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 133 | asctime_date(<<"Oct ", D1, D2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " ", Y1, Y2, Y3, Y4 >>) -> 134 | {{?DIGITS(Y1, Y2, Y3, Y4), 10, asctime_day(D1, D2)}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 135 | asctime_date(<<"Nov ", D1, D2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " ", Y1, Y2, Y3, Y4 >>) -> 136 | {{?DIGITS(Y1, Y2, Y3, Y4), 11, asctime_day(D1, D2)}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}; 137 | asctime_date(<<"Dec ", D1, D2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " ", Y1, Y2, Y3, Y4 >>) -> 138 | {{?DIGITS(Y1, Y2, Y3, Y4), 12, asctime_day(D1, D2)}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}. 139 | 140 | asctime_day($\s, D2) -> (D2 - $0); 141 | asctime_day(D1, D2) -> (D1 - $0) * 10 + (D2 - $0). 142 | 143 | -ifdef(TEST). 144 | day_name() -> oneof(["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]). 145 | day_name_l() -> oneof(["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]). 146 | year() -> integer(1951, 2050). 147 | month() -> integer(1, 12). 148 | day() -> integer(1, 31). 149 | hour() -> integer(0, 23). 150 | minute() -> integer(0, 59). 151 | second() -> integer(0, 60). 152 | 153 | fixdate_gen() -> 154 | ?LET({DayName, Y, Mo, D, H, Mi, S}, 155 | {day_name(), year(), month(), day(), hour(), minute(), second()}, 156 | {{{Y, Mo, D}, {H, Mi, S}}, 157 | list_to_binary([DayName, ", ", pad_int(D), " ", month(Mo), " ", integer_to_binary(Y), 158 | " ", pad_int(H), ":", pad_int(Mi), ":", pad_int(S), " GMT"])}). 159 | 160 | rfc850_gen() -> 161 | ?LET({DayName, Y, Mo, D, H, Mi, S}, 162 | {day_name_l(), year(), month(), day(), hour(), minute(), second()}, 163 | {{{Y, Mo, D}, {H, Mi, S}}, 164 | list_to_binary([DayName, ", ", pad_int(D), "-", month(Mo), "-", pad_int(Y rem 100), 165 | " ", pad_int(H), ":", pad_int(Mi), ":", pad_int(S), " GMT"])}). 166 | 167 | asctime_gen() -> 168 | ?LET({DayName, Y, Mo, D, H, Mi, S}, 169 | {day_name(), year(), month(), day(), hour(), minute(), second()}, 170 | {{{Y, Mo, D}, {H, Mi, S}}, 171 | list_to_binary([DayName, " ", month(Mo), " ", 172 | if D < 10 -> << $\s, (D + $0) >>; true -> integer_to_binary(D) end, 173 | " ", pad_int(H), ":", pad_int(Mi), ":", pad_int(S), " ", integer_to_binary(Y)])}). 174 | 175 | prop_http_date() -> 176 | ?FORALL({Date, DateBin}, 177 | oneof([fixdate_gen(), rfc850_gen(), asctime_gen()]), 178 | Date =:= parse_date(DateBin)). 179 | 180 | http_date_test_() -> 181 | Tests = [ 182 | {<<"Sun, 06 Nov 1994 08:49:37 GMT">>, {{1994, 11, 6}, {8, 49, 37}}}, 183 | {<<"Sunday, 06-Nov-94 08:49:37 GMT">>, {{1994, 11, 6}, {8, 49, 37}}}, 184 | {<<"Sun Nov 6 08:49:37 1994">>, {{1994, 11, 6}, {8, 49, 37}}} 185 | ], 186 | [{V, fun() -> R = http_date(V) end} || {V, R} <- Tests]. 187 | 188 | horse_http_date_fixdate() -> 189 | horse:repeat(200000, 190 | http_date(<<"Sun, 06 Nov 1994 08:49:37 GMT">>) 191 | ). 192 | 193 | horse_http_date_rfc850() -> 194 | horse:repeat(200000, 195 | http_date(<<"Sunday, 06-Nov-94 08:49:37 GMT">>) 196 | ). 197 | 198 | horse_http_date_asctime() -> 199 | horse:repeat(200000, 200 | http_date(<<"Sun Nov 6 08:49:37 1994">>) 201 | ). 202 | -endif. 203 | 204 | %% @doc Return the date formatted according to RFC1123. 205 | 206 | -spec rfc1123(calendar:datetime()) -> binary(). 207 | rfc1123(DateTime) -> 208 | rfc7231(DateTime). 209 | 210 | %% @doc Return the date formatted according to RFC2109. 211 | 212 | -spec rfc2109(calendar:datetime()) -> binary(). 213 | rfc2109({Date = {Y, Mo, D}, {H, Mi, S}}) -> 214 | Wday = calendar:day_of_the_week(Date), 215 | << (weekday(Wday))/binary, ", ", 216 | (pad_int(D))/binary, "-", 217 | (month(Mo))/binary, "-", 218 | (year(Y))/binary, " ", 219 | (pad_int(H))/binary, ":", 220 | (pad_int(Mi))/binary, ":", 221 | (pad_int(S))/binary, " GMT" >>. 222 | 223 | -ifdef(TEST). 224 | rfc2109_test_() -> 225 | Tests = [ 226 | {<<"Sat, 14-May-2011 14:25:33 GMT">>, {{2011, 5, 14}, {14, 25, 33}}}, 227 | {<<"Sun, 01-Jan-2012 00:00:00 GMT">>, {{2012, 1, 1}, { 0, 0, 0}}} 228 | ], 229 | [{R, fun() -> R = rfc2109(D) end} || {R, D} <- Tests]. 230 | 231 | horse_rfc2109_20130101_000000() -> 232 | horse:repeat(100000, 233 | rfc2109({{2013, 1, 1}, {0, 0, 0}}) 234 | ). 235 | 236 | horse_rfc2109_20131231_235959() -> 237 | horse:repeat(100000, 238 | rfc2109({{2013, 12, 31}, {23, 59, 59}}) 239 | ). 240 | 241 | horse_rfc2109_12340506_070809() -> 242 | horse:repeat(100000, 243 | rfc2109({{1234, 5, 6}, {7, 8, 9}}) 244 | ). 245 | -endif. 246 | 247 | %% @doc Return the date formatted according to RFC7231. 248 | 249 | -spec rfc7231(calendar:datetime()) -> binary(). 250 | rfc7231({Date = {Y, Mo, D}, {H, Mi, S}}) -> 251 | Wday = calendar:day_of_the_week(Date), 252 | << (weekday(Wday))/binary, ", ", 253 | (pad_int(D))/binary, " ", 254 | (month(Mo))/binary, " ", 255 | (year(Y))/binary, " ", 256 | (pad_int(H))/binary, ":", 257 | (pad_int(Mi))/binary, ":", 258 | (pad_int(S))/binary, " GMT" >>. 259 | 260 | -ifdef(TEST). 261 | rfc7231_test_() -> 262 | Tests = [ 263 | {<<"Sat, 14 May 2011 14:25:33 GMT">>, {{2011, 5, 14}, {14, 25, 33}}}, 264 | {<<"Sun, 01 Jan 2012 00:00:00 GMT">>, {{2012, 1, 1}, { 0, 0, 0}}} 265 | ], 266 | [{R, fun() -> R = rfc7231(D) end} || {R, D} <- Tests]. 267 | 268 | horse_rfc7231_20130101_000000() -> 269 | horse:repeat(100000, 270 | rfc7231({{2013, 1, 1}, {0, 0, 0}}) 271 | ). 272 | 273 | horse_rfc7231_20131231_235959() -> 274 | horse:repeat(100000, 275 | rfc7231({{2013, 12, 31}, {23, 59, 59}}) 276 | ). 277 | 278 | horse_rfc7231_12340506_070809() -> 279 | horse:repeat(100000, 280 | rfc7231({{1234, 5, 6}, {7, 8, 9}}) 281 | ). 282 | -endif. 283 | 284 | %% Internal. 285 | 286 | -spec pad_int(0..59) -> <<_:16>>. 287 | pad_int( 0) -> <<"00">>; 288 | pad_int( 1) -> <<"01">>; 289 | pad_int( 2) -> <<"02">>; 290 | pad_int( 3) -> <<"03">>; 291 | pad_int( 4) -> <<"04">>; 292 | pad_int( 5) -> <<"05">>; 293 | pad_int( 6) -> <<"06">>; 294 | pad_int( 7) -> <<"07">>; 295 | pad_int( 8) -> <<"08">>; 296 | pad_int( 9) -> <<"09">>; 297 | pad_int(10) -> <<"10">>; 298 | pad_int(11) -> <<"11">>; 299 | pad_int(12) -> <<"12">>; 300 | pad_int(13) -> <<"13">>; 301 | pad_int(14) -> <<"14">>; 302 | pad_int(15) -> <<"15">>; 303 | pad_int(16) -> <<"16">>; 304 | pad_int(17) -> <<"17">>; 305 | pad_int(18) -> <<"18">>; 306 | pad_int(19) -> <<"19">>; 307 | pad_int(20) -> <<"20">>; 308 | pad_int(21) -> <<"21">>; 309 | pad_int(22) -> <<"22">>; 310 | pad_int(23) -> <<"23">>; 311 | pad_int(24) -> <<"24">>; 312 | pad_int(25) -> <<"25">>; 313 | pad_int(26) -> <<"26">>; 314 | pad_int(27) -> <<"27">>; 315 | pad_int(28) -> <<"28">>; 316 | pad_int(29) -> <<"29">>; 317 | pad_int(30) -> <<"30">>; 318 | pad_int(31) -> <<"31">>; 319 | pad_int(32) -> <<"32">>; 320 | pad_int(33) -> <<"33">>; 321 | pad_int(34) -> <<"34">>; 322 | pad_int(35) -> <<"35">>; 323 | pad_int(36) -> <<"36">>; 324 | pad_int(37) -> <<"37">>; 325 | pad_int(38) -> <<"38">>; 326 | pad_int(39) -> <<"39">>; 327 | pad_int(40) -> <<"40">>; 328 | pad_int(41) -> <<"41">>; 329 | pad_int(42) -> <<"42">>; 330 | pad_int(43) -> <<"43">>; 331 | pad_int(44) -> <<"44">>; 332 | pad_int(45) -> <<"45">>; 333 | pad_int(46) -> <<"46">>; 334 | pad_int(47) -> <<"47">>; 335 | pad_int(48) -> <<"48">>; 336 | pad_int(49) -> <<"49">>; 337 | pad_int(50) -> <<"50">>; 338 | pad_int(51) -> <<"51">>; 339 | pad_int(52) -> <<"52">>; 340 | pad_int(53) -> <<"53">>; 341 | pad_int(54) -> <<"54">>; 342 | pad_int(55) -> <<"55">>; 343 | pad_int(56) -> <<"56">>; 344 | pad_int(57) -> <<"57">>; 345 | pad_int(58) -> <<"58">>; 346 | pad_int(59) -> <<"59">>; 347 | pad_int(60) -> <<"60">>; 348 | pad_int(Int) -> integer_to_binary(Int). 349 | 350 | -spec weekday(1..7) -> <<_:24>>. 351 | weekday(1) -> <<"Mon">>; 352 | weekday(2) -> <<"Tue">>; 353 | weekday(3) -> <<"Wed">>; 354 | weekday(4) -> <<"Thu">>; 355 | weekday(5) -> <<"Fri">>; 356 | weekday(6) -> <<"Sat">>; 357 | weekday(7) -> <<"Sun">>. 358 | 359 | -spec month(1..12) -> <<_:24>>. 360 | month( 1) -> <<"Jan">>; 361 | month( 2) -> <<"Feb">>; 362 | month( 3) -> <<"Mar">>; 363 | month( 4) -> <<"Apr">>; 364 | month( 5) -> <<"May">>; 365 | month( 6) -> <<"Jun">>; 366 | month( 7) -> <<"Jul">>; 367 | month( 8) -> <<"Aug">>; 368 | month( 9) -> <<"Sep">>; 369 | month(10) -> <<"Oct">>; 370 | month(11) -> <<"Nov">>; 371 | month(12) -> <<"Dec">>. 372 | 373 | -spec year(pos_integer()) -> <<_:32>>. 374 | year(1970) -> <<"1970">>; 375 | year(1971) -> <<"1971">>; 376 | year(1972) -> <<"1972">>; 377 | year(1973) -> <<"1973">>; 378 | year(1974) -> <<"1974">>; 379 | year(1975) -> <<"1975">>; 380 | year(1976) -> <<"1976">>; 381 | year(1977) -> <<"1977">>; 382 | year(1978) -> <<"1978">>; 383 | year(1979) -> <<"1979">>; 384 | year(1980) -> <<"1980">>; 385 | year(1981) -> <<"1981">>; 386 | year(1982) -> <<"1982">>; 387 | year(1983) -> <<"1983">>; 388 | year(1984) -> <<"1984">>; 389 | year(1985) -> <<"1985">>; 390 | year(1986) -> <<"1986">>; 391 | year(1987) -> <<"1987">>; 392 | year(1988) -> <<"1988">>; 393 | year(1989) -> <<"1989">>; 394 | year(1990) -> <<"1990">>; 395 | year(1991) -> <<"1991">>; 396 | year(1992) -> <<"1992">>; 397 | year(1993) -> <<"1993">>; 398 | year(1994) -> <<"1994">>; 399 | year(1995) -> <<"1995">>; 400 | year(1996) -> <<"1996">>; 401 | year(1997) -> <<"1997">>; 402 | year(1998) -> <<"1998">>; 403 | year(1999) -> <<"1999">>; 404 | year(2000) -> <<"2000">>; 405 | year(2001) -> <<"2001">>; 406 | year(2002) -> <<"2002">>; 407 | year(2003) -> <<"2003">>; 408 | year(2004) -> <<"2004">>; 409 | year(2005) -> <<"2005">>; 410 | year(2006) -> <<"2006">>; 411 | year(2007) -> <<"2007">>; 412 | year(2008) -> <<"2008">>; 413 | year(2009) -> <<"2009">>; 414 | year(2010) -> <<"2010">>; 415 | year(2011) -> <<"2011">>; 416 | year(2012) -> <<"2012">>; 417 | year(2013) -> <<"2013">>; 418 | year(2014) -> <<"2014">>; 419 | year(2015) -> <<"2015">>; 420 | year(2016) -> <<"2016">>; 421 | year(2017) -> <<"2017">>; 422 | year(2018) -> <<"2018">>; 423 | year(2019) -> <<"2019">>; 424 | year(2020) -> <<"2020">>; 425 | year(2021) -> <<"2021">>; 426 | year(2022) -> <<"2022">>; 427 | year(2023) -> <<"2023">>; 428 | year(2024) -> <<"2024">>; 429 | year(2025) -> <<"2025">>; 430 | year(2026) -> <<"2026">>; 431 | year(2027) -> <<"2027">>; 432 | year(2028) -> <<"2028">>; 433 | year(2029) -> <<"2029">>; 434 | year(Year) -> integer_to_binary(Year). 435 | -------------------------------------------------------------------------------- /src/cow_deflate.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) jdamanalo 2 | %% Copyright (c) Loïc Hoguin 3 | %% 4 | %% Permission to use, copy, modify, and/or distribute this software for any 5 | %% purpose with or without fee is hereby granted, provided that the above 6 | %% copyright notice and this permission notice appear in all copies. 7 | %% 8 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | 16 | -module(cow_deflate). 17 | 18 | -export([inflate/3]). 19 | 20 | -spec inflate(zlib:zstream(), iodata(), non_neg_integer() | infinity) 21 | -> {ok, binary()} | {error, data_error | size_error}. 22 | 23 | inflate(Z, Data, Limit) -> 24 | try 25 | {Status, Output} = zlib:safeInflate(Z, Data), 26 | do_inflate(Z, iolist_size(Output), Limit, Status, [Output]) 27 | catch 28 | error:data_error -> 29 | {error, data_error} 30 | end. 31 | 32 | do_inflate(_, Size, Limit, _, _) when Size > Limit -> 33 | {error, size_error}; 34 | do_inflate(Z, Size0, Limit, continue, Acc) -> 35 | {Status, Output} = zlib:safeInflate(Z, []), 36 | Size = Size0 + iolist_size(Output), 37 | do_inflate(Z, Size, Limit, Status, [Output | Acc]); 38 | do_inflate(_, _, _, finished, Acc) -> 39 | {ok, iolist_to_binary(lists:reverse(Acc))}. 40 | -------------------------------------------------------------------------------- /src/cow_http.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) Loïc Hoguin 2 | %% 3 | %% Permission to use, copy, modify, and/or distribute this software for any 4 | %% purpose with or without fee is hereby granted, provided that the above 5 | %% copyright notice and this permission notice appear in all copies. 6 | %% 7 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | %% This module contains functions and types common 16 | %% to all or most HTTP versions. 17 | -module(cow_http). 18 | 19 | %% The HTTP/1 functions have been moved to cow_http1. 20 | %% In order to remain backward compatible we redirect 21 | %% calls to cow_http1. The type version() was moved 22 | %% and no fallback is provided. 23 | %% 24 | %% @todo Remove the aliases in Cowlib 3.0. 25 | -export([parse_request_line/1]). 26 | -export([parse_status_line/1]). 27 | -export([status_to_integer/1]). 28 | -export([parse_headers/1]). 29 | -export([parse_fullpath/1]). 30 | -export([parse_version/1]). 31 | -export([request/4]). 32 | -export([response/3]). 33 | -export([headers/1]). 34 | -export([version/1]). 35 | 36 | %% Functions used by HTTP/2+. 37 | 38 | -export([format_semantic_error/1]). 39 | -export([merge_pseudo_headers/2]). 40 | -export([process_headers/5]). 41 | -export([remove_http1_headers/1]). 42 | 43 | %% Types used by all versions of HTTP. 44 | 45 | -type status() :: 100..999. 46 | -export_type([status/0]). 47 | 48 | -type headers() :: [{binary(), iodata()}]. 49 | -export_type([headers/0]). 50 | 51 | %% Types used by HTTP/2+. 52 | 53 | -type pseudo_headers() :: #{} %% Trailers 54 | | #{ %% Responses. 55 | status := cow_http:status() 56 | } | #{ %% Normal CONNECT requests. 57 | method := binary(), 58 | authority := binary() 59 | } | #{ %% Extended CONNECT requests. 60 | method := binary(), 61 | scheme := binary(), 62 | authority := binary(), 63 | path := binary(), 64 | protocol := binary() 65 | } | #{ %% Other requests. 66 | method := binary(), 67 | scheme := binary(), 68 | authority => binary(), 69 | path := binary() 70 | }. 71 | -export_type([pseudo_headers/0]). 72 | 73 | -type fin() :: fin | nofin. 74 | -export_type([fin/0]). 75 | 76 | %% HTTP/1 function aliases. 77 | 78 | -spec parse_request_line(binary()) -> {binary(), binary(), cow_http1:version(), binary()}. 79 | parse_request_line(Data) -> cow_http1:parse_request_line(Data). 80 | 81 | -spec parse_status_line(binary()) -> {cow_http1:version(), status(), binary(), binary()}. 82 | parse_status_line(Data) -> cow_http1:parse_status_line(Data). 83 | 84 | -spec status_to_integer(status() | binary()) -> status(). 85 | status_to_integer(Status) -> cow_http1:status_to_integer(Status). 86 | 87 | -spec parse_headers(binary()) -> {[{binary(), binary()}], binary()}. 88 | parse_headers(Data) -> cow_http1:parse_headers(Data). 89 | 90 | -spec parse_fullpath(binary()) -> {binary(), binary()}. 91 | parse_fullpath(Fullpath) -> cow_http1:parse_fullpath(Fullpath). 92 | 93 | -spec parse_version(binary()) -> cow_http1:version(). 94 | parse_version(Data) -> cow_http1:parse_version(Data). 95 | 96 | -spec request(binary(), iodata(), cow_http1:version(), headers()) -> iodata(). 97 | request(Method, Path, Version, Headers) -> cow_http1:request(Method, Path, Version, Headers). 98 | 99 | -spec response(status() | binary(), cow_http1:version(), headers()) -> iodata(). 100 | response(Status, Version, Headers) -> cow_http1:response(Status, Version, Headers). 101 | 102 | -spec headers(headers()) -> iodata(). 103 | headers(Headers) -> cow_http1:headers(Headers). 104 | 105 | -spec version(cow_http1:version()) -> binary(). 106 | version(Version) -> cow_http1:version(Version). 107 | 108 | %% Functions used by HTTP/2+. 109 | 110 | %% Semantic errors are common to all HTTP versions. 111 | 112 | -spec format_semantic_error(atom()) -> atom(). 113 | 114 | format_semantic_error(connect_invalid_content_length_2xx) -> 115 | 'Content-length header received in a 2xx response to a CONNECT request. (RFC7230 3.3.2).'; 116 | format_semantic_error(invalid_content_length_header) -> 117 | 'The content-length header is invalid. (RFC7230 3.3.2)'; 118 | format_semantic_error(invalid_content_length_header_1xx) -> 119 | 'Content-length header received in a 1xx response. (RFC7230 3.3.2)'; 120 | format_semantic_error(invalid_content_length_header_204) -> 121 | 'Content-length header received in a 204 response. (RFC7230 3.3.2)'; 122 | format_semantic_error(multiple_content_length_headers) -> 123 | 'Multiple content-length headers were received. (RFC7230 3.3.2)'. 124 | 125 | %% Merge pseudo headers at the start of headers. 126 | 127 | -spec merge_pseudo_headers(pseudo_headers(), headers()) -> headers(). 128 | 129 | merge_pseudo_headers(PseudoHeaders, Headers0) -> 130 | lists:foldl(fun 131 | ({status, Status}, Acc) when is_integer(Status) -> 132 | [{<<":status">>, integer_to_binary(Status)}|Acc]; 133 | ({Name, Value}, Acc) -> 134 | [{iolist_to_binary([$:, atom_to_binary(Name, latin1)]), Value}|Acc] 135 | end, Headers0, maps:to_list(PseudoHeaders)). 136 | 137 | %% Process HTTP/2+ headers. This is done after decoding them. 138 | 139 | -spec process_headers(headers(), request | push_promise | response | trailers, 140 | binary() | undefined, fin(), #{enable_connect_protocol => boolean(), any() => any()}) 141 | -> {headers, headers(), pseudo_headers(), non_neg_integer() | undefined} 142 | | {push_promise, headers(), pseudo_headers()} 143 | | {trailers, headers()} 144 | | {error, atom()}. 145 | 146 | process_headers(Headers0, Type, ReqMethod, IsFin, LocalSettings) 147 | when Type =:= request; Type =:= push_promise -> 148 | IsExtendedConnectEnabled = maps:get(enable_connect_protocol, LocalSettings, false), 149 | case request_pseudo_headers(Headers0, #{}) of 150 | %% Extended CONNECT method (HTTP/2: RFC8441, HTTP/3: RFC9220). 151 | {ok, PseudoHeaders=#{method := <<"CONNECT">>, scheme := _, 152 | authority := _, path := _, protocol := _}, Headers} 153 | when IsExtendedConnectEnabled -> 154 | regular_headers(Headers, Type, ReqMethod, IsFin, PseudoHeaders); 155 | {ok, #{method := <<"CONNECT">>, scheme := _, 156 | authority := _, path := _}, _} 157 | when IsExtendedConnectEnabled -> 158 | {error, extended_connect_missing_protocol}; 159 | {ok, #{protocol := _}, _} -> 160 | {error, invalid_protocol_pseudo_header}; 161 | %% Normal CONNECT (no scheme/path). 162 | {ok, PseudoHeaders = #{method := <<"CONNECT">>, authority := _}, Headers} 163 | when map_size(PseudoHeaders) =:= 2 -> 164 | regular_headers(Headers, Type, ReqMethod, IsFin, PseudoHeaders); 165 | {ok, #{method := <<"CONNECT">>, authority := _}, _} -> 166 | {error, connect_invalid_pseudo_header}; 167 | {ok, #{method := <<"CONNECT">>}, _} -> 168 | {error, connect_missing_authority}; 169 | %% Other requests. 170 | {ok, PseudoHeaders = #{method := _, scheme := _, path := _}, Headers} -> 171 | regular_headers(Headers, Type, ReqMethod, IsFin, PseudoHeaders); 172 | {ok, _, _} -> 173 | {error, missing_pseudo_header}; 174 | Error = {error, _} -> 175 | Error 176 | end; 177 | process_headers(Headers0, Type = response, ReqMethod, IsFin, _LocalSettings) -> 178 | case response_pseudo_headers(Headers0, #{}) of 179 | {ok, PseudoHeaders=#{status := _}, Headers} -> 180 | regular_headers(Headers, Type, ReqMethod, IsFin, PseudoHeaders); 181 | {ok, _, _} -> 182 | {error, missing_pseudo_header}; 183 | Error = {error, _} -> 184 | Error 185 | end; 186 | process_headers(Headers, Type = trailers, ReqMethod, IsFin, _LocalSettings) -> 187 | case trailers_have_pseudo_headers(Headers) of 188 | false -> 189 | regular_headers(Headers, Type, ReqMethod, IsFin, #{}); 190 | true -> 191 | {error, trailer_invalid_pseudo_header} 192 | end. 193 | 194 | request_pseudo_headers([{<<":method">>, _}|_], #{method := _}) -> 195 | {error, multiple_method_pseudo_headers}; 196 | request_pseudo_headers([{<<":method">>, Method}|Tail], PseudoHeaders) -> 197 | request_pseudo_headers(Tail, PseudoHeaders#{method => Method}); 198 | request_pseudo_headers([{<<":scheme">>, _}|_], #{scheme := _}) -> 199 | {error, multiple_scheme_pseudo_headers}; 200 | request_pseudo_headers([{<<":scheme">>, Scheme}|Tail], PseudoHeaders) -> 201 | request_pseudo_headers(Tail, PseudoHeaders#{scheme => Scheme}); 202 | request_pseudo_headers([{<<":authority">>, _}|_], #{authority := _}) -> 203 | {error, multiple_authority_pseudo_headers}; 204 | request_pseudo_headers([{<<":authority">>, Authority}|Tail], PseudoHeaders) -> 205 | request_pseudo_headers(Tail, PseudoHeaders#{authority => Authority}); 206 | request_pseudo_headers([{<<":path">>, _}|_], #{path := _}) -> 207 | {error, multiple_path_pseudo_headers}; 208 | request_pseudo_headers([{<<":path">>, Path}|Tail], PseudoHeaders) -> 209 | request_pseudo_headers(Tail, PseudoHeaders#{path => Path}); 210 | request_pseudo_headers([{<<":protocol">>, _}|_], #{protocol := _}) -> 211 | {error, multiple_protocol_pseudo_headers}; 212 | request_pseudo_headers([{<<":protocol">>, Protocol}|Tail], PseudoHeaders) -> 213 | request_pseudo_headers(Tail, PseudoHeaders#{protocol => Protocol}); 214 | request_pseudo_headers([{<<":", _/bits>>, _}|_], _) -> 215 | {error, invalid_pseudo_header}; 216 | request_pseudo_headers(Headers, PseudoHeaders) -> 217 | {ok, PseudoHeaders, Headers}. 218 | 219 | response_pseudo_headers([{<<":status">>, _}|_], #{status := _}) -> 220 | {error, multiple_status_pseudo_headers}; 221 | response_pseudo_headers([{<<":status">>, Status}|Tail], PseudoHeaders) -> 222 | try cow_http:status_to_integer(Status) of 223 | IntStatus -> 224 | response_pseudo_headers(Tail, PseudoHeaders#{status => IntStatus}) 225 | catch _:_ -> 226 | {error, invalid_status_pseudo_header} 227 | end; 228 | response_pseudo_headers([{<<":", _/bits>>, _}|_], _) -> 229 | {error, invalid_pseudo_header}; 230 | response_pseudo_headers(Headers, PseudoHeaders) -> 231 | {ok, PseudoHeaders, Headers}. 232 | 233 | trailers_have_pseudo_headers([]) -> 234 | false; 235 | trailers_have_pseudo_headers([{<<":", _/bits>>, _}|_]) -> 236 | true; 237 | trailers_have_pseudo_headers([_|Tail]) -> 238 | trailers_have_pseudo_headers(Tail). 239 | 240 | %% Rejecting invalid regular headers might be a bit too strong for clients. 241 | regular_headers(Headers, Type, ReqMethod, IsFin, PseudoHeaders) -> 242 | case regular_headers(Headers, Type) of 243 | ok when Type =:= request -> 244 | request_expected_size(Headers, IsFin, PseudoHeaders); 245 | ok when Type =:= push_promise -> 246 | return_push_promise(Headers, PseudoHeaders); 247 | ok when Type =:= response -> 248 | response_expected_size(Headers, ReqMethod, IsFin, PseudoHeaders); 249 | ok when Type =:= trailers -> 250 | return_trailers(Headers); 251 | Error = {error, _} -> 252 | Error 253 | end. 254 | 255 | regular_headers([{<<>>, _}|_], _) -> 256 | {error, empty_header_name}; 257 | regular_headers([{<<":", _/bits>>, _}|_], _) -> 258 | {error, pseudo_header_after_regular}; 259 | regular_headers([{<<"connection">>, _}|_], _) -> 260 | {error, invalid_connection_header}; 261 | regular_headers([{<<"keep-alive">>, _}|_], _) -> 262 | {error, invalid_keep_alive_header}; 263 | regular_headers([{<<"proxy-authenticate">>, _}|_], _) -> 264 | {error, invalid_proxy_authenticate_header}; 265 | regular_headers([{<<"proxy-authorization">>, _}|_], _) -> 266 | {error, invalid_proxy_authorization_header}; 267 | regular_headers([{<<"transfer-encoding">>, _}|_], _) -> 268 | {error, invalid_transfer_encoding_header}; 269 | regular_headers([{<<"upgrade">>, _}|_], _) -> 270 | {error, invalid_upgrade_header}; 271 | regular_headers([{<<"te">>, Value}|_], request) when Value =/= <<"trailers">> -> 272 | {error, invalid_te_value}; 273 | regular_headers([{<<"te">>, _}|_], Type) when Type =/= request -> 274 | {error, invalid_te_header}; 275 | regular_headers([{Name, _}|Tail], Type) -> 276 | Pattern = [ 277 | <<$A>>, <<$B>>, <<$C>>, <<$D>>, <<$E>>, <<$F>>, <<$G>>, <<$H>>, <<$I>>, 278 | <<$J>>, <<$K>>, <<$L>>, <<$M>>, <<$N>>, <<$O>>, <<$P>>, <<$Q>>, <<$R>>, 279 | <<$S>>, <<$T>>, <<$U>>, <<$V>>, <<$W>>, <<$X>>, <<$Y>>, <<$Z>> 280 | ], 281 | case binary:match(Name, Pattern) of 282 | nomatch -> regular_headers(Tail, Type); 283 | _ -> {error, uppercase_header_name} 284 | end; 285 | regular_headers([], _) -> 286 | ok. 287 | 288 | request_expected_size(Headers, IsFin, PseudoHeaders) -> 289 | case [CL || {<<"content-length">>, CL} <- Headers] of 290 | [] when IsFin =:= fin -> 291 | return_headers(Headers, PseudoHeaders, 0); 292 | [] -> 293 | return_headers(Headers, PseudoHeaders, undefined); 294 | [<<"0">>] -> 295 | return_headers(Headers, PseudoHeaders, 0); 296 | [_] when IsFin =:= fin -> 297 | {error, non_zero_length_with_fin_flag}; 298 | [BinLen] -> 299 | parse_expected_size(Headers, PseudoHeaders, BinLen); 300 | _ -> 301 | {error, multiple_content_length_headers} 302 | end. 303 | 304 | response_expected_size(Headers, ReqMethod, IsFin, PseudoHeaders = #{status := Status}) -> 305 | case [CL || {<<"content-length">>, CL} <- Headers] of 306 | [] when IsFin =:= fin -> 307 | return_headers(Headers, PseudoHeaders, 0); 308 | [] -> 309 | return_headers(Headers, PseudoHeaders, undefined); 310 | [_] when Status >= 100, Status =< 199 -> 311 | {error, invalid_content_length_header_1xx}; 312 | [_] when Status =:= 204 -> 313 | {error, invalid_content_length_header_204}; 314 | [_] when Status >= 200, Status =< 299, ReqMethod =:= <<"CONNECT">> -> 315 | {error, connect_invalid_content_length_2xx}; 316 | %% Responses to HEAD requests, and 304 responses may contain 317 | %% a content-length header that must be ignored. (RFC7230 3.3.2) 318 | [_] when ReqMethod =:= <<"HEAD">> -> 319 | return_headers(Headers, PseudoHeaders, 0); 320 | [_] when Status =:= 304 -> 321 | return_headers(Headers, PseudoHeaders, 0); 322 | [<<"0">>] when IsFin =:= fin -> 323 | return_headers(Headers, PseudoHeaders, 0); 324 | [_] when IsFin =:= fin -> 325 | {error, non_zero_length_with_fin_flag}; 326 | [BinLen] -> 327 | parse_expected_size(Headers, PseudoHeaders, BinLen); 328 | _ -> 329 | {error, multiple_content_length_headers} 330 | end. 331 | 332 | parse_expected_size(Headers, PseudoHeaders, BinLen) -> 333 | try cow_http_hd:parse_content_length(BinLen) of 334 | Len -> 335 | return_headers(Headers, PseudoHeaders, Len) 336 | catch _:_ -> 337 | {error, invalid_content_length_header} 338 | end. 339 | 340 | return_headers(Headers, PseudoHeaders, Len) -> 341 | {headers, Headers, PseudoHeaders, Len}. 342 | 343 | return_push_promise(Headers, PseudoHeaders) -> 344 | {push_promise, Headers, PseudoHeaders}. 345 | 346 | return_trailers(Headers) -> 347 | {trailers, Headers}. 348 | 349 | %% Remove HTTP/1-specific headers. 350 | 351 | -spec remove_http1_headers(headers()) -> headers(). 352 | 353 | remove_http1_headers(Headers) -> 354 | RemoveHeaders0 = [ 355 | <<"keep-alive">>, 356 | <<"proxy-connection">>, 357 | <<"transfer-encoding">>, 358 | <<"upgrade">> 359 | ], 360 | RemoveHeaders = case lists:keyfind(<<"connection">>, 1, Headers) of 361 | false -> 362 | RemoveHeaders0; 363 | {_, ConnHd} -> 364 | %% We do not need to worry about any "close" header because 365 | %% that header name is reserved. 366 | Connection = cow_http_hd:parse_connection(ConnHd), 367 | Connection ++ [<<"connection">>|RemoveHeaders0] 368 | end, 369 | lists:filter(fun({Name, _}) -> 370 | not lists:member(Name, RemoveHeaders) 371 | end, Headers). 372 | -------------------------------------------------------------------------------- /src/cow_http1.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) Loïc Hoguin 2 | %% 3 | %% Permission to use, copy, modify, and/or distribute this software for any 4 | %% purpose with or without fee is hereby granted, provided that the above 5 | %% copyright notice and this permission notice appear in all copies. 6 | %% 7 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | -module(cow_http1). 16 | 17 | -export([parse_request_line/1]). 18 | -export([parse_status_line/1]). 19 | -export([status_to_integer/1]). 20 | -export([parse_headers/1]). 21 | 22 | -export([parse_fullpath/1]). 23 | -export([parse_version/1]). 24 | 25 | -export([request/4]). 26 | -export([response/3]). 27 | -export([headers/1]). 28 | -export([version/1]). 29 | 30 | -type version() :: 'HTTP/1.0' | 'HTTP/1.1'. 31 | -export_type([version/0]). 32 | 33 | -include("cow_inline.hrl"). 34 | 35 | %% @doc Parse the request line. 36 | 37 | -spec parse_request_line(binary()) -> {binary(), binary(), version(), binary()}. 38 | parse_request_line(Data) -> 39 | {Pos, _} = binary:match(Data, <<"\r">>), 40 | <> = Data, 41 | [Method, Target, Version0] = binary:split(RequestLine, <<$\s>>, [trim_all, global]), 42 | Version = case Version0 of 43 | <<"HTTP/1.1">> -> 'HTTP/1.1'; 44 | <<"HTTP/1.0">> -> 'HTTP/1.0' 45 | end, 46 | {Method, Target, Version, Rest}. 47 | 48 | -ifdef(TEST). 49 | parse_request_line_test_() -> 50 | Tests = [ 51 | {<<"GET /path HTTP/1.0\r\nRest">>, 52 | {<<"GET">>, <<"/path">>, 'HTTP/1.0', <<"Rest">>}}, 53 | {<<"GET /path HTTP/1.1\r\nRest">>, 54 | {<<"GET">>, <<"/path">>, 'HTTP/1.1', <<"Rest">>}}, 55 | {<<"CONNECT proxy.example.org:1080 HTTP/1.1\r\nRest">>, 56 | {<<"CONNECT">>, <<"proxy.example.org:1080">>, 'HTTP/1.1', <<"Rest">>}} 57 | ], 58 | [{V, fun() -> R = parse_request_line(V) end} 59 | || {V, R} <- Tests]. 60 | 61 | parse_request_line_error_test_() -> 62 | Tests = [ 63 | <<>>, 64 | <<"GET">>, 65 | <<"GET /path\r\n">>, 66 | <<"GET /path HTTP/1.1">>, 67 | <<"GET /path HTTP/1.1\r">>, 68 | <<"GET /path HTTP/1.1\n">>, 69 | <<"GET /path HTTP/0.9\r\n">>, 70 | <<"content-type: text/plain\r\n">>, 71 | <<0:80, "\r\n">> 72 | ], 73 | [{V, fun() -> {'EXIT', _} = (catch parse_request_line(V)) end} 74 | || V <- Tests]. 75 | 76 | horse_parse_request_line_get_path() -> 77 | horse:repeat(200000, 78 | parse_request_line(<<"GET /path HTTP/1.1\r\n">>) 79 | ). 80 | -endif. 81 | 82 | %% @doc Parse the status line. 83 | 84 | -spec parse_status_line(binary()) -> {version(), cow_http:status(), binary(), binary()}. 85 | parse_status_line(<< "HTTP/1.1 200 OK\r\n", Rest/bits >>) -> 86 | {'HTTP/1.1', 200, <<"OK">>, Rest}; 87 | parse_status_line(<< "HTTP/1.1 404 Not Found\r\n", Rest/bits >>) -> 88 | {'HTTP/1.1', 404, <<"Not Found">>, Rest}; 89 | parse_status_line(<< "HTTP/1.1 500 Internal Server Error\r\n", Rest/bits >>) -> 90 | {'HTTP/1.1', 500, <<"Internal Server Error">>, Rest}; 91 | parse_status_line(<< "HTTP/1.1 ", Status/bits >>) -> 92 | parse_status_line(Status, 'HTTP/1.1'); 93 | parse_status_line(<< "HTTP/1.0 ", Status/bits >>) -> 94 | parse_status_line(Status, 'HTTP/1.0'). 95 | 96 | parse_status_line(<>, Version) -> 97 | Status = status_to_integer(H, T, U), 98 | {Pos, _} = binary:match(Rest, <<"\r">>), 99 | << StatusStr:Pos/binary, "\r\n", Rest2/bits >> = Rest, 100 | {Version, Status, StatusStr, Rest2}. 101 | 102 | -spec status_to_integer(cow_http:status() | binary()) -> cow_http:status(). 103 | status_to_integer(Status) when is_integer(Status) -> 104 | Status; 105 | status_to_integer(Status) -> 106 | case Status of 107 | <> -> 108 | status_to_integer(H, T, U); 109 | <> -> 110 | status_to_integer(H, T, U) 111 | end. 112 | 113 | status_to_integer(H, T, U) 114 | when $0 =< H, H =< $9, $0 =< T, T =< $9, $0 =< U, U =< $9 -> 115 | (H - $0) * 100 + (T - $0) * 10 + (U - $0). 116 | 117 | -ifdef(TEST). 118 | parse_status_line_test_() -> 119 | Tests = [ 120 | {<<"HTTP/1.1 200 OK\r\nRest">>, 121 | {'HTTP/1.1', 200, <<"OK">>, <<"Rest">>}}, 122 | {<<"HTTP/1.0 404 Not Found\r\nRest">>, 123 | {'HTTP/1.0', 404, <<"Not Found">>, <<"Rest">>}}, 124 | {<<"HTTP/1.1 500 Something very funny here\r\nRest">>, 125 | {'HTTP/1.1', 500, <<"Something very funny here">>, <<"Rest">>}}, 126 | {<<"HTTP/1.1 200 \r\nRest">>, 127 | {'HTTP/1.1', 200, <<>>, <<"Rest">>}} 128 | ], 129 | [{V, fun() -> R = parse_status_line(V) end} 130 | || {V, R} <- Tests]. 131 | 132 | parse_status_line_error_test_() -> 133 | Tests = [ 134 | <<>>, 135 | <<"HTTP/1.1">>, 136 | <<"HTTP/1.1 200\r\n">>, 137 | <<"HTTP/1.1 200 OK">>, 138 | <<"HTTP/1.1 200 OK\r">>, 139 | <<"HTTP/1.1 200 OK\n">>, 140 | <<"HTTP/0.9 200 OK\r\n">>, 141 | <<"HTTP/1.1 42 Answer\r\n">>, 142 | <<"HTTP/1.1 999999999 More than OK\r\n">>, 143 | <<"content-type: text/plain\r\n">>, 144 | <<0:80, "\r\n">> 145 | ], 146 | [{V, fun() -> {'EXIT', _} = (catch parse_status_line(V)) end} 147 | || V <- Tests]. 148 | 149 | horse_parse_status_line_200() -> 150 | horse:repeat(200000, 151 | parse_status_line(<<"HTTP/1.1 200 OK\r\n">>) 152 | ). 153 | 154 | horse_parse_status_line_404() -> 155 | horse:repeat(200000, 156 | parse_status_line(<<"HTTP/1.1 404 Not Found\r\n">>) 157 | ). 158 | 159 | horse_parse_status_line_500() -> 160 | horse:repeat(200000, 161 | parse_status_line(<<"HTTP/1.1 500 Internal Server Error\r\n">>) 162 | ). 163 | 164 | horse_parse_status_line_other() -> 165 | horse:repeat(200000, 166 | parse_status_line(<<"HTTP/1.1 416 Requested range not satisfiable\r\n">>) 167 | ). 168 | -endif. 169 | 170 | %% @doc Parse the list of headers. 171 | 172 | -spec parse_headers(binary()) -> {[{binary(), binary()}], binary()}. 173 | parse_headers(Data) -> 174 | parse_header(Data, []). 175 | 176 | parse_header(<< $\r, $\n, Rest/bits >>, Acc) -> 177 | {lists:reverse(Acc), Rest}; 178 | parse_header(Data, Acc) -> 179 | parse_hd_name(Data, Acc, <<>>). 180 | 181 | parse_hd_name(<< C, Rest/bits >>, Acc, SoFar) -> 182 | case C of 183 | $: -> parse_hd_before_value(Rest, Acc, SoFar); 184 | $\s -> parse_hd_name_ws(Rest, Acc, SoFar); 185 | $\t -> parse_hd_name_ws(Rest, Acc, SoFar); 186 | _ -> ?LOWER(parse_hd_name, Rest, Acc, SoFar) 187 | end. 188 | 189 | parse_hd_name_ws(<< C, Rest/bits >>, Acc, Name) -> 190 | case C of 191 | $: -> parse_hd_before_value(Rest, Acc, Name); 192 | $\s -> parse_hd_name_ws(Rest, Acc, Name); 193 | $\t -> parse_hd_name_ws(Rest, Acc, Name) 194 | end. 195 | 196 | parse_hd_before_value(<< $\s, Rest/bits >>, Acc, Name) -> 197 | parse_hd_before_value(Rest, Acc, Name); 198 | parse_hd_before_value(<< $\t, Rest/bits >>, Acc, Name) -> 199 | parse_hd_before_value(Rest, Acc, Name); 200 | parse_hd_before_value(Data, Acc, Name) -> 201 | parse_hd_value(Data, Acc, Name, <<>>). 202 | 203 | parse_hd_value(<< $\r, Rest/bits >>, Acc, Name, SoFar) -> 204 | case Rest of 205 | << $\n, C, Rest2/bits >> when C =:= $\s; C =:= $\t -> 206 | parse_hd_value(Rest2, Acc, Name, << SoFar/binary, C >>); 207 | << $\n, Rest2/bits >> -> 208 | Value = clean_value_ws_end(SoFar, byte_size(SoFar) - 1), 209 | parse_header(Rest2, [{Name, Value}|Acc]) 210 | end; 211 | parse_hd_value(<< C, Rest/bits >>, Acc, Name, SoFar) -> 212 | parse_hd_value(Rest, Acc, Name, << SoFar/binary, C >>). 213 | 214 | %% This function has been copied from cowboy_http. 215 | clean_value_ws_end(_, -1) -> 216 | <<>>; 217 | clean_value_ws_end(Value, N) -> 218 | case binary:at(Value, N) of 219 | $\s -> clean_value_ws_end(Value, N - 1); 220 | $\t -> clean_value_ws_end(Value, N - 1); 221 | _ -> 222 | S = N + 1, 223 | << Value2:S/binary, _/bits >> = Value, 224 | Value2 225 | end. 226 | 227 | -ifdef(TEST). 228 | parse_headers_test_() -> 229 | Tests = [ 230 | {<<"\r\nRest">>, 231 | {[], <<"Rest">>}}, 232 | {<<"Server: Erlang/R17 \r\n\r\n">>, 233 | {[{<<"server">>, <<"Erlang/R17">>}], <<>>}}, 234 | {<<"Server: Erlang/R17\r\n" 235 | "Date: Sun, 23 Feb 2014 09:30:39 GMT\r\n" 236 | "Multiline-Header: why hello!\r\n" 237 | " I didn't see you all the way over there!\r\n" 238 | "Content-Length: 12\r\n" 239 | "Content-Type: text/plain\r\n" 240 | "\r\nRest">>, 241 | {[{<<"server">>, <<"Erlang/R17">>}, 242 | {<<"date">>, <<"Sun, 23 Feb 2014 09:30:39 GMT">>}, 243 | {<<"multiline-header">>, 244 | <<"why hello! I didn't see you all the way over there!">>}, 245 | {<<"content-length">>, <<"12">>}, 246 | {<<"content-type">>, <<"text/plain">>}], 247 | <<"Rest">>}} 248 | ], 249 | [{V, fun() -> R = parse_headers(V) end} 250 | || {V, R} <- Tests]. 251 | 252 | parse_headers_error_test_() -> 253 | Tests = [ 254 | <<>>, 255 | <<"\r">>, 256 | <<"Malformed\r\n\r\n">>, 257 | <<"content-type: text/plain\r\nMalformed\r\n\r\n">>, 258 | <<"HTTP/1.1 200 OK\r\n\r\n">>, 259 | <<0:80, "\r\n\r\n">>, 260 | <<"content-type: text/plain\r\ncontent-length: 12\r\n">> 261 | ], 262 | [{V, fun() -> {'EXIT', _} = (catch parse_headers(V)) end} 263 | || V <- Tests]. 264 | 265 | horse_parse_headers() -> 266 | horse:repeat(50000, 267 | parse_headers(<<"Server: Erlang/R17\r\n" 268 | "Date: Sun, 23 Feb 2014 09:30:39 GMT\r\n" 269 | "Multiline-Header: why hello!\r\n" 270 | " I didn't see you all the way over there!\r\n" 271 | "Content-Length: 12\r\n" 272 | "Content-Type: text/plain\r\n" 273 | "\r\nRest">>) 274 | ). 275 | -endif. 276 | 277 | %% @doc Extract path and query string from a binary, 278 | %% removing any fragment component. 279 | 280 | -spec parse_fullpath(binary()) -> {binary(), binary()}. 281 | parse_fullpath(Fullpath) -> 282 | parse_fullpath(Fullpath, <<>>). 283 | 284 | parse_fullpath(<<>>, Path) -> {Path, <<>>}; 285 | parse_fullpath(<< $#, _/bits >>, Path) -> {Path, <<>>}; 286 | parse_fullpath(<< $?, Qs/bits >>, Path) -> parse_fullpath_query(Qs, Path, <<>>); 287 | parse_fullpath(<< C, Rest/bits >>, SoFar) -> parse_fullpath(Rest, << SoFar/binary, C >>). 288 | 289 | parse_fullpath_query(<<>>, Path, Query) -> {Path, Query}; 290 | parse_fullpath_query(<< $#, _/bits >>, Path, Query) -> {Path, Query}; 291 | parse_fullpath_query(<< C, Rest/bits >>, Path, SoFar) -> 292 | parse_fullpath_query(Rest, Path, << SoFar/binary, C >>). 293 | 294 | -ifdef(TEST). 295 | parse_fullpath_test() -> 296 | {<<"*">>, <<>>} = parse_fullpath(<<"*">>), 297 | {<<"/">>, <<>>} = parse_fullpath(<<"/">>), 298 | {<<"/path/to/resource">>, <<>>} = parse_fullpath(<<"/path/to/resource#fragment">>), 299 | {<<"/path/to/resource">>, <<>>} = parse_fullpath(<<"/path/to/resource">>), 300 | {<<"/">>, <<>>} = parse_fullpath(<<"/?">>), 301 | {<<"/">>, <<"q=cowboy">>} = parse_fullpath(<<"/?q=cowboy#fragment">>), 302 | {<<"/">>, <<"q=cowboy">>} = parse_fullpath(<<"/?q=cowboy">>), 303 | {<<"/path/to/resource">>, <<"q=cowboy">>} 304 | = parse_fullpath(<<"/path/to/resource?q=cowboy">>), 305 | ok. 306 | -endif. 307 | 308 | %% @doc Convert an HTTP version to atom. 309 | 310 | -spec parse_version(binary()) -> version(). 311 | parse_version(<<"HTTP/1.1">>) -> 'HTTP/1.1'; 312 | parse_version(<<"HTTP/1.0">>) -> 'HTTP/1.0'. 313 | 314 | -ifdef(TEST). 315 | parse_version_test() -> 316 | 'HTTP/1.1' = parse_version(<<"HTTP/1.1">>), 317 | 'HTTP/1.0' = parse_version(<<"HTTP/1.0">>), 318 | {'EXIT', _} = (catch parse_version(<<"HTTP/1.2">>)), 319 | ok. 320 | -endif. 321 | 322 | %% @doc Return formatted request-line and headers. 323 | %% @todo Add tests when the corresponding reverse functions are added. 324 | 325 | -spec request(binary(), iodata(), version(), cow_http:headers()) -> iodata(). 326 | request(Method, Path, Version, Headers) -> 327 | [Method, <<" ">>, Path, <<" ">>, version(Version), <<"\r\n">>, 328 | [[N, <<": ">>, V, <<"\r\n">>] || {N, V} <- Headers], 329 | <<"\r\n">>]. 330 | 331 | -spec response(cow_http:status() | binary(), version(), cow_http:headers()) 332 | -> iodata(). 333 | response(Status, Version, Headers) -> 334 | [version(Version), <<" ">>, status(Status), <<"\r\n">>, 335 | headers(Headers), <<"\r\n">>]. 336 | 337 | -spec headers(cow_http:headers()) -> iodata(). 338 | headers(Headers) -> 339 | [[N, <<": ">>, V, <<"\r\n">>] || {N, V} <- Headers]. 340 | 341 | %% @doc Return the version as a binary. 342 | 343 | -spec version(version()) -> binary(). 344 | version('HTTP/1.1') -> <<"HTTP/1.1">>; 345 | version('HTTP/1.0') -> <<"HTTP/1.0">>. 346 | 347 | -ifdef(TEST). 348 | version_test() -> 349 | <<"HTTP/1.1">> = version('HTTP/1.1'), 350 | <<"HTTP/1.0">> = version('HTTP/1.0'), 351 | {'EXIT', _} = (catch version('HTTP/1.2')), 352 | ok. 353 | -endif. 354 | 355 | %% @doc Return the status code and string as binary. 356 | 357 | -spec status(cow_http:status() | binary()) -> binary(). 358 | status(100) -> <<"100 Continue">>; 359 | status(101) -> <<"101 Switching Protocols">>; 360 | status(102) -> <<"102 Processing">>; 361 | status(103) -> <<"103 Early Hints">>; 362 | status(200) -> <<"200 OK">>; 363 | status(201) -> <<"201 Created">>; 364 | status(202) -> <<"202 Accepted">>; 365 | status(203) -> <<"203 Non-Authoritative Information">>; 366 | status(204) -> <<"204 No Content">>; 367 | status(205) -> <<"205 Reset Content">>; 368 | status(206) -> <<"206 Partial Content">>; 369 | status(207) -> <<"207 Multi-Status">>; 370 | status(208) -> <<"208 Already Reported">>; 371 | status(226) -> <<"226 IM Used">>; 372 | status(300) -> <<"300 Multiple Choices">>; 373 | status(301) -> <<"301 Moved Permanently">>; 374 | status(302) -> <<"302 Found">>; 375 | status(303) -> <<"303 See Other">>; 376 | status(304) -> <<"304 Not Modified">>; 377 | status(305) -> <<"305 Use Proxy">>; 378 | status(306) -> <<"306 Switch Proxy">>; 379 | status(307) -> <<"307 Temporary Redirect">>; 380 | status(308) -> <<"308 Permanent Redirect">>; 381 | status(400) -> <<"400 Bad Request">>; 382 | status(401) -> <<"401 Unauthorized">>; 383 | status(402) -> <<"402 Payment Required">>; 384 | status(403) -> <<"403 Forbidden">>; 385 | status(404) -> <<"404 Not Found">>; 386 | status(405) -> <<"405 Method Not Allowed">>; 387 | status(406) -> <<"406 Not Acceptable">>; 388 | status(407) -> <<"407 Proxy Authentication Required">>; 389 | status(408) -> <<"408 Request Timeout">>; 390 | status(409) -> <<"409 Conflict">>; 391 | status(410) -> <<"410 Gone">>; 392 | status(411) -> <<"411 Length Required">>; 393 | status(412) -> <<"412 Precondition Failed">>; 394 | status(413) -> <<"413 Request Entity Too Large">>; 395 | status(414) -> <<"414 Request-URI Too Long">>; 396 | status(415) -> <<"415 Unsupported Media Type">>; 397 | status(416) -> <<"416 Requested Range Not Satisfiable">>; 398 | status(417) -> <<"417 Expectation Failed">>; 399 | status(418) -> <<"418 I'm a teapot">>; 400 | status(421) -> <<"421 Misdirected Request">>; 401 | status(422) -> <<"422 Unprocessable Entity">>; 402 | status(423) -> <<"423 Locked">>; 403 | status(424) -> <<"424 Failed Dependency">>; 404 | status(425) -> <<"425 Unordered Collection">>; 405 | status(426) -> <<"426 Upgrade Required">>; 406 | status(428) -> <<"428 Precondition Required">>; 407 | status(429) -> <<"429 Too Many Requests">>; 408 | status(431) -> <<"431 Request Header Fields Too Large">>; 409 | status(451) -> <<"451 Unavailable For Legal Reasons">>; 410 | status(500) -> <<"500 Internal Server Error">>; 411 | status(501) -> <<"501 Not Implemented">>; 412 | status(502) -> <<"502 Bad Gateway">>; 413 | status(503) -> <<"503 Service Unavailable">>; 414 | status(504) -> <<"504 Gateway Timeout">>; 415 | status(505) -> <<"505 HTTP Version Not Supported">>; 416 | status(506) -> <<"506 Variant Also Negotiates">>; 417 | status(507) -> <<"507 Insufficient Storage">>; 418 | status(508) -> <<"508 Loop Detected">>; 419 | status(510) -> <<"510 Not Extended">>; 420 | status(511) -> <<"511 Network Authentication Required">>; 421 | status(B) when is_binary(B) -> B. 422 | -------------------------------------------------------------------------------- /src/cow_http3.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) Loïc Hoguin 2 | %% 3 | %% Permission to use, copy, modify, and/or distribute this software for any 4 | %% purpose with or without fee is hereby granted, provided that the above 5 | %% copyright notice and this permission notice appear in all copies. 6 | %% 7 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | -module(cow_http3). 16 | 17 | %% Parsing. 18 | -export([parse/1]). 19 | -export([parse_unidi_stream_header/1]). 20 | -export([code_to_error/1]). 21 | 22 | %% Building. 23 | -export([data/1]). 24 | -export([headers/1]). 25 | -export([settings/1]). 26 | -export([error_to_code/1]). 27 | -export([encode_int/1]). 28 | 29 | -type stream_id() :: non_neg_integer(). 30 | -export_type([stream_id/0]). 31 | 32 | -type push_id() :: non_neg_integer(). 33 | -export_type([push_id/0]). 34 | 35 | -type settings() :: #{ 36 | qpack_max_table_capacity => 0..16#3fffffffffffffff, 37 | max_field_section_size => 0..16#3fffffffffffffff, 38 | qpack_blocked_streams => 0..16#3fffffffffffffff, 39 | enable_connect_protocol => boolean() 40 | }. 41 | -export_type([settings/0]). 42 | 43 | -type error() :: h3_no_error 44 | | h3_general_protocol_error 45 | | h3_internal_error 46 | | h3_stream_creation_error 47 | | h3_closed_critical_stream 48 | | h3_frame_unexpected 49 | | h3_frame_error 50 | | h3_excessive_load 51 | | h3_id_error 52 | | h3_settings_error 53 | | h3_missing_settings 54 | | h3_request_rejected 55 | | h3_request_cancelled 56 | | h3_request_incomplete 57 | | h3_message_error 58 | | h3_connect_error 59 | | h3_version_fallback. 60 | -export_type([error/0]). 61 | 62 | -type frame() :: {data, binary()} 63 | | {headers, binary()} 64 | | {cancel_push, push_id()} 65 | | {settings, settings()} 66 | | {push_promise, push_id(), binary()} 67 | | {goaway, stream_id() | push_id()} 68 | | {max_push_id, push_id()}. 69 | -export_type([frame/0]). 70 | 71 | %% Parsing. 72 | 73 | -spec parse(binary()) 74 | -> {ok, frame(), binary()} 75 | | {more, {data, binary()} | ignore, non_neg_integer()} 76 | | {ignore, binary()} 77 | | {connection_error, h3_frame_error | h3_frame_unexpected | h3_settings_error, atom()} 78 | | more. 79 | 80 | %% 81 | %% DATA frames. 82 | %% 83 | parse(<<0, 0:2, Len:6, Data:Len/binary, Rest/bits>>) -> 84 | {ok, {data, Data}, Rest}; 85 | parse(<<0, 1:2, Len:14, Data:Len/binary, Rest/bits>>) -> 86 | {ok, {data, Data}, Rest}; 87 | parse(<<0, 2:2, Len:30, Data:Len/binary, Rest/bits>>) -> 88 | {ok, {data, Data}, Rest}; 89 | parse(<<0, 3:2, Len:62, Data:Len/binary, Rest/bits>>) -> 90 | {ok, {data, Data}, Rest}; 91 | %% DATA frames may be split over multiple QUIC packets 92 | %% but we want to process them immediately rather than 93 | %% risk buffering a very large payload. 94 | parse(<<0, 0:2, Len:6, Data/bits>>) when byte_size(Data) < Len -> 95 | {more, {data, Data}, Len - byte_size(Data)}; 96 | parse(<<0, 1:2, Len:14, Data/bits>>) when byte_size(Data) < Len -> 97 | {more, {data, Data}, Len - byte_size(Data)}; 98 | parse(<<0, 2:2, Len:30, Data/bits>>) when byte_size(Data) < Len -> 99 | {more, {data, Data}, Len - byte_size(Data)}; 100 | parse(<<0, 3:2, Len:62, Data/bits>>) when byte_size(Data) < Len -> 101 | {more, {data, Data}, Len - byte_size(Data)}; 102 | %% 103 | %% HEADERS frames. 104 | %% 105 | parse(<<1, 0:2, 0:6, _/bits>>) -> 106 | {connection_error, h3_frame_error, 107 | 'HEADERS frames payload CANNOT be 0 bytes wide. (RFC9114 7.1, RFC9114 7.2.2)'}; 108 | parse(<<1, 1:2, 0:14, _/bits>>) -> 109 | {connection_error, h3_frame_error, 110 | 'HEADERS frames payload CANNOT be 0 bytes wide. (RFC9114 7.1, RFC9114 7.2.2)'}; 111 | parse(<<1, 2:2, 0:30, _/bits>>) -> 112 | {connection_error, h3_frame_error, 113 | 'HEADERS frames payload CANNOT be 0 bytes wide. (RFC9114 7.1, RFC9114 7.2.2)'}; 114 | parse(<<1, 3:2, 0:62, _/bits>>) -> 115 | {connection_error, h3_frame_error, 116 | 'HEADERS frames payload CANNOT be 0 bytes wide. (RFC9114 7.1, RFC9114 7.2.2)'}; 117 | parse(<<1, 0:2, Len:6, EncodedFieldSection:Len/binary, Rest/bits>>) -> 118 | {ok, {headers, EncodedFieldSection}, Rest}; 119 | parse(<<1, 1:2, Len:14, EncodedFieldSection:Len/binary, Rest/bits>>) -> 120 | {ok, {headers, EncodedFieldSection}, Rest}; 121 | parse(<<1, 2:2, Len:30, EncodedFieldSection:Len/binary, Rest/bits>>) -> 122 | {ok, {headers, EncodedFieldSection}, Rest}; 123 | parse(<<1, 3:2, Len:62, EncodedFieldSection:Len/binary, Rest/bits>>) -> 124 | {ok, {headers, EncodedFieldSection}, Rest}; 125 | %% 126 | %% CANCEL_PUSH frames. 127 | %% 128 | parse(<<3, 0:2, 1:6, 0:2, PushID:6, Rest/bits>>) -> 129 | {ok, {cancel_push, PushID}, Rest}; 130 | parse(<<3, 0:2, 2:6, 1:2, PushID:14, Rest/bits>>) -> 131 | {ok, {cancel_push, PushID}, Rest}; 132 | parse(<<3, 0:2, 4:6, 2:2, PushID:30, Rest/bits>>) -> 133 | {ok, {cancel_push, PushID}, Rest}; 134 | parse(<<3, 0:2, 8:6, 3:2, PushID:62, Rest/bits>>) -> 135 | {ok, {cancel_push, PushID}, Rest}; 136 | parse(<<3, _/bits>>) -> 137 | {connection_error, h3_frame_error, 138 | 'CANCEL_PUSH frames payload MUST be 1, 2, 4 or 8 bytes wide. (RFC9114 7.1, RFC9114 7.2.3)'}; 139 | %% 140 | %% SETTINGS frames. 141 | %% 142 | parse(<<4, 0:2, Len:6, Rest/bits>>) when byte_size(Rest) >= Len -> 143 | parse_settings_id(Rest, Len, #{}); 144 | parse(<<4, 1:2, Len:14, Rest/bits>>) when byte_size(Rest) >= Len -> 145 | parse_settings_id(Rest, Len, #{}); 146 | parse(<<4, 2:2, Len:30, Rest/bits>>) when byte_size(Rest) >= Len -> 147 | parse_settings_id(Rest, Len, #{}); 148 | parse(<<4, 3:2, Len:62, Rest/bits>>) when byte_size(Rest) >= Len -> 149 | parse_settings_id(Rest, Len, #{}); 150 | %% 151 | %% PUSH_PROMISE frames. 152 | %% 153 | parse(<<5, 0:2, Len:6, Rest/bits>>) when byte_size(Rest) >= Len -> 154 | parse_push_promise(Rest, Len); 155 | parse(<<5, 1:2, Len:14, Rest/bits>>) when byte_size(Rest) >= Len -> 156 | parse_push_promise(Rest, Len); 157 | parse(<<5, 2:2, Len:30, Rest/bits>>) when byte_size(Rest) >= Len -> 158 | parse_push_promise(Rest, Len); 159 | parse(<<5, 3:2, Len:62, Rest/bits>>) when byte_size(Rest) >= Len -> 160 | parse_push_promise(Rest, Len); 161 | %% 162 | %% GOAWAY frames. 163 | %% 164 | parse(<<7, 0:2, 1:6, 0:2, StreamOrPushID:6, Rest/bits>>) -> 165 | {ok, {goaway, StreamOrPushID}, Rest}; 166 | parse(<<7, 0:2, 2:6, 1:2, StreamOrPushID:14, Rest/bits>>) -> 167 | {ok, {goaway, StreamOrPushID}, Rest}; 168 | parse(<<7, 0:2, 4:6, 2:2, StreamOrPushID:30, Rest/bits>>) -> 169 | {ok, {goaway, StreamOrPushID}, Rest}; 170 | parse(<<7, 0:2, 8:6, 3:2, StreamOrPushID:62, Rest/bits>>) -> 171 | {ok, {goaway, StreamOrPushID}, Rest}; 172 | parse(<<7, 0:2, N:6, _/bits>>) when N =:= 1; N =:= 2; N =:= 4; N =:= 8 -> 173 | more; 174 | parse(<<7, _/bits>>) -> 175 | {connection_error, h3_frame_error, 176 | 'GOAWAY frames payload MUST be 1, 2, 4 or 8 bytes wide. (RFC9114 7.1, RFC9114 7.2.6)'}; 177 | %% 178 | %% MAX_PUSH_ID frames. 179 | %% 180 | parse(<<13, 0:2, 1:6, 0:2, PushID:6, Rest/bits>>) -> 181 | {ok, {max_push_id, PushID}, Rest}; 182 | parse(<<13, 0:2, 2:6, 1:2, PushID:14, Rest/bits>>) -> 183 | {ok, {max_push_id, PushID}, Rest}; 184 | parse(<<13, 0:2, 4:6, 2:2, PushID:30, Rest/bits>>) -> 185 | {ok, {max_push_id, PushID}, Rest}; 186 | parse(<<13, 0:2, 8:6, 3:2, PushID:62, Rest/bits>>) -> 187 | {ok, {max_push_id, PushID}, Rest}; 188 | parse(<<13, 0:2, N:6, _/bits>>) when N =:= 1; N =:= 2; N =:= 4; N =:= 8 -> 189 | more; 190 | parse(<<13, _/bits>>) -> 191 | {connection_error, h3_frame_error, 192 | 'MAX_PUSH_ID frames payload MUST be 1, 2, 4 or 8 bytes wide. (RFC9114 7.1, RFC9114 7.2.6)'}; 193 | %% 194 | %% HTTP/2 frame types must be rejected. 195 | %% 196 | parse(<<2, _/bits>>) -> 197 | {connection_error, h3_frame_unexpected, 198 | 'HTTP/2 PRIORITY frame not defined for HTTP/3 must be rejected. (RFC9114 7.2.8)'}; 199 | parse(<<6, _/bits>>) -> 200 | {connection_error, h3_frame_unexpected, 201 | 'HTTP/2 PING frame not defined for HTTP/3 must be rejected. (RFC9114 7.2.8)'}; 202 | parse(<<8, _/bits>>) -> 203 | {connection_error, h3_frame_unexpected, 204 | 'HTTP/2 WINDOW_UPDATE frame not defined for HTTP/3 must be rejected. (RFC9114 7.2.8)'}; 205 | parse(<<9, _/bits>>) -> 206 | {connection_error, h3_frame_unexpected, 207 | 'HTTP/2 CONTINUATION frame not defined for HTTP/3 must be rejected. (RFC9114 7.2.8)'}; 208 | %% 209 | %% Unknown frames must be ignored. 210 | parse(<<0:2, Type:6, 0:2, Len:6, Rest/bits>>) 211 | when Type =:= 10; Type =:= 11; Type =:= 12; Type > 13 -> 212 | parse_ignore(Rest, Len); 213 | parse(<<0:2, Type:6, 1:2, Len:14, Rest/bits>>) 214 | when Type =:= 10; Type =:= 11; Type =:= 12; Type > 13 -> 215 | parse_ignore(Rest, Len); 216 | parse(<<0:2, Type:6, 2:2, Len:30, Rest/bits>>) 217 | when Type =:= 10; Type =:= 11; Type =:= 12; Type > 13 -> 218 | parse_ignore(Rest, Len); 219 | parse(<<0:2, Type:6, 3:2, Len:62, Rest/bits>>) 220 | when Type =:= 10; Type =:= 11; Type =:= 12; Type > 13 -> 221 | parse_ignore(Rest, Len); 222 | parse(<<1:2, _:14, 0:2, Len:6, Rest/bits>>) -> 223 | parse_ignore(Rest, Len); 224 | parse(<<1:2, _:14, 1:2, Len:14, Rest/bits>>) -> 225 | parse_ignore(Rest, Len); 226 | parse(<<1:2, _:14, 2:2, Len:30, Rest/bits>>) -> 227 | parse_ignore(Rest, Len); 228 | parse(<<1:2, _:14, 3:2, Len:62, Rest/bits>>) -> 229 | parse_ignore(Rest, Len); 230 | parse(<<2:2, _:30, 0:2, Len:6, Rest/bits>>) -> 231 | parse_ignore(Rest, Len); 232 | parse(<<2:2, _:30, 1:2, Len:14, Rest/bits>>) -> 233 | parse_ignore(Rest, Len); 234 | parse(<<2:2, _:30, 2:2, Len:30, Rest/bits>>) -> 235 | parse_ignore(Rest, Len); 236 | parse(<<2:2, _:30, 3:2, Len:62, Rest/bits>>) -> 237 | parse_ignore(Rest, Len); 238 | parse(<<3:2, _:62, 0:2, Len:6, Rest/bits>>) -> 239 | parse_ignore(Rest, Len); 240 | parse(<<3:2, _:62, 1:2, Len:14, Rest/bits>>) -> 241 | parse_ignore(Rest, Len); 242 | parse(<<3:2, _:62, 2:2, Len:30, Rest/bits>>) -> 243 | parse_ignore(Rest, Len); 244 | parse(<<3:2, _:62, 3:2, Len:62, Rest/bits>>) -> 245 | parse_ignore(Rest, Len); 246 | %% 247 | %% Incomplete frames for those we fully process only. 248 | %% 249 | parse(_) -> 250 | more. 251 | 252 | parse_settings_id(Rest, 0, Settings) -> 253 | {ok, {settings, Settings}, Rest}; 254 | parse_settings_id(<<0:2, Identifier:6, Rest/bits>>, Len, Settings) when Len >= 1 -> 255 | parse_settings_val(Rest, Len - 1, Settings, Identifier); 256 | parse_settings_id(<<1:2, Identifier:14, Rest/bits>>, Len, Settings) when Len >= 2 -> 257 | parse_settings_val(Rest, Len - 2, Settings, Identifier); 258 | parse_settings_id(<<2:2, Identifier:30, Rest/bits>>, Len, Settings) when Len >= 4 -> 259 | parse_settings_val(Rest, Len - 4, Settings, Identifier); 260 | parse_settings_id(<<3:2, Identifier:62, Rest/bits>>, Len, Settings) when Len >= 8 -> 261 | parse_settings_val(Rest, Len - 8, Settings, Identifier); 262 | parse_settings_id(_, _, _) -> 263 | {connection_error, h3_frame_error, 264 | 'SETTINGS payload size exceeds the length given. (RFC9114 7.1, RFC9114 7.2.4)'}. 265 | 266 | parse_settings_val(<<0:2, Value:6, Rest/bits>>, Len, Settings, Identifier) when Len >= 1 -> 267 | parse_settings_id_val(Rest, Len - 1, Settings, Identifier, Value); 268 | parse_settings_val(<<1:2, Value:14, Rest/bits>>, Len, Settings, Identifier) when Len >= 2 -> 269 | parse_settings_id_val(Rest, Len - 2, Settings, Identifier, Value); 270 | parse_settings_val(<<2:2, Value:30, Rest/bits>>, Len, Settings, Identifier) when Len >= 4 -> 271 | parse_settings_id_val(Rest, Len - 4, Settings, Identifier, Value); 272 | parse_settings_val(<<3:2, Value:62, Rest/bits>>, Len, Settings, Identifier) when Len >= 8 -> 273 | parse_settings_id_val(Rest, Len - 8, Settings, Identifier, Value); 274 | parse_settings_val(_, _, _, _) -> 275 | {connection_error, h3_frame_error, 276 | 'SETTINGS payload size exceeds the length given. (RFC9114 7.1, RFC9114 7.2.4)'}. 277 | 278 | parse_settings_id_val(Rest, Len, Settings, Identifier, Value) -> 279 | case Identifier of 280 | %% SETTINGS_QPACK_MAX_TABLE_CAPACITY (RFC9204). 281 | 1 -> 282 | parse_settings_key_val(Rest, Len, Settings, qpack_max_table_capacity, Value); 283 | %% SETTINGS_MAX_FIELD_SECTION_SIZE (RFC9114). 284 | 6 -> 285 | parse_settings_key_val(Rest, Len, Settings, max_field_section_size, Value); 286 | %% SETTINGS_QPACK_BLOCKED_STREAMS (RFC9204). 287 | 7 -> 288 | parse_settings_key_val(Rest, Len, Settings, qpack_blocked_streams, Value); 289 | %% SETTINGS_ENABLE_CONNECT_PROTOCOL (RFC9220). 290 | 8 when Value =:= 0 -> 291 | parse_settings_key_val(Rest, Len, Settings, enable_connect_protocol, false); 292 | 8 when Value =:= 1 -> 293 | parse_settings_key_val(Rest, Len, Settings, enable_connect_protocol, true); 294 | 8 -> 295 | {connection_error, h3_settings_error, 296 | 'The SETTINGS_ENABLE_CONNECT_PROTOCOL value MUST be 0 or 1. (RFC9220 3, RFC8441 3)'}; 297 | _ when Identifier < 6 -> 298 | {connection_error, h3_settings_error, 299 | 'HTTP/2 setting not defined for HTTP/3 must be rejected. (RFC9114 7.2.4.1)'}; 300 | %% Unknown settings must be ignored. 301 | _ -> 302 | parse_settings_id(Rest, Len, Settings) 303 | end. 304 | 305 | parse_settings_key_val(Rest, Len, Settings, Key, Value) -> 306 | case Settings of 307 | #{Key := _} -> 308 | {connection_error, h3_settings_error, 309 | 'A duplicate setting identifier was found. (RFC9114 7.2.4)'}; 310 | _ -> 311 | parse_settings_id(Rest, Len, Settings#{Key => Value}) 312 | end. 313 | 314 | parse_push_promise(<<0:2, PushID:6, Data/bits>>, Len) -> 315 | <> = Data, 316 | {ok, {push_promise, PushID, EncodedFieldSection}, Rest}; 317 | parse_push_promise(<<1:2, PushID:14, Data/bits>>, Len) -> 318 | <> = Data, 319 | {ok, {push_promise, PushID, EncodedFieldSection}, Rest}; 320 | parse_push_promise(<<2:2, PushID:30, Data/bits>>, Len) -> 321 | <> = Data, 322 | {ok, {push_promise, PushID, EncodedFieldSection}, Rest}; 323 | parse_push_promise(<<3:2, PushID:62, Data/bits>>, Len) -> 324 | <> = Data, 325 | {ok, {push_promise, PushID, EncodedFieldSection}, Rest}. 326 | 327 | %% Large ignored frames could lead to DoS. Users of 328 | %% this module must limit the size of such frames. 329 | parse_ignore(Data, Len) -> 330 | case Data of 331 | <<_:Len/binary, Rest/bits>> -> 332 | {ignore, Rest}; 333 | _ -> 334 | {more, ignore, Len - byte_size(Data)} 335 | end. 336 | 337 | -spec parse_unidi_stream_header(binary()) 338 | -> {ok, control | push | encoder | decoder, binary()} 339 | | {undefined, binary()}. 340 | 341 | parse_unidi_stream_header(<<0, Rest/bits>>) -> 342 | {ok, control, Rest}; 343 | parse_unidi_stream_header(<<1, Rest/bits>>) -> 344 | {ok, push, Rest}; 345 | parse_unidi_stream_header(<<2, Rest/bits>>) -> 346 | {ok, encoder, Rest}; 347 | parse_unidi_stream_header(<<3, Rest/bits>>) -> 348 | {ok, decoder, Rest}; 349 | parse_unidi_stream_header(<<0:2, _:6, Rest/bits>>) -> 350 | {undefined, Rest}; 351 | parse_unidi_stream_header(<<1:2, _:14, Rest/bits>>) -> 352 | {undefined, Rest}; 353 | parse_unidi_stream_header(<<2:2, _:30, Rest/bits>>) -> 354 | {undefined, Rest}; 355 | parse_unidi_stream_header(<<3:2, _:62, Rest/bits>>) -> 356 | {undefined, Rest}. 357 | 358 | -spec code_to_error(non_neg_integer()) -> error(). 359 | 360 | code_to_error(16#0100) -> h3_no_error; 361 | code_to_error(16#0101) -> h3_general_protocol_error; 362 | code_to_error(16#0102) -> h3_internal_error; 363 | code_to_error(16#0103) -> h3_stream_creation_error; 364 | code_to_error(16#0104) -> h3_closed_critical_stream; 365 | code_to_error(16#0105) -> h3_frame_unexpected; 366 | code_to_error(16#0106) -> h3_frame_error; 367 | code_to_error(16#0107) -> h3_excessive_load; 368 | code_to_error(16#0108) -> h3_id_error; 369 | code_to_error(16#0109) -> h3_settings_error; 370 | code_to_error(16#010a) -> h3_missing_settings; 371 | code_to_error(16#010b) -> h3_request_rejected; 372 | code_to_error(16#010c) -> h3_request_cancelled; 373 | code_to_error(16#010d) -> h3_request_incomplete; 374 | code_to_error(16#010e) -> h3_message_error; 375 | code_to_error(16#010f) -> h3_connect_error; 376 | code_to_error(16#0110) -> h3_version_fallback; 377 | %% Unknown/reserved error codes must be treated 378 | %% as equivalent to H3_NO_ERROR. 379 | code_to_error(_) -> h3_no_error. 380 | 381 | %% Building. 382 | 383 | -spec data(iodata()) -> iolist(). 384 | 385 | data(Data) -> 386 | Len = encode_int(iolist_size(Data)), 387 | [<<0:8>>, Len, Data]. 388 | 389 | -spec headers(iodata()) -> iolist(). 390 | 391 | headers(HeaderBlock) -> 392 | Len = encode_int(iolist_size(HeaderBlock)), 393 | [<<1:8>>, Len, HeaderBlock]. 394 | 395 | -spec settings(settings()) -> iolist(). 396 | 397 | settings(Settings) when Settings =:= #{} -> 398 | <<4:8, 0:8>>; 399 | settings(Settings) -> 400 | Payload = settings_payload(Settings), 401 | Len = encode_int(iolist_size(Payload)), 402 | [<<4:8>>, Len, Payload]. 403 | 404 | settings_payload(Settings) -> 405 | Payload = [case Key of 406 | %% SETTINGS_QPACK_MAX_TABLE_CAPACITY (RFC9204). 407 | qpack_max_table_capacity when Value =:= 0 -> <<>>; 408 | qpack_max_table_capacity -> [encode_int(1), encode_int(Value)]; 409 | %% SETTINGS_MAX_FIELD_SECTION_SIZE (RFC9114). 410 | max_header_list_size when Value =:= infinity -> <<>>; 411 | max_header_list_size -> [encode_int(6), encode_int(Value)]; 412 | %% SETTINGS_QPACK_BLOCKED_STREAMS (RFC9204). 413 | qpack_blocked_streams when Value =:= 0 -> <<>>; 414 | qpack_blocked_streams -> [encode_int(1), encode_int(Value)]; 415 | %% SETTINGS_ENABLE_CONNECT_PROTOCOL (RFC9220). 416 | enable_connect_protocol when Value -> [encode_int(8), encode_int(1)]; 417 | enable_connect_protocol -> [encode_int(8), encode_int(0)] 418 | end || {Key, Value} <- maps:to_list(Settings)], 419 | %% Include one reserved identifier in addition. 420 | ReservedType = 16#1f * (rand:uniform(148764065110560900) - 1) + 16#21, 421 | [encode_int(ReservedType), encode_int(rand:uniform(15384) - 1)|Payload]. 422 | 423 | -spec error_to_code(error()) -> non_neg_integer(). 424 | 425 | error_to_code(h3_no_error) -> 426 | %% Implementations should select a reserved error code 427 | %% with some probability when they would have sent H3_NO_ERROR. (RFC9114 8.1) 428 | case rand:uniform(2) of 429 | 1 -> 16#0100; 430 | 2 -> 16#1f * (rand:uniform(148764065110560900) - 1) + 16#21 431 | end; 432 | error_to_code(h3_general_protocol_error) -> 16#0101; 433 | error_to_code(h3_internal_error) -> 16#0102; 434 | error_to_code(h3_stream_creation_error) -> 16#0103; 435 | error_to_code(h3_closed_critical_stream) -> 16#0104; 436 | error_to_code(h3_frame_unexpected) -> 16#0105; 437 | error_to_code(h3_frame_error) -> 16#0106; 438 | error_to_code(h3_excessive_load) -> 16#0107; 439 | error_to_code(h3_id_error) -> 16#0108; 440 | error_to_code(h3_settings_error) -> 16#0109; 441 | error_to_code(h3_missing_settings) -> 16#010a; 442 | error_to_code(h3_request_rejected) -> 16#010b; 443 | error_to_code(h3_request_cancelled) -> 16#010c; 444 | error_to_code(h3_request_incomplete) -> 16#010d; 445 | error_to_code(h3_message_error) -> 16#010e; 446 | error_to_code(h3_connect_error) -> 16#010f; 447 | error_to_code(h3_version_fallback) -> 16#0110. 448 | 449 | -spec encode_int(0..16#3fffffffffffffff) -> binary(). 450 | 451 | encode_int(I) when I < 64 -> 452 | <<0:2, I:6>>; 453 | encode_int(I) when I < 16384 -> 454 | <<1:2, I:14>>; 455 | encode_int(I) when I < 1073741824 -> 456 | <<2:2, I:30>>; 457 | encode_int(I) when I < 4611686018427387904 -> 458 | <<3:2, I:62>>. 459 | -------------------------------------------------------------------------------- /src/cow_http_struct_hd.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) Loïc Hoguin 2 | %% 3 | %% Permission to use, copy, modify, and/or distribute this software for any 4 | %% purpose with or without fee is hereby granted, provided that the above 5 | %% copyright notice and this permission notice appear in all copies. 6 | %% 7 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | %% The mapping between Erlang and structured headers types is as follow: 16 | %% 17 | %% List: list() 18 | %% Inner list: {list, [item()], params()} 19 | %% Dictionary: [{binary(), item()}] 20 | %% There is no distinction between empty list and empty dictionary. 21 | %% Item with parameters: {item, bare_item(), params()} 22 | %% Parameters: [{binary(), bare_item()}] 23 | %% Bare item: one bare_item() that can be of type: 24 | %% Integer: integer() 25 | %% Decimal: {decimal, {integer(), integer()}} 26 | %% String: {string, binary()} 27 | %% Token: {token, binary()} 28 | %% Byte sequence: {binary, binary()} 29 | %% Boolean: boolean() 30 | 31 | -module(cow_http_struct_hd). 32 | 33 | -export([parse_dictionary/1]). 34 | -export([parse_item/1]). 35 | -export([parse_list/1]). 36 | -export([dictionary/1]). 37 | -export([item/1]). 38 | -export([list/1]). 39 | 40 | -include("cow_parse.hrl"). 41 | 42 | -type sh_list() :: [sh_item() | sh_inner_list()]. 43 | -type sh_inner_list() :: {list, [sh_item()], sh_params()}. 44 | -type sh_params() :: [{binary(), sh_bare_item()}]. 45 | -type sh_dictionary() :: [{binary(), sh_item() | sh_inner_list()}]. 46 | -type sh_item() :: {item, sh_bare_item(), sh_params()}. 47 | -type sh_bare_item() :: integer() | sh_decimal() | boolean() 48 | | {string | token | binary, binary()}. 49 | -type sh_decimal() :: {decimal, {integer(), integer()}}. 50 | 51 | -define(IS_LC_ALPHA(C), 52 | (C =:= $a) or (C =:= $b) or (C =:= $c) or (C =:= $d) or (C =:= $e) or 53 | (C =:= $f) or (C =:= $g) or (C =:= $h) or (C =:= $i) or (C =:= $j) or 54 | (C =:= $k) or (C =:= $l) or (C =:= $m) or (C =:= $n) or (C =:= $o) or 55 | (C =:= $p) or (C =:= $q) or (C =:= $r) or (C =:= $s) or (C =:= $t) or 56 | (C =:= $u) or (C =:= $v) or (C =:= $w) or (C =:= $x) or (C =:= $y) or 57 | (C =:= $z) 58 | ). 59 | 60 | %% Parsing. 61 | 62 | -spec parse_dictionary(binary()) -> sh_dictionary(). 63 | parse_dictionary(<<>>) -> 64 | []; 65 | parse_dictionary(<>) when ?IS_LC_ALPHA(C) or (C =:= $*) -> 66 | parse_dict_key(R, [], <>). 67 | 68 | parse_dict_key(<<$=,$(,R0/bits>>, Acc, K) -> 69 | {Item, R} = parse_inner_list(R0, []), 70 | parse_dict_before_sep(R, lists:keystore(K, 1, Acc, {K, Item})); 71 | parse_dict_key(<<$=,R0/bits>>, Acc, K) -> 72 | {Item, R} = parse_item1(R0), 73 | parse_dict_before_sep(R, lists:keystore(K, 1, Acc, {K, Item})); 74 | parse_dict_key(<>, Acc, K) 75 | when ?IS_LC_ALPHA(C) or ?IS_DIGIT(C) 76 | or (C =:= $_) or (C =:= $-) or (C =:= $.) or (C =:= $*) -> 77 | parse_dict_key(R, Acc, <>); 78 | parse_dict_key(<<$;,R0/bits>>, Acc, K) -> 79 | {Params, R} = parse_before_param(R0, []), 80 | parse_dict_before_sep(R, lists:keystore(K, 1, Acc, {K, {item, true, Params}})); 81 | parse_dict_key(R, Acc, K) -> 82 | parse_dict_before_sep(R, lists:keystore(K, 1, Acc, {K, {item, true, []}})). 83 | 84 | parse_dict_before_sep(<<$\s,R/bits>>, Acc) -> 85 | parse_dict_before_sep(R, Acc); 86 | parse_dict_before_sep(<<$\t,R/bits>>, Acc) -> 87 | parse_dict_before_sep(R, Acc); 88 | parse_dict_before_sep(<>, Acc) when C =:= $, -> 89 | parse_dict_before_member(R, Acc); 90 | parse_dict_before_sep(<<>>, Acc) -> 91 | Acc. 92 | 93 | parse_dict_before_member(<<$\s,R/bits>>, Acc) -> 94 | parse_dict_before_member(R, Acc); 95 | parse_dict_before_member(<<$\t,R/bits>>, Acc) -> 96 | parse_dict_before_member(R, Acc); 97 | parse_dict_before_member(<>, Acc) when ?IS_LC_ALPHA(C) or (C =:= $*) -> 98 | parse_dict_key(R, Acc, <>). 99 | 100 | -spec parse_item(binary()) -> sh_item(). 101 | parse_item(Bin) -> 102 | {Item, <<>>} = parse_item1(Bin), 103 | Item. 104 | 105 | parse_item1(Bin) -> 106 | case parse_bare_item(Bin) of 107 | {Item, <<$;,R/bits>>} -> 108 | {Params, Rest} = parse_before_param(R, []), 109 | {{item, Item, Params}, Rest}; 110 | {Item, Rest} -> 111 | {{item, Item, []}, Rest} 112 | end. 113 | 114 | -spec parse_list(binary()) -> sh_list(). 115 | parse_list(<<>>) -> 116 | []; 117 | parse_list(Bin) -> 118 | parse_list_before_member(Bin, []). 119 | 120 | parse_list_member(<<$(,R0/bits>>, Acc) -> 121 | {Item, R} = parse_inner_list(R0, []), 122 | parse_list_before_sep(R, [Item|Acc]); 123 | parse_list_member(R0, Acc) -> 124 | {Item, R} = parse_item1(R0), 125 | parse_list_before_sep(R, [Item|Acc]). 126 | 127 | parse_list_before_sep(<<$\s,R/bits>>, Acc) -> 128 | parse_list_before_sep(R, Acc); 129 | parse_list_before_sep(<<$\t,R/bits>>, Acc) -> 130 | parse_list_before_sep(R, Acc); 131 | parse_list_before_sep(<<$,,R/bits>>, Acc) -> 132 | parse_list_before_member(R, Acc); 133 | parse_list_before_sep(<<>>, Acc) -> 134 | lists:reverse(Acc). 135 | 136 | parse_list_before_member(<<$\s,R/bits>>, Acc) -> 137 | parse_list_before_member(R, Acc); 138 | parse_list_before_member(<<$\t,R/bits>>, Acc) -> 139 | parse_list_before_member(R, Acc); 140 | parse_list_before_member(R, Acc) -> 141 | parse_list_member(R, Acc). 142 | 143 | %% Internal. 144 | 145 | parse_inner_list(<<$\s,R/bits>>, Acc) -> 146 | parse_inner_list(R, Acc); 147 | parse_inner_list(<<$),$;,R0/bits>>, Acc) -> 148 | {Params, R} = parse_before_param(R0, []), 149 | {{list, lists:reverse(Acc), Params}, R}; 150 | parse_inner_list(<<$),R/bits>>, Acc) -> 151 | {{list, lists:reverse(Acc), []}, R}; 152 | parse_inner_list(R0, Acc) -> 153 | {Item, R = <>} = parse_item1(R0), 154 | true = (C =:= $\s) orelse (C =:= $)), 155 | parse_inner_list(R, [Item|Acc]). 156 | 157 | parse_before_param(<<$\s,R/bits>>, Acc) -> 158 | parse_before_param(R, Acc); 159 | parse_before_param(<>, Acc) when ?IS_LC_ALPHA(C) or (C =:= $*) -> 160 | parse_param(R, Acc, <>). 161 | 162 | parse_param(<<$;,R/bits>>, Acc, K) -> 163 | parse_before_param(R, lists:keystore(K, 1, Acc, {K, true})); 164 | parse_param(<<$=,R0/bits>>, Acc, K) -> 165 | case parse_bare_item(R0) of 166 | {Item, <<$;,R/bits>>} -> 167 | parse_before_param(R, lists:keystore(K, 1, Acc, {K, Item})); 168 | {Item, R} -> 169 | {lists:keystore(K, 1, Acc, {K, Item}), R} 170 | end; 171 | parse_param(<>, Acc, K) 172 | when ?IS_LC_ALPHA(C) or ?IS_DIGIT(C) 173 | or (C =:= $_) or (C =:= $-) or (C =:= $.) or (C =:= $*) -> 174 | parse_param(R, Acc, <>); 175 | parse_param(R, Acc, K) -> 176 | {lists:keystore(K, 1, Acc, {K, true}), R}. 177 | 178 | %% Integer or decimal. 179 | parse_bare_item(<<$-,R/bits>>) -> parse_number(R, 0, <<$->>); 180 | parse_bare_item(<>) when ?IS_DIGIT(C) -> parse_number(R, 1, <>); 181 | %% String. 182 | parse_bare_item(<<$",R/bits>>) -> parse_string(R, <<>>); 183 | %% Token. 184 | parse_bare_item(<>) when ?IS_ALPHA(C) or (C =:= $*) -> parse_token(R, <>); 185 | %% Byte sequence. 186 | parse_bare_item(<<$:,R/bits>>) -> parse_binary(R, <<>>); 187 | %% Boolean. 188 | parse_bare_item(<<"?0",R/bits>>) -> {false, R}; 189 | parse_bare_item(<<"?1",R/bits>>) -> {true, R}. 190 | 191 | parse_number(<>, L, Acc) when ?IS_DIGIT(C) -> 192 | parse_number(R, L+1, <>); 193 | parse_number(<<$.,R/bits>>, L, Acc) -> 194 | parse_decimal(R, L, 0, Acc, <<>>); 195 | parse_number(R, L, Acc) when L =< 15 -> 196 | {binary_to_integer(Acc), R}. 197 | 198 | parse_decimal(<>, L1, L2, IntAcc, FracAcc) when ?IS_DIGIT(C) -> 199 | parse_decimal(R, L1, L2+1, IntAcc, <>); 200 | parse_decimal(R, L1, L2, IntAcc, FracAcc0) when L1 =< 12, L2 >= 1, L2 =< 3 -> 201 | %% While not strictly required this gives a more consistent representation. 202 | FracAcc = case FracAcc0 of 203 | <<$0>> -> <<>>; 204 | <<$0,$0>> -> <<>>; 205 | <<$0,$0,$0>> -> <<>>; 206 | <> -> <>; 207 | <> -> <>; 208 | <> -> <>; 209 | _ -> FracAcc0 210 | end, 211 | Mul = case byte_size(FracAcc) of 212 | 3 -> 1000; 213 | 2 -> 100; 214 | 1 -> 10; 215 | 0 -> 1 216 | end, 217 | Int = binary_to_integer(IntAcc), 218 | Frac = case FracAcc of 219 | <<>> -> 0; 220 | %% Mind the sign. 221 | _ when Int < 0 -> -binary_to_integer(FracAcc); 222 | _ -> binary_to_integer(FracAcc) 223 | end, 224 | {{decimal, {Int * Mul + Frac, -byte_size(FracAcc)}}, R}. 225 | 226 | parse_string(<<$\\,$",R/bits>>, Acc) -> 227 | parse_string(R, <>); 228 | parse_string(<<$\\,$\\,R/bits>>, Acc) -> 229 | parse_string(R, <>); 230 | parse_string(<<$",R/bits>>, Acc) -> 231 | {{string, Acc}, R}; 232 | parse_string(<>, Acc) when 233 | C >= 16#20, C =< 16#21; 234 | C >= 16#23, C =< 16#5b; 235 | C >= 16#5d, C =< 16#7e -> 236 | parse_string(R, <>). 237 | 238 | parse_token(<>, Acc) when ?IS_TOKEN(C) or (C =:= $:) or (C =:= $/) -> 239 | parse_token(R, <>); 240 | parse_token(R, Acc) -> 241 | {{token, Acc}, R}. 242 | 243 | parse_binary(<<$:,R/bits>>, Acc) -> 244 | {{binary, base64:decode(Acc)}, R}; 245 | parse_binary(<>, Acc) when ?IS_ALPHANUM(C) or (C =:= $+) or (C =:= $/) or (C =:= $=) -> 246 | parse_binary(R, <>). 247 | 248 | -ifdef(TEST). 249 | parse_struct_hd_test_() -> 250 | Files = filelib:wildcard("deps/structured-header-tests/*.json"), 251 | lists:flatten([begin 252 | {ok, JSON} = file:read_file(File), 253 | Tests = jsx:decode(JSON, [return_maps]), 254 | [ 255 | {iolist_to_binary(io_lib:format("~s: ~s", [filename:basename(File), Name])), fun() -> 256 | %% The implementation is strict. We fail whenever we can. 257 | CanFail = maps:get(<<"can_fail">>, Test, false), 258 | MustFail = maps:get(<<"must_fail">>, Test, false), 259 | io:format("must fail ~p~nexpected json ~0p~n", 260 | [MustFail, maps:get(<<"expected">>, Test, undefined)]), 261 | Expected = case MustFail of 262 | true -> undefined; 263 | false -> expected_to_term(maps:get(<<"expected">>, Test)) 264 | end, 265 | io:format("expected term: ~0p", [Expected]), 266 | Raw = raw_to_binary(Raw0), 267 | case HeaderType of 268 | <<"dictionary">> when MustFail; CanFail -> 269 | {'EXIT', _} = (catch parse_dictionary(Raw)); 270 | %% The test "binary.json: non-zero pad bits" does not fail 271 | %% due to our reliance on Erlang/OTP's base64 module. 272 | <<"item">> when CanFail -> 273 | case (catch parse_item(Raw)) of 274 | {'EXIT', _} -> ok; 275 | Expected -> ok 276 | end; 277 | <<"item">> when MustFail -> 278 | {'EXIT', _} = (catch parse_item(Raw)); 279 | <<"list">> when MustFail; CanFail -> 280 | {'EXIT', _} = (catch parse_list(Raw)); 281 | <<"dictionary">> -> 282 | Expected = (catch parse_dictionary(Raw)); 283 | <<"item">> -> 284 | Expected = (catch parse_item(Raw)); 285 | <<"list">> -> 286 | Expected = (catch parse_list(Raw)) 287 | end 288 | end} 289 | || Test=#{ 290 | <<"name">> := Name, 291 | <<"header_type">> := HeaderType, 292 | <<"raw">> := Raw0 293 | } <- Tests] 294 | end || File <- Files]). 295 | 296 | %% The tests JSON use arrays for almost everything. Identifying 297 | %% what is what requires looking deeper in the values: 298 | %% 299 | %% dict: [["k", v], ["k2", v2]] (values may have params) 300 | %% params: [["k", v], ["k2", v2]] (no params for values) 301 | %% list: [e1, e2, e3] 302 | %% inner-list: [[ [items...], params]] 303 | %% item: [bare, params] 304 | 305 | %% Item. 306 | expected_to_term([Bare, []]) 307 | when is_boolean(Bare); is_number(Bare); is_binary(Bare); is_map(Bare) -> 308 | {item, e2tb(Bare), []}; 309 | expected_to_term([Bare, Params = [[<<_/bits>>, _]|_]]) 310 | when is_boolean(Bare); is_number(Bare); is_binary(Bare); is_map(Bare) -> 311 | {item, e2tb(Bare), e2tp(Params)}; 312 | %% Empty list or dictionary. 313 | expected_to_term([]) -> 314 | []; 315 | %% Dictionary. 316 | %% 317 | %% We exclude empty list from values because that could 318 | %% be confused with an outer list of strings. There is 319 | %% currently no conflicts in the tests thankfully. 320 | expected_to_term(Dict = [[<<_/bits>>, V]|_]) when V =/= [] -> 321 | e2t(Dict); 322 | %% Outer list. 323 | expected_to_term(List) when is_list(List) -> 324 | [e2t(E) || E <- List]. 325 | 326 | %% Dictionary. 327 | e2t(Dict = [[<<_/bits>>, _]|_]) -> 328 | [{K, e2t(V)} || [K, V] <- Dict]; 329 | %% Inner list. 330 | e2t([List, Params]) when is_list(List) -> 331 | {list, [e2t(E) || E <- List], e2tp(Params)}; 332 | %% Item. 333 | e2t([Bare, Params]) -> 334 | {item, e2tb(Bare), e2tp(Params)}. 335 | 336 | %% Bare item. 337 | e2tb(#{<<"__type">> := <<"token">>, <<"value">> := V}) -> 338 | {token, V}; 339 | e2tb(#{<<"__type">> := <<"binary">>, <<"value">> := V}) -> 340 | {binary, base32:decode(V)}; 341 | e2tb(V) when is_binary(V) -> 342 | {string, V}; 343 | e2tb(V) when is_float(V) -> 344 | %% There should be no rounding needed for the test cases. 345 | {decimal, decimal:to_decimal(V, #{precision => 3, rounding => round_down})}; 346 | e2tb(V) -> 347 | V. 348 | 349 | %% Params. 350 | e2tp([]) -> 351 | []; 352 | e2tp(Params) -> 353 | [{K, e2tb(V)} || [K, V] <- Params]. 354 | 355 | %% The Cowlib parsers currently do not support resuming parsing 356 | %% in the case of multiple headers. To make tests work we modify 357 | %% the raw value the same way Cowboy does when encountering 358 | %% multiple headers: by adding a comma and space in between. 359 | %% 360 | %% Similarly, the Cowlib parsers expect the leading and trailing 361 | %% whitespace to be removed before calling the parser. 362 | raw_to_binary(RawList) -> 363 | trim_ws(iolist_to_binary(lists:join(<<", ">>, RawList))). 364 | 365 | trim_ws(<<$\s,R/bits>>) -> trim_ws(R); 366 | trim_ws(R) -> trim_ws_end(R, byte_size(R) - 1). 367 | 368 | trim_ws_end(_, -1) -> 369 | <<>>; 370 | trim_ws_end(Value, N) -> 371 | case binary:at(Value, N) of 372 | $\s -> trim_ws_end(Value, N - 1); 373 | _ -> 374 | S = N + 1, 375 | << Value2:S/binary, _/bits >> = Value, 376 | Value2 377 | end. 378 | -endif. 379 | 380 | %% Building. 381 | 382 | -spec dictionary(#{binary() => sh_item() | sh_inner_list()} | sh_dictionary()) 383 | -> iolist(). 384 | dictionary(Map) when is_map(Map) -> 385 | dictionary(maps:to_list(Map)); 386 | dictionary(KVList) when is_list(KVList) -> 387 | lists:join(<<", ">>, [ 388 | case Value of 389 | true -> Key; 390 | _ -> [Key, $=, item_or_inner_list(Value)] 391 | end 392 | || {Key, Value} <- KVList]). 393 | 394 | -spec item(sh_item()) -> iolist(). 395 | item({item, BareItem, Params}) -> 396 | [bare_item(BareItem), params(Params)]. 397 | 398 | -spec list(sh_list()) -> iolist(). 399 | list(List) -> 400 | lists:join(<<", ">>, [item_or_inner_list(Value) || Value <- List]). 401 | 402 | item_or_inner_list(Value = {list, _, _}) -> 403 | inner_list(Value); 404 | item_or_inner_list(Value) -> 405 | item(Value). 406 | 407 | inner_list({list, List, Params}) -> 408 | [$(, lists:join($\s, [item(Value) || Value <- List]), $), params(Params)]. 409 | 410 | bare_item({string, String}) -> 411 | [$", escape_string(String, <<>>), $"]; 412 | %% @todo Must fail if Token has invalid characters. 413 | bare_item({token, Token}) -> 414 | Token; 415 | bare_item({binary, Binary}) -> 416 | [$:, base64:encode(Binary), $:]; 417 | bare_item({decimal, {Base, Exp}}) when Exp >= 0 -> 418 | Mul = case Exp of 419 | 0 -> 1; 420 | 1 -> 10; 421 | 2 -> 100; 422 | 3 -> 1000; 423 | 4 -> 10000; 424 | 5 -> 100000; 425 | 6 -> 1000000; 426 | 7 -> 10000000; 427 | 8 -> 100000000; 428 | 9 -> 1000000000; 429 | 10 -> 10000000000; 430 | 11 -> 100000000000; 431 | 12 -> 1000000000000 432 | end, 433 | MaxLenWithSign = if 434 | Base < 0 -> 13; 435 | true -> 12 436 | end, 437 | Bin = integer_to_binary(Base * Mul), 438 | true = byte_size(Bin) =< MaxLenWithSign, 439 | [Bin, <<".0">>]; 440 | bare_item({decimal, {Base, -1}}) -> 441 | Int = Base div 10, 442 | Frac = abs(Base) rem 10, 443 | [integer_to_binary(Int), $., integer_to_binary(Frac)]; 444 | bare_item({decimal, {Base, -2}}) -> 445 | Int = Base div 100, 446 | Frac = abs(Base) rem 100, 447 | [integer_to_binary(Int), $., integer_to_binary(Frac)]; 448 | bare_item({decimal, {Base, -3}}) -> 449 | Int = Base div 1000, 450 | Frac = abs(Base) rem 1000, 451 | [integer_to_binary(Int), $., integer_to_binary(Frac)]; 452 | bare_item({decimal, {Base, Exp}}) -> 453 | Div = exp_div(Exp), 454 | Int0 = Base div Div, 455 | true = abs(Int0) < 1000000000000, 456 | Frac0 = abs(Base) rem Div, 457 | DivFrac = Div div 1000, 458 | Frac1 = Frac0 div DivFrac, 459 | {Int, Frac} = if 460 | (Frac0 rem DivFrac) > (DivFrac div 2) -> 461 | case Frac1 of 462 | 999 when Int0 < 0 -> {Int0 - 1, 0}; 463 | 999 -> {Int0 + 1, 0}; 464 | _ -> {Int0, Frac1 + 1} 465 | end; 466 | true -> 467 | {Int0, Frac1} 468 | end, 469 | [integer_to_binary(Int), $., if 470 | Frac < 10 -> [$0, $0, integer_to_binary(Frac)]; 471 | Frac < 100 -> [$0, integer_to_binary(Frac)]; 472 | true -> integer_to_binary(Frac) 473 | end]; 474 | bare_item(Integer) when is_integer(Integer) -> 475 | integer_to_binary(Integer); 476 | bare_item(true) -> 477 | <<"?1">>; 478 | bare_item(false) -> 479 | <<"?0">>. 480 | 481 | exp_div(0) -> 1; 482 | exp_div(N) -> 10 * exp_div(N + 1). 483 | 484 | escape_string(<<>>, Acc) -> Acc; 485 | escape_string(<<$\\,R/bits>>, Acc) -> escape_string(R, <>); 486 | escape_string(<<$",R/bits>>, Acc) -> escape_string(R, <>); 487 | escape_string(<>, Acc) -> escape_string(R, <>). 488 | 489 | params(Params) -> 490 | [case Param of 491 | {Key, true} -> [$;, Key]; 492 | {Key, Value} -> [$;, Key, $=, bare_item(Value)] 493 | end || Param <- Params]. 494 | 495 | -ifdef(TEST). 496 | struct_hd_identity_test_() -> 497 | Files = filelib:wildcard("deps/structured-header-tests/*.json"), 498 | lists:flatten([begin 499 | {ok, JSON} = file:read_file(File), 500 | Tests = jsx:decode(JSON, [return_maps]), 501 | [ 502 | {iolist_to_binary(io_lib:format("~s: ~s", [filename:basename(File), Name])), fun() -> 503 | io:format("expected json ~0p~n", [Expected0]), 504 | Expected = expected_to_term(Expected0), 505 | io:format("expected term: ~0p", [Expected]), 506 | case HeaderType of 507 | <<"dictionary">> -> 508 | Expected = parse_dictionary(iolist_to_binary(dictionary(Expected))); 509 | <<"item">> -> 510 | Expected = parse_item(iolist_to_binary(item(Expected))); 511 | <<"list">> -> 512 | Expected = parse_list(iolist_to_binary(list(Expected))) 513 | end 514 | end} 515 | || #{ 516 | <<"name">> := Name, 517 | <<"header_type">> := HeaderType, 518 | %% We only run tests that must not fail. 519 | <<"expected">> := Expected0 520 | } <- Tests] 521 | end || File <- Files]). 522 | -endif. 523 | -------------------------------------------------------------------------------- /src/cow_http_te.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) Loïc Hoguin 2 | %% 3 | %% Permission to use, copy, modify, and/or distribute this software for any 4 | %% purpose with or without fee is hereby granted, provided that the above 5 | %% copyright notice and this permission notice appear in all copies. 6 | %% 7 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | -module(cow_http_te). 16 | 17 | %% Identity. 18 | -export([stream_identity/2]). 19 | -export([identity/1]). 20 | 21 | %% Chunked. 22 | -export([stream_chunked/2]). 23 | -export([chunk/1]). 24 | -export([last_chunk/0]). 25 | 26 | %% The state type is the same for both identity and chunked. 27 | -type state() :: {non_neg_integer(), non_neg_integer()}. 28 | -export_type([state/0]). 29 | 30 | -type decode_ret() :: more 31 | | {more, Data::binary(), state()} 32 | | {more, Data::binary(), RemLen::non_neg_integer(), state()} 33 | | {more, Data::binary(), Rest::binary(), state()} 34 | | {done, HasTrailers::trailers | no_trailers, Rest::binary()} 35 | | {done, Data::binary(), HasTrailers::trailers | no_trailers, Rest::binary()}. 36 | -export_type([decode_ret/0]). 37 | 38 | -include("cow_parse.hrl"). 39 | 40 | -ifdef(TEST). 41 | dripfeed(<< C, Rest/bits >>, Acc, State, F) -> 42 | case F(<< Acc/binary, C >>, State) of 43 | more -> 44 | dripfeed(Rest, << Acc/binary, C >>, State, F); 45 | {more, _, State2} -> 46 | dripfeed(Rest, <<>>, State2, F); 47 | {more, _, Length, State2} when is_integer(Length) -> 48 | dripfeed(Rest, <<>>, State2, F); 49 | {more, _, Acc2, State2} -> 50 | dripfeed(Rest, Acc2, State2, F); 51 | {done, _, <<>>} -> 52 | ok; 53 | {done, _, _, <<>>} -> 54 | ok 55 | end. 56 | -endif. 57 | 58 | %% Identity. 59 | 60 | %% @doc Decode an identity stream. 61 | 62 | -spec stream_identity(Data, State) 63 | -> {more, Data, Len, State} | {done, Data, Len, Data} 64 | when Data::binary(), State::state(), Len::non_neg_integer(). 65 | stream_identity(Data, {Streamed, Total}) -> 66 | Streamed2 = Streamed + byte_size(Data), 67 | if 68 | Streamed2 < Total -> 69 | {more, Data, Total - Streamed2, {Streamed2, Total}}; 70 | true -> 71 | Size = Total - Streamed, 72 | << Data2:Size/binary, Rest/bits >> = Data, 73 | {done, Data2, Total, Rest} 74 | end. 75 | 76 | -spec identity(Data) -> Data when Data::iodata(). 77 | identity(Data) -> 78 | Data. 79 | 80 | -ifdef(TEST). 81 | stream_identity_test() -> 82 | {done, <<>>, 0, <<>>} 83 | = stream_identity(identity(<<>>), {0, 0}), 84 | {done, <<"\r\n">>, 2, <<>>} 85 | = stream_identity(identity(<<"\r\n">>), {0, 2}), 86 | {done, << 0:80000 >>, 10000, <<>>} 87 | = stream_identity(identity(<< 0:80000 >>), {0, 10000}), 88 | ok. 89 | 90 | stream_identity_parts_test() -> 91 | {more, << 0:8000 >>, 1999, S1} 92 | = stream_identity(<< 0:8000 >>, {0, 2999}), 93 | {more, << 0:8000 >>, 999, S2} 94 | = stream_identity(<< 0:8000 >>, S1), 95 | {done, << 0:7992 >>, 2999, <<>>} 96 | = stream_identity(<< 0:7992 >>, S2), 97 | ok. 98 | 99 | %% Using the same data as the chunked one for comparison. 100 | horse_stream_identity() -> 101 | horse:repeat(10000, 102 | stream_identity(<< 103 | "4\r\n" 104 | "Wiki\r\n" 105 | "5\r\n" 106 | "pedia\r\n" 107 | "e\r\n" 108 | " in\r\n\r\nchunks.\r\n" 109 | "0\r\n" 110 | "\r\n">>, {0, 43}) 111 | ). 112 | 113 | horse_stream_identity_dripfeed() -> 114 | horse:repeat(10000, 115 | dripfeed(<< 116 | "4\r\n" 117 | "Wiki\r\n" 118 | "5\r\n" 119 | "pedia\r\n" 120 | "e\r\n" 121 | " in\r\n\r\nchunks.\r\n" 122 | "0\r\n" 123 | "\r\n">>, <<>>, {0, 43}, fun stream_identity/2) 124 | ). 125 | -endif. 126 | 127 | %% Chunked. 128 | 129 | %% @doc Decode a chunked stream. 130 | 131 | -spec stream_chunked(Data, State) 132 | -> more | {more, Data, State} | {more, Data, non_neg_integer(), State} 133 | | {more, Data, Data, State} 134 | | {done, HasTrailers, Data} | {done, Data, HasTrailers, Data} 135 | when Data::binary(), State::state(), HasTrailers::trailers | no_trailers. 136 | stream_chunked(Data, State) -> 137 | stream_chunked(Data, State, <<>>). 138 | 139 | %% New chunk. 140 | stream_chunked(Data = << C, _/bits >>, {0, Streamed}, Acc) when C =/= $\r -> 141 | case chunked_len(Data, Streamed, Acc, 0) of 142 | {next, Rest, State, Acc2} -> 143 | stream_chunked(Rest, State, Acc2); 144 | {more, State, Acc2} -> 145 | {more, Acc2, Data, State}; 146 | Ret -> 147 | Ret 148 | end; 149 | %% Trailing \r\n before next chunk. 150 | stream_chunked(<< "\r\n", Rest/bits >>, {2, Streamed}, Acc) -> 151 | stream_chunked(Rest, {0, Streamed}, Acc); 152 | %% Trailing \r before next chunk. 153 | stream_chunked(<< "\r" >>, {2, Streamed}, Acc) -> 154 | {more, Acc, {1, Streamed}}; 155 | %% Trailing \n before next chunk. 156 | stream_chunked(<< "\n", Rest/bits >>, {1, Streamed}, Acc) -> 157 | stream_chunked(Rest, {0, Streamed}, Acc); 158 | %% More data needed. 159 | stream_chunked(<<>>, State = {Rem, _}, Acc) -> 160 | {more, Acc, Rem, State}; 161 | %% Chunk data. 162 | stream_chunked(Data, {Rem, Streamed}, Acc) when Rem > 2 -> 163 | DataSize = byte_size(Data), 164 | RemSize = Rem - 2, 165 | case Data of 166 | << Chunk:RemSize/binary, "\r\n", Rest/bits >> -> 167 | stream_chunked(Rest, {0, Streamed + RemSize}, << Acc/binary, Chunk/binary >>); 168 | << Chunk:RemSize/binary, "\r" >> -> 169 | {more, << Acc/binary, Chunk/binary >>, {1, Streamed + RemSize}}; 170 | %% Everything in Data is part of the chunk. If we have more 171 | %% data than the chunk accepts, then this is an error and we crash. 172 | _ when DataSize =< RemSize -> 173 | Rem2 = Rem - DataSize, 174 | {more, << Acc/binary, Data/binary >>, Rem2, {Rem2, Streamed + DataSize}} 175 | end. 176 | 177 | chunked_len(<< $0, R/bits >>, S, A, Len) -> chunked_len(R, S, A, Len * 16); 178 | chunked_len(<< $1, R/bits >>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 1); 179 | chunked_len(<< $2, R/bits >>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 2); 180 | chunked_len(<< $3, R/bits >>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 3); 181 | chunked_len(<< $4, R/bits >>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 4); 182 | chunked_len(<< $5, R/bits >>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 5); 183 | chunked_len(<< $6, R/bits >>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 6); 184 | chunked_len(<< $7, R/bits >>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 7); 185 | chunked_len(<< $8, R/bits >>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 8); 186 | chunked_len(<< $9, R/bits >>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 9); 187 | chunked_len(<< $A, R/bits >>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 10); 188 | chunked_len(<< $B, R/bits >>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 11); 189 | chunked_len(<< $C, R/bits >>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 12); 190 | chunked_len(<< $D, R/bits >>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 13); 191 | chunked_len(<< $E, R/bits >>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 14); 192 | chunked_len(<< $F, R/bits >>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 15); 193 | chunked_len(<< $a, R/bits >>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 10); 194 | chunked_len(<< $b, R/bits >>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 11); 195 | chunked_len(<< $c, R/bits >>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 12); 196 | chunked_len(<< $d, R/bits >>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 13); 197 | chunked_len(<< $e, R/bits >>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 14); 198 | chunked_len(<< $f, R/bits >>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 15); 199 | %% Chunk extensions. 200 | %% 201 | %% Note that we currently skip the first character we encounter here, 202 | %% and not in the skip_chunk_ext function. If we latter implement 203 | %% chunk extensions (unlikely) we will need to change this clause too. 204 | chunked_len(<< C, R/bits >>, S, A, Len) when ?IS_WS(C); C =:= $; -> skip_chunk_ext(R, S, A, Len, 0); 205 | %% Final chunk. 206 | %% 207 | %% When trailers are following we simply return them as the Rest. 208 | %% Then the user code can decide to call the stream_trailers function 209 | %% to parse them. The user can therefore ignore trailers as necessary 210 | %% if they do not wish to handle them. 211 | chunked_len(<< "\r\n\r\n", R/bits >>, _, <<>>, 0) -> {done, no_trailers, R}; 212 | chunked_len(<< "\r\n\r\n", R/bits >>, _, A, 0) -> {done, A, no_trailers, R}; 213 | chunked_len(<< "\r\n", R/bits >>, _, <<>>, 0) when byte_size(R) > 2 -> {done, trailers, R}; 214 | chunked_len(<< "\r\n", R/bits >>, _, A, 0) when byte_size(R) > 2 -> {done, A, trailers, R}; 215 | chunked_len(_, _, _, 0) -> more; 216 | %% Normal chunk. Add 2 to Len for the trailing \r\n. 217 | chunked_len(<< "\r\n", R/bits >>, S, A, Len) -> {next, R, {Len + 2, S}, A}; 218 | chunked_len(<<"\r">>, _, <<>>, _) -> more; 219 | chunked_len(<<"\r">>, S, A, _) -> {more, {0, S}, A}; 220 | chunked_len(<<>>, _, <<>>, _) -> more; 221 | chunked_len(<<>>, S, A, _) -> {more, {0, S}, A}. 222 | 223 | skip_chunk_ext(R = << "\r", _/bits >>, S, A, Len, _) -> chunked_len(R, S, A, Len); 224 | skip_chunk_ext(R = <<>>, S, A, Len, _) -> chunked_len(R, S, A, Len); 225 | %% We skip up to 128 characters of chunk extensions. The value 226 | %% is hardcoded: chunk extensions are very rarely seen in the 227 | %% wild and Cowboy doesn't do anything with them anyway. 228 | %% 229 | %% Line breaks are not allowed in the middle of chunk extensions. 230 | skip_chunk_ext(<< C, R/bits >>, S, A, Len, Skipped) when C =/= $\n, Skipped < 128 -> 231 | skip_chunk_ext(R, S, A, Len, Skipped + 1). 232 | 233 | %% @doc Encode a chunk. 234 | 235 | -spec chunk(D) -> D when D::iodata(). 236 | chunk(Data) -> 237 | [integer_to_list(iolist_size(Data), 16), <<"\r\n">>, 238 | Data, <<"\r\n">>]. 239 | 240 | %% @doc Encode the last chunk of a chunked stream. 241 | 242 | -spec last_chunk() -> << _:40 >>. 243 | last_chunk() -> 244 | <<"0\r\n\r\n">>. 245 | 246 | -ifdef(TEST). 247 | stream_chunked_identity_test() -> 248 | {done, <<"Wikipedia in\r\n\r\nchunks.">>, no_trailers, <<>>} 249 | = stream_chunked(iolist_to_binary([ 250 | chunk("Wiki"), 251 | chunk("pedia"), 252 | chunk(" in\r\n\r\nchunks."), 253 | last_chunk() 254 | ]), {0, 0}), 255 | ok. 256 | 257 | stream_chunked_one_pass_test() -> 258 | {done, no_trailers, <<>>} = stream_chunked(<<"0\r\n\r\n">>, {0, 0}), 259 | {done, <<"Wikipedia in\r\n\r\nchunks.">>, no_trailers, <<>>} 260 | = stream_chunked(<< 261 | "4\r\n" 262 | "Wiki\r\n" 263 | "5\r\n" 264 | "pedia\r\n" 265 | "e\r\n" 266 | " in\r\n\r\nchunks.\r\n" 267 | "0\r\n" 268 | "\r\n">>, {0, 0}), 269 | %% Same but with extra spaces or chunk extensions. 270 | {done, <<"Wikipedia in\r\n\r\nchunks.">>, no_trailers, <<>>} 271 | = stream_chunked(<< 272 | "4 \r\n" 273 | "Wiki\r\n" 274 | "5 ; ext = abc\r\n" 275 | "pedia\r\n" 276 | "e;ext=abc\r\n" 277 | " in\r\n\r\nchunks.\r\n" 278 | "0;ext\r\n" 279 | "\r\n">>, {0, 0}), 280 | %% Same but with trailers. 281 | {done, <<"Wikipedia in\r\n\r\nchunks.">>, trailers, Rest} 282 | = stream_chunked(<< 283 | "4\r\n" 284 | "Wiki\r\n" 285 | "5\r\n" 286 | "pedia\r\n" 287 | "e\r\n" 288 | " in\r\n\r\nchunks.\r\n" 289 | "0\r\n" 290 | "x-foo-bar: bar foo\r\n" 291 | "\r\n">>, {0, 0}), 292 | {[{<<"x-foo-bar">>, <<"bar foo">>}], <<>>} = cow_http:parse_headers(Rest), 293 | ok. 294 | 295 | stream_chunked_n_passes_test() -> 296 | S0 = {0, 0}, 297 | more = stream_chunked(<<"4\r">>, S0), 298 | {more, <<>>, 6, S1} = stream_chunked(<<"4\r\n">>, S0), 299 | {more, <<"Wiki">>, 0, S2} = stream_chunked(<<"Wiki\r\n">>, S1), 300 | {more, <<"pedia">>, <<"e\r">>, S3} = stream_chunked(<<"5\r\npedia\r\ne\r">>, S2), 301 | {more, <<" in\r\n\r\nchunks.">>, 2, S4} = stream_chunked(<<"e\r\n in\r\n\r\nchunks.">>, S3), 302 | {done, no_trailers, <<>>} = stream_chunked(<<"\r\n0\r\n\r\n">>, S4), 303 | %% A few extra for coverage purposes. 304 | more = stream_chunked(<<"\n3">>, {1, 0}), 305 | {more, <<"abc">>, 2, {2, 3}} = stream_chunked(<<"\n3\r\nabc">>, {1, 0}), 306 | {more, <<"abc">>, {1, 3}} = stream_chunked(<<"3\r\nabc\r">>, {0, 0}), 307 | {more, <<"abc">>, <<"123">>, {0, 3}} = stream_chunked(<<"3\r\nabc\r\n123">>, {0, 0}), 308 | ok. 309 | 310 | stream_chunked_dripfeed_test() -> 311 | dripfeed(<< 312 | "4\r\n" 313 | "Wiki\r\n" 314 | "5\r\n" 315 | "pedia\r\n" 316 | "e\r\n" 317 | " in\r\n\r\nchunks.\r\n" 318 | "0\r\n" 319 | "\r\n">>, <<>>, {0, 0}, fun stream_chunked/2). 320 | 321 | do_body_to_chunks(_, <<>>, Acc) -> 322 | lists:reverse([<<"0\r\n\r\n">>|Acc]); 323 | do_body_to_chunks(ChunkSize, Body, Acc) -> 324 | BodySize = byte_size(Body), 325 | ChunkSize2 = case BodySize < ChunkSize of 326 | true -> BodySize; 327 | false -> ChunkSize 328 | end, 329 | << Chunk:ChunkSize2/binary, Rest/binary >> = Body, 330 | ChunkSizeBin = list_to_binary(integer_to_list(ChunkSize2, 16)), 331 | do_body_to_chunks(ChunkSize, Rest, 332 | [<< ChunkSizeBin/binary, "\r\n", Chunk/binary, "\r\n" >>|Acc]). 333 | 334 | stream_chunked_dripfeed2_test() -> 335 | Body = list_to_binary(io_lib:format("~p", [lists:seq(1, 100)])), 336 | Body2 = iolist_to_binary(do_body_to_chunks(50, Body, [])), 337 | dripfeed(Body2, <<>>, {0, 0}, fun stream_chunked/2). 338 | 339 | stream_chunked_error_test_() -> 340 | Tests = [ 341 | {<<>>, undefined}, 342 | {<<"\n\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa">>, {2, 0}} 343 | ], 344 | [{lists:flatten(io_lib:format("value ~p state ~p", [V, S])), 345 | fun() -> {'EXIT', _} = (catch stream_chunked(V, S)) end} 346 | || {V, S} <- Tests]. 347 | 348 | horse_stream_chunked() -> 349 | horse:repeat(10000, 350 | stream_chunked(<< 351 | "4\r\n" 352 | "Wiki\r\n" 353 | "5\r\n" 354 | "pedia\r\n" 355 | "e\r\n" 356 | " in\r\n\r\nchunks.\r\n" 357 | "0\r\n" 358 | "\r\n">>, {0, 0}) 359 | ). 360 | 361 | horse_stream_chunked_dripfeed() -> 362 | horse:repeat(10000, 363 | dripfeed(<< 364 | "4\r\n" 365 | "Wiki\r\n" 366 | "5\r\n" 367 | "pedia\r\n" 368 | "e\r\n" 369 | " in\r\n\r\nchunks.\r\n" 370 | "0\r\n" 371 | "\r\n">>, <<>>, {0, 43}, fun stream_chunked/2) 372 | ). 373 | -endif. 374 | -------------------------------------------------------------------------------- /src/cow_iolists.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) Loïc Hoguin 2 | %% 3 | %% Permission to use, copy, modify, and/or distribute this software for any 4 | %% purpose with or without fee is hereby granted, provided that the above 5 | %% copyright notice and this permission notice appear in all copies. 6 | %% 7 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | -module(cow_iolists). 16 | 17 | -export([split/2]). 18 | 19 | -ifdef(TEST). 20 | -include_lib("proper/include/proper.hrl"). 21 | -endif. 22 | 23 | -spec split(non_neg_integer(), iodata()) -> {iodata(), iodata()}. 24 | split(N, Iolist) -> 25 | case split(N, Iolist, []) of 26 | {ok, Before, After} -> 27 | {Before, After}; 28 | {more, _, Before} -> 29 | {lists:reverse(Before), <<>>} 30 | end. 31 | 32 | split(0, Rest, Acc) -> 33 | {ok, lists:reverse(Acc), Rest}; 34 | split(N, [], Acc) -> 35 | {more, N, Acc}; 36 | split(N, Binary, Acc) when byte_size(Binary) =< N -> 37 | {more, N - byte_size(Binary), [Binary|Acc]}; 38 | split(N, Binary, Acc) when is_binary(Binary) -> 39 | << Before:N/binary, After/bits >> = Binary, 40 | {ok, lists:reverse([Before|Acc]), After}; 41 | split(N, [Binary|Tail], Acc) when byte_size(Binary) =< N -> 42 | split(N - byte_size(Binary), Tail, [Binary|Acc]); 43 | split(N, [Binary|Tail], Acc) when is_binary(Binary) -> 44 | << Before:N/binary, After/bits >> = Binary, 45 | {ok, lists:reverse([Before|Acc]), [After|Tail]}; 46 | split(N, [Char|Tail], Acc) when is_integer(Char) -> 47 | split(N - 1, Tail, [Char|Acc]); 48 | split(N, [List|Tail], Acc0) -> 49 | case split(N, List, Acc0) of 50 | {ok, Before, After} -> 51 | {ok, Before, [After|Tail]}; 52 | {more, More, Acc} -> 53 | split(More, Tail, Acc) 54 | end. 55 | 56 | -ifdef(TEST). 57 | 58 | split_test_() -> 59 | Tests = [ 60 | {10, "Hello world!", "Hello worl", "d!"}, 61 | {10, <<"Hello world!">>, "Hello worl", "d!"}, 62 | {10, ["He", [<<"llo">>], $\s, [["world"], <<"!">>]], "Hello worl", "d!"}, 63 | {10, ["Hello "|<<"world!">>], "Hello worl", "d!"}, 64 | {10, "Hello!", "Hello!", ""}, 65 | {10, <<"Hello!">>, "Hello!", ""}, 66 | {10, ["He", [<<"ll">>], $o, [["!"]]], "Hello!", ""}, 67 | {10, ["Hel"|<<"lo!">>], "Hello!", ""}, 68 | {10, [[<<>>|<<>>], [], <<"Hello world!">>], "Hello worl", "d!"}, 69 | {10, [[<<"He">>|<<"llo">>], [$\s], <<"world!">>], "Hello worl", "d!"}, 70 | {10, [[[]|<<"He">>], [[]|<<"llo wor">>]|<<"ld!">>], "Hello worl", "d!"} 71 | ], 72 | [{iolist_to_binary(V), fun() -> 73 | {B, A} = split(N, V), 74 | true = iolist_to_binary(RB) =:= iolist_to_binary(B), 75 | true = iolist_to_binary(RA) =:= iolist_to_binary(A) 76 | end} || {N, V, RB, RA} <- Tests]. 77 | 78 | prop_split_test() -> 79 | ?FORALL({N, Input}, 80 | {non_neg_integer(), iolist()}, 81 | begin 82 | Size = iolist_size(Input), 83 | {Before, After} = split(N, Input), 84 | if 85 | N >= Size -> 86 | ((iolist_size(After) =:= 0) 87 | andalso iolist_to_binary(Before) =:= iolist_to_binary(Input)); 88 | true -> 89 | <> = iolist_to_binary(Input), 90 | (ExpectBefore =:= iolist_to_binary(Before)) 91 | andalso (ExpectAfter =:= iolist_to_binary(After)) 92 | end 93 | end). 94 | 95 | -endif. 96 | -------------------------------------------------------------------------------- /src/cow_link.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) Loïc Hoguin 2 | %% 3 | %% Permission to use, copy, modify, and/or distribute this software for any 4 | %% purpose with or without fee is hereby granted, provided that the above 5 | %% copyright notice and this permission notice appear in all copies. 6 | %% 7 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | -module(cow_link). 16 | -compile({no_auto_import, [link/1]}). 17 | 18 | -export([parse_link/1]). 19 | -export([resolve_link/2]). 20 | -export([resolve_link/3]). 21 | -export([link/1]). 22 | 23 | -include("cow_inline.hrl"). 24 | -include("cow_parse.hrl"). 25 | 26 | -type link() :: #{ 27 | target := binary(), 28 | rel := binary(), 29 | attributes := [{binary(), binary()}] 30 | }. 31 | -export_type([link/0]). 32 | 33 | -type resolve_opts() :: #{ 34 | allow_anchor => boolean() 35 | }. 36 | 37 | -type uri() :: uri_string:uri_map() | uri_string:uri_string() | undefined. 38 | 39 | %% Parse a link header. 40 | 41 | %% This function returns the URI target from the header directly. 42 | %% Relative URIs must then be resolved as per RFC3986 5. In some 43 | %% cases it might not be possible to resolve URIs, for example when 44 | %% the link header is returned with a 404 status code. 45 | -spec parse_link(binary()) -> [link()]. 46 | parse_link(Link) -> 47 | before_target(Link, []). 48 | 49 | before_target(<<>>, Acc) -> lists:reverse(Acc); 50 | before_target(<<$<,R/bits>>, Acc) -> target(R, Acc, <<>>); 51 | before_target(<>, Acc) when ?IS_WS(C) -> before_target(R, Acc). 52 | 53 | target(<<$>,R/bits>>, Acc, T) -> param_sep(R, Acc, T, []); 54 | target(<>, Acc, T) -> target(R, Acc, <>). 55 | 56 | param_sep(<<>>, Acc, T, P) -> lists:reverse(acc_link(Acc, T, P)); 57 | param_sep(<<$,,R/bits>>, Acc, T, P) -> before_target(R, acc_link(Acc, T, P)); 58 | param_sep(<<$;,R/bits>>, Acc, T, P) -> before_param(R, Acc, T, P); 59 | param_sep(<>, Acc, T, P) when ?IS_WS(C) -> param_sep(R, Acc, T, P). 60 | 61 | before_param(<>, Acc, T, P) when ?IS_WS(C) -> before_param(R, Acc, T, P); 62 | before_param(<>, Acc, T, P) when ?IS_TOKEN(C) -> ?LOWER(param, R, Acc, T, P, <<>>). 63 | 64 | param(<<$=,$",R/bits>>, Acc, T, P, K) -> quoted(R, Acc, T, P, K, <<>>); 65 | param(<<$=,C,R/bits>>, Acc, T, P, K) when ?IS_TOKEN(C) -> value(R, Acc, T, P, K, <>); 66 | param(<>, Acc, T, P, K) when ?IS_TOKEN(C) -> ?LOWER(param, R, Acc, T, P, K). 67 | 68 | quoted(<<$",R/bits>>, Acc, T, P, K, V) -> param_sep(R, Acc, T, [{K, V}|P]); 69 | quoted(<<$\\,C,R/bits>>, Acc, T, P, K, V) when ?IS_VCHAR_OBS(C) -> quoted(R, Acc, T, P, K, <>); 70 | quoted(<>, Acc, T, P, K, V) when ?IS_VCHAR_OBS(C) -> quoted(R, Acc, T, P, K, <>). 71 | 72 | value(<>, Acc, T, P, K, V) when ?IS_TOKEN(C) -> value(R, Acc, T, P, K, <>); 73 | value(R, Acc, T, P, K, V) -> param_sep(R, Acc, T, [{K, V}|P]). 74 | 75 | acc_link(Acc, Target, Params0) -> 76 | Params1 = lists:reverse(Params0), 77 | %% The rel parameter MUST be present. (RFC8288 3.3) 78 | {value, {_, Rel}, Params2} = lists:keytake(<<"rel">>, 1, Params1), 79 | %% Occurrences after the first MUST be ignored by parsers. 80 | Params = filter_out_duplicates(Params2, #{}), 81 | [#{ 82 | target => Target, 83 | rel => ?LOWER(Rel), 84 | attributes => Params 85 | }|Acc]. 86 | 87 | %% This function removes duplicates for attributes that don't allow them. 88 | filter_out_duplicates([], _) -> 89 | []; 90 | %% The "rel" is mandatory and was already removed from params. 91 | filter_out_duplicates([{<<"rel">>, _}|Tail], State) -> 92 | filter_out_duplicates(Tail, State); 93 | filter_out_duplicates([{<<"anchor">>, _}|Tail], State=#{anchor := true}) -> 94 | filter_out_duplicates(Tail, State); 95 | filter_out_duplicates([{<<"media">>, _}|Tail], State=#{media := true}) -> 96 | filter_out_duplicates(Tail, State); 97 | filter_out_duplicates([{<<"title">>, _}|Tail], State=#{title := true}) -> 98 | filter_out_duplicates(Tail, State); 99 | filter_out_duplicates([{<<"title*">>, _}|Tail], State=#{title_star := true}) -> 100 | filter_out_duplicates(Tail, State); 101 | filter_out_duplicates([{<<"type">>, _}|Tail], State=#{type := true}) -> 102 | filter_out_duplicates(Tail, State); 103 | filter_out_duplicates([Tuple={<<"anchor">>, _}|Tail], State) -> 104 | [Tuple|filter_out_duplicates(Tail, State#{anchor => true})]; 105 | filter_out_duplicates([Tuple={<<"media">>, _}|Tail], State) -> 106 | [Tuple|filter_out_duplicates(Tail, State#{media => true})]; 107 | filter_out_duplicates([Tuple={<<"title">>, _}|Tail], State) -> 108 | [Tuple|filter_out_duplicates(Tail, State#{title => true})]; 109 | filter_out_duplicates([Tuple={<<"title*">>, _}|Tail], State) -> 110 | [Tuple|filter_out_duplicates(Tail, State#{title_star => true})]; 111 | filter_out_duplicates([Tuple={<<"type">>, _}|Tail], State) -> 112 | [Tuple|filter_out_duplicates(Tail, State#{type => true})]; 113 | filter_out_duplicates([Tuple|Tail], State) -> 114 | [Tuple|filter_out_duplicates(Tail, State)]. 115 | 116 | -ifdef(TEST). 117 | parse_link_test_() -> 118 | Tests = [ 119 | {<<>>, []}, 120 | {<<" ">>, []}, 121 | %% Examples from the RFC. 122 | {<<"; rel=\"previous\"; title=\"previous chapter\"">>, [ 123 | #{ 124 | target => <<"http://example.com/TheBook/chapter2">>, 125 | rel => <<"previous">>, 126 | attributes => [ 127 | {<<"title">>, <<"previous chapter">>} 128 | ] 129 | } 130 | ]}, 131 | {<<"; rel=\"http://example.net/foo\"">>, [ 132 | #{ 133 | target => <<"/">>, 134 | rel => <<"http://example.net/foo">>, 135 | attributes => [] 136 | } 137 | ]}, 138 | {<<"; rel=\"copyright\"; anchor=\"#foo\"">>, [ 139 | #{ 140 | target => <<"/terms">>, 141 | rel => <<"copyright">>, 142 | attributes => [ 143 | {<<"anchor">>, <<"#foo">>} 144 | ] 145 | } 146 | ]}, 147 | % {<<"; rel=\"previous\"; title*=UTF-8'de'letztes%20Kapitel, " 148 | % "; rel=\"next\"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel">>, [ 149 | % %% @todo 150 | % ]} 151 | {<<"; rel=\"start http://example.net/relation/other\"">>, [ 152 | #{ 153 | target => <<"http://example.org/">>, 154 | rel => <<"start http://example.net/relation/other">>, 155 | attributes => [] 156 | } 157 | ]}, 158 | {<<"; rel=\"start\", " 159 | "; rel=\"index\"">>, [ 160 | #{ 161 | target => <<"https://example.org/">>, 162 | rel => <<"start">>, 163 | attributes => [] 164 | }, 165 | #{ 166 | target => <<"https://example.org/index">>, 167 | rel => <<"index">>, 168 | attributes => [] 169 | } 170 | ]}, 171 | %% Relation types are case insensitive. 172 | {<<"; rel=\"SELF\"">>, [ 173 | #{ 174 | target => <<"/">>, 175 | rel => <<"self">>, 176 | attributes => [] 177 | } 178 | ]}, 179 | {<<"; rel=\"HTTP://EXAMPLE.NET/FOO\"">>, [ 180 | #{ 181 | target => <<"/">>, 182 | rel => <<"http://example.net/foo">>, 183 | attributes => [] 184 | } 185 | ]}, 186 | %% Attribute names are case insensitive. 187 | {<<"; REL=\"copyright\"; ANCHOR=\"#foo\"">>, [ 188 | #{ 189 | target => <<"/terms">>, 190 | rel => <<"copyright">>, 191 | attributes => [ 192 | {<<"anchor">>, <<"#foo">>} 193 | ] 194 | } 195 | ]} 196 | ], 197 | [{V, fun() -> R = parse_link(V) end} || {V, R} <- Tests]. 198 | -endif. 199 | 200 | %% Resolve a link based on the context URI and options. 201 | 202 | -spec resolve_link(Link, uri()) -> Link | false when Link::link(). 203 | resolve_link(Link, ContextURI) -> 204 | resolve_link(Link, ContextURI, #{}). 205 | 206 | -spec resolve_link(Link, uri(), resolve_opts()) -> Link | false when Link::link(). 207 | %% When we do not have a context URI we only succeed when the target URI is absolute. 208 | %% The target URI will only be normalized in that case. 209 | resolve_link(Link=#{target := TargetURI}, undefined, _) -> 210 | case uri_string:parse(TargetURI) of 211 | URIMap = #{scheme := _} -> 212 | Link#{target => uri_string:normalize(URIMap)}; 213 | _ -> 214 | false 215 | end; 216 | resolve_link(Link=#{attributes := Params}, ContextURI, Opts) -> 217 | AllowAnchor = maps:get(allow_anchor, Opts, true), 218 | case lists:keyfind(<<"anchor">>, 1, Params) of 219 | false -> 220 | do_resolve_link(Link, ContextURI); 221 | {_, Anchor} when AllowAnchor -> 222 | do_resolve_link(Link, resolve(Anchor, ContextURI)); 223 | _ -> 224 | false 225 | end. 226 | 227 | do_resolve_link(Link=#{target := TargetURI}, ContextURI) -> 228 | Link#{target => uri_string:recompose(resolve(TargetURI, ContextURI))}. 229 | 230 | -ifdef(TEST). 231 | resolve_link_test_() -> 232 | Tests = [ 233 | %% No context URI available. 234 | {#{target => <<"http://a/b/./c">>}, undefined, #{}, 235 | #{target => <<"http://a/b/c">>}}, 236 | {#{target => <<"a/b/./c">>}, undefined, #{}, 237 | false}, 238 | %% Context URI available, allow_anchor => true. 239 | {#{target => <<"http://a/b">>, attributes => []}, <<"http://a/c">>, #{}, 240 | #{target => <<"http://a/b">>, attributes => []}}, 241 | {#{target => <<"b">>, attributes => []}, <<"http://a/c">>, #{}, 242 | #{target => <<"http://a/b">>, attributes => []}}, 243 | {#{target => <<"b">>, attributes => [{<<"anchor">>, <<"#frag">>}]}, <<"http://a/c">>, #{}, 244 | #{target => <<"http://a/b">>, attributes => [{<<"anchor">>, <<"#frag">>}]}}, 245 | {#{target => <<"b">>, attributes => [{<<"anchor">>, <<"d/e">>}]}, <<"http://a/c">>, #{}, 246 | #{target => <<"http://a/d/b">>, attributes => [{<<"anchor">>, <<"d/e">>}]}}, 247 | %% Context URI available, allow_anchor => false. 248 | {#{target => <<"http://a/b">>, attributes => []}, <<"http://a/c">>, #{allow_anchor => false}, 249 | #{target => <<"http://a/b">>, attributes => []}}, 250 | {#{target => <<"b">>, attributes => []}, <<"http://a/c">>, #{allow_anchor => false}, 251 | #{target => <<"http://a/b">>, attributes => []}}, 252 | {#{target => <<"b">>, attributes => [{<<"anchor">>, <<"#frag">>}]}, 253 | <<"http://a/c">>, #{allow_anchor => false}, false}, 254 | {#{target => <<"b">>, attributes => [{<<"anchor">>, <<"d/e">>}]}, 255 | <<"http://a/c">>, #{allow_anchor => false}, false} 256 | ], 257 | [{iolist_to_binary(io_lib:format("~0p", [L])), 258 | fun() -> R = resolve_link(L, C, O) end} || {L, C, O, R} <- Tests]. 259 | -endif. 260 | 261 | %% @todo This function has been added to Erlang/OTP 22.3 as uri_string:resolve/2,3. 262 | resolve(URI, BaseURI) -> 263 | case resolve1(ensure_map_uri(URI), BaseURI) of 264 | TargetURI = #{path := Path0} -> 265 | %% We remove dot segments. Normalizing the entire URI 266 | %% will sometimes add an extra slash we don't want. 267 | #{path := Path} = uri_string:normalize(#{path => Path0}, [return_map]), 268 | TargetURI#{path => Path}; 269 | TargetURI -> 270 | TargetURI 271 | end. 272 | 273 | resolve1(URI=#{scheme := _}, _) -> 274 | URI; 275 | resolve1(URI=#{host := _}, BaseURI) -> 276 | #{scheme := Scheme} = ensure_map_uri(BaseURI), 277 | URI#{scheme => Scheme}; 278 | resolve1(URI=#{path := <<>>}, BaseURI0) -> 279 | BaseURI = ensure_map_uri(BaseURI0), 280 | Keys = case maps:is_key(query, URI) of 281 | true -> [scheme, host, port, path]; 282 | false -> [scheme, host, port, path, query] 283 | end, 284 | maps:merge(URI, maps:with(Keys, BaseURI)); 285 | resolve1(URI=#{path := <<"/",_/bits>>}, BaseURI0) -> 286 | BaseURI = ensure_map_uri(BaseURI0), 287 | maps:merge(URI, maps:with([scheme, host, port], BaseURI)); 288 | resolve1(URI=#{path := Path}, BaseURI0) -> 289 | BaseURI = ensure_map_uri(BaseURI0), 290 | maps:merge( 291 | URI#{path := merge_paths(Path, BaseURI)}, 292 | maps:with([scheme, host, port], BaseURI)). 293 | 294 | merge_paths(Path, #{host := _, path := <<>>}) -> 295 | <<$/, Path/binary>>; 296 | merge_paths(Path, #{path := BasePath0}) -> 297 | case string:split(BasePath0, <<$/>>, trailing) of 298 | [BasePath, _] -> <>; 299 | [_] -> <<$/, Path/binary>> 300 | end. 301 | 302 | ensure_map_uri(URI) when is_map(URI) -> URI; 303 | ensure_map_uri(URI) -> uri_string:parse(iolist_to_binary(URI)). 304 | 305 | -ifdef(TEST). 306 | resolve_test_() -> 307 | Tests = [ 308 | %% 5.4.1. Normal Examples 309 | {<<"g:h">>, <<"g:h">>}, 310 | {<<"g">>, <<"http://a/b/c/g">>}, 311 | {<<"./g">>, <<"http://a/b/c/g">>}, 312 | {<<"g/">>, <<"http://a/b/c/g/">>}, 313 | {<<"/g">>, <<"http://a/g">>}, 314 | {<<"//g">>, <<"http://g">>}, 315 | {<<"?y">>, <<"http://a/b/c/d;p?y">>}, 316 | {<<"g?y">>, <<"http://a/b/c/g?y">>}, 317 | {<<"#s">>, <<"http://a/b/c/d;p?q#s">>}, 318 | {<<"g#s">>, <<"http://a/b/c/g#s">>}, 319 | {<<"g?y#s">>, <<"http://a/b/c/g?y#s">>}, 320 | {<<";x">>, <<"http://a/b/c/;x">>}, 321 | {<<"g;x">>, <<"http://a/b/c/g;x">>}, 322 | {<<"g;x?y#s">>, <<"http://a/b/c/g;x?y#s">>}, 323 | {<<"">>, <<"http://a/b/c/d;p?q">>}, 324 | {<<".">>, <<"http://a/b/c/">>}, 325 | {<<"./">>, <<"http://a/b/c/">>}, 326 | {<<"..">>, <<"http://a/b/">>}, 327 | {<<"../">>, <<"http://a/b/">>}, 328 | {<<"../g">>, <<"http://a/b/g">>}, 329 | {<<"../..">>, <<"http://a/">>}, 330 | {<<"../../">>, <<"http://a/">>}, 331 | {<<"../../g">>, <<"http://a/g">>}, 332 | %% 5.4.2. Abnormal Examples 333 | {<<"../../../g">>, <<"http://a/g">>}, 334 | {<<"../../../../g">>, <<"http://a/g">>}, 335 | {<<"/./g">>, <<"http://a/g">>}, 336 | {<<"/../g">>, <<"http://a/g">>}, 337 | {<<"g.">>, <<"http://a/b/c/g.">>}, 338 | {<<".g">>, <<"http://a/b/c/.g">>}, 339 | {<<"g..">>, <<"http://a/b/c/g..">>}, 340 | {<<"..g">>, <<"http://a/b/c/..g">>}, 341 | {<<"./../g">>, <<"http://a/b/g">>}, 342 | {<<"./g/.">>, <<"http://a/b/c/g/">>}, 343 | {<<"g/./h">>, <<"http://a/b/c/g/h">>}, 344 | {<<"g/../h">>, <<"http://a/b/c/h">>}, 345 | {<<"g;x=1/./y">>, <<"http://a/b/c/g;x=1/y">>}, 346 | {<<"g;x=1/../y">>, <<"http://a/b/c/y">>}, 347 | {<<"g?y/./x">>, <<"http://a/b/c/g?y/./x">>}, 348 | {<<"g?y/../x">>, <<"http://a/b/c/g?y/../x">>}, 349 | {<<"g#s/./x">>, <<"http://a/b/c/g#s/./x">>}, 350 | {<<"g#s/../x">>, <<"http://a/b/c/g#s/../x">>}, 351 | {<<"http:g">>, <<"http:g">>} %% for strict parsers 352 | ], 353 | [{V, fun() -> R = uri_string:recompose(resolve(V, <<"http://a/b/c/d;p?q">>)) end} || {V, R} <- Tests]. 354 | -endif. 355 | 356 | %% Build a link header. 357 | 358 | -spec link([#{ 359 | target := binary(), 360 | rel := binary(), 361 | attributes := [{binary(), binary()}] 362 | }]) -> iodata(). 363 | link(Links) -> 364 | lists:join(<<", ">>, [do_link(Link) || Link <- Links]). 365 | 366 | do_link(#{target := TargetURI, rel := Rel, attributes := Params}) -> 367 | [ 368 | $<, TargetURI, <<">" 369 | "; rel=\"">>, Rel, $", 370 | [[<<"; ">>, Key, <<"=\"">>, escape(iolist_to_binary(Value), <<>>), $"] 371 | || {Key, Value} <- Params] 372 | ]. 373 | 374 | escape(<<>>, Acc) -> Acc; 375 | escape(<<$\\,R/bits>>, Acc) -> escape(R, <>); 376 | escape(<<$\",R/bits>>, Acc) -> escape(R, <>); 377 | escape(<>, Acc) -> escape(R, <>). 378 | 379 | -ifdef(TEST). 380 | link_test_() -> 381 | Tests = [ 382 | {<<>>, []}, 383 | %% Examples from the RFC. 384 | {<<"; rel=\"previous\"; title=\"previous chapter\"">>, [ 385 | #{ 386 | target => <<"http://example.com/TheBook/chapter2">>, 387 | rel => <<"previous">>, 388 | attributes => [ 389 | {<<"title">>, <<"previous chapter">>} 390 | ] 391 | } 392 | ]}, 393 | {<<"; rel=\"http://example.net/foo\"">>, [ 394 | #{ 395 | target => <<"/">>, 396 | rel => <<"http://example.net/foo">>, 397 | attributes => [] 398 | } 399 | ]}, 400 | {<<"; rel=\"copyright\"; anchor=\"#foo\"">>, [ 401 | #{ 402 | target => <<"/terms">>, 403 | rel => <<"copyright">>, 404 | attributes => [ 405 | {<<"anchor">>, <<"#foo">>} 406 | ] 407 | } 408 | ]}, 409 | % {<<"; rel=\"previous\"; title*=UTF-8'de'letztes%20Kapitel, " 410 | % "; rel=\"next\"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel">>, [ 411 | % %% @todo 412 | % ]} 413 | {<<"; rel=\"start http://example.net/relation/other\"">>, [ 414 | #{ 415 | target => <<"http://example.org/">>, 416 | rel => <<"start http://example.net/relation/other">>, 417 | attributes => [] 418 | } 419 | ]}, 420 | {<<"; rel=\"start\", " 421 | "; rel=\"index\"">>, [ 422 | #{ 423 | target => <<"https://example.org/">>, 424 | rel => <<"start">>, 425 | attributes => [] 426 | }, 427 | #{ 428 | target => <<"https://example.org/index">>, 429 | rel => <<"index">>, 430 | attributes => [] 431 | } 432 | ]}, 433 | {<<"; rel=\"previous\"; quoted=\"name=\\\"value\\\"\"">>, [ 434 | #{ 435 | target => <<"/">>, 436 | rel => <<"previous">>, 437 | attributes => [ 438 | {<<"quoted">>, <<"name=\"value\"">>} 439 | ] 440 | } 441 | ]} 442 | ], 443 | [{iolist_to_binary(io_lib:format("~0p", [V])), 444 | fun() -> R = iolist_to_binary(link(V)) end} || {R, V} <- Tests]. 445 | -endif. 446 | -------------------------------------------------------------------------------- /src/cow_mimetypes.erl.src: -------------------------------------------------------------------------------- 1 | %% Copyright (c) Loïc Hoguin 2 | %% 3 | %% Permission to use, copy, modify, and/or distribute this software for any 4 | %% purpose with or without fee is hereby granted, provided that the above 5 | %% copyright notice and this permission notice appear in all copies. 6 | %% 7 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | -module(cow_mimetypes). 16 | 17 | -export([all/1]). 18 | -export([web/1]). 19 | 20 | %% @doc Return the mimetype for any file by looking at its extension. 21 | 22 | -spec all(binary()) -> {binary(), binary(), []}. 23 | all(Path) -> 24 | case filename:extension(Path) of 25 | <<>> -> {<<"application">>, <<"octet-stream">>, []}; 26 | %% @todo Convert to string:lowercase on OTP-20+. 27 | << $., Ext/binary >> -> all_ext(list_to_binary(string:to_lower(binary_to_list(Ext)))) 28 | end. 29 | 30 | %% @doc Return the mimetype for a Web related file by looking at its extension. 31 | 32 | -spec web(binary()) -> {binary(), binary(), []}. 33 | web(Path) -> 34 | case filename:extension(Path) of 35 | <<>> -> {<<"application">>, <<"octet-stream">>, []}; 36 | %% @todo Convert to string:lowercase on OTP-20+. 37 | << $., Ext/binary >> -> web_ext(list_to_binary(string:to_lower(binary_to_list(Ext)))) 38 | end. 39 | 40 | %% Internal. 41 | 42 | %% GENERATED 43 | all_ext(_) -> {<<"application">>, <<"octet-stream">>, []}. 44 | 45 | web_ext(<<"css">>) -> {<<"text">>, <<"css">>, []}; 46 | web_ext(<<"gif">>) -> {<<"image">>, <<"gif">>, []}; 47 | web_ext(<<"html">>) -> {<<"text">>, <<"html">>, []}; 48 | web_ext(<<"htm">>) -> {<<"text">>, <<"html">>, []}; 49 | web_ext(<<"ico">>) -> {<<"image">>, <<"x-icon">>, []}; 50 | web_ext(<<"jpeg">>) -> {<<"image">>, <<"jpeg">>, []}; 51 | web_ext(<<"jpg">>) -> {<<"image">>, <<"jpeg">>, []}; 52 | web_ext(<<"js">>) -> {<<"application">>, <<"javascript">>, []}; 53 | web_ext(<<"mjs">>) -> {<<"text">>, <<"javascript">>, []}; 54 | web_ext(<<"mp3">>) -> {<<"audio">>, <<"mpeg">>, []}; 55 | web_ext(<<"mp4">>) -> {<<"video">>, <<"mp4">>, []}; 56 | web_ext(<<"ogg">>) -> {<<"audio">>, <<"ogg">>, []}; 57 | web_ext(<<"ogv">>) -> {<<"video">>, <<"ogg">>, []}; 58 | web_ext(<<"png">>) -> {<<"image">>, <<"png">>, []}; 59 | web_ext(<<"svg">>) -> {<<"image">>, <<"svg+xml">>, []}; 60 | web_ext(<<"wav">>) -> {<<"audio">>, <<"x-wav">>, []}; 61 | web_ext(<<"webm">>) -> {<<"video">>, <<"webm">>, []}; 62 | web_ext(_) -> {<<"application">>, <<"octet-stream">>, []}. 63 | -------------------------------------------------------------------------------- /src/cow_spdy.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) Loïc Hoguin 2 | %% 3 | %% Permission to use, copy, modify, and/or distribute this software for any 4 | %% purpose with or without fee is hereby granted, provided that the above 5 | %% copyright notice and this permission notice appear in all copies. 6 | %% 7 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | -module(cow_spdy). 16 | 17 | %% Zstream. 18 | -export([deflate_init/0]). 19 | -export([inflate_init/0]). 20 | 21 | %% Parse. 22 | -export([split/1]). 23 | -export([parse/2]). 24 | 25 | %% Build. 26 | -export([data/3]). 27 | -export([syn_stream/12]). 28 | -export([syn_reply/6]). 29 | -export([rst_stream/2]). 30 | -export([settings/2]). 31 | -export([ping/1]). 32 | -export([goaway/2]). 33 | %% @todo headers 34 | %% @todo window_update 35 | 36 | -include("cow_spdy.hrl"). 37 | 38 | %% Zstream. 39 | 40 | deflate_init() -> 41 | Zdef = zlib:open(), 42 | ok = zlib:deflateInit(Zdef), 43 | _ = zlib:deflateSetDictionary(Zdef, ?ZDICT), 44 | Zdef. 45 | 46 | inflate_init() -> 47 | Zinf = zlib:open(), 48 | ok = zlib:inflateInit(Zinf), 49 | Zinf. 50 | 51 | %% Parse. 52 | 53 | split(Data = << _:40, Length:24, _/bits >>) 54 | when byte_size(Data) >= Length + 8 -> 55 | Length2 = Length + 8, 56 | << Frame:Length2/binary, Rest/bits >> = Data, 57 | {true, Frame, Rest}; 58 | split(_) -> 59 | false. 60 | 61 | parse(<< 0:1, StreamID:31, 0:7, IsFinFlag:1, _:24, Data/bits >>, _) -> 62 | {data, StreamID, from_flag(IsFinFlag), Data}; 63 | parse(<< 1:1, 3:15, 1:16, 0:6, IsUnidirectionalFlag:1, IsFinFlag:1, 64 | _:25, StreamID:31, _:1, AssocToStreamID:31, Priority:3, _:5, 65 | 0:8, Rest/bits >>, Zinf) -> 66 | case parse_headers(Rest, Zinf) of 67 | {ok, Headers, [{<<":host">>, Host}, {<<":method">>, Method}, 68 | {<<":path">>, Path}, {<<":scheme">>, Scheme}, 69 | {<<":version">>, Version}]} -> 70 | {syn_stream, StreamID, AssocToStreamID, from_flag(IsFinFlag), 71 | from_flag(IsUnidirectionalFlag), Priority, Method, 72 | Scheme, Host, Path, Version, Headers}; 73 | _ -> 74 | {error, badprotocol} 75 | end; 76 | parse(<< 1:1, 3:15, 2:16, 0:7, IsFinFlag:1, _:25, 77 | StreamID:31, Rest/bits >>, Zinf) -> 78 | case parse_headers(Rest, Zinf) of 79 | {ok, Headers, [{<<":status">>, Status}, {<<":version">>, Version}]} -> 80 | {syn_reply, StreamID, from_flag(IsFinFlag), 81 | Status, Version, Headers}; 82 | _ -> 83 | {error, badprotocol} 84 | end; 85 | parse(<< 1:1, 3:15, 3:16, 0:8, _:56, StatusCode:32 >>, _) 86 | when StatusCode =:= 0; StatusCode > 11 -> 87 | {error, badprotocol}; 88 | parse(<< 1:1, 3:15, 3:16, 0:8, _:25, StreamID:31, StatusCode:32 >>, _) -> 89 | Status = case StatusCode of 90 | 1 -> protocol_error; 91 | 2 -> invalid_stream; 92 | 3 -> refused_stream; 93 | 4 -> unsupported_version; 94 | 5 -> cancel; 95 | 6 -> internal_error; 96 | 7 -> flow_control_error; 97 | 8 -> stream_in_use; 98 | 9 -> stream_already_closed; 99 | 10 -> invalid_credentials; 100 | 11 -> frame_too_large 101 | end, 102 | {rst_stream, StreamID, Status}; 103 | parse(<< 1:1, 3:15, 4:16, 0:7, ClearSettingsFlag:1, _:24, 104 | NbEntries:32, Rest/bits >>, _) -> 105 | try 106 | Settings = [begin 107 | Is0 = 0, 108 | Key = case ID of 109 | 1 -> upload_bandwidth; 110 | 2 -> download_bandwidth; 111 | 3 -> round_trip_time; 112 | 4 -> max_concurrent_streams; 113 | 5 -> current_cwnd; 114 | 6 -> download_retrans_rate; 115 | 7 -> initial_window_size; 116 | 8 -> client_certificate_vector_size 117 | end, 118 | {Key, Value, from_flag(PersistFlag), from_flag(WasPersistedFlag)} 119 | end || << Is0:6, WasPersistedFlag:1, PersistFlag:1, 120 | ID:24, Value:32 >> <= Rest], 121 | NbEntries = length(Settings), 122 | {settings, from_flag(ClearSettingsFlag), Settings} 123 | catch _:_ -> 124 | {error, badprotocol} 125 | end; 126 | parse(<< 1:1, 3:15, 6:16, 0:8, _:24, PingID:32 >>, _) -> 127 | {ping, PingID}; 128 | parse(<< 1:1, 3:15, 7:16, 0:8, _:56, StatusCode:32 >>, _) 129 | when StatusCode > 2 -> 130 | {error, badprotocol}; 131 | parse(<< 1:1, 3:15, 7:16, 0:8, _:25, LastGoodStreamID:31, 132 | StatusCode:32 >>, _) -> 133 | Status = case StatusCode of 134 | 0 -> ok; 135 | 1 -> protocol_error; 136 | 2 -> internal_error 137 | end, 138 | {goaway, LastGoodStreamID, Status}; 139 | parse(<< 1:1, 3:15, 8:16, 0:7, IsFinFlag:1, _:25, StreamID:31, 140 | Rest/bits >>, Zinf) -> 141 | case parse_headers(Rest, Zinf) of 142 | {ok, Headers, []} -> 143 | {headers, StreamID, from_flag(IsFinFlag), Headers}; 144 | _ -> 145 | {error, badprotocol} 146 | end; 147 | parse(<< 1:1, 3:15, 9:16, 0:8, _:57, 0:31 >>, _) -> 148 | {error, badprotocol}; 149 | parse(<< 1:1, 3:15, 9:16, 0:8, _:25, StreamID:31, 150 | _:1, DeltaWindowSize:31 >>, _) -> 151 | {window_update, StreamID, DeltaWindowSize}; 152 | parse(_, _) -> 153 | {error, badprotocol}. 154 | 155 | parse_headers(Data, Zinf) -> 156 | [<< NbHeaders:32, Rest/bits >>] = inflate(Zinf, Data), 157 | parse_headers(Rest, NbHeaders, [], []). 158 | 159 | parse_headers(<<>>, 0, Headers, SpHeaders) -> 160 | {ok, lists:reverse(Headers), lists:sort(SpHeaders)}; 161 | parse_headers(<<>>, _, _, _) -> 162 | error; 163 | parse_headers(_, 0, _, _) -> 164 | error; 165 | parse_headers(<< 0:32, _/bits >>, _, _, _) -> 166 | error; 167 | parse_headers(<< L1:32, Key:L1/binary, L2:32, Value:L2/binary, Rest/bits >>, 168 | NbHeaders, Acc, SpAcc) -> 169 | case Key of 170 | << $:, _/bits >> -> 171 | parse_headers(Rest, NbHeaders - 1, Acc, 172 | lists:keystore(Key, 1, SpAcc, {Key, Value})); 173 | _ -> 174 | parse_headers(Rest, NbHeaders - 1, [{Key, Value}|Acc], SpAcc) 175 | end. 176 | 177 | inflate(Zinf, Data) -> 178 | try 179 | zlib:inflate(Zinf, Data) 180 | catch _:_ -> 181 | ok = zlib:inflateSetDictionary(Zinf, ?ZDICT), 182 | zlib:inflate(Zinf, <<>>) 183 | end. 184 | 185 | from_flag(0) -> false; 186 | from_flag(1) -> true. 187 | 188 | %% Build. 189 | 190 | data(StreamID, IsFin, Data) -> 191 | IsFinFlag = to_flag(IsFin), 192 | Length = iolist_size(Data), 193 | [<< 0:1, StreamID:31, 0:7, IsFinFlag:1, Length:24 >>, Data]. 194 | 195 | syn_stream(Zdef, StreamID, AssocToStreamID, IsFin, IsUnidirectional, 196 | Priority, Method, Scheme, Host, Path, Version, Headers) -> 197 | IsFinFlag = to_flag(IsFin), 198 | IsUnidirectionalFlag = to_flag(IsUnidirectional), 199 | HeaderBlock = build_headers(Zdef, [ 200 | {<<":method">>, Method}, 201 | {<<":scheme">>, Scheme}, 202 | {<<":host">>, Host}, 203 | {<<":path">>, Path}, 204 | {<<":version">>, Version} 205 | |Headers]), 206 | Length = 10 + iolist_size(HeaderBlock), 207 | [<< 1:1, 3:15, 1:16, 0:6, IsUnidirectionalFlag:1, IsFinFlag:1, 208 | Length:24, 0:1, StreamID:31, 0:1, AssocToStreamID:31, 209 | Priority:3, 0:5, 0:8 >>, HeaderBlock]. 210 | 211 | syn_reply(Zdef, StreamID, IsFin, Status, Version, Headers) -> 212 | IsFinFlag = to_flag(IsFin), 213 | HeaderBlock = build_headers(Zdef, [ 214 | {<<":status">>, Status}, 215 | {<<":version">>, Version} 216 | |Headers]), 217 | Length = 4 + iolist_size(HeaderBlock), 218 | [<< 1:1, 3:15, 2:16, 0:7, IsFinFlag:1, Length:24, 219 | 0:1, StreamID:31 >>, HeaderBlock]. 220 | 221 | rst_stream(StreamID, Status) -> 222 | StatusCode = case Status of 223 | protocol_error -> 1; 224 | invalid_stream -> 2; 225 | refused_stream -> 3; 226 | unsupported_version -> 4; 227 | cancel -> 5; 228 | internal_error -> 6; 229 | flow_control_error -> 7; 230 | stream_in_use -> 8; 231 | stream_already_closed -> 9; 232 | invalid_credentials -> 10; 233 | frame_too_large -> 11 234 | end, 235 | << 1:1, 3:15, 3:16, 0:8, 8:24, 236 | 0:1, StreamID:31, StatusCode:32 >>. 237 | 238 | settings(ClearSettingsFlag, Settings) -> 239 | IsClearSettingsFlag = to_flag(ClearSettingsFlag), 240 | NbEntries = length(Settings), 241 | Entries = [begin 242 | IsWasPersistedFlag = to_flag(WasPersistedFlag), 243 | IsPersistFlag = to_flag(PersistFlag), 244 | ID = case Key of 245 | upload_bandwidth -> 1; 246 | download_bandwidth -> 2; 247 | round_trip_time -> 3; 248 | max_concurrent_streams -> 4; 249 | current_cwnd -> 5; 250 | download_retrans_rate -> 6; 251 | initial_window_size -> 7; 252 | client_certificate_vector_size -> 8 253 | end, 254 | << 0:6, IsWasPersistedFlag:1, IsPersistFlag:1, ID:24, Value:32 >> 255 | end || {Key, Value, WasPersistedFlag, PersistFlag} <- Settings], 256 | Length = 4 + iolist_size(Entries), 257 | [<< 1:1, 3:15, 4:16, 0:7, IsClearSettingsFlag:1, Length:24, 258 | NbEntries:32 >>, Entries]. 259 | 260 | -ifdef(TEST). 261 | settings_frame_test() -> 262 | ClearSettingsFlag = false, 263 | Settings = [{max_concurrent_streams,1000,false,false}, 264 | {initial_window_size,10485760,false,false}], 265 | Bin = list_to_binary(cow_spdy:settings(ClearSettingsFlag, Settings)), 266 | P = cow_spdy:parse(Bin, undefined), 267 | P = {settings, ClearSettingsFlag, Settings}, 268 | ok. 269 | -endif. 270 | 271 | ping(PingID) -> 272 | << 1:1, 3:15, 6:16, 0:8, 4:24, PingID:32 >>. 273 | 274 | goaway(LastGoodStreamID, Status) -> 275 | StatusCode = case Status of 276 | ok -> 0; 277 | protocol_error -> 1; 278 | internal_error -> 2 279 | end, 280 | << 1:1, 3:15, 7:16, 0:8, 8:24, 281 | 0:1, LastGoodStreamID:31, StatusCode:32 >>. 282 | 283 | %% @todo headers 284 | %% @todo window_update 285 | 286 | build_headers(Zdef, Headers) -> 287 | Headers1 = merge_headers(lists:sort(Headers), []), 288 | NbHeaders = length(Headers1), 289 | Headers2 = [begin 290 | L1 = iolist_size(Key), 291 | L2 = iolist_size(Value), 292 | [<< L1:32 >>, Key, << L2:32 >>, Value] 293 | end || {Key, Value} <- Headers1], 294 | zlib:deflate(Zdef, [<< NbHeaders:32 >>, Headers2], full). 295 | 296 | merge_headers([], Acc) -> 297 | lists:reverse(Acc); 298 | merge_headers([{Name, Value1}, {Name, Value2}|Tail], Acc) -> 299 | merge_headers([{Name, [Value1, 0, Value2]}|Tail], Acc); 300 | merge_headers([Head|Tail], Acc) -> 301 | merge_headers(Tail, [Head|Acc]). 302 | 303 | -ifdef(TEST). 304 | merge_headers_test_() -> 305 | Tests = [ 306 | {[{<<"set-cookie">>, <<"session=123">>}, {<<"set-cookie">>, <<"other=456">>}, {<<"content-type">>, <<"text/html">>}], 307 | [{<<"set-cookie">>, [<<"session=123">>, 0, <<"other=456">>]}, {<<"content-type">>, <<"text/html">>}]} 308 | ], 309 | [fun() -> D = merge_headers(R, []) end || {R, D} <- Tests]. 310 | -endif. 311 | 312 | to_flag(false) -> 0; 313 | to_flag(true) -> 1. 314 | -------------------------------------------------------------------------------- /src/cow_spdy.hrl: -------------------------------------------------------------------------------- 1 | %% Zlib dictionary. 2 | 3 | -define(ZDICT, << 4 | 16#00, 16#00, 16#00, 16#07, 16#6f, 16#70, 16#74, 16#69, 5 | 16#6f, 16#6e, 16#73, 16#00, 16#00, 16#00, 16#04, 16#68, 6 | 16#65, 16#61, 16#64, 16#00, 16#00, 16#00, 16#04, 16#70, 7 | 16#6f, 16#73, 16#74, 16#00, 16#00, 16#00, 16#03, 16#70, 8 | 16#75, 16#74, 16#00, 16#00, 16#00, 16#06, 16#64, 16#65, 9 | 16#6c, 16#65, 16#74, 16#65, 16#00, 16#00, 16#00, 16#05, 10 | 16#74, 16#72, 16#61, 16#63, 16#65, 16#00, 16#00, 16#00, 11 | 16#06, 16#61, 16#63, 16#63, 16#65, 16#70, 16#74, 16#00, 12 | 16#00, 16#00, 16#0e, 16#61, 16#63, 16#63, 16#65, 16#70, 13 | 16#74, 16#2d, 16#63, 16#68, 16#61, 16#72, 16#73, 16#65, 14 | 16#74, 16#00, 16#00, 16#00, 16#0f, 16#61, 16#63, 16#63, 15 | 16#65, 16#70, 16#74, 16#2d, 16#65, 16#6e, 16#63, 16#6f, 16 | 16#64, 16#69, 16#6e, 16#67, 16#00, 16#00, 16#00, 16#0f, 17 | 16#61, 16#63, 16#63, 16#65, 16#70, 16#74, 16#2d, 16#6c, 18 | 16#61, 16#6e, 16#67, 16#75, 16#61, 16#67, 16#65, 16#00, 19 | 16#00, 16#00, 16#0d, 16#61, 16#63, 16#63, 16#65, 16#70, 20 | 16#74, 16#2d, 16#72, 16#61, 16#6e, 16#67, 16#65, 16#73, 21 | 16#00, 16#00, 16#00, 16#03, 16#61, 16#67, 16#65, 16#00, 22 | 16#00, 16#00, 16#05, 16#61, 16#6c, 16#6c, 16#6f, 16#77, 23 | 16#00, 16#00, 16#00, 16#0d, 16#61, 16#75, 16#74, 16#68, 24 | 16#6f, 16#72, 16#69, 16#7a, 16#61, 16#74, 16#69, 16#6f, 25 | 16#6e, 16#00, 16#00, 16#00, 16#0d, 16#63, 16#61, 16#63, 26 | 16#68, 16#65, 16#2d, 16#63, 16#6f, 16#6e, 16#74, 16#72, 27 | 16#6f, 16#6c, 16#00, 16#00, 16#00, 16#0a, 16#63, 16#6f, 28 | 16#6e, 16#6e, 16#65, 16#63, 16#74, 16#69, 16#6f, 16#6e, 29 | 16#00, 16#00, 16#00, 16#0c, 16#63, 16#6f, 16#6e, 16#74, 30 | 16#65, 16#6e, 16#74, 16#2d, 16#62, 16#61, 16#73, 16#65, 31 | 16#00, 16#00, 16#00, 16#10, 16#63, 16#6f, 16#6e, 16#74, 32 | 16#65, 16#6e, 16#74, 16#2d, 16#65, 16#6e, 16#63, 16#6f, 33 | 16#64, 16#69, 16#6e, 16#67, 16#00, 16#00, 16#00, 16#10, 34 | 16#63, 16#6f, 16#6e, 16#74, 16#65, 16#6e, 16#74, 16#2d, 35 | 16#6c, 16#61, 16#6e, 16#67, 16#75, 16#61, 16#67, 16#65, 36 | 16#00, 16#00, 16#00, 16#0e, 16#63, 16#6f, 16#6e, 16#74, 37 | 16#65, 16#6e, 16#74, 16#2d, 16#6c, 16#65, 16#6e, 16#67, 38 | 16#74, 16#68, 16#00, 16#00, 16#00, 16#10, 16#63, 16#6f, 39 | 16#6e, 16#74, 16#65, 16#6e, 16#74, 16#2d, 16#6c, 16#6f, 40 | 16#63, 16#61, 16#74, 16#69, 16#6f, 16#6e, 16#00, 16#00, 41 | 16#00, 16#0b, 16#63, 16#6f, 16#6e, 16#74, 16#65, 16#6e, 42 | 16#74, 16#2d, 16#6d, 16#64, 16#35, 16#00, 16#00, 16#00, 43 | 16#0d, 16#63, 16#6f, 16#6e, 16#74, 16#65, 16#6e, 16#74, 44 | 16#2d, 16#72, 16#61, 16#6e, 16#67, 16#65, 16#00, 16#00, 45 | 16#00, 16#0c, 16#63, 16#6f, 16#6e, 16#74, 16#65, 16#6e, 46 | 16#74, 16#2d, 16#74, 16#79, 16#70, 16#65, 16#00, 16#00, 47 | 16#00, 16#04, 16#64, 16#61, 16#74, 16#65, 16#00, 16#00, 48 | 16#00, 16#04, 16#65, 16#74, 16#61, 16#67, 16#00, 16#00, 49 | 16#00, 16#06, 16#65, 16#78, 16#70, 16#65, 16#63, 16#74, 50 | 16#00, 16#00, 16#00, 16#07, 16#65, 16#78, 16#70, 16#69, 51 | 16#72, 16#65, 16#73, 16#00, 16#00, 16#00, 16#04, 16#66, 52 | 16#72, 16#6f, 16#6d, 16#00, 16#00, 16#00, 16#04, 16#68, 53 | 16#6f, 16#73, 16#74, 16#00, 16#00, 16#00, 16#08, 16#69, 54 | 16#66, 16#2d, 16#6d, 16#61, 16#74, 16#63, 16#68, 16#00, 55 | 16#00, 16#00, 16#11, 16#69, 16#66, 16#2d, 16#6d, 16#6f, 56 | 16#64, 16#69, 16#66, 16#69, 16#65, 16#64, 16#2d, 16#73, 57 | 16#69, 16#6e, 16#63, 16#65, 16#00, 16#00, 16#00, 16#0d, 58 | 16#69, 16#66, 16#2d, 16#6e, 16#6f, 16#6e, 16#65, 16#2d, 59 | 16#6d, 16#61, 16#74, 16#63, 16#68, 16#00, 16#00, 16#00, 60 | 16#08, 16#69, 16#66, 16#2d, 16#72, 16#61, 16#6e, 16#67, 61 | 16#65, 16#00, 16#00, 16#00, 16#13, 16#69, 16#66, 16#2d, 62 | 16#75, 16#6e, 16#6d, 16#6f, 16#64, 16#69, 16#66, 16#69, 63 | 16#65, 16#64, 16#2d, 16#73, 16#69, 16#6e, 16#63, 16#65, 64 | 16#00, 16#00, 16#00, 16#0d, 16#6c, 16#61, 16#73, 16#74, 65 | 16#2d, 16#6d, 16#6f, 16#64, 16#69, 16#66, 16#69, 16#65, 66 | 16#64, 16#00, 16#00, 16#00, 16#08, 16#6c, 16#6f, 16#63, 67 | 16#61, 16#74, 16#69, 16#6f, 16#6e, 16#00, 16#00, 16#00, 68 | 16#0c, 16#6d, 16#61, 16#78, 16#2d, 16#66, 16#6f, 16#72, 69 | 16#77, 16#61, 16#72, 16#64, 16#73, 16#00, 16#00, 16#00, 70 | 16#06, 16#70, 16#72, 16#61, 16#67, 16#6d, 16#61, 16#00, 71 | 16#00, 16#00, 16#12, 16#70, 16#72, 16#6f, 16#78, 16#79, 72 | 16#2d, 16#61, 16#75, 16#74, 16#68, 16#65, 16#6e, 16#74, 73 | 16#69, 16#63, 16#61, 16#74, 16#65, 16#00, 16#00, 16#00, 74 | 16#13, 16#70, 16#72, 16#6f, 16#78, 16#79, 16#2d, 16#61, 75 | 16#75, 16#74, 16#68, 16#6f, 16#72, 16#69, 16#7a, 16#61, 76 | 16#74, 16#69, 16#6f, 16#6e, 16#00, 16#00, 16#00, 16#05, 77 | 16#72, 16#61, 16#6e, 16#67, 16#65, 16#00, 16#00, 16#00, 78 | 16#07, 16#72, 16#65, 16#66, 16#65, 16#72, 16#65, 16#72, 79 | 16#00, 16#00, 16#00, 16#0b, 16#72, 16#65, 16#74, 16#72, 80 | 16#79, 16#2d, 16#61, 16#66, 16#74, 16#65, 16#72, 16#00, 81 | 16#00, 16#00, 16#06, 16#73, 16#65, 16#72, 16#76, 16#65, 82 | 16#72, 16#00, 16#00, 16#00, 16#02, 16#74, 16#65, 16#00, 83 | 16#00, 16#00, 16#07, 16#74, 16#72, 16#61, 16#69, 16#6c, 84 | 16#65, 16#72, 16#00, 16#00, 16#00, 16#11, 16#74, 16#72, 85 | 16#61, 16#6e, 16#73, 16#66, 16#65, 16#72, 16#2d, 16#65, 86 | 16#6e, 16#63, 16#6f, 16#64, 16#69, 16#6e, 16#67, 16#00, 87 | 16#00, 16#00, 16#07, 16#75, 16#70, 16#67, 16#72, 16#61, 88 | 16#64, 16#65, 16#00, 16#00, 16#00, 16#0a, 16#75, 16#73, 89 | 16#65, 16#72, 16#2d, 16#61, 16#67, 16#65, 16#6e, 16#74, 90 | 16#00, 16#00, 16#00, 16#04, 16#76, 16#61, 16#72, 16#79, 91 | 16#00, 16#00, 16#00, 16#03, 16#76, 16#69, 16#61, 16#00, 92 | 16#00, 16#00, 16#07, 16#77, 16#61, 16#72, 16#6e, 16#69, 93 | 16#6e, 16#67, 16#00, 16#00, 16#00, 16#10, 16#77, 16#77, 94 | 16#77, 16#2d, 16#61, 16#75, 16#74, 16#68, 16#65, 16#6e, 95 | 16#74, 16#69, 16#63, 16#61, 16#74, 16#65, 16#00, 16#00, 96 | 16#00, 16#06, 16#6d, 16#65, 16#74, 16#68, 16#6f, 16#64, 97 | 16#00, 16#00, 16#00, 16#03, 16#67, 16#65, 16#74, 16#00, 98 | 16#00, 16#00, 16#06, 16#73, 16#74, 16#61, 16#74, 16#75, 99 | 16#73, 16#00, 16#00, 16#00, 16#06, 16#32, 16#30, 16#30, 100 | 16#20, 16#4f, 16#4b, 16#00, 16#00, 16#00, 16#07, 16#76, 101 | 16#65, 16#72, 16#73, 16#69, 16#6f, 16#6e, 16#00, 16#00, 102 | 16#00, 16#08, 16#48, 16#54, 16#54, 16#50, 16#2f, 16#31, 103 | 16#2e, 16#31, 16#00, 16#00, 16#00, 16#03, 16#75, 16#72, 104 | 16#6c, 16#00, 16#00, 16#00, 16#06, 16#70, 16#75, 16#62, 105 | 16#6c, 16#69, 16#63, 16#00, 16#00, 16#00, 16#0a, 16#73, 106 | 16#65, 16#74, 16#2d, 16#63, 16#6f, 16#6f, 16#6b, 16#69, 107 | 16#65, 16#00, 16#00, 16#00, 16#0a, 16#6b, 16#65, 16#65, 108 | 16#70, 16#2d, 16#61, 16#6c, 16#69, 16#76, 16#65, 16#00, 109 | 16#00, 16#00, 16#06, 16#6f, 16#72, 16#69, 16#67, 16#69, 110 | 16#6e, 16#31, 16#30, 16#30, 16#31, 16#30, 16#31, 16#32, 111 | 16#30, 16#31, 16#32, 16#30, 16#32, 16#32, 16#30, 16#35, 112 | 16#32, 16#30, 16#36, 16#33, 16#30, 16#30, 16#33, 16#30, 113 | 16#32, 16#33, 16#30, 16#33, 16#33, 16#30, 16#34, 16#33, 114 | 16#30, 16#35, 16#33, 16#30, 16#36, 16#33, 16#30, 16#37, 115 | 16#34, 16#30, 16#32, 16#34, 16#30, 16#35, 16#34, 16#30, 116 | 16#36, 16#34, 16#30, 16#37, 16#34, 16#30, 16#38, 16#34, 117 | 16#30, 16#39, 16#34, 16#31, 16#30, 16#34, 16#31, 16#31, 118 | 16#34, 16#31, 16#32, 16#34, 16#31, 16#33, 16#34, 16#31, 119 | 16#34, 16#34, 16#31, 16#35, 16#34, 16#31, 16#36, 16#34, 120 | 16#31, 16#37, 16#35, 16#30, 16#32, 16#35, 16#30, 16#34, 121 | 16#35, 16#30, 16#35, 16#32, 16#30, 16#33, 16#20, 16#4e, 122 | 16#6f, 16#6e, 16#2d, 16#41, 16#75, 16#74, 16#68, 16#6f, 123 | 16#72, 16#69, 16#74, 16#61, 16#74, 16#69, 16#76, 16#65, 124 | 16#20, 16#49, 16#6e, 16#66, 16#6f, 16#72, 16#6d, 16#61, 125 | 16#74, 16#69, 16#6f, 16#6e, 16#32, 16#30, 16#34, 16#20, 126 | 16#4e, 16#6f, 16#20, 16#43, 16#6f, 16#6e, 16#74, 16#65, 127 | 16#6e, 16#74, 16#33, 16#30, 16#31, 16#20, 16#4d, 16#6f, 128 | 16#76, 16#65, 16#64, 16#20, 16#50, 16#65, 16#72, 16#6d, 129 | 16#61, 16#6e, 16#65, 16#6e, 16#74, 16#6c, 16#79, 16#34, 130 | 16#30, 16#30, 16#20, 16#42, 16#61, 16#64, 16#20, 16#52, 131 | 16#65, 16#71, 16#75, 16#65, 16#73, 16#74, 16#34, 16#30, 132 | 16#31, 16#20, 16#55, 16#6e, 16#61, 16#75, 16#74, 16#68, 133 | 16#6f, 16#72, 16#69, 16#7a, 16#65, 16#64, 16#34, 16#30, 134 | 16#33, 16#20, 16#46, 16#6f, 16#72, 16#62, 16#69, 16#64, 135 | 16#64, 16#65, 16#6e, 16#34, 16#30, 16#34, 16#20, 16#4e, 136 | 16#6f, 16#74, 16#20, 16#46, 16#6f, 16#75, 16#6e, 16#64, 137 | 16#35, 16#30, 16#30, 16#20, 16#49, 16#6e, 16#74, 16#65, 138 | 16#72, 16#6e, 16#61, 16#6c, 16#20, 16#53, 16#65, 16#72, 139 | 16#76, 16#65, 16#72, 16#20, 16#45, 16#72, 16#72, 16#6f, 140 | 16#72, 16#35, 16#30, 16#31, 16#20, 16#4e, 16#6f, 16#74, 141 | 16#20, 16#49, 16#6d, 16#70, 16#6c, 16#65, 16#6d, 16#65, 142 | 16#6e, 16#74, 16#65, 16#64, 16#35, 16#30, 16#33, 16#20, 143 | 16#53, 16#65, 16#72, 16#76, 16#69, 16#63, 16#65, 16#20, 144 | 16#55, 16#6e, 16#61, 16#76, 16#61, 16#69, 16#6c, 16#61, 145 | 16#62, 16#6c, 16#65, 16#4a, 16#61, 16#6e, 16#20, 16#46, 146 | 16#65, 16#62, 16#20, 16#4d, 16#61, 16#72, 16#20, 16#41, 147 | 16#70, 16#72, 16#20, 16#4d, 16#61, 16#79, 16#20, 16#4a, 148 | 16#75, 16#6e, 16#20, 16#4a, 16#75, 16#6c, 16#20, 16#41, 149 | 16#75, 16#67, 16#20, 16#53, 16#65, 16#70, 16#74, 16#20, 150 | 16#4f, 16#63, 16#74, 16#20, 16#4e, 16#6f, 16#76, 16#20, 151 | 16#44, 16#65, 16#63, 16#20, 16#30, 16#30, 16#3a, 16#30, 152 | 16#30, 16#3a, 16#30, 16#30, 16#20, 16#4d, 16#6f, 16#6e, 153 | 16#2c, 16#20, 16#54, 16#75, 16#65, 16#2c, 16#20, 16#57, 154 | 16#65, 16#64, 16#2c, 16#20, 16#54, 16#68, 16#75, 16#2c, 155 | 16#20, 16#46, 16#72, 16#69, 16#2c, 16#20, 16#53, 16#61, 156 | 16#74, 16#2c, 16#20, 16#53, 16#75, 16#6e, 16#2c, 16#20, 157 | 16#47, 16#4d, 16#54, 16#63, 16#68, 16#75, 16#6e, 16#6b, 158 | 16#65, 16#64, 16#2c, 16#74, 16#65, 16#78, 16#74, 16#2f, 159 | 16#68, 16#74, 16#6d, 16#6c, 16#2c, 16#69, 16#6d, 16#61, 160 | 16#67, 16#65, 16#2f, 16#70, 16#6e, 16#67, 16#2c, 16#69, 161 | 16#6d, 16#61, 16#67, 16#65, 16#2f, 16#6a, 16#70, 16#67, 162 | 16#2c, 16#69, 16#6d, 16#61, 16#67, 16#65, 16#2f, 16#67, 163 | 16#69, 16#66, 16#2c, 16#61, 16#70, 16#70, 16#6c, 16#69, 164 | 16#63, 16#61, 16#74, 16#69, 16#6f, 16#6e, 16#2f, 16#78, 165 | 16#6d, 16#6c, 16#2c, 16#61, 16#70, 16#70, 16#6c, 16#69, 166 | 16#63, 16#61, 16#74, 16#69, 16#6f, 16#6e, 16#2f, 16#78, 167 | 16#68, 16#74, 16#6d, 16#6c, 16#2b, 16#78, 16#6d, 16#6c, 168 | 16#2c, 16#74, 16#65, 16#78, 16#74, 16#2f, 16#70, 16#6c, 169 | 16#61, 16#69, 16#6e, 16#2c, 16#74, 16#65, 16#78, 16#74, 170 | 16#2f, 16#6a, 16#61, 16#76, 16#61, 16#73, 16#63, 16#72, 171 | 16#69, 16#70, 16#74, 16#2c, 16#70, 16#75, 16#62, 16#6c, 172 | 16#69, 16#63, 16#70, 16#72, 16#69, 16#76, 16#61, 16#74, 173 | 16#65, 16#6d, 16#61, 16#78, 16#2d, 16#61, 16#67, 16#65, 174 | 16#3d, 16#67, 16#7a, 16#69, 16#70, 16#2c, 16#64, 16#65, 175 | 16#66, 16#6c, 16#61, 16#74, 16#65, 16#2c, 16#73, 16#64, 176 | 16#63, 16#68, 16#63, 16#68, 16#61, 16#72, 16#73, 16#65, 177 | 16#74, 16#3d, 16#75, 16#74, 16#66, 16#2d, 16#38, 16#63, 178 | 16#68, 16#61, 16#72, 16#73, 16#65, 16#74, 16#3d, 16#69, 179 | 16#73, 16#6f, 16#2d, 16#38, 16#38, 16#35, 16#39, 16#2d, 180 | 16#31, 16#2c, 16#75, 16#74, 16#66, 16#2d, 16#2c, 16#2a, 181 | 16#2c, 16#65, 16#6e, 16#71, 16#3d, 16#30, 16#2e >>). 182 | -------------------------------------------------------------------------------- /src/cow_sse.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) Loïc Hoguin 2 | %% 3 | %% Permission to use, copy, modify, and/or distribute this software for any 4 | %% purpose with or without fee is hereby granted, provided that the above 5 | %% copyright notice and this permission notice appear in all copies. 6 | %% 7 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | -module(cow_sse). 16 | 17 | -export([init/0]). 18 | -export([parse/2]). 19 | -export([events/1]). 20 | -export([event/1]). 21 | 22 | -record(state, { 23 | state_name = bom :: bom | events, 24 | buffer = <<>> :: binary(), 25 | last_event_id = <<>> :: binary(), 26 | last_event_id_set = false :: boolean(), 27 | event_type = <<>> :: binary(), 28 | data = [] :: iolist(), 29 | retry = undefined :: undefined | non_neg_integer() 30 | }). 31 | -type state() :: #state{}. 32 | -export_type([state/0]). 33 | 34 | -type parsed_event() :: #{ 35 | last_event_id := binary(), 36 | event_type := binary(), 37 | data := iolist() 38 | }. 39 | 40 | -type event() :: #{ 41 | comment => iodata(), 42 | data => iodata(), 43 | event => iodata() | atom(), 44 | id => iodata(), 45 | retry => non_neg_integer() 46 | }. 47 | -export_type([event/0]). 48 | 49 | -spec init() -> state(). 50 | init() -> 51 | #state{}. 52 | 53 | %% @todo Add a function to retrieve the retry value from the state. 54 | 55 | -spec parse(binary(), State) 56 | -> {event, parsed_event(), State} | {more, State} 57 | when State::state(). 58 | parse(Data0, State=#state{state_name=bom, buffer=Buffer}) -> 59 | Data1 = case Buffer of 60 | <<>> -> Data0; 61 | _ -> << Buffer/binary, Data0/binary >> 62 | end, 63 | case Data1 of 64 | %% Skip the BOM. 65 | << 16#fe, 16#ff, Data/bits >> -> 66 | parse_event(Data, State#state{state_name=events, buffer= <<>>}); 67 | %% Not enough data to know wether we have a BOM. 68 | << 16#fe >> -> 69 | {more, State#state{buffer=Data1}}; 70 | <<>> -> 71 | {more, State}; 72 | %% No BOM. 73 | _ -> 74 | parse_event(Data1, State#state{state_name=events, buffer= <<>>}) 75 | end; 76 | %% Try to process data from the buffer if there is no new input. 77 | parse(<<>>, State=#state{buffer=Buffer}) -> 78 | parse_event(Buffer, State#state{buffer= <<>>}); 79 | %% Otherwise process the input data as-is. 80 | parse(Data0, State=#state{buffer=Buffer}) -> 81 | Data = case Buffer of 82 | <<>> -> Data0; 83 | _ -> << Buffer/binary, Data0/binary >> 84 | end, 85 | parse_event(Data, State). 86 | 87 | parse_event(Data, State0) -> 88 | case binary:split(Data, [<<"\r\n">>, <<"\r">>, <<"\n">>]) of 89 | [Line, Rest] -> 90 | case parse_line(Line, State0) of 91 | {ok, State} -> 92 | parse_event(Rest, State); 93 | {event, Event, State} -> 94 | {event, Event, State#state{buffer=Rest}} 95 | end; 96 | [_] -> 97 | {more, State0#state{buffer=Data}} 98 | end. 99 | 100 | %% Dispatch events on empty line. 101 | parse_line(<<>>, State) -> 102 | dispatch_event(State); 103 | %% Ignore comments. 104 | parse_line(<< $:, _/bits >>, State) -> 105 | {ok, State}; 106 | %% Normal line. 107 | parse_line(Line, State) -> 108 | case binary:split(Line, [<<":\s">>, <<":">>]) of 109 | [Field, Value] -> 110 | process_field(Field, Value, State); 111 | [Field] -> 112 | process_field(Field, <<>>, State) 113 | end. 114 | 115 | process_field(<<"event">>, Value, State) -> 116 | {ok, State#state{event_type=Value}}; 117 | process_field(<<"data">>, Value, State=#state{data=Data}) -> 118 | {ok, State#state{data=[<<$\n>>, Value|Data]}}; 119 | process_field(<<"id">>, Value, State) -> 120 | {ok, State#state{last_event_id=Value, last_event_id_set=true}}; 121 | process_field(<<"retry">>, Value, State) -> 122 | try 123 | {ok, State#state{retry=binary_to_integer(Value)}} 124 | catch _:_ -> 125 | {ok, State} 126 | end; 127 | process_field(_, _, State) -> 128 | {ok, State}. 129 | 130 | %% Data is an empty string; abort. 131 | dispatch_event(State=#state{last_event_id_set=false, data=[]}) -> 132 | {ok, State#state{event_type= <<>>}}; 133 | %% Data is an empty string but we have a last_event_id: 134 | %% propagate it on its own so that the caller knows the 135 | %% most recent ID. 136 | dispatch_event(State=#state{last_event_id=LastEventID, data=[]}) -> 137 | {event, #{ 138 | last_event_id => LastEventID 139 | }, State#state{last_event_id_set=false, event_type= <<>>}}; 140 | %% Dispatch the event. 141 | %% 142 | %% Always remove the last linebreak from the data. 143 | dispatch_event(State=#state{last_event_id=LastEventID, 144 | event_type=EventType, data=[_|Data]}) -> 145 | {event, #{ 146 | last_event_id => LastEventID, 147 | event_type => case EventType of 148 | <<>> -> <<"message">>; 149 | _ -> EventType 150 | end, 151 | data => lists:reverse(Data) 152 | }, State#state{last_event_id_set=false, event_type= <<>>, data=[]}}. 153 | 154 | -ifdef(TEST). 155 | parse_example1_test() -> 156 | {event, #{ 157 | event_type := <<"message">>, 158 | last_event_id := <<>>, 159 | data := Data 160 | }, State} = parse(<< 161 | "data: YHOO\n" 162 | "data: +2\n" 163 | "data: 10\n" 164 | "\n">>, init()), 165 | <<"YHOO\n+2\n10">> = iolist_to_binary(Data), 166 | {more, _} = parse(<<>>, State), 167 | ok. 168 | 169 | parse_example2_test() -> 170 | {event, #{ 171 | event_type := <<"message">>, 172 | last_event_id := <<"1">>, 173 | data := Data1 174 | }, State0} = parse(<< 175 | ": test stream\n" 176 | "\n" 177 | "data: first event\n" 178 | "id: 1\n" 179 | "\n" 180 | "data:second event\n" 181 | "id\n" 182 | "\n" 183 | "data: third event\n" 184 | "\n">>, init()), 185 | <<"first event">> = iolist_to_binary(Data1), 186 | {event, #{ 187 | event_type := <<"message">>, 188 | last_event_id := <<>>, 189 | data := Data2 190 | }, State1} = parse(<<>>, State0), 191 | <<"second event">> = iolist_to_binary(Data2), 192 | {event, #{ 193 | event_type := <<"message">>, 194 | last_event_id := <<>>, 195 | data := Data3 196 | }, State} = parse(<<>>, State1), 197 | <<" third event">> = iolist_to_binary(Data3), 198 | {more, _} = parse(<<>>, State), 199 | ok. 200 | 201 | parse_example3_test() -> 202 | {event, #{ 203 | event_type := <<"message">>, 204 | last_event_id := <<>>, 205 | data := Data1 206 | }, State0} = parse(<< 207 | "data\n" 208 | "\n" 209 | "data\n" 210 | "data\n" 211 | "\n" 212 | "data:\n">>, init()), 213 | <<>> = iolist_to_binary(Data1), 214 | {event, #{ 215 | event_type := <<"message">>, 216 | last_event_id := <<>>, 217 | data := Data2 218 | }, State} = parse(<<>>, State0), 219 | <<"\n">> = iolist_to_binary(Data2), 220 | {more, _} = parse(<<>>, State), 221 | ok. 222 | 223 | parse_example4_test() -> 224 | {event, Event, State0} = parse(<< 225 | "data:test\n" 226 | "\n" 227 | "data: test\n" 228 | "\n">>, init()), 229 | {event, Event, State} = parse(<<>>, State0), 230 | {more, _} = parse(<<>>, State), 231 | ok. 232 | 233 | parse_id_without_data_test() -> 234 | {event, Event1, State0} = parse(<< 235 | "id: 1\n" 236 | "\n" 237 | "data: data\n" 238 | "\n" 239 | "id: 2\n" 240 | "\n">>, init()), 241 | 1 = maps:size(Event1), 242 | #{last_event_id := <<"1">>} = Event1, 243 | {event, #{ 244 | event_type := <<"message">>, 245 | last_event_id := <<"1">>, 246 | data := Data 247 | }, State1} = parse(<<>>, State0), 248 | <<"data">> = iolist_to_binary(Data), 249 | {event, Event2, State} = parse(<<>>, State1), 250 | 1 = maps:size(Event2), 251 | #{last_event_id := <<"2">>} = Event2, 252 | {more, _} = parse(<<>>, State), 253 | ok. 254 | 255 | parse_repeated_id_without_data_test() -> 256 | {event, Event1, State0} = parse(<< 257 | "id: 1\n" 258 | "\n" 259 | "event: message\n" %% This will be ignored since there's no data. 260 | "\n" 261 | "id: 1\n" 262 | "\n" 263 | "id: 2\n" 264 | "\n">>, init()), 265 | {event, Event1, State1} = parse(<<>>, State0), 266 | 1 = maps:size(Event1), 267 | #{last_event_id := <<"1">>} = Event1, 268 | {event, Event2, State} = parse(<<>>, State1), 269 | 1 = maps:size(Event2), 270 | #{last_event_id := <<"2">>} = Event2, 271 | {more, _} = parse(<<>>, State), 272 | ok. 273 | 274 | parse_split_event_test() -> 275 | {more, State} = parse(<< 276 | "data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 277 | "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 278 | "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA">>, init()), 279 | {event, _, _} = parse(<<"==\n\n">>, State), 280 | ok. 281 | -endif. 282 | 283 | -spec events([event()]) -> iolist(). 284 | events(Events) -> 285 | [event(Event) || Event <- Events]. 286 | 287 | -spec event(event()) -> iolist(). 288 | event(Event) -> 289 | [ 290 | event_comment(Event), 291 | event_id(Event), 292 | event_name(Event), 293 | event_data(Event), 294 | event_retry(Event), 295 | $\n 296 | ]. 297 | 298 | event_comment(#{comment := Comment}) -> 299 | prefix_lines(Comment, <<>>); 300 | event_comment(_) -> 301 | []. 302 | 303 | event_id(#{id := ID}) -> 304 | nomatch = binary:match(iolist_to_binary(ID), <<"\n">>), 305 | [<<"id: ">>, ID, $\n]; 306 | event_id(_) -> 307 | []. 308 | 309 | event_name(#{event := Name0}) -> 310 | Name = if 311 | is_atom(Name0) -> atom_to_binary(Name0, utf8); 312 | true -> iolist_to_binary(Name0) 313 | end, 314 | nomatch = binary:match(Name, <<"\n">>), 315 | [<<"event: ">>, Name, $\n]; 316 | event_name(_) -> 317 | []. 318 | 319 | event_data(#{data := Data}) -> 320 | prefix_lines(Data, <<"data">>); 321 | event_data(_) -> 322 | []. 323 | 324 | event_retry(#{retry := Retry}) -> 325 | [<<"retry: ">>, integer_to_binary(Retry), $\n]; 326 | event_retry(_) -> 327 | []. 328 | 329 | prefix_lines(IoData, Prefix) -> 330 | Lines = binary:split(iolist_to_binary(IoData), <<"\n">>, [global]), 331 | [[Prefix, <<": ">>, Line, $\n] || Line <- Lines]. 332 | 333 | -ifdef(TEST). 334 | event_test() -> 335 | _ = event(#{}), 336 | _ = event(#{comment => "test"}), 337 | _ = event(#{data => "test"}), 338 | _ = event(#{data => "test\ntest\ntest"}), 339 | _ = event(#{data => "test\ntest\ntest\n"}), 340 | _ = event(#{data => <<"test\ntest\ntest">>}), 341 | _ = event(#{data => [<<"test">>, $\n, <<"test">>, [$\n, "test"]]}), 342 | _ = event(#{event => test}), 343 | _ = event(#{event => "test"}), 344 | _ = event(#{id => "test"}), 345 | _ = event(#{retry => 5000}), 346 | _ = event(#{event => "test", data => "test"}), 347 | _ = event(#{id => "test", event => "test", data => "test"}), 348 | ok. 349 | -endif. 350 | -------------------------------------------------------------------------------- /src/cow_uri.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) Loïc Hoguin 2 | %% 3 | %% Permission to use, copy, modify, and/or distribute this software for any 4 | %% purpose with or without fee is hereby granted, provided that the above 5 | %% copyright notice and this permission notice appear in all copies. 6 | %% 7 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | -module(cow_uri). 16 | -dialyzer(no_improper_lists). 17 | 18 | -export([urldecode/1]). 19 | -export([urlencode/1]). 20 | 21 | -include("cow_inline.hrl"). 22 | 23 | -define(IS_PLAIN(C), ( 24 | (C =:= $!) orelse (C =:= $$) orelse (C =:= $&) orelse (C =:= $') orelse 25 | (C =:= $() orelse (C =:= $)) orelse (C =:= $*) orelse (C =:= $+) orelse 26 | (C =:= $,) orelse (C =:= $-) orelse (C =:= $.) orelse (C =:= $0) orelse 27 | (C =:= $1) orelse (C =:= $2) orelse (C =:= $3) orelse (C =:= $4) orelse 28 | (C =:= $5) orelse (C =:= $6) orelse (C =:= $7) orelse (C =:= $8) orelse 29 | (C =:= $9) orelse (C =:= $:) orelse (C =:= $;) orelse (C =:= $=) orelse 30 | (C =:= $@) orelse (C =:= $A) orelse (C =:= $B) orelse (C =:= $C) orelse 31 | (C =:= $D) orelse (C =:= $E) orelse (C =:= $F) orelse (C =:= $G) orelse 32 | (C =:= $H) orelse (C =:= $I) orelse (C =:= $J) orelse (C =:= $K) orelse 33 | (C =:= $L) orelse (C =:= $M) orelse (C =:= $N) orelse (C =:= $O) orelse 34 | (C =:= $P) orelse (C =:= $Q) orelse (C =:= $R) orelse (C =:= $S) orelse 35 | (C =:= $T) orelse (C =:= $U) orelse (C =:= $V) orelse (C =:= $W) orelse 36 | (C =:= $X) orelse (C =:= $Y) orelse (C =:= $Z) orelse (C =:= $_) orelse 37 | (C =:= $a) orelse (C =:= $b) orelse (C =:= $c) orelse (C =:= $d) orelse 38 | (C =:= $e) orelse (C =:= $f) orelse (C =:= $g) orelse (C =:= $h) orelse 39 | (C =:= $i) orelse (C =:= $j) orelse (C =:= $k) orelse (C =:= $l) orelse 40 | (C =:= $m) orelse (C =:= $n) orelse (C =:= $o) orelse (C =:= $p) orelse 41 | (C =:= $q) orelse (C =:= $r) orelse (C =:= $s) orelse (C =:= $t) orelse 42 | (C =:= $u) orelse (C =:= $v) orelse (C =:= $w) orelse (C =:= $x) orelse 43 | (C =:= $y) orelse (C =:= $z) orelse (C =:= $~) 44 | )). 45 | 46 | %% Decode a percent encoded string. (RFC3986 2.1) 47 | %% 48 | %% Inspiration for some of the optimisations done here come 49 | %% from the new `json` module as it was in mid-2024. 50 | %% 51 | %% Possible input includes: 52 | %% 53 | %% * nothing encoded (no % character): 54 | %% We want to return the binary as-is to avoid an allocation. 55 | %% 56 | %% * small number of encoded characters: 57 | %% We can "skip" words of text. 58 | %% 59 | %% * mostly encoded characters (non-ascii languages) 60 | %% We can decode characters in bulk. 61 | 62 | -spec urldecode(binary()) -> binary(). 63 | 64 | urldecode(Binary) -> 65 | skip_dec(Binary, Binary, 0). 66 | 67 | %% This functions helps avoid a binary allocation when 68 | %% there is nothing to decode. 69 | skip_dec(Binary, Orig, Len) -> 70 | case Binary of 71 | <> 72 | when ?IS_PLAIN(C1) andalso ?IS_PLAIN(C2) 73 | andalso ?IS_PLAIN(C3) andalso ?IS_PLAIN(C4) -> 74 | skip_dec(Rest, Orig, Len + 4); 75 | _ -> 76 | dec(Binary, [], Orig, 0, Len) 77 | end. 78 | 79 | %% This clause helps speed up decoding of highly encoded values. 80 | dec(<<$%, H1, L1, $%, H2, L2, $%, H3, L3, $%, H4, L4, Rest/bits>>, Acc, Orig, Skip, Len) -> 81 | C1 = ?UNHEX(H1, L1), 82 | C2 = ?UNHEX(H2, L2), 83 | C3 = ?UNHEX(H3, L3), 84 | C4 = ?UNHEX(H4, L4), 85 | case Len of 86 | 0 -> 87 | dec(Rest, [Acc|<>], Orig, Skip + 12, 0); 88 | _ -> 89 | Part = binary_part(Orig, Skip, Len), 90 | dec(Rest, [Acc, Part|<>], Orig, Skip + Len + 12, 0) 91 | end; 92 | dec(<<$%, H, L, Rest/bits>>, Acc, Orig, Skip, Len) -> 93 | C = ?UNHEX(H, L), 94 | case Len of 95 | 0 -> 96 | dec(Rest, [Acc|<>], Orig, Skip + 3, 0); 97 | _ -> 98 | Part = binary_part(Orig, Skip, Len), 99 | dec(Rest, [Acc, Part|<>], Orig, Skip + Len + 3, 0) 100 | end; 101 | %% This clause helps speed up decoding of barely encoded values. 102 | dec(<>, Acc, Orig, Skip, Len) 103 | when ?IS_PLAIN(C1) andalso ?IS_PLAIN(C2) 104 | andalso ?IS_PLAIN(C3) andalso ?IS_PLAIN(C4) -> 105 | dec(Rest, Acc, Orig, Skip, Len + 4); 106 | dec(<>, Acc, Orig, Skip, Len) 107 | when ?IS_PLAIN(C) -> 108 | dec(Rest, Acc, Orig, Skip, Len + 1); 109 | dec(<<>>, _, Orig, 0, _) -> 110 | Orig; 111 | dec(<<>>, Acc, _, _, 0) -> 112 | iolist_to_binary(Acc); 113 | dec(<<>>, Acc, Orig, Skip, Len) -> 114 | Part = binary_part(Orig, Skip, Len), 115 | iolist_to_binary([Acc|Part]); 116 | dec(_, _, Orig, Skip, Len) -> 117 | error({invalid_byte, binary:at(Orig, Skip + Len)}). 118 | 119 | -ifdef(TEST). 120 | urldecode_test_() -> 121 | Tests = [ 122 | {<<"%20">>, <<" ">>}, 123 | {<<"+">>, <<"+">>}, 124 | {<<"%00">>, <<0>>}, 125 | {<<"%fF">>, <<255>>}, 126 | {<<"123">>, <<"123">>}, 127 | {<<"%i5">>, error}, 128 | {<<"%5">>, error} 129 | ], 130 | [{Qs, fun() -> 131 | E = try urldecode(Qs) of 132 | R -> R 133 | catch _:_ -> 134 | error 135 | end 136 | end} || {Qs, E} <- Tests]. 137 | 138 | urldecode_identity_test_() -> 139 | Tests = [ 140 | <<"%20">>, 141 | <<"+">>, 142 | <<"nothingnothingnothingnothing">>, 143 | <<"Small+fast+modular+HTTP+server">>, 144 | <<"Small%20fast%20modular%20HTTP%20server">>, 145 | <<"Small%2F+fast%2F+modular+HTTP+server.">>, 146 | <<"%E3%83%84%E3%82%A4%E3%83%B3%E3%82%BD%E3%82%A6%E3%83" 147 | "%AB%E3%80%9C%E8%BC%AA%E5%BB%BB%E3%81%99%E3%82%8B%E6%97%8B%E5" 148 | "%BE%8B%E3%80%9C">> 149 | ], 150 | [{V, fun() -> V = urlencode(urldecode(V)) end} || V <- Tests]. 151 | 152 | horse_urldecode() -> 153 | horse:repeat(100000, 154 | urldecode(<<"nothingnothingnothingnothing">>) 155 | ). 156 | 157 | horse_urldecode_hex() -> 158 | horse:repeat(100000, 159 | urldecode(<<"Small%2C%20fast%2C%20modular%20HTTP%20server.">>) 160 | ). 161 | 162 | horse_urldecode_jp_hex() -> 163 | horse:repeat(100000, 164 | urldecode(<<"%E3%83%84%E3%82%A4%E3%83%B3%E3%82%BD%E3%82%A6%E3%83" 165 | "%AB%E3%80%9C%E8%BC%AA%E5%BB%BB%E3%81%99%E3%82%8B%E6%97%8B%E5" 166 | "%BE%8B%E3%80%9C">>) 167 | ). 168 | 169 | horse_urldecode_jp_mixed_hex() -> 170 | horse:repeat(100000, 171 | urldecode(<<"%E3%83%84%E3%82%A4%E3%83%B3%E3%82%BD%E3123%82%A6%E3%83" 172 | "%AB%E3%80%9C%E8%BC%AA%E5%BB%BB%E3%81%99%E3%82%8B%E6%97%8B%E5" 173 | "%BE%8B%E3%80%9C">>) 174 | ). 175 | 176 | horse_urldecode_worst_case_hex() -> 177 | horse:repeat(100000, 178 | urldecode(<<"%e3%83123%84%e3123%82%a4123%e3%83123%b3%e3123%82%bd123">>) 179 | ). 180 | -endif. 181 | 182 | %% Percent encode a string. (RFC3986 2.1) 183 | %% 184 | %% This function is meant to be used for path components. 185 | 186 | -spec urlencode(binary()) -> binary(). 187 | 188 | urlencode(Binary) -> 189 | skip_enc(Binary, Binary, 0). 190 | 191 | skip_enc(Binary, Orig, Len) -> 192 | case Binary of 193 | <> 194 | when ?IS_PLAIN(C1) andalso ?IS_PLAIN(C2) 195 | andalso ?IS_PLAIN(C3) andalso ?IS_PLAIN(C4) -> 196 | skip_enc(Rest, Orig, Len + 4); 197 | _ -> 198 | enc(Binary, [], Orig, 0, Len) 199 | end. 200 | 201 | enc(<>, Acc, Orig, Skip, Len) 202 | when ?IS_PLAIN(C1) andalso ?IS_PLAIN(C2) 203 | andalso ?IS_PLAIN(C3) andalso ?IS_PLAIN(C4) -> 204 | enc(Rest, Acc, Orig, Skip, Len + 4); 205 | enc(<>, Acc, Orig, Skip, Len) 206 | when ?IS_PLAIN(C) -> 207 | enc(Rest, Acc, Orig, Skip, Len + 1); 208 | enc(<>, Acc, Orig, Skip, Len) 209 | when (not ?IS_PLAIN(C2)) andalso (not ?IS_PLAIN(C3)) 210 | andalso (not ?IS_PLAIN(C4)) -> 211 | Enc = <<$%, ?HEX(C1), $%, ?HEX(C2), $%, ?HEX(C3), $%, ?HEX(C4)>>, 212 | case Len of 213 | 0 -> 214 | enc(Rest, [Acc|Enc], Orig, Skip + 4, 0); 215 | _ -> 216 | Part = binary_part(Orig, Skip, Len), 217 | enc(Rest, [Acc, Part|Enc], Orig, Skip + Len + 4, 0) 218 | end; 219 | enc(<>, Acc, Orig, Skip, Len) -> 220 | Enc = <<$%, ?HEX(C)>>, 221 | case Len of 222 | 0 -> 223 | enc(Rest, [Acc|Enc], Orig, Skip + 1, 0); 224 | _ -> 225 | Part = binary_part(Orig, Skip, Len), 226 | enc(Rest, [Acc, Part|Enc], Orig, Skip + Len + 1, 0) 227 | end; 228 | enc(<<>>, _, Orig, 0, _) -> 229 | Orig; 230 | enc(<<>>, Acc, _, _, 0) -> 231 | iolist_to_binary(Acc); 232 | enc(<<>>, Acc, Orig, Skip, Len) -> 233 | Part = binary_part(Orig, Skip, Len), 234 | iolist_to_binary([Acc|Part]); 235 | enc(_, _, Orig, Skip, Len) -> 236 | error({invalid_byte, binary:at(Orig, Skip + Len)}). 237 | 238 | -ifdef(TEST). 239 | urlencode_test_() -> 240 | Tests = [ 241 | {<<255, 0>>, <<"%FF%00">>}, 242 | {<<255, " ">>, <<"%FF%20">>}, 243 | {<<"+">>, <<"+">>}, 244 | {<<"aBc123">>, <<"aBc123">>}, 245 | {<<"!$&'()*+,:;=@-._~">>, <<"!$&'()*+,:;=@-._~">>} 246 | ], 247 | [{V, fun() -> E = urlencode(V) end} || {V, E} <- Tests]. 248 | 249 | urlencode_identity_test_() -> 250 | Tests = [ 251 | <<"+">>, 252 | <<"nothingnothingnothingnothing">>, 253 | <<"Small fast modular HTTP server">>, 254 | <<"Small, fast, modular HTTP server.">>, 255 | <<227,131,132,227,130,164,227,131,179,227,130,189,227, 256 | 130,166,227,131,171,227,128,156,232,188,170,229,187,187,227, 257 | 129,153,227,130,139,230,151,139,229,190,139,227,128,156>> 258 | ], 259 | [{V, fun() -> V = urldecode(urlencode(V)) end} || V <- Tests]. 260 | 261 | horse_urlencode() -> 262 | horse:repeat(100000, 263 | urlencode(<<"nothingnothingnothingnothing">>) 264 | ). 265 | 266 | horse_urlencode_spaces() -> 267 | horse:repeat(100000, 268 | urlencode(<<"Small fast modular HTTP server">>) 269 | ). 270 | 271 | horse_urlencode_jp() -> 272 | horse:repeat(100000, 273 | urlencode(<<227,131,132,227,130,164,227,131,179,227,130,189,227, 274 | 130,166,227,131,171,227,128,156,232,188,170,229,187,187,227, 275 | 129,153,227,130,139,230,151,139,229,190,139,227,128,156>>) 276 | ). 277 | 278 | horse_urlencode_jp_mixed() -> 279 | horse:repeat(100000, 280 | urlencode(<<227,131,132,227,130,164,227,131,179,227,130,189,227, 281 | $1, $2, $3, 282 | 130,166,227,131,171,227,128,156,232,188,170,229,187,187,227, 283 | 129,153,227,130,139,230,151,139,229,190,139,227,128,156>>) 284 | ). 285 | 286 | horse_urlencode_mix() -> 287 | horse:repeat(100000, 288 | urlencode(<<"Small, fast, modular HTTP server.">>) 289 | ). 290 | -endif. 291 | -------------------------------------------------------------------------------- /src/cow_uri_template.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) Loïc Hoguin 2 | %% 3 | %% Permission to use, copy, modify, and/or distribute this software for any 4 | %% purpose with or without fee is hereby granted, provided that the above 5 | %% copyright notice and this permission notice appear in all copies. 6 | %% 7 | %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | %% This is a full level 4 implementation of URI Templates 16 | %% as defined by RFC6570. 17 | 18 | -module(cow_uri_template). 19 | 20 | -export([parse/1]). 21 | -export([expand/2]). 22 | 23 | -type op() :: simple_string_expansion 24 | | reserved_expansion 25 | | fragment_expansion 26 | | label_expansion_with_dot_prefix 27 | | path_segment_expansion 28 | | path_style_parameter_expansion 29 | | form_style_query_expansion 30 | | form_style_query_continuation. 31 | 32 | -type var_list() :: [ 33 | {no_modifier, binary()} 34 | | {{prefix_modifier, pos_integer()}, binary()} 35 | | {explode_modifier, binary()} 36 | ]. 37 | 38 | -type uri_template() :: [ 39 | binary() | {expr, op(), var_list()} 40 | ]. 41 | -export_type([uri_template/0]). 42 | 43 | -type variables() :: #{ 44 | binary() => binary() 45 | | integer() 46 | | float() 47 | | [binary()] 48 | | #{binary() => binary()} 49 | }. 50 | 51 | -include("cow_inline.hrl"). 52 | -include("cow_parse.hrl"). 53 | 54 | %% Parse a URI template. 55 | 56 | -spec parse(binary()) -> uri_template(). 57 | parse(URITemplate) -> 58 | parse(URITemplate, <<>>). 59 | 60 | parse(<<>>, <<>>) -> 61 | []; 62 | parse(<<>>, Acc) -> 63 | [Acc]; 64 | parse(<<${,R/bits>>, <<>>) -> 65 | parse_expr(R); 66 | parse(<<${,R/bits>>, Acc) -> 67 | [Acc|parse_expr(R)]; 68 | %% @todo Probably should reject unallowed characters so that 69 | %% we don't produce invalid URIs. 70 | parse(<>, Acc) when C =/= $} -> 71 | parse(R, <>). 72 | 73 | parse_expr(<<$+,R/bits>>) -> 74 | parse_var_list(R, reserved_expansion, []); 75 | parse_expr(<<$#,R/bits>>) -> 76 | parse_var_list(R, fragment_expansion, []); 77 | parse_expr(<<$.,R/bits>>) -> 78 | parse_var_list(R, label_expansion_with_dot_prefix, []); 79 | parse_expr(<<$/,R/bits>>) -> 80 | parse_var_list(R, path_segment_expansion, []); 81 | parse_expr(<<$;,R/bits>>) -> 82 | parse_var_list(R, path_style_parameter_expansion, []); 83 | parse_expr(<<$?,R/bits>>) -> 84 | parse_var_list(R, form_style_query_expansion, []); 85 | parse_expr(<<$&,R/bits>>) -> 86 | parse_var_list(R, form_style_query_continuation, []); 87 | parse_expr(R) -> 88 | parse_var_list(R, simple_string_expansion, []). 89 | 90 | parse_var_list(<>, Op, List) 91 | when ?IS_ALPHANUM(C) or (C =:= $_) -> 92 | parse_varname(R, Op, List, <>). 93 | 94 | parse_varname(<>, Op, List, Name) 95 | when ?IS_ALPHANUM(C) or (C =:= $_) or (C =:= $.) or (C =:= $%) -> 96 | parse_varname(R, Op, List, <>); 97 | parse_varname(<<$:,C,R/bits>>, Op, List, Name) 98 | when (C =:= $1) or (C =:= $2) or (C =:= $3) or (C =:= $4) or (C =:= $5) 99 | or (C =:= $6) or (C =:= $7) or (C =:= $8) or (C =:= $9) -> 100 | parse_prefix_modifier(R, Op, List, Name, <>); 101 | parse_varname(<<$*,$,,R/bits>>, Op, List, Name) -> 102 | parse_var_list(R, Op, [{explode_modifier, Name}|List]); 103 | parse_varname(<<$*,$},R/bits>>, Op, List, Name) -> 104 | [{expr, Op, lists:reverse([{explode_modifier, Name}|List])}|parse(R, <<>>)]; 105 | parse_varname(<<$,,R/bits>>, Op, List, Name) -> 106 | parse_var_list(R, Op, [{no_modifier, Name}|List]); 107 | parse_varname(<<$},R/bits>>, Op, List, Name) -> 108 | [{expr, Op, lists:reverse([{no_modifier, Name}|List])}|parse(R, <<>>)]. 109 | 110 | parse_prefix_modifier(<>, Op, List, Name, Acc) 111 | when ?IS_DIGIT(C), byte_size(Acc) < 4 -> 112 | parse_prefix_modifier(R, Op, List, Name, <>); 113 | parse_prefix_modifier(<<$,,R/bits>>, Op, List, Name, Acc) -> 114 | parse_var_list(R, Op, [{{prefix_modifier, binary_to_integer(Acc)}, Name}|List]); 115 | parse_prefix_modifier(<<$},R/bits>>, Op, List, Name, Acc) -> 116 | [{expr, Op, lists:reverse([{{prefix_modifier, binary_to_integer(Acc)}, Name}|List])}|parse(R, <<>>)]. 117 | 118 | %% Expand a URI template (after parsing it if necessary). 119 | 120 | -spec expand(binary() | uri_template(), variables()) -> iodata(). 121 | expand(URITemplate, Vars) when is_binary(URITemplate) -> 122 | expand(parse(URITemplate), Vars); 123 | expand(URITemplate, Vars) -> 124 | expand1(URITemplate, Vars). 125 | 126 | expand1([], _) -> 127 | []; 128 | expand1([Literal|Tail], Vars) when is_binary(Literal) -> 129 | [Literal|expand1(Tail, Vars)]; 130 | expand1([{expr, simple_string_expansion, VarList}|Tail], Vars) -> 131 | [simple_string_expansion(VarList, Vars)|expand1(Tail, Vars)]; 132 | expand1([{expr, reserved_expansion, VarList}|Tail], Vars) -> 133 | [reserved_expansion(VarList, Vars)|expand1(Tail, Vars)]; 134 | expand1([{expr, fragment_expansion, VarList}|Tail], Vars) -> 135 | [fragment_expansion(VarList, Vars)|expand1(Tail, Vars)]; 136 | expand1([{expr, label_expansion_with_dot_prefix, VarList}|Tail], Vars) -> 137 | [label_expansion_with_dot_prefix(VarList, Vars)|expand1(Tail, Vars)]; 138 | expand1([{expr, path_segment_expansion, VarList}|Tail], Vars) -> 139 | [path_segment_expansion(VarList, Vars)|expand1(Tail, Vars)]; 140 | expand1([{expr, path_style_parameter_expansion, VarList}|Tail], Vars) -> 141 | [path_style_parameter_expansion(VarList, Vars)|expand1(Tail, Vars)]; 142 | expand1([{expr, form_style_query_expansion, VarList}|Tail], Vars) -> 143 | [form_style_query_expansion(VarList, Vars)|expand1(Tail, Vars)]; 144 | expand1([{expr, form_style_query_continuation, VarList}|Tail], Vars) -> 145 | [form_style_query_continuation(VarList, Vars)|expand1(Tail, Vars)]. 146 | 147 | simple_string_expansion(VarList, Vars) -> 148 | lists:join($,, [ 149 | apply_modifier(Modifier, unreserved, $,, Value) 150 | || {Modifier, _Name, Value} <- lookup_variables(VarList, Vars)]). 151 | 152 | reserved_expansion(VarList, Vars) -> 153 | lists:join($,, [ 154 | apply_modifier(Modifier, reserved, $,, Value) 155 | || {Modifier, _Name, Value} <- lookup_variables(VarList, Vars)]). 156 | 157 | fragment_expansion(VarList, Vars) -> 158 | case reserved_expansion(VarList, Vars) of 159 | [] -> []; 160 | Expanded -> [$#, Expanded] 161 | end. 162 | 163 | label_expansion_with_dot_prefix(VarList, Vars) -> 164 | segment_expansion(VarList, Vars, $.). 165 | 166 | path_segment_expansion(VarList, Vars) -> 167 | segment_expansion(VarList, Vars, $/). 168 | 169 | segment_expansion(VarList, Vars, Sep) -> 170 | Expanded = lists:join(Sep, [ 171 | apply_modifier(Modifier, unreserved, Sep, Value) 172 | || {Modifier, _Name, Value} <- lookup_variables(VarList, Vars)]), 173 | case Expanded of 174 | [] -> []; 175 | [[]] -> []; 176 | _ -> [Sep, Expanded] 177 | end. 178 | 179 | path_style_parameter_expansion(VarList, Vars) -> 180 | parameter_expansion(VarList, Vars, $;, $;, trim). 181 | 182 | form_style_query_expansion(VarList, Vars) -> 183 | parameter_expansion(VarList, Vars, $?, $&, no_trim). 184 | 185 | form_style_query_continuation(VarList, Vars) -> 186 | parameter_expansion(VarList, Vars, $&, $&, no_trim). 187 | 188 | parameter_expansion(VarList, Vars, LeadingSep, Sep, Trim) -> 189 | Expanded = lists:join(Sep, [ 190 | apply_parameter_modifier(Modifier, unreserved, Sep, Trim, Name, Value) 191 | || {Modifier, Name, Value} <- lookup_variables(VarList, Vars)]), 192 | case Expanded of 193 | [] -> []; 194 | [[]] -> []; 195 | _ -> [LeadingSep, Expanded] 196 | end. 197 | 198 | lookup_variables([], _) -> 199 | []; 200 | lookup_variables([{Modifier, Name}|Tail], Vars) -> 201 | case Vars of 202 | #{Name := Value} -> [{Modifier, Name, Value}|lookup_variables(Tail, Vars)]; 203 | _ -> lookup_variables(Tail, Vars) 204 | end. 205 | 206 | apply_modifier(no_modifier, AllowedChars, _, List) when is_list(List) -> 207 | lists:join($,, [urlencode(Value, AllowedChars) || Value <- List]); 208 | apply_modifier(explode_modifier, AllowedChars, ExplodeSep, List) when is_list(List) -> 209 | lists:join(ExplodeSep, [urlencode(Value, AllowedChars) || Value <- List]); 210 | apply_modifier(Modifier, AllowedChars, ExplodeSep, Map) when is_map(Map) -> 211 | {JoinSep, KVSep} = case Modifier of 212 | no_modifier -> {$,, $,}; 213 | explode_modifier -> {ExplodeSep, $=} 214 | end, 215 | lists:reverse(lists:join(JoinSep, 216 | maps:fold(fun(Key, Value, Acc) -> 217 | [[ 218 | urlencode(Key, AllowedChars), 219 | KVSep, 220 | urlencode(Value, AllowedChars) 221 | ]|Acc] 222 | end, [], Map) 223 | )); 224 | apply_modifier({prefix_modifier, MaxLen}, AllowedChars, _, Value) -> 225 | urlencode(string:slice(binarize(Value), 0, MaxLen), AllowedChars); 226 | apply_modifier(_, AllowedChars, _, Value) -> 227 | urlencode(binarize(Value), AllowedChars). 228 | 229 | apply_parameter_modifier(_, _, _, _, _, []) -> 230 | []; 231 | apply_parameter_modifier(_, _, _, _, _, Map) when Map =:= #{} -> 232 | []; 233 | apply_parameter_modifier(no_modifier, AllowedChars, _, _, Name, List) when is_list(List) -> 234 | [ 235 | Name, 236 | $=, 237 | lists:join($,, [urlencode(Value, AllowedChars) || Value <- List]) 238 | ]; 239 | apply_parameter_modifier(explode_modifier, AllowedChars, ExplodeSep, _, Name, List) when is_list(List) -> 240 | lists:join(ExplodeSep, [[ 241 | Name, 242 | $=, 243 | urlencode(Value, AllowedChars) 244 | ] || Value <- List]); 245 | apply_parameter_modifier(Modifier, AllowedChars, ExplodeSep, _, Name, Map) when is_map(Map) -> 246 | {JoinSep, KVSep} = case Modifier of 247 | no_modifier -> {$,, $,}; 248 | explode_modifier -> {ExplodeSep, $=} 249 | end, 250 | [ 251 | case Modifier of 252 | no_modifier -> 253 | [ 254 | Name, 255 | $= 256 | ]; 257 | explode_modifier -> 258 | [] 259 | end, 260 | lists:reverse(lists:join(JoinSep, 261 | maps:fold(fun(Key, Value, Acc) -> 262 | [[ 263 | urlencode(Key, AllowedChars), 264 | KVSep, 265 | urlencode(Value, AllowedChars) 266 | ]|Acc] 267 | end, [], Map) 268 | )) 269 | ]; 270 | apply_parameter_modifier(Modifier, AllowedChars, _, Trim, Name, Value0) -> 271 | Value1 = binarize(Value0), 272 | Value = case Modifier of 273 | {prefix_modifier, MaxLen} -> 274 | string:slice(Value1, 0, MaxLen); 275 | no_modifier -> 276 | Value1 277 | end, 278 | [ 279 | Name, 280 | case Value of 281 | <<>> when Trim =:= trim -> 282 | []; 283 | <<>> when Trim =:= no_trim -> 284 | $=; 285 | _ -> 286 | [ 287 | $=, 288 | urlencode(Value, AllowedChars) 289 | ] 290 | end 291 | ]. 292 | 293 | binarize(Value) when is_integer(Value) -> 294 | integer_to_binary(Value); 295 | binarize(Value) when is_float(Value) -> 296 | float_to_binary(Value, [{decimals, 10}, compact]); 297 | binarize(Value) -> 298 | Value. 299 | 300 | urlencode(Value, unreserved) -> 301 | urlencode_unreserved(Value, <<>>); 302 | urlencode(Value, reserved) -> 303 | urlencode_reserved(Value, <<>>). 304 | 305 | urlencode_unreserved(<>, Acc) 306 | when ?IS_URI_UNRESERVED(C) -> 307 | urlencode_unreserved(R, <>); 308 | urlencode_unreserved(<>, Acc) -> 309 | urlencode_unreserved(R, <>); 310 | urlencode_unreserved(<<>>, Acc) -> 311 | Acc. 312 | 313 | urlencode_reserved(<<$%,H,L,R/bits>>, Acc) 314 | when ?IS_HEX(H), ?IS_HEX(L) -> 315 | urlencode_reserved(R, <>); 316 | urlencode_reserved(<>, Acc) 317 | when ?IS_URI_UNRESERVED(C) or ?IS_URI_GEN_DELIMS(C) or ?IS_URI_SUB_DELIMS(C) -> 318 | urlencode_reserved(R, <>); 319 | urlencode_reserved(<>, Acc) -> 320 | urlencode_reserved(R, <>); 321 | urlencode_reserved(<<>>, Acc) -> 322 | Acc. 323 | 324 | -ifdef(TEST). 325 | expand_uritemplate_test_() -> 326 | Files = filelib:wildcard("deps/uritemplate-tests/*.json"), 327 | lists:flatten([begin 328 | {ok, JSON} = file:read_file(File), 329 | Tests = jsx:decode(JSON, [return_maps]), 330 | [begin 331 | %% Erlang doesn't have a NULL value. 332 | Vars = maps:remove(<<"undef">>, Vars0), 333 | [ 334 | {iolist_to_binary(io_lib:format("~s - ~s: ~s => ~s", 335 | [filename:basename(File), Section, URITemplate, 336 | if 337 | is_list(Expected) -> lists:join(<<" OR ">>, Expected); 338 | true -> Expected 339 | end 340 | ])), 341 | fun() -> 342 | io:format("expected: ~0p", [Expected]), 343 | case Expected of 344 | false -> 345 | {'EXIT', _} = (catch expand(URITemplate, Vars)); 346 | [_|_] -> 347 | Result = iolist_to_binary(expand(URITemplate, Vars)), 348 | io:format("~p", [Result]), 349 | true = lists:member(Result, Expected); 350 | _ -> 351 | Expected = iolist_to_binary(expand(URITemplate, Vars)) 352 | end 353 | end} 354 | || [URITemplate, Expected] <- Cases] 355 | end || {Section, #{ 356 | <<"variables">> := Vars0, 357 | <<"testcases">> := Cases 358 | }} <- maps:to_list(Tests)] 359 | end || File <- Files]). 360 | -endif. 361 | --------------------------------------------------------------------------------