From f60a8b1725809abc94bee830b2297807290a0d83 Mon Sep 17 00:00:00 2001 From: absc Date: Sun, 11 Aug 2024 22:08:52 +0000 Subject: [PATCH] Import "gun", the HTTP client. We will use it for the tests and the ActivityPub activities. --- gun/.gitignore | 11 + gun/LICENSE | 13 + gun/Makefile | 7 + gun/README.asciidoc | 43 + gun/doc/src/guide/book.asciidoc | 35 + gun/doc/src/guide/connect.asciidoc | 166 + gun/doc/src/guide/gun.sty | 8 + gun/doc/src/guide/http.asciidoc | 387 + .../src/guide/internals_tls_over_tls.asciidoc | 177 + gun/doc/src/guide/introduction.asciidoc | 49 + gun/doc/src/guide/migrating_from_1.0.asciidoc | 21 + gun/doc/src/guide/migrating_from_1.1.asciidoc | 28 + gun/doc/src/guide/migrating_from_1.2.asciidoc | 39 + gun/doc/src/guide/migrating_from_1.3.asciidoc | 329 + gun/doc/src/guide/migrating_from_2.0.asciidoc | 25 + gun/doc/src/guide/protocols.asciidoc | 120 + gun/doc/src/guide/start.asciidoc | 43 + gun/doc/src/guide/websocket.asciidoc | 124 + gun/doc/src/manual/gun.asciidoc | 639 ++ gun/doc/src/manual/gun.await.asciidoc | 122 + gun/doc/src/manual/gun.await_body.asciidoc | 91 + gun/doc/src/manual/gun.await_up.asciidoc | 75 + gun/doc/src/manual/gun.cancel.asciidoc | 70 + gun/doc/src/manual/gun.close.asciidoc | 45 + gun/doc/src/manual/gun.connect.asciidoc | 115 + gun/doc/src/manual/gun.data.asciidoc | 75 + gun/doc/src/manual/gun.delete.asciidoc | 82 + gun/doc/src/manual/gun.flush.asciidoc | 59 + gun/doc/src/manual/gun.get.asciidoc | 85 + gun/doc/src/manual/gun.head.asciidoc | 92 + gun/doc/src/manual/gun.headers.asciidoc | 86 + gun/doc/src/manual/gun.info.asciidoc | 69 + gun/doc/src/manual/gun.open.asciidoc | 77 + gun/doc/src/manual/gun.open_unix.asciidoc | 65 + gun/doc/src/manual/gun.options.asciidoc | 83 + gun/doc/src/manual/gun.patch.asciidoc | 123 + gun/doc/src/manual/gun.post.asciidoc | 120 + gun/doc/src/manual/gun.put.asciidoc | 119 + gun/doc/src/manual/gun.request.asciidoc | 97 + gun/doc/src/manual/gun.set_owner.asciidoc | 57 + gun/doc/src/manual/gun.shutdown.asciidoc | 67 + gun/doc/src/manual/gun.stream_info.asciidoc | 79 + gun/doc/src/manual/gun.update_flow.asciidoc | 67 + gun/doc/src/manual/gun.ws_send.asciidoc | 72 + gun/doc/src/manual/gun.ws_upgrade.asciidoc | 117 + gun/doc/src/manual/gun_app.asciidoc | 44 + gun/doc/src/manual/gun_cookies.asciidoc | 179 + .../manual/gun_cookies.domain_match.asciidoc | 52 + .../manual/gun_cookies.path_match.asciidoc | 52 + gun/doc/src/manual/gun_cookies_list.asciidoc | 55 + gun/doc/src/manual/gun_data.asciidoc | 78 + gun/doc/src/manual/gun_down.asciidoc | 79 + gun/doc/src/manual/gun_error.asciidoc | 68 + gun/doc/src/manual/gun_event.asciidoc | 509 + gun/doc/src/manual/gun_inform.asciidoc | 70 + gun/doc/src/manual/gun_push.asciidoc | 90 + gun/doc/src/manual/gun_response.asciidoc | 76 + gun/doc/src/manual/gun_trailers.asciidoc | 68 + gun/doc/src/manual/gun_tunnel_up.asciidoc | 66 + gun/doc/src/manual/gun_up.asciidoc | 66 + gun/doc/src/manual/gun_upgrade.asciidoc | 72 + gun/doc/src/manual/gun_ws.asciidoc | 65 + gun/doc/src/manual/gun_ws_protocol.asciidoc | 108 + gun/ebin/gun.app | 10 + gun/src/Makefile | 23 + gun/src/gun.erl | 1868 ++++ gun/src/gun_app.erl | 30 + gun/src/gun_conns_sup.erl | 36 + gun/src/gun_content_handler.erl | 76 + gun/src/gun_cookies.erl | 668 ++ gun/src/gun_cookies_list.erl | 144 + gun/src/gun_data_h.erl | 33 + gun/src/gun_default_event_h.erl | 129 + gun/src/gun_event.erl | 316 + gun/src/gun_http.erl | 1058 ++ gun/src/gun_http2.erl | 1601 +++ gun/src/gun_pool.erl | 719 ++ gun/src/gun_pool_events_h.erl | 157 + gun/src/gun_pools_sup.erl | 37 + gun/src/gun_protocols.erl | 72 + gun/src/gun_public_suffix.erl | 9520 +++++++++++++++++ gun/src/gun_public_suffix.erl.src | 29 + gun/src/gun_raw.erl | 97 + gun/src/gun_socks.erl | 207 + gun/src/gun_sse_h.erl | 63 + gun/src/gun_sup.erl | 37 + gun/src/gun_tcp.erl | 112 + gun/src/gun_tcp_proxy.erl | 70 + gun/src/gun_tls.erl | 49 + gun/src/gun_tls_proxy.erl | 492 + gun/src/gun_tls_proxy_cb.erl | 41 + gun/src/gun_tls_proxy_http2_connect.erl | 69 + gun/src/gun_tunnel.erl | 637 ++ gun/src/gun_ws.erl | 354 + gun/src/gun_ws_h.erl | 44 + gun/src/gun_ws_protocol.erl | 25 + gun/test/event_SUITE.erl | 2170 ++++ gun/test/flow_SUITE.erl | 339 + gun/test/gun_SUITE.erl | 754 ++ gun/test/gun_ct_hook.erl | 22 + gun/test/gun_test.erl | 133 + gun/test/gun_test_event_h.erl | 64 + gun/test/gun_test_fun_event_h.erl | 55 + gun/test/h2specd_SUITE.erl | 139 + gun/test/handlers/cookie_echo_h.erl | 11 + gun/test/handlers/cookie_informational_h.erl | 10 + gun/test/handlers/cookie_parser_h.erl | 23 + gun/test/handlers/cookie_parser_result_h.erl | 25 + gun/test/handlers/cookie_set_h.erl | 42 + gun/test/handlers/delayed_hello_h.erl | 11 + gun/test/handlers/delayed_push_h.erl | 13 + gun/test/handlers/empty_h.erl | 11 + gun/test/handlers/hello_h.erl | 10 + gun/test/handlers/inform_h.erl | 16 + gun/test/handlers/pool_ws_handler.erl | 13 + gun/test/handlers/proxied_h.erl | 11 + gun/test/handlers/push_h.erl | 12 + gun/test/handlers/sse_clock_close_h.erl | 23 + gun/test/handlers/sse_clock_h.erl | 25 + gun/test/handlers/sse_lone_id_h.erl | 19 + gun/test/handlers/sse_mime_param_h.erl | 19 + gun/test/handlers/stream_h.erl | 14 + gun/test/handlers/trailers_h.erl | 18 + gun/test/handlers/ws_cookie_h.erl | 24 + gun/test/handlers/ws_echo_h.erl | 22 + gun/test/handlers/ws_frozen_h.erl | 23 + gun/test/handlers/ws_reject_h.erl | 7 + gun/test/handlers/ws_subprotocol_h.erl | 31 + gun/test/handlers/ws_timeout_close_h.erl | 25 + gun/test/pool_SUITE.erl | 408 + gun/test/raw_SUITE.erl | 391 + gun/test/rfc6265bis_SUITE.erl | 956 ++ gun/test/rfc7230_SUITE.erl | 101 + gun/test/rfc7231_SUITE.erl | 568 + gun/test/rfc7540_SUITE.erl | 808 ++ gun/test/send_errors_SUITE.erl | 139 + gun/test/shutdown_SUITE.erl | 672 ++ gun/test/socks_SUITE.erl | 521 + gun/test/sse_SUITE.erl | 161 + gun/test/tunnel_SUITE.erl | 1116 ++ gun/test/wpt/cookies/attributes_expires.json | 1 + gun/test/wpt/cookies/attributes_invalid.json | 1 + gun/test/wpt/cookies/attributes_max_age.json | 1 + gun/test/wpt/cookies/attributes_path.json | 1 + gun/test/wpt/cookies/attributes_secure.json | 1 + .../cookies/attributes_secure_non_secure.json | 1 + gun/test/wpt/cookies/encoding_charset.json | 1 + gun/test/wpt/cookies/name.json | 1 + gun/test/wpt/cookies/size_attributes.json | 1 + gun/test/wpt/cookies/size_name_and_value.json | 1 + gun/test/wpt/cookies/value.json | 1 + gun/test/ws_SUITE.erl | 270 + gun/test/ws_autobahn_SUITE.erl | 170 + gun/test/ws_autobahn_SUITE_data/server.json | 7 + 154 files changed, 35526 insertions(+) create mode 100644 gun/.gitignore create mode 100644 gun/LICENSE create mode 100644 gun/Makefile create mode 100644 gun/README.asciidoc create mode 100644 gun/doc/src/guide/book.asciidoc create mode 100644 gun/doc/src/guide/connect.asciidoc create mode 100644 gun/doc/src/guide/gun.sty create mode 100644 gun/doc/src/guide/http.asciidoc create mode 100644 gun/doc/src/guide/internals_tls_over_tls.asciidoc create mode 100644 gun/doc/src/guide/introduction.asciidoc create mode 100644 gun/doc/src/guide/migrating_from_1.0.asciidoc create mode 100644 gun/doc/src/guide/migrating_from_1.1.asciidoc create mode 100644 gun/doc/src/guide/migrating_from_1.2.asciidoc create mode 100644 gun/doc/src/guide/migrating_from_1.3.asciidoc create mode 100644 gun/doc/src/guide/migrating_from_2.0.asciidoc create mode 100644 gun/doc/src/guide/protocols.asciidoc create mode 100644 gun/doc/src/guide/start.asciidoc create mode 100644 gun/doc/src/guide/websocket.asciidoc create mode 100644 gun/doc/src/manual/gun.asciidoc create mode 100644 gun/doc/src/manual/gun.await.asciidoc create mode 100644 gun/doc/src/manual/gun.await_body.asciidoc create mode 100644 gun/doc/src/manual/gun.await_up.asciidoc create mode 100644 gun/doc/src/manual/gun.cancel.asciidoc create mode 100644 gun/doc/src/manual/gun.close.asciidoc create mode 100644 gun/doc/src/manual/gun.connect.asciidoc create mode 100644 gun/doc/src/manual/gun.data.asciidoc create mode 100644 gun/doc/src/manual/gun.delete.asciidoc create mode 100644 gun/doc/src/manual/gun.flush.asciidoc create mode 100644 gun/doc/src/manual/gun.get.asciidoc create mode 100644 gun/doc/src/manual/gun.head.asciidoc create mode 100644 gun/doc/src/manual/gun.headers.asciidoc create mode 100644 gun/doc/src/manual/gun.info.asciidoc create mode 100644 gun/doc/src/manual/gun.open.asciidoc create mode 100644 gun/doc/src/manual/gun.open_unix.asciidoc create mode 100644 gun/doc/src/manual/gun.options.asciidoc create mode 100644 gun/doc/src/manual/gun.patch.asciidoc create mode 100644 gun/doc/src/manual/gun.post.asciidoc create mode 100644 gun/doc/src/manual/gun.put.asciidoc create mode 100644 gun/doc/src/manual/gun.request.asciidoc create mode 100644 gun/doc/src/manual/gun.set_owner.asciidoc create mode 100644 gun/doc/src/manual/gun.shutdown.asciidoc create mode 100644 gun/doc/src/manual/gun.stream_info.asciidoc create mode 100644 gun/doc/src/manual/gun.update_flow.asciidoc create mode 100644 gun/doc/src/manual/gun.ws_send.asciidoc create mode 100644 gun/doc/src/manual/gun.ws_upgrade.asciidoc create mode 100644 gun/doc/src/manual/gun_app.asciidoc create mode 100644 gun/doc/src/manual/gun_cookies.asciidoc create mode 100644 gun/doc/src/manual/gun_cookies.domain_match.asciidoc create mode 100644 gun/doc/src/manual/gun_cookies.path_match.asciidoc create mode 100644 gun/doc/src/manual/gun_cookies_list.asciidoc create mode 100644 gun/doc/src/manual/gun_data.asciidoc create mode 100644 gun/doc/src/manual/gun_down.asciidoc create mode 100644 gun/doc/src/manual/gun_error.asciidoc create mode 100644 gun/doc/src/manual/gun_event.asciidoc create mode 100644 gun/doc/src/manual/gun_inform.asciidoc create mode 100644 gun/doc/src/manual/gun_push.asciidoc create mode 100644 gun/doc/src/manual/gun_response.asciidoc create mode 100644 gun/doc/src/manual/gun_trailers.asciidoc create mode 100644 gun/doc/src/manual/gun_tunnel_up.asciidoc create mode 100644 gun/doc/src/manual/gun_up.asciidoc create mode 100644 gun/doc/src/manual/gun_upgrade.asciidoc create mode 100644 gun/doc/src/manual/gun_ws.asciidoc create mode 100644 gun/doc/src/manual/gun_ws_protocol.asciidoc create mode 100644 gun/ebin/gun.app create mode 100644 gun/src/Makefile create mode 100644 gun/src/gun.erl create mode 100644 gun/src/gun_app.erl create mode 100644 gun/src/gun_conns_sup.erl create mode 100644 gun/src/gun_content_handler.erl create mode 100644 gun/src/gun_cookies.erl create mode 100644 gun/src/gun_cookies_list.erl create mode 100644 gun/src/gun_data_h.erl create mode 100644 gun/src/gun_default_event_h.erl create mode 100644 gun/src/gun_event.erl create mode 100644 gun/src/gun_http.erl create mode 100644 gun/src/gun_http2.erl create mode 100644 gun/src/gun_pool.erl create mode 100644 gun/src/gun_pool_events_h.erl create mode 100644 gun/src/gun_pools_sup.erl create mode 100644 gun/src/gun_protocols.erl create mode 100644 gun/src/gun_public_suffix.erl create mode 100644 gun/src/gun_public_suffix.erl.src create mode 100644 gun/src/gun_raw.erl create mode 100644 gun/src/gun_socks.erl create mode 100644 gun/src/gun_sse_h.erl create mode 100644 gun/src/gun_sup.erl create mode 100644 gun/src/gun_tcp.erl create mode 100644 gun/src/gun_tcp_proxy.erl create mode 100644 gun/src/gun_tls.erl create mode 100644 gun/src/gun_tls_proxy.erl create mode 100644 gun/src/gun_tls_proxy_cb.erl create mode 100644 gun/src/gun_tls_proxy_http2_connect.erl create mode 100644 gun/src/gun_tunnel.erl create mode 100644 gun/src/gun_ws.erl create mode 100644 gun/src/gun_ws_h.erl create mode 100644 gun/src/gun_ws_protocol.erl create mode 100644 gun/test/event_SUITE.erl create mode 100644 gun/test/flow_SUITE.erl create mode 100644 gun/test/gun_SUITE.erl create mode 100644 gun/test/gun_ct_hook.erl create mode 100644 gun/test/gun_test.erl create mode 100644 gun/test/gun_test_event_h.erl create mode 100644 gun/test/gun_test_fun_event_h.erl create mode 100644 gun/test/h2specd_SUITE.erl create mode 100644 gun/test/handlers/cookie_echo_h.erl create mode 100644 gun/test/handlers/cookie_informational_h.erl create mode 100644 gun/test/handlers/cookie_parser_h.erl create mode 100644 gun/test/handlers/cookie_parser_result_h.erl create mode 100644 gun/test/handlers/cookie_set_h.erl create mode 100644 gun/test/handlers/delayed_hello_h.erl create mode 100644 gun/test/handlers/delayed_push_h.erl create mode 100644 gun/test/handlers/empty_h.erl create mode 100644 gun/test/handlers/hello_h.erl create mode 100644 gun/test/handlers/inform_h.erl create mode 100644 gun/test/handlers/pool_ws_handler.erl create mode 100644 gun/test/handlers/proxied_h.erl create mode 100644 gun/test/handlers/push_h.erl create mode 100644 gun/test/handlers/sse_clock_close_h.erl create mode 100644 gun/test/handlers/sse_clock_h.erl create mode 100644 gun/test/handlers/sse_lone_id_h.erl create mode 100644 gun/test/handlers/sse_mime_param_h.erl create mode 100644 gun/test/handlers/stream_h.erl create mode 100644 gun/test/handlers/trailers_h.erl create mode 100644 gun/test/handlers/ws_cookie_h.erl create mode 100644 gun/test/handlers/ws_echo_h.erl create mode 100644 gun/test/handlers/ws_frozen_h.erl create mode 100644 gun/test/handlers/ws_reject_h.erl create mode 100644 gun/test/handlers/ws_subprotocol_h.erl create mode 100644 gun/test/handlers/ws_timeout_close_h.erl create mode 100644 gun/test/pool_SUITE.erl create mode 100644 gun/test/raw_SUITE.erl create mode 100644 gun/test/rfc6265bis_SUITE.erl create mode 100644 gun/test/rfc7230_SUITE.erl create mode 100644 gun/test/rfc7231_SUITE.erl create mode 100644 gun/test/rfc7540_SUITE.erl create mode 100644 gun/test/send_errors_SUITE.erl create mode 100644 gun/test/shutdown_SUITE.erl create mode 100644 gun/test/socks_SUITE.erl create mode 100644 gun/test/sse_SUITE.erl create mode 100644 gun/test/tunnel_SUITE.erl create mode 100644 gun/test/wpt/cookies/attributes_expires.json create mode 100644 gun/test/wpt/cookies/attributes_invalid.json create mode 100644 gun/test/wpt/cookies/attributes_max_age.json create mode 100644 gun/test/wpt/cookies/attributes_path.json create mode 100644 gun/test/wpt/cookies/attributes_secure.json create mode 100644 gun/test/wpt/cookies/attributes_secure_non_secure.json create mode 100644 gun/test/wpt/cookies/encoding_charset.json create mode 100644 gun/test/wpt/cookies/name.json create mode 100644 gun/test/wpt/cookies/size_attributes.json create mode 100644 gun/test/wpt/cookies/size_name_and_value.json create mode 100644 gun/test/wpt/cookies/value.json create mode 100644 gun/test/ws_SUITE.erl create mode 100644 gun/test/ws_autobahn_SUITE.erl create mode 100644 gun/test/ws_autobahn_SUITE_data/server.json diff --git a/gun/.gitignore b/gun/.gitignore new file mode 100644 index 0000000..afc2852 --- /dev/null +++ b/gun/.gitignore @@ -0,0 +1,11 @@ +.erlang.mk +.*.plt +*.d +deps +doc/guide.pdf +doc/html +doc/man* +ebin/test +ebin/*.beam +logs +test/*.beam diff --git a/gun/LICENSE b/gun/LICENSE new file mode 100644 index 0000000..4f91160 --- /dev/null +++ b/gun/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2013-2023, Loïc Hoguin + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/gun/Makefile b/gun/Makefile new file mode 100644 index 0000000..160022d --- /dev/null +++ b/gun/Makefile @@ -0,0 +1,7 @@ +.PHONY: all clean + +all: + ${MAKE} -C src + +clean: + ${MAKE} -C src clean diff --git a/gun/README.asciidoc b/gun/README.asciidoc new file mode 100644 index 0000000..5ce3102 --- /dev/null +++ b/gun/README.asciidoc @@ -0,0 +1,43 @@ += Gun + +Gun is an Erlang HTTP client with support for HTTP/1.1, +HTTP/2, Websocket and more. + +== Goals + +Gun aims to provide an *easy to use* client compatible with +HTTP/1.1, HTTP/2 and Websocket. Gun can connect through any +combination of Socks and HTTP proxies. + +Gun is *always connected*. It will maintain a permanent +connection to the server, reopening it as soon as the server +closes it, saving time for the requests that come in. + +All connections are *supervised* automatically, allowing +developers to focus on writing their code without worrying. + +== Sponsors + +Gun was previously sponsored by +http://leo-project.net/leofs/[LeoFS Cloud Storage], +https://sameroom.io/[Sameroom], +and https://pleroma.social/[Pleroma]. + +== Online documentation + +* https://ninenines.eu/docs/en/gun/2.1/guide[User guide] +* https://ninenines.eu/docs/en/gun/2.1/manual[Function reference] + +== Offline documentation + +* While still online, run `make docs` +* User guide available in `doc/` in PDF and HTML formats +* Function reference man pages available in `doc/man3/` and `doc/man7/` +* Run `make install-docs` to install man pages on your system +* Full documentation in Asciidoc available in `doc/src/` + +== Getting help + +* Official IRC Channel: #ninenines on irc.freenode.net +* https://github.com/ninenines/gun/issues[Issues tracker] +* https://ninenines.eu/services/[Commercial Support] diff --git a/gun/doc/src/guide/book.asciidoc b/gun/doc/src/guide/book.asciidoc new file mode 100644 index 0000000..3d1926d --- /dev/null +++ b/gun/doc/src/guide/book.asciidoc @@ -0,0 +1,35 @@ +// a2x: --dblatex-opts "-P latex.output.revhistory=0 -P doc.publisher.show=0 -P index.numbered=0" +// a2x: --dblatex-opts "-s gun" +// a2x: -d book --attribute tabsize=4 + += Gun User Guide + += Interface + +include::introduction.asciidoc[Introduction] + +include::start.asciidoc[Starting and stopping] + +include::protocols.asciidoc[Supported protocols] + +include::connect.asciidoc[Connection] + +include::http.asciidoc[Using HTTP] + +include::websocket.asciidoc[Using Websocket] + += Advanced + +include::internals_tls_over_tls.asciidoc[Internals: TLS over TLS] + += Additional information + +include::migrating_from_2.0.asciidoc[Changes since Gun 2.0] + +include::migrating_from_1.3.asciidoc[Migrating from Gun 1.3 to 2.0] + +include::migrating_from_1.2.asciidoc[Migrating from Gun 1.2 to 1.3] + +include::migrating_from_1.1.asciidoc[Migrating from Gun 1.1 to 1.2] + +include::migrating_from_1.0.asciidoc[Migrating from Gun 1.0 to 1.1] diff --git a/gun/doc/src/guide/connect.asciidoc b/gun/doc/src/guide/connect.asciidoc new file mode 100644 index 0000000..08f8db2 --- /dev/null +++ b/gun/doc/src/guide/connect.asciidoc @@ -0,0 +1,166 @@ +[[connect]] +== Connection + +This chapter describes how to open, monitor and close +a connection using the Gun client. + +=== Gun connections + +Gun is designed with the HTTP/2 and Websocket protocols in mind. +They are built for long-running connections that allow concurrent +exchange of data, either in the form of request/responses for +HTTP/2 or in the form of messages for Websocket. + +A Gun connection is an Erlang process that manages a socket to +a remote endpoint. This Gun connection is owned by a user +process that is called the _owner_ of the connection, and is +managed by the supervision tree of the `gun` application. + +Any process can communicate with the Gun connection +by calling functions from the module `gun`. All functions +perform their respective operations asynchronously. The Gun +connection will send Erlang messages to the calling process +whenever needed. + +When the remote endpoint closes the connection, Gun attempts +to reconnect automatically. + +=== Opening a new connection + +The `gun:open/2,3` function must be used to open a connection. + +.Opening a connection to example.org on port 443 +[source,erlang] +---- +{ok, ConnPid} = gun:open("example.org", 443). +---- + +If the port given is 443, Gun will attempt to connect using +TLS. The protocol will be selected automatically using the +ALPN extension for TLS. By default Gun supports HTTP/2 +and HTTP/1.1 when connecting using TLS. + +For any other port, Gun will attempt to connect using +plain TCP and will use the HTTP/1.1 protocol. + +The transport and protocol used can be overriden via +options. The manual documents all available options. + +Options can be provided as a third argument, and take the +form of a map. + +.Opening a TLS connection to example.org on port 8443 +[source,erlang] +---- +{ok, ConnPid} = gun:open("example.org", 8443, #{transport => tls}). +---- + +When using TLS you may want to tweak the +http://erlang.org/doc/man/ssl_app.html#configuration[configuration] +for the `ssl` application, in particular the `session_lifetime` +and `session_cache_client_max` to limit the amount of memory +used for the TLS sessions cache. + +=== Waiting for the connection to be established + +When Gun successfully connects to the server, it sends a +`gun_up` message with the protocol that has been selected +for the connection. + +Gun provides the functions `gun:await_up/1,2,3` that wait +for the `gun_up` message. They can optionally take a monitor +reference and/or timeout value. If no monitor is provided, +one will be created for the duration of the function call. + +.Synchronous opening of a connection +[source,erlang] +---- +{ok, ConnPid} = gun:open("example.org", 443), +{ok, Protocol} = gun:await_up(ConnPid). +---- + +=== Handling connection loss + +When the connection is lost, Gun will send a `gun_down` +message indicating the current protocol, the reason the +connection was lost and two lists of stream references. + +The first list indicates open streams that _may_ have been +processed by the server. The second list indicates open +streams that the server did not process. + +=== Monitoring the connection process + +Because software errors are unavoidable, it is important to +detect when the Gun process crashes. It is also important +to detect when it exits normally. Erlang provides two ways +to do that: links and monitors. + +Gun leaves you the choice as to which one will be used. +However, if you use the `gun:await/2,3` or `gun:await_body/2,3` +functions, a monitor may be used for you to avoid getting +stuck waiting for a message that will never come. + +If you choose to monitor yourself you can do it on a permanent +basis rather than on every message you will receive, saving +resources. Indeed, the `gun:await/3,4` and `gun:await_body/3,4` +functions both accept a monitor argument if you have one already. + +.Monitoring the connection process +[source,erlang] +---- +{ok, ConnPid} = gun:open("example.org", 443). +MRef = monitor(process, ConnPid). +---- + +This monitor reference can be kept and used until the connection +process exits. + +.Handling `DOWN` messages +[source,erlang] +---- +receive + %% Receive Gun messages here... + {'DOWN', Mref, process, ConnPid, Reason} -> + error_logger:error_msg("Oops!"), + exit(Reason) +end. +---- + +What to do when you receive a `DOWN` message is entirely up to you. + +=== Closing the connection abruptly + +The connection can be stopped abruptly at any time by calling +the `gun:close/1` function. + +.Immediate closing of the connection +[source,erlang] +---- +gun:close(ConnPid). +---- + +The process is stopped immediately without having a chance to +perform the protocol's closing handshake, if any. + +//=== Closing the connection gracefully +// +//The connection can also be stopped gracefully by calling the +//`gun:shutdown/1` function. +// +//.Graceful shutdown of the connection +//[source,erlang] +//---- +//gun:shutdown(ConnPid). +//---- +// +//Gun will refuse any new requests or messages after you call +//this function. It will however continue to send you messages +//for existing streams until they are all completed. +// +//For example if you performed a GET request just before calling +//`gun:shutdown/1`, you will still receive the response before +//Gun closes the connection. +// +//If you set a monitor beforehand, you will receive a message +//when the connection has been closed. diff --git a/gun/doc/src/guide/gun.sty b/gun/doc/src/guide/gun.sty new file mode 100644 index 0000000..d5e0d3b --- /dev/null +++ b/gun/doc/src/guide/gun.sty @@ -0,0 +1,8 @@ +\NeedsTeXFormat{LaTeX2e} +\ProvidesPackage{asciidoc-dblatex}[2012/10/24 AsciiDoc DocBook Style] + +%% Just use the original package and pass the options. +\RequirePackageWithOptions{docbook} + +%% Define an alias for make snippets to be compatible with source-highlighter. +\lstalias{makefile}{make} diff --git a/gun/doc/src/guide/http.asciidoc b/gun/doc/src/guide/http.asciidoc new file mode 100644 index 0000000..51cb994 --- /dev/null +++ b/gun/doc/src/guide/http.asciidoc @@ -0,0 +1,387 @@ +[[http]] +== HTTP + +This chapter describes how to use the Gun client for +communicating with an HTTP/1.1 or HTTP/2 server. + +=== Streams + +Every time a request is initiated, Gun creates a _stream_. +A _stream reference_ uniquely identifies a set of request and +response and must be used to perform additional operations +with a stream or to identify its messages. + +Stream references use the Erlang _reference_ data type and +are therefore unique. + +Streams can be canceled at any time. This will stop any further +messages from being sent to the calling process. Depending on +its capabilities, the server will also be instructed to cancel +the request. + +Canceling a stream may result in Gun dropping the connection +temporarily, to avoid uploading or downloading data that will +not be used. + +.Cancelling a stream +[source,erlang] +---- +gun:cancel(ConnPid, StreamRef). +---- + +=== Sending requests + +Gun provides many convenient functions for performing common +operations, like GET, POST or DELETE. It also provides a +general purpose function in case you need other methods. + +The availability of these methods on the server can vary +depending on the software used but also on a per-resource +basis. + +Gun will automatically set a few headers depending on the +method used. For all methods however it will set the host +header if it has not been provided in the request arguments. + +This section focuses on the act of sending a request. The +handling of responses will be explained further on. + +==== GET and HEAD + +Use `gun:get/2,3,4` to request a resource. + +.GET "/organizations/ninenines" +[source,erlang] +---- +StreamRef = gun:get(ConnPid, "/organizations/ninenines"). +---- + +.GET "/organizations/ninenines" with custom headers +[source,erlang] +---- +StreamRef = gun:get(ConnPid, "/organizations/ninenines", [ + {<<"accept">>, "application/json"}, + {<<"user-agent">>, "revolver/1.0"} +]). +---- + +Note that the list of headers has the field name as a binary. +The field value is iodata, which is either a binary or an +iolist. + +Use `gun:head/2,3,4` if you don't need the response body. + +.HEAD "/organizations/ninenines" +[source,erlang] +---- +StreamRef = gun:head(ConnPid, "/organizations/ninenines"). +---- + +.HEAD "/organizations/ninenines" with custom headers +[source,erlang] +---- +StreamRef = gun:head(ConnPid, "/organizations/ninenines", [ + {<<"accept">>, "application/json"}, + {<<"user-agent">>, "revolver/1.0"} +]). +---- + +It is not possible to send a request body with a GET or HEAD +request. + +==== POST, PUT and PATCH + +HTTP defines three methods to create or update a resource. + +POST is generally used when the resource identifier (URI) isn't known +in advance when creating a resource. POST can also be used to +replace an existing resource, although PUT is more appropriate +in that situation. + +PUT creates or replaces a resource identified by the URI. + +PATCH provides instructions on how to modify the resource. + +Both POST and PUT send the entire resource representation in their +request body. The PATCH method can be used when this is not +desirable. The request body of a PATCH method may be a partial +representation or a list of instructions on how to update the +resource. + +The functions `gun:post/4,5`, `gun:put/4,5` and `gun:patch/4,5` +take a body as their fourth argument. These functions do +not require any body-specific header to be set, although +it is always recommended to set the content-type header. +Gun will set the other headers automatically. + +In this and the following examples in this section, `gun:post` +can be replaced by `gun:put` or `gun:patch` for performing +a PUT or PATCH request, respectively. + +.POST "/organizations/ninenines" +[source,erlang] +---- +Body = "{\"msg\": \"Hello world!\"}", +StreamRef = gun:post(ConnPid, "/organizations/ninenines", [ + {<<"content-type">>, "application/json"} +], Body). +---- + +The functions `gun:post/3,4`, `gun:put/3,4` and `gun:patch/3,4` +do not take a body in their arguments: the body must be +provided later on using the `gun:data/4` function. + +It is recommended to send the content-length header if you +know it in advance, although this is not required. If it +is not set, HTTP/1.1 will use the chunked transfer-encoding, +and HTTP/2 will continue normally as it is chunked by design. + +.POST "/organizations/ninenines" with delayed body +[source,erlang] +---- +Body = "{\"msg\": \"Hello world!\"}", +StreamRef = gun:post(ConnPid, "/organizations/ninenines", [ + {<<"content-length">>, integer_to_binary(length(Body))}, + {<<"content-type">>, "application/json"} +]), +gun:data(ConnPid, StreamRef, fin, Body). +---- + +The atom `fin` indicates this is the last chunk of data to +be sent. You can call the `gun:data/4` function as many +times as needed until you have sent the entire body. The +last call must use `fin` and all the previous calls must +use `nofin`. The last chunk may be empty. + +.Streaming the request body +[source,erlang] +---- +sendfile(ConnPid, StreamRef, Filepath) -> + {ok, IoDevice} = file:open(Filepath, [read, binary, raw]), + do_sendfile(ConnPid, StreamRef, IoDevice). + +do_sendfile(ConnPid, StreamRef, IoDevice) -> + case file:read(IoDevice, 8000) of + eof -> + gun:data(ConnPid, StreamRef, fin, <<>>), + file:close(IoDevice); + {ok, Bin} -> + gun:data(ConnPid, StreamRef, nofin, Bin), + do_sendfile(ConnPid, StreamRef, IoDevice) + end. +---- + +==== DELETE + +Use `gun:delete/2,3,4` to delete a resource. + +.DELETE "/organizations/ninenines" +[source,erlang] +---- +StreamRef = gun:delete(ConnPid, "/organizations/ninenines"). +---- + +.DELETE "/organizations/ninenines" with custom headers +[source,erlang] +---- +StreamRef = gun:delete(ConnPid, "/organizations/ninenines", [ + {<<"user-agent">>, "revolver/1.0"} +]). +---- + +==== OPTIONS + +Use `gun:options/2,3` to request information about a resource. + +.OPTIONS "/organizations/ninenines" +[source,erlang] +---- +StreamRef = gun:options(ConnPid, "/organizations/ninenines"). +---- + +.OPTIONS "/organizations/ninenines" with custom headers +[source,erlang] +---- +StreamRef = gun:options(ConnPid, "/organizations/ninenines", [ + {<<"user-agent">>, "revolver/1.0"} +]). +---- + +You can also use this function to request information about +the server itself. + +.OPTIONS "*" +[source,erlang] +---- +StreamRef = gun:options(ConnPid, "*"). +---- + +==== Requests with an arbitrary method + +The functions `gun:headers/4,5` or `gun:request/5,6` can be +used to send requests with a configurable method name. It is +mostly useful when you need a method that Gun does not +understand natively. + +.Example of a TRACE request +[source,erlang] +---- +gun:request(ConnPid, "TRACE", "/", [ + {<<"max-forwards">>, "30"} +], <<>>). +---- + +=== Processing responses + +All data received from the server is sent to the calling +process as a message. First a `gun_response` message is sent, +followed by zero or more `gun_data` messages. If something goes wrong, +a `gun_error` message is sent instead. + +The response message will inform you whether there will be +data messages following. If it contains `fin` there will be +no data messages. If it contains `nofin` then one or more data +messages will follow. + +When using HTTP/2 this value is sent with the frame and simply +passed on in the message. When using HTTP/1.1 however Gun must +guess whether data will follow by looking at the response headers. + +You can receive messages directly, or you can use the _await_ +functions to let Gun receive them for you. + +.Receiving a response using receive +[source,erlang] +---- +print_body(ConnPid, MRef) -> + StreamRef = gun:get(ConnPid, "/"), + receive + {gun_response, ConnPid, StreamRef, fin, Status, Headers} -> + no_data; + {gun_response, ConnPid, StreamRef, nofin, Status, Headers} -> + receive_data(ConnPid, MRef, StreamRef); + {'DOWN', MRef, process, ConnPid, Reason} -> + error_logger:error_msg("Oops!"), + exit(Reason) + after 1000 -> + exit(timeout) + end. + +receive_data(ConnPid, MRef, StreamRef) -> + receive + {gun_data, ConnPid, StreamRef, nofin, Data} -> + io:format("~s~n", [Data]), + receive_data(ConnPid, MRef, StreamRef); + {gun_data, ConnPid, StreamRef, fin, Data} -> + io:format("~s~n", [Data]); + {'DOWN', MRef, process, ConnPid, Reason} -> + error_logger:error_msg("Oops!"), + exit(Reason) + after 1000 -> + exit(timeout) + end. +---- + +While it may seem verbose, using messages like this has the +advantage of never locking your process, allowing you to +easily debug your code. It also allows you to start more than +one connection and concurrently perform queries on all of them +at the same time. + +You can also use Gun in a synchronous manner by using the _await_ +functions. + +The `gun:await/2,3,4` function will wait until it receives +a response to, a pushed resource related to, or data from +the given stream. + +When calling `gun:await/2,3` and not passing a monitor +reference, one is automatically created for you for the +duration of the call. + +The `gun:await_body/2,3,4` works similarly, but returns the +body received. Both functions can be combined to receive the +response and its body sequentially. + +.Receiving a response using await +[source,erlang] +---- +StreamRef = gun:get(ConnPid, "/"), +case gun:await(ConnPid, StreamRef) of + {response, fin, Status, Headers} -> + no_data; + {response, nofin, Status, Headers} -> + {ok, Body} = gun:await_body(ConnPid, StreamRef), + io:format("~s~n", [Body]) +end. +---- + +=== Handling streams pushed by the server + +The HTTP/2 protocol allows the server to push more than one +resource for every request. It will start sending those +extra resources before it starts sending the response itself, +so Gun will send you `gun_push` messages before `gun_response` +when that happens. + +You can safely choose to ignore `gun_push` messages, or +you can handle them. If you do, you can either receive the +messages directly or use _await_ functions. + +The `gun_push` message contains both the new stream reference +and the stream reference of the original request. + +.Receiving a pushed response using receive +[source,erlang] +---- +receive + {gun_push, ConnPid, OriginalStreamRef, PushedStreamRef, + Method, Host, Path, Headers} -> + enjoy() +end. +---- + +If you use the `gun:await/2,3,4` function, however, Gun +will use the original reference to identify the message but +will return a tuple that doesn't contain it. + +.Receiving a pushed response using await +[source,erlang] +---- +{push, PushedStreamRef, Method, URI, Headers} + = gun:await(ConnPid, OriginalStreamRef). +---- + +The `PushedStreamRef` variable can then be used with `gun:await/2,3,4` +and `gun:await_body/2,3,4`. + +=== Flushing unwanted messages + +Gun provides the function `gun:flush/1` to quickly get rid +of unwanted messages sitting in the process mailbox. You +can use it to get rid of all messages related to a connection, +or just the messages related to a stream. + +.Flush all messages from a Gun connection +[source,erlang] +---- +gun:flush(ConnPid). +---- + +.Flush all messages from a specific stream +[source,erlang] +---- +gun:flush(StreamRef). +---- + +=== Redirecting responses to a different process + +Gun allows you to specify which process will handle responses +to a request via the `reply_to` request option. + +.GET "/organizations/ninenines" to a different process +[source,erlang] +---- +StreamRef = gun:get(ConnPid, "/organizations/ninenines", [], + #{reply_to => Pid}). +---- diff --git a/gun/doc/src/guide/internals_tls_over_tls.asciidoc b/gun/doc/src/guide/internals_tls_over_tls.asciidoc new file mode 100644 index 0000000..07e8669 --- /dev/null +++ b/gun/doc/src/guide/internals_tls_over_tls.asciidoc @@ -0,0 +1,177 @@ +== Internals: TLS over TLS + +The `ssl` application that comes with Erlang/OTP implements +TLS using an interface equivalent to the `gen_tcp` interface: +you get and manipulate a socket. The TLS encoding and +decoding is applied transparently to the data sent and +received. + +In order to have a TLS layer inside another TLS layer we +need a way to encode the data of the inner layer before +we pass it to the outer layer. We cannot do this with +a socket interface. Thankfully, the `ssl` application comes +with options that allow to transform an `sslsocket()` into +an encoder/decoder. + +The implementation is however a little convoluted as a +result. This chapter aims to give an overview of how it +all works under the hood. + +=== gun_tls_proxy + +The module `gun_tls_proxy` implements an intermediary process +that sits between the Gun process and the TLS process. It is +responsible for routing data from the Gun process to the TLS +process, and from the TLS process to the Gun process. + +In order to obtain the TLS encoded data the `cb_info` option +is given to the `ssl:connect/3` function. This replaces the +default TCP outer socket module with our own custom module. +Gun uses the `gun_tls_proxy_cb` module instead. This module +will forward all messages to the `gun_tls_proxy` process. + +The resulting operations looks like this: + +---- +Gun process <-> gun_tls_proxy <-> sslsocket() <-> gun_tls_proxy <-> "inner socket" +---- + +The "inner socket" is the socket for the Gun connection. +The `gun_tls_proxy` process decides where to send or +receive the data based on where it's coming from. This +is how it knows whether the data has been encoded/decoded +or not. + +Because the `ssl:connect/3` function call is blocking, +a temporary process is used while connecting. This is required +because the `gun_tls_proxy` needs to forward data even while +performing the TLS handshake, otherwise the `ssl:connect/3` +call will not complete. + +The result of the `ssl:connect/3` call is forward to the Gun +process, along with the negotiated protocols when the connection +was successful. + +The `gun_tls_proxy_cb` module does not actually implement +`{active,N}` as requested by the `ssl` application. Instead +it uses `{active,true}`. + +The behavior of the `gun_tls_proxy` process will vary depending +on whether the TLS over TLS is done connection-wide or only +stream-wide. + +=== Connection-wide TLS over TLS + +When used for the entire connection, the `gun_tls_proxy` process +will act like a real socket once connected. The only difference +is how the connection is performed. As mentioned earlier, the +result of the `ssl:connect/3` call is sent back to the Gun process. + +When doing TLS over TLS the processes will end up looking like +this: + +---- +Gun process <-> gun_tls_proxy <-> "inner socket" +---- + +The details of the interactions between `gun_tls_proxy` and +its associated `sslsocket()` have been removed in order to +better illustrate the concept. + +When adding another layer this becomes: + +---- +Gun process <-> gun_tls_proxy <-> gun_tls_proxy <-> sslsocket() +---- + +This is what is done when only HTTP/1.1 and SOCKS proxies are +involved. + +=== Stream-wide TLS over TLS + +The same cannot be done for HTTP/2 proxies. This is because the +HTTP/2 CONNECT method does not apply to the entire connection, +but only to a stream. The proxied data must be wrapped inside +a DATA frame. It cannot be sent directly. This is what must be +done: + +---- +Gun process -> gun_tls_proxy -> Gun process -> "inner socket" +---- + +The "inner socket" is the socket for the HTTP/2 connection. + +In order to tell Gun to continue processing the data, the +`handle_continue` mechanism is introduced. When `gun_tls_proxy` +has TLS encoded the data it sends it back to the Gun process, +wrapped in a `handle_continue` tuple. This tuple contains +enough information to figure out what stream the data belongs +to and what should be done next. Gun will therefore route the +data to the correct layer and continue sending it. + +This solution is also used for receiving data, except in the +reverse order. + +=== Routing to the right stream + +In order to know where to route the data, the `stream_ref` +had to be modified to contain all the references to the +individual streams. So if the tunnel is identified by +`StreamA` and a request on this tunnel is identified +by `StreamB`, then the stream is known as `[StreamA, StreamB]` +to the user. Gun then routes first to `StreamA`, a +tunnel, which continues routing to `StreamB`. + +A problem comes up if an intermediary is a SOCKS server, +for example in the following scenario: + +---- +HTTP/2 proxy <-> SOCKS proxy <-> HTTP/1.1 origin +---- + +The SOCKS protocol doesn't have a concept of stream, +therefore when we refer to request to the origin server +they are `[StreamA, StreamB]`, not `[StreamA, StreamB, StreamC]`. +This is a problem for routing encoded/decoded TLS data +to the SOCKS layer: we don't have a built-in way of referring +to the SOCKS layer. + +The solution is to have a separate `handle_continue_stream_ref` +value that assigns a reference to the SOCKS layers. Gun will +then be able to forward `handle_continue` message, and only +them, to the appropriate layer. + +Gun therefore has two different routing avenues, but the +mechanism remains the same otherwise. + +=== gun_tunnel + +In order to simplify the routing, the `gun_tunnel` module +was introduced. For each intermediary (including the original +CONNECT stream at the HTTP/2 layer) there is a `gun_tunnel` +module. + +Going back to the example above: + +---- +HTTP/2 proxy <-> SOCKS proxy <-> HTTP/1.1 origin +---- + +In this case the modules involved to handle the request +will be as follow: + +---- +gun_http2 <-> gun_tunnel <-> gun_tunnel <-> gun_http +---- + +The `gun_http2` module doesn't do any routing, it just +passes everything to the `gun_tunnel` module. The `gun_tunnel` +module will then do the routing, which involves removing +`StreamA` from `[StreamA, StreamB]` where appropriate. + +The `gun_tunnel` module also receives the TLS encoded/decoded +data and forwards it appropriately. When it comes to sending +data, it will return a `send` command that allows the previous +module to continue sending the data. The `gun_http2` module +will ultimately wrap the data to be sent in a DATA frame and +send it to the "inner socket". diff --git a/gun/doc/src/guide/introduction.asciidoc b/gun/doc/src/guide/introduction.asciidoc new file mode 100644 index 0000000..948dde9 --- /dev/null +++ b/gun/doc/src/guide/introduction.asciidoc @@ -0,0 +1,49 @@ +[[introduction]] +== Introduction + +Gun is an HTTP client for Erlang/OTP. + +Gun supports the HTTP/2, HTTP/1.1 and Websocket protocols. + +=== Prerequisites + +Knowledge of Erlang, but also of the HTTP/1.1, HTTP/2 and Websocket +protocols is required in order to read this guide. + +=== Supported platforms + +Gun is tested and supported on Linux, FreeBSD, Windows and OSX. + +Gun is developed for Erlang/OTP 22.0 and newer. + +=== License + +Gun uses the ISC License. + +---- +Copyright (c) 2013-2023, Loïc Hoguin + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +---- + +=== Versioning + +Gun uses http://semver.org/[Semantic Versioning 2.0.0]. + +=== Conventions + +In the HTTP protocol, the method name is case sensitive. All standard +method names are uppercase. + +Header names are case insensitive. Gun converts all the header names +to lowercase, including request headers provided by your application. diff --git a/gun/doc/src/guide/migrating_from_1.0.asciidoc b/gun/doc/src/guide/migrating_from_1.0.asciidoc new file mode 100644 index 0000000..3a11cfd --- /dev/null +++ b/gun/doc/src/guide/migrating_from_1.0.asciidoc @@ -0,0 +1,21 @@ +[appendix] +== Migrating from Gun 1.0 to 1.1 + +Gun 1.1 updates the Cowlib dependency to 2.5.1 and fixes a +few problems with experimental features. + +=== Features added + +* Update Cowlib to 2.5.1 + +=== Bugs fixed + +* A bug in the experimental `gun_sse_h` where lone id lines + were not propagated has been fixed by updating the Cowlib + dependency. + +* The status code was incorrectly given to the experimental + content handlers as a binary. It has been fixed an an + integer is now given as was intended. + +* A number of Dialyzer warnings have been fixed. diff --git a/gun/doc/src/guide/migrating_from_1.1.asciidoc b/gun/doc/src/guide/migrating_from_1.1.asciidoc new file mode 100644 index 0000000..7e0acf9 --- /dev/null +++ b/gun/doc/src/guide/migrating_from_1.1.asciidoc @@ -0,0 +1,28 @@ +[appendix] +== Migrating from Gun 1.1 to 1.2 + +Gun 1.2 adds support for the CONNECT request over HTTP/1.1 +connections. + +=== Features added + +* CONNECT requests can now be issued on HTTP/1.1 connections. + The tunneled connection can use any of the protocols Gun + supports: HTTP/1.1, HTTP/2 and Websocket over both TCP and + TLS transports. Note that Gun currently does not support + tunneling a TLS connection over a TLS connection due to + limitations in Erlang/OTP. + +* Gun supports sending multiple CONNECT requests, allowing + the tunnel to the origin server to go through multiple + proxies. + +* Gun supports sending CONNECT requests with authorization + credentials using the Basic authentication mechanism. + +* Update Cowlib to 2.6.0 + +=== Functions added + +* The functions `gun:connect/2,3,4` have been added. They can + be used to initiate CONNECT requests on HTTP/1.1 connections. diff --git a/gun/doc/src/guide/migrating_from_1.2.asciidoc b/gun/doc/src/guide/migrating_from_1.2.asciidoc new file mode 100644 index 0000000..3b31092 --- /dev/null +++ b/gun/doc/src/guide/migrating_from_1.2.asciidoc @@ -0,0 +1,39 @@ +[appendix] +== Migrating from Gun 1.2 to 1.3 + +Gun 1.3 improves the support for CONNECT requests +introduced in the previous version and documents +Websocket protocol negotiation. + +=== Features added + +* The `protocols` CONNECT destination option has been added + as a replacement for the now deprecated `protocol` option. + +* Add built-in support for Websocket protocol negotiation + through the Websocket option `protocols`. The interface + of the handler module currently remains undocumented and + must be set to `gun_ws_h`. + +* Add the h2specd HTTP/2 test suite from the h2spec project. + +=== Bugs fixed + +* Fix connecting to HTTP/2 over TLS origin servers via + HTTP/1.1 CONNECT proxies. + +* Do not send the HTTP/1.1 keepalive while waiting for + a response to a CONNECT request. + +* Do not crash on HTTP/2 HEADERS frames with the + PRIORITY flag set. + +* Do not crash on HTTP/2 HEADERS frames when the + END_HEADERS flag is not set. + +* Do not crash on unknown HTTP/2 frame types. + +* Reject HTTP/2 WINDOW_UPDATE frames when they would + cause the window to overflow. + +* Send a GOAWAY frame on closing the HTTP/2 connection. diff --git a/gun/doc/src/guide/migrating_from_1.3.asciidoc b/gun/doc/src/guide/migrating_from_1.3.asciidoc new file mode 100644 index 0000000..59f3381 --- /dev/null +++ b/gun/doc/src/guide/migrating_from_1.3.asciidoc @@ -0,0 +1,329 @@ +[appendix] +== Migrating from Gun 1.3 to 2.0 + +Gun 2.0 includes state of the art tunnel support. With +Gun 2.0 it is possible to make requests or data go through +any number of proxy endpoints using any combination of +TCP or TLS transports and HTTP/1.1, HTTP/2 or SOCKS5 +protocols. All combinations of the scenario Proxy1 -> +Proxy2 -> Origin are tested and known to work. + +Gun 2.0 adds many more features such as Websocket over +HTTP/2, a built-in cookie store, graceful shutdown, flow +control for data messages, event handlers and more. + +Gun 2.0 also introduces an experimental pool module that +automatically maintains connections and routes requests +to the right process, in a similar way as browsers do. + +Gun 2.0 greatly improves the HTTP/2 performance when it +comes to receiving large response bodies; and when receiving +response bodies from many separate requests concurrently. + +Gun now shares much of its HTTP/2 code with Cowboy, +including the HTTP/2 state machine. Numerous issues were +fixed as a result because the Cowboy implementation was +much more advanced. + +The Gun connection process is now implemented using `gen_statem`. + +Gun 2.0 requires Erlang/OTP 22.0 or greater. + +=== Features added + +* Cookie store support has been added. The `cookie_store` + option allows configuring the cookie store backend. + The `gun_cookies` module provides functions to help + implementing such a backend. Gun comes with the backend + `gun_cookies_list` which provides a per-connection, + non-persistent cookie store. The cookie store engine + implements the entire RFC6265bis draft algorithms except + the parts about non-HTTP cookies as no such interface is + provided; and the parts about SameSite as Gun has no + concept of "browsing context". + +* Graceful shutdown has been implemented. Graceful shutdown + can be initiated on the client side by calling the new + function `gun:shutdown/1` or when the owner process goes + away; or on the peer side via the connection: close HTTP/1.1 + header, the HTTP/2 GOAWAY frame or the Websocket close frame. + Gun will try to complete existing streams when possible; + other streams get canceled immediately. The `closing_timeout` + option controls how long we are willing to wait at most + before closing the connection. + +* Gun will better detect connection failures by checking the + return value from sending data to the socket. This applies + to all supported protocols. In addition, Gun now enables + `send_timeout_close` with a `send_timeout` value defaulting + to 15s. + +* Flow control has been added. It allows limiting the number + of data/Websocket messages Gun sends to the calling process. + Gun will stop reading from the socket or stop updating the + protocol's flow control window when applicable as well, to + apply some backpressure to the remote endpoint(s). It is + disabled by default and can be applied on a per-request + basis if necessary. + +* An event handler interface has been added providing access + to many internal Gun events. This can be used for a variety + of purposes including logging, tracing or otherwise + instrumenting a Gun connection. + +* In order to get separate events when connecting, the domain + lookup, connection and TLS handshakes are now performed + separately by Gun. As a result, there exists three separate + timeout options for each of the steps, and the transport + options had to be split into `tcp_opts` and `tls_opts`. + +* Gun now supports connecting through SOCKS proxies, + including secure SOCKS proxies. Both unauthenticated + and username/password authentication are supported. + +* Gun can connect through any number of HTTP, HTTPS, SOCKS + or secure SOCKS proxies, including SOCKS proxies + located after HTTP(S) proxies. The ultimate endpoint + may be using any protocol, including plain TCP, TLS, + HTTP/1.1 or HTTP/2. + +* When specifying which protocols to use, options can + now be provided specific to those protocols. It is + now possible to have separate HTTP options for an + HTTP proxy and the origin HTTP server, for example. + See the new `gun:protocols()` type for details. + +* Gun can now be used to send and receive raw data, + as if it was just a normal socket. This can be + useful when needing to connect through a number + of HTTP/Socks proxies, allowing the use of Gun's + great proxying capabilities (including TLS over TLS) + for any sort of protocols. This can also be useful + when performing HTTP/1.1 Upgrade to custom protocols. + +* Headers can now be provided as a map. + +* Header names may now be provided as binary, string or atom. + +* Gun now automatically lowercases provided header names. + +* Many HTTP/2 options have been added, allowing great + control over how Gun and the remote endpoint are + using the HTTP/2 connection. They can be used to + improve performance or lower the memory usage, for + example. + +* A new `keepalive_tolerance` option for HTTP/2 enables + closing the connection automatically when ping acks + are not received in a timely manner. It nicely + complements the `keepalive` option that makes Gun + send pings. + +* Gun now supports Websocket subprotocol negotiation + and the feature is fully documented and tested. + This can be used to create handlers that will + implement a protocol from within the Gun process itself. + The negotiation is enabled by setting the `protocols` + setting. The `default_protocol` and `user_opts` + settings are also useful. + +* It is now possible to send many Websocket frames in + a single `gun:ws_send/3` call. + +* Gun may now send Websocket ping frames automatically + at intervals determined by the `keepalive` option. It + is disabled by default. + +* A new `silence_pings` option can be set to `false` to + receive all ping and pong frames when using Websocket. + They are typically not needed and therefore silent by + default. + +* The `reply_to` option has been added to `gun:ws_upgrade/4`. + The option applies to both the response and subsequent + Websocket frames. + +* The `reply_to` option is also propagated to messages + following a CONNECT request when the protocol requested + is not HTTP. + +* A new option `retry_fun` can be used to implement + different backoff strategies when reconnecting. + +* A new option `supervise` can be used to start a Gun + connection without using Gun's supervisor. It defaults + to `true`. + +* Many improvements have been done to postpone or reject + requests and other operations while in the wrong state + (for example during state transitions when switching + protocols or connecting to proxies). + +* Update Cowlib to 2.12.0. + +=== Experimental features added + +* The `gun_pool` module was introduced. Its interface + is very similar to the `gun` module, but as it is an + experimental feature, it has not been documented yet. + The intent is to obtain feedback and document it in + an upcoming minor release. Pools are created for each + authority (host/port) and scope (user-defined value) + pairs and are resolved accordingly using the information + provided in the request and request options. Connections + may concurrently handle multiple requests/responses + from as many different processes as required. + +=== Features removed + +* Gun used to reject operations by processes that were not + the owner of the connection. This behavior has been removed. + In general the caller of a request or other operation will + receive the relevant messages unless the `reply_to` option + is used. + +* The `connect_destination()` option `protocol` has been + removed. It was previously deprecated in favor of `protocols`. + +* The `keepalive` timeout is now disabled by default + for HTTP/1.1 and HTTP/2. To be perfectly clear, this + is unrelated to the HTTP/1.1 keep-alive mechanism. + +=== Functions added + +* The function `gun:set_owner/2` has been added. It allows + changing the owner of a connection process. Only the current + owner can do this operation. + +* The function `gun:shutdown/1` has been added. It initiates + the graceful shutdown of the connection, followed by the + termination of the Gun process. + +* The function `gun:stream_info/2` has been added. It provides + information about a specific HTTP stream. + +=== Functions modified + +* The function `gun:info/1` now returns the owner of the + connection as well as the cookie store. + +* The functions `gun:await/2,3,4`, `gun:await_body/2,3,4` and + `gun:await_up/1,2,3` now distinguish the error types. They + can be a timeout, a connection error, a stream error or a + down error (when the Gun process exited while waiting). + +* The functions `gun:await/2,3,4` will now receive upgrades, + tunnel up and Websocket messages and return them. + +* Requests may now include the `tunnel` option to send the + request on a specific tunnel. + +* The functions `gun:request/4,5,6` have been replaced with + `gun:headers/4,5` and `gun:request/5,6`. This provides a + cleaner separation between requests that are followed by + a body and those that aren't. + +* The function `gun:ws_send/2` has been replaced with the + function `gun:ws_send/3`. The stream reference for the + corresponding Websocket upgrade request must now be given. + +=== Messages added + +* The `gun_tunnel_up` message has been added. + +=== Messages modified + +* The `gun_down` message no longer has its final element + documented as `UnprocessedStreams`. It never worked and + was always an empty list. + +=== Bugs fixed + +* *POTENTIAL SECURITY VULNERABILITY*: Fix transfer-encoding + precedence over content-length in responses. This bug may + contribute to a response smuggling security vulnerability + when Gun is used inside a proxy. + +* Gun will now better detect connection closes in some cases. + +* Gun will no longer send duplicate connection-wide `gun_error` + messages to the same process. + +* Gun no longer crashes when trying to upgrade to Websocket + over a connection restricted to HTTP/1.0. + +* The default value for the preferred protocols when using + CONNECT over TLS has been corrected. It was mistakenly not + enabling HTTP/2. + +* Protocol options provided for a tunnel destination were + sometimes ignored. This should no longer be the case. + +* Gun will no longer send an empty HTTP/2 DATA frame when + there is no request body. It was not necessary. + +* Gun will no longer error out when the owner process exits. + The error reason will now be a `shutdown` tuple instead. + +* The host header was set incorrectly during Websocket upgrades + when the host was configured with an IP address, resulting + in a crash. This has been corrected. + +* A completed stream could be found in the `gun_down` message when + the response contained a connection: close header. This is no + longer the case. + +* Hostnames can now be provided as atom as stated by the + documentation. + +* Gun will no longer attempt to send empty data chunks. When + using HTTP/1.1 chunked transfer-encoding this caused the + request body to end, even when `nofin` was given. + +* Gun now always retries connecting immediately when the + connection goes down. + +* The default port number for the HTTP and HTTPS schemes is + no longer sent in the host header. + +* An invalid stream reference was sent on failed Websocket + upgrade responses. This has been corrected. + +* HTTP/2 connection preface errors are now properly detected + and propagated in the `gun_down` message to the connection + owner as well as the exit reason of the Gun process. + +* HTTP/2 connection preface errors now provide a different + human readable error when the data received looks like an + HTTP/1.x response. + +* HTTP/2 connection errors were missing the human readable + reason in the `gun_error` message. This has been corrected. + +* Fix the host and :authority (pseudo-)headers when connecting + to an IPv6 address given as a tuple. They were lacking the + surrounding brackets. + +* Fix a crash in gun:info/1 when the socket was closed before + we call Transport:sockname/1. + +* Fix flushing by stream reference. When the `gun_inform` + message was flushed the function would switch to flushing + all messages from the pid instead of only messages from + the given stream. + +* Allow setting a custom SNI value. + +* Fix double sending of last chunk in HTTP/1.1 when Gun is + asked to send empty data before closing the stream. + +* Gun will now properly ignore parameters when the media + type is text/event-stream. + +* Avoid noisy crashes in the TLS over TLS code. + +* Gun will now include the StreamRef of Websocket streams + when sending `gun_down` messages. + +* Gun will no longer reject HTTP proxies that use HTTP/1.0 + for the version in their response. diff --git a/gun/doc/src/guide/migrating_from_2.0.asciidoc b/gun/doc/src/guide/migrating_from_2.0.asciidoc new file mode 100644 index 0000000..cfd64a8 --- /dev/null +++ b/gun/doc/src/guide/migrating_from_2.0.asciidoc @@ -0,0 +1,25 @@ +[appendix] +== Migrating from Gun 2.0 to 2.1 + +Gun 2.1 contains a small security improvement for +the HTTP/2 protocol, as well as includes a small +number of fixes and improvements. + +Gun 2.1 requires Erlang/OTP 22.0 or greater. + +=== Features added + +* A new HTTP/2 option `max_fragmented_header_block_size` has + been added to limit the size of header blocks that are + sent over multiple HEADERS and CONTINUATION frames. + +* Update Cowlib to 2.13.0. + +=== Bugs fixed + +* Gun will no longer configure the NPN TLS extension, + which has long been replaced by ALPN. NPN is not + compatible with TLS 1.3. + +* Gun will no longer crash when TLS connections close + very early in the connection's life time. diff --git a/gun/doc/src/guide/protocols.asciidoc b/gun/doc/src/guide/protocols.asciidoc new file mode 100644 index 0000000..cd6de2c --- /dev/null +++ b/gun/doc/src/guide/protocols.asciidoc @@ -0,0 +1,120 @@ +[[protocols]] +== Supported protocols + +This chapter describes the protocols supported and the +operations available to them. + +=== HTTP/1.1 + +HTTP/1.1 is a text request-response protocol. The client +sends a request, the server sends back a response. + +Gun provides convenience functions for performing GET, HEAD, +OPTIONS, POST, PATCH, PUT, and DELETE requests. All these +functions are aliases of `gun:headers/4,5` or `gun:request/5,6` +for the respective methods. Gun also provides a `gun:data/4` +function for streaming the request body. + +Gun will send a `gun_inform` message for every intermediate +informational responses received. They will always be sent +before the `gun_response` message. + +Gun will send a `gun_response` message for every response +received, followed by zero or more `gun_data` messages for +the response body, which is optionally terminated by a +`gun_trailers` message. If something goes wrong, a `gun_error` +will be sent instead. + +Gun provides convenience functions for dealing with messages. +The `gun:await/2,3,4` function waits for a response to the given +request, and the `gun:await_body/2,3,4` function for the +response body. The `gun:flush/1` function can be used to clear all +messages related to a request or a connection from the mailbox +of the calling process. + +The function `gun:cancel/2` can be used to silence the +response to a request previously sent if it is no longer +needed. When using HTTP/1.1 there is no multiplexing so +Gun will have to receive the response fully before any +other responses can be received. + +Finally, Gun can upgrade an HTTP/1.1 connection to Websocket. +It provides the `gun:ws_upgrade/2,3,4` function for that +purpose. A `gun_upgrade` message will be sent on success; +a `gun_response` message otherwise. + +=== HTTP/2 + +HTTP/2 is a binary protocol based on HTTP, compatible with +the HTTP semantics, that reduces the complexity of parsing +requests and responses, compresses the HTTP headers and +allows the server to push additional resources along with +the normal response to the original request. + +The HTTP/2 interface is very similar to HTTP/1.1, so this +section instead focuses on the differences in the interface +for the two protocols. + +Gun will send `gun_push` messages for every push received. +They will always be sent before the `gun_response` message. +They can be ignored safely if they are not needed, or they +can be canceled. + +The `gun:cancel/2` function will use the HTTP/2 stream +cancellation mechanism which allows Gun to inform the +server to stop sending a response for this particular +request, saving resources. + +=== Websocket + +Websocket is a binary protocol built on top of HTTP that +allows asynchronous concurrent communication between the +client and the server. A Websocket server can push data to +the client at any time. + +Once the Websocket connection is established over an HTTP/1.1 +connection, the only operation available on this connection +is sending Websocket frames using `gun:ws_send/3`. + +Gun will send a `gun_ws` message for every frame received. + +=== Summary + +The two following tables summarize the supported operations +and the messages Gun sends depending on the connection's +current protocol. + +.Supported operations per protocol +[cols="<,3*^",options="header"] +|=== +| Operation | HTTP/1.1 | HTTP/2 | Websocket +| delete | yes | yes | no +| get | yes | yes | no +| head | yes | yes | no +| options | yes | yes | no +| patch | yes | yes | no +| post | yes | yes | no +| put | yes | yes | no +| request | yes | yes | no +| data | yes | yes | no +| await | yes | yes | no +| await_body | yes | yes | no +| flush | yes | yes | no +| cancel | yes | yes | no +| ws_upgrade | yes | yes | no +| ws_send | no | no | yes +|=== + +.Messages sent per protocol +[cols="<,3*^",options="header"] +|=== +| Message | HTTP/1.1 | HTTP/2 | Websocket +| gun_push | no | yes | no +| gun_inform | yes | yes | no +| gun_response | yes | yes | no +| gun_data | yes | yes | no +| gun_trailers | yes | yes | no +| gun_error | yes | yes | yes +| gun_upgrade | yes | yes | no +| gun_ws | no | no | yes +|=== diff --git a/gun/doc/src/guide/start.asciidoc b/gun/doc/src/guide/start.asciidoc new file mode 100644 index 0000000..09720dc --- /dev/null +++ b/gun/doc/src/guide/start.asciidoc @@ -0,0 +1,43 @@ +[[start]] +== Starting and stopping + +This chapter describes how to start and stop the Gun application. + +=== Setting up + +Specify Gun as a dependency to your application in your favorite +build tool. + +With Erlang.mk this is done by adding `gun` to the `DEPS` variable +in your Makefile. + +.Adding Gun as an Erlang.mk dependency +[source,make] +---- +DEPS = gun +---- + +=== Starting + +Gun is an _OTP application_. It needs to be started before you can +use it. + +.Starting Gun in an Erlang shell +[source,erlang] +---- +1> application:ensure_all_started(gun). +{ok,[crypto,cowlib,asn1,public_key,ssl,gun]} +---- + +=== Stopping + +You can stop Gun using the `application:stop/1` function, however +only Gun will be stopped. This is the reverse of `application:start/1`. +The `application_ensure_all_started/1` function has no equivalent for +stopping all applications. + +.Stopping Gun +[source,erlang] +---- +application:stop(gun). +---- diff --git a/gun/doc/src/guide/websocket.asciidoc b/gun/doc/src/guide/websocket.asciidoc new file mode 100644 index 0000000..ba06e2c --- /dev/null +++ b/gun/doc/src/guide/websocket.asciidoc @@ -0,0 +1,124 @@ +[[websocket]] +== Websocket + +This chapter describes how to use the Gun client for +communicating with a Websocket server. + +// @todo recovering from connection failure, reconnecting to Websocket etc. + +=== HTTP upgrade + +Websocket is a protocol built on top of HTTP. To use Websocket, +you must first request for the connection to be upgraded. Only +HTTP/1.1 connections can be upgraded to Websocket, so you might +need to restrict the protocol to HTTP/1.1 if you are planning +to use Websocket over TLS. + +You must use the `gun:ws_upgrade/2,3,4` function to upgrade +to Websocket. This function can be called anytime after connection, +so you can send HTTP requests before upgrading to Websocket. + +.Upgrade to Websocket +[source,erlang] +---- +gun:ws_upgrade(ConnPid, "/websocket"). +---- + +Gun will set all the necessary headers for performing the +Websocket upgrade, but you can specify additional headers +if needed. For example you can authenticate. + +.Upgrade to Websocket using HTTP authentication +[source,erlang] +---- +gun:ws_upgrade(ConnPid, "/websocket", [ + {<<"authorization">>, "Basic dXNlcm5hbWU6cGFzc3dvcmQ="} +]). +---- + +You can pass the Websocket options as part of the `gun:open/2,3` +call when opening the connection, or using the `gun:ws_upgrade/4`. +The fourth argument is those same options. + +Gun can negotiate the protocol to be used for the Websocket +connection. The `protocols` option can be given with a list +of protocols accepted and the corresponding handler module. +Note that the interface for handler modules is currently +undocumented and must be set to `gun_ws_h`. + +.Upgrade to Websocket with protocol negotiation +[source,erlang] +---- +StreamRef = gun:ws_upgrade(ConnPid, "/websocket", [] + #{protocols => [{<<"xmpp">>, gun_ws_h}]}). +---- + +The upgrade will fail if the server cannot satisfy the +protocol negotiation. + +When the upgrade succeeds, a `gun_upgrade` message is sent. +If the server does not understand Websocket or refused the +upgrade, a `gun_response` message is sent. If Gun couldn't +perform the upgrade due to an error (for example attempting +to upgrade to Websocket on an HTTP/1.0 connection) then a +`gun_error` message is sent. + +When the server does not understand Websocket, it may send +a meaningful response which should be processed. In the +following example we however ignore it: + +[source,erlang] +---- +receive + {gun_upgrade, ConnPid, StreamRef, [<<"websocket">>], Headers} -> + upgrade_success(ConnPid, StreamRef); + {gun_response, ConnPid, _, _, Status, Headers} -> + exit({ws_upgrade_failed, Status, Headers}); + {gun_error, ConnPid, StreamRef, Reason} -> + exit({ws_upgrade_failed, Reason}) + %% More clauses here as needed. +after 1000 -> + exit(timeout) +end. +---- + +=== Sending data + +Once the Websocket upgrade has completed successfully, you no +longer have access to functions for performing requests. You +can only send and receive Websocket messages. + +Use `gun:ws_send/3` to send messages to the server. + +.Send a text frame +[source,erlang] +---- +gun:ws_send(ConnPid, StreamRef, {text, "Hello!"}). +---- + +.Send a text frame, a binary frame and then close the connection +[source,erlang] +---- +gun:ws_send(ConnPid, StreamRef, [ + {text, "Hello!"}, + {binary, BinaryValue}, + close +]). +---- + +Note that if you send a close frame, Gun will close the connection +cleanly but may attempt to reconnect afterwards depending on the +`retry` configuration. + +=== Receiving data + +Gun sends an Erlang message to the owner process for every +Websocket message it receives. + +[source,erlang] +---- +receive + {gun_ws, ConnPid, StreamRef, Frame} -> + handle_frame(ConnPid, StreamRef, Frame) +end. +---- diff --git a/gun/doc/src/manual/gun.asciidoc b/gun/doc/src/manual/gun.asciidoc new file mode 100644 index 0000000..60f217a --- /dev/null +++ b/gun/doc/src/manual/gun.asciidoc @@ -0,0 +1,639 @@ += gun(3) + +== Name + +gun - Asynchronous HTTP client + +== Description + +The `gun` module provides an asynchronous interface for +connecting and communicating with Web servers over HTTP, +HTTP/2 or Websocket. + +== Exports + +Connection: + +* link:man:gun:open(3)[gun:open(3)] - Open a connection to the given host and port +* link:man:gun:open_unix(3)[gun:open_unix(3)] - Open a connection to the given Unix domain socket +* link:man:gun:set_owner(3)[gun:set_owner(3)] - Set a new owner for the connection +* link:man:gun:shutdown(3)[gun:shutdown(3)] - Gracefully close the connection +* link:man:gun:close(3)[gun:close(3)] - Brutally close the connection +* link:man:gun:info(3)[gun:info(3)] - Obtain information about the connection + +Requests: + +* link:man:gun:get(3)[gun:get(3)] - Get a resource representation +* link:man:gun:head(3)[gun:head(3)] - Get headers of a resource representation +* link:man:gun:options(3)[gun:options(3)] - Query the capabilities of the server or a resource +* link:man:gun:patch(3)[gun:patch(3)] - Apply a set of changes to a resource +* link:man:gun:post(3)[gun:post(3)] - Process the enclosed representation according to a resource's own semantics +* link:man:gun:put(3)[gun:put(3)] - Create or replace a resource +* link:man:gun:delete(3)[gun:delete(3)] - Delete a resource +* link:man:gun:headers(3)[gun:headers(3)] - Initiate the given request +* link:man:gun:request(3)[gun:request(3)] - Perform the given request +* link:man:gun:data(3)[gun:data(3)] - Stream the body of a request + +Proxies: + +* link:man:gun:connect(3)[gun:connect(3)] - Establish a tunnel to the origin server + +Messages: + +* link:man:gun:await(3)[gun:await(3)] - Wait for a response +* link:man:gun:await_body(3)[gun:await_body(3)] - Wait for the complete response body +* link:man:gun:await_up(3)[gun:await_up(3)] - Wait for the connection to be up +* link:man:gun:flush(3)[gun:flush(3)] - Flush all messages related to a connection or a stream + +Streams: + +* link:man:gun:update_flow(3)[gun:update_flow(3)] - Update a stream's flow control value +* link:man:gun:cancel(3)[gun:cancel(3)] - Cancel the given stream +* link:man:gun:stream_info(3)[gun:stream_info(3)] - Obtain information about a stream + +Websocket: + +* link:man:gun:ws_upgrade(3)[gun:ws_upgrade(3)] - Upgrade to Websocket +* link:man:gun:ws_send(3)[gun:ws_send(3)] - Send Websocket frames + +== Messages + +Gun will inform the calling process of events asynchronously +by sending any of the following messages: + +Connection: + +* link:man:gun_up(3)[gun_up(3)] - The connection is up +* link:man:gun_tunnel_up(3)[gun_tunnel_up(3)] - The tunnel is up +* link:man:gun_down(3)[gun_down(3)] - The connection is down +* link:man:gun_upgrade(3)[gun_upgrade(3)] - Successful protocol upgrade +* link:man:gun_error(3)[gun_error(3)] - Stream or connection-wide error + +Responses: + +* link:man:gun_push(3)[gun_push(3)] - Server-initiated push +* link:man:gun_inform(3)[gun_inform(3)] - Informational response +* link:man:gun_response(3)[gun_response(3)] - Response +* link:man:gun_data(3)[gun_data(3)] - Response body +* link:man:gun_trailers(3)[gun_trailers(3)] - Response trailers + +Websocket: + +* link:man:gun_ws(3)[gun_ws(3)] - Websocket frame + +The response messages will be sent to the process that opened +the connection by default. The `reply_to` request option can +be used to redirect request-specific messages to a different +process. + +== Types + +=== connect_destination() + +[source,erlang] +---- +connect_destination() :: #{ + host := inet:hostname() | inet:ip_address(), + port := inet:port_number(), + + username => iodata(), + password => iodata(), + protocols => protocols(), + transport => tcp | tls, + + tls_opts => [ssl:tls_client_option()], + tls_handshake_timeout => timeout() +} +---- + +Destination of a CONNECT request. + +The default value, if any, is given next to the option name: + +host, port:: + +Destination hostname and port number. Mandatory. ++ +Upon successful completion of the CONNECT request, Gun will +begin using these as the host and port of the origin server +for subsequent requests. + +username, password:: + +Proxy authorization credentials. They are only sent when +both options are provided. + +protocols - see below:: + +Ordered list of preferred protocols. Please refer to the +`protocols()` type documentation for details. ++ +Defaults to `[http]` when the transport is `tcp`, +and `[http2, http]` when the transport is `tls`. + +transport (tcp):: + +Transport that will be used for tunneled requests. + +tls_opts ([]):: + +Options to use for tunneled TLS connections. + +tls_handshake_timeout (infinity):: + +Handshake timeout for tunneled TLS connections. + +=== http_opts() + +[source,erlang] +---- +http_opts() :: #{ + closing_timeout => timeout(), + cookie_ignore_informational => boolean(), + flow => pos_integer(), + keepalive => timeout(), + transform_header_name => fun((binary()) -> binary()), + version => 'HTTP/1.1' | 'HTTP/1.0' +} +---- + +Configuration for the HTTP protocol. + +The default value is given next to the option name: + +// @todo Document content_handlers and gun_sse_h. + +closing_timeout (15000):: + +Time to wait before brutally closing the connection when a +graceful shutdown was requested via a call to +link:man:gun:shutdown(3)[gun:shutdown(3)]. + +cookie_ignore_informational (false):: + +Whether cookies received inside informational responses +(1xx status code) must be ignored. + +flow - see below:: + +The initial flow control value for all HTTP/1.1 streams. +By default flow control is disabled. + +keepalive (infinity):: + +Time between pings in milliseconds. Since the HTTP protocol has +no standardized way to ping the server, Gun will simply send an +empty line when the connection is idle. Gun only makes a best +effort here as servers usually have configurable limits to drop +idle connections. Disabled by default due to potential +incompatibilities. + +transform_header_name - see below:: + +A function that will be applied to all header names before they +are sent to the server. Gun assumes that all header names are in +lower case. This function is useful if you, for example, need to +re-case header names in the event that the server incorrectly +considers the case of header names to be significant. + +version (`'HTTP/1.1'`):: + +HTTP version to use. + +=== http2_opts() + +[source,erlang] +---- +http2_opts() :: #{ + closing_timeout => timeout(), + cookie_ignore_informational => boolean(), + flow => pos_integer(), + keepalive => timeout(), + keepalive_tolerance => non_neg_integer(), + + %% HTTP/2 state machine configuration. + connection_window_margin_size => 0..16#7fffffff, + connection_window_update_threshold => 0..16#7fffffff, + enable_connect_protocol => boolean(), + initial_connection_window_size => 65535..16#7fffffff, + initial_stream_window_size => 0..16#7fffffff, + max_concurrent_streams => non_neg_integer() | infinity, + max_connection_window_size => 0..16#7fffffff, + max_decode_table_size => non_neg_integer(), + max_encode_table_size => non_neg_integer(), + max_fragmented_header_block_size => 16384..16#7fffffff, + max_frame_size_received => 16384..16777215, + max_frame_size_sent => 16384..16777215 | infinity, + max_stream_window_size => 0..16#7fffffff, + preface_timeout => timeout(), + settings_timeout => timeout(), + stream_window_data_threshold => 0..16#7fffffff, + stream_window_margin_size => 0..16#7fffffff, + stream_window_update_threshold => 0..16#7fffffff +} +---- + +Configuration for the HTTP/2 protocol. + +The default value is given next to the option name: + +// @todo Document content_handlers and gun_sse_h. + +closing_timeout (15000):: + +Time to wait before brutally closing the connection when a +graceful shutdown was requested either via a call to +link:man:gun:shutdown(3)[gun:shutdown(3)] or by the server. + +cookie_ignore_informational (false):: + +Whether cookies received inside informational responses +(1xx status code) must be ignored. + +flow - see below:: + +The initial flow control value for all HTTP/2 streams. +By default flow control is disabled. + +keepalive (infinity):: + +Time between pings in milliseconds. + +keepalive_tolerance - see below:: + +The number of unacknowledged pings in flight that are +tolerated before the connection is closed. By default +this mechanism is disabled even if `keepalive` is +enabled. + +=== opts() + +[source,erlang] +---- +opts() :: #{ + connect_timeout => timeout(), + cookie_store => gun_cookies:store(), + domain_lookup_timeout => timeout(), + http_opts => http_opts(), + http2_opts => http2_opts(), + protocols => protocols(), + retry => non_neg_integer(), + retry_fun => fun(), + retry_timeout => pos_integer(), + supervise => boolean(), + tcp_opts => [gen_tcp:connect_option()], + tls_handshake_timeout => timeout(), + tls_opts => [ssl:tls_client_option()], + trace => boolean(), + transport => tcp | tls, + ws_opts => ws_opts() +} +---- + +Configuration for the connection. + +The default value is given next to the option name: + +connect_timeout (infinity):: + +Connection timeout. + +cookie_store - see below:: + +The cookie store that Gun will use for this connection. +When configured, Gun will query the store for cookies +and include them in the request headers; and add cookies +found in response headers to the store. ++ +By default no cookie store will be used. + +domain_lookup_timeout (infinity):: + +Domain lookup timeout. + +http_opts (#{}):: + +Options specific to the HTTP protocol. + +http2_opts (#{}):: + +Options specific to the HTTP/2 protocol. + +protocols - see below:: + +Ordered list of preferred protocols. Please refer to the +`protocols()` type documentation for details. ++ +Defaults to `[http]` when the transport is `tcp`, +and `[http2, http]` when the transport is `tls`. + +retry (5):: + +Number of times Gun will try to reconnect on failure before giving up. + +retry_fun - see below:: + +A fun that will be called before every reconnect attempt. It receives +the current number of retries left and the Gun options. It returns the +next number of retries left and the timeout to apply before reconnecting. + +The default fun will remove one to the number of retries and set the +timeout to the `retry_timeout` value. + +The fun must be defined as follow: + +[source,erlang] +---- +fun ((non_neg_integer(), opts()) -> #{ + retries => non_neg_integer(), + timeout => pos_integer() +}) +---- + +The fun will never be called when the `retry` option is set to 0. When +this function returns 0 in the `retries` value, Gun will do one last +reconnect attempt before giving up. + +retry_timeout (5000):: + +Time between retries in milliseconds. + +supervise (true):: + +Whether the Gun process should be started under the `gun_sup` +supervisor. Set to `false` to use your own supervisor. + +tcp_opts (DefaultOpts):: + +TCP options used when establishing the connection. +By default Gun enables send timeouts with the options +`[{send_timeout, 15000}, {send_timeout_close, true}]`. + +tls_handshake_timeout (infinity):: + +TLS handshake timeout. + +tls_opts ([]):: + +TLS options used for the TLS handshake after the connection +has been established, when the transport is set to `tls`. + +trace (false):: + +Whether to enable `dbg` tracing of the connection process. Should +only be used during debugging. + +transport - see below:: + +Whether to use TLS or plain TCP. The default varies depending on the +port used. Port 443 defaults to `tls`. All other ports default to `tcp`. + +ws_opts (#{}):: + +Options specific to the Websocket protocol. + +=== protocols() + +[source,erlang] +---- +Protocol :: http | {http, http_opts()} + | http2 | {http2, http2_opts()} + | raw | {raw, raw_opts()} + | socks | {socks, socks_opts()} + +protocols() :: [Protocol] +---- + +Ordered list of preferred protocols. When the transport is `tcp`, +this list must contain exactly one protocol. When the transport +is `tls`, this list must contain at least one protocol and will be +used to negotiate a protocol via ALPN. When the server does not +support ALPN then `http` will be used, except when the list of +preferred protocols is set to only accept `socks`. + +Defaults to `[http]` when the transport is `tcp`, +and `[http2, http]` when the transport is `tls`. + +=== raw_opts() + +[source,erlang] +---- +raw_opts() :: #{ +} +---- + +Configuration for the "raw" protocol. + +// The default value is given next to the option name: + +=== req_headers() + +[source,erlang] +---- +req_headers() :: [{binary() | string() | atom(), iodata()}] + | #{binary() | string() | atom() => iodata()} +---- + +Request headers. + +=== req_opts() + +[source,erlang] +---- +req_opts() :: #{ + flow => pos_integer(), + reply_to => pid() +} +---- + +Configuration for a particular request. + +The default value is given next to the option name: + +flow - see below:: + +The initial flow control value for the stream. By default +flow control is disabled. + +reply_to (`self()`):: + +The pid of the process that will receive the response messages. + +=== socks_opts() + +[source,erlang] +---- +socks_opts() :: #{ + host := inet:hostname() | inet:ip_address(), + port := inet:port_number(), + + auth => [{username_password, binary(), binary()} | none], + protocols => protocols(), + transport => tcp | tls, + version => 5, + + tls_opts => [ssl:tls_client_option()], + tls_handshake_timeout => timeout() +} +---- + +Configuration for the Socks protocol. + +The default value, if any, is given next to the option name: + +host, port:: + +Destination hostname and port number. Mandatory. ++ +Upon successful completion of the Socks connection, Gun will +begin using these as the host and port of the origin server +for subsequent requests. + +auth ([none]):: + +Authentication methods Gun advertises to the Socks proxy. + +protocols - see below:: + +Ordered list of preferred protocols. Please refer to the +`protocols()` type documentation for details. ++ +Defaults to `[http]` when the transport is `tcp`, +and `[http2, http]` when the transport is `tls`. + +transport (tcp):: + +Transport that will be used for tunneled requests. + +tls_opts ([]):: + +Options to use for tunneled TLS connections. + +tls_handshake_timeout (infinity):: + +Handshake timeout for tunneled TLS connections. + +version (5):: + +Version of the Socks protocol to use. + +=== stream_ref() + +[source,erlang] +---- +stream_ref() - see below +---- + +Unique identifier for a stream. + +The exact type is considered to be an implementation +detail. + +=== ws_opts() + +[source,erlang] +---- +ws_opts() :: #{ + closing_timeout => timeout(), + compress => boolean(), + default_protocol => module(), + flow => pos_integer(), + keepalive => timeout(), + protocols => [{binary(), module()}], + silence_pings => boolean(), + user_opts => any() +} +---- + +Configuration for the Websocket protocol. + +The default value is given next to the option name: + +closing_timeout (15000):: + +Time to wait before brutally closing the connection when a +graceful shutdown was requested either via a call to +link:man:gun:shutdown(3)[gun:shutdown(3)] or by the server. + +compress (false):: + +Whether to enable permessage-deflate compression. This does +not guarantee that compression will be used as it is the +server that ultimately decides. Defaults to false. + +default_protocol (gun_ws_h):: + +Default protocol module when no Websocket subprotocol is +negotiated. + +flow - see below:: + +The initial flow control value for the Websocket connection. +By default flow control is disabled. + +keepalive (infinity):: + +Time between pings in milliseconds. + +protocols ([]):: + +A non-empty list enables Websocket protocol negotiation. The +list of protocols will be sent in the sec-websocket-protocol +request header. The given module must follow the +link:man:gun_ws_protocol(3)[gun_ws_protocol(3)] interface. +Gun comes with a default interface in `gun_ws_h` that may +be reused for negotiated protocols. + +silence_pings (true):: + +Whether the ping and pong frames should be sent to the user. +In all cases Gun will automatically send a pong frame back +when receiving a ping. + +user_opts - see below:: + +Additional options that are not in use by Gun unless a custom +Websocket subprotocol is configured and negotiated. +By default no user option is defined. + +== Changelog + +* *2.1*: The HTTP/2 option list was updated with new options. +* *2.0*: The `default_protocol` and `user_opts` Websocket + options were added. +* *2.0*: The `stream_ref()` type was added. +* *2.0*: The option `cookie_store` was added. It can be used + to configure a cookie store that Gun will use + automatically. A related option, `cookie_ignore_informational`, + was added to both `http_opts()` and `http2_opts()`. +* *2.0*: The types `protocols()` and `socks_opts()` have been + added. Support for the Socks protocol has been added + in every places where protocol selection is available. + In addition it is now possible to specify separate + HTTP options for the CONNECT proxy and the origin server. +* *2.0*: The `connect_timeout` option has been split into + three options: `domain_lookup_timeout`, `connect_timeout` + and when applicable `tls_handshake_timeout`. +* *2.0*: The option `retry_fun` was added. It can be used to + implement different reconnect strategies. +* *2.0*: The `transport_opts` option has been split into + two options: `tcp_opts` and `tls_opts`. +* *2.0*: Function `gun:update_flow/3` introduced. The `flow` + option was added to request options and HTTP/1.1, + HTTP/2 and Websocket options as well. +* *2.0*: Introduce the type `req_headers()` and extend the + types accepted for header names for greater + interoperability. Header names are automatically + lowercased as well. +* *2.0*: Function `gun:headers/4,5` introduced. +* *2.0*: The `keepalive` option is now set to `infinity` by + default for all protocols. This means it is disabled. +* *2.0*: Websocket options `keepalive` and `silence_pings` introduced. +* *2.0*: Remove the `protocol` option from `connect_destination()`. +* *1.3*: Add the CONNECT destination's `protocols` option and + deprecate the previously introduced `protocol` option. +* *1.2*: Introduce the type `connect_destination()`. + +== See also + +link:man:gun(7)[gun(7)] diff --git a/gun/doc/src/manual/gun.await.asciidoc b/gun/doc/src/manual/gun.await.asciidoc new file mode 100644 index 0000000..703ce36 --- /dev/null +++ b/gun/doc/src/manual/gun.await.asciidoc @@ -0,0 +1,122 @@ += gun:await(3) + +== Name + +gun:await - Wait for a response + +== Description + +[source,erlang] +---- +await(ConnPid, StreamRef) + -> await(ConnPid, StreamRef, 5000, MonitorRef) + +await(ConnPid, StreamRef, MonitorRef) + -> await(ConnPid, StreamRef, 5000, MonitorRef) + +await(ConnPid, StreamRef, Timeout) + -> await(ConnPid, StreamRef, Timeout, MonitorRef) + +await(ConnPid, StreamRef, Timeout, MonitorRef) + -> Result + +ConnPid :: pid() +StreamRef :: gun:stream_ref() +MonitorRef :: reference() +Timeout :: timeout() +Result :: tuple() - see below +---- + +Wait for a response. + +This function waits for a message from the given stream and +returns it as a tuple. An error will be returned should the +process fail or a relevant message is not received within +the specified duration. + +== Arguments + +ConnPid:: + +The pid of the Gun connection process. + +StreamRef:: + +Identifier of the stream for the original request. + +Timeout:: + +How long to wait for a message, in milliseconds. + +MonitorRef:: + +Monitor for the Gun connection process. ++ +A monitor is automatically created for the duration of this +call when one is not provided. + +== Return value + +A number of different tuples can be returned. They correspond +to the message of the same name and they contain the same +elements minus the pid and stream reference. Error tuples +may also be returned when a timeout or an error occur. + +[source,erlang] +---- +Result :: {inform, Status, Headers} + | {response, IsFin, Status, Headers} + | {data, IsFin, Data} + | {trailers, Trailers} + | {push, NewStreamRef, Method, URI, Headers} + | {upgrade, Protocols, Headers} + | {ws, Frame} + | {error, Reason} + +Reason :: {stream_error | connection_error | down, any()} + | timeout +---- + +Because the messages and returned tuples are equivalent, +please refer to the manual pages for each message for +further information: + +* link:man:gun_inform(3)[gun_inform(3)] - Informational response +* link:man:gun_response(3)[gun_response(3)] - Response +* link:man:gun_data(3)[gun_data(3)] - Response body +* link:man:gun_trailers(3)[gun_trailers(3)] - Response trailers +* link:man:gun_push(3)[gun_push(3)] - Server-initiated push +* link:man:gun_upgrade(3)[gun_upgrade(3)] - Successful protocol upgrade +* link:man:gun_ws(3)[gun_ws(3)] - Websocket frame + +== Changelog + +* *2.0*: `upgrade` and `ws` tuples can now be returned. +* *2.0*: The error tuple type now includes the type of error. +* *1.0*: Function introduced. + +== Examples + +.Wait for a response +[source,erlang] +---- +StreamRef = gun:get(ConnPid, "/articles", [ + {<<"accept">>, <<"text/html;q=1.0, application/xml;q=0.1">>} +]). +{response, nofin, 200, _Headers} = gun:await(ConnPid, StreamRef). +{data, fin, <<"Hello world!">>} = gun:await(ConnPid, StreamRef). +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:get(3)[gun:get(3)], +link:man:gun:head(3)[gun:head(3)], +link:man:gun:options(3)[gun:options(3)], +link:man:gun:patch(3)[gun:patch(3)], +link:man:gun:post(3)[gun:post(3)], +link:man:gun:put(3)[gun:put(3)], +link:man:gun:delete(3)[gun:delete(3)], +link:man:gun:headers(3)[gun:headers(3)], +link:man:gun:request(3)[gun:request(3)], +link:man:gun:await_body(3)[gun:await_body(3)] diff --git a/gun/doc/src/manual/gun.await_body.asciidoc b/gun/doc/src/manual/gun.await_body.asciidoc new file mode 100644 index 0000000..11f87bc --- /dev/null +++ b/gun/doc/src/manual/gun.await_body.asciidoc @@ -0,0 +1,91 @@ += gun:await_body(3) + +== Name + +gun:await_body - Wait for the complete response body + +== Description + +[source,erlang] +---- +await_body(ConnPid, StreamRef) + -> await_body(ConnPid, StreamRef, 5000, MonitorRef) + +await_body(ConnPid, StreamRef, MonitorRef) + -> await_body(ConnPid, StreamRef, 5000, MonitorRef) + +await_body(ConnPid, StreamRef, Timeout) + -> await_body(ConnPid, StreamRef, Timeout, MonitorRef) + +await_body(ConnPid, StreamRef, Timeout, MonitorRef) + -> {ok, Body} | {ok, Body, Trailers} | {error, Reason} + +ConnPid :: pid() +StreamRef :: gun:stream_ref() +MonitorRef :: reference() +Timeout :: timeout() +Body :: binary() +Trailers :: [{binary(), binary()}] +Reason :: {stream_error | connection_error | down, any()} + | timeout +---- + +Wait for the complete response body. + +== Arguments + +ConnPid:: + +The pid of the Gun connection process. + +StreamRef:: + +Identifier of the stream for the original request. + +Timeout:: + +How long to wait for each message, in milliseconds. + +MonitorRef:: + +Monitor for the Gun connection process. ++ +A monitor is automatically created for the duration of this +call when one is not provided. + +== Return value + +The body is returned, possibly with trailers if the +request contained a `te: trailers` header. Error tuples +may also be returned when a timeout or an error occur. + +== Changelog + +* *2.0*: The error tuple type now includes the type of error. +* *1.0*: Function introduced. + +== Examples + +.Wait for the complete response body +[source,erlang] +---- +StreamRef = gun:get(ConnPid, "/articles", [ + {<<"accept">>, <<"text/html;q=1.0, application/xml;q=0.1">>} +]). +{response, nofin, 200, _Headers} = gun:await(ConnPid, StreamRef). +{ok, _Body} = gun:await_body(ConnPid, StreamRef). +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:get(3)[gun:get(3)], +link:man:gun:head(3)[gun:head(3)], +link:man:gun:options(3)[gun:options(3)], +link:man:gun:patch(3)[gun:patch(3)], +link:man:gun:post(3)[gun:post(3)], +link:man:gun:put(3)[gun:put(3)], +link:man:gun:delete(3)[gun:delete(3)], +link:man:gun:headers(3)[gun:headers(3)], +link:man:gun:request(3)[gun:request(3)], +link:man:gun:await(3)[gun:await(3)] diff --git a/gun/doc/src/manual/gun.await_up.asciidoc b/gun/doc/src/manual/gun.await_up.asciidoc new file mode 100644 index 0000000..ffe39fd --- /dev/null +++ b/gun/doc/src/manual/gun.await_up.asciidoc @@ -0,0 +1,75 @@ += gun:await_up(3) + +== Name + +gun:await_up - Wait for the connection to be up + +== Description + +[source,erlang] +---- +await_up(ConnPid) + -> await_up(ConnPid, 5000, MonitorRef) + +await_up(ConnPid, MonitorRef) + -> await_up(ConnPid, 5000, MonitorRef) + +await_up(ConnPid, Timeout) + -> await_up(ConnPid, Timeout, MonitorRef) + +await_up(ConnPid, Timeout, MonitorRef) + -> {ok, Protocol} | {error, Reason} + +ConnPid :: pid() +MonitorRef :: reference() +Timeout :: timeout() +Protocol :: http | http2 | socks +Reason :: {down, any()} | timeout +---- + +Wait for the connection to be up. + +== Arguments + +ConnPid:: + +The pid of the Gun connection process. + +Timeout:: + +How long to wait for, in milliseconds. + +MonitorRef:: + +Monitor for the Gun connection process. ++ +A monitor is automatically created for the duration of this +call when one is not provided. + +== Return value + +The protocol selected for this connection. It can be used +to determine the capabilities of the server. Error tuples +may also be returned when a timeout or an error occur. + +== Changelog + +* *2.0*: The error tuple type now includes the type of error. +* *1.0*: Function introduced. + +== Examples + +.Wait for the connection to be up +[source,erlang] +---- +{ok, ConnPid} = gun:open("example.org", 443). +{ok, _} = gun:await_up(ConnPid). +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:open(3)[gun:open(3)], +link:man:gun:open_unix(3)[gun:open_unix(3)], +link:man:gun_tunnel_up(3)[gun_tunnel_up(3)], +link:man:gun_up(3)[gun_up(3)] diff --git a/gun/doc/src/manual/gun.cancel.asciidoc b/gun/doc/src/manual/gun.cancel.asciidoc new file mode 100644 index 0000000..9c2e428 --- /dev/null +++ b/gun/doc/src/manual/gun.cancel.asciidoc @@ -0,0 +1,70 @@ += gun:cancel(3) + +== Name + +gun:cancel - Cancel the given stream + +== Description + +[source,erlang] +---- +cancel(ConnPid, StreamRef) -> ok + +ConnPid :: pid() +StreamRef :: gun:stream_ref() +---- + +Cancel the given stream. + +The behavior of this function depends on the protocol +selected. + +HTTP/1.1 does not support this feature. Gun will simply +silence the stream and stop relaying messages. Gun may +also decide to close the connection if the response body +is too large, to avoid wasting time and bandwidth. + +HTTP/2 allows cancelling streams at any time. + +This function is asynchronous. Messages related to this +stream may still be sent after the function returns. + +== Arguments + +ConnPid:: + +The pid of the Gun connection process. + +StreamRef:: + +Identifier of the stream for the original request. + +== Return value + +The atom `ok` is returned. + +== Changelog + +* *1.0*: Function introduced. + +== Examples + +.Cancel a stream +[source,erlang] +---- +gun:cancel(ConnPid, StreamRef). +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:get(3)[gun:get(3)], +link:man:gun:head(3)[gun:head(3)], +link:man:gun:options(3)[gun:options(3)], +link:man:gun:patch(3)[gun:patch(3)], +link:man:gun:post(3)[gun:post(3)], +link:man:gun:put(3)[gun:put(3)], +link:man:gun:delete(3)[gun:delete(3)], +link:man:gun:headers(3)[gun:headers(3)], +link:man:gun:request(3)[gun:request(3)], +link:man:gun:stream_info(3)[gun:stream_info(3)] diff --git a/gun/doc/src/manual/gun.close.asciidoc b/gun/doc/src/manual/gun.close.asciidoc new file mode 100644 index 0000000..cdbe05f --- /dev/null +++ b/gun/doc/src/manual/gun.close.asciidoc @@ -0,0 +1,45 @@ += gun:close(3) + +== Name + +gun:close - Brutally close the connection + +== Description + +[source,erlang] +---- +close(ConnPid) -> ok + +ConnPid :: pid() +---- + +Brutally close the connection. + +== Arguments + +ConnPid:: + +The pid of the Gun connection process. + +== Return value + +The atom `ok` is returned. + +== Changelog + +* *1.0*: Function introduced. + +== Examples + +.Close the connection +[source,erlang] +---- +ok = gun:close(ConnPid). +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:open(3)[gun:open(3)], +link:man:gun:open_unix(3)[gun:open_unix(3)], +link:man:gun:shutdown(3)[gun:shutdown(3)] diff --git a/gun/doc/src/manual/gun.connect.asciidoc b/gun/doc/src/manual/gun.connect.asciidoc new file mode 100644 index 0000000..b2bdfe9 --- /dev/null +++ b/gun/doc/src/manual/gun.connect.asciidoc @@ -0,0 +1,115 @@ += gun:connect(3) + +== Name + +gun:connect - Establish a tunnel to the origin server + +== Description + +[source,erlang] +---- +connect(ConnPid, Destination) + -> connect(ConnPid, Destination, [], #{}). + +connect(ConnPid, Destination, Headers) + -> connect(ConnPid, Destination, Headers, #{}). + +connect(ConnPid, Destination, Headers, ReqOpts) + -> StreamRef + +ConnPid :: pid() +Destination :: gun:connect_destination() +Headers :: gun:req_headers() +ReqOpts :: gun:req_opts() +StreamRef :: gun:stream_ref() +---- + +Establish a tunnel to the origin server. + +This feature is currently only available for HTTP/1.1 connections. +Upon successful completion of the CONNECT request a tunnel is +established and subsequent requests will go through the tunnel. + +Gun will not automatically re-issue the CONNECT request upon +reconnection to the proxy server. The `gun_up` message can +be used to know when the tunnel needs to be established again. + +== Arguments + +ConnPid:: + +The pid of the Gun connection process. + +Destination:: + +Destination of the CONNECT request. + +Headers:: + +Additional request headers. + +ReqOpts:: + +Request options. + +== Return value + +A reference that identifies the newly created stream is +returned. It is this reference that must be passed in +subsequent calls and will be received in messages related +to this new stream. + +== Changelog + +* *1.2*: Function introduced. + +== Examples + +.Establish a tunnel +[source,erlang] +---- +{ok, ConnPid} = gun:open("proxy.example.org", 1080), +{ok, http} = gun:await_up(ConnPid), +StreamRef = gun:connect(ConnPid, #{ + host => "origin-server.example.org", + port => 80 +}), +{response, fin, 200, _} = gun:await(ConnPid, StreamRef), +%% Subsequent requests will be sent to origin-server.example.org. +---- + +.Establish a tunnel for a secure HTTP/2 connection +[source,erlang] +---- +{ok, ConnPid} = gun:open("proxy.example.org", 1080), +{ok, http} = gun:await_up(ConnPid), +StreamRef = gun:connect(ConnPid, #{ + host => "origin-server.example.org", + port => 443, + protocols => [http2], + transport => tls +}), +{response, fin, 200, _} = gun:await(ConnPid, StreamRef), +%% Subsequent requests will be sent to origin-server.example.org. +---- + +.Establish a tunnel using proxy authorization +[source,erlang] +---- +{ok, ConnPid} = gun:open("proxy.example.org", 1080), +{ok, http} = gun:await_up(ConnPid), +StreamRef = gun:connect(ConnPid, #{ + host => "origin-server.example.org", + port => 80, + username => "essen", + password => "myrealpasswordis" +}), +{response, fin, 200, _} = gun:await(ConnPid, StreamRef), +%% Subsequent requests will be sent to origin-server.example.org. +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:await_up(3)[gun:await_up(3)], +link:man:gun_up(3)[gun_up(3)] diff --git a/gun/doc/src/manual/gun.data.asciidoc b/gun/doc/src/manual/gun.data.asciidoc new file mode 100644 index 0000000..44bae36 --- /dev/null +++ b/gun/doc/src/manual/gun.data.asciidoc @@ -0,0 +1,75 @@ += gun:data(3) + +== Name + +gun:data - Stream the body of a request + +== Description + +[source,erlang] +---- +data(ConnPid, StreamRef, IsFin, Data) -> ok + +ConnPid :: pid() +StreamRef :: gun:stream_ref() +IsFin :: fin | nofin +Data :: iodata() +---- + +Stream the body of a request. + +This function can only be used if the original request +had headers indicating that a body would be streamed. + +All calls to this function must use the `nofin` flag +except for the last which must use `fin` to indicate +the end of the request body. + +Empty data is allowed regardless of the value of `IsFin`. +Gun may or may not send empty data chunks, however. + +== Arguments + +ConnPid:: + +The pid of the Gun connection process. + +StreamRef:: + +Identifier of the stream for the original request. + +IsFin:: + +Whether this message terminates the request. + +Data:: + +All or part of the response body. + +== Return value + +The atom `ok` is returned. + +== Changelog + +* *1.0*: Function introduced. + +== Examples + +.Stream the body of a request +[source,erlang] +---- +StreamRef = gun:put(ConnPid, "/lang/fr_FR/hello", [ + {<<"content-type">>, <<"text/plain">>} +]). +gun:data(ConnPid, StreamRef, nofin, <<"Bonjour !\n">>). +gun:data(ConnPid, StreamRef, fin, <<"Bonsoir !\n">>). +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:patch(3)[gun:patch(3)], +link:man:gun:post(3)[gun:post(3)], +link:man:gun:put(3)[gun:put(3)], +link:man:gun:headers(3)[gun:headers(3)] diff --git a/gun/doc/src/manual/gun.delete.asciidoc b/gun/doc/src/manual/gun.delete.asciidoc new file mode 100644 index 0000000..36682f5 --- /dev/null +++ b/gun/doc/src/manual/gun.delete.asciidoc @@ -0,0 +1,82 @@ += gun:delete(3) + +== Name + +gun:delete - Delete a resource + +== Description + +[source,erlang] +---- +delete(ConnPid, Path) + -> delete(ConnPid, Path, [], #{}). + +delete(ConnPid, Path, Headers) + -> delete(ConnPid, Path, Headers, #{}) + +delete(ConnPid, Path, Headers, ReqOpts) + -> StreamRef + +ConnPid :: pid() +Path :: iodata() +Headers :: gun:req_headers() +ReqOpts :: gun:req_opts() +StreamRef :: gun:stream_ref() +---- + +Delete a resource. + +== Arguments + +ConnPid:: + +The pid of the Gun connection process. + +Path:: + +Path to the resource. + +Headers:: + +Additional request headers. + +ReqOpts:: + +Request options. + +== Return value + +A reference that identifies the newly created stream is +returned. It is this reference that must be passed in +subsequent calls and will be received in messages related +to this new stream. + +== Changelog + +* *1.0*: Function introduced. + +== Examples + +.Delete a resource +[source,erlang] +---- +StreamRef = gun:delete(ConnPid, "/drafts/123"). +---- + +.Delete a resource with request options +[source,erlang] +---- +StreamRef = gun:delete(ConnPid, "/drafts/123", [], + #{reply_to => ReplyToPid}). +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:put(3)[gun:put(3)], +link:man:gun:await(3)[gun:await(3)], +link:man:gun:await_body(3)[gun:await_body(3)], +link:man:gun_push(3)[gun_push(3)], +link:man:gun_inform(3)[gun_inform(3)], +link:man:gun_response(3)[gun_response(3)], +link:man:gun_data(3)[gun_data(3)] diff --git a/gun/doc/src/manual/gun.flush.asciidoc b/gun/doc/src/manual/gun.flush.asciidoc new file mode 100644 index 0000000..d4ee0c6 --- /dev/null +++ b/gun/doc/src/manual/gun.flush.asciidoc @@ -0,0 +1,59 @@ += gun:flush(3) + +== Name + +gun:flush - Flush all messages related to a connection or a stream + +== Description + +[source,erlang] +---- +flush(ConnPid) -> ok +flush(StreamRef) -> ok + +ConnPid :: pid() +StreamRef :: gun:stream_ref() +---- + +Flush all messages related to a connection or a stream. + +== Arguments + +Either of these arguments may be provided: + +ConnPid:: + +The pid of the Gun connection process. + +StreamRef:: + +Identifier of the stream for the original request. + +== Return value + +The atom `ok` is returned. + +== Changelog + +* *1.0*: Function introduced. + +== Examples + +.Flush all messages from a connection +[source,erlang] +---- +gun:flush(ConnPid). +---- + +.Flush messages from a single stream +[source,erlang] +---- +gun:flush(StreamRef). +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:await(3)[gun:await(3)], +link:man:gun:await_body(3)[gun:await_body(3)], +link:man:gun:await_up(3)[gun:await_up(3)] diff --git a/gun/doc/src/manual/gun.get.asciidoc b/gun/doc/src/manual/gun.get.asciidoc new file mode 100644 index 0000000..6c44636 --- /dev/null +++ b/gun/doc/src/manual/gun.get.asciidoc @@ -0,0 +1,85 @@ += gun:get(3) + +== Name + +gun:get - Get a resource representation + +== Description + +[source,erlang] +---- +get(ConnPid, Path) + -> get(ConnPid, Path, [], #{}). + +get(ConnPid, Path, Headers) + -> get(ConnPid, Path, Headers, #{}) + +get(ConnPid, Path, Headers, ReqOpts) + -> StreamRef + +ConnPid :: pid() +Path :: iodata() +Headers :: gun:req_headers() +ReqOpts :: gun:req_opts() +StreamRef :: gun:stream_ref() +---- + +Get a resource representation. + +== Arguments + +ConnPid:: + +The pid of the Gun connection process. + +Path:: + +Path to the resource. + +Headers:: + +Additional request headers. + +ReqOpts:: + +Request options. + +== Return value + +A reference that identifies the newly created stream is +returned. It is this reference that must be passed in +subsequent calls and will be received in messages related +to this new stream. + +== Changelog + +* *1.0*: Function introduced. + +== Examples + +.Get a resource representation +[source,erlang] +---- +StreamRef = gun:get(ConnPid, "/articles", [ + {<<"accept">>, <<"text/html;q=1.0, application/xml;q=0.1">>} +]). +---- + +.Get a resource representation with request options +[source,erlang] +---- +StreamRef = gun:get(ConnPid, "/articles", [], #{ + reply_to => ReplyToPid +}). +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:head(3)[gun:head(3)], +link:man:gun:await(3)[gun:await(3)], +link:man:gun:await_body(3)[gun:await_body(3)], +link:man:gun_push(3)[gun_push(3)], +link:man:gun_inform(3)[gun_inform(3)], +link:man:gun_response(3)[gun_response(3)], +link:man:gun_data(3)[gun_data(3)] diff --git a/gun/doc/src/manual/gun.head.asciidoc b/gun/doc/src/manual/gun.head.asciidoc new file mode 100644 index 0000000..aab97ce --- /dev/null +++ b/gun/doc/src/manual/gun.head.asciidoc @@ -0,0 +1,92 @@ += gun:head(3) + +== Name + +gun:head - Get headers of a resource representation + +== Description + +[source,erlang] +---- +head(ConnPid, Path) + -> head(ConnPid, Path, [], #{}). + +head(ConnPid, Path, Headers) + -> head(ConnPid, Path, Headers, #{}) + +head(ConnPid, Path, Headers, ReqOpts) + -> StreamRef + +ConnPid :: pid() +Path :: iodata() +Headers :: gun:req_headers() +ReqOpts :: gun:req_opts() +StreamRef :: gun:stream_ref() +---- + +Get headers of a resource representation. + +This function performs the same operation as +link:man:gun:get(3)[gun:get(3)], except the server will not +send the resource representation, only the response's status +code and headers. + +While servers are supposed to send the same headers as for +a GET request, they sometimes will not. For example the +`content-length` header may be dropped from the response. + +== Arguments + +ConnPid:: + +The pid of the Gun connection process. + +Path:: + +Path to the resource. + +Headers:: + +Additional request headers. + +ReqOpts:: + +Request options. + +== Return value + +A reference that identifies the newly created stream is +returned. It is this reference that must be passed in +subsequent calls and will be received in messages related +to this new stream. + +== Changelog + +* *1.0*: Function introduced. + +== Examples + +.Get headers of a resource representation +[source,erlang] +---- +StreamRef = gun:head(ConnPid, "/articles", [ + {<<"accept">>, <<"text/html;q=1.0, application/xml;q=0.1">>} +]). +---- + +.Get headers of a resource representation with request options +[source,erlang] +---- +StreamRef = gun:head(ConnPid, "/articles", [], #{ + reply_to => ReplyToPid +}). +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:get(3)[gun:head(3)], +link:man:gun:await(3)[gun:await(3)], +link:man:gun_push(3)[gun_push(3)], +link:man:gun_inform(3)[gun_inform(3)], +link:man:gun_response(3)[gun_response(3)] diff --git a/gun/doc/src/manual/gun.headers.asciidoc b/gun/doc/src/manual/gun.headers.asciidoc new file mode 100644 index 0000000..24458aa --- /dev/null +++ b/gun/doc/src/manual/gun.headers.asciidoc @@ -0,0 +1,86 @@ += gun:headers(3) + +== Name + +gun:headers - Initiate the given request + +== Description + +[source,erlang] +---- +headers(ConnPid, Method, Path, Headers) + -> headers(ConnPid, Method, Path, Headers, #{}) + +headers(ConnPid, Method, Path, Headers, ReqOpts) + -> StreamRef + +ConnPid :: pid() +Method :: binary() +Path :: iodata() +Headers :: gun:req_headers() +ReqOpts :: gun:req_opts() +StreamRef :: gun:stream_ref() +---- + +Initiate the given request. + +This is a general purpose function that should only be +used when other method-specific functions do not apply. + +The function `headers/4,5` initiates a request but does +not send the request body. It must be sent separately +using link:man:gun:data(3)[gun:data(3)]. + +== Arguments + +ConnPid:: + +The pid of the Gun connection process. + +Method:: + +Method to be used for the request. + +Path:: + +Path to the resource. + +Headers:: + +Additional request headers. + +ReqOpts:: + +Request options. + +== Return value + +A reference that identifies the newly created stream is +returned. It is this reference that must be passed in +subsequent calls and will be received in messages related +to this new stream. + +== Changelog + +* *2.0*: Function introduced. + +== Examples + +.Initiate a request +[source,erlang] +---- +StreamRef = gun:headers(ConnPid, <<"PUT">>, + "/lang/fr_FR/hello", + [{<<"content-type">>, <<"text/plain">>}]). +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:request(3)[gun:request(3)], +link:man:gun:await(3)[gun:await(3)], +link:man:gun:await_body(3)[gun:await_body(3)], +link:man:gun_push(3)[gun_push(3)], +link:man:gun_inform(3)[gun_inform(3)], +link:man:gun_response(3)[gun_response(3)], +link:man:gun_data(3)[gun_data(3)] diff --git a/gun/doc/src/manual/gun.info.asciidoc b/gun/doc/src/manual/gun.info.asciidoc new file mode 100644 index 0000000..e3c19d3 --- /dev/null +++ b/gun/doc/src/manual/gun.info.asciidoc @@ -0,0 +1,69 @@ += gun:info(3) + +== Name + +gun:info - Obtain information about the connection + +== Description + +[source,erlang] +---- +info(ConnPid) -> Info + +ConnPid :: pid() +Info :: #{ + owner => pid(), + socket => inet:socket() | ssl:sslsocket(), + transport => tcp | tls, + protocol => http | http2 | socks | ws, + sock_ip => inet:ip_address(), + sock_port => inet:port_number(), + origin_scheme => binary() | undefined, + origin_host => inet:hostname() | inet:ip_address(), + origin_port => inet:port_number(), + intermediaries => [Intermediary], + cookie_store => gun_cookies:cookie_store() +} +Intermediary :: #{ + type => connect | socks5, + host => inet:hostname() | inet:ip_address(), + port => inet:port_number(), + transport => tcp | tls, + protocol => http | http2 | socks | raw +} +---- + +Obtain information about the connection. + +== Arguments + +ConnPid:: + +The pid of the Gun connection process. + +== Return value + +A map is returned containing various informations about +the connection. + +== Changelog + +* *2.0*: The values `owner`, `origin_scheme` and `cookie_store` were + added. +* *1.3*: The values `socket`, `transport`, `protocol`, `origin_host`, + `origin_port` and `intermediaries` were added. +* *1.0*: Function introduced. + +== Examples + +.Obtain information about the connection +[source,erlang] +---- +Info = gun:info(ConnPid). +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:open(3)[gun:open(3)], +link:man:gun:open_unix(3)[gun:open_unix(3)] diff --git a/gun/doc/src/manual/gun.open.asciidoc b/gun/doc/src/manual/gun.open.asciidoc new file mode 100644 index 0000000..57d4490 --- /dev/null +++ b/gun/doc/src/manual/gun.open.asciidoc @@ -0,0 +1,77 @@ += gun:open(3) + +== Name + +gun:open - Open a connection to the given host and port + +== Description + +[source,erlang] +---- +open(Host, Port) -> open(Host, Port, #{}) +open(Host, Port, Opts) -> {ok, pid()} | {error, Reason} + +Host :: inet:hostname() | inet:ip_address() +Port :: inet:port_number() +Opts :: gun:opts() +Reason :: {options, OptName} + | {options, {http | http2 | socks | ws, OptName}} + | any() +OptName :: atom() +---- + +Open a connection to the given host and port. + +== Arguments + +Host:: + +Host or IP address to connect to. + +Port:: + +Port to connect to. + +Opts:: + +Options for this connection. + +== Return value + +The pid of the newly created Gun process is returned. +Note that this does not indicate that the connection +has been successfully opened; the link:man:gun_up(3)[gun_up(3)] +message will be sent for that. + +== Changelog + +* *1.0*: Function introduced. + +== Examples + +.Connect to a server +[source,erlang] +---- +{ok, ConnPid} = gun:open("example.org", 443). +---- + +.Connect to a server with custom options +[source,erlang] +---- +{ok, ConnPid} = gun:open("example.org", 443, + #{protocols => [http2]}). +---- + +.Connect to a server using its IP address +[source,erlang] +---- +{ok, ConnPid} = gun:open({127,0,0,1}, 443). +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:open_unix(3)[gun:open_unix(3)], +link:man:gun:await_up(3)[gun:await_up(3)], +link:man:gun_tunnel_up(3)[gun_tunnel_up(3)], +link:man:gun_up(3)[gun_up(3)] diff --git a/gun/doc/src/manual/gun.open_unix.asciidoc b/gun/doc/src/manual/gun.open_unix.asciidoc new file mode 100644 index 0000000..0063728 --- /dev/null +++ b/gun/doc/src/manual/gun.open_unix.asciidoc @@ -0,0 +1,65 @@ += gun:open_unix(3) + +== Name + +gun:open_unix - Open a connection to the given Unix domain socket + +== Description + +[source,erlang] +---- +open_unix(SocketPath, Opts) -> {ok, pid()} | {error, Reason} + +SocketPath :: string() +Opts :: gun:opts() +Reason :: {options, OptName} + | {options, {http | http2 | socks | ws, OptName}} + | any() +OptName :: atom() +---- + +Open a connection to the given Unix domain socket. + +== Arguments + +SocketPath:: + +Path to the Unix domain socket to connect to. + +Opts:: + +Options for this connection. + +== Return value + +The pid of the newly created Gun process is returned. +Note that this does not indicate that the connection +has been successfully opened; the link:man:gun_up(3)[gun_up(3)] +message will be sent for that. + +== Changelog + +* *1.0*: Function introduced. + +== Examples + +.Connect to a server via a Unix domain socket +[source,erlang] +---- +{ok, ConnPid} = gun:open_unix("/var/run/dbus/system_bus_socket", #{}). +---- + +.Connect to a server via a Unix domain socket with custom options +[source,erlang] +---- +{ok, ConnPid} = gun:open_unix("/var/run/dbus/system_bus_socket", + #{protocols => [http2]}). +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:open(3)[gun:open(3)], +link:man:gun:await_up(3)[gun:await_up(3)], +link:man:gun_tunnel_up(3)[gun_tunnel_up(3)], +link:man:gun_up(3)[gun_up(3)] diff --git a/gun/doc/src/manual/gun.options.asciidoc b/gun/doc/src/manual/gun.options.asciidoc new file mode 100644 index 0000000..964c601 --- /dev/null +++ b/gun/doc/src/manual/gun.options.asciidoc @@ -0,0 +1,83 @@ += gun:options(3) + +== Name + +gun:options - Query the capabilities of the server or a resource + +== Description + +[source,erlang] +---- +options(ConnPid, Path) + -> options(ConnPid, Path, [], #{}). + +options(ConnPid, Path, Headers) + -> options(ConnPid, Path, Headers, #{}) + +options(ConnPid, Path, Headers, ReqOpts) + -> StreamRef + +ConnPid :: pid() +Path :: iodata() +Headers :: gun:req_headers() +ReqOpts :: gun:req_opts() +StreamRef :: gun:stream_ref() +---- + +Query the capabilities of the server or a resource. + +The special path `"*"` can be used to obtain information about +the server as a whole. Any other path will return information +about that resource specifically. + +== Arguments + +ConnPid:: + +The pid of the Gun connection process. + +Path:: + +Path to the resource. + +Headers:: + +Additional request headers. + +ReqOpts:: + +Request options. + +== Return value + +A reference that identifies the newly created stream is +returned. It is this reference that must be passed in +subsequent calls and will be received in messages related +to this new stream. + +== Changelog + +* *1.0*: Function introduced. + +== Examples + +.Query the capabilities of the server +[source,erlang] +---- +StreamRef = gun:options(ConnPid, "*"). +---- + +.Query the capabilities of a resource +[source,erlang] +---- +StreamRef = gun:options(ConnPid, "/articles"). +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:await(3)[gun:await(3)], +link:man:gun:await_body(3)[gun:await_body(3)], +link:man:gun_inform(3)[gun_inform(3)], +link:man:gun_response(3)[gun_response(3)], +link:man:gun_data(3)[gun_data(3)] diff --git a/gun/doc/src/manual/gun.patch.asciidoc b/gun/doc/src/manual/gun.patch.asciidoc new file mode 100644 index 0000000..ee87b2c --- /dev/null +++ b/gun/doc/src/manual/gun.patch.asciidoc @@ -0,0 +1,123 @@ += gun:patch(3) + +== Name + +gun:patch - Apply a set of changes to a resource + +== Description + +[source,erlang] +---- +patch(ConnPid, Path, Headers) + -> patch(ConnPid, Path, Headers, #{}) + +patch(ConnPid, Path, Headers, ReqOpts) + -> StreamRef + +patch(ConnPid, Path, Headers, Body) + -> patch(ConnPid, Path, Headers, Body, #{}) + +patch(ConnPid, Path, Headers, Body, ReqOpts) + -> StreamRef + +ConnPid :: pid() +Path :: iodata() +Headers :: gun:req_headers() +Body :: iodata() +ReqOpts :: gun:req_opts() +StreamRef :: gun:stream_ref() +---- + +Apply a set of changes to a resource. + +The behavior of this function varies depending on whether +a body is provided. + +The function `patch/3,4` does not send a body. It must be +sent separately using link:man:gun:data(3)[gun:data(3)]. + +The function `patch/4,5` sends the entire request, including +the request body, immediately. It is therefore not possible +to use link:man:gun:data(3)[gun:data(3)] after that. You +should provide a content-type header. Gun will set the +content-length header automatically. + +The body sent in this request should be a patch document +with instructions on how to update the resource. + +== Arguments + +ConnPid:: + +The pid of the Gun connection process. + +Path:: + +Path to the resource. + +Headers:: + +Additional request headers. + +Body:: + +Request body. + +ReqOpts:: + +Request options. + +== Return value + +A reference that identifies the newly created stream is +returned. It is this reference that must be passed in +subsequent calls and will be received in messages related +to this new stream. + +== Changelog + +* *2.0*: Implicit body detection has been removed. The body + must now be provided either directly (even if empty) + or using separate calls. +* *1.0*: Function introduced. + +== Examples + +.Patch a resource +[source,erlang] +---- +StreamRef = gun:patch(ConnPid, "/users/1", + [{<<"content-type">>, <<"application/json-patch+json">>}], + <<"[{\"op\":\"add\",\"path\":\"/baz\",\"value\":\"qux\"}]">>). +---- + +.Patch a resource in multiple calls +[source,erlang] +---- +StreamRef = gun:patch(ConnPid, "/users/1", [ + {<<"content-type">>, <<"application/json-patch+json">>} +]). +gun:data(ConnPid, StreamRef, fin, + <<"[{\"op\":\"add\",\"path\":\"/baz\",\"value\":\"qux\"}]">>). +---- + +.Patch a resource with request options +[source,erlang] +---- +StreamRef = gun:patch(ConnPid, "/users/1", + [{<<"content-type">>, <<"application/json-patch+json">>}], + <<"[{\"op\":\"add\",\"path\":\"/baz\",\"value\":\"qux\"}]">>, + #{reply_to => ReplyToPid}). +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:post(3)[gun:post(3)], +link:man:gun:put(3)[gun:put(3)], +link:man:gun:await(3)[gun:await(3)], +link:man:gun:await_body(3)[gun:await_body(3)], +link:man:gun_push(3)[gun_push(3)], +link:man:gun_inform(3)[gun_inform(3)], +link:man:gun_response(3)[gun_response(3)], +link:man:gun_data(3)[gun_data(3)] diff --git a/gun/doc/src/manual/gun.post.asciidoc b/gun/doc/src/manual/gun.post.asciidoc new file mode 100644 index 0000000..e6d08ba --- /dev/null +++ b/gun/doc/src/manual/gun.post.asciidoc @@ -0,0 +1,120 @@ += gun:post(3) + +== Name + +gun:post - Process the enclosed representation according to a resource's own semantics + +== Description + +[source,erlang] +---- +post(ConnPid, Path, Headers) + -> post(ConnPid, Path, Headers, #{}) + +post(ConnPid, Path, Headers, ReqOpts) + -> StreamRef + +post(ConnPid, Path, Headers, Body) + -> post(ConnPid, Path, Headers, Body, #{}) + +post(ConnPid, Path, Headers, Body, ReqOpts) + -> StreamRef + +ConnPid :: pid() +Path :: iodata() +Headers :: gun:req_headers() +Body :: iodata() +ReqOpts :: gun:req_opts() +StreamRef :: gun:stream_ref() +---- + +Process the enclosed representation according to a resource's +own semantics. + +The behavior of this function varies depending on whether +a body is provided. + +The function `post/3,4` does not send a body. It must be +sent separately using link:man:gun:data(3)[gun:data(3)]. + +The function `post/4,5` sends the entire request, including +the request body, immediately. It is therefore not possible +to use link:man:gun:data(3)[gun:data(3)] after that. You +should provide a content-type header. Gun will set the +content-length header automatically. + +== Arguments + +ConnPid:: + +The pid of the Gun connection process. + +Path:: + +Path to the resource. + +Headers:: + +Additional request headers. + +Body:: + +Request body. + +ReqOpts:: + +Request options. + +== Return value + +A reference that identifies the newly created stream is +returned. It is this reference that must be passed in +subsequent calls and will be received in messages related +to this new stream. + +== Changelog + +* *2.0*: Implicit body detection has been removed. The body + must now be provided either directly (even if empty) + or using separate calls. +* *1.0*: Function introduced. + +== Examples + +.Post to a resource +[source,erlang] +---- +StreamRef = gun:post(ConnPid, "/search", + [{<<"content-type">>, <<"application/x-www-form-urlencoded">>}], + <<"q=nine%20nines">>). +---- + +.Post to a resource in multiple calls +[source,erlang] +---- +StreamRef = gun:post(ConnPid, "/search", [ + {<<"content-type">>, <<"application/x-www-form-urlencoded">>} +]). +gun:data(ConnPid, StreamRef, fin, <<"q=nine%20nines">>). +---- + +.Post to a resource with request options +[source,erlang] +---- +StreamRef = gun:post(ConnPid, "/search", + [{<<"content-type">>, <<"application/x-www-form-urlencoded">>}], + <<"q=nine%20nines">>, + #{reply_to => ReplyToPid}). +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:patch(3)[gun:patch(3)], +link:man:gun:put(3)[gun:put(3)], +link:man:gun:await(3)[gun:await(3)], +link:man:gun:await_body(3)[gun:await_body(3)], +link:man:gun_push(3)[gun_push(3)], +link:man:gun_inform(3)[gun_inform(3)], +link:man:gun_response(3)[gun_response(3)], +link:man:gun_data(3)[gun_data(3)] diff --git a/gun/doc/src/manual/gun.put.asciidoc b/gun/doc/src/manual/gun.put.asciidoc new file mode 100644 index 0000000..63cd7c5 --- /dev/null +++ b/gun/doc/src/manual/gun.put.asciidoc @@ -0,0 +1,119 @@ += gun:put(3) + +== Name + +gun:put - Create or replace a resource + +== Description + +[source,erlang] +---- +put(ConnPid, Path, Headers) + -> put(ConnPid, Path, Headers, #{}) + +put(ConnPid, Path, Headers, ReqOpts) + -> StreamRef + +put(ConnPid, Path, Headers, Body) + -> put(ConnPid, Path, Headers, Body, #{}) + +put(ConnPid, Path, Headers, Body, ReqOpts) + -> StreamRef + +ConnPid :: pid() +Path :: iodata() +Headers :: gun:req_headers() +Body :: iodata() +ReqOpts :: gun:req_opts() +StreamRef :: gun:stream_ref() +---- + +Create or replace a resource. + +The behavior of this function varies depending on whether +a body is provided. + +The function `put/3,4` does not send a body. It must be +sent separately using link:man:gun:data(3)[gun:data(3)]. + +The function `put/4,5` sends the entire request, including +the request body, immediately. It is therefore not possible +to use link:man:gun:data(3)[gun:data(3)] after that. You +should provide a content-type header. Gun will set the +content-length header automatically. + +== Arguments + +ConnPid:: + +The pid of the Gun connection process. + +Path:: + +Path to the resource. + +Headers:: + +Additional request headers. + +Body:: + +Request body. + +ReqOpts:: + +Request options. + +== Return value + +A reference that identifies the newly created stream is +returned. It is this reference that must be passed in +subsequent calls and will be received in messages related +to this new stream. + +== Changelog + +* *2.0*: Implicit body detection has been removed. The body + must now be provided either directly (even if empty) + or using separate calls. +* *1.0*: Function introduced. + +== Examples + +.Put a resource +[source,erlang] +---- +StreamRef = gun:put(ConnPid, "/lang/fr_FR/hello", + [{<<"content-type">>, <<"text/plain">>}], + <<"Bonjour !">>). +---- + +.Put a resource in multiple calls +[source,erlang] +---- +StreamRef = gun:put(ConnPid, "/lang/fr_FR/hello", [ + {<<"content-type">>, <<"text/plain">>} +]). +gun:data(ConnPid, StreamRef, fin, <<"Bonjour !">>). +---- + +.Put a resource with request options +[source,erlang] +---- +StreamRef = gun:put(ConnPid, "/lang/fr_FR/hello", + [{<<"content-type">>, <<"text/plain">>}], + <<"Bonjour !">>, + #{reply_to => ReplyToPid}). +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:patch(3)[gun:patch(3)], +link:man:gun:post(3)[gun:post(3)], +link:man:gun:await(3)[gun:await(3)], +link:man:gun:await_body(3)[gun:await_body(3)], +link:man:gun_push(3)[gun_push(3)], +link:man:gun_inform(3)[gun_inform(3)], +link:man:gun_response(3)[gun_response(3)], +link:man:gun_data(3)[gun_data(3)] diff --git a/gun/doc/src/manual/gun.request.asciidoc b/gun/doc/src/manual/gun.request.asciidoc new file mode 100644 index 0000000..c004b4c --- /dev/null +++ b/gun/doc/src/manual/gun.request.asciidoc @@ -0,0 +1,97 @@ += gun:request(3) + +== Name + +gun:request - Perform the given request + +== Description + +[source,erlang] +---- +request(ConnPid, Method, Path, Headers, Body) + -> request(ConnPid, Method, Path, Headers, Body, #{}) + +request(ConnPid, Method, Path, Headers, Body, ReqOpts) + -> StreamRef + +ConnPid :: pid() +Method :: binary() +Path :: iodata() +Headers :: gun:req_headers() +Body :: iodata() +ReqOpts :: gun:req_opts() +StreamRef :: gun:stream_ref() +---- + +Perform the given request. + +This is a general purpose function that should only be +used when other method-specific functions do not apply. + +The function `request/5,6` sends the entire request, including +the request body, immediately. It is therefore not possible +to use link:man:gun:data(3)[gun:data(3)] after that. You +should provide a content-type header. Gun will set the +content-length header automatically. + +== Arguments + +ConnPid:: + +The pid of the Gun connection process. + +Method:: + +Method to be used for the request. + +Path:: + +Path to the resource. + +Headers:: + +Additional request headers. + +Body:: + +Request body. + +ReqOpts:: + +Request options. + +== Return value + +A reference that identifies the newly created stream is +returned. It is this reference that must be passed in +subsequent calls and will be received in messages related +to this new stream. + +== Changelog + +* *2.0*: Implicit body detection has been removed. The body + must now be provided either directly (even if empty) + or using link:man:gun:headers(3)[gun:headers(3)]. +* *1.0*: Function introduced. + +== Examples + +.Perform a request +[source,erlang] +---- +StreamRef = gun:request(ConnPid, <<"PUT">>, + "/lang/fr_FR/hello", + [{<<"content-type">>, <<"text/plain">>}], + <<"Bonjour !">>). +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:headers(3)[gun:headers(3)], +link:man:gun:await(3)[gun:await(3)], +link:man:gun:await_body(3)[gun:await_body(3)], +link:man:gun_push(3)[gun_push(3)], +link:man:gun_inform(3)[gun_inform(3)], +link:man:gun_response(3)[gun_response(3)], +link:man:gun_data(3)[gun_data(3)] diff --git a/gun/doc/src/manual/gun.set_owner.asciidoc b/gun/doc/src/manual/gun.set_owner.asciidoc new file mode 100644 index 0000000..ee12a67 --- /dev/null +++ b/gun/doc/src/manual/gun.set_owner.asciidoc @@ -0,0 +1,57 @@ += gun:set_owner(3) + +== Name + +gun:set_owner - Set a new owner for the connection + +== Description + +[source,erlang] +---- +set_owner(ConnPid, OwnerPid) -> ok + +ConnPid :: pid() +OwnerPid :: pid() +---- + +Set a new owner for the connection. + +Only the current owner of the connection can set a new +owner. + +Gun monitors the owner of the connection and automatically +shuts down gracefully when the owner exits. + +== Arguments + +ConnPid:: + +The pid of the Gun connection process. + +OwnerPid:: + +The pid of the new owner for the connection. + +== Return value + +The atom `ok` is returned. + +== Changelog + +* *2.0*: Function introduced. + +== Examples + +.Set a new owner for the connection +[source,erlang] +---- +ok = gun:set_owner(ConnPid, OwnerPid). +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:open(3)[gun:open(3)], +link:man:gun:open_unix(3)[gun:open_unix(3)], +link:man:gun:shutdown(3)[gun:shutdown(3)], +link:man:gun:close(3)[gun:close(3)] diff --git a/gun/doc/src/manual/gun.shutdown.asciidoc b/gun/doc/src/manual/gun.shutdown.asciidoc new file mode 100644 index 0000000..94db39d --- /dev/null +++ b/gun/doc/src/manual/gun.shutdown.asciidoc @@ -0,0 +1,67 @@ += gun:shutdown(3) + +== Name + +gun:shutdown - Gracefully close the connection + +== Description + +[source,erlang] +---- +shutdown(ConnPid) -> ok + +ConnPid :: pid() +---- + +Gracefully close the connection. + +Gun will wait for up to `closing_timeout` milliseconds +before brutally closing the connection. The graceful +shutdown mechanism varies between the different protocols: + +* For HTTP/1.1 there is no such mechanism and Gun will + close the connection once the current response is + received. Any pipelined requests are immediately + terminated. + +* For HTTP/2 Gun will send a GOAWAY frame and wait for + the existing streams to terminate. + +* For Websocket Gun will send a close frame and wait + for the server's close frame before closing the + connection. + +The function returns immediately. The connection may +therefore still be up for some time after this call. + +Gun will not attempt to reconnect once graceful +shutdown has been initiated. + +== Arguments + +ConnPid:: + +The pid of the Gun connection process. + +== Return value + +The atom `ok` is returned. + +== Changelog + +* *2.0*: Function introduced. + +== Examples + +.Gracefully shutdown the connection +[source,erlang] +---- +ok = gun:shutdown(ConnPid). +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:open(3)[gun:open(3)], +link:man:gun:open_unix(3)[gun:open_unix(3)], +link:man:gun:close(3)[gun:close(3)] diff --git a/gun/doc/src/manual/gun.stream_info.asciidoc b/gun/doc/src/manual/gun.stream_info.asciidoc new file mode 100644 index 0000000..de09cb5 --- /dev/null +++ b/gun/doc/src/manual/gun.stream_info.asciidoc @@ -0,0 +1,79 @@ += gun:stream_info(3) + +== Name + +gun:stream_info - Obtain information about a stream + +== Description + +[source,erlang] +---- +stream_info(ConnPid, StreamRef) -> {ok, undefined | Info} | {error, not_connected} + +ConnPid :: pid() +StreamRef :: gun:stream_ref() +Info :: #{ + ref => gun:stream_ref(), + reply_to => pid(), + state => running | stopping, + intermediaries => [Intermediary], + tunnel => Tunnel +} +Intermediary :: #{ + type => connect | socks5, + host => inet:hostname() | inet:ip_address(), + port => inet:port_number(), + transport => tcp | tls, + protocol => http | http2 | socks | raw +} +Tunnel :: #{ + transport => tcp | tls, + protocol => http | http2 | socks | raw, + origin_scheme => binary() | undefined, + origin_host => inet:hostname() | inet:ip_address(), + origin_port => inet:port_number() +} +---- + +Obtain information about a stream. + +== Arguments + +ConnPid:: + +The pid of the Gun connection process. + +StreamRef:: + +Identifier of the stream for the original request. + +== Return value + +A map is returned containing various informations about +the stream. + +== Changelog + +* *2.0*: Function introduced. + +== Examples + +.Obtain information about a stream +[source,erlang] +---- +Info = gun:stream_info(ConnPid, StreamRef). +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:get(3)[gun:get(3)], +link:man:gun:head(3)[gun:head(3)], +link:man:gun:options(3)[gun:options(3)], +link:man:gun:patch(3)[gun:patch(3)], +link:man:gun:post(3)[gun:post(3)], +link:man:gun:put(3)[gun:put(3)], +link:man:gun:delete(3)[gun:delete(3)], +link:man:gun:headers(3)[gun:headers(3)], +link:man:gun:request(3)[gun:request(3)], +link:man:gun:cancel(3)[gun:cancel(3)] diff --git a/gun/doc/src/manual/gun.update_flow.asciidoc b/gun/doc/src/manual/gun.update_flow.asciidoc new file mode 100644 index 0000000..60e9d78 --- /dev/null +++ b/gun/doc/src/manual/gun.update_flow.asciidoc @@ -0,0 +1,67 @@ += gun:update_flow(3) + +== Name + +gun:update_flow - Update a stream's flow control value + +== Description + +[source,erlang] +---- +update_flow(ConnPid, StreamRef, Flow) -> ok + +ConnPid :: pid() +StreamRef :: gun:stream_ref() +Flow :: pos_integer() +---- + +Update a stream's flow control value. + +The flow value can only ever be incremented. + +This function does nothing for streams that have flow +control disabled (which is the default). + +== Arguments + +ConnPid:: + +The pid of the Gun connection process. + +StreamRef:: + +Identifier of the stream for the original request. + +Flow:: + +Flow control value increment. + +== Return value + +The atom `ok` is returned. + +== Changelog + +* *2.0*: Function introduced. + +== Examples + +.Update a stream's flow control value +[source,erlang] +---- +gun:update_flow(ConnPid, StreamRef, 10). +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:get(3)[gun:get(3)], +link:man:gun:head(3)[gun:head(3)], +link:man:gun:options(3)[gun:options(3)], +link:man:gun:patch(3)[gun:patch(3)], +link:man:gun:post(3)[gun:post(3)], +link:man:gun:put(3)[gun:put(3)], +link:man:gun:delete(3)[gun:delete(3)], +link:man:gun:headers(3)[gun:headers(3)], +link:man:gun:request(3)[gun:request(3)], +link:man:gun:ws_upgrade(3)[gun:ws_upgrade(3)] diff --git a/gun/doc/src/manual/gun.ws_send.asciidoc b/gun/doc/src/manual/gun.ws_send.asciidoc new file mode 100644 index 0000000..224472e --- /dev/null +++ b/gun/doc/src/manual/gun.ws_send.asciidoc @@ -0,0 +1,72 @@ += gun:ws_send(3) + +== Name + +gun:ws_send - Send Websocket frames + +== Description + +[source,erlang] +---- +ws_send(ConnPid, StreamRef, Frames) -> ok + +ConnPid :: pid() +StreamRef :: gun:stream_ref() +Frames :: Frame | [Frame] +Frame :: close | ping | pong + | {text | binary | close | ping | pong, iodata()} + | {close, non_neg_integer(), iodata()} +---- + +Send Websocket frames. + +The connection must first be upgraded to Websocket using +the function link:man:gun:ws_upgrade(3)[gun:ws_upgrade(3)]. + +== Arguments + +ConnPid:: + +The pid of the Gun connection process. + +StreamRef:: + +Identifier of the stream that was upgraded to Websocket. + +Frames:: + +One or more Websocket frame(s). + +== Return value + +The atom `ok` is returned. + +== Changelog + +* *2.0*: The mandatory `StreamRef` argument was added. +* *2.0*: It is now possible to send multiple frames at once. +* *1.0*: Function introduced. + +== Examples + +.Send a single frame +[source,erlang] +---- +gun:ws_send(ConnPid, StreamRef, {text, <<"Hello world!">>}). +---- + +.Send many frames including a close frame +[source,erlang] +---- +gun:ws_send(ConnPid, StreamRef, [ + {text, <<"See you later, world!">>}, + close +]). +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:ws_upgrade(3)[gun:ws_upgrade(3)], +link:man:gun_upgrade(3)[gun_upgrade(3)], +link:man:gun_ws(3)[gun_ws(3)] diff --git a/gun/doc/src/manual/gun.ws_upgrade.asciidoc b/gun/doc/src/manual/gun.ws_upgrade.asciidoc new file mode 100644 index 0000000..c6e3850 --- /dev/null +++ b/gun/doc/src/manual/gun.ws_upgrade.asciidoc @@ -0,0 +1,117 @@ += gun:ws_upgrade(3) + +== Name + +gun:ws_upgrade - Upgrade to Websocket + +== Description + +[source,erlang] +---- +ws_upgrade(ConnPid, Path) + -> ws_upgrade(ConnPid, Path, []) + +ws_upgrade(ConnPid, Path, Headers) + -> StreamRef + +ws_upgrade(ConnPid, Path, Headers, WsOpts) + -> StreamRef + +ConnPid :: pid() +Path :: iodata() +Headers :: gun:req_headers() +WsOpts :: gun:ws_opts() +StreamRef :: gun:stream_ref() +---- + +Upgrade to Websocket. + +The behavior of this function depends on the protocol +selected. + +HTTP/1.1 cannot handle Websocket and HTTP requests +concurrently. The upgrade, if successful, will result +in the complete takeover of the connection. Any +subsequent HTTP requests will be rejected. + +Gun does not currently support Websocket over HTTP/2. + +By default Gun will take the Websocket options from +the connection's `ws_opts`. + +Websocket subprotocol negotiation is enabled when +the `protocols` option is given. It takes a subprotocol +name and a module implementing the +link:man:gun_ws_protocol(3)[gun_ws_protocol(3)] behavior. + +== Arguments + +ConnPid:: + +The pid of the Gun connection process. + +Path:: + +Path to the resource. + +Headers:: + +Additional request headers. + +WsOpts:: + +Configuration for the Websocket protocol. + +== Return value + +A reference that identifies the newly created stream is +returned. It is this reference that must be passed in +subsequent calls and will be received in messages related +to this new stream. + +== Changelog + +* *1.0*: Function introduced. + +== Examples + +.Upgrade to Websocket +[source,erlang] +---- +StreamRef = gun:ws_upgrade(ConnPid, "/ws", [ + {<<"sec-websocket-protocol">>, <<"chat">>} +]). +receive + {gun_upgrade, ConnPid, StreamRef, [<<"websocket">>], _} -> + ok +after 5000 -> + error(timeout) +end. +---- + +.Upgrade to Websocket with different options +[source,erlang] +---- +StreamRef = gun:ws_upgrade(ConnPid, "/ws", [], #{ + compress => false +}). +---- + +.Upgrade to Websocket with protocol negotiation +[source,erlang] +---- +StreamRef = gun:ws_upgrade(ConnPid, "/ws", [], #{ + protocols => [ + {<<"mqtt">>, gun_ws_mqtt_h}, + {<<"v12.stomp">>, gun_ws_stomp_h} + ] +}). +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:ws_send(3)[gun:ws_send(3)], +link:man:gun_upgrade(3)[gun_upgrade(3)], +link:man:gun_ws(3)[gun_ws(3)], +link:man:gun_ws_protocol(3)[gun_ws_protocol(3)] diff --git a/gun/doc/src/manual/gun_app.asciidoc b/gun/doc/src/manual/gun_app.asciidoc new file mode 100644 index 0000000..ca05594 --- /dev/null +++ b/gun/doc/src/manual/gun_app.asciidoc @@ -0,0 +1,44 @@ += gun(7) + +== Name + +gun - HTTP/1.1, HTTP/2 and Websocket client for Erlang/OTP + +== Description + +Gun is an HTTP client for Erlang/OTP with support for the +HTTP/1.1, HTTP/2 and Websocket protocols. + +Gun aims to provide an easy to use, asynchronous and +always-connected client. It maintains a permanent connection +to the server and reconnects automatically when necessary. + +== Modules + +* link:man:gun(3)[gun(3)] - Asynchronous HTTP client +* link:man:gun_cookies(3)[gun_cookies(3)] - Cookie store engine +* link:man:gun_cookies_list(3)[gun_cookies_list(3)] - Cookie store backend: in-memory, per connection +* link:man:gun_event(3)[gun_event(3)] - Events +* link:man:gun_ws_protocol(3)[gun_ws_protocol(3)] - Websocket subprotocols + +== Dependencies + +* link:man:cowlib(7)[cowlib(7)] - Support library for manipulating Web protocols +* ssl - Secure communication over sockets + +All these applications must be started before the `gun` +application. To start Gun and all dependencies at once: + +[source,erlang] +---- +{ok, _} = application:ensure_all_started(gun). +---- + +== Environment + +The `gun` application does not define any application +environment configuration parameters. + +== See also + +link:man:cowlib(7)[cowlib(7)] diff --git a/gun/doc/src/manual/gun_cookies.asciidoc b/gun/doc/src/manual/gun_cookies.asciidoc new file mode 100644 index 0000000..06f1daf --- /dev/null +++ b/gun/doc/src/manual/gun_cookies.asciidoc @@ -0,0 +1,179 @@ += gun_cookies(3) + +== Name + +gun_cookies - Cookie store engine + +== Description + +The `gun_cookies` module implements a cookie store engine. +It will be used by Gun when a cookie store is configured. +It also defines the interface and provides functions used +to implement cookie store backends. + +== Callbacks + +Cookie store backends implement the following interface. +Functions are organized by theme: initialization, querying, +storing and garbage collecting: + +=== init + +[source,erlang] +---- +init(Opts :: any()) -> gun_cookies:store() +---- + +Initialize the cookie store. + +=== query + +[source,erlang] +---- +query(State, URI) -> {ok, [Cookie], State} + +URI :: uri_string:uri_map() +Cookie :: gun_cookies:cookie() +State :: any() +---- + +Query the store for the cookies for the given URI. + +=== set_cookie_secure_match + +[source,erlang] +---- +set_cookie_secure_match(State, Match) -> match | nomatch + +State :: any() +Match :: #{ + name := binary(), +% secure_only := true, + domain := binary(), + path := binary() +} +---- + +Perform a secure match against cookies already in the store. +This is part of the heuristics that the cookie store engine +applies to decide whether the cookie must be stored. + +The `secure_only` attribute is implied, it is not actually +passed in the argument. + +=== set_cookie_get_exact_match + +[source,erlang] +---- +set_cookie_get_exact_match(State, Match) + -> {ok, gun_cookies:cookie(), State} | error + +State :: any() +Match :: #{ + name := binary(), + domain := binary(), + host_only := boolean(), + path := binary() +} +---- + +Perform an exact match against cookies already in the store. +This is part of the heuristics that the cookie store engine +applies to decide whether the cookie must be stored. + +When a cookie is found, it must be returned so that it gets +updated. When nothing is found a new cookie will be stored. + +=== store + +[source,erlang] +---- +store(State, gun_cookies:cookie()) + -> {ok, State} | {error, any()} + +State :: any() +---- + +Unconditionally store the cookie into the cookie store. + +=== gc + +[source,erlang] +---- +gc(State) -> {ok, State} + +State :: any() +---- + +Remove all cookies from the cookie store that are expired. + +Other cookies may be removed as well, at the discretion +of the cookie store. For example excess cookies may be +removed to reduce the memory footprint. + +=== session_gc + +[source,erlang] +---- +session_gc(State) -> {ok, State} + +State :: any() +---- + +Remove all cookies from the cookie store that have the +`persistent` flag set to `false`. + +== Exports + +* link:man:gun_cookies:domain_match(3)[gun_cookies:domain_match(3)] - Cookie domain match +* link:man:gun_cookies:path_match(3)[gun_cookies:path_match(3)] - Cookie path match + +== Types + +=== cookie() + +[source,erlang] +---- +cookie() :: #{ + name := binary(), + value := binary(), + domain := binary(), + path := binary(), + creation_time := calendar:datetime(), + last_access_time := calendar:datetime(), + expiry_time := calendar:datetime() | infinity, + persistent := boolean(), + host_only := boolean(), + secure_only := boolean(), + http_only := boolean(), + same_site := strict | lax | none +} +---- + +A cookie. + +This contains the cookie name, value, attributes and flags. +This is the representation that the cookie store engine +and Gun expects. Cookies do not have to be kept in this +format in the cookie store backend. + +=== store() + +[source,erlang] +---- +store() :: {module(), StoreState :: any()} +---- + +The cookie store. + +This is a tuple containing the cookie store backend module +and its current state. + +== Changelog + +* *2.0*: Module introduced. + +== See also + +link:man:gun(7)[gun(7)], +link:man:gun_cookies_list(3)[gun_cookies_list(3)] diff --git a/gun/doc/src/manual/gun_cookies.domain_match.asciidoc b/gun/doc/src/manual/gun_cookies.domain_match.asciidoc new file mode 100644 index 0000000..34f52ec --- /dev/null +++ b/gun/doc/src/manual/gun_cookies.domain_match.asciidoc @@ -0,0 +1,52 @@ += gun_cookies:domain_match(3) + +== Name + +gun_cookies:domain_match - Cookie domain match + +== Description + +[source,erlang] +---- +domain_match(String, DomainString) -> boolean() + +String :: binary() +DomainString :: binary() +---- + +Cookie domain match. + +This function can be used when implementing the +`set_cookie_secure_match` callback of a cookie store. + +== Arguments + +String:: + +The string to match. + +DomainString:: + +The domain string that will be matched against. + +== Return value + +Returns `true` when `String` domain-matches `DomainString`, +and `false` otherwise. + +== Changelog + +* *2.0*: Function introduced. + +== Examples + +.Perform a domain match +[source,erlang] +---- +Match = gun_cookies:domain_match(Domain, CookieDomain). +---- + +== See also + +link:man:gun_cookies(3)[gun_cookies(3)], +link:man:gun_cookies:path_match(3)[gun_cookies:path_match(3)] diff --git a/gun/doc/src/manual/gun_cookies.path_match.asciidoc b/gun/doc/src/manual/gun_cookies.path_match.asciidoc new file mode 100644 index 0000000..2e1771a --- /dev/null +++ b/gun/doc/src/manual/gun_cookies.path_match.asciidoc @@ -0,0 +1,52 @@ += gun_cookies:path_match(3) + +== Name + +gun_cookies:path_match - Cookie path match + +== Description + +[source,erlang] +---- +path_match(ReqPath, CookiePath) -> boolean() + +ReqPath :: binary() +CookiePath :: binary() +---- + +Cookie path match. + +This function can be used when implementing the +`set_cookie_secure_match` callback of a cookie store. + +== Arguments + +ReqPath:: + +The request path to match. + +CookiePath:: + +The cookie path that will be matched against. + +== Return value + +Returns `true` when `ReqPath` path-matches `CookiePath`, +and `false` otherwise. + +== Changelog + +* *2.0*: Function introduced. + +== Examples + +.Perform a path match +[source,erlang] +---- +Match = gun_cookies:path_match(ReqPath, CookiePath). +---- + +== See also + +link:man:gun_cookies(3)[gun_cookies(3)], +link:man:gun_cookies:domain_match(3)[gun_cookies:domain_match(3)] diff --git a/gun/doc/src/manual/gun_cookies_list.asciidoc b/gun/doc/src/manual/gun_cookies_list.asciidoc new file mode 100644 index 0000000..2daef8e --- /dev/null +++ b/gun/doc/src/manual/gun_cookies_list.asciidoc @@ -0,0 +1,55 @@ += gun_cookies_list(3) + +== Name + +gun_cookies_list - Cookie store backend: in-memory, per connection + +== Description + +The `gun_cookies_list` module implements a cookie store +backend that keeps all the cookie data in-memory and tied +to a specific connection. + +It is possible to implement a custom backend on top of +`gun_cookies_list` in order to add persistence or sharing +properties. + +== Exports + +This module implements the callbacks defined in +link:man:gun_cookies(3)[gun_cookies(3)]. + +== Types + +=== opts() + +[source,erlang] +---- +opts() :: #{ +} +---- + +Cookie store backend options. + +There are currently no options available for this backend. + +// The default value is given next to the option name: + +== Changelog + +* *2.0*: Module introduced. + +== Examples + +.Open a connection with a cookie store configured +[source,erlang] +---- +{ok, ConnPid} = gun:open(Host, Port, #{ + cookie_store => gun_cookies_list:init(#{}) +}) +---- + +== See also + +link:man:gun(7)[gun(7)], +link:man:gun_cookies(3)[gun_cookies(3)] diff --git a/gun/doc/src/manual/gun_data.asciidoc b/gun/doc/src/manual/gun_data.asciidoc new file mode 100644 index 0000000..bcae2f8 --- /dev/null +++ b/gun/doc/src/manual/gun_data.asciidoc @@ -0,0 +1,78 @@ += gun_data(3) + +== Name + +gun_data - Response body + +== Description + +[source,erlang] +---- +{gun_data, ConnPid, StreamRef, IsFin, Data} + +ConnPid :: pid() +StreamRef :: gun:stream_ref() +IsFin :: fin | nofin +Data :: binary() +---- + +Response body. + +This message informs the relevant process that the server +sent a all or part of the body for the response to the +original request. + +A data message is always preceded by a response message. + +The response body may be terminated either by a data +message with the flag `fin` set or by a +link:man:gun_trailers(3)[gun_trailers(3)] message. + +== Elements + +ConnPid:: + +The pid of the Gun connection process. + +StreamRef:: + +Identifier of the stream for the original request. + +IsFin:: + +Whether this message terminates the response. + +Data:: + +All or part of the response body. + +== Changelog + +* *1.0*: Message introduced. + +== Examples + +.Receive a gun_data message in a gen_server +[source,erlang] +---- +handle_info({gun_data, ConnPid, _StreamRef, + _IsFin, _Data}, + State=#state{conn_pid=ConnPid}) -> + %% Do something. + {noreply, State}. +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:get(3)[gun:get(3)], +link:man:gun:head(3)[gun:head(3)], +link:man:gun:patch(3)[gun:patch(3)], +link:man:gun:post(3)[gun:post(3)], +link:man:gun:put(3)[gun:put(3)], +link:man:gun:delete(3)[gun:delete(3)], +link:man:gun:options(3)[gun:options(3)], +link:man:gun:headers(3)[gun:headers(3)], +link:man:gun:request(3)[gun:request(3)], +link:man:gun_response(3)[gun_response(3)], +link:man:gun_trailers(3)[gun_trailers(3)] diff --git a/gun/doc/src/manual/gun_down.asciidoc b/gun/doc/src/manual/gun_down.asciidoc new file mode 100644 index 0000000..f00f8b1 --- /dev/null +++ b/gun/doc/src/manual/gun_down.asciidoc @@ -0,0 +1,79 @@ += gun_down(3) + +== Name + +gun_down - The connection is down + +== Description + +[source,erlang] +---- +{gun_down, ConnPid, Protocol, Reason, KilledStreams} + +ConnPid :: pid() +Protocol :: http | http2 | socks | ws +Reason :: any() +KilledStreams :: [gun:stream_ref()] +---- + +The connection is down. + +This message informs the owner process that the connection +was lost. Depending on the `retry` and `retry_timeout` +options Gun may automatically attempt to reconnect. + +When the connection goes back up, Gun will not attempt to retry +requests. It will also not upgrade to Websocket automatically +if that was the protocol in use when the connection was lost. + +== Elements + +ConnPid:: + +The pid of the Gun connection process. + +Protocol:: + +The protocol that was selected for this connection +or upgraded to during the course of the connection. + +Reason:: + +The reason for the loss of the connection. ++ +It is present for debugging purposes only. You should not +rely on this value to perform operations programmatically. + +KilledStreams:: + +List of streams that have been brutally terminated. ++ +They are active streams that did not complete before the closing +of the connection. Whether they can be retried safely depends +on the protocol used and the idempotence property of the requests. + +== Changelog + +* *2.0*: The last element of the message's tuple, `UnprocessedStreams` + has been removed. +* *1.0*: Message introduced. + +== Examples + +.Receive a gun_down message in a gen_server +[source,erlang] +---- +handle_info({gun_down, ConnPid, _Protocol, _Reason, _Killed}, + State=#state{conn_pid=ConnPid}) -> + %% Do something. + {noreply, State}. +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:open(3)[gun:open(3)], +link:man:gun:open_unix(3)[gun:open_unix(3)], +link:man:gun_up(3)[gun_up(3)], +link:man:gun_tunnel_up(3)[gun_tunnel_up(3)], +link:man:gun_error(3)[gun_error(3)] diff --git a/gun/doc/src/manual/gun_error.asciidoc b/gun/doc/src/manual/gun_error.asciidoc new file mode 100644 index 0000000..90c7c33 --- /dev/null +++ b/gun/doc/src/manual/gun_error.asciidoc @@ -0,0 +1,68 @@ += gun_error(3) + +== Name + +gun_error - Stream or connection-wide error + +== Description + +[source,erlang] +---- +{gun_error, ConnPid, StreamRef, Reason} +{gun_error, ConnPid, Reason} + +ConnPid :: pid() +StreamRef :: gun:stream_ref() +Reason :: any() +---- + +Stream or connection-wide error. + +These messages inform the relevant process that an error +occurred. A reference is given when the error pertains +to a specific stream. Connection-wide errors do not +imply that the connection is no longer usable, they are +used for all errors that are not specific to a stream. + +== Elements + +ConnPid:: + +The pid of the Gun connection process. + +StreamRef:: + +Identifier of the stream that resulted in an error. + +Reason:: + +The reason for the error. ++ +It is present for debugging purposes only. You should not +rely on this value to perform operations programmatically. + +== Changelog + +* *1.0*: Message introduced. + +== Examples + +.Receive a gun_error message in a gen_server +[source,erlang] +---- +handle_info({gun_error, ConnPid, _Reason}, + State=#state{conn_pid=ConnPid}) -> + %% Do something. + {noreply, State}; +handle_info({gun_error, ConnPid, _StreamRef, _Reason}, + State=#state{conn_pid=ConnPid}) -> + %% Do something. + {noreply, State}. +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun_up(3)[gun_up(3)], +link:man:gun_tunnel_up(3)[gun_tunnel_up(3)], +link:man:gun_down(3)[gun_down(3)] diff --git a/gun/doc/src/manual/gun_event.asciidoc b/gun/doc/src/manual/gun_event.asciidoc new file mode 100644 index 0000000..c58ac8e --- /dev/null +++ b/gun/doc/src/manual/gun_event.asciidoc @@ -0,0 +1,509 @@ += gun_event(3) + +== Name + +gun_event - Events + +== Description + +The `gun_event` module provides the callback interface +and types for implementing event handlers. + +== Callbacks + +Event handlers implement the following interface. +Because types are tied to specific events, they +are documented alongside them. All event types +are exported and can be referred to as `gun_event:Type()`. + +The events are ordered by the order they are likely +to be triggered, with the most frequent events listed +first. + +=== init + +[source,erlang] +---- +init_event() :: #{ + owner := pid(), + transport := tcp | tls, + origin_scheme := binary(), + origin_host := inet:hostname() | inet:ip_address(), + origin_port := inet:port_number(), + opts := gun:opts() +} + +init(init_event(), State) -> State +---- + +Gun is initializing. + +=== domain_lookup_start + +[source,erlang] +---- +domain_lookup_event() :: #{ + host := inet:hostname() | inet:ip_address(), + port := inet:port_number(), + tcp_opts := [gen_tcp:connect_option()], + timeout := timeout(), + lookup_info => gun_tcp:lookup_info(), + error => any() +} + +domain_lookup_start(domain_lookup_event(), State) -> State +---- + +Gun is starting to resolve the host address. + +The `lookup_info` and `error` keys are never set +for this event. + +=== domain_lookup_end + +[source,erlang] +---- +domain_lookup_end(domain_lookup_event(), State) -> State +---- + +Gun has finished resolving the host address. + +The `lookup_info` key is only set when the lookup is +successful. The `error` key is set otherwise. + +=== connect_start + +[source,erlang] +---- +connect_event() :: #{ + lookup_info := gun_tcp:lookup_info(), + timeout := timeout(), + socket => inet:socket(), + protocol => http | http2 | socks | raw, + error => any() +} + +connect_start(connect_event(), State) -> State +---- + +Gun is starting to connect to the host address and port. + +The `socket`, `protocol` and `error` keys are never set +for this event. + +=== connect_end + +[source,erlang] +---- +connect_end(connect_event(), State) -> State +---- + +Gun has finished connecting to the host address and port. + +The `socket` key is set on connect success. The `error` +key is set otherwise. + +The `protocol` key is only set when the transport is +`tcp` and the connection is successful. The protocol +is only known in the `tls_handshake_end` event otherwise. + +=== tls_handshake_start + +[source,erlang] +---- +tls_handshake_event() :: #{ + stream_ref => gun:stream_ref(), + reply_to => pid(), + socket := inet:socket() | ssl:sslsocket() | pid(), %% The socket before/after will be different. + tls_opts := [ssl:tls_client_option()], + timeout := timeout(), + protocol => http | http2 | socks | raw, + error => any() +} + +tls_handshake_start(tls_handshake_event(), State) -> State +---- + +Gun has started a TLS handshake. + +This and the `tls_handshake_end` event only occur when +connecting to a TLS server or when upgrading the connection +or a stream to use TLS, for example when using CONNECT or +when connecting to a secure SOCKS server. + +The `stream_ref` and `reply_to` keys are only set when the +TLS handshake occurs as a result of a CONNECT request or +inside an existing CONNECT tunnel. + +The `protocol` and `error` keys are never set for this event. + +=== tls_handshake_end + +[source,erlang] +---- +tls_handshake_end(tls_handshake_event(), State) -> State +---- + +Gun has finished a TLS handshake. + +The `protocol` key is set on TLS handshake success. The +`error` key is set otherwise. + +=== request_start + +[source,erlang] +---- +request_start_event() :: #{ + stream_ref := gun:stream_ref(), + reply_to := pid(), + function := headers | request | ws_upgrade | connect, + method := iodata(), + scheme => binary(), + authority := iodata(), + path => iodata(), + headers := [{binary(), iodata()}] +} + +request_start(request_start_event(), State) -> State +---- + +Gun is starting to send a request. + +The `scheme` and `path` keys are never set when the `function` +is set to `connect`. + +=== request_headers + +[source,erlang] +---- +request_headers(request_start_event(), State) -> State +---- + +Gun has finished sending the request headers. + +=== request_end + +[source,erlang] +---- +request_end_event() :: #{ + stream_ref := gun:stream_ref(), + reply_to := pid() +} + +request_end(request_end_event(), State) -> State +---- + +Gun has finished sending the request. + +=== push_promise_start + +[source,erlang] +---- +push_promise_start_event() :: #{ + stream_ref := gun:stream_ref(), + reply_to := pid() +} + +push_promise_start(push_promise_start_event(), State) -> State +---- + +Gun has begun receiving a promised request (server push). + +=== push_promise_end + +[source,erlang] +---- +push_promise_end_event() :: #{ + stream_ref := gun:stream_ref(), + reply_to := pid(), + promised_stream_ref => gun:stream_ref(), + method := binary(), + uri := binary(), + headers := [{binary(), iodata()}] +} + +push_promise_end(push_promise_end_event(), State) -> State +---- + +Gun has finished receiving a promised request (server push). +Promised requests never include a body. + +Promised requests received during the graceful shutdown of the +connection get canceled immediately. + +// @todo The cancel event should probably trigger as well. + +=== response_start + +[source,erlang] +---- +response_start_event() :: #{ + stream_ref := gun:stream_ref(), + reply_to := pid() +} + +response_start(response_start_event(), State) -> State +---- + +Gun has begun receiving a response. + +=== response_inform + +[source,erlang] +---- +response_headers_event() :: #{ + stream_ref := gun:stream_ref(), + reply_to := pid(), + status := non_neg_integer(), + headers := [{binary(), binary()}] +} + +response_inform(response_headers_event(), State) -> State +---- + +Gun has received an informational response (1xx status code). + +A `status` with value 101 indicates that the response has +concluded as the stream will be upgraded to a new protocol. + +=== response_headers + +[source,erlang] +---- +response_headers(response_headers_event(), State) -> State +---- + +Gun has finished receiving response headers. + +=== response_trailers + +[source,erlang] +---- +response_trailers_event() :: #{ + stream_ref := gun:stream_ref(), + reply_to := pid(), + headers := [{binary(), binary()}] +} + +response_trailers(response_trailers_event(), State) -> State +---- + +Gun has received response trailers. + +=== response_end + +[source,erlang] +---- +response_end_event() :: #{ + stream_ref := gun:stream_ref(), + reply_to := pid() +} + +response_end(response_end_event(), State) -> State +---- + +Gun has finished receiving a response. + +=== ws_upgrade + +[source,erlang] +---- +ws_upgrade_event() :: #{ + stream_ref := gun:stream_ref(), + reply_to := pid(), + opts := gun:ws_opts() +} + +ws_upgrade(ws_upgrade_event(), State) -> State +---- + +A Websocket upgrade was requested. + +Success is indicated by a response (101 informational +if HTTP/1.1, 2xx if HTTP/2) followed by a `protocol_changed` +event. + +=== ws_recv_frame_start + +[source,erlang] +---- +ws_recv_frame_start_event() :: #{ + stream_ref := gun:stream_ref(), + reply_to := pid(), + frag_state := cow_ws:frag_state(), + extensions := cow_ws:extensions() +} + +ws_recv_frame_start(ws_recv_frame_start_event(), State) -> State +---- + +Gun has begun receiving a Websocket frame. + +=== ws_recv_frame_header + +[source,erlang] +---- +ws_recv_frame_header_event() :: #{ + stream_ref := gun:stream_ref(), + reply_to := pid(), + frag_state := cow_ws:frag_state(), + extensions := cow_ws:extensions(), + type := cow_ws:frame_type(), + rsv := cow_ws:rsv(), + len := non_neg_integer(), + mask_key := cow_ws:mask_key() +} + +ws_recv_frame_header(ws_recv_frame_header_event(), State) -> State +---- + +Gun has received the header part of a Websocket frame. + +It will be immediately be followed by the frame's payload. + +=== ws_recv_frame_end + +[source,erlang] +---- +ws_recv_frame_end_event() :: #{ + stream_ref := gun:stream_ref(), + reply_to := pid(), + extensions := cow_ws:extensions(), + close_code := undefined | cow_ws:close_code(), + payload := binary() +} + +ws_recv_frame_end(ws_recv_frame_end_event(), State) -> State +---- + +Gun has finished receiving a Websocket frame. + +=== ws_send_frame_start + +[source,erlang] +---- +ws_send_frame_event() :: #{ + stream_ref := gun:stream_ref(), + reply_to := pid(), + extensions := cow_ws:extensions(), + frame := gun:ws_frame() +} + +ws_send_frame_start(ws_send_frame_event(), State) -> State +---- + +Gun has started sending a Websocket frame. + +=== ws_send_frame_end + +[source,erlang] +---- +ws_send_frame_end(ws_send_frame_event(), State) -> State +---- + +Gun has finished sending a Websocket frame. + +=== protocol_changed + +[source,erlang] +---- +protocol_changed_event() :: #{ + stream_ref := gun:stream_ref(), + protocol := http | http2 | socks | raw | ws +} + +protocol_changed(protocol_changed_event(), State) -> State +---- + +The protocol has changed for either the entire Gun connection +or for a specific stream. + +The `stream_ref` key is only set when the protocol has +changed for a specific stream or inside a CONNECT tunnel. + +This event occurs during successful Websocket upgrades, +as a result of successful CONNECT requests or after a +SOCKS tunnel was successfully established. + +=== origin_changed + +[source,erlang] +---- +origin_changed_event() :: #{ + stream_ref => gun:stream_ref(), + type := connect | socks5, + origin_scheme := binary(), + origin_host := inet:hostname() | inet:ip_address(), + origin_port := inet:port_number() +} + +origin_changed(origin_changed_event(), State) -> State +---- + +The origin server has changed for either the Gun connection +or for a specific stream. + +The `stream_ref` key is only set when the origin has +changed for a specific stream or inside a CONNECT tunnel. + +=== cancel + +[source,erlang] +---- +cancel_event() :: #{ + stream_ref := gun:stream_ref(), + reply_to := pid(), + endpoint := local | remote, + reason := atom() +} + +cancel(cancel_event(), State) -> State +---- + +A stream has been canceled. + +HTTP/1.1 streams can't be canceled at the protocol level. In +this case Gun will silence the stream for the user but events +may still occur. + +HTTP/2 streams can be canceled both by the client and the +server. Events may still occur for a short time after the +stream has been canceled. + +=== disconnect + +[source,erlang] +---- +disconnect_event() :: #{ + reason := normal | closed | {error, any()} +} + +disconnect(disconnect_event(), State) -> State +---- + +Gun has been disconnected from the server. + +=== terminate + +[source,erlang] +---- +terminate_event() :: #{ + state := not_connected + | domain_lookup | connecting | initial_tls_handshake | tls_handshake + | connected | connected_data_only | connected_ws_only, + reason := normal | shutdown | {shutdown, any()} | any() +} + +terminate(terminate_event(), State) -> State +---- + +Gun is terminating. + +== Changelog + +* *2.0*: Module introduced. + +== See also + +link:man:gun(7)[gun(7)], +link:man:gun(3)[gun(3)] diff --git a/gun/doc/src/manual/gun_inform.asciidoc b/gun/doc/src/manual/gun_inform.asciidoc new file mode 100644 index 0000000..33d7971 --- /dev/null +++ b/gun/doc/src/manual/gun_inform.asciidoc @@ -0,0 +1,70 @@ += gun_inform(3) + +== Name + +gun_inform - Informational response + +== Description + +[source,erlang] +---- +{gun_inform, ConnPid, StreamRef, Status, Headers} + +ConnPid :: pid() +StreamRef :: gun:stream_ref() +Status :: 100..199 +Headers :: [{binary(), binary()}] +---- + +Informational response. + +This message informs the relevant process that the server +sent an informational response to the original request. + +Informational responses are only intermediate responses +and provide no guarantees as to what the final response +will be. An informational response always precedes the +response to the original request. + +== Elements + +ConnPid:: + +The pid of the Gun connection process. + +StreamRef:: + +Identifier of the stream for the original request. + +Status:: + +Status code for the informational response. + +Headers:: + +Headers sent with the informational response. + +== Changelog + +* *1.0*: Message introduced. + +== Examples + +.Receive a gun_inform message in a gen_server +[source,erlang] +---- +handle_info({gun_inform, ConnPid, _StreamRef, + _Status, _Headers}, + State=#state{conn_pid=ConnPid}) -> + %% Do something. + {noreply, State}. +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:get(3)[gun:get(3)], +link:man:gun:patch(3)[gun:patch(3)], +link:man:gun:post(3)[gun:post(3)], +link:man:gun:put(3)[gun:put(3)], +link:man:gun_response(3)[gun_response(3)] diff --git a/gun/doc/src/manual/gun_push.asciidoc b/gun/doc/src/manual/gun_push.asciidoc new file mode 100644 index 0000000..fb0b8b1 --- /dev/null +++ b/gun/doc/src/manual/gun_push.asciidoc @@ -0,0 +1,90 @@ += gun_push(3) + +== Name + +gun_push - Server-initiated push + +== Description + +[source,erlang] +---- +{gun_push, ConnPid, StreamRef, NewStreamRef, Method, URI, Headers} + +ConnPid :: pid() +StreamRef :: gun:stream_ref() +NewStreamRef :: gun:stream_ref() +Method :: binary() +URI :: binary() +Headers :: [{binary(), binary()}] +---- + +Server-initiated push. + +This message informs the relevant process that the server +is pushing a resource related to the effective target URI +of the original request. + +A server-initiated push message always precedes the response +to the original request. + +This message will not be sent when using the HTTP/1.1 protocol +because it lacks the concept of server-initiated push. + +== Elements + +ConnPid:: + +The pid of the Gun connection process. + +StreamRef:: + +Identifier of the stream for the original request. + +NewStreamRef:: + +Identifier of the stream being pushed. + +Method:: + +Method of the equivalent HTTP request. + +URI:: + +URI of the resource being pushed. + +Headers:: + +Headers of the equivalent HTTP request. + +== Changelog + +* *1.0*: Message introduced. + +== Examples + +.Receive a gun_push message in a gen_server +[source,erlang] +---- +handle_info({gun_push, ConnPid, _StreamRef, + _NewStreamRef, _Method, _URI, _Headers}, + State=#state{conn_pid=ConnPid}) -> + %% Do something. + {noreply, State}. +---- + +.Cancel an unwanted push +[source,erlang] +---- +handle_info({gun_push, ConnPid, _StreamRef, + NewStreamRef, _Method, _URI, _Headers}, + State=#state{conn_pid=ConnPid}) -> + gun:cancel(ConnPid, NewStreamRef), + {noreply, State}. +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:get(3)[gun:get(3)], +link:man:gun:cancel(3)[gun:cancel(3)], +link:man:gun_response(3)[gun_response(3)] diff --git a/gun/doc/src/manual/gun_response.asciidoc b/gun/doc/src/manual/gun_response.asciidoc new file mode 100644 index 0000000..94edecc --- /dev/null +++ b/gun/doc/src/manual/gun_response.asciidoc @@ -0,0 +1,76 @@ += gun_response(3) + +== Name + +gun_response - Response + +== Description + +[source,erlang] +---- +{gun_response, ConnPid, StreamRef, IsFin, Status, Headers} + +ConnPid :: pid() +StreamRef :: gun:stream_ref() +IsFin :: fin | nofin +Status :: non_neg_integer() +Headers :: [{binary(), binary()}] +---- + +Response. + +This message informs the relevant process that the server +sent a response to the original request. + +== Elements + +ConnPid:: + +The pid of the Gun connection process. + +StreamRef:: + +Identifier of the stream for the original request. + +IsFin:: + +Whether this message terminates the response. + +Status:: + +Status code for the response. + +Headers:: + +Headers sent with the response. + +== Changelog + +* *1.0*: Message introduced. + +== Examples + +.Receive a gun_response message in a gen_server +[source,erlang] +---- +handle_info({gun_response, ConnPid, _StreamRef, + _IsFin, _Status, _Headers}, + State=#state{conn_pid=ConnPid}) -> + %% Do something. + {noreply, State}. +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:get(3)[gun:get(3)], +link:man:gun:head(3)[gun:head(3)], +link:man:gun:patch(3)[gun:patch(3)], +link:man:gun:post(3)[gun:post(3)], +link:man:gun:put(3)[gun:put(3)], +link:man:gun:delete(3)[gun:delete(3)], +link:man:gun:options(3)[gun:options(3)], +link:man:gun:headers(3)[gun:headers(3)], +link:man:gun:request(3)[gun:request(3)], +link:man:gun_inform(3)[gun_inform(3)], +link:man:gun_push(3)[gun_push(3)] diff --git a/gun/doc/src/manual/gun_trailers.asciidoc b/gun/doc/src/manual/gun_trailers.asciidoc new file mode 100644 index 0000000..d195ade --- /dev/null +++ b/gun/doc/src/manual/gun_trailers.asciidoc @@ -0,0 +1,68 @@ += gun_trailers(3) + +== Name + +gun_trailers - Response trailers + +== Description + +[source,erlang] +---- +{gun_trailers, ConnPid, StreamRef, Headers} + +ConnPid :: pid() +StreamRef :: gun:stream_ref() +Headers :: [{binary(), binary()}] +---- + +Response trailers. + +This message informs the relevant process that the server +sent response trailers for the response to the original +request. + +A trailers message terminates the response. + +== Elements + +ConnPid:: + +The pid of the Gun connection process. + +StreamRef:: + +Identifier of the stream for the original request. + +Headers:: + +Trailing headers sent after the response body. + +== Changelog + +* *1.0*: Message introduced. + +== Examples + +.Receive a gun_trailers message in a gen_server +[source,erlang] +---- +handle_info({gun_trailers, ConnPid, _StreamRef, _Headers}, + State=#state{conn_pid=ConnPid}) -> + %% Do something. + {noreply, State}. +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:get(3)[gun:get(3)], +link:man:gun:head(3)[gun:head(3)], +link:man:gun:patch(3)[gun:patch(3)], +link:man:gun:post(3)[gun:post(3)], +link:man:gun:put(3)[gun:put(3)], +link:man:gun:delete(3)[gun:delete(3)], +link:man:gun:options(3)[gun:options(3)], +link:man:gun:headers(3)[gun:headers(3)], +link:man:gun:request(3)[gun:request(3)], +link:man:gun_response(3)[gun_response(3)], +link:man:gun_data(3)[gun_data(3)] diff --git a/gun/doc/src/manual/gun_tunnel_up.asciidoc b/gun/doc/src/manual/gun_tunnel_up.asciidoc new file mode 100644 index 0000000..41fbea8 --- /dev/null +++ b/gun/doc/src/manual/gun_tunnel_up.asciidoc @@ -0,0 +1,66 @@ += gun_tunnel_up(3) + +== Name + +gun_tunnel_up - The tunnel is up + +== Description + +[source,erlang] +---- +{gun_tunnel_up, ConnPid, StreamRef, Protocol} + +ConnPid :: pid() +StreamRef :: gun:stream_ref() | undefined +Protocol :: http | http2 | socks +---- + +The tunnel is up. + +This message informs the owner/calling process that the connection +completed through the SOCKS or CONNECT proxy. + +If Gun is configured to connect to another SOCKS server, then the +connection is not usable yet. One or more +link:man:gun_tunnel_up(3)[gun_tunnel_up(3)] messages will follow. + +== Elements + +ConnPid:: + +The pid of the Gun connection process. + +StreamRef:: + +The stream reference the tunnel is running on, or `undefined` +if there are no underlying stream. + +Protocol:: + +The protocol selected for this connection. It can be used +to determine the capabilities of the server. + +== Changelog + +* *2.0*: Message introduced. + +== Examples + +.Receive a gun_tunnel_up message in a gen_server +[source,erlang] +---- +handle_info({gun_tunnel_up, ConnPid, _StreamRef, _Protocol}, + State=#state{conn_pid=ConnPid}) -> + %% Do something. + {noreply, State}. +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:open(3)[gun:open(3)], +link:man:gun:open_unix(3)[gun:open_unix(3)], +link:man:gun:await_up(3)[gun:await_up(3)], +link:man:gun_up(3)[gun_up(3)], +link:man:gun_down(3)[gun_down(3)], +link:man:gun_error(3)[gun_error(3)] diff --git a/gun/doc/src/manual/gun_up.asciidoc b/gun/doc/src/manual/gun_up.asciidoc new file mode 100644 index 0000000..2c05432 --- /dev/null +++ b/gun/doc/src/manual/gun_up.asciidoc @@ -0,0 +1,66 @@ += gun_up(3) + +== Name + +gun_up - The connection is up + +== Description + +[source,erlang] +---- +{gun_up, ConnPid, Protocol} + +ConnPid :: pid() +Protocol :: http | http2 | raw | socks +---- + +The connection is up. + +This message informs the owner process that the connection or +reconnection completed. + +If Gun is configured to connect to a Socks server, then the +connection is not usable yet. One or more +link:man:gun_tunnel_up(3)[gun_tunnel_up(3)] messages will follow. + +Otherwise, Gun will start processing the messages it received while +waiting for the connection to be up. If this is a reconnection, +then this may not be desirable for all requests. Those requests +should be cancelled when the connection goes down, and any +subsequent messages ignored. + +== Elements + +ConnPid:: + +The pid of the Gun connection process. + +Protocol:: + +The protocol selected for this connection. It can be used +to determine the capabilities of the server. + +== Changelog + +* *1.0*: Message introduced. + +== Examples + +.Receive a gun_up message in a gen_server +[source,erlang] +---- +handle_info({gun_up, ConnPid, _Protocol}, + State=#state{conn_pid=ConnPid}) -> + %% Do something. + {noreply, State}. +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:open(3)[gun:open(3)], +link:man:gun:open_unix(3)[gun:open_unix(3)], +link:man:gun:await_up(3)[gun:await_up(3)], +link:man:gun_tunnel_up(3)[gun_tunnel_up(3)], +link:man:gun_down(3)[gun_down(3)], +link:man:gun_error(3)[gun_error(3)] diff --git a/gun/doc/src/manual/gun_upgrade.asciidoc b/gun/doc/src/manual/gun_upgrade.asciidoc new file mode 100644 index 0000000..eaea701 --- /dev/null +++ b/gun/doc/src/manual/gun_upgrade.asciidoc @@ -0,0 +1,72 @@ += gun_upgrade(3) + +== Name + +gun_upgrade - Successful protocol upgrade + +== Description + +[source,erlang] +---- +{gun_upgrade, ConnPid, StreamRef, Protocols, Headers} + +ConnPid :: pid() +StreamRef :: gun:stream_ref() +Protocols :: [<<"websocket">>] +Headers :: [{binary(), binary()}] +---- + +Successful protocol upgrade. + +This message informs the relevant process that the server +accepted to upgrade to one or more protocols given in the +original request. + +The exact semantics of this message depend on the original +protocol. HTTP/1.1 upgrades apply to the entire connection. +HTTP/2 uses a different mechanism which allows switching +specific streams to a different protocol. + +Gun currently only supports upgrading HTTP/1.1 connections +to the Websocket protocol. + +== Elements + +ConnPid:: + +The pid of the Gun connection process. + +StreamRef:: + +Identifier of the stream that resulted in an upgrade. + +Protocols:: + +List of protocols this stream was upgraded to. + +Headers:: + +Headers sent with the upgrade response. + +== Changelog + +* *1.0*: Message introduced. + +== Examples + +.Receive a gun_upgrade message in a gen_server +[source,erlang] +---- +handle_info({gun_upgrade, ConnPid, _StreamRef, + _Protocols, _Headers}, + State=#state{conn_pid=ConnPid}) -> + %% Do something. + {noreply, State}. +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:ws_upgrade(3)[gun:ws_upgrade(3)], +link:man:gun:ws_send(3)[gun:ws_send(3)], +link:man:gun_ws(3)[gun_ws(3)] diff --git a/gun/doc/src/manual/gun_ws.asciidoc b/gun/doc/src/manual/gun_ws.asciidoc new file mode 100644 index 0000000..4e5abde --- /dev/null +++ b/gun/doc/src/manual/gun_ws.asciidoc @@ -0,0 +1,65 @@ += gun_ws(3) + +== Name + +gun_ws - Websocket frame + +== Description + +[source,erlang] +---- +{gun_ws, ConnPid, StreamRef, Frame} + +ConnPid :: pid() +StreamRef :: gun:stream_ref() +Frame :: close | ping | pong + | {text | binary | close, binary()} + | {close, non_neg_integer(), binary()} + | {ping | pong, binary()} +---- + +Websocket frame. + +This message informs the relevant process that the server +sent the enclosed frame. + +This message can only be sent on streams that were upgraded +to the Websocket protocol. + +== Elements + +ConnPid:: + +The pid of the Gun connection process. + +StreamRef:: + +Identifier of the stream that was upgraded to Websocket. + +Frame:: + +The Websocket frame in question. + +== Changelog + +* *2.0*: Depending on the option `silence_pings`, ping and + pong frames may be sent as well. +* *1.0*: Message introduced. + +== Examples + +.Receive a gun_ws message in a gen_server +[source,erlang] +---- +handle_info({gun_ws, ConnPid, _StreamRef, _Frame}, + State=#state{conn_pid=ConnPid}) -> + %% Do something. + {noreply, State}. +---- + +== See also + +link:man:gun(3)[gun(3)], +link:man:gun:ws_upgrade(3)[gun:ws_upgrade(3)], +link:man:gun:ws_send(3)[gun:ws_send(3)], +link:man:gun_upgrade(3)[gun_upgrade(3)] diff --git a/gun/doc/src/manual/gun_ws_protocol.asciidoc b/gun/doc/src/manual/gun_ws_protocol.asciidoc new file mode 100644 index 0000000..417ba94 --- /dev/null +++ b/gun/doc/src/manual/gun_ws_protocol.asciidoc @@ -0,0 +1,108 @@ += gun_ws_protocol(3) + +== Name + +gun_ws_protocol - Websocket subprotocols + +== Description + +The `gun_ws_protocol` module provides the callback interface +and types for implementing Websocket subprotocols. + +== Callbacks + +Websocket subprotocols implement the following interface. + +=== init + +[source,erlang] +---- +init(ReplyTo, StreamRef, Headers, Opts) -> {ok, State} + +ReplyTo :: pid() +StreamRef :: reference() +Headers :: cow_http:headers() +Opts :: gun:ws_opts() +State :: protocol_state() +---- + +Initialize the Websocket protocol. + +ReplyTo:: + +The pid of the process that owns the stream and to +which messages will be sent to. + +StreamRef:: + +The reference for the stream. Must be sent in messages +to distinguish between different streams. + +Headers:: + +Headers that were sent in the response establishing +the Websocket connection. + +Opts:: + +Websocket options. Custom options can be provided in +the `user_opts` key. + +State:: + +State for the protocol. + +=== handle + +[source,erlang] +---- +handle(Frame, State) -> {ok, FlowDec, State} + +Frame :: cow_ws:frame() +State :: protocol_state() +FlowDec :: non_neg_integer() +---- + +Handle a Websocket frame. + +This callback may receive fragmented frames depending +on the protocol and may need to rebuild the full +frame to process it. + +Frame:: + +Websocket frame. + +State:: + +State for the protocol. + +FlowDec:: + +How many messages were sent. Used to update the flow +control state when the feature is enabled. + +== Types + +=== protocol_state() + +[source,erlang] +---- +protocol_state() :: any() +---- + +State for the protocol. + +As this part of the implementation of the protocol +the type may differ between different Websocket +protocol modules. + +== Changelog + +* *2.0*: Module introduced. + +== See also + +link:man:gun(7)[gun(7)], +link:man:gun(3)[gun(3)], +link:man:gun:ws_upgrade(3)[gun:ws_upgrade(3)] diff --git a/gun/ebin/gun.app b/gun/ebin/gun.app new file mode 100644 index 0000000..e156075 --- /dev/null +++ b/gun/ebin/gun.app @@ -0,0 +1,10 @@ +{application, 'gun', [ + {description, "HTTP/1.1, HTTP/2 and Websocket client for Erlang/OTP."}, + {vsn, "2.1.0"}, + {modules, ['gun','gun_app','gun_conns_sup','gun_content_handler','gun_cookies','gun_cookies_list','gun_data_h','gun_default_event_h','gun_event','gun_http','gun_http2','gun_pool','gun_pool_events_h','gun_pools_sup','gun_protocols','gun_public_suffix','gun_raw','gun_socks','gun_sse_h','gun_sup','gun_tcp','gun_tcp_proxy','gun_tls','gun_tls_proxy','gun_tls_proxy_cb','gun_tls_proxy_http2_connect','gun_tunnel','gun_ws','gun_ws_h','gun_ws_protocol']}, + {registered, [gun_sup]}, + {applications, [kernel,stdlib,public_key,ssl,cowlib]}, + {optional_applications, []}, + {mod, {gun_app, []}}, + {env, []} +]}. \ No newline at end of file diff --git a/gun/src/Makefile b/gun/src/Makefile new file mode 100644 index 0000000..7c1761e --- /dev/null +++ b/gun/src/Makefile @@ -0,0 +1,23 @@ +.PHONY: all +.SUFFIXES: .erl .beam + +ERLC?= erlc -server + +OBJS= gun_ws_protocol.beam gun.beam gun_app.beam gun_conns_sup.beam +OBJS+= gun_cookies.beam gun_cookies_list.beam gun_data_h.beam +OBJS+= gun_default_event_h.beam gun_event.beam gun_http.beam +OBJS+= gun_http2.beam gun_pool.beam gun_pool_events_h.beam gun_pools_sup.beam +OBJS+= gun_protocols.beam gun_public_suffix.beam gun_raw.beam +OBJS+= gun_socks.beam gun_sse_h.beam gun_sup.beam gun_tcp.beam +OBJS+= gun_tcp_proxy.beam gun_tls.beam gun_tls_proxy.beam +OBJS+= gun_tls_proxy_cb.beam gun_tls_proxy_http2_connect.beam +OBJS+= gun_tunnel.beam gun_ws.beam gun_ws_h.beam gun_content_handler.beam + +all: ${OBJS} + +.erl.beam: + ${ERLC} ${ERLOPTS} ${ERLFLAGS} $< + +clean: + rm -f *.beam + diff --git a/gun/src/gun.erl b/gun/src/gun.erl new file mode 100644 index 0000000..b4c1686 --- /dev/null +++ b/gun/src/gun.erl @@ -0,0 +1,1868 @@ +%% Copyright (c) 2013-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun). +-behavior(gen_statem). + +-ifdef(OTP_RELEASE). +-compile({nowarn_deprecated_function, [{erlang, get_stacktrace, 0}]}). +-endif. + +%% Connection. +-export([open/2]). +-export([open/3]). +-export([open_unix/2]). +-export([set_owner/2]). +-export([info/1]). +-export([close/1]). +-export([shutdown/1]). + +%% Requests. +-export([delete/2]). +-export([delete/3]). +-export([delete/4]). +-export([get/2]). +-export([get/3]). +-export([get/4]). +-export([head/2]). +-export([head/3]). +-export([head/4]). +-export([options/2]). +-export([options/3]). +-export([options/4]). +-export([patch/3]). +-export([patch/4]). +-export([patch/5]). +-export([post/3]). +-export([post/4]). +-export([post/5]). +-export([put/3]). +-export([put/4]). +-export([put/5]). + +%% Generic requests interface. +-export([headers/4]). +-export([headers/5]). +-export([request/5]). +-export([request/6]). + +%% Streaming data. +-export([data/4]). + +%% Tunneling. +-export([connect/2]). +-export([connect/3]). +-export([connect/4]). + +%% Cookies. +%% @todo -export([gc_cookies/1]). +%% @todo -export([session_gc_cookies/1]). + +%% Awaiting gun messages. +-export([await/2]). +-export([await/3]). +-export([await/4]). +-export([await_body/2]). +-export([await_body/3]). +-export([await_body/4]). +-export([await_up/1]). +-export([await_up/2]). +-export([await_up/3]). + +%% Flushing gun messages. +-export([flush/1]). + +%% Streams. +-export([update_flow/3]). +-export([cancel/2]). +-export([stream_info/2]). + +%% Websocket. +-export([ws_upgrade/2]). +-export([ws_upgrade/3]). +-export([ws_upgrade/4]). +-export([ws_send/3]). + +%% Internals. +-export([start_link/4]). +-export([callback_mode/0]). +-export([init/1]). +-export([default_transport/1]). +-export([not_connected/3]). +-export([domain_lookup/3]). +-export([connecting/3]). +-export([initial_tls_handshake/3]). +-export([ensure_tls_opts/3]). +-export([tls_handshake/3]). +-export([connected_protocol_init/3]). +-export([connected/3]). +-export([connected_data_only/3]). +-export([connected_no_input/3]). +-export([connected_ws_only/3]). +-export([closing/3]). +-export([terminate/3]). + +-type req_headers() :: [{binary() | string() | atom(), iodata()}] + | #{binary() | string() | atom() => iodata()}. +-export_type([req_headers/0]). + +-type ws_close_code() :: 1000..4999. + +-type ws_frame() :: close | ping | pong + | {text | binary | close | ping | pong, iodata()} + | {close, ws_close_code(), iodata()}. +-export_type([ws_frame/0]). + +-type protocol() :: http | http2 | raw | socks + | {http, http_opts()} | {http2, http2_opts()} | {raw, raw_opts()} | {socks, socks_opts()}. +-export_type([protocol/0]). + +-type protocols() :: [protocol()]. +-export_type([protocols/0]). + +-type stream_ref() :: reference() | [reference()]. +-export_type([stream_ref/0]). + +-type opts() :: #{ + connect_timeout => timeout(), + cookie_store => gun_cookies:store(), + domain_lookup_timeout => timeout(), + event_handler => {module(), any()}, + http_opts => http_opts(), + http2_opts => http2_opts(), + protocols => protocols(), + raw_opts => raw_opts(), + retry => non_neg_integer(), + retry_fun => fun((non_neg_integer(), opts()) + -> #{retries => non_neg_integer(), timeout => pos_integer()}), + retry_timeout => pos_integer(), + socks_opts => socks_opts(), + supervise => boolean(), + tcp_opts => [gen_tcp:connect_option()], + tls_handshake_timeout => timeout(), + tls_opts => [ssl:tls_client_option()], + trace => boolean(), + transport => tcp | tls | ssl, + ws_opts => ws_opts() +}. +-export_type([opts/0]). + +-type connect_destination() :: #{ + host := inet:hostname() | inet:ip_address(), + port := inet:port_number(), + username => iodata(), + password => iodata(), + protocols => protocols(), + transport => tcp | tls, + tls_opts => [ssl:tls_client_option()], + tls_handshake_timeout => timeout() +}. +-export_type([connect_destination/0]). + +-type intermediary() :: #{ + type := connect | socks5, + host := inet:hostname() | inet:ip_address(), + port := inet:port_number(), + transport := tcp | tls | tls_proxy, + protocol := http | socks +}. + +-type tunnel_info() :: #{ + %% Tunnel. + host := inet:hostname() | inet:ip_address(), + port := inet:port_number(), + + %% Origin. + origin_host => inet:hostname() | inet:ip_address(), + origin_port => inet:port_number() +}. +-export_type([tunnel_info/0]). + +-type raw_opts() :: #{ + flow => pos_integer(), + %% Internal. + tunnel_transport => tcp | tls +}. +-export_type([raw_opts/0]). + +-type req_opts() :: #{ + flow => pos_integer(), + reply_to => pid(), + tunnel => stream_ref() +}. +-export_type([req_opts/0]). + +-type http_opts() :: #{ + closing_timeout => timeout(), + content_handlers => gun_content_handler:opt(), + cookie_ignore_informational => boolean(), + flow => pos_integer(), + keepalive => timeout(), + transform_header_name => fun((binary()) -> binary()), + version => 'HTTP/1.1' | 'HTTP/1.0', + + %% Internal. + tunnel_transport => tcp | tls +}. +-export_type([http_opts/0]). + +%% @todo Accept http_opts, http2_opts, and so on. +-type http2_opts() :: #{ + closing_timeout => timeout(), + content_handlers => gun_content_handler:opt(), + cookie_ignore_informational => boolean(), + flow => pos_integer(), + keepalive => timeout(), + keepalive_tolerance => non_neg_integer(), + notify_settings_changed => boolean(), + + %% Options copied from cow_http2_machine. + connection_window_margin_size => 0..16#7fffffff, + connection_window_update_threshold => 0..16#7fffffff, + enable_connect_protocol => boolean(), + initial_connection_window_size => 65535..16#7fffffff, + initial_stream_window_size => 0..16#7fffffff, + max_connection_window_size => 0..16#7fffffff, + max_concurrent_streams => non_neg_integer() | infinity, + max_decode_table_size => non_neg_integer(), + max_encode_table_size => non_neg_integer(), + max_fragmented_header_block_size => 16384..16#7fffffff, + max_frame_size_received => 16384..16777215, + max_frame_size_sent => 16384..16777215 | infinity, + max_stream_window_size => 0..16#7fffffff, + preface_timeout => timeout(), + settings_timeout => timeout(), + stream_window_data_threshold => 0..16#7fffffff, + stream_window_margin_size => 0..16#7fffffff, + stream_window_update_threshold => 0..16#7fffffff, + + %% Internal. + tunnel_transport => tcp | tls +}. +-export_type([http2_opts/0]). + +-type socks_opts() :: #{ + version => 5, + auth => [{username_password, binary(), binary()} | none], + host := inet:hostname() | inet:ip_address(), + port := inet:port_number(), + protocols => protocols(), + transport => tcp | tls, + tls_opts => [ssl:tls_client_option()], + tls_handshake_timeout => timeout(), + + %% Internal. + tunnel_transport => tcp | tls +}. +-export_type([socks_opts/0]). + +-type ws_opts() :: #{ + closing_timeout => timeout(), + compress => boolean(), + default_protocol => module(), + flow => pos_integer(), + keepalive => timeout(), + protocols => [{binary(), module()}], + reply_to => pid(), + silence_pings => boolean(), + tunnel => stream_ref(), + user_opts => any() +}. +-export_type([ws_opts/0]). + +-type resp_headers() :: [{binary(), binary()}]. + +-type await_result() :: {inform, 100..199, resp_headers()} + | {response, fin | nofin, non_neg_integer(), resp_headers()} + | {data, fin | nofin, binary()} + | {sse, cow_sse:event() | fin} + | {trailers, resp_headers()} + | {push, stream_ref(), binary(), binary(), resp_headers()} + | {upgrade, [binary()], resp_headers()} + | {ws, ws_frame()} + | {up, http | http2 | raw | socks} + | {notify, settings_changed, map()} + | {error, {stream_error | connection_error | down, any()} | timeout}. +-export_type([await_result/0]). + +-type await_body_result() :: {ok, binary()} + | {ok, binary(), resp_headers()} + | {error, {stream_error | connection_error | down, any()} | timeout}. +-export_type([await_body_result/0]). + +-record(state, { + owner :: pid(), + status :: {up, reference()} | {down, any()} | shutdown, + host :: inet:hostname() | inet:ip_address(), + port :: inet:port_number(), + origin_scheme :: binary(), + origin_host :: inet:hostname() | inet:ip_address(), + origin_port :: inet:port_number(), + intermediaries = [] :: [intermediary()], + opts :: opts(), + keepalive_ref :: undefined | reference(), + socket :: undefined | inet:socket() | ssl:sslsocket() | pid(), + transport :: module(), + active = true :: boolean(), + messages :: {atom(), atom(), atom()}, + protocol :: module(), + protocol_state :: any(), + cookie_store :: undefined | {module(), any()}, + event_handler :: module(), + event_handler_state :: any() +}). + +%% Connection. + +-spec open(inet:hostname() | inet:ip_address(), inet:port_number()) + -> {ok, pid()} | {error, any()}. +open(Host, Port) -> + open(Host, Port, #{}). + +-spec open(inet:hostname() | inet:ip_address(), inet:port_number(), opts()) + -> {ok, pid()} | {error, any()}. +open(Host, Port, Opts) when is_list(Host); is_atom(Host); is_tuple(Host) -> + do_open(Host, Port, Opts). + +-spec open_unix(Path::string(), opts()) + -> {ok, pid()} | {error, any()}. +open_unix(SocketPath, Opts) -> + do_open({local, SocketPath}, 0, Opts). + +do_open(Host, Port, Opts0) -> + %% We accept both ssl and tls but only use tls in the code. + Opts = case Opts0 of + #{transport := ssl} -> Opts0#{transport => tls}; + _ -> Opts0 + end, + case check_options(maps:to_list(Opts)) of + ok -> + Result = case maps:get(supervise, Opts, true) of + true -> supervisor:start_child(gun_conns_sup, [self(), Host, Port, Opts]); + false -> start_link(self(), Host, Port, Opts) + end, + case Result of + OK = {ok, ServerPid} -> + consider_tracing(ServerPid, Opts), + OK; + StartError -> + StartError + end; + CheckError -> + CheckError + end. + +check_options([]) -> + ok; +check_options([{connect_timeout, infinity}|Opts]) -> + check_options(Opts); +check_options([{connect_timeout, T}|Opts]) when is_integer(T), T >= 0 -> + check_options(Opts); +check_options([{cookie_store, {Mod, _}}|Opts]) when is_atom(Mod) -> + check_options(Opts); +check_options([{domain_lookup_timeout, infinity}|Opts]) -> + check_options(Opts); +check_options([{domain_lookup_timeout, T}|Opts]) when is_integer(T), T >= 0 -> + check_options(Opts); +check_options([{event_handler, {Mod, _}}|Opts]) when is_atom(Mod) -> + check_options(Opts); +check_options([{http_opts, ProtoOpts}|Opts]) when is_map(ProtoOpts) -> + case gun_http:check_options(ProtoOpts) of + ok -> + check_options(Opts); + Error -> + Error + end; +check_options([{http2_opts, ProtoOpts}|Opts]) when is_map(ProtoOpts) -> + case gun_http2:check_options(ProtoOpts) of + ok -> + check_options(Opts); + Error -> + Error + end; +check_options([Opt = {protocols, L}|Opts]) when is_list(L) -> + case check_protocols_opt(L) of + ok -> check_options(Opts); + error -> {error, {options, Opt}} + end; +check_options([{raw_opts, ProtoOpts}|Opts]) when is_map(ProtoOpts) -> + case gun_raw:check_options(ProtoOpts) of + ok -> + check_options(Opts); + Error -> + Error + end; +check_options([{retry, R}|Opts]) when is_integer(R), R >= 0 -> + check_options(Opts); +check_options([{retry_fun, F}|Opts]) when is_function(F, 2) -> + check_options(Opts); +check_options([{retry_timeout, T}|Opts]) when is_integer(T), T >= 0 -> + check_options(Opts); +check_options([{socks_opts, ProtoOpts}|Opts]) when is_map(ProtoOpts) -> + case gun_socks:check_options(ProtoOpts) of + ok -> + check_options(Opts); + Error -> + Error + end; +check_options([{supervise, B}|Opts]) when is_boolean(B) -> + check_options(Opts); +check_options([{tcp_opts, L}|Opts]) when is_list(L) -> + check_options(Opts); +check_options([{tls_handshake_timeout, infinity}|Opts]) -> + check_options(Opts); +check_options([{tls_handshake_timeout, T}|Opts]) when is_integer(T), T >= 0 -> + check_options(Opts); +check_options([{tls_opts, L}|Opts]) when is_list(L) -> + check_options(Opts); +check_options([{trace, B}|Opts]) when is_boolean(B) -> + check_options(Opts); +check_options([{transport, T}|Opts]) when T =:= tcp; T =:= tls -> + check_options(Opts); +check_options([{ws_opts, ProtoOpts}|Opts]) when is_map(ProtoOpts) -> + case gun_ws:check_options(ProtoOpts) of + ok -> + check_options(Opts); + Error -> + Error + end; +check_options([Opt|_]) -> + {error, {options, Opt}}. + +check_protocols_opt(Protocols) -> + %% Protocols must not appear more than once, and they + %% must be one of http, http2 or socks. + ProtoNames0 = lists:usort([case P0 of {P, _} -> P; P -> P end || P0 <- Protocols]), + ProtoNames = [P || P <- ProtoNames0, lists:member(P, [http, http2, raw, socks])], + case length(Protocols) =:= length(ProtoNames) of + false -> error; + true -> + %% When options are given alongside a protocol, they + %% must be checked as well. + TupleCheck = [case P of + {http, Opts} -> gun_http:check_options(Opts); + {http2, Opts} -> gun_http2:check_options(Opts); + {raw, Opts} -> gun_raw:check_options(Opts); + {socks, Opts} -> gun_socks:check_options(Opts) + end || P <- Protocols, is_tuple(P)], + case lists:usort(TupleCheck) of + [] -> ok; + [ok] -> ok; + _ -> error + end + end. + +consider_tracing(ServerPid, #{trace := true}) -> + dbg:tracer(), + _ = dbg:tpl(gun, [{'_', [], [{return_trace}]}]), + _ = dbg:tpl(gun_http, [{'_', [], [{return_trace}]}]), + _ = dbg:tpl(gun_http2, [{'_', [], [{return_trace}]}]), + _ = dbg:tpl(gun_raw, [{'_', [], [{return_trace}]}]), + _ = dbg:tpl(gun_socks, [{'_', [], [{return_trace}]}]), + _ = dbg:tpl(gun_ws, [{'_', [], [{return_trace}]}]), + dbg:p(ServerPid, all); +consider_tracing(_, _) -> + ok. + +-spec set_owner(pid(), pid()) -> ok. +set_owner(ServerPid, NewOwnerPid) -> + gen_statem:cast(ServerPid, {set_owner, self(), NewOwnerPid}). + +-spec info(pid()) -> map(). +info(ServerPid) -> + {_, #state{ + owner=Owner, + socket=Socket, + transport=Transport, + protocol=Protocol, + origin_scheme=OriginScheme, + origin_host=OriginHost, + origin_port=OriginPort, + intermediaries=Intermediaries, + cookie_store=CookieStore + }} = sys:get_state(ServerPid), + Info0 = #{ + owner => Owner, + socket => Socket, + transport => case OriginScheme of + <<"http">> -> tcp; + <<"https">> -> tls + end, + origin_scheme => case Protocol of + gun_raw -> undefined; + gun_socks -> undefined; + _ -> OriginScheme + end, + origin_host => OriginHost, + origin_port => OriginPort, + intermediaries => intermediaries_info(Intermediaries, []), + cookie_store => CookieStore + }, + Info = case Socket of + undefined -> + Info0; + _ -> + case Transport:sockname(Socket) of + {ok, {SockIP, SockPort}} -> + Info0#{ + sock_ip => SockIP, + sock_port => SockPort + }; + {error, _} -> + Info0 + end + end, + case Protocol of + undefined -> Info; + _ -> Info#{protocol => Protocol:name()} + end. + +%% We change tls_proxy into tls for intermediaries. +%% +%% Intermediaries are listed in the order data goes through them, +%% that's why we reverse the order here. +intermediaries_info([], Acc) -> + Acc; +intermediaries_info([Intermediary=#{transport := Transport0}|Tail], Acc) -> + Transport = case Transport0 of + tls_proxy -> tls; + _ -> Transport0 + end, + intermediaries_info(Tail, [Intermediary#{transport => Transport}|Acc]). + +-spec close(pid()) -> ok. +close(ServerPid) -> + supervisor:terminate_child(gun_conns_sup, ServerPid). + +-spec shutdown(pid()) -> ok. +shutdown(ServerPid) -> + gen_statem:cast(ServerPid, shutdown). + +%% Requests. + +-spec delete(pid(), iodata()) -> stream_ref(). +delete(ServerPid, Path) -> + request(ServerPid, <<"DELETE">>, Path, [], <<>>). + +-spec delete(pid(), iodata(), req_headers()) -> stream_ref(). +delete(ServerPid, Path, Headers) -> + request(ServerPid, <<"DELETE">>, Path, Headers, <<>>). + +-spec delete(pid(), iodata(), req_headers(), req_opts()) -> stream_ref(). +delete(ServerPid, Path, Headers, ReqOpts) -> + request(ServerPid, <<"DELETE">>, Path, Headers, <<>>, ReqOpts). + +-spec get(pid(), iodata()) -> stream_ref(). +get(ServerPid, Path) -> + request(ServerPid, <<"GET">>, Path, [], <<>>). + +-spec get(pid(), iodata(), req_headers()) -> stream_ref(). +get(ServerPid, Path, Headers) -> + request(ServerPid, <<"GET">>, Path, Headers, <<>>). + +-spec get(pid(), iodata(), req_headers(), req_opts()) -> stream_ref(). +get(ServerPid, Path, Headers, ReqOpts) -> + request(ServerPid, <<"GET">>, Path, Headers, <<>>, ReqOpts). + +-spec head(pid(), iodata()) -> stream_ref(). +head(ServerPid, Path) -> + request(ServerPid, <<"HEAD">>, Path, [], <<>>). + +-spec head(pid(), iodata(), req_headers()) -> stream_ref(). +head(ServerPid, Path, Headers) -> + request(ServerPid, <<"HEAD">>, Path, Headers, <<>>). + +-spec head(pid(), iodata(), req_headers(), req_opts()) -> stream_ref(). +head(ServerPid, Path, Headers, ReqOpts) -> + request(ServerPid, <<"HEAD">>, Path, Headers, <<>>, ReqOpts). + +-spec options(pid(), iodata()) -> stream_ref(). +options(ServerPid, Path) -> + request(ServerPid, <<"OPTIONS">>, Path, [], <<>>). + +-spec options(pid(), iodata(), req_headers()) -> stream_ref(). +options(ServerPid, Path, Headers) -> + request(ServerPid, <<"OPTIONS">>, Path, Headers, <<>>). + +-spec options(pid(), iodata(), req_headers(), req_opts()) -> stream_ref(). +options(ServerPid, Path, Headers, ReqOpts) -> + request(ServerPid, <<"OPTIONS">>, Path, Headers, <<>>, ReqOpts). + +-spec patch(pid(), iodata(), req_headers()) -> stream_ref(). +patch(ServerPid, Path, Headers) -> + headers(ServerPid, <<"PATCH">>, Path, Headers). + +-spec patch(pid(), iodata(), req_headers(), iodata() | req_opts()) -> stream_ref(). +patch(ServerPid, Path, Headers, ReqOpts) when is_map(ReqOpts) -> + headers(ServerPid, <<"PATCH">>, Path, Headers, ReqOpts); +patch(ServerPid, Path, Headers, Body) -> + request(ServerPid, <<"PATCH">>, Path, Headers, Body). + +-spec patch(pid(), iodata(), req_headers(), iodata(), req_opts()) -> stream_ref(). +patch(ServerPid, Path, Headers, Body, ReqOpts) -> + request(ServerPid, <<"PATCH">>, Path, Headers, Body, ReqOpts). + +-spec post(pid(), iodata(), req_headers()) -> stream_ref(). +post(ServerPid, Path, Headers) -> + headers(ServerPid, <<"POST">>, Path, Headers). + +-spec post(pid(), iodata(), req_headers(), iodata() | req_opts()) -> stream_ref(). +post(ServerPid, Path, Headers, ReqOpts) when is_map(ReqOpts) -> + headers(ServerPid, <<"POST">>, Path, Headers, ReqOpts); +post(ServerPid, Path, Headers, Body) -> + request(ServerPid, <<"POST">>, Path, Headers, Body). + +-spec post(pid(), iodata(), req_headers(), iodata(), req_opts()) -> stream_ref(). +post(ServerPid, Path, Headers, Body, ReqOpts) -> + request(ServerPid, <<"POST">>, Path, Headers, Body, ReqOpts). + +-spec put(pid(), iodata(), req_headers()) -> stream_ref(). +put(ServerPid, Path, Headers) -> + headers(ServerPid, <<"PUT">>, Path, Headers). + +-spec put(pid(), iodata(), req_headers(), iodata() | req_opts()) -> stream_ref(). +put(ServerPid, Path, Headers, ReqOpts) when is_map(ReqOpts) -> + headers(ServerPid, <<"PUT">>, Path, Headers, ReqOpts); +put(ServerPid, Path, Headers, Body) -> + request(ServerPid, <<"PUT">>, Path, Headers, Body). + +-spec put(pid(), iodata(), req_headers(), iodata(), req_opts()) -> stream_ref(). +put(ServerPid, Path, Headers, Body, ReqOpts) -> + request(ServerPid, <<"PUT">>, Path, Headers, Body, ReqOpts). + +%% Generic requests interface. +%% +%% @todo Accept a TargetURI map as well as a normal Path. + +-spec headers(pid(), iodata(), iodata(), req_headers()) -> stream_ref(). +headers(ServerPid, Method, Path, Headers) -> + headers(ServerPid, Method, Path, Headers, #{}). + +-spec headers(pid(), iodata(), iodata(), req_headers(), req_opts()) -> stream_ref(). +headers(ServerPid, Method, Path, Headers0, ReqOpts) -> + Tunnel = get_tunnel(ReqOpts), + StreamRef = make_stream_ref(Tunnel), + InitialFlow = maps:get(flow, ReqOpts, infinity), + ReplyTo = maps:get(reply_to, ReqOpts, self()), + gen_statem:cast(ServerPid, {headers, ReplyTo, StreamRef, + Method, Path, normalize_headers(Headers0), InitialFlow}), + StreamRef. + +-spec request(pid(), iodata(), iodata(), req_headers(), iodata()) -> stream_ref(). +request(ServerPid, Method, Path, Headers, Body) -> + request(ServerPid, Method, Path, Headers, Body, #{}). + +-spec request(pid(), iodata(), iodata(), req_headers(), iodata(), req_opts()) -> stream_ref(). +request(ServerPid, Method, Path, Headers, Body, ReqOpts) -> + Tunnel = get_tunnel(ReqOpts), + StreamRef = make_stream_ref(Tunnel), + InitialFlow = maps:get(flow, ReqOpts, infinity), + ReplyTo = maps:get(reply_to, ReqOpts, self()), + gen_statem:cast(ServerPid, {request, ReplyTo, StreamRef, + Method, Path, normalize_headers(Headers), Body, InitialFlow}), + StreamRef. + +get_tunnel(#{tunnel := Tunnel}) when is_reference(Tunnel) -> + [Tunnel]; +get_tunnel(#{tunnel := Tunnel}) -> + Tunnel; +get_tunnel(_) -> + undefined. + +make_stream_ref(undefined) -> make_ref(); +make_stream_ref(Tunnel) -> Tunnel ++ [make_ref()]. + +normalize_headers([]) -> + []; +normalize_headers([{Name, Value}|Tail]) when is_binary(Name) -> + [{string:lowercase(Name), Value}|normalize_headers(Tail)]; +normalize_headers([{Name, Value}|Tail]) when is_list(Name) -> + [{string:lowercase(unicode:characters_to_binary(Name)), Value}|normalize_headers(Tail)]; +normalize_headers([{Name, Value}|Tail]) when is_atom(Name) -> + [{string:lowercase(atom_to_binary(Name, latin1)), Value}|normalize_headers(Tail)]; +normalize_headers(Headers) when is_map(Headers) -> + normalize_headers(maps:to_list(Headers)). + +%% Streaming data. + +-spec data(pid(), stream_ref(), fin | nofin, iodata()) -> ok. +data(ServerPid, StreamRef, IsFin, Data) -> + case iolist_size(Data) of + 0 when IsFin =:= nofin -> + ok; + _ -> + gen_statem:cast(ServerPid, {data, self(), StreamRef, IsFin, Data}) + end. + +%% Tunneling. + +-spec connect(pid(), connect_destination()) -> stream_ref(). +connect(ServerPid, Destination) -> + connect(ServerPid, Destination, [], #{}). + +-spec connect(pid(), connect_destination(), req_headers()) -> stream_ref(). +connect(ServerPid, Destination, Headers) -> + connect(ServerPid, Destination, Headers, #{}). + +-spec connect(pid(), connect_destination(), req_headers(), req_opts()) -> stream_ref(). +connect(ServerPid, Destination, Headers, ReqOpts) -> + Tunnel = get_tunnel(ReqOpts), + StreamRef = make_stream_ref(Tunnel), + InitialFlow = maps:get(flow, ReqOpts, infinity), + ReplyTo = maps:get(reply_to, ReqOpts, self()), + gen_statem:cast(ServerPid, {connect, ReplyTo, StreamRef, + Destination, Headers, InitialFlow}), + StreamRef. + +%% Awaiting gun messages. + +-spec await(pid(), stream_ref()) -> await_result(). +await(ServerPid, StreamRef) -> + MRef = monitor(process, ServerPid), + Res = await(ServerPid, StreamRef, 5000, MRef), + demonitor(MRef, [flush]), + Res. + +-spec await(pid(), stream_ref(), timeout() | reference()) -> await_result(). +await(ServerPid, StreamRef, MRef) when is_reference(MRef) -> + await(ServerPid, StreamRef, 5000, MRef); +await(ServerPid, StreamRef, Timeout) -> + MRef = monitor(process, ServerPid), + Res = await(ServerPid, StreamRef, Timeout, MRef), + demonitor(MRef, [flush]), + Res. + +-spec await(pid(), stream_ref(), timeout(), reference()) -> await_result(). +await(ServerPid, StreamRef, Timeout, MRef) -> + receive + {gun_inform, ServerPid, StreamRef, Status, Headers} -> + {inform, Status, Headers}; + {gun_response, ServerPid, StreamRef, IsFin, Status, Headers} -> + {response, IsFin, Status, Headers}; + {gun_data, ServerPid, StreamRef, IsFin, Data} -> + {data, IsFin, Data}; + {gun_sse, ServerPid, StreamRef, Event} -> + {sse, Event}; + {gun_trailers, ServerPid, StreamRef, Trailers} -> + {trailers, Trailers}; + {gun_push, ServerPid, StreamRef, NewStreamRef, Method, URI, Headers} -> + {push, NewStreamRef, Method, URI, Headers}; + {gun_upgrade, ServerPid, StreamRef, Protocols, Headers} -> + {upgrade, Protocols, Headers}; + {gun_ws, ServerPid, StreamRef, Frame} -> + {ws, Frame}; + {gun_tunnel_up, ServerPid, StreamRef, Protocol} -> + {up, Protocol}; + {gun_notify, ServerPid, Type, Info} -> + {notify, Type, Info}; + {gun_error, ServerPid, StreamRef, Reason} -> + {error, {stream_error, Reason}}; + {gun_error, ServerPid, Reason} -> + {error, {connection_error, Reason}}; + {'DOWN', MRef, process, ServerPid, Reason} -> + {error, {down, Reason}} + after Timeout -> + {error, timeout} + end. + +-spec await_body(pid(), stream_ref()) -> await_body_result(). +await_body(ServerPid, StreamRef) -> + MRef = monitor(process, ServerPid), + Res = await_body(ServerPid, StreamRef, 5000, MRef, <<>>), + demonitor(MRef, [flush]), + Res. + +-spec await_body(pid(), stream_ref(), timeout() | reference()) -> await_body_result(). +await_body(ServerPid, StreamRef, MRef) when is_reference(MRef) -> + await_body(ServerPid, StreamRef, 5000, MRef, <<>>); +await_body(ServerPid, StreamRef, Timeout) -> + MRef = monitor(process, ServerPid), + Res = await_body(ServerPid, StreamRef, Timeout, MRef, <<>>), + demonitor(MRef, [flush]), + Res. + +-spec await_body(pid(), stream_ref(), timeout(), reference()) -> await_body_result(). +await_body(ServerPid, StreamRef, Timeout, MRef) -> + await_body(ServerPid, StreamRef, Timeout, MRef, <<>>). + +await_body(ServerPid, StreamRef, Timeout, MRef, Acc) -> + receive + {gun_data, ServerPid, StreamRef, nofin, Data} -> + await_body(ServerPid, StreamRef, Timeout, MRef, + << Acc/binary, Data/binary >>); + {gun_data, ServerPid, StreamRef, fin, Data} -> + {ok, << Acc/binary, Data/binary >>}; + %% It's OK to return trailers here because the client + %% specifically requested them. + {gun_trailers, ServerPid, StreamRef, Trailers} -> + {ok, Acc, Trailers}; + {gun_error, ServerPid, StreamRef, Reason} -> + {error, {stream_error, Reason}}; + {gun_error, ServerPid, Reason} -> + {error, {connection_error, Reason}}; + {'DOWN', MRef, process, ServerPid, Reason} -> + {error, {down, Reason}} + after Timeout -> + {error, timeout} + end. + +-spec await_up(pid()) + -> {ok, http | http2 | raw | socks} + | {error, {down, any()} | timeout}. +await_up(ServerPid) -> + MRef = monitor(process, ServerPid), + Res = await_up(ServerPid, 5000, MRef), + demonitor(MRef, [flush]), + Res. + +-spec await_up(pid(), reference() | timeout()) + -> {ok, http | http2 | raw | socks} + | {error, {down, any()} | timeout}. +await_up(ServerPid, MRef) when is_reference(MRef) -> + await_up(ServerPid, 5000, MRef); +await_up(ServerPid, Timeout) -> + MRef = monitor(process, ServerPid), + Res = await_up(ServerPid, Timeout, MRef), + demonitor(MRef, [flush]), + Res. + +-spec await_up(pid(), timeout(), reference()) + -> {ok, http | http2 | raw | socks} + | {error, {down, any()} | timeout}. +await_up(ServerPid, Timeout, MRef) -> + receive + {gun_up, ServerPid, Protocol} -> + {ok, Protocol}; + {'DOWN', MRef, process, ServerPid, Reason} -> + {error, {down, Reason}} + after Timeout -> + {error, timeout} + end. + +%% Flushing gun messages. + +-spec flush(pid() | stream_ref()) -> ok. +flush(ServerPid) when is_pid(ServerPid) -> + flush_pid(ServerPid); +flush(StreamRef) -> + flush_ref(StreamRef). + +-spec flush_pid(pid()) -> ok. +flush_pid(ServerPid) -> + receive + {gun_up, ServerPid, _} -> + flush_pid(ServerPid); + {gun_down, ServerPid, _, _, _} -> + flush_pid(ServerPid); + {gun_inform, ServerPid, _, _, _} -> + flush_pid(ServerPid); + {gun_response, ServerPid, _, _, _, _} -> + flush_pid(ServerPid); + {gun_data, ServerPid, _, _, _} -> + flush_pid(ServerPid); + {gun_trailers, ServerPid, _, _} -> + flush_pid(ServerPid); + {gun_push, ServerPid, _, _, _, _, _, _} -> + flush_pid(ServerPid); + {gun_error, ServerPid, _, _} -> + flush_pid(ServerPid); + {gun_error, ServerPid, _} -> + flush_pid(ServerPid); + {gun_tunnel_up, ServerPid, _, _} -> + flush_pid(ServerPid); + {gun_upgrade, ServerPid, _, _, _} -> + flush_pid(ServerPid); + {gun_ws, ServerPid, _, _} -> + flush_pid(ServerPid); + {'DOWN', _, process, ServerPid, _} -> + flush_pid(ServerPid) + after 0 -> + ok + end. + +-spec flush_ref(stream_ref()) -> ok. +flush_ref(StreamRef) -> + receive + {gun_inform, _, StreamRef, _, _} -> + flush_ref(StreamRef); + {gun_response, _, StreamRef, _, _, _} -> + flush_ref(StreamRef); + {gun_data, _, StreamRef, _, _} -> + flush_ref(StreamRef); + {gun_trailers, _, StreamRef, _} -> + flush_ref(StreamRef); + {gun_push, _, StreamRef, _, _, _, _, _} -> + flush_ref(StreamRef); + {gun_error, _, StreamRef, _} -> + flush_ref(StreamRef); + {gun_tunnel_up, _, StreamRef, _} -> + flush_ref(StreamRef); + {gun_upgrade, _, StreamRef, _, _} -> + flush_ref(StreamRef); + {gun_ws, _, StreamRef, _} -> + flush_ref(StreamRef) + after 0 -> + ok + end. + +%% Flow control. + +-spec update_flow(pid(), stream_ref(), pos_integer()) -> ok. +update_flow(ServerPid, StreamRef, Flow) -> + gen_statem:cast(ServerPid, {update_flow, self(), StreamRef, Flow}). + +%% Cancelling a stream. + +-spec cancel(pid(), stream_ref()) -> ok. +cancel(ServerPid, StreamRef) -> + gen_statem:cast(ServerPid, {cancel, self(), StreamRef}). + +%% Information about a stream. + +-spec stream_info(pid(), stream_ref()) -> {ok, map() | undefined} | {error, not_connected}. +stream_info(ServerPid, StreamRef) -> + gen_statem:call(ServerPid, {stream_info, StreamRef}). + +%% Websocket. + +-spec ws_upgrade(pid(), iodata()) -> stream_ref(). +ws_upgrade(ServerPid, Path) -> + ws_upgrade(ServerPid, Path, []). + +-spec ws_upgrade(pid(), iodata(), req_headers()) -> stream_ref(). +ws_upgrade(ServerPid, Path, Headers) -> + StreamRef = make_ref(), + gen_statem:cast(ServerPid, {ws_upgrade, self(), StreamRef, Path, normalize_headers(Headers)}), + StreamRef. + +-spec ws_upgrade(pid(), iodata(), req_headers(), ws_opts()) -> stream_ref(). +ws_upgrade(ServerPid, Path, Headers, Opts0) -> + Tunnel = get_tunnel(Opts0), + Opts = maps:without([tunnel], Opts0), + ok = gun_ws:check_options(Opts), + StreamRef = make_stream_ref(Tunnel), + ReplyTo = maps:get(reply_to, Opts, self()), + gen_statem:cast(ServerPid, {ws_upgrade, ReplyTo, StreamRef, Path, normalize_headers(Headers), Opts}), + StreamRef. + +-spec ws_send(pid(), stream_ref(), ws_frame() | [ws_frame()]) -> ok. +ws_send(ServerPid, StreamRef, Frames) -> + gen_statem:cast(ServerPid, {ws_send, self(), StreamRef, Frames}). + +%% Internals. + +callback_mode() -> state_functions. + +start_link(Owner, Host, Port, Opts) -> + gen_statem:start_link(?MODULE, {Owner, Host, Port, Opts}, []). + +init({Owner, Host, Port, Opts}) -> + Retry = maps:get(retry, Opts, 5), + OriginTransport = maps:get(transport, Opts, default_transport(Port)), + %% The OriginScheme is not really http when we connect to socks/raw. + %% This is corrected in the gun:info/1 and gun:stream_info/2 functions where applicable. + {OriginScheme, Transport} = case OriginTransport of + tcp -> {<<"http">>, gun_tcp}; + tls -> {<<"https">>, gun_tls} + end, + OwnerRef = monitor(process, Owner), + {EvHandler, EvHandlerState0} = maps:get(event_handler, Opts, + {gun_default_event_h, undefined}), + EvHandlerState = EvHandler:init(#{ + owner => Owner, + transport => OriginTransport, + origin_scheme => OriginScheme, + origin_host => Host, + origin_port => Port, + opts => Opts + }, EvHandlerState0), + CookieStore = maps:get(cookie_store, Opts, undefined), + State = #state{owner=Owner, status={up, OwnerRef}, + host=Host, port=Port, origin_scheme=OriginScheme, + origin_host=Host, origin_port=Port, opts=Opts, + transport=Transport, messages=Transport:messages(), + event_handler=EvHandler, event_handler_state=EvHandlerState, + cookie_store=CookieStore}, + {ok, domain_lookup, State, + {next_event, internal, {retries, Retry, not_connected}}}. + +default_transport(443) -> tls; +default_transport(_) -> tcp. + +not_connected(_, {retries, 0, normal}, State) -> + {stop, normal, State}; +not_connected(_, {retries, 0, Reason}, State) -> + {stop, {shutdown, Reason}, State}; +not_connected(_, {retries, Retries0, _}, State=#state{opts=Opts}) -> + Fun = maps:get(retry_fun, Opts, fun default_retry_fun/2), + #{ + timeout := Timeout, + retries := Retries + } = Fun(Retries0, Opts), + {next_state, domain_lookup, State, + {state_timeout, Timeout, {retries, Retries, not_connected}}}; +not_connected({call, From}, {stream_info, _}, _) -> + {keep_state_and_data, {reply, From, {error, not_connected}}}; +not_connected(Type, Event, State) -> + handle_common(Type, Event, ?FUNCTION_NAME, State). + +default_retry_fun(Retries, Opts) -> + %% We retry immediately after a disconnect. + Timeout = case maps:get(retry, Opts, 5) of + Retries -> 0; + _ -> maps:get(retry_timeout, Opts, 5000) + end, + #{ + retries => Retries - 1, + timeout => Timeout + }. + +domain_lookup(_, {retries, Retries, _}, State=#state{host=Host, port=Port, opts=Opts, + event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> + TransOpts = maps:get(tcp_opts, Opts, [ + {send_timeout, 15000}, + {send_timeout_close, true} + ]), + DomainLookupTimeout = maps:get(domain_lookup_timeout, Opts, infinity), + DomainLookupEvent = #{ + host => Host, + port => Port, + tcp_opts => TransOpts, + timeout => DomainLookupTimeout + }, + EvHandlerState1 = EvHandler:domain_lookup_start(DomainLookupEvent, EvHandlerState0), + case gun_tcp:domain_lookup(Host, Port, TransOpts, DomainLookupTimeout) of + {ok, LookupInfo} -> + EvHandlerState = EvHandler:domain_lookup_end(DomainLookupEvent#{ + lookup_info => LookupInfo + }, EvHandlerState1), + {next_state, connecting, State#state{event_handler_state=EvHandlerState}, + {next_event, internal, {retries, Retries, LookupInfo}}}; + {error, Reason} -> + EvHandlerState = EvHandler:domain_lookup_end(DomainLookupEvent#{ + error => Reason + }, EvHandlerState1), + {next_state, not_connected, State#state{event_handler_state=EvHandlerState}, + {next_event, internal, {retries, Retries, Reason}}} + end; +domain_lookup({call, From}, {stream_info, _}, _) -> + {keep_state_and_data, {reply, From, {error, not_connected}}}; +domain_lookup(Type, Event, State) -> + handle_common(Type, Event, ?FUNCTION_NAME, State). + +connecting(_, {retries, Retries, LookupInfo}, State=#state{opts=Opts, + transport=Transport, event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> + ConnectTimeout = maps:get(connect_timeout, Opts, infinity), + ConnectEvent = #{ + lookup_info => LookupInfo, + timeout => ConnectTimeout + }, + EvHandlerState1 = EvHandler:connect_start(ConnectEvent, EvHandlerState0), + case gun_tcp:connect(LookupInfo, ConnectTimeout) of + {ok, Socket} when Transport =:= gun_tcp -> + [Protocol] = maps:get(protocols, Opts, [http]), + ProtocolName = case Protocol of + {P, _} -> P; + P -> P + end, + EvHandlerState = EvHandler:connect_end(ConnectEvent#{ + socket => Socket, + protocol => ProtocolName + }, EvHandlerState1), + {next_state, connected_protocol_init, + State#state{event_handler_state=EvHandlerState}, + {next_event, internal, {connected, Retries, Socket, Protocol}}}; + {ok, Socket} when Transport =:= gun_tls -> + EvHandlerState = EvHandler:connect_end(ConnectEvent#{ + socket => Socket + }, EvHandlerState1), + {next_state, initial_tls_handshake, State#state{event_handler_state=EvHandlerState}, + {next_event, internal, {retries, Retries, Socket}}}; + {error, Reason} -> + EvHandlerState = EvHandler:connect_end(ConnectEvent#{ + error => Reason + }, EvHandlerState1), + {next_state, not_connected, State#state{event_handler_state=EvHandlerState}, + {next_event, internal, {retries, Retries, Reason}}} + end. + +initial_tls_handshake(_, {retries, Retries, Socket}, State0=#state{opts=Opts, origin_host=OriginHost}) -> + Protocols = maps:get(protocols, Opts, [http2, http]), + HandshakeEvent = #{ + tls_opts => ensure_tls_opts(Protocols, maps:get(tls_opts, Opts, []), OriginHost), + timeout => maps:get(tls_handshake_timeout, Opts, infinity) + }, + case normal_tls_handshake(Socket, State0, HandshakeEvent, Protocols) of + {ok, TLSSocket, Protocol, State} -> + {next_state, connected_protocol_init, State, + {next_event, internal, {connected, Retries, TLSSocket, Protocol}}}; + {error, Reason, State} -> + {next_state, not_connected, State, + {next_event, internal, {retries, Retries, Reason}}} + end. + +ensure_tls_opts(Protocols0, TransOpts0, OriginHost) -> + %% CA certificates. + TransOpts1 = case lists:keymember(cacerts, 1, TransOpts0) of + true -> + TransOpts0; + false -> + case lists:keymember(cacertfile, 1, TransOpts0) of + true -> + TransOpts0; + false -> + %% This function was added in OTP-25. We use it when it is + %% available and keep the previous behavior when it isn't. + case erlang:function_exported(public_key, cacerts_get, 0) of + true -> + [{cacerts, public_key:cacerts_get()}|TransOpts0]; + false -> + TransOpts0 + end + end + end, + %% ALPN. + Protocols = lists:foldl(fun + (http, Acc) -> [<<"http/1.1">>|Acc]; + ({http, _}, Acc) -> [<<"http/1.1">>|Acc]; + (http2, Acc) -> [<<"h2">>|Acc]; + ({http2, _}, Acc) -> [<<"h2">>|Acc]; + (_, Acc) -> Acc + end, [], Protocols0), + TransOpts = [ + {alpn_advertised_protocols, Protocols} + |TransOpts1], + %% SNI. + %% + %% Normally only DNS hostnames are supported for SNI. However, the ssl + %% application itself allows any string through so we do the same. + %% + %% Only add SNI if not already present and OriginHost isn't an IP address. + case lists:keymember(server_name_indication, 1, TransOpts) of + false when is_list(OriginHost) -> + [{server_name_indication, OriginHost}|TransOpts]; + false when is_atom(OriginHost) -> + [{server_name_indication, atom_to_list(OriginHost)}|TransOpts]; + _ -> + TransOpts + end. + +%% Normal TLS handshake. +tls_handshake(internal, {tls_handshake, HandshakeEvent, Protocols, ReplyTo}, + State0=#state{socket=Socket, transport=gun_tcp}) -> + StreamRef = maps:get(stream_ref, HandshakeEvent, undefined), + case normal_tls_handshake(Socket, State0, HandshakeEvent, Protocols) of + {ok, TLSSocket, NewProtocol0, State} -> + NewProtocol1 = gun_protocols:add_stream_ref(NewProtocol0, StreamRef), + NewProtocol = case NewProtocol1 of + {NewProtocolName, NewProtocolOpts} -> {NewProtocolName, NewProtocolOpts#{tunnel_transport => tls}}; + NewProtocolName -> {NewProtocolName, #{tunnel_transport => tls}} + end, + Protocol = gun_protocols:handler(NewProtocol), + ReplyTo ! {gun_tunnel_up, self(), StreamRef, Protocol:name()}, + commands([ + {switch_transport, gun_tls, TLSSocket}, + {switch_protocol, NewProtocol, ReplyTo} + ], State); + {error, Reason, State} -> + commands({error, Reason}, State) + end; +%% TLS over TLS. +tls_handshake(internal, {tls_handshake, + HandshakeEvent0=#{tls_opts := TLSOpts0, timeout := TLSTimeout}, Protocols, ReplyTo}, + State=#state{socket=Socket, transport=Transport, origin_host=OriginHost, origin_port=OriginPort, + event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> + TLSOpts = ensure_tls_opts(Protocols, TLSOpts0, OriginHost), + HandshakeEvent = HandshakeEvent0#{ + tls_opts => TLSOpts, + socket => Socket + }, + EvHandlerState = EvHandler:tls_handshake_start(HandshakeEvent, EvHandlerState0), + {ok, ProxyPid} = gun_tls_proxy:start_link(OriginHost, OriginPort, + TLSOpts, TLSTimeout, Socket, Transport, {HandshakeEvent, Protocols, ReplyTo}), + commands([{switch_transport, gun_tls_proxy, ProxyPid}], State#state{ + socket=ProxyPid, transport=gun_tls_proxy, event_handler_state=EvHandlerState}); +%% When using gun_tls_proxy we need a separate message to know whether +%% the handshake succeeded and whether we need to switch to a different protocol. +tls_handshake(info, {gun_tls_proxy, Socket, {ok, Negotiated}, {HandshakeEvent, Protocols, ReplyTo}}, + State0=#state{socket=Socket, event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> + NewProtocol0 = gun_protocols:negotiated(Negotiated, Protocols), + StreamRef = maps:get(stream_ref, HandshakeEvent, undefined), + NewProtocol1 = gun_protocols:add_stream_ref(NewProtocol0, StreamRef), + NewProtocol = case NewProtocol1 of + {NewProtocolName, NewProtocolOpts} -> {NewProtocolName, NewProtocolOpts#{tunnel_transport => tls}}; + NewProtocolName -> {NewProtocolName, #{tunnel_transport => tls}} + end, + Protocol = gun_protocols:handler(NewProtocol), + ReplyTo ! {gun_tunnel_up, self(), StreamRef, Protocol:name()}, + EvHandlerState = EvHandler:tls_handshake_end(HandshakeEvent#{ + socket => Socket, + protocol => Protocol:name() + }, EvHandlerState0), + commands([{switch_protocol, NewProtocol, ReplyTo}], State0#state{event_handler_state=EvHandlerState}); +tls_handshake(info, {gun_tls_proxy, Socket, Error = {error, Reason}, {HandshakeEvent, _, _}}, + State=#state{socket=Socket, event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> + EvHandlerState = EvHandler:tls_handshake_end(HandshakeEvent#{ + error => Reason + }, EvHandlerState0), + commands([Error], State#state{event_handler_state=EvHandlerState}); +tls_handshake(Type, Event, State) -> + handle_common_connected_no_input(Type, Event, ?FUNCTION_NAME, State). + +normal_tls_handshake(Socket, State=#state{ + origin_host=OriginHost, event_handler=EvHandler, event_handler_state=EvHandlerState0}, + HandshakeEvent0=#{tls_opts := TLSOpts0, timeout := TLSTimeout}, Protocols) -> + TLSOpts = ensure_tls_opts(Protocols, TLSOpts0, OriginHost), + HandshakeEvent = HandshakeEvent0#{ + tls_opts => TLSOpts, + socket => Socket + }, + EvHandlerState1 = EvHandler:tls_handshake_start(HandshakeEvent, EvHandlerState0), + case gun_tls:connect(Socket, TLSOpts, TLSTimeout) of + {ok, TLSSocket} -> + %% This call may return {error,closed} when the socket has + %% been closed by the peer. This should be very rare (due to + %% timing) but can happen for example when client certificates + %% were required but not sent or invalid with some servers. + case ssl:negotiated_protocol(TLSSocket) of + {error, Reason = closed} -> + EvHandlerState = EvHandler:tls_handshake_end(HandshakeEvent#{ + error => Reason + }, EvHandlerState1), + {error, Reason, State#state{event_handler_state=EvHandlerState}}; + NegotiatedProtocol -> + NewProtocol = gun_protocols:negotiated(NegotiatedProtocol, Protocols), + Protocol = gun_protocols:handler(NewProtocol), + EvHandlerState = EvHandler:tls_handshake_end(HandshakeEvent#{ + socket => TLSSocket, + protocol => Protocol:name() + }, EvHandlerState1), + {ok, TLSSocket, NewProtocol, + State#state{event_handler_state=EvHandlerState}} + end; + {error, Reason} -> + EvHandlerState = EvHandler:tls_handshake_end(HandshakeEvent#{ + error => Reason + }, EvHandlerState1), + {error, Reason, State#state{event_handler_state=EvHandlerState}} + end. + +connected_protocol_init(internal, {connected, Retries, Socket, NewProtocol}, + State0=#state{owner=Owner, opts=Opts, transport=Transport}) -> + {Protocol, ProtoOpts} = gun_protocols:handler_and_opts(NewProtocol, Opts), + case Protocol:init(Owner, Socket, Transport, ProtoOpts) of + {error, Reason} -> + {next_state, not_connected, State0, + {next_event, internal, {retries, Retries, Reason}}}; + {ok, StateName, ProtoState} -> + %% @todo Don't send gun_up and gun_down if active/1 fails here. + Owner ! {gun_up, self(), Protocol:name()}, + State1 = State0#state{socket=Socket, protocol=Protocol, protocol_state=ProtoState}, + case active(State1) of + {ok, State2} -> + State = case Protocol:has_keepalive() of + true -> keepalive_timeout(State2); + false -> State2 + end, + {next_state, StateName, State}; + Disconnect -> + Disconnect + end + end. + +connected_no_input(Type, Event, State) -> + handle_common_connected_no_input(Type, Event, ?FUNCTION_NAME, State). + +connected_data_only(cast, Msg, _) + when element(1, Msg) =:= headers; element(1, Msg) =:= request; + element(1, Msg) =:= connect; element(1, Msg) =:= ws_upgrade; + element(1, Msg) =:= ws_send -> + ReplyTo = element(2, Msg), + ReplyTo ! {gun_error, self(), {badstate, + "This connection does not accept new requests to be opened " + "nor does it accept Websocket frames."}}, + keep_state_and_data; +connected_data_only(Type, Event, State) -> + handle_common_connected(Type, Event, ?FUNCTION_NAME, State). + +connected_ws_only(cast, {ws_send, ReplyTo, StreamRef, Frames}, State=#state{ + protocol=Protocol=gun_ws, protocol_state=ProtoState, + event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> + {Commands, EvHandlerState} = Protocol:ws_send(Frames, + ProtoState, dereference_stream_ref(StreamRef, State), + ReplyTo, EvHandler, EvHandlerState0), + commands(Commands, State#state{event_handler_state=EvHandlerState}); +connected_ws_only(cast, Msg, _) + when element(1, Msg) =:= headers; element(1, Msg) =:= request; element(1, Msg) =:= data; + element(1, Msg) =:= connect; element(1, Msg) =:= ws_upgrade -> + ReplyTo = element(2, Msg), + ReplyTo ! {gun_error, self(), {badstate, + "This connection only accepts Websocket frames."}}, + keep_state_and_data; +connected_ws_only(Type, Event, State) -> + handle_common_connected_no_input(Type, Event, ?FUNCTION_NAME, State). + +%% Public HTTP interface. +%% +%% @todo It might be better, internally, to pass around a URIMap +%% containing the target URI, instead of separate Host/Port/PathWithQs. +connected(cast, {headers, ReplyTo, StreamRef, Method, Path, Headers, InitialFlow}, + State=#state{origin_host=Host, origin_port=Port, + protocol=Protocol, protocol_state=ProtoState, cookie_store=CookieStore0, + event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> + {Commands, CookieStore, EvHandlerState} = Protocol:headers(ProtoState, + dereference_stream_ref(StreamRef, State), ReplyTo, + Method, Host, Port, Path, Headers, + InitialFlow, CookieStore0, EvHandler, EvHandlerState0), + commands(Commands, State#state{cookie_store=CookieStore, + event_handler_state=EvHandlerState}); +connected(cast, {request, ReplyTo, StreamRef, Method, Path, Headers, Body, InitialFlow}, + State=#state{origin_host=Host, origin_port=Port, + protocol=Protocol, protocol_state=ProtoState, cookie_store=CookieStore0, + event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> + {Commands, CookieStore, EvHandlerState} = Protocol:request(ProtoState, + dereference_stream_ref(StreamRef, State), ReplyTo, + Method, Host, Port, Path, Headers, Body, + InitialFlow, CookieStore0, EvHandler, EvHandlerState0), + commands(Commands, State#state{cookie_store=CookieStore, + event_handler_state=EvHandlerState}); +connected(cast, {connect, ReplyTo, StreamRef, Destination, Headers, InitialFlow}, + State=#state{origin_host=Host, origin_port=Port, + protocol=Protocol, protocol_state=ProtoState, + event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> + {Commands, EvHandlerState} = Protocol:connect(ProtoState, + dereference_stream_ref(StreamRef, State), ReplyTo, + Destination, #{host => Host, port => Port}, + Headers, InitialFlow, EvHandler, EvHandlerState0), + commands(Commands, State#state{event_handler_state=EvHandlerState}); +%% Public Websocket interface. +connected(cast, {ws_upgrade, ReplyTo, StreamRef, Path, Headers}, State=#state{opts=Opts}) -> + WsOpts = maps:get(ws_opts, Opts, #{}), + connected(cast, {ws_upgrade, ReplyTo, StreamRef, Path, Headers, WsOpts}, State); +connected(cast, {ws_upgrade, ReplyTo, StreamRef, Path, Headers, WsOpts}, + State=#state{origin_host=Host, origin_port=Port, + protocol=Protocol, protocol_state=ProtoState, cookie_store=CookieStore0, + event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> + EvHandlerState1 = EvHandler:ws_upgrade(#{ + stream_ref => StreamRef, + reply_to => ReplyTo, + opts => WsOpts + }, EvHandlerState0), + %% @todo Can fail if HTTP/1.0. + {Commands, CookieStore, EvHandlerState} = Protocol:ws_upgrade(ProtoState, + dereference_stream_ref(StreamRef, State), ReplyTo, + Host, Port, Path, Headers, WsOpts, CookieStore0, EvHandler, EvHandlerState1), + commands(Commands, State#state{cookie_store=CookieStore, + event_handler_state=EvHandlerState}); +%% @todo Maybe better standardize the protocol callbacks argument orders. +connected(cast, {ws_send, ReplyTo, StreamRef, Frames}, State=#state{ + protocol=Protocol, protocol_state=ProtoState, + event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> + {Commands, EvHandlerState} = Protocol:ws_send(Frames, + ProtoState, dereference_stream_ref(StreamRef, State), + ReplyTo, EvHandler, EvHandlerState0), + commands(Commands, State#state{event_handler_state=EvHandlerState}); +connected(Type, Event, State) -> + handle_common_connected(Type, Event, ?FUNCTION_NAME, State). + +%% When the origin is using raw we do not dereference the stream_ref +%% because it expects the full stream_ref to function (there's no +%% other stream involved for this connection). +dereference_stream_ref(StreamRef, #state{protocol=gun_raw}) -> + StreamRef; +dereference_stream_ref(StreamRef, #state{intermediaries=Intermediaries}) -> + %% @todo It would be better to validate with the intermediary's stream_refs. + case length([http || #{protocol := http} <- Intermediaries]) of + 0 -> + StreamRef; + N -> + {_, Tail} = lists:split(N, StreamRef), + case Tail of + [SR] -> SR; + _ -> Tail + end + end. + +%% Switch to the graceful connection close state. +closing(State=#state{protocol=Protocol, protocol_state=ProtoState, + event_handler=EvHandler, event_handler_state=EvHandlerState0}, Reason) -> + {Commands, EvHandlerState} = Protocol:closing(Reason, ProtoState, EvHandler, EvHandlerState0), + commands(Commands, State#state{event_handler_state=EvHandlerState}). + +%% @todo Should explicitly reject ws_send in this state? +closing(state_timeout, closing_timeout, State=#state{status=Status}) -> + Reason = case Status of + shutdown -> shutdown; + {down, _} -> owner_down; + _ -> normal + end, + disconnect(State, Reason); +%% When reconnect is disabled, fail HTTP/Websocket operations immediately. +closing(cast, {headers, ReplyTo, StreamRef, _Method, _Path, _Headers, _InitialFlow}, + State=#state{opts=#{retry := 0}}) -> + ReplyTo ! {gun_error, self(), StreamRef, closing}, + {keep_state, State}; +closing(cast, {request, ReplyTo, StreamRef, _Method, _Path, _Headers, _Body, _InitialFlow}, + State=#state{opts=#{retry := 0}}) -> + ReplyTo ! {gun_error, self(), StreamRef, closing}, + {keep_state, State}; +closing(cast, {connect, ReplyTo, StreamRef, _Destination, _Headers, _InitialFlow}, + State=#state{opts=#{retry := 0}}) -> + ReplyTo ! {gun_error, self(), StreamRef, closing}, + {keep_state, State}; +closing(cast, {ws_upgrade, ReplyTo, StreamRef, _Path, _Headers}, + State=#state{opts=#{retry := 0}}) -> + ReplyTo ! {gun_error, self(), StreamRef, closing}, + {keep_state, State}; +closing(cast, {ws_upgrade, ReplyTo, StreamRef, _Path, _Headers, _WsOpts}, + State=#state{opts=#{retry := 0}}) -> + ReplyTo ! {gun_error, self(), StreamRef, closing}, + {keep_state, State}; +closing(Type, Event, State) -> + handle_common_connected(Type, Event, ?FUNCTION_NAME, State). + +%% Common events when we have a connection. +%% +%% One function accepts new input, the other doesn't. + +%% @todo Do we want to reject ReplyTo if it's not the process +%% who initiated the connection? For both data and cancel. +handle_common_connected(cast, {data, ReplyTo, StreamRef, IsFin, Data}, _, + State=#state{protocol=Protocol, protocol_state=ProtoState, + event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> + {Commands, EvHandlerState} = Protocol:data(ProtoState, + dereference_stream_ref(StreamRef, State), + ReplyTo, IsFin, Data, EvHandler, EvHandlerState0), + commands(Commands, State#state{event_handler_state=EvHandlerState}); +handle_common_connected(info, {timeout, TRef, Name}, _, + State=#state{protocol=Protocol, protocol_state=ProtoState}) -> + Commands = Protocol:timeout(ProtoState, Name, TRef), + commands(Commands, State); +handle_common_connected(Type, Event, StateName, StateData) -> + handle_common_connected_no_input(Type, Event, StateName, StateData). + +%% Socket events. +handle_common_connected_no_input(info, {OK, Socket, Data}, _, + State0=#state{socket=Socket, messages={OK, _, _}, + protocol=Protocol, protocol_state=ProtoState, cookie_store=CookieStore0, + event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> + {Commands, CookieStore, EvHandlerState} = Protocol:handle(Data, + ProtoState, CookieStore0, EvHandler, EvHandlerState0), + maybe_active(commands(Commands, State0#state{cookie_store=CookieStore, + event_handler_state=EvHandlerState})); +handle_common_connected_no_input(info, {Closed, Socket}, _, + State=#state{socket=Socket, messages={_, Closed, _}}) -> + disconnect(State, closed); +handle_common_connected_no_input(info, {Error, Socket, Reason}, _, + State=#state{socket=Socket, messages={_, _, Error}}) -> + disconnect(State, {error, Reason}); +%% Socket events from TLS proxy sockets set up by HTTP/2 CONNECT. +%% We always forward the messages to Protocol:handle_continue. +handle_common_connected_no_input(info, + Msg={gun_tls_proxy, _, _, {handle_continue, StreamRef, _, _}}, _, + State0=#state{protocol=Protocol, protocol_state=ProtoState, cookie_store=CookieStore0, + event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> + {Commands, CookieStore, EvHandlerState} = Protocol:handle_continue( + dereference_stream_ref(StreamRef, State0), + Msg, ProtoState, CookieStore0, EvHandler, EvHandlerState0), + maybe_active(commands(Commands, State0#state{cookie_store=CookieStore, + event_handler_state=EvHandlerState})); +handle_common_connected_no_input(info, {handle_continue, StreamRef, Msg}, _, + State0=#state{protocol=Protocol, protocol_state=ProtoState, cookie_store=CookieStore0, + event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> + {Commands, CookieStore, EvHandlerState} = Protocol:handle_continue( + dereference_stream_ref(StreamRef, State0), + Msg, ProtoState, CookieStore0, EvHandler, EvHandlerState0), + maybe_active(commands(Commands, State0#state{cookie_store=CookieStore, + event_handler_state=EvHandlerState})); +%% Timeouts. +handle_common_connected_no_input(info, keepalive, _, + State=#state{protocol=Protocol, protocol_state=ProtoState0, + event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> + {Commands, EvHandlerState} = Protocol:keepalive(ProtoState0, EvHandler, EvHandlerState0), + commands(Commands, keepalive_timeout(State#state{ + event_handler_state=EvHandlerState})); +handle_common_connected_no_input(cast, {update_flow, ReplyTo, StreamRef, Flow}, _, + State0=#state{protocol=Protocol, protocol_state=ProtoState}) -> + Commands = Protocol:update_flow(ProtoState, ReplyTo, StreamRef, Flow), + maybe_active(commands(Commands, State0)); +handle_common_connected_no_input(cast, {cancel, ReplyTo, StreamRef}, _, + State=#state{protocol=Protocol, protocol_state=ProtoState, + event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> + {Commands, EvHandlerState} = Protocol:cancel(ProtoState, + dereference_stream_ref(StreamRef, State), ReplyTo, EvHandler, EvHandlerState0), + commands(Commands, State#state{event_handler_state=EvHandlerState}); +handle_common_connected_no_input({call, From}, {stream_info, StreamRef}, _, + State=#state{intermediaries=Intermediaries0, protocol=Protocol, protocol_state=ProtoState}) -> + Intermediaries = [I || I=#{protocol := http} <- Intermediaries0], + {keep_state_and_data, {reply, From, + if + %% The stream_ref refers to an intermediary. + length(StreamRef) =< length(Intermediaries) -> + Intermediary = lists:nth(length(StreamRef), lists:reverse(Intermediaries)), + {Intermediaries1, Tail} = lists:splitwith( + fun(Int) -> Int =/= Intermediary end, + lists:reverse(Intermediaries0)), + Tunnel = tunnel_info_from_intermediaries(State, Tail), + {ok, #{ + ref => StreamRef, + reply_to => undefined, %% @todo + state => running, + intermediaries => intermediaries_info(lists:reverse(Intermediaries1), []), + tunnel => Tunnel + }}; + is_reference(StreamRef), Intermediaries =/= [] -> + %% We take all intermediaries up to the first CONNECT intermediary. + {Intermediaries1, Tail} = lists:splitwith( + fun(#{protocol := P}) -> P =:= socks end, + lists:reverse(Intermediaries0)), + Tunnel = tunnel_info_from_intermediaries(State, Tail), + {ok, #{ + ref => StreamRef, + reply_to => undefined, %% @todo + state => running, + intermediaries => intermediaries_info(lists:reverse(Intermediaries1), []), + tunnel => Tunnel + }}; + true -> + case Protocol:stream_info(ProtoState, dereference_stream_ref(StreamRef, State)) of + {ok, undefined} -> + {ok, undefined}; + {ok, Info0} -> + Info = Info0#{ref => StreamRef}, + case Intermediaries0 of + [] -> + {ok, Info}; + _ -> + Tail = maps:get(intermediaries, Info, []), + {ok, Info#{ + intermediaries => intermediaries_info(Intermediaries0, []) ++ Tail + }} + end + end + end + }}; +handle_common_connected_no_input(Type, Event, StateName, State) -> + handle_common(Type, Event, StateName, State). + +maybe_active({keep_state, State0}) -> + case active(State0) of + {ok, State} -> + {keep_state, State}; + Disconnect -> + Disconnect + end; +maybe_active({next_state, closing, State0, Actions}) -> + case active(State0) of + {ok, State} -> + {next_state, closing, State, Actions}; + Disconnect -> + Disconnect + end; +maybe_active(Other) -> + Other. + +active(State=#state{active=false}) -> + {ok, State}; +active(State=#state{socket=Socket, transport=Transport}) -> + case Transport:setopts(Socket, [{active, once}]) of + ok -> + {ok, State}; + {error, closed} -> + disconnect(State, closed); + Error = {error, _} -> + disconnect(State, Error) + end. + +tunnel_info_from_intermediaries(State, Tail) -> + case Tail of + %% If the next endpoint is an intermediary take its infos. + [_, Intermediary|_] -> + #{ + host := IntermediaryHost, + port := IntermediaryPort, + transport := IntermediaryTransport, + protocol := IntermediaryProtocol + } = Intermediary, + #{ + transport => IntermediaryTransport, + protocol => IntermediaryProtocol, + origin_scheme => case IntermediaryTransport of + tcp -> <<"http">>; + tls -> <<"https">> + end, + origin_host => IntermediaryHost, + origin_port => IntermediaryPort + }; + %% Otherwise take the infos from the state. + _ -> + tunnel_info_from_state(State) + end. + +tunnel_info_from_state(#state{origin_scheme=OriginScheme, + origin_host=OriginHost, origin_port=OriginPort, protocol=Proto}) -> + #{ + transport => case OriginScheme of + <<"http">> -> tcp; + <<"https">> -> tls + end, + protocol => Proto:name(), + origin_scheme => case Proto of + gun_raw -> undefined; + _ -> OriginScheme + end, + origin_host => OriginHost, + origin_port => OriginPort + }. + +%% Common events. +handle_common(cast, {set_owner, CurrentOwner, NewOwner}, _, + State=#state{owner=CurrentOwner, status={up, CurrentOwnerRef}}) -> + %% @todo This should probably trigger an event. + demonitor(CurrentOwnerRef, [flush]), + NewOwnerRef = monitor(process, NewOwner), + {keep_state, State#state{owner=NewOwner, status={up, NewOwnerRef}}}; +%% We cannot change the owner when we are shutting down. +handle_common(cast, {set_owner, CurrentOwner, _}, _, #state{owner=CurrentOwner}) -> + CurrentOwner ! {gun_error, self(), {badstate, + "The owner of the connection cannot be changed when the connection is shutting down."}}, + keep_state_and_state; +handle_common(cast, shutdown, StateName, State=#state{ + status=Status, socket=Socket, transport=Transport, protocol=Protocol}) -> + case {Socket, Protocol} of + {undefined, _} -> + {stop, shutdown}; + {_, undefined} -> + %% @todo This is missing the disconnect/terminate events. + Transport:close(Socket), + {stop, shutdown}; + _ when StateName =:= closing, element(1, Status) =:= up -> + {keep_state, status(State, shutdown)}; + _ when StateName =:= closing -> + keep_state_and_data; + _ -> + closing(status(State, shutdown), shutdown) + end; +%% We stop when the owner is down. +%% @todo We need to demonitor/flush when the status is no longer up. +handle_common(info, {'DOWN', OwnerRef, process, Owner, Reason}, StateName, State=#state{ + owner=Owner, status={up, OwnerRef}, socket=Socket, transport=Transport, protocol=Protocol}) -> + case Socket of + undefined -> + owner_down(Reason, State); + _ -> + case Protocol of + undefined -> + %% @todo This is missing the disconnect/terminate events. + Transport:close(Socket), + owner_down(Reason, State); + %% We are already closing so no need to initiate closing again. + _ when StateName =:= closing -> + {keep_state, status(State, {down, Reason})}; + _ -> + closing(status(State, {down, Reason}), owner_down) + end + end; +handle_common({call, From}, _, _, _) -> + {keep_state_and_data, {reply, From, {error, bad_call}}}; +%% We postpone all HTTP/Websocket operations until we are connected. +handle_common(cast, _, StateName, _) when StateName =/= connected -> + {keep_state_and_data, postpone}; +handle_common(Type, Event, StateName, StateData) -> + error_logger:error_msg("Unexpected event in state ~p of type ~p:~n~w~n~p~n", + [StateName, Type, Event, StateData]), + keep_state_and_data. + +commands(Command, State) when not is_list(Command) -> + commands([Command], State); +commands([], State) -> + {keep_state, State}; +commands([close|_], State) -> + disconnect(State, normal); +commands([{closing, Timeout}|_], State) -> + {next_state, closing, keepalive_cancel(State), + {state_timeout, Timeout, closing_timeout}}; +commands([Error={error, _}|_], State) -> + disconnect(State, Error); +commands([{active, Active}|Tail], State) when is_boolean(Active) -> + commands(Tail, State#state{active=Active}); +commands([{state, ProtoState}|Tail], State) -> + commands(Tail, State#state{protocol_state=ProtoState}); +%% Order is important: the origin must be changed before +%% the transport and/or protocol in order to keep track +%% of the intermediaries properly. +commands([{origin, Scheme, Host, Port, Type}|Tail], + State=#state{protocol=Protocol, origin_scheme=IntermediateScheme, + origin_host=IntermediateHost, origin_port=IntermediatePort, intermediaries=Intermediaries, + event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> + EvHandlerState = EvHandler:origin_changed(#{ + type => Type, + origin_scheme => Scheme, + origin_host => Host, + origin_port => Port + }, EvHandlerState0), + Info = #{ + type => Type, + host => IntermediateHost, + port => IntermediatePort, + transport => case IntermediateScheme of + <<"http">> -> tcp; + <<"https">> -> tls + end, + protocol => Protocol:name() + }, + commands(Tail, State#state{origin_scheme=Scheme, + origin_host=Host, origin_port=Port, intermediaries=[Info|Intermediaries], + event_handler_state=EvHandlerState}); +commands([{switch_transport, Transport, Socket}|Tail], State0=#state{ + protocol=Protocol, protocol_state=ProtoState0}) -> + ProtoState = Protocol:switch_transport(Transport, Socket, ProtoState0), + State1 = State0#state{socket=Socket, transport=Transport, + messages=Transport:messages(), protocol_state=ProtoState}, + case active(State1) of + {ok, State} -> + commands(Tail, State); + Disconnect -> + Disconnect + end; +commands([{switch_protocol, NewProtocol, ReplyTo}], State0=#state{ + opts=Opts, socket=Socket, transport=Transport, + event_handler=EvHandler, event_handler_state=EvHandlerState0}) -> + {Protocol, ProtoOpts0} = gun_protocols:handler_and_opts(NewProtocol, Opts), + ProtoOpts = case ProtoOpts0 of + #{tunnel_transport := _} -> ProtoOpts0; + _ -> ProtoOpts0#{tunnel_transport => tcp} + end, + %% @todo Handle error result from Protocol:init/4 + {ok, StateName, ProtoState} = Protocol:init(ReplyTo, Socket, Transport, ProtoOpts), + ProtocolChangedEvent = case ProtoOpts of + #{stream_ref := StreamRef} -> + #{stream_ref => StreamRef, protocol => Protocol:name()}; + _ -> + #{protocol => Protocol:name()} + end, + EvHandlerState = EvHandler:protocol_changed(ProtocolChangedEvent, EvHandlerState0), + %% We cancel the existing keepalive and, depending on the protocol, + %% we enable keepalive again, effectively resetting the timer. + State1 = State0#state{protocol=Protocol, protocol_state=ProtoState, + event_handler_state=EvHandlerState}, + case active(State1) of + {ok, State2} -> + State = keepalive_cancel(State2), + case Protocol:has_keepalive() of + true -> {next_state, StateName, keepalive_timeout(State)}; + false -> {next_state, StateName, State} + end; + Disconnect -> + Disconnect + end; +%% Perform a TLS handshake. +commands([TLSHandshake={tls_handshake, _, _, _}], State) -> + {next_state, tls_handshake, State, + {next_event, internal, TLSHandshake}}. + +disconnect(State0=#state{owner=Owner, status=Status, opts=Opts, + intermediaries=Intermediaries, socket=Socket, transport=Transport0, + protocol=Protocol, protocol_state=ProtoState, + event_handler=EvHandler, event_handler_state=EvHandlerState0}, Reason) -> + EvHandlerState1 = Protocol:close(Reason, ProtoState, EvHandler, EvHandlerState0), + _ = Transport0:close(Socket), + EvHandlerState = EvHandler:disconnect(#{reason => Reason}, EvHandlerState1), + State1 = State0#state{event_handler_state=EvHandlerState}, + case Status of + {down, DownReason} -> + owner_down(DownReason, State1); + shutdown -> + {stop, shutdown, State1}; + {up, _} -> + %% We closed the socket, discard any remaining socket events. + disconnect_flush(State1), + KilledStreams = Protocol:down(ProtoState), + Owner ! {gun_down, self(), Protocol:name(), Reason, KilledStreams}, + Retry = maps:get(retry, Opts, 5), + State2 = keepalive_cancel(State1#state{ + socket=undefined, protocol=undefined, protocol_state=undefined}), + State = case Intermediaries of + [] -> + State2; + _ -> + #{host := OriginHost, port := OriginPort, + transport := OriginTransport} = lists:last(Intermediaries), + {OriginScheme, Transport} = case OriginTransport of + tcp -> {<<"http">>, gun_tcp}; + tls -> {<<"https">>, gun_tls} + end, + State2#state{transport=Transport, origin_scheme=OriginScheme, + origin_host=OriginHost, origin_port=OriginPort, + intermediaries=[]} + end, + {next_state, not_connected, State, + {next_event, internal, {retries, Retry, Reason}}} + end. + +disconnect_flush(State=#state{socket=Socket, messages={OK, Closed, Error}}) -> + receive + {OK, Socket, _} -> disconnect_flush(State); + {Closed, Socket} -> disconnect_flush(State); + {Error, Socket, _} -> disconnect_flush(State) + after 0 -> + ok + end. + +status(State=#state{status={up, OwnerRef}}, NewStatus) -> + demonitor(OwnerRef, [flush]), + State#state{status=NewStatus}; +status(State, NewStatus) -> + State#state{status=NewStatus}. + +keepalive_timeout(State=#state{opts=Opts, protocol=Protocol}) -> + ProtoOpts = maps:get(Protocol:opts_name(), Opts, #{}), + Keepalive = maps:get(keepalive, ProtoOpts, Protocol:default_keepalive()), + KeepaliveRef = case Keepalive of + infinity -> undefined; + %% @todo Maybe change that to a start_timer. + _ -> erlang:send_after(Keepalive, self(), keepalive) + end, + State#state{keepalive_ref=KeepaliveRef}. + +keepalive_cancel(State=#state{keepalive_ref=undefined}) -> + State; +keepalive_cancel(State=#state{keepalive_ref=KeepaliveRef}) -> + _ = erlang:cancel_timer(KeepaliveRef), + %% Flush if we have a keepalive message + receive + keepalive -> ok + after 0 -> + ok + end, + State#state{keepalive_ref=undefined}. + +owner_down(normal, State) -> {stop, normal, State}; +owner_down(shutdown, State) -> {stop, shutdown, State}; +owner_down(Shutdown = {shutdown, _}, State) -> {stop, Shutdown, State}; +owner_down(Reason, State) -> {stop, {shutdown, {owner_down, Reason}}, State}. + +terminate(Reason, StateName, #state{event_handler=EvHandler, + event_handler_state=EvHandlerState, cookie_store=Store}) -> + _ = case Store of + undefined -> ok; + %% Optimization: gun_cookies_list isn't a persistent cookie store. + {gun_cookies_list, _} -> ok; + _ -> gun_cookies:session_gc(Store) + end, + TerminateEvent = #{ + state => StateName, + reason => Reason + }, + EvHandler:terminate(TerminateEvent, EvHandlerState). diff --git a/gun/src/gun_app.erl b/gun/src/gun_app.erl new file mode 100644 index 0000000..34c297a --- /dev/null +++ b/gun/src/gun_app.erl @@ -0,0 +1,30 @@ +%% Copyright (c) 2013-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +%% @private +-module(gun_app). +-behaviour(application). + +%% API. +-export([start/2]). +-export([stop/1]). + +%% API. + +start(_Type, _Args) -> + gun_pools = ets:new(gun_pools, [ordered_set, public, named_table]), + gun_sup:start_link(). + +stop(_State) -> + ok. diff --git a/gun/src/gun_conns_sup.erl b/gun/src/gun_conns_sup.erl new file mode 100644 index 0000000..3535a47 --- /dev/null +++ b/gun/src/gun_conns_sup.erl @@ -0,0 +1,36 @@ +%% Copyright (c) 2013-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_conns_sup). +-behaviour(supervisor). + +%% API. +-export([start_link/0]). + +%% supervisor. +-export([init/1]). + +%% API. + +-spec start_link() -> {ok, pid()}. +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +%% supervisor. + +init([]) -> + Procs = [ + #{id => gun, start => {gun, start_link, []}, restart => temporary} + ], + {ok, {#{strategy => simple_one_for_one}, Procs}}. diff --git a/gun/src/gun_content_handler.erl b/gun/src/gun_content_handler.erl new file mode 100644 index 0000000..c9a2678 --- /dev/null +++ b/gun/src/gun_content_handler.erl @@ -0,0 +1,76 @@ +%% Copyright (c) 2017-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_content_handler). + +-export([init/5]). +-export([handle/3]). +-export([check_option/1]). + +-type opt() :: [module() | {module(), map()}]. +-export_type([opt/0]). + +-type state() :: opt() | [{module(), any()}]. +-export_type([state/0]). + +-callback init(pid(), any(), cow_http:status(), + cow_http:headers(), map()) -> {ok, any()} | disable. +%% @todo Make fin | nofin its own type. +-callback handle(fin | nofin, any(), State) + -> {ok, any(), non_neg_integer(), State} + | {done, non_neg_integer(), State} + when State::any(). + +-spec init(pid(), any(), cow_http:status(), + cow_http:headers(), State) -> State when State::state(). +init(_, _, _, _, []) -> + []; +init(ReplyTo, StreamRef, Status, Headers, [Handler|Tail]) -> + {Mod, Opts} = case Handler of + Tuple = {_, _} -> Tuple; + Atom -> {Atom, #{}} + end, + case Mod:init(ReplyTo, StreamRef, Status, Headers, Opts) of + {ok, State} -> [{Mod, State}|init(ReplyTo, StreamRef, Status, Headers, Tail)]; + disable -> init(ReplyTo, StreamRef, Status, Headers, Tail) + end. + +-spec handle(fin | nofin, any(), State) -> {ok, non_neg_integer(), State} when State::state(). +handle(IsFin, Data, State) -> + handle(IsFin, Data, State, 0, []). + +handle(_, _, [], Flow, Acc) -> + {ok, Flow, lists:reverse(Acc)}; +handle(IsFin, Data0, [{Mod, State0}|Tail], Flow, Acc) -> + case Mod:handle(IsFin, Data0, State0) of + {ok, Data, Inc, State} -> + handle(IsFin, Data, Tail, Flow + Inc, [{Mod, State}|Acc]); + {done, Inc, State} -> + {ok, Flow + Inc, lists:reverse([{Mod, State}|Acc], Tail)} + end. + +-spec check_option(list()) -> ok | error. +check_option([]) -> + error; +check_option(Opt) -> + check_option1(Opt). + +check_option1([]) -> + ok; +check_option1([Atom|Tail]) when is_atom(Atom) -> + check_option1(Tail); +check_option1([{Atom, #{}}|Tail]) when is_atom(Atom) -> + check_option1(Tail); +check_option1(_) -> + error. diff --git a/gun/src/gun_cookies.erl b/gun/src/gun_cookies.erl new file mode 100644 index 0000000..8f6dd8d --- /dev/null +++ b/gun/src/gun_cookies.erl @@ -0,0 +1,668 @@ +%% Copyright (c) 2020-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_cookies). + +-export([add_cookie_header/5]). +-export([domain_match/2]). +-export([gc/1]). +-export([path_match/2]). +-export([query/2]). +-export([session_gc/1]). +-export([set_cookie/5]). +-export([set_cookie_header/7]). + +-ifdef(TEST). +-export([wpt_http_state_test_files/1]). %% Also used in rfc6265bis_SUITE. +-endif. + +-type store_state() :: any(). + +-type store() :: {module(), store_state()}. +-export_type([store/0]). + +-type cookie() :: #{ + name := binary(), + value := binary(), + domain := binary(), + path := binary(), + creation_time := calendar:datetime(), + last_access_time := calendar:datetime(), + expiry_time := calendar:datetime() | infinity, + persistent := boolean(), + host_only := boolean(), + secure_only := boolean(), + http_only := boolean(), + same_site := default | none | strict | lax +}. +-export_type([cookie/0]). + +-callback init(any()) -> store(). + +-callback query(State, uri_string:uri_map()) + -> {ok, [gun_cookies:cookie()], State} + when State::store_state(). + +-callback set_cookie_secure_match(store_state(), #{ + name := binary(), +% secure_only := true, + domain := binary(), + path := binary() +}) -> match | nomatch. + +-callback set_cookie_get_exact_match(State, #{ + name := binary(), + domain := binary(), + host_only := boolean(), + path := binary() +}) -> {ok, cookie(), State} | error + when State::store_state(). + +-callback store(State, cookie()) + -> {ok, State} | {error, any()} + when State::store_state(). + +-callback gc(State) + -> {ok, State} + when State::store_state(). + +-callback session_gc(State) + -> {ok, State} + when State::store_state(). + +-spec add_cookie_header(binary(), iodata(), iodata(), Headers, Store) + -> {Headers, Store} when Headers :: [{binary(), iodata()}], Store :: undefined | store(). +add_cookie_header(_, _, _, Headers, Store=undefined) -> + {Headers, Store}; +add_cookie_header(Scheme, Authority, PathWithQs, Headers0, Store0) -> + #{ + host := Host, + path := Path + } = uri_string:parse([Scheme, <<"://">>, Authority, PathWithQs]), + URIMap = uri_string:normalize(#{ + scheme => Scheme, + host => iolist_to_binary(Host), + path => iolist_to_binary(Path) + }, [return_map]), + {ok, Cookies0, Store} = query(Store0, URIMap), + Headers = case Cookies0 of + [] -> + Headers0; + _ -> + Cookies = [{Name, Value} || #{name := Name, value := Value} <- Cookies0], + %% We put cookies at the end of the headers list as it's the least important header. + Headers0 ++ [{<<"cookie">>, cow_cookie:cookie(Cookies)}] + end, + {Headers, Store}. + +-spec domain_match(binary(), binary()) -> boolean(). +domain_match(String, String) -> + true; +domain_match(String, DomainString) -> + SkipLen = byte_size(String) - byte_size(DomainString) - 1, + case String of + <<_:SkipLen/unit:8, $., DomainString/binary>> -> + case inet:parse_strict_address(binary_to_list(String)) of + {ok, _} -> + false; + {error, einval} -> + true + end; + _ -> + false + end. + +-spec gc(Store) -> {ok, Store} when Store::store(). +gc({Mod, State0}) -> + {ok, State} = Mod:gc(State0), + {ok, {Mod, State}}. + +-spec path_match(binary(), binary()) -> boolean(). +path_match(Path, Path) -> + true; +path_match(ReqPath, CookiePath) -> + Len = byte_size(CookiePath), + CookieLast = binary:last(CookiePath), + case ReqPath of + <> when CookieLast =:= $/ -> + true; + <> -> + true; + _ -> + false + end. + +-ifdef(TEST). +path_match_test_() -> + Tests = [ + {<<"/">>, <<"/">>, true}, + {<<"/path/to/resource">>, <<"/path/to/resource">>, true}, + {<<"/path/">>, <<"/path/">>, true}, + {<<"/path/to/resource">>, <<"/path/">>, true}, + {<<"/path/to/resource">>, <<"/path">>, true}, + {<<"/path/to/resource">>, <<"/path/to/">>, true}, + {<<"/path/to/resource">>, <<"/path/to">>, true}, + {<<"/path/to/resource">>, <<"/pa">>, false}, + {<<"/path/to/resource">>, <<"/pat">>, false}, + {<<"/path/to/resource">>, <<"/path/to/r">>, false}, + {<<"/abc">>, <<"/def">>, false} + ], + [{iolist_to_binary(io_lib:format("(~p,~p)", [PA, PB])), + fun() -> Res = path_match(PA, PB) end} + || {PA, PB, Res} <- Tests]. +-endif. + +%% @todo The given URI must be normalized. +-spec query(Store, uri_string:uri_map()) + -> {ok, [cookie()], Store} + when Store::store(). +query({Mod, State0}, URI) -> + {ok, Cookies0, State} = Mod:query(State0, URI), + Cookies = lists:sort(fun + (#{path := P, creation_time := CTA}, #{path := P, creation_time := CTB}) -> + CTA =< CTB; + (#{path := PA}, #{path := PB}) -> + PA > PB + end, Cookies0), + {ok, Cookies, {Mod, State}}. + +-spec session_gc(Store) -> {ok, Store} when Store::store(). +session_gc({Mod, State0}) -> + {ok, State} = Mod:session_gc(State0), + {ok, {Mod, State}}. + +%% @todo The given URI must be normalized. +-spec set_cookie(Store, uri_string:uri_map(), binary(), binary(), cow_cookie:cookie_attrs()) + -> {ok, Store} | {error, any()} when Store::store(). +set_cookie(_, _, Name, Value, _) when byte_size(Name) + byte_size(Value) > 4096 -> + {error, larger_than_4096_bytes}; +set_cookie(Store, URI=#{host := Host}, Name, Value, Attrs) -> + %% This is where we would add a feature to block cookies (like a blacklist). + CurrentTime = erlang:universaltime(), + Cookie0 = #{ + name => Name, + value => Value, + creation_time => CurrentTime, + last_access_time => CurrentTime + }, + Cookie = case Attrs of + #{max_age := ExpiryTime} -> + Cookie0#{ + persistent => true, + expiry_time => ExpiryTime + }; + #{expires := ExpiryTime} -> + Cookie0#{ + persistent => true, + expiry_time => ExpiryTime + }; + _ -> + Cookie0#{ + persistent => false, + expiry_time => infinity + } + end, + Domain0 = maps:get(domain, Attrs, <<>>), + Domain = case gun_public_suffix:match(Domain0) of + false -> + Domain0; + true when Host =:= Domain0 -> + <<>>; + true -> + {error, domain_is_public_suffix} + end, + case Domain of + <<>> -> + set_cookie(Store, URI, Attrs, Cookie#{ + host_only => true, + domain => Host + }); + Error = {error, _} -> + Error; + _ -> + case domain_match(Host, Domain) of + true -> + set_cookie(Store, URI, Attrs, Cookie#{ + host_only => false, + domain => Domain + }); + false -> + {error, domain_match_failed} + end + end. + +set_cookie(Store, URI, Attrs, Cookie0) -> + Cookie1 = case Attrs of + #{path := Path} -> + Cookie0#{path => Path}; + _ -> + Cookie0#{path => default_path(URI)} + end, + SecureOnly = maps:get(secure, Attrs, false), + case {SecureOnly, maps:get(scheme, URI)} of + {true, <<"http">>} -> + {error, secure_scheme_only}; + _ -> + Cookie = Cookie1#{ + secure_only => SecureOnly, + http_only => maps:get(http_only, Attrs, false) + }, + %% This is where we would drop cookies from non-HTTP APIs. + set_cookie1(Store, URI, Attrs, Cookie) + end. + +default_path(#{path := Path}) -> + case string:split(Path, <<"/">>, trailing) of + [_] -> <<"/">>; + [<<>>, _] -> <<"/">>; + [DefaultPath, _] -> DefaultPath + end; +default_path(_) -> + <<"/">>. + +set_cookie1(Store, URI=#{scheme := <<"http">>}, Attrs, Cookie=#{secure_only := false}) -> + Match = maps:with([name, domain, path], Cookie), + case set_cookie_secure_match(Store, Match) of + match -> + {error, secure_cookie_matches}; + nomatch -> + set_cookie2(Store, URI, Attrs, Cookie) + end; +set_cookie1(Store, URI, Attrs, Cookie) -> + set_cookie2(Store, URI, Attrs, Cookie). + +set_cookie_secure_match({Mod, State}, Match) -> + Mod:set_cookie_secure_match(State, Match). + +set_cookie2(Store, _URI, Attrs, Cookie0) -> + Cookie = Cookie0#{same_site => maps:get(same_site, Attrs, default)}, + %% This is where we would perform the same-site checks. + %% + %% It seems that an option would need to be added to Gun + %% in order to define the "site for cookies" value. It is + %% not the same as the site identified by the URI. Although + %% I do wonder if in the case of server push we may consider + %% the requested URI to be the "site for cookies", at least + %% by default. + %% + %% The URI argument will be used if/when the above gets + %% implemented. + set_cookie3(Store, Attrs, Cookie). + +set_cookie3(Store, Attrs, Cookie=#{name := Name, + host_only := HostOnly, secure_only := SecureOnly}) -> + Path = maps:get(path, Attrs, undefined), + case Name of + <<"__Secure-",_/bits>> when not SecureOnly -> + {error, name_prefix_secure_requires_secure_only}; + <<"__Host-",_/bits>> when not SecureOnly -> + {error, name_prefix_host_requires_secure_only}; + <<"__Host-",_/bits>> when not HostOnly -> + {error, name_prefix_host_requires_host_only}; + <<"__Host-",_/bits>> when Path =/= <<"/">> -> + {error, name_prefix_host_requires_top_level_path}; + _ -> + set_cookie_store(Store, Cookie) + end. + +set_cookie_store(Store0, Cookie) -> + Match = maps:with([name, domain, host_only, path], Cookie), + case set_cookie_get_exact_match(Store0, Match) of + {ok, #{creation_time := CreationTime}, Store} -> + %% This is where we would reject a new non-HTTP cookie + %% if the OldCookie has http_only set to true. + store(Store, Cookie#{creation_time => CreationTime}); + error -> + store(Store0, Cookie) + end. + +set_cookie_get_exact_match({Mod, State0}, Match) -> + case Mod:set_cookie_get_exact_match(State0, Match) of + {ok, Cookie, State} -> + {ok, Cookie, {Mod, State}}; + Error -> + Error + end. + +store({Mod, State0}, Cookie) -> + case Mod:store(State0, Cookie) of + {ok, State} -> + {ok, {Mod, State}}; + %% @todo Is this return value useful? Can't it just return {ok, State}? + Error -> + Error + end. + +-spec set_cookie_header(binary(), iodata(), iodata(), cow_http:status(), + Headers, Store, #{cookie_ignore_informational := boolean()}) + -> {Headers, Store} when Headers :: [{binary(), iodata()}], Store :: undefined | store(). +%% Don't set cookies when cookie store isn't configured. +set_cookie_header(_, _, _, _, _, Store=undefined, _) -> + Store; +%% Ignore cookies set on informational responses when configured to do so. +%% This includes cookies set to Websocket upgrade responses! +set_cookie_header(_, _, _, Status, _, Store, #{cookie_ignore_informational := true}) + when Status >= 100, Status =< 199 -> + Store; +set_cookie_header(Scheme, Authority, PathWithQs, _, Headers, Store0, _) -> + #{host := Host, path := Path} = uri_string:parse([Scheme, <<"://">>, Authority, PathWithQs]), + URIMap = uri_string:normalize(#{ + scheme => Scheme, + host => iolist_to_binary(Host), + path => iolist_to_binary(Path) + }, [return_map]), + SetCookies = [SC || {<<"set-cookie">>, SC} <- Headers], + lists:foldl(fun(SC, Store1) -> + case cow_cookie:parse_set_cookie(SC) of + {ok, N, V, A} -> + case set_cookie(Store1, URIMap, N, V, A) of + {ok, Store} -> Store; + {error, _} -> Store1 + end; + ignore -> + Store1 + end + end, Store0, SetCookies). + +-ifdef(TEST). +gc_test() -> + URIMap = #{scheme => <<"http">>, host => <<"example.org">>, path => <<"/path/to/resource">>}, + Store0 = gun_cookies_list:init(), + %% Add a cookie that expires in 2 seconds. GC. Cookie can be retrieved. + {ok, N0, V0, A0} = cow_cookie:parse_set_cookie(<<"a=b; Path=/; Max-Age=2">>), + {ok, Store1} = set_cookie(Store0, URIMap, N0, V0, A0), + {ok, Store2} = gc(Store1), + {ok, [_], _} = query(Store2, URIMap), + %% Wait 3 seconds. GC. Cookie was removed. + timer:sleep(3000), + {ok, Store} = gc(Store2), + {ok, [], _} = query(Store, URIMap), + ok. + +gc_expiry_time_infinity_test() -> + URIMap = #{scheme => <<"http">>, host => <<"example.org">>, path => <<"/path/to/resource">>}, + Store0 = gun_cookies_list:init(), + %% Add a session cookie. GC. Cookie can be retrieved. + {ok, N0, V0, A0} = cow_cookie:parse_set_cookie(<<"a=b; Path=/">>), + {ok, Store1} = set_cookie(Store0, URIMap, N0, V0, A0), + {ok, Store} = gc(Store1), + {ok, [_], _} = query(Store, URIMap), + ok. + +session_gc_test() -> + URIMap = #{scheme => <<"http">>, host => <<"example.org">>, path => <<"/path/to/resource">>}, + Store0 = gun_cookies_list:init(), + %% Add a persistent and a session cookie. GC session cookies. Only persistent can be retrieved. + {ok, N0, V0, A0} = cow_cookie:parse_set_cookie(<<"s=s; Path=/">>), + {ok, Store1} = set_cookie(Store0, URIMap, N0, V0, A0), + {ok, N1, V1, A1} = cow_cookie:parse_set_cookie(<<"p=p; Path=/; Max-Age=999">>), + {ok, Store2} = set_cookie(Store1, URIMap, N1, V1, A1), + {ok, Store} = session_gc(Store2), + {ok, [#{name := <<"p">>}], _} = query(Store, URIMap), + ok. + +%% Most of the tests for this module are converted from the +%% Web platform test suite. At the time of writing they could +%% be found at https://github.com/web-platform-tests/wpt/tree/master/cookies +%% +%% Some of the tests use files from wpt directly, namely the +%% http-state ones. They are copied to the test/wpt/cookies directory. + +-define(HOST, "web-platform.test"). + +%% WPT: domain/domain-attribute-host-with-and-without-leading-period +%% WPT: domain/domain-attribute-host-with-leading-period +wpt_domain_with_and_without_leading_period_test() -> + URIMap = #{scheme => <<"http">>, host => <>, path => <<"/path/to/resource">>}, + Store0 = gun_cookies_list:init(), + %% Add a cookie with a leading period in the domain. Cookie can be retrieved. + {ok, N0, V0, A0} = cow_cookie:parse_set_cookie(<<"a=b; Path=/; Domain=." ?HOST>>), + {ok, Store1} = set_cookie(Store0, URIMap, N0, V0, A0), + {ok, [#{value := <<"b">>}], _} = query(Store1, URIMap), + {ok, [#{value := <<"b">>}], _} = query(Store1, URIMap#{host => <<"sub." ?HOST>>}), + %% Add a cookie without a leading period in the domain. Overrides the existing cookie. + {ok, N1, V1, A1} = cow_cookie:parse_set_cookie(<<"a=c; Path=/; Domain=" ?HOST>>), + {ok, Store} = set_cookie(Store1, URIMap, N1, V1, A1), + {ok, [#{value := <<"c">>}], _} = query(Store, URIMap), + {ok, [#{value := <<"c">>}], _} = query(Store, URIMap#{host => <<"sub." ?HOST>>}), + ok. + +%% WPT: domain/domain-attribute-matches-host +wpt_domain_matches_host_test() -> + URIMap = #{scheme => <<"http">>, host => <>, path => <<"/path/to/resource">>}, + Store0 = gun_cookies_list:init(), + %% Add a cookie without a leading period in the domain. Cookie can be retrieved. + {ok, N1, V1, A1} = cow_cookie:parse_set_cookie(<<"a=c; Path=/; Domain=" ?HOST>>), + {ok, Store} = set_cookie(Store0, URIMap, N1, V1, A1), + {ok, [#{value := <<"c">>}], _} = query(Store, URIMap), + {ok, [#{value := <<"c">>}], _} = query(Store, URIMap#{host => <<"sub." ?HOST>>}), + ok. + +%% WPT: domain/domain-attribute-missing +wpt_domain_missing_test() -> + URIMap = #{scheme => <<"http">>, host => <>, path => <<"/path/to/resource">>}, + Store0 = gun_cookies_list:init(), + %% Add a cookie without a domain attribute. Cookie is not sent on subdomains. + {ok, N1, V1, A1} = cow_cookie:parse_set_cookie(<<"a=c; Path=/">>), + {ok, Store} = set_cookie(Store0, URIMap, N1, V1, A1), + {ok, [#{value := <<"c">>}], _} = query(Store, URIMap), + {ok, [], _} = query(Store, URIMap#{host => <<"sub." ?HOST>>}), + ok. + +%% WPT: http-state/*-tests +wpt_http_state_test_files() -> + wpt_http_state_test_files("test/"). + +wpt_http_state_test_files(TestPath) -> + filelib:wildcard(TestPath ++ "wpt/cookies/*-test") -- [ + TestPath ++ "wpt/cookies/attribute0023-test", %% Doesn't match the spec (path override). + TestPath ++ "wpt/cookies/disabled-chromium0020-test", %% Maximum cookie name of 4096 characters. + TestPath ++ "wpt/cookies/disabled-chromium0022-test" %% Nonsense. + ]. + +wpt_http_state_test_() -> + URIMap0 = #{scheme => <<"http">>, host => <<"home.example.org">>, path => <<"/cookie-parser">>}, + TestFiles = wpt_http_state_test_files(), + [{F, fun() -> + {ok, Test} = file:read_file(F), + %% We don't want the final empty line. + Lines = lists:reverse(tl(lists:reverse(string:split(Test, <<"\n">>, all)))), + {Store, URIMap2} = lists:foldl(fun + (<<"Set-Cookie: ",SetCookie/bits>>, Acc={Store0, URIMap1}) -> + case cow_cookie:parse_set_cookie(SetCookie) of + {ok, N, V, A} -> + %% We use the URIMap that corresponds to the request. + case set_cookie(Store0, URIMap0, N, V, A) of + {ok, Store1} -> {Store1, URIMap1}; + {error, _} -> Acc + end; + ignore -> + Acc + end; + (<<"Set-Cookie:">>, Acc) -> + Acc; + (<<"Location: ",Location/bits>>, {Store0, URIMap1}) -> + {Store0, maps:merge(URIMap1, uri_string:normalize(Location, [return_map]))} + end, {gun_cookies_list:init(), URIMap0}, Lines), + %% We must change the URI if it wasn't already changed by the test. + URIMap = case URIMap2 of + URIMap0 -> maps:merge(URIMap0, uri_string:normalize(<<"/cookie-parser-result">>, [return_map])); + _ -> URIMap2 + end, + {ok, Cookies, _} = query(Store, URIMap), + case file:read_file(iolist_to_binary(string:replace(F, <<"-test">>, <<"-expected">>))) of + {ok, ExpectedFile} when ExpectedFile =:= <<>>; ExpectedFile =:= <<"\n">> -> + [] = Cookies, + ok; + {ok, <<"Cookie: ",CookiesBin0/bits>>} -> + %% We only care about the first line. + [CookiesBin, <<>>|_] = string:split(CookiesBin0, <<"\n">>, all), + CookiesBin = iolist_to_binary(cow_cookie:cookie( + [{Name, Value} || #{name := Name, value := Value} <- Cookies])), + ok + end + end} || F <- TestFiles]. + +%% WPT: path/default +wpt_path_default_test() -> + URIMap = #{scheme => <<"http">>, host => <>, path => <<"/path/to/resource">>}, + Store0 = gun_cookies_list:init(), + %% Add a cookie without a path attribute. + {ok, N1, V1, A1} = cow_cookie:parse_set_cookie(<<"cookies-path-default=1">>), + {ok, Store1} = set_cookie(Store0, URIMap, N1, V1, A1), + %% Confirm the cookie was stored with the proper default path, + %% and gets sent for the same path, other resources at the same level or child paths. + {ok, [#{path := <<"/path/to">>}], _} = query(Store1, URIMap), + {ok, [#{path := <<"/path/to">>}], _} = query(Store1, URIMap#{path => <<"/path/to/other">>}), + {ok, [#{path := <<"/path/to">>}], _} = query(Store1, URIMap#{path => <<"/path/to/resource/sub">>}), + %% Confirm that the cookie cannot be retrieved for parent or unrelated paths. + {ok, [], _} = query(Store1, URIMap#{path => <<"/path">>}), + {ok, [], _} = query(Store1, URIMap#{path => <<"/path/toon">>}), + {ok, [], _} = query(Store1, URIMap#{path => <<"/">>}), + %% Expire the cookie. + {ok, N2, V2, A2} = cow_cookie:parse_set_cookie(<<"cookies-path-default=1; Max-Age=0">>), + {ok, Store} = set_cookie(Store1, URIMap, N2, V2, A2), + {ok, [], _} = query(Store, URIMap), + {ok, [], _} = query(Store, URIMap#{path => <<"/path/to/other">>}), + {ok, [], _} = query(Store, URIMap#{path => <<"/path/to/resource/sub">>}), + ok. + +%% WPT: path/match +wpt_path_match_test_() -> + URIMap = #{ + scheme => <<"http">>, + host => <>, + path => <<"/cookies/resources/echo-cookie.html">> + }, + MatchTests = [ + <<"/">>, + <<"match.html">>, + <<"cookies">>, + <<"/cookies">>, + <<"/cookies/">>, + <<"/cookies/resources/echo-cookie.html">> + ], + NegTests = [ + <<"/cook">>, + <<"/w/">> + ], + [{P, fun() -> + {ok, N1, V1, A1} = cow_cookie:parse_set_cookie(<<"a=b; Path=",P/binary>>), + {ok, Store0} = set_cookie(gun_cookies_list:init(), URIMap, N1, V1, A1), + {ok, [#{name := <<"a">>}], _} = query(Store0, URIMap), + {ok, N2, V2, A2} = cow_cookie:parse_set_cookie(<<"a=b; Max-Age=0; Path=",P/binary>>), + {ok, Store} = set_cookie(Store0, URIMap, N2, V2, A2), + {ok, [], _} = query(Store, URIMap) + end} || P <- MatchTests] + ++ + [{P, fun() -> + {ok, N, V, A} = cow_cookie:parse_set_cookie(<<"a=b; Path=",P/binary>>), + {ok, Store} = set_cookie(gun_cookies_list:init(), URIMap, N, V, A), + {ok, [], _} = query(Store, URIMap) + end} || P <- NegTests]. + +%% WPT: prefix/__host.header +wpt_prefix_host_test_() -> + Tests = [ + {<<"http">>, <<"__Host-foo=bar; Path=/;">>, false}, + {<<"http">>, <<"__Host-foo=bar; Path=/;domain=" ?HOST>>, false}, + {<<"http">>, <<"__Host-foo=bar; Path=/;Max-Age=10">>, false}, + {<<"http">>, <<"__Host-foo=bar; Path=/;HttpOnly">>, false}, + {<<"http">>, <<"__Host-foo=bar; Secure; Path=/;">>, false}, + {<<"http">>, <<"__Host-foo=bar; Secure; Path=/;domain=" ?HOST>>, false}, + {<<"http">>, <<"__Host-foo=bar; Secure; Path=/;Max-Age=10">>, false}, + {<<"http">>, <<"__Host-foo=bar; Secure; Path=/;HttpOnly">>, false}, + {<<"http">>, <<"__Host-foo=bar; Secure; Path=/; Domain=" ?HOST "; ">>, false}, + {<<"http">>, <<"__Host-foo=bar; Secure; Path=/; Domain=" ?HOST "; domain=" ?HOST>>, false}, + {<<"http">>, <<"__Host-foo=bar; Secure; Path=/; Domain=" ?HOST "; Max-Age=10">>, false}, + {<<"http">>, <<"__Host-foo=bar; Secure; Path=/; Domain=" ?HOST "; HttpOnly">>, false}, + {<<"http">>, <<"__Host-foo=bar; Secure; Path=/cookies/resources/list.py">>, false}, + {<<"https">>, <<"__Host-foo=bar; Path=/;">>, false}, + {<<"https">>, <<"__Host-foo=bar; Path=/;Max-Age=10">>, false}, + {<<"https">>, <<"__Host-foo=bar; Path=/;HttpOnly">>, false}, + {<<"https">>, <<"__Host-foo=bar; Secure; Path=/;">>, true}, + {<<"https">>, <<"__Host-foo=bar; Secure; Path=/;Max-Age=10">>, true}, + {<<"https">>, <<"__Host-foo=bar; Secure; Path=/;HttpOnly">>, true}, + {<<"https">>, <<"__Host-foo=bar; Secure; Path=/; Domain=" ?HOST "; ">>, false}, + {<<"https">>, <<"__Host-foo=bar; Secure; Path=/; Domain=" ?HOST "; Max-Age=10">>, false}, + {<<"https">>, <<"__Host-foo=bar; Secure; Path=/; Domain=" ?HOST "; HttpOnly">>, false}, + {<<"https">>, <<"__Host-foo=bar; Secure; Path=/cookies/resources/list.py">>, false} + ], + wpt_prefix_common(Tests, <<"__Host-foo">>). + +%% WPT: prefix/__secure.header +wpt_prefix_secure_test_() -> + Tests = [ + {<<"http">>, <<"__Secure-foo=bar; Path=/;">>, false}, + {<<"http">>, <<"__Secure-foo=bar; Path=/;domain=" ?HOST>>, false}, + {<<"http">>, <<"__Secure-foo=bar; Path=/;Max-Age=10">>, false}, + {<<"http">>, <<"__Secure-foo=bar; Path=/;HttpOnly">>, false}, + {<<"http">>, <<"__Secure-foo=bar; Secure; Path=/;">>, false}, + {<<"http">>, <<"__Secure-foo=bar; Secure; Path=/;domain=" ?HOST>>, false}, + {<<"http">>, <<"__Secure-foo=bar; Secure; Path=/;Max-Age=10">>, false}, + {<<"http">>, <<"__Secure-foo=bar; Secure; Path=/;HttpOnly">>, false}, + {<<"https">>, <<"__Secure-foo=bar; Path=/;">>, false}, + {<<"https">>, <<"__Secure-foo=bar; Path=/;Max-Age=10">>, false}, + {<<"https">>, <<"__Secure-foo=bar; Path=/;HttpOnly">>, false}, + {<<"https">>, <<"__Secure-foo=bar; Secure; Path=/;">>, true}, + {<<"https">>, <<"__Secure-foo=bar; Secure; Path=/;Max-Age=10">>, true}, + {<<"https">>, <<"__Secure-foo=bar; Secure; Path=/;HttpOnly">>, true} + %% Missing two SameSite cases from prefix/__secure.header.https. (Not implemented.) + ], + wpt_prefix_common(Tests, <<"__Secure-foo">>). + +wpt_prefix_common(Tests, Name) -> + URIMap0 = #{ + host => <>, + path => <<"/cookies/resources/set.py">> + }, + [{<>, fun() -> + URIMap1 = URIMap0#{scheme => S}, + {ok, N, V, A} = cow_cookie:parse_set_cookie(H), + case set_cookie(gun_cookies_list:init(), URIMap1, N, V, A) of + {ok, Store} when Res =:= true -> + URIMap = URIMap1#{path => <<"/cookies/resources/list.py">>}, + {ok, [#{name := Name}], _} = query(Store, URIMap), + ok; + {error, _} -> + ok + end + end} || {S, H, Res} <- Tests]. + +%% WPT: samesite-none-secure/ (Not implemented.) +%% WPT: samesite/ (Not implemented.) + +wpt_secure_https_test() -> + URIMap = #{ + scheme => <<"https">>, + host => <>, + path => <<"/cookies/secure/any.html">> + }, + {ok, N, V, A} = cow_cookie:parse_set_cookie(<<"secure_from_secure_http=1; Secure; Path=/">>), + {ok, Store} = set_cookie(gun_cookies_list:init(), URIMap, N, V, A), + {ok, [#{name := <<"secure_from_secure_http">>}], _} = query(Store, URIMap), + ok. + +wpt_secure_http_test() -> + URIMap = #{ + scheme => <<"http">>, + host => <>, + path => <<"/cookies/secure/any.html">> + }, + {ok, N, V, A} = cow_cookie:parse_set_cookie(<<"secure_from_nonsecure_http=1; Secure; Path=/">>), + {error, secure_scheme_only} = set_cookie(gun_cookies_list:init(), URIMap, N, V, A), + ok. + +%% WPT: secure/set-from-ws* (Anything special required?) +-endif. diff --git a/gun/src/gun_cookies_list.erl b/gun/src/gun_cookies_list.erl new file mode 100644 index 0000000..e7c588d --- /dev/null +++ b/gun/src/gun_cookies_list.erl @@ -0,0 +1,144 @@ +%% Copyright (c) 2020-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +%% A reference cookie store implemented as a list of cookies. +%% This cookie store cannot be shared between connections. +-module(gun_cookies_list). +-behavior(gun_cookies). + +-export([init/0]). +-export([init/1]). +-export([query/2]). +-export([set_cookie_secure_match/2]). +-export([set_cookie_get_exact_match/2]). +-export([store/2]). +-export([gc/1]). +-export([session_gc/1]). + +-type state() :: #{ + cookies := [gun_cookies:cookie()] +%% @todo max_cookies_per_domain => non_neg_integer() | infinity, +%% @todo max_cookies => non_neg_integer() | infinity +}. + +-type opts() :: #{ +}. +-export_type([opts/0]). + +-spec init() -> {?MODULE, state()}. +init() -> + init(#{}). + +-spec init(opts()) -> {?MODULE, state()}. +init(_Opts) -> + {?MODULE, #{cookies => []}}. + +-spec query(State, uri_string:uri_map()) + -> {ok, [gun_cookies:cookie()], State} + when State::state(). +query(State=#{cookies := Cookies}, URI) -> + CurrentTime = erlang:universaltime(), + query(State, URI, Cookies, CurrentTime, [], []). + +query(State, _, [], _, CookieList, Cookies) -> + {ok, CookieList, State#{cookies => Cookies}}; +query(State, URI=#{scheme := Scheme, host := Host, path := Path}, + [Cookie|Tail], CurrentTime, CookieList, Acc) -> + Match0 = case Cookie of + #{host_only := true, domain := Host} -> + true; + #{host_only := false, domain := Domain} -> + gun_cookies:domain_match(Host, Domain); + _ -> + false + end, + Match1 = Match0 andalso + gun_cookies:path_match(Path, maps:get(path, Cookie)), + Match = Match1 andalso + case {Cookie, Scheme} of + {#{secure_only := true}, <<"https">>} -> true; + {#{secure_only := false}, _} -> true; + _ -> false + end, + %% This is where we would check the http_only flag should + %% we want to implement a non-HTTP interface. + %% This is where we would check for same-site/cross-site. + case Match of + true -> + UpdatedCookie = Cookie#{last_access_time => CurrentTime}, + query(State, URI, Tail, CurrentTime, + [UpdatedCookie|CookieList], + [UpdatedCookie|Acc]); + false -> + query(State, URI, Tail, CurrentTime, CookieList, [Cookie|Acc]) + end. + +-spec set_cookie_secure_match(state(), #{ + name := binary(), +% secure_only := true, + domain := binary(), + path := binary() +}) -> match | nomatch. +set_cookie_secure_match(#{cookies := Cookies}, + #{name := Name, domain := Domain, path := Path}) -> + Result = [Cookie || Cookie=#{name := CookieName, secure_only := true} <- Cookies, + CookieName =:= Name, + gun_cookies:domain_match(Domain, maps:get(domain, Cookie)) + orelse gun_cookies:domain_match(maps:get(domain, Cookie), Domain), + gun_cookies:path_match(Path, maps:get(path, Cookie))], + case Result of + [] -> nomatch; + _ -> match + end. + +-spec set_cookie_get_exact_match(State, #{ + name := binary(), + domain := binary(), + host_only := boolean(), + path := binary() +}) -> {ok, gun_cookies:cookie(), State} | error when State::state(). +set_cookie_get_exact_match(State=#{cookies := Cookies0}, Match) -> + Result = [Cookie || Cookie <- Cookies0, + Match =:= maps:with([name, domain, host_only, path], Cookie)], + Cookies = [Cookie || Cookie <- Cookies0, + Match =/= maps:with([name, domain, host_only, path], Cookie)], + case Result of + [] -> error; + [Cookie] -> {ok, Cookie, State#{cookies => Cookies}} + end. + +-spec store(State, gun_cookies:cookie()) + -> {ok, State} | {error, any()} + when State::state(). +store(State=#{cookies := Cookies}, NewCookie=#{expiry_time := ExpiryTime}) -> + CurrentTime = erlang:universaltime(), + if + %% Do not store cookies with an expiry time in the past. + ExpiryTime =/= infinity, CurrentTime >= ExpiryTime -> + {ok, State}; + true -> + {ok, State#{cookies => [NewCookie|Cookies]}} + end. + +-spec gc(State) -> {ok, State} when State::state(). +gc(State=#{cookies := Cookies0}) -> + CurrentTime = erlang:universaltime(), + Cookies = [C || C=#{expiry_time := ExpiryTime} <- Cookies0, + (ExpiryTime =:= infinity) orelse (ExpiryTime >= CurrentTime)], + {ok, State#{cookies => Cookies}}. + +-spec session_gc(State) -> {ok, State} when State::state(). +session_gc(State=#{cookies := Cookies0}) -> + Cookies = [C || C=#{persistent := true} <- Cookies0], + {ok, State#{cookies => Cookies}}. diff --git a/gun/src/gun_data_h.erl b/gun/src/gun_data_h.erl new file mode 100644 index 0000000..17019d2 --- /dev/null +++ b/gun/src/gun_data_h.erl @@ -0,0 +1,33 @@ +%% Copyright (c) 2017-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_data_h). +-behavior(gun_content_handler). + +-export([init/5]). +-export([handle/3]). + +-record(state, { + reply_to :: pid(), + stream_ref :: gun:stream_ref() +}). + +-spec init(pid(), gun:stream_ref(), _, _, _) -> {ok, #state{}}. +init(ReplyTo, StreamRef, _, _, _) -> + {ok, #state{reply_to=ReplyTo, stream_ref=StreamRef}}. + +-spec handle(fin | nofin, binary(), State) -> {done, 1, State} when State::#state{}. +handle(IsFin, Data, State=#state{reply_to=ReplyTo, stream_ref=StreamRef}) -> + ReplyTo ! {gun_data, self(), StreamRef, IsFin, Data}, + {done, 1, State}. diff --git a/gun/src/gun_default_event_h.erl b/gun/src/gun_default_event_h.erl new file mode 100644 index 0000000..93244db --- /dev/null +++ b/gun/src/gun_default_event_h.erl @@ -0,0 +1,129 @@ +%% Copyright (c) 2019-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_default_event_h). +-behavior(gun_event). + +-export([init/2]). +-export([domain_lookup_start/2]). +-export([domain_lookup_end/2]). +-export([connect_start/2]). +-export([connect_end/2]). +-export([tls_handshake_start/2]). +-export([tls_handshake_end/2]). +-export([request_start/2]). +-export([request_headers/2]). +-export([request_end/2]). +-export([push_promise_start/2]). +-export([push_promise_end/2]). +-export([response_start/2]). +-export([response_inform/2]). +-export([response_headers/2]). +-export([response_trailers/2]). +-export([response_end/2]). +-export([ws_upgrade/2]). +-export([ws_recv_frame_start/2]). +-export([ws_recv_frame_header/2]). +-export([ws_recv_frame_end/2]). +-export([ws_send_frame_start/2]). +-export([ws_send_frame_end/2]). +-export([protocol_changed/2]). +-export([origin_changed/2]). +-export([cancel/2]). +-export([disconnect/2]). +-export([terminate/2]). + +init(_EventData, State) -> + State. + +domain_lookup_start(_EventData, State) -> + State. + +domain_lookup_end(_EventData, State) -> + State. + +connect_start(_EventData, State) -> + State. + +connect_end(_EventData, State) -> + State. + +tls_handshake_start(_EventData, State) -> + State. + +tls_handshake_end(_EventData, State) -> + State. + +request_start(_EventData, State) -> + State. + +request_headers(_EventData, State) -> + State. + +request_end(_EventData, State) -> + State. + +push_promise_start(_EventData, State) -> + State. + +push_promise_end(_EventData, State) -> + State. + +response_start(_EventData, State) -> + State. + +response_inform(_EventData, State) -> + State. + +response_headers(_EventData, State) -> + State. + +response_trailers(_EventData, State) -> + State. + +response_end(_EventData, State) -> + State. + +ws_upgrade(_EventData, State) -> + State. + +ws_recv_frame_start(_EventData, State) -> + State. + +ws_recv_frame_header(_EventData, State) -> + State. + +ws_recv_frame_end(_EventData, State) -> + State. + +ws_send_frame_start(_EventData, State) -> + State. + +ws_send_frame_end(_EventData, State) -> + State. + +protocol_changed(_EventData, State) -> + State. + +origin_changed(_EventData, State) -> + State. + +cancel(_EventData, State) -> + State. + +disconnect(_EventData, State) -> + State. + +terminate(_EventData, State) -> + State. diff --git a/gun/src/gun_event.erl b/gun/src/gun_event.erl new file mode 100644 index 0000000..8fc90bf --- /dev/null +++ b/gun/src/gun_event.erl @@ -0,0 +1,316 @@ +%% Copyright (c) 2019-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_event). + +%% init. + +-type init_event() :: #{ + owner := pid(), + transport := tcp | tls, + origin_scheme := binary(), + origin_host := inet:hostname() | inet:ip_address(), + origin_port := inet:port_number(), + opts := gun:opts() +}. +-export_type([init_event/0]). + +-callback init(init_event(), State) -> State. + +%% domain_lookup_start/domain_lookup_end. + +-type domain_lookup_event() :: #{ + host := inet:hostname() | inet:ip_address(), + port := inet:port_number(), + tcp_opts := [gen_tcp:connect_option()], + timeout := timeout(), + lookup_info => gun_tcp:lookup_info(), + error => any() +}. +-export_type([domain_lookup_event/0]). + +-callback domain_lookup_start(domain_lookup_event(), State) -> State. +-callback domain_lookup_end(domain_lookup_event(), State) -> State. + +%% connect_start/connect_end. + +-type connect_event() :: #{ + lookup_info := gun_tcp:lookup_info(), + timeout := timeout(), + socket => inet:socket(), + protocol => http | http2 | socks | raw, %% Only when transport is tcp. + error => any() +}. +-export_type([connect_event/0]). + +-callback connect_start(connect_event(), State) -> State. +-callback connect_end(connect_event(), State) -> State. + +%% tls_handshake_start/tls_handshake_end. +%% +%% These events occur when connecting to a TLS server or when +%% upgrading the connection or stream to use TLS, for example +%% using CONNECT. The stream_ref/reply_to values are only +%% present when the TLS handshake occurs in the scope of a request. + +-type tls_handshake_event() :: #{ + stream_ref => gun:stream_ref(), + reply_to => pid(), + socket := inet:socket() | ssl:sslsocket() | pid(), %% The socket before/after will be different. + tls_opts := [ssl:tls_client_option()], + timeout := timeout(), + protocol => http | http2 | socks | raw, + error => any() +}. +-export_type([tls_handshake_event/0]). + +-callback tls_handshake_start(tls_handshake_event(), State) -> State. +-callback tls_handshake_end(tls_handshake_event(), State) -> State. + +%% request_start/request_headers. + +-type request_start_event() :: #{ + stream_ref := gun:stream_ref(), + reply_to := pid(), + function := headers | request | ws_upgrade, %% @todo connect? + method := iodata(), + scheme => binary(), + authority := iodata(), + path => iodata(), + headers := [{binary(), iodata()}] +}. +-export_type([request_start_event/0]). + +-callback request_start(request_start_event(), State) -> State. +-callback request_headers(request_start_event(), State) -> State. + +%% request_end. + +-type request_end_event() :: #{ + stream_ref := gun:stream_ref(), + reply_to := pid() +}. +-export_type([request_end_event/0]). + +-callback request_end(request_end_event(), State) -> State. + +%% push_promise_start. + +-type push_promise_start_event() :: #{ + stream_ref := gun:stream_ref(), + reply_to := pid() +}. +-export_type([push_promise_start_event/0]). + +-callback push_promise_start(push_promise_start_event(), State) -> State. + +%% push_promise_end. + +-type push_promise_end_event() :: #{ + stream_ref := gun:stream_ref(), + reply_to := pid(), + %% No stream is created if we receive the push_promise while + %% in the process of gracefully shutting down the connection. + %% The promised stream is canceled immediately. + promised_stream_ref => gun:stream_ref(), + method := binary(), + uri := binary(), + headers := [{binary(), iodata()}] +}. +-export_type([push_promise_end_event/0]). + +-callback push_promise_end(push_promise_end_event(), State) -> State. + +%% response_start. + +-type response_start_event() :: #{ + stream_ref := gun:stream_ref(), + reply_to := pid() +}. +-export_type([response_start_event/0]). + +-callback response_start(response_start_event(), State) -> State. + +%% response_inform/response_headers. + +-type response_headers_event() :: #{ + stream_ref := gun:stream_ref(), + reply_to := pid(), + status := non_neg_integer(), + headers := [{binary(), binary()}] +}. +-export_type([response_headers_event/0]). + +-callback response_inform(response_headers_event(), State) -> State. +-callback response_headers(response_headers_event(), State) -> State. + +%% response_trailers. + +-type response_trailers_event() :: #{ + stream_ref := gun:stream_ref(), + reply_to := pid(), + headers := [{binary(), binary()}] +}. +-export_type([response_trailers_event/0]). + +-callback response_trailers(response_trailers_event(), State) -> State. + +%% response_end. + +-type response_end_event() :: #{ + stream_ref := gun:stream_ref(), + reply_to := pid() +}. +-export_type([response_end_event/0]). + +-callback response_end(response_end_event(), State) -> State. + +%% ws_upgrade. +%% +%% This event is a signal that the following request and response +%% result from a gun:ws_upgrade/2,3,4 call. +%% +%% There is no corresponding "end" event. Instead, the success is +%% indicated by a protocol_changed event following the informational +%% response. + +-type ws_upgrade_event() :: #{ + stream_ref := gun:stream_ref(), + reply_to := pid(), + opts := gun:ws_opts() +}. +-export_type([ws_upgrade_event/0]). + +-callback ws_upgrade(ws_upgrade_event(), State) -> State. + +%% ws_recv_frame_start. + +-type ws_recv_frame_start_event() :: #{ + stream_ref := gun:stream_ref(), + reply_to := pid(), + frag_state := cow_ws:frag_state(), + extensions := cow_ws:extensions() +}. +-export_type([ws_recv_frame_start_event/0]). + +-callback ws_recv_frame_start(ws_recv_frame_start_event(), State) -> State. + +%% ws_recv_frame_header. + +-type ws_recv_frame_header_event() :: #{ + stream_ref := gun:stream_ref(), + reply_to := pid(), + frag_state := cow_ws:frag_state(), + extensions := cow_ws:extensions(), + type := cow_ws:frame_type(), + rsv := cow_ws:rsv(), + len := non_neg_integer(), + mask_key := cow_ws:mask_key() +}. +-export_type([ws_recv_frame_header_event/0]). + +-callback ws_recv_frame_header(ws_recv_frame_header_event(), State) -> State. + +%% ws_recv_frame_end. + +-type ws_recv_frame_end_event() :: #{ + stream_ref := gun:stream_ref(), + reply_to := pid(), + extensions := cow_ws:extensions(), + close_code := undefined | cow_ws:close_code(), + payload := binary() +}. +-export_type([ws_recv_frame_end_event/0]). + +-callback ws_recv_frame_end(ws_recv_frame_end_event(), State) -> State. + +%% ws_send_frame_start/ws_send_frame_end. + +-type ws_send_frame_event() :: #{ + stream_ref := gun:stream_ref(), + reply_to := pid(), + extensions := cow_ws:extensions(), + frame := gun:ws_frame() +}. +-export_type([ws_send_frame_event/0]). + +-callback ws_send_frame_start(ws_send_frame_event(), State) -> State. +-callback ws_send_frame_end(ws_send_frame_event(), State) -> State. + +%% protocol_changed. +%% +%% This event can occur either following a successful ws_upgrade +%% event, following a successful CONNECT request or a SOCKS tunnel. + +-type protocol_changed_event() :: #{ + stream_ref => gun:stream_ref(), + protocol := http | http2 | socks | raw | ws +}. +-export_type([protocol_changed_event/0]). + +-callback protocol_changed(protocol_changed_event(), State) -> State. + +%% origin_changed. + +-type origin_changed_event() :: #{ + stream_ref => gun:stream_ref(), + type := connect | socks5, + origin_scheme := binary(), + origin_host := inet:hostname() | inet:ip_address(), + origin_port := inet:port_number() +}. +-export_type([origin_changed_event/0]). + +-callback origin_changed(origin_changed_event(), State) -> State. + +%% cancel. +%% +%% In the case of HTTP/1.1 we cannot actually cancel the stream, +%% we only silence the stream to the user. Further response events +%% may therefore be received and they provide a useful metric as +%% these canceled requests monopolize the connection. +%% +%% For HTTP/2 both the client and the server may cancel streams. +%% Events may still occur for a short time after the cancel. + +-type cancel_event() :: #{ + stream_ref := gun:stream_ref(), + reply_to := pid(), + endpoint := local | remote, + reason := atom() +}. +-export_type([cancel_event/0]). + +-callback cancel(cancel_event(), State) -> State. + +%% disconnect. + +-type disconnect_event() :: #{ + reason := normal | closed | {error, any()} +}. +-export_type([disconnect_event/0]). + +-callback disconnect(disconnect_event(), State) -> State. + +%% terminate. + +-type terminate_event() :: #{ + state := not_connected + | domain_lookup | connecting | initial_tls_handshake | tls_handshake + | connected | connected_data_only | connected_ws_only, + reason := normal | shutdown | {shutdown, any()} | any() +}. +-export_type([terminate_event/0]). + +-callback terminate(terminate_event(), State) -> State. diff --git a/gun/src/gun_http.erl b/gun/src/gun_http.erl new file mode 100644 index 0000000..58f4ed6 --- /dev/null +++ b/gun/src/gun_http.erl @@ -0,0 +1,1058 @@ +%% Copyright (c) 2014-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_http). + +-export([check_options/1]). +-export([name/0]). +-export([opts_name/0]). +-export([has_keepalive/0]). +-export([default_keepalive/0]). +-export([init/4]). +-export([switch_transport/3]). +-export([handle/5]). +-export([update_flow/4]). +-export([closing/4]). +-export([close/4]). +-export([keepalive/3]). +-export([headers/12]). +-export([request/13]). +-export([data/7]). +-export([connect/9]). +-export([cancel/5]). +-export([stream_info/2]). +-export([down/1]). +-export([ws_upgrade/11]). + +%% Functions shared with gun_http2 and gun_pool. +-export([host_header/3]). + +-type io() :: head | {body, non_neg_integer()} | body_close | body_chunked | body_trailer. + +%% @todo Make that a record. +-type connect_info() :: {connect, gun:stream_ref(), gun:connect_destination()}. + +-record(websocket, { + ref :: gun:stream_ref(), + reply_to :: pid(), + key :: binary(), + extensions :: [binary()], + opts :: gun:ws_opts() +}). + +-record(stream, { + ref :: gun:stream_ref() | connect_info() | #websocket{}, + reply_to :: pid(), + flow :: integer() | infinity, + method :: binary(), + + %% Request target URI. + authority :: iodata(), + path :: iodata(), + + is_alive :: boolean(), + handler_state :: undefined | gun_content_handler:state() +}). + +-record(http_state, { + socket :: inet:socket() | ssl:sslsocket(), + transport :: module(), + opts = #{} :: gun:http_opts(), + version = 'HTTP/1.1' :: cow_http:version(), + connection = keepalive :: keepalive | close, + buffer = <<>> :: binary(), + + %% Base stream ref, defined when the protocol runs + %% inside an HTTP/2 CONNECT stream. + base_stream_ref = undefined :: undefined | gun:stream_ref(), + + streams = [] :: [#stream{}], + in = head :: io(), + in_state = {0, 0} :: {non_neg_integer(), non_neg_integer()}, + out = head :: io() +}). + +check_options(Opts) -> + do_check_options(maps:to_list(Opts)). + +do_check_options([]) -> + ok; +do_check_options([{closing_timeout, infinity}|Opts]) -> + do_check_options(Opts); +do_check_options([{closing_timeout, T}|Opts]) when is_integer(T), T > 0 -> + do_check_options(Opts); +do_check_options([Opt={content_handlers, Handlers}|Opts]) -> + case gun_content_handler:check_option(Handlers) of + ok -> do_check_options(Opts); + error -> {error, {options, {http, Opt}}} + end; +do_check_options([{cookie_ignore_informational, B}|Opts]) when is_boolean(B) -> + do_check_options(Opts); +do_check_options([{flow, InitialFlow}|Opts]) when is_integer(InitialFlow), InitialFlow > 0 -> + do_check_options(Opts); +do_check_options([{keepalive, infinity}|Opts]) -> + do_check_options(Opts); +do_check_options([{keepalive, K}|Opts]) when is_integer(K), K > 0 -> + do_check_options(Opts); +do_check_options([{transform_header_name, F}|Opts]) when is_function(F) -> + do_check_options(Opts); +do_check_options([{version, V}|Opts]) when V =:= 'HTTP/1.1'; V =:= 'HTTP/1.0' -> + do_check_options(Opts); +do_check_options([Opt|_]) -> + {error, {options, {http, Opt}}}. + +name() -> http. +opts_name() -> http_opts. +has_keepalive() -> true. +default_keepalive() -> infinity. + +init(_ReplyTo, Socket, Transport, Opts) -> + BaseStreamRef = maps:get(stream_ref, Opts, undefined), + Version = maps:get(version, Opts, 'HTTP/1.1'), + {ok, connected, #http_state{socket=Socket, transport=Transport, + opts=Opts, version=Version, base_stream_ref=BaseStreamRef}}. + +switch_transport(Transport, Socket, State) -> + State#http_state{socket=Socket, transport=Transport}. + +%% Stop looping when we got no more data. +handle(<<>>, State, CookieStore, _, EvHandlerState) -> + {{state, State}, CookieStore, EvHandlerState}; +%% Close when server responds and we don't have any open streams. +handle(_, #http_state{streams=[]}, CookieStore, _, EvHandlerState) -> + {close, CookieStore, EvHandlerState}; +%% Wait for the full response headers before trying to parse them. +handle(Data, State=#http_state{in=head, buffer=Buffer, + streams=[#stream{ref=StreamRef, reply_to=ReplyTo}|_]}, + CookieStore, EvHandler, EvHandlerState0) -> + %% Send the event only if there was no data in the buffer. + %% If there is data in the buffer then we already sent the event. + EvHandlerState = case Buffer of + <<>> -> + EvHandler:response_start(#{ + stream_ref => stream_ref(State, StreamRef), + reply_to => ReplyTo + }, EvHandlerState0); + _ -> + EvHandlerState0 + end, + Data2 = << Buffer/binary, Data/binary >>, + case binary:match(Data2, <<"\r\n\r\n">>) of + nomatch -> + {{state, State#http_state{buffer=Data2}}, CookieStore, EvHandlerState}; + {_, _} -> + handle_head(Data2, State#http_state{buffer= <<>>}, + CookieStore, EvHandler, EvHandlerState) + end; +%% Everything sent to the socket until it closes is part of the response body. +handle(Data, State=#http_state{in=body_close}, CookieStore, _, EvHandlerState) -> + {send_data(Data, State, nofin), CookieStore, EvHandlerState}; +%% Chunked transfer-encoding may contain both data and trailers. +handle(Data, State=#http_state{in=body_chunked, in_state=InState, buffer=Buffer, + streams=[#stream{ref=StreamRef, reply_to=ReplyTo}|_], connection=Conn}, + CookieStore, EvHandler, EvHandlerState0) -> + Buffer2 = << Buffer/binary, Data/binary >>, + case cow_http_te:stream_chunked(Buffer2, InState) of + more -> + {{state, State#http_state{buffer=Buffer2}}, CookieStore, EvHandlerState0}; + {more, Data2, InState2} -> + {send_data(Data2, State#http_state{buffer= <<>>, in_state=InState2}, nofin), + CookieStore, EvHandlerState0}; + {more, Data2, Length, InState2} when is_integer(Length) -> + %% @todo See if we can recv faster than one message at a time. + {send_data(Data2, State#http_state{buffer= <<>>, in_state=InState2}, nofin), + CookieStore, EvHandlerState0}; + {more, Data2, Rest, InState2} -> + %% @todo See if we can recv faster than one message at a time. + {send_data(Data2, State#http_state{buffer=Rest, in_state=InState2}, nofin), + CookieStore, EvHandlerState0}; + {done, HasTrailers, Rest} -> + %% @todo response_end should be called AFTER send_data + {IsFin, EvHandlerState} = case HasTrailers of + trailers -> + {nofin, EvHandlerState0}; + no_trailers -> + EvHandlerState1 = EvHandler:response_end(#{ + stream_ref => stream_ref(State, StreamRef), + reply_to => ReplyTo + }, EvHandlerState0), + {fin, EvHandlerState1} + end, + %% I suppose it doesn't hurt to append an empty binary. + %% We ignore the active command because the stream ended. + [{state, State1}|_] = send_data(<<>>, State, IsFin), + case {HasTrailers, Conn} of + {trailers, _} -> + handle(Rest, State1#http_state{buffer = <<>>, in=body_trailer}, + CookieStore, EvHandler, EvHandlerState); + {no_trailers, keepalive} -> + handle(Rest, end_stream(State1#http_state{buffer= <<>>}), + CookieStore, EvHandler, EvHandlerState); + {no_trailers, close} -> + {[{state, end_stream(State1)}, close], CookieStore, EvHandlerState} + end; + {done, Data2, HasTrailers, Rest} -> + %% @todo response_end should be called AFTER send_data + {IsFin, EvHandlerState} = case HasTrailers of + trailers -> + {nofin, EvHandlerState0}; + no_trailers -> + EvHandlerState1 = EvHandler:response_end(#{ + stream_ref => stream_ref(State, StreamRef), + reply_to => ReplyTo + }, EvHandlerState0), + {fin, EvHandlerState1} + end, + %% We ignore the active command because the stream ended. + [{state, State1}|_] = send_data(Data2, State, IsFin), + case {HasTrailers, Conn} of + {trailers, _} -> + handle(Rest, State1#http_state{buffer = <<>>, in=body_trailer}, + CookieStore, EvHandler, EvHandlerState); + {no_trailers, keepalive} -> + handle(Rest, end_stream(State1#http_state{buffer= <<>>}), + CookieStore, EvHandler, EvHandlerState); + {no_trailers, close} -> + {[{state, end_stream(State1)}, close], CookieStore, EvHandlerState} + end + end; +handle(Data, State=#http_state{in=body_trailer, buffer=Buffer, connection=Conn, + streams=[#stream{ref=StreamRef, reply_to=ReplyTo}|_]}, + CookieStore, EvHandler, EvHandlerState0) -> + Data2 = << Buffer/binary, Data/binary >>, + case binary:match(Data2, <<"\r\n\r\n">>) of + nomatch -> + {{state, State#http_state{buffer=Data2}}, CookieStore, EvHandlerState0}; + {_, _} -> + {Trailers, Rest} = cow_http:parse_headers(Data2), + %% @todo We probably want to pass this to gun_content_handler? + RealStreamRef = stream_ref(State, StreamRef), + ReplyTo ! {gun_trailers, self(), RealStreamRef, Trailers}, + ResponseEvent = #{ + stream_ref => RealStreamRef, + reply_to => ReplyTo + }, + EvHandlerState1 = EvHandler:response_trailers(ResponseEvent#{headers => Trailers}, EvHandlerState0), + EvHandlerState = EvHandler:response_end(ResponseEvent, EvHandlerState1), + case Conn of + keepalive -> + handle(Rest, end_stream(State#http_state{buffer= <<>>}), + CookieStore, EvHandler, EvHandlerState); + close -> + {[{state, end_stream(State)}, close], CookieStore, EvHandlerState} + end + end; +%% We know the length of the rest of the body. +handle(Data, State=#http_state{in={body, Length}, connection=Conn, + streams=[#stream{ref=StreamRef, reply_to=ReplyTo}|_]}, + CookieStore, EvHandler, EvHandlerState0) -> + DataSize = byte_size(Data), + if + %% More data coming. + DataSize < Length -> + {send_data(Data, State#http_state{in={body, Length - DataSize}}, nofin), + CookieStore, EvHandlerState0}; + %% Stream finished, no rest. + DataSize =:= Length -> + %% We ignore the active command because the stream ended. + [{state, State1}|_] = send_data(Data, State, fin), + EvHandlerState = EvHandler:response_end(#{ + stream_ref => stream_ref(State, StreamRef), + reply_to => ReplyTo + }, EvHandlerState0), + case Conn of + keepalive -> + {[{state, end_stream(State1)}, {active, true}], CookieStore, EvHandlerState}; + close -> + {[{state, end_stream(State1)}, close], CookieStore, EvHandlerState} + end; + %% Stream finished, rest. + true -> + << Body:Length/binary, Rest/bits >> = Data, + %% We ignore the active command because the stream ended. + [{state, State1}|_] = send_data(Body, State, fin), + EvHandlerState = EvHandler:response_end(#{ + stream_ref => stream_ref(State1, StreamRef), + reply_to => ReplyTo + }, EvHandlerState0), + case Conn of + keepalive -> handle(Rest, end_stream(State1), CookieStore, EvHandler, EvHandlerState); + close -> {[{state, end_stream(State1)}, close], CookieStore, EvHandlerState} + end + end. + +handle_head(Data, State=#http_state{opts=Opts, + streams=[#stream{ref=StreamRef, authority=Authority, path=Path}|_]}, + CookieStore0, EvHandler, EvHandlerState) -> + {Version, Status, _, Rest0} = cow_http:parse_status_line(Data), + {Headers, Rest} = cow_http:parse_headers(Rest0), + CookieStore = gun_cookies:set_cookie_header(scheme(State), + Authority, Path, Status, Headers, CookieStore0, Opts), + case StreamRef of + {connect, _, _} when Status >= 200, Status < 300 -> + handle_connect(Rest, State, CookieStore, EvHandler, EvHandlerState, Status, Headers); + _ when Status >= 100, Status =< 199 -> + handle_inform(Rest, State, CookieStore, EvHandler, EvHandlerState, Version, Status, Headers); + _ -> + handle_response(Rest, State, CookieStore, EvHandler, EvHandlerState, Version, Status, Headers) + end. + +%% We handle HTTP/1.0 responses to CONNECT requests the same as HTTP/1.1. +%% This is because many proxies have historically used HTTP/1.0 for their +%% response. The HTTP/1.1 specification does not disallow it: servers that +%% respond positively to a CONNECT request are supposed to implement it. +handle_connect(Rest, State=#http_state{ + streams=[Stream=#stream{ref={_, StreamRef, Destination}, reply_to=ReplyTo}|Tail]}, + CookieStore, EvHandler, EvHandlerState0, Status, Headers) -> + RealStreamRef = stream_ref(State, StreamRef), + %% @todo If the stream is cancelled we probably shouldn't finish the CONNECT setup. + _ = case Stream of + #stream{is_alive=false} -> ok; + _ -> ReplyTo ! {gun_response, self(), RealStreamRef, fin, Status, Headers} + end, + %% @todo Figure out whether the event should trigger if the stream was cancelled. + EvHandlerState1 = EvHandler:response_headers(#{ + stream_ref => RealStreamRef, + reply_to => ReplyTo, + status => Status, + headers => Headers + }, EvHandlerState0), + EvHandlerState = EvHandler:response_end(#{ + stream_ref => RealStreamRef, + reply_to => ReplyTo + }, EvHandlerState1), + %% We expect there to be no additional data after the CONNECT response. + %% @todo That's probably wrong. + <<>> = Rest, + _ = end_stream(State#http_state{streams=[Stream|Tail]}), + NewHost = maps:get(host, Destination), + NewPort = maps:get(port, Destination), + case Destination of + #{transport := tls} -> + HandshakeEvent = #{ + stream_ref => RealStreamRef, + reply_to => ReplyTo, + tls_opts => maps:get(tls_opts, Destination, []), + timeout => maps:get(tls_handshake_timeout, Destination, infinity) + }, + Protocols = maps:get(protocols, Destination, [http2, http]), + {[ + {origin, <<"https">>, NewHost, NewPort, connect}, + {tls_handshake, HandshakeEvent, Protocols, ReplyTo} + ], CookieStore, EvHandlerState}; + _ -> + [NewProtocol0] = maps:get(protocols, Destination, [http]), + NewProtocol = gun_protocols:add_stream_ref(NewProtocol0, RealStreamRef), + Protocol = gun_protocols:handler(NewProtocol), + ReplyTo ! {gun_tunnel_up, self(), RealStreamRef, Protocol:name()}, + {[ + {origin, <<"http">>, NewHost, NewPort, connect}, + {switch_protocol, NewProtocol, ReplyTo} + ], CookieStore, EvHandlerState} + end. + +%% @todo We probably shouldn't send info messages if the stream is not alive. +handle_inform(Rest, State=#http_state{ + streams=[#stream{ref=StreamRef, reply_to=ReplyTo}|_]}, + CookieStore, EvHandler, EvHandlerState0, Version, Status, Headers) -> + EvHandlerState = EvHandler:response_inform(#{ + stream_ref => stream_ref(State, StreamRef), + reply_to => ReplyTo, + status => Status, + headers => Headers + }, EvHandlerState0), + case {Version, Status, StreamRef} of + {'HTTP/1.1', 101, #websocket{}} -> + {ws_handshake(Rest, State, StreamRef, Headers), CookieStore, EvHandlerState}; + %% Any other 101 response results in us switching to the raw protocol. + %% @todo We should check that we asked for an upgrade before accepting it. + {'HTTP/1.1', 101, _} when is_reference(StreamRef) -> + try + %% @todo We shouldn't ignore Rest. + {_, Upgrade0} = lists:keyfind(<<"upgrade">>, 1, Headers), + Upgrade = cow_http_hd:parse_upgrade(Upgrade0), + ReplyTo ! {gun_upgrade, self(), stream_ref(State, StreamRef), Upgrade, Headers}, + %% @todo We probably need to add_stream_ref? + {{switch_protocol, raw, ReplyTo}, CookieStore, EvHandlerState0} + catch _:_ -> + %% When the Upgrade header is missing or invalid we treat + %% the response as any other informational response. + ReplyTo ! {gun_inform, self(), stream_ref(State, StreamRef), Status, Headers}, + handle(Rest, State, CookieStore, EvHandler, EvHandlerState) + end; + _ -> + ReplyTo ! {gun_inform, self(), stream_ref(State, StreamRef), Status, Headers}, + handle(Rest, State, CookieStore, EvHandler, EvHandlerState) + end. + +handle_response(Rest, State=#http_state{version=ClientVersion, opts=Opts, connection=Conn, + streams=[Stream=#stream{ref=StreamRef, reply_to=ReplyTo, method=Method, is_alive=IsAlive}|Tail]}, + CookieStore, EvHandler, EvHandlerState0, Version, Status, Headers) -> + In = response_io_from_headers(Method, Version, Status, Headers), + IsFin = case In of head -> fin; _ -> nofin end, + RealStreamRef = stream_ref(State, StreamRef), + %% @todo Figure out whether the event should trigger if the stream was cancelled. + {Handlers, EvHandlerState2} = case IsAlive of + false -> + {undefined, EvHandlerState0}; + true -> + ReplyTo ! {gun_response, self(), RealStreamRef, IsFin, Status, Headers}, + EvHandlerState1 = EvHandler:response_headers(#{ + stream_ref => RealStreamRef, + reply_to => ReplyTo, + status => Status, + headers => Headers + }, EvHandlerState0), + case IsFin of + fin -> {undefined, EvHandlerState1}; + nofin -> + Handlers0 = maps:get(content_handlers, Opts, [gun_data_h]), + {gun_content_handler:init(ReplyTo, RealStreamRef, + Status, Headers, Handlers0), EvHandlerState1} + end + end, + EvHandlerState3 = case IsFin of + nofin -> + EvHandlerState2; + fin -> + EvHandler:response_end(#{ + stream_ref => RealStreamRef, + reply_to => ReplyTo + }, EvHandlerState2) + end, + Conn2 = if + Conn =:= close -> close; + Version =:= 'HTTP/1.0' -> close; + ClientVersion =:= 'HTTP/1.0' -> close; + true -> conn_from_headers(Version, Headers) + end, + %% We always reset in_state even if not chunked. + if + IsFin =:= fin, Conn2 =:= close -> + {close, CookieStore, EvHandlerState3}; + IsFin =:= fin -> + handle(Rest, end_stream(State#http_state{in=In, + in_state={0, 0}, connection=Conn2, + streams=[Stream#stream{handler_state=Handlers}|Tail]}), + CookieStore, EvHandler, EvHandlerState3); + Conn2 =:= close -> + close_streams(State, Tail, closing), + {CommandOrCommands, CookieStore1, EvHandlerState4} = + handle(Rest, State#http_state{in=In, + in_state={0, 0}, connection=Conn2, + streams=[Stream#stream{handler_state=Handlers}]}, + CookieStore, EvHandler, EvHandlerState3), + Commands = if + is_list(CommandOrCommands) -> + CommandOrCommands ++ [closing(State)]; + true -> + [CommandOrCommands, closing(State)] + end, + {Commands, CookieStore1, EvHandlerState4}; + true -> + handle(Rest, State#http_state{in=In, + in_state={0, 0}, connection=Conn2, + streams=[Stream#stream{handler_state=Handlers}|Tail]}, + CookieStore, EvHandler, EvHandlerState3) + end. + +%% The state must be first in order to retrieve it when the stream ended. +send_data(<<>>, State, nofin) -> + [{state, State}, {active, true}]; +%% @todo What if we receive data when the HEAD method was used? +send_data(Data, State=#http_state{streams=[Stream=#stream{ + flow=Flow0, is_alive=true, handler_state=Handlers0}|Tail]}, IsFin) -> + {ok, Dec, Handlers} = gun_content_handler:handle(IsFin, Data, Handlers0), + Flow = case Flow0 of + infinity -> infinity; + _ -> Flow0 - Dec + end, + [ + {state, State#http_state{streams=[Stream#stream{flow=Flow, handler_state=Handlers}|Tail]}}, + {active, Flow > 0} + ]; +send_data(_, State, _) -> + [{state, State}, {active, true}]. + +%% We only update the active state when the current stream is being updated. +update_flow(State=#http_state{streams=[Stream=#stream{ref=StreamRef, flow=Flow0}|Tail]}, + _ReplyTo, StreamRef, Inc) -> + Flow = case Flow0 of + infinity -> infinity; + _ -> Flow0 + Inc + end, + [ + {state, State#http_state{streams=[Stream#stream{flow=Flow}|Tail]}}, + {active, Flow > 0} + ]; +update_flow(State=#http_state{streams=Streams0}, _ReplyTo, StreamRef, Inc) -> + Streams = [case Ref of + StreamRef when Flow =/= infinity -> + Tuple#stream{flow=Flow + Inc}; + _ -> + Tuple + end || Tuple = #stream{ref=Ref, flow=Flow} <- Streams0], + {state, State#http_state{streams=Streams}}. + +%% We can immediately close the connection when there's no streams. +closing(_, #http_state{streams=[]}, _, EvHandlerState) -> + {close, EvHandlerState}; +%% Otherwise we set connection: close (even if the header was not sent) +%% and close any pipelined streams, only keeping the active stream. +closing(Reason, State=#http_state{streams=[LastStream|Tail]}, _, EvHandlerState) -> + close_streams(State, Tail, {closing, Reason}), + {[ + {state, State#http_state{connection=close, streams=[LastStream]}}, + closing(State) + ], EvHandlerState}. + +closing(#http_state{opts=Opts}) -> + Timeout = maps:get(closing_timeout, Opts, 15000), + {closing, Timeout}. + +close(Reason, State=#http_state{in=body_close, + streams=[#stream{ref=StreamRef, reply_to=ReplyTo}|Tail]}, + EvHandler, EvHandlerState) -> + %% We may have more than one stream in case we somehow close abruptly. + close_streams(State, Tail, close_reason(Reason)), + _ = send_data(<<>>, State, fin), + EvHandler:response_end(#{ + stream_ref => stream_ref(State, StreamRef), + reply_to => ReplyTo + }, EvHandlerState); +close(Reason, State=#http_state{streams=Streams}, _, EvHandlerState) -> + close_streams(State, Streams, close_reason(Reason)), + EvHandlerState. + +close_reason(closed) -> closed; +close_reason(Reason) -> {closed, Reason}. + +%% @todo Do we want an event for this? +%% +%% @todo Need to propagate stream closing to tunneled streams. +close_streams(_, [], _) -> + ok; +close_streams(State, [#stream{is_alive=false}|Tail], Reason) -> + close_streams(State, Tail, Reason); +close_streams(State, [#stream{ref=StreamRef, reply_to=ReplyTo}|Tail], Reason) -> + ReplyTo ! {gun_error, self(), stream_ref(State, StreamRef), Reason}, + close_streams(State, Tail, Reason). + +%% We don't send a keep-alive when a CONNECT request was initiated. +keepalive(#http_state{streams=[#stream{ref={connect, _, _}}]}, _, EvHandlerState) -> + {[], EvHandlerState}; +%% We can only keep-alive by sending an empty line in-between streams. +keepalive(#http_state{socket=Socket, transport=Transport, out=head}, _, EvHandlerState) -> + case Transport:send(Socket, <<"\r\n">>) of + ok -> {[], EvHandlerState}; + Error={error, _} -> {Error, EvHandlerState} + end; +keepalive(_State, _, EvHandlerState) -> + {[], EvHandlerState}. + +headers(State, StreamRef, ReplyTo, _, _, _, _, _, _, CookieStore, _, EvHandlerState) + when is_list(StreamRef) -> + ReplyTo ! {gun_error, self(), stream_ref(State, StreamRef), + {badstate, "The stream is not a tunnel."}}, + {[], CookieStore, EvHandlerState}; +headers(State=#http_state{opts=Opts, out=head}, + StreamRef, ReplyTo, Method, Host, Port, Path, Headers, + InitialFlow0, CookieStore0, EvHandler, EvHandlerState0) -> + {SendResult, Authority, Conn, Out, CookieStore, EvHandlerState} = send_request(State, + StreamRef, ReplyTo, Method, Host, Port, Path, Headers, undefined, + CookieStore0, EvHandler, EvHandlerState0, ?FUNCTION_NAME), + Command = case SendResult of + ok -> + InitialFlow = initial_flow(InitialFlow0, Opts), + {state, new_stream(State#http_state{connection=Conn, out=Out}, StreamRef, + ReplyTo, Method, Authority, Path, InitialFlow)}; + Error={error, _} -> + Error + end, + {Command, CookieStore, EvHandlerState}. + +request(State, StreamRef, ReplyTo, _, _, _, _, _, _, _, CookieStore, _, EvHandlerState) + when is_list(StreamRef) -> + ReplyTo ! {gun_error, self(), stream_ref(State, StreamRef), + {badstate, "The stream is not a tunnel."}}, + {[], CookieStore, EvHandlerState}; +request(State=#http_state{opts=Opts, out=head}, StreamRef, ReplyTo, + Method, Host, Port, Path, Headers, Body, + InitialFlow0, CookieStore0, EvHandler, EvHandlerState0) -> + {SendResult, Authority, Conn, Out, CookieStore, EvHandlerState} = send_request(State, + StreamRef, ReplyTo, Method, Host, Port, Path, Headers, Body, + CookieStore0, EvHandler, EvHandlerState0, ?FUNCTION_NAME), + Command = case SendResult of + ok -> + InitialFlow = initial_flow(InitialFlow0, Opts), + {state, new_stream(State#http_state{connection=Conn, out=Out}, StreamRef, + ReplyTo, Method, Authority, Path, InitialFlow)}; + Error={error, _} -> + Error + end, + {Command, CookieStore, EvHandlerState}. + +initial_flow(infinity, #{flow := InitialFlow}) -> InitialFlow; +initial_flow(InitialFlow, _) -> InitialFlow. + +send_request(State=#http_state{socket=Socket, transport=Transport, version=Version}, + StreamRef, ReplyTo, Method, Host, Port, Path, Headers0, Body, + CookieStore0, EvHandler, EvHandlerState0, Function) -> + Headers1 = lists:keydelete(<<"transfer-encoding">>, 1, Headers0), + Headers2 = case Body of + undefined -> Headers1; + _ -> lists:keydelete(<<"content-length">>, 1, Headers1) + end, + %% We use Headers2 because this is the smallest list. + Conn = conn_from_headers(Version, Headers2), + Out = case Body of + undefined when Function =:= ws_upgrade -> head; + undefined -> request_io_from_headers(Headers2); + _ -> head + end, + {Authority, Headers3} = case lists:keyfind(<<"host">>, 1, Headers2) of + false -> + Authority0 = host_header(Transport:name(), Host, Port), + {Authority0, [{<<"host">>, Authority0}|Headers2]}; + {_, Authority1} -> + {Authority1, Headers2} + end, + Headers4 = transform_header_names(State, Headers3), + Headers5 = case {Body, Out} of + {undefined, body_chunked} when Version =:= 'HTTP/1.0' -> Headers4; + {undefined, body_chunked} -> [{<<"transfer-encoding">>, <<"chunked">>}|Headers4]; + {undefined, _} -> Headers4; + _ -> [{<<"content-length">>, integer_to_binary(iolist_size(Body))}|Headers4] + end, + {Headers, CookieStore} = gun_cookies:add_cookie_header( + scheme(State), Authority, Path, Headers5, CookieStore0), + RealStreamRef = stream_ref(State, StreamRef), + RequestEvent = #{ + stream_ref => RealStreamRef, + reply_to => ReplyTo, + function => Function, + method => Method, + authority => Authority, + path => Path, + headers => Headers + }, + EvHandlerState1 = EvHandler:request_start(RequestEvent, EvHandlerState0), + SendResult = Transport:send(Socket, [ + cow_http:request(Method, Path, Version, Headers), + [Body || Body =/= undefined]]), + EvHandlerState2 = EvHandler:request_headers(RequestEvent, EvHandlerState1), + EvHandlerState = case Out of + head -> + RequestEndEvent = #{ + stream_ref => RealStreamRef, + reply_to => ReplyTo + }, + EvHandler:request_end(RequestEndEvent, EvHandlerState2); + _ -> + EvHandlerState2 + end, + {SendResult, Authority, Conn, Out, CookieStore, EvHandlerState}. + +host_header(TransportName, Host0, Port) -> + Host = case Host0 of + {local, _SocketPath} -> <<>>; + Tuple when tuple_size(Tuple) =:= 8 -> [$[, inet:ntoa(Tuple), $]]; %% IPv6. + Tuple when tuple_size(Tuple) =:= 4 -> inet:ntoa(Tuple); %% IPv4. + Atom when is_atom(Atom) -> atom_to_list(Atom); + _ -> Host0 + end, + case {TransportName, Port} of + {tcp, 80} -> Host; + {tls, 443} -> Host; + _ -> [Host, $:, integer_to_binary(Port)] + end. + +transform_header_names(#http_state{opts=Opts}, Headers) -> + case maps:get(transform_header_name, Opts, undefined) of + undefined -> Headers; + Fun -> lists:keymap(Fun, 1, Headers) + end. + +scheme(#http_state{transport=Transport}) -> + case Transport of + gun_tls -> <<"https">>; + gun_tls_proxy -> <<"https">>; + _ -> <<"http">> + end. + +%% We are expecting a new stream. +data(State=#http_state{out=head}, StreamRef, ReplyTo, _, _, _, EvHandlerState) -> + error_stream_closed(State, StreamRef, ReplyTo), + {[], EvHandlerState}; +%% There are no active streams. +data(State=#http_state{streams=[]}, StreamRef, ReplyTo, _, _, _, EvHandlerState) -> + error_stream_not_found(State, StreamRef, ReplyTo), + {[], EvHandlerState}; +%% We can only send data on the last created stream. +data(State=#http_state{socket=Socket, transport=Transport, version=Version, + out=Out, streams=Streams}, StreamRef, ReplyTo, IsFin, Data, + EvHandler, EvHandlerState0) -> + case lists:last(Streams) of + #stream{ref=StreamRef, is_alive=true} -> + DataLength = iolist_size(Data), + case Out of + body_chunked when Version =:= 'HTTP/1.1', IsFin =:= fin -> + DataToSend = if + DataLength =:= 0 -> + cow_http_te:last_chunk(); + true -> + [ + cow_http_te:chunk(Data), + cow_http_te:last_chunk() + ] + end, + case Transport:send(Socket, DataToSend) of + ok -> + RequestEndEvent = #{ + stream_ref => stream_ref(State, StreamRef), + reply_to => ReplyTo + }, + EvHandlerState = EvHandler:request_end(RequestEndEvent, + EvHandlerState0), + {{state, State#http_state{out=head}}, EvHandlerState}; + Error={error, _} -> + {Error, EvHandlerState0} + end; + body_chunked when Version =:= 'HTTP/1.1' -> + case Transport:send(Socket, cow_http_te:chunk(Data)) of + ok -> {[], EvHandlerState0}; + Error={error, _} -> {Error, EvHandlerState0} + end; + {body, Length} when DataLength =< Length -> + Length2 = Length - DataLength, + case Transport:send(Socket, Data) of + ok when Length2 =:= 0, IsFin =:= fin -> + RequestEndEvent = #{ + stream_ref => stream_ref(State, StreamRef), + reply_to => ReplyTo + }, + EvHandlerState = EvHandler:request_end(RequestEndEvent, EvHandlerState0), + {{state, State#http_state{out=head}}, EvHandlerState}; + ok when Length2 > 0, IsFin =:= nofin -> + {{state, State#http_state{out={body, Length2}}}, EvHandlerState0}; + Error={error, _} -> + {Error, EvHandlerState0} + end; + body_chunked -> %% HTTP/1.0 + case Transport:send(Socket, Data) of + ok -> {[], EvHandlerState0}; + Error={error, _} -> {Error, EvHandlerState0} + end + end; + _ -> + error_stream_not_found(State, StreamRef, ReplyTo), + {[], EvHandlerState0} + end. + +connect(State, StreamRef, ReplyTo, _, _, _, _, _, EvHandlerState) + when is_list(StreamRef) -> + ReplyTo ! {gun_error, self(), stream_ref(State, StreamRef), + {badstate, "The stream is not a tunnel."}}, + {[], EvHandlerState}; +connect(State=#http_state{streams=Streams}, StreamRef, ReplyTo, _, _, _, _, _, EvHandlerState) + when Streams =/= [] -> + ReplyTo ! {gun_error, self(), stream_ref(State, StreamRef), {badstate, + "CONNECT can only be used with HTTP/1.1 when no other streams are active."}}, + {[], EvHandlerState}; +connect(State=#http_state{socket=Socket, transport=Transport, opts=Opts, version=Version}, + StreamRef, ReplyTo, Destination=#{host := Host0}, _TunnelInfo, Headers0, InitialFlow0, + EvHandler, EvHandlerState0) -> + Host = case Host0 of + Tuple when is_tuple(Tuple) -> inet:ntoa(Tuple); + _ -> Host0 + end, + Port = maps:get(port, Destination, 1080), + Authority = [Host, $:, integer_to_binary(Port)], + Headers1 = lists:keydelete(<<"content-length">>, 1, + lists:keydelete(<<"transfer-encoding">>, 1, Headers0)), + Headers2 = case lists:keymember(<<"host">>, 1, Headers1) of + false -> [{<<"host">>, Authority}|Headers1]; + true -> Headers1 + end, + HasProxyAuthorization = lists:keymember(<<"proxy-authorization">>, 1, Headers2), + Headers3 = case {HasProxyAuthorization, Destination} of + {false, #{username := UserID, password := Password}} -> + [{<<"proxy-authorization">>, [ + <<"Basic ">>, + base64:encode(iolist_to_binary([UserID, $:, Password]))]} + |Headers2]; + _ -> + Headers2 + end, + Headers = transform_header_names(State, Headers3), + RealStreamRef = stream_ref(State, StreamRef), + RequestEvent = #{ + stream_ref => RealStreamRef, + reply_to => ReplyTo, + function => connect, + method => <<"CONNECT">>, + authority => Authority, + headers => Headers + }, + EvHandlerState1 = EvHandler:request_start(RequestEvent, EvHandlerState0), + case Transport:send(Socket, cow_http:request(<<"CONNECT">>, + Authority, Version, Headers)) of + ok -> + EvHandlerState2 = EvHandler:request_headers(RequestEvent, EvHandlerState1), + RequestEndEvent = #{ + stream_ref => RealStreamRef, + reply_to => ReplyTo + }, + EvHandlerState = EvHandler:request_end(RequestEndEvent, EvHandlerState2), + InitialFlow = initial_flow(InitialFlow0, Opts), + {{state, new_stream(State, {connect, StreamRef, Destination}, + ReplyTo, <<"CONNECT">>, Authority, <<>>, InitialFlow)}, + EvHandlerState}; + Error={error, _} -> + {Error, EvHandlerState1} + end. + +%% We can't cancel anything, we can just stop forwarding messages to the owner. +cancel(State0, StreamRef, ReplyTo, EvHandler, EvHandlerState0) -> + case is_stream(State0, StreamRef) of + true -> + State = cancel_stream(State0, StreamRef), + EvHandlerState = EvHandler:cancel(#{ + stream_ref => stream_ref(State, StreamRef), + reply_to => ReplyTo, + endpoint => local, + reason => cancel + }, EvHandlerState0), + {{state, State}, EvHandlerState}; + false -> + error_stream_not_found(State0, StreamRef, ReplyTo), + {[], EvHandlerState0} + end. + +stream_info(#http_state{streams=Streams}, StreamRef) -> + case lists:keyfind(StreamRef, #stream.ref, Streams) of + #stream{reply_to=ReplyTo, is_alive=IsAlive} -> + {ok, #{ + ref => StreamRef, %% @todo Wrong stream_ref? base_stream_ref it? + reply_to => ReplyTo, + state => case IsAlive of + true -> running; + false -> stopping + end + }}; + false -> + {ok, undefined} + end. + +down(#http_state{streams=Streams}) -> + [case Ref of + {connect, Ref2, _} -> Ref2; + #websocket{ref=Ref2} -> Ref2; + _ -> Ref + end || #stream{ref=Ref} <- Streams]. + +error_stream_closed(State, StreamRef, ReplyTo) -> + ReplyTo ! {gun_error, self(), stream_ref(State, StreamRef), {badstate, + "The stream has already been closed."}}, + ok. + +error_stream_not_found(State, StreamRef, ReplyTo) -> + ReplyTo ! {gun_error, self(), stream_ref(State, StreamRef), {badstate, + "The stream cannot be found."}}, + ok. + +%% Headers information retrieval. + +conn_from_headers(Version, Headers) -> + case lists:keyfind(<<"connection">>, 1, Headers) of + false when Version =:= 'HTTP/1.0' -> + close; + false -> + keepalive; + {_, ConnHd} -> + conn_from_header(cow_http_hd:parse_connection(ConnHd)) + end. + +conn_from_header([]) -> close; +conn_from_header([<<"keep-alive">>|_]) -> keepalive; +conn_from_header([<<"upgrade">>|_]) -> keepalive; +conn_from_header([_|Tail]) -> conn_from_header(Tail). + +request_io_from_headers(Headers) -> + case lists:keyfind(<<"content-length">>, 1, Headers) of + {_, Length} -> + {body, cow_http_hd:parse_content_length(Length)}; + _ -> + body_chunked + end. + +response_io_from_headers(<<"HEAD">>, _, _, _) -> + head; +response_io_from_headers(_, _, Status, _) when (Status =:= 204) or (Status =:= 304) -> + head; +response_io_from_headers(_, Version, _Status, Headers) -> + case lists:keyfind(<<"transfer-encoding">>, 1, Headers) of + {_, TE} when Version =:= 'HTTP/1.1' -> + case cow_http_hd:parse_transfer_encoding(TE) of + [<<"chunked">>] -> body_chunked; + [<<"identity">>] -> body_close + end; + _ -> + case lists:keyfind(<<"content-length">>, 1, Headers) of + {_, <<"0">>} -> + head; + {_, Length} -> + {body, cow_http_hd:parse_content_length(Length)}; + _ -> + body_close + end + end. + +%% Streams. + +stream_ref(#http_state{base_stream_ref=undefined}, StreamRef) -> + stream_ref(StreamRef); +stream_ref(#http_state{base_stream_ref=BaseStreamRef}, StreamRef) + when is_reference(BaseStreamRef) -> + [BaseStreamRef, stream_ref(StreamRef)]; +stream_ref(#http_state{base_stream_ref=BaseStreamRef}, StreamRef) -> + BaseStreamRef ++ [stream_ref(StreamRef)]. + +stream_ref({connect, StreamRef, _}) -> StreamRef; +stream_ref(#websocket{ref=StreamRef}) -> StreamRef; +stream_ref(StreamRef) -> StreamRef. + +new_stream(State=#http_state{streams=Streams}, StreamRef, ReplyTo, + Method, Authority, Path, InitialFlow) -> + State#http_state{streams=Streams + ++ [#stream{ref=StreamRef, reply_to=ReplyTo, flow=InitialFlow, + method=iolist_to_binary(Method), authority=Authority, + path=iolist_to_binary(Path), is_alive=true}]}. + +is_stream(#http_state{streams=Streams}, StreamRef) -> + lists:keymember(StreamRef, #stream.ref, Streams). + +cancel_stream(State=#http_state{streams=Streams}, StreamRef) -> + Streams2 = [case Ref of + StreamRef -> + Tuple#stream{is_alive=false}; + _ -> + Tuple + end || Tuple = #stream{ref=Ref} <- Streams], + State#http_state{streams=Streams2}. + +end_stream(State=#http_state{streams=[_|Tail]}) -> + State#http_state{in=head, streams=Tail}. + +%% Websocket upgrade. + +ws_upgrade(State, StreamRef, ReplyTo, _, _, _, _, _, CookieStore, _, EvHandlerState) + when is_list(StreamRef) -> + ReplyTo ! {gun_error, self(), stream_ref(State, StreamRef), + {badstate, "The stream is not a tunnel."}}, + {[], CookieStore, EvHandlerState}; +ws_upgrade(State=#http_state{version='HTTP/1.0'}, + StreamRef, ReplyTo, _, _, _, _, _, CookieStore, _, EvHandlerState) -> + ReplyTo ! {gun_error, self(), stream_ref(State, StreamRef), {badstate, + "Websocket cannot be used over an HTTP/1.0 connection."}}, + {[], CookieStore, EvHandlerState}; +ws_upgrade(State=#http_state{out=head}, StreamRef, ReplyTo, + Host, Port, Path, Headers0, WsOpts, CookieStore0, EvHandler, EvHandlerState0) -> + {Headers1, GunExtensions} = case maps:get(compress, WsOpts, false) of + true -> {[{<<"sec-websocket-extensions">>, + <<"permessage-deflate; client_max_window_bits; server_max_window_bits=15">>} + |Headers0], + [<<"permessage-deflate">>]}; + false -> {Headers0, []} + end, + Headers2 = case maps:get(protocols, WsOpts, []) of + [] -> Headers1; + ProtoOpt -> + << _, _, Proto/bits >> = iolist_to_binary([[<<", ">>, P] || {P, _} <- ProtoOpt]), + [{<<"sec-websocket-protocol">>, Proto}|Headers1] + end, + Key = cow_ws:key(), + Headers = [ + {<<"connection">>, <<"upgrade">>}, + {<<"upgrade">>, <<"websocket">>}, + {<<"sec-websocket-version">>, <<"13">>}, + {<<"sec-websocket-key">>, Key} + |Headers2 + ], + {SendResult, Authority, Conn, Out, CookieStore, EvHandlerState} = send_request(State, + StreamRef, ReplyTo, <<"GET">>, Host, Port, Path, Headers, undefined, + CookieStore0, EvHandler, EvHandlerState0, ?FUNCTION_NAME), + Command = case SendResult of + ok -> + InitialFlow = maps:get(flow, WsOpts, infinity), + {state, new_stream(State#http_state{connection=Conn, out=Out}, + #websocket{ref=StreamRef, reply_to=ReplyTo, key=Key, + extensions=GunExtensions, opts=WsOpts}, + ReplyTo, <<"GET">>, Authority, Path, InitialFlow)}; + Error={error, _} -> + Error + end, + {Command, CookieStore, EvHandlerState}. + +ws_handshake(Buffer, State, Ws=#websocket{key=Key}, Headers) -> + %% @todo check upgrade, connection + case lists:keyfind(<<"sec-websocket-accept">>, 1, Headers) of + false -> + close; + {_, Accept} -> + case cow_ws:encode_key(Key) of + Accept -> + ws_handshake_extensions_and_protocol(Buffer, State, Ws, Headers); + _ -> + close + end + end. + +ws_handshake_extensions_and_protocol(Buffer, State, + Ws=#websocket{extensions=Extensions0, opts=WsOpts}, Headers) -> + case gun_ws:select_extensions(Headers, Extensions0, WsOpts) of + close -> + close; + Extensions -> + case gun_ws:select_protocol(Headers, WsOpts) of + close -> + close; + Handler -> + ws_handshake_end(Buffer, State, Ws, Headers, Extensions, Handler) + end + end. + +%% We know that the most recent stream is the Websocket one. +ws_handshake_end(Buffer, + State=#http_state{socket=Socket, transport=Transport, streams=[#stream{flow=InitialFlow}|_]}, + #websocket{ref=StreamRef, reply_to=ReplyTo, opts=Opts}, Headers, Extensions, Handler) -> + %% Send ourselves the remaining buffer, if any. + _ = case Buffer of + <<>> -> + ok; + _ -> + {OK, _, _} = Transport:messages(), + self() ! {OK, Socket, Buffer} + end, + %% Inform the user that the upgrade was successful and switch the protocol. + RealStreamRef = stream_ref(State, StreamRef), + ReplyTo ! {gun_upgrade, self(), RealStreamRef, [<<"websocket">>], Headers}, + {switch_protocol, {ws, #{ + stream_ref => RealStreamRef, + headers => Headers, + extensions => Extensions, + flow => InitialFlow, + handler => Handler, + opts => Opts + }}, ReplyTo}. diff --git a/gun/src/gun_http2.erl b/gun/src/gun_http2.erl new file mode 100644 index 0000000..bfd2d31 --- /dev/null +++ b/gun/src/gun_http2.erl @@ -0,0 +1,1601 @@ +%% Copyright (c) 2016-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_http2). + +-export([check_options/1]). +-export([name/0]). +-export([opts_name/0]). +-export([has_keepalive/0]). +-export([default_keepalive/0]). +-export([init/4]). +-export([switch_transport/3]). +-export([handle/5]). +-export([handle_continue/6]). +-export([update_flow/4]). +-export([closing/4]). +-export([close/4]). +-export([keepalive/3]). +-export([headers/12]). +-export([request/13]). +-export([data/7]). +-export([connect/9]). +-export([cancel/5]). +-export([timeout/3]). +-export([stream_info/2]). +-export([down/1]). +-export([ws_upgrade/11]). +-export([ws_send/6]). + +-record(websocket_info, { + extensions :: [binary()], + opts :: gun:ws_opts() +}). + +-record(tunnel, { + state = requested :: requested | established, + + %% Destination information. + destination = undefined :: undefined | gun:connect_destination(), + + %% Tunnel information. + info = undefined :: gun:tunnel_info() | #websocket_info{}, + + %% Protocol module and state of the outer layer. Only initialized + %% after the TLS handshake has completed when TLS is involved. + protocol = undefined :: module(), + protocol_state = undefined :: any() +}). + +-record(stream, { + id = undefined :: cow_http2:streamid(), + + %% Reference used by the user of Gun to refer to this stream. + %% This may be only a part of a stream_ref() for tunneled streams. + ref :: reference(), + + %% Process to send messages to. + reply_to :: pid(), + + %% Flow control. + flow :: integer() | infinity, + + %% Request target URI. + authority :: iodata(), + path :: iodata(), + + %% Content handlers state. + handler_state :: undefined | gun_content_handler:state(), + + %% CONNECT tunnel. + tunnel :: undefined | #tunnel{} +}). + +-record(http2_state, { + reply_to :: pid(), + socket :: inet:socket() | ssl:sslsocket(), + transport :: module(), + opts = #{} :: gun:http2_opts(), + content_handlers :: gun_content_handler:opt(), + buffer = <<>> :: binary(), + + %% Base stream ref, defined when the protocol runs + %% inside an HTTP/2 CONNECT stream. + base_stream_ref = undefined :: undefined | gun:stream_ref(), + + %% Real transport for the HTTP/2 layer, defined when we are + %% in a non-HTTP/2 tunnel. + tunnel_transport = undefined :: undefined | tcp | tls, + + %% Current status of the connection. We use this to ensure we are + %% not sending the GOAWAY frame more than once, and to validate + %% the server connection preface. + status = preface :: preface | connected | goaway | closing, + + %% HTTP/2 state machine. + http2_machine :: cow_http2_machine:http2_machine(), + + %% Currently active HTTP/2 streams. Streams may be initiated either + %% by the client or by the server through PUSH_PROMISE frames. + %% + %% Streams can be found by ID or by Ref. The most common should be + %% the idea, that's why the main map has the ID as key. Then we also + %% have a Ref->ID index for faster lookup when we only have the Ref. + streams = #{} :: #{cow_http2:streamid() => #stream{}}, + stream_refs = #{} :: #{reference() => cow_http2:streamid()}, + + %% Number of pings that have been sent but not yet acknowledged. + %% Used to determine whether the connection should be closed when + %% the keepalive_tolerance option is set. + pings_unack = 0 :: non_neg_integer() +}). + +check_options(Opts) -> + do_check_options(maps:to_list(Opts)). + +%% @todo Accept http_opts, http2_opts, and so on. +do_check_options([]) -> + ok; +do_check_options([{closing_timeout, infinity}|Opts]) -> + do_check_options(Opts); +do_check_options([{closing_timeout, T}|Opts]) when is_integer(T), T > 0 -> + do_check_options(Opts); +do_check_options([Opt={content_handlers, Handlers}|Opts]) -> + case gun_content_handler:check_option(Handlers) of + ok -> do_check_options(Opts); + error -> {error, {options, {http2, Opt}}} + end; +do_check_options([{cookie_ignore_informational, B}|Opts]) when is_boolean(B) -> + do_check_options(Opts); +do_check_options([{flow, InitialFlow}|Opts]) when is_integer(InitialFlow), InitialFlow > 0 -> + do_check_options(Opts); +do_check_options([{keepalive, infinity}|Opts]) -> + do_check_options(Opts); +do_check_options([{keepalive, K}|Opts]) when is_integer(K), K > 0 -> + do_check_options(Opts); +do_check_options([{keepalive_tolerance, K}|Opts]) when is_integer(K), K >= 0 -> + do_check_options(Opts); +do_check_options([{notify_settings_changed, B}|Opts]) when is_boolean(B) -> + do_check_options(Opts); +do_check_options([Opt={Name, _}|Opts]) -> + %% We blindly accept all cow_http2_machine options. + HTTP2MachineOpts = [ + connection_window_margin_size, + connection_window_update_threshold, + enable_connect_protocol, + initial_connection_window_size, + initial_stream_window_size, + max_connection_window_size, + max_concurrent_streams, + max_decode_table_size, + max_encode_table_size, + max_fragmented_header_block_size, + max_frame_size_received, + max_frame_size_sent, + max_stream_window_size, + preface_timeout, + settings_timeout, + stream_window_data_threshold, + stream_window_margin_size, + stream_window_update_threshold + ], + case lists:member(Name, HTTP2MachineOpts) of + true -> do_check_options(Opts); + false -> {error, {options, {http2, Opt}}} + end. + +name() -> http2. +opts_name() -> http2_opts. +has_keepalive() -> true. +default_keepalive() -> infinity. + +init(ReplyTo, Socket, Transport, Opts0) -> + %% We have different defaults than the protocol in order + %% to optimize for performance when receiving responses. + Opts = Opts0#{ + initial_connection_window_size => maps:get(initial_connection_window_size, Opts0, 8000000), + initial_stream_window_size => maps:get(initial_stream_window_size, Opts0, 8000000) + }, + Handlers = maps:get(content_handlers, Opts, [gun_data_h]), + BaseStreamRef = maps:get(stream_ref, Opts, undefined), + TunnelTransport = maps:get(tunnel_transport, Opts, undefined), + {ok, Preface, HTTP2Machine} = cow_http2_machine:init(client, Opts#{message_tag => BaseStreamRef}), + case Transport:send(Socket, Preface) of + ok -> + {ok, connected, #http2_state{reply_to=ReplyTo, socket=Socket, transport=Transport, + opts=Opts, base_stream_ref=BaseStreamRef, tunnel_transport=TunnelTransport, + content_handlers=Handlers, http2_machine=HTTP2Machine}}; + Error={error, _Reason} -> + Error + end. + +switch_transport(Transport, Socket, State) -> + State#http2_state{socket=Socket, transport=Transport}. + +handle(Data, State=#http2_state{buffer=Buffer}, CookieStore, EvHandler, EvHandlerState) -> + parse(<< Buffer/binary, Data/binary >>, State#http2_state{buffer= <<>>}, + CookieStore, EvHandler, EvHandlerState). + +parse(Data, State0=#http2_state{status=preface, http2_machine=HTTP2Machine}, + CookieStore0, EvHandler, EvHandlerState0) -> + MaxFrameSize = cow_http2_machine:get_local_setting(max_frame_size, HTTP2Machine), + case cow_http2:parse(Data, MaxFrameSize) of + {ok, Frame, Rest} when element(1, Frame) =:= settings -> + case frame(State0#http2_state{status=connected}, Frame, CookieStore0, EvHandler, EvHandlerState0) of + {Error={error, _}, CookieStore, EvHandlerState} -> + {Error, CookieStore, EvHandlerState}; + {{state, State}, CookieStore, EvHandlerState} -> + parse(Rest, State, CookieStore, EvHandler, EvHandlerState) + end; + more -> + {{state, State0#http2_state{buffer=Data}}, CookieStore0, EvHandlerState0}; + %% Any error in the preface is converted to this specific error + %% to make debugging the problem easier (it's the server's fault). + _ -> + Reason = case Data of + <<"HTTP/1",_/bits>> -> + 'Invalid connection preface received. Appears to be an HTTP/1 response? (RFC7540 3.5)'; + _ -> + 'Invalid connection preface received. (RFC7540 3.5)' + end, + {connection_error(State0, {connection_error, protocol_error, Reason}), + CookieStore0, EvHandlerState0} + end; +parse(Data, State0=#http2_state{status=Status, http2_machine=HTTP2Machine, streams=Streams}, + CookieStore0, EvHandler, EvHandlerState0) -> + MaxFrameSize = cow_http2_machine:get_local_setting(max_frame_size, HTTP2Machine), + case cow_http2:parse(Data, MaxFrameSize) of + {ok, Frame, Rest} -> + case frame(State0, Frame, CookieStore0, EvHandler, EvHandlerState0) of + {Error={error, _}, CookieStore, EvHandlerState} -> + {Error, CookieStore, EvHandlerState}; + {{state, State}, CookieStore, EvHandlerState} -> + parse(Rest, State, CookieStore, EvHandler, EvHandlerState) + end; + {ignore, Rest} -> + case ignored_frame(State0) of + Error = {error, _} -> + {Error, CookieStore0, EvHandlerState0}; + {state, State} -> + parse(Rest, State, CookieStore0, EvHandler, EvHandlerState0) + end; + {stream_error, StreamID, Reason, Human, Rest} -> + case reset_stream(State0, StreamID, {stream_error, Reason, Human}) of + {state, State} -> + parse(Rest, State, CookieStore0, EvHandler, EvHandlerState0); + Error={error, _} -> + {Error, CookieStore0, EvHandlerState0} + end; + Error = {connection_error, _, _} -> + {connection_error(State0, Error), CookieStore0, EvHandlerState0}; + %% If we both received and sent a GOAWAY frame and there are no streams + %% currently running, we can close the connection immediately. + more when Status =/= connected, Streams =:= #{} -> + {[{state, State0#http2_state{buffer=Data, status=closing}}, close], + CookieStore0, EvHandlerState0}; + %% Otherwise we enter the closing state. + more when Status =:= goaway -> + {[{state, State0#http2_state{buffer=Data, status=closing}}, closing(State0)], + CookieStore0, EvHandlerState0}; + more -> + {{state, State0#http2_state{buffer=Data}}, + CookieStore0, EvHandlerState0} + end. + +%% Frames received. + +frame(State=#http2_state{http2_machine=HTTP2Machine0}, Frame, CookieStore, EvHandler, EvHandlerState0) -> + EvHandlerState = if + element(1, Frame) =:= headers; element(1, Frame) =:= push_promise -> + EvStreamID = element(2, Frame), + case cow_http2_machine:get_stream_remote_state(EvStreamID, HTTP2Machine0) of + {ok, idle} -> + #stream{ref=StreamRef, reply_to=ReplyTo} = get_stream_by_id(State, EvStreamID), + EvCallback = case element(1, Frame) of + headers -> response_start; + push_promise -> push_promise_start + end, + EvHandler:EvCallback(#{ + stream_ref => stream_ref(State, StreamRef), + reply_to => ReplyTo + }, EvHandlerState0); + %% Trailers or invalid header frame. + _ -> + EvHandlerState0 + end; + true -> + EvHandlerState0 + end, + case cow_http2_machine:frame(Frame, HTTP2Machine0) of + %% We only update the connection's window when receiving a lingering data frame. + {ok, HTTP2Machine} when element(1, Frame) =:= data -> + {update_window(State#http2_state{http2_machine=HTTP2Machine}), + CookieStore, EvHandlerState}; + {ok, HTTP2Machine} -> + {maybe_ack_or_notify(State#http2_state{http2_machine=HTTP2Machine}, Frame), + CookieStore, EvHandlerState}; + {ok, {data, StreamID, IsFin, Data}, HTTP2Machine} -> + data_frame(State#http2_state{http2_machine=HTTP2Machine}, + StreamID, IsFin, Data, CookieStore, EvHandler, EvHandlerState); + {ok, {headers, StreamID, IsFin, Headers, PseudoHeaders, BodyLen}, HTTP2Machine} -> + headers_frame(State#http2_state{http2_machine=HTTP2Machine}, + StreamID, IsFin, Headers, PseudoHeaders, BodyLen, + CookieStore, EvHandler, EvHandlerState); + {ok, {trailers, StreamID, Trailers}, HTTP2Machine} -> + {StateOrError, EvHandlerStateRet} = trailers_frame( + State#http2_state{http2_machine=HTTP2Machine}, + StreamID, Trailers, EvHandler, EvHandlerState), + {StateOrError, CookieStore, EvHandlerStateRet}; + {ok, {rst_stream, StreamID, Reason}, HTTP2Machine} -> + {StateOrError, EvHandlerStateRet} = rst_stream_frame( + State#http2_state{http2_machine=HTTP2Machine}, + StreamID, Reason, EvHandler, EvHandlerState), + {StateOrError, CookieStore, EvHandlerStateRet}; + {ok, {push_promise, StreamID, PromisedStreamID, Headers, PseudoHeaders}, HTTP2Machine} -> + {StateOrError, EvHandlerStateRet} = push_promise_frame( + State#http2_state{http2_machine=HTTP2Machine}, + StreamID, PromisedStreamID, Headers, PseudoHeaders, + EvHandler, EvHandlerState), + {StateOrError, CookieStore, EvHandlerStateRet}; + {ok, GoAway={goaway, _, _, _}, HTTP2Machine} -> + {goaway(State#http2_state{http2_machine=HTTP2Machine}, GoAway), + CookieStore, EvHandlerState}; + {send, SendData, HTTP2Machine} -> + case maybe_ack_or_notify(State#http2_state{http2_machine=HTTP2Machine}, Frame) of + {state, State1} -> + {StateOrError, EvHandlerStateRet} = send_data(State1, + SendData, EvHandler, EvHandlerState), + {StateOrError, CookieStore, EvHandlerStateRet}; + Error={error, _} -> + {Error, CookieStore, EvHandlerState} + end; + {error, {stream_error, StreamID, Reason, Human}, HTTP2Machine} -> + {reset_stream(State#http2_state{http2_machine=HTTP2Machine}, + StreamID, {stream_error, Reason, Human}), + CookieStore, EvHandlerState}; + {error, Error={connection_error, _, _}, HTTP2Machine} -> + {connection_error(State#http2_state{http2_machine=HTTP2Machine}, Error), + CookieStore, EvHandlerState} + end. + +maybe_ack_or_notify(State=#http2_state{reply_to=ReplyTo, socket=Socket, + transport=Transport, opts=Opts, http2_machine=HTTP2Machine, + pings_unack=PingsUnack}, Frame) -> + case Frame of + {settings, _} -> + %% We notify remote settings changes only if the user requested it. + _ = case Opts of + #{notify_settings_changed := true} -> + ReplyTo ! {gun_notify, self(), settings_changed, + cow_http2_machine:get_remote_settings(HTTP2Machine)}; + _ -> + ok + end, + case Transport:send(Socket, cow_http2:settings_ack()) of + ok -> {state, State}; + Error={error, _} -> Error + end; + {ping, Opaque} -> + case Transport:send(Socket, cow_http2:ping_ack(Opaque)) of + ok -> {state, State}; + Error={error, _} -> Error + end; + {ping_ack, _Opaque} -> + {state, State#http2_state{pings_unack=PingsUnack - 1}}; + _ -> + {state, State} + end. + +data_frame(State0, StreamID, IsFin, Data, CookieStore0, EvHandler, EvHandlerState0) -> + case get_stream_by_id(State0, StreamID) of + Stream=#stream{tunnel=undefined} -> + {StateOrError, EvHandlerState} = data_frame1(State0, + StreamID, IsFin, Data, EvHandler, EvHandlerState0, Stream), + {StateOrError, CookieStore0, EvHandlerState}; + Stream=#stream{tunnel=#tunnel{protocol=Proto, protocol_state=ProtoState0}} -> +% %% @todo What about IsFin? + {Commands, CookieStore, EvHandlerState1} = Proto:handle(Data, + ProtoState0, CookieStore0, EvHandler, EvHandlerState0), + %% The frame/parse functions only handle state or error commands. + {ResCommands, EvHandlerState} = tunnel_commands(Commands, + Stream, State0, EvHandler, EvHandlerState1), + {ResCommands, CookieStore, EvHandlerState} + end. + +%% Send errors are returned. Other errors cause the stream to be deleted. +tunnel_commands(Command, Stream, State, EvHandler, EvHandlerState) + when not is_list(Command) -> + tunnel_commands([Command], Stream, State, EvHandler, EvHandlerState); +tunnel_commands([], Stream, State, _EvHandler, EvHandlerState) -> + {{state, store_stream(State, Stream)}, EvHandlerState}; +tunnel_commands([{send, IsFin, Data}|Tail], Stream=#stream{id=StreamID}, + State0, EvHandler, EvHandlerState0) -> + case maybe_send_data(State0, StreamID, + IsFin, Data, EvHandler, EvHandlerState0) of + {{state, State}, EvHandlerState} -> + tunnel_commands(Tail, Stream, State, EvHandler, EvHandlerState); + ErrorResult={{error, _Reason}, _EvHandlerState} -> + ErrorResult + end; +tunnel_commands([{state, ProtoState}|Tail], Stream=#stream{tunnel=Tunnel}, + State, EvHandler, EvHandlerState) -> + tunnel_commands(Tail, Stream#stream{tunnel=Tunnel#tunnel{protocol_state=ProtoState}}, + State, EvHandler, EvHandlerState); +tunnel_commands([{error, Reason}|_], #stream{id=StreamID, ref=StreamRef, reply_to=ReplyTo}, + State, _EvHandler, EvHandlerState) -> + ReplyTo ! {gun_error, self(), stream_ref(State, StreamRef), + {stream_error, Reason, 'Tunnel closed unexpectedly.'}}, + {{state, delete_stream(State, StreamID)}, EvHandlerState}; +%% @todo Set a timeout for closing the Websocket stream. +tunnel_commands([{closing, _}|Tail], Stream, State, EvHandler, EvHandlerState) -> + tunnel_commands(Tail, Stream, State, EvHandler, EvHandlerState); +%% @todo Maybe we should stop increasing the window when not in active mode. (HTTP/2 Websocket only.) +tunnel_commands([{active, _}|Tail], Stream, State, EvHandler, EvHandlerState) -> + tunnel_commands(Tail, Stream, State, EvHandler, EvHandlerState). + +continue_stream_ref(#http2_state{socket=#{handle_continue_stream_ref := ContinueStreamRef}}, StreamRef) -> + case ContinueStreamRef of + [_|_] -> ContinueStreamRef ++ [StreamRef]; + _ -> [ContinueStreamRef, StreamRef] + end; +continue_stream_ref(State, StreamRef) -> + stream_ref(State, StreamRef). + +data_frame1(State0, StreamID, IsFin, Data, EvHandler, EvHandlerState0, + Stream=#stream{ref=StreamRef, reply_to=ReplyTo, flow=Flow0, handler_state=Handlers0}) -> + {ok, Dec, Handlers} = gun_content_handler:handle(IsFin, Data, Handlers0), + Flow = case Flow0 of + infinity -> infinity; + _ -> Flow0 - Dec + end, + State1 = store_stream(State0, Stream#stream{flow=Flow, handler_state=Handlers}), + {StateOrError, EvHandlerState} = case byte_size(Data) of + %% We do not send a WINDOW_UPDATE if the DATA frame was of size 0. + 0 when IsFin =:= fin -> + EvHandlerState1 = EvHandler:response_end(#{ + stream_ref => stream_ref(State1, StreamRef), + reply_to => ReplyTo + }, EvHandlerState0), + {{state, State1}, EvHandlerState1}; + 0 -> + {{state, State1}, EvHandlerState0}; + _ -> + %% We do not send a stream WINDOW_UPDATE when the flow control kicks in + %% (it'll be sent when the flow recovers) or for the last DATA frame. + case IsFin of + nofin when Flow =< 0 -> + {update_window(State1), EvHandlerState0}; + nofin -> + {update_window(State1, StreamID), EvHandlerState0}; + fin -> + EvHandlerState1 = EvHandler:response_end(#{ + stream_ref => stream_ref(State1, StreamRef), + reply_to => ReplyTo + }, EvHandlerState0), + {update_window(State1), EvHandlerState1} + end + end, + case StateOrError of + {state, State} -> + {{state, maybe_delete_stream(State, StreamID, remote, IsFin)}, + EvHandlerState}; + Error={error, _} -> + %% @todo Delete stream and return new state and error commands. + {Error, EvHandlerState} + end. + +headers_frame(State0=#http2_state{opts=Opts}, + StreamID, IsFin, Headers, #{status := Status}, _BodyLen, + CookieStore0, EvHandler, EvHandlerState0) -> + Stream = get_stream_by_id(State0, StreamID), + #stream{ + authority=Authority, + path=Path, + tunnel=Tunnel + } = Stream, + CookieStore = gun_cookies:set_cookie_header(scheme(State0), + Authority, Path, Status, Headers, CookieStore0, Opts), + {StateOrError, EvHandlerState} = if + Status >= 100, Status =< 199 -> + headers_frame_inform(State0, Stream, Status, Headers, EvHandler, EvHandlerState0); + Status >= 200, Status =< 299, element(#tunnel.state, Tunnel) =:= requested, IsFin =:= nofin -> + headers_frame_connect(State0, Stream, Status, Headers, EvHandler, EvHandlerState0); + true -> + headers_frame_response(State0, Stream, IsFin, Status, Headers, EvHandler, EvHandlerState0) + end, + {StateOrError, CookieStore, EvHandlerState}. + +headers_frame_inform(State, #stream{ref=StreamRef, reply_to=ReplyTo}, + Status, Headers, EvHandler, EvHandlerState0) -> + RealStreamRef = stream_ref(State, StreamRef), + ReplyTo ! {gun_inform, self(), RealStreamRef, Status, Headers}, + EvHandlerState = EvHandler:response_inform(#{ + stream_ref => RealStreamRef, + reply_to => ReplyTo, + status => Status, + headers => Headers + }, EvHandlerState0), + {{state, State}, EvHandlerState}. + +headers_frame_connect(State0=#http2_state{http2_machine=HTTP2Machine0}, + Stream=#stream{id=StreamID, ref=StreamRef, reply_to=ReplyTo, tunnel=#tunnel{ + info=#websocket_info{extensions=Extensions0, opts=WsOpts}}}, + Status, Headers, EvHandler, EvHandlerState0) -> + RealStreamRef = stream_ref(State0, StreamRef), + EvHandlerState1 = EvHandler:response_headers(#{ + stream_ref => RealStreamRef, + reply_to => ReplyTo, + status => Status, + headers => Headers + }, EvHandlerState0), + %% Websocket CONNECT response headers terminate the response but not the stream. + EvHandlerState = EvHandler:response_end(#{ + stream_ref => RealStreamRef, + reply_to => ReplyTo + }, EvHandlerState1), + case gun_ws:select_extensions(Headers, Extensions0, WsOpts) of + close -> + {ok, HTTP2Machine} = cow_http2_machine:reset_stream(StreamID, HTTP2Machine0), + State1 = State0#http2_state{http2_machine=HTTP2Machine}, + StateOrError = reset_stream(State1, StreamID, {stream_error, cancel, + 'The sec-websocket-extensions header is invalid. (RFC6455 9.1, RFC7692 7)'}), + {StateOrError, EvHandlerState}; + Extensions -> + case gun_ws:select_protocol(Headers, WsOpts) of + close -> + {ok, HTTP2Machine} = cow_http2_machine:reset_stream(StreamID, HTTP2Machine0), + State1 = State0#http2_state{http2_machine=HTTP2Machine}, + StateOrError = reset_stream(State1, StreamID, {stream_error, cancel, + 'The sec-websocket-protocol header is invalid. (RFC6455 4.1)'}), + {StateOrError, EvHandlerState}; + Handler -> + headers_frame_connect_websocket(State0, Stream, Headers, + EvHandler, EvHandlerState, Extensions, Handler) + end + end; +headers_frame_connect(State=#http2_state{transport=Transport, opts=Opts, tunnel_transport=TunnelTransport}, + Stream=#stream{ref=StreamRef, reply_to=ReplyTo, tunnel=Tunnel=#tunnel{ + destination=Destination=#{host := DestHost, port := DestPort}, info=TunnelInfo0}}, + Status, Headers, EvHandler, EvHandlerState0) -> + RealStreamRef = stream_ref(State, StreamRef), + TunnelInfo = TunnelInfo0#{ + origin_host => DestHost, + origin_port => DestPort + }, + ReplyTo ! {gun_response, self(), RealStreamRef, fin, Status, Headers}, + EvHandlerState1 = EvHandler:response_headers(#{ + stream_ref => RealStreamRef, + reply_to => ReplyTo, + status => Status, + headers => Headers + }, EvHandlerState0), + EvHandlerState2 = EvHandler:response_end(#{ + stream_ref => RealStreamRef, + reply_to => ReplyTo + }, EvHandlerState1), + EvHandlerState3 = EvHandler:origin_changed(#{ + stream_ref => RealStreamRef, + type => connect, + origin_scheme => case Destination of + #{transport := tls} -> <<"https">>; + _ -> <<"http">> + end, + origin_host => DestHost, + origin_port => DestPort + }, EvHandlerState2), + ContinueStreamRef = continue_stream_ref(State, StreamRef), + OriginSocket = #{ + gun_pid => self(), + reply_to => ReplyTo, + stream_ref => RealStreamRef, + handle_continue_stream_ref => ContinueStreamRef + }, + Proto = gun_tunnel, + ProtoOpts = case Destination of + #{transport := tls} -> + Protocols = maps:get(protocols, Destination, [http2, http]), + TLSOpts = gun:ensure_tls_opts(Protocols, maps:get(tls_opts, Destination, []), DestHost), + HandshakeEvent = #{ + stream_ref => RealStreamRef, + reply_to => ReplyTo, + tls_opts => TLSOpts, + timeout => maps:get(tls_handshake_timeout, Destination, infinity) + }, + Opts#{ + stream_ref => RealStreamRef, + tunnel => #{ + type => connect, + transport_name => case TunnelTransport of + undefined -> Transport:name(); + _ -> TunnelTransport + end, + protocol_name => http2, + info => TunnelInfo, + handshake_event => HandshakeEvent, + protocols => Protocols + } + }; + _ -> + [NewProtocol] = maps:get(protocols, Destination, [http]), + Opts#{ + stream_ref => RealStreamRef, + tunnel => #{ + type => connect, + transport_name => case TunnelTransport of + undefined -> Transport:name(); + _ -> TunnelTransport + end, + protocol_name => http2, + info => TunnelInfo, + new_protocol => NewProtocol + } + } + end, + case Proto:init(ReplyTo, OriginSocket, gun_tcp_proxy, ProtoOpts, EvHandler, EvHandlerState3) of + {tunnel, ProtoState, EvHandlerState} -> + {{state, store_stream(State, Stream#stream{tunnel=Tunnel#tunnel{state=established, + info=TunnelInfo, protocol=Proto, protocol_state=ProtoState}})}, + EvHandlerState}; + %% @todo We should not error out the entire connection on tunnel errors. + Error={error, _} -> + {Error, EvHandlerState3} + end. + +headers_frame_connect_websocket(State, Stream=#stream{ref=StreamRef, reply_to=ReplyTo, + tunnel=Tunnel=#tunnel{info=#websocket_info{opts=WsOpts}}}, + Headers, EvHandler, EvHandlerState0, Extensions, Handler) -> + RealStreamRef = stream_ref(State, StreamRef), + ContinueStreamRef = continue_stream_ref(State, StreamRef), + OriginSocket = #{ + gun_pid => self(), + reply_to => ReplyTo, + stream_ref => RealStreamRef, + handle_continue_stream_ref => ContinueStreamRef + }, + ReplyTo ! {gun_upgrade, self(), RealStreamRef, [<<"websocket">>], Headers}, + Proto = gun_ws, + EvHandlerState = EvHandler:protocol_changed(#{ + stream_ref => RealStreamRef, + protocol => Proto:name() + }, EvHandlerState0), + ProtoOpts = #{ + stream_ref => RealStreamRef, + headers => Headers, + extensions => Extensions, + flow => maps:get(flow, WsOpts, infinity), + handler => Handler, + opts => WsOpts + }, + %% @todo Handle error result from Proto:init/4 + {ok, connected_ws_only, ProtoState} = Proto:init( + ReplyTo, OriginSocket, gun_tcp_proxy, ProtoOpts), + {{state, store_stream(State, Stream#stream{tunnel=Tunnel#tunnel{state=established, + protocol=Proto, protocol_state=ProtoState}})}, + EvHandlerState}. + +headers_frame_response(State=#http2_state{content_handlers=Handlers0}, + Stream=#stream{id=StreamID, ref=StreamRef, reply_to=ReplyTo}, + IsFin, Status, Headers, EvHandler, EvHandlerState0) -> + RealStreamRef = stream_ref(State, StreamRef), + ReplyTo ! {gun_response, self(), RealStreamRef, IsFin, Status, Headers}, + EvHandlerState1 = EvHandler:response_headers(#{ + stream_ref => RealStreamRef, + reply_to => ReplyTo, + status => Status, + headers => Headers + }, EvHandlerState0), + {Handlers, EvHandlerState} = case IsFin of + fin -> + EvHandlerState2 = EvHandler:response_end(#{ + stream_ref => RealStreamRef, + reply_to => ReplyTo + }, EvHandlerState1), + {undefined, EvHandlerState2}; + nofin -> + {gun_content_handler:init(ReplyTo, RealStreamRef, + Status, Headers, Handlers0), EvHandlerState1} + end, + %% We disable the tunnel, if any, when receiving any non 2xx response. + {{state, maybe_delete_stream(store_stream(State, + Stream#stream{handler_state=Handlers, tunnel=undefined}), + StreamID, remote, IsFin)}, EvHandlerState}. + +trailers_frame(State, StreamID, Trailers, EvHandler, EvHandlerState0) -> + #stream{ref=StreamRef, reply_to=ReplyTo} = get_stream_by_id(State, StreamID), + %% @todo We probably want to pass this to gun_content_handler? + RealStreamRef = stream_ref(State, StreamRef), + ReplyTo ! {gun_trailers, self(), RealStreamRef, Trailers}, + ResponseEvent = #{ + stream_ref => RealStreamRef, + reply_to => ReplyTo + }, + EvHandlerState1 = EvHandler:response_trailers(ResponseEvent#{headers => Trailers}, EvHandlerState0), + EvHandlerState = EvHandler:response_end(ResponseEvent, EvHandlerState1), + {{state, maybe_delete_stream(State, StreamID, remote, fin)}, EvHandlerState}. + +rst_stream_frame(State0, StreamID, Reason, EvHandler, EvHandlerState0) -> + case take_stream(State0, StreamID) of + {#stream{ref=StreamRef, reply_to=ReplyTo}, State} -> + ReplyTo ! {gun_error, self(), stream_ref(State0, StreamRef), + {stream_error, Reason, 'Stream reset by server.'}}, + EvHandlerState = EvHandler:cancel(#{ + stream_ref => stream_ref(State, StreamRef), + reply_to => ReplyTo, + endpoint => remote, + reason => Reason + }, EvHandlerState0), + {{state, State}, EvHandlerState}; + error -> + {{state, State0}, EvHandlerState0} + end. + +%% Pushed streams receive the same initial flow value as the parent stream. +push_promise_frame(State=#http2_state{socket=Socket, transport=Transport, + status=Status, http2_machine=HTTP2Machine0}, + StreamID, PromisedStreamID, Headers, #{ + method := Method, scheme := Scheme, + authority := Authority, path := Path}, + EvHandler, EvHandlerState0) -> + #stream{ref=StreamRef, reply_to=ReplyTo, flow=InitialFlow} = get_stream_by_id(State, StreamID), + PromisedStreamRef = make_ref(), + RealPromisedStreamRef = stream_ref(State, PromisedStreamRef), + URI = iolist_to_binary([Scheme, <<"://">>, Authority, Path]), + PushPromiseEvent0 = #{ + stream_ref => stream_ref(State, StreamRef), + reply_to => ReplyTo, + method => Method, + uri => URI, + headers => Headers + }, + PushPromiseEvent = case Status of + connected -> + ReplyTo ! {gun_push, self(), stream_ref(State, StreamRef), + RealPromisedStreamRef, Method, URI, Headers}, + PushPromiseEvent0#{promised_stream_ref => RealPromisedStreamRef}; + _ -> + PushPromiseEvent0 + end, + EvHandlerState = EvHandler:push_promise_end(PushPromiseEvent, EvHandlerState0), + case Status of + connected -> + NewStream = #stream{id=PromisedStreamID, ref=PromisedStreamRef, + reply_to=ReplyTo, flow=InitialFlow, authority=Authority, path=Path}, + {{state, create_stream(State, NewStream)}, EvHandlerState}; + %% We cancel the push_promise immediately when we are shutting down. + _ -> + {ok, HTTP2Machine} = cow_http2_machine:reset_stream(PromisedStreamID, HTTP2Machine0), + case Transport:send(Socket, cow_http2:rst_stream(PromisedStreamID, cancel)) of + ok -> + {{state, State#http2_state{http2_machine=HTTP2Machine}}, EvHandlerState}; + Error={error, _} -> + {Error, EvHandlerState} + end + end. + +ignored_frame(State=#http2_state{http2_machine=HTTP2Machine0}) -> + case cow_http2_machine:ignored_frame(HTTP2Machine0) of + {ok, HTTP2Machine} -> + {state, State#http2_state{http2_machine=HTTP2Machine}}; + {error, Error={connection_error, _, _}, HTTP2Machine} -> + connection_error(State#http2_state{http2_machine=HTTP2Machine}, Error) + end. + +%% We always pass handle_continue messages to the tunnel. +handle_continue(ContinueStreamRef, Msg, State0, CookieStore0, EvHandler, EvHandlerState0) -> + StreamRef = case ContinueStreamRef of + [SR|_] -> SR; + _ -> ContinueStreamRef + end, + case get_stream_by_ref(State0, StreamRef) of + Stream=#stream{tunnel=#tunnel{protocol=Proto, protocol_state=ProtoState0}} -> + {Commands, CookieStore, EvHandlerState1} = Proto:handle_continue(ContinueStreamRef, + Msg, ProtoState0, CookieStore0, EvHandler, EvHandlerState0), + {ResCommands, EvHandlerState} = tunnel_commands(Commands, + Stream, State0, EvHandler, EvHandlerState1), + {ResCommands, CookieStore, EvHandlerState}; + %% The stream may have ended while TLS was being decoded. + %% We do not trigger an error because this is an internal event. + %% The stream_error, if any, was already sent from tunnel_commands. + error -> + {[], CookieStore0, EvHandlerState0} + end. + +update_flow(State, _ReplyTo, StreamRef, Inc) -> + case get_stream_by_ref(State, StreamRef) of + Stream=#stream{id=StreamID, flow=Flow0} -> + Flow = case Flow0 of + infinity -> infinity; + _ -> Flow0 + Inc + end, + if + %% Flow is active again, update the stream's window. + Flow0 =< 0, Flow > 0 -> + update_window(store_stream(State, + Stream#stream{flow=Flow}), StreamID); + true -> + {state, store_stream(State, Stream#stream{flow=Flow})} + end; + error -> + [] + end. + +%% Only update the connection's window. +update_window(State=#http2_state{socket=Socket, transport=Transport, + opts=#{initial_connection_window_size := ConnWindow}, http2_machine=HTTP2Machine0}) -> + case cow_http2_machine:ensure_window(ConnWindow, HTTP2Machine0) of + ok -> + {state, State}; + {ok, Increment, HTTP2Machine} -> + case Transport:send(Socket, cow_http2:window_update(Increment)) of + ok -> {state, State#http2_state{http2_machine=HTTP2Machine}}; + Error={error, _} -> Error + end + end. + +%% Update both the connection and the stream's window. +update_window(State0=#http2_state{socket=Socket, transport=Transport, + opts=#{initial_connection_window_size := ConnWindow, initial_stream_window_size := StreamWindow}, + http2_machine=HTTP2Machine0}, StreamID) -> + {Data1, HTTP2Machine2} = case cow_http2_machine:ensure_window(ConnWindow, HTTP2Machine0) of + ok -> {<<>>, HTTP2Machine0}; + {ok, Increment1, HTTP2Machine1} -> {cow_http2:window_update(Increment1), HTTP2Machine1} + end, + {Data2, HTTP2Machine} = case cow_http2_machine:ensure_window(StreamID, StreamWindow, HTTP2Machine2) of + ok -> {<<>>, HTTP2Machine2}; + {ok, Increment2, HTTP2Machine3} -> {cow_http2:window_update(StreamID, Increment2), HTTP2Machine3} + end, + State = State0#http2_state{http2_machine=HTTP2Machine}, + case {Data1, Data2} of + {<<>>, <<>>} -> + {state, State}; + _ -> + case Transport:send(Socket, [Data1, Data2]) of + ok -> {state, State}; + Error={error, _} -> Error + end + end. + +%% We may have to cancel streams even if we receive multiple +%% GOAWAY frames as the LastStreamID value may be lower than +%% the one previously received. +goaway(State0=#http2_state{socket=Socket, transport=Transport, http2_machine=HTTP2Machine, + status=Status, streams=Streams0, stream_refs=Refs}, {goaway, LastStreamID, Reason, _}) -> + {Streams, RemovedRefs} = goaway_streams(State0, maps:to_list(Streams0), LastStreamID, + {goaway, Reason, 'The connection is going away.'}, [], []), + State = State0#http2_state{ + streams=maps:from_list(Streams), + stream_refs=maps:without(RemovedRefs, Refs) + }, + case Status of + connected -> + case Transport:send(Socket, cow_http2:goaway( + cow_http2_machine:get_last_streamid(HTTP2Machine), + no_error, <<>>)) of + ok -> {state, State#http2_state{status=goaway}}; + Error={error, _} -> Error + end; + _ -> + {state, State} + end. + +%% Cancel server-initiated streams that are above LastStreamID. +goaway_streams(_, [], _, _, Acc, RefsAcc) -> + {Acc, RefsAcc}; +goaway_streams(State, [{StreamID, Stream=#stream{ref=StreamRef}}|Tail], LastStreamID, Reason, Acc, RefsAcc) + when StreamID > LastStreamID, (StreamID rem 2) =:= 1 -> + close_stream(State, Stream, Reason), + goaway_streams(State, Tail, LastStreamID, Reason, Acc, [StreamRef|RefsAcc]); +goaway_streams(State, [StreamWithID|Tail], LastStreamID, Reason, Acc, RefsAcc) -> + goaway_streams(State, Tail, LastStreamID, Reason, [StreamWithID|Acc], RefsAcc). + +%% We are already closing, do nothing. +closing(_, #http2_state{status=closing}, _, EvHandlerState) -> + {[], EvHandlerState}; +closing(Reason0, State=#http2_state{socket=Socket, transport=Transport, + http2_machine=HTTP2Machine}, _, EvHandlerState) -> + Reason = case Reason0 of + normal -> no_error; + owner_down -> no_error; + _ -> internal_error + end, + case Transport:send(Socket, cow_http2:goaway( + cow_http2_machine:get_last_streamid(HTTP2Machine), + Reason, <<>>)) of + ok -> + {[ + {state, State#http2_state{status=closing}}, + closing(State) + ], EvHandlerState}; + Error={error, _} -> + {Error, EvHandlerState} + end. + +closing(#http2_state{opts=Opts}) -> + Timeout = maps:get(closing_timeout, Opts, 15000), + {closing, Timeout}. + +close(Reason0, State=#http2_state{streams=Streams}, _, EvHandlerState) -> + Reason = close_reason(Reason0), + _ = maps:fold(fun(_, Stream, _) -> + close_stream(State, Stream, Reason) + end, [], Streams), + EvHandlerState. + +close_reason(closed) -> closed; +close_reason(Reason) -> {closed, Reason}. + +%% @todo Do we want an event for this? +close_stream(State, #stream{ref=StreamRef, reply_to=ReplyTo}, Reason) -> + ReplyTo ! {gun_error, self(), stream_ref(State, StreamRef), Reason}, + ok. + +keepalive(State=#http2_state{pings_unack=PingsUnack, opts=Opts}, _, EvHandlerState) + when PingsUnack >= map_get(keepalive_tolerance, Opts) -> + {connection_error(State, {connection_error, no_error, + 'The number of unacknowledged pings exceed the configured tolerance value.'}), + EvHandlerState}; +keepalive(State=#http2_state{socket=Socket, transport=Transport, pings_unack=PingsUnack}, + _, EvHandlerState) -> + case Transport:send(Socket, cow_http2:ping(0)) of + ok -> + {{state, State#http2_state{pings_unack=PingsUnack + 1}}, EvHandlerState}; + Error={error, _} -> + {Error, EvHandlerState} + end. + +headers(State=#http2_state{socket=Socket, transport=Transport, opts=Opts, + http2_machine=HTTP2Machine0}, StreamRef, ReplyTo, Method, Host, Port, + Path, Headers0, InitialFlow0, CookieStore0, EvHandler, EvHandlerState0) + when is_reference(StreamRef) -> + {ok, StreamID, HTTP2Machine1} = cow_http2_machine:init_stream( + iolist_to_binary(Method), HTTP2Machine0), + {ok, PseudoHeaders, Headers, CookieStore} = prepare_headers( + State, Method, Host, Port, Path, Headers0, CookieStore0), + Authority = maps:get(authority, PseudoHeaders), + RequestEvent = #{ + stream_ref => stream_ref(State, StreamRef), + reply_to => ReplyTo, + function => ?FUNCTION_NAME, + method => Method, + authority => Authority, + path => Path, + headers => Headers + }, + EvHandlerState1 = EvHandler:request_start(RequestEvent, EvHandlerState0), + {ok, IsFin, HeaderBlock, HTTP2Machine} = cow_http2_machine:prepare_headers( + StreamID, HTTP2Machine1, nofin, PseudoHeaders, Headers), + case Transport:send(Socket, cow_http2:headers(StreamID, IsFin, HeaderBlock)) of + ok -> + EvHandlerState = EvHandler:request_headers(RequestEvent, + EvHandlerState1), + InitialFlow = initial_flow(InitialFlow0, Opts), + Stream = #stream{id=StreamID, ref=StreamRef, reply_to=ReplyTo, + flow=InitialFlow, authority=Authority, path=Path}, + {{state, create_stream(State#http2_state{ + http2_machine=HTTP2Machine}, Stream)}, CookieStore, + EvHandlerState}; + Error={error, _} -> + {Error, CookieStore, EvHandlerState1} + end; +%% Tunneled request. +headers(State, RealStreamRef=[StreamRef|_], ReplyTo, Method, _Host, _Port, + Path, Headers, InitialFlow, CookieStore0, EvHandler, EvHandlerState0) -> + case get_stream_by_ref(State, StreamRef) of + %% @todo We should send an error to the user if the stream isn't ready. + Stream=#stream{tunnel=#tunnel{protocol=Proto, protocol_state=ProtoState0, info=#{ + origin_host := OriginHost, origin_port := OriginPort}}} -> + {Commands, CookieStore, EvHandlerState1} = Proto:headers(ProtoState0, RealStreamRef, + ReplyTo, Method, OriginHost, OriginPort, Path, Headers, + InitialFlow, CookieStore0, EvHandler, EvHandlerState0), + {ResCommands, EvHandlerState} = tunnel_commands(Commands, Stream, + State, EvHandler, EvHandlerState1), + {ResCommands, CookieStore, EvHandlerState}; + #stream{tunnel=undefined} -> + ReplyTo ! {gun_error, self(), stream_ref(State, StreamRef), {badstate, + "The stream is not a tunnel."}}, + {[], CookieStore0, EvHandlerState0}; + error -> + error_stream_not_found(State, StreamRef, ReplyTo), + {[], CookieStore0, EvHandlerState0} + end. + +request(State0=#http2_state{socket=Socket, transport=Transport, opts=Opts, + http2_machine=HTTP2Machine0}, StreamRef, ReplyTo, Method, Host, Port, + Path, Headers0, Body, InitialFlow0, CookieStore0, EvHandler, EvHandlerState0) + when is_reference(StreamRef) -> + Headers1 = lists:keystore(<<"content-length">>, 1, Headers0, + {<<"content-length">>, integer_to_binary(iolist_size(Body))}), + {ok, StreamID, HTTP2Machine1} = cow_http2_machine:init_stream( + iolist_to_binary(Method), HTTP2Machine0), + {ok, PseudoHeaders, Headers, CookieStore} = prepare_headers( + State0, Method, Host, Port, Path, Headers1, CookieStore0), + Authority = maps:get(authority, PseudoHeaders), + RealStreamRef = stream_ref(State0, StreamRef), + RequestEvent = #{ + stream_ref => RealStreamRef, + reply_to => ReplyTo, + function => ?FUNCTION_NAME, + method => Method, + authority => Authority, + path => Path, + headers => Headers + }, + EvHandlerState1 = EvHandler:request_start(RequestEvent, EvHandlerState0), + IsFin0 = case iolist_size(Body) of + 0 -> fin; + _ -> nofin + end, + {ok, IsFin, HeaderBlock, HTTP2Machine} = cow_http2_machine:prepare_headers( + StreamID, HTTP2Machine1, IsFin0, PseudoHeaders, Headers), + case Transport:send(Socket, cow_http2:headers(StreamID, IsFin, HeaderBlock)) of + ok -> + EvHandlerState = EvHandler:request_headers(RequestEvent, EvHandlerState1), + InitialFlow = initial_flow(InitialFlow0, Opts), + Stream = #stream{id=StreamID, ref=StreamRef, reply_to=ReplyTo, flow=InitialFlow, + authority=Authority, path=Path}, + State = create_stream(State0#http2_state{http2_machine=HTTP2Machine}, Stream), + case IsFin of + fin -> + RequestEndEvent = #{ + stream_ref => RealStreamRef, + reply_to => ReplyTo + }, + {{state, State}, CookieStore, EvHandler:request_end(RequestEndEvent, EvHandlerState)}; + nofin -> + {StateOrError, EvHandlerStateRet} = maybe_send_data( + State, StreamID, fin, Body, EvHandler, EvHandlerState), + {StateOrError, CookieStore, EvHandlerStateRet} + end; + Error={error, _} -> + {Error, CookieStore, EvHandlerState1} + end; +%% Tunneled request. +request(State, RealStreamRef=[StreamRef|_], ReplyTo, Method, _Host, _Port, + Path, Headers, Body, InitialFlow, CookieStore0, EvHandler, EvHandlerState0) -> + case get_stream_by_ref(State, StreamRef) of + %% @todo We should send an error to the user if the stream isn't ready. + Stream=#stream{tunnel=#tunnel{protocol=Proto, protocol_state=ProtoState0, info=#{ + origin_host := OriginHost, origin_port := OriginPort}}} -> + {Commands, CookieStore, EvHandlerState1} = Proto:request(ProtoState0, RealStreamRef, + ReplyTo, Method, OriginHost, OriginPort, Path, Headers, Body, + InitialFlow, CookieStore0, EvHandler, EvHandlerState0), + {ResCommands, EvHandlerState} = tunnel_commands(Commands, + Stream, State, EvHandler, EvHandlerState1), + {ResCommands, CookieStore, EvHandlerState}; + #stream{tunnel=undefined} -> + ReplyTo ! {gun_error, self(), stream_ref(State, StreamRef), {badstate, + "The stream is not a tunnel."}}, + {[], CookieStore0, EvHandlerState0}; + error -> + error_stream_not_found(State, StreamRef, ReplyTo), + {[], CookieStore0, EvHandlerState0} + end. + +initial_flow(infinity, #{flow := InitialFlow}) -> InitialFlow; +initial_flow(InitialFlow, _) -> InitialFlow. + +prepare_headers(State=#http2_state{transport=Transport}, + Method, Host0, Port, Path, Headers0, CookieStore0) -> + Scheme = scheme(State), + Authority = case lists:keyfind(<<"host">>, 1, Headers0) of + {_, Host} -> Host; + _ -> gun_http:host_header(Transport:name(), Host0, Port) + end, + %% @todo We also must remove any header found in the connection header. + %% @todo Much of this is duplicated in cow_http2_machine; sort things out. + Headers1 = + lists:keydelete(<<"host">>, 1, + lists:keydelete(<<"connection">>, 1, + lists:keydelete(<<"keep-alive">>, 1, + lists:keydelete(<<"proxy-connection">>, 1, + lists:keydelete(<<"transfer-encoding">>, 1, + lists:keydelete(<<"upgrade">>, 1, Headers0)))))), + {Headers, CookieStore} = gun_cookies:add_cookie_header( + Scheme, Authority, Path, Headers1, CookieStore0), + PseudoHeaders = #{ + method => Method, + scheme => Scheme, + authority => Authority, + path => Path + }, + {ok, PseudoHeaders, Headers, CookieStore}. + +scheme(#http2_state{transport=Transport}) -> + case Transport of + gun_tls -> <<"https">>; + gun_tls_proxy -> <<"https">>; + gun_tcp -> <<"http">>; + gun_tcp_proxy -> <<"http">>; + gun_tls_proxy_http2_connect -> <<"http">> + end. + +%% @todo Make all calls go through this clause. +data(State=#http2_state{http2_machine=HTTP2Machine}, StreamRef, ReplyTo, IsFin, Data, + EvHandler, EvHandlerState) when is_reference(StreamRef) -> + case get_stream_by_ref(State, StreamRef) of + Stream=#stream{id=StreamID, tunnel=Tunnel} -> + case cow_http2_machine:get_stream_local_state(StreamID, HTTP2Machine) of + {ok, fin, _} -> + error_stream_closed(State, StreamRef, ReplyTo), + {[], EvHandlerState}; + {ok, _, fin} -> + error_stream_closed(State, StreamRef, ReplyTo), + {[], EvHandlerState}; + {ok, _, _} when Tunnel =:= undefined -> + maybe_send_data(State, + StreamID, IsFin, Data, EvHandler, EvHandlerState); + {ok, _, _} -> + #tunnel{protocol=Proto, protocol_state=ProtoState0} = Tunnel, + {Commands, EvHandlerState1} = Proto:data(ProtoState0, StreamRef, + ReplyTo, IsFin, Data, EvHandler, EvHandlerState), + tunnel_commands(Commands, Stream, State, EvHandler, EvHandlerState1) + end; + error -> + error_stream_not_found(State, StreamRef, ReplyTo), + {[], EvHandlerState} + end; +%% Tunneled data. +data(State, RealStreamRef=[StreamRef|_], ReplyTo, IsFin, Data, EvHandler, EvHandlerState0) -> + case get_stream_by_ref(State, StreamRef) of + Stream=#stream{tunnel=#tunnel{protocol=Proto, protocol_state=ProtoState0}} -> + {Commands, EvHandlerState1} = Proto:data(ProtoState0, RealStreamRef, + ReplyTo, IsFin, Data, EvHandler, EvHandlerState0), + tunnel_commands(Commands, Stream, State, EvHandler, EvHandlerState1); + #stream{tunnel=undefined} -> + ReplyTo ! {gun_error, self(), stream_ref(State, StreamRef), {badstate, + "The stream is not a tunnel."}}, + {[], EvHandlerState0}; + error -> + error_stream_not_found(State, StreamRef, ReplyTo), + {[], EvHandlerState0} + end. + +maybe_send_data(State=#http2_state{http2_machine=HTTP2Machine0}, StreamID, IsFin, Data0, + EvHandler, EvHandlerState) -> + Data = case is_tuple(Data0) of + false -> {data, Data0}; + true -> Data0 + end, + case cow_http2_machine:send_or_queue_data(StreamID, HTTP2Machine0, IsFin, Data) of + {ok, HTTP2Machine} -> + {{state, State#http2_state{http2_machine=HTTP2Machine}}, EvHandlerState}; + {send, SendData, HTTP2Machine} -> + send_data(State#http2_state{http2_machine=HTTP2Machine}, SendData, + EvHandler, EvHandlerState) + end. + +send_data(State, [], _, EvHandlerState) -> + {{state, State}, EvHandlerState}; +send_data(State0, [{StreamID, IsFin, SendData}|Tail], EvHandler, EvHandlerState0) -> + case send_data(State0, StreamID, IsFin, SendData, EvHandler, EvHandlerState0) of + {{state, State}, EvHandlerState} -> + send_data(State, Tail, EvHandler, EvHandlerState); + ErrorResult={{error, _}, _EvHandlerState} -> + ErrorResult + end. + +send_data(State0, StreamID, IsFin, [Data], EvHandler, EvHandlerState0) -> + case send_data_frame(State0, StreamID, IsFin, Data) of + {state, State} -> + EvHandlerState = case IsFin of + nofin -> + EvHandlerState0; + fin -> + #stream{ref=StreamRef, reply_to=ReplyTo} = get_stream_by_id(State, StreamID), + RequestEndEvent = #{ + stream_ref => stream_ref(State, StreamRef), + reply_to => ReplyTo + }, + EvHandler:request_end(RequestEndEvent, EvHandlerState0) + end, + {{state, maybe_delete_stream(State, StreamID, local, IsFin)}, EvHandlerState}; + Error={error, _Reason} -> + {Error, EvHandlerState0} + end; + +send_data(State0, StreamID, IsFin, [Data|Tail], EvHandler, EvHandlerState) -> + case send_data_frame(State0, StreamID, nofin, Data) of + {state, State} -> + send_data(State, StreamID, IsFin, Tail, EvHandler, EvHandlerState); + Error={error, _Reason} -> + {Error, EvHandlerState} + end. + +send_data_frame(State=#http2_state{socket=Socket, transport=Transport}, + StreamID, IsFin, {data, Data}) -> + case Transport:send(Socket, cow_http2:data(StreamID, IsFin, Data)) of + ok -> + {state, State}; + Error={error, _} -> + Error + end; +%% @todo Uncomment this once sendfile is supported. +%send_data_frame(State=#http2_state{socket=Socket, transport=Transport}, +% StreamID, IsFin, {sendfile, Offset, Bytes, Path}) -> +% Transport:send(Socket, cow_http2:data_header(StreamID, IsFin, Bytes)), +% Transport:sendfile(Socket, Path, Offset, Bytes), +% {state, State}; +%% The stream is terminated in cow_http2_machine:prepare_trailers. +send_data_frame(State=#http2_state{socket=Socket, transport=Transport, + http2_machine=HTTP2Machine0}, StreamID, nofin, {trailers, Trailers}) -> + {ok, HeaderBlock, HTTP2Machine} + = cow_http2_machine:prepare_trailers(StreamID, HTTP2Machine0, Trailers), + case Transport:send(Socket, cow_http2:headers(StreamID, fin, HeaderBlock)) of + ok -> + {state, State#http2_state{http2_machine=HTTP2Machine}}; + Error={error, _} -> + Error + end. + +reset_stream(State0=#http2_state{socket=Socket, transport=Transport}, + StreamID, StreamError={stream_error, Reason, _}) -> + case Transport:send(Socket, cow_http2:rst_stream(StreamID, Reason)) of + ok -> + case take_stream(State0, StreamID) of + {#stream{ref=StreamRef, reply_to=ReplyTo}, State} -> + ReplyTo ! {gun_error, self(), stream_ref(State, StreamRef), StreamError}, + {state, State}; + error -> + {state, State0} + end; + Error={error, _} -> + Error + end. + +connect(State=#http2_state{socket=Socket, transport=Transport, opts=Opts, + http2_machine=HTTP2Machine0}, StreamRef, ReplyTo, + Destination=#{host := Host0}, TunnelInfo, Headers0, InitialFlow0, + EvHandler, EvHandlerState0) + when is_reference(StreamRef) -> + Host = case Host0 of + Tuple when is_tuple(Tuple) -> inet:ntoa(Tuple); + _ -> Host0 + end, + Port = maps:get(port, Destination, 1080), + Authority = [Host, $:, integer_to_binary(Port)], + PseudoHeaders = #{ + method => <<"CONNECT">>, + authority => iolist_to_binary(Authority) + }, + Headers1 = + lists:keydelete(<<"host">>, 1, + lists:keydelete(<<"content-length">>, 1, Headers0)), + HasProxyAuthorization = lists:keymember(<<"proxy-authorization">>, 1, Headers1), + Headers = case {HasProxyAuthorization, Destination} of + {false, #{username := UserID, password := Password}} -> + [{<<"proxy-authorization">>, [ + <<"Basic ">>, + base64:encode(iolist_to_binary([UserID, $:, Password]))]} + |Headers1]; + _ -> + Headers1 + end, + {ok, StreamID, HTTP2Machine1} = cow_http2_machine:init_stream(<<"CONNECT">>, HTTP2Machine0), + RealStreamRef = stream_ref(State, StreamRef), + RequestEvent = #{ + stream_ref => RealStreamRef, + reply_to => ReplyTo, + function => connect, + method => <<"CONNECT">>, + authority => Authority, + headers => Headers + }, + EvHandlerState1 = EvHandler:request_start(RequestEvent, EvHandlerState0), + {ok, nofin, HeaderBlock, HTTP2Machine} = cow_http2_machine:prepare_headers( + StreamID, HTTP2Machine1, nofin, PseudoHeaders, Headers), + case Transport:send(Socket, cow_http2:headers(StreamID, nofin, HeaderBlock)) of + ok -> + EvHandlerState2 = EvHandler:request_headers(RequestEvent, EvHandlerState1), + RequestEndEvent = #{ + stream_ref => RealStreamRef, + reply_to => ReplyTo + }, + EvHandlerState = EvHandler:request_end(RequestEndEvent, EvHandlerState2), + InitialFlow = initial_flow(InitialFlow0, Opts), + Stream = #stream{id=StreamID, ref=StreamRef, reply_to=ReplyTo, + flow=InitialFlow, authority=Authority, path= <<>>, + tunnel=#tunnel{destination=Destination, info=TunnelInfo}}, + {{state, create_stream(State#http2_state{http2_machine=HTTP2Machine}, Stream)}, + EvHandlerState}; + Error={error, _} -> + {Error, EvHandlerState1} + end; +%% Tunneled request. +connect(State, RealStreamRef=[StreamRef|_], ReplyTo, Destination, TunnelInfo, Headers0, InitialFlow, + EvHandler, EvHandlerState0) -> + case get_stream_by_ref(State, StreamRef) of + %% @todo Should we send an error to the user if the stream isn't ready. + Stream=#stream{tunnel=#tunnel{protocol=Proto, protocol_state=ProtoState0}} -> + {Commands, EvHandlerState1} = Proto:connect(ProtoState0, RealStreamRef, + ReplyTo, Destination, TunnelInfo, Headers0, InitialFlow, + EvHandler, EvHandlerState0), + tunnel_commands(Commands, Stream, State, EvHandler, EvHandlerState1); + #stream{tunnel=undefined} -> + ReplyTo ! {gun_error, self(), stream_ref(State, StreamRef), {badstate, + "The stream is not a tunnel."}}, + {[], EvHandlerState0}; + error -> + error_stream_not_found(State, StreamRef, ReplyTo), + {[], EvHandlerState0} + end. + +cancel(State=#http2_state{socket=Socket, transport=Transport, http2_machine=HTTP2Machine0}, + StreamRef, ReplyTo, EvHandler, EvHandlerState0) + when is_reference(StreamRef) -> + case get_stream_by_ref(State, StreamRef) of + #stream{id=StreamID} -> + {ok, HTTP2Machine} = cow_http2_machine:reset_stream(StreamID, HTTP2Machine0), + case Transport:send(Socket, cow_http2:rst_stream(StreamID, cancel)) of + ok -> + EvHandlerState = EvHandler:cancel(#{ + stream_ref => stream_ref(State, StreamRef), + reply_to => ReplyTo, + endpoint => local, + reason => cancel + }, EvHandlerState0), + {{state, delete_stream(State#http2_state{http2_machine=HTTP2Machine}, + StreamID)}, EvHandlerState}; + Error={error, _} -> + {Error, EvHandlerState0} + end; + error -> + error_stream_not_found(State, StreamRef, ReplyTo), + {[], EvHandlerState0} + end; +%% Tunneled request. +cancel(State, RealStreamRef=[StreamRef|_], ReplyTo, EvHandler, EvHandlerState0) -> + case get_stream_by_ref(State, StreamRef) of + Stream=#stream{tunnel=#tunnel{protocol=Proto, protocol_state=ProtoState0}} -> + {Commands, EvHandlerState1} = Proto:cancel(ProtoState0, + RealStreamRef, ReplyTo, EvHandler, EvHandlerState0), + tunnel_commands(Commands, Stream, State, EvHandler, EvHandlerState1); + #stream{tunnel=undefined} -> + ReplyTo ! {gun_error, self(), stream_ref(State, StreamRef), {badstate, + "The stream is not a tunnel."}}, + {[], EvHandlerState0}; + error -> + error_stream_not_found(State, StreamRef, ReplyTo), + {[], EvHandlerState0} + end. + +timeout(State=#http2_state{http2_machine=HTTP2Machine0}, {cow_http2_machine, undefined, Name}, TRef) -> + case cow_http2_machine:timeout(Name, TRef, HTTP2Machine0) of + {ok, HTTP2Machine} -> + {state, State#http2_state{http2_machine=HTTP2Machine}}; + {error, Error={connection_error, _, _}, _HTTP2Machine} -> + connection_error(State, Error) + end; +%% Timeouts occurring in tunnels. +timeout(State, {cow_http2_machine, RealStreamRef, Name}, TRef) -> + {StreamRef, SubStreamRef} = if + is_reference(RealStreamRef) -> {RealStreamRef, undefined}; + true -> {hd(RealStreamRef), tl(RealStreamRef)} + end, + case get_stream_by_ref(State, StreamRef) of + Stream=#stream{id=StreamID, tunnel=Tunnel=#tunnel{protocol=Proto, protocol_state=ProtoState0}} -> + case Proto:timeout(ProtoState0, {cow_http2_machine, SubStreamRef, Name}, TRef) of + {state, ProtoState} -> + {state, store_stream(State, Stream#stream{ + tunnel=Tunnel#tunnel{protocol_state=ProtoState}})}; + {error, {connection_error, Reason, Human}} -> + reset_stream(State, StreamID, {stream_error, Reason, Human}) + end; + %% We ignore timeout events for streams that no longer exist. + error -> + {state, State} + end. + +stream_info(State, StreamRef) when is_reference(StreamRef) -> + case get_stream_by_ref(State, StreamRef) of + #stream{reply_to=ReplyTo, tunnel=#tunnel{destination=Destination, + info=#{origin_host := OriginHost, origin_port := OriginPort}, + protocol=Proto, protocol_state=ProtoState}} -> + Transport = maps:get(transport, Destination, tcp), + Protocol = Proto:tunneled_name(ProtoState, true), + {ok, #{ + ref => StreamRef, + reply_to => ReplyTo, + state => running, + tunnel => #{ + transport => Transport, + protocol => Protocol, + origin_scheme => case {Transport, Protocol} of + {_, raw} -> undefined; + {tcp, _} -> <<"http">>; + {tls, _} -> <<"https">> + end, + origin_host => OriginHost, + origin_port => OriginPort + } + }}; + #stream{reply_to=ReplyTo} -> + {ok, #{ + ref => StreamRef, + reply_to => ReplyTo, + state => running + }}; + error -> + {ok, undefined} + end; +%% Tunneled streams. +stream_info(State, RealStreamRef=[StreamRef|_]) -> + case get_stream_by_ref(State, StreamRef) of + #stream{tunnel=#tunnel{protocol=Proto, protocol_state=ProtoState}} -> + %% We must return the real stream_ref as seen by the user. + %% We therefore set it on return, with the outer layer "winning". + case Proto:stream_info(ProtoState, RealStreamRef) of + {ok, undefined} -> + {ok, undefined}; + {ok, Info} -> + {ok, Info#{ref => RealStreamRef}} + end; + error -> + {ok, undefined} + end. + +%% @todo Tunnels. +down(#http2_state{stream_refs=Refs}) -> + maps:keys(Refs). + +ws_upgrade(State=#http2_state{socket=Socket, transport=Transport, + http2_machine=HTTP2Machine0}, StreamRef, ReplyTo, + Host, Port, Path, Headers0, WsOpts, + CookieStore0, EvHandler, EvHandlerState0) + when is_reference(StreamRef) -> + {ok, StreamID, HTTP2Machine1} = cow_http2_machine:init_stream( + <<"CONNECT">>, HTTP2Machine0), + {ok, PseudoHeaders, Headers1, CookieStore} = prepare_headers(State, + <<"CONNECT">>, Host, Port, Path, Headers0, CookieStore0), + {Headers2, GunExtensions} = case maps:get(compress, WsOpts, false) of + true -> + {[{<<"sec-websocket-extensions">>, + <<"permessage-deflate; client_max_window_bits; server_max_window_bits=15">>} + |Headers1], [<<"permessage-deflate">>]}; + false -> + {Headers1, []} + end, + Headers3 = case maps:get(protocols, WsOpts, []) of + [] -> + Headers2; + ProtoOpt -> + << _, _, Proto/bits >> = iolist_to_binary([[<<", ">>, P] || {P, _} <- ProtoOpt]), + [{<<"sec-websocket-protocol">>, Proto}|Headers2] + end, + Headers = [{<<"sec-websocket-version">>, <<"13">>}|Headers3], + Authority = maps:get(authority, PseudoHeaders), + RealStreamRef = stream_ref(State, StreamRef), + RequestEvent = #{ + stream_ref => RealStreamRef, + reply_to => ReplyTo, + function => ?FUNCTION_NAME, + method => <<"CONNECT">>, + authority => Authority, + path => Path, + headers => Headers + }, + EvHandlerState1 = EvHandler:request_start(RequestEvent, EvHandlerState0), + {ok, IsFin, HeaderBlock, HTTP2Machine} = cow_http2_machine:prepare_headers( + StreamID, HTTP2Machine1, nofin, PseudoHeaders#{protocol => <<"websocket">>}, Headers), + case Transport:send(Socket, cow_http2:headers(StreamID, IsFin, HeaderBlock)) of + ok -> + EvHandlerState2 = EvHandler:request_headers(RequestEvent, + EvHandlerState1), + RequestEndEvent = #{ + stream_ref => RealStreamRef, + reply_to => ReplyTo + }, + EvHandlerState = EvHandler:request_end(RequestEndEvent, + EvHandlerState2), + InitialFlow = maps:get(flow, WsOpts, infinity), + Stream = #stream{id=StreamID, ref=StreamRef, reply_to=ReplyTo, + flow=InitialFlow, authority=Authority, path=Path, + tunnel=#tunnel{info=#websocket_info{ + extensions=GunExtensions, opts=WsOpts}}}, + {{state, create_stream(State#http2_state{http2_machine=HTTP2Machine}, + Stream)}, CookieStore, EvHandlerState}; + Error={error, _} -> + {Error, EvHandlerState1} + end; +ws_upgrade(State, RealStreamRef=[StreamRef|_], ReplyTo, + Host, Port, Path, Headers, WsOpts, CookieStore0, EvHandler, EvHandlerState0) -> + case get_stream_by_ref(State, StreamRef) of + Stream=#stream{tunnel=#tunnel{protocol=Proto, protocol_state=ProtoState0}} -> + {Commands, CookieStore, EvHandlerState1} = Proto:ws_upgrade( + ProtoState0, RealStreamRef, ReplyTo, + Host, Port, Path, Headers, WsOpts, + CookieStore0, EvHandler, EvHandlerState0), + {ResCommands, EvHandlerState} = tunnel_commands(Commands, + Stream, State, EvHandler, EvHandlerState1), + {ResCommands, CookieStore, EvHandlerState} + %% @todo Error conditions? + end. + +ws_send(Frames, State, RealStreamRef, ReplyTo, EvHandler, EvHandlerState0) -> + StreamRef = case RealStreamRef of + [SR|_] -> SR; + _ -> RealStreamRef + end, + case get_stream_by_ref(State, StreamRef) of + Stream=#stream{tunnel=#tunnel{protocol=Proto, protocol_state=ProtoState}} -> + {Commands, EvHandlerState1} = Proto:ws_send(Frames, ProtoState, + RealStreamRef, ReplyTo, EvHandler, EvHandlerState0), + tunnel_commands(Commands, Stream, State, EvHandler, EvHandlerState1) + %% @todo Error conditions? + end. + +connection_error(#http2_state{socket=Socket, transport=Transport, + http2_machine=HTTP2Machine, streams=Streams}, + Error={connection_error, Reason, HumanReadable}) -> + Pids = lists:usort(maps:fold( + fun(_, #stream{reply_to=ReplyTo}, Acc) -> [ReplyTo|Acc] end, + [], Streams)), + _ = [Pid ! {gun_error, self(), {Reason, HumanReadable}} || Pid <- Pids], + Transport:send(Socket, cow_http2:goaway( + cow_http2_machine:get_last_streamid(HTTP2Machine), + Reason, <<>>)), + {error, Error}. + +%% Stream functions. + +error_stream_closed(State, StreamRef, ReplyTo) -> + ReplyTo ! {gun_error, self(), stream_ref(State, StreamRef), {badstate, + "The stream has already been closed."}}, + ok. + +error_stream_not_found(State, StreamRef, ReplyTo) -> + ReplyTo ! {gun_error, self(), stream_ref(State, StreamRef), {badstate, + "The stream cannot be found."}}, + ok. + +%% Streams. + +stream_ref(#http2_state{base_stream_ref=undefined}, StreamRef) -> + StreamRef; +stream_ref(#http2_state{base_stream_ref=BaseStreamRef}, StreamRef) + when is_reference(BaseStreamRef) -> + [BaseStreamRef, StreamRef]; +stream_ref(#http2_state{base_stream_ref=BaseStreamRef}, StreamRef) -> + BaseStreamRef ++ [StreamRef]. + +get_stream_by_id(#http2_state{streams=Streams}, StreamID) -> + maps:get(StreamID, Streams). + +get_stream_by_ref(#http2_state{streams=Streams, stream_refs=Refs}, StreamRef) -> + case maps:get(StreamRef, Refs, error) of + error -> error; + StreamID -> maps:get(StreamID, Streams) + end. + +create_stream(State=#http2_state{streams=Streams, stream_refs=Refs}, + Stream=#stream{id=StreamID, ref=StreamRef}) -> + State#http2_state{ + streams=Streams#{StreamID => Stream}, + stream_refs=Refs#{StreamRef => StreamID} + }. + +store_stream(State=#http2_state{streams=Streams}, Stream=#stream{id=StreamID}) -> + State#http2_state{streams=Streams#{StreamID => Stream}}. + +take_stream(State=#http2_state{streams=Streams0, stream_refs=Refs}, StreamID) -> + case maps:take(StreamID, Streams0) of + {Stream=#stream{ref=StreamRef}, Streams} -> + {Stream, State#http2_state{ + streams=Streams, + stream_refs=maps:remove(StreamRef, Refs) + }}; + error -> + error + end. + +maybe_delete_stream(State=#http2_state{http2_machine=HTTP2Machine}, StreamID, local, fin) -> + case cow_http2_machine:get_stream_remote_state(StreamID, HTTP2Machine) of + {ok, fin} -> delete_stream(State, StreamID); + {error, closed} -> delete_stream(State, StreamID); + _ -> State + end; +maybe_delete_stream(State=#http2_state{http2_machine=HTTP2Machine}, StreamID, remote, fin) -> + case cow_http2_machine:get_stream_local_state(StreamID, HTTP2Machine) of + {ok, fin, _} -> delete_stream(State, StreamID); + {error, closed} -> delete_stream(State, StreamID); + _ -> State + end; +maybe_delete_stream(State, _, _, _) -> + State. + +delete_stream(State=#http2_state{streams=Streams, stream_refs=Refs}, StreamID) -> + #{StreamID := #stream{ref=StreamRef}} = Streams, + State#http2_state{ + streams=maps:remove(StreamID, Streams), + stream_refs=maps:remove(StreamRef, Refs) + }. diff --git a/gun/src/gun_pool.erl b/gun/src/gun_pool.erl new file mode 100644 index 0000000..66e2d18 --- /dev/null +++ b/gun/src/gun_pool.erl @@ -0,0 +1,719 @@ +%% Copyright (c) 2021-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_pool). +-behaviour(gen_statem). + +%% Pools. +-export([start_pool/3]). +-export([stop_pool/2]). +-export([stop_pool/3]). +% @todo shutdown pool? +-export([info/0]). +-export([info/1]). +-export([info/2]). +-export([await_up/1]). +-export([await_up/2]). +-export([checkout/2]). %% Use responsibly! + +%% Requests. +-export([delete/2]). +-export([delete/3]). +-export([get/2]). +-export([get/3]). +-export([head/2]). +-export([head/3]). +-export([options/2]). +-export([options/3]). +-export([patch/2]). +-export([patch/3]). +-export([patch/4]). +-export([post/2]). +-export([post/3]). +-export([post/4]). +-export([put/2]). +-export([put/3]). +-export([put/4]). + +%% Generic requests interface. +-export([headers/3]). +-export([headers/4]). +-export([request/4]). +-export([request/5]). + +%% Streaming data. +-export([data/3]). + +%% Tunneling. (HTTP/2+ only.) +%% @todo -export([connect/2]). +%% @todo -export([connect/3]). +%% @todo -export([connect/4]). + +%% Cookies. +%% @todo -export([gc_cookies/1]). +%% @todo -export([session_gc_cookies/1]). + +%% Awaiting gun messages. +-export([await/1]). +-export([await/2]). +-export([await/3]). +-export([await_body/1]). +-export([await_body/2]). +-export([await_body/3]). + +%% Flushing gun messages. +-export([flush/1]). + +%% Streams. +-export([update_flow/2]). +-export([cancel/1]). +-export([stream_info/1]). + +%% Websocket. (HTTP/2+ only for upgrade.) +%% -export([ws_upgrade/1]). +%% -export([ws_upgrade/2]). +%% -export([ws_upgrade/3]). +-export([ws_send/2]). +%% -export([ws_send/3]). (HTTP/2+ only.) + +%% Internals. +-export([callback_mode/0]). +-export([start_link/3]). +-export([init/1]). +-export([degraded/3]). +-export([operational/3]). +-export([terminate/3]). + +-type setup_msg() :: {gun_up, pid(), http | http2 | raw | socks} + | {gun_upgrade, pid(), gun:stream_ref(), [binary()], [{binary(), binary()}]}. + +-type opts() :: #{ + conn_opts => gun:opts(), + scope => any(), + setup_fun => {fun((pid(), setup_msg(), any()) -> any()), any()}, + size => non_neg_integer() +}. +-export_type([opts/0]). + +-type pool_stream_ref() :: {pid(), gun:stream_ref()}. +-export_type([pool_stream_ref/0]). + +-type error_result() :: {error, pool_not_found | no_connection_available, atom()}. %% @todo {pool_start_error, SupError} + +-type request_result() :: {async, pool_stream_ref()} + %% @todo {sync, ...} perhaps with Status, Headers, Body, and Extra info such as intermediate responses. + | error_result(). + +-type req_opts() :: #{ + %% Options common with normal Gun. + flow => pos_integer(), + reply_to => pid(), +% @todo tunnel => stream_ref(), + + %% Options specific to pools. + checkout_call_timeout => timeout(), + checkout_retry => [pos_integer()], + scope => any(), + start_pool_if_missing => boolean() +}. +-export_type([req_opts/0]). + +-type ws_send_opts() :: #{ + authority => iodata(), + + %% Options specific to pools. + checkout_call_timeout => timeout(), + checkout_retry => [pos_integer()], + scope => any(), + start_pool_if_missing => boolean() +}. +-export_type([ws_send_opts/0]). + +%% @todo tunnel +-type meta() :: #{pid() => #{ws => gun:stream_ref()}}. + +-record(state, { + host :: inet:hostname() | inet:ip_address(), + port :: inet:port_number(), + opts :: opts(), + table :: ets:tid(), + conns :: #{pid() => down | {setup, any()} | {up, http | http2 | ws | raw, map()}}, + conns_meta = #{} :: meta(), + await_up = [] :: [{pid(), any()}] +}). + +%% Pool management. + +-spec start_pool(inet:hostname() | inet:ip_address(), inet:port_number(), opts()) + -> {ok, pid()} | {error, any()}. +start_pool(Host, Port, Opts) -> + supervisor:start_child(gun_pools_sup, [Host, Port, Opts]). + +-spec stop_pool(inet:hostname() | inet:ip_address(), inet:port_number()) + -> ok. +stop_pool(Host, Port) -> + stop_pool(Host, Port, #{}). + +-type stop_opts() :: #{ + scope => any(), + transport => tcp | tls +}. + +-spec stop_pool(inet:hostname() | inet:ip_address(), inet:port_number(), stop_opts()) + -> ok | {error, pool_not_found, atom()}. +stop_pool(Host, Port, StopOpts) -> + Transport = maps:get(transport, StopOpts, gun:default_transport(Port)), + Authority = gun_http:host_header(Transport, Host, Port), + case get_pool(Authority, StopOpts) of + undefined -> + {error, pool_not_found, + 'No pool was found for the given scope and authority.'}; + ManagerPid -> + supervisor:terminate_child(gun_pools_sup, ManagerPid) + end. + +-spec info() -> [map()]. +info() -> + ets:foldl(fun({{Scope, _}, ManagerPid}, Acc) -> + {StateName, Info} = info(ManagerPid), + [Info#{scope => Scope, state => StateName}|Acc] + end, [], gun_pools). + +-spec info(pid() | binary()) -> undefined | {degraded | operational, map()}. +info(ManagerPid) when is_pid(ManagerPid) -> + gen_statem:call(ManagerPid, info); +info(Authority) -> + info(Authority, default). + +-spec info(binary(), any()) -> undefined | {degraded | operational, map()}. +info(Authority, Scope) -> + case ets:lookup(gun_pools, {Scope, Authority}) of + [] -> + undefined; + [{_, ManagerPid}] -> + gen_statem:call(ManagerPid, info) + end. + +-spec await_up(pid() | binary()) -> ok | {error, pool_not_found, atom()}. +await_up(ManagerPid) when is_pid(ManagerPid) -> + gen_statem:call(ManagerPid, await_up, 5000); +await_up(Authority) -> + await_up(Authority, default). + +-spec await_up(binary(), any()) -> ok | {error, pool_not_found, atom()}. +await_up(Authority, Scope) -> + case ets:lookup(gun_pools, {Scope, Authority}) of + [] -> + {error, pool_not_found, + 'No pool was found for the given scope and authority.'}; + [{_, ManagerPid}] -> + gen_statem:call(ManagerPid, await_up, 5000) + end. + +-spec checkout(pid(), req_opts() | ws_send_opts()) -> undefined | {pid(), map()}. +checkout(ManagerPid, ReqOpts=#{checkout_retry := Retry}) when is_list(Retry) -> + CallTimeout = maps:get(checkout_call_timeout, ReqOpts, 5000), + case gen_server:call(ManagerPid, {checkout, ReqOpts}, CallTimeout) of + undefined -> + checkout_retry(ManagerPid, ReqOpts, CallTimeout, Retry); + Result -> + Result + end; +checkout(ManagerPid, ReqOpts) -> + CallTimeout = maps:get(checkout_call_timeout, ReqOpts, 5000), + gen_server:call(ManagerPid, {checkout, ReqOpts}, CallTimeout). + +%% When the checkout_retry option is used, and the first call resulted +%% in no connection being given out, we wait for the configured amount +%% of time then try again. We loop over the wait times until there is +%% none. +checkout_retry(_, _, _, []) -> + undefined; +checkout_retry(ManagerPid, ReqOpts, CallTimeout, [Wait|Retry]) -> + timer:sleep(Wait), + case gen_server:call(ManagerPid, {checkout, ReqOpts}, CallTimeout) of + undefined -> + checkout_retry(ManagerPid, ReqOpts, CallTimeout, Retry); + Result -> + Result + end. + +%% Requests. + +-spec delete(iodata(), gun:req_headers()) -> request_result(). +delete(Path, Headers) -> + request(<<"DELETE">>, Path, Headers, <<>>). + +-spec delete(iodata(), gun:req_headers(), req_opts()) -> request_result(). +delete(Path, Headers, ReqOpts) -> + request(<<"DELETE">>, Path, Headers, <<>>, ReqOpts). + +-spec get(iodata(), gun:req_headers()) -> request_result(). +get(Path, Headers) -> + request(<<"GET">>, Path, Headers, <<>>). + +-spec get(iodata(), gun:req_headers(), req_opts()) -> request_result(). +get(Path, Headers, ReqOpts) -> + request(<<"GET">>, Path, Headers, <<>>, ReqOpts). + +-spec head(iodata(), gun:req_headers()) -> request_result(). +head(Path, Headers) -> + request(<<"HEAD">>, Path, Headers, <<>>). + +-spec head(iodata(), gun:req_headers(), req_opts()) -> request_result(). +head(Path, Headers, ReqOpts) -> + request(<<"HEAD">>, Path, Headers, <<>>, ReqOpts). + +-spec options(iodata(), gun:req_headers()) -> request_result(). +options(Path, Headers) -> + request(<<"OPTIONS">>, Path, Headers, <<>>). + +-spec options(iodata(), gun:req_headers(), req_opts()) -> request_result(). +options(Path, Headers, ReqOpts) -> + request(<<"OPTIONS">>, Path, Headers, <<>>, ReqOpts). + +-spec patch(iodata(), gun:req_headers()) -> request_result(). +patch(Path, Headers) -> + headers(<<"PATCH">>, Path, Headers). + +-spec patch(iodata(), gun:req_headers(), iodata() | req_opts()) -> request_result(). +patch(Path, Headers, ReqOpts) when is_map(ReqOpts) -> + headers(<<"PATCH">>, Path, Headers, ReqOpts); +patch(Path, Headers, Body) -> + request(<<"PATCH">>, Path, Headers, Body). + +-spec patch(iodata(), gun:req_headers(), iodata(), req_opts()) -> request_result(). +patch(Path, Headers, Body, ReqOpts) -> + request(<<"PATCH">>, Path, Headers, Body, ReqOpts). + +-spec post(iodata(), gun:req_headers()) -> request_result(). +post(Path, Headers) -> + headers(<<"POST">>, Path, Headers). + +-spec post(iodata(), gun:req_headers(), iodata() | req_opts()) -> request_result(). +post(Path, Headers, ReqOpts) when is_map(ReqOpts) -> + headers(<<"POST">>, Path, Headers, ReqOpts); +post(Path, Headers, Body) -> + request(<<"POST">>, Path, Headers, Body). + +-spec post(iodata(), gun:req_headers(), iodata(), req_opts()) -> request_result(). +post(Path, Headers, Body, ReqOpts) -> + request(<<"POST">>, Path, Headers, Body, ReqOpts). + +-spec put(iodata(), gun:req_headers()) -> request_result(). +put(Path, Headers) -> + headers(<<"PUT">>, Path, Headers). + +-spec put(iodata(), gun:req_headers(), iodata() | req_opts()) -> request_result(). +put(Path, Headers, ReqOpts) when is_map(ReqOpts) -> + headers(<<"PUT">>, Path, Headers, ReqOpts); +put(Path, Headers, Body) -> + request(<<"PUT">>, Path, Headers, Body). + +-spec put(iodata(), gun:req_headers(), iodata(), req_opts()) -> request_result(). +put(Path, Headers, Body, ReqOpts) -> + request(<<"PUT">>, Path, Headers, Body, ReqOpts). + +%% Generic requests interface. +%% +%% @todo Accept a TargetURI map as well as a normal Path. + +-spec headers(iodata(), iodata(), gun:req_headers()) -> request_result(). +headers(Method, Path, Headers) -> + headers(Method, Path, Headers, #{}). + +-spec headers(iodata(), iodata(), gun:req_headers(), req_opts()) -> request_result(). +headers(Method, Path, Headers, ReqOpts) -> + case get_pool(authority(Headers), ReqOpts) of + undefined -> + {error, pool_not_found, + 'No pool was found for the given scope and authority.'}; + ManagerPid -> + case checkout(ManagerPid, ReqOpts) of + undefined -> + {error, no_connection_available, + 'No connection in the pool with enough capacity available to open a new stream.'}; + {ConnPid, _Meta} -> + StreamRef = gun:headers(ConnPid, Method, Path, Headers, ReqOpts), + %% @todo Synchronous mode. + {async, {ConnPid, StreamRef}} + end + end. + +-spec request(iodata(), iodata(), gun:req_headers(), iodata()) -> request_result(). +request(Method, Path, Headers, Body) -> + request(Method, Path, Headers, Body, #{}). + +-spec request(iodata(), iodata(), gun:req_headers(), iodata(), req_opts()) -> request_result(). +request(Method, Path, Headers, Body, ReqOpts) -> + case get_pool(authority(Headers), ReqOpts) of + undefined -> + {error, pool_not_found, + 'No pool was found for the given scope and authority.'}; + ManagerPid -> + case checkout(ManagerPid, ReqOpts) of + undefined -> + {error, no_connection_available, + 'No connection in the pool with enough capacity available to open a new stream.'}; + {ConnPid, _Meta} -> + StreamRef = gun:request(ConnPid, Method, Path, Headers, Body, ReqOpts), + %% @todo Synchronous mode. + {async, {ConnPid, StreamRef}} + end + end. + +%% We require the host to be given in the headers for the time being. +%% @todo Allow passing it in options. Websocket send already does that. +authority(#{<<"host">> := Authority}) -> + Authority; +authority(Headers) -> + {_, Authority} = lists:keyfind(<<"host">>, 1, Headers), + Authority. + +-spec get_pool(iolist(), req_opts() | ws_send_opts() | stop_opts()) -> pid() | undefined. +get_pool(Authority0, ReqOpts) -> + Authority = iolist_to_binary(Authority0), + %% @todo Perhaps rename this to temporary. + %% There's two concepts: temporary pool is started and stops + %% when there's no traffic. Dynamic pool simply reduces its number + %% of connections when there's no traffic. + StartPoolIfMissing = maps:get(start_pool_if_missing, ReqOpts, false), + Scope = maps:get(scope, ReqOpts, default), + case ets:lookup(gun_pools, {Scope, Authority}) of + [] when StartPoolIfMissing -> + start_missing_pool(Authority, ReqOpts); + [] -> + undefined; + [{_, ManagerPid}] -> + %% @todo With temporary pool, getting a pid here doesn't mean the pool can be used. + %% Indeed the manager could be in process of stopping. I suppose we must + %% do a check but perhaps it's best to leave that detail to the user + %% (they can easily retry and recreate the pool if necessary). + ManagerPid + end. + +start_missing_pool(_Authority, _ReqOpts) -> + undefined. + +%% Streaming data. + +-spec data(pool_stream_ref(), fin | nofin, iodata()) -> ok. +data({ConnPid, StreamRef}, IsFin, Data) -> + gun:data(ConnPid, StreamRef, IsFin, Data). + +%% Awaiting gun messages. + +-spec await(pool_stream_ref()) -> gun:await_result(). +await({ConnPid, StreamRef}) -> + gun:await(ConnPid, StreamRef). + +-spec await(pool_stream_ref(), timeout() | reference()) -> gun:await_result(). +await({ConnPid, StreamRef}, MRefOrTimeout) -> + gun:await(ConnPid, StreamRef, MRefOrTimeout). + +-spec await(pool_stream_ref(), timeout(), reference()) -> gun:await_result(). +await({ConnPid, StreamRef}, Timeout, MRef) -> + gun:await(ConnPid, StreamRef, Timeout, MRef). + +-spec await_body(pool_stream_ref()) -> gun:await_body_result(). +await_body({ConnPid, StreamRef}) -> + gun:await_body(ConnPid, StreamRef). + +-spec await_body(pool_stream_ref(), timeout() | reference()) -> gun:await_body_result(). +await_body({ConnPid, StreamRef}, MRefOrTimeout) -> + gun:await_body(ConnPid, StreamRef, MRefOrTimeout). + +-spec await_body(pool_stream_ref(), timeout(), reference()) -> gun:await_body_result(). +await_body({ConnPid, StreamRef}, Timeout, MRef) -> + gun:await_body(ConnPid, StreamRef, Timeout, MRef). + +%% Flushing gun messages. + +-spec flush(pool_stream_ref()) -> ok. +flush({ConnPid, _}) -> + gun:flush(ConnPid). + +%% Flow control. + +-spec update_flow(pool_stream_ref(), pos_integer()) -> ok. +update_flow({ConnPid, StreamRef}, Flow) -> + gun:update_flow(ConnPid, StreamRef, Flow). + +%% Cancelling a stream. + +-spec cancel(pool_stream_ref()) -> ok. +cancel({ConnPid, StreamRef}) -> + gun:cancel(ConnPid, StreamRef). + +%% Information about a stream. + +-spec stream_info(pool_stream_ref()) -> {ok, map() | undefined} | {error, not_connected}. +stream_info({ConnPid, StreamRef}) -> + gun:stream_info(ConnPid, StreamRef). + +%% Websocket. + +-spec ws_send(gun:ws_frame() | [gun:ws_frame()], ws_send_opts()) -> ok | error_result(). +ws_send(Frames, WsSendOpts=#{authority := Authority}) -> + case get_pool(Authority, WsSendOpts) of + undefined -> + {error, pool_not_found, + 'No pool was found for the given scope and authority.'}; + ManagerPid -> + case checkout(ManagerPid, WsSendOpts) of + undefined -> + {error, no_connection_available, + 'No connection in the pool with enough capacity available to send Websocket frames.'}; + {ConnPid, #{ws := StreamRef}} -> + gun:ws_send(ConnPid, StreamRef, Frames) + end + end. + +%% Pool manager internals. +%% +%% The pool manager is responsible for starting connection processes +%% and restarting them as necessary. It also provides a suitable +%% connection process to any caller that needs it. +%% +%% The pool manager installs an event handler into each connection. +%% The event handler is responsible for counting the number of +%% active streams. It updates the gun_pooled_conns ets table +%% whenever a stream begins or ends. +%% +%% A connection is deemed suitable if it is possible to open new +%% streams. How many streams can be open at any one time depends +%% on the protocol. For HTTP/2 the manager process keeps track of +%% the connection's settings to know the maximum. For non-stream +%% based protocols, there is no limit. +%% +%% The connection to be used is otherwise chosen randomly. The +%% first connection that is suitable is returned. There is no +%% need to "give back" the connection to the manager. + +%% @todo +%% What should happen if we always fail to reconnect? I suspect we keep the manager +%% around and propagate errors, the same as if there's no more capacity? Perhaps have alarms? + +callback_mode() -> state_functions. + +start_link(Host, Port, Opts) -> + gen_statem:start_link(?MODULE, {Host, Port, Opts}, []). + +init({Host, Port, Opts}) -> + process_flag(trap_exit, true), + true = ets:insert_new(gun_pools, {gun_pools_key(Host, Port, Opts), self()}), + Tid = ets:new(gun_pooled_conns, [ordered_set, public]), + Size = maps:get(size, Opts, 8), + %% @todo Only start processes in static mode. + ConnOpts = conn_opts(Tid, Opts), + Conns = maps:from_list([begin + {ok, ConnPid} = gun:open(Host, Port, ConnOpts), + _ = monitor(process, ConnPid), + {ConnPid, down} + end || _ <- lists:seq(1, Size)]), + State = #state{ + host=Host, + port=Port, + opts=Opts, + table=Tid, + conns=Conns + }, + %% If Size is 0 then we can never be operational. + {ok, degraded, State}. + +gun_pools_key(Host, Port, Opts) -> + Transport = maps:get(transport, Opts, gun:default_transport(Port)), + Authority = gun_http:host_header(Transport, Host, Port), + Scope = maps:get(scope, Opts, default), + {Scope, iolist_to_binary(Authority)}. + +conn_opts(Tid, Opts) -> + ConnOpts = maps:get(conn_opts, Opts, #{}), + EventHandlerState = maps:with([event_handler], ConnOpts), + H2Opts = maps:get(http2_opts, ConnOpts, #{}), + ConnOpts#{ + event_handler => {gun_pool_events_h, EventHandlerState#{ + table => Tid + }}, + http2_opts => H2Opts#{ + notify_settings_changed => true + } + }. + +%% We use the degraded state as long as at least one connection is degraded. +%% @todo Probably keep count of connections separately to avoid counting every time. +degraded(info, Msg={gun_up, ConnPid, _}, StateData=#state{opts=Opts, conns=Conns}) -> + #{ConnPid := down} = Conns, + %% We optionally run the setup function if one is defined. The + %% setup function tells us whether we are fully up or not. The + %% setup function may be called repeatedly until the connection + %% is established. + %% + %% @todo It is possible that the connection never does get + %% fully established. We should deal with this. We probably + %% need to handle all messages. + {SetupFun, SetupState0} = setup_fun(Opts), + degraded_setup(ConnPid, Msg, StateData, SetupFun, SetupState0); +%% @todo +%degraded(info, Msg={gun_tunnel_up, ConnPid, _, _}, StateData0=#state{conns=Conns}) -> +% ; +degraded(info, Msg={gun_upgrade, ConnPid, _, _, _}, + StateData=#state{opts=#{setup_fun := {SetupFun, _}}, conns=Conns}) -> + %% @todo Probably shouldn't crash if the state is incorrect, that's programmer error though. + #{ConnPid := {setup, SetupState0}} = Conns, + %% We run the setup function again using the state previously kept. + degraded_setup(ConnPid, Msg, StateData, SetupFun, SetupState0); +degraded(Type, Event, StateData) -> + handle_common(Type, Event, ?FUNCTION_NAME, StateData). + +setup_fun(#{setup_fun := SetupFun}) -> + SetupFun; +setup_fun(_) -> + {fun (_, {gun_up, _, Protocol}, _) -> + {up, Protocol, #{}} + end, undefined}. + +degraded_setup(ConnPid, Msg, StateData0=#state{conns=Conns, conns_meta=ConnsMeta, + await_up=AwaitUp}, SetupFun, SetupState0) -> + case SetupFun(ConnPid, Msg, SetupState0) of + Setup={setup, _SetupState} -> + StateData = StateData0#state{conns=Conns#{ConnPid => Setup}}, + {keep_state, StateData}; + %% The Meta is different from Settings. It allows passing around + %% Websocket or tunnel stream refs. + {up, Protocol, Meta} -> + Settings = #{}, + StateData = StateData0#state{ + conns=Conns#{ConnPid => {up, Protocol, Settings}}, + conns_meta=ConnsMeta#{ConnPid => Meta} + }, + case is_degraded(StateData) of + true -> {keep_state, StateData}; + false -> {next_state, operational, StateData#state{await_up=[]}, + [{reply, ReplyTo, ok} || ReplyTo <- AwaitUp]} + end + end. + +is_degraded(#state{conns=Conns0}) -> + Conns = maps:to_list(Conns0), + Len = length(Conns), + Ups = [up || {_, {up, _, _}} <- Conns], + Len =/= length(Ups). + +operational(Type, Event, StateData) -> + handle_common(Type, Event, ?FUNCTION_NAME, StateData). + +handle_common({call, From}, {checkout, _ReqOpts}, _, + StateData=#state{conns_meta=ConnsMeta}) -> + case find_available_connection(StateData) of + none -> + {keep_state_and_data, {reply, From, undefined}}; + ConnPid -> + Meta = maps:get(ConnPid, ConnsMeta, #{}), + {keep_state_and_data, {reply, From, {ConnPid, Meta}}} + end; +handle_common(info, {gun_notify, ConnPid, settings_changed, Settings}, _, StateData=#state{conns=Conns}) -> + %% Assert that the state is correct. + {up, http2, _} = maps:get(ConnPid, Conns), + {keep_state, StateData#state{conns=Conns#{ConnPid => {up, http2, Settings}}}}; +handle_common(info, {gun_down, ConnPid, Protocol, _Reason, _KilledStreams}, _, StateData=#state{conns=Conns}) -> + {up, Protocol, _} = maps:get(ConnPid, Conns), + {next_state, degraded, StateData#state{conns=Conns#{ConnPid => down}}}; +%% @todo We do not want to reconnect automatically when the pool is dynamic. +handle_common(info, {'DOWN', _MRef, process, ConnPid0, Reason}, _, + StateData=#state{host=Host, port=Port, opts=Opts, table=Tid, conns=Conns0, conns_meta=ConnsMeta0}) -> + Conns = maps:remove(ConnPid0, Conns0), + ConnsMeta = maps:remove(ConnPid0, ConnsMeta0), + case Reason of + %% The process is down because of a configuration error. + %% Do NOT attempt to reconnect, leave the pool in a degraded state. + badarg -> + {next_state, degraded, StateData#state{conns=Conns, conns_meta=ConnsMeta}}; + _ -> + ConnOpts = conn_opts(Tid, Opts), + {ok, ConnPid} = gun:open(Host, Port, ConnOpts), + _ = monitor(process, ConnPid), + {next_state, degraded, StateData#state{conns=Conns#{ConnPid => down}, conns_meta=ConnsMeta}} + end; +handle_common({call, From}, info, StateName, #state{host=Host, port=Port, + opts=Opts, table=Tid, conns=Conns, conns_meta=ConnsMeta}) -> + {keep_state_and_data, {reply, From, {StateName, #{ + %% @todo Not sure whether all of this should be documented. Maybe not ConnsMeta for now? + host => Host, + port => Port, + opts => Opts, + table => Tid, + conns => Conns, + conns_meta => ConnsMeta + }}}}; +handle_common({call, From}, await_up, operational, _) -> + {keep_state_and_data, {reply, From, ok}}; +handle_common({call, From}, await_up, _, StateData=#state{await_up=AwaitUp}) -> + {keep_state, StateData#state{await_up=[From|AwaitUp]}}; +handle_common(Type, Event, StateName, StateData) -> + logger:error("Unexpected event in state ~p of type ~p:~n~w~n~p~n", + [StateName, Type, Event, StateData]), + keep_state_and_data. + +%% We go over every connection and return the first one +%% we find that has capacity. How we determine whether +%% capacity is available depends on the protocol. For +%% HTTP/2 we look into the protocol settings. The +%% current number of streams is maintained by the +%% event handler gun_pool_events_h. +find_available_connection(#state{table=Tid, conns=Conns}) -> + I = lists:sort([{rand:uniform(), K} || K <- maps:keys(Conns)]), + find_available_connection(I, Conns, Tid). + +find_available_connection([], _, _) -> + none; +find_available_connection([{_, ConnPid}|I], Conns, Tid) -> + case maps:get(ConnPid, Conns) of + {up, Protocol, Settings} -> + MaxStreams = max_streams(Protocol, Settings), + CurrentStreams = case ets:lookup(Tid, ConnPid) of + [] -> + 0; + [{_, CS}] -> + CS + end, + if + CurrentStreams + 1 > MaxStreams -> + find_available_connection(I, Conns, Tid); + true -> + ConnPid + end; + _ -> + find_available_connection(I, Conns, Tid) + end. + +max_streams(http, _) -> + 1; +max_streams(http2, #{max_concurrent_streams := MaxStreams}) -> + MaxStreams; +max_streams(http2, #{}) -> + infinity; +%% There are no streams or Gun is not aware of streams when +%% the protocol is Websocket or raw. +max_streams(ws, _) -> + infinity; +max_streams(raw, _) -> + infinity. + +terminate(Reason, StateName, #state{host=Host, port=Port, opts=Opts, await_up=AwaitUp}) -> + gen_statem:reply([ + {reply, ReplyTo, {error, {terminate, StateName, Reason}}} + || ReplyTo <- AwaitUp]), + true = ets:delete(gun_pools, gun_pools_key(Host, Port, Opts)), + ok. diff --git a/gun/src/gun_pool_events_h.erl b/gun/src/gun_pool_events_h.erl new file mode 100644 index 0000000..09a5e74 --- /dev/null +++ b/gun/src/gun_pool_events_h.erl @@ -0,0 +1,157 @@ +%% Copyright (c) 2021-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_pool_events_h). + +-export([init/2]). +-export([domain_lookup_start/2]). +-export([domain_lookup_end/2]). +-export([connect_start/2]). +-export([connect_end/2]). +-export([tls_handshake_start/2]). +-export([tls_handshake_end/2]). +-export([request_start/2]). +-export([request_headers/2]). +-export([request_end/2]). +-export([push_promise_start/2]). +-export([push_promise_end/2]). +-export([response_start/2]). +-export([response_inform/2]). +-export([response_headers/2]). +-export([response_trailers/2]). +-export([response_end/2]). +-export([ws_upgrade/2]). +-export([ws_recv_frame_start/2]). +-export([ws_recv_frame_header/2]). +-export([ws_recv_frame_end/2]). +-export([ws_send_frame_start/2]). +-export([ws_send_frame_end/2]). +-export([protocol_changed/2]). +-export([origin_changed/2]). +-export([cancel/2]). +-export([disconnect/2]). +-export([terminate/2]). + +init(Event, State) -> + propagate(Event, State, ?FUNCTION_NAME). + +domain_lookup_start(Event, State) -> + propagate(Event, State, ?FUNCTION_NAME). + +domain_lookup_end(Event, State) -> + propagate(Event, State, ?FUNCTION_NAME). + +connect_start(Event, State) -> + propagate(Event, State, ?FUNCTION_NAME). + +connect_end(Event, State) -> + propagate(Event, State, ?FUNCTION_NAME). + +tls_handshake_start(Event, State) -> + propagate(Event, State, ?FUNCTION_NAME). + +tls_handshake_end(Event, State) -> + propagate(Event, State, ?FUNCTION_NAME). + +request_start(Event=#{stream_ref := StreamRef}, State=#{table := Tid}) -> + _ = ets:update_counter(Tid, self(), +1, {self(), 0}), + propagate(Event, State#{ + StreamRef => {nofin, nofin} + }, ?FUNCTION_NAME). + +request_headers(Event, State) -> + propagate(Event, State, ?FUNCTION_NAME). + +request_end(Event=#{stream_ref := StreamRef}, State0=#{table := Tid}) -> + State = case State0 of + #{StreamRef := {nofin, fin}} -> + _ = ets:update_counter(Tid, self(), -1), + maps:remove(StreamRef, State0); + #{StreamRef := {nofin, IsFin}} -> + State0#{StreamRef => {fin, IsFin}} + end, + propagate(Event, State, ?FUNCTION_NAME). + +push_promise_start(Event, State) -> + propagate(Event, State, ?FUNCTION_NAME). + +push_promise_end(Event, State) -> + propagate(Event, State, ?FUNCTION_NAME). + +response_start(Event, State) -> + propagate(Event, State, ?FUNCTION_NAME). + +response_inform(Event, State) -> + propagate(Event, State, ?FUNCTION_NAME). + +response_headers(Event, State) -> + propagate(Event, State, ?FUNCTION_NAME). + +response_trailers(Event, State) -> + propagate(Event, State, ?FUNCTION_NAME). + +response_end(Event=#{stream_ref := StreamRef}, State0=#{table := Tid}) -> + State = case State0 of + #{StreamRef := {fin, nofin}} -> + _ = ets:update_counter(Tid, self(), -1), + maps:remove(StreamRef, State0); + #{StreamRef := {IsFin, nofin}} -> + State0#{StreamRef => {IsFin, fin}} + end, + propagate(Event, State, ?FUNCTION_NAME). + +ws_upgrade(Event, State) -> + propagate(Event, State, ?FUNCTION_NAME). + +ws_recv_frame_start(Event, State) -> + propagate(Event, State, ?FUNCTION_NAME). + +ws_recv_frame_header(Event, State) -> + propagate(Event, State, ?FUNCTION_NAME). + +ws_recv_frame_end(Event, State) -> + propagate(Event, State, ?FUNCTION_NAME). + +ws_send_frame_start(Event, State) -> + propagate(Event, State, ?FUNCTION_NAME). + +ws_send_frame_end(Event, State) -> + propagate(Event, State, ?FUNCTION_NAME). + +protocol_changed(Event, State) -> + propagate(Event, State, ?FUNCTION_NAME). + +origin_changed(Event, State) -> + propagate(Event, State, ?FUNCTION_NAME). + +cancel(Event, State) -> + propagate(Event, State, ?FUNCTION_NAME). + +disconnect(Event, State=#{table := Tid}) -> + %% The ets:delete/2 call might fail when the pool has shut down. + try + true = ets:delete(Tid, self()) + catch _:_ -> + ok + end, + propagate(Event, maps:with([event_handler, table], State), ?FUNCTION_NAME). + +terminate(Event, State) -> + propagate(Event, State, ?FUNCTION_NAME). + +propagate(Event, State=#{event_handler := {Mod, ModState0}}, Fun) -> + ModState = Mod:Fun(Event, ModState0), + State#{event_handler => {Mod, ModState}}; +propagate(_, State, _) -> + State. diff --git a/gun/src/gun_pools_sup.erl b/gun/src/gun_pools_sup.erl new file mode 100644 index 0000000..af7ddf6 --- /dev/null +++ b/gun/src/gun_pools_sup.erl @@ -0,0 +1,37 @@ +%% Copyright (c) 2021-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_pools_sup). +-behaviour(supervisor). + +%% API. +-export([start_link/0]). + +%% supervisor. +-export([init/1]). + +%% API. + +-spec start_link() -> {ok, pid()}. +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +%% supervisor. + +init([]) -> + %% @todo Review restart strategies. + Procs = [ + #{id => gun_pool, start => {gun_pool, start_link, []}, restart => transient} + ], + {ok, {#{strategy => simple_one_for_one}, Procs}}. diff --git a/gun/src/gun_protocols.erl b/gun/src/gun_protocols.erl new file mode 100644 index 0000000..4232e2f --- /dev/null +++ b/gun/src/gun_protocols.erl @@ -0,0 +1,72 @@ +%% Copyright (c) 2020-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_protocols). + +-export([add_stream_ref/2]). +-export([handler/1]). +-export([handler_and_opts/2]). +-export([negotiated/2]). +-export([stream_ref/1]). + +-spec add_stream_ref(Protocol, undefined | gun:stream_ref()) + -> Protocol when Protocol :: gun:protocol(). +add_stream_ref(Protocol, undefined) -> + Protocol; +add_stream_ref({ProtocolName, ProtocolOpts}, StreamRef) -> + {ProtocolName, ProtocolOpts#{stream_ref => StreamRef}}; +add_stream_ref(ProtocolName, StreamRef) -> + {ProtocolName, #{stream_ref => StreamRef}}. + +-spec handler(gun:protocol()) -> module(). +handler(http) -> gun_http; +handler({http, _}) -> gun_http; +handler(http2) -> gun_http2; +handler({http2, _}) -> gun_http2; +handler(raw) -> gun_raw; +handler({raw, _}) -> gun_raw; +handler(socks) -> gun_socks; +handler({socks, _}) -> gun_socks; +handler(ws) -> gun_ws; +handler({ws, _}) -> gun_ws. + +-spec handler_and_opts(gun:protocol(), map()) -> {module(), map()}. +handler_and_opts({ProtocolName, ProtocolOpts}, _) -> + {handler(ProtocolName), ProtocolOpts}; +handler_and_opts(ProtocolName, Opts) -> + Protocol = handler(ProtocolName), + {Protocol, maps:get(Protocol:opts_name(), Opts, #{})}. + +-spec negotiated({ok, binary()} | {error, protocol_not_negotiated}, gun:protocols()) + -> gun:protocol(). +negotiated({ok, <<"h2">>}, Protocols) -> + lists:foldl(fun + (E = http2, _) -> E; + (E = {http2, _}, _) -> E; + (_, Acc) -> Acc + end, http2, Protocols); +negotiated({ok, <<"http/1.1">>}, Protocols) -> + lists:foldl(fun + (E = http, _) -> E; + (E = {http, _}, _) -> E; + (_, Acc) -> Acc + end, http, Protocols); +negotiated({error, protocol_not_negotiated}, [Protocol]) -> + Protocol; +negotiated({error, protocol_not_negotiated}, _) -> + http. + +-spec stream_ref(gun:protocol()) -> undefined | gun:stream_ref(). +stream_ref({_, ProtocolOpts}) -> maps:get(stream_ref, ProtocolOpts, undefined); +stream_ref(_) -> undefined. diff --git a/gun/src/gun_public_suffix.erl b/gun/src/gun_public_suffix.erl new file mode 100644 index 0000000..b5b1147 --- /dev/null +++ b/gun/src/gun_public_suffix.erl @@ -0,0 +1,9520 @@ +%% Copyright (c) 2020-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_public_suffix). +-compile(no_type_opt). %% Until at least OTP-23. + +-export([match/1]). + +-spec match(binary()) -> boolean(). +match(Domain) -> + Subdomains = string:split(Domain, <<".">>, all), + m(Subdomains). + +m(S = [<<"ac">>]) -> e(S); +m(S = [<<"com">>,<<"ac">>]) -> e(S); +m(S = [<<"edu">>,<<"ac">>]) -> e(S); +m(S = [<<"gov">>,<<"ac">>]) -> e(S); +m(S = [<<"net">>,<<"ac">>]) -> e(S); +m(S = [<<"mil">>,<<"ac">>]) -> e(S); +m(S = [<<"org">>,<<"ac">>]) -> e(S); +m(S = [<<"ad">>]) -> e(S); +m(S = [<<"nom">>,<<"ad">>]) -> e(S); +m(S = [<<"ae">>]) -> e(S); +m(S = [<<"co">>,<<"ae">>]) -> e(S); +m(S = [<<"net">>,<<"ae">>]) -> e(S); +m(S = [<<"org">>,<<"ae">>]) -> e(S); +m(S = [<<"sch">>,<<"ae">>]) -> e(S); +m(S = [<<"ac">>,<<"ae">>]) -> e(S); +m(S = [<<"gov">>,<<"ae">>]) -> e(S); +m(S = [<<"mil">>,<<"ae">>]) -> e(S); +m(S = [<<"aero">>]) -> e(S); +m(S = [<<"accident-investigation">>,<<"aero">>]) -> e(S); +m(S = [<<"accident-prevention">>,<<"aero">>]) -> e(S); +m(S = [<<"aerobatic">>,<<"aero">>]) -> e(S); +m(S = [<<"aeroclub">>,<<"aero">>]) -> e(S); +m(S = [<<"aerodrome">>,<<"aero">>]) -> e(S); +m(S = [<<"agents">>,<<"aero">>]) -> e(S); +m(S = [<<"aircraft">>,<<"aero">>]) -> e(S); +m(S = [<<"airline">>,<<"aero">>]) -> e(S); +m(S = [<<"airport">>,<<"aero">>]) -> e(S); +m(S = [<<"air-surveillance">>,<<"aero">>]) -> e(S); +m(S = [<<"airtraffic">>,<<"aero">>]) -> e(S); +m(S = [<<"air-traffic-control">>,<<"aero">>]) -> e(S); +m(S = [<<"ambulance">>,<<"aero">>]) -> e(S); +m(S = [<<"amusement">>,<<"aero">>]) -> e(S); +m(S = [<<"association">>,<<"aero">>]) -> e(S); +m(S = [<<"author">>,<<"aero">>]) -> e(S); +m(S = [<<"ballooning">>,<<"aero">>]) -> e(S); +m(S = [<<"broker">>,<<"aero">>]) -> e(S); +m(S = [<<"caa">>,<<"aero">>]) -> e(S); +m(S = [<<"cargo">>,<<"aero">>]) -> e(S); +m(S = [<<"catering">>,<<"aero">>]) -> e(S); +m(S = [<<"certification">>,<<"aero">>]) -> e(S); +m(S = [<<"championship">>,<<"aero">>]) -> e(S); +m(S = [<<"charter">>,<<"aero">>]) -> e(S); +m(S = [<<"civilaviation">>,<<"aero">>]) -> e(S); +m(S = [<<"club">>,<<"aero">>]) -> e(S); +m(S = [<<"conference">>,<<"aero">>]) -> e(S); +m(S = [<<"consultant">>,<<"aero">>]) -> e(S); +m(S = [<<"consulting">>,<<"aero">>]) -> e(S); +m(S = [<<"control">>,<<"aero">>]) -> e(S); +m(S = [<<"council">>,<<"aero">>]) -> e(S); +m(S = [<<"crew">>,<<"aero">>]) -> e(S); +m(S = [<<"design">>,<<"aero">>]) -> e(S); +m(S = [<<"dgca">>,<<"aero">>]) -> e(S); +m(S = [<<"educator">>,<<"aero">>]) -> e(S); +m(S = [<<"emergency">>,<<"aero">>]) -> e(S); +m(S = [<<"engine">>,<<"aero">>]) -> e(S); +m(S = [<<"engineer">>,<<"aero">>]) -> e(S); +m(S = [<<"entertainment">>,<<"aero">>]) -> e(S); +m(S = [<<"equipment">>,<<"aero">>]) -> e(S); +m(S = [<<"exchange">>,<<"aero">>]) -> e(S); +m(S = [<<"express">>,<<"aero">>]) -> e(S); +m(S = [<<"federation">>,<<"aero">>]) -> e(S); +m(S = [<<"flight">>,<<"aero">>]) -> e(S); +m(S = [<<"fuel">>,<<"aero">>]) -> e(S); +m(S = [<<"gliding">>,<<"aero">>]) -> e(S); +m(S = [<<"government">>,<<"aero">>]) -> e(S); +m(S = [<<"groundhandling">>,<<"aero">>]) -> e(S); +m(S = [<<"group">>,<<"aero">>]) -> e(S); +m(S = [<<"hanggliding">>,<<"aero">>]) -> e(S); +m(S = [<<"homebuilt">>,<<"aero">>]) -> e(S); +m(S = [<<"insurance">>,<<"aero">>]) -> e(S); +m(S = [<<"journal">>,<<"aero">>]) -> e(S); +m(S = [<<"journalist">>,<<"aero">>]) -> e(S); +m(S = [<<"leasing">>,<<"aero">>]) -> e(S); +m(S = [<<"logistics">>,<<"aero">>]) -> e(S); +m(S = [<<"magazine">>,<<"aero">>]) -> e(S); +m(S = [<<"maintenance">>,<<"aero">>]) -> e(S); +m(S = [<<"media">>,<<"aero">>]) -> e(S); +m(S = [<<"microlight">>,<<"aero">>]) -> e(S); +m(S = [<<"modelling">>,<<"aero">>]) -> e(S); +m(S = [<<"navigation">>,<<"aero">>]) -> e(S); +m(S = [<<"parachuting">>,<<"aero">>]) -> e(S); +m(S = [<<"paragliding">>,<<"aero">>]) -> e(S); +m(S = [<<"passenger-association">>,<<"aero">>]) -> e(S); +m(S = [<<"pilot">>,<<"aero">>]) -> e(S); +m(S = [<<"press">>,<<"aero">>]) -> e(S); +m(S = [<<"production">>,<<"aero">>]) -> e(S); +m(S = [<<"recreation">>,<<"aero">>]) -> e(S); +m(S = [<<"repbody">>,<<"aero">>]) -> e(S); +m(S = [<<"res">>,<<"aero">>]) -> e(S); +m(S = [<<"research">>,<<"aero">>]) -> e(S); +m(S = [<<"rotorcraft">>,<<"aero">>]) -> e(S); +m(S = [<<"safety">>,<<"aero">>]) -> e(S); +m(S = [<<"scientist">>,<<"aero">>]) -> e(S); +m(S = [<<"services">>,<<"aero">>]) -> e(S); +m(S = [<<"show">>,<<"aero">>]) -> e(S); +m(S = [<<"skydiving">>,<<"aero">>]) -> e(S); +m(S = [<<"software">>,<<"aero">>]) -> e(S); +m(S = [<<"student">>,<<"aero">>]) -> e(S); +m(S = [<<"trader">>,<<"aero">>]) -> e(S); +m(S = [<<"trading">>,<<"aero">>]) -> e(S); +m(S = [<<"trainer">>,<<"aero">>]) -> e(S); +m(S = [<<"union">>,<<"aero">>]) -> e(S); +m(S = [<<"workinggroup">>,<<"aero">>]) -> e(S); +m(S = [<<"works">>,<<"aero">>]) -> e(S); +m(S = [<<"af">>]) -> e(S); +m(S = [<<"gov">>,<<"af">>]) -> e(S); +m(S = [<<"com">>,<<"af">>]) -> e(S); +m(S = [<<"org">>,<<"af">>]) -> e(S); +m(S = [<<"net">>,<<"af">>]) -> e(S); +m(S = [<<"edu">>,<<"af">>]) -> e(S); +m(S = [<<"ag">>]) -> e(S); +m(S = [<<"com">>,<<"ag">>]) -> e(S); +m(S = [<<"org">>,<<"ag">>]) -> e(S); +m(S = [<<"net">>,<<"ag">>]) -> e(S); +m(S = [<<"co">>,<<"ag">>]) -> e(S); +m(S = [<<"nom">>,<<"ag">>]) -> e(S); +m(S = [<<"ai">>]) -> e(S); +m(S = [<<"off">>,<<"ai">>]) -> e(S); +m(S = [<<"com">>,<<"ai">>]) -> e(S); +m(S = [<<"net">>,<<"ai">>]) -> e(S); +m(S = [<<"org">>,<<"ai">>]) -> e(S); +m(S = [<<"al">>]) -> e(S); +m(S = [<<"com">>,<<"al">>]) -> e(S); +m(S = [<<"edu">>,<<"al">>]) -> e(S); +m(S = [<<"gov">>,<<"al">>]) -> e(S); +m(S = [<<"mil">>,<<"al">>]) -> e(S); +m(S = [<<"net">>,<<"al">>]) -> e(S); +m(S = [<<"org">>,<<"al">>]) -> e(S); +m(S = [<<"am">>]) -> e(S); +m(S = [<<"co">>,<<"am">>]) -> e(S); +m(S = [<<"com">>,<<"am">>]) -> e(S); +m(S = [<<"commune">>,<<"am">>]) -> e(S); +m(S = [<<"net">>,<<"am">>]) -> e(S); +m(S = [<<"org">>,<<"am">>]) -> e(S); +m(S = [<<"ao">>]) -> e(S); +m(S = [<<"ed">>,<<"ao">>]) -> e(S); +m(S = [<<"gv">>,<<"ao">>]) -> e(S); +m(S = [<<"og">>,<<"ao">>]) -> e(S); +m(S = [<<"co">>,<<"ao">>]) -> e(S); +m(S = [<<"pb">>,<<"ao">>]) -> e(S); +m(S = [<<"it">>,<<"ao">>]) -> e(S); +m(S = [<<"aq">>]) -> e(S); +m(S = [<<"ar">>]) -> e(S); +m(S = [<<"bet">>,<<"ar">>]) -> e(S); +m(S = [<<"com">>,<<"ar">>]) -> e(S); +m(S = [<<"coop">>,<<"ar">>]) -> e(S); +m(S = [<<"edu">>,<<"ar">>]) -> e(S); +m(S = [<<"gob">>,<<"ar">>]) -> e(S); +m(S = [<<"gov">>,<<"ar">>]) -> e(S); +m(S = [<<"int">>,<<"ar">>]) -> e(S); +m(S = [<<"mil">>,<<"ar">>]) -> e(S); +m(S = [<<"musica">>,<<"ar">>]) -> e(S); +m(S = [<<"mutual">>,<<"ar">>]) -> e(S); +m(S = [<<"net">>,<<"ar">>]) -> e(S); +m(S = [<<"org">>,<<"ar">>]) -> e(S); +m(S = [<<"senasa">>,<<"ar">>]) -> e(S); +m(S = [<<"tur">>,<<"ar">>]) -> e(S); +m(S = [<<"arpa">>]) -> e(S); +m(S = [<<"e164">>,<<"arpa">>]) -> e(S); +m(S = [<<"in-addr">>,<<"arpa">>]) -> e(S); +m(S = [<<"ip6">>,<<"arpa">>]) -> e(S); +m(S = [<<"iris">>,<<"arpa">>]) -> e(S); +m(S = [<<"uri">>,<<"arpa">>]) -> e(S); +m(S = [<<"urn">>,<<"arpa">>]) -> e(S); +m(S = [<<"as">>]) -> e(S); +m(S = [<<"gov">>,<<"as">>]) -> e(S); +m(S = [<<"asia">>]) -> e(S); +m(S = [<<"at">>]) -> e(S); +m(S = [<<"ac">>,<<"at">>]) -> e(S); +m(S = [<<"co">>,<<"at">>]) -> e(S); +m(S = [<<"gv">>,<<"at">>]) -> e(S); +m(S = [<<"or">>,<<"at">>]) -> e(S); +m(S = [<<"sth">>,<<"ac">>,<<"at">>]) -> e(S); +m(S = [<<"au">>]) -> e(S); +m(S = [<<"com">>,<<"au">>]) -> e(S); +m(S = [<<"net">>,<<"au">>]) -> e(S); +m(S = [<<"org">>,<<"au">>]) -> e(S); +m(S = [<<"edu">>,<<"au">>]) -> e(S); +m(S = [<<"gov">>,<<"au">>]) -> e(S); +m(S = [<<"asn">>,<<"au">>]) -> e(S); +m(S = [<<"id">>,<<"au">>]) -> e(S); +m(S = [<<"info">>,<<"au">>]) -> e(S); +m(S = [<<"conf">>,<<"au">>]) -> e(S); +m(S = [<<"oz">>,<<"au">>]) -> e(S); +m(S = [<<"act">>,<<"au">>]) -> e(S); +m(S = [<<"nsw">>,<<"au">>]) -> e(S); +m(S = [<<"nt">>,<<"au">>]) -> e(S); +m(S = [<<"qld">>,<<"au">>]) -> e(S); +m(S = [<<"sa">>,<<"au">>]) -> e(S); +m(S = [<<"tas">>,<<"au">>]) -> e(S); +m(S = [<<"vic">>,<<"au">>]) -> e(S); +m(S = [<<"wa">>,<<"au">>]) -> e(S); +m(S = [<<"act">>,<<"edu">>,<<"au">>]) -> e(S); +m(S = [<<"catholic">>,<<"edu">>,<<"au">>]) -> e(S); +m(S = [<<"nsw">>,<<"edu">>,<<"au">>]) -> e(S); +m(S = [<<"nt">>,<<"edu">>,<<"au">>]) -> e(S); +m(S = [<<"qld">>,<<"edu">>,<<"au">>]) -> e(S); +m(S = [<<"sa">>,<<"edu">>,<<"au">>]) -> e(S); +m(S = [<<"tas">>,<<"edu">>,<<"au">>]) -> e(S); +m(S = [<<"vic">>,<<"edu">>,<<"au">>]) -> e(S); +m(S = [<<"wa">>,<<"edu">>,<<"au">>]) -> e(S); +m(S = [<<"qld">>,<<"gov">>,<<"au">>]) -> e(S); +m(S = [<<"sa">>,<<"gov">>,<<"au">>]) -> e(S); +m(S = [<<"tas">>,<<"gov">>,<<"au">>]) -> e(S); +m(S = [<<"vic">>,<<"gov">>,<<"au">>]) -> e(S); +m(S = [<<"wa">>,<<"gov">>,<<"au">>]) -> e(S); +m(S = [<<"schools">>,<<"nsw">>,<<"edu">>,<<"au">>]) -> e(S); +m(S = [<<"aw">>]) -> e(S); +m(S = [<<"com">>,<<"aw">>]) -> e(S); +m(S = [<<"ax">>]) -> e(S); +m(S = [<<"az">>]) -> e(S); +m(S = [<<"com">>,<<"az">>]) -> e(S); +m(S = [<<"net">>,<<"az">>]) -> e(S); +m(S = [<<"int">>,<<"az">>]) -> e(S); +m(S = [<<"gov">>,<<"az">>]) -> e(S); +m(S = [<<"org">>,<<"az">>]) -> e(S); +m(S = [<<"edu">>,<<"az">>]) -> e(S); +m(S = [<<"info">>,<<"az">>]) -> e(S); +m(S = [<<"pp">>,<<"az">>]) -> e(S); +m(S = [<<"mil">>,<<"az">>]) -> e(S); +m(S = [<<"name">>,<<"az">>]) -> e(S); +m(S = [<<"pro">>,<<"az">>]) -> e(S); +m(S = [<<"biz">>,<<"az">>]) -> e(S); +m(S = [<<"ba">>]) -> e(S); +m(S = [<<"com">>,<<"ba">>]) -> e(S); +m(S = [<<"edu">>,<<"ba">>]) -> e(S); +m(S = [<<"gov">>,<<"ba">>]) -> e(S); +m(S = [<<"mil">>,<<"ba">>]) -> e(S); +m(S = [<<"net">>,<<"ba">>]) -> e(S); +m(S = [<<"org">>,<<"ba">>]) -> e(S); +m(S = [<<"bb">>]) -> e(S); +m(S = [<<"biz">>,<<"bb">>]) -> e(S); +m(S = [<<"co">>,<<"bb">>]) -> e(S); +m(S = [<<"com">>,<<"bb">>]) -> e(S); +m(S = [<<"edu">>,<<"bb">>]) -> e(S); +m(S = [<<"gov">>,<<"bb">>]) -> e(S); +m(S = [<<"info">>,<<"bb">>]) -> e(S); +m(S = [<<"net">>,<<"bb">>]) -> e(S); +m(S = [<<"org">>,<<"bb">>]) -> e(S); +m(S = [<<"store">>,<<"bb">>]) -> e(S); +m(S = [<<"tv">>,<<"bb">>]) -> e(S); +m(S = [_,<<"bd">>]) -> e(S); +m(S = [<<"be">>]) -> e(S); +m(S = [<<"ac">>,<<"be">>]) -> e(S); +m(S = [<<"bf">>]) -> e(S); +m(S = [<<"gov">>,<<"bf">>]) -> e(S); +m(S = [<<"bg">>]) -> e(S); +m(S = [<<"a">>,<<"bg">>]) -> e(S); +m(S = [<<"b">>,<<"bg">>]) -> e(S); +m(S = [<<"c">>,<<"bg">>]) -> e(S); +m(S = [<<"d">>,<<"bg">>]) -> e(S); +m(S = [<<"e">>,<<"bg">>]) -> e(S); +m(S = [<<"f">>,<<"bg">>]) -> e(S); +m(S = [<<"g">>,<<"bg">>]) -> e(S); +m(S = [<<"h">>,<<"bg">>]) -> e(S); +m(S = [<<"i">>,<<"bg">>]) -> e(S); +m(S = [<<"j">>,<<"bg">>]) -> e(S); +m(S = [<<"k">>,<<"bg">>]) -> e(S); +m(S = [<<"l">>,<<"bg">>]) -> e(S); +m(S = [<<"m">>,<<"bg">>]) -> e(S); +m(S = [<<"n">>,<<"bg">>]) -> e(S); +m(S = [<<"o">>,<<"bg">>]) -> e(S); +m(S = [<<"p">>,<<"bg">>]) -> e(S); +m(S = [<<"q">>,<<"bg">>]) -> e(S); +m(S = [<<"r">>,<<"bg">>]) -> e(S); +m(S = [<<"s">>,<<"bg">>]) -> e(S); +m(S = [<<"t">>,<<"bg">>]) -> e(S); +m(S = [<<"u">>,<<"bg">>]) -> e(S); +m(S = [<<"v">>,<<"bg">>]) -> e(S); +m(S = [<<"w">>,<<"bg">>]) -> e(S); +m(S = [<<"x">>,<<"bg">>]) -> e(S); +m(S = [<<"y">>,<<"bg">>]) -> e(S); +m(S = [<<"z">>,<<"bg">>]) -> e(S); +m(S = [<<"0">>,<<"bg">>]) -> e(S); +m(S = [<<"1">>,<<"bg">>]) -> e(S); +m(S = [<<"2">>,<<"bg">>]) -> e(S); +m(S = [<<"3">>,<<"bg">>]) -> e(S); +m(S = [<<"4">>,<<"bg">>]) -> e(S); +m(S = [<<"5">>,<<"bg">>]) -> e(S); +m(S = [<<"6">>,<<"bg">>]) -> e(S); +m(S = [<<"7">>,<<"bg">>]) -> e(S); +m(S = [<<"8">>,<<"bg">>]) -> e(S); +m(S = [<<"9">>,<<"bg">>]) -> e(S); +m(S = [<<"bh">>]) -> e(S); +m(S = [<<"com">>,<<"bh">>]) -> e(S); +m(S = [<<"edu">>,<<"bh">>]) -> e(S); +m(S = [<<"net">>,<<"bh">>]) -> e(S); +m(S = [<<"org">>,<<"bh">>]) -> e(S); +m(S = [<<"gov">>,<<"bh">>]) -> e(S); +m(S = [<<"bi">>]) -> e(S); +m(S = [<<"co">>,<<"bi">>]) -> e(S); +m(S = [<<"com">>,<<"bi">>]) -> e(S); +m(S = [<<"edu">>,<<"bi">>]) -> e(S); +m(S = [<<"or">>,<<"bi">>]) -> e(S); +m(S = [<<"org">>,<<"bi">>]) -> e(S); +m(S = [<<"biz">>]) -> e(S); +m(S = [<<"bj">>]) -> e(S); +m(S = [<<"asso">>,<<"bj">>]) -> e(S); +m(S = [<<"barreau">>,<<"bj">>]) -> e(S); +m(S = [<<"gouv">>,<<"bj">>]) -> e(S); +m(S = [<<"bm">>]) -> e(S); +m(S = [<<"com">>,<<"bm">>]) -> e(S); +m(S = [<<"edu">>,<<"bm">>]) -> e(S); +m(S = [<<"gov">>,<<"bm">>]) -> e(S); +m(S = [<<"net">>,<<"bm">>]) -> e(S); +m(S = [<<"org">>,<<"bm">>]) -> e(S); +m(S = [<<"bn">>]) -> e(S); +m(S = [<<"com">>,<<"bn">>]) -> e(S); +m(S = [<<"edu">>,<<"bn">>]) -> e(S); +m(S = [<<"gov">>,<<"bn">>]) -> e(S); +m(S = [<<"net">>,<<"bn">>]) -> e(S); +m(S = [<<"org">>,<<"bn">>]) -> e(S); +m(S = [<<"bo">>]) -> e(S); +m(S = [<<"com">>,<<"bo">>]) -> e(S); +m(S = [<<"edu">>,<<"bo">>]) -> e(S); +m(S = [<<"gob">>,<<"bo">>]) -> e(S); +m(S = [<<"int">>,<<"bo">>]) -> e(S); +m(S = [<<"org">>,<<"bo">>]) -> e(S); +m(S = [<<"net">>,<<"bo">>]) -> e(S); +m(S = [<<"mil">>,<<"bo">>]) -> e(S); +m(S = [<<"tv">>,<<"bo">>]) -> e(S); +m(S = [<<"web">>,<<"bo">>]) -> e(S); +m(S = [<<"academia">>,<<"bo">>]) -> e(S); +m(S = [<<"agro">>,<<"bo">>]) -> e(S); +m(S = [<<"arte">>,<<"bo">>]) -> e(S); +m(S = [<<"blog">>,<<"bo">>]) -> e(S); +m(S = [<<"bolivia">>,<<"bo">>]) -> e(S); +m(S = [<<"ciencia">>,<<"bo">>]) -> e(S); +m(S = [<<"cooperativa">>,<<"bo">>]) -> e(S); +m(S = [<<"democracia">>,<<"bo">>]) -> e(S); +m(S = [<<"deporte">>,<<"bo">>]) -> e(S); +m(S = [<<"ecologia">>,<<"bo">>]) -> e(S); +m(S = [<<"economia">>,<<"bo">>]) -> e(S); +m(S = [<<"empresa">>,<<"bo">>]) -> e(S); +m(S = [<<"indigena">>,<<"bo">>]) -> e(S); +m(S = [<<"industria">>,<<"bo">>]) -> e(S); +m(S = [<<"info">>,<<"bo">>]) -> e(S); +m(S = [<<"medicina">>,<<"bo">>]) -> e(S); +m(S = [<<"movimiento">>,<<"bo">>]) -> e(S); +m(S = [<<"musica">>,<<"bo">>]) -> e(S); +m(S = [<<"natural">>,<<"bo">>]) -> e(S); +m(S = [<<"nombre">>,<<"bo">>]) -> e(S); +m(S = [<<"noticias">>,<<"bo">>]) -> e(S); +m(S = [<<"patria">>,<<"bo">>]) -> e(S); +m(S = [<<"politica">>,<<"bo">>]) -> e(S); +m(S = [<<"profesional">>,<<"bo">>]) -> e(S); +m(S = [<<"plurinacional">>,<<"bo">>]) -> e(S); +m(S = [<<"pueblo">>,<<"bo">>]) -> e(S); +m(S = [<<"revista">>,<<"bo">>]) -> e(S); +m(S = [<<"salud">>,<<"bo">>]) -> e(S); +m(S = [<<"tecnologia">>,<<"bo">>]) -> e(S); +m(S = [<<"tksat">>,<<"bo">>]) -> e(S); +m(S = [<<"transporte">>,<<"bo">>]) -> e(S); +m(S = [<<"wiki">>,<<"bo">>]) -> e(S); +m(S = [<<"br">>]) -> e(S); +m(S = [<<"9guacu">>,<<"br">>]) -> e(S); +m(S = [<<"abc">>,<<"br">>]) -> e(S); +m(S = [<<"adm">>,<<"br">>]) -> e(S); +m(S = [<<"adv">>,<<"br">>]) -> e(S); +m(S = [<<"agr">>,<<"br">>]) -> e(S); +m(S = [<<"aju">>,<<"br">>]) -> e(S); +m(S = [<<"am">>,<<"br">>]) -> e(S); +m(S = [<<"anani">>,<<"br">>]) -> e(S); +m(S = [<<"aparecida">>,<<"br">>]) -> e(S); +m(S = [<<"app">>,<<"br">>]) -> e(S); +m(S = [<<"arq">>,<<"br">>]) -> e(S); +m(S = [<<"art">>,<<"br">>]) -> e(S); +m(S = [<<"ato">>,<<"br">>]) -> e(S); +m(S = [<<"b">>,<<"br">>]) -> e(S); +m(S = [<<"barueri">>,<<"br">>]) -> e(S); +m(S = [<<"belem">>,<<"br">>]) -> e(S); +m(S = [<<"bhz">>,<<"br">>]) -> e(S); +m(S = [<<"bib">>,<<"br">>]) -> e(S); +m(S = [<<"bio">>,<<"br">>]) -> e(S); +m(S = [<<"blog">>,<<"br">>]) -> e(S); +m(S = [<<"bmd">>,<<"br">>]) -> e(S); +m(S = [<<"boavista">>,<<"br">>]) -> e(S); +m(S = [<<"bsb">>,<<"br">>]) -> e(S); +m(S = [<<"campinagrande">>,<<"br">>]) -> e(S); +m(S = [<<"campinas">>,<<"br">>]) -> e(S); +m(S = [<<"caxias">>,<<"br">>]) -> e(S); +m(S = [<<"cim">>,<<"br">>]) -> e(S); +m(S = [<<"cng">>,<<"br">>]) -> e(S); +m(S = [<<"cnt">>,<<"br">>]) -> e(S); +m(S = [<<"com">>,<<"br">>]) -> e(S); +m(S = [<<"contagem">>,<<"br">>]) -> e(S); +m(S = [<<"coop">>,<<"br">>]) -> e(S); +m(S = [<<"coz">>,<<"br">>]) -> e(S); +m(S = [<<"cri">>,<<"br">>]) -> e(S); +m(S = [<<"cuiaba">>,<<"br">>]) -> e(S); +m(S = [<<"curitiba">>,<<"br">>]) -> e(S); +m(S = [<<"def">>,<<"br">>]) -> e(S); +m(S = [<<"des">>,<<"br">>]) -> e(S); +m(S = [<<"det">>,<<"br">>]) -> e(S); +m(S = [<<"dev">>,<<"br">>]) -> e(S); +m(S = [<<"ecn">>,<<"br">>]) -> e(S); +m(S = [<<"eco">>,<<"br">>]) -> e(S); +m(S = [<<"edu">>,<<"br">>]) -> e(S); +m(S = [<<"emp">>,<<"br">>]) -> e(S); +m(S = [<<"enf">>,<<"br">>]) -> e(S); +m(S = [<<"eng">>,<<"br">>]) -> e(S); +m(S = [<<"esp">>,<<"br">>]) -> e(S); +m(S = [<<"etc">>,<<"br">>]) -> e(S); +m(S = [<<"eti">>,<<"br">>]) -> e(S); +m(S = [<<"far">>,<<"br">>]) -> e(S); +m(S = [<<"feira">>,<<"br">>]) -> e(S); +m(S = [<<"flog">>,<<"br">>]) -> e(S); +m(S = [<<"floripa">>,<<"br">>]) -> e(S); +m(S = [<<"fm">>,<<"br">>]) -> e(S); +m(S = [<<"fnd">>,<<"br">>]) -> e(S); +m(S = [<<"fortal">>,<<"br">>]) -> e(S); +m(S = [<<"fot">>,<<"br">>]) -> e(S); +m(S = [<<"foz">>,<<"br">>]) -> e(S); +m(S = [<<"fst">>,<<"br">>]) -> e(S); +m(S = [<<"g12">>,<<"br">>]) -> e(S); +m(S = [<<"geo">>,<<"br">>]) -> e(S); +m(S = [<<"ggf">>,<<"br">>]) -> e(S); +m(S = [<<"goiania">>,<<"br">>]) -> e(S); +m(S = [<<"gov">>,<<"br">>]) -> e(S); +m(S = [<<"ac">>,<<"gov">>,<<"br">>]) -> e(S); +m(S = [<<"al">>,<<"gov">>,<<"br">>]) -> e(S); +m(S = [<<"am">>,<<"gov">>,<<"br">>]) -> e(S); +m(S = [<<"ap">>,<<"gov">>,<<"br">>]) -> e(S); +m(S = [<<"ba">>,<<"gov">>,<<"br">>]) -> e(S); +m(S = [<<"ce">>,<<"gov">>,<<"br">>]) -> e(S); +m(S = [<<"df">>,<<"gov">>,<<"br">>]) -> e(S); +m(S = [<<"es">>,<<"gov">>,<<"br">>]) -> e(S); +m(S = [<<"go">>,<<"gov">>,<<"br">>]) -> e(S); +m(S = [<<"ma">>,<<"gov">>,<<"br">>]) -> e(S); +m(S = [<<"mg">>,<<"gov">>,<<"br">>]) -> e(S); +m(S = [<<"ms">>,<<"gov">>,<<"br">>]) -> e(S); +m(S = [<<"mt">>,<<"gov">>,<<"br">>]) -> e(S); +m(S = [<<"pa">>,<<"gov">>,<<"br">>]) -> e(S); +m(S = [<<"pb">>,<<"gov">>,<<"br">>]) -> e(S); +m(S = [<<"pe">>,<<"gov">>,<<"br">>]) -> e(S); +m(S = [<<"pi">>,<<"gov">>,<<"br">>]) -> e(S); +m(S = [<<"pr">>,<<"gov">>,<<"br">>]) -> e(S); +m(S = [<<"rj">>,<<"gov">>,<<"br">>]) -> e(S); +m(S = [<<"rn">>,<<"gov">>,<<"br">>]) -> e(S); +m(S = [<<"ro">>,<<"gov">>,<<"br">>]) -> e(S); +m(S = [<<"rr">>,<<"gov">>,<<"br">>]) -> e(S); +m(S = [<<"rs">>,<<"gov">>,<<"br">>]) -> e(S); +m(S = [<<"sc">>,<<"gov">>,<<"br">>]) -> e(S); +m(S = [<<"se">>,<<"gov">>,<<"br">>]) -> e(S); +m(S = [<<"sp">>,<<"gov">>,<<"br">>]) -> e(S); +m(S = [<<"to">>,<<"gov">>,<<"br">>]) -> e(S); +m(S = [<<"gru">>,<<"br">>]) -> e(S); +m(S = [<<"imb">>,<<"br">>]) -> e(S); +m(S = [<<"ind">>,<<"br">>]) -> e(S); +m(S = [<<"inf">>,<<"br">>]) -> e(S); +m(S = [<<"jab">>,<<"br">>]) -> e(S); +m(S = [<<"jampa">>,<<"br">>]) -> e(S); +m(S = [<<"jdf">>,<<"br">>]) -> e(S); +m(S = [<<"joinville">>,<<"br">>]) -> e(S); +m(S = [<<"jor">>,<<"br">>]) -> e(S); +m(S = [<<"jus">>,<<"br">>]) -> e(S); +m(S = [<<"leg">>,<<"br">>]) -> e(S); +m(S = [<<"lel">>,<<"br">>]) -> e(S); +m(S = [<<"log">>,<<"br">>]) -> e(S); +m(S = [<<"londrina">>,<<"br">>]) -> e(S); +m(S = [<<"macapa">>,<<"br">>]) -> e(S); +m(S = [<<"maceio">>,<<"br">>]) -> e(S); +m(S = [<<"manaus">>,<<"br">>]) -> e(S); +m(S = [<<"maringa">>,<<"br">>]) -> e(S); +m(S = [<<"mat">>,<<"br">>]) -> e(S); +m(S = [<<"med">>,<<"br">>]) -> e(S); +m(S = [<<"mil">>,<<"br">>]) -> e(S); +m(S = [<<"morena">>,<<"br">>]) -> e(S); +m(S = [<<"mp">>,<<"br">>]) -> e(S); +m(S = [<<"mus">>,<<"br">>]) -> e(S); +m(S = [<<"natal">>,<<"br">>]) -> e(S); +m(S = [<<"net">>,<<"br">>]) -> e(S); +m(S = [<<"niteroi">>,<<"br">>]) -> e(S); +m(S = [_,<<"nom">>,<<"br">>]) -> e(S); +m(S = [<<"not">>,<<"br">>]) -> e(S); +m(S = [<<"ntr">>,<<"br">>]) -> e(S); +m(S = [<<"odo">>,<<"br">>]) -> e(S); +m(S = [<<"ong">>,<<"br">>]) -> e(S); +m(S = [<<"org">>,<<"br">>]) -> e(S); +m(S = [<<"osasco">>,<<"br">>]) -> e(S); +m(S = [<<"palmas">>,<<"br">>]) -> e(S); +m(S = [<<"poa">>,<<"br">>]) -> e(S); +m(S = [<<"ppg">>,<<"br">>]) -> e(S); +m(S = [<<"pro">>,<<"br">>]) -> e(S); +m(S = [<<"psc">>,<<"br">>]) -> e(S); +m(S = [<<"psi">>,<<"br">>]) -> e(S); +m(S = [<<"pvh">>,<<"br">>]) -> e(S); +m(S = [<<"qsl">>,<<"br">>]) -> e(S); +m(S = [<<"radio">>,<<"br">>]) -> e(S); +m(S = [<<"rec">>,<<"br">>]) -> e(S); +m(S = [<<"recife">>,<<"br">>]) -> e(S); +m(S = [<<"rep">>,<<"br">>]) -> e(S); +m(S = [<<"ribeirao">>,<<"br">>]) -> e(S); +m(S = [<<"rio">>,<<"br">>]) -> e(S); +m(S = [<<"riobranco">>,<<"br">>]) -> e(S); +m(S = [<<"riopreto">>,<<"br">>]) -> e(S); +m(S = [<<"salvador">>,<<"br">>]) -> e(S); +m(S = [<<"sampa">>,<<"br">>]) -> e(S); +m(S = [<<"santamaria">>,<<"br">>]) -> e(S); +m(S = [<<"santoandre">>,<<"br">>]) -> e(S); +m(S = [<<"saobernardo">>,<<"br">>]) -> e(S); +m(S = [<<"saogonca">>,<<"br">>]) -> e(S); +m(S = [<<"seg">>,<<"br">>]) -> e(S); +m(S = [<<"sjc">>,<<"br">>]) -> e(S); +m(S = [<<"slg">>,<<"br">>]) -> e(S); +m(S = [<<"slz">>,<<"br">>]) -> e(S); +m(S = [<<"sorocaba">>,<<"br">>]) -> e(S); +m(S = [<<"srv">>,<<"br">>]) -> e(S); +m(S = [<<"taxi">>,<<"br">>]) -> e(S); +m(S = [<<"tc">>,<<"br">>]) -> e(S); +m(S = [<<"tec">>,<<"br">>]) -> e(S); +m(S = [<<"teo">>,<<"br">>]) -> e(S); +m(S = [<<"the">>,<<"br">>]) -> e(S); +m(S = [<<"tmp">>,<<"br">>]) -> e(S); +m(S = [<<"trd">>,<<"br">>]) -> e(S); +m(S = [<<"tur">>,<<"br">>]) -> e(S); +m(S = [<<"tv">>,<<"br">>]) -> e(S); +m(S = [<<"udi">>,<<"br">>]) -> e(S); +m(S = [<<"vet">>,<<"br">>]) -> e(S); +m(S = [<<"vix">>,<<"br">>]) -> e(S); +m(S = [<<"vlog">>,<<"br">>]) -> e(S); +m(S = [<<"wiki">>,<<"br">>]) -> e(S); +m(S = [<<"zlg">>,<<"br">>]) -> e(S); +m(S = [<<"bs">>]) -> e(S); +m(S = [<<"com">>,<<"bs">>]) -> e(S); +m(S = [<<"net">>,<<"bs">>]) -> e(S); +m(S = [<<"org">>,<<"bs">>]) -> e(S); +m(S = [<<"edu">>,<<"bs">>]) -> e(S); +m(S = [<<"gov">>,<<"bs">>]) -> e(S); +m(S = [<<"bt">>]) -> e(S); +m(S = [<<"com">>,<<"bt">>]) -> e(S); +m(S = [<<"edu">>,<<"bt">>]) -> e(S); +m(S = [<<"gov">>,<<"bt">>]) -> e(S); +m(S = [<<"net">>,<<"bt">>]) -> e(S); +m(S = [<<"org">>,<<"bt">>]) -> e(S); +m(S = [<<"bv">>]) -> e(S); +m(S = [<<"bw">>]) -> e(S); +m(S = [<<"co">>,<<"bw">>]) -> e(S); +m(S = [<<"org">>,<<"bw">>]) -> e(S); +m(S = [<<"by">>]) -> e(S); +m(S = [<<"gov">>,<<"by">>]) -> e(S); +m(S = [<<"mil">>,<<"by">>]) -> e(S); +m(S = [<<"com">>,<<"by">>]) -> e(S); +m(S = [<<"of">>,<<"by">>]) -> e(S); +m(S = [<<"bz">>]) -> e(S); +m(S = [<<"com">>,<<"bz">>]) -> e(S); +m(S = [<<"net">>,<<"bz">>]) -> e(S); +m(S = [<<"org">>,<<"bz">>]) -> e(S); +m(S = [<<"edu">>,<<"bz">>]) -> e(S); +m(S = [<<"gov">>,<<"bz">>]) -> e(S); +m(S = [<<"ca">>]) -> e(S); +m(S = [<<"ab">>,<<"ca">>]) -> e(S); +m(S = [<<"bc">>,<<"ca">>]) -> e(S); +m(S = [<<"mb">>,<<"ca">>]) -> e(S); +m(S = [<<"nb">>,<<"ca">>]) -> e(S); +m(S = [<<"nf">>,<<"ca">>]) -> e(S); +m(S = [<<"nl">>,<<"ca">>]) -> e(S); +m(S = [<<"ns">>,<<"ca">>]) -> e(S); +m(S = [<<"nt">>,<<"ca">>]) -> e(S); +m(S = [<<"nu">>,<<"ca">>]) -> e(S); +m(S = [<<"on">>,<<"ca">>]) -> e(S); +m(S = [<<"pe">>,<<"ca">>]) -> e(S); +m(S = [<<"qc">>,<<"ca">>]) -> e(S); +m(S = [<<"sk">>,<<"ca">>]) -> e(S); +m(S = [<<"yk">>,<<"ca">>]) -> e(S); +m(S = [<<"gc">>,<<"ca">>]) -> e(S); +m(S = [<<"cat">>]) -> e(S); +m(S = [<<"cc">>]) -> e(S); +m(S = [<<"cd">>]) -> e(S); +m(S = [<<"gov">>,<<"cd">>]) -> e(S); +m(S = [<<"cf">>]) -> e(S); +m(S = [<<"cg">>]) -> e(S); +m(S = [<<"ch">>]) -> e(S); +m(S = [<<"ci">>]) -> e(S); +m(S = [<<"org">>,<<"ci">>]) -> e(S); +m(S = [<<"or">>,<<"ci">>]) -> e(S); +m(S = [<<"com">>,<<"ci">>]) -> e(S); +m(S = [<<"co">>,<<"ci">>]) -> e(S); +m(S = [<<"edu">>,<<"ci">>]) -> e(S); +m(S = [<<"ed">>,<<"ci">>]) -> e(S); +m(S = [<<"ac">>,<<"ci">>]) -> e(S); +m(S = [<<"net">>,<<"ci">>]) -> e(S); +m(S = [<<"go">>,<<"ci">>]) -> e(S); +m(S = [<<"asso">>,<<"ci">>]) -> e(S); +m(S = [<<"xn--aroport-bya">>,<<"ci">>]) -> e(S); +m(S = [<<"int">>,<<"ci">>]) -> e(S); +m(S = [<<"presse">>,<<"ci">>]) -> e(S); +m(S = [<<"md">>,<<"ci">>]) -> e(S); +m(S = [<<"gouv">>,<<"ci">>]) -> e(S); +m(S = [_,<<"ck">>]) -> e(S); +m(S = [<<"cl">>]) -> e(S); +m(S = [<<"co">>,<<"cl">>]) -> e(S); +m(S = [<<"gob">>,<<"cl">>]) -> e(S); +m(S = [<<"gov">>,<<"cl">>]) -> e(S); +m(S = [<<"mil">>,<<"cl">>]) -> e(S); +m(S = [<<"cm">>]) -> e(S); +m(S = [<<"co">>,<<"cm">>]) -> e(S); +m(S = [<<"com">>,<<"cm">>]) -> e(S); +m(S = [<<"gov">>,<<"cm">>]) -> e(S); +m(S = [<<"net">>,<<"cm">>]) -> e(S); +m(S = [<<"cn">>]) -> e(S); +m(S = [<<"ac">>,<<"cn">>]) -> e(S); +m(S = [<<"com">>,<<"cn">>]) -> e(S); +m(S = [<<"edu">>,<<"cn">>]) -> e(S); +m(S = [<<"gov">>,<<"cn">>]) -> e(S); +m(S = [<<"net">>,<<"cn">>]) -> e(S); +m(S = [<<"org">>,<<"cn">>]) -> e(S); +m(S = [<<"mil">>,<<"cn">>]) -> e(S); +m(S = [<<"xn--55qx5d">>,<<"cn">>]) -> e(S); +m(S = [<<"xn--io0a7i">>,<<"cn">>]) -> e(S); +m(S = [<<"xn--od0alg">>,<<"cn">>]) -> e(S); +m(S = [<<"ah">>,<<"cn">>]) -> e(S); +m(S = [<<"bj">>,<<"cn">>]) -> e(S); +m(S = [<<"cq">>,<<"cn">>]) -> e(S); +m(S = [<<"fj">>,<<"cn">>]) -> e(S); +m(S = [<<"gd">>,<<"cn">>]) -> e(S); +m(S = [<<"gs">>,<<"cn">>]) -> e(S); +m(S = [<<"gz">>,<<"cn">>]) -> e(S); +m(S = [<<"gx">>,<<"cn">>]) -> e(S); +m(S = [<<"ha">>,<<"cn">>]) -> e(S); +m(S = [<<"hb">>,<<"cn">>]) -> e(S); +m(S = [<<"he">>,<<"cn">>]) -> e(S); +m(S = [<<"hi">>,<<"cn">>]) -> e(S); +m(S = [<<"hl">>,<<"cn">>]) -> e(S); +m(S = [<<"hn">>,<<"cn">>]) -> e(S); +m(S = [<<"jl">>,<<"cn">>]) -> e(S); +m(S = [<<"js">>,<<"cn">>]) -> e(S); +m(S = [<<"jx">>,<<"cn">>]) -> e(S); +m(S = [<<"ln">>,<<"cn">>]) -> e(S); +m(S = [<<"nm">>,<<"cn">>]) -> e(S); +m(S = [<<"nx">>,<<"cn">>]) -> e(S); +m(S = [<<"qh">>,<<"cn">>]) -> e(S); +m(S = [<<"sc">>,<<"cn">>]) -> e(S); +m(S = [<<"sd">>,<<"cn">>]) -> e(S); +m(S = [<<"sh">>,<<"cn">>]) -> e(S); +m(S = [<<"sn">>,<<"cn">>]) -> e(S); +m(S = [<<"sx">>,<<"cn">>]) -> e(S); +m(S = [<<"tj">>,<<"cn">>]) -> e(S); +m(S = [<<"xj">>,<<"cn">>]) -> e(S); +m(S = [<<"xz">>,<<"cn">>]) -> e(S); +m(S = [<<"yn">>,<<"cn">>]) -> e(S); +m(S = [<<"zj">>,<<"cn">>]) -> e(S); +m(S = [<<"hk">>,<<"cn">>]) -> e(S); +m(S = [<<"mo">>,<<"cn">>]) -> e(S); +m(S = [<<"tw">>,<<"cn">>]) -> e(S); +m(S = [<<"co">>]) -> e(S); +m(S = [<<"arts">>,<<"co">>]) -> e(S); +m(S = [<<"com">>,<<"co">>]) -> e(S); +m(S = [<<"edu">>,<<"co">>]) -> e(S); +m(S = [<<"firm">>,<<"co">>]) -> e(S); +m(S = [<<"gov">>,<<"co">>]) -> e(S); +m(S = [<<"info">>,<<"co">>]) -> e(S); +m(S = [<<"int">>,<<"co">>]) -> e(S); +m(S = [<<"mil">>,<<"co">>]) -> e(S); +m(S = [<<"net">>,<<"co">>]) -> e(S); +m(S = [<<"nom">>,<<"co">>]) -> e(S); +m(S = [<<"org">>,<<"co">>]) -> e(S); +m(S = [<<"rec">>,<<"co">>]) -> e(S); +m(S = [<<"web">>,<<"co">>]) -> e(S); +m(S = [<<"com">>]) -> e(S); +m(S = [<<"coop">>]) -> e(S); +m(S = [<<"cr">>]) -> e(S); +m(S = [<<"ac">>,<<"cr">>]) -> e(S); +m(S = [<<"co">>,<<"cr">>]) -> e(S); +m(S = [<<"ed">>,<<"cr">>]) -> e(S); +m(S = [<<"fi">>,<<"cr">>]) -> e(S); +m(S = [<<"go">>,<<"cr">>]) -> e(S); +m(S = [<<"or">>,<<"cr">>]) -> e(S); +m(S = [<<"sa">>,<<"cr">>]) -> e(S); +m(S = [<<"cu">>]) -> e(S); +m(S = [<<"com">>,<<"cu">>]) -> e(S); +m(S = [<<"edu">>,<<"cu">>]) -> e(S); +m(S = [<<"org">>,<<"cu">>]) -> e(S); +m(S = [<<"net">>,<<"cu">>]) -> e(S); +m(S = [<<"gov">>,<<"cu">>]) -> e(S); +m(S = [<<"inf">>,<<"cu">>]) -> e(S); +m(S = [<<"cv">>]) -> e(S); +m(S = [<<"com">>,<<"cv">>]) -> e(S); +m(S = [<<"edu">>,<<"cv">>]) -> e(S); +m(S = [<<"int">>,<<"cv">>]) -> e(S); +m(S = [<<"nome">>,<<"cv">>]) -> e(S); +m(S = [<<"org">>,<<"cv">>]) -> e(S); +m(S = [<<"cw">>]) -> e(S); +m(S = [<<"com">>,<<"cw">>]) -> e(S); +m(S = [<<"edu">>,<<"cw">>]) -> e(S); +m(S = [<<"net">>,<<"cw">>]) -> e(S); +m(S = [<<"org">>,<<"cw">>]) -> e(S); +m(S = [<<"cx">>]) -> e(S); +m(S = [<<"gov">>,<<"cx">>]) -> e(S); +m(S = [<<"cy">>]) -> e(S); +m(S = [<<"ac">>,<<"cy">>]) -> e(S); +m(S = [<<"biz">>,<<"cy">>]) -> e(S); +m(S = [<<"com">>,<<"cy">>]) -> e(S); +m(S = [<<"ekloges">>,<<"cy">>]) -> e(S); +m(S = [<<"gov">>,<<"cy">>]) -> e(S); +m(S = [<<"ltd">>,<<"cy">>]) -> e(S); +m(S = [<<"mil">>,<<"cy">>]) -> e(S); +m(S = [<<"net">>,<<"cy">>]) -> e(S); +m(S = [<<"org">>,<<"cy">>]) -> e(S); +m(S = [<<"press">>,<<"cy">>]) -> e(S); +m(S = [<<"pro">>,<<"cy">>]) -> e(S); +m(S = [<<"tm">>,<<"cy">>]) -> e(S); +m(S = [<<"cz">>]) -> e(S); +m(S = [<<"de">>]) -> e(S); +m(S = [<<"dj">>]) -> e(S); +m(S = [<<"dk">>]) -> e(S); +m(S = [<<"dm">>]) -> e(S); +m(S = [<<"com">>,<<"dm">>]) -> e(S); +m(S = [<<"net">>,<<"dm">>]) -> e(S); +m(S = [<<"org">>,<<"dm">>]) -> e(S); +m(S = [<<"edu">>,<<"dm">>]) -> e(S); +m(S = [<<"gov">>,<<"dm">>]) -> e(S); +m(S = [<<"do">>]) -> e(S); +m(S = [<<"art">>,<<"do">>]) -> e(S); +m(S = [<<"com">>,<<"do">>]) -> e(S); +m(S = [<<"edu">>,<<"do">>]) -> e(S); +m(S = [<<"gob">>,<<"do">>]) -> e(S); +m(S = [<<"gov">>,<<"do">>]) -> e(S); +m(S = [<<"mil">>,<<"do">>]) -> e(S); +m(S = [<<"net">>,<<"do">>]) -> e(S); +m(S = [<<"org">>,<<"do">>]) -> e(S); +m(S = [<<"sld">>,<<"do">>]) -> e(S); +m(S = [<<"web">>,<<"do">>]) -> e(S); +m(S = [<<"dz">>]) -> e(S); +m(S = [<<"art">>,<<"dz">>]) -> e(S); +m(S = [<<"asso">>,<<"dz">>]) -> e(S); +m(S = [<<"com">>,<<"dz">>]) -> e(S); +m(S = [<<"edu">>,<<"dz">>]) -> e(S); +m(S = [<<"gov">>,<<"dz">>]) -> e(S); +m(S = [<<"org">>,<<"dz">>]) -> e(S); +m(S = [<<"net">>,<<"dz">>]) -> e(S); +m(S = [<<"pol">>,<<"dz">>]) -> e(S); +m(S = [<<"soc">>,<<"dz">>]) -> e(S); +m(S = [<<"tm">>,<<"dz">>]) -> e(S); +m(S = [<<"ec">>]) -> e(S); +m(S = [<<"com">>,<<"ec">>]) -> e(S); +m(S = [<<"info">>,<<"ec">>]) -> e(S); +m(S = [<<"net">>,<<"ec">>]) -> e(S); +m(S = [<<"fin">>,<<"ec">>]) -> e(S); +m(S = [<<"k12">>,<<"ec">>]) -> e(S); +m(S = [<<"med">>,<<"ec">>]) -> e(S); +m(S = [<<"pro">>,<<"ec">>]) -> e(S); +m(S = [<<"org">>,<<"ec">>]) -> e(S); +m(S = [<<"edu">>,<<"ec">>]) -> e(S); +m(S = [<<"gov">>,<<"ec">>]) -> e(S); +m(S = [<<"gob">>,<<"ec">>]) -> e(S); +m(S = [<<"mil">>,<<"ec">>]) -> e(S); +m(S = [<<"edu">>]) -> e(S); +m(S = [<<"ee">>]) -> e(S); +m(S = [<<"edu">>,<<"ee">>]) -> e(S); +m(S = [<<"gov">>,<<"ee">>]) -> e(S); +m(S = [<<"riik">>,<<"ee">>]) -> e(S); +m(S = [<<"lib">>,<<"ee">>]) -> e(S); +m(S = [<<"med">>,<<"ee">>]) -> e(S); +m(S = [<<"com">>,<<"ee">>]) -> e(S); +m(S = [<<"pri">>,<<"ee">>]) -> e(S); +m(S = [<<"aip">>,<<"ee">>]) -> e(S); +m(S = [<<"org">>,<<"ee">>]) -> e(S); +m(S = [<<"fie">>,<<"ee">>]) -> e(S); +m(S = [<<"eg">>]) -> e(S); +m(S = [<<"com">>,<<"eg">>]) -> e(S); +m(S = [<<"edu">>,<<"eg">>]) -> e(S); +m(S = [<<"eun">>,<<"eg">>]) -> e(S); +m(S = [<<"gov">>,<<"eg">>]) -> e(S); +m(S = [<<"mil">>,<<"eg">>]) -> e(S); +m(S = [<<"name">>,<<"eg">>]) -> e(S); +m(S = [<<"net">>,<<"eg">>]) -> e(S); +m(S = [<<"org">>,<<"eg">>]) -> e(S); +m(S = [<<"sci">>,<<"eg">>]) -> e(S); +m(S = [_,<<"er">>]) -> e(S); +m(S = [<<"es">>]) -> e(S); +m(S = [<<"com">>,<<"es">>]) -> e(S); +m(S = [<<"nom">>,<<"es">>]) -> e(S); +m(S = [<<"org">>,<<"es">>]) -> e(S); +m(S = [<<"gob">>,<<"es">>]) -> e(S); +m(S = [<<"edu">>,<<"es">>]) -> e(S); +m(S = [<<"et">>]) -> e(S); +m(S = [<<"com">>,<<"et">>]) -> e(S); +m(S = [<<"gov">>,<<"et">>]) -> e(S); +m(S = [<<"org">>,<<"et">>]) -> e(S); +m(S = [<<"edu">>,<<"et">>]) -> e(S); +m(S = [<<"biz">>,<<"et">>]) -> e(S); +m(S = [<<"name">>,<<"et">>]) -> e(S); +m(S = [<<"info">>,<<"et">>]) -> e(S); +m(S = [<<"net">>,<<"et">>]) -> e(S); +m(S = [<<"eu">>]) -> e(S); +m(S = [<<"fi">>]) -> e(S); +m(S = [<<"aland">>,<<"fi">>]) -> e(S); +m(S = [<<"fj">>]) -> e(S); +m(S = [<<"ac">>,<<"fj">>]) -> e(S); +m(S = [<<"biz">>,<<"fj">>]) -> e(S); +m(S = [<<"com">>,<<"fj">>]) -> e(S); +m(S = [<<"gov">>,<<"fj">>]) -> e(S); +m(S = [<<"info">>,<<"fj">>]) -> e(S); +m(S = [<<"mil">>,<<"fj">>]) -> e(S); +m(S = [<<"name">>,<<"fj">>]) -> e(S); +m(S = [<<"net">>,<<"fj">>]) -> e(S); +m(S = [<<"org">>,<<"fj">>]) -> e(S); +m(S = [<<"pro">>,<<"fj">>]) -> e(S); +m(S = [_,<<"fk">>]) -> e(S); +m(S = [<<"com">>,<<"fm">>]) -> e(S); +m(S = [<<"edu">>,<<"fm">>]) -> e(S); +m(S = [<<"net">>,<<"fm">>]) -> e(S); +m(S = [<<"org">>,<<"fm">>]) -> e(S); +m(S = [<<"fm">>]) -> e(S); +m(S = [<<"fo">>]) -> e(S); +m(S = [<<"fr">>]) -> e(S); +m(S = [<<"asso">>,<<"fr">>]) -> e(S); +m(S = [<<"com">>,<<"fr">>]) -> e(S); +m(S = [<<"gouv">>,<<"fr">>]) -> e(S); +m(S = [<<"nom">>,<<"fr">>]) -> e(S); +m(S = [<<"prd">>,<<"fr">>]) -> e(S); +m(S = [<<"tm">>,<<"fr">>]) -> e(S); +m(S = [<<"aeroport">>,<<"fr">>]) -> e(S); +m(S = [<<"avocat">>,<<"fr">>]) -> e(S); +m(S = [<<"avoues">>,<<"fr">>]) -> e(S); +m(S = [<<"cci">>,<<"fr">>]) -> e(S); +m(S = [<<"chambagri">>,<<"fr">>]) -> e(S); +m(S = [<<"chirurgiens-dentistes">>,<<"fr">>]) -> e(S); +m(S = [<<"experts-comptables">>,<<"fr">>]) -> e(S); +m(S = [<<"geometre-expert">>,<<"fr">>]) -> e(S); +m(S = [<<"greta">>,<<"fr">>]) -> e(S); +m(S = [<<"huissier-justice">>,<<"fr">>]) -> e(S); +m(S = [<<"medecin">>,<<"fr">>]) -> e(S); +m(S = [<<"notaires">>,<<"fr">>]) -> e(S); +m(S = [<<"pharmacien">>,<<"fr">>]) -> e(S); +m(S = [<<"port">>,<<"fr">>]) -> e(S); +m(S = [<<"veterinaire">>,<<"fr">>]) -> e(S); +m(S = [<<"ga">>]) -> e(S); +m(S = [<<"gb">>]) -> e(S); +m(S = [<<"edu">>,<<"gd">>]) -> e(S); +m(S = [<<"gov">>,<<"gd">>]) -> e(S); +m(S = [<<"gd">>]) -> e(S); +m(S = [<<"ge">>]) -> e(S); +m(S = [<<"com">>,<<"ge">>]) -> e(S); +m(S = [<<"edu">>,<<"ge">>]) -> e(S); +m(S = [<<"gov">>,<<"ge">>]) -> e(S); +m(S = [<<"org">>,<<"ge">>]) -> e(S); +m(S = [<<"mil">>,<<"ge">>]) -> e(S); +m(S = [<<"net">>,<<"ge">>]) -> e(S); +m(S = [<<"pvt">>,<<"ge">>]) -> e(S); +m(S = [<<"gf">>]) -> e(S); +m(S = [<<"gg">>]) -> e(S); +m(S = [<<"co">>,<<"gg">>]) -> e(S); +m(S = [<<"net">>,<<"gg">>]) -> e(S); +m(S = [<<"org">>,<<"gg">>]) -> e(S); +m(S = [<<"gh">>]) -> e(S); +m(S = [<<"com">>,<<"gh">>]) -> e(S); +m(S = [<<"edu">>,<<"gh">>]) -> e(S); +m(S = [<<"gov">>,<<"gh">>]) -> e(S); +m(S = [<<"org">>,<<"gh">>]) -> e(S); +m(S = [<<"mil">>,<<"gh">>]) -> e(S); +m(S = [<<"gi">>]) -> e(S); +m(S = [<<"com">>,<<"gi">>]) -> e(S); +m(S = [<<"ltd">>,<<"gi">>]) -> e(S); +m(S = [<<"gov">>,<<"gi">>]) -> e(S); +m(S = [<<"mod">>,<<"gi">>]) -> e(S); +m(S = [<<"edu">>,<<"gi">>]) -> e(S); +m(S = [<<"org">>,<<"gi">>]) -> e(S); +m(S = [<<"gl">>]) -> e(S); +m(S = [<<"co">>,<<"gl">>]) -> e(S); +m(S = [<<"com">>,<<"gl">>]) -> e(S); +m(S = [<<"edu">>,<<"gl">>]) -> e(S); +m(S = [<<"net">>,<<"gl">>]) -> e(S); +m(S = [<<"org">>,<<"gl">>]) -> e(S); +m(S = [<<"gm">>]) -> e(S); +m(S = [<<"gn">>]) -> e(S); +m(S = [<<"ac">>,<<"gn">>]) -> e(S); +m(S = [<<"com">>,<<"gn">>]) -> e(S); +m(S = [<<"edu">>,<<"gn">>]) -> e(S); +m(S = [<<"gov">>,<<"gn">>]) -> e(S); +m(S = [<<"org">>,<<"gn">>]) -> e(S); +m(S = [<<"net">>,<<"gn">>]) -> e(S); +m(S = [<<"gov">>]) -> e(S); +m(S = [<<"gp">>]) -> e(S); +m(S = [<<"com">>,<<"gp">>]) -> e(S); +m(S = [<<"net">>,<<"gp">>]) -> e(S); +m(S = [<<"mobi">>,<<"gp">>]) -> e(S); +m(S = [<<"edu">>,<<"gp">>]) -> e(S); +m(S = [<<"org">>,<<"gp">>]) -> e(S); +m(S = [<<"asso">>,<<"gp">>]) -> e(S); +m(S = [<<"gq">>]) -> e(S); +m(S = [<<"gr">>]) -> e(S); +m(S = [<<"com">>,<<"gr">>]) -> e(S); +m(S = [<<"edu">>,<<"gr">>]) -> e(S); +m(S = [<<"net">>,<<"gr">>]) -> e(S); +m(S = [<<"org">>,<<"gr">>]) -> e(S); +m(S = [<<"gov">>,<<"gr">>]) -> e(S); +m(S = [<<"gs">>]) -> e(S); +m(S = [<<"gt">>]) -> e(S); +m(S = [<<"com">>,<<"gt">>]) -> e(S); +m(S = [<<"edu">>,<<"gt">>]) -> e(S); +m(S = [<<"gob">>,<<"gt">>]) -> e(S); +m(S = [<<"ind">>,<<"gt">>]) -> e(S); +m(S = [<<"mil">>,<<"gt">>]) -> e(S); +m(S = [<<"net">>,<<"gt">>]) -> e(S); +m(S = [<<"org">>,<<"gt">>]) -> e(S); +m(S = [<<"gu">>]) -> e(S); +m(S = [<<"com">>,<<"gu">>]) -> e(S); +m(S = [<<"edu">>,<<"gu">>]) -> e(S); +m(S = [<<"gov">>,<<"gu">>]) -> e(S); +m(S = [<<"guam">>,<<"gu">>]) -> e(S); +m(S = [<<"info">>,<<"gu">>]) -> e(S); +m(S = [<<"net">>,<<"gu">>]) -> e(S); +m(S = [<<"org">>,<<"gu">>]) -> e(S); +m(S = [<<"web">>,<<"gu">>]) -> e(S); +m(S = [<<"gw">>]) -> e(S); +m(S = [<<"gy">>]) -> e(S); +m(S = [<<"co">>,<<"gy">>]) -> e(S); +m(S = [<<"com">>,<<"gy">>]) -> e(S); +m(S = [<<"edu">>,<<"gy">>]) -> e(S); +m(S = [<<"gov">>,<<"gy">>]) -> e(S); +m(S = [<<"net">>,<<"gy">>]) -> e(S); +m(S = [<<"org">>,<<"gy">>]) -> e(S); +m(S = [<<"hk">>]) -> e(S); +m(S = [<<"com">>,<<"hk">>]) -> e(S); +m(S = [<<"edu">>,<<"hk">>]) -> e(S); +m(S = [<<"gov">>,<<"hk">>]) -> e(S); +m(S = [<<"idv">>,<<"hk">>]) -> e(S); +m(S = [<<"net">>,<<"hk">>]) -> e(S); +m(S = [<<"org">>,<<"hk">>]) -> e(S); +m(S = [<<"xn--55qx5d">>,<<"hk">>]) -> e(S); +m(S = [<<"xn--wcvs22d">>,<<"hk">>]) -> e(S); +m(S = [<<"xn--lcvr32d">>,<<"hk">>]) -> e(S); +m(S = [<<"xn--mxtq1m">>,<<"hk">>]) -> e(S); +m(S = [<<"xn--gmqw5a">>,<<"hk">>]) -> e(S); +m(S = [<<"xn--ciqpn">>,<<"hk">>]) -> e(S); +m(S = [<<"xn--gmq050i">>,<<"hk">>]) -> e(S); +m(S = [<<"xn--zf0avx">>,<<"hk">>]) -> e(S); +m(S = [<<"xn--io0a7i">>,<<"hk">>]) -> e(S); +m(S = [<<"xn--mk0axi">>,<<"hk">>]) -> e(S); +m(S = [<<"xn--od0alg">>,<<"hk">>]) -> e(S); +m(S = [<<"xn--od0aq3b">>,<<"hk">>]) -> e(S); +m(S = [<<"xn--tn0ag">>,<<"hk">>]) -> e(S); +m(S = [<<"xn--uc0atv">>,<<"hk">>]) -> e(S); +m(S = [<<"xn--uc0ay4a">>,<<"hk">>]) -> e(S); +m(S = [<<"hm">>]) -> e(S); +m(S = [<<"hn">>]) -> e(S); +m(S = [<<"com">>,<<"hn">>]) -> e(S); +m(S = [<<"edu">>,<<"hn">>]) -> e(S); +m(S = [<<"org">>,<<"hn">>]) -> e(S); +m(S = [<<"net">>,<<"hn">>]) -> e(S); +m(S = [<<"mil">>,<<"hn">>]) -> e(S); +m(S = [<<"gob">>,<<"hn">>]) -> e(S); +m(S = [<<"hr">>]) -> e(S); +m(S = [<<"iz">>,<<"hr">>]) -> e(S); +m(S = [<<"from">>,<<"hr">>]) -> e(S); +m(S = [<<"name">>,<<"hr">>]) -> e(S); +m(S = [<<"com">>,<<"hr">>]) -> e(S); +m(S = [<<"ht">>]) -> e(S); +m(S = [<<"com">>,<<"ht">>]) -> e(S); +m(S = [<<"shop">>,<<"ht">>]) -> e(S); +m(S = [<<"firm">>,<<"ht">>]) -> e(S); +m(S = [<<"info">>,<<"ht">>]) -> e(S); +m(S = [<<"adult">>,<<"ht">>]) -> e(S); +m(S = [<<"net">>,<<"ht">>]) -> e(S); +m(S = [<<"pro">>,<<"ht">>]) -> e(S); +m(S = [<<"org">>,<<"ht">>]) -> e(S); +m(S = [<<"med">>,<<"ht">>]) -> e(S); +m(S = [<<"art">>,<<"ht">>]) -> e(S); +m(S = [<<"coop">>,<<"ht">>]) -> e(S); +m(S = [<<"pol">>,<<"ht">>]) -> e(S); +m(S = [<<"asso">>,<<"ht">>]) -> e(S); +m(S = [<<"edu">>,<<"ht">>]) -> e(S); +m(S = [<<"rel">>,<<"ht">>]) -> e(S); +m(S = [<<"gouv">>,<<"ht">>]) -> e(S); +m(S = [<<"perso">>,<<"ht">>]) -> e(S); +m(S = [<<"hu">>]) -> e(S); +m(S = [<<"co">>,<<"hu">>]) -> e(S); +m(S = [<<"info">>,<<"hu">>]) -> e(S); +m(S = [<<"org">>,<<"hu">>]) -> e(S); +m(S = [<<"priv">>,<<"hu">>]) -> e(S); +m(S = [<<"sport">>,<<"hu">>]) -> e(S); +m(S = [<<"tm">>,<<"hu">>]) -> e(S); +m(S = [<<"2000">>,<<"hu">>]) -> e(S); +m(S = [<<"agrar">>,<<"hu">>]) -> e(S); +m(S = [<<"bolt">>,<<"hu">>]) -> e(S); +m(S = [<<"casino">>,<<"hu">>]) -> e(S); +m(S = [<<"city">>,<<"hu">>]) -> e(S); +m(S = [<<"erotica">>,<<"hu">>]) -> e(S); +m(S = [<<"erotika">>,<<"hu">>]) -> e(S); +m(S = [<<"film">>,<<"hu">>]) -> e(S); +m(S = [<<"forum">>,<<"hu">>]) -> e(S); +m(S = [<<"games">>,<<"hu">>]) -> e(S); +m(S = [<<"hotel">>,<<"hu">>]) -> e(S); +m(S = [<<"ingatlan">>,<<"hu">>]) -> e(S); +m(S = [<<"jogasz">>,<<"hu">>]) -> e(S); +m(S = [<<"konyvelo">>,<<"hu">>]) -> e(S); +m(S = [<<"lakas">>,<<"hu">>]) -> e(S); +m(S = [<<"media">>,<<"hu">>]) -> e(S); +m(S = [<<"news">>,<<"hu">>]) -> e(S); +m(S = [<<"reklam">>,<<"hu">>]) -> e(S); +m(S = [<<"sex">>,<<"hu">>]) -> e(S); +m(S = [<<"shop">>,<<"hu">>]) -> e(S); +m(S = [<<"suli">>,<<"hu">>]) -> e(S); +m(S = [<<"szex">>,<<"hu">>]) -> e(S); +m(S = [<<"tozsde">>,<<"hu">>]) -> e(S); +m(S = [<<"utazas">>,<<"hu">>]) -> e(S); +m(S = [<<"video">>,<<"hu">>]) -> e(S); +m(S = [<<"id">>]) -> e(S); +m(S = [<<"ac">>,<<"id">>]) -> e(S); +m(S = [<<"biz">>,<<"id">>]) -> e(S); +m(S = [<<"co">>,<<"id">>]) -> e(S); +m(S = [<<"desa">>,<<"id">>]) -> e(S); +m(S = [<<"go">>,<<"id">>]) -> e(S); +m(S = [<<"mil">>,<<"id">>]) -> e(S); +m(S = [<<"my">>,<<"id">>]) -> e(S); +m(S = [<<"net">>,<<"id">>]) -> e(S); +m(S = [<<"or">>,<<"id">>]) -> e(S); +m(S = [<<"ponpes">>,<<"id">>]) -> e(S); +m(S = [<<"sch">>,<<"id">>]) -> e(S); +m(S = [<<"web">>,<<"id">>]) -> e(S); +m(S = [<<"ie">>]) -> e(S); +m(S = [<<"gov">>,<<"ie">>]) -> e(S); +m(S = [<<"il">>]) -> e(S); +m(S = [<<"ac">>,<<"il">>]) -> e(S); +m(S = [<<"co">>,<<"il">>]) -> e(S); +m(S = [<<"gov">>,<<"il">>]) -> e(S); +m(S = [<<"idf">>,<<"il">>]) -> e(S); +m(S = [<<"k12">>,<<"il">>]) -> e(S); +m(S = [<<"muni">>,<<"il">>]) -> e(S); +m(S = [<<"net">>,<<"il">>]) -> e(S); +m(S = [<<"org">>,<<"il">>]) -> e(S); +m(S = [<<"xn--4dbrk0ce">>]) -> e(S); +m(S = [<<"xn--4dbgdty6c">>,<<"xn--4dbrk0ce">>]) -> e(S); +m(S = [<<"xn--5dbhl8d">>,<<"xn--4dbrk0ce">>]) -> e(S); +m(S = [<<"xn--8dbq2a">>,<<"xn--4dbrk0ce">>]) -> e(S); +m(S = [<<"xn--hebda8b">>,<<"xn--4dbrk0ce">>]) -> e(S); +m(S = [<<"im">>]) -> e(S); +m(S = [<<"ac">>,<<"im">>]) -> e(S); +m(S = [<<"co">>,<<"im">>]) -> e(S); +m(S = [<<"com">>,<<"im">>]) -> e(S); +m(S = [<<"ltd">>,<<"co">>,<<"im">>]) -> e(S); +m(S = [<<"net">>,<<"im">>]) -> e(S); +m(S = [<<"org">>,<<"im">>]) -> e(S); +m(S = [<<"plc">>,<<"co">>,<<"im">>]) -> e(S); +m(S = [<<"tt">>,<<"im">>]) -> e(S); +m(S = [<<"tv">>,<<"im">>]) -> e(S); +m(S = [<<"in">>]) -> e(S); +m(S = [<<"5g">>,<<"in">>]) -> e(S); +m(S = [<<"6g">>,<<"in">>]) -> e(S); +m(S = [<<"ac">>,<<"in">>]) -> e(S); +m(S = [<<"ai">>,<<"in">>]) -> e(S); +m(S = [<<"am">>,<<"in">>]) -> e(S); +m(S = [<<"bihar">>,<<"in">>]) -> e(S); +m(S = [<<"biz">>,<<"in">>]) -> e(S); +m(S = [<<"business">>,<<"in">>]) -> e(S); +m(S = [<<"ca">>,<<"in">>]) -> e(S); +m(S = [<<"cn">>,<<"in">>]) -> e(S); +m(S = [<<"co">>,<<"in">>]) -> e(S); +m(S = [<<"com">>,<<"in">>]) -> e(S); +m(S = [<<"coop">>,<<"in">>]) -> e(S); +m(S = [<<"cs">>,<<"in">>]) -> e(S); +m(S = [<<"delhi">>,<<"in">>]) -> e(S); +m(S = [<<"dr">>,<<"in">>]) -> e(S); +m(S = [<<"edu">>,<<"in">>]) -> e(S); +m(S = [<<"er">>,<<"in">>]) -> e(S); +m(S = [<<"firm">>,<<"in">>]) -> e(S); +m(S = [<<"gen">>,<<"in">>]) -> e(S); +m(S = [<<"gov">>,<<"in">>]) -> e(S); +m(S = [<<"gujarat">>,<<"in">>]) -> e(S); +m(S = [<<"ind">>,<<"in">>]) -> e(S); +m(S = [<<"info">>,<<"in">>]) -> e(S); +m(S = [<<"int">>,<<"in">>]) -> e(S); +m(S = [<<"internet">>,<<"in">>]) -> e(S); +m(S = [<<"io">>,<<"in">>]) -> e(S); +m(S = [<<"me">>,<<"in">>]) -> e(S); +m(S = [<<"mil">>,<<"in">>]) -> e(S); +m(S = [<<"net">>,<<"in">>]) -> e(S); +m(S = [<<"nic">>,<<"in">>]) -> e(S); +m(S = [<<"org">>,<<"in">>]) -> e(S); +m(S = [<<"pg">>,<<"in">>]) -> e(S); +m(S = [<<"post">>,<<"in">>]) -> e(S); +m(S = [<<"pro">>,<<"in">>]) -> e(S); +m(S = [<<"res">>,<<"in">>]) -> e(S); +m(S = [<<"travel">>,<<"in">>]) -> e(S); +m(S = [<<"tv">>,<<"in">>]) -> e(S); +m(S = [<<"uk">>,<<"in">>]) -> e(S); +m(S = [<<"up">>,<<"in">>]) -> e(S); +m(S = [<<"us">>,<<"in">>]) -> e(S); +m(S = [<<"info">>]) -> e(S); +m(S = [<<"int">>]) -> e(S); +m(S = [<<"eu">>,<<"int">>]) -> e(S); +m(S = [<<"io">>]) -> e(S); +m(S = [<<"com">>,<<"io">>]) -> e(S); +m(S = [<<"iq">>]) -> e(S); +m(S = [<<"gov">>,<<"iq">>]) -> e(S); +m(S = [<<"edu">>,<<"iq">>]) -> e(S); +m(S = [<<"mil">>,<<"iq">>]) -> e(S); +m(S = [<<"com">>,<<"iq">>]) -> e(S); +m(S = [<<"org">>,<<"iq">>]) -> e(S); +m(S = [<<"net">>,<<"iq">>]) -> e(S); +m(S = [<<"ir">>]) -> e(S); +m(S = [<<"ac">>,<<"ir">>]) -> e(S); +m(S = [<<"co">>,<<"ir">>]) -> e(S); +m(S = [<<"gov">>,<<"ir">>]) -> e(S); +m(S = [<<"id">>,<<"ir">>]) -> e(S); +m(S = [<<"net">>,<<"ir">>]) -> e(S); +m(S = [<<"org">>,<<"ir">>]) -> e(S); +m(S = [<<"sch">>,<<"ir">>]) -> e(S); +m(S = [<<"xn--mgba3a4f16a">>,<<"ir">>]) -> e(S); +m(S = [<<"xn--mgba3a4fra">>,<<"ir">>]) -> e(S); +m(S = [<<"is">>]) -> e(S); +m(S = [<<"net">>,<<"is">>]) -> e(S); +m(S = [<<"com">>,<<"is">>]) -> e(S); +m(S = [<<"edu">>,<<"is">>]) -> e(S); +m(S = [<<"gov">>,<<"is">>]) -> e(S); +m(S = [<<"org">>,<<"is">>]) -> e(S); +m(S = [<<"int">>,<<"is">>]) -> e(S); +m(S = [<<"it">>]) -> e(S); +m(S = [<<"gov">>,<<"it">>]) -> e(S); +m(S = [<<"edu">>,<<"it">>]) -> e(S); +m(S = [<<"abr">>,<<"it">>]) -> e(S); +m(S = [<<"abruzzo">>,<<"it">>]) -> e(S); +m(S = [<<"aosta-valley">>,<<"it">>]) -> e(S); +m(S = [<<"aostavalley">>,<<"it">>]) -> e(S); +m(S = [<<"bas">>,<<"it">>]) -> e(S); +m(S = [<<"basilicata">>,<<"it">>]) -> e(S); +m(S = [<<"cal">>,<<"it">>]) -> e(S); +m(S = [<<"calabria">>,<<"it">>]) -> e(S); +m(S = [<<"cam">>,<<"it">>]) -> e(S); +m(S = [<<"campania">>,<<"it">>]) -> e(S); +m(S = [<<"emilia-romagna">>,<<"it">>]) -> e(S); +m(S = [<<"emiliaromagna">>,<<"it">>]) -> e(S); +m(S = [<<"emr">>,<<"it">>]) -> e(S); +m(S = [<<"friuli-v-giulia">>,<<"it">>]) -> e(S); +m(S = [<<"friuli-ve-giulia">>,<<"it">>]) -> e(S); +m(S = [<<"friuli-vegiulia">>,<<"it">>]) -> e(S); +m(S = [<<"friuli-venezia-giulia">>,<<"it">>]) -> e(S); +m(S = [<<"friuli-veneziagiulia">>,<<"it">>]) -> e(S); +m(S = [<<"friuli-vgiulia">>,<<"it">>]) -> e(S); +m(S = [<<"friuliv-giulia">>,<<"it">>]) -> e(S); +m(S = [<<"friulive-giulia">>,<<"it">>]) -> e(S); +m(S = [<<"friulivegiulia">>,<<"it">>]) -> e(S); +m(S = [<<"friulivenezia-giulia">>,<<"it">>]) -> e(S); +m(S = [<<"friuliveneziagiulia">>,<<"it">>]) -> e(S); +m(S = [<<"friulivgiulia">>,<<"it">>]) -> e(S); +m(S = [<<"fvg">>,<<"it">>]) -> e(S); +m(S = [<<"laz">>,<<"it">>]) -> e(S); +m(S = [<<"lazio">>,<<"it">>]) -> e(S); +m(S = [<<"lig">>,<<"it">>]) -> e(S); +m(S = [<<"liguria">>,<<"it">>]) -> e(S); +m(S = [<<"lom">>,<<"it">>]) -> e(S); +m(S = [<<"lombardia">>,<<"it">>]) -> e(S); +m(S = [<<"lombardy">>,<<"it">>]) -> e(S); +m(S = [<<"lucania">>,<<"it">>]) -> e(S); +m(S = [<<"mar">>,<<"it">>]) -> e(S); +m(S = [<<"marche">>,<<"it">>]) -> e(S); +m(S = [<<"mol">>,<<"it">>]) -> e(S); +m(S = [<<"molise">>,<<"it">>]) -> e(S); +m(S = [<<"piedmont">>,<<"it">>]) -> e(S); +m(S = [<<"piemonte">>,<<"it">>]) -> e(S); +m(S = [<<"pmn">>,<<"it">>]) -> e(S); +m(S = [<<"pug">>,<<"it">>]) -> e(S); +m(S = [<<"puglia">>,<<"it">>]) -> e(S); +m(S = [<<"sar">>,<<"it">>]) -> e(S); +m(S = [<<"sardegna">>,<<"it">>]) -> e(S); +m(S = [<<"sardinia">>,<<"it">>]) -> e(S); +m(S = [<<"sic">>,<<"it">>]) -> e(S); +m(S = [<<"sicilia">>,<<"it">>]) -> e(S); +m(S = [<<"sicily">>,<<"it">>]) -> e(S); +m(S = [<<"taa">>,<<"it">>]) -> e(S); +m(S = [<<"tos">>,<<"it">>]) -> e(S); +m(S = [<<"toscana">>,<<"it">>]) -> e(S); +m(S = [<<"trentin-sud-tirol">>,<<"it">>]) -> e(S); +m(S = [<<"xn--trentin-sd-tirol-rzb">>,<<"it">>]) -> e(S); +m(S = [<<"trentin-sudtirol">>,<<"it">>]) -> e(S); +m(S = [<<"xn--trentin-sdtirol-7vb">>,<<"it">>]) -> e(S); +m(S = [<<"trentin-sued-tirol">>,<<"it">>]) -> e(S); +m(S = [<<"trentin-suedtirol">>,<<"it">>]) -> e(S); +m(S = [<<"trentino-a-adige">>,<<"it">>]) -> e(S); +m(S = [<<"trentino-aadige">>,<<"it">>]) -> e(S); +m(S = [<<"trentino-alto-adige">>,<<"it">>]) -> e(S); +m(S = [<<"trentino-altoadige">>,<<"it">>]) -> e(S); +m(S = [<<"trentino-s-tirol">>,<<"it">>]) -> e(S); +m(S = [<<"trentino-stirol">>,<<"it">>]) -> e(S); +m(S = [<<"trentino-sud-tirol">>,<<"it">>]) -> e(S); +m(S = [<<"xn--trentino-sd-tirol-c3b">>,<<"it">>]) -> e(S); +m(S = [<<"trentino-sudtirol">>,<<"it">>]) -> e(S); +m(S = [<<"xn--trentino-sdtirol-szb">>,<<"it">>]) -> e(S); +m(S = [<<"trentino-sued-tirol">>,<<"it">>]) -> e(S); +m(S = [<<"trentino-suedtirol">>,<<"it">>]) -> e(S); +m(S = [<<"trentino">>,<<"it">>]) -> e(S); +m(S = [<<"trentinoa-adige">>,<<"it">>]) -> e(S); +m(S = [<<"trentinoaadige">>,<<"it">>]) -> e(S); +m(S = [<<"trentinoalto-adige">>,<<"it">>]) -> e(S); +m(S = [<<"trentinoaltoadige">>,<<"it">>]) -> e(S); +m(S = [<<"trentinos-tirol">>,<<"it">>]) -> e(S); +m(S = [<<"trentinostirol">>,<<"it">>]) -> e(S); +m(S = [<<"trentinosud-tirol">>,<<"it">>]) -> e(S); +m(S = [<<"xn--trentinosd-tirol-rzb">>,<<"it">>]) -> e(S); +m(S = [<<"trentinosudtirol">>,<<"it">>]) -> e(S); +m(S = [<<"xn--trentinosdtirol-7vb">>,<<"it">>]) -> e(S); +m(S = [<<"trentinosued-tirol">>,<<"it">>]) -> e(S); +m(S = [<<"trentinosuedtirol">>,<<"it">>]) -> e(S); +m(S = [<<"trentinsud-tirol">>,<<"it">>]) -> e(S); +m(S = [<<"xn--trentinsd-tirol-6vb">>,<<"it">>]) -> e(S); +m(S = [<<"trentinsudtirol">>,<<"it">>]) -> e(S); +m(S = [<<"xn--trentinsdtirol-nsb">>,<<"it">>]) -> e(S); +m(S = [<<"trentinsued-tirol">>,<<"it">>]) -> e(S); +m(S = [<<"trentinsuedtirol">>,<<"it">>]) -> e(S); +m(S = [<<"tuscany">>,<<"it">>]) -> e(S); +m(S = [<<"umb">>,<<"it">>]) -> e(S); +m(S = [<<"umbria">>,<<"it">>]) -> e(S); +m(S = [<<"val-d-aosta">>,<<"it">>]) -> e(S); +m(S = [<<"val-daosta">>,<<"it">>]) -> e(S); +m(S = [<<"vald-aosta">>,<<"it">>]) -> e(S); +m(S = [<<"valdaosta">>,<<"it">>]) -> e(S); +m(S = [<<"valle-aosta">>,<<"it">>]) -> e(S); +m(S = [<<"valle-d-aosta">>,<<"it">>]) -> e(S); +m(S = [<<"valle-daosta">>,<<"it">>]) -> e(S); +m(S = [<<"valleaosta">>,<<"it">>]) -> e(S); +m(S = [<<"valled-aosta">>,<<"it">>]) -> e(S); +m(S = [<<"valledaosta">>,<<"it">>]) -> e(S); +m(S = [<<"vallee-aoste">>,<<"it">>]) -> e(S); +m(S = [<<"xn--valle-aoste-ebb">>,<<"it">>]) -> e(S); +m(S = [<<"vallee-d-aoste">>,<<"it">>]) -> e(S); +m(S = [<<"xn--valle-d-aoste-ehb">>,<<"it">>]) -> e(S); +m(S = [<<"valleeaoste">>,<<"it">>]) -> e(S); +m(S = [<<"xn--valleaoste-e7a">>,<<"it">>]) -> e(S); +m(S = [<<"valleedaoste">>,<<"it">>]) -> e(S); +m(S = [<<"xn--valledaoste-ebb">>,<<"it">>]) -> e(S); +m(S = [<<"vao">>,<<"it">>]) -> e(S); +m(S = [<<"vda">>,<<"it">>]) -> e(S); +m(S = [<<"ven">>,<<"it">>]) -> e(S); +m(S = [<<"veneto">>,<<"it">>]) -> e(S); +m(S = [<<"ag">>,<<"it">>]) -> e(S); +m(S = [<<"agrigento">>,<<"it">>]) -> e(S); +m(S = [<<"al">>,<<"it">>]) -> e(S); +m(S = [<<"alessandria">>,<<"it">>]) -> e(S); +m(S = [<<"alto-adige">>,<<"it">>]) -> e(S); +m(S = [<<"altoadige">>,<<"it">>]) -> e(S); +m(S = [<<"an">>,<<"it">>]) -> e(S); +m(S = [<<"ancona">>,<<"it">>]) -> e(S); +m(S = [<<"andria-barletta-trani">>,<<"it">>]) -> e(S); +m(S = [<<"andria-trani-barletta">>,<<"it">>]) -> e(S); +m(S = [<<"andriabarlettatrani">>,<<"it">>]) -> e(S); +m(S = [<<"andriatranibarletta">>,<<"it">>]) -> e(S); +m(S = [<<"ao">>,<<"it">>]) -> e(S); +m(S = [<<"aosta">>,<<"it">>]) -> e(S); +m(S = [<<"aoste">>,<<"it">>]) -> e(S); +m(S = [<<"ap">>,<<"it">>]) -> e(S); +m(S = [<<"aq">>,<<"it">>]) -> e(S); +m(S = [<<"aquila">>,<<"it">>]) -> e(S); +m(S = [<<"ar">>,<<"it">>]) -> e(S); +m(S = [<<"arezzo">>,<<"it">>]) -> e(S); +m(S = [<<"ascoli-piceno">>,<<"it">>]) -> e(S); +m(S = [<<"ascolipiceno">>,<<"it">>]) -> e(S); +m(S = [<<"asti">>,<<"it">>]) -> e(S); +m(S = [<<"at">>,<<"it">>]) -> e(S); +m(S = [<<"av">>,<<"it">>]) -> e(S); +m(S = [<<"avellino">>,<<"it">>]) -> e(S); +m(S = [<<"ba">>,<<"it">>]) -> e(S); +m(S = [<<"balsan-sudtirol">>,<<"it">>]) -> e(S); +m(S = [<<"xn--balsan-sdtirol-nsb">>,<<"it">>]) -> e(S); +m(S = [<<"balsan-suedtirol">>,<<"it">>]) -> e(S); +m(S = [<<"balsan">>,<<"it">>]) -> e(S); +m(S = [<<"bari">>,<<"it">>]) -> e(S); +m(S = [<<"barletta-trani-andria">>,<<"it">>]) -> e(S); +m(S = [<<"barlettatraniandria">>,<<"it">>]) -> e(S); +m(S = [<<"belluno">>,<<"it">>]) -> e(S); +m(S = [<<"benevento">>,<<"it">>]) -> e(S); +m(S = [<<"bergamo">>,<<"it">>]) -> e(S); +m(S = [<<"bg">>,<<"it">>]) -> e(S); +m(S = [<<"bi">>,<<"it">>]) -> e(S); +m(S = [<<"biella">>,<<"it">>]) -> e(S); +m(S = [<<"bl">>,<<"it">>]) -> e(S); +m(S = [<<"bn">>,<<"it">>]) -> e(S); +m(S = [<<"bo">>,<<"it">>]) -> e(S); +m(S = [<<"bologna">>,<<"it">>]) -> e(S); +m(S = [<<"bolzano-altoadige">>,<<"it">>]) -> e(S); +m(S = [<<"bolzano">>,<<"it">>]) -> e(S); +m(S = [<<"bozen-sudtirol">>,<<"it">>]) -> e(S); +m(S = [<<"xn--bozen-sdtirol-2ob">>,<<"it">>]) -> e(S); +m(S = [<<"bozen-suedtirol">>,<<"it">>]) -> e(S); +m(S = [<<"bozen">>,<<"it">>]) -> e(S); +m(S = [<<"br">>,<<"it">>]) -> e(S); +m(S = [<<"brescia">>,<<"it">>]) -> e(S); +m(S = [<<"brindisi">>,<<"it">>]) -> e(S); +m(S = [<<"bs">>,<<"it">>]) -> e(S); +m(S = [<<"bt">>,<<"it">>]) -> e(S); +m(S = [<<"bulsan-sudtirol">>,<<"it">>]) -> e(S); +m(S = [<<"xn--bulsan-sdtirol-nsb">>,<<"it">>]) -> e(S); +m(S = [<<"bulsan-suedtirol">>,<<"it">>]) -> e(S); +m(S = [<<"bulsan">>,<<"it">>]) -> e(S); +m(S = [<<"bz">>,<<"it">>]) -> e(S); +m(S = [<<"ca">>,<<"it">>]) -> e(S); +m(S = [<<"cagliari">>,<<"it">>]) -> e(S); +m(S = [<<"caltanissetta">>,<<"it">>]) -> e(S); +m(S = [<<"campidano-medio">>,<<"it">>]) -> e(S); +m(S = [<<"campidanomedio">>,<<"it">>]) -> e(S); +m(S = [<<"campobasso">>,<<"it">>]) -> e(S); +m(S = [<<"carbonia-iglesias">>,<<"it">>]) -> e(S); +m(S = [<<"carboniaiglesias">>,<<"it">>]) -> e(S); +m(S = [<<"carrara-massa">>,<<"it">>]) -> e(S); +m(S = [<<"carraramassa">>,<<"it">>]) -> e(S); +m(S = [<<"caserta">>,<<"it">>]) -> e(S); +m(S = [<<"catania">>,<<"it">>]) -> e(S); +m(S = [<<"catanzaro">>,<<"it">>]) -> e(S); +m(S = [<<"cb">>,<<"it">>]) -> e(S); +m(S = [<<"ce">>,<<"it">>]) -> e(S); +m(S = [<<"cesena-forli">>,<<"it">>]) -> e(S); +m(S = [<<"xn--cesena-forl-mcb">>,<<"it">>]) -> e(S); +m(S = [<<"cesenaforli">>,<<"it">>]) -> e(S); +m(S = [<<"xn--cesenaforl-i8a">>,<<"it">>]) -> e(S); +m(S = [<<"ch">>,<<"it">>]) -> e(S); +m(S = [<<"chieti">>,<<"it">>]) -> e(S); +m(S = [<<"ci">>,<<"it">>]) -> e(S); +m(S = [<<"cl">>,<<"it">>]) -> e(S); +m(S = [<<"cn">>,<<"it">>]) -> e(S); +m(S = [<<"co">>,<<"it">>]) -> e(S); +m(S = [<<"como">>,<<"it">>]) -> e(S); +m(S = [<<"cosenza">>,<<"it">>]) -> e(S); +m(S = [<<"cr">>,<<"it">>]) -> e(S); +m(S = [<<"cremona">>,<<"it">>]) -> e(S); +m(S = [<<"crotone">>,<<"it">>]) -> e(S); +m(S = [<<"cs">>,<<"it">>]) -> e(S); +m(S = [<<"ct">>,<<"it">>]) -> e(S); +m(S = [<<"cuneo">>,<<"it">>]) -> e(S); +m(S = [<<"cz">>,<<"it">>]) -> e(S); +m(S = [<<"dell-ogliastra">>,<<"it">>]) -> e(S); +m(S = [<<"dellogliastra">>,<<"it">>]) -> e(S); +m(S = [<<"en">>,<<"it">>]) -> e(S); +m(S = [<<"enna">>,<<"it">>]) -> e(S); +m(S = [<<"fc">>,<<"it">>]) -> e(S); +m(S = [<<"fe">>,<<"it">>]) -> e(S); +m(S = [<<"fermo">>,<<"it">>]) -> e(S); +m(S = [<<"ferrara">>,<<"it">>]) -> e(S); +m(S = [<<"fg">>,<<"it">>]) -> e(S); +m(S = [<<"fi">>,<<"it">>]) -> e(S); +m(S = [<<"firenze">>,<<"it">>]) -> e(S); +m(S = [<<"florence">>,<<"it">>]) -> e(S); +m(S = [<<"fm">>,<<"it">>]) -> e(S); +m(S = [<<"foggia">>,<<"it">>]) -> e(S); +m(S = [<<"forli-cesena">>,<<"it">>]) -> e(S); +m(S = [<<"xn--forl-cesena-fcb">>,<<"it">>]) -> e(S); +m(S = [<<"forlicesena">>,<<"it">>]) -> e(S); +m(S = [<<"xn--forlcesena-c8a">>,<<"it">>]) -> e(S); +m(S = [<<"fr">>,<<"it">>]) -> e(S); +m(S = [<<"frosinone">>,<<"it">>]) -> e(S); +m(S = [<<"ge">>,<<"it">>]) -> e(S); +m(S = [<<"genoa">>,<<"it">>]) -> e(S); +m(S = [<<"genova">>,<<"it">>]) -> e(S); +m(S = [<<"go">>,<<"it">>]) -> e(S); +m(S = [<<"gorizia">>,<<"it">>]) -> e(S); +m(S = [<<"gr">>,<<"it">>]) -> e(S); +m(S = [<<"grosseto">>,<<"it">>]) -> e(S); +m(S = [<<"iglesias-carbonia">>,<<"it">>]) -> e(S); +m(S = [<<"iglesiascarbonia">>,<<"it">>]) -> e(S); +m(S = [<<"im">>,<<"it">>]) -> e(S); +m(S = [<<"imperia">>,<<"it">>]) -> e(S); +m(S = [<<"is">>,<<"it">>]) -> e(S); +m(S = [<<"isernia">>,<<"it">>]) -> e(S); +m(S = [<<"kr">>,<<"it">>]) -> e(S); +m(S = [<<"la-spezia">>,<<"it">>]) -> e(S); +m(S = [<<"laquila">>,<<"it">>]) -> e(S); +m(S = [<<"laspezia">>,<<"it">>]) -> e(S); +m(S = [<<"latina">>,<<"it">>]) -> e(S); +m(S = [<<"lc">>,<<"it">>]) -> e(S); +m(S = [<<"le">>,<<"it">>]) -> e(S); +m(S = [<<"lecce">>,<<"it">>]) -> e(S); +m(S = [<<"lecco">>,<<"it">>]) -> e(S); +m(S = [<<"li">>,<<"it">>]) -> e(S); +m(S = [<<"livorno">>,<<"it">>]) -> e(S); +m(S = [<<"lo">>,<<"it">>]) -> e(S); +m(S = [<<"lodi">>,<<"it">>]) -> e(S); +m(S = [<<"lt">>,<<"it">>]) -> e(S); +m(S = [<<"lu">>,<<"it">>]) -> e(S); +m(S = [<<"lucca">>,<<"it">>]) -> e(S); +m(S = [<<"macerata">>,<<"it">>]) -> e(S); +m(S = [<<"mantova">>,<<"it">>]) -> e(S); +m(S = [<<"massa-carrara">>,<<"it">>]) -> e(S); +m(S = [<<"massacarrara">>,<<"it">>]) -> e(S); +m(S = [<<"matera">>,<<"it">>]) -> e(S); +m(S = [<<"mb">>,<<"it">>]) -> e(S); +m(S = [<<"mc">>,<<"it">>]) -> e(S); +m(S = [<<"me">>,<<"it">>]) -> e(S); +m(S = [<<"medio-campidano">>,<<"it">>]) -> e(S); +m(S = [<<"mediocampidano">>,<<"it">>]) -> e(S); +m(S = [<<"messina">>,<<"it">>]) -> e(S); +m(S = [<<"mi">>,<<"it">>]) -> e(S); +m(S = [<<"milan">>,<<"it">>]) -> e(S); +m(S = [<<"milano">>,<<"it">>]) -> e(S); +m(S = [<<"mn">>,<<"it">>]) -> e(S); +m(S = [<<"mo">>,<<"it">>]) -> e(S); +m(S = [<<"modena">>,<<"it">>]) -> e(S); +m(S = [<<"monza-brianza">>,<<"it">>]) -> e(S); +m(S = [<<"monza-e-della-brianza">>,<<"it">>]) -> e(S); +m(S = [<<"monza">>,<<"it">>]) -> e(S); +m(S = [<<"monzabrianza">>,<<"it">>]) -> e(S); +m(S = [<<"monzaebrianza">>,<<"it">>]) -> e(S); +m(S = [<<"monzaedellabrianza">>,<<"it">>]) -> e(S); +m(S = [<<"ms">>,<<"it">>]) -> e(S); +m(S = [<<"mt">>,<<"it">>]) -> e(S); +m(S = [<<"na">>,<<"it">>]) -> e(S); +m(S = [<<"naples">>,<<"it">>]) -> e(S); +m(S = [<<"napoli">>,<<"it">>]) -> e(S); +m(S = [<<"no">>,<<"it">>]) -> e(S); +m(S = [<<"novara">>,<<"it">>]) -> e(S); +m(S = [<<"nu">>,<<"it">>]) -> e(S); +m(S = [<<"nuoro">>,<<"it">>]) -> e(S); +m(S = [<<"og">>,<<"it">>]) -> e(S); +m(S = [<<"ogliastra">>,<<"it">>]) -> e(S); +m(S = [<<"olbia-tempio">>,<<"it">>]) -> e(S); +m(S = [<<"olbiatempio">>,<<"it">>]) -> e(S); +m(S = [<<"or">>,<<"it">>]) -> e(S); +m(S = [<<"oristano">>,<<"it">>]) -> e(S); +m(S = [<<"ot">>,<<"it">>]) -> e(S); +m(S = [<<"pa">>,<<"it">>]) -> e(S); +m(S = [<<"padova">>,<<"it">>]) -> e(S); +m(S = [<<"padua">>,<<"it">>]) -> e(S); +m(S = [<<"palermo">>,<<"it">>]) -> e(S); +m(S = [<<"parma">>,<<"it">>]) -> e(S); +m(S = [<<"pavia">>,<<"it">>]) -> e(S); +m(S = [<<"pc">>,<<"it">>]) -> e(S); +m(S = [<<"pd">>,<<"it">>]) -> e(S); +m(S = [<<"pe">>,<<"it">>]) -> e(S); +m(S = [<<"perugia">>,<<"it">>]) -> e(S); +m(S = [<<"pesaro-urbino">>,<<"it">>]) -> e(S); +m(S = [<<"pesarourbino">>,<<"it">>]) -> e(S); +m(S = [<<"pescara">>,<<"it">>]) -> e(S); +m(S = [<<"pg">>,<<"it">>]) -> e(S); +m(S = [<<"pi">>,<<"it">>]) -> e(S); +m(S = [<<"piacenza">>,<<"it">>]) -> e(S); +m(S = [<<"pisa">>,<<"it">>]) -> e(S); +m(S = [<<"pistoia">>,<<"it">>]) -> e(S); +m(S = [<<"pn">>,<<"it">>]) -> e(S); +m(S = [<<"po">>,<<"it">>]) -> e(S); +m(S = [<<"pordenone">>,<<"it">>]) -> e(S); +m(S = [<<"potenza">>,<<"it">>]) -> e(S); +m(S = [<<"pr">>,<<"it">>]) -> e(S); +m(S = [<<"prato">>,<<"it">>]) -> e(S); +m(S = [<<"pt">>,<<"it">>]) -> e(S); +m(S = [<<"pu">>,<<"it">>]) -> e(S); +m(S = [<<"pv">>,<<"it">>]) -> e(S); +m(S = [<<"pz">>,<<"it">>]) -> e(S); +m(S = [<<"ra">>,<<"it">>]) -> e(S); +m(S = [<<"ragusa">>,<<"it">>]) -> e(S); +m(S = [<<"ravenna">>,<<"it">>]) -> e(S); +m(S = [<<"rc">>,<<"it">>]) -> e(S); +m(S = [<<"re">>,<<"it">>]) -> e(S); +m(S = [<<"reggio-calabria">>,<<"it">>]) -> e(S); +m(S = [<<"reggio-emilia">>,<<"it">>]) -> e(S); +m(S = [<<"reggiocalabria">>,<<"it">>]) -> e(S); +m(S = [<<"reggioemilia">>,<<"it">>]) -> e(S); +m(S = [<<"rg">>,<<"it">>]) -> e(S); +m(S = [<<"ri">>,<<"it">>]) -> e(S); +m(S = [<<"rieti">>,<<"it">>]) -> e(S); +m(S = [<<"rimini">>,<<"it">>]) -> e(S); +m(S = [<<"rm">>,<<"it">>]) -> e(S); +m(S = [<<"rn">>,<<"it">>]) -> e(S); +m(S = [<<"ro">>,<<"it">>]) -> e(S); +m(S = [<<"roma">>,<<"it">>]) -> e(S); +m(S = [<<"rome">>,<<"it">>]) -> e(S); +m(S = [<<"rovigo">>,<<"it">>]) -> e(S); +m(S = [<<"sa">>,<<"it">>]) -> e(S); +m(S = [<<"salerno">>,<<"it">>]) -> e(S); +m(S = [<<"sassari">>,<<"it">>]) -> e(S); +m(S = [<<"savona">>,<<"it">>]) -> e(S); +m(S = [<<"si">>,<<"it">>]) -> e(S); +m(S = [<<"siena">>,<<"it">>]) -> e(S); +m(S = [<<"siracusa">>,<<"it">>]) -> e(S); +m(S = [<<"so">>,<<"it">>]) -> e(S); +m(S = [<<"sondrio">>,<<"it">>]) -> e(S); +m(S = [<<"sp">>,<<"it">>]) -> e(S); +m(S = [<<"sr">>,<<"it">>]) -> e(S); +m(S = [<<"ss">>,<<"it">>]) -> e(S); +m(S = [<<"suedtirol">>,<<"it">>]) -> e(S); +m(S = [<<"xn--sdtirol-n2a">>,<<"it">>]) -> e(S); +m(S = [<<"sv">>,<<"it">>]) -> e(S); +m(S = [<<"ta">>,<<"it">>]) -> e(S); +m(S = [<<"taranto">>,<<"it">>]) -> e(S); +m(S = [<<"te">>,<<"it">>]) -> e(S); +m(S = [<<"tempio-olbia">>,<<"it">>]) -> e(S); +m(S = [<<"tempioolbia">>,<<"it">>]) -> e(S); +m(S = [<<"teramo">>,<<"it">>]) -> e(S); +m(S = [<<"terni">>,<<"it">>]) -> e(S); +m(S = [<<"tn">>,<<"it">>]) -> e(S); +m(S = [<<"to">>,<<"it">>]) -> e(S); +m(S = [<<"torino">>,<<"it">>]) -> e(S); +m(S = [<<"tp">>,<<"it">>]) -> e(S); +m(S = [<<"tr">>,<<"it">>]) -> e(S); +m(S = [<<"trani-andria-barletta">>,<<"it">>]) -> e(S); +m(S = [<<"trani-barletta-andria">>,<<"it">>]) -> e(S); +m(S = [<<"traniandriabarletta">>,<<"it">>]) -> e(S); +m(S = [<<"tranibarlettaandria">>,<<"it">>]) -> e(S); +m(S = [<<"trapani">>,<<"it">>]) -> e(S); +m(S = [<<"trento">>,<<"it">>]) -> e(S); +m(S = [<<"treviso">>,<<"it">>]) -> e(S); +m(S = [<<"trieste">>,<<"it">>]) -> e(S); +m(S = [<<"ts">>,<<"it">>]) -> e(S); +m(S = [<<"turin">>,<<"it">>]) -> e(S); +m(S = [<<"tv">>,<<"it">>]) -> e(S); +m(S = [<<"ud">>,<<"it">>]) -> e(S); +m(S = [<<"udine">>,<<"it">>]) -> e(S); +m(S = [<<"urbino-pesaro">>,<<"it">>]) -> e(S); +m(S = [<<"urbinopesaro">>,<<"it">>]) -> e(S); +m(S = [<<"va">>,<<"it">>]) -> e(S); +m(S = [<<"varese">>,<<"it">>]) -> e(S); +m(S = [<<"vb">>,<<"it">>]) -> e(S); +m(S = [<<"vc">>,<<"it">>]) -> e(S); +m(S = [<<"ve">>,<<"it">>]) -> e(S); +m(S = [<<"venezia">>,<<"it">>]) -> e(S); +m(S = [<<"venice">>,<<"it">>]) -> e(S); +m(S = [<<"verbania">>,<<"it">>]) -> e(S); +m(S = [<<"vercelli">>,<<"it">>]) -> e(S); +m(S = [<<"verona">>,<<"it">>]) -> e(S); +m(S = [<<"vi">>,<<"it">>]) -> e(S); +m(S = [<<"vibo-valentia">>,<<"it">>]) -> e(S); +m(S = [<<"vibovalentia">>,<<"it">>]) -> e(S); +m(S = [<<"vicenza">>,<<"it">>]) -> e(S); +m(S = [<<"viterbo">>,<<"it">>]) -> e(S); +m(S = [<<"vr">>,<<"it">>]) -> e(S); +m(S = [<<"vs">>,<<"it">>]) -> e(S); +m(S = [<<"vt">>,<<"it">>]) -> e(S); +m(S = [<<"vv">>,<<"it">>]) -> e(S); +m(S = [<<"je">>]) -> e(S); +m(S = [<<"co">>,<<"je">>]) -> e(S); +m(S = [<<"net">>,<<"je">>]) -> e(S); +m(S = [<<"org">>,<<"je">>]) -> e(S); +m(S = [_,<<"jm">>]) -> e(S); +m(S = [<<"jo">>]) -> e(S); +m(S = [<<"com">>,<<"jo">>]) -> e(S); +m(S = [<<"org">>,<<"jo">>]) -> e(S); +m(S = [<<"net">>,<<"jo">>]) -> e(S); +m(S = [<<"edu">>,<<"jo">>]) -> e(S); +m(S = [<<"sch">>,<<"jo">>]) -> e(S); +m(S = [<<"gov">>,<<"jo">>]) -> e(S); +m(S = [<<"mil">>,<<"jo">>]) -> e(S); +m(S = [<<"name">>,<<"jo">>]) -> e(S); +m(S = [<<"jobs">>]) -> e(S); +m(S = [<<"jp">>]) -> e(S); +m(S = [<<"ac">>,<<"jp">>]) -> e(S); +m(S = [<<"ad">>,<<"jp">>]) -> e(S); +m(S = [<<"co">>,<<"jp">>]) -> e(S); +m(S = [<<"ed">>,<<"jp">>]) -> e(S); +m(S = [<<"go">>,<<"jp">>]) -> e(S); +m(S = [<<"gr">>,<<"jp">>]) -> e(S); +m(S = [<<"lg">>,<<"jp">>]) -> e(S); +m(S = [<<"ne">>,<<"jp">>]) -> e(S); +m(S = [<<"or">>,<<"jp">>]) -> e(S); +m(S = [<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"aomori">>,<<"jp">>]) -> e(S); +m(S = [<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"ehime">>,<<"jp">>]) -> e(S); +m(S = [<<"fukui">>,<<"jp">>]) -> e(S); +m(S = [<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"hiroshima">>,<<"jp">>]) -> e(S); +m(S = [<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"ishikawa">>,<<"jp">>]) -> e(S); +m(S = [<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"kagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"kagoshima">>,<<"jp">>]) -> e(S); +m(S = [<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"kumamoto">>,<<"jp">>]) -> e(S); +m(S = [<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"miyazaki">>,<<"jp">>]) -> e(S); +m(S = [<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"nagasaki">>,<<"jp">>]) -> e(S); +m(S = [<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"oita">>,<<"jp">>]) -> e(S); +m(S = [<<"okayama">>,<<"jp">>]) -> e(S); +m(S = [<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"saga">>,<<"jp">>]) -> e(S); +m(S = [<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"shiga">>,<<"jp">>]) -> e(S); +m(S = [<<"shimane">>,<<"jp">>]) -> e(S); +m(S = [<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"tokushima">>,<<"jp">>]) -> e(S); +m(S = [<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"tottori">>,<<"jp">>]) -> e(S); +m(S = [<<"toyama">>,<<"jp">>]) -> e(S); +m(S = [<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"yamaguchi">>,<<"jp">>]) -> e(S); +m(S = [<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--4pvxs">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--vgu402c">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--c3s14m">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--f6qx53a">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--8pvr4u">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--uist22h">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--djrs72d6uy">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--mkru45i">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--0trq7p7nn">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--8ltr62k">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--2m4a15e">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--efvn9s">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--32vp30h">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--4it797k">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--1lqs71d">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--5rtp49c">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--5js045d">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--ehqz56n">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--1lqs03n">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--qqqt11m">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--kbrq7o">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--pssu33l">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--ntsq17g">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--uisz3g">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--6btw5a">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--1ctwo">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--6orx2r">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--rht61e">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--rht27z">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--djty4k">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--nit225k">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--rht3d">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--klty5x">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--kltx9a">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--kltp7d">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--uuwu58a">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--zbx025d">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--ntso0iqx3a">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--elqq16h">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--4it168d">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--klt787d">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--rny31h">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--7t0a264c">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--5rtq34k">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--k7yn95e">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--tor131o">>,<<"jp">>]) -> e(S); +m(S = [<<"xn--d5qv7z876c">>,<<"jp">>]) -> e(S); +m(S = [_,<<"kawasaki">>,<<"jp">>]) -> e(S); +m(S = [_,<<"kitakyushu">>,<<"jp">>]) -> e(S); +m(S = [_,<<"kobe">>,<<"jp">>]) -> e(S); +m(S = [_,<<"nagoya">>,<<"jp">>]) -> e(S); +m(S = [_,<<"sapporo">>,<<"jp">>]) -> e(S); +m(S = [_,<<"sendai">>,<<"jp">>]) -> e(S); +m(S = [_,<<"yokohama">>,<<"jp">>]) -> e(S); +m(S = [<<"aisai">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"ama">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"anjo">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"asuke">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"chiryu">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"chita">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"fuso">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"gamagori">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"handa">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"hazu">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"hekinan">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"higashiura">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"ichinomiya">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"inazawa">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"inuyama">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"isshiki">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"iwakura">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"kanie">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"kariya">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"kasugai">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"kira">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"kiyosu">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"komaki">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"konan">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"kota">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"mihama">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"miyoshi">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"nishio">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"nisshin">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"obu">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"oguchi">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"oharu">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"okazaki">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"owariasahi">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"seto">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"shikatsu">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"shinshiro">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"shitara">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"tahara">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"takahama">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"tobishima">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"toei">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"togo">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"tokai">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"tokoname">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"toyoake">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"toyohashi">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"toyokawa">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"toyone">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"toyota">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"tsushima">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"yatomi">>,<<"aichi">>,<<"jp">>]) -> e(S); +m(S = [<<"akita">>,<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"daisen">>,<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"fujisato">>,<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"gojome">>,<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"hachirogata">>,<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"happou">>,<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"higashinaruse">>,<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"honjo">>,<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"honjyo">>,<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"ikawa">>,<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"kamikoani">>,<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"kamioka">>,<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"katagami">>,<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"kazuno">>,<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"kitaakita">>,<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"kosaka">>,<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"kyowa">>,<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"misato">>,<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"mitane">>,<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"moriyoshi">>,<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"nikaho">>,<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"noshiro">>,<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"odate">>,<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"oga">>,<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"ogata">>,<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"semboku">>,<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"yokote">>,<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"yurihonjo">>,<<"akita">>,<<"jp">>]) -> e(S); +m(S = [<<"aomori">>,<<"aomori">>,<<"jp">>]) -> e(S); +m(S = [<<"gonohe">>,<<"aomori">>,<<"jp">>]) -> e(S); +m(S = [<<"hachinohe">>,<<"aomori">>,<<"jp">>]) -> e(S); +m(S = [<<"hashikami">>,<<"aomori">>,<<"jp">>]) -> e(S); +m(S = [<<"hiranai">>,<<"aomori">>,<<"jp">>]) -> e(S); +m(S = [<<"hirosaki">>,<<"aomori">>,<<"jp">>]) -> e(S); +m(S = [<<"itayanagi">>,<<"aomori">>,<<"jp">>]) -> e(S); +m(S = [<<"kuroishi">>,<<"aomori">>,<<"jp">>]) -> e(S); +m(S = [<<"misawa">>,<<"aomori">>,<<"jp">>]) -> e(S); +m(S = [<<"mutsu">>,<<"aomori">>,<<"jp">>]) -> e(S); +m(S = [<<"nakadomari">>,<<"aomori">>,<<"jp">>]) -> e(S); +m(S = [<<"noheji">>,<<"aomori">>,<<"jp">>]) -> e(S); +m(S = [<<"oirase">>,<<"aomori">>,<<"jp">>]) -> e(S); +m(S = [<<"owani">>,<<"aomori">>,<<"jp">>]) -> e(S); +m(S = [<<"rokunohe">>,<<"aomori">>,<<"jp">>]) -> e(S); +m(S = [<<"sannohe">>,<<"aomori">>,<<"jp">>]) -> e(S); +m(S = [<<"shichinohe">>,<<"aomori">>,<<"jp">>]) -> e(S); +m(S = [<<"shingo">>,<<"aomori">>,<<"jp">>]) -> e(S); +m(S = [<<"takko">>,<<"aomori">>,<<"jp">>]) -> e(S); +m(S = [<<"towada">>,<<"aomori">>,<<"jp">>]) -> e(S); +m(S = [<<"tsugaru">>,<<"aomori">>,<<"jp">>]) -> e(S); +m(S = [<<"tsuruta">>,<<"aomori">>,<<"jp">>]) -> e(S); +m(S = [<<"abiko">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"asahi">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"chonan">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"chosei">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"choshi">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"chuo">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"funabashi">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"futtsu">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"hanamigawa">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"ichihara">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"ichikawa">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"ichinomiya">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"inzai">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"isumi">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"kamagaya">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"kamogawa">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"kashiwa">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"katori">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"katsuura">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"kimitsu">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"kisarazu">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"kozaki">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"kujukuri">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"kyonan">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"matsudo">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"midori">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"mihama">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"minamiboso">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"mobara">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"mutsuzawa">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"nagara">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"nagareyama">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"narashino">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"narita">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"noda">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"oamishirasato">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"omigawa">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"onjuku">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"otaki">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"sakae">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"sakura">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"shimofusa">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"shirako">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"shiroi">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"shisui">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"sodegaura">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"sosa">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"tako">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"tateyama">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"togane">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"tohnosho">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"tomisato">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"urayasu">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"yachimata">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"yachiyo">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"yokaichiba">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"yokoshibahikari">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"yotsukaido">>,<<"chiba">>,<<"jp">>]) -> e(S); +m(S = [<<"ainan">>,<<"ehime">>,<<"jp">>]) -> e(S); +m(S = [<<"honai">>,<<"ehime">>,<<"jp">>]) -> e(S); +m(S = [<<"ikata">>,<<"ehime">>,<<"jp">>]) -> e(S); +m(S = [<<"imabari">>,<<"ehime">>,<<"jp">>]) -> e(S); +m(S = [<<"iyo">>,<<"ehime">>,<<"jp">>]) -> e(S); +m(S = [<<"kamijima">>,<<"ehime">>,<<"jp">>]) -> e(S); +m(S = [<<"kihoku">>,<<"ehime">>,<<"jp">>]) -> e(S); +m(S = [<<"kumakogen">>,<<"ehime">>,<<"jp">>]) -> e(S); +m(S = [<<"masaki">>,<<"ehime">>,<<"jp">>]) -> e(S); +m(S = [<<"matsuno">>,<<"ehime">>,<<"jp">>]) -> e(S); +m(S = [<<"matsuyama">>,<<"ehime">>,<<"jp">>]) -> e(S); +m(S = [<<"namikata">>,<<"ehime">>,<<"jp">>]) -> e(S); +m(S = [<<"niihama">>,<<"ehime">>,<<"jp">>]) -> e(S); +m(S = [<<"ozu">>,<<"ehime">>,<<"jp">>]) -> e(S); +m(S = [<<"saijo">>,<<"ehime">>,<<"jp">>]) -> e(S); +m(S = [<<"seiyo">>,<<"ehime">>,<<"jp">>]) -> e(S); +m(S = [<<"shikokuchuo">>,<<"ehime">>,<<"jp">>]) -> e(S); +m(S = [<<"tobe">>,<<"ehime">>,<<"jp">>]) -> e(S); +m(S = [<<"toon">>,<<"ehime">>,<<"jp">>]) -> e(S); +m(S = [<<"uchiko">>,<<"ehime">>,<<"jp">>]) -> e(S); +m(S = [<<"uwajima">>,<<"ehime">>,<<"jp">>]) -> e(S); +m(S = [<<"yawatahama">>,<<"ehime">>,<<"jp">>]) -> e(S); +m(S = [<<"echizen">>,<<"fukui">>,<<"jp">>]) -> e(S); +m(S = [<<"eiheiji">>,<<"fukui">>,<<"jp">>]) -> e(S); +m(S = [<<"fukui">>,<<"fukui">>,<<"jp">>]) -> e(S); +m(S = [<<"ikeda">>,<<"fukui">>,<<"jp">>]) -> e(S); +m(S = [<<"katsuyama">>,<<"fukui">>,<<"jp">>]) -> e(S); +m(S = [<<"mihama">>,<<"fukui">>,<<"jp">>]) -> e(S); +m(S = [<<"minamiechizen">>,<<"fukui">>,<<"jp">>]) -> e(S); +m(S = [<<"obama">>,<<"fukui">>,<<"jp">>]) -> e(S); +m(S = [<<"ohi">>,<<"fukui">>,<<"jp">>]) -> e(S); +m(S = [<<"ono">>,<<"fukui">>,<<"jp">>]) -> e(S); +m(S = [<<"sabae">>,<<"fukui">>,<<"jp">>]) -> e(S); +m(S = [<<"sakai">>,<<"fukui">>,<<"jp">>]) -> e(S); +m(S = [<<"takahama">>,<<"fukui">>,<<"jp">>]) -> e(S); +m(S = [<<"tsuruga">>,<<"fukui">>,<<"jp">>]) -> e(S); +m(S = [<<"wakasa">>,<<"fukui">>,<<"jp">>]) -> e(S); +m(S = [<<"ashiya">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"buzen">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"chikugo">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"chikuho">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"chikujo">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"chikushino">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"chikuzen">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"chuo">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"dazaifu">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"fukuchi">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"hakata">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"higashi">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"hirokawa">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"hisayama">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"iizuka">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"inatsuki">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"kaho">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"kasuga">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"kasuya">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"kawara">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"keisen">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"koga">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"kurate">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"kurogi">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"kurume">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"minami">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"miyako">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"miyama">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"miyawaka">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"mizumaki">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"munakata">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"nakagawa">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"nakama">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"nishi">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"nogata">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"ogori">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"okagaki">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"okawa">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"oki">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"omuta">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"onga">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"onojo">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"oto">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"saigawa">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"sasaguri">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"shingu">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"shinyoshitomi">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"shonai">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"soeda">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"sue">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"tachiarai">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"tagawa">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"takata">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"toho">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"toyotsu">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"tsuiki">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"ukiha">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"umi">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"usui">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"yamada">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"yame">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"yanagawa">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"yukuhashi">>,<<"fukuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"aizubange">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"aizumisato">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"aizuwakamatsu">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"asakawa">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"bandai">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"date">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"fukushima">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"furudono">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"futaba">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"hanawa">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"higashi">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"hirata">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"hirono">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"iitate">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"inawashiro">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"ishikawa">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"iwaki">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"izumizaki">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"kagamiishi">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"kaneyama">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"kawamata">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"kitakata">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"kitashiobara">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"koori">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"koriyama">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"kunimi">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"miharu">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"mishima">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"namie">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"nango">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"nishiaizu">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"nishigo">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"okuma">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"omotego">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"ono">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"otama">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"samegawa">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"shimogo">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"shirakawa">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"showa">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"soma">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"sukagawa">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"taishin">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"tamakawa">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"tanagura">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"tenei">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"yabuki">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"yamato">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"yamatsuri">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"yanaizu">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"yugawa">>,<<"fukushima">>,<<"jp">>]) -> e(S); +m(S = [<<"anpachi">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"ena">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"gifu">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"ginan">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"godo">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"gujo">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"hashima">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"hichiso">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"hida">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"higashishirakawa">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"ibigawa">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"ikeda">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"kakamigahara">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"kani">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"kasahara">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"kasamatsu">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"kawaue">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"kitagata">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"mino">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"minokamo">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"mitake">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"mizunami">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"motosu">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"nakatsugawa">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"ogaki">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"sakahogi">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"seki">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"sekigahara">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"shirakawa">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"tajimi">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"takayama">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"tarui">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"toki">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"tomika">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"wanouchi">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"yamagata">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"yaotsu">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"yoro">>,<<"gifu">>,<<"jp">>]) -> e(S); +m(S = [<<"annaka">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"chiyoda">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"fujioka">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"higashiagatsuma">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"isesaki">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"itakura">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"kanna">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"kanra">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"katashina">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"kawaba">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"kiryu">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"kusatsu">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"maebashi">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"meiwa">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"midori">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"minakami">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"naganohara">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"nakanojo">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"nanmoku">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"numata">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"oizumi">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"ora">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"ota">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"shibukawa">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"shimonita">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"shinto">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"showa">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"takasaki">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"takayama">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"tamamura">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"tatebayashi">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"tomioka">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"tsukiyono">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"tsumagoi">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"ueno">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"yoshioka">>,<<"gunma">>,<<"jp">>]) -> e(S); +m(S = [<<"asaminami">>,<<"hiroshima">>,<<"jp">>]) -> e(S); +m(S = [<<"daiwa">>,<<"hiroshima">>,<<"jp">>]) -> e(S); +m(S = [<<"etajima">>,<<"hiroshima">>,<<"jp">>]) -> e(S); +m(S = [<<"fuchu">>,<<"hiroshima">>,<<"jp">>]) -> e(S); +m(S = [<<"fukuyama">>,<<"hiroshima">>,<<"jp">>]) -> e(S); +m(S = [<<"hatsukaichi">>,<<"hiroshima">>,<<"jp">>]) -> e(S); +m(S = [<<"higashihiroshima">>,<<"hiroshima">>,<<"jp">>]) -> e(S); +m(S = [<<"hongo">>,<<"hiroshima">>,<<"jp">>]) -> e(S); +m(S = [<<"jinsekikogen">>,<<"hiroshima">>,<<"jp">>]) -> e(S); +m(S = [<<"kaita">>,<<"hiroshima">>,<<"jp">>]) -> e(S); +m(S = [<<"kui">>,<<"hiroshima">>,<<"jp">>]) -> e(S); +m(S = [<<"kumano">>,<<"hiroshima">>,<<"jp">>]) -> e(S); +m(S = [<<"kure">>,<<"hiroshima">>,<<"jp">>]) -> e(S); +m(S = [<<"mihara">>,<<"hiroshima">>,<<"jp">>]) -> e(S); +m(S = [<<"miyoshi">>,<<"hiroshima">>,<<"jp">>]) -> e(S); +m(S = [<<"naka">>,<<"hiroshima">>,<<"jp">>]) -> e(S); +m(S = [<<"onomichi">>,<<"hiroshima">>,<<"jp">>]) -> e(S); +m(S = [<<"osakikamijima">>,<<"hiroshima">>,<<"jp">>]) -> e(S); +m(S = [<<"otake">>,<<"hiroshima">>,<<"jp">>]) -> e(S); +m(S = [<<"saka">>,<<"hiroshima">>,<<"jp">>]) -> e(S); +m(S = [<<"sera">>,<<"hiroshima">>,<<"jp">>]) -> e(S); +m(S = [<<"seranishi">>,<<"hiroshima">>,<<"jp">>]) -> e(S); +m(S = [<<"shinichi">>,<<"hiroshima">>,<<"jp">>]) -> e(S); +m(S = [<<"shobara">>,<<"hiroshima">>,<<"jp">>]) -> e(S); +m(S = [<<"takehara">>,<<"hiroshima">>,<<"jp">>]) -> e(S); +m(S = [<<"abashiri">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"abira">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"aibetsu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"akabira">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"akkeshi">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"asahikawa">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"ashibetsu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"ashoro">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"assabu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"atsuma">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"bibai">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"biei">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"bifuka">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"bihoro">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"biratori">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"chippubetsu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"chitose">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"date">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"ebetsu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"embetsu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"eniwa">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"erimo">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"esan">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"esashi">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"fukagawa">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"fukushima">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"furano">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"furubira">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"haboro">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"hakodate">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"hamatonbetsu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"hidaka">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"higashikagura">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"higashikawa">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"hiroo">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"hokuryu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"hokuto">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"honbetsu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"horokanai">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"horonobe">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"ikeda">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"imakane">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"ishikari">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"iwamizawa">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"iwanai">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"kamifurano">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"kamikawa">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"kamishihoro">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"kamisunagawa">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"kamoenai">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"kayabe">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"kembuchi">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"kikonai">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"kimobetsu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"kitahiroshima">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"kitami">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"kiyosato">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"koshimizu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"kunneppu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"kuriyama">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"kuromatsunai">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"kushiro">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"kutchan">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"kyowa">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"mashike">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"matsumae">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"mikasa">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"minamifurano">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"mombetsu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"moseushi">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"mukawa">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"muroran">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"naie">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"nakagawa">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"nakasatsunai">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"nakatombetsu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"nanae">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"nanporo">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"nayoro">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"nemuro">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"niikappu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"niki">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"nishiokoppe">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"noboribetsu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"numata">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"obihiro">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"obira">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"oketo">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"okoppe">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"otaru">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"otobe">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"otofuke">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"otoineppu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"oumu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"ozora">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"pippu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"rankoshi">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"rebun">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"rikubetsu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"rishiri">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"rishirifuji">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"saroma">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"sarufutsu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"shakotan">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"shari">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"shibecha">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"shibetsu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"shikabe">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"shikaoi">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"shimamaki">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"shimizu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"shimokawa">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"shinshinotsu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"shintoku">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"shiranuka">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"shiraoi">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"shiriuchi">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"sobetsu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"sunagawa">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"taiki">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"takasu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"takikawa">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"takinoue">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"teshikaga">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"tobetsu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"tohma">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"tomakomai">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"tomari">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"toya">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"toyako">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"toyotomi">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"toyoura">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"tsubetsu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"tsukigata">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"urakawa">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"urausu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"uryu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"utashinai">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"wakkanai">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"wassamu">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"yakumo">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"yoichi">>,<<"hokkaido">>,<<"jp">>]) -> e(S); +m(S = [<<"aioi">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"akashi">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"ako">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"amagasaki">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"aogaki">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"asago">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"ashiya">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"awaji">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"fukusaki">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"goshiki">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"harima">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"himeji">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"ichikawa">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"inagawa">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"itami">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"kakogawa">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"kamigori">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"kamikawa">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"kasai">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"kasuga">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"kawanishi">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"miki">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"minamiawaji">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"nishinomiya">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"nishiwaki">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"ono">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"sanda">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"sannan">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"sasayama">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"sayo">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"shingu">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"shinonsen">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"shiso">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"sumoto">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"taishi">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"taka">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"takarazuka">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"takasago">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"takino">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"tamba">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"tatsuno">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"toyooka">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"yabu">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"yashiro">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"yoka">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"yokawa">>,<<"hyogo">>,<<"jp">>]) -> e(S); +m(S = [<<"ami">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"asahi">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"bando">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"chikusei">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"daigo">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"fujishiro">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"hitachi">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"hitachinaka">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"hitachiomiya">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"hitachiota">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"ibaraki">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"ina">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"inashiki">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"itako">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"iwama">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"joso">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"kamisu">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"kasama">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"kashima">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"kasumigaura">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"koga">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"miho">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"mito">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"moriya">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"naka">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"namegata">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"oarai">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"ogawa">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"omitama">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"ryugasaki">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"sakai">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"sakuragawa">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"shimodate">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"shimotsuma">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"shirosato">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"sowa">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"suifu">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"takahagi">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"tamatsukuri">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"tokai">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"tomobe">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"tone">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"toride">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"tsuchiura">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"tsukuba">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"uchihara">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"ushiku">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"yachiyo">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"yamagata">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"yawara">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"yuki">>,<<"ibaraki">>,<<"jp">>]) -> e(S); +m(S = [<<"anamizu">>,<<"ishikawa">>,<<"jp">>]) -> e(S); +m(S = [<<"hakui">>,<<"ishikawa">>,<<"jp">>]) -> e(S); +m(S = [<<"hakusan">>,<<"ishikawa">>,<<"jp">>]) -> e(S); +m(S = [<<"kaga">>,<<"ishikawa">>,<<"jp">>]) -> e(S); +m(S = [<<"kahoku">>,<<"ishikawa">>,<<"jp">>]) -> e(S); +m(S = [<<"kanazawa">>,<<"ishikawa">>,<<"jp">>]) -> e(S); +m(S = [<<"kawakita">>,<<"ishikawa">>,<<"jp">>]) -> e(S); +m(S = [<<"komatsu">>,<<"ishikawa">>,<<"jp">>]) -> e(S); +m(S = [<<"nakanoto">>,<<"ishikawa">>,<<"jp">>]) -> e(S); +m(S = [<<"nanao">>,<<"ishikawa">>,<<"jp">>]) -> e(S); +m(S = [<<"nomi">>,<<"ishikawa">>,<<"jp">>]) -> e(S); +m(S = [<<"nonoichi">>,<<"ishikawa">>,<<"jp">>]) -> e(S); +m(S = [<<"noto">>,<<"ishikawa">>,<<"jp">>]) -> e(S); +m(S = [<<"shika">>,<<"ishikawa">>,<<"jp">>]) -> e(S); +m(S = [<<"suzu">>,<<"ishikawa">>,<<"jp">>]) -> e(S); +m(S = [<<"tsubata">>,<<"ishikawa">>,<<"jp">>]) -> e(S); +m(S = [<<"tsurugi">>,<<"ishikawa">>,<<"jp">>]) -> e(S); +m(S = [<<"uchinada">>,<<"ishikawa">>,<<"jp">>]) -> e(S); +m(S = [<<"wajima">>,<<"ishikawa">>,<<"jp">>]) -> e(S); +m(S = [<<"fudai">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"fujisawa">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"hanamaki">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"hiraizumi">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"hirono">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"ichinohe">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"ichinoseki">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"iwaizumi">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"iwate">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"joboji">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"kamaishi">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"kanegasaki">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"karumai">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"kawai">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"kitakami">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"kuji">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"kunohe">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"kuzumaki">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"miyako">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"mizusawa">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"morioka">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"ninohe">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"noda">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"ofunato">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"oshu">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"otsuchi">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"rikuzentakata">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"shiwa">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"shizukuishi">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"sumita">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"tanohata">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"tono">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"yahaba">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"yamada">>,<<"iwate">>,<<"jp">>]) -> e(S); +m(S = [<<"ayagawa">>,<<"kagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"higashikagawa">>,<<"kagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"kanonji">>,<<"kagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"kotohira">>,<<"kagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"manno">>,<<"kagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"marugame">>,<<"kagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"mitoyo">>,<<"kagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"naoshima">>,<<"kagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"sanuki">>,<<"kagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"tadotsu">>,<<"kagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"takamatsu">>,<<"kagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"tonosho">>,<<"kagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"uchinomi">>,<<"kagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"utazu">>,<<"kagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"zentsuji">>,<<"kagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"akune">>,<<"kagoshima">>,<<"jp">>]) -> e(S); +m(S = [<<"amami">>,<<"kagoshima">>,<<"jp">>]) -> e(S); +m(S = [<<"hioki">>,<<"kagoshima">>,<<"jp">>]) -> e(S); +m(S = [<<"isa">>,<<"kagoshima">>,<<"jp">>]) -> e(S); +m(S = [<<"isen">>,<<"kagoshima">>,<<"jp">>]) -> e(S); +m(S = [<<"izumi">>,<<"kagoshima">>,<<"jp">>]) -> e(S); +m(S = [<<"kagoshima">>,<<"kagoshima">>,<<"jp">>]) -> e(S); +m(S = [<<"kanoya">>,<<"kagoshima">>,<<"jp">>]) -> e(S); +m(S = [<<"kawanabe">>,<<"kagoshima">>,<<"jp">>]) -> e(S); +m(S = [<<"kinko">>,<<"kagoshima">>,<<"jp">>]) -> e(S); +m(S = [<<"kouyama">>,<<"kagoshima">>,<<"jp">>]) -> e(S); +m(S = [<<"makurazaki">>,<<"kagoshima">>,<<"jp">>]) -> e(S); +m(S = [<<"matsumoto">>,<<"kagoshima">>,<<"jp">>]) -> e(S); +m(S = [<<"minamitane">>,<<"kagoshima">>,<<"jp">>]) -> e(S); +m(S = [<<"nakatane">>,<<"kagoshima">>,<<"jp">>]) -> e(S); +m(S = [<<"nishinoomote">>,<<"kagoshima">>,<<"jp">>]) -> e(S); +m(S = [<<"satsumasendai">>,<<"kagoshima">>,<<"jp">>]) -> e(S); +m(S = [<<"soo">>,<<"kagoshima">>,<<"jp">>]) -> e(S); +m(S = [<<"tarumizu">>,<<"kagoshima">>,<<"jp">>]) -> e(S); +m(S = [<<"yusui">>,<<"kagoshima">>,<<"jp">>]) -> e(S); +m(S = [<<"aikawa">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"atsugi">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"ayase">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"chigasaki">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"ebina">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"fujisawa">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"hadano">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"hakone">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"hiratsuka">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"isehara">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"kaisei">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"kamakura">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"kiyokawa">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"matsuda">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"minamiashigara">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"miura">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"nakai">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"ninomiya">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"odawara">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"oi">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"oiso">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"sagamihara">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"samukawa">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"tsukui">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"yamakita">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"yamato">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"yokosuka">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"yugawara">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"zama">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"zushi">>,<<"kanagawa">>,<<"jp">>]) -> e(S); +m(S = [<<"aki">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"geisei">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"hidaka">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"higashitsuno">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"ino">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"kagami">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"kami">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"kitagawa">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"kochi">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"mihara">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"motoyama">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"muroto">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"nahari">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"nakamura">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"nankoku">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"nishitosa">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"niyodogawa">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"ochi">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"okawa">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"otoyo">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"otsuki">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"sakawa">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"sukumo">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"susaki">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"tosa">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"tosashimizu">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"toyo">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"tsuno">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"umaji">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"yasuda">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"yusuhara">>,<<"kochi">>,<<"jp">>]) -> e(S); +m(S = [<<"amakusa">>,<<"kumamoto">>,<<"jp">>]) -> e(S); +m(S = [<<"arao">>,<<"kumamoto">>,<<"jp">>]) -> e(S); +m(S = [<<"aso">>,<<"kumamoto">>,<<"jp">>]) -> e(S); +m(S = [<<"choyo">>,<<"kumamoto">>,<<"jp">>]) -> e(S); +m(S = [<<"gyokuto">>,<<"kumamoto">>,<<"jp">>]) -> e(S); +m(S = [<<"kamiamakusa">>,<<"kumamoto">>,<<"jp">>]) -> e(S); +m(S = [<<"kikuchi">>,<<"kumamoto">>,<<"jp">>]) -> e(S); +m(S = [<<"kumamoto">>,<<"kumamoto">>,<<"jp">>]) -> e(S); +m(S = [<<"mashiki">>,<<"kumamoto">>,<<"jp">>]) -> e(S); +m(S = [<<"mifune">>,<<"kumamoto">>,<<"jp">>]) -> e(S); +m(S = [<<"minamata">>,<<"kumamoto">>,<<"jp">>]) -> e(S); +m(S = [<<"minamioguni">>,<<"kumamoto">>,<<"jp">>]) -> e(S); +m(S = [<<"nagasu">>,<<"kumamoto">>,<<"jp">>]) -> e(S); +m(S = [<<"nishihara">>,<<"kumamoto">>,<<"jp">>]) -> e(S); +m(S = [<<"oguni">>,<<"kumamoto">>,<<"jp">>]) -> e(S); +m(S = [<<"ozu">>,<<"kumamoto">>,<<"jp">>]) -> e(S); +m(S = [<<"sumoto">>,<<"kumamoto">>,<<"jp">>]) -> e(S); +m(S = [<<"takamori">>,<<"kumamoto">>,<<"jp">>]) -> e(S); +m(S = [<<"uki">>,<<"kumamoto">>,<<"jp">>]) -> e(S); +m(S = [<<"uto">>,<<"kumamoto">>,<<"jp">>]) -> e(S); +m(S = [<<"yamaga">>,<<"kumamoto">>,<<"jp">>]) -> e(S); +m(S = [<<"yamato">>,<<"kumamoto">>,<<"jp">>]) -> e(S); +m(S = [<<"yatsushiro">>,<<"kumamoto">>,<<"jp">>]) -> e(S); +m(S = [<<"ayabe">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"fukuchiyama">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"higashiyama">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"ide">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"ine">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"joyo">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"kameoka">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"kamo">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"kita">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"kizu">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"kumiyama">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"kyotamba">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"kyotanabe">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"kyotango">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"maizuru">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"minami">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"minamiyamashiro">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"miyazu">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"muko">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"nagaokakyo">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"nakagyo">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"nantan">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"oyamazaki">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"sakyo">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"seika">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"tanabe">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"uji">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"ujitawara">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"wazuka">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"yamashina">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"yawata">>,<<"kyoto">>,<<"jp">>]) -> e(S); +m(S = [<<"asahi">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"inabe">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"ise">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"kameyama">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"kawagoe">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"kiho">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"kisosaki">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"kiwa">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"komono">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"kumano">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"kuwana">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"matsusaka">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"meiwa">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"mihama">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"minamiise">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"misugi">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"miyama">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"nabari">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"shima">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"suzuka">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"tado">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"taiki">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"taki">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"tamaki">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"toba">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"tsu">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"udono">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"ureshino">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"watarai">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"yokkaichi">>,<<"mie">>,<<"jp">>]) -> e(S); +m(S = [<<"furukawa">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"higashimatsushima">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"ishinomaki">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"iwanuma">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"kakuda">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"kami">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"kawasaki">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"marumori">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"matsushima">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"minamisanriku">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"misato">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"murata">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"natori">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"ogawara">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"ohira">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"onagawa">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"osaki">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"rifu">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"semine">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"shibata">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"shichikashuku">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"shikama">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"shiogama">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"shiroishi">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"tagajo">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"taiwa">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"tome">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"tomiya">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"wakuya">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"watari">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"yamamoto">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"zao">>,<<"miyagi">>,<<"jp">>]) -> e(S); +m(S = [<<"aya">>,<<"miyazaki">>,<<"jp">>]) -> e(S); +m(S = [<<"ebino">>,<<"miyazaki">>,<<"jp">>]) -> e(S); +m(S = [<<"gokase">>,<<"miyazaki">>,<<"jp">>]) -> e(S); +m(S = [<<"hyuga">>,<<"miyazaki">>,<<"jp">>]) -> e(S); +m(S = [<<"kadogawa">>,<<"miyazaki">>,<<"jp">>]) -> e(S); +m(S = [<<"kawaminami">>,<<"miyazaki">>,<<"jp">>]) -> e(S); +m(S = [<<"kijo">>,<<"miyazaki">>,<<"jp">>]) -> e(S); +m(S = [<<"kitagawa">>,<<"miyazaki">>,<<"jp">>]) -> e(S); +m(S = [<<"kitakata">>,<<"miyazaki">>,<<"jp">>]) -> e(S); +m(S = [<<"kitaura">>,<<"miyazaki">>,<<"jp">>]) -> e(S); +m(S = [<<"kobayashi">>,<<"miyazaki">>,<<"jp">>]) -> e(S); +m(S = [<<"kunitomi">>,<<"miyazaki">>,<<"jp">>]) -> e(S); +m(S = [<<"kushima">>,<<"miyazaki">>,<<"jp">>]) -> e(S); +m(S = [<<"mimata">>,<<"miyazaki">>,<<"jp">>]) -> e(S); +m(S = [<<"miyakonojo">>,<<"miyazaki">>,<<"jp">>]) -> e(S); +m(S = [<<"miyazaki">>,<<"miyazaki">>,<<"jp">>]) -> e(S); +m(S = [<<"morotsuka">>,<<"miyazaki">>,<<"jp">>]) -> e(S); +m(S = [<<"nichinan">>,<<"miyazaki">>,<<"jp">>]) -> e(S); +m(S = [<<"nishimera">>,<<"miyazaki">>,<<"jp">>]) -> e(S); +m(S = [<<"nobeoka">>,<<"miyazaki">>,<<"jp">>]) -> e(S); +m(S = [<<"saito">>,<<"miyazaki">>,<<"jp">>]) -> e(S); +m(S = [<<"shiiba">>,<<"miyazaki">>,<<"jp">>]) -> e(S); +m(S = [<<"shintomi">>,<<"miyazaki">>,<<"jp">>]) -> e(S); +m(S = [<<"takaharu">>,<<"miyazaki">>,<<"jp">>]) -> e(S); +m(S = [<<"takanabe">>,<<"miyazaki">>,<<"jp">>]) -> e(S); +m(S = [<<"takazaki">>,<<"miyazaki">>,<<"jp">>]) -> e(S); +m(S = [<<"tsuno">>,<<"miyazaki">>,<<"jp">>]) -> e(S); +m(S = [<<"achi">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"agematsu">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"anan">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"aoki">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"asahi">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"azumino">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"chikuhoku">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"chikuma">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"chino">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"fujimi">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"hakuba">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"hara">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"hiraya">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"iida">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"iijima">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"iiyama">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"iizuna">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"ikeda">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"ikusaka">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"ina">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"karuizawa">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"kawakami">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"kiso">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"kisofukushima">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"kitaaiki">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"komagane">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"komoro">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"matsukawa">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"matsumoto">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"miasa">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"minamiaiki">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"minamimaki">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"minamiminowa">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"minowa">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"miyada">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"miyota">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"mochizuki">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"nagano">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"nagawa">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"nagiso">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"nakagawa">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"nakano">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"nozawaonsen">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"obuse">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"ogawa">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"okaya">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"omachi">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"omi">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"ookuwa">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"ooshika">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"otaki">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"otari">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"sakae">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"sakaki">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"saku">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"sakuho">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"shimosuwa">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"shinanomachi">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"shiojiri">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"suwa">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"suzaka">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"takagi">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"takamori">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"takayama">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"tateshina">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"tatsuno">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"togakushi">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"togura">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"tomi">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"ueda">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"wada">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"yamagata">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"yamanouchi">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"yasaka">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"yasuoka">>,<<"nagano">>,<<"jp">>]) -> e(S); +m(S = [<<"chijiwa">>,<<"nagasaki">>,<<"jp">>]) -> e(S); +m(S = [<<"futsu">>,<<"nagasaki">>,<<"jp">>]) -> e(S); +m(S = [<<"goto">>,<<"nagasaki">>,<<"jp">>]) -> e(S); +m(S = [<<"hasami">>,<<"nagasaki">>,<<"jp">>]) -> e(S); +m(S = [<<"hirado">>,<<"nagasaki">>,<<"jp">>]) -> e(S); +m(S = [<<"iki">>,<<"nagasaki">>,<<"jp">>]) -> e(S); +m(S = [<<"isahaya">>,<<"nagasaki">>,<<"jp">>]) -> e(S); +m(S = [<<"kawatana">>,<<"nagasaki">>,<<"jp">>]) -> e(S); +m(S = [<<"kuchinotsu">>,<<"nagasaki">>,<<"jp">>]) -> e(S); +m(S = [<<"matsuura">>,<<"nagasaki">>,<<"jp">>]) -> e(S); +m(S = [<<"nagasaki">>,<<"nagasaki">>,<<"jp">>]) -> e(S); +m(S = [<<"obama">>,<<"nagasaki">>,<<"jp">>]) -> e(S); +m(S = [<<"omura">>,<<"nagasaki">>,<<"jp">>]) -> e(S); +m(S = [<<"oseto">>,<<"nagasaki">>,<<"jp">>]) -> e(S); +m(S = [<<"saikai">>,<<"nagasaki">>,<<"jp">>]) -> e(S); +m(S = [<<"sasebo">>,<<"nagasaki">>,<<"jp">>]) -> e(S); +m(S = [<<"seihi">>,<<"nagasaki">>,<<"jp">>]) -> e(S); +m(S = [<<"shimabara">>,<<"nagasaki">>,<<"jp">>]) -> e(S); +m(S = [<<"shinkamigoto">>,<<"nagasaki">>,<<"jp">>]) -> e(S); +m(S = [<<"togitsu">>,<<"nagasaki">>,<<"jp">>]) -> e(S); +m(S = [<<"tsushima">>,<<"nagasaki">>,<<"jp">>]) -> e(S); +m(S = [<<"unzen">>,<<"nagasaki">>,<<"jp">>]) -> e(S); +m(S = [<<"ando">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"gose">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"heguri">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"higashiyoshino">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"ikaruga">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"ikoma">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"kamikitayama">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"kanmaki">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"kashiba">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"kashihara">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"katsuragi">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"kawai">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"kawakami">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"kawanishi">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"koryo">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"kurotaki">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"mitsue">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"miyake">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"nara">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"nosegawa">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"oji">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"ouda">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"oyodo">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"sakurai">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"sango">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"shimoichi">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"shimokitayama">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"shinjo">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"soni">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"takatori">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"tawaramoto">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"tenkawa">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"tenri">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"uda">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"yamatokoriyama">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"yamatotakada">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"yamazoe">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"yoshino">>,<<"nara">>,<<"jp">>]) -> e(S); +m(S = [<<"aga">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"agano">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"gosen">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"itoigawa">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"izumozaki">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"joetsu">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"kamo">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"kariwa">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"kashiwazaki">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"minamiuonuma">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"mitsuke">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"muika">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"murakami">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"myoko">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"nagaoka">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"niigata">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"ojiya">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"omi">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"sado">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"sanjo">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"seiro">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"seirou">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"sekikawa">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"shibata">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"tagami">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"tainai">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"tochio">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"tokamachi">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"tsubame">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"tsunan">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"uonuma">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"yahiko">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"yoita">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"yuzawa">>,<<"niigata">>,<<"jp">>]) -> e(S); +m(S = [<<"beppu">>,<<"oita">>,<<"jp">>]) -> e(S); +m(S = [<<"bungoono">>,<<"oita">>,<<"jp">>]) -> e(S); +m(S = [<<"bungotakada">>,<<"oita">>,<<"jp">>]) -> e(S); +m(S = [<<"hasama">>,<<"oita">>,<<"jp">>]) -> e(S); +m(S = [<<"hiji">>,<<"oita">>,<<"jp">>]) -> e(S); +m(S = [<<"himeshima">>,<<"oita">>,<<"jp">>]) -> e(S); +m(S = [<<"hita">>,<<"oita">>,<<"jp">>]) -> e(S); +m(S = [<<"kamitsue">>,<<"oita">>,<<"jp">>]) -> e(S); +m(S = [<<"kokonoe">>,<<"oita">>,<<"jp">>]) -> e(S); +m(S = [<<"kuju">>,<<"oita">>,<<"jp">>]) -> e(S); +m(S = [<<"kunisaki">>,<<"oita">>,<<"jp">>]) -> e(S); +m(S = [<<"kusu">>,<<"oita">>,<<"jp">>]) -> e(S); +m(S = [<<"oita">>,<<"oita">>,<<"jp">>]) -> e(S); +m(S = [<<"saiki">>,<<"oita">>,<<"jp">>]) -> e(S); +m(S = [<<"taketa">>,<<"oita">>,<<"jp">>]) -> e(S); +m(S = [<<"tsukumi">>,<<"oita">>,<<"jp">>]) -> e(S); +m(S = [<<"usa">>,<<"oita">>,<<"jp">>]) -> e(S); +m(S = [<<"usuki">>,<<"oita">>,<<"jp">>]) -> e(S); +m(S = [<<"yufu">>,<<"oita">>,<<"jp">>]) -> e(S); +m(S = [<<"akaiwa">>,<<"okayama">>,<<"jp">>]) -> e(S); +m(S = [<<"asakuchi">>,<<"okayama">>,<<"jp">>]) -> e(S); +m(S = [<<"bizen">>,<<"okayama">>,<<"jp">>]) -> e(S); +m(S = [<<"hayashima">>,<<"okayama">>,<<"jp">>]) -> e(S); +m(S = [<<"ibara">>,<<"okayama">>,<<"jp">>]) -> e(S); +m(S = [<<"kagamino">>,<<"okayama">>,<<"jp">>]) -> e(S); +m(S = [<<"kasaoka">>,<<"okayama">>,<<"jp">>]) -> e(S); +m(S = [<<"kibichuo">>,<<"okayama">>,<<"jp">>]) -> e(S); +m(S = [<<"kumenan">>,<<"okayama">>,<<"jp">>]) -> e(S); +m(S = [<<"kurashiki">>,<<"okayama">>,<<"jp">>]) -> e(S); +m(S = [<<"maniwa">>,<<"okayama">>,<<"jp">>]) -> e(S); +m(S = [<<"misaki">>,<<"okayama">>,<<"jp">>]) -> e(S); +m(S = [<<"nagi">>,<<"okayama">>,<<"jp">>]) -> e(S); +m(S = [<<"niimi">>,<<"okayama">>,<<"jp">>]) -> e(S); +m(S = [<<"nishiawakura">>,<<"okayama">>,<<"jp">>]) -> e(S); +m(S = [<<"okayama">>,<<"okayama">>,<<"jp">>]) -> e(S); +m(S = [<<"satosho">>,<<"okayama">>,<<"jp">>]) -> e(S); +m(S = [<<"setouchi">>,<<"okayama">>,<<"jp">>]) -> e(S); +m(S = [<<"shinjo">>,<<"okayama">>,<<"jp">>]) -> e(S); +m(S = [<<"shoo">>,<<"okayama">>,<<"jp">>]) -> e(S); +m(S = [<<"soja">>,<<"okayama">>,<<"jp">>]) -> e(S); +m(S = [<<"takahashi">>,<<"okayama">>,<<"jp">>]) -> e(S); +m(S = [<<"tamano">>,<<"okayama">>,<<"jp">>]) -> e(S); +m(S = [<<"tsuyama">>,<<"okayama">>,<<"jp">>]) -> e(S); +m(S = [<<"wake">>,<<"okayama">>,<<"jp">>]) -> e(S); +m(S = [<<"yakage">>,<<"okayama">>,<<"jp">>]) -> e(S); +m(S = [<<"aguni">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"ginowan">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"ginoza">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"gushikami">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"haebaru">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"higashi">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"hirara">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"iheya">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"ishigaki">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"ishikawa">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"itoman">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"izena">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"kadena">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"kin">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"kitadaito">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"kitanakagusuku">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"kumejima">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"kunigami">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"minamidaito">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"motobu">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"nago">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"naha">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"nakagusuku">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"nakijin">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"nanjo">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"nishihara">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"ogimi">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"okinawa">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"onna">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"shimoji">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"taketomi">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"tarama">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"tokashiki">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"tomigusuku">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"tonaki">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"urasoe">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"uruma">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"yaese">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"yomitan">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"yonabaru">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"yonaguni">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"zamami">>,<<"okinawa">>,<<"jp">>]) -> e(S); +m(S = [<<"abeno">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"chihayaakasaka">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"chuo">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"daito">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"fujiidera">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"habikino">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"hannan">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"higashiosaka">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"higashisumiyoshi">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"higashiyodogawa">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"hirakata">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"ibaraki">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"ikeda">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"izumi">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"izumiotsu">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"izumisano">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"kadoma">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"kaizuka">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"kanan">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"kashiwara">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"katano">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"kawachinagano">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"kishiwada">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"kita">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"kumatori">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"matsubara">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"minato">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"minoh">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"misaki">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"moriguchi">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"neyagawa">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"nishi">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"nose">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"osakasayama">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"sakai">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"sayama">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"sennan">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"settsu">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"shijonawate">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"shimamoto">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"suita">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"tadaoka">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"taishi">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"tajiri">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"takaishi">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"takatsuki">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"tondabayashi">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"toyonaka">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"toyono">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"yao">>,<<"osaka">>,<<"jp">>]) -> e(S); +m(S = [<<"ariake">>,<<"saga">>,<<"jp">>]) -> e(S); +m(S = [<<"arita">>,<<"saga">>,<<"jp">>]) -> e(S); +m(S = [<<"fukudomi">>,<<"saga">>,<<"jp">>]) -> e(S); +m(S = [<<"genkai">>,<<"saga">>,<<"jp">>]) -> e(S); +m(S = [<<"hamatama">>,<<"saga">>,<<"jp">>]) -> e(S); +m(S = [<<"hizen">>,<<"saga">>,<<"jp">>]) -> e(S); +m(S = [<<"imari">>,<<"saga">>,<<"jp">>]) -> e(S); +m(S = [<<"kamimine">>,<<"saga">>,<<"jp">>]) -> e(S); +m(S = [<<"kanzaki">>,<<"saga">>,<<"jp">>]) -> e(S); +m(S = [<<"karatsu">>,<<"saga">>,<<"jp">>]) -> e(S); +m(S = [<<"kashima">>,<<"saga">>,<<"jp">>]) -> e(S); +m(S = [<<"kitagata">>,<<"saga">>,<<"jp">>]) -> e(S); +m(S = [<<"kitahata">>,<<"saga">>,<<"jp">>]) -> e(S); +m(S = [<<"kiyama">>,<<"saga">>,<<"jp">>]) -> e(S); +m(S = [<<"kouhoku">>,<<"saga">>,<<"jp">>]) -> e(S); +m(S = [<<"kyuragi">>,<<"saga">>,<<"jp">>]) -> e(S); +m(S = [<<"nishiarita">>,<<"saga">>,<<"jp">>]) -> e(S); +m(S = [<<"ogi">>,<<"saga">>,<<"jp">>]) -> e(S); +m(S = [<<"omachi">>,<<"saga">>,<<"jp">>]) -> e(S); +m(S = [<<"ouchi">>,<<"saga">>,<<"jp">>]) -> e(S); +m(S = [<<"saga">>,<<"saga">>,<<"jp">>]) -> e(S); +m(S = [<<"shiroishi">>,<<"saga">>,<<"jp">>]) -> e(S); +m(S = [<<"taku">>,<<"saga">>,<<"jp">>]) -> e(S); +m(S = [<<"tara">>,<<"saga">>,<<"jp">>]) -> e(S); +m(S = [<<"tosu">>,<<"saga">>,<<"jp">>]) -> e(S); +m(S = [<<"yoshinogari">>,<<"saga">>,<<"jp">>]) -> e(S); +m(S = [<<"arakawa">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"asaka">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"chichibu">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"fujimi">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"fujimino">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"fukaya">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"hanno">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"hanyu">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"hasuda">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"hatogaya">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"hatoyama">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"hidaka">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"higashichichibu">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"higashimatsuyama">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"honjo">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"ina">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"iruma">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"iwatsuki">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"kamiizumi">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"kamikawa">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"kamisato">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"kasukabe">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"kawagoe">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"kawaguchi">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"kawajima">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"kazo">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"kitamoto">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"koshigaya">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"kounosu">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"kuki">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"kumagaya">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"matsubushi">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"minano">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"misato">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"miyashiro">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"miyoshi">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"moroyama">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"nagatoro">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"namegawa">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"niiza">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"ogano">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"ogawa">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"ogose">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"okegawa">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"omiya">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"otaki">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"ranzan">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"ryokami">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"saitama">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"sakado">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"satte">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"sayama">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"shiki">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"shiraoka">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"soka">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"sugito">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"toda">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"tokigawa">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"tokorozawa">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"tsurugashima">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"urawa">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"warabi">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"yashio">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"yokoze">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"yono">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"yorii">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"yoshida">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"yoshikawa">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"yoshimi">>,<<"saitama">>,<<"jp">>]) -> e(S); +m(S = [<<"aisho">>,<<"shiga">>,<<"jp">>]) -> e(S); +m(S = [<<"gamo">>,<<"shiga">>,<<"jp">>]) -> e(S); +m(S = [<<"higashiomi">>,<<"shiga">>,<<"jp">>]) -> e(S); +m(S = [<<"hikone">>,<<"shiga">>,<<"jp">>]) -> e(S); +m(S = [<<"koka">>,<<"shiga">>,<<"jp">>]) -> e(S); +m(S = [<<"konan">>,<<"shiga">>,<<"jp">>]) -> e(S); +m(S = [<<"kosei">>,<<"shiga">>,<<"jp">>]) -> e(S); +m(S = [<<"koto">>,<<"shiga">>,<<"jp">>]) -> e(S); +m(S = [<<"kusatsu">>,<<"shiga">>,<<"jp">>]) -> e(S); +m(S = [<<"maibara">>,<<"shiga">>,<<"jp">>]) -> e(S); +m(S = [<<"moriyama">>,<<"shiga">>,<<"jp">>]) -> e(S); +m(S = [<<"nagahama">>,<<"shiga">>,<<"jp">>]) -> e(S); +m(S = [<<"nishiazai">>,<<"shiga">>,<<"jp">>]) -> e(S); +m(S = [<<"notogawa">>,<<"shiga">>,<<"jp">>]) -> e(S); +m(S = [<<"omihachiman">>,<<"shiga">>,<<"jp">>]) -> e(S); +m(S = [<<"otsu">>,<<"shiga">>,<<"jp">>]) -> e(S); +m(S = [<<"ritto">>,<<"shiga">>,<<"jp">>]) -> e(S); +m(S = [<<"ryuoh">>,<<"shiga">>,<<"jp">>]) -> e(S); +m(S = [<<"takashima">>,<<"shiga">>,<<"jp">>]) -> e(S); +m(S = [<<"takatsuki">>,<<"shiga">>,<<"jp">>]) -> e(S); +m(S = [<<"torahime">>,<<"shiga">>,<<"jp">>]) -> e(S); +m(S = [<<"toyosato">>,<<"shiga">>,<<"jp">>]) -> e(S); +m(S = [<<"yasu">>,<<"shiga">>,<<"jp">>]) -> e(S); +m(S = [<<"akagi">>,<<"shimane">>,<<"jp">>]) -> e(S); +m(S = [<<"ama">>,<<"shimane">>,<<"jp">>]) -> e(S); +m(S = [<<"gotsu">>,<<"shimane">>,<<"jp">>]) -> e(S); +m(S = [<<"hamada">>,<<"shimane">>,<<"jp">>]) -> e(S); +m(S = [<<"higashiizumo">>,<<"shimane">>,<<"jp">>]) -> e(S); +m(S = [<<"hikawa">>,<<"shimane">>,<<"jp">>]) -> e(S); +m(S = [<<"hikimi">>,<<"shimane">>,<<"jp">>]) -> e(S); +m(S = [<<"izumo">>,<<"shimane">>,<<"jp">>]) -> e(S); +m(S = [<<"kakinoki">>,<<"shimane">>,<<"jp">>]) -> e(S); +m(S = [<<"masuda">>,<<"shimane">>,<<"jp">>]) -> e(S); +m(S = [<<"matsue">>,<<"shimane">>,<<"jp">>]) -> e(S); +m(S = [<<"misato">>,<<"shimane">>,<<"jp">>]) -> e(S); +m(S = [<<"nishinoshima">>,<<"shimane">>,<<"jp">>]) -> e(S); +m(S = [<<"ohda">>,<<"shimane">>,<<"jp">>]) -> e(S); +m(S = [<<"okinoshima">>,<<"shimane">>,<<"jp">>]) -> e(S); +m(S = [<<"okuizumo">>,<<"shimane">>,<<"jp">>]) -> e(S); +m(S = [<<"shimane">>,<<"shimane">>,<<"jp">>]) -> e(S); +m(S = [<<"tamayu">>,<<"shimane">>,<<"jp">>]) -> e(S); +m(S = [<<"tsuwano">>,<<"shimane">>,<<"jp">>]) -> e(S); +m(S = [<<"unnan">>,<<"shimane">>,<<"jp">>]) -> e(S); +m(S = [<<"yakumo">>,<<"shimane">>,<<"jp">>]) -> e(S); +m(S = [<<"yasugi">>,<<"shimane">>,<<"jp">>]) -> e(S); +m(S = [<<"yatsuka">>,<<"shimane">>,<<"jp">>]) -> e(S); +m(S = [<<"arai">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"atami">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"fuji">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"fujieda">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"fujikawa">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"fujinomiya">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"fukuroi">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"gotemba">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"haibara">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"hamamatsu">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"higashiizu">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"ito">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"iwata">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"izu">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"izunokuni">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"kakegawa">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"kannami">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"kawanehon">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"kawazu">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"kikugawa">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"kosai">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"makinohara">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"matsuzaki">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"minamiizu">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"mishima">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"morimachi">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"nishiizu">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"numazu">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"omaezaki">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"shimada">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"shimizu">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"shimoda">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"shizuoka">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"susono">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"yaizu">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"yoshida">>,<<"shizuoka">>,<<"jp">>]) -> e(S); +m(S = [<<"ashikaga">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"bato">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"haga">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"ichikai">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"iwafune">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"kaminokawa">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"kanuma">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"karasuyama">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"kuroiso">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"mashiko">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"mibu">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"moka">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"motegi">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"nasu">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"nasushiobara">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"nikko">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"nishikata">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"nogi">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"ohira">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"ohtawara">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"oyama">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"sakura">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"sano">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"shimotsuke">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"shioya">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"takanezawa">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"tochigi">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"tsuga">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"ujiie">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"utsunomiya">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"yaita">>,<<"tochigi">>,<<"jp">>]) -> e(S); +m(S = [<<"aizumi">>,<<"tokushima">>,<<"jp">>]) -> e(S); +m(S = [<<"anan">>,<<"tokushima">>,<<"jp">>]) -> e(S); +m(S = [<<"ichiba">>,<<"tokushima">>,<<"jp">>]) -> e(S); +m(S = [<<"itano">>,<<"tokushima">>,<<"jp">>]) -> e(S); +m(S = [<<"kainan">>,<<"tokushima">>,<<"jp">>]) -> e(S); +m(S = [<<"komatsushima">>,<<"tokushima">>,<<"jp">>]) -> e(S); +m(S = [<<"matsushige">>,<<"tokushima">>,<<"jp">>]) -> e(S); +m(S = [<<"mima">>,<<"tokushima">>,<<"jp">>]) -> e(S); +m(S = [<<"minami">>,<<"tokushima">>,<<"jp">>]) -> e(S); +m(S = [<<"miyoshi">>,<<"tokushima">>,<<"jp">>]) -> e(S); +m(S = [<<"mugi">>,<<"tokushima">>,<<"jp">>]) -> e(S); +m(S = [<<"nakagawa">>,<<"tokushima">>,<<"jp">>]) -> e(S); +m(S = [<<"naruto">>,<<"tokushima">>,<<"jp">>]) -> e(S); +m(S = [<<"sanagochi">>,<<"tokushima">>,<<"jp">>]) -> e(S); +m(S = [<<"shishikui">>,<<"tokushima">>,<<"jp">>]) -> e(S); +m(S = [<<"tokushima">>,<<"tokushima">>,<<"jp">>]) -> e(S); +m(S = [<<"wajiki">>,<<"tokushima">>,<<"jp">>]) -> e(S); +m(S = [<<"adachi">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"akiruno">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"akishima">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"aogashima">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"arakawa">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"bunkyo">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"chiyoda">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"chofu">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"chuo">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"edogawa">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"fuchu">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"fussa">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"hachijo">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"hachioji">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"hamura">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"higashikurume">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"higashimurayama">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"higashiyamato">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"hino">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"hinode">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"hinohara">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"inagi">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"itabashi">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"katsushika">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"kita">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"kiyose">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"kodaira">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"koganei">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"kokubunji">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"komae">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"koto">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"kouzushima">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"kunitachi">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"machida">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"meguro">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"minato">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"mitaka">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"mizuho">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"musashimurayama">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"musashino">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"nakano">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"nerima">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"ogasawara">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"okutama">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"ome">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"oshima">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"ota">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"setagaya">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"shibuya">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"shinagawa">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"shinjuku">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"suginami">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"sumida">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"tachikawa">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"taito">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"tama">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"toshima">>,<<"tokyo">>,<<"jp">>]) -> e(S); +m(S = [<<"chizu">>,<<"tottori">>,<<"jp">>]) -> e(S); +m(S = [<<"hino">>,<<"tottori">>,<<"jp">>]) -> e(S); +m(S = [<<"kawahara">>,<<"tottori">>,<<"jp">>]) -> e(S); +m(S = [<<"koge">>,<<"tottori">>,<<"jp">>]) -> e(S); +m(S = [<<"kotoura">>,<<"tottori">>,<<"jp">>]) -> e(S); +m(S = [<<"misasa">>,<<"tottori">>,<<"jp">>]) -> e(S); +m(S = [<<"nanbu">>,<<"tottori">>,<<"jp">>]) -> e(S); +m(S = [<<"nichinan">>,<<"tottori">>,<<"jp">>]) -> e(S); +m(S = [<<"sakaiminato">>,<<"tottori">>,<<"jp">>]) -> e(S); +m(S = [<<"tottori">>,<<"tottori">>,<<"jp">>]) -> e(S); +m(S = [<<"wakasa">>,<<"tottori">>,<<"jp">>]) -> e(S); +m(S = [<<"yazu">>,<<"tottori">>,<<"jp">>]) -> e(S); +m(S = [<<"yonago">>,<<"tottori">>,<<"jp">>]) -> e(S); +m(S = [<<"asahi">>,<<"toyama">>,<<"jp">>]) -> e(S); +m(S = [<<"fuchu">>,<<"toyama">>,<<"jp">>]) -> e(S); +m(S = [<<"fukumitsu">>,<<"toyama">>,<<"jp">>]) -> e(S); +m(S = [<<"funahashi">>,<<"toyama">>,<<"jp">>]) -> e(S); +m(S = [<<"himi">>,<<"toyama">>,<<"jp">>]) -> e(S); +m(S = [<<"imizu">>,<<"toyama">>,<<"jp">>]) -> e(S); +m(S = [<<"inami">>,<<"toyama">>,<<"jp">>]) -> e(S); +m(S = [<<"johana">>,<<"toyama">>,<<"jp">>]) -> e(S); +m(S = [<<"kamiichi">>,<<"toyama">>,<<"jp">>]) -> e(S); +m(S = [<<"kurobe">>,<<"toyama">>,<<"jp">>]) -> e(S); +m(S = [<<"nakaniikawa">>,<<"toyama">>,<<"jp">>]) -> e(S); +m(S = [<<"namerikawa">>,<<"toyama">>,<<"jp">>]) -> e(S); +m(S = [<<"nanto">>,<<"toyama">>,<<"jp">>]) -> e(S); +m(S = [<<"nyuzen">>,<<"toyama">>,<<"jp">>]) -> e(S); +m(S = [<<"oyabe">>,<<"toyama">>,<<"jp">>]) -> e(S); +m(S = [<<"taira">>,<<"toyama">>,<<"jp">>]) -> e(S); +m(S = [<<"takaoka">>,<<"toyama">>,<<"jp">>]) -> e(S); +m(S = [<<"tateyama">>,<<"toyama">>,<<"jp">>]) -> e(S); +m(S = [<<"toga">>,<<"toyama">>,<<"jp">>]) -> e(S); +m(S = [<<"tonami">>,<<"toyama">>,<<"jp">>]) -> e(S); +m(S = [<<"toyama">>,<<"toyama">>,<<"jp">>]) -> e(S); +m(S = [<<"unazuki">>,<<"toyama">>,<<"jp">>]) -> e(S); +m(S = [<<"uozu">>,<<"toyama">>,<<"jp">>]) -> e(S); +m(S = [<<"yamada">>,<<"toyama">>,<<"jp">>]) -> e(S); +m(S = [<<"arida">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"aridagawa">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"gobo">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"hashimoto">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"hidaka">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"hirogawa">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"inami">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"iwade">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"kainan">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"kamitonda">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"katsuragi">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"kimino">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"kinokawa">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"kitayama">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"koya">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"koza">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"kozagawa">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"kudoyama">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"kushimoto">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"mihama">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"misato">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"nachikatsuura">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"shingu">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"shirahama">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"taiji">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"tanabe">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"wakayama">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"yuasa">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"yura">>,<<"wakayama">>,<<"jp">>]) -> e(S); +m(S = [<<"asahi">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"funagata">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"higashine">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"iide">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"kahoku">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"kaminoyama">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"kaneyama">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"kawanishi">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"mamurogawa">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"mikawa">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"murayama">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"nagai">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"nakayama">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"nanyo">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"nishikawa">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"obanazawa">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"oe">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"oguni">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"ohkura">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"oishida">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"sagae">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"sakata">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"sakegawa">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"shinjo">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"shirataka">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"shonai">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"takahata">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"tendo">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"tozawa">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"tsuruoka">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"yamagata">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"yamanobe">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"yonezawa">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"yuza">>,<<"yamagata">>,<<"jp">>]) -> e(S); +m(S = [<<"abu">>,<<"yamaguchi">>,<<"jp">>]) -> e(S); +m(S = [<<"hagi">>,<<"yamaguchi">>,<<"jp">>]) -> e(S); +m(S = [<<"hikari">>,<<"yamaguchi">>,<<"jp">>]) -> e(S); +m(S = [<<"hofu">>,<<"yamaguchi">>,<<"jp">>]) -> e(S); +m(S = [<<"iwakuni">>,<<"yamaguchi">>,<<"jp">>]) -> e(S); +m(S = [<<"kudamatsu">>,<<"yamaguchi">>,<<"jp">>]) -> e(S); +m(S = [<<"mitou">>,<<"yamaguchi">>,<<"jp">>]) -> e(S); +m(S = [<<"nagato">>,<<"yamaguchi">>,<<"jp">>]) -> e(S); +m(S = [<<"oshima">>,<<"yamaguchi">>,<<"jp">>]) -> e(S); +m(S = [<<"shimonoseki">>,<<"yamaguchi">>,<<"jp">>]) -> e(S); +m(S = [<<"shunan">>,<<"yamaguchi">>,<<"jp">>]) -> e(S); +m(S = [<<"tabuse">>,<<"yamaguchi">>,<<"jp">>]) -> e(S); +m(S = [<<"tokuyama">>,<<"yamaguchi">>,<<"jp">>]) -> e(S); +m(S = [<<"toyota">>,<<"yamaguchi">>,<<"jp">>]) -> e(S); +m(S = [<<"ube">>,<<"yamaguchi">>,<<"jp">>]) -> e(S); +m(S = [<<"yuu">>,<<"yamaguchi">>,<<"jp">>]) -> e(S); +m(S = [<<"chuo">>,<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"doshi">>,<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"fuefuki">>,<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"fujikawa">>,<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"fujikawaguchiko">>,<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"fujiyoshida">>,<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"hayakawa">>,<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"hokuto">>,<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"ichikawamisato">>,<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"kai">>,<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"kofu">>,<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"koshu">>,<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"kosuge">>,<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"minami-alps">>,<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"minobu">>,<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"nakamichi">>,<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"nanbu">>,<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"narusawa">>,<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"nirasaki">>,<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"nishikatsura">>,<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"oshino">>,<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"otsuki">>,<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"showa">>,<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"tabayama">>,<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"tsuru">>,<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"uenohara">>,<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"yamanakako">>,<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"yamanashi">>,<<"yamanashi">>,<<"jp">>]) -> e(S); +m(S = [<<"ke">>]) -> e(S); +m(S = [<<"ac">>,<<"ke">>]) -> e(S); +m(S = [<<"co">>,<<"ke">>]) -> e(S); +m(S = [<<"go">>,<<"ke">>]) -> e(S); +m(S = [<<"info">>,<<"ke">>]) -> e(S); +m(S = [<<"me">>,<<"ke">>]) -> e(S); +m(S = [<<"mobi">>,<<"ke">>]) -> e(S); +m(S = [<<"ne">>,<<"ke">>]) -> e(S); +m(S = [<<"or">>,<<"ke">>]) -> e(S); +m(S = [<<"sc">>,<<"ke">>]) -> e(S); +m(S = [<<"kg">>]) -> e(S); +m(S = [<<"org">>,<<"kg">>]) -> e(S); +m(S = [<<"net">>,<<"kg">>]) -> e(S); +m(S = [<<"com">>,<<"kg">>]) -> e(S); +m(S = [<<"edu">>,<<"kg">>]) -> e(S); +m(S = [<<"gov">>,<<"kg">>]) -> e(S); +m(S = [<<"mil">>,<<"kg">>]) -> e(S); +m(S = [_,<<"kh">>]) -> e(S); +m(S = [<<"ki">>]) -> e(S); +m(S = [<<"edu">>,<<"ki">>]) -> e(S); +m(S = [<<"biz">>,<<"ki">>]) -> e(S); +m(S = [<<"net">>,<<"ki">>]) -> e(S); +m(S = [<<"org">>,<<"ki">>]) -> e(S); +m(S = [<<"gov">>,<<"ki">>]) -> e(S); +m(S = [<<"info">>,<<"ki">>]) -> e(S); +m(S = [<<"com">>,<<"ki">>]) -> e(S); +m(S = [<<"km">>]) -> e(S); +m(S = [<<"org">>,<<"km">>]) -> e(S); +m(S = [<<"nom">>,<<"km">>]) -> e(S); +m(S = [<<"gov">>,<<"km">>]) -> e(S); +m(S = [<<"prd">>,<<"km">>]) -> e(S); +m(S = [<<"tm">>,<<"km">>]) -> e(S); +m(S = [<<"edu">>,<<"km">>]) -> e(S); +m(S = [<<"mil">>,<<"km">>]) -> e(S); +m(S = [<<"ass">>,<<"km">>]) -> e(S); +m(S = [<<"com">>,<<"km">>]) -> e(S); +m(S = [<<"coop">>,<<"km">>]) -> e(S); +m(S = [<<"asso">>,<<"km">>]) -> e(S); +m(S = [<<"presse">>,<<"km">>]) -> e(S); +m(S = [<<"medecin">>,<<"km">>]) -> e(S); +m(S = [<<"notaires">>,<<"km">>]) -> e(S); +m(S = [<<"pharmaciens">>,<<"km">>]) -> e(S); +m(S = [<<"veterinaire">>,<<"km">>]) -> e(S); +m(S = [<<"gouv">>,<<"km">>]) -> e(S); +m(S = [<<"kn">>]) -> e(S); +m(S = [<<"net">>,<<"kn">>]) -> e(S); +m(S = [<<"org">>,<<"kn">>]) -> e(S); +m(S = [<<"edu">>,<<"kn">>]) -> e(S); +m(S = [<<"gov">>,<<"kn">>]) -> e(S); +m(S = [<<"kp">>]) -> e(S); +m(S = [<<"com">>,<<"kp">>]) -> e(S); +m(S = [<<"edu">>,<<"kp">>]) -> e(S); +m(S = [<<"gov">>,<<"kp">>]) -> e(S); +m(S = [<<"org">>,<<"kp">>]) -> e(S); +m(S = [<<"rep">>,<<"kp">>]) -> e(S); +m(S = [<<"tra">>,<<"kp">>]) -> e(S); +m(S = [<<"kr">>]) -> e(S); +m(S = [<<"ac">>,<<"kr">>]) -> e(S); +m(S = [<<"co">>,<<"kr">>]) -> e(S); +m(S = [<<"es">>,<<"kr">>]) -> e(S); +m(S = [<<"go">>,<<"kr">>]) -> e(S); +m(S = [<<"hs">>,<<"kr">>]) -> e(S); +m(S = [<<"kg">>,<<"kr">>]) -> e(S); +m(S = [<<"mil">>,<<"kr">>]) -> e(S); +m(S = [<<"ms">>,<<"kr">>]) -> e(S); +m(S = [<<"ne">>,<<"kr">>]) -> e(S); +m(S = [<<"or">>,<<"kr">>]) -> e(S); +m(S = [<<"pe">>,<<"kr">>]) -> e(S); +m(S = [<<"re">>,<<"kr">>]) -> e(S); +m(S = [<<"sc">>,<<"kr">>]) -> e(S); +m(S = [<<"busan">>,<<"kr">>]) -> e(S); +m(S = [<<"chungbuk">>,<<"kr">>]) -> e(S); +m(S = [<<"chungnam">>,<<"kr">>]) -> e(S); +m(S = [<<"daegu">>,<<"kr">>]) -> e(S); +m(S = [<<"daejeon">>,<<"kr">>]) -> e(S); +m(S = [<<"gangwon">>,<<"kr">>]) -> e(S); +m(S = [<<"gwangju">>,<<"kr">>]) -> e(S); +m(S = [<<"gyeongbuk">>,<<"kr">>]) -> e(S); +m(S = [<<"gyeonggi">>,<<"kr">>]) -> e(S); +m(S = [<<"gyeongnam">>,<<"kr">>]) -> e(S); +m(S = [<<"incheon">>,<<"kr">>]) -> e(S); +m(S = [<<"jeju">>,<<"kr">>]) -> e(S); +m(S = [<<"jeonbuk">>,<<"kr">>]) -> e(S); +m(S = [<<"jeonnam">>,<<"kr">>]) -> e(S); +m(S = [<<"seoul">>,<<"kr">>]) -> e(S); +m(S = [<<"ulsan">>,<<"kr">>]) -> e(S); +m(S = [<<"kw">>]) -> e(S); +m(S = [<<"com">>,<<"kw">>]) -> e(S); +m(S = [<<"edu">>,<<"kw">>]) -> e(S); +m(S = [<<"emb">>,<<"kw">>]) -> e(S); +m(S = [<<"gov">>,<<"kw">>]) -> e(S); +m(S = [<<"ind">>,<<"kw">>]) -> e(S); +m(S = [<<"net">>,<<"kw">>]) -> e(S); +m(S = [<<"org">>,<<"kw">>]) -> e(S); +m(S = [<<"ky">>]) -> e(S); +m(S = [<<"com">>,<<"ky">>]) -> e(S); +m(S = [<<"edu">>,<<"ky">>]) -> e(S); +m(S = [<<"net">>,<<"ky">>]) -> e(S); +m(S = [<<"org">>,<<"ky">>]) -> e(S); +m(S = [<<"kz">>]) -> e(S); +m(S = [<<"org">>,<<"kz">>]) -> e(S); +m(S = [<<"edu">>,<<"kz">>]) -> e(S); +m(S = [<<"net">>,<<"kz">>]) -> e(S); +m(S = [<<"gov">>,<<"kz">>]) -> e(S); +m(S = [<<"mil">>,<<"kz">>]) -> e(S); +m(S = [<<"com">>,<<"kz">>]) -> e(S); +m(S = [<<"la">>]) -> e(S); +m(S = [<<"int">>,<<"la">>]) -> e(S); +m(S = [<<"net">>,<<"la">>]) -> e(S); +m(S = [<<"info">>,<<"la">>]) -> e(S); +m(S = [<<"edu">>,<<"la">>]) -> e(S); +m(S = [<<"gov">>,<<"la">>]) -> e(S); +m(S = [<<"per">>,<<"la">>]) -> e(S); +m(S = [<<"com">>,<<"la">>]) -> e(S); +m(S = [<<"org">>,<<"la">>]) -> e(S); +m(S = [<<"lb">>]) -> e(S); +m(S = [<<"com">>,<<"lb">>]) -> e(S); +m(S = [<<"edu">>,<<"lb">>]) -> e(S); +m(S = [<<"gov">>,<<"lb">>]) -> e(S); +m(S = [<<"net">>,<<"lb">>]) -> e(S); +m(S = [<<"org">>,<<"lb">>]) -> e(S); +m(S = [<<"lc">>]) -> e(S); +m(S = [<<"com">>,<<"lc">>]) -> e(S); +m(S = [<<"net">>,<<"lc">>]) -> e(S); +m(S = [<<"co">>,<<"lc">>]) -> e(S); +m(S = [<<"org">>,<<"lc">>]) -> e(S); +m(S = [<<"edu">>,<<"lc">>]) -> e(S); +m(S = [<<"gov">>,<<"lc">>]) -> e(S); +m(S = [<<"li">>]) -> e(S); +m(S = [<<"lk">>]) -> e(S); +m(S = [<<"gov">>,<<"lk">>]) -> e(S); +m(S = [<<"sch">>,<<"lk">>]) -> e(S); +m(S = [<<"net">>,<<"lk">>]) -> e(S); +m(S = [<<"int">>,<<"lk">>]) -> e(S); +m(S = [<<"com">>,<<"lk">>]) -> e(S); +m(S = [<<"org">>,<<"lk">>]) -> e(S); +m(S = [<<"edu">>,<<"lk">>]) -> e(S); +m(S = [<<"ngo">>,<<"lk">>]) -> e(S); +m(S = [<<"soc">>,<<"lk">>]) -> e(S); +m(S = [<<"web">>,<<"lk">>]) -> e(S); +m(S = [<<"ltd">>,<<"lk">>]) -> e(S); +m(S = [<<"assn">>,<<"lk">>]) -> e(S); +m(S = [<<"grp">>,<<"lk">>]) -> e(S); +m(S = [<<"hotel">>,<<"lk">>]) -> e(S); +m(S = [<<"ac">>,<<"lk">>]) -> e(S); +m(S = [<<"lr">>]) -> e(S); +m(S = [<<"com">>,<<"lr">>]) -> e(S); +m(S = [<<"edu">>,<<"lr">>]) -> e(S); +m(S = [<<"gov">>,<<"lr">>]) -> e(S); +m(S = [<<"org">>,<<"lr">>]) -> e(S); +m(S = [<<"net">>,<<"lr">>]) -> e(S); +m(S = [<<"ls">>]) -> e(S); +m(S = [<<"ac">>,<<"ls">>]) -> e(S); +m(S = [<<"biz">>,<<"ls">>]) -> e(S); +m(S = [<<"co">>,<<"ls">>]) -> e(S); +m(S = [<<"edu">>,<<"ls">>]) -> e(S); +m(S = [<<"gov">>,<<"ls">>]) -> e(S); +m(S = [<<"info">>,<<"ls">>]) -> e(S); +m(S = [<<"net">>,<<"ls">>]) -> e(S); +m(S = [<<"org">>,<<"ls">>]) -> e(S); +m(S = [<<"sc">>,<<"ls">>]) -> e(S); +m(S = [<<"lt">>]) -> e(S); +m(S = [<<"gov">>,<<"lt">>]) -> e(S); +m(S = [<<"lu">>]) -> e(S); +m(S = [<<"lv">>]) -> e(S); +m(S = [<<"com">>,<<"lv">>]) -> e(S); +m(S = [<<"edu">>,<<"lv">>]) -> e(S); +m(S = [<<"gov">>,<<"lv">>]) -> e(S); +m(S = [<<"org">>,<<"lv">>]) -> e(S); +m(S = [<<"mil">>,<<"lv">>]) -> e(S); +m(S = [<<"id">>,<<"lv">>]) -> e(S); +m(S = [<<"net">>,<<"lv">>]) -> e(S); +m(S = [<<"asn">>,<<"lv">>]) -> e(S); +m(S = [<<"conf">>,<<"lv">>]) -> e(S); +m(S = [<<"ly">>]) -> e(S); +m(S = [<<"com">>,<<"ly">>]) -> e(S); +m(S = [<<"net">>,<<"ly">>]) -> e(S); +m(S = [<<"gov">>,<<"ly">>]) -> e(S); +m(S = [<<"plc">>,<<"ly">>]) -> e(S); +m(S = [<<"edu">>,<<"ly">>]) -> e(S); +m(S = [<<"sch">>,<<"ly">>]) -> e(S); +m(S = [<<"med">>,<<"ly">>]) -> e(S); +m(S = [<<"org">>,<<"ly">>]) -> e(S); +m(S = [<<"id">>,<<"ly">>]) -> e(S); +m(S = [<<"ma">>]) -> e(S); +m(S = [<<"co">>,<<"ma">>]) -> e(S); +m(S = [<<"net">>,<<"ma">>]) -> e(S); +m(S = [<<"gov">>,<<"ma">>]) -> e(S); +m(S = [<<"org">>,<<"ma">>]) -> e(S); +m(S = [<<"ac">>,<<"ma">>]) -> e(S); +m(S = [<<"press">>,<<"ma">>]) -> e(S); +m(S = [<<"mc">>]) -> e(S); +m(S = [<<"tm">>,<<"mc">>]) -> e(S); +m(S = [<<"asso">>,<<"mc">>]) -> e(S); +m(S = [<<"md">>]) -> e(S); +m(S = [<<"me">>]) -> e(S); +m(S = [<<"co">>,<<"me">>]) -> e(S); +m(S = [<<"net">>,<<"me">>]) -> e(S); +m(S = [<<"org">>,<<"me">>]) -> e(S); +m(S = [<<"edu">>,<<"me">>]) -> e(S); +m(S = [<<"ac">>,<<"me">>]) -> e(S); +m(S = [<<"gov">>,<<"me">>]) -> e(S); +m(S = [<<"its">>,<<"me">>]) -> e(S); +m(S = [<<"priv">>,<<"me">>]) -> e(S); +m(S = [<<"mg">>]) -> e(S); +m(S = [<<"org">>,<<"mg">>]) -> e(S); +m(S = [<<"nom">>,<<"mg">>]) -> e(S); +m(S = [<<"gov">>,<<"mg">>]) -> e(S); +m(S = [<<"prd">>,<<"mg">>]) -> e(S); +m(S = [<<"tm">>,<<"mg">>]) -> e(S); +m(S = [<<"edu">>,<<"mg">>]) -> e(S); +m(S = [<<"mil">>,<<"mg">>]) -> e(S); +m(S = [<<"com">>,<<"mg">>]) -> e(S); +m(S = [<<"co">>,<<"mg">>]) -> e(S); +m(S = [<<"mh">>]) -> e(S); +m(S = [<<"mil">>]) -> e(S); +m(S = [<<"mk">>]) -> e(S); +m(S = [<<"com">>,<<"mk">>]) -> e(S); +m(S = [<<"org">>,<<"mk">>]) -> e(S); +m(S = [<<"net">>,<<"mk">>]) -> e(S); +m(S = [<<"edu">>,<<"mk">>]) -> e(S); +m(S = [<<"gov">>,<<"mk">>]) -> e(S); +m(S = [<<"inf">>,<<"mk">>]) -> e(S); +m(S = [<<"name">>,<<"mk">>]) -> e(S); +m(S = [<<"ml">>]) -> e(S); +m(S = [<<"com">>,<<"ml">>]) -> e(S); +m(S = [<<"edu">>,<<"ml">>]) -> e(S); +m(S = [<<"gouv">>,<<"ml">>]) -> e(S); +m(S = [<<"gov">>,<<"ml">>]) -> e(S); +m(S = [<<"net">>,<<"ml">>]) -> e(S); +m(S = [<<"org">>,<<"ml">>]) -> e(S); +m(S = [<<"presse">>,<<"ml">>]) -> e(S); +m(S = [_,<<"mm">>]) -> e(S); +m(S = [<<"mn">>]) -> e(S); +m(S = [<<"gov">>,<<"mn">>]) -> e(S); +m(S = [<<"edu">>,<<"mn">>]) -> e(S); +m(S = [<<"org">>,<<"mn">>]) -> e(S); +m(S = [<<"mo">>]) -> e(S); +m(S = [<<"com">>,<<"mo">>]) -> e(S); +m(S = [<<"net">>,<<"mo">>]) -> e(S); +m(S = [<<"org">>,<<"mo">>]) -> e(S); +m(S = [<<"edu">>,<<"mo">>]) -> e(S); +m(S = [<<"gov">>,<<"mo">>]) -> e(S); +m(S = [<<"mobi">>]) -> e(S); +m(S = [<<"mp">>]) -> e(S); +m(S = [<<"mq">>]) -> e(S); +m(S = [<<"mr">>]) -> e(S); +m(S = [<<"gov">>,<<"mr">>]) -> e(S); +m(S = [<<"ms">>]) -> e(S); +m(S = [<<"com">>,<<"ms">>]) -> e(S); +m(S = [<<"edu">>,<<"ms">>]) -> e(S); +m(S = [<<"gov">>,<<"ms">>]) -> e(S); +m(S = [<<"net">>,<<"ms">>]) -> e(S); +m(S = [<<"org">>,<<"ms">>]) -> e(S); +m(S = [<<"mt">>]) -> e(S); +m(S = [<<"com">>,<<"mt">>]) -> e(S); +m(S = [<<"edu">>,<<"mt">>]) -> e(S); +m(S = [<<"net">>,<<"mt">>]) -> e(S); +m(S = [<<"org">>,<<"mt">>]) -> e(S); +m(S = [<<"mu">>]) -> e(S); +m(S = [<<"com">>,<<"mu">>]) -> e(S); +m(S = [<<"net">>,<<"mu">>]) -> e(S); +m(S = [<<"org">>,<<"mu">>]) -> e(S); +m(S = [<<"gov">>,<<"mu">>]) -> e(S); +m(S = [<<"ac">>,<<"mu">>]) -> e(S); +m(S = [<<"co">>,<<"mu">>]) -> e(S); +m(S = [<<"or">>,<<"mu">>]) -> e(S); +m(S = [<<"museum">>]) -> e(S); +m(S = [<<"academy">>,<<"museum">>]) -> e(S); +m(S = [<<"agriculture">>,<<"museum">>]) -> e(S); +m(S = [<<"air">>,<<"museum">>]) -> e(S); +m(S = [<<"airguard">>,<<"museum">>]) -> e(S); +m(S = [<<"alabama">>,<<"museum">>]) -> e(S); +m(S = [<<"alaska">>,<<"museum">>]) -> e(S); +m(S = [<<"amber">>,<<"museum">>]) -> e(S); +m(S = [<<"ambulance">>,<<"museum">>]) -> e(S); +m(S = [<<"american">>,<<"museum">>]) -> e(S); +m(S = [<<"americana">>,<<"museum">>]) -> e(S); +m(S = [<<"americanantiques">>,<<"museum">>]) -> e(S); +m(S = [<<"americanart">>,<<"museum">>]) -> e(S); +m(S = [<<"amsterdam">>,<<"museum">>]) -> e(S); +m(S = [<<"and">>,<<"museum">>]) -> e(S); +m(S = [<<"annefrank">>,<<"museum">>]) -> e(S); +m(S = [<<"anthro">>,<<"museum">>]) -> e(S); +m(S = [<<"anthropology">>,<<"museum">>]) -> e(S); +m(S = [<<"antiques">>,<<"museum">>]) -> e(S); +m(S = [<<"aquarium">>,<<"museum">>]) -> e(S); +m(S = [<<"arboretum">>,<<"museum">>]) -> e(S); +m(S = [<<"archaeological">>,<<"museum">>]) -> e(S); +m(S = [<<"archaeology">>,<<"museum">>]) -> e(S); +m(S = [<<"architecture">>,<<"museum">>]) -> e(S); +m(S = [<<"art">>,<<"museum">>]) -> e(S); +m(S = [<<"artanddesign">>,<<"museum">>]) -> e(S); +m(S = [<<"artcenter">>,<<"museum">>]) -> e(S); +m(S = [<<"artdeco">>,<<"museum">>]) -> e(S); +m(S = [<<"arteducation">>,<<"museum">>]) -> e(S); +m(S = [<<"artgallery">>,<<"museum">>]) -> e(S); +m(S = [<<"arts">>,<<"museum">>]) -> e(S); +m(S = [<<"artsandcrafts">>,<<"museum">>]) -> e(S); +m(S = [<<"asmatart">>,<<"museum">>]) -> e(S); +m(S = [<<"assassination">>,<<"museum">>]) -> e(S); +m(S = [<<"assisi">>,<<"museum">>]) -> e(S); +m(S = [<<"association">>,<<"museum">>]) -> e(S); +m(S = [<<"astronomy">>,<<"museum">>]) -> e(S); +m(S = [<<"atlanta">>,<<"museum">>]) -> e(S); +m(S = [<<"austin">>,<<"museum">>]) -> e(S); +m(S = [<<"australia">>,<<"museum">>]) -> e(S); +m(S = [<<"automotive">>,<<"museum">>]) -> e(S); +m(S = [<<"aviation">>,<<"museum">>]) -> e(S); +m(S = [<<"axis">>,<<"museum">>]) -> e(S); +m(S = [<<"badajoz">>,<<"museum">>]) -> e(S); +m(S = [<<"baghdad">>,<<"museum">>]) -> e(S); +m(S = [<<"bahn">>,<<"museum">>]) -> e(S); +m(S = [<<"bale">>,<<"museum">>]) -> e(S); +m(S = [<<"baltimore">>,<<"museum">>]) -> e(S); +m(S = [<<"barcelona">>,<<"museum">>]) -> e(S); +m(S = [<<"baseball">>,<<"museum">>]) -> e(S); +m(S = [<<"basel">>,<<"museum">>]) -> e(S); +m(S = [<<"baths">>,<<"museum">>]) -> e(S); +m(S = [<<"bauern">>,<<"museum">>]) -> e(S); +m(S = [<<"beauxarts">>,<<"museum">>]) -> e(S); +m(S = [<<"beeldengeluid">>,<<"museum">>]) -> e(S); +m(S = [<<"bellevue">>,<<"museum">>]) -> e(S); +m(S = [<<"bergbau">>,<<"museum">>]) -> e(S); +m(S = [<<"berkeley">>,<<"museum">>]) -> e(S); +m(S = [<<"berlin">>,<<"museum">>]) -> e(S); +m(S = [<<"bern">>,<<"museum">>]) -> e(S); +m(S = [<<"bible">>,<<"museum">>]) -> e(S); +m(S = [<<"bilbao">>,<<"museum">>]) -> e(S); +m(S = [<<"bill">>,<<"museum">>]) -> e(S); +m(S = [<<"birdart">>,<<"museum">>]) -> e(S); +m(S = [<<"birthplace">>,<<"museum">>]) -> e(S); +m(S = [<<"bonn">>,<<"museum">>]) -> e(S); +m(S = [<<"boston">>,<<"museum">>]) -> e(S); +m(S = [<<"botanical">>,<<"museum">>]) -> e(S); +m(S = [<<"botanicalgarden">>,<<"museum">>]) -> e(S); +m(S = [<<"botanicgarden">>,<<"museum">>]) -> e(S); +m(S = [<<"botany">>,<<"museum">>]) -> e(S); +m(S = [<<"brandywinevalley">>,<<"museum">>]) -> e(S); +m(S = [<<"brasil">>,<<"museum">>]) -> e(S); +m(S = [<<"bristol">>,<<"museum">>]) -> e(S); +m(S = [<<"british">>,<<"museum">>]) -> e(S); +m(S = [<<"britishcolumbia">>,<<"museum">>]) -> e(S); +m(S = [<<"broadcast">>,<<"museum">>]) -> e(S); +m(S = [<<"brunel">>,<<"museum">>]) -> e(S); +m(S = [<<"brussel">>,<<"museum">>]) -> e(S); +m(S = [<<"brussels">>,<<"museum">>]) -> e(S); +m(S = [<<"bruxelles">>,<<"museum">>]) -> e(S); +m(S = [<<"building">>,<<"museum">>]) -> e(S); +m(S = [<<"burghof">>,<<"museum">>]) -> e(S); +m(S = [<<"bus">>,<<"museum">>]) -> e(S); +m(S = [<<"bushey">>,<<"museum">>]) -> e(S); +m(S = [<<"cadaques">>,<<"museum">>]) -> e(S); +m(S = [<<"california">>,<<"museum">>]) -> e(S); +m(S = [<<"cambridge">>,<<"museum">>]) -> e(S); +m(S = [<<"can">>,<<"museum">>]) -> e(S); +m(S = [<<"canada">>,<<"museum">>]) -> e(S); +m(S = [<<"capebreton">>,<<"museum">>]) -> e(S); +m(S = [<<"carrier">>,<<"museum">>]) -> e(S); +m(S = [<<"cartoonart">>,<<"museum">>]) -> e(S); +m(S = [<<"casadelamoneda">>,<<"museum">>]) -> e(S); +m(S = [<<"castle">>,<<"museum">>]) -> e(S); +m(S = [<<"castres">>,<<"museum">>]) -> e(S); +m(S = [<<"celtic">>,<<"museum">>]) -> e(S); +m(S = [<<"center">>,<<"museum">>]) -> e(S); +m(S = [<<"chattanooga">>,<<"museum">>]) -> e(S); +m(S = [<<"cheltenham">>,<<"museum">>]) -> e(S); +m(S = [<<"chesapeakebay">>,<<"museum">>]) -> e(S); +m(S = [<<"chicago">>,<<"museum">>]) -> e(S); +m(S = [<<"children">>,<<"museum">>]) -> e(S); +m(S = [<<"childrens">>,<<"museum">>]) -> e(S); +m(S = [<<"childrensgarden">>,<<"museum">>]) -> e(S); +m(S = [<<"chiropractic">>,<<"museum">>]) -> e(S); +m(S = [<<"chocolate">>,<<"museum">>]) -> e(S); +m(S = [<<"christiansburg">>,<<"museum">>]) -> e(S); +m(S = [<<"cincinnati">>,<<"museum">>]) -> e(S); +m(S = [<<"cinema">>,<<"museum">>]) -> e(S); +m(S = [<<"circus">>,<<"museum">>]) -> e(S); +m(S = [<<"civilisation">>,<<"museum">>]) -> e(S); +m(S = [<<"civilization">>,<<"museum">>]) -> e(S); +m(S = [<<"civilwar">>,<<"museum">>]) -> e(S); +m(S = [<<"clinton">>,<<"museum">>]) -> e(S); +m(S = [<<"clock">>,<<"museum">>]) -> e(S); +m(S = [<<"coal">>,<<"museum">>]) -> e(S); +m(S = [<<"coastaldefence">>,<<"museum">>]) -> e(S); +m(S = [<<"cody">>,<<"museum">>]) -> e(S); +m(S = [<<"coldwar">>,<<"museum">>]) -> e(S); +m(S = [<<"collection">>,<<"museum">>]) -> e(S); +m(S = [<<"colonialwilliamsburg">>,<<"museum">>]) -> e(S); +m(S = [<<"coloradoplateau">>,<<"museum">>]) -> e(S); +m(S = [<<"columbia">>,<<"museum">>]) -> e(S); +m(S = [<<"columbus">>,<<"museum">>]) -> e(S); +m(S = [<<"communication">>,<<"museum">>]) -> e(S); +m(S = [<<"communications">>,<<"museum">>]) -> e(S); +m(S = [<<"community">>,<<"museum">>]) -> e(S); +m(S = [<<"computer">>,<<"museum">>]) -> e(S); +m(S = [<<"computerhistory">>,<<"museum">>]) -> e(S); +m(S = [<<"xn--comunicaes-v6a2o">>,<<"museum">>]) -> e(S); +m(S = [<<"contemporary">>,<<"museum">>]) -> e(S); +m(S = [<<"contemporaryart">>,<<"museum">>]) -> e(S); +m(S = [<<"convent">>,<<"museum">>]) -> e(S); +m(S = [<<"copenhagen">>,<<"museum">>]) -> e(S); +m(S = [<<"corporation">>,<<"museum">>]) -> e(S); +m(S = [<<"xn--correios-e-telecomunicaes-ghc29a">>,<<"museum">>]) -> e(S); +m(S = [<<"corvette">>,<<"museum">>]) -> e(S); +m(S = [<<"costume">>,<<"museum">>]) -> e(S); +m(S = [<<"countryestate">>,<<"museum">>]) -> e(S); +m(S = [<<"county">>,<<"museum">>]) -> e(S); +m(S = [<<"crafts">>,<<"museum">>]) -> e(S); +m(S = [<<"cranbrook">>,<<"museum">>]) -> e(S); +m(S = [<<"creation">>,<<"museum">>]) -> e(S); +m(S = [<<"cultural">>,<<"museum">>]) -> e(S); +m(S = [<<"culturalcenter">>,<<"museum">>]) -> e(S); +m(S = [<<"culture">>,<<"museum">>]) -> e(S); +m(S = [<<"cyber">>,<<"museum">>]) -> e(S); +m(S = [<<"cymru">>,<<"museum">>]) -> e(S); +m(S = [<<"dali">>,<<"museum">>]) -> e(S); +m(S = [<<"dallas">>,<<"museum">>]) -> e(S); +m(S = [<<"database">>,<<"museum">>]) -> e(S); +m(S = [<<"ddr">>,<<"museum">>]) -> e(S); +m(S = [<<"decorativearts">>,<<"museum">>]) -> e(S); +m(S = [<<"delaware">>,<<"museum">>]) -> e(S); +m(S = [<<"delmenhorst">>,<<"museum">>]) -> e(S); +m(S = [<<"denmark">>,<<"museum">>]) -> e(S); +m(S = [<<"depot">>,<<"museum">>]) -> e(S); +m(S = [<<"design">>,<<"museum">>]) -> e(S); +m(S = [<<"detroit">>,<<"museum">>]) -> e(S); +m(S = [<<"dinosaur">>,<<"museum">>]) -> e(S); +m(S = [<<"discovery">>,<<"museum">>]) -> e(S); +m(S = [<<"dolls">>,<<"museum">>]) -> e(S); +m(S = [<<"donostia">>,<<"museum">>]) -> e(S); +m(S = [<<"durham">>,<<"museum">>]) -> e(S); +m(S = [<<"eastafrica">>,<<"museum">>]) -> e(S); +m(S = [<<"eastcoast">>,<<"museum">>]) -> e(S); +m(S = [<<"education">>,<<"museum">>]) -> e(S); +m(S = [<<"educational">>,<<"museum">>]) -> e(S); +m(S = [<<"egyptian">>,<<"museum">>]) -> e(S); +m(S = [<<"eisenbahn">>,<<"museum">>]) -> e(S); +m(S = [<<"elburg">>,<<"museum">>]) -> e(S); +m(S = [<<"elvendrell">>,<<"museum">>]) -> e(S); +m(S = [<<"embroidery">>,<<"museum">>]) -> e(S); +m(S = [<<"encyclopedic">>,<<"museum">>]) -> e(S); +m(S = [<<"england">>,<<"museum">>]) -> e(S); +m(S = [<<"entomology">>,<<"museum">>]) -> e(S); +m(S = [<<"environment">>,<<"museum">>]) -> e(S); +m(S = [<<"environmentalconservation">>,<<"museum">>]) -> e(S); +m(S = [<<"epilepsy">>,<<"museum">>]) -> e(S); +m(S = [<<"essex">>,<<"museum">>]) -> e(S); +m(S = [<<"estate">>,<<"museum">>]) -> e(S); +m(S = [<<"ethnology">>,<<"museum">>]) -> e(S); +m(S = [<<"exeter">>,<<"museum">>]) -> e(S); +m(S = [<<"exhibition">>,<<"museum">>]) -> e(S); +m(S = [<<"family">>,<<"museum">>]) -> e(S); +m(S = [<<"farm">>,<<"museum">>]) -> e(S); +m(S = [<<"farmequipment">>,<<"museum">>]) -> e(S); +m(S = [<<"farmers">>,<<"museum">>]) -> e(S); +m(S = [<<"farmstead">>,<<"museum">>]) -> e(S); +m(S = [<<"field">>,<<"museum">>]) -> e(S); +m(S = [<<"figueres">>,<<"museum">>]) -> e(S); +m(S = [<<"filatelia">>,<<"museum">>]) -> e(S); +m(S = [<<"film">>,<<"museum">>]) -> e(S); +m(S = [<<"fineart">>,<<"museum">>]) -> e(S); +m(S = [<<"finearts">>,<<"museum">>]) -> e(S); +m(S = [<<"finland">>,<<"museum">>]) -> e(S); +m(S = [<<"flanders">>,<<"museum">>]) -> e(S); +m(S = [<<"florida">>,<<"museum">>]) -> e(S); +m(S = [<<"force">>,<<"museum">>]) -> e(S); +m(S = [<<"fortmissoula">>,<<"museum">>]) -> e(S); +m(S = [<<"fortworth">>,<<"museum">>]) -> e(S); +m(S = [<<"foundation">>,<<"museum">>]) -> e(S); +m(S = [<<"francaise">>,<<"museum">>]) -> e(S); +m(S = [<<"frankfurt">>,<<"museum">>]) -> e(S); +m(S = [<<"franziskaner">>,<<"museum">>]) -> e(S); +m(S = [<<"freemasonry">>,<<"museum">>]) -> e(S); +m(S = [<<"freiburg">>,<<"museum">>]) -> e(S); +m(S = [<<"fribourg">>,<<"museum">>]) -> e(S); +m(S = [<<"frog">>,<<"museum">>]) -> e(S); +m(S = [<<"fundacio">>,<<"museum">>]) -> e(S); +m(S = [<<"furniture">>,<<"museum">>]) -> e(S); +m(S = [<<"gallery">>,<<"museum">>]) -> e(S); +m(S = [<<"garden">>,<<"museum">>]) -> e(S); +m(S = [<<"gateway">>,<<"museum">>]) -> e(S); +m(S = [<<"geelvinck">>,<<"museum">>]) -> e(S); +m(S = [<<"gemological">>,<<"museum">>]) -> e(S); +m(S = [<<"geology">>,<<"museum">>]) -> e(S); +m(S = [<<"georgia">>,<<"museum">>]) -> e(S); +m(S = [<<"giessen">>,<<"museum">>]) -> e(S); +m(S = [<<"glas">>,<<"museum">>]) -> e(S); +m(S = [<<"glass">>,<<"museum">>]) -> e(S); +m(S = [<<"gorge">>,<<"museum">>]) -> e(S); +m(S = [<<"grandrapids">>,<<"museum">>]) -> e(S); +m(S = [<<"graz">>,<<"museum">>]) -> e(S); +m(S = [<<"guernsey">>,<<"museum">>]) -> e(S); +m(S = [<<"halloffame">>,<<"museum">>]) -> e(S); +m(S = [<<"hamburg">>,<<"museum">>]) -> e(S); +m(S = [<<"handson">>,<<"museum">>]) -> e(S); +m(S = [<<"harvestcelebration">>,<<"museum">>]) -> e(S); +m(S = [<<"hawaii">>,<<"museum">>]) -> e(S); +m(S = [<<"health">>,<<"museum">>]) -> e(S); +m(S = [<<"heimatunduhren">>,<<"museum">>]) -> e(S); +m(S = [<<"hellas">>,<<"museum">>]) -> e(S); +m(S = [<<"helsinki">>,<<"museum">>]) -> e(S); +m(S = [<<"hembygdsforbund">>,<<"museum">>]) -> e(S); +m(S = [<<"heritage">>,<<"museum">>]) -> e(S); +m(S = [<<"histoire">>,<<"museum">>]) -> e(S); +m(S = [<<"historical">>,<<"museum">>]) -> e(S); +m(S = [<<"historicalsociety">>,<<"museum">>]) -> e(S); +m(S = [<<"historichouses">>,<<"museum">>]) -> e(S); +m(S = [<<"historisch">>,<<"museum">>]) -> e(S); +m(S = [<<"historisches">>,<<"museum">>]) -> e(S); +m(S = [<<"history">>,<<"museum">>]) -> e(S); +m(S = [<<"historyofscience">>,<<"museum">>]) -> e(S); +m(S = [<<"horology">>,<<"museum">>]) -> e(S); +m(S = [<<"house">>,<<"museum">>]) -> e(S); +m(S = [<<"humanities">>,<<"museum">>]) -> e(S); +m(S = [<<"illustration">>,<<"museum">>]) -> e(S); +m(S = [<<"imageandsound">>,<<"museum">>]) -> e(S); +m(S = [<<"indian">>,<<"museum">>]) -> e(S); +m(S = [<<"indiana">>,<<"museum">>]) -> e(S); +m(S = [<<"indianapolis">>,<<"museum">>]) -> e(S); +m(S = [<<"indianmarket">>,<<"museum">>]) -> e(S); +m(S = [<<"intelligence">>,<<"museum">>]) -> e(S); +m(S = [<<"interactive">>,<<"museum">>]) -> e(S); +m(S = [<<"iraq">>,<<"museum">>]) -> e(S); +m(S = [<<"iron">>,<<"museum">>]) -> e(S); +m(S = [<<"isleofman">>,<<"museum">>]) -> e(S); +m(S = [<<"jamison">>,<<"museum">>]) -> e(S); +m(S = [<<"jefferson">>,<<"museum">>]) -> e(S); +m(S = [<<"jerusalem">>,<<"museum">>]) -> e(S); +m(S = [<<"jewelry">>,<<"museum">>]) -> e(S); +m(S = [<<"jewish">>,<<"museum">>]) -> e(S); +m(S = [<<"jewishart">>,<<"museum">>]) -> e(S); +m(S = [<<"jfk">>,<<"museum">>]) -> e(S); +m(S = [<<"journalism">>,<<"museum">>]) -> e(S); +m(S = [<<"judaica">>,<<"museum">>]) -> e(S); +m(S = [<<"judygarland">>,<<"museum">>]) -> e(S); +m(S = [<<"juedisches">>,<<"museum">>]) -> e(S); +m(S = [<<"juif">>,<<"museum">>]) -> e(S); +m(S = [<<"karate">>,<<"museum">>]) -> e(S); +m(S = [<<"karikatur">>,<<"museum">>]) -> e(S); +m(S = [<<"kids">>,<<"museum">>]) -> e(S); +m(S = [<<"koebenhavn">>,<<"museum">>]) -> e(S); +m(S = [<<"koeln">>,<<"museum">>]) -> e(S); +m(S = [<<"kunst">>,<<"museum">>]) -> e(S); +m(S = [<<"kunstsammlung">>,<<"museum">>]) -> e(S); +m(S = [<<"kunstunddesign">>,<<"museum">>]) -> e(S); +m(S = [<<"labor">>,<<"museum">>]) -> e(S); +m(S = [<<"labour">>,<<"museum">>]) -> e(S); +m(S = [<<"lajolla">>,<<"museum">>]) -> e(S); +m(S = [<<"lancashire">>,<<"museum">>]) -> e(S); +m(S = [<<"landes">>,<<"museum">>]) -> e(S); +m(S = [<<"lans">>,<<"museum">>]) -> e(S); +m(S = [<<"xn--lns-qla">>,<<"museum">>]) -> e(S); +m(S = [<<"larsson">>,<<"museum">>]) -> e(S); +m(S = [<<"lewismiller">>,<<"museum">>]) -> e(S); +m(S = [<<"lincoln">>,<<"museum">>]) -> e(S); +m(S = [<<"linz">>,<<"museum">>]) -> e(S); +m(S = [<<"living">>,<<"museum">>]) -> e(S); +m(S = [<<"livinghistory">>,<<"museum">>]) -> e(S); +m(S = [<<"localhistory">>,<<"museum">>]) -> e(S); +m(S = [<<"london">>,<<"museum">>]) -> e(S); +m(S = [<<"losangeles">>,<<"museum">>]) -> e(S); +m(S = [<<"louvre">>,<<"museum">>]) -> e(S); +m(S = [<<"loyalist">>,<<"museum">>]) -> e(S); +m(S = [<<"lucerne">>,<<"museum">>]) -> e(S); +m(S = [<<"luxembourg">>,<<"museum">>]) -> e(S); +m(S = [<<"luzern">>,<<"museum">>]) -> e(S); +m(S = [<<"mad">>,<<"museum">>]) -> e(S); +m(S = [<<"madrid">>,<<"museum">>]) -> e(S); +m(S = [<<"mallorca">>,<<"museum">>]) -> e(S); +m(S = [<<"manchester">>,<<"museum">>]) -> e(S); +m(S = [<<"mansion">>,<<"museum">>]) -> e(S); +m(S = [<<"mansions">>,<<"museum">>]) -> e(S); +m(S = [<<"manx">>,<<"museum">>]) -> e(S); +m(S = [<<"marburg">>,<<"museum">>]) -> e(S); +m(S = [<<"maritime">>,<<"museum">>]) -> e(S); +m(S = [<<"maritimo">>,<<"museum">>]) -> e(S); +m(S = [<<"maryland">>,<<"museum">>]) -> e(S); +m(S = [<<"marylhurst">>,<<"museum">>]) -> e(S); +m(S = [<<"media">>,<<"museum">>]) -> e(S); +m(S = [<<"medical">>,<<"museum">>]) -> e(S); +m(S = [<<"medizinhistorisches">>,<<"museum">>]) -> e(S); +m(S = [<<"meeres">>,<<"museum">>]) -> e(S); +m(S = [<<"memorial">>,<<"museum">>]) -> e(S); +m(S = [<<"mesaverde">>,<<"museum">>]) -> e(S); +m(S = [<<"michigan">>,<<"museum">>]) -> e(S); +m(S = [<<"midatlantic">>,<<"museum">>]) -> e(S); +m(S = [<<"military">>,<<"museum">>]) -> e(S); +m(S = [<<"mill">>,<<"museum">>]) -> e(S); +m(S = [<<"miners">>,<<"museum">>]) -> e(S); +m(S = [<<"mining">>,<<"museum">>]) -> e(S); +m(S = [<<"minnesota">>,<<"museum">>]) -> e(S); +m(S = [<<"missile">>,<<"museum">>]) -> e(S); +m(S = [<<"missoula">>,<<"museum">>]) -> e(S); +m(S = [<<"modern">>,<<"museum">>]) -> e(S); +m(S = [<<"moma">>,<<"museum">>]) -> e(S); +m(S = [<<"money">>,<<"museum">>]) -> e(S); +m(S = [<<"monmouth">>,<<"museum">>]) -> e(S); +m(S = [<<"monticello">>,<<"museum">>]) -> e(S); +m(S = [<<"montreal">>,<<"museum">>]) -> e(S); +m(S = [<<"moscow">>,<<"museum">>]) -> e(S); +m(S = [<<"motorcycle">>,<<"museum">>]) -> e(S); +m(S = [<<"muenchen">>,<<"museum">>]) -> e(S); +m(S = [<<"muenster">>,<<"museum">>]) -> e(S); +m(S = [<<"mulhouse">>,<<"museum">>]) -> e(S); +m(S = [<<"muncie">>,<<"museum">>]) -> e(S); +m(S = [<<"museet">>,<<"museum">>]) -> e(S); +m(S = [<<"museumcenter">>,<<"museum">>]) -> e(S); +m(S = [<<"museumvereniging">>,<<"museum">>]) -> e(S); +m(S = [<<"music">>,<<"museum">>]) -> e(S); +m(S = [<<"national">>,<<"museum">>]) -> e(S); +m(S = [<<"nationalfirearms">>,<<"museum">>]) -> e(S); +m(S = [<<"nationalheritage">>,<<"museum">>]) -> e(S); +m(S = [<<"nativeamerican">>,<<"museum">>]) -> e(S); +m(S = [<<"naturalhistory">>,<<"museum">>]) -> e(S); +m(S = [<<"naturalhistorymuseum">>,<<"museum">>]) -> e(S); +m(S = [<<"naturalsciences">>,<<"museum">>]) -> e(S); +m(S = [<<"nature">>,<<"museum">>]) -> e(S); +m(S = [<<"naturhistorisches">>,<<"museum">>]) -> e(S); +m(S = [<<"natuurwetenschappen">>,<<"museum">>]) -> e(S); +m(S = [<<"naumburg">>,<<"museum">>]) -> e(S); +m(S = [<<"naval">>,<<"museum">>]) -> e(S); +m(S = [<<"nebraska">>,<<"museum">>]) -> e(S); +m(S = [<<"neues">>,<<"museum">>]) -> e(S); +m(S = [<<"newhampshire">>,<<"museum">>]) -> e(S); +m(S = [<<"newjersey">>,<<"museum">>]) -> e(S); +m(S = [<<"newmexico">>,<<"museum">>]) -> e(S); +m(S = [<<"newport">>,<<"museum">>]) -> e(S); +m(S = [<<"newspaper">>,<<"museum">>]) -> e(S); +m(S = [<<"newyork">>,<<"museum">>]) -> e(S); +m(S = [<<"niepce">>,<<"museum">>]) -> e(S); +m(S = [<<"norfolk">>,<<"museum">>]) -> e(S); +m(S = [<<"north">>,<<"museum">>]) -> e(S); +m(S = [<<"nrw">>,<<"museum">>]) -> e(S); +m(S = [<<"nyc">>,<<"museum">>]) -> e(S); +m(S = [<<"nyny">>,<<"museum">>]) -> e(S); +m(S = [<<"oceanographic">>,<<"museum">>]) -> e(S); +m(S = [<<"oceanographique">>,<<"museum">>]) -> e(S); +m(S = [<<"omaha">>,<<"museum">>]) -> e(S); +m(S = [<<"online">>,<<"museum">>]) -> e(S); +m(S = [<<"ontario">>,<<"museum">>]) -> e(S); +m(S = [<<"openair">>,<<"museum">>]) -> e(S); +m(S = [<<"oregon">>,<<"museum">>]) -> e(S); +m(S = [<<"oregontrail">>,<<"museum">>]) -> e(S); +m(S = [<<"otago">>,<<"museum">>]) -> e(S); +m(S = [<<"oxford">>,<<"museum">>]) -> e(S); +m(S = [<<"pacific">>,<<"museum">>]) -> e(S); +m(S = [<<"paderborn">>,<<"museum">>]) -> e(S); +m(S = [<<"palace">>,<<"museum">>]) -> e(S); +m(S = [<<"paleo">>,<<"museum">>]) -> e(S); +m(S = [<<"palmsprings">>,<<"museum">>]) -> e(S); +m(S = [<<"panama">>,<<"museum">>]) -> e(S); +m(S = [<<"paris">>,<<"museum">>]) -> e(S); +m(S = [<<"pasadena">>,<<"museum">>]) -> e(S); +m(S = [<<"pharmacy">>,<<"museum">>]) -> e(S); +m(S = [<<"philadelphia">>,<<"museum">>]) -> e(S); +m(S = [<<"philadelphiaarea">>,<<"museum">>]) -> e(S); +m(S = [<<"philately">>,<<"museum">>]) -> e(S); +m(S = [<<"phoenix">>,<<"museum">>]) -> e(S); +m(S = [<<"photography">>,<<"museum">>]) -> e(S); +m(S = [<<"pilots">>,<<"museum">>]) -> e(S); +m(S = [<<"pittsburgh">>,<<"museum">>]) -> e(S); +m(S = [<<"planetarium">>,<<"museum">>]) -> e(S); +m(S = [<<"plantation">>,<<"museum">>]) -> e(S); +m(S = [<<"plants">>,<<"museum">>]) -> e(S); +m(S = [<<"plaza">>,<<"museum">>]) -> e(S); +m(S = [<<"portal">>,<<"museum">>]) -> e(S); +m(S = [<<"portland">>,<<"museum">>]) -> e(S); +m(S = [<<"portlligat">>,<<"museum">>]) -> e(S); +m(S = [<<"posts-and-telecommunications">>,<<"museum">>]) -> e(S); +m(S = [<<"preservation">>,<<"museum">>]) -> e(S); +m(S = [<<"presidio">>,<<"museum">>]) -> e(S); +m(S = [<<"press">>,<<"museum">>]) -> e(S); +m(S = [<<"project">>,<<"museum">>]) -> e(S); +m(S = [<<"public">>,<<"museum">>]) -> e(S); +m(S = [<<"pubol">>,<<"museum">>]) -> e(S); +m(S = [<<"quebec">>,<<"museum">>]) -> e(S); +m(S = [<<"railroad">>,<<"museum">>]) -> e(S); +m(S = [<<"railway">>,<<"museum">>]) -> e(S); +m(S = [<<"research">>,<<"museum">>]) -> e(S); +m(S = [<<"resistance">>,<<"museum">>]) -> e(S); +m(S = [<<"riodejaneiro">>,<<"museum">>]) -> e(S); +m(S = [<<"rochester">>,<<"museum">>]) -> e(S); +m(S = [<<"rockart">>,<<"museum">>]) -> e(S); +m(S = [<<"roma">>,<<"museum">>]) -> e(S); +m(S = [<<"russia">>,<<"museum">>]) -> e(S); +m(S = [<<"saintlouis">>,<<"museum">>]) -> e(S); +m(S = [<<"salem">>,<<"museum">>]) -> e(S); +m(S = [<<"salvadordali">>,<<"museum">>]) -> e(S); +m(S = [<<"salzburg">>,<<"museum">>]) -> e(S); +m(S = [<<"sandiego">>,<<"museum">>]) -> e(S); +m(S = [<<"sanfrancisco">>,<<"museum">>]) -> e(S); +m(S = [<<"santabarbara">>,<<"museum">>]) -> e(S); +m(S = [<<"santacruz">>,<<"museum">>]) -> e(S); +m(S = [<<"santafe">>,<<"museum">>]) -> e(S); +m(S = [<<"saskatchewan">>,<<"museum">>]) -> e(S); +m(S = [<<"satx">>,<<"museum">>]) -> e(S); +m(S = [<<"savannahga">>,<<"museum">>]) -> e(S); +m(S = [<<"schlesisches">>,<<"museum">>]) -> e(S); +m(S = [<<"schoenbrunn">>,<<"museum">>]) -> e(S); +m(S = [<<"schokoladen">>,<<"museum">>]) -> e(S); +m(S = [<<"school">>,<<"museum">>]) -> e(S); +m(S = [<<"schweiz">>,<<"museum">>]) -> e(S); +m(S = [<<"science">>,<<"museum">>]) -> e(S); +m(S = [<<"scienceandhistory">>,<<"museum">>]) -> e(S); +m(S = [<<"scienceandindustry">>,<<"museum">>]) -> e(S); +m(S = [<<"sciencecenter">>,<<"museum">>]) -> e(S); +m(S = [<<"sciencecenters">>,<<"museum">>]) -> e(S); +m(S = [<<"science-fiction">>,<<"museum">>]) -> e(S); +m(S = [<<"sciencehistory">>,<<"museum">>]) -> e(S); +m(S = [<<"sciences">>,<<"museum">>]) -> e(S); +m(S = [<<"sciencesnaturelles">>,<<"museum">>]) -> e(S); +m(S = [<<"scotland">>,<<"museum">>]) -> e(S); +m(S = [<<"seaport">>,<<"museum">>]) -> e(S); +m(S = [<<"settlement">>,<<"museum">>]) -> e(S); +m(S = [<<"settlers">>,<<"museum">>]) -> e(S); +m(S = [<<"shell">>,<<"museum">>]) -> e(S); +m(S = [<<"sherbrooke">>,<<"museum">>]) -> e(S); +m(S = [<<"sibenik">>,<<"museum">>]) -> e(S); +m(S = [<<"silk">>,<<"museum">>]) -> e(S); +m(S = [<<"ski">>,<<"museum">>]) -> e(S); +m(S = [<<"skole">>,<<"museum">>]) -> e(S); +m(S = [<<"society">>,<<"museum">>]) -> e(S); +m(S = [<<"sologne">>,<<"museum">>]) -> e(S); +m(S = [<<"soundandvision">>,<<"museum">>]) -> e(S); +m(S = [<<"southcarolina">>,<<"museum">>]) -> e(S); +m(S = [<<"southwest">>,<<"museum">>]) -> e(S); +m(S = [<<"space">>,<<"museum">>]) -> e(S); +m(S = [<<"spy">>,<<"museum">>]) -> e(S); +m(S = [<<"square">>,<<"museum">>]) -> e(S); +m(S = [<<"stadt">>,<<"museum">>]) -> e(S); +m(S = [<<"stalbans">>,<<"museum">>]) -> e(S); +m(S = [<<"starnberg">>,<<"museum">>]) -> e(S); +m(S = [<<"state">>,<<"museum">>]) -> e(S); +m(S = [<<"stateofdelaware">>,<<"museum">>]) -> e(S); +m(S = [<<"station">>,<<"museum">>]) -> e(S); +m(S = [<<"steam">>,<<"museum">>]) -> e(S); +m(S = [<<"steiermark">>,<<"museum">>]) -> e(S); +m(S = [<<"stjohn">>,<<"museum">>]) -> e(S); +m(S = [<<"stockholm">>,<<"museum">>]) -> e(S); +m(S = [<<"stpetersburg">>,<<"museum">>]) -> e(S); +m(S = [<<"stuttgart">>,<<"museum">>]) -> e(S); +m(S = [<<"suisse">>,<<"museum">>]) -> e(S); +m(S = [<<"surgeonshall">>,<<"museum">>]) -> e(S); +m(S = [<<"surrey">>,<<"museum">>]) -> e(S); +m(S = [<<"svizzera">>,<<"museum">>]) -> e(S); +m(S = [<<"sweden">>,<<"museum">>]) -> e(S); +m(S = [<<"sydney">>,<<"museum">>]) -> e(S); +m(S = [<<"tank">>,<<"museum">>]) -> e(S); +m(S = [<<"tcm">>,<<"museum">>]) -> e(S); +m(S = [<<"technology">>,<<"museum">>]) -> e(S); +m(S = [<<"telekommunikation">>,<<"museum">>]) -> e(S); +m(S = [<<"television">>,<<"museum">>]) -> e(S); +m(S = [<<"texas">>,<<"museum">>]) -> e(S); +m(S = [<<"textile">>,<<"museum">>]) -> e(S); +m(S = [<<"theater">>,<<"museum">>]) -> e(S); +m(S = [<<"time">>,<<"museum">>]) -> e(S); +m(S = [<<"timekeeping">>,<<"museum">>]) -> e(S); +m(S = [<<"topology">>,<<"museum">>]) -> e(S); +m(S = [<<"torino">>,<<"museum">>]) -> e(S); +m(S = [<<"touch">>,<<"museum">>]) -> e(S); +m(S = [<<"town">>,<<"museum">>]) -> e(S); +m(S = [<<"transport">>,<<"museum">>]) -> e(S); +m(S = [<<"tree">>,<<"museum">>]) -> e(S); +m(S = [<<"trolley">>,<<"museum">>]) -> e(S); +m(S = [<<"trust">>,<<"museum">>]) -> e(S); +m(S = [<<"trustee">>,<<"museum">>]) -> e(S); +m(S = [<<"uhren">>,<<"museum">>]) -> e(S); +m(S = [<<"ulm">>,<<"museum">>]) -> e(S); +m(S = [<<"undersea">>,<<"museum">>]) -> e(S); +m(S = [<<"university">>,<<"museum">>]) -> e(S); +m(S = [<<"usa">>,<<"museum">>]) -> e(S); +m(S = [<<"usantiques">>,<<"museum">>]) -> e(S); +m(S = [<<"usarts">>,<<"museum">>]) -> e(S); +m(S = [<<"uscountryestate">>,<<"museum">>]) -> e(S); +m(S = [<<"usculture">>,<<"museum">>]) -> e(S); +m(S = [<<"usdecorativearts">>,<<"museum">>]) -> e(S); +m(S = [<<"usgarden">>,<<"museum">>]) -> e(S); +m(S = [<<"ushistory">>,<<"museum">>]) -> e(S); +m(S = [<<"ushuaia">>,<<"museum">>]) -> e(S); +m(S = [<<"uslivinghistory">>,<<"museum">>]) -> e(S); +m(S = [<<"utah">>,<<"museum">>]) -> e(S); +m(S = [<<"uvic">>,<<"museum">>]) -> e(S); +m(S = [<<"valley">>,<<"museum">>]) -> e(S); +m(S = [<<"vantaa">>,<<"museum">>]) -> e(S); +m(S = [<<"versailles">>,<<"museum">>]) -> e(S); +m(S = [<<"viking">>,<<"museum">>]) -> e(S); +m(S = [<<"village">>,<<"museum">>]) -> e(S); +m(S = [<<"virginia">>,<<"museum">>]) -> e(S); +m(S = [<<"virtual">>,<<"museum">>]) -> e(S); +m(S = [<<"virtuel">>,<<"museum">>]) -> e(S); +m(S = [<<"vlaanderen">>,<<"museum">>]) -> e(S); +m(S = [<<"volkenkunde">>,<<"museum">>]) -> e(S); +m(S = [<<"wales">>,<<"museum">>]) -> e(S); +m(S = [<<"wallonie">>,<<"museum">>]) -> e(S); +m(S = [<<"war">>,<<"museum">>]) -> e(S); +m(S = [<<"washingtondc">>,<<"museum">>]) -> e(S); +m(S = [<<"watchandclock">>,<<"museum">>]) -> e(S); +m(S = [<<"watch-and-clock">>,<<"museum">>]) -> e(S); +m(S = [<<"western">>,<<"museum">>]) -> e(S); +m(S = [<<"westfalen">>,<<"museum">>]) -> e(S); +m(S = [<<"whaling">>,<<"museum">>]) -> e(S); +m(S = [<<"wildlife">>,<<"museum">>]) -> e(S); +m(S = [<<"williamsburg">>,<<"museum">>]) -> e(S); +m(S = [<<"windmill">>,<<"museum">>]) -> e(S); +m(S = [<<"workshop">>,<<"museum">>]) -> e(S); +m(S = [<<"york">>,<<"museum">>]) -> e(S); +m(S = [<<"yorkshire">>,<<"museum">>]) -> e(S); +m(S = [<<"yosemite">>,<<"museum">>]) -> e(S); +m(S = [<<"youth">>,<<"museum">>]) -> e(S); +m(S = [<<"zoological">>,<<"museum">>]) -> e(S); +m(S = [<<"zoology">>,<<"museum">>]) -> e(S); +m(S = [<<"xn--9dbhblg6di">>,<<"museum">>]) -> e(S); +m(S = [<<"xn--h1aegh">>,<<"museum">>]) -> e(S); +m(S = [<<"mv">>]) -> e(S); +m(S = [<<"aero">>,<<"mv">>]) -> e(S); +m(S = [<<"biz">>,<<"mv">>]) -> e(S); +m(S = [<<"com">>,<<"mv">>]) -> e(S); +m(S = [<<"coop">>,<<"mv">>]) -> e(S); +m(S = [<<"edu">>,<<"mv">>]) -> e(S); +m(S = [<<"gov">>,<<"mv">>]) -> e(S); +m(S = [<<"info">>,<<"mv">>]) -> e(S); +m(S = [<<"int">>,<<"mv">>]) -> e(S); +m(S = [<<"mil">>,<<"mv">>]) -> e(S); +m(S = [<<"museum">>,<<"mv">>]) -> e(S); +m(S = [<<"name">>,<<"mv">>]) -> e(S); +m(S = [<<"net">>,<<"mv">>]) -> e(S); +m(S = [<<"org">>,<<"mv">>]) -> e(S); +m(S = [<<"pro">>,<<"mv">>]) -> e(S); +m(S = [<<"mw">>]) -> e(S); +m(S = [<<"ac">>,<<"mw">>]) -> e(S); +m(S = [<<"biz">>,<<"mw">>]) -> e(S); +m(S = [<<"co">>,<<"mw">>]) -> e(S); +m(S = [<<"com">>,<<"mw">>]) -> e(S); +m(S = [<<"coop">>,<<"mw">>]) -> e(S); +m(S = [<<"edu">>,<<"mw">>]) -> e(S); +m(S = [<<"gov">>,<<"mw">>]) -> e(S); +m(S = [<<"int">>,<<"mw">>]) -> e(S); +m(S = [<<"museum">>,<<"mw">>]) -> e(S); +m(S = [<<"net">>,<<"mw">>]) -> e(S); +m(S = [<<"org">>,<<"mw">>]) -> e(S); +m(S = [<<"mx">>]) -> e(S); +m(S = [<<"com">>,<<"mx">>]) -> e(S); +m(S = [<<"org">>,<<"mx">>]) -> e(S); +m(S = [<<"gob">>,<<"mx">>]) -> e(S); +m(S = [<<"edu">>,<<"mx">>]) -> e(S); +m(S = [<<"net">>,<<"mx">>]) -> e(S); +m(S = [<<"my">>]) -> e(S); +m(S = [<<"biz">>,<<"my">>]) -> e(S); +m(S = [<<"com">>,<<"my">>]) -> e(S); +m(S = [<<"edu">>,<<"my">>]) -> e(S); +m(S = [<<"gov">>,<<"my">>]) -> e(S); +m(S = [<<"mil">>,<<"my">>]) -> e(S); +m(S = [<<"name">>,<<"my">>]) -> e(S); +m(S = [<<"net">>,<<"my">>]) -> e(S); +m(S = [<<"org">>,<<"my">>]) -> e(S); +m(S = [<<"mz">>]) -> e(S); +m(S = [<<"ac">>,<<"mz">>]) -> e(S); +m(S = [<<"adv">>,<<"mz">>]) -> e(S); +m(S = [<<"co">>,<<"mz">>]) -> e(S); +m(S = [<<"edu">>,<<"mz">>]) -> e(S); +m(S = [<<"gov">>,<<"mz">>]) -> e(S); +m(S = [<<"mil">>,<<"mz">>]) -> e(S); +m(S = [<<"net">>,<<"mz">>]) -> e(S); +m(S = [<<"org">>,<<"mz">>]) -> e(S); +m(S = [<<"na">>]) -> e(S); +m(S = [<<"info">>,<<"na">>]) -> e(S); +m(S = [<<"pro">>,<<"na">>]) -> e(S); +m(S = [<<"name">>,<<"na">>]) -> e(S); +m(S = [<<"school">>,<<"na">>]) -> e(S); +m(S = [<<"or">>,<<"na">>]) -> e(S); +m(S = [<<"dr">>,<<"na">>]) -> e(S); +m(S = [<<"us">>,<<"na">>]) -> e(S); +m(S = [<<"mx">>,<<"na">>]) -> e(S); +m(S = [<<"ca">>,<<"na">>]) -> e(S); +m(S = [<<"in">>,<<"na">>]) -> e(S); +m(S = [<<"cc">>,<<"na">>]) -> e(S); +m(S = [<<"tv">>,<<"na">>]) -> e(S); +m(S = [<<"ws">>,<<"na">>]) -> e(S); +m(S = [<<"mobi">>,<<"na">>]) -> e(S); +m(S = [<<"co">>,<<"na">>]) -> e(S); +m(S = [<<"com">>,<<"na">>]) -> e(S); +m(S = [<<"org">>,<<"na">>]) -> e(S); +m(S = [<<"name">>]) -> e(S); +m(S = [<<"nc">>]) -> e(S); +m(S = [<<"asso">>,<<"nc">>]) -> e(S); +m(S = [<<"nom">>,<<"nc">>]) -> e(S); +m(S = [<<"ne">>]) -> e(S); +m(S = [<<"net">>]) -> e(S); +m(S = [<<"nf">>]) -> e(S); +m(S = [<<"com">>,<<"nf">>]) -> e(S); +m(S = [<<"net">>,<<"nf">>]) -> e(S); +m(S = [<<"per">>,<<"nf">>]) -> e(S); +m(S = [<<"rec">>,<<"nf">>]) -> e(S); +m(S = [<<"web">>,<<"nf">>]) -> e(S); +m(S = [<<"arts">>,<<"nf">>]) -> e(S); +m(S = [<<"firm">>,<<"nf">>]) -> e(S); +m(S = [<<"info">>,<<"nf">>]) -> e(S); +m(S = [<<"other">>,<<"nf">>]) -> e(S); +m(S = [<<"store">>,<<"nf">>]) -> e(S); +m(S = [<<"ng">>]) -> e(S); +m(S = [<<"com">>,<<"ng">>]) -> e(S); +m(S = [<<"edu">>,<<"ng">>]) -> e(S); +m(S = [<<"gov">>,<<"ng">>]) -> e(S); +m(S = [<<"i">>,<<"ng">>]) -> e(S); +m(S = [<<"mil">>,<<"ng">>]) -> e(S); +m(S = [<<"mobi">>,<<"ng">>]) -> e(S); +m(S = [<<"name">>,<<"ng">>]) -> e(S); +m(S = [<<"net">>,<<"ng">>]) -> e(S); +m(S = [<<"org">>,<<"ng">>]) -> e(S); +m(S = [<<"sch">>,<<"ng">>]) -> e(S); +m(S = [<<"ni">>]) -> e(S); +m(S = [<<"ac">>,<<"ni">>]) -> e(S); +m(S = [<<"biz">>,<<"ni">>]) -> e(S); +m(S = [<<"co">>,<<"ni">>]) -> e(S); +m(S = [<<"com">>,<<"ni">>]) -> e(S); +m(S = [<<"edu">>,<<"ni">>]) -> e(S); +m(S = [<<"gob">>,<<"ni">>]) -> e(S); +m(S = [<<"in">>,<<"ni">>]) -> e(S); +m(S = [<<"info">>,<<"ni">>]) -> e(S); +m(S = [<<"int">>,<<"ni">>]) -> e(S); +m(S = [<<"mil">>,<<"ni">>]) -> e(S); +m(S = [<<"net">>,<<"ni">>]) -> e(S); +m(S = [<<"nom">>,<<"ni">>]) -> e(S); +m(S = [<<"org">>,<<"ni">>]) -> e(S); +m(S = [<<"web">>,<<"ni">>]) -> e(S); +m(S = [<<"nl">>]) -> e(S); +m(S = [<<"no">>]) -> e(S); +m(S = [<<"fhs">>,<<"no">>]) -> e(S); +m(S = [<<"vgs">>,<<"no">>]) -> e(S); +m(S = [<<"fylkesbibl">>,<<"no">>]) -> e(S); +m(S = [<<"folkebibl">>,<<"no">>]) -> e(S); +m(S = [<<"museum">>,<<"no">>]) -> e(S); +m(S = [<<"idrett">>,<<"no">>]) -> e(S); +m(S = [<<"priv">>,<<"no">>]) -> e(S); +m(S = [<<"mil">>,<<"no">>]) -> e(S); +m(S = [<<"stat">>,<<"no">>]) -> e(S); +m(S = [<<"dep">>,<<"no">>]) -> e(S); +m(S = [<<"kommune">>,<<"no">>]) -> e(S); +m(S = [<<"herad">>,<<"no">>]) -> e(S); +m(S = [<<"aa">>,<<"no">>]) -> e(S); +m(S = [<<"ah">>,<<"no">>]) -> e(S); +m(S = [<<"bu">>,<<"no">>]) -> e(S); +m(S = [<<"fm">>,<<"no">>]) -> e(S); +m(S = [<<"hl">>,<<"no">>]) -> e(S); +m(S = [<<"hm">>,<<"no">>]) -> e(S); +m(S = [<<"jan-mayen">>,<<"no">>]) -> e(S); +m(S = [<<"mr">>,<<"no">>]) -> e(S); +m(S = [<<"nl">>,<<"no">>]) -> e(S); +m(S = [<<"nt">>,<<"no">>]) -> e(S); +m(S = [<<"of">>,<<"no">>]) -> e(S); +m(S = [<<"ol">>,<<"no">>]) -> e(S); +m(S = [<<"oslo">>,<<"no">>]) -> e(S); +m(S = [<<"rl">>,<<"no">>]) -> e(S); +m(S = [<<"sf">>,<<"no">>]) -> e(S); +m(S = [<<"st">>,<<"no">>]) -> e(S); +m(S = [<<"svalbard">>,<<"no">>]) -> e(S); +m(S = [<<"tm">>,<<"no">>]) -> e(S); +m(S = [<<"tr">>,<<"no">>]) -> e(S); +m(S = [<<"va">>,<<"no">>]) -> e(S); +m(S = [<<"vf">>,<<"no">>]) -> e(S); +m(S = [<<"gs">>,<<"aa">>,<<"no">>]) -> e(S); +m(S = [<<"gs">>,<<"ah">>,<<"no">>]) -> e(S); +m(S = [<<"gs">>,<<"bu">>,<<"no">>]) -> e(S); +m(S = [<<"gs">>,<<"fm">>,<<"no">>]) -> e(S); +m(S = [<<"gs">>,<<"hl">>,<<"no">>]) -> e(S); +m(S = [<<"gs">>,<<"hm">>,<<"no">>]) -> e(S); +m(S = [<<"gs">>,<<"jan-mayen">>,<<"no">>]) -> e(S); +m(S = [<<"gs">>,<<"mr">>,<<"no">>]) -> e(S); +m(S = [<<"gs">>,<<"nl">>,<<"no">>]) -> e(S); +m(S = [<<"gs">>,<<"nt">>,<<"no">>]) -> e(S); +m(S = [<<"gs">>,<<"of">>,<<"no">>]) -> e(S); +m(S = [<<"gs">>,<<"ol">>,<<"no">>]) -> e(S); +m(S = [<<"gs">>,<<"oslo">>,<<"no">>]) -> e(S); +m(S = [<<"gs">>,<<"rl">>,<<"no">>]) -> e(S); +m(S = [<<"gs">>,<<"sf">>,<<"no">>]) -> e(S); +m(S = [<<"gs">>,<<"st">>,<<"no">>]) -> e(S); +m(S = [<<"gs">>,<<"svalbard">>,<<"no">>]) -> e(S); +m(S = [<<"gs">>,<<"tm">>,<<"no">>]) -> e(S); +m(S = [<<"gs">>,<<"tr">>,<<"no">>]) -> e(S); +m(S = [<<"gs">>,<<"va">>,<<"no">>]) -> e(S); +m(S = [<<"gs">>,<<"vf">>,<<"no">>]) -> e(S); +m(S = [<<"akrehamn">>,<<"no">>]) -> e(S); +m(S = [<<"xn--krehamn-dxa">>,<<"no">>]) -> e(S); +m(S = [<<"algard">>,<<"no">>]) -> e(S); +m(S = [<<"xn--lgrd-poac">>,<<"no">>]) -> e(S); +m(S = [<<"arna">>,<<"no">>]) -> e(S); +m(S = [<<"brumunddal">>,<<"no">>]) -> e(S); +m(S = [<<"bryne">>,<<"no">>]) -> e(S); +m(S = [<<"bronnoysund">>,<<"no">>]) -> e(S); +m(S = [<<"xn--brnnysund-m8ac">>,<<"no">>]) -> e(S); +m(S = [<<"drobak">>,<<"no">>]) -> e(S); +m(S = [<<"xn--drbak-wua">>,<<"no">>]) -> e(S); +m(S = [<<"egersund">>,<<"no">>]) -> e(S); +m(S = [<<"fetsund">>,<<"no">>]) -> e(S); +m(S = [<<"floro">>,<<"no">>]) -> e(S); +m(S = [<<"xn--flor-jra">>,<<"no">>]) -> e(S); +m(S = [<<"fredrikstad">>,<<"no">>]) -> e(S); +m(S = [<<"hokksund">>,<<"no">>]) -> e(S); +m(S = [<<"honefoss">>,<<"no">>]) -> e(S); +m(S = [<<"xn--hnefoss-q1a">>,<<"no">>]) -> e(S); +m(S = [<<"jessheim">>,<<"no">>]) -> e(S); +m(S = [<<"jorpeland">>,<<"no">>]) -> e(S); +m(S = [<<"xn--jrpeland-54a">>,<<"no">>]) -> e(S); +m(S = [<<"kirkenes">>,<<"no">>]) -> e(S); +m(S = [<<"kopervik">>,<<"no">>]) -> e(S); +m(S = [<<"krokstadelva">>,<<"no">>]) -> e(S); +m(S = [<<"langevag">>,<<"no">>]) -> e(S); +m(S = [<<"xn--langevg-jxa">>,<<"no">>]) -> e(S); +m(S = [<<"leirvik">>,<<"no">>]) -> e(S); +m(S = [<<"mjondalen">>,<<"no">>]) -> e(S); +m(S = [<<"xn--mjndalen-64a">>,<<"no">>]) -> e(S); +m(S = [<<"mo-i-rana">>,<<"no">>]) -> e(S); +m(S = [<<"mosjoen">>,<<"no">>]) -> e(S); +m(S = [<<"xn--mosjen-eya">>,<<"no">>]) -> e(S); +m(S = [<<"nesoddtangen">>,<<"no">>]) -> e(S); +m(S = [<<"orkanger">>,<<"no">>]) -> e(S); +m(S = [<<"osoyro">>,<<"no">>]) -> e(S); +m(S = [<<"xn--osyro-wua">>,<<"no">>]) -> e(S); +m(S = [<<"raholt">>,<<"no">>]) -> e(S); +m(S = [<<"xn--rholt-mra">>,<<"no">>]) -> e(S); +m(S = [<<"sandnessjoen">>,<<"no">>]) -> e(S); +m(S = [<<"xn--sandnessjen-ogb">>,<<"no">>]) -> e(S); +m(S = [<<"skedsmokorset">>,<<"no">>]) -> e(S); +m(S = [<<"slattum">>,<<"no">>]) -> e(S); +m(S = [<<"spjelkavik">>,<<"no">>]) -> e(S); +m(S = [<<"stathelle">>,<<"no">>]) -> e(S); +m(S = [<<"stavern">>,<<"no">>]) -> e(S); +m(S = [<<"stjordalshalsen">>,<<"no">>]) -> e(S); +m(S = [<<"xn--stjrdalshalsen-sqb">>,<<"no">>]) -> e(S); +m(S = [<<"tananger">>,<<"no">>]) -> e(S); +m(S = [<<"tranby">>,<<"no">>]) -> e(S); +m(S = [<<"vossevangen">>,<<"no">>]) -> e(S); +m(S = [<<"afjord">>,<<"no">>]) -> e(S); +m(S = [<<"xn--fjord-lra">>,<<"no">>]) -> e(S); +m(S = [<<"agdenes">>,<<"no">>]) -> e(S); +m(S = [<<"al">>,<<"no">>]) -> e(S); +m(S = [<<"xn--l-1fa">>,<<"no">>]) -> e(S); +m(S = [<<"alesund">>,<<"no">>]) -> e(S); +m(S = [<<"xn--lesund-hua">>,<<"no">>]) -> e(S); +m(S = [<<"alstahaug">>,<<"no">>]) -> e(S); +m(S = [<<"alta">>,<<"no">>]) -> e(S); +m(S = [<<"xn--lt-liac">>,<<"no">>]) -> e(S); +m(S = [<<"alaheadju">>,<<"no">>]) -> e(S); +m(S = [<<"xn--laheadju-7ya">>,<<"no">>]) -> e(S); +m(S = [<<"alvdal">>,<<"no">>]) -> e(S); +m(S = [<<"amli">>,<<"no">>]) -> e(S); +m(S = [<<"xn--mli-tla">>,<<"no">>]) -> e(S); +m(S = [<<"amot">>,<<"no">>]) -> e(S); +m(S = [<<"xn--mot-tla">>,<<"no">>]) -> e(S); +m(S = [<<"andebu">>,<<"no">>]) -> e(S); +m(S = [<<"andoy">>,<<"no">>]) -> e(S); +m(S = [<<"xn--andy-ira">>,<<"no">>]) -> e(S); +m(S = [<<"andasuolo">>,<<"no">>]) -> e(S); +m(S = [<<"ardal">>,<<"no">>]) -> e(S); +m(S = [<<"xn--rdal-poa">>,<<"no">>]) -> e(S); +m(S = [<<"aremark">>,<<"no">>]) -> e(S); +m(S = [<<"arendal">>,<<"no">>]) -> e(S); +m(S = [<<"xn--s-1fa">>,<<"no">>]) -> e(S); +m(S = [<<"aseral">>,<<"no">>]) -> e(S); +m(S = [<<"xn--seral-lra">>,<<"no">>]) -> e(S); +m(S = [<<"asker">>,<<"no">>]) -> e(S); +m(S = [<<"askim">>,<<"no">>]) -> e(S); +m(S = [<<"askvoll">>,<<"no">>]) -> e(S); +m(S = [<<"askoy">>,<<"no">>]) -> e(S); +m(S = [<<"xn--asky-ira">>,<<"no">>]) -> e(S); +m(S = [<<"asnes">>,<<"no">>]) -> e(S); +m(S = [<<"xn--snes-poa">>,<<"no">>]) -> e(S); +m(S = [<<"audnedaln">>,<<"no">>]) -> e(S); +m(S = [<<"aukra">>,<<"no">>]) -> e(S); +m(S = [<<"aure">>,<<"no">>]) -> e(S); +m(S = [<<"aurland">>,<<"no">>]) -> e(S); +m(S = [<<"aurskog-holand">>,<<"no">>]) -> e(S); +m(S = [<<"xn--aurskog-hland-jnb">>,<<"no">>]) -> e(S); +m(S = [<<"austevoll">>,<<"no">>]) -> e(S); +m(S = [<<"austrheim">>,<<"no">>]) -> e(S); +m(S = [<<"averoy">>,<<"no">>]) -> e(S); +m(S = [<<"xn--avery-yua">>,<<"no">>]) -> e(S); +m(S = [<<"balestrand">>,<<"no">>]) -> e(S); +m(S = [<<"ballangen">>,<<"no">>]) -> e(S); +m(S = [<<"balat">>,<<"no">>]) -> e(S); +m(S = [<<"xn--blt-elab">>,<<"no">>]) -> e(S); +m(S = [<<"balsfjord">>,<<"no">>]) -> e(S); +m(S = [<<"bahccavuotna">>,<<"no">>]) -> e(S); +m(S = [<<"xn--bhccavuotna-k7a">>,<<"no">>]) -> e(S); +m(S = [<<"bamble">>,<<"no">>]) -> e(S); +m(S = [<<"bardu">>,<<"no">>]) -> e(S); +m(S = [<<"beardu">>,<<"no">>]) -> e(S); +m(S = [<<"beiarn">>,<<"no">>]) -> e(S); +m(S = [<<"bajddar">>,<<"no">>]) -> e(S); +m(S = [<<"xn--bjddar-pta">>,<<"no">>]) -> e(S); +m(S = [<<"baidar">>,<<"no">>]) -> e(S); +m(S = [<<"xn--bidr-5nac">>,<<"no">>]) -> e(S); +m(S = [<<"berg">>,<<"no">>]) -> e(S); +m(S = [<<"bergen">>,<<"no">>]) -> e(S); +m(S = [<<"berlevag">>,<<"no">>]) -> e(S); +m(S = [<<"xn--berlevg-jxa">>,<<"no">>]) -> e(S); +m(S = [<<"bearalvahki">>,<<"no">>]) -> e(S); +m(S = [<<"xn--bearalvhki-y4a">>,<<"no">>]) -> e(S); +m(S = [<<"bindal">>,<<"no">>]) -> e(S); +m(S = [<<"birkenes">>,<<"no">>]) -> e(S); +m(S = [<<"bjarkoy">>,<<"no">>]) -> e(S); +m(S = [<<"xn--bjarky-fya">>,<<"no">>]) -> e(S); +m(S = [<<"bjerkreim">>,<<"no">>]) -> e(S); +m(S = [<<"bjugn">>,<<"no">>]) -> e(S); +m(S = [<<"bodo">>,<<"no">>]) -> e(S); +m(S = [<<"xn--bod-2na">>,<<"no">>]) -> e(S); +m(S = [<<"badaddja">>,<<"no">>]) -> e(S); +m(S = [<<"xn--bdddj-mrabd">>,<<"no">>]) -> e(S); +m(S = [<<"budejju">>,<<"no">>]) -> e(S); +m(S = [<<"bokn">>,<<"no">>]) -> e(S); +m(S = [<<"bremanger">>,<<"no">>]) -> e(S); +m(S = [<<"bronnoy">>,<<"no">>]) -> e(S); +m(S = [<<"xn--brnny-wuac">>,<<"no">>]) -> e(S); +m(S = [<<"bygland">>,<<"no">>]) -> e(S); +m(S = [<<"bykle">>,<<"no">>]) -> e(S); +m(S = [<<"barum">>,<<"no">>]) -> e(S); +m(S = [<<"xn--brum-voa">>,<<"no">>]) -> e(S); +m(S = [<<"bo">>,<<"telemark">>,<<"no">>]) -> e(S); +m(S = [<<"xn--b-5ga">>,<<"telemark">>,<<"no">>]) -> e(S); +m(S = [<<"bo">>,<<"nordland">>,<<"no">>]) -> e(S); +m(S = [<<"xn--b-5ga">>,<<"nordland">>,<<"no">>]) -> e(S); +m(S = [<<"bievat">>,<<"no">>]) -> e(S); +m(S = [<<"xn--bievt-0qa">>,<<"no">>]) -> e(S); +m(S = [<<"bomlo">>,<<"no">>]) -> e(S); +m(S = [<<"xn--bmlo-gra">>,<<"no">>]) -> e(S); +m(S = [<<"batsfjord">>,<<"no">>]) -> e(S); +m(S = [<<"xn--btsfjord-9za">>,<<"no">>]) -> e(S); +m(S = [<<"bahcavuotna">>,<<"no">>]) -> e(S); +m(S = [<<"xn--bhcavuotna-s4a">>,<<"no">>]) -> e(S); +m(S = [<<"dovre">>,<<"no">>]) -> e(S); +m(S = [<<"drammen">>,<<"no">>]) -> e(S); +m(S = [<<"drangedal">>,<<"no">>]) -> e(S); +m(S = [<<"dyroy">>,<<"no">>]) -> e(S); +m(S = [<<"xn--dyry-ira">>,<<"no">>]) -> e(S); +m(S = [<<"donna">>,<<"no">>]) -> e(S); +m(S = [<<"xn--dnna-gra">>,<<"no">>]) -> e(S); +m(S = [<<"eid">>,<<"no">>]) -> e(S); +m(S = [<<"eidfjord">>,<<"no">>]) -> e(S); +m(S = [<<"eidsberg">>,<<"no">>]) -> e(S); +m(S = [<<"eidskog">>,<<"no">>]) -> e(S); +m(S = [<<"eidsvoll">>,<<"no">>]) -> e(S); +m(S = [<<"eigersund">>,<<"no">>]) -> e(S); +m(S = [<<"elverum">>,<<"no">>]) -> e(S); +m(S = [<<"enebakk">>,<<"no">>]) -> e(S); +m(S = [<<"engerdal">>,<<"no">>]) -> e(S); +m(S = [<<"etne">>,<<"no">>]) -> e(S); +m(S = [<<"etnedal">>,<<"no">>]) -> e(S); +m(S = [<<"evenes">>,<<"no">>]) -> e(S); +m(S = [<<"evenassi">>,<<"no">>]) -> e(S); +m(S = [<<"xn--eveni-0qa01ga">>,<<"no">>]) -> e(S); +m(S = [<<"evje-og-hornnes">>,<<"no">>]) -> e(S); +m(S = [<<"farsund">>,<<"no">>]) -> e(S); +m(S = [<<"fauske">>,<<"no">>]) -> e(S); +m(S = [<<"fuossko">>,<<"no">>]) -> e(S); +m(S = [<<"fuoisku">>,<<"no">>]) -> e(S); +m(S = [<<"fedje">>,<<"no">>]) -> e(S); +m(S = [<<"fet">>,<<"no">>]) -> e(S); +m(S = [<<"finnoy">>,<<"no">>]) -> e(S); +m(S = [<<"xn--finny-yua">>,<<"no">>]) -> e(S); +m(S = [<<"fitjar">>,<<"no">>]) -> e(S); +m(S = [<<"fjaler">>,<<"no">>]) -> e(S); +m(S = [<<"fjell">>,<<"no">>]) -> e(S); +m(S = [<<"flakstad">>,<<"no">>]) -> e(S); +m(S = [<<"flatanger">>,<<"no">>]) -> e(S); +m(S = [<<"flekkefjord">>,<<"no">>]) -> e(S); +m(S = [<<"flesberg">>,<<"no">>]) -> e(S); +m(S = [<<"flora">>,<<"no">>]) -> e(S); +m(S = [<<"fla">>,<<"no">>]) -> e(S); +m(S = [<<"xn--fl-zia">>,<<"no">>]) -> e(S); +m(S = [<<"folldal">>,<<"no">>]) -> e(S); +m(S = [<<"forsand">>,<<"no">>]) -> e(S); +m(S = [<<"fosnes">>,<<"no">>]) -> e(S); +m(S = [<<"frei">>,<<"no">>]) -> e(S); +m(S = [<<"frogn">>,<<"no">>]) -> e(S); +m(S = [<<"froland">>,<<"no">>]) -> e(S); +m(S = [<<"frosta">>,<<"no">>]) -> e(S); +m(S = [<<"frana">>,<<"no">>]) -> e(S); +m(S = [<<"xn--frna-woa">>,<<"no">>]) -> e(S); +m(S = [<<"froya">>,<<"no">>]) -> e(S); +m(S = [<<"xn--frya-hra">>,<<"no">>]) -> e(S); +m(S = [<<"fusa">>,<<"no">>]) -> e(S); +m(S = [<<"fyresdal">>,<<"no">>]) -> e(S); +m(S = [<<"forde">>,<<"no">>]) -> e(S); +m(S = [<<"xn--frde-gra">>,<<"no">>]) -> e(S); +m(S = [<<"gamvik">>,<<"no">>]) -> e(S); +m(S = [<<"gangaviika">>,<<"no">>]) -> e(S); +m(S = [<<"xn--ggaviika-8ya47h">>,<<"no">>]) -> e(S); +m(S = [<<"gaular">>,<<"no">>]) -> e(S); +m(S = [<<"gausdal">>,<<"no">>]) -> e(S); +m(S = [<<"gildeskal">>,<<"no">>]) -> e(S); +m(S = [<<"xn--gildeskl-g0a">>,<<"no">>]) -> e(S); +m(S = [<<"giske">>,<<"no">>]) -> e(S); +m(S = [<<"gjemnes">>,<<"no">>]) -> e(S); +m(S = [<<"gjerdrum">>,<<"no">>]) -> e(S); +m(S = [<<"gjerstad">>,<<"no">>]) -> e(S); +m(S = [<<"gjesdal">>,<<"no">>]) -> e(S); +m(S = [<<"gjovik">>,<<"no">>]) -> e(S); +m(S = [<<"xn--gjvik-wua">>,<<"no">>]) -> e(S); +m(S = [<<"gloppen">>,<<"no">>]) -> e(S); +m(S = [<<"gol">>,<<"no">>]) -> e(S); +m(S = [<<"gran">>,<<"no">>]) -> e(S); +m(S = [<<"grane">>,<<"no">>]) -> e(S); +m(S = [<<"granvin">>,<<"no">>]) -> e(S); +m(S = [<<"gratangen">>,<<"no">>]) -> e(S); +m(S = [<<"grimstad">>,<<"no">>]) -> e(S); +m(S = [<<"grong">>,<<"no">>]) -> e(S); +m(S = [<<"kraanghke">>,<<"no">>]) -> e(S); +m(S = [<<"xn--kranghke-b0a">>,<<"no">>]) -> e(S); +m(S = [<<"grue">>,<<"no">>]) -> e(S); +m(S = [<<"gulen">>,<<"no">>]) -> e(S); +m(S = [<<"hadsel">>,<<"no">>]) -> e(S); +m(S = [<<"halden">>,<<"no">>]) -> e(S); +m(S = [<<"halsa">>,<<"no">>]) -> e(S); +m(S = [<<"hamar">>,<<"no">>]) -> e(S); +m(S = [<<"hamaroy">>,<<"no">>]) -> e(S); +m(S = [<<"habmer">>,<<"no">>]) -> e(S); +m(S = [<<"xn--hbmer-xqa">>,<<"no">>]) -> e(S); +m(S = [<<"hapmir">>,<<"no">>]) -> e(S); +m(S = [<<"xn--hpmir-xqa">>,<<"no">>]) -> e(S); +m(S = [<<"hammerfest">>,<<"no">>]) -> e(S); +m(S = [<<"hammarfeasta">>,<<"no">>]) -> e(S); +m(S = [<<"xn--hmmrfeasta-s4ac">>,<<"no">>]) -> e(S); +m(S = [<<"haram">>,<<"no">>]) -> e(S); +m(S = [<<"hareid">>,<<"no">>]) -> e(S); +m(S = [<<"harstad">>,<<"no">>]) -> e(S); +m(S = [<<"hasvik">>,<<"no">>]) -> e(S); +m(S = [<<"aknoluokta">>,<<"no">>]) -> e(S); +m(S = [<<"xn--koluokta-7ya57h">>,<<"no">>]) -> e(S); +m(S = [<<"hattfjelldal">>,<<"no">>]) -> e(S); +m(S = [<<"aarborte">>,<<"no">>]) -> e(S); +m(S = [<<"haugesund">>,<<"no">>]) -> e(S); +m(S = [<<"hemne">>,<<"no">>]) -> e(S); +m(S = [<<"hemnes">>,<<"no">>]) -> e(S); +m(S = [<<"hemsedal">>,<<"no">>]) -> e(S); +m(S = [<<"heroy">>,<<"more-og-romsdal">>,<<"no">>]) -> e(S); +m(S = [<<"xn--hery-ira">>,<<"xn--mre-og-romsdal-qqb">>,<<"no">>]) -> e(S); +m(S = [<<"heroy">>,<<"nordland">>,<<"no">>]) -> e(S); +m(S = [<<"xn--hery-ira">>,<<"nordland">>,<<"no">>]) -> e(S); +m(S = [<<"hitra">>,<<"no">>]) -> e(S); +m(S = [<<"hjartdal">>,<<"no">>]) -> e(S); +m(S = [<<"hjelmeland">>,<<"no">>]) -> e(S); +m(S = [<<"hobol">>,<<"no">>]) -> e(S); +m(S = [<<"xn--hobl-ira">>,<<"no">>]) -> e(S); +m(S = [<<"hof">>,<<"no">>]) -> e(S); +m(S = [<<"hol">>,<<"no">>]) -> e(S); +m(S = [<<"hole">>,<<"no">>]) -> e(S); +m(S = [<<"holmestrand">>,<<"no">>]) -> e(S); +m(S = [<<"holtalen">>,<<"no">>]) -> e(S); +m(S = [<<"xn--holtlen-hxa">>,<<"no">>]) -> e(S); +m(S = [<<"hornindal">>,<<"no">>]) -> e(S); +m(S = [<<"horten">>,<<"no">>]) -> e(S); +m(S = [<<"hurdal">>,<<"no">>]) -> e(S); +m(S = [<<"hurum">>,<<"no">>]) -> e(S); +m(S = [<<"hvaler">>,<<"no">>]) -> e(S); +m(S = [<<"hyllestad">>,<<"no">>]) -> e(S); +m(S = [<<"hagebostad">>,<<"no">>]) -> e(S); +m(S = [<<"xn--hgebostad-g3a">>,<<"no">>]) -> e(S); +m(S = [<<"hoyanger">>,<<"no">>]) -> e(S); +m(S = [<<"xn--hyanger-q1a">>,<<"no">>]) -> e(S); +m(S = [<<"hoylandet">>,<<"no">>]) -> e(S); +m(S = [<<"xn--hylandet-54a">>,<<"no">>]) -> e(S); +m(S = [<<"ha">>,<<"no">>]) -> e(S); +m(S = [<<"xn--h-2fa">>,<<"no">>]) -> e(S); +m(S = [<<"ibestad">>,<<"no">>]) -> e(S); +m(S = [<<"inderoy">>,<<"no">>]) -> e(S); +m(S = [<<"xn--indery-fya">>,<<"no">>]) -> e(S); +m(S = [<<"iveland">>,<<"no">>]) -> e(S); +m(S = [<<"jevnaker">>,<<"no">>]) -> e(S); +m(S = [<<"jondal">>,<<"no">>]) -> e(S); +m(S = [<<"jolster">>,<<"no">>]) -> e(S); +m(S = [<<"xn--jlster-bya">>,<<"no">>]) -> e(S); +m(S = [<<"karasjok">>,<<"no">>]) -> e(S); +m(S = [<<"karasjohka">>,<<"no">>]) -> e(S); +m(S = [<<"xn--krjohka-hwab49j">>,<<"no">>]) -> e(S); +m(S = [<<"karlsoy">>,<<"no">>]) -> e(S); +m(S = [<<"galsa">>,<<"no">>]) -> e(S); +m(S = [<<"xn--gls-elac">>,<<"no">>]) -> e(S); +m(S = [<<"karmoy">>,<<"no">>]) -> e(S); +m(S = [<<"xn--karmy-yua">>,<<"no">>]) -> e(S); +m(S = [<<"kautokeino">>,<<"no">>]) -> e(S); +m(S = [<<"guovdageaidnu">>,<<"no">>]) -> e(S); +m(S = [<<"klepp">>,<<"no">>]) -> e(S); +m(S = [<<"klabu">>,<<"no">>]) -> e(S); +m(S = [<<"xn--klbu-woa">>,<<"no">>]) -> e(S); +m(S = [<<"kongsberg">>,<<"no">>]) -> e(S); +m(S = [<<"kongsvinger">>,<<"no">>]) -> e(S); +m(S = [<<"kragero">>,<<"no">>]) -> e(S); +m(S = [<<"xn--krager-gya">>,<<"no">>]) -> e(S); +m(S = [<<"kristiansand">>,<<"no">>]) -> e(S); +m(S = [<<"kristiansund">>,<<"no">>]) -> e(S); +m(S = [<<"krodsherad">>,<<"no">>]) -> e(S); +m(S = [<<"xn--krdsherad-m8a">>,<<"no">>]) -> e(S); +m(S = [<<"kvalsund">>,<<"no">>]) -> e(S); +m(S = [<<"rahkkeravju">>,<<"no">>]) -> e(S); +m(S = [<<"xn--rhkkervju-01af">>,<<"no">>]) -> e(S); +m(S = [<<"kvam">>,<<"no">>]) -> e(S); +m(S = [<<"kvinesdal">>,<<"no">>]) -> e(S); +m(S = [<<"kvinnherad">>,<<"no">>]) -> e(S); +m(S = [<<"kviteseid">>,<<"no">>]) -> e(S); +m(S = [<<"kvitsoy">>,<<"no">>]) -> e(S); +m(S = [<<"xn--kvitsy-fya">>,<<"no">>]) -> e(S); +m(S = [<<"kvafjord">>,<<"no">>]) -> e(S); +m(S = [<<"xn--kvfjord-nxa">>,<<"no">>]) -> e(S); +m(S = [<<"giehtavuoatna">>,<<"no">>]) -> e(S); +m(S = [<<"kvanangen">>,<<"no">>]) -> e(S); +m(S = [<<"xn--kvnangen-k0a">>,<<"no">>]) -> e(S); +m(S = [<<"navuotna">>,<<"no">>]) -> e(S); +m(S = [<<"xn--nvuotna-hwa">>,<<"no">>]) -> e(S); +m(S = [<<"kafjord">>,<<"no">>]) -> e(S); +m(S = [<<"xn--kfjord-iua">>,<<"no">>]) -> e(S); +m(S = [<<"gaivuotna">>,<<"no">>]) -> e(S); +m(S = [<<"xn--givuotna-8ya">>,<<"no">>]) -> e(S); +m(S = [<<"larvik">>,<<"no">>]) -> e(S); +m(S = [<<"lavangen">>,<<"no">>]) -> e(S); +m(S = [<<"lavagis">>,<<"no">>]) -> e(S); +m(S = [<<"loabat">>,<<"no">>]) -> e(S); +m(S = [<<"xn--loabt-0qa">>,<<"no">>]) -> e(S); +m(S = [<<"lebesby">>,<<"no">>]) -> e(S); +m(S = [<<"davvesiida">>,<<"no">>]) -> e(S); +m(S = [<<"leikanger">>,<<"no">>]) -> e(S); +m(S = [<<"leirfjord">>,<<"no">>]) -> e(S); +m(S = [<<"leka">>,<<"no">>]) -> e(S); +m(S = [<<"leksvik">>,<<"no">>]) -> e(S); +m(S = [<<"lenvik">>,<<"no">>]) -> e(S); +m(S = [<<"leangaviika">>,<<"no">>]) -> e(S); +m(S = [<<"xn--leagaviika-52b">>,<<"no">>]) -> e(S); +m(S = [<<"lesja">>,<<"no">>]) -> e(S); +m(S = [<<"levanger">>,<<"no">>]) -> e(S); +m(S = [<<"lier">>,<<"no">>]) -> e(S); +m(S = [<<"lierne">>,<<"no">>]) -> e(S); +m(S = [<<"lillehammer">>,<<"no">>]) -> e(S); +m(S = [<<"lillesand">>,<<"no">>]) -> e(S); +m(S = [<<"lindesnes">>,<<"no">>]) -> e(S); +m(S = [<<"lindas">>,<<"no">>]) -> e(S); +m(S = [<<"xn--linds-pra">>,<<"no">>]) -> e(S); +m(S = [<<"lom">>,<<"no">>]) -> e(S); +m(S = [<<"loppa">>,<<"no">>]) -> e(S); +m(S = [<<"lahppi">>,<<"no">>]) -> e(S); +m(S = [<<"xn--lhppi-xqa">>,<<"no">>]) -> e(S); +m(S = [<<"lund">>,<<"no">>]) -> e(S); +m(S = [<<"lunner">>,<<"no">>]) -> e(S); +m(S = [<<"luroy">>,<<"no">>]) -> e(S); +m(S = [<<"xn--lury-ira">>,<<"no">>]) -> e(S); +m(S = [<<"luster">>,<<"no">>]) -> e(S); +m(S = [<<"lyngdal">>,<<"no">>]) -> e(S); +m(S = [<<"lyngen">>,<<"no">>]) -> e(S); +m(S = [<<"ivgu">>,<<"no">>]) -> e(S); +m(S = [<<"lardal">>,<<"no">>]) -> e(S); +m(S = [<<"lerdal">>,<<"no">>]) -> e(S); +m(S = [<<"xn--lrdal-sra">>,<<"no">>]) -> e(S); +m(S = [<<"lodingen">>,<<"no">>]) -> e(S); +m(S = [<<"xn--ldingen-q1a">>,<<"no">>]) -> e(S); +m(S = [<<"lorenskog">>,<<"no">>]) -> e(S); +m(S = [<<"xn--lrenskog-54a">>,<<"no">>]) -> e(S); +m(S = [<<"loten">>,<<"no">>]) -> e(S); +m(S = [<<"xn--lten-gra">>,<<"no">>]) -> e(S); +m(S = [<<"malvik">>,<<"no">>]) -> e(S); +m(S = [<<"masoy">>,<<"no">>]) -> e(S); +m(S = [<<"xn--msy-ula0h">>,<<"no">>]) -> e(S); +m(S = [<<"muosat">>,<<"no">>]) -> e(S); +m(S = [<<"xn--muost-0qa">>,<<"no">>]) -> e(S); +m(S = [<<"mandal">>,<<"no">>]) -> e(S); +m(S = [<<"marker">>,<<"no">>]) -> e(S); +m(S = [<<"marnardal">>,<<"no">>]) -> e(S); +m(S = [<<"masfjorden">>,<<"no">>]) -> e(S); +m(S = [<<"meland">>,<<"no">>]) -> e(S); +m(S = [<<"meldal">>,<<"no">>]) -> e(S); +m(S = [<<"melhus">>,<<"no">>]) -> e(S); +m(S = [<<"meloy">>,<<"no">>]) -> e(S); +m(S = [<<"xn--mely-ira">>,<<"no">>]) -> e(S); +m(S = [<<"meraker">>,<<"no">>]) -> e(S); +m(S = [<<"xn--merker-kua">>,<<"no">>]) -> e(S); +m(S = [<<"moareke">>,<<"no">>]) -> e(S); +m(S = [<<"xn--moreke-jua">>,<<"no">>]) -> e(S); +m(S = [<<"midsund">>,<<"no">>]) -> e(S); +m(S = [<<"midtre-gauldal">>,<<"no">>]) -> e(S); +m(S = [<<"modalen">>,<<"no">>]) -> e(S); +m(S = [<<"modum">>,<<"no">>]) -> e(S); +m(S = [<<"molde">>,<<"no">>]) -> e(S); +m(S = [<<"moskenes">>,<<"no">>]) -> e(S); +m(S = [<<"moss">>,<<"no">>]) -> e(S); +m(S = [<<"mosvik">>,<<"no">>]) -> e(S); +m(S = [<<"malselv">>,<<"no">>]) -> e(S); +m(S = [<<"xn--mlselv-iua">>,<<"no">>]) -> e(S); +m(S = [<<"malatvuopmi">>,<<"no">>]) -> e(S); +m(S = [<<"xn--mlatvuopmi-s4a">>,<<"no">>]) -> e(S); +m(S = [<<"namdalseid">>,<<"no">>]) -> e(S); +m(S = [<<"aejrie">>,<<"no">>]) -> e(S); +m(S = [<<"namsos">>,<<"no">>]) -> e(S); +m(S = [<<"namsskogan">>,<<"no">>]) -> e(S); +m(S = [<<"naamesjevuemie">>,<<"no">>]) -> e(S); +m(S = [<<"xn--nmesjevuemie-tcba">>,<<"no">>]) -> e(S); +m(S = [<<"laakesvuemie">>,<<"no">>]) -> e(S); +m(S = [<<"nannestad">>,<<"no">>]) -> e(S); +m(S = [<<"narvik">>,<<"no">>]) -> e(S); +m(S = [<<"narviika">>,<<"no">>]) -> e(S); +m(S = [<<"naustdal">>,<<"no">>]) -> e(S); +m(S = [<<"nedre-eiker">>,<<"no">>]) -> e(S); +m(S = [<<"nes">>,<<"akershus">>,<<"no">>]) -> e(S); +m(S = [<<"nes">>,<<"buskerud">>,<<"no">>]) -> e(S); +m(S = [<<"nesna">>,<<"no">>]) -> e(S); +m(S = [<<"nesodden">>,<<"no">>]) -> e(S); +m(S = [<<"nesseby">>,<<"no">>]) -> e(S); +m(S = [<<"unjarga">>,<<"no">>]) -> e(S); +m(S = [<<"xn--unjrga-rta">>,<<"no">>]) -> e(S); +m(S = [<<"nesset">>,<<"no">>]) -> e(S); +m(S = [<<"nissedal">>,<<"no">>]) -> e(S); +m(S = [<<"nittedal">>,<<"no">>]) -> e(S); +m(S = [<<"nord-aurdal">>,<<"no">>]) -> e(S); +m(S = [<<"nord-fron">>,<<"no">>]) -> e(S); +m(S = [<<"nord-odal">>,<<"no">>]) -> e(S); +m(S = [<<"norddal">>,<<"no">>]) -> e(S); +m(S = [<<"nordkapp">>,<<"no">>]) -> e(S); +m(S = [<<"davvenjarga">>,<<"no">>]) -> e(S); +m(S = [<<"xn--davvenjrga-y4a">>,<<"no">>]) -> e(S); +m(S = [<<"nordre-land">>,<<"no">>]) -> e(S); +m(S = [<<"nordreisa">>,<<"no">>]) -> e(S); +m(S = [<<"raisa">>,<<"no">>]) -> e(S); +m(S = [<<"xn--risa-5na">>,<<"no">>]) -> e(S); +m(S = [<<"nore-og-uvdal">>,<<"no">>]) -> e(S); +m(S = [<<"notodden">>,<<"no">>]) -> e(S); +m(S = [<<"naroy">>,<<"no">>]) -> e(S); +m(S = [<<"xn--nry-yla5g">>,<<"no">>]) -> e(S); +m(S = [<<"notteroy">>,<<"no">>]) -> e(S); +m(S = [<<"xn--nttery-byae">>,<<"no">>]) -> e(S); +m(S = [<<"odda">>,<<"no">>]) -> e(S); +m(S = [<<"oksnes">>,<<"no">>]) -> e(S); +m(S = [<<"xn--ksnes-uua">>,<<"no">>]) -> e(S); +m(S = [<<"oppdal">>,<<"no">>]) -> e(S); +m(S = [<<"oppegard">>,<<"no">>]) -> e(S); +m(S = [<<"xn--oppegrd-ixa">>,<<"no">>]) -> e(S); +m(S = [<<"orkdal">>,<<"no">>]) -> e(S); +m(S = [<<"orland">>,<<"no">>]) -> e(S); +m(S = [<<"xn--rland-uua">>,<<"no">>]) -> e(S); +m(S = [<<"orskog">>,<<"no">>]) -> e(S); +m(S = [<<"xn--rskog-uua">>,<<"no">>]) -> e(S); +m(S = [<<"orsta">>,<<"no">>]) -> e(S); +m(S = [<<"xn--rsta-fra">>,<<"no">>]) -> e(S); +m(S = [<<"os">>,<<"hedmark">>,<<"no">>]) -> e(S); +m(S = [<<"os">>,<<"hordaland">>,<<"no">>]) -> e(S); +m(S = [<<"osen">>,<<"no">>]) -> e(S); +m(S = [<<"osteroy">>,<<"no">>]) -> e(S); +m(S = [<<"xn--ostery-fya">>,<<"no">>]) -> e(S); +m(S = [<<"ostre-toten">>,<<"no">>]) -> e(S); +m(S = [<<"xn--stre-toten-zcb">>,<<"no">>]) -> e(S); +m(S = [<<"overhalla">>,<<"no">>]) -> e(S); +m(S = [<<"ovre-eiker">>,<<"no">>]) -> e(S); +m(S = [<<"xn--vre-eiker-k8a">>,<<"no">>]) -> e(S); +m(S = [<<"oyer">>,<<"no">>]) -> e(S); +m(S = [<<"xn--yer-zna">>,<<"no">>]) -> e(S); +m(S = [<<"oygarden">>,<<"no">>]) -> e(S); +m(S = [<<"xn--ygarden-p1a">>,<<"no">>]) -> e(S); +m(S = [<<"oystre-slidre">>,<<"no">>]) -> e(S); +m(S = [<<"xn--ystre-slidre-ujb">>,<<"no">>]) -> e(S); +m(S = [<<"porsanger">>,<<"no">>]) -> e(S); +m(S = [<<"porsangu">>,<<"no">>]) -> e(S); +m(S = [<<"xn--porsgu-sta26f">>,<<"no">>]) -> e(S); +m(S = [<<"porsgrunn">>,<<"no">>]) -> e(S); +m(S = [<<"radoy">>,<<"no">>]) -> e(S); +m(S = [<<"xn--rady-ira">>,<<"no">>]) -> e(S); +m(S = [<<"rakkestad">>,<<"no">>]) -> e(S); +m(S = [<<"rana">>,<<"no">>]) -> e(S); +m(S = [<<"ruovat">>,<<"no">>]) -> e(S); +m(S = [<<"randaberg">>,<<"no">>]) -> e(S); +m(S = [<<"rauma">>,<<"no">>]) -> e(S); +m(S = [<<"rendalen">>,<<"no">>]) -> e(S); +m(S = [<<"rennebu">>,<<"no">>]) -> e(S); +m(S = [<<"rennesoy">>,<<"no">>]) -> e(S); +m(S = [<<"xn--rennesy-v1a">>,<<"no">>]) -> e(S); +m(S = [<<"rindal">>,<<"no">>]) -> e(S); +m(S = [<<"ringebu">>,<<"no">>]) -> e(S); +m(S = [<<"ringerike">>,<<"no">>]) -> e(S); +m(S = [<<"ringsaker">>,<<"no">>]) -> e(S); +m(S = [<<"rissa">>,<<"no">>]) -> e(S); +m(S = [<<"risor">>,<<"no">>]) -> e(S); +m(S = [<<"xn--risr-ira">>,<<"no">>]) -> e(S); +m(S = [<<"roan">>,<<"no">>]) -> e(S); +m(S = [<<"rollag">>,<<"no">>]) -> e(S); +m(S = [<<"rygge">>,<<"no">>]) -> e(S); +m(S = [<<"ralingen">>,<<"no">>]) -> e(S); +m(S = [<<"xn--rlingen-mxa">>,<<"no">>]) -> e(S); +m(S = [<<"rodoy">>,<<"no">>]) -> e(S); +m(S = [<<"xn--rdy-0nab">>,<<"no">>]) -> e(S); +m(S = [<<"romskog">>,<<"no">>]) -> e(S); +m(S = [<<"xn--rmskog-bya">>,<<"no">>]) -> e(S); +m(S = [<<"roros">>,<<"no">>]) -> e(S); +m(S = [<<"xn--rros-gra">>,<<"no">>]) -> e(S); +m(S = [<<"rost">>,<<"no">>]) -> e(S); +m(S = [<<"xn--rst-0na">>,<<"no">>]) -> e(S); +m(S = [<<"royken">>,<<"no">>]) -> e(S); +m(S = [<<"xn--ryken-vua">>,<<"no">>]) -> e(S); +m(S = [<<"royrvik">>,<<"no">>]) -> e(S); +m(S = [<<"xn--ryrvik-bya">>,<<"no">>]) -> e(S); +m(S = [<<"rade">>,<<"no">>]) -> e(S); +m(S = [<<"xn--rde-ula">>,<<"no">>]) -> e(S); +m(S = [<<"salangen">>,<<"no">>]) -> e(S); +m(S = [<<"siellak">>,<<"no">>]) -> e(S); +m(S = [<<"saltdal">>,<<"no">>]) -> e(S); +m(S = [<<"salat">>,<<"no">>]) -> e(S); +m(S = [<<"xn--slt-elab">>,<<"no">>]) -> e(S); +m(S = [<<"xn--slat-5na">>,<<"no">>]) -> e(S); +m(S = [<<"samnanger">>,<<"no">>]) -> e(S); +m(S = [<<"sande">>,<<"more-og-romsdal">>,<<"no">>]) -> e(S); +m(S = [<<"sande">>,<<"xn--mre-og-romsdal-qqb">>,<<"no">>]) -> e(S); +m(S = [<<"sande">>,<<"vestfold">>,<<"no">>]) -> e(S); +m(S = [<<"sandefjord">>,<<"no">>]) -> e(S); +m(S = [<<"sandnes">>,<<"no">>]) -> e(S); +m(S = [<<"sandoy">>,<<"no">>]) -> e(S); +m(S = [<<"xn--sandy-yua">>,<<"no">>]) -> e(S); +m(S = [<<"sarpsborg">>,<<"no">>]) -> e(S); +m(S = [<<"sauda">>,<<"no">>]) -> e(S); +m(S = [<<"sauherad">>,<<"no">>]) -> e(S); +m(S = [<<"sel">>,<<"no">>]) -> e(S); +m(S = [<<"selbu">>,<<"no">>]) -> e(S); +m(S = [<<"selje">>,<<"no">>]) -> e(S); +m(S = [<<"seljord">>,<<"no">>]) -> e(S); +m(S = [<<"sigdal">>,<<"no">>]) -> e(S); +m(S = [<<"siljan">>,<<"no">>]) -> e(S); +m(S = [<<"sirdal">>,<<"no">>]) -> e(S); +m(S = [<<"skaun">>,<<"no">>]) -> e(S); +m(S = [<<"skedsmo">>,<<"no">>]) -> e(S); +m(S = [<<"ski">>,<<"no">>]) -> e(S); +m(S = [<<"skien">>,<<"no">>]) -> e(S); +m(S = [<<"skiptvet">>,<<"no">>]) -> e(S); +m(S = [<<"skjervoy">>,<<"no">>]) -> e(S); +m(S = [<<"xn--skjervy-v1a">>,<<"no">>]) -> e(S); +m(S = [<<"skierva">>,<<"no">>]) -> e(S); +m(S = [<<"xn--skierv-uta">>,<<"no">>]) -> e(S); +m(S = [<<"skjak">>,<<"no">>]) -> e(S); +m(S = [<<"xn--skjk-soa">>,<<"no">>]) -> e(S); +m(S = [<<"skodje">>,<<"no">>]) -> e(S); +m(S = [<<"skanland">>,<<"no">>]) -> e(S); +m(S = [<<"xn--sknland-fxa">>,<<"no">>]) -> e(S); +m(S = [<<"skanit">>,<<"no">>]) -> e(S); +m(S = [<<"xn--sknit-yqa">>,<<"no">>]) -> e(S); +m(S = [<<"smola">>,<<"no">>]) -> e(S); +m(S = [<<"xn--smla-hra">>,<<"no">>]) -> e(S); +m(S = [<<"snillfjord">>,<<"no">>]) -> e(S); +m(S = [<<"snasa">>,<<"no">>]) -> e(S); +m(S = [<<"xn--snsa-roa">>,<<"no">>]) -> e(S); +m(S = [<<"snoasa">>,<<"no">>]) -> e(S); +m(S = [<<"snaase">>,<<"no">>]) -> e(S); +m(S = [<<"xn--snase-nra">>,<<"no">>]) -> e(S); +m(S = [<<"sogndal">>,<<"no">>]) -> e(S); +m(S = [<<"sokndal">>,<<"no">>]) -> e(S); +m(S = [<<"sola">>,<<"no">>]) -> e(S); +m(S = [<<"solund">>,<<"no">>]) -> e(S); +m(S = [<<"songdalen">>,<<"no">>]) -> e(S); +m(S = [<<"sortland">>,<<"no">>]) -> e(S); +m(S = [<<"spydeberg">>,<<"no">>]) -> e(S); +m(S = [<<"stange">>,<<"no">>]) -> e(S); +m(S = [<<"stavanger">>,<<"no">>]) -> e(S); +m(S = [<<"steigen">>,<<"no">>]) -> e(S); +m(S = [<<"steinkjer">>,<<"no">>]) -> e(S); +m(S = [<<"stjordal">>,<<"no">>]) -> e(S); +m(S = [<<"xn--stjrdal-s1a">>,<<"no">>]) -> e(S); +m(S = [<<"stokke">>,<<"no">>]) -> e(S); +m(S = [<<"stor-elvdal">>,<<"no">>]) -> e(S); +m(S = [<<"stord">>,<<"no">>]) -> e(S); +m(S = [<<"stordal">>,<<"no">>]) -> e(S); +m(S = [<<"storfjord">>,<<"no">>]) -> e(S); +m(S = [<<"omasvuotna">>,<<"no">>]) -> e(S); +m(S = [<<"strand">>,<<"no">>]) -> e(S); +m(S = [<<"stranda">>,<<"no">>]) -> e(S); +m(S = [<<"stryn">>,<<"no">>]) -> e(S); +m(S = [<<"sula">>,<<"no">>]) -> e(S); +m(S = [<<"suldal">>,<<"no">>]) -> e(S); +m(S = [<<"sund">>,<<"no">>]) -> e(S); +m(S = [<<"sunndal">>,<<"no">>]) -> e(S); +m(S = [<<"surnadal">>,<<"no">>]) -> e(S); +m(S = [<<"sveio">>,<<"no">>]) -> e(S); +m(S = [<<"svelvik">>,<<"no">>]) -> e(S); +m(S = [<<"sykkylven">>,<<"no">>]) -> e(S); +m(S = [<<"sogne">>,<<"no">>]) -> e(S); +m(S = [<<"xn--sgne-gra">>,<<"no">>]) -> e(S); +m(S = [<<"somna">>,<<"no">>]) -> e(S); +m(S = [<<"xn--smna-gra">>,<<"no">>]) -> e(S); +m(S = [<<"sondre-land">>,<<"no">>]) -> e(S); +m(S = [<<"xn--sndre-land-0cb">>,<<"no">>]) -> e(S); +m(S = [<<"sor-aurdal">>,<<"no">>]) -> e(S); +m(S = [<<"xn--sr-aurdal-l8a">>,<<"no">>]) -> e(S); +m(S = [<<"sor-fron">>,<<"no">>]) -> e(S); +m(S = [<<"xn--sr-fron-q1a">>,<<"no">>]) -> e(S); +m(S = [<<"sor-odal">>,<<"no">>]) -> e(S); +m(S = [<<"xn--sr-odal-q1a">>,<<"no">>]) -> e(S); +m(S = [<<"sor-varanger">>,<<"no">>]) -> e(S); +m(S = [<<"xn--sr-varanger-ggb">>,<<"no">>]) -> e(S); +m(S = [<<"matta-varjjat">>,<<"no">>]) -> e(S); +m(S = [<<"xn--mtta-vrjjat-k7af">>,<<"no">>]) -> e(S); +m(S = [<<"sorfold">>,<<"no">>]) -> e(S); +m(S = [<<"xn--srfold-bya">>,<<"no">>]) -> e(S); +m(S = [<<"sorreisa">>,<<"no">>]) -> e(S); +m(S = [<<"xn--srreisa-q1a">>,<<"no">>]) -> e(S); +m(S = [<<"sorum">>,<<"no">>]) -> e(S); +m(S = [<<"xn--srum-gra">>,<<"no">>]) -> e(S); +m(S = [<<"tana">>,<<"no">>]) -> e(S); +m(S = [<<"deatnu">>,<<"no">>]) -> e(S); +m(S = [<<"time">>,<<"no">>]) -> e(S); +m(S = [<<"tingvoll">>,<<"no">>]) -> e(S); +m(S = [<<"tinn">>,<<"no">>]) -> e(S); +m(S = [<<"tjeldsund">>,<<"no">>]) -> e(S); +m(S = [<<"dielddanuorri">>,<<"no">>]) -> e(S); +m(S = [<<"tjome">>,<<"no">>]) -> e(S); +m(S = [<<"xn--tjme-hra">>,<<"no">>]) -> e(S); +m(S = [<<"tokke">>,<<"no">>]) -> e(S); +m(S = [<<"tolga">>,<<"no">>]) -> e(S); +m(S = [<<"torsken">>,<<"no">>]) -> e(S); +m(S = [<<"tranoy">>,<<"no">>]) -> e(S); +m(S = [<<"xn--trany-yua">>,<<"no">>]) -> e(S); +m(S = [<<"tromso">>,<<"no">>]) -> e(S); +m(S = [<<"xn--troms-zua">>,<<"no">>]) -> e(S); +m(S = [<<"tromsa">>,<<"no">>]) -> e(S); +m(S = [<<"romsa">>,<<"no">>]) -> e(S); +m(S = [<<"trondheim">>,<<"no">>]) -> e(S); +m(S = [<<"troandin">>,<<"no">>]) -> e(S); +m(S = [<<"trysil">>,<<"no">>]) -> e(S); +m(S = [<<"trana">>,<<"no">>]) -> e(S); +m(S = [<<"xn--trna-woa">>,<<"no">>]) -> e(S); +m(S = [<<"trogstad">>,<<"no">>]) -> e(S); +m(S = [<<"xn--trgstad-r1a">>,<<"no">>]) -> e(S); +m(S = [<<"tvedestrand">>,<<"no">>]) -> e(S); +m(S = [<<"tydal">>,<<"no">>]) -> e(S); +m(S = [<<"tynset">>,<<"no">>]) -> e(S); +m(S = [<<"tysfjord">>,<<"no">>]) -> e(S); +m(S = [<<"divtasvuodna">>,<<"no">>]) -> e(S); +m(S = [<<"divttasvuotna">>,<<"no">>]) -> e(S); +m(S = [<<"tysnes">>,<<"no">>]) -> e(S); +m(S = [<<"tysvar">>,<<"no">>]) -> e(S); +m(S = [<<"xn--tysvr-vra">>,<<"no">>]) -> e(S); +m(S = [<<"tonsberg">>,<<"no">>]) -> e(S); +m(S = [<<"xn--tnsberg-q1a">>,<<"no">>]) -> e(S); +m(S = [<<"ullensaker">>,<<"no">>]) -> e(S); +m(S = [<<"ullensvang">>,<<"no">>]) -> e(S); +m(S = [<<"ulvik">>,<<"no">>]) -> e(S); +m(S = [<<"utsira">>,<<"no">>]) -> e(S); +m(S = [<<"vadso">>,<<"no">>]) -> e(S); +m(S = [<<"xn--vads-jra">>,<<"no">>]) -> e(S); +m(S = [<<"cahcesuolo">>,<<"no">>]) -> e(S); +m(S = [<<"xn--hcesuolo-7ya35b">>,<<"no">>]) -> e(S); +m(S = [<<"vaksdal">>,<<"no">>]) -> e(S); +m(S = [<<"valle">>,<<"no">>]) -> e(S); +m(S = [<<"vang">>,<<"no">>]) -> e(S); +m(S = [<<"vanylven">>,<<"no">>]) -> e(S); +m(S = [<<"vardo">>,<<"no">>]) -> e(S); +m(S = [<<"xn--vard-jra">>,<<"no">>]) -> e(S); +m(S = [<<"varggat">>,<<"no">>]) -> e(S); +m(S = [<<"xn--vrggt-xqad">>,<<"no">>]) -> e(S); +m(S = [<<"vefsn">>,<<"no">>]) -> e(S); +m(S = [<<"vaapste">>,<<"no">>]) -> e(S); +m(S = [<<"vega">>,<<"no">>]) -> e(S); +m(S = [<<"vegarshei">>,<<"no">>]) -> e(S); +m(S = [<<"xn--vegrshei-c0a">>,<<"no">>]) -> e(S); +m(S = [<<"vennesla">>,<<"no">>]) -> e(S); +m(S = [<<"verdal">>,<<"no">>]) -> e(S); +m(S = [<<"verran">>,<<"no">>]) -> e(S); +m(S = [<<"vestby">>,<<"no">>]) -> e(S); +m(S = [<<"vestnes">>,<<"no">>]) -> e(S); +m(S = [<<"vestre-slidre">>,<<"no">>]) -> e(S); +m(S = [<<"vestre-toten">>,<<"no">>]) -> e(S); +m(S = [<<"vestvagoy">>,<<"no">>]) -> e(S); +m(S = [<<"xn--vestvgy-ixa6o">>,<<"no">>]) -> e(S); +m(S = [<<"vevelstad">>,<<"no">>]) -> e(S); +m(S = [<<"vik">>,<<"no">>]) -> e(S); +m(S = [<<"vikna">>,<<"no">>]) -> e(S); +m(S = [<<"vindafjord">>,<<"no">>]) -> e(S); +m(S = [<<"volda">>,<<"no">>]) -> e(S); +m(S = [<<"voss">>,<<"no">>]) -> e(S); +m(S = [<<"varoy">>,<<"no">>]) -> e(S); +m(S = [<<"xn--vry-yla5g">>,<<"no">>]) -> e(S); +m(S = [<<"vagan">>,<<"no">>]) -> e(S); +m(S = [<<"xn--vgan-qoa">>,<<"no">>]) -> e(S); +m(S = [<<"voagat">>,<<"no">>]) -> e(S); +m(S = [<<"vagsoy">>,<<"no">>]) -> e(S); +m(S = [<<"xn--vgsy-qoa0j">>,<<"no">>]) -> e(S); +m(S = [<<"vaga">>,<<"no">>]) -> e(S); +m(S = [<<"xn--vg-yiab">>,<<"no">>]) -> e(S); +m(S = [<<"valer">>,<<"ostfold">>,<<"no">>]) -> e(S); +m(S = [<<"xn--vler-qoa">>,<<"xn--stfold-9xa">>,<<"no">>]) -> e(S); +m(S = [<<"valer">>,<<"hedmark">>,<<"no">>]) -> e(S); +m(S = [<<"xn--vler-qoa">>,<<"hedmark">>,<<"no">>]) -> e(S); +m(S = [_,<<"np">>]) -> e(S); +m(S = [<<"nr">>]) -> e(S); +m(S = [<<"biz">>,<<"nr">>]) -> e(S); +m(S = [<<"info">>,<<"nr">>]) -> e(S); +m(S = [<<"gov">>,<<"nr">>]) -> e(S); +m(S = [<<"edu">>,<<"nr">>]) -> e(S); +m(S = [<<"org">>,<<"nr">>]) -> e(S); +m(S = [<<"net">>,<<"nr">>]) -> e(S); +m(S = [<<"com">>,<<"nr">>]) -> e(S); +m(S = [<<"nu">>]) -> e(S); +m(S = [<<"nz">>]) -> e(S); +m(S = [<<"ac">>,<<"nz">>]) -> e(S); +m(S = [<<"co">>,<<"nz">>]) -> e(S); +m(S = [<<"cri">>,<<"nz">>]) -> e(S); +m(S = [<<"geek">>,<<"nz">>]) -> e(S); +m(S = [<<"gen">>,<<"nz">>]) -> e(S); +m(S = [<<"govt">>,<<"nz">>]) -> e(S); +m(S = [<<"health">>,<<"nz">>]) -> e(S); +m(S = [<<"iwi">>,<<"nz">>]) -> e(S); +m(S = [<<"kiwi">>,<<"nz">>]) -> e(S); +m(S = [<<"maori">>,<<"nz">>]) -> e(S); +m(S = [<<"mil">>,<<"nz">>]) -> e(S); +m(S = [<<"xn--mori-qsa">>,<<"nz">>]) -> e(S); +m(S = [<<"net">>,<<"nz">>]) -> e(S); +m(S = [<<"org">>,<<"nz">>]) -> e(S); +m(S = [<<"parliament">>,<<"nz">>]) -> e(S); +m(S = [<<"school">>,<<"nz">>]) -> e(S); +m(S = [<<"om">>]) -> e(S); +m(S = [<<"co">>,<<"om">>]) -> e(S); +m(S = [<<"com">>,<<"om">>]) -> e(S); +m(S = [<<"edu">>,<<"om">>]) -> e(S); +m(S = [<<"gov">>,<<"om">>]) -> e(S); +m(S = [<<"med">>,<<"om">>]) -> e(S); +m(S = [<<"museum">>,<<"om">>]) -> e(S); +m(S = [<<"net">>,<<"om">>]) -> e(S); +m(S = [<<"org">>,<<"om">>]) -> e(S); +m(S = [<<"pro">>,<<"om">>]) -> e(S); +m(S = [<<"onion">>]) -> e(S); +m(S = [<<"org">>]) -> e(S); +m(S = [<<"pa">>]) -> e(S); +m(S = [<<"ac">>,<<"pa">>]) -> e(S); +m(S = [<<"gob">>,<<"pa">>]) -> e(S); +m(S = [<<"com">>,<<"pa">>]) -> e(S); +m(S = [<<"org">>,<<"pa">>]) -> e(S); +m(S = [<<"sld">>,<<"pa">>]) -> e(S); +m(S = [<<"edu">>,<<"pa">>]) -> e(S); +m(S = [<<"net">>,<<"pa">>]) -> e(S); +m(S = [<<"ing">>,<<"pa">>]) -> e(S); +m(S = [<<"abo">>,<<"pa">>]) -> e(S); +m(S = [<<"med">>,<<"pa">>]) -> e(S); +m(S = [<<"nom">>,<<"pa">>]) -> e(S); +m(S = [<<"pe">>]) -> e(S); +m(S = [<<"edu">>,<<"pe">>]) -> e(S); +m(S = [<<"gob">>,<<"pe">>]) -> e(S); +m(S = [<<"nom">>,<<"pe">>]) -> e(S); +m(S = [<<"mil">>,<<"pe">>]) -> e(S); +m(S = [<<"org">>,<<"pe">>]) -> e(S); +m(S = [<<"com">>,<<"pe">>]) -> e(S); +m(S = [<<"net">>,<<"pe">>]) -> e(S); +m(S = [<<"pf">>]) -> e(S); +m(S = [<<"com">>,<<"pf">>]) -> e(S); +m(S = [<<"org">>,<<"pf">>]) -> e(S); +m(S = [<<"edu">>,<<"pf">>]) -> e(S); +m(S = [_,<<"pg">>]) -> e(S); +m(S = [<<"ph">>]) -> e(S); +m(S = [<<"com">>,<<"ph">>]) -> e(S); +m(S = [<<"net">>,<<"ph">>]) -> e(S); +m(S = [<<"org">>,<<"ph">>]) -> e(S); +m(S = [<<"gov">>,<<"ph">>]) -> e(S); +m(S = [<<"edu">>,<<"ph">>]) -> e(S); +m(S = [<<"ngo">>,<<"ph">>]) -> e(S); +m(S = [<<"mil">>,<<"ph">>]) -> e(S); +m(S = [<<"i">>,<<"ph">>]) -> e(S); +m(S = [<<"pk">>]) -> e(S); +m(S = [<<"com">>,<<"pk">>]) -> e(S); +m(S = [<<"net">>,<<"pk">>]) -> e(S); +m(S = [<<"edu">>,<<"pk">>]) -> e(S); +m(S = [<<"org">>,<<"pk">>]) -> e(S); +m(S = [<<"fam">>,<<"pk">>]) -> e(S); +m(S = [<<"biz">>,<<"pk">>]) -> e(S); +m(S = [<<"web">>,<<"pk">>]) -> e(S); +m(S = [<<"gov">>,<<"pk">>]) -> e(S); +m(S = [<<"gob">>,<<"pk">>]) -> e(S); +m(S = [<<"gok">>,<<"pk">>]) -> e(S); +m(S = [<<"gon">>,<<"pk">>]) -> e(S); +m(S = [<<"gop">>,<<"pk">>]) -> e(S); +m(S = [<<"gos">>,<<"pk">>]) -> e(S); +m(S = [<<"info">>,<<"pk">>]) -> e(S); +m(S = [<<"pl">>]) -> e(S); +m(S = [<<"com">>,<<"pl">>]) -> e(S); +m(S = [<<"net">>,<<"pl">>]) -> e(S); +m(S = [<<"org">>,<<"pl">>]) -> e(S); +m(S = [<<"aid">>,<<"pl">>]) -> e(S); +m(S = [<<"agro">>,<<"pl">>]) -> e(S); +m(S = [<<"atm">>,<<"pl">>]) -> e(S); +m(S = [<<"auto">>,<<"pl">>]) -> e(S); +m(S = [<<"biz">>,<<"pl">>]) -> e(S); +m(S = [<<"edu">>,<<"pl">>]) -> e(S); +m(S = [<<"gmina">>,<<"pl">>]) -> e(S); +m(S = [<<"gsm">>,<<"pl">>]) -> e(S); +m(S = [<<"info">>,<<"pl">>]) -> e(S); +m(S = [<<"mail">>,<<"pl">>]) -> e(S); +m(S = [<<"miasta">>,<<"pl">>]) -> e(S); +m(S = [<<"media">>,<<"pl">>]) -> e(S); +m(S = [<<"mil">>,<<"pl">>]) -> e(S); +m(S = [<<"nieruchomosci">>,<<"pl">>]) -> e(S); +m(S = [<<"nom">>,<<"pl">>]) -> e(S); +m(S = [<<"pc">>,<<"pl">>]) -> e(S); +m(S = [<<"powiat">>,<<"pl">>]) -> e(S); +m(S = [<<"priv">>,<<"pl">>]) -> e(S); +m(S = [<<"realestate">>,<<"pl">>]) -> e(S); +m(S = [<<"rel">>,<<"pl">>]) -> e(S); +m(S = [<<"sex">>,<<"pl">>]) -> e(S); +m(S = [<<"shop">>,<<"pl">>]) -> e(S); +m(S = [<<"sklep">>,<<"pl">>]) -> e(S); +m(S = [<<"sos">>,<<"pl">>]) -> e(S); +m(S = [<<"szkola">>,<<"pl">>]) -> e(S); +m(S = [<<"targi">>,<<"pl">>]) -> e(S); +m(S = [<<"tm">>,<<"pl">>]) -> e(S); +m(S = [<<"tourism">>,<<"pl">>]) -> e(S); +m(S = [<<"travel">>,<<"pl">>]) -> e(S); +m(S = [<<"turystyka">>,<<"pl">>]) -> e(S); +m(S = [<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"ap">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"ic">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"is">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"us">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"kmpsp">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"kppsp">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"kwpsp">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"psp">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"wskr">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"kwp">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"mw">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"ug">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"um">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"umig">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"ugim">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"upow">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"uw">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"starostwo">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"pa">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"po">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"psse">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"pup">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"rzgw">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"sa">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"so">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"sr">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"wsa">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"sko">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"uzs">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"wiih">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"winb">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"pinb">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"wios">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"witd">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"wzmiuw">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"piw">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"wiw">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"griw">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"wif">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"oum">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"sdn">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"zp">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"uppo">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"mup">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"wuoz">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"konsulat">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"oirm">>,<<"gov">>,<<"pl">>]) -> e(S); +m(S = [<<"augustow">>,<<"pl">>]) -> e(S); +m(S = [<<"babia-gora">>,<<"pl">>]) -> e(S); +m(S = [<<"bedzin">>,<<"pl">>]) -> e(S); +m(S = [<<"beskidy">>,<<"pl">>]) -> e(S); +m(S = [<<"bialowieza">>,<<"pl">>]) -> e(S); +m(S = [<<"bialystok">>,<<"pl">>]) -> e(S); +m(S = [<<"bielawa">>,<<"pl">>]) -> e(S); +m(S = [<<"bieszczady">>,<<"pl">>]) -> e(S); +m(S = [<<"boleslawiec">>,<<"pl">>]) -> e(S); +m(S = [<<"bydgoszcz">>,<<"pl">>]) -> e(S); +m(S = [<<"bytom">>,<<"pl">>]) -> e(S); +m(S = [<<"cieszyn">>,<<"pl">>]) -> e(S); +m(S = [<<"czeladz">>,<<"pl">>]) -> e(S); +m(S = [<<"czest">>,<<"pl">>]) -> e(S); +m(S = [<<"dlugoleka">>,<<"pl">>]) -> e(S); +m(S = [<<"elblag">>,<<"pl">>]) -> e(S); +m(S = [<<"elk">>,<<"pl">>]) -> e(S); +m(S = [<<"glogow">>,<<"pl">>]) -> e(S); +m(S = [<<"gniezno">>,<<"pl">>]) -> e(S); +m(S = [<<"gorlice">>,<<"pl">>]) -> e(S); +m(S = [<<"grajewo">>,<<"pl">>]) -> e(S); +m(S = [<<"ilawa">>,<<"pl">>]) -> e(S); +m(S = [<<"jaworzno">>,<<"pl">>]) -> e(S); +m(S = [<<"jelenia-gora">>,<<"pl">>]) -> e(S); +m(S = [<<"jgora">>,<<"pl">>]) -> e(S); +m(S = [<<"kalisz">>,<<"pl">>]) -> e(S); +m(S = [<<"kazimierz-dolny">>,<<"pl">>]) -> e(S); +m(S = [<<"karpacz">>,<<"pl">>]) -> e(S); +m(S = [<<"kartuzy">>,<<"pl">>]) -> e(S); +m(S = [<<"kaszuby">>,<<"pl">>]) -> e(S); +m(S = [<<"katowice">>,<<"pl">>]) -> e(S); +m(S = [<<"kepno">>,<<"pl">>]) -> e(S); +m(S = [<<"ketrzyn">>,<<"pl">>]) -> e(S); +m(S = [<<"klodzko">>,<<"pl">>]) -> e(S); +m(S = [<<"kobierzyce">>,<<"pl">>]) -> e(S); +m(S = [<<"kolobrzeg">>,<<"pl">>]) -> e(S); +m(S = [<<"konin">>,<<"pl">>]) -> e(S); +m(S = [<<"konskowola">>,<<"pl">>]) -> e(S); +m(S = [<<"kutno">>,<<"pl">>]) -> e(S); +m(S = [<<"lapy">>,<<"pl">>]) -> e(S); +m(S = [<<"lebork">>,<<"pl">>]) -> e(S); +m(S = [<<"legnica">>,<<"pl">>]) -> e(S); +m(S = [<<"lezajsk">>,<<"pl">>]) -> e(S); +m(S = [<<"limanowa">>,<<"pl">>]) -> e(S); +m(S = [<<"lomza">>,<<"pl">>]) -> e(S); +m(S = [<<"lowicz">>,<<"pl">>]) -> e(S); +m(S = [<<"lubin">>,<<"pl">>]) -> e(S); +m(S = [<<"lukow">>,<<"pl">>]) -> e(S); +m(S = [<<"malbork">>,<<"pl">>]) -> e(S); +m(S = [<<"malopolska">>,<<"pl">>]) -> e(S); +m(S = [<<"mazowsze">>,<<"pl">>]) -> e(S); +m(S = [<<"mazury">>,<<"pl">>]) -> e(S); +m(S = [<<"mielec">>,<<"pl">>]) -> e(S); +m(S = [<<"mielno">>,<<"pl">>]) -> e(S); +m(S = [<<"mragowo">>,<<"pl">>]) -> e(S); +m(S = [<<"naklo">>,<<"pl">>]) -> e(S); +m(S = [<<"nowaruda">>,<<"pl">>]) -> e(S); +m(S = [<<"nysa">>,<<"pl">>]) -> e(S); +m(S = [<<"olawa">>,<<"pl">>]) -> e(S); +m(S = [<<"olecko">>,<<"pl">>]) -> e(S); +m(S = [<<"olkusz">>,<<"pl">>]) -> e(S); +m(S = [<<"olsztyn">>,<<"pl">>]) -> e(S); +m(S = [<<"opoczno">>,<<"pl">>]) -> e(S); +m(S = [<<"opole">>,<<"pl">>]) -> e(S); +m(S = [<<"ostroda">>,<<"pl">>]) -> e(S); +m(S = [<<"ostroleka">>,<<"pl">>]) -> e(S); +m(S = [<<"ostrowiec">>,<<"pl">>]) -> e(S); +m(S = [<<"ostrowwlkp">>,<<"pl">>]) -> e(S); +m(S = [<<"pila">>,<<"pl">>]) -> e(S); +m(S = [<<"pisz">>,<<"pl">>]) -> e(S); +m(S = [<<"podhale">>,<<"pl">>]) -> e(S); +m(S = [<<"podlasie">>,<<"pl">>]) -> e(S); +m(S = [<<"polkowice">>,<<"pl">>]) -> e(S); +m(S = [<<"pomorze">>,<<"pl">>]) -> e(S); +m(S = [<<"pomorskie">>,<<"pl">>]) -> e(S); +m(S = [<<"prochowice">>,<<"pl">>]) -> e(S); +m(S = [<<"pruszkow">>,<<"pl">>]) -> e(S); +m(S = [<<"przeworsk">>,<<"pl">>]) -> e(S); +m(S = [<<"pulawy">>,<<"pl">>]) -> e(S); +m(S = [<<"radom">>,<<"pl">>]) -> e(S); +m(S = [<<"rawa-maz">>,<<"pl">>]) -> e(S); +m(S = [<<"rybnik">>,<<"pl">>]) -> e(S); +m(S = [<<"rzeszow">>,<<"pl">>]) -> e(S); +m(S = [<<"sanok">>,<<"pl">>]) -> e(S); +m(S = [<<"sejny">>,<<"pl">>]) -> e(S); +m(S = [<<"slask">>,<<"pl">>]) -> e(S); +m(S = [<<"slupsk">>,<<"pl">>]) -> e(S); +m(S = [<<"sosnowiec">>,<<"pl">>]) -> e(S); +m(S = [<<"stalowa-wola">>,<<"pl">>]) -> e(S); +m(S = [<<"skoczow">>,<<"pl">>]) -> e(S); +m(S = [<<"starachowice">>,<<"pl">>]) -> e(S); +m(S = [<<"stargard">>,<<"pl">>]) -> e(S); +m(S = [<<"suwalki">>,<<"pl">>]) -> e(S); +m(S = [<<"swidnica">>,<<"pl">>]) -> e(S); +m(S = [<<"swiebodzin">>,<<"pl">>]) -> e(S); +m(S = [<<"swinoujscie">>,<<"pl">>]) -> e(S); +m(S = [<<"szczecin">>,<<"pl">>]) -> e(S); +m(S = [<<"szczytno">>,<<"pl">>]) -> e(S); +m(S = [<<"tarnobrzeg">>,<<"pl">>]) -> e(S); +m(S = [<<"tgory">>,<<"pl">>]) -> e(S); +m(S = [<<"turek">>,<<"pl">>]) -> e(S); +m(S = [<<"tychy">>,<<"pl">>]) -> e(S); +m(S = [<<"ustka">>,<<"pl">>]) -> e(S); +m(S = [<<"walbrzych">>,<<"pl">>]) -> e(S); +m(S = [<<"warmia">>,<<"pl">>]) -> e(S); +m(S = [<<"warszawa">>,<<"pl">>]) -> e(S); +m(S = [<<"waw">>,<<"pl">>]) -> e(S); +m(S = [<<"wegrow">>,<<"pl">>]) -> e(S); +m(S = [<<"wielun">>,<<"pl">>]) -> e(S); +m(S = [<<"wlocl">>,<<"pl">>]) -> e(S); +m(S = [<<"wloclawek">>,<<"pl">>]) -> e(S); +m(S = [<<"wodzislaw">>,<<"pl">>]) -> e(S); +m(S = [<<"wolomin">>,<<"pl">>]) -> e(S); +m(S = [<<"wroclaw">>,<<"pl">>]) -> e(S); +m(S = [<<"zachpomor">>,<<"pl">>]) -> e(S); +m(S = [<<"zagan">>,<<"pl">>]) -> e(S); +m(S = [<<"zarow">>,<<"pl">>]) -> e(S); +m(S = [<<"zgora">>,<<"pl">>]) -> e(S); +m(S = [<<"zgorzelec">>,<<"pl">>]) -> e(S); +m(S = [<<"pm">>]) -> e(S); +m(S = [<<"pn">>]) -> e(S); +m(S = [<<"gov">>,<<"pn">>]) -> e(S); +m(S = [<<"co">>,<<"pn">>]) -> e(S); +m(S = [<<"org">>,<<"pn">>]) -> e(S); +m(S = [<<"edu">>,<<"pn">>]) -> e(S); +m(S = [<<"net">>,<<"pn">>]) -> e(S); +m(S = [<<"post">>]) -> e(S); +m(S = [<<"pr">>]) -> e(S); +m(S = [<<"com">>,<<"pr">>]) -> e(S); +m(S = [<<"net">>,<<"pr">>]) -> e(S); +m(S = [<<"org">>,<<"pr">>]) -> e(S); +m(S = [<<"gov">>,<<"pr">>]) -> e(S); +m(S = [<<"edu">>,<<"pr">>]) -> e(S); +m(S = [<<"isla">>,<<"pr">>]) -> e(S); +m(S = [<<"pro">>,<<"pr">>]) -> e(S); +m(S = [<<"biz">>,<<"pr">>]) -> e(S); +m(S = [<<"info">>,<<"pr">>]) -> e(S); +m(S = [<<"name">>,<<"pr">>]) -> e(S); +m(S = [<<"est">>,<<"pr">>]) -> e(S); +m(S = [<<"prof">>,<<"pr">>]) -> e(S); +m(S = [<<"ac">>,<<"pr">>]) -> e(S); +m(S = [<<"pro">>]) -> e(S); +m(S = [<<"aaa">>,<<"pro">>]) -> e(S); +m(S = [<<"aca">>,<<"pro">>]) -> e(S); +m(S = [<<"acct">>,<<"pro">>]) -> e(S); +m(S = [<<"avocat">>,<<"pro">>]) -> e(S); +m(S = [<<"bar">>,<<"pro">>]) -> e(S); +m(S = [<<"cpa">>,<<"pro">>]) -> e(S); +m(S = [<<"eng">>,<<"pro">>]) -> e(S); +m(S = [<<"jur">>,<<"pro">>]) -> e(S); +m(S = [<<"law">>,<<"pro">>]) -> e(S); +m(S = [<<"med">>,<<"pro">>]) -> e(S); +m(S = [<<"recht">>,<<"pro">>]) -> e(S); +m(S = [<<"ps">>]) -> e(S); +m(S = [<<"edu">>,<<"ps">>]) -> e(S); +m(S = [<<"gov">>,<<"ps">>]) -> e(S); +m(S = [<<"sec">>,<<"ps">>]) -> e(S); +m(S = [<<"plo">>,<<"ps">>]) -> e(S); +m(S = [<<"com">>,<<"ps">>]) -> e(S); +m(S = [<<"org">>,<<"ps">>]) -> e(S); +m(S = [<<"net">>,<<"ps">>]) -> e(S); +m(S = [<<"pt">>]) -> e(S); +m(S = [<<"net">>,<<"pt">>]) -> e(S); +m(S = [<<"gov">>,<<"pt">>]) -> e(S); +m(S = [<<"org">>,<<"pt">>]) -> e(S); +m(S = [<<"edu">>,<<"pt">>]) -> e(S); +m(S = [<<"int">>,<<"pt">>]) -> e(S); +m(S = [<<"publ">>,<<"pt">>]) -> e(S); +m(S = [<<"com">>,<<"pt">>]) -> e(S); +m(S = [<<"nome">>,<<"pt">>]) -> e(S); +m(S = [<<"pw">>]) -> e(S); +m(S = [<<"co">>,<<"pw">>]) -> e(S); +m(S = [<<"ne">>,<<"pw">>]) -> e(S); +m(S = [<<"or">>,<<"pw">>]) -> e(S); +m(S = [<<"ed">>,<<"pw">>]) -> e(S); +m(S = [<<"go">>,<<"pw">>]) -> e(S); +m(S = [<<"belau">>,<<"pw">>]) -> e(S); +m(S = [<<"py">>]) -> e(S); +m(S = [<<"com">>,<<"py">>]) -> e(S); +m(S = [<<"coop">>,<<"py">>]) -> e(S); +m(S = [<<"edu">>,<<"py">>]) -> e(S); +m(S = [<<"gov">>,<<"py">>]) -> e(S); +m(S = [<<"mil">>,<<"py">>]) -> e(S); +m(S = [<<"net">>,<<"py">>]) -> e(S); +m(S = [<<"org">>,<<"py">>]) -> e(S); +m(S = [<<"qa">>]) -> e(S); +m(S = [<<"com">>,<<"qa">>]) -> e(S); +m(S = [<<"edu">>,<<"qa">>]) -> e(S); +m(S = [<<"gov">>,<<"qa">>]) -> e(S); +m(S = [<<"mil">>,<<"qa">>]) -> e(S); +m(S = [<<"name">>,<<"qa">>]) -> e(S); +m(S = [<<"net">>,<<"qa">>]) -> e(S); +m(S = [<<"org">>,<<"qa">>]) -> e(S); +m(S = [<<"sch">>,<<"qa">>]) -> e(S); +m(S = [<<"re">>]) -> e(S); +m(S = [<<"asso">>,<<"re">>]) -> e(S); +m(S = [<<"com">>,<<"re">>]) -> e(S); +m(S = [<<"nom">>,<<"re">>]) -> e(S); +m(S = [<<"ro">>]) -> e(S); +m(S = [<<"arts">>,<<"ro">>]) -> e(S); +m(S = [<<"com">>,<<"ro">>]) -> e(S); +m(S = [<<"firm">>,<<"ro">>]) -> e(S); +m(S = [<<"info">>,<<"ro">>]) -> e(S); +m(S = [<<"nom">>,<<"ro">>]) -> e(S); +m(S = [<<"nt">>,<<"ro">>]) -> e(S); +m(S = [<<"org">>,<<"ro">>]) -> e(S); +m(S = [<<"rec">>,<<"ro">>]) -> e(S); +m(S = [<<"store">>,<<"ro">>]) -> e(S); +m(S = [<<"tm">>,<<"ro">>]) -> e(S); +m(S = [<<"www">>,<<"ro">>]) -> e(S); +m(S = [<<"rs">>]) -> e(S); +m(S = [<<"ac">>,<<"rs">>]) -> e(S); +m(S = [<<"co">>,<<"rs">>]) -> e(S); +m(S = [<<"edu">>,<<"rs">>]) -> e(S); +m(S = [<<"gov">>,<<"rs">>]) -> e(S); +m(S = [<<"in">>,<<"rs">>]) -> e(S); +m(S = [<<"org">>,<<"rs">>]) -> e(S); +m(S = [<<"ru">>]) -> e(S); +m(S = [<<"rw">>]) -> e(S); +m(S = [<<"ac">>,<<"rw">>]) -> e(S); +m(S = [<<"co">>,<<"rw">>]) -> e(S); +m(S = [<<"coop">>,<<"rw">>]) -> e(S); +m(S = [<<"gov">>,<<"rw">>]) -> e(S); +m(S = [<<"mil">>,<<"rw">>]) -> e(S); +m(S = [<<"net">>,<<"rw">>]) -> e(S); +m(S = [<<"org">>,<<"rw">>]) -> e(S); +m(S = [<<"sa">>]) -> e(S); +m(S = [<<"com">>,<<"sa">>]) -> e(S); +m(S = [<<"net">>,<<"sa">>]) -> e(S); +m(S = [<<"org">>,<<"sa">>]) -> e(S); +m(S = [<<"gov">>,<<"sa">>]) -> e(S); +m(S = [<<"med">>,<<"sa">>]) -> e(S); +m(S = [<<"pub">>,<<"sa">>]) -> e(S); +m(S = [<<"edu">>,<<"sa">>]) -> e(S); +m(S = [<<"sch">>,<<"sa">>]) -> e(S); +m(S = [<<"sb">>]) -> e(S); +m(S = [<<"com">>,<<"sb">>]) -> e(S); +m(S = [<<"edu">>,<<"sb">>]) -> e(S); +m(S = [<<"gov">>,<<"sb">>]) -> e(S); +m(S = [<<"net">>,<<"sb">>]) -> e(S); +m(S = [<<"org">>,<<"sb">>]) -> e(S); +m(S = [<<"sc">>]) -> e(S); +m(S = [<<"com">>,<<"sc">>]) -> e(S); +m(S = [<<"gov">>,<<"sc">>]) -> e(S); +m(S = [<<"net">>,<<"sc">>]) -> e(S); +m(S = [<<"org">>,<<"sc">>]) -> e(S); +m(S = [<<"edu">>,<<"sc">>]) -> e(S); +m(S = [<<"sd">>]) -> e(S); +m(S = [<<"com">>,<<"sd">>]) -> e(S); +m(S = [<<"net">>,<<"sd">>]) -> e(S); +m(S = [<<"org">>,<<"sd">>]) -> e(S); +m(S = [<<"edu">>,<<"sd">>]) -> e(S); +m(S = [<<"med">>,<<"sd">>]) -> e(S); +m(S = [<<"tv">>,<<"sd">>]) -> e(S); +m(S = [<<"gov">>,<<"sd">>]) -> e(S); +m(S = [<<"info">>,<<"sd">>]) -> e(S); +m(S = [<<"se">>]) -> e(S); +m(S = [<<"a">>,<<"se">>]) -> e(S); +m(S = [<<"ac">>,<<"se">>]) -> e(S); +m(S = [<<"b">>,<<"se">>]) -> e(S); +m(S = [<<"bd">>,<<"se">>]) -> e(S); +m(S = [<<"brand">>,<<"se">>]) -> e(S); +m(S = [<<"c">>,<<"se">>]) -> e(S); +m(S = [<<"d">>,<<"se">>]) -> e(S); +m(S = [<<"e">>,<<"se">>]) -> e(S); +m(S = [<<"f">>,<<"se">>]) -> e(S); +m(S = [<<"fh">>,<<"se">>]) -> e(S); +m(S = [<<"fhsk">>,<<"se">>]) -> e(S); +m(S = [<<"fhv">>,<<"se">>]) -> e(S); +m(S = [<<"g">>,<<"se">>]) -> e(S); +m(S = [<<"h">>,<<"se">>]) -> e(S); +m(S = [<<"i">>,<<"se">>]) -> e(S); +m(S = [<<"k">>,<<"se">>]) -> e(S); +m(S = [<<"komforb">>,<<"se">>]) -> e(S); +m(S = [<<"kommunalforbund">>,<<"se">>]) -> e(S); +m(S = [<<"komvux">>,<<"se">>]) -> e(S); +m(S = [<<"l">>,<<"se">>]) -> e(S); +m(S = [<<"lanbib">>,<<"se">>]) -> e(S); +m(S = [<<"m">>,<<"se">>]) -> e(S); +m(S = [<<"n">>,<<"se">>]) -> e(S); +m(S = [<<"naturbruksgymn">>,<<"se">>]) -> e(S); +m(S = [<<"o">>,<<"se">>]) -> e(S); +m(S = [<<"org">>,<<"se">>]) -> e(S); +m(S = [<<"p">>,<<"se">>]) -> e(S); +m(S = [<<"parti">>,<<"se">>]) -> e(S); +m(S = [<<"pp">>,<<"se">>]) -> e(S); +m(S = [<<"press">>,<<"se">>]) -> e(S); +m(S = [<<"r">>,<<"se">>]) -> e(S); +m(S = [<<"s">>,<<"se">>]) -> e(S); +m(S = [<<"t">>,<<"se">>]) -> e(S); +m(S = [<<"tm">>,<<"se">>]) -> e(S); +m(S = [<<"u">>,<<"se">>]) -> e(S); +m(S = [<<"w">>,<<"se">>]) -> e(S); +m(S = [<<"x">>,<<"se">>]) -> e(S); +m(S = [<<"y">>,<<"se">>]) -> e(S); +m(S = [<<"z">>,<<"se">>]) -> e(S); +m(S = [<<"sg">>]) -> e(S); +m(S = [<<"com">>,<<"sg">>]) -> e(S); +m(S = [<<"net">>,<<"sg">>]) -> e(S); +m(S = [<<"org">>,<<"sg">>]) -> e(S); +m(S = [<<"gov">>,<<"sg">>]) -> e(S); +m(S = [<<"edu">>,<<"sg">>]) -> e(S); +m(S = [<<"per">>,<<"sg">>]) -> e(S); +m(S = [<<"sh">>]) -> e(S); +m(S = [<<"com">>,<<"sh">>]) -> e(S); +m(S = [<<"net">>,<<"sh">>]) -> e(S); +m(S = [<<"gov">>,<<"sh">>]) -> e(S); +m(S = [<<"org">>,<<"sh">>]) -> e(S); +m(S = [<<"mil">>,<<"sh">>]) -> e(S); +m(S = [<<"si">>]) -> e(S); +m(S = [<<"sj">>]) -> e(S); +m(S = [<<"sk">>]) -> e(S); +m(S = [<<"sl">>]) -> e(S); +m(S = [<<"com">>,<<"sl">>]) -> e(S); +m(S = [<<"net">>,<<"sl">>]) -> e(S); +m(S = [<<"edu">>,<<"sl">>]) -> e(S); +m(S = [<<"gov">>,<<"sl">>]) -> e(S); +m(S = [<<"org">>,<<"sl">>]) -> e(S); +m(S = [<<"sm">>]) -> e(S); +m(S = [<<"sn">>]) -> e(S); +m(S = [<<"art">>,<<"sn">>]) -> e(S); +m(S = [<<"com">>,<<"sn">>]) -> e(S); +m(S = [<<"edu">>,<<"sn">>]) -> e(S); +m(S = [<<"gouv">>,<<"sn">>]) -> e(S); +m(S = [<<"org">>,<<"sn">>]) -> e(S); +m(S = [<<"perso">>,<<"sn">>]) -> e(S); +m(S = [<<"univ">>,<<"sn">>]) -> e(S); +m(S = [<<"so">>]) -> e(S); +m(S = [<<"com">>,<<"so">>]) -> e(S); +m(S = [<<"edu">>,<<"so">>]) -> e(S); +m(S = [<<"gov">>,<<"so">>]) -> e(S); +m(S = [<<"me">>,<<"so">>]) -> e(S); +m(S = [<<"net">>,<<"so">>]) -> e(S); +m(S = [<<"org">>,<<"so">>]) -> e(S); +m(S = [<<"sr">>]) -> e(S); +m(S = [<<"ss">>]) -> e(S); +m(S = [<<"biz">>,<<"ss">>]) -> e(S); +m(S = [<<"com">>,<<"ss">>]) -> e(S); +m(S = [<<"edu">>,<<"ss">>]) -> e(S); +m(S = [<<"gov">>,<<"ss">>]) -> e(S); +m(S = [<<"me">>,<<"ss">>]) -> e(S); +m(S = [<<"net">>,<<"ss">>]) -> e(S); +m(S = [<<"org">>,<<"ss">>]) -> e(S); +m(S = [<<"sch">>,<<"ss">>]) -> e(S); +m(S = [<<"st">>]) -> e(S); +m(S = [<<"co">>,<<"st">>]) -> e(S); +m(S = [<<"com">>,<<"st">>]) -> e(S); +m(S = [<<"consulado">>,<<"st">>]) -> e(S); +m(S = [<<"edu">>,<<"st">>]) -> e(S); +m(S = [<<"embaixada">>,<<"st">>]) -> e(S); +m(S = [<<"mil">>,<<"st">>]) -> e(S); +m(S = [<<"net">>,<<"st">>]) -> e(S); +m(S = [<<"org">>,<<"st">>]) -> e(S); +m(S = [<<"principe">>,<<"st">>]) -> e(S); +m(S = [<<"saotome">>,<<"st">>]) -> e(S); +m(S = [<<"store">>,<<"st">>]) -> e(S); +m(S = [<<"su">>]) -> e(S); +m(S = [<<"sv">>]) -> e(S); +m(S = [<<"com">>,<<"sv">>]) -> e(S); +m(S = [<<"edu">>,<<"sv">>]) -> e(S); +m(S = [<<"gob">>,<<"sv">>]) -> e(S); +m(S = [<<"org">>,<<"sv">>]) -> e(S); +m(S = [<<"red">>,<<"sv">>]) -> e(S); +m(S = [<<"sx">>]) -> e(S); +m(S = [<<"gov">>,<<"sx">>]) -> e(S); +m(S = [<<"sy">>]) -> e(S); +m(S = [<<"edu">>,<<"sy">>]) -> e(S); +m(S = [<<"gov">>,<<"sy">>]) -> e(S); +m(S = [<<"net">>,<<"sy">>]) -> e(S); +m(S = [<<"mil">>,<<"sy">>]) -> e(S); +m(S = [<<"com">>,<<"sy">>]) -> e(S); +m(S = [<<"org">>,<<"sy">>]) -> e(S); +m(S = [<<"sz">>]) -> e(S); +m(S = [<<"co">>,<<"sz">>]) -> e(S); +m(S = [<<"ac">>,<<"sz">>]) -> e(S); +m(S = [<<"org">>,<<"sz">>]) -> e(S); +m(S = [<<"tc">>]) -> e(S); +m(S = [<<"td">>]) -> e(S); +m(S = [<<"tel">>]) -> e(S); +m(S = [<<"tf">>]) -> e(S); +m(S = [<<"tg">>]) -> e(S); +m(S = [<<"th">>]) -> e(S); +m(S = [<<"ac">>,<<"th">>]) -> e(S); +m(S = [<<"co">>,<<"th">>]) -> e(S); +m(S = [<<"go">>,<<"th">>]) -> e(S); +m(S = [<<"in">>,<<"th">>]) -> e(S); +m(S = [<<"mi">>,<<"th">>]) -> e(S); +m(S = [<<"net">>,<<"th">>]) -> e(S); +m(S = [<<"or">>,<<"th">>]) -> e(S); +m(S = [<<"tj">>]) -> e(S); +m(S = [<<"ac">>,<<"tj">>]) -> e(S); +m(S = [<<"biz">>,<<"tj">>]) -> e(S); +m(S = [<<"co">>,<<"tj">>]) -> e(S); +m(S = [<<"com">>,<<"tj">>]) -> e(S); +m(S = [<<"edu">>,<<"tj">>]) -> e(S); +m(S = [<<"go">>,<<"tj">>]) -> e(S); +m(S = [<<"gov">>,<<"tj">>]) -> e(S); +m(S = [<<"int">>,<<"tj">>]) -> e(S); +m(S = [<<"mil">>,<<"tj">>]) -> e(S); +m(S = [<<"name">>,<<"tj">>]) -> e(S); +m(S = [<<"net">>,<<"tj">>]) -> e(S); +m(S = [<<"nic">>,<<"tj">>]) -> e(S); +m(S = [<<"org">>,<<"tj">>]) -> e(S); +m(S = [<<"test">>,<<"tj">>]) -> e(S); +m(S = [<<"web">>,<<"tj">>]) -> e(S); +m(S = [<<"tk">>]) -> e(S); +m(S = [<<"tl">>]) -> e(S); +m(S = [<<"gov">>,<<"tl">>]) -> e(S); +m(S = [<<"tm">>]) -> e(S); +m(S = [<<"com">>,<<"tm">>]) -> e(S); +m(S = [<<"co">>,<<"tm">>]) -> e(S); +m(S = [<<"org">>,<<"tm">>]) -> e(S); +m(S = [<<"net">>,<<"tm">>]) -> e(S); +m(S = [<<"nom">>,<<"tm">>]) -> e(S); +m(S = [<<"gov">>,<<"tm">>]) -> e(S); +m(S = [<<"mil">>,<<"tm">>]) -> e(S); +m(S = [<<"edu">>,<<"tm">>]) -> e(S); +m(S = [<<"tn">>]) -> e(S); +m(S = [<<"com">>,<<"tn">>]) -> e(S); +m(S = [<<"ens">>,<<"tn">>]) -> e(S); +m(S = [<<"fin">>,<<"tn">>]) -> e(S); +m(S = [<<"gov">>,<<"tn">>]) -> e(S); +m(S = [<<"ind">>,<<"tn">>]) -> e(S); +m(S = [<<"info">>,<<"tn">>]) -> e(S); +m(S = [<<"intl">>,<<"tn">>]) -> e(S); +m(S = [<<"mincom">>,<<"tn">>]) -> e(S); +m(S = [<<"nat">>,<<"tn">>]) -> e(S); +m(S = [<<"net">>,<<"tn">>]) -> e(S); +m(S = [<<"org">>,<<"tn">>]) -> e(S); +m(S = [<<"perso">>,<<"tn">>]) -> e(S); +m(S = [<<"tourism">>,<<"tn">>]) -> e(S); +m(S = [<<"to">>]) -> e(S); +m(S = [<<"com">>,<<"to">>]) -> e(S); +m(S = [<<"gov">>,<<"to">>]) -> e(S); +m(S = [<<"net">>,<<"to">>]) -> e(S); +m(S = [<<"org">>,<<"to">>]) -> e(S); +m(S = [<<"edu">>,<<"to">>]) -> e(S); +m(S = [<<"mil">>,<<"to">>]) -> e(S); +m(S = [<<"tr">>]) -> e(S); +m(S = [<<"av">>,<<"tr">>]) -> e(S); +m(S = [<<"bbs">>,<<"tr">>]) -> e(S); +m(S = [<<"bel">>,<<"tr">>]) -> e(S); +m(S = [<<"biz">>,<<"tr">>]) -> e(S); +m(S = [<<"com">>,<<"tr">>]) -> e(S); +m(S = [<<"dr">>,<<"tr">>]) -> e(S); +m(S = [<<"edu">>,<<"tr">>]) -> e(S); +m(S = [<<"gen">>,<<"tr">>]) -> e(S); +m(S = [<<"gov">>,<<"tr">>]) -> e(S); +m(S = [<<"info">>,<<"tr">>]) -> e(S); +m(S = [<<"mil">>,<<"tr">>]) -> e(S); +m(S = [<<"k12">>,<<"tr">>]) -> e(S); +m(S = [<<"kep">>,<<"tr">>]) -> e(S); +m(S = [<<"name">>,<<"tr">>]) -> e(S); +m(S = [<<"net">>,<<"tr">>]) -> e(S); +m(S = [<<"org">>,<<"tr">>]) -> e(S); +m(S = [<<"pol">>,<<"tr">>]) -> e(S); +m(S = [<<"tel">>,<<"tr">>]) -> e(S); +m(S = [<<"tsk">>,<<"tr">>]) -> e(S); +m(S = [<<"tv">>,<<"tr">>]) -> e(S); +m(S = [<<"web">>,<<"tr">>]) -> e(S); +m(S = [<<"nc">>,<<"tr">>]) -> e(S); +m(S = [<<"gov">>,<<"nc">>,<<"tr">>]) -> e(S); +m(S = [<<"tt">>]) -> e(S); +m(S = [<<"co">>,<<"tt">>]) -> e(S); +m(S = [<<"com">>,<<"tt">>]) -> e(S); +m(S = [<<"org">>,<<"tt">>]) -> e(S); +m(S = [<<"net">>,<<"tt">>]) -> e(S); +m(S = [<<"biz">>,<<"tt">>]) -> e(S); +m(S = [<<"info">>,<<"tt">>]) -> e(S); +m(S = [<<"pro">>,<<"tt">>]) -> e(S); +m(S = [<<"int">>,<<"tt">>]) -> e(S); +m(S = [<<"coop">>,<<"tt">>]) -> e(S); +m(S = [<<"jobs">>,<<"tt">>]) -> e(S); +m(S = [<<"mobi">>,<<"tt">>]) -> e(S); +m(S = [<<"travel">>,<<"tt">>]) -> e(S); +m(S = [<<"museum">>,<<"tt">>]) -> e(S); +m(S = [<<"aero">>,<<"tt">>]) -> e(S); +m(S = [<<"name">>,<<"tt">>]) -> e(S); +m(S = [<<"gov">>,<<"tt">>]) -> e(S); +m(S = [<<"edu">>,<<"tt">>]) -> e(S); +m(S = [<<"tv">>]) -> e(S); +m(S = [<<"tw">>]) -> e(S); +m(S = [<<"edu">>,<<"tw">>]) -> e(S); +m(S = [<<"gov">>,<<"tw">>]) -> e(S); +m(S = [<<"mil">>,<<"tw">>]) -> e(S); +m(S = [<<"com">>,<<"tw">>]) -> e(S); +m(S = [<<"net">>,<<"tw">>]) -> e(S); +m(S = [<<"org">>,<<"tw">>]) -> e(S); +m(S = [<<"idv">>,<<"tw">>]) -> e(S); +m(S = [<<"game">>,<<"tw">>]) -> e(S); +m(S = [<<"ebiz">>,<<"tw">>]) -> e(S); +m(S = [<<"club">>,<<"tw">>]) -> e(S); +m(S = [<<"xn--zf0ao64a">>,<<"tw">>]) -> e(S); +m(S = [<<"xn--uc0atv">>,<<"tw">>]) -> e(S); +m(S = [<<"xn--czrw28b">>,<<"tw">>]) -> e(S); +m(S = [<<"tz">>]) -> e(S); +m(S = [<<"ac">>,<<"tz">>]) -> e(S); +m(S = [<<"co">>,<<"tz">>]) -> e(S); +m(S = [<<"go">>,<<"tz">>]) -> e(S); +m(S = [<<"hotel">>,<<"tz">>]) -> e(S); +m(S = [<<"info">>,<<"tz">>]) -> e(S); +m(S = [<<"me">>,<<"tz">>]) -> e(S); +m(S = [<<"mil">>,<<"tz">>]) -> e(S); +m(S = [<<"mobi">>,<<"tz">>]) -> e(S); +m(S = [<<"ne">>,<<"tz">>]) -> e(S); +m(S = [<<"or">>,<<"tz">>]) -> e(S); +m(S = [<<"sc">>,<<"tz">>]) -> e(S); +m(S = [<<"tv">>,<<"tz">>]) -> e(S); +m(S = [<<"ua">>]) -> e(S); +m(S = [<<"com">>,<<"ua">>]) -> e(S); +m(S = [<<"edu">>,<<"ua">>]) -> e(S); +m(S = [<<"gov">>,<<"ua">>]) -> e(S); +m(S = [<<"in">>,<<"ua">>]) -> e(S); +m(S = [<<"net">>,<<"ua">>]) -> e(S); +m(S = [<<"org">>,<<"ua">>]) -> e(S); +m(S = [<<"cherkassy">>,<<"ua">>]) -> e(S); +m(S = [<<"cherkasy">>,<<"ua">>]) -> e(S); +m(S = [<<"chernigov">>,<<"ua">>]) -> e(S); +m(S = [<<"chernihiv">>,<<"ua">>]) -> e(S); +m(S = [<<"chernivtsi">>,<<"ua">>]) -> e(S); +m(S = [<<"chernovtsy">>,<<"ua">>]) -> e(S); +m(S = [<<"ck">>,<<"ua">>]) -> e(S); +m(S = [<<"cn">>,<<"ua">>]) -> e(S); +m(S = [<<"cr">>,<<"ua">>]) -> e(S); +m(S = [<<"crimea">>,<<"ua">>]) -> e(S); +m(S = [<<"cv">>,<<"ua">>]) -> e(S); +m(S = [<<"dn">>,<<"ua">>]) -> e(S); +m(S = [<<"dnepropetrovsk">>,<<"ua">>]) -> e(S); +m(S = [<<"dnipropetrovsk">>,<<"ua">>]) -> e(S); +m(S = [<<"donetsk">>,<<"ua">>]) -> e(S); +m(S = [<<"dp">>,<<"ua">>]) -> e(S); +m(S = [<<"if">>,<<"ua">>]) -> e(S); +m(S = [<<"ivano-frankivsk">>,<<"ua">>]) -> e(S); +m(S = [<<"kh">>,<<"ua">>]) -> e(S); +m(S = [<<"kharkiv">>,<<"ua">>]) -> e(S); +m(S = [<<"kharkov">>,<<"ua">>]) -> e(S); +m(S = [<<"kherson">>,<<"ua">>]) -> e(S); +m(S = [<<"khmelnitskiy">>,<<"ua">>]) -> e(S); +m(S = [<<"khmelnytskyi">>,<<"ua">>]) -> e(S); +m(S = [<<"kiev">>,<<"ua">>]) -> e(S); +m(S = [<<"kirovograd">>,<<"ua">>]) -> e(S); +m(S = [<<"km">>,<<"ua">>]) -> e(S); +m(S = [<<"kr">>,<<"ua">>]) -> e(S); +m(S = [<<"krym">>,<<"ua">>]) -> e(S); +m(S = [<<"ks">>,<<"ua">>]) -> e(S); +m(S = [<<"kv">>,<<"ua">>]) -> e(S); +m(S = [<<"kyiv">>,<<"ua">>]) -> e(S); +m(S = [<<"lg">>,<<"ua">>]) -> e(S); +m(S = [<<"lt">>,<<"ua">>]) -> e(S); +m(S = [<<"lugansk">>,<<"ua">>]) -> e(S); +m(S = [<<"lutsk">>,<<"ua">>]) -> e(S); +m(S = [<<"lv">>,<<"ua">>]) -> e(S); +m(S = [<<"lviv">>,<<"ua">>]) -> e(S); +m(S = [<<"mk">>,<<"ua">>]) -> e(S); +m(S = [<<"mykolaiv">>,<<"ua">>]) -> e(S); +m(S = [<<"nikolaev">>,<<"ua">>]) -> e(S); +m(S = [<<"od">>,<<"ua">>]) -> e(S); +m(S = [<<"odesa">>,<<"ua">>]) -> e(S); +m(S = [<<"odessa">>,<<"ua">>]) -> e(S); +m(S = [<<"pl">>,<<"ua">>]) -> e(S); +m(S = [<<"poltava">>,<<"ua">>]) -> e(S); +m(S = [<<"rivne">>,<<"ua">>]) -> e(S); +m(S = [<<"rovno">>,<<"ua">>]) -> e(S); +m(S = [<<"rv">>,<<"ua">>]) -> e(S); +m(S = [<<"sb">>,<<"ua">>]) -> e(S); +m(S = [<<"sebastopol">>,<<"ua">>]) -> e(S); +m(S = [<<"sevastopol">>,<<"ua">>]) -> e(S); +m(S = [<<"sm">>,<<"ua">>]) -> e(S); +m(S = [<<"sumy">>,<<"ua">>]) -> e(S); +m(S = [<<"te">>,<<"ua">>]) -> e(S); +m(S = [<<"ternopil">>,<<"ua">>]) -> e(S); +m(S = [<<"uz">>,<<"ua">>]) -> e(S); +m(S = [<<"uzhgorod">>,<<"ua">>]) -> e(S); +m(S = [<<"vinnica">>,<<"ua">>]) -> e(S); +m(S = [<<"vinnytsia">>,<<"ua">>]) -> e(S); +m(S = [<<"vn">>,<<"ua">>]) -> e(S); +m(S = [<<"volyn">>,<<"ua">>]) -> e(S); +m(S = [<<"yalta">>,<<"ua">>]) -> e(S); +m(S = [<<"zaporizhzhe">>,<<"ua">>]) -> e(S); +m(S = [<<"zaporizhzhia">>,<<"ua">>]) -> e(S); +m(S = [<<"zhitomir">>,<<"ua">>]) -> e(S); +m(S = [<<"zhytomyr">>,<<"ua">>]) -> e(S); +m(S = [<<"zp">>,<<"ua">>]) -> e(S); +m(S = [<<"zt">>,<<"ua">>]) -> e(S); +m(S = [<<"ug">>]) -> e(S); +m(S = [<<"co">>,<<"ug">>]) -> e(S); +m(S = [<<"or">>,<<"ug">>]) -> e(S); +m(S = [<<"ac">>,<<"ug">>]) -> e(S); +m(S = [<<"sc">>,<<"ug">>]) -> e(S); +m(S = [<<"go">>,<<"ug">>]) -> e(S); +m(S = [<<"ne">>,<<"ug">>]) -> e(S); +m(S = [<<"com">>,<<"ug">>]) -> e(S); +m(S = [<<"org">>,<<"ug">>]) -> e(S); +m(S = [<<"uk">>]) -> e(S); +m(S = [<<"ac">>,<<"uk">>]) -> e(S); +m(S = [<<"co">>,<<"uk">>]) -> e(S); +m(S = [<<"gov">>,<<"uk">>]) -> e(S); +m(S = [<<"ltd">>,<<"uk">>]) -> e(S); +m(S = [<<"me">>,<<"uk">>]) -> e(S); +m(S = [<<"net">>,<<"uk">>]) -> e(S); +m(S = [<<"nhs">>,<<"uk">>]) -> e(S); +m(S = [<<"org">>,<<"uk">>]) -> e(S); +m(S = [<<"plc">>,<<"uk">>]) -> e(S); +m(S = [<<"police">>,<<"uk">>]) -> e(S); +m(S = [_,<<"sch">>,<<"uk">>]) -> e(S); +m(S = [<<"us">>]) -> e(S); +m(S = [<<"dni">>,<<"us">>]) -> e(S); +m(S = [<<"fed">>,<<"us">>]) -> e(S); +m(S = [<<"isa">>,<<"us">>]) -> e(S); +m(S = [<<"kids">>,<<"us">>]) -> e(S); +m(S = [<<"nsn">>,<<"us">>]) -> e(S); +m(S = [<<"ak">>,<<"us">>]) -> e(S); +m(S = [<<"al">>,<<"us">>]) -> e(S); +m(S = [<<"ar">>,<<"us">>]) -> e(S); +m(S = [<<"as">>,<<"us">>]) -> e(S); +m(S = [<<"az">>,<<"us">>]) -> e(S); +m(S = [<<"ca">>,<<"us">>]) -> e(S); +m(S = [<<"co">>,<<"us">>]) -> e(S); +m(S = [<<"ct">>,<<"us">>]) -> e(S); +m(S = [<<"dc">>,<<"us">>]) -> e(S); +m(S = [<<"de">>,<<"us">>]) -> e(S); +m(S = [<<"fl">>,<<"us">>]) -> e(S); +m(S = [<<"ga">>,<<"us">>]) -> e(S); +m(S = [<<"gu">>,<<"us">>]) -> e(S); +m(S = [<<"hi">>,<<"us">>]) -> e(S); +m(S = [<<"ia">>,<<"us">>]) -> e(S); +m(S = [<<"id">>,<<"us">>]) -> e(S); +m(S = [<<"il">>,<<"us">>]) -> e(S); +m(S = [<<"in">>,<<"us">>]) -> e(S); +m(S = [<<"ks">>,<<"us">>]) -> e(S); +m(S = [<<"ky">>,<<"us">>]) -> e(S); +m(S = [<<"la">>,<<"us">>]) -> e(S); +m(S = [<<"ma">>,<<"us">>]) -> e(S); +m(S = [<<"md">>,<<"us">>]) -> e(S); +m(S = [<<"me">>,<<"us">>]) -> e(S); +m(S = [<<"mi">>,<<"us">>]) -> e(S); +m(S = [<<"mn">>,<<"us">>]) -> e(S); +m(S = [<<"mo">>,<<"us">>]) -> e(S); +m(S = [<<"ms">>,<<"us">>]) -> e(S); +m(S = [<<"mt">>,<<"us">>]) -> e(S); +m(S = [<<"nc">>,<<"us">>]) -> e(S); +m(S = [<<"nd">>,<<"us">>]) -> e(S); +m(S = [<<"ne">>,<<"us">>]) -> e(S); +m(S = [<<"nh">>,<<"us">>]) -> e(S); +m(S = [<<"nj">>,<<"us">>]) -> e(S); +m(S = [<<"nm">>,<<"us">>]) -> e(S); +m(S = [<<"nv">>,<<"us">>]) -> e(S); +m(S = [<<"ny">>,<<"us">>]) -> e(S); +m(S = [<<"oh">>,<<"us">>]) -> e(S); +m(S = [<<"ok">>,<<"us">>]) -> e(S); +m(S = [<<"or">>,<<"us">>]) -> e(S); +m(S = [<<"pa">>,<<"us">>]) -> e(S); +m(S = [<<"pr">>,<<"us">>]) -> e(S); +m(S = [<<"ri">>,<<"us">>]) -> e(S); +m(S = [<<"sc">>,<<"us">>]) -> e(S); +m(S = [<<"sd">>,<<"us">>]) -> e(S); +m(S = [<<"tn">>,<<"us">>]) -> e(S); +m(S = [<<"tx">>,<<"us">>]) -> e(S); +m(S = [<<"ut">>,<<"us">>]) -> e(S); +m(S = [<<"vi">>,<<"us">>]) -> e(S); +m(S = [<<"vt">>,<<"us">>]) -> e(S); +m(S = [<<"va">>,<<"us">>]) -> e(S); +m(S = [<<"wa">>,<<"us">>]) -> e(S); +m(S = [<<"wi">>,<<"us">>]) -> e(S); +m(S = [<<"wv">>,<<"us">>]) -> e(S); +m(S = [<<"wy">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"ak">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"al">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"ar">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"as">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"az">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"ca">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"co">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"ct">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"dc">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"de">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"fl">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"ga">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"gu">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"ia">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"id">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"il">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"in">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"ks">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"ky">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"la">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"ma">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"md">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"me">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"mi">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"mn">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"mo">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"ms">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"mt">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"nc">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"ne">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"nh">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"nj">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"nm">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"nv">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"ny">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"oh">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"ok">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"or">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"pa">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"pr">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"sc">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"tn">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"tx">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"ut">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"vi">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"vt">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"va">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"wa">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"wi">>,<<"us">>]) -> e(S); +m(S = [<<"k12">>,<<"wy">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"ak">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"al">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"ar">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"as">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"az">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"ca">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"co">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"ct">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"dc">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"de">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"fl">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"ga">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"gu">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"hi">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"ia">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"id">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"il">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"in">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"ks">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"ky">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"la">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"ma">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"md">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"me">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"mi">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"mn">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"mo">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"ms">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"mt">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"nc">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"nd">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"ne">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"nh">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"nj">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"nm">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"nv">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"ny">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"oh">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"ok">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"or">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"pa">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"pr">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"ri">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"sc">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"sd">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"tn">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"tx">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"ut">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"vi">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"vt">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"va">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"wa">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"wi">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"wv">>,<<"us">>]) -> e(S); +m(S = [<<"cc">>,<<"wy">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"ak">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"al">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"ar">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"as">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"az">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"ca">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"co">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"ct">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"dc">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"fl">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"ga">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"gu">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"hi">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"ia">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"id">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"il">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"in">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"ks">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"ky">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"la">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"ma">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"md">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"me">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"mi">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"mn">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"mo">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"ms">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"mt">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"nc">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"nd">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"ne">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"nh">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"nj">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"nm">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"nv">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"ny">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"oh">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"ok">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"or">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"pa">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"pr">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"ri">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"sc">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"sd">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"tn">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"tx">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"ut">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"vi">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"vt">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"va">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"wa">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"wi">>,<<"us">>]) -> e(S); +m(S = [<<"lib">>,<<"wy">>,<<"us">>]) -> e(S); +m(S = [<<"pvt">>,<<"k12">>,<<"ma">>,<<"us">>]) -> e(S); +m(S = [<<"chtr">>,<<"k12">>,<<"ma">>,<<"us">>]) -> e(S); +m(S = [<<"paroch">>,<<"k12">>,<<"ma">>,<<"us">>]) -> e(S); +m(S = [<<"ann-arbor">>,<<"mi">>,<<"us">>]) -> e(S); +m(S = [<<"cog">>,<<"mi">>,<<"us">>]) -> e(S); +m(S = [<<"dst">>,<<"mi">>,<<"us">>]) -> e(S); +m(S = [<<"eaton">>,<<"mi">>,<<"us">>]) -> e(S); +m(S = [<<"gen">>,<<"mi">>,<<"us">>]) -> e(S); +m(S = [<<"mus">>,<<"mi">>,<<"us">>]) -> e(S); +m(S = [<<"tec">>,<<"mi">>,<<"us">>]) -> e(S); +m(S = [<<"washtenaw">>,<<"mi">>,<<"us">>]) -> e(S); +m(S = [<<"uy">>]) -> e(S); +m(S = [<<"com">>,<<"uy">>]) -> e(S); +m(S = [<<"edu">>,<<"uy">>]) -> e(S); +m(S = [<<"gub">>,<<"uy">>]) -> e(S); +m(S = [<<"mil">>,<<"uy">>]) -> e(S); +m(S = [<<"net">>,<<"uy">>]) -> e(S); +m(S = [<<"org">>,<<"uy">>]) -> e(S); +m(S = [<<"uz">>]) -> e(S); +m(S = [<<"co">>,<<"uz">>]) -> e(S); +m(S = [<<"com">>,<<"uz">>]) -> e(S); +m(S = [<<"net">>,<<"uz">>]) -> e(S); +m(S = [<<"org">>,<<"uz">>]) -> e(S); +m(S = [<<"va">>]) -> e(S); +m(S = [<<"vc">>]) -> e(S); +m(S = [<<"com">>,<<"vc">>]) -> e(S); +m(S = [<<"net">>,<<"vc">>]) -> e(S); +m(S = [<<"org">>,<<"vc">>]) -> e(S); +m(S = [<<"gov">>,<<"vc">>]) -> e(S); +m(S = [<<"mil">>,<<"vc">>]) -> e(S); +m(S = [<<"edu">>,<<"vc">>]) -> e(S); +m(S = [<<"ve">>]) -> e(S); +m(S = [<<"arts">>,<<"ve">>]) -> e(S); +m(S = [<<"bib">>,<<"ve">>]) -> e(S); +m(S = [<<"co">>,<<"ve">>]) -> e(S); +m(S = [<<"com">>,<<"ve">>]) -> e(S); +m(S = [<<"e12">>,<<"ve">>]) -> e(S); +m(S = [<<"edu">>,<<"ve">>]) -> e(S); +m(S = [<<"firm">>,<<"ve">>]) -> e(S); +m(S = [<<"gob">>,<<"ve">>]) -> e(S); +m(S = [<<"gov">>,<<"ve">>]) -> e(S); +m(S = [<<"info">>,<<"ve">>]) -> e(S); +m(S = [<<"int">>,<<"ve">>]) -> e(S); +m(S = [<<"mil">>,<<"ve">>]) -> e(S); +m(S = [<<"net">>,<<"ve">>]) -> e(S); +m(S = [<<"nom">>,<<"ve">>]) -> e(S); +m(S = [<<"org">>,<<"ve">>]) -> e(S); +m(S = [<<"rar">>,<<"ve">>]) -> e(S); +m(S = [<<"rec">>,<<"ve">>]) -> e(S); +m(S = [<<"store">>,<<"ve">>]) -> e(S); +m(S = [<<"tec">>,<<"ve">>]) -> e(S); +m(S = [<<"web">>,<<"ve">>]) -> e(S); +m(S = [<<"vg">>]) -> e(S); +m(S = [<<"vi">>]) -> e(S); +m(S = [<<"co">>,<<"vi">>]) -> e(S); +m(S = [<<"com">>,<<"vi">>]) -> e(S); +m(S = [<<"k12">>,<<"vi">>]) -> e(S); +m(S = [<<"net">>,<<"vi">>]) -> e(S); +m(S = [<<"org">>,<<"vi">>]) -> e(S); +m(S = [<<"vn">>]) -> e(S); +m(S = [<<"com">>,<<"vn">>]) -> e(S); +m(S = [<<"net">>,<<"vn">>]) -> e(S); +m(S = [<<"org">>,<<"vn">>]) -> e(S); +m(S = [<<"edu">>,<<"vn">>]) -> e(S); +m(S = [<<"gov">>,<<"vn">>]) -> e(S); +m(S = [<<"int">>,<<"vn">>]) -> e(S); +m(S = [<<"ac">>,<<"vn">>]) -> e(S); +m(S = [<<"biz">>,<<"vn">>]) -> e(S); +m(S = [<<"info">>,<<"vn">>]) -> e(S); +m(S = [<<"name">>,<<"vn">>]) -> e(S); +m(S = [<<"pro">>,<<"vn">>]) -> e(S); +m(S = [<<"health">>,<<"vn">>]) -> e(S); +m(S = [<<"vu">>]) -> e(S); +m(S = [<<"com">>,<<"vu">>]) -> e(S); +m(S = [<<"edu">>,<<"vu">>]) -> e(S); +m(S = [<<"net">>,<<"vu">>]) -> e(S); +m(S = [<<"org">>,<<"vu">>]) -> e(S); +m(S = [<<"wf">>]) -> e(S); +m(S = [<<"ws">>]) -> e(S); +m(S = [<<"com">>,<<"ws">>]) -> e(S); +m(S = [<<"net">>,<<"ws">>]) -> e(S); +m(S = [<<"org">>,<<"ws">>]) -> e(S); +m(S = [<<"gov">>,<<"ws">>]) -> e(S); +m(S = [<<"edu">>,<<"ws">>]) -> e(S); +m(S = [<<"yt">>]) -> e(S); +m(S = [<<"xn--mgbaam7a8h">>]) -> e(S); +m(S = [<<"xn--y9a3aq">>]) -> e(S); +m(S = [<<"xn--54b7fta0cc">>]) -> e(S); +m(S = [<<"xn--90ae">>]) -> e(S); +m(S = [<<"xn--mgbcpq6gpa1a">>]) -> e(S); +m(S = [<<"xn--90ais">>]) -> e(S); +m(S = [<<"xn--fiqs8s">>]) -> e(S); +m(S = [<<"xn--fiqz9s">>]) -> e(S); +m(S = [<<"xn--lgbbat1ad8j">>]) -> e(S); +m(S = [<<"xn--wgbh1c">>]) -> e(S); +m(S = [<<"xn--e1a4c">>]) -> e(S); +m(S = [<<"xn--qxa6a">>]) -> e(S); +m(S = [<<"xn--mgbah1a3hjkrd">>]) -> e(S); +m(S = [<<"xn--node">>]) -> e(S); +m(S = [<<"xn--qxam">>]) -> e(S); +m(S = [<<"xn--j6w193g">>]) -> e(S); +m(S = [<<"xn--55qx5d">>,<<"xn--j6w193g">>]) -> e(S); +m(S = [<<"xn--wcvs22d">>,<<"xn--j6w193g">>]) -> e(S); +m(S = [<<"xn--mxtq1m">>,<<"xn--j6w193g">>]) -> e(S); +m(S = [<<"xn--gmqw5a">>,<<"xn--j6w193g">>]) -> e(S); +m(S = [<<"xn--od0alg">>,<<"xn--j6w193g">>]) -> e(S); +m(S = [<<"xn--uc0atv">>,<<"xn--j6w193g">>]) -> e(S); +m(S = [<<"xn--2scrj9c">>]) -> e(S); +m(S = [<<"xn--3hcrj9c">>]) -> e(S); +m(S = [<<"xn--45br5cyl">>]) -> e(S); +m(S = [<<"xn--h2breg3eve">>]) -> e(S); +m(S = [<<"xn--h2brj9c8c">>]) -> e(S); +m(S = [<<"xn--mgbgu82a">>]) -> e(S); +m(S = [<<"xn--rvc1e0am3e">>]) -> e(S); +m(S = [<<"xn--h2brj9c">>]) -> e(S); +m(S = [<<"xn--mgbbh1a">>]) -> e(S); +m(S = [<<"xn--mgbbh1a71e">>]) -> e(S); +m(S = [<<"xn--fpcrj9c3d">>]) -> e(S); +m(S = [<<"xn--gecrj9c">>]) -> e(S); +m(S = [<<"xn--s9brj9c">>]) -> e(S); +m(S = [<<"xn--45brj9c">>]) -> e(S); +m(S = [<<"xn--xkc2dl3a5ee0h">>]) -> e(S); +m(S = [<<"xn--mgba3a4f16a">>]) -> e(S); +m(S = [<<"xn--mgba3a4fra">>]) -> e(S); +m(S = [<<"xn--mgbtx2b">>]) -> e(S); +m(S = [<<"xn--mgbayh7gpa">>]) -> e(S); +m(S = [<<"xn--3e0b707e">>]) -> e(S); +m(S = [<<"xn--80ao21a">>]) -> e(S); +m(S = [<<"xn--q7ce6a">>]) -> e(S); +m(S = [<<"xn--fzc2c9e2c">>]) -> e(S); +m(S = [<<"xn--xkc2al3hye2a">>]) -> e(S); +m(S = [<<"xn--mgbc0a9azcg">>]) -> e(S); +m(S = [<<"xn--d1alf">>]) -> e(S); +m(S = [<<"xn--l1acc">>]) -> e(S); +m(S = [<<"xn--mix891f">>]) -> e(S); +m(S = [<<"xn--mix082f">>]) -> e(S); +m(S = [<<"xn--mgbx4cd0ab">>]) -> e(S); +m(S = [<<"xn--mgb9awbf">>]) -> e(S); +m(S = [<<"xn--mgbai9azgqp6j">>]) -> e(S); +m(S = [<<"xn--mgbai9a5eva00b">>]) -> e(S); +m(S = [<<"xn--ygbi2ammx">>]) -> e(S); +m(S = [<<"xn--90a3ac">>]) -> e(S); +m(S = [<<"xn--o1ac">>,<<"xn--90a3ac">>]) -> e(S); +m(S = [<<"xn--c1avg">>,<<"xn--90a3ac">>]) -> e(S); +m(S = [<<"xn--90azh">>,<<"xn--90a3ac">>]) -> e(S); +m(S = [<<"xn--d1at">>,<<"xn--90a3ac">>]) -> e(S); +m(S = [<<"xn--o1ach">>,<<"xn--90a3ac">>]) -> e(S); +m(S = [<<"xn--80au">>,<<"xn--90a3ac">>]) -> e(S); +m(S = [<<"xn--p1ai">>]) -> e(S); +m(S = [<<"xn--wgbl6a">>]) -> e(S); +m(S = [<<"xn--mgberp4a5d4ar">>]) -> e(S); +m(S = [<<"xn--mgberp4a5d4a87g">>]) -> e(S); +m(S = [<<"xn--mgbqly7c0a67fbc">>]) -> e(S); +m(S = [<<"xn--mgbqly7cvafr">>]) -> e(S); +m(S = [<<"xn--mgbpl2fh">>]) -> e(S); +m(S = [<<"xn--yfro4i67o">>]) -> e(S); +m(S = [<<"xn--clchc0ea0b2g2a9gcd">>]) -> e(S); +m(S = [<<"xn--ogbpf8fl">>]) -> e(S); +m(S = [<<"xn--mgbtf8fl">>]) -> e(S); +m(S = [<<"xn--o3cw4h">>]) -> e(S); +m(S = [<<"xn--12c1fe0br">>,<<"xn--o3cw4h">>]) -> e(S); +m(S = [<<"xn--12co0c3b4eva">>,<<"xn--o3cw4h">>]) -> e(S); +m(S = [<<"xn--h3cuzk1di">>,<<"xn--o3cw4h">>]) -> e(S); +m(S = [<<"xn--o3cyx2a">>,<<"xn--o3cw4h">>]) -> e(S); +m(S = [<<"xn--m3ch0j3a">>,<<"xn--o3cw4h">>]) -> e(S); +m(S = [<<"xn--12cfi8ixb8l">>,<<"xn--o3cw4h">>]) -> e(S); +m(S = [<<"xn--pgbs0dh">>]) -> e(S); +m(S = [<<"xn--kpry57d">>]) -> e(S); +m(S = [<<"xn--kprw13d">>]) -> e(S); +m(S = [<<"xn--nnx388a">>]) -> e(S); +m(S = [<<"xn--j1amh">>]) -> e(S); +m(S = [<<"xn--mgb2ddes">>]) -> e(S); +m(S = [<<"xxx">>]) -> e(S); +m(S = [<<"ye">>]) -> e(S); +m(S = [<<"com">>,<<"ye">>]) -> e(S); +m(S = [<<"edu">>,<<"ye">>]) -> e(S); +m(S = [<<"gov">>,<<"ye">>]) -> e(S); +m(S = [<<"net">>,<<"ye">>]) -> e(S); +m(S = [<<"mil">>,<<"ye">>]) -> e(S); +m(S = [<<"org">>,<<"ye">>]) -> e(S); +m(S = [<<"ac">>,<<"za">>]) -> e(S); +m(S = [<<"agric">>,<<"za">>]) -> e(S); +m(S = [<<"alt">>,<<"za">>]) -> e(S); +m(S = [<<"co">>,<<"za">>]) -> e(S); +m(S = [<<"edu">>,<<"za">>]) -> e(S); +m(S = [<<"gov">>,<<"za">>]) -> e(S); +m(S = [<<"grondar">>,<<"za">>]) -> e(S); +m(S = [<<"law">>,<<"za">>]) -> e(S); +m(S = [<<"mil">>,<<"za">>]) -> e(S); +m(S = [<<"net">>,<<"za">>]) -> e(S); +m(S = [<<"ngo">>,<<"za">>]) -> e(S); +m(S = [<<"nic">>,<<"za">>]) -> e(S); +m(S = [<<"nis">>,<<"za">>]) -> e(S); +m(S = [<<"nom">>,<<"za">>]) -> e(S); +m(S = [<<"org">>,<<"za">>]) -> e(S); +m(S = [<<"school">>,<<"za">>]) -> e(S); +m(S = [<<"tm">>,<<"za">>]) -> e(S); +m(S = [<<"web">>,<<"za">>]) -> e(S); +m(S = [<<"zm">>]) -> e(S); +m(S = [<<"ac">>,<<"zm">>]) -> e(S); +m(S = [<<"biz">>,<<"zm">>]) -> e(S); +m(S = [<<"co">>,<<"zm">>]) -> e(S); +m(S = [<<"com">>,<<"zm">>]) -> e(S); +m(S = [<<"edu">>,<<"zm">>]) -> e(S); +m(S = [<<"gov">>,<<"zm">>]) -> e(S); +m(S = [<<"info">>,<<"zm">>]) -> e(S); +m(S = [<<"mil">>,<<"zm">>]) -> e(S); +m(S = [<<"net">>,<<"zm">>]) -> e(S); +m(S = [<<"org">>,<<"zm">>]) -> e(S); +m(S = [<<"sch">>,<<"zm">>]) -> e(S); +m(S = [<<"zw">>]) -> e(S); +m(S = [<<"ac">>,<<"zw">>]) -> e(S); +m(S = [<<"co">>,<<"zw">>]) -> e(S); +m(S = [<<"gov">>,<<"zw">>]) -> e(S); +m(S = [<<"mil">>,<<"zw">>]) -> e(S); +m(S = [<<"org">>,<<"zw">>]) -> e(S); +m(S = [<<"aaa">>]) -> e(S); +m(S = [<<"aarp">>]) -> e(S); +m(S = [<<"abarth">>]) -> e(S); +m(S = [<<"abb">>]) -> e(S); +m(S = [<<"abbott">>]) -> e(S); +m(S = [<<"abbvie">>]) -> e(S); +m(S = [<<"abc">>]) -> e(S); +m(S = [<<"able">>]) -> e(S); +m(S = [<<"abogado">>]) -> e(S); +m(S = [<<"abudhabi">>]) -> e(S); +m(S = [<<"academy">>]) -> e(S); +m(S = [<<"accenture">>]) -> e(S); +m(S = [<<"accountant">>]) -> e(S); +m(S = [<<"accountants">>]) -> e(S); +m(S = [<<"aco">>]) -> e(S); +m(S = [<<"actor">>]) -> e(S); +m(S = [<<"ads">>]) -> e(S); +m(S = [<<"adult">>]) -> e(S); +m(S = [<<"aeg">>]) -> e(S); +m(S = [<<"aetna">>]) -> e(S); +m(S = [<<"afl">>]) -> e(S); +m(S = [<<"africa">>]) -> e(S); +m(S = [<<"agakhan">>]) -> e(S); +m(S = [<<"agency">>]) -> e(S); +m(S = [<<"aig">>]) -> e(S); +m(S = [<<"airbus">>]) -> e(S); +m(S = [<<"airforce">>]) -> e(S); +m(S = [<<"airtel">>]) -> e(S); +m(S = [<<"akdn">>]) -> e(S); +m(S = [<<"alfaromeo">>]) -> e(S); +m(S = [<<"alibaba">>]) -> e(S); +m(S = [<<"alipay">>]) -> e(S); +m(S = [<<"allfinanz">>]) -> e(S); +m(S = [<<"allstate">>]) -> e(S); +m(S = [<<"ally">>]) -> e(S); +m(S = [<<"alsace">>]) -> e(S); +m(S = [<<"alstom">>]) -> e(S); +m(S = [<<"amazon">>]) -> e(S); +m(S = [<<"americanexpress">>]) -> e(S); +m(S = [<<"americanfamily">>]) -> e(S); +m(S = [<<"amex">>]) -> e(S); +m(S = [<<"amfam">>]) -> e(S); +m(S = [<<"amica">>]) -> e(S); +m(S = [<<"amsterdam">>]) -> e(S); +m(S = [<<"analytics">>]) -> e(S); +m(S = [<<"android">>]) -> e(S); +m(S = [<<"anquan">>]) -> e(S); +m(S = [<<"anz">>]) -> e(S); +m(S = [<<"aol">>]) -> e(S); +m(S = [<<"apartments">>]) -> e(S); +m(S = [<<"app">>]) -> e(S); +m(S = [<<"apple">>]) -> e(S); +m(S = [<<"aquarelle">>]) -> e(S); +m(S = [<<"arab">>]) -> e(S); +m(S = [<<"aramco">>]) -> e(S); +m(S = [<<"archi">>]) -> e(S); +m(S = [<<"army">>]) -> e(S); +m(S = [<<"art">>]) -> e(S); +m(S = [<<"arte">>]) -> e(S); +m(S = [<<"asda">>]) -> e(S); +m(S = [<<"associates">>]) -> e(S); +m(S = [<<"athleta">>]) -> e(S); +m(S = [<<"attorney">>]) -> e(S); +m(S = [<<"auction">>]) -> e(S); +m(S = [<<"audi">>]) -> e(S); +m(S = [<<"audible">>]) -> e(S); +m(S = [<<"audio">>]) -> e(S); +m(S = [<<"auspost">>]) -> e(S); +m(S = [<<"author">>]) -> e(S); +m(S = [<<"auto">>]) -> e(S); +m(S = [<<"autos">>]) -> e(S); +m(S = [<<"avianca">>]) -> e(S); +m(S = [<<"aws">>]) -> e(S); +m(S = [<<"axa">>]) -> e(S); +m(S = [<<"azure">>]) -> e(S); +m(S = [<<"baby">>]) -> e(S); +m(S = [<<"baidu">>]) -> e(S); +m(S = [<<"banamex">>]) -> e(S); +m(S = [<<"bananarepublic">>]) -> e(S); +m(S = [<<"band">>]) -> e(S); +m(S = [<<"bank">>]) -> e(S); +m(S = [<<"bar">>]) -> e(S); +m(S = [<<"barcelona">>]) -> e(S); +m(S = [<<"barclaycard">>]) -> e(S); +m(S = [<<"barclays">>]) -> e(S); +m(S = [<<"barefoot">>]) -> e(S); +m(S = [<<"bargains">>]) -> e(S); +m(S = [<<"baseball">>]) -> e(S); +m(S = [<<"basketball">>]) -> e(S); +m(S = [<<"bauhaus">>]) -> e(S); +m(S = [<<"bayern">>]) -> e(S); +m(S = [<<"bbc">>]) -> e(S); +m(S = [<<"bbt">>]) -> e(S); +m(S = [<<"bbva">>]) -> e(S); +m(S = [<<"bcg">>]) -> e(S); +m(S = [<<"bcn">>]) -> e(S); +m(S = [<<"beats">>]) -> e(S); +m(S = [<<"beauty">>]) -> e(S); +m(S = [<<"beer">>]) -> e(S); +m(S = [<<"bentley">>]) -> e(S); +m(S = [<<"berlin">>]) -> e(S); +m(S = [<<"best">>]) -> e(S); +m(S = [<<"bestbuy">>]) -> e(S); +m(S = [<<"bet">>]) -> e(S); +m(S = [<<"bharti">>]) -> e(S); +m(S = [<<"bible">>]) -> e(S); +m(S = [<<"bid">>]) -> e(S); +m(S = [<<"bike">>]) -> e(S); +m(S = [<<"bing">>]) -> e(S); +m(S = [<<"bingo">>]) -> e(S); +m(S = [<<"bio">>]) -> e(S); +m(S = [<<"black">>]) -> e(S); +m(S = [<<"blackfriday">>]) -> e(S); +m(S = [<<"blockbuster">>]) -> e(S); +m(S = [<<"blog">>]) -> e(S); +m(S = [<<"bloomberg">>]) -> e(S); +m(S = [<<"blue">>]) -> e(S); +m(S = [<<"bms">>]) -> e(S); +m(S = [<<"bmw">>]) -> e(S); +m(S = [<<"bnpparibas">>]) -> e(S); +m(S = [<<"boats">>]) -> e(S); +m(S = [<<"boehringer">>]) -> e(S); +m(S = [<<"bofa">>]) -> e(S); +m(S = [<<"bom">>]) -> e(S); +m(S = [<<"bond">>]) -> e(S); +m(S = [<<"boo">>]) -> e(S); +m(S = [<<"book">>]) -> e(S); +m(S = [<<"booking">>]) -> e(S); +m(S = [<<"bosch">>]) -> e(S); +m(S = [<<"bostik">>]) -> e(S); +m(S = [<<"boston">>]) -> e(S); +m(S = [<<"bot">>]) -> e(S); +m(S = [<<"boutique">>]) -> e(S); +m(S = [<<"box">>]) -> e(S); +m(S = [<<"bradesco">>]) -> e(S); +m(S = [<<"bridgestone">>]) -> e(S); +m(S = [<<"broadway">>]) -> e(S); +m(S = [<<"broker">>]) -> e(S); +m(S = [<<"brother">>]) -> e(S); +m(S = [<<"brussels">>]) -> e(S); +m(S = [<<"build">>]) -> e(S); +m(S = [<<"builders">>]) -> e(S); +m(S = [<<"business">>]) -> e(S); +m(S = [<<"buy">>]) -> e(S); +m(S = [<<"buzz">>]) -> e(S); +m(S = [<<"bzh">>]) -> e(S); +m(S = [<<"cab">>]) -> e(S); +m(S = [<<"cafe">>]) -> e(S); +m(S = [<<"cal">>]) -> e(S); +m(S = [<<"call">>]) -> e(S); +m(S = [<<"calvinklein">>]) -> e(S); +m(S = [<<"cam">>]) -> e(S); +m(S = [<<"camera">>]) -> e(S); +m(S = [<<"camp">>]) -> e(S); +m(S = [<<"canon">>]) -> e(S); +m(S = [<<"capetown">>]) -> e(S); +m(S = [<<"capital">>]) -> e(S); +m(S = [<<"capitalone">>]) -> e(S); +m(S = [<<"car">>]) -> e(S); +m(S = [<<"caravan">>]) -> e(S); +m(S = [<<"cards">>]) -> e(S); +m(S = [<<"care">>]) -> e(S); +m(S = [<<"career">>]) -> e(S); +m(S = [<<"careers">>]) -> e(S); +m(S = [<<"cars">>]) -> e(S); +m(S = [<<"casa">>]) -> e(S); +m(S = [<<"case">>]) -> e(S); +m(S = [<<"cash">>]) -> e(S); +m(S = [<<"casino">>]) -> e(S); +m(S = [<<"catering">>]) -> e(S); +m(S = [<<"catholic">>]) -> e(S); +m(S = [<<"cba">>]) -> e(S); +m(S = [<<"cbn">>]) -> e(S); +m(S = [<<"cbre">>]) -> e(S); +m(S = [<<"cbs">>]) -> e(S); +m(S = [<<"center">>]) -> e(S); +m(S = [<<"ceo">>]) -> e(S); +m(S = [<<"cern">>]) -> e(S); +m(S = [<<"cfa">>]) -> e(S); +m(S = [<<"cfd">>]) -> e(S); +m(S = [<<"chanel">>]) -> e(S); +m(S = [<<"channel">>]) -> e(S); +m(S = [<<"charity">>]) -> e(S); +m(S = [<<"chase">>]) -> e(S); +m(S = [<<"chat">>]) -> e(S); +m(S = [<<"cheap">>]) -> e(S); +m(S = [<<"chintai">>]) -> e(S); +m(S = [<<"christmas">>]) -> e(S); +m(S = [<<"chrome">>]) -> e(S); +m(S = [<<"church">>]) -> e(S); +m(S = [<<"cipriani">>]) -> e(S); +m(S = [<<"circle">>]) -> e(S); +m(S = [<<"cisco">>]) -> e(S); +m(S = [<<"citadel">>]) -> e(S); +m(S = [<<"citi">>]) -> e(S); +m(S = [<<"citic">>]) -> e(S); +m(S = [<<"city">>]) -> e(S); +m(S = [<<"cityeats">>]) -> e(S); +m(S = [<<"claims">>]) -> e(S); +m(S = [<<"cleaning">>]) -> e(S); +m(S = [<<"click">>]) -> e(S); +m(S = [<<"clinic">>]) -> e(S); +m(S = [<<"clinique">>]) -> e(S); +m(S = [<<"clothing">>]) -> e(S); +m(S = [<<"cloud">>]) -> e(S); +m(S = [<<"club">>]) -> e(S); +m(S = [<<"clubmed">>]) -> e(S); +m(S = [<<"coach">>]) -> e(S); +m(S = [<<"codes">>]) -> e(S); +m(S = [<<"coffee">>]) -> e(S); +m(S = [<<"college">>]) -> e(S); +m(S = [<<"cologne">>]) -> e(S); +m(S = [<<"comcast">>]) -> e(S); +m(S = [<<"commbank">>]) -> e(S); +m(S = [<<"community">>]) -> e(S); +m(S = [<<"company">>]) -> e(S); +m(S = [<<"compare">>]) -> e(S); +m(S = [<<"computer">>]) -> e(S); +m(S = [<<"comsec">>]) -> e(S); +m(S = [<<"condos">>]) -> e(S); +m(S = [<<"construction">>]) -> e(S); +m(S = [<<"consulting">>]) -> e(S); +m(S = [<<"contact">>]) -> e(S); +m(S = [<<"contractors">>]) -> e(S); +m(S = [<<"cooking">>]) -> e(S); +m(S = [<<"cookingchannel">>]) -> e(S); +m(S = [<<"cool">>]) -> e(S); +m(S = [<<"corsica">>]) -> e(S); +m(S = [<<"country">>]) -> e(S); +m(S = [<<"coupon">>]) -> e(S); +m(S = [<<"coupons">>]) -> e(S); +m(S = [<<"courses">>]) -> e(S); +m(S = [<<"cpa">>]) -> e(S); +m(S = [<<"credit">>]) -> e(S); +m(S = [<<"creditcard">>]) -> e(S); +m(S = [<<"creditunion">>]) -> e(S); +m(S = [<<"cricket">>]) -> e(S); +m(S = [<<"crown">>]) -> e(S); +m(S = [<<"crs">>]) -> e(S); +m(S = [<<"cruise">>]) -> e(S); +m(S = [<<"cruises">>]) -> e(S); +m(S = [<<"cuisinella">>]) -> e(S); +m(S = [<<"cymru">>]) -> e(S); +m(S = [<<"cyou">>]) -> e(S); +m(S = [<<"dabur">>]) -> e(S); +m(S = [<<"dad">>]) -> e(S); +m(S = [<<"dance">>]) -> e(S); +m(S = [<<"data">>]) -> e(S); +m(S = [<<"date">>]) -> e(S); +m(S = [<<"dating">>]) -> e(S); +m(S = [<<"datsun">>]) -> e(S); +m(S = [<<"day">>]) -> e(S); +m(S = [<<"dclk">>]) -> e(S); +m(S = [<<"dds">>]) -> e(S); +m(S = [<<"deal">>]) -> e(S); +m(S = [<<"dealer">>]) -> e(S); +m(S = [<<"deals">>]) -> e(S); +m(S = [<<"degree">>]) -> e(S); +m(S = [<<"delivery">>]) -> e(S); +m(S = [<<"dell">>]) -> e(S); +m(S = [<<"deloitte">>]) -> e(S); +m(S = [<<"delta">>]) -> e(S); +m(S = [<<"democrat">>]) -> e(S); +m(S = [<<"dental">>]) -> e(S); +m(S = [<<"dentist">>]) -> e(S); +m(S = [<<"desi">>]) -> e(S); +m(S = [<<"design">>]) -> e(S); +m(S = [<<"dev">>]) -> e(S); +m(S = [<<"dhl">>]) -> e(S); +m(S = [<<"diamonds">>]) -> e(S); +m(S = [<<"diet">>]) -> e(S); +m(S = [<<"digital">>]) -> e(S); +m(S = [<<"direct">>]) -> e(S); +m(S = [<<"directory">>]) -> e(S); +m(S = [<<"discount">>]) -> e(S); +m(S = [<<"discover">>]) -> e(S); +m(S = [<<"dish">>]) -> e(S); +m(S = [<<"diy">>]) -> e(S); +m(S = [<<"dnp">>]) -> e(S); +m(S = [<<"docs">>]) -> e(S); +m(S = [<<"doctor">>]) -> e(S); +m(S = [<<"dog">>]) -> e(S); +m(S = [<<"domains">>]) -> e(S); +m(S = [<<"dot">>]) -> e(S); +m(S = [<<"download">>]) -> e(S); +m(S = [<<"drive">>]) -> e(S); +m(S = [<<"dtv">>]) -> e(S); +m(S = [<<"dubai">>]) -> e(S); +m(S = [<<"dunlop">>]) -> e(S); +m(S = [<<"dupont">>]) -> e(S); +m(S = [<<"durban">>]) -> e(S); +m(S = [<<"dvag">>]) -> e(S); +m(S = [<<"dvr">>]) -> e(S); +m(S = [<<"earth">>]) -> e(S); +m(S = [<<"eat">>]) -> e(S); +m(S = [<<"eco">>]) -> e(S); +m(S = [<<"edeka">>]) -> e(S); +m(S = [<<"education">>]) -> e(S); +m(S = [<<"email">>]) -> e(S); +m(S = [<<"emerck">>]) -> e(S); +m(S = [<<"energy">>]) -> e(S); +m(S = [<<"engineer">>]) -> e(S); +m(S = [<<"engineering">>]) -> e(S); +m(S = [<<"enterprises">>]) -> e(S); +m(S = [<<"epson">>]) -> e(S); +m(S = [<<"equipment">>]) -> e(S); +m(S = [<<"ericsson">>]) -> e(S); +m(S = [<<"erni">>]) -> e(S); +m(S = [<<"esq">>]) -> e(S); +m(S = [<<"estate">>]) -> e(S); +m(S = [<<"etisalat">>]) -> e(S); +m(S = [<<"eurovision">>]) -> e(S); +m(S = [<<"eus">>]) -> e(S); +m(S = [<<"events">>]) -> e(S); +m(S = [<<"exchange">>]) -> e(S); +m(S = [<<"expert">>]) -> e(S); +m(S = [<<"exposed">>]) -> e(S); +m(S = [<<"express">>]) -> e(S); +m(S = [<<"extraspace">>]) -> e(S); +m(S = [<<"fage">>]) -> e(S); +m(S = [<<"fail">>]) -> e(S); +m(S = [<<"fairwinds">>]) -> e(S); +m(S = [<<"faith">>]) -> e(S); +m(S = [<<"family">>]) -> e(S); +m(S = [<<"fan">>]) -> e(S); +m(S = [<<"fans">>]) -> e(S); +m(S = [<<"farm">>]) -> e(S); +m(S = [<<"farmers">>]) -> e(S); +m(S = [<<"fashion">>]) -> e(S); +m(S = [<<"fast">>]) -> e(S); +m(S = [<<"fedex">>]) -> e(S); +m(S = [<<"feedback">>]) -> e(S); +m(S = [<<"ferrari">>]) -> e(S); +m(S = [<<"ferrero">>]) -> e(S); +m(S = [<<"fiat">>]) -> e(S); +m(S = [<<"fidelity">>]) -> e(S); +m(S = [<<"fido">>]) -> e(S); +m(S = [<<"film">>]) -> e(S); +m(S = [<<"final">>]) -> e(S); +m(S = [<<"finance">>]) -> e(S); +m(S = [<<"financial">>]) -> e(S); +m(S = [<<"fire">>]) -> e(S); +m(S = [<<"firestone">>]) -> e(S); +m(S = [<<"firmdale">>]) -> e(S); +m(S = [<<"fish">>]) -> e(S); +m(S = [<<"fishing">>]) -> e(S); +m(S = [<<"fit">>]) -> e(S); +m(S = [<<"fitness">>]) -> e(S); +m(S = [<<"flickr">>]) -> e(S); +m(S = [<<"flights">>]) -> e(S); +m(S = [<<"flir">>]) -> e(S); +m(S = [<<"florist">>]) -> e(S); +m(S = [<<"flowers">>]) -> e(S); +m(S = [<<"fly">>]) -> e(S); +m(S = [<<"foo">>]) -> e(S); +m(S = [<<"food">>]) -> e(S); +m(S = [<<"foodnetwork">>]) -> e(S); +m(S = [<<"football">>]) -> e(S); +m(S = [<<"ford">>]) -> e(S); +m(S = [<<"forex">>]) -> e(S); +m(S = [<<"forsale">>]) -> e(S); +m(S = [<<"forum">>]) -> e(S); +m(S = [<<"foundation">>]) -> e(S); +m(S = [<<"fox">>]) -> e(S); +m(S = [<<"free">>]) -> e(S); +m(S = [<<"fresenius">>]) -> e(S); +m(S = [<<"frl">>]) -> e(S); +m(S = [<<"frogans">>]) -> e(S); +m(S = [<<"frontdoor">>]) -> e(S); +m(S = [<<"frontier">>]) -> e(S); +m(S = [<<"ftr">>]) -> e(S); +m(S = [<<"fujitsu">>]) -> e(S); +m(S = [<<"fun">>]) -> e(S); +m(S = [<<"fund">>]) -> e(S); +m(S = [<<"furniture">>]) -> e(S); +m(S = [<<"futbol">>]) -> e(S); +m(S = [<<"fyi">>]) -> e(S); +m(S = [<<"gal">>]) -> e(S); +m(S = [<<"gallery">>]) -> e(S); +m(S = [<<"gallo">>]) -> e(S); +m(S = [<<"gallup">>]) -> e(S); +m(S = [<<"game">>]) -> e(S); +m(S = [<<"games">>]) -> e(S); +m(S = [<<"gap">>]) -> e(S); +m(S = [<<"garden">>]) -> e(S); +m(S = [<<"gay">>]) -> e(S); +m(S = [<<"gbiz">>]) -> e(S); +m(S = [<<"gdn">>]) -> e(S); +m(S = [<<"gea">>]) -> e(S); +m(S = [<<"gent">>]) -> e(S); +m(S = [<<"genting">>]) -> e(S); +m(S = [<<"george">>]) -> e(S); +m(S = [<<"ggee">>]) -> e(S); +m(S = [<<"gift">>]) -> e(S); +m(S = [<<"gifts">>]) -> e(S); +m(S = [<<"gives">>]) -> e(S); +m(S = [<<"giving">>]) -> e(S); +m(S = [<<"glass">>]) -> e(S); +m(S = [<<"gle">>]) -> e(S); +m(S = [<<"global">>]) -> e(S); +m(S = [<<"globo">>]) -> e(S); +m(S = [<<"gmail">>]) -> e(S); +m(S = [<<"gmbh">>]) -> e(S); +m(S = [<<"gmo">>]) -> e(S); +m(S = [<<"gmx">>]) -> e(S); +m(S = [<<"godaddy">>]) -> e(S); +m(S = [<<"gold">>]) -> e(S); +m(S = [<<"goldpoint">>]) -> e(S); +m(S = [<<"golf">>]) -> e(S); +m(S = [<<"goo">>]) -> e(S); +m(S = [<<"goodyear">>]) -> e(S); +m(S = [<<"goog">>]) -> e(S); +m(S = [<<"google">>]) -> e(S); +m(S = [<<"gop">>]) -> e(S); +m(S = [<<"got">>]) -> e(S); +m(S = [<<"grainger">>]) -> e(S); +m(S = [<<"graphics">>]) -> e(S); +m(S = [<<"gratis">>]) -> e(S); +m(S = [<<"green">>]) -> e(S); +m(S = [<<"gripe">>]) -> e(S); +m(S = [<<"grocery">>]) -> e(S); +m(S = [<<"group">>]) -> e(S); +m(S = [<<"guardian">>]) -> e(S); +m(S = [<<"gucci">>]) -> e(S); +m(S = [<<"guge">>]) -> e(S); +m(S = [<<"guide">>]) -> e(S); +m(S = [<<"guitars">>]) -> e(S); +m(S = [<<"guru">>]) -> e(S); +m(S = [<<"hair">>]) -> e(S); +m(S = [<<"hamburg">>]) -> e(S); +m(S = [<<"hangout">>]) -> e(S); +m(S = [<<"haus">>]) -> e(S); +m(S = [<<"hbo">>]) -> e(S); +m(S = [<<"hdfc">>]) -> e(S); +m(S = [<<"hdfcbank">>]) -> e(S); +m(S = [<<"health">>]) -> e(S); +m(S = [<<"healthcare">>]) -> e(S); +m(S = [<<"help">>]) -> e(S); +m(S = [<<"helsinki">>]) -> e(S); +m(S = [<<"here">>]) -> e(S); +m(S = [<<"hermes">>]) -> e(S); +m(S = [<<"hgtv">>]) -> e(S); +m(S = [<<"hiphop">>]) -> e(S); +m(S = [<<"hisamitsu">>]) -> e(S); +m(S = [<<"hitachi">>]) -> e(S); +m(S = [<<"hiv">>]) -> e(S); +m(S = [<<"hkt">>]) -> e(S); +m(S = [<<"hockey">>]) -> e(S); +m(S = [<<"holdings">>]) -> e(S); +m(S = [<<"holiday">>]) -> e(S); +m(S = [<<"homedepot">>]) -> e(S); +m(S = [<<"homegoods">>]) -> e(S); +m(S = [<<"homes">>]) -> e(S); +m(S = [<<"homesense">>]) -> e(S); +m(S = [<<"honda">>]) -> e(S); +m(S = [<<"horse">>]) -> e(S); +m(S = [<<"hospital">>]) -> e(S); +m(S = [<<"host">>]) -> e(S); +m(S = [<<"hosting">>]) -> e(S); +m(S = [<<"hot">>]) -> e(S); +m(S = [<<"hoteles">>]) -> e(S); +m(S = [<<"hotels">>]) -> e(S); +m(S = [<<"hotmail">>]) -> e(S); +m(S = [<<"house">>]) -> e(S); +m(S = [<<"how">>]) -> e(S); +m(S = [<<"hsbc">>]) -> e(S); +m(S = [<<"hughes">>]) -> e(S); +m(S = [<<"hyatt">>]) -> e(S); +m(S = [<<"hyundai">>]) -> e(S); +m(S = [<<"ibm">>]) -> e(S); +m(S = [<<"icbc">>]) -> e(S); +m(S = [<<"ice">>]) -> e(S); +m(S = [<<"icu">>]) -> e(S); +m(S = [<<"ieee">>]) -> e(S); +m(S = [<<"ifm">>]) -> e(S); +m(S = [<<"ikano">>]) -> e(S); +m(S = [<<"imamat">>]) -> e(S); +m(S = [<<"imdb">>]) -> e(S); +m(S = [<<"immo">>]) -> e(S); +m(S = [<<"immobilien">>]) -> e(S); +m(S = [<<"inc">>]) -> e(S); +m(S = [<<"industries">>]) -> e(S); +m(S = [<<"infiniti">>]) -> e(S); +m(S = [<<"ing">>]) -> e(S); +m(S = [<<"ink">>]) -> e(S); +m(S = [<<"institute">>]) -> e(S); +m(S = [<<"insurance">>]) -> e(S); +m(S = [<<"insure">>]) -> e(S); +m(S = [<<"international">>]) -> e(S); +m(S = [<<"intuit">>]) -> e(S); +m(S = [<<"investments">>]) -> e(S); +m(S = [<<"ipiranga">>]) -> e(S); +m(S = [<<"irish">>]) -> e(S); +m(S = [<<"ismaili">>]) -> e(S); +m(S = [<<"ist">>]) -> e(S); +m(S = [<<"istanbul">>]) -> e(S); +m(S = [<<"itau">>]) -> e(S); +m(S = [<<"itv">>]) -> e(S); +m(S = [<<"jaguar">>]) -> e(S); +m(S = [<<"java">>]) -> e(S); +m(S = [<<"jcb">>]) -> e(S); +m(S = [<<"jeep">>]) -> e(S); +m(S = [<<"jetzt">>]) -> e(S); +m(S = [<<"jewelry">>]) -> e(S); +m(S = [<<"jio">>]) -> e(S); +m(S = [<<"jll">>]) -> e(S); +m(S = [<<"jmp">>]) -> e(S); +m(S = [<<"jnj">>]) -> e(S); +m(S = [<<"joburg">>]) -> e(S); +m(S = [<<"jot">>]) -> e(S); +m(S = [<<"joy">>]) -> e(S); +m(S = [<<"jpmorgan">>]) -> e(S); +m(S = [<<"jprs">>]) -> e(S); +m(S = [<<"juegos">>]) -> e(S); +m(S = [<<"juniper">>]) -> e(S); +m(S = [<<"kaufen">>]) -> e(S); +m(S = [<<"kddi">>]) -> e(S); +m(S = [<<"kerryhotels">>]) -> e(S); +m(S = [<<"kerrylogistics">>]) -> e(S); +m(S = [<<"kerryproperties">>]) -> e(S); +m(S = [<<"kfh">>]) -> e(S); +m(S = [<<"kia">>]) -> e(S); +m(S = [<<"kids">>]) -> e(S); +m(S = [<<"kim">>]) -> e(S); +m(S = [<<"kinder">>]) -> e(S); +m(S = [<<"kindle">>]) -> e(S); +m(S = [<<"kitchen">>]) -> e(S); +m(S = [<<"kiwi">>]) -> e(S); +m(S = [<<"koeln">>]) -> e(S); +m(S = [<<"komatsu">>]) -> e(S); +m(S = [<<"kosher">>]) -> e(S); +m(S = [<<"kpmg">>]) -> e(S); +m(S = [<<"kpn">>]) -> e(S); +m(S = [<<"krd">>]) -> e(S); +m(S = [<<"kred">>]) -> e(S); +m(S = [<<"kuokgroup">>]) -> e(S); +m(S = [<<"kyoto">>]) -> e(S); +m(S = [<<"lacaixa">>]) -> e(S); +m(S = [<<"lamborghini">>]) -> e(S); +m(S = [<<"lamer">>]) -> e(S); +m(S = [<<"lancaster">>]) -> e(S); +m(S = [<<"lancia">>]) -> e(S); +m(S = [<<"land">>]) -> e(S); +m(S = [<<"landrover">>]) -> e(S); +m(S = [<<"lanxess">>]) -> e(S); +m(S = [<<"lasalle">>]) -> e(S); +m(S = [<<"lat">>]) -> e(S); +m(S = [<<"latino">>]) -> e(S); +m(S = [<<"latrobe">>]) -> e(S); +m(S = [<<"law">>]) -> e(S); +m(S = [<<"lawyer">>]) -> e(S); +m(S = [<<"lds">>]) -> e(S); +m(S = [<<"lease">>]) -> e(S); +m(S = [<<"leclerc">>]) -> e(S); +m(S = [<<"lefrak">>]) -> e(S); +m(S = [<<"legal">>]) -> e(S); +m(S = [<<"lego">>]) -> e(S); +m(S = [<<"lexus">>]) -> e(S); +m(S = [<<"lgbt">>]) -> e(S); +m(S = [<<"lidl">>]) -> e(S); +m(S = [<<"life">>]) -> e(S); +m(S = [<<"lifeinsurance">>]) -> e(S); +m(S = [<<"lifestyle">>]) -> e(S); +m(S = [<<"lighting">>]) -> e(S); +m(S = [<<"like">>]) -> e(S); +m(S = [<<"lilly">>]) -> e(S); +m(S = [<<"limited">>]) -> e(S); +m(S = [<<"limo">>]) -> e(S); +m(S = [<<"lincoln">>]) -> e(S); +m(S = [<<"linde">>]) -> e(S); +m(S = [<<"link">>]) -> e(S); +m(S = [<<"lipsy">>]) -> e(S); +m(S = [<<"live">>]) -> e(S); +m(S = [<<"living">>]) -> e(S); +m(S = [<<"llc">>]) -> e(S); +m(S = [<<"llp">>]) -> e(S); +m(S = [<<"loan">>]) -> e(S); +m(S = [<<"loans">>]) -> e(S); +m(S = [<<"locker">>]) -> e(S); +m(S = [<<"locus">>]) -> e(S); +m(S = [<<"loft">>]) -> e(S); +m(S = [<<"lol">>]) -> e(S); +m(S = [<<"london">>]) -> e(S); +m(S = [<<"lotte">>]) -> e(S); +m(S = [<<"lotto">>]) -> e(S); +m(S = [<<"love">>]) -> e(S); +m(S = [<<"lpl">>]) -> e(S); +m(S = [<<"lplfinancial">>]) -> e(S); +m(S = [<<"ltd">>]) -> e(S); +m(S = [<<"ltda">>]) -> e(S); +m(S = [<<"lundbeck">>]) -> e(S); +m(S = [<<"luxe">>]) -> e(S); +m(S = [<<"luxury">>]) -> e(S); +m(S = [<<"macys">>]) -> e(S); +m(S = [<<"madrid">>]) -> e(S); +m(S = [<<"maif">>]) -> e(S); +m(S = [<<"maison">>]) -> e(S); +m(S = [<<"makeup">>]) -> e(S); +m(S = [<<"man">>]) -> e(S); +m(S = [<<"management">>]) -> e(S); +m(S = [<<"mango">>]) -> e(S); +m(S = [<<"map">>]) -> e(S); +m(S = [<<"market">>]) -> e(S); +m(S = [<<"marketing">>]) -> e(S); +m(S = [<<"markets">>]) -> e(S); +m(S = [<<"marriott">>]) -> e(S); +m(S = [<<"marshalls">>]) -> e(S); +m(S = [<<"maserati">>]) -> e(S); +m(S = [<<"mattel">>]) -> e(S); +m(S = [<<"mba">>]) -> e(S); +m(S = [<<"mckinsey">>]) -> e(S); +m(S = [<<"med">>]) -> e(S); +m(S = [<<"media">>]) -> e(S); +m(S = [<<"meet">>]) -> e(S); +m(S = [<<"melbourne">>]) -> e(S); +m(S = [<<"meme">>]) -> e(S); +m(S = [<<"memorial">>]) -> e(S); +m(S = [<<"men">>]) -> e(S); +m(S = [<<"menu">>]) -> e(S); +m(S = [<<"merckmsd">>]) -> e(S); +m(S = [<<"miami">>]) -> e(S); +m(S = [<<"microsoft">>]) -> e(S); +m(S = [<<"mini">>]) -> e(S); +m(S = [<<"mint">>]) -> e(S); +m(S = [<<"mit">>]) -> e(S); +m(S = [<<"mitsubishi">>]) -> e(S); +m(S = [<<"mlb">>]) -> e(S); +m(S = [<<"mls">>]) -> e(S); +m(S = [<<"mma">>]) -> e(S); +m(S = [<<"mobile">>]) -> e(S); +m(S = [<<"moda">>]) -> e(S); +m(S = [<<"moe">>]) -> e(S); +m(S = [<<"moi">>]) -> e(S); +m(S = [<<"mom">>]) -> e(S); +m(S = [<<"monash">>]) -> e(S); +m(S = [<<"money">>]) -> e(S); +m(S = [<<"monster">>]) -> e(S); +m(S = [<<"mormon">>]) -> e(S); +m(S = [<<"mortgage">>]) -> e(S); +m(S = [<<"moscow">>]) -> e(S); +m(S = [<<"moto">>]) -> e(S); +m(S = [<<"motorcycles">>]) -> e(S); +m(S = [<<"mov">>]) -> e(S); +m(S = [<<"movie">>]) -> e(S); +m(S = [<<"msd">>]) -> e(S); +m(S = [<<"mtn">>]) -> e(S); +m(S = [<<"mtr">>]) -> e(S); +m(S = [<<"music">>]) -> e(S); +m(S = [<<"mutual">>]) -> e(S); +m(S = [<<"nab">>]) -> e(S); +m(S = [<<"nagoya">>]) -> e(S); +m(S = [<<"natura">>]) -> e(S); +m(S = [<<"navy">>]) -> e(S); +m(S = [<<"nba">>]) -> e(S); +m(S = [<<"nec">>]) -> e(S); +m(S = [<<"netbank">>]) -> e(S); +m(S = [<<"netflix">>]) -> e(S); +m(S = [<<"network">>]) -> e(S); +m(S = [<<"neustar">>]) -> e(S); +m(S = [<<"new">>]) -> e(S); +m(S = [<<"news">>]) -> e(S); +m(S = [<<"next">>]) -> e(S); +m(S = [<<"nextdirect">>]) -> e(S); +m(S = [<<"nexus">>]) -> e(S); +m(S = [<<"nfl">>]) -> e(S); +m(S = [<<"ngo">>]) -> e(S); +m(S = [<<"nhk">>]) -> e(S); +m(S = [<<"nico">>]) -> e(S); +m(S = [<<"nike">>]) -> e(S); +m(S = [<<"nikon">>]) -> e(S); +m(S = [<<"ninja">>]) -> e(S); +m(S = [<<"nissan">>]) -> e(S); +m(S = [<<"nissay">>]) -> e(S); +m(S = [<<"nokia">>]) -> e(S); +m(S = [<<"northwesternmutual">>]) -> e(S); +m(S = [<<"norton">>]) -> e(S); +m(S = [<<"now">>]) -> e(S); +m(S = [<<"nowruz">>]) -> e(S); +m(S = [<<"nowtv">>]) -> e(S); +m(S = [<<"nra">>]) -> e(S); +m(S = [<<"nrw">>]) -> e(S); +m(S = [<<"ntt">>]) -> e(S); +m(S = [<<"nyc">>]) -> e(S); +m(S = [<<"obi">>]) -> e(S); +m(S = [<<"observer">>]) -> e(S); +m(S = [<<"office">>]) -> e(S); +m(S = [<<"okinawa">>]) -> e(S); +m(S = [<<"olayan">>]) -> e(S); +m(S = [<<"olayangroup">>]) -> e(S); +m(S = [<<"oldnavy">>]) -> e(S); +m(S = [<<"ollo">>]) -> e(S); +m(S = [<<"omega">>]) -> e(S); +m(S = [<<"one">>]) -> e(S); +m(S = [<<"ong">>]) -> e(S); +m(S = [<<"onl">>]) -> e(S); +m(S = [<<"online">>]) -> e(S); +m(S = [<<"ooo">>]) -> e(S); +m(S = [<<"open">>]) -> e(S); +m(S = [<<"oracle">>]) -> e(S); +m(S = [<<"orange">>]) -> e(S); +m(S = [<<"organic">>]) -> e(S); +m(S = [<<"origins">>]) -> e(S); +m(S = [<<"osaka">>]) -> e(S); +m(S = [<<"otsuka">>]) -> e(S); +m(S = [<<"ott">>]) -> e(S); +m(S = [<<"ovh">>]) -> e(S); +m(S = [<<"page">>]) -> e(S); +m(S = [<<"panasonic">>]) -> e(S); +m(S = [<<"paris">>]) -> e(S); +m(S = [<<"pars">>]) -> e(S); +m(S = [<<"partners">>]) -> e(S); +m(S = [<<"parts">>]) -> e(S); +m(S = [<<"party">>]) -> e(S); +m(S = [<<"passagens">>]) -> e(S); +m(S = [<<"pay">>]) -> e(S); +m(S = [<<"pccw">>]) -> e(S); +m(S = [<<"pet">>]) -> e(S); +m(S = [<<"pfizer">>]) -> e(S); +m(S = [<<"pharmacy">>]) -> e(S); +m(S = [<<"phd">>]) -> e(S); +m(S = [<<"philips">>]) -> e(S); +m(S = [<<"phone">>]) -> e(S); +m(S = [<<"photo">>]) -> e(S); +m(S = [<<"photography">>]) -> e(S); +m(S = [<<"photos">>]) -> e(S); +m(S = [<<"physio">>]) -> e(S); +m(S = [<<"pics">>]) -> e(S); +m(S = [<<"pictet">>]) -> e(S); +m(S = [<<"pictures">>]) -> e(S); +m(S = [<<"pid">>]) -> e(S); +m(S = [<<"pin">>]) -> e(S); +m(S = [<<"ping">>]) -> e(S); +m(S = [<<"pink">>]) -> e(S); +m(S = [<<"pioneer">>]) -> e(S); +m(S = [<<"pizza">>]) -> e(S); +m(S = [<<"place">>]) -> e(S); +m(S = [<<"play">>]) -> e(S); +m(S = [<<"playstation">>]) -> e(S); +m(S = [<<"plumbing">>]) -> e(S); +m(S = [<<"plus">>]) -> e(S); +m(S = [<<"pnc">>]) -> e(S); +m(S = [<<"pohl">>]) -> e(S); +m(S = [<<"poker">>]) -> e(S); +m(S = [<<"politie">>]) -> e(S); +m(S = [<<"porn">>]) -> e(S); +m(S = [<<"pramerica">>]) -> e(S); +m(S = [<<"praxi">>]) -> e(S); +m(S = [<<"press">>]) -> e(S); +m(S = [<<"prime">>]) -> e(S); +m(S = [<<"prod">>]) -> e(S); +m(S = [<<"productions">>]) -> e(S); +m(S = [<<"prof">>]) -> e(S); +m(S = [<<"progressive">>]) -> e(S); +m(S = [<<"promo">>]) -> e(S); +m(S = [<<"properties">>]) -> e(S); +m(S = [<<"property">>]) -> e(S); +m(S = [<<"protection">>]) -> e(S); +m(S = [<<"pru">>]) -> e(S); +m(S = [<<"prudential">>]) -> e(S); +m(S = [<<"pub">>]) -> e(S); +m(S = [<<"pwc">>]) -> e(S); +m(S = [<<"qpon">>]) -> e(S); +m(S = [<<"quebec">>]) -> e(S); +m(S = [<<"quest">>]) -> e(S); +m(S = [<<"racing">>]) -> e(S); +m(S = [<<"radio">>]) -> e(S); +m(S = [<<"read">>]) -> e(S); +m(S = [<<"realestate">>]) -> e(S); +m(S = [<<"realtor">>]) -> e(S); +m(S = [<<"realty">>]) -> e(S); +m(S = [<<"recipes">>]) -> e(S); +m(S = [<<"red">>]) -> e(S); +m(S = [<<"redstone">>]) -> e(S); +m(S = [<<"redumbrella">>]) -> e(S); +m(S = [<<"rehab">>]) -> e(S); +m(S = [<<"reise">>]) -> e(S); +m(S = [<<"reisen">>]) -> e(S); +m(S = [<<"reit">>]) -> e(S); +m(S = [<<"reliance">>]) -> e(S); +m(S = [<<"ren">>]) -> e(S); +m(S = [<<"rent">>]) -> e(S); +m(S = [<<"rentals">>]) -> e(S); +m(S = [<<"repair">>]) -> e(S); +m(S = [<<"report">>]) -> e(S); +m(S = [<<"republican">>]) -> e(S); +m(S = [<<"rest">>]) -> e(S); +m(S = [<<"restaurant">>]) -> e(S); +m(S = [<<"review">>]) -> e(S); +m(S = [<<"reviews">>]) -> e(S); +m(S = [<<"rexroth">>]) -> e(S); +m(S = [<<"rich">>]) -> e(S); +m(S = [<<"richardli">>]) -> e(S); +m(S = [<<"ricoh">>]) -> e(S); +m(S = [<<"ril">>]) -> e(S); +m(S = [<<"rio">>]) -> e(S); +m(S = [<<"rip">>]) -> e(S); +m(S = [<<"rocher">>]) -> e(S); +m(S = [<<"rocks">>]) -> e(S); +m(S = [<<"rodeo">>]) -> e(S); +m(S = [<<"rogers">>]) -> e(S); +m(S = [<<"room">>]) -> e(S); +m(S = [<<"rsvp">>]) -> e(S); +m(S = [<<"rugby">>]) -> e(S); +m(S = [<<"ruhr">>]) -> e(S); +m(S = [<<"run">>]) -> e(S); +m(S = [<<"rwe">>]) -> e(S); +m(S = [<<"ryukyu">>]) -> e(S); +m(S = [<<"saarland">>]) -> e(S); +m(S = [<<"safe">>]) -> e(S); +m(S = [<<"safety">>]) -> e(S); +m(S = [<<"sakura">>]) -> e(S); +m(S = [<<"sale">>]) -> e(S); +m(S = [<<"salon">>]) -> e(S); +m(S = [<<"samsclub">>]) -> e(S); +m(S = [<<"samsung">>]) -> e(S); +m(S = [<<"sandvik">>]) -> e(S); +m(S = [<<"sandvikcoromant">>]) -> e(S); +m(S = [<<"sanofi">>]) -> e(S); +m(S = [<<"sap">>]) -> e(S); +m(S = [<<"sarl">>]) -> e(S); +m(S = [<<"sas">>]) -> e(S); +m(S = [<<"save">>]) -> e(S); +m(S = [<<"saxo">>]) -> e(S); +m(S = [<<"sbi">>]) -> e(S); +m(S = [<<"sbs">>]) -> e(S); +m(S = [<<"sca">>]) -> e(S); +m(S = [<<"scb">>]) -> e(S); +m(S = [<<"schaeffler">>]) -> e(S); +m(S = [<<"schmidt">>]) -> e(S); +m(S = [<<"scholarships">>]) -> e(S); +m(S = [<<"school">>]) -> e(S); +m(S = [<<"schule">>]) -> e(S); +m(S = [<<"schwarz">>]) -> e(S); +m(S = [<<"science">>]) -> e(S); +m(S = [<<"scot">>]) -> e(S); +m(S = [<<"search">>]) -> e(S); +m(S = [<<"seat">>]) -> e(S); +m(S = [<<"secure">>]) -> e(S); +m(S = [<<"security">>]) -> e(S); +m(S = [<<"seek">>]) -> e(S); +m(S = [<<"select">>]) -> e(S); +m(S = [<<"sener">>]) -> e(S); +m(S = [<<"services">>]) -> e(S); +m(S = [<<"ses">>]) -> e(S); +m(S = [<<"seven">>]) -> e(S); +m(S = [<<"sew">>]) -> e(S); +m(S = [<<"sex">>]) -> e(S); +m(S = [<<"sexy">>]) -> e(S); +m(S = [<<"sfr">>]) -> e(S); +m(S = [<<"shangrila">>]) -> e(S); +m(S = [<<"sharp">>]) -> e(S); +m(S = [<<"shaw">>]) -> e(S); +m(S = [<<"shell">>]) -> e(S); +m(S = [<<"shia">>]) -> e(S); +m(S = [<<"shiksha">>]) -> e(S); +m(S = [<<"shoes">>]) -> e(S); +m(S = [<<"shop">>]) -> e(S); +m(S = [<<"shopping">>]) -> e(S); +m(S = [<<"shouji">>]) -> e(S); +m(S = [<<"show">>]) -> e(S); +m(S = [<<"showtime">>]) -> e(S); +m(S = [<<"silk">>]) -> e(S); +m(S = [<<"sina">>]) -> e(S); +m(S = [<<"singles">>]) -> e(S); +m(S = [<<"site">>]) -> e(S); +m(S = [<<"ski">>]) -> e(S); +m(S = [<<"skin">>]) -> e(S); +m(S = [<<"sky">>]) -> e(S); +m(S = [<<"skype">>]) -> e(S); +m(S = [<<"sling">>]) -> e(S); +m(S = [<<"smart">>]) -> e(S); +m(S = [<<"smile">>]) -> e(S); +m(S = [<<"sncf">>]) -> e(S); +m(S = [<<"soccer">>]) -> e(S); +m(S = [<<"social">>]) -> e(S); +m(S = [<<"softbank">>]) -> e(S); +m(S = [<<"software">>]) -> e(S); +m(S = [<<"sohu">>]) -> e(S); +m(S = [<<"solar">>]) -> e(S); +m(S = [<<"solutions">>]) -> e(S); +m(S = [<<"song">>]) -> e(S); +m(S = [<<"sony">>]) -> e(S); +m(S = [<<"soy">>]) -> e(S); +m(S = [<<"spa">>]) -> e(S); +m(S = [<<"space">>]) -> e(S); +m(S = [<<"sport">>]) -> e(S); +m(S = [<<"spot">>]) -> e(S); +m(S = [<<"srl">>]) -> e(S); +m(S = [<<"stada">>]) -> e(S); +m(S = [<<"staples">>]) -> e(S); +m(S = [<<"star">>]) -> e(S); +m(S = [<<"statebank">>]) -> e(S); +m(S = [<<"statefarm">>]) -> e(S); +m(S = [<<"stc">>]) -> e(S); +m(S = [<<"stcgroup">>]) -> e(S); +m(S = [<<"stockholm">>]) -> e(S); +m(S = [<<"storage">>]) -> e(S); +m(S = [<<"store">>]) -> e(S); +m(S = [<<"stream">>]) -> e(S); +m(S = [<<"studio">>]) -> e(S); +m(S = [<<"study">>]) -> e(S); +m(S = [<<"style">>]) -> e(S); +m(S = [<<"sucks">>]) -> e(S); +m(S = [<<"supplies">>]) -> e(S); +m(S = [<<"supply">>]) -> e(S); +m(S = [<<"support">>]) -> e(S); +m(S = [<<"surf">>]) -> e(S); +m(S = [<<"surgery">>]) -> e(S); +m(S = [<<"suzuki">>]) -> e(S); +m(S = [<<"swatch">>]) -> e(S); +m(S = [<<"swiss">>]) -> e(S); +m(S = [<<"sydney">>]) -> e(S); +m(S = [<<"systems">>]) -> e(S); +m(S = [<<"tab">>]) -> e(S); +m(S = [<<"taipei">>]) -> e(S); +m(S = [<<"talk">>]) -> e(S); +m(S = [<<"taobao">>]) -> e(S); +m(S = [<<"target">>]) -> e(S); +m(S = [<<"tatamotors">>]) -> e(S); +m(S = [<<"tatar">>]) -> e(S); +m(S = [<<"tattoo">>]) -> e(S); +m(S = [<<"tax">>]) -> e(S); +m(S = [<<"taxi">>]) -> e(S); +m(S = [<<"tci">>]) -> e(S); +m(S = [<<"tdk">>]) -> e(S); +m(S = [<<"team">>]) -> e(S); +m(S = [<<"tech">>]) -> e(S); +m(S = [<<"technology">>]) -> e(S); +m(S = [<<"temasek">>]) -> e(S); +m(S = [<<"tennis">>]) -> e(S); +m(S = [<<"teva">>]) -> e(S); +m(S = [<<"thd">>]) -> e(S); +m(S = [<<"theater">>]) -> e(S); +m(S = [<<"theatre">>]) -> e(S); +m(S = [<<"tiaa">>]) -> e(S); +m(S = [<<"tickets">>]) -> e(S); +m(S = [<<"tienda">>]) -> e(S); +m(S = [<<"tiffany">>]) -> e(S); +m(S = [<<"tips">>]) -> e(S); +m(S = [<<"tires">>]) -> e(S); +m(S = [<<"tirol">>]) -> e(S); +m(S = [<<"tjmaxx">>]) -> e(S); +m(S = [<<"tjx">>]) -> e(S); +m(S = [<<"tkmaxx">>]) -> e(S); +m(S = [<<"tmall">>]) -> e(S); +m(S = [<<"today">>]) -> e(S); +m(S = [<<"tokyo">>]) -> e(S); +m(S = [<<"tools">>]) -> e(S); +m(S = [<<"top">>]) -> e(S); +m(S = [<<"toray">>]) -> e(S); +m(S = [<<"toshiba">>]) -> e(S); +m(S = [<<"total">>]) -> e(S); +m(S = [<<"tours">>]) -> e(S); +m(S = [<<"town">>]) -> e(S); +m(S = [<<"toyota">>]) -> e(S); +m(S = [<<"toys">>]) -> e(S); +m(S = [<<"trade">>]) -> e(S); +m(S = [<<"trading">>]) -> e(S); +m(S = [<<"training">>]) -> e(S); +m(S = [<<"travel">>]) -> e(S); +m(S = [<<"travelchannel">>]) -> e(S); +m(S = [<<"travelers">>]) -> e(S); +m(S = [<<"travelersinsurance">>]) -> e(S); +m(S = [<<"trust">>]) -> e(S); +m(S = [<<"trv">>]) -> e(S); +m(S = [<<"tube">>]) -> e(S); +m(S = [<<"tui">>]) -> e(S); +m(S = [<<"tunes">>]) -> e(S); +m(S = [<<"tushu">>]) -> e(S); +m(S = [<<"tvs">>]) -> e(S); +m(S = [<<"ubank">>]) -> e(S); +m(S = [<<"ubs">>]) -> e(S); +m(S = [<<"unicom">>]) -> e(S); +m(S = [<<"university">>]) -> e(S); +m(S = [<<"uno">>]) -> e(S); +m(S = [<<"uol">>]) -> e(S); +m(S = [<<"ups">>]) -> e(S); +m(S = [<<"vacations">>]) -> e(S); +m(S = [<<"vana">>]) -> e(S); +m(S = [<<"vanguard">>]) -> e(S); +m(S = [<<"vegas">>]) -> e(S); +m(S = [<<"ventures">>]) -> e(S); +m(S = [<<"verisign">>]) -> e(S); +m(S = [<<"versicherung">>]) -> e(S); +m(S = [<<"vet">>]) -> e(S); +m(S = [<<"viajes">>]) -> e(S); +m(S = [<<"video">>]) -> e(S); +m(S = [<<"vig">>]) -> e(S); +m(S = [<<"viking">>]) -> e(S); +m(S = [<<"villas">>]) -> e(S); +m(S = [<<"vin">>]) -> e(S); +m(S = [<<"vip">>]) -> e(S); +m(S = [<<"virgin">>]) -> e(S); +m(S = [<<"visa">>]) -> e(S); +m(S = [<<"vision">>]) -> e(S); +m(S = [<<"viva">>]) -> e(S); +m(S = [<<"vivo">>]) -> e(S); +m(S = [<<"vlaanderen">>]) -> e(S); +m(S = [<<"vodka">>]) -> e(S); +m(S = [<<"volkswagen">>]) -> e(S); +m(S = [<<"volvo">>]) -> e(S); +m(S = [<<"vote">>]) -> e(S); +m(S = [<<"voting">>]) -> e(S); +m(S = [<<"voto">>]) -> e(S); +m(S = [<<"voyage">>]) -> e(S); +m(S = [<<"vuelos">>]) -> e(S); +m(S = [<<"wales">>]) -> e(S); +m(S = [<<"walmart">>]) -> e(S); +m(S = [<<"walter">>]) -> e(S); +m(S = [<<"wang">>]) -> e(S); +m(S = [<<"wanggou">>]) -> e(S); +m(S = [<<"watch">>]) -> e(S); +m(S = [<<"watches">>]) -> e(S); +m(S = [<<"weather">>]) -> e(S); +m(S = [<<"weatherchannel">>]) -> e(S); +m(S = [<<"webcam">>]) -> e(S); +m(S = [<<"weber">>]) -> e(S); +m(S = [<<"website">>]) -> e(S); +m(S = [<<"wedding">>]) -> e(S); +m(S = [<<"weibo">>]) -> e(S); +m(S = [<<"weir">>]) -> e(S); +m(S = [<<"whoswho">>]) -> e(S); +m(S = [<<"wien">>]) -> e(S); +m(S = [<<"wiki">>]) -> e(S); +m(S = [<<"williamhill">>]) -> e(S); +m(S = [<<"win">>]) -> e(S); +m(S = [<<"windows">>]) -> e(S); +m(S = [<<"wine">>]) -> e(S); +m(S = [<<"winners">>]) -> e(S); +m(S = [<<"wme">>]) -> e(S); +m(S = [<<"wolterskluwer">>]) -> e(S); +m(S = [<<"woodside">>]) -> e(S); +m(S = [<<"work">>]) -> e(S); +m(S = [<<"works">>]) -> e(S); +m(S = [<<"world">>]) -> e(S); +m(S = [<<"wow">>]) -> e(S); +m(S = [<<"wtc">>]) -> e(S); +m(S = [<<"wtf">>]) -> e(S); +m(S = [<<"xbox">>]) -> e(S); +m(S = [<<"xerox">>]) -> e(S); +m(S = [<<"xfinity">>]) -> e(S); +m(S = [<<"xihuan">>]) -> e(S); +m(S = [<<"xin">>]) -> e(S); +m(S = [<<"xn--11b4c3d">>]) -> e(S); +m(S = [<<"xn--1ck2e1b">>]) -> e(S); +m(S = [<<"xn--1qqw23a">>]) -> e(S); +m(S = [<<"xn--30rr7y">>]) -> e(S); +m(S = [<<"xn--3bst00m">>]) -> e(S); +m(S = [<<"xn--3ds443g">>]) -> e(S); +m(S = [<<"xn--3pxu8k">>]) -> e(S); +m(S = [<<"xn--42c2d9a">>]) -> e(S); +m(S = [<<"xn--45q11c">>]) -> e(S); +m(S = [<<"xn--4gbrim">>]) -> e(S); +m(S = [<<"xn--55qw42g">>]) -> e(S); +m(S = [<<"xn--55qx5d">>]) -> e(S); +m(S = [<<"xn--5su34j936bgsg">>]) -> e(S); +m(S = [<<"xn--5tzm5g">>]) -> e(S); +m(S = [<<"xn--6frz82g">>]) -> e(S); +m(S = [<<"xn--6qq986b3xl">>]) -> e(S); +m(S = [<<"xn--80adxhks">>]) -> e(S); +m(S = [<<"xn--80aqecdr1a">>]) -> e(S); +m(S = [<<"xn--80asehdb">>]) -> e(S); +m(S = [<<"xn--80aswg">>]) -> e(S); +m(S = [<<"xn--8y0a063a">>]) -> e(S); +m(S = [<<"xn--9dbq2a">>]) -> e(S); +m(S = [<<"xn--9et52u">>]) -> e(S); +m(S = [<<"xn--9krt00a">>]) -> e(S); +m(S = [<<"xn--b4w605ferd">>]) -> e(S); +m(S = [<<"xn--bck1b9a5dre4c">>]) -> e(S); +m(S = [<<"xn--c1avg">>]) -> e(S); +m(S = [<<"xn--c2br7g">>]) -> e(S); +m(S = [<<"xn--cck2b3b">>]) -> e(S); +m(S = [<<"xn--cckwcxetd">>]) -> e(S); +m(S = [<<"xn--cg4bki">>]) -> e(S); +m(S = [<<"xn--czr694b">>]) -> e(S); +m(S = [<<"xn--czrs0t">>]) -> e(S); +m(S = [<<"xn--czru2d">>]) -> e(S); +m(S = [<<"xn--d1acj3b">>]) -> e(S); +m(S = [<<"xn--eckvdtc9d">>]) -> e(S); +m(S = [<<"xn--efvy88h">>]) -> e(S); +m(S = [<<"xn--fct429k">>]) -> e(S); +m(S = [<<"xn--fhbei">>]) -> e(S); +m(S = [<<"xn--fiq228c5hs">>]) -> e(S); +m(S = [<<"xn--fiq64b">>]) -> e(S); +m(S = [<<"xn--fjq720a">>]) -> e(S); +m(S = [<<"xn--flw351e">>]) -> e(S); +m(S = [<<"xn--fzys8d69uvgm">>]) -> e(S); +m(S = [<<"xn--g2xx48c">>]) -> e(S); +m(S = [<<"xn--gckr3f0f">>]) -> e(S); +m(S = [<<"xn--gk3at1e">>]) -> e(S); +m(S = [<<"xn--hxt814e">>]) -> e(S); +m(S = [<<"xn--i1b6b1a6a2e">>]) -> e(S); +m(S = [<<"xn--imr513n">>]) -> e(S); +m(S = [<<"xn--io0a7i">>]) -> e(S); +m(S = [<<"xn--j1aef">>]) -> e(S); +m(S = [<<"xn--jlq480n2rg">>]) -> e(S); +m(S = [<<"xn--jvr189m">>]) -> e(S); +m(S = [<<"xn--kcrx77d1x4a">>]) -> e(S); +m(S = [<<"xn--kput3i">>]) -> e(S); +m(S = [<<"xn--mgba3a3ejt">>]) -> e(S); +m(S = [<<"xn--mgba7c0bbn0a">>]) -> e(S); +m(S = [<<"xn--mgbaakc7dvf">>]) -> e(S); +m(S = [<<"xn--mgbab2bd">>]) -> e(S); +m(S = [<<"xn--mgbca7dzdo">>]) -> e(S); +m(S = [<<"xn--mgbi4ecexp">>]) -> e(S); +m(S = [<<"xn--mgbt3dhd">>]) -> e(S); +m(S = [<<"xn--mk1bu44c">>]) -> e(S); +m(S = [<<"xn--mxtq1m">>]) -> e(S); +m(S = [<<"xn--ngbc5azd">>]) -> e(S); +m(S = [<<"xn--ngbe9e0a">>]) -> e(S); +m(S = [<<"xn--ngbrx">>]) -> e(S); +m(S = [<<"xn--nqv7f">>]) -> e(S); +m(S = [<<"xn--nqv7fs00ema">>]) -> e(S); +m(S = [<<"xn--nyqy26a">>]) -> e(S); +m(S = [<<"xn--otu796d">>]) -> e(S); +m(S = [<<"xn--p1acf">>]) -> e(S); +m(S = [<<"xn--pssy2u">>]) -> e(S); +m(S = [<<"xn--q9jyb4c">>]) -> e(S); +m(S = [<<"xn--qcka1pmc">>]) -> e(S); +m(S = [<<"xn--rhqv96g">>]) -> e(S); +m(S = [<<"xn--rovu88b">>]) -> e(S); +m(S = [<<"xn--ses554g">>]) -> e(S); +m(S = [<<"xn--t60b56a">>]) -> e(S); +m(S = [<<"xn--tckwe">>]) -> e(S); +m(S = [<<"xn--tiq49xqyj">>]) -> e(S); +m(S = [<<"xn--unup4y">>]) -> e(S); +m(S = [<<"xn--vermgensberater-ctb">>]) -> e(S); +m(S = [<<"xn--vermgensberatung-pwb">>]) -> e(S); +m(S = [<<"xn--vhquv">>]) -> e(S); +m(S = [<<"xn--vuq861b">>]) -> e(S); +m(S = [<<"xn--w4r85el8fhu5dnra">>]) -> e(S); +m(S = [<<"xn--w4rs40l">>]) -> e(S); +m(S = [<<"xn--xhq521b">>]) -> e(S); +m(S = [<<"xn--zfr164b">>]) -> e(S); +m(S = [<<"xyz">>]) -> e(S); +m(S = [<<"yachts">>]) -> e(S); +m(S = [<<"yahoo">>]) -> e(S); +m(S = [<<"yamaxun">>]) -> e(S); +m(S = [<<"yandex">>]) -> e(S); +m(S = [<<"yodobashi">>]) -> e(S); +m(S = [<<"yoga">>]) -> e(S); +m(S = [<<"yokohama">>]) -> e(S); +m(S = [<<"you">>]) -> e(S); +m(S = [<<"youtube">>]) -> e(S); +m(S = [<<"yun">>]) -> e(S); +m(S = [<<"zappos">>]) -> e(S); +m(S = [<<"zara">>]) -> e(S); +m(S = [<<"zero">>]) -> e(S); +m(S = [<<"zip">>]) -> e(S); +m(S = [<<"zone">>]) -> e(S); +m(S = [<<"zuerich">>]) -> e(S); +m(S = [<<"cc">>,<<"ua">>]) -> e(S); +m(S = [<<"inf">>,<<"ua">>]) -> e(S); +m(S = [<<"ltd">>,<<"ua">>]) -> e(S); +m(S = [<<"611">>,<<"to">>]) -> e(S); +m(S = [<<"graphox">>,<<"us">>]) -> e(S); +m(S = [_,<<"devcdnaccesso">>,<<"com">>]) -> e(S); +m(S = [_,<<"on-acorn">>,<<"io">>]) -> e(S); +m(S = [<<"activetrail">>,<<"biz">>]) -> e(S); +m(S = [<<"adobeaemcloud">>,<<"com">>]) -> e(S); +m(S = [_,<<"dev">>,<<"adobeaemcloud">>,<<"com">>]) -> e(S); +m(S = [<<"hlx">>,<<"live">>]) -> e(S); +m(S = [<<"adobeaemcloud">>,<<"net">>]) -> e(S); +m(S = [<<"hlx">>,<<"page">>]) -> e(S); +m(S = [<<"hlx3">>,<<"page">>]) -> e(S); +m(S = [<<"beep">>,<<"pl">>]) -> e(S); +m(S = [<<"airkitapps">>,<<"com">>]) -> e(S); +m(S = [<<"airkitapps-au">>,<<"com">>]) -> e(S); +m(S = [<<"airkitapps">>,<<"eu">>]) -> e(S); +m(S = [<<"aivencloud">>,<<"com">>]) -> e(S); +m(S = [<<"barsy">>,<<"ca">>]) -> e(S); +m(S = [_,<<"compute">>,<<"estate">>]) -> e(S); +m(S = [_,<<"alces">>,<<"network">>]) -> e(S); +m(S = [<<"kasserver">>,<<"com">>]) -> e(S); +m(S = [<<"altervista">>,<<"org">>]) -> e(S); +m(S = [<<"alwaysdata">>,<<"net">>]) -> e(S); +m(S = [<<"myamaze">>,<<"net">>]) -> e(S); +m(S = [<<"cloudfront">>,<<"net">>]) -> e(S); +m(S = [_,<<"compute">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [_,<<"compute-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [_,<<"compute">>,<<"amazonaws">>,<<"com">>, + <<"cn">>]) -> e(S); +m(S = [<<"us-east-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3">>,<<"cn-north-1">>,<<"amazonaws">>,<<"com">>,<<"cn">>]) -> e(S); +m(S = [<<"s3">>,<<"dualstack">>,<<"ap-northeast-1">>,<<"amazonaws">>, + <<"com">>]) -> e(S); +m(S = [<<"s3">>,<<"dualstack">>,<<"ap-northeast-2">>,<<"amazonaws">>, + <<"com">>]) -> e(S); +m(S = [<<"s3">>,<<"ap-northeast-2">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-website">>,<<"ap-northeast-2">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3">>,<<"dualstack">>,<<"ap-south-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3">>,<<"ap-south-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-website">>,<<"ap-south-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3">>,<<"dualstack">>,<<"ap-southeast-1">>,<<"amazonaws">>, + <<"com">>]) -> e(S); +m(S = [<<"s3">>,<<"dualstack">>,<<"ap-southeast-2">>,<<"amazonaws">>, + <<"com">>]) -> e(S); +m(S = [<<"s3">>,<<"dualstack">>,<<"ca-central-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3">>,<<"ca-central-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-website">>,<<"ca-central-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3">>,<<"dualstack">>,<<"eu-central-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3">>,<<"eu-central-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-website">>,<<"eu-central-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3">>,<<"dualstack">>,<<"eu-west-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3">>,<<"dualstack">>,<<"eu-west-2">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3">>,<<"eu-west-2">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-website">>,<<"eu-west-2">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3">>,<<"dualstack">>,<<"eu-west-3">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3">>,<<"eu-west-3">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-website">>,<<"eu-west-3">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-ap-northeast-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-ap-northeast-2">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-ap-south-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-ap-southeast-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-ap-southeast-2">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-ca-central-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-eu-central-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-eu-west-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-eu-west-2">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-eu-west-3">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-external-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-fips-us-gov-west-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-sa-east-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-us-east-2">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-us-gov-west-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-us-west-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-us-west-2">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-website-ap-northeast-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-website-ap-southeast-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-website-ap-southeast-2">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-website-eu-west-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-website-sa-east-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-website-us-east-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-website-us-west-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-website-us-west-2">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3">>,<<"dualstack">>,<<"sa-east-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3">>,<<"dualstack">>,<<"us-east-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3">>,<<"dualstack">>,<<"us-east-2">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3">>,<<"us-east-2">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"s3-website">>,<<"us-east-2">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"vfs">>,<<"cloud9">>,<<"af-south-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"webview-assets">>,<<"cloud9">>,<<"af-south-1">>,<<"amazonaws">>, + <<"com">>]) -> e(S); +m(S = [<<"vfs">>,<<"cloud9">>,<<"ap-east-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"webview-assets">>,<<"cloud9">>,<<"ap-east-1">>,<<"amazonaws">>, + <<"com">>]) -> e(S); +m(S = [<<"vfs">>,<<"cloud9">>,<<"ap-northeast-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"webview-assets">>,<<"cloud9">>,<<"ap-northeast-1">>,<<"amazonaws">>, + <<"com">>]) -> e(S); +m(S = [<<"vfs">>,<<"cloud9">>,<<"ap-northeast-2">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"webview-assets">>,<<"cloud9">>,<<"ap-northeast-2">>,<<"amazonaws">>, + <<"com">>]) -> e(S); +m(S = [<<"vfs">>,<<"cloud9">>,<<"ap-northeast-3">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"webview-assets">>,<<"cloud9">>,<<"ap-northeast-3">>,<<"amazonaws">>, + <<"com">>]) -> e(S); +m(S = [<<"vfs">>,<<"cloud9">>,<<"ap-south-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"webview-assets">>,<<"cloud9">>,<<"ap-south-1">>,<<"amazonaws">>, + <<"com">>]) -> e(S); +m(S = [<<"vfs">>,<<"cloud9">>,<<"ap-southeast-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"webview-assets">>,<<"cloud9">>,<<"ap-southeast-1">>,<<"amazonaws">>, + <<"com">>]) -> e(S); +m(S = [<<"vfs">>,<<"cloud9">>,<<"ap-southeast-2">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"webview-assets">>,<<"cloud9">>,<<"ap-southeast-2">>,<<"amazonaws">>, + <<"com">>]) -> e(S); +m(S = [<<"vfs">>,<<"cloud9">>,<<"ca-central-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"webview-assets">>,<<"cloud9">>,<<"ca-central-1">>,<<"amazonaws">>, + <<"com">>]) -> e(S); +m(S = [<<"vfs">>,<<"cloud9">>,<<"eu-central-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"webview-assets">>,<<"cloud9">>,<<"eu-central-1">>,<<"amazonaws">>, + <<"com">>]) -> e(S); +m(S = [<<"vfs">>,<<"cloud9">>,<<"eu-north-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"webview-assets">>,<<"cloud9">>,<<"eu-north-1">>,<<"amazonaws">>, + <<"com">>]) -> e(S); +m(S = [<<"vfs">>,<<"cloud9">>,<<"eu-south-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"webview-assets">>,<<"cloud9">>,<<"eu-south-1">>,<<"amazonaws">>, + <<"com">>]) -> e(S); +m(S = [<<"vfs">>,<<"cloud9">>,<<"eu-west-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"webview-assets">>,<<"cloud9">>,<<"eu-west-1">>,<<"amazonaws">>, + <<"com">>]) -> e(S); +m(S = [<<"vfs">>,<<"cloud9">>,<<"eu-west-2">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"webview-assets">>,<<"cloud9">>,<<"eu-west-2">>,<<"amazonaws">>, + <<"com">>]) -> e(S); +m(S = [<<"vfs">>,<<"cloud9">>,<<"eu-west-3">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"webview-assets">>,<<"cloud9">>,<<"eu-west-3">>,<<"amazonaws">>, + <<"com">>]) -> e(S); +m(S = [<<"vfs">>,<<"cloud9">>,<<"me-south-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"webview-assets">>,<<"cloud9">>,<<"me-south-1">>,<<"amazonaws">>, + <<"com">>]) -> e(S); +m(S = [<<"vfs">>,<<"cloud9">>,<<"sa-east-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"webview-assets">>,<<"cloud9">>,<<"sa-east-1">>,<<"amazonaws">>, + <<"com">>]) -> e(S); +m(S = [<<"vfs">>,<<"cloud9">>,<<"us-east-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"webview-assets">>,<<"cloud9">>,<<"us-east-1">>,<<"amazonaws">>, + <<"com">>]) -> e(S); +m(S = [<<"vfs">>,<<"cloud9">>,<<"us-east-2">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"webview-assets">>,<<"cloud9">>,<<"us-east-2">>,<<"amazonaws">>, + <<"com">>]) -> e(S); +m(S = [<<"vfs">>,<<"cloud9">>,<<"us-west-1">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"webview-assets">>,<<"cloud9">>,<<"us-west-1">>,<<"amazonaws">>, + <<"com">>]) -> e(S); +m(S = [<<"vfs">>,<<"cloud9">>,<<"us-west-2">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"webview-assets">>,<<"cloud9">>,<<"us-west-2">>,<<"amazonaws">>, + <<"com">>]) -> e(S); +m(S = [<<"cn-north-1">>,<<"eb">>,<<"amazonaws">>,<<"com">>,<<"cn">>]) -> e(S); +m(S = [<<"cn-northwest-1">>,<<"eb">>,<<"amazonaws">>,<<"com">>,<<"cn">>]) -> e(S); +m(S = [<<"elasticbeanstalk">>,<<"com">>]) -> e(S); +m(S = [<<"ap-northeast-1">>,<<"elasticbeanstalk">>,<<"com">>]) -> e(S); +m(S = [<<"ap-northeast-2">>,<<"elasticbeanstalk">>,<<"com">>]) -> e(S); +m(S = [<<"ap-northeast-3">>,<<"elasticbeanstalk">>,<<"com">>]) -> e(S); +m(S = [<<"ap-south-1">>,<<"elasticbeanstalk">>,<<"com">>]) -> e(S); +m(S = [<<"ap-southeast-1">>,<<"elasticbeanstalk">>,<<"com">>]) -> e(S); +m(S = [<<"ap-southeast-2">>,<<"elasticbeanstalk">>,<<"com">>]) -> e(S); +m(S = [<<"ca-central-1">>,<<"elasticbeanstalk">>,<<"com">>]) -> e(S); +m(S = [<<"eu-central-1">>,<<"elasticbeanstalk">>,<<"com">>]) -> e(S); +m(S = [<<"eu-west-1">>,<<"elasticbeanstalk">>,<<"com">>]) -> e(S); +m(S = [<<"eu-west-2">>,<<"elasticbeanstalk">>,<<"com">>]) -> e(S); +m(S = [<<"eu-west-3">>,<<"elasticbeanstalk">>,<<"com">>]) -> e(S); +m(S = [<<"sa-east-1">>,<<"elasticbeanstalk">>,<<"com">>]) -> e(S); +m(S = [<<"us-east-1">>,<<"elasticbeanstalk">>,<<"com">>]) -> e(S); +m(S = [<<"us-east-2">>,<<"elasticbeanstalk">>,<<"com">>]) -> e(S); +m(S = [<<"us-gov-west-1">>,<<"elasticbeanstalk">>,<<"com">>]) -> e(S); +m(S = [<<"us-west-1">>,<<"elasticbeanstalk">>,<<"com">>]) -> e(S); +m(S = [<<"us-west-2">>,<<"elasticbeanstalk">>,<<"com">>]) -> e(S); +m(S = [_,<<"elb">>,<<"amazonaws">>,<<"com">>, + <<"cn">>]) -> e(S); +m(S = [_,<<"elb">>,<<"amazonaws">>,<<"com">>]) -> e(S); +m(S = [<<"awsglobalaccelerator">>,<<"com">>]) -> e(S); +m(S = [<<"eero">>,<<"online">>]) -> e(S); +m(S = [<<"eero-stage">>,<<"online">>]) -> e(S); +m(S = [<<"t3l3p0rt">>,<<"net">>]) -> e(S); +m(S = [<<"tele">>,<<"amune">>,<<"org">>]) -> e(S); +m(S = [<<"apigee">>,<<"io">>]) -> e(S); +m(S = [<<"siiites">>,<<"com">>]) -> e(S); +m(S = [<<"appspacehosted">>,<<"com">>]) -> e(S); +m(S = [<<"appspaceusercontent">>,<<"com">>]) -> e(S); +m(S = [<<"appudo">>,<<"net">>]) -> e(S); +m(S = [<<"on-aptible">>,<<"com">>]) -> e(S); +m(S = [<<"user">>,<<"aseinet">>,<<"ne">>,<<"jp">>]) -> e(S); +m(S = [<<"gv">>,<<"vc">>]) -> e(S); +m(S = [<<"d">>,<<"gv">>,<<"vc">>]) -> e(S); +m(S = [<<"user">>,<<"party">>,<<"eus">>]) -> e(S); +m(S = [<<"pimienta">>,<<"org">>]) -> e(S); +m(S = [<<"poivron">>,<<"org">>]) -> e(S); +m(S = [<<"potager">>,<<"org">>]) -> e(S); +m(S = [<<"sweetpepper">>,<<"org">>]) -> e(S); +m(S = [<<"myasustor">>,<<"com">>]) -> e(S); +m(S = [<<"cdn">>,<<"prod">>,<<"atlassian-dev">>,<<"net">>]) -> e(S); +m(S = [<<"translated">>,<<"page">>]) -> e(S); +m(S = [<<"myfritz">>,<<"net">>]) -> e(S); +m(S = [<<"onavstack">>,<<"net">>]) -> e(S); +m(S = [_,<<"awdev">>,<<"ca">>]) -> e(S); +m(S = [_,<<"advisor">>,<<"ws">>]) -> e(S); +m(S = [<<"ecommerce-shop">>,<<"pl">>]) -> e(S); +m(S = [<<"b-data">>,<<"io">>]) -> e(S); +m(S = [<<"backplaneapp">>,<<"io">>]) -> e(S); +m(S = [<<"balena-devices">>,<<"com">>]) -> e(S); +m(S = [<<"rs">>,<<"ba">>]) -> e(S); +m(S = [_,<<"banzai">>,<<"cloud">>]) -> e(S); +m(S = [<<"app">>,<<"banzaicloud">>,<<"io">>]) -> e(S); +m(S = [_,<<"backyards">>,<<"banzaicloud">>,<<"io">>]) -> e(S); +m(S = [<<"base">>,<<"ec">>]) -> e(S); +m(S = [<<"official">>,<<"ec">>]) -> e(S); +m(S = [<<"buyshop">>,<<"jp">>]) -> e(S); +m(S = [<<"fashionstore">>,<<"jp">>]) -> e(S); +m(S = [<<"handcrafted">>,<<"jp">>]) -> e(S); +m(S = [<<"kawaiishop">>,<<"jp">>]) -> e(S); +m(S = [<<"supersale">>,<<"jp">>]) -> e(S); +m(S = [<<"theshop">>,<<"jp">>]) -> e(S); +m(S = [<<"shopselect">>,<<"net">>]) -> e(S); +m(S = [<<"base">>,<<"shop">>]) -> e(S); +m(S = [<<"beagleboard">>,<<"io">>]) -> e(S); +m(S = [_,<<"beget">>,<<"app">>]) -> e(S); +m(S = [<<"betainabox">>,<<"com">>]) -> e(S); +m(S = [<<"bnr">>,<<"la">>]) -> e(S); +m(S = [<<"bitbucket">>,<<"io">>]) -> e(S); +m(S = [<<"blackbaudcdn">>,<<"net">>]) -> e(S); +m(S = [<<"of">>,<<"je">>]) -> e(S); +m(S = [<<"bluebite">>,<<"io">>]) -> e(S); +m(S = [<<"boomla">>,<<"net">>]) -> e(S); +m(S = [<<"boutir">>,<<"com">>]) -> e(S); +m(S = [<<"boxfuse">>,<<"io">>]) -> e(S); +m(S = [<<"square7">>,<<"ch">>]) -> e(S); +m(S = [<<"bplaced">>,<<"com">>]) -> e(S); +m(S = [<<"bplaced">>,<<"de">>]) -> e(S); +m(S = [<<"square7">>,<<"de">>]) -> e(S); +m(S = [<<"bplaced">>,<<"net">>]) -> e(S); +m(S = [<<"square7">>,<<"net">>]) -> e(S); +m(S = [<<"shop">>,<<"brendly">>,<<"rs">>]) -> e(S); +m(S = [<<"browsersafetymark">>,<<"io">>]) -> e(S); +m(S = [<<"uk0">>,<<"bigv">>,<<"io">>]) -> e(S); +m(S = [<<"dh">>,<<"bytemark">>,<<"co">>,<<"uk">>]) -> e(S); +m(S = [<<"vm">>,<<"bytemark">>,<<"co">>,<<"uk">>]) -> e(S); +m(S = [<<"cafjs">>,<<"com">>]) -> e(S); +m(S = [<<"mycd">>,<<"eu">>]) -> e(S); +m(S = [<<"drr">>,<<"ac">>]) -> e(S); +m(S = [<<"uwu">>,<<"ai">>]) -> e(S); +m(S = [<<"carrd">>,<<"co">>]) -> e(S); +m(S = [<<"crd">>,<<"co">>]) -> e(S); +m(S = [<<"ju">>,<<"mp">>]) -> e(S); +m(S = [<<"ae">>,<<"org">>]) -> e(S); +m(S = [<<"br">>,<<"com">>]) -> e(S); +m(S = [<<"cn">>,<<"com">>]) -> e(S); +m(S = [<<"com">>,<<"de">>]) -> e(S); +m(S = [<<"com">>,<<"se">>]) -> e(S); +m(S = [<<"de">>,<<"com">>]) -> e(S); +m(S = [<<"eu">>,<<"com">>]) -> e(S); +m(S = [<<"gb">>,<<"net">>]) -> e(S); +m(S = [<<"hu">>,<<"net">>]) -> e(S); +m(S = [<<"jp">>,<<"net">>]) -> e(S); +m(S = [<<"jpn">>,<<"com">>]) -> e(S); +m(S = [<<"mex">>,<<"com">>]) -> e(S); +m(S = [<<"ru">>,<<"com">>]) -> e(S); +m(S = [<<"sa">>,<<"com">>]) -> e(S); +m(S = [<<"se">>,<<"net">>]) -> e(S); +m(S = [<<"uk">>,<<"com">>]) -> e(S); +m(S = [<<"uk">>,<<"net">>]) -> e(S); +m(S = [<<"us">>,<<"com">>]) -> e(S); +m(S = [<<"za">>,<<"bz">>]) -> e(S); +m(S = [<<"za">>,<<"com">>]) -> e(S); +m(S = [<<"ar">>,<<"com">>]) -> e(S); +m(S = [<<"hu">>,<<"com">>]) -> e(S); +m(S = [<<"kr">>,<<"com">>]) -> e(S); +m(S = [<<"no">>,<<"com">>]) -> e(S); +m(S = [<<"qc">>,<<"com">>]) -> e(S); +m(S = [<<"uy">>,<<"com">>]) -> e(S); +m(S = [<<"africa">>,<<"com">>]) -> e(S); +m(S = [<<"gr">>,<<"com">>]) -> e(S); +m(S = [<<"in">>,<<"net">>]) -> e(S); +m(S = [<<"web">>,<<"in">>]) -> e(S); +m(S = [<<"us">>,<<"org">>]) -> e(S); +m(S = [<<"co">>,<<"com">>]) -> e(S); +m(S = [<<"aus">>,<<"basketball">>]) -> e(S); +m(S = [<<"nz">>,<<"basketball">>]) -> e(S); +m(S = [<<"radio">>,<<"am">>]) -> e(S); +m(S = [<<"radio">>,<<"fm">>]) -> e(S); +m(S = [<<"c">>,<<"la">>]) -> e(S); +m(S = [<<"certmgr">>,<<"org">>]) -> e(S); +m(S = [<<"cx">>,<<"ua">>]) -> e(S); +m(S = [<<"discourse">>,<<"group">>]) -> e(S); +m(S = [<<"discourse">>,<<"team">>]) -> e(S); +m(S = [<<"cleverapps">>,<<"io">>]) -> e(S); +m(S = [<<"clerk">>,<<"app">>]) -> e(S); +m(S = [<<"clerkstage">>,<<"app">>]) -> e(S); +m(S = [_,<<"lcl">>,<<"dev">>]) -> e(S); +m(S = [_,<<"lclstage">>,<<"dev">>]) -> e(S); +m(S = [_,<<"stg">>,<<"dev">>]) -> e(S); +m(S = [_,<<"stgstage">>,<<"dev">>]) -> e(S); +m(S = [<<"clickrising">>,<<"net">>]) -> e(S); +m(S = [<<"c66">>,<<"me">>]) -> e(S); +m(S = [<<"cloud66">>,<<"ws">>]) -> e(S); +m(S = [<<"cloud66">>,<<"zone">>]) -> e(S); +m(S = [<<"jdevcloud">>,<<"com">>]) -> e(S); +m(S = [<<"wpdevcloud">>,<<"com">>]) -> e(S); +m(S = [<<"cloudaccess">>,<<"host">>]) -> e(S); +m(S = [<<"freesite">>,<<"host">>]) -> e(S); +m(S = [<<"cloudaccess">>,<<"net">>]) -> e(S); +m(S = [<<"cloudcontrolled">>,<<"com">>]) -> e(S); +m(S = [<<"cloudcontrolapp">>,<<"com">>]) -> e(S); +m(S = [_,<<"cloudera">>,<<"site">>]) -> e(S); +m(S = [<<"cf-ipfs">>,<<"com">>]) -> e(S); +m(S = [<<"cloudflare-ipfs">>,<<"com">>]) -> e(S); +m(S = [<<"trycloudflare">>,<<"com">>]) -> e(S); +m(S = [<<"pages">>,<<"dev">>]) -> e(S); +m(S = [<<"r2">>,<<"dev">>]) -> e(S); +m(S = [<<"workers">>,<<"dev">>]) -> e(S); +m(S = [<<"wnext">>,<<"app">>]) -> e(S); +m(S = [<<"co">>,<<"ca">>]) -> e(S); +m(S = [_,<<"otap">>,<<"co">>]) -> e(S); +m(S = [<<"co">>,<<"cz">>]) -> e(S); +m(S = [<<"c">>,<<"cdn77">>,<<"org">>]) -> e(S); +m(S = [<<"cdn77-ssl">>,<<"net">>]) -> e(S); +m(S = [<<"r">>,<<"cdn77">>,<<"net">>]) -> e(S); +m(S = [<<"rsc">>,<<"cdn77">>,<<"org">>]) -> e(S); +m(S = [<<"ssl">>,<<"origin">>,<<"cdn77-secure">>,<<"org">>]) -> e(S); +m(S = [<<"cloudns">>,<<"asia">>]) -> e(S); +m(S = [<<"cloudns">>,<<"biz">>]) -> e(S); +m(S = [<<"cloudns">>,<<"club">>]) -> e(S); +m(S = [<<"cloudns">>,<<"cc">>]) -> e(S); +m(S = [<<"cloudns">>,<<"eu">>]) -> e(S); +m(S = [<<"cloudns">>,<<"in">>]) -> e(S); +m(S = [<<"cloudns">>,<<"info">>]) -> e(S); +m(S = [<<"cloudns">>,<<"org">>]) -> e(S); +m(S = [<<"cloudns">>,<<"pro">>]) -> e(S); +m(S = [<<"cloudns">>,<<"pw">>]) -> e(S); +m(S = [<<"cloudns">>,<<"us">>]) -> e(S); +m(S = [<<"cnpy">>,<<"gdn">>]) -> e(S); +m(S = [<<"codeberg">>,<<"page">>]) -> e(S); +m(S = [<<"co">>,<<"nl">>]) -> e(S); +m(S = [<<"co">>,<<"no">>]) -> e(S); +m(S = [<<"webhosting">>,<<"be">>]) -> e(S); +m(S = [<<"hosting-cluster">>,<<"nl">>]) -> e(S); +m(S = [<<"ac">>,<<"ru">>]) -> e(S); +m(S = [<<"edu">>,<<"ru">>]) -> e(S); +m(S = [<<"gov">>,<<"ru">>]) -> e(S); +m(S = [<<"int">>,<<"ru">>]) -> e(S); +m(S = [<<"mil">>,<<"ru">>]) -> e(S); +m(S = [<<"test">>,<<"ru">>]) -> e(S); +m(S = [<<"dyn">>,<<"cosidns">>,<<"de">>]) -> e(S); +m(S = [<<"dynamisches-dns">>,<<"de">>]) -> e(S); +m(S = [<<"dnsupdater">>,<<"de">>]) -> e(S); +m(S = [<<"internet-dns">>,<<"de">>]) -> e(S); +m(S = [<<"l-o-g-i-n">>,<<"de">>]) -> e(S); +m(S = [<<"dynamic-dns">>,<<"info">>]) -> e(S); +m(S = [<<"feste-ip">>,<<"net">>]) -> e(S); +m(S = [<<"knx-server">>,<<"net">>]) -> e(S); +m(S = [<<"static-access">>,<<"net">>]) -> e(S); +m(S = [<<"realm">>,<<"cz">>]) -> e(S); +m(S = [_,<<"cryptonomic">>,<<"net">>]) -> e(S); +m(S = [<<"cupcake">>,<<"is">>]) -> e(S); +m(S = [<<"curv">>,<<"dev">>]) -> e(S); +m(S = [_,<<"customer-oci">>,<<"com">>]) -> e(S); +m(S = [_,<<"oci">>,<<"customer-oci">>,<<"com">>]) -> e(S); +m(S = [_,<<"ocp">>,<<"customer-oci">>,<<"com">>]) -> e(S); +m(S = [_,<<"ocs">>,<<"customer-oci">>,<<"com">>]) -> e(S); +m(S = [<<"cyon">>,<<"link">>]) -> e(S); +m(S = [<<"cyon">>,<<"site">>]) -> e(S); +m(S = [<<"fnwk">>,<<"site">>]) -> e(S); +m(S = [<<"folionetwork">>,<<"site">>]) -> e(S); +m(S = [<<"platform0">>,<<"app">>]) -> e(S); +m(S = [<<"daplie">>,<<"me">>]) -> e(S); +m(S = [<<"localhost">>,<<"daplie">>,<<"me">>]) -> e(S); +m(S = [<<"dattolocal">>,<<"com">>]) -> e(S); +m(S = [<<"dattorelay">>,<<"com">>]) -> e(S); +m(S = [<<"dattoweb">>,<<"com">>]) -> e(S); +m(S = [<<"mydatto">>,<<"com">>]) -> e(S); +m(S = [<<"dattolocal">>,<<"net">>]) -> e(S); +m(S = [<<"mydatto">>,<<"net">>]) -> e(S); +m(S = [<<"biz">>,<<"dk">>]) -> e(S); +m(S = [<<"co">>,<<"dk">>]) -> e(S); +m(S = [<<"firm">>,<<"dk">>]) -> e(S); +m(S = [<<"reg">>,<<"dk">>]) -> e(S); +m(S = [<<"store">>,<<"dk">>]) -> e(S); +m(S = [<<"dyndns">>,<<"dappnode">>,<<"io">>]) -> e(S); +m(S = [_,<<"dapps">>,<<"earth">>]) -> e(S); +m(S = [_,<<"bzz">>,<<"dapps">>,<<"earth">>]) -> e(S); +m(S = [<<"builtwithdark">>,<<"com">>]) -> e(S); +m(S = [<<"demo">>,<<"datadetect">>,<<"com">>]) -> e(S); +m(S = [<<"instance">>,<<"datadetect">>,<<"com">>]) -> e(S); +m(S = [<<"edgestack">>,<<"me">>]) -> e(S); +m(S = [<<"ddns5">>,<<"com">>]) -> e(S); +m(S = [<<"debian">>,<<"net">>]) -> e(S); +m(S = [<<"deno">>,<<"dev">>]) -> e(S); +m(S = [<<"deno-staging">>,<<"dev">>]) -> e(S); +m(S = [<<"dedyn">>,<<"io">>]) -> e(S); +m(S = [<<"deta">>,<<"app">>]) -> e(S); +m(S = [<<"deta">>,<<"dev">>]) -> e(S); +m(S = [_,<<"rss">>,<<"my">>,<<"id">>]) -> e(S); +m(S = [_,<<"diher">>,<<"solutions">>]) -> e(S); +m(S = [<<"discordsays">>,<<"com">>]) -> e(S); +m(S = [<<"discordsez">>,<<"com">>]) -> e(S); +m(S = [<<"jozi">>,<<"biz">>]) -> e(S); +m(S = [<<"dnshome">>,<<"de">>]) -> e(S); +m(S = [<<"online">>,<<"th">>]) -> e(S); +m(S = [<<"shop">>,<<"th">>]) -> e(S); +m(S = [<<"drayddns">>,<<"com">>]) -> e(S); +m(S = [<<"shoparena">>,<<"pl">>]) -> e(S); +m(S = [<<"dreamhosters">>,<<"com">>]) -> e(S); +m(S = [<<"mydrobo">>,<<"com">>]) -> e(S); +m(S = [<<"drud">>,<<"io">>]) -> e(S); +m(S = [<<"drud">>,<<"us">>]) -> e(S); +m(S = [<<"duckdns">>,<<"org">>]) -> e(S); +m(S = [<<"bip">>,<<"sh">>]) -> e(S); +m(S = [<<"bitbridge">>,<<"net">>]) -> e(S); +m(S = [<<"dy">>,<<"fi">>]) -> e(S); +m(S = [<<"tunk">>,<<"org">>]) -> e(S); +m(S = [<<"dyndns-at-home">>,<<"com">>]) -> e(S); +m(S = [<<"dyndns-at-work">>,<<"com">>]) -> e(S); +m(S = [<<"dyndns-blog">>,<<"com">>]) -> e(S); +m(S = [<<"dyndns-free">>,<<"com">>]) -> e(S); +m(S = [<<"dyndns-home">>,<<"com">>]) -> e(S); +m(S = [<<"dyndns-ip">>,<<"com">>]) -> e(S); +m(S = [<<"dyndns-mail">>,<<"com">>]) -> e(S); +m(S = [<<"dyndns-office">>,<<"com">>]) -> e(S); +m(S = [<<"dyndns-pics">>,<<"com">>]) -> e(S); +m(S = [<<"dyndns-remote">>,<<"com">>]) -> e(S); +m(S = [<<"dyndns-server">>,<<"com">>]) -> e(S); +m(S = [<<"dyndns-web">>,<<"com">>]) -> e(S); +m(S = [<<"dyndns-wiki">>,<<"com">>]) -> e(S); +m(S = [<<"dyndns-work">>,<<"com">>]) -> e(S); +m(S = [<<"dyndns">>,<<"biz">>]) -> e(S); +m(S = [<<"dyndns">>,<<"info">>]) -> e(S); +m(S = [<<"dyndns">>,<<"org">>]) -> e(S); +m(S = [<<"dyndns">>,<<"tv">>]) -> e(S); +m(S = [<<"at-band-camp">>,<<"net">>]) -> e(S); +m(S = [<<"ath">>,<<"cx">>]) -> e(S); +m(S = [<<"barrel-of-knowledge">>,<<"info">>]) -> e(S); +m(S = [<<"barrell-of-knowledge">>,<<"info">>]) -> e(S); +m(S = [<<"better-than">>,<<"tv">>]) -> e(S); +m(S = [<<"blogdns">>,<<"com">>]) -> e(S); +m(S = [<<"blogdns">>,<<"net">>]) -> e(S); +m(S = [<<"blogdns">>,<<"org">>]) -> e(S); +m(S = [<<"blogsite">>,<<"org">>]) -> e(S); +m(S = [<<"boldlygoingnowhere">>,<<"org">>]) -> e(S); +m(S = [<<"broke-it">>,<<"net">>]) -> e(S); +m(S = [<<"buyshouses">>,<<"net">>]) -> e(S); +m(S = [<<"cechire">>,<<"com">>]) -> e(S); +m(S = [<<"dnsalias">>,<<"com">>]) -> e(S); +m(S = [<<"dnsalias">>,<<"net">>]) -> e(S); +m(S = [<<"dnsalias">>,<<"org">>]) -> e(S); +m(S = [<<"dnsdojo">>,<<"com">>]) -> e(S); +m(S = [<<"dnsdojo">>,<<"net">>]) -> e(S); +m(S = [<<"dnsdojo">>,<<"org">>]) -> e(S); +m(S = [<<"does-it">>,<<"net">>]) -> e(S); +m(S = [<<"doesntexist">>,<<"com">>]) -> e(S); +m(S = [<<"doesntexist">>,<<"org">>]) -> e(S); +m(S = [<<"dontexist">>,<<"com">>]) -> e(S); +m(S = [<<"dontexist">>,<<"net">>]) -> e(S); +m(S = [<<"dontexist">>,<<"org">>]) -> e(S); +m(S = [<<"doomdns">>,<<"com">>]) -> e(S); +m(S = [<<"doomdns">>,<<"org">>]) -> e(S); +m(S = [<<"dvrdns">>,<<"org">>]) -> e(S); +m(S = [<<"dyn-o-saur">>,<<"com">>]) -> e(S); +m(S = [<<"dynalias">>,<<"com">>]) -> e(S); +m(S = [<<"dynalias">>,<<"net">>]) -> e(S); +m(S = [<<"dynalias">>,<<"org">>]) -> e(S); +m(S = [<<"dynathome">>,<<"net">>]) -> e(S); +m(S = [<<"dyndns">>,<<"ws">>]) -> e(S); +m(S = [<<"endofinternet">>,<<"net">>]) -> e(S); +m(S = [<<"endofinternet">>,<<"org">>]) -> e(S); +m(S = [<<"endoftheinternet">>,<<"org">>]) -> e(S); +m(S = [<<"est-a-la-maison">>,<<"com">>]) -> e(S); +m(S = [<<"est-a-la-masion">>,<<"com">>]) -> e(S); +m(S = [<<"est-le-patron">>,<<"com">>]) -> e(S); +m(S = [<<"est-mon-blogueur">>,<<"com">>]) -> e(S); +m(S = [<<"for-better">>,<<"biz">>]) -> e(S); +m(S = [<<"for-more">>,<<"biz">>]) -> e(S); +m(S = [<<"for-our">>,<<"info">>]) -> e(S); +m(S = [<<"for-some">>,<<"biz">>]) -> e(S); +m(S = [<<"for-the">>,<<"biz">>]) -> e(S); +m(S = [<<"forgot">>,<<"her">>,<<"name">>]) -> e(S); +m(S = [<<"forgot">>,<<"his">>,<<"name">>]) -> e(S); +m(S = [<<"from-ak">>,<<"com">>]) -> e(S); +m(S = [<<"from-al">>,<<"com">>]) -> e(S); +m(S = [<<"from-ar">>,<<"com">>]) -> e(S); +m(S = [<<"from-az">>,<<"net">>]) -> e(S); +m(S = [<<"from-ca">>,<<"com">>]) -> e(S); +m(S = [<<"from-co">>,<<"net">>]) -> e(S); +m(S = [<<"from-ct">>,<<"com">>]) -> e(S); +m(S = [<<"from-dc">>,<<"com">>]) -> e(S); +m(S = [<<"from-de">>,<<"com">>]) -> e(S); +m(S = [<<"from-fl">>,<<"com">>]) -> e(S); +m(S = [<<"from-ga">>,<<"com">>]) -> e(S); +m(S = [<<"from-hi">>,<<"com">>]) -> e(S); +m(S = [<<"from-ia">>,<<"com">>]) -> e(S); +m(S = [<<"from-id">>,<<"com">>]) -> e(S); +m(S = [<<"from-il">>,<<"com">>]) -> e(S); +m(S = [<<"from-in">>,<<"com">>]) -> e(S); +m(S = [<<"from-ks">>,<<"com">>]) -> e(S); +m(S = [<<"from-ky">>,<<"com">>]) -> e(S); +m(S = [<<"from-la">>,<<"net">>]) -> e(S); +m(S = [<<"from-ma">>,<<"com">>]) -> e(S); +m(S = [<<"from-md">>,<<"com">>]) -> e(S); +m(S = [<<"from-me">>,<<"org">>]) -> e(S); +m(S = [<<"from-mi">>,<<"com">>]) -> e(S); +m(S = [<<"from-mn">>,<<"com">>]) -> e(S); +m(S = [<<"from-mo">>,<<"com">>]) -> e(S); +m(S = [<<"from-ms">>,<<"com">>]) -> e(S); +m(S = [<<"from-mt">>,<<"com">>]) -> e(S); +m(S = [<<"from-nc">>,<<"com">>]) -> e(S); +m(S = [<<"from-nd">>,<<"com">>]) -> e(S); +m(S = [<<"from-ne">>,<<"com">>]) -> e(S); +m(S = [<<"from-nh">>,<<"com">>]) -> e(S); +m(S = [<<"from-nj">>,<<"com">>]) -> e(S); +m(S = [<<"from-nm">>,<<"com">>]) -> e(S); +m(S = [<<"from-nv">>,<<"com">>]) -> e(S); +m(S = [<<"from-ny">>,<<"net">>]) -> e(S); +m(S = [<<"from-oh">>,<<"com">>]) -> e(S); +m(S = [<<"from-ok">>,<<"com">>]) -> e(S); +m(S = [<<"from-or">>,<<"com">>]) -> e(S); +m(S = [<<"from-pa">>,<<"com">>]) -> e(S); +m(S = [<<"from-pr">>,<<"com">>]) -> e(S); +m(S = [<<"from-ri">>,<<"com">>]) -> e(S); +m(S = [<<"from-sc">>,<<"com">>]) -> e(S); +m(S = [<<"from-sd">>,<<"com">>]) -> e(S); +m(S = [<<"from-tn">>,<<"com">>]) -> e(S); +m(S = [<<"from-tx">>,<<"com">>]) -> e(S); +m(S = [<<"from-ut">>,<<"com">>]) -> e(S); +m(S = [<<"from-va">>,<<"com">>]) -> e(S); +m(S = [<<"from-vt">>,<<"com">>]) -> e(S); +m(S = [<<"from-wa">>,<<"com">>]) -> e(S); +m(S = [<<"from-wi">>,<<"com">>]) -> e(S); +m(S = [<<"from-wv">>,<<"com">>]) -> e(S); +m(S = [<<"from-wy">>,<<"com">>]) -> e(S); +m(S = [<<"ftpaccess">>,<<"cc">>]) -> e(S); +m(S = [<<"fuettertdasnetz">>,<<"de">>]) -> e(S); +m(S = [<<"game-host">>,<<"org">>]) -> e(S); +m(S = [<<"game-server">>,<<"cc">>]) -> e(S); +m(S = [<<"getmyip">>,<<"com">>]) -> e(S); +m(S = [<<"gets-it">>,<<"net">>]) -> e(S); +m(S = [<<"go">>,<<"dyndns">>,<<"org">>]) -> e(S); +m(S = [<<"gotdns">>,<<"com">>]) -> e(S); +m(S = [<<"gotdns">>,<<"org">>]) -> e(S); +m(S = [<<"groks-the">>,<<"info">>]) -> e(S); +m(S = [<<"groks-this">>,<<"info">>]) -> e(S); +m(S = [<<"ham-radio-op">>,<<"net">>]) -> e(S); +m(S = [<<"here-for-more">>,<<"info">>]) -> e(S); +m(S = [<<"hobby-site">>,<<"com">>]) -> e(S); +m(S = [<<"hobby-site">>,<<"org">>]) -> e(S); +m(S = [<<"home">>,<<"dyndns">>,<<"org">>]) -> e(S); +m(S = [<<"homedns">>,<<"org">>]) -> e(S); +m(S = [<<"homeftp">>,<<"net">>]) -> e(S); +m(S = [<<"homeftp">>,<<"org">>]) -> e(S); +m(S = [<<"homeip">>,<<"net">>]) -> e(S); +m(S = [<<"homelinux">>,<<"com">>]) -> e(S); +m(S = [<<"homelinux">>,<<"net">>]) -> e(S); +m(S = [<<"homelinux">>,<<"org">>]) -> e(S); +m(S = [<<"homeunix">>,<<"com">>]) -> e(S); +m(S = [<<"homeunix">>,<<"net">>]) -> e(S); +m(S = [<<"homeunix">>,<<"org">>]) -> e(S); +m(S = [<<"iamallama">>,<<"com">>]) -> e(S); +m(S = [<<"in-the-band">>,<<"net">>]) -> e(S); +m(S = [<<"is-a-anarchist">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-blogger">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-bookkeeper">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-bruinsfan">>,<<"org">>]) -> e(S); +m(S = [<<"is-a-bulls-fan">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-candidate">>,<<"org">>]) -> e(S); +m(S = [<<"is-a-caterer">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-celticsfan">>,<<"org">>]) -> e(S); +m(S = [<<"is-a-chef">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-chef">>,<<"net">>]) -> e(S); +m(S = [<<"is-a-chef">>,<<"org">>]) -> e(S); +m(S = [<<"is-a-conservative">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-cpa">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-cubicle-slave">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-democrat">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-designer">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-doctor">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-financialadvisor">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-geek">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-geek">>,<<"net">>]) -> e(S); +m(S = [<<"is-a-geek">>,<<"org">>]) -> e(S); +m(S = [<<"is-a-green">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-guru">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-hard-worker">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-hunter">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-knight">>,<<"org">>]) -> e(S); +m(S = [<<"is-a-landscaper">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-lawyer">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-liberal">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-libertarian">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-linux-user">>,<<"org">>]) -> e(S); +m(S = [<<"is-a-llama">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-musician">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-nascarfan">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-nurse">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-painter">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-patsfan">>,<<"org">>]) -> e(S); +m(S = [<<"is-a-personaltrainer">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-photographer">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-player">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-republican">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-rockstar">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-socialist">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-soxfan">>,<<"org">>]) -> e(S); +m(S = [<<"is-a-student">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-teacher">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-techie">>,<<"com">>]) -> e(S); +m(S = [<<"is-a-therapist">>,<<"com">>]) -> e(S); +m(S = [<<"is-an-accountant">>,<<"com">>]) -> e(S); +m(S = [<<"is-an-actor">>,<<"com">>]) -> e(S); +m(S = [<<"is-an-actress">>,<<"com">>]) -> e(S); +m(S = [<<"is-an-anarchist">>,<<"com">>]) -> e(S); +m(S = [<<"is-an-artist">>,<<"com">>]) -> e(S); +m(S = [<<"is-an-engineer">>,<<"com">>]) -> e(S); +m(S = [<<"is-an-entertainer">>,<<"com">>]) -> e(S); +m(S = [<<"is-by">>,<<"us">>]) -> e(S); +m(S = [<<"is-certified">>,<<"com">>]) -> e(S); +m(S = [<<"is-found">>,<<"org">>]) -> e(S); +m(S = [<<"is-gone">>,<<"com">>]) -> e(S); +m(S = [<<"is-into-anime">>,<<"com">>]) -> e(S); +m(S = [<<"is-into-cars">>,<<"com">>]) -> e(S); +m(S = [<<"is-into-cartoons">>,<<"com">>]) -> e(S); +m(S = [<<"is-into-games">>,<<"com">>]) -> e(S); +m(S = [<<"is-leet">>,<<"com">>]) -> e(S); +m(S = [<<"is-lost">>,<<"org">>]) -> e(S); +m(S = [<<"is-not-certified">>,<<"com">>]) -> e(S); +m(S = [<<"is-saved">>,<<"org">>]) -> e(S); +m(S = [<<"is-slick">>,<<"com">>]) -> e(S); +m(S = [<<"is-uberleet">>,<<"com">>]) -> e(S); +m(S = [<<"is-very-bad">>,<<"org">>]) -> e(S); +m(S = [<<"is-very-evil">>,<<"org">>]) -> e(S); +m(S = [<<"is-very-good">>,<<"org">>]) -> e(S); +m(S = [<<"is-very-nice">>,<<"org">>]) -> e(S); +m(S = [<<"is-very-sweet">>,<<"org">>]) -> e(S); +m(S = [<<"is-with-theband">>,<<"com">>]) -> e(S); +m(S = [<<"isa-geek">>,<<"com">>]) -> e(S); +m(S = [<<"isa-geek">>,<<"net">>]) -> e(S); +m(S = [<<"isa-geek">>,<<"org">>]) -> e(S); +m(S = [<<"isa-hockeynut">>,<<"com">>]) -> e(S); +m(S = [<<"issmarterthanyou">>,<<"com">>]) -> e(S); +m(S = [<<"isteingeek">>,<<"de">>]) -> e(S); +m(S = [<<"istmein">>,<<"de">>]) -> e(S); +m(S = [<<"kicks-ass">>,<<"net">>]) -> e(S); +m(S = [<<"kicks-ass">>,<<"org">>]) -> e(S); +m(S = [<<"knowsitall">>,<<"info">>]) -> e(S); +m(S = [<<"land-4-sale">>,<<"us">>]) -> e(S); +m(S = [<<"lebtimnetz">>,<<"de">>]) -> e(S); +m(S = [<<"leitungsen">>,<<"de">>]) -> e(S); +m(S = [<<"likes-pie">>,<<"com">>]) -> e(S); +m(S = [<<"likescandy">>,<<"com">>]) -> e(S); +m(S = [<<"merseine">>,<<"nu">>]) -> e(S); +m(S = [<<"mine">>,<<"nu">>]) -> e(S); +m(S = [<<"misconfused">>,<<"org">>]) -> e(S); +m(S = [<<"mypets">>,<<"ws">>]) -> e(S); +m(S = [<<"myphotos">>,<<"cc">>]) -> e(S); +m(S = [<<"neat-url">>,<<"com">>]) -> e(S); +m(S = [<<"office-on-the">>,<<"net">>]) -> e(S); +m(S = [<<"on-the-web">>,<<"tv">>]) -> e(S); +m(S = [<<"podzone">>,<<"net">>]) -> e(S); +m(S = [<<"podzone">>,<<"org">>]) -> e(S); +m(S = [<<"readmyblog">>,<<"org">>]) -> e(S); +m(S = [<<"saves-the-whales">>,<<"com">>]) -> e(S); +m(S = [<<"scrapper-site">>,<<"net">>]) -> e(S); +m(S = [<<"scrapping">>,<<"cc">>]) -> e(S); +m(S = [<<"selfip">>,<<"biz">>]) -> e(S); +m(S = [<<"selfip">>,<<"com">>]) -> e(S); +m(S = [<<"selfip">>,<<"info">>]) -> e(S); +m(S = [<<"selfip">>,<<"net">>]) -> e(S); +m(S = [<<"selfip">>,<<"org">>]) -> e(S); +m(S = [<<"sells-for-less">>,<<"com">>]) -> e(S); +m(S = [<<"sells-for-u">>,<<"com">>]) -> e(S); +m(S = [<<"sells-it">>,<<"net">>]) -> e(S); +m(S = [<<"sellsyourhome">>,<<"org">>]) -> e(S); +m(S = [<<"servebbs">>,<<"com">>]) -> e(S); +m(S = [<<"servebbs">>,<<"net">>]) -> e(S); +m(S = [<<"servebbs">>,<<"org">>]) -> e(S); +m(S = [<<"serveftp">>,<<"net">>]) -> e(S); +m(S = [<<"serveftp">>,<<"org">>]) -> e(S); +m(S = [<<"servegame">>,<<"org">>]) -> e(S); +m(S = [<<"shacknet">>,<<"nu">>]) -> e(S); +m(S = [<<"simple-url">>,<<"com">>]) -> e(S); +m(S = [<<"space-to-rent">>,<<"com">>]) -> e(S); +m(S = [<<"stuff-4-sale">>,<<"org">>]) -> e(S); +m(S = [<<"stuff-4-sale">>,<<"us">>]) -> e(S); +m(S = [<<"teaches-yoga">>,<<"com">>]) -> e(S); +m(S = [<<"thruhere">>,<<"net">>]) -> e(S); +m(S = [<<"traeumtgerade">>,<<"de">>]) -> e(S); +m(S = [<<"webhop">>,<<"biz">>]) -> e(S); +m(S = [<<"webhop">>,<<"info">>]) -> e(S); +m(S = [<<"webhop">>,<<"net">>]) -> e(S); +m(S = [<<"webhop">>,<<"org">>]) -> e(S); +m(S = [<<"worse-than">>,<<"tv">>]) -> e(S); +m(S = [<<"writesthisblog">>,<<"com">>]) -> e(S); +m(S = [<<"ddnss">>,<<"de">>]) -> e(S); +m(S = [<<"dyn">>,<<"ddnss">>,<<"de">>]) -> e(S); +m(S = [<<"dyndns">>,<<"ddnss">>,<<"de">>]) -> e(S); +m(S = [<<"dyndns1">>,<<"de">>]) -> e(S); +m(S = [<<"dyn-ip24">>,<<"de">>]) -> e(S); +m(S = [<<"home-webserver">>,<<"de">>]) -> e(S); +m(S = [<<"dyn">>,<<"home-webserver">>,<<"de">>]) -> e(S); +m(S = [<<"myhome-server">>,<<"de">>]) -> e(S); +m(S = [<<"ddnss">>,<<"org">>]) -> e(S); +m(S = [<<"definima">>,<<"net">>]) -> e(S); +m(S = [<<"definima">>,<<"io">>]) -> e(S); +m(S = [<<"ondigitalocean">>,<<"app">>]) -> e(S); +m(S = [_,<<"digitaloceanspaces">>,<<"com">>]) -> e(S); +m(S = [<<"bci">>,<<"dnstrace">>,<<"pro">>]) -> e(S); +m(S = [<<"ddnsfree">>,<<"com">>]) -> e(S); +m(S = [<<"ddnsgeek">>,<<"com">>]) -> e(S); +m(S = [<<"giize">>,<<"com">>]) -> e(S); +m(S = [<<"gleeze">>,<<"com">>]) -> e(S); +m(S = [<<"kozow">>,<<"com">>]) -> e(S); +m(S = [<<"loseyourip">>,<<"com">>]) -> e(S); +m(S = [<<"ooguy">>,<<"com">>]) -> e(S); +m(S = [<<"theworkpc">>,<<"com">>]) -> e(S); +m(S = [<<"casacam">>,<<"net">>]) -> e(S); +m(S = [<<"dynu">>,<<"net">>]) -> e(S); +m(S = [<<"accesscam">>,<<"org">>]) -> e(S); +m(S = [<<"camdvr">>,<<"org">>]) -> e(S); +m(S = [<<"freeddns">>,<<"org">>]) -> e(S); +m(S = [<<"mywire">>,<<"org">>]) -> e(S); +m(S = [<<"webredirect">>,<<"org">>]) -> e(S); +m(S = [<<"myddns">>,<<"rocks">>]) -> e(S); +m(S = [<<"blogsite">>,<<"xyz">>]) -> e(S); +m(S = [<<"dynv6">>,<<"net">>]) -> e(S); +m(S = [<<"e4">>,<<"cz">>]) -> e(S); +m(S = [<<"easypanel">>,<<"app">>]) -> e(S); +m(S = [<<"easypanel">>,<<"host">>]) -> e(S); +m(S = [<<"elementor">>,<<"cloud">>]) -> e(S); +m(S = [<<"elementor">>,<<"cool">>]) -> e(S); +m(S = [<<"en-root">>,<<"fr">>]) -> e(S); +m(S = [<<"mytuleap">>,<<"com">>]) -> e(S); +m(S = [<<"tuleap-partners">>,<<"com">>]) -> e(S); +m(S = [<<"encr">>,<<"app">>]) -> e(S); +m(S = [<<"encoreapi">>,<<"com">>]) -> e(S); +m(S = [<<"onred">>,<<"one">>]) -> e(S); +m(S = [<<"staging">>,<<"onred">>,<<"one">>]) -> e(S); +m(S = [<<"eu">>,<<"encoway">>,<<"cloud">>]) -> e(S); +m(S = [<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"al">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"asso">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"at">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"au">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"be">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"bg">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"ca">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"cd">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"ch">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"cn">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"cy">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"cz">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"de">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"dk">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"edu">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"ee">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"es">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"fi">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"fr">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"gr">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"hr">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"hu">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"ie">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"il">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"in">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"int">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"is">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"it">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"jp">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"kr">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"lt">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"lu">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"lv">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"mc">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"me">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"mk">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"mt">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"my">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"net">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"ng">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"nl">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"no">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"nz">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"paris">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"pl">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"pt">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"q-a">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"ro">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"ru">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"se">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"si">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"sk">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"tr">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"uk">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"us">>,<<"eu">>,<<"org">>]) -> e(S); +m(S = [<<"eurodir">>,<<"ru">>]) -> e(S); +m(S = [<<"eu-1">>,<<"evennode">>,<<"com">>]) -> e(S); +m(S = [<<"eu-2">>,<<"evennode">>,<<"com">>]) -> e(S); +m(S = [<<"eu-3">>,<<"evennode">>,<<"com">>]) -> e(S); +m(S = [<<"eu-4">>,<<"evennode">>,<<"com">>]) -> e(S); +m(S = [<<"us-1">>,<<"evennode">>,<<"com">>]) -> e(S); +m(S = [<<"us-2">>,<<"evennode">>,<<"com">>]) -> e(S); +m(S = [<<"us-3">>,<<"evennode">>,<<"com">>]) -> e(S); +m(S = [<<"us-4">>,<<"evennode">>,<<"com">>]) -> e(S); +m(S = [<<"twmail">>,<<"cc">>]) -> e(S); +m(S = [<<"twmail">>,<<"net">>]) -> e(S); +m(S = [<<"twmail">>,<<"org">>]) -> e(S); +m(S = [<<"mymailer">>,<<"com">>,<<"tw">>]) -> e(S); +m(S = [<<"url">>,<<"tw">>]) -> e(S); +m(S = [<<"onfabrica">>,<<"com">>]) -> e(S); +m(S = [<<"apps">>,<<"fbsbx">>,<<"com">>]) -> e(S); +m(S = [<<"ru">>,<<"net">>]) -> e(S); +m(S = [<<"adygeya">>,<<"ru">>]) -> e(S); +m(S = [<<"bashkiria">>,<<"ru">>]) -> e(S); +m(S = [<<"bir">>,<<"ru">>]) -> e(S); +m(S = [<<"cbg">>,<<"ru">>]) -> e(S); +m(S = [<<"com">>,<<"ru">>]) -> e(S); +m(S = [<<"dagestan">>,<<"ru">>]) -> e(S); +m(S = [<<"grozny">>,<<"ru">>]) -> e(S); +m(S = [<<"kalmykia">>,<<"ru">>]) -> e(S); +m(S = [<<"kustanai">>,<<"ru">>]) -> e(S); +m(S = [<<"marine">>,<<"ru">>]) -> e(S); +m(S = [<<"mordovia">>,<<"ru">>]) -> e(S); +m(S = [<<"msk">>,<<"ru">>]) -> e(S); +m(S = [<<"mytis">>,<<"ru">>]) -> e(S); +m(S = [<<"nalchik">>,<<"ru">>]) -> e(S); +m(S = [<<"nov">>,<<"ru">>]) -> e(S); +m(S = [<<"pyatigorsk">>,<<"ru">>]) -> e(S); +m(S = [<<"spb">>,<<"ru">>]) -> e(S); +m(S = [<<"vladikavkaz">>,<<"ru">>]) -> e(S); +m(S = [<<"vladimir">>,<<"ru">>]) -> e(S); +m(S = [<<"abkhazia">>,<<"su">>]) -> e(S); +m(S = [<<"adygeya">>,<<"su">>]) -> e(S); +m(S = [<<"aktyubinsk">>,<<"su">>]) -> e(S); +m(S = [<<"arkhangelsk">>,<<"su">>]) -> e(S); +m(S = [<<"armenia">>,<<"su">>]) -> e(S); +m(S = [<<"ashgabad">>,<<"su">>]) -> e(S); +m(S = [<<"azerbaijan">>,<<"su">>]) -> e(S); +m(S = [<<"balashov">>,<<"su">>]) -> e(S); +m(S = [<<"bashkiria">>,<<"su">>]) -> e(S); +m(S = [<<"bryansk">>,<<"su">>]) -> e(S); +m(S = [<<"bukhara">>,<<"su">>]) -> e(S); +m(S = [<<"chimkent">>,<<"su">>]) -> e(S); +m(S = [<<"dagestan">>,<<"su">>]) -> e(S); +m(S = [<<"east-kazakhstan">>,<<"su">>]) -> e(S); +m(S = [<<"exnet">>,<<"su">>]) -> e(S); +m(S = [<<"georgia">>,<<"su">>]) -> e(S); +m(S = [<<"grozny">>,<<"su">>]) -> e(S); +m(S = [<<"ivanovo">>,<<"su">>]) -> e(S); +m(S = [<<"jambyl">>,<<"su">>]) -> e(S); +m(S = [<<"kalmykia">>,<<"su">>]) -> e(S); +m(S = [<<"kaluga">>,<<"su">>]) -> e(S); +m(S = [<<"karacol">>,<<"su">>]) -> e(S); +m(S = [<<"karaganda">>,<<"su">>]) -> e(S); +m(S = [<<"karelia">>,<<"su">>]) -> e(S); +m(S = [<<"khakassia">>,<<"su">>]) -> e(S); +m(S = [<<"krasnodar">>,<<"su">>]) -> e(S); +m(S = [<<"kurgan">>,<<"su">>]) -> e(S); +m(S = [<<"kustanai">>,<<"su">>]) -> e(S); +m(S = [<<"lenug">>,<<"su">>]) -> e(S); +m(S = [<<"mangyshlak">>,<<"su">>]) -> e(S); +m(S = [<<"mordovia">>,<<"su">>]) -> e(S); +m(S = [<<"msk">>,<<"su">>]) -> e(S); +m(S = [<<"murmansk">>,<<"su">>]) -> e(S); +m(S = [<<"nalchik">>,<<"su">>]) -> e(S); +m(S = [<<"navoi">>,<<"su">>]) -> e(S); +m(S = [<<"north-kazakhstan">>,<<"su">>]) -> e(S); +m(S = [<<"nov">>,<<"su">>]) -> e(S); +m(S = [<<"obninsk">>,<<"su">>]) -> e(S); +m(S = [<<"penza">>,<<"su">>]) -> e(S); +m(S = [<<"pokrovsk">>,<<"su">>]) -> e(S); +m(S = [<<"sochi">>,<<"su">>]) -> e(S); +m(S = [<<"spb">>,<<"su">>]) -> e(S); +m(S = [<<"tashkent">>,<<"su">>]) -> e(S); +m(S = [<<"termez">>,<<"su">>]) -> e(S); +m(S = [<<"togliatti">>,<<"su">>]) -> e(S); +m(S = [<<"troitsk">>,<<"su">>]) -> e(S); +m(S = [<<"tselinograd">>,<<"su">>]) -> e(S); +m(S = [<<"tula">>,<<"su">>]) -> e(S); +m(S = [<<"tuva">>,<<"su">>]) -> e(S); +m(S = [<<"vladikavkaz">>,<<"su">>]) -> e(S); +m(S = [<<"vladimir">>,<<"su">>]) -> e(S); +m(S = [<<"vologda">>,<<"su">>]) -> e(S); +m(S = [<<"channelsdvr">>,<<"net">>]) -> e(S); +m(S = [<<"u">>,<<"channelsdvr">>,<<"net">>]) -> e(S); +m(S = [<<"edgecompute">>,<<"app">>]) -> e(S); +m(S = [<<"fastly-terrarium">>,<<"com">>]) -> e(S); +m(S = [<<"fastlylb">>,<<"net">>]) -> e(S); +m(S = [<<"map">>,<<"fastlylb">>,<<"net">>]) -> e(S); +m(S = [<<"freetls">>,<<"fastly">>,<<"net">>]) -> e(S); +m(S = [<<"map">>,<<"fastly">>,<<"net">>]) -> e(S); +m(S = [<<"a">>,<<"prod">>,<<"fastly">>,<<"net">>]) -> e(S); +m(S = [<<"global">>,<<"prod">>,<<"fastly">>,<<"net">>]) -> e(S); +m(S = [<<"a">>,<<"ssl">>,<<"fastly">>,<<"net">>]) -> e(S); +m(S = [<<"b">>,<<"ssl">>,<<"fastly">>,<<"net">>]) -> e(S); +m(S = [<<"global">>,<<"ssl">>,<<"fastly">>,<<"net">>]) -> e(S); +m(S = [_,<<"user">>,<<"fm">>]) -> e(S); +m(S = [<<"fastvps-server">>,<<"com">>]) -> e(S); +m(S = [<<"fastvps">>,<<"host">>]) -> e(S); +m(S = [<<"myfast">>,<<"host">>]) -> e(S); +m(S = [<<"fastvps">>,<<"site">>]) -> e(S); +m(S = [<<"myfast">>,<<"space">>]) -> e(S); +m(S = [<<"fedorainfracloud">>,<<"org">>]) -> e(S); +m(S = [<<"fedorapeople">>,<<"org">>]) -> e(S); +m(S = [<<"cloud">>,<<"fedoraproject">>,<<"org">>]) -> e(S); +m(S = [<<"app">>,<<"os">>,<<"fedoraproject">>,<<"org">>]) -> e(S); +m(S = [<<"app">>,<<"os">>,<<"stg">>,<<"fedoraproject">>,<<"org">>]) -> e(S); +m(S = [<<"conn">>,<<"uk">>]) -> e(S); +m(S = [<<"copro">>,<<"uk">>]) -> e(S); +m(S = [<<"hosp">>,<<"uk">>]) -> e(S); +m(S = [<<"mydobiss">>,<<"com">>]) -> e(S); +m(S = [<<"fh-muenster">>,<<"io">>]) -> e(S); +m(S = [<<"filegear">>,<<"me">>]) -> e(S); +m(S = [<<"filegear-au">>,<<"me">>]) -> e(S); +m(S = [<<"filegear-de">>,<<"me">>]) -> e(S); +m(S = [<<"filegear-gb">>,<<"me">>]) -> e(S); +m(S = [<<"filegear-ie">>,<<"me">>]) -> e(S); +m(S = [<<"filegear-jp">>,<<"me">>]) -> e(S); +m(S = [<<"filegear-sg">>,<<"me">>]) -> e(S); +m(S = [<<"firebaseapp">>,<<"com">>]) -> e(S); +m(S = [<<"fireweb">>,<<"app">>]) -> e(S); +m(S = [<<"flap">>,<<"id">>]) -> e(S); +m(S = [<<"onflashdrive">>,<<"app">>]) -> e(S); +m(S = [<<"fldrv">>,<<"com">>]) -> e(S); +m(S = [<<"fly">>,<<"dev">>]) -> e(S); +m(S = [<<"edgeapp">>,<<"net">>]) -> e(S); +m(S = [<<"shw">>,<<"io">>]) -> e(S); +m(S = [<<"flynnhosting">>,<<"net">>]) -> e(S); +m(S = [<<"forgeblocks">>,<<"com">>]) -> e(S); +m(S = [<<"id">>,<<"forgerock">>,<<"io">>]) -> e(S); +m(S = [<<"framer">>,<<"app">>]) -> e(S); +m(S = [<<"framercanvas">>,<<"com">>]) -> e(S); +m(S = [<<"framer">>,<<"media">>]) -> e(S); +m(S = [<<"framer">>,<<"photos">>]) -> e(S); +m(S = [<<"framer">>,<<"website">>]) -> e(S); +m(S = [<<"framer">>,<<"wiki">>]) -> e(S); +m(S = [_,<<"frusky">>,<<"de">>]) -> e(S); +m(S = [<<"ravpage">>,<<"co">>,<<"il">>]) -> e(S); +m(S = [<<"0e">>,<<"vc">>]) -> e(S); +m(S = [<<"freebox-os">>,<<"com">>]) -> e(S); +m(S = [<<"freeboxos">>,<<"com">>]) -> e(S); +m(S = [<<"fbx-os">>,<<"fr">>]) -> e(S); +m(S = [<<"fbxos">>,<<"fr">>]) -> e(S); +m(S = [<<"freebox-os">>,<<"fr">>]) -> e(S); +m(S = [<<"freeboxos">>,<<"fr">>]) -> e(S); +m(S = [<<"freedesktop">>,<<"org">>]) -> e(S); +m(S = [<<"freemyip">>,<<"com">>]) -> e(S); +m(S = [<<"wien">>,<<"funkfeuer">>,<<"at">>]) -> e(S); +m(S = [_,<<"futurecms">>,<<"at">>]) -> e(S); +m(S = [_,<<"ex">>,<<"futurecms">>,<<"at">>]) -> e(S); +m(S = [_,<<"in">>,<<"futurecms">>,<<"at">>]) -> e(S); +m(S = [<<"futurehosting">>,<<"at">>]) -> e(S); +m(S = [<<"futuremailing">>,<<"at">>]) -> e(S); +m(S = [_,<<"ex">>,<<"ortsinfo">>,<<"at">>]) -> e(S); +m(S = [_,<<"kunden">>,<<"ortsinfo">>,<<"at">>]) -> e(S); +m(S = [_,<<"statics">>,<<"cloud">>]) -> e(S); +m(S = [<<"independent-commission">>,<<"uk">>]) -> e(S); +m(S = [<<"independent-inquest">>,<<"uk">>]) -> e(S); +m(S = [<<"independent-inquiry">>,<<"uk">>]) -> e(S); +m(S = [<<"independent-panel">>,<<"uk">>]) -> e(S); +m(S = [<<"independent-review">>,<<"uk">>]) -> e(S); +m(S = [<<"public-inquiry">>,<<"uk">>]) -> e(S); +m(S = [<<"royal-commission">>,<<"uk">>]) -> e(S); +m(S = [<<"campaign">>,<<"gov">>,<<"uk">>]) -> e(S); +m(S = [<<"service">>,<<"gov">>,<<"uk">>]) -> e(S); +m(S = [<<"api">>,<<"gov">>,<<"uk">>]) -> e(S); +m(S = [<<"gehirn">>,<<"ne">>,<<"jp">>]) -> e(S); +m(S = [<<"usercontent">>,<<"jp">>]) -> e(S); +m(S = [<<"gentapps">>,<<"com">>]) -> e(S); +m(S = [<<"gentlentapis">>,<<"com">>]) -> e(S); +m(S = [<<"lab">>,<<"ms">>]) -> e(S); +m(S = [<<"cdn-edges">>,<<"net">>]) -> e(S); +m(S = [<<"ghost">>,<<"io">>]) -> e(S); +m(S = [<<"gsj">>,<<"bz">>]) -> e(S); +m(S = [<<"githubusercontent">>,<<"com">>]) -> e(S); +m(S = [<<"githubpreview">>,<<"dev">>]) -> e(S); +m(S = [<<"github">>,<<"io">>]) -> e(S); +m(S = [<<"gitlab">>,<<"io">>]) -> e(S); +m(S = [<<"gitapp">>,<<"si">>]) -> e(S); +m(S = [<<"gitpage">>,<<"si">>]) -> e(S); +m(S = [<<"glitch">>,<<"me">>]) -> e(S); +m(S = [<<"nog">>,<<"community">>]) -> e(S); +m(S = [<<"co">>,<<"ro">>]) -> e(S); +m(S = [<<"shop">>,<<"ro">>]) -> e(S); +m(S = [<<"lolipop">>,<<"io">>]) -> e(S); +m(S = [<<"angry">>,<<"jp">>]) -> e(S); +m(S = [<<"babyblue">>,<<"jp">>]) -> e(S); +m(S = [<<"babymilk">>,<<"jp">>]) -> e(S); +m(S = [<<"backdrop">>,<<"jp">>]) -> e(S); +m(S = [<<"bambina">>,<<"jp">>]) -> e(S); +m(S = [<<"bitter">>,<<"jp">>]) -> e(S); +m(S = [<<"blush">>,<<"jp">>]) -> e(S); +m(S = [<<"boo">>,<<"jp">>]) -> e(S); +m(S = [<<"boy">>,<<"jp">>]) -> e(S); +m(S = [<<"boyfriend">>,<<"jp">>]) -> e(S); +m(S = [<<"but">>,<<"jp">>]) -> e(S); +m(S = [<<"candypop">>,<<"jp">>]) -> e(S); +m(S = [<<"capoo">>,<<"jp">>]) -> e(S); +m(S = [<<"catfood">>,<<"jp">>]) -> e(S); +m(S = [<<"cheap">>,<<"jp">>]) -> e(S); +m(S = [<<"chicappa">>,<<"jp">>]) -> e(S); +m(S = [<<"chillout">>,<<"jp">>]) -> e(S); +m(S = [<<"chips">>,<<"jp">>]) -> e(S); +m(S = [<<"chowder">>,<<"jp">>]) -> e(S); +m(S = [<<"chu">>,<<"jp">>]) -> e(S); +m(S = [<<"ciao">>,<<"jp">>]) -> e(S); +m(S = [<<"cocotte">>,<<"jp">>]) -> e(S); +m(S = [<<"coolblog">>,<<"jp">>]) -> e(S); +m(S = [<<"cranky">>,<<"jp">>]) -> e(S); +m(S = [<<"cutegirl">>,<<"jp">>]) -> e(S); +m(S = [<<"daa">>,<<"jp">>]) -> e(S); +m(S = [<<"deca">>,<<"jp">>]) -> e(S); +m(S = [<<"deci">>,<<"jp">>]) -> e(S); +m(S = [<<"digick">>,<<"jp">>]) -> e(S); +m(S = [<<"egoism">>,<<"jp">>]) -> e(S); +m(S = [<<"fakefur">>,<<"jp">>]) -> e(S); +m(S = [<<"fem">>,<<"jp">>]) -> e(S); +m(S = [<<"flier">>,<<"jp">>]) -> e(S); +m(S = [<<"floppy">>,<<"jp">>]) -> e(S); +m(S = [<<"fool">>,<<"jp">>]) -> e(S); +m(S = [<<"frenchkiss">>,<<"jp">>]) -> e(S); +m(S = [<<"girlfriend">>,<<"jp">>]) -> e(S); +m(S = [<<"girly">>,<<"jp">>]) -> e(S); +m(S = [<<"gloomy">>,<<"jp">>]) -> e(S); +m(S = [<<"gonna">>,<<"jp">>]) -> e(S); +m(S = [<<"greater">>,<<"jp">>]) -> e(S); +m(S = [<<"hacca">>,<<"jp">>]) -> e(S); +m(S = [<<"heavy">>,<<"jp">>]) -> e(S); +m(S = [<<"her">>,<<"jp">>]) -> e(S); +m(S = [<<"hiho">>,<<"jp">>]) -> e(S); +m(S = [<<"hippy">>,<<"jp">>]) -> e(S); +m(S = [<<"holy">>,<<"jp">>]) -> e(S); +m(S = [<<"hungry">>,<<"jp">>]) -> e(S); +m(S = [<<"icurus">>,<<"jp">>]) -> e(S); +m(S = [<<"itigo">>,<<"jp">>]) -> e(S); +m(S = [<<"jellybean">>,<<"jp">>]) -> e(S); +m(S = [<<"kikirara">>,<<"jp">>]) -> e(S); +m(S = [<<"kill">>,<<"jp">>]) -> e(S); +m(S = [<<"kilo">>,<<"jp">>]) -> e(S); +m(S = [<<"kuron">>,<<"jp">>]) -> e(S); +m(S = [<<"littlestar">>,<<"jp">>]) -> e(S); +m(S = [<<"lolipopmc">>,<<"jp">>]) -> e(S); +m(S = [<<"lolitapunk">>,<<"jp">>]) -> e(S); +m(S = [<<"lomo">>,<<"jp">>]) -> e(S); +m(S = [<<"lovepop">>,<<"jp">>]) -> e(S); +m(S = [<<"lovesick">>,<<"jp">>]) -> e(S); +m(S = [<<"main">>,<<"jp">>]) -> e(S); +m(S = [<<"mods">>,<<"jp">>]) -> e(S); +m(S = [<<"mond">>,<<"jp">>]) -> e(S); +m(S = [<<"mongolian">>,<<"jp">>]) -> e(S); +m(S = [<<"moo">>,<<"jp">>]) -> e(S); +m(S = [<<"namaste">>,<<"jp">>]) -> e(S); +m(S = [<<"nikita">>,<<"jp">>]) -> e(S); +m(S = [<<"nobushi">>,<<"jp">>]) -> e(S); +m(S = [<<"noor">>,<<"jp">>]) -> e(S); +m(S = [<<"oops">>,<<"jp">>]) -> e(S); +m(S = [<<"parallel">>,<<"jp">>]) -> e(S); +m(S = [<<"parasite">>,<<"jp">>]) -> e(S); +m(S = [<<"pecori">>,<<"jp">>]) -> e(S); +m(S = [<<"peewee">>,<<"jp">>]) -> e(S); +m(S = [<<"penne">>,<<"jp">>]) -> e(S); +m(S = [<<"pepper">>,<<"jp">>]) -> e(S); +m(S = [<<"perma">>,<<"jp">>]) -> e(S); +m(S = [<<"pigboat">>,<<"jp">>]) -> e(S); +m(S = [<<"pinoko">>,<<"jp">>]) -> e(S); +m(S = [<<"punyu">>,<<"jp">>]) -> e(S); +m(S = [<<"pupu">>,<<"jp">>]) -> e(S); +m(S = [<<"pussycat">>,<<"jp">>]) -> e(S); +m(S = [<<"pya">>,<<"jp">>]) -> e(S); +m(S = [<<"raindrop">>,<<"jp">>]) -> e(S); +m(S = [<<"readymade">>,<<"jp">>]) -> e(S); +m(S = [<<"sadist">>,<<"jp">>]) -> e(S); +m(S = [<<"schoolbus">>,<<"jp">>]) -> e(S); +m(S = [<<"secret">>,<<"jp">>]) -> e(S); +m(S = [<<"staba">>,<<"jp">>]) -> e(S); +m(S = [<<"stripper">>,<<"jp">>]) -> e(S); +m(S = [<<"sub">>,<<"jp">>]) -> e(S); +m(S = [<<"sunnyday">>,<<"jp">>]) -> e(S); +m(S = [<<"thick">>,<<"jp">>]) -> e(S); +m(S = [<<"tonkotsu">>,<<"jp">>]) -> e(S); +m(S = [<<"under">>,<<"jp">>]) -> e(S); +m(S = [<<"upper">>,<<"jp">>]) -> e(S); +m(S = [<<"velvet">>,<<"jp">>]) -> e(S); +m(S = [<<"verse">>,<<"jp">>]) -> e(S); +m(S = [<<"versus">>,<<"jp">>]) -> e(S); +m(S = [<<"vivian">>,<<"jp">>]) -> e(S); +m(S = [<<"watson">>,<<"jp">>]) -> e(S); +m(S = [<<"weblike">>,<<"jp">>]) -> e(S); +m(S = [<<"whitesnow">>,<<"jp">>]) -> e(S); +m(S = [<<"zombie">>,<<"jp">>]) -> e(S); +m(S = [<<"heteml">>,<<"net">>]) -> e(S); +m(S = [<<"cloudapps">>,<<"digital">>]) -> e(S); +m(S = [<<"london">>,<<"cloudapps">>,<<"digital">>]) -> e(S); +m(S = [<<"pymnt">>,<<"uk">>]) -> e(S); +m(S = [<<"homeoffice">>,<<"gov">>,<<"uk">>]) -> e(S); +m(S = [<<"ro">>,<<"im">>]) -> e(S); +m(S = [<<"goip">>,<<"de">>]) -> e(S); +m(S = [<<"run">>,<<"app">>]) -> e(S); +m(S = [<<"a">>,<<"run">>,<<"app">>]) -> e(S); +m(S = [<<"web">>,<<"app">>]) -> e(S); +m(S = [_,<<"0emm">>,<<"com">>]) -> e(S); +m(S = [<<"appspot">>,<<"com">>]) -> e(S); +m(S = [_,<<"r">>,<<"appspot">>,<<"com">>]) -> e(S); +m(S = [<<"codespot">>,<<"com">>]) -> e(S); +m(S = [<<"googleapis">>,<<"com">>]) -> e(S); +m(S = [<<"googlecode">>,<<"com">>]) -> e(S); +m(S = [<<"pagespeedmobilizer">>,<<"com">>]) -> e(S); +m(S = [<<"publishproxy">>,<<"com">>]) -> e(S); +m(S = [<<"withgoogle">>,<<"com">>]) -> e(S); +m(S = [<<"withyoutube">>,<<"com">>]) -> e(S); +m(S = [_,<<"gateway">>,<<"dev">>]) -> e(S); +m(S = [<<"cloud">>,<<"goog">>]) -> e(S); +m(S = [<<"translate">>,<<"goog">>]) -> e(S); +m(S = [_,<<"usercontent">>,<<"goog">>]) -> e(S); +m(S = [<<"cloudfunctions">>,<<"net">>]) -> e(S); +m(S = [<<"blogspot">>,<<"ae">>]) -> e(S); +m(S = [<<"blogspot">>,<<"al">>]) -> e(S); +m(S = [<<"blogspot">>,<<"am">>]) -> e(S); +m(S = [<<"blogspot">>,<<"ba">>]) -> e(S); +m(S = [<<"blogspot">>,<<"be">>]) -> e(S); +m(S = [<<"blogspot">>,<<"bg">>]) -> e(S); +m(S = [<<"blogspot">>,<<"bj">>]) -> e(S); +m(S = [<<"blogspot">>,<<"ca">>]) -> e(S); +m(S = [<<"blogspot">>,<<"cf">>]) -> e(S); +m(S = [<<"blogspot">>,<<"ch">>]) -> e(S); +m(S = [<<"blogspot">>,<<"cl">>]) -> e(S); +m(S = [<<"blogspot">>,<<"co">>,<<"at">>]) -> e(S); +m(S = [<<"blogspot">>,<<"co">>,<<"id">>]) -> e(S); +m(S = [<<"blogspot">>,<<"co">>,<<"il">>]) -> e(S); +m(S = [<<"blogspot">>,<<"co">>,<<"ke">>]) -> e(S); +m(S = [<<"blogspot">>,<<"co">>,<<"nz">>]) -> e(S); +m(S = [<<"blogspot">>,<<"co">>,<<"uk">>]) -> e(S); +m(S = [<<"blogspot">>,<<"co">>,<<"za">>]) -> e(S); +m(S = [<<"blogspot">>,<<"com">>]) -> e(S); +m(S = [<<"blogspot">>,<<"com">>,<<"ar">>]) -> e(S); +m(S = [<<"blogspot">>,<<"com">>,<<"au">>]) -> e(S); +m(S = [<<"blogspot">>,<<"com">>,<<"br">>]) -> e(S); +m(S = [<<"blogspot">>,<<"com">>,<<"by">>]) -> e(S); +m(S = [<<"blogspot">>,<<"com">>,<<"co">>]) -> e(S); +m(S = [<<"blogspot">>,<<"com">>,<<"cy">>]) -> e(S); +m(S = [<<"blogspot">>,<<"com">>,<<"ee">>]) -> e(S); +m(S = [<<"blogspot">>,<<"com">>,<<"eg">>]) -> e(S); +m(S = [<<"blogspot">>,<<"com">>,<<"es">>]) -> e(S); +m(S = [<<"blogspot">>,<<"com">>,<<"mt">>]) -> e(S); +m(S = [<<"blogspot">>,<<"com">>,<<"ng">>]) -> e(S); +m(S = [<<"blogspot">>,<<"com">>,<<"tr">>]) -> e(S); +m(S = [<<"blogspot">>,<<"com">>,<<"uy">>]) -> e(S); +m(S = [<<"blogspot">>,<<"cv">>]) -> e(S); +m(S = [<<"blogspot">>,<<"cz">>]) -> e(S); +m(S = [<<"blogspot">>,<<"de">>]) -> e(S); +m(S = [<<"blogspot">>,<<"dk">>]) -> e(S); +m(S = [<<"blogspot">>,<<"fi">>]) -> e(S); +m(S = [<<"blogspot">>,<<"fr">>]) -> e(S); +m(S = [<<"blogspot">>,<<"gr">>]) -> e(S); +m(S = [<<"blogspot">>,<<"hk">>]) -> e(S); +m(S = [<<"blogspot">>,<<"hr">>]) -> e(S); +m(S = [<<"blogspot">>,<<"hu">>]) -> e(S); +m(S = [<<"blogspot">>,<<"ie">>]) -> e(S); +m(S = [<<"blogspot">>,<<"in">>]) -> e(S); +m(S = [<<"blogspot">>,<<"is">>]) -> e(S); +m(S = [<<"blogspot">>,<<"it">>]) -> e(S); +m(S = [<<"blogspot">>,<<"jp">>]) -> e(S); +m(S = [<<"blogspot">>,<<"kr">>]) -> e(S); +m(S = [<<"blogspot">>,<<"li">>]) -> e(S); +m(S = [<<"blogspot">>,<<"lt">>]) -> e(S); +m(S = [<<"blogspot">>,<<"lu">>]) -> e(S); +m(S = [<<"blogspot">>,<<"md">>]) -> e(S); +m(S = [<<"blogspot">>,<<"mk">>]) -> e(S); +m(S = [<<"blogspot">>,<<"mr">>]) -> e(S); +m(S = [<<"blogspot">>,<<"mx">>]) -> e(S); +m(S = [<<"blogspot">>,<<"my">>]) -> e(S); +m(S = [<<"blogspot">>,<<"nl">>]) -> e(S); +m(S = [<<"blogspot">>,<<"no">>]) -> e(S); +m(S = [<<"blogspot">>,<<"pe">>]) -> e(S); +m(S = [<<"blogspot">>,<<"pt">>]) -> e(S); +m(S = [<<"blogspot">>,<<"qa">>]) -> e(S); +m(S = [<<"blogspot">>,<<"re">>]) -> e(S); +m(S = [<<"blogspot">>,<<"ro">>]) -> e(S); +m(S = [<<"blogspot">>,<<"rs">>]) -> e(S); +m(S = [<<"blogspot">>,<<"ru">>]) -> e(S); +m(S = [<<"blogspot">>,<<"se">>]) -> e(S); +m(S = [<<"blogspot">>,<<"sg">>]) -> e(S); +m(S = [<<"blogspot">>,<<"si">>]) -> e(S); +m(S = [<<"blogspot">>,<<"sk">>]) -> e(S); +m(S = [<<"blogspot">>,<<"sn">>]) -> e(S); +m(S = [<<"blogspot">>,<<"td">>]) -> e(S); +m(S = [<<"blogspot">>,<<"tw">>]) -> e(S); +m(S = [<<"blogspot">>,<<"ug">>]) -> e(S); +m(S = [<<"blogspot">>,<<"vn">>]) -> e(S); +m(S = [<<"goupile">>,<<"fr">>]) -> e(S); +m(S = [<<"gov">>,<<"nl">>]) -> e(S); +m(S = [<<"awsmppl">>,<<"com">>]) -> e(S); +m(S = [<<"xn--gnstigbestellen-zvb">>,<<"de">>]) -> e(S); +m(S = [<<"xn--gnstigliefern-wob">>,<<"de">>]) -> e(S); +m(S = [<<"fin">>,<<"ci">>]) -> e(S); +m(S = [<<"free">>,<<"hr">>]) -> e(S); +m(S = [<<"caa">>,<<"li">>]) -> e(S); +m(S = [<<"ua">>,<<"rs">>]) -> e(S); +m(S = [<<"conf">>,<<"se">>]) -> e(S); +m(S = [<<"hs">>,<<"zone">>]) -> e(S); +m(S = [<<"hs">>,<<"run">>]) -> e(S); +m(S = [<<"hashbang">>,<<"sh">>]) -> e(S); +m(S = [<<"hasura">>,<<"app">>]) -> e(S); +m(S = [<<"hasura-app">>,<<"io">>]) -> e(S); +m(S = [<<"pages">>,<<"it">>,<<"hs-heilbronn">>,<<"de">>]) -> e(S); +m(S = [<<"hepforge">>,<<"org">>]) -> e(S); +m(S = [<<"herokuapp">>,<<"com">>]) -> e(S); +m(S = [<<"herokussl">>,<<"com">>]) -> e(S); +m(S = [<<"ravendb">>,<<"cloud">>]) -> e(S); +m(S = [<<"ravendb">>,<<"community">>]) -> e(S); +m(S = [<<"ravendb">>,<<"me">>]) -> e(S); +m(S = [<<"development">>,<<"run">>]) -> e(S); +m(S = [<<"ravendb">>,<<"run">>]) -> e(S); +m(S = [<<"homesklep">>,<<"pl">>]) -> e(S); +m(S = [<<"secaas">>,<<"hk">>]) -> e(S); +m(S = [<<"hoplix">>,<<"shop">>]) -> e(S); +m(S = [<<"orx">>,<<"biz">>]) -> e(S); +m(S = [<<"biz">>,<<"gl">>]) -> e(S); +m(S = [<<"col">>,<<"ng">>]) -> e(S); +m(S = [<<"firm">>,<<"ng">>]) -> e(S); +m(S = [<<"gen">>,<<"ng">>]) -> e(S); +m(S = [<<"ltd">>,<<"ng">>]) -> e(S); +m(S = [<<"ngo">>,<<"ng">>]) -> e(S); +m(S = [<<"edu">>,<<"scot">>]) -> e(S); +m(S = [<<"sch">>,<<"so">>]) -> e(S); +m(S = [<<"hostyhosting">>,<<"io">>]) -> e(S); +m(S = [<<"xn--hkkinen-5wa">>,<<"fi">>]) -> e(S); +m(S = [_,<<"moonscale">>,<<"io">>]) -> e(S); +m(S = [<<"moonscale">>,<<"net">>]) -> e(S); +m(S = [<<"iki">>,<<"fi">>]) -> e(S); +m(S = [<<"ibxos">>,<<"it">>]) -> e(S); +m(S = [<<"iliadboxos">>,<<"it">>]) -> e(S); +m(S = [<<"impertrixcdn">>,<<"com">>]) -> e(S); +m(S = [<<"impertrix">>,<<"com">>]) -> e(S); +m(S = [<<"smushcdn">>,<<"com">>]) -> e(S); +m(S = [<<"wphostedmail">>,<<"com">>]) -> e(S); +m(S = [<<"wpmucdn">>,<<"com">>]) -> e(S); +m(S = [<<"tempurl">>,<<"host">>]) -> e(S); +m(S = [<<"wpmudev">>,<<"host">>]) -> e(S); +m(S = [<<"dyn-berlin">>,<<"de">>]) -> e(S); +m(S = [<<"in-berlin">>,<<"de">>]) -> e(S); +m(S = [<<"in-brb">>,<<"de">>]) -> e(S); +m(S = [<<"in-butter">>,<<"de">>]) -> e(S); +m(S = [<<"in-dsl">>,<<"de">>]) -> e(S); +m(S = [<<"in-dsl">>,<<"net">>]) -> e(S); +m(S = [<<"in-dsl">>,<<"org">>]) -> e(S); +m(S = [<<"in-vpn">>,<<"de">>]) -> e(S); +m(S = [<<"in-vpn">>,<<"net">>]) -> e(S); +m(S = [<<"in-vpn">>,<<"org">>]) -> e(S); +m(S = [<<"biz">>,<<"at">>]) -> e(S); +m(S = [<<"info">>,<<"at">>]) -> e(S); +m(S = [<<"info">>,<<"cx">>]) -> e(S); +m(S = [<<"ac">>,<<"leg">>,<<"br">>]) -> e(S); +m(S = [<<"al">>,<<"leg">>,<<"br">>]) -> e(S); +m(S = [<<"am">>,<<"leg">>,<<"br">>]) -> e(S); +m(S = [<<"ap">>,<<"leg">>,<<"br">>]) -> e(S); +m(S = [<<"ba">>,<<"leg">>,<<"br">>]) -> e(S); +m(S = [<<"ce">>,<<"leg">>,<<"br">>]) -> e(S); +m(S = [<<"df">>,<<"leg">>,<<"br">>]) -> e(S); +m(S = [<<"es">>,<<"leg">>,<<"br">>]) -> e(S); +m(S = [<<"go">>,<<"leg">>,<<"br">>]) -> e(S); +m(S = [<<"ma">>,<<"leg">>,<<"br">>]) -> e(S); +m(S = [<<"mg">>,<<"leg">>,<<"br">>]) -> e(S); +m(S = [<<"ms">>,<<"leg">>,<<"br">>]) -> e(S); +m(S = [<<"mt">>,<<"leg">>,<<"br">>]) -> e(S); +m(S = [<<"pa">>,<<"leg">>,<<"br">>]) -> e(S); +m(S = [<<"pb">>,<<"leg">>,<<"br">>]) -> e(S); +m(S = [<<"pe">>,<<"leg">>,<<"br">>]) -> e(S); +m(S = [<<"pi">>,<<"leg">>,<<"br">>]) -> e(S); +m(S = [<<"pr">>,<<"leg">>,<<"br">>]) -> e(S); +m(S = [<<"rj">>,<<"leg">>,<<"br">>]) -> e(S); +m(S = [<<"rn">>,<<"leg">>,<<"br">>]) -> e(S); +m(S = [<<"ro">>,<<"leg">>,<<"br">>]) -> e(S); +m(S = [<<"rr">>,<<"leg">>,<<"br">>]) -> e(S); +m(S = [<<"rs">>,<<"leg">>,<<"br">>]) -> e(S); +m(S = [<<"sc">>,<<"leg">>,<<"br">>]) -> e(S); +m(S = [<<"se">>,<<"leg">>,<<"br">>]) -> e(S); +m(S = [<<"sp">>,<<"leg">>,<<"br">>]) -> e(S); +m(S = [<<"to">>,<<"leg">>,<<"br">>]) -> e(S); +m(S = [<<"pixolino">>,<<"com">>]) -> e(S); +m(S = [<<"na4u">>,<<"ru">>]) -> e(S); +m(S = [<<"iopsys">>,<<"se">>]) -> e(S); +m(S = [<<"ipifony">>,<<"net">>]) -> e(S); +m(S = [<<"iservschule">>,<<"de">>]) -> e(S); +m(S = [<<"mein-iserv">>,<<"de">>]) -> e(S); +m(S = [<<"schulplattform">>,<<"de">>]) -> e(S); +m(S = [<<"schulserver">>,<<"de">>]) -> e(S); +m(S = [<<"test-iserv">>,<<"de">>]) -> e(S); +m(S = [<<"iserv">>,<<"dev">>]) -> e(S); +m(S = [<<"iobb">>,<<"net">>]) -> e(S); +m(S = [<<"mel">>,<<"cloudlets">>,<<"com">>,<<"au">>]) -> e(S); +m(S = [<<"cloud">>,<<"interhostsolutions">>,<<"be">>]) -> e(S); +m(S = [<<"users">>,<<"scale">>,<<"virtualcloud">>,<<"com">>,<<"br">>]) -> e(S); +m(S = [<<"mycloud">>,<<"by">>]) -> e(S); +m(S = [<<"alp1">>,<<"ae">>,<<"flow">>,<<"ch">>]) -> e(S); +m(S = [<<"appengine">>,<<"flow">>,<<"ch">>]) -> e(S); +m(S = [<<"es-1">>,<<"axarnet">>,<<"cloud">>]) -> e(S); +m(S = [<<"diadem">>,<<"cloud">>]) -> e(S); +m(S = [<<"vip">>,<<"jelastic">>,<<"cloud">>]) -> e(S); +m(S = [<<"jele">>,<<"cloud">>]) -> e(S); +m(S = [<<"it1">>,<<"eur">>,<<"aruba">>,<<"jenv-aruba">>,<<"cloud">>]) -> e(S); +m(S = [<<"it1">>,<<"jenv-aruba">>,<<"cloud">>]) -> e(S); +m(S = [<<"keliweb">>,<<"cloud">>]) -> e(S); +m(S = [<<"cs">>,<<"keliweb">>,<<"cloud">>]) -> e(S); +m(S = [<<"oxa">>,<<"cloud">>]) -> e(S); +m(S = [<<"tn">>,<<"oxa">>,<<"cloud">>]) -> e(S); +m(S = [<<"uk">>,<<"oxa">>,<<"cloud">>]) -> e(S); +m(S = [<<"primetel">>,<<"cloud">>]) -> e(S); +m(S = [<<"uk">>,<<"primetel">>,<<"cloud">>]) -> e(S); +m(S = [<<"ca">>,<<"reclaim">>,<<"cloud">>]) -> e(S); +m(S = [<<"uk">>,<<"reclaim">>,<<"cloud">>]) -> e(S); +m(S = [<<"us">>,<<"reclaim">>,<<"cloud">>]) -> e(S); +m(S = [<<"ch">>,<<"trendhosting">>,<<"cloud">>]) -> e(S); +m(S = [<<"de">>,<<"trendhosting">>,<<"cloud">>]) -> e(S); +m(S = [<<"jele">>,<<"club">>]) -> e(S); +m(S = [<<"amscompute">>,<<"com">>]) -> e(S); +m(S = [<<"clicketcloud">>,<<"com">>]) -> e(S); +m(S = [<<"dopaas">>,<<"com">>]) -> e(S); +m(S = [<<"hidora">>,<<"com">>]) -> e(S); +m(S = [<<"paas">>,<<"hosted-by-previder">>,<<"com">>]) -> e(S); +m(S = [<<"rag-cloud">>,<<"hosteur">>,<<"com">>]) -> e(S); +m(S = [<<"rag-cloud-ch">>,<<"hosteur">>,<<"com">>]) -> e(S); +m(S = [<<"jcloud">>,<<"ik-server">>,<<"com">>]) -> e(S); +m(S = [<<"jcloud-ver-jpc">>,<<"ik-server">>,<<"com">>]) -> e(S); +m(S = [<<"demo">>,<<"jelastic">>,<<"com">>]) -> e(S); +m(S = [<<"kilatiron">>,<<"com">>]) -> e(S); +m(S = [<<"paas">>,<<"massivegrid">>,<<"com">>]) -> e(S); +m(S = [<<"jed">>,<<"wafaicloud">>,<<"com">>]) -> e(S); +m(S = [<<"lon">>,<<"wafaicloud">>,<<"com">>]) -> e(S); +m(S = [<<"ryd">>,<<"wafaicloud">>,<<"com">>]) -> e(S); +m(S = [<<"j">>,<<"scaleforce">>,<<"com">>,<<"cy">>]) -> e(S); +m(S = [<<"jelastic">>,<<"dogado">>,<<"eu">>]) -> e(S); +m(S = [<<"fi">>,<<"cloudplatform">>,<<"fi">>]) -> e(S); +m(S = [<<"demo">>,<<"datacenter">>,<<"fi">>]) -> e(S); +m(S = [<<"paas">>,<<"datacenter">>,<<"fi">>]) -> e(S); +m(S = [<<"jele">>,<<"host">>]) -> e(S); +m(S = [<<"mircloud">>,<<"host">>]) -> e(S); +m(S = [<<"paas">>,<<"beebyte">>,<<"io">>]) -> e(S); +m(S = [<<"sekd1">>,<<"beebyteapp">>,<<"io">>]) -> e(S); +m(S = [<<"jele">>,<<"io">>]) -> e(S); +m(S = [<<"cloud-fr1">>,<<"unispace">>,<<"io">>]) -> e(S); +m(S = [<<"jc">>,<<"neen">>,<<"it">>]) -> e(S); +m(S = [<<"cloud">>,<<"jelastic">>,<<"open">>,<<"tim">>,<<"it">>]) -> e(S); +m(S = [<<"jcloud">>,<<"kz">>]) -> e(S); +m(S = [<<"upaas">>,<<"kazteleport">>,<<"kz">>]) -> e(S); +m(S = [<<"cloudjiffy">>,<<"net">>]) -> e(S); +m(S = [<<"fra1-de">>,<<"cloudjiffy">>,<<"net">>]) -> e(S); +m(S = [<<"west1-us">>,<<"cloudjiffy">>,<<"net">>]) -> e(S); +m(S = [<<"jls-sto1">>,<<"elastx">>,<<"net">>]) -> e(S); +m(S = [<<"jls-sto2">>,<<"elastx">>,<<"net">>]) -> e(S); +m(S = [<<"jls-sto3">>,<<"elastx">>,<<"net">>]) -> e(S); +m(S = [<<"faststacks">>,<<"net">>]) -> e(S); +m(S = [<<"fr-1">>,<<"paas">>,<<"massivegrid">>,<<"net">>]) -> e(S); +m(S = [<<"lon-1">>,<<"paas">>,<<"massivegrid">>,<<"net">>]) -> e(S); +m(S = [<<"lon-2">>,<<"paas">>,<<"massivegrid">>,<<"net">>]) -> e(S); +m(S = [<<"ny-1">>,<<"paas">>,<<"massivegrid">>,<<"net">>]) -> e(S); +m(S = [<<"ny-2">>,<<"paas">>,<<"massivegrid">>,<<"net">>]) -> e(S); +m(S = [<<"sg-1">>,<<"paas">>,<<"massivegrid">>,<<"net">>]) -> e(S); +m(S = [<<"jelastic">>,<<"saveincloud">>,<<"net">>]) -> e(S); +m(S = [<<"nordeste-idc">>,<<"saveincloud">>,<<"net">>]) -> e(S); +m(S = [<<"j">>,<<"scaleforce">>,<<"net">>]) -> e(S); +m(S = [<<"jelastic">>,<<"tsukaeru">>,<<"net">>]) -> e(S); +m(S = [<<"sdscloud">>,<<"pl">>]) -> e(S); +m(S = [<<"unicloud">>,<<"pl">>]) -> e(S); +m(S = [<<"mircloud">>,<<"ru">>]) -> e(S); +m(S = [<<"jelastic">>,<<"regruhosting">>,<<"ru">>]) -> e(S); +m(S = [<<"enscaled">>,<<"sg">>]) -> e(S); +m(S = [<<"jele">>,<<"site">>]) -> e(S); +m(S = [<<"jelastic">>,<<"team">>]) -> e(S); +m(S = [<<"orangecloud">>,<<"tn">>]) -> e(S); +m(S = [<<"j">>,<<"layershift">>,<<"co">>,<<"uk">>]) -> e(S); +m(S = [<<"phx">>,<<"enscaled">>,<<"us">>]) -> e(S); +m(S = [<<"mircloud">>,<<"us">>]) -> e(S); +m(S = [<<"myjino">>,<<"ru">>]) -> e(S); +m(S = [_,<<"hosting">>,<<"myjino">>,<<"ru">>]) -> e(S); +m(S = [_,<<"landing">>,<<"myjino">>,<<"ru">>]) -> e(S); +m(S = [_,<<"spectrum">>,<<"myjino">>,<<"ru">>]) -> e(S); +m(S = [_,<<"vps">>,<<"myjino">>,<<"ru">>]) -> e(S); +m(S = [<<"jotelulu">>,<<"cloud">>]) -> e(S); +m(S = [_,<<"triton">>,<<"zone">>]) -> e(S); +m(S = [_,<<"cns">>,<<"joyent">>,<<"com">>]) -> e(S); +m(S = [<<"js">>,<<"org">>]) -> e(S); +m(S = [<<"kaas">>,<<"gg">>]) -> e(S); +m(S = [<<"khplay">>,<<"nl">>]) -> e(S); +m(S = [<<"ktistory">>,<<"com">>]) -> e(S); +m(S = [<<"kapsi">>,<<"fi">>]) -> e(S); +m(S = [<<"keymachine">>,<<"de">>]) -> e(S); +m(S = [<<"kinghost">>,<<"net">>]) -> e(S); +m(S = [<<"uni5">>,<<"net">>]) -> e(S); +m(S = [<<"knightpoint">>,<<"systems">>]) -> e(S); +m(S = [<<"koobin">>,<<"events">>]) -> e(S); +m(S = [<<"oya">>,<<"to">>]) -> e(S); +m(S = [<<"kuleuven">>,<<"cloud">>]) -> e(S); +m(S = [<<"ezproxy">>,<<"kuleuven">>,<<"be">>]) -> e(S); +m(S = [<<"co">>,<<"krd">>]) -> e(S); +m(S = [<<"edu">>,<<"krd">>]) -> e(S); +m(S = [<<"krellian">>,<<"net">>]) -> e(S); +m(S = [<<"webthings">>,<<"io">>]) -> e(S); +m(S = [<<"git-repos">>,<<"de">>]) -> e(S); +m(S = [<<"lcube-server">>,<<"de">>]) -> e(S); +m(S = [<<"svn-repos">>,<<"de">>]) -> e(S); +m(S = [<<"leadpages">>,<<"co">>]) -> e(S); +m(S = [<<"lpages">>,<<"co">>]) -> e(S); +m(S = [<<"lpusercontent">>,<<"com">>]) -> e(S); +m(S = [<<"lelux">>,<<"site">>]) -> e(S); +m(S = [<<"co">>,<<"business">>]) -> e(S); +m(S = [<<"co">>,<<"education">>]) -> e(S); +m(S = [<<"co">>,<<"events">>]) -> e(S); +m(S = [<<"co">>,<<"financial">>]) -> e(S); +m(S = [<<"co">>,<<"network">>]) -> e(S); +m(S = [<<"co">>,<<"place">>]) -> e(S); +m(S = [<<"co">>,<<"technology">>]) -> e(S); +m(S = [<<"app">>,<<"lmpm">>,<<"com">>]) -> e(S); +m(S = [<<"linkyard">>,<<"cloud">>]) -> e(S); +m(S = [<<"linkyard-cloud">>,<<"ch">>]) -> e(S); +m(S = [<<"members">>,<<"linode">>,<<"com">>]) -> e(S); +m(S = [_,<<"nodebalancer">>,<<"linode">>,<<"com">>]) -> e(S); +m(S = [_,<<"linodeobjects">>,<<"com">>]) -> e(S); +m(S = [<<"ip">>,<<"linodeusercontent">>,<<"com">>]) -> e(S); +m(S = [<<"we">>,<<"bs">>]) -> e(S); +m(S = [_,<<"user">>,<<"localcert">>,<<"dev">>]) -> e(S); +m(S = [<<"localzone">>,<<"xyz">>]) -> e(S); +m(S = [<<"loginline">>,<<"app">>]) -> e(S); +m(S = [<<"loginline">>,<<"dev">>]) -> e(S); +m(S = [<<"loginline">>,<<"io">>]) -> e(S); +m(S = [<<"loginline">>,<<"services">>]) -> e(S); +m(S = [<<"loginline">>,<<"site">>]) -> e(S); +m(S = [<<"servers">>,<<"run">>]) -> e(S); +m(S = [<<"lohmus">>,<<"me">>]) -> e(S); +m(S = [<<"krasnik">>,<<"pl">>]) -> e(S); +m(S = [<<"leczna">>,<<"pl">>]) -> e(S); +m(S = [<<"lubartow">>,<<"pl">>]) -> e(S); +m(S = [<<"lublin">>,<<"pl">>]) -> e(S); +m(S = [<<"poniatowa">>,<<"pl">>]) -> e(S); +m(S = [<<"swidnik">>,<<"pl">>]) -> e(S); +m(S = [<<"glug">>,<<"org">>,<<"uk">>]) -> e(S); +m(S = [<<"lug">>,<<"org">>,<<"uk">>]) -> e(S); +m(S = [<<"lugs">>,<<"org">>,<<"uk">>]) -> e(S); +m(S = [<<"barsy">>,<<"bg">>]) -> e(S); +m(S = [<<"barsy">>,<<"co">>,<<"uk">>]) -> e(S); +m(S = [<<"barsyonline">>,<<"co">>,<<"uk">>]) -> e(S); +m(S = [<<"barsycenter">>,<<"com">>]) -> e(S); +m(S = [<<"barsyonline">>,<<"com">>]) -> e(S); +m(S = [<<"barsy">>,<<"club">>]) -> e(S); +m(S = [<<"barsy">>,<<"de">>]) -> e(S); +m(S = [<<"barsy">>,<<"eu">>]) -> e(S); +m(S = [<<"barsy">>,<<"in">>]) -> e(S); +m(S = [<<"barsy">>,<<"info">>]) -> e(S); +m(S = [<<"barsy">>,<<"io">>]) -> e(S); +m(S = [<<"barsy">>,<<"me">>]) -> e(S); +m(S = [<<"barsy">>,<<"menu">>]) -> e(S); +m(S = [<<"barsy">>,<<"mobi">>]) -> e(S); +m(S = [<<"barsy">>,<<"net">>]) -> e(S); +m(S = [<<"barsy">>,<<"online">>]) -> e(S); +m(S = [<<"barsy">>,<<"org">>]) -> e(S); +m(S = [<<"barsy">>,<<"pro">>]) -> e(S); +m(S = [<<"barsy">>,<<"pub">>]) -> e(S); +m(S = [<<"barsy">>,<<"ro">>]) -> e(S); +m(S = [<<"barsy">>,<<"shop">>]) -> e(S); +m(S = [<<"barsy">>,<<"site">>]) -> e(S); +m(S = [<<"barsy">>,<<"support">>]) -> e(S); +m(S = [<<"barsy">>,<<"uk">>]) -> e(S); +m(S = [_,<<"magentosite">>,<<"cloud">>]) -> e(S); +m(S = [<<"mayfirst">>,<<"info">>]) -> e(S); +m(S = [<<"mayfirst">>,<<"org">>]) -> e(S); +m(S = [<<"hb">>,<<"cldmail">>,<<"ru">>]) -> e(S); +m(S = [<<"cn">>,<<"vu">>]) -> e(S); +m(S = [<<"mazeplay">>,<<"com">>]) -> e(S); +m(S = [<<"mcpe">>,<<"me">>]) -> e(S); +m(S = [<<"mcdir">>,<<"me">>]) -> e(S); +m(S = [<<"mcdir">>,<<"ru">>]) -> e(S); +m(S = [<<"mcpre">>,<<"ru">>]) -> e(S); +m(S = [<<"vps">>,<<"mcdir">>,<<"ru">>]) -> e(S); +m(S = [<<"mediatech">>,<<"by">>]) -> e(S); +m(S = [<<"mediatech">>,<<"dev">>]) -> e(S); +m(S = [<<"hra">>,<<"health">>]) -> e(S); +m(S = [<<"miniserver">>,<<"com">>]) -> e(S); +m(S = [<<"memset">>,<<"net">>]) -> e(S); +m(S = [<<"messerli">>,<<"app">>]) -> e(S); +m(S = [_,<<"cloud">>,<<"metacentrum">>,<<"cz">>]) -> e(S); +m(S = [<<"custom">>,<<"metacentrum">>,<<"cz">>]) -> e(S); +m(S = [<<"flt">>,<<"cloud">>,<<"muni">>,<<"cz">>]) -> e(S); +m(S = [<<"usr">>,<<"cloud">>,<<"muni">>,<<"cz">>]) -> e(S); +m(S = [<<"meteorapp">>,<<"com">>]) -> e(S); +m(S = [<<"eu">>,<<"meteorapp">>,<<"com">>]) -> e(S); +m(S = [<<"co">>,<<"pl">>]) -> e(S); +m(S = [_,<<"azurecontainer">>,<<"io">>]) -> e(S); +m(S = [<<"azurewebsites">>,<<"net">>]) -> e(S); +m(S = [<<"azure-mobile">>,<<"net">>]) -> e(S); +m(S = [<<"cloudapp">>,<<"net">>]) -> e(S); +m(S = [<<"azurestaticapps">>,<<"net">>]) -> e(S); +m(S = [<<"1">>,<<"azurestaticapps">>,<<"net">>]) -> e(S); +m(S = [<<"2">>,<<"azurestaticapps">>,<<"net">>]) -> e(S); +m(S = [<<"centralus">>,<<"azurestaticapps">>,<<"net">>]) -> e(S); +m(S = [<<"eastasia">>,<<"azurestaticapps">>,<<"net">>]) -> e(S); +m(S = [<<"eastus2">>,<<"azurestaticapps">>,<<"net">>]) -> e(S); +m(S = [<<"westeurope">>,<<"azurestaticapps">>,<<"net">>]) -> e(S); +m(S = [<<"westus2">>,<<"azurestaticapps">>,<<"net">>]) -> e(S); +m(S = [<<"csx">>,<<"cc">>]) -> e(S); +m(S = [<<"mintere">>,<<"site">>]) -> e(S); +m(S = [<<"forte">>,<<"id">>]) -> e(S); +m(S = [<<"mozilla-iot">>,<<"org">>]) -> e(S); +m(S = [<<"bmoattachments">>,<<"org">>]) -> e(S); +m(S = [<<"net">>,<<"ru">>]) -> e(S); +m(S = [<<"org">>,<<"ru">>]) -> e(S); +m(S = [<<"pp">>,<<"ru">>]) -> e(S); +m(S = [<<"hostedpi">>,<<"com">>]) -> e(S); +m(S = [<<"customer">>,<<"mythic-beasts">>,<<"com">>]) -> e(S); +m(S = [<<"caracal">>,<<"mythic-beasts">>,<<"com">>]) -> e(S); +m(S = [<<"fentiger">>,<<"mythic-beasts">>,<<"com">>]) -> e(S); +m(S = [<<"lynx">>,<<"mythic-beasts">>,<<"com">>]) -> e(S); +m(S = [<<"ocelot">>,<<"mythic-beasts">>,<<"com">>]) -> e(S); +m(S = [<<"oncilla">>,<<"mythic-beasts">>,<<"com">>]) -> e(S); +m(S = [<<"onza">>,<<"mythic-beasts">>,<<"com">>]) -> e(S); +m(S = [<<"sphinx">>,<<"mythic-beasts">>,<<"com">>]) -> e(S); +m(S = [<<"vs">>,<<"mythic-beasts">>,<<"com">>]) -> e(S); +m(S = [<<"x">>,<<"mythic-beasts">>,<<"com">>]) -> e(S); +m(S = [<<"yali">>,<<"mythic-beasts">>,<<"com">>]) -> e(S); +m(S = [<<"cust">>,<<"retrosnub">>,<<"co">>,<<"uk">>]) -> e(S); +m(S = [<<"ui">>,<<"nabu">>,<<"casa">>]) -> e(S); +m(S = [<<"cloud">>,<<"nospamproxy">>,<<"com">>]) -> e(S); +m(S = [<<"netlify">>,<<"app">>]) -> e(S); +m(S = [<<"4u">>,<<"com">>]) -> e(S); +m(S = [<<"ngrok">>,<<"io">>]) -> e(S); +m(S = [<<"nh-serv">>,<<"co">>,<<"uk">>]) -> e(S); +m(S = [<<"nfshost">>,<<"com">>]) -> e(S); +m(S = [_,<<"developer">>,<<"app">>]) -> e(S); +m(S = [<<"noop">>,<<"app">>]) -> e(S); +m(S = [_,<<"northflank">>,<<"app">>]) -> e(S); +m(S = [_,<<"build">>,<<"run">>]) -> e(S); +m(S = [_,<<"code">>,<<"run">>]) -> e(S); +m(S = [_,<<"database">>,<<"run">>]) -> e(S); +m(S = [_,<<"migration">>,<<"run">>]) -> e(S); +m(S = [<<"noticeable">>,<<"news">>]) -> e(S); +m(S = [<<"dnsking">>,<<"ch">>]) -> e(S); +m(S = [<<"mypi">>,<<"co">>]) -> e(S); +m(S = [<<"n4t">>,<<"co">>]) -> e(S); +m(S = [<<"001www">>,<<"com">>]) -> e(S); +m(S = [<<"ddnslive">>,<<"com">>]) -> e(S); +m(S = [<<"myiphost">>,<<"com">>]) -> e(S); +m(S = [<<"forumz">>,<<"info">>]) -> e(S); +m(S = [<<"16-b">>,<<"it">>]) -> e(S); +m(S = [<<"32-b">>,<<"it">>]) -> e(S); +m(S = [<<"64-b">>,<<"it">>]) -> e(S); +m(S = [<<"soundcast">>,<<"me">>]) -> e(S); +m(S = [<<"tcp4">>,<<"me">>]) -> e(S); +m(S = [<<"dnsup">>,<<"net">>]) -> e(S); +m(S = [<<"hicam">>,<<"net">>]) -> e(S); +m(S = [<<"now-dns">>,<<"net">>]) -> e(S); +m(S = [<<"ownip">>,<<"net">>]) -> e(S); +m(S = [<<"vpndns">>,<<"net">>]) -> e(S); +m(S = [<<"dynserv">>,<<"org">>]) -> e(S); +m(S = [<<"now-dns">>,<<"org">>]) -> e(S); +m(S = [<<"x443">>,<<"pw">>]) -> e(S); +m(S = [<<"now-dns">>,<<"top">>]) -> e(S); +m(S = [<<"ntdll">>,<<"top">>]) -> e(S); +m(S = [<<"freeddns">>,<<"us">>]) -> e(S); +m(S = [<<"crafting">>,<<"xyz">>]) -> e(S); +m(S = [<<"zapto">>,<<"xyz">>]) -> e(S); +m(S = [<<"nsupdate">>,<<"info">>]) -> e(S); +m(S = [<<"nerdpol">>,<<"ovh">>]) -> e(S); +m(S = [<<"blogsyte">>,<<"com">>]) -> e(S); +m(S = [<<"brasilia">>,<<"me">>]) -> e(S); +m(S = [<<"cable-modem">>,<<"org">>]) -> e(S); +m(S = [<<"ciscofreak">>,<<"com">>]) -> e(S); +m(S = [<<"collegefan">>,<<"org">>]) -> e(S); +m(S = [<<"couchpotatofries">>,<<"org">>]) -> e(S); +m(S = [<<"damnserver">>,<<"com">>]) -> e(S); +m(S = [<<"ddns">>,<<"me">>]) -> e(S); +m(S = [<<"ditchyourip">>,<<"com">>]) -> e(S); +m(S = [<<"dnsfor">>,<<"me">>]) -> e(S); +m(S = [<<"dnsiskinky">>,<<"com">>]) -> e(S); +m(S = [<<"dvrcam">>,<<"info">>]) -> e(S); +m(S = [<<"dynns">>,<<"com">>]) -> e(S); +m(S = [<<"eating-organic">>,<<"net">>]) -> e(S); +m(S = [<<"fantasyleague">>,<<"cc">>]) -> e(S); +m(S = [<<"geekgalaxy">>,<<"com">>]) -> e(S); +m(S = [<<"golffan">>,<<"us">>]) -> e(S); +m(S = [<<"health-carereform">>,<<"com">>]) -> e(S); +m(S = [<<"homesecuritymac">>,<<"com">>]) -> e(S); +m(S = [<<"homesecuritypc">>,<<"com">>]) -> e(S); +m(S = [<<"hopto">>,<<"me">>]) -> e(S); +m(S = [<<"ilovecollege">>,<<"info">>]) -> e(S); +m(S = [<<"loginto">>,<<"me">>]) -> e(S); +m(S = [<<"mlbfan">>,<<"org">>]) -> e(S); +m(S = [<<"mmafan">>,<<"biz">>]) -> e(S); +m(S = [<<"myactivedirectory">>,<<"com">>]) -> e(S); +m(S = [<<"mydissent">>,<<"net">>]) -> e(S); +m(S = [<<"myeffect">>,<<"net">>]) -> e(S); +m(S = [<<"mymediapc">>,<<"net">>]) -> e(S); +m(S = [<<"mypsx">>,<<"net">>]) -> e(S); +m(S = [<<"mysecuritycamera">>,<<"com">>]) -> e(S); +m(S = [<<"mysecuritycamera">>,<<"net">>]) -> e(S); +m(S = [<<"mysecuritycamera">>,<<"org">>]) -> e(S); +m(S = [<<"net-freaks">>,<<"com">>]) -> e(S); +m(S = [<<"nflfan">>,<<"org">>]) -> e(S); +m(S = [<<"nhlfan">>,<<"net">>]) -> e(S); +m(S = [<<"no-ip">>,<<"ca">>]) -> e(S); +m(S = [<<"no-ip">>,<<"co">>,<<"uk">>]) -> e(S); +m(S = [<<"no-ip">>,<<"net">>]) -> e(S); +m(S = [<<"noip">>,<<"us">>]) -> e(S); +m(S = [<<"onthewifi">>,<<"com">>]) -> e(S); +m(S = [<<"pgafan">>,<<"net">>]) -> e(S); +m(S = [<<"point2this">>,<<"com">>]) -> e(S); +m(S = [<<"pointto">>,<<"us">>]) -> e(S); +m(S = [<<"privatizehealthinsurance">>,<<"net">>]) -> e(S); +m(S = [<<"quicksytes">>,<<"com">>]) -> e(S); +m(S = [<<"read-books">>,<<"org">>]) -> e(S); +m(S = [<<"securitytactics">>,<<"com">>]) -> e(S); +m(S = [<<"serveexchange">>,<<"com">>]) -> e(S); +m(S = [<<"servehumour">>,<<"com">>]) -> e(S); +m(S = [<<"servep2p">>,<<"com">>]) -> e(S); +m(S = [<<"servesarcasm">>,<<"com">>]) -> e(S); +m(S = [<<"stufftoread">>,<<"com">>]) -> e(S); +m(S = [<<"ufcfan">>,<<"org">>]) -> e(S); +m(S = [<<"unusualperson">>,<<"com">>]) -> e(S); +m(S = [<<"workisboring">>,<<"com">>]) -> e(S); +m(S = [<<"3utilities">>,<<"com">>]) -> e(S); +m(S = [<<"bounceme">>,<<"net">>]) -> e(S); +m(S = [<<"ddns">>,<<"net">>]) -> e(S); +m(S = [<<"ddnsking">>,<<"com">>]) -> e(S); +m(S = [<<"gotdns">>,<<"ch">>]) -> e(S); +m(S = [<<"hopto">>,<<"org">>]) -> e(S); +m(S = [<<"myftp">>,<<"biz">>]) -> e(S); +m(S = [<<"myftp">>,<<"org">>]) -> e(S); +m(S = [<<"myvnc">>,<<"com">>]) -> e(S); +m(S = [<<"no-ip">>,<<"biz">>]) -> e(S); +m(S = [<<"no-ip">>,<<"info">>]) -> e(S); +m(S = [<<"no-ip">>,<<"org">>]) -> e(S); +m(S = [<<"noip">>,<<"me">>]) -> e(S); +m(S = [<<"redirectme">>,<<"net">>]) -> e(S); +m(S = [<<"servebeer">>,<<"com">>]) -> e(S); +m(S = [<<"serveblog">>,<<"net">>]) -> e(S); +m(S = [<<"servecounterstrike">>,<<"com">>]) -> e(S); +m(S = [<<"serveftp">>,<<"com">>]) -> e(S); +m(S = [<<"servegame">>,<<"com">>]) -> e(S); +m(S = [<<"servehalflife">>,<<"com">>]) -> e(S); +m(S = [<<"servehttp">>,<<"com">>]) -> e(S); +m(S = [<<"serveirc">>,<<"com">>]) -> e(S); +m(S = [<<"serveminecraft">>,<<"net">>]) -> e(S); +m(S = [<<"servemp3">>,<<"com">>]) -> e(S); +m(S = [<<"servepics">>,<<"com">>]) -> e(S); +m(S = [<<"servequake">>,<<"com">>]) -> e(S); +m(S = [<<"sytes">>,<<"net">>]) -> e(S); +m(S = [<<"webhop">>,<<"me">>]) -> e(S); +m(S = [<<"zapto">>,<<"org">>]) -> e(S); +m(S = [<<"stage">>,<<"nodeart">>,<<"io">>]) -> e(S); +m(S = [<<"pcloud">>,<<"host">>]) -> e(S); +m(S = [<<"nyc">>,<<"mn">>]) -> e(S); +m(S = [<<"static">>,<<"observableusercontent">>,<<"com">>]) -> e(S); +m(S = [<<"cya">>,<<"gg">>]) -> e(S); +m(S = [<<"omg">>,<<"lol">>]) -> e(S); +m(S = [<<"cloudycluster">>,<<"net">>]) -> e(S); +m(S = [<<"omniwe">>,<<"site">>]) -> e(S); +m(S = [<<"123hjemmeside">>,<<"dk">>]) -> e(S); +m(S = [<<"123hjemmeside">>,<<"no">>]) -> e(S); +m(S = [<<"123homepage">>,<<"it">>]) -> e(S); +m(S = [<<"123kotisivu">>,<<"fi">>]) -> e(S); +m(S = [<<"123minsida">>,<<"se">>]) -> e(S); +m(S = [<<"123miweb">>,<<"es">>]) -> e(S); +m(S = [<<"123paginaweb">>,<<"pt">>]) -> e(S); +m(S = [<<"123sait">>,<<"ru">>]) -> e(S); +m(S = [<<"123siteweb">>,<<"fr">>]) -> e(S); +m(S = [<<"123webseite">>,<<"at">>]) -> e(S); +m(S = [<<"123webseite">>,<<"de">>]) -> e(S); +m(S = [<<"123website">>,<<"be">>]) -> e(S); +m(S = [<<"123website">>,<<"ch">>]) -> e(S); +m(S = [<<"123website">>,<<"lu">>]) -> e(S); +m(S = [<<"123website">>,<<"nl">>]) -> e(S); +m(S = [<<"service">>,<<"one">>]) -> e(S); +m(S = [<<"simplesite">>,<<"com">>]) -> e(S); +m(S = [<<"simplesite">>,<<"com">>,<<"br">>]) -> e(S); +m(S = [<<"simplesite">>,<<"gr">>]) -> e(S); +m(S = [<<"simplesite">>,<<"pl">>]) -> e(S); +m(S = [<<"nid">>,<<"io">>]) -> e(S); +m(S = [<<"opensocial">>,<<"site">>]) -> e(S); +m(S = [<<"opencraft">>,<<"hosting">>]) -> e(S); +m(S = [<<"orsites">>,<<"com">>]) -> e(S); +m(S = [<<"operaunite">>,<<"com">>]) -> e(S); +m(S = [<<"tech">>,<<"orange">>]) -> e(S); +m(S = [<<"authgear-staging">>,<<"com">>]) -> e(S); +m(S = [<<"authgearapps">>,<<"com">>]) -> e(S); +m(S = [<<"skygearapp">>,<<"com">>]) -> e(S); +m(S = [<<"outsystemscloud">>,<<"com">>]) -> e(S); +m(S = [_,<<"webpaas">>,<<"ovh">>,<<"net">>]) -> e(S); +m(S = [_,<<"hosting">>,<<"ovh">>,<<"net">>]) -> e(S); +m(S = [<<"ownprovider">>,<<"com">>]) -> e(S); +m(S = [<<"own">>,<<"pm">>]) -> e(S); +m(S = [_,<<"owo">>,<<"codes">>]) -> e(S); +m(S = [<<"ox">>,<<"rs">>]) -> e(S); +m(S = [<<"oy">>,<<"lc">>]) -> e(S); +m(S = [<<"pgfog">>,<<"com">>]) -> e(S); +m(S = [<<"pagefrontapp">>,<<"com">>]) -> e(S); +m(S = [<<"pagexl">>,<<"com">>]) -> e(S); +m(S = [_,<<"paywhirl">>,<<"com">>]) -> e(S); +m(S = [<<"bar0">>,<<"net">>]) -> e(S); +m(S = [<<"bar1">>,<<"net">>]) -> e(S); +m(S = [<<"bar2">>,<<"net">>]) -> e(S); +m(S = [<<"rdv">>,<<"to">>]) -> e(S); +m(S = [<<"art">>,<<"pl">>]) -> e(S); +m(S = [<<"gliwice">>,<<"pl">>]) -> e(S); +m(S = [<<"krakow">>,<<"pl">>]) -> e(S); +m(S = [<<"poznan">>,<<"pl">>]) -> e(S); +m(S = [<<"wroc">>,<<"pl">>]) -> e(S); +m(S = [<<"zakopane">>,<<"pl">>]) -> e(S); +m(S = [<<"pantheonsite">>,<<"io">>]) -> e(S); +m(S = [<<"gotpantheon">>,<<"com">>]) -> e(S); +m(S = [<<"mypep">>,<<"link">>]) -> e(S); +m(S = [<<"perspecta">>,<<"cloud">>]) -> e(S); +m(S = [<<"lk3">>,<<"ru">>]) -> e(S); +m(S = [<<"on-web">>,<<"fr">>]) -> e(S); +m(S = [<<"bc">>,<<"platform">>,<<"sh">>]) -> e(S); +m(S = [<<"ent">>,<<"platform">>,<<"sh">>]) -> e(S); +m(S = [<<"eu">>,<<"platform">>,<<"sh">>]) -> e(S); +m(S = [<<"us">>,<<"platform">>,<<"sh">>]) -> e(S); +m(S = [_,<<"platformsh">>,<<"site">>]) -> e(S); +m(S = [_,<<"tst">>,<<"site">>]) -> e(S); +m(S = [<<"platter-app">>,<<"com">>]) -> e(S); +m(S = [<<"platter-app">>,<<"dev">>]) -> e(S); +m(S = [<<"platterp">>,<<"us">>]) -> e(S); +m(S = [<<"pdns">>,<<"page">>]) -> e(S); +m(S = [<<"plesk">>,<<"page">>]) -> e(S); +m(S = [<<"pleskns">>,<<"com">>]) -> e(S); +m(S = [<<"dyn53">>,<<"io">>]) -> e(S); +m(S = [<<"onporter">>,<<"run">>]) -> e(S); +m(S = [<<"co">>,<<"bn">>]) -> e(S); +m(S = [<<"postman-echo">>,<<"com">>]) -> e(S); +m(S = [<<"pstmn">>,<<"io">>]) -> e(S); +m(S = [<<"mock">>,<<"pstmn">>,<<"io">>]) -> e(S); +m(S = [<<"httpbin">>,<<"org">>]) -> e(S); +m(S = [<<"prequalifyme">>,<<"today">>]) -> e(S); +m(S = [<<"xen">>,<<"prgmr">>,<<"com">>]) -> e(S); +m(S = [<<"priv">>,<<"at">>]) -> e(S); +m(S = [<<"prvcy">>,<<"page">>]) -> e(S); +m(S = [_,<<"dweb">>,<<"link">>]) -> e(S); +m(S = [<<"protonet">>,<<"io">>]) -> e(S); +m(S = [<<"chirurgiens-dentistes-en-france">>,<<"fr">>]) -> e(S); +m(S = [<<"byen">>,<<"site">>]) -> e(S); +m(S = [<<"pubtls">>,<<"org">>]) -> e(S); +m(S = [<<"pythonanywhere">>,<<"com">>]) -> e(S); +m(S = [<<"eu">>,<<"pythonanywhere">>,<<"com">>]) -> e(S); +m(S = [<<"qoto">>,<<"io">>]) -> e(S); +m(S = [<<"qualifioapp">>,<<"com">>]) -> e(S); +m(S = [<<"qbuser">>,<<"com">>]) -> e(S); +m(S = [<<"cloudsite">>,<<"builders">>]) -> e(S); +m(S = [<<"instances">>,<<"spawn">>,<<"cc">>]) -> e(S); +m(S = [<<"instantcloud">>,<<"cn">>]) -> e(S); +m(S = [<<"ras">>,<<"ru">>]) -> e(S); +m(S = [<<"qa2">>,<<"com">>]) -> e(S); +m(S = [<<"qcx">>,<<"io">>]) -> e(S); +m(S = [_,<<"sys">>,<<"qcx">>,<<"io">>]) -> e(S); +m(S = [<<"dev-myqnapcloud">>,<<"com">>]) -> e(S); +m(S = [<<"alpha-myqnapcloud">>,<<"com">>]) -> e(S); +m(S = [<<"myqnapcloud">>,<<"com">>]) -> e(S); +m(S = [_,<<"quipelements">>,<<"com">>]) -> e(S); +m(S = [<<"vapor">>,<<"cloud">>]) -> e(S); +m(S = [<<"vaporcloud">>,<<"io">>]) -> e(S); +m(S = [<<"rackmaze">>,<<"com">>]) -> e(S); +m(S = [<<"rackmaze">>,<<"net">>]) -> e(S); +m(S = [<<"g">>,<<"vbrplsbx">>,<<"io">>]) -> e(S); +m(S = [_,<<"on-k3s">>,<<"io">>]) -> e(S); +m(S = [_,<<"on-rancher">>,<<"cloud">>]) -> e(S); +m(S = [_,<<"on-rio">>,<<"io">>]) -> e(S); +m(S = [<<"readthedocs">>,<<"io">>]) -> e(S); +m(S = [<<"rhcloud">>,<<"com">>]) -> e(S); +m(S = [<<"app">>,<<"render">>,<<"com">>]) -> e(S); +m(S = [<<"onrender">>,<<"com">>]) -> e(S); +m(S = [<<"firewalledreplit">>,<<"co">>]) -> e(S); +m(S = [<<"id">>,<<"firewalledreplit">>,<<"co">>]) -> e(S); +m(S = [<<"repl">>,<<"co">>]) -> e(S); +m(S = [<<"id">>,<<"repl">>,<<"co">>]) -> e(S); +m(S = [<<"repl">>,<<"run">>]) -> e(S); +m(S = [<<"resindevice">>,<<"io">>]) -> e(S); +m(S = [<<"devices">>,<<"resinstaging">>,<<"io">>]) -> e(S); +m(S = [<<"hzc">>,<<"io">>]) -> e(S); +m(S = [<<"wellbeingzone">>,<<"eu">>]) -> e(S); +m(S = [<<"wellbeingzone">>,<<"co">>,<<"uk">>]) -> e(S); +m(S = [<<"adimo">>,<<"co">>,<<"uk">>]) -> e(S); +m(S = [<<"itcouldbewor">>,<<"se">>]) -> e(S); +m(S = [<<"git-pages">>,<<"rit">>,<<"edu">>]) -> e(S); +m(S = [<<"rocky">>,<<"page">>]) -> e(S); +m(S = [<<"xn--90amc">>,<<"xn--p1acf">>]) -> e(S); +m(S = [<<"xn--j1aef">>,<<"xn--p1acf">>]) -> e(S); +m(S = [<<"xn--j1ael8b">>,<<"xn--p1acf">>]) -> e(S); +m(S = [<<"xn--h1ahn">>,<<"xn--p1acf">>]) -> e(S); +m(S = [<<"xn--j1adp">>,<<"xn--p1acf">>]) -> e(S); +m(S = [<<"xn--c1avg">>,<<"xn--p1acf">>]) -> e(S); +m(S = [<<"xn--80aaa0cvac">>,<<"xn--p1acf">>]) -> e(S); +m(S = [<<"xn--h1aliz">>,<<"xn--p1acf">>]) -> e(S); +m(S = [<<"xn--90a1af">>,<<"xn--p1acf">>]) -> e(S); +m(S = [<<"xn--41a">>,<<"xn--p1acf">>]) -> e(S); +m(S = [_,<<"builder">>,<<"code">>,<<"com">>]) -> e(S); +m(S = [_,<<"dev-builder">>,<<"code">>,<<"com">>]) -> e(S); +m(S = [_,<<"stg-builder">>,<<"code">>,<<"com">>]) -> e(S); +m(S = [<<"sandcats">>,<<"io">>]) -> e(S); +m(S = [<<"logoip">>,<<"de">>]) -> e(S); +m(S = [<<"logoip">>,<<"com">>]) -> e(S); +m(S = [<<"fr-par-1">>,<<"baremetal">>,<<"scw">>,<<"cloud">>]) -> e(S); +m(S = [<<"fr-par-2">>,<<"baremetal">>,<<"scw">>,<<"cloud">>]) -> e(S); +m(S = [<<"nl-ams-1">>,<<"baremetal">>,<<"scw">>,<<"cloud">>]) -> e(S); +m(S = [<<"fnc">>,<<"fr-par">>,<<"scw">>,<<"cloud">>]) -> e(S); +m(S = [<<"functions">>,<<"fnc">>,<<"fr-par">>,<<"scw">>,<<"cloud">>]) -> e(S); +m(S = [<<"k8s">>,<<"fr-par">>,<<"scw">>,<<"cloud">>]) -> e(S); +m(S = [<<"nodes">>,<<"k8s">>,<<"fr-par">>,<<"scw">>,<<"cloud">>]) -> e(S); +m(S = [<<"s3">>,<<"fr-par">>,<<"scw">>,<<"cloud">>]) -> e(S); +m(S = [<<"s3-website">>,<<"fr-par">>,<<"scw">>,<<"cloud">>]) -> e(S); +m(S = [<<"whm">>,<<"fr-par">>,<<"scw">>,<<"cloud">>]) -> e(S); +m(S = [<<"priv">>,<<"instances">>,<<"scw">>,<<"cloud">>]) -> e(S); +m(S = [<<"pub">>,<<"instances">>,<<"scw">>,<<"cloud">>]) -> e(S); +m(S = [<<"k8s">>,<<"scw">>,<<"cloud">>]) -> e(S); +m(S = [<<"k8s">>,<<"nl-ams">>,<<"scw">>,<<"cloud">>]) -> e(S); +m(S = [<<"nodes">>,<<"k8s">>,<<"nl-ams">>,<<"scw">>,<<"cloud">>]) -> e(S); +m(S = [<<"s3">>,<<"nl-ams">>,<<"scw">>,<<"cloud">>]) -> e(S); +m(S = [<<"s3-website">>,<<"nl-ams">>,<<"scw">>,<<"cloud">>]) -> e(S); +m(S = [<<"whm">>,<<"nl-ams">>,<<"scw">>,<<"cloud">>]) -> e(S); +m(S = [<<"k8s">>,<<"pl-waw">>,<<"scw">>,<<"cloud">>]) -> e(S); +m(S = [<<"nodes">>,<<"k8s">>,<<"pl-waw">>,<<"scw">>,<<"cloud">>]) -> e(S); +m(S = [<<"s3">>,<<"pl-waw">>,<<"scw">>,<<"cloud">>]) -> e(S); +m(S = [<<"s3-website">>,<<"pl-waw">>,<<"scw">>,<<"cloud">>]) -> e(S); +m(S = [<<"scalebook">>,<<"scw">>,<<"cloud">>]) -> e(S); +m(S = [<<"smartlabeling">>,<<"scw">>,<<"cloud">>]) -> e(S); +m(S = [<<"dedibox">>,<<"fr">>]) -> e(S); +m(S = [<<"schokokeks">>,<<"net">>]) -> e(S); +m(S = [<<"gov">>,<<"scot">>]) -> e(S); +m(S = [<<"service">>,<<"gov">>,<<"scot">>]) -> e(S); +m(S = [<<"scrysec">>,<<"com">>]) -> e(S); +m(S = [<<"firewall-gateway">>,<<"com">>]) -> e(S); +m(S = [<<"firewall-gateway">>,<<"de">>]) -> e(S); +m(S = [<<"my-gateway">>,<<"de">>]) -> e(S); +m(S = [<<"my-router">>,<<"de">>]) -> e(S); +m(S = [<<"spdns">>,<<"de">>]) -> e(S); +m(S = [<<"spdns">>,<<"eu">>]) -> e(S); +m(S = [<<"firewall-gateway">>,<<"net">>]) -> e(S); +m(S = [<<"my-firewall">>,<<"org">>]) -> e(S); +m(S = [<<"myfirewall">>,<<"org">>]) -> e(S); +m(S = [<<"spdns">>,<<"org">>]) -> e(S); +m(S = [<<"seidat">>,<<"net">>]) -> e(S); +m(S = [<<"sellfy">>,<<"store">>]) -> e(S); +m(S = [<<"senseering">>,<<"net">>]) -> e(S); +m(S = [<<"minisite">>,<<"ms">>]) -> e(S); +m(S = [<<"magnet">>,<<"page">>]) -> e(S); +m(S = [<<"biz">>,<<"ua">>]) -> e(S); +m(S = [<<"co">>,<<"ua">>]) -> e(S); +m(S = [<<"pp">>,<<"ua">>]) -> e(S); +m(S = [<<"shiftcrypto">>,<<"dev">>]) -> e(S); +m(S = [<<"shiftcrypto">>,<<"io">>]) -> e(S); +m(S = [<<"shiftedit">>,<<"io">>]) -> e(S); +m(S = [<<"myshopblocks">>,<<"com">>]) -> e(S); +m(S = [<<"myshopify">>,<<"com">>]) -> e(S); +m(S = [<<"shopitsite">>,<<"com">>]) -> e(S); +m(S = [<<"shopware">>,<<"store">>]) -> e(S); +m(S = [<<"mo-siemens">>,<<"io">>]) -> e(S); +m(S = [<<"1kapp">>,<<"com">>]) -> e(S); +m(S = [<<"appchizi">>,<<"com">>]) -> e(S); +m(S = [<<"applinzi">>,<<"com">>]) -> e(S); +m(S = [<<"sinaapp">>,<<"com">>]) -> e(S); +m(S = [<<"vipsinaapp">>,<<"com">>]) -> e(S); +m(S = [<<"siteleaf">>,<<"net">>]) -> e(S); +m(S = [<<"bounty-full">>,<<"com">>]) -> e(S); +m(S = [<<"alpha">>,<<"bounty-full">>,<<"com">>]) -> e(S); +m(S = [<<"beta">>,<<"bounty-full">>,<<"com">>]) -> e(S); +m(S = [<<"small-web">>,<<"org">>]) -> e(S); +m(S = [<<"vp4">>,<<"me">>]) -> e(S); +m(S = [<<"streamlitapp">>,<<"com">>]) -> e(S); +m(S = [<<"try-snowplow">>,<<"com">>]) -> e(S); +m(S = [<<"srht">>,<<"site">>]) -> e(S); +m(S = [<<"stackhero-network">>,<<"com">>]) -> e(S); +m(S = [<<"musician">>,<<"io">>]) -> e(S); +m(S = [<<"novecore">>,<<"site">>]) -> e(S); +m(S = [<<"static">>,<<"land">>]) -> e(S); +m(S = [<<"dev">>,<<"static">>,<<"land">>]) -> e(S); +m(S = [<<"sites">>,<<"static">>,<<"land">>]) -> e(S); +m(S = [<<"storebase">>,<<"store">>]) -> e(S); +m(S = [<<"vps-host">>,<<"net">>]) -> e(S); +m(S = [<<"atl">>,<<"jelastic">>,<<"vps-host">>,<<"net">>]) -> e(S); +m(S = [<<"njs">>,<<"jelastic">>,<<"vps-host">>,<<"net">>]) -> e(S); +m(S = [<<"ric">>,<<"jelastic">>,<<"vps-host">>,<<"net">>]) -> e(S); +m(S = [<<"playstation-cloud">>,<<"com">>]) -> e(S); +m(S = [<<"apps">>,<<"lair">>,<<"io">>]) -> e(S); +m(S = [_,<<"stolos">>,<<"io">>]) -> e(S); +m(S = [<<"spacekit">>,<<"io">>]) -> e(S); +m(S = [<<"customer">>,<<"speedpartner">>,<<"de">>]) -> e(S); +m(S = [<<"myspreadshop">>,<<"at">>]) -> e(S); +m(S = [<<"myspreadshop">>,<<"com">>,<<"au">>]) -> e(S); +m(S = [<<"myspreadshop">>,<<"be">>]) -> e(S); +m(S = [<<"myspreadshop">>,<<"ca">>]) -> e(S); +m(S = [<<"myspreadshop">>,<<"ch">>]) -> e(S); +m(S = [<<"myspreadshop">>,<<"com">>]) -> e(S); +m(S = [<<"myspreadshop">>,<<"de">>]) -> e(S); +m(S = [<<"myspreadshop">>,<<"dk">>]) -> e(S); +m(S = [<<"myspreadshop">>,<<"es">>]) -> e(S); +m(S = [<<"myspreadshop">>,<<"fi">>]) -> e(S); +m(S = [<<"myspreadshop">>,<<"fr">>]) -> e(S); +m(S = [<<"myspreadshop">>,<<"ie">>]) -> e(S); +m(S = [<<"myspreadshop">>,<<"it">>]) -> e(S); +m(S = [<<"myspreadshop">>,<<"net">>]) -> e(S); +m(S = [<<"myspreadshop">>,<<"nl">>]) -> e(S); +m(S = [<<"myspreadshop">>,<<"no">>]) -> e(S); +m(S = [<<"myspreadshop">>,<<"pl">>]) -> e(S); +m(S = [<<"myspreadshop">>,<<"se">>]) -> e(S); +m(S = [<<"myspreadshop">>,<<"co">>,<<"uk">>]) -> e(S); +m(S = [<<"api">>,<<"stdlib">>,<<"com">>]) -> e(S); +m(S = [<<"storj">>,<<"farm">>]) -> e(S); +m(S = [<<"utwente">>,<<"io">>]) -> e(S); +m(S = [<<"soc">>,<<"srcf">>,<<"net">>]) -> e(S); +m(S = [<<"user">>,<<"srcf">>,<<"net">>]) -> e(S); +m(S = [<<"temp-dns">>,<<"com">>]) -> e(S); +m(S = [<<"supabase">>,<<"co">>]) -> e(S); +m(S = [<<"supabase">>,<<"in">>]) -> e(S); +m(S = [<<"supabase">>,<<"net">>]) -> e(S); +m(S = [<<"su">>,<<"paba">>,<<"se">>]) -> e(S); +m(S = [_,<<"s5y">>,<<"io">>]) -> e(S); +m(S = [_,<<"sensiosite">>,<<"cloud">>]) -> e(S); +m(S = [<<"syncloud">>,<<"it">>]) -> e(S); +m(S = [<<"dscloud">>,<<"biz">>]) -> e(S); +m(S = [<<"direct">>,<<"quickconnect">>,<<"cn">>]) -> e(S); +m(S = [<<"dsmynas">>,<<"com">>]) -> e(S); +m(S = [<<"familyds">>,<<"com">>]) -> e(S); +m(S = [<<"diskstation">>,<<"me">>]) -> e(S); +m(S = [<<"dscloud">>,<<"me">>]) -> e(S); +m(S = [<<"i234">>,<<"me">>]) -> e(S); +m(S = [<<"myds">>,<<"me">>]) -> e(S); +m(S = [<<"synology">>,<<"me">>]) -> e(S); +m(S = [<<"dscloud">>,<<"mobi">>]) -> e(S); +m(S = [<<"dsmynas">>,<<"net">>]) -> e(S); +m(S = [<<"familyds">>,<<"net">>]) -> e(S); +m(S = [<<"dsmynas">>,<<"org">>]) -> e(S); +m(S = [<<"familyds">>,<<"org">>]) -> e(S); +m(S = [<<"vpnplus">>,<<"to">>]) -> e(S); +m(S = [<<"direct">>,<<"quickconnect">>,<<"to">>]) -> e(S); +m(S = [<<"tabitorder">>,<<"co">>,<<"il">>]) -> e(S); +m(S = [<<"mytabit">>,<<"co">>,<<"il">>]) -> e(S); +m(S = [<<"mytabit">>,<<"com">>]) -> e(S); +m(S = [<<"taifun-dns">>,<<"de">>]) -> e(S); +m(S = [<<"beta">>,<<"tailscale">>,<<"net">>]) -> e(S); +m(S = [<<"ts">>,<<"net">>]) -> e(S); +m(S = [<<"gda">>,<<"pl">>]) -> e(S); +m(S = [<<"gdansk">>,<<"pl">>]) -> e(S); +m(S = [<<"gdynia">>,<<"pl">>]) -> e(S); +m(S = [<<"med">>,<<"pl">>]) -> e(S); +m(S = [<<"sopot">>,<<"pl">>]) -> e(S); +m(S = [<<"site">>,<<"tb-hosting">>,<<"com">>]) -> e(S); +m(S = [<<"edugit">>,<<"io">>]) -> e(S); +m(S = [<<"s3">>,<<"teckids">>,<<"org">>]) -> e(S); +m(S = [<<"telebit">>,<<"app">>]) -> e(S); +m(S = [<<"telebit">>,<<"io">>]) -> e(S); +m(S = [_,<<"telebit">>,<<"xyz">>]) -> e(S); +m(S = [_,<<"firenet">>,<<"ch">>]) -> e(S); +m(S = [_,<<"svc">>,<<"firenet">>,<<"ch">>]) -> e(S); +m(S = [<<"reservd">>,<<"com">>]) -> e(S); +m(S = [<<"thingdustdata">>,<<"com">>]) -> e(S); +m(S = [<<"cust">>,<<"dev">>,<<"thingdust">>,<<"io">>]) -> e(S); +m(S = [<<"cust">>,<<"disrec">>,<<"thingdust">>,<<"io">>]) -> e(S); +m(S = [<<"cust">>,<<"prod">>,<<"thingdust">>,<<"io">>]) -> e(S); +m(S = [<<"cust">>,<<"testing">>,<<"thingdust">>,<<"io">>]) -> e(S); +m(S = [<<"reservd">>,<<"dev">>,<<"thingdust">>,<<"io">>]) -> e(S); +m(S = [<<"reservd">>,<<"disrec">>,<<"thingdust">>,<<"io">>]) -> e(S); +m(S = [<<"reservd">>,<<"testing">>,<<"thingdust">>,<<"io">>]) -> e(S); +m(S = [<<"tickets">>,<<"io">>]) -> e(S); +m(S = [<<"arvo">>,<<"network">>]) -> e(S); +m(S = [<<"azimuth">>,<<"network">>]) -> e(S); +m(S = [<<"tlon">>,<<"network">>]) -> e(S); +m(S = [<<"torproject">>,<<"net">>]) -> e(S); +m(S = [<<"pages">>,<<"torproject">>,<<"net">>]) -> e(S); +m(S = [<<"bloxcms">>,<<"com">>]) -> e(S); +m(S = [<<"townnews-staging">>,<<"com">>]) -> e(S); +m(S = [<<"12hp">>,<<"at">>]) -> e(S); +m(S = [<<"2ix">>,<<"at">>]) -> e(S); +m(S = [<<"4lima">>,<<"at">>]) -> e(S); +m(S = [<<"lima-city">>,<<"at">>]) -> e(S); +m(S = [<<"12hp">>,<<"ch">>]) -> e(S); +m(S = [<<"2ix">>,<<"ch">>]) -> e(S); +m(S = [<<"4lima">>,<<"ch">>]) -> e(S); +m(S = [<<"lima-city">>,<<"ch">>]) -> e(S); +m(S = [<<"trafficplex">>,<<"cloud">>]) -> e(S); +m(S = [<<"de">>,<<"cool">>]) -> e(S); +m(S = [<<"12hp">>,<<"de">>]) -> e(S); +m(S = [<<"2ix">>,<<"de">>]) -> e(S); +m(S = [<<"4lima">>,<<"de">>]) -> e(S); +m(S = [<<"lima-city">>,<<"de">>]) -> e(S); +m(S = [<<"1337">>,<<"pictures">>]) -> e(S); +m(S = [<<"clan">>,<<"rip">>]) -> e(S); +m(S = [<<"lima-city">>,<<"rocks">>]) -> e(S); +m(S = [<<"webspace">>,<<"rocks">>]) -> e(S); +m(S = [<<"lima">>,<<"zone">>]) -> e(S); +m(S = [_,<<"transurl">>,<<"be">>]) -> e(S); +m(S = [_,<<"transurl">>,<<"eu">>]) -> e(S); +m(S = [_,<<"transurl">>,<<"nl">>]) -> e(S); +m(S = [<<"site">>,<<"transip">>,<<"me">>]) -> e(S); +m(S = [<<"tuxfamily">>,<<"org">>]) -> e(S); +m(S = [<<"dd-dns">>,<<"de">>]) -> e(S); +m(S = [<<"diskstation">>,<<"eu">>]) -> e(S); +m(S = [<<"diskstation">>,<<"org">>]) -> e(S); +m(S = [<<"dray-dns">>,<<"de">>]) -> e(S); +m(S = [<<"draydns">>,<<"de">>]) -> e(S); +m(S = [<<"dyn-vpn">>,<<"de">>]) -> e(S); +m(S = [<<"dynvpn">>,<<"de">>]) -> e(S); +m(S = [<<"mein-vigor">>,<<"de">>]) -> e(S); +m(S = [<<"my-vigor">>,<<"de">>]) -> e(S); +m(S = [<<"my-wan">>,<<"de">>]) -> e(S); +m(S = [<<"syno-ds">>,<<"de">>]) -> e(S); +m(S = [<<"synology-diskstation">>,<<"de">>]) -> e(S); +m(S = [<<"synology-ds">>,<<"de">>]) -> e(S); +m(S = [<<"typedream">>,<<"app">>]) -> e(S); +m(S = [<<"pro">>,<<"typeform">>,<<"com">>]) -> e(S); +m(S = [<<"uber">>,<<"space">>]) -> e(S); +m(S = [_,<<"uberspace">>,<<"de">>]) -> e(S); +m(S = [<<"hk">>,<<"com">>]) -> e(S); +m(S = [<<"hk">>,<<"org">>]) -> e(S); +m(S = [<<"ltd">>,<<"hk">>]) -> e(S); +m(S = [<<"inc">>,<<"hk">>]) -> e(S); +m(S = [<<"name">>,<<"pm">>]) -> e(S); +m(S = [<<"sch">>,<<"tf">>]) -> e(S); +m(S = [<<"biz">>,<<"wf">>]) -> e(S); +m(S = [<<"sch">>,<<"wf">>]) -> e(S); +m(S = [<<"org">>,<<"yt">>]) -> e(S); +m(S = [<<"virtualuser">>,<<"de">>]) -> e(S); +m(S = [<<"virtual-user">>,<<"de">>]) -> e(S); +m(S = [<<"upli">>,<<"io">>]) -> e(S); +m(S = [<<"urown">>,<<"cloud">>]) -> e(S); +m(S = [<<"dnsupdate">>,<<"info">>]) -> e(S); +m(S = [<<"lib">>,<<"de">>,<<"us">>]) -> e(S); +m(S = [<<"2038">>,<<"io">>]) -> e(S); +m(S = [<<"vercel">>,<<"app">>]) -> e(S); +m(S = [<<"vercel">>,<<"dev">>]) -> e(S); +m(S = [<<"now">>,<<"sh">>]) -> e(S); +m(S = [<<"router">>,<<"management">>]) -> e(S); +m(S = [<<"v-info">>,<<"info">>]) -> e(S); +m(S = [<<"voorloper">>,<<"cloud">>]) -> e(S); +m(S = [<<"neko">>,<<"am">>]) -> e(S); +m(S = [<<"nyaa">>,<<"am">>]) -> e(S); +m(S = [<<"be">>,<<"ax">>]) -> e(S); +m(S = [<<"cat">>,<<"ax">>]) -> e(S); +m(S = [<<"es">>,<<"ax">>]) -> e(S); +m(S = [<<"eu">>,<<"ax">>]) -> e(S); +m(S = [<<"gg">>,<<"ax">>]) -> e(S); +m(S = [<<"mc">>,<<"ax">>]) -> e(S); +m(S = [<<"us">>,<<"ax">>]) -> e(S); +m(S = [<<"xy">>,<<"ax">>]) -> e(S); +m(S = [<<"nl">>,<<"ci">>]) -> e(S); +m(S = [<<"xx">>,<<"gl">>]) -> e(S); +m(S = [<<"app">>,<<"gp">>]) -> e(S); +m(S = [<<"blog">>,<<"gt">>]) -> e(S); +m(S = [<<"de">>,<<"gt">>]) -> e(S); +m(S = [<<"to">>,<<"gt">>]) -> e(S); +m(S = [<<"be">>,<<"gy">>]) -> e(S); +m(S = [<<"cc">>,<<"hn">>]) -> e(S); +m(S = [<<"blog">>,<<"kg">>]) -> e(S); +m(S = [<<"io">>,<<"kg">>]) -> e(S); +m(S = [<<"jp">>,<<"kg">>]) -> e(S); +m(S = [<<"tv">>,<<"kg">>]) -> e(S); +m(S = [<<"uk">>,<<"kg">>]) -> e(S); +m(S = [<<"us">>,<<"kg">>]) -> e(S); +m(S = [<<"de">>,<<"ls">>]) -> e(S); +m(S = [<<"at">>,<<"md">>]) -> e(S); +m(S = [<<"de">>,<<"md">>]) -> e(S); +m(S = [<<"jp">>,<<"md">>]) -> e(S); +m(S = [<<"to">>,<<"md">>]) -> e(S); +m(S = [<<"indie">>,<<"porn">>]) -> e(S); +m(S = [<<"vxl">>,<<"sh">>]) -> e(S); +m(S = [<<"ch">>,<<"tc">>]) -> e(S); +m(S = [<<"me">>,<<"tc">>]) -> e(S); +m(S = [<<"we">>,<<"tc">>]) -> e(S); +m(S = [<<"nyan">>,<<"to">>]) -> e(S); +m(S = [<<"at">>,<<"vg">>]) -> e(S); +m(S = [<<"blog">>,<<"vu">>]) -> e(S); +m(S = [<<"dev">>,<<"vu">>]) -> e(S); +m(S = [<<"me">>,<<"vu">>]) -> e(S); +m(S = [<<"v">>,<<"ua">>]) -> e(S); +m(S = [_,<<"vultrobjects">>,<<"com">>]) -> e(S); +m(S = [<<"wafflecell">>,<<"com">>]) -> e(S); +m(S = [_,<<"webhare">>,<<"dev">>]) -> e(S); +m(S = [<<"reserve-online">>,<<"net">>]) -> e(S); +m(S = [<<"reserve-online">>,<<"com">>]) -> e(S); +m(S = [<<"bookonline">>,<<"app">>]) -> e(S); +m(S = [<<"hotelwithflight">>,<<"com">>]) -> e(S); +m(S = [<<"wedeploy">>,<<"io">>]) -> e(S); +m(S = [<<"wedeploy">>,<<"me">>]) -> e(S); +m(S = [<<"wedeploy">>,<<"sh">>]) -> e(S); +m(S = [<<"remotewd">>,<<"com">>]) -> e(S); +m(S = [<<"pages">>,<<"wiardweb">>,<<"com">>]) -> e(S); +m(S = [<<"wmflabs">>,<<"org">>]) -> e(S); +m(S = [<<"toolforge">>,<<"org">>]) -> e(S); +m(S = [<<"wmcloud">>,<<"org">>]) -> e(S); +m(S = [<<"panel">>,<<"gg">>]) -> e(S); +m(S = [<<"daemon">>,<<"panel">>,<<"gg">>]) -> e(S); +m(S = [<<"messwithdns">>,<<"com">>]) -> e(S); +m(S = [<<"woltlab-demo">>,<<"com">>]) -> e(S); +m(S = [<<"myforum">>,<<"community">>]) -> e(S); +m(S = [<<"community-pro">>,<<"de">>]) -> e(S); +m(S = [<<"diskussionsbereich">>,<<"de">>]) -> e(S); +m(S = [<<"community-pro">>,<<"net">>]) -> e(S); +m(S = [<<"meinforum">>,<<"net">>]) -> e(S); +m(S = [<<"affinitylottery">>,<<"org">>,<<"uk">>]) -> e(S); +m(S = [<<"raffleentry">>,<<"org">>,<<"uk">>]) -> e(S); +m(S = [<<"weeklylottery">>,<<"org">>,<<"uk">>]) -> e(S); +m(S = [<<"wpenginepowered">>,<<"com">>]) -> e(S); +m(S = [<<"js">>,<<"wpenginepowered">>,<<"com">>]) -> e(S); +m(S = [<<"wixsite">>,<<"com">>]) -> e(S); +m(S = [<<"editorx">>,<<"io">>]) -> e(S); +m(S = [<<"half">>,<<"host">>]) -> e(S); +m(S = [<<"xnbay">>,<<"com">>]) -> e(S); +m(S = [<<"u2">>,<<"xnbay">>,<<"com">>]) -> e(S); +m(S = [<<"u2-local">>,<<"xnbay">>,<<"com">>]) -> e(S); +m(S = [<<"cistron">>,<<"nl">>]) -> e(S); +m(S = [<<"demon">>,<<"nl">>]) -> e(S); +m(S = [<<"xs4all">>,<<"space">>]) -> e(S); +m(S = [<<"yandexcloud">>,<<"net">>]) -> e(S); +m(S = [<<"storage">>,<<"yandexcloud">>,<<"net">>]) -> e(S); +m(S = [<<"website">>,<<"yandexcloud">>,<<"net">>]) -> e(S); +m(S = [<<"official">>,<<"academy">>]) -> e(S); +m(S = [<<"yolasite">>,<<"com">>]) -> e(S); +m(S = [<<"ybo">>,<<"faith">>]) -> e(S); +m(S = [<<"yombo">>,<<"me">>]) -> e(S); +m(S = [<<"homelink">>,<<"one">>]) -> e(S); +m(S = [<<"ybo">>,<<"party">>]) -> e(S); +m(S = [<<"ybo">>,<<"review">>]) -> e(S); +m(S = [<<"ybo">>,<<"science">>]) -> e(S); +m(S = [<<"ybo">>,<<"trade">>]) -> e(S); +m(S = [<<"ynh">>,<<"fr">>]) -> e(S); +m(S = [<<"nohost">>,<<"me">>]) -> e(S); +m(S = [<<"noho">>,<<"st">>]) -> e(S); +m(S = [<<"za">>,<<"net">>]) -> e(S); +m(S = [<<"za">>,<<"org">>]) -> e(S); +m(S = [<<"bss">>,<<"design">>]) -> e(S); +m(S = [<<"basicserver">>,<<"io">>]) -> e(S); +m(S = [<<"virtualserver">>,<<"io">>]) -> e(S); +m(S = [<<"enterprisecloud">>,<<"nu">>]) -> e(S); +m(_) -> false. + +e([<<"www">>,<<"ck">>]) -> false; +e([<<"city">>,<<"kawasaki">>,<<"jp">>]) -> false; +e([<<"city">>,<<"kitakyushu">>,<<"jp">>]) -> false; +e([<<"city">>,<<"kobe">>,<<"jp">>]) -> false; +e([<<"city">>,<<"nagoya">>,<<"jp">>]) -> false; +e([<<"city">>,<<"sapporo">>,<<"jp">>]) -> false; +e([<<"city">>,<<"sendai">>,<<"jp">>]) -> false; +e([<<"city">>,<<"yokohama">>,<<"jp">>]) -> false; +e(_) -> true. diff --git a/gun/src/gun_public_suffix.erl.src b/gun/src/gun_public_suffix.erl.src new file mode 100644 index 0000000..b16ec35 --- /dev/null +++ b/gun/src/gun_public_suffix.erl.src @@ -0,0 +1,29 @@ +%% Copyright (c) 2020-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_public_suffix). +-compile(no_type_opt). %% Until at least OTP-23. + +-export([match/1]). + +-spec match(binary()) -> boolean(). +match(Domain) -> + Subdomains = string:split(Domain, <<".">>, all), + m(Subdomains). + +%% GENERATED_M +m(_) -> false. + +%% GENERATED_E +e(_) -> true. diff --git a/gun/src/gun_raw.erl b/gun/src/gun_raw.erl new file mode 100644 index 0000000..50786e3 --- /dev/null +++ b/gun/src/gun_raw.erl @@ -0,0 +1,97 @@ +%% Copyright (c) 2019-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_raw). + +-export([check_options/1]). +-export([name/0]). +-export([opts_name/0]). +-export([has_keepalive/0]). +-export([init/4]). +-export([handle/5]). +-export([update_flow/4]). +-export([closing/4]). +-export([close/4]). +-export([data/7]). +-export([down/1]). + +-record(raw_state, { + ref :: undefined | gun:stream_ref(), + reply_to :: pid(), + socket :: inet:socket() | ssl:sslsocket(), + transport :: module(), + flow :: integer() | infinity +}). + +check_options(Opts) -> + do_check_options(maps:to_list(Opts)). + +do_check_options([]) -> + ok; +do_check_options([{flow, Flow}|Opts]) when is_integer(Flow); Flow == infinity -> + do_check_options(Opts); +do_check_options([Opt|_]) -> + {error, {options, {raw, Opt}}}. + +name() -> raw. +opts_name() -> raw_opts. +has_keepalive() -> false. + +init(ReplyTo, Socket, Transport, Opts) -> + StreamRef = maps:get(stream_ref, Opts, undefined), + InitialFlow = maps:get(flow, Opts, infinity), + {ok, connected_data_only, #raw_state{ref=StreamRef, reply_to=ReplyTo, + socket=Socket, transport=Transport, flow=InitialFlow}}. + +handle(Data, State=#raw_state{ref=StreamRef, reply_to=ReplyTo, flow=Flow0}, + CookieStore, _, EvHandlerState) -> + %% When we take over the entire connection there is no stream reference. + ReplyTo ! {gun_data, self(), StreamRef, nofin, Data}, + Flow = case Flow0 of + infinity -> infinity; + _ -> Flow0 - 1 + end, + {[ + {state, State#raw_state{flow=Flow}}, + {active, Flow > 0} + ], CookieStore, EvHandlerState}. + +update_flow(State=#raw_state{flow=Flow0}, _ReplyTo, _StreamRef, Inc) -> + Flow = case Flow0 of + infinity -> infinity; + _ -> Flow0 + Inc + end, + [ + {state, State#raw_state{flow=Flow}}, + {active, Flow > 0} + ]. + +%% We can always close immediately. +closing(_, _, _, EvHandlerState) -> + {close, EvHandlerState}. + +close(_, _, _, EvHandlerState) -> + EvHandlerState. + +%% @todo Initiate closing on IsFin=fin. +data(#raw_state{ref=StreamRef, socket=Socket, transport=Transport}, StreamRef, + _ReplyTo, _IsFin, Data, _EvHandler, EvHandlerState) -> + case Transport:send(Socket, Data) of + ok -> {[], EvHandlerState}; + Error={error, _} -> {Error, EvHandlerState} + end. + +%% raw has no concept of streams. +down(_) -> + []. diff --git a/gun/src/gun_socks.erl b/gun/src/gun_socks.erl new file mode 100644 index 0000000..1b868a2 --- /dev/null +++ b/gun/src/gun_socks.erl @@ -0,0 +1,207 @@ +%% Copyright (c) 2019-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_socks). + +-export([check_options/1]). +-export([name/0]). +-export([opts_name/0]). +-export([has_keepalive/0]). +-export([init/4]). +-export([switch_transport/3]). +-export([handle/5]). +-export([closing/4]). +-export([close/4]). +%% @todo down + +-record(socks_state, { + ref :: undefined | gun:stream_ref(), + reply_to :: pid(), + socket :: inet:socket() | ssl:sslsocket(), + transport :: module(), + opts = #{} :: gun:socks_opts(), + %% We only support version 5 at the moment. + version :: 5, + status :: auth_method_select | auth_username_password | connect +}). + +check_options(Opts=#{host := _, port := _}) -> + do_check_options(maps:to_list(maps:without([host, port], Opts))); +%% Host and port are not optional. +check_options(#{host := _}) -> + {error, options, {socks, port}}; +check_options(#{}) -> + {error, options, {socks, host}}. + +do_check_options([]) -> + ok; +do_check_options([Opt={auth, L}|Opts]) -> + case check_auth_opt(L) of + ok -> do_check_options(Opts); + error -> {error, {options, {socks, Opt}}} + end; +%% @todo Proper protocols check. +do_check_options([{protocols, P}|Opts]) when is_list(P) -> + do_check_options(Opts); +do_check_options([{tls_handshake_timeout, infinity}|Opts]) -> + do_check_options(Opts); +do_check_options([{tls_handshake_timeout, T}|Opts]) when is_integer(T), T >= 0 -> + do_check_options(Opts); +do_check_options([{tls_opts, L}|Opts]) when is_list(L) -> + do_check_options(Opts); +do_check_options([{transport, T}|Opts]) when T =:= tcp; T =:= tls -> + do_check_options(Opts); +do_check_options([{version, 5}|Opts]) -> + do_check_options(Opts); +do_check_options([Opt|_]) -> + {error, {options, {socks, Opt}}}. + +check_auth_opt(Methods) -> + %% Methods must not appear more than once, and they + %% must be one of none or {username_password, binary(), binary()}. + Check = lists:usort([case M of + none -> ok; + {username_password, U, P} when is_binary(U), is_binary(P) -> ok + end || M <- Methods]), + case {length(Methods) =:= length(Check), lists:usort(Check)} of + {true, []} -> ok; + {true, [ok]} -> ok; + _ -> error + end. + +name() -> socks. +opts_name() -> socks_opts. +has_keepalive() -> false. + +init(ReplyTo, Socket, Transport, Opts) -> + StreamRef = maps:get(stream_ref, Opts, undefined), + 5 = Version = maps:get(version, Opts, 5), + Auth = maps:get(auth, Opts, [none]), + Methods = < <<2>>; + none -> <<0>> + end || A <- Auth>>, + case Transport:send(Socket, [<<5, (length(Auth))>>, Methods]) of + ok -> + {ok, connected_no_input, #socks_state{ref=StreamRef, reply_to=ReplyTo, + socket=Socket, transport=Transport, + opts=Opts, version=Version, status=auth_method_select}}; + Error={error, _Reason} -> + Error + end. + +switch_transport(Transport, Socket, State) -> + State#socks_state{socket=Socket, transport=Transport}. + +handle(Data, State, CookieStore, _, EvHandlerState) -> + {handle(Data, State), CookieStore, EvHandlerState}. + +%% No authentication. +handle(<<5, 0>>, State=#socks_state{version=5, status=auth_method_select}) -> + case send_socks5_connect(State) of + ok -> {state, State#socks_state{status=connect}}; + Error={error, _} -> Error + end; +%% Username/password authentication. +handle(<<5, 2>>, State=#socks_state{socket=Socket, transport=Transport, opts=#{auth := AuthMethods}, + version=5, status=auth_method_select}) -> + [{username_password, Username, Password}] = [Method || Method <- AuthMethods], + ULen = byte_size(Username), + PLen = byte_size(Password), + case Transport:send(Socket, <<1, ULen, Username/binary, PLen, Password/binary>>) of + ok -> {state, State#socks_state{status=auth_username_password}}; + Error={error, _} -> Error + end; +%% Username/password authentication successful. +handle(<<1, 0>>, State=#socks_state{version=5, status=auth_username_password}) -> + case send_socks5_connect(State) of + ok -> {state, State#socks_state{status=connect}}; + Error={error, _} -> Error + end; +%% Username/password authentication error. +handle(<<1, _>>, #socks_state{version=5, status=auth_username_password}) -> + {error, {socks5, username_password_auth_failure}}; +%% Connect reply. +handle(<<5, 0, 0, Rest0/bits>>, #socks_state{ref=StreamRef, reply_to=ReplyTo, opts=Opts, + version=5, status=connect}) -> + %% @todo What to do with BoundAddr and BoundPort? Add as metadata to origin info? + {_BoundAddr, _BoundPort} = case Rest0 of + %% @todo Seen a server with <<1, 0:48>>. + <<1, A, B, C, D, Port:16>> -> + {{A, B, C, D}, Port}; + <<3, Len, Host:Len/binary, Port:16>> -> + %% We convert to list to get an inet:hostname(). + {unicode:characters_to_list(Host), Port}; + <<4, A:16, B:16, C:16, D:16, E:16, F:16, G:16, H:16, Port:16>> -> + {{A, B, C, D, E, F, G, H}, Port} + end, + %% @todo Maybe an event indicating success. + #{host := NewHost, port := NewPort} = Opts, + %% There is no origin scheme when not using HTTP but we act as if + %% there is and simply correct the value in the info functions. + case Opts of + #{transport := tls} -> + HandshakeEvent0 = #{ + reply_to => ReplyTo, + tls_opts => maps:get(tls_opts, Opts, []), + timeout => maps:get(tls_handshake_timeout, Opts, infinity) + }, + HandshakeEvent = case StreamRef of + undefined -> HandshakeEvent0; + _ -> HandshakeEvent0#{stream_ref => StreamRef} + end, + [{origin, <<"https">>, NewHost, NewPort, socks5}, + {tls_handshake, HandshakeEvent, maps:get(protocols, Opts, [http2, http]), ReplyTo}]; + _ -> + [NewProtocol0] = maps:get(protocols, Opts, [http]), + NewProtocol = gun_protocols:add_stream_ref(NewProtocol0, StreamRef), + Protocol = gun_protocols:handler(NewProtocol), + ReplyTo ! {gun_tunnel_up, self(), StreamRef, Protocol:name()}, + [{origin, <<"http">>, NewHost, NewPort, socks5}, + {switch_protocol, NewProtocol, ReplyTo}] + end; +handle(<<5, Error, _/bits>>, #socks_state{version=5, status=connect}) -> + Reason = case Error of + 1 -> general_socks_server_failure; + 2 -> connection_not_allowed_by_ruleset; + 3 -> network_unreachable; + 4 -> host_unreachable; + 5 -> connection_refused; + 6 -> ttl_expired; + 7 -> command_not_supported; + 8 -> address_type_not_supported; + _ -> {unknown_error, Error} + end, + {error, {socks5, Reason}}. + +send_socks5_connect(#socks_state{socket=Socket, transport=Transport, opts=Opts}) -> + ATypeAndDestAddr = case maps:get(host, Opts) of + {A, B, C, D} -> <<1, A, B, C, D>>; + {A, B, C, D, E, F, G, H} -> <<4, A:16, B:16, C:16, D:16, E:16, F:16, G:16, H:16>>; + Host when is_atom(Host) -> + DestAddr0 = atom_to_binary(Host, utf8), + <<3, (byte_size(DestAddr0)), DestAddr0/binary>>; + Host -> + DestAddr0 = unicode:characters_to_binary(Host, utf8), + <<3, (byte_size(DestAddr0)), DestAddr0/binary>> + end, + DestPort = maps:get(port, Opts), + Transport:send(Socket, <<5, 1, 0, ATypeAndDestAddr/binary, DestPort:16>>). + +%% We can always close immediately. +closing(_, _, _, EvHandlerState) -> + {close, EvHandlerState}. + +close(_, _, _, EvHandlerState) -> + EvHandlerState. diff --git a/gun/src/gun_sse_h.erl b/gun/src/gun_sse_h.erl new file mode 100644 index 0000000..03d190b --- /dev/null +++ b/gun/src/gun_sse_h.erl @@ -0,0 +1,63 @@ +%% Copyright (c) 2017-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_sse_h). +-behavior(gun_content_handler). + +-export([init/5]). +-export([handle/3]). + +-record(state, { + reply_to :: pid(), + stream_ref :: reference(), + sse_state :: cow_sse:state() +}). + +%% @todo In the future we want to allow different media types. + +-spec init(pid(), reference(), _, cow_http:headers(), _) + -> {ok, #state{}} | disable. +init(ReplyTo, StreamRef, _, Headers, _) -> + case lists:keyfind(<<"content-type">>, 1, Headers) of + {_, ContentType} -> + case cow_http_hd:parse_content_type(ContentType) of + {<<"text">>, <<"event-stream">>, _Ignored} -> + {ok, #state{reply_to=ReplyTo, stream_ref=StreamRef, + sse_state=cow_sse:init()}}; + _ -> + disable + end; + _ -> + disable + end. + +-spec handle(_, binary(), State) -> {done, non_neg_integer(), State} when State::#state{}. +handle(IsFin, Data, State) -> + handle(IsFin, Data, State, 0). + +handle(IsFin, Data, State=#state{reply_to=ReplyTo, stream_ref=StreamRef, sse_state=SSE0}, Flow) -> + case cow_sse:parse(Data, SSE0) of + {event, Event, SSE} -> + ReplyTo ! {gun_sse, self(), StreamRef, Event}, + handle(IsFin, <<>>, State#state{sse_state=SSE}, Flow + 1); + {more, SSE} -> + Inc = case IsFin of + fin -> + ReplyTo ! {gun_sse, self(), StreamRef, fin}, + 1; + _ -> + 0 + end, + {done, Flow + Inc, State#state{sse_state=SSE}} + end. diff --git a/gun/src/gun_sup.erl b/gun/src/gun_sup.erl new file mode 100644 index 0000000..b48fba4 --- /dev/null +++ b/gun/src/gun_sup.erl @@ -0,0 +1,37 @@ +%% Copyright (c) 2013-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_sup). +-behaviour(supervisor). + +%% API. +-export([start_link/0]). + +%% supervisor. +-export([init/1]). + +%% API. + +-spec start_link() -> {ok, pid()}. +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +%% supervisor. + +init([]) -> + Procs = [ + #{id => gun_conns_sup, start => {gun_conns_sup, start_link, []}, type => supervisor}, + #{id => gun_pools_sup, start => {gun_pools_sup, start_link, []}, type => supervisor} + ], + {ok, {#{}, Procs}}. diff --git a/gun/src/gun_tcp.erl b/gun/src/gun_tcp.erl new file mode 100644 index 0000000..759b054 --- /dev/null +++ b/gun/src/gun_tcp.erl @@ -0,0 +1,112 @@ +%% Copyright (c) 2011-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_tcp). + +-export([name/0]). +-export([messages/0]). +-export([domain_lookup/4]). +-export([connect/2]). +-export([send/2]). +-export([setopts/2]). +-export([sockname/1]). +-export([close/1]). + +-type lookup_info() :: #{ + ip_addresses := [inet:ip_address()], + port := inet:port_number(), + tcp_module := module(), + tcp_opts := [gen_tcp:connect_option()] +}. +-export_type([lookup_info/0]). + +name() -> tcp. + +messages() -> {tcp, tcp_closed, tcp_error}. + +%% The functions domain_lookup/4 and connect/2 are very similar +%% to gen_tcp:connect/4 except the logic is split in order to +%% be able to trigger events between the domain lookup step +%% and the actual connect step. + +-spec domain_lookup(inet:ip_address() | inet:hostname(), + inet:port_number(), [gen_tcp:connect_option()], timeout()) + -> {ok, lookup_info()} | {error, atom()}. +domain_lookup(Address, Port0, Opts0, Timeout) -> + {Mod, Opts} = inet:tcp_module(Opts0, Address), + Timer = inet:start_timer(Timeout), + try Mod:getaddrs(Address, Timer) of + {ok, IPs} -> + case Mod:getserv(Port0) of + {ok, Port} -> + {ok, #{ + ip_addresses => IPs, + port => Port, + tcp_module => Mod, + tcp_opts => Opts ++ [binary, {active, false}, {packet, raw}] + }}; + Error -> + maybe_exit(Error) + end; + Error -> + maybe_exit(Error) + after + _ = inet:stop_timer(Timer) + end. + +-spec connect(lookup_info(), timeout()) + -> {ok, inet:socket()} | {error, atom()}. +connect(#{ip_addresses := IPs, port := Port, tcp_module := Mod, tcp_opts := Opts}, Timeout) -> + Timer = inet:start_timer(Timeout), + Res = try + try_connect(IPs, Port, Opts, Timer, Mod, {error, einval}) + after + _ = inet:stop_timer(Timer) + end, + case Res of + {ok, S} -> {ok, S}; + Error -> maybe_exit(Error) + end. + +try_connect([IP|IPs], Port, Opts, Timer, Mod, _) -> + Timeout = inet:timeout(Timer), + case Mod:connect(IP, Port, Opts, Timeout) of + {ok, S} -> {ok, S}; + {error, einval} -> {error, einval}; + {error, timeout} -> {error, timeout}; + Error -> try_connect(IPs, Port, Opts, Timer, Mod, Error) + end; +try_connect([], _, _, _, _, Error) -> + Error. + +maybe_exit({error, einval}) -> exit(badarg); +maybe_exit({error, eaddrnotavail}) -> exit(badarg); +maybe_exit(Error) -> Error. + +-spec send(inet:socket(), iodata()) -> ok | {error, atom()}. +send(Socket, Packet) -> + gen_tcp:send(Socket, Packet). + +-spec setopts(inet:socket(), list()) -> ok | {error, atom()}. +setopts(Socket, Opts) -> + inet:setopts(Socket, Opts). + +-spec sockname(inet:socket()) + -> {ok, {inet:ip_address(), inet:port_number()}} | {error, atom()}. +sockname(Socket) -> + inet:sockname(Socket). + +-spec close(inet:socket()) -> ok. +close(Socket) -> + gen_tcp:close(Socket). diff --git a/gun/src/gun_tcp_proxy.erl b/gun/src/gun_tcp_proxy.erl new file mode 100644 index 0000000..02f02b0 --- /dev/null +++ b/gun/src/gun_tcp_proxy.erl @@ -0,0 +1,70 @@ +%% Copyright (c) 2020-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_tcp_proxy). + +-export([name/0]). +-export([messages/0]). +-export([connect/3]). +-export([connect/4]). +-export([send/2]). +-export([setopts/2]). +-export([sockname/1]). +-export([close/1]). + +-type socket() :: #{ + %% The pid of the Gun connection. + gun_pid := pid(), + + %% The pid of the process that gets replies for this tunnel. + reply_to := pid(), + + %% The full stream reference for this tunnel. + stream_ref := gun:stream_ref() +}. + +name() -> tcp_proxy. + +messages() -> {tcp_proxy, tcp_proxy_closed, tcp_proxy_error}. + +-spec connect(_, _, _) -> no_return(). +connect(_, _, _) -> + error(not_implemented). + +-spec connect(_, _, _, _) -> no_return(). +connect(_, _, _, _) -> + error(not_implemented). + +-spec send(socket(), iodata()) -> ok. +send(#{gun_pid := GunPid, reply_to := ReplyTo, stream_ref := StreamRef, + handle_continue_stream_ref := ContinueStreamRef}, Data) -> + GunPid ! {handle_continue, ContinueStreamRef, {data, ReplyTo, StreamRef, nofin, Data}}, + ok; +send(#{reply_to := ReplyTo, stream_ref := StreamRef}, Data) -> + gen_statem:cast(self(), {data, ReplyTo, StreamRef, nofin, Data}). + +-spec setopts(_, _) -> no_return(). +setopts(#{handle_continue_stream_ref := _}, _) -> + %% We send messages automatically regardless of active mode. + ok; +setopts(_, _) -> + error(not_implemented). + +-spec sockname(_) -> no_return(). +sockname(_) -> + error(not_implemented). + +-spec close(socket()) -> ok. +close(_) -> + ok. diff --git a/gun/src/gun_tls.erl b/gun/src/gun_tls.erl new file mode 100644 index 0000000..408e3e5 --- /dev/null +++ b/gun/src/gun_tls.erl @@ -0,0 +1,49 @@ +%% Copyright (c) 2011-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_tls). + +-export([name/0]). +-export([messages/0]). +-export([connect/3]). +-export([send/2]). +-export([setopts/2]). +-export([sockname/1]). +-export([close/1]). + +name() -> tls. + +messages() -> {ssl, ssl_closed, ssl_error}. + +-spec connect(inet:socket(), any(), timeout()) + -> {ok, ssl:sslsocket()} | {error, atom()}. +connect(Socket, Opts, Timeout) -> + ssl:connect(Socket, Opts, Timeout). + +-spec send(ssl:sslsocket(), iodata()) -> ok | {error, atom()}. +send(Socket, Packet) -> + ssl:send(Socket, Packet). + +-spec setopts(ssl:sslsocket(), list()) -> ok | {error, atom()}. +setopts(Socket, Opts) -> + ssl:setopts(Socket, Opts). + +-spec sockname(ssl:sslsocket()) + -> {ok, {inet:ip_address(), inet:port_number()}} | {error, atom()}. +sockname(Socket) -> + ssl:sockname(Socket). + +-spec close(ssl:sslsocket()) -> ok. +close(Socket) -> + ssl:close(Socket). diff --git a/gun/src/gun_tls_proxy.erl b/gun/src/gun_tls_proxy.erl new file mode 100644 index 0000000..9dc67f6 --- /dev/null +++ b/gun/src/gun_tls_proxy.erl @@ -0,0 +1,492 @@ +%% Copyright (c) 2019-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +%% Intermediary process for proxying TLS connections. This process +%% is started by Gun when CONNECT request is issued and stays alive +%% while the proxy connection exists. +%% +%% Data comes in and out of this process, which is responsible for +%% passing incoming/outgoing data through the fake ssl socket process +%% it created to perform the decoding/encoding operations. +%% +%% Normal scenario: +%% Gun process -> TLS socket +%% +%% One proxy socket scenario: +%% Gun process -> gun_tls_proxy (proxied socket) -> TLS socket +%% +%% N proxy socket scenario: +%% Gun process -> gun_tls_proxy -> ... -> gun_tls_proxy -> TLS socket +%% +%% The difficult part is the connection. Because ssl:connect/4 does +%% not return until the connection is setup, and we need to send and +%% receive data for the TLS handshake, we need a temporary process +%% to call this function, and communicate with it. Once the connection +%% is setup the temporary process is gone and things go back to normal. +%% The send operations also require a temporary process in order to avoid +%% blocking because the same gun_tls_proxy process must send twice (first +%% to the fake ssl socket and then to the outgoing socket). + +-module(gun_tls_proxy). +-behaviour(gen_statem). + +%% Gun-specific interface. +-export([start_link/7]). + +%% gun_tls_proxy_cb interface. +-export([cb_controlling_process/2]). +-export([cb_send/2]). +-export([cb_setopts/2]). + +%% Gun transport. +-export([name/0]). +-export([messages/0]). +-export([connect/3]). +-export([connect/4]). +-export([send/2]). +-export([setopts/2]). +-export([sockname/1]). +-export([close/1]). + +%% Internals. +-export([callback_mode/0]). +-export([init/1]). +-export([connect_proc/5]). +-export([not_connected/3]). +-export([connected/3]). + +-record(state, { + %% The pid of the owner process. This is where we send active messages. + owner_pid :: pid(), + owner_active = false :: false | once | true | pos_integer(), + owner_buffer = <<>> :: binary(), + + %% The host/port the fake ssl socket thinks it's connected to. + host :: inet:ip_address() | inet:hostname(), + port :: inet:port_number(), + + %% The fake ssl socket we are using in the proxy. + proxy_socket :: any(), + proxy_pid :: pid(), + proxy_active = false :: false | once | true | pos_integer(), + proxy_buffer = <<>> :: binary(), + + %% The socket or proxy process we are sending to. + out_socket :: any(), + out_transport :: module(), + out_messages :: {atom(), atom(), atom()}, %% @todo Missing passive. + + %% Extra information to be sent to the owner when the handshake completes. + extra :: any() +}). + +%-define(DEBUG_PROXY,1). +-ifdef(DEBUG_PROXY). +-define(DEBUG_LOG(Format, Args), + io:format(user, "(~p) ~p:~p/~p:" ++ Format ++ "~n", + [self(), ?MODULE, ?FUNCTION_NAME, ?FUNCTION_ARITY] ++ Args)). +-else. +-define(DEBUG_LOG(Format, Args), _ = Format, _ = Args, ok). +-endif. + +%% Gun-specific interface. + +start_link(Host, Port, Opts, Timeout, OutSocket, OutTransport, Extra) -> + ?DEBUG_LOG("host ~0p port ~0p opts ~0p timeout ~0p out_socket ~0p out_transport ~0p", + [Host, Port, Opts, Timeout, OutSocket, OutTransport]), + case gen_statem:start_link(?MODULE, + {self(), Host, Port, Opts, Timeout, OutSocket, OutTransport, Extra}, + []) of + {ok, Pid} when is_port(OutSocket) -> + ok = gen_tcp:controlling_process(OutSocket, Pid), + {ok, Pid}; + {ok, Pid} when is_map(OutSocket) -> + {ok, Pid}; + {ok, Pid} when not is_pid(OutSocket) -> + ok = ssl:controlling_process(OutSocket, Pid), + {ok, Pid}; + Other -> + Other + end. + +%% gun_tls_proxy_cb interface. + +cb_controlling_process(Pid, ControllingPid) -> + ?DEBUG_LOG("pid ~0p controlling_pid ~0p", [Pid, ControllingPid]), + gen_statem:cast(Pid, {?FUNCTION_NAME, ControllingPid}). + +cb_send(Pid, Data) -> + ?DEBUG_LOG("pid ~0p data ~0p", [Pid, Data]), + try + gen_statem:call(Pid, {?FUNCTION_NAME, Data}) + catch + exit:{noproc, _} -> + {error, closed} + end. + +cb_setopts(Pid, Opts) -> + ?DEBUG_LOG("pid ~0p opts ~0p", [Pid, Opts]), + try + gen_statem:call(Pid, {?FUNCTION_NAME, Opts}) + catch + exit:{noproc, _} -> + {error, einval} + end. + +%% Transport. + +name() -> tls_proxy. + +messages() -> {tls_proxy, tls_proxy_closed, tls_proxy_error}. + +-spec connect(_, _, _) -> no_return(). +connect(_, _, _) -> + error(not_implemented). + +-spec connect(_, _, _, _) -> no_return(). +connect(_, _, _, _) -> + error(not_implemented). + +-spec send(pid(), iodata()) -> ok | {error, atom()}. +send(Pid, Data) -> + ?DEBUG_LOG("pid ~0p data ~0p", [Pid, Data]), + gen_statem:call(Pid, {?FUNCTION_NAME, Data}). + +-spec setopts(pid(), list()) -> ok. +setopts(Pid, Opts) -> + ?DEBUG_LOG("pid ~0p opts ~0p", [Pid, Opts]), + gen_statem:cast(Pid, {?FUNCTION_NAME, Opts}). + +-spec sockname(pid()) + -> {ok, {inet:ip_address(), inet:port_number()}} | {error, atom()}. +sockname(Pid) -> + ?DEBUG_LOG("pid ~0p", [Pid]), + gen_statem:call(Pid, ?FUNCTION_NAME). + +-spec close(pid()) -> ok. +close(Pid) -> + ?DEBUG_LOG("pid ~0p", [Pid]), + try + gen_statem:call(Pid, ?FUNCTION_NAME) + catch + %% May happen for example when the handshake fails. + exit:{noproc, _} -> + ok + end. + +%% gen_statem. + +callback_mode() -> state_functions. + +init({OwnerPid, Host, Port, Opts, Timeout, OutSocket, OutTransport, Extra}) -> + if + is_pid(OutSocket) -> + gen_statem:cast(OutSocket, {set_owner, self()}); + true -> + ok + end, + Messages = case OutTransport of + gen_tcp -> {tcp, tcp_closed, tcp_error}; + ssl -> {ssl, ssl_closed, ssl_error}; + _ -> OutTransport:messages() + end, + ProxyPid = spawn_link(?MODULE, connect_proc, [self(), Host, Port, Opts, Timeout]), + ?DEBUG_LOG("owner_pid ~0p host ~0p port ~0p opts ~0p timeout ~0p" + " out_socket ~0p out_transport ~0p proxy_pid ~0p", + [OwnerPid, Host, Port, Opts, Timeout, OutSocket, OutTransport, ProxyPid]), + {ok, not_connected, #state{owner_pid=OwnerPid, host=Host, port=Port, proxy_pid=ProxyPid, + out_socket=OutSocket, out_transport=OutTransport, out_messages=Messages, + extra=Extra}}. + +connect_proc(ProxyPid, Host, Port, Opts, Timeout) -> + ?DEBUG_LOG("proxy_pid ~0p host ~0p port ~0p opts ~0p timeout ~0p", + [ProxyPid, Host, Port, Opts, Timeout]), + _ = case ssl:connect(Host, Port, [ + {active, false}, binary, + {cb_info, {gun_tls_proxy_cb, tls_proxy, tls_proxy_closed, tls_proxy_error}}, + {?MODULE, ProxyPid} + |Opts], Timeout) of + {ok, Socket} -> + ?DEBUG_LOG("socket ~0p", [Socket]), + ok = ssl:controlling_process(Socket, ProxyPid), + gen_statem:cast(ProxyPid, {?FUNCTION_NAME, {ok, Socket}}); + Error -> + ?DEBUG_LOG("error ~0p", [Error]), + gen_statem:cast(ProxyPid, {?FUNCTION_NAME, Error}) + end, + ok. + +%% Postpone events that require the proxy socket to be up. +not_connected({call, _}, Msg={send, _}, State) -> + ?DEBUG_LOG("postpone ~0p state ~0p", [Msg, State]), + {keep_state_and_data, postpone}; +not_connected(cast, Msg={setopts, _}, State) -> + ?DEBUG_LOG("postpone ~0p state ~0p", [Msg, State]), + {keep_state_and_data, postpone}; +not_connected(cast, Msg={connect_proc, {ok, Socket}}, State=#state{owner_pid=OwnerPid, extra=Extra}) -> + ?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]), + OwnerPid ! {?MODULE, self(), {ok, ssl:negotiated_protocol(Socket)}, Extra}, + %% We need to spawn this call before OTP-21.2 because it triggers + %% a cb_setopts call that blocks us. Might be OK to just leave it + %% like this once we support 21.2+ only. + spawn(fun() -> ok = ssl:setopts(Socket, [{active, true}]) end), + {next_state, connected, State#state{proxy_socket=Socket}}; +not_connected(cast, Msg={connect_proc, Error}, State=#state{owner_pid=OwnerPid, extra=Extra}) -> + ?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]), + OwnerPid ! {?MODULE, self(), Error, Extra}, + %% We unlink from the owner process to avoid taking it down with us. + unlink(OwnerPid), + {stop, {shutdown, Error}, State}; +not_connected(Type, Event, State) -> + handle_common(Type, Event, State). + +%% Send data through the proxy socket. +connected({call, From}, Msg={send, Data}, State=#state{proxy_socket=Socket}) -> + ?DEBUG_LOG("msg ~0p from ~0p state ~0p", [Msg, From, State]), + Self = self(), + SpawnedPid = spawn(fun() -> + gen_statem:cast(Self, {send_result, From, ssl:send(Socket, Data)}) + end), + ?DEBUG_LOG("spawned ~0p", [SpawnedPid]), + keep_state_and_data; +%% Messages from the proxy socket. +%% +%% When the out_socket is a #{stream_ref := _} map we know that processing +%% of the data isn't yet complete. We wrap the message in a handle_continue +%% tuple and provide the StreamRef for further processing. +connected(info, Msg={ssl, Socket, Data}, State=#state{owner_pid=OwnerPid, proxy_socket=Socket, + out_socket=#{handle_continue_stream_ref := StreamRef}}) -> + ?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]), + OwnerPid ! {handle_continue, StreamRef, {tls_proxy, self(), Data}}, + keep_state_and_data; +connected(info, Msg={ssl_closed, Socket}, State=#state{owner_pid=OwnerPid, proxy_socket=Socket, + out_socket=#{handle_continue_stream_ref := StreamRef}}) -> + ?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]), + OwnerPid ! {handle_continue, StreamRef, {tls_proxy_closed, self()}}, + keep_state_and_data; +connected(info, Msg={ssl_error, Socket, Reason}, State=#state{owner_pid=OwnerPid, proxy_socket=Socket, + out_socket=#{handle_continue_stream_ref := StreamRef}}) -> + ?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]), + OwnerPid ! {handle_continue, StreamRef, {tls_proxy_error, self(), Reason}}, + keep_state_and_data; +%% When the out_socket is anything else then the data is sent like normal +%% socket data. It does not need to be handled specially. +connected(info, Msg={ssl, Socket, Data}, State=#state{owner_pid=OwnerPid, proxy_socket=Socket}) -> + ?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]), + OwnerPid ! {tls_proxy, self(), Data}, + keep_state_and_data; +connected(info, Msg={ssl_closed, Socket}, State=#state{owner_pid=OwnerPid, proxy_socket=Socket}) -> + ?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]), + OwnerPid ! {tls_proxy_closed, self()}, + keep_state_and_data; +connected(info, Msg={ssl_error, Socket, Reason}, State=#state{owner_pid=OwnerPid, proxy_socket=Socket}) -> + ?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]), + OwnerPid ! {tls_proxy_error, self(), Reason}, + keep_state_and_data; +connected(Type, Event, State) -> + handle_common(Type, Event, State). + +handle_common({call, From}, Msg={cb_send, Data}, State=#state{ + out_socket=OutSocket, out_transport=OutTransport}) -> + ?DEBUG_LOG("msg ~0p from ~0p state ~0p", [Msg, From, State]), + Self = self(), + SpawnedPid = spawn(fun() -> + gen_statem:cast(Self, {send_result, From, OutTransport:send(OutSocket, Data)}) + end), + ?DEBUG_LOG("spawned ~0p", [SpawnedPid]), + keep_state_and_data; +handle_common({call, From}, Msg={cb_setopts, Opts}, State=#state{ + out_socket=OutSocket, out_transport=OutTransport0}) -> + ?DEBUG_LOG("msg ~0p from ~0p state ~0p", [Msg, From, State]), + OutTransport = case OutTransport0 of + gen_tcp -> inet; + _ -> OutTransport0 + end, + {keep_state, proxy_setopts(Opts, State), + {reply, From, OutTransport:setopts(OutSocket, [{active, true}])}}; +handle_common({call, From}, Msg=sockname, State=#state{ + out_socket=OutSocket, out_transport=OutTransport}) -> + ?DEBUG_LOG("msg ~0p from ~0p state ~0p", [Msg, From, State]), + {keep_state, State, + {reply, From, OutTransport:sockname(OutSocket)}}; +handle_common({call, From}, Msg=close, State) -> + ?DEBUG_LOG("msg ~0p from ~0p state ~0p", [Msg, From, State]), + {stop_and_reply, {shutdown, close}, {reply, From, ok}}; +handle_common(cast, Msg={set_owner, OwnerPid}, State) -> + ?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]), + {keep_state, State#state{owner_pid=OwnerPid}}; +handle_common(cast, Msg={cb_controlling_process, ProxyPid}, State) -> + ?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]), + {keep_state, State#state{proxy_pid=ProxyPid}}; +handle_common(cast, Msg={setopts, Opts}, State) -> + ?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]), + {keep_state, owner_setopts(Opts, State)}; +handle_common(cast, Msg={send_result, From, Result}, State) -> + ?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]), + gen_statem:reply(From, Result), + keep_state_and_data; +%% Messages from the real socket. +%% @todo Make _Socket and __Socket match again. +handle_common(info, Msg={OK, _Socket, Data}, State=#state{proxy_pid=ProxyPid, + out_socket=__Socket, out_messages={OK, _, _}}) -> + ?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]), + ProxyPid ! {tls_proxy, self(), Data}, + keep_state_and_data; +handle_common(info, Msg={Closed, Socket}, State=#state{proxy_pid=ProxyPid, + out_socket=Socket, out_messages={_, Closed, _}}) -> + ?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]), + ProxyPid ! {tls_proxy_closed, self()}, + keep_state_and_data; +handle_common(info, Msg={Error, Socket, Reason}, State=#state{proxy_pid=ProxyPid, + out_socket=Socket, out_messages={_, _, Error}}) -> + ?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]), + ProxyPid ! {tls_proxy_error, self(), Reason}, + keep_state_and_data; +%% Other messages. +handle_common(Type, Msg, State) -> + ?DEBUG_LOG("IGNORED type ~0p msg ~0p state ~0p", [Type, Msg, State]), + case Type of + {call, From} -> + {keep_state, State, {reply, From, {error, bad_call}}}; + _ -> + keep_state_and_data + end. + +%% Internal. + +owner_setopts(Opts, State0) -> + case [A || {active, A} <- Opts] of + [] -> State0; + [false] -> State0#state{owner_active=false}; +% [0] -> OwnerPid ! {tls_proxy_passive, self()}, State0#state{owner_active=false}; + [Active] -> owner_active(State0#state{owner_active=Active}) + end. + +owner_active(State=#state{owner_buffer= <<>>}) -> + State; +owner_active(State=#state{owner_active=false}) -> + State; +owner_active(State=#state{owner_pid=OwnerPid, owner_active=Active0, owner_buffer=Buffer}) -> + OwnerPid ! {tls_proxy, self(), Buffer}, + Active = case Active0 of + true -> true; + once -> false%; +% 1 -> OwnerPid ! {tls_proxy_passive, self()}, false; +% N -> N - 1 + end, + State#state{owner_active=Active, owner_buffer= <<>>}. + +proxy_setopts(Opts, State0=#state{proxy_socket=ProxySocket, proxy_pid=ProxyPid}) -> + case [A || {active, A} <- Opts] of + [] -> State0; + [false] -> State0#state{proxy_active=false}; + [0] -> ProxyPid ! {tls_proxy_passive, ProxySocket}, State0#state{proxy_active=false}; + [Active] -> proxy_active(State0#state{proxy_active=Active}) + end. + +proxy_active(State=#state{proxy_buffer= <<>>}) -> + State; +proxy_active(State=#state{proxy_active=false}) -> + State; +proxy_active(State=#state{proxy_pid=ProxyPid, proxy_active=Active0, proxy_buffer=Buffer}) -> + ProxyPid ! {tls_proxy, self(), Buffer}, + Active = case Active0 of + true -> true; + once -> false; + %% Note that tcp_passive is hardcoded in ssl until OTP-21.3.1. + 1 -> ProxyPid ! {tcp_passive, self()}, false; + N -> N - 1 + end, + State#state{proxy_active=Active, proxy_buffer= <<>>}. + +-ifdef(DISABLED_TEST). +proxy_test_() -> + {timeout, 15000, [ + {"TCP proxy", fun proxy_tcp1_t/0}, + {"TLS proxy", fun proxy_ssl1_t/0}, + {"Double TLS proxy", fun proxy_ssl2_t/0} + ]}. + +proxy_tcp1_t() -> + ssl:start(), + {ok, Socket} = gen_tcp:connect("google.com", 443, [binary, {active, false}]), + {ok, ProxyPid1} = start_link("google.com", 443, [], 5000, Socket, gen_tcp, #{}), + send(ProxyPid1, <<"GET / HTTP/1.1\r\nHost: google.com\r\n\r\n">>), + receive {tls_proxy, ProxyPid1, <<"HTTP/1.1 ", _/bits>>} -> ok end. + +proxy_ssl1_t() -> + ssl:start(), + _ = (catch ct_helper:make_certs_in_ets()), + {ok, _, Port} = do_proxy_start("google.com", 443), + {ok, Socket} = ssl:connect("localhost", Port, [binary, {active, false}]), + {ok, ProxyPid1} = start_link("google.com", 443, [], 5000, Socket, ssl, #{}), + send(ProxyPid1, <<"GET / HTTP/1.1\r\nHost: google.com\r\n\r\n">>), + receive {tls_proxy, ProxyPid1, <<"HTTP/1.1 ", _/bits>>} -> ok end. + +proxy_ssl2_t() -> + ssl:start(), + _ = (catch ct_helper:make_certs_in_ets()), + {ok, _, Port1} = do_proxy_start("google.com", 443), + {ok, _, Port2} = do_proxy_start("localhost", Port1), + {ok, Socket} = ssl:connect("localhost", Port2, [binary, {active, false}]), + {ok, ProxyPid1} = start_link("localhost", Port1, [], 5000, Socket, ssl, #{}), + {ok, ProxyPid2} = start_link("google.com", 443, [], 5000, ProxyPid1, ?MODULE, #{}), + send(ProxyPid2, <<"GET / HTTP/1.1\r\nHost: google.com\r\n\r\n">>), + receive {tls_proxy, ProxyPid2, <<"HTTP/1.1 ", _/bits>>} -> ok end. + +do_proxy_start(Host, Port) -> + Self = self(), + Pid = spawn_link(fun() -> do_proxy_init(Self, Host, Port) end), + ListenPort = receive_from(Pid), + {ok, Pid, ListenPort}. + +do_proxy_init(Parent, Host, Port) -> + Opts = ct_helper:get_certs_from_ets(), + {ok, ListenSocket} = ssl:listen(0, [binary, {active, false}|Opts]), + {ok, {_, ListenPort}} = ssl:sockname(ListenSocket), + Parent ! {self(), ListenPort}, + {ok, ClientSocket0} = ssl:transport_accept(ListenSocket, 10000), + {ok, ClientSocket} = ssl:handshake(ClientSocket0, 10000), + {ok, OriginSocket} = gen_tcp:connect( + Host, Port, + [binary, {active, false}]), + ssl:setopts(ClientSocket, [{active, true}]), + inet:setopts(OriginSocket, [{active, true}]), + do_proxy_loop(ClientSocket, OriginSocket). + +do_proxy_loop(ClientSocket, OriginSocket) -> + receive + {ssl, ClientSocket, Data} -> + ok = gen_tcp:send(OriginSocket, Data), + do_proxy_loop(ClientSocket, OriginSocket); + {tcp, OriginSocket, Data} -> + ok = ssl:send(ClientSocket, Data), + do_proxy_loop(ClientSocket, OriginSocket); + {tcp_closed, _} -> + ok; + Msg -> + error(Msg) + end. + +receive_from(Pid) -> + receive_from(Pid, 5000). + +receive_from(Pid, Timeout) -> + receive + {Pid, Msg} -> + Msg + after Timeout -> + error(timeout) + end. +-endif. diff --git a/gun/src/gun_tls_proxy_cb.erl b/gun/src/gun_tls_proxy_cb.erl new file mode 100644 index 0000000..059f963 --- /dev/null +++ b/gun/src/gun_tls_proxy_cb.erl @@ -0,0 +1,41 @@ +%% Copyright (c) 2019-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +%% Transport callback for ssl. + +-module(gun_tls_proxy_cb). + +-export([connect/4]). +-export([controlling_process/2]). +-export([send/2]). +-export([setopts/2]). +-export([close/1]). + +%% The connect/4 function is called by the process +%% that calls ssl:connect/2,3,4. +connect(_Address, _Port, Opts, _Timeout) -> + {_, GunPid} = lists:keyfind(gun_tls_proxy, 1, Opts), + {ok, GunPid}. + +controlling_process(Socket, ControllingPid) -> + gun_tls_proxy:cb_controlling_process(Socket, ControllingPid). + +send(Socket, Data) -> + gun_tls_proxy:cb_send(Socket, Data). + +setopts(Socket, Opts) -> + gun_tls_proxy:cb_setopts(Socket, Opts). + +close(_) -> + ok. diff --git a/gun/src/gun_tls_proxy_http2_connect.erl b/gun/src/gun_tls_proxy_http2_connect.erl new file mode 100644 index 0000000..653064f --- /dev/null +++ b/gun/src/gun_tls_proxy_http2_connect.erl @@ -0,0 +1,69 @@ +%% Copyright (c) 2020-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_tls_proxy_http2_connect). + +-export([name/0]). +-export([messages/0]). +-export([connect/3]). +-export([connect/4]). +-export([send/2]). +-export([setopts/2]). +-export([sockname/1]). +-export([close/1]). + +-type socket() :: #{ + %% The pid of the Gun connection. + gun_pid := pid(), + + %% The pid of the process that gets replies for this tunnel. + reply_to := pid(), + + %% The full stream reference for this tunnel. + stream_ref := gun:stream_ref(), + + %% The full stream reference for the responsible HTTP/2 stream. + handle_continue_stream_ref := gun:stream_ref() +}. + +name() -> tls_proxy_http2_connect. + +messages() -> {tls_proxy_http2_connect, tls_proxy_http2_connect_closed, tls_proxy_http2_connect_error}. + +-spec connect(_, _, _) -> no_return(). +connect(_, _, _) -> + error(not_implemented). + +-spec connect(_, _, _, _) -> no_return(). +connect(_, _, _, _) -> + error(not_implemented). + +-spec send(socket(), iodata()) -> ok. +send(#{gun_pid := GunPid, reply_to := ReplyTo, stream_ref := DataStreamRef, + handle_continue_stream_ref := StreamRef}, Data) -> + GunPid ! {handle_continue, StreamRef, {data, ReplyTo, DataStreamRef, nofin, Data}}, + ok. + +-spec setopts(_, _) -> no_return(). +setopts(_, _) -> + %% We send messages automatically regardless of active mode. + ok. + +-spec sockname(_) -> no_return(). +sockname(_) -> + error(not_implemented). + +-spec close(socket()) -> ok. +close(_) -> + ok. diff --git a/gun/src/gun_tunnel.erl b/gun/src/gun_tunnel.erl new file mode 100644 index 0000000..789d1e3 --- /dev/null +++ b/gun/src/gun_tunnel.erl @@ -0,0 +1,637 @@ +%% Copyright (c) 2020-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +%% This module is used when a tunnel is established and either +%% StreamRef dereference or a TLS proxy process must be handled +%% by the tunnel layer. +-module(gun_tunnel). + +-export([init/6]). +-export([handle/5]). +-export([handle_continue/6]). +-export([update_flow/4]). +-export([closing/4]). +-export([close/4]). +-export([keepalive/3]). +-export([headers/12]). +-export([request/13]). +-export([data/7]). +-export([connect/9]). +-export([cancel/5]). +-export([timeout/3]). +-export([stream_info/2]). +-export([tunneled_name/2]). +-export([down/1]). +-export([ws_upgrade/11]). +-export([ws_send/6]). + +-record(tunnel_state, { + %% Fake socket and transport. + %% We accept 'undefined' only to simplify the init code. + socket = undefined :: #{ + gun_pid := pid(), + reply_to := pid(), + stream_ref := gun:stream_ref(), + handle_continue_stream_ref := gun:stream_ref() + } | pid() | undefined, + transport = undefined :: gun_tcp_proxy | gun_tls_proxy | undefined, + + %% The stream_ref from which the stream was created. When + %% the tunnel exists as a result of HTTP/2 CONNECT -> HTTP/1.1 CONNECT + %% the stream_ref is the same as the HTTP/1.1 CONNECT one. + stream_ref = undefined :: gun:stream_ref(), + + %% The pid we send messages to. + reply_to = undefined :: pid(), + + %% When the tunnel is a 'connect' tunnel we must dereference the + %% stream_ref. When it is 'socks' we must not as there was no + %% stream involved in creating the tunnel. + type = undefined :: connect | socks5, + + %% Transport and protocol name of the tunnel layer. + tunnel_transport = undefined :: tcp | tls, + tunnel_protocol = undefined :: http | http2 | socks, + + %% Tunnel information. + info = undefined :: gun:tunnel_info(), + + %% The origin socket of the TLS proxy, if any. This is used to forward + %% messages to the proxy process in order to decrypt the data. + tls_origin_socket = undefined :: undefined | #{ + gun_pid := pid(), + reply_to := pid(), + stream_ref := gun:stream_ref(), + handle_continue_stream_ref => gun:stream_ref() + }, + + opts = undefined :: undefined | any(), %% @todo Opts type. + + %% Protocol module and state of the outer layer. Only initialized + %% after the TLS handshake has completed when TLS is involved. + protocol = undefined :: module(), + protocol_state = undefined :: any(), + + %% When the protocol is being switched the origin may change. + %% We keep the new information to provide it in TunnelInfo of + %% the new protocol when the switch occurs. + protocol_origin = undefined :: undefined + | {origin, binary(), inet:hostname() | inet:ip_address(), inet:port_number(), connect | socks5} +}). + +%% Socket is the "origin socket" and Transport the "origin transport". +%% When the Transport indicate a TLS handshake was requested, the socket +%% and transport are given to the intermediary TLS proxy process. +%% +%% Opts is the options for the underlying HTTP/2 connection, +%% with some extra information added for the tunnel. +%% +%% @todo Mark the tunnel options as reserved. +init(ReplyTo, OriginSocket, OriginTransport, Opts=#{stream_ref := StreamRef, tunnel := Tunnel}, + EvHandler, EvHandlerState0) -> + #{ + type := TunnelType, + transport_name := TunnelTransport, + protocol_name := TunnelProtocol, + info := TunnelInfo + } = Tunnel, + State = #tunnel_state{stream_ref=StreamRef, reply_to=ReplyTo, type=TunnelType, + tunnel_transport=TunnelTransport, tunnel_protocol=TunnelProtocol, + info=TunnelInfo, opts=maps:without([stream_ref, tunnel], Opts)}, + case Tunnel of + %% Initialize the protocol. + #{new_protocol := NewProtocol} -> + {Proto, ProtoOpts} = gun_protocols:handler_and_opts(NewProtocol, Opts), + case Proto:init(ReplyTo, OriginSocket, OriginTransport, + ProtoOpts#{stream_ref => StreamRef, tunnel_transport => tcp}) of + {ok, _, ProtoState} -> + EvHandlerState = EvHandler:protocol_changed(#{ + stream_ref => StreamRef, + protocol => Proto:name() + }, EvHandlerState0), + %% When the tunnel protocol is HTTP/1.1 or SOCKS + %% the gun_tunnel_up message was already sent. + _ = case TunnelProtocol of + http -> ok; + socks -> ok; + _ -> ReplyTo ! {gun_tunnel_up, self(), StreamRef, Proto:name()} + end, + {tunnel, State#tunnel_state{socket=OriginSocket, transport=OriginTransport, + protocol=Proto, protocol_state=ProtoState}, + EvHandlerState}; + Error={error, _} -> + Error + end; + %% We can't initialize the protocol until the TLS handshake has completed. + #{handshake_event := HandshakeEvent0, protocols := Protocols} -> + #{handle_continue_stream_ref := ContinueStreamRef} = OriginSocket, + #{ + origin_host := DestHost, + origin_port := DestPort + } = TunnelInfo, + #{ + tls_opts := TLSOpts, + timeout := TLSTimeout + } = HandshakeEvent0, + HandshakeEvent = HandshakeEvent0#{socket => OriginSocket}, + EvHandlerState = EvHandler:tls_handshake_start(HandshakeEvent, EvHandlerState0), + {ok, ProxyPid} = gun_tls_proxy:start_link(DestHost, DestPort, + TLSOpts, TLSTimeout, OriginSocket, gun_tls_proxy_http2_connect, + {handle_continue, ContinueStreamRef, HandshakeEvent, Protocols}), + {tunnel, State#tunnel_state{socket=ProxyPid, transport=gun_tls_proxy, + tls_origin_socket=OriginSocket}, EvHandlerState} + end. + +%% When we receive data we pass it forward directly for TCP; +%% or we decrypt it and pass it via handle_continue for TLS. +handle(Data, State=#tunnel_state{transport=gun_tcp_proxy, + protocol=Proto, protocol_state=ProtoState0}, + CookieStore0, EvHandler, EvHandlerState0) -> + {Commands, CookieStore, EvHandlerState1} = Proto:handle( + Data, ProtoState0, CookieStore0, EvHandler, EvHandlerState0), + {ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1), + {ResCommands, CookieStore, EvHandlerState}; +handle(Data, State=#tunnel_state{transport=gun_tls_proxy, + socket=ProxyPid, tls_origin_socket=OriginSocket}, + CookieStore, _EvHandler, EvHandlerState) -> + %% When we receive a DATA frame that contains TLS-encoded data, + %% we must first forward it to the ProxyPid to be decoded. The + %% Gun process will receive it back as a tls_proxy_http2_connect + %% message and forward it to the right stream via the handle_continue + %% callback. + ProxyPid ! {tls_proxy_http2_connect, OriginSocket, Data}, + {{state, State}, CookieStore, EvHandlerState}. + +%% This callback will only be called for TLS. +%% +%% The StreamRef in this callback is special because it includes +%% a reference() for Socks layers as well. +handle_continue(ContinueStreamRef, {gun_tls_proxy, ProxyPid, {ok, Negotiated}, + {handle_continue, _, HandshakeEvent, Protocols}}, + State=#tunnel_state{socket=ProxyPid, stream_ref=StreamRef, opts=Opts}, + CookieStore, EvHandler, EvHandlerState0) + when is_reference(ContinueStreamRef) -> + #{reply_to := ReplyTo} = HandshakeEvent, + NewProtocol = gun_protocols:negotiated(Negotiated, Protocols), + {Proto, ProtoOpts} = gun_protocols:handler_and_opts(NewProtocol, Opts), + EvHandlerState1 = EvHandler:tls_handshake_end(HandshakeEvent#{ + socket => ProxyPid, + protocol => Proto:name() + }, EvHandlerState0), + EvHandlerState = EvHandler:protocol_changed(#{ + stream_ref => StreamRef, + protocol => Proto:name() + }, EvHandlerState1), + %% @todo Terminate the current protocol or something? + OriginSocket = #{ + gun_pid => self(), + reply_to => ReplyTo, + stream_ref => StreamRef + }, + case Proto:init(ReplyTo, OriginSocket, gun_tcp_proxy, + ProtoOpts#{stream_ref => StreamRef, tunnel_transport => tls}) of + {ok, _, ProtoState} -> + ReplyTo ! {gun_tunnel_up, self(), StreamRef, Proto:name()}, + {{state, State#tunnel_state{protocol=Proto, protocol_state=ProtoState}}, + CookieStore, EvHandlerState}; + Error={error, _} -> + {Error, CookieStore, EvHandlerState} + end; +handle_continue(ContinueStreamRef, {gun_tls_proxy, ProxyPid, {error, Reason}, + {handle_continue, _, HandshakeEvent, _}}, + #tunnel_state{socket=ProxyPid}, CookieStore, EvHandler, EvHandlerState0) + when is_reference(ContinueStreamRef) -> + EvHandlerState = EvHandler:tls_handshake_end(HandshakeEvent#{ + error => Reason + }, EvHandlerState0), +%% @todo +%% The TCP connection can be closed by either peer. The END_STREAM flag +%% on a DATA frame is treated as being equivalent to the TCP FIN bit. A +%% client is expected to send a DATA frame with the END_STREAM flag set +%% after receiving a frame bearing the END_STREAM flag. A proxy that +%% receives a DATA frame with the END_STREAM flag set sends the attached +%% data with the FIN bit set on the last TCP segment. A proxy that +%% receives a TCP segment with the FIN bit set sends a DATA frame with +%% the END_STREAM flag set. Note that the final TCP segment or DATA +%% frame could be empty. + {{error, Reason}, CookieStore, EvHandlerState}; +%% Send the data. This causes TLS to encrypt the data and send it to the inner layer. +handle_continue(ContinueStreamRef, {data, _ReplyTo, _StreamRef, IsFin, Data}, + #tunnel_state{}, CookieStore, _EvHandler, EvHandlerState) + when is_reference(ContinueStreamRef) -> + {{send, IsFin, Data}, CookieStore, EvHandlerState}; +handle_continue(ContinueStreamRef, {tls_proxy, ProxyPid, Data}, + State=#tunnel_state{socket=ProxyPid, protocol=Proto, protocol_state=ProtoState}, + CookieStore0, EvHandler, EvHandlerState0) + when is_reference(ContinueStreamRef) -> + {Commands, CookieStore, EvHandlerState1} = Proto:handle( + Data, ProtoState, CookieStore0, EvHandler, EvHandlerState0), + {ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1), + {ResCommands, CookieStore, EvHandlerState}; +handle_continue(ContinueStreamRef, {tls_proxy_closed, ProxyPid}, + #tunnel_state{socket=ProxyPid}, CookieStore, _EvHandler, EvHandlerState0) + when is_reference(ContinueStreamRef) -> + %% @todo All sub-streams must produce a stream_error. + {{error, closed}, CookieStore, EvHandlerState0}; +handle_continue(ContinueStreamRef, {tls_proxy_error, ProxyPid, Reason}, + #tunnel_state{socket=ProxyPid}, CookieStore, _EvHandler, EvHandlerState0) + when is_reference(ContinueStreamRef) -> + %% @todo All sub-streams must produce a stream_error. + {{error, Reason}, CookieStore, EvHandlerState0}; +%% We always dereference the ContinueStreamRef because it includes a +%% reference() for Socks layers too. +%% +%% @todo Assert StreamRef to be our reference(). +handle_continue([_StreamRef|ContinueStreamRef0], Msg, + State=#tunnel_state{protocol=Proto, protocol_state=ProtoState}, + CookieStore0, EvHandler, EvHandlerState0) -> + ContinueStreamRef = case ContinueStreamRef0 of + [CSR] -> CSR; + _ -> ContinueStreamRef0 + end, + {Commands, CookieStore, EvHandlerState1} = Proto:handle_continue( + ContinueStreamRef, Msg, ProtoState, CookieStore0, EvHandler, EvHandlerState0), + {ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1), + {ResCommands, CookieStore, EvHandlerState}. + +%% @todo This function will need EvHandler/EvHandlerState? +update_flow(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState}, + ReplyTo, StreamRef0, Inc) -> + StreamRef = maybe_dereference(State, StreamRef0), + Commands = Proto:update_flow(ProtoState, ReplyTo, StreamRef, Inc), + {ResCommands, undefined} = commands(Commands, State, undefined, undefined), + ResCommands. + +closing(_Reason, _State, _EvHandler, EvHandlerState) -> + %% @todo Graceful shutdown must be propagated to tunnels. + {[], EvHandlerState}. + +close(_Reason, _State, _EvHandler, EvHandlerState) -> + %% @todo Closing must be propagated to tunnels. + EvHandlerState. + +keepalive(_State, _EvHandler, EvHandlerState) -> + %% @todo Need to figure out how to handle keepalive for tunnels. + {[], EvHandlerState}. + +%% We pass the headers forward and optionally dereference StreamRef. +headers(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState0}, + StreamRef0, ReplyTo, Method, Host, Port, Path, Headers, + InitialFlow, CookieStore0, EvHandler, EvHandlerState0) -> + StreamRef = maybe_dereference(State, StreamRef0), + {Commands, CookieStore, EvHandlerState1} = Proto:headers(ProtoState0, StreamRef, + ReplyTo, Method, Host, Port, Path, Headers, + InitialFlow, CookieStore0, EvHandler, EvHandlerState0), + {ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1), + {ResCommands, CookieStore, EvHandlerState}. + +%% We pass the request forward and optionally dereference StreamRef. +request(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState0, + info=#{origin_host := OriginHost, origin_port := OriginPort}}, + StreamRef0, ReplyTo, Method, _Host, _Port, Path, Headers, Body, + InitialFlow, CookieStore0, EvHandler, EvHandlerState0) -> + StreamRef = maybe_dereference(State, StreamRef0), + {Commands, CookieStore, EvHandlerState1} = Proto:request(ProtoState0, StreamRef, + ReplyTo, Method, OriginHost, OriginPort, Path, Headers, Body, + InitialFlow, CookieStore0, EvHandler, EvHandlerState0), + {ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1), + {ResCommands, CookieStore, EvHandlerState}. + +%% When the next tunnel is SOCKS we pass the data forward directly. +%% This is needed because SOCKS has no StreamRef and the data cannot +%% therefore be passed forward through the usual method. +data(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState0, + protocol_origin={origin, _, _, _, socks5}}, + StreamRef, ReplyTo, IsFin, Data, EvHandler, EvHandlerState0) -> + {Commands, EvHandlerState1} = Proto:data(ProtoState0, StreamRef, + ReplyTo, IsFin, Data, EvHandler, EvHandlerState0), + {ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1), + {ResCommands, EvHandlerState}; +%% CONNECT tunnels pass the data forward and dereference StreamRef +%% unless they are the recipient of the callback, in which case the +%% data is sent to the socket. +data(State=#tunnel_state{socket=Socket, transport=Transport, + stream_ref=TunnelStreamRef0, protocol=Proto, protocol_state=ProtoState0}, + StreamRef0, ReplyTo, IsFin, Data, EvHandler, EvHandlerState0) -> + TunnelStreamRef = outer_stream_ref(TunnelStreamRef0), + case StreamRef0 of + TunnelStreamRef -> + case Transport:send(Socket, Data) of + ok -> {[], EvHandlerState0}; + Error={error, _} -> {Error, EvHandlerState0} + end; + _ -> + StreamRef = maybe_dereference(State, StreamRef0), + {Commands, EvHandlerState1} = Proto:data(ProtoState0, StreamRef, + ReplyTo, IsFin, Data, EvHandler, EvHandlerState0), + {ResCommands, EvHandlerState} = commands(Commands, State, + EvHandler, EvHandlerState1), + {ResCommands, EvHandlerState} + end. + +%% We pass the CONNECT request forward and optionally dereference StreamRef. +connect(State=#tunnel_state{info=#{origin_host := Host, origin_port := Port}, + protocol=Proto, protocol_state=ProtoState0}, + StreamRef0, ReplyTo, Destination, _, Headers, InitialFlow, + EvHandler, EvHandlerState0) -> + StreamRef = maybe_dereference(State, StreamRef0), + {Commands, EvHandlerState1} = Proto:connect(ProtoState0, StreamRef, + ReplyTo, Destination, #{host => Host, port => Port}, Headers, InitialFlow, + EvHandler, EvHandlerState0), + {ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1), + {ResCommands, EvHandlerState}. + +cancel(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState0}, + StreamRef0, ReplyTo, EvHandler, EvHandlerState0) -> + StreamRef = maybe_dereference(State, StreamRef0), + {Commands, EvHandlerState1} = Proto:cancel(ProtoState0, StreamRef, + ReplyTo, EvHandler, EvHandlerState0), + {ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1), + {ResCommands, EvHandlerState}. + +timeout(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState0}, Msg, TRef) -> + case Proto:timeout(ProtoState0, Msg, TRef) of + {state, ProtoState} -> + {state, State#tunnel_state{protocol_state=ProtoState}}; + Other -> + Other + end. + +stream_info(#tunnel_state{transport=Transport0, stream_ref=TunnelStreamRef, reply_to=ReplyTo, + tunnel_protocol=TunnelProtocol, + info=#{origin_host := OriginHost, origin_port := OriginPort}, + protocol=Proto, protocol_state=ProtoState}, StreamRef) + when is_reference(StreamRef), TunnelProtocol =/= socks -> + Transport = case Transport0 of + gun_tcp_proxy -> tcp; + gun_tls_proxy -> tls + end, + Protocol = case Proto of + gun_tunnel -> Proto:tunneled_name(ProtoState, false); + _ -> Proto:name() + end, + {ok, #{ + ref => TunnelStreamRef, + reply_to => ReplyTo, + state => running, + tunnel => #{ + transport => Transport, + protocol => Protocol, + origin_scheme => case {Transport, Protocol} of + {_, raw} -> undefined; + {tcp, _} -> <<"http">>; + {tls, _} -> <<"https">> + end, + origin_host => OriginHost, + origin_port => OriginPort + } + }}; +stream_info(State=#tunnel_state{type=Type, + tunnel_transport=IntermediaryTransport, tunnel_protocol=IntermediaryProtocol, + info=TunnelInfo, protocol=Proto, protocol_state=ProtoState}, StreamRef0) -> + StreamRef = maybe_dereference(State, StreamRef0), + case Proto:stream_info(ProtoState, StreamRef) of + {ok, undefined} -> + {ok, undefined}; + {ok, Info} -> + #{ + host := IntermediateHost, + port := IntermediatePort + } = TunnelInfo, + IntermediaryInfo = #{ + type => Type, + host => IntermediateHost, + port => IntermediatePort, + transport => IntermediaryTransport, + protocol => IntermediaryProtocol + }, + Intermediaries = maps:get(intermediaries, Info, []), + {ok, Info#{ + intermediaries => [IntermediaryInfo|Intermediaries] + }} + end. + +tunneled_name(#tunnel_state{protocol=Proto=gun_tunnel, protocol_state=ProtoState}, true) -> + Proto:tunneled_name(ProtoState, false); +tunneled_name(#tunnel_state{tunnel_protocol=TunnelProto}, false) -> + TunnelProto; +tunneled_name(#tunnel_state{protocol=Proto}, _) -> + Proto:name(). + +down(_State) -> + %% @todo Tunnels must be included in the gun_down message. + []. + +ws_upgrade(State=#tunnel_state{info=TunnelInfo, protocol=Proto, protocol_state=ProtoState0}, + StreamRef0, ReplyTo, _, _, Path, Headers, WsOpts, + CookieStore0, EvHandler, EvHandlerState0) -> + StreamRef = maybe_dereference(State, StreamRef0), + #{ + origin_host := Host, + origin_port := Port + } = TunnelInfo, + {Commands, CookieStore, EvHandlerState1} = Proto:ws_upgrade(ProtoState0, StreamRef, ReplyTo, + Host, Port, Path, Headers, WsOpts, + CookieStore0, EvHandler, EvHandlerState0), + {ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1), + {ResCommands, CookieStore, EvHandlerState}. + +ws_send(Frames, State=#tunnel_state{protocol=Proto, protocol_state=ProtoState}, + StreamRef0, ReplyTo, EvHandler, EvHandlerState0) -> + StreamRef = maybe_dereference(State, StreamRef0), + {Commands, EvHandlerState1} = Proto:ws_send(Frames, + ProtoState, StreamRef, ReplyTo, EvHandler, EvHandlerState0), + {ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1), + {ResCommands, EvHandlerState}. + +%% Internal. + +%% Returns an error on send errors, a state otherwise +commands(Command, State, EvHandler, EvHandlerState) when not is_list(Command) -> + commands([Command], State, EvHandler, EvHandlerState); +commands([], State, _, EvHandlerState) -> + {{state, State}, EvHandlerState}; +commands([Error = {error, _}|_], + State=#tunnel_state{socket=Socket, transport=Transport}, + _, EvHandlerState) -> + %% We must terminate the TLS proxy pid if any. + case Transport of + gun_tls_proxy -> gun_tls_proxy:close(Socket); + _ -> ok + end, + {[{state, State}, Error], EvHandlerState}; +commands([{state, ProtoState}|Tail], State, EvHandler, EvHandlerState) -> + commands(Tail, State#tunnel_state{protocol_state=ProtoState}, EvHandler, EvHandlerState); +%% @todo What to do about IsFin? +commands([{send, _IsFin, Data}|Tail], + State=#tunnel_state{socket=Socket, transport=Transport}, + EvHandler, EvHandlerState) -> + case Transport:send(Socket, Data) of + ok -> commands(Tail, State, EvHandler, EvHandlerState); + Error={error, _} -> {Error, EvHandlerState} + end; +commands([Origin={origin, Scheme, Host, Port, Type}|Tail], + State=#tunnel_state{stream_ref=StreamRef}, + EvHandler, EvHandlerState0) -> + EvHandlerState = EvHandler:origin_changed(#{ + stream_ref => StreamRef, + type => Type, + origin_scheme => Scheme, + origin_host => Host, + origin_port => Port + }, EvHandlerState0), + commands(Tail, State#tunnel_state{protocol_origin=Origin}, EvHandler, EvHandlerState); +commands([{switch_protocol, NewProtocol, ReplyTo}|Tail], + State=#tunnel_state{socket=Socket, transport=Transport, opts=Opts, + protocol_origin=undefined}, + EvHandler, EvHandlerState0) -> + {Proto, ProtoOpts} = gun_protocols:handler_and_opts(NewProtocol, Opts), + %% This should only apply to Websocket for the time being. + case Proto:init(ReplyTo, Socket, Transport, ProtoOpts) of + {ok, connected_ws_only, ProtoState} -> + #{stream_ref := StreamRef} = ProtoOpts, + EvHandlerState = EvHandler:protocol_changed(#{ + stream_ref => StreamRef, + protocol => Proto:name() + }, EvHandlerState0), + commands(Tail, State#tunnel_state{protocol=Proto, protocol_state=ProtoState}, + EvHandler, EvHandlerState); + Error={error, _} -> + {Error, EvHandlerState0} + end; +commands([{switch_protocol, NewProtocol, ReplyTo}|Tail], + State=#tunnel_state{transport=Transport, stream_ref=TunnelStreamRef, + info=#{origin_host := Host, origin_port := Port}, opts=Opts, protocol=CurrentProto, + protocol_origin={origin, _Scheme, OriginHost, OriginPort, Type}}, + EvHandler, EvHandlerState0) -> + StreamRef = case Type of + socks5 -> TunnelStreamRef; + connect -> gun_protocols:stream_ref(NewProtocol) + end, + ContinueStreamRef0 = continue_stream_ref(State), + ContinueStreamRef = case Type of + socks5 -> ContinueStreamRef0 ++ [make_ref()]; + connect -> ContinueStreamRef0 ++ [lists:last(StreamRef)] + end, + OriginSocket = #{ + gun_pid => self(), + reply_to => ReplyTo, + stream_ref => StreamRef, + handle_continue_stream_ref => ContinueStreamRef + }, + ProtoOpts = Opts#{ + stream_ref => StreamRef, + tunnel => #{ + type => Type, + transport_name => case Transport of + gun_tcp_proxy -> tcp; + gun_tls_proxy -> tls + end, + protocol_name => CurrentProto:name(), + info => #{ + host => Host, + port => Port, + origin_host => OriginHost, + origin_port => OriginPort + }, + new_protocol => NewProtocol + } + }, + Proto = gun_tunnel, + case Proto:init(ReplyTo, OriginSocket, gun_tcp_proxy, ProtoOpts, EvHandler, EvHandlerState0) of + {tunnel, ProtoState, EvHandlerState} -> + commands(Tail, State#tunnel_state{protocol=Proto, protocol_state=ProtoState}, + EvHandler, EvHandlerState); + Error={error, _} -> + {Error, EvHandlerState0} + end; +commands([{tls_handshake, HandshakeEvent0, Protocols, ReplyTo}|Tail], + State=#tunnel_state{transport=Transport, + info=#{origin_host := Host, origin_port := Port}, opts=Opts, protocol=CurrentProto, + protocol_origin={origin, _Scheme, OriginHost, OriginPort, Type}}, + EvHandler, EvHandlerState0) -> + #{ + stream_ref := StreamRef, + tls_opts := TLSOpts0 + } = HandshakeEvent0, + TLSOpts = gun:ensure_tls_opts(Protocols, TLSOpts0, OriginHost), + HandshakeEvent = HandshakeEvent0#{ + tls_opts => TLSOpts + }, + ContinueStreamRef0 = continue_stream_ref(State), + ContinueStreamRef = case Type of + socks5 -> ContinueStreamRef0 ++ [make_ref()]; + connect -> ContinueStreamRef0 ++ [lists:last(StreamRef)] + end, + OriginSocket = #{ + gun_pid => self(), + reply_to => ReplyTo, + stream_ref => StreamRef, + handle_continue_stream_ref => ContinueStreamRef + }, + ProtoOpts = Opts#{ + stream_ref => StreamRef, + tunnel => #{ + type => Type, + transport_name => case Transport of + gun_tcp_proxy -> tcp; + gun_tls_proxy -> tls + end, + protocol_name => CurrentProto:name(), + info => #{ + host => Host, + port => Port, + origin_host => OriginHost, + origin_port => OriginPort + }, + handshake_event => HandshakeEvent, + protocols => Protocols + } + }, + Proto = gun_tunnel, + case Proto:init(ReplyTo, OriginSocket, gun_tcp_proxy, ProtoOpts, EvHandler, EvHandlerState0) of + {tunnel, ProtoState, EvHandlerState} -> + commands(Tail, State#tunnel_state{protocol=Proto, protocol_state=ProtoState}, + EvHandler, EvHandlerState); + Error={error, _} -> + {Error, EvHandlerState0} + end; +commands([{active, true}|Tail], State, EvHandler, EvHandlerState) -> + commands(Tail, State, EvHandler, EvHandlerState). + +continue_stream_ref(#tunnel_state{socket=#{handle_continue_stream_ref := ContinueStreamRef}}) -> + if + is_list(ContinueStreamRef) -> ContinueStreamRef; + true -> [ContinueStreamRef] + end; +continue_stream_ref(#tunnel_state{tls_origin_socket=#{handle_continue_stream_ref := ContinueStreamRef}}) -> + if + is_list(ContinueStreamRef) -> ContinueStreamRef; + true -> [ContinueStreamRef] + end. + +maybe_dereference(#tunnel_state{stream_ref=RealStreamRef, type=connect}, [StreamRef|Tail]) -> + %% We ensure that the stream_ref is correct. + StreamRef = outer_stream_ref(RealStreamRef), + case Tail of + [Ref] -> Ref; + _ -> Tail + end; +maybe_dereference(#tunnel_state{type=socks5}, StreamRef) -> + StreamRef. + +outer_stream_ref(StreamRef) when is_list(StreamRef) -> + lists:last(StreamRef); +outer_stream_ref(StreamRef) -> + StreamRef. diff --git a/gun/src/gun_ws.erl b/gun/src/gun_ws.erl new file mode 100644 index 0000000..c59686e --- /dev/null +++ b/gun/src/gun_ws.erl @@ -0,0 +1,354 @@ +%% Copyright (c) 2015-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_ws). + +-export([check_options/1]). +-export([select_extensions/3]). +-export([select_protocol/2]). +-export([name/0]). +-export([opts_name/0]). +-export([has_keepalive/0]). +-export([default_keepalive/0]). +-export([init/4]). +-export([handle/5]). +-export([handle_continue/6]). +-export([update_flow/4]). +-export([closing/4]). +-export([close/4]). +-export([keepalive/3]). +-export([ws_send/6]). +-export([down/1]). + +-record(payload, { + type = undefined :: cow_ws:frame_type(), + rsv = undefined :: cow_ws:rsv(), + len = undefined :: non_neg_integer(), + mask_key = undefined :: cow_ws:mask_key(), + close_code = undefined :: undefined | cow_ws:close_code(), + unmasked = <<>> :: binary(), + unmasked_len = 0 :: non_neg_integer() +}). + +-record(ws_state, { + reply_to :: pid(), + stream_ref :: reference(), + socket :: inet:socket() | ssl:sslsocket(), + transport :: module(), + opts = #{} :: gun:ws_opts(), + buffer = <<>> :: binary(), + in = head :: head | #payload{} | close, + out = head :: head | close, + frag_state = undefined :: cow_ws:frag_state(), + utf8_state = 0 :: cow_ws:utf8_state(), + extensions = #{} :: cow_ws:extensions(), + flow :: integer() | infinity, + handler :: module(), + handler_state :: any() +}). + +check_options(Opts) -> + do_check_options(maps:to_list(Opts)). + +do_check_options([]) -> + ok; +do_check_options([{closing_timeout, infinity}|Opts]) -> + do_check_options(Opts); +do_check_options([{closing_timeout, T}|Opts]) when is_integer(T), T > 0 -> + do_check_options(Opts); +do_check_options([{compress, B}|Opts]) when is_boolean(B) -> + do_check_options(Opts); +do_check_options([{default_protocol, M}|Opts]) when is_atom(M) -> + do_check_options(Opts); +do_check_options([{flow, InitialFlow}|Opts]) when is_integer(InitialFlow), InitialFlow > 0 -> + do_check_options(Opts); +do_check_options([{keepalive, infinity}|Opts]) -> + do_check_options(Opts); +do_check_options([{keepalive, K}|Opts]) when is_integer(K), K > 0 -> + do_check_options(Opts); +do_check_options([Opt={protocols, L}|Opts]) when is_list(L) -> + case lists:usort(lists:flatten([[is_binary(B), is_atom(M)] || {B, M} <- L])) of + [true] -> do_check_options(Opts); + _ -> {error, {options, {ws, Opt}}} + end; +do_check_options([{reply_to, P}|Opts]) when is_pid(P) -> + do_check_options(Opts); +do_check_options([{silence_pings, B}|Opts]) when is_boolean(B) -> + do_check_options(Opts); +do_check_options([{user_opts, _}|Opts]) -> + do_check_options(Opts); +do_check_options([Opt|_]) -> + {error, {options, {ws, Opt}}}. + +select_extensions(Headers, Extensions0, Opts) -> + case lists:keyfind(<<"sec-websocket-extensions">>, 1, Headers) of + false -> + #{}; + {_, ExtHd} -> + ParsedExtHd = cow_http_hd:parse_sec_websocket_extensions(ExtHd), + validate_extensions(ParsedExtHd, Extensions0, Opts, #{}) + end. + +validate_extensions([], _, _, Acc) -> + Acc; +validate_extensions([{Name = <<"permessage-deflate">>, Params}|Tail], Extensions, Opts, Acc0) -> + case lists:member(Name, Extensions) of + true -> + case cow_ws:validate_permessage_deflate(Params, Acc0, Opts) of + {ok, Acc} -> validate_extensions(Tail, Extensions, Opts, Acc); + error -> close + end; + %% Fail the connection if extension was not requested. + false -> + close + end; +%% Fail the connection on unknown extension. +validate_extensions(_, _, _, _) -> + close. + +%% @todo Validate protocols. +select_protocol(Headers, Opts) -> + case lists:keyfind(<<"sec-websocket-protocol">>, 1, Headers) of + false -> + maps:get(default_protocol, Opts, gun_ws_h); + {_, Proto} -> + ProtoOpt = maps:get(protocols, Opts, []), + case lists:keyfind(Proto, 1, ProtoOpt) of + {_, Handler} -> + Handler; + false -> + close + end + end. + +name() -> ws. +opts_name() -> ws_opts. +has_keepalive() -> true. +default_keepalive() -> infinity. + +init(ReplyTo, Socket, Transport, #{stream_ref := StreamRef, headers := Headers, + extensions := Extensions, flow := InitialFlow, handler := Handler, opts := Opts}) -> + {ok, HandlerState} = Handler:init(ReplyTo, StreamRef, Headers, Opts), + {ok, connected_ws_only, #ws_state{reply_to=ReplyTo, stream_ref=StreamRef, + socket=Socket, transport=Transport, opts=Opts, extensions=Extensions, + flow=InitialFlow, handler=Handler, handler_state=HandlerState}}. + +handle(Data, State, CookieStore, EvHandler, EvHandlerState0) -> + {Commands, EvHandlerState} = handle(Data, State, EvHandler, EvHandlerState0), + {Commands, CookieStore, EvHandlerState}. + +%% Do not handle anything if we received a close frame. +%% Initiate or terminate the closing state depending on whether we sent a close yet. +handle(_, State=#ws_state{in=close, out=close}, _, EvHandlerState) -> + {[{state, State}, close], EvHandlerState}; +handle(_, State=#ws_state{in=close}, EvHandler, EvHandlerState) -> + closing(normal, State, EvHandler, EvHandlerState); +%% Shortcut for common case when Data is empty after processing a frame. +handle(<<>>, State=#ws_state{in=head}, _, EvHandlerState) -> + maybe_active(State, EvHandlerState); +handle(Data, State=#ws_state{reply_to=ReplyTo, stream_ref=StreamRef, buffer=Buffer, + in=head, frag_state=FragState, extensions=Extensions}, + EvHandler, EvHandlerState0) -> + %% Send the event only if there was no data in the buffer. + %% If there is data in the buffer then we already sent the event. + EvHandlerState1 = case Buffer of + <<>> -> + EvHandler:ws_recv_frame_start(#{ + stream_ref => StreamRef, + reply_to => ReplyTo, + frag_state => FragState, + extensions => Extensions + }, EvHandlerState0); + _ -> + EvHandlerState0 + end, + Data2 = << Buffer/binary, Data/binary >>, + case cow_ws:parse_header(Data2, Extensions, FragState) of + {Type, FragState2, Rsv, Len, MaskKey, Rest} -> + EvHandlerState = EvHandler:ws_recv_frame_header(#{ + stream_ref => StreamRef, + reply_to => ReplyTo, + frag_state => FragState2, + extensions => Extensions, + type => Type, + rsv => Rsv, + len => Len, + mask_key => MaskKey + }, EvHandlerState1), + handle(Rest, State#ws_state{buffer= <<>>, + in=#payload{type=Type, rsv=Rsv, len=Len, mask_key=MaskKey}, + frag_state=FragState2}, EvHandler, EvHandlerState); + more -> + maybe_active(State#ws_state{buffer=Data2}, EvHandlerState1); + error -> + closing({error, badframe}, State, EvHandler, EvHandlerState1) + end; +handle(Data, State=#ws_state{in=In=#payload{type=Type, rsv=Rsv, len=Len, mask_key=MaskKey, + close_code=CloseCode, unmasked=Unmasked, unmasked_len=UnmaskedLen}, frag_state=FragState, + utf8_state=Utf8State, extensions=Extensions}, EvHandler, EvHandlerState) -> + case cow_ws:parse_payload(Data, MaskKey, Utf8State, UnmaskedLen, Type, Len, FragState, Extensions, Rsv) of + {ok, CloseCode2, Payload, Utf8State2, Rest} -> + dispatch(Rest, State#ws_state{in=head, utf8_state=Utf8State2}, Type, + <>, CloseCode2, + EvHandler, EvHandlerState); + {ok, Payload, Utf8State2, Rest} -> + dispatch(Rest, State#ws_state{in=head, utf8_state=Utf8State2}, Type, + <>, CloseCode, + EvHandler, EvHandlerState); + {more, CloseCode2, Payload, Utf8State2} -> + maybe_active(State#ws_state{in=In#payload{close_code=CloseCode2, + unmasked= <>, + len=Len - byte_size(Data), unmasked_len=2 + byte_size(Data)}, utf8_state=Utf8State2}, + EvHandlerState); + {more, Payload, Utf8State2} -> + maybe_active(State#ws_state{in=In#payload{unmasked= <>, + len=Len - byte_size(Data), unmasked_len=UnmaskedLen + byte_size(Data)}, utf8_state=Utf8State2}, + EvHandlerState); + Error = {error, _Reason} -> + closing(Error, State, EvHandler, EvHandlerState) + end. + +handle_continue(ContinueStreamRef, {data, _ReplyTo, _StreamRef, IsFin, Data}, + #ws_state{}, CookieStore, _EvHandler, EvHandlerState) + when is_reference(ContinueStreamRef) -> + {{send, IsFin, Data}, CookieStore, EvHandlerState}. + +maybe_active(State=#ws_state{flow=Flow}, EvHandlerState) -> + {[ + {state, State}, + {active, Flow > 0} + ], EvHandlerState}. + +dispatch(Rest, State0=#ws_state{reply_to=ReplyTo, stream_ref=StreamRef, + frag_state=FragState, extensions=Extensions, flow=Flow0, + handler=Handler, handler_state=HandlerState0}, + Type, Payload, CloseCode, EvHandler, EvHandlerState0) -> + EvHandlerState1 = EvHandler:ws_recv_frame_end(#{ + stream_ref => StreamRef, + reply_to => ReplyTo, + extensions => Extensions, + close_code => CloseCode, + payload => Payload + }, EvHandlerState0), + case cow_ws:make_frame(Type, Payload, CloseCode, FragState) of + Frame -> + {ok, Dec, HandlerState} = Handler:handle(Frame, HandlerState0), + Flow = case Flow0 of + infinity -> infinity; + _ -> Flow0 - Dec + end, + State1 = State0#ws_state{flow=Flow, handler_state=HandlerState}, + {State, EvHandlerState} = case Frame of + ping -> + {[], EvHandlerState2} = send(pong, State1, ReplyTo, EvHandler, EvHandlerState1), + {State1, EvHandlerState2}; + {ping, Payload} -> + {[], EvHandlerState2} = send({pong, Payload}, State1, ReplyTo, EvHandler, EvHandlerState1), + {State1, EvHandlerState2}; + close -> + {State1#ws_state{in=close}, EvHandlerState1}; + {close, _, _} -> + {State1#ws_state{in=close}, EvHandlerState1}; + {fragment, fin, _, _} -> + {State1#ws_state{frag_state=undefined}, EvHandlerState1}; + _ -> + {State1, EvHandlerState1} + end, + handle(Rest, State, EvHandler, EvHandlerState) + end. + +update_flow(State=#ws_state{flow=Flow0}, _ReplyTo, _StreamRef, Inc) -> + Flow = case Flow0 of + infinity -> infinity; + _ -> Flow0 + Inc + end, + [ + {state, State#ws_state{flow=Flow}}, + {active, Flow > 0} + ]. + +%% The user already sent the close frame; do nothing. +closing(_, State=#ws_state{out=close}, _, EvHandlerState) -> + {closing(State), EvHandlerState}; +closing(Reason, State=#ws_state{reply_to=ReplyTo}, EvHandler, EvHandlerState) -> + Code = case Reason of + normal -> 1000; + owner_down -> 1001; + shutdown -> 1001; + {error, badframe} -> 1002; + {error, badencoding} -> 1007 + end, + send({close, Code, <<>>}, State, ReplyTo, EvHandler, EvHandlerState). + +closing(#ws_state{opts=Opts}) -> + Timeout = maps:get(closing_timeout, Opts, 15000), + {closing, Timeout}. + +close(_, _, _, EvHandlerState) -> + EvHandlerState. + +keepalive(State=#ws_state{reply_to=ReplyTo}, EvHandler, EvHandlerState0) -> + send(ping, State, ReplyTo, EvHandler, EvHandlerState0). + +%% Send one frame. +send(Frame, State=#ws_state{stream_ref=StreamRef, + socket=Socket, transport=Transport, in=In, extensions=Extensions}, + ReplyTo, EvHandler, EvHandlerState0) -> + WsSendFrameEvent = #{ + stream_ref => StreamRef, + reply_to => ReplyTo, + extensions => Extensions, + frame => Frame + }, + EvHandlerState1 = EvHandler:ws_send_frame_start(WsSendFrameEvent, EvHandlerState0), + case Transport:send(Socket, cow_ws:masked_frame(Frame, Extensions)) of + ok -> + EvHandlerState = EvHandler:ws_send_frame_end(WsSendFrameEvent, EvHandlerState1), + if + Frame =:= close; element(1, Frame) =:= close -> + {[ + {state, State#ws_state{out=close}}, + %% We can close immediately if we already + %% received a close frame. + case In of + close -> close; + _ -> closing(State) + end + ], EvHandlerState}; + true -> + {[], EvHandlerState} + end; + Error={error, _} -> + {Error, EvHandlerState0} + end. + +%% Send many frames. +ws_send(Frame, State, ReplyTo, EvHandler, EvHandlerState) when not is_list(Frame) -> + send(Frame, State, ReplyTo, EvHandler, EvHandlerState); +ws_send([], _, _, _, EvHandlerState) -> + {[], EvHandlerState}; +ws_send([Frame|Tail], State, ReplyTo, EvHandler, EvHandlerState0) -> + case send(Frame, State, ReplyTo, EvHandler, EvHandlerState0) of + {[], EvHandlerState} -> + ws_send(Tail, State, ReplyTo, EvHandler, EvHandlerState); + Other -> + Other + end. + +%% @todo We should probably check the _StreamRef value. +ws_send(Frames, State, _StreamRef, ReplyTo, EvHandler, EvHandlerState) -> + ws_send(Frames, State, ReplyTo, EvHandler, EvHandlerState). + +down(#ws_state{stream_ref=StreamRef}) -> + [StreamRef]. diff --git a/gun/src/gun_ws_h.erl b/gun/src/gun_ws_h.erl new file mode 100644 index 0000000..a7dc9fc --- /dev/null +++ b/gun/src/gun_ws_h.erl @@ -0,0 +1,44 @@ +%% Copyright (c) 2017-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_ws_h). +-behavior(gun_ws_protocol). + +-export([init/4]). +-export([handle/2]). + +-record(state, { + reply_to :: pid(), + stream_ref :: reference(), + frag_buffer = <<>> :: binary(), + silence_pings :: boolean() +}). + +init(ReplyTo, StreamRef, _, Opts) -> + {ok, #state{reply_to=ReplyTo, stream_ref=StreamRef, + silence_pings=maps:get(silence_pings, Opts, true)}}. + +handle({fragment, nofin, _, Payload}, + State=#state{frag_buffer=SoFar}) -> + {ok, 0, State#state{frag_buffer= << SoFar/binary, Payload/binary >>}}; +handle({fragment, fin, Type, Payload}, + State=#state{reply_to=ReplyTo, stream_ref=StreamRef, frag_buffer=SoFar}) -> + ReplyTo ! {gun_ws, self(), StreamRef, {Type, << SoFar/binary, Payload/binary >>}}, + {ok, 1, State#state{frag_buffer= <<>>}}; +handle(Frame, State=#state{silence_pings=true}) when Frame =:= ping; Frame =:= pong; + element(1, Frame) =:= ping; element(1, Frame) =:= pong -> + {ok, 0, State}; +handle(Frame, State=#state{reply_to=ReplyTo, stream_ref=StreamRef}) -> + ReplyTo ! {gun_ws, self(), StreamRef, Frame}, + {ok, 1, State}. diff --git a/gun/src/gun_ws_protocol.erl b/gun/src/gun_ws_protocol.erl new file mode 100644 index 0000000..332b704 --- /dev/null +++ b/gun/src/gun_ws_protocol.erl @@ -0,0 +1,25 @@ +%% Copyright (c) 2022-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_ws_protocol). + +-type protocol_state() :: any(). +-export_type([protocol_state/0]). + +-callback init(pid(), reference(), cow_http:headers(), gun:ws_opts()) + -> {ok, protocol_state()}. + +-callback handle(cow_ws:frame(), State) + -> {ok, non_neg_integer(), State} + when State::protocol_state(). diff --git a/gun/test/event_SUITE.erl b/gun/test/event_SUITE.erl new file mode 100644 index 0000000..81cdf09 --- /dev/null +++ b/gun/test/event_SUITE.erl @@ -0,0 +1,2170 @@ +%% Copyright (c) 2019-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(event_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-behavior(gun_event). + +-import(ct_helper, [config/2]). +-import(ct_helper, [doc/1]). +-import(gun_test, [init_origin/1]). + +all() -> + [ + {group, http}, + {group, http2} + ]. + +groups() -> + Tests = ct_helper:all(?MODULE), + %% Some tests are written only for HTTP/1.0 or HTTP/1.1. + HTTP1Tests = [T || T <- Tests, lists:sublist(atom_to_list(T), 6) =:= "http1_"], + %% Push is not possible over HTTP/1.1. + PushTests = [T || T <- Tests, lists:sublist(atom_to_list(T), 5) =:= "push_"], + [ + {http, [parallel], Tests -- [cancel_remote, cancel_remote_connect|PushTests]}, + {http2, [parallel], Tests -- HTTP1Tests} + ]. + +init_per_suite(Config) -> + Routes = [ + {"/", hello_h, []}, + {"/empty", empty_h, []}, + {"/inform", inform_h, []}, + {"/push", push_h, []}, + {"/stream", stream_h, []}, + {"/trailers", trailers_h, []}, + {"/ws", ws_echo_h, []} + ], + ProtoOpts = #{ + enable_connect_protocol => true, + env => #{dispatch => cowboy_router:compile([{'_', Routes}])} + }, + {ok, _} = cowboy:start_clear({?MODULE, tcp}, [], ProtoOpts), + TCPOriginPort = ranch:get_port({?MODULE, tcp}), + {ok, _} = cowboy:start_tls({?MODULE, tls}, + [{fail_if_no_peer_cert, false}|ct_helper:get_certs_from_ets()], + ProtoOpts), + TLSOriginPort = ranch:get_port({?MODULE, tls}), + [{tcp_origin_port, TCPOriginPort}, {tls_origin_port, TLSOriginPort}|Config]. + +end_per_suite(_) -> + ok = cowboy:stop_listener({?MODULE, tls}), + ok = cowboy:stop_listener({?MODULE, tcp}). + +%% init. + +init(Config) -> + doc("Confirm that the init event callback is called."), + Self = self(), + Opts = #{ + event_handler => {?MODULE, self()}, + protocols => [config(name, config(tc_group_properties, Config))] + }, + {ok, Pid} = gun:open("localhost", 12345, Opts), + #{ + owner := Self, + transport := tcp, + origin_scheme := <<"http">>, + origin_host := "localhost", + origin_port := 12345, + opts := Opts + } = do_receive_event(?FUNCTION_NAME), + gun:close(Pid). + +%% domain_lookup_start/domain_lookup_end. + +domain_lookup_start(Config) -> + doc("Confirm that the domain_lookup_start event callback is called."), + {ok, Pid, _} = do_gun_open(12345, Config), + #{ + host := "localhost", + port := 12345, + tcp_opts := _, + timeout := _ + } = do_receive_event(?FUNCTION_NAME), + gun:close(Pid). + +domain_lookup_end_error(Config) -> + doc("Confirm that the domain_lookup_end event callback is called on lookup failure."), + Opts = #{ + event_handler => {?MODULE, self()}, + protocols => [config(name, config(tc_group_properties, Config))] + }, + {ok, Pid} = gun:open("this.should.not.exist", 12345, Opts), + #{ + host := "this.should.not.exist", + port := 12345, + tcp_opts := _, + timeout := _, + error := nxdomain + } = do_receive_event(domain_lookup_end), + gun:close(Pid). + +domain_lookup_end_ok(Config) -> + doc("Confirm that the domain_lookup_end event callback is called on lookup success."), + {ok, Pid, _} = do_gun_open(12345, Config), + #{ + host := "localhost", + port := 12345, + tcp_opts := _, + timeout := _, + lookup_info := #{ + ip_addresses := [_|_], + port := 12345, + tcp_module := _, + tcp_opts := _ + } + } = do_receive_event(domain_lookup_end), + gun:close(Pid). + +%% connect_start/connect_end. + +connect_start(Config) -> + doc("Confirm that the connect_start event callback is called."), + {ok, Pid, _} = do_gun_open(12345, Config), + #{ + lookup_info := #{ + ip_addresses := [_|_], + port := 12345, + tcp_module := _, + tcp_opts := _ + }, + timeout := _ + } = do_receive_event(?FUNCTION_NAME), + gun:close(Pid). + +connect_end_error(Config) -> + doc("Confirm that the connect_end event callback is called on connect failure."), + {ok, Pid, _} = do_gun_open(12345, Config), + #{ + lookup_info := #{ + ip_addresses := [_|_], + port := 12345, + tcp_module := _, + tcp_opts := _ + }, + timeout := _, + error := _ + } = do_receive_event(connect_end), + gun:close(Pid). + +connect_end_ok_tcp(Config) -> + doc("Confirm that the connect_end event callback is called on connect success with TCP."), + {ok, Pid, OriginPort} = do_gun_open(Config), + {ok, Protocol} = gun:await_up(Pid), + #{ + lookup_info := #{ + ip_addresses := [_|_], + port := OriginPort, + tcp_module := _, + tcp_opts := _ + }, + timeout := _, + socket := _, + protocol := Protocol + } = do_receive_event(connect_end), + gun:close(Pid). + +connect_end_ok_tls(Config) -> + doc("Confirm that the connect_end event callback is called on connect success with TLS."), + {ok, Pid, OriginPort} = do_gun_open_tls(Config), + Event = #{ + lookup_info := #{ + ip_addresses := [_|_], + port := OriginPort, + tcp_module := _, + tcp_opts := _ + }, + timeout := _, + socket := _ + } = do_receive_event(connect_end), + false = maps:is_key(protocol, Event), + gun:close(Pid). + +%% tls_handshake_start/tls_handshake_end. + +tls_handshake_start(Config) -> + doc("Confirm that the tls_handshake_start event callback is called."), + {ok, Pid, _} = do_gun_open_tls(Config), + #{ + socket := Socket, + tls_opts := _, + timeout := _ + } = do_receive_event(?FUNCTION_NAME), + true = is_port(Socket), + gun:close(Pid). + +tls_handshake_end_error(Config) -> + doc("Confirm that the tls_handshake_end event callback is called on TLS handshake error."), + %% We use the wrong port on purpose to trigger a handshake error. + OriginPort = config(tcp_origin_port, Config), + Opts = #{ + event_handler => {?MODULE, self()}, + protocols => [config(name, config(tc_group_properties, Config))], + transport => tls, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}] + }, + {ok, Pid} = gun:open("localhost", OriginPort, Opts), + #{ + socket := Socket, + tls_opts := _, + timeout := _, + error := {tls_alert, _} + } = do_receive_event(tls_handshake_end), + true = is_port(Socket), + gun:close(Pid). + +tls_handshake_end_ok(Config) -> + doc("Confirm that the tls_handshake_end event callback is called on TLS handshake success."), + {ok, Pid, _} = do_gun_open_tls(Config), + {ok, Protocol} = gun:await_up(Pid), + #{ + socket := Socket, + tls_opts := _, + timeout := _, + protocol := Protocol + } = do_receive_event(tls_handshake_end), + true = is_tuple(Socket), + gun:close(Pid). + +tls_handshake_start_tcp_connect_tls(Config) -> + doc("Confirm that the tls_handshake_start event callback is called " + "when using CONNECT to a TLS server via a TCP proxy."), + OriginPort = config(tls_origin_port, Config), + Protocol = config(name, config(tc_group_properties, Config)), + {ok, ProxyPid, ProxyPort} = do_proxy_start(Protocol, tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [Protocol], + transport => tcp + }), + {ok, Protocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(Protocol, ProxyPid), + StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + transport => tls, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}] + }), + ReplyTo = self(), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo, + socket := Socket, + tls_opts := _, + timeout := _ + } = do_receive_event(tls_handshake_start), + true = case Protocol of + http -> is_port(Socket); + http2 -> is_map(Socket) + end, + gun:close(ConnPid). + +tls_handshake_end_error_tcp_connect_tls(Config) -> + doc("Confirm that the tls_handshake_end event callback is called on TLS handshake error " + "when using CONNECT to a TLS server via a TCP proxy."), + %% We use the wrong port on purpose to trigger a handshake error. + OriginPort = config(tcp_origin_port, Config), + Protocol = config(name, config(tc_group_properties, Config)), + {ok, ProxyPid, ProxyPort} = do_proxy_start(Protocol, tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [Protocol], + transport => tcp + }), + {ok, Protocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(Protocol, ProxyPid), + StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + transport => tls, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}] + }), + ReplyTo = self(), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo, + socket := Socket, + tls_opts := _, + timeout := _, + error := {tls_alert, _} + } = do_receive_event(tls_handshake_end), + true = case Protocol of + http -> is_port(Socket); + http2 -> is_map(Socket) + end, + gun:close(ConnPid). + +tls_handshake_end_ok_tcp_connect_tls(Config) -> + doc("Confirm that the tls_handshake_end event callback is called on TLS handshake success " + "when using CONNECT to a TLS server via a TCP proxy."), + OriginPort = config(tls_origin_port, Config), + Protocol = config(name, config(tc_group_properties, Config)), + {ok, ProxyPid, ProxyPort} = do_proxy_start(Protocol, tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [Protocol], + transport => tcp + }), + {ok, Protocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(Protocol, ProxyPid), + StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + transport => tls, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}] + }), + ReplyTo = self(), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo, + socket := Socket, + tls_opts := _, + timeout := _, + protocol := http2 + } = do_receive_event(tls_handshake_end), + true = case Protocol of + http -> is_tuple(Socket); + http2 -> is_pid(Socket) + end, + gun:close(ConnPid). + +tls_handshake_start_tls_connect_tls(Config) -> + doc("Confirm that the tls_handshake_start event callback is called " + "when using CONNECT to a TLS server via a TLS proxy."), + OriginPort = config(tls_origin_port, Config), + Protocol = config(name, config(tc_group_properties, Config)), + {ok, ProxyPid, ProxyPort} = do_proxy_start(Protocol, tls), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [Protocol], + transport => tls, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}] + }), + {ok, Protocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(Protocol, ProxyPid), + %% We skip the TLS handshake event to the TLS proxy. + _ = do_receive_event(tls_handshake_start), + StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + transport => tls, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}] + }), + ReplyTo = self(), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo, + socket := Socket, + tls_opts := _, + timeout := _ + } = do_receive_event(tls_handshake_start), + true = case Protocol of + http -> is_tuple(Socket); + http2 -> is_map(Socket) + end, + gun:close(ConnPid). + +tls_handshake_end_error_tls_connect_tls(Config) -> + doc("Confirm that the tls_handshake_end event callback is called on TLS handshake error " + "when using CONNECT to a TLS server via a TLS proxy."), + %% We use the wrong port on purpose to trigger a handshake error. + OriginPort = config(tcp_origin_port, Config), + Protocol = config(name, config(tc_group_properties, Config)), + {ok, ProxyPid, ProxyPort} = do_proxy_start(Protocol, tls), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [Protocol], + transport => tls, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}] + }), + {ok, Protocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(Protocol, ProxyPid), + %% We skip the TLS handshake event to the TLS proxy. + _ = do_receive_event(tls_handshake_end), + StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + transport => tls, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}] + }), + ReplyTo = self(), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo, + socket := Socket, + tls_opts := _, + timeout := _, + error := {tls_alert, _} + } = do_receive_event(tls_handshake_end), + true = case Protocol of + http -> is_tuple(Socket); + http2 -> is_map(Socket) + end, + gun:close(ConnPid). + +tls_handshake_end_ok_tls_connect_tls(Config) -> + doc("Confirm that the tls_handshake_end event callback is called on TLS handshake success " + "when using CONNECT to a TLS server via a TLS proxy."), + OriginPort = config(tls_origin_port, Config), + Protocol = config(name, config(tc_group_properties, Config)), + {ok, ProxyPid, ProxyPort} = do_proxy_start(Protocol, tls), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [Protocol], + transport => tls, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}] + }), + {ok, Protocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(Protocol, ProxyPid), + %% We skip the TLS handshake event to the TLS proxy. + _ = do_receive_event(tls_handshake_end), + StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + transport => tls, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}] + }), + ReplyTo = self(), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo, + socket := Socket, + tls_opts := _, + timeout := _, + protocol := http2 + } = do_receive_event(tls_handshake_end), + true = is_pid(Socket), + gun:close(ConnPid). + +%% request_start/request_headers/request_end. + +request_start(Config) -> + doc("Confirm that the request_start event callback is called."), + do_request_event(Config, ?FUNCTION_NAME), + do_request_event_headers(Config, ?FUNCTION_NAME). + +request_headers(Config) -> + doc("Confirm that the request_headers event callback is called."), + do_request_event(Config, ?FUNCTION_NAME), + do_request_event_headers(Config, ?FUNCTION_NAME). + +do_request_event(Config, EventName) -> + {ok, Pid, OriginPort} = do_gun_open(Config), + {ok, _} = gun:await_up(Pid), + StreamRef = gun:get(Pid, "/"), + ReplyTo = self(), + Authority = iolist_to_binary([<<"localhost:">>, integer_to_list(OriginPort)]), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo, + function := request, + method := <<"GET">>, + authority := EventAuthority, + path := "/", + headers := [_|_] + } = do_receive_event(EventName), + Authority = iolist_to_binary(EventAuthority), + gun:close(Pid). + +do_request_event_headers(Config, EventName) -> + {ok, Pid, OriginPort} = do_gun_open(Config), + {ok, _} = gun:await_up(Pid), + StreamRef = gun:put(Pid, "/", [ + {<<"content-type">>, <<"text/plain">>} + ]), + ReplyTo = self(), + Authority = iolist_to_binary([<<"localhost:">>, integer_to_list(OriginPort)]), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo, + function := headers, + method := <<"PUT">>, + authority := EventAuthority, + path := "/", + headers := [_|_] + } = do_receive_event(EventName), + Authority = iolist_to_binary(EventAuthority), + gun:close(Pid). + +request_start_connect(Config) -> + doc("Confirm that the request_start event callback is called " + "for requests going through a CONNECT proxy."), + do_request_event_connect(Config, request_start), + do_request_event_headers_connect(Config, request_start). + +request_headers_connect(Config) -> + doc("Confirm that the request_headers event callback is called " + "for requests going through a CONNECT proxy."), + do_request_event_connect(Config, request_headers), + do_request_event_headers_connect(Config, request_headers). + +do_request_event_connect(Config, EventName) -> + OriginPort = config(tcp_origin_port, Config), + Authority = iolist_to_binary([<<"localhost:">>, integer_to_list(OriginPort)]), + Protocol = config(name, config(tc_group_properties, Config)), + ReplyTo = self(), + {ok, ProxyPid, ProxyPort} = do_proxy_start(Protocol, tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [Protocol] + }), + {ok, Protocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(Protocol, ProxyPid), + StreamRef1 = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + protocols => [Protocol] + }, []), + #{ + stream_ref := StreamRef1, + reply_to := ReplyTo, + function := connect, + method := <<"CONNECT">>, + authority := EventAuthority1, + headers := Headers1 + } = do_receive_event(EventName), + Authority = iolist_to_binary(EventAuthority1), + %% Gun doesn't send headers with an HTTP/2 CONNECT request + %% so we only check that the headers are given as a list. + true = is_list(Headers1), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {up, Protocol} = gun:await(ConnPid, StreamRef1), + StreamRef2 = gun:get(ConnPid, "/", [], #{tunnel => StreamRef1}), + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo, + function := request, + method := <<"GET">>, + authority := EventAuthority2, + path := "/", + headers := [_|_] + } = do_receive_event(EventName), + Authority = iolist_to_binary(EventAuthority2), + gun:close(ConnPid). + +do_request_event_headers_connect(Config, EventName) -> + OriginPort = config(tcp_origin_port, Config), + Authority = iolist_to_binary([<<"localhost:">>, integer_to_list(OriginPort)]), + Protocol = config(name, config(tc_group_properties, Config)), + ReplyTo = self(), + {ok, ProxyPid, ProxyPort} = do_proxy_start(Protocol, tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [Protocol] + }), + {ok, Protocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(Protocol, ProxyPid), + StreamRef1 = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + protocols => [Protocol] + }, []), + #{ + stream_ref := StreamRef1, + reply_to := ReplyTo, + function := connect, + method := <<"CONNECT">>, + authority := EventAuthority1, + headers := Headers1 + } = do_receive_event(EventName), + Authority = iolist_to_binary(EventAuthority1), + %% Gun doesn't send headers with an HTTP/2 CONNECT request + %% so we only check that the headers are given as a list. + true = is_list(Headers1), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {up, Protocol} = gun:await(ConnPid, StreamRef1), + StreamRef2 = gun:put(ConnPid, "/", [ + {<<"content-type">>, <<"text/plain">>} + ], #{tunnel => StreamRef1}), + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo, + function := headers, + method := <<"PUT">>, + authority := EventAuthority2, + path := "/", + headers := [_|_] + } = do_receive_event(EventName), + Authority = iolist_to_binary(EventAuthority2), + gun:close(ConnPid). + +request_end(Config) -> + doc("Confirm that the request_end event callback is called."), + do_request_end(Config, ?FUNCTION_NAME), + do_request_end_headers(Config, ?FUNCTION_NAME), + do_request_end_headers_content_length(Config, ?FUNCTION_NAME), + do_request_end_headers_content_length_0(Config, ?FUNCTION_NAME). + +do_request_end(Config, EventName) -> + {ok, Pid, _} = do_gun_open(Config), + {ok, _} = gun:await_up(Pid), + StreamRef = gun:get(Pid, "/"), + ReplyTo = self(), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo + } = do_receive_event(EventName), + gun:close(Pid). + +do_request_end_headers(Config, EventName) -> + {ok, Pid, _} = do_gun_open(Config), + {ok, _} = gun:await_up(Pid), + StreamRef = gun:put(Pid, "/", [ + {<<"content-type">>, <<"text/plain">>} + ]), + gun:data(Pid, StreamRef, nofin, <<"Hello ">>), + gun:data(Pid, StreamRef, fin, <<"world!">>), + ReplyTo = self(), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo + } = do_receive_event(EventName), + gun:close(Pid). + +do_request_end_headers_content_length(Config, EventName) -> + {ok, Pid, _} = do_gun_open(Config), + {ok, _} = gun:await_up(Pid), + StreamRef = gun:put(Pid, "/", [ + {<<"content-type">>, <<"text/plain">>}, + {<<"content-length">>, <<"12">>} + ]), + gun:data(Pid, StreamRef, nofin, <<"Hello ">>), + gun:data(Pid, StreamRef, fin, <<"world!">>), + ReplyTo = self(), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo + } = do_receive_event(EventName), + gun:close(Pid). + +do_request_end_headers_content_length_0(Config, EventName) -> + {ok, Pid, _} = do_gun_open(Config), + {ok, _} = gun:await_up(Pid), + StreamRef = gun:put(Pid, "/", [ + {<<"content-type">>, <<"text/plain">>}, + {<<"content-length">>, <<"0">>} + ]), + gun:data(Pid, StreamRef, fin, <<>>), + ReplyTo = self(), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo + } = do_receive_event(EventName), + gun:close(Pid). + +request_end_connect(Config) -> + doc("Confirm that the request_end event callback is called " + "for requests going through a CONNECT proxy."), + do_request_end_connect(Config, request_end), + do_request_end_headers_connect(Config, request_end), + do_request_end_headers_content_length_connect(Config, request_end), + do_request_end_headers_content_length_0_connect(Config, request_end). + +do_request_end_connect(Config, EventName) -> + OriginPort = config(tcp_origin_port, Config), + Protocol = config(name, config(tc_group_properties, Config)), + ReplyTo = self(), + {ok, ProxyPid, ProxyPort} = do_proxy_start(Protocol, tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [Protocol] + }), + {ok, Protocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(Protocol, ProxyPid), + StreamRef1 = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + protocols => [Protocol] + }, []), + #{ + stream_ref := StreamRef1, + reply_to := ReplyTo + } = do_receive_event(EventName), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {up, Protocol} = gun:await(ConnPid, StreamRef1), + StreamRef2 = gun:get(ConnPid, "/", [], #{tunnel => StreamRef1}), + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo + } = do_receive_event(EventName), + gun:close(ConnPid). + +do_request_end_headers_connect(Config, EventName) -> + OriginPort = config(tcp_origin_port, Config), + Protocol = config(name, config(tc_group_properties, Config)), + ReplyTo = self(), + {ok, ProxyPid, ProxyPort} = do_proxy_start(Protocol, tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [Protocol] + }), + {ok, Protocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(Protocol, ProxyPid), + StreamRef1 = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + protocols => [Protocol] + }, []), + #{ + stream_ref := StreamRef1, + reply_to := ReplyTo + } = do_receive_event(EventName), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {up, Protocol} = gun:await(ConnPid, StreamRef1), + StreamRef2 = gun:put(ConnPid, "/", [ + {<<"content-type">>, <<"text/plain">>} + ], #{tunnel => StreamRef1}), + gun:data(ConnPid, StreamRef2, nofin, <<"Hello ">>), + gun:data(ConnPid, StreamRef2, fin, <<"world!">>), + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo + } = do_receive_event(EventName), + gun:close(ConnPid). + +do_request_end_headers_content_length_connect(Config, EventName) -> + OriginPort = config(tcp_origin_port, Config), + Protocol = config(name, config(tc_group_properties, Config)), + ReplyTo = self(), + {ok, ProxyPid, ProxyPort} = do_proxy_start(Protocol, tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [Protocol] + }), + {ok, Protocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(Protocol, ProxyPid), + StreamRef1 = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + protocols => [Protocol] + }, []), + #{ + stream_ref := StreamRef1, + reply_to := ReplyTo + } = do_receive_event(EventName), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {up, Protocol} = gun:await(ConnPid, StreamRef1), + StreamRef2 = gun:put(ConnPid, "/", [ + {<<"content-type">>, <<"text/plain">>}, + {<<"content-length">>, <<"12">>} + ], #{tunnel => StreamRef1}), + gun:data(ConnPid, StreamRef2, nofin, <<"Hello ">>), + gun:data(ConnPid, StreamRef2, fin, <<"world!">>), + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo + } = do_receive_event(EventName), + gun:close(ConnPid). + +do_request_end_headers_content_length_0_connect(Config, EventName) -> + OriginPort = config(tcp_origin_port, Config), + Protocol = config(name, config(tc_group_properties, Config)), + ReplyTo = self(), + {ok, ProxyPid, ProxyPort} = do_proxy_start(Protocol, tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [Protocol] + }), + {ok, Protocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(Protocol, ProxyPid), + StreamRef1 = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + protocols => [Protocol] + }, []), + #{ + stream_ref := StreamRef1, + reply_to := ReplyTo + } = do_receive_event(EventName), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {up, Protocol} = gun:await(ConnPid, StreamRef1), + StreamRef2 = gun:put(ConnPid, "/", [ + {<<"content-type">>, <<"text/plain">>}, + {<<"content-length">>, <<"0">>} + ], #{tunnel => StreamRef1}), + gun:data(ConnPid, StreamRef2, fin, <<>>), + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo + } = do_receive_event(EventName), + gun:close(ConnPid). + +%% push_promise_start/push_promise_end. + +push_promise_start(Config) -> + doc("Confirm that the push_promise_start event callback is called."), + {ok, Pid, _} = do_gun_open(Config), + {ok, _} = gun:await_up(Pid), + StreamRef = gun:get(Pid, "/push"), + ReplyTo = self(), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo + } = do_receive_event(?FUNCTION_NAME), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo + } = do_receive_event(?FUNCTION_NAME), + gun:close(Pid). + +push_promise_start_connect(Config) -> + doc("Confirm that the push_promise_start event callback is called " + "for requests going through a CONNECT proxy."), + do_push_promise_start_connect(Config, http), + do_push_promise_start_connect(Config, http2). + +do_push_promise_start_connect(Config, ProxyProtocol) -> + OriginPort = config(tcp_origin_port, Config), + {ok, ProxyPid, ProxyPort} = do_proxy_start(ProxyProtocol, tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [ProxyProtocol] + }), + {ok, ProxyProtocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(ProxyProtocol, ProxyPid), + StreamRef1 = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + protocols => [http2] + }, []), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {up, http2} = gun:await(ConnPid, StreamRef1), + StreamRef2 = gun:get(ConnPid, "/push", [], #{tunnel => StreamRef1}), + ReplyTo = self(), + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo + } = do_receive_event(push_promise_start), + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo + } = do_receive_event(push_promise_start), + gun:close(ConnPid). + +push_promise_end(Config) -> + doc("Confirm that the push_promise_end event callback is called."), + {ok, Pid, _} = do_gun_open(Config), + {ok, _} = gun:await_up(Pid), + StreamRef = gun:get(Pid, "/push"), + ReplyTo = self(), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo, + promised_stream_ref := _, + method := <<"GET">>, + uri := <<"http://",_/bits>>, + headers := [_|_] + } = do_receive_event(?FUNCTION_NAME), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo, + promised_stream_ref := _, + method := <<"GET">>, + uri := <<"http://",_/bits>>, + headers := [_|_] + } = do_receive_event(?FUNCTION_NAME), + gun:close(Pid). + +push_promise_end_connect(Config) -> + doc("Confirm that the push_promise_end event callback is called " + "for requests going through a CONNECT proxy."), + do_push_promise_end_connect(Config, http), + do_push_promise_end_connect(Config, http2). + +do_push_promise_end_connect(Config, ProxyProtocol) -> + OriginPort = config(tcp_origin_port, Config), + {ok, ProxyPid, ProxyPort} = do_proxy_start(ProxyProtocol, tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [ProxyProtocol] + }), + {ok, ProxyProtocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(ProxyProtocol, ProxyPid), + StreamRef1 = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + protocols => [http2] + }, []), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {up, http2} = gun:await(ConnPid, StreamRef1), + StreamRef2 = gun:get(ConnPid, "/push", [], #{tunnel => StreamRef1}), + ReplyTo = self(), + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo, + promised_stream_ref := [StreamRef1|_], + method := <<"GET">>, + uri := <<"http://",_/bits>>, + headers := [_|_] + } = do_receive_event(push_promise_end), + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo, + promised_stream_ref := [StreamRef1|_], + method := <<"GET">>, + uri := <<"http://",_/bits>>, + headers := [_|_] + } = do_receive_event(push_promise_end), + gun:close(ConnPid). + +push_promise_followed_by_response(Config) -> + doc("Confirm that the push_promise_end event callbacks are followed by response_start."), + {ok, Pid, _} = do_gun_open(Config), + {ok, _} = gun:await_up(Pid), + _ = gun:get(Pid, "/push"), + #{promised_stream_ref := PromisedStreamRef} = do_receive_event(push_promise_end), + #{stream_ref := StreamRef1} = do_receive_event(response_start), + #{stream_ref := StreamRef2} = do_receive_event(response_start), + #{stream_ref := StreamRef3} = do_receive_event(response_start), + true = lists:member(PromisedStreamRef, [StreamRef1, StreamRef2, StreamRef3]), + gun:close(Pid). + +%% response_start/response_inform/response_headers/response_trailers/response_end. + +response_start(Config) -> + doc("Confirm that the response_start event callback is called."), + {ok, Pid, _} = do_gun_open(Config), + {ok, _} = gun:await_up(Pid), + StreamRef = gun:get(Pid, "/"), + ReplyTo = self(), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo + } = do_receive_event(?FUNCTION_NAME), + gun:close(Pid). + +response_start_connect(Config) -> + doc("Confirm that the response_start event callback is called " + "for requests going through a CONNECT proxy."), + OriginPort = config(tcp_origin_port, Config), + Protocol = config(name, config(tc_group_properties, Config)), + ReplyTo = self(), + {ok, ProxyPid, ProxyPort} = do_proxy_start(Protocol, tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [Protocol] + }), + {ok, Protocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(Protocol, ProxyPid), + StreamRef1 = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + protocols => [Protocol] + }, []), + #{ + stream_ref := StreamRef1, + reply_to := ReplyTo + } = do_receive_event(response_start), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {up, Protocol} = gun:await(ConnPid, StreamRef1), + StreamRef2 = gun:get(ConnPid, "/", [], #{tunnel => StreamRef1}), + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo + } = do_receive_event(response_start), + gun:close(ConnPid). + +response_inform(Config) -> + doc("Confirm that the response_inform event callback is called."), + {ok, Pid, _} = do_gun_open(Config), + {ok, _} = gun:await_up(Pid), + StreamRef = gun:get(Pid, "/inform"), + ReplyTo = self(), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo, + status := 103, + headers := [_|_] + } = do_receive_event(?FUNCTION_NAME), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo, + status := 103, + headers := [_|_] + } = do_receive_event(?FUNCTION_NAME), + gun:close(Pid). + +response_inform_connect(Config) -> + doc("Confirm that the response_inform event callback is called " + "for requests going through a CONNECT proxy."), + OriginPort = config(tcp_origin_port, Config), + Protocol = config(name, config(tc_group_properties, Config)), + ReplyTo = self(), + {ok, ProxyPid, ProxyPort} = do_proxy_start(Protocol, tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [Protocol] + }), + {ok, Protocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(Protocol, ProxyPid), + StreamRef1 = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + protocols => [Protocol] + }, []), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {up, Protocol} = gun:await(ConnPid, StreamRef1), + StreamRef2 = gun:get(ConnPid, "/inform", [], #{tunnel => StreamRef1}), + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo, + status := 103, + headers := [_|_] + } = do_receive_event(response_inform), + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo, + status := 103, + headers := [_|_] + } = do_receive_event(response_inform), + gun:close(ConnPid). + +response_headers(Config) -> + doc("Confirm that the response_headers event callback is called."), + {ok, Pid, _} = do_gun_open(Config), + {ok, _} = gun:await_up(Pid), + StreamRef = gun:get(Pid, "/"), + ReplyTo = self(), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo, + status := 200, + headers := [_|_] + } = do_receive_event(?FUNCTION_NAME), + gun:close(Pid). + +response_headers_connect(Config) -> + doc("Confirm that the response_headers event callback is called " + "for requests going through a CONNECT proxy."), + OriginPort = config(tcp_origin_port, Config), + Protocol = config(name, config(tc_group_properties, Config)), + ReplyTo = self(), + {ok, ProxyPid, ProxyPort} = do_proxy_start(Protocol, tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [Protocol] + }), + {ok, Protocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(Protocol, ProxyPid), + StreamRef1 = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + protocols => [Protocol] + }, []), + #{ + stream_ref := StreamRef1, + reply_to := ReplyTo, + status := 200, + headers := Headers1 + } = do_receive_event(response_headers), + true = is_list(Headers1), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {up, Protocol} = gun:await(ConnPid, StreamRef1), + StreamRef2 = gun:get(ConnPid, "/", [], #{tunnel => StreamRef1}), + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo, + status := 200, + headers := [_|_] + } = do_receive_event(response_headers), + gun:close(ConnPid). + +response_trailers(Config) -> + doc("Confirm that the response_trailers event callback is called " + "for requests going through a CONNECT proxy."), + OriginPort = config(tcp_origin_port, Config), + Protocol = config(name, config(tc_group_properties, Config)), + ReplyTo = self(), + {ok, ProxyPid, ProxyPort} = do_proxy_start(Protocol, tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [Protocol] + }), + {ok, Protocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(Protocol, ProxyPid), + StreamRef1 = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + protocols => [Protocol] + }, []), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {up, Protocol} = gun:await(ConnPid, StreamRef1), + StreamRef2 = gun:get(ConnPid, "/trailers", [{<<"te">>, <<"trailers">>}], #{tunnel => StreamRef1}), + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo, + headers := [_|_] + } = do_receive_event(response_trailers), + gun:close(ConnPid). + +response_end(Config) -> + doc("Confirm that the response_end event callback is called."), + do_response_end(Config, ?FUNCTION_NAME, "/"), + do_response_end(Config, ?FUNCTION_NAME, "/empty"), + do_response_end(Config, ?FUNCTION_NAME, "/stream"), + do_response_end(Config, ?FUNCTION_NAME, "/trailers"). + +do_response_end(Config, EventName, Path) -> + {ok, Pid, _} = do_gun_open(Config), + {ok, _} = gun:await_up(Pid), + StreamRef = gun:get(Pid, Path, [{<<"te">>, <<"trailers">>}]), + ReplyTo = self(), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo + } = do_receive_event(EventName), + gun:close(Pid). + +response_end_connect(Config) -> + doc("Confirm that the response_end event callback is called " + "for requests going through a CONNECT proxy."), + do_response_end_connect(Config, response_end, "/"), + do_response_end_connect(Config, response_end, "/empty"), + do_response_end_connect(Config, response_end, "/stream"), + do_response_end_connect(Config, response_end, "/trailers"). + +do_response_end_connect(Config, EventName, Path) -> + OriginPort = config(tcp_origin_port, Config), + Protocol = config(name, config(tc_group_properties, Config)), + ReplyTo = self(), + {ok, ProxyPid, ProxyPort} = do_proxy_start(Protocol, tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [Protocol] + }), + {ok, Protocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(Protocol, ProxyPid), + StreamRef1 = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + protocols => [Protocol] + }, []), + #{ + stream_ref := StreamRef1, + reply_to := ReplyTo + } = do_receive_event(EventName), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {up, Protocol} = gun:await(ConnPid, StreamRef1), + StreamRef2 = gun:get(ConnPid, Path, [{<<"te">>, <<"trailers">>}], #{tunnel => StreamRef1}), + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo + } = do_receive_event(EventName), + gun:close(ConnPid). + +http1_response_end_body_close(Config) -> + doc("Confirm that the response_end event callback is called " + "when using HTTP/1.0 and the content-length header is not set."), + OriginPort = config(tcp_origin_port, Config), + Opts = #{ + event_handler => {?MODULE, self()}, + http_opts => #{version => 'HTTP/1.0'}, + protocols => [config(name, config(tc_group_properties, Config))] + }, + {ok, Pid} = gun:open("localhost", OriginPort, Opts), + {ok, _} = gun:await_up(Pid), + StreamRef = gun:get(Pid, "/stream"), + ReplyTo = self(), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo + } = do_receive_event(response_end), + gun:close(Pid). + +%% @todo Figure out how to test both this and TLS handshake errors. Maybe a proxy option? +%response_end_body_close_connect(Config) -> +% doc("Confirm that the response_end event callback is called " +% "when using HTTP/1.0 and the content-length header is not set " +% "for requests going through a CONNECT proxy."), +% OriginPort = config(tcp_origin_port, Config), +% Protocol = config(name, config(tc_group_properties, Config)), +% ReplyTo = self(), +% {ok, ProxyPid, ProxyPort} = do_proxy_start(Protocol, tcp), +% {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ +% event_handler => {?MODULE, self()}, +% protocols => [Protocol] +% }), +% {ok, Protocol} = gun:await_up(ConnPid), +% tunnel_SUITE:do_handshake_completed(Protocol, ProxyPid), +% StreamRef1 = gun:connect(ConnPid, #{ +% host => "localhost", +% port => OriginPort, +% protocols => [{http, #{version => 'HTTP/1.0'}}] +% }, []), +% #{ +% stream_ref := StreamRef1, +% reply_to := ReplyTo +% } = do_receive_event(response_end), +% {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), +% {up, http} = gun:await(ConnPid, StreamRef1), +% StreamRef2 = gun:get(ConnPid, "/stream", [], #{tunnel => StreamRef1}), +% #{ +% stream_ref := StreamRef2, +% reply_to := ReplyTo +% } = do_receive_event(response_end), +% gun:close(ConnPid). + +%% ws_upgrade. + +ws_upgrade(Config) -> + doc("Confirm that the ws_upgrade event callback is called."), + Protocol = config(name, config(tc_group_properties, Config)), + {ok, Pid, _} = do_gun_open(Config), + {ok, Protocol} = gun:await_up(Pid), + ws_SUITE:do_await_enable_connect_protocol(Protocol, Pid), + StreamRef = gun:ws_upgrade(Pid, "/ws"), + ReplyTo = self(), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo, + opts := #{} + } = do_receive_event(?FUNCTION_NAME), + gun:close(Pid). + +ws_upgrade_connect(Config) -> + doc("Confirm that the ws_upgrade event callback is called " + "for requests going through a CONNECT proxy."), + do_ws_upgrade_connect(Config, http), + do_ws_upgrade_connect(Config, http2). + +do_ws_upgrade_connect(Config, ProxyProtocol) -> + OriginPort = config(tcp_origin_port, Config), + OriginProtocol = config(name, config(tc_group_properties, Config)), + ReplyTo = self(), + {ok, ProxyPid, ProxyPort} = do_proxy_start(ProxyProtocol, tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [ProxyProtocol] + }), + {ok, ProxyProtocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(ProxyProtocol, ProxyPid), + StreamRef1 = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + protocols => [case OriginProtocol of + http -> http; + http2 -> {http2, #{notify_settings_changed => true}} + end] + }, []), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {up, OriginProtocol} = gun:await(ConnPid, StreamRef1), + ws_SUITE:do_await_enable_connect_protocol(OriginProtocol, ConnPid), + StreamRef2 = gun:ws_upgrade(ConnPid, "/ws", [], #{tunnel => StreamRef1}), + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo, + opts := #{} + } = do_receive_event(ws_upgrade), + gun:close(ConnPid). + +ws_upgrade_all_events(Config) -> + doc("Confirm that a Websocket upgrade triggers all relevant events."), + Protocol = config(name, config(tc_group_properties, Config)), + {ok, Pid, OriginPort} = do_gun_open(Config), + {ok, Protocol} = gun:await_up(Pid), + ws_SUITE:do_await_enable_connect_protocol(Protocol, Pid), + StreamRef = gun:ws_upgrade(Pid, "/ws"), + ReplyTo = self(), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo, + opts := #{} + } = do_receive_event(ws_upgrade), + Authority = iolist_to_binary([<<"localhost:">>, integer_to_list(OriginPort)]), + Method = case Protocol of + http -> <<"GET">>; + http2 -> <<"CONNECT">> + end, + #{ + stream_ref := StreamRef, + reply_to := ReplyTo, + function := ws_upgrade, + method := Method, + authority := EventAuthority1, + path := "/ws", + headers := [_|_] + } = do_receive_event(request_start), + Authority = iolist_to_binary(EventAuthority1), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo, + function := ws_upgrade, + method := Method, + authority := EventAuthority2, + path := "/ws", + headers := [_|_] + } = do_receive_event(request_headers), + Authority = iolist_to_binary(EventAuthority2), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo + } = do_receive_event(request_end), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo + } = do_receive_event(response_start), + _ = case Protocol of + http -> + #{ + stream_ref := StreamRef, + reply_to := ReplyTo, + status := 101, + headers := [_|_] + } = do_receive_event(response_inform); + http2 -> + #{ + stream_ref := StreamRef, + reply_to := ReplyTo, + status := 200, + headers := [_|_] + } = do_receive_event(response_headers), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo + } = do_receive_event(response_end) + end, + #{ + stream_ref := StreamRef, + protocol := ws + } = do_receive_event(protocol_changed), + gun:close(Pid). + +ws_upgrade_all_events_connect(Config) -> + doc("Confirm that a Websocket upgrade triggers all relevant events " + "for requests going through a CONNECT proxy."), + do_ws_upgrade_all_events_connect(Config, http), + do_ws_upgrade_all_events_connect(Config, http2). + +do_ws_upgrade_all_events_connect(Config, ProxyProtocol) -> + OriginPort = config(tcp_origin_port, Config), + OriginProtocol = config(name, config(tc_group_properties, Config)), + ReplyTo = self(), + {ok, ProxyPid, ProxyPort} = do_proxy_start(ProxyProtocol, tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [ProxyProtocol] + }), + {ok, ProxyProtocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(ProxyProtocol, ProxyPid), + StreamRef1 = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + protocols => [case OriginProtocol of + http -> http; + http2 -> {http2, #{notify_settings_changed => true}} + end] + }, []), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {up, OriginProtocol} = gun:await(ConnPid, StreamRef1), + ws_SUITE:do_await_enable_connect_protocol(OriginProtocol, ConnPid), + %% Skip all CONNECT-related events that may conflict. + _ = do_receive_event(request_start), + _ = do_receive_event(request_headers), + _ = do_receive_event(request_end), + _ = do_receive_event(response_start), + _ = do_receive_event(response_headers), + _ = do_receive_event(response_end), + _ = do_receive_event(protocol_changed), + %% Check the Websocket events. + StreamRef2 = gun:ws_upgrade(ConnPid, "/ws", [], #{tunnel => StreamRef1}), + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo, + opts := #{} + } = do_receive_event(ws_upgrade), + Authority = iolist_to_binary([<<"localhost:">>, integer_to_list(OriginPort)]), + Method = case OriginProtocol of + http -> <<"GET">>; + http2 -> <<"CONNECT">> + end, + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo, + function := ws_upgrade, + method := Method, + authority := EventAuthority1, + path := "/ws", + headers := [_|_] + } = do_receive_event(request_start), + Authority = iolist_to_binary(EventAuthority1), + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo, + function := ws_upgrade, + method := Method, + authority := EventAuthority2, + path := "/ws", + headers := [_|_] + } = do_receive_event(request_headers), + Authority = iolist_to_binary(EventAuthority2), + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo + } = do_receive_event(request_end), + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo + } = do_receive_event(response_start), + _ = case OriginProtocol of + http -> + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo, + status := 101, + headers := [_|_] + } = do_receive_event(response_inform); + http2 -> + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo, + status := 200, + headers := [_|_] + } = do_receive_event(response_headers), + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo + } = do_receive_event(response_end) + end, + #{ + stream_ref := StreamRef2, + protocol := ws + } = do_receive_event(protocol_changed), + gun:close(ConnPid). + +%% ws_recv_frame_start/ws_recv_frame_header/ws_recv_frame_end. + +ws_recv_frame_start(Config) -> + doc("Confirm that the ws_recv_frame_start event callback is called."), + Protocol = config(name, config(tc_group_properties, Config)), + {ok, Pid, _} = do_gun_open(Config), + {ok, Protocol} = gun:await_up(Pid), + ws_SUITE:do_await_enable_connect_protocol(Protocol, Pid), + StreamRef = gun:ws_upgrade(Pid, "/ws"), + {upgrade, [<<"websocket">>], _} = gun:await(Pid, StreamRef), + gun:ws_send(Pid, StreamRef, {text, <<"Hello!">>}), + ReplyTo = self(), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo, + frag_state := undefined, + extensions := #{} + } = do_receive_event(?FUNCTION_NAME), + gun:close(Pid). + +ws_recv_frame_start_connect(Config) -> + doc("Confirm that the ws_recv_frame_start event callback is called " + "for requests going through a CONNECT proxy."), + do_ws_recv_frame_start_connect(Config, http), + do_ws_recv_frame_start_connect(Config, http2). + +do_ws_recv_frame_start_connect(Config, ProxyProtocol) -> + OriginPort = config(tcp_origin_port, Config), + OriginProtocol = config(name, config(tc_group_properties, Config)), + ReplyTo = self(), + {ok, ProxyPid, ProxyPort} = do_proxy_start(ProxyProtocol, tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [ProxyProtocol] + }), + {ok, ProxyProtocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(ProxyProtocol, ProxyPid), + StreamRef1 = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + protocols => [case OriginProtocol of + http -> http; + http2 -> {http2, #{notify_settings_changed => true}} + end] + }, []), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {up, OriginProtocol} = gun:await(ConnPid, StreamRef1), + ws_SUITE:do_await_enable_connect_protocol(OriginProtocol, ConnPid), + StreamRef2 = gun:ws_upgrade(ConnPid, "/ws", [], #{tunnel => StreamRef1}), + {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef2), + gun:ws_send(ConnPid, StreamRef2, {text, <<"Hello!">>}), + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo, + frag_state := undefined, + extensions := #{} + } = do_receive_event(ws_recv_frame_start), + gun:close(ConnPid). + +ws_recv_frame_header(Config) -> + doc("Confirm that the ws_recv_frame_header event callback is called."), + Protocol = config(name, config(tc_group_properties, Config)), + {ok, Pid, _} = do_gun_open(Config), + {ok, Protocol} = gun:await_up(Pid), + ws_SUITE:do_await_enable_connect_protocol(Protocol, Pid), + StreamRef = gun:ws_upgrade(Pid, "/ws"), + {upgrade, [<<"websocket">>], _} = gun:await(Pid, StreamRef), + gun:ws_send(Pid, StreamRef, {text, <<"Hello!">>}), + ReplyTo = self(), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo, + frag_state := undefined, + extensions := #{}, + type := text, + rsv := <<0:3>>, + len := 6, + mask_key := _ + } = do_receive_event(?FUNCTION_NAME), + gun:close(Pid). + +ws_recv_frame_header_connect(Config) -> + doc("Confirm that the ws_recv_frame_header event callback is called " + "for requests going through a CONNECT proxy."), + do_ws_recv_frame_header_connect(Config, http), + do_ws_recv_frame_header_connect(Config, http2). + +do_ws_recv_frame_header_connect(Config, ProxyProtocol) -> + OriginPort = config(tcp_origin_port, Config), + OriginProtocol = config(name, config(tc_group_properties, Config)), + ReplyTo = self(), + {ok, ProxyPid, ProxyPort} = do_proxy_start(ProxyProtocol, tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [ProxyProtocol] + }), + {ok, ProxyProtocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(ProxyProtocol, ProxyPid), + StreamRef1 = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + protocols => [case OriginProtocol of + http -> http; + http2 -> {http2, #{notify_settings_changed => true}} + end] + }, []), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {up, OriginProtocol} = gun:await(ConnPid, StreamRef1), + ws_SUITE:do_await_enable_connect_protocol(OriginProtocol, ConnPid), + StreamRef2 = gun:ws_upgrade(ConnPid, "/ws", [], #{tunnel => StreamRef1}), + {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef2), + gun:ws_send(ConnPid, StreamRef2, {text, <<"Hello!">>}), + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo, + frag_state := undefined, + extensions := #{}, + type := text, + rsv := <<0:3>>, + len := 6, + mask_key := _ + } = do_receive_event(ws_recv_frame_header), + gun:close(ConnPid). + +ws_recv_frame_end(Config) -> + doc("Confirm that the ws_recv_frame_end event callback is called."), + Protocol = config(name, config(tc_group_properties, Config)), + {ok, Pid, _} = do_gun_open(Config), + {ok, Protocol} = gun:await_up(Pid), + ws_SUITE:do_await_enable_connect_protocol(Protocol, Pid), + StreamRef = gun:ws_upgrade(Pid, "/ws"), + {upgrade, [<<"websocket">>], _} = gun:await(Pid, StreamRef), + gun:ws_send(Pid, StreamRef, {text, <<"Hello!">>}), + ReplyTo = self(), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo, + extensions := #{}, + close_code := undefined, + payload := <<"Hello!">> + } = do_receive_event(?FUNCTION_NAME), + gun:close(Pid). + +ws_recv_frame_end_connect(Config) -> + doc("Confirm that the ws_recv_frame_end event callback is called " + "for requests going through a CONNECT proxy."), + do_ws_recv_frame_end_connect(Config, http), + do_ws_recv_frame_end_connect(Config, http2). + +do_ws_recv_frame_end_connect(Config, ProxyProtocol) -> + OriginPort = config(tcp_origin_port, Config), + OriginProtocol = config(name, config(tc_group_properties, Config)), + ReplyTo = self(), + {ok, ProxyPid, ProxyPort} = do_proxy_start(ProxyProtocol, tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [ProxyProtocol] + }), + {ok, ProxyProtocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(ProxyProtocol, ProxyPid), + StreamRef1 = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + protocols => [case OriginProtocol of + http -> http; + http2 -> {http2, #{notify_settings_changed => true}} + end] + }, []), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {up, OriginProtocol} = gun:await(ConnPid, StreamRef1), + ws_SUITE:do_await_enable_connect_protocol(OriginProtocol, ConnPid), + StreamRef2 = gun:ws_upgrade(ConnPid, "/ws", [], #{tunnel => StreamRef1}), + {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef2), + gun:ws_send(ConnPid, StreamRef2, {text, <<"Hello!">>}), + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo, + extensions := #{}, + close_code := undefined, + payload := <<"Hello!">> + } = do_receive_event(ws_recv_frame_end), + gun:close(ConnPid). + +%% ws_send_frame_start/ws_send_frame_end. + +ws_send_frame_start(Config) -> + doc("Confirm that the ws_send_frame_start event callback is called."), + do_ws_send_frame(Config, ?FUNCTION_NAME). + +ws_send_frame_end(Config) -> + doc("Confirm that the ws_send_frame_end event callback is called."), + do_ws_send_frame(Config, ?FUNCTION_NAME). + +do_ws_send_frame(Config, EventName) -> + Protocol = config(name, config(tc_group_properties, Config)), + {ok, Pid, _} = do_gun_open(Config), + {ok, Protocol} = gun:await_up(Pid), + ws_SUITE:do_await_enable_connect_protocol(Protocol, Pid), + StreamRef = gun:ws_upgrade(Pid, "/ws"), + {upgrade, [<<"websocket">>], _} = gun:await(Pid, StreamRef), + gun:ws_send(Pid, StreamRef, {text, <<"Hello!">>}), + ReplyTo = self(), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo, + extensions := #{}, + frame := {text, <<"Hello!">>} + } = do_receive_event(EventName), + gun:close(Pid). + +ws_send_frame_start_connect(Config) -> + doc("Confirm that the ws_send_frame_start event callback is called " + "for requests going through a CONNECT proxy."), + do_ws_send_frame_connect(Config, http, ws_send_frame_start), + do_ws_send_frame_connect(Config, http2, ws_send_frame_start). + +ws_send_frame_end_connect(Config) -> + doc("Confirm that the ws_send_frame_end event callback is called " + "for requests going through a CONNECT proxy."), + do_ws_send_frame_connect(Config, http, ws_send_frame_end), + do_ws_send_frame_connect(Config, http2, ws_send_frame_end). + +do_ws_send_frame_connect(Config, ProxyProtocol, EventName) -> + OriginPort = config(tcp_origin_port, Config), + OriginProtocol = config(name, config(tc_group_properties, Config)), + ReplyTo = self(), + {ok, ProxyPid, ProxyPort} = do_proxy_start(ProxyProtocol, tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [ProxyProtocol] + }), + {ok, ProxyProtocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(ProxyProtocol, ProxyPid), + StreamRef1 = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + protocols => [case OriginProtocol of + http -> http; + http2 -> {http2, #{notify_settings_changed => true}} + end] + }, []), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {up, OriginProtocol} = gun:await(ConnPid, StreamRef1), + ws_SUITE:do_await_enable_connect_protocol(OriginProtocol, ConnPid), + StreamRef2 = gun:ws_upgrade(ConnPid, "/ws", [], #{tunnel => StreamRef1}), + {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef2), + gun:ws_send(ConnPid, StreamRef2, {text, <<"Hello!">>}), + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo, + extensions := #{}, + frame := {text, <<"Hello!">>} + } = do_receive_event(EventName), + gun:close(ConnPid). + +%% protocol_changed. + +ws_protocol_changed(Config) -> + doc("Confirm that the protocol_changed event callback is called on Websocket upgrade success."), + Protocol = config(name, config(tc_group_properties, Config)), + {ok, Pid, _} = do_gun_open(Config), + {ok, Protocol} = gun:await_up(Pid), + ws_SUITE:do_await_enable_connect_protocol(Protocol, Pid), + _ = gun:ws_upgrade(Pid, "/ws"), + #{ + protocol := ws + } = do_receive_event(protocol_changed), + gun:close(Pid). + +ws_protocol_changed_connect(Config) -> + doc("Confirm that the protocol_changed event callback is called on Websocket upgrade success " + "for requests going through a CONNECT proxy."), + do_ws_protocol_changed_connect(Config, http), + do_ws_protocol_changed_connect(Config, http2). + +do_ws_protocol_changed_connect(Config, ProxyProtocol) -> + OriginPort = config(tcp_origin_port, Config), + OriginProtocol = config(name, config(tc_group_properties, Config)), + {ok, ProxyPid, ProxyPort} = do_proxy_start(ProxyProtocol, tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [ProxyProtocol] + }), + {ok, ProxyProtocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(ProxyProtocol, ProxyPid), + StreamRef1 = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + protocols => [case OriginProtocol of + http -> http; + http2 -> {http2, #{notify_settings_changed => true}} + end] + }, []), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {up, OriginProtocol} = gun:await(ConnPid, StreamRef1), + ws_SUITE:do_await_enable_connect_protocol(OriginProtocol, ConnPid), + #{ + stream_ref := StreamRef1, + protocol := OriginProtocol + } = do_receive_event(protocol_changed), + StreamRef2 = gun:ws_upgrade(ConnPid, "/ws", [], #{tunnel => StreamRef1}), + #{ + stream_ref := StreamRef2, + protocol := ws + } = do_receive_event(protocol_changed), + gun:close(ConnPid). + +protocol_changed_connect(Config) -> + doc("Confirm that the protocol_changed event callback is called on CONNECT success " + "when connecting through a TCP server via a TCP proxy."), + do_protocol_changed_connect(Config, http), + do_protocol_changed_connect(Config, http2). + +do_protocol_changed_connect(Config, OriginProtocol) -> + OriginPort = config(tcp_origin_port, Config), + ProxyProtocol = config(name, config(tc_group_properties, Config)), + {ok, ProxyPid, ProxyPort} = do_proxy_start(ProxyProtocol, tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [ProxyProtocol], + transport => tcp + }), + {ok, ProxyProtocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(ProxyProtocol, ProxyPid), + StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + protocols => [OriginProtocol] + }), + #{ + stream_ref := StreamRef, + protocol := OriginProtocol + } = do_receive_event(protocol_changed), + gun:close(ConnPid). + +protocol_changed_tls_connect(Config) -> + doc("Confirm that the protocol_changed event callback is called on CONNECT success " + "when connecting to a TLS server via a TLS proxy."), + do_protocol_changed_tls_connect(Config, http), + do_protocol_changed_tls_connect(Config, http2). + +do_protocol_changed_tls_connect(Config, OriginProtocol) -> + OriginPort = config(tls_origin_port, Config), + ProxyProtocol = config(name, config(tc_group_properties, Config)), + {ok, ProxyPid, ProxyPort} = do_proxy_start(ProxyProtocol, tls), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [ProxyProtocol], + transport => tls, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}] + }), + {ok, ProxyProtocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(ProxyProtocol, ProxyPid), + StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + transport => tls, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [OriginProtocol] + }), + #{ + stream_ref := StreamRef, + protocol := OriginProtocol + } = do_receive_event(protocol_changed), + gun:close(ConnPid). + +%% origin_changed. + +origin_changed_connect(Config) -> + doc("Confirm that the origin_changed event callback is called on CONNECT success."), + OriginPort = config(tcp_origin_port, Config), + ProxyProtocol = config(name, config(tc_group_properties, Config)), + {ok, ProxyPid, ProxyPort} = do_proxy_start(ProxyProtocol, tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [ProxyProtocol], + transport => tcp + }), + {ok, ProxyProtocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(ProxyProtocol, ProxyPid), + StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort + }), + Event = #{ + type := connect, + origin_scheme := <<"http">>, + origin_host := "localhost", + origin_port := OriginPort + } = do_receive_event(origin_changed), + case ProxyProtocol of + http -> ok; + http2 -> + #{stream_ref := StreamRef} = Event + end, + gun:close(ConnPid). + +origin_changed_connect_connect(Config) -> + doc("Confirm that the origin_changed event callback is called on CONNECT success " + "when performed inside another CONNECT tunnel."), + OriginPort = config(tcp_origin_port, Config), + ProxyProtocol = config(name, config(tc_group_properties, Config)), + {ok, Proxy1Pid, Proxy1Port} = do_proxy_start(ProxyProtocol, tcp), + {ok, Proxy2Pid, Proxy2Port} = do_proxy_start(ProxyProtocol, tcp), + {ok, ConnPid} = gun:open("localhost", Proxy1Port, #{ + event_handler => {?MODULE, self()}, + protocols => [ProxyProtocol], + transport => tcp + }), + {ok, ProxyProtocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(ProxyProtocol, Proxy1Pid), + StreamRef1 = gun:connect(ConnPid, #{ + host => "localhost", + port => Proxy2Port, + protocols => [ProxyProtocol] + }), + Event1 = #{ + type := connect, + origin_scheme := <<"http">>, + origin_host := "localhost", + origin_port := Proxy2Port + } = do_receive_event(origin_changed), + case ProxyProtocol of + http -> ok; + http2 -> + #{stream_ref := StreamRef1} = Event1 + end, + tunnel_SUITE:do_handshake_completed(ProxyProtocol, Proxy2Pid), + StreamRef2 = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort + }, [], #{tunnel => StreamRef1}), + Event2 = #{ + type := connect, + origin_scheme := <<"http">>, + origin_host := "localhost", + origin_port := OriginPort + } = do_receive_event(origin_changed), + case ProxyProtocol of + http -> ok; + http2 -> + #{stream_ref := StreamRef2} = Event2 + end, + gun:close(ConnPid). + +%% cancel. + +cancel(Config) -> + doc("Confirm that the cancel event callback is called when we cancel a stream."), + {ok, Pid, _} = do_gun_open(Config), + {ok, _} = gun:await_up(Pid), + StreamRef = gun:post(Pid, "/stream", []), + gun:cancel(Pid, StreamRef), + ReplyTo = self(), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo, + endpoint := local, + reason := cancel + } = do_receive_event(?FUNCTION_NAME), + gun:close(Pid). + +cancel_remote(Config) -> + doc("Confirm that the cancel event callback is called " + "when the remote endpoint cancels the stream."), + {ok, Pid, _} = do_gun_open(Config), + {ok, _} = gun:await_up(Pid), + StreamRef = gun:post(Pid, "/stream", []), + ReplyTo = self(), + #{ + stream_ref := StreamRef, + reply_to := ReplyTo, + endpoint := remote, + reason := _ + } = do_receive_event(cancel), + gun:close(Pid). + +cancel_connect(Config) -> + doc("Confirm that the cancel event callback is called when we " + "cancel a stream running inside a CONNECT proxy."), + OriginPort = config(tcp_origin_port, Config), + Protocol = config(name, config(tc_group_properties, Config)), + {ok, ProxyPid, ProxyPort} = do_proxy_start(Protocol, tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [Protocol], + transport => tcp + }), + {ok, Protocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(Protocol, ProxyPid), + StreamRef1 = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + protocols => [Protocol] + }), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {up, Protocol} = gun:await(ConnPid, StreamRef1), + StreamRef2 = gun:post(ConnPid, "/stream", [], #{tunnel => StreamRef1}), + gun:cancel(ConnPid, StreamRef2), + ReplyTo = self(), + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo, + endpoint := local, + reason := cancel + } = do_receive_event(cancel), + gun:close(ConnPid). + +cancel_remote_connect(Config) -> + doc("Confirm that the cancel event callback is called when the " + "remote endpoint cancels a stream running inside a CONNECT proxy."), + OriginPort = config(tcp_origin_port, Config), + Protocol = config(name, config(tc_group_properties, Config)), + {ok, ProxyPid, ProxyPort} = do_proxy_start(Protocol, tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + event_handler => {?MODULE, self()}, + protocols => [Protocol], + transport => tcp + }), + {ok, Protocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(Protocol, ProxyPid), + StreamRef1 = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + protocols => [Protocol] + }), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {up, Protocol} = gun:await(ConnPid, StreamRef1), + StreamRef2 = gun:post(ConnPid, "/stream", [], #{tunnel => StreamRef1}), + ReplyTo = self(), + #{ + stream_ref := StreamRef2, + reply_to := ReplyTo, + endpoint := remote, + reason := _ + } = do_receive_event(cancel), + gun:close(ConnPid). + +%% disconnect. + +disconnect(Config) -> + doc("Confirm that the disconnect event callback is called on disconnect."), + {ok, OriginPid, OriginPort} = init_origin(tcp), + {ok, Pid, _} = do_gun_open(OriginPort, Config), + {ok, _} = gun:await_up(Pid), + %% We make the origin exit to trigger a disconnect. + unlink(OriginPid), + exit(OriginPid, shutdown), + #{ + reason := closed + } = do_receive_event(?FUNCTION_NAME), + gun:close(Pid). + +%% terminate. + +terminate(Config) -> + doc("Confirm that the terminate event callback is called on terminate."), + {ok, Pid, _} = do_gun_open(12345, Config), + gun:close(Pid), + #{ + state := _, + reason := shutdown + } = do_receive_event(?FUNCTION_NAME), + ok. + +%% Internal. + +do_gun_open(Config) -> + OriginPort = config(tcp_origin_port, Config), + do_gun_open(OriginPort, Config). + +do_gun_open(OriginPort, Config) -> + Opts = #{ + event_handler => {?MODULE, self()}, + http2_opts => #{notify_settings_changed => true}, + protocols => [config(name, config(tc_group_properties, Config))] + }, + {ok, Pid} = gun:open("localhost", OriginPort, Opts), + {ok, Pid, OriginPort}. + +do_gun_open_tls(Config) -> + OriginPort = config(tls_origin_port, Config), + Opts = #{ + event_handler => {?MODULE, self()}, + http2_opts => #{notify_settings_changed => true}, + protocols => [config(name, config(tc_group_properties, Config))], + transport => tls, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}] + }, + {ok, Pid} = gun:open("localhost", OriginPort, Opts), + {ok, Pid, OriginPort}. + +do_receive_event(Event) -> + receive + {Event, EventData} -> + EventData + after 5000 -> + error(timeout) + end. + +do_proxy_start(Protocol, Transport) -> + case Protocol of + http -> rfc7231_SUITE:do_proxy_start(Transport); + http2 -> rfc7540_SUITE:do_proxy_start(Transport) + end. + +%% gun_event callbacks. + +init(EventData, Pid) -> + %% We enable trap_exit to ensure we get a terminate event + %% when we call gun:close/1. + process_flag(trap_exit, true), + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +domain_lookup_start(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +domain_lookup_end(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +connect_start(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +connect_end(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +tls_handshake_start(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +tls_handshake_end(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +request_start(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +request_headers(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +request_end(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +push_promise_start(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +push_promise_end(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +response_start(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +response_inform(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +response_headers(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +response_trailers(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +response_end(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +ws_upgrade(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +ws_recv_frame_start(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +ws_recv_frame_header(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +ws_recv_frame_end(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +ws_send_frame_start(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +ws_send_frame_end(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +protocol_changed(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +origin_changed(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +cancel(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +disconnect(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. + +terminate(EventData, Pid) -> + Pid ! {?FUNCTION_NAME, EventData}, + Pid. diff --git a/gun/test/flow_SUITE.erl b/gun/test/flow_SUITE.erl new file mode 100644 index 0000000..78064ef --- /dev/null +++ b/gun/test/flow_SUITE.erl @@ -0,0 +1,339 @@ +%% Copyright (c) 2019-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(flow_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-import(ct_helper, [doc/1]). + +all() -> + [{group, flow}]. + +groups() -> + [{flow, [parallel], ct_helper:all(?MODULE)}]. + +%% Tests. + +default_flow_http(_) -> + doc("Confirm flow control default can be changed and overriden for HTTP/1.1."), + {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], #{env => #{ + dispatch => cowboy_router:compile([{'_', [{"/", sse_clock_h, date}]}]) + }}), + Port = ranch:get_port(?FUNCTION_NAME), + try + %% First we check that we can set the flow for the entire connection. + {ok, ConnPid1} = gun:open("localhost", Port, #{ + http_opts => #{flow => 1} + }), + {ok, http} = gun:await_up(ConnPid1), + StreamRef1 = gun:get(ConnPid1, "/"), + {response, nofin, 200, _} = gun:await(ConnPid1, StreamRef1), + {data, nofin, _} = gun:await(ConnPid1, StreamRef1), + {error, timeout} = gun:await(ConnPid1, StreamRef1, 1500), + gun:close(ConnPid1), + %% Then we confirm that we can override it per request. + {ok, ConnPid2} = gun:open("localhost", Port, #{ + http_opts => #{flow => 1} + }), + {ok, http} = gun:await_up(ConnPid2), + StreamRef2 = gun:get(ConnPid2, "/", [], #{flow => 2}), + {response, nofin, 200, _} = gun:await(ConnPid2, StreamRef2), + {data, nofin, _} = gun:await(ConnPid2, StreamRef2), + {data, nofin, _} = gun:await(ConnPid2, StreamRef2), + {error, timeout} = gun:await(ConnPid2, StreamRef2, 1500), + gun:close(ConnPid2) + after + cowboy:stop_listener(?FUNCTION_NAME) + end. + +default_flow_http2(_) -> + doc("Confirm flow control default can be changed and overriden for HTTP/2."), + {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], #{env => #{ + dispatch => cowboy_router:compile([{'_', [{"/", sse_clock_h, 40000}]}]) + }}), + Port = ranch:get_port(?FUNCTION_NAME), + try + %% First we check that we can set the flow for the entire connection. + {ok, ConnPid} = gun:open("localhost", Port, #{ + http2_opts => #{ + flow => 1, + %% We set the max frame size to the same as the initial + %% window size in order to reduce the number of data messages. + initial_connection_window_size => 65535, + initial_stream_window_size => 65535, + max_frame_size_received => 65535 + }, + protocols => [http2] + }), + {ok, http2} = gun:await_up(ConnPid), + StreamRef1 = gun:get(ConnPid, "/"), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef1), + %% We set the flow to 1 therefore we will receive *2* data messages, + %% and then nothing because the window was fully consumed. + {data, nofin, _} = gun:await(ConnPid, StreamRef1), + {data, nofin, _} = gun:await(ConnPid, StreamRef1), + {error, timeout} = gun:await(ConnPid, StreamRef1, 1500), + %% Then we confirm that we can override it per request. + StreamRef2 = gun:get(ConnPid, "/", [], #{flow => 2}), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2), + %% We set the flow to 2 but due to the ensure_window algorithm + %% we end up receiving *4* data messages before we consume + %% the window. + {data, nofin, _} = gun:await(ConnPid, StreamRef2), + {data, nofin, _} = gun:await(ConnPid, StreamRef2), + {data, nofin, _} = gun:await(ConnPid, StreamRef2), + {data, nofin, _} = gun:await(ConnPid, StreamRef2), + {error, timeout} = gun:await(ConnPid, StreamRef2, 1500), + gun:close(ConnPid) + after + cowboy:stop_listener(?FUNCTION_NAME) + end. + +flow_http(_) -> + doc("Confirm flow control works as intended for HTTP/1.1."), + {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], #{env => #{ + dispatch => cowboy_router:compile([{'_', [{"/", sse_clock_h, date}]}]) + }}), + Port = ranch:get_port(?FUNCTION_NAME), + try + {ok, ConnPid} = gun:open("localhost", Port), + {ok, http} = gun:await_up(ConnPid), + StreamRef = gun:get(ConnPid, "/", [], #{flow => 1}), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef), + %% We set the flow to 1 therefore we will receive 1 data message, + %% and then nothing because Gun doesn't read from the socket. + {data, nofin, _} = gun:await(ConnPid, StreamRef), + {error, timeout} = gun:await(ConnPid, StreamRef, 3000), + %% We then update the flow and get 2 more data messages but no more. + gun:update_flow(ConnPid, StreamRef, 2), + {data, nofin, _} = gun:await(ConnPid, StreamRef), + {data, nofin, _} = gun:await(ConnPid, StreamRef), + {error, timeout} = gun:await(ConnPid, StreamRef, 1000), + gun:close(ConnPid) + after + cowboy:stop_listener(?FUNCTION_NAME) + end. + +flow_http2(_) -> + doc("Confirm flow control works as intended for HTTP/2."), + {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], #{env => #{ + dispatch => cowboy_router:compile([{'_', [{"/", sse_clock_h, 40000}]}]) + }}), + Port = ranch:get_port(?FUNCTION_NAME), + try + {ok, ConnPid} = gun:open("localhost", Port, #{ + %% We set the max frame size to the same as the initial + %% window size in order to reduce the number of data messages. + http2_opts => #{ + initial_connection_window_size => 65535, + initial_stream_window_size => 65535, + max_frame_size_received => 65535 + }, + protocols => [http2] + }), + {ok, http2} = gun:await_up(ConnPid), + StreamRef = gun:get(ConnPid, "/", [], #{flow => 1}), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef), + %% We set the flow to 1 therefore we will receive *2* data messages, + %% and then nothing because the window was fully consumed. + {data, nofin, D1} = gun:await(ConnPid, StreamRef), + {data, nofin, D2} = gun:await(ConnPid, StreamRef), + %% We consumed all the window available. + 65535 = byte_size(D1) + byte_size(D2), + {error, timeout} = gun:await(ConnPid, StreamRef, 3500), + %% We then update the flow and get *5* more data messages but no more. + gun:update_flow(ConnPid, StreamRef, 2), + {data, nofin, D3} = gun:await(ConnPid, StreamRef), + {data, nofin, D4} = gun:await(ConnPid, StreamRef), + {data, nofin, D5} = gun:await(ConnPid, StreamRef), + {data, nofin, D6} = gun:await(ConnPid, StreamRef), + {data, nofin, D7} = gun:await(ConnPid, StreamRef), + %% We consumed all the window available again. + %% D3 is the end of the truncated D2, D4, D5 and D6 are full and D7 truncated. + 131070 = byte_size(D3) + byte_size(D4) + byte_size(D5) + byte_size(D6) + byte_size(D7), + {error, timeout} = gun:await(ConnPid, StreamRef, 1000), + gun:close(ConnPid) + after + cowboy:stop_listener(?FUNCTION_NAME) + end. + +flow_ws(_) -> + doc("Confirm flow control works as intended for Websocket."), + {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], #{env => #{ + dispatch => cowboy_router:compile([{'_', [{"/", ws_echo_h, []}]}]) + }}), + Port = ranch:get_port(?FUNCTION_NAME), + try + {ok, ConnPid} = gun:open("localhost", Port), + {ok, http} = gun:await_up(ConnPid), + StreamRef = gun:ws_upgrade(ConnPid, "/", [], #{flow => 1}), + {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), + %% We send 2 frames with some time in between to make sure that + %% Gun handles them in separate Protocol:handle calls. + Frame = {text, <<"Hello!">>}, + gun:ws_send(ConnPid, StreamRef, Frame), + timer:sleep(500), + gun:ws_send(ConnPid, StreamRef, Frame), + %% We set the flow to 1 therefore we will receive 1 data message, + %% and then nothing because Gun doesn't read from the socket. + {ws, _} = gun:await(ConnPid, StreamRef), + {error, timeout} = gun:await(ConnPid, StreamRef, 3000), + %% We then update the flow, send 2 frames with some time in between + %% and get 2 more data messages but no more. + gun:update_flow(ConnPid, StreamRef, 2), + gun:ws_send(ConnPid, StreamRef, Frame), + timer:sleep(500), + gun:ws_send(ConnPid, StreamRef, Frame), + {ws, _} = gun:await(ConnPid, StreamRef), + {ws, _} = gun:await(ConnPid, StreamRef), + {error, timeout} = gun:await(ConnPid, StreamRef, 1000), + gun:close(ConnPid) + after + cowboy:stop_listener(?FUNCTION_NAME) + end. + +no_flow_http(_) -> + doc("Ignore flow updates for no-flow streams for HTTP/1.1."), + {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], #{env => #{ + dispatch => cowboy_router:compile([{'_', [{"/", sse_clock_h, date}]}]) + }}), + Port = ranch:get_port(?FUNCTION_NAME), + try + {ok, ConnPid} = gun:open("localhost", Port), + {ok, http} = gun:await_up(ConnPid), + StreamRef = gun:get(ConnPid, "/", []), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef), + gun:update_flow(ConnPid, StreamRef, 2), + {data, nofin, _} = gun:await(ConnPid, StreamRef), + {data, nofin, _} = gun:await(ConnPid, StreamRef), + {data, nofin, _} = gun:await(ConnPid, StreamRef), + gun:close(ConnPid) + after + cowboy:stop_listener(?FUNCTION_NAME) + end. + +no_flow_http2(_) -> + doc("Ignore flow updates for no-flow streams for HTTP/2."), + {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], #{env => #{ + dispatch => cowboy_router:compile([{'_', [{"/", sse_clock_h, date}]}]) + }}), + Port = ranch:get_port(?FUNCTION_NAME), + try + {ok, ConnPid} = gun:open("localhost", Port, #{ + protocols => [http2] + }), + {ok, http2} = gun:await_up(ConnPid), + StreamRef = gun:get(ConnPid, "/", []), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef), + gun:update_flow(ConnPid, StreamRef, 2), + {data, nofin, _} = gun:await(ConnPid, StreamRef), + {data, nofin, _} = gun:await(ConnPid, StreamRef), + {data, nofin, _} = gun:await(ConnPid, StreamRef), + gun:close(ConnPid) + after + cowboy:stop_listener(?FUNCTION_NAME) + end. + +no_flow_ws(_) -> + doc("Ignore flow updates for no-flow streams for Websocket."), + {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], #{env => #{ + dispatch => cowboy_router:compile([{'_', [{"/", ws_echo_h, []}]}]) + }}), + Port = ranch:get_port(?FUNCTION_NAME), + try + {ok, ConnPid} = gun:open("localhost", Port), + {ok, http} = gun:await_up(ConnPid), + StreamRef = gun:ws_upgrade(ConnPid, "/", []), + {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), + gun:update_flow(ConnPid, StreamRef, 2), + Frame = {text, <<"Hello!">>}, + gun:ws_send(ConnPid, StreamRef, Frame), + timer:sleep(100), + gun:ws_send(ConnPid, StreamRef, Frame), + {ws, _} = gun:await(ConnPid, StreamRef), + {ws, _} = gun:await(ConnPid, StreamRef), + gun:close(ConnPid) + after + cowboy:stop_listener(?FUNCTION_NAME) + end. + +sse_flow_http(_) -> + doc("Confirm flow control works as intended for HTTP/1.1 " + "when using the gun_sse_h content handler."), + {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], #{env => #{ + dispatch => cowboy_router:compile([{'_', [{"/", sse_clock_h, date}]}]) + }}), + Port = ranch:get_port(?FUNCTION_NAME), + try + {ok, ConnPid} = gun:open("localhost", Port, #{ + http_opts => #{content_handlers => [gun_sse_h, gun_data_h]} + }), + {ok, http} = gun:await_up(ConnPid), + StreamRef = gun:get(ConnPid, "/", [], #{flow => 1}), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef), + %% We set the flow to 1 therefore we will receive 1 event message, + %% and then nothing because Gun doesn't read from the socket. We + %% set the timeout to 2500 to ensure there is only going to be one + %% message queued up. + {sse, _} = gun:await(ConnPid, StreamRef), + {error, timeout} = gun:await(ConnPid, StreamRef, 2500), + %% We then update the flow and get 2 more event messages but no more. + gun:update_flow(ConnPid, StreamRef, 2), + {sse, _} = gun:await(ConnPid, StreamRef), + {sse, _} = gun:await(ConnPid, StreamRef), + {error, timeout} = gun:await(ConnPid, StreamRef, 1000), + gun:close(ConnPid) + after + cowboy:stop_listener(?FUNCTION_NAME) + end. + +sse_flow_http2(_) -> + doc("Confirm flow control works as intended for HTTP/2 " + "when using the gun_sse_h content handler."), + {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], #{env => #{ + dispatch => cowboy_router:compile([{'_', [{"/", sse_clock_h, 40000}]}]) + }}), + Port = ranch:get_port(?FUNCTION_NAME), + try + {ok, ConnPid} = gun:open("localhost", Port, #{ + %% We set the max frame size to the same as the initial + %% window size in order to reduce the number of data messages. + http2_opts => #{ + content_handlers => [gun_sse_h, gun_data_h], + initial_connection_window_size => 65535, + initial_stream_window_size => 65535, + max_frame_size_received => 65535 + }, + protocols => [http2] + }), + {ok, http2} = gun:await_up(ConnPid), + StreamRef = gun:get(ConnPid, "/", [], #{flow => 1}), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef), + %% We set the flow to 1 therefore we will receive 1 event message, + %% and then nothing because the window was fully consumed before + %% the second event was fully received. + {sse, _} = gun:await(ConnPid, StreamRef), + {error, timeout} = gun:await(ConnPid, StreamRef, 3000), + %% We then update the flow and get 3 more event messages but no more. + %% We get an extra message because of the ensure_window algorithm. + gun:update_flow(ConnPid, StreamRef, 2), + {sse, _} = gun:await(ConnPid, StreamRef), + {sse, _} = gun:await(ConnPid, StreamRef), + {sse, _} = gun:await(ConnPid, StreamRef), + {error, timeout} = gun:await(ConnPid, StreamRef, 1000), + gun:close(ConnPid) + after + cowboy:stop_listener(?FUNCTION_NAME) + end. diff --git a/gun/test/gun_SUITE.erl b/gun/test/gun_SUITE.erl new file mode 100644 index 0000000..8b90774 --- /dev/null +++ b/gun/test/gun_SUITE.erl @@ -0,0 +1,754 @@ +%% Copyright (c) 2017-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-import(ct_helper, [doc/1]). +-import(ct_helper, [name/0]). +-import(gun_test, [init_origin/2]). +-import(gun_test, [init_origin/3]). +-import(gun_test, [receive_from/1]). +-import(gun_test, [receive_all_from/2]). +-import(gun_test_event_h, [receive_event/1]). +-import(gun_test_event_h, [receive_event/2]). + +suite() -> + [{timetrap, 30000}]. + +all() -> + [{group, gun}]. + +groups() -> + [{gun, [parallel], ct_helper:all(?MODULE)}]. + +%% Tests. + +atom_header_name(_) -> + doc("Header names may be given as atom."), + {ok, OriginPid, OriginPort} = init_origin(tcp, http), + {ok, Pid} = gun:open("localhost", OriginPort), + {ok, http} = gun:await_up(Pid), + handshake_completed = receive_from(OriginPid), + _ = gun:get(Pid, "/", [ + {'User-Agent', "Gun/atom-headers"} + ]), + Data = receive_from(OriginPid), + Lines = binary:split(Data, <<"\r\n">>, [global]), + [<<"user-agent: Gun/atom-headers">>] = [L || <<"user-agent: ", _/bits>> = L <- Lines], + gun:close(Pid). + +atom_hostname(_) -> + doc("Hostnames may be given as atom."), + {ok, OriginPid, OriginPort} = init_origin(tcp, http), + {ok, Pid} = gun:open('localhost', OriginPort), + {ok, http} = gun:await_up(Pid), + handshake_completed = receive_from(OriginPid), + _ = gun:get(Pid, "/"), + Data = receive_from(OriginPid), + Lines = binary:split(Data, <<"\r\n">>, [global]), + [<<"host: localhost:", _/bits>>] = [L || <<"host: ", _/bits>> = L <- Lines], + gun:close(Pid). + +connect_timeout(_) -> + doc("Ensure an integer value for connect_timeout is accepted."), + do_timeout(connect_timeout, 1000). + +connect_timeout_infinity(_) -> + doc("Ensure infinity for connect_timeout is accepted."), + do_timeout(connect_timeout, infinity). + +domain_lookup_timeout(_) -> + doc("Ensure an integer value for domain_lookup_timeout is accepted."), + do_timeout(domain_lookup_timeout, 1000). + +domain_lookup_timeout_infinity(_) -> + doc("Ensure infinity for domain_lookup_timeout is accepted."), + do_timeout(domain_lookup_timeout, infinity). + +do_timeout(Opt, Timeout) -> + {ok, ConnPid} = gun:open("localhost", 12345, #{ + Opt => Timeout, + event_handler => {gun_test_event_h, self()}, + retry => 0 + }), + %% The connection will not succeed. We will however receive + %% an init event from the connection process that indicates + %% that the timeout value was accepted, since the timeout + %% checks occur earlier. + {_, init, _} = receive_event(ConnPid), + gun:close(ConnPid). + +ignore_empty_data_http(_) -> + doc("When gun:data/4 is called with nofin and empty data, it must be ignored."), + {ok, OriginPid, OriginPort} = init_origin(tcp, http), + {ok, Pid} = gun:open("localhost", OriginPort), + {ok, http} = gun:await_up(Pid), + handshake_completed = receive_from(OriginPid), + Ref = gun:put(Pid, "/", []), + gun:data(Pid, Ref, nofin, "hello "), + gun:data(Pid, Ref, nofin, ["", <<>>]), + gun:data(Pid, Ref, fin, "world!"), + Data = receive_all_from(OriginPid, 500), + Lines = binary:split(Data, <<"\r\n">>, [global]), + Zero = [Z || <<"0">> = Z <- Lines], + 1 = length(Zero), + gun:close(Pid). + +ignore_empty_data_fin_http(_) -> + doc("When gun:data/4 is called with fin and empty data, it must send a final empty chunk."), + {ok, OriginPid, OriginPort} = init_origin(tcp, http), + {ok, Pid} = gun:open("localhost", OriginPort), + {ok, http} = gun:await_up(Pid), + handshake_completed = receive_from(OriginPid), + Ref = gun:post(Pid, "/", []), + gun:data(Pid, Ref, nofin, "hello"), + gun:data(Pid, Ref, fin, ["", <<>>]), + Data = receive_all_from(OriginPid, 500), + Lines = binary:split(Data, <<"\r\n">>, [global]), + Zero = [Z || <<"0">> = Z <- Lines], + 1 = length(Zero), + gun:close(Pid). + +ignore_empty_data_http2(_) -> + doc("When gun:data/4 is called with nofin and empty data, it must be ignored."), + {ok, OriginPid, OriginPort} = init_origin(tcp, http2), + {ok, Pid} = gun:open("localhost", OriginPort, #{protocols => [http2]}), + {ok, http2} = gun:await_up(Pid), + handshake_completed = receive_from(OriginPid), + Ref = gun:put(Pid, "/", []), + gun:data(Pid, Ref, nofin, "hello "), + gun:data(Pid, Ref, nofin, ["", <<>>]), + gun:data(Pid, Ref, fin, "world!"), + Data = receive_all_from(OriginPid, 500), + << + %% HEADERS frame. + Len1:24, 1, _:40, _:Len1/unit:8, + %% First DATA frame. + 6:24, 0, _:7, 0:1, _:32, "hello ", + %% Second and final DATA frame. + 6:24, 0, _:7, 1:1, _:32, "world!" + >> = Data, + gun:close(Pid). + +ignore_empty_data_fin_http2(_) -> + doc("When gun:data/4 is called with fin and empty data, it must send a final empty DATA frame."), + {ok, OriginPid, OriginPort} = init_origin(tcp, http2), + {ok, Pid} = gun:open("localhost", OriginPort, #{protocols => [http2]}), + {ok, http2} = gun:await_up(Pid), + handshake_completed = receive_from(OriginPid), + Ref = gun:put(Pid, "/", []), + gun:data(Pid, Ref, nofin, "hello "), + gun:data(Pid, Ref, nofin, "world!"), + gun:data(Pid, Ref, fin, ["", <<>>]), + Data = receive_all_from(OriginPid, 500), + << + %% HEADERS frame. + Len1:24, 1, _:40, _:Len1/unit:8, + %% First DATA frame. + 6:24, 0, _:7, 0:1, _:32, "hello ", + %% Second DATA frame. + 6:24, 0, _:7, 0:1, _:32, "world!", + %% Final empty DATA frame. + 0:24, 0, _:7, 1:1, _:32 + >> = Data, + gun:close(Pid). + +info(_) -> + doc("Get info from the Gun connection."), + {ok, ListenSocket} = gen_tcp:listen(0, [binary, {active, false}]), + {ok, {_, Port}} = inet:sockname(ListenSocket), + {ok, Pid} = gun:open("localhost", Port), + {ok, _} = gen_tcp:accept(ListenSocket, 5000), + #{sock_ip := _, sock_port := _} = gun:info(Pid), + gun:close(Pid). + +keepalive_infinity(_) -> + doc("Ensure infinity for keepalive is accepted by all protocols."), + {ok, ConnPid} = gun:open("localhost", 12345, #{ + event_handler => {gun_test_event_h, self()}, + http_opts => #{keepalive => infinity}, + http2_opts => #{keepalive => infinity}, + retry => 0 + }), + %% The connection will not succeed. We will however receive + %% an init event from the connection process that indicates + %% that the timeout value was accepted, since the timeout + %% checks occur earlier. + {_, init, _} = receive_event(ConnPid), + gun:close(ConnPid). + +killed_streams_http(_) -> + doc("Ensure completed responses with a connection: close are not considered killed streams."), + {ok, _, OriginPort} = init_origin(tcp, http, + fun (_, _, ClientSocket, ClientTransport) -> + {ok, _} = ClientTransport:recv(ClientSocket, 0, 1000), + ClientTransport:send(ClientSocket, + "HTTP/1.1 200 OK\r\n" + "connection: close\r\n" + "content-length: 12\r\n" + "\r\n" + "hello world!" + ) + end), + {ok, ConnPid} = gun:open("localhost", OriginPort), + {ok, http} = gun:await_up(ConnPid), + StreamRef = gun:get(ConnPid, "/"), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef), + {ok, <<"hello world!">>} = gun:await_body(ConnPid, StreamRef), + receive + {gun_down, ConnPid, http, normal, KilledStreams} -> + [] = KilledStreams, + gun:close(ConnPid) + end. + +list_header_name(_) -> + doc("Header names may be given as list."), + {ok, OriginPid, OriginPort} = init_origin(tcp, http), + {ok, Pid} = gun:open("localhost", OriginPort), + {ok, http} = gun:await_up(Pid), + handshake_completed = receive_from(OriginPid), + _ = gun:get(Pid, "/", [ + {"User-Agent", "Gun/list-headers"} + ]), + Data = receive_from(OriginPid), + Lines = binary:split(Data, <<"\r\n">>, [global]), + [<<"user-agent: Gun/list-headers">>] = [L || <<"user-agent: ", _/bits>> = L <- Lines], + gun:close(Pid). + +map_headers(_) -> + doc("Header names may be given as a map."), + {ok, OriginPid, OriginPort} = init_origin(tcp, http), + {ok, Pid} = gun:open("localhost", OriginPort), + {ok, http} = gun:await_up(Pid), + handshake_completed = receive_from(OriginPid), + _ = gun:get(Pid, "/", #{ + <<"USER-agent">> => "Gun/map-headers" + }), + Data = receive_from(OriginPid), + Lines = binary:split(Data, <<"\r\n">>, [global]), + [<<"user-agent: Gun/map-headers">>] = [L || <<"user-agent: ", _/bits>> = L <- Lines], + gun:close(Pid). + +postpone_request_while_not_connected(_) -> + doc("Ensure Gun doesn't raise error when requesting in retries"), + %% Try connecting to a server that isn't up yet. + {ok, ConnPid} = gun:open("localhost", 23456, #{ + event_handler => {gun_test_event_h, self()}, + retry => 5, + retry_timeout => 1000 + }), + _ = gun:get(ConnPid, "/postponed"), + %% Wait for the connection attempt. to fail. + {_, connect_end, #{error := _}} = receive_event(ConnPid, connect_end), + %% Start the server so that next retry will result in the client connecting successfully. + {ok, ListenSocket} = gen_tcp:listen(23456, [binary, {active, false}, {reuseaddr, true}]), + {ok, ClientSocket} = gen_tcp:accept(ListenSocket, 5000), + %% The client should now be up. + {ok, http} = gun:await_up(ConnPid), + %% The server receives the postponed request. + {ok, <<"GET /postponed HTTP/1.1\r\n", _/bits>>} = gen_tcp:recv(ClientSocket, 0, 5000), + gun:close(ConnPid). + +reply_to_http(_) -> + doc("The reply_to option allows using a separate process for requests."), + do_reply_to(http). + +reply_to_http2(_) -> + doc("The reply_to option allows using a separate process for requests."), + do_reply_to(http2). + +do_reply_to(Protocol) -> + {ok, OriginPid, OriginPort} = init_origin(tcp, Protocol, + fun(_, _, ClientSocket, ClientTransport) -> + {ok, _} = ClientTransport:recv(ClientSocket, 0, infinity), + ResponseData = case Protocol of + http -> + "HTTP/1.1 200 OK\r\n" + "Content-length: 12\r\n" + "\r\n" + "Hello world!"; + http2 -> + %% Send a HEADERS frame with PRIORITY back. + {HeadersBlock, _} = cow_hpack:encode([ + {<<":status">>, <<"200">>} + ]), + Len = iolist_size(HeadersBlock), + [ + <>, + HeadersBlock + ] + end, + ok = ClientTransport:send(ClientSocket, ResponseData), + timer:sleep(1000) + end), + {ok, Pid} = gun:open("localhost", OriginPort, #{protocols => [Protocol]}), + {ok, Protocol} = gun:await_up(Pid), + handshake_completed = receive_from(OriginPid), + Self = self(), + ReplyTo = spawn(fun() -> + receive Ref when is_reference(Ref) -> + Response = gun:await(Pid, Ref, infinity), + Self ! Response + end + end), + Ref = gun:get(Pid, "/", [], #{reply_to => ReplyTo}), + ReplyTo ! Ref, + receive + Msg -> + {response, _, _, _} = Msg, + gun:close(Pid) + end. + +retry_0(_) -> + doc("Ensure Gun gives up immediately with retry=0."), + {ok, ConnPid} = gun:open("localhost", 12345, #{ + event_handler => {gun_test_event_h, self()}, + retry => 0, + retry_timeout => 500 + }), + {_, init, _} = receive_event(ConnPid), + {_, domain_lookup_start, _} = receive_event(ConnPid), + {_, domain_lookup_end, _} = receive_event(ConnPid), + {_, connect_start, _} = receive_event(ConnPid), + {_, connect_end, #{error := _}} = receive_event(ConnPid), + {_, terminate, _} = receive_event(ConnPid), + ok. + +retry_0_disconnect(_) -> + doc("Ensure Gun gives up immediately with retry=0 after a successful connection."), + {ok, ListenSocket} = gen_tcp:listen(0, [binary, {active, false}]), + {ok, {_, Port}} = inet:sockname(ListenSocket), + {ok, ConnPid} = gun:open("localhost", Port, #{ + event_handler => {gun_test_event_h, self()}, + retry => 0, + retry_timeout => 500 + }), + {_, init, _} = receive_event(ConnPid), + {_, domain_lookup_start, _} = receive_event(ConnPid), + {_, domain_lookup_end, _} = receive_event(ConnPid), + {_, connect_start, _} = receive_event(ConnPid), + %% We accept the connection and then close it to trigger a disconnect. + {ok, ClientSocket} = gen_tcp:accept(ListenSocket, 5000), + gen_tcp:close(ClientSocket), + %% Connection was successful. + {_, connect_end, ConnectEndEvent} = receive_event(ConnPid), + false = maps:is_key(error, ConnectEndEvent), + %% When the connection is closed we terminate immediately. + {_, disconnect, _} = receive_event(ConnPid), + {_, terminate, _} = receive_event(ConnPid), + ok. + +retry_1(_) -> + doc("Ensure Gun gives up with retry=1."), + {ok, ConnPid} = gun:open("localhost", 12345, #{ + event_handler => {gun_test_event_h, self()}, + retry => 1, + retry_timeout => 500 + }), + {_, init, _} = receive_event(ConnPid), + %% Initial attempt. + {_, domain_lookup_start, _} = receive_event(ConnPid), + {_, domain_lookup_end, _} = receive_event(ConnPid), + {_, connect_start, _} = receive_event(ConnPid), + {_, connect_end, #{error := _}} = receive_event(ConnPid), + %% Retry. + {_, domain_lookup_start, _} = receive_event(ConnPid), + {_, domain_lookup_end, _} = receive_event(ConnPid), + {_, connect_start, _} = receive_event(ConnPid), + {_, connect_end, #{error := _}} = receive_event(ConnPid), + {_, terminate, _} = receive_event(ConnPid), + ok. + +retry_1_disconnect(_) -> + doc("Ensure Gun doesn't give up with retry=1 after a successful connection " + "and attempts to reconnect immediately, ignoring retry_timeout."), + {ok, ListenSocket} = gen_tcp:listen(0, [binary, {active, false}]), + {ok, {_, Port}} = inet:sockname(ListenSocket), + {ok, ConnPid} = gun:open("localhost", Port, #{ + event_handler => {gun_test_event_h, self()}, + retry => 1, + retry_timeout => 30000 + }), + {_, init, _} = receive_event(ConnPid), + {_, domain_lookup_start, _} = receive_event(ConnPid), + {_, domain_lookup_end, _} = receive_event(ConnPid), + {_, connect_start, _} = receive_event(ConnPid), + %% We accept the connection and then close it to trigger a disconnect. + {ok, ClientSocket} = gen_tcp:accept(ListenSocket, 5000), + gen_tcp:close(ClientSocket), + %% Connection was successful. + {_, connect_end, ConnectEndEvent} = receive_event(ConnPid), + false = maps:is_key(error, ConnectEndEvent), + %% We confirm that Gun reconnects before the retry timeout, + %% as it is ignored on the first reconnection. + {ok, _} = gen_tcp:accept(ListenSocket, 5000), + gun:close(ConnPid). + +retry_fun(_) -> + doc("Ensure the retry_fun is used when provided."), + {ok, ConnPid} = gun:open("localhost", 12345, #{ + event_handler => {gun_test_event_h, self()}, + retry => 5, + retry_fun => fun(_, _) -> #{retries => 0, timeout => 0} end, + retry_timeout => 60000 + }), + {_, init, _} = receive_event(ConnPid), + %% Initial attempt. + {_, domain_lookup_start, _} = receive_event(ConnPid), + {_, domain_lookup_end, _} = receive_event(ConnPid), + {_, connect_start, _} = receive_event(ConnPid), + {_, connect_end, #{error := _}} = receive_event(ConnPid), + %% When retry is not disabled (retry!=0) we necessarily + %% have at least one retry attempt using the fun. + {_, domain_lookup_start, _} = receive_event(ConnPid), + {_, domain_lookup_end, _} = receive_event(ConnPid), + {_, connect_start, _} = receive_event(ConnPid), + {_, connect_end, #{error := _}} = receive_event(ConnPid), + {_, terminate, _} = receive_event(ConnPid), + ok. + +retry_timeout(_) -> + doc("Ensure the retry_timeout value is enforced. The first retry is immediate " + "and therefore does not use the timeout."), + {ok, ConnPid} = gun:open("localhost", 12345, #{ + event_handler => {gun_test_event_h, self()}, + retry => 2, + retry_timeout => 1000 + }), + {_, init, _} = receive_event(ConnPid), + %% Initial attempt. + {_, domain_lookup_start, _} = receive_event(ConnPid), + {_, domain_lookup_end, _} = receive_event(ConnPid), + {_, connect_start, _} = receive_event(ConnPid), + {_, connect_end, #{error := _, ts := TS1}} = receive_event(ConnPid), + %% First retry is immediate. + {_, domain_lookup_start, #{ts := TS2}} = receive_event(ConnPid), + true = (TS2 - TS1) < 1000, + {_, domain_lookup_end, _} = receive_event(ConnPid), + {_, connect_start, _} = receive_event(ConnPid), + {_, connect_end, #{error := _, ts := TS3}} = receive_event(ConnPid), + %% Second retry occurs after the retry_timeout. + {_, domain_lookup_start, #{ts := TS4}} = receive_event(ConnPid), + true = (TS4 - TS3) >= 1000, + {_, domain_lookup_end, _} = receive_event(ConnPid), + {_, connect_start, _} = receive_event(ConnPid), + {_, connect_end, #{error := _}} = receive_event(ConnPid), + {_, terminate, _} = receive_event(ConnPid), + ok. + +server_name_indication_custom(_) -> + doc("Ensure a custom server_name_indication is accepted."), + do_server_name_indication("localhost", net_adm:localhost(), #{ + tls_opts => [ + {verify, verify_none}, {versions, ['tlsv1.2']}, + {fail_if_no_peer_cert, false}, + {server_name_indication, net_adm:localhost()}] + }). + +server_name_indication_default(_) -> + doc("Ensure a default server_name_indication is accepted."), + do_server_name_indication(net_adm:localhost(), net_adm:localhost(), #{ + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}, + {fail_if_no_peer_cert, false}] + }). + +do_server_name_indication(Host, Expected, GunOpts) -> + Self = self(), + {ok, OriginPid, OriginPort} = init_origin(tls, http, + fun(_, _, ClientSocket, _) -> + {ok, Info} = ssl:connection_information(ClientSocket), + Msg = {sni_hostname, _} = lists:keyfind(sni_hostname, 1, Info), + Self ! Msg + end), + {ok, ConnPid} = gun:open(Host, OriginPort, GunOpts#{ + transport => tls, + retry => 0 + }), + handshake_completed = receive_from(OriginPid), + %% The connection will succeed, look up the SNI hostname + %% and send it to us as a message, where we can check it. + {sni_hostname, Expected} = receive Msg = {sni_hostname, _} -> Msg end, + gun:close(ConnPid). + +set_owner(_) -> + doc("The owner of the connection can be changed."), + Self = self(), + spawn(fun() -> + {ok, ConnPid} = gun:open("localhost", 12345), + gun:set_owner(ConnPid, Self), + Self ! {conn, ConnPid} + end), + ConnPid = receive {conn, C} -> C end, + #{owner := Self} = gun:info(ConnPid), + gun:close(ConnPid). + +shutdown_reason(_) -> + doc("The last connection failure must be propagated."), + do_shutdown_reason(). + +do_shutdown_reason() -> + %% We set retry=1 so that we can monitor before the process terminates. + {ok, ConnPid} = gun:open("localhost", 12345, #{ + retry => 1, + retry_timeout => 500 + }), + Ref = monitor(process, ConnPid), + receive + %% Depending on timings we may monitor AFTER the process already + %% failed to connect and exited. In that case we just try again. + %% We rely on timetrap_timeout to stop the test if it takes too long. + {'DOWN', Ref, process, ConnPid, noproc} -> + ct:log("Monitor got noproc, trying again..."), + do_shutdown_reason(); + {'DOWN', Ref, process, ConnPid, Reason} -> + {shutdown, econnrefused} = Reason, + gun:close(ConnPid) + end. + +stream_info_http(_) -> + doc("Ensure the function gun:stream_info/2 works as expected for HTTP/1.1."), + {ok, OriginPid, OriginPort} = init_origin(tcp, http, + fun(_, _, ClientSocket, ClientTransport) -> + %% Wait for the cancel signal. + receive cancel -> ok end, + %% Then terminate the stream. + ClientTransport:send(ClientSocket, + "HTTP/1.1 200 OK\r\n" + "content-length: 0\r\n" + "\r\n" + ), + receive disconnect -> ok end + end), + {ok, Pid} = gun:open("localhost", OriginPort, #{ + event_handler => {gun_test_event_h, self()} + }), + {ok, http} = gun:await_up(Pid), + {ok, undefined} = gun:stream_info(Pid, make_ref()), + StreamRef = gun:get(Pid, "/"), + Self = self(), + {ok, #{ + ref := StreamRef, + reply_to := Self, + state := running + }} = gun:stream_info(Pid, StreamRef), + gun:cancel(Pid, StreamRef), + {ok, #{ + ref := StreamRef, + reply_to := Self, + state := stopping + }} = gun:stream_info(Pid, StreamRef), + %% Cancel and wait for the stream to be canceled. + OriginPid ! cancel, + receive_event(Pid, cancel), + fun F() -> + case gun:stream_info(Pid, StreamRef) of + {ok, undefined} -> ok; + {ok, #{state := stopping}} -> F() + end + end(), + %% Wait for the connection to terminate. + OriginPid ! disconnect, + receive_event(Pid, disconnect), + {error, not_connected} = gun:stream_info(Pid, StreamRef), + gun:close(Pid). + +stream_info_http2(_) -> + doc("Ensure the function gun:stream_info/2 works as expected for HTTP/2."), + {ok, OriginPid, OriginPort} = init_origin(tcp, http2, + fun(_, _, _, _) -> receive disconnect -> ok end end), + {ok, Pid} = gun:open("localhost", OriginPort, #{ + event_handler => {gun_test_event_h, self()}, + protocols => [http2] + }), + {ok, http2} = gun:await_up(Pid), + handshake_completed = receive_from(OriginPid), + {ok, undefined} = gun:stream_info(Pid, make_ref()), + StreamRef = gun:get(Pid, "/"), + Self = self(), + {ok, #{ + ref := StreamRef, + reply_to := Self, + state := running + }} = gun:stream_info(Pid, StreamRef), + gun:cancel(Pid, StreamRef), + %% Wait for the connection to terminate. + OriginPid ! disconnect, + receive_event(Pid, disconnect), + {error, not_connected} = gun:stream_info(Pid, StreamRef), + gun:close(Pid). + +supervise_false(_) -> + doc("The supervise option allows starting without a supervisor."), + {ok, _, OriginPort} = init_origin(tcp, http), + {ok, Pid} = gun:open("localhost", OriginPort, #{supervise => false}), + {ok, http} = gun:await_up(Pid), + [] = [P || {_, P, _, _} <- supervisor:which_children(gun_sup), P =:= Pid], + ok. + +tls_handshake_error_gun_http2_init_retry_0(_) -> + doc("Ensure an early TLS connection close is propagated " + "to the user of the connection."), + %% The server will immediately close the connection upon + %% establishment so that the client's socket is down + %% before it attempts to initialise HTTP/2. + %% + %% We use 'http' for the server to skip the HTTP/2 init + %% but the client is connecting using 'http2'. + {ok, _, OriginPort} = init_origin(tls, http, + fun(_, _, ClientSocket, ClientTransport) -> + ClientTransport:close(ClientSocket) + end), + {ok, ConnPid} = gun:open("localhost", OriginPort, #{ + event_handler => {gun_test_fun_event_h, #{ + tls_handshake_end => fun(_, #{socket := _}) -> + %% We sleep right after the gun_tls:connect succeeds to make + %% sure the next call to the socket fails (as the socket + %% should be disconnected at that point by the server because + %% we are not sending a certificate). The call we want to fail + %% is the sending of the HTTP/2 preface in gun_http2:init. + timer:sleep(1000) + end + }}, + protocols => [http2], + retry => 0, + transport => tls, + tls_opts => [{verify, verify_none}] + }), + {error, {down, {shutdown, closed}}} = gun:await_up(ConnPid), + gun:close(ConnPid). + +tls_handshake_error_gun_http2_init_retry_1(_) -> + doc("Ensure an early TLS connection close is propagated " + "to the user of the connection and does not result " + "in an infinite connect loop."), + %% The server will immediately close the connection upon + %% establishment so that the client's socket is down + %% before it attempts to initialise HTTP/2. + %% + %% We use 'http' for the server to skip the HTTP/2 init + %% but the client is connecting using 'http2'. + %% + %% We immediately accept another connection to handle + %% the coming retry and close that connection again. + {ok, _, OriginPort} = init_origin(tls, http, + fun(_, ListenSocket, ClientSocket1, ClientTransport) -> + ClientTransport:close(ClientSocket1), + %% We immediately accept a second connection and close it again. + {ok, ClientSocket2t} = ssl:transport_accept(ListenSocket, 5000), + {ok, ClientSocket2} = ssl:handshake(ClientSocket2t, 5000), + ClientTransport:close(ClientSocket2) + end), + {ok, ConnPid} = gun:open("localhost", OriginPort, #{ + event_handler => {gun_test_fun_event_h, #{ + tls_handshake_end => fun(_, #{socket := _}) -> + %% See tls_handshake_error_gun_http2_init_retry_0 for details. + timer:sleep(1000) + end + }}, + protocols => [http2], + retry => 1, + transport => tls, + tls_opts => [{verify, verify_none}] + }), + {error, {down, {shutdown, closed}}} = gun:await_up(ConnPid), + gun:close(ConnPid). + +tls_handshake_timeout(_) -> + doc("Ensure an integer value for tls_handshake_timeout is accepted."), + do_timeout(tls_handshake_timeout, 1000). + +tls_handshake_timeout_infinity(_) -> + doc("Ensure infinity for tls_handshake_timeout is accepted."), + do_timeout(tls_handshake_timeout, infinity). + +transform_header_name(_) -> + doc("The transform_header_name option allows changing the case of header names."), + {ok, ListenSocket} = gen_tcp:listen(0, [binary, {active, false}]), + {ok, {_, Port}} = inet:sockname(ListenSocket), + {ok, Pid} = gun:open("localhost", Port, #{ + protocols => [http], + http_opts => #{ + transform_header_name => fun(<<"host">>) -> <<"HOST">>; (N) -> N end + } + }), + {ok, ClientSocket} = gen_tcp:accept(ListenSocket, 5000), + {ok, http} = gun:await_up(Pid), + _ = gun:get(Pid, "/"), + {ok, Data} = gen_tcp:recv(ClientSocket, 0, 5000), + %% We do some very crude parsing of the response headers + %% to check that the header name was properly transformed. + Lines = binary:split(Data, <<"\r\n">>, [global]), + HostLines = [L || <<"HOST: ", _/bits>> = L <- Lines], + 1 = length(HostLines), + gun:close(Pid). + +unix_socket_connect(_) -> + case os:type() of + {win32, _} -> + {skip, "Unix Domain Sockets are not available on Windows."}; + _ -> + do_unix_socket_connect() + end. + +do_unix_socket_connect() -> + doc("Ensure we can send data via a unix domain socket."), + DataDir = "/tmp/gun", + SocketPath = filename:join(DataDir, "gun.sock"), + ok = filelib:ensure_dir(SocketPath), + _ = file:delete(SocketPath), + TCPOpts = [ + {ifaddr, {local, SocketPath}}, + binary, {nodelay, true}, {active, false}, + {packet, raw}, {reuseaddr, true} + ], + {ok, LSock} = gen_tcp:listen(0, TCPOpts), + Tester = self(), + Acceptor = fun() -> + {ok, S} = gen_tcp:accept(LSock), + {ok, R} = gen_tcp:recv(S, 0), + Tester ! {recv, R}, + ok = gen_tcp:close(S), + ok = gen_tcp:close(LSock) + end, + spawn(Acceptor), + {ok, Pid} = gun:open_unix(SocketPath, #{}), + _ = gun:get(Pid, "/", [{<<"host">>, <<"localhost">>}]), + receive + {recv, _} -> + gun:close(Pid) + end. + +uppercase_header_name(_) -> + doc("Header names may be given with uppercase characters."), + {ok, OriginPid, OriginPort} = init_origin(tcp, http), + {ok, Pid} = gun:open("localhost", OriginPort), + {ok, http} = gun:await_up(Pid), + handshake_completed = receive_from(OriginPid), + _ = gun:get(Pid, "/", [ + {<<"USER-agent">>, "Gun/uppercase-headers"} + ]), + Data = receive_from(OriginPid), + Lines = binary:split(Data, <<"\r\n">>, [global]), + [<<"user-agent: Gun/uppercase-headers">>] = [L || <<"user-agent: ", _/bits>> = L <- Lines], + gun:close(Pid). diff --git a/gun/test/gun_ct_hook.erl b/gun/test/gun_ct_hook.erl new file mode 100644 index 0000000..fc1a07f --- /dev/null +++ b/gun/test/gun_ct_hook.erl @@ -0,0 +1,22 @@ +%% Copyright (c) 2015-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_ct_hook). + +-export([init/2]). + +init(_, _) -> + ct_helper:start([cowboy, gun]), + ct_helper:make_certs_in_ets(), + {ok, undefined}. diff --git a/gun/test/gun_test.erl b/gun/test/gun_test.erl new file mode 100644 index 0000000..cffeed5 --- /dev/null +++ b/gun/test/gun_test.erl @@ -0,0 +1,133 @@ +%% Copyright (c) 2018-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_test). +-compile(export_all). +-compile(nowarn_export_all). + +%% Cowboy listeners. + +init_cowboy_tcp(Ref, ProtoOpts, Config) -> + {ok, _} = cowboy:start_clear(Ref, [{port, 0}], ProtoOpts), + [{ref, Ref}, {port, ranch:get_port(Ref)}|Config]. + +init_cowboy_tls(Ref, ProtoOpts, Config) -> + Opts = ct_helper:get_certs_from_ets(), + {ok, _} = cowboy:start_tls(Ref, + [{verify, verify_none}, {fail_if_no_peer_cert, false}] + ++ Opts ++ [{port, 0}], ProtoOpts), + [{ref, Ref}, {port, ranch:get_port(Ref)}|Config]. + +%% Origin server helpers. + +init_origin(Transport) -> + init_origin(Transport, http). + +init_origin(Transport, Protocol) -> + init_origin(Transport, Protocol, fun loop_origin/4). + +init_origin(Transport, Protocol, Fun) -> + Pid = spawn_link(?MODULE, init_origin, [self(), Transport, Protocol, Fun]), + Port = receive_from(Pid), + {ok, Pid, Port}. + +init_origin(Parent, Transport, Protocol, Fun) + when Transport =:= tcp; Transport =:= tcp6 -> + InetOpt = case Transport of + tcp -> inet; + tcp6 -> inet6 + end, + {ok, ListenSocket} = gen_tcp:listen(0, [binary, {active, false}, InetOpt]), + {ok, {_, Port}} = inet:sockname(ListenSocket), + Parent ! {self(), Port}, + {ok, ClientSocket} = gen_tcp:accept(ListenSocket, 5000), + case Protocol of + http2 -> http2_handshake(ClientSocket, gen_tcp); + _ -> ok + end, + Parent ! {self(), handshake_completed}, + Fun(Parent, ListenSocket, ClientSocket, gen_tcp); +init_origin(Parent, tls, Protocol, Fun) -> + Opts0 = ct_helper:get_certs_from_ets(), + Opts1 = case Protocol of + http2 -> [{alpn_preferred_protocols, [<<"h2">>]}|Opts0]; + _ -> Opts0 + end, + %% sni_hosts is necessary for SNI tests to succeed. + Opts = [{sni_hosts, [{net_adm:localhost(), []}]}|Opts1], + {ok, ListenSocket} = ssl:listen(0, [binary, {active, false}, + {fail_if_no_peer_cert, false}|Opts]), + {ok, {_, Port}} = ssl:sockname(ListenSocket), + Parent ! {self(), Port}, + {ok, ClientSocket0} = ssl:transport_accept(ListenSocket, 5000), + {ok, ClientSocket} = ssl:handshake(ClientSocket0, 5000), + case Protocol of + http2 -> + {ok, <<"h2">>} = ssl:negotiated_protocol(ClientSocket), + http2_handshake(ClientSocket, ssl); + _ -> + ok + end, + Parent ! {self(), handshake_completed}, + Fun(Parent, ListenSocket, ClientSocket, ssl). + +http2_handshake(Socket, Transport) -> + %% Send a valid preface. + ok = Transport:send(Socket, cow_http2:settings(#{})), + %% Receive the fixed sequence from the preface. + Preface = <<"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n">>, + {ok, Preface} = Transport:recv(Socket, byte_size(Preface), 5000), + %% Receive the SETTINGS from the preface. + {ok, <>} = Transport:recv(Socket, 3, 5000), + {ok, <<4:8, 0:40, _:Len/binary>>} = Transport:recv(Socket, 6 + Len, 5000), + %% Receive the WINDOW_UPDATE sent with the preface. + {ok, <<4:24, 8:8, 0:40, _:32>>} = Transport:recv(Socket, 13, 5000), + %% Send the SETTINGS ack. + ok = Transport:send(Socket, cow_http2:settings_ack()), + %% Receive the SETTINGS ack. + {ok, <<0:24, 4:8, 1:8, 0:32>>} = Transport:recv(Socket, 9, 5000), + ok. + +loop_origin(Parent, ListenSocket, ClientSocket, ClientTransport) -> + case ClientTransport:recv(ClientSocket, 0, 5000) of + {ok, Data} -> + Parent ! {self(), Data}, + loop_origin(Parent, ListenSocket, ClientSocket, ClientTransport); + {error, closed} -> + ok + end. + +%% Common helpers. + +receive_from(Pid) -> + receive_from(Pid, 5000). + +receive_from(Pid, Timeout) -> + receive + {Pid, Msg} -> + Msg + after Timeout -> + error(timeout) + end. + +receive_all_from(Pid, Timeout) -> + receive_all_from(Pid, Timeout, <<>>). + +receive_all_from(Pid, Timeout, Acc) -> + try + More = receive_from(Pid, Timeout), + receive_all_from(Pid, Timeout, <>) + catch error:timeout -> + Acc + end. diff --git a/gun/test/gun_test_event_h.erl b/gun/test/gun_test_event_h.erl new file mode 100644 index 0000000..e9cb6b1 --- /dev/null +++ b/gun/test/gun_test_event_h.erl @@ -0,0 +1,64 @@ +%% Copyright (c) 2020-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_test_event_h). +-compile(export_all). +-compile(nowarn_export_all). + +init(Event, State) -> common(?FUNCTION_NAME, Event, State). +domain_lookup_start(Event, State) -> common(?FUNCTION_NAME, Event, State). +domain_lookup_end(Event, State) -> common(?FUNCTION_NAME, Event, State). +connect_start(Event, State) -> common(?FUNCTION_NAME, Event, State). +connect_end(Event, State) -> common(?FUNCTION_NAME, Event, State). +tls_handshake_start(Event, State) -> common(?FUNCTION_NAME, Event, State). +tls_handshake_end(Event, State) -> common(?FUNCTION_NAME, Event, State). +request_start(Event, State) -> common(?FUNCTION_NAME, Event, State). +request_headers(Event, State) -> common(?FUNCTION_NAME, Event, State). +request_end(Event, State) -> common(?FUNCTION_NAME, Event, State). +push_promise_start(Event, State) -> common(?FUNCTION_NAME, Event, State). +push_promise_end(Event, State) -> common(?FUNCTION_NAME, Event, State). +response_start(Event, State) -> common(?FUNCTION_NAME, Event, State). +response_inform(Event, State) -> common(?FUNCTION_NAME, Event, State). +response_headers(Event, State) -> common(?FUNCTION_NAME, Event, State). +response_trailers(Event, State) -> common(?FUNCTION_NAME, Event, State). +response_end(Event, State) -> common(?FUNCTION_NAME, Event, State). +ws_upgrade(Event, State) -> common(?FUNCTION_NAME, Event, State). +ws_recv_frame_start(Event, State) -> common(?FUNCTION_NAME, Event, State). +ws_recv_frame_header(Event, State) -> common(?FUNCTION_NAME, Event, State). +ws_recv_frame_end(Event, State) -> common(?FUNCTION_NAME, Event, State). +ws_send_frame_start(Event, State) -> common(?FUNCTION_NAME, Event, State). +ws_send_frame_end(Event, State) -> common(?FUNCTION_NAME, Event, State). +protocol_changed(Event, State) -> common(?FUNCTION_NAME, Event, State). +origin_changed(Event, State) -> common(?FUNCTION_NAME, Event, State). +cancel(Event, State) -> common(?FUNCTION_NAME, Event, State). +disconnect(Event, State) -> common(?FUNCTION_NAME, Event, State). +terminate(Event, State) -> common(?FUNCTION_NAME, Event, State). + +common(EventType, Event, State=Pid) -> + Pid ! {self(), EventType, Event#{ + ts => erlang:system_time(millisecond) + }}, + State. + +receive_event(Pid) -> + receive + Msg = {Pid, EventType, Event} when is_atom(EventType), is_map(Event) -> + Msg + end. + +receive_event(Pid, EventType) -> + receive + Msg = {Pid, EventType, Event} when is_map(Event) -> + Msg + end. diff --git a/gun/test/gun_test_fun_event_h.erl b/gun/test/gun_test_fun_event_h.erl new file mode 100644 index 0000000..4006a69 --- /dev/null +++ b/gun/test/gun_test_fun_event_h.erl @@ -0,0 +1,55 @@ +%% Copyright (c) 2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(gun_test_fun_event_h). +-compile(export_all). +-compile(nowarn_export_all). + +init(Event, State) -> common(?FUNCTION_NAME, Event, State). +domain_lookup_start(Event, State) -> common(?FUNCTION_NAME, Event, State). +domain_lookup_end(Event, State) -> common(?FUNCTION_NAME, Event, State). +connect_start(Event, State) -> common(?FUNCTION_NAME, Event, State). +connect_end(Event, State) -> common(?FUNCTION_NAME, Event, State). +tls_handshake_start(Event, State) -> common(?FUNCTION_NAME, Event, State). +tls_handshake_end(Event, State) -> common(?FUNCTION_NAME, Event, State). +request_start(Event, State) -> common(?FUNCTION_NAME, Event, State). +request_headers(Event, State) -> common(?FUNCTION_NAME, Event, State). +request_end(Event, State) -> common(?FUNCTION_NAME, Event, State). +push_promise_start(Event, State) -> common(?FUNCTION_NAME, Event, State). +push_promise_end(Event, State) -> common(?FUNCTION_NAME, Event, State). +response_start(Event, State) -> common(?FUNCTION_NAME, Event, State). +response_inform(Event, State) -> common(?FUNCTION_NAME, Event, State). +response_headers(Event, State) -> common(?FUNCTION_NAME, Event, State). +response_trailers(Event, State) -> common(?FUNCTION_NAME, Event, State). +response_end(Event, State) -> common(?FUNCTION_NAME, Event, State). +ws_upgrade(Event, State) -> common(?FUNCTION_NAME, Event, State). +ws_recv_frame_start(Event, State) -> common(?FUNCTION_NAME, Event, State). +ws_recv_frame_header(Event, State) -> common(?FUNCTION_NAME, Event, State). +ws_recv_frame_end(Event, State) -> common(?FUNCTION_NAME, Event, State). +ws_send_frame_start(Event, State) -> common(?FUNCTION_NAME, Event, State). +ws_send_frame_end(Event, State) -> common(?FUNCTION_NAME, Event, State). +protocol_changed(Event, State) -> common(?FUNCTION_NAME, Event, State). +origin_changed(Event, State) -> common(?FUNCTION_NAME, Event, State). +cancel(Event, State) -> common(?FUNCTION_NAME, Event, State). +disconnect(Event, State) -> common(?FUNCTION_NAME, Event, State). +terminate(Event, State) -> common(?FUNCTION_NAME, Event, State). + +common(EventType, Event, State=EventFunsMap) -> + case EventFunsMap of + #{EventType := Fun} -> + Fun(EventType, Event), + State; + _ -> + State + end. diff --git a/gun/test/h2specd_SUITE.erl b/gun/test/h2specd_SUITE.erl new file mode 100644 index 0000000..888e4f4 --- /dev/null +++ b/gun/test/h2specd_SUITE.erl @@ -0,0 +1,139 @@ +%% Copyright (c) 2018-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(h2specd_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-import(ct_helper, [config/2]). +-import(ct_helper, [doc/1]). + +%% ct. + +all() -> + [h2specd]. + +init_per_suite(Config) -> + case os:getenv("H2SPECD") of + false -> {skip, "$H2SPECD isn't set."}; + H2specd -> + case filelib:is_file(H2specd) of + false -> {skip, "$H2SPECD file not found."}; + true -> + %% We ensure that SASL is started for this test suite + %% to have the crash reports in the CT logs. + {ok, Apps} = application:ensure_all_started(sasl), + [{sasl_started, Apps =/= []}|Config] + end + end. + +end_per_suite(Config) -> + case config(sasl_started, Config) of + true -> application:stop(sasl); + false -> ok + end. + +%% Tests. + +h2specd(Config) -> + doc("h2specd test suite for the HTTP/2 protocol."), + Self = self(), + Pid = spawn_link(fun() -> start_port(Config, Self) end), + receive ready -> ok after 10000 -> error(timeout) end, + try + run_tests(), + timer:sleep(100), + maybe_fail() + after + unlink(Pid), + os:cmd("killall h2specd") + end. + +start_port(Config, Pid) -> + H2specd = os:getenv("H2SPECD"), + Port = open_port( + {spawn, H2specd ++ " -S -p 45678"}, + [{line, 10000}, {cd, config(priv_dir, Config)}, binary, exit_status]), + Pid ! ready, + receive_infinity(Port, []). + +receive_infinity(Port, Acc) -> + receive + {Port, {data, {eol, Line}}} -> + ct:log("~ts", [Line]), + %% Somehow we may receive the same line multiple times. + %% We therefore only print if it's a line we didn't print before. + case lists:member(Line, Acc) of + false -> io:format(user, "~s~n", [Line]); + true -> ok + end, + receive_infinity(Port, [Line|Acc]); + {Port, Reason={exit_status, _}} -> + ct:log("~ts", [[[L, $\n] || L <- lists:reverse(Acc)]]), + exit({shutdown, Reason}) + end. + +run_tests() -> + timer:sleep(1000), + Tests = scrape_tests(), + ct:pal("Test ports: ~p~n", [Tests]), + run_tests(Tests), + timer:sleep(1000). + +run_tests([]) -> + ok; +run_tests([Port|Tail]) -> + try + {ok, Conn} = gun:open("127.0.0.1", Port, #{ + protocols => [http2], + retry => 0, + tcp_opts => [{nodelay, true}] + }), + MRef = monitor(process, Conn), + {ok, http2} = gun:await_up(Conn), + StreamRef = gun:get(Conn, "/"), + receive + {gun_response, Conn, StreamRef, _, _, _} -> + timer:sleep(100); + {'DOWN', MRef, process, Conn, _} -> + ok + after 100 -> + ok + end + after + run_tests(Tail) + end. + +scrape_tests() -> + {ok, Conn} = gun:open("127.0.0.1", 45678), + {ok, http} = gun:await_up(Conn), + StreamRef = gun:get(Conn, "/"), + {response, nofin, 200, _} = gun:await(Conn, StreamRef), + {ok, Body} = gun:await_body(Conn, StreamRef), + ok = gun:close(Conn), + {match, Matches} = re:run(Body, ">] <- Matches]. + +maybe_fail() -> + {ok, Conn} = gun:open("127.0.0.1", 45678), + {ok, http} = gun:await_up(Conn), + StreamRef = gun:get(Conn, "/report", [{<<"accept">>, "text/plain"}]), + {response, nofin, 200, _} = gun:await(Conn, StreamRef), + {ok, Body} = gun:await_body(Conn, StreamRef), + ok = gun:close(Conn), + case binary:match(Body, <<"0 skipped, 0 failed">>) of + nomatch -> exit(failed); + _ -> ok + end. diff --git a/gun/test/handlers/cookie_echo_h.erl b/gun/test/handlers/cookie_echo_h.erl new file mode 100644 index 0000000..28c5dde --- /dev/null +++ b/gun/test/handlers/cookie_echo_h.erl @@ -0,0 +1,11 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(cookie_echo_h). + +-export([init/2]). + +init(Req, State) -> + {ok, cowboy_req:reply(200, + #{<<"content-type">> => <<"text/plain">>}, + cowboy_req:header(<<"cookie">>, Req, <<"UNDEF">>), + Req), State}. diff --git a/gun/test/handlers/cookie_informational_h.erl b/gun/test/handlers/cookie_informational_h.erl new file mode 100644 index 0000000..71e1b4a --- /dev/null +++ b/gun/test/handlers/cookie_informational_h.erl @@ -0,0 +1,10 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(cookie_informational_h). + +-export([init/2]). + +init(Req0, State) -> + cowboy_req:inform(103, #{<<"set-cookie">> => [<<"informational=1">>]}, Req0), + Req = cowboy_req:reply(204, #{<<"set-cookie">> => [<<"final=1">>]}, Req0), + {ok, Req, State}. diff --git a/gun/test/handlers/cookie_parser_h.erl b/gun/test/handlers/cookie_parser_h.erl new file mode 100644 index 0000000..cff5901 --- /dev/null +++ b/gun/test/handlers/cookie_parser_h.erl @@ -0,0 +1,23 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(cookie_parser_h). + +-export([init/2]). + +init(Req0=#{qs := Qs}, State) -> + %% Hardcoded path, but I doubt it's going to break anytime soon. + TestFile = iolist_to_binary(["../../test/wpt/cookies/", Qs, "-test"]), + {ok, Test} = file:read_file(TestFile), + %% We don't want the final empty line. + Lines = lists:reverse(tl(lists:reverse(string:split(Test, <<"\n">>, all)))), + Req = lists:foldl(fun + (<<"Set-Cookie: ",SetCookie/bits>>, Req1) -> + %% We do not use set_resp_cookie because we want to preserve ordering. + SetCookieList = cowboy_req:resp_header(<<"set-cookie">>, Req1, []), + cowboy_req:set_resp_header(<<"set-cookie">>, SetCookieList ++ [SetCookie], Req1); + (<<"Set-Cookie:">>, Req1) -> + Req1; + (<<"Location: ",Location/bits>>, Req1) -> + cowboy_req:set_resp_header(<<"location">>, Location, Req1) + end, Req0, Lines), + {ok, cowboy_req:reply(204, Req), State}. diff --git a/gun/test/handlers/cookie_parser_result_h.erl b/gun/test/handlers/cookie_parser_result_h.erl new file mode 100644 index 0000000..a1fa899 --- /dev/null +++ b/gun/test/handlers/cookie_parser_result_h.erl @@ -0,0 +1,25 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(cookie_parser_result_h). + +-export([init/2]). + +init(Req=#{qs := Qs}, State) -> + %% Hardcoded path, but I doubt it's going to break anytime soon. + ExpectedFile = iolist_to_binary(["../../test/wpt/cookies/", Qs, "-expected"]), + CookieHd = cowboy_req:header(<<"cookie">>, Req), + case file:read_file(ExpectedFile) of + {ok, Expected} when Expected =:= <<>>; Expected =:= <<"\n">> -> + undefined = CookieHd, + ok; + {ok, <<"Cookie: ",CookiesBin0/bits>>} -> + %% We only care about the first line. + [CookiesBin, <<>>|_] = string:split(CookiesBin0, <<"\n">>, all), + CookiesBin = CookieHd, + ok + end, + %% We echo back the cookie header in order to log it. + {ok, cowboy_req:reply(204, case CookieHd of + undefined -> #{<<"x-no-cookie-received">> => <<"Cookie header missing.">>}; + _ -> #{<<"x-cookie-received">> => CookieHd} + end, Req), State}. diff --git a/gun/test/handlers/cookie_set_h.erl b/gun/test/handlers/cookie_set_h.erl new file mode 100644 index 0000000..93ad86d --- /dev/null +++ b/gun/test/handlers/cookie_set_h.erl @@ -0,0 +1,42 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(cookie_set_h). + +-export([init/2]). + +init(Req0, State) -> + SetCookieList = set_cookie_list(Req0), + Req = cowboy_req:set_resp_header(<<"set-cookie">>, SetCookieList, Req0), + {ok, cowboy_req:reply(204, Req), State}. + +-define(HOST, "web-platform.test"). + +set_cookie_list(#{qs := <<"domain_with_and_without_leading_period">>}) -> + [ + <<"a=b; Path=/; Domain=." ?HOST>>, + <<"a=c; Path=/; Domain=" ?HOST>> + ]; +set_cookie_list(#{qs := <<"domain_with_leading_period">>}) -> + [<<"a=b; Path=/; Domain=." ?HOST>>]; +set_cookie_list(#{qs := <<"domain_matches_host">>}) -> + [<<"a=b; Path=/; Domain=" ?HOST>>]; +set_cookie_list(#{qs := <<"domain_missing">>}) -> + [<<"a=b; Path=/;">>]; +set_cookie_list(#{qs := <<"path_default">>}) -> + [<<"cookie-path-default=1">>]; +set_cookie_list(#{qs := <<"path_default_expire">>}) -> + [<<"cookie-path-default=1; Max-Age=0">>]; +set_cookie_list(#{qs := <<"path=",Path/bits>>}) -> + [[<<"a=b; Path=">>, Path]]; +set_cookie_list(Req=#{qs := <<"prefix">>}) -> + [cowboy_req:header(<<"please-set-cookie">>, Req)]; +set_cookie_list(#{qs := <<"secure_http">>}) -> + [<<"secure_from_nonsecure_http=1; Secure; Path=/">>]; +set_cookie_list(#{qs := <<"secure_https">>}) -> + [<<"secure_from_secure_http=1; Secure; Path=/">>]; +set_cookie_list(Req=#{qs := <<"ttb=",_/bits>>}) -> + #{ttb := SetCookies} = cowboy_req:match_qs([ttb], Req), + case binary_to_term(SetCookies) of + List when is_list(List) -> List; + Bin -> [Bin] + end. diff --git a/gun/test/handlers/delayed_hello_h.erl b/gun/test/handlers/delayed_hello_h.erl new file mode 100644 index 0000000..68ef1ad --- /dev/null +++ b/gun/test/handlers/delayed_hello_h.erl @@ -0,0 +1,11 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(delayed_hello_h). + +-export([init/2]). + +init(Req, Timeout) -> + timer:sleep(Timeout), + {ok, cowboy_req:reply(200, #{ + <<"content-type">> => <<"text/plain">> + }, <<"Hello world!">>, Req), Timeout}. diff --git a/gun/test/handlers/delayed_push_h.erl b/gun/test/handlers/delayed_push_h.erl new file mode 100644 index 0000000..dbb8e56 --- /dev/null +++ b/gun/test/handlers/delayed_push_h.erl @@ -0,0 +1,13 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(delayed_push_h). + +-export([init/2]). + +init(Req, Timeout) -> + timer:sleep(Timeout), + cowboy_req:push("/", #{<<"accept">> => <<"text/plain">>}, Req), + cowboy_req:push("/empty", #{<<"accept">> => <<"text/plain">>}, Req), + {ok, cowboy_req:reply(200, #{ + <<"content-type">> => <<"text/plain">> + }, <<"Hello world!">>, Req), Timeout}. diff --git a/gun/test/handlers/empty_h.erl b/gun/test/handlers/empty_h.erl new file mode 100644 index 0000000..7b634cb --- /dev/null +++ b/gun/test/handlers/empty_h.erl @@ -0,0 +1,11 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(empty_h). + +-export([init/2]). + +init(Req, State) -> + {ok, cowboy_req:reply(200, #{ + <<"content-type">> => <<"text/plain">> + }, Req), State}. + diff --git a/gun/test/handlers/hello_h.erl b/gun/test/handlers/hello_h.erl new file mode 100644 index 0000000..e3c71ab --- /dev/null +++ b/gun/test/handlers/hello_h.erl @@ -0,0 +1,10 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(hello_h). + +-export([init/2]). + +init(Req, State) -> + {ok, cowboy_req:reply(200, #{ + <<"content-type">> => <<"text/plain">> + }, <<"Hello world!">>, Req), State}. diff --git a/gun/test/handlers/inform_h.erl b/gun/test/handlers/inform_h.erl new file mode 100644 index 0000000..f62b31f --- /dev/null +++ b/gun/test/handlers/inform_h.erl @@ -0,0 +1,16 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(inform_h). + +-export([init/2]). + +init(Req, State) -> + cowboy_req:inform(103, #{ + <<"content-type">> => <<"text/plain">> + }, Req), + cowboy_req:inform(103, #{ + <<"content-type">> => <<"text/plain">> + }, Req), + {ok, cowboy_req:reply(200, #{ + <<"content-type">> => <<"text/plain">> + }, <<"Hello world!">>, Req), State}. diff --git a/gun/test/handlers/pool_ws_handler.erl b/gun/test/handlers/pool_ws_handler.erl new file mode 100644 index 0000000..5475e88 --- /dev/null +++ b/gun/test/handlers/pool_ws_handler.erl @@ -0,0 +1,13 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(pool_ws_handler). + +-export([init/4]). +-export([handle/2]). + +init(_, _, _, #{user_opts := ReplyTo}) -> + {ok, ReplyTo}. + +handle(Frame, ReplyTo) -> + ReplyTo ! Frame, + {ok, 0, ReplyTo}. diff --git a/gun/test/handlers/proxied_h.erl b/gun/test/handlers/proxied_h.erl new file mode 100644 index 0000000..818823c --- /dev/null +++ b/gun/test/handlers/proxied_h.erl @@ -0,0 +1,11 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(proxied_h). + +-export([init/2]). + +-spec init(cowboy_req:req(), _) -> no_return(). +init(Req, _) -> + _ = cowboy_req:stream_reply(200, #{<<"content-type">> => <<"text/plain">>}, Req), + %% We never return to allow querying the stream_info. + receive after infinity -> ok end. diff --git a/gun/test/handlers/push_h.erl b/gun/test/handlers/push_h.erl new file mode 100644 index 0000000..4184227 --- /dev/null +++ b/gun/test/handlers/push_h.erl @@ -0,0 +1,12 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(push_h). + +-export([init/2]). + +init(Req, State) -> + cowboy_req:push("/", #{<<"accept">> => <<"text/plain">>}, Req), + cowboy_req:push("/empty", #{<<"accept">> => <<"text/plain">>}, Req), + {ok, cowboy_req:reply(200, #{ + <<"content-type">> => <<"text/plain">> + }, <<"Hello world!">>, Req), State}. diff --git a/gun/test/handlers/sse_clock_close_h.erl b/gun/test/handlers/sse_clock_close_h.erl new file mode 100644 index 0000000..c5911ff --- /dev/null +++ b/gun/test/handlers/sse_clock_close_h.erl @@ -0,0 +1,23 @@ +%% This module implements a loop handler that sends +%% the current time every second using SSE. In contrast +%% to sse_clock_h, this one sends a "Connection: close" +%% header. + +-module(sse_clock_close_h). + +-export([init/2]). +-export([info/3]). + +init(Req, State) -> + self() ! timeout, + {cowboy_loop, cowboy_req:stream_reply(200, #{ + <<"content-type">> => <<"text/event-stream">>, + <<"connection">> => <<"close">> + }, Req), State}. + +info(timeout, Req, State) -> + erlang:send_after(1000, self(), timeout), + cowboy_req:stream_events(#{ + data => cowboy_clock:rfc1123() + }, nofin, Req), + {ok, Req, State}. diff --git a/gun/test/handlers/sse_clock_h.erl b/gun/test/handlers/sse_clock_h.erl new file mode 100644 index 0000000..dcc7c3f --- /dev/null +++ b/gun/test/handlers/sse_clock_h.erl @@ -0,0 +1,25 @@ +%% This module implements a loop handler that sends +%% the current time every second using SSE. + +-module(sse_clock_h). + +-export([init/2]). +-export([info/3]). + +init(Req, State) -> + self() ! timeout, + {cowboy_loop, cowboy_req:stream_reply(200, #{ + <<"content-type">> => <<"text/event-stream">> + }, Req), State}. + +info(timeout, Req, State) -> + erlang:send_after(1000, self(), timeout), + cowboy_req:stream_events(#{ + data => data(State) + }, nofin, Req), + {ok, Req, State}. + +data(date) -> + cowboy_clock:rfc1123(); +data(Size) when is_integer(Size) -> + lists:duplicate(Size, $0). diff --git a/gun/test/handlers/sse_lone_id_h.erl b/gun/test/handlers/sse_lone_id_h.erl new file mode 100644 index 0000000..3684fa9 --- /dev/null +++ b/gun/test/handlers/sse_lone_id_h.erl @@ -0,0 +1,19 @@ +%% This module implements a loop handler that sends +%% a lone id: line. + +-module(sse_lone_id_h). + +-export([init/2]). +-export([info/3]). + +init(Req, State) -> + self() ! timeout, + {cowboy_loop, cowboy_req:stream_reply(200, #{ + <<"content-type">> => <<"text/event-stream">> + }, Req), State}. + +info(timeout, Req, State) -> + cowboy_req:stream_events(#{ + id => <<"hello">> + }, nofin, Req), + {stop, Req, State}. diff --git a/gun/test/handlers/sse_mime_param_h.erl b/gun/test/handlers/sse_mime_param_h.erl new file mode 100644 index 0000000..7e0c0e9 --- /dev/null +++ b/gun/test/handlers/sse_mime_param_h.erl @@ -0,0 +1,19 @@ +%% This module implements a loop handler that sends +%% a lone id: line. + +-module(sse_mime_param_h). + +-export([init/2]). +-export([info/3]). + +init(Req, State) -> + self() ! timeout, + {cowboy_loop, cowboy_req:stream_reply(200, #{ + <<"content-type">> => <<"text/event-stream;encoding=UTF-8">> + }, Req), State}. + +info(timeout, Req, State) -> + cowboy_req:stream_events(#{ + id => <<"hello">> + }, nofin, Req), + {stop, Req, State}. diff --git a/gun/test/handlers/stream_h.erl b/gun/test/handlers/stream_h.erl new file mode 100644 index 0000000..fd6774e --- /dev/null +++ b/gun/test/handlers/stream_h.erl @@ -0,0 +1,14 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(stream_h). + +-export([init/2]). + +init(Req0, State) -> + Req = cowboy_req:stream_reply(200, #{ + <<"content-type">> => <<"text/plain">> + }, Req0), + cowboy_req:stream_body(<<"Hello ">>, nofin, Req), + cowboy_req:stream_body(<<"world!">>, nofin, Req), + %% The stream will be closed by Cowboy. + {ok, Req, State}. diff --git a/gun/test/handlers/trailers_h.erl b/gun/test/handlers/trailers_h.erl new file mode 100644 index 0000000..a94d974 --- /dev/null +++ b/gun/test/handlers/trailers_h.erl @@ -0,0 +1,18 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(trailers_h). + +-export([init/2]). + +init(Req0, State) -> + Req = cowboy_req:stream_reply(200, #{ + <<"content-type">> => <<"text/plain">>, + <<"trailer">> => <<"expires">> + }, Req0), + cowboy_req:stream_body(<<"Hello ">>, nofin, Req), + cowboy_req:stream_body(<<"world!">>, nofin, Req), + cowboy_req:stream_trailers(#{ + <<"expires">> => <<"Sun, 10 Dec 2017 19:13:47 GMT">> + }, Req), + {ok, Req, State}. + diff --git a/gun/test/handlers/ws_cookie_h.erl b/gun/test/handlers/ws_cookie_h.erl new file mode 100644 index 0000000..39889b3 --- /dev/null +++ b/gun/test/handlers/ws_cookie_h.erl @@ -0,0 +1,24 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(ws_cookie_h). + +-export([init/2]). +-export([websocket_handle/2]). +-export([websocket_info/2]). + +init(Req0, _) -> + Req = cowboy_req:set_resp_header(<<"set-cookie">>, + [<<"ws_cookie=1; Secure; path=/">>], Req0), + {cowboy_websocket, Req, undefined, #{ + compress => true + }}. + +websocket_handle({text, Data}, State) -> + {[{text, Data}], State}; +websocket_handle({binary, Data}, State) -> + {[{binary, Data}], State}; +websocket_handle(_Frame, State) -> + {[], State}. + +websocket_info(_Info, State) -> + {[], State}. diff --git a/gun/test/handlers/ws_echo_h.erl b/gun/test/handlers/ws_echo_h.erl new file mode 100644 index 0000000..692e6a6 --- /dev/null +++ b/gun/test/handlers/ws_echo_h.erl @@ -0,0 +1,22 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(ws_echo_h). + +-export([init/2]). +-export([websocket_handle/2]). +-export([websocket_info/2]). + +init(Req, _) -> + {cowboy_websocket, Req, undefined, #{ + compress => true + }}. + +websocket_handle({text, Data}, State) -> + {[{text, Data}], State}; +websocket_handle({binary, Data}, State) -> + {[{binary, Data}], State}; +websocket_handle(_Frame, State) -> + {[], State}. + +websocket_info(_Info, State) -> + {[], State}. diff --git a/gun/test/handlers/ws_frozen_h.erl b/gun/test/handlers/ws_frozen_h.erl new file mode 100644 index 0000000..bac77c2 --- /dev/null +++ b/gun/test/handlers/ws_frozen_h.erl @@ -0,0 +1,23 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(ws_frozen_h). + +-export([init/2]). +-export([websocket_init/1]). +-export([websocket_handle/2]). +-export([websocket_info/2]). + +init(Req, State) -> + {cowboy_websocket, Req, State, #{ + compress => true + }}. + +websocket_init(Timeout) -> + timer:sleep(Timeout), + {ok, undefined}. + +websocket_handle(_Frame, State) -> + {[], State}. + +websocket_info(_Info, State) -> + {[], State}. diff --git a/gun/test/handlers/ws_reject_h.erl b/gun/test/handlers/ws_reject_h.erl new file mode 100644 index 0000000..c58e672 --- /dev/null +++ b/gun/test/handlers/ws_reject_h.erl @@ -0,0 +1,7 @@ +%% This handler rejects all Websocket connections. +-module(ws_reject_h). + +-export([init/2]). + +init(Req0, Env) -> + {ok, cowboy_req:reply(400, #{}, <<"Upgrade rejected">>, Req0), Env}. diff --git a/gun/test/handlers/ws_subprotocol_h.erl b/gun/test/handlers/ws_subprotocol_h.erl new file mode 100644 index 0000000..509d74d --- /dev/null +++ b/gun/test/handlers/ws_subprotocol_h.erl @@ -0,0 +1,31 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(ws_subprotocol_h). + +-export([init/2]). +-export([websocket_handle/2]). +-export([websocket_info/2]). + +init(Req, State) -> + Protos = cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req), + init_protos(Req, State, Protos). + +init_protos(Req, State, undefined) -> + {ok, cowboy_req:reply(400, #{}, <<"undefined">>, Req), State}; +init_protos(Req, State, []) -> + {ok, cowboy_req:reply(400, #{}, <<"nomatch">>, Req), State}; +init_protos(Req0, State, [<<"echo">> | _]) -> + Req = cowboy_req:set_resp_header(<<"sec-websocket-protocol">>, <<"echo">>, Req0), + {cowboy_websocket, Req, State}; +init_protos(Req, State, [_ | Protos]) -> + init_protos(Req, State, Protos). + +websocket_handle({text, Data}, State) -> + {[{text, Data}], State}; +websocket_handle({binary, Data}, State) -> + {[{binary, Data}], State}; +websocket_handle(_Frame, State) -> + {[], State}. + +websocket_info(_Info, State) -> + {[], State}. diff --git a/gun/test/handlers/ws_timeout_close_h.erl b/gun/test/handlers/ws_timeout_close_h.erl new file mode 100644 index 0000000..6fef168 --- /dev/null +++ b/gun/test/handlers/ws_timeout_close_h.erl @@ -0,0 +1,25 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(ws_timeout_close_h). + +-export([init/2]). +-export([websocket_init/1]). +-export([websocket_handle/2]). +-export([websocket_info/2]). + +init(Req, State) -> + {cowboy_websocket, Req, State, #{ + compress => true + }}. + +websocket_init(Timeout) -> + _ = erlang:send_after(Timeout, self(), timeout_close), + {[], undefined}. + +websocket_handle(_Frame, State) -> + {[], State}. + +websocket_info(timeout_close, State) -> + {[{close, 3333, <<>>}], State}; +websocket_info(_Info, State) -> + {[], State}. diff --git a/gun/test/pool_SUITE.erl b/gun/test/pool_SUITE.erl new file mode 100644 index 0000000..62280eb --- /dev/null +++ b/gun/test/pool_SUITE.erl @@ -0,0 +1,408 @@ +%% Copyright (c) 2021-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(pool_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-import(ct_helper, [doc/1]). +-import(ct_helper, [config/2]). +-import(gun_test, [receive_from/1]). + +all() -> + ct_helper:all(?MODULE). + +init_per_suite(Config) -> + {ok, _} = cowboy:start_clear({?MODULE, tcp}, [], do_proto_opts()), + Port = ranch:get_port({?MODULE, tcp}), + [{port, Port}|Config]. + +end_per_suite(_) -> + ExtraListeners = [ + max_streams_h2_size_1, + max_streams_h2_size_2, + reconnect_h1 + ], + _ = [cowboy:stop_listener(Listener) || Listener <- ExtraListeners], + ok. + +do_proto_opts() -> + Routes = [ + {"/", hello_h, []}, + {"/delay", delayed_hello_h, 3000}, + {"/ws", ws_echo_h, []} + ], + #{ + env => #{dispatch => cowboy_router:compile([{'_', Routes}])} + }. + +%% Tests. + +hello_pool_h1(Config) -> + doc("Confirm the pool can be used for HTTP/1.1 connections."), + Port = config(port, Config), + {ok, ManagerPid} = gun_pool:start_pool("localhost", Port, #{ + conn_opts => #{protocols => [http]}, + scope => ?FUNCTION_NAME + }), + gun_pool:await_up(ManagerPid), + Streams = [{async, _} = gun_pool:get("/", + #{<<"host">> => ["localhost:", integer_to_binary(Port)]}, + #{scope => ?FUNCTION_NAME} + ) || _ <- lists:seq(1, 8)], + _ = [begin + {response, nofin, 200, _} = gun_pool:await(StreamRef), + {ok, <<"Hello world!">>} = gun_pool:await_body(StreamRef) + end || {async, StreamRef} <- Streams]. + +hello_pool_h2(Config) -> + doc("Confirm the pool can be used for HTTP/2 connections."), + Port = config(port, Config), + {ok, ManagerPid} = gun_pool:start_pool("localhost", Port, #{ + conn_opts => #{protocols => [http2]}, + scope => ?FUNCTION_NAME + }), + gun_pool:await_up(ManagerPid), + Streams = [{async, _} = gun_pool:get("/", + #{<<"host">> => ["localhost:", integer_to_binary(Port)]}, + #{scope => ?FUNCTION_NAME} + ) || _ <- lists:seq(1, 800)], + _ = [begin + {response, nofin, 200, _} = gun_pool:await(StreamRef), + {ok, <<"Hello world!">>} = gun_pool:await_body(StreamRef) + end || {async, StreamRef} <- Streams]. + +hello_pool_ws(Config) -> + doc("Confirm the pool can be used for HTTP/1.1 connections upgraded to Websocket."), + Port = config(port, Config), + {ok, ManagerPid} = gun_pool:start_pool("localhost", Port, #{ + conn_opts => #{ + protocols => [http], + ws_opts => #{ + default_protocol => pool_ws_handler, + user_opts => self() + } + }, + scope => ?FUNCTION_NAME, + setup_fun => {fun + (ConnPid, {gun_up, _, http}, SetupState) -> + _ = gun:ws_upgrade(ConnPid, "/ws"), + {setup, SetupState}; + (_, {gun_upgrade, _, StreamRef, _, _}, _) -> + {up, ws, #{ws => StreamRef}}; + (ConnPid, Msg, SetupState) -> + ct:pal("Unexpected setup message for ~p: ~p", [ConnPid, Msg]), + {setup, SetupState} + end, undefined} + }), + gun_pool:await_up(ManagerPid), + _ = [gun_pool:ws_send({text, <<"Hello world!">>}, #{ + authority => ["localhost:", integer_to_binary(Port)], + scope => ?FUNCTION_NAME + }) || _ <- lists:seq(1, 8)], + %% The pool_ws_handler module sends frames back to us. + _ = [receive + {text, <<"Hello world!">>} -> + ok + end || _ <- lists:seq(1, 8)]. + +max_streams_h1(Config) -> + doc("Confirm requests are rejected when the maximum number " + "of streams is reached for HTTP/1.1 connections."), + Port = config(port, Config), + Authority = ["localhost:", integer_to_binary(Port)], + {ok, ManagerPid} = gun_pool:start_pool("localhost", Port, #{ + conn_opts => #{protocols => [http]}, + scope => ?FUNCTION_NAME, + size => 1 + }), + gun_pool:await_up(ManagerPid), + {async, _} = gun_pool:get("/delay", + #{<<"host">> => Authority}, #{scope => ?FUNCTION_NAME}), + timer:sleep(500), + {error, no_connection_available, _} = gun_pool:get("/delay", + #{<<"host">> => Authority}, #{scope => ?FUNCTION_NAME}). + +max_streams_h1_retry(Config) -> + doc("Confirm connection checkout is retried when the maximum number " + "of streams is reached for HTTP/1.1 connections."), + Port = config(port, Config), + Authority = ["localhost:", integer_to_binary(Port)], + {ok, ManagerPid} = gun_pool:start_pool("localhost", Port, #{ + conn_opts => #{protocols => [http]}, + scope => ?FUNCTION_NAME, + size => 1 + }), + gun_pool:await_up(ManagerPid), + {async, _} = gun_pool:get("/delay", + #{<<"host">> => Authority}, #{scope => ?FUNCTION_NAME}), + timer:sleep(500), + {error, no_connection_available, _} = gun_pool:get("/delay", + #{<<"host">> => Authority}, #{scope => ?FUNCTION_NAME}), + {async, _} = gun_pool:get("/delay", #{<<"host">> => Authority}, #{ + checkout_retry => [100, 500, 500, 500, 500, 500, 500], + scope => ?FUNCTION_NAME + }). + +max_streams_h2_size_1(_) -> + doc("Confirm requests are rejected when the maximum number " + "of streams is reached for HTTP/2 connections."), + ProtoOpts = do_proto_opts(), + {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], ProtoOpts#{ + max_concurrent_streams => 5 + }), + Port = ranch:get_port(?FUNCTION_NAME), + Authority = ["localhost:", integer_to_binary(Port)], + {ok, ManagerPid} = gun_pool:start_pool("localhost", Port, #{ + conn_opts => #{protocols => [http2]}, + size => 1 + }), + gun_pool:await_up(ManagerPid), + [{async, _} = gun_pool:get("/delay", #{<<"host">> => Authority}) || _ <- lists:seq(1, 5)], + timer:sleep(500), + {error, no_connection_available, _} = gun_pool:get("/delay", #{<<"host">> => Authority}). + +max_streams_h2_size_1_retry(_) -> + doc("Confirm connection checkout is retried when the maximum number " + "of streams is reached for HTTP/2 connections."), + ProtoOpts = do_proto_opts(), + {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], ProtoOpts#{ + max_concurrent_streams => 5 + }), + Port = ranch:get_port(?FUNCTION_NAME), + Authority = ["localhost:", integer_to_binary(Port)], + {ok, ManagerPid} = gun_pool:start_pool("localhost", Port, #{ + conn_opts => #{protocols => [http2]}, + size => 1 + }), + gun_pool:await_up(ManagerPid), + [{async, _} = gun_pool:get("/delay", #{<<"host">> => Authority}) || _ <- lists:seq(1, 5)], + timer:sleep(500), + {error, no_connection_available, _} = gun_pool:get("/delay", #{<<"host">> => Authority}), + {async, _} = gun_pool:get("/delay", #{<<"host">> => Authority}, #{ + checkout_retry => [100, 500, 500, 500, 500, 500, 500] + }). + +max_streams_h2_size_2(_) -> + doc("Confirm requests are rejected when the maximum number " + "of streams is reached for HTTP/2 connections."), + ProtoOpts = do_proto_opts(), + {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], ProtoOpts#{ + max_concurrent_streams => 5 + }), + Port = ranch:get_port(?FUNCTION_NAME), + Authority = ["localhost:", integer_to_binary(Port)], + {ok, ManagerPid} = gun_pool:start_pool("localhost", Port, #{ + conn_opts => #{protocols => [http2]}, + size => 2 + }), + gun_pool:await_up(ManagerPid), + [begin + {async, _} = gun_pool:get("/delay", #{<<"host">> => Authority}), + %% We need to wait a bit for the request to be sent because the + %% request is sent and counted asynchronously. + timer:sleep(10) + end || _ <- lists:seq(1, 10)], + timer:sleep(500), + {error, no_connection_available, _} = gun_pool:get("/delay", #{<<"host">> => Authority}). + +max_streams_h2_size_2_retry(_) -> + doc("Confirm connection checkout is retried when the maximum number " + "of streams is reached for HTTP/2 connections."), + ProtoOpts = do_proto_opts(), + {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], ProtoOpts#{ + max_concurrent_streams => 5 + }), + Port = ranch:get_port(?FUNCTION_NAME), + Authority = ["localhost:", integer_to_binary(Port)], + {ok, ManagerPid} = gun_pool:start_pool("localhost", Port, #{ + conn_opts => #{protocols => [http2]}, + size => 2 + }), + gun_pool:await_up(ManagerPid), + [begin + {async, _} = gun_pool:get("/delay", #{<<"host">> => Authority}), + %% We need to wait a bit for the request to be sent because the + %% request is sent and counted asynchronously. + timer:sleep(10) + end || _ <- lists:seq(1, 10)], + timer:sleep(500), + {error, no_connection_available, _} = gun_pool:get("/delay", #{<<"host">> => Authority}), + {async, _} = gun_pool:get("/delay", #{<<"host">> => Authority}, #{ + checkout_retry => [100, 500, 500, 500, 500, 500, 500] + }). + +kill_restart_h1(Config) -> + doc("Confirm the Gun process is restarted and the pool operational " + "after an HTTP/1.1 Gun process has crashed."), + Port = config(port, Config), + Authority = ["localhost:", integer_to_binary(Port)], + {ok, ManagerPid} = gun_pool:start_pool("localhost", Port, #{ + conn_opts => #{protocols => [http]}, + scope => ?FUNCTION_NAME + }), + gun_pool:await_up(ManagerPid), + Streams1 = [{async, _} = gun_pool:get("/", + #{<<"host">> => Authority}, + #{scope => ?FUNCTION_NAME} + ) || _ <- lists:seq(1, 8)], + _ = [begin + {response, nofin, 200, _} = gun_pool:await(StreamRef), + {ok, <<"Hello world!">>} = gun_pool:await_body(StreamRef) + end || {async, StreamRef} <- Streams1], + %% Get a connection and kill the process. + {operational, #{conns := Conns}} = gun_pool:info(ManagerPid), + ConnPid = hd(maps:keys(Conns)), + MRef = monitor(process, ConnPid), + exit(ConnPid, {shutdown, ?FUNCTION_NAME}), + receive {'DOWN', MRef, process, ConnPid, _} -> ok end, + {degraded, _} = gun_pool:info(ManagerPid), + gun_pool:await_up(ManagerPid), + Streams2 = [{async, _} = gun_pool:get("/", + #{<<"host">> => Authority}, + #{scope => ?FUNCTION_NAME} + ) || _ <- lists:seq(1, 8)], + _ = [begin + {response, nofin, 200, _} = gun_pool:await(StreamRef), + {ok, <<"Hello world!">>} = gun_pool:await_body(StreamRef) + end || {async, StreamRef} <- Streams2]. + +kill_restart_h2(Config) -> + doc("Confirm the Gun process is restarted and the pool operational " + "after an HTTP/2 Gun process has crashed."), + Port = config(port, Config), + Authority = ["localhost:", integer_to_binary(Port)], + {ok, ManagerPid} = gun_pool:start_pool("localhost", Port, #{ + conn_opts => #{protocols => [http2]}, + scope => ?FUNCTION_NAME + }), + gun_pool:await_up(ManagerPid), + Streams1 = [{async, _} = gun_pool:get("/", + #{<<"host">> => Authority}, + #{scope => ?FUNCTION_NAME} + ) || _ <- lists:seq(1, 800)], + _ = [begin + {response, nofin, 200, _} = gun_pool:await(StreamRef), + {ok, <<"Hello world!">>} = gun_pool:await_body(StreamRef) + end || {async, StreamRef} <- Streams1], + %% Get a connection and kill the process. + {operational, #{conns := Conns}} = gun_pool:info(ManagerPid), + ConnPid = hd(maps:keys(Conns)), + MRef = monitor(process, ConnPid), + exit(ConnPid, {shutdown, ?FUNCTION_NAME}), + receive {'DOWN', MRef, process, ConnPid, _} -> ok end, + {degraded, _} = gun_pool:info(ManagerPid), + gun_pool:await_up(ManagerPid), + Streams2 = [{async, _} = gun_pool:get("/", + #{<<"host">> => Authority}, + #{scope => ?FUNCTION_NAME} + ) || _ <- lists:seq(1, 800)], + _ = [begin + {response, nofin, 200, _} = gun_pool:await(StreamRef), + {ok, <<"Hello world!">>} = gun_pool:await_body(StreamRef) + end || {async, StreamRef} <- Streams2]. + +%% @todo kill_restart_ws + +reconnect_h1(_) -> + doc("Confirm the Gun process reconnects automatically for HTTP/1.1 connections."), + ProtoOpts = do_proto_opts(), + {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], ProtoOpts#{ + idle_timeout => 500, + scope => ?FUNCTION_NAME + }), + Port = ranch:get_port(?FUNCTION_NAME), + Authority = ["localhost:", integer_to_binary(Port)], + {ok, ManagerPid} = gun_pool:start_pool("localhost", Port, #{ + conn_opts => #{protocols => [http]} + }), + gun_pool:await_up(ManagerPid), + Streams1 = [{async, _} = gun_pool:get("/", #{<<"host">> => Authority}) || _ <- lists:seq(1, 8)], + _ = [begin + {response, nofin, 200, _} = gun_pool:await(StreamRef), + {ok, <<"Hello world!">>} = gun_pool:await_body(StreamRef) + end || {async, StreamRef} <- Streams1], + %% Wait for the idle timeout to trigger. + timer:sleep(600), +% {degraded, _} = gun_pool:info(ManagerPid), + gun_pool:await_up(ManagerPid), + Streams2 = [{async, _} = gun_pool:get("/", #{<<"host">> => Authority}) || _ <- lists:seq(1, 8)], + _ = [begin + {response, nofin, 200, _} = gun_pool:await(StreamRef), + {ok, <<"Hello world!">>} = gun_pool:await_body(StreamRef) + end || {async, StreamRef} <- Streams2]. + +reconnect_h2(Config) -> + doc("Confirm the Gun process reconnects automatically for HTTP/2 connections."), + Port = config(port, Config), + Authority = ["localhost:", integer_to_binary(Port)], + {ok, ManagerPid} = gun_pool:start_pool("localhost", Port, #{ + conn_opts => #{protocols => [http2]}, + scope => ?FUNCTION_NAME + }), + gun_pool:await_up(ManagerPid), + Streams1 = [{async, _} = gun_pool:get("/", + #{<<"host">> => Authority}, + #{scope => ?FUNCTION_NAME} + ) || _ <- lists:seq(1, 800)], + _ = [begin + {response, nofin, 200, _} = gun_pool:await(StreamRef), + {ok, <<"Hello world!">>} = gun_pool:await_body(StreamRef) + end || {async, StreamRef} <- Streams1], + %% Wait for the idle timeout to trigger. + timer:sleep(600), +% {degraded, _} = gun_pool:info(ManagerPid), + gun_pool:await_up(ManagerPid), + Streams2 = [{async, _} = gun_pool:get("/", + #{<<"host">> => Authority}, + #{scope => ?FUNCTION_NAME} + ) || _ <- lists:seq(1, 800)], + _ = [begin + {response, nofin, 200, _} = gun_pool:await(StreamRef), + {ok, <<"Hello world!">>} = gun_pool:await_body(StreamRef) + end || {async, StreamRef} <- Streams2]. + +%% @todo reconnect_ws + +stop_pool(Config) -> + doc("Confirm the pool can be stopped."), + Port = config(port, Config), + {ok, ManagerPid} = gun_pool:start_pool("localhost", Port, #{scope => ?FUNCTION_NAME}), + gun_pool:await_up(ManagerPid), + gun_pool:stop_pool("localhost", Port, #{scope => ?FUNCTION_NAME}). + +degraded_configuration_error(Config) -> + case os:type() of + {win32, _} -> + {skip, "The initial connect timeout on Windows is too large."}; + _ -> + do_degraded_configuration_error(Config) + end. + +do_degraded_configuration_error(Config) -> + doc("Confirm the pool ends up in a degraded state " + "when connection is impossible because of bad configuration."), + Port = config(port, Config), + %% We attempt to connect to an unreachable IP. + {ok, ManagerPid} = gun_pool:start_pool({20, 20, 20, 1}, Port, #{ + conn_opts => #{tcp_opts => [{ip, {127, 0, 0, 1}}]}, + scope => ?FUNCTION_NAME, + size => 1 + }), + %% Wait for the lookup/connect to fail. + timer:sleep(500), + {degraded, #{conns := Conns}} = gun_pool:info(ManagerPid), + true = Conns =:= #{}, + %% We can stop the pool even if degraded. + gun_pool:stop_pool({20, 20, 20, 1}, Port, #{scope => ?FUNCTION_NAME}). diff --git a/gun/test/raw_SUITE.erl b/gun/test/raw_SUITE.erl new file mode 100644 index 0000000..57c04b9 --- /dev/null +++ b/gun/test/raw_SUITE.erl @@ -0,0 +1,391 @@ +%% Copyright (c) 2019-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(raw_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-import(ct_helper, [doc/1]). +-import(gun_test, [init_origin/3]). +-import(gun_test, [receive_from/1]). + +all() -> + [{group, raw}]. + +groups() -> + [{raw, [parallel], ct_helper:all(?MODULE)}]. + +%% Tests. + +direct_raw_tcp(_) -> + doc("Directly connect to a remote endpoint using the raw protocol over TCP."), + do_direct_raw(tcp, flow_control_disabled, client_side_close). + +direct_raw_tls(_) -> + doc("Directly connect to a remote endpoint using the raw protocol over TLS."), + do_direct_raw(tls, flow_control_disabled, client_side_close). + +direct_raw_tcp_with_flow_control(_) -> + doc("Directly connect to a remote endpoint using the raw protocol over TCP " + "with flow control enabled."), + do_direct_raw(tcp, flow_control_enabled, client_side_close). + +direct_raw_tls_with_flow_control(_) -> + doc("Directly connect to a remote endpoint using the raw protocol over TLS " + "with flow control enabled."), + do_direct_raw(tls, flow_control_enabled, client_side_close). + +direct_raw_tcp_with_server_side_close(_) -> + doc("Directly connect to a remote endpoint using the raw protocol over TCP " + "with server-side close."), + do_direct_raw(tcp, flow_control_disabled, server_side_close). + +direct_raw_tls_with_server_side_close(_) -> + doc("Directly connect to a remote endpoint using the raw protocol over TLS " + "with server-side close."), + do_direct_raw(tls, flow_control_disabled, server_side_close). + +do_direct_raw(OriginTransport, FlowControl, CloseSide) -> + {ok, OriginPid, OriginPort} = init_origin(OriginTransport, raw, fun do_echo/4), + Opts0 = #{ + transport => OriginTransport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [raw] + }, + Opts = do_maybe_add_flow(FlowControl, Opts0), + {ok, ConnPid} = gun:open("localhost", OriginPort, Opts), + {ok, raw} = gun:await_up(ConnPid), + handshake_completed = receive_from(OriginPid), + %% When we take over the entire connection there is no stream reference. + gun:data(ConnPid, undefined, nofin, <<"Hello world!">>), + {data, nofin, <<"Hello world!">>} = gun:await(ConnPid, undefined), + do_flow_control(FlowControl, ConnPid), + #{ + transport := OriginTransport, + protocol := raw, + origin_scheme := undefined, + origin_host := "localhost", + origin_port := OriginPort, + intermediaries := [] + } = gun:info(ConnPid), + do_close(CloseSide, ConnPid). + +do_maybe_add_flow(flow_control_enabled, Opts) -> + Opts#{raw_opts => #{flow => 1}}; +do_maybe_add_flow(flow_control_disabled, Opts) -> + Opts. + +do_flow_control(flow_control_enabled, ConnPid) -> + gun:data(ConnPid, undefined, nofin, <<"Hello world!">>), + {error, timeout} = gun:await(ConnPid, undefined, 1000), + ok = gun:update_flow(ConnPid, undefined, 1), + {data, nofin, <<"Hello world!">>} = gun:await(ConnPid, undefined); +do_flow_control(flow_control_disabled, _ConnPid) -> + ok. + +do_close(client_side_close, ConnPid) -> + gun:close(ConnPid); +do_close(server_side_close, ConnPid) -> + gun:data(ConnPid, undefined, nofin, <<"close">>), + receive + {gun_down, ConnPid, raw, closed, []} -> ok + after + 1000 -> error(timeout) + end. + +socks5_tcp_raw_tcp(_) -> + doc("Use Socks5 over TCP to connect to a remote endpoint using the raw protocol over TCP."), + do_socks5_raw(tcp, tcp). + +socks5_tcp_raw_tls(_) -> + doc("Use Socks5 over TCP to connect to a remote endpoint using the raw protocol over TLS."), + do_socks5_raw(tcp, tls). + +socks5_tls_raw_tcp(_) -> + doc("Use Socks5 over TLS to connect to a remote endpoint using the raw protocol over TCP."), + do_socks5_raw(tls, tcp). + +socks5_tls_raw_tls(_) -> + doc("Use Socks5 over TLS to connect to a remote endpoint using the raw protocol over TLS."), + do_socks5_raw(tls, tls). + +do_socks5_raw(OriginTransport, ProxyTransport) -> + {ok, OriginPid, OriginPort} = init_origin(OriginTransport, raw, fun do_echo/4), + {ok, ProxyPid, ProxyPort} = socks_SUITE:do_proxy_start(ProxyTransport, none), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + transport => ProxyTransport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [{socks, #{ + host => "localhost", + port => OriginPort, + transport => OriginTransport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [raw] + }}] + }), + %% We receive a gun_up and a gun_tunnel_up. + {ok, socks} = gun:await_up(ConnPid), + {up, raw} = gun:await(ConnPid, undefined), + %% The proxy received two packets. + {auth_methods, 1, [none]} = receive_from(ProxyPid), + {connect, <<"localhost">>, OriginPort} = receive_from(ProxyPid), + handshake_completed = receive_from(OriginPid), + %% When we take over the entire connection there is no stream reference. + gun:data(ConnPid, undefined, nofin, <<"Hello world!">>), + {data, nofin, <<"Hello world!">>} = gun:await(ConnPid, undefined), + #{ + transport := OriginTransport, + protocol := raw, + origin_scheme := undefined, + origin_host := "localhost", + origin_port := OriginPort, + intermediaries := [#{ + type := socks5, + host := "localhost", + port := ProxyPort, + transport := ProxyTransport, + protocol := socks + }]} = gun:info(ConnPid), + gun:close(ConnPid). + +connect_tcp_raw_tcp(_) -> + doc("Use CONNECT over TCP to connect to a remote endpoint using the raw protocol over TCP."), + do_connect_raw(tcp, tcp). + +connect_tcp_raw_tls(_) -> + doc("Use CONNECT over TCP to connect to a remote endpoint using the raw protocol over TLS."), + do_connect_raw(tcp, tls). + +connect_tls_raw_tcp(_) -> + doc("Use CONNECT over TLS to connect to a remote endpoint using the raw protocol over TCP."), + do_connect_raw(tls, tcp). + +connect_tls_raw_tls(_) -> + doc("Use CONNECT over TLS to connect to a remote endpoint using the raw protocol over TLS."), + do_connect_raw(tls, tls). + +do_connect_raw(OriginTransport, ProxyTransport) -> + {ok, OriginPid, OriginPort} = init_origin(OriginTransport, raw, fun do_echo/4), + {ok, ProxyPid, ProxyPort} = rfc7231_SUITE:do_proxy_start(ProxyTransport), + Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + transport => ProxyTransport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}] + }), + {ok, http} = gun:await_up(ConnPid), + StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + transport => OriginTransport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [raw] + }), + {request, <<"CONNECT">>, Authority, 'HTTP/1.1', _} = receive_from(ProxyPid), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef), + handshake_completed = receive_from(OriginPid), + {up, raw} = gun:await(ConnPid, StreamRef), + gun:data(ConnPid, StreamRef, nofin, <<"Hello world!">>), + {data, nofin, <<"Hello world!">>} = gun:await(ConnPid, StreamRef), + #{ + transport := OriginTransport, + protocol := raw, + origin_scheme := undefined, + origin_host := "localhost", + origin_port := OriginPort, + intermediaries := [#{ + type := connect, + host := "localhost", + port := ProxyPort, + transport := ProxyTransport, + protocol := http + }]} = gun:info(ConnPid), + gun:close(ConnPid). + +connect_raw_reply_to(_) -> + doc("When using CONNECT to establish a connection with the reply_to option set, " + "Gun must honor this option in the raw protocol."), + Self = self(), + ReplyTo = spawn(fun() -> + {ConnPid, StreamRef} = receive Msg -> Msg after 1000 -> error(timeout) end, + {response, fin, 200, _} = gun:await(ConnPid, StreamRef), + {up, raw} = gun:await(ConnPid, StreamRef), + Self ! {self(), ready}, + {data, nofin, <<"Hello world!">>} = gun:await(ConnPid, StreamRef), + Self ! {self(), ok} + end), + {ok, OriginPid, OriginPort} = init_origin(tcp, raw, fun do_echo/4), + {ok, ProxyPid, ProxyPort} = rfc7231_SUITE:do_proxy_start(tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort), + {ok, http} = gun:await_up(ConnPid), + StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + protocols => [raw] + }, [], #{reply_to => ReplyTo}), + ReplyTo ! {ConnPid, StreamRef}, + {request, <<"CONNECT">>, _, 'HTTP/1.1', _} = receive_from(ProxyPid), + handshake_completed = receive_from(OriginPid), + receive {ReplyTo, ready} -> ok after 1000 -> error(timeout) end, + gun:data(ConnPid, StreamRef, nofin, <<"Hello world!">>), + receive {ReplyTo, ok} -> gun:close(ConnPid) after 1000 -> error(timeout) end. + +http11_upgrade_raw_tcp(_) -> + doc("Use the HTTP Upgrade mechanism to switch to the raw protocol over TCP."), + do_http11_upgrade_raw(tcp). + +http11_upgrade_raw_tls(_) -> + doc("Use the HTTP Upgrade mechanism to switch to the raw protocol over TLS."), + do_http11_upgrade_raw(tls). + +do_http11_upgrade_raw(OriginTransport) -> + {ok, OriginPid, OriginPort} = init_origin(OriginTransport, raw, + fun (Parent, ListenSocket, ClientSocket, ClientTransport) -> + %% We skip the request and send a 101 response unconditionally. + {ok, _} = ClientTransport:recv(ClientSocket, 0, 5000), + ClientTransport:send(ClientSocket, + "HTTP/1.1 101 Switching Protocols\r\n" + "Connection: upgrade\r\n" + "Upgrade: custom/1.0\r\n" + "\r\n"), + do_echo(Parent, ListenSocket, ClientSocket, ClientTransport) + end), + {ok, ConnPid} = gun:open("localhost", OriginPort, #{ + transport => OriginTransport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}] + }), + {ok, http} = gun:await_up(ConnPid), + handshake_completed = receive_from(OriginPid), + StreamRef = gun:get(ConnPid, "/", #{ + <<"connection">> => <<"upgrade">>, + <<"upgrade">> => <<"custom/1.0">> + }), + {upgrade, [<<"custom/1.0">>], _} = gun:await(ConnPid, StreamRef), + %% When we take over the entire connection there is no stream reference. + gun:data(ConnPid, undefined, nofin, <<"Hello world!">>), + {data, nofin, <<"Hello world!">>} = gun:await(ConnPid, undefined), + #{ + transport := OriginTransport, + protocol := raw, + origin_scheme := undefined, + origin_host := "localhost", + origin_port := OriginPort, + intermediaries := [] + } = gun:info(ConnPid), + gun:close(ConnPid). + +http11_upgrade_raw_reply_to(_) -> + doc("When upgrading an HTTP/1.1 connection with the reply_to option set, " + "Gun must honor this option in the raw protocol."), + Self = self(), + ReplyTo = spawn(fun() -> + {ConnPid, StreamRef} = receive Msg -> Msg after 1000 -> error(timeout) end, + {upgrade, [<<"custom/1.0">>], _} = gun:await(ConnPid, StreamRef), + Self ! {self(), ready}, + {data, nofin, <<"Hello world!">>} = gun:await(ConnPid, undefined), + Self ! {self(), ok} + end), + {ok, OriginPid, OriginPort} = init_origin(tcp, raw, + fun (Parent, ListenSocket, ClientSocket, ClientTransport) -> + %% We skip the request and send a 101 response unconditionally. + {ok, _} = ClientTransport:recv(ClientSocket, 0, 5000), + ClientTransport:send(ClientSocket, + "HTTP/1.1 101 Switching Protocols\r\n" + "Connection: upgrade\r\n" + "Upgrade: custom/1.0\r\n" + "\r\n"), + do_echo(Parent, ListenSocket, ClientSocket, ClientTransport) + end), + {ok, ConnPid} = gun:open("localhost", OriginPort), + {ok, http} = gun:await_up(ConnPid), + handshake_completed = receive_from(OriginPid), + StreamRef = gun:get(ConnPid, "/", #{ + <<"connection">> => <<"upgrade">>, + <<"upgrade">> => <<"custom/1.0">> + }, #{reply_to => ReplyTo}), + ReplyTo ! {ConnPid, StreamRef}, + receive {ReplyTo, ready} -> ok after 1000 -> error(timeout) end, + gun:data(ConnPid, undefined, nofin, <<"Hello world!">>), + receive {ReplyTo, ok} -> gun:close(ConnPid) after 1000 -> error(timeout) end. + +http2_connect_tcp_raw_tcp(_) -> + doc("Use CONNECT over clear HTTP/2 to connect to a remote endpoint using the raw protocol over TCP."), + do_http2_connect_raw(tcp, <<"http">>, tcp). + +http2_connect_tls_raw_tcp(_) -> + doc("Use CONNECT over secure HTTP/2 to connect to a remote endpoint using the raw protocol over TCP."), + do_http2_connect_raw(tcp, <<"https">>, tls). + +do_http2_connect_raw(OriginTransport, ProxyScheme, ProxyTransport) -> + {ok, OriginPid, OriginPort} = init_origin(OriginTransport, raw, fun do_echo/4), + {ok, ProxyPid, ProxyPort} = rfc7540_SUITE:do_proxy_start(ProxyTransport, [ + {proxy_stream, 1, 200, [], 0, undefined} + ]), + Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + transport => ProxyTransport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [http2] + }), + {ok, http2} = gun:await_up(ConnPid), + handshake_completed = receive_from(ProxyPid), + StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + transport => OriginTransport, + protocols => [raw] + }), + {request, #{ + <<":method">> := <<"CONNECT">>, + <<":authority">> := Authority + }} = receive_from(ProxyPid), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef), + handshake_completed = receive_from(OriginPid), + {up, raw} = gun:await(ConnPid, StreamRef), + gun:data(ConnPid, StreamRef, nofin, <<"Hello world!">>), + {data, nofin, <<"Hello world!">>} = gun:await(ConnPid, StreamRef), + #{ + transport := ProxyTransport, + protocol := http2, + origin_scheme := ProxyScheme, + origin_host := "localhost", + origin_port := ProxyPort, + intermediaries := [] + } = gun:info(ConnPid), + Self = self(), + {ok, #{ + ref := StreamRef, + reply_to := Self, + state := running, + tunnel := #{ + transport := OriginTransport, + protocol := raw, + origin_scheme := undefined, + origin_host := "localhost", + origin_port := OriginPort + } + }} = gun:stream_info(ConnPid, StreamRef), + gun:close(ConnPid). + +%% The origin server will echo everything back. + +do_echo(Parent, ListenSocket, ClientSocket, ClientTransport) -> + case ClientTransport:recv(ClientSocket, 0, 5000) of + {ok, <<"close">>} -> + ok = ClientTransport:close(ClientSocket); + {ok, Data} -> + ClientTransport:send(ClientSocket, Data), + do_echo(Parent, ListenSocket, ClientSocket, ClientTransport); + {error, closed} -> + ok + end. diff --git a/gun/test/rfc6265bis_SUITE.erl b/gun/test/rfc6265bis_SUITE.erl new file mode 100644 index 0000000..460aeb7 --- /dev/null +++ b/gun/test/rfc6265bis_SUITE.erl @@ -0,0 +1,956 @@ +%% Copyright (c) 2020-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(rfc6265bis_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-import(ct_helper, [config/2]). +-import(ct_helper, [doc/1]). + +%% ct. + +all() -> + [ + {group, http}, + {group, https}, + {group, h2c}, + {group, h2} + ]. + +groups() -> + CommonTests = ct_helper:all(?MODULE), + [ + {http, [parallel], CommonTests}, + {https, [parallel], CommonTests}, + %% Websocket over HTTP/2 is currently not supported. + {h2c, [parallel], (CommonTests -- [wpt_secure_ws])}, + {h2, [parallel], (CommonTests -- [wpt_secure_ws])} + ]. + +init_per_group(Ref, Config0) when Ref =:= http; Ref =:= h2c -> + Protocol = case Ref of + http -> http; + h2c -> http2 + end, + Config = gun_test:init_cowboy_tcp(Ref, #{ + env => #{dispatch => cowboy_router:compile(init_routes())} + }, Config0), + [{transport, tcp}, {protocol, Protocol}|Config]; +init_per_group(Ref, Config0) when Ref =:= https; Ref =:= h2 -> + Protocol = case Ref of + https -> http; + h2 -> http2 + end, + Config = gun_test:init_cowboy_tls(Ref, #{ + env => #{dispatch => cowboy_router:compile(init_routes())} + }, Config0), + [{transport, tls}, {protocol, Protocol}|Config]. + +end_per_group(Ref, _) -> + cowboy:stop_listener(Ref). + +init_routes() -> [ + {'_', [ + {"/cookie-echo/[...]", cookie_echo_h, []}, + {"/cookie-parser/[...]", cookie_parser_h, []}, + {"/cookie-parser-result/[...]", cookie_parser_result_h, []}, + {"/cookie-set/[...]", cookie_set_h, []}, + {"/cookies/resources/echo-cookie.html", cookie_echo_h, []}, + {"/cookies/resources/set-cookie.html", cookie_set_h, []}, + {"/cookies/resources/echo.py", cookie_echo_h, []}, + {"/cookies/resources/set.py", cookie_set_h, []}, + {"/informational", cookie_informational_h, []}, + {"/ws", ws_cookie_h, []} + ]} +]. + +%% Tests. + +dont_ignore_informational_set_cookie(Config) -> + doc("User agents may accept set-cookie headers " + "sent in informational responses. (RFC6265bis 3)"), + [{<<"informational">>, <<"1">>}, {<<"final">>, <<"1">>}] + = do_informational_set_cookie(Config, false). + +ignore_informational_set_cookie(Config) -> + doc("User agents may ignore set-cookie headers " + "sent in informational responses. (RFC6265bis 3)"), + [{<<"final">>, <<"1">>}] + = do_informational_set_cookie(Config, true). + +do_informational_set_cookie(Config, Boolean) -> + Protocol = config(protocol, Config), + {ok, ConnPid} = gun:open("localhost", config(port, Config), #{ + transport => config(transport, Config), + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [{Protocol, #{cookie_ignore_informational => Boolean}}], + cookie_store => gun_cookies_list:init() + }), + {ok, Protocol} = gun:await_up(ConnPid), + StreamRef1 = gun:get(ConnPid, "/informational"), + {inform, 103, Headers1} = gun:await(ConnPid, StreamRef1), + ct:log("Headers1:~n~p", [Headers1]), + {response, fin, 204, Headers2} = gun:await(ConnPid, StreamRef1), + ct:log("Headers2:~n~p", [Headers2]), + StreamRef2 = gun:get(ConnPid, "/cookie-echo"), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2), + {ok, Body2} = gun:await_body(ConnPid, StreamRef2), + ct:log("Body2:~n~p", [Body2]), + Res = cow_cookie:parse_cookie(Body2), + gun:close(ConnPid), + Res. + +set_cookie_connect_tcp(Config) -> + doc("Cookies may also be set in responses going through CONNECT tunnels."), + Transport = config(transport, Config), + Protocol = config(protocol, Config), + {ok, ProxyPid, ProxyPort} = event_SUITE:do_proxy_start(Protocol, tcp), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + transport => tcp, + protocols => [Protocol], + cookie_store => gun_cookies_list:init() + }), + {ok, Protocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(Protocol, ProxyPid), + StreamRef1 = gun:connect(ConnPid, #{ + host => "localhost", + port => config(port, Config), + transport => Transport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [Protocol] + }), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {up, Protocol} = gun:await(ConnPid, StreamRef1), + StreamRef2 = gun:get(ConnPid, "/cookie-set?prefix", #{ + <<"please-set-cookie">> => <<"a=b">> + }, #{tunnel => StreamRef1}), + {response, fin, 204, Headers2} = gun:await(ConnPid, StreamRef2), + ct:log("Headers2:~n~p", [Headers2]), + StreamRef3 = gun:get(ConnPid, "/cookie-echo", [], #{tunnel => StreamRef1}), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef3), + {ok, Body3} = gun:await_body(ConnPid, StreamRef3), + ct:log("Body3:~n~p", [Body3]), + [{<<"a">>, <<"b">>}] = cow_cookie:parse_cookie(Body3), + gun:close(ConnPid). + +set_cookie_connect_tls(Config) -> + doc("Cookies may also be set in responses going through CONNECT tunnels."), + Transport = config(transport, Config), + Protocol = config(protocol, Config), + {ok, ProxyPid, ProxyPort} = event_SUITE:do_proxy_start(Protocol, tls), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + transport => tls, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [Protocol], + cookie_store => gun_cookies_list:init() + }), + {ok, Protocol} = gun:await_up(ConnPid), + tunnel_SUITE:do_handshake_completed(Protocol, ProxyPid), + StreamRef1 = gun:connect(ConnPid, #{ + host => "localhost", + port => config(port, Config), + transport => Transport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [Protocol] + }), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {up, Protocol} = gun:await(ConnPid, StreamRef1), + StreamRef2 = gun:get(ConnPid, "/cookie-set?prefix", #{ + <<"please-set-cookie">> => <<"a=b">> + }, #{tunnel => StreamRef1}), + {response, fin, 204, Headers2} = gun:await(ConnPid, StreamRef2), + ct:log("Headers2:~n~p", [Headers2]), + StreamRef3 = gun:get(ConnPid, "/cookie-echo", [], #{tunnel => StreamRef1}), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef3), + {ok, Body3} = gun:await_body(ConnPid, StreamRef3), + ct:log("Body3:~n~p", [Body3]), + [{<<"a">>, <<"b">>}] = cow_cookie:parse_cookie(Body3), + gun:close(ConnPid). + +%% Web Platform Tests converted to Erlang. +%% +%% Tests are not automatically updated, the process is manual. +%% Some test data is exported in JSON files in the "test/wpt" directory. +%% https://github.com/web-platform-tests/wpt/tree/master/cookies + +-define(WPT_HOST, "web-platform.test"). + +%% WPT: browser-only tests +%% +%% cookie-enabled-noncookie-frame.html +%% meta-blocked.html +%% navigated-away.html +%% prefix/document-cookie.non-secure.html +%% prefix/__host.document-cookie.html +%% prefix/__host.document-cookie.https.html +%% prefix/__secure.document-cookie.html +%% prefix/__secure.document-cookie.https.html +%% secure/set-from-dom.https.sub.html +%% secure/set-from-dom.sub.html + +%% WPT: attributes/attributes-ctl +%% +%% attributes/attributes-ctl.sub.html +%% +%% The original tests use the DOM. We can't do that so +%% we use a simple HTTP test instead. The original test +%% also includes a string representation of the CTL in +%% the cookie name. We don't bother. +%% +%% The expected value is only used for the \t CTL. +%% The original test retains the \t in the value because +%% it uses the DOM. The Set-Cookie algorithm requires +%% us to drop it. +wpt_attributes_ctl_domain(Config) -> + doc("Test cookie attribute parsing with control characters: " + "in Domain attribute value."), + do_wpt_ctl_test(fun(CTL) -> { + <<"testdomain">>, + <<"testdomain=t; Domain=test", CTL, ".co; Domain=", ?WPT_HOST>>, + <<"testdomain=t">> + } end, "/cookies/attributes", Config). + +wpt_attributes_ctl_domain2(Config) -> + doc("Test cookie attribute parsing with control characters: " + "after Domain attribute value."), + do_wpt_ctl_test(fun(CTL) -> { + <<"testdomain2">>, + <<"testdomain2=t; Domain=", ?WPT_HOST, CTL>>, + <<"testdomain2=t">> + } end, "/cookies/attributes", Config). + +wpt_attributes_ctl_path(Config) -> + doc("Test cookie attribute parsing with control characters: " + "in Path attribute value."), + do_wpt_ctl_test(fun(CTL) -> { + <<"testpath">>, + <<"testpath=t; Path=/te", CTL, "st; Path=/cookies/attributes">>, + <<"testpath=t">> + } end, "/cookies/attributes", Config). + +wpt_attributes_ctl_path2(Config) -> + doc("Test cookie attribute parsing with control characters: " + "after Path attribute value."), + do_wpt_ctl_test(fun(CTL) -> { + <<"testpath2">>, + <<"testpath2=t; Path=/cookies/attributes", CTL>>, + <<"testpath2=t">> + } end, "/cookies/attributes", Config). + +wpt_attributes_ctl_max_age(Config) -> + doc("Test cookie attribute parsing with control characters: " + "in Max-Age attribute value."), + do_wpt_ctl_test(fun(CTL) -> { + <<"testmaxage">>, + <<"testmaxage=t; Max-Age=10", CTL, "00; Max-Age=1000">>, + <<"testmaxage=t">> + } end, "/cookies/attributes", Config). + +wpt_attributes_ctl_max_age2(Config) -> + doc("Test cookie attribute parsing with control characters: " + "after Max-Age attribute value."), + do_wpt_ctl_test(fun(CTL) -> { + <<"testmaxage2">>, + <<"testmaxage2=t; Max-Age=1000", CTL>>, + <<"testmaxage2=t">> + } end, "/cookies/attributes", Config). + +wpt_attributes_ctl_expires(Config) -> + doc("Test cookie attribute parsing with control characters: " + "in Expires attribute value."), + do_wpt_ctl_test(fun(CTL) -> { + <<"testexpires">>, + <<"testexpires=t" + "; Expires=Fri, 01 Jan 20", CTL, "38 00:00:00 GMT" + "; Expires=Fri, 01 Jan 2038 00:00:00 GMT">>, + <<"testexpires=t">> + } end, "/cookies/attributes", Config). + +wpt_attributes_ctl_expires2(Config) -> + doc("Test cookie attribute parsing with control characters: " + "after Expires attribute value."), + do_wpt_ctl_test(fun(CTL) -> { + <<"testexpires2">>, + <<"testexpires2=t; Expires=Fri, 01 Jan 2038 00:00:00 GMT", CTL>>, + <<"testexpires2=t">> + } end, "/cookies/attributes", Config). + +wpt_attributes_ctl_secure(Config) -> + doc("Test cookie attribute parsing with control characters: " + "in Secure attribute."), + do_wpt_ctl_test(fun(CTL) -> { + <<"testsecure">>, + <<"testsecure=t; Sec", CTL, "ure">>, + <<"testsecure=t">> + } end, "/cookies/attributes", Config). + +wpt_attributes_ctl_secure2(Config) -> + doc("Test cookie attribute parsing with control characters: " + "after Secure attribute."), + do_wpt_ctl_test(fun(CTL) -> { + <<"testsecure2">>, + <<"testsecure2=t; Secure", CTL>>, + case config(transport, Config) of + tcp -> <<>>; %% Secure causes the cookie to be rejected over TCP. + tls -> <<"testsecure2=t">> + end + } end, "/cookies/attributes", Config). + +wpt_attributes_ctl_httponly(Config) -> + doc("Test cookie attribute parsing with control characters: " + "in HttpOnly attribute."), + do_wpt_ctl_test(fun(CTL) -> { + <<"testhttponly">>, + <<"testhttponly=t; Http", CTL, "Only">>, + <<"testhttponly=t">> + } end, "/cookies/attributes", Config). + +wpt_attributes_ctl_samesite(Config) -> + doc("Test cookie attribute parsing with control characters: " + "in SameSite attribute value."), + do_wpt_ctl_test(fun(CTL) -> { + <<"testsamesite">>, + <<"testsamesite=t; SameSite=No", CTL, "ne; SameSite=None">>, + <<"testsamesite=t">> + } end, "/cookies/attributes", Config). + +wpt_attributes_ctl_samesite2(Config) -> + doc("Test cookie attribute parsing with control characters: " + "after SameSite attribute value."), + do_wpt_ctl_test(fun(CTL) -> { + <<"testsamesite2">>, + <<"testsamesite2=t; SameSite=None", CTL>>, + <<"testsamesite2=t">> + } end, "/cookies/attributes", Config). + +%% @todo Redirect cookie test. +%% attributes/domain.sub.html +%% attributes/resources/domain-child.sub.html + +%% WPT: attributes/expires +%% +%% attributes/expires.html +wpt_attributes_expires(Config) -> + doc("Test expires attribute parsing."), + do_wpt_json_test("attributes_expires", "/cookies/attributes", Config). + +%% WPT: attributes/invalid +%% +%% attributes/invalid.html +wpt_attributes_invalid(Config) -> + doc("Test invalid attribute parsing."), + do_wpt_json_test("attributes_invalid", "/cookies/attributes", Config). + +%% WPT: attributes/max_age +%% +%% attributes/max-age.html +wpt_attributes_max_age(Config) -> + doc("Test max-age attribute parsing."), + do_wpt_json_test("attributes_max_age", "/cookies/attributes", Config). + +%% WPT: attributes/path +%% +%% attributes/path.html +wpt_attributes_path(Config) -> + doc("Test cookie path attribute parsing."), + do_wpt_json_test("attributes_path", "/cookies/attributes", Config). + +%% @todo Redirect cookie test. +%% attributes/path-redirect.html +%% attributes/resources/pathfakeout.html +%% attributes/resources/path-redirect-shared.js +%% attributes/resources/path.html +%% attributes/resources/path.html.headers +%% attributes/resources/path/one.html +%% attributes/resources/path/three.html +%% attributes/resources/path/two.html +%% attributes/resources/pathfakeout/one.html + +%% WPT: attributes/secure +%% +%% attributes/secure.https.html +%% attributes/secure-non-secure.html +%% attributes/resources/secure-non-secure-child.html +wpt_attributes_secure(Config) -> + doc("Test cookie secure attribute parsing."), + TestFile = case config(transport, Config) of + tcp -> "attributes_secure_non_secure"; + tls -> "attributes_secure" + end, + do_wpt_json_test(TestFile, "/cookies/attributes", Config). + +%% WPT: domain/domain-attribute-host-with-and-without-leading-period +%% +%% domain/domain-attribute-host-with-and-without-leading-period.sub.https.html +%% domain/domain-attribute-host-with-and-without-leading-period.sub.https.html.sub.headers +wpt_domain_with_and_without_leading_period(Config) -> + doc("Domain with and without leading period."), + #{ + same_origin := [{<<"a">>, <<"c">>}], + subdomain := [{<<"a">>, <<"c">>}] + } = do_wpt_domain_test(Config, "domain_with_and_without_leading_period"), + ok. + +%% WPT: domain/domain-attribute-host-with-leading-period +%% +%% domain/domain-attribute-host-with-leading-period.sub.https.html +%% domain/domain-attribute-host-with-leading-period.sub.https.html.sub.headers +wpt_domain_with_leading_period(Config) -> + doc("Domain with leading period."), + #{ + same_origin := [{<<"a">>, <<"b">>}], + subdomain := [{<<"a">>, <<"b">>}] + } = do_wpt_domain_test(Config, "domain_with_leading_period"), + ok. + +%% @todo WPT: domain/domain-attribute-idn-host +%% +%% domain/domain-attribute-idn-host.sub.https.html +%% domain/support/idn-child.sub.https.html +%% domain/support/idn.py + +%% WPT: domain/domain-attribute-matches-host +%% +%% domain/domain-attribute-matches-host.sub.https.html +%% domain/domain-attribute-matches-host.sub.https.html.sub.headers +wpt_domain_matches_host(Config) -> + doc("Domain matches host header."), + #{ + same_origin := [{<<"a">>, <<"b">>}], + subdomain := [{<<"a">>, <<"b">>}] + } = do_wpt_domain_test(Config, "domain_matches_host"), + ok. + +%% WPT: domain/domain-attribute-missing +%% +%% domain/domain-attribute-missing.sub.html +%% domain/domain-attribute-missing.sub.html.headers +wpt_domain_missing(Config) -> + doc("Domain attribute missing."), + #{ + same_origin := [{<<"a">>, <<"b">>}], + subdomain := undefined + } = do_wpt_domain_test(Config, "domain_missing"), + ok. + +do_wpt_domain_test(Config, TestCase) -> + Protocol = config(protocol, Config), + {ok, ConnPid} = gun:open("localhost", config(port, Config), #{ + transport => config(transport, Config), + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [Protocol], + cookie_store => gun_cookies_list:init() + }), + {ok, Protocol} = gun:await_up(ConnPid), + StreamRef1 = gun:get(ConnPid, ["/cookie-set?", TestCase], #{<<"host">> => ?WPT_HOST}), + {response, fin, 204, Headers1} = gun:await(ConnPid, StreamRef1), + ct:log("Headers1:~n~p", [Headers1]), + StreamRef2 = gun:get(ConnPid, "/cookie-echo", #{<<"host">> => ?WPT_HOST}), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2), + {ok, Body2} = gun:await_body(ConnPid, StreamRef2), + ct:log("Body2:~n~p", [Body2]), + StreamRef3 = gun:get(ConnPid, "/cookie-echo", #{<<"host">> => "sub." ?WPT_HOST}), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef3), + {ok, Body3} = gun:await_body(ConnPid, StreamRef3), + ct:log("Body3:~n~p", [Body3]), + gun:close(ConnPid), + #{ + same_origin => case Body2 of <<"UNDEF">> -> undefined; _ -> cow_cookie:parse_cookie(Body2) end, + subdomain => case Body3 of <<"UNDEF">> -> undefined; _ -> cow_cookie:parse_cookie(Body3) end + }. + +%% WPT: encoding/charset +%% +%% encoding/charset.html +wpt_encoding(Config) -> + doc("Test UTF-8 and ASCII cookie parsing."), + do_wpt_json_test("encoding_charset", "/cookies/encoding", Config). + +%% WPT: name/name +%% +%% name/name.html +wpt_name(Config) -> + doc("Test cookie name parsing."), + do_wpt_json_test("name", "/cookies/name", Config). + +%% WPT: name/name-ctl +%% +%% name/name-ctl.html +%% +%% The original tests use the DOM. We can't do that so +%% we use a simple HTTP test instead. The original test +%% also includes a string representation of the CTL in +%% the cookie name. We don't bother. +%% +%% The expected value is only used for the \t CTL. +%% The original test retains the \t in the value because +%% it uses the DOM. The Set-Cookie algorithm requires +%% us to drop it. +wpt_name_ctl(Config) -> + doc("Test cookie name parsing with control characters."), + do_wpt_ctl_test(fun(CTL) -> { + <<"test", CTL, "name">>, + <<"test", CTL, "name=", CTL>>, + <<"test", CTL, "name=">> + } end, "/cookies/name", Config). + +%% @todo Redirect cookie test. +%% ordering/ordering.sub.html +%% ordering/resources/ordering-child.sub.html + +%% WPT: partitioned-cookies (Not implemented; proposal.) + +%% WPT: path/default +%% +%% path/default.html +wpt_path_default(Config) -> + doc("Cookie set on the default path can be retrieved."), + Protocol = config(protocol, Config), + {ok, ConnPid} = gun:open("localhost", config(port, Config), #{ + transport => config(transport, Config), + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [Protocol], + cookie_store => gun_cookies_list:init() + }), + {ok, Protocol} = gun:await_up(ConnPid), + %% Set and retrieve the cookie. + StreamRef1 = gun:get(ConnPid, "/cookie-set?path_default"), + {response, fin, 204, Headers1} = gun:await(ConnPid, StreamRef1), + ct:log("Headers1:~n~p", [Headers1]), + StreamRef2 = gun:get(ConnPid, "/cookie-echo"), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2), + {ok, Body2} = gun:await_body(ConnPid, StreamRef2), + ct:log("Body2:~n~p", [Body2]), + [{<<"cookie-path-default">>, <<"1">>}] = cow_cookie:parse_cookie(Body2), + %% Expire the cookie. + StreamRef3 = gun:get(ConnPid, "/cookie-set?path_default_expire"), + {response, fin, 204, Headers3} = gun:await(ConnPid, StreamRef3), + ct:log("Headers3:~n~p", [Headers3]), + StreamRef4 = gun:get(ConnPid, "/cookie-echo"), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef4), + {ok, Body4} = gun:await_body(ConnPid, StreamRef4), + ct:log("Body4:~n~p", [Body4]), + <<"UNDEF">> = Body4, + gun:close(ConnPid). + +%% WPT: path/match +%% +%% path/match.html +wpt_path_match(Config) -> + doc("Cookie path match."), + MatchTests = [ + <<"/">>, + <<"match.html">>, + <<"cookies">>, + <<"/cookies">>, + <<"/cookies/">>, + <<"/cookies/resources/echo-cookie.html">> + ], + NegTests = [ + <<"/cook">>, + <<"/w/">> + ], + Protocol = config(protocol, Config), + _ = [begin + ct:log("Positive test: ~s", [P]), + {ok, ConnPid} = gun:open("localhost", config(port, Config), #{ + transport => config(transport, Config), + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [Protocol], + cookie_store => gun_cookies_list:init() + }), + {ok, Protocol} = gun:await_up(ConnPid), + %% Set and retrieve the cookie. + StreamRef1 = gun:get(ConnPid, ["/cookies/resources/set-cookie.html?path=", P]), + {response, fin, 204, Headers1} = gun:await(ConnPid, StreamRef1), + ct:log("Headers1:~n~p", [Headers1]), + StreamRef2 = gun:get(ConnPid, "/cookies/resources/echo-cookie.html"), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2), + {ok, Body2} = gun:await_body(ConnPid, StreamRef2), + ct:log("Body2:~n~p", [Body2]), + [{<<"a">>, <<"b">>}] = cow_cookie:parse_cookie(Body2), + gun:close(ConnPid) + end || P <- MatchTests], + _ = [begin + ct:log("Negative test: ~s", [P]), + {ok, ConnPid} = gun:open("localhost", config(port, Config), #{ + transport => config(transport, Config), + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [Protocol], + cookie_store => gun_cookies_list:init() + }), + {ok, Protocol} = gun:await_up(ConnPid), + %% Set and retrieve the cookie. + StreamRef1 = gun:get(ConnPid, ["/cookies/resources/set-cookie.html?path=", P]), + {response, fin, 204, Headers1} = gun:await(ConnPid, StreamRef1), + ct:log("Headers1:~n~p", [Headers1]), + StreamRef2 = gun:get(ConnPid, "/cookies/resources/echo-cookie.html"), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2), + {ok, Body2} = gun:await_body(ConnPid, StreamRef2), + ct:log("Body2:~n~p", [Body2]), + <<"UNDEF">> = Body2, + gun:close(ConnPid) + end || P <- NegTests], + ok. + +%% WPT: prefix/__host.header +%% +%% prefix/__host.header.html +%% prefix/__host.header.https.html +wpt_prefix_host(Config) -> + doc("__Host- prefix."), + Tests = case config(transport, Config) of + tcp -> [ + {<<"__Host-foo=bar; Path=/;">>, false}, + {<<"__Host-foo=bar; Path=/;domain=" ?WPT_HOST>>, false}, + {<<"__Host-foo=bar; Path=/;Max-Age=10">>, false}, + {<<"__Host-foo=bar; Path=/;HttpOnly">>, false}, + {<<"__Host-foo=bar; Secure; Path=/;">>, false}, + {<<"__Host-foo=bar; Secure; Path=/;domain=" ?WPT_HOST>>, false}, + {<<"__Host-foo=bar; Secure; Path=/;Max-Age=10">>, false}, + {<<"__Host-foo=bar; Secure; Path=/;HttpOnly">>, false}, + {<<"__Host-foo=bar; Secure; Path=/; Domain=" ?WPT_HOST "; ">>, false}, + {<<"__Host-foo=bar; Secure; Path=/; Domain=" ?WPT_HOST "; domain=" ?WPT_HOST>>, false}, + {<<"__Host-foo=bar; Secure; Path=/; Domain=" ?WPT_HOST "; Max-Age=10">>, false}, + {<<"__Host-foo=bar; Secure; Path=/; Domain=" ?WPT_HOST "; HttpOnly">>, false}, + {<<"__Host-foo=bar; Secure; Path=/cookies/resources/list.py">>, false} + ]; + tls -> [ + {<<"__Host-foo=bar; Path=/;">>, false}, + {<<"__Host-foo=bar; Path=/;Max-Age=10">>, false}, + {<<"__Host-foo=bar; Path=/;HttpOnly">>, false}, + {<<"__Host-foo=bar; Secure; Path=/;">>, true}, + {<<"__Host-foo=bar; Secure; Path=/;Max-Age=10">>, true}, + {<<"__Host-foo=bar; Secure; Path=/;HttpOnly">>, true}, + {<<"__Host-foo=bar; Secure; Path=/; Domain=" ?WPT_HOST "; ">>, false}, + {<<"__Host-foo=bar; Secure; Path=/; Domain=" ?WPT_HOST "; Max-Age=10">>, false}, + {<<"__Host-foo=bar; Secure; Path=/; Domain=" ?WPT_HOST "; HttpOnly">>, false}, + {<<"__Host-foo=bar; Secure; Path=/cookies/resources/list.py">>, false} + ] + end, + _ = [do_wpt_prefix_common(Config, TestCase, Expected, <<"__Host-foo">>) + || {TestCase, Expected} <- Tests], + ok. + +%% WPT: prefix/__secure.header +%% +%% prefix/__secure.header.html +%% prefix/__secure.header.https.html +wpt_prefix_secure(Config) -> + doc("__Secure- prefix."), + Tests = case config(transport, Config) of + tcp -> [ + {<<"__Secure-foo=bar; Path=/;">>, false}, + {<<"__Secure-foo=bar; Path=/;domain=" ?WPT_HOST>>, false}, + {<<"__Secure-foo=bar; Path=/;Max-Age=10">>, false}, + {<<"__Secure-foo=bar; Path=/;HttpOnly">>, false}, + {<<"__Secure-foo=bar; Secure; Path=/;">>, false}, + {<<"__Secure-foo=bar; Secure; Path=/;domain=" ?WPT_HOST>>, false}, + {<<"__Secure-foo=bar; Secure; Path=/;Max-Age=10">>, false}, + {<<"__Secure-foo=bar; Secure; Path=/;HttpOnly">>, false} + ]; + tls -> [ + {<<"__Secure-foo=bar; Path=/;">>, false}, + {<<"__Secure-foo=bar; Path=/;Max-Age=10">>, false}, + {<<"__Secure-foo=bar; Path=/;HttpOnly">>, false}, + {<<"__Secure-foo=bar; Secure; Path=/;">>, true}, + {<<"__Secure-foo=bar; Secure; Path=/;Max-Age=10">>, true}, + {<<"__Secure-foo=bar; Secure; Path=/;HttpOnly">>, true} + %% Missing two SameSite cases from prefix/__secure.header.https. (Not implemented.) + ] + end, + _ = [do_wpt_prefix_common(Config, TestCase, Expected, <<"__Secure-foo">>) + || {TestCase, Expected} <- Tests], + ok. + +do_wpt_prefix_common(Config, TestCase, Expected, Name) -> + Protocol = config(protocol, Config), + ct:log("Test case: ~s~nCookie must be set? ~s", [TestCase, Expected]), + {ok, ConnPid} = gun:open("localhost", config(port, Config), #{ + transport => config(transport, Config), + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [Protocol], + cookie_store => gun_cookies_list:init() + }), + {ok, Protocol} = gun:await_up(ConnPid), + %% Set and retrieve the cookie. + StreamRef1 = gun:get(ConnPid, "/cookies/resources/set.py?prefix", #{ + <<"host">> => ?WPT_HOST, + <<"please-set-cookie">> => TestCase + }), + {response, fin, 204, Headers1} = gun:await(ConnPid, StreamRef1), + ct:log("Headers1:~n~p", [Headers1]), + StreamRef2 = gun:get(ConnPid, "/cookies/resources/echo.py", #{ + <<"host">> => ?WPT_HOST + }), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2), + {ok, Body2} = gun:await_body(ConnPid, StreamRef2), + ct:log("Body2:~n~p", [Body2]), + case Expected of + true -> + [{Name, _}] = cow_cookie:parse_cookie(Body2), + ok; + false -> + <<"UNDEF">> = Body2, + ok + end, + gun:close(ConnPid). + +%% WPT: samesite/ (Not implemented.) +%% WPT: samesite-none-secure/ (Not implemented.) +%% WPT: schemeful-same-site/ (Not implemented.) + +%% WPT: secure/set-from-http.* +%% +%% secure/set-from-http.sub.html +%% secure/set-from-http.sub.html.headers +%% secure/set-from-http.https.sub.html +%% secure/set-from-http.https.sub.html.headers +wpt_secure(Config) -> + doc("Secure attribute."), + case config(transport, Config) of + tcp -> + undefined = do_wpt_secure_common(Config, <<"secure_http">>), + ok; + tls -> + [{<<"secure_from_secure_http">>, <<"1">>}] = do_wpt_secure_common(Config, <<"secure_https">>), + ok + end. + +do_wpt_secure_common(Config, TestCase) -> + Protocol = config(protocol, Config), + {ok, ConnPid} = gun:open("localhost", config(port, Config), #{ + transport => config(transport, Config), + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [Protocol], + cookie_store => gun_cookies_list:init() + }), + {ok, Protocol} = gun:await_up(ConnPid), + StreamRef1 = gun:get(ConnPid, ["/cookie-set?", TestCase]), + {response, fin, 204, Headers1} = gun:await(ConnPid, StreamRef1), + ct:log("Headers1:~n~p", [Headers1]), + StreamRef2 = gun:get(ConnPid, "/cookie-echo"), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2), + {ok, Body2} = gun:await_body(ConnPid, StreamRef2), + ct:log("Body2:~n~p", [Body2]), + gun:close(ConnPid), + case Body2 of + <<"UNDEF">> -> undefined; + _ -> cow_cookie:parse_cookie(Body2) + end. + +%% WPT: secure/set-from-ws* +%% +%% secure/set-from-ws.sub.html +%% secure/set-from-wss.https.sub.html +wpt_secure_ws(Config) -> + doc("Secure attribute in Websocket upgrade response."), + case config(transport, Config) of + tcp -> + undefined = do_wpt_secure_ws_common(Config), + ok; + tls -> + [{<<"ws_cookie">>, <<"1">>}] = do_wpt_secure_ws_common(Config), + ok + end. + +do_wpt_secure_ws_common(Config) -> + Protocol = config(protocol, Config), + {ok, ConnPid1} = gun:open("localhost", config(port, Config), #{ + transport => config(transport, Config), + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [Protocol], + cookie_store => gun_cookies_list:init() + }), + {ok, Protocol} = gun:await_up(ConnPid1), + StreamRef1 = gun:ws_upgrade(ConnPid1, "/ws"), + {upgrade, [<<"websocket">>], Headers1} = gun:await(ConnPid1, StreamRef1), + ct:log("Headers1:~n~p", [Headers1]), + %% We must extract the cookie store because it is tied to the connection. + #{cookie_store := CookieStore} = gun:info(ConnPid1), + gun:close(ConnPid1), + {ok, ConnPid2} = gun:open("localhost", config(port, Config), #{ + transport => config(transport, Config), + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [Protocol], + cookie_store => CookieStore + }), + StreamRef2 = gun:get(ConnPid2, "/cookie-echo"), + {response, nofin, 200, _} = gun:await(ConnPid2, StreamRef2), + {ok, Body2} = gun:await_body(ConnPid2, StreamRef2), + ct:log("Body2:~n~p", [Body2]), + gun:close(ConnPid2), + case Body2 of + <<"UNDEF">> -> undefined; + _ -> cow_cookie:parse_cookie(Body2) + end. + +%% WPT: size/attributes +%% +%% size/attributes.www.sub.html +wpt_size_attributes(Config) -> + doc("Test cookie attribute size restrictions."), + do_wpt_json_test("size_attributes", "/cookies/size", Config). + +%% WPT: size/name-and-value +%% +%% size/name-and-value.html +wpt_size_name_and_value(Config) -> + doc("Test cookie name/value size restrictions."), + do_wpt_json_test("size_name_and_value", "/cookies/size", Config). + +%% WPT: value/value +%% +%% value/value.html +wpt_value(Config) -> + doc("Test cookie value parsing."), + Tests = do_load_json("value"), + _ = [begin + #{ + <<"name">> := Name, + <<"cookie">> := Cookie, + <<"expected">> := Expected + } = Test, + false = maps:is_key(<<"defaultPath">>, Test), + do_wpt_set_test(<<"/cookies/value">>, + Name, Cookie, Expected, Config) + end || Test <- Tests, + %% The original test uses the DOM, we use HTTP, and are + %% required to drop the cookie entirely if it contains + %% a \n (RFC6265bis 5.4) so we skip this test. + maps:get(<<"expected">>, Test) =/= <<"test=13">>], + ok. + +%% WPT: value/value-ctl +%% +%% value/value-ctl.html +%% +%% The original tests use the DOM. We can't do that so +%% we use a simple HTTP test instead. The original test +%% also includes a string representation of the CTL in +%% the cookie value. We don't bother. +%% +%% The expected value is only used for the \t CTL. +%% The original test retains the \t in the value because +%% it uses the DOM. The Set-Cookie algorithm requires +%% us to drop it. +wpt_value_ctl(Config) -> + doc("Test cookie value parsing with control characters."), + do_wpt_ctl_test(fun(CTL) -> { + <<"test">>, + <<"test=", CTL, "value">>, + <<"test=value">> + } end, "/cookies/value", Config). + +%% JSON files are created by taking the Javascript Object +%% from the HTML files in the WPT suite, using the browser +%% Developer console to convert into JSON: +%% Obj = +%% JSON.stringify(Obj) +%% Then copying the result into the JSON file; removing +%% the quoting (first and last character) and if needed +%% fixing the escaping in Vim using: +%% :%s/\\\\/\\/g +%% The host may also need to be replaced to match WPT_HOST. +do_load_json(File0) -> + File = "../../test/wpt/cookies/" ++ File0 ++ ".json", + {ok, Bin} = file:read_file(File), + jsx:decode(Bin, [{return_maps, true}]). + +do_wpt_json_test(TestFile, TestPath, Config) -> + Tests = do_load_json(TestFile), + _ = [begin + #{ + <<"name">> := Name, + <<"cookie">> := Cookie, + <<"expected">> := Expected + } = Test, + DefaultPath = maps:get(<<"defaultPath">>, Test, true), + do_wpt_set_test(TestPath, Name, Cookie, Expected, DefaultPath, Config) + end || Test <- Tests], + ok. + +do_wpt_ctl_test(Fun, TestPath, Config) -> + %% Control characters are defined by RFC5234 to be %x00-1F / %x7F. + %% We exclude \r for HTTP/1.1 because this causes errors + %% at the header parsing level. + CTLs0 = lists:seq(0, 16#1F) ++ [16#7F], + CTLs = case config(protocol, Config) of + http -> CTLs0 -- "\r"; + http2 -> CTLs0 + end, + %% All CTLs except \t should cause the cookie to be rejected. + _ = [begin + {Name, Cookie, Expected} = Fun(CTL), + case CTL of + $\t -> + do_wpt_set_test(TestPath, Name, Cookie, Expected, false, Config); + _ -> + do_wpt_set_test(TestPath, Name, Cookie, <<>>, false, Config) + end + end || CTL <- CTLs], + ok. + +%% Equivalent to httpCookieTest. +do_wpt_set_test(TestPath, Name, Cookie, Expected, Config) -> + do_wpt_set_test(TestPath, Name, Cookie, Expected, true, Config). + +do_wpt_set_test(TestPath, Name, Cookie, Expected, DefaultPath, Config) -> + ct:log("Name: ~s", [Name]), + Protocol = config(protocol, Config), + {ok, ConnPid} = gun:open("localhost", config(port, Config), #{ + transport => config(transport, Config), + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [Protocol], + cookie_store => gun_cookies_list:init() + }), + {ok, Protocol} = gun:await_up(ConnPid), + StreamRef1 = gun:get(ConnPid, + ["/cookie-set?ttb=", cow_qs:urlencode(term_to_binary(Cookie))], + #{<<"host">> => ?WPT_HOST}), + {response, fin, 204, Headers} = gun:await(ConnPid, StreamRef1), + ct:log("Headers:~n~p", [Headers]), + #{cookie_store := Store} = gun:info(ConnPid), + ct:log("Store:~n~p", [Store]), + Result1 = case DefaultPath of + true -> + %% We do another request to get the cookie. + StreamRef2 = gun:get(ConnPid, "/cookie-echo", + #{<<"host">> => ?WPT_HOST}), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2), + {ok, Body2} = gun:await_body(ConnPid, StreamRef2), + case Body2 of + <<"UNDEF">> -> <<>>; + _ -> Body2 + end; + false -> + %% We call this function to get a request header representation + %% of a cookie, similar to what document.cookie returns. + case gun_cookies:add_cookie_header( + case config(transport, Config) of + tcp -> <<"http">>; + tls -> <<"https">> + end, + <>, TestPath, [], Store) of + {[{<<"cookie">>, Result0}], _} -> + Result0; + {[], _} -> + <<>> + end + end, + Result = unicode:characters_to_binary(Result1), + ct:log("Expected:~n~p~nResult:~n~p", [Expected, Result]), + {Name, Cookie, Expected} = {Name, Cookie, Result}, + gun:close(ConnPid). diff --git a/gun/test/rfc7230_SUITE.erl b/gun/test/rfc7230_SUITE.erl new file mode 100644 index 0000000..4d1e902 --- /dev/null +++ b/gun/test/rfc7230_SUITE.erl @@ -0,0 +1,101 @@ +%% Copyright (c) 2019-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(rfc7230_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-import(ct_helper, [doc/1]). +-import(gun_test, [init_origin/2]). +-import(gun_test, [init_origin/3]). +-import(gun_test, [receive_from/1]). + +all() -> + ct_helper:all(?MODULE). + +%% Tests. + +host_default_port_http(_) -> + doc("The default port for http should not be sent in the host header. (RFC7230 2.7.1)"), + do_host_port(tcp, 80, <<>>). + +host_default_port_https(_) -> + doc("The default port for https should not be sent in the host header. (RFC7230 2.7.2)"), + do_host_port(tls, 443, <<>>). + +host_ipv6(_) -> + doc("When connecting to a server using an IPv6 address the host " + "header must wrap the address with brackets. (RFC7230 5.4, RFC3986 3.2.2)"), + {ok, OriginPid, OriginPort} = init_origin(tcp6, http), + {ok, ConnPid} = gun:open({0,0,0,0,0,0,0,1}, OriginPort, #{transport => tcp}), + {ok, http} = gun:await_up(ConnPid), + _ = gun:get(ConnPid, "/"), + handshake_completed = receive_from(OriginPid), + Data = receive_from(OriginPid), + Lines = binary:split(Data, <<"\r\n">>, [global]), + [<<"host: [::1]", _/bits>>] = [L || <<"host: ", _/bits>> = L <- Lines], + gun:close(ConnPid). + +host_other_port_http(_) -> + doc("Non-default ports for http must be sent in the host header. (RFC7230 2.7.1)"), + do_host_port(tcp, 443, <<":443">>). + +host_other_port_https(_) -> + doc("Non-default ports for https must be sent in the host header. (RFC7230 2.7.2)"), + do_host_port(tls, 80, <<":80">>). + +do_host_port(Transport, DefaultPort, HostHeaderPort) -> + {ok, OriginPid, OriginPort} = init_origin(Transport, http), + {ok, ConnPid} = gun:open("localhost", OriginPort, #{ + transport => Transport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}] + }), + {ok, http} = gun:await_up(ConnPid), + %% Change the origin's port in the state to trigger the default port behavior. + _ = sys:replace_state(ConnPid, fun({StateName, StateData}) -> + {StateName, setelement(8, StateData, DefaultPort)} + end, 5000), + %% Confirm the default port is not sent in the request. + _ = gun:get(ConnPid, "/"), + handshake_completed = receive_from(OriginPid), + Data = receive_from(OriginPid), + Lines = binary:split(Data, <<"\r\n">>, [global]), + [<<"host: localhost", Rest/bits>>] = [L || <<"host: ", _/bits>> = L <- Lines], + HostHeaderPort = Rest, + gun:close(ConnPid). + +transfer_encoding_overrides_content_length(_) -> + doc("When both transfer-encoding and content-length are provided, " + "content-length must be ignored. (RFC7230 3.3.3)"), + {ok, _, OriginPort} = init_origin(tcp, http, + fun(_, _, ClientSocket, ClientTransport) -> + {ok, _} = ClientTransport:recv(ClientSocket, 0, 1000), + ClientTransport:send(ClientSocket, + "HTTP/1.1 200 OK\r\n" + "content-length: 12\r\n" + "transfer-encoding: chunked\r\n" + "\r\n" + "6\r\n" + "hello \r\n" + "6\r\n" + "world!\r\n" + "0\r\n\r\n" + ) + end), + {ok, ConnPid} = gun:open("localhost", OriginPort), + {ok, http} = gun:await_up(ConnPid), + StreamRef = gun:get(ConnPid, "/"), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef), + {ok, <<"hello world!">>} = gun:await_body(ConnPid, StreamRef), + gun:close(ConnPid). diff --git a/gun/test/rfc7231_SUITE.erl b/gun/test/rfc7231_SUITE.erl new file mode 100644 index 0000000..f3a780e --- /dev/null +++ b/gun/test/rfc7231_SUITE.erl @@ -0,0 +1,568 @@ +%% Copyright (c) 2018-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(rfc7231_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-import(ct_helper, [doc/1]). +-import(gun_test, [init_origin/1]). +-import(gun_test, [init_origin/2]). +-import(gun_test, [receive_from/1]). +-import(gun_test, [receive_from/2]). + +all() -> + ct_helper:all(?MODULE). + +%% Proxy helpers. + +do_proxy_start(Transport) -> + do_proxy_start(Transport, 200, []). + +do_proxy_start(Transport, Status) -> + do_proxy_start(Transport, Status, []). + +do_proxy_start(Transport, Status, ConnectRespHeaders) -> + do_proxy_start(Transport, Status, ConnectRespHeaders, 0). + +do_proxy_start(Transport, Status, ConnectRespHeaders, Delay) -> + do_proxy_start(Transport, Status, ConnectRespHeaders, Delay, <<"HTTP/1.1">>). + +do_proxy_start(Transport0, Status, ConnectRespHeaders, Delay, ConnectRespVersion) -> + Transport = case Transport0 of + tcp -> gun_tcp; + tls -> gun_tls + end, + Self = self(), + Pid = spawn_link(fun() -> do_proxy_init(Self, Transport, Status, ConnectRespHeaders, Delay, ConnectRespVersion) end), + Port = receive_from(Pid), + {ok, Pid, Port}. + +do_proxy_init(Parent, Transport, Status, ConnectRespHeaders, Delay, ConnectRespVersion) -> + {ok, ListenSocket} = case Transport of + gun_tcp -> + gen_tcp:listen(0, [binary, {active, false}]); + gun_tls -> + Opts = ct_helper:get_certs_from_ets(), + ssl:listen(0, [binary, {active, false}, {verify, verify_none}, + {fail_if_no_peer_cert, false}|Opts]) + end, + {ok, {_, Port}} = Transport:sockname(ListenSocket), + Parent ! {self(), Port}, + {ok, ClientSocket} = case Transport of + gun_tcp -> + gen_tcp:accept(ListenSocket, infinity); + gun_tls -> + {ok, ClientSocket0} = ssl:transport_accept(ListenSocket, infinity), + {ok, ClientSocket1} = ssl:handshake(ClientSocket0, infinity), + {ok, ClientSocket1} + end, + {ok, Data} = case Transport of + gun_tcp -> + gen_tcp:recv(ClientSocket, 0, infinity); + gun_tls -> + ssl:recv(ClientSocket, 0, infinity) + end, + {Method= <<"CONNECT">>, Authority, Version, Rest} = cow_http:parse_request_line(Data), + {Headers, <<>>} = cow_http:parse_headers(Rest), + timer:sleep(Delay), + Parent ! {self(), {request, Method, Authority, Version, Headers}}, + {OriginHost, OriginPort} = cow_http_hd:parse_host(Authority), + ok = Transport:send(ClientSocket, [ + ConnectRespVersion, <<" ">>, + integer_to_binary(Status), + <<" Reason phrase\r\n">>, + cow_http:headers(ConnectRespHeaders), + <<"\r\n">> + ]), + if + Status >= 200, Status < 300 -> + {ok, OriginSocket} = gen_tcp:connect( + binary_to_list(OriginHost), OriginPort, + [binary, {active, false}]), + Transport:setopts(ClientSocket, [{active, true}]), + inet:setopts(OriginSocket, [{active, true}]), + do_proxy_loop(Transport, ClientSocket, OriginSocket); + true -> + timer:sleep(infinity) + end. + +do_proxy_loop(Transport, ClientSocket, OriginSocket) -> + {OK, _, _} = Transport:messages(), + receive + {OK, ClientSocket, Data} -> + case gen_tcp:send(OriginSocket, Data) of + ok -> + do_proxy_loop(Transport, ClientSocket, OriginSocket); + {error, _} -> + ok + end; + {tcp, OriginSocket, Data} -> + case Transport:send(ClientSocket, Data) of + ok -> + do_proxy_loop(Transport, ClientSocket, OriginSocket); + {error, _} -> + ok + end; + %% Wait forever when a connection gets closed. We will exit with the test process. + {tcp_closed, _} -> + timer:sleep(infinity); + {ssl_closed, _} -> + timer:sleep(infinity); + Msg -> + error(Msg) + end. + +%% Tests. + +connect_http(_) -> + doc("CONNECT can be used to establish a TCP connection " + "to an HTTP/1.1 server via an HTTP proxy. (RFC7231 4.3.6)"), + do_connect_http(<<"http">>, tcp, tcp). + +connect_https(_) -> + doc("CONNECT can be used to establish a TLS connection " + "to an HTTP/1.1 server via an HTTP proxy. (RFC7231 4.3.6)"), + do_connect_http(<<"https">>, tls, tcp). + +connect_http_over_https_proxy(_) -> + doc("CONNECT can be used to establish a TCP connection " + "to an HTTP/1.1 server via an HTTPS proxy. (RFC7231 4.3.6)"), + do_connect_http(<<"http">>, tcp, tls). + +connect_https_over_https_proxy(_) -> + doc("CONNECT can be used to establish a TLS connection " + "to an HTTP/1.1 server via an HTTPS proxy. (RFC7231 4.3.6)"), + do_connect_http(<<"https">>, tls, tls). + +do_connect_http(OriginScheme, OriginTransport, ProxyTransport) -> + {ok, OriginPid, OriginPort} = init_origin(OriginTransport, http), + {ok, ProxyPid, ProxyPort} = do_proxy_start(ProxyTransport), + Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + transport => ProxyTransport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}] + }), + {ok, http} = gun:await_up(ConnPid), + StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + transport => OriginTransport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}] + }), + {request, <<"CONNECT">>, Authority, 'HTTP/1.1', _} = receive_from(ProxyPid), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef), + %% @todo Do we still need these handshake_completed messages? + handshake_completed = receive_from(OriginPid), + {up, http} = gun:await(ConnPid, StreamRef), + _ = gun:get(ConnPid, "/proxied", [], #{tunnel => StreamRef}), + Data = receive_from(OriginPid), + Lines = binary:split(Data, <<"\r\n">>, [global]), + [<<"host: ", Authority/bits>>] = [L || <<"host: ", _/bits>> = L <- Lines], + #{ + transport := OriginTransport, + protocol := http, + origin_scheme := OriginScheme, + origin_host := "localhost", + origin_port := OriginPort, + intermediaries := [#{ + type := connect, + host := "localhost", + port := ProxyPort, + transport := ProxyTransport, + protocol := http + }]} = gun:info(ConnPid), + gun:close(ConnPid). + +connect_h2c(_) -> + doc("CONNECT can be used to establish a TCP connection " + "to an HTTP/2 server via an HTTP proxy. (RFC7231 4.3.6)"), + do_connect_h2(<<"http">>, tcp, tcp). + +connect_h2(_) -> + doc("CONNECT can be used to establish a TLS connection " + "to an HTTP/2 server via an HTTP proxy. (RFC7231 4.3.6)"), + do_connect_h2(<<"https">>, tls, tcp). + +connect_h2c_over_https_proxy(_) -> + doc("CONNECT can be used to establish a TCP connection " + "to an HTTP/2 server via an HTTPS proxy. (RFC7231 4.3.6)"), + do_connect_h2(<<"http">>, tcp, tls). + +connect_h2_over_https_proxy(_) -> + doc("CONNECT can be used to establish a TLS connection " + "to an HTTP/2 server via an HTTPS proxy. (RFC7231 4.3.6)"), + do_connect_h2(<<"https">>, tls, tls). + +do_connect_h2(OriginScheme, OriginTransport, ProxyTransport) -> + {ok, OriginPid, OriginPort} = init_origin(OriginTransport, http2), + {ok, ProxyPid, ProxyPort} = do_proxy_start(ProxyTransport), + Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + transport => ProxyTransport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}] + }), + {ok, http} = gun:await_up(ConnPid), + StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + transport => OriginTransport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [http2] + }), + {request, <<"CONNECT">>, Authority, 'HTTP/1.1', _} = receive_from(ProxyPid), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef), + handshake_completed = receive_from(OriginPid), + {up, http2} = gun:await(ConnPid, StreamRef), + _ = gun:get(ConnPid, "/proxied", [], #{tunnel => StreamRef}), + <<_:24, 1:8, _/bits>> = receive_from(OriginPid), + #{ + transport := OriginTransport, + protocol := http2, + origin_scheme := OriginScheme, + origin_host := "localhost", + origin_port := OriginPort, + intermediaries := [#{ + type := connect, + host := "localhost", + port := ProxyPort, + transport := ProxyTransport, + protocol := http + }]} = gun:info(ConnPid), + gun:close(ConnPid). + +connect_tcp_through_multiple_tcp_proxies(_) -> + doc("CONNECT can be used to establish a TCP connection " + "to an HTTP/1.1 server via a tunnel going through " + "two separate HTTP proxies. (RFC7231 4.3.6)"), + do_connect_through_multiple_proxies(<<"http">>, tcp, tcp). + +connect_tls_through_multiple_tls_proxies(_) -> + doc("CONNECT can be used to establish a TLS connection " + "to an HTTP/1.1 server via a tunnel going through " + "two separate HTTPS proxies. (RFC7231 4.3.6)"), + do_connect_through_multiple_proxies(<<"https">>, tls, tls). + +do_connect_through_multiple_proxies(OriginScheme, OriginTransport, ProxiesTransport) -> + {ok, OriginPid, OriginPort} = init_origin(OriginTransport), + {ok, Proxy1Pid, Proxy1Port} = do_proxy_start(ProxiesTransport), + {ok, Proxy2Pid, Proxy2Port} = do_proxy_start(ProxiesTransport), + {ok, ConnPid} = gun:open("localhost", Proxy1Port, #{ + transport => ProxiesTransport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}] + }), + {ok, http} = gun:await_up(ConnPid), + Authority1 = iolist_to_binary(["localhost:", integer_to_binary(Proxy2Port)]), + StreamRef1 = gun:connect(ConnPid, #{ + host => "localhost", + port => Proxy2Port, + transport => ProxiesTransport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}] + }), + {request, <<"CONNECT">>, Authority1, 'HTTP/1.1', _} = receive_from(Proxy1Pid), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {up, http} = gun:await(ConnPid, StreamRef1), + Authority2 = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), + StreamRef2 = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + transport => OriginTransport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}] + }, [], #{tunnel => StreamRef1}), + {request, <<"CONNECT">>, Authority2, 'HTTP/1.1', _} = receive_from(Proxy2Pid), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef2), + handshake_completed = receive_from(OriginPid), + {up, http} = gun:await(ConnPid, StreamRef2), + _ = gun:get(ConnPid, "/proxied", [], #{tunnel => StreamRef2}), + Data = receive_from(OriginPid), + Lines = binary:split(Data, <<"\r\n">>, [global]), + [<<"host: ", Authority2/bits>>] = [L || <<"host: ", _/bits>> = L <- Lines], + #{ + transport := OriginTransport, + protocol := http, + origin_scheme := OriginScheme, + origin_host := "localhost", + origin_port := OriginPort, + intermediaries := [#{ + type := connect, + host := "localhost", + port := Proxy1Port, + transport := ProxiesTransport, + protocol := http + }, #{ + type := connect, + host := "localhost", + port := Proxy2Port, + transport := ProxiesTransport, + protocol := http + }]} = gun:info(ConnPid), + gun:close(ConnPid). + +connect_delay(_) -> + doc("The CONNECT response may not be immediate."), + {ok, OriginPid, OriginPort} = init_origin(tcp), + {ok, ProxyPid, ProxyPort} = do_proxy_start(tcp, 201, [], 2000), + Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), + {ok, ConnPid} = gun:open("localhost", ProxyPort, + #{http_opts => #{keepalive => 1000}}), + {ok, http} = gun:await_up(ConnPid), + StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort + }), + {request, <<"CONNECT">>, Authority, 'HTTP/1.1', _} = receive_from(ProxyPid, 3000), + {response, fin, 201, _} = gun:await(ConnPid, StreamRef), + handshake_completed = receive_from(OriginPid), + {up, http} = gun:await(ConnPid, StreamRef), + _ = gun:get(ConnPid, "/proxied", [], #{tunnel => StreamRef}), + Data = receive_from(OriginPid), + Lines = binary:split(Data, <<"\r\n">>, [global]), + [<<"host: ", Authority/bits>>] = [L || <<"host: ", _/bits>> = L <- Lines], + #{ + transport := tcp, + protocol := http, + origin_scheme := <<"http">>, + origin_host := "localhost", + origin_port := OriginPort, + intermediaries := [#{ + type := connect, + host := "localhost", + port := ProxyPort, + transport := tcp, + protocol := http + }]} = gun:info(ConnPid), + gun:close(ConnPid). + +connect_response_201(_) -> + doc("2xx responses to CONNECT requests indicate " + "the tunnel was set up successfully. (RFC7231 4.3.6)"), + {ok, OriginPid, OriginPort} = init_origin(tcp), + {ok, ProxyPid, ProxyPort} = do_proxy_start(tcp, 201), + Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), + {ok, ConnPid} = gun:open("localhost", ProxyPort), + {ok, http} = gun:await_up(ConnPid), + StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort + }), + {request, <<"CONNECT">>, Authority, 'HTTP/1.1', _} = receive_from(ProxyPid), + {response, fin, 201, _} = gun:await(ConnPid, StreamRef), + handshake_completed = receive_from(OriginPid), + {up, http} = gun:await(ConnPid, StreamRef), + _ = gun:get(ConnPid, "/proxied", [], #{tunnel => StreamRef}), + Data = receive_from(OriginPid), + Lines = binary:split(Data, <<"\r\n">>, [global]), + [<<"host: ", Authority/bits>>] = [L || <<"host: ", _/bits>> = L <- Lines], + #{ + transport := tcp, + protocol := http, + origin_scheme := <<"http">>, + origin_host := "localhost", + origin_port := OriginPort, + intermediaries := [#{ + type := connect, + host := "localhost", + port := ProxyPort, + transport := tcp, + protocol := http + }]} = gun:info(ConnPid), + gun:close(ConnPid). + +connect_response_302(_) -> + doc("3xx responses to CONNECT requests indicate " + "the tunnel was not set up. (RFC7231 4.3.6)"), + do_connect_failure(302). + +connect_response_403(_) -> + doc("4xx responses to CONNECT requests indicate " + "the tunnel was not set up. (RFC7231 4.3.6)"), + do_connect_failure(403). + +connect_response_500(_) -> + doc("5xx responses to CONNECT requests indicate " + "the tunnel was not set up. (RFC7231 4.3.6)"), + do_connect_failure(500). + +do_connect_failure(Status) -> + OriginPort = 33333, %% Doesn't matter because we won't try to connect. + Headers = [{<<"content-length">>, <<"0">>}], + {ok, ProxyPid, ProxyPort} = do_proxy_start(tcp, Status, Headers), + Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), + {ok, ConnPid} = gun:open("localhost", ProxyPort), + {ok, http} = gun:await_up(ConnPid), + StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort + }), + {request, <<"CONNECT">>, Authority, 'HTTP/1.1', _} = receive_from(ProxyPid), + {response, fin, Status, Headers} = gun:await(ConnPid, StreamRef), + %% We cannot do a request because the StreamRef is not a tunnel. + FailedStreamRef = gun:get(ConnPid, "/proxied", [], #{tunnel => StreamRef}), + {error, {stream_error, {badstate, _}}} = gun:await(ConnPid, FailedStreamRef), + #{ + transport := tcp, + protocol := http, + origin_scheme := <<"http">>, + origin_host := "localhost", + origin_port := ProxyPort, + intermediaries := [] + } = gun:info(ConnPid), + gun:close(ConnPid). + +connect_response_http10(_) -> + doc("CONNECT can be used to establish a TCP connection " + "to a server via an HTTP/1.0 proxy. (RFC7230 2.6, RFC7231 4.3.6)"), + {ok, OriginPid, OriginPort} = init_origin(tcp), + {ok, ProxyPid, ProxyPort} = do_proxy_start(tcp, 201, [], 0, <<"HTTP/1.0">>), + Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), + {ok, ConnPid} = gun:open("localhost", ProxyPort), + {ok, http} = gun:await_up(ConnPid), + StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort + }), + {request, <<"CONNECT">>, Authority, 'HTTP/1.1', _} = receive_from(ProxyPid), + {response, fin, 201, _} = gun:await(ConnPid, StreamRef), + handshake_completed = receive_from(OriginPid), + {up, http} = gun:await(ConnPid, StreamRef), + _ = gun:get(ConnPid, "/proxied", [], #{tunnel => StreamRef}), + Data = receive_from(OriginPid), + Lines = binary:split(Data, <<"\r\n">>, [global]), + [<<"host: ", Authority/bits>>] = [L || <<"host: ", _/bits>> = L <- Lines], + #{ + transport := tcp, + protocol := http, + origin_scheme := <<"http">>, + origin_host := "localhost", + origin_port := OriginPort, + intermediaries := [#{ + type := connect, + host := "localhost", + port := ProxyPort, + transport := tcp, + protocol := http + }]} = gun:info(ConnPid), + gun:close(ConnPid). + +connect_authority_form(_) -> + doc("CONNECT requests must use the authority-form. (RFC7231 4.3.6)"), + {ok, _OriginPid, OriginPort} = init_origin(tcp), + {ok, ProxyPid, ProxyPort} = do_proxy_start(tcp), + Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), + {ok, ConnPid} = gun:open("localhost", ProxyPort), + {ok, http} = gun:await_up(ConnPid), + _StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort + }), + {request, <<"CONNECT">>, Authority, 'HTTP/1.1', _} = receive_from(ProxyPid), + {<<"localhost">>, OriginPort} = cow_http_hd:parse_host(Authority), + gun:close(ConnPid). + +connect_proxy_authorization(_) -> + doc("CONNECT requests may include a proxy-authorization header. (RFC7231 4.3.6)"), + {ok, _OriginPid, OriginPort} = init_origin(tcp), + {ok, ProxyPid, ProxyPort} = do_proxy_start(tcp), + Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), + {ok, ConnPid} = gun:open("localhost", ProxyPort), + {ok, http} = gun:await_up(ConnPid), + _StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + username => "essen", + password => "myrealpasswordis" + }), + {request, <<"CONNECT">>, Authority, 'HTTP/1.1', Headers} = receive_from(ProxyPid), + {_, ProxyAuthorization} = lists:keyfind(<<"proxy-authorization">>, 1, Headers), + {basic, <<"essen">>, <<"myrealpasswordis">>} + = cow_http_hd:parse_proxy_authorization(ProxyAuthorization), + gun:close(ConnPid). + +connect_request_no_transfer_encoding(_) -> + doc("The payload for CONNECT requests has no defined semantics. " + "The transfer-encoding header should not be sent. (RFC7231 4.3.6)"), + {ok, _OriginPid, OriginPort} = init_origin(tcp), + {ok, ProxyPid, ProxyPort} = do_proxy_start(tcp), + Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), + {ok, ConnPid} = gun:open("localhost", ProxyPort), + {ok, http} = gun:await_up(ConnPid), + _StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort + }), + {request, <<"CONNECT">>, Authority, 'HTTP/1.1', Headers} = receive_from(ProxyPid), + false = lists:keyfind(<<"transfer-encoding">>, 1, Headers), + gun:close(ConnPid). + +connect_request_no_content_length(_) -> + doc("The payload for CONNECT requests has no defined semantics. " + "The content-length header should not be sent. (RFC7231 4.3.6)"), + {ok, _OriginPid, OriginPort} = init_origin(tcp), + {ok, ProxyPid, ProxyPort} = do_proxy_start(tcp), + Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), + {ok, ConnPid} = gun:open("localhost", ProxyPort), + {ok, http} = gun:await_up(ConnPid), + _StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort + }), + {request, <<"CONNECT">>, Authority, 'HTTP/1.1', Headers} = receive_from(ProxyPid), + false = lists:keyfind(<<"content-length">>, 1, Headers), + gun:close(ConnPid). + +connect_response_ignore_transfer_encoding(_) -> + doc("Clients must ignore transfer-encoding headers in responses " + "to CONNECT requests. (RFC7231 4.3.6)"), + {ok, OriginPid, OriginPort} = init_origin(tcp), + Headers = [{<<"transfer-encoding">>, <<"chunked">>}], + {ok, ProxyPid, ProxyPort} = do_proxy_start(tcp, 200, Headers), + Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), + {ok, ConnPid} = gun:open("localhost", ProxyPort), + {ok, http} = gun:await_up(ConnPid), + StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort + }), + {request, <<"CONNECT">>, Authority, 'HTTP/1.1', _} = receive_from(ProxyPid), + {response, fin, 200, Headers} = gun:await(ConnPid, StreamRef), + handshake_completed = receive_from(OriginPid), + {up, http} = gun:await(ConnPid, StreamRef), + _ = gun:get(ConnPid, "/proxied", [], #{tunnel => StreamRef}), + Data = receive_from(OriginPid), + Lines = binary:split(Data, <<"\r\n">>, [global]), + [<<"host: ", Authority/bits>>] = [L || <<"host: ", _/bits>> = L <- Lines], + gun:close(ConnPid). + +connect_response_ignore_content_length(_) -> + doc("Clients must ignore content-length headers in responses " + "to CONNECT requests. (RFC7231 4.3.6)"), + {ok, OriginPid, OriginPort} = init_origin(tcp), + Headers = [{<<"content-length">>, <<"1000">>}], + {ok, ProxyPid, ProxyPort} = do_proxy_start(tcp, 200, Headers), + Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), + {ok, ConnPid} = gun:open("localhost", ProxyPort), + {ok, http} = gun:await_up(ConnPid), + StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort + }), + {request, <<"CONNECT">>, Authority, 'HTTP/1.1', _} = receive_from(ProxyPid), + {response, fin, 200, Headers} = gun:await(ConnPid, StreamRef), + handshake_completed = receive_from(OriginPid), + {up, http} = gun:await(ConnPid, StreamRef), + _ = gun:get(ConnPid, "/proxied", [], #{tunnel => StreamRef}), + Data = receive_from(OriginPid), + Lines = binary:split(Data, <<"\r\n">>, [global]), + [<<"host: ", Authority/bits>>] = [L || <<"host: ", _/bits>> = L <- Lines], + gun:close(ConnPid). diff --git a/gun/test/rfc7540_SUITE.erl b/gun/test/rfc7540_SUITE.erl new file mode 100644 index 0000000..79ae347 --- /dev/null +++ b/gun/test/rfc7540_SUITE.erl @@ -0,0 +1,808 @@ +%% Copyright (c) 2018-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(rfc7540_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-import(ct_helper, [doc/1]). +-import(gun_test, [init_origin/2]). +-import(gun_test, [init_origin/3]). +-import(gun_test, [receive_from/1]). + +all() -> + ct_helper:all(?MODULE). + +%% Proxy helpers. + +-record(proxy_stream, { + id, + status, + resp_headers = [], + delay = 0, + origin_socket +}). + +-record(proxy, { + parent, + socket, + transport, + streams = [], + decode_state = cow_hpack:init(), + encode_state = cow_hpack:init() +}). + +do_proxy_start(Transport) -> + do_proxy_start(Transport, [#proxy_stream{id=1, status=200, resp_headers=[], delay=0}]). + +do_proxy_start(Transport0, Streams) -> + Transport = case Transport0 of + tcp -> gun_tcp; + tls -> gun_tls + end, + Proxy = #proxy{parent=self(), transport=Transport, streams=Streams}, + Pid = spawn_link(fun() -> do_proxy_init(Proxy) end), + Port = receive_from(Pid), + {ok, Pid, Port}. + +do_proxy_init(Proxy=#proxy{parent=Parent, transport=Transport}) -> + {ok, ListenSocket} = case Transport of + gun_tcp -> + gen_tcp:listen(0, [binary, {active, false}]); + gun_tls -> + Opts = ct_helper:get_certs_from_ets(), + ssl:listen(0, [binary, {active, false}, {verify, verify_none}, + {fail_if_no_peer_cert, false}, + {alpn_preferred_protocols, [<<"h2">>]}|Opts]) + end, + {ok, {_, Port}} = Transport:sockname(ListenSocket), + Parent ! {self(), Port}, + {ok, Socket} = case Transport of + gun_tcp -> + gen_tcp:accept(ListenSocket, infinity); + gun_tls -> + {ok, Socket0} = ssl:transport_accept(ListenSocket, infinity), + ssl:handshake(Socket0, infinity), + {ok, <<"h2">>} = ssl:negotiated_protocol(Socket0), + {ok, Socket0} + end, + gun_test:http2_handshake(Socket, case Transport of + gun_tcp -> gen_tcp; + gun_tls -> ssl + end), + Parent ! {self(), handshake_completed}, + Transport:setopts(Socket, [{active, true}]), + do_proxy_receive(<<>>, Proxy#proxy{socket=Socket}). + +do_proxy_receive(Buffer, Proxy=#proxy{socket=Socket, transport=Transport}) -> + {OK, _, _} = Transport:messages(), + receive + {OK, Socket, Data0} -> + do_proxy_parse(<>, Proxy); + {tcp, OriginSocket, OriginData} -> + do_proxy_forward(Buffer, Proxy, OriginSocket, OriginData); + %% Wait forever when a connection gets closed. We will exit with the test process. + {tcp_closed, _} -> + timer:sleep(infinity); + {ssl_closed, _} -> + timer:sleep(infinity); + Msg -> + error(Msg) + end. + +%% We only expect to receive data on a CONNECT stream. +do_proxy_parse(<>, + Proxy=#proxy{streams=Streams}) -> + #proxy_stream{origin_socket=OriginSocket} + = lists:keyfind(StreamID, #proxy_stream.id, Streams), + case gen_tcp:send(OriginSocket, Payload) of + ok -> + do_proxy_parse(Rest, Proxy); + {error, _} -> + ok + end; +do_proxy_parse(<>, + Proxy=#proxy{parent=Parent, socket=Socket, transport=Transport, + streams=Streams0, decode_state=DecodeState0, encode_state=EncodeState0}) -> + #proxy_stream{status=Status, resp_headers=RespHeaders, delay=Delay} + = Stream = lists:keyfind(StreamID, #proxy_stream.id, Streams0), + {ReqHeaders0, DecodeState} = cow_hpack:decode(ReqHeadersBlock, DecodeState0), + ReqHeaders = maps:from_list(ReqHeaders0), + timer:sleep(Delay), + Parent ! {self(), {request, ReqHeaders}}, + {IsFin, OriginSocket} = case ReqHeaders of + #{<<":method">> := <<"CONNECT">>, <<":authority">> := Authority} + when Status >= 200, Status < 300 -> + {OriginHost, OriginPort} = cow_http_hd:parse_host(Authority), + {ok, OriginSocket0} = gen_tcp:connect( + binary_to_list(OriginHost), OriginPort, + [binary, {active, true}]), + {nofin, OriginSocket0}; + #{} -> + {fin, undefined} + end, + {RespHeadersBlock, EncodeState} = cow_hpack:encode([ + {<<":status">>, integer_to_binary(Status)} + |RespHeaders], EncodeState0), + ok = Transport:send(Socket, [ + cow_http2:headers(StreamID, IsFin, RespHeadersBlock) + ]), + Streams = lists:keystore(StreamID, #proxy_stream.id, Streams0, + Stream#proxy_stream{origin_socket=OriginSocket}), + do_proxy_parse(Rest, Proxy#proxy{streams=Streams, + decode_state=DecodeState, encode_state=EncodeState}); +%% An RST_STREAM was received. Stop the proxy. +do_proxy_parse(<<_:24, 3:8, _/bits>>, _) -> + ok; +do_proxy_parse(<>, Proxy) -> + ct:pal("Ignoring packet header ~0p~npayload ~p", [Header, Payload]), + do_proxy_parse(Rest, Proxy); +do_proxy_parse(Rest, Proxy) -> + do_proxy_receive(Rest, Proxy). + +do_proxy_forward(Buffer, Proxy=#proxy{socket=Socket, transport=Transport, streams=Streams}, + OriginSocket, OriginData) -> + #proxy_stream{id=StreamID} = lists:keyfind(OriginSocket, #proxy_stream.origin_socket, Streams), + Len = byte_size(OriginData), + Data = [<>, OriginData], + case Transport:send(Socket, Data) of + ok -> + do_proxy_receive(Buffer, Proxy); + {error, _} -> + ok + end. + +%% Tests. + +authority_default_port_http(_) -> + doc("The default port for http should not be sent in " + "the :authority pseudo-header. (RFC7540 3, RFC7230 2.7.1)"), + do_authority_port(tcp, 80, <<>>). + +authority_default_port_https(_) -> + doc("The default port for https should not be sent in " + "the :authority pseudo-header. (RFC7540 3, RFC7230 2.7.2)"), + do_authority_port(tls, 443, <<>>). + +authority_ipv6(_) -> + doc("When connecting to a server using an IPv6 address the :authority " + "pseudo-header must wrap the address with brackets. (RFC7540 8.1.2.3, RFC3986 3.2.2)"), + {ok, OriginPid, OriginPort} = init_origin(tcp6, http2, fun(Parent, _, Socket, Transport) -> + %% Receive the HEADERS frame and send the headers decoded. + {ok, <>} = Transport:recv(Socket, 9, 1000), + {ok, ReqHeadersBlock} = Transport:recv(Socket, Len, 1000), + {ReqHeaders, _} = cow_hpack:decode(ReqHeadersBlock), + Parent ! {self(), ReqHeaders} + end), + {ok, ConnPid} = gun:open({0,0,0,0,0,0,0,1}, OriginPort, #{ + transport => tcp, + protocols => [http2] + }), + {ok, http2} = gun:await_up(ConnPid), + handshake_completed = receive_from(OriginPid), + _ = gun:get(ConnPid, "/"), + ReqHeaders = receive_from(OriginPid), + {_, <<"[::1]", _/bits>>} = lists:keyfind(<<":authority">>, 1, ReqHeaders), + gun:close(ConnPid). + +authority_other_port_http(_) -> + doc("Non-default ports for http must be sent in " + "the :authority pseudo-header. (RFC7540 3, RFC7230 2.7.1)"), + do_authority_port(tcp, 443, <<":443">>). + +authority_other_port_https(_) -> + doc("Non-default ports for https must be sent in " + "the :authority pseudo-header. (RFC7540 3, RFC7230 2.7.2)"), + do_authority_port(tls, 80, <<":80">>). + +do_authority_port(Transport0, DefaultPort, AuthorityHeaderPort) -> + {ok, OriginPid, OriginPort} = init_origin(Transport0, http2, fun(Parent, _, Socket, Transport) -> + %% Receive the HEADERS frame and send the headers decoded. + {ok, <>} = Transport:recv(Socket, 9, 1000), + {ok, ReqHeadersBlock} = Transport:recv(Socket, Len, 1000), + {ReqHeaders, _} = cow_hpack:decode(ReqHeadersBlock), + Parent ! {self(), ReqHeaders} + end), + {ok, ConnPid} = gun:open("localhost", OriginPort, #{ + transport => Transport0, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [http2] + }), + {ok, http2} = gun:await_up(ConnPid), + handshake_completed = receive_from(OriginPid), + %% Change the origin's port in the state to trigger the default port behavior. + _ = sys:replace_state(ConnPid, fun({StateName, StateData}) -> + {StateName, setelement(8, StateData, DefaultPort)} + end, 5000), + %% Confirm the default port is not sent in the request. + _ = gun:get(ConnPid, "/"), + ReqHeaders = receive_from(OriginPid), + {_, <<"localhost", Rest/bits>>} = lists:keyfind(<<":authority">>, 1, ReqHeaders), + AuthorityHeaderPort = Rest, + gun:close(ConnPid). + +prior_knowledge_preface_garbage(_) -> + doc("A PROTOCOL_ERROR connection error must result from the server sending " + "an invalid preface in the form of garbage when connecting " + "using the prior knowledge method. (RFC7540 3.4, RFC7540 3.5)"), + %% We use 'http' here because we are going to do the handshake manually. + {ok, OriginPid, Port} = init_origin(tcp, http, fun(_, _, Socket, Transport) -> + ok = Transport:send(Socket, <<0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15>>), + timer:sleep(100) + end), + {ok, ConnPid} = gun:open("localhost", Port, #{protocols => [http2]}), + {ok, http2} = gun:await_up(ConnPid), + handshake_completed = receive_from(OriginPid), + receive + {gun_down, ConnPid, http2, {error, {connection_error, protocol_error, + 'Invalid connection preface received. (RFC7540 3.5)'}}, []} -> + gun:close(ConnPid); + Msg -> + error({unexpected_msg, Msg}) + after 1000 -> + error(timeout) + end. + +prior_knowledge_preface_http1(_) -> + doc("A PROTOCOL_ERROR connection error must result from the server sending " + "an invalid preface in the form of an HTTP/1.1 response when connecting " + "using the prior knowledge method. (RFC7540 3.4, RFC7540 3.5)"), + %% We use 'http' here because we are going to do the handshake manually. + {ok, OriginPid, Port} = init_origin(tcp, http, fun(_, _, Socket, Transport) -> + ok = Transport:send(Socket, << + "HTTP/1.1 400 Bad Request\r\n" + "Connection: close\r\n" + "Content-Length: 0\r\n" + "Date: Thu, 27 Feb 2020 09:32:17 GMT\r\n" + "\r\n">>), + timer:sleep(100) + end), + {ok, ConnPid} = gun:open("localhost", Port, #{protocols => [http2]}), + {ok, http2} = gun:await_up(ConnPid), + handshake_completed = receive_from(OriginPid), + receive + {gun_down, ConnPid, http2, {error, {connection_error, protocol_error, + 'Invalid connection preface received. Appears to be an HTTP/1 response? (RFC7540 3.5)'}}, []} -> + gun:close(ConnPid); + Msg -> + error({unexpected_msg, Msg}) + after 1000 -> + error(timeout) + end. + +prior_knowledge_preface_http1_await(_) -> + doc("A PROTOCOL_ERROR connection error must result from the server sending " + "an invalid preface in the form of an HTTP/1.1 response when connecting " + "using the prior knowledge method. (RFC7540 3.4, RFC7540 3.5)"), + %% We use 'http' here because we are going to do the handshake manually. + {ok, OriginPid, Port} = init_origin(tcp, http, fun(_, _, Socket, Transport) -> + timer:sleep(100), + ok = Transport:send(Socket, << + "HTTP/1.1 400 Bad Request\r\n" + "Connection: close\r\n" + "Content-Length: 0\r\n" + "Date: Thu, 27 Feb 2020 09:32:17 GMT\r\n" + "\r\n">>), + timer:sleep(100) + end), + {ok, ConnPid} = gun:open("localhost", Port, #{protocols => [http2], retry => 0}), + {ok, http2} = gun:await_up(ConnPid), + handshake_completed = receive_from(OriginPid), + {error, {down, {shutdown, {error, {connection_error, protocol_error, + 'Invalid connection preface received. Appears to be an HTTP/1 response? (RFC7540 3.5)'}}}}} + = gun:await(ConnPid, make_ref()), + gun:close(ConnPid). + +prior_knowledge_preface_other_frame(_) -> + doc("A PROTOCOL_ERROR connection error must result from the server sending " + "an invalid preface in the form of a non-SETTINGS frame when connecting " + "using the prior knowledge method. (RFC7540 3.4, RFC7540 3.5)"), + %% We use 'http' here because we are going to do the handshake manually. + {ok, OriginPid, Port} = init_origin(tcp, http, fun(_, _, Socket, Transport) -> + ok = Transport:send(Socket, cow_http2:window_update(1)), + timer:sleep(100) + end), + {ok, ConnPid} = gun:open("localhost", Port, #{protocols => [http2]}), + {ok, http2} = gun:await_up(ConnPid), + handshake_completed = receive_from(OriginPid), + receive + {gun_down, ConnPid, http2, {error, {connection_error, protocol_error, + 'Invalid connection preface received. (RFC7540 3.5)'}}, []} -> + gun:close(ConnPid); + Msg -> + error({unexpected_msg, Msg}) + after 1000 -> + error(timeout) + end. + +lingering_data_counts_toward_connection_window(_) -> + doc("DATA frames received after sending RST_STREAM must be counted " + "toward the connection flow-control window. (RFC7540 5.1)"), + {ok, OriginPid, Port} = init_origin(tcp, http2, fun(_, _, Socket, Transport) -> + %% Step 2. + %% Receive a HEADERS frame. + {ok, <>} = Transport:recv(Socket, 9, 1000), + %% Skip the header. + {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), + %% Step 3. + %% Send a HEADERS frame. + {HeadersBlock, _} = cow_hpack:encode([ + {<<":status">>, <<"200">>} + ]), + ok = Transport:send(Socket, [ + cow_http2:headers(1, nofin, HeadersBlock) + ]), + %% Step 5. + %% Make sure Gun sends the RST_STREAM. + timer:sleep(100), + %% Step 7. + ok = Transport:send(Socket, [ + cow_http2:data(1, nofin, <<0:0/unit:8>>), + cow_http2:data(1, nofin, <<0:1000/unit:8>>) + ]), + %% Skip RST_STREAM. + {ok, << 4:24, 3:8, 1:40, _:32 >>} = gen_tcp:recv(Socket, 13, 1000), + %% Received a WINDOW_UPDATE frame after we got RST_STREAM. + {ok, << 4:24, 8:8, 0:40, Increment:32 >>} = gen_tcp:recv(Socket, 13, 1000), + true = Increment > 0 + end), + {ok, ConnPid} = gun:open("localhost", Port, #{ + protocols => [http2], + http2_opts => #{ + %% We don't set 65535 because we still want to have an initial WINDOW_UPDATE. + initial_connection_window_size => 65536, + initial_stream_window_size => 65535 + } + }), + {ok, http2} = gun:await_up(ConnPid), + handshake_completed = receive_from(OriginPid), + %% Step 1. + StreamRef = gun:get(ConnPid, "/"), + %% Step 4. + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef), + %% Step 6. + gun:cancel(ConnPid, StreamRef), + %% Make sure Gun sends the WINDOW_UPDATE and the server test passes. + timer:sleep(300), + gun:close(ConnPid). + +headers_priority_flag(_) -> + doc("HEADERS frames may include a PRIORITY flag indicating " + "that stream dependency information is attached. (RFC7540 6.2)"), + {ok, OriginPid, Port} = init_origin(tcp, http2, fun(_, _, Socket, Transport) -> + %% Receive a HEADERS frame. + {ok, <<_:24, 1:8, _:8, 1:32>>} = Transport:recv(Socket, 9, 1000), + %% Send a HEADERS frame with PRIORITY back. + {HeadersBlock, _} = cow_hpack:encode([ + {<<":status">>, <<"200">>} + ]), + Len = iolist_size(HeadersBlock) + 5, + ok = Transport:send(Socket, [ + <>, %% Weight. + HeadersBlock + ]), + timer:sleep(1000) + end), + {ok, ConnPid} = gun:open("localhost", Port, #{protocols => [http2]}), + {ok, http2} = gun:await_up(ConnPid), + handshake_completed = receive_from(OriginPid), + StreamRef = gun:get(ConnPid, "/"), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef), + gun:close(ConnPid). + +settings_ack_timeout(_) -> + doc("Failure to acknowledge the client's SETTINGS frame " + "results in a SETTINGS_TIMEOUT connection error. (RFC7540 6.5.3)"), + %% We use 'http' here because we are going to do the handshake manually. + {ok, _, Port} = init_origin(tcp, http, fun(_, _, Socket, Transport) -> + %% Send a valid preface. + ok = Transport:send(Socket, cow_http2:settings(#{})), + %% Receive the fixed sequence from the preface. + Preface = <<"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n">>, + {ok, Preface} = Transport:recv(Socket, byte_size(Preface), 5000), + %% Receive the SETTINGS from the preface. + {ok, <>} = Transport:recv(Socket, 3, 5000), + {ok, <<4:8, 0:40, _:Len/binary>>} = Transport:recv(Socket, 6 + Len, 5000), + %% Receive the WINDOW_UPDATE sent with the preface. + {ok, <<4:24, 8:8, 0:40, _:32>>} = Transport:recv(Socket, 13, 5000), + %% Receive the SETTINGS ack. + {ok, <<0:24, 4:8, 1:8, 0:32>>} = Transport:recv(Socket, 9, 5000), + %% Do not ack the client preface. Expect a GOAWAY with reason SETTINGS_TIMEOUT. + {ok, << _:24, 7:8, _:72, 4:32 >>} = Transport:recv(Socket, 17, 6000) + end), + {ok, ConnPid} = gun:open("localhost", Port, #{protocols => [http2]}), + {ok, http2} = gun:await_up(ConnPid), + timer:sleep(6000), + gun:close(ConnPid). + +keepalive_tolerance_ping_ack_timeout(_) -> + doc("The PING frame may be used to easily test a connection. (RFC7540 8.1.4)"), + {ok, OriginPid, OriginPort} = init_origin(tcp, http2, do_ping_ack_loop_fun()), + {ok, Pid} = gun:open("localhost", OriginPort, #{ + protocols => [http2], + http2_opts => #{keepalive => 1000, keepalive_tolerance => 2} + }), + {ok, http2} = gun:await_up(Pid), + handshake_completed = receive_from(OriginPid), + %% When Gun sends the first ping, the server acks immediately. + receive ping_received -> OriginPid ! send_ping_ack end, + timer:sleep(2500), %% Gun sends 2 pings while we sleep. 2 pings not acked. + %% Server acks one ping. One ping still not acked. + receive ping_received -> OriginPid ! send_ping_ack end, + timer:sleep(1000), %% Gun sends 1 ping while we sleep. 2 pings not acked. + %% Server acks one ping. One ping still not acked. + receive ping_received -> OriginPid ! send_ping_ack end, + timer:sleep(1000), %% Gun sends 1 ping while we sleep. 2 pings not acked. + %% Check that we haven't received a gun_down yet. + receive + GunDown when element(1, GunDown) =:= gun_down -> + error(unexpected) + after 0 -> + ok + end, + %% Within the next 500ms, Gun wants to send another ping, which would + %% result in 3 outstanding pings. Instead, Gun goes down. + receive + {gun_down, Pid, http2, {error, {connection_error, no_error, _}}, []} -> + gun:close(Pid) + after 1000 -> + error(timeout) + end. + +do_ping_ack_loop_fun() -> + %% Receive ping, sync with parent, send ping ack, loop. + fun Loop(Parent, ListenSocket, Socket, Transport) -> + {ok, Data} = Transport:recv(Socket, 9, infinity), + <> = Data, + {ok, Payload} = Transport:recv(Socket, Len, 1000), + 8 = Len = byte_size(Payload), + Parent ! ping_received, + receive + send_ping_ack -> + Ack = <<8:24, 6:8, %% PING + 1:8, %% Ack flag + 0:1, 0:31, Payload/binary>>, + ok = Transport:send(Socket, Ack) + end, + Loop(Parent, ListenSocket, Socket, Transport) + end. + +connect_http_via_h2c(_) -> + doc("CONNECT can be used to establish a TCP connection " + "to an HTTP/1.1 server via a TCP HTTP/2 proxy. (RFC7540 8.3)"), + do_connect_http(<<"http">>, tcp, http, <<"http">>, tcp). + +connect_https_via_h2c(_) -> + doc("CONNECT can be used to establish a TLS connection " + "to an HTTP/1.1 server via a TCP HTTP/2 proxy. (RFC7540 8.3)"), + do_connect_http(<<"https">>, tls, http, <<"http">>, tcp). + +connect_http_via_h2(_) -> + doc("CONNECT can be used to establish a TCP connection " + "to an HTTP/1.1 server via a TLS HTTP/2 proxy. (RFC7540 8.3)"), + do_connect_http(<<"http">>, tcp, http, <<"https">>, tls). + +connect_https_via_h2(_) -> + doc("CONNECT can be used to establish a TLS connection " + "to an HTTP/1.1 server via a TLS HTTP/2 proxy. (RFC7540 8.3)"), + do_connect_http(<<"https">>, tls, http, <<"https">>, tls). + +connect_h2c_via_h2c(_) -> + doc("CONNECT can be used to establish a TCP connection " + "to an HTTP/2 server via a TCP HTTP/2 proxy. (RFC7540 8.3)"), + do_connect_http(<<"http">>, tcp, http2, <<"http">>, tcp). + +connect_h2_via_h2c(_) -> + doc("CONNECT can be used to establish a TLS connection " + "to an HTTP/2 server via a TCP HTTP/2 proxy. (RFC7540 8.3)"), + do_connect_http(<<"https">>, tls, http2, <<"http">>, tcp). + +connect_h2c_via_h2(_) -> + doc("CONNECT can be used to establish a TCP connection " + "to an HTTP/2 server via a TLS HTTP/2 proxy. (RFC7540 8.3)"), + do_connect_http(<<"http">>, tcp, http2, <<"https">>, tls). + +connect_h2_via_h2(_) -> + doc("CONNECT can be used to establish a TLS connection " + "to an HTTP/2 server via a TLS HTTP/2 proxy. (RFC7540 8.3)"), + do_connect_http(<<"https">>, tls, http2, <<"https">>, tls). + +do_origin_fun(http) -> + fun(Parent, ListenSocket, Socket, Transport) -> + %% Receive the request-line and headers, parse and send them. + {ok, Data} = Transport:recv(Socket, 0, 5000), + {Method, Target, 'HTTP/1.1', Rest} = cow_http:parse_request_line(Data), + {Headers0, _} = cow_http:parse_headers(Rest), + Headers = maps:from_list(Headers0), + %% We roughly transform the HTTP/1.1 headers into HTTP/2 format. + Parent ! {self(), Headers#{ + <<":authority">> => maps:get(<<"host">>, Headers, <<>>), + <<":method">> => Method, + <<":path">> => Target + }}, + gun_test:loop_origin(Parent, ListenSocket, Socket, Transport) + end; +do_origin_fun(http2) -> + fun(Parent, ListenSocket, Socket, Transport) -> + %% Receive the HEADERS frame and send the headers decoded. + {ok, <>} = Transport:recv(Socket, 9, 1000), + {ok, ReqHeadersBlock} = Transport:recv(Socket, Len, 1000), + {ReqHeaders, _} = cow_hpack:decode(ReqHeadersBlock), + Parent ! {self(), maps:from_list(ReqHeaders)}, + gun_test:loop_origin(Parent, ListenSocket, Socket, Transport) + end. + +do_connect_http(OriginScheme, OriginTransport, OriginProtocol, ProxyScheme, ProxyTransport) -> + {ok, OriginPid, OriginPort} = init_origin(OriginTransport, OriginProtocol, do_origin_fun(OriginProtocol)), + {ok, ProxyPid, ProxyPort} = do_proxy_start(ProxyTransport, [ + #proxy_stream{id=1, status=200} + ]), + Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + transport => ProxyTransport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [http2] + }), + {ok, http2} = gun:await_up(ConnPid), + handshake_completed = receive_from(ProxyPid), + StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + transport => OriginTransport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [OriginProtocol] + }), + {request, #{ + <<":method">> := <<"CONNECT">>, + <<":authority">> := Authority + }} = receive_from(ProxyPid), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef), + handshake_completed = receive_from(OriginPid), + {up, OriginProtocol} = gun:await(ConnPid, StreamRef), + ProxiedStreamRef = gun:get(ConnPid, "/proxied", #{}, #{tunnel => StreamRef}), + #{<<":authority">> := Authority} = receive_from(OriginPid), + #{ + transport := ProxyTransport, + protocol := http2, + origin_scheme := ProxyScheme, + origin_host := "localhost", + origin_port := ProxyPort, + intermediaries := [] %% Intermediaries are specific to the CONNECT stream. + } = gun:info(ConnPid), + {ok, #{ + ref := StreamRef, + reply_to := Self, + state := running, + tunnel := #{ + transport := OriginTransport, + protocol := OriginProtocol, + origin_scheme := OriginScheme, + origin_host := "localhost", + origin_port := OriginPort + } + }} = gun:stream_info(ConnPid, StreamRef), + {ok, #{ + ref := ProxiedStreamRef, + reply_to := Self, + state := running, + intermediaries := [#{ + type := connect, + host := "localhost", + port := ProxyPort, + transport := ProxyTransport, + protocol := http2 + }] + }} = gun:stream_info(ConnPid, ProxiedStreamRef), + gun:close(ConnPid). + +connect_cowboy_http_via_h2c(_) -> + doc("CONNECT can be used to establish a TCP connection " + "to an HTTP/1.1 server via a TCP HTTP/2 proxy. (RFC7540 8.3)"), + do_connect_cowboy(<<"http">>, tcp, http, <<"http">>, tcp). + +connect_cowboy_https_via_h2c(_) -> + doc("CONNECT can be used to establish a TLS connection " + "to an HTTP/1.1 server via a TCP HTTP/2 proxy. (RFC7540 8.3)"), + do_connect_cowboy(<<"https">>, tls, http, <<"http">>, tcp). + +connect_cowboy_http_via_h2(_) -> + doc("CONNECT can be used to establish a TCP connection " + "to an HTTP/1.1 server via a TLS HTTP/2 proxy. (RFC7540 8.3)"), + do_connect_cowboy(<<"http">>, tcp, http, <<"https">>, tls). + +connect_cowboy_https_via_h2(_) -> + doc("CONNECT can be used to establish a TLS connection " + "to an HTTP/1.1 server via a TLS HTTP/2 proxy. (RFC7540 8.3)"), + do_connect_cowboy(<<"https">>, tls, http, <<"https">>, tls). + +connect_cowboy_h2c_via_h2c(_) -> + doc("CONNECT can be used to establish a TCP connection " + "to an HTTP/2 server via a TCP HTTP/2 proxy. (RFC7540 8.3)"), + do_connect_cowboy(<<"http">>, tcp, http2, <<"http">>, tcp). + +connect_cowboy_h2_via_h2c(_) -> + doc("CONNECT can be used to establish a TLS connection " + "to an HTTP/2 server via a TCP HTTP/2 proxy. (RFC7540 8.3)"), + do_connect_cowboy(<<"https">>, tls, http2, <<"http">>, tcp). + +connect_cowboy_h2c_via_h2(_) -> + doc("CONNECT can be used to establish a TCP connection " + "to an HTTP/2 server via a TLS HTTP/2 proxy. (RFC7540 8.3)"), + do_connect_cowboy(<<"http">>, tcp, http2, <<"https">>, tls). + +connect_cowboy_h2_via_h2(_) -> + doc("CONNECT can be used to establish a TLS connection " + "to an HTTP/2 server via a TLS HTTP/2 proxy. (RFC7540 8.3)"), + do_connect_cowboy(<<"https">>, tls, http2, <<"https">>, tls). + +do_connect_cowboy(_OriginScheme, OriginTransport, OriginProtocol, _ProxyScheme, ProxyTransport) -> + {ok, Ref, OriginPort} = do_cowboy_origin(OriginTransport, OriginProtocol), + try + {ok, ProxyPid, ProxyPort} = do_proxy_start(ProxyTransport, [ + #proxy_stream{id=1, status=200}, + #proxy_stream{id=3, status=299} + ]), + Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + transport => ProxyTransport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [http2] + }), + {ok, http2} = gun:await_up(ConnPid), + handshake_completed = receive_from(ProxyPid), + StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + transport => OriginTransport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [OriginProtocol] + }), + {request, #{ + <<":method">> := <<"CONNECT">>, + <<":authority">> := Authority + }} = receive_from(ProxyPid), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef), + {up, OriginProtocol} = gun:await(ConnPid, StreamRef), + ProxiedStreamRef = gun:get(ConnPid, "/proxied", #{}, #{tunnel => StreamRef}), + timer:sleep(1000), %% @todo Why? + {response, nofin, 200, _} = gun:await(ConnPid, ProxiedStreamRef), + %% We can create more requests on the proxy as well. + ProxyStreamRef = gun:get(ConnPid, "/"), + {response, fin, 299, _} = gun:await(ConnPid, ProxyStreamRef), + gun:close(ConnPid) + after + cowboy:stop_listener(Ref) + end. + +do_cowboy_origin(OriginTransport, OriginProtocol) -> + Ref = make_ref(), + ProtoOpts0 = case OriginTransport of + tcp -> #{protocols => [OriginProtocol]}; + tls -> #{} + end, + ProtoOpts = ProtoOpts0#{ + env => #{dispatch => cowboy_router:compile([{'_', [ + {"/proxied/[...]", proxied_h, []} + ]}])} + }, + [{ref, _}, {port, Port}] = case OriginTransport of + tcp -> gun_test:init_cowboy_tcp(Ref, ProtoOpts, []); + tls -> gun_test:init_cowboy_tls(Ref, ProtoOpts, []) + end, + {ok, Ref, Port}. + +connect_handshake_timeout(_) -> + doc("HTTP/2 timeouts are properly routed to the appropriate " + "tunnel layer. (RFC7540 3.5, RFC7540 8.3)"), + {ok, _, OriginPort} = init_origin(tcp, raw, fun(_, _, _, _) -> + timer:sleep(5000) + end), + {ok, ProxyPid, ProxyPort} = do_proxy_start(tcp, [ + #proxy_stream{id=1, status=200} + ]), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + protocols => [http2] + }), + {ok, http2} = gun:await_up(ConnPid), + handshake_completed = receive_from(ProxyPid), + StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + protocols => [{http2, #{preface_timeout => 500}}] + }), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef), + {up, http2} = gun:await(ConnPid, StreamRef), + %% @todo The error should be normalized. + %% @todo Do we want to indicate that a connection_error occurred within the tunnel stream? + {error, {stream_error, {stream_error, protocol_error, + 'The preface was not received in a reasonable amount of time.'}}} + = gun:await(ConnPid, StreamRef), + gun:close(ConnPid). + +connect_http_via_http_via_h2c(_) -> + doc("CONNECT can be used to establish a TCP connection " + "to an HTTP/1.1 server via a tunnel going through both " + "a TCP HTTP/2 and a TCP HTTP/1.1 proxy. (RFC7540 8.3)"), + do_connect_via_multiple_proxies(tcp, http, tcp, http, tcp). + +connect_https_via_https_via_h2(_) -> + doc("CONNECT can be used to establish a TLS connection " + "to an HTTP/1.1 server via a tunnel going through both " + "a TLS HTTP/2 and a TLS HTTP/1.1 proxy. (RFC7540 8.3)"), + do_connect_via_multiple_proxies(tls, http, tls, http, tls). + +do_connect_via_multiple_proxies(OriginTransport, OriginProtocol, + Proxy2Transport, Proxy2Protocol, Proxy1Transport) -> + {ok, Ref, OriginPort} = do_cowboy_origin(OriginTransport, OriginProtocol), + try + {ok, Proxy1Pid, Proxy1Port} = do_proxy_start(Proxy1Transport, [ + #proxy_stream{id=1, status=200} + ]), + {ok, Proxy2Pid, Proxy2Port} = rfc7231_SUITE:do_proxy_start(Proxy2Transport), + %% First proxy. + {ok, ConnPid} = gun:open("localhost", Proxy1Port, #{ + transport => Proxy1Transport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [http2] + }), + {ok, http2} = gun:await_up(ConnPid), + handshake_completed = receive_from(Proxy1Pid), + %% Second proxy. + StreamRef1 = gun:connect(ConnPid, #{ + host => "localhost", + port => Proxy2Port, + transport => Proxy2Transport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [Proxy2Protocol] + }, []), + Authority1 = iolist_to_binary(["localhost:", integer_to_binary(Proxy2Port)]), + {request, #{ + <<":method">> := <<"CONNECT">>, + <<":authority">> := Authority1 + }} = receive_from(Proxy1Pid), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {up, Proxy2Protocol} = gun:await(ConnPid, StreamRef1), + %% Origin. + StreamRef2 = gun:connect(ConnPid, #{ + host => "localhost", + port => OriginPort, + transport => OriginTransport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [OriginProtocol] + }, [], #{tunnel => StreamRef1}), + Authority2 = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), + {request, <<"CONNECT">>, Authority2, 'HTTP/1.1', _} = receive_from(Proxy2Pid), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef2), + {up, OriginProtocol} = gun:await(ConnPid, StreamRef2), + %% Tunneled request to the origin. + ProxiedStreamRef = gun:get(ConnPid, "/proxied", [], #{tunnel => StreamRef2}), + {response, nofin, 200, _} = gun:await(ConnPid, ProxiedStreamRef), + gun:close(ConnPid) + %% @todo Also test stream_info. + after + cowboy:stop_listener(Ref) + end. diff --git a/gun/test/send_errors_SUITE.erl b/gun/test/send_errors_SUITE.erl new file mode 100644 index 0000000..8cce875 --- /dev/null +++ b/gun/test/send_errors_SUITE.erl @@ -0,0 +1,139 @@ +%% Copyright (c) 2020-2023, Björn Svensson +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(send_errors_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-import(ct_helper, [doc/1]). +-import(gun_test, [http2_handshake/2]). + +suite() -> + [{timetrap, 180000}]. + +all() -> + [{group, gun}]. + +groups() -> + [{gun, [parallel], ct_helper:all(?MODULE)}]. + +init_per_suite(Config) -> + case os:type() of + {_, linux} -> Config; + _ -> {skip, "This test suite is Linux-only due to socket juggling."} + end. + +end_per_suite(_) -> ok. + +%% Tests. + +http2_send_request_fail(_) -> + doc("Handle send failures of requests in HTTP/2."), + {ok, ListenSocket} = gen_tcp:listen(0, [binary, {active, false}]), + {ok, {_, Port}} = inet:sockname(ListenSocket), + %% Socket buffers needs to be smaller than local_window/ConnWindow + {ok, Pid} = gun:open("localhost", Port, #{ + protocols => [http2], + tcp_opts => [ + {send_timeout, 250}, + {send_timeout_close, true}, + {sndbuf, 2048}, + {nodelay, true} + ] + }), + {ok, ClientSocket} = gen_tcp:accept(ListenSocket, 5000), + inet:setopts(ClientSocket, [{recbuf, 512}]), + http2_handshake(ClientSocket, gen_tcp), + {ok, http2} = gun:await_up(Pid), + post_loop(Pid, 1000), %% Fill buffer + receive + {gun_error, Pid, _, {closed, {error, _}}} -> + gun:close(Pid); + Msg -> + error({fail, Msg}) + after 5000 -> + error(timeout) + end. + +http2_send_ping_fail(_) -> + doc("Handle send failures of ping in HTTP/2."), + {ok, ListenSocket} = gen_tcp:listen(0, [binary, {active, false}]), + {ok, {_, Port}} = inet:sockname(ListenSocket), + {ok, Pid} = gun:open("localhost", Port, #{ + protocols => [http2], + http2_opts => #{keepalive => 1}, + tcp_opts => [ + {send_timeout, 250}, + {send_timeout_close, true}, + {sndbuf, 256}, + {nodelay, true} + ] + }), + {ok, ClientSocket} = gen_tcp:accept(ListenSocket, 5000), + inet:setopts(ClientSocket, [{recbuf, 256}]), + http2_handshake(ClientSocket, gen_tcp), + {ok, http2} = gun:await_up(Pid), + receive + {gun_down, Pid, http2, {error, _}, []} -> + gun:close(Pid); + Msg -> + error({fail, Msg}) + after 5000 -> + error(timeout) + end. + +http2_send_ping_ack_fail(_) -> + doc("Handle send failures of ping ack in HTTP/2."), + {ok, ListenSocket} = gen_tcp:listen(0, [binary, {active, false}]), + {ok, {_, Port}} = inet:sockname(ListenSocket), + {ok, Pid} = gun:open("localhost", Port, #{ + protocols => [http2], + http2_opts => #{keepalive => infinity}, + tcp_opts => [ + {send_timeout, 250}, + {send_timeout_close, true}, + {sndbuf, 256}, + {nodelay, true} + ] + }), + {ok, ClientSocket} = gen_tcp:accept(ListenSocket, 5000), + inet:setopts(ClientSocket, [{recbuf, 256}]), + http2_handshake(ClientSocket, gen_tcp), + {ok, http2} = gun:await_up(Pid), + ping_loop(ClientSocket, 1800), %% Send pings triggering ping acks + receive + {gun_down, Pid, http2, {error, _}, []} -> + gun:close(Pid); + Msg -> + error({fail, Msg}) + after 5000 -> + error(timeout) + end. + +%% Helpers + +post_loop(_Pid, 0) -> + ok; +post_loop(Pid, Loops) -> + Body = <<0:1000>>, + gun:post(Pid, "/organizations/ninenines", + [{<<"content-type">>, "application/octet-stream"}], + Body), + post_loop(Pid, Loops - 1). + +ping_loop(_Socket, 0) -> + ok; +ping_loop(Socket, Loops) -> + gun_tcp:send(Socket, cow_http2:ping(0)), + ping_loop(Socket, Loops - 1). diff --git a/gun/test/shutdown_SUITE.erl b/gun/test/shutdown_SUITE.erl new file mode 100644 index 0000000..77a2903 --- /dev/null +++ b/gun/test/shutdown_SUITE.erl @@ -0,0 +1,672 @@ +%% Copyright (c) 2019-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(shutdown_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-import(ct_helper, [doc/1]). +-import(ct_helper, [config/2]). +-import(gun_test, [init_origin/3]). +-import(gun_test, [receive_from/1]). + +suite() -> + [{timetrap, 30000}]. + +all() -> + [{group, shutdown}]. + +groups() -> + [{shutdown, [parallel], ct_helper:all(?MODULE)}]. + +init_per_suite(Config) -> + ProtoOpts = #{env => #{ + dispatch => cowboy_router:compile([{'_', [ + {"/", hello_h, []}, + {"/delayed", delayed_hello_h, 500}, + {"/delayed_push", delayed_push_h, 500}, + {"/empty", empty_h, []}, + {"/ws", ws_echo_h, []}, + {"/ws_frozen", ws_frozen_h, 500}, + %% This timeout determines how long the test suite will run. + {"/ws_frozen_long", ws_frozen_h, 1500}, + {"/ws_timeout_close", ws_timeout_close_h, 500} + ]}]) + }}, + {ok, _} = cowboy:start_clear(?MODULE, [], ProtoOpts), + OriginPort = ranch:get_port(?MODULE), + [{origin_port, OriginPort}|Config]. + +end_per_suite(_) -> + ok = cowboy:stop_listener(?MODULE). + +%% Tests. +%% +%% This test suite checks that the various ways to shut down +%% the connection are all working as expected for the different +%% protocols and scenarios. + +not_connected_gun_shutdown(_) -> + doc("Confirm that the Gun process shuts down gracefully " + "when calling gun:shutdown/1 while it isn't connected."), + {ok, ConnPid} = gun:open("localhost", 12345), + ConnRef = monitor(process, ConnPid), + gun:shutdown(ConnPid), + gun_is_down(ConnPid, ConnRef, shutdown). + +not_connected_owner_down(_) -> + doc("Confirm that the Gun process shuts down when the owner exits normally " + "while it isn't connected."), + do_not_connected_owner_down(normal, normal). + +not_connected_owner_down_error(_) -> + doc("Confirm that the Gun process shuts down when the owner exits with an error " + "while it isn't connected."), + do_not_connected_owner_down(unexpected, {shutdown, {owner_down, unexpected}}). + +do_not_connected_owner_down(ExitReason, DownReason) -> + Self = self(), + spawn(fun() -> + {ok, ConnPid} = gun:open("localhost", 12345), + Self ! {conn, ConnPid}, + timer:sleep(500), + exit(ExitReason) + end), + ConnPid = receive {conn, C} -> C end, + ConnRef = monitor(process, ConnPid), + gun_is_down(ConnPid, ConnRef, DownReason). + +http1_gun_shutdown_no_streams(Config) -> + doc("HTTP/1.1: Confirm that the Gun process shuts down gracefully " + "when calling gun:shutdown/1 with no active streams."), + do_http_gun_shutdown_no_streams(Config, http). + +do_http_gun_shutdown_no_streams(Config, Protocol) -> + {ok, ConnPid} = gun:open("localhost", config(origin_port, Config), #{ + protocols => [Protocol] + }), + {ok, Protocol} = gun:await_up(ConnPid), + ConnRef = monitor(process, ConnPid), + gun:shutdown(ConnPid), + gun_is_down(ConnPid, ConnRef, shutdown). + +http1_gun_shutdown_one_stream(Config) -> + doc("HTTP/1.1: Confirm that the Gun process shuts down gracefully " + "when calling gun:shutdown/1 with one active stream."), + do_http_gun_shutdown_one_stream(Config, http). + +do_http_gun_shutdown_one_stream(Config, Protocol) -> + {ok, ConnPid} = gun:open("localhost", config(origin_port, Config), #{ + protocols => [Protocol] + }), + {ok, Protocol} = gun:await_up(ConnPid), + ConnRef = monitor(process, ConnPid), + StreamRef = gun:get(ConnPid, "/delayed"), + gun:shutdown(ConnPid), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef), + {ok, _} = gun:await_body(ConnPid, StreamRef), + gun_is_down(ConnPid, ConnRef, shutdown). + +http1_gun_shutdown_pipelined_streams(Config) -> + doc("HTTP/1.1: Confirm that the Gun process shuts down gracefully " + "when calling gun:shutdown/1 with one active stream and additional pipelined streams."), + Protocol = http, + {ok, ConnPid} = gun:open("localhost", config(origin_port, Config), #{ + protocols => [Protocol] + }), + {ok, Protocol} = gun:await_up(ConnPid), + ConnRef = monitor(process, ConnPid), + StreamRef1 = gun:get(ConnPid, "/delayed"), + StreamRef2 = gun:get(ConnPid, "/delayed"), + StreamRef3 = gun:get(ConnPid, "/delayed"), + gun:shutdown(ConnPid), + %% Pipelined streams are canceled immediately. + {error, {stream_error, {closing, shutdown}}} = gun:await(ConnPid, StreamRef2), + {error, {stream_error, {closing, shutdown}}} = gun:await(ConnPid, StreamRef3), + %% The active stream is still processed. + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef1), + {ok, _} = gun:await_body(ConnPid, StreamRef1), + gun_is_down(ConnPid, ConnRef, shutdown). + +http1_gun_shutdown_timeout(Config) -> + doc("HTTP/1.1: Confirm that the Gun process shuts down when the closing_timeout " + "triggers after calling gun:shutdown/1 with one active stream."), + do_http_gun_shutdown_timeout(Config, http, http_opts). + +do_http_gun_shutdown_timeout(Config, Protocol, ProtoOpts) -> + {ok, ConnPid} = gun:open("localhost", config(origin_port, Config), #{ + ProtoOpts => #{closing_timeout => 100}, + protocols => [Protocol] + }), + {ok, Protocol} = gun:await_up(ConnPid), + ConnRef = monitor(process, ConnPid), + StreamRef = gun:get(ConnPid, "/delayed"), + gun:shutdown(ConnPid), + %% The closing timeout occurs before the server gets to send the response. + %% We get a 'closed' error instead of 'closing' as a result. + {error, {stream_error, {closed, shutdown}}} = gun:await(ConnPid, StreamRef), + gun_is_down(ConnPid, ConnRef, shutdown). + +http1_owner_down(Config) -> + doc("HTTP/1.1: Confirm that the Gun process shuts down when the owner exits normally."), + do_http_owner_down(Config, http, normal, normal). + +http1_owner_down_error(Config) -> + doc("HTTP/1.1: Confirm that the Gun process shuts down when the owner exits with an error."), + do_http_owner_down(Config, http, unexpected, {shutdown, {owner_down, unexpected}}). + +do_http_owner_down(Config, Protocol, ExitReason, DownReason) -> + Self = self(), + spawn(fun() -> + {ok, ConnPid} = gun:open("localhost", config(origin_port, Config), #{ + protocols => [Protocol] + }), + Self ! {conn, ConnPid}, + {ok, Protocol} = gun:await_up(ConnPid), + timer:sleep(500), + exit(ExitReason) + end), + ConnPid = receive {conn, C} -> C end, + ConnRef = monitor(process, ConnPid), + gun_is_down(ConnPid, ConnRef, DownReason). + +http1_request_connection_close(Config) -> + doc("HTTP/1.1: Confirm that the Gun process shuts down gracefully " + "when sending a request with the connection: close header and " + "retry is disabled."), + Protocol = http, + {ok, ConnPid} = gun:open("localhost", config(origin_port, Config), #{ + protocols => [Protocol], + retry => 0 + }), + {ok, Protocol} = gun:await_up(ConnPid), + ConnRef = monitor(process, ConnPid), + StreamRef = gun:get(ConnPid, "/", #{ + <<"connection">> => <<"close">> + }), + %% We get the response followed by Gun shutting down. + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef), + {ok, _} = gun:await_body(ConnPid, StreamRef), + gun_is_down(ConnPid, ConnRef, normal). + +http1_request_connection_close_pipeline(Config) -> + doc("HTTP/1.1: Confirm that the Gun process shuts down gracefully " + "when sending a request with the connection: close header and " + "retry is disabled. Pipelined requests get canceled."), + Protocol = http, + {ok, ConnPid} = gun:open("localhost", config(origin_port, Config), #{ + protocols => [Protocol], + retry => 0 + }), + {ok, Protocol} = gun:await_up(ConnPid), + ConnRef = monitor(process, ConnPid), + StreamRef1 = gun:get(ConnPid, "/", #{ + <<"connection">> => <<"close">> + }), + StreamRef2 = gun:get(ConnPid, "/"), + StreamRef3 = gun:get(ConnPid, "/"), + %% We get the response, pipelined streams get canceled, followed by Gun shutting down. + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef1), + {error, {stream_error, closing}} = gun:await(ConnPid, StreamRef2), + {error, {stream_error, closing}} = gun:await(ConnPid, StreamRef3), + {ok, _} = gun:await_body(ConnPid, StreamRef1), + gun_is_down(ConnPid, ConnRef, normal). + +http1_response_connection_close(_) -> + doc("HTTP/1.1: Confirm that the Gun process shuts down gracefully " + "when receiving a response with the connection: close header and " + "retry is disabled."), + {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], #{ + env => #{dispatch => cowboy_router:compile([{'_', [{"/", hello_h, []}]}])}, + max_keepalive => 1 + }), + OriginPort = ranch:get_port(?FUNCTION_NAME), + try + Protocol = http, + {ok, ConnPid} = gun:open("localhost", OriginPort, #{ + protocols => [Protocol], + retry => 0 + }), + {ok, Protocol} = gun:await_up(ConnPid), + ConnRef = monitor(process, ConnPid), + StreamRef = gun:get(ConnPid, "/"), + %% We get the response followed by Gun shutting down. + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef), + {ok, _} = gun:await_body(ConnPid, StreamRef), + gun_is_down(ConnPid, ConnRef, normal) + after + cowboy:stop_listener(?FUNCTION_NAME) + end. + +http1_response_connection_close_pipeline(_) -> + doc("HTTP/1.1: Confirm that the Gun process shuts down gracefully " + "when receiving a response with the connection: close header and " + "retry is disabled. Pipelined requests get canceled."), + {ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], #{ + env => #{dispatch => cowboy_router:compile([{'_', [{"/", hello_h, []}]}])}, + max_keepalive => 1 + }), + OriginPort = ranch:get_port(?FUNCTION_NAME), + try + Protocol = http, + {ok, ConnPid} = gun:open("localhost", OriginPort, #{ + protocols => [Protocol], + retry => 0 + }), + {ok, Protocol} = gun:await_up(ConnPid), + ConnRef = monitor(process, ConnPid), + StreamRef1 = gun:get(ConnPid, "/"), + StreamRef2 = gun:get(ConnPid, "/"), + StreamRef3 = gun:get(ConnPid, "/"), + %% We get the response, pipelined streams get canceled, followed by Gun shutting down. + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef1), + {ok, _} = gun:await_body(ConnPid, StreamRef1), + {error, {stream_error, closing}} = gun:await(ConnPid, StreamRef2), + {error, {stream_error, closing}} = gun:await(ConnPid, StreamRef3), + gun_is_down(ConnPid, ConnRef, normal) + after + cowboy:stop_listener(?FUNCTION_NAME) + end. + +http10_connection_close(Config) -> + doc("HTTP/1.0: Confirm that the Gun process shuts down gracefully " + "when sending a request without a connection header and " + "retry is disabled."), + Protocol = http, + {ok, ConnPid} = gun:open("localhost", config(origin_port, Config), #{ + http_opts => #{version => 'HTTP/1.0'}, + protocols => [Protocol], + retry => 0 + }), + {ok, Protocol} = gun:await_up(ConnPid), + ConnRef = monitor(process, ConnPid), + StreamRef = gun:get(ConnPid, "/"), + %% We get the response followed by Gun shutting down. + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef), + {ok, _} = gun:await_body(ConnPid, StreamRef), + gun_is_down(ConnPid, ConnRef, normal). + +http1_response_connection_close_delayed_body(_) -> + doc("HTTP/1.1: Confirm that requests initiated when Gun has received a " + "connection: close response header fail immediately if retry " + "is disabled, without waiting for the response body."), + ServerFun = fun(_, _, ClientSocket, gen_tcp) -> + try + {ok, Req} = gen_tcp:recv(ClientSocket, 0, 5000), + <<"GET / HTTP/1.1\r\n", _/binary>> = Req, + ok = gen_tcp:send(ClientSocket, <<"HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Content-Length: 12\r\n\r\nHello">>), + timer:sleep(500), + ok = gen_tcp:send(ClientSocket, " world!") + after + timer:sleep(1000), + gen_tcp:close(ClientSocket) + end + end, + {ok, ServerPid, OriginPort} = gun_test:init_origin(tcp, http, ServerFun), + %% Client connects. + {ok, ConnPid} = gun:open("localhost", OriginPort, #{ + protocols => [http], + retry => 0 + }), + {ok, _Protocol} = gun:await_up(ConnPid), + receive {ServerPid, handshake_completed} -> ok end, + ConnRef = monitor(process, ConnPid), + StreamRef1 = gun:get(ConnPid, "/"), + StreamRef2 = gun:get(ConnPid, "/"), + %% We get the response headers with connection: close. + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef1), + %% Pipelined request fails immediately. + {gun_error, ConnPid, StreamRef2, closing} = receive E2 -> E2 end, + {gun_data, ConnPid, StreamRef1, nofin, <<"Hello">>} = + receive PartialBody -> PartialBody end, + %% Request initiated when Gun is in closing state fails immediately. + StreamRef3 = gun:get(ConnPid, "/"), + {gun_error, ConnPid, StreamRef3, closing} = receive E3 -> E3 end, + {gun_data, ConnPid, StreamRef1, fin, <<" world!">>} = + receive RestBody -> RestBody end, + gun_is_down(ConnPid, ConnRef, normal). + +http2_gun_shutdown_no_streams(Config) -> + doc("HTTP/2: Confirm that the Gun process shuts down gracefully " + "when calling gun:shutdown/1 with no active streams."), + do_http_gun_shutdown_no_streams(Config, http2). + +http2_gun_shutdown_one_stream(Config) -> + doc("HTTP/2: Confirm that the Gun process shuts down gracefully " + "when calling gun:shutdown/1 with one active stream."), + do_http_gun_shutdown_one_stream(Config, http2). + +http2_gun_shutdown_many_streams(Config) -> + doc("HTTP/2: Confirm that the Gun process shuts down gracefully " + "when calling gun:shutdown/1 with many active streams."), + Protocol = http2, + {ok, ConnPid} = gun:open("localhost", config(origin_port, Config), #{ + protocols => [Protocol] + }), + {ok, Protocol} = gun:await_up(ConnPid), + ConnRef = monitor(process, ConnPid), + StreamRef1 = gun:get(ConnPid, "/delayed"), + StreamRef2 = gun:get(ConnPid, "/delayed"), + StreamRef3 = gun:get(ConnPid, "/delayed"), + gun:shutdown(ConnPid), + %% All streams are processed. + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef1), + {ok, _} = gun:await_body(ConnPid, StreamRef1), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2), + {ok, _} = gun:await_body(ConnPid, StreamRef2), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef3), + {ok, _} = gun:await_body(ConnPid, StreamRef3), + gun_is_down(ConnPid, ConnRef, shutdown). + +http2_gun_shutdown_timeout(Config) -> + doc("HTTP/2: Confirm that the Gun process shuts down when the closing_timeout " + "triggers after calling gun:shutdown/1 with one active stream."), + do_http_gun_shutdown_timeout(Config, http2, http2_opts). + +http2_gun_shutdown_ignore_push_promise(Config) -> + doc("HTTP/2: Confirm that the Gun process shuts down gracefully " + "when calling gun:shutdown/1 with one active stream. The " + "resource pushed by the server after we sent the GOAWAY frame " + "must be ignored."), + Protocol = http2, + {ok, ConnPid} = gun:open("localhost", config(origin_port, Config), #{ + protocols => [Protocol] + }), + {ok, Protocol} = gun:await_up(ConnPid), + ConnRef = monitor(process, ConnPid), + StreamRef = gun:get(ConnPid, "/delayed_push"), + gun:shutdown(ConnPid), + %% We do not receive the push streams. Only the response. + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef), + {ok, _} = gun:await_body(ConnPid, StreamRef), + gun_is_down(ConnPid, ConnRef, shutdown). + +http2_owner_down(Config) -> + doc("HTTP/2: Confirm that the Gun process shuts down when the owner exits normally."), + do_http_owner_down(Config, http2, normal, normal). + +http2_owner_down_error(Config) -> + doc("HTTP/2: Confirm that the Gun process shuts down when the owner exits with an error."), + do_http_owner_down(Config, http2, unexpected, {shutdown, {owner_down, unexpected}}). + +http2_server_goaway_no_streams(_) -> + doc("HTTP/2: Confirm that the Gun process shuts down gracefully " + "when receiving a GOAWAY frame with no active streams and " + "retry is disabled."), + {ok, OriginPid, Port} = init_origin(tcp, http2, fun(_, _, Socket, Transport) -> + receive go_away -> ok end, + Transport:send(Socket, cow_http2:goaway(0, no_error, <<>>)), + timer:sleep(500) + end), + Protocol = http2, + {ok, ConnPid} = gun:open("localhost", Port, #{ + protocols => [Protocol], + retry => 0 + }), + {ok, Protocol} = gun:await_up(ConnPid), + ConnRef = monitor(process, ConnPid), + OriginPid ! go_away, + gun_is_down(ConnPid, ConnRef, normal). + +http2_server_goaway_one_stream(_) -> + doc("HTTP/2: Confirm that the Gun process shuts down gracefully " + "when receiving a GOAWAY frame with one active stream and " + "retry is disabled."), + {ok, OriginPid, OriginPort} = init_origin(tcp, http2, fun(_, _, Socket, Transport) -> + %% Receive a HEADERS frame. + {ok, <>} = Transport:recv(Socket, 9, 1000), + %% Skip the header. + {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), + %% Send a GOAWAY frame. + Transport:send(Socket, cow_http2:goaway(1, no_error, <<>>)), + %% Wait before sending the response back and closing the connection. + timer:sleep(500), + %% Send a HEADERS frame. + {HeadersBlock, _} = cow_hpack:encode([ + {<<":status">>, <<"200">>} + ]), + ok = Transport:send(Socket, [ + cow_http2:headers(1, fin, HeadersBlock) + ]), + timer:sleep(500) + end), + Protocol = http2, + {ok, ConnPid} = gun:open("localhost", OriginPort, #{ + protocols => [Protocol], + retry => 0 + }), + {ok, Protocol} = gun:await_up(ConnPid), + handshake_completed = receive_from(OriginPid), + StreamRef = gun:get(ConnPid, "/"), + ConnRef = monitor(process, ConnPid), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef), + gun_is_down(ConnPid, ConnRef, normal). + +http2_server_goaway_many_streams(_) -> + doc("HTTP/2: Confirm that the Gun process shuts down gracefully " + "when receiving a GOAWAY frame with many active streams and " + "retry is disabled."), + {ok, OriginPid, OriginPort} = init_origin(tcp, http2, fun(_, _, Socket, Transport) -> + %% Stream 1. + %% Receive a HEADERS frame. + {ok, <>} = Transport:recv(Socket, 9, 1000), + %% Skip the header. + {ok, _} = gen_tcp:recv(Socket, SkipLen1, 1000), + %% Stream 2. + %% Receive a HEADERS frame. + {ok, <>} = Transport:recv(Socket, 9, 1000), + %% Skip the header. + {ok, _} = gen_tcp:recv(Socket, SkipLen2, 1000), + %% Stream 3. + %% Receive a HEADERS frame. + {ok, <>} = Transport:recv(Socket, 9, 1000), + %% Skip the header. + {ok, _} = gen_tcp:recv(Socket, SkipLen3, 1000), + %% Stream 4. + %% Receive a HEADERS frame, but simulate that it is still + %% in-flight when the GOAWAY frame is sent. + {ok, <>} = Transport:recv(Socket, 9, 1000), + %% Skip the header. + {ok, _} = gen_tcp:recv(Socket, SkipLen4, 1000), + %% Send a GOAWAY frame. Simulate that GOAWAY was sent before + %% receiving stream 4 by including last stream ID of stream 3. + Transport:send(Socket, cow_http2:goaway(5, no_error, <<>>)), + %% Gun replies with GOAWAY. + {ok, <>} = Transport:recv(Socket, 9, 1000), + {ok, _SkippedPayload} = gen_tcp:recv(Socket, SkipLen5, 1000), + timer:sleep(500), + %% Send replies for streams 1-3. + {HeadersBlock1, State0} = cow_hpack:encode([ + {<<":status">>, <<"200">>} + ]), + ok = Transport:send(Socket, [ + cow_http2:headers(1, fin, HeadersBlock1) + ]), + {HeadersBlock2, State} = cow_hpack:encode([ + {<<":status">>, <<"200">>} + ], State0), + ok = Transport:send(Socket, [ + cow_http2:headers(3, fin, HeadersBlock2) + ]), + {HeadersBlock3, _} = cow_hpack:encode([ + {<<":status">>, <<"200">>} + ], State), + ok = Transport:send(Socket, [ + cow_http2:headers(5, fin, HeadersBlock3) + ]), + %% Gun closes the connection. + {error, closed} = gen_tcp:recv(Socket, 9) + end), + Protocol = http2, + {ok, ConnPid} = gun:open("localhost", OriginPort, #{ + protocols => [Protocol], + retry => 0 + }), + {ok, Protocol} = gun:await_up(ConnPid), + handshake_completed = receive_from(OriginPid), + StreamRef1 = gun:get(ConnPid, "/"), + StreamRef2 = gun:get(ConnPid, "/"), + StreamRef3 = gun:get(ConnPid, "/"), + StreamRef4 = gun:get(ConnPid, "/"), + ConnRef = monitor(process, ConnPid), + %% GOAWAY received. Stream 4 is cancelled. + {gun_error, ConnPid, StreamRef4, Reason4} = receive E4 -> E4 end, + {goaway, no_error, _} = Reason4, + StreamRef5 = gun:get(ConnPid, "/"), + {gun_error, ConnPid, StreamRef5, closing} = receive E5 -> E5 end, + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef2), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef3), + gun_is_down(ConnPid, ConnRef, normal). + +ws_gun_shutdown(Config) -> + doc("Websocket: Confirm that the Gun process shuts down gracefully " + "when calling gun:shutdown/1."), + {ok, ConnPid} = gun:open("localhost", config(origin_port, Config)), + {ok, http} = gun:await_up(ConnPid), + ConnRef = monitor(process, ConnPid), + StreamRef = gun:ws_upgrade(ConnPid, "/ws", []), + {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), + gun:shutdown(ConnPid), + gun_is_down(ConnPid, ConnRef, shutdown). + +ws_gun_shutdown_timeout(Config) -> + doc("Websocket: Confirm that the Gun process shuts down when " + "the closing_timeout triggers after calling gun:shutdown/1."), + {ok, ConnPid} = gun:open("localhost", config(origin_port, Config), #{ + ws_opts => #{closing_timeout => 100} + }), + {ok, http} = gun:await_up(ConnPid), + ConnRef = monitor(process, ConnPid), + StreamRef = gun:ws_upgrade(ConnPid, "/ws_frozen_long", []), + {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), + gun:shutdown(ConnPid), + gun_is_down(ConnPid, ConnRef, shutdown). + +ws_owner_down(Config) -> + doc("Websocket: Confirm that the Gun process shuts down when the owner exits normally."), + do_ws_owner_down(Config, normal, normal). + +ws_owner_down_error(Config) -> + doc("Websocket: Confirm that the Gun process shuts down when the owner exits with an error."), + do_ws_owner_down(Config, unexpected, {shutdown, {owner_down, unexpected}}). + +do_ws_owner_down(Config, ExitReason, DownReason) -> + Self = self(), + spawn(fun() -> + {ok, ConnPid} = gun:open("localhost", config(origin_port, Config)), + Self ! {conn, ConnPid}, + {ok, http} = gun:await_up(ConnPid), + StreamRef = gun:ws_upgrade(ConnPid, "/ws", []), + {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), + timer:sleep(500), + exit(ExitReason) + end), + ConnPid = receive {conn, C} -> C end, + ConnRef = monitor(process, ConnPid), + gun_is_down(ConnPid, ConnRef, DownReason). + +ws_gun_send_close_frame(Config) -> + doc("Websocket: Confirm that the Gun process shuts down gracefully " + "when sending a close frame, with retry disabled."), + {ok, ConnPid} = gun:open("localhost", config(origin_port, Config), #{ + retry => 0 + }), + {ok, http} = gun:await_up(ConnPid), + ConnRef = monitor(process, ConnPid), + StreamRef = gun:ws_upgrade(ConnPid, "/ws", []), + {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), + %% We send a close frame. We expect the same frame back + %% before the connection is closed. + Frame = {close, 3333, <<>>}, + gun:ws_send(ConnPid, StreamRef, Frame), + {ws, Frame} = gun:await(ConnPid, StreamRef), + ws_is_down(ConnPid, StreamRef, normal), + gun_is_down(ConnPid, ConnRef, normal). + +ws_gun_receive_close_frame(Config) -> + doc("Websocket: Confirm that the Gun process shuts down gracefully " + "when receiving a close frame, with retry disabled."), + {ok, ConnPid} = gun:open("localhost", config(origin_port, Config), #{ + retry => 0 + }), + {ok, http} = gun:await_up(ConnPid), + ConnRef = monitor(process, ConnPid), + StreamRef = gun:ws_upgrade(ConnPid, "/ws_timeout_close", []), + {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), + %% We expect a close frame before the connection is closed. + {ws, {close, 3333, <<>>}} = gun:await(ConnPid, StreamRef), + ws_is_down(ConnPid, StreamRef, normal), + gun_is_down(ConnPid, ConnRef, normal). + +closing_gun_shutdown(Config) -> + doc("Confirm that the Gun process shuts down gracefully " + "when calling gun:shutdown/1 while Gun is closing a connection."), + {ok, ConnPid} = gun:open("localhost", config(origin_port, Config)), + {ok, http} = gun:await_up(ConnPid), + ConnRef = monitor(process, ConnPid), + StreamRef = gun:ws_upgrade(ConnPid, "/ws_frozen", []), + {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), + %% We send a close frame then immediately call gun:shutdown/1. + %% We expect Gun to go down without retrying to reconnect. + Frame = {close, 3333, <<>>}, + gun:ws_send(ConnPid, StreamRef, Frame), + gun:shutdown(ConnPid), + {ws, Frame} = gun:await(ConnPid, StreamRef), + gun_is_down(ConnPid, ConnRef, shutdown). + +closing_owner_down(Config) -> + doc("Confirm that the Gun process shuts down gracefully " + "when the owner exits normally while Gun is closing a connection."), + do_closing_owner_down(Config, normal, normal). + +closing_owner_down_error(Config) -> + doc("Confirm that the Gun process shuts down gracefully " + "when the owner exits with an error while Gun is closing a connection."), + do_closing_owner_down(Config, unexpected, {shutdown, {owner_down, unexpected}}). + +do_closing_owner_down(Config, ExitReason, DownReason) -> + Self = self(), + spawn(fun() -> + {ok, ConnPid} = gun:open("localhost", config(origin_port, Config)), + Self ! {conn, ConnPid}, + {ok, http} = gun:await_up(ConnPid), + StreamRef = gun:ws_upgrade(ConnPid, "/ws_frozen", []), + {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), + gun:ws_send(ConnPid, StreamRef, {close, 3333, <<>>}), + timer:sleep(100), + exit(ExitReason) + end), + ConnPid = receive {conn, C} -> C end, + ConnRef = monitor(process, ConnPid), + gun_is_down(ConnPid, ConnRef, DownReason). + +%% Internal. + +gun_is_down(ConnPid, ConnRef, Expected) -> + receive + {'DOWN', ConnRef, process, ConnPid, Reason} -> + Expected = Reason, + ok + end. + +ws_is_down(ConnPid, StreamRef, Expected) -> + receive + {gun_down, ConnPid, ws, Reason, StreamsDown} -> + Expected = Reason, + [StreamRef] = StreamsDown, + ok + end. diff --git a/gun/test/socks_SUITE.erl b/gun/test/socks_SUITE.erl new file mode 100644 index 0000000..19d15ca --- /dev/null +++ b/gun/test/socks_SUITE.erl @@ -0,0 +1,521 @@ +%% Copyright (c) 2019-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +%% This test suite covers the following RFCs and specifications: +%% +%% * RFC 1928 +%% * RFC 1929 +%% * http://ftp.icm.edu.pl/packages/socks/socks4/SOCKS4.protocol +%% * https://www.openssh.com/txt/socks4a.protocol + +-module(socks_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-import(ct_helper, [doc/1]). +-import(gun_test, [init_origin/1]). +-import(gun_test, [init_origin/2]). +-import(gun_test, [receive_from/1]). +-import(gun_test, [receive_from/2]). + +all() -> + ct_helper:all(?MODULE). + +%% Proxy helpers. + +do_proxy_start(Transport0, Auth) -> + Transport = case Transport0 of + tcp -> gun_tcp; + tls -> gun_tls + end, + Self = self(), + Pid = spawn_link(fun() -> do_proxy_init(Self, Transport, Auth) end), + Port = receive_from(Pid), + {ok, Pid, Port}. + +do_proxy_init(Parent, Transport, Auth) -> + {ok, ListenSocket} = case Transport of + gun_tcp -> + gen_tcp:listen(0, [binary, {active, false}]); + gun_tls -> + Opts = ct_helper:get_certs_from_ets(), + ssl:listen(0, [binary, {active, false}, {verify, verify_none}, + {fail_if_no_peer_cert, false}|Opts]) + end, + {ok, {_, Port}} = Transport:sockname(ListenSocket), + Parent ! {self(), Port}, + {ok, ClientSocket} = case Transport of + gun_tcp -> + gen_tcp:accept(ListenSocket, 5000); + gun_tls -> + {ok, ClientSocket0} = ssl:transport_accept(ListenSocket, 5000), + {ok, ClientSocket1} = ssl:handshake(ClientSocket0, 5000), + {ok, ClientSocket1} + end, + Recv = case Transport of + gun_tcp -> fun gen_tcp:recv/3; + gun_tls -> fun ssl:recv/3 + end, + %% Authentication method. + {ok, <<5, NumAuths, Auths0/bits>>} = Recv(ClientSocket, 0, 1000), + Auths = [case A of + 0 -> none; + 2 -> username_password + end || <> <= Auths0], + Parent ! {self(), {auth_methods, NumAuths, Auths}}, + AuthMethod = do_auth_method(Auth), + ok = case {AuthMethod, lists:member(AuthMethod, Auths)} of + {none, true} -> + Transport:send(ClientSocket, <<5, 0>>); + {username_password, true} -> + Transport:send(ClientSocket, <<5, 2>>), + {ok, <<1, ULen, User:ULen/binary, PLen, Pass:PLen/binary>>} = Recv(ClientSocket, 0, 1000), + Parent ! {self(), {username_password, User, Pass}}, + %% @todo Test errors too (byte 2). + Transport:send(ClientSocket, <<1, 0>>); + {_, false} -> + %% @todo + not_ok + end, + %% Connection request. + {ok, <<5, 1, 0, AType, Rest/bits>>} = Recv(ClientSocket, 0, 1000), + {OriginHost, OriginPort} = case AType of + 1 -> + <> = Rest, + {{A, B, C, D}, P}; + 3 -> + <> = Rest, + {H, P}; + 4 -> + <> = Rest, + {{A, B, C, D, E, F, G, H}, P} + end, + Parent ! {self(), {connect, OriginHost, OriginPort}}, + %% @todo Test errors too (byte 2). + %% @todo Configurable bound address. + Transport:send(ClientSocket, <<5, 0, 0, 1, 1, 2, 3, 4, 33333:16>>), + if + true -> + {ok, OriginSocket} = gen_tcp:connect( + binary_to_list(OriginHost), OriginPort, + [binary, {active, false}]), + Transport:setopts(ClientSocket, [{active, true}]), + inet:setopts(OriginSocket, [{active, true}]), + do_proxy_loop(Transport, ClientSocket, OriginSocket) + end. + +do_proxy_loop(Transport, ClientSocket, OriginSocket) -> + {OK, _, _} = Transport:messages(), + receive + {OK, ClientSocket, Data} -> + case gen_tcp:send(OriginSocket, Data) of + ok -> + do_proxy_loop(Transport, ClientSocket, OriginSocket); + {error, _} -> + ok + end; + {tcp, OriginSocket, Data} -> + case Transport:send(ClientSocket, Data) of + ok -> + do_proxy_loop(Transport, ClientSocket, OriginSocket); + {error, _} -> + ok + end; + {tcp_closed, _} -> + ok; + {ssl_closed, _} -> + ok; + Msg -> + error(Msg) + end. + +do_auth_method(none) -> none; +do_auth_method({username_password, _, _}) -> username_password. + +%% Tests. + +socks5_tcp_http_none(_) -> + doc("Use Socks5 over TCP and without authentication to connect to an HTTP server."), + do_socks5(<<"http">>, tcp, http, tcp, none). + +socks5_tcp_http_username_password(_) -> + doc("Use Socks5 over TCP and without authentication to connect to an HTTP server."), + do_socks5(<<"http">>, tcp, http, tcp, {username_password, <<"user">>, <<"password">>}). + +socks5_tcp_https_none(_) -> + doc("Use Socks5 over TCP and without authentication to connect to an HTTPS server."), + do_socks5(<<"https">>, tls, http, tcp, none). + +socks5_tcp_https_username_password(_) -> + doc("Use Socks5 over TCP and without authentication to connect to an HTTPS server."), + do_socks5(<<"https">>, tls, http, tcp, {username_password, <<"user">>, <<"password">>}). + +socks5_tls_http_none(_) -> + doc("Use Socks5 over TLS and without authentication to connect to an HTTP server."), + do_socks5(<<"http">>, tcp, http, tls, none). + +socks5_tls_http_username_password(_) -> + doc("Use Socks5 over TLS and without authentication to connect to an HTTP server."), + do_socks5(<<"http">>, tcp, http, tls, {username_password, <<"user">>, <<"password">>}). + +socks5_tls_https_none(_) -> + doc("Use Socks5 over TLS and without authentication to connect to an HTTPS server."), + do_socks5(<<"https">>, tls, http, tls, none). + +socks5_tls_https_username_password(_) -> + doc("Use Socks5 over TLS and without authentication to connect to an HTTPS server."), + do_socks5(<<"https">>, tls, http, tls, {username_password, <<"user">>, <<"password">>}). + +socks5_tcp_h2c_none(_) -> + doc("Use Socks5 over TCP and without authentication to connect to an HTTP/2 server over TCP."), + do_socks5(<<"http">>, tcp, http2, tcp, none). + +socks5_tcp_h2c_username_password(_) -> + doc("Use Socks5 over TCP and without authentication to connect to an HTTP/2 server over TCP."), + do_socks5(<<"http">>, tcp, http2, tcp, {username_password, <<"user">>, <<"password">>}). + +socks5_tcp_h2_none(_) -> + doc("Use Socks5 over TCP and without authentication to connect to an HTTP/2 server over TLS."), + do_socks5(<<"https">>, tls, http2, tcp, none). + +socks5_tcp_h2_username_password(_) -> + doc("Use Socks5 over TCP and without authentication to connect to an HTTP/2 server over TLS."), + do_socks5(<<"https">>, tls, http2, tcp, {username_password, <<"user">>, <<"password">>}). + +socks5_tls_h2c_none(_) -> + doc("Use Socks5 over TLS and without authentication to connect to an HTTP/2 server over TCP."), + do_socks5(<<"http">>, tcp, http2, tls, none). + +socks5_tls_h2c_username_password(_) -> + doc("Use Socks5 over TLS and without authentication to connect to an HTTP/2 server over TCP."), + do_socks5(<<"http">>, tcp, http2, tls, {username_password, <<"user">>, <<"password">>}). + +socks5_tls_h2_none(_) -> + doc("Use Socks5 over TLS and without authentication to connect to an HTTP/2 server over TLS."), + do_socks5(<<"https">>, tls, http2, tls, none). + +socks5_tls_h2_username_password(_) -> + doc("Use Socks5 over TLS and without authentication to connect to an HTTP/2 server over TLS."), + do_socks5(<<"https">>, tls, http2, tls, {username_password, <<"user">>, <<"password">>}). + +do_socks5(OriginScheme, OriginTransport, OriginProtocol, ProxyTransport, SocksAuth) -> + {ok, OriginPid, OriginPort} = init_origin(OriginTransport, OriginProtocol), + {ok, ProxyPid, ProxyPort} = do_proxy_start(ProxyTransport, SocksAuth), + Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), + {ok, ConnPid} = gun:open("localhost", ProxyPort, #{ + transport => ProxyTransport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [{socks, #{ + auth => [SocksAuth], + host => "localhost", + port => OriginPort, + transport => OriginTransport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [OriginProtocol] + }}] + }), + %% We receive a gun_up and a gun_tunnel_up. + {ok, socks} = gun:await_up(ConnPid), + {up, OriginProtocol} = gun:await(ConnPid, undefined), + %% The proxy received two packets. + AuthMethod = do_auth_method(SocksAuth), + {auth_methods, 1, [AuthMethod]} = receive_from(ProxyPid), + _ = case AuthMethod of + none -> ok; + username_password -> SocksAuth = receive_from(ProxyPid) + end, + {connect, <<"localhost">>, OriginPort} = receive_from(ProxyPid), + handshake_completed = receive_from(OriginPid), + _ = gun:get(ConnPid, "/proxied"), + _ = case OriginProtocol of + http -> + Data = receive_from(OriginPid), + Lines = binary:split(Data, <<"\r\n">>, [global]), + [<<"host: ", Authority/bits>>] = [L || <<"host: ", _/bits>> = L <- Lines]; + http2 -> + <<_:24, 1:8, _/bits>> = receive_from(OriginPid) + end, + #{ + transport := OriginTransport, + protocol := OriginProtocol, + origin_scheme := OriginScheme, + origin_host := "localhost", + origin_port := OriginPort, + intermediaries := [#{ + type := socks5, + host := "localhost", + port := ProxyPort, + transport := ProxyTransport, + protocol := socks + }]} = gun:info(ConnPid), + gun:close(ConnPid). + +socks5_tcp_through_multiple_tcp_proxies(_) -> + doc("Gun can be used to establish a TCP connection " + "to an HTTP/1.1 server via a tunnel going through " + "two separate TCP Socks5 proxies."), + do_socks5_through_multiple_proxies(<<"http">>, tcp, tcp). + +socks5_tcp_through_multiple_tls_proxies(_) -> + doc("Gun can be used to establish a TCP connection " + "to an HTTP/1.1 server via a tunnel going through " + "two separate TLS Socks5 proxies."), + do_socks5_through_multiple_proxies(<<"http">>, tcp, tls). + +socks5_tls_through_multiple_tcp_proxies(_) -> + doc("Gun can be used to establish a TLS connection " + "to an HTTP/1.1 server via a tunnel going through " + "two separate TCP Socks5 proxies."), + do_socks5_through_multiple_proxies(<<"https">>, tls, tcp). + +socks5_tls_through_multiple_tls_proxies(_) -> + doc("Gun can be used to establish a TLS connection " + "to an HTTP/1.1 server via a tunnel going through " + "two separate TLS Socks5 proxies."), + do_socks5_through_multiple_proxies(<<"https">>, tls, tls). + +do_socks5_through_multiple_proxies(OriginScheme, OriginTransport, ProxyTransport) -> + {ok, OriginPid, OriginPort} = init_origin(OriginTransport, http), + {ok, Proxy1Pid, Proxy1Port} = do_proxy_start(ProxyTransport, none), + {ok, Proxy2Pid, Proxy2Port} = do_proxy_start(ProxyTransport, none), + Authority = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), + {ok, ConnPid} = gun:open("localhost", Proxy1Port, #{ + transport => ProxyTransport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [{socks, #{ + host => "localhost", + port => Proxy2Port, + transport => ProxyTransport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [{socks, #{ + host => "localhost", + port => OriginPort, + transport => OriginTransport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}] + }}] + }}] + }), + %% We receive a gun_up and two gun_tunnel_up. + {ok, socks} = gun:await_up(ConnPid), + {up, socks} = gun:await(ConnPid, undefined), + {up, http} = gun:await(ConnPid, undefined), + %% The first proxy received two packets. + {auth_methods, 1, [none]} = receive_from(Proxy1Pid), + {connect, <<"localhost">>, Proxy2Port} = receive_from(Proxy1Pid), + %% So did the second proxy. + {auth_methods, 1, [none]} = receive_from(Proxy2Pid), + {connect, <<"localhost">>, OriginPort} = receive_from(Proxy2Pid), + handshake_completed = receive_from(OriginPid), + _ = gun:get(ConnPid, "/proxied"), + Data = receive_from(OriginPid), + Lines = binary:split(Data, <<"\r\n">>, [global]), + [<<"host: ", Authority/bits>>] = [L || <<"host: ", _/bits>> = L <- Lines], + #{ + transport := OriginTransport, + protocol := http, + origin_scheme := OriginScheme, + origin_host := "localhost", + origin_port := OriginPort, + intermediaries := [#{ + type := socks5, + host := "localhost", + port := Proxy1Port, + transport := ProxyTransport, + protocol := socks + }, #{ + type := socks5, + host := "localhost", + port := Proxy2Port, + transport := ProxyTransport, + protocol := socks + }]} = gun:info(ConnPid), + gun:close(ConnPid). + +socks5_tcp_through_connect_tcp_to_tcp_origin(_) -> + doc("CONNECT can be used to establish a TCP connection " + "to an HTTP/1.1 server via a tunnel going through " + "an HTTP proxy followed by a Socks5 proxy."), + do_socks5_through_connect_proxy(<<"http">>, tcp, tcp). + +socks5_tls_through_connect_tls_to_tcp_origin(_) -> + doc("CONNECT can be used to establish a TCP connection " + "to an HTTP/1.1 server via a tunnel going through " + "an HTTPS proxy followed by a TLS Socks5 proxy."), + do_socks5_through_connect_proxy(<<"http">>, tcp, tls). + +socks5_tcp_through_connect_tcp_to_tls_origin(_) -> + doc("CONNECT can be used to establish a TCP connection " + "to an HTTP/1.1 server via a tunnel going through " + "an HTTP proxy followed by a Socks5 proxy."), + do_socks5_through_connect_proxy(<<"https">>, tls, tcp). + +socks5_tls_through_connect_tls_to_tls_origin(_) -> + doc("CONNECT can be used to establish a TCP connection " + "to an HTTP/1.1 server via a tunnel going through " + "an HTTPS proxy followed by a TLS Socks5 proxy."), + do_socks5_through_connect_proxy(<<"https">>, tls, tls). + +do_socks5_through_connect_proxy(OriginScheme, OriginTransport, ProxyTransport) -> + {ok, OriginPid, OriginPort} = init_origin(OriginTransport, http), + {ok, Proxy1Pid, Proxy1Port} = rfc7231_SUITE:do_proxy_start(ProxyTransport), + {ok, Proxy2Pid, Proxy2Port} = do_proxy_start(ProxyTransport, none), + {ok, ConnPid} = gun:open("localhost", Proxy1Port, #{ + transport => ProxyTransport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}] + }), + %% We receive a gun_up first. This is the HTTP proxy. + {ok, http} = gun:await_up(ConnPid), + Authority1 = iolist_to_binary(["localhost:", integer_to_binary(Proxy2Port)]), + StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => Proxy2Port, + transport => ProxyTransport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [{socks, #{ + host => "localhost", + port => OriginPort, + transport => OriginTransport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}] + }}] + }), + {request, <<"CONNECT">>, Authority1, 'HTTP/1.1', _} = receive_from(Proxy1Pid), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef), + %% We receive two gun_tunnel_up messages. First the SOCKS server and then the origin HTTP server. + {up, socks} = gun:await(ConnPid, StreamRef), + {up, http} = gun:await(ConnPid, StreamRef), + %% The second proxy receives a Socks5 auth/connect request. + {auth_methods, 1, [none]} = receive_from(Proxy2Pid), + {connect, <<"localhost">>, OriginPort} = receive_from(Proxy2Pid), + handshake_completed = receive_from(OriginPid), + Authority2 = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), + _ = gun:get(ConnPid, "/proxied", [], #{tunnel => StreamRef}), + Data = receive_from(OriginPid), + Lines = binary:split(Data, <<"\r\n">>, [global]), + [<<"host: ", Authority2/bits>>] = [L || <<"host: ", _/bits>> = L <- Lines], + #{ + transport := OriginTransport, + protocol := http, + origin_scheme := OriginScheme, + origin_host := "localhost", + origin_port := OriginPort, + intermediaries := [#{ + type := connect, + host := "localhost", + port := Proxy1Port, + transport := ProxyTransport, + protocol := http + }, #{ + type := socks5, + host := "localhost", + port := Proxy2Port, + transport := ProxyTransport, + protocol := socks + }]} = gun:info(ConnPid), + gun:close(ConnPid). + +socks5_tcp_through_h2_connect_tcp_to_tcp_origin(_) -> + doc("CONNECT can be used to establish a TCP connection " + "to an HTTP/1.1 server via a tunnel going through " + "a TCP HTTP/2 proxy followed by a Socks5 proxy."), + do_socks5_through_h2_connect_proxy(<<"http">>, tcp, <<"http">>, tcp). + +do_socks5_through_h2_connect_proxy(_OriginScheme, OriginTransport, ProxyScheme, ProxyTransport) -> + {ok, OriginPid, OriginPort} = init_origin(OriginTransport, http), + {ok, Proxy1Pid, Proxy1Port} = rfc7540_SUITE:do_proxy_start(ProxyTransport, [ + {proxy_stream, 1, 200, [], 0, undefined} + ]), + {ok, Proxy2Pid, Proxy2Port} = do_proxy_start(ProxyTransport, none), + {ok, ConnPid} = gun:open("localhost", Proxy1Port, #{ + transport => ProxyTransport, + protocols => [http2] + }), + %% We receive a gun_up first. This is the HTTP proxy. + {ok, http2} = gun:await_up(ConnPid), + handshake_completed = receive_from(Proxy1Pid), + Authority1 = iolist_to_binary(["localhost:", integer_to_binary(Proxy2Port)]), + StreamRef = gun:connect(ConnPid, #{ + host => "localhost", + port => Proxy2Port, + transport => ProxyTransport, + protocols => [{socks, #{ + host => "localhost", + port => OriginPort, + transport => OriginTransport + }}] + }), + {request, #{ + <<":method">> := <<"CONNECT">>, + <<":authority">> := Authority1 + }} = receive_from(Proxy1Pid), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef), + %% First the HTTP/2 tunnel is up, then the SOCKS tunnel to the origin HTTP server. + {up, socks} = gun:await(ConnPid, StreamRef), + {up, http} = gun:await(ConnPid, StreamRef), + %% The second proxy receives a Socks5 auth/connect request. + {auth_methods, 1, [none]} = receive_from(Proxy2Pid), + {connect, <<"localhost">>, OriginPort} = receive_from(Proxy2Pid), + handshake_completed = receive_from(OriginPid), + ProxiedStreamRef = gun:get(ConnPid, "/proxied", #{}, #{tunnel => StreamRef}), + Authority2 = iolist_to_binary(["localhost:", integer_to_binary(OriginPort)]), + Data = receive_from(OriginPid), + Lines = binary:split(Data, <<"\r\n">>, [global]), + [<<"host: ", Authority2/bits>>] = [L || <<"host: ", _/bits>> = L <- Lines], + #{ + transport := ProxyTransport, + protocol := http2, + origin_scheme := ProxyScheme, + origin_host := "localhost", + origin_port := Proxy1Port, + intermediaries := [] %% Intermediaries are specific to the CONNECT stream. + } = gun:info(ConnPid), + {ok, #{ + ref := StreamRef, + reply_to := Self, + state := running, + tunnel := #{ + transport := ProxyTransport, + protocol := socks, + %% @todo They're not necessarily the origin. Should be named scheme/host/port. + origin_scheme := ProxyScheme, + origin_host := "localhost", + origin_port := Proxy2Port + } + }} = gun:stream_info(ConnPid, StreamRef), + {ok, #{ + ref := ProxiedStreamRef, + reply_to := Self, + state := running, +%% @todo Add "authority" when the stream is not a tunnel. +% authority := #{ +% scheme := OriginScheme +% transport := +% protocol := +% host := +% port := +% }, + intermediaries := [#{ + type := connect, + host := "localhost", + port := Proxy1Port, + transport := ProxyTransport, + protocol := http2 + }, #{ + type := socks5, + host := "localhost", + port := Proxy2Port, + transport := ProxyTransport, + protocol := socks + }] + }} = gun:stream_info(ConnPid, ProxiedStreamRef), + gun:close(ConnPid). diff --git a/gun/test/sse_SUITE.erl b/gun/test/sse_SUITE.erl new file mode 100644 index 0000000..e1fe840 --- /dev/null +++ b/gun/test/sse_SUITE.erl @@ -0,0 +1,161 @@ +%% Copyright (c) 2017-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(sse_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-import(ct_helper, [config/2]). + +all() -> + [http_clock, http2_clock, lone_id, with_mime_param, http_clock_close]. + +init_per_suite(Config) -> + gun_test:init_cowboy_tls(?MODULE, #{ + env => #{dispatch => cowboy_router:compile(init_routes())} + }, Config). + +end_per_suite(Config) -> + cowboy:stop_listener(config(ref, Config)). + +init_routes() -> [ + {"localhost", [ + {"/clock", sse_clock_h, date}, + {"/lone_id", sse_lone_id_h, []}, + {"/with_mime_param", sse_mime_param_h, []}, + {"/connection_close", sse_clock_close_h, []} + ]} +]. + +http_clock(Config) -> + {ok, Pid} = gun:open("localhost", config(port, Config), #{ + transport => tls, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [http], + http_opts => #{content_handlers => [gun_sse_h, gun_data_h]} + }), + {ok, http} = gun:await_up(Pid), + do_clock_common(Pid, "/clock"). + +http2_clock(Config) -> + {ok, Pid} = gun:open("localhost", config(port, Config), #{ + transport => tls, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [http2], + http2_opts => #{content_handlers => [gun_sse_h, gun_data_h]} + }), + {ok, http2} = gun:await_up(Pid), + do_clock_common(Pid, "/clock"). + +http_clock_close(Config) -> + {ok, Pid} = gun:open("localhost", config(port, Config), #{ + transport => tls, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [http], + http_opts => #{ + content_handlers => [gun_sse_h, gun_data_h], + closing_timeout => 1000 + } + }), + {ok, http} = gun:await_up(Pid), + do_clock_common(Pid, "/connection_close"). + +do_clock_common(Pid, Path) -> + Ref = gun:get(Pid, Path, [ + {<<"host">>, <<"localhost">>}, + {<<"accept">>, <<"text/event-stream">>} + ]), + receive + {gun_response, Pid, Ref, nofin, 200, Headers} -> + {_, <<"text/event-stream">>} + = lists:keyfind(<<"content-type">>, 1, Headers), + event_loop(Pid, Ref, 3) + after 5000 -> + error(timeout) + end. + +event_loop(Pid, _, 0) -> + gun:close(Pid); +event_loop(Pid, Ref, N) -> + receive + {gun_sse, Pid, Ref, Event} -> + ct:pal("Event: ~p~n", [Event]), + #{ + last_event_id := <<>>, + event_type := <<"message">>, + data := Data + } = Event, + true = is_list(Data) orelse is_binary(Data), + event_loop(Pid, Ref, N - 1); + Other -> + ct:pal("Other: ~p~n", [Other]) + after 10000 -> + error(timeout) + end. + +lone_id(Config) -> + {ok, Pid} = gun:open("localhost", config(port, Config), #{ + transport => tls, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [http], + http_opts => #{content_handlers => [gun_sse_h, gun_data_h]} + }), + {ok, http} = gun:await_up(Pid), + Ref = gun:get(Pid, "/lone_id", [ + {<<"host">>, <<"localhost">>}, + {<<"accept">>, <<"text/event-stream">>} + ]), + receive + {gun_response, Pid, Ref, nofin, 200, Headers} -> + {_, <<"text/event-stream">>} + = lists:keyfind(<<"content-type">>, 1, Headers), + receive + {gun_sse, Pid, Ref, Event} -> + #{last_event_id := <<"hello">>} = Event, + 1 = maps:size(Event), + gun:close(Pid) + after 10000 -> + error(timeout) + end + after 5000 -> + error(timeout) + end. + +with_mime_param(Config) -> + {ok, Pid} = gun:open("localhost", config(port, Config), #{ + transport => tls, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [http], + http_opts => #{content_handlers => [gun_sse_h, gun_data_h]} + }), + {ok, http} = gun:await_up(Pid), + Ref = gun:get(Pid, "/with_mime_param", [ + {<<"host">>, <<"localhost">>}, + {<<"accept">>, <<"text/event-stream">>} + ]), + receive + {gun_response, Pid, Ref, nofin, 200, Headers} -> + {_, <<"text/event-stream;", _Params/binary>>} + = lists:keyfind(<<"content-type">>, 1, Headers), + receive + {gun_sse, Pid, Ref, Event} -> + #{last_event_id := <<"hello">>} = Event, + 1 = maps:size(Event), + gun:close(Pid) + after 10000 -> + error(timeout) + end + after 5000 -> + error(timeout) + end. diff --git a/gun/test/tunnel_SUITE.erl b/gun/test/tunnel_SUITE.erl new file mode 100644 index 0000000..14fb2ef --- /dev/null +++ b/gun/test/tunnel_SUITE.erl @@ -0,0 +1,1116 @@ +%% Copyright (c) 2020-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(tunnel_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-import(ct_helper, [doc/1]). +-import(gun_test, [receive_from/1]). + +all() -> + ct_helper:all(?MODULE). + +%% Tests. +%% +%% Test names list the endpoint in the order the connection +%% goes through, with proxies first and the origin server last. +%% Each endpoint is identified by one of the following identifiers: +%% +%% Identifier | Protocol | Transport +%% ---------- |----------|-------- +%% http | HTTP/1.1 | TCP +%% https | HTTP/1.1 | TLS +%% h2c | HTTP/2 | TCP +%% h2 | HTTP/2 | TLS +%% socks5 | SOCKS5 | TCP +%% socks5tls | SOCKS5 | TLS +%% raw | Raw | TCP +%% rawtls | Raw | TLS + +http_http_http(_) -> + do_tunnel(?FUNCTION_NAME). + +http_http_https(_) -> + do_tunnel(?FUNCTION_NAME). + +http_http_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +http_http_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +http_http_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +http_http_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +http_https_http(_) -> + do_tunnel(?FUNCTION_NAME). + +http_https_https(_) -> + do_tunnel(?FUNCTION_NAME). + +http_https_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +http_https_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +http_https_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +http_https_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +http_h2c_http(_) -> + do_tunnel(?FUNCTION_NAME). + +http_h2c_https(_) -> + do_tunnel(?FUNCTION_NAME). + +http_h2c_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +http_h2c_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +http_h2c_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +http_h2c_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +http_h2_http(_) -> + do_tunnel(?FUNCTION_NAME). + +http_h2_https(_) -> + do_tunnel(?FUNCTION_NAME). + +http_h2_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +http_h2_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +http_h2_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +http_h2_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +http_socks5_http(_) -> + do_tunnel(?FUNCTION_NAME). + +http_socks5_https(_) -> + do_tunnel(?FUNCTION_NAME). + +http_socks5_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +http_socks5_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +http_socks5_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +http_socks5_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +http_socks5tls_http(_) -> + do_tunnel(?FUNCTION_NAME). + +http_socks5tls_https(_) -> + do_tunnel(?FUNCTION_NAME). + +http_socks5tls_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +http_socks5tls_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +http_socks5tls_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +http_socks5tls_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +https_http_http(_) -> + do_tunnel(?FUNCTION_NAME). + +https_http_https(_) -> + do_tunnel(?FUNCTION_NAME). + +https_http_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +https_http_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +https_http_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +https_http_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +https_https_http(_) -> + do_tunnel(?FUNCTION_NAME). + +https_https_https(_) -> + do_tunnel(?FUNCTION_NAME). + +https_https_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +https_https_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +https_https_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +https_https_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +https_h2c_http(_) -> + do_tunnel(?FUNCTION_NAME). + +https_h2c_https(_) -> + do_tunnel(?FUNCTION_NAME). + +https_h2c_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +https_h2c_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +https_h2c_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +https_h2c_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +https_h2_http(_) -> + do_tunnel(?FUNCTION_NAME). + +https_h2_https(_) -> + do_tunnel(?FUNCTION_NAME). + +https_h2_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +https_h2_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +https_h2_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +https_h2_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +https_socks5_http(_) -> + do_tunnel(?FUNCTION_NAME). + +https_socks5_https(_) -> + do_tunnel(?FUNCTION_NAME). + +https_socks5_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +https_socks5_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +https_socks5_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +https_socks5_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +https_socks5tls_http(_) -> + do_tunnel(?FUNCTION_NAME). + +https_socks5tls_https(_) -> + do_tunnel(?FUNCTION_NAME). + +https_socks5tls_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +https_socks5tls_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +https_socks5tls_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +https_socks5tls_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +h2c_http_http(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_http_https(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_http_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_http_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_http_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_http_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +h2c_https_http(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_https_https(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_https_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_https_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_https_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_https_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +h2c_h2c_http(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_h2c_https(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_h2c_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_h2c_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_h2c_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_h2c_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +h2c_h2_http(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_h2_https(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_h2_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_h2_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_h2_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_h2_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +h2c_socks5_http(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_socks5_https(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_socks5_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_socks5_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_socks5_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_socks5_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +h2c_socks5tls_http(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_socks5tls_https(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_socks5tls_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_socks5tls_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_socks5tls_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +h2c_socks5tls_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +h2_http_http(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_http_https(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_http_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_http_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_http_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_http_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +h2_https_http(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_https_https(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_https_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_https_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_https_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_https_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +h2_h2c_http(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_h2c_https(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_h2c_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_h2c_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_h2c_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_h2c_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +h2_h2_http(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_h2_https(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_h2_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_h2_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_h2_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_h2_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +h2_socks5_http(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_socks5_https(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_socks5_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_socks5_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_socks5_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_socks5_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +h2_socks5tls_http(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_socks5tls_https(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_socks5tls_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_socks5tls_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_socks5tls_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +h2_socks5tls_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +socks5_http_http(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_http_https(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_http_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_http_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_http_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_http_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +socks5_https_http(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_https_https(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_https_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_https_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_https_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_https_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +socks5_h2c_http(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_h2c_https(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_h2c_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_h2c_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_h2c_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_h2c_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +socks5_h2_http(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_h2_https(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_h2_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_h2_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_h2_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_h2_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +socks5_socks5_http(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_socks5_https(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_socks5_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_socks5_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_socks5_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_socks5_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +socks5_socks5tls_http(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_socks5tls_https(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_socks5tls_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_socks5tls_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_socks5tls_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5_socks5tls_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +socks5tls_http_http(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_http_https(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_http_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_http_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_http_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_http_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +socks5tls_https_http(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_https_https(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_https_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_https_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_https_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_https_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +socks5tls_h2c_http(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_h2c_https(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_h2c_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_h2c_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_h2c_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_h2c_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +socks5tls_h2_http(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_h2_https(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_h2_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_h2_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_h2_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_h2_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +socks5tls_socks5_http(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_socks5_https(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_socks5_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_socks5_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_socks5_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_socks5_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% + +socks5tls_socks5tls_http(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_socks5tls_https(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_socks5tls_h2c(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_socks5tls_h2(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_socks5tls_raw(_) -> + do_tunnel(?FUNCTION_NAME). + +socks5tls_socks5tls_rawtls(_) -> + do_tunnel(?FUNCTION_NAME). + +%% Common code for all the test cases. + +-record(st, { + proxy1, + proxy1_pid, + proxy1_port, + + proxy2, + proxy2_pid, + proxy2_port, + + origin, + origin_pid, + origin_port +}). + +do_tunnel(FunctionName) -> + [Proxy1, Proxy2, Origin] = [list_to_atom(Lex) || Lex <- string:lexemes(atom_to_list(FunctionName), "_")], + do_doc(Proxy1, Proxy2, Origin), + {ok, OriginPid, OriginPort} = do_origin_start(Origin), + {ok, Proxy1Pid, Proxy1Port} = do_proxy_start(Proxy1), + {ok, Proxy2Pid, Proxy2Port} = do_proxy_start(Proxy2), + State = #st{ + proxy1=Proxy1, proxy1_pid=Proxy1Pid, proxy1_port=Proxy1Port, + proxy2=Proxy2, proxy2_pid=Proxy2Pid, proxy2_port=Proxy2Port, + origin=Origin, origin_pid=OriginPid, origin_port=OriginPort + }, + ConnPid = do_proxy1(State), + StreamRef1 = do_proxy2(State, ConnPid), + StreamRef2 = do_origin(State, ConnPid, StreamRef1), + StreamRef3 = do_origin_stream(State, ConnPid, StreamRef2), + do_proxy1_stream_info(State, ConnPid, StreamRef1), + do_proxy2_stream_info(State, ConnPid, StreamRef2), + do_origin_stream_info(State, ConnPid, StreamRef3), + do_info(State, ConnPid). + +do_doc(Proxy1, Proxy2, Origin) -> + doc(do_doc(Proxy1, "proxy") ++ " -> " ++ do_doc(Proxy2, "proxy") ++ " -> " ++ do_doc(Origin, "origin")). + +do_doc(Type, Endpoint) -> + {Transport, Protocol} = do_type(Type), + case Protocol of + http -> "HTTP/1.1"; + http2 -> "HTTP/2"; + socks -> "SOCKS5"; + raw -> "Raw" + end + ++ " " ++ Endpoint ++ " over " ++ + case Transport of + tcp -> "TCP"; + tls -> "TLS" + end. + +do_origin_start(Type) when Type =:= raw; Type =:= rawtls -> + {Transport, Protocol} = do_type(Type), + gun_test:init_origin(Transport, Protocol, fun raw_SUITE:do_echo/4); +do_origin_start(Type) -> + {Transport, Protocol} = do_type(Type), + rfc7540_SUITE:do_cowboy_origin(Transport, Protocol). + +do_proxy_start(Type) when Type =:= http; Type =:= https -> + {Transport, _} = do_type(Type), + rfc7231_SUITE:do_proxy_start(Transport); +do_proxy_start(Type) when Type =:= h2; Type =:= h2c -> + {Transport, _} = do_type(Type), + rfc7540_SUITE:do_proxy_start(Transport); +do_proxy_start(Type) when Type =:= socks5; Type =:= socks5tls -> + {Transport, _} = do_type(Type), + socks_SUITE:do_proxy_start(Transport, none). + +do_proxy1(State=#st{proxy1=Type, proxy1_pid=Proxy1Pid, proxy1_port=Port}) -> + {Transport, Protocol} = do_type(Type), + {ok, ConnPid} = gun:open("localhost", Port, #{ + transport => Transport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [case Protocol of + socks -> + {Protocol, do_proxy2_socks_opts(State)}; + _ -> + Protocol + end] + }), + {ok, Protocol} = gun:await_up(ConnPid), + do_handshake_completed(Protocol, Proxy1Pid), + ConnPid. + +do_proxy2_socks_opts(State=#st{proxy2=Type, proxy2_port=Port}) -> + {Transport, Protocol} = do_type(Type), + #{ + host => "localhost", + port => Port, + transport => Transport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [case Protocol of + socks -> + {Protocol, do_origin_socks_opts(State)}; + _ -> + Protocol + end] + }. + +do_origin_socks_opts(#st{origin=Type, origin_port=Port}) -> + {Transport, Protocol} = do_type(Type), + #{ + host => "localhost", + port => Port, + transport => Transport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [Protocol] + }. + +%% When the first proxy was socks all we need to do is wait for +%% the second proxy to be up. +do_proxy2(#st{proxy1=Proxy1Type, proxy2=Proxy2Type, proxy2_pid=Proxy2Pid}, ConnPid) + when Proxy1Type =:= socks5; Proxy1Type =:= socks5tls -> + {_, Protocol} = do_type(Proxy2Type), + {up, Protocol} = gun:await(ConnPid, undefined), + do_handshake_completed(Protocol, Proxy2Pid), + undefined; +do_proxy2(State=#st{proxy2=Type, proxy2_pid=Proxy2Pid, proxy2_port=Port}, ConnPid) -> + {Transport, Protocol} = do_type(Type), + StreamRef1 = gun:connect(ConnPid, #{ + host => "localhost", + port => Port, + transport => Transport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [case Protocol of + socks -> + {Protocol, do_origin_socks_opts(State)}; + _ -> + Protocol + end] + }), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef1), + {up, Protocol} = gun:await(ConnPid, StreamRef1), + do_handshake_completed(Protocol, Proxy2Pid), + StreamRef1. + +%% When the second proxy was socks all we need to do is wait for +%% the origin to be up. +do_origin(#st{proxy2=Proxy2Type, origin=OriginType}, ConnPid, StreamRef) + when Proxy2Type =:= socks5; Proxy2Type =:= socks5tls -> + {_, Protocol} = do_type(OriginType), + {up, Protocol} = gun:await(ConnPid, StreamRef), + StreamRef; +%% We can't have a socks5/socks5tls origin. +do_origin(#st{origin=Type, origin_port=Port}, ConnPid, StreamRef1) -> + {Transport, Protocol} = do_type(Type), + StreamRef2 = gun:connect(ConnPid, #{ + host => "localhost", + port => Port, + transport => Transport, + tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}], + protocols => [Protocol] + }, [], #{tunnel => StreamRef1}), + {response, fin, 200, _} = gun:await(ConnPid, StreamRef2), + {up, Protocol} = gun:await(ConnPid, StreamRef2), + StreamRef2. + +do_handshake_completed(http2, ProxyPid) -> + handshake_completed = receive_from(ProxyPid), + ok; +do_handshake_completed(_, _) -> + ok. + +do_origin_stream(#st{origin=Type}, ConnPid, StreamRef2) + when Type =:= raw; Type =:= rawtls -> + gun:data(ConnPid, StreamRef2, nofin, <<"Hello world!">>), + {data, nofin, <<"Hello world!">>} = gun:await(ConnPid, StreamRef2), + StreamRef2; +do_origin_stream(#st{}, ConnPid, StreamRef2) -> + StreamRef3 = gun:get(ConnPid, "/proxied", #{}, #{tunnel => StreamRef2}), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef3), + StreamRef3. + +do_proxy1_stream_info(#st{proxy1=Proxy1}, _, _) + when Proxy1 =:= socks5; Proxy1 =:= socks5tls -> + ok; +do_proxy1_stream_info(#st{proxy1=Proxy1, proxy2=Proxy2, proxy2_port=Proxy2Port}, ConnPid, StreamRef1) -> + ct:log("1: ~p~n~p", [StreamRef1, gun:stream_info(ConnPid, StreamRef1)]), + Self = if + %% We do not currently keep reply_to after switch_protocol. + Proxy1 =:= http; Proxy1 =:= https -> + undefined; + true -> + self() + end, + {Proxy2Transport, Proxy2Protocol} = do_type(Proxy2), + Proxy2Scheme = case Proxy2Transport of + tcp -> <<"http">>; + tls -> <<"https">> + end, + {ok, #{ + ref := StreamRef1, + reply_to := Self, + state := running, + tunnel := #{ + transport := Proxy2Transport, + protocol := Proxy2Protocol, + origin_scheme := Proxy2Scheme, + origin_host := "localhost", + origin_port := Proxy2Port + } + }} = gun:stream_info(ConnPid, StreamRef1), + ok. + +do_proxy2_stream_info(#st{proxy2=Proxy2}, _, _) + when Proxy2 =:= socks5; Proxy2 =:= socks5tls -> + ok; +do_proxy2_stream_info(#st{proxy1=Proxy1, proxy1_port=Proxy1Port, proxy2=Proxy2, + origin=Origin, origin_port=OriginPort}, ConnPid, StreamRef2) -> + ct:log("2: ~p~n~p", [StreamRef2, gun:stream_info(ConnPid, StreamRef2)]), + Self = if + %% We do not currently keep reply_to after switch_protocol. + Proxy1 =/= h2, Proxy1 =/= h2c, (Proxy2 =:= http) orelse (Proxy2 =:= https) -> + undefined; + true -> + self() + end, + {Proxy1Transport, Proxy1Protocol} = do_type(Proxy1), + Proxy1Type = case Proxy1 of + socks5 -> socks5; + socks5tls -> socks5; + _ -> connect + end, + {OriginTransport, OriginProtocol} = do_type(Origin), + OriginScheme = case {OriginTransport, OriginProtocol} of + {_, raw} -> undefined; + {tcp, _} -> <<"http">>; + {tls, _} -> <<"https">> + end, + {ok, #{ + ref := StreamRef2, + reply_to := Self, + state := running, + intermediaries := [#{ + type := Proxy1Type, + host := "localhost", + port := Proxy1Port, + transport := Proxy1Transport, + protocol := Proxy1Protocol + }], + tunnel := #{ + transport := OriginTransport, + protocol := OriginProtocol, + origin_scheme := OriginScheme, + origin_host := "localhost", + origin_port := OriginPort + } + }} = gun:stream_info(ConnPid, StreamRef2), + ok. + +do_origin_stream_info(#st{origin=Type}, _, _) + when Type =:= raw; Type =:= rawtls -> + ok; +do_origin_stream_info(#st{proxy1=Proxy1, proxy1_port=Proxy1Port, + proxy2=Proxy2, proxy2_port=Proxy2Port}, ConnPid, StreamRef3) -> + ct:log("3: ~p~n~p", [StreamRef3, gun:stream_info(ConnPid, StreamRef3)]), + {Proxy1Transport, Proxy1Protocol} = do_type(Proxy1), + Proxy1Type = case Proxy1 of + socks5 -> socks5; + socks5tls -> socks5; + _ -> connect + end, + {Proxy2Transport, Proxy2Protocol} = do_type(Proxy2), + Proxy2Type = case Proxy2 of + socks5 -> socks5; + socks5tls -> socks5; + _ -> connect + end, + {ok, #{ + ref := StreamRef3, + reply_to := _, %% @todo + state := running, + intermediaries := [#{ + type := Proxy1Type, + host := "localhost", + port := Proxy1Port, + transport := Proxy1Transport, + protocol := Proxy1Protocol + }, #{ + type := Proxy2Type, + host := "localhost", + port := Proxy2Port, + transport := Proxy2Transport, + protocol := Proxy2Protocol + }] + }} = gun:stream_info(ConnPid, StreamRef3), + ok. + +do_info(#st{ + proxy1=Proxy1, proxy1_port=Proxy1Port, + proxy2=Proxy2, proxy2_port=Proxy2Port, + origin=Origin, origin_port=OriginPort + }, ConnPid) -> + {Proxy1Transport, Proxy1Protocol} = do_type(Proxy1), + Proxy1Type = case Proxy1Protocol of + socks -> socks5; + _ -> connect + end, + {Proxy2Transport, Proxy2Protocol} = do_type(Proxy2), + Proxy2Type = case Proxy2Protocol of + socks -> socks5; + _ -> connect + end, + Intermediary1 = #{ + type => Proxy1Type, + host => "localhost", + port => Proxy1Port, + transport => Proxy1Transport, + protocol => Proxy1Protocol + }, + Intermediary2 = #{ + type => Proxy2Type, + host => "localhost", + port => Proxy2Port, + transport => Proxy2Transport, + protocol => Proxy2Protocol + }, + %% There are no connection-wide intermediaries for HTTP/2. + Intermediaries = case {Proxy1Protocol, Proxy2Protocol} of + {http2, _} -> []; + {_, http2} -> [Intermediary1]; + _ -> [Intermediary1, Intermediary2] + end, + %% The transport, protocol, scheme and port of the origin + %% will also vary depending on where we started using HTTP/2 CONNECT. + %% In that case the connection-wide origin is the first HTTP/2 endpoint. + {OriginTransport, OriginProtocol} = do_type(Origin), + {InfoTransport, InfoProtocol, InfoPort} = case {Proxy1Protocol, Proxy2Protocol} of + {http2, _} -> {Proxy1Transport, Proxy1Protocol, Proxy1Port}; + {_, http2} -> {Proxy2Transport, Proxy2Protocol, Proxy2Port}; + _ -> {OriginTransport, OriginProtocol, OriginPort} + end, + InfoScheme = case {InfoTransport, InfoProtocol} of + {_, raw} -> undefined; + {tcp, _} -> <<"http">>; + {tls, _} -> <<"https">> + end, + #{ + transport := InfoTransport, + protocol := InfoProtocol, + origin_scheme := InfoScheme, + origin_host := "localhost", + origin_port := InfoPort, + intermediaries := Intermediaries + } = gun:info(ConnPid), + ok. + +do_type(http) -> {tcp, http}; +do_type(https) -> {tls, http}; +do_type(h2c) -> {tcp, http2}; +do_type(h2) -> {tls, http2}; +do_type(socks5) -> {tcp, socks}; +do_type(socks5tls) -> {tls, socks}; +do_type(raw) -> {tcp, raw}; +do_type(rawtls) -> {tls, raw}. diff --git a/gun/test/wpt/cookies/attributes_expires.json b/gun/test/wpt/cookies/attributes_expires.json new file mode 100644 index 0000000..59b8ee3 --- /dev/null +++ b/gun/test/wpt/cookies/attributes_expires.json @@ -0,0 +1 @@ +[{"cookie":"test=1; Expires=Fri, 01 Jan 2038 00:00:00 GMT","expected":"test=1","name":"Set cookie with expires value containing a comma"},{"cookie":"test=2; Expires=Fri 01 Jan 2038 00:00:00 GMT, baz=qux","expected":"test=2","name":"Set cookie with expires value followed by comma"},{"cookie":"test=3; Expires=Fri, 01 Jan 2038 00:00:00 GMT","expected":"test=3","name":"Set cookie with future expiration"},{"cookie":["test=expired; Expires=Fri, 07 Aug 2007 08:04:19 GMT","test=4; Expires=Fri, 07 Aug 2027 08:04:19 GMT"],"expected":"test=4","name":"Set expired cookie along with valid cookie"},{"cookie":"test=5; expires=Thu, 10 Apr 1980 16:33:12 GMT","expected":"","name":"Don't set cookie with expires set to the past"}] diff --git a/gun/test/wpt/cookies/attributes_invalid.json b/gun/test/wpt/cookies/attributes_invalid.json new file mode 100644 index 0000000..48c240f --- /dev/null +++ b/gun/test/wpt/cookies/attributes_invalid.json @@ -0,0 +1 @@ +[{"cookie":"test=1; lol; Path=/","expected":"test=1","name":"Set cookie with invalid attribute","defaultPath":false},{"cookie":"test=2; Path=/; lol","expected":"test=2","name":"Set cookie ending with invalid attribute.","defaultPath":false},{"cookie":"test=3; Path=/; 'lol'","expected":"test=3","name":"Set cookie ending with quoted invalid attribute.","defaultPath":false},{"cookie":"test=4; Path=/; \"lol\"","expected":"test=4","name":"Set cookie ending with double-quoted invalid attribute.","defaultPath":false},{"cookie":"test=5; Path=/; lol=","expected":"test=5","name":"Set cookie ending with invalid attribute equals.","defaultPath":false},{"cookie":"test=6; lol=\"aaa;bbb\"; Path=/","expected":"test=6","name":"Set cookie with two invalid attributes (lol=\"aaa and bbb).","defaultPath":false},{"cookie":"test=7; Path=/; lol=\"aaa;bbb\"","expected":"test=7","name":"Set cookie ending with two invalid attributes (lol=\"aaa and bbb).","defaultPath":false},{"cookie":"test=8; \"Secure\"","expected":"test=8","name":"Set cookie for quoted Secure attribute"},{"cookie":"test=9; Secure qux","expected":"test=9","name":"Set cookie for Secure qux"},{"cookie":"test=10; b,az=qux","expected":"test=10","name":"Ignore invalid attribute name with comma"},{"cookie":"test=11; baz=q,ux","expected":"test=11","name":"Ignore invalid attribute value with comma"},{"cookie":" test = 12 ;foo;;; bar","expected":"test=12","name":"Set cookie ignoring multiple invalid attributes, whitespace, and semicolons"},{"cookie":" test=== 13 ;foo;;; bar","expected":"test=== 13","name":"Set cookie with multiple '='s in its value, ignoring multiple invalid attributes, whitespace, and semicolons"},{"cookie":"test=14; version=1;","expected":"test=14","name":"Set cookie with (invalid) version=1 attribute"},{"cookie":"test=15; version=1000;","expected":"test=15","name":"Set cookie with (invalid) version=1000 attribute"},{"cookie":"test=16; customvalue='1000 or more';","expected":"test=16","name":"Set cookie ignoring anything after ; (which looks like an invalid attribute)"},{"cookie":"test=17; customvalue='1000 or more'","expected":"test=17","name":"Set cookie ignoring anything after ; (which looks like an invalid attribute, with no trailing semicolon)"},{"cookie":"test=18; foo=bar, a=b","expected":"test=18","name":"Ignore keys after semicolon"},{"cookie":"test=19;max-age=3600, c=d;path=/","expected":"test=19","name":"Ignore attributes after semicolon","defaultPath":false},{"cookie":["testA=20","=","testb=20"],"expected":"testA=20; testb=20","name":"Ignore `Set-Cookie: =`"},{"cookie":["test=21",""],"expected":"test=21","name":"Ignore empty cookie string"},{"cookie":["test22","="],"expected":"test22","name":"Ignore `Set-Cookie: =` with other `Set-Cookie` headers"},{"cookie":["testA23","; testB23"],"expected":"testA23","name":"Ignore name- and value-less `Set-Cookie: ; bar`"},{"cookie":["test24"," "],"expected":"test24","name":"Ignore name- and value-less `Set-Cookie: `"},{"cookie":["test25","\t"],"expected":"test25","name":"Ignore name- and value-less `Set-Cookie: \\t`"},{"cookie":"test=26; domain=.parser.test; ;; ;=; ,,, ===,abc,=; abracadabra! max-age=20;=;;","expected":"","name":"Ignore cookie with domain that won't domain match (along with other invalid noise)"}] diff --git a/gun/test/wpt/cookies/attributes_max_age.json b/gun/test/wpt/cookies/attributes_max_age.json new file mode 100644 index 0000000..146157f --- /dev/null +++ b/gun/test/wpt/cookies/attributes_max_age.json @@ -0,0 +1 @@ +[{"cookie":"test=1; Max-Age=50,399","expected":"test=1","name":"Ignore max-age attribute with invalid non-zero-digit (containing a comma)"},{"cookie":"test=2; max-age=10000","expected":"test=2","name":"Set cookie with age"},{"cookie":"test=3; max-age=0","expected":"","name":"Set no cookie with max-age=0"},{"cookie":"test=4; max-age=-1","expected":"","name":"Set no cookie with max-age=-1"},{"cookie":"test=5; max-age=-20","expected":"","name":"Set no cookie with max-age=-20"},{"cookie":["testA=6; max-age=60","testB=6; max-age=60"],"expected":"testA=6; testB=6","name":"Set multiple cookies with max-age attribute"},{"cookie":["testA=7; max-age=60","testB=7; max-age=60","testA=differentvalue; max-age=0"],"expected":"testB=7","name":"Expire later cookie with same name and max-age=0"},{"cookie":["testA=8; max-age=60","testB=8; max-age=60","testA=differentvalue; max-age=0","testC=8; max-age=0"],"expected":"testB=8","name":"Expire later cookie with same name and max-age=0, and don't set cookie with max-age=0"},{"cookie":["test=\"9! = foo;bar\";\" parser; max-age=6","test9; max-age=2.63,"],"expected":"test=\"9! = foo; test9","name":"Set mulitiple cookies with valid max-age values"},{"cookie":["test=10; max-age=0","test10; max-age=0"],"expected":"","name":"Don't set multiple cookies with max-age=0"}] diff --git a/gun/test/wpt/cookies/attributes_path.json b/gun/test/wpt/cookies/attributes_path.json new file mode 100644 index 0000000..1c8f35d --- /dev/null +++ b/gun/test/wpt/cookies/attributes_path.json @@ -0,0 +1 @@ +[{"cookie":"test=1; Path","expected":"test=1","name":"Set cookie for bare Path"},{"cookie":"test=2; Path=","expected":"test=2","name":"Set cookie for Path="},{"cookie":"test=3; Path=/","expected":"test=3","name":"Set cookie for Path=/","defaultPath":false},{"cookie":"test=4; Path=/qux","expected":"","name":"No cookie returned for mismatched path","defaultPath":false},{"cookie":"test=5; Path =/qux","expected":"","name":"No cookie returned for path space equals mismatched path","defaultPath":false},{"cookie":"test=6; Path= /qux","expected":"","name":"No cookie returned for path equals space mismatched path","defaultPath":false},{"cookie":"test=7; Path=/qux ; taz","expected":"","name":"No cookie returned for mismatched path and attribute","defaultPath":false},{"cookie":"test=8; Path=/qux; Path=/","expected":"test=8","name":"Set cookie for mismatched and root path"},{"cookie":"test=9; Path=/; Path=/qux","expected":"","name":"No cookie returned for root and mismatched path","defaultPath":false},{"cookie":"test=10; Path=/lol; Path=/qux","expected":"","name":"No cookie returned for multiple mismatched paths","defaultPath":false},{"cookie":["testA=11; path=/","testB=11; path=/cookies/attributes"],"expected":"testB=11; testA=11","name":"Return 2 cookies sorted by matching path length (earlier name with shorter path set first)","defaultPath":false},{"cookie":["testB=12; path=/","testA=12; path=/cookies/attributes"],"expected":"testA=12; testB=12","name":"Return 2 cookies sorted by matching path length (later name with shorter path set first)","defaultPath":false},{"cookie":["testA=13; path=/cookies/attributes","testB=13; path=/"],"expected":"testA=13; testB=13","name":"Return 2 cookies sorted by matching path length (earlier name with longer path set first)","defaultPath":false},{"cookie":["testB=14; path=/cookies/attributes","testA=14; path=/"],"expected":"testB=14; testA=14","name":"Return 2 cookies sorted by matching path length (later name with longer path set first)","defaultPath":false},{"cookie":["test=15; path=/cookies/attributes/foo"],"expected":"","name":"No cookie returned for partial path match","defaultPath":false},{"cookie":["test=16","test=0; path=/cookies/attributes/foo"],"expected":"test=16","name":"No cookie returned for partial path match, return cookie for default path"},{"cookie":["test=17; path= /"],"expected":"test=17","name":"Return cookie for path= / (whitespace after equals)"},{"cookie":["test=18; path=/cookies/ATTRIBUTES"],"expected":"","name":"No cookie returned for case mismatched path","defaultPath":false},{"cookie":["testA=19; \tpath\t=\t/cookies/attributes","testB=19; \tpath\t=\t/book"],"expected":"testA=19","name":"Return cookie A on path match, no cookie returned for path mismatch (plus whitespace)","defaultPath":false},{"cookie":["test=20; path=; path=/dog"],"expected":"","name":"No cookie returned for mismatched path (after bare path=)","defaultPath":false},{"cookie":["test=21; path=/dog; path="],"expected":"test=21","name":"Return cookie for bare path= (after mismatched path)"}] diff --git a/gun/test/wpt/cookies/attributes_secure.json b/gun/test/wpt/cookies/attributes_secure.json new file mode 100644 index 0000000..63accac --- /dev/null +++ b/gun/test/wpt/cookies/attributes_secure.json @@ -0,0 +1 @@ +[{"cookie":"test=1; Secure","expected":"test=1","name":"Set cookie for Secure attribute"},{"cookie":"test=2; seCURe","expected":"test=2","name":"Set cookie for seCURe attribute"},{"cookie":"test=3; Secure=","expected":"test=3","name":"Set cookie for for Secure= attribute"},{"cookie":"test=4; Secure=aaaa","expected":"test=4","name":"Set cookie for Secure=aaaa"},{"cookie":"test=5; Secure =aaaaa","expected":"test=5","name":"Set cookie for Secure space equals"},{"cookie":"test=6; Secure= aaaaa","expected":"test=6","name":"Set cookie for Secure equals space"},{"cookie":"test=7; Secure","expected":"test=7","name":"Set cookie for spaced Secure"},{"cookie":"test=8; Secure ;","expected":"test=8","name":"Set cookie for space Secure with ;"}] diff --git a/gun/test/wpt/cookies/attributes_secure_non_secure.json b/gun/test/wpt/cookies/attributes_secure_non_secure.json new file mode 100644 index 0000000..60777b3 --- /dev/null +++ b/gun/test/wpt/cookies/attributes_secure_non_secure.json @@ -0,0 +1 @@ +[{"cookie":"test=1; Secure","expected":"","name":"(non-secure) Ignore cookie for Secure attribute"},{"cookie":"test=2; seCURe","expected":"","name":"(non-secure) Ignore cookie for seCURe attribute"},{"cookie":"test=3; Secure=","expected":"","name":"(non-secure) Ignore cookie for for Secure= attribute"},{"cookie":"test=4; Secure=aaaa","expected":"","name":"(non-secure) Ignore cookie for Secure=aaaa"},{"cookie":"test=5; Secure =aaaaa","expected":"","name":"(non-secure) Ignore cookie for Secure space equals"},{"cookie":"test=6; Secure= aaaaa","expected":"","name":"(non-secure) Ignore cookie for Secure equals space"},{"cookie":"test=7; Secure","expected":"","name":"(non-secure) Ignore cookie for spaced Secure"},{"cookie":"test=8; Secure ;","expected":"","name":"(non-secure) Ignore cookie for space Secure with ;"},{"cookie":"__Secure-test=9; Secure","expected":"","name":"(non-secure) Ignore cookie with __Secure- prefix and Secure"},{"cookie":"__Secure-test=10","expected":"","name":"(non-secure) Ignore cookie with __Secure- prefix and without Secure"},{"cookie":"__%53ecure-test=11","expected":"__%53ecure-test=11","name":"(non-secure) Cookie returned with __%53ecure- prefix and without Secure"}] diff --git a/gun/test/wpt/cookies/encoding_charset.json b/gun/test/wpt/cookies/encoding_charset.json new file mode 100644 index 0000000..d6e7048 --- /dev/null +++ b/gun/test/wpt/cookies/encoding_charset.json @@ -0,0 +1 @@ +[{"cookie":"test=1春节回家路·春运完全手册","expected":"test=1春节回家路·春运完全手册","name":"ASCII name and utf-8 value"},{"cookie":"тест=2","expected":"тест=2","name":"utf-8 name and ASCII value"},{"cookie":"test=\"3春节回家路·春运完全手册\"","expected":"test=\"3春节回家路·春运完全手册\"","name":"ASCII name and quoted utf-8 value"},{"cookie":"春节回=4家路·春运完全手册","expected":"春节回=4家路·春运完全手册","name":"utf-8 name and value"},{"cookie":"\"春节回=5家路·春运完全手册\"","expected":"\"春节回=5家路·春运完全手册\"","name":"quoted utf-8 name and value"},{"cookie":"春节回=6家路·春运; 完全手册","expected":"春节回=6家路·春运","name":"utf-8 name and value, with (invalid) utf-8 attribute"}] diff --git a/gun/test/wpt/cookies/name.json b/gun/test/wpt/cookies/name.json new file mode 100644 index 0000000..be33321 --- /dev/null +++ b/gun/test/wpt/cookies/name.json @@ -0,0 +1 @@ +[{"cookie":"test1=; path = /","expected":"test1=","name":"Set valueless cookie to its name with empty value","defaultPath":false},{"cookie":"=test=2","expected":"test=2","name":"Set a nameless cookie (that has an = in its value)"},{"cookie":"===test=2b","expected":"==test=2b","name":"Set a nameless cookie (that has multiple ='s in its value)"},{"cookie":"=test2c","expected":"test2c","name":"Set a nameless cookie"},{"cookie":"test =3","expected":"test=3","name":"Remove trailing WSP characters from the name string"},{"cookie":" test=4","expected":"test=4","name":"Remove leading WSP characters from the name string"},{"cookie":["\"test=5\"=test","\"test=5"],"expected":"\"test=5","name":"Only return the new cookie (with the same name)"},{"cookie":"test6;cool=dude","expected":"test6","name":"Ignore invalid attributes after nameless cookie"},{"cookie":"$Version=1; test=7","expected":"$Version=1","name":"Ignore invalid attributes after valid name (that looks like Cookie2 Version attribute)"},{"cookie":"test test=8","expected":"test test=8","name":"Set a cookie that has whitespace in its name"},{"cookie":"\"test9;test\"=9","expected":"\"test9","name":"Set a nameless cookie ignoring characters after first ;"},{"cookie":"\"test\"10;baz\"=qux","expected":"\"test\"10","name":"Set a nameless cookie ignoring characters after first ; (2)"},{"cookie":["=test=11","test11"],"expected":"test11","name":"Return the most recent nameless cookie"},{"cookie":["test11","test11a"],"expected":"test11a","name":"Return the most recent nameless cookie, without leading ="},{"cookie":["test11","test11a","=test11b"],"expected":"test11b","name":"Return the most recent nameless cookie, even if preceded by ="},{"cookie":["test11","test11a","=test11b","test=11c"],"expected":"test11b; test=11c","name":"Return the most recent nameless cookie, even if preceded by =, in addition to other valid cookie"},{"cookie":["test12=11","test12=12"],"expected":"test12=12","name":"Use last value for cookies with identical names"},{"cookie":["testA=13","testB=13"],"expected":"testA=13; testB=13","name":"Keep first-in, first-out name order"},{"cookie":["a=test14","z=test14"],"expected":"a=test14; z=test14","name":"Keep first-in, first-out single-char name order"},{"cookie":["z=test15","a=test15"],"expected":"z=test15; a=test15","name":"Keep non-alphabetic first-in, first-out name order"},{"cookie":"z=test16, a=test16","expected":"z=test16, a=test16","name":"Keep first-in, first-out order if comma-separated"},{"cookie":["testA=16","=test16","testB=16"],"expected":"testA=16; test16; testB=16","name":"Set nameless cookie, given `Set-Cookie: =test16`"},{"cookie":["test17a","test17b"],"expected":"test17b","name":"Overwrite nameless cookie"},{"cookie":"=","expected":"","name":"Ignore cookie with empty name and empty value"},{"cookie":"","expected":"","name":"Ignore cookie with no name or value"},{"cookie":"%74%65%73%74=20","expected":"%74%65%73%74=20","name":"URL-encoded cookie name is not decoded"}] diff --git a/gun/test/wpt/cookies/size_attributes.json b/gun/test/wpt/cookies/size_attributes.json new file mode 100644 index 0000000..1eb6af4 --- /dev/null +++ b/gun/test/wpt/cookies/size_attributes.json @@ -0,0 +1 @@ +[{"cookie":"test=1; path=/cookies/size; path=/cookies/sizeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","expected":"test=1","name":"Too long path attribute (>1024 bytes) is ignored; previous valid path wins.","defaultPath":false},{"cookie":"test=2; path=/cookies/sizeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee; path=/cookies/size","expected":"test=2","name":"Too long path attribute (>1024 bytes) is ignored; next valid path wins.","defaultPath":false},{"cookie":"test=3; path=/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;","expected":"","name":"Max size path attribute (1024 bytes) is not ignored"},{"cookie":"test=4; path=/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;","expected":"test=4","name":"Too long path attribute (>1024 bytes) is ignored"},{"cookie":"test=5; domain=web-platform.test; domain=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com","expected":"test=5","name":"Too long domain attribute (>1024 bytes) is ignored; previous valid domain wins."},{"cookie":"test=6; domain=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com; domain=web-platform.test","expected":"test=6","name":"Too long domain attribute (>1024 bytes) is ignored; next valid domain wins."},{"cookie":"test=7; domain=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com;","expected":"","name":"Max size domain attribute (1024 bytes) is not ignored"},{"cookie":"test=8; domain=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com;","expected":"test=8","name":"Too long domain attribute (>1024 bytes) is ignored"},{"cookie":"tttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt=11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111; domain=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com; domain=web-platform.test","expected":"tttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt=11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111","name":"Set cookie with max size name/value pair and max size attribute value"},{"cookie":"tttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt=11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111; domain=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com; domain=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com; domain=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com; domain=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com; domain=web-platform.test","expected":"tttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt=11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111","name":"Set cookie with max size name/value pair and multiple max size attributes (>8k bytes total)"},{"cookie":"test=11; max-age=1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111;","expected":"test=11","name":"Max length Max-Age attribute value (1024 bytes) doesn't cause cookie rejection"},{"cookie":"test=12; max-age=11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111;","expected":"test=12","name":"Too long Max-Age attribute value (>1024 bytes) doesn't cause cookie rejection"},{"cookie":"test=13; max-age=-111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111;","expected":"","name":"Max length negative Max-Age attribute value (1024 bytes) doesn't get ignored"},{"cookie":"test=14; max-age=-1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111;","expected":"test=14","name":"Too long negative Max-Age attribute value (>1024 bytes) gets ignored"}] diff --git a/gun/test/wpt/cookies/size_name_and_value.json b/gun/test/wpt/cookies/size_name_and_value.json new file mode 100644 index 0000000..983fdf8 --- /dev/null +++ b/gun/test/wpt/cookies/size_name_and_value.json @@ -0,0 +1 @@ +[{"cookie":"tttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt=11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111","expected":"tttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt=11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111","name":"Set max-size cookie with largest possible name and value (4096 bytes)"},{"cookie":"ttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt=1","expected":"","name":"Ignore cookie with name larger than 4096 and 1 byte value"},{"cookie":"tttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt=","expected":"tttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt=","name":"Set max-size value-less cookie"},{"cookie":"ttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt=","expected":"","name":"Ignore value-less cookie with name larger than 4096 bytes"},{"cookie":"t=111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111","expected":"t=111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111","name":"Set max-size cookie with largest possible value (4095 bytes)"},{"cookie":"t=1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111","expected":"","name":"Ignore named cookie (with non-zero length) and value larger than 4095 bytes"},{"cookie":"tttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt=1","expected":"","name":"Ignore named cookie with length larger than 4095 bytes, and a non-zero value"},{"cookie":"=1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111","expected":"1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111","name":"Set max-size name-less cookie"},{"cookie":"=11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111","expected":"","name":"Ignore name-less cookie with value larger than 4096 bytes"},{"cookie":"11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111","expected":"","name":"Ignore name-less cookie (without leading =) with value larger than 4096 bytes"},{"cookie":"tttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt=11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111; Max-Age:43110;","expected":"tttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt=11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111","name":"Set max-size cookie that also has an attribute"}] diff --git a/gun/test/wpt/cookies/value.json b/gun/test/wpt/cookies/value.json new file mode 100644 index 0000000..6200fba --- /dev/null +++ b/gun/test/wpt/cookies/value.json @@ -0,0 +1 @@ +[{"cookie":"test=1, baz=qux","expected":"test=1, baz=qux","name":"Set value containing a comma"},{"cookie":"test=\"2, baz=qux\"","expected":"test=\"2, baz=qux\"","name":"Set quoted value containing a comma"},{"cookie":"test=\"3zz;pp\" ; ;","expected":"test=\"3zz","name":"Ignore values after semicolon"},{"cookie":"test=\"4zz ;","expected":"test=\"4zz","name":"Ignore whitespace at the end of value"},{"cookie":"test=\"5zzz \" \"ppp\" ;","expected":"test=\"5zzz \" \"ppp\"","name":"Set value including quotes and whitespace up until semicolon"},{"cookie":"test=6A\"B ;","expected":"test=6A\"B","name":"Set value with a single quote excluding whitespace"},{"cookie":"test7","expected":"test7","name":"Set nameless cookie to its value"},{"cookie":"\"test8\"HHH\"","expected":"\"test8\"HHH\"","name":"Set nameless cookie to its value with an escaped quote"},{"cookie":"test=\"9","expected":"test=\"9","name":"Set value with unbalanced leading quote"},{"cookie":"=test10","expected":"test10","name":"Set nameless cookie followed by '=' to its value"},{"cookie":"test=11aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","expected":"test=11aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","name":"Set cookie with large name + value ( = 4kb)"},{"cookie":"test=12aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","expected":"","name":"Ignore cookie with large name + value ( > 4kb)"},{"cookie":"test=13\nZYX","expected":"test=13","name":"Set cookie but ignore value after LF"},{"cookie":"test=\"14 \" ;","expected":"test=\"14 \"","name":"Set cookie ignoring whitespace after value endquote"},{"cookie":"test=15 ;","expected":"test=15","name":"Ignore whitespace and ; after value"},{"cookie":"test= 16","expected":"test=16","name":"Ignore whitespace preceding value"},{"cookie":"test=\"17\"","expected":"test=\"17\"","name":"Set cookie with quotes in value"},{"cookie":"test=\" 18 \"","expected":"test=\" 18 \"","name":"Set cookie keeping whitespace inside quoted value"},{"cookie":"test=\"19;wow\"","expected":"test=\"19","name":"Set cookie value ignoring characters after semicolon"},{"cookie":"test=\"20=20\"","expected":"test=\"20=20\"","name":"Set cookie with another = inside quoted value"},{"cookie":"test\t=\t21\t \t;\tttt","expected":"test=21","name":"Set cookie ignoring whitespace surrounding value and characters after first semicolon"},{"cookie":["testA=22","test22=","testB=22"],"expected":"testA=22; test22=; testB=22","name":"Set valueless cookie, given `Set-Cookie: test22=`"},{"cookie":"test=%32%33","expected":"test=%32%33","name":"URL-encoded cookie value is not decoded"},{"cookie":"test24==","expected":"test24==","name":"Set cookie with value set to ="},{"cookie":"test=25=25","expected":"test=25=25","name":"Set cookie with one = inside an unquoted value"},{"cookie":"test=26=26=26","expected":"test=26=26=26","name":"Set cookie with two = inside an unquoted value"},{"cookie":"test=27 test","expected":"test=27 test","name":"Set cookie with a space character in the value"},{"cookie":" test test28 ;","expected":"test test28","name":"Set a nameless cookie with a space character in the value"}] diff --git a/gun/test/ws_SUITE.erl b/gun/test/ws_SUITE.erl new file mode 100644 index 0000000..ab67b89 --- /dev/null +++ b/gun/test/ws_SUITE.erl @@ -0,0 +1,270 @@ +%% Copyright (c) 2018-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(ws_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-import(ct_helper, [config/2]). +-import(ct_helper, [doc/1]). + +%% ct. + +all() -> + [{group, http}, {group, http2}]. + +groups() -> + Tests = ct_helper:all(?MODULE), + HTTP1Tests = [ + http10_upgrade_error, + http11_request_error, + http11_keepalive, + http11_keepalive_default_silence_pings + ], + [ + {http, [], Tests}, + {http2, [], Tests -- HTTP1Tests} + ]. + +init_per_suite(Config) -> + Routes = [ + {"/", ws_echo_h, []}, + {"/reject", ws_reject_h, []}, + {"/subprotocol", ws_subprotocol_h, []} + ], + {ok, _} = cowboy:start_clear(ws, [], #{ + enable_connect_protocol => true, + env => #{dispatch => cowboy_router:compile([{'_', Routes}])} + }), + Port = ranch:get_port(ws), + [{port, Port}|Config]. + +end_per_suite(_) -> + cowboy:stop_listener(ws). + +%% Tests. + +await(Config) -> + doc("Ensure gun:await/2 can be used to receive Websocket frames."), + Protocol = config(name, config(tc_group_properties, Config)), + {ok, ConnPid} = gun:open("localhost", config(port, Config), #{ + protocols => [Protocol], + http2_opts => #{notify_settings_changed => true} + }), + {ok, Protocol} = gun:await_up(ConnPid), + do_await_enable_connect_protocol(Protocol, ConnPid), + StreamRef = gun:ws_upgrade(ConnPid, "/", []), + {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), + Frame = {text, <<"Hello!">>}, + gun:ws_send(ConnPid, StreamRef, Frame), + {ws, Frame} = gun:await(ConnPid, StreamRef), + gun:close(ConnPid). + +headers_normalized_upgrade(Config) -> + doc("Headers passed to ws_upgrade are normalized before being used."), + Protocol = config(name, config(tc_group_properties, Config)), + {ok, ConnPid} = gun:open("localhost", config(port, Config), #{ + protocols => [Protocol], + http2_opts => #{notify_settings_changed => true} + }), + {ok, Protocol} = gun:await_up(ConnPid), + do_await_enable_connect_protocol(Protocol, ConnPid), + StreamRef = gun:ws_upgrade(ConnPid, "/", #{ + atom_header_name => <<"value">>, + "string_header_name" => <<"value">> + }), + {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), + gun:close(ConnPid). + +http10_upgrade_error(Config) -> + doc("Attempting to upgrade HTTP/1.0 to Websocket produces an error."), + {ok, ConnPid} = gun:open("localhost", config(port, Config), #{ + http_opts => #{version => 'HTTP/1.0'} + }), + {ok, _} = gun:await_up(ConnPid), + StreamRef = gun:ws_upgrade(ConnPid, "/", []), + receive + {gun_error, ConnPid, StreamRef, {badstate, _}} -> + gun:close(ConnPid); + Msg -> + error({fail, Msg}) + after 1000 -> + error(timeout) + end. + +http11_keepalive(Config) -> + doc("Ensure that Gun automatically sends ping frames."), + {ok, ConnPid} = gun:open("localhost", config(port, Config), #{ + ws_opts => #{ + keepalive => 100, + silence_pings => false + } + }), + {ok, _} = gun:await_up(ConnPid), + StreamRef = gun:ws_upgrade(ConnPid, "/", []), + {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), + %% Gun sent a ping automatically, we therefore receive a pong. + {ws, pong} = gun:await(ConnPid, StreamRef), + gun:close(ConnPid). + +http11_keepalive_default_silence_pings(Config) -> + doc("Ensure that Gun does not forward ping/pong by default."), + {ok, ConnPid} = gun:open("localhost", config(port, Config), #{ + ws_opts => #{keepalive => 100} + }), + {ok, _} = gun:await_up(ConnPid), + StreamRef = gun:ws_upgrade(ConnPid, "/", []), + {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), + %% Gun sent a ping automatically, but we silence ping/pong by default. + {error, timeout} = gun:await(ConnPid, StreamRef, 1000), + gun:close(ConnPid). + +http11_request_error(Config) -> + doc("Ensure that HTTP/1.1 requests are rejected while using Websocket."), + {ok, ConnPid} = gun:open("localhost", config(port, Config)), + {ok, _} = gun:await_up(ConnPid), + StreamRef1 = gun:ws_upgrade(ConnPid, "/", []), + {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef1), + StreamRef2 = gun:get(ConnPid, "/"), + {error, {connection_error, {badstate, _}}} = gun:await(ConnPid, StreamRef2), + gun:close(ConnPid). + +reject_upgrade(Config) -> + doc("Ensure Websocket connections can be rejected."), + Protocol = config(name, config(tc_group_properties, Config)), + {ok, ConnPid} = gun:open("localhost", config(port, Config), #{ + protocols => [Protocol], + http2_opts => #{notify_settings_changed => true} + }), + {ok, Protocol} = gun:await_up(ConnPid), + do_await_enable_connect_protocol(Protocol, ConnPid), + StreamRef = gun:ws_upgrade(ConnPid, "/reject", []), + receive + {gun_response, ConnPid, StreamRef, nofin, 400, _} -> + {ok, <<"Upgrade rejected">>} = gun:await_body(ConnPid, StreamRef, 1000), + gun:close(ConnPid); + Msg -> + error({fail, Msg}) + after 1000 -> + error(timeout) + end. + +reply_to(Config) -> + doc("Ensure the reply_to request option is respected."), + Self = self(), + Frame = {text, <<"Hello!">>}, + ReplyTo = spawn(fun() -> + {ConnPid, StreamRef} = receive + {C, S} when is_pid(C), is_reference(S) -> {C, S} + after 1000 -> + error(timeout) + end, + {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), + Self ! {self(), ready}, + {ws, Frame} = gun:await(ConnPid, StreamRef), + Self ! {self(), ok} + end), + Protocol = config(name, config(tc_group_properties, Config)), + {ok, ConnPid} = gun:open("localhost", config(port, Config), #{ + protocols => [Protocol], + http2_opts => #{notify_settings_changed => true} + }), + {ok, Protocol} = gun:await_up(ConnPid), + do_await_enable_connect_protocol(Protocol, ConnPid), + StreamRef = gun:ws_upgrade(ConnPid, "/", [], #{reply_to => ReplyTo}), + ReplyTo ! {ConnPid, StreamRef}, + receive {ReplyTo, ready} -> gun:ws_send(ConnPid, StreamRef, Frame) after 1000 -> error(timeout) end, + receive {ReplyTo, ok} -> gun:close(ConnPid) after 1000 -> error(timeout) end. + +send_many(Config) -> + doc("Ensure we can send a list of frames in one gun:ws_send call."), + Protocol = config(name, config(tc_group_properties, Config)), + {ok, ConnPid} = gun:open("localhost", config(port, Config), #{ + protocols => [Protocol], + http2_opts => #{notify_settings_changed => true} + }), + {ok, Protocol} = gun:await_up(ConnPid), + do_await_enable_connect_protocol(Protocol, ConnPid), + StreamRef = gun:ws_upgrade(ConnPid, "/", []), + {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), + Frame1 = {text, <<"Hello!">>}, + Frame2 = {binary, <<"World!">>}, + gun:ws_send(ConnPid, StreamRef, [Frame1, Frame2]), + {ws, Frame1} = gun:await(ConnPid, StreamRef), + {ws, Frame2} = gun:await(ConnPid, StreamRef), + gun:close(ConnPid). + +send_many_close(Config) -> + doc("Ensure we can send a list of frames in one gun:ws_send call, including a close frame."), + Protocol = config(name, config(tc_group_properties, Config)), + {ok, ConnPid} = gun:open("localhost", config(port, Config), #{ + protocols => [Protocol], + http2_opts => #{notify_settings_changed => true} + }), + {ok, Protocol} = gun:await_up(ConnPid), + do_await_enable_connect_protocol(Protocol, ConnPid), + StreamRef = gun:ws_upgrade(ConnPid, "/", []), + {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), + Frame1 = {text, <<"Hello!">>}, + Frame2 = {binary, <<"World!">>}, + gun:ws_send(ConnPid, StreamRef, [Frame1, Frame2, close]), + {ws, Frame1} = gun:await(ConnPid, StreamRef), + {ws, Frame2} = gun:await(ConnPid, StreamRef), + {ws, close} = gun:await(ConnPid, StreamRef), + gun:close(ConnPid). + +subprotocol_match(Config) -> + doc("Websocket subprotocol successfully negotiated."), + Protocols = [{P, gun_ws_h} || P <- [<<"dummy">>, <<"echo">>, <<"junk">>]], + {ok, ConnPid} = gun:open("localhost", config(port, Config)), + {ok, _} = gun:await_up(ConnPid), + StreamRef = gun:ws_upgrade(ConnPid, "/subprotocol", [], #{ + protocols => Protocols + }), + {upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef), + Frame = {text, <<"Hello!">>}, + gun:ws_send(ConnPid, StreamRef, Frame), + {ws, Frame} = gun:await(ConnPid, StreamRef), + gun:close(ConnPid). + +subprotocol_nomatch(Config) -> + doc("Websocket subprotocol negotiation failure."), + Protocols = [{P, gun_ws_h} || P <- [<<"dummy">>, <<"junk">>]], + {ok, ConnPid} = gun:open("localhost", config(port, Config)), + {ok, _} = gun:await_up(ConnPid), + StreamRef = gun:ws_upgrade(ConnPid, "/subprotocol", [], #{ + protocols => Protocols + }), + {response, nofin, 400, _} = gun:await(ConnPid, StreamRef), + {ok, <<"nomatch">>} = gun:await_body(ConnPid, StreamRef), + gun:close(ConnPid). + +subprotocol_required_but_missing(Config) -> + doc("Websocket subprotocol not negotiated but required by the server."), + {ok, ConnPid} = gun:open("localhost", config(port, Config)), + {ok, _} = gun:await_up(ConnPid), + StreamRef = gun:ws_upgrade(ConnPid, "/subprotocol", []), + {response, nofin, 400, _} = gun:await(ConnPid, StreamRef), + {ok, <<"undefined">>} = gun:await_body(ConnPid, StreamRef), + gun:close(ConnPid). + +%% Internal. + +do_await_enable_connect_protocol(http, _) -> + ok; +%% We cannot do a CONNECT :protocol request until the server tells us we can. +do_await_enable_connect_protocol(http2, ConnPid) -> + {notify, settings_changed, #{enable_connect_protocol := true}} + = gun:await(ConnPid, undefined), %% @todo Maybe have a gun:await/1? + ok. diff --git a/gun/test/ws_autobahn_SUITE.erl b/gun/test/ws_autobahn_SUITE.erl new file mode 100644 index 0000000..e087432 --- /dev/null +++ b/gun/test/ws_autobahn_SUITE.erl @@ -0,0 +1,170 @@ +%% Copyright (c) 2015-2023, Loïc Hoguin +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(ws_autobahn_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-import(ct_helper, [config/2]). + +%% ct. + +all() -> + [{group, autobahn}]. + +groups() -> + [{autobahn, [], [autobahn_fuzzingserver]}]. + +init_per_group(autobahn, Config) -> + %% Some systems have it named pip2. + Out = os:cmd("pip show autobahntestsuite ; pip2 show autobahntestsuite"), + case string:str(Out, "autobahntestsuite") of + 0 -> + ct:pal("Skipping the autobahn group because the " + "Autobahn Test Suite is not installed.~nTo install it, " + "please follow the instructions on this page:~n~n " + "http://autobahn.ws/testsuite/installation.html"), + {skip, "Autobahn Test Suite not installed."}; + _ -> + Config + end; +init_per_group(_, _) -> + ok. + +end_per_group(_, _) -> + ok. + +%% Tests. + +autobahn_fuzzingserver(Config) -> + Self = self(), + spawn_link(fun() -> start_port(Config, Self) end), + receive autobahn_ready -> ok end, + N = get_case_count(), + run_cases(0, N), + Report = config(priv_dir, Config) ++ "reports/clients/index.html", + ct:log("

Full report

~n", [Report]), + ct:print("Autobahn Test Suite report: file://~s~n", [Report]), + log_output(), + terminate(), + {ok, HTML} = file:read_file(Report), + case length(binary:matches(HTML, <<"case_failed">>)) > 2 of + true -> error(failed); + false -> ok + end. + +start_port(Config, Pid) -> + Port = open_port({spawn, "wstest -m fuzzingserver -s " ++ config(data_dir, Config) ++ "server.json"}, + [{line, 10000}, {cd, config(priv_dir, Config)}, binary]), + receive_preamble(Port, Pid), + receive_infinity(Port). + +receive_preamble(Port, Pid) -> + receive + {Port, {data, {eol, Line = <<"Ok, will run", _/bits>>}}} -> + Pid ! autobahn_ready, + io:format(user, "~s~n", [Line]); + {Port, {data, {eol, Line}}} -> + io:format(user, "~s~n", [Line]), + receive_preamble(Port, Pid) + after 5000 -> + terminate(), + error(timeout) + end. + +receive_infinity(Port) -> + receive + {Port, {data, {eol, <<"Updating reports", _/bits>>}}} -> + receive_infinity(Port); + {Port, {data, {eol, Line}}} -> + io:format(user, "~s~n", [Line]), + receive_infinity(Port) + end. + +get_case_count() -> + {Pid, MRef, StreamRef} = connect("/getCaseCount"), + receive + {gun_ws, Pid, StreamRef, {text, N}} -> + close(Pid, MRef), + binary_to_integer(N); + Msg -> + ct:pal("Unexpected message ~p", [Msg]), + terminate(), + error(failed) + end. + +run_cases(Total, Total) -> + ok; +run_cases(N, Total) -> + {Pid, MRef, StreamRef} = connect(["/runCase?case=", integer_to_binary(N + 1), "&agent=Gun"]), + loop(Pid, MRef, StreamRef), + update_reports(), + run_cases(N + 1, Total). + +loop(Pid, MRef, StreamRef) -> + receive + {gun_ws, Pid, StreamRef, close} -> + gun:ws_send(Pid, StreamRef, close), + loop(Pid, MRef, StreamRef); + {gun_ws, Pid, StreamRef, {close, Code, _}} -> + gun:ws_send(Pid, StreamRef, {close, Code, <<>>}), + loop(Pid, MRef, StreamRef); + {gun_ws, Pid, StreamRef, Frame} -> + gun:ws_send(Pid, StreamRef, Frame), + loop(Pid, MRef, StreamRef); + {gun_down, Pid, ws, _, _} -> + close(Pid, MRef); + {'DOWN', MRef, process, Pid, normal} -> + close(Pid, MRef); + Msg -> + ct:pal("Unexpected message ~p", [Msg]), + close(Pid, MRef) + end. + +update_reports() -> + {Pid, MRef, StreamRef} = connect("/updateReports?agent=Gun"), + receive + {gun_ws, Pid, StreamRef, close} -> + close(Pid, MRef) + after 5000 -> + error(failed) + end. + +log_output() -> + ok. + +connect(Path) -> + {ok, Pid} = gun:open("127.0.0.1", 33080, #{retry => 0}), + {ok, http} = gun:await_up(Pid), + MRef = monitor(process, Pid), + StreamRef = gun:ws_upgrade(Pid, Path, [], #{compress => true}), + receive + {gun_upgrade, Pid, StreamRef, [<<"websocket">>], _} -> + ok; + Msg -> + ct:pal("Unexpected message ~p", [Msg]), + terminate(), + error(failed) + end, + {Pid, MRef, StreamRef}. + +close(Pid, MRef) -> + demonitor(MRef), + gun:close(Pid), + gun:flush(Pid). + +terminate() -> + Res = os:cmd("killall wstest"), + io:format(user, "~s", [Res]), + ok. diff --git a/gun/test/ws_autobahn_SUITE_data/server.json b/gun/test/ws_autobahn_SUITE_data/server.json new file mode 100644 index 0000000..902b9b3 --- /dev/null +++ b/gun/test/ws_autobahn_SUITE_data/server.json @@ -0,0 +1,7 @@ +{ + "url": "ws://localhost:33080", + + "cases": ["*"], + "exclude-cases": [], + "exclude-agent-cases": {} +}