diff --git a/ranch/LICENSE b/ranch/LICENSE new file mode 100644 index 0000000..9c29efd --- /dev/null +++ b/ranch/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2011-2021, 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/ranch/Makefile b/ranch/Makefile new file mode 100644 index 0000000..160022d --- /dev/null +++ b/ranch/Makefile @@ -0,0 +1,7 @@ +.PHONY: all clean + +all: + ${MAKE} -C src + +clean: + ${MAKE} -C src clean diff --git a/ranch/README.asciidoc b/ranch/README.asciidoc new file mode 100644 index 0000000..eb3e482 --- /dev/null +++ b/ranch/README.asciidoc @@ -0,0 +1,37 @@ += Ranch + +Ranch is a socket acceptor pool for TCP protocols. + +== Goals + +Ranch aims to provide everything you need to accept TCP connections with +a *small* code base and *low latency* while being easy to use directly +as an application or to *embed* into your own. + +Ranch provides a *modular* design, letting you choose which transport +and protocol are going to be used for a particular listener. Listeners +accept and manage connections on one port, and include facilities to +limit the number of *concurrent* connections. Connections are sorted +into *pools*, each pool having a different configurable limit. + +Ranch also allows you to *upgrade* the acceptor pool without having +to close any of the currently opened sockets. + +== Online documentation + +* https://ninenines.eu/docs/en/ranch/2.1/guide[User guide] +* https://ninenines.eu/docs/en/ranch/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/` +* Examples available in `examples/` + +== Getting help + +* https://github.com/ninenines/ranch/issues[Issues tracker] +* https://ninenines.eu/services[Commercial Support] diff --git a/ranch/doc/src/guide/book.asciidoc b/ranch/doc/src/guide/book.asciidoc new file mode 100644 index 0000000..ce3071e --- /dev/null +++ b/ranch/doc/src/guide/book.asciidoc @@ -0,0 +1,40 @@ +// a2x: --dblatex-opts "-P latex.output.revhistory=0 -P doc.publisher.show=0 -P index.numbered=0" +// a2x: -d book --attribute tabsize=4 + += Ranch User Guide + += Interface + +include::introduction.asciidoc[Introduction] + +include::listeners.asciidoc[Listeners] + +include::transports.asciidoc[Transports] + +include::protocols.asciidoc[Protocols] + +include::embedded.asciidoc[Embedded mode] + += How to + +include::parsers.asciidoc[Writing parsers] + +include::ssl_auth.asciidoc[SSL client authentication] + +include::connection_draining.asciidoc[Connection draining] + += Advanced + +include::internals.asciidoc[Internals] + += Additional information + +include::migrating_from_2.0.asciidoc[Migrating from Ranch 2.0 to 2.1] + +include::migrating_from_1.7.asciidoc[Migrating from Ranch 1.7 to 2.0] + +include::migrating_from_1.6.asciidoc[Migrating from Ranch 1.6 to 1.7] + +include::migrating_from_1.5.asciidoc[Migrating from Ranch 1.5 to 1.6] + +include::migrating_from_1.x.asciidoc[Migrating from Ranch 1.x] diff --git a/ranch/doc/src/guide/connection_draining.asciidoc b/ranch/doc/src/guide/connection_draining.asciidoc new file mode 100644 index 0000000..2ccdbc8 --- /dev/null +++ b/ranch/doc/src/guide/connection_draining.asciidoc @@ -0,0 +1,98 @@ +== Connection draining + +Stopping a Ranch listener via `ranch:stop_listener/1` will invariably kill +all connection processes the listener hosts. However, you may want to stop +a listener in a graceful fashion, ie by not accepting any new connections, +but allowing the existing connection processes to exit by themselves instead +of being killed. + +For this purpose, you should first suspend the listener you wish to +stop gracefully, and then wait for its connection count to drop to +zero. + +.Draining a single listener + +[source,erlang] +---- +ok = ranch:suspend_listener(Ref), +ok = ranch:wait_for_connections(Ref, '==', 0), +ok = ranch:stop_listener(Ref). +---- + +If you want to drain more than just one listener, it may be important to first suspend +them all before beginning to wait for their connection counts to reach zero. Otherwise, +the not yet suspended listeners will still be accepting connections while you wait for +the suspended ones to be drained. + +.Draining multiple listeners + +[source,erlang] +---- +lists:foreach( + fun (Ref) -> + ok = ranch:suspend_listener(Ref) + end, + Refs +), +lists:foreach( + fun (Ref) -> + ok = ranch:wait_for_connections(Ref, '==', 0), + ok = ranch:stop_listener(Ref) + end, + Refs +). +---- + +If you have long-running connection processes hosted by the listener you want to stop +gracefully, draining may take a long time, possibly forever. If you just want to give +the connection processes a chance to finish, but are not willing to wait for infinity, +the waiting part could be handled in a separate process. + +.Draining a listener with a timeout + +[source,erlang] +---- +ok = ranch:suspend_listener(Ref), +{DrainPid, DrainRef} = spawn_monitor( + fun () -> + ok = ranch:wait_for_connections(Ref, '==', 0) + end +), +receive + {'DOWN', DrainRef, process, DrainPid, _} -> + ok +after DrainTimeout -> + exit(DrainPid, kill), + ok +end, +ok = ranch:stop_listener(Ref). +---- + +To drain listeners automatically as part of your application shutdown routine, +use the `prep_stop/1` function of your application module. + +.Draining listeners automatically on application shutdown + +[source,erlang] +---- +-module(my_app). + +-behavior(application). + +-export([start/2]). +-export([prep_stop/1]). +-export([stop/1]). + +start(_StartType, _StartArgs) -> + {ok, _} = ranch:start_listener(my_listener, ranch_tcp, #{}, my_protocol, []), + my_app_sup:start_link(). + +prep_stop(State) -> + ok = ranch:suspend_listener(my_listener), + ok = ranch:wait_for_connections(my_listener, '==', 0), + ok = ranch:stop_listener(my_listener), + State. + +stop(_State) -> + ok. +---- diff --git a/ranch/doc/src/guide/embedded.asciidoc b/ranch/doc/src/guide/embedded.asciidoc new file mode 100644 index 0000000..28f567b --- /dev/null +++ b/ranch/doc/src/guide/embedded.asciidoc @@ -0,0 +1,47 @@ +== Embedded mode + +Embedded mode allows you to insert Ranch listeners directly +in your supervision tree. This allows for greater fault tolerance +control by permitting the shutdown of a listener due to the +failure of another part of the application and vice versa. + +However, just as for non-embedded listeners that were started via +`ranch:start_listener/5`, it is required that the `ranch` application +is running before you can start embedded listeners. Furthermore, +this also means that embedded listeners will restart when `ranch_sup` fails. + +WARNING: By using embedded mode, it is possible to start a listener with the same name +as an already existing listener. This will corrupt the information Ranch +keeps for that listener, so you should take care to ensure unique listener +names yourself. A good way to achieve this is by combining the embedded +listener's name with `?MODULE`, or the name of the application it is used +in. + +=== Embedding + +To embed Ranch in your application you can simply add the child specs +to your supervision tree. This can all be done in the `init/1` function +of one of your application supervisors. + +Ranch has a convenience function for getting the listeners child specs +called `ranch:child_spec/5`, that works like `ranch:start_listener/5`, +except that it doesn't start anything, it only returns child specs. + +The following example adds one listener to another application's +supervision tree. + +.Embed Ranch directly in your supervision tree + +[source,erlang] +---- +init([]) -> + ListenerSpec = ranch:child_spec({?MODULE, echo}, + ranch_tcp, #{socket_opts => [{port, 5555}]}, + echo_protocol, [] + ), + {ok, {#{}, [ListenerSpec]}}. +---- + +Embedded listeners cannot be stopped via `ranch:stop_listener/1`. Instead, +are to be stopped as part of the shutdown of your application's supervison +tree. diff --git a/ranch/doc/src/guide/internals.asciidoc b/ranch/doc/src/guide/internals.asciidoc new file mode 100644 index 0000000..600920f --- /dev/null +++ b/ranch/doc/src/guide/internals.asciidoc @@ -0,0 +1,99 @@ +== Internals + +This chapter may not apply to embedded Ranch as embedding allows you +to use an architecture specific to your application, which may or may +not be compatible with the description of the Ranch application. + +Note that for everything related to efficiency and performance, +you should perform the benchmarks yourself to get the numbers that +matter to you. Generic benchmarks found on the web may or may not +be of use to you, you can never know until you benchmark your own +system. + +A third party dive into the internals of Ranch is available should +you be interested: https://baozi.technology/ranch-under-the-hood/[Ranch: what's under the hood?] +We make no claims with regard to its freshness or accuracy but this +is a nice document to read along this section. + +=== Architecture + +Ranch is an OTP application. + +Like all OTP applications, Ranch has a top supervisor. It is responsible +for supervising the `ranch_server` process and all the listeners that +will be started. + +The `ranch_server` gen_server is a central process keeping track of the +listeners and their acceptors. It does so through the use of a public ets +table called `ranch_server`. The table is owned by the top supervisor +to improve fault tolerance. This way if the `ranch_server` gen_server +fails, it doesn't lose any information and the restarted process can +continue as if nothing happened. + +Ranch uses a custom supervisor for managing connections. This supervisor +keeps track of the number of connections and handles connection limits +directly. While it is heavily optimized to perform the task of creating +connection processes for accepted connections, it is still following the +OTP principles and the usual `sys` and `supervisor` calls will work on +it as expected. + +Listeners are grouped into the `ranch_listener_sup` supervisor and +consist of three kinds of processes: the listener gen_server, the +acceptor processes and the connection processes, both grouped under +their own supervisor. All of these processes are registered to the +`ranch_server` gen_server with varying amount of information. + +All socket operations, including listening for connections, go through +transport handlers. Accepted connections are given to the protocol handler. +Transport handlers are simple callback modules for performing operations on +sockets. Protocol handlers start a new process, which receives socket +ownership, with no requirements on how the code should be written inside +that new process. + +=== Number of acceptors + +The second argument to `ranch:start_listener/5` is the number of +processes that will be accepting connections. Care should be taken +when choosing this number. + +First of all, it should not be confused with the maximum number +of connections. Acceptor processes are only used for accepting and +have nothing else in common with connection processes. Therefore +there is nothing to be gained from setting this number too high, +in fact it can slow everything else down. + +Second, this number should be high enough to allow Ranch to accept +connections concurrently. But the number of cores available doesn't +seem to be the only factor for choosing this number, as we can +observe faster accepts if we have more acceptors than cores. It +might be entirely dependent on the protocol, however. + +Our observations suggest that using 100 acceptors on modern hardware +is a good solution, as it's big enough to always have acceptors ready +and it's low enough that it doesn't have a negative impact on the +system's performances. + +=== Platform-specific TCP features + +Some socket options are platform-specific and not supported by `inet`. +They can be of interest because they generally are related to +optimizations provided by the underlying OS. They can still be enabled +thanks to the `raw` option, for which we will see an example. + +One of these features is `TCP_DEFER_ACCEPT` on Linux. It is a simplified +accept mechanism which will wait for application data to come in before +handing out the connection to the Erlang process. + +This is especially useful if you expect many connections to be mostly +idle, perhaps part of a connection pool. They can be handled by the +kernel directly until they send any real data, instead of allocating +resources to idle connections. + +To enable this mechanism, the following option can be used. + +.Using raw transport options + +[source,erlang] +{raw, 6, 9, << 30:32/native >>} + +It means go on layer 6, turn on option 9 with the given integer parameter. diff --git a/ranch/doc/src/guide/introduction.asciidoc b/ranch/doc/src/guide/introduction.asciidoc new file mode 100644 index 0000000..a682c46 --- /dev/null +++ b/ranch/doc/src/guide/introduction.asciidoc @@ -0,0 +1,25 @@ +== Introduction + +Ranch is a socket acceptor pool for TCP protocols. + +Ranch aims to provide everything you need to accept TCP connections +with a small code base and low latency while being easy to use directly +as an application or to embed into your own. + +=== Prerequisites + +It is assumed the developer already knows Erlang and has some experience +with socket programming and TCP protocols. + +=== Supported platforms + +Ranch is tested and supported on Linux, FreeBSD, macOS and Windows. + +Ranch is developed for Erlang/OTP 22+. + +Ranch may be compiled on earlier Erlang versions with small source code +modifications but there is no guarantee that it will work as expected. + +=== Versioning + +Ranch uses http://semver.org/[Semantic Versioning 2.0.0] diff --git a/ranch/doc/src/guide/listeners.asciidoc b/ranch/doc/src/guide/listeners.asciidoc new file mode 100644 index 0000000..9ee2d98 --- /dev/null +++ b/ranch/doc/src/guide/listeners.asciidoc @@ -0,0 +1,479 @@ +== Listeners + +A listener is a set of processes whose role is to listen on a port +for new connections. It manages a pool of acceptor processes, each +of them indefinitely accepting connections. When it does, it starts +a new process executing the protocol handler code. All the socket +programming is abstracted through the use of transport handlers. + +The listener takes care of supervising all the acceptor and connection +processes, allowing developers to focus on building their application. + +=== Starting a listener + +Ranch does nothing by default. It is up to the application developer +to request that Ranch listens for connections. + +A listener can be started and stopped at will. + +When starting a listener, a number of different settings are required: + +* A name to identify it locally and be able to interact with it. +* A transport handler and its associated options. +* A protocol handler and its associated options. + +Ranch includes both TCP and SSL transport handlers, respectively +`ranch_tcp` and `ranch_ssl`. + +A listener can be started by calling the `ranch:start_listener/5` +function. Before doing so however, you must ensure that the `ranch` +application is started. + +.Starting the Ranch application + +[source,erlang] +ok = application:start(ranch). + +You are then ready to start a listener. Let's call it `tcp_echo`. It will +have a pool of 100 acceptors, use a TCP transport and forward connections +to the `echo_protocol` handler. + +.Starting a listener for TCP connections on port 5555 + +[source,erlang] +{ok, _} = ranch:start_listener(tcp_echo, + ranch_tcp, #{socket_opts => [{port, 5555}]}, + echo_protocol, [] +). + +You can try this out by compiling and running the `tcp_echo` example in the +examples directory. To do so, open a shell in the 'examples/tcp_echo/' +directory and run the following command: + +.Building and starting a Ranch example + +[source,bash] +$ make run + +You can then connect to it using telnet and see the echo server reply +everything you send to it. Then when you're done testing, you can use +the `Ctrl+]` key to escape to the telnet command line and type +`quit` to exit. + +.Connecting to the example listener with telnet + +[source,bash] +---- +$ telnet localhost 5555 +Trying 127.0.0.1... +Connected to localhost. +Escape character is '^]'. +Hello! +Hello! +It works! +It works! +^] + +telnet> quit +Connection closed. +---- + +=== Stopping a listener + +All you need to stop a Ranch listener is to call the +`ranch:stop_listener/1` function with the listener's name +as argument. In the previous section we started the listener +named `tcp_echo`. We can now stop it. + +.Stopping a listener + +[source,erlang] +ranch:stop_listener(tcp_echo). + +=== Suspending and resuming a listener + +Listeners can be suspended and resumed by calling +`ranch:suspend_listener/1` and `ranch:resume_listener/1`, +respectively, with the name of the listener as argument. + +Suspending a listener will cause it to stop listening and not accept +new connections, but existing connection processes will not be stopped. + +.Suspending a listener + +[source,erlang] +ranch:suspend_listener(tcp_echo). + +Resuming a listener will cause it to start listening and accept new +connections again. +It is worth mentioning, however, that if the listener is configured +to listen on a random port, it will listen on a different port than +before it was suspended. + +.Resuming a listener + +[source,erlang] +ranch:resume_listener(tcp_echo). + +Whether a listener is currently running or suspended can be queried +by calling `ranch:get_status/1` with the listener name as argument. + +=== Default transport options + +By default the socket will be set to return `binary` data, with the +options `{active, false}`, `{packet, raw}`, `{reuseaddr, true}` set. +These values can't be overridden when starting the listener, but +they can be overridden using `Transport:setopts/2` in the protocol. + +It will also set `{backlog, 1024}` and `{nodelay, true}`, which +can be overridden at listener startup. + +=== Listening on a random port + +You do not have to specify a specific port to listen on. If you give +the port number 0, or if you omit the port number entirely, Ranch will +start listening on a random port. + +You can retrieve this port number by calling `ranch:get_port/1`. The +argument is the name of the listener you gave in `ranch:start_listener/5`. + +.Starting a listener for TCP connections on a random port + +[source,erlang] +{ok, _} = ranch:start_listener(tcp_echo, + ranch_tcp, #{socket_opts => [{port, 0}]}, + echo_protocol, [] +). +Port = ranch:get_port(tcp_echo). + +=== Listening on privileged ports + +Some systems limit access to ports below 1024 for security reasons. +This can easily be identified by an `{error, eacces}` error when trying +to open a listening socket on such a port. + +The methods for listening on privileged ports vary between systems, +please refer to your system's documentation for more information. + +We recommend the use of port rewriting for systems with a single server, +and load balancing for systems with multiple servers. Documenting these +solutions is however out of the scope of this guide. + +=== Listening on a UNIX Domain socket + +On UNIX systems, it is also possible to use Ranch to listen on a UNIX +domain socket by specifying `{local, SocketFile}` for the `ip` socket +option. In this case, the port must be set to 0 or omitted. The given +file must not exist: Ranch must be able to create it. + +.Starting a listener for TCP connections on a UNIX Domain socket + +[source,erlang] +{ok, _} = ranch:start_listener(tcp_echo, + ranch_tcp, #{socket_opts => [ + {ip, {local, "/tmp/ranch_echo.sock"}}, + {port, 0} + ]}, echo_protocol, [] +). + +=== Performing additional setup steps on a listening socket + +If it is necessary to perform additional setup steps on the listening +socket, it is possible to specify a function with the transport option +`post_listen_callback`. This function will be called after the listening +socket has been created but before accepting connections on it, +with the socket as the single argument. + +The function must return either the atom `ok`, after which the listener +will start accepting connections on the socket, or a tuple +`{error, Reason}` which will cause the listener to fail starting with +`Reason`. + +.Setting permissions on a UNIX Domain socket file + +[source,erlang] +---- +PostListenCb = fun (Sock) -> + case ranch_tcp:sockname(Sock) of + {ok, {local, SockFile}} -> + file:change_mode(SockFile, 8#777); + {ok, _} -> + ok; + Error = {error, _} -> + Error + end +end, + +{ok, _} = ranch:start_listener(tcp_echo, + ranch_tcp, #{ + socket_opts => [ + {ip, {local, "/tmp/ranch_echo.sock"}}, + {port, 0}], + post_listen_callback => PostListenCb}, + echo_protocol, [] +). +---- + +=== Accepting connections on an existing socket + +If you want to accept connections on an existing socket, you can write +a custom `ranch_transport` implementation that fetches or otherwise +acquires a listening socket in the `listen/1` callback and returns it +in the form of `{ok, ListenSocket}`. + +The custom `listen/1` function must ensure that the listener process +(usually the process calling it) is also made the controlling process +of the socket it returns. Failing to do so will result in stop/start +and suspend/resume not working properly for that listener. + +=== Limiting the number of concurrent connections + +The `max_connections` transport option allows you to limit the number +of concurrent connections per connection supervisor (see below). +It defaults to 1024. Its purpose is to prevent your system from being +overloaded and ensuring all the connections are handled optimally. + +.Customizing the maximum number of concurrent connections + +[source,erlang] +{ok, _} = ranch:start_listener(tcp_echo, + ranch_tcp, #{socket_opts => [{port, 5555}], max_connections => 100}, + echo_protocol, [] +). + +You can disable this limit by setting its value to the atom `infinity`. + +.Disabling the limit for the number of connections + +[source,erlang] +{ok, _} = ranch:start_listener(tcp_echo, + ranch_tcp, #{socket_opts => [{port, 5555}], max_connections => infinity}, + echo_protocol, [] +). + +The maximum number of connections is a soft limit. In practice, it +can reach `max_connections` + the number of acceptors. + +When the maximum number of connections is reached, Ranch will stop +accepting connections. This will not result in further connections +being rejected, as the kernel option allows queueing incoming +connections. The size of this queue is determined by the `backlog` +option and defaults to 1024. Ranch does not know about the number +of connections that are in the backlog. + +You may not always want connections to be counted when checking for +`max_connections`. For example you might have a protocol where both +short-lived and long-lived connections are possible. If the long-lived +connections are mostly waiting for messages, then they don't consume +much resources and can safely be removed from the count. + +To remove the connection from the count, you must call the +`ranch:remove_connection/1` from within the connection process, +with the name of the listener as the only argument. + +.Removing a connection from the count of connections + +[source,erlang] +ranch:remove_connection(Ref). + +As seen in the chapter covering protocols, this reference is received +as the first argument of the protocol's `start_link/3` callback. + +You can modify the `max_connections` value on a running listener by +using the `ranch:set_max_connections/2` function, with the name of the +listener as first argument and the new value as the second. + +.Upgrading the maximum number of connections + +[source,erlang] +ranch:set_max_connections(tcp_echo, MaxConns). + +The change will occur immediately. + +=== Customizing the number of acceptor processes + +By default Ranch will use 10 acceptor processes. Their role is +to accept connections and spawn a connection process for every +new connection. + +This number can be tweaked to improve performance. A good +number is typically between 10 or 100 acceptors. You must +measure to find the best value for your application. + +.Specifying a custom number of acceptor processes + +[source,erlang] +{ok, _} = ranch:start_listener(tcp_echo, + ranch_tcp, #{socket_opts => [{port, 5555}], num_acceptors => 42}, + echo_protocol, [] +). + +=== Customizing the number of connection supervisors + +By default Ranch will use one connection supervisor for each +acceptor process (but not vice versa). Their task is to +supervise the connection processes started by an acceptor. +The number of connection supervisors can be tweaked. + +Note that the association between the individual acceptors and +connection supervisors is fixed, meaning that acceptors will +always use the same connection supervisor to start connection +processes. + +.Specifying a custom number of connection supervisors + +[source,erlang] +{ok, _} = ranch:start_listener(tcp_echo, + ranch_tcp, #{socket_opts => [{port, 5555}], num_conns_sups => 42}, + echo_protocol, [] +). + +=== Setting connection count alarms + +The `alarms` transport option allows you to configure alarms +which will be triggered when the number of connections tracked +by one connection supervisor reaches or exceeds the defined treshold. + +The `alarms` transport option takes a map with alarm names as keys and alarm +options as values. + +Any term is allowed as an alarm name. + +Alarm options include the alarm type and a treshold that, when reached, +triggers the given callback. A cooldown prevents the alarm from being +triggered too often. + +.Log warnings when the number of connections exceeds 100 + +[source,erlang] +---- +Alarms = #{ + my_alarm => #{ + type => num_connections, + treshold => 100, + callback => fun(Ref, Name, ConnSup, ConnPids]) -> + logger:warning("Warning (~s): " + "Supervisor ~s of listener ~s " + "has ~b connections", + [Name, Ref, ConnSup, length(ConnPids)]) + end + } +}, +{ok, _} = ranch:start_listener(tcp_echo, + ranch_tcp, #{alarms => Alarms, socket_opts => [{port, 5555}]}, + echo_protocol, [] +). +---- + +In the example code, an alarm named `my_alarm` is defined, which will +call the given function when the number of connections tracked +by the connection supervisor reaches or exceeds 100. When the number of +connections is still (or again) above 100 after the default cooldown +period of 5 seconds, the alarm will trigger again. + +=== When running out of file descriptors + +Operating systems have limits on the number of sockets +which can be opened by applications. When this maximum is +reached the listener can no longer accept new connections. The +accept rate of the listener will be automatically reduced, and a +warning message will be logged. + +---- +=ERROR REPORT==== 13-Jan-2016::12:24:38 === +Ranch acceptor reducing accept rate: out of file descriptors +---- + +If you notice messages like this you should increase the number +of file-descriptors which can be opened by your application. How +this should be done is operating-system dependent. Please consult +the documentation of your operating system. + +=== Using a supervisor for connection processes + +Ranch allows you to define the type of process that will be used +for the connection processes. By default it expects a `worker`. +When the `connection_type` configuration value is set to `supervisor`, +Ranch will consider that the connection process it manages is a +supervisor and will reflect that in its supervision tree. + +Connection processes of type `supervisor` can either handle the +socket directly or through one of their children. In the latter +case the start function for the connection process must return +two pids: the pid of the supervisor you created (that will be +supervised) and the pid of the protocol handling process (that +will receive the socket). + +Instead of returning `{ok, ConnPid}`, simply return +`{ok, SupPid, ConnPid}`. + +It is very important that the connection process be created +under the supervisor process so that everything works as intended. +If not, you will most likely experience issues when the supervised +process is stopped. + +=== Upgrading + +Ranch allows you to upgrade the protocol options. This takes effect +immediately and for all subsequent connections. + +To upgrade the protocol options, call `ranch:set_protocol_options/2` +with the name of the listener as first argument and the new options +as the second. + +.Upgrading the protocol options + +[source,erlang] +ranch:set_protocol_options(tcp_echo, NewOpts). + +All future connections will use the new options. + +You can also retrieve the current options similarly by +calling `ranch:get_protocol_options/1`. + +.Retrieving the current protocol options + +[source,erlang] +Opts = ranch:get_protocol_options(tcp_echo). + +=== Changing transport options + +Ranch allows you to change the transport options of a listener with +the `ranch:set_transport_options/2` function, for example to change the +number of acceptors or to make it listen on a different port. + +.Changing the transport options + +[source,erlang] +ranch:set_transport_options(tcp_echo, NewOpts). + +You can retrieve the current transport options by calling +`ranch:get_transport_options/1`. + +.Retrieving the current transport options + +[source,erlang] +Opts = ranch:get_transport_options(tcp_echo). + +=== Obtaining information about listeners + +Ranch provides two functions for retrieving information about the +listeners, for reporting and diagnostic purposes. + +The `ranch:info/0` function will return detailed information +about all listeners. + +.Retrieving detailed information +[source,erlang] +ranch:info(). + +The `ranch:procs/2` function will return all acceptor or listener +processes for a given listener. + +.Get all acceptor processes +[source,erlang] +ranch:procs(tcp_echo, acceptors). + +.Get all connection processes +[source,erlang] +ranch:procs(tcp_echo, connections). diff --git a/ranch/doc/src/guide/migrating_from_1.5.asciidoc b/ranch/doc/src/guide/migrating_from_1.5.asciidoc new file mode 100644 index 0000000..a454f93 --- /dev/null +++ b/ranch/doc/src/guide/migrating_from_1.5.asciidoc @@ -0,0 +1,76 @@ +[appendix] +== Migrating from Ranch 1.5 to 1.6 + +Ranch 1.6 added the ability to suspend and resume listeners. +It also deprecates a number of features and add interfaces +that will be used in Ranch 2.0. + +Ranch 1.6 is compatible with Erlang/OTP 18.0 onward. Support +for older releases has been removed. + +=== Features added + +* Listeners can now be suspended/resumed without stopping existing + connection processes. This effectively closes the listening socket + and stops the acceptor processes. + +* Transport options can now be updated for suspended listeners. + +* The `Socket` argument given when the protocol starts has been + deprecated. In Ranch 2.0 the socket will be obtainable only + by calling `ranch:handshake/1,2`. + +* Ranch-specific transport options and socket options are now + better separated. When passing Ranch-specific transport options, + Ranch now expects to receive a map, in which case socket + options are passed in the `socket_opts` value. When there + are only socket options they can be passed to Ranch directly + as a convenience. + +* Any future transport option will only be added to the map + type. This includes transport options added in this release. + +* The transport option `ack_timeout` was renamed to `handshake_timeout` + in the map type. + +* The `cacerts` socket option is now silenced in error logs + just like the `certs` and `key` options. + +* The manual has been heavily updated and now features one + manual page per function and module, complete with a per-function + changelog, examples and more. + +=== Experimental features added + +* It is now possible to configure the restart intensity for + `ranch_sup` using the OTP application environment. This + feature will remain undocumented unless there is popular + demand for it. + +* Add the transport option `logger` that allows configuring + which logger module will be used. The logger module must + follow the interface of the new `logger` module in Erlang/OTP 21, + or be set to `error_logger` to keep the old behavior. + +=== Changed behaviors + +* Transport modules must now implement `Transport:handshake/2,3` + which deprecates and will replace `Transport:accept_ack/1` in + Ranch 2.0. It returns a new socket and can therefore be used + for implementing TLS upgrade mechanisms. + +=== New functions + +* The functions `ranch:suspend_listener/1` and `ranch:resume_listener/1` + were added. In addition the function `ranch:get_state/1` can be used + to obtain the running state of a listener. + +* A function `ranch:wait_for_connections/3` was added. + +* A function `ranch:handshake/1,2` was added to replace the + function `ranch:accept_ack/1`. + +=== Bugs fixed + +* The specs for the function `Transport:sendfile/2,4,5` have been + corrected. The type used for the filename was too restricting. diff --git a/ranch/doc/src/guide/migrating_from_1.6.asciidoc b/ranch/doc/src/guide/migrating_from_1.6.asciidoc new file mode 100644 index 0000000..f0c32e8 --- /dev/null +++ b/ranch/doc/src/guide/migrating_from_1.6.asciidoc @@ -0,0 +1,46 @@ +[appendix] +== Migrating from Ranch 1.6 to 1.7 + +Ranch 1.7 adds built-in support for the PROXY protocol. + +The PROXY protocol is a simple and efficient way for proxies +to transmit information about the client. + +While a third-party library already existed, it was not +entirely compatible with the Ranch interface, in particular +when socket active mode was involved. This new implementation +fixes that and supports the full protocol with as little +overhead as possible compared to normal operations: just one +extra function call. + +Ranch 1.7 is compatible with Erlang/OTP 19.0 onward. Support +for Erlang/OTP 18 has been removed. + +=== Features added + +* Full support for the PROXY protocol was added. + +=== New functions + +* Add the function `ranch:recv_proxy_header/2` to receive + the PROXY protocol header and parse it. It must be called + before `ranch:handshake/1,2`. + +* Add the functions `ranch_proxy_header:parse/1` and + `ranch_proxy_header:header/1,2` to parse and build a + PROXY protocol header, respectively. + +=== Bugs fixed + +* Fix a race condition when the listener is restarted + after `ranch_listener_sup` crashes. This resulted in + the original options being used even if the options + were updated at runtime. + +* Make the acceptors exit instead of crash when the + listening socket has been closed to prevent + unnecessary logs. + +* Fix an issue where listener information would not get + cleaned up when an embedded listener was stopped. This + was fixed in Ranch 1.6.2. diff --git a/ranch/doc/src/guide/migrating_from_1.7.asciidoc b/ranch/doc/src/guide/migrating_from_1.7.asciidoc new file mode 100644 index 0000000..e9b1c96 --- /dev/null +++ b/ranch/doc/src/guide/migrating_from_1.7.asciidoc @@ -0,0 +1,163 @@ +[appendix] +== Migrating from Ranch 1.7+ to Ranch 2.0 + +Ranch 2.0 adds support for multiple connection supervisors. + +Ranch 1.x had a bottleneck because it used only a single +connection supervisor. This was more evident when many +connections were dropped at once as the supervisor couldn't +keep up and failed to accept new connections while cleaning +up the old ones. Ranch 2.0 behaves much better in this scenario +by default. Multiple connection supervisors also helps with +concurrently accepting new connections. + +Ranch 2.0 also adds experimental support for opening more +than one listening socket on a single port. + +Starting with Ranch 2.0 we are also providing a +https://github.com/juhlig/prometheus_ranch[Prometheus collector] +as a separate project as well as a +https://github.com/juhlig/prometheus_ranch/blob/master/dashboards/ranch-dashboard.json[Grafana dashboard]. + +Ranch 2.0 is compatible with Erlang/OTP 21.0 onward. Support +for Erlang/OTP 19 and 20 has been removed. + +=== Features added + +* Ranch now comes with a `ranch.appup` file necessary for + performing release upgrades. A test suite has been added + to confirm release upgrades work from one tag to the next. + Numerous fixes were made that will also improve error recovery. + Release upgrades will only be supported from Ranch 2.0 + onward. + +* The `num_conns_sups` option has been added. It allows + configuring the number of connection supervisors. It + now defaults to `num_accceptors`. The old behavior can + be obtained by setting this value to 1. + +* The `logger` option is no longer experimental. It now + defaults to `logger` instead of `error_logger`. + +* UNIX domain sockets are now supported. + +* The active N socket option is now supported. It requires + Erlang/OTP 21.3 or above for TLS, however. + +* Embedded listeners are now failing in a predictable + manner when `ranch_server` goes down. It is no longer + necessary to embed `ranch_sup` and the recommendation + is now to just start Ranch normally when using embedded + listeners. + +* Two steps handshake is now supported. This allows + obtaining TLS extensions and updating options before + resuming the handshake. The handshake can also be + canceled. + +=== Experimental features added + +* The experimental `num_listen_sockets` option has been + added. It allows opening more than one listening socket + per listener. It can only be used alongside the Linux + `SO_REUSEPORT` socket option or equivalent. It allows + working around a bottleneck in the kernel and maximizes + resource usage, leading to increased rates for accepting + new connections. + +=== Features removed + +* The `socket` option was removed. A more viable solution + is to define a custom transport module that returns a fresh + socket when `Transport:listen/1` is called. + +=== Changed behaviors + +* The callback function `Transport:listen/1` and its + implementations in `ranch_tcp` and `ranch_ssl` have changed + to accept a map of transport options instead of only + socket options. + +* The callback function `Transport:messages/0` return value + now includes the tag used for passive messages. + +* The `Socket` argument was removed from `Protocol:start_link/3`. + The socket must now be obtained by calling `ranch:handshake/1,2`. + +=== Added functions + +* The functions `ranch:handshake_continue/1,2` and + `ranch:handshake_cancel/1` can be used to perform + a two steps handshake. These functions may not be + supported by all transports. + +=== Changed functions + +* The `NumAcceptors` argument was removed from `ranch:start_listener/5` + and `ranch:child_spec/5` and moved to the transport options. + +* Ranch options can no longer be passed along with socket options + as a proplist. The only forms allowed are now the `ranch:opts()` + map or only socket options as-is. Individual transport options + are now validated as well. The `ranch:opts()` map must + be used when socket options also use a map. This applies to the + `ranch:start_listener/5`, `ranch:child_spec/5` and + `ranch:set_transport_options/2` functions. + +* The function `ranch:info/1,2` now returns a map containing + each listener's information rather than a list of key/values. + The key `num_acceptors` was removed as it can be found in the + transport options. + +* The function `ranch:set_transport_options/2` no longer requires + the listener to be suspended. Which options apply immediately, + on suspend/resume or on restart has been documented. Some work + has also been done to make these option changes more predictable. + +=== Removed functions + +* The function `ranch:accept_ack/1` has been removed in favor + of `ranch:handshake/1,2`. + +=== Bugs fixed + +* Calling `ranch:remove_connection/1` will now resume a sleeping + acceptor process when applicable. + +* Repeatedly calling `ranch:remove_connection/1` from a connection + process would crash the respective connection supervisor. This has + now been fixed. + +* When a connection process was failing to start, the socket was + not closed and this lead to leaking sockets. This is now corrected. + +=== Other changes + +* Connection draining has now been documented in the guide + following user feedback and discussions. + +* Ranch is now tested against https://concuerror.com/[Concuerror], + a model checking tool for debugging, testing and verifying + concurrent Erlang programs. Two tests have been added in this + release and more will follow in the future. + +* Ranch is now tested against `stampede`, a chaos monkey style + testing tool. Currently includes three scenarios: normal + TCP and TLS listeners and embedded TCP listener. This new + test suite helped uncover a misplaced `monitor/2` call + added during the development of Ranch 2.0 (we were using a + similar tool, `havoc`, at the time of finding that issue). + +* The supervisor for acceptors and the parent supervisor for + connection supervisors now have an adaptive restart + intensity limit set to `1 + ceil(math:log2(NumChildren))` + to allow room for errors when they have many children. + +* Ranch now uses stricter compiler options. Missing function + specs were added to internal modules. + +* Ranch now calls `ssl:handshake/1,2,3` instead of + `ssl:ssl_accept/1,2`. + +* The `ranch_ssl:ssl_opt()` type has been updated to conform + with Erlang/OTP 23.0. diff --git a/ranch/doc/src/guide/migrating_from_1.x.asciidoc b/ranch/doc/src/guide/migrating_from_1.x.asciidoc new file mode 100644 index 0000000..44babf1 --- /dev/null +++ b/ranch/doc/src/guide/migrating_from_1.x.asciidoc @@ -0,0 +1,70 @@ +[appendix] +== Migrating from Ranch 1.x + +The changelog for Ranch releases before 1.6 can be found +in this section. + +=== 1.5.0 + +* Add transport functions getopts/2, getstat/1 and getstat/2 +* Fix ranch:info/0 and ranch:procs/2 in embedded mode +* Prevent ranch_conns_sup from stopping on unexpected messages + +=== 1.4.0 + +* Add new transport option num_acceptor +* Deprecate ranch:start_listener/6 in favor of start_listener/5 +* Deprecate ranch:child_spec/6 in favor of child_spec/5 + +=== 1.3.0 + +The version numbers 1.3.1 and 1.3.2 were later made to fix +small mistakes made during the 1.3.0 release process. They +do not include code changes. + +* Tested with OTP R16B+ on Linux, FreeBSD, OSX and Windows +* Add ssl to the list of dependencies +* Add ranch:info/0 and ranch:procs/2 to retrieve Ranch state information +* Allow configuring a listener with only SNI, without a default certificate +* Blacklist transport options instead of whitelist +** Unknown options are now allowed, but will result in a Dialyzer warning +* Add many transport options typespecs and documentation +* Don't silently drop the accept rate when running out of fds +* Prevent a race condition when stopping listeners +* Improve reporting for common errors, for example eaddrinuse +* Fix double removal of connections bug +** The number of active connections should now be exact +* Fix stuck acceptor bug when controlling_socket returned errors +* Numerous documentation and examples improvements + +=== 1.2.1 + +* Fix bug preventing node shutdown when SSL is used with OTP 17.1+ +* Tune restart intensity in all supervisors + +=== 1.2.0 + +* Allow the supervised process and the process owning the socket to be different +* Add many transport options (please refer to the documentation) +* Add function ranch:get_addr/1 to retrieve both IP and port of listener +* Don't pass Ranch-specific options down to transports +** Should make Dialyzer happy in user projects +** New types ranch:opt(), ranch_tcp:opt(), ranch_ssl:ssl_opt() and ranch_ssl:opt() +* Fix crash when filtering unknown options out +* Print a warning for each option filtered out +* Handle Transport:controlling_socket/2 errors and close the socket +* Handle Protocol:start_link/4 crashes to avoid killing all active connections +* Use Asciidoc for documentation +* Test Ranch across 14 Erlang versions on CircleCI +* Improve and document test suites with recent ct_helper improvements +* Fix a number of intermittent test issues + +=== 1.1.0 + +* Add Transport:secure/0 +* Add SSL partial_chain option +* Stop reporting errors on {error, closed} in accept_ack + +=== 1.0.0 + +* Initial release diff --git a/ranch/doc/src/guide/migrating_from_2.0.asciidoc b/ranch/doc/src/guide/migrating_from_2.0.asciidoc new file mode 100644 index 0000000..2b4b192 --- /dev/null +++ b/ranch/doc/src/guide/migrating_from_2.0.asciidoc @@ -0,0 +1,70 @@ +[appendix] +== Migrating from Ranch 2.0 to Ranch 2.1 + +Ranch 2.1 adds counters and alarms. + +The https://github.com/juhlig/prometheus_ranch[Prometheus collector] +was updated to include accepted/terminated connections +metrics. + +Ranch 2.1 is compatible with Erlang/OTP 22.0 onward. Support +for Erlang/OTP 21 has been removed. + +=== Features added + +* Metrics are now provided by `ranch:info/0,1`. Currently + includes accepted/terminated connection counts per + connection supervisor. + +* Alarms can now be configured. The only alarm currently + available is `num_connections`. When the number of + connections goes over a configurable treshold Ranch + will call the given callback. This can be used to + programmatically shut down idle connections to + make up space for new connections, for example. + +* A `post_listen` callback option has been added. It + receives sockets immediately after the `Transport:listen/1` + call. It can be used for some additional initialization + of the socket, such as setting file permissions on + Unix domain sockets. + +* It is now possible to use TLS-PSK authentication + without having to specify a default certificate + for TLS < 1.3. + +=== Experimental features added + +* The `inet_backend` option is now properly handled + and tested for TCP listeners. This allows using + the experimental `socket` backend. The `socket` + backend is now tested with Ranch. Note that + there are known issues and Windows support is not + currently implemented. + +=== Changed behaviors + +* Ranch will now remove unsupported SSL/TLS options + where applicable. A warning will be logged when + this happens. Options are only removed when they + are not compatible with the selected TLS version + and leaving them would prevent the listener from + starting. ++ + The following options are removed when using TLS + 1.1, 1.2 or 1.3: `beast_mitigation` and `padding_check`. ++ + The following options are removed when using TLS + 1.3 exclusively: `client_renegotiation`, + `next_protocols_advertised`, `psk_identity`, + `reuse_session`, `reuse_sessions`, + `secure_renegotiate` and `user_lookup_fun`. + +=== Added functions + +* The function `ranch_proxy_header:to_connection_info/1` + converts PROXY protocol information to the same + format as `ssl:connection_information/1`. Because + there is little overlap only the `protocol`, + `selected_cipher_suite` and `sni_hostname` will + be available, however. diff --git a/ranch/doc/src/guide/parsers.asciidoc b/ranch/doc/src/guide/parsers.asciidoc new file mode 100644 index 0000000..7a9c5a5 --- /dev/null +++ b/ranch/doc/src/guide/parsers.asciidoc @@ -0,0 +1,92 @@ +== Writing parsers + +There are three kinds of protocols: + +* Text protocols +* Schema-less binary protocols +* Schema-based binary protocols + +This chapter introduces the first two kinds. It will not cover +more advanced topics such as continuations or parser generators. + +This chapter isn't specifically about Ranch, we assume here that +you know how to read data from the socket. The data you read and +the data that hasn't been parsed is saved in a buffer. Every +time you read from the socket, the data read is appended to the +buffer. What happens next depends on the kind of protocol. We +will only cover the first two. + +=== Parsing text + +Text protocols are generally line based. This means that we can't +do anything with them until we receive the full line. + +A simple way to get a full line is to use `binary:split/2,3`. + +.Using binary:split/2 to get a line of input + +[source,erlang] +case binary:split(Buffer, <<"\n">>) of + [_] -> + get_more_data(Buffer); + [Line, Rest] -> + handle_line(Line, Rest) +end. + +In the above example, we can have two results. Either there was +a line break in the buffer and we get it split into two parts, +the line and the rest of the buffer; or there was no line break +in the buffer and we need to get more data from the socket. + +Next, we need to parse the line. The simplest way is to again +split, here on space. The difference is that we want to split +on all spaces character, as we want to tokenize the whole string. + +.Using binary:split/3 to split text + +[source,erlang] +case binary:split(Line, <<" ">>, [global]) of + [<<"HELLO">>] -> + be_polite(); + [<<"AUTH">>, User, Password] -> + authenticate_user(User, Password); + [<<"QUIT">>, Reason] -> + quit(Reason) + %% ... +end. + +Pretty simple, right? Match on the command name, get the rest +of the tokens in variables and call the respective functions. + +After doing this, you will want to check if there is another +line in the buffer, and handle it immediately if any. +Otherwise wait for more data. + +=== Parsing binary + +Binary protocols can be more varied, although most of them are +pretty similar. The first four bytes of a frame tend to be +the size of the frame, which is followed by a certain number +of bytes for the type of frame and then various parameters. + +Sometimes the size of the frame includes the first four bytes, +sometimes not. Other times this size is encoded over two bytes. +And even other times little-endian is used instead of big-endian. + +The general idea stays the same though. + +.Using binary pattern matching to split frames + +[source,erlang] +<< Size:32, _/bits >> = Buffer, +case Buffer of + << Frame:Size/binary, Rest/bits >> -> + handle_frame(Frame, Rest); + _ -> + get_more_data(Buffer) +end. + +You will then need to parse this frame using binary pattern +matching, and handle it. Then you will want to check if there +is another frame fully received in the buffer, and handle it +immediately if any. Otherwise wait for more data. diff --git a/ranch/doc/src/guide/protocols.asciidoc b/ranch/doc/src/guide/protocols.asciidoc new file mode 100644 index 0000000..8f55cea --- /dev/null +++ b/ranch/doc/src/guide/protocols.asciidoc @@ -0,0 +1,113 @@ +== Protocols + +A protocol handler starts a connection process and defines the +protocol logic executed in this process. + +=== Writing a protocol handler + +All protocol handlers must implement the `ranch_protocol` behavior +which defines a single callback, `start_link/3`. This callback is +responsible for spawning a new process for handling the connection. +It receives three arguments: the name of the listener, the +transport handler being used and the protocol options defined in +the call to `ranch:start_listener/5`. This callback must +return `{ok, Pid}`, with `Pid` the pid of the new process. + +The newly started process can then freely initialize itself. However, +it must call `ranch:handshake/1,2` before doing any socket operation. +This will ensure the connection process is the owner of the socket. +It expects the listener's name as argument. + +.Perform the socket handshake + +[source,erlang] +{ok, Socket} = ranch:handshake(Ref). + +If your protocol code requires specific socket options, you should +set them while initializing your connection process, after +calling `ranch:handshake/1,2`. You can use `Transport:setopts/2` +for that purpose. + +Following is the complete protocol code for the example found +in `examples/tcp_echo/`. + +.Protocol module that echoes everything it receives + +[source,erlang] +---- +-module(echo_protocol). +-behaviour(ranch_protocol). + +-export([start_link/3]). +-export([init/3]). + +start_link(Ref, Transport, Opts) -> + Pid = spawn_link(?MODULE, init, [Ref, Transport, Opts]), + {ok, Pid}. + +init(Ref, Transport, _Opts = []) -> + {ok, Socket} = ranch:handshake(Ref), + loop(Socket, Transport). + +loop(Socket, Transport) -> + case Transport:recv(Socket, 0, 5000) of + {ok, Data} -> + Transport:send(Socket, Data), + loop(Socket, Transport); + _ -> + ok = Transport:close(Socket) + end. +---- + +=== Using gen_statem and gen_server + +Special processes like the ones that use the `gen_statem` or `gen_server` +behaviours have the particularity of having their `start_link` call not +return until the `init` function returns. This is problematic, because +you won't be able to call `ranch:handshake/1,2` from the `init` callback +as this would cause a deadlock to happen. + +This problem can be addressed in several ways. + +==== gen_statem + +* Use state enter calls and place the `ranch:handshake/1,2` call in the enter + clause of the initial state. Check the `tcp_reverse` example for a complete + example. +* Use a `next_event` action in the return from `init/1` and place the + `ranch:handshake/1,2` call in the clause handling the event in the initial + state. +* Use the `gen_statem:enter_loop/4` function and start your process with + `proc_lib:spawn_link/3` or `proc_lib:start_link/3,4,5`. See below for an + example. + +.Using gen_statem:enter_loop/4 to start a protocol + +[source,erlang] +---- +-module(my_protocol). +-behaviour(gen_statem). +-behaviour(ranch_protocol). + +-export([start_link/3]). +-export([init/1]). +%% Exports of other gen_statem callbacks here. + +start_link(Ref, Transport, Opts) -> + {ok, proc_lib:spawn_link(?MODULE, init, [{Ref, Transport, Opts}])}. + +init({Ref, Transport, _Opts}) -> + %% Perform any required state initialization here. + {ok, Socket} = ranch:handshake(Ref), + ok = Transport:setopts(Socket, [{active, once}]), + gen_statem:enter_loop(?MODULE, [], state_name, {state_data, Socket, Transport}). + +%% Other gen_statem callbacks here. +---- + +==== gen_server + +* Use `{continue, Continue}` in the return from `init/1` and place the + `ranch:handshake/1,2` call in a corresponding `handle_continue/2` clause. +* Use the `gen_server:enter_loop/3` function and start your process with + `proc_lib:spawn_link/3` or `proc_lib:start_link/3,4,5`. diff --git a/ranch/doc/src/guide/ssl_auth.asciidoc b/ranch/doc/src/guide/ssl_auth.asciidoc new file mode 100644 index 0000000..f4364d0 --- /dev/null +++ b/ranch/doc/src/guide/ssl_auth.asciidoc @@ -0,0 +1,120 @@ +== SSL client authentication + +=== Purpose + +SSL client authentication is a mechanism allowing applications to +identify certificates. This allows your application to make sure that +the client is an authorized certificate, but makes no claim about +whether the user can be trusted. This can be combined with a password +based authentication to attain greater security. + +The server only needs to retain the certificate serial number and +the certificate issuer to authenticate the certificate. Together, +they can be used to uniquely identify a certificate. + +As Ranch allows the same protocol code to be used for both SSL and +non-SSL transports, you need to make sure you are in an SSL context +before attempting to perform an SSL client authentication. This +can be done by checking the return value of `Transport:name/0`. + +=== Obtaining client certificates + +You can obtain client certificates from various sources. You can +generate them yourself, or you can use a service like CAcert.org +which allows you to generate client and server certificates for +free. + +Following are the steps you need to take to create a CAcert.org +account, generate a certificate and install it in your favorite +browser. + +* Open http://cacert.org in your favorite browser +* Root Certificate link: install both certificates +* Join (Register an account) +* Verify your account (check your email inbox!) +* Log in +* Client Certificates: New +* Follow instructions to create the certificate +* Install the certificate in your browser + +You can optionally save the certificate for later use, for example +to extract the `IssuerID` information as will be detailed later on. + +=== Transport configuration + +The SSL transport does not request a client certificate by default. +You need to specify the `{verify, verify_peer}` option when starting +the listener to enable this behavior. + +.Configure a listener for SSL authentication + +[source,erlang] +{ok, _} = ranch:start_listener(my_ssl, + ranch_ssl, #{socket_opts => [ + {port, SSLPort}, + {certfile, PathToCertfile}, + {cacertfile, PathToCACertfile}, + {verify, verify_peer} + ]}, + my_protocol, [] +). + +In this example we set the required `port` and `certfile`, but also +the `cacertfile` containing the CACert.org root certificate, and +the option to request the client certificate. + +If you enable the `{verify, verify_peer}` option and the client does +not have a client certificate configured for your domain, then no +certificate will be sent. This allows you to use SSL for more than +just authenticated clients. + +=== Authentication + +To authenticate users, you must first save the certificate information +required. If you have your users' certificate files, you can simply +load the certificate and retrieve the information directly. + +.Retrieve the issuer ID from a certificate + +[source,erlang] +---- +certfile_to_issuer_id(Filename) -> + {ok, Data} = file:read_file(Filename), + [{'Certificate', Cert, not_encrypted}] = public_key:pem_decode(Data), + {ok, IssuerID} = public_key:pkix_issuer_id(Cert, self), + IssuerID. +---- + +The `IssuerID` variable contains both the certificate serial number +and the certificate issuer stored in a tuple, so this value alone can +be used to uniquely identify the user certificate. You can save this +value in a database, a configuration file or any other place where an +Erlang term can be stored and retrieved. + +To retrieve the `IssuerID` from a running connection, you need to first +retrieve the client certificate and then extract this information from +it. Ranch does not provide a function to retrieve the client certificate. +Instead you can use the `ssl:peercert/1` function. Once you have the +certificate, you can again use the `public_key:pkix_issuer_id/2` to +extract the `IssuerID` value. + +The following function returns the `IssuerID` or `false` if no client +certificate was found. This snippet is intended to be used from your +protocol code. + +.Retrieve the issuer ID from the certificate for the current connection + +[source,erlang] +---- +socket_to_issuer_id(Socket) -> + case ssl:peercert(Socket) of + {error, no_peercert} -> + false; + {ok, Cert} -> + {ok, IssuerID} = public_key:pkix_issuer_id(Cert, self), + IssuerID + end. +---- + +You then only need to match the `IssuerID` value to authenticate the +user. diff --git a/ranch/doc/src/guide/transports.asciidoc b/ranch/doc/src/guide/transports.asciidoc new file mode 100644 index 0000000..73747fd --- /dev/null +++ b/ranch/doc/src/guide/transports.asciidoc @@ -0,0 +1,177 @@ +== Transports + +A transport defines the interface to interact with a socket. + +Transports can be used for connecting, listening and accepting +connections, but also for receiving and sending data. Both +passive and active mode are supported, although all sockets +are initialized as passive. + +=== TCP transport + +The TCP transport is a thin wrapper around `gen_tcp`. + +=== SSL transport + +The SSL transport is a thin wrapper around `ssl`. + +Ranch depends on `ssl` by default so any necessary +dependencies will start when Ranch is started. It is +possible to remove the dependency when the SSL transport +will not be used. Refer to your release build tool's +documentation for more information. + +When embedding Ranch listeners that have an SSL transport, +your application must depend on the `ssl` application for +proper behavior. + +=== Sending and receiving data + +This section assumes that `Transport` is a valid transport handler +(like `ranch_tcp` or `ranch_ssl`) and `Socket` is a connected +socket obtained through the listener. + +You can send data to a socket by calling the `Transport:send/2` +function. The data can be given as `iodata()`, which is defined as +`binary() | iolist()`. All the following calls will work: + +.Sending data to the socket + +[source,erlang] +---- +Transport:send(Socket, <<"Ranch is cool!">>). +Transport:send(Socket, "Ranch is cool!"). +Transport:send(Socket, ["Ranch", ["is", "cool!"]]). +Transport:send(Socket, ["Ranch", [<<"is">>, "cool!"]]). +---- + +You can receive data either in passive or in active mode. Passive mode +means that you will perform a blocking `Transport:recv/3` call, while +active mode means that you will receive the data as a message. + +By default, all data will be received as binary. It is possible to +receive data as strings, although this is not recommended as binaries +are a more efficient construct, especially for binary protocols. + +Receiving data using passive mode requires a single function call. The +first argument is the socket, and the third argument is a timeout duration +before the call returns with `{error, timeout}`. + +The second argument is the amount of data in bytes that we want to receive. +The function will wait for data until it has received exactly this amount. +If you are not expecting a precise size, you can specify 0 which will make +this call return as soon as data was read, regardless of its size. + +.Receiving data from the socket in passive mode + +[source,erlang] +{ok, Data} = Transport:recv(Socket, 0, 5000). + +Active mode requires you to inform the socket that you want to receive +data as a message and to write the code to actually receive it. + +There are three kinds of active modes: `{active, once}`, `{active, N}` +and `{active, true}`. The first will send a single message before going +back to passive mode; the second will send `N` messages followed by +a `Passive` message when switching back to passive mode; the third +will send messages indefinitely. We recommend not using the `{active, true}` +mode as it could quickly flood your process mailbox. It's better to keep +the data in the socket and read it only when required. + +Four different messages can be received: + +* Incoming data: `{OK, Socket, Data}` +* Socket closed: `{Closed, Socket}` +* Socket error: `{Error, Socket, Reason}` +* Switch to passive mode: `{Passive, Socket}` + +The value of `OK`, `Closed`, `Error` and `Passive` can be different +depending on the transport being used. To be able to properly match +on them you must first call the `Transport:messages/0` function. + +.Retrieving the transport's active message identifiers + +[source,erlang] +{OK, Closed, Error, Passive} = Transport:messages(). + +To start receiving messages you will need to call the `Transport:setopts/2` +function, and do so every time you want to receive data. + +.Receiving messages from the socket in active mode + +[source,erlang] +---- +{OK, Closed, Error, Passive} = Transport:messages(), +Transport:setopts(Socket, [{active, once}]), +receive + {OK, Socket, Data} -> + io:format("data received: ~p~n", [Data]); + {Closed, Socket} -> + io:format("socket got closed!~n"); + {Error, Socket, Reason} -> + io:format("error happened: ~p~n", [Reason]) +end. +---- + +You can easily integrate active sockets with existing Erlang code as all +you really need is just a few more clauses when receiving messages. + +=== Sending files + +As in the previous section it is assumed `Transport` is a valid transport +handler and `Socket` is a connected socket obtained through the listener. + +To send a whole file, with name `Filename`, over a socket: + +.Sending a file by filename + +[source,erlang] +{ok, SentBytes} = Transport:sendfile(Socket, Filename). + +Or part of a file, with `Offset` greater than or equal to 0, `Bytes` number of +bytes and chunks of size `ChunkSize`: + +.Sending part of a file by filename in chunks + +[source,erlang] +Opts = [{chunk_size, ChunkSize}], +{ok, SentBytes} = Transport:sendfile(Socket, Filename, Offset, Bytes, Opts). + +To improve efficiency when sending multiple parts of the same file it is also +possible to use a file descriptor opened in raw mode: + +.Sending a file opened in raw mode + +[source,erlang] +{ok, RawFile} = file:open(Filename, [raw, read, binary]), +{ok, SentBytes} = Transport:sendfile(Socket, RawFile, Offset, Bytes, Opts). + +=== Upgrading a TCP socket to SSL + +A connected TCP socket can be upgraded to a SSL socket via the function +`ranch_ssl:handshake/3`. The socket *must* be in `{active, false}` mode +before telling the client that the server is ready to upgrade in order +to avoid race conditions. + +IMPORTANT: The new socket received from `ranch_ssl:handshake/3` must be +used via the `ranch_ssl` transport. + +.Performing a TLS handshake on a TCP socket +[source,erlang] +{ok, SslSocket} = ranch_ssl:handshake(TcpSocket, SslOpts, 5000). + +=== Writing a transport handler + +A transport handler is a module implementing the `ranch_transport` behavior. +It defines a certain number of callbacks that must be written in order to +allow transparent usage of the transport handler. + +The behavior doesn't define the socket options available when opening a +socket. These do not need to be common to all transports as it's easy enough +to write different initialization functions for the different transports that +will be used. With one exception though. The `setopts/2` function *must* +implement the `{active, once}` and the `{active, true}` options. + +If the transport handler doesn't have a native implementation of `sendfile/5` a +fallback is available, `ranch_transport:sendfile/6`. The extra first argument +is the transport's module. See `ranch_ssl` for an example. diff --git a/ranch/doc/src/manual/ranch.asciidoc b/ranch/doc/src/manual/ranch.asciidoc new file mode 100644 index 0000000..14add7d --- /dev/null +++ b/ranch/doc/src/manual/ranch.asciidoc @@ -0,0 +1,211 @@ += ranch(3) + +== Name + +ranch - Socket acceptor pool + +== Description + +The module `ranch` provides functions for starting and +manipulating Ranch listeners. + +== Exports + +Start/stop: + +* link:man:ranch:start_listener(3)[ranch:start_listener(3)] - Start a listener +* link:man:ranch:stop_listener(3)[ranch:stop_listener(3)] - Stop a listener +* link:man:ranch:child_spec(3)[ranch:child_spec(3)] - Build child specifications for a new listener + +Suspend/resume: + +* link:man:ranch:suspend_listener(3)[ranch:suspend_listener(3)] - Suspend a running listener +* link:man:ranch:resume_listener(3)[ranch:resume_listener(3)] - Resume a suspended listener +* link:man:ranch:get_status(3)[ranch:get_status(3)] - Get a listener's running state + +Connections: + +* link:man:ranch:handshake(3)[ranch:handshake(3)] - Perform the transport handshake +* link:man:ranch:handshake_continue(3)[ranch:handshake_continue(3)] - Resume the paused transport handshake +* link:man:ranch:handshake_cancel(3)[ranch:handshake_cancel(3)] - Cancel the paused transport handshake +* link:man:ranch:recv_proxy_header(3)[ranch:recv_proxy_header(3)] - Receive the PROXY protocol header +* link:man:ranch:remove_connection(3)[ranch:remove_connection(3)] - Remove connection from the count + +Options: + +* link:man:ranch:get_max_connections(3)[ranch:get_max_connections(3)] - Get the max number of connections per connection supervisor +* link:man:ranch:get_protocol_options(3)[ranch:get_protocol_options(3)] - Get the current protocol options +* link:man:ranch:get_transport_options(3)[ranch:get_transport_options(3)] - Get the current transport options +* link:man:ranch:set_max_connections(3)[ranch:set_max_connections(3)] - Set the max number of connections per connection supervisor +* link:man:ranch:set_protocol_options(3)[ranch:set_protocol_options(3)] - Set the protocol options +* link:man:ranch:set_transport_options(3)[ranch:set_transport_options(3)] - Set the transport options + +Introspection: + +* link:man:ranch:get_addr(3)[ranch:get_addr(3)] - Get the listening address +* link:man:ranch:get_port(3)[ranch:get_port(3)] - Get the listening port +* link:man:ranch:info(3)[ranch:info(3)] - Overview of Ranch listeners +* link:man:ranch:procs(3)[ranch:procs(3)] - Retrieve pids from a listener +* link:man:ranch:wait_for_connections(3)[ranch:wait_for_connections(3)] - Wait for a specific number of connections + +== Types + +=== max_conns() + +[source,erlang] +---- +max_conns() = non_neg_integer() | infinity +---- + +Maximum number of connections allowed per connection supervisor. + +This is a soft limit. The actual number of connections +might be slightly above the limit due to concurrency +when accepting new connections. Some connections may +also be removed from this count explicitly by the user +code. + +=== opts() + +[source,erlang] +---- +opts() = any() | transport_opts(any()) +---- + +Transport or socket options. + +=== ref() + +[source,erlang] +---- +ref() = any() +---- + +Unique name used to refer to a listener. + +=== transport_opts(SocketOpts) + +[source,erlang] +---- +transport_opts(SocketOpts) = #{ + alarms => #{ + term() => #{ + type := num_connections, + treshold := non_neg_integer(), + callback := fun((ref(), term(), pid(), [pid()]) -> any()), + cooldown => non_neg_integer() + } + }, + connection_type => worker | supervisor, + handshake_timeout => timeout(), + max_connections => max_conns(), + logger => module(), + num_acceptors => pos_integer(), + num_conns_sups => pos_integer(), + post_listen_callback => fun((term()) -> ok | {error, term()}), + shutdown => timeout() | brutal_kill, + socket_opts => SocketOpts +} +---- + +Transport options. + +The transport options are a combination of Ranch-specific +options and transport-specific socket options. + +None of the options are required. + +alarms (#{}):: + +Alarms to call a function when the number of connections tracked +by one connection supervisor reaches or exceeds a defined treshold. ++ +The map keys are the alarm names, which can be any `term`. The +associated values are the respective alarm options, again in a map +with the following keys: + +type::: + +Must be set to `num_connections`. + +treshold::: + +Treshold value, which must be a `non_neg_integer`. When the +number of connections tracked by a single connection supervisor +reaches or exceeds this value, The alarm will trigger and call +the function defined in the `callback` key (see below). + +callback::: + +The alarm function, which takes the listener name, the alarm +name, the pid of the connection supervisor and a list of the pids +of all connection processes under that supervisor as arguments. +The return value is ignored. + +cooldown (5000)::: + +The minimum time after which the alarm can be triggered again, +in milliseconds. + +connection_type (worker):: + +Type of process that will handle the connection. + +handshake_timeout (5000):: + +Maximum allowed time for the `ranch:handshake/1,2` call to finish. + +logger (logger):: + +The module that will be used to write log messages. + +max_connections (1024):: + +Maximum number of active connections per connection supervisor. +Soft limit. Use `infinity` to disable the limit entirely. + +num_acceptors (10):: + +Number of processes that accept connections. + +num_conns_sups - see below:: + +Number of processes that supervise connection processes. +If not specified, defaults to be equal to `num_acceptors`. + +post_listen_callback (fun(_ListenSock) -> ok end):: + +A function which will be called after a listen socket has been successfully +created, with the socket as argument. It can be used to perform any +necessary setup steps on the socket. ++ +If the callback function returns `ok`, the listener will start accepting +connections on the socket. If it returns `{error, Reason}`, the listener +will fail to start. + +shutdown (5000):: + +Maximum allowed time for children to stop on listener shutdown. + +socket_opts:: + +Socket options to be used by `Transport:listen/1`. Please refer to the +documentation of the transport module you are using for more details. + +== Changelog + +* *2.0*: The type `transport_opts(SocketOpts)` was added. +* *2.0*: The function `ranch:accept_ack/1` was removed in favor of + link:man:ranch:handshake(3)[ranch:handshake(3)]. +* *2.0*: The option `max_connections` is now per connection supervisor. +* *2.0*: The `num_conns_sup` option was added. +* *2.0*: The `socket` option was removed. +* *2.0*: The `logger` option is no longer experimental. It now defaults + to `logger` instead of `error_logger`. +* *2.0*: The `opt()` type was removed. +* *1.6*: The experimental `logger` option was added. +* *1.6*: The `opt()` type was deprecated in favor of the new `opts()` type. + +== See also + +link:man:ranch(7)[ranch(7)] diff --git a/ranch/doc/src/manual/ranch.child_spec.asciidoc b/ranch/doc/src/manual/ranch.child_spec.asciidoc new file mode 100644 index 0000000..a904217 --- /dev/null +++ b/ranch/doc/src/manual/ranch.child_spec.asciidoc @@ -0,0 +1,106 @@ += ranch:child_spec(3) + +== Name + +ranch:child_spec - Build child specifications for a new listener + +== Description + +[source,erlang] +---- +child_spec(Ref :: ranch_ref(), + Transport :: module(), + TransOpts :: ranch:opts(), + Protocol :: module(), + ProtoOpts :: any()) + -> supervisor:child_spec() +---- + +Build child specifications for a new listener which can +be embedded directly in an application's supervision +tree. + +The actual listener is placed under a supervisor which +monitors `ranch_server` via a proxy process and will +restart the listener if `ranch_server` crashes. + +== Arguments + +Ref:: + +The listener name is used to refer to this listener in +future calls, for example when updating the configuration. ++ +It can be any Erlang term. An atom is generally good enough, +for example `api`, `my_app_clear` or `my_app_tls`. + +Transport:: + +The transport module that will be used by Ranch to accept +connections and that will be passed to the protocol module +along with the socket. ++ +The interface of the transport module is documented in the +link:man:ranch_transport(3)[ranch_transport(3)] manual. + +TransportOpts:: + +Transport options include the Ranch-specific options +and the socket options. The listener's port number must +be defined in the socket options. ++ +The available options for the built-in Ranch transports +are documented in the link:man:ranch_tcp(3)[ranch_tcp(3)] +and link:man:ranch_ssl(3)[ranch_ssl(3)] manuals. + +Protocol:: + +The protocol module that will be used by Ranch after +the connection has been accepted. ++ +The interface of the protocol module is documented in the +link:man:ranch_protocol(3)[ranch_protocol(3)] manual. + +ProtocolOpts:: + +The protocol options given when calling the protocol +module. Please consult the documentation of the protocol +module you are using for more details. + +== Return value + +Child specifications are returned. + +== Changelog + +* *2.0*: The actual listener is placed under a supervisor in order to + restart the listener if `ranch_server` crashes. +* *2.0*: The `TransOpts` argument must no longer contain + Ranch-specific options if given as a list. Use a map. +* *1.4*: The `NumAcceptors` argument was moved to the transport options. + +== Examples + +.Embed a listener +[source,erlang] +---- +-behavior(supervisor). + +init(_) -> + {ok, {#{strategy => one_for_one}, [ + ranch:child_spec(echo, + ranch_tcp, [{port, 5555}], + echo_protocol, [] + ) + ]}}. +---- + +== See also + +link:man:ranch:start_listener(3)[ranch:start_listener(3)], +link:man:ranch:stop_listener(3)[ranch:stop_listener(3)], +link:man:ranch(3)[ranch(3)], +link:man:ranch_tcp(3)[ranch_tcp(3)], +link:man:ranch_ssl(3)[ranch_ssl(3)], +link:man:ranch_transport(3)[ranch_transport(3)], +link:man:ranch_protocol(3)[ranch_protocol(3)] diff --git a/ranch/doc/src/manual/ranch.get_addr.asciidoc b/ranch/doc/src/manual/ranch.get_addr.asciidoc new file mode 100644 index 0000000..7c48b3c --- /dev/null +++ b/ranch/doc/src/manual/ranch.get_addr.asciidoc @@ -0,0 +1,59 @@ += ranch:get_addr(3) + +== Name + +ranch:get_addr - Get the listening address + +== Description + +[source,erlang] +---- +get_addr(Ref :: ranch:ref()) + -> {IP :: inet:ip_address(), + Port :: inet:port_number()} + | {local, SocketFile :: binary()} + | {undefined, undefined} +---- + +Get the listening address. + +== Arguments + +Ref:: + +The listener name. + +== Return value + +The address of the listener is returned as a tuple of the form +`{IP, Port}` when listening on a network interface, or +`{local, SocketFile}` when listening on a UNIX Domain socket. +When the listener is suspended, `{undefined, undefined}` will +be returned. + +The IP address is the IP of the network interface the +socket is bound to. + +The socket file is the path of a file on your system the +socket is bound to. + +== Examples + +.Get the listening port and IP +[source,erlang] +---- +{IP, Port} = ranch:get_addr(example). +---- + +.Get the listening UNIX Domain socket file +[source,erlang] +---- +{local, SocketFile} = ranch:get_addr(example). +---- + +== See also + +link:man:ranch:start_listener(3)[ranch:start_listener(3)], +link:man:ranch:get_port(3)[ranch:get_port(3)], +link:man:ranch:info(3)[ranch:info(3)], +link:man:ranch(3)[ranch(3)] diff --git a/ranch/doc/src/manual/ranch.get_max_connections.asciidoc b/ranch/doc/src/manual/ranch.get_max_connections.asciidoc new file mode 100644 index 0000000..d10c975 --- /dev/null +++ b/ranch/doc/src/manual/ranch.get_max_connections.asciidoc @@ -0,0 +1,45 @@ += ranch:get_max_connections(3) + +== Name + +ranch:get_max_connections - Get the max number of connections per connection supervisor + +== Description + +[source,erlang] +---- +get_max_connections(Ref :: ranch:ref()) + -> MaxConns :: ranch:max_conns() +---- + +Get the max number of connections per connection supervisor. + +== Arguments + +Ref:: + +The listener name. + +== Return value + +The maximum number of connections per connection supervisor +is returned. + +== Changelog + +* *2.0*: The maximum number of connections is now per connection supervisor. + +== Examples + +.Get the max number of connections per connection supervisor +[source,erlang] +---- +MaxConns = ranch:get_max_connections(example). +---- + +== See also + +link:man:ranch:get_protocol_options(3)[ranch:get_protocol_options(3)], +link:man:ranch:get_transport_options(3)[ranch:get_transport_options(3)], +link:man:ranch:set_max_connections(3)[ranch:set_max_connections(3)], +link:man:ranch(3)[ranch(3)] diff --git a/ranch/doc/src/manual/ranch.get_port.asciidoc b/ranch/doc/src/manual/ranch.get_port.asciidoc new file mode 100644 index 0000000..5c5563e --- /dev/null +++ b/ranch/doc/src/manual/ranch.get_port.asciidoc @@ -0,0 +1,48 @@ += ranch:get_port(3) + +== Name + +ranch:get_port - Get the listening port + +== Description + +[source,erlang] +---- +get_port(Ref :: ranch:ref()) + -> Port :: inet:port_number() | undefined +---- + +Get the listening port. + +This function is particularly useful to retrieve the +listening port number when it was not provided in the +options and was chosen randomly instead. + +== Arguments + +Ref:: + +The listener name. + +== Return value + +The listening port is returned. + +When the listener is suspended or using a UNIX Domain +socket instead of a network interface, `undefined` +will be returned. + +== Examples + +.Get the listening port +[source,erlang] +---- +Port = ranch:get_port(example). +---- + +== See also + +link:man:ranch:start_listener(3)[ranch:start_listener(3)], +link:man:ranch:get_addr(3)[ranch:get_addr(3)], +link:man:ranch:info(3)[ranch:info(3)], +link:man:ranch(3)[ranch(3)] diff --git a/ranch/doc/src/manual/ranch.get_protocol_options.asciidoc b/ranch/doc/src/manual/ranch.get_protocol_options.asciidoc new file mode 100644 index 0000000..b5b2a9e --- /dev/null +++ b/ranch/doc/src/manual/ranch.get_protocol_options.asciidoc @@ -0,0 +1,40 @@ += ranch:get_protocol_options(3) + +== Name + +ranch:get_protocol_options - Get the current protocol options + +== Description + +[source,erlang] +---- +get_protocol_options(Ref :: ranch:ref()) + -> ProtoOpts :: any() +---- + +Get the current protocol options. + +== Arguments + +Ref:: + +The listener name. + +== Return value + +The current protocol options are returned. + +== Examples + +.Get the current protocol options +[source,erlang] +---- +ProtoOpts = ranch:get_protocol_options(example). +---- + +== See also + +link:man:ranch:get_max_connections(3)[ranch:get_max_connections(3)], +link:man:ranch:get_transport_options(3)[ranch:get_transport_options(3)], +link:man:ranch:set_protocol_options(3)[ranch:set_protocol_options(3)], +link:man:ranch(3)[ranch(3)] diff --git a/ranch/doc/src/manual/ranch.get_status.asciidoc b/ranch/doc/src/manual/ranch.get_status.asciidoc new file mode 100644 index 0000000..eb45614 --- /dev/null +++ b/ranch/doc/src/manual/ranch.get_status.asciidoc @@ -0,0 +1,46 @@ += ranch:get_status(3) + +== Name + +ranch:get_status - Get a listener's running state + +== Description + +[source,erlang] +---- +get_status(Ref :: ranch_ref()) -> running | suspended +---- + +Get a listener's running state. + +== Arguments + +Ref:: + +The listener name. + +== Return value + +An atom is returned indicating the running status of the listener. + +== Changelog + +* *1.6*: Function introduced. + +== Examples + +.Get a listener's running state +[source,erlang] +---- +ranch:get_status(example). +---- + +== See also + +link:man:ranch:start_listener(3)[ranch:start_listener(3)], +link:man:ranch:stop_listener(3)[ranch:stop_listener(3)], +link:man:ranch:suspend_listener(3)[ranch:suspend_listener(3)], +link:man:ranch:resume_listener(3)[ranch:resume_listener(3)], +link:man:ranch:set_transport_options(3)[ranch:set_transport_options(3)], +link:man:ranch:wait_for_connections(3)[ranch:wait_for_connections(3)], +link:man:ranch(3)[ranch(3)] diff --git a/ranch/doc/src/manual/ranch.get_transport_options.asciidoc b/ranch/doc/src/manual/ranch.get_transport_options.asciidoc new file mode 100644 index 0000000..9b311da --- /dev/null +++ b/ranch/doc/src/manual/ranch.get_transport_options.asciidoc @@ -0,0 +1,40 @@ += ranch:get_transport_options(3) + +== Name + +ranch:get_transport_options - Get the current transport options + +== Description + +[source,erlang] +---- +get_transport_options(Ref :: ranch:ref()) + -> TransOpts :: ranch:transport_opts(any()) +---- + +Get the current transport options. + +== Arguments + +Ref:: + +The listener name. + +== Return value + +The current transport options are returned. + +== Examples + +.Get the current transport options +[source,erlang] +---- +TransOpts = ranch:get_transport_options(example). +---- + +== See also + +link:man:ranch:get_max_connections(3)[ranch:get_max_connections(3)], +link:man:ranch:get_protocol_options(3)[ranch:get_protocol_options(3)], +link:man:ranch:set_transport_options(3)[ranch:set_transport_options(3)], +link:man:ranch(3)[ranch(3)] diff --git a/ranch/doc/src/manual/ranch.handshake.asciidoc b/ranch/doc/src/manual/ranch.handshake.asciidoc new file mode 100644 index 0000000..508abe5 --- /dev/null +++ b/ranch/doc/src/manual/ranch.handshake.asciidoc @@ -0,0 +1,84 @@ += ranch:handshake(3) + +== Name + +ranch:handshake - Perform the transport handshake + +== Description + +[source,erlang] +---- +handshake(Ref) -> {ok, Socket} | {continue, Info} +handshake(Ref, Opts) -> {ok, Socket} | {continue, Info} + +Ref :: ranch:ref() +Opts :: any() +Socket :: any() +Info :: any() +---- + +Perform the transport handshake. + +This function must be called by the protocol process in order +to retrieve the socket for the connection. Ranch performs the +handshake necessary to give control of the socket to this +process and also does the transport handshake, for example +setting up the TLS connection. + +== Arguments + +Ref:: + +The listener name. + +Opts:: + +Transport handshake options. ++ +Allowed options depend on the transport module. + +== Return value + +An `ok` tuple is returned containing the socket for the connection +by default. + +Depending on configuration, a `continue` tuple can otherwise +be returned when the handshake operation is paused. It contains +data provided by the transport that can be used to inform further +decisions before resuming the handshake, for example to provide +new transport options. The handshake can be resumed using +link:man:ranch:handshake_continue(3)[ranch:handshake_continue(3)] +or canceled using +link:man:ranch:handshake_cancel(3)[ranch:handshake_cancel(3)]. + +This function will trigger an exception when an error occurs. + +== Changelog + +* *2.0*: The `continue` tuple can now be returned. +* *1.6*: Function introduced. Replaces `ranch:accept_ack/1`. + +== Examples + +.Initialize the connection process +[source,erlang] +---- +start_link(Ref, Transport, Opts) -> + Pid = proc_lib:spawn_link(?MODULE, init, + [Ref, Transport, Opts]), + {ok, Pid}. + +init(Ref, Transport, Opts) -> + {ok, Socket} = ranch:handshake(Ref), + loop(#state{ref=Ref, socket=Socket, + transport=Transport, opts=Opts}). +---- + +== See also + +link:man:ranch:start_listener(3)[ranch:start_listener(3)], +link:man:ranch:handshake_continue(3)[ranch:handshake_continue(3)], +link:man:ranch:handshake_cancel(3)[ranch:handshake_cancel(3)], +link:man:ranch:recv_proxy_header(3)[ranch:recv_proxy_header(3)], +link:man:ranch:remove_connection(3)[ranch:remove_connection(3)], +link:man:ranch(3)[ranch(3)] diff --git a/ranch/doc/src/manual/ranch.handshake_cancel.asciidoc b/ranch/doc/src/manual/ranch.handshake_cancel.asciidoc new file mode 100644 index 0000000..6f742f5 --- /dev/null +++ b/ranch/doc/src/manual/ranch.handshake_cancel.asciidoc @@ -0,0 +1,55 @@ += ranch:handshake_cancel(3) + +== Name + +ranch:handshake_cancel - Cancel the paused transport handshake + +== Description + +[source,erlang] +---- +handshake_cancel(Ref :: ranch:ref()) -> ok +---- + +Cancel the paused transport handshake. + +This function may be called by the protocol process +to cancel a paused handshake. + +== Arguments + +Ref:: + +The listener name. ++ +Allowed options depend on the transport module. + +== Return value + +The return value depends on the transport module. + +== Changelog + +* *2.0*: Function introduced. + +== Examples + +.Cancel a paused transport handshake +[source,erlang] +---- +start_link(Ref, Transport, Opts) -> + Pid = proc_lib:spawn_link(?MODULE, init, + [Ref, Transport, Opts]), + {ok, Pid}. + +init(Ref, Transport, Opts) -> + {continue, _Info} = ranch:handshake(Ref), + ranch:handshake_cancel(Ref), + exit(handshake_cancelled). +---- + +== See also + +link:man:ranch:handshake(3)[ranch:handshake(3)], +link:man:ranch:handshake_continue(3)[ranch:handshake_continue(3)], +link:man:ranch(3)[ranch(3)] diff --git a/ranch/doc/src/manual/ranch.handshake_continue.asciidoc b/ranch/doc/src/manual/ranch.handshake_continue.asciidoc new file mode 100644 index 0000000..4fb08bf --- /dev/null +++ b/ranch/doc/src/manual/ranch.handshake_continue.asciidoc @@ -0,0 +1,67 @@ += ranch:handshake_continue(3) + +== Name + +ranch:handshake_continue - Resume the paused transport handshake + +== Description + +[source,erlang] +---- +handshake_continue(Ref) -> {ok, Socket} +handshake_continue(Ref, Opts) -> {ok, Socket} + +Ref :: ranch:ref() +Opts :: any() +Socket :: any() +---- + +Resume the paused transport handshake. + +This function must be called by the protocol process in order +to resume a paused handshake. + +== Arguments + +Ref:: + +The listener name. + +Opts:: + +Transport handshake options. ++ +Allowed options depend on the transport module. + +== Return value + +An `ok` tuple is returned containing the socket for the connection. + +This function will trigger an exception when an error occurs. + +== Changelog + +* *2.0*: Function introduced. + +== Examples + +.Continue a paused transport handshake +[source,erlang] +---- +start_link(Ref, Transport, Opts) -> + Pid = proc_lib:spawn_link(?MODULE, init, + [Ref, Transport, Opts]), + {ok, Pid}. + +init(Ref, Transport, Opts) -> + {continue, _Info} = ranch:handshake(Ref), + {ok, Socket} = ranch:handshake_continue(Ref), + loop(#state{ref=Ref, socket=Socket, + transport=Transport, opts=Opts}). +---- + +== See also + +link:man:ranch:handshake(3)[ranch:handshake(3)], +link:man:ranch:handshake_cancel(3)[ranch:handshake_cancel(3)], +link:man:ranch(3)[ranch(3)] diff --git a/ranch/doc/src/manual/ranch.info.asciidoc b/ranch/doc/src/manual/ranch.info.asciidoc new file mode 100644 index 0000000..6404fcc --- /dev/null +++ b/ranch/doc/src/manual/ranch.info.asciidoc @@ -0,0 +1,75 @@ += ranch:info(3) + +== Name + +ranch:info - Overview of Ranch listeners + +== Description + +[source,erlang] +---- +info() -> #{Ref := Info} +info(Ref) -> Info + +Info :: #{Key :: atom() := Value :: any()} +---- + +Overview of Ranch listeners. + +== Arguments + +Ref:: + +The listener name. + +== Return value + +Returns detailed information about one or all +Ranch listeners. The following keys are returned: + +pid:: Pid of the listener's top-level supervisor. +status:: Listener status, either running or suspended. +ip:: Interface Ranch listens on. +port:: Port number Ranch listens on. +max_connections:: Maximum number of connections per connection supervisor. +active_connections:: Number of active connections. +all_connections:: Number of connections, including those removed from the count. +transport:: Transport module. +transport_options:: Transport options. +protocol:: Protocol module. +protocol_options:: Protocol options. +metrics:: Listener metrics. + +== Metrics + +Listener metrics are provided as a map, with the following keys: + +{conns_sup, Index, accept}:: Number of accepted connections, per connection supervisor. +{conns_sup, Index, terminate}:: Number of terminated connection processes, per connection supervisor. + +== Changelog + +* *2.1*: Added accept/terminate metrics to the output of `ranch:info/0,1`. +* *2.0*: The listener info is now returned as a map. +* *2.0*: The `num_acceptors` key has been removed. + +== Examples + +.Get information about all listeners +[source,erlang] +---- +AllInfo = ranch:info(). +---- + +.Get information about a specific listener +[source,erlang] +---- +Info = ranch:info(example). +---- + +== See also + +link:man:ranch:get_addr(3)[ranch:get_addr(3)], +link:man:ranch:get_port(3)[ranch:get_port(3)], +link:man:ranch:procs(3)[ranch:procs(3)], +link:man:ranch(3)[ranch(3)] diff --git a/ranch/doc/src/manual/ranch.procs.asciidoc b/ranch/doc/src/manual/ranch.procs.asciidoc new file mode 100644 index 0000000..0f29533 --- /dev/null +++ b/ranch/doc/src/manual/ranch.procs.asciidoc @@ -0,0 +1,51 @@ += ranch:procs(3) + +== Name + +ranch:procs - Retrieve pids from a listener + +== Description + +[source,erlang] +---- +procs(Ref :: ranch:ref(), + Type :: acceptors | connections) + -> Pids :: [pid()] +---- + +Retrieve pids from a listener. + +== Arguments + +Ref:: + +The listener name. + +Type:: + +The type of process that will be returned. + +== Return value + +A list of pids is returned. + +== Examples + +.Get the pids of the acceptor processes +[source,erlang] +---- +Pids = ranch:procs(acceptors). +---- + +.Get the pids of the connection processes +[source,erlang] +---- +Pids = ranch:procs(connections). +---- + +== See also + +link:man:ranch:get_addr(3)[ranch:get_addr(3)], +link:man:ranch:get_port(3)[ranch:get_port(3)], +link:man:ranch:info(3)[ranch:info(3)], +link:man:ranch(3)[ranch(3)] diff --git a/ranch/doc/src/manual/ranch.recv_proxy_header.asciidoc b/ranch/doc/src/manual/ranch.recv_proxy_header.asciidoc new file mode 100644 index 0000000..0536e3d --- /dev/null +++ b/ranch/doc/src/manual/ranch.recv_proxy_header.asciidoc @@ -0,0 +1,71 @@ += ranch:recv_proxy_header(3) + +== Name + +ranch:recv_proxy_header - Receive the PROXY protocol header + +== Description + +[source,erlang] +---- +recv_proxy_header(ranch:ref(), timeout()) + -> {ok, ranch_proxy_header:proxy_info()} + | {error, Reason :: atom()} + | {error, protocol_error, HumanReadable :: atom()} +---- + +Receive the PROXY protocol header. + +This function must be called before `ranch:handshake/1,2` +on newly accepted connections to read and parse the PROXY +protocol header, if any. + +== Arguments + +Ref:: + +The listener name. + +Timeout:: + +Receive timeout in milliseconds. + +== Return value + +An `ok` tuple is returned containing PROXY header information +on success. + +An `error` 2-tuple is returned when a socket error occurs. + +An `error` 3-tuple is returned when a protocol error occurs +and Ranch was not able to parse the PROXY header information. +The third element contains a human-readable description of +the error. + +== Changelog + +* *1.7*: Function introduced. + +== Examples + +.Receive the PROXY protocol header +[source,erlang] +---- +start_link(Ref, Transport, Opts) -> + Pid = proc_lib:spawn_link(?MODULE, init, + [Ref, Transport, Opts]), + {ok, Pid}. + +init(Ref, Transport, Opts) -> + {ok, ProxyInfo} = ranch:recv_proxy_header(Ref, 1000), + {ok, Socket} = ranch:handshake(Ref), + loop(#state{ref=Ref, socket=Socket, transport=Transport, + proxy_info=ProxyInfo, opts=Opts}). +---- + +== See also + +link:man:ranch:start_listener(3)[ranch:start_listener(3)], +link:man:ranch:handshake(3)[ranch:handshake(3)], +link:man:ranch:remove_connection(3)[ranch:remove_connection(3)], +link:man:ranch(3)[ranch(3)] diff --git a/ranch/doc/src/manual/ranch.remove_connection.asciidoc b/ranch/doc/src/manual/ranch.remove_connection.asciidoc new file mode 100644 index 0000000..59f79f1 --- /dev/null +++ b/ranch/doc/src/manual/ranch.remove_connection.asciidoc @@ -0,0 +1,46 @@ += ranch:remove_connection(3) + +== Name + +ranch:remove_connection - Remove connection from the count + +== Description + +[source,erlang] +---- +remove_connection(Ref :: ranch:ref()) -> ok +---- + +Remove connection from the count. + +This connection will no longer be included in the count when +limiting the number of connections. This can be useful in a +mixed environment where some connections are active and others +are passive. Passive connections spend most of their time idling +and are not consuming much resources. + +This function may only be called from a connection process. + +== Arguments + +Ref:: + +The listener name. + +== Return value + +The atom `ok` is always returned. It can be safely ignored. + +== Examples + +.Remove the connection process from the count +[source,erlang] +---- +ranch:remove_connection(example). +---- + +== See also + +link:man:ranch:start_listener(3)[ranch:start_listener(3)], +link:man:ranch:handshake(3)[ranch:handshake(3)], +link:man:ranch(3)[ranch(3)] diff --git a/ranch/doc/src/manual/ranch.resume_listener.asciidoc b/ranch/doc/src/manual/ranch.resume_listener.asciidoc new file mode 100644 index 0000000..f3a9f84 --- /dev/null +++ b/ranch/doc/src/manual/ranch.resume_listener.asciidoc @@ -0,0 +1,57 @@ += ranch:resume_listener(3) + +== Name + +ranch:resume_listener - Resume a suspended listener + +== Description + +[source,erlang] +---- +resume_listener(Ref :: ranch_ref()) + -> ok | {error, any()} +---- + +Resume a suspended listener. + +Ranch will start listening for and accepting connections +again. The function +link:man:ranch:set_transport_options(3)[ranch:set_transport_options(3)] +can be used to change the transport options before resuming +the listener. + +Nothing is done when the listener is already running. + +== Arguments + +Ref:: + +The listener name. + +== Return value + +The atom `ok` is returned on success. + +An error tuple is returned when the listener could not be restarted. + +== Changelog + +* *1.6*: Function introduced. + +== Examples + +.Resume a listener +[source,erlang] +---- +ok = ranch:resume_listener(example). +---- + +== See also + +link:man:ranch:start_listener(3)[ranch:start_listener(3)], +link:man:ranch:stop_listener(3)[ranch:stop_listener(3)], +link:man:ranch:suspend_listener(3)[ranch:suspend_listener(3)], +link:man:ranch:get_status(3)[ranch:get_status(3)], +link:man:ranch:set_transport_options(3)[ranch:set_transport_options(3)], +link:man:ranch:wait_for_connections(3)[ranch:wait_for_connections(3)], +link:man:ranch(3)[ranch(3)] diff --git a/ranch/doc/src/manual/ranch.set_max_connections.asciidoc b/ranch/doc/src/manual/ranch.set_max_connections.asciidoc new file mode 100644 index 0000000..cc1bb20 --- /dev/null +++ b/ranch/doc/src/manual/ranch.set_max_connections.asciidoc @@ -0,0 +1,54 @@ += ranch:set_max_connections(3) + +== Name + +ranch:set_max_connections - Set the max number of connections per connection supervisor + +== Description + +[source,erlang] +---- +set_max_connections(Ref :: ranch:ref(), + MaxConns :: ranch:max_conns()) + -> ok +---- + +Set the max number of connections per connection supervisor. + +The change will be applied immediately. If the new value is +smaller than the previous one, Ranch will wait for the extra +connections to terminate and will not accept new connections +until the number of connections goes below the limit. + +== Arguments + +Ref:: + +The listener name. + +MaxConns:: + +The new maximum number of connections per connection supervisor. + +== Return value + +The atom `ok` is always returned. It can be safely ignored. + +== Changelog + +* *2.0*: The maximum number of connections is now per connection supervisor. + +== Examples + +.Set the max number of connections per connection supervisor +[source,erlang] +---- +ranch:set_max_connections(example, 10000). +---- + +== See also + +link:man:ranch:get_max_connections(3)[ranch:get_max_connections(3)], +link:man:ranch:set_protocol_options(3)[ranch:set_protocol_options(3)], +link:man:ranch:set_transport_options(3)[ranch:set_transport_options(3)], +link:man:ranch(3)[ranch(3)] diff --git a/ranch/doc/src/manual/ranch.set_protocol_options.asciidoc b/ranch/doc/src/manual/ranch.set_protocol_options.asciidoc new file mode 100644 index 0000000..36370c8 --- /dev/null +++ b/ranch/doc/src/manual/ranch.set_protocol_options.asciidoc @@ -0,0 +1,61 @@ += ranch:set_protocol_options(3) + +== Name + +ranch:set_protocol_options - Set the protocol options + +== Description + +[source,erlang] +---- +set_protocol_options(Ref :: ranch:ref(), + ProtoOpts :: any()) + -> ok +---- + +Set the protocol options. + +The change will be applied immediately for all new connections. +Old connections will not receive the new options. + +Note that the complete set of protocol options is replaced. To update a subset +of the options, it is recommended to get the current protocol options using +link:man:ranch:get_protocol_options(3)[ranch:get_protocol_options(3)], update +them and then set them back using this function. + +== Arguments + +Ref:: + +The listener name. + +ProtoOpts:: + +The new protocol options. + +== Return value + +The atom `ok` is always returned. It can be safely ignored. + +== Examples + +.Set the protocol options +[source,erlang] +---- +ranch:set_protocol_options(example, ProtoOpts). +---- + +.Update some of the protocol options +[source,erlang] +---- +ProtoOpts0 = ranch:get_protocol_options(example), +ProtoOpts = ProtoOpts0#{request_timeout => 2000}, +ranch:set_protocol_options(example, ProtoOpts). +---- + +== See also + +link:man:ranch:get_protocol_options(3)[ranch:get_protocol_options(3)], +link:man:ranch:set_max_connections(3)[ranch:set_max_connections(3)], +link:man:ranch:set_transport_options(3)[ranch:set_transport_options(3)], +link:man:ranch(3)[ranch(3)] diff --git a/ranch/doc/src/manual/ranch.set_transport_options.asciidoc b/ranch/doc/src/manual/ranch.set_transport_options.asciidoc new file mode 100644 index 0000000..125d037 --- /dev/null +++ b/ranch/doc/src/manual/ranch.set_transport_options.asciidoc @@ -0,0 +1,97 @@ += ranch:set_transport_options(3) + +== Name + +ranch:set_transport_options - Set the transport options + +== Description + +[source,erlang] +---- +set_transport_options(Ref :: ranch:ref(), + TransOpts :: ranch:opts()) + -> ok | {error, Reason :: term()} +---- + +Set the transport options. + +The complete set of transport options is replaced. To update a subset of the +transport options, it is recommended to get the current transport options using +link:man:ranch:get_transport_options(3)[ranch:get_transport_options(3)], update +them and then set them back using this function. + +Changes to the following options will take effect... + +* immediately: +** `max_connections` +** `handshake_timeout` +** `shutdown` +* only after the listener has been suspended and resumed: +** `num_acceptors` +** `num_listen_sockets` +** `post_listen_callback` +** `socket_opts` +* only when the entire listener is restarted: +** `connection_type` +** `num_conns_sups` +** `logger` + +== Arguments + +Ref:: + +The listener name. + +TransOpts:: + +The new transport options. + +== Return value + +The atom `ok` is returned on success. + +An error tuple is returned on failure, for example if the given +transport options contain invalid values. + +== Changelog + +* *2.0*: The restriction that the listener must be suspended + has been removed. +* *2.0*: The `TransOpts` argument must no longer contain + Ranch-specific options if given as a list. Use a map. + +== Examples + +.Set the transport options +[source,erlang] +---- +Ref = example, + +ok = ranch:suspend_listener(Ref), +ok = ranch:set_transport_options(Ref, TransOpts), +ok = ranch:resume_listener(Ref). +---- + +.Update the listener TCP port within the `socket_opts` transport option +[source,erlang] +---- +Ref = example, + +TransOpts0 = ranch:get_transport_options(Ref), +#{socket_opts = SocketOpts0} = TransOpts0, +SocketOpts = [{port, 12345}|proplists:delete(port, SocketOpts0)], +TransOpts = TransOpts0#{socket_opts = SocketOpts}, + +ok = ranch:suspend_listener(Ref), +ok = ranch:set_transport_options(Ref, TransOpts), +ok = ranch:resume_listener(Ref). +---- + +== See also + +link:man:ranch:suspend_listener(3)[ranch:suspend_listener(3)], +link:man:ranch:resume_listener(3)[ranch:resume_listener(3)], +link:man:ranch:get_transport_options(3)[ranch:get_transport_options(3)], +link:man:ranch:set_max_connections(3)[ranch:set_max_connections(3)], +link:man:ranch:set_protocol_options(3)[ranch:set_protocol_options(3)], +link:man:ranch(3)[ranch(3)] diff --git a/ranch/doc/src/manual/ranch.start_listener.asciidoc b/ranch/doc/src/manual/ranch.start_listener.asciidoc new file mode 100644 index 0000000..eebcbeb --- /dev/null +++ b/ranch/doc/src/manual/ranch.start_listener.asciidoc @@ -0,0 +1,133 @@ += ranch:start_listener(3) + +== Name + +ranch:start_listener - Start a listener + +== Description + +[source,erlang] +---- +start_listener(Ref :: ranch_ref(), + Transport :: module(), + TransOpts :: ranch:opts(), + Protocol :: module(), + ProtoOpts :: any()) + -> {ok, ListenerPid :: pid()} + | {error, any()} +---- + +Start a listener. + +A listener is a set of processes that accepts and manages +connections using the given transport and protocol modules. + +== Arguments + +Ref:: + +The listener name is used to refer to this listener in +future calls, for example when stopping it or when +updating the configuration. ++ +It can be any Erlang term. An atom is generally good enough, +for example `api`, `my_app_clear` or `my_app_tls`. + +Transport:: + +The transport module that will be used by Ranch to accept +connections and that will be passed to the protocol module +along with the socket. ++ +The interface of the transport module is documented in the +link:man:ranch_transport(3)[ranch_transport(3)] manual. + +TransportOpts:: + +Transport options include the Ranch-specific options +and the socket options. The listener's port number must +be defined in the socket options. ++ +Socket options may be given directly if there are no +Ranch-specific options. ++ +The available options for the built-in Ranch transports +are documented in the link:man:ranch_tcp(3)[ranch_tcp(3)] +and link:man:ranch_ssl(3)[ranch_ssl(3)] manuals. + +Protocol:: + +The protocol module that will be used by Ranch after +the connection has been accepted. ++ +The interface of the protocol module is documented in the +link:man:ranch_protocol(3)[ranch_protocol(3)] manual. + +ProtocolOpts:: + +The protocol options given when calling the protocol +module. Please consult the documentation of the protocol +module you are using for more details. + +== Return value + +An ok tuple is returned on success. It contains the pid of +the top-level supervisor for the listener. + +An error tuple is returned on error. The error reason may +be any Erlang term. + +A common error is `eaddrinuse`. It indicates that the port +configured for Ranch is already in use. + +== Changelog + +* *2.0*: The `TransOpts` argument must no longer contain + Ranch-specific options if given as a list. Use a map. +* *1.4*: The `NumAcceptors` argument was moved to the transport options. + +== Examples + +.Start a listener +[source,erlang] +---- +{ok, _} = ranch:start_listener(example, + ranch_tcp, [{port, 8080}], + cowboy_http2, #{} +). +---- + +.Start a listener with Ranch-specific options +[source,erlang] +---- +{ok, _} = ranch:start_listener(example, + ranch_tcp, #{ + num_acceptors => 75, + socket_opts => [{port, 8080}] + }, + cowboy_http2, #{} +). +---- + +.Start a listener on a random port +[source,erlang] +---- +Ref = example, + +{ok, _} = ranch:start_listener(Ref, + ranch_tcp, #{}, + cowboy_http2, #{} +), + +Port = ranch:get_port(Ref). +---- + +== See also + +link:man:ranch:stop_listener(3)[ranch:stop_listener(3)], +link:man:ranch:child_spec(3)[ranch:child_spec(3)], +link:man:ranch(3)[ranch(3)], +link:man:ranch_tcp(3)[ranch_tcp(3)], +link:man:ranch_ssl(3)[ranch_ssl(3)], +link:man:ranch_transport(3)[ranch_transport(3)], +link:man:ranch_protocol(3)[ranch_protocol(3)] diff --git a/ranch/doc/src/manual/ranch.stop_listener.asciidoc b/ranch/doc/src/manual/ranch.stop_listener.asciidoc new file mode 100644 index 0000000..6de24f5 --- /dev/null +++ b/ranch/doc/src/manual/ranch.stop_listener.asciidoc @@ -0,0 +1,58 @@ += ranch:stop_listener(3) + +== Name + +ranch:stop_listener - Stop a listener + +== Description + +[source,erlang] +---- +stop_listener(Ref :: ranch_ref()) + -> ok | {error, not_found} +---- + +Stop a listener. + +The listener is stopped gracefully, first by closing the +listening port, then by stopping the connection processes. +These processes are stopped according to the `shutdown` +transport option, which may be set to brutally kill all +connection processes or give them some time to stop properly. + +In order for the connection processes to exit gracefully, +they need to trap exit signals and stop before the configured +shutdown timeout. If greater control over the shutdown is +required the functions link:man:ranch:suspend_listener(3)[ranch:suspend_listener(3)] +and link:man:ranch:wait_for_connections(3)[ranch:wait_for_connections(3)] +can be used. + +This function does not return until the listener is +completely stopped. + +== Arguments + +Ref:: + +The listener name. + +== Return value + +The atom `ok` is returned on success. + +An error tuple is returned when the listener is not found. + +== Examples + +.Stop a listener +[source,erlang] +---- +ok = ranch:stop_listener(example). +---- + +== See also + +link:man:ranch:start_listener(3)[ranch:start_listener(3)], +link:man:ranch:child_spec(3)[ranch:child_spec(3)], +link:man:ranch:suspend_listener(3)[ranch:suspend_listener(3)], +link:man:ranch(3)[ranch(3)] diff --git a/ranch/doc/src/manual/ranch.suspend_listener.asciidoc b/ranch/doc/src/manual/ranch.suspend_listener.asciidoc new file mode 100644 index 0000000..322b993 --- /dev/null +++ b/ranch/doc/src/manual/ranch.suspend_listener.asciidoc @@ -0,0 +1,62 @@ += ranch:suspend_listener(3) + +== Name + +ranch:suspend_listener - Suspend a running listener + +== Description + +[source,erlang] +---- +suspend_listener(Ref :: ranch_ref()) + -> ok | {error, any()} +---- + +Suspend a running listener. + +Ranch will stop listening for and accepting connections and +the listening socket will be closed. Existing connections +will continue undisturbed. The function +link:man:ranch:wait_for_connections(3)[ranch:wait_for_connections(3)] +can be used to wait for connections to be closed if necessary. + +Some transport options can only be changed when the listener is +suspended. Please consult the +link:man:ranch:set_transport_options(3)[ranch:set_transport_options(3)] +manual for more information. + +Nothing is done when the listener is already suspended. + +== Arguments + +Ref:: + +The listener name. + +== Return value + +The atom `ok` is returned on success. + +An error tuple is returned when the listener could not be suspended. + +== Changelog + +* *1.6*: Function introduced. + +== Examples + +.Suspend a listener +[source,erlang] +---- +ok = ranch:suspend_listener(example). +---- + +== See also + +link:man:ranch:start_listener(3)[ranch:start_listener(3)], +link:man:ranch:stop_listener(3)[ranch:stop_listener(3)], +link:man:ranch:resume_listener(3)[ranch:resume_listener(3)], +link:man:ranch:get_status(3)[ranch:get_status(3)], +link:man:ranch:set_transport_options(3)[ranch:set_transport_options(3)], +link:man:ranch:wait_for_connections(3)[ranch:wait_for_connections(3)], +link:man:ranch(3)[ranch(3)] diff --git a/ranch/doc/src/manual/ranch.wait_for_connections.asciidoc b/ranch/doc/src/manual/ranch.wait_for_connections.asciidoc new file mode 100644 index 0000000..3fdc2a1 --- /dev/null +++ b/ranch/doc/src/manual/ranch.wait_for_connections.asciidoc @@ -0,0 +1,76 @@ += ranch:wait_for_connections(3) + +== Name + +ranch:wait_for_connections - Wait for a specific number of connections + +== Description + +[source,erlang] +---- +wait_for_connections(Ref :: ranch:ref(), + Operator, + NumConns :: non_neg_integer()) + -> ok + +Operator :: '>' | '>=' | '==' | '=<' | '<' +---- + +Wait for a specific number of connections. + +This function waits until the number of connections on the +given listener becomes higher than, equal to or lower than +the given number. It never returns otherwise. + +This function can be used to gracefully shutdown a listener +by first suspending the listener and then waiting for +connections to terminate before finally stopping the listener. + +// @todo The suspend/wait/stop pattern should be tested. + +== Arguments + +Ref:: + +The listener name. + +Operator:: + +The operator to use for the comparison. + +NumConns:: + +The number of connections to reach. + +== Return value + +The atom `ok` is always returned. It can be safely ignored. + +== Changelog + +* *1.6*: Function introduced. + +== Examples + +.Wait for at least 100 connections +[source,erlang] +---- +ranch:wait_for_connections(example, '>=', 100). +---- + +.Gracefully shutdown a listener +[source,erlang] +---- +Ref = example, + +ok = ranch:suspend_listener(Ref), +ranch:wait_for_connections(Ref, '==', 0), +ok = ranch:stop_listener(Ref). +---- + +== See also + +link:man:ranch:stop_listener(3)[ranch:stop_listener(3)], +link:man:ranch:suspend_listener(3)[ranch:suspend_listener(3)], +link:man:ranch:resume_listener(3)[ranch:resume_listener(3)], +link:man:ranch(3)[ranch(3)] diff --git a/ranch/doc/src/manual/ranch_app.asciidoc b/ranch/doc/src/manual/ranch_app.asciidoc new file mode 100644 index 0000000..3186e6c --- /dev/null +++ b/ranch/doc/src/manual/ranch_app.asciidoc @@ -0,0 +1,61 @@ += ranch(7) + +== Name + +ranch - Socket acceptor pool for TCP protocols + +== Description + +Ranch is a socket acceptor pool for TCP protocols. + +Ranch manages listeners which are a set of processes that +accept and manage connections. The connection's transport +and protocol modules are configured per listener. Listeners +can be inspected and reconfigured without interruptions in +service. + +== Modules + +Functions: + +* link:man:ranch(3)[ranch(3)] - Socket acceptor pool +* link:man:ranch_proxy_header(3)[ranch_proxy_header(3)] - PROXY protocol + +Transports: + +* link:man:ranch_ssl(3)[ranch_ssl(3)] - SSL transport +* link:man:ranch_tcp(3)[ranch_tcp(3)] - TCP transport + +Behaviors: + +* link:man:ranch_protocol(3)[ranch_protocol(3)] - Protocol modules +* link:man:ranch_transport(3)[ranch_transport(3)] - Transport modules + +== Dependencies + +* ssl - Secure communication over sockets + +All these applications must be started before the `ranch` +application. To start Ranch and all dependencies at once: + +[source,erlang] +---- +{ok, _} = application:ensure_all_started(ranch). +---- + +== Environment + +The `ranch` application defines one application environment +configuration parameter. + +profile (false):: + +When enabled, Ranch will start `eprof` profiling automatically. ++ +You can use the `ranch_app:profile_output/0` function to stop +profiling and output the results to the files 'procs.profile' +and 'total.profile'. Do not use in production. + +== See also + +ssl(7) diff --git a/ranch/doc/src/manual/ranch_protocol.asciidoc b/ranch/doc/src/manual/ranch_protocol.asciidoc new file mode 100644 index 0000000..5a94399 --- /dev/null +++ b/ranch/doc/src/manual/ranch_protocol.asciidoc @@ -0,0 +1,56 @@ += ranch_protocol(3) + +== Name + +ranch_protocol - Protocol modules + +== Description + +The module `ranch_protocol` defines the interface used +by Ranch protocols. + +== Callbacks + +Ranch protocols implement the following interface: + +[source,erlang] +---- +start_link(Ref :: ranch:ref(), + Transport :: module(), + ProtoOpts :: any()) + -> {ok, ConnPid :: pid()} + | {ok, SupPid :: pid(), ConnPid :: pid()} +---- + +Start a new connection process. + +The only purpose of this callback is to start a process that +will handle the socket. It must spawn the process, link and +then return the new pid. This function will always be called +from inside a supervisor. + +This callback can also return two pids. The first pid is the +pid of the process that will be supervised. The second pid is +the pid of the process that will receive ownership of the +socket. This second process must be a child of the first. This +form is only available when `connection_type` is set to +`supervisor`. + +If any other value is returned, the supervisor will close the +socket and assume no process has been started. + +Do not perform any operations in this callback, as this would +block the supervisor responsible for starting connection +processes and degrade performance severely. + +== Changelog + +* *2.0*: The second argument `Socket` was removed. +* *1.6*: The second argument `Socket` was deprecated. Call + link:man:ranch:handshake(3)[ranch:handshake(3)] + to obtain the socket. + +== See also + +link:man:ranch:handshake(3)[ranch:handshake(3)], +link:man:ranch(7)[ranch(7)] diff --git a/ranch/doc/src/manual/ranch_proxy_header.asciidoc b/ranch/doc/src/manual/ranch_proxy_header.asciidoc new file mode 100644 index 0000000..c194d98 --- /dev/null +++ b/ranch/doc/src/manual/ranch_proxy_header.asciidoc @@ -0,0 +1,165 @@ += ranch_proxy_header(3) + +== Name + +ranch_proxy_header - PROXY protocol + +== Description + +The module `ranch_proxy_header` provides functions +for parsing and building the PROXY protocol header. + +== Exports + +* link:man:ranch_proxy_header:parse(3)[ranch_proxy_header:parse(3)] - Parse a PROXY protocol header +* link:man:ranch_proxy_header:header(3)[ranch_proxy_header:header(3)] - Build a PROXY protocol header +* link:man:ranch_proxy_header:to_connection_info(3)[ranch_proxy_header:to_connection_info(3)] - Convert proxy_info() to ssl:connection_info() + +== Types + +=== proxy_info() + +[source,erlang] +---- +proxy_info() = #{ + %% Mandatory part. + version := 1 | 2, + command := local | proxy, + transport_family => undefined | ipv4 | ipv6 | unix, + transport_protocol => undefined | stream | dgram, + + %% Addresses. + src_address => inet:ip_address() | binary(), + src_port => inet:port_number(), + dest_address => inet:ip_address() | binary(), + dest_port => inet:port_number(), + + %% Extra TLV-encoded data. + alpn => binary(), %% US-ASCII. + authority => binary(), %% UTF-8. + netns => binary(), %% US-ASCII. + ssl => #{ + client := [ssl | cert_conn | cert_sess], + verified := boolean(), + version => binary(), %% US-ASCII. + cipher => binary(), %% US-ASCII. + sig_alg => binary(), %% US-ASCII. + key_alg => binary(), %% US-ASCII. + cn => binary() %% UTF-8. + }, + + %% Unknown TLVs can't be parsed so the raw data is given. + raw_tlvs => [{0..255, binary()}] +}. +---- + +The PROXY protocol information. + +The following fields may be found, although most of them are +optional: + +version:: + +The PROXY protocol version used. + +command:: + +`proxy` is used for proxied connections. `local` for non-proxied +connections. Those do not have any additional information. + +transport_family:: + +The transport family of the original connection. + +transport_protocol:: + +The transport protocol of the original connection. + +src_address:: + +The source address of the original connection. This is the +original address of the client. + +src_port:: + +The source port of the original connection. This is the +port the client opened on its end for the connection. It +is not defined for UNIX domain sockets. + +dest_address:: + +The destination address of the original connection. + +dest_port:: + +The destination port of the original connection. It +is not defined for UNIX domain sockets. + +alpn:: + +The upper layer protocol in use over the connection. This +is typically negotiated via the ALPN extension for TLS. + +authority:: + +The host name serving as authority for the connection. +This is typically passed using the SNI extension for TLS. + +netns:: + +The namespace's name for the original connection. + +ssl:: + +Various information pertaining to the original SSL/TLS +connection. + +client::: + +A list containing a number of flags. `ssl` indicates +that the client connected over SSL/TLS. `cert_conn` +indicates that the client provided a certificate over +the original connection. `cert_sess` indicates that +the client provided a certificate at least once over +the TLS session this connection belongs to. + +verified::: + +Whether the client presented a certificate and it was +successfully verified. + +version::: + +The US-ASCII string containing the SSL/TLS version +used for the original connection. + +cipher::: + +The US-ASCII string name of the cipher used. + +sig_alg::: + +The US-ASCII string name of the algorithm used to sign +the certificate provided by the client. + +key_alg::: + +The US-ASCII string name of the algorithm used to generate +the key of the certificate provided by the client. + +cn::: + +The UTF-8 string representation of the Common Name field +of the client certificate's Distinguished Name. + +raw_tlvs:: + +The non-standard TLVs that Ranch was not able to parse. + +== Changelog + +* *1.7*: Module introduced. + +== See also + +link:man:ranch(7)[ranch(7)] diff --git a/ranch/doc/src/manual/ranch_proxy_header.header.asciidoc b/ranch/doc/src/manual/ranch_proxy_header.header.asciidoc new file mode 100644 index 0000000..c551110 --- /dev/null +++ b/ranch/doc/src/manual/ranch_proxy_header.header.asciidoc @@ -0,0 +1,75 @@ += ranch_proxy_header:header(3) + +== Name + +ranch_proxy_header:header - Build a PROXY protocol header + +== Description + +[source,erlang] +---- +header(ProxyInfo) -> header(ProxyInfo, #{}) +header(ProxyInfo, BuildOpts) -> iodata() + +ProxyInfo :: ranch_proxy_header:proxy_info() +BuildOpts :: #{ + checksum => crc32c, + padding => pos_integer() %% >= 3 +} +---- + +Build a PROXY protocol header. + +== Arguments + +ProxyInfo:: + +The proxy information to encode. + +BuildOpts:: + +Options to control whether to add a checksum or padding +should be included in the encoded PROXY protocol header. + +== Return value + +The PROXY protocol header is returned. + +== Changelog + +* *1.7*: Function introduced. + +== Examples + +.Build a PROXY protocol header +[source,erlang] +---- +ProxyInfo = #{ + version => 2, + command => proxy, + + transport_family => ipv4, + transport_protocol => stream, + + src_address => {192, 168, 1, 11}, + src_port => 54321, + dest_address => {192, 168, 1, 42}, + dest_port => 443 +}, +Data = ranch_proxy_header:parse(ProxyInfo). +---- + +.Build a PROXY protocol header with checksum and padding +[source,erlang] +---- +Data = ranch_proxy_header:parse(ProxyInfo, #{ + checksum => crc32c, + padding => 7 +}). +---- + +== See also + +link:man:ranch_proxy_header:header(3)[ranch_proxy_header:header(3)], +link:man:ranch_proxy_header(3)[ranch_proxy_header(3)] + diff --git a/ranch/doc/src/manual/ranch_proxy_header.parse.asciidoc b/ranch/doc/src/manual/ranch_proxy_header.parse.asciidoc new file mode 100644 index 0000000..5e03a83 --- /dev/null +++ b/ranch/doc/src/manual/ranch_proxy_header.parse.asciidoc @@ -0,0 +1,50 @@ += ranch_proxy_header:parse(3) + +== Name + +ranch_proxy_header:parse - Parse a PROXY protocol header + +== Description + +[source,erlang] +---- +parse(Data :: binary()) + -> {ok, ranch_proxy_header:proxy_info(), Rest :: binary()} + | {error, HumanReadable :: atom()} +---- + +Parse a PROXY protocol header. + +== Arguments + +Data:: + +The PROXY protocol header optionally followed by more data. + +== Return value + +An `ok` tuple is returned on success, containing the proxy +information found in the header and the rest of the data +if more was provided. + +An `error` tuple is returned when a protocol error is +detected. It contains a human readable message about the +error. + +== Changelog + +* *1.7*: Function introduced. + +== Examples + +.Parse the PROXY protocol header +[source,erlang] +---- +{ok ProxyInfo, Rest} = ranch_proxy_header:parse(Data). +---- + +== See also + +link:man:ranch_proxy_header:header(3)[ranch_proxy_header:header(3)], +link:man:ranch_proxy_header:to_connection_info(3)[ranch_proxy_header:to_connection_info(3)], +link:man:ranch_proxy_header(3)[ranch_proxy_header(3)] diff --git a/ranch/doc/src/manual/ranch_proxy_header.to_connection_info.asciidoc b/ranch/doc/src/manual/ranch_proxy_header.to_connection_info.asciidoc new file mode 100644 index 0000000..9a53723 --- /dev/null +++ b/ranch/doc/src/manual/ranch_proxy_header.to_connection_info.asciidoc @@ -0,0 +1,49 @@ += ranch_proxy_header:to_connection_info(3) + +== Name + +ranch_proxy_header:to_connection_info - Convert proxy_info() to ssl:connection_info() + +== Description + +[source,erlang] +---- +to_connection_info(ProxyInfo :: proxy_info()) + -> ssl:connection_info() +---- + +Convert `ranch_proxy_header:proxy_info()` information +to the `ssl:connection_info()` format returned by +`ssl:connection_information/1,2`. + +== Arguments + +ProxyInfo:: + +The PROXY protocol information. + +== Return value + +Connection information is returned as a proplist. + +Because the PROXY protocol header includes limited +information, only the keys `protocol`, `selected_cipher_suite` +and `sni_hostname` will be returned, at most. All keys +are optional. + +== Changelog + +* *2.1*: Function introduced. + +== Examples + +.Convert the PROXY protocol information +[source,erlang] +---- +ConnInfo = ranch_proxy_header:to_connection_info(ProxyInfo). +---- + +== See also + +link:man:ranch_proxy_header:parse(3)[ranch_proxy_header:parse(3)], +link:man:ranch_proxy_header(3)[ranch_proxy_header(3)] diff --git a/ranch/doc/src/manual/ranch_ssl.asciidoc b/ranch/doc/src/manual/ranch_ssl.asciidoc new file mode 100644 index 0000000..754d90b --- /dev/null +++ b/ranch/doc/src/manual/ranch_ssl.asciidoc @@ -0,0 +1,333 @@ += ranch_ssl(3) + +== Name + +ranch_ssl - SSL transport + +== Description + +The module `ranch_ssl` implements an SSL Ranch transport. + +== Exports + +The module `ranch_ssl` implements the interface defined +by link:man:ranch_transport(3)[ranch_transport(3)]. + +== Types + +=== opt() + +[source,erlang] +---- +opt() :: ranch_tcp:opt() | ssl_opt() +---- + +Listen options. + +The TCP options are defined in link:man:ranch_tcp(3)[ranch_tcp(3)]. + +=== opts() + +[source,erlang] +---- +opts() :: [opt()] +---- + +List of listen options. + +=== ssl_opt() + +[source,erlang] +---- +ssl_opt() = {alpn_preferred_protocols, [binary()]} + | {anti_replay, '10k' | '100k' | {integer(), integer(), integer()}} + | {beast_mitigation, one_n_minus_one | zero_n | disabled} + | {cacertfile, file:filename()} + | {cacerts, [public_key:der_encoded()]} + | {cert, public_key:der_encoded()} + | {certfile, file:filename()} + | {ciphers, ssl:ciphers()} + | {client_renegotiation, boolean()} + | {crl_cache, [any()]} + | {crl_check, boolean() | peer | best_effort} + | {depth, integer()} + | {dh, binary()} + | {dhfile, file:filename()} + | {eccs, [ssl:named_curve()]} + | {fail_if_no_peer_cert, boolean()} + | {handshake, hello | full} + | {hibernate_after, timeout()} + | {honor_cipher_order, boolean()} + | {honor_ecc_order, boolean()} + | {key, ssl:key()} + | {key_update_at, pos_integer()} + | {keyfile, file:filename()} + | {log_alert, boolean()} + | {log_level, logger:level()} + | {max_handshake_size, integer()} + | {middlebox_comp_mode, boolean()} + | {next_protocols_advertised, [binary()]} + | {padding_check, boolean()} + | {partial_chain, fun()} + | {password, string()} + | {protocol, tls | dtls} + | {psk_identity, string()} + | {reuse_session, fun()} + | {reuse_sessions, boolean()} + | {secure_renegotiate, boolean()} + | {session_tickets, disabled | stateful | stateless} + | {signature_algs, [{ssl:hash(), ssl:sign_algo()}]} + | {signature_algs_cert, [ssl:sign_scheme()]} + | {sni_fun, fun()} + | {sni_hosts, [{string(), ssl_opt()}]} + | {supported_groups, [ssl:group()]} + | {user_lookup_fun, {fun(), any()}} + | {verify, verify_none | verify_peer} + | {verify_fun, {fun(), any()}} + | {versions, [ssl:protocol_version()]} +---- + +SSL-specific listen options. + +Specifying a certificate is mandatory, either through the `cert` +or `certfile` option, or by configuring SNI. None of the other +options are required. + +The default value is given next to the option name: + +alpn_preferred_protocols:: + +Perform Application-Layer Protocol Negotiation +with the given list of preferred protocols. + +anti_replay:: + +Configures the server's built-in anti replay feature based on +Bloom filters. + +beast_mitigation (one_n_minus_one):: + +Change the BEAST mitigation strategy for SSL-3.0 and TLS-1.0 +to interoperate with legacy software. + +cacertfile:: + +Path to PEM encoded trusted certificates file used to verify +peer certificates. + +cacerts:: + +List of DER encoded trusted certificates. + +cert:: + +DER encoded user certificate. + +certfile:: + +Path to the PEM encoded user certificate file. May also +contain the private key. + +ciphers:: + +List of ciphers that clients are allowed to use. + +client_renegotiation (true):: + +Whether to allow client-initiated renegotiation. + +crl_cache ({ssl_crl_cache, {internal, []}}):: + +Customize the module used to cache Certificate Revocation Lists. + +crl_check (false):: + +Whether to perform CRL check on all certificates in the chain +during validation. + +depth (1):: + +Maximum of intermediate certificates allowed in the +certification path. + +dh:: + +DER encoded Diffie-Hellman parameters. + +dhfile:: + +Path to the PEM encoded Diffie-Hellman parameters file. + +eccs:: + +List of named ECC curves. + +fail_if_no_peer_cert (false):: + +Whether to refuse the connection if the client sends an +empty certificate. + +handshake (full):: + +If `hello` is specified for this option, the handshake is +paused after receiving the client hello message. The handshake +can then be resumed via `handshake_continue/3`, or cancelled +via `handshake_cancel/1`. ++ +This option cannot be given to `ranch:handshake/1,2`. + +hibernate_after (undefined):: + +Time in ms after which SSL socket processes go into +hibernation to reduce memory usage. + +honor_cipher_order (false):: + +If true, use the server's preference for cipher selection. +If false, use the client's preference. + +honor_ecc_order (false):: + +If true, use the server's preference for ECC curve selection. +If false, use the client's preference. + +key:: + +DER encoded user private key. + +key_update_at:: + +Configures the maximum amount of bytes that can be sent on a +TLS 1.3 connection before an automatic key update is performed. + +keyfile:: + +Path to the PEM encoded private key file, if different from +the certfile. + +log_alert (true):: + +If false, error reports will not be displayed. + +log_level:: + +Specifies the log level for TLS/DTLS. + +max_handshake_size (256*1024):: + +Used to limit the size of valid TLS handshake packets to +avoid DoS attacks. + +middlebox_comp_mode (true):: + +Configures the middlebox compatibility mode on a TLS 1.3 +connection. + +next_protocols_advertised:: + +List of protocols to send to the client if it supports the +Next Protocol extension. + +padding_check:: + +Allow disabling the block cipher padding check for TLS-1.0 +to be able to interoperate with legacy software. + +partial_chain:: + +Claim an intermediate CA in the chain as trusted. + +password:: + +Password to the private key file, if password protected. + +protocol (tls):: + +Choose TLS or DTLS protocol for the transport layer security. + +psk_identity:: + +Provide the given PSK identity hint to the client during the +handshake. + +reuse_session:: + +Custom policy to decide whether a session should be reused. + +reuse_sessions (false):: + +Whether to allow session reuse. + +secure_renegotiate (false):: + +Whether to reject renegotiation attempts that do not conform +to RFC5746. + +session_tickets:: + +Configures the session ticket functionality. + +signature_algs:: + +The TLS signature algorithm extension may be used, from TLS 1.2, +to negotiate which signature algorithm to use during the TLS +handshake. + +signature_algs_cert:: + +List of signature schemes for the signature_algs_cert extension +introduced in TLS 1.3, in order to make special requirements +on signatures used in certificates. + +sni_fun:: + +Function called when the client requests a host using Server +Name Indication. Returns options to apply. + +sni_hosts:: + +Options to apply for the host that matches what the client +requested with Server Name Indication. + +supported_groups([x25519, x448, secp256r1, secp384r1]):: + +TLS 1.3 introduces the `supported_groups` extension that is +used for negotiating the Diffie-Hellman parameters in a +TLS 1.3 handshake. Both client and server can specify a list +of parameters that they are willing to use. + +user_lookup_fun:: + +Function called to determine the shared secret when using PSK, +or provide parameters when using SRP. + +verify (verify_none):: + +Use `verify_peer` to request a certificate from the client. + +verify_fun:: + +Custom policy to decide whether a client certificate is valid. + +versions:: + +TLS protocol versions that will be supported. + +Note that the client will not send a certificate unless the +value for the `verify` option is set to `verify_peer`. This +means that `fail_if_no_peer_cert` only applies when combined +with the `verify` option. The `verify_fun` option allows +greater control over the client certificate validation. + +The options `sni_fun` and `sni_hosts` are mutually exclusive. + +== Changelog + +* *2.0*: The `ssl_opt()` type was updated for OTP-23.0. + +== See also + +link:man:ranch(7)[ranch(7)], +link:man:ranch_transport(3)[ranch_transport(3)], +link:man:ranch_tcp(3)[ranch_tcp(3)], +ssl(3) diff --git a/ranch/doc/src/manual/ranch_tcp.asciidoc b/ranch/doc/src/manual/ranch_tcp.asciidoc new file mode 100644 index 0000000..bee73a7 --- /dev/null +++ b/ranch/doc/src/manual/ranch_tcp.asciidoc @@ -0,0 +1,194 @@ += ranch_tcp(3) + +== Name + +ranch_tcp - TCP transport + +== Description + +The module `ranch_tcp` implements a TCP Ranch transport. + +The function `sendfile` may not work correctly when used +against files stored in a VirtualBox shared folder. + +== Exports + +The module `ranch_tcp` implements the interface defined +by link:man:ranch_transport(3)[ranch_transport(3)]. + +== Types + +=== opt() + +[source,erlang] +---- +opt() = {backlog, non_neg_integer()} + | {buffer, non_neg_integer()} + | {delay_send, boolean()} + | {dontroute, boolean()} + | {exit_on_close, boolean()} + | {fd, non_neg_integer()} + | {high_msgq_watermark, non_neg_integer()} + | {high_watermark, non_neg_integer()} + | inet + | inet6 + | {ip, inet:ip_address() | inet:local_address()} + | {ipv6_v6only, boolean()} + | {keepalive, boolean()} + | {linger, {boolean(), non_neg_integer()}} + | {low_msgq_watermark, non_neg_integer()} + | {low_watermark, non_neg_integer()} + | {nodelay, boolean()} + | {port, inet:port_number()} + | {priority, integer()} + | {raw, non_neg_integer(), non_neg_integer(), binary()} + | {recbuf, non_neg_integer()} + | {send_timeout, timeout()} + | {send_timeout_close, boolean()} + | {sndbuf, non_neg_integer()} + | {tos, integer()} +---- + +Listen options. + +Note that additional options may be set by the protocol +module using `Transport:setopts/2`. + +None of the options are required. + +Please consult the `gen_tcp` and `inet` manuals for a more +thorough description of these options. This manual only aims +to provide a short description along with what the defaults +are. Defaults may be different in Ranch compared to `gen_tcp`. +Defaults are given next to the option name: + +backlog (1024):: + +Max length of the queue of pending connections. + +buffer:: + +Size of the buffer used by the Erlang driver. Default +is system-dependent. + +delay_send (false):: + +Always queue data before sending, to send fewer, larger +packets over the network. + +dontroute (false):: + +Don't send via a gateway, only send to directly connected hosts. + +exit_on_close (true):: + +Disable to allow sending data after a close has been detected. + +fd:: + +File descriptor of the socket, if it was opened externally. + +high_msgq_watermark (8192):: + +Limit in the amount of data in the socket message queue before +the queue becomes busy. + +high_watermark (8192):: + +Limit in the amount of data in the ERTS socket implementation's +queue before the socket becomes busy. + +inet:: + +Set up the socket for IPv4. + +inet6:: + +Set up the socket for IPv6. + +ip:: + +Interface to listen on. Listen on all network interfaces by default. + +On UNIX systems, it is also possible to use a UNIX Domain +socket file by specifying `{local, SocketFile}`. + +ipv6_v6only (false):: + +Listen on IPv4 and IPv6 (false) or only on IPv6 (true). +Use with inet6. + +keepalive (false):: + +Enable sending of keep-alive messages. + +linger ({false, 0}):: + +Whether to wait and how long to flush data sent before closing +the socket. + +low_msgq_watermark (4096):: + +Amount of data in the socket message queue before the queue +leaves busy state. + +low_watermark (4096):: + +Amount of data in the ERTS socket implementation's queue +before the socket leaves busy state. + +nodelay (true):: + +Whether to enable TCP_NODELAY. + +port (0):: + +TCP port number to listen on. 0 means a random port will be used. + +priority (0):: + +Priority value for all packets to be sent on this socket. + +recbuf:: + +Minimum size of the socket's receive buffer. +Default is system-dependent. + +send_timeout (30000):: + +How long the send call may wait for confirmation before returning. + +send_timeout_close (true):: + +Whether to close the socket when the confirmation wasn't received. + +sndbuf:: + +Minimum size of the socket's send buffer. +Default is system-dependent. + +tos:: + +Value for the IP_TOS IP level option. Use with caution. + +In addition, the `raw` option can be used to set system-specific +options by specifying the protocol level, the option number and +the actual option value specified as a binary. This option is not +portable. Use with caution. + +=== opts() + +[source,erlang] +---- +opts() :: [opt()] +---- + +List of listen options. + +== See also + +link:man:ranch(7)[ranch(7)], +link:man:ranch_transport(3)[ranch_transport(3)], +link:man:ranch_ssl(3)[ranch_ssl(3)], +gen_tcp(3), +inet(3) diff --git a/ranch/doc/src/manual/ranch_transport.asciidoc b/ranch/doc/src/manual/ranch_transport.asciidoc new file mode 100644 index 0000000..c26d91c --- /dev/null +++ b/ranch/doc/src/manual/ranch_transport.asciidoc @@ -0,0 +1,355 @@ += ranch_transport(3) + +== Name + +ranch_transport - Transport modules + +== Description + +The module `ranch_transport` defines the interface used +by Ranch transports. + +== Callbacks + +Ranch transports implement the following interface: + +=== accept + +[source,erlang] +---- +accept(LSocket :: socket(), Timeout :: timeout()) + -> {ok, Socket :: socket()} + | {error, closed | timeout | atom()} +---- + +Use the listening socket returned by `listen/1` +to accept a new connection. The timeout is specified +in milliseconds. + +=== close + +[source,erlang] +---- +close(Socket :: socket()) -> ok +---- + +Close the socket. + +=== controlling_process + +[source,erlang] +---- +controlling_process(Socket :: socket(), Pid :: pid()) + -> ok | {error, closed | not_owner | atom()} +---- + +Assign a new controlling process to the socket. The +controlling process is the process that is linked to +and receives messages from the socket. + +=== getopts + +[source,erlang] +---- +getopts(Socket :: socket(), SockOpts :: [atom()]) + -> {ok, any()} | {error, atom()} +---- + +Get one or more options for the socket. + +=== getstat + +[source,erlang] +---- +getstat(Socket :: socket()) + -> {ok, SockStatValues :: any()} | {error, atom()} +---- + +Get all statistics for the socket. + +[source,erlang] +---- +getstat(Socket :: socket(), SockStats :: [atom()]) + -> {ok, SockStatValues :: any()} | {error, atom()} +---- + +Get one or more statistic options for the socket. + +=== handshake + +[source,erlang] +---- +handshake(Socket0 :: socket(), + Timeout :: timeout()) + -> {ok, Socket :: socket()} + | {ok, Socket :: socket(), Info :: any()} + | {error, any()} + +handshake(Socket0 :: socket(), + SockOpts :: opts(), + Timeout :: timeout()) + -> {ok, Socket :: socket()} + | {ok, Socket :: socket(), Info :: any()} + | {error, any()} +---- + +Perform the transport-level handshake. + +This function will be called by connection processes +before performing any socket operation. It allows +transports that require extra initialization to perform +their task and return a socket that is ready to use. + +If the handshake is completed by this call, the function will +return `{ok, Socket}`. However, some transports (notably, +`ranch_ssl` if `{handshake, hello}` is specified in the socket +options) may pause the handshake at a certain point and return +`{ok, Socket, Info}` instead, in order to allow for +additional decisions to be made before resuming the handshake +with `handshake_continue/3` or cancelling it with +`handshake_cancel/1`. + +This function may also be used to upgrade a connection +from a transport to another depending on the capabilities +of the transports. For example a `ranch_tcp` socket may +be upgraded to a `ranch_ssl` one using this function. + +=== handshake_continue + +[source,erlang] +---- +handshake_continue(Socket0 :: socket(), + Timeout :: timeout()) + -> {ok, Socket :: socket()} + | {error, any()} + +handshake_continue(Socket0 :: socket(), + SockOpts :: opts(), + Timeout :: timeout()) + -> {ok, Socket :: socket()} + | {error, any()} +---- + +Resume the paused transport-level handshake and return a socket +that is ready to use. + +This function will be called by connection processes +to resume a paused handshake. + +=== handshake_cancel + +[source,erlang] +---- +handshake_cancel(Socket :: socket()) -> ok +---- + +Cancel the paused transport-level handshake. + +=== listen + +[source,erlang] +---- +listen(TransportOpts :: ranch:transport_opts(any())) + -> {ok, LSocket :: socket()} | {error, atom()} +---- + +Create a socket that listens on the port given in the +socket options. + +The port may not be specified or may be set to 0, which +means a random available port number will be chosen. + +=== messages + +[source,erlang] +---- +messages() + -> {OK :: atom(), + Closed :: atom(), + Error :: atom(), + Passive :: atom()} +---- + +Return the tuple keys for the messages sent by the socket. + +=== name + +[source,erlang] +---- +name() -> Name :: atom() +---- + +Return the name of the transport. + +=== peername + +[source,erlang] +---- +peername(Socket :: socket()) + -> {ok, {inet:ip_address(), inet:port_number()}} + | {local, binary()} | {error, atom()}. +---- + +Return the address and port number for the other end of +the connection. + +For UNIX Domain sockets the return value will be +`{local, PeerSocket}`, with `PeerSocket` typically +an empty binary. + +=== recv + +[source,erlang] +---- +recv(Socket :: socket(), + Length :: non_neg_integer(), + Timeout :: timeout()) + -> {ok, Packet :: any()} + | {error, closed | timeout | atom()} +---- + +Receive a packet from the socket in passive mode. + +Attempting to receive data from a socket that is +in active mode will return an error. + +A length of 0 will return the data available on +the socket as soon as possible, regardless of length. + +While it is possible to use the timeout value `infinity`, +it is highly discouraged as it could cause your process +to get stuck waiting for data that will never come. This may +happen when a socket becomes half-open due to a crash of the +remote endpoint. Wi-Fi going down is another common culprit. + +=== secure + +[source,erlang] +---- +secure() -> boolean() +---- + +Return whether the transport can be used for secure connections. + +=== send + +[source,erlang] +---- +send(Socket :: socket(), Packet :: iodata()) + -> ok | {error, atom()} +---- + +Send a packet on the socket. + +=== sendfile + +[source,erlang] +---- +sendfile(Socket, File) + -> sendfile(Socket, File, 0, 0, []) + +sendfile(Socket, File, Offset, Bytes) + -> sendfile(Socket, File, Offset, Bytes, []) + +sendfile(Socket :: socket(), + File :: file:name_all() | file:fd(), + Offset :: non_neg_integer(), + Bytes :: non_neg_integer(), + Opts :: sendfile_opts()) + -> {ok, SentBytes :: non_neg_integer()} | {error, atom()} +---- + +Send a file on the socket. + +The file may be sent full or in parts, and may be specified +by its filename or by an already open file descriptor. + +Transports that manipulate TCP directly may use the +`file:sendfile/2,4,5` function, which calls the `sendfile` +syscall where applicable (on Linux, for example). Other +transports can use the `sendfile/6` function exported from +this module. + +=== setopts + +[source,erlang] +---- +setopts(Socket :: socket(), SockOpts :: any()) + -> ok | {error, atom()} +---- + +Set one or more options for the socket. + +=== shutdown + +[source,erlang] +---- +shutdown(Socket :: socket(), + How :: read | write | read_write) + -> ok | {error, atom()} +---- + +Close the socket for reading and/or writing. + +=== sockname + +[source,erlang] +---- +sockname(Socket :: socket()) + -> {ok, {inet:ip_address(), inet:port_number()}} + | {error, atom()}. +---- + +Return the address and port number for the local end +of the connection. + +For UNIX Domain sockets the return value will be +`{local, SocketFile}`. + +== Exports + +The following function can be used when implementing +transport modules: + +* link:man:ranch_transport:sendfile(3)[ranch_transport:sendfile(3)] - Send a file on the socket + +== Types + +=== sendfile_opts() + +[source,erlang] +---- +sendfile_opts() :: [{chunk_size, non_neg_integer()}] +---- + +Options accepted by the sendfile function and callbacks: + +chunk_size (8191):: + +The chunk size, in bytes. + +=== socket() + +[source,erlang] +---- +socket() :: any() +---- + +The socket. + +The exact type will vary depending on the transport module. + +== Changelog + +* *2.0*: The callback `listen/1` has changed to accept a map of + transport options instead of socket options. +* *2.0*: The callback `messages/0` return value was updated to + include the passive message for `{active, N}`. +* *1.6*: The `socket()` type was added for documentation purposes. +* *1.6*: The type of the sendfile filename was extended. + +== See also + +link:man:ranch(7)[ranch(7)], +link:man:ranch_tcp(3)[ranch_tcp(3)], +link:man:ranch_ssl(3)[ranch_ssl(3)] diff --git a/ranch/doc/src/manual/ranch_transport.sendfile.asciidoc b/ranch/doc/src/manual/ranch_transport.sendfile.asciidoc new file mode 100644 index 0000000..1bbc79b --- /dev/null +++ b/ranch/doc/src/manual/ranch_transport.sendfile.asciidoc @@ -0,0 +1,84 @@ += ranch_transport:sendfile(3) + +== Name + +ranch_transport:sendfile - Send a file on the socket + +== Description + +[source,erlang] +---- +sendfile(Transport :: module(), + Socket :: ranch_transport:socket(), + File :: file:name_all() | file:fd(), + Offset :: non_neg_integer(), + Bytes :: non_neg_integer(), + Opts :: ranch_transport:sendfile_opts()) + -> {ok, SentBytes :: non_neg_integer()} | {error, atom()} +---- + +Send a file on the socket. + +The file may be sent full or in parts, and may be specified +by its filename or by an already open file descriptor. + +This function emulates the function `file:sendfile/2,4,5` +and may be used when transports are not manipulating TCP +directly. + +== Arguments + +Transport:: + +The transport module. + +Socket:: + +The socket. + +File:: + +The filename or file descriptor for the file to be sent. + +Offset:: + +Start position in the file, in bytes. + +Bytes:: + +Length in bytes. + +Opts:: + +Additional options. + +== Return value + +The number of bytes actually sent is returned on success +inside an `ok` tuple. + +An `error` tuple is returned otherwise. + +== Changelog + +* *1.6*: The type of the `File` argument was extended. + +== Examples + +.Implement Transport:sendfile using the fallback +[source,erlang] +---- +sendfile(Socket, Filename) -> + sendfile(Socket, Filename, 0, 0, []). + +sendfile(Socket, File, Offset, Bytes) -> + sendfile(Socket, File, Offset, Bytes, []). + +sendfile(Socket, File, Offset, Bytes, Opts) -> + ranch_transport:sendfile(?MODULE, Socket, + File, Offset, Bytes, Opts). +---- + +== See also + +link:man:ranch_transport(3)[ranch_transport(3)] diff --git a/ranch/ebin/ranch.app b/ranch/ebin/ranch.app new file mode 100644 index 0000000..b4488ae --- /dev/null +++ b/ranch/ebin/ranch.app @@ -0,0 +1,9 @@ +{application, 'ranch', [ + {description, "Socket acceptor pool for TCP protocols."}, + {vsn, "2.1.0"}, + {modules, ['ranch','ranch_acceptor','ranch_acceptors_sup','ranch_app','ranch_conns_sup','ranch_conns_sup_sup','ranch_crc32c','ranch_embedded_sup','ranch_listener_sup','ranch_protocol','ranch_proxy_header','ranch_server','ranch_server_proxy','ranch_ssl','ranch_sup','ranch_tcp','ranch_transport']}, + {registered, [ranch_sup,ranch_server]}, + {applications, [kernel,stdlib,ssl]}, + {mod, {ranch_app, []}}, + {env, []} +]}. \ No newline at end of file diff --git a/ranch/examples/tcp_echo/Makefile b/ranch/examples/tcp_echo/Makefile new file mode 100644 index 0000000..72f1164 --- /dev/null +++ b/ranch/examples/tcp_echo/Makefile @@ -0,0 +1,4 @@ +PROJECT = tcp_echo +DEPS = ranch +dep_ranch_commit = master +include ../../erlang.mk diff --git a/ranch/examples/tcp_echo/README.md b/ranch/examples/tcp_echo/README.md new file mode 100644 index 0000000..0a5fa1d --- /dev/null +++ b/ranch/examples/tcp_echo/README.md @@ -0,0 +1,27 @@ +Ranch TCP echo example +====================== + +To try this example, you need GNU `make` and `git` in your PATH. + +To build the example, run the following command: + +``` bash +$ make +``` + +To start the release in the foreground: + +``` bash +$ ./_rel/tcp_echo_example/bin/tcp_echo_example console +``` + +Then start a telnet session to port 5555: + +``` bash +$ telnet localhost 5555 +``` + +Type in a few words and see them echoed back. + +Be aware that there is a timeout of 5 seconds without receiving +data before the example server disconnects your session. diff --git a/ranch/examples/tcp_echo/relx.config b/ranch/examples/tcp_echo/relx.config new file mode 100644 index 0000000..ee746d5 --- /dev/null +++ b/ranch/examples/tcp_echo/relx.config @@ -0,0 +1,2 @@ +{release, {tcp_echo_example, "1"}, [tcp_echo, sasl]}. +{extended_start_script, true}. diff --git a/ranch/examples/tcp_echo/src/echo_protocol.erl b/ranch/examples/tcp_echo/src/echo_protocol.erl new file mode 100644 index 0000000..bbddf87 --- /dev/null +++ b/ranch/examples/tcp_echo/src/echo_protocol.erl @@ -0,0 +1,24 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(echo_protocol). +-behaviour(ranch_protocol). + +-export([start_link/3]). +-export([init/3]). + +start_link(Ref, Transport, Opts) -> + Pid = spawn_link(?MODULE, init, [Ref, Transport, Opts]), + {ok, Pid}. + +init(Ref, Transport, _Opts = []) -> + {ok, Socket} = ranch:handshake(Ref), + loop(Socket, Transport). + +loop(Socket, Transport) -> + case Transport:recv(Socket, 0, 60000) of + {ok, Data} when Data =/= <<4>> -> + Transport:send(Socket, Data), + loop(Socket, Transport); + _ -> + ok = Transport:close(Socket) + end. diff --git a/ranch/examples/tcp_echo/src/tcp_echo_app.erl b/ranch/examples/tcp_echo/src/tcp_echo_app.erl new file mode 100644 index 0000000..41b676e --- /dev/null +++ b/ranch/examples/tcp_echo/src/tcp_echo_app.erl @@ -0,0 +1,20 @@ +%% Feel free to use, reuse and abuse the code in this file. + +%% @private +-module(tcp_echo_app). +-behaviour(application). + +%% API. +-export([start/2]). +-export([stop/1]). + +%% API. + +start(_Type, _Args) -> + {ok, _} = ranch:start_listener(tcp_echo, + ranch_tcp, #{socket_opts => [{port, 5555}]}, + echo_protocol, []), + tcp_echo_sup:start_link(). + +stop(_State) -> + ok. diff --git a/ranch/examples/tcp_echo/src/tcp_echo_sup.erl b/ranch/examples/tcp_echo/src/tcp_echo_sup.erl new file mode 100644 index 0000000..8f33593 --- /dev/null +++ b/ranch/examples/tcp_echo/src/tcp_echo_sup.erl @@ -0,0 +1,22 @@ +%% Feel free to use, reuse and abuse the code in this file. + +%% @private +-module(tcp_echo_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([]) -> + {ok, {{one_for_one, 10, 10}, []}}. diff --git a/ranch/examples/tcp_reverse/Makefile b/ranch/examples/tcp_reverse/Makefile new file mode 100644 index 0000000..c390c43 --- /dev/null +++ b/ranch/examples/tcp_reverse/Makefile @@ -0,0 +1,4 @@ +PROJECT = tcp_reverse +DEPS = ranch +dep_ranch_commit = master +include ../../erlang.mk diff --git a/ranch/examples/tcp_reverse/README.md b/ranch/examples/tcp_reverse/README.md new file mode 100644 index 0000000..d4ab1de --- /dev/null +++ b/ranch/examples/tcp_reverse/README.md @@ -0,0 +1,33 @@ +Ranch TCP reverse example +========================= + +This example uses a `gen_statem` to handle a protocol to revese input. +See `reverse_protocol.erl` for the implementation. Documentation about +this topic can be found in the guide: + + http://ninenines.eu/docs/en/ranch/HEAD/guide/protocols/#using_gen_statem + +To try this example, you need GNU `make` and `git` in your PATH. + +To build the example, run the following command: + +``` bash +$ make +``` + +To start the release in the foreground: + +``` bash +$ ./_rel/tcp_reverse_example/bin/tcp_reverse_example console +``` + +Then start a telnet session to port 5555: + +``` bash +$ telnet localhost 5555 +``` + +Type in a few words and see them reversed! Amazing! + +Be aware that there is a timeout of 5 seconds without receiving +data before the example server disconnects your session. diff --git a/ranch/examples/tcp_reverse/relx.config b/ranch/examples/tcp_reverse/relx.config new file mode 100644 index 0000000..2b1651f --- /dev/null +++ b/ranch/examples/tcp_reverse/relx.config @@ -0,0 +1,2 @@ +{release, {tcp_reverse_example, "1"}, [tcp_reverse, sasl]}. +{extended_start_script, true}. diff --git a/ranch/examples/tcp_reverse/src/reverse_protocol.erl b/ranch/examples/tcp_reverse/src/reverse_protocol.erl new file mode 100644 index 0000000..73d7aff --- /dev/null +++ b/ranch/examples/tcp_reverse/src/reverse_protocol.erl @@ -0,0 +1,87 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(reverse_protocol). +-behaviour(gen_statem). +-behaviour(ranch_protocol). + +%% API. +-export([start_link/3]). + +%% gen_statem. +-export([callback_mode/0]). +-export([init/1]). +-export([connected/3]). +-export([terminate/3]). +-export([code_change/4]). + +-define(TIMEOUT, 60000). + +-record(state, {ref, transport, socket}). + +%% API. + +start_link(Ref, Transport, Opts) -> + gen_statem:start_link(?MODULE, {Ref, Transport, Opts}, []). + +%% gen_statem. + +callback_mode() -> + [state_functions, state_enter]. + +init({Ref, Transport, _Opts = []}) -> + {ok, connected, #state{ref=Ref, transport=Transport}, ?TIMEOUT}. + +connected(enter, connected, StateData=#state{ + ref=Ref, transport=Transport}) -> + {ok, Socket} = ranch:handshake(Ref), + ok = Transport:setopts(Socket, [{active, once}, {packet, line}]), + {keep_state, StateData#state{socket=Socket}}; +connected(info, {tcp, Socket, Data}, _StateData=#state{ + socket=Socket, transport=Transport}) + when byte_size(Data) >= 1 -> + Transport:setopts(Socket, [{active, once}]), + Transport:send(Socket, reverse_binary(Data)), + {keep_state_and_data, ?TIMEOUT}; +connected(info, {tcp_closed, _Socket}, _StateData) -> + {stop, normal}; +connected(info, {tcp_error, _, Reason}, _StateData) -> + {stop, Reason}; +connected({call, From}, _Request, _StateData) -> + gen_statem:reply(From, ok), + keep_state_and_data; +connected(cast, _Msg, _StateData) -> + keep_state_and_data; +connected(timeout, _Msg, _StateData) -> + {stop, normal}; +connected(_EventType, _Msg, _StateData) -> + {stop, normal}. + +terminate(Reason, StateName, StateData=#state{ + socket=Socket, transport=Transport}) + when Socket=/=undefined, Transport=/=undefined -> + catch Transport:close(Socket), + terminate(Reason, StateName, + StateData#state{socket=undefined, transport=undefined}); +terminate(_Reason, _StateName, _StateData) -> + ok. + +code_change(_OldVsn, StateName, StateData, _Extra) -> + {ok, StateName, StateData}. + +%% Internal. + +reverse_binary(B0) when is_binary(B0) -> + Size = bit_size(B0), + <> = B0, + case <> of + %% Take care of different possible line terminators. + <<$\n, $\r, B2/binary>> -> + %% CR/LF (Windows) + <>; + <<$\n, B2/binary>> -> + %% LF (Linux, Mac OS X and later) + <>; + <<$\r, B2/binary>> -> + %% CR (Mac Classic, ie prior to Mac OS X) + <> + end. diff --git a/ranch/examples/tcp_reverse/src/tcp_reverse_app.erl b/ranch/examples/tcp_reverse/src/tcp_reverse_app.erl new file mode 100644 index 0000000..c97f6ee --- /dev/null +++ b/ranch/examples/tcp_reverse/src/tcp_reverse_app.erl @@ -0,0 +1,20 @@ +%% Feel free to use, reuse and abuse the code in this file. + +%% @private +-module(tcp_reverse_app). +-behaviour(application). + +%% API. +-export([start/2]). +-export([stop/1]). + +%% API. + +start(_Type, _Args) -> + {ok, _} = ranch:start_listener(tcp_reverse, + ranch_tcp, #{socket_opts => [{port, 5555}]}, + reverse_protocol, []), + tcp_reverse_sup:start_link(). + +stop(_State) -> + ok. diff --git a/ranch/examples/tcp_reverse/src/tcp_reverse_sup.erl b/ranch/examples/tcp_reverse/src/tcp_reverse_sup.erl new file mode 100644 index 0000000..4264d18 --- /dev/null +++ b/ranch/examples/tcp_reverse/src/tcp_reverse_sup.erl @@ -0,0 +1,22 @@ +%% Feel free to use, reuse and abuse the code in this file. + +%% @private +-module(tcp_reverse_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([]) -> + {ok, {{one_for_one, 10, 10}, []}}. diff --git a/ranch/src/Makefile b/ranch/src/Makefile new file mode 100644 index 0000000..4bd8ad2 --- /dev/null +++ b/ranch/src/Makefile @@ -0,0 +1,20 @@ +.PHONY: all +.SUFFIXES: .erl .beam + +ERLC?= erlc -server -pa ${PWD} +ERLFLAGS+= +warn_missing_spec +warn_untyped_record + +OBJS= ranch_transport.beam ranch.beam ranch_acceptor.beam ranch_acceptors_sup.beam +OBJS+= ranch_conns_sup.beam ranch_conns_sup_sup.beam ranch_crc32c.beam +OBJS+= ranch_embedded_sup.beam ranch_listener_sup.beam ranch_protocol.beam +OBJS+= ranch_proxy_header.beam ranch_server.beam ranch_server_proxy.beam +OBJS+= ranch_ssl.beam ranch_sup.beam ranch_tcp.beam ranch_app.beam + +all: ${OBJS} + +.erl.beam: + ${ERLC} ${ERLOPTS} ${ERLFLAGS} $< + +clean: + rm -f *.beam + diff --git a/ranch/src/ranch.appup b/ranch/src/ranch.appup new file mode 100644 index 0000000..04f2d2c --- /dev/null +++ b/ranch/src/ranch.appup @@ -0,0 +1,104 @@ +%% ranch_conns_sup is a custom supervisor (special process), +%% not an OTP supervisor. When "supervisor" is used, the +%% module is always loaded before calling system_code_change, +%% for both upgrade and downgrade operations. This is so that +%% the supervisor's init callback of the upgraded/downgraded +%% module gets called. But the custom supervisors in Ranch +%% do not have an init callback and therefore can function +%% like other special processes: when upgrading, load the +%% module then call system_code_change; and when downgrading, +%% call system_code_change and then load the module. + +{"2.1.0", + [{<<"2\\.0\\.[0-9]+.*">>, [ + {apply, {ranch, stop_all_acceptors, []}}, + {load_module, ranch}, + {load_module, ranch_acceptor}, + {update, ranch_acceptors_sup, supervisor}, + {load_module, ranch_app}, + {update, ranch_server, {advanced, []}}, + {update, ranch_conns_sup_sup, supervisor}, + %% See comments at the top of the file about ranch_conns_sup. + {update, ranch_conns_sup, {advanced, []}}, + {load_module, ranch_crc32c}, + {update, ranch_embedded_sup, supervisor}, + {update, ranch_listener_sup, supervisor}, + {load_module, ranch_protocol}, + {load_module, ranch_proxy_header}, + {update, ranch_server_proxy, {advanced, []}}, + {load_module, ranch_ssl}, + {update, ranch_sup, supervisor}, + {load_module, ranch_tcp}, + {load_module, ranch_transport}, + {apply, {ranch, restart_all_acceptors, []}} + ]}], + [{<<"2\\.0\\.[0-9]+.*">>, [ + {apply, {ranch, stop_all_acceptors, []}}, + {load_module, ranch}, + {load_module, ranch_acceptor}, + {update, ranch_acceptors_sup, supervisor}, + {load_module, ranch_app}, + %% See comments at the top of the file about ranch_conns_sup. + {update, ranch_conns_sup, {advanced, []}}, + {update, ranch_conns_sup_sup, supervisor}, + {load_module, ranch_crc32c}, + {update, ranch_embedded_sup, supervisor}, + {update, ranch_listener_sup, supervisor}, + {load_module, ranch_protocol}, + {load_module, ranch_proxy_header}, + {update, ranch_server, {advanced, []}}, + {update, ranch_server_proxy, {advanced, []}}, + {load_module, ranch_ssl}, + {update, ranch_sup, supervisor}, + {load_module, ranch_tcp}, + {load_module, ranch_transport}, + {apply, {ranch, restart_all_acceptors, []}} + ]}] +}. + +{"2.0.0", + [{<<"2\\.0\\.[0-9]+.*">>, [ + {apply, {ranch, stop_all_acceptors, []}}, + {load_module, ranch}, + {load_module, ranch_acceptor}, + {update, ranch_acceptors_sup, supervisor}, + {load_module, ranch_app}, + {update, ranch_server, {advanced, []}}, + {update, ranch_conns_sup_sup, supervisor}, + %% See comments at the top of the file about ranch_conns_sup. + {update, ranch_conns_sup, {advanced, []}}, + {load_module, ranch_crc32c}, + {update, ranch_embedded_sup, supervisor}, + {update, ranch_listener_sup, supervisor}, + {load_module, ranch_protocol}, + {load_module, ranch_proxy_header}, + {update, ranch_server_proxy, {advanced, []}}, + {load_module, ranch_ssl}, + {update, ranch_sup, supervisor}, + {load_module, ranch_tcp}, + {load_module, ranch_transport}, + {apply, {ranch, restart_all_acceptors, []}} + ]}], + [{<<"2\\.0\\.[0-9]+.*">>, [ + {apply, {ranch, stop_all_acceptors, []}}, + {load_module, ranch}, + {load_module, ranch_acceptor}, + {update, ranch_acceptors_sup, supervisor}, + {load_module, ranch_app}, + %% See comments at the top of the file about ranch_conns_sup. + {update, ranch_conns_sup, {advanced, []}}, + {update, ranch_conns_sup_sup, supervisor}, + {load_module, ranch_crc32c}, + {update, ranch_embedded_sup, supervisor}, + {update, ranch_listener_sup, supervisor}, + {load_module, ranch_protocol}, + {load_module, ranch_proxy_header}, + {update, ranch_server, {advanced, []}}, + {update, ranch_server_proxy, {advanced, []}}, + {load_module, ranch_ssl}, + {update, ranch_sup, supervisor}, + {load_module, ranch_tcp}, + {load_module, ranch_transport}, + {apply, {ranch, restart_all_acceptors, []}} + ]}] +}. diff --git a/ranch/src/ranch.erl b/ranch/src/ranch.erl new file mode 100644 index 0000000..c9cc035 --- /dev/null +++ b/ranch/src/ranch.erl @@ -0,0 +1,625 @@ +%% Copyright (c) 2011-2021, Loïc Hoguin +%% Copyright (c) 2020-2021, Jan Uhlig +%% Copyright (c) 2021, Maria Scott +%% +%% 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(ranch). + +-export([start_listener/5]). +-export([normalize_opts/1]). +-export([stop_listener/1]). +-export([suspend_listener/1]). +-export([resume_listener/1]). +-export([stop_all_acceptors/0]). +-export([restart_all_acceptors/0]). +-export([child_spec/5]). +-export([handshake/1]). +-export([handshake/2]). +-export([handshake_continue/1]). +-export([handshake_continue/2]). +-export([handshake_cancel/1]). +-export([recv_proxy_header/2]). +-export([remove_connection/1]). +-export([get_status/1]). +-export([get_addr/1]). +-export([get_port/1]). +-export([get_max_connections/1]). +-export([set_max_connections/2]). +-export([get_transport_options/1]). +-export([set_transport_options/2]). +-export([get_protocol_options/1]). +-export([set_protocol_options/2]). +-export([info/0]). +-export([info/1]). +-export([procs/2]). +-export([wait_for_connections/3]). +-export([wait_for_connections/4]). +-export([filter_options/4]). +-export([set_option_default/3]). +-export([require/1]). +-export([log/4]). + +-type max_conns() :: non_neg_integer() | infinity. +-export_type([max_conns/0]). + +-type opts() :: any() | transport_opts(any()). +-export_type([opts/0]). + +-type alarm(Type, Callback) :: #{ + type := Type, + callback := Callback, + treshold := non_neg_integer(), + cooldown => non_neg_integer() +}. + +-type alarm_num_connections() :: alarm(num_connections, fun((ref(), term(), pid(), [pid()]) -> any())). + +-type transport_opts(SocketOpts) :: #{ + alarms => #{term() => alarm_num_connections()}, + connection_type => worker | supervisor, + handshake_timeout => timeout(), + logger => module(), + max_connections => max_conns(), + num_acceptors => pos_integer(), + num_conns_sups => pos_integer(), + num_listen_sockets => pos_integer(), + post_listen_callback => fun((term()) -> ok | {error, term()}), + shutdown => timeout() | brutal_kill, + socket_opts => SocketOpts +}. +-export_type([transport_opts/1]). + +-type ref() :: any(). +-export_type([ref/0]). + +-spec start_listener(ref(), module(), opts(), module(), any()) + -> supervisor:startchild_ret(). +start_listener(Ref, Transport, TransOpts0, Protocol, ProtoOpts) + when is_atom(Transport), is_atom(Protocol) -> + TransOpts = normalize_opts(TransOpts0), + _ = code:ensure_loaded(Transport), + case {erlang:function_exported(Transport, name, 0), validate_transport_opts(TransOpts)} of + {true, ok} -> + ChildSpec = #{id => {ranch_listener_sup, Ref}, start => {ranch_listener_sup, start_link, [ + Ref, Transport, TransOpts, Protocol, ProtoOpts + ]}, type => supervisor}, + maybe_started(supervisor:start_child(ranch_sup, ChildSpec)); + {false, _} -> + {error, {bad_transport, Transport}}; + {_, TransOptsError} -> + TransOptsError + end. + +-spec normalize_opts(opts()) -> transport_opts(any()). +normalize_opts(Map) when is_map(Map) -> + Map; +normalize_opts(Any) -> + #{socket_opts => Any}. + +-spec validate_transport_opts(transport_opts(any())) -> ok | {error, any()}. +validate_transport_opts(Opts) -> + maps:fold(fun + (Key, Value, ok) -> + case validate_transport_opt(Key, Value, Opts) of + true -> + ok; + false -> + {error, {bad_option, Key}} + end; + (_, _, Acc) -> + Acc + end, ok, Opts). + +-spec validate_transport_opt(any(), any(), transport_opts(any())) -> boolean(). +validate_transport_opt(connection_type, worker, _) -> + true; +validate_transport_opt(connection_type, supervisor, _) -> + true; +validate_transport_opt(handshake_timeout, infinity, _) -> + true; +validate_transport_opt(handshake_timeout, Value, _) -> + is_integer(Value) andalso Value >= 0; +validate_transport_opt(max_connections, infinity, _) -> + true; +validate_transport_opt(max_connections, Value, _) -> + is_integer(Value) andalso Value >= 0; +validate_transport_opt(alarms, Alarms, _) -> + maps:fold( + fun + (_, Opts, true) -> + validate_alarm(Opts); + (_, _, false) -> + false + end, + true, + Alarms); +validate_transport_opt(logger, Value, _) -> + is_atom(Value); +validate_transport_opt(num_acceptors, Value, _) -> + is_integer(Value) andalso Value > 0; +validate_transport_opt(num_conns_sups, Value, _) -> + is_integer(Value) andalso Value > 0; +validate_transport_opt(num_listen_sockets, Value, Opts) -> + is_integer(Value) andalso Value > 0 + andalso Value =< maps:get(num_acceptors, Opts, 10); +validate_transport_opt(post_listen_callback, Value, _) -> + is_function(Value, 1); +validate_transport_opt(shutdown, brutal_kill, _) -> + true; +validate_transport_opt(shutdown, infinity, _) -> + true; +validate_transport_opt(shutdown, Value, _) -> + is_integer(Value) andalso Value >= 0; +validate_transport_opt(socket_opts, _, _) -> + true; +validate_transport_opt(_, _, _) -> + false. + +validate_alarm(Alarm = #{type := num_connections, treshold := Treshold, + callback := Callback}) -> + is_integer(Treshold) andalso Treshold >= 0 + andalso is_function(Callback, 4) + andalso case Alarm of + #{cooldown := Cooldown} -> + is_integer(Cooldown) andalso Cooldown >= 0; + _ -> + true + end; +validate_alarm(_) -> + false. + +maybe_started({error, {{shutdown, + {failed_to_start_child, ranch_acceptors_sup, + {listen_error, _, Reason}}}, _}} = Error) -> + start_error(Reason, Error); +maybe_started(Res) -> + Res. + +start_error(E=eaddrinuse, _) -> {error, E}; +start_error(E=eacces, _) -> {error, E}; +start_error(E=no_cert, _) -> {error, E}; +start_error(_, Error) -> Error. + +-spec stop_listener(ref()) -> ok | {error, not_found}. +stop_listener(Ref) -> + [_, Transport, _, _, _] = ranch_server:get_listener_start_args(Ref), + TransOpts = get_transport_options(Ref), + case supervisor:terminate_child(ranch_sup, {ranch_listener_sup, Ref}) of + ok -> + _ = supervisor:delete_child(ranch_sup, {ranch_listener_sup, Ref}), + ranch_server:cleanup_listener_opts(Ref), + Transport:cleanup(TransOpts); + {error, Reason} -> + {error, Reason} + end. + +-spec suspend_listener(ref()) -> ok | {error, any()}. +suspend_listener(Ref) -> + case get_status(Ref) of + running -> + ListenerSup = ranch_server:get_listener_sup(Ref), + ok = ranch_server:set_addr(Ref, {undefined, undefined}), + supervisor:terminate_child(ListenerSup, ranch_acceptors_sup); + suspended -> + ok + end. + +-spec resume_listener(ref()) -> ok | {error, any()}. +resume_listener(Ref) -> + case get_status(Ref) of + running -> + ok; + suspended -> + ListenerSup = ranch_server:get_listener_sup(Ref), + Res = supervisor:restart_child(ListenerSup, ranch_acceptors_sup), + maybe_resumed(Res) + end. + +maybe_resumed(Error={error, {listen_error, _, Reason}}) -> + start_error(Reason, Error); +maybe_resumed({ok, _}) -> + ok; +maybe_resumed({ok, _, _}) -> + ok; +maybe_resumed(Res) -> + Res. + +-spec stop_all_acceptors() -> ok. +stop_all_acceptors() -> + _ = [ok = do_acceptors(Pid, terminate_child) + || {_, Pid} <- ranch_server:get_listener_sups()], + ok. + +-spec restart_all_acceptors() -> ok. +restart_all_acceptors() -> + _ = [ok = do_acceptors(Pid, restart_child) + || {_, Pid} <- ranch_server:get_listener_sups()], + ok. + +do_acceptors(ListenerSup, F) -> + ListenerChildren = supervisor:which_children(ListenerSup), + case lists:keyfind(ranch_acceptors_sup, 1, ListenerChildren) of + {_, AcceptorsSup, _, _} when is_pid(AcceptorsSup) -> + AcceptorChildren = supervisor:which_children(AcceptorsSup), + %% @todo What about errors? + _ = [supervisor:F(AcceptorsSup, AcceptorId) + || {AcceptorId, _, _, _} <- AcceptorChildren], + ok; + {_, Atom, _, _} -> + {error, Atom} + end. + +-spec child_spec(ref(), module(), opts(), module(), any()) + -> supervisor:child_spec(). +child_spec(Ref, Transport, TransOpts0, Protocol, ProtoOpts) -> + TransOpts = normalize_opts(TransOpts0), + #{id => {ranch_embedded_sup, Ref}, start => {ranch_embedded_sup, start_link, [ + Ref, Transport, TransOpts, Protocol, ProtoOpts + ]}, type => supervisor}. + +-spec handshake(ref()) -> {ok, ranch_transport:socket()} | {continue, any()}. +handshake(Ref) -> + handshake1(Ref, undefined). + +-spec handshake(ref(), any()) -> {ok, ranch_transport:socket()} | {continue, any()}. +handshake(Ref, Opts) -> + handshake1(Ref, {opts, Opts}). + +handshake1(Ref, Opts) -> + receive {handshake, Ref, Transport, CSocket, Timeout} -> + Handshake = handshake_transport(Transport, handshake, CSocket, Opts, Timeout), + handshake_result(Handshake, Ref, Transport, CSocket, Timeout) + end. + +-spec handshake_continue(ref()) -> {ok, ranch_transport:socket()}. +handshake_continue(Ref) -> + handshake_continue1(Ref, undefined). + +-spec handshake_continue(ref(), any()) -> {ok, ranch_transport:socket()}. +handshake_continue(Ref, Opts) -> + handshake_continue1(Ref, {opts, Opts}). + +handshake_continue1(Ref, Opts) -> + receive {handshake_continue, Ref, Transport, CSocket, Timeout} -> + Handshake = handshake_transport(Transport, handshake_continue, CSocket, Opts, Timeout), + handshake_result(Handshake, Ref, Transport, CSocket, Timeout) + end. + +handshake_transport(Transport, Fun, CSocket, undefined, Timeout) -> + Transport:Fun(CSocket, Timeout); +handshake_transport(Transport, Fun, CSocket, {opts, Opts}, Timeout) -> + Transport:Fun(CSocket, Opts, Timeout). + +handshake_result(Result, Ref, Transport, CSocket, Timeout) -> + case Result of + OK = {ok, _} -> + OK; + {ok, CSocket2, Info} -> + self() ! {handshake_continue, Ref, Transport, CSocket2, Timeout}, + {continue, Info}; + {error, {tls_alert, _}} -> + ok = Transport:close(CSocket), + exit(normal); + {error, Reason} when Reason =:= timeout; Reason =:= closed -> + ok = Transport:close(CSocket), + exit(normal); + {error, Reason} -> + ok = Transport:close(CSocket), + error(Reason) + end. + +-spec handshake_cancel(ref()) -> ok. +handshake_cancel(Ref) -> + receive {handshake_continue, Ref, Transport, CSocket, _} -> + Transport:handshake_cancel(CSocket) + end. + +%% Unlike handshake/2 this function always return errors because +%% the communication between the proxy and the server are expected +%% to be reliable. If there is a problem while receiving the proxy +%% header, we probably want to know about it. +-spec recv_proxy_header(ref(), timeout()) + -> {ok, ranch_proxy_header:proxy_info()} + | {error, closed | atom()} + | {error, protocol_error, atom()}. +recv_proxy_header(Ref, Timeout) -> + receive HandshakeState={handshake, Ref, Transport, CSocket, _} -> + self() ! HandshakeState, + Transport:recv_proxy_header(CSocket, Timeout) + end. + +-spec remove_connection(ref()) -> ok. +remove_connection(Ref) -> + ListenerSup = ranch_server:get_listener_sup(Ref), + {_, ConnsSupSup, _, _} = lists:keyfind(ranch_conns_sup_sup, 1, + supervisor:which_children(ListenerSup)), + _ = [ConnsSup ! {remove_connection, Ref, self()} || + {_, ConnsSup, _, _} <- supervisor:which_children(ConnsSupSup)], + ok. + +-spec get_status(ref()) -> running | suspended. +get_status(Ref) -> + ListenerSup = ranch_server:get_listener_sup(Ref), + Children = supervisor:which_children(ListenerSup), + case lists:keyfind(ranch_acceptors_sup, 1, Children) of + {_, undefined, _, _} -> + suspended; + _ -> + running + end. + +-spec get_addr(ref()) -> {inet:ip_address(), inet:port_number()} | + {local, binary()} | {undefined, undefined}. +get_addr(Ref) -> + ranch_server:get_addr(Ref). + +-spec get_port(ref()) -> inet:port_number() | undefined. +get_port(Ref) -> + case get_addr(Ref) of + {local, _} -> + undefined; + {_, Port} -> + Port + end. + +-spec get_connections(ref(), active|all) -> non_neg_integer(). +get_connections(Ref, active) -> + SupCounts = [ranch_conns_sup:active_connections(ConnsSup) || + {_, ConnsSup} <- ranch_server:get_connections_sups(Ref)], + lists:sum(SupCounts); +get_connections(Ref, all) -> + SupCounts = [proplists:get_value(active, supervisor:count_children(ConnsSup)) || + {_, ConnsSup} <- ranch_server:get_connections_sups(Ref)], + lists:sum(SupCounts). + +-spec get_max_connections(ref()) -> max_conns(). +get_max_connections(Ref) -> + ranch_server:get_max_connections(Ref). + +-spec set_max_connections(ref(), max_conns()) -> ok. +set_max_connections(Ref, MaxConnections) -> + ranch_server:set_max_connections(Ref, MaxConnections). + +-spec get_transport_options(ref()) -> transport_opts(any()). +get_transport_options(Ref) -> + ranch_server:get_transport_options(Ref). + +-spec set_transport_options(ref(), opts()) -> ok | {error, term()}. +set_transport_options(Ref, TransOpts0) -> + TransOpts = normalize_opts(TransOpts0), + case validate_transport_opts(TransOpts) of + ok -> + ok = ranch_server:set_transport_options(Ref, TransOpts), + ok = apply_transport_options(Ref, TransOpts); + TransOptsError -> + TransOptsError + end. + +apply_transport_options(Ref, TransOpts) -> + _ = [ConnsSup ! {set_transport_options, TransOpts} + || {_, ConnsSup} <- ranch_server:get_connections_sups(Ref)], + ok. + +-spec get_protocol_options(ref()) -> any(). +get_protocol_options(Ref) -> + ranch_server:get_protocol_options(Ref). + +-spec set_protocol_options(ref(), any()) -> ok. +set_protocol_options(Ref, Opts) -> + ranch_server:set_protocol_options(Ref, Opts). + +-spec info() -> #{ref() := #{atom() := term()}}. +info() -> + lists:foldl( + fun ({Ref, Pid}, Acc) -> + Acc#{Ref => listener_info(Ref, Pid)} + end, + #{}, + ranch_server:get_listener_sups() + ). + +-spec info(ref()) -> #{atom() := term()}. +info(Ref) -> + Pid = ranch_server:get_listener_sup(Ref), + listener_info(Ref, Pid). + +listener_info(Ref, Pid) -> + [_, Transport, _, Protocol, _] = ranch_server:get_listener_start_args(Ref), + Status = get_status(Ref), + {IP, Port} = case get_addr(Ref) of + Addr = {local, _} -> + {Addr, undefined}; + Addr -> + Addr + end, + MaxConns = get_max_connections(Ref), + TransOpts = ranch_server:get_transport_options(Ref), + ProtoOpts = get_protocol_options(Ref), + #{ + pid => Pid, + status => Status, + ip => IP, + port => Port, + max_connections => MaxConns, + active_connections => get_connections(Ref, active), + all_connections => get_connections(Ref, all), + transport => Transport, + transport_options => TransOpts, + protocol => Protocol, + protocol_options => ProtoOpts, + metrics => metrics(Ref) + }. + +-spec procs(ref(), acceptors | connections) -> [pid()]. +procs(Ref, Type) -> + ListenerSup = ranch_server:get_listener_sup(Ref), + procs1(ListenerSup, Type). + +procs1(ListenerSup, acceptors) -> + {_, SupPid, _, _} = lists:keyfind(ranch_acceptors_sup, 1, + supervisor:which_children(ListenerSup)), + try + [Pid || {_, Pid, _, _} <- supervisor:which_children(SupPid)] + catch exit:{noproc, _} -> + [] + end; +procs1(ListenerSup, connections) -> + {_, SupSupPid, _, _} = lists:keyfind(ranch_conns_sup_sup, 1, + supervisor:which_children(ListenerSup)), + Conns= + lists:map(fun ({_, SupPid, _, _}) -> + [Pid || {_, Pid, _, _} <- supervisor:which_children(SupPid)] + end, + supervisor:which_children(SupSupPid) + ), + lists:flatten(Conns). + +-spec metrics(ref()) -> #{}. +metrics(Ref) -> + Counters = ranch_server:get_stats_counters(Ref), + CounterInfo = counters:info(Counters), + NumCounters = maps:get(size, CounterInfo), + NumConnsSups = NumCounters div 2, + lists:foldl( + fun (Id, Acc) -> + Acc#{ + {conns_sup, Id, accept} => counters:get(Counters, 2*Id-1), + {conns_sup, Id, terminate} => counters:get(Counters, 2*Id) + } + end, + #{}, + lists:seq(1, NumConnsSups) + ). + +-spec wait_for_connections + (ref(), '>' | '>=' | '==' | '=<', non_neg_integer()) -> ok; + (ref(), '<', pos_integer()) -> ok. +wait_for_connections(Ref, Op, NumConns) -> + wait_for_connections(Ref, Op, NumConns, 1000). + +-spec wait_for_connections + (ref(), '>' | '>=' | '==' | '=<', non_neg_integer(), non_neg_integer()) -> ok; + (ref(), '<', pos_integer(), non_neg_integer()) -> ok. +wait_for_connections(Ref, Op, NumConns, Interval) -> + validate_op(Op, NumConns), + validate_num_conns(NumConns), + validate_interval(Interval), + wait_for_connections_loop(Ref, Op, NumConns, Interval). + +validate_op('>', _) -> ok; +validate_op('>=', _) -> ok; +validate_op('==', _) -> ok; +validate_op('=<', _) -> ok; +validate_op('<', NumConns) when NumConns > 0 -> ok; +validate_op(_, _) -> error(badarg). + +validate_num_conns(NumConns) when is_integer(NumConns), NumConns >= 0 -> ok; +validate_num_conns(_) -> error(badarg). + +validate_interval(Interval) when is_integer(Interval), Interval >= 0 -> ok; +validate_interval(_) -> error(badarg). + +wait_for_connections_loop(Ref, Op, NumConns, Interval) -> + CurConns = try + get_connections(Ref, all) + catch _:_ -> + 0 + end, + case erlang:Op(CurConns, NumConns) of + true -> + ok; + false when Interval =:= 0 -> + wait_for_connections_loop(Ref, Op, NumConns, Interval); + false -> + timer:sleep(Interval), + wait_for_connections_loop(Ref, Op, NumConns, Interval) + end. + +-spec filter_options([inet | inet6 | {atom(), any()} | {raw, any(), any(), any()}], + [atom()], Acc, module()) -> Acc when Acc :: [any()]. +filter_options(UserOptions, DisallowedKeys, DefaultOptions, Logger) -> + AllowedOptions = filter_user_options(UserOptions, DisallowedKeys, Logger), + lists:foldl(fun merge_options/2, DefaultOptions, AllowedOptions). + +%% 2-tuple options. +filter_user_options([Opt = {Key, _}|Tail], DisallowedKeys, Logger) -> + case lists:member(Key, DisallowedKeys) of + false -> + [Opt|filter_user_options(Tail, DisallowedKeys, Logger)]; + true -> + filter_options_warning(Opt, Logger), + filter_user_options(Tail, DisallowedKeys, Logger) + end; +%% Special option forms. +filter_user_options([inet|Tail], DisallowedKeys, Logger) -> + [inet|filter_user_options(Tail, DisallowedKeys, Logger)]; +filter_user_options([inet6|Tail], DisallowedKeys, Logger) -> + [inet6|filter_user_options(Tail, DisallowedKeys, Logger)]; +filter_user_options([Opt = {raw, _, _, _}|Tail], DisallowedKeys, Logger) -> + [Opt|filter_user_options(Tail, DisallowedKeys, Logger)]; +filter_user_options([Opt|Tail], DisallowedKeys, Logger) -> + filter_options_warning(Opt, Logger), + filter_user_options(Tail, DisallowedKeys, Logger); +filter_user_options([], _, _) -> + []. + +filter_options_warning(Opt, Logger) -> + log(warning, + "Transport option ~p unknown or invalid.~n", + [Opt], Logger). + +merge_options({Key, _} = Option, OptionList) -> + lists:keystore(Key, 1, OptionList, Option); +merge_options(Option, OptionList) -> + [Option|OptionList]. + +-spec set_option_default(Opts, atom(), any()) + -> Opts when Opts :: [{atom(), any()}]. +set_option_default(Opts, Key, Value) -> + case lists:keymember(Key, 1, Opts) of + true -> Opts; + false -> [{Key, Value}|Opts] + end. + +-spec require([atom()]) -> ok. +require([]) -> + ok; +require([App|Tail]) -> + case application:start(App) of + ok -> ok; + {error, {already_started, App}} -> ok + end, + require(Tail). + +-spec log(logger:level(), io:format(), list(), module() | #{logger => module()}) -> ok. +log(Level, Format, Args, Logger) when is_atom(Logger) -> + log(Level, Format, Args, #{logger => Logger}); +log(Level, Format, Args, #{logger := Logger}) + when Logger =/= error_logger -> + _ = Logger:Level(Format, Args), + ok; +%% Because error_logger does not have all the levels +%% we accept we have to do some mapping to error_logger functions. +log(Level, Format, Args, _) -> + Function = case Level of + emergency -> error_msg; + alert -> error_msg; + critical -> error_msg; + error -> error_msg; + warning -> warning_msg; + notice -> warning_msg; + info -> info_msg; + debug -> info_msg + end, + error_logger:Function(Format, Args). diff --git a/ranch/src/ranch_acceptor.erl b/ranch/src/ranch_acceptor.erl new file mode 100644 index 0000000..7d684cd --- /dev/null +++ b/ranch/src/ranch_acceptor.erl @@ -0,0 +1,72 @@ +%% Copyright (c) 2011-2021, 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(ranch_acceptor). + +-export([start_link/5]). +-export([init/4]). +-export([loop/5]). + +-spec start_link(ranch:ref(), pos_integer(), inet:socket(), module(), module()) + -> {ok, pid()}. +start_link(Ref, AcceptorId, LSocket, Transport, Logger) -> + ConnsSup = ranch_server:get_connections_sup(Ref, AcceptorId), + Pid = spawn_link(?MODULE, init, [LSocket, Transport, Logger, ConnsSup]), + {ok, Pid}. + +-spec init(inet:socket(), module(), module(), pid()) -> no_return(). +init(LSocket, Transport, Logger, ConnsSup) -> + MonitorRef = monitor(process, ConnsSup), + loop(LSocket, Transport, Logger, ConnsSup, MonitorRef). + +-spec loop(inet:socket(), module(), module(), pid(), reference()) -> no_return(). +loop(LSocket, Transport, Logger, ConnsSup, MonitorRef) -> + _ = case Transport:accept(LSocket, infinity) of + {ok, CSocket} -> + case Transport:controlling_process(CSocket, ConnsSup) of + ok -> + %% This call will not return until process has been started + %% AND we are below the maximum number of connections. + ranch_conns_sup:start_protocol(ConnsSup, MonitorRef, + CSocket); + {error, _} -> + Transport:close(CSocket) + end; + %% Reduce the accept rate if we run out of file descriptors. + %% We can't accept anymore anyway, so we might as well wait + %% a little for the situation to resolve itself. + {error, emfile} -> + ranch:log(warning, + "Ranch acceptor reducing accept rate: out of file descriptors~n", + [], Logger), + receive after 100 -> ok end; + %% Exit if the listening socket got closed. + {error, closed} -> + exit(closed); + %% Continue otherwise. + {error, _} -> + ok + end, + flush(Logger), + ?MODULE:loop(LSocket, Transport, Logger, ConnsSup, MonitorRef). + +flush(Logger) -> + receive Msg -> + ranch:log(warning, + "Ranch acceptor received unexpected message: ~p~n", + [Msg], Logger), + flush(Logger) + after 0 -> + ok + end. diff --git a/ranch/src/ranch_acceptors_sup.erl b/ranch/src/ranch_acceptors_sup.erl new file mode 100644 index 0000000..52c68dc --- /dev/null +++ b/ranch/src/ranch_acceptors_sup.erl @@ -0,0 +1,103 @@ +%% Copyright (c) 2011-2021, Loïc Hoguin +%% Copyright (c) 2020-2021, Jan Uhlig +%% +%% 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(ranch_acceptors_sup). +-behaviour(supervisor). + +-export([start_link/3]). +-export([init/1]). + +-spec start_link(ranch:ref(), module(), module()) + -> {ok, pid()}. +start_link(Ref, Transport, Logger) -> + supervisor:start_link(?MODULE, [Ref, Transport, Logger]). + +-spec init([term()]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. +init([Ref, Transport, Logger]) -> + TransOpts = ranch_server:get_transport_options(Ref), + NumAcceptors = maps:get(num_acceptors, TransOpts, 10), + NumListenSockets = maps:get(num_listen_sockets, TransOpts, 1), + LSockets = case get(lsockets) of + undefined -> + LSockets1 = start_listen_sockets(Ref, NumListenSockets, Transport, TransOpts, Logger), + put(lsockets, LSockets1), + LSockets1; + LSockets1 -> + LSockets1 + end, + Procs = [begin + LSocketId = (AcceptorId rem NumListenSockets) + 1, + {_, LSocket} = lists:keyfind(LSocketId, 1, LSockets), + #{ + id => {acceptor, self(), AcceptorId}, + start => {ranch_acceptor, start_link, [Ref, AcceptorId, LSocket, Transport, Logger]}, + shutdown => brutal_kill + } + end || AcceptorId <- lists:seq(1, NumAcceptors)], + {ok, {#{intensity => 1 + ceil(math:log2(NumAcceptors))}, Procs}}. + +-spec start_listen_sockets(any(), pos_integer(), module(), map(), module()) + -> [{pos_integer(), inet:socket()}]. +start_listen_sockets(Ref, NumListenSockets, Transport, TransOpts0, Logger) when NumListenSockets > 0 -> + BaseSocket = start_listen_socket(Ref, Transport, TransOpts0, Logger), + {ok, Addr} = Transport:sockname(BaseSocket), + ExtraSockets = case Addr of + {local, _} when NumListenSockets > 1 -> + listen_error(Ref, Transport, TransOpts0, reuseport_local, Logger); + {local, _} -> + []; + {_, Port} -> + SocketOpts = maps:get(socket_opts, TransOpts0, []), + SocketOpts1 = lists:keystore(port, 1, SocketOpts, {port, Port}), + TransOpts1 = TransOpts0#{socket_opts => SocketOpts1}, + [{N, start_listen_socket(Ref, Transport, TransOpts1, Logger)} + || N <- lists:seq(2, NumListenSockets)] + end, + ranch_server:set_addr(Ref, Addr), + [{1, BaseSocket}|ExtraSockets]. + +-spec start_listen_socket(any(), module(), map(), module()) -> inet:socket(). +start_listen_socket(Ref, Transport, TransOpts, Logger) -> + case Transport:listen(TransOpts) of + {ok, Socket} -> + PostListenCb = maps:get(post_listen_callback, TransOpts, fun (_) -> ok end), + case PostListenCb(Socket) of + ok -> + Socket; + {error, Reason} -> + listen_error(Ref, Transport, TransOpts, Reason, Logger) + end; + {error, Reason} -> + listen_error(Ref, Transport, TransOpts, Reason, Logger) + end. + +-spec listen_error(any(), module(), any(), atom(), module()) -> no_return(). +listen_error(Ref, Transport, TransOpts0, Reason, Logger) -> + SocketOpts0 = maps:get(socket_opts, TransOpts0, []), + SocketOpts1 = [{cert, '...'}|proplists:delete(cert, SocketOpts0)], + SocketOpts2 = [{key, '...'}|proplists:delete(key, SocketOpts1)], + SocketOpts = [{cacerts, '...'}|proplists:delete(cacerts, SocketOpts2)], + TransOpts = TransOpts0#{socket_opts => SocketOpts}, + ranch:log(error, + "Failed to start Ranch listener ~p in ~p:listen(~999999p) for reason ~p (~s)~n", + [Ref, Transport, TransOpts, Reason, format_error(Reason)], Logger), + exit({listen_error, Ref, Reason}). + +format_error(no_cert) -> + "no certificate provided; see cert, certfile, sni_fun or sni_hosts options"; +format_error(reuseport_local) -> + "num_listen_sockets must be set to 1 for local sockets"; +format_error(Reason) -> + inet:format_error(Reason). diff --git a/ranch/src/ranch_app.erl b/ranch/src/ranch_app.erl new file mode 100644 index 0000000..f6aeb26 --- /dev/null +++ b/ranch/src/ranch_app.erl @@ -0,0 +1,48 @@ +%% Copyright (c) 2011-2021, 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(ranch_app). +-behaviour(application). + +-export([start/2]). +-export([stop/1]). +-export([profile_output/0]). + +-spec start(application:start_type(), term()) -> {ok, pid()} | {error, term()}. +start(_, _) -> + _ = consider_profiling(), + ranch_server = ets:new(ranch_server, [ + ordered_set, public, named_table]), + ranch_sup:start_link(). + +-spec stop(term()) -> ok. +stop(_) -> + ok. + +-spec profile_output() -> ok. +profile_output() -> + eprof:stop_profiling(), + eprof:log("procs.profile"), + eprof:analyze(procs), + eprof:log("total.profile"), + eprof:analyze(total). + +consider_profiling() -> + case application:get_env(profile) of + {ok, true} -> + {ok, _Pid} = eprof:start(), + eprof:start_profiling([self()]); + _ -> + not_profiling + end. diff --git a/ranch/src/ranch_conns_sup.erl b/ranch/src/ranch_conns_sup.erl new file mode 100644 index 0000000..649f856 --- /dev/null +++ b/ranch/src/ranch_conns_sup.erl @@ -0,0 +1,508 @@ +%% Copyright (c) 2011-2021, Loïc Hoguin +%% Copyright (c) 2021, Maria Scott +%% +%% 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. + +%% Make sure to never reload this module outside a release upgrade, +%% as calling l(ranch_conns_sup) twice will kill the process and all +%% the currently open connections. +-module(ranch_conns_sup). + +%% API. +-export([start_link/6]). +-export([start_protocol/3]). +-export([active_connections/1]). + +%% Supervisor internals. +-export([init/7]). +-export([system_continue/3]). +-export([system_terminate/4]). +-export([system_code_change/4]). + +-type conn_type() :: worker | supervisor. +-type shutdown() :: brutal_kill | timeout(). + +-record(state, { + parent = undefined :: pid(), + ref :: ranch:ref(), + id :: pos_integer(), + conn_type :: conn_type(), + shutdown :: shutdown(), + transport = undefined :: module(), + protocol = undefined :: module(), + opts :: any(), + handshake_timeout :: timeout(), + max_conns = undefined :: ranch:max_conns(), + stats_counters_ref :: counters:counters_ref(), + alarms = #{} :: #{term() => {map(), undefined | reference()}}, + logger = undefined :: module() +}). + +%% API. + +-spec start_link(ranch:ref(), pos_integer(), module(), any(), module(), module()) -> {ok, pid()}. +start_link(Ref, Id, Transport, TransOpts, Protocol, Logger) -> + proc_lib:start_link(?MODULE, init, + [self(), Ref, Id, Transport, TransOpts, Protocol, Logger]). + +%% We can safely assume we are on the same node as the supervisor. +%% +%% We can also safely avoid having a monitor and a timeout here +%% because only three things can happen: +%% * The supervisor died; rest_for_one strategy killed all acceptors +%% so this very calling process is going to di-- +%% * There's too many connections, the supervisor will resume the +%% acceptor only when we get below the limit again. +%% * The supervisor is overloaded, there's either too many acceptors +%% or the max_connections limit is too large. It's better if we +%% don't keep accepting connections because this leaves +%% more room for the situation to be resolved. +%% +%% We do not need the reply, we only need the ok from the supervisor +%% to continue. The supervisor sends its own pid when the acceptor can +%% continue. +-spec start_protocol(pid(), reference(), inet:socket()) -> ok. +start_protocol(SupPid, MonitorRef, Socket) -> + SupPid ! {?MODULE, start_protocol, self(), Socket}, + receive + SupPid -> + ok; + {'DOWN', MonitorRef, process, SupPid, Reason} -> + error(Reason) + end. + +%% We can't make the above assumptions here. This function might be +%% called from anywhere. +-spec active_connections(pid()) -> non_neg_integer(). +active_connections(SupPid) -> + Tag = erlang:monitor(process, SupPid), + catch erlang:send(SupPid, {?MODULE, active_connections, self(), Tag}, + [noconnect]), + receive + {Tag, Ret} -> + erlang:demonitor(Tag, [flush]), + Ret; + {'DOWN', Tag, _, _, noconnection} -> + exit({nodedown, node(SupPid)}); + {'DOWN', Tag, _, _, Reason} -> + exit(Reason) + after 5000 -> + erlang:demonitor(Tag, [flush]), + exit(timeout) + end. + +%% Supervisor internals. + +-spec init(pid(), ranch:ref(), pos_integer(), module(), any(), module(), module()) -> no_return(). +init(Parent, Ref, Id, Transport, TransOpts, Protocol, Logger) -> + process_flag(trap_exit, true), + ok = ranch_server:set_connections_sup(Ref, Id, self()), + MaxConns = ranch_server:get_max_connections(Ref), + Alarms = get_alarms(TransOpts), + ConnType = maps:get(connection_type, TransOpts, worker), + Shutdown = maps:get(shutdown, TransOpts, 5000), + HandshakeTimeout = maps:get(handshake_timeout, TransOpts, 5000), + ProtoOpts = ranch_server:get_protocol_options(Ref), + StatsCounters = ranch_server:get_stats_counters(Ref), + ok = proc_lib:init_ack(Parent, {ok, self()}), + loop(#state{parent=Parent, ref=Ref, id=Id, conn_type=ConnType, + shutdown=Shutdown, transport=Transport, protocol=Protocol, + opts=ProtoOpts, stats_counters_ref=StatsCounters, + handshake_timeout=HandshakeTimeout, + max_conns=MaxConns, alarms=Alarms, + logger=Logger}, 0, 0, []). + +loop(State=#state{parent=Parent, ref=Ref, id=Id, conn_type=ConnType, + transport=Transport, protocol=Protocol, opts=Opts, stats_counters_ref=StatsCounters, + alarms=Alarms, max_conns=MaxConns, logger=Logger}, CurConns, NbChildren, Sleepers) -> + receive + {?MODULE, start_protocol, To, Socket} -> + try Protocol:start_link(Ref, Transport, Opts) of + {ok, Pid} -> + inc_accept(StatsCounters, Id, 1), + handshake(State, CurConns, NbChildren, Sleepers, To, Socket, Pid, Pid); + {ok, SupPid, ProtocolPid} when ConnType =:= supervisor -> + inc_accept(StatsCounters, Id, 1), + handshake(State, CurConns, NbChildren, Sleepers, To, Socket, SupPid, ProtocolPid); + Ret -> + To ! self(), + ranch:log(error, + "Ranch listener ~p connection process start failure; " + "~p:start_link/3 returned: ~999999p~n", + [Ref, Protocol, Ret], Logger), + Transport:close(Socket), + loop(State, CurConns, NbChildren, Sleepers) + catch Class:Reason -> + To ! self(), + ranch:log(error, + "Ranch listener ~p connection process start failure; " + "~p:start_link/3 crashed with reason: ~p:~999999p~n", + [Ref, Protocol, Class, Reason], Logger), + Transport:close(Socket), + loop(State, CurConns, NbChildren, Sleepers) + end; + {?MODULE, active_connections, To, Tag} -> + To ! {Tag, CurConns}, + loop(State, CurConns, NbChildren, Sleepers); + %% Remove a connection from the count of connections. + {remove_connection, Ref, Pid} -> + case put(Pid, removed) of + active when Sleepers =:= [] -> + loop(State, CurConns - 1, NbChildren, Sleepers); + active -> + [To|Sleepers2] = Sleepers, + To ! self(), + loop(State, CurConns - 1, NbChildren, Sleepers2); + removed -> + loop(State, CurConns, NbChildren, Sleepers); + undefined -> + _ = erase(Pid), + loop(State, CurConns, NbChildren, Sleepers) + end; + %% Upgrade the max number of connections allowed concurrently. + %% We resume all sleeping acceptors if this number increases. + {set_max_conns, MaxConns2} when MaxConns2 > MaxConns -> + _ = [To ! self() || To <- Sleepers], + loop(State#state{max_conns=MaxConns2}, + CurConns, NbChildren, []); + {set_max_conns, MaxConns2} -> + loop(State#state{max_conns=MaxConns2}, + CurConns, NbChildren, Sleepers); + %% Upgrade the transport options. + {set_transport_options, TransOpts} -> + set_transport_options(State, CurConns, NbChildren, Sleepers, TransOpts); + %% Upgrade the protocol options. + {set_protocol_options, Opts2} -> + loop(State#state{opts=Opts2}, + CurConns, NbChildren, Sleepers); + {timeout, _, {activate_alarm, AlarmName}} when is_map_key(AlarmName, Alarms) -> + {AlarmOpts, _} = maps:get(AlarmName, Alarms), + NewAlarm = trigger_alarm(Ref, AlarmName, {AlarmOpts, undefined}, CurConns), + loop(State#state{alarms=Alarms#{AlarmName => NewAlarm}}, CurConns, NbChildren, Sleepers); + {timeout, _, {activate_alarm, _}} -> + loop(State, CurConns, NbChildren, Sleepers); + {'EXIT', Parent, Reason} -> + terminate(State, Reason, NbChildren); + {'EXIT', Pid, Reason} when Sleepers =:= [] -> + case erase(Pid) of + active -> + inc_terminate(StatsCounters, Id, 1), + report_error(Logger, Ref, Protocol, Pid, Reason), + loop(State, CurConns - 1, NbChildren - 1, Sleepers); + removed -> + inc_terminate(StatsCounters, Id, 1), + report_error(Logger, Ref, Protocol, Pid, Reason), + loop(State, CurConns, NbChildren - 1, Sleepers); + undefined -> + loop(State, CurConns, NbChildren, Sleepers) + end; + %% Resume a sleeping acceptor if needed. + {'EXIT', Pid, Reason} -> + case erase(Pid) of + active when CurConns > MaxConns -> + inc_terminate(StatsCounters, Id, 1), + report_error(Logger, Ref, Protocol, Pid, Reason), + loop(State, CurConns - 1, NbChildren - 1, Sleepers); + active -> + inc_terminate(StatsCounters, Id, 1), + report_error(Logger, Ref, Protocol, Pid, Reason), + [To|Sleepers2] = Sleepers, + To ! self(), + loop(State, CurConns - 1, NbChildren - 1, Sleepers2); + removed -> + inc_terminate(StatsCounters, Id, 1), + report_error(Logger, Ref, Protocol, Pid, Reason), + loop(State, CurConns, NbChildren - 1, Sleepers); + undefined -> + loop(State, CurConns, NbChildren, Sleepers) + end; + {system, From, Request} -> + sys:handle_system_msg(Request, From, Parent, ?MODULE, [], + {State, CurConns, NbChildren, Sleepers}); + %% Calls from the supervisor module. + {'$gen_call', {To, Tag}, which_children} -> + Children = [{Protocol, Pid, ConnType, [Protocol]} + || {Pid, Type} <- get(), + Type =:= active orelse Type =:= removed], + To ! {Tag, Children}, + loop(State, CurConns, NbChildren, Sleepers); + {'$gen_call', {To, Tag}, count_children} -> + Counts = case ConnType of + worker -> [{supervisors, 0}, {workers, NbChildren}]; + supervisor -> [{supervisors, NbChildren}, {workers, 0}] + end, + Counts2 = [{specs, 1}, {active, NbChildren}|Counts], + To ! {Tag, Counts2}, + loop(State, CurConns, NbChildren, Sleepers); + {'$gen_call', {To, Tag}, _} -> + To ! {Tag, {error, ?MODULE}}, + loop(State, CurConns, NbChildren, Sleepers); + Msg -> + ranch:log(error, + "Ranch listener ~p received unexpected message ~p~n", + [Ref, Msg], Logger), + loop(State, CurConns, NbChildren, Sleepers) + end. + +handshake(State=#state{ref=Ref, transport=Transport, handshake_timeout=HandshakeTimeout, + max_conns=MaxConns, alarms=Alarms0}, CurConns, NbChildren, Sleepers, To, Socket, SupPid, ProtocolPid) -> + case Transport:controlling_process(Socket, ProtocolPid) of + ok -> + ProtocolPid ! {handshake, Ref, Transport, Socket, HandshakeTimeout}, + put(SupPid, active), + CurConns2 = CurConns + 1, + Sleepers2 = if CurConns2 < MaxConns -> + To ! self(), + Sleepers; + true -> + [To|Sleepers] + end, + Alarms1 = trigger_alarms(Ref, Alarms0, CurConns2), + loop(State#state{alarms=Alarms1}, CurConns2, NbChildren + 1, Sleepers2); + {error, _} -> + Transport:close(Socket), + %% Only kill the supervised pid, because the connection's pid, + %% when different, is supposed to be sitting under it and linked. + exit(SupPid, kill), + To ! self(), + loop(State, CurConns, NbChildren, Sleepers) + end. + +trigger_alarms(Ref, Alarms, CurConns) -> + maps:map( + fun + (AlarmName, Alarm) -> + trigger_alarm(Ref, AlarmName, Alarm, CurConns) + end, + Alarms + ). + +trigger_alarm(Ref, AlarmName, {Opts=#{treshold := Treshold, callback := Callback}, undefined}, CurConns) when CurConns >= Treshold -> + ActiveConns = [Pid || {Pid, active} <- get()], + case Callback of + {Mod, Fun} -> + spawn(Mod, Fun, [Ref, AlarmName, self(), ActiveConns]); + _ -> + Self = self(), + spawn(fun () -> Callback(Ref, AlarmName, Self, ActiveConns) end) + end, + {Opts, schedule_activate_alarm(AlarmName, Opts)}; +trigger_alarm(_, _, Alarm, _) -> + Alarm. + +schedule_activate_alarm(AlarmName, #{cooldown := Cooldown}) when Cooldown > 0 -> + erlang:start_timer(Cooldown, self(), {activate_alarm, AlarmName}); +schedule_activate_alarm(_, _) -> + undefined. + +get_alarms(#{alarms := Alarms}) when is_map(Alarms) -> + maps:fold( + fun + (Name, Opts = #{type := num_connections, cooldown := _}, Acc) -> + Acc#{Name => {Opts, undefined}}; + (Name, Opts = #{type := num_connections}, Acc) -> + Acc#{Name => {Opts#{cooldown => 5000}, undefined}}; + (_, _, Acc) -> Acc + end, + #{}, + Alarms + ); +get_alarms(_) -> + #{}. + +set_transport_options(State=#state{max_conns=MaxConns0}, CurConns, NbChildren, Sleepers0, TransOpts) -> + MaxConns1 = maps:get(max_connections, TransOpts, 1024), + HandshakeTimeout = maps:get(handshake_timeout, TransOpts, 5000), + Shutdown = maps:get(shutdown, TransOpts, 5000), + Sleepers1 = case MaxConns1 > MaxConns0 of + true -> + _ = [To ! self() || To <- Sleepers0], + []; + false -> + Sleepers0 + end, + State1=set_alarm_option(State, TransOpts, CurConns), + loop(State1#state{max_conns=MaxConns1, handshake_timeout=HandshakeTimeout, shutdown=Shutdown}, + CurConns, NbChildren, Sleepers1). + +set_alarm_option(State=#state{ref=Ref, alarms=OldAlarms}, TransOpts, CurConns) -> + NewAlarms0 = get_alarms(TransOpts), + NewAlarms1 = merge_alarms(OldAlarms, NewAlarms0), + NewAlarms2 = trigger_alarms(Ref, NewAlarms1, CurConns), + State#state{alarms=NewAlarms2}. + +merge_alarms(Old, New) -> + OldList = lists:sort(maps:to_list(Old)), + NewList = lists:sort(maps:to_list(New)), + Merged = merge_alarms(OldList, NewList, []), + maps:from_list(Merged). + +merge_alarms([], News, Acc) -> + News ++ Acc; +merge_alarms([{_, {_, undefined}}|Olds], [], Acc) -> + merge_alarms(Olds, [], Acc); +merge_alarms([{_, {_, Timer}}|Olds], [], Acc) -> + _ = cancel_alarm_reactivation_timer(Timer), + merge_alarms(Olds, [], Acc); +merge_alarms([{Name, {OldOpts, Timer}}|Olds], [{Name, {NewOpts, _}}|News], Acc) -> + merge_alarms(Olds, News, [{Name, {NewOpts, adapt_alarm_timer(Name, Timer, OldOpts, NewOpts)}}|Acc]); +merge_alarms([{OldName, {_, Timer}}|Olds], News=[{NewName, _}|_], Acc) when OldName < NewName -> + _ = cancel_alarm_reactivation_timer(Timer), + merge_alarms(Olds, News, Acc); +merge_alarms(Olds, [New|News], Acc) -> + merge_alarms(Olds, News, [New|Acc]). + +%% Not in cooldown. +adapt_alarm_timer(_, undefined, _, _) -> + undefined; +%% Cooldown unchanged. +adapt_alarm_timer(_, Timer, #{cooldown := Cooldown}, #{cooldown := Cooldown}) -> + Timer; +%% Cooldown changed to no cooldown, cancel cooldown timer. +adapt_alarm_timer(_, Timer, _, #{cooldown := 0}) -> + _ = cancel_alarm_reactivation_timer(Timer), + undefined; +%% Cooldown changed, cancel current and start new timer taking the already elapsed time into account. +adapt_alarm_timer(Name, Timer, #{cooldown := OldCooldown}, #{cooldown := NewCooldown}) -> + OldTimeLeft = cancel_alarm_reactivation_timer(Timer), + case NewCooldown-OldCooldown+OldTimeLeft of + NewTimeLeft when NewTimeLeft>0 -> + erlang:start_timer(NewTimeLeft, self(), {activate_alarm, Name}); + _ -> + undefined + end. + +cancel_alarm_reactivation_timer(Timer) -> + case erlang:cancel_timer(Timer) of + %% Timer had already expired when we tried to cancel it, so we flush the + %% reactivation message it sent and return 0 as remaining time. + false -> + ok = receive {timeout, Timer, {activate_alarm, _}} -> ok after 0 -> ok end, + 0; + %% Timer has not yet expired, we return the amount of time that was remaining. + TimeLeft -> + TimeLeft + end. + +-spec terminate(#state{}, any(), non_neg_integer()) -> no_return(). +terminate(#state{shutdown=brutal_kill, id=Id, + stats_counters_ref=StatsCounters}, Reason, NbChildren) -> + kill_children(get_keys(active)), + kill_children(get_keys(removed)), + inc_terminate(StatsCounters, Id, NbChildren), + exit(Reason); +%% Attempt to gracefully shutdown all children. +terminate(#state{shutdown=Shutdown, id=Id, + stats_counters_ref=StatsCounters}, Reason, NbChildren) -> + shutdown_children(get_keys(active)), + shutdown_children(get_keys(removed)), + _ = if + Shutdown =:= infinity -> + ok; + true -> + erlang:send_after(Shutdown, self(), kill) + end, + wait_children(NbChildren), + inc_terminate(StatsCounters, Id, NbChildren), + exit(Reason). + +inc_accept(StatsCounters, Id, N) -> + %% Accepts are counted in the odd indexes. + counters:add(StatsCounters, 2*Id-1, N). + +inc_terminate(StatsCounters, Id, N) -> + %% Terminates are counted in the even indexes. + counters:add(StatsCounters, 2*Id, N). + +%% Kill all children and then exit. We unlink first to avoid +%% getting a message for each child getting killed. +kill_children(Pids) -> + _ = [begin + unlink(P), + exit(P, kill) + end || P <- Pids], + ok. + +%% Monitor processes so we can know which ones have shutdown +%% before the timeout. Unlink so we avoid receiving an extra +%% message. Then send a shutdown exit signal. +shutdown_children(Pids) -> + _ = [begin + monitor(process, P), + unlink(P), + exit(P, shutdown) + end || P <- Pids], + ok. + +wait_children(0) -> + ok; +wait_children(NbChildren) -> + receive + {'DOWN', _, process, Pid, _} -> + case erase(Pid) of + active -> wait_children(NbChildren - 1); + removed -> wait_children(NbChildren - 1); + _ -> wait_children(NbChildren) + end; + kill -> + Active = get_keys(active), + _ = [exit(P, kill) || P <- Active], + Removed = get_keys(removed), + _ = [exit(P, kill) || P <- Removed], + ok + end. + +-spec system_continue(_, _, any()) -> no_return(). +system_continue(_, _, {State, CurConns, NbChildren, Sleepers}) -> + loop(State, CurConns, NbChildren, Sleepers). + +-spec system_terminate(any(), _, _, _) -> no_return(). +system_terminate(Reason, _, _, {State, _, NbChildren, _}) -> + terminate(State, Reason, NbChildren). + +-spec system_code_change(any(), _, _, _) -> {ok, any()}. +system_code_change({#state{parent=Parent, ref=Ref, conn_type=ConnType, + shutdown=Shutdown, transport=Transport, protocol=Protocol, + opts=Opts, handshake_timeout=HandshakeTimeout, + max_conns=MaxConns, logger=Logger}, CurConns, NbChildren, + Sleepers}, _, {down, _}, _) -> + {ok, {{state, Parent, Ref, ConnType, Shutdown, Transport, Protocol, + Opts, HandshakeTimeout, MaxConns, Logger}, CurConns, NbChildren, + Sleepers}}; +system_code_change({{state, Parent, Ref, ConnType, Shutdown, Transport, Protocol, + Opts, HandshakeTimeout, MaxConns, Logger}, CurConns, NbChildren, + Sleepers}, _, _, _) -> + Self = self(), + [Id] = [Id || {Id, Pid} <- ranch_server:get_connections_sups(Ref), Pid=:=Self], + StatsCounters = ranch_server:get_stats_counters(Ref), + {ok, {#state{parent=Parent, ref=Ref, id=Id, conn_type=ConnType, shutdown=Shutdown, + transport=Transport, protocol=Protocol, opts=Opts, + handshake_timeout=HandshakeTimeout, max_conns=MaxConns, + stats_counters_ref=StatsCounters, + logger=Logger}, CurConns, NbChildren, Sleepers}}; +system_code_change(Misc, _, _, _) -> + {ok, Misc}. + +%% We use ~999999p here instead of ~w because the latter doesn't +%% support printable strings. +report_error(_, _, _, _, normal) -> + ok; +report_error(_, _, _, _, shutdown) -> + ok; +report_error(_, _, _, _, {shutdown, _}) -> + ok; +report_error(Logger, Ref, Protocol, Pid, Reason) -> + ranch:log(error, + "Ranch listener ~p had connection process started with " + "~p:start_link/3 at ~p exit with reason: ~999999p~n", + [Ref, Protocol, Pid, Reason], Logger). diff --git a/ranch/src/ranch_conns_sup_sup.erl b/ranch/src/ranch_conns_sup_sup.erl new file mode 100644 index 0000000..c532cec --- /dev/null +++ b/ranch/src/ranch_conns_sup_sup.erl @@ -0,0 +1,42 @@ +%% Copyright (c) 2019-2021, Jan Uhlig +%% +%% 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(ranch_conns_sup_sup). + +-behaviour(supervisor). + +-export([start_link/4]). +-export([init/1]). + +-spec start_link(ranch:ref(), module(), module(), module()) -> {ok, pid()}. +start_link(Ref, Transport, Protocol, Logger) -> + ok = ranch_server:cleanup_connections_sups(Ref), + supervisor:start_link(?MODULE, { + Ref, Transport, Protocol, Logger + }). + +-spec init({ranch:ref(), module(), module(), module()}) + -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. +init({Ref, Transport, Protocol, Logger}) -> + TransOpts = ranch_server:get_transport_options(Ref), + NumAcceptors = maps:get(num_acceptors, TransOpts, 10), + NumConnsSups = maps:get(num_conns_sups, TransOpts, NumAcceptors), + StatsCounters = counters:new(2*NumConnsSups, []), + ok = ranch_server:set_stats_counters(Ref, StatsCounters), + ChildSpecs = [#{ + id => {ranch_conns_sup, N}, + start => {ranch_conns_sup, start_link, [Ref, N, Transport, TransOpts, Protocol, Logger]}, + type => supervisor + } || N <- lists:seq(1, NumConnsSups)], + {ok, {#{intensity => 1 + ceil(math:log2(NumConnsSups))}, ChildSpecs}}. diff --git a/ranch/src/ranch_crc32c.erl b/ranch/src/ranch_crc32c.erl new file mode 100644 index 0000000..9512b87 --- /dev/null +++ b/ranch/src/ranch_crc32c.erl @@ -0,0 +1,115 @@ +%% Copyright (c) 2018-2021, 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(ranch_crc32c). + +-export([crc32c/1]). +-export([crc32c/2]). + +-define(CRC32C_TABLE, { + 16#00000000, 16#F26B8303, 16#E13B70F7, 16#1350F3F4, + 16#C79A971F, 16#35F1141C, 16#26A1E7E8, 16#D4CA64EB, + 16#8AD958CF, 16#78B2DBCC, 16#6BE22838, 16#9989AB3B, + 16#4D43CFD0, 16#BF284CD3, 16#AC78BF27, 16#5E133C24, + 16#105EC76F, 16#E235446C, 16#F165B798, 16#030E349B, + 16#D7C45070, 16#25AFD373, 16#36FF2087, 16#C494A384, + 16#9A879FA0, 16#68EC1CA3, 16#7BBCEF57, 16#89D76C54, + 16#5D1D08BF, 16#AF768BBC, 16#BC267848, 16#4E4DFB4B, + 16#20BD8EDE, 16#D2D60DDD, 16#C186FE29, 16#33ED7D2A, + 16#E72719C1, 16#154C9AC2, 16#061C6936, 16#F477EA35, + 16#AA64D611, 16#580F5512, 16#4B5FA6E6, 16#B93425E5, + 16#6DFE410E, 16#9F95C20D, 16#8CC531F9, 16#7EAEB2FA, + 16#30E349B1, 16#C288CAB2, 16#D1D83946, 16#23B3BA45, + 16#F779DEAE, 16#05125DAD, 16#1642AE59, 16#E4292D5A, + 16#BA3A117E, 16#4851927D, 16#5B016189, 16#A96AE28A, + 16#7DA08661, 16#8FCB0562, 16#9C9BF696, 16#6EF07595, + 16#417B1DBC, 16#B3109EBF, 16#A0406D4B, 16#522BEE48, + 16#86E18AA3, 16#748A09A0, 16#67DAFA54, 16#95B17957, + 16#CBA24573, 16#39C9C670, 16#2A993584, 16#D8F2B687, + 16#0C38D26C, 16#FE53516F, 16#ED03A29B, 16#1F682198, + 16#5125DAD3, 16#A34E59D0, 16#B01EAA24, 16#42752927, + 16#96BF4DCC, 16#64D4CECF, 16#77843D3B, 16#85EFBE38, + 16#DBFC821C, 16#2997011F, 16#3AC7F2EB, 16#C8AC71E8, + 16#1C661503, 16#EE0D9600, 16#FD5D65F4, 16#0F36E6F7, + 16#61C69362, 16#93AD1061, 16#80FDE395, 16#72966096, + 16#A65C047D, 16#5437877E, 16#4767748A, 16#B50CF789, + 16#EB1FCBAD, 16#197448AE, 16#0A24BB5A, 16#F84F3859, + 16#2C855CB2, 16#DEEEDFB1, 16#CDBE2C45, 16#3FD5AF46, + 16#7198540D, 16#83F3D70E, 16#90A324FA, 16#62C8A7F9, + 16#B602C312, 16#44694011, 16#5739B3E5, 16#A55230E6, + 16#FB410CC2, 16#092A8FC1, 16#1A7A7C35, 16#E811FF36, + 16#3CDB9BDD, 16#CEB018DE, 16#DDE0EB2A, 16#2F8B6829, + 16#82F63B78, 16#709DB87B, 16#63CD4B8F, 16#91A6C88C, + 16#456CAC67, 16#B7072F64, 16#A457DC90, 16#563C5F93, + 16#082F63B7, 16#FA44E0B4, 16#E9141340, 16#1B7F9043, + 16#CFB5F4A8, 16#3DDE77AB, 16#2E8E845F, 16#DCE5075C, + 16#92A8FC17, 16#60C37F14, 16#73938CE0, 16#81F80FE3, + 16#55326B08, 16#A759E80B, 16#B4091BFF, 16#466298FC, + 16#1871A4D8, 16#EA1A27DB, 16#F94AD42F, 16#0B21572C, + 16#DFEB33C7, 16#2D80B0C4, 16#3ED04330, 16#CCBBC033, + 16#A24BB5A6, 16#502036A5, 16#4370C551, 16#B11B4652, + 16#65D122B9, 16#97BAA1BA, 16#84EA524E, 16#7681D14D, + 16#2892ED69, 16#DAF96E6A, 16#C9A99D9E, 16#3BC21E9D, + 16#EF087A76, 16#1D63F975, 16#0E330A81, 16#FC588982, + 16#B21572C9, 16#407EF1CA, 16#532E023E, 16#A145813D, + 16#758FE5D6, 16#87E466D5, 16#94B49521, 16#66DF1622, + 16#38CC2A06, 16#CAA7A905, 16#D9F75AF1, 16#2B9CD9F2, + 16#FF56BD19, 16#0D3D3E1A, 16#1E6DCDEE, 16#EC064EED, + 16#C38D26C4, 16#31E6A5C7, 16#22B65633, 16#D0DDD530, + 16#0417B1DB, 16#F67C32D8, 16#E52CC12C, 16#1747422F, + 16#49547E0B, 16#BB3FFD08, 16#A86F0EFC, 16#5A048DFF, + 16#8ECEE914, 16#7CA56A17, 16#6FF599E3, 16#9D9E1AE0, + 16#D3D3E1AB, 16#21B862A8, 16#32E8915C, 16#C083125F, + 16#144976B4, 16#E622F5B7, 16#F5720643, 16#07198540, + 16#590AB964, 16#AB613A67, 16#B831C993, 16#4A5A4A90, + 16#9E902E7B, 16#6CFBAD78, 16#7FAB5E8C, 16#8DC0DD8F, + 16#E330A81A, 16#115B2B19, 16#020BD8ED, 16#F0605BEE, + 16#24AA3F05, 16#D6C1BC06, 16#C5914FF2, 16#37FACCF1, + 16#69E9F0D5, 16#9B8273D6, 16#88D28022, 16#7AB90321, + 16#AE7367CA, 16#5C18E4C9, 16#4F48173D, 16#BD23943E, + 16#F36E6F75, 16#0105EC76, 16#12551F82, 16#E03E9C81, + 16#34F4F86A, 16#C69F7B69, 16#D5CF889D, 16#27A40B9E, + 16#79B737BA, 16#8BDCB4B9, 16#988C474D, 16#6AE7C44E, + 16#BE2DA0A5, 16#4C4623A6, 16#5F16D052, 16#AD7D5351 +}). + +%% The interface mirrors erlang:crc32/1,2. +-spec crc32c(iodata()) -> non_neg_integer(). +crc32c(Data) -> + do_crc32c(16#ffffffff, iolist_to_binary(Data)). + +-spec crc32c(CRC, iodata()) -> CRC when CRC::non_neg_integer(). +crc32c(OldCrc, Data) -> + do_crc32c(OldCrc bxor 16#ffffffff, iolist_to_binary(Data)). + +do_crc32c(OldCrc, <>) -> + do_crc32c((OldCrc bsr 8) bxor element(1 + ((OldCrc bxor C) band 16#ff), ?CRC32C_TABLE), + Rest); +do_crc32c(OldCrc, <<>>) -> + OldCrc bxor 16#ffffffff. + +-ifdef(TEST). +crc32c_test_() -> + Tests = [ + %% Tests from RFC3720 B.4. + {<<0:32/unit:8>>, 16#8a9136aa}, + {iolist_to_binary([16#ff || _ <- lists:seq(1, 32)]), 16#62a8ab43}, + {iolist_to_binary([N || N <- lists:seq(0, 16#1f)]), 16#46dd794e}, + {iolist_to_binary([N || N <- lists:seq(16#1f, 0, -1)]), 16#113fdb5c}, + {<<16#01c00000:32, 0:32, 0:32, 0:32, 16#14000000:32, 16#00000400:32, 16#00000014:32, + 16#00000018:32, 16#28000000:32, 0:32, 16#02000000:32, 0:32>>, 16#d9963a56} + ], + [{iolist_to_binary(io_lib:format("16#~8.16.0b", [R])), + fun() -> R = crc32c(V) end} || {V, R} <- Tests]. +-endif. diff --git a/ranch/src/ranch_embedded_sup.erl b/ranch/src/ranch_embedded_sup.erl new file mode 100644 index 0000000..f9f8d61 --- /dev/null +++ b/ranch/src/ranch_embedded_sup.erl @@ -0,0 +1,36 @@ +%% Copyright (c) 2019-2021, Jan Uhlig +%% +%% 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(ranch_embedded_sup). + +-behavior(supervisor). + +-export([start_link/5]). +-export([init/1]). + +-spec start_link(ranch:ref(), module(), any(), module(), any()) + -> {ok, pid()}. +start_link(Ref, Transport, TransOpts, Protocol, ProtoOpts) -> + supervisor:start_link(?MODULE, {Ref, Transport, TransOpts, Protocol, ProtoOpts}). + +-spec init({ranch:ref(), module(), any(), module(), any()}) + -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. +init({Ref, Transport, TransOpts, Protocol, ProtoOpts}) -> + Proxy = #{id => ranch_server_proxy, + start => {ranch_server_proxy, start_link, []}, + shutdown => brutal_kill}, + Listener = #{id => {ranch_listener_sup, Ref}, + start => {ranch_listener_sup, start_link, [Ref, Transport, TransOpts, Protocol, ProtoOpts]}, + type => supervisor}, + {ok, {#{strategy => rest_for_one}, [Proxy, Listener]}}. diff --git a/ranch/src/ranch_listener_sup.erl b/ranch/src/ranch_listener_sup.erl new file mode 100644 index 0000000..b33e42d --- /dev/null +++ b/ranch/src/ranch_listener_sup.erl @@ -0,0 +1,48 @@ +%% Copyright (c) 2011-2021, 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(ranch_listener_sup). +-behaviour(supervisor). + +-export([start_link/5]). +-export([init/1]). + +-spec start_link(ranch:ref(), module(), any(), module(), any()) + -> {ok, pid()}. +start_link(Ref, Transport, TransOpts, Protocol, ProtoOpts) -> + MaxConns = maps:get(max_connections, TransOpts, 1024), + Logger = maps:get(logger, TransOpts, logger), + ranch_server:set_new_listener_opts(Ref, MaxConns, TransOpts, ProtoOpts, + [Ref, Transport, TransOpts, Protocol, ProtoOpts]), + supervisor:start_link(?MODULE, { + Ref, Transport, Protocol, Logger + }). + +-spec init({ranch:ref(), module(), module(), module()}) + -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. +init({Ref, Transport, Protocol, Logger}) -> + ok = ranch_server:set_listener_sup(Ref, self()), + ChildSpecs = [ + #{ + id => ranch_conns_sup_sup, + start => {ranch_conns_sup_sup, start_link, [Ref, Transport, Protocol, Logger]}, + type => supervisor + }, + #{ + id => ranch_acceptors_sup, + start => {ranch_acceptors_sup, start_link, [Ref, Transport, Logger]}, + type => supervisor + } + ], + {ok, {#{strategy => rest_for_one}, ChildSpecs}}. diff --git a/ranch/src/ranch_protocol.erl b/ranch/src/ranch_protocol.erl new file mode 100644 index 0000000..4562348 --- /dev/null +++ b/ranch/src/ranch_protocol.erl @@ -0,0 +1,23 @@ +%% Copyright (c) 2012-2021, 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(ranch_protocol). + +%% Start a new connection process for the given socket. +-callback start_link( + Ref::ranch:ref(), + Transport::module(), + ProtocolOptions::any()) + -> {ok, ConnectionPid::pid()} + | {ok, SupPid::pid(), ConnectionPid::pid()}. diff --git a/ranch/src/ranch_proxy_header.erl b/ranch/src/ranch_proxy_header.erl new file mode 100644 index 0000000..8f73dba --- /dev/null +++ b/ranch/src/ranch_proxy_header.erl @@ -0,0 +1,1007 @@ +%% Copyright (c) 2018-2021, 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(ranch_proxy_header). + +-export([parse/1]). +-export([header/1]). +-export([header/2]). +-export([to_connection_info/1]). + +-type proxy_info() :: #{ + %% Mandatory part. + version := 1 | 2, + command := local | proxy, + transport_family => undefined | ipv4 | ipv6 | unix, + transport_protocol => undefined | stream | dgram, + %% Addresses. + src_address => inet:ip_address() | binary(), + src_port => inet:port_number(), + dest_address => inet:ip_address() | binary(), + dest_port => inet:port_number(), + %% Extra TLV-encoded data. + alpn => binary(), %% US-ASCII. + authority => binary(), %% UTF-8. + ssl => #{ + client := [ssl | cert_conn | cert_sess], + verified := boolean(), + version => binary(), %% US-ASCII. + cipher => binary(), %% US-ASCII. + sig_alg => binary(), %% US-ASCII. + key_alg => binary(), %% US-ASCII. + cn => binary() %% UTF-8. + }, + netns => binary(), %% US-ASCII. + %% Unknown TLVs can't be parsed so the raw data is given. + raw_tlvs => [{0..255, binary()}] +}. +-export_type([proxy_info/0]). + +-type build_opts() :: #{ + checksum => crc32c, + padding => pos_integer() %% >= 3 +}. + +%% Parsing. + +-spec parse(Data) -> {ok, proxy_info(), Data} | {error, atom()} when Data::binary(). +parse(<<"\r\n\r\n\0\r\nQUIT\n", Rest/bits>>) -> + parse_v2(Rest); +parse(<<"PROXY ", Rest/bits>>) -> + parse_v1(Rest); +parse(_) -> + {error, 'The PROXY protocol header signature was not recognized. (PP 2.1, PP 2.2)'}. + +-ifdef(TEST). +parse_unrecognized_header_test() -> + {error, _} = parse(<<"GET / HTTP/1.1\r\n">>), + ok. +-endif. + +%% Human-readable header format (Version 1). +parse_v1(<<"TCP4 ", Rest/bits>>) -> + parse_v1(Rest, ipv4); +parse_v1(<<"TCP6 ", Rest/bits>>) -> + parse_v1(Rest, ipv6); +parse_v1(<<"UNKNOWN\r\n", Rest/bits>>) -> + {ok, #{ + version => 1, + command => proxy, + transport_family => undefined, + transport_protocol => undefined + }, Rest}; +parse_v1(<<"UNKNOWN ", Rest0/bits>>) -> + case binary:split(Rest0, <<"\r\n">>) of + [_, Rest] -> + {ok, #{ + version => 1, + command => proxy, + transport_family => undefined, + transport_protocol => undefined + }, Rest}; + [_] -> + {error, 'Malformed or incomplete PROXY protocol header line. (PP 2.1)'} + end; +parse_v1(_) -> + {error, 'The INET protocol and family string was not recognized. (PP 2.1)'}. + +parse_v1(Rest0, Family) -> + try + {ok, SrcAddr, Rest1} = parse_ip(Rest0, Family), + {ok, DestAddr, Rest2} = parse_ip(Rest1, Family), + {ok, SrcPort, Rest3} = parse_port(Rest2, $\s), + {ok, DestPort, Rest4} = parse_port(Rest3, $\r), + <<"\n", Rest/bits>> = Rest4, + {ok, #{ + version => 1, + command => proxy, + transport_family => Family, + transport_protocol => stream, + src_address => SrcAddr, + src_port => SrcPort, + dest_address => DestAddr, + dest_port => DestPort + }, Rest} + catch + throw:parse_ipv4_error -> + {error, 'Failed to parse an IPv4 address in the PROXY protocol header line. (PP 2.1)'}; + throw:parse_ipv6_error -> + {error, 'Failed to parse an IPv6 address in the PROXY protocol header line. (PP 2.1)'}; + throw:parse_port_error -> + {error, 'Failed to parse a port number in the PROXY protocol header line. (PP 2.1)'}; + _:_ -> + {error, 'Malformed or incomplete PROXY protocol header line. (PP 2.1)'} + end. + +parse_ip(<>, ipv4) -> parse_ipv4(Addr, Rest); +parse_ip(<>, ipv4) -> parse_ipv4(Addr, Rest); +parse_ip(<>, ipv4) -> parse_ipv4(Addr, Rest); +parse_ip(<>, ipv4) -> parse_ipv4(Addr, Rest); +parse_ip(<>, ipv4) -> parse_ipv4(Addr, Rest); +parse_ip(<>, ipv4) -> parse_ipv4(Addr, Rest); +parse_ip(<>, ipv4) -> parse_ipv4(Addr, Rest); +parse_ip(<>, ipv4) -> parse_ipv4(Addr, Rest); +parse_ip(<>, ipv4) -> parse_ipv4(Addr, Rest); +parse_ip(Data, ipv6) -> + [Addr, Rest] = binary:split(Data, <<$\s>>), + parse_ipv6(Addr, Rest). + +parse_ipv4(Addr0, Rest) -> + case inet:parse_ipv4strict_address(binary_to_list(Addr0)) of + {ok, Addr} -> {ok, Addr, Rest}; + {error, einval} -> throw(parse_ipv4_error) + end. + +parse_ipv6(Addr0, Rest) -> + case inet:parse_ipv6strict_address(binary_to_list(Addr0)) of + {ok, Addr} -> {ok, Addr, Rest}; + {error, einval} -> throw(parse_ipv6_error) + end. + +parse_port(<>, C) -> parse_port(Port, Rest); +parse_port(<>, C) -> parse_port(Port, Rest); +parse_port(<>, C) -> parse_port(Port, Rest); +parse_port(<>, C) -> parse_port(Port, Rest); +parse_port(<>, C) -> parse_port(Port, Rest); + +parse_port(Port0, Rest) -> + try binary_to_integer(Port0) of + Port when Port > 0, Port =< 65535 -> + {ok, Port, Rest}; + _ -> + throw(parse_port_error) + catch _:_ -> + throw(parse_port_error) + end. + +-ifdef(TEST). +parse_v1_test() -> + %% Examples taken from the PROXY protocol header specification. + {ok, #{ + version := 1, + command := proxy, + transport_family := ipv4, + transport_protocol := stream, + src_address := {255, 255, 255, 255}, + src_port := 65535, + dest_address := {255, 255, 255, 255}, + dest_port := 65535 + }, <<>>} = parse(<<"PROXY TCP4 255.255.255.255 255.255.255.255 65535 65535\r\n">>), + {ok, #{ + version := 1, + command := proxy, + transport_family := ipv6, + transport_protocol := stream, + src_address := {65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535}, + src_port := 65535, + dest_address := {65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535}, + dest_port := 65535 + }, <<>>} = parse(<<"PROXY TCP6 " + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff " + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff 65535 65535\r\n">>), + {ok, #{ + version := 1, + command := proxy, + transport_family := undefined, + transport_protocol := undefined + }, <<>>} = parse(<<"PROXY UNKNOWN\r\n">>), + {ok, #{ + version := 1, + command := proxy, + transport_family := undefined, + transport_protocol := undefined + }, <<>>} = parse(<<"PROXY UNKNOWN " + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff " + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff 65535 65535\r\n">>), + {ok, #{ + version := 1, + command := proxy, + transport_family := ipv4, + transport_protocol := stream, + src_address := {192, 168, 0, 1}, + src_port := 56324, + dest_address := {192, 168, 0, 11}, + dest_port := 443 + }, <<"GET / HTTP/1.1\r\nHost: 192.168.0.11\r\n\r\n">>} = parse(<< + "PROXY TCP4 192.168.0.1 192.168.0.11 56324 443\r\n" + "GET / HTTP/1.1\r\n" + "Host: 192.168.0.11\r\n" + "\r\n">>), + %% Test cases taken from tomciopp/proxy_protocol. + {ok, #{ + version := 1, + command := proxy, + transport_family := ipv4, + transport_protocol := stream, + src_address := {192, 168, 0, 1}, + src_port := 56324, + dest_address := {192, 168, 0, 11}, + dest_port := 443 + }, <<"GET / HTTP/1.1\r">>} = parse(<< + "PROXY TCP4 192.168.0.1 192.168.0.11 56324 443\r\nGET / HTTP/1.1\r">>), + {error, _} = parse(<<"PROXY TCP4 192.1638.0.1 192.168.0.11 56324 443\r\nGET / HTTP/1.1\r">>), + {error, _} = parse(<<"PROXY TCP4 192.168.0.1 192.168.0.11 1111111 443\r\nGET / HTTP/1.1\r">>), + {ok, #{ + version := 1, + command := proxy, + transport_family := ipv6, + transport_protocol := stream, + src_address := {8193, 3512, 0, 66, 0, 35374, 880, 29492}, + src_port := 4124, + dest_address := {8193, 3512, 0, 66, 0, 35374, 880, 29493}, + dest_port := 443 + }, <<"GET / HTTP/1.1\r">>} = parse(<<"PROXY TCP6 " + "2001:0db8:0000:0042:0000:8a2e:0370:7334 " + "2001:0db8:0000:0042:0000:8a2e:0370:7335 4124 443\r\nGET / HTTP/1.1\r">>), + {error, _} = parse(<<"PROXY TCP6 " + "2001:0db8:0000:0042:0000:8a2e:0370:7334 " + "2001:0db8:00;0:0042:0000:8a2e:0370:7335 4124 443\r\nGET / HTTP/1.1\r">>), + {error, _} = parse(<<"PROXY TCP6 " + "2001:0db8:0000:0042:0000:8a2e:0370:7334 " + "2001:0db8:0000:0042:0000:8a2e:0370:7335 4124 foo\r\nGET / HTTP/1.1\r">>), + {ok, #{ + version := 1, + command := proxy, + transport_family := undefined, + transport_protocol := undefined + }, <<"GET / HTTP/1.1\r">>} = parse(<<"PROXY UNKNOWN 4124 443\r\nGET / HTTP/1.1\r">>), + {ok, #{ + version := 1, + command := proxy, + transport_family := undefined, + transport_protocol := undefined + }, <<"GET / HTTP/1.1\r">>} = parse(<<"PROXY UNKNOWN\r\nGET / HTTP/1.1\r">>), + ok. +-endif. + +%% Binary header format (version 2). + +%% LOCAL. +parse_v2(<<2:4, 0:4, _:8, Len:16, Rest0/bits>>) -> + case Rest0 of + <<_:Len/binary, Rest/bits>> -> + {ok, #{ + version => 2, + command => local + }, Rest}; + _ -> + {error, 'Missing data in the PROXY protocol binary header. (PP 2.2)'} + end; +%% PROXY. +parse_v2(<<2:4, 1:4, Family:4, Protocol:4, Len:16, Rest/bits>>) + when Family =< 3, Protocol =< 2 -> + case Rest of + <> -> + parse_v2(Rest, Len, parse_family(Family), parse_protocol(Protocol), + <>); + _ -> + {error, 'Missing data in the PROXY protocol binary header. (PP 2.2)'} + end; +%% Errors. +parse_v2(<>) when Version =/= 2 -> + {error, 'Invalid version in the PROXY protocol binary header. (PP 2.2)'}; +parse_v2(<<_:4, Command:4, _/bits>>) when Command > 1 -> + {error, 'Invalid command in the PROXY protocol binary header. (PP 2.2)'}; +parse_v2(<<_:8, Family:4, _/bits>>) when Family > 3 -> + {error, 'Invalid address family in the PROXY protocol binary header. (PP 2.2)'}; +parse_v2(<<_:12, Protocol:4, _/bits>>) when Protocol > 2 -> + {error, 'Invalid transport protocol in the PROXY protocol binary header. (PP 2.2)'}. + +parse_family(0) -> undefined; +parse_family(1) -> ipv4; +parse_family(2) -> ipv6; +parse_family(3) -> unix. + +parse_protocol(0) -> undefined; +parse_protocol(1) -> stream; +parse_protocol(2) -> dgram. + +parse_v2(Data, Len, Family, Protocol, _) + when Family =:= undefined; Protocol =:= undefined -> + <<_:Len/binary, Rest/bits>> = Data, + {ok, #{ + version => 2, + command => proxy, + %% In case only one value was undefined, we set both explicitly. + %% It doesn't make sense to have only one known value. + transport_family => undefined, + transport_protocol => undefined + }, Rest}; +parse_v2(<< + S1, S2, S3, S4, + D1, D2, D3, D4, + SrcPort:16, DestPort:16, Rest/bits>>, Len, Family=ipv4, Protocol, Header) + when Len >= 12 -> + parse_tlv(Rest, Len - 12, #{ + version => 2, + command => proxy, + transport_family => Family, + transport_protocol => Protocol, + src_address => {S1, S2, S3, S4}, + src_port => SrcPort, + dest_address => {D1, D2, D3, D4}, + dest_port => DestPort + }, Header); +parse_v2(<< + S1:16, S2:16, S3:16, S4:16, S5:16, S6:16, S7:16, S8:16, + D1:16, D2:16, D3:16, D4:16, D5:16, D6:16, D7:16, D8:16, + SrcPort:16, DestPort:16, Rest/bits>>, Len, Family=ipv6, Protocol, Header) + when Len >= 36 -> + parse_tlv(Rest, Len - 36, #{ + version => 2, + command => proxy, + transport_family => Family, + transport_protocol => Protocol, + src_address => {S1, S2, S3, S4, S5, S6, S7, S8}, + src_port => SrcPort, + dest_address => {D1, D2, D3, D4, D5, D6, D7, D8}, + dest_port => DestPort + }, Header); +parse_v2(<>, + Len, Family=unix, Protocol, Header) + when Len >= 216 -> + try + [SrcAddr, _] = binary:split(SrcAddr0, <<0>>), + true = byte_size(SrcAddr) > 0, + [DestAddr, _] = binary:split(DestAddr0, <<0>>), + true = byte_size(DestAddr) > 0, + parse_tlv(Rest, Len - 216, #{ + version => 2, + command => proxy, + transport_family => Family, + transport_protocol => Protocol, + src_address => SrcAddr, + dest_address => DestAddr + }, Header) + catch _:_ -> + {error, 'Invalid UNIX address in PROXY protocol binary header. (PP 2.2)'} + end; +parse_v2(_, _, _, _, _) -> + {error, 'Invalid length in the PROXY protocol binary header. (PP 2.2)'}. + +-ifdef(TEST). +parse_v2_test() -> + %% Test cases taken from tomciopp/proxy_protocol. + {ok, #{ + version := 2, + command := proxy, + transport_family := ipv4, + transport_protocol := stream, + src_address := {127, 0, 0, 1}, + src_port := 444, + dest_address := {192, 168, 0, 1}, + dest_port := 443 + }, <<"GET / HTTP/1.1\r\n">>} = parse(<< + 13, 10, 13, 10, 0, 13, 10, 81, 85, 73, 84, 10, %% Signature. + 33, %% Version and command. + 17, %% Family and protocol. + 0, 12, %% Length. + 127, 0, 0, 1, %% Source address. + 192, 168, 0, 1, %% Destination address. + 1, 188, %% Source port. + 1, 187, %% Destination port. + "GET / HTTP/1.1\r\n">>), + {ok, #{ + version := 2, + command := proxy, + transport_family := ipv4, + transport_protocol := dgram, + src_address := {127, 0, 0, 1}, + src_port := 444, + dest_address := {192, 168, 0, 1}, + dest_port := 443 + }, <<"GET / HTTP/1.1\r\n">>} = parse(<< + 13, 10, 13, 10, 0, 13, 10, 81, 85, 73, 84, 10, %% Signature. + 33, %% Version and command. + 18, %% Family and protocol. + 0, 12, %% Length. + 127, 0, 0, 1, %% Source address. + 192, 168, 0, 1, %% Destination address. + 1, 188, %% Source port. + 1, 187, %% Destination port. + "GET / HTTP/1.1\r\n">>), + {ok, #{ + version := 2, + command := proxy, + transport_family := ipv6, + transport_protocol := stream, + src_address := {5532, 4240, 1, 0, 0, 0, 0, 0}, + src_port := 444, + dest_address := {8193, 3512, 1, 0, 0, 0, 0, 0}, + dest_port := 443 + }, <<"GET / HTTP/1.1\r\n">>} = parse(<< + 13, 10, 13, 10, 0, 13, 10, 81, 85, 73, 84, 10, %% Signature. + 33, %% Version and command. + 33, %% Family and protocol. + 0, 36, %% Length. + 21, 156, 16, 144, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, %% Source address. + 32, 1, 13, 184, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, %% Destination address. + 1, 188, %% Source port. + 1, 187, %% Destination port. + "GET / HTTP/1.1\r\n">>), + {ok, #{ + version := 2, + command := proxy, + transport_family := ipv6, + transport_protocol := dgram, + src_address := {5532, 4240, 1, 0, 0, 0, 0, 0}, + src_port := 444, + dest_address := {8193, 3512, 1, 0, 0, 0, 0, 0}, + dest_port := 443 + }, <<"GET / HTTP/1.1\r\n">>} = parse(<< + 13, 10, 13, 10, 0, 13, 10, 81, 85, 73, 84, 10, %% Signature. + 33, %% Version and command. + 34, %% Family and protocol. + 0, 36, %% Length. + 21, 156, 16, 144, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, %% Source address. + 32, 1, 13, 184, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, %% Destination address. + 1, 188, %% Source port. + 1, 187, %% Destination port. + "GET / HTTP/1.1\r\n">>), + Path = <<"/var/pgsql_sock">>, + Len = byte_size(Path), + Padding = 8 * (108 - Len), + {ok, #{ + version := 2, + command := proxy, + transport_family := unix, + transport_protocol := stream, + src_address := Path, + dest_address := Path + }, <<"GET / HTTP/1.1\r\n">>} = parse(<< + 13, 10, 13, 10, 0, 13, 10, 81, 85, 73, 84, 10, + 33, + 49, + 0, 216, + Path/binary, 0:Padding, + Path/binary, 0:Padding, + "GET / HTTP/1.1\r\n">>), + {ok, #{ + version := 2, + command := proxy, + transport_family := unix, + transport_protocol := dgram, + src_address := Path, + dest_address := Path + }, <<"GET / HTTP/1.1\r\n">>} = parse(<< + 13, 10, 13, 10, 0, 13, 10, 81, 85, 73, 84, 10, + 33, + 50, + 0, 216, + Path/binary, 0:Padding, + Path/binary, 0:Padding, + "GET / HTTP/1.1\r\n">>), + ok. + +parse_v2_regression_test() -> + %% Real packet received from AWS. We confirm that the CRC32C + %% check succeeds only (in other words that ok is returned). + {ok, _, <<>>} = parse(<< + 13, 10, 13, 10, 0, 13, 10, 81, 85, 73, 84, 10, 33, 17, 0, 84, + 172, 31, 7, 113, 172, 31, 10, 31, 200, 242, 0, 80, 3, 0, 4, + 232, 214, 137, 45, 234, 0, 23, 1, 118, 112, 99, 101, 45, 48, + 56, 100, 50, 98, 102, 49, 53, 102, 97, 99, 53, 48, 48, 49, 99, + 57, 4, 0, 36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>), + ok. +-endif. + +parse_tlv(Rest, 0, Info, _) -> + {ok, Info, Rest}; +%% PP2_TYPE_ALPN. +parse_tlv(<<16#1, TLVLen:16, ALPN:TLVLen/binary, Rest/bits>>, Len, Info, Header) -> + parse_tlv(Rest, Len - TLVLen - 3, Info#{alpn => ALPN}, Header); +%% PP2_TYPE_AUTHORITY. +parse_tlv(<<16#2, TLVLen:16, Authority:TLVLen/binary, Rest/bits>>, Len, Info, Header) -> + parse_tlv(Rest, Len - TLVLen - 3, Info#{authority => Authority}, Header); +%% PP2_TYPE_CRC32C. +parse_tlv(<<16#3, TLVLen:16, CRC32C:32, Rest/bits>>, Len0, Info, Header) when TLVLen =:= 4 -> + Len = Len0 - TLVLen - 3, + BeforeLen = byte_size(Header) - Len - TLVLen, + <> = Header, + %% The initial CRC is ranch_crc32c:crc32c(<<"\r\n\r\n\0\r\nQUIT\n", 2:4, 1:4>>). + case ranch_crc32c:crc32c(2900412422, [Before, <<0:32>>, After]) of + CRC32C -> + parse_tlv(Rest, Len, Info, Header); + _ -> + {error, 'Failed CRC32C verification in PROXY protocol binary header. (PP 2.2)'} + end; +%% PP2_TYPE_NOOP. +parse_tlv(<<16#4, TLVLen:16, _:TLVLen/binary, Rest/bits>>, Len, Info, Header) -> + parse_tlv(Rest, Len - TLVLen - 3, Info, Header); +%% PP2_TYPE_SSL. +parse_tlv(<<16#20, TLVLen:16, Client, Verify:32, Rest0/bits>>, Len, Info, Header) -> + SubsLen = TLVLen - 5, + case Rest0 of + <> -> + SSL0 = #{ + client => parse_client(<>), + verified => Verify =:= 0 + }, + case parse_ssl_tlv(Subs, SubsLen, SSL0) of + {ok, SSL, <<>>} -> + parse_tlv(Rest, Len - TLVLen - 3, Info#{ssl => SSL}, Header); + Error={error, _} -> + Error + end; + _ -> + {error, 'Invalid TLV length in the PROXY protocol binary header. (PP 2.2)'} + end; +%% PP2_TYPE_NETNS. +parse_tlv(<<16#30, TLVLen:16, NetNS:TLVLen/binary, Rest/bits>>, Len, Info, Header) -> + parse_tlv(Rest, Len - TLVLen - 3, Info#{netns => NetNS}, Header); +%% Unknown TLV. +parse_tlv(<>, Len, Info, Header) -> + RawTLVs = maps:get(raw_tlvs, Info, []), + parse_tlv(Rest, Len - TLVLen - 3, Info#{raw_tlvs => [{TLVType, TLVValue}|RawTLVs]}, Header); +%% Invalid TLV length. +parse_tlv(_, _, _, _) -> + {error, 'Invalid TLV length in the PROXY protocol binary header. (PP 2.2)'}. + +parse_client(<<_:5, ClientCertSess:1, ClientCertConn:1, ClientSSL:1>>) -> + Client0 = case ClientCertSess of + 0 -> []; + 1 -> [cert_sess] + end, + Client1 = case ClientCertConn of + 0 -> Client0; + 1 -> [cert_conn|Client0] + end, + case ClientSSL of + 0 -> Client1; + 1 -> [ssl|Client1] + end. + +parse_ssl_tlv(Rest, 0, Info) -> + {ok, Info, Rest}; +%% Valid TLVs. +parse_ssl_tlv(<>, Len, Info) -> + case ssl_subtype(TLVType) of + undefined -> + {error, 'Invalid TLV subtype for PP2_TYPE_SSL in PROXY protocol binary header. (PP 2.2)'}; + Type -> + parse_ssl_tlv(Rest, Len - TLVLen - 3, Info#{Type => TLVValue}) + end; +%% Invalid TLV length. +parse_ssl_tlv(_, _, _) -> + {error, 'Invalid TLV length in the PROXY protocol binary header. (PP 2.2)'}. + +ssl_subtype(16#21) -> version; +ssl_subtype(16#22) -> cn; +ssl_subtype(16#23) -> cipher; +ssl_subtype(16#24) -> sig_alg; +ssl_subtype(16#25) -> key_alg; +ssl_subtype(_) -> undefined. + +%% Building. + +-spec header(proxy_info()) -> iodata(). +header(ProxyInfo) -> + header(ProxyInfo, #{}). + +-spec header(proxy_info(), build_opts()) -> iodata(). +header(#{version := 2, command := local}, _) -> + <<"\r\n\r\n\0\r\nQUIT\n", 2:4, 0:28>>; +header(#{version := 2, command := proxy, + transport_family := Family, + transport_protocol := Protocol}, _) + when Family =:= undefined; Protocol =:= undefined -> + <<"\r\n\r\n\0\r\nQUIT\n", 2:4, 1:4, 0:24>>; +header(ProxyInfo=#{version := 2, command := proxy, + transport_family := Family, + transport_protocol := Protocol}, Opts) -> + Addresses = addresses(ProxyInfo), + TLVs = tlvs(ProxyInfo, Opts), + ExtraLen = case Opts of + #{checksum := crc32c} -> 7; + _ -> 0 + end, + Len = iolist_size(Addresses) + iolist_size(TLVs) + ExtraLen, + Header = [ + <<"\r\n\r\n\0\r\nQUIT\n", 2:4, 1:4>>, + <<(family(Family)):4, (protocol(Protocol)):4>>, + <>, + Addresses, + TLVs + ], + case Opts of + #{checksum := crc32c} -> + CRC32C = ranch_crc32c:crc32c([Header, <<16#3, 4:16, 0:32>>]), + [Header, <<16#3, 4:16, CRC32C:32>>]; + _ -> + Header + end; +header(#{version := 1, command := proxy, + transport_family := undefined, + transport_protocol := undefined}, _) -> + <<"PROXY UNKNOWN\r\n">>; +header(#{version := 1, command := proxy, + transport_family := Family0, + transport_protocol := stream, + src_address := SrcAddress, src_port := SrcPort, + dest_address := DestAddress, dest_port := DestPort}, _) + when SrcPort > 0, SrcPort =< 65535, DestPort > 0, DestPort =< 65535 -> + [ + <<"PROXY ">>, + case Family0 of + ipv4 when tuple_size(SrcAddress) =:= 4, tuple_size(DestAddress) =:= 4 -> + [<<"TCP4 ">>, inet:ntoa(SrcAddress), $\s, inet:ntoa(DestAddress)]; + ipv6 when tuple_size(SrcAddress) =:= 8, tuple_size(DestAddress) =:= 8 -> + [<<"TCP6 ">>, inet:ntoa(SrcAddress), $\s, inet:ntoa(DestAddress)] + end, + $\s, + integer_to_binary(SrcPort), + $\s, + integer_to_binary(DestPort), + $\r, $\n + ]. + +family(ipv4) -> 1; +family(ipv6) -> 2; +family(unix) -> 3. + +protocol(stream) -> 1; +protocol(dgram) -> 2. + +addresses(#{transport_family := ipv4, + src_address := {S1, S2, S3, S4}, src_port := SrcPort, + dest_address := {D1, D2, D3, D4}, dest_port := DestPort}) + when SrcPort > 0, SrcPort =< 65535, DestPort > 0, DestPort =< 65535 -> + <>; +addresses(#{transport_family := ipv6, + src_address := {S1, S2, S3, S4, S5, S6, S7, S8}, src_port := SrcPort, + dest_address := {D1, D2, D3, D4, D5, D6, D7, D8}, dest_port := DestPort}) + when SrcPort > 0, SrcPort =< 65535, DestPort > 0, DestPort =< 65535 -> + << + S1:16, S2:16, S3:16, S4:16, S5:16, S6:16, S7:16, S8:16, + D1:16, D2:16, D3:16, D4:16, D5:16, D6:16, D7:16, D8:16, + SrcPort:16, DestPort:16 + >>; +addresses(#{transport_family := unix, + src_address := SrcAddress, dest_address := DestAddress}) + when byte_size(SrcAddress) =< 108, byte_size(DestAddress) =< 108 -> + SrcPadding = 8 * (108 - byte_size(SrcAddress)), + DestPadding = 8 * (108 - byte_size(DestAddress)), + << + SrcAddress/binary, 0:SrcPadding, + DestAddress/binary, 0:DestPadding + >>. + +tlvs(ProxyInfo, Opts) -> + [ + binary_tlv(ProxyInfo, alpn, 16#1), + binary_tlv(ProxyInfo, authority, 16#2), + ssl_tlv(ProxyInfo), + binary_tlv(ProxyInfo, netns, 16#30), + raw_tlvs(ProxyInfo), + noop_tlv(Opts) + ]. + +binary_tlv(Info, Key, Type) -> + case Info of + #{Key := Bin} -> + Len = byte_size(Bin), + <>; + _ -> + <<>> + end. + +noop_tlv(#{padding := Len0}) when Len0 >= 3 -> + Len = Len0 - 3, + <<16#4, Len:16, 0:Len/unit:8>>; +noop_tlv(_) -> + <<>>. + +ssl_tlv(#{ssl := Info=#{client := Client0, verified := Verify0}}) -> + Client = client(Client0, 0), + Verify = if + Verify0 -> 0; + not Verify0 -> 1 + end, + TLVs = [ + binary_tlv(Info, version, 16#21), + binary_tlv(Info, cn, 16#22), + binary_tlv(Info, cipher, 16#23), + binary_tlv(Info, sig_alg, 16#24), + binary_tlv(Info, key_alg, 16#25) + ], + Len = iolist_size(TLVs) + 5, + [<<16#20, Len:16, Client, Verify:32>>, TLVs]; +ssl_tlv(_) -> + <<>>. + +client([], Client) -> Client; +client([ssl|Tail], Client) -> client(Tail, Client bor 16#1); +client([cert_conn|Tail], Client) -> client(Tail, Client bor 16#2); +client([cert_sess|Tail], Client) -> client(Tail, Client bor 16#4). + +raw_tlvs(Info) -> + [begin + Len = byte_size(Bin), + <> + end || {Type, Bin} <- maps:get(raw_tlvs, Info, [])]. + +-ifdef(TEST). +v1_test() -> + Test1 = #{ + version => 1, + command => proxy, + transport_family => undefined, + transport_protocol => undefined + }, + {ok, Test1, <<>>} = parse(iolist_to_binary(header(Test1))), + Test2 = #{ + version => 1, + command => proxy, + transport_family => ipv4, + transport_protocol => stream, + src_address => {127, 0, 0, 1}, + src_port => 1234, + dest_address => {10, 11, 12, 13}, + dest_port => 23456 + }, + {ok, Test2, <<>>} = parse(iolist_to_binary(header(Test2))), + Test3 = #{ + version => 1, + command => proxy, + transport_family => ipv6, + transport_protocol => stream, + src_address => {1, 2, 3, 4, 5, 6, 7, 8}, + src_port => 1234, + dest_address => {65535, 55555, 2222, 333, 1, 9999, 777, 8}, + dest_port => 23456 + }, + {ok, Test3, <<>>} = parse(iolist_to_binary(header(Test3))), + ok. + +v2_test() -> + Test0 = #{ + version => 2, + command => local + }, + {ok, Test0, <<>>} = parse(iolist_to_binary(header(Test0))), + Test1 = #{ + version => 2, + command => proxy, + transport_family => undefined, + transport_protocol => undefined + }, + {ok, Test1, <<>>} = parse(iolist_to_binary(header(Test1))), + Test2 = #{ + version => 2, + command => proxy, + transport_family => ipv4, + transport_protocol => stream, + src_address => {127, 0, 0, 1}, + src_port => 1234, + dest_address => {10, 11, 12, 13}, + dest_port => 23456 + }, + {ok, Test2, <<>>} = parse(iolist_to_binary(header(Test2))), + Test3 = #{ + version => 2, + command => proxy, + transport_family => ipv6, + transport_protocol => stream, + src_address => {1, 2, 3, 4, 5, 6, 7, 8}, + src_port => 1234, + dest_address => {65535, 55555, 2222, 333, 1, 9999, 777, 8}, + dest_port => 23456 + }, + {ok, Test3, <<>>} = parse(iolist_to_binary(header(Test3))), + Test4 = #{ + version => 2, + command => proxy, + transport_family => unix, + transport_protocol => dgram, + src_address => <<"/run/source.sock">>, + dest_address => <<"/run/destination.sock">> + }, + {ok, Test4, <<>>} = parse(iolist_to_binary(header(Test4))), + ok. + +v2_tlvs_test() -> + Common = #{ + version => 2, + command => proxy, + transport_family => ipv4, + transport_protocol => stream, + src_address => {127, 0, 0, 1}, + src_port => 1234, + dest_address => {10, 11, 12, 13}, + dest_port => 23456 + }, + Test1 = Common#{alpn => <<"h2">>}, + {ok, Test1, <<>>} = parse(iolist_to_binary(header(Test1))), + Test2 = Common#{authority => <<"internal.example.org">>}, + {ok, Test2, <<>>} = parse(iolist_to_binary(header(Test2))), + Test3 = Common#{netns => <<"/var/run/netns/example">>}, + {ok, Test3, <<>>} = parse(iolist_to_binary(header(Test3))), + Test4 = Common#{ssl => #{ + client => [ssl, cert_conn, cert_sess], + verified => true, + version => <<"TLSv1.3">>, + cipher => <<"ECDHE-RSA-AES128-GCM-SHA256">>, + sig_alg => <<"SHA256">>, + key_alg => <<"RSA2048">>, + cn => <<"example.com">> + }}, + {ok, Test4, <<>>} = parse(iolist_to_binary(header(Test4))), + %% Note that the raw_tlvs order is not relevant and therefore + %% the parser does not reverse the list it builds. + Test5In = Common#{raw_tlvs => RawTLVs=[ + %% The only custom TLV I am aware of is defined at: + %% https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-target-groups.html#proxy-protocol + {16#ea, <<16#1, "instance-id">>}, + %% This TLV is entirely fictional. + {16#ff, <<1, 2, 3, 4, 5, 6, 7, 8, 9, 0>>} + ]}, + Test5Out = Test5In#{raw_tlvs => lists:reverse(RawTLVs)}, + {ok, Test5Out, <<>>} = parse(iolist_to_binary(header(Test5In))), + ok. + +v2_checksum_test() -> + Test = #{ + version => 2, + command => proxy, + transport_family => ipv4, + transport_protocol => stream, + src_address => {127, 0, 0, 1}, + src_port => 1234, + dest_address => {10, 11, 12, 13}, + dest_port => 23456 + }, + {ok, Test, <<>>} = parse(iolist_to_binary(header(Test, #{checksum => crc32c}))), + ok. + +v2_padding_test() -> + Test = #{ + version => 2, + command => proxy, + transport_family => ipv4, + transport_protocol => stream, + src_address => {127, 0, 0, 1}, + src_port => 1234, + dest_address => {10, 11, 12, 13}, + dest_port => 23456 + }, + {ok, Test, <<>>} = parse(iolist_to_binary(header(Test, #{padding => 123}))), + ok. +-endif. + +%% Helper to convert proxy_info() to ssl:connection_info(). +%% +%% Because there isn't a lot of fields common to both types +%% this only ends up returning the keys protocol, selected_cipher_suite +%% and sni_hostname *at most*. + +-spec to_connection_info(proxy_info()) -> ssl:connection_info(). +to_connection_info(ProxyInfo=#{ssl := SSL}) -> + ConnInfo0 = case ProxyInfo of + #{authority := Authority} -> + [{sni_hostname, Authority}]; + _ -> + [] + end, + ConnInfo = case SSL of + #{cipher := Cipher} -> + case ssl:str_to_suite(binary_to_list(Cipher)) of + {error, {not_recognized, _}} -> + ConnInfo0; + CipherInfo -> + [{selected_cipher_suite, CipherInfo}|ConnInfo0] + end; + _ -> + ConnInfo0 + end, + %% https://www.openssl.org/docs/man1.1.1/man3/SSL_get_version.html + case SSL of + #{version := <<"TLSv1.3">>} -> [{protocol, 'tlsv1.3'}|ConnInfo]; + #{version := <<"TLSv1.2">>} -> [{protocol, 'tlsv1.2'}|ConnInfo]; + #{version := <<"TLSv1.1">>} -> [{protocol, 'tlsv1.1'}|ConnInfo]; + #{version := <<"TLSv1">>} -> [{protocol, tlsv1}|ConnInfo]; + #{version := <<"SSLv3">>} -> [{protocol, sslv3}|ConnInfo]; + #{version := <<"SSLv2">>} -> [{protocol, sslv2}|ConnInfo]; + %% <<"unknown">>, unsupported or missing version. + _ -> ConnInfo + end; +%% No SSL/TLS information available. +to_connection_info(_) -> + []. + +-ifdef(TEST). +to_connection_info_test() -> + Common = #{ + version => 2, + command => proxy, + transport_family => ipv4, + transport_protocol => stream, + src_address => {127, 0, 0, 1}, + src_port => 1234, + dest_address => {10, 11, 12, 13}, + dest_port => 23456 + }, + %% Version 1. + [] = to_connection_info(#{ + version => 1, + command => proxy, + transport_family => undefined, + transport_protocol => undefined + }), + [] = to_connection_info(Common#{version => 1}), + %% Version 2, no ssl data. + [] = to_connection_info(#{ + version => 2, + command => local + }), + [] = to_connection_info(#{ + version => 2, + command => proxy, + transport_family => undefined, + transport_protocol => undefined + }), + [] = to_connection_info(Common), + [] = to_connection_info(#{ + version => 2, + command => proxy, + transport_family => unix, + transport_protocol => dgram, + src_address => <<"/run/source.sock">>, + dest_address => <<"/run/destination.sock">> + }), + [] = to_connection_info(Common#{netns => <<"/var/run/netns/example">>}), + [] = to_connection_info(Common#{raw_tlvs => [ + {16#ff, <<1, 2, 3, 4, 5, 6, 7, 8, 9, 0>>} + ]}), + %% Version 2, with ssl-related data. + [] = to_connection_info(Common#{alpn => <<"h2">>}), + %% The authority alone is not enough to deduce that this is SNI. + [] = to_connection_info(Common#{authority => <<"internal.example.org">>}), + [ + {protocol, 'tlsv1.3'}, + {selected_cipher_suite, #{ + cipher := aes_128_gcm, + key_exchange := ecdhe_rsa, + mac := aead, + prf := sha256 + }} + ] = to_connection_info(Common#{ssl => #{ + client => [ssl, cert_conn, cert_sess], + verified => true, + version => <<"TLSv1.3">>, + cipher => <<"ECDHE-RSA-AES128-GCM-SHA256">>, + sig_alg => <<"SHA256">>, + key_alg => <<"RSA2048">>, + cn => <<"example.com">> + }}), + [ + {protocol, 'tlsv1.3'}, + {selected_cipher_suite, #{ + cipher := aes_128_gcm, + key_exchange := ecdhe_rsa, + mac := aead, + prf := sha256 + }}, + {sni_hostname, <<"internal.example.org">>} + ] = to_connection_info(Common#{authority => <<"internal.example.org">>, ssl => #{ + client => [ssl, cert_conn, cert_sess], + verified => true, + version => <<"TLSv1.3">>, + cipher => <<"ECDHE-RSA-AES128-GCM-SHA256">>, + sig_alg => <<"SHA256">>, + key_alg => <<"RSA2048">>, + cn => <<"example.com">> + }}), + ok. +-endif. diff --git a/ranch/src/ranch_server.erl b/ranch/src/ranch_server.erl new file mode 100644 index 0000000..3966c1b --- /dev/null +++ b/ranch/src/ranch_server.erl @@ -0,0 +1,279 @@ +%% Copyright (c) 2012-2021, Loïc Hoguin +%% Copyright (c) 2020-2021, Jan Uhlig +%% +%% 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(ranch_server). +-behaviour(gen_server). + +%% API. +-export([start_link/0]). +-export([set_new_listener_opts/5]). +-export([cleanup_listener_opts/1]). +-export([cleanup_connections_sups/1]). +-export([set_connections_sup/3]). +-export([get_connections_sup/2]). +-export([get_connections_sups/1]). +-export([get_connections_sups/0]). +-export([set_listener_sup/2]). +-export([get_listener_sup/1]). +-export([get_listener_sups/0]). +-export([set_addr/2]). +-export([get_addr/1]). +-export([set_max_connections/2]). +-export([get_max_connections/1]). +-export([set_stats_counters/2]). +-export([get_stats_counters/1]). +-export([set_transport_options/2]). +-export([get_transport_options/1]). +-export([set_protocol_options/2]). +-export([get_protocol_options/1]). +-export([get_listener_start_args/1]). +-export([count_connections/1]). + +%% gen_server. +-export([init/1]). +-export([handle_call/3]). +-export([handle_cast/2]). +-export([handle_info/2]). +-export([terminate/2]). +-export([code_change/3]). + +-define(TAB, ?MODULE). + +-type monitors() :: [{{reference(), pid()}, any()}]. +-record(state, { + monitors = [] :: monitors() +}). + +%% API. + +-spec start_link() -> {ok, pid()}. +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +-spec set_new_listener_opts(ranch:ref(), ranch:max_conns(), any(), any(), [any()]) -> ok. +set_new_listener_opts(Ref, MaxConns, TransOpts, ProtoOpts, StartArgs) -> + gen_server:call(?MODULE, {set_new_listener_opts, Ref, MaxConns, TransOpts, ProtoOpts, StartArgs}). + +-spec cleanup_listener_opts(ranch:ref()) -> ok. +cleanup_listener_opts(Ref) -> + _ = ets:delete(?TAB, {addr, Ref}), + _ = ets:delete(?TAB, {max_conns, Ref}), + _ = ets:delete(?TAB, {trans_opts, Ref}), + _ = ets:delete(?TAB, {proto_opts, Ref}), + _ = ets:delete(?TAB, {listener_start_args, Ref}), + %% We also remove the pid of the connection supervisors. + %% Depending on the timing, they might already have been deleted + %% when we handled the monitor DOWN message. However, in some + %% cases when calling stop_listener followed by get_connections_sup, + %% we could end up with the pid still being returned, when we + %% expected a crash (because the listener was stopped). + %% Deleting it explicitly here removes any possible confusion. + _ = ets:match_delete(?TAB, {{conns_sup, Ref, '_'}, '_'}), + _ = ets:delete(?TAB, {stats_counters, Ref}), + %% Ditto for the listener supervisor. + _ = ets:delete(?TAB, {listener_sup, Ref}), + ok. + +-spec cleanup_connections_sups(ranch:ref()) -> ok. +cleanup_connections_sups(Ref) -> + _ = ets:match_delete(?TAB, {{conns_sup, Ref, '_'}, '_'}), + _ = ets:delete(?TAB, {stats_counters, Ref}), + ok. + +-spec set_connections_sup(ranch:ref(), non_neg_integer(), pid()) -> ok. +set_connections_sup(Ref, Id, Pid) -> + gen_server:call(?MODULE, {set_connections_sup, Ref, Id, Pid}). + +-spec get_connections_sup(ranch:ref(), pos_integer()) -> pid(). +get_connections_sup(Ref, Id) -> + ConnsSups = get_connections_sups(Ref), + NConnsSups = length(ConnsSups), + {_, Pid} = lists:keyfind((Id rem NConnsSups) + 1, 1, ConnsSups), + Pid. + +-spec get_connections_sups(ranch:ref()) -> [{pos_integer(), pid()}]. +get_connections_sups(Ref) -> + [{Id, Pid} || + [Id, Pid] <- ets:match(?TAB, {{conns_sup, Ref, '$1'}, '$2'})]. + +-spec get_connections_sups() -> [{ranch:ref(), pos_integer(), pid()}]. +get_connections_sups() -> + [{Ref, Id, Pid} || + [Ref, Id, Pid] <- ets:match(?TAB, {{conns_sup, '$1', '$2'}, '$3'})]. + +-spec set_listener_sup(ranch:ref(), pid()) -> ok. +set_listener_sup(Ref, Pid) -> + gen_server:call(?MODULE, {set_listener_sup, Ref, Pid}). + +-spec get_listener_sup(ranch:ref()) -> pid(). +get_listener_sup(Ref) -> + ets:lookup_element(?TAB, {listener_sup, Ref}, 2). + +-spec get_listener_sups() -> [{ranch:ref(), pid()}]. +get_listener_sups() -> + [{Ref, Pid} || [Ref, Pid] <- ets:match(?TAB, {{listener_sup, '$1'}, '$2'})]. + +-spec set_addr(ranch:ref(), {inet:ip_address(), inet:port_number()} | + {local, binary()} | {undefined, undefined}) -> ok. +set_addr(Ref, Addr) -> + gen_server:call(?MODULE, {set_addr, Ref, Addr}). + +-spec get_addr(ranch:ref()) -> {inet:ip_address(), inet:port_number()} | + {local, binary()} | {undefined, undefined}. +get_addr(Ref) -> + ets:lookup_element(?TAB, {addr, Ref}, 2). + +-spec set_max_connections(ranch:ref(), ranch:max_conns()) -> ok. +set_max_connections(Ref, MaxConnections) -> + gen_server:call(?MODULE, {set_max_conns, Ref, MaxConnections}). + +-spec get_max_connections(ranch:ref()) -> ranch:max_conns(). +get_max_connections(Ref) -> + ets:lookup_element(?TAB, {max_conns, Ref}, 2). + +-spec set_stats_counters(ranch:ref(), counters:counters_ref()) -> ok. +set_stats_counters(Ref, Counters) -> + gen_server:call(?MODULE, {set_stats_counters, Ref, Counters}). + +-spec get_stats_counters(ranch:ref()) -> counters:counters_ref(). +get_stats_counters(Ref) -> + ets:lookup_element(?TAB, {stats_counters, Ref}, 2). + +-spec set_transport_options(ranch:ref(), any()) -> ok. +set_transport_options(Ref, TransOpts) -> + gen_server:call(?MODULE, {set_trans_opts, Ref, TransOpts}). + +-spec get_transport_options(ranch:ref()) -> any(). +get_transport_options(Ref) -> + ets:lookup_element(?TAB, {trans_opts, Ref}, 2). + +-spec set_protocol_options(ranch:ref(), any()) -> ok. +set_protocol_options(Ref, ProtoOpts) -> + gen_server:call(?MODULE, {set_proto_opts, Ref, ProtoOpts}). + +-spec get_protocol_options(ranch:ref()) -> any(). +get_protocol_options(Ref) -> + ets:lookup_element(?TAB, {proto_opts, Ref}, 2). + +-spec get_listener_start_args(ranch:ref()) -> [any()]. +get_listener_start_args(Ref) -> + ets:lookup_element(?TAB, {listener_start_args, Ref}, 2). + +-spec count_connections(ranch:ref()) -> non_neg_integer(). +count_connections(Ref) -> + lists:foldl( + fun ({_, ConnsSup}, Acc) -> + Acc+ranch_conns_sup:active_connections(ConnsSup) + end, + 0, + get_connections_sups(Ref)). + +%% gen_server. + +-spec init([]) -> {ok, #state{}}. +init([]) -> + ConnMonitors = [{{erlang:monitor(process, Pid), Pid}, {conns_sup, Ref, Id}} || + [Ref, Id, Pid] <- ets:match(?TAB, {{conns_sup, '$1', '$2'}, '$3'})], + ListenerMonitors = [{{erlang:monitor(process, Pid), Pid}, {listener_sup, Ref}} || + [Ref, Pid] <- ets:match(?TAB, {{listener_sup, '$1'}, '$2'})], + {ok, #state{monitors=ConnMonitors++ListenerMonitors}}. + +-spec handle_call(term(), {pid(), reference()}, #state{}) -> {reply, ok | ignore, #state{}}. +handle_call({set_new_listener_opts, Ref, MaxConns, TransOpts, ProtoOpts, StartArgs}, _, State) -> + ets:insert_new(?TAB, {{max_conns, Ref}, MaxConns}), + ets:insert_new(?TAB, {{trans_opts, Ref}, TransOpts}), + ets:insert_new(?TAB, {{proto_opts, Ref}, ProtoOpts}), + ets:insert_new(?TAB, {{listener_start_args, Ref}, StartArgs}), + {reply, ok, State}; +handle_call({set_connections_sup, Ref, Id, Pid}, _, State0) -> + State = set_monitored_process({conns_sup, Ref, Id}, Pid, State0), + {reply, ok, State}; +handle_call({set_listener_sup, Ref, Pid}, _, State0) -> + State = set_monitored_process({listener_sup, Ref}, Pid, State0), + {reply, ok, State}; +handle_call({set_addr, Ref, Addr}, _, State) -> + true = ets:insert(?TAB, {{addr, Ref}, Addr}), + {reply, ok, State}; +handle_call({set_max_conns, Ref, MaxConns}, _, State) -> + ets:insert(?TAB, {{max_conns, Ref}, MaxConns}), + _ = [ConnsSup ! {set_max_conns, MaxConns} || {_, ConnsSup} <- get_connections_sups(Ref)], + {reply, ok, State}; +handle_call({set_stats_counters, Ref, Counters}, _, State) -> + ets:insert(?TAB, {{stats_counters, Ref}, Counters}), + {reply, ok, State}; +handle_call({set_trans_opts, Ref, Opts}, _, State) -> + ets:insert(?TAB, {{trans_opts, Ref}, Opts}), + {reply, ok, State}; +handle_call({set_proto_opts, Ref, Opts}, _, State) -> + ets:insert(?TAB, {{proto_opts, Ref}, Opts}), + _ = [ConnsSup ! {set_protocol_options, Opts} || {_, ConnsSup} <- get_connections_sups(Ref)], + {reply, ok, State}; +handle_call(_Request, _From, State) -> + {reply, ignore, State}. + +-spec handle_cast(_, #state{}) -> {noreply, #state{}}. +handle_cast(_Request, State) -> + {noreply, State}. + +-spec handle_info(term(), #state{}) -> {noreply, #state{}}. +handle_info({'DOWN', MonitorRef, process, Pid, Reason}, + State=#state{monitors=Monitors}) -> + {_, TypeRef} = lists:keyfind({MonitorRef, Pid}, 1, Monitors), + ok = case {TypeRef, Reason} of + {{listener_sup, Ref}, normal} -> + cleanup_listener_opts(Ref); + {{listener_sup, Ref}, shutdown} -> + cleanup_listener_opts(Ref); + {{listener_sup, Ref}, {shutdown, _}} -> + cleanup_listener_opts(Ref); + _ -> + _ = ets:delete(?TAB, TypeRef), + ok + end, + Monitors2 = lists:keydelete({MonitorRef, Pid}, 1, Monitors), + {noreply, State#state{monitors=Monitors2}}; +handle_info(_Info, State) -> + {noreply, State}. + +-spec terminate(_, #state{}) -> ok. +terminate(_Reason, _State) -> + ok. + +-spec code_change(term() | {down, term()}, #state{}, term()) -> {ok, term()}. +code_change({down, _}, State, _Extra) -> + true = ets:match_delete(?TAB, {{stats_counters, '_'}, '_'}), + {ok, State}; +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%% Internal. + +set_monitored_process(Key, Pid, State=#state{monitors=Monitors0}) -> + %% First we cleanup the monitor if a residual one exists. + %% This can happen during crashes when the restart is faster + %% than the cleanup. + Monitors = case lists:keytake(Key, 2, Monitors0) of + false -> + Monitors0; + {value, {{OldMonitorRef, _}, _}, Monitors1} -> + true = erlang:demonitor(OldMonitorRef, [flush]), + Monitors1 + end, + %% Then we unconditionally insert in the ets table. + %% If residual data is there, it will be overwritten. + true = ets:insert(?TAB, {Key, Pid}), + %% Finally we start monitoring this new process. + MonitorRef = erlang:monitor(process, Pid), + State#state{monitors=[{{MonitorRef, Pid}, Key}|Monitors]}. diff --git a/ranch/src/ranch_server_proxy.erl b/ranch/src/ranch_server_proxy.erl new file mode 100644 index 0000000..22f9dc8 --- /dev/null +++ b/ranch/src/ranch_server_proxy.erl @@ -0,0 +1,67 @@ +%% Copyright (c) 2019-2021, Jan Uhlig +%% +%% 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(ranch_server_proxy). + +-behavior(gen_server). + +-export([start_link/0]). +-export([init/1]). +-export([handle_call/3]). +-export([handle_cast/2]). +-export([handle_info/2]). +-export([code_change/3]). + +-spec start_link() -> {ok, pid()} | {error, term()}. +start_link() -> + gen_server:start_link(?MODULE, [], []). + +-spec init([]) -> {ok, pid()} | {stop, term()}. +init([]) -> + case wait_ranch_server(50) of + {ok, Monitor} -> + {ok, Monitor, hibernate}; + {error, Reason} -> + {stop, Reason} + end. + +-spec handle_call(_, _, reference()) -> {noreply, reference(), hibernate}. +handle_call(_, _, Monitor) -> + {noreply, Monitor, hibernate}. + +-spec handle_cast(_, reference()) -> {noreply, reference(), hibernate}. +handle_cast(_, Monitor) -> + {noreply, Monitor, hibernate}. + +-spec handle_info(term(), reference()) -> {noreply, reference(), hibernate} | {stop, term(), reference()}. +handle_info({'DOWN', Monitor, process, _, Reason}, Monitor) -> + {stop, Reason, Monitor}; +handle_info(_, Monitor) -> + {noreply, Monitor, hibernate}. + +-spec code_change(term() | {down, term()}, reference(), term()) -> {ok, reference()}. +code_change(_, Monitor, _) -> + {ok, Monitor}. + +wait_ranch_server(N) -> + case whereis(ranch_server) of + undefined when N > 0 -> + receive after 100 -> ok end, + wait_ranch_server(N - 1); + undefined -> + {error, noproc}; + Pid -> + Monitor = monitor(process, Pid), + {ok, Monitor} + end. diff --git a/ranch/src/ranch_ssl.erl b/ranch/src/ranch_ssl.erl new file mode 100644 index 0000000..ff5831e --- /dev/null +++ b/ranch/src/ranch_ssl.erl @@ -0,0 +1,341 @@ +%% Copyright (c) 2011-2021, Loïc Hoguin +%% Copyright (c) 2020-2021, Jan Uhlig +%% Copyright (c) 2021, Maria Scott +%% +%% 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(ranch_ssl). +-behaviour(ranch_transport). + +-export([name/0]). +-export([secure/0]). +-export([messages/0]). +-export([listen/1]). +-export([disallowed_listen_options/0]). +-export([accept/2]). +-export([handshake/2]). +-export([handshake/3]). +-export([handshake_continue/2]). +-export([handshake_continue/3]). +-export([handshake_cancel/1]). +-export([connect/3]). +-export([connect/4]). +-export([recv/3]). +-export([recv_proxy_header/2]). +-export([send/2]). +-export([sendfile/2]). +-export([sendfile/4]). +-export([sendfile/5]). +-export([setopts/2]). +-export([getopts/2]). +-export([getstat/1]). +-export([getstat/2]). +-export([controlling_process/2]). +-export([peername/1]). +-export([sockname/1]). +-export([shutdown/2]). +-export([close/1]). +-export([cleanup/1]). + +-type ssl_opt() :: {alpn_preferred_protocols, [binary()]} + | {anti_replay, '10k' | '100k' | {integer(), integer(), integer()}} + | {beast_mitigation, one_n_minus_one | zero_n | disabled} + | {cacertfile, file:filename()} + | {cacerts, [public_key:der_encoded()]} + | {cert, public_key:der_encoded()} + | {certfile, file:filename()} + | {ciphers, ssl:ciphers()} + | {client_renegotiation, boolean()} + | {crl_cache, [any()]} + | {crl_check, boolean() | peer | best_effort} + | {depth, integer()} + | {dh, binary()} + | {dhfile, file:filename()} + | {eccs, [ssl:named_curve()]} + | {fail_if_no_peer_cert, boolean()} + | {handshake, hello | full} + | {hibernate_after, timeout()} + | {honor_cipher_order, boolean()} + | {honor_ecc_order, boolean()} + | {key, ssl:key()} + | {key_update_at, pos_integer()} + | {keyfile, file:filename()} + | {log_alert, boolean()} + | {log_level, logger:level()} + | {max_handshake_size, integer()} + | {middlebox_comp_mode, boolean()} + | {next_protocols_advertised, [binary()]} + | {padding_check, boolean()} + | {partial_chain, fun()} + | {password, string()} + | {protocol, tls | dtls} + | {psk_identity, string()} + | {reuse_session, fun()} + | {reuse_sessions, boolean()} + | {secure_renegotiate, boolean()} + | {session_tickets, disabled | stateful | stateless} + | {signature_algs, [{ssl:hash(), ssl:sign_algo()}]} + | {signature_algs_cert, [ssl:sign_scheme()]} + | {sni_fun, fun()} + | {sni_hosts, [{string(), ssl_opt()}]} + | {supported_groups, [ssl:group()]} + | {user_lookup_fun, {fun(), any()}} + | {verify, verify_none | verify_peer} + | {verify_fun, {fun(), any()}} + | {versions, [ssl:protocol_version()]}. +-export_type([ssl_opt/0]). + +-type opt() :: ranch_tcp:opt() | ssl_opt(). +-export_type([opt/0]). + +-type opts() :: [opt()]. +-export_type([opts/0]). + +-spec name() -> ssl. +name() -> ssl. + +-spec secure() -> boolean(). +secure() -> + true. + +-spec messages() -> {ssl, ssl_closed, ssl_error, ssl_passive}. +messages() -> {ssl, ssl_closed, ssl_error, ssl_passive}. + +-spec listen(ranch:transport_opts(opts())) -> {ok, ssl:sslsocket()} | {error, atom()}. +listen(TransOpts) -> + ok = cleanup(TransOpts), + SocketOpts = maps:get(socket_opts, TransOpts, []), + case lists:keymember(cert, 1, SocketOpts) + orelse lists:keymember(certfile, 1, SocketOpts) + orelse lists:keymember(sni_fun, 1, SocketOpts) + orelse lists:keymember(sni_hosts, 1, SocketOpts) + orelse lists:keymember(user_lookup_fun, 1, SocketOpts) of + true -> + Logger = maps:get(logger, TransOpts, logger), + do_listen(SocketOpts, Logger); + false -> + {error, no_cert} + end. + +do_listen(SocketOpts0, Logger) -> + SocketOpts1 = ranch:set_option_default(SocketOpts0, backlog, 1024), + SocketOpts2 = ranch:set_option_default(SocketOpts1, nodelay, true), + SocketOpts3 = ranch:set_option_default(SocketOpts2, send_timeout, 30000), + SocketOpts = ranch:set_option_default(SocketOpts3, send_timeout_close, true), + DisallowedOpts0 = disallowed_listen_options(), + DisallowedOpts = unsupported_tls_options(SocketOpts) ++ DisallowedOpts0, + %% We set the port to 0 because it is given in the Opts directly. + %% The port in the options takes precedence over the one in the + %% first argument. + ssl:listen(0, ranch:filter_options(SocketOpts, DisallowedOpts, + [binary, {active, false}, {packet, raw}, {reuseaddr, true}], Logger)). + +%% 'binary' and 'list' are disallowed but they are handled +%% specifically as they do not have 2-tuple equivalents. +-spec disallowed_listen_options() -> [atom()]. +disallowed_listen_options() -> + [alpn_advertised_protocols, client_preferred_next_protocols, + fallback, server_name_indication, srp_identity + |ranch_tcp:disallowed_listen_options()]. + +unsupported_tls_options(SocketOpts) -> + unsupported_tls_version_options(lists:usort(get_tls_versions(SocketOpts))). + +unsupported_tls_version_options([tlsv1|_]) -> + []; +unsupported_tls_version_options(['tlsv1.1'|_]) -> + [beast_mitigation, padding_check]; +unsupported_tls_version_options(['tlsv1.2'|_]) -> + [beast_mitigation, padding_check]; +unsupported_tls_version_options(['tlsv1.3'|_]) -> + [beast_mitigation, client_renegotiation, next_protocols_advertised, + padding_check, psk_identity, reuse_session, reuse_sessions, + secure_renegotiate, user_lookup_fun]; +unsupported_tls_version_options(_) -> + []. + +-spec accept(ssl:sslsocket(), timeout()) + -> {ok, ssl:sslsocket()} | {error, closed | timeout | atom()}. +accept(LSocket, Timeout) -> + ssl:transport_accept(LSocket, Timeout). + +-spec handshake(inet:socket() | ssl:sslsocket(), timeout()) + -> {ok, ssl:sslsocket()} | {ok, ssl:sslsocket(), ssl:protocol_extensions()} | {error, any()}. +handshake(CSocket, Timeout) -> + handshake(CSocket, [], Timeout). + +-spec handshake(inet:socket() | ssl:sslsocket(), opts(), timeout()) + -> {ok, ssl:sslsocket()} | {ok, ssl:sslsocket(), ssl:protocol_extensions()} | {error, any()}. +handshake(CSocket, Opts, Timeout) -> + case ssl:handshake(CSocket, Opts, Timeout) of + OK = {ok, _} -> + OK; + OK = {ok, _, _} -> + OK; + Error = {error, _} -> + Error + end. + +-spec handshake_continue(ssl:sslsocket(), timeout()) + -> {ok, ssl:sslsocket()} | {error, any()}. +handshake_continue(CSocket, Timeout) -> + handshake_continue(CSocket, [], Timeout). + +-spec handshake_continue(ssl:sslsocket(), [ssl:tls_server_option()], timeout()) + -> {ok, ssl:sslsocket()} | {error, any()}. +handshake_continue(CSocket, Opts, Timeout) -> + case ssl:handshake_continue(CSocket, Opts, Timeout) of + OK = {ok, _} -> + OK; + Error = {error, _} -> + Error + end. + +-spec handshake_cancel(ssl:sslsocket()) -> ok. +handshake_cancel(CSocket) -> + ok = ssl:handshake_cancel(CSocket). + +%% @todo Probably filter Opts? +-spec connect(inet:ip_address() | inet:hostname(), + inet:port_number(), any()) + -> {ok, inet:socket()} | {error, atom()}. +connect(Host, Port, Opts) when is_integer(Port) -> + ssl:connect(Host, Port, + Opts ++ [binary, {active, false}, {packet, raw}]). + +%% @todo Probably filter Opts? +-spec connect(inet:ip_address() | inet:hostname(), + inet:port_number(), any(), timeout()) + -> {ok, inet:socket()} | {error, atom()}. +connect(Host, Port, Opts, Timeout) when is_integer(Port) -> + ssl:connect(Host, Port, + Opts ++ [binary, {active, false}, {packet, raw}], + Timeout). + +-spec recv(ssl:sslsocket(), non_neg_integer(), timeout()) + -> {ok, any()} | {error, closed | atom()}. +recv(Socket, Length, Timeout) -> + ssl:recv(Socket, Length, Timeout). + +-spec recv_proxy_header(ssl:sslsocket(), timeout()) + -> {ok, ranch_proxy_header:proxy_info()} + | {error, closed | atom()} + | {error, protocol_error, atom()}. +recv_proxy_header(SSLSocket, Timeout) -> + %% There's currently no documented way to perform a TCP recv + %% on an sslsocket(), even before the TLS handshake. However + %% nothing prevents us from retrieving the TCP socket and using + %% it. Since it's an undocumented interface this may however + %% make forward-compatibility more difficult. + {sslsocket, {gen_tcp, TCPSocket, _, _}, _} = SSLSocket, + ranch_tcp:recv_proxy_header(TCPSocket, Timeout). + +-spec send(ssl:sslsocket(), iodata()) -> ok | {error, atom()}. +send(Socket, Packet) -> + ssl:send(Socket, Packet). + +-spec sendfile(ssl:sslsocket(), file:name_all() | file:fd()) + -> {ok, non_neg_integer()} | {error, atom()}. +sendfile(Socket, Filename) -> + sendfile(Socket, Filename, 0, 0, []). + +-spec sendfile(ssl:sslsocket(), file:name_all() | file:fd(), + non_neg_integer(), non_neg_integer()) + -> {ok, non_neg_integer()} | {error, atom()}. +sendfile(Socket, File, Offset, Bytes) -> + sendfile(Socket, File, Offset, Bytes, []). + +%% Unlike with TCP, no syscall can be used here, so sending files +%% through SSL will be much slower in comparison. Note that unlike +%% file:sendfile/5 this function accepts either a file or a file name. +-spec sendfile(ssl:sslsocket(), file:name_all() | file:fd(), + non_neg_integer(), non_neg_integer(), ranch_transport:sendfile_opts()) + -> {ok, non_neg_integer()} | {error, atom()}. +sendfile(Socket, File, Offset, Bytes, Opts) -> + ranch_transport:sendfile(?MODULE, Socket, File, Offset, Bytes, Opts). + +%% @todo Probably filter Opts? +-spec setopts(ssl:sslsocket(), list()) -> ok | {error, atom()}. +setopts(Socket, Opts) -> + ssl:setopts(Socket, Opts). + +-spec getopts(ssl:sslsocket(), [atom()]) -> {ok, list()} | {error, atom()}. +getopts(Socket, Opts) -> + ssl:getopts(Socket, Opts). + +-spec getstat(ssl:sslsocket()) -> {ok, list()} | {error, atom()}. +getstat(Socket) -> + ssl:getstat(Socket). + +-spec getstat(ssl:sslsocket(), [atom()]) -> {ok, list()} | {error, atom()}. +getstat(Socket, OptionNames) -> + ssl:getstat(Socket, OptionNames). + +-spec controlling_process(ssl:sslsocket(), pid()) + -> ok | {error, closed | not_owner | atom()}. +controlling_process(Socket, Pid) -> + ssl:controlling_process(Socket, Pid). + +-spec peername(ssl:sslsocket()) + -> {ok, {inet:ip_address(), inet:port_number()} | {local, binary()}} | {error, atom()}. +peername(Socket) -> + ssl:peername(Socket). + +-spec sockname(ssl:sslsocket()) + -> {ok, {inet:ip_address(), inet:port_number()} | {local, binary()}} | {error, atom()}. +sockname(Socket) -> + ssl:sockname(Socket). + +-spec shutdown(ssl:sslsocket(), read | write | read_write) + -> ok | {error, atom()}. +shutdown(Socket, How) -> + ssl:shutdown(Socket, How). + +-spec close(ssl:sslsocket()) -> ok. +close(Socket) -> + ssl:close(Socket). + +-spec cleanup(ranch:transport_opts(opts())) -> ok. +cleanup(#{socket_opts:=SocketOpts}) -> + case lists:keyfind(ip, 1, lists:reverse(SocketOpts)) of + {ip, {local, SockFile}} -> + _ = file:delete(SockFile), + ok; + _ -> + ok + end; +cleanup(_) -> + ok. + +get_tls_versions(SocketOpts) -> + %% Socket options need to be reversed for keyfind because later options + %% take precedence when contained multiple times, but keyfind will return + %% the earliest occurence. + case lists:keyfind(versions, 1, lists:reverse(SocketOpts)) of + {versions, Versions} -> + Versions; + false -> + get_tls_versions_env() + end. + +get_tls_versions_env() -> + case application:get_env(ssl, protocol_version) of + {ok, Versions} -> + Versions; + undefined -> + get_tls_versions_app() + end. + +get_tls_versions_app() -> + {supported, Versions} = lists:keyfind(supported, 1, ssl:versions()), + Versions. diff --git a/ranch/src/ranch_sup.erl b/ranch/src/ranch_sup.erl new file mode 100644 index 0000000..15b8b81 --- /dev/null +++ b/ranch/src/ranch_sup.erl @@ -0,0 +1,39 @@ +%% Copyright (c) 2011-2021, Loïc Hoguin +%% Copyright (c) 2020-2021, Jan Uhlig +%% +%% 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(ranch_sup). +-behaviour(supervisor). + +-export([start_link/0]). +-export([init/1]). + +-spec start_link() -> {ok, pid()}. +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +-spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. +init([]) -> + Intensity = case application:get_env(ranch_sup_intensity) of + {ok, Value1} -> Value1; + undefined -> 1 + end, + Period = case application:get_env(ranch_sup_period) of + {ok, Value2} -> Value2; + undefined -> 5 + end, + Procs = [ + #{id => ranch_server, start => {ranch_server, start_link, []}} + ], + {ok, {#{intensity => Intensity, period => Period}, Procs}}. diff --git a/ranch/src/ranch_tcp.erl b/ranch/src/ranch_tcp.erl new file mode 100644 index 0000000..3541289 --- /dev/null +++ b/ranch/src/ranch_tcp.erl @@ -0,0 +1,287 @@ +%% Copyright (c) 2011-2021, Loïc Hoguin +%% Copyright (c) 2020-2021, Jan Uhlig +%% +%% 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(ranch_tcp). +-behaviour(ranch_transport). + +-export([name/0]). +-export([secure/0]). +-export([messages/0]). +-export([listen/1]). +-export([disallowed_listen_options/0]). +-export([accept/2]). +-export([handshake/2]). +-export([handshake/3]). +-export([handshake_continue/2]). +-export([handshake_continue/3]). +-export([handshake_cancel/1]). +-export([connect/3]). +-export([connect/4]). +-export([recv/3]). +-export([recv_proxy_header/2]). +-export([send/2]). +-export([sendfile/2]). +-export([sendfile/4]). +-export([sendfile/5]). +-export([setopts/2]). +-export([getopts/2]). +-export([getstat/1]). +-export([getstat/2]). +-export([controlling_process/2]). +-export([peername/1]). +-export([sockname/1]). +-export([shutdown/2]). +-export([close/1]). +-export([cleanup/1]). + +-type opt() :: {backlog, non_neg_integer()} + | {buffer, non_neg_integer()} + | {delay_send, boolean()} + | {dontroute, boolean()} + | {exit_on_close, boolean()} + | {fd, non_neg_integer()} + | {high_msgq_watermark, non_neg_integer()} + | {high_watermark, non_neg_integer()} + | inet + | inet6 + | {ip, inet:ip_address() | inet:local_address()} + | {ipv6_v6only, boolean()} + | {keepalive, boolean()} + | {linger, {boolean(), non_neg_integer()}} + | {low_msgq_watermark, non_neg_integer()} + | {low_watermark, non_neg_integer()} + | {nodelay, boolean()} + | {port, inet:port_number()} + | {priority, integer()} + | {raw, non_neg_integer(), non_neg_integer(), binary()} + | {recbuf, non_neg_integer()} + | {send_timeout, timeout()} + | {send_timeout_close, boolean()} + | {sndbuf, non_neg_integer()} + | {tos, integer()}. +-export_type([opt/0]). + +-type opts() :: [opt()]. +-export_type([opts/0]). + +-spec name() -> tcp. +name() -> tcp. + +-spec secure() -> boolean(). +secure() -> + false. + +-spec messages() -> {tcp, tcp_closed, tcp_error, tcp_passive}. +messages() -> {tcp, tcp_closed, tcp_error, tcp_passive}. + +-spec listen(ranch:transport_opts(opts())) -> {ok, inet:socket()} | {error, atom()}. +listen(TransOpts) -> + ok = cleanup(TransOpts), + Logger = maps:get(logger, TransOpts, logger), + SocketOpts = maps:get(socket_opts, TransOpts, []), + %% We set the port to 0 because it is given in the Opts directly. + %% The port in the options takes precedence over the one in the + %% first argument. + gen_tcp:listen(0, prepare_socket_opts(SocketOpts, Logger)). + +prepare_socket_opts([Backend = {inet_backend, _}|SocketOpts], Logger) -> + %% In OTP/23, the inet_backend option may be used to activate the + %% experimental socket backend for inet/gen_tcp. If present, it must + %% be the first option in the list. + [Backend|prepare_socket_opts(SocketOpts, Logger)]; +prepare_socket_opts(SocketOpts0, Logger) -> + SocketOpts1 = ranch:set_option_default(SocketOpts0, backlog, 1024), + SocketOpts2 = ranch:set_option_default(SocketOpts1, nodelay, true), + SocketOpts3 = ranch:set_option_default(SocketOpts2, send_timeout, 30000), + SocketOpts4 = ranch:set_option_default(SocketOpts3, send_timeout_close, true), + ranch:filter_options(SocketOpts4, disallowed_listen_options(), + [binary, {active, false}, {packet, raw}, {reuseaddr, true}], Logger). + +%% 'binary' and 'list' are disallowed but they are handled +%% specifically as they do not have 2-tuple equivalents. +-spec disallowed_listen_options() -> [atom()]. +disallowed_listen_options() -> + [active, header, mode, packet, packet_size, line_delimiter, reuseaddr]. + +-spec accept(inet:socket(), timeout()) + -> {ok, inet:socket()} | {error, closed | timeout | atom()}. +accept(LSocket, Timeout) -> + gen_tcp:accept(LSocket, Timeout). + +-spec handshake(inet:socket(), timeout()) -> {ok, inet:socket()}. +handshake(CSocket, Timeout) -> + handshake(CSocket, [], Timeout). + +-spec handshake(inet:socket(), opts(), timeout()) -> {ok, inet:socket()}. +handshake(CSocket, _, _) -> + {ok, CSocket}. + +-spec handshake_continue(inet:socket(), timeout()) -> no_return(). +handshake_continue(CSocket, Timeout) -> + handshake_continue(CSocket, [], Timeout). + +-spec handshake_continue(inet:socket(), opts(), timeout()) -> no_return(). +handshake_continue(_, _, _) -> + error(not_supported). + +-spec handshake_cancel(inet:socket()) -> no_return(). +handshake_cancel(_) -> + error(not_supported). + +%% @todo Probably filter Opts? +-spec connect(inet:ip_address() | inet:hostname(), + inet:port_number(), any()) + -> {ok, inet:socket()} | {error, atom()}. +connect(Host, Port, Opts) when is_integer(Port) -> + gen_tcp:connect(Host, Port, + Opts ++ [binary, {active, false}, {packet, raw}]). + +%% @todo Probably filter Opts? +-spec connect(inet:ip_address() | inet:hostname(), + inet:port_number(), any(), timeout()) + -> {ok, inet:socket()} | {error, atom()}. +connect(Host, Port, Opts, Timeout) when is_integer(Port) -> + gen_tcp:connect(Host, Port, + Opts ++ [binary, {active, false}, {packet, raw}], + Timeout). + +-spec recv(inet:socket(), non_neg_integer(), timeout()) + -> {ok, any()} | {error, closed | atom()}. +recv(Socket, Length, Timeout) -> + gen_tcp:recv(Socket, Length, Timeout). + +-spec recv_proxy_header(inet:socket(), timeout()) + -> {ok, ranch_proxy_header:proxy_info()} + | {error, closed | atom()} + | {error, protocol_error, atom()}. +recv_proxy_header(Socket, Timeout) -> + case recv(Socket, 0, Timeout) of + {ok, Data} -> + case ranch_proxy_header:parse(Data) of + {ok, ProxyInfo, <<>>} -> + {ok, ProxyInfo}; + {ok, ProxyInfo, Rest} -> + case gen_tcp:unrecv(Socket, Rest) of + ok -> + {ok, ProxyInfo}; + Error -> + Error + end; + {error, HumanReadable} -> + {error, protocol_error, HumanReadable} + end; + Error -> + Error + end. + +-spec send(inet:socket(), iodata()) -> ok | {error, atom()}. +send(Socket, Packet) -> + gen_tcp:send(Socket, Packet). + +-spec sendfile(inet:socket(), file:name_all() | file:fd()) + -> {ok, non_neg_integer()} | {error, atom()}. +sendfile(Socket, Filename) -> + sendfile(Socket, Filename, 0, 0, []). + +-spec sendfile(inet:socket(), file:name_all() | file:fd(), non_neg_integer(), + non_neg_integer()) + -> {ok, non_neg_integer()} | {error, atom()}. +sendfile(Socket, File, Offset, Bytes) -> + sendfile(Socket, File, Offset, Bytes, []). + +-spec sendfile(inet:socket(), file:name_all() | file:fd(), non_neg_integer(), + non_neg_integer(), [{chunk_size, non_neg_integer()}]) + -> {ok, non_neg_integer()} | {error, atom()}. +sendfile(Socket, Filename, Offset, Bytes, Opts) + when is_list(Filename) orelse is_atom(Filename) + orelse is_binary(Filename) -> + case file:open(Filename, [read, raw, binary]) of + {ok, RawFile} -> + try sendfile(Socket, RawFile, Offset, Bytes, Opts) of + Result -> Result + after + ok = file:close(RawFile) + end; + {error, _} = Error -> + Error + end; +sendfile(Socket, RawFile, Offset, Bytes, Opts) -> + Opts2 = case Opts of + [] -> [{chunk_size, 16#1FFF}]; + _ -> Opts + end, + try file:sendfile(RawFile, Socket, Offset, Bytes, Opts2) of + Result -> Result + catch + error:{badmatch, {error, enotconn}} -> + %% file:sendfile/5 might fail by throwing a + %% {badmatch, {error, enotconn}}. This is because its + %% implementation fails with a badmatch in + %% prim_file:sendfile/10 if the socket is not connected. + {error, closed} + end. + +%% @todo Probably filter Opts? +-spec setopts(inet:socket(), list()) -> ok | {error, atom()}. +setopts(Socket, Opts) -> + inet:setopts(Socket, Opts). + +-spec getopts(inet:socket(), [atom()]) -> {ok, list()} | {error, atom()}. +getopts(Socket, Opts) -> + inet:getopts(Socket, Opts). + +-spec getstat(inet:socket()) -> {ok, list()} | {error, atom()}. +getstat(Socket) -> + inet:getstat(Socket). + +-spec getstat(inet:socket(), [atom()]) -> {ok, list()} | {error, atom()}. +getstat(Socket, OptionNames) -> + inet:getstat(Socket, OptionNames). + +-spec controlling_process(inet:socket(), pid()) + -> ok | {error, closed | not_owner | atom()}. +controlling_process(Socket, Pid) -> + gen_tcp:controlling_process(Socket, Pid). + +-spec peername(inet:socket()) + -> {ok, {inet:ip_address(), inet:port_number()} | {local, binary()}} | {error, atom()}. +peername(Socket) -> + inet:peername(Socket). + +-spec sockname(inet:socket()) + -> {ok, {inet:ip_address(), inet:port_number()} | {local, binary()}} | {error, atom()}. +sockname(Socket) -> + inet:sockname(Socket). + +-spec shutdown(inet:socket(), read | write | read_write) + -> ok | {error, atom()}. +shutdown(Socket, How) -> + gen_tcp:shutdown(Socket, How). + +-spec close(inet:socket()) -> ok. +close(Socket) -> + gen_tcp:close(Socket). + +-spec cleanup(ranch:transport_opts(opts())) -> ok. +cleanup(#{socket_opts:=SocketOpts}) -> + case lists:keyfind(ip, 1, lists:reverse(SocketOpts)) of + {ip, {local, SockFile}} -> + _ = file:delete(SockFile), + ok; + _ -> + ok + end; +cleanup(_) -> + ok. diff --git a/ranch/src/ranch_transport.erl b/ranch/src/ranch_transport.erl new file mode 100644 index 0000000..52eeba3 --- /dev/null +++ b/ranch/src/ranch_transport.erl @@ -0,0 +1,157 @@ +%% Copyright (c) 2012-2021, Loïc Hoguin +%% Copyright (c) 2020-2021, Jan Uhlig +%% +%% 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(ranch_transport). + +-export([sendfile/6]). + +-type socket() :: any(). +-export_type([socket/0]). + +-type opts() :: any(). +-type stats() :: any(). +-type sendfile_opts() :: [{chunk_size, non_neg_integer()}]. +-export_type([sendfile_opts/0]). + +-callback name() -> atom(). +-callback secure() -> boolean(). +-callback messages() -> {OK::atom(), Closed::atom(), Error::atom(), Passive::atom()}. +-callback listen(ranch:transport_opts(any())) -> {ok, socket()} | {error, atom()}. +-callback accept(socket(), timeout()) + -> {ok, socket()} | {error, closed | timeout | atom()}. +-callback handshake(socket(), timeout()) -> {ok, socket()} | {ok, socket(), any()} | {error, any()}. +-callback handshake(socket(), opts(), timeout()) -> {ok, socket()} | {ok, socket(), any()} | {error, any()}. +-callback handshake_continue(socket(), timeout()) -> {ok, socket()} | {error, any()}. +-callback handshake_continue(socket(), opts(), timeout()) -> {ok, socket()} | {error, any()}. +-callback handshake_cancel(socket()) -> ok. +-callback connect(string(), inet:port_number(), opts()) + -> {ok, socket()} | {error, atom()}. +-callback connect(string(), inet:port_number(), opts(), timeout()) + -> {ok, socket()} | {error, atom()}. +-callback recv(socket(), non_neg_integer(), timeout()) + -> {ok, any()} | {error, closed | timeout | atom()}. +-callback recv_proxy_header(socket(), timeout()) + -> {ok, ranch_proxy_header:proxy_info()} + | {error, closed | atom()} + | {error, protocol_error, atom()}. +-callback send(socket(), iodata()) -> ok | {error, atom()}. +-callback sendfile(socket(), file:name_all() | file:fd()) + -> {ok, non_neg_integer()} | {error, atom()}. +-callback sendfile(socket(), file:name_all() | file:fd(), non_neg_integer(), + non_neg_integer()) -> {ok, non_neg_integer()} | {error, atom()}. +-callback sendfile(socket(), file:name_all() | file:fd(), non_neg_integer(), + non_neg_integer(), sendfile_opts()) + -> {ok, non_neg_integer()} | {error, atom()}. +-callback setopts(socket(), opts()) -> ok | {error, atom()}. +-callback getopts(socket(), [atom()]) -> {ok, opts()} | {error, atom()}. +-callback getstat(socket()) -> {ok, stats()} | {error, atom()}. +-callback getstat(socket(), [atom()]) -> {ok, stats()} | {error, atom()}. +-callback controlling_process(socket(), pid()) + -> ok | {error, closed | not_owner | atom()}. +-callback peername(socket()) + -> {ok, {inet:ip_address(), inet:port_number()} | {local, binary()}} | {error, atom()}. +-callback sockname(socket()) + -> {ok, {inet:ip_address(), inet:port_number()} | {local, binary()}} | {error, atom()}. +-callback shutdown(socket(), read | write | read_write) + -> ok | {error, atom()}. +-callback close(socket()) -> ok. +-callback cleanup(ranch:transport_opts(any())) -> ok. + +%% A fallback for transports that don't have a native sendfile implementation. +%% Note that the ordering of arguments is different from file:sendfile/5 and +%% that this function accepts either a raw file or a file name. +-spec sendfile(module(), socket(), file:name_all() | file:fd(), + non_neg_integer(), non_neg_integer(), sendfile_opts()) + -> {ok, non_neg_integer()} | {error, atom()}. +sendfile(Transport, Socket, Filename, Offset, Bytes, Opts) + when is_list(Filename) orelse is_atom(Filename) + orelse is_binary(Filename) -> + ChunkSize = chunk_size(Opts), + case file:open(Filename, [read, raw, binary]) of + {ok, RawFile} -> + _ = case Offset of + 0 -> + ok; + _ -> + {ok, _} = file:position(RawFile, {bof, Offset}) + end, + try + sendfile_loop(Transport, Socket, RawFile, Bytes, 0, ChunkSize) + after + ok = file:close(RawFile) + end; + {error, _Reason} = Error -> + Error + end; +sendfile(Transport, Socket, RawFile, Offset, Bytes, Opts) -> + ChunkSize = chunk_size(Opts), + Initial2 = case file:position(RawFile, {cur, 0}) of + {ok, Offset} -> + Offset; + {ok, Initial} -> + {ok, _} = file:position(RawFile, {bof, Offset}), + Initial + end, + case sendfile_loop(Transport, Socket, RawFile, Bytes, 0, ChunkSize) of + {ok, _Sent} = Result -> + {ok, _} = file:position(RawFile, {bof, Initial2}), + Result; + {error, _Reason} = Error -> + Error + end. + +-spec chunk_size(sendfile_opts()) -> pos_integer(). +chunk_size(Opts) -> + case lists:keyfind(chunk_size, 1, Opts) of + {chunk_size, ChunkSize} + when is_integer(ChunkSize) andalso ChunkSize > 0 -> + ChunkSize; + {chunk_size, 0} -> + 16#1FFF; + false -> + 16#1FFF + end. + +-spec sendfile_loop(module(), socket(), file:fd(), non_neg_integer(), + non_neg_integer(), pos_integer()) + -> {ok, non_neg_integer()} | {error, any()}. +sendfile_loop(_Transport, _Socket, _RawFile, Sent, Sent, _ChunkSize) + when Sent =/= 0 -> + %% All requested data has been read and sent, return number of bytes sent. + {ok, Sent}; +sendfile_loop(Transport, Socket, RawFile, Bytes, Sent, ChunkSize) -> + ReadSize = read_size(Bytes, Sent, ChunkSize), + case file:read(RawFile, ReadSize) of + {ok, IoData} -> + case Transport:send(Socket, IoData) of + ok -> + Sent2 = iolist_size(IoData) + Sent, + sendfile_loop(Transport, Socket, RawFile, Bytes, Sent2, + ChunkSize); + {error, _Reason} = Error -> + Error + end; + eof -> + {ok, Sent}; + {error, _Reason} = Error -> + Error + end. + +-spec read_size(non_neg_integer(), non_neg_integer(), non_neg_integer()) -> + non_neg_integer(). +read_size(0, _Sent, ChunkSize) -> + ChunkSize; +read_size(Bytes, Sent, ChunkSize) -> + min(Bytes - Sent, ChunkSize). diff --git a/ranch/test/acceptor_SUITE.erl b/ranch/test/acceptor_SUITE.erl new file mode 100644 index 0000000..afb6ffb --- /dev/null +++ b/ranch/test/acceptor_SUITE.erl @@ -0,0 +1,1927 @@ +%% Copyright (c) 2011-2021, Loïc Hoguin +%% Copyright (c) 2020-2021, Jan Uhlig +%% Copyright (c) 2021, Maria Scott +%% +%% 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(acceptor_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-dialyzer({nowarn_function, misc_wait_for_connections/1}). +%% @todo Remove when specs in ssl are updated to accept local addresses. +-dialyzer({nowarn_function, do_ssl_local_echo/0}). + +-import(ct_helper, [config/2]). +-import(ct_helper, [doc/1]). +-import(ct_helper, [name/0]). + +%% ct. + +all() -> + [{group, tcp}, {group, tcp_socket}, {group, ssl}, {group, misc}, {group, supervisor}]. + +groups() -> + [{tcp, [ + tcp_active_echo, + tcp_active_n_echo, + tcp_echo, + tcp_local_echo, + tcp_graceful, + tcp_inherit_options, + tcp_max_connections, + tcp_max_connections_and_beyond, + tcp_max_connections_infinity, + tcp_remove_connections, + tcp_remove_connections_acceptor_wakeup, + tcp_set_max_connections, + tcp_set_max_connections_clean, + tcp_getopts_capability, + tcp_getstat_capability, + tcp_upgrade, + tcp_10_acceptors_10_listen_sockets, + tcp_many_listen_sockets_no_reuseport, + tcp_error_eaddrinuse, + tcp_error_eacces + ]}, {tcp_socket, [ + tcp_active_echo, + tcp_active_n_echo, + tcp_echo, + tcp_graceful, + tcp_inherit_options, + tcp_max_connections, + tcp_max_connections_and_beyond, + tcp_max_connections_infinity, + tcp_remove_connections, + tcp_remove_connections_acceptor_wakeup, + tcp_set_max_connections, + tcp_set_max_connections_clean, + tcp_getopts_capability, + tcp_getstat_capability, + tcp_upgrade, + %% @TODO: Enable when https://github.com/erlang/otp/issues/5122 + %% is in an official release, probably 24.1. + % tcp_10_acceptors_10_listen_sockets, + % tcp_many_listen_sockets_no_reuseport, + tcp_error_eaddrinuse + %% @TODO: Not working in OTP/24.0 but fixed in current master. + %% Enable when fixed in an official release, probably 24.1. + % tcp_error_eacces + ]}, {ssl, [ + ssl_accept_error, + ssl_active_echo, + ssl_active_n_echo, + ssl_echo, + ssl_local_echo, + ssl_graceful, + ssl_handshake, + ssl_sni_echo, + ssl_sni_fail, + ssl_tls_psk, + ssl_tls_psk_fail, + ssl_upgrade_from_tcp, + ssl_getopts_capability, + ssl_getstat_capability, + ssl_10_acceptors_10_listen_sockets, + ssl_many_listen_sockets_no_reuseport, + ssl_error_eaddrinuse, + ssl_error_no_cert, + ssl_error_eacces, + ssl_unsupported_tlsv13_options + ]}, {misc, [ + misc_bad_transport, + misc_bad_transport_options, + misc_repeated_remove, + misc_info, + misc_info_embedded, + misc_metrics, + misc_opts_logger, + misc_post_listen_callback, + misc_post_listen_callback_error, + misc_set_transport_options, + misc_wait_for_connections, + misc_multiple_ip_local_socket_opts, + misc_connection_alarms + ]}, {supervisor, [ + connection_type_supervisor, + connection_type_supervisor_separate_from_connection, + supervisor_10_acceptors_1_conns_sup, + supervisor_9_acceptors_4_conns_sups, + supervisor_10_acceptors_10_conns_sups, + supervisor_1_acceptor_10_conns_sups, + supervisor_changed_options_restart, + supervisor_clean_child_restart, + supervisor_clean_restart, + supervisor_conns_alive, + supervisor_embedded_ranch_server_crash, + supervisor_protocol_start_link_crash, + supervisor_server_recover_state, + supervisor_unexpected_message + ]}]. + +init_per_group(tcp_socket, Config) -> + %% The socket backend for inet/gen_tcp was introduced as an experimental + %% feature in OTP/23.0, and bugs https://bugs.erlang.org/browse/ERL-1284, + %% 1287 and 1293 were solved in OTP/23.1. socket:use_registry/1 first + %% appears in this release. + %% Due to https://bugs.erlang.org/browse/ERL-1401, the socket backend + %% is not working on Windows. + case + os:type() =/= {win32, nt} andalso + code:ensure_loaded(socket) =:= {module, socket} andalso + erlang:function_exported(socket, use_registry, 1) + of + true -> + [{socket_opts, [{inet_backend, socket}]}|Config]; + false -> + {skip, "No socket backend support"} + end; +init_per_group(_, Config) -> + case + code:ensure_loaded(socket) =:= {module, socket} andalso + erlang:function_exported(socket, use_registry, 1) + of + true -> + [{socket_opts, [{inet_backend, inet}]}|Config]; + false -> + [{socket_opts, []}|Config] + end. + +end_per_group(_, _) -> + ok. + +%% misc. + +misc_bad_transport(_) -> + doc("Reject invalid transport modules."), + {error, {bad_transport, invalid_transport}} = ranch:start_listener(misc_bad_transport, + invalid_transport, #{}, + echo_protocol, []), + ok. + +misc_bad_transport_options(_) -> + doc("Ignore invalid transport options."), + {ok, _} = ranch:start_listener(misc_bad_transport_options, + ranch_tcp, [binary, {packet, 4}, <<"garbage">>, raw, backlog], + echo_protocol, []), + ok. + +misc_info(_) -> + doc("Information about listeners."), + %% Open a listener with a few connections. + {ok, Pid1} = ranch:start_listener({misc_info, tcp}, + ranch_tcp, #{num_acceptors => 1}, + remove_conn_and_wait_protocol, [{remove, true, 2500}]), + Port1 = ranch:get_port({misc_info, tcp}), + %% Open a few more listeners with different arguments. + {ok, Pid2} = ranch:start_listener({misc_info, act}, + ranch_tcp, #{num_acceptors => 2}, + active_echo_protocol, {}), + Port2 = ranch:get_port({misc_info, act}), + ranch:set_max_connections({misc_info, act}, infinity), + Opts = ct_helper:get_certs_from_ets(), + {ok, Pid3} = ranch:start_listener({misc_info, ssl}, + ranch_ssl, #{num_acceptors => 3, socket_opts => Opts}, + echo_protocol, [{}]), + Port3 = ranch:get_port({misc_info, ssl}), + %% Open 5 connections, 3 removed from the count. + {ok, _} = gen_tcp:connect("localhost", Port1, [binary, {active, false}, {packet, raw}]), + {ok, _} = gen_tcp:connect("localhost", Port1, [binary, {active, false}, {packet, raw}]), + {ok, _} = gen_tcp:connect("localhost", Port1, [binary, {active, false}, {packet, raw}]), + receive after 250 -> ok end, + ranch:set_protocol_options({misc_info, tcp}, [{remove, false, 2500}]), + receive after 250 -> ok end, + {ok, _} = gen_tcp:connect("localhost", Port1, [binary, {active, false}, {packet, raw}]), + {ok, _} = gen_tcp:connect("localhost", Port1, [binary, {active, false}, {packet, raw}]), + receive after 250 -> ok end, + %% Confirm the info returned by Ranch is correct. + #{ + {misc_info, act} := #{ + pid := Pid2, + port := Port2, + max_connections := infinity, %% Option was modified. + active_connections := 0, + all_connections := 0, + transport := ranch_tcp, + transport_options := #{num_acceptors := 2}, + protocol := active_echo_protocol, + protocol_options := {} + }, + {misc_info, ssl} := #{ + pid := Pid3, + port := Port3, + max_connections := 1024, + active_connections := 0, + all_connections := 0, + transport := ranch_ssl, + transport_options := #{num_acceptors := 3, socket_opts := Opts}, + protocol := echo_protocol, + protocol_options := [{}] + }, + {misc_info, tcp} := #{ + pid := Pid1, + port := Port1, + max_connections := 1024, + active_connections := 2, + all_connections := 5, + transport := ranch_tcp, + transport_options := #{num_acceptors := 1}, + protocol := remove_conn_and_wait_protocol, + protocol_options := [{remove, false, 2500}] %% Option was modified. + } + } = ranch:info(), + %% Get acceptors. + [_] = ranch:procs({misc_info, tcp}, acceptors), + [_, _] = ranch:procs({misc_info, act}, acceptors), + [_, _, _] = ranch:procs({misc_info, ssl}, acceptors), + %% Get connections. + [_, _, _, _, _] = ranch:procs({misc_info, tcp}, connections), + [] = ranch:procs({misc_info, act}, connections), + [] = ranch:procs({misc_info, ssl}, connections), + ok. + +misc_info_embedded(_) -> + doc("Information about listeners in embedded mode."), + {ok, SupPid} = embedded_sup:start_link(), + %% Open a listener with a few connections. + {ok, EmbeddedSupPid1} = embedded_sup:start_listener(SupPid, {misc_info_embedded, tcp}, + ranch_tcp, #{num_acceptors => 1}, + remove_conn_and_wait_protocol, [{remove, true, 2500}]), + {_, Pid1, _, _} = lists:keyfind({ranch_listener_sup, {misc_info_embedded, tcp}}, 1, + supervisor:which_children(EmbeddedSupPid1)), + Port1 = ranch:get_port({misc_info_embedded, tcp}), + %% Open a few more listeners with different arguments. + {ok, EmbeddedSupPid2} = embedded_sup:start_listener(SupPid, {misc_info_embedded, act}, + ranch_tcp, #{num_acceptors => 2}, + active_echo_protocol, {}), + {_, Pid2, _, _} = lists:keyfind({ranch_listener_sup, {misc_info_embedded, act}}, 1, + supervisor:which_children(EmbeddedSupPid2)), + Port2 = ranch:get_port({misc_info_embedded, act}), + ranch:set_max_connections({misc_info_embedded, act}, infinity), + Opts = ct_helper:get_certs_from_ets(), + {ok, EmbeddedSupPid3} = embedded_sup:start_listener(SupPid, {misc_info_embedded, ssl}, + ranch_ssl, #{num_acceptors => 3, socket_opts => Opts}, + echo_protocol, [{}]), + {_, Pid3, _, _} = lists:keyfind({ranch_listener_sup, {misc_info_embedded, ssl}}, 1, + supervisor:which_children(EmbeddedSupPid3)), + Port3 = ranch:get_port({misc_info_embedded, ssl}), + %% Open 5 connections, 3 removed from the count. + {ok, _} = gen_tcp:connect("localhost", Port1, [binary, {active, false}, {packet, raw}]), + {ok, _} = gen_tcp:connect("localhost", Port1, [binary, {active, false}, {packet, raw}]), + {ok, _} = gen_tcp:connect("localhost", Port1, [binary, {active, false}, {packet, raw}]), + receive after 250 -> ok end, + ranch:set_protocol_options({misc_info_embedded, tcp}, [{remove, false, 2500}]), + receive after 250 -> ok end, + {ok, _} = gen_tcp:connect("localhost", Port1, [binary, {active, false}, {packet, raw}]), + {ok, _} = gen_tcp:connect("localhost", Port1, [binary, {active, false}, {packet, raw}]), + receive after 250 -> ok end, + %% Confirm the info returned by Ranch is correct. + #{ + {misc_info_embedded, act} := #{ + pid := Pid2, + port := Port2, + max_connections := infinity, %% Option was modified. + active_connections := 0, + all_connections := 0, + transport := ranch_tcp, + transport_options := #{num_acceptors := 2}, + protocol := active_echo_protocol, + protocol_options := {} + }, + {misc_info_embedded, ssl} := #{ + pid := Pid3, + port := Port3, + max_connections := 1024, + active_connections := 0, + all_connections := 0, + transport := ranch_ssl, + transport_options := #{num_acceptors := 3, socket_opts := Opts}, + protocol := echo_protocol, + protocol_options := [{}] + }, + {misc_info_embedded, tcp} := #{ + pid := Pid1, + port := Port1, + max_connections := 1024, + active_connections := 2, + all_connections := 5, + transport := ranch_tcp, + transport_options := #{num_acceptors := 1}, + protocol := remove_conn_and_wait_protocol, + protocol_options := [{remove, false, 2500}] %% Option was modified. + } + } = ranch:info(), + %% Get acceptors. + [_] = ranch:procs({misc_info_embedded, tcp}, acceptors), + [_, _] = ranch:procs({misc_info_embedded, act}, acceptors), + [_, _, _] = ranch:procs({misc_info_embedded, ssl}, acceptors), + %% Get connections. + [_, _, _, _, _] = ranch:procs({misc_info_embedded, tcp}, connections), + [] = ranch:procs({misc_info_embedded, act}, connections), + [] = ranch:procs({misc_info_embedded, ssl}, connections), + %% Stop embedded tcp listener and ensure it is gone. + ok = embedded_sup:stop_listener(SupPid, {misc_info_embedded, tcp}), + timer:sleep(500), + false = maps:is_key({misc_info_embedded, tcp}, ranch:info()), + %% Stop embedded act listener and ensure it is gone. + ok = embedded_sup:stop_listener(SupPid, {misc_info_embedded, act}), + timer:sleep(500), + false = maps:is_key({misc_info_embedded, act}, ranch:info()), + %% Stop embedded ssl listener and ensure it is gone. + ok = embedded_sup:stop_listener(SupPid, {misc_info_embedded, ssl}), + timer:sleep(500), + false = maps:is_key({misc_info_embedded, ssl}, ranch:info()), + %% Stop embedded supervisor. + embedded_sup:stop(SupPid), + ok. + +misc_metrics(_) -> + doc("Confirm accept/terminate metrics are correct."), + Name = name(), + {ok, _} = ranch:start_listener(Name, ranch_tcp, #{}, + notify_and_wait_protocol, #{pid => self()}), + Port = ranch:get_port(Name), + %% Start 10 connections. + ok = connect_loop(Port, 10, 0), + {10, ConnPids1} = receive_loop(connected, 400), + #{metrics := Metrics1} = ranch:info(Name), + {10, 0} = do_accumulate_metrics(Metrics1), + %% Start 10 more connections. + ok = connect_loop(Port, 10, 0), + {10, ConnPids2} = receive_loop(connected, 400), + #{metrics := Metrics2} = ranch:info(Name), + {20, 0} = do_accumulate_metrics(Metrics2), + %% Terminate 10 connections. + ok = terminate_loop(stop, ConnPids2), + timer:sleep(100), + #{metrics := Metrics3} = ranch:info(Name), + {20, 10} = do_accumulate_metrics(Metrics3), + %% Terminate 10 more connections. + ok = terminate_loop(stop, ConnPids1), + timer:sleep(100), + #{metrics := Metrics4} = ranch:info(Name), + {20, 20} = do_accumulate_metrics(Metrics4), + ok = ranch:stop_listener(Name), + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +do_accumulate_metrics(Metrics) -> + maps:fold( + fun + ({conns_sup, _, accept}, N, {Accepts, Terminates}) -> + {Accepts+N, Terminates}; + ({conns_sup, _, terminate}, N, {Accepts, Terminates}) -> + {Accepts, Terminates+N} + end, + {0, 0}, + Metrics + ). + +misc_opts_logger(_) -> + doc("Confirm that messages are sent via the configured logger module."), + register(misc_opts_logger, self()), + {ok, _} = ranch:start_listener(name(), + ranch_tcp, #{logger => ?MODULE, socket_opts => [<<"garbage">>]}, + echo_protocol, []), + receive + {warning, "Transport option " ++ _, [<<"garbage">>]} -> + ok + after 1000 -> + error(timeout) + end. + +warning(Format, Args) -> + misc_opts_logger ! {warning, Format, Args}. + +misc_post_listen_callback(_) -> + doc("Ensure that the post-listen callback works."), + Name = name(), + Self = self(), + Ref = make_ref(), + PostListenCb = fun (LSock) -> + ok = ranch_tcp:setopts(LSock, [{send_timeout, 1000}]), + Self ! {post_listen, Ref, LSock}, + ok + end, + {ok, _} = ranch:start_listener(Name, + ranch_tcp, #{post_listen_callback => PostListenCb, + socket_opts => [{send_timeout, infinity}]}, + echo_protocol, []), + receive + {post_listen, Ref, LSock} -> + {ok, [{send_timeout, 1000}]} = ranch_tcp:getopts(LSock, [send_timeout]), + ok + after 1000 -> + error(timeout) + end, + Port = ranch:get_port(Name), + {ok, S} = gen_tcp:connect("localhost", Port, [binary, {active, false}, {packet, raw}]), + ok = gen_tcp:send(S, <<"Test">>), + {ok, <<"Test">>} = gen_tcp:recv(S, 4, 1000), + ok = ranch:stop_listener(Name), + {error, closed} = gen_tcp:recv(S, 0, 1000), + %% Make sure the listener stopped. + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +misc_post_listen_callback_error(_) -> + doc("Ensure that starting a listener fails when the post-listen callback returns an error."), + Name = name(), + PostListenCb = fun (_) -> {error, test} end, + {error, _} = ranch:start_listener(Name, + ranch_tcp, #{post_listen_callback => PostListenCb}, + echo_protocol, []), + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +misc_repeated_remove(_) -> + doc("Ensure repeated removal of connection does not crash the connection supervisor."), + Name = name(), + {ok, _} = ranch:start_listener(Name, + ranch_tcp, #{}, + remove_conn_and_wait_protocol, [{remove, 5, 0}]), + Port = ranch:get_port(Name), + ConnsSups = lists:sort(ranch_server:get_connections_sups(Name)), + {ok, _} = gen_tcp:connect("localhost", Port, [binary, {active, false}, {packet, raw}]), + timer:sleep(1000), + ConnsSups = lists:sort(ranch_server:get_connections_sups(Name)), + true = lists:all(fun ({_, ConnsSup}) -> erlang:is_process_alive(ConnsSup) end, ConnsSups), + ok = ranch:stop_listener(Name). + +misc_set_transport_options(_) -> + doc(""), + Name = name(), + {ok, ListenerSupPid} = ranch:start_listener(Name, ranch_tcp, #{max_connections => 10, + handshake_timeout => 5000, shutdown => 1000, num_acceptors => 1, + socket_opts => [{send_timeout, 5000}]}, echo_protocol, []), + ok = ranch:set_transport_options(Name, #{max_connections => 20, handshake_timeout => 5001, + num_acceptors => 2, shutdown => 1001, socket_opts => [{send_timeout, 5002}]}), + ConnsSups = [ConnsSup || {_, ConnsSup} <- ranch_server:get_connections_sups(Name)], + _ = [begin + {State, _, _, _} = sys:get_state(ConnsSup), + 20 = element(11, State), + 5001 = element(10, State), + 1001 = element(6, State) + end || ConnsSup <- ConnsSups], + ok = ranch:suspend_listener(Name), + ok = ranch:resume_listener(Name), + 2 = length(ranch:procs(Name, acceptors)), + LSocket = do_get_listener_socket(ListenerSupPid), + {ok, [{send_timeout, 5002}]} = ranch_tcp:getopts(LSocket, [send_timeout]), + ok = ranch:stop_listener(Name). + +misc_wait_for_connections(_) -> + doc("Ensure wait for connections works."), + Name = name(), + Self = self(), + %% Ensure invalid arguments are rejected. + {'EXIT', {badarg, _}} = begin catch ranch:wait_for_connections(Name, 'foo', 0) end, + {'EXIT', {badarg, _}} = begin catch ranch:wait_for_connections(Name, '==', -1) end, + {'EXIT', {badarg, _}} = begin catch ranch:wait_for_connections(Name, '==', 0, -1) end, + {'EXIT', {badarg, _}} = begin catch ranch:wait_for_connections(Name, '<', 0) end, + %% Create waiters for increasing number of connections. + Pid1GT = do_create_waiter(Self, Name, '>', 0), + Pid1GE = do_create_waiter(Self, Name, '>=', 1), + Pid1EQ = do_create_waiter(Self, Name, '==', 1), + Pid2GT = do_create_waiter(Self, Name, '>', 1), + Pid2GE = do_create_waiter(Self, Name, '>=', 2), + Pid2EQ = do_create_waiter(Self, Name, '==', 2), + {ok, _} = ranch:start_listener(Name, + ranch_tcp, #{num_acceptors => 1}, + echo_protocol, []), + Port = ranch:get_port(Name), + %% Create some connections, ensure that waiters respond. + {ok, Sock1} = gen_tcp:connect("localhost", Port, []), + ok = do_expect_waiter(Pid1GT), + ok = do_expect_waiter(Pid1GE), + ok = do_expect_waiter(Pid1EQ), + ok = do_expect_waiter(undefined), + {ok, Sock2} = gen_tcp:connect("localhost", Port, []), + ok = do_expect_waiter(Pid2GT), + ok = do_expect_waiter(Pid2GE), + ok = do_expect_waiter(Pid2EQ), + ok = do_expect_waiter(undefined), + %% Create waiters for decreasing number of connections. + Pid3LT = do_create_waiter(Self, Name, '<', 2), + Pid3LE = do_create_waiter(Self, Name, '=<', 1), + Pid3EQ = do_create_waiter(Self, Name, '==', 1), + Pid4LT = do_create_waiter(Self, Name, '<', 1), + Pid4LE = do_create_waiter(Self, Name, '=<', 0), + Pid4EQ = do_create_waiter(Self, Name, '==', 0), + %% Close connections, ensure that waiters respond. + ok = gen_tcp:close(Sock1), + ok = do_expect_waiter(Pid3LT), + ok = do_expect_waiter(Pid3LE), + ok = do_expect_waiter(Pid3EQ), + ok = do_expect_waiter(undefined), + ok = gen_tcp:close(Sock2), + ok = do_expect_waiter(Pid4LT), + ok = do_expect_waiter(Pid4LE), + ok = do_expect_waiter(Pid4EQ), + ok = do_expect_waiter(undefined), + ok = ranch:stop_listener(Name), + %% Make sure the listener stopped. + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +do_create_waiter(ReplyTo, Ref, Op, NumConns) -> + spawn(fun () -> ok = ranch:wait_for_connections(Ref, Op, NumConns, 100), + ReplyTo ! {wait_connections, self()} end). + +do_expect_waiter(WaiterPid) -> + receive + {wait_connections, _} when WaiterPid=:=undefined -> + error; + {wait_connections, Pid} when Pid=:=WaiterPid -> + ok + after 1000 -> + case WaiterPid of + undefined -> + ok; + _ -> + timeout + end + end. + +misc_multiple_ip_local_socket_opts(_) -> + case do_os_supports_local_sockets() of + true -> + do_misc_multiple_ip_local_socket_opts(); + false -> + {skip, "No local socket support."} + end. + +do_misc_multiple_ip_local_socket_opts() -> + doc("Ensure that a listener uses the expected ip option if multiple are given."), + Name = name(), + SockFile1 = do_tempname(), + SockFile2 = do_tempname(), + Opts = [{ip, {local, SockFile1}}, {ip, {local, SockFile2}}], + {ok, _} = ranch:start_listener(Name, ranch_tcp, #{socket_opts => Opts}, echo_protocol, []), + {local, SockFile2} = ranch:get_addr(Name), + %% Make sure the socket file from the ignored ip option + %% has not been created. + {error, enoent} = file:read_file_info(SockFile1), + ok = ranch:stop_listener(Name), + %% Make sure the socket file from the accepted ip option + %% is removed. + {error, enoent} = file:read_file_info(SockFile2), + ok. + +misc_connection_alarms(_) -> + doc("Ensure that connection alarms work."), + Name = name(), + + Self = self(), + TransOpts0 = #{num_conns_sups => 1}, + AlarmCallback = fun (Ref, AlarmName, _, ActiveConns) -> + Self ! {connection_alarm, {Ref, AlarmName, length(ActiveConns)}} + end, + Alarms0 = #{ + test1 => Alarm1 = #{type => num_connections, treshold => 2, cooldown => 0, callback => AlarmCallback}, + test2 => Alarm2 = #{type => num_connections, treshold => 3, cooldown => 0, callback => AlarmCallback} + }, + ConnectOpts = [binary, {active, false}, {packet, raw}], + + {ok, _} = ranch:start_listener(Name, ranch_tcp, + TransOpts0#{alarms => Alarms0}, notify_and_wait_protocol, #{pid => self()}), + Port = ranch:get_port(Name), + + {ok, _} = gen_tcp:connect("localhost", Port, ConnectOpts), + {1, [Conn1]} = receive_loop(connected, 100), + #{test1 := undefined, test2 := undefined} = do_recv_connection_alarms(Name, 100), + + {ok, _} = gen_tcp:connect("localhost", Port, ConnectOpts), + {1, [Conn2]} = receive_loop(connected, 100), + #{test1 := 2, test2 := undefined} = do_recv_connection_alarms(Name, 100), + + {ok, _} = gen_tcp:connect("localhost", Port, ConnectOpts), + {1, [Conn3]} = receive_loop(connected, 100), + #{test1 := 3, test2 := 3} = do_recv_connection_alarms(Name, 100), + + Alarms1 = #{ + test1 => Alarm1#{cooldown => 100}, + test2 => Alarm2#{cooldown => 100} + }, + ok = ranch:set_transport_options(Name, TransOpts0#{alarms => Alarms1}), + ok = do_flush_connection_alarms(Name), + #{test1 := 3, test2 := 3} = do_recv_connection_alarms(Name, 100), + ok = do_flush_connection_alarms(Name), + #{test1 := 3, test2 := 3} = do_recv_connection_alarms(Name, 100), + + Conn3 ! stop, + timer:sleep(100), + ok = do_flush_connection_alarms(Name), + #{test1 := 2, test2 := undefined} = do_recv_connection_alarms(Name, 100), + + Conn2 ! stop, + timer:sleep(100), + ok = do_flush_connection_alarms(Name), + #{test1 := undefined, test2 := undefined} = do_recv_connection_alarms(Name, 100), + + Conn1 ! stop, + + ok = ranch:stop_listener(Name), + ok. + +do_recv_connection_alarms(Name, Timeout) -> + do_recv_connection_alarms(Name, Timeout, #{test1 => undefined, test2 => undefined}). + +do_recv_connection_alarms(Name, Timeout, Acc) -> + receive {connection_alarm, {Name, AlarmName, N}} -> + do_recv_connection_alarms(Name, Timeout, Acc#{AlarmName => N}) + after Timeout -> + Acc + end. + +do_flush_connection_alarms(Name) -> + receive {connection_alarm, {Name, _, _}} -> + do_flush_connection_alarms(Name) + after 0 -> + ok + end. + +%% ssl. + +ssl_accept_error(_) -> + doc("Acceptor must not crash if client disconnects in the middle of SSL handshake."), + Name = name(), + Opts = ct_helper:get_certs_from_ets(), + {ok, ListenerSup} = ranch:start_listener(Name, + ranch_ssl, #{num_acceptors => 1, socket_opts => Opts}, + echo_protocol, []), + Port = ranch:get_port(Name), + ListenerSupChildren = supervisor:which_children(ListenerSup), + {_, AcceptorsSup, _, _} = lists:keyfind(ranch_acceptors_sup, 1, ListenerSupChildren), + [{{acceptor, _, _}, AcceptorPid, _, _}] = supervisor:which_children(AcceptorsSup), + true = is_process_alive(AcceptorPid), + {ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {active, false}, {packet, raw}]), + ok = gen_tcp:close(Socket), + receive after 500 -> ok end, + true = is_process_alive(AcceptorPid), + ok = ranch:stop_listener(Name). + +ssl_10_acceptors_10_listen_sockets(_) -> + case do_os_supports_reuseport() of + true -> + ok = do_ssl_10_acceptors_10_listen_sockets(); + false -> + {skip, "No SO_REUSEPORT support."} + end. + +do_ssl_10_acceptors_10_listen_sockets() -> + doc("Ensure that we can use 10 listen sockets across 10 acceptors with SSL."), + Name = name(), + Opts = ct_helper:get_certs_from_ets(), + {ok, ListenerSupPid} = ranch:start_listener(Name, + ranch_ssl, #{ + num_acceptors => 10, + num_listen_sockets => 10, + socket_opts => [{raw, 1, 15, <<1:32/native>>}|Opts]}, + echo_protocol, []), + 10 = length(do_get_listener_sockets(ListenerSupPid)), + ok = ranch:stop_listener(Name), + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +ssl_many_listen_sockets_no_reuseport(_) -> + case do_os_supports_reuseport() of + true -> + ok = do_ssl_many_listen_sockets_no_reuseport(); + false -> + {skip, "No SO_REUSEPORT support."} + end. + +do_ssl_many_listen_sockets_no_reuseport() -> + doc("Confirm that ranch:start_listener/5 fails when SO_REUSEPORT is not available with SSL."), + Name = name(), + Opts = ct_helper:get_certs_from_ets(), + {error, eaddrinuse} = ranch:start_listener(Name, + ranch_ssl, #{ + num_acceptors => 10, + num_listen_sockets => 10, + socket_opts => [{raw, 1, 15, <<0:32/native>>}|Opts]}, + echo_protocol, []), + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +ssl_active_echo(_) -> + doc("Ensure that active mode works with SSL transport."), + Name = name(), + Opts = ct_helper:get_certs_from_ets(), + {ok, _} = ranch:start_listener(Name, + ranch_ssl, Opts, + active_echo_protocol, []), + Port = ranch:get_port(Name), + {ok, Socket} = ssl:connect("localhost", Port, [binary, {active, false}, {packet, raw}]), + ok = ssl:send(Socket, <<"SSL Ranch is working!">>), + {ok, <<"SSL Ranch is working!">>} = ssl:recv(Socket, 21, 1000), + ok = ranch:stop_listener(Name), + {error, closed} = ssl:recv(Socket, 0, 1000), + %% Make sure the listener stopped. + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +ssl_active_n_echo(_) -> + case do_get_ssl_version() >= {9, 2, 0} of + true -> + do_ssl_active_n_echo(); + false -> + {skip, "No Active N support."} + end. + +do_ssl_active_n_echo() -> + doc("Ensure that active N mode works with SSL transport."), + Name = name(), + Opts = ct_helper:get_certs_from_ets(), + {ok, _} = ranch:start_listener(Name, + ranch_ssl, Opts, + batch_echo_protocol, [{batch_size, 3}]), + Port = ranch:get_port(Name), + {ok, Socket} = ssl:connect("localhost", Port, [binary, {active, false}, {packet, raw}]), + ok = ssl:send(Socket, <<"One">>), + {ok, <<"OK">>} = ssl:recv(Socket, 2, 1000), + ok = ssl:send(Socket, <<"Two">>), + {ok, <<"OK">>} = ssl:recv(Socket, 2, 1000), + ok = ssl:send(Socket, <<"Three">>), + {ok, <<"OK">>} = ssl:recv(Socket, 2, 1000), + {ok, <<"OneTwoThree">>} = ssl:recv(Socket, 11, 1000), + ok = ranch:stop_listener(Name), + {error, closed} = ssl:recv(Socket, 0, 1000), + %% Make sure the listener stopped. + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +ssl_echo(_) -> + doc("Ensure that passive mode works with SSL transport."), + Name = name(), + Opts = ct_helper:get_certs_from_ets(), + {ok, _} = ranch:start_listener(Name, + ranch_ssl, Opts, + echo_protocol, []), + Port = ranch:get_port(Name), + {ok, Socket} = ssl:connect("localhost", Port, [binary, {active, false}, {packet, raw}]), + ok = ssl:send(Socket, <<"SSL Ranch is working!">>), + {ok, <<"SSL Ranch is working!">>} = ssl:recv(Socket, 21, 1000), + ok = ranch:stop_listener(Name), + {error, closed} = ssl:recv(Socket, 0, 1000), + %% Make sure the listener stopped. + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +ssl_handshake(_) -> + doc("Ensure that multiple steps handshake works with SSL transport."), + Name = name(), + {CaCert1, Cert1, Key1} = ct_helper:make_certs(), + {CaCert2, Cert2, Key2} = ct_helper:make_certs(), + Opts1 = [{cert, Cert1}, {key, Key1}, {cacerts, [CaCert1]}, {verify, verify_peer}], + Opts2 = [{cert, Cert2}, {key, Key2}, {cacerts, [CaCert2]}, {verify, verify_peer}], + DefaultOpts = ct_helper:get_certs_from_ets(), + {ok, _} = ranch:start_listener(Name, + ranch_ssl, [{handshake, hello}|DefaultOpts], + handshake_protocol, #{"ranch1" => Opts1, "ranch2" => Opts2}), + Port = ranch:get_port(Name), + {ok, Socket1} = ssl:connect("localhost", Port, [binary, {active, false}, {packet, raw}, + {server_name_indication, "ranch1"}], 5000), + {ok, Cert1} = ssl:peercert(Socket1), + ok = ssl:send(Socket1, <<"SSL Ranch is working!">>), + {ok, <<"SSL Ranch is working!">>} = ssl:recv(Socket1, 21, 1000), + {ok, Socket2} = ssl:connect("localhost", Port, [binary, {active, false}, {packet, raw}, + {server_name_indication, "ranch2"}], 5000), + {ok, Cert2} = ssl:peercert(Socket2), + ok = ssl:send(Socket2, <<"SSL Ranch is working!">>), + {ok, <<"SSL Ranch is working!">>} = ssl:recv(Socket2, 21, 1000), + ok = ranch:stop_listener(Name), + {error, closed} = ssl:recv(Socket1, 0, 1000), + {error, closed} = ssl:recv(Socket2, 0, 1000), + %% Make sure the listener stopped. + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +ssl_local_echo(_) -> + case do_os_supports_local_sockets() of + true -> + do_ssl_local_echo(); + false -> + {skip, "No local socket support."} + end. + +do_ssl_local_echo() -> + doc("Ensure that listening on a local socket works with SSL transport."), + SockFile = do_tempname(), + try + Name = name(), + Opts = ct_helper:get_certs_from_ets(), + {ok, _} = ranch:start_listener(Name, + ranch_ssl, #{socket_opts => [{ip, {local, SockFile}}|Opts]}, + echo_protocol, []), + undefined = ranch:get_port(Name), + {ok, Socket} = ssl:connect({local, SockFile}, 0, [binary, {active, false}, {packet, raw}]), + ok = ssl:send(Socket, <<"SSL Ranch is working!">>), + {ok, <<"SSL Ranch is working!">>} = ssl:recv(Socket, 21, 1000), + ok = ranch:stop_listener(Name), + {error, closed} = ssl:recv(Socket, 0, 1000), + %% Make sure the listener stopped. + {'EXIT', _} = begin catch ranch:get_port(Name) end, + %% Make sure the socket file is removed. + {error, enoent} = file:read_file_info(SockFile), + ok + after + file:delete(SockFile) + end. + +ssl_sni_echo(_) -> + doc("Ensure that SNI works with SSL transport."), + Name = name(), + Opts = ct_helper:get_certs_from_ets(), + {ok, _} = ranch:start_listener(Name, + ranch_ssl, [{sni_hosts, [{"localhost", Opts}]}], + echo_protocol, []), + Port = ranch:get_port(Name), + %% We stick to TLS 1.2 because there seems to be a bug in OTP-23.0rc2 + %% that leads to a malformed_handshake_data error. + {ok, Socket} = ssl:connect("localhost", Port, + [binary, {active, false}, {packet, raw}, {versions, ['tlsv1.2']}]), + ok = ssl:send(Socket, <<"SSL Ranch is working!">>), + {ok, <<"SSL Ranch is working!">>} = ssl:recv(Socket, 21, 1000), + ok = ranch:stop_listener(Name), + {error, closed} = ssl:recv(Socket, 0, 1000), + %% Make sure the listener stopped. + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +ssl_sni_fail(_) -> + doc("Ensure that connection fails when host is not in SNI list."), + Name = name(), + Opts = ct_helper:get_certs_from_ets(), + {ok, _} = ranch:start_listener(Name, + ranch_ssl, [{sni_hosts, [{"pouet", Opts}]}], + echo_protocol, []), + Port = ranch:get_port(Name), + %% We stick to TLS 1.2 because there seems to be a bug in OTP-23.0rc2 + %% that leads to a malformed_handshake_data error. + {error, _} = ssl:connect("localhost", Port, + [binary, {active, false}, {packet, raw}, {versions, ['tlsv1.2']}]), + ok = ranch:stop_listener(Name), + %% Make sure the listener stopped. + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +ssl_tls_psk(_) -> + doc("Ensure that TLS-PSK works without certificate."), + Name = name(), + Ciphers = [#{cipher => aes_256_gcm, key_exchange => psk, mac => aead, prf => sha384}], + LookupFun = {fun psk_lookup_helper/3, <<"shared_secret">>}, + {ok, _} = ranch:start_listener(Name, + ranch_ssl, [{ciphers, Ciphers}, {user_lookup_fun, LookupFun}, {versions, ['tlsv1.2']}], + echo_protocol, []), + Port = ranch:get_port(Name), + {ok, Socket} = ssl:connect("localhost", Port, [ + binary, {active, false}, {ciphers, Ciphers}, + {user_lookup_fun, LookupFun}, {versions, ['tlsv1.2']} + ]), + ok = ssl:send(Socket, <<"SSL Ranch is working!">>), + {ok, <<"SSL Ranch is working!">>} = ssl:recv(Socket, 21, 1000), + ok = ranch:stop_listener(Name), + {error, closed} = ssl:recv(Socket, 0, 1000), + %% Make sure the listener stopped. + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +ssl_tls_psk_fail(_) -> + doc("Ensure that TLS-PSK filed for different shared keys."), + Name = name(), + Ciphers = [#{cipher => aes_256_gcm, key_exchange => psk, mac => aead, prf => sha384}], + ServerLookupFun = {fun psk_lookup_helper/3, <<"server_secret">>}, + ClientLookupFun = {fun psk_lookup_helper/3, <<"client_secret">>}, + {ok, _} = ranch:start_listener(Name, + ranch_ssl, [{ciphers, Ciphers}, {user_lookup_fun, ServerLookupFun}, {versions, ['tlsv1.2']}], + echo_protocol, []), + Port = ranch:get_port(Name), + {error, _} = ssl:connect("localhost", Port, [ + binary, {active, false}, {ciphers, Ciphers}, + {user_lookup_fun, ClientLookupFun}, {versions, ['tlsv1.2']} + ]), + ok = ranch:stop_listener(Name), + %% Make sure the listener stopped. + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +psk_lookup_helper(psk, _PskIdentity, UserState) -> + {ok, UserState}. + +ssl_upgrade_from_tcp(_) -> + doc("Ensure a TCP socket can be upgraded to SSL"), + Name = name(), + {ok, _} = ranch:start_listener(Name, + ranch_tcp, #{}, + ssl_upgrade_protocol, []), + Port = ranch:get_port(Name), + {ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {active, false}, {packet, raw}]), + ok = gen_tcp:send(Socket, <<"ECHO Before upgrading to SSL">>), + {ok, <<"Before upgrading to SSL">>} = gen_tcp:recv(Socket, 23, 1000), + ok = gen_tcp:send(Socket, <<"UPGRADE">>), + {ok, <<"READY">>} = gen_tcp:recv(Socket, 5, 1000), + {ok, SslSocket} = ssl:connect(Socket, [{verify, verify_none}], 5000), + ok = ssl:send(SslSocket, <<"ECHO After upgrading to SSL">>), + {ok, <<"After upgrading to SSL">>} = ssl:recv(SslSocket, 22, 1000), + ok = ranch:stop_listener(Name), + {error, closed} = ssl:recv(SslSocket, 0, 1000), + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +ssl_graceful(_) -> + doc("Ensure suspending and resuming of listeners does not kill active connections."), + Name = name(), + Opts = ct_helper:get_certs_from_ets(), + {ok, _} = ranch:start_listener(Name, + ranch_ssl, Opts, + echo_protocol, []), + Port = ranch:get_port(Name), + %% Make sure connections with a fresh listener work. + running = ranch:get_status(Name), + {ok, Socket1} = ssl:connect("localhost", Port, + [binary, {active, false}, {packet, raw}]), + ok = ssl:send(Socket1, <<"SSL with fresh listener">>), + {ok, <<"SSL with fresh listener">>} = ssl:recv(Socket1, 23, 1000), + %% Suspend listener, make sure established connections keep running. + ok = ranch:suspend_listener(Name), + suspended = ranch:get_status(Name), + ok = ssl:send(Socket1, <<"SSL with suspended listener">>), + {ok, <<"SSL with suspended listener">>} = ssl:recv(Socket1, 27, 1000), + %% Make sure new connections are refused on the suspended listener. + {error, econnrefused} = ssl:connect("localhost", Port, + [binary, {active, false}, {packet, raw}]), + %% Make sure transport options can be changed when listener is suspended. + ok = ranch:set_transport_options(Name, #{socket_opts => [{port, Port}|Opts]}), + %% Resume listener, make sure connections can be established again. + ok = ranch:resume_listener(Name), + running = ranch:get_status(Name), + {ok, Socket2} = ssl:connect("localhost", Port, + [binary, {active, false}, {packet, raw}]), + ok = ssl:send(Socket2, <<"SSL with resumed listener">>), + {ok, <<"SSL with resumed listener">>} = ssl:recv(Socket2, 25, 1000), + ok = ranch:stop_listener(Name), + {error, closed} = ssl:recv(Socket1, 0, 1000), + {error, closed} = ssl:recv(Socket2, 0, 1000), + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +ssl_getopts_capability(_) -> + doc("Ensure getopts/2 capability."), + Name=name(), + Opts=ct_helper:get_certs_from_ets(), + {ok, _} = ranch:start_listener(Name, + ranch_ssl, Opts, + transport_capabilities_protocol, []), + Port=ranch:get_port(Name), + {ok, Socket}=ssl:connect("localhost", Port, [binary, {active, false}, {packet, raw}]), + ok=ssl:send(Socket, <<"getopts/2">>), + {ok, <<"OK">>}=ssl:recv(Socket, 0, 1000), + ok=ranch:stop_listener(Name), + {error, closed}=ssl:recv(Socket, 0, 1000), + {'EXIT', _}=begin catch ranch:get_port(Name) end, + ok. + +ssl_getstat_capability(_) -> + doc("Ensure getstat/1,2 capability."), + Name=name(), + Opts=ct_helper:get_certs_from_ets(), + {ok, _} = ranch:start_listener(Name, + ranch_ssl, Opts, + transport_capabilities_protocol, []), + Port=ranch:get_port(Name), + {ok, Socket}=ssl:connect("localhost", Port, [binary, {active, false}, {packet, raw}]), + ok=ssl:send(Socket, <<"getstat/1">>), + {ok, <<"OK">>}=ssl:recv(Socket, 0, 1000), + ok=ssl:send(Socket, <<"getstat/2">>), + {ok, <<"OK">>}=ssl:recv(Socket, 0, 1000), + ok=ranch:stop_listener(Name), + {error, closed}=ssl:recv(Socket, 0, 1000), + {'EXIT', _}=begin catch ranch:get_port(Name) end, + ok. + +ssl_error_eaddrinuse(_) -> + doc("Ensure that failure due to an eaddrinuse returns a compact readable error."), + Name = name(), + Opts = ct_helper:get_certs_from_ets(), + {ok, _} = ranch:start_listener(Name, + ranch_ssl, Opts, + active_echo_protocol, []), + Port = ranch:get_port(Name), + {error, eaddrinuse} = ranch:start_listener({Name, fails}, + ranch_ssl, [{port, Port}|Opts], + active_echo_protocol, []), + ok = ranch:stop_listener(Name), + %% Make sure the listener stopped. + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +ssl_error_no_cert(_) -> + doc("Ensure that failure due to missing certificate returns a compact readable error."), + {error, no_cert} = ranch:start_listener(name(), + ranch_ssl, #{}, + active_echo_protocol, []), + ok. + +ssl_error_eacces(_) -> + case os:type() of + {win32, nt} -> + {skip, "No privileged ports."}; + {unix, darwin} -> + {skip, "No privileged ports."}; + _ -> + doc("Ensure that failure due to an eacces returns a compact readable error."), + Name = name(), + Opts = ct_helper:get_certs_from_ets(), + {error, eacces} = ranch:start_listener(Name, + ranch_ssl, [{port, 283}|Opts], + active_echo_protocol, []), + ok + end. + +ssl_unsupported_tlsv13_options(_) -> + {available, Versions} = lists:keyfind(available, 1, ssl:versions()), + case {lists:member('tlsv1.3', Versions), do_get_ssl_version() >= {10, 0, 0}} of + {true, true} -> + do_ssl_unsupported_tlsv13_options(); + {false, _} -> + {skip, "No TLSv1.3 support."}; + {_, false} -> + {skip, "No TLSv1.3 option dependency checking."} + end. + +do_ssl_unsupported_tlsv13_options() -> + doc("Ensure that a listener can be started when TLSv1.3 is " + "the only protocol and unsupported options are present."), + CheckOpts = [ + {beast_mitigation, one_n_minus_one}, + {client_renegotiation, true}, + {next_protocols_advertised, [<<"dummy">>]}, + {padding_check, true}, + {psk_identity, "dummy"}, + {secure_renegotiate, true}, + {reuse_session, fun (_, _, _, _) -> true end}, + {reuse_sessions, true}, + {user_lookup_fun, {fun (_, _, _) -> error end, <<"dummy">>}} + ], + Name = name(), + Opts = ct_helper:get_certs_from_ets() ++ [{versions, ['tlsv1.3']}], + ok = lists:foreach( + fun (CheckOpt) -> + Opts1 = Opts ++ [CheckOpt], + {error, {options, dependency, _}} = ssl:listen(0, Opts1), + {ok, _} = ranch:start_listener(Name, + ranch_ssl, #{socket_opts => Opts1}, + echo_protocol, []), + ok = ranch:stop_listener(Name) + end, + CheckOpts + ), + ok. + +%% tcp. + +tcp_10_acceptors_10_listen_sockets(Config) -> + case do_os_supports_reuseport() of + true -> + ok = do_tcp_10_acceptors_10_listen_sockets(Config); + false -> + {skip, "No SO_REUSEPORT support."} + end. + +do_tcp_10_acceptors_10_listen_sockets(Config) -> + doc("Ensure that we can use 10 listen sockets across 10 acceptors with TCP."), + Name = name(), + SockOpts = config(socket_opts, Config), + Self = self(), + Tag = make_ref(), + {ok, _} = ranch:start_listener(Name, + ranch_tcp, #{ + num_acceptors => 10, + num_listen_sockets => 10, + socket_opts => SockOpts ++ [{raw, 1, 15, <<1:32/native>>}], + post_listen_callback => fun (LSocket) -> Self ! {Tag, LSocket}, ok end}, + echo_protocol, []), + LSockets = [receive {Tag, LSocket} -> LSocket after 1000 -> error(timeout) end + || _ <- lists:seq(1, 10)], + 10 = length(LSockets), + 10 = length(lists:usort(LSockets)), + ok = ranch:stop_listener(Name), + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +tcp_many_listen_sockets_no_reuseport(Config) -> + case do_os_supports_reuseport() of + true -> + ok = do_tcp_many_listen_sockets_no_reuseport(Config); + false -> + {skip, "No SO_REUSEPORT support."} + end. + +do_tcp_many_listen_sockets_no_reuseport(Config) -> + doc("Confirm that ranch:start_listener/5 fails when SO_REUSEPORT is not available with TCP."), + Name = name(), + SockOpts = config(socket_opts, Config), + {error, eaddrinuse} = ranch:start_listener(Name, + ranch_tcp, #{ + num_acceptors => 10, + num_listen_sockets => 10, + socket_opts => SockOpts ++ [{raw, 1, 15, <<0:32/native>>}]}, + echo_protocol, []), + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +tcp_active_echo(Config) -> + doc("Ensure that active mode works with TCP transport."), + Name = name(), + SockOpts = config(socket_opts, Config), + {ok, _} = ranch:start_listener(Name, + ranch_tcp, #{socket_opts => SockOpts}, + active_echo_protocol, []), + Port = ranch:get_port(Name), + {ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {active, false}, {packet, raw}]), + ok = gen_tcp:send(Socket, <<"TCP Ranch is working!">>), + {ok, <<"TCP Ranch is working!">>} = gen_tcp:recv(Socket, 21, 1000), + ok = ranch:stop_listener(Name), + {error, closed} = gen_tcp:recv(Socket, 0, 1000), + %% Make sure the listener stopped. + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +tcp_active_n_echo(Config) -> + doc("Ensure that active N mode works with TCP transport."), + Name = name(), + SockOpts = config(socket_opts, Config), + {ok, _} = ranch:start_listener(Name, + ranch_tcp, #{socket_opts => SockOpts}, + batch_echo_protocol, [{batch_size, 3}]), + Port = ranch:get_port(Name), + {ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {active, false}, {packet, raw}]), + ok = gen_tcp:send(Socket, <<"One">>), + {ok, <<"OK">>} = gen_tcp:recv(Socket, 2, 1000), + ok = gen_tcp:send(Socket, <<"Two">>), + {ok, <<"OK">>} = gen_tcp:recv(Socket, 2, 1000), + ok = gen_tcp:send(Socket, <<"Three">>), + {ok, <<"OK">>} = gen_tcp:recv(Socket, 2, 1000), + {ok, <<"OneTwoThree">>} = gen_tcp:recv(Socket, 11, 1000), + ok = ranch:stop_listener(Name), + {error, closed} = gen_tcp:recv(Socket, 0, 1000), + %% Make sure the listener stopped. + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +tcp_echo(Config) -> + doc("Ensure that passive mode works with TCP transport."), + Name = name(), + SockOpts = config(socket_opts, Config), + {ok, _} = ranch:start_listener(Name, + ranch_tcp, #{socket_opts => SockOpts}, + echo_protocol, []), + Port = ranch:get_port(Name), + {ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {active, false}, {packet, raw}]), + ok = gen_tcp:send(Socket, <<"TCP Ranch is working!">>), + {ok, <<"TCP Ranch is working!">>} = gen_tcp:recv(Socket, 21, 1000), + ok = ranch:stop_listener(Name), + {error, closed} = gen_tcp:recv(Socket, 0, 1000), + %% Make sure the listener stopped. + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +tcp_local_echo(_) -> + case do_os_supports_local_sockets() of + true -> + do_tcp_local_echo(); + false -> + {skip, "No local socket support."} + end. + +do_tcp_local_echo() -> + doc("Ensure that listening on a local socket works with TCP transport."), + SockFile = do_tempname(), + try + Name = name(), + {ok, _} = ranch:start_listener(Name, + ranch_tcp, #{socket_opts => [{ip, {local, SockFile}}]}, + echo_protocol, []), + undefined = ranch:get_port(Name), + {ok, Socket} = gen_tcp:connect({local, SockFile}, 0, [binary, {active, false}, {packet, raw}]), + ok = gen_tcp:send(Socket, <<"TCP Ranch is working!">>), + {ok, <<"TCP Ranch is working!">>} = gen_tcp:recv(Socket, 21, 1000), + ok = ranch:stop_listener(Name), + {error, closed} = gen_tcp:recv(Socket, 0, 1000), + %% Make sure the listener stopped. + {'EXIT', _} = begin catch ranch:get_port(Name) end, + %% Make sure the socket file is removed. + {error, enoent} = file:read_file_info(SockFile), + ok + after + file:delete(SockFile) + end. + +tcp_graceful(Config) -> + doc("Ensure suspending and resuming of listeners does not kill active connections."), + Name = name(), + SockOpts = config(socket_opts, Config), + {ok, _} = ranch:start_listener(Name, + ranch_tcp, #{socket_opts => SockOpts}, + echo_protocol, []), + Port = ranch:get_port(Name), + %% Make sure connections with a fresh listener work. + running = ranch:get_status(Name), + {ok, Socket1} = gen_tcp:connect("localhost", Port, + [binary, {active, false}, {packet, raw}]), + ok = gen_tcp:send(Socket1, <<"TCP with fresh listener">>), + {ok, <<"TCP with fresh listener">>} = gen_tcp:recv(Socket1, 23, 1000), + %% Suspend listener, make sure established connections keep running. + ok = ranch:suspend_listener(Name), + suspended = ranch:get_status(Name), + ok = gen_tcp:send(Socket1, <<"TCP with suspended listener">>), + {ok, <<"TCP with suspended listener">>} = gen_tcp:recv(Socket1, 27, 1000), + %% Make sure new connections are refused on the suspended listener. + {error, econnrefused} = gen_tcp:connect("localhost", Port, + [binary, {active, false}, {packet, raw}]), + %% Make sure transport options can be changed when listener is suspended. + ok = ranch:set_transport_options(Name, [{port, Port}]), + %% Resume listener, make sure connections can be established again. + ok = ranch:resume_listener(Name), + running = ranch:get_status(Name), + {ok, Socket2} = gen_tcp:connect("localhost", Port, + [binary, {active, false}, {packet, raw}]), + ok = gen_tcp:send(Socket2, <<"TCP with resumed listener">>), + {ok, <<"TCP with resumed listener">>} = gen_tcp:recv(Socket2, 25, 1000), + ok = ranch:stop_listener(Name), + {error, closed} = gen_tcp:recv(Socket1, 0, 1000), + {error, closed} = gen_tcp:recv(Socket2, 0, 1000), + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +tcp_inherit_options(Config) -> + doc("Ensure TCP options are inherited in the protocol."), + Name = name(), + Opts0 = config(socket_opts, Config), + Opts1 = [{nodelay, false}, {send_timeout_close, false}], + {ok, _} = ranch:start_listener(Name, + ranch_tcp, #{socket_opts => Opts0 ++ Opts1}, + check_tcp_options, [{pid, self()} | Opts1]), + Port = ranch:get_port(Name), + {ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {active, true}, {packet, raw}]), + receive checked -> ok after 1000 -> error(timeout) end, + ok = gen_tcp:close(Socket), + ok = ranch:stop_listener(Name). + +tcp_max_connections(Config) -> + doc("Ensure the max_connections option actually limits connections."), + Name = name(), + SockOpts = config(socket_opts, Config), + {ok, _} = ranch:start_listener(Name, + ranch_tcp, #{max_connections => 10, num_acceptors => 1, socket_opts => SockOpts}, + notify_and_wait_protocol, #{pid => self()}), + Port = ranch:get_port(Name), + ok = connect_loop(Port, 11, 150), + 10 = ranch_server:count_connections(Name), + {10, Pids1} = receive_loop(connected, 400), + ok = terminate_loop(stop, Pids1), + {1, Pids2} = receive_loop(connected, 1000), + ok = terminate_loop(stop, Pids2), + ok = ranch:stop_listener(Name). + +tcp_max_connections_and_beyond(Config) -> + doc("Ensure the max_connections option works when connections are removed from the count."), + Name = name(), + SockOpts = config(socket_opts, Config), + {ok, _} = ranch:start_listener(Name, + ranch_tcp, #{max_connections => 10, num_acceptors => 1, socket_opts => SockOpts}, + remove_conn_and_wait_protocol, [{remove, true, 2500}]), + Port = ranch:get_port(Name), + ok = connect_loop(Port, 10, 0), + receive after 250 -> ok end, + 0 = ranch_server:count_connections(Name), + 10 = length(do_conns_which_children(Name)), + Counts = do_conns_count_children(Name), + {_, 1} = lists:keyfind(specs, 1, Counts), + {_, 0} = lists:keyfind(supervisors, 1, Counts), + {_, 10} = lists:keyfind(active, 1, Counts), + {_, 10} = lists:keyfind(workers, 1, Counts), + ranch:set_protocol_options(Name, [{remove, false, 2500}]), + receive after 250 -> ok end, + ok = connect_loop(Port, 10, 0), + receive after 250 -> ok end, + 10 = ranch_server:count_connections(Name), + 20 = length(do_conns_which_children(Name)), + Counts2 = do_conns_count_children(Name), + {_, 20} = lists:keyfind(active, 1, Counts2), + {_, 20} = lists:keyfind(workers, 1, Counts2), + ok = ranch:stop_listener(Name). + +tcp_max_connections_infinity(Config) -> + doc("Set the max_connections option from 10 to infinity and back to 10."), + Name = name(), + SockOpts = config(socket_opts, Config), + {ok, _} = ranch:start_listener(Name, + ranch_tcp, #{max_connections => 10, num_acceptors => 1, socket_opts => SockOpts}, + notify_and_wait_protocol, #{pid => self()}), + Port = ranch:get_port(Name), + ok = connect_loop(Port, 20, 0), + 10 = ranch_server:count_connections(Name), + {10, Pids1} = receive_loop(connected, 1000), + 10 = ranch_server:count_connections(Name), + 10 = ranch:get_max_connections(Name), + ranch:set_max_connections(Name, infinity), + receive after 250 -> ok end, + 20 = ranch_server:count_connections(Name), + infinity = ranch:get_max_connections(Name), + ranch:set_max_connections(Name, 10), + 20 = ranch_server:count_connections(Name), + {10, Pids2} = receive_loop(connected, 1000), + ok = terminate_loop(stop, Pids1 ++ Pids2), + ok = ranch:stop_listener(Name). + +tcp_remove_connections(Config) -> + doc("Ensure that removed connections are only removed once."), + Name = name(), + SockOpts = config(socket_opts, Config), + {ok, _} = ranch:start_listener(Name, + ranch_tcp, #{socket_opts => SockOpts}, + remove_conn_and_wait_protocol, [{remove, true, 0}]), + Port = ranch:get_port(Name), + ok = connect_loop(Port, 10, 0), + receive after 250 -> ok end, + 0 = ranch_server:count_connections(Name), + ok = ranch:stop_listener(Name). + +tcp_remove_connections_acceptor_wakeup(Config) -> + doc("Ensure that removed connections wake up acceptors."), + Name = name(), + SockOpts = config(socket_opts, Config), + {ok, _} = ranch:start_listener(Name, + ranch_tcp, #{max_connections => 1, num_acceptors => 1, socket_opts => SockOpts}, + remove_conn_and_wait_protocol, [{remove, true, infinity}]), + Port = ranch:get_port(Name), + ConnectOptions = [binary, {active, false}], + Localhost = "localhost", + {ok, Socket1} = gen_tcp:connect(Localhost, Port, ConnectOptions), + {ok, Socket2} = gen_tcp:connect(Localhost, Port, ConnectOptions), + {ok, Socket3} = gen_tcp:connect(Localhost, Port, ConnectOptions), + ok = gen_tcp:send(Socket3, <<"bye">>), + true = maps:get(all_connections, ranch:info(Name)) >= 2, + ok = gen_tcp:send(Socket1, <<"bye">>), + ok = gen_tcp:send(Socket2, <<"bye">>), + ok = ranch:stop_listener(Name). + +tcp_set_max_connections(Config) -> + doc("Ensure that changing the max_connections option to a larger value allows for more connections."), + Name = name(), + SockOpts = config(socket_opts, Config), + {ok, _} = ranch:start_listener(Name, + ranch_tcp, #{max_connections => 10, num_acceptors => 1, socket_opts => SockOpts}, + notify_and_wait_protocol, #{pid => self()}), + Port = ranch:get_port(Name), + ok = connect_loop(Port, 20, 0), + 10 = ranch_server:count_connections(Name), + {10, Pids1} = receive_loop(connected, 1000), + 10 = ranch:get_max_connections(Name), + ranch:set_max_connections(Name, 20), + {10, Pids2} = receive_loop(connected, 1000), + 20 = ranch:get_max_connections(Name), + ok = terminate_loop(stop, Pids1 ++ Pids2), + ok = ranch:stop_listener(Name). + +tcp_set_max_connections_clean(Config) -> + doc("Ensure that setting max_connections does not crash any process."), + Name = name(), + SockOpts = config(socket_opts, Config), + {ok, ListSupPid} = ranch:start_listener(Name, + ranch_tcp, #{max_connections => 4, socket_opts => SockOpts}, + notify_and_wait_protocol, #{pid => self()}), + Children = supervisor:which_children(ListSupPid), + {_, AccSupPid, _, _} = lists:keyfind(ranch_acceptors_sup, 1, Children), + 1 = erlang:trace(ListSupPid, true, [procs]), + 1 = erlang:trace(AccSupPid, true, [procs]), + Port = ranch:get_port(Name), + N = 20, + ok = connect_loop(Port, N*5, 0), + %% Randomly set max_connections. + [spawn(ranch, set_max_connections, [Name, Max]) || + Max <- lists:flatten(lists:duplicate(N, [6, 4, 8, infinity]))], + receive + {trace, _, spawn, _, _} -> + error(dirty_set_max_connections) + after + 2000 -> ok + end, + _ = erlang:trace(all, false, [all]), + ok = clean_traces(), + ok = ranch:stop_listener(Name). + +tcp_getopts_capability(Config) -> + doc("Ensure getopts/2 capability."), + Name=name(), + SockOpts = config(socket_opts, Config), + {ok, _}=ranch:start_listener(Name, + ranch_tcp, #{socket_opts => SockOpts}, + transport_capabilities_protocol, []), + Port=ranch:get_port(Name), + {ok, Socket}=gen_tcp:connect("localhost", Port, [binary, {active, false}, {packet, raw}]), + ok=gen_tcp:send(Socket, <<"getopts/2">>), + {ok, <<"OK">>}=gen_tcp:recv(Socket, 0, 1000), + ok=ranch:stop_listener(Name), + {error, closed}=gen_tcp:recv(Socket, 0, 1000), + {'EXIT', _}=begin catch ranch:get_port(Name) end, + ok. + +tcp_getstat_capability(Config) -> + doc("Ensure getstat/1,2 capability."), + Name=name(), + SockOpts = config(socket_opts, Config), + {ok, _}=ranch:start_listener(Name, + ranch_tcp, #{socket_opts => SockOpts}, + transport_capabilities_protocol, []), + Port=ranch:get_port(Name), + {ok, Socket}=gen_tcp:connect("localhost", Port, [binary, {active, false}, {packet, raw}]), + ok=gen_tcp:send(Socket, <<"getstat/1">>), + {ok, <<"OK">>}=gen_tcp:recv(Socket, 0, 1000), + ok=gen_tcp:send(Socket, <<"getstat/2">>), + {ok, <<"OK">>}=gen_tcp:recv(Socket, 0, 1000), + ok=ranch:stop_listener(Name), + {error, closed}=gen_tcp:recv(Socket, 0, 1000), + {'EXIT', _}=begin catch ranch:get_port(Name) end, + ok. + +tcp_upgrade(Config) -> + doc("Ensure that protocol options can be updated."), + Name = name(), + SockOpts = config(socket_opts, Config), + {ok, _} = ranch:start_listener(Name, + ranch_tcp, #{socket_opts => SockOpts}, + notify_and_wait_protocol, #{pid => self()}), + Port = ranch:get_port(Name), + ok = connect_loop(Port, 1, 0), + {1, Pids1} = receive_loop(connected, 1000), + ranch:set_protocol_options(Name, #{msg => upgraded, pid => self()}), + ok = connect_loop(Port, 1, 0), + {1, Pids2} = receive_loop(upgraded, 1000), + ok = terminate_loop(stop, Pids1 ++ Pids2), + ok = ranch:stop_listener(Name). + +tcp_error_eaddrinuse(Config) -> + doc("Ensure that failure due to an eaddrinuse returns a compact readable error."), + Name = name(), + SockOpts = config(socket_opts, Config), + {ok, _} = ranch:start_listener(Name, + ranch_tcp, #{socket_opts => SockOpts}, + active_echo_protocol, []), + Port = ranch:get_port(Name), + {error, eaddrinuse} = ranch:start_listener({Name, fails}, + ranch_tcp, [{port, Port}], + active_echo_protocol, []), + ok = ranch:stop_listener(Name), + %% Make sure the listener stopped. + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +tcp_error_eacces(Config) -> + case os:type() of + {win32, nt} -> + {skip, "No privileged ports."}; + {unix, darwin} -> + {skip, "No privileged ports."}; + _ -> + doc("Ensure that failure due to an eacces returns a compact readable error."), + Name = name(), + SockOpts = config(socket_opts, Config), + {error, eacces} = ranch:start_listener(Name, + ranch_tcp, #{socket_opts => SockOpts ++ [{port, 283}]}, + active_echo_protocol, []), + ok + end. + +%% Supervisor tests + +connection_type_supervisor(_) -> + doc("The supervisor connection type must be reflected in the specifications."), + Name = name(), + {ok, _} = ranch:start_listener(Name, + ranch_tcp, #{connection_type => supervisor}, + echo_protocol, []), + Port = ranch:get_port(Name), + {ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {active, false}, {packet, raw}]), + ok = gen_tcp:send(Socket, <<"TCP Ranch is working!">>), + {ok, <<"TCP Ranch is working!">>} = gen_tcp:recv(Socket, 21, 1000), + [{echo_protocol, _, supervisor, [echo_protocol]}] = do_conns_which_children(Name), + ok = ranch:stop_listener(Name), + {error, closed} = gen_tcp:recv(Socket, 0, 1000), + %% Make sure the listener stopped. + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +connection_type_supervisor_separate_from_connection(_) -> + doc("The supervisor connection type allows separate supervised and connection processes."), + Name = name(), + {ok, _} = ranch:start_listener(Name, + ranch_tcp, #{connection_type => supervisor}, + supervisor_separate, []), + Port = ranch:get_port(Name), + {ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {active, false}, {packet, raw}]), + ok = gen_tcp:send(Socket, <<"TCP Ranch is working!">>), + {ok, <<"TCP Ranch is working!">>} = gen_tcp:recv(Socket, 21, 1000), + [{supervisor_separate, _, supervisor, [supervisor_separate]}] = do_conns_which_children(Name), + ok = ranch:stop_listener(Name), + {error, closed} = gen_tcp:recv(Socket, 0, 1000), + %% Make sure the listener stopped. + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +supervisor_10_acceptors_1_conns_sup(_) -> + doc("Ensure that using 10 acceptors and 1 connection supervisor works."), + ok = do_supervisor_n_acceptors_m_conns_sups(10, 1). + +supervisor_9_acceptors_4_conns_sups(_) -> + doc("Ensure that using 9 acceptors and 4 connection supervisors works."), + ok = do_supervisor_n_acceptors_m_conns_sups(9, 4). + +supervisor_10_acceptors_10_conns_sups(_) -> + doc("Ensure that using 10 acceptors and 10 connection supervisors works."), + ok = do_supervisor_n_acceptors_m_conns_sups(10, 10). + +supervisor_1_acceptor_10_conns_sups(_) -> + doc("Ensure that using 1 acceptor and 10 connection supervisors works."), + ok = do_supervisor_n_acceptors_m_conns_sups(1, 10). + +do_supervisor_n_acceptors_m_conns_sups(NumAcceptors, NumConnsSups) -> + Name = name(), + {ok, Pid} = ranch:start_listener(Name, + ranch_tcp, #{num_conns_sups => NumConnsSups, num_acceptors => NumAcceptors}, + notify_and_wait_protocol, #{pid => self()}), + Port = ranch:get_port(Name), + ConnsSups = [ConnsSup || {_, ConnsSup} <- ranch_server:get_connections_sups(Name)], + NumConnsSups = length(ConnsSups), + {ranch_acceptors_sup, AcceptorsSup, supervisor, _} = + lists:keyfind(ranch_acceptors_sup, 1, supervisor:which_children(Pid)), + AcceptorIds = [AcceptorId || + {{acceptor, _, AcceptorId}, _, worker, _} <- supervisor:which_children(AcceptorsSup)], + NumAcceptors = length(AcceptorIds), + AcceptorConnsSups0 = [ranch_server:get_connections_sup(Name, AcceptorId) || + AcceptorId <- AcceptorIds], + AcceptorConnsSups1 = lists:usort(AcceptorConnsSups0), + if + NumAcceptors > NumConnsSups -> + NumConnsSups = length(AcceptorConnsSups1), + [] = ConnsSups -- AcceptorConnsSups1; + NumAcceptors < NumConnsSups -> + NumAcceptors = length(AcceptorConnsSups1), + [] = AcceptorConnsSups1 -- ConnsSups; + NumAcceptors =:= NumConnsSups -> + NumConnsSups = length(AcceptorConnsSups1), + NumAcceptors = length(AcceptorConnsSups1), + [] = ConnsSups -- AcceptorConnsSups1, + [] = AcceptorConnsSups1 -- ConnsSups + end, + ok = connect_loop(Port, 100, 0), + {100, Pids} = receive_loop(connected, 1000), + 100 = ranch_server:count_connections(Name), + ok = terminate_loop(stop, Pids), + ok = ranch:stop_listener(Name), + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +supervisor_changed_options_restart(_) -> + doc("Ensure that a listener is restarted with changed transport options."), + Name = name(), + %% Start a listener using send_timeout as option change marker. + {ok, ListenerSupPid1} = ranch:start_listener(Name, + ranch_tcp, [{send_timeout, 300000}], + echo_protocol, []), + %% Ensure send_timeout is really set to initial value. + {ok, [{send_timeout, 300000}]} + = inet:getopts(do_get_listener_socket(ListenerSupPid1), [send_timeout]), + %% Change send_timeout option. + ok = ranch:suspend_listener(Name), + ok = ranch:set_transport_options(Name, [{send_timeout, 300001}]), + ok = ranch:resume_listener(Name), + %% Ensure send_timeout is really set to the changed value. + {ok, [{send_timeout, 300001}]} + = inet:getopts(do_get_listener_socket(ListenerSupPid1), [send_timeout]), + %% Crash the listener_sup process, allow a short time for restart to succeed. + %% We silence the expected log events coming from the relevant supervisors. + ListenerChilds = [ChildPid || {_, ChildPid, _, _} <- supervisor:which_children(ListenerSupPid1)], + FilterFun = fun (#{meta := #{pid := EventPid}}, _) -> + case lists:member(EventPid, ListenerChilds) of + true -> stop; + false -> ignore + end + end, + ok = logger:add_primary_filter(?MODULE, {FilterFun, undefined}), + try + exit(ListenerSupPid1, kill), + timer:sleep(1000) + after + ok = logger:remove_primary_filter(?MODULE) + end, + %% Obtain pid of restarted listener_sup process. + [ListenerSupPid2] = [Pid || {{ranch_listener_sup, Ref}, Pid, supervisor, _} + <- supervisor:which_children(ranch_sup), Ref =:= Name], + %% Ensure send_timeout is still set to the changed value. + {ok, [{send_timeout, 300001}]} + = inet:getopts(do_get_listener_socket(ListenerSupPid2), [send_timeout]), + ok = ranch:stop_listener(Name), + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +supervisor_clean_child_restart(_) -> + doc("Verify that only the relevant parts of the supervision tree restarted " + "when the listening socket is closed."), + Name = name(), + %% Trace socket allocations. + {module, ranch_tcp} = code:ensure_loaded(ranch_tcp), + _ = erlang:trace(new, true, [call]), + 1 = erlang:trace_pattern({ranch_tcp, listen, 1}, + [{'_', [], [{return_trace}]}], [global]), + {ok, Pid} = ranch:start_listener(Name, + ranch_tcp, #{num_acceptors => 1}, + echo_protocol, []), + %% Trace supervisor spawns. + 1 = erlang:trace(Pid, true, [procs, set_on_spawn]), + ConnsSups = ranch_server:get_connections_sups(Name), + %% Manually shut the listening socket down. + LSocket = receive + {trace, _, return_from, {ranch_tcp, listen, 1}, {ok, Socket}} -> + Socket + after 0 -> + error(lsocket_unknown) + end, + ok = gen_tcp:close(LSocket), + receive after 1000 -> ok end, + %% Verify that supervisor and its first two children are alive. + true = is_process_alive(Pid), + true = lists:all(fun erlang:is_process_alive/1, [ConnsSup || {_, ConnsSup} <- ConnsSups]), + %% Check that acceptors_sup is restarted properly. + AccSupPid = receive {trace, Pid, spawn, Pid1, _} -> Pid1 end, + receive {trace, AccSupPid, spawn, _, _} -> ok end, + %% No more traces then. + receive + {trace, _, spawn, _, _} -> error(invalid_restart) + after 1000 -> ok end, + %% Verify that children still registered right. + ConnsSups = ranch_server:get_connections_sups(Name), + _ = erlang:trace_pattern({ranch_tcp, listen, 1}, false, []), + _ = erlang:trace(all, false, [all]), + ok = clean_traces(), + ok = ranch:stop_listener(Name). + +supervisor_clean_restart(_) -> + doc("Verify that killing ranch_conns_sup does not crash everything " + "and that it restarts properly."), + Name = name(), + NumAcc = 4, + {ok, Pid} = ranch:start_listener(Name, + ranch_tcp, #{num_acceptors => NumAcc}, + echo_protocol, []), + %% Trace supervisor spawns. + 1 = erlang:trace(Pid, true, [procs, set_on_spawn]), + {_, ConnsSupSup0, _, _} = lists:keyfind(ranch_conns_sup_sup, 1, supervisor:which_children(Pid)), + exit(ConnsSupSup0, kill), + receive after 1000 -> ok end, + %% Verify that supervisor is alive + true = is_process_alive(Pid), + %% ...but children are dead. + false = is_process_alive(ConnsSupSup0), + %% Receive traces from newly started children + ConnsSupSup = receive {trace, Pid, spawn, Pid2, _} -> Pid2 end, + [receive {trace, ConnsSupSup, spawn, _Pid, _} -> ok end || + _ <- lists:seq(1, NumAcc)], + AccSupPid = receive {trace, Pid, spawn, Pid3, _} -> Pid3 end, + %% ...and its acceptors. + [receive {trace, AccSupPid, spawn, _Pid, _} -> ok end || + _ <- lists:seq(1, NumAcc)], + %% No more traces then. + receive + {trace, EPid, spawn, _, _} when EPid == Pid; EPid == AccSupPid -> + error(invalid_restart) + after 1000 -> ok end, + %% Verify that new children registered themselves properly. + _ = erlang:trace(all, false, [all]), + ok = clean_traces(), + ok = ranch:stop_listener(Name). + +supervisor_conns_alive(_) -> + doc("Ensure that active connections stay open when the listening socket gets closed."), + Name = name(), + {module, ranch_tcp} = code:ensure_loaded(ranch_tcp), + _ = erlang:trace(new, true, [call]), + 1 = erlang:trace_pattern({ranch_tcp, listen, 1}, + [{'_', [], [{return_trace}]}], [global]), + {ok, _} = ranch:start_listener(Name, + ranch_tcp, #{}, + remove_conn_and_wait_protocol, [{remove, false, 2500}]), + %% Get the listener socket + LSocket = receive + {trace, _, return_from, {ranch_tcp, listen, 1}, {ok, S}} -> + S + after 500 -> + error(lsocket_unknown) + end, + TcpPort = ranch:get_port(Name), + {ok, Socket} = gen_tcp:connect("localhost", TcpPort, + [binary, {active, true}, {packet, raw}]), + receive after 500 -> ok end, + %% Shut the socket down + ok = gen_tcp:close(LSocket), + %% Assert that client is still viable. + receive {tcp_closed, _} -> error(closed) after 1500 -> ok end, + ok = gen_tcp:send(Socket, <<"poke">>), + receive {tcp_closed, _} -> ok end, + _ = erlang:trace(all, false, [all]), + ok = clean_traces(), + ok = ranch:stop_listener(Name). + +supervisor_embedded_ranch_server_crash(_) -> + doc("Ensure that restarting ranch_server also restarts embedded listeners."), + Name = name(), + {ok, SupPid} = embedded_sup:start_link(), + {ok, EmbeddedSupPid} = embedded_sup:start_listener(SupPid, Name, + ranch_tcp, #{}, + echo_protocol, []), + [{{ranch_listener_sup, Name}, ListenerPid, supervisor, _}, + {ranch_server_proxy, ProxyPid, worker, _}] = supervisor:which_children(EmbeddedSupPid), + ProxyMonitor = monitor(process, ProxyPid), + ListenerMonitor = monitor(process, ListenerPid), + ok = supervisor:terminate_child(ranch_sup, ranch_server), + receive {'DOWN', ProxyMonitor, process, ProxyPid, shutdown} -> ok after 1000 -> exit(timeout) end, + receive {'DOWN', ListenerMonitor, process, ListenerPid, shutdown} -> ok after 1000 -> exit(timeout) end, + {ok, _} = supervisor:restart_child(ranch_sup, ranch_server), + receive after 1000 -> ok end, + [{{ranch_listener_sup, Name}, _, supervisor, _}, + {ranch_server_proxy, _, worker, _}] = supervisor:which_children(EmbeddedSupPid), + embedded_sup:stop_listener(SupPid, Name), + embedded_sup:stop(SupPid), + ok. + +supervisor_protocol_start_link_crash(_) -> + doc("Ensure a protocol start crash does not kill all connections."), + Name = name(), + {ok, _} = ranch:start_listener(Name, + ranch_tcp, #{}, + crash_protocol, []), + ConnsSups = ranch_server:get_connections_sups(Name), + Port = ranch:get_port(Name), + {ok, _} = gen_tcp:connect("localhost", Port, [binary, {active, true}, {packet, raw}]), + receive after 500 -> ok end, + ConnsSups = ranch_server:get_connections_sups(Name), + ok = ranch:stop_listener(Name). + +supervisor_server_recover_state(_) -> + doc("Ensure that when ranch_server crashes and restarts, it recovers " + "its state and continues monitoring the same processes."), + Name = name(), + _ = erlang:trace(new, true, [call]), + 1 = erlang:trace_pattern({ranch_server, init, 1}, + [{'_', [], [{return_trace}]}], [global]), + {ok, _} = ranch:start_listener(Name, + ranch_tcp, #{}, + echo_protocol, []), + ConnsSups = ranch_server:get_connections_sups(Name), + ServerPid = erlang:whereis(ranch_server), + {monitors, Monitors} = erlang:process_info(ServerPid, monitors), + erlang:exit(ServerPid, kill), + receive + {trace, ServerPid2, return_from, {ranch_server, init, 1}, _Result} -> + {monitors, Monitors2} = erlang:process_info(ServerPid2, monitors), + %% Check that ranch_server is monitoring the same processes. + true = (lists:usort(Monitors) == lists:usort(Monitors2)) + after + 1000 -> + error(timeout) + end, + ConnsSups = ranch_server:get_connections_sups(Name), + ok = ranch:stop_listener(Name), + %% Check ranch_server has removed the ranch_conns_sup. + [] = (catch ranch_server:get_connections_sups(Name)), + _ = erlang:trace(all, false, [all]), + ok = clean_traces(). + +supervisor_unexpected_message(_) -> + doc("Ensure the connections supervisor stays alive when it receives " + "an unexpected message."), + Name = name(), + {ok, _} = ranch:start_listener(Name, + ranch_tcp, #{}, + echo_protocol, []), + Port = ranch:get_port(Name), + {ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {active, false}, {packet, raw}]), + ok = gen_tcp:send(Socket, <<"TCP Ranch is working!">>), + {ok, <<"TCP Ranch is working!">>} = gen_tcp:recv(Socket, 21, 1000), + %% Send the unexpected message to all ranch_conns_sups. + _ = [ConnSup ! hello || {_, ConnSup} <- ranch_server:get_connections_sups(Name)], + %% Connection is still up. + ok = gen_tcp:send(Socket, <<"TCP Ranch is working!">>), + {ok, <<"TCP Ranch is working!">>} = gen_tcp:recv(Socket, 21, 1000), + ok = ranch:stop_listener(Name), + {error, closed} = gen_tcp:recv(Socket, 0, 1000), + %% Make sure the listener stopped. + {'EXIT', _} = begin catch ranch:get_port(Name) end, + ok. + +%% Utility functions. + +connect_loop(_, 0, _) -> + ok; +connect_loop(Port, N, Sleep) -> + {ok, _} = gen_tcp:connect("localhost", Port, + [binary, {active, false}, {packet, raw}]), + receive after Sleep -> ok end, + connect_loop(Port, N - 1, Sleep). + +receive_loop(Message, Timeout) -> + receive_loop(Message, Timeout, 0, []). +receive_loop(Message, Timeout, N, Acc) -> + receive {Pid, Message} -> + receive_loop(Message, Timeout, N + 1, [Pid|Acc]) + after Timeout -> + {N, Acc} + end. + +terminate_loop(_, []) -> + ok; +terminate_loop(Message, [Pid|Pids]) -> + Pid ! Message, + terminate_loop(Message, Pids). + +clean_traces() -> + receive + {trace, _, _, _} -> + clean_traces(); + {trace, _, _, _, _} -> + clean_traces() + after 0 -> + ok + end. + +do_get_listener_socket(ListenerSupPid) -> + [LSocket] = do_get_listener_sockets(ListenerSupPid), + LSocket. + +do_get_listener_sockets(ListenerSupPid) -> + [AcceptorsSupPid] = [Pid || {ranch_acceptors_sup, Pid, supervisor, _} + <- supervisor:which_children(ListenerSupPid)], + {links, Links} = erlang:process_info(AcceptorsSupPid, links), + [P || P <- Links, is_port(P)]. + +do_conns_which_children(Name) -> + Conns = [supervisor:which_children(ConnsSup) || + {_, ConnsSup} <- ranch_server:get_connections_sups(Name)], + lists:flatten(Conns). + +do_conns_count_children(Name) -> + lists:foldl( + fun + (Stats, undefined) -> + Stats; + (Stats, Acc) -> + lists:zipwith( + fun ({K, V1}, {K, V2}) -> {K, V1+V2} end, + Acc, + Stats + ) + end, + undefined, + [supervisor:count_children(ConnsSup) || + {_, ConnsSup} <- ranch_server:get_connections_sups(Name)] + ). + +do_os_supports_reuseport() -> + case {os:type(), os:version()} of + {{unix, linux}, {Major, _, _}} when Major > 3 -> true; + {{unix, linux}, {3, Minor, _}} when Minor >= 9 -> true; + _ -> false + end. + +do_os_supports_local_sockets() -> + case os:type() of + {unix, _} -> true; + _ -> false + end. + +do_tempname() -> + list_to_binary(lists:droplast(os:cmd("mktemp -u"))). + +do_get_ssl_version() -> + {ok, Vsn} = application:get_key(ssl, vsn), + Vsns0 = re:split(Vsn, "\\D+", [{return, list}]), + Vsns1 = lists:map(fun list_to_integer/1, Vsns0), + case Vsns1 of + [] -> {0, 0, 0}; + [Major] -> {Major, 0, 0}; + [Major, Minor] -> {Major, Minor, 0}; + [Major, Minor, Patch|_] -> {Major, Minor, Patch} + end. diff --git a/ranch/test/active_echo_protocol.erl b/ranch/test/active_echo_protocol.erl new file mode 100644 index 0000000..6ae47a6 --- /dev/null +++ b/ranch/test/active_echo_protocol.erl @@ -0,0 +1,26 @@ +-module(active_echo_protocol). +-behaviour(ranch_protocol). + +-export([start_link/3]). +-export([init/3]). + +start_link(Ref, Transport, Opts) -> + Pid = spawn_link(?MODULE, init, [Ref, Transport, Opts]), + {ok, Pid}. + +init(Ref, Transport, _Opts = []) -> + {ok, Socket} = ranch:handshake(Ref), + loop(Socket, Transport). + +loop(Socket, Transport) -> + {OK, Closed, Error, _Passive} = Transport:messages(), + Transport:setopts(Socket, [{active, once}]), + receive + {OK, Socket, Data} -> + Transport:send(Socket, Data), + loop(Socket, Transport); + {Closed, Socket} -> + ok; + {Error, Socket, _} -> + ok = Transport:close(Socket) + end. diff --git a/ranch/test/batch_echo_protocol.erl b/ranch/test/batch_echo_protocol.erl new file mode 100644 index 0000000..b48fcbe --- /dev/null +++ b/ranch/test/batch_echo_protocol.erl @@ -0,0 +1,30 @@ +-module(batch_echo_protocol). +-behaviour(ranch_protocol). + +-export([start_link/3]). +-export([init/3]). + +start_link(Ref, Transport, [{batch_size, N}]) -> + Pid = spawn_link(?MODULE, init, [Ref, Transport, N]), + {ok, Pid}. + +init(Ref, Transport, N) -> + {ok, Socket} = ranch:handshake(Ref), + Transport:setopts(Socket, [{active, N}]), + loop(Socket, Transport, N, <<>>). + +loop(Socket, Transport, N, Acc) -> + {OK, Closed, Error, Passive} = Transport:messages(), + receive + {OK, Socket, Data} -> + Transport:send(Socket, <<"OK">>), + loop(Socket, Transport, N, <>); + {Passive, Socket} -> + Transport:send(Socket, Acc), + Transport:setopts(Socket, [{active, N}]), + loop(Socket, Transport, N, <<>>); + {Closed, Socket} -> + ok; + {Error, Socket, _} -> + ok = Transport:close(Socket) + end. diff --git a/ranch/test/check_tcp_options.erl b/ranch/test/check_tcp_options.erl new file mode 100644 index 0000000..ecafef7 --- /dev/null +++ b/ranch/test/check_tcp_options.erl @@ -0,0 +1,16 @@ +-module(check_tcp_options). +-behaviour(ranch_protocol). + +-export([start_link/3]). +-export([init/3]). + +start_link(Ref, _, [{pid, TestPid}|TcpOptions]) -> + Pid = spawn_link(?MODULE, init, [Ref, TestPid, TcpOptions]), + {ok, Pid}. + +init(Ref, Pid, TcpOptions) -> + {ok, Socket} = ranch:handshake(Ref), + {ok, RealTcpOptions} = inet:getopts(Socket, [Key || {Key, _} <- TcpOptions]), + true = TcpOptions =:= RealTcpOptions, + Pid ! checked, + receive after 2500 -> ok end. diff --git a/ranch/test/cover.spec b/ranch/test/cover.spec new file mode 100644 index 0000000..13c52ba --- /dev/null +++ b/ranch/test/cover.spec @@ -0,0 +1 @@ +{incl_app, ranch, details}. diff --git a/ranch/test/crash_protocol.erl b/ranch/test/crash_protocol.erl new file mode 100644 index 0000000..e584f4d --- /dev/null +++ b/ranch/test/crash_protocol.erl @@ -0,0 +1,7 @@ +-module(crash_protocol). + +-export([start_link/4]). + +-spec start_link(_, _, _, _) -> no_return(). +start_link(_, _, _, _) -> + exit(crash). diff --git a/ranch/test/echo_protocol.erl b/ranch/test/echo_protocol.erl new file mode 100644 index 0000000..7bcf15a --- /dev/null +++ b/ranch/test/echo_protocol.erl @@ -0,0 +1,22 @@ +-module(echo_protocol). +-behaviour(ranch_protocol). + +-export([start_link/3]). +-export([init/3]). + +start_link(Ref, Transport, Opts) -> + Pid = spawn_link(?MODULE, init, [Ref, Transport, Opts]), + {ok, Pid}. + +init(Ref, Transport, _Opts = []) -> + {ok, Socket} = ranch:handshake(Ref), + loop(Socket, Transport). + +loop(Socket, Transport) -> + case Transport:recv(Socket, 0, 5000) of + {ok, Data} -> + Transport:send(Socket, Data), + loop(Socket, Transport); + _ -> + ok = Transport:close(Socket) + end. diff --git a/ranch/test/embedded_sup.erl b/ranch/test/embedded_sup.erl new file mode 100644 index 0000000..49f2d0c --- /dev/null +++ b/ranch/test/embedded_sup.erl @@ -0,0 +1,28 @@ +-module(embedded_sup). +-behaviour(supervisor). +-export([init/1]). + +-export([start_link/0]). +-export([stop/1]). +-export([start_listener/6]). +-export([stop_listener/2]). + +start_link() -> + supervisor:start_link(?MODULE, []). + +stop(SupPid) -> + erlang:exit(SupPid, normal). + +init([]) -> + {ok, {{one_for_one, 10, 10}, []}}. + +start_listener(SupPid, Ref, Transport, TransOpts, Protocol, ProtoOpts) -> + supervisor:start_child( + SupPid, + ranch:child_spec(Ref, Transport, TransOpts, Protocol, ProtoOpts) + ). + +stop_listener(SupPid, Ref) -> + ok = supervisor:terminate_child(SupPid, {ranch_embedded_sup, Ref}), + ok = supervisor:delete_child(SupPid, {ranch_embedded_sup, Ref}), + ranch_server:cleanup_listener_opts(Ref). diff --git a/ranch/test/handshake_protocol.erl b/ranch/test/handshake_protocol.erl new file mode 100644 index 0000000..cedbe2d --- /dev/null +++ b/ranch/test/handshake_protocol.erl @@ -0,0 +1,32 @@ +-module(handshake_protocol). +-behaviour(ranch_protocol). + +-export([start_link/3]). +-export([init/3]). + +start_link(Ref, Transport, Opts) -> + Pid = spawn_link(?MODULE, init, [Ref, Transport, Opts]), + {ok, Pid}. + +init(Ref, Transport, Opts) -> + SniHost = case ranch:handshake(Ref) of + %% Due to a bug in ssl (https://bugs.erlang.org/browse/ERL-951, + %% fixed in OTP 22.0.3) the value for sni may be {sni, Hostname} + %% instead of Hostname. + {continue, #{sni := {sni, Hostname}}} -> + Hostname; + {continue, #{sni := Hostname}} -> + Hostname + end, + SniHostOpts = maps:get(SniHost, Opts), + {ok, Socket} = ranch:handshake_continue(Ref, SniHostOpts), + loop(Socket, Transport). + +loop(Socket, Transport) -> + case Transport:recv(Socket, 0, 5000) of + {ok, Data} -> + Transport:send(Socket, Data), + loop(Socket, Transport); + _ -> + ok = Transport:close(Socket) + end. diff --git a/ranch/test/notify_and_wait_protocol.erl b/ranch/test/notify_and_wait_protocol.erl new file mode 100644 index 0000000..26f6907 --- /dev/null +++ b/ranch/test/notify_and_wait_protocol.erl @@ -0,0 +1,16 @@ +-module(notify_and_wait_protocol). +-behaviour(ranch_protocol). + +-export([start_link/3]). +-export([init/4]). + +start_link(_, _, Opts = #{pid := TestPid}) -> + Msg = maps:get(msg, Opts, connected), + TerminateMsg = maps:get(terminate_msg, Opts, stop), + Timeout = maps:get(timeout, Opts, infinity), + Pid = spawn_link(?MODULE, init, [Msg, TestPid, TerminateMsg, Timeout]), + {ok, Pid}. + +init(Msg, Pid, TerminateMsg, Timeout) -> + Pid ! {self(), Msg}, + receive TerminateMsg -> ok after Timeout -> ok end. diff --git a/ranch/test/proxy_header_SUITE.erl b/ranch/test/proxy_header_SUITE.erl new file mode 100644 index 0000000..7599abd --- /dev/null +++ b/ranch/test/proxy_header_SUITE.erl @@ -0,0 +1,232 @@ +%% Copyright (c) 2018-2021, 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(proxy_header_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-import(ct_helper, [doc/1]). +-import(ct_helper, [name/0]). + +%% ct. + +all() -> + ct_helper:all(?MODULE). + +%% Tests. + +recv_v1_proxy_header_tcp(_) -> + doc("Confirm we can read the proxy header at the start of the connection."), + Name = name(), + ProxyInfo = #{ + version => 1, + command => proxy, + transport_family => ipv4, + transport_protocol => stream, + src_address => {127, 0, 0, 1}, + src_port => 444, + dest_address => {192, 168, 0, 1}, + dest_port => 443 + }, + do_proxy_header_tcp(Name, ProxyInfo, <<>>, <<"TCP Ranch is working!">>). + +recv_v1_proxy_header_tcp_extra_data(_) -> + doc("Confirm we can read the proxy header at the start of the connection " + "and that the extra data in the first packet can be read afterwards."), + Name = name(), + ProxyInfo = #{ + version => 1, + command => proxy, + transport_family => ipv4, + transport_protocol => stream, + src_address => {127, 0, 0, 1}, + src_port => 444, + dest_address => {192, 168, 0, 1}, + dest_port => 443 + }, + do_proxy_header_tcp(Name, ProxyInfo, <<"HELLO">>, <<"TCP Ranch is working!">>). + +recv_v2_proxy_header_tcp(_) -> + doc("Confirm we can read the proxy header at the start of the connection."), + Name = name(), + ProxyInfo = #{ + version => 2, + command => proxy, + transport_family => ipv4, + transport_protocol => stream, + src_address => {127, 0, 0, 1}, + src_port => 444, + dest_address => {192, 168, 0, 1}, + dest_port => 443 + }, + do_proxy_header_tcp(Name, ProxyInfo, <<>>, <<"TCP Ranch is working!">>). + +recv_v2_proxy_header_tcp_extra_data(_) -> + doc("Confirm we can read the proxy header at the start of the connection " + "and that the extra data in the first packet can be read afterwards."), + Name = name(), + ProxyInfo = #{ + version => 2, + command => proxy, + transport_family => ipv4, + transport_protocol => stream, + src_address => {127, 0, 0, 1}, + src_port => 444, + dest_address => {192, 168, 0, 1}, + dest_port => 443 + }, + do_proxy_header_tcp(Name, ProxyInfo, <<"HELLO">>, <<"TCP Ranch is working!">>). + +recv_v2_local_header_tcp(_) -> + doc("Confirm we can read the proxy header at the start of the connection."), + Name = name(), + ProxyInfo = #{ + version => 2, + command => local + }, + do_proxy_header_tcp(Name, ProxyInfo, <<>>, <<"TCP Ranch is working!">>). + +recv_v2_local_header_tcp_extra_data(_) -> + doc("Confirm we can read the proxy header at the start of the connection " + "and that the extra data in the first packet can be read afterwards."), + Name = name(), + ProxyInfo = #{ + version => 2, + command => local + }, + do_proxy_header_tcp(Name, ProxyInfo, <<"HELLO">>, <<"TCP Ranch is working!">>). + +do_proxy_header_tcp(Name, ProxyInfo, Data1, Data2) -> + {ok, _} = ranch:start_listener(Name, + ranch_tcp, #{}, + proxy_protocol, []), + Port = ranch:get_port(Name), + {ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {active, false}, {packet, raw}]), + ok = gen_tcp:send(Socket, [ranch_proxy_header:header(ProxyInfo), Data1]), + receive + {proxy_protocol, ProxyInfo} -> + ok + after 2000 -> + error(timeout) + end, + ok = gen_tcp:send(Socket, Data2), + Len1 = byte_size(Data1), + Len2 = byte_size(Data2), + {ok, <>} = gen_tcp:recv(Socket, Len1 + Len2, 1000), + ok = ranch:stop_listener(Name), + ok. + +recv_v1_proxy_header_ssl(_) -> + doc("Confirm we can read the proxy header at the start of the connection."), + Name = name(), + ProxyInfo = #{ + version => 1, + command => proxy, + transport_family => ipv4, + transport_protocol => stream, + src_address => {127, 0, 0, 1}, + src_port => 444, + dest_address => {192, 168, 0, 1}, + dest_port => 443 + }, + do_proxy_header_ssl(Name, ProxyInfo, <<>>, <<"TCP Ranch is working!">>). + +recv_v1_proxy_header_ssl_extra_data(_) -> + doc("Confirm we can read the proxy header at the start of the connection " + "and that the extra data in the first packet can be read afterwards."), + Name = name(), + ProxyInfo = #{ + version => 1, + command => proxy, + transport_family => ipv4, + transport_protocol => stream, + src_address => {127, 0, 0, 1}, + src_port => 444, + dest_address => {192, 168, 0, 1}, + dest_port => 443 + }, + do_proxy_header_ssl(Name, ProxyInfo, <<"HELLO">>, <<"TCP Ranch is working!">>). + +recv_v2_proxy_header_ssl(_) -> + doc("Confirm we can read the proxy header at the start of the connection."), + Name = name(), + ProxyInfo = #{ + version => 2, + command => proxy, + transport_family => ipv4, + transport_protocol => stream, + src_address => {127, 0, 0, 1}, + src_port => 444, + dest_address => {192, 168, 0, 1}, + dest_port => 443 + }, + do_proxy_header_ssl(Name, ProxyInfo, <<>>, <<"TCP Ranch is working!">>). + +recv_v2_proxy_header_ssl_extra_data(_) -> + doc("Confirm we can read the proxy header at the start of the connection " + "and that the extra data in the first packet can be read afterwards."), + Name = name(), + ProxyInfo = #{ + version => 2, + command => proxy, + transport_family => ipv4, + transport_protocol => stream, + src_address => {127, 0, 0, 1}, + src_port => 444, + dest_address => {192, 168, 0, 1}, + dest_port => 443 + }, + do_proxy_header_ssl(Name, ProxyInfo, <<"HELLO">>, <<"TCP Ranch is working!">>). + +recv_v2_local_header_ssl(_) -> + doc("Confirm we can read the proxy header at the start of the connection."), + Name = name(), + ProxyInfo = #{ + version => 2, + command => local + }, + do_proxy_header_ssl(Name, ProxyInfo, <<>>, <<"TCP Ranch is working!">>). + +recv_v2_local_header_ssl_extra_data(_) -> + doc("Confirm we can read the proxy header at the start of the connection " + "and that the extra data in the first packet can be read afterwards."), + Name = name(), + ProxyInfo = #{ + version => 2, + command => local + }, + do_proxy_header_ssl(Name, ProxyInfo, <<"HELLO">>, <<"TCP Ranch is working!">>). + +do_proxy_header_ssl(Name, ProxyInfo, Data1, Data2) -> + Opts = ct_helper:get_certs_from_ets(), + {ok, _} = ranch:start_listener(Name, + ranch_ssl, Opts, + proxy_protocol, []), + Port = ranch:get_port(Name), + {ok, Socket0} = gen_tcp:connect("localhost", Port, [binary, {active, false}, {packet, raw}]), + ok = gen_tcp:send(Socket0, [ranch_proxy_header:header(ProxyInfo)]), + {ok, Socket} = ssl:connect(Socket0, [], 1000), + ok = ssl:send(Socket, Data1), + receive + {proxy_protocol, ProxyInfo} -> + ok + after 2000 -> + error(timeout) + end, + ok = ssl:send(Socket, Data2), + Len1 = byte_size(Data1), + Len2 = byte_size(Data2), + {ok, <>} = ssl:recv(Socket, Len1 + Len2, 1000), + ok = ranch:stop_listener(Name), + ok. diff --git a/ranch/test/proxy_protocol.erl b/ranch/test/proxy_protocol.erl new file mode 100644 index 0000000..88eb9ff --- /dev/null +++ b/ranch/test/proxy_protocol.erl @@ -0,0 +1,28 @@ +-module(proxy_protocol). +-behaviour(ranch_protocol). + +-export([start_link/3]). +-export([init/3]). + +start_link(Ref, Transport, Opts) -> + Pid = spawn_link(?MODULE, init, [Ref, Transport, Opts]), + {ok, Pid}. + +init(Ref, Transport, _Opts = []) -> + {ok, ProxyInfo} = ranch:recv_proxy_header(Ref, 1000), + {ok, Socket} = ranch:handshake(Ref), + Pid = case Transport of + ranch_tcp -> ct_helper:get_remote_pid_tcp(Socket); + ranch_ssl -> ct_helper:get_remote_pid_tls(Socket) + end, + Pid ! {?MODULE, ProxyInfo}, + loop(Socket, Transport). + +loop(Socket, Transport) -> + case Transport:recv(Socket, 0, 5000) of + {ok, Data} -> + _ = Transport:send(Socket, Data), + loop(Socket, Transport); + _ -> + ok = Transport:close(Socket) + end. diff --git a/ranch/test/ranch_concuerror.erl b/ranch/test/ranch_concuerror.erl new file mode 100644 index 0000000..0834c6f --- /dev/null +++ b/ranch/test/ranch_concuerror.erl @@ -0,0 +1,83 @@ +%% Copyright (c) 2020-2021, 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(ranch_concuerror). +-compile(export_all). +-compile(nowarn_export_all). + +-concuerror_options([ + {after_timeout, 5000}, + {treat_as_normal, [ + killed, %% Acceptors are killed on shutdown. + shutdown %% This is a normal exit reason in OTP. + ]} +]). + +%% Convenience functions. + +do_start() -> + {ok, SupPid} = ranch_app:start(normal, []), + SupPid. + +-spec do_stop(pid()) -> no_return(). +do_stop(SupPid) -> + exit(SupPid, shutdown), + %% We make sure that SupPid terminated before the test ends, + %% because otherwise the shutdown will not be ordered and + %% can produce error exit reasons. + receive after infinity -> ok end. + +%% Tests. + +-spec start_stop() -> no_return(). +start_stop() -> + %% Start a listener then stop it. + SupPid = do_start(), + {ok, _} = ranch:start_listener(?FUNCTION_NAME, + ranch_erlang_transport, #{ + num_acceptors => 1 + }, + echo_protocol, []), + ok = ranch:stop_listener(?FUNCTION_NAME), + do_stop(SupPid). + +%% @todo This takes a huge amount of time. +%start_stop_twice() -> +% %% Start a listener then stop it. Then start and stop it again. +% SupPid = do_start(), +% {ok, _} = ranch:start_listener(?FUNCTION_NAME, +% ranch_erlang_transport, #{ +% num_acceptors => 1 +% }, +% echo_protocol, []), +% ok = ranch:stop_listener(?FUNCTION_NAME), +% {ok, _} = ranch:start_listener(?FUNCTION_NAME, +% ranch_erlang_transport, #{ +% num_acceptors => 1 +% }, +% echo_protocol, []), +% ok = ranch:stop_listener(?FUNCTION_NAME), +% do_stop(SupPid). + +-spec info() -> no_return(). +info() -> + %% Ensure we can call ranch:info/1 after starting a listener. + SupPid = do_start(), + {ok, _} = ranch:start_listener(?FUNCTION_NAME, + ranch_erlang_transport, #{ + num_acceptors => 1 + }, + echo_protocol, []), + #{} = ranch:info(?FUNCTION_NAME), + do_stop(SupPid). diff --git a/ranch/test/ranch_ct_hook.erl b/ranch/test/ranch_ct_hook.erl new file mode 100644 index 0000000..8dc0f9b --- /dev/null +++ b/ranch/test/ranch_ct_hook.erl @@ -0,0 +1,43 @@ +%% Copyright (c) 2015-2021, 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(ranch_ct_hook). + +-export([init/2]). + +init(_, _) -> + %% Allow a more relaxed restart intensity because + %% some tests will cause quick restarts of several + %% ranch_sup children. + application:set_env(ranch, ranch_sup_intensity, 10), + application:set_env(ranch, ranch_sup_period, 1), + ok = application:load(ssl), + case {os:type(), application:get_key(ssl, vsn)} of + %% Internal active,N is broken on Windows since + %% OTP 21.2/ssl 9.1. + %% @todo Put an upper limit on the version when + %% this is fixed in a future OTP version. + {_, {ok, "9.0"++_}} -> + ok; + {{win32, nt}, {ok, "9."++_}} -> + application:set_env(ssl, internal_active_n, 1); + {{win32, nt}, {ok, "10."++_}} -> + application:set_env(ssl, internal_active_n, 1); + _ -> + ok + end, + ct_helper:start([ranch]), + ct_helper:make_certs_in_ets(), + error_logger:add_report_handler(ct_helper_error_h), + {ok, undefined}. diff --git a/ranch/test/ranch_erlang_transport.erl b/ranch/test/ranch_erlang_transport.erl new file mode 100644 index 0000000..4bb999c --- /dev/null +++ b/ranch/test/ranch_erlang_transport.erl @@ -0,0 +1,174 @@ +%% Copyright (c) 2020-2021, 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(ranch_erlang_transport). +-behaviour(ranch_transport). + +-export([name/0]). +-export([secure/0]). +-export([messages/0]). +-export([listen/1]). +-export([accept/2]). +-export([handshake/2]). +-export([handshake/3]). +-export([handshake_continue/2]). +-export([handshake_continue/3]). +-export([handshake_cancel/1]). +-export([connect/3]). +-export([connect/4]). +-export([recv/3]). +-export([recv_proxy_header/2]). +-export([send/2]). +-export([sendfile/2]). +-export([sendfile/4]). +-export([sendfile/5]). +-export([setopts/2]). +-export([getopts/2]). +-export([getstat/1]). +-export([getstat/2]). +-export([controlling_process/2]). +-export([peername/1]). +-export([sockname/1]). +-export([shutdown/2]). +-export([close/1]). +-export([cleanup/1]). + +-type opts() :: []. +-export_type([opts/0]). + +-spec name() -> erlang. +name() -> erlang. + +-spec secure() -> boolean(). +secure() -> + false. + +-spec messages() -> {erlang, erlang_closed, erlang_error, erlang_passive}. +messages() -> {erlang, erlang_closed, erlang_error, erlang_passive}. + +-spec listen(ranch:transport_opts(opts())) -> {ok, reference()}. +listen(_TransOpts) -> + {ok, make_ref()}. + +-spec accept(reference(), timeout()) -> no_return(). % {ok, reference()}. +accept(_LSocket, _Timeout) -> + receive after infinity -> {ok, make_ref()} end. + +-spec handshake(reference(), timeout()) -> {ok, reference()}. +handshake(CSocket, Timeout) -> + handshake(CSocket, [], Timeout). + +-spec handshake(reference(), opts(), timeout()) -> {ok, reference()}. +handshake(CSocket, _, _) -> + {ok, CSocket}. + +-spec handshake_continue(reference(), timeout()) -> no_return(). +handshake_continue(CSocket, Timeout) -> + handshake_continue(CSocket, [], Timeout). + +-spec handshake_continue(reference(), opts(), timeout()) -> no_return(). +handshake_continue(_, _, _) -> + error(not_supported). + +-spec handshake_cancel(reference()) -> no_return(). +handshake_cancel(_) -> + error(not_supported). + +-spec connect(inet:ip_address() | inet:hostname(), + inet:port_number(), any()) + -> {ok, reference()}. +connect(_Host, Port, _Opts) when is_integer(Port) -> + {ok, make_ref()}. + +-spec connect(inet:ip_address() | inet:hostname(), + inet:port_number(), any(), timeout()) + -> {ok, reference()}. +connect(_Host, Port, _Opts, _Timeout) when is_integer(Port) -> + {ok, make_ref()}. + +-spec recv(reference(), non_neg_integer(), timeout()) + -> {ok, any()} | {error, closed | atom()}. +recv(_Socket, _Length, _Timeout) -> + {ok, <<>>}. + +-spec recv_proxy_header(reference(), timeout()) -> no_return(). +recv_proxy_header(_Socket, _Timeout) -> + error(not_supported). + +-spec send(reference(), iodata()) -> ok | {error, atom()}. +send(_Socket, _Packet) -> + ok. + +-spec sendfile(reference(), file:name_all() | file:fd()) + -> no_return(). +sendfile(Socket, Filename) -> + sendfile(Socket, Filename, 0, 0, []). + +-spec sendfile(reference(), file:name_all() | file:fd(), non_neg_integer(), + non_neg_integer()) + -> no_return(). +sendfile(Socket, File, Offset, Bytes) -> + sendfile(Socket, File, Offset, Bytes, []). + +-spec sendfile(reference(), file:name_all() | file:fd(), non_neg_integer(), + non_neg_integer(), [{chunk_size, non_neg_integer()}]) + -> no_return(). +sendfile(_Socket, Filename, _Offset, _Bytes, _Opts) + when is_list(Filename) orelse is_atom(Filename) + orelse is_binary(Filename) -> + error(not_supported). + +-spec setopts(reference(), list()) -> ok. +setopts(_Socket, _Opts) -> + ok. + +-spec getopts(reference(), [atom()]) -> {ok, list()} | {error, atom()}. +getopts(_Socket, _Opts) -> + {ok, []}. + +-spec getstat(reference()) -> {ok, list()} | {error, atom()}. +getstat(_Socket) -> + {ok, []}. + +-spec getstat(reference(), [atom()]) -> {ok, list()} | {error, atom()}. +getstat(_Socket, _OptionNames) -> + {ok, []}. + +-spec controlling_process(reference(), pid()) + -> ok | {error, closed | not_owner | atom()}. +controlling_process(_Socket, _Pid) -> + ok. + +-spec peername(reference()) + -> {ok, {inet:ip_address(), inet:port_number()} | {local, binary()}} | {error, atom()}. +peername(_Socket) -> + {ok, {{127, 0, 0, 1}, 12701}}. + +-spec sockname(reference()) + -> {ok, {inet:ip_address(), inet:port_number()} | {local, binary()}} | {error, atom()}. +sockname(_Socket) -> + {ok, {{127, 0, 0, 1}, 12710}}. + +-spec shutdown(reference(), read | write | read_write) + -> ok | {error, atom()}. +shutdown(_Socket, _How) -> + ok. + +-spec close(reference()) -> ok. +close(_Socket) -> + ok. + +-spec cleanup(ranch:transport_opts(opts())) -> ok. +cleanup(_) -> + ok. diff --git a/ranch/test/remove_conn_and_wait_protocol.erl b/ranch/test/remove_conn_and_wait_protocol.erl new file mode 100644 index 0000000..abb4281 --- /dev/null +++ b/ranch/test/remove_conn_and_wait_protocol.erl @@ -0,0 +1,21 @@ +-module(remove_conn_and_wait_protocol). +-behaviour(ranch_protocol). + +-export([start_link/3]). +-export([init/3]). + +start_link(Ref, _, [{remove, MaybeRemove, Timeout}]) -> + Pid = spawn_link(?MODULE, init, [Ref, MaybeRemove, Timeout]), + {ok, Pid}. + +init(Ref, MaybeRemove, Timeout) -> + {ok, _} = ranch:handshake(Ref), + _ = case MaybeRemove of + true -> + ranch:remove_connection(Ref); + false -> + ok; + N -> + [ranch:remove_connection(Ref) || _ <- lists:seq(1, N)] + end, + receive after Timeout -> ok end. diff --git a/ranch/test/sendfile_SUITE.erl b/ranch/test/sendfile_SUITE.erl new file mode 100644 index 0000000..30e89e3 --- /dev/null +++ b/ranch/test/sendfile_SUITE.erl @@ -0,0 +1,342 @@ +%% Copyright (c) 2013, James Fish +%% Copyright (c) 2015-2021, 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(sendfile_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-import(ct_helper, [config/2]). +-import(ct_helper, [doc/1]). + +all() -> + [{group, tcp}, {group, tcp_socket}, {group, ssl}]. + +suite() -> + [{timetrap, {seconds, 60}}]. + +groups() -> + Tests = [ + filename, + rawfile, + rawfile_bytes_large, + rawfile_bytes_zero, + rawfile_chunk_size_large, + rawfile_offset_large, + rawfile_range_large, + rawfile_range_medium, + rawfile_range_small + ], + [ + {tcp, [parallel], Tests}, + {tcp_socket, [parallel], Tests}, + {ssl, [parallel], Tests ++ [ssl_chunk_size]} + ]. + +init_per_suite(Config) -> + Filename = filename:join(config(priv_dir, Config), "sendfile"), + Binary = crypto:strong_rand_bytes(20 * 1024 * 1024), + ok = file:write_file(Filename, Binary), + [{filename, Filename} | Config]. + +end_per_suite(Config) -> + Filename = config(filename, Config), + ok = file:delete(Filename), + ok. + +init_per_group(ssl, Config) -> + SslOpts = ct_helper:get_certs_from_ets(), + [{transport, ranch_ssl}, {transport_opts, SslOpts} | Config]; +init_per_group(tcp, Config) -> + [{transport, ranch_tcp}, {transport_opts, []} | Config]; +init_per_group(tcp_socket, Config) -> + %% The socket backend for inet/gen_tcp was introduced as an experimental + %% feature in OTP/23.0, and bugs https://bugs.erlang.org/browse/ERL-1284, + %% 1287 and 1293 were solved in OTP/23.1. socket:use_registry/1 first + %% appears in this release. + %% Due to https://bugs.erlang.org/browse/ERL-1401, the socket backend + %% is not working on Windows. + case + os:type() =/= {win32, nt} andalso + code:ensure_loaded(socket) =:= {module, socket} andalso + erlang:function_exported(socket, use_registry, 1) + of + true -> + [{transport, ranch_tcp}, {transport_opts, [{inet_backend, socket}]} | Config]; + false -> + {skip, "No socket backend support"} + end. + +end_per_group(_, _) -> + ok. + +filename(Config) -> + doc("Use sendfile with a filename."), + Transport = config(transport, Config), + Filename = config(filename, Config), + {ok, Binary} = file:read_file(Filename), + Size = byte_size(Binary), + {ok, {Server, Client}} = sockets(Config), + Ref = recv(Transport, Server, Size), + {ok, Size} = Transport:sendfile(Client, Filename), + {ok, Binary} = result(Ref), + {error, timeout} = Transport:recv(Server, 1, 100), + ok = Transport:close(Client), + ok = Transport:close(Server). + +rawfile(Config) -> + doc("Use sendfile with a file descriptor (raw file)."), + Transport = config(transport, Config), + Filename = config(filename, Config), + {ok, Binary} = file:read_file(Filename), + Size = byte_size(Binary), + {ok, {Server, Client}} = sockets(Config), + {ok, RawFile} = file:open(Filename, [read, raw, binary]), + Ref = recv(Transport, Server, Size), + {ok, Size} = Transport:sendfile(Client, RawFile, 0, Size), + {ok, Binary} = result(Ref), + {error, timeout} = Transport:recv(Server, 1, 100), + {ok, 0} = file:position(RawFile, {cur, 0}), + ok = file:close(RawFile), + ok = Transport:close(Client), + ok = Transport:close(Server). + +rawfile_bytes_large(Config) -> + doc("Use sendfile with a file descriptor. Try to send a size larger than file size."), + Transport = config(transport, Config), + Filename = config(filename, Config), + {ok, Binary} = file:read_file(Filename), + Size = byte_size(Binary), + {ok, {Server, Client}} = sockets(Config), + {ok, RawFile} = file:open(Filename, [read, raw, binary]), + Ref = recv(Transport, Server, Size), + %% Only send Size not Size * 2 + {ok, Size} = Transport:sendfile(Client, RawFile, 0, Size * 2), + {ok, Binary} = result(Ref), + {error, timeout} = Transport:recv(Server, 1, 100), + {ok, 0} = file:position(RawFile, {cur, 0}), + ok = file:close(RawFile), + ok = Transport:close(Client), + ok = Transport:close(Server). + +rawfile_bytes_zero(Config) -> + doc("Use sendfile with a file descriptor. Ensure using a size of 0 sends the whole file."), + Transport = config(transport, Config), + Filename = config(filename, Config), + {ok, Binary} = file:read_file(Filename), + Size = byte_size(Binary), + {ok, {Server, Client}} = sockets(Config), + {ok, RawFile} = file:open(Filename, [read, raw, binary]), + Ref = recv(Transport, Server, Size), + {ok, Size} = Transport:sendfile(Client, RawFile, 0, 0), + {ok, Binary} = result(Ref), + {error, timeout} = Transport:recv(Server, 1, 100), + {ok, 0} = file:position(RawFile, {cur, 0}), + ok = file:close(RawFile), + ok = Transport:close(Client), + ok = Transport:close(Server). + +rawfile_chunk_size_large(Config) -> + doc("Use sendfile with a file descriptor. Try to use a chunk size larger than file size."), + Transport = config(transport, Config), + Filename = config(filename, Config), + {ok, Binary} = file:read_file(Filename), + Size = byte_size(Binary), + {ok, {Server, Client}} = sockets(Config), + {ok, RawFile} = file:open(Filename, [read, raw, binary]), + Ref = recv(Transport, Server, Size), + {ok, Size} = Transport:sendfile(Client, RawFile, 0, Size, [{chunk_size, Size * 2}]), + {ok, Binary} = result(Ref), + {error, timeout} = Transport:recv(Server, 1, 100), + {ok, 0} = file:position(RawFile, {cur, 0}), + ok = file:close(RawFile), + ok = Transport:close(Client), + ok = Transport:close(Server). + +rawfile_offset_large(Config) -> + doc("Use sendfile with a file descriptor. Ensure using an offset larger than file size sends nothing."), + Transport = config(transport, Config), + Filename = config(filename, Config), + {ok, Binary} = file:read_file(Filename), + Size = byte_size(Binary), + {ok, {Server, Client}} = sockets(Config), + {ok, RawFile} = file:open(Filename, [read, raw, binary]), + {ok, 0} = Transport:sendfile(Client, RawFile, Size, 1), + {error, timeout} = Transport:recv(Server, 1, 100), + ok = file:close(RawFile), + ok = Transport:close(Client), + ok = Transport:close(Server). + +rawfile_range_large(Config) -> + doc("Use sendfile with a file descriptor. " + "Set an offset and try to send a size larger than remaining file size."), + Transport = config(transport, Config), + Filename = config(filename, Config), + {ok, Binary} = file:read_file(Filename), + Size = byte_size(Binary), + {ok, {Server, Client}} = sockets(Config), + {ok, RawFile} = file:open(Filename, [read, raw, binary]), + Initial = 499, + {ok, _} = file:position(RawFile, {bof, Initial}), + Offset = 75, + Bytes = Size * 2, + Sent = Size - Offset, + Ref = recv(Transport, Server, Sent), + {ok, Sent} = Transport:sendfile(Client, RawFile, Offset, Bytes), + Binary2 = binary:part(Binary, Offset, Sent), + {ok, Binary2} = result(Ref), + {error, timeout} = Transport:recv(Server, 1, 100), + {ok, Initial} = file:position(RawFile, {cur, 0}), + ok = file:close(RawFile), + ok = Transport:close(Client), + ok = Transport:close(Server). + +rawfile_range_medium(Config) -> + doc("Use sendfile with a file descriptor. " + "Set an offset and try to send a size lower than remaining file size."), + Transport = config(transport, Config), + Filename = config(filename, Config), + {ok, Binary} = file:read_file(Filename), + Size = byte_size(Binary), + {ok, {Server, Client}} = sockets(Config), + {ok, RawFile} = file:open(Filename, [read, raw, binary]), + Initial = 50, + {ok, _} = file:position(RawFile, {bof, Initial}), + Offset = 50, + Bytes = Size - Offset - 50, + Ref = recv(Transport, Server, Bytes), + {ok, Bytes} = Transport:sendfile(Client, RawFile, Offset, Bytes), + Binary2 = binary:part(Binary, Offset, Bytes), + {ok, Binary2} = result(Ref), + {error, timeout} = Transport:recv(Server, 1, 100), + {ok, Initial} = file:position(RawFile, {cur, 0}), + ok = file:close(RawFile), + ok = Transport:close(Client), + ok = Transport:close(Server). + +rawfile_range_small(Config) -> + doc("Use sendfile with a file descriptor. " + "Set an offset and try to send a size lower than remaining file size, " + "which is in turn lower than the chunk size."), + Transport = config(transport, Config), + Filename = config(filename, Config), + {ok, Binary} = file:read_file(Filename), + {ok, {Server, Client}} = sockets(Config), + {ok, RawFile} = file:open(Filename, [read, raw, binary]), + Initial = 3, + {ok, _} = file:position(RawFile, {bof, Initial}), + Offset = 7, + Bytes = 19, + Ref = recv(Transport, Server, Bytes), + {ok, Bytes} = Transport:sendfile(Client, RawFile, Offset, Bytes, [{chunk_size, 16#FFFF}]), + Binary2 = binary:part(Binary, Offset, Bytes), + {ok, Binary2} = result(Ref), + {error, timeout} = Transport:recv(Server, 1, 100), + {ok, Initial} = file:position(RawFile, {cur, 0}), + ok = file:close(RawFile), + ok = Transport:close(Client), + ok = Transport:close(Server). + +ssl_chunk_size(Config) -> + doc("Use sendfile with SSL. Ensure the sendfile fallback respects the chunk size."), + Transport = config(transport, Config), + Filename = config(filename, Config), + {ok, Binary} = file:read_file(Filename), + Size = byte_size(Binary), + Self = self(), + ChunkSize = 8 * 1024, + Fun = fun() -> + receive go -> ok after 5000 -> error(timeout) end, + {ok, {Server, Client}} = sockets(Config), + {ok, RawFile} = file:open(Filename, [read, raw, binary]), + Ref = recv(Transport, Server, Size), + {ok, Size} = Transport:sendfile(Client, RawFile, 0, Size, [{chunk_size, ChunkSize}]), + {ok, Binary} = result(Ref), + {error, timeout} = Transport:recv(Server, 1, 100), + Self ! done, + ok = file:close(RawFile), + ok = Transport:close(Client), + ok = Transport:close(Server) + end, + Pid = spawn_link(Fun), + 1 = erlang:trace(Pid, true, [call]), + _ = erlang:trace_pattern({Transport, send, 2}, true, [global]), + Pid ! go, + receive done -> ok after 30000 -> error(timeout) end, + Sizes = lists:duplicate(Size div ChunkSize, ChunkSize) ++ + [Size rem ChunkSize || (Size rem ChunkSize) =/= 0], + ok = recv_send_trace(Sizes, Pid), + _ = erlang:trace(all, false, [all]), + ok = clean_traces(). + +%% Internal. + +sockets(Config) -> + Transport = config(transport, Config), + TransportOpts = config(transport_opts, Config), + {ok, LSocket} = Transport:listen(#{socket_opts => TransportOpts}), + {ok, {_, Port}} = Transport:sockname(LSocket), + Self = self(), + Fun = fun() -> + {ok, Client} = Transport:connect("localhost", Port, TransportOpts), + ok = Transport:controlling_process(Client, Self), + Self ! {ok, Client} + end, + _ = spawn_link(Fun), + {ok, Server} = Transport:accept(LSocket, 5000), + {ok, _} = Transport:handshake(Server, [], 5000), + receive + {ok, Client} -> + ok = Transport:close(LSocket), + {ok, {Server, Client}} + after 1000 -> + {error, timeout} + end. + +recv(Transport, Server, Size) -> + Self = self(), + Ref = make_ref(), + spawn_link(fun() -> Self ! {Ref, Transport:recv(Server, Size, 20000)} end), + Ref. + +result(Ref) -> + receive + {Ref, Result} -> + Result + after + 30000 -> + {error, result_timedout} + end. + +recv_send_trace([], _Pid) -> + ok; +recv_send_trace([Size | Rest], Pid) -> + receive + {trace, Pid, call, {_, _, [_, Chunk]}} when byte_size(Chunk) == Size -> + recv_send_trace(Rest, Pid); + {trace, Pid, call, {_, _, [_, Chunk]}} -> + {error, {invalid_chunk, Size, byte_size(Chunk)}} + after 1000 -> + {error, timeout} + end. + +clean_traces() -> + receive + {trace, _, _, _} -> + clean_traces(); + {trace, _, _, _, _} -> + clean_traces() + after 0 -> + ok + end. diff --git a/ranch/test/shutdown_SUITE.erl b/ranch/test/shutdown_SUITE.erl new file mode 100644 index 0000000..f75ec68 --- /dev/null +++ b/ranch/test/shutdown_SUITE.erl @@ -0,0 +1,157 @@ +%% Copyright (c) 2013-2021, 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, [name/0]). + +%% ct. + +all() -> + ct_helper:all(?MODULE). + +%% Tests. + +brutal_kill(_) -> + doc("Shutdown Ranch listener with shutdown option set to brutal_kill."), + Name = name(), + {ok, ListenerSup} = ranch:start_listener(Name, + ranch_tcp, #{shutdown => brutal_kill}, + echo_protocol, []), + Port = ranch:get_port(Name), + ok = do_connect_and_ping(Port), + ListenerSupChildren = supervisor:which_children(ListenerSup), + {_, ConnsSupSup, _, _} = lists:keyfind(ranch_conns_sup_sup, 1, ListenerSupChildren), + [Pid] = do_get_conn_pids(ConnsSupSup), + true = is_process_alive(Pid), + ok = ranch:stop_listener(Name), + receive after 100 -> ok end, + false = is_process_alive(Pid), + false = is_process_alive(ListenerSup), + {error, _} = gen_tcp:connect("localhost", Port, []), + ok. + +infinity(_) -> + doc("Shutdown Ranch listener with shutdown option set to infinity."), + Name = name(), + {ok, ListenerSup} = ranch:start_listener(Name, + ranch_tcp, #{shutdown => infinity}, + echo_protocol, []), + Port = ranch:get_port(Name), + ok = do_connect_and_ping(Port), + ListenerSupChildren = supervisor:which_children(ListenerSup), + {_, ConnsSupSup, _, _} = lists:keyfind(ranch_conns_sup_sup, 1, ListenerSupChildren), + [Pid] = do_get_conn_pids(ConnsSupSup), + true = is_process_alive(Pid), + ok = ranch:stop_listener(Name), + receive after 100 -> ok end, + false = is_process_alive(Pid), + false = is_process_alive(ListenerSup), + {error, _} = gen_tcp:connect("localhost", Port, []), + ok. + +infinity_trap_exit(_) -> + doc("Shutdown Ranch listener with shutdown option set to infinity " + "and protocol process trapping exits. The listener must not stop " + "until the protocol process terminates."), + Name = name(), + {ok, ListenerSup} = ranch:start_listener(Name, + ranch_tcp, #{shutdown => infinity}, + trap_exit_protocol, []), + Port = ranch:get_port(Name), + ok = do_connect_and_ping(Port), + ListenerSupChildren = supervisor:which_children(ListenerSup), + {_, ConnsSupSup, _, _} = lists:keyfind(ranch_conns_sup_sup, 1, ListenerSupChildren), + [Pid] = do_get_conn_pids(ConnsSupSup), + true = is_process_alive(Pid), + %% This call will block infinitely. + SpawnPid = spawn(fun() -> ok = ranch:stop_listener(Name) end), + receive after 100 -> ok end, + %% The protocol traps exit signals, and ignore them, so it won't die. + true = is_process_alive(Pid), + %% The listener will stay up forever too. + true = is_process_alive(ListenerSup), + %% We can't connect, though. + {error, _} = gen_tcp:connect("localhost", Port, []), + %% Killing the process unblocks everything. + exit(Pid, kill), + receive after 100 -> ok end, + false = is_process_alive(ListenerSup), + false = is_process_alive(SpawnPid), + ok. + +timeout(_) -> + doc("Shutdown Ranch listener with shutdown option set to 500ms."), + Name = name(), + {ok, ListenerSup} = ranch:start_listener(Name, + ranch_tcp, #{shutdown => 500}, + echo_protocol, []), + Port = ranch:get_port(Name), + ok = do_connect_and_ping(Port), + ListenerSupChildren = supervisor:which_children(ListenerSup), + {_, ConnsSupSup, _, _} = lists:keyfind(ranch_conns_sup_sup, 1, ListenerSupChildren), + [Pid] = do_get_conn_pids(ConnsSupSup), + true = is_process_alive(Pid), + ok = ranch:stop_listener(Name), + receive after 100 -> ok end, + false = is_process_alive(Pid), + false = is_process_alive(ListenerSup), + {error, _} = gen_tcp:connect("localhost", Port, []), + ok. + +timeout_trap_exit(_) -> + doc("Shutdown Ranch listener with shutdown option set to 500ms " + "and protocol process trapping exits. The listener will only stop " + "after the 500ms timeout."), + Name = name(), + {ok, ListenerSup} = ranch:start_listener(Name, + ranch_tcp, #{shutdown => 500}, + trap_exit_protocol, []), + Port = ranch:get_port(Name), + ok = do_connect_and_ping(Port), + ListenerSupChildren = supervisor:which_children(ListenerSup), + {_, ConnsSupSup, _, _} = lists:keyfind(ranch_conns_sup_sup, 1, ListenerSupChildren), + [Pid] = do_get_conn_pids(ConnsSupSup), + true = is_process_alive(Pid), + %% This call will block for the duration of the shutdown. + SpawnPid = spawn(fun() -> ok = ranch:stop_listener(Name) end), + receive after 100 -> ok end, + %% The protocol traps exit signals, and ignore them, so it won't die. + true = is_process_alive(Pid), + %% The listener will stay up for now too. + true = is_process_alive(ListenerSup), + %% We can't connect, though. + {error, _} = gen_tcp:connect("localhost", Port, []), + %% Wait for the timeout to finish and see that everything is killed. + receive after 500 -> ok end, + false = is_process_alive(Pid), + false = is_process_alive(ListenerSup), + false = is_process_alive(SpawnPid), + ok. + +do_get_conn_pids(ConnsSupSup) -> + ConnsSups = [ConnsSup || + {_, ConnsSup, _, _} <- supervisor:which_children(ConnsSupSup)], + ConnChildren = lists:flatten( + [supervisor:which_children(ConnsSup) || ConnsSup <- ConnsSups]), + [ConnPid || {_, ConnPid, _, _} <- ConnChildren]. + +do_connect_and_ping(Port) -> + {ok, Conn} = gen_tcp:connect("localhost", Port, [binary, {active, false}, {packet, raw}]), + ok = gen_tcp:send(Conn, <<"PING">>), + {ok, <<"PING">>} = gen_tcp:recv(Conn, 4, 1000), + ok. diff --git a/ranch/test/ssl_upgrade_protocol.erl b/ranch/test/ssl_upgrade_protocol.erl new file mode 100644 index 0000000..67aec2b --- /dev/null +++ b/ranch/test/ssl_upgrade_protocol.erl @@ -0,0 +1,27 @@ +-module(ssl_upgrade_protocol). +-behaviour(ranch_protocol). + +-export([start_link/3]). +-export([init/3]). + +start_link(Ref, Transport, Opts) -> + Pid = spawn_link(?MODULE, init, [Ref, Transport, Opts]), + {ok, Pid}. + +init(Ref, Transport, _Opts = []) -> + {ok, Socket} = ranch:handshake(Ref), + loop(Socket, Transport). + +loop(Socket, Transport) -> + case Transport:recv(Socket, 0, 5000) of + {ok, <<"UPGRADE">>} when Transport =:= ranch_tcp -> + ok = Transport:send(Socket, <<"READY">>), + Opts = ct_helper:get_certs_from_ets(), + {ok, NewSocket} = ranch_ssl:handshake(Socket, [{verify, verify_none}|Opts], 1000), + loop(NewSocket, ranch_ssl); + {ok, <<"ECHO ", More/binary>>} -> + ok = Transport:send(Socket, More), + loop(Socket, Transport); + _ -> + ok = Transport:close(Socket) + end. diff --git a/ranch/test/stampede_SUITE.erl b/ranch/test/stampede_SUITE.erl new file mode 100644 index 0000000..58b55f6 --- /dev/null +++ b/ranch/test/stampede_SUITE.erl @@ -0,0 +1,193 @@ +%% Copyright (c) 2019-2021, 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(stampede_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-import(ct_helper, [doc/1]). +-import(ct_helper, [name/0]). + +%% ct. + +all() -> + [{group, tcp}, {group, tcp_socket}, {group, ssl}]. + +groups() -> + [ + { + tcp, + [], + [ + stampede_tcp, + stampede_embedded + ] + }, + { + tcp_socket, + [], + [ + stampede_tcp, + stampede_embedded + ] + }, + { + ssl, + [], + [ + stampede_ssl + ] + } + ]. + +init_per_group(tcp_socket, Config) -> + %% The socket backend for inet/gen_tcp was introduced as an experimental + %% feature in OTP/23.0, and bugs https://bugs.erlang.org/browse/ERL-1284, + %% 1287 and 1293 were solved in OTP/23.1. socket:use_registry/1 first + %% appears in this release. + %% Due to https://bugs.erlang.org/browse/ERL-1401, the socket backend + %% is not working on Windows. + case + os:type() =/= {win32, nt} andalso + code:ensure_loaded(socket) =:= {module, socket} andalso + erlang:function_exported(socket, use_registry, 1) + of + true -> + [{socket_opts, [{inet_backend, socket}]}|Config]; + false -> + {skip, "No socket backend support"} + end; +init_per_group(_, Config) -> + Config. + +end_per_group(_, _) -> + ok. + +init_per_suite(Config) -> + {ok, _} = application:ensure_all_started(ranch), + ok = application:start(stampede), + %% Enable logging of progress reports. + %% They will only be available in the HTML reports by default. + ok = logger:set_primary_config(level, none), + ok = logger:set_module_level(?MODULE, info), + ok = logger:set_application_level(stampede, error), + Config. + +end_per_suite(_) -> + ok = application:stop(stampede), + ok = application:stop(ranch). + +%% Tests. + +stampede_tcp(Config) -> + doc("Start a TCP listener, establish a hundred connections, " + "run stampede, confirm we can still connect."), + %% Start a TCP listener. + Name = name(), + SockOpts = do_get_sockopts(Config), + {ok, _} = ranch:start_listener(Name, + ranch_tcp, #{socket_opts => SockOpts}, + echo_protocol, []), + %% Set restart frequency of ranch_sup. + do_set_sup_frequencies([ranch_sup], 999999, 1), + %% Run stampede. + {ok, _} = stampede:start_herd(ranch_stampede, {application, ranch}, + #{interval => {100, 100}, before_kill => fun do_log/1}), + %% Establish a hundred connections. + ok = do_connect(100, ranch_tcp, ranch:get_port(Name), 1000), + ok = stampede:activate(ranch_stampede), + timer:sleep(10000), + ok = stampede:stop_herd(ranch_stampede), + timer:sleep(1000), + %% Confirm we can still connect. + ok = do_connect(1, ranch_tcp, ranch:get_port(Name), 1000), + ok = ranch:stop_listener(Name). + +stampede_ssl(_) -> + doc("Start a SSL listener, establish a hundred connections, " + "run stampede, confirm we can still connect."), + %% Start a TCP listener. + Name = name(), + {ok, _} = ranch:start_listener(Name, + ranch_ssl, ct_helper:get_certs_from_ets(), + echo_protocol, []), + %% Set restart frequencies of ranch_sup and ssl_sup. + do_set_sup_frequencies([ranch_sup, ssl_sup], 999999, 1), + %% Run stampede. + {ok, _} = stampede:start_herd(ranch_stampede, {application, ranch}, + #{interval => {100, 100}, before_kill => fun do_log/1}), + {ok, _} = stampede:start_herd(ssl_stampede, {application, ssl}, + #{interval => {100, 100}, before_kill => fun do_log/1}), + %% Establish a hundred connections. + ok = do_connect(100, ranch_ssl, ranch:get_port(Name), 1000), + ok = stampede:activate(ranch_stampede), + ok = stampede:activate(ssl_stampede), + timer:sleep(10000), + ok = stampede:stop_herd(ssl_stampede), + ok = stampede:stop_herd(ranch_stampede), + timer:sleep(1000), + %% Confirm we can still connect. + ok = do_connect(1, ranch_ssl, ranch:get_port(Name), 1000), + ok = ranch:stop_listener(Name). + +stampede_embedded(Config) -> + doc("Start an embedded TCP listener, establish a hundred connections, " + "run stampede, confirm we can still connect."), + %% Start embedded listener. + Name = name(), + SockOpts = do_get_sockopts(Config), + {ok, SupPid} = embedded_sup:start_link(), + {ok, _} = embedded_sup:start_listener(SupPid, Name, + ranch_tcp, #{socket_opts => SockOpts}, echo_protocol, []), + %% Set restart frequency of ranch_sup and embedded_sup. + do_set_sup_frequencies([ranch_sup, SupPid], 999999, 1), + %% Run stampede. + {ok, _} = stampede:start_herd(ranch_stampede, {application, ranch}, + #{interval => {100, 100}, before_kill => fun do_log/1}), + {ok, _} = stampede:start_herd(embedded_stampede, {supervisor, SupPid}, + #{interval => {100, 100}, before_kill => fun do_log/1}), + %% Establish a hundred connections. + ok = do_connect(100, ranch_tcp, ranch:get_port(Name), 1000), + ok = stampede:activate(ranch_stampede), + ok = stampede:activate(embedded_stampede), + timer:sleep(10000), + ok = stampede:stop_herd(ranch_stampede), + ok = stampede:stop_herd(embedded_stampede), + timer:sleep(1000), + %% Confirm we can still connect. + ok = do_connect(1, ranch_tcp, ranch:get_port(Name), 1000), + ok = embedded_sup:stop_listener(SupPid, Name), + embedded_sup:stop(SupPid), + ok. + +do_set_sup_frequencies(Sups, Intensity, Period) -> + StateFun = fun (S) -> setelement(7, setelement(6, S, Intensity), Period) end, + _ = [sys:replace_state(Sup, StateFun) || Sup <- Sups], + ok. + +do_connect(0, _, _, _) -> + ok; +do_connect(N, Transport, Port, Timeout) -> + {ok, _} = Transport:connect("localhost", Port, [{active, false}], Timeout), + do_connect(N - 1, Transport, Port, Timeout). + +do_log(Pid) when is_pid(Pid) -> + ct:log(info, "~p: ~p~n", [Pid, erlang:process_info(Pid)]), + true; +do_log(Port) when is_port(Port) -> + ct:log(info, "~p: ~p~n", [Port, erlang:port_info(Port)]), + true. + +do_get_sockopts(Config) -> + proplists:get_value(socket_opts, Config, []). diff --git a/ranch/test/supervisor_separate.erl b/ranch/test/supervisor_separate.erl new file mode 100644 index 0000000..f96c7e4 --- /dev/null +++ b/ranch/test/supervisor_separate.erl @@ -0,0 +1,16 @@ +-module(supervisor_separate). +-behavior(supervisor). +-behavior(ranch_protocol). + +-export([start_link/3]). +-export([init/1]). + +start_link(Ref, Transport, Opts) -> + {ok, SupPid} = supervisor:start_link(?MODULE, []), + {ok, ConnPid} = supervisor:start_child(SupPid, + {echo_protocol, {echo_protocol, start_link, [Ref, Transport, Opts]}, + temporary, 5000, worker, [echo_protocol]}), + {ok, SupPid, ConnPid}. + +init([]) -> + {ok, {{one_for_one, 1, 1}, []}}. diff --git a/ranch/test/transport_capabilities_protocol.erl b/ranch/test/transport_capabilities_protocol.erl new file mode 100644 index 0000000..5f23d39 --- /dev/null +++ b/ranch/test/transport_capabilities_protocol.erl @@ -0,0 +1,53 @@ +-module(transport_capabilities_protocol). +-behaviour(ranch_protocol). + +-export([start_link/3]). +-export([init/3]). + +start_link(Ref, Transport, Opts) -> + Pid = spawn_link(?MODULE, init, [Ref, Transport, Opts]), + {ok, Pid}. + +init(Ref, Transport, _Opts = []) -> + {ok, Socket} = ranch:handshake(Ref), + loop(Socket, Transport). + +loop(Socket, Transport) -> + case Transport:recv(Socket, 0, 5000) of + {ok, Data} -> + Reply = + case check(Socket, Transport, Data) of + ok -> + <<"OK">>; + error -> + <<"ERROR">> + end, + Transport:send(Socket, Reply), + loop(Socket, Transport); + _ -> + ok = Transport:close(Socket) + end. + +check(Socket, Transport, <<"getopts/2">>) -> + case catch Transport:getopts(Socket, []) of + {ok, _} -> + ok; + _ -> + error + end; + +check(Socket, Transport, <<"getstat/1">>) -> + case catch Transport:getstat(Socket) of + {ok, _} -> + ok; + _ -> + error + end; + +check(Socket, Transport, <<"getstat/2">>) -> + case catch Transport:getstat(Socket, []) of + {ok, _} -> + ok; + _ -> + error + end. diff --git a/ranch/test/trap_exit_protocol.erl b/ranch/test/trap_exit_protocol.erl new file mode 100644 index 0000000..da71fb4 --- /dev/null +++ b/ranch/test/trap_exit_protocol.erl @@ -0,0 +1,23 @@ +-module(trap_exit_protocol). +-behaviour(ranch_protocol). + +-export([start_link/3]). +-export([init/3]). + +start_link(Ref, Transport, Opts) -> + Pid = spawn_link(?MODULE, init, [Ref, Transport, Opts]), + {ok, Pid}. + +init(Ref, Transport, _Opts = []) -> + process_flag(trap_exit, true), + {ok, Socket} = ranch:handshake(Ref), + loop(Socket, Transport). + +loop(Socket, Transport) -> + case Transport:recv(Socket, 0, infinity) of + {ok, Data} -> + Transport:send(Socket, Data), + loop(Socket, Transport); + _ -> + ok = Transport:close(Socket) + end. diff --git a/ranch/test/upgrade_SUITE.erl b/ranch/test/upgrade_SUITE.erl new file mode 100644 index 0000000..8e14bcc --- /dev/null +++ b/ranch/test/upgrade_SUITE.erl @@ -0,0 +1,261 @@ +%% Copyright (c) 2020-2021, Loïc Hoguin +%% Copyright (c) 2021, Maria Scott +%% +%% 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(upgrade_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-import(ct_helper, [doc/1]). + +%% ct. + +all() -> + ct_helper:all(?MODULE). + +init_per_suite(Config) -> + %% Remove environment variables inherited from Erlang.mk. + os:unsetenv("ERLANG_MK_TMP"), + os:unsetenv("APPS_DIR"), + os:unsetenv("DEPS_DIR"), + os:unsetenv("ERL_LIBS"), + os:unsetenv("CI_ERLANG_MK"), + %% Ensure we are using the C locale for all os:cmd calls. + os:putenv("LC_ALL", "C"), + Config. + +end_per_suite(_Config) -> + ok. + +%% Find GNU Make. + +do_find_make_cmd() -> + case os:getenv("MAKE") of + false -> + case os:find_executable("gmake") of + false -> "make"; + Cmd -> Cmd + end; + Cmd -> + Cmd + end. + +%% Manipulate the release. + +do_copy(Example0) -> + Example = atom_to_list(Example0), + {ok, CWD} = file:get_cwd(), + _ = do_exec_log("cp -R " ++ CWD ++ "/../../examples/" ++ Example ++ " " ++ CWD), + Dir = CWD ++ "/" ++ Example, + _ = do_exec_log("sed -i.bak s/\"include \\.\\.\\/\\.\\.\\/erlang.mk\"/\"include ..\\/..\\/..\\/erlang.mk\"/ " ++ Dir ++ "/Makefile"), + ok. + +do_remove(Example0) -> + Example = atom_to_list(Example0), + {ok, CWD} = file:get_cwd(), + _ = do_exec_log("rm -rf " ++ CWD ++ "/" ++ Example), + ok. + +do_get_paths(Example0) -> + Example = atom_to_list(Example0), + {ok, CWD} = file:get_cwd(), + Dir = CWD ++ "/" ++ Example, + Rel = Dir ++ "/_rel/" ++ Example ++ "_example/bin/" ++ Example ++ "_example", + Log = Dir ++ "/_rel/" ++ Example ++ "_example/log/erlang.log.1", + {Dir, Rel, Log}. + +do_compile_and_start(Example) -> + Make = do_find_make_cmd(), + {Dir, Rel, _} = do_get_paths(Example), + _ = do_exec_log(Make ++ " -C " ++ Dir ++ " distclean"), + %% TERM=dumb disables relx coloring. + _ = do_exec_log(Make ++ " -C " ++ Dir ++ " TERM=dumb"), + %% For some reason the release has ExampleStr.boot + %% while the downgrade expects start.boot? + ExampleStr = atom_to_list(Example), + _ = do_exec_log("cp " + ++ Dir ++ "/_rel/" ++ ExampleStr + ++ "_example/releases/1/" ++ ExampleStr ++ "_example.boot " + ++ Dir ++ "/_rel/" ++ ExampleStr + ++ "_example/releases/1/start.boot"), + _ = do_exec_log(Rel ++ " stop"), + _ = do_exec_log(Rel ++ " start"), + timer:sleep(2000), + _ = do_exec_log(Rel ++ " eval 'application:info()'"), + ok. + +do_stop(Example) -> + {Dir, Rel, Log} = do_get_paths(Example), + _ = do_exec_log("sed -i.bak s/\"2\"/\"1\"/ " ++ Dir ++ "/relx.config"), + _ = do_exec_log(Rel ++ " stop"), + ct:log("~s~n", [element(2, file:read_file(Log))]). + +%% When we are on a tag (git describe --exact-match succeeds), +%% we use the tag before that as a starting point. Otherwise +%% we use the most recent tag. +do_use_ranch_previous(Example) -> + TagsOutput = do_exec_log("git tag | tr - \\~ | sort -V | tr \\~ -"), + Tags = string:lexemes(TagsOutput, "\n"), + DescribeOutput = do_exec_log("git describe --exact-match"), + {CommitOrTag, Prev} = case DescribeOutput of + "fatal: no tag exactly matches " ++ _ -> {commit, hd(lists:reverse(Tags))}; + _ -> {tag, hd(tl(lists:reverse(Tags)))} + end, + do_use_ranch_commit(Example, Prev), + CommitOrTag. + +%% Replace the current Ranch commit with the one given as argument. +do_use_ranch_commit(Example, Commit) -> + {Dir, _, _} = do_get_paths(Example), + _ = do_exec_log( + "sed -i.bak s/\"dep_ranch_commit = .*\"/\"dep_ranch_commit = " + ++ Commit ++ "\"/ " ++ Dir ++ "/Makefile" + ), + ok. + +%% Remove Ranch and rebuild, this time generating a relup. +do_build_relup(Example, CommitOrTag) -> + Make = do_find_make_cmd(), + {Dir, _, _} = do_get_paths(Example), + _ = do_exec_log("rm -rf " ++ Dir ++ "/deps/ranch/*"), + _ = do_exec_log("sed -i.bak s/\"1\"/\"2\"/ " ++ Dir ++ "/relx.config"), + %% We need Ranch to be fetched first in order to copy the current appup + %% and optionally update its version when we are not on a tag. + _ = do_exec_log("cp -R " + ++ Dir ++ "/../../../Makefile " + ++ Dir ++ "/../../../erlang.mk " + ++ Dir ++ "/../../../src " + ++ Dir ++ "/deps/ranch/"), + _ = do_exec_log(Make ++ " -C " ++ Dir ++ " deps"), + _ = case CommitOrTag of + tag -> ok; + commit -> + %% Force the rebuild of Ranch. + _ = do_exec_log(Make ++ " -C " ++ Dir ++ "/deps/ranch clean"), + %% Update the Ranch version so that the upgrade can be applied. + ProjectVersion = do_exec_log("grep \"PROJECT_VERSION = \" " ++ Dir ++ "/deps/ranch/Makefile"), + ["PROJECT_VERSION = " ++ Vsn0|_] = string:lexemes(ProjectVersion, "\n"), + [A, B|Tail] = string:lexemes(Vsn0, "."), + Vsn = binary_to_list(iolist_to_binary([A, $., B, ".9", lists:join($., Tail)])), + ct:log("Changing Ranch version from ~s to ~s~n", [Vsn0, Vsn]), + _ = do_exec_log( + "sed -i.bak s/\"PROJECT_VERSION = .*\"/\"PROJECT_VERSION = " ++ Vsn ++ "\"/ " + ++ Dir ++ "/deps/ranch/Makefile" + ), + %% The version in the appup must be the same as PROJECT_VERSION. + _ = do_exec_log( + "sed -i.bak s/\"" ++ Vsn0 ++ "\"/\"" ++ Vsn ++ "\"/ " + ++ Dir ++ "/deps/ranch/src/ranch.appup" + ) + end, + _ = do_exec_log(Make ++ " -C " ++ Dir ++ " relup"), + ok. + +%% Copy the tarball in the correct location and upgrade. +do_upgrade(Example) -> + ExampleStr = atom_to_list(Example), + {Dir, Rel, _} = do_get_paths(Example), + _ = do_exec_log("cp " + ++ Dir ++ "/_rel/" ++ ExampleStr + ++ "_example/" ++ ExampleStr ++ "_example-2.tar.gz " + ++ Dir ++ "/_rel/" ++ ExampleStr + ++ "_example/releases/2/" ++ ExampleStr ++ "_example.tar.gz"), + _ = do_exec_log(Rel ++ " upgrade \"2\""), + _ = do_exec_log(Rel ++ " eval 'application:info()'"), + ok. + +do_downgrade(Example) -> + {_, Rel, _} = do_get_paths(Example), + _ = do_exec_log(Rel ++ " downgrade \"1\""), + _ = do_exec_log(Rel ++ " eval 'application:info()'"), + ok. + +%% Tests. + +upgrade_ranch_one_conn(_) -> + case os:type() of + {win32, nt} -> + {skip, "This test suite is not currently supported on Windows."}; + _ -> + do_upgrade_ranch_one_conn() + end. + +do_upgrade_ranch_one_conn() -> + Example = tcp_echo, + ExampleStr = atom_to_list(Example), + Port = 5555, + {_, Rel, _} = do_get_paths(Example), + try + %% Copy the example. + do_copy(Example), + %% Build and start the example release using the previous Ranch version. + CommitOrTag = do_use_ranch_previous(Example), + do_compile_and_start(Example), + %% Ensure that the metrics key is not present in the ranch:info output. + "false\n" = do_exec_log(Rel ++ " eval " + "'maps:is_key(metrics, ranch:info(" ++ ExampleStr ++ "))'"), + %% Establish a connection and check that it works. + {ok, S} = gen_tcp:connect("localhost", Port, [{active, false}, binary]), + ok = gen_tcp:send(S, "Hello!"), + {ok, <<"Hello!">>} = gen_tcp:recv(S, 0, 1000), + %% Update Ranch to master then build a release upgrade. + do_build_relup(Example, CommitOrTag), + %% Perform the upgrade, then check that our connection is still up. + do_upgrade(Example), + %% Ensure that the mextrics key is present in the ranch:info output. + "true\n" = do_exec_log(Rel ++ " eval " + "'maps:is_key(metrics, ranch:info(" ++ ExampleStr ++ "))'"), + ok = gen_tcp:send(S, "Hello!"), + {ok, <<"Hello!">>} = gen_tcp:recv(S, 0, 1000), + %% Ensure that no accepts have been counted yet. + "0\n" = do_exec_log(Rel ++ " eval " + "'lists:sum([N || {{conns_sup, _, accept}, N} <- " + "maps:to_list(maps:get(metrics, ranch:info(" ++ ExampleStr ++ ")))])'"), + %% Check that new connections are still accepted. + {ok, S2} = gen_tcp:connect("localhost", Port, [{active, false}, binary]), + %% Ensure that the accept has been counted. + "1\n" = do_exec_log(Rel ++ " eval " + "'lists:sum([N || {{conns_sup, _, accept}, N} <- " + "maps:to_list(maps:get(metrics, ranch:info(" ++ ExampleStr ++ ")))])'"), + %% Ensure that no terminates have been counted yet. + "0\n" = do_exec_log(Rel ++ " eval " + "'lists:sum([N || {{conns_sup, _, terminate}, N} <- " + "maps:to_list(maps:get(metrics, ranch:info(" ++ ExampleStr ++ ")))])'"), + %% Close the socket, ensure that the termination has been counted. + ok = gen_tcp:close(S2), + "1\n" = do_exec_log(Rel ++ " eval " + "'lists:sum([N || {{conns_sup, _, terminate}, N} <- " + "maps:to_list(maps:get(metrics, ranch:info(" ++ ExampleStr ++ ")))])'"), + %% Perform the downgrade, then check that our connection is still up. + do_downgrade(Example), + %% Ensure that the mextrics key is not present any more. + "false\n" = do_exec_log(Rel ++ " eval " + "'maps:is_key(metrics, ranch:info(" ++ ExampleStr ++ "))'"), + ok = gen_tcp:send(S, "Hello!"), + {ok, <<"Hello!">>} = gen_tcp:recv(S, 0, 1000), + %% Check that new connections are still accepted. + {ok, _} = gen_tcp:connect("localhost", Port, [{active, false}, binary]), + ok + after + do_stop(tcp_echo), + do_remove(Example) + end. + +%% @todo upgrade_ranch_max_conn + +do_exec_log(Cmd) -> + ct:log("Command: ~s~n", [Cmd]), + Out=os:cmd(Cmd), + ct:log("Output:~n~n~s~n", [Out]), + Out.