Import "gun", the HTTP client. We will use it for the tests and the
ActivityPub activities.main
parent
640ffe4d75
commit
f60a8b1725
|
@ -0,0 +1,11 @@
|
|||
.erlang.mk
|
||||
.*.plt
|
||||
*.d
|
||||
deps
|
||||
doc/guide.pdf
|
||||
doc/html
|
||||
doc/man*
|
||||
ebin/test
|
||||
ebin/*.beam
|
||||
logs
|
||||
test/*.beam
|
|
@ -0,0 +1,13 @@
|
|||
Copyright (c) 2013-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
|
||||
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.
|
|
@ -0,0 +1,7 @@
|
|||
.PHONY: all clean
|
||||
|
||||
all:
|
||||
${MAKE} -C src
|
||||
|
||||
clean:
|
||||
${MAKE} -C src clean
|
|
@ -0,0 +1,43 @@
|
|||
= Gun
|
||||
|
||||
Gun is an Erlang HTTP client with support for HTTP/1.1,
|
||||
HTTP/2, Websocket and more.
|
||||
|
||||
== Goals
|
||||
|
||||
Gun aims to provide an *easy to use* client compatible with
|
||||
HTTP/1.1, HTTP/2 and Websocket. Gun can connect through any
|
||||
combination of Socks and HTTP proxies.
|
||||
|
||||
Gun is *always connected*. It will maintain a permanent
|
||||
connection to the server, reopening it as soon as the server
|
||||
closes it, saving time for the requests that come in.
|
||||
|
||||
All connections are *supervised* automatically, allowing
|
||||
developers to focus on writing their code without worrying.
|
||||
|
||||
== Sponsors
|
||||
|
||||
Gun was previously sponsored by
|
||||
http://leo-project.net/leofs/[LeoFS Cloud Storage],
|
||||
https://sameroom.io/[Sameroom],
|
||||
and https://pleroma.social/[Pleroma].
|
||||
|
||||
== Online documentation
|
||||
|
||||
* https://ninenines.eu/docs/en/gun/2.1/guide[User guide]
|
||||
* https://ninenines.eu/docs/en/gun/2.1/manual[Function reference]
|
||||
|
||||
== Offline documentation
|
||||
|
||||
* While still online, run `make docs`
|
||||
* User guide available in `doc/` in PDF and HTML formats
|
||||
* Function reference man pages available in `doc/man3/` and `doc/man7/`
|
||||
* Run `make install-docs` to install man pages on your system
|
||||
* Full documentation in Asciidoc available in `doc/src/`
|
||||
|
||||
== Getting help
|
||||
|
||||
* Official IRC Channel: #ninenines on irc.freenode.net
|
||||
* https://github.com/ninenines/gun/issues[Issues tracker]
|
||||
* https://ninenines.eu/services/[Commercial Support]
|
|
@ -0,0 +1,35 @@
|
|||
// a2x: --dblatex-opts "-P latex.output.revhistory=0 -P doc.publisher.show=0 -P index.numbered=0"
|
||||
// a2x: --dblatex-opts "-s gun"
|
||||
// a2x: -d book --attribute tabsize=4
|
||||
|
||||
= Gun User Guide
|
||||
|
||||
= Interface
|
||||
|
||||
include::introduction.asciidoc[Introduction]
|
||||
|
||||
include::start.asciidoc[Starting and stopping]
|
||||
|
||||
include::protocols.asciidoc[Supported protocols]
|
||||
|
||||
include::connect.asciidoc[Connection]
|
||||
|
||||
include::http.asciidoc[Using HTTP]
|
||||
|
||||
include::websocket.asciidoc[Using Websocket]
|
||||
|
||||
= Advanced
|
||||
|
||||
include::internals_tls_over_tls.asciidoc[Internals: TLS over TLS]
|
||||
|
||||
= Additional information
|
||||
|
||||
include::migrating_from_2.0.asciidoc[Changes since Gun 2.0]
|
||||
|
||||
include::migrating_from_1.3.asciidoc[Migrating from Gun 1.3 to 2.0]
|
||||
|
||||
include::migrating_from_1.2.asciidoc[Migrating from Gun 1.2 to 1.3]
|
||||
|
||||
include::migrating_from_1.1.asciidoc[Migrating from Gun 1.1 to 1.2]
|
||||
|
||||
include::migrating_from_1.0.asciidoc[Migrating from Gun 1.0 to 1.1]
|
|
@ -0,0 +1,166 @@
|
|||
[[connect]]
|
||||
== Connection
|
||||
|
||||
This chapter describes how to open, monitor and close
|
||||
a connection using the Gun client.
|
||||
|
||||
=== Gun connections
|
||||
|
||||
Gun is designed with the HTTP/2 and Websocket protocols in mind.
|
||||
They are built for long-running connections that allow concurrent
|
||||
exchange of data, either in the form of request/responses for
|
||||
HTTP/2 or in the form of messages for Websocket.
|
||||
|
||||
A Gun connection is an Erlang process that manages a socket to
|
||||
a remote endpoint. This Gun connection is owned by a user
|
||||
process that is called the _owner_ of the connection, and is
|
||||
managed by the supervision tree of the `gun` application.
|
||||
|
||||
Any process can communicate with the Gun connection
|
||||
by calling functions from the module `gun`. All functions
|
||||
perform their respective operations asynchronously. The Gun
|
||||
connection will send Erlang messages to the calling process
|
||||
whenever needed.
|
||||
|
||||
When the remote endpoint closes the connection, Gun attempts
|
||||
to reconnect automatically.
|
||||
|
||||
=== Opening a new connection
|
||||
|
||||
The `gun:open/2,3` function must be used to open a connection.
|
||||
|
||||
.Opening a connection to example.org on port 443
|
||||
[source,erlang]
|
||||
----
|
||||
{ok, ConnPid} = gun:open("example.org", 443).
|
||||
----
|
||||
|
||||
If the port given is 443, Gun will attempt to connect using
|
||||
TLS. The protocol will be selected automatically using the
|
||||
ALPN extension for TLS. By default Gun supports HTTP/2
|
||||
and HTTP/1.1 when connecting using TLS.
|
||||
|
||||
For any other port, Gun will attempt to connect using
|
||||
plain TCP and will use the HTTP/1.1 protocol.
|
||||
|
||||
The transport and protocol used can be overriden via
|
||||
options. The manual documents all available options.
|
||||
|
||||
Options can be provided as a third argument, and take the
|
||||
form of a map.
|
||||
|
||||
.Opening a TLS connection to example.org on port 8443
|
||||
[source,erlang]
|
||||
----
|
||||
{ok, ConnPid} = gun:open("example.org", 8443, #{transport => tls}).
|
||||
----
|
||||
|
||||
When using TLS you may want to tweak the
|
||||
http://erlang.org/doc/man/ssl_app.html#configuration[configuration]
|
||||
for the `ssl` application, in particular the `session_lifetime`
|
||||
and `session_cache_client_max` to limit the amount of memory
|
||||
used for the TLS sessions cache.
|
||||
|
||||
=== Waiting for the connection to be established
|
||||
|
||||
When Gun successfully connects to the server, it sends a
|
||||
`gun_up` message with the protocol that has been selected
|
||||
for the connection.
|
||||
|
||||
Gun provides the functions `gun:await_up/1,2,3` that wait
|
||||
for the `gun_up` message. They can optionally take a monitor
|
||||
reference and/or timeout value. If no monitor is provided,
|
||||
one will be created for the duration of the function call.
|
||||
|
||||
.Synchronous opening of a connection
|
||||
[source,erlang]
|
||||
----
|
||||
{ok, ConnPid} = gun:open("example.org", 443),
|
||||
{ok, Protocol} = gun:await_up(ConnPid).
|
||||
----
|
||||
|
||||
=== Handling connection loss
|
||||
|
||||
When the connection is lost, Gun will send a `gun_down`
|
||||
message indicating the current protocol, the reason the
|
||||
connection was lost and two lists of stream references.
|
||||
|
||||
The first list indicates open streams that _may_ have been
|
||||
processed by the server. The second list indicates open
|
||||
streams that the server did not process.
|
||||
|
||||
=== Monitoring the connection process
|
||||
|
||||
Because software errors are unavoidable, it is important to
|
||||
detect when the Gun process crashes. It is also important
|
||||
to detect when it exits normally. Erlang provides two ways
|
||||
to do that: links and monitors.
|
||||
|
||||
Gun leaves you the choice as to which one will be used.
|
||||
However, if you use the `gun:await/2,3` or `gun:await_body/2,3`
|
||||
functions, a monitor may be used for you to avoid getting
|
||||
stuck waiting for a message that will never come.
|
||||
|
||||
If you choose to monitor yourself you can do it on a permanent
|
||||
basis rather than on every message you will receive, saving
|
||||
resources. Indeed, the `gun:await/3,4` and `gun:await_body/3,4`
|
||||
functions both accept a monitor argument if you have one already.
|
||||
|
||||
.Monitoring the connection process
|
||||
[source,erlang]
|
||||
----
|
||||
{ok, ConnPid} = gun:open("example.org", 443).
|
||||
MRef = monitor(process, ConnPid).
|
||||
----
|
||||
|
||||
This monitor reference can be kept and used until the connection
|
||||
process exits.
|
||||
|
||||
.Handling `DOWN` messages
|
||||
[source,erlang]
|
||||
----
|
||||
receive
|
||||
%% Receive Gun messages here...
|
||||
{'DOWN', Mref, process, ConnPid, Reason} ->
|
||||
error_logger:error_msg("Oops!"),
|
||||
exit(Reason)
|
||||
end.
|
||||
----
|
||||
|
||||
What to do when you receive a `DOWN` message is entirely up to you.
|
||||
|
||||
=== Closing the connection abruptly
|
||||
|
||||
The connection can be stopped abruptly at any time by calling
|
||||
the `gun:close/1` function.
|
||||
|
||||
.Immediate closing of the connection
|
||||
[source,erlang]
|
||||
----
|
||||
gun:close(ConnPid).
|
||||
----
|
||||
|
||||
The process is stopped immediately without having a chance to
|
||||
perform the protocol's closing handshake, if any.
|
||||
|
||||
//=== Closing the connection gracefully
|
||||
//
|
||||
//The connection can also be stopped gracefully by calling the
|
||||
//`gun:shutdown/1` function.
|
||||
//
|
||||
//.Graceful shutdown of the connection
|
||||
//[source,erlang]
|
||||
//----
|
||||
//gun:shutdown(ConnPid).
|
||||
//----
|
||||
//
|
||||
//Gun will refuse any new requests or messages after you call
|
||||
//this function. It will however continue to send you messages
|
||||
//for existing streams until they are all completed.
|
||||
//
|
||||
//For example if you performed a GET request just before calling
|
||||
//`gun:shutdown/1`, you will still receive the response before
|
||||
//Gun closes the connection.
|
||||
//
|
||||
//If you set a monitor beforehand, you will receive a message
|
||||
//when the connection has been closed.
|
|
@ -0,0 +1,8 @@
|
|||
\NeedsTeXFormat{LaTeX2e}
|
||||
\ProvidesPackage{asciidoc-dblatex}[2012/10/24 AsciiDoc DocBook Style]
|
||||
|
||||
%% Just use the original package and pass the options.
|
||||
\RequirePackageWithOptions{docbook}
|
||||
|
||||
%% Define an alias for make snippets to be compatible with source-highlighter.
|
||||
\lstalias{makefile}{make}
|
|
@ -0,0 +1,387 @@
|
|||
[[http]]
|
||||
== HTTP
|
||||
|
||||
This chapter describes how to use the Gun client for
|
||||
communicating with an HTTP/1.1 or HTTP/2 server.
|
||||
|
||||
=== Streams
|
||||
|
||||
Every time a request is initiated, Gun creates a _stream_.
|
||||
A _stream reference_ uniquely identifies a set of request and
|
||||
response and must be used to perform additional operations
|
||||
with a stream or to identify its messages.
|
||||
|
||||
Stream references use the Erlang _reference_ data type and
|
||||
are therefore unique.
|
||||
|
||||
Streams can be canceled at any time. This will stop any further
|
||||
messages from being sent to the calling process. Depending on
|
||||
its capabilities, the server will also be instructed to cancel
|
||||
the request.
|
||||
|
||||
Canceling a stream may result in Gun dropping the connection
|
||||
temporarily, to avoid uploading or downloading data that will
|
||||
not be used.
|
||||
|
||||
.Cancelling a stream
|
||||
[source,erlang]
|
||||
----
|
||||
gun:cancel(ConnPid, StreamRef).
|
||||
----
|
||||
|
||||
=== Sending requests
|
||||
|
||||
Gun provides many convenient functions for performing common
|
||||
operations, like GET, POST or DELETE. It also provides a
|
||||
general purpose function in case you need other methods.
|
||||
|
||||
The availability of these methods on the server can vary
|
||||
depending on the software used but also on a per-resource
|
||||
basis.
|
||||
|
||||
Gun will automatically set a few headers depending on the
|
||||
method used. For all methods however it will set the host
|
||||
header if it has not been provided in the request arguments.
|
||||
|
||||
This section focuses on the act of sending a request. The
|
||||
handling of responses will be explained further on.
|
||||
|
||||
==== GET and HEAD
|
||||
|
||||
Use `gun:get/2,3,4` to request a resource.
|
||||
|
||||
.GET "/organizations/ninenines"
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:get(ConnPid, "/organizations/ninenines").
|
||||
----
|
||||
|
||||
.GET "/organizations/ninenines" with custom headers
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:get(ConnPid, "/organizations/ninenines", [
|
||||
{<<"accept">>, "application/json"},
|
||||
{<<"user-agent">>, "revolver/1.0"}
|
||||
]).
|
||||
----
|
||||
|
||||
Note that the list of headers has the field name as a binary.
|
||||
The field value is iodata, which is either a binary or an
|
||||
iolist.
|
||||
|
||||
Use `gun:head/2,3,4` if you don't need the response body.
|
||||
|
||||
.HEAD "/organizations/ninenines"
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:head(ConnPid, "/organizations/ninenines").
|
||||
----
|
||||
|
||||
.HEAD "/organizations/ninenines" with custom headers
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:head(ConnPid, "/organizations/ninenines", [
|
||||
{<<"accept">>, "application/json"},
|
||||
{<<"user-agent">>, "revolver/1.0"}
|
||||
]).
|
||||
----
|
||||
|
||||
It is not possible to send a request body with a GET or HEAD
|
||||
request.
|
||||
|
||||
==== POST, PUT and PATCH
|
||||
|
||||
HTTP defines three methods to create or update a resource.
|
||||
|
||||
POST is generally used when the resource identifier (URI) isn't known
|
||||
in advance when creating a resource. POST can also be used to
|
||||
replace an existing resource, although PUT is more appropriate
|
||||
in that situation.
|
||||
|
||||
PUT creates or replaces a resource identified by the URI.
|
||||
|
||||
PATCH provides instructions on how to modify the resource.
|
||||
|
||||
Both POST and PUT send the entire resource representation in their
|
||||
request body. The PATCH method can be used when this is not
|
||||
desirable. The request body of a PATCH method may be a partial
|
||||
representation or a list of instructions on how to update the
|
||||
resource.
|
||||
|
||||
The functions `gun:post/4,5`, `gun:put/4,5` and `gun:patch/4,5`
|
||||
take a body as their fourth argument. These functions do
|
||||
not require any body-specific header to be set, although
|
||||
it is always recommended to set the content-type header.
|
||||
Gun will set the other headers automatically.
|
||||
|
||||
In this and the following examples in this section, `gun:post`
|
||||
can be replaced by `gun:put` or `gun:patch` for performing
|
||||
a PUT or PATCH request, respectively.
|
||||
|
||||
.POST "/organizations/ninenines"
|
||||
[source,erlang]
|
||||
----
|
||||
Body = "{\"msg\": \"Hello world!\"}",
|
||||
StreamRef = gun:post(ConnPid, "/organizations/ninenines", [
|
||||
{<<"content-type">>, "application/json"}
|
||||
], Body).
|
||||
----
|
||||
|
||||
The functions `gun:post/3,4`, `gun:put/3,4` and `gun:patch/3,4`
|
||||
do not take a body in their arguments: the body must be
|
||||
provided later on using the `gun:data/4` function.
|
||||
|
||||
It is recommended to send the content-length header if you
|
||||
know it in advance, although this is not required. If it
|
||||
is not set, HTTP/1.1 will use the chunked transfer-encoding,
|
||||
and HTTP/2 will continue normally as it is chunked by design.
|
||||
|
||||
.POST "/organizations/ninenines" with delayed body
|
||||
[source,erlang]
|
||||
----
|
||||
Body = "{\"msg\": \"Hello world!\"}",
|
||||
StreamRef = gun:post(ConnPid, "/organizations/ninenines", [
|
||||
{<<"content-length">>, integer_to_binary(length(Body))},
|
||||
{<<"content-type">>, "application/json"}
|
||||
]),
|
||||
gun:data(ConnPid, StreamRef, fin, Body).
|
||||
----
|
||||
|
||||
The atom `fin` indicates this is the last chunk of data to
|
||||
be sent. You can call the `gun:data/4` function as many
|
||||
times as needed until you have sent the entire body. The
|
||||
last call must use `fin` and all the previous calls must
|
||||
use `nofin`. The last chunk may be empty.
|
||||
|
||||
.Streaming the request body
|
||||
[source,erlang]
|
||||
----
|
||||
sendfile(ConnPid, StreamRef, Filepath) ->
|
||||
{ok, IoDevice} = file:open(Filepath, [read, binary, raw]),
|
||||
do_sendfile(ConnPid, StreamRef, IoDevice).
|
||||
|
||||
do_sendfile(ConnPid, StreamRef, IoDevice) ->
|
||||
case file:read(IoDevice, 8000) of
|
||||
eof ->
|
||||
gun:data(ConnPid, StreamRef, fin, <<>>),
|
||||
file:close(IoDevice);
|
||||
{ok, Bin} ->
|
||||
gun:data(ConnPid, StreamRef, nofin, Bin),
|
||||
do_sendfile(ConnPid, StreamRef, IoDevice)
|
||||
end.
|
||||
----
|
||||
|
||||
==== DELETE
|
||||
|
||||
Use `gun:delete/2,3,4` to delete a resource.
|
||||
|
||||
.DELETE "/organizations/ninenines"
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:delete(ConnPid, "/organizations/ninenines").
|
||||
----
|
||||
|
||||
.DELETE "/organizations/ninenines" with custom headers
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:delete(ConnPid, "/organizations/ninenines", [
|
||||
{<<"user-agent">>, "revolver/1.0"}
|
||||
]).
|
||||
----
|
||||
|
||||
==== OPTIONS
|
||||
|
||||
Use `gun:options/2,3` to request information about a resource.
|
||||
|
||||
.OPTIONS "/organizations/ninenines"
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:options(ConnPid, "/organizations/ninenines").
|
||||
----
|
||||
|
||||
.OPTIONS "/organizations/ninenines" with custom headers
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:options(ConnPid, "/organizations/ninenines", [
|
||||
{<<"user-agent">>, "revolver/1.0"}
|
||||
]).
|
||||
----
|
||||
|
||||
You can also use this function to request information about
|
||||
the server itself.
|
||||
|
||||
.OPTIONS "*"
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:options(ConnPid, "*").
|
||||
----
|
||||
|
||||
==== Requests with an arbitrary method
|
||||
|
||||
The functions `gun:headers/4,5` or `gun:request/5,6` can be
|
||||
used to send requests with a configurable method name. It is
|
||||
mostly useful when you need a method that Gun does not
|
||||
understand natively.
|
||||
|
||||
.Example of a TRACE request
|
||||
[source,erlang]
|
||||
----
|
||||
gun:request(ConnPid, "TRACE", "/", [
|
||||
{<<"max-forwards">>, "30"}
|
||||
], <<>>).
|
||||
----
|
||||
|
||||
=== Processing responses
|
||||
|
||||
All data received from the server is sent to the calling
|
||||
process as a message. First a `gun_response` message is sent,
|
||||
followed by zero or more `gun_data` messages. If something goes wrong,
|
||||
a `gun_error` message is sent instead.
|
||||
|
||||
The response message will inform you whether there will be
|
||||
data messages following. If it contains `fin` there will be
|
||||
no data messages. If it contains `nofin` then one or more data
|
||||
messages will follow.
|
||||
|
||||
When using HTTP/2 this value is sent with the frame and simply
|
||||
passed on in the message. When using HTTP/1.1 however Gun must
|
||||
guess whether data will follow by looking at the response headers.
|
||||
|
||||
You can receive messages directly, or you can use the _await_
|
||||
functions to let Gun receive them for you.
|
||||
|
||||
.Receiving a response using receive
|
||||
[source,erlang]
|
||||
----
|
||||
print_body(ConnPid, MRef) ->
|
||||
StreamRef = gun:get(ConnPid, "/"),
|
||||
receive
|
||||
{gun_response, ConnPid, StreamRef, fin, Status, Headers} ->
|
||||
no_data;
|
||||
{gun_response, ConnPid, StreamRef, nofin, Status, Headers} ->
|
||||
receive_data(ConnPid, MRef, StreamRef);
|
||||
{'DOWN', MRef, process, ConnPid, Reason} ->
|
||||
error_logger:error_msg("Oops!"),
|
||||
exit(Reason)
|
||||
after 1000 ->
|
||||
exit(timeout)
|
||||
end.
|
||||
|
||||
receive_data(ConnPid, MRef, StreamRef) ->
|
||||
receive
|
||||
{gun_data, ConnPid, StreamRef, nofin, Data} ->
|
||||
io:format("~s~n", [Data]),
|
||||
receive_data(ConnPid, MRef, StreamRef);
|
||||
{gun_data, ConnPid, StreamRef, fin, Data} ->
|
||||
io:format("~s~n", [Data]);
|
||||
{'DOWN', MRef, process, ConnPid, Reason} ->
|
||||
error_logger:error_msg("Oops!"),
|
||||
exit(Reason)
|
||||
after 1000 ->
|
||||
exit(timeout)
|
||||
end.
|
||||
----
|
||||
|
||||
While it may seem verbose, using messages like this has the
|
||||
advantage of never locking your process, allowing you to
|
||||
easily debug your code. It also allows you to start more than
|
||||
one connection and concurrently perform queries on all of them
|
||||
at the same time.
|
||||
|
||||
You can also use Gun in a synchronous manner by using the _await_
|
||||
functions.
|
||||
|
||||
The `gun:await/2,3,4` function will wait until it receives
|
||||
a response to, a pushed resource related to, or data from
|
||||
the given stream.
|
||||
|
||||
When calling `gun:await/2,3` and not passing a monitor
|
||||
reference, one is automatically created for you for the
|
||||
duration of the call.
|
||||
|
||||
The `gun:await_body/2,3,4` works similarly, but returns the
|
||||
body received. Both functions can be combined to receive the
|
||||
response and its body sequentially.
|
||||
|
||||
.Receiving a response using await
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:get(ConnPid, "/"),
|
||||
case gun:await(ConnPid, StreamRef) of
|
||||
{response, fin, Status, Headers} ->
|
||||
no_data;
|
||||
{response, nofin, Status, Headers} ->
|
||||
{ok, Body} = gun:await_body(ConnPid, StreamRef),
|
||||
io:format("~s~n", [Body])
|
||||
end.
|
||||
----
|
||||
|
||||
=== Handling streams pushed by the server
|
||||
|
||||
The HTTP/2 protocol allows the server to push more than one
|
||||
resource for every request. It will start sending those
|
||||
extra resources before it starts sending the response itself,
|
||||
so Gun will send you `gun_push` messages before `gun_response`
|
||||
when that happens.
|
||||
|
||||
You can safely choose to ignore `gun_push` messages, or
|
||||
you can handle them. If you do, you can either receive the
|
||||
messages directly or use _await_ functions.
|
||||
|
||||
The `gun_push` message contains both the new stream reference
|
||||
and the stream reference of the original request.
|
||||
|
||||
.Receiving a pushed response using receive
|
||||
[source,erlang]
|
||||
----
|
||||
receive
|
||||
{gun_push, ConnPid, OriginalStreamRef, PushedStreamRef,
|
||||
Method, Host, Path, Headers} ->
|
||||
enjoy()
|
||||
end.
|
||||
----
|
||||
|
||||
If you use the `gun:await/2,3,4` function, however, Gun
|
||||
will use the original reference to identify the message but
|
||||
will return a tuple that doesn't contain it.
|
||||
|
||||
.Receiving a pushed response using await
|
||||
[source,erlang]
|
||||
----
|
||||
{push, PushedStreamRef, Method, URI, Headers}
|
||||
= gun:await(ConnPid, OriginalStreamRef).
|
||||
----
|
||||
|
||||
The `PushedStreamRef` variable can then be used with `gun:await/2,3,4`
|
||||
and `gun:await_body/2,3,4`.
|
||||
|
||||
=== Flushing unwanted messages
|
||||
|
||||
Gun provides the function `gun:flush/1` to quickly get rid
|
||||
of unwanted messages sitting in the process mailbox. You
|
||||
can use it to get rid of all messages related to a connection,
|
||||
or just the messages related to a stream.
|
||||
|
||||
.Flush all messages from a Gun connection
|
||||
[source,erlang]
|
||||
----
|
||||
gun:flush(ConnPid).
|
||||
----
|
||||
|
||||
.Flush all messages from a specific stream
|
||||
[source,erlang]
|
||||
----
|
||||
gun:flush(StreamRef).
|
||||
----
|
||||
|
||||
=== Redirecting responses to a different process
|
||||
|
||||
Gun allows you to specify which process will handle responses
|
||||
to a request via the `reply_to` request option.
|
||||
|
||||
.GET "/organizations/ninenines" to a different process
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:get(ConnPid, "/organizations/ninenines", [],
|
||||
#{reply_to => Pid}).
|
||||
----
|
|
@ -0,0 +1,177 @@
|
|||
== Internals: TLS over TLS
|
||||
|
||||
The `ssl` application that comes with Erlang/OTP implements
|
||||
TLS using an interface equivalent to the `gen_tcp` interface:
|
||||
you get and manipulate a socket. The TLS encoding and
|
||||
decoding is applied transparently to the data sent and
|
||||
received.
|
||||
|
||||
In order to have a TLS layer inside another TLS layer we
|
||||
need a way to encode the data of the inner layer before
|
||||
we pass it to the outer layer. We cannot do this with
|
||||
a socket interface. Thankfully, the `ssl` application comes
|
||||
with options that allow to transform an `sslsocket()` into
|
||||
an encoder/decoder.
|
||||
|
||||
The implementation is however a little convoluted as a
|
||||
result. This chapter aims to give an overview of how it
|
||||
all works under the hood.
|
||||
|
||||
=== gun_tls_proxy
|
||||
|
||||
The module `gun_tls_proxy` implements an intermediary process
|
||||
that sits between the Gun process and the TLS process. It is
|
||||
responsible for routing data from the Gun process to the TLS
|
||||
process, and from the TLS process to the Gun process.
|
||||
|
||||
In order to obtain the TLS encoded data the `cb_info` option
|
||||
is given to the `ssl:connect/3` function. This replaces the
|
||||
default TCP outer socket module with our own custom module.
|
||||
Gun uses the `gun_tls_proxy_cb` module instead. This module
|
||||
will forward all messages to the `gun_tls_proxy` process.
|
||||
|
||||
The resulting operations looks like this:
|
||||
|
||||
----
|
||||
Gun process <-> gun_tls_proxy <-> sslsocket() <-> gun_tls_proxy <-> "inner socket"
|
||||
----
|
||||
|
||||
The "inner socket" is the socket for the Gun connection.
|
||||
The `gun_tls_proxy` process decides where to send or
|
||||
receive the data based on where it's coming from. This
|
||||
is how it knows whether the data has been encoded/decoded
|
||||
or not.
|
||||
|
||||
Because the `ssl:connect/3` function call is blocking,
|
||||
a temporary process is used while connecting. This is required
|
||||
because the `gun_tls_proxy` needs to forward data even while
|
||||
performing the TLS handshake, otherwise the `ssl:connect/3`
|
||||
call will not complete.
|
||||
|
||||
The result of the `ssl:connect/3` call is forward to the Gun
|
||||
process, along with the negotiated protocols when the connection
|
||||
was successful.
|
||||
|
||||
The `gun_tls_proxy_cb` module does not actually implement
|
||||
`{active,N}` as requested by the `ssl` application. Instead
|
||||
it uses `{active,true}`.
|
||||
|
||||
The behavior of the `gun_tls_proxy` process will vary depending
|
||||
on whether the TLS over TLS is done connection-wide or only
|
||||
stream-wide.
|
||||
|
||||
=== Connection-wide TLS over TLS
|
||||
|
||||
When used for the entire connection, the `gun_tls_proxy` process
|
||||
will act like a real socket once connected. The only difference
|
||||
is how the connection is performed. As mentioned earlier, the
|
||||
result of the `ssl:connect/3` call is sent back to the Gun process.
|
||||
|
||||
When doing TLS over TLS the processes will end up looking like
|
||||
this:
|
||||
|
||||
----
|
||||
Gun process <-> gun_tls_proxy <-> "inner socket"
|
||||
----
|
||||
|
||||
The details of the interactions between `gun_tls_proxy` and
|
||||
its associated `sslsocket()` have been removed in order to
|
||||
better illustrate the concept.
|
||||
|
||||
When adding another layer this becomes:
|
||||
|
||||
----
|
||||
Gun process <-> gun_tls_proxy <-> gun_tls_proxy <-> sslsocket()
|
||||
----
|
||||
|
||||
This is what is done when only HTTP/1.1 and SOCKS proxies are
|
||||
involved.
|
||||
|
||||
=== Stream-wide TLS over TLS
|
||||
|
||||
The same cannot be done for HTTP/2 proxies. This is because the
|
||||
HTTP/2 CONNECT method does not apply to the entire connection,
|
||||
but only to a stream. The proxied data must be wrapped inside
|
||||
a DATA frame. It cannot be sent directly. This is what must be
|
||||
done:
|
||||
|
||||
----
|
||||
Gun process -> gun_tls_proxy -> Gun process -> "inner socket"
|
||||
----
|
||||
|
||||
The "inner socket" is the socket for the HTTP/2 connection.
|
||||
|
||||
In order to tell Gun to continue processing the data, the
|
||||
`handle_continue` mechanism is introduced. When `gun_tls_proxy`
|
||||
has TLS encoded the data it sends it back to the Gun process,
|
||||
wrapped in a `handle_continue` tuple. This tuple contains
|
||||
enough information to figure out what stream the data belongs
|
||||
to and what should be done next. Gun will therefore route the
|
||||
data to the correct layer and continue sending it.
|
||||
|
||||
This solution is also used for receiving data, except in the
|
||||
reverse order.
|
||||
|
||||
=== Routing to the right stream
|
||||
|
||||
In order to know where to route the data, the `stream_ref`
|
||||
had to be modified to contain all the references to the
|
||||
individual streams. So if the tunnel is identified by
|
||||
`StreamA` and a request on this tunnel is identified
|
||||
by `StreamB`, then the stream is known as `[StreamA, StreamB]`
|
||||
to the user. Gun then routes first to `StreamA`, a
|
||||
tunnel, which continues routing to `StreamB`.
|
||||
|
||||
A problem comes up if an intermediary is a SOCKS server,
|
||||
for example in the following scenario:
|
||||
|
||||
----
|
||||
HTTP/2 proxy <-> SOCKS proxy <-> HTTP/1.1 origin
|
||||
----
|
||||
|
||||
The SOCKS protocol doesn't have a concept of stream,
|
||||
therefore when we refer to request to the origin server
|
||||
they are `[StreamA, StreamB]`, not `[StreamA, StreamB, StreamC]`.
|
||||
This is a problem for routing encoded/decoded TLS data
|
||||
to the SOCKS layer: we don't have a built-in way of referring
|
||||
to the SOCKS layer.
|
||||
|
||||
The solution is to have a separate `handle_continue_stream_ref`
|
||||
value that assigns a reference to the SOCKS layers. Gun will
|
||||
then be able to forward `handle_continue` message, and only
|
||||
them, to the appropriate layer.
|
||||
|
||||
Gun therefore has two different routing avenues, but the
|
||||
mechanism remains the same otherwise.
|
||||
|
||||
=== gun_tunnel
|
||||
|
||||
In order to simplify the routing, the `gun_tunnel` module
|
||||
was introduced. For each intermediary (including the original
|
||||
CONNECT stream at the HTTP/2 layer) there is a `gun_tunnel`
|
||||
module.
|
||||
|
||||
Going back to the example above:
|
||||
|
||||
----
|
||||
HTTP/2 proxy <-> SOCKS proxy <-> HTTP/1.1 origin
|
||||
----
|
||||
|
||||
In this case the modules involved to handle the request
|
||||
will be as follow:
|
||||
|
||||
----
|
||||
gun_http2 <-> gun_tunnel <-> gun_tunnel <-> gun_http
|
||||
----
|
||||
|
||||
The `gun_http2` module doesn't do any routing, it just
|
||||
passes everything to the `gun_tunnel` module. The `gun_tunnel`
|
||||
module will then do the routing, which involves removing
|
||||
`StreamA` from `[StreamA, StreamB]` where appropriate.
|
||||
|
||||
The `gun_tunnel` module also receives the TLS encoded/decoded
|
||||
data and forwards it appropriately. When it comes to sending
|
||||
data, it will return a `send` command that allows the previous
|
||||
module to continue sending the data. The `gun_http2` module
|
||||
will ultimately wrap the data to be sent in a DATA frame and
|
||||
send it to the "inner socket".
|
|
@ -0,0 +1,49 @@
|
|||
[[introduction]]
|
||||
== Introduction
|
||||
|
||||
Gun is an HTTP client for Erlang/OTP.
|
||||
|
||||
Gun supports the HTTP/2, HTTP/1.1 and Websocket protocols.
|
||||
|
||||
=== Prerequisites
|
||||
|
||||
Knowledge of Erlang, but also of the HTTP/1.1, HTTP/2 and Websocket
|
||||
protocols is required in order to read this guide.
|
||||
|
||||
=== Supported platforms
|
||||
|
||||
Gun is tested and supported on Linux, FreeBSD, Windows and OSX.
|
||||
|
||||
Gun is developed for Erlang/OTP 22.0 and newer.
|
||||
|
||||
=== License
|
||||
|
||||
Gun uses the ISC License.
|
||||
|
||||
----
|
||||
Copyright (c) 2013-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
----
|
||||
|
||||
=== Versioning
|
||||
|
||||
Gun uses http://semver.org/[Semantic Versioning 2.0.0].
|
||||
|
||||
=== Conventions
|
||||
|
||||
In the HTTP protocol, the method name is case sensitive. All standard
|
||||
method names are uppercase.
|
||||
|
||||
Header names are case insensitive. Gun converts all the header names
|
||||
to lowercase, including request headers provided by your application.
|
|
@ -0,0 +1,21 @@
|
|||
[appendix]
|
||||
== Migrating from Gun 1.0 to 1.1
|
||||
|
||||
Gun 1.1 updates the Cowlib dependency to 2.5.1 and fixes a
|
||||
few problems with experimental features.
|
||||
|
||||
=== Features added
|
||||
|
||||
* Update Cowlib to 2.5.1
|
||||
|
||||
=== Bugs fixed
|
||||
|
||||
* A bug in the experimental `gun_sse_h` where lone id lines
|
||||
were not propagated has been fixed by updating the Cowlib
|
||||
dependency.
|
||||
|
||||
* The status code was incorrectly given to the experimental
|
||||
content handlers as a binary. It has been fixed an an
|
||||
integer is now given as was intended.
|
||||
|
||||
* A number of Dialyzer warnings have been fixed.
|
|
@ -0,0 +1,28 @@
|
|||
[appendix]
|
||||
== Migrating from Gun 1.1 to 1.2
|
||||
|
||||
Gun 1.2 adds support for the CONNECT request over HTTP/1.1
|
||||
connections.
|
||||
|
||||
=== Features added
|
||||
|
||||
* CONNECT requests can now be issued on HTTP/1.1 connections.
|
||||
The tunneled connection can use any of the protocols Gun
|
||||
supports: HTTP/1.1, HTTP/2 and Websocket over both TCP and
|
||||
TLS transports. Note that Gun currently does not support
|
||||
tunneling a TLS connection over a TLS connection due to
|
||||
limitations in Erlang/OTP.
|
||||
|
||||
* Gun supports sending multiple CONNECT requests, allowing
|
||||
the tunnel to the origin server to go through multiple
|
||||
proxies.
|
||||
|
||||
* Gun supports sending CONNECT requests with authorization
|
||||
credentials using the Basic authentication mechanism.
|
||||
|
||||
* Update Cowlib to 2.6.0
|
||||
|
||||
=== Functions added
|
||||
|
||||
* The functions `gun:connect/2,3,4` have been added. They can
|
||||
be used to initiate CONNECT requests on HTTP/1.1 connections.
|
|
@ -0,0 +1,39 @@
|
|||
[appendix]
|
||||
== Migrating from Gun 1.2 to 1.3
|
||||
|
||||
Gun 1.3 improves the support for CONNECT requests
|
||||
introduced in the previous version and documents
|
||||
Websocket protocol negotiation.
|
||||
|
||||
=== Features added
|
||||
|
||||
* The `protocols` CONNECT destination option has been added
|
||||
as a replacement for the now deprecated `protocol` option.
|
||||
|
||||
* Add built-in support for Websocket protocol negotiation
|
||||
through the Websocket option `protocols`. The interface
|
||||
of the handler module currently remains undocumented and
|
||||
must be set to `gun_ws_h`.
|
||||
|
||||
* Add the h2specd HTTP/2 test suite from the h2spec project.
|
||||
|
||||
=== Bugs fixed
|
||||
|
||||
* Fix connecting to HTTP/2 over TLS origin servers via
|
||||
HTTP/1.1 CONNECT proxies.
|
||||
|
||||
* Do not send the HTTP/1.1 keepalive while waiting for
|
||||
a response to a CONNECT request.
|
||||
|
||||
* Do not crash on HTTP/2 HEADERS frames with the
|
||||
PRIORITY flag set.
|
||||
|
||||
* Do not crash on HTTP/2 HEADERS frames when the
|
||||
END_HEADERS flag is not set.
|
||||
|
||||
* Do not crash on unknown HTTP/2 frame types.
|
||||
|
||||
* Reject HTTP/2 WINDOW_UPDATE frames when they would
|
||||
cause the window to overflow.
|
||||
|
||||
* Send a GOAWAY frame on closing the HTTP/2 connection.
|
|
@ -0,0 +1,329 @@
|
|||
[appendix]
|
||||
== Migrating from Gun 1.3 to 2.0
|
||||
|
||||
Gun 2.0 includes state of the art tunnel support. With
|
||||
Gun 2.0 it is possible to make requests or data go through
|
||||
any number of proxy endpoints using any combination of
|
||||
TCP or TLS transports and HTTP/1.1, HTTP/2 or SOCKS5
|
||||
protocols. All combinations of the scenario Proxy1 ->
|
||||
Proxy2 -> Origin are tested and known to work.
|
||||
|
||||
Gun 2.0 adds many more features such as Websocket over
|
||||
HTTP/2, a built-in cookie store, graceful shutdown, flow
|
||||
control for data messages, event handlers and more.
|
||||
|
||||
Gun 2.0 also introduces an experimental pool module that
|
||||
automatically maintains connections and routes requests
|
||||
to the right process, in a similar way as browsers do.
|
||||
|
||||
Gun 2.0 greatly improves the HTTP/2 performance when it
|
||||
comes to receiving large response bodies; and when receiving
|
||||
response bodies from many separate requests concurrently.
|
||||
|
||||
Gun now shares much of its HTTP/2 code with Cowboy,
|
||||
including the HTTP/2 state machine. Numerous issues were
|
||||
fixed as a result because the Cowboy implementation was
|
||||
much more advanced.
|
||||
|
||||
The Gun connection process is now implemented using `gen_statem`.
|
||||
|
||||
Gun 2.0 requires Erlang/OTP 22.0 or greater.
|
||||
|
||||
=== Features added
|
||||
|
||||
* Cookie store support has been added. The `cookie_store`
|
||||
option allows configuring the cookie store backend.
|
||||
The `gun_cookies` module provides functions to help
|
||||
implementing such a backend. Gun comes with the backend
|
||||
`gun_cookies_list` which provides a per-connection,
|
||||
non-persistent cookie store. The cookie store engine
|
||||
implements the entire RFC6265bis draft algorithms except
|
||||
the parts about non-HTTP cookies as no such interface is
|
||||
provided; and the parts about SameSite as Gun has no
|
||||
concept of "browsing context".
|
||||
|
||||
* Graceful shutdown has been implemented. Graceful shutdown
|
||||
can be initiated on the client side by calling the new
|
||||
function `gun:shutdown/1` or when the owner process goes
|
||||
away; or on the peer side via the connection: close HTTP/1.1
|
||||
header, the HTTP/2 GOAWAY frame or the Websocket close frame.
|
||||
Gun will try to complete existing streams when possible;
|
||||
other streams get canceled immediately. The `closing_timeout`
|
||||
option controls how long we are willing to wait at most
|
||||
before closing the connection.
|
||||
|
||||
* Gun will better detect connection failures by checking the
|
||||
return value from sending data to the socket. This applies
|
||||
to all supported protocols. In addition, Gun now enables
|
||||
`send_timeout_close` with a `send_timeout` value defaulting
|
||||
to 15s.
|
||||
|
||||
* Flow control has been added. It allows limiting the number
|
||||
of data/Websocket messages Gun sends to the calling process.
|
||||
Gun will stop reading from the socket or stop updating the
|
||||
protocol's flow control window when applicable as well, to
|
||||
apply some backpressure to the remote endpoint(s). It is
|
||||
disabled by default and can be applied on a per-request
|
||||
basis if necessary.
|
||||
|
||||
* An event handler interface has been added providing access
|
||||
to many internal Gun events. This can be used for a variety
|
||||
of purposes including logging, tracing or otherwise
|
||||
instrumenting a Gun connection.
|
||||
|
||||
* In order to get separate events when connecting, the domain
|
||||
lookup, connection and TLS handshakes are now performed
|
||||
separately by Gun. As a result, there exists three separate
|
||||
timeout options for each of the steps, and the transport
|
||||
options had to be split into `tcp_opts` and `tls_opts`.
|
||||
|
||||
* Gun now supports connecting through SOCKS proxies,
|
||||
including secure SOCKS proxies. Both unauthenticated
|
||||
and username/password authentication are supported.
|
||||
|
||||
* Gun can connect through any number of HTTP, HTTPS, SOCKS
|
||||
or secure SOCKS proxies, including SOCKS proxies
|
||||
located after HTTP(S) proxies. The ultimate endpoint
|
||||
may be using any protocol, including plain TCP, TLS,
|
||||
HTTP/1.1 or HTTP/2.
|
||||
|
||||
* When specifying which protocols to use, options can
|
||||
now be provided specific to those protocols. It is
|
||||
now possible to have separate HTTP options for an
|
||||
HTTP proxy and the origin HTTP server, for example.
|
||||
See the new `gun:protocols()` type for details.
|
||||
|
||||
* Gun can now be used to send and receive raw data,
|
||||
as if it was just a normal socket. This can be
|
||||
useful when needing to connect through a number
|
||||
of HTTP/Socks proxies, allowing the use of Gun's
|
||||
great proxying capabilities (including TLS over TLS)
|
||||
for any sort of protocols. This can also be useful
|
||||
when performing HTTP/1.1 Upgrade to custom protocols.
|
||||
|
||||
* Headers can now be provided as a map.
|
||||
|
||||
* Header names may now be provided as binary, string or atom.
|
||||
|
||||
* Gun now automatically lowercases provided header names.
|
||||
|
||||
* Many HTTP/2 options have been added, allowing great
|
||||
control over how Gun and the remote endpoint are
|
||||
using the HTTP/2 connection. They can be used to
|
||||
improve performance or lower the memory usage, for
|
||||
example.
|
||||
|
||||
* A new `keepalive_tolerance` option for HTTP/2 enables
|
||||
closing the connection automatically when ping acks
|
||||
are not received in a timely manner. It nicely
|
||||
complements the `keepalive` option that makes Gun
|
||||
send pings.
|
||||
|
||||
* Gun now supports Websocket subprotocol negotiation
|
||||
and the feature is fully documented and tested.
|
||||
This can be used to create handlers that will
|
||||
implement a protocol from within the Gun process itself.
|
||||
The negotiation is enabled by setting the `protocols`
|
||||
setting. The `default_protocol` and `user_opts`
|
||||
settings are also useful.
|
||||
|
||||
* It is now possible to send many Websocket frames in
|
||||
a single `gun:ws_send/3` call.
|
||||
|
||||
* Gun may now send Websocket ping frames automatically
|
||||
at intervals determined by the `keepalive` option. It
|
||||
is disabled by default.
|
||||
|
||||
* A new `silence_pings` option can be set to `false` to
|
||||
receive all ping and pong frames when using Websocket.
|
||||
They are typically not needed and therefore silent by
|
||||
default.
|
||||
|
||||
* The `reply_to` option has been added to `gun:ws_upgrade/4`.
|
||||
The option applies to both the response and subsequent
|
||||
Websocket frames.
|
||||
|
||||
* The `reply_to` option is also propagated to messages
|
||||
following a CONNECT request when the protocol requested
|
||||
is not HTTP.
|
||||
|
||||
* A new option `retry_fun` can be used to implement
|
||||
different backoff strategies when reconnecting.
|
||||
|
||||
* A new option `supervise` can be used to start a Gun
|
||||
connection without using Gun's supervisor. It defaults
|
||||
to `true`.
|
||||
|
||||
* Many improvements have been done to postpone or reject
|
||||
requests and other operations while in the wrong state
|
||||
(for example during state transitions when switching
|
||||
protocols or connecting to proxies).
|
||||
|
||||
* Update Cowlib to 2.12.0.
|
||||
|
||||
=== Experimental features added
|
||||
|
||||
* The `gun_pool` module was introduced. Its interface
|
||||
is very similar to the `gun` module, but as it is an
|
||||
experimental feature, it has not been documented yet.
|
||||
The intent is to obtain feedback and document it in
|
||||
an upcoming minor release. Pools are created for each
|
||||
authority (host/port) and scope (user-defined value)
|
||||
pairs and are resolved accordingly using the information
|
||||
provided in the request and request options. Connections
|
||||
may concurrently handle multiple requests/responses
|
||||
from as many different processes as required.
|
||||
|
||||
=== Features removed
|
||||
|
||||
* Gun used to reject operations by processes that were not
|
||||
the owner of the connection. This behavior has been removed.
|
||||
In general the caller of a request or other operation will
|
||||
receive the relevant messages unless the `reply_to` option
|
||||
is used.
|
||||
|
||||
* The `connect_destination()` option `protocol` has been
|
||||
removed. It was previously deprecated in favor of `protocols`.
|
||||
|
||||
* The `keepalive` timeout is now disabled by default
|
||||
for HTTP/1.1 and HTTP/2. To be perfectly clear, this
|
||||
is unrelated to the HTTP/1.1 keep-alive mechanism.
|
||||
|
||||
=== Functions added
|
||||
|
||||
* The function `gun:set_owner/2` has been added. It allows
|
||||
changing the owner of a connection process. Only the current
|
||||
owner can do this operation.
|
||||
|
||||
* The function `gun:shutdown/1` has been added. It initiates
|
||||
the graceful shutdown of the connection, followed by the
|
||||
termination of the Gun process.
|
||||
|
||||
* The function `gun:stream_info/2` has been added. It provides
|
||||
information about a specific HTTP stream.
|
||||
|
||||
=== Functions modified
|
||||
|
||||
* The function `gun:info/1` now returns the owner of the
|
||||
connection as well as the cookie store.
|
||||
|
||||
* The functions `gun:await/2,3,4`, `gun:await_body/2,3,4` and
|
||||
`gun:await_up/1,2,3` now distinguish the error types. They
|
||||
can be a timeout, a connection error, a stream error or a
|
||||
down error (when the Gun process exited while waiting).
|
||||
|
||||
* The functions `gun:await/2,3,4` will now receive upgrades,
|
||||
tunnel up and Websocket messages and return them.
|
||||
|
||||
* Requests may now include the `tunnel` option to send the
|
||||
request on a specific tunnel.
|
||||
|
||||
* The functions `gun:request/4,5,6` have been replaced with
|
||||
`gun:headers/4,5` and `gun:request/5,6`. This provides a
|
||||
cleaner separation between requests that are followed by
|
||||
a body and those that aren't.
|
||||
|
||||
* The function `gun:ws_send/2` has been replaced with the
|
||||
function `gun:ws_send/3`. The stream reference for the
|
||||
corresponding Websocket upgrade request must now be given.
|
||||
|
||||
=== Messages added
|
||||
|
||||
* The `gun_tunnel_up` message has been added.
|
||||
|
||||
=== Messages modified
|
||||
|
||||
* The `gun_down` message no longer has its final element
|
||||
documented as `UnprocessedStreams`. It never worked and
|
||||
was always an empty list.
|
||||
|
||||
=== Bugs fixed
|
||||
|
||||
* *POTENTIAL SECURITY VULNERABILITY*: Fix transfer-encoding
|
||||
precedence over content-length in responses. This bug may
|
||||
contribute to a response smuggling security vulnerability
|
||||
when Gun is used inside a proxy.
|
||||
|
||||
* Gun will now better detect connection closes in some cases.
|
||||
|
||||
* Gun will no longer send duplicate connection-wide `gun_error`
|
||||
messages to the same process.
|
||||
|
||||
* Gun no longer crashes when trying to upgrade to Websocket
|
||||
over a connection restricted to HTTP/1.0.
|
||||
|
||||
* The default value for the preferred protocols when using
|
||||
CONNECT over TLS has been corrected. It was mistakenly not
|
||||
enabling HTTP/2.
|
||||
|
||||
* Protocol options provided for a tunnel destination were
|
||||
sometimes ignored. This should no longer be the case.
|
||||
|
||||
* Gun will no longer send an empty HTTP/2 DATA frame when
|
||||
there is no request body. It was not necessary.
|
||||
|
||||
* Gun will no longer error out when the owner process exits.
|
||||
The error reason will now be a `shutdown` tuple instead.
|
||||
|
||||
* The host header was set incorrectly during Websocket upgrades
|
||||
when the host was configured with an IP address, resulting
|
||||
in a crash. This has been corrected.
|
||||
|
||||
* A completed stream could be found in the `gun_down` message when
|
||||
the response contained a connection: close header. This is no
|
||||
longer the case.
|
||||
|
||||
* Hostnames can now be provided as atom as stated by the
|
||||
documentation.
|
||||
|
||||
* Gun will no longer attempt to send empty data chunks. When
|
||||
using HTTP/1.1 chunked transfer-encoding this caused the
|
||||
request body to end, even when `nofin` was given.
|
||||
|
||||
* Gun now always retries connecting immediately when the
|
||||
connection goes down.
|
||||
|
||||
* The default port number for the HTTP and HTTPS schemes is
|
||||
no longer sent in the host header.
|
||||
|
||||
* An invalid stream reference was sent on failed Websocket
|
||||
upgrade responses. This has been corrected.
|
||||
|
||||
* HTTP/2 connection preface errors are now properly detected
|
||||
and propagated in the `gun_down` message to the connection
|
||||
owner as well as the exit reason of the Gun process.
|
||||
|
||||
* HTTP/2 connection preface errors now provide a different
|
||||
human readable error when the data received looks like an
|
||||
HTTP/1.x response.
|
||||
|
||||
* HTTP/2 connection errors were missing the human readable
|
||||
reason in the `gun_error` message. This has been corrected.
|
||||
|
||||
* Fix the host and :authority (pseudo-)headers when connecting
|
||||
to an IPv6 address given as a tuple. They were lacking the
|
||||
surrounding brackets.
|
||||
|
||||
* Fix a crash in gun:info/1 when the socket was closed before
|
||||
we call Transport:sockname/1.
|
||||
|
||||
* Fix flushing by stream reference. When the `gun_inform`
|
||||
message was flushed the function would switch to flushing
|
||||
all messages from the pid instead of only messages from
|
||||
the given stream.
|
||||
|
||||
* Allow setting a custom SNI value.
|
||||
|
||||
* Fix double sending of last chunk in HTTP/1.1 when Gun is
|
||||
asked to send empty data before closing the stream.
|
||||
|
||||
* Gun will now properly ignore parameters when the media
|
||||
type is text/event-stream.
|
||||
|
||||
* Avoid noisy crashes in the TLS over TLS code.
|
||||
|
||||
* Gun will now include the StreamRef of Websocket streams
|
||||
when sending `gun_down` messages.
|
||||
|
||||
* Gun will no longer reject HTTP proxies that use HTTP/1.0
|
||||
for the version in their response.
|
|
@ -0,0 +1,25 @@
|
|||
[appendix]
|
||||
== Migrating from Gun 2.0 to 2.1
|
||||
|
||||
Gun 2.1 contains a small security improvement for
|
||||
the HTTP/2 protocol, as well as includes a small
|
||||
number of fixes and improvements.
|
||||
|
||||
Gun 2.1 requires Erlang/OTP 22.0 or greater.
|
||||
|
||||
=== Features added
|
||||
|
||||
* A new HTTP/2 option `max_fragmented_header_block_size` has
|
||||
been added to limit the size of header blocks that are
|
||||
sent over multiple HEADERS and CONTINUATION frames.
|
||||
|
||||
* Update Cowlib to 2.13.0.
|
||||
|
||||
=== Bugs fixed
|
||||
|
||||
* Gun will no longer configure the NPN TLS extension,
|
||||
which has long been replaced by ALPN. NPN is not
|
||||
compatible with TLS 1.3.
|
||||
|
||||
* Gun will no longer crash when TLS connections close
|
||||
very early in the connection's life time.
|
|
@ -0,0 +1,120 @@
|
|||
[[protocols]]
|
||||
== Supported protocols
|
||||
|
||||
This chapter describes the protocols supported and the
|
||||
operations available to them.
|
||||
|
||||
=== HTTP/1.1
|
||||
|
||||
HTTP/1.1 is a text request-response protocol. The client
|
||||
sends a request, the server sends back a response.
|
||||
|
||||
Gun provides convenience functions for performing GET, HEAD,
|
||||
OPTIONS, POST, PATCH, PUT, and DELETE requests. All these
|
||||
functions are aliases of `gun:headers/4,5` or `gun:request/5,6`
|
||||
for the respective methods. Gun also provides a `gun:data/4`
|
||||
function for streaming the request body.
|
||||
|
||||
Gun will send a `gun_inform` message for every intermediate
|
||||
informational responses received. They will always be sent
|
||||
before the `gun_response` message.
|
||||
|
||||
Gun will send a `gun_response` message for every response
|
||||
received, followed by zero or more `gun_data` messages for
|
||||
the response body, which is optionally terminated by a
|
||||
`gun_trailers` message. If something goes wrong, a `gun_error`
|
||||
will be sent instead.
|
||||
|
||||
Gun provides convenience functions for dealing with messages.
|
||||
The `gun:await/2,3,4` function waits for a response to the given
|
||||
request, and the `gun:await_body/2,3,4` function for the
|
||||
response body. The `gun:flush/1` function can be used to clear all
|
||||
messages related to a request or a connection from the mailbox
|
||||
of the calling process.
|
||||
|
||||
The function `gun:cancel/2` can be used to silence the
|
||||
response to a request previously sent if it is no longer
|
||||
needed. When using HTTP/1.1 there is no multiplexing so
|
||||
Gun will have to receive the response fully before any
|
||||
other responses can be received.
|
||||
|
||||
Finally, Gun can upgrade an HTTP/1.1 connection to Websocket.
|
||||
It provides the `gun:ws_upgrade/2,3,4` function for that
|
||||
purpose. A `gun_upgrade` message will be sent on success;
|
||||
a `gun_response` message otherwise.
|
||||
|
||||
=== HTTP/2
|
||||
|
||||
HTTP/2 is a binary protocol based on HTTP, compatible with
|
||||
the HTTP semantics, that reduces the complexity of parsing
|
||||
requests and responses, compresses the HTTP headers and
|
||||
allows the server to push additional resources along with
|
||||
the normal response to the original request.
|
||||
|
||||
The HTTP/2 interface is very similar to HTTP/1.1, so this
|
||||
section instead focuses on the differences in the interface
|
||||
for the two protocols.
|
||||
|
||||
Gun will send `gun_push` messages for every push received.
|
||||
They will always be sent before the `gun_response` message.
|
||||
They can be ignored safely if they are not needed, or they
|
||||
can be canceled.
|
||||
|
||||
The `gun:cancel/2` function will use the HTTP/2 stream
|
||||
cancellation mechanism which allows Gun to inform the
|
||||
server to stop sending a response for this particular
|
||||
request, saving resources.
|
||||
|
||||
=== Websocket
|
||||
|
||||
Websocket is a binary protocol built on top of HTTP that
|
||||
allows asynchronous concurrent communication between the
|
||||
client and the server. A Websocket server can push data to
|
||||
the client at any time.
|
||||
|
||||
Once the Websocket connection is established over an HTTP/1.1
|
||||
connection, the only operation available on this connection
|
||||
is sending Websocket frames using `gun:ws_send/3`.
|
||||
|
||||
Gun will send a `gun_ws` message for every frame received.
|
||||
|
||||
=== Summary
|
||||
|
||||
The two following tables summarize the supported operations
|
||||
and the messages Gun sends depending on the connection's
|
||||
current protocol.
|
||||
|
||||
.Supported operations per protocol
|
||||
[cols="<,3*^",options="header"]
|
||||
|===
|
||||
| Operation | HTTP/1.1 | HTTP/2 | Websocket
|
||||
| delete | yes | yes | no
|
||||
| get | yes | yes | no
|
||||
| head | yes | yes | no
|
||||
| options | yes | yes | no
|
||||
| patch | yes | yes | no
|
||||
| post | yes | yes | no
|
||||
| put | yes | yes | no
|
||||
| request | yes | yes | no
|
||||
| data | yes | yes | no
|
||||
| await | yes | yes | no
|
||||
| await_body | yes | yes | no
|
||||
| flush | yes | yes | no
|
||||
| cancel | yes | yes | no
|
||||
| ws_upgrade | yes | yes | no
|
||||
| ws_send | no | no | yes
|
||||
|===
|
||||
|
||||
.Messages sent per protocol
|
||||
[cols="<,3*^",options="header"]
|
||||
|===
|
||||
| Message | HTTP/1.1 | HTTP/2 | Websocket
|
||||
| gun_push | no | yes | no
|
||||
| gun_inform | yes | yes | no
|
||||
| gun_response | yes | yes | no
|
||||
| gun_data | yes | yes | no
|
||||
| gun_trailers | yes | yes | no
|
||||
| gun_error | yes | yes | yes
|
||||
| gun_upgrade | yes | yes | no
|
||||
| gun_ws | no | no | yes
|
||||
|===
|
|
@ -0,0 +1,43 @@
|
|||
[[start]]
|
||||
== Starting and stopping
|
||||
|
||||
This chapter describes how to start and stop the Gun application.
|
||||
|
||||
=== Setting up
|
||||
|
||||
Specify Gun as a dependency to your application in your favorite
|
||||
build tool.
|
||||
|
||||
With Erlang.mk this is done by adding `gun` to the `DEPS` variable
|
||||
in your Makefile.
|
||||
|
||||
.Adding Gun as an Erlang.mk dependency
|
||||
[source,make]
|
||||
----
|
||||
DEPS = gun
|
||||
----
|
||||
|
||||
=== Starting
|
||||
|
||||
Gun is an _OTP application_. It needs to be started before you can
|
||||
use it.
|
||||
|
||||
.Starting Gun in an Erlang shell
|
||||
[source,erlang]
|
||||
----
|
||||
1> application:ensure_all_started(gun).
|
||||
{ok,[crypto,cowlib,asn1,public_key,ssl,gun]}
|
||||
----
|
||||
|
||||
=== Stopping
|
||||
|
||||
You can stop Gun using the `application:stop/1` function, however
|
||||
only Gun will be stopped. This is the reverse of `application:start/1`.
|
||||
The `application_ensure_all_started/1` function has no equivalent for
|
||||
stopping all applications.
|
||||
|
||||
.Stopping Gun
|
||||
[source,erlang]
|
||||
----
|
||||
application:stop(gun).
|
||||
----
|
|
@ -0,0 +1,124 @@
|
|||
[[websocket]]
|
||||
== Websocket
|
||||
|
||||
This chapter describes how to use the Gun client for
|
||||
communicating with a Websocket server.
|
||||
|
||||
// @todo recovering from connection failure, reconnecting to Websocket etc.
|
||||
|
||||
=== HTTP upgrade
|
||||
|
||||
Websocket is a protocol built on top of HTTP. To use Websocket,
|
||||
you must first request for the connection to be upgraded. Only
|
||||
HTTP/1.1 connections can be upgraded to Websocket, so you might
|
||||
need to restrict the protocol to HTTP/1.1 if you are planning
|
||||
to use Websocket over TLS.
|
||||
|
||||
You must use the `gun:ws_upgrade/2,3,4` function to upgrade
|
||||
to Websocket. This function can be called anytime after connection,
|
||||
so you can send HTTP requests before upgrading to Websocket.
|
||||
|
||||
.Upgrade to Websocket
|
||||
[source,erlang]
|
||||
----
|
||||
gun:ws_upgrade(ConnPid, "/websocket").
|
||||
----
|
||||
|
||||
Gun will set all the necessary headers for performing the
|
||||
Websocket upgrade, but you can specify additional headers
|
||||
if needed. For example you can authenticate.
|
||||
|
||||
.Upgrade to Websocket using HTTP authentication
|
||||
[source,erlang]
|
||||
----
|
||||
gun:ws_upgrade(ConnPid, "/websocket", [
|
||||
{<<"authorization">>, "Basic dXNlcm5hbWU6cGFzc3dvcmQ="}
|
||||
]).
|
||||
----
|
||||
|
||||
You can pass the Websocket options as part of the `gun:open/2,3`
|
||||
call when opening the connection, or using the `gun:ws_upgrade/4`.
|
||||
The fourth argument is those same options.
|
||||
|
||||
Gun can negotiate the protocol to be used for the Websocket
|
||||
connection. The `protocols` option can be given with a list
|
||||
of protocols accepted and the corresponding handler module.
|
||||
Note that the interface for handler modules is currently
|
||||
undocumented and must be set to `gun_ws_h`.
|
||||
|
||||
.Upgrade to Websocket with protocol negotiation
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:ws_upgrade(ConnPid, "/websocket", []
|
||||
#{protocols => [{<<"xmpp">>, gun_ws_h}]}).
|
||||
----
|
||||
|
||||
The upgrade will fail if the server cannot satisfy the
|
||||
protocol negotiation.
|
||||
|
||||
When the upgrade succeeds, a `gun_upgrade` message is sent.
|
||||
If the server does not understand Websocket or refused the
|
||||
upgrade, a `gun_response` message is sent. If Gun couldn't
|
||||
perform the upgrade due to an error (for example attempting
|
||||
to upgrade to Websocket on an HTTP/1.0 connection) then a
|
||||
`gun_error` message is sent.
|
||||
|
||||
When the server does not understand Websocket, it may send
|
||||
a meaningful response which should be processed. In the
|
||||
following example we however ignore it:
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
receive
|
||||
{gun_upgrade, ConnPid, StreamRef, [<<"websocket">>], Headers} ->
|
||||
upgrade_success(ConnPid, StreamRef);
|
||||
{gun_response, ConnPid, _, _, Status, Headers} ->
|
||||
exit({ws_upgrade_failed, Status, Headers});
|
||||
{gun_error, ConnPid, StreamRef, Reason} ->
|
||||
exit({ws_upgrade_failed, Reason})
|
||||
%% More clauses here as needed.
|
||||
after 1000 ->
|
||||
exit(timeout)
|
||||
end.
|
||||
----
|
||||
|
||||
=== Sending data
|
||||
|
||||
Once the Websocket upgrade has completed successfully, you no
|
||||
longer have access to functions for performing requests. You
|
||||
can only send and receive Websocket messages.
|
||||
|
||||
Use `gun:ws_send/3` to send messages to the server.
|
||||
|
||||
.Send a text frame
|
||||
[source,erlang]
|
||||
----
|
||||
gun:ws_send(ConnPid, StreamRef, {text, "Hello!"}).
|
||||
----
|
||||
|
||||
.Send a text frame, a binary frame and then close the connection
|
||||
[source,erlang]
|
||||
----
|
||||
gun:ws_send(ConnPid, StreamRef, [
|
||||
{text, "Hello!"},
|
||||
{binary, BinaryValue},
|
||||
close
|
||||
]).
|
||||
----
|
||||
|
||||
Note that if you send a close frame, Gun will close the connection
|
||||
cleanly but may attempt to reconnect afterwards depending on the
|
||||
`retry` configuration.
|
||||
|
||||
=== Receiving data
|
||||
|
||||
Gun sends an Erlang message to the owner process for every
|
||||
Websocket message it receives.
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
receive
|
||||
{gun_ws, ConnPid, StreamRef, Frame} ->
|
||||
handle_frame(ConnPid, StreamRef, Frame)
|
||||
end.
|
||||
----
|
|
@ -0,0 +1,639 @@
|
|||
= gun(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun - Asynchronous HTTP client
|
||||
|
||||
== Description
|
||||
|
||||
The `gun` module provides an asynchronous interface for
|
||||
connecting and communicating with Web servers over HTTP,
|
||||
HTTP/2 or Websocket.
|
||||
|
||||
== Exports
|
||||
|
||||
Connection:
|
||||
|
||||
* link:man:gun:open(3)[gun:open(3)] - Open a connection to the given host and port
|
||||
* link:man:gun:open_unix(3)[gun:open_unix(3)] - Open a connection to the given Unix domain socket
|
||||
* link:man:gun:set_owner(3)[gun:set_owner(3)] - Set a new owner for the connection
|
||||
* link:man:gun:shutdown(3)[gun:shutdown(3)] - Gracefully close the connection
|
||||
* link:man:gun:close(3)[gun:close(3)] - Brutally close the connection
|
||||
* link:man:gun:info(3)[gun:info(3)] - Obtain information about the connection
|
||||
|
||||
Requests:
|
||||
|
||||
* link:man:gun:get(3)[gun:get(3)] - Get a resource representation
|
||||
* link:man:gun:head(3)[gun:head(3)] - Get headers of a resource representation
|
||||
* link:man:gun:options(3)[gun:options(3)] - Query the capabilities of the server or a resource
|
||||
* link:man:gun:patch(3)[gun:patch(3)] - Apply a set of changes to a resource
|
||||
* link:man:gun:post(3)[gun:post(3)] - Process the enclosed representation according to a resource's own semantics
|
||||
* link:man:gun:put(3)[gun:put(3)] - Create or replace a resource
|
||||
* link:man:gun:delete(3)[gun:delete(3)] - Delete a resource
|
||||
* link:man:gun:headers(3)[gun:headers(3)] - Initiate the given request
|
||||
* link:man:gun:request(3)[gun:request(3)] - Perform the given request
|
||||
* link:man:gun:data(3)[gun:data(3)] - Stream the body of a request
|
||||
|
||||
Proxies:
|
||||
|
||||
* link:man:gun:connect(3)[gun:connect(3)] - Establish a tunnel to the origin server
|
||||
|
||||
Messages:
|
||||
|
||||
* link:man:gun:await(3)[gun:await(3)] - Wait for a response
|
||||
* link:man:gun:await_body(3)[gun:await_body(3)] - Wait for the complete response body
|
||||
* link:man:gun:await_up(3)[gun:await_up(3)] - Wait for the connection to be up
|
||||
* link:man:gun:flush(3)[gun:flush(3)] - Flush all messages related to a connection or a stream
|
||||
|
||||
Streams:
|
||||
|
||||
* link:man:gun:update_flow(3)[gun:update_flow(3)] - Update a stream's flow control value
|
||||
* link:man:gun:cancel(3)[gun:cancel(3)] - Cancel the given stream
|
||||
* link:man:gun:stream_info(3)[gun:stream_info(3)] - Obtain information about a stream
|
||||
|
||||
Websocket:
|
||||
|
||||
* link:man:gun:ws_upgrade(3)[gun:ws_upgrade(3)] - Upgrade to Websocket
|
||||
* link:man:gun:ws_send(3)[gun:ws_send(3)] - Send Websocket frames
|
||||
|
||||
== Messages
|
||||
|
||||
Gun will inform the calling process of events asynchronously
|
||||
by sending any of the following messages:
|
||||
|
||||
Connection:
|
||||
|
||||
* link:man:gun_up(3)[gun_up(3)] - The connection is up
|
||||
* link:man:gun_tunnel_up(3)[gun_tunnel_up(3)] - The tunnel is up
|
||||
* link:man:gun_down(3)[gun_down(3)] - The connection is down
|
||||
* link:man:gun_upgrade(3)[gun_upgrade(3)] - Successful protocol upgrade
|
||||
* link:man:gun_error(3)[gun_error(3)] - Stream or connection-wide error
|
||||
|
||||
Responses:
|
||||
|
||||
* link:man:gun_push(3)[gun_push(3)] - Server-initiated push
|
||||
* link:man:gun_inform(3)[gun_inform(3)] - Informational response
|
||||
* link:man:gun_response(3)[gun_response(3)] - Response
|
||||
* link:man:gun_data(3)[gun_data(3)] - Response body
|
||||
* link:man:gun_trailers(3)[gun_trailers(3)] - Response trailers
|
||||
|
||||
Websocket:
|
||||
|
||||
* link:man:gun_ws(3)[gun_ws(3)] - Websocket frame
|
||||
|
||||
The response messages will be sent to the process that opened
|
||||
the connection by default. The `reply_to` request option can
|
||||
be used to redirect request-specific messages to a different
|
||||
process.
|
||||
|
||||
== Types
|
||||
|
||||
=== connect_destination()
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
connect_destination() :: #{
|
||||
host := inet:hostname() | inet:ip_address(),
|
||||
port := inet:port_number(),
|
||||
|
||||
username => iodata(),
|
||||
password => iodata(),
|
||||
protocols => protocols(),
|
||||
transport => tcp | tls,
|
||||
|
||||
tls_opts => [ssl:tls_client_option()],
|
||||
tls_handshake_timeout => timeout()
|
||||
}
|
||||
----
|
||||
|
||||
Destination of a CONNECT request.
|
||||
|
||||
The default value, if any, is given next to the option name:
|
||||
|
||||
host, port::
|
||||
|
||||
Destination hostname and port number. Mandatory.
|
||||
+
|
||||
Upon successful completion of the CONNECT request, Gun will
|
||||
begin using these as the host and port of the origin server
|
||||
for subsequent requests.
|
||||
|
||||
username, password::
|
||||
|
||||
Proxy authorization credentials. They are only sent when
|
||||
both options are provided.
|
||||
|
||||
protocols - see below::
|
||||
|
||||
Ordered list of preferred protocols. Please refer to the
|
||||
`protocols()` type documentation for details.
|
||||
+
|
||||
Defaults to `[http]` when the transport is `tcp`,
|
||||
and `[http2, http]` when the transport is `tls`.
|
||||
|
||||
transport (tcp)::
|
||||
|
||||
Transport that will be used for tunneled requests.
|
||||
|
||||
tls_opts ([])::
|
||||
|
||||
Options to use for tunneled TLS connections.
|
||||
|
||||
tls_handshake_timeout (infinity)::
|
||||
|
||||
Handshake timeout for tunneled TLS connections.
|
||||
|
||||
=== http_opts()
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
http_opts() :: #{
|
||||
closing_timeout => timeout(),
|
||||
cookie_ignore_informational => boolean(),
|
||||
flow => pos_integer(),
|
||||
keepalive => timeout(),
|
||||
transform_header_name => fun((binary()) -> binary()),
|
||||
version => 'HTTP/1.1' | 'HTTP/1.0'
|
||||
}
|
||||
----
|
||||
|
||||
Configuration for the HTTP protocol.
|
||||
|
||||
The default value is given next to the option name:
|
||||
|
||||
// @todo Document content_handlers and gun_sse_h.
|
||||
|
||||
closing_timeout (15000)::
|
||||
|
||||
Time to wait before brutally closing the connection when a
|
||||
graceful shutdown was requested via a call to
|
||||
link:man:gun:shutdown(3)[gun:shutdown(3)].
|
||||
|
||||
cookie_ignore_informational (false)::
|
||||
|
||||
Whether cookies received inside informational responses
|
||||
(1xx status code) must be ignored.
|
||||
|
||||
flow - see below::
|
||||
|
||||
The initial flow control value for all HTTP/1.1 streams.
|
||||
By default flow control is disabled.
|
||||
|
||||
keepalive (infinity)::
|
||||
|
||||
Time between pings in milliseconds. Since the HTTP protocol has
|
||||
no standardized way to ping the server, Gun will simply send an
|
||||
empty line when the connection is idle. Gun only makes a best
|
||||
effort here as servers usually have configurable limits to drop
|
||||
idle connections. Disabled by default due to potential
|
||||
incompatibilities.
|
||||
|
||||
transform_header_name - see below::
|
||||
|
||||
A function that will be applied to all header names before they
|
||||
are sent to the server. Gun assumes that all header names are in
|
||||
lower case. This function is useful if you, for example, need to
|
||||
re-case header names in the event that the server incorrectly
|
||||
considers the case of header names to be significant.
|
||||
|
||||
version (`'HTTP/1.1'`)::
|
||||
|
||||
HTTP version to use.
|
||||
|
||||
=== http2_opts()
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
http2_opts() :: #{
|
||||
closing_timeout => timeout(),
|
||||
cookie_ignore_informational => boolean(),
|
||||
flow => pos_integer(),
|
||||
keepalive => timeout(),
|
||||
keepalive_tolerance => non_neg_integer(),
|
||||
|
||||
%% HTTP/2 state machine configuration.
|
||||
connection_window_margin_size => 0..16#7fffffff,
|
||||
connection_window_update_threshold => 0..16#7fffffff,
|
||||
enable_connect_protocol => boolean(),
|
||||
initial_connection_window_size => 65535..16#7fffffff,
|
||||
initial_stream_window_size => 0..16#7fffffff,
|
||||
max_concurrent_streams => non_neg_integer() | infinity,
|
||||
max_connection_window_size => 0..16#7fffffff,
|
||||
max_decode_table_size => non_neg_integer(),
|
||||
max_encode_table_size => non_neg_integer(),
|
||||
max_fragmented_header_block_size => 16384..16#7fffffff,
|
||||
max_frame_size_received => 16384..16777215,
|
||||
max_frame_size_sent => 16384..16777215 | infinity,
|
||||
max_stream_window_size => 0..16#7fffffff,
|
||||
preface_timeout => timeout(),
|
||||
settings_timeout => timeout(),
|
||||
stream_window_data_threshold => 0..16#7fffffff,
|
||||
stream_window_margin_size => 0..16#7fffffff,
|
||||
stream_window_update_threshold => 0..16#7fffffff
|
||||
}
|
||||
----
|
||||
|
||||
Configuration for the HTTP/2 protocol.
|
||||
|
||||
The default value is given next to the option name:
|
||||
|
||||
// @todo Document content_handlers and gun_sse_h.
|
||||
|
||||
closing_timeout (15000)::
|
||||
|
||||
Time to wait before brutally closing the connection when a
|
||||
graceful shutdown was requested either via a call to
|
||||
link:man:gun:shutdown(3)[gun:shutdown(3)] or by the server.
|
||||
|
||||
cookie_ignore_informational (false)::
|
||||
|
||||
Whether cookies received inside informational responses
|
||||
(1xx status code) must be ignored.
|
||||
|
||||
flow - see below::
|
||||
|
||||
The initial flow control value for all HTTP/2 streams.
|
||||
By default flow control is disabled.
|
||||
|
||||
keepalive (infinity)::
|
||||
|
||||
Time between pings in milliseconds.
|
||||
|
||||
keepalive_tolerance - see below::
|
||||
|
||||
The number of unacknowledged pings in flight that are
|
||||
tolerated before the connection is closed. By default
|
||||
this mechanism is disabled even if `keepalive` is
|
||||
enabled.
|
||||
|
||||
=== opts()
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
opts() :: #{
|
||||
connect_timeout => timeout(),
|
||||
cookie_store => gun_cookies:store(),
|
||||
domain_lookup_timeout => timeout(),
|
||||
http_opts => http_opts(),
|
||||
http2_opts => http2_opts(),
|
||||
protocols => protocols(),
|
||||
retry => non_neg_integer(),
|
||||
retry_fun => fun(),
|
||||
retry_timeout => pos_integer(),
|
||||
supervise => boolean(),
|
||||
tcp_opts => [gen_tcp:connect_option()],
|
||||
tls_handshake_timeout => timeout(),
|
||||
tls_opts => [ssl:tls_client_option()],
|
||||
trace => boolean(),
|
||||
transport => tcp | tls,
|
||||
ws_opts => ws_opts()
|
||||
}
|
||||
----
|
||||
|
||||
Configuration for the connection.
|
||||
|
||||
The default value is given next to the option name:
|
||||
|
||||
connect_timeout (infinity)::
|
||||
|
||||
Connection timeout.
|
||||
|
||||
cookie_store - see below::
|
||||
|
||||
The cookie store that Gun will use for this connection.
|
||||
When configured, Gun will query the store for cookies
|
||||
and include them in the request headers; and add cookies
|
||||
found in response headers to the store.
|
||||
+
|
||||
By default no cookie store will be used.
|
||||
|
||||
domain_lookup_timeout (infinity)::
|
||||
|
||||
Domain lookup timeout.
|
||||
|
||||
http_opts (#{})::
|
||||
|
||||
Options specific to the HTTP protocol.
|
||||
|
||||
http2_opts (#{})::
|
||||
|
||||
Options specific to the HTTP/2 protocol.
|
||||
|
||||
protocols - see below::
|
||||
|
||||
Ordered list of preferred protocols. Please refer to the
|
||||
`protocols()` type documentation for details.
|
||||
+
|
||||
Defaults to `[http]` when the transport is `tcp`,
|
||||
and `[http2, http]` when the transport is `tls`.
|
||||
|
||||
retry (5)::
|
||||
|
||||
Number of times Gun will try to reconnect on failure before giving up.
|
||||
|
||||
retry_fun - see below::
|
||||
|
||||
A fun that will be called before every reconnect attempt. It receives
|
||||
the current number of retries left and the Gun options. It returns the
|
||||
next number of retries left and the timeout to apply before reconnecting.
|
||||
|
||||
The default fun will remove one to the number of retries and set the
|
||||
timeout to the `retry_timeout` value.
|
||||
|
||||
The fun must be defined as follow:
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
fun ((non_neg_integer(), opts()) -> #{
|
||||
retries => non_neg_integer(),
|
||||
timeout => pos_integer()
|
||||
})
|
||||
----
|
||||
|
||||
The fun will never be called when the `retry` option is set to 0. When
|
||||
this function returns 0 in the `retries` value, Gun will do one last
|
||||
reconnect attempt before giving up.
|
||||
|
||||
retry_timeout (5000)::
|
||||
|
||||
Time between retries in milliseconds.
|
||||
|
||||
supervise (true)::
|
||||
|
||||
Whether the Gun process should be started under the `gun_sup`
|
||||
supervisor. Set to `false` to use your own supervisor.
|
||||
|
||||
tcp_opts (DefaultOpts)::
|
||||
|
||||
TCP options used when establishing the connection.
|
||||
By default Gun enables send timeouts with the options
|
||||
`[{send_timeout, 15000}, {send_timeout_close, true}]`.
|
||||
|
||||
tls_handshake_timeout (infinity)::
|
||||
|
||||
TLS handshake timeout.
|
||||
|
||||
tls_opts ([])::
|
||||
|
||||
TLS options used for the TLS handshake after the connection
|
||||
has been established, when the transport is set to `tls`.
|
||||
|
||||
trace (false)::
|
||||
|
||||
Whether to enable `dbg` tracing of the connection process. Should
|
||||
only be used during debugging.
|
||||
|
||||
transport - see below::
|
||||
|
||||
Whether to use TLS or plain TCP. The default varies depending on the
|
||||
port used. Port 443 defaults to `tls`. All other ports default to `tcp`.
|
||||
|
||||
ws_opts (#{})::
|
||||
|
||||
Options specific to the Websocket protocol.
|
||||
|
||||
=== protocols()
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
Protocol :: http | {http, http_opts()}
|
||||
| http2 | {http2, http2_opts()}
|
||||
| raw | {raw, raw_opts()}
|
||||
| socks | {socks, socks_opts()}
|
||||
|
||||
protocols() :: [Protocol]
|
||||
----
|
||||
|
||||
Ordered list of preferred protocols. When the transport is `tcp`,
|
||||
this list must contain exactly one protocol. When the transport
|
||||
is `tls`, this list must contain at least one protocol and will be
|
||||
used to negotiate a protocol via ALPN. When the server does not
|
||||
support ALPN then `http` will be used, except when the list of
|
||||
preferred protocols is set to only accept `socks`.
|
||||
|
||||
Defaults to `[http]` when the transport is `tcp`,
|
||||
and `[http2, http]` when the transport is `tls`.
|
||||
|
||||
=== raw_opts()
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
raw_opts() :: #{
|
||||
}
|
||||
----
|
||||
|
||||
Configuration for the "raw" protocol.
|
||||
|
||||
// The default value is given next to the option name:
|
||||
|
||||
=== req_headers()
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
req_headers() :: [{binary() | string() | atom(), iodata()}]
|
||||
| #{binary() | string() | atom() => iodata()}
|
||||
----
|
||||
|
||||
Request headers.
|
||||
|
||||
=== req_opts()
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
req_opts() :: #{
|
||||
flow => pos_integer(),
|
||||
reply_to => pid()
|
||||
}
|
||||
----
|
||||
|
||||
Configuration for a particular request.
|
||||
|
||||
The default value is given next to the option name:
|
||||
|
||||
flow - see below::
|
||||
|
||||
The initial flow control value for the stream. By default
|
||||
flow control is disabled.
|
||||
|
||||
reply_to (`self()`)::
|
||||
|
||||
The pid of the process that will receive the response messages.
|
||||
|
||||
=== socks_opts()
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
socks_opts() :: #{
|
||||
host := inet:hostname() | inet:ip_address(),
|
||||
port := inet:port_number(),
|
||||
|
||||
auth => [{username_password, binary(), binary()} | none],
|
||||
protocols => protocols(),
|
||||
transport => tcp | tls,
|
||||
version => 5,
|
||||
|
||||
tls_opts => [ssl:tls_client_option()],
|
||||
tls_handshake_timeout => timeout()
|
||||
}
|
||||
----
|
||||
|
||||
Configuration for the Socks protocol.
|
||||
|
||||
The default value, if any, is given next to the option name:
|
||||
|
||||
host, port::
|
||||
|
||||
Destination hostname and port number. Mandatory.
|
||||
+
|
||||
Upon successful completion of the Socks connection, Gun will
|
||||
begin using these as the host and port of the origin server
|
||||
for subsequent requests.
|
||||
|
||||
auth ([none])::
|
||||
|
||||
Authentication methods Gun advertises to the Socks proxy.
|
||||
|
||||
protocols - see below::
|
||||
|
||||
Ordered list of preferred protocols. Please refer to the
|
||||
`protocols()` type documentation for details.
|
||||
+
|
||||
Defaults to `[http]` when the transport is `tcp`,
|
||||
and `[http2, http]` when the transport is `tls`.
|
||||
|
||||
transport (tcp)::
|
||||
|
||||
Transport that will be used for tunneled requests.
|
||||
|
||||
tls_opts ([])::
|
||||
|
||||
Options to use for tunneled TLS connections.
|
||||
|
||||
tls_handshake_timeout (infinity)::
|
||||
|
||||
Handshake timeout for tunneled TLS connections.
|
||||
|
||||
version (5)::
|
||||
|
||||
Version of the Socks protocol to use.
|
||||
|
||||
=== stream_ref()
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
stream_ref() - see below
|
||||
----
|
||||
|
||||
Unique identifier for a stream.
|
||||
|
||||
The exact type is considered to be an implementation
|
||||
detail.
|
||||
|
||||
=== ws_opts()
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
ws_opts() :: #{
|
||||
closing_timeout => timeout(),
|
||||
compress => boolean(),
|
||||
default_protocol => module(),
|
||||
flow => pos_integer(),
|
||||
keepalive => timeout(),
|
||||
protocols => [{binary(), module()}],
|
||||
silence_pings => boolean(),
|
||||
user_opts => any()
|
||||
}
|
||||
----
|
||||
|
||||
Configuration for the Websocket protocol.
|
||||
|
||||
The default value is given next to the option name:
|
||||
|
||||
closing_timeout (15000)::
|
||||
|
||||
Time to wait before brutally closing the connection when a
|
||||
graceful shutdown was requested either via a call to
|
||||
link:man:gun:shutdown(3)[gun:shutdown(3)] or by the server.
|
||||
|
||||
compress (false)::
|
||||
|
||||
Whether to enable permessage-deflate compression. This does
|
||||
not guarantee that compression will be used as it is the
|
||||
server that ultimately decides. Defaults to false.
|
||||
|
||||
default_protocol (gun_ws_h)::
|
||||
|
||||
Default protocol module when no Websocket subprotocol is
|
||||
negotiated.
|
||||
|
||||
flow - see below::
|
||||
|
||||
The initial flow control value for the Websocket connection.
|
||||
By default flow control is disabled.
|
||||
|
||||
keepalive (infinity)::
|
||||
|
||||
Time between pings in milliseconds.
|
||||
|
||||
protocols ([])::
|
||||
|
||||
A non-empty list enables Websocket protocol negotiation. The
|
||||
list of protocols will be sent in the sec-websocket-protocol
|
||||
request header. The given module must follow the
|
||||
link:man:gun_ws_protocol(3)[gun_ws_protocol(3)] interface.
|
||||
Gun comes with a default interface in `gun_ws_h` that may
|
||||
be reused for negotiated protocols.
|
||||
|
||||
silence_pings (true)::
|
||||
|
||||
Whether the ping and pong frames should be sent to the user.
|
||||
In all cases Gun will automatically send a pong frame back
|
||||
when receiving a ping.
|
||||
|
||||
user_opts - see below::
|
||||
|
||||
Additional options that are not in use by Gun unless a custom
|
||||
Websocket subprotocol is configured and negotiated.
|
||||
By default no user option is defined.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *2.1*: The HTTP/2 option list was updated with new options.
|
||||
* *2.0*: The `default_protocol` and `user_opts` Websocket
|
||||
options were added.
|
||||
* *2.0*: The `stream_ref()` type was added.
|
||||
* *2.0*: The option `cookie_store` was added. It can be used
|
||||
to configure a cookie store that Gun will use
|
||||
automatically. A related option, `cookie_ignore_informational`,
|
||||
was added to both `http_opts()` and `http2_opts()`.
|
||||
* *2.0*: The types `protocols()` and `socks_opts()` have been
|
||||
added. Support for the Socks protocol has been added
|
||||
in every places where protocol selection is available.
|
||||
In addition it is now possible to specify separate
|
||||
HTTP options for the CONNECT proxy and the origin server.
|
||||
* *2.0*: The `connect_timeout` option has been split into
|
||||
three options: `domain_lookup_timeout`, `connect_timeout`
|
||||
and when applicable `tls_handshake_timeout`.
|
||||
* *2.0*: The option `retry_fun` was added. It can be used to
|
||||
implement different reconnect strategies.
|
||||
* *2.0*: The `transport_opts` option has been split into
|
||||
two options: `tcp_opts` and `tls_opts`.
|
||||
* *2.0*: Function `gun:update_flow/3` introduced. The `flow`
|
||||
option was added to request options and HTTP/1.1,
|
||||
HTTP/2 and Websocket options as well.
|
||||
* *2.0*: Introduce the type `req_headers()` and extend the
|
||||
types accepted for header names for greater
|
||||
interoperability. Header names are automatically
|
||||
lowercased as well.
|
||||
* *2.0*: Function `gun:headers/4,5` introduced.
|
||||
* *2.0*: The `keepalive` option is now set to `infinity` by
|
||||
default for all protocols. This means it is disabled.
|
||||
* *2.0*: Websocket options `keepalive` and `silence_pings` introduced.
|
||||
* *2.0*: Remove the `protocol` option from `connect_destination()`.
|
||||
* *1.3*: Add the CONNECT destination's `protocols` option and
|
||||
deprecate the previously introduced `protocol` option.
|
||||
* *1.2*: Introduce the type `connect_destination()`.
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(7)[gun(7)]
|
|
@ -0,0 +1,122 @@
|
|||
= gun:await(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun:await - Wait for a response
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
await(ConnPid, StreamRef)
|
||||
-> await(ConnPid, StreamRef, 5000, MonitorRef)
|
||||
|
||||
await(ConnPid, StreamRef, MonitorRef)
|
||||
-> await(ConnPid, StreamRef, 5000, MonitorRef)
|
||||
|
||||
await(ConnPid, StreamRef, Timeout)
|
||||
-> await(ConnPid, StreamRef, Timeout, MonitorRef)
|
||||
|
||||
await(ConnPid, StreamRef, Timeout, MonitorRef)
|
||||
-> Result
|
||||
|
||||
ConnPid :: pid()
|
||||
StreamRef :: gun:stream_ref()
|
||||
MonitorRef :: reference()
|
||||
Timeout :: timeout()
|
||||
Result :: tuple() - see below
|
||||
----
|
||||
|
||||
Wait for a response.
|
||||
|
||||
This function waits for a message from the given stream and
|
||||
returns it as a tuple. An error will be returned should the
|
||||
process fail or a relevant message is not received within
|
||||
the specified duration.
|
||||
|
||||
== Arguments
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
StreamRef::
|
||||
|
||||
Identifier of the stream for the original request.
|
||||
|
||||
Timeout::
|
||||
|
||||
How long to wait for a message, in milliseconds.
|
||||
|
||||
MonitorRef::
|
||||
|
||||
Monitor for the Gun connection process.
|
||||
+
|
||||
A monitor is automatically created for the duration of this
|
||||
call when one is not provided.
|
||||
|
||||
== Return value
|
||||
|
||||
A number of different tuples can be returned. They correspond
|
||||
to the message of the same name and they contain the same
|
||||
elements minus the pid and stream reference. Error tuples
|
||||
may also be returned when a timeout or an error occur.
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
Result :: {inform, Status, Headers}
|
||||
| {response, IsFin, Status, Headers}
|
||||
| {data, IsFin, Data}
|
||||
| {trailers, Trailers}
|
||||
| {push, NewStreamRef, Method, URI, Headers}
|
||||
| {upgrade, Protocols, Headers}
|
||||
| {ws, Frame}
|
||||
| {error, Reason}
|
||||
|
||||
Reason :: {stream_error | connection_error | down, any()}
|
||||
| timeout
|
||||
----
|
||||
|
||||
Because the messages and returned tuples are equivalent,
|
||||
please refer to the manual pages for each message for
|
||||
further information:
|
||||
|
||||
* link:man:gun_inform(3)[gun_inform(3)] - Informational response
|
||||
* link:man:gun_response(3)[gun_response(3)] - Response
|
||||
* link:man:gun_data(3)[gun_data(3)] - Response body
|
||||
* link:man:gun_trailers(3)[gun_trailers(3)] - Response trailers
|
||||
* link:man:gun_push(3)[gun_push(3)] - Server-initiated push
|
||||
* link:man:gun_upgrade(3)[gun_upgrade(3)] - Successful protocol upgrade
|
||||
* link:man:gun_ws(3)[gun_ws(3)] - Websocket frame
|
||||
|
||||
== Changelog
|
||||
|
||||
* *2.0*: `upgrade` and `ws` tuples can now be returned.
|
||||
* *2.0*: The error tuple type now includes the type of error.
|
||||
* *1.0*: Function introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Wait for a response
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:get(ConnPid, "/articles", [
|
||||
{<<"accept">>, <<"text/html;q=1.0, application/xml;q=0.1">>}
|
||||
]).
|
||||
{response, nofin, 200, _Headers} = gun:await(ConnPid, StreamRef).
|
||||
{data, fin, <<"Hello world!">>} = gun:await(ConnPid, StreamRef).
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:get(3)[gun:get(3)],
|
||||
link:man:gun:head(3)[gun:head(3)],
|
||||
link:man:gun:options(3)[gun:options(3)],
|
||||
link:man:gun:patch(3)[gun:patch(3)],
|
||||
link:man:gun:post(3)[gun:post(3)],
|
||||
link:man:gun:put(3)[gun:put(3)],
|
||||
link:man:gun:delete(3)[gun:delete(3)],
|
||||
link:man:gun:headers(3)[gun:headers(3)],
|
||||
link:man:gun:request(3)[gun:request(3)],
|
||||
link:man:gun:await_body(3)[gun:await_body(3)]
|
|
@ -0,0 +1,91 @@
|
|||
= gun:await_body(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun:await_body - Wait for the complete response body
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
await_body(ConnPid, StreamRef)
|
||||
-> await_body(ConnPid, StreamRef, 5000, MonitorRef)
|
||||
|
||||
await_body(ConnPid, StreamRef, MonitorRef)
|
||||
-> await_body(ConnPid, StreamRef, 5000, MonitorRef)
|
||||
|
||||
await_body(ConnPid, StreamRef, Timeout)
|
||||
-> await_body(ConnPid, StreamRef, Timeout, MonitorRef)
|
||||
|
||||
await_body(ConnPid, StreamRef, Timeout, MonitorRef)
|
||||
-> {ok, Body} | {ok, Body, Trailers} | {error, Reason}
|
||||
|
||||
ConnPid :: pid()
|
||||
StreamRef :: gun:stream_ref()
|
||||
MonitorRef :: reference()
|
||||
Timeout :: timeout()
|
||||
Body :: binary()
|
||||
Trailers :: [{binary(), binary()}]
|
||||
Reason :: {stream_error | connection_error | down, any()}
|
||||
| timeout
|
||||
----
|
||||
|
||||
Wait for the complete response body.
|
||||
|
||||
== Arguments
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
StreamRef::
|
||||
|
||||
Identifier of the stream for the original request.
|
||||
|
||||
Timeout::
|
||||
|
||||
How long to wait for each message, in milliseconds.
|
||||
|
||||
MonitorRef::
|
||||
|
||||
Monitor for the Gun connection process.
|
||||
+
|
||||
A monitor is automatically created for the duration of this
|
||||
call when one is not provided.
|
||||
|
||||
== Return value
|
||||
|
||||
The body is returned, possibly with trailers if the
|
||||
request contained a `te: trailers` header. Error tuples
|
||||
may also be returned when a timeout or an error occur.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *2.0*: The error tuple type now includes the type of error.
|
||||
* *1.0*: Function introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Wait for the complete response body
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:get(ConnPid, "/articles", [
|
||||
{<<"accept">>, <<"text/html;q=1.0, application/xml;q=0.1">>}
|
||||
]).
|
||||
{response, nofin, 200, _Headers} = gun:await(ConnPid, StreamRef).
|
||||
{ok, _Body} = gun:await_body(ConnPid, StreamRef).
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:get(3)[gun:get(3)],
|
||||
link:man:gun:head(3)[gun:head(3)],
|
||||
link:man:gun:options(3)[gun:options(3)],
|
||||
link:man:gun:patch(3)[gun:patch(3)],
|
||||
link:man:gun:post(3)[gun:post(3)],
|
||||
link:man:gun:put(3)[gun:put(3)],
|
||||
link:man:gun:delete(3)[gun:delete(3)],
|
||||
link:man:gun:headers(3)[gun:headers(3)],
|
||||
link:man:gun:request(3)[gun:request(3)],
|
||||
link:man:gun:await(3)[gun:await(3)]
|
|
@ -0,0 +1,75 @@
|
|||
= gun:await_up(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun:await_up - Wait for the connection to be up
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
await_up(ConnPid)
|
||||
-> await_up(ConnPid, 5000, MonitorRef)
|
||||
|
||||
await_up(ConnPid, MonitorRef)
|
||||
-> await_up(ConnPid, 5000, MonitorRef)
|
||||
|
||||
await_up(ConnPid, Timeout)
|
||||
-> await_up(ConnPid, Timeout, MonitorRef)
|
||||
|
||||
await_up(ConnPid, Timeout, MonitorRef)
|
||||
-> {ok, Protocol} | {error, Reason}
|
||||
|
||||
ConnPid :: pid()
|
||||
MonitorRef :: reference()
|
||||
Timeout :: timeout()
|
||||
Protocol :: http | http2 | socks
|
||||
Reason :: {down, any()} | timeout
|
||||
----
|
||||
|
||||
Wait for the connection to be up.
|
||||
|
||||
== Arguments
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
Timeout::
|
||||
|
||||
How long to wait for, in milliseconds.
|
||||
|
||||
MonitorRef::
|
||||
|
||||
Monitor for the Gun connection process.
|
||||
+
|
||||
A monitor is automatically created for the duration of this
|
||||
call when one is not provided.
|
||||
|
||||
== Return value
|
||||
|
||||
The protocol selected for this connection. It can be used
|
||||
to determine the capabilities of the server. Error tuples
|
||||
may also be returned when a timeout or an error occur.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *2.0*: The error tuple type now includes the type of error.
|
||||
* *1.0*: Function introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Wait for the connection to be up
|
||||
[source,erlang]
|
||||
----
|
||||
{ok, ConnPid} = gun:open("example.org", 443).
|
||||
{ok, _} = gun:await_up(ConnPid).
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:open(3)[gun:open(3)],
|
||||
link:man:gun:open_unix(3)[gun:open_unix(3)],
|
||||
link:man:gun_tunnel_up(3)[gun_tunnel_up(3)],
|
||||
link:man:gun_up(3)[gun_up(3)]
|
|
@ -0,0 +1,70 @@
|
|||
= gun:cancel(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun:cancel - Cancel the given stream
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
cancel(ConnPid, StreamRef) -> ok
|
||||
|
||||
ConnPid :: pid()
|
||||
StreamRef :: gun:stream_ref()
|
||||
----
|
||||
|
||||
Cancel the given stream.
|
||||
|
||||
The behavior of this function depends on the protocol
|
||||
selected.
|
||||
|
||||
HTTP/1.1 does not support this feature. Gun will simply
|
||||
silence the stream and stop relaying messages. Gun may
|
||||
also decide to close the connection if the response body
|
||||
is too large, to avoid wasting time and bandwidth.
|
||||
|
||||
HTTP/2 allows cancelling streams at any time.
|
||||
|
||||
This function is asynchronous. Messages related to this
|
||||
stream may still be sent after the function returns.
|
||||
|
||||
== Arguments
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
StreamRef::
|
||||
|
||||
Identifier of the stream for the original request.
|
||||
|
||||
== Return value
|
||||
|
||||
The atom `ok` is returned.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *1.0*: Function introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Cancel a stream
|
||||
[source,erlang]
|
||||
----
|
||||
gun:cancel(ConnPid, StreamRef).
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:get(3)[gun:get(3)],
|
||||
link:man:gun:head(3)[gun:head(3)],
|
||||
link:man:gun:options(3)[gun:options(3)],
|
||||
link:man:gun:patch(3)[gun:patch(3)],
|
||||
link:man:gun:post(3)[gun:post(3)],
|
||||
link:man:gun:put(3)[gun:put(3)],
|
||||
link:man:gun:delete(3)[gun:delete(3)],
|
||||
link:man:gun:headers(3)[gun:headers(3)],
|
||||
link:man:gun:request(3)[gun:request(3)],
|
||||
link:man:gun:stream_info(3)[gun:stream_info(3)]
|
|
@ -0,0 +1,45 @@
|
|||
= gun:close(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun:close - Brutally close the connection
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
close(ConnPid) -> ok
|
||||
|
||||
ConnPid :: pid()
|
||||
----
|
||||
|
||||
Brutally close the connection.
|
||||
|
||||
== Arguments
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
== Return value
|
||||
|
||||
The atom `ok` is returned.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *1.0*: Function introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Close the connection
|
||||
[source,erlang]
|
||||
----
|
||||
ok = gun:close(ConnPid).
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:open(3)[gun:open(3)],
|
||||
link:man:gun:open_unix(3)[gun:open_unix(3)],
|
||||
link:man:gun:shutdown(3)[gun:shutdown(3)]
|
|
@ -0,0 +1,115 @@
|
|||
= gun:connect(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun:connect - Establish a tunnel to the origin server
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
connect(ConnPid, Destination)
|
||||
-> connect(ConnPid, Destination, [], #{}).
|
||||
|
||||
connect(ConnPid, Destination, Headers)
|
||||
-> connect(ConnPid, Destination, Headers, #{}).
|
||||
|
||||
connect(ConnPid, Destination, Headers, ReqOpts)
|
||||
-> StreamRef
|
||||
|
||||
ConnPid :: pid()
|
||||
Destination :: gun:connect_destination()
|
||||
Headers :: gun:req_headers()
|
||||
ReqOpts :: gun:req_opts()
|
||||
StreamRef :: gun:stream_ref()
|
||||
----
|
||||
|
||||
Establish a tunnel to the origin server.
|
||||
|
||||
This feature is currently only available for HTTP/1.1 connections.
|
||||
Upon successful completion of the CONNECT request a tunnel is
|
||||
established and subsequent requests will go through the tunnel.
|
||||
|
||||
Gun will not automatically re-issue the CONNECT request upon
|
||||
reconnection to the proxy server. The `gun_up` message can
|
||||
be used to know when the tunnel needs to be established again.
|
||||
|
||||
== Arguments
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
Destination::
|
||||
|
||||
Destination of the CONNECT request.
|
||||
|
||||
Headers::
|
||||
|
||||
Additional request headers.
|
||||
|
||||
ReqOpts::
|
||||
|
||||
Request options.
|
||||
|
||||
== Return value
|
||||
|
||||
A reference that identifies the newly created stream is
|
||||
returned. It is this reference that must be passed in
|
||||
subsequent calls and will be received in messages related
|
||||
to this new stream.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *1.2*: Function introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Establish a tunnel
|
||||
[source,erlang]
|
||||
----
|
||||
{ok, ConnPid} = gun:open("proxy.example.org", 1080),
|
||||
{ok, http} = gun:await_up(ConnPid),
|
||||
StreamRef = gun:connect(ConnPid, #{
|
||||
host => "origin-server.example.org",
|
||||
port => 80
|
||||
}),
|
||||
{response, fin, 200, _} = gun:await(ConnPid, StreamRef),
|
||||
%% Subsequent requests will be sent to origin-server.example.org.
|
||||
----
|
||||
|
||||
.Establish a tunnel for a secure HTTP/2 connection
|
||||
[source,erlang]
|
||||
----
|
||||
{ok, ConnPid} = gun:open("proxy.example.org", 1080),
|
||||
{ok, http} = gun:await_up(ConnPid),
|
||||
StreamRef = gun:connect(ConnPid, #{
|
||||
host => "origin-server.example.org",
|
||||
port => 443,
|
||||
protocols => [http2],
|
||||
transport => tls
|
||||
}),
|
||||
{response, fin, 200, _} = gun:await(ConnPid, StreamRef),
|
||||
%% Subsequent requests will be sent to origin-server.example.org.
|
||||
----
|
||||
|
||||
.Establish a tunnel using proxy authorization
|
||||
[source,erlang]
|
||||
----
|
||||
{ok, ConnPid} = gun:open("proxy.example.org", 1080),
|
||||
{ok, http} = gun:await_up(ConnPid),
|
||||
StreamRef = gun:connect(ConnPid, #{
|
||||
host => "origin-server.example.org",
|
||||
port => 80,
|
||||
username => "essen",
|
||||
password => "myrealpasswordis"
|
||||
}),
|
||||
{response, fin, 200, _} = gun:await(ConnPid, StreamRef),
|
||||
%% Subsequent requests will be sent to origin-server.example.org.
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:await_up(3)[gun:await_up(3)],
|
||||
link:man:gun_up(3)[gun_up(3)]
|
|
@ -0,0 +1,75 @@
|
|||
= gun:data(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun:data - Stream the body of a request
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
data(ConnPid, StreamRef, IsFin, Data) -> ok
|
||||
|
||||
ConnPid :: pid()
|
||||
StreamRef :: gun:stream_ref()
|
||||
IsFin :: fin | nofin
|
||||
Data :: iodata()
|
||||
----
|
||||
|
||||
Stream the body of a request.
|
||||
|
||||
This function can only be used if the original request
|
||||
had headers indicating that a body would be streamed.
|
||||
|
||||
All calls to this function must use the `nofin` flag
|
||||
except for the last which must use `fin` to indicate
|
||||
the end of the request body.
|
||||
|
||||
Empty data is allowed regardless of the value of `IsFin`.
|
||||
Gun may or may not send empty data chunks, however.
|
||||
|
||||
== Arguments
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
StreamRef::
|
||||
|
||||
Identifier of the stream for the original request.
|
||||
|
||||
IsFin::
|
||||
|
||||
Whether this message terminates the request.
|
||||
|
||||
Data::
|
||||
|
||||
All or part of the response body.
|
||||
|
||||
== Return value
|
||||
|
||||
The atom `ok` is returned.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *1.0*: Function introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Stream the body of a request
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:put(ConnPid, "/lang/fr_FR/hello", [
|
||||
{<<"content-type">>, <<"text/plain">>}
|
||||
]).
|
||||
gun:data(ConnPid, StreamRef, nofin, <<"Bonjour !\n">>).
|
||||
gun:data(ConnPid, StreamRef, fin, <<"Bonsoir !\n">>).
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:patch(3)[gun:patch(3)],
|
||||
link:man:gun:post(3)[gun:post(3)],
|
||||
link:man:gun:put(3)[gun:put(3)],
|
||||
link:man:gun:headers(3)[gun:headers(3)]
|
|
@ -0,0 +1,82 @@
|
|||
= gun:delete(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun:delete - Delete a resource
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
delete(ConnPid, Path)
|
||||
-> delete(ConnPid, Path, [], #{}).
|
||||
|
||||
delete(ConnPid, Path, Headers)
|
||||
-> delete(ConnPid, Path, Headers, #{})
|
||||
|
||||
delete(ConnPid, Path, Headers, ReqOpts)
|
||||
-> StreamRef
|
||||
|
||||
ConnPid :: pid()
|
||||
Path :: iodata()
|
||||
Headers :: gun:req_headers()
|
||||
ReqOpts :: gun:req_opts()
|
||||
StreamRef :: gun:stream_ref()
|
||||
----
|
||||
|
||||
Delete a resource.
|
||||
|
||||
== Arguments
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
Path::
|
||||
|
||||
Path to the resource.
|
||||
|
||||
Headers::
|
||||
|
||||
Additional request headers.
|
||||
|
||||
ReqOpts::
|
||||
|
||||
Request options.
|
||||
|
||||
== Return value
|
||||
|
||||
A reference that identifies the newly created stream is
|
||||
returned. It is this reference that must be passed in
|
||||
subsequent calls and will be received in messages related
|
||||
to this new stream.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *1.0*: Function introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Delete a resource
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:delete(ConnPid, "/drafts/123").
|
||||
----
|
||||
|
||||
.Delete a resource with request options
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:delete(ConnPid, "/drafts/123", [],
|
||||
#{reply_to => ReplyToPid}).
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:put(3)[gun:put(3)],
|
||||
link:man:gun:await(3)[gun:await(3)],
|
||||
link:man:gun:await_body(3)[gun:await_body(3)],
|
||||
link:man:gun_push(3)[gun_push(3)],
|
||||
link:man:gun_inform(3)[gun_inform(3)],
|
||||
link:man:gun_response(3)[gun_response(3)],
|
||||
link:man:gun_data(3)[gun_data(3)]
|
|
@ -0,0 +1,59 @@
|
|||
= gun:flush(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun:flush - Flush all messages related to a connection or a stream
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
flush(ConnPid) -> ok
|
||||
flush(StreamRef) -> ok
|
||||
|
||||
ConnPid :: pid()
|
||||
StreamRef :: gun:stream_ref()
|
||||
----
|
||||
|
||||
Flush all messages related to a connection or a stream.
|
||||
|
||||
== Arguments
|
||||
|
||||
Either of these arguments may be provided:
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
StreamRef::
|
||||
|
||||
Identifier of the stream for the original request.
|
||||
|
||||
== Return value
|
||||
|
||||
The atom `ok` is returned.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *1.0*: Function introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Flush all messages from a connection
|
||||
[source,erlang]
|
||||
----
|
||||
gun:flush(ConnPid).
|
||||
----
|
||||
|
||||
.Flush messages from a single stream
|
||||
[source,erlang]
|
||||
----
|
||||
gun:flush(StreamRef).
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:await(3)[gun:await(3)],
|
||||
link:man:gun:await_body(3)[gun:await_body(3)],
|
||||
link:man:gun:await_up(3)[gun:await_up(3)]
|
|
@ -0,0 +1,85 @@
|
|||
= gun:get(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun:get - Get a resource representation
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
get(ConnPid, Path)
|
||||
-> get(ConnPid, Path, [], #{}).
|
||||
|
||||
get(ConnPid, Path, Headers)
|
||||
-> get(ConnPid, Path, Headers, #{})
|
||||
|
||||
get(ConnPid, Path, Headers, ReqOpts)
|
||||
-> StreamRef
|
||||
|
||||
ConnPid :: pid()
|
||||
Path :: iodata()
|
||||
Headers :: gun:req_headers()
|
||||
ReqOpts :: gun:req_opts()
|
||||
StreamRef :: gun:stream_ref()
|
||||
----
|
||||
|
||||
Get a resource representation.
|
||||
|
||||
== Arguments
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
Path::
|
||||
|
||||
Path to the resource.
|
||||
|
||||
Headers::
|
||||
|
||||
Additional request headers.
|
||||
|
||||
ReqOpts::
|
||||
|
||||
Request options.
|
||||
|
||||
== Return value
|
||||
|
||||
A reference that identifies the newly created stream is
|
||||
returned. It is this reference that must be passed in
|
||||
subsequent calls and will be received in messages related
|
||||
to this new stream.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *1.0*: Function introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Get a resource representation
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:get(ConnPid, "/articles", [
|
||||
{<<"accept">>, <<"text/html;q=1.0, application/xml;q=0.1">>}
|
||||
]).
|
||||
----
|
||||
|
||||
.Get a resource representation with request options
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:get(ConnPid, "/articles", [], #{
|
||||
reply_to => ReplyToPid
|
||||
}).
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:head(3)[gun:head(3)],
|
||||
link:man:gun:await(3)[gun:await(3)],
|
||||
link:man:gun:await_body(3)[gun:await_body(3)],
|
||||
link:man:gun_push(3)[gun_push(3)],
|
||||
link:man:gun_inform(3)[gun_inform(3)],
|
||||
link:man:gun_response(3)[gun_response(3)],
|
||||
link:man:gun_data(3)[gun_data(3)]
|
|
@ -0,0 +1,92 @@
|
|||
= gun:head(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun:head - Get headers of a resource representation
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
head(ConnPid, Path)
|
||||
-> head(ConnPid, Path, [], #{}).
|
||||
|
||||
head(ConnPid, Path, Headers)
|
||||
-> head(ConnPid, Path, Headers, #{})
|
||||
|
||||
head(ConnPid, Path, Headers, ReqOpts)
|
||||
-> StreamRef
|
||||
|
||||
ConnPid :: pid()
|
||||
Path :: iodata()
|
||||
Headers :: gun:req_headers()
|
||||
ReqOpts :: gun:req_opts()
|
||||
StreamRef :: gun:stream_ref()
|
||||
----
|
||||
|
||||
Get headers of a resource representation.
|
||||
|
||||
This function performs the same operation as
|
||||
link:man:gun:get(3)[gun:get(3)], except the server will not
|
||||
send the resource representation, only the response's status
|
||||
code and headers.
|
||||
|
||||
While servers are supposed to send the same headers as for
|
||||
a GET request, they sometimes will not. For example the
|
||||
`content-length` header may be dropped from the response.
|
||||
|
||||
== Arguments
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
Path::
|
||||
|
||||
Path to the resource.
|
||||
|
||||
Headers::
|
||||
|
||||
Additional request headers.
|
||||
|
||||
ReqOpts::
|
||||
|
||||
Request options.
|
||||
|
||||
== Return value
|
||||
|
||||
A reference that identifies the newly created stream is
|
||||
returned. It is this reference that must be passed in
|
||||
subsequent calls and will be received in messages related
|
||||
to this new stream.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *1.0*: Function introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Get headers of a resource representation
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:head(ConnPid, "/articles", [
|
||||
{<<"accept">>, <<"text/html;q=1.0, application/xml;q=0.1">>}
|
||||
]).
|
||||
----
|
||||
|
||||
.Get headers of a resource representation with request options
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:head(ConnPid, "/articles", [], #{
|
||||
reply_to => ReplyToPid
|
||||
}).
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:get(3)[gun:head(3)],
|
||||
link:man:gun:await(3)[gun:await(3)],
|
||||
link:man:gun_push(3)[gun_push(3)],
|
||||
link:man:gun_inform(3)[gun_inform(3)],
|
||||
link:man:gun_response(3)[gun_response(3)]
|
|
@ -0,0 +1,86 @@
|
|||
= gun:headers(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun:headers - Initiate the given request
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
headers(ConnPid, Method, Path, Headers)
|
||||
-> headers(ConnPid, Method, Path, Headers, #{})
|
||||
|
||||
headers(ConnPid, Method, Path, Headers, ReqOpts)
|
||||
-> StreamRef
|
||||
|
||||
ConnPid :: pid()
|
||||
Method :: binary()
|
||||
Path :: iodata()
|
||||
Headers :: gun:req_headers()
|
||||
ReqOpts :: gun:req_opts()
|
||||
StreamRef :: gun:stream_ref()
|
||||
----
|
||||
|
||||
Initiate the given request.
|
||||
|
||||
This is a general purpose function that should only be
|
||||
used when other method-specific functions do not apply.
|
||||
|
||||
The function `headers/4,5` initiates a request but does
|
||||
not send the request body. It must be sent separately
|
||||
using link:man:gun:data(3)[gun:data(3)].
|
||||
|
||||
== Arguments
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
Method::
|
||||
|
||||
Method to be used for the request.
|
||||
|
||||
Path::
|
||||
|
||||
Path to the resource.
|
||||
|
||||
Headers::
|
||||
|
||||
Additional request headers.
|
||||
|
||||
ReqOpts::
|
||||
|
||||
Request options.
|
||||
|
||||
== Return value
|
||||
|
||||
A reference that identifies the newly created stream is
|
||||
returned. It is this reference that must be passed in
|
||||
subsequent calls and will be received in messages related
|
||||
to this new stream.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *2.0*: Function introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Initiate a request
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:headers(ConnPid, <<"PUT">>,
|
||||
"/lang/fr_FR/hello",
|
||||
[{<<"content-type">>, <<"text/plain">>}]).
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:request(3)[gun:request(3)],
|
||||
link:man:gun:await(3)[gun:await(3)],
|
||||
link:man:gun:await_body(3)[gun:await_body(3)],
|
||||
link:man:gun_push(3)[gun_push(3)],
|
||||
link:man:gun_inform(3)[gun_inform(3)],
|
||||
link:man:gun_response(3)[gun_response(3)],
|
||||
link:man:gun_data(3)[gun_data(3)]
|
|
@ -0,0 +1,69 @@
|
|||
= gun:info(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun:info - Obtain information about the connection
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
info(ConnPid) -> Info
|
||||
|
||||
ConnPid :: pid()
|
||||
Info :: #{
|
||||
owner => pid(),
|
||||
socket => inet:socket() | ssl:sslsocket(),
|
||||
transport => tcp | tls,
|
||||
protocol => http | http2 | socks | ws,
|
||||
sock_ip => inet:ip_address(),
|
||||
sock_port => inet:port_number(),
|
||||
origin_scheme => binary() | undefined,
|
||||
origin_host => inet:hostname() | inet:ip_address(),
|
||||
origin_port => inet:port_number(),
|
||||
intermediaries => [Intermediary],
|
||||
cookie_store => gun_cookies:cookie_store()
|
||||
}
|
||||
Intermediary :: #{
|
||||
type => connect | socks5,
|
||||
host => inet:hostname() | inet:ip_address(),
|
||||
port => inet:port_number(),
|
||||
transport => tcp | tls,
|
||||
protocol => http | http2 | socks | raw
|
||||
}
|
||||
----
|
||||
|
||||
Obtain information about the connection.
|
||||
|
||||
== Arguments
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
== Return value
|
||||
|
||||
A map is returned containing various informations about
|
||||
the connection.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *2.0*: The values `owner`, `origin_scheme` and `cookie_store` were
|
||||
added.
|
||||
* *1.3*: The values `socket`, `transport`, `protocol`, `origin_host`,
|
||||
`origin_port` and `intermediaries` were added.
|
||||
* *1.0*: Function introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Obtain information about the connection
|
||||
[source,erlang]
|
||||
----
|
||||
Info = gun:info(ConnPid).
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:open(3)[gun:open(3)],
|
||||
link:man:gun:open_unix(3)[gun:open_unix(3)]
|
|
@ -0,0 +1,77 @@
|
|||
= gun:open(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun:open - Open a connection to the given host and port
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
open(Host, Port) -> open(Host, Port, #{})
|
||||
open(Host, Port, Opts) -> {ok, pid()} | {error, Reason}
|
||||
|
||||
Host :: inet:hostname() | inet:ip_address()
|
||||
Port :: inet:port_number()
|
||||
Opts :: gun:opts()
|
||||
Reason :: {options, OptName}
|
||||
| {options, {http | http2 | socks | ws, OptName}}
|
||||
| any()
|
||||
OptName :: atom()
|
||||
----
|
||||
|
||||
Open a connection to the given host and port.
|
||||
|
||||
== Arguments
|
||||
|
||||
Host::
|
||||
|
||||
Host or IP address to connect to.
|
||||
|
||||
Port::
|
||||
|
||||
Port to connect to.
|
||||
|
||||
Opts::
|
||||
|
||||
Options for this connection.
|
||||
|
||||
== Return value
|
||||
|
||||
The pid of the newly created Gun process is returned.
|
||||
Note that this does not indicate that the connection
|
||||
has been successfully opened; the link:man:gun_up(3)[gun_up(3)]
|
||||
message will be sent for that.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *1.0*: Function introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Connect to a server
|
||||
[source,erlang]
|
||||
----
|
||||
{ok, ConnPid} = gun:open("example.org", 443).
|
||||
----
|
||||
|
||||
.Connect to a server with custom options
|
||||
[source,erlang]
|
||||
----
|
||||
{ok, ConnPid} = gun:open("example.org", 443,
|
||||
#{protocols => [http2]}).
|
||||
----
|
||||
|
||||
.Connect to a server using its IP address
|
||||
[source,erlang]
|
||||
----
|
||||
{ok, ConnPid} = gun:open({127,0,0,1}, 443).
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:open_unix(3)[gun:open_unix(3)],
|
||||
link:man:gun:await_up(3)[gun:await_up(3)],
|
||||
link:man:gun_tunnel_up(3)[gun_tunnel_up(3)],
|
||||
link:man:gun_up(3)[gun_up(3)]
|
|
@ -0,0 +1,65 @@
|
|||
= gun:open_unix(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun:open_unix - Open a connection to the given Unix domain socket
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
open_unix(SocketPath, Opts) -> {ok, pid()} | {error, Reason}
|
||||
|
||||
SocketPath :: string()
|
||||
Opts :: gun:opts()
|
||||
Reason :: {options, OptName}
|
||||
| {options, {http | http2 | socks | ws, OptName}}
|
||||
| any()
|
||||
OptName :: atom()
|
||||
----
|
||||
|
||||
Open a connection to the given Unix domain socket.
|
||||
|
||||
== Arguments
|
||||
|
||||
SocketPath::
|
||||
|
||||
Path to the Unix domain socket to connect to.
|
||||
|
||||
Opts::
|
||||
|
||||
Options for this connection.
|
||||
|
||||
== Return value
|
||||
|
||||
The pid of the newly created Gun process is returned.
|
||||
Note that this does not indicate that the connection
|
||||
has been successfully opened; the link:man:gun_up(3)[gun_up(3)]
|
||||
message will be sent for that.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *1.0*: Function introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Connect to a server via a Unix domain socket
|
||||
[source,erlang]
|
||||
----
|
||||
{ok, ConnPid} = gun:open_unix("/var/run/dbus/system_bus_socket", #{}).
|
||||
----
|
||||
|
||||
.Connect to a server via a Unix domain socket with custom options
|
||||
[source,erlang]
|
||||
----
|
||||
{ok, ConnPid} = gun:open_unix("/var/run/dbus/system_bus_socket",
|
||||
#{protocols => [http2]}).
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:open(3)[gun:open(3)],
|
||||
link:man:gun:await_up(3)[gun:await_up(3)],
|
||||
link:man:gun_tunnel_up(3)[gun_tunnel_up(3)],
|
||||
link:man:gun_up(3)[gun_up(3)]
|
|
@ -0,0 +1,83 @@
|
|||
= gun:options(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun:options - Query the capabilities of the server or a resource
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
options(ConnPid, Path)
|
||||
-> options(ConnPid, Path, [], #{}).
|
||||
|
||||
options(ConnPid, Path, Headers)
|
||||
-> options(ConnPid, Path, Headers, #{})
|
||||
|
||||
options(ConnPid, Path, Headers, ReqOpts)
|
||||
-> StreamRef
|
||||
|
||||
ConnPid :: pid()
|
||||
Path :: iodata()
|
||||
Headers :: gun:req_headers()
|
||||
ReqOpts :: gun:req_opts()
|
||||
StreamRef :: gun:stream_ref()
|
||||
----
|
||||
|
||||
Query the capabilities of the server or a resource.
|
||||
|
||||
The special path `"*"` can be used to obtain information about
|
||||
the server as a whole. Any other path will return information
|
||||
about that resource specifically.
|
||||
|
||||
== Arguments
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
Path::
|
||||
|
||||
Path to the resource.
|
||||
|
||||
Headers::
|
||||
|
||||
Additional request headers.
|
||||
|
||||
ReqOpts::
|
||||
|
||||
Request options.
|
||||
|
||||
== Return value
|
||||
|
||||
A reference that identifies the newly created stream is
|
||||
returned. It is this reference that must be passed in
|
||||
subsequent calls and will be received in messages related
|
||||
to this new stream.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *1.0*: Function introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Query the capabilities of the server
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:options(ConnPid, "*").
|
||||
----
|
||||
|
||||
.Query the capabilities of a resource
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:options(ConnPid, "/articles").
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:await(3)[gun:await(3)],
|
||||
link:man:gun:await_body(3)[gun:await_body(3)],
|
||||
link:man:gun_inform(3)[gun_inform(3)],
|
||||
link:man:gun_response(3)[gun_response(3)],
|
||||
link:man:gun_data(3)[gun_data(3)]
|
|
@ -0,0 +1,123 @@
|
|||
= gun:patch(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun:patch - Apply a set of changes to a resource
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
patch(ConnPid, Path, Headers)
|
||||
-> patch(ConnPid, Path, Headers, #{})
|
||||
|
||||
patch(ConnPid, Path, Headers, ReqOpts)
|
||||
-> StreamRef
|
||||
|
||||
patch(ConnPid, Path, Headers, Body)
|
||||
-> patch(ConnPid, Path, Headers, Body, #{})
|
||||
|
||||
patch(ConnPid, Path, Headers, Body, ReqOpts)
|
||||
-> StreamRef
|
||||
|
||||
ConnPid :: pid()
|
||||
Path :: iodata()
|
||||
Headers :: gun:req_headers()
|
||||
Body :: iodata()
|
||||
ReqOpts :: gun:req_opts()
|
||||
StreamRef :: gun:stream_ref()
|
||||
----
|
||||
|
||||
Apply a set of changes to a resource.
|
||||
|
||||
The behavior of this function varies depending on whether
|
||||
a body is provided.
|
||||
|
||||
The function `patch/3,4` does not send a body. It must be
|
||||
sent separately using link:man:gun:data(3)[gun:data(3)].
|
||||
|
||||
The function `patch/4,5` sends the entire request, including
|
||||
the request body, immediately. It is therefore not possible
|
||||
to use link:man:gun:data(3)[gun:data(3)] after that. You
|
||||
should provide a content-type header. Gun will set the
|
||||
content-length header automatically.
|
||||
|
||||
The body sent in this request should be a patch document
|
||||
with instructions on how to update the resource.
|
||||
|
||||
== Arguments
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
Path::
|
||||
|
||||
Path to the resource.
|
||||
|
||||
Headers::
|
||||
|
||||
Additional request headers.
|
||||
|
||||
Body::
|
||||
|
||||
Request body.
|
||||
|
||||
ReqOpts::
|
||||
|
||||
Request options.
|
||||
|
||||
== Return value
|
||||
|
||||
A reference that identifies the newly created stream is
|
||||
returned. It is this reference that must be passed in
|
||||
subsequent calls and will be received in messages related
|
||||
to this new stream.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *2.0*: Implicit body detection has been removed. The body
|
||||
must now be provided either directly (even if empty)
|
||||
or using separate calls.
|
||||
* *1.0*: Function introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Patch a resource
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:patch(ConnPid, "/users/1",
|
||||
[{<<"content-type">>, <<"application/json-patch+json">>}],
|
||||
<<"[{\"op\":\"add\",\"path\":\"/baz\",\"value\":\"qux\"}]">>).
|
||||
----
|
||||
|
||||
.Patch a resource in multiple calls
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:patch(ConnPid, "/users/1", [
|
||||
{<<"content-type">>, <<"application/json-patch+json">>}
|
||||
]).
|
||||
gun:data(ConnPid, StreamRef, fin,
|
||||
<<"[{\"op\":\"add\",\"path\":\"/baz\",\"value\":\"qux\"}]">>).
|
||||
----
|
||||
|
||||
.Patch a resource with request options
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:patch(ConnPid, "/users/1",
|
||||
[{<<"content-type">>, <<"application/json-patch+json">>}],
|
||||
<<"[{\"op\":\"add\",\"path\":\"/baz\",\"value\":\"qux\"}]">>,
|
||||
#{reply_to => ReplyToPid}).
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:post(3)[gun:post(3)],
|
||||
link:man:gun:put(3)[gun:put(3)],
|
||||
link:man:gun:await(3)[gun:await(3)],
|
||||
link:man:gun:await_body(3)[gun:await_body(3)],
|
||||
link:man:gun_push(3)[gun_push(3)],
|
||||
link:man:gun_inform(3)[gun_inform(3)],
|
||||
link:man:gun_response(3)[gun_response(3)],
|
||||
link:man:gun_data(3)[gun_data(3)]
|
|
@ -0,0 +1,120 @@
|
|||
= gun:post(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun:post - Process the enclosed representation according to a resource's own semantics
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
post(ConnPid, Path, Headers)
|
||||
-> post(ConnPid, Path, Headers, #{})
|
||||
|
||||
post(ConnPid, Path, Headers, ReqOpts)
|
||||
-> StreamRef
|
||||
|
||||
post(ConnPid, Path, Headers, Body)
|
||||
-> post(ConnPid, Path, Headers, Body, #{})
|
||||
|
||||
post(ConnPid, Path, Headers, Body, ReqOpts)
|
||||
-> StreamRef
|
||||
|
||||
ConnPid :: pid()
|
||||
Path :: iodata()
|
||||
Headers :: gun:req_headers()
|
||||
Body :: iodata()
|
||||
ReqOpts :: gun:req_opts()
|
||||
StreamRef :: gun:stream_ref()
|
||||
----
|
||||
|
||||
Process the enclosed representation according to a resource's
|
||||
own semantics.
|
||||
|
||||
The behavior of this function varies depending on whether
|
||||
a body is provided.
|
||||
|
||||
The function `post/3,4` does not send a body. It must be
|
||||
sent separately using link:man:gun:data(3)[gun:data(3)].
|
||||
|
||||
The function `post/4,5` sends the entire request, including
|
||||
the request body, immediately. It is therefore not possible
|
||||
to use link:man:gun:data(3)[gun:data(3)] after that. You
|
||||
should provide a content-type header. Gun will set the
|
||||
content-length header automatically.
|
||||
|
||||
== Arguments
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
Path::
|
||||
|
||||
Path to the resource.
|
||||
|
||||
Headers::
|
||||
|
||||
Additional request headers.
|
||||
|
||||
Body::
|
||||
|
||||
Request body.
|
||||
|
||||
ReqOpts::
|
||||
|
||||
Request options.
|
||||
|
||||
== Return value
|
||||
|
||||
A reference that identifies the newly created stream is
|
||||
returned. It is this reference that must be passed in
|
||||
subsequent calls and will be received in messages related
|
||||
to this new stream.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *2.0*: Implicit body detection has been removed. The body
|
||||
must now be provided either directly (even if empty)
|
||||
or using separate calls.
|
||||
* *1.0*: Function introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Post to a resource
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:post(ConnPid, "/search",
|
||||
[{<<"content-type">>, <<"application/x-www-form-urlencoded">>}],
|
||||
<<"q=nine%20nines">>).
|
||||
----
|
||||
|
||||
.Post to a resource in multiple calls
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:post(ConnPid, "/search", [
|
||||
{<<"content-type">>, <<"application/x-www-form-urlencoded">>}
|
||||
]).
|
||||
gun:data(ConnPid, StreamRef, fin, <<"q=nine%20nines">>).
|
||||
----
|
||||
|
||||
.Post to a resource with request options
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:post(ConnPid, "/search",
|
||||
[{<<"content-type">>, <<"application/x-www-form-urlencoded">>}],
|
||||
<<"q=nine%20nines">>,
|
||||
#{reply_to => ReplyToPid}).
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:patch(3)[gun:patch(3)],
|
||||
link:man:gun:put(3)[gun:put(3)],
|
||||
link:man:gun:await(3)[gun:await(3)],
|
||||
link:man:gun:await_body(3)[gun:await_body(3)],
|
||||
link:man:gun_push(3)[gun_push(3)],
|
||||
link:man:gun_inform(3)[gun_inform(3)],
|
||||
link:man:gun_response(3)[gun_response(3)],
|
||||
link:man:gun_data(3)[gun_data(3)]
|
|
@ -0,0 +1,119 @@
|
|||
= gun:put(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun:put - Create or replace a resource
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
put(ConnPid, Path, Headers)
|
||||
-> put(ConnPid, Path, Headers, #{})
|
||||
|
||||
put(ConnPid, Path, Headers, ReqOpts)
|
||||
-> StreamRef
|
||||
|
||||
put(ConnPid, Path, Headers, Body)
|
||||
-> put(ConnPid, Path, Headers, Body, #{})
|
||||
|
||||
put(ConnPid, Path, Headers, Body, ReqOpts)
|
||||
-> StreamRef
|
||||
|
||||
ConnPid :: pid()
|
||||
Path :: iodata()
|
||||
Headers :: gun:req_headers()
|
||||
Body :: iodata()
|
||||
ReqOpts :: gun:req_opts()
|
||||
StreamRef :: gun:stream_ref()
|
||||
----
|
||||
|
||||
Create or replace a resource.
|
||||
|
||||
The behavior of this function varies depending on whether
|
||||
a body is provided.
|
||||
|
||||
The function `put/3,4` does not send a body. It must be
|
||||
sent separately using link:man:gun:data(3)[gun:data(3)].
|
||||
|
||||
The function `put/4,5` sends the entire request, including
|
||||
the request body, immediately. It is therefore not possible
|
||||
to use link:man:gun:data(3)[gun:data(3)] after that. You
|
||||
should provide a content-type header. Gun will set the
|
||||
content-length header automatically.
|
||||
|
||||
== Arguments
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
Path::
|
||||
|
||||
Path to the resource.
|
||||
|
||||
Headers::
|
||||
|
||||
Additional request headers.
|
||||
|
||||
Body::
|
||||
|
||||
Request body.
|
||||
|
||||
ReqOpts::
|
||||
|
||||
Request options.
|
||||
|
||||
== Return value
|
||||
|
||||
A reference that identifies the newly created stream is
|
||||
returned. It is this reference that must be passed in
|
||||
subsequent calls and will be received in messages related
|
||||
to this new stream.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *2.0*: Implicit body detection has been removed. The body
|
||||
must now be provided either directly (even if empty)
|
||||
or using separate calls.
|
||||
* *1.0*: Function introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Put a resource
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:put(ConnPid, "/lang/fr_FR/hello",
|
||||
[{<<"content-type">>, <<"text/plain">>}],
|
||||
<<"Bonjour !">>).
|
||||
----
|
||||
|
||||
.Put a resource in multiple calls
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:put(ConnPid, "/lang/fr_FR/hello", [
|
||||
{<<"content-type">>, <<"text/plain">>}
|
||||
]).
|
||||
gun:data(ConnPid, StreamRef, fin, <<"Bonjour !">>).
|
||||
----
|
||||
|
||||
.Put a resource with request options
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:put(ConnPid, "/lang/fr_FR/hello",
|
||||
[{<<"content-type">>, <<"text/plain">>}],
|
||||
<<"Bonjour !">>,
|
||||
#{reply_to => ReplyToPid}).
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:patch(3)[gun:patch(3)],
|
||||
link:man:gun:post(3)[gun:post(3)],
|
||||
link:man:gun:await(3)[gun:await(3)],
|
||||
link:man:gun:await_body(3)[gun:await_body(3)],
|
||||
link:man:gun_push(3)[gun_push(3)],
|
||||
link:man:gun_inform(3)[gun_inform(3)],
|
||||
link:man:gun_response(3)[gun_response(3)],
|
||||
link:man:gun_data(3)[gun_data(3)]
|
|
@ -0,0 +1,97 @@
|
|||
= gun:request(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun:request - Perform the given request
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
request(ConnPid, Method, Path, Headers, Body)
|
||||
-> request(ConnPid, Method, Path, Headers, Body, #{})
|
||||
|
||||
request(ConnPid, Method, Path, Headers, Body, ReqOpts)
|
||||
-> StreamRef
|
||||
|
||||
ConnPid :: pid()
|
||||
Method :: binary()
|
||||
Path :: iodata()
|
||||
Headers :: gun:req_headers()
|
||||
Body :: iodata()
|
||||
ReqOpts :: gun:req_opts()
|
||||
StreamRef :: gun:stream_ref()
|
||||
----
|
||||
|
||||
Perform the given request.
|
||||
|
||||
This is a general purpose function that should only be
|
||||
used when other method-specific functions do not apply.
|
||||
|
||||
The function `request/5,6` sends the entire request, including
|
||||
the request body, immediately. It is therefore not possible
|
||||
to use link:man:gun:data(3)[gun:data(3)] after that. You
|
||||
should provide a content-type header. Gun will set the
|
||||
content-length header automatically.
|
||||
|
||||
== Arguments
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
Method::
|
||||
|
||||
Method to be used for the request.
|
||||
|
||||
Path::
|
||||
|
||||
Path to the resource.
|
||||
|
||||
Headers::
|
||||
|
||||
Additional request headers.
|
||||
|
||||
Body::
|
||||
|
||||
Request body.
|
||||
|
||||
ReqOpts::
|
||||
|
||||
Request options.
|
||||
|
||||
== Return value
|
||||
|
||||
A reference that identifies the newly created stream is
|
||||
returned. It is this reference that must be passed in
|
||||
subsequent calls and will be received in messages related
|
||||
to this new stream.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *2.0*: Implicit body detection has been removed. The body
|
||||
must now be provided either directly (even if empty)
|
||||
or using link:man:gun:headers(3)[gun:headers(3)].
|
||||
* *1.0*: Function introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Perform a request
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:request(ConnPid, <<"PUT">>,
|
||||
"/lang/fr_FR/hello",
|
||||
[{<<"content-type">>, <<"text/plain">>}],
|
||||
<<"Bonjour !">>).
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:headers(3)[gun:headers(3)],
|
||||
link:man:gun:await(3)[gun:await(3)],
|
||||
link:man:gun:await_body(3)[gun:await_body(3)],
|
||||
link:man:gun_push(3)[gun_push(3)],
|
||||
link:man:gun_inform(3)[gun_inform(3)],
|
||||
link:man:gun_response(3)[gun_response(3)],
|
||||
link:man:gun_data(3)[gun_data(3)]
|
|
@ -0,0 +1,57 @@
|
|||
= gun:set_owner(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun:set_owner - Set a new owner for the connection
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
set_owner(ConnPid, OwnerPid) -> ok
|
||||
|
||||
ConnPid :: pid()
|
||||
OwnerPid :: pid()
|
||||
----
|
||||
|
||||
Set a new owner for the connection.
|
||||
|
||||
Only the current owner of the connection can set a new
|
||||
owner.
|
||||
|
||||
Gun monitors the owner of the connection and automatically
|
||||
shuts down gracefully when the owner exits.
|
||||
|
||||
== Arguments
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
OwnerPid::
|
||||
|
||||
The pid of the new owner for the connection.
|
||||
|
||||
== Return value
|
||||
|
||||
The atom `ok` is returned.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *2.0*: Function introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Set a new owner for the connection
|
||||
[source,erlang]
|
||||
----
|
||||
ok = gun:set_owner(ConnPid, OwnerPid).
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:open(3)[gun:open(3)],
|
||||
link:man:gun:open_unix(3)[gun:open_unix(3)],
|
||||
link:man:gun:shutdown(3)[gun:shutdown(3)],
|
||||
link:man:gun:close(3)[gun:close(3)]
|
|
@ -0,0 +1,67 @@
|
|||
= gun:shutdown(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun:shutdown - Gracefully close the connection
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
shutdown(ConnPid) -> ok
|
||||
|
||||
ConnPid :: pid()
|
||||
----
|
||||
|
||||
Gracefully close the connection.
|
||||
|
||||
Gun will wait for up to `closing_timeout` milliseconds
|
||||
before brutally closing the connection. The graceful
|
||||
shutdown mechanism varies between the different protocols:
|
||||
|
||||
* For HTTP/1.1 there is no such mechanism and Gun will
|
||||
close the connection once the current response is
|
||||
received. Any pipelined requests are immediately
|
||||
terminated.
|
||||
|
||||
* For HTTP/2 Gun will send a GOAWAY frame and wait for
|
||||
the existing streams to terminate.
|
||||
|
||||
* For Websocket Gun will send a close frame and wait
|
||||
for the server's close frame before closing the
|
||||
connection.
|
||||
|
||||
The function returns immediately. The connection may
|
||||
therefore still be up for some time after this call.
|
||||
|
||||
Gun will not attempt to reconnect once graceful
|
||||
shutdown has been initiated.
|
||||
|
||||
== Arguments
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
== Return value
|
||||
|
||||
The atom `ok` is returned.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *2.0*: Function introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Gracefully shutdown the connection
|
||||
[source,erlang]
|
||||
----
|
||||
ok = gun:shutdown(ConnPid).
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:open(3)[gun:open(3)],
|
||||
link:man:gun:open_unix(3)[gun:open_unix(3)],
|
||||
link:man:gun:close(3)[gun:close(3)]
|
|
@ -0,0 +1,79 @@
|
|||
= gun:stream_info(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun:stream_info - Obtain information about a stream
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
stream_info(ConnPid, StreamRef) -> {ok, undefined | Info} | {error, not_connected}
|
||||
|
||||
ConnPid :: pid()
|
||||
StreamRef :: gun:stream_ref()
|
||||
Info :: #{
|
||||
ref => gun:stream_ref(),
|
||||
reply_to => pid(),
|
||||
state => running | stopping,
|
||||
intermediaries => [Intermediary],
|
||||
tunnel => Tunnel
|
||||
}
|
||||
Intermediary :: #{
|
||||
type => connect | socks5,
|
||||
host => inet:hostname() | inet:ip_address(),
|
||||
port => inet:port_number(),
|
||||
transport => tcp | tls,
|
||||
protocol => http | http2 | socks | raw
|
||||
}
|
||||
Tunnel :: #{
|
||||
transport => tcp | tls,
|
||||
protocol => http | http2 | socks | raw,
|
||||
origin_scheme => binary() | undefined,
|
||||
origin_host => inet:hostname() | inet:ip_address(),
|
||||
origin_port => inet:port_number()
|
||||
}
|
||||
----
|
||||
|
||||
Obtain information about a stream.
|
||||
|
||||
== Arguments
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
StreamRef::
|
||||
|
||||
Identifier of the stream for the original request.
|
||||
|
||||
== Return value
|
||||
|
||||
A map is returned containing various informations about
|
||||
the stream.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *2.0*: Function introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Obtain information about a stream
|
||||
[source,erlang]
|
||||
----
|
||||
Info = gun:stream_info(ConnPid, StreamRef).
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:get(3)[gun:get(3)],
|
||||
link:man:gun:head(3)[gun:head(3)],
|
||||
link:man:gun:options(3)[gun:options(3)],
|
||||
link:man:gun:patch(3)[gun:patch(3)],
|
||||
link:man:gun:post(3)[gun:post(3)],
|
||||
link:man:gun:put(3)[gun:put(3)],
|
||||
link:man:gun:delete(3)[gun:delete(3)],
|
||||
link:man:gun:headers(3)[gun:headers(3)],
|
||||
link:man:gun:request(3)[gun:request(3)],
|
||||
link:man:gun:cancel(3)[gun:cancel(3)]
|
|
@ -0,0 +1,67 @@
|
|||
= gun:update_flow(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun:update_flow - Update a stream's flow control value
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
update_flow(ConnPid, StreamRef, Flow) -> ok
|
||||
|
||||
ConnPid :: pid()
|
||||
StreamRef :: gun:stream_ref()
|
||||
Flow :: pos_integer()
|
||||
----
|
||||
|
||||
Update a stream's flow control value.
|
||||
|
||||
The flow value can only ever be incremented.
|
||||
|
||||
This function does nothing for streams that have flow
|
||||
control disabled (which is the default).
|
||||
|
||||
== Arguments
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
StreamRef::
|
||||
|
||||
Identifier of the stream for the original request.
|
||||
|
||||
Flow::
|
||||
|
||||
Flow control value increment.
|
||||
|
||||
== Return value
|
||||
|
||||
The atom `ok` is returned.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *2.0*: Function introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Update a stream's flow control value
|
||||
[source,erlang]
|
||||
----
|
||||
gun:update_flow(ConnPid, StreamRef, 10).
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:get(3)[gun:get(3)],
|
||||
link:man:gun:head(3)[gun:head(3)],
|
||||
link:man:gun:options(3)[gun:options(3)],
|
||||
link:man:gun:patch(3)[gun:patch(3)],
|
||||
link:man:gun:post(3)[gun:post(3)],
|
||||
link:man:gun:put(3)[gun:put(3)],
|
||||
link:man:gun:delete(3)[gun:delete(3)],
|
||||
link:man:gun:headers(3)[gun:headers(3)],
|
||||
link:man:gun:request(3)[gun:request(3)],
|
||||
link:man:gun:ws_upgrade(3)[gun:ws_upgrade(3)]
|
|
@ -0,0 +1,72 @@
|
|||
= gun:ws_send(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun:ws_send - Send Websocket frames
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
ws_send(ConnPid, StreamRef, Frames) -> ok
|
||||
|
||||
ConnPid :: pid()
|
||||
StreamRef :: gun:stream_ref()
|
||||
Frames :: Frame | [Frame]
|
||||
Frame :: close | ping | pong
|
||||
| {text | binary | close | ping | pong, iodata()}
|
||||
| {close, non_neg_integer(), iodata()}
|
||||
----
|
||||
|
||||
Send Websocket frames.
|
||||
|
||||
The connection must first be upgraded to Websocket using
|
||||
the function link:man:gun:ws_upgrade(3)[gun:ws_upgrade(3)].
|
||||
|
||||
== Arguments
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
StreamRef::
|
||||
|
||||
Identifier of the stream that was upgraded to Websocket.
|
||||
|
||||
Frames::
|
||||
|
||||
One or more Websocket frame(s).
|
||||
|
||||
== Return value
|
||||
|
||||
The atom `ok` is returned.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *2.0*: The mandatory `StreamRef` argument was added.
|
||||
* *2.0*: It is now possible to send multiple frames at once.
|
||||
* *1.0*: Function introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Send a single frame
|
||||
[source,erlang]
|
||||
----
|
||||
gun:ws_send(ConnPid, StreamRef, {text, <<"Hello world!">>}).
|
||||
----
|
||||
|
||||
.Send many frames including a close frame
|
||||
[source,erlang]
|
||||
----
|
||||
gun:ws_send(ConnPid, StreamRef, [
|
||||
{text, <<"See you later, world!">>},
|
||||
close
|
||||
]).
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:ws_upgrade(3)[gun:ws_upgrade(3)],
|
||||
link:man:gun_upgrade(3)[gun_upgrade(3)],
|
||||
link:man:gun_ws(3)[gun_ws(3)]
|
|
@ -0,0 +1,117 @@
|
|||
= gun:ws_upgrade(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun:ws_upgrade - Upgrade to Websocket
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
ws_upgrade(ConnPid, Path)
|
||||
-> ws_upgrade(ConnPid, Path, [])
|
||||
|
||||
ws_upgrade(ConnPid, Path, Headers)
|
||||
-> StreamRef
|
||||
|
||||
ws_upgrade(ConnPid, Path, Headers, WsOpts)
|
||||
-> StreamRef
|
||||
|
||||
ConnPid :: pid()
|
||||
Path :: iodata()
|
||||
Headers :: gun:req_headers()
|
||||
WsOpts :: gun:ws_opts()
|
||||
StreamRef :: gun:stream_ref()
|
||||
----
|
||||
|
||||
Upgrade to Websocket.
|
||||
|
||||
The behavior of this function depends on the protocol
|
||||
selected.
|
||||
|
||||
HTTP/1.1 cannot handle Websocket and HTTP requests
|
||||
concurrently. The upgrade, if successful, will result
|
||||
in the complete takeover of the connection. Any
|
||||
subsequent HTTP requests will be rejected.
|
||||
|
||||
Gun does not currently support Websocket over HTTP/2.
|
||||
|
||||
By default Gun will take the Websocket options from
|
||||
the connection's `ws_opts`.
|
||||
|
||||
Websocket subprotocol negotiation is enabled when
|
||||
the `protocols` option is given. It takes a subprotocol
|
||||
name and a module implementing the
|
||||
link:man:gun_ws_protocol(3)[gun_ws_protocol(3)] behavior.
|
||||
|
||||
== Arguments
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
Path::
|
||||
|
||||
Path to the resource.
|
||||
|
||||
Headers::
|
||||
|
||||
Additional request headers.
|
||||
|
||||
WsOpts::
|
||||
|
||||
Configuration for the Websocket protocol.
|
||||
|
||||
== Return value
|
||||
|
||||
A reference that identifies the newly created stream is
|
||||
returned. It is this reference that must be passed in
|
||||
subsequent calls and will be received in messages related
|
||||
to this new stream.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *1.0*: Function introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Upgrade to Websocket
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:ws_upgrade(ConnPid, "/ws", [
|
||||
{<<"sec-websocket-protocol">>, <<"chat">>}
|
||||
]).
|
||||
receive
|
||||
{gun_upgrade, ConnPid, StreamRef, [<<"websocket">>], _} ->
|
||||
ok
|
||||
after 5000 ->
|
||||
error(timeout)
|
||||
end.
|
||||
----
|
||||
|
||||
.Upgrade to Websocket with different options
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:ws_upgrade(ConnPid, "/ws", [], #{
|
||||
compress => false
|
||||
}).
|
||||
----
|
||||
|
||||
.Upgrade to Websocket with protocol negotiation
|
||||
[source,erlang]
|
||||
----
|
||||
StreamRef = gun:ws_upgrade(ConnPid, "/ws", [], #{
|
||||
protocols => [
|
||||
{<<"mqtt">>, gun_ws_mqtt_h},
|
||||
{<<"v12.stomp">>, gun_ws_stomp_h}
|
||||
]
|
||||
}).
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:ws_send(3)[gun:ws_send(3)],
|
||||
link:man:gun_upgrade(3)[gun_upgrade(3)],
|
||||
link:man:gun_ws(3)[gun_ws(3)],
|
||||
link:man:gun_ws_protocol(3)[gun_ws_protocol(3)]
|
|
@ -0,0 +1,44 @@
|
|||
= gun(7)
|
||||
|
||||
== Name
|
||||
|
||||
gun - HTTP/1.1, HTTP/2 and Websocket client for Erlang/OTP
|
||||
|
||||
== Description
|
||||
|
||||
Gun is an HTTP client for Erlang/OTP with support for the
|
||||
HTTP/1.1, HTTP/2 and Websocket protocols.
|
||||
|
||||
Gun aims to provide an easy to use, asynchronous and
|
||||
always-connected client. It maintains a permanent connection
|
||||
to the server and reconnects automatically when necessary.
|
||||
|
||||
== Modules
|
||||
|
||||
* link:man:gun(3)[gun(3)] - Asynchronous HTTP client
|
||||
* link:man:gun_cookies(3)[gun_cookies(3)] - Cookie store engine
|
||||
* link:man:gun_cookies_list(3)[gun_cookies_list(3)] - Cookie store backend: in-memory, per connection
|
||||
* link:man:gun_event(3)[gun_event(3)] - Events
|
||||
* link:man:gun_ws_protocol(3)[gun_ws_protocol(3)] - Websocket subprotocols
|
||||
|
||||
== Dependencies
|
||||
|
||||
* link:man:cowlib(7)[cowlib(7)] - Support library for manipulating Web protocols
|
||||
* ssl - Secure communication over sockets
|
||||
|
||||
All these applications must be started before the `gun`
|
||||
application. To start Gun and all dependencies at once:
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
{ok, _} = application:ensure_all_started(gun).
|
||||
----
|
||||
|
||||
== Environment
|
||||
|
||||
The `gun` application does not define any application
|
||||
environment configuration parameters.
|
||||
|
||||
== See also
|
||||
|
||||
link:man:cowlib(7)[cowlib(7)]
|
|
@ -0,0 +1,179 @@
|
|||
= gun_cookies(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun_cookies - Cookie store engine
|
||||
|
||||
== Description
|
||||
|
||||
The `gun_cookies` module implements a cookie store engine.
|
||||
It will be used by Gun when a cookie store is configured.
|
||||
It also defines the interface and provides functions used
|
||||
to implement cookie store backends.
|
||||
|
||||
== Callbacks
|
||||
|
||||
Cookie store backends implement the following interface.
|
||||
Functions are organized by theme: initialization, querying,
|
||||
storing and garbage collecting:
|
||||
|
||||
=== init
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
init(Opts :: any()) -> gun_cookies:store()
|
||||
----
|
||||
|
||||
Initialize the cookie store.
|
||||
|
||||
=== query
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
query(State, URI) -> {ok, [Cookie], State}
|
||||
|
||||
URI :: uri_string:uri_map()
|
||||
Cookie :: gun_cookies:cookie()
|
||||
State :: any()
|
||||
----
|
||||
|
||||
Query the store for the cookies for the given URI.
|
||||
|
||||
=== set_cookie_secure_match
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
set_cookie_secure_match(State, Match) -> match | nomatch
|
||||
|
||||
State :: any()
|
||||
Match :: #{
|
||||
name := binary(),
|
||||
% secure_only := true,
|
||||
domain := binary(),
|
||||
path := binary()
|
||||
}
|
||||
----
|
||||
|
||||
Perform a secure match against cookies already in the store.
|
||||
This is part of the heuristics that the cookie store engine
|
||||
applies to decide whether the cookie must be stored.
|
||||
|
||||
The `secure_only` attribute is implied, it is not actually
|
||||
passed in the argument.
|
||||
|
||||
=== set_cookie_get_exact_match
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
set_cookie_get_exact_match(State, Match)
|
||||
-> {ok, gun_cookies:cookie(), State} | error
|
||||
|
||||
State :: any()
|
||||
Match :: #{
|
||||
name := binary(),
|
||||
domain := binary(),
|
||||
host_only := boolean(),
|
||||
path := binary()
|
||||
}
|
||||
----
|
||||
|
||||
Perform an exact match against cookies already in the store.
|
||||
This is part of the heuristics that the cookie store engine
|
||||
applies to decide whether the cookie must be stored.
|
||||
|
||||
When a cookie is found, it must be returned so that it gets
|
||||
updated. When nothing is found a new cookie will be stored.
|
||||
|
||||
=== store
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
store(State, gun_cookies:cookie())
|
||||
-> {ok, State} | {error, any()}
|
||||
|
||||
State :: any()
|
||||
----
|
||||
|
||||
Unconditionally store the cookie into the cookie store.
|
||||
|
||||
=== gc
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
gc(State) -> {ok, State}
|
||||
|
||||
State :: any()
|
||||
----
|
||||
|
||||
Remove all cookies from the cookie store that are expired.
|
||||
|
||||
Other cookies may be removed as well, at the discretion
|
||||
of the cookie store. For example excess cookies may be
|
||||
removed to reduce the memory footprint.
|
||||
|
||||
=== session_gc
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
session_gc(State) -> {ok, State}
|
||||
|
||||
State :: any()
|
||||
----
|
||||
|
||||
Remove all cookies from the cookie store that have the
|
||||
`persistent` flag set to `false`.
|
||||
|
||||
== Exports
|
||||
|
||||
* link:man:gun_cookies:domain_match(3)[gun_cookies:domain_match(3)] - Cookie domain match
|
||||
* link:man:gun_cookies:path_match(3)[gun_cookies:path_match(3)] - Cookie path match
|
||||
|
||||
== Types
|
||||
|
||||
=== cookie()
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
cookie() :: #{
|
||||
name := binary(),
|
||||
value := binary(),
|
||||
domain := binary(),
|
||||
path := binary(),
|
||||
creation_time := calendar:datetime(),
|
||||
last_access_time := calendar:datetime(),
|
||||
expiry_time := calendar:datetime() | infinity,
|
||||
persistent := boolean(),
|
||||
host_only := boolean(),
|
||||
secure_only := boolean(),
|
||||
http_only := boolean(),
|
||||
same_site := strict | lax | none
|
||||
}
|
||||
----
|
||||
|
||||
A cookie.
|
||||
|
||||
This contains the cookie name, value, attributes and flags.
|
||||
This is the representation that the cookie store engine
|
||||
and Gun expects. Cookies do not have to be kept in this
|
||||
format in the cookie store backend.
|
||||
|
||||
=== store()
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
store() :: {module(), StoreState :: any()}
|
||||
----
|
||||
|
||||
The cookie store.
|
||||
|
||||
This is a tuple containing the cookie store backend module
|
||||
and its current state.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *2.0*: Module introduced.
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(7)[gun(7)],
|
||||
link:man:gun_cookies_list(3)[gun_cookies_list(3)]
|
|
@ -0,0 +1,52 @@
|
|||
= gun_cookies:domain_match(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun_cookies:domain_match - Cookie domain match
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
domain_match(String, DomainString) -> boolean()
|
||||
|
||||
String :: binary()
|
||||
DomainString :: binary()
|
||||
----
|
||||
|
||||
Cookie domain match.
|
||||
|
||||
This function can be used when implementing the
|
||||
`set_cookie_secure_match` callback of a cookie store.
|
||||
|
||||
== Arguments
|
||||
|
||||
String::
|
||||
|
||||
The string to match.
|
||||
|
||||
DomainString::
|
||||
|
||||
The domain string that will be matched against.
|
||||
|
||||
== Return value
|
||||
|
||||
Returns `true` when `String` domain-matches `DomainString`,
|
||||
and `false` otherwise.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *2.0*: Function introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Perform a domain match
|
||||
[source,erlang]
|
||||
----
|
||||
Match = gun_cookies:domain_match(Domain, CookieDomain).
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun_cookies(3)[gun_cookies(3)],
|
||||
link:man:gun_cookies:path_match(3)[gun_cookies:path_match(3)]
|
|
@ -0,0 +1,52 @@
|
|||
= gun_cookies:path_match(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun_cookies:path_match - Cookie path match
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
path_match(ReqPath, CookiePath) -> boolean()
|
||||
|
||||
ReqPath :: binary()
|
||||
CookiePath :: binary()
|
||||
----
|
||||
|
||||
Cookie path match.
|
||||
|
||||
This function can be used when implementing the
|
||||
`set_cookie_secure_match` callback of a cookie store.
|
||||
|
||||
== Arguments
|
||||
|
||||
ReqPath::
|
||||
|
||||
The request path to match.
|
||||
|
||||
CookiePath::
|
||||
|
||||
The cookie path that will be matched against.
|
||||
|
||||
== Return value
|
||||
|
||||
Returns `true` when `ReqPath` path-matches `CookiePath`,
|
||||
and `false` otherwise.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *2.0*: Function introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Perform a path match
|
||||
[source,erlang]
|
||||
----
|
||||
Match = gun_cookies:path_match(ReqPath, CookiePath).
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun_cookies(3)[gun_cookies(3)],
|
||||
link:man:gun_cookies:domain_match(3)[gun_cookies:domain_match(3)]
|
|
@ -0,0 +1,55 @@
|
|||
= gun_cookies_list(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun_cookies_list - Cookie store backend: in-memory, per connection
|
||||
|
||||
== Description
|
||||
|
||||
The `gun_cookies_list` module implements a cookie store
|
||||
backend that keeps all the cookie data in-memory and tied
|
||||
to a specific connection.
|
||||
|
||||
It is possible to implement a custom backend on top of
|
||||
`gun_cookies_list` in order to add persistence or sharing
|
||||
properties.
|
||||
|
||||
== Exports
|
||||
|
||||
This module implements the callbacks defined in
|
||||
link:man:gun_cookies(3)[gun_cookies(3)].
|
||||
|
||||
== Types
|
||||
|
||||
=== opts()
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
opts() :: #{
|
||||
}
|
||||
----
|
||||
|
||||
Cookie store backend options.
|
||||
|
||||
There are currently no options available for this backend.
|
||||
|
||||
// The default value is given next to the option name:
|
||||
|
||||
== Changelog
|
||||
|
||||
* *2.0*: Module introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Open a connection with a cookie store configured
|
||||
[source,erlang]
|
||||
----
|
||||
{ok, ConnPid} = gun:open(Host, Port, #{
|
||||
cookie_store => gun_cookies_list:init(#{})
|
||||
})
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(7)[gun(7)],
|
||||
link:man:gun_cookies(3)[gun_cookies(3)]
|
|
@ -0,0 +1,78 @@
|
|||
= gun_data(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun_data - Response body
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
{gun_data, ConnPid, StreamRef, IsFin, Data}
|
||||
|
||||
ConnPid :: pid()
|
||||
StreamRef :: gun:stream_ref()
|
||||
IsFin :: fin | nofin
|
||||
Data :: binary()
|
||||
----
|
||||
|
||||
Response body.
|
||||
|
||||
This message informs the relevant process that the server
|
||||
sent a all or part of the body for the response to the
|
||||
original request.
|
||||
|
||||
A data message is always preceded by a response message.
|
||||
|
||||
The response body may be terminated either by a data
|
||||
message with the flag `fin` set or by a
|
||||
link:man:gun_trailers(3)[gun_trailers(3)] message.
|
||||
|
||||
== Elements
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
StreamRef::
|
||||
|
||||
Identifier of the stream for the original request.
|
||||
|
||||
IsFin::
|
||||
|
||||
Whether this message terminates the response.
|
||||
|
||||
Data::
|
||||
|
||||
All or part of the response body.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *1.0*: Message introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Receive a gun_data message in a gen_server
|
||||
[source,erlang]
|
||||
----
|
||||
handle_info({gun_data, ConnPid, _StreamRef,
|
||||
_IsFin, _Data},
|
||||
State=#state{conn_pid=ConnPid}) ->
|
||||
%% Do something.
|
||||
{noreply, State}.
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:get(3)[gun:get(3)],
|
||||
link:man:gun:head(3)[gun:head(3)],
|
||||
link:man:gun:patch(3)[gun:patch(3)],
|
||||
link:man:gun:post(3)[gun:post(3)],
|
||||
link:man:gun:put(3)[gun:put(3)],
|
||||
link:man:gun:delete(3)[gun:delete(3)],
|
||||
link:man:gun:options(3)[gun:options(3)],
|
||||
link:man:gun:headers(3)[gun:headers(3)],
|
||||
link:man:gun:request(3)[gun:request(3)],
|
||||
link:man:gun_response(3)[gun_response(3)],
|
||||
link:man:gun_trailers(3)[gun_trailers(3)]
|
|
@ -0,0 +1,79 @@
|
|||
= gun_down(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun_down - The connection is down
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
{gun_down, ConnPid, Protocol, Reason, KilledStreams}
|
||||
|
||||
ConnPid :: pid()
|
||||
Protocol :: http | http2 | socks | ws
|
||||
Reason :: any()
|
||||
KilledStreams :: [gun:stream_ref()]
|
||||
----
|
||||
|
||||
The connection is down.
|
||||
|
||||
This message informs the owner process that the connection
|
||||
was lost. Depending on the `retry` and `retry_timeout`
|
||||
options Gun may automatically attempt to reconnect.
|
||||
|
||||
When the connection goes back up, Gun will not attempt to retry
|
||||
requests. It will also not upgrade to Websocket automatically
|
||||
if that was the protocol in use when the connection was lost.
|
||||
|
||||
== Elements
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
Protocol::
|
||||
|
||||
The protocol that was selected for this connection
|
||||
or upgraded to during the course of the connection.
|
||||
|
||||
Reason::
|
||||
|
||||
The reason for the loss of the connection.
|
||||
+
|
||||
It is present for debugging purposes only. You should not
|
||||
rely on this value to perform operations programmatically.
|
||||
|
||||
KilledStreams::
|
||||
|
||||
List of streams that have been brutally terminated.
|
||||
+
|
||||
They are active streams that did not complete before the closing
|
||||
of the connection. Whether they can be retried safely depends
|
||||
on the protocol used and the idempotence property of the requests.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *2.0*: The last element of the message's tuple, `UnprocessedStreams`
|
||||
has been removed.
|
||||
* *1.0*: Message introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Receive a gun_down message in a gen_server
|
||||
[source,erlang]
|
||||
----
|
||||
handle_info({gun_down, ConnPid, _Protocol, _Reason, _Killed},
|
||||
State=#state{conn_pid=ConnPid}) ->
|
||||
%% Do something.
|
||||
{noreply, State}.
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:open(3)[gun:open(3)],
|
||||
link:man:gun:open_unix(3)[gun:open_unix(3)],
|
||||
link:man:gun_up(3)[gun_up(3)],
|
||||
link:man:gun_tunnel_up(3)[gun_tunnel_up(3)],
|
||||
link:man:gun_error(3)[gun_error(3)]
|
|
@ -0,0 +1,68 @@
|
|||
= gun_error(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun_error - Stream or connection-wide error
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
{gun_error, ConnPid, StreamRef, Reason}
|
||||
{gun_error, ConnPid, Reason}
|
||||
|
||||
ConnPid :: pid()
|
||||
StreamRef :: gun:stream_ref()
|
||||
Reason :: any()
|
||||
----
|
||||
|
||||
Stream or connection-wide error.
|
||||
|
||||
These messages inform the relevant process that an error
|
||||
occurred. A reference is given when the error pertains
|
||||
to a specific stream. Connection-wide errors do not
|
||||
imply that the connection is no longer usable, they are
|
||||
used for all errors that are not specific to a stream.
|
||||
|
||||
== Elements
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
StreamRef::
|
||||
|
||||
Identifier of the stream that resulted in an error.
|
||||
|
||||
Reason::
|
||||
|
||||
The reason for the error.
|
||||
+
|
||||
It is present for debugging purposes only. You should not
|
||||
rely on this value to perform operations programmatically.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *1.0*: Message introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Receive a gun_error message in a gen_server
|
||||
[source,erlang]
|
||||
----
|
||||
handle_info({gun_error, ConnPid, _Reason},
|
||||
State=#state{conn_pid=ConnPid}) ->
|
||||
%% Do something.
|
||||
{noreply, State};
|
||||
handle_info({gun_error, ConnPid, _StreamRef, _Reason},
|
||||
State=#state{conn_pid=ConnPid}) ->
|
||||
%% Do something.
|
||||
{noreply, State}.
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun_up(3)[gun_up(3)],
|
||||
link:man:gun_tunnel_up(3)[gun_tunnel_up(3)],
|
||||
link:man:gun_down(3)[gun_down(3)]
|
|
@ -0,0 +1,509 @@
|
|||
= gun_event(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun_event - Events
|
||||
|
||||
== Description
|
||||
|
||||
The `gun_event` module provides the callback interface
|
||||
and types for implementing event handlers.
|
||||
|
||||
== Callbacks
|
||||
|
||||
Event handlers implement the following interface.
|
||||
Because types are tied to specific events, they
|
||||
are documented alongside them. All event types
|
||||
are exported and can be referred to as `gun_event:Type()`.
|
||||
|
||||
The events are ordered by the order they are likely
|
||||
to be triggered, with the most frequent events listed
|
||||
first.
|
||||
|
||||
=== init
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
init_event() :: #{
|
||||
owner := pid(),
|
||||
transport := tcp | tls,
|
||||
origin_scheme := binary(),
|
||||
origin_host := inet:hostname() | inet:ip_address(),
|
||||
origin_port := inet:port_number(),
|
||||
opts := gun:opts()
|
||||
}
|
||||
|
||||
init(init_event(), State) -> State
|
||||
----
|
||||
|
||||
Gun is initializing.
|
||||
|
||||
=== domain_lookup_start
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
domain_lookup_event() :: #{
|
||||
host := inet:hostname() | inet:ip_address(),
|
||||
port := inet:port_number(),
|
||||
tcp_opts := [gen_tcp:connect_option()],
|
||||
timeout := timeout(),
|
||||
lookup_info => gun_tcp:lookup_info(),
|
||||
error => any()
|
||||
}
|
||||
|
||||
domain_lookup_start(domain_lookup_event(), State) -> State
|
||||
----
|
||||
|
||||
Gun is starting to resolve the host address.
|
||||
|
||||
The `lookup_info` and `error` keys are never set
|
||||
for this event.
|
||||
|
||||
=== domain_lookup_end
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
domain_lookup_end(domain_lookup_event(), State) -> State
|
||||
----
|
||||
|
||||
Gun has finished resolving the host address.
|
||||
|
||||
The `lookup_info` key is only set when the lookup is
|
||||
successful. The `error` key is set otherwise.
|
||||
|
||||
=== connect_start
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
connect_event() :: #{
|
||||
lookup_info := gun_tcp:lookup_info(),
|
||||
timeout := timeout(),
|
||||
socket => inet:socket(),
|
||||
protocol => http | http2 | socks | raw,
|
||||
error => any()
|
||||
}
|
||||
|
||||
connect_start(connect_event(), State) -> State
|
||||
----
|
||||
|
||||
Gun is starting to connect to the host address and port.
|
||||
|
||||
The `socket`, `protocol` and `error` keys are never set
|
||||
for this event.
|
||||
|
||||
=== connect_end
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
connect_end(connect_event(), State) -> State
|
||||
----
|
||||
|
||||
Gun has finished connecting to the host address and port.
|
||||
|
||||
The `socket` key is set on connect success. The `error`
|
||||
key is set otherwise.
|
||||
|
||||
The `protocol` key is only set when the transport is
|
||||
`tcp` and the connection is successful. The protocol
|
||||
is only known in the `tls_handshake_end` event otherwise.
|
||||
|
||||
=== tls_handshake_start
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
tls_handshake_event() :: #{
|
||||
stream_ref => gun:stream_ref(),
|
||||
reply_to => pid(),
|
||||
socket := inet:socket() | ssl:sslsocket() | pid(), %% The socket before/after will be different.
|
||||
tls_opts := [ssl:tls_client_option()],
|
||||
timeout := timeout(),
|
||||
protocol => http | http2 | socks | raw,
|
||||
error => any()
|
||||
}
|
||||
|
||||
tls_handshake_start(tls_handshake_event(), State) -> State
|
||||
----
|
||||
|
||||
Gun has started a TLS handshake.
|
||||
|
||||
This and the `tls_handshake_end` event only occur when
|
||||
connecting to a TLS server or when upgrading the connection
|
||||
or a stream to use TLS, for example when using CONNECT or
|
||||
when connecting to a secure SOCKS server.
|
||||
|
||||
The `stream_ref` and `reply_to` keys are only set when the
|
||||
TLS handshake occurs as a result of a CONNECT request or
|
||||
inside an existing CONNECT tunnel.
|
||||
|
||||
The `protocol` and `error` keys are never set for this event.
|
||||
|
||||
=== tls_handshake_end
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
tls_handshake_end(tls_handshake_event(), State) -> State
|
||||
----
|
||||
|
||||
Gun has finished a TLS handshake.
|
||||
|
||||
The `protocol` key is set on TLS handshake success. The
|
||||
`error` key is set otherwise.
|
||||
|
||||
=== request_start
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
request_start_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
reply_to := pid(),
|
||||
function := headers | request | ws_upgrade | connect,
|
||||
method := iodata(),
|
||||
scheme => binary(),
|
||||
authority := iodata(),
|
||||
path => iodata(),
|
||||
headers := [{binary(), iodata()}]
|
||||
}
|
||||
|
||||
request_start(request_start_event(), State) -> State
|
||||
----
|
||||
|
||||
Gun is starting to send a request.
|
||||
|
||||
The `scheme` and `path` keys are never set when the `function`
|
||||
is set to `connect`.
|
||||
|
||||
=== request_headers
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
request_headers(request_start_event(), State) -> State
|
||||
----
|
||||
|
||||
Gun has finished sending the request headers.
|
||||
|
||||
=== request_end
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
request_end_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
reply_to := pid()
|
||||
}
|
||||
|
||||
request_end(request_end_event(), State) -> State
|
||||
----
|
||||
|
||||
Gun has finished sending the request.
|
||||
|
||||
=== push_promise_start
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
push_promise_start_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
reply_to := pid()
|
||||
}
|
||||
|
||||
push_promise_start(push_promise_start_event(), State) -> State
|
||||
----
|
||||
|
||||
Gun has begun receiving a promised request (server push).
|
||||
|
||||
=== push_promise_end
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
push_promise_end_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
reply_to := pid(),
|
||||
promised_stream_ref => gun:stream_ref(),
|
||||
method := binary(),
|
||||
uri := binary(),
|
||||
headers := [{binary(), iodata()}]
|
||||
}
|
||||
|
||||
push_promise_end(push_promise_end_event(), State) -> State
|
||||
----
|
||||
|
||||
Gun has finished receiving a promised request (server push).
|
||||
Promised requests never include a body.
|
||||
|
||||
Promised requests received during the graceful shutdown of the
|
||||
connection get canceled immediately.
|
||||
|
||||
// @todo The cancel event should probably trigger as well.
|
||||
|
||||
=== response_start
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
response_start_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
reply_to := pid()
|
||||
}
|
||||
|
||||
response_start(response_start_event(), State) -> State
|
||||
----
|
||||
|
||||
Gun has begun receiving a response.
|
||||
|
||||
=== response_inform
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
response_headers_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
reply_to := pid(),
|
||||
status := non_neg_integer(),
|
||||
headers := [{binary(), binary()}]
|
||||
}
|
||||
|
||||
response_inform(response_headers_event(), State) -> State
|
||||
----
|
||||
|
||||
Gun has received an informational response (1xx status code).
|
||||
|
||||
A `status` with value 101 indicates that the response has
|
||||
concluded as the stream will be upgraded to a new protocol.
|
||||
|
||||
=== response_headers
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
response_headers(response_headers_event(), State) -> State
|
||||
----
|
||||
|
||||
Gun has finished receiving response headers.
|
||||
|
||||
=== response_trailers
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
response_trailers_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
reply_to := pid(),
|
||||
headers := [{binary(), binary()}]
|
||||
}
|
||||
|
||||
response_trailers(response_trailers_event(), State) -> State
|
||||
----
|
||||
|
||||
Gun has received response trailers.
|
||||
|
||||
=== response_end
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
response_end_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
reply_to := pid()
|
||||
}
|
||||
|
||||
response_end(response_end_event(), State) -> State
|
||||
----
|
||||
|
||||
Gun has finished receiving a response.
|
||||
|
||||
=== ws_upgrade
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
ws_upgrade_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
reply_to := pid(),
|
||||
opts := gun:ws_opts()
|
||||
}
|
||||
|
||||
ws_upgrade(ws_upgrade_event(), State) -> State
|
||||
----
|
||||
|
||||
A Websocket upgrade was requested.
|
||||
|
||||
Success is indicated by a response (101 informational
|
||||
if HTTP/1.1, 2xx if HTTP/2) followed by a `protocol_changed`
|
||||
event.
|
||||
|
||||
=== ws_recv_frame_start
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
ws_recv_frame_start_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
reply_to := pid(),
|
||||
frag_state := cow_ws:frag_state(),
|
||||
extensions := cow_ws:extensions()
|
||||
}
|
||||
|
||||
ws_recv_frame_start(ws_recv_frame_start_event(), State) -> State
|
||||
----
|
||||
|
||||
Gun has begun receiving a Websocket frame.
|
||||
|
||||
=== ws_recv_frame_header
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
ws_recv_frame_header_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
reply_to := pid(),
|
||||
frag_state := cow_ws:frag_state(),
|
||||
extensions := cow_ws:extensions(),
|
||||
type := cow_ws:frame_type(),
|
||||
rsv := cow_ws:rsv(),
|
||||
len := non_neg_integer(),
|
||||
mask_key := cow_ws:mask_key()
|
||||
}
|
||||
|
||||
ws_recv_frame_header(ws_recv_frame_header_event(), State) -> State
|
||||
----
|
||||
|
||||
Gun has received the header part of a Websocket frame.
|
||||
|
||||
It will be immediately be followed by the frame's payload.
|
||||
|
||||
=== ws_recv_frame_end
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
ws_recv_frame_end_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
reply_to := pid(),
|
||||
extensions := cow_ws:extensions(),
|
||||
close_code := undefined | cow_ws:close_code(),
|
||||
payload := binary()
|
||||
}
|
||||
|
||||
ws_recv_frame_end(ws_recv_frame_end_event(), State) -> State
|
||||
----
|
||||
|
||||
Gun has finished receiving a Websocket frame.
|
||||
|
||||
=== ws_send_frame_start
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
ws_send_frame_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
reply_to := pid(),
|
||||
extensions := cow_ws:extensions(),
|
||||
frame := gun:ws_frame()
|
||||
}
|
||||
|
||||
ws_send_frame_start(ws_send_frame_event(), State) -> State
|
||||
----
|
||||
|
||||
Gun has started sending a Websocket frame.
|
||||
|
||||
=== ws_send_frame_end
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
ws_send_frame_end(ws_send_frame_event(), State) -> State
|
||||
----
|
||||
|
||||
Gun has finished sending a Websocket frame.
|
||||
|
||||
=== protocol_changed
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
protocol_changed_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
protocol := http | http2 | socks | raw | ws
|
||||
}
|
||||
|
||||
protocol_changed(protocol_changed_event(), State) -> State
|
||||
----
|
||||
|
||||
The protocol has changed for either the entire Gun connection
|
||||
or for a specific stream.
|
||||
|
||||
The `stream_ref` key is only set when the protocol has
|
||||
changed for a specific stream or inside a CONNECT tunnel.
|
||||
|
||||
This event occurs during successful Websocket upgrades,
|
||||
as a result of successful CONNECT requests or after a
|
||||
SOCKS tunnel was successfully established.
|
||||
|
||||
=== origin_changed
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
origin_changed_event() :: #{
|
||||
stream_ref => gun:stream_ref(),
|
||||
type := connect | socks5,
|
||||
origin_scheme := binary(),
|
||||
origin_host := inet:hostname() | inet:ip_address(),
|
||||
origin_port := inet:port_number()
|
||||
}
|
||||
|
||||
origin_changed(origin_changed_event(), State) -> State
|
||||
----
|
||||
|
||||
The origin server has changed for either the Gun connection
|
||||
or for a specific stream.
|
||||
|
||||
The `stream_ref` key is only set when the origin has
|
||||
changed for a specific stream or inside a CONNECT tunnel.
|
||||
|
||||
=== cancel
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
cancel_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
reply_to := pid(),
|
||||
endpoint := local | remote,
|
||||
reason := atom()
|
||||
}
|
||||
|
||||
cancel(cancel_event(), State) -> State
|
||||
----
|
||||
|
||||
A stream has been canceled.
|
||||
|
||||
HTTP/1.1 streams can't be canceled at the protocol level. In
|
||||
this case Gun will silence the stream for the user but events
|
||||
may still occur.
|
||||
|
||||
HTTP/2 streams can be canceled both by the client and the
|
||||
server. Events may still occur for a short time after the
|
||||
stream has been canceled.
|
||||
|
||||
=== disconnect
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
disconnect_event() :: #{
|
||||
reason := normal | closed | {error, any()}
|
||||
}
|
||||
|
||||
disconnect(disconnect_event(), State) -> State
|
||||
----
|
||||
|
||||
Gun has been disconnected from the server.
|
||||
|
||||
=== terminate
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
terminate_event() :: #{
|
||||
state := not_connected
|
||||
| domain_lookup | connecting | initial_tls_handshake | tls_handshake
|
||||
| connected | connected_data_only | connected_ws_only,
|
||||
reason := normal | shutdown | {shutdown, any()} | any()
|
||||
}
|
||||
|
||||
terminate(terminate_event(), State) -> State
|
||||
----
|
||||
|
||||
Gun is terminating.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *2.0*: Module introduced.
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(7)[gun(7)],
|
||||
link:man:gun(3)[gun(3)]
|
|
@ -0,0 +1,70 @@
|
|||
= gun_inform(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun_inform - Informational response
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
{gun_inform, ConnPid, StreamRef, Status, Headers}
|
||||
|
||||
ConnPid :: pid()
|
||||
StreamRef :: gun:stream_ref()
|
||||
Status :: 100..199
|
||||
Headers :: [{binary(), binary()}]
|
||||
----
|
||||
|
||||
Informational response.
|
||||
|
||||
This message informs the relevant process that the server
|
||||
sent an informational response to the original request.
|
||||
|
||||
Informational responses are only intermediate responses
|
||||
and provide no guarantees as to what the final response
|
||||
will be. An informational response always precedes the
|
||||
response to the original request.
|
||||
|
||||
== Elements
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
StreamRef::
|
||||
|
||||
Identifier of the stream for the original request.
|
||||
|
||||
Status::
|
||||
|
||||
Status code for the informational response.
|
||||
|
||||
Headers::
|
||||
|
||||
Headers sent with the informational response.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *1.0*: Message introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Receive a gun_inform message in a gen_server
|
||||
[source,erlang]
|
||||
----
|
||||
handle_info({gun_inform, ConnPid, _StreamRef,
|
||||
_Status, _Headers},
|
||||
State=#state{conn_pid=ConnPid}) ->
|
||||
%% Do something.
|
||||
{noreply, State}.
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:get(3)[gun:get(3)],
|
||||
link:man:gun:patch(3)[gun:patch(3)],
|
||||
link:man:gun:post(3)[gun:post(3)],
|
||||
link:man:gun:put(3)[gun:put(3)],
|
||||
link:man:gun_response(3)[gun_response(3)]
|
|
@ -0,0 +1,90 @@
|
|||
= gun_push(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun_push - Server-initiated push
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
{gun_push, ConnPid, StreamRef, NewStreamRef, Method, URI, Headers}
|
||||
|
||||
ConnPid :: pid()
|
||||
StreamRef :: gun:stream_ref()
|
||||
NewStreamRef :: gun:stream_ref()
|
||||
Method :: binary()
|
||||
URI :: binary()
|
||||
Headers :: [{binary(), binary()}]
|
||||
----
|
||||
|
||||
Server-initiated push.
|
||||
|
||||
This message informs the relevant process that the server
|
||||
is pushing a resource related to the effective target URI
|
||||
of the original request.
|
||||
|
||||
A server-initiated push message always precedes the response
|
||||
to the original request.
|
||||
|
||||
This message will not be sent when using the HTTP/1.1 protocol
|
||||
because it lacks the concept of server-initiated push.
|
||||
|
||||
== Elements
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
StreamRef::
|
||||
|
||||
Identifier of the stream for the original request.
|
||||
|
||||
NewStreamRef::
|
||||
|
||||
Identifier of the stream being pushed.
|
||||
|
||||
Method::
|
||||
|
||||
Method of the equivalent HTTP request.
|
||||
|
||||
URI::
|
||||
|
||||
URI of the resource being pushed.
|
||||
|
||||
Headers::
|
||||
|
||||
Headers of the equivalent HTTP request.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *1.0*: Message introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Receive a gun_push message in a gen_server
|
||||
[source,erlang]
|
||||
----
|
||||
handle_info({gun_push, ConnPid, _StreamRef,
|
||||
_NewStreamRef, _Method, _URI, _Headers},
|
||||
State=#state{conn_pid=ConnPid}) ->
|
||||
%% Do something.
|
||||
{noreply, State}.
|
||||
----
|
||||
|
||||
.Cancel an unwanted push
|
||||
[source,erlang]
|
||||
----
|
||||
handle_info({gun_push, ConnPid, _StreamRef,
|
||||
NewStreamRef, _Method, _URI, _Headers},
|
||||
State=#state{conn_pid=ConnPid}) ->
|
||||
gun:cancel(ConnPid, NewStreamRef),
|
||||
{noreply, State}.
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:get(3)[gun:get(3)],
|
||||
link:man:gun:cancel(3)[gun:cancel(3)],
|
||||
link:man:gun_response(3)[gun_response(3)]
|
|
@ -0,0 +1,76 @@
|
|||
= gun_response(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun_response - Response
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
{gun_response, ConnPid, StreamRef, IsFin, Status, Headers}
|
||||
|
||||
ConnPid :: pid()
|
||||
StreamRef :: gun:stream_ref()
|
||||
IsFin :: fin | nofin
|
||||
Status :: non_neg_integer()
|
||||
Headers :: [{binary(), binary()}]
|
||||
----
|
||||
|
||||
Response.
|
||||
|
||||
This message informs the relevant process that the server
|
||||
sent a response to the original request.
|
||||
|
||||
== Elements
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
StreamRef::
|
||||
|
||||
Identifier of the stream for the original request.
|
||||
|
||||
IsFin::
|
||||
|
||||
Whether this message terminates the response.
|
||||
|
||||
Status::
|
||||
|
||||
Status code for the response.
|
||||
|
||||
Headers::
|
||||
|
||||
Headers sent with the response.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *1.0*: Message introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Receive a gun_response message in a gen_server
|
||||
[source,erlang]
|
||||
----
|
||||
handle_info({gun_response, ConnPid, _StreamRef,
|
||||
_IsFin, _Status, _Headers},
|
||||
State=#state{conn_pid=ConnPid}) ->
|
||||
%% Do something.
|
||||
{noreply, State}.
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:get(3)[gun:get(3)],
|
||||
link:man:gun:head(3)[gun:head(3)],
|
||||
link:man:gun:patch(3)[gun:patch(3)],
|
||||
link:man:gun:post(3)[gun:post(3)],
|
||||
link:man:gun:put(3)[gun:put(3)],
|
||||
link:man:gun:delete(3)[gun:delete(3)],
|
||||
link:man:gun:options(3)[gun:options(3)],
|
||||
link:man:gun:headers(3)[gun:headers(3)],
|
||||
link:man:gun:request(3)[gun:request(3)],
|
||||
link:man:gun_inform(3)[gun_inform(3)],
|
||||
link:man:gun_push(3)[gun_push(3)]
|
|
@ -0,0 +1,68 @@
|
|||
= gun_trailers(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun_trailers - Response trailers
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
{gun_trailers, ConnPid, StreamRef, Headers}
|
||||
|
||||
ConnPid :: pid()
|
||||
StreamRef :: gun:stream_ref()
|
||||
Headers :: [{binary(), binary()}]
|
||||
----
|
||||
|
||||
Response trailers.
|
||||
|
||||
This message informs the relevant process that the server
|
||||
sent response trailers for the response to the original
|
||||
request.
|
||||
|
||||
A trailers message terminates the response.
|
||||
|
||||
== Elements
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
StreamRef::
|
||||
|
||||
Identifier of the stream for the original request.
|
||||
|
||||
Headers::
|
||||
|
||||
Trailing headers sent after the response body.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *1.0*: Message introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Receive a gun_trailers message in a gen_server
|
||||
[source,erlang]
|
||||
----
|
||||
handle_info({gun_trailers, ConnPid, _StreamRef, _Headers},
|
||||
State=#state{conn_pid=ConnPid}) ->
|
||||
%% Do something.
|
||||
{noreply, State}.
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:get(3)[gun:get(3)],
|
||||
link:man:gun:head(3)[gun:head(3)],
|
||||
link:man:gun:patch(3)[gun:patch(3)],
|
||||
link:man:gun:post(3)[gun:post(3)],
|
||||
link:man:gun:put(3)[gun:put(3)],
|
||||
link:man:gun:delete(3)[gun:delete(3)],
|
||||
link:man:gun:options(3)[gun:options(3)],
|
||||
link:man:gun:headers(3)[gun:headers(3)],
|
||||
link:man:gun:request(3)[gun:request(3)],
|
||||
link:man:gun_response(3)[gun_response(3)],
|
||||
link:man:gun_data(3)[gun_data(3)]
|
|
@ -0,0 +1,66 @@
|
|||
= gun_tunnel_up(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun_tunnel_up - The tunnel is up
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
{gun_tunnel_up, ConnPid, StreamRef, Protocol}
|
||||
|
||||
ConnPid :: pid()
|
||||
StreamRef :: gun:stream_ref() | undefined
|
||||
Protocol :: http | http2 | socks
|
||||
----
|
||||
|
||||
The tunnel is up.
|
||||
|
||||
This message informs the owner/calling process that the connection
|
||||
completed through the SOCKS or CONNECT proxy.
|
||||
|
||||
If Gun is configured to connect to another SOCKS server, then the
|
||||
connection is not usable yet. One or more
|
||||
link:man:gun_tunnel_up(3)[gun_tunnel_up(3)] messages will follow.
|
||||
|
||||
== Elements
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
StreamRef::
|
||||
|
||||
The stream reference the tunnel is running on, or `undefined`
|
||||
if there are no underlying stream.
|
||||
|
||||
Protocol::
|
||||
|
||||
The protocol selected for this connection. It can be used
|
||||
to determine the capabilities of the server.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *2.0*: Message introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Receive a gun_tunnel_up message in a gen_server
|
||||
[source,erlang]
|
||||
----
|
||||
handle_info({gun_tunnel_up, ConnPid, _StreamRef, _Protocol},
|
||||
State=#state{conn_pid=ConnPid}) ->
|
||||
%% Do something.
|
||||
{noreply, State}.
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:open(3)[gun:open(3)],
|
||||
link:man:gun:open_unix(3)[gun:open_unix(3)],
|
||||
link:man:gun:await_up(3)[gun:await_up(3)],
|
||||
link:man:gun_up(3)[gun_up(3)],
|
||||
link:man:gun_down(3)[gun_down(3)],
|
||||
link:man:gun_error(3)[gun_error(3)]
|
|
@ -0,0 +1,66 @@
|
|||
= gun_up(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun_up - The connection is up
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
{gun_up, ConnPid, Protocol}
|
||||
|
||||
ConnPid :: pid()
|
||||
Protocol :: http | http2 | raw | socks
|
||||
----
|
||||
|
||||
The connection is up.
|
||||
|
||||
This message informs the owner process that the connection or
|
||||
reconnection completed.
|
||||
|
||||
If Gun is configured to connect to a Socks server, then the
|
||||
connection is not usable yet. One or more
|
||||
link:man:gun_tunnel_up(3)[gun_tunnel_up(3)] messages will follow.
|
||||
|
||||
Otherwise, Gun will start processing the messages it received while
|
||||
waiting for the connection to be up. If this is a reconnection,
|
||||
then this may not be desirable for all requests. Those requests
|
||||
should be cancelled when the connection goes down, and any
|
||||
subsequent messages ignored.
|
||||
|
||||
== Elements
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
Protocol::
|
||||
|
||||
The protocol selected for this connection. It can be used
|
||||
to determine the capabilities of the server.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *1.0*: Message introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Receive a gun_up message in a gen_server
|
||||
[source,erlang]
|
||||
----
|
||||
handle_info({gun_up, ConnPid, _Protocol},
|
||||
State=#state{conn_pid=ConnPid}) ->
|
||||
%% Do something.
|
||||
{noreply, State}.
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:open(3)[gun:open(3)],
|
||||
link:man:gun:open_unix(3)[gun:open_unix(3)],
|
||||
link:man:gun:await_up(3)[gun:await_up(3)],
|
||||
link:man:gun_tunnel_up(3)[gun_tunnel_up(3)],
|
||||
link:man:gun_down(3)[gun_down(3)],
|
||||
link:man:gun_error(3)[gun_error(3)]
|
|
@ -0,0 +1,72 @@
|
|||
= gun_upgrade(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun_upgrade - Successful protocol upgrade
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
{gun_upgrade, ConnPid, StreamRef, Protocols, Headers}
|
||||
|
||||
ConnPid :: pid()
|
||||
StreamRef :: gun:stream_ref()
|
||||
Protocols :: [<<"websocket">>]
|
||||
Headers :: [{binary(), binary()}]
|
||||
----
|
||||
|
||||
Successful protocol upgrade.
|
||||
|
||||
This message informs the relevant process that the server
|
||||
accepted to upgrade to one or more protocols given in the
|
||||
original request.
|
||||
|
||||
The exact semantics of this message depend on the original
|
||||
protocol. HTTP/1.1 upgrades apply to the entire connection.
|
||||
HTTP/2 uses a different mechanism which allows switching
|
||||
specific streams to a different protocol.
|
||||
|
||||
Gun currently only supports upgrading HTTP/1.1 connections
|
||||
to the Websocket protocol.
|
||||
|
||||
== Elements
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
StreamRef::
|
||||
|
||||
Identifier of the stream that resulted in an upgrade.
|
||||
|
||||
Protocols::
|
||||
|
||||
List of protocols this stream was upgraded to.
|
||||
|
||||
Headers::
|
||||
|
||||
Headers sent with the upgrade response.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *1.0*: Message introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Receive a gun_upgrade message in a gen_server
|
||||
[source,erlang]
|
||||
----
|
||||
handle_info({gun_upgrade, ConnPid, _StreamRef,
|
||||
_Protocols, _Headers},
|
||||
State=#state{conn_pid=ConnPid}) ->
|
||||
%% Do something.
|
||||
{noreply, State}.
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:ws_upgrade(3)[gun:ws_upgrade(3)],
|
||||
link:man:gun:ws_send(3)[gun:ws_send(3)],
|
||||
link:man:gun_ws(3)[gun_ws(3)]
|
|
@ -0,0 +1,65 @@
|
|||
= gun_ws(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun_ws - Websocket frame
|
||||
|
||||
== Description
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
{gun_ws, ConnPid, StreamRef, Frame}
|
||||
|
||||
ConnPid :: pid()
|
||||
StreamRef :: gun:stream_ref()
|
||||
Frame :: close | ping | pong
|
||||
| {text | binary | close, binary()}
|
||||
| {close, non_neg_integer(), binary()}
|
||||
| {ping | pong, binary()}
|
||||
----
|
||||
|
||||
Websocket frame.
|
||||
|
||||
This message informs the relevant process that the server
|
||||
sent the enclosed frame.
|
||||
|
||||
This message can only be sent on streams that were upgraded
|
||||
to the Websocket protocol.
|
||||
|
||||
== Elements
|
||||
|
||||
ConnPid::
|
||||
|
||||
The pid of the Gun connection process.
|
||||
|
||||
StreamRef::
|
||||
|
||||
Identifier of the stream that was upgraded to Websocket.
|
||||
|
||||
Frame::
|
||||
|
||||
The Websocket frame in question.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *2.0*: Depending on the option `silence_pings`, ping and
|
||||
pong frames may be sent as well.
|
||||
* *1.0*: Message introduced.
|
||||
|
||||
== Examples
|
||||
|
||||
.Receive a gun_ws message in a gen_server
|
||||
[source,erlang]
|
||||
----
|
||||
handle_info({gun_ws, ConnPid, _StreamRef, _Frame},
|
||||
State=#state{conn_pid=ConnPid}) ->
|
||||
%% Do something.
|
||||
{noreply, State}.
|
||||
----
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:ws_upgrade(3)[gun:ws_upgrade(3)],
|
||||
link:man:gun:ws_send(3)[gun:ws_send(3)],
|
||||
link:man:gun_upgrade(3)[gun_upgrade(3)]
|
|
@ -0,0 +1,108 @@
|
|||
= gun_ws_protocol(3)
|
||||
|
||||
== Name
|
||||
|
||||
gun_ws_protocol - Websocket subprotocols
|
||||
|
||||
== Description
|
||||
|
||||
The `gun_ws_protocol` module provides the callback interface
|
||||
and types for implementing Websocket subprotocols.
|
||||
|
||||
== Callbacks
|
||||
|
||||
Websocket subprotocols implement the following interface.
|
||||
|
||||
=== init
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
init(ReplyTo, StreamRef, Headers, Opts) -> {ok, State}
|
||||
|
||||
ReplyTo :: pid()
|
||||
StreamRef :: reference()
|
||||
Headers :: cow_http:headers()
|
||||
Opts :: gun:ws_opts()
|
||||
State :: protocol_state()
|
||||
----
|
||||
|
||||
Initialize the Websocket protocol.
|
||||
|
||||
ReplyTo::
|
||||
|
||||
The pid of the process that owns the stream and to
|
||||
which messages will be sent to.
|
||||
|
||||
StreamRef::
|
||||
|
||||
The reference for the stream. Must be sent in messages
|
||||
to distinguish between different streams.
|
||||
|
||||
Headers::
|
||||
|
||||
Headers that were sent in the response establishing
|
||||
the Websocket connection.
|
||||
|
||||
Opts::
|
||||
|
||||
Websocket options. Custom options can be provided in
|
||||
the `user_opts` key.
|
||||
|
||||
State::
|
||||
|
||||
State for the protocol.
|
||||
|
||||
=== handle
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
handle(Frame, State) -> {ok, FlowDec, State}
|
||||
|
||||
Frame :: cow_ws:frame()
|
||||
State :: protocol_state()
|
||||
FlowDec :: non_neg_integer()
|
||||
----
|
||||
|
||||
Handle a Websocket frame.
|
||||
|
||||
This callback may receive fragmented frames depending
|
||||
on the protocol and may need to rebuild the full
|
||||
frame to process it.
|
||||
|
||||
Frame::
|
||||
|
||||
Websocket frame.
|
||||
|
||||
State::
|
||||
|
||||
State for the protocol.
|
||||
|
||||
FlowDec::
|
||||
|
||||
How many messages were sent. Used to update the flow
|
||||
control state when the feature is enabled.
|
||||
|
||||
== Types
|
||||
|
||||
=== protocol_state()
|
||||
|
||||
[source,erlang]
|
||||
----
|
||||
protocol_state() :: any()
|
||||
----
|
||||
|
||||
State for the protocol.
|
||||
|
||||
As this part of the implementation of the protocol
|
||||
the type may differ between different Websocket
|
||||
protocol modules.
|
||||
|
||||
== Changelog
|
||||
|
||||
* *2.0*: Module introduced.
|
||||
|
||||
== See also
|
||||
|
||||
link:man:gun(7)[gun(7)],
|
||||
link:man:gun(3)[gun(3)],
|
||||
link:man:gun:ws_upgrade(3)[gun:ws_upgrade(3)]
|
|
@ -0,0 +1,10 @@
|
|||
{application, 'gun', [
|
||||
{description, "HTTP/1.1, HTTP/2 and Websocket client for Erlang/OTP."},
|
||||
{vsn, "2.1.0"},
|
||||
{modules, ['gun','gun_app','gun_conns_sup','gun_content_handler','gun_cookies','gun_cookies_list','gun_data_h','gun_default_event_h','gun_event','gun_http','gun_http2','gun_pool','gun_pool_events_h','gun_pools_sup','gun_protocols','gun_public_suffix','gun_raw','gun_socks','gun_sse_h','gun_sup','gun_tcp','gun_tcp_proxy','gun_tls','gun_tls_proxy','gun_tls_proxy_cb','gun_tls_proxy_http2_connect','gun_tunnel','gun_ws','gun_ws_h','gun_ws_protocol']},
|
||||
{registered, [gun_sup]},
|
||||
{applications, [kernel,stdlib,public_key,ssl,cowlib]},
|
||||
{optional_applications, []},
|
||||
{mod, {gun_app, []}},
|
||||
{env, []}
|
||||
]}.
|
|
@ -0,0 +1,23 @@
|
|||
.PHONY: all
|
||||
.SUFFIXES: .erl .beam
|
||||
|
||||
ERLC?= erlc -server
|
||||
|
||||
OBJS= gun_ws_protocol.beam gun.beam gun_app.beam gun_conns_sup.beam
|
||||
OBJS+= gun_cookies.beam gun_cookies_list.beam gun_data_h.beam
|
||||
OBJS+= gun_default_event_h.beam gun_event.beam gun_http.beam
|
||||
OBJS+= gun_http2.beam gun_pool.beam gun_pool_events_h.beam gun_pools_sup.beam
|
||||
OBJS+= gun_protocols.beam gun_public_suffix.beam gun_raw.beam
|
||||
OBJS+= gun_socks.beam gun_sse_h.beam gun_sup.beam gun_tcp.beam
|
||||
OBJS+= gun_tcp_proxy.beam gun_tls.beam gun_tls_proxy.beam
|
||||
OBJS+= gun_tls_proxy_cb.beam gun_tls_proxy_http2_connect.beam
|
||||
OBJS+= gun_tunnel.beam gun_ws.beam gun_ws_h.beam gun_content_handler.beam
|
||||
|
||||
all: ${OBJS}
|
||||
|
||||
.erl.beam:
|
||||
${ERLC} ${ERLOPTS} ${ERLFLAGS} $<
|
||||
|
||||
clean:
|
||||
rm -f *.beam
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,30 @@
|
|||
%% Copyright (c) 2013-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
%% @private
|
||||
-module(gun_app).
|
||||
-behaviour(application).
|
||||
|
||||
%% API.
|
||||
-export([start/2]).
|
||||
-export([stop/1]).
|
||||
|
||||
%% API.
|
||||
|
||||
start(_Type, _Args) ->
|
||||
gun_pools = ets:new(gun_pools, [ordered_set, public, named_table]),
|
||||
gun_sup:start_link().
|
||||
|
||||
stop(_State) ->
|
||||
ok.
|
|
@ -0,0 +1,36 @@
|
|||
%% Copyright (c) 2013-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(gun_conns_sup).
|
||||
-behaviour(supervisor).
|
||||
|
||||
%% API.
|
||||
-export([start_link/0]).
|
||||
|
||||
%% supervisor.
|
||||
-export([init/1]).
|
||||
|
||||
%% API.
|
||||
|
||||
-spec start_link() -> {ok, pid()}.
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
%% supervisor.
|
||||
|
||||
init([]) ->
|
||||
Procs = [
|
||||
#{id => gun, start => {gun, start_link, []}, restart => temporary}
|
||||
],
|
||||
{ok, {#{strategy => simple_one_for_one}, Procs}}.
|
|
@ -0,0 +1,76 @@
|
|||
%% Copyright (c) 2017-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(gun_content_handler).
|
||||
|
||||
-export([init/5]).
|
||||
-export([handle/3]).
|
||||
-export([check_option/1]).
|
||||
|
||||
-type opt() :: [module() | {module(), map()}].
|
||||
-export_type([opt/0]).
|
||||
|
||||
-type state() :: opt() | [{module(), any()}].
|
||||
-export_type([state/0]).
|
||||
|
||||
-callback init(pid(), any(), cow_http:status(),
|
||||
cow_http:headers(), map()) -> {ok, any()} | disable.
|
||||
%% @todo Make fin | nofin its own type.
|
||||
-callback handle(fin | nofin, any(), State)
|
||||
-> {ok, any(), non_neg_integer(), State}
|
||||
| {done, non_neg_integer(), State}
|
||||
when State::any().
|
||||
|
||||
-spec init(pid(), any(), cow_http:status(),
|
||||
cow_http:headers(), State) -> State when State::state().
|
||||
init(_, _, _, _, []) ->
|
||||
[];
|
||||
init(ReplyTo, StreamRef, Status, Headers, [Handler|Tail]) ->
|
||||
{Mod, Opts} = case Handler of
|
||||
Tuple = {_, _} -> Tuple;
|
||||
Atom -> {Atom, #{}}
|
||||
end,
|
||||
case Mod:init(ReplyTo, StreamRef, Status, Headers, Opts) of
|
||||
{ok, State} -> [{Mod, State}|init(ReplyTo, StreamRef, Status, Headers, Tail)];
|
||||
disable -> init(ReplyTo, StreamRef, Status, Headers, Tail)
|
||||
end.
|
||||
|
||||
-spec handle(fin | nofin, any(), State) -> {ok, non_neg_integer(), State} when State::state().
|
||||
handle(IsFin, Data, State) ->
|
||||
handle(IsFin, Data, State, 0, []).
|
||||
|
||||
handle(_, _, [], Flow, Acc) ->
|
||||
{ok, Flow, lists:reverse(Acc)};
|
||||
handle(IsFin, Data0, [{Mod, State0}|Tail], Flow, Acc) ->
|
||||
case Mod:handle(IsFin, Data0, State0) of
|
||||
{ok, Data, Inc, State} ->
|
||||
handle(IsFin, Data, Tail, Flow + Inc, [{Mod, State}|Acc]);
|
||||
{done, Inc, State} ->
|
||||
{ok, Flow + Inc, lists:reverse([{Mod, State}|Acc], Tail)}
|
||||
end.
|
||||
|
||||
-spec check_option(list()) -> ok | error.
|
||||
check_option([]) ->
|
||||
error;
|
||||
check_option(Opt) ->
|
||||
check_option1(Opt).
|
||||
|
||||
check_option1([]) ->
|
||||
ok;
|
||||
check_option1([Atom|Tail]) when is_atom(Atom) ->
|
||||
check_option1(Tail);
|
||||
check_option1([{Atom, #{}}|Tail]) when is_atom(Atom) ->
|
||||
check_option1(Tail);
|
||||
check_option1(_) ->
|
||||
error.
|
|
@ -0,0 +1,668 @@
|
|||
%% Copyright (c) 2020-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(gun_cookies).
|
||||
|
||||
-export([add_cookie_header/5]).
|
||||
-export([domain_match/2]).
|
||||
-export([gc/1]).
|
||||
-export([path_match/2]).
|
||||
-export([query/2]).
|
||||
-export([session_gc/1]).
|
||||
-export([set_cookie/5]).
|
||||
-export([set_cookie_header/7]).
|
||||
|
||||
-ifdef(TEST).
|
||||
-export([wpt_http_state_test_files/1]). %% Also used in rfc6265bis_SUITE.
|
||||
-endif.
|
||||
|
||||
-type store_state() :: any().
|
||||
|
||||
-type store() :: {module(), store_state()}.
|
||||
-export_type([store/0]).
|
||||
|
||||
-type cookie() :: #{
|
||||
name := binary(),
|
||||
value := binary(),
|
||||
domain := binary(),
|
||||
path := binary(),
|
||||
creation_time := calendar:datetime(),
|
||||
last_access_time := calendar:datetime(),
|
||||
expiry_time := calendar:datetime() | infinity,
|
||||
persistent := boolean(),
|
||||
host_only := boolean(),
|
||||
secure_only := boolean(),
|
||||
http_only := boolean(),
|
||||
same_site := default | none | strict | lax
|
||||
}.
|
||||
-export_type([cookie/0]).
|
||||
|
||||
-callback init(any()) -> store().
|
||||
|
||||
-callback query(State, uri_string:uri_map())
|
||||
-> {ok, [gun_cookies:cookie()], State}
|
||||
when State::store_state().
|
||||
|
||||
-callback set_cookie_secure_match(store_state(), #{
|
||||
name := binary(),
|
||||
% secure_only := true,
|
||||
domain := binary(),
|
||||
path := binary()
|
||||
}) -> match | nomatch.
|
||||
|
||||
-callback set_cookie_get_exact_match(State, #{
|
||||
name := binary(),
|
||||
domain := binary(),
|
||||
host_only := boolean(),
|
||||
path := binary()
|
||||
}) -> {ok, cookie(), State} | error
|
||||
when State::store_state().
|
||||
|
||||
-callback store(State, cookie())
|
||||
-> {ok, State} | {error, any()}
|
||||
when State::store_state().
|
||||
|
||||
-callback gc(State)
|
||||
-> {ok, State}
|
||||
when State::store_state().
|
||||
|
||||
-callback session_gc(State)
|
||||
-> {ok, State}
|
||||
when State::store_state().
|
||||
|
||||
-spec add_cookie_header(binary(), iodata(), iodata(), Headers, Store)
|
||||
-> {Headers, Store} when Headers :: [{binary(), iodata()}], Store :: undefined | store().
|
||||
add_cookie_header(_, _, _, Headers, Store=undefined) ->
|
||||
{Headers, Store};
|
||||
add_cookie_header(Scheme, Authority, PathWithQs, Headers0, Store0) ->
|
||||
#{
|
||||
host := Host,
|
||||
path := Path
|
||||
} = uri_string:parse([Scheme, <<"://">>, Authority, PathWithQs]),
|
||||
URIMap = uri_string:normalize(#{
|
||||
scheme => Scheme,
|
||||
host => iolist_to_binary(Host),
|
||||
path => iolist_to_binary(Path)
|
||||
}, [return_map]),
|
||||
{ok, Cookies0, Store} = query(Store0, URIMap),
|
||||
Headers = case Cookies0 of
|
||||
[] ->
|
||||
Headers0;
|
||||
_ ->
|
||||
Cookies = [{Name, Value} || #{name := Name, value := Value} <- Cookies0],
|
||||
%% We put cookies at the end of the headers list as it's the least important header.
|
||||
Headers0 ++ [{<<"cookie">>, cow_cookie:cookie(Cookies)}]
|
||||
end,
|
||||
{Headers, Store}.
|
||||
|
||||
-spec domain_match(binary(), binary()) -> boolean().
|
||||
domain_match(String, String) ->
|
||||
true;
|
||||
domain_match(String, DomainString) ->
|
||||
SkipLen = byte_size(String) - byte_size(DomainString) - 1,
|
||||
case String of
|
||||
<<_:SkipLen/unit:8, $., DomainString/binary>> ->
|
||||
case inet:parse_strict_address(binary_to_list(String)) of
|
||||
{ok, _} ->
|
||||
false;
|
||||
{error, einval} ->
|
||||
true
|
||||
end;
|
||||
_ ->
|
||||
false
|
||||
end.
|
||||
|
||||
-spec gc(Store) -> {ok, Store} when Store::store().
|
||||
gc({Mod, State0}) ->
|
||||
{ok, State} = Mod:gc(State0),
|
||||
{ok, {Mod, State}}.
|
||||
|
||||
-spec path_match(binary(), binary()) -> boolean().
|
||||
path_match(Path, Path) ->
|
||||
true;
|
||||
path_match(ReqPath, CookiePath) ->
|
||||
Len = byte_size(CookiePath),
|
||||
CookieLast = binary:last(CookiePath),
|
||||
case ReqPath of
|
||||
<<CookiePath:Len/binary, _/bits>> when CookieLast =:= $/ ->
|
||||
true;
|
||||
<<CookiePath:Len/binary, $/, _/bits>> ->
|
||||
true;
|
||||
_ ->
|
||||
false
|
||||
end.
|
||||
|
||||
-ifdef(TEST).
|
||||
path_match_test_() ->
|
||||
Tests = [
|
||||
{<<"/">>, <<"/">>, true},
|
||||
{<<"/path/to/resource">>, <<"/path/to/resource">>, true},
|
||||
{<<"/path/">>, <<"/path/">>, true},
|
||||
{<<"/path/to/resource">>, <<"/path/">>, true},
|
||||
{<<"/path/to/resource">>, <<"/path">>, true},
|
||||
{<<"/path/to/resource">>, <<"/path/to/">>, true},
|
||||
{<<"/path/to/resource">>, <<"/path/to">>, true},
|
||||
{<<"/path/to/resource">>, <<"/pa">>, false},
|
||||
{<<"/path/to/resource">>, <<"/pat">>, false},
|
||||
{<<"/path/to/resource">>, <<"/path/to/r">>, false},
|
||||
{<<"/abc">>, <<"/def">>, false}
|
||||
],
|
||||
[{iolist_to_binary(io_lib:format("(~p,~p)", [PA, PB])),
|
||||
fun() -> Res = path_match(PA, PB) end}
|
||||
|| {PA, PB, Res} <- Tests].
|
||||
-endif.
|
||||
|
||||
%% @todo The given URI must be normalized.
|
||||
-spec query(Store, uri_string:uri_map())
|
||||
-> {ok, [cookie()], Store}
|
||||
when Store::store().
|
||||
query({Mod, State0}, URI) ->
|
||||
{ok, Cookies0, State} = Mod:query(State0, URI),
|
||||
Cookies = lists:sort(fun
|
||||
(#{path := P, creation_time := CTA}, #{path := P, creation_time := CTB}) ->
|
||||
CTA =< CTB;
|
||||
(#{path := PA}, #{path := PB}) ->
|
||||
PA > PB
|
||||
end, Cookies0),
|
||||
{ok, Cookies, {Mod, State}}.
|
||||
|
||||
-spec session_gc(Store) -> {ok, Store} when Store::store().
|
||||
session_gc({Mod, State0}) ->
|
||||
{ok, State} = Mod:session_gc(State0),
|
||||
{ok, {Mod, State}}.
|
||||
|
||||
%% @todo The given URI must be normalized.
|
||||
-spec set_cookie(Store, uri_string:uri_map(), binary(), binary(), cow_cookie:cookie_attrs())
|
||||
-> {ok, Store} | {error, any()} when Store::store().
|
||||
set_cookie(_, _, Name, Value, _) when byte_size(Name) + byte_size(Value) > 4096 ->
|
||||
{error, larger_than_4096_bytes};
|
||||
set_cookie(Store, URI=#{host := Host}, Name, Value, Attrs) ->
|
||||
%% This is where we would add a feature to block cookies (like a blacklist).
|
||||
CurrentTime = erlang:universaltime(),
|
||||
Cookie0 = #{
|
||||
name => Name,
|
||||
value => Value,
|
||||
creation_time => CurrentTime,
|
||||
last_access_time => CurrentTime
|
||||
},
|
||||
Cookie = case Attrs of
|
||||
#{max_age := ExpiryTime} ->
|
||||
Cookie0#{
|
||||
persistent => true,
|
||||
expiry_time => ExpiryTime
|
||||
};
|
||||
#{expires := ExpiryTime} ->
|
||||
Cookie0#{
|
||||
persistent => true,
|
||||
expiry_time => ExpiryTime
|
||||
};
|
||||
_ ->
|
||||
Cookie0#{
|
||||
persistent => false,
|
||||
expiry_time => infinity
|
||||
}
|
||||
end,
|
||||
Domain0 = maps:get(domain, Attrs, <<>>),
|
||||
Domain = case gun_public_suffix:match(Domain0) of
|
||||
false ->
|
||||
Domain0;
|
||||
true when Host =:= Domain0 ->
|
||||
<<>>;
|
||||
true ->
|
||||
{error, domain_is_public_suffix}
|
||||
end,
|
||||
case Domain of
|
||||
<<>> ->
|
||||
set_cookie(Store, URI, Attrs, Cookie#{
|
||||
host_only => true,
|
||||
domain => Host
|
||||
});
|
||||
Error = {error, _} ->
|
||||
Error;
|
||||
_ ->
|
||||
case domain_match(Host, Domain) of
|
||||
true ->
|
||||
set_cookie(Store, URI, Attrs, Cookie#{
|
||||
host_only => false,
|
||||
domain => Domain
|
||||
});
|
||||
false ->
|
||||
{error, domain_match_failed}
|
||||
end
|
||||
end.
|
||||
|
||||
set_cookie(Store, URI, Attrs, Cookie0) ->
|
||||
Cookie1 = case Attrs of
|
||||
#{path := Path} ->
|
||||
Cookie0#{path => Path};
|
||||
_ ->
|
||||
Cookie0#{path => default_path(URI)}
|
||||
end,
|
||||
SecureOnly = maps:get(secure, Attrs, false),
|
||||
case {SecureOnly, maps:get(scheme, URI)} of
|
||||
{true, <<"http">>} ->
|
||||
{error, secure_scheme_only};
|
||||
_ ->
|
||||
Cookie = Cookie1#{
|
||||
secure_only => SecureOnly,
|
||||
http_only => maps:get(http_only, Attrs, false)
|
||||
},
|
||||
%% This is where we would drop cookies from non-HTTP APIs.
|
||||
set_cookie1(Store, URI, Attrs, Cookie)
|
||||
end.
|
||||
|
||||
default_path(#{path := Path}) ->
|
||||
case string:split(Path, <<"/">>, trailing) of
|
||||
[_] -> <<"/">>;
|
||||
[<<>>, _] -> <<"/">>;
|
||||
[DefaultPath, _] -> DefaultPath
|
||||
end;
|
||||
default_path(_) ->
|
||||
<<"/">>.
|
||||
|
||||
set_cookie1(Store, URI=#{scheme := <<"http">>}, Attrs, Cookie=#{secure_only := false}) ->
|
||||
Match = maps:with([name, domain, path], Cookie),
|
||||
case set_cookie_secure_match(Store, Match) of
|
||||
match ->
|
||||
{error, secure_cookie_matches};
|
||||
nomatch ->
|
||||
set_cookie2(Store, URI, Attrs, Cookie)
|
||||
end;
|
||||
set_cookie1(Store, URI, Attrs, Cookie) ->
|
||||
set_cookie2(Store, URI, Attrs, Cookie).
|
||||
|
||||
set_cookie_secure_match({Mod, State}, Match) ->
|
||||
Mod:set_cookie_secure_match(State, Match).
|
||||
|
||||
set_cookie2(Store, _URI, Attrs, Cookie0) ->
|
||||
Cookie = Cookie0#{same_site => maps:get(same_site, Attrs, default)},
|
||||
%% This is where we would perform the same-site checks.
|
||||
%%
|
||||
%% It seems that an option would need to be added to Gun
|
||||
%% in order to define the "site for cookies" value. It is
|
||||
%% not the same as the site identified by the URI. Although
|
||||
%% I do wonder if in the case of server push we may consider
|
||||
%% the requested URI to be the "site for cookies", at least
|
||||
%% by default.
|
||||
%%
|
||||
%% The URI argument will be used if/when the above gets
|
||||
%% implemented.
|
||||
set_cookie3(Store, Attrs, Cookie).
|
||||
|
||||
set_cookie3(Store, Attrs, Cookie=#{name := Name,
|
||||
host_only := HostOnly, secure_only := SecureOnly}) ->
|
||||
Path = maps:get(path, Attrs, undefined),
|
||||
case Name of
|
||||
<<"__Secure-",_/bits>> when not SecureOnly ->
|
||||
{error, name_prefix_secure_requires_secure_only};
|
||||
<<"__Host-",_/bits>> when not SecureOnly ->
|
||||
{error, name_prefix_host_requires_secure_only};
|
||||
<<"__Host-",_/bits>> when not HostOnly ->
|
||||
{error, name_prefix_host_requires_host_only};
|
||||
<<"__Host-",_/bits>> when Path =/= <<"/">> ->
|
||||
{error, name_prefix_host_requires_top_level_path};
|
||||
_ ->
|
||||
set_cookie_store(Store, Cookie)
|
||||
end.
|
||||
|
||||
set_cookie_store(Store0, Cookie) ->
|
||||
Match = maps:with([name, domain, host_only, path], Cookie),
|
||||
case set_cookie_get_exact_match(Store0, Match) of
|
||||
{ok, #{creation_time := CreationTime}, Store} ->
|
||||
%% This is where we would reject a new non-HTTP cookie
|
||||
%% if the OldCookie has http_only set to true.
|
||||
store(Store, Cookie#{creation_time => CreationTime});
|
||||
error ->
|
||||
store(Store0, Cookie)
|
||||
end.
|
||||
|
||||
set_cookie_get_exact_match({Mod, State0}, Match) ->
|
||||
case Mod:set_cookie_get_exact_match(State0, Match) of
|
||||
{ok, Cookie, State} ->
|
||||
{ok, Cookie, {Mod, State}};
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
store({Mod, State0}, Cookie) ->
|
||||
case Mod:store(State0, Cookie) of
|
||||
{ok, State} ->
|
||||
{ok, {Mod, State}};
|
||||
%% @todo Is this return value useful? Can't it just return {ok, State}?
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec set_cookie_header(binary(), iodata(), iodata(), cow_http:status(),
|
||||
Headers, Store, #{cookie_ignore_informational := boolean()})
|
||||
-> {Headers, Store} when Headers :: [{binary(), iodata()}], Store :: undefined | store().
|
||||
%% Don't set cookies when cookie store isn't configured.
|
||||
set_cookie_header(_, _, _, _, _, Store=undefined, _) ->
|
||||
Store;
|
||||
%% Ignore cookies set on informational responses when configured to do so.
|
||||
%% This includes cookies set to Websocket upgrade responses!
|
||||
set_cookie_header(_, _, _, Status, _, Store, #{cookie_ignore_informational := true})
|
||||
when Status >= 100, Status =< 199 ->
|
||||
Store;
|
||||
set_cookie_header(Scheme, Authority, PathWithQs, _, Headers, Store0, _) ->
|
||||
#{host := Host, path := Path} = uri_string:parse([Scheme, <<"://">>, Authority, PathWithQs]),
|
||||
URIMap = uri_string:normalize(#{
|
||||
scheme => Scheme,
|
||||
host => iolist_to_binary(Host),
|
||||
path => iolist_to_binary(Path)
|
||||
}, [return_map]),
|
||||
SetCookies = [SC || {<<"set-cookie">>, SC} <- Headers],
|
||||
lists:foldl(fun(SC, Store1) ->
|
||||
case cow_cookie:parse_set_cookie(SC) of
|
||||
{ok, N, V, A} ->
|
||||
case set_cookie(Store1, URIMap, N, V, A) of
|
||||
{ok, Store} -> Store;
|
||||
{error, _} -> Store1
|
||||
end;
|
||||
ignore ->
|
||||
Store1
|
||||
end
|
||||
end, Store0, SetCookies).
|
||||
|
||||
-ifdef(TEST).
|
||||
gc_test() ->
|
||||
URIMap = #{scheme => <<"http">>, host => <<"example.org">>, path => <<"/path/to/resource">>},
|
||||
Store0 = gun_cookies_list:init(),
|
||||
%% Add a cookie that expires in 2 seconds. GC. Cookie can be retrieved.
|
||||
{ok, N0, V0, A0} = cow_cookie:parse_set_cookie(<<"a=b; Path=/; Max-Age=2">>),
|
||||
{ok, Store1} = set_cookie(Store0, URIMap, N0, V0, A0),
|
||||
{ok, Store2} = gc(Store1),
|
||||
{ok, [_], _} = query(Store2, URIMap),
|
||||
%% Wait 3 seconds. GC. Cookie was removed.
|
||||
timer:sleep(3000),
|
||||
{ok, Store} = gc(Store2),
|
||||
{ok, [], _} = query(Store, URIMap),
|
||||
ok.
|
||||
|
||||
gc_expiry_time_infinity_test() ->
|
||||
URIMap = #{scheme => <<"http">>, host => <<"example.org">>, path => <<"/path/to/resource">>},
|
||||
Store0 = gun_cookies_list:init(),
|
||||
%% Add a session cookie. GC. Cookie can be retrieved.
|
||||
{ok, N0, V0, A0} = cow_cookie:parse_set_cookie(<<"a=b; Path=/">>),
|
||||
{ok, Store1} = set_cookie(Store0, URIMap, N0, V0, A0),
|
||||
{ok, Store} = gc(Store1),
|
||||
{ok, [_], _} = query(Store, URIMap),
|
||||
ok.
|
||||
|
||||
session_gc_test() ->
|
||||
URIMap = #{scheme => <<"http">>, host => <<"example.org">>, path => <<"/path/to/resource">>},
|
||||
Store0 = gun_cookies_list:init(),
|
||||
%% Add a persistent and a session cookie. GC session cookies. Only persistent can be retrieved.
|
||||
{ok, N0, V0, A0} = cow_cookie:parse_set_cookie(<<"s=s; Path=/">>),
|
||||
{ok, Store1} = set_cookie(Store0, URIMap, N0, V0, A0),
|
||||
{ok, N1, V1, A1} = cow_cookie:parse_set_cookie(<<"p=p; Path=/; Max-Age=999">>),
|
||||
{ok, Store2} = set_cookie(Store1, URIMap, N1, V1, A1),
|
||||
{ok, Store} = session_gc(Store2),
|
||||
{ok, [#{name := <<"p">>}], _} = query(Store, URIMap),
|
||||
ok.
|
||||
|
||||
%% Most of the tests for this module are converted from the
|
||||
%% Web platform test suite. At the time of writing they could
|
||||
%% be found at https://github.com/web-platform-tests/wpt/tree/master/cookies
|
||||
%%
|
||||
%% Some of the tests use files from wpt directly, namely the
|
||||
%% http-state ones. They are copied to the test/wpt/cookies directory.
|
||||
|
||||
-define(HOST, "web-platform.test").
|
||||
|
||||
%% WPT: domain/domain-attribute-host-with-and-without-leading-period
|
||||
%% WPT: domain/domain-attribute-host-with-leading-period
|
||||
wpt_domain_with_and_without_leading_period_test() ->
|
||||
URIMap = #{scheme => <<"http">>, host => <<?HOST>>, path => <<"/path/to/resource">>},
|
||||
Store0 = gun_cookies_list:init(),
|
||||
%% Add a cookie with a leading period in the domain. Cookie can be retrieved.
|
||||
{ok, N0, V0, A0} = cow_cookie:parse_set_cookie(<<"a=b; Path=/; Domain=." ?HOST>>),
|
||||
{ok, Store1} = set_cookie(Store0, URIMap, N0, V0, A0),
|
||||
{ok, [#{value := <<"b">>}], _} = query(Store1, URIMap),
|
||||
{ok, [#{value := <<"b">>}], _} = query(Store1, URIMap#{host => <<"sub." ?HOST>>}),
|
||||
%% Add a cookie without a leading period in the domain. Overrides the existing cookie.
|
||||
{ok, N1, V1, A1} = cow_cookie:parse_set_cookie(<<"a=c; Path=/; Domain=" ?HOST>>),
|
||||
{ok, Store} = set_cookie(Store1, URIMap, N1, V1, A1),
|
||||
{ok, [#{value := <<"c">>}], _} = query(Store, URIMap),
|
||||
{ok, [#{value := <<"c">>}], _} = query(Store, URIMap#{host => <<"sub." ?HOST>>}),
|
||||
ok.
|
||||
|
||||
%% WPT: domain/domain-attribute-matches-host
|
||||
wpt_domain_matches_host_test() ->
|
||||
URIMap = #{scheme => <<"http">>, host => <<?HOST>>, path => <<"/path/to/resource">>},
|
||||
Store0 = gun_cookies_list:init(),
|
||||
%% Add a cookie without a leading period in the domain. Cookie can be retrieved.
|
||||
{ok, N1, V1, A1} = cow_cookie:parse_set_cookie(<<"a=c; Path=/; Domain=" ?HOST>>),
|
||||
{ok, Store} = set_cookie(Store0, URIMap, N1, V1, A1),
|
||||
{ok, [#{value := <<"c">>}], _} = query(Store, URIMap),
|
||||
{ok, [#{value := <<"c">>}], _} = query(Store, URIMap#{host => <<"sub." ?HOST>>}),
|
||||
ok.
|
||||
|
||||
%% WPT: domain/domain-attribute-missing
|
||||
wpt_domain_missing_test() ->
|
||||
URIMap = #{scheme => <<"http">>, host => <<?HOST>>, path => <<"/path/to/resource">>},
|
||||
Store0 = gun_cookies_list:init(),
|
||||
%% Add a cookie without a domain attribute. Cookie is not sent on subdomains.
|
||||
{ok, N1, V1, A1} = cow_cookie:parse_set_cookie(<<"a=c; Path=/">>),
|
||||
{ok, Store} = set_cookie(Store0, URIMap, N1, V1, A1),
|
||||
{ok, [#{value := <<"c">>}], _} = query(Store, URIMap),
|
||||
{ok, [], _} = query(Store, URIMap#{host => <<"sub." ?HOST>>}),
|
||||
ok.
|
||||
|
||||
%% WPT: http-state/*-tests
|
||||
wpt_http_state_test_files() ->
|
||||
wpt_http_state_test_files("test/").
|
||||
|
||||
wpt_http_state_test_files(TestPath) ->
|
||||
filelib:wildcard(TestPath ++ "wpt/cookies/*-test") -- [
|
||||
TestPath ++ "wpt/cookies/attribute0023-test", %% Doesn't match the spec (path override).
|
||||
TestPath ++ "wpt/cookies/disabled-chromium0020-test", %% Maximum cookie name of 4096 characters.
|
||||
TestPath ++ "wpt/cookies/disabled-chromium0022-test" %% Nonsense.
|
||||
].
|
||||
|
||||
wpt_http_state_test_() ->
|
||||
URIMap0 = #{scheme => <<"http">>, host => <<"home.example.org">>, path => <<"/cookie-parser">>},
|
||||
TestFiles = wpt_http_state_test_files(),
|
||||
[{F, fun() ->
|
||||
{ok, Test} = file:read_file(F),
|
||||
%% We don't want the final empty line.
|
||||
Lines = lists:reverse(tl(lists:reverse(string:split(Test, <<"\n">>, all)))),
|
||||
{Store, URIMap2} = lists:foldl(fun
|
||||
(<<"Set-Cookie: ",SetCookie/bits>>, Acc={Store0, URIMap1}) ->
|
||||
case cow_cookie:parse_set_cookie(SetCookie) of
|
||||
{ok, N, V, A} ->
|
||||
%% We use the URIMap that corresponds to the request.
|
||||
case set_cookie(Store0, URIMap0, N, V, A) of
|
||||
{ok, Store1} -> {Store1, URIMap1};
|
||||
{error, _} -> Acc
|
||||
end;
|
||||
ignore ->
|
||||
Acc
|
||||
end;
|
||||
(<<"Set-Cookie:">>, Acc) ->
|
||||
Acc;
|
||||
(<<"Location: ",Location/bits>>, {Store0, URIMap1}) ->
|
||||
{Store0, maps:merge(URIMap1, uri_string:normalize(Location, [return_map]))}
|
||||
end, {gun_cookies_list:init(), URIMap0}, Lines),
|
||||
%% We must change the URI if it wasn't already changed by the test.
|
||||
URIMap = case URIMap2 of
|
||||
URIMap0 -> maps:merge(URIMap0, uri_string:normalize(<<"/cookie-parser-result">>, [return_map]));
|
||||
_ -> URIMap2
|
||||
end,
|
||||
{ok, Cookies, _} = query(Store, URIMap),
|
||||
case file:read_file(iolist_to_binary(string:replace(F, <<"-test">>, <<"-expected">>))) of
|
||||
{ok, ExpectedFile} when ExpectedFile =:= <<>>; ExpectedFile =:= <<"\n">> ->
|
||||
[] = Cookies,
|
||||
ok;
|
||||
{ok, <<"Cookie: ",CookiesBin0/bits>>} ->
|
||||
%% We only care about the first line.
|
||||
[CookiesBin, <<>>|_] = string:split(CookiesBin0, <<"\n">>, all),
|
||||
CookiesBin = iolist_to_binary(cow_cookie:cookie(
|
||||
[{Name, Value} || #{name := Name, value := Value} <- Cookies])),
|
||||
ok
|
||||
end
|
||||
end} || F <- TestFiles].
|
||||
|
||||
%% WPT: path/default
|
||||
wpt_path_default_test() ->
|
||||
URIMap = #{scheme => <<"http">>, host => <<?HOST>>, path => <<"/path/to/resource">>},
|
||||
Store0 = gun_cookies_list:init(),
|
||||
%% Add a cookie without a path attribute.
|
||||
{ok, N1, V1, A1} = cow_cookie:parse_set_cookie(<<"cookies-path-default=1">>),
|
||||
{ok, Store1} = set_cookie(Store0, URIMap, N1, V1, A1),
|
||||
%% Confirm the cookie was stored with the proper default path,
|
||||
%% and gets sent for the same path, other resources at the same level or child paths.
|
||||
{ok, [#{path := <<"/path/to">>}], _} = query(Store1, URIMap),
|
||||
{ok, [#{path := <<"/path/to">>}], _} = query(Store1, URIMap#{path => <<"/path/to/other">>}),
|
||||
{ok, [#{path := <<"/path/to">>}], _} = query(Store1, URIMap#{path => <<"/path/to/resource/sub">>}),
|
||||
%% Confirm that the cookie cannot be retrieved for parent or unrelated paths.
|
||||
{ok, [], _} = query(Store1, URIMap#{path => <<"/path">>}),
|
||||
{ok, [], _} = query(Store1, URIMap#{path => <<"/path/toon">>}),
|
||||
{ok, [], _} = query(Store1, URIMap#{path => <<"/">>}),
|
||||
%% Expire the cookie.
|
||||
{ok, N2, V2, A2} = cow_cookie:parse_set_cookie(<<"cookies-path-default=1; Max-Age=0">>),
|
||||
{ok, Store} = set_cookie(Store1, URIMap, N2, V2, A2),
|
||||
{ok, [], _} = query(Store, URIMap),
|
||||
{ok, [], _} = query(Store, URIMap#{path => <<"/path/to/other">>}),
|
||||
{ok, [], _} = query(Store, URIMap#{path => <<"/path/to/resource/sub">>}),
|
||||
ok.
|
||||
|
||||
%% WPT: path/match
|
||||
wpt_path_match_test_() ->
|
||||
URIMap = #{
|
||||
scheme => <<"http">>,
|
||||
host => <<?HOST>>,
|
||||
path => <<"/cookies/resources/echo-cookie.html">>
|
||||
},
|
||||
MatchTests = [
|
||||
<<"/">>,
|
||||
<<"match.html">>,
|
||||
<<"cookies">>,
|
||||
<<"/cookies">>,
|
||||
<<"/cookies/">>,
|
||||
<<"/cookies/resources/echo-cookie.html">>
|
||||
],
|
||||
NegTests = [
|
||||
<<"/cook">>,
|
||||
<<"/w/">>
|
||||
],
|
||||
[{P, fun() ->
|
||||
{ok, N1, V1, A1} = cow_cookie:parse_set_cookie(<<"a=b; Path=",P/binary>>),
|
||||
{ok, Store0} = set_cookie(gun_cookies_list:init(), URIMap, N1, V1, A1),
|
||||
{ok, [#{name := <<"a">>}], _} = query(Store0, URIMap),
|
||||
{ok, N2, V2, A2} = cow_cookie:parse_set_cookie(<<"a=b; Max-Age=0; Path=",P/binary>>),
|
||||
{ok, Store} = set_cookie(Store0, URIMap, N2, V2, A2),
|
||||
{ok, [], _} = query(Store, URIMap)
|
||||
end} || P <- MatchTests]
|
||||
++
|
||||
[{P, fun() ->
|
||||
{ok, N, V, A} = cow_cookie:parse_set_cookie(<<"a=b; Path=",P/binary>>),
|
||||
{ok, Store} = set_cookie(gun_cookies_list:init(), URIMap, N, V, A),
|
||||
{ok, [], _} = query(Store, URIMap)
|
||||
end} || P <- NegTests].
|
||||
|
||||
%% WPT: prefix/__host.header
|
||||
wpt_prefix_host_test_() ->
|
||||
Tests = [
|
||||
{<<"http">>, <<"__Host-foo=bar; Path=/;">>, false},
|
||||
{<<"http">>, <<"__Host-foo=bar; Path=/;domain=" ?HOST>>, false},
|
||||
{<<"http">>, <<"__Host-foo=bar; Path=/;Max-Age=10">>, false},
|
||||
{<<"http">>, <<"__Host-foo=bar; Path=/;HttpOnly">>, false},
|
||||
{<<"http">>, <<"__Host-foo=bar; Secure; Path=/;">>, false},
|
||||
{<<"http">>, <<"__Host-foo=bar; Secure; Path=/;domain=" ?HOST>>, false},
|
||||
{<<"http">>, <<"__Host-foo=bar; Secure; Path=/;Max-Age=10">>, false},
|
||||
{<<"http">>, <<"__Host-foo=bar; Secure; Path=/;HttpOnly">>, false},
|
||||
{<<"http">>, <<"__Host-foo=bar; Secure; Path=/; Domain=" ?HOST "; ">>, false},
|
||||
{<<"http">>, <<"__Host-foo=bar; Secure; Path=/; Domain=" ?HOST "; domain=" ?HOST>>, false},
|
||||
{<<"http">>, <<"__Host-foo=bar; Secure; Path=/; Domain=" ?HOST "; Max-Age=10">>, false},
|
||||
{<<"http">>, <<"__Host-foo=bar; Secure; Path=/; Domain=" ?HOST "; HttpOnly">>, false},
|
||||
{<<"http">>, <<"__Host-foo=bar; Secure; Path=/cookies/resources/list.py">>, false},
|
||||
{<<"https">>, <<"__Host-foo=bar; Path=/;">>, false},
|
||||
{<<"https">>, <<"__Host-foo=bar; Path=/;Max-Age=10">>, false},
|
||||
{<<"https">>, <<"__Host-foo=bar; Path=/;HttpOnly">>, false},
|
||||
{<<"https">>, <<"__Host-foo=bar; Secure; Path=/;">>, true},
|
||||
{<<"https">>, <<"__Host-foo=bar; Secure; Path=/;Max-Age=10">>, true},
|
||||
{<<"https">>, <<"__Host-foo=bar; Secure; Path=/;HttpOnly">>, true},
|
||||
{<<"https">>, <<"__Host-foo=bar; Secure; Path=/; Domain=" ?HOST "; ">>, false},
|
||||
{<<"https">>, <<"__Host-foo=bar; Secure; Path=/; Domain=" ?HOST "; Max-Age=10">>, false},
|
||||
{<<"https">>, <<"__Host-foo=bar; Secure; Path=/; Domain=" ?HOST "; HttpOnly">>, false},
|
||||
{<<"https">>, <<"__Host-foo=bar; Secure; Path=/cookies/resources/list.py">>, false}
|
||||
],
|
||||
wpt_prefix_common(Tests, <<"__Host-foo">>).
|
||||
|
||||
%% WPT: prefix/__secure.header
|
||||
wpt_prefix_secure_test_() ->
|
||||
Tests = [
|
||||
{<<"http">>, <<"__Secure-foo=bar; Path=/;">>, false},
|
||||
{<<"http">>, <<"__Secure-foo=bar; Path=/;domain=" ?HOST>>, false},
|
||||
{<<"http">>, <<"__Secure-foo=bar; Path=/;Max-Age=10">>, false},
|
||||
{<<"http">>, <<"__Secure-foo=bar; Path=/;HttpOnly">>, false},
|
||||
{<<"http">>, <<"__Secure-foo=bar; Secure; Path=/;">>, false},
|
||||
{<<"http">>, <<"__Secure-foo=bar; Secure; Path=/;domain=" ?HOST>>, false},
|
||||
{<<"http">>, <<"__Secure-foo=bar; Secure; Path=/;Max-Age=10">>, false},
|
||||
{<<"http">>, <<"__Secure-foo=bar; Secure; Path=/;HttpOnly">>, false},
|
||||
{<<"https">>, <<"__Secure-foo=bar; Path=/;">>, false},
|
||||
{<<"https">>, <<"__Secure-foo=bar; Path=/;Max-Age=10">>, false},
|
||||
{<<"https">>, <<"__Secure-foo=bar; Path=/;HttpOnly">>, false},
|
||||
{<<"https">>, <<"__Secure-foo=bar; Secure; Path=/;">>, true},
|
||||
{<<"https">>, <<"__Secure-foo=bar; Secure; Path=/;Max-Age=10">>, true},
|
||||
{<<"https">>, <<"__Secure-foo=bar; Secure; Path=/;HttpOnly">>, true}
|
||||
%% Missing two SameSite cases from prefix/__secure.header.https. (Not implemented.)
|
||||
],
|
||||
wpt_prefix_common(Tests, <<"__Secure-foo">>).
|
||||
|
||||
wpt_prefix_common(Tests, Name) ->
|
||||
URIMap0 = #{
|
||||
host => <<?HOST>>,
|
||||
path => <<"/cookies/resources/set.py">>
|
||||
},
|
||||
[{<<S/binary," ",H/binary>>, fun() ->
|
||||
URIMap1 = URIMap0#{scheme => S},
|
||||
{ok, N, V, A} = cow_cookie:parse_set_cookie(H),
|
||||
case set_cookie(gun_cookies_list:init(), URIMap1, N, V, A) of
|
||||
{ok, Store} when Res =:= true ->
|
||||
URIMap = URIMap1#{path => <<"/cookies/resources/list.py">>},
|
||||
{ok, [#{name := Name}], _} = query(Store, URIMap),
|
||||
ok;
|
||||
{error, _} ->
|
||||
ok
|
||||
end
|
||||
end} || {S, H, Res} <- Tests].
|
||||
|
||||
%% WPT: samesite-none-secure/ (Not implemented.)
|
||||
%% WPT: samesite/ (Not implemented.)
|
||||
|
||||
wpt_secure_https_test() ->
|
||||
URIMap = #{
|
||||
scheme => <<"https">>,
|
||||
host => <<?HOST>>,
|
||||
path => <<"/cookies/secure/any.html">>
|
||||
},
|
||||
{ok, N, V, A} = cow_cookie:parse_set_cookie(<<"secure_from_secure_http=1; Secure; Path=/">>),
|
||||
{ok, Store} = set_cookie(gun_cookies_list:init(), URIMap, N, V, A),
|
||||
{ok, [#{name := <<"secure_from_secure_http">>}], _} = query(Store, URIMap),
|
||||
ok.
|
||||
|
||||
wpt_secure_http_test() ->
|
||||
URIMap = #{
|
||||
scheme => <<"http">>,
|
||||
host => <<?HOST>>,
|
||||
path => <<"/cookies/secure/any.html">>
|
||||
},
|
||||
{ok, N, V, A} = cow_cookie:parse_set_cookie(<<"secure_from_nonsecure_http=1; Secure; Path=/">>),
|
||||
{error, secure_scheme_only} = set_cookie(gun_cookies_list:init(), URIMap, N, V, A),
|
||||
ok.
|
||||
|
||||
%% WPT: secure/set-from-ws* (Anything special required?)
|
||||
-endif.
|
|
@ -0,0 +1,144 @@
|
|||
%% Copyright (c) 2020-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
%% A reference cookie store implemented as a list of cookies.
|
||||
%% This cookie store cannot be shared between connections.
|
||||
-module(gun_cookies_list).
|
||||
-behavior(gun_cookies).
|
||||
|
||||
-export([init/0]).
|
||||
-export([init/1]).
|
||||
-export([query/2]).
|
||||
-export([set_cookie_secure_match/2]).
|
||||
-export([set_cookie_get_exact_match/2]).
|
||||
-export([store/2]).
|
||||
-export([gc/1]).
|
||||
-export([session_gc/1]).
|
||||
|
||||
-type state() :: #{
|
||||
cookies := [gun_cookies:cookie()]
|
||||
%% @todo max_cookies_per_domain => non_neg_integer() | infinity,
|
||||
%% @todo max_cookies => non_neg_integer() | infinity
|
||||
}.
|
||||
|
||||
-type opts() :: #{
|
||||
}.
|
||||
-export_type([opts/0]).
|
||||
|
||||
-spec init() -> {?MODULE, state()}.
|
||||
init() ->
|
||||
init(#{}).
|
||||
|
||||
-spec init(opts()) -> {?MODULE, state()}.
|
||||
init(_Opts) ->
|
||||
{?MODULE, #{cookies => []}}.
|
||||
|
||||
-spec query(State, uri_string:uri_map())
|
||||
-> {ok, [gun_cookies:cookie()], State}
|
||||
when State::state().
|
||||
query(State=#{cookies := Cookies}, URI) ->
|
||||
CurrentTime = erlang:universaltime(),
|
||||
query(State, URI, Cookies, CurrentTime, [], []).
|
||||
|
||||
query(State, _, [], _, CookieList, Cookies) ->
|
||||
{ok, CookieList, State#{cookies => Cookies}};
|
||||
query(State, URI=#{scheme := Scheme, host := Host, path := Path},
|
||||
[Cookie|Tail], CurrentTime, CookieList, Acc) ->
|
||||
Match0 = case Cookie of
|
||||
#{host_only := true, domain := Host} ->
|
||||
true;
|
||||
#{host_only := false, domain := Domain} ->
|
||||
gun_cookies:domain_match(Host, Domain);
|
||||
_ ->
|
||||
false
|
||||
end,
|
||||
Match1 = Match0 andalso
|
||||
gun_cookies:path_match(Path, maps:get(path, Cookie)),
|
||||
Match = Match1 andalso
|
||||
case {Cookie, Scheme} of
|
||||
{#{secure_only := true}, <<"https">>} -> true;
|
||||
{#{secure_only := false}, _} -> true;
|
||||
_ -> false
|
||||
end,
|
||||
%% This is where we would check the http_only flag should
|
||||
%% we want to implement a non-HTTP interface.
|
||||
%% This is where we would check for same-site/cross-site.
|
||||
case Match of
|
||||
true ->
|
||||
UpdatedCookie = Cookie#{last_access_time => CurrentTime},
|
||||
query(State, URI, Tail, CurrentTime,
|
||||
[UpdatedCookie|CookieList],
|
||||
[UpdatedCookie|Acc]);
|
||||
false ->
|
||||
query(State, URI, Tail, CurrentTime, CookieList, [Cookie|Acc])
|
||||
end.
|
||||
|
||||
-spec set_cookie_secure_match(state(), #{
|
||||
name := binary(),
|
||||
% secure_only := true,
|
||||
domain := binary(),
|
||||
path := binary()
|
||||
}) -> match | nomatch.
|
||||
set_cookie_secure_match(#{cookies := Cookies},
|
||||
#{name := Name, domain := Domain, path := Path}) ->
|
||||
Result = [Cookie || Cookie=#{name := CookieName, secure_only := true} <- Cookies,
|
||||
CookieName =:= Name,
|
||||
gun_cookies:domain_match(Domain, maps:get(domain, Cookie))
|
||||
orelse gun_cookies:domain_match(maps:get(domain, Cookie), Domain),
|
||||
gun_cookies:path_match(Path, maps:get(path, Cookie))],
|
||||
case Result of
|
||||
[] -> nomatch;
|
||||
_ -> match
|
||||
end.
|
||||
|
||||
-spec set_cookie_get_exact_match(State, #{
|
||||
name := binary(),
|
||||
domain := binary(),
|
||||
host_only := boolean(),
|
||||
path := binary()
|
||||
}) -> {ok, gun_cookies:cookie(), State} | error when State::state().
|
||||
set_cookie_get_exact_match(State=#{cookies := Cookies0}, Match) ->
|
||||
Result = [Cookie || Cookie <- Cookies0,
|
||||
Match =:= maps:with([name, domain, host_only, path], Cookie)],
|
||||
Cookies = [Cookie || Cookie <- Cookies0,
|
||||
Match =/= maps:with([name, domain, host_only, path], Cookie)],
|
||||
case Result of
|
||||
[] -> error;
|
||||
[Cookie] -> {ok, Cookie, State#{cookies => Cookies}}
|
||||
end.
|
||||
|
||||
-spec store(State, gun_cookies:cookie())
|
||||
-> {ok, State} | {error, any()}
|
||||
when State::state().
|
||||
store(State=#{cookies := Cookies}, NewCookie=#{expiry_time := ExpiryTime}) ->
|
||||
CurrentTime = erlang:universaltime(),
|
||||
if
|
||||
%% Do not store cookies with an expiry time in the past.
|
||||
ExpiryTime =/= infinity, CurrentTime >= ExpiryTime ->
|
||||
{ok, State};
|
||||
true ->
|
||||
{ok, State#{cookies => [NewCookie|Cookies]}}
|
||||
end.
|
||||
|
||||
-spec gc(State) -> {ok, State} when State::state().
|
||||
gc(State=#{cookies := Cookies0}) ->
|
||||
CurrentTime = erlang:universaltime(),
|
||||
Cookies = [C || C=#{expiry_time := ExpiryTime} <- Cookies0,
|
||||
(ExpiryTime =:= infinity) orelse (ExpiryTime >= CurrentTime)],
|
||||
{ok, State#{cookies => Cookies}}.
|
||||
|
||||
-spec session_gc(State) -> {ok, State} when State::state().
|
||||
session_gc(State=#{cookies := Cookies0}) ->
|
||||
Cookies = [C || C=#{persistent := true} <- Cookies0],
|
||||
{ok, State#{cookies => Cookies}}.
|
|
@ -0,0 +1,33 @@
|
|||
%% Copyright (c) 2017-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(gun_data_h).
|
||||
-behavior(gun_content_handler).
|
||||
|
||||
-export([init/5]).
|
||||
-export([handle/3]).
|
||||
|
||||
-record(state, {
|
||||
reply_to :: pid(),
|
||||
stream_ref :: gun:stream_ref()
|
||||
}).
|
||||
|
||||
-spec init(pid(), gun:stream_ref(), _, _, _) -> {ok, #state{}}.
|
||||
init(ReplyTo, StreamRef, _, _, _) ->
|
||||
{ok, #state{reply_to=ReplyTo, stream_ref=StreamRef}}.
|
||||
|
||||
-spec handle(fin | nofin, binary(), State) -> {done, 1, State} when State::#state{}.
|
||||
handle(IsFin, Data, State=#state{reply_to=ReplyTo, stream_ref=StreamRef}) ->
|
||||
ReplyTo ! {gun_data, self(), StreamRef, IsFin, Data},
|
||||
{done, 1, State}.
|
|
@ -0,0 +1,129 @@
|
|||
%% Copyright (c) 2019-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(gun_default_event_h).
|
||||
-behavior(gun_event).
|
||||
|
||||
-export([init/2]).
|
||||
-export([domain_lookup_start/2]).
|
||||
-export([domain_lookup_end/2]).
|
||||
-export([connect_start/2]).
|
||||
-export([connect_end/2]).
|
||||
-export([tls_handshake_start/2]).
|
||||
-export([tls_handshake_end/2]).
|
||||
-export([request_start/2]).
|
||||
-export([request_headers/2]).
|
||||
-export([request_end/2]).
|
||||
-export([push_promise_start/2]).
|
||||
-export([push_promise_end/2]).
|
||||
-export([response_start/2]).
|
||||
-export([response_inform/2]).
|
||||
-export([response_headers/2]).
|
||||
-export([response_trailers/2]).
|
||||
-export([response_end/2]).
|
||||
-export([ws_upgrade/2]).
|
||||
-export([ws_recv_frame_start/2]).
|
||||
-export([ws_recv_frame_header/2]).
|
||||
-export([ws_recv_frame_end/2]).
|
||||
-export([ws_send_frame_start/2]).
|
||||
-export([ws_send_frame_end/2]).
|
||||
-export([protocol_changed/2]).
|
||||
-export([origin_changed/2]).
|
||||
-export([cancel/2]).
|
||||
-export([disconnect/2]).
|
||||
-export([terminate/2]).
|
||||
|
||||
init(_EventData, State) ->
|
||||
State.
|
||||
|
||||
domain_lookup_start(_EventData, State) ->
|
||||
State.
|
||||
|
||||
domain_lookup_end(_EventData, State) ->
|
||||
State.
|
||||
|
||||
connect_start(_EventData, State) ->
|
||||
State.
|
||||
|
||||
connect_end(_EventData, State) ->
|
||||
State.
|
||||
|
||||
tls_handshake_start(_EventData, State) ->
|
||||
State.
|
||||
|
||||
tls_handshake_end(_EventData, State) ->
|
||||
State.
|
||||
|
||||
request_start(_EventData, State) ->
|
||||
State.
|
||||
|
||||
request_headers(_EventData, State) ->
|
||||
State.
|
||||
|
||||
request_end(_EventData, State) ->
|
||||
State.
|
||||
|
||||
push_promise_start(_EventData, State) ->
|
||||
State.
|
||||
|
||||
push_promise_end(_EventData, State) ->
|
||||
State.
|
||||
|
||||
response_start(_EventData, State) ->
|
||||
State.
|
||||
|
||||
response_inform(_EventData, State) ->
|
||||
State.
|
||||
|
||||
response_headers(_EventData, State) ->
|
||||
State.
|
||||
|
||||
response_trailers(_EventData, State) ->
|
||||
State.
|
||||
|
||||
response_end(_EventData, State) ->
|
||||
State.
|
||||
|
||||
ws_upgrade(_EventData, State) ->
|
||||
State.
|
||||
|
||||
ws_recv_frame_start(_EventData, State) ->
|
||||
State.
|
||||
|
||||
ws_recv_frame_header(_EventData, State) ->
|
||||
State.
|
||||
|
||||
ws_recv_frame_end(_EventData, State) ->
|
||||
State.
|
||||
|
||||
ws_send_frame_start(_EventData, State) ->
|
||||
State.
|
||||
|
||||
ws_send_frame_end(_EventData, State) ->
|
||||
State.
|
||||
|
||||
protocol_changed(_EventData, State) ->
|
||||
State.
|
||||
|
||||
origin_changed(_EventData, State) ->
|
||||
State.
|
||||
|
||||
cancel(_EventData, State) ->
|
||||
State.
|
||||
|
||||
disconnect(_EventData, State) ->
|
||||
State.
|
||||
|
||||
terminate(_EventData, State) ->
|
||||
State.
|
|
@ -0,0 +1,316 @@
|
|||
%% Copyright (c) 2019-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(gun_event).
|
||||
|
||||
%% init.
|
||||
|
||||
-type init_event() :: #{
|
||||
owner := pid(),
|
||||
transport := tcp | tls,
|
||||
origin_scheme := binary(),
|
||||
origin_host := inet:hostname() | inet:ip_address(),
|
||||
origin_port := inet:port_number(),
|
||||
opts := gun:opts()
|
||||
}.
|
||||
-export_type([init_event/0]).
|
||||
|
||||
-callback init(init_event(), State) -> State.
|
||||
|
||||
%% domain_lookup_start/domain_lookup_end.
|
||||
|
||||
-type domain_lookup_event() :: #{
|
||||
host := inet:hostname() | inet:ip_address(),
|
||||
port := inet:port_number(),
|
||||
tcp_opts := [gen_tcp:connect_option()],
|
||||
timeout := timeout(),
|
||||
lookup_info => gun_tcp:lookup_info(),
|
||||
error => any()
|
||||
}.
|
||||
-export_type([domain_lookup_event/0]).
|
||||
|
||||
-callback domain_lookup_start(domain_lookup_event(), State) -> State.
|
||||
-callback domain_lookup_end(domain_lookup_event(), State) -> State.
|
||||
|
||||
%% connect_start/connect_end.
|
||||
|
||||
-type connect_event() :: #{
|
||||
lookup_info := gun_tcp:lookup_info(),
|
||||
timeout := timeout(),
|
||||
socket => inet:socket(),
|
||||
protocol => http | http2 | socks | raw, %% Only when transport is tcp.
|
||||
error => any()
|
||||
}.
|
||||
-export_type([connect_event/0]).
|
||||
|
||||
-callback connect_start(connect_event(), State) -> State.
|
||||
-callback connect_end(connect_event(), State) -> State.
|
||||
|
||||
%% tls_handshake_start/tls_handshake_end.
|
||||
%%
|
||||
%% These events occur when connecting to a TLS server or when
|
||||
%% upgrading the connection or stream to use TLS, for example
|
||||
%% using CONNECT. The stream_ref/reply_to values are only
|
||||
%% present when the TLS handshake occurs in the scope of a request.
|
||||
|
||||
-type tls_handshake_event() :: #{
|
||||
stream_ref => gun:stream_ref(),
|
||||
reply_to => pid(),
|
||||
socket := inet:socket() | ssl:sslsocket() | pid(), %% The socket before/after will be different.
|
||||
tls_opts := [ssl:tls_client_option()],
|
||||
timeout := timeout(),
|
||||
protocol => http | http2 | socks | raw,
|
||||
error => any()
|
||||
}.
|
||||
-export_type([tls_handshake_event/0]).
|
||||
|
||||
-callback tls_handshake_start(tls_handshake_event(), State) -> State.
|
||||
-callback tls_handshake_end(tls_handshake_event(), State) -> State.
|
||||
|
||||
%% request_start/request_headers.
|
||||
|
||||
-type request_start_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
reply_to := pid(),
|
||||
function := headers | request | ws_upgrade, %% @todo connect?
|
||||
method := iodata(),
|
||||
scheme => binary(),
|
||||
authority := iodata(),
|
||||
path => iodata(),
|
||||
headers := [{binary(), iodata()}]
|
||||
}.
|
||||
-export_type([request_start_event/0]).
|
||||
|
||||
-callback request_start(request_start_event(), State) -> State.
|
||||
-callback request_headers(request_start_event(), State) -> State.
|
||||
|
||||
%% request_end.
|
||||
|
||||
-type request_end_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
reply_to := pid()
|
||||
}.
|
||||
-export_type([request_end_event/0]).
|
||||
|
||||
-callback request_end(request_end_event(), State) -> State.
|
||||
|
||||
%% push_promise_start.
|
||||
|
||||
-type push_promise_start_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
reply_to := pid()
|
||||
}.
|
||||
-export_type([push_promise_start_event/0]).
|
||||
|
||||
-callback push_promise_start(push_promise_start_event(), State) -> State.
|
||||
|
||||
%% push_promise_end.
|
||||
|
||||
-type push_promise_end_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
reply_to := pid(),
|
||||
%% No stream is created if we receive the push_promise while
|
||||
%% in the process of gracefully shutting down the connection.
|
||||
%% The promised stream is canceled immediately.
|
||||
promised_stream_ref => gun:stream_ref(),
|
||||
method := binary(),
|
||||
uri := binary(),
|
||||
headers := [{binary(), iodata()}]
|
||||
}.
|
||||
-export_type([push_promise_end_event/0]).
|
||||
|
||||
-callback push_promise_end(push_promise_end_event(), State) -> State.
|
||||
|
||||
%% response_start.
|
||||
|
||||
-type response_start_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
reply_to := pid()
|
||||
}.
|
||||
-export_type([response_start_event/0]).
|
||||
|
||||
-callback response_start(response_start_event(), State) -> State.
|
||||
|
||||
%% response_inform/response_headers.
|
||||
|
||||
-type response_headers_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
reply_to := pid(),
|
||||
status := non_neg_integer(),
|
||||
headers := [{binary(), binary()}]
|
||||
}.
|
||||
-export_type([response_headers_event/0]).
|
||||
|
||||
-callback response_inform(response_headers_event(), State) -> State.
|
||||
-callback response_headers(response_headers_event(), State) -> State.
|
||||
|
||||
%% response_trailers.
|
||||
|
||||
-type response_trailers_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
reply_to := pid(),
|
||||
headers := [{binary(), binary()}]
|
||||
}.
|
||||
-export_type([response_trailers_event/0]).
|
||||
|
||||
-callback response_trailers(response_trailers_event(), State) -> State.
|
||||
|
||||
%% response_end.
|
||||
|
||||
-type response_end_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
reply_to := pid()
|
||||
}.
|
||||
-export_type([response_end_event/0]).
|
||||
|
||||
-callback response_end(response_end_event(), State) -> State.
|
||||
|
||||
%% ws_upgrade.
|
||||
%%
|
||||
%% This event is a signal that the following request and response
|
||||
%% result from a gun:ws_upgrade/2,3,4 call.
|
||||
%%
|
||||
%% There is no corresponding "end" event. Instead, the success is
|
||||
%% indicated by a protocol_changed event following the informational
|
||||
%% response.
|
||||
|
||||
-type ws_upgrade_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
reply_to := pid(),
|
||||
opts := gun:ws_opts()
|
||||
}.
|
||||
-export_type([ws_upgrade_event/0]).
|
||||
|
||||
-callback ws_upgrade(ws_upgrade_event(), State) -> State.
|
||||
|
||||
%% ws_recv_frame_start.
|
||||
|
||||
-type ws_recv_frame_start_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
reply_to := pid(),
|
||||
frag_state := cow_ws:frag_state(),
|
||||
extensions := cow_ws:extensions()
|
||||
}.
|
||||
-export_type([ws_recv_frame_start_event/0]).
|
||||
|
||||
-callback ws_recv_frame_start(ws_recv_frame_start_event(), State) -> State.
|
||||
|
||||
%% ws_recv_frame_header.
|
||||
|
||||
-type ws_recv_frame_header_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
reply_to := pid(),
|
||||
frag_state := cow_ws:frag_state(),
|
||||
extensions := cow_ws:extensions(),
|
||||
type := cow_ws:frame_type(),
|
||||
rsv := cow_ws:rsv(),
|
||||
len := non_neg_integer(),
|
||||
mask_key := cow_ws:mask_key()
|
||||
}.
|
||||
-export_type([ws_recv_frame_header_event/0]).
|
||||
|
||||
-callback ws_recv_frame_header(ws_recv_frame_header_event(), State) -> State.
|
||||
|
||||
%% ws_recv_frame_end.
|
||||
|
||||
-type ws_recv_frame_end_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
reply_to := pid(),
|
||||
extensions := cow_ws:extensions(),
|
||||
close_code := undefined | cow_ws:close_code(),
|
||||
payload := binary()
|
||||
}.
|
||||
-export_type([ws_recv_frame_end_event/0]).
|
||||
|
||||
-callback ws_recv_frame_end(ws_recv_frame_end_event(), State) -> State.
|
||||
|
||||
%% ws_send_frame_start/ws_send_frame_end.
|
||||
|
||||
-type ws_send_frame_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
reply_to := pid(),
|
||||
extensions := cow_ws:extensions(),
|
||||
frame := gun:ws_frame()
|
||||
}.
|
||||
-export_type([ws_send_frame_event/0]).
|
||||
|
||||
-callback ws_send_frame_start(ws_send_frame_event(), State) -> State.
|
||||
-callback ws_send_frame_end(ws_send_frame_event(), State) -> State.
|
||||
|
||||
%% protocol_changed.
|
||||
%%
|
||||
%% This event can occur either following a successful ws_upgrade
|
||||
%% event, following a successful CONNECT request or a SOCKS tunnel.
|
||||
|
||||
-type protocol_changed_event() :: #{
|
||||
stream_ref => gun:stream_ref(),
|
||||
protocol := http | http2 | socks | raw | ws
|
||||
}.
|
||||
-export_type([protocol_changed_event/0]).
|
||||
|
||||
-callback protocol_changed(protocol_changed_event(), State) -> State.
|
||||
|
||||
%% origin_changed.
|
||||
|
||||
-type origin_changed_event() :: #{
|
||||
stream_ref => gun:stream_ref(),
|
||||
type := connect | socks5,
|
||||
origin_scheme := binary(),
|
||||
origin_host := inet:hostname() | inet:ip_address(),
|
||||
origin_port := inet:port_number()
|
||||
}.
|
||||
-export_type([origin_changed_event/0]).
|
||||
|
||||
-callback origin_changed(origin_changed_event(), State) -> State.
|
||||
|
||||
%% cancel.
|
||||
%%
|
||||
%% In the case of HTTP/1.1 we cannot actually cancel the stream,
|
||||
%% we only silence the stream to the user. Further response events
|
||||
%% may therefore be received and they provide a useful metric as
|
||||
%% these canceled requests monopolize the connection.
|
||||
%%
|
||||
%% For HTTP/2 both the client and the server may cancel streams.
|
||||
%% Events may still occur for a short time after the cancel.
|
||||
|
||||
-type cancel_event() :: #{
|
||||
stream_ref := gun:stream_ref(),
|
||||
reply_to := pid(),
|
||||
endpoint := local | remote,
|
||||
reason := atom()
|
||||
}.
|
||||
-export_type([cancel_event/0]).
|
||||
|
||||
-callback cancel(cancel_event(), State) -> State.
|
||||
|
||||
%% disconnect.
|
||||
|
||||
-type disconnect_event() :: #{
|
||||
reason := normal | closed | {error, any()}
|
||||
}.
|
||||
-export_type([disconnect_event/0]).
|
||||
|
||||
-callback disconnect(disconnect_event(), State) -> State.
|
||||
|
||||
%% terminate.
|
||||
|
||||
-type terminate_event() :: #{
|
||||
state := not_connected
|
||||
| domain_lookup | connecting | initial_tls_handshake | tls_handshake
|
||||
| connected | connected_data_only | connected_ws_only,
|
||||
reason := normal | shutdown | {shutdown, any()} | any()
|
||||
}.
|
||||
-export_type([terminate_event/0]).
|
||||
|
||||
-callback terminate(terminate_event(), State) -> State.
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,719 @@
|
|||
%% Copyright (c) 2021-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(gun_pool).
|
||||
-behaviour(gen_statem).
|
||||
|
||||
%% Pools.
|
||||
-export([start_pool/3]).
|
||||
-export([stop_pool/2]).
|
||||
-export([stop_pool/3]).
|
||||
% @todo shutdown pool?
|
||||
-export([info/0]).
|
||||
-export([info/1]).
|
||||
-export([info/2]).
|
||||
-export([await_up/1]).
|
||||
-export([await_up/2]).
|
||||
-export([checkout/2]). %% Use responsibly!
|
||||
|
||||
%% Requests.
|
||||
-export([delete/2]).
|
||||
-export([delete/3]).
|
||||
-export([get/2]).
|
||||
-export([get/3]).
|
||||
-export([head/2]).
|
||||
-export([head/3]).
|
||||
-export([options/2]).
|
||||
-export([options/3]).
|
||||
-export([patch/2]).
|
||||
-export([patch/3]).
|
||||
-export([patch/4]).
|
||||
-export([post/2]).
|
||||
-export([post/3]).
|
||||
-export([post/4]).
|
||||
-export([put/2]).
|
||||
-export([put/3]).
|
||||
-export([put/4]).
|
||||
|
||||
%% Generic requests interface.
|
||||
-export([headers/3]).
|
||||
-export([headers/4]).
|
||||
-export([request/4]).
|
||||
-export([request/5]).
|
||||
|
||||
%% Streaming data.
|
||||
-export([data/3]).
|
||||
|
||||
%% Tunneling. (HTTP/2+ only.)
|
||||
%% @todo -export([connect/2]).
|
||||
%% @todo -export([connect/3]).
|
||||
%% @todo -export([connect/4]).
|
||||
|
||||
%% Cookies.
|
||||
%% @todo -export([gc_cookies/1]).
|
||||
%% @todo -export([session_gc_cookies/1]).
|
||||
|
||||
%% Awaiting gun messages.
|
||||
-export([await/1]).
|
||||
-export([await/2]).
|
||||
-export([await/3]).
|
||||
-export([await_body/1]).
|
||||
-export([await_body/2]).
|
||||
-export([await_body/3]).
|
||||
|
||||
%% Flushing gun messages.
|
||||
-export([flush/1]).
|
||||
|
||||
%% Streams.
|
||||
-export([update_flow/2]).
|
||||
-export([cancel/1]).
|
||||
-export([stream_info/1]).
|
||||
|
||||
%% Websocket. (HTTP/2+ only for upgrade.)
|
||||
%% -export([ws_upgrade/1]).
|
||||
%% -export([ws_upgrade/2]).
|
||||
%% -export([ws_upgrade/3]).
|
||||
-export([ws_send/2]).
|
||||
%% -export([ws_send/3]). (HTTP/2+ only.)
|
||||
|
||||
%% Internals.
|
||||
-export([callback_mode/0]).
|
||||
-export([start_link/3]).
|
||||
-export([init/1]).
|
||||
-export([degraded/3]).
|
||||
-export([operational/3]).
|
||||
-export([terminate/3]).
|
||||
|
||||
-type setup_msg() :: {gun_up, pid(), http | http2 | raw | socks}
|
||||
| {gun_upgrade, pid(), gun:stream_ref(), [binary()], [{binary(), binary()}]}.
|
||||
|
||||
-type opts() :: #{
|
||||
conn_opts => gun:opts(),
|
||||
scope => any(),
|
||||
setup_fun => {fun((pid(), setup_msg(), any()) -> any()), any()},
|
||||
size => non_neg_integer()
|
||||
}.
|
||||
-export_type([opts/0]).
|
||||
|
||||
-type pool_stream_ref() :: {pid(), gun:stream_ref()}.
|
||||
-export_type([pool_stream_ref/0]).
|
||||
|
||||
-type error_result() :: {error, pool_not_found | no_connection_available, atom()}. %% @todo {pool_start_error, SupError}
|
||||
|
||||
-type request_result() :: {async, pool_stream_ref()}
|
||||
%% @todo {sync, ...} perhaps with Status, Headers, Body, and Extra info such as intermediate responses.
|
||||
| error_result().
|
||||
|
||||
-type req_opts() :: #{
|
||||
%% Options common with normal Gun.
|
||||
flow => pos_integer(),
|
||||
reply_to => pid(),
|
||||
% @todo tunnel => stream_ref(),
|
||||
|
||||
%% Options specific to pools.
|
||||
checkout_call_timeout => timeout(),
|
||||
checkout_retry => [pos_integer()],
|
||||
scope => any(),
|
||||
start_pool_if_missing => boolean()
|
||||
}.
|
||||
-export_type([req_opts/0]).
|
||||
|
||||
-type ws_send_opts() :: #{
|
||||
authority => iodata(),
|
||||
|
||||
%% Options specific to pools.
|
||||
checkout_call_timeout => timeout(),
|
||||
checkout_retry => [pos_integer()],
|
||||
scope => any(),
|
||||
start_pool_if_missing => boolean()
|
||||
}.
|
||||
-export_type([ws_send_opts/0]).
|
||||
|
||||
%% @todo tunnel
|
||||
-type meta() :: #{pid() => #{ws => gun:stream_ref()}}.
|
||||
|
||||
-record(state, {
|
||||
host :: inet:hostname() | inet:ip_address(),
|
||||
port :: inet:port_number(),
|
||||
opts :: opts(),
|
||||
table :: ets:tid(),
|
||||
conns :: #{pid() => down | {setup, any()} | {up, http | http2 | ws | raw, map()}},
|
||||
conns_meta = #{} :: meta(),
|
||||
await_up = [] :: [{pid(), any()}]
|
||||
}).
|
||||
|
||||
%% Pool management.
|
||||
|
||||
-spec start_pool(inet:hostname() | inet:ip_address(), inet:port_number(), opts())
|
||||
-> {ok, pid()} | {error, any()}.
|
||||
start_pool(Host, Port, Opts) ->
|
||||
supervisor:start_child(gun_pools_sup, [Host, Port, Opts]).
|
||||
|
||||
-spec stop_pool(inet:hostname() | inet:ip_address(), inet:port_number())
|
||||
-> ok.
|
||||
stop_pool(Host, Port) ->
|
||||
stop_pool(Host, Port, #{}).
|
||||
|
||||
-type stop_opts() :: #{
|
||||
scope => any(),
|
||||
transport => tcp | tls
|
||||
}.
|
||||
|
||||
-spec stop_pool(inet:hostname() | inet:ip_address(), inet:port_number(), stop_opts())
|
||||
-> ok | {error, pool_not_found, atom()}.
|
||||
stop_pool(Host, Port, StopOpts) ->
|
||||
Transport = maps:get(transport, StopOpts, gun:default_transport(Port)),
|
||||
Authority = gun_http:host_header(Transport, Host, Port),
|
||||
case get_pool(Authority, StopOpts) of
|
||||
undefined ->
|
||||
{error, pool_not_found,
|
||||
'No pool was found for the given scope and authority.'};
|
||||
ManagerPid ->
|
||||
supervisor:terminate_child(gun_pools_sup, ManagerPid)
|
||||
end.
|
||||
|
||||
-spec info() -> [map()].
|
||||
info() ->
|
||||
ets:foldl(fun({{Scope, _}, ManagerPid}, Acc) ->
|
||||
{StateName, Info} = info(ManagerPid),
|
||||
[Info#{scope => Scope, state => StateName}|Acc]
|
||||
end, [], gun_pools).
|
||||
|
||||
-spec info(pid() | binary()) -> undefined | {degraded | operational, map()}.
|
||||
info(ManagerPid) when is_pid(ManagerPid) ->
|
||||
gen_statem:call(ManagerPid, info);
|
||||
info(Authority) ->
|
||||
info(Authority, default).
|
||||
|
||||
-spec info(binary(), any()) -> undefined | {degraded | operational, map()}.
|
||||
info(Authority, Scope) ->
|
||||
case ets:lookup(gun_pools, {Scope, Authority}) of
|
||||
[] ->
|
||||
undefined;
|
||||
[{_, ManagerPid}] ->
|
||||
gen_statem:call(ManagerPid, info)
|
||||
end.
|
||||
|
||||
-spec await_up(pid() | binary()) -> ok | {error, pool_not_found, atom()}.
|
||||
await_up(ManagerPid) when is_pid(ManagerPid) ->
|
||||
gen_statem:call(ManagerPid, await_up, 5000);
|
||||
await_up(Authority) ->
|
||||
await_up(Authority, default).
|
||||
|
||||
-spec await_up(binary(), any()) -> ok | {error, pool_not_found, atom()}.
|
||||
await_up(Authority, Scope) ->
|
||||
case ets:lookup(gun_pools, {Scope, Authority}) of
|
||||
[] ->
|
||||
{error, pool_not_found,
|
||||
'No pool was found for the given scope and authority.'};
|
||||
[{_, ManagerPid}] ->
|
||||
gen_statem:call(ManagerPid, await_up, 5000)
|
||||
end.
|
||||
|
||||
-spec checkout(pid(), req_opts() | ws_send_opts()) -> undefined | {pid(), map()}.
|
||||
checkout(ManagerPid, ReqOpts=#{checkout_retry := Retry}) when is_list(Retry) ->
|
||||
CallTimeout = maps:get(checkout_call_timeout, ReqOpts, 5000),
|
||||
case gen_server:call(ManagerPid, {checkout, ReqOpts}, CallTimeout) of
|
||||
undefined ->
|
||||
checkout_retry(ManagerPid, ReqOpts, CallTimeout, Retry);
|
||||
Result ->
|
||||
Result
|
||||
end;
|
||||
checkout(ManagerPid, ReqOpts) ->
|
||||
CallTimeout = maps:get(checkout_call_timeout, ReqOpts, 5000),
|
||||
gen_server:call(ManagerPid, {checkout, ReqOpts}, CallTimeout).
|
||||
|
||||
%% When the checkout_retry option is used, and the first call resulted
|
||||
%% in no connection being given out, we wait for the configured amount
|
||||
%% of time then try again. We loop over the wait times until there is
|
||||
%% none.
|
||||
checkout_retry(_, _, _, []) ->
|
||||
undefined;
|
||||
checkout_retry(ManagerPid, ReqOpts, CallTimeout, [Wait|Retry]) ->
|
||||
timer:sleep(Wait),
|
||||
case gen_server:call(ManagerPid, {checkout, ReqOpts}, CallTimeout) of
|
||||
undefined ->
|
||||
checkout_retry(ManagerPid, ReqOpts, CallTimeout, Retry);
|
||||
Result ->
|
||||
Result
|
||||
end.
|
||||
|
||||
%% Requests.
|
||||
|
||||
-spec delete(iodata(), gun:req_headers()) -> request_result().
|
||||
delete(Path, Headers) ->
|
||||
request(<<"DELETE">>, Path, Headers, <<>>).
|
||||
|
||||
-spec delete(iodata(), gun:req_headers(), req_opts()) -> request_result().
|
||||
delete(Path, Headers, ReqOpts) ->
|
||||
request(<<"DELETE">>, Path, Headers, <<>>, ReqOpts).
|
||||
|
||||
-spec get(iodata(), gun:req_headers()) -> request_result().
|
||||
get(Path, Headers) ->
|
||||
request(<<"GET">>, Path, Headers, <<>>).
|
||||
|
||||
-spec get(iodata(), gun:req_headers(), req_opts()) -> request_result().
|
||||
get(Path, Headers, ReqOpts) ->
|
||||
request(<<"GET">>, Path, Headers, <<>>, ReqOpts).
|
||||
|
||||
-spec head(iodata(), gun:req_headers()) -> request_result().
|
||||
head(Path, Headers) ->
|
||||
request(<<"HEAD">>, Path, Headers, <<>>).
|
||||
|
||||
-spec head(iodata(), gun:req_headers(), req_opts()) -> request_result().
|
||||
head(Path, Headers, ReqOpts) ->
|
||||
request(<<"HEAD">>, Path, Headers, <<>>, ReqOpts).
|
||||
|
||||
-spec options(iodata(), gun:req_headers()) -> request_result().
|
||||
options(Path, Headers) ->
|
||||
request(<<"OPTIONS">>, Path, Headers, <<>>).
|
||||
|
||||
-spec options(iodata(), gun:req_headers(), req_opts()) -> request_result().
|
||||
options(Path, Headers, ReqOpts) ->
|
||||
request(<<"OPTIONS">>, Path, Headers, <<>>, ReqOpts).
|
||||
|
||||
-spec patch(iodata(), gun:req_headers()) -> request_result().
|
||||
patch(Path, Headers) ->
|
||||
headers(<<"PATCH">>, Path, Headers).
|
||||
|
||||
-spec patch(iodata(), gun:req_headers(), iodata() | req_opts()) -> request_result().
|
||||
patch(Path, Headers, ReqOpts) when is_map(ReqOpts) ->
|
||||
headers(<<"PATCH">>, Path, Headers, ReqOpts);
|
||||
patch(Path, Headers, Body) ->
|
||||
request(<<"PATCH">>, Path, Headers, Body).
|
||||
|
||||
-spec patch(iodata(), gun:req_headers(), iodata(), req_opts()) -> request_result().
|
||||
patch(Path, Headers, Body, ReqOpts) ->
|
||||
request(<<"PATCH">>, Path, Headers, Body, ReqOpts).
|
||||
|
||||
-spec post(iodata(), gun:req_headers()) -> request_result().
|
||||
post(Path, Headers) ->
|
||||
headers(<<"POST">>, Path, Headers).
|
||||
|
||||
-spec post(iodata(), gun:req_headers(), iodata() | req_opts()) -> request_result().
|
||||
post(Path, Headers, ReqOpts) when is_map(ReqOpts) ->
|
||||
headers(<<"POST">>, Path, Headers, ReqOpts);
|
||||
post(Path, Headers, Body) ->
|
||||
request(<<"POST">>, Path, Headers, Body).
|
||||
|
||||
-spec post(iodata(), gun:req_headers(), iodata(), req_opts()) -> request_result().
|
||||
post(Path, Headers, Body, ReqOpts) ->
|
||||
request(<<"POST">>, Path, Headers, Body, ReqOpts).
|
||||
|
||||
-spec put(iodata(), gun:req_headers()) -> request_result().
|
||||
put(Path, Headers) ->
|
||||
headers(<<"PUT">>, Path, Headers).
|
||||
|
||||
-spec put(iodata(), gun:req_headers(), iodata() | req_opts()) -> request_result().
|
||||
put(Path, Headers, ReqOpts) when is_map(ReqOpts) ->
|
||||
headers(<<"PUT">>, Path, Headers, ReqOpts);
|
||||
put(Path, Headers, Body) ->
|
||||
request(<<"PUT">>, Path, Headers, Body).
|
||||
|
||||
-spec put(iodata(), gun:req_headers(), iodata(), req_opts()) -> request_result().
|
||||
put(Path, Headers, Body, ReqOpts) ->
|
||||
request(<<"PUT">>, Path, Headers, Body, ReqOpts).
|
||||
|
||||
%% Generic requests interface.
|
||||
%%
|
||||
%% @todo Accept a TargetURI map as well as a normal Path.
|
||||
|
||||
-spec headers(iodata(), iodata(), gun:req_headers()) -> request_result().
|
||||
headers(Method, Path, Headers) ->
|
||||
headers(Method, Path, Headers, #{}).
|
||||
|
||||
-spec headers(iodata(), iodata(), gun:req_headers(), req_opts()) -> request_result().
|
||||
headers(Method, Path, Headers, ReqOpts) ->
|
||||
case get_pool(authority(Headers), ReqOpts) of
|
||||
undefined ->
|
||||
{error, pool_not_found,
|
||||
'No pool was found for the given scope and authority.'};
|
||||
ManagerPid ->
|
||||
case checkout(ManagerPid, ReqOpts) of
|
||||
undefined ->
|
||||
{error, no_connection_available,
|
||||
'No connection in the pool with enough capacity available to open a new stream.'};
|
||||
{ConnPid, _Meta} ->
|
||||
StreamRef = gun:headers(ConnPid, Method, Path, Headers, ReqOpts),
|
||||
%% @todo Synchronous mode.
|
||||
{async, {ConnPid, StreamRef}}
|
||||
end
|
||||
end.
|
||||
|
||||
-spec request(iodata(), iodata(), gun:req_headers(), iodata()) -> request_result().
|
||||
request(Method, Path, Headers, Body) ->
|
||||
request(Method, Path, Headers, Body, #{}).
|
||||
|
||||
-spec request(iodata(), iodata(), gun:req_headers(), iodata(), req_opts()) -> request_result().
|
||||
request(Method, Path, Headers, Body, ReqOpts) ->
|
||||
case get_pool(authority(Headers), ReqOpts) of
|
||||
undefined ->
|
||||
{error, pool_not_found,
|
||||
'No pool was found for the given scope and authority.'};
|
||||
ManagerPid ->
|
||||
case checkout(ManagerPid, ReqOpts) of
|
||||
undefined ->
|
||||
{error, no_connection_available,
|
||||
'No connection in the pool with enough capacity available to open a new stream.'};
|
||||
{ConnPid, _Meta} ->
|
||||
StreamRef = gun:request(ConnPid, Method, Path, Headers, Body, ReqOpts),
|
||||
%% @todo Synchronous mode.
|
||||
{async, {ConnPid, StreamRef}}
|
||||
end
|
||||
end.
|
||||
|
||||
%% We require the host to be given in the headers for the time being.
|
||||
%% @todo Allow passing it in options. Websocket send already does that.
|
||||
authority(#{<<"host">> := Authority}) ->
|
||||
Authority;
|
||||
authority(Headers) ->
|
||||
{_, Authority} = lists:keyfind(<<"host">>, 1, Headers),
|
||||
Authority.
|
||||
|
||||
-spec get_pool(iolist(), req_opts() | ws_send_opts() | stop_opts()) -> pid() | undefined.
|
||||
get_pool(Authority0, ReqOpts) ->
|
||||
Authority = iolist_to_binary(Authority0),
|
||||
%% @todo Perhaps rename this to temporary.
|
||||
%% There's two concepts: temporary pool is started and stops
|
||||
%% when there's no traffic. Dynamic pool simply reduces its number
|
||||
%% of connections when there's no traffic.
|
||||
StartPoolIfMissing = maps:get(start_pool_if_missing, ReqOpts, false),
|
||||
Scope = maps:get(scope, ReqOpts, default),
|
||||
case ets:lookup(gun_pools, {Scope, Authority}) of
|
||||
[] when StartPoolIfMissing ->
|
||||
start_missing_pool(Authority, ReqOpts);
|
||||
[] ->
|
||||
undefined;
|
||||
[{_, ManagerPid}] ->
|
||||
%% @todo With temporary pool, getting a pid here doesn't mean the pool can be used.
|
||||
%% Indeed the manager could be in process of stopping. I suppose we must
|
||||
%% do a check but perhaps it's best to leave that detail to the user
|
||||
%% (they can easily retry and recreate the pool if necessary).
|
||||
ManagerPid
|
||||
end.
|
||||
|
||||
start_missing_pool(_Authority, _ReqOpts) ->
|
||||
undefined.
|
||||
|
||||
%% Streaming data.
|
||||
|
||||
-spec data(pool_stream_ref(), fin | nofin, iodata()) -> ok.
|
||||
data({ConnPid, StreamRef}, IsFin, Data) ->
|
||||
gun:data(ConnPid, StreamRef, IsFin, Data).
|
||||
|
||||
%% Awaiting gun messages.
|
||||
|
||||
-spec await(pool_stream_ref()) -> gun:await_result().
|
||||
await({ConnPid, StreamRef}) ->
|
||||
gun:await(ConnPid, StreamRef).
|
||||
|
||||
-spec await(pool_stream_ref(), timeout() | reference()) -> gun:await_result().
|
||||
await({ConnPid, StreamRef}, MRefOrTimeout) ->
|
||||
gun:await(ConnPid, StreamRef, MRefOrTimeout).
|
||||
|
||||
-spec await(pool_stream_ref(), timeout(), reference()) -> gun:await_result().
|
||||
await({ConnPid, StreamRef}, Timeout, MRef) ->
|
||||
gun:await(ConnPid, StreamRef, Timeout, MRef).
|
||||
|
||||
-spec await_body(pool_stream_ref()) -> gun:await_body_result().
|
||||
await_body({ConnPid, StreamRef}) ->
|
||||
gun:await_body(ConnPid, StreamRef).
|
||||
|
||||
-spec await_body(pool_stream_ref(), timeout() | reference()) -> gun:await_body_result().
|
||||
await_body({ConnPid, StreamRef}, MRefOrTimeout) ->
|
||||
gun:await_body(ConnPid, StreamRef, MRefOrTimeout).
|
||||
|
||||
-spec await_body(pool_stream_ref(), timeout(), reference()) -> gun:await_body_result().
|
||||
await_body({ConnPid, StreamRef}, Timeout, MRef) ->
|
||||
gun:await_body(ConnPid, StreamRef, Timeout, MRef).
|
||||
|
||||
%% Flushing gun messages.
|
||||
|
||||
-spec flush(pool_stream_ref()) -> ok.
|
||||
flush({ConnPid, _}) ->
|
||||
gun:flush(ConnPid).
|
||||
|
||||
%% Flow control.
|
||||
|
||||
-spec update_flow(pool_stream_ref(), pos_integer()) -> ok.
|
||||
update_flow({ConnPid, StreamRef}, Flow) ->
|
||||
gun:update_flow(ConnPid, StreamRef, Flow).
|
||||
|
||||
%% Cancelling a stream.
|
||||
|
||||
-spec cancel(pool_stream_ref()) -> ok.
|
||||
cancel({ConnPid, StreamRef}) ->
|
||||
gun:cancel(ConnPid, StreamRef).
|
||||
|
||||
%% Information about a stream.
|
||||
|
||||
-spec stream_info(pool_stream_ref()) -> {ok, map() | undefined} | {error, not_connected}.
|
||||
stream_info({ConnPid, StreamRef}) ->
|
||||
gun:stream_info(ConnPid, StreamRef).
|
||||
|
||||
%% Websocket.
|
||||
|
||||
-spec ws_send(gun:ws_frame() | [gun:ws_frame()], ws_send_opts()) -> ok | error_result().
|
||||
ws_send(Frames, WsSendOpts=#{authority := Authority}) ->
|
||||
case get_pool(Authority, WsSendOpts) of
|
||||
undefined ->
|
||||
{error, pool_not_found,
|
||||
'No pool was found for the given scope and authority.'};
|
||||
ManagerPid ->
|
||||
case checkout(ManagerPid, WsSendOpts) of
|
||||
undefined ->
|
||||
{error, no_connection_available,
|
||||
'No connection in the pool with enough capacity available to send Websocket frames.'};
|
||||
{ConnPid, #{ws := StreamRef}} ->
|
||||
gun:ws_send(ConnPid, StreamRef, Frames)
|
||||
end
|
||||
end.
|
||||
|
||||
%% Pool manager internals.
|
||||
%%
|
||||
%% The pool manager is responsible for starting connection processes
|
||||
%% and restarting them as necessary. It also provides a suitable
|
||||
%% connection process to any caller that needs it.
|
||||
%%
|
||||
%% The pool manager installs an event handler into each connection.
|
||||
%% The event handler is responsible for counting the number of
|
||||
%% active streams. It updates the gun_pooled_conns ets table
|
||||
%% whenever a stream begins or ends.
|
||||
%%
|
||||
%% A connection is deemed suitable if it is possible to open new
|
||||
%% streams. How many streams can be open at any one time depends
|
||||
%% on the protocol. For HTTP/2 the manager process keeps track of
|
||||
%% the connection's settings to know the maximum. For non-stream
|
||||
%% based protocols, there is no limit.
|
||||
%%
|
||||
%% The connection to be used is otherwise chosen randomly. The
|
||||
%% first connection that is suitable is returned. There is no
|
||||
%% need to "give back" the connection to the manager.
|
||||
|
||||
%% @todo
|
||||
%% What should happen if we always fail to reconnect? I suspect we keep the manager
|
||||
%% around and propagate errors, the same as if there's no more capacity? Perhaps have alarms?
|
||||
|
||||
callback_mode() -> state_functions.
|
||||
|
||||
start_link(Host, Port, Opts) ->
|
||||
gen_statem:start_link(?MODULE, {Host, Port, Opts}, []).
|
||||
|
||||
init({Host, Port, Opts}) ->
|
||||
process_flag(trap_exit, true),
|
||||
true = ets:insert_new(gun_pools, {gun_pools_key(Host, Port, Opts), self()}),
|
||||
Tid = ets:new(gun_pooled_conns, [ordered_set, public]),
|
||||
Size = maps:get(size, Opts, 8),
|
||||
%% @todo Only start processes in static mode.
|
||||
ConnOpts = conn_opts(Tid, Opts),
|
||||
Conns = maps:from_list([begin
|
||||
{ok, ConnPid} = gun:open(Host, Port, ConnOpts),
|
||||
_ = monitor(process, ConnPid),
|
||||
{ConnPid, down}
|
||||
end || _ <- lists:seq(1, Size)]),
|
||||
State = #state{
|
||||
host=Host,
|
||||
port=Port,
|
||||
opts=Opts,
|
||||
table=Tid,
|
||||
conns=Conns
|
||||
},
|
||||
%% If Size is 0 then we can never be operational.
|
||||
{ok, degraded, State}.
|
||||
|
||||
gun_pools_key(Host, Port, Opts) ->
|
||||
Transport = maps:get(transport, Opts, gun:default_transport(Port)),
|
||||
Authority = gun_http:host_header(Transport, Host, Port),
|
||||
Scope = maps:get(scope, Opts, default),
|
||||
{Scope, iolist_to_binary(Authority)}.
|
||||
|
||||
conn_opts(Tid, Opts) ->
|
||||
ConnOpts = maps:get(conn_opts, Opts, #{}),
|
||||
EventHandlerState = maps:with([event_handler], ConnOpts),
|
||||
H2Opts = maps:get(http2_opts, ConnOpts, #{}),
|
||||
ConnOpts#{
|
||||
event_handler => {gun_pool_events_h, EventHandlerState#{
|
||||
table => Tid
|
||||
}},
|
||||
http2_opts => H2Opts#{
|
||||
notify_settings_changed => true
|
||||
}
|
||||
}.
|
||||
|
||||
%% We use the degraded state as long as at least one connection is degraded.
|
||||
%% @todo Probably keep count of connections separately to avoid counting every time.
|
||||
degraded(info, Msg={gun_up, ConnPid, _}, StateData=#state{opts=Opts, conns=Conns}) ->
|
||||
#{ConnPid := down} = Conns,
|
||||
%% We optionally run the setup function if one is defined. The
|
||||
%% setup function tells us whether we are fully up or not. The
|
||||
%% setup function may be called repeatedly until the connection
|
||||
%% is established.
|
||||
%%
|
||||
%% @todo It is possible that the connection never does get
|
||||
%% fully established. We should deal with this. We probably
|
||||
%% need to handle all messages.
|
||||
{SetupFun, SetupState0} = setup_fun(Opts),
|
||||
degraded_setup(ConnPid, Msg, StateData, SetupFun, SetupState0);
|
||||
%% @todo
|
||||
%degraded(info, Msg={gun_tunnel_up, ConnPid, _, _}, StateData0=#state{conns=Conns}) ->
|
||||
% ;
|
||||
degraded(info, Msg={gun_upgrade, ConnPid, _, _, _},
|
||||
StateData=#state{opts=#{setup_fun := {SetupFun, _}}, conns=Conns}) ->
|
||||
%% @todo Probably shouldn't crash if the state is incorrect, that's programmer error though.
|
||||
#{ConnPid := {setup, SetupState0}} = Conns,
|
||||
%% We run the setup function again using the state previously kept.
|
||||
degraded_setup(ConnPid, Msg, StateData, SetupFun, SetupState0);
|
||||
degraded(Type, Event, StateData) ->
|
||||
handle_common(Type, Event, ?FUNCTION_NAME, StateData).
|
||||
|
||||
setup_fun(#{setup_fun := SetupFun}) ->
|
||||
SetupFun;
|
||||
setup_fun(_) ->
|
||||
{fun (_, {gun_up, _, Protocol}, _) ->
|
||||
{up, Protocol, #{}}
|
||||
end, undefined}.
|
||||
|
||||
degraded_setup(ConnPid, Msg, StateData0=#state{conns=Conns, conns_meta=ConnsMeta,
|
||||
await_up=AwaitUp}, SetupFun, SetupState0) ->
|
||||
case SetupFun(ConnPid, Msg, SetupState0) of
|
||||
Setup={setup, _SetupState} ->
|
||||
StateData = StateData0#state{conns=Conns#{ConnPid => Setup}},
|
||||
{keep_state, StateData};
|
||||
%% The Meta is different from Settings. It allows passing around
|
||||
%% Websocket or tunnel stream refs.
|
||||
{up, Protocol, Meta} ->
|
||||
Settings = #{},
|
||||
StateData = StateData0#state{
|
||||
conns=Conns#{ConnPid => {up, Protocol, Settings}},
|
||||
conns_meta=ConnsMeta#{ConnPid => Meta}
|
||||
},
|
||||
case is_degraded(StateData) of
|
||||
true -> {keep_state, StateData};
|
||||
false -> {next_state, operational, StateData#state{await_up=[]},
|
||||
[{reply, ReplyTo, ok} || ReplyTo <- AwaitUp]}
|
||||
end
|
||||
end.
|
||||
|
||||
is_degraded(#state{conns=Conns0}) ->
|
||||
Conns = maps:to_list(Conns0),
|
||||
Len = length(Conns),
|
||||
Ups = [up || {_, {up, _, _}} <- Conns],
|
||||
Len =/= length(Ups).
|
||||
|
||||
operational(Type, Event, StateData) ->
|
||||
handle_common(Type, Event, ?FUNCTION_NAME, StateData).
|
||||
|
||||
handle_common({call, From}, {checkout, _ReqOpts}, _,
|
||||
StateData=#state{conns_meta=ConnsMeta}) ->
|
||||
case find_available_connection(StateData) of
|
||||
none ->
|
||||
{keep_state_and_data, {reply, From, undefined}};
|
||||
ConnPid ->
|
||||
Meta = maps:get(ConnPid, ConnsMeta, #{}),
|
||||
{keep_state_and_data, {reply, From, {ConnPid, Meta}}}
|
||||
end;
|
||||
handle_common(info, {gun_notify, ConnPid, settings_changed, Settings}, _, StateData=#state{conns=Conns}) ->
|
||||
%% Assert that the state is correct.
|
||||
{up, http2, _} = maps:get(ConnPid, Conns),
|
||||
{keep_state, StateData#state{conns=Conns#{ConnPid => {up, http2, Settings}}}};
|
||||
handle_common(info, {gun_down, ConnPid, Protocol, _Reason, _KilledStreams}, _, StateData=#state{conns=Conns}) ->
|
||||
{up, Protocol, _} = maps:get(ConnPid, Conns),
|
||||
{next_state, degraded, StateData#state{conns=Conns#{ConnPid => down}}};
|
||||
%% @todo We do not want to reconnect automatically when the pool is dynamic.
|
||||
handle_common(info, {'DOWN', _MRef, process, ConnPid0, Reason}, _,
|
||||
StateData=#state{host=Host, port=Port, opts=Opts, table=Tid, conns=Conns0, conns_meta=ConnsMeta0}) ->
|
||||
Conns = maps:remove(ConnPid0, Conns0),
|
||||
ConnsMeta = maps:remove(ConnPid0, ConnsMeta0),
|
||||
case Reason of
|
||||
%% The process is down because of a configuration error.
|
||||
%% Do NOT attempt to reconnect, leave the pool in a degraded state.
|
||||
badarg ->
|
||||
{next_state, degraded, StateData#state{conns=Conns, conns_meta=ConnsMeta}};
|
||||
_ ->
|
||||
ConnOpts = conn_opts(Tid, Opts),
|
||||
{ok, ConnPid} = gun:open(Host, Port, ConnOpts),
|
||||
_ = monitor(process, ConnPid),
|
||||
{next_state, degraded, StateData#state{conns=Conns#{ConnPid => down}, conns_meta=ConnsMeta}}
|
||||
end;
|
||||
handle_common({call, From}, info, StateName, #state{host=Host, port=Port,
|
||||
opts=Opts, table=Tid, conns=Conns, conns_meta=ConnsMeta}) ->
|
||||
{keep_state_and_data, {reply, From, {StateName, #{
|
||||
%% @todo Not sure whether all of this should be documented. Maybe not ConnsMeta for now?
|
||||
host => Host,
|
||||
port => Port,
|
||||
opts => Opts,
|
||||
table => Tid,
|
||||
conns => Conns,
|
||||
conns_meta => ConnsMeta
|
||||
}}}};
|
||||
handle_common({call, From}, await_up, operational, _) ->
|
||||
{keep_state_and_data, {reply, From, ok}};
|
||||
handle_common({call, From}, await_up, _, StateData=#state{await_up=AwaitUp}) ->
|
||||
{keep_state, StateData#state{await_up=[From|AwaitUp]}};
|
||||
handle_common(Type, Event, StateName, StateData) ->
|
||||
logger:error("Unexpected event in state ~p of type ~p:~n~w~n~p~n",
|
||||
[StateName, Type, Event, StateData]),
|
||||
keep_state_and_data.
|
||||
|
||||
%% We go over every connection and return the first one
|
||||
%% we find that has capacity. How we determine whether
|
||||
%% capacity is available depends on the protocol. For
|
||||
%% HTTP/2 we look into the protocol settings. The
|
||||
%% current number of streams is maintained by the
|
||||
%% event handler gun_pool_events_h.
|
||||
find_available_connection(#state{table=Tid, conns=Conns}) ->
|
||||
I = lists:sort([{rand:uniform(), K} || K <- maps:keys(Conns)]),
|
||||
find_available_connection(I, Conns, Tid).
|
||||
|
||||
find_available_connection([], _, _) ->
|
||||
none;
|
||||
find_available_connection([{_, ConnPid}|I], Conns, Tid) ->
|
||||
case maps:get(ConnPid, Conns) of
|
||||
{up, Protocol, Settings} ->
|
||||
MaxStreams = max_streams(Protocol, Settings),
|
||||
CurrentStreams = case ets:lookup(Tid, ConnPid) of
|
||||
[] ->
|
||||
0;
|
||||
[{_, CS}] ->
|
||||
CS
|
||||
end,
|
||||
if
|
||||
CurrentStreams + 1 > MaxStreams ->
|
||||
find_available_connection(I, Conns, Tid);
|
||||
true ->
|
||||
ConnPid
|
||||
end;
|
||||
_ ->
|
||||
find_available_connection(I, Conns, Tid)
|
||||
end.
|
||||
|
||||
max_streams(http, _) ->
|
||||
1;
|
||||
max_streams(http2, #{max_concurrent_streams := MaxStreams}) ->
|
||||
MaxStreams;
|
||||
max_streams(http2, #{}) ->
|
||||
infinity;
|
||||
%% There are no streams or Gun is not aware of streams when
|
||||
%% the protocol is Websocket or raw.
|
||||
max_streams(ws, _) ->
|
||||
infinity;
|
||||
max_streams(raw, _) ->
|
||||
infinity.
|
||||
|
||||
terminate(Reason, StateName, #state{host=Host, port=Port, opts=Opts, await_up=AwaitUp}) ->
|
||||
gen_statem:reply([
|
||||
{reply, ReplyTo, {error, {terminate, StateName, Reason}}}
|
||||
|| ReplyTo <- AwaitUp]),
|
||||
true = ets:delete(gun_pools, gun_pools_key(Host, Port, Opts)),
|
||||
ok.
|
|
@ -0,0 +1,157 @@
|
|||
%% Copyright (c) 2021-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(gun_pool_events_h).
|
||||
|
||||
-export([init/2]).
|
||||
-export([domain_lookup_start/2]).
|
||||
-export([domain_lookup_end/2]).
|
||||
-export([connect_start/2]).
|
||||
-export([connect_end/2]).
|
||||
-export([tls_handshake_start/2]).
|
||||
-export([tls_handshake_end/2]).
|
||||
-export([request_start/2]).
|
||||
-export([request_headers/2]).
|
||||
-export([request_end/2]).
|
||||
-export([push_promise_start/2]).
|
||||
-export([push_promise_end/2]).
|
||||
-export([response_start/2]).
|
||||
-export([response_inform/2]).
|
||||
-export([response_headers/2]).
|
||||
-export([response_trailers/2]).
|
||||
-export([response_end/2]).
|
||||
-export([ws_upgrade/2]).
|
||||
-export([ws_recv_frame_start/2]).
|
||||
-export([ws_recv_frame_header/2]).
|
||||
-export([ws_recv_frame_end/2]).
|
||||
-export([ws_send_frame_start/2]).
|
||||
-export([ws_send_frame_end/2]).
|
||||
-export([protocol_changed/2]).
|
||||
-export([origin_changed/2]).
|
||||
-export([cancel/2]).
|
||||
-export([disconnect/2]).
|
||||
-export([terminate/2]).
|
||||
|
||||
init(Event, State) ->
|
||||
propagate(Event, State, ?FUNCTION_NAME).
|
||||
|
||||
domain_lookup_start(Event, State) ->
|
||||
propagate(Event, State, ?FUNCTION_NAME).
|
||||
|
||||
domain_lookup_end(Event, State) ->
|
||||
propagate(Event, State, ?FUNCTION_NAME).
|
||||
|
||||
connect_start(Event, State) ->
|
||||
propagate(Event, State, ?FUNCTION_NAME).
|
||||
|
||||
connect_end(Event, State) ->
|
||||
propagate(Event, State, ?FUNCTION_NAME).
|
||||
|
||||
tls_handshake_start(Event, State) ->
|
||||
propagate(Event, State, ?FUNCTION_NAME).
|
||||
|
||||
tls_handshake_end(Event, State) ->
|
||||
propagate(Event, State, ?FUNCTION_NAME).
|
||||
|
||||
request_start(Event=#{stream_ref := StreamRef}, State=#{table := Tid}) ->
|
||||
_ = ets:update_counter(Tid, self(), +1, {self(), 0}),
|
||||
propagate(Event, State#{
|
||||
StreamRef => {nofin, nofin}
|
||||
}, ?FUNCTION_NAME).
|
||||
|
||||
request_headers(Event, State) ->
|
||||
propagate(Event, State, ?FUNCTION_NAME).
|
||||
|
||||
request_end(Event=#{stream_ref := StreamRef}, State0=#{table := Tid}) ->
|
||||
State = case State0 of
|
||||
#{StreamRef := {nofin, fin}} ->
|
||||
_ = ets:update_counter(Tid, self(), -1),
|
||||
maps:remove(StreamRef, State0);
|
||||
#{StreamRef := {nofin, IsFin}} ->
|
||||
State0#{StreamRef => {fin, IsFin}}
|
||||
end,
|
||||
propagate(Event, State, ?FUNCTION_NAME).
|
||||
|
||||
push_promise_start(Event, State) ->
|
||||
propagate(Event, State, ?FUNCTION_NAME).
|
||||
|
||||
push_promise_end(Event, State) ->
|
||||
propagate(Event, State, ?FUNCTION_NAME).
|
||||
|
||||
response_start(Event, State) ->
|
||||
propagate(Event, State, ?FUNCTION_NAME).
|
||||
|
||||
response_inform(Event, State) ->
|
||||
propagate(Event, State, ?FUNCTION_NAME).
|
||||
|
||||
response_headers(Event, State) ->
|
||||
propagate(Event, State, ?FUNCTION_NAME).
|
||||
|
||||
response_trailers(Event, State) ->
|
||||
propagate(Event, State, ?FUNCTION_NAME).
|
||||
|
||||
response_end(Event=#{stream_ref := StreamRef}, State0=#{table := Tid}) ->
|
||||
State = case State0 of
|
||||
#{StreamRef := {fin, nofin}} ->
|
||||
_ = ets:update_counter(Tid, self(), -1),
|
||||
maps:remove(StreamRef, State0);
|
||||
#{StreamRef := {IsFin, nofin}} ->
|
||||
State0#{StreamRef => {IsFin, fin}}
|
||||
end,
|
||||
propagate(Event, State, ?FUNCTION_NAME).
|
||||
|
||||
ws_upgrade(Event, State) ->
|
||||
propagate(Event, State, ?FUNCTION_NAME).
|
||||
|
||||
ws_recv_frame_start(Event, State) ->
|
||||
propagate(Event, State, ?FUNCTION_NAME).
|
||||
|
||||
ws_recv_frame_header(Event, State) ->
|
||||
propagate(Event, State, ?FUNCTION_NAME).
|
||||
|
||||
ws_recv_frame_end(Event, State) ->
|
||||
propagate(Event, State, ?FUNCTION_NAME).
|
||||
|
||||
ws_send_frame_start(Event, State) ->
|
||||
propagate(Event, State, ?FUNCTION_NAME).
|
||||
|
||||
ws_send_frame_end(Event, State) ->
|
||||
propagate(Event, State, ?FUNCTION_NAME).
|
||||
|
||||
protocol_changed(Event, State) ->
|
||||
propagate(Event, State, ?FUNCTION_NAME).
|
||||
|
||||
origin_changed(Event, State) ->
|
||||
propagate(Event, State, ?FUNCTION_NAME).
|
||||
|
||||
cancel(Event, State) ->
|
||||
propagate(Event, State, ?FUNCTION_NAME).
|
||||
|
||||
disconnect(Event, State=#{table := Tid}) ->
|
||||
%% The ets:delete/2 call might fail when the pool has shut down.
|
||||
try
|
||||
true = ets:delete(Tid, self())
|
||||
catch _:_ ->
|
||||
ok
|
||||
end,
|
||||
propagate(Event, maps:with([event_handler, table], State), ?FUNCTION_NAME).
|
||||
|
||||
terminate(Event, State) ->
|
||||
propagate(Event, State, ?FUNCTION_NAME).
|
||||
|
||||
propagate(Event, State=#{event_handler := {Mod, ModState0}}, Fun) ->
|
||||
ModState = Mod:Fun(Event, ModState0),
|
||||
State#{event_handler => {Mod, ModState}};
|
||||
propagate(_, State, _) ->
|
||||
State.
|
|
@ -0,0 +1,37 @@
|
|||
%% Copyright (c) 2021-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(gun_pools_sup).
|
||||
-behaviour(supervisor).
|
||||
|
||||
%% API.
|
||||
-export([start_link/0]).
|
||||
|
||||
%% supervisor.
|
||||
-export([init/1]).
|
||||
|
||||
%% API.
|
||||
|
||||
-spec start_link() -> {ok, pid()}.
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
%% supervisor.
|
||||
|
||||
init([]) ->
|
||||
%% @todo Review restart strategies.
|
||||
Procs = [
|
||||
#{id => gun_pool, start => {gun_pool, start_link, []}, restart => transient}
|
||||
],
|
||||
{ok, {#{strategy => simple_one_for_one}, Procs}}.
|
|
@ -0,0 +1,72 @@
|
|||
%% Copyright (c) 2020-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(gun_protocols).
|
||||
|
||||
-export([add_stream_ref/2]).
|
||||
-export([handler/1]).
|
||||
-export([handler_and_opts/2]).
|
||||
-export([negotiated/2]).
|
||||
-export([stream_ref/1]).
|
||||
|
||||
-spec add_stream_ref(Protocol, undefined | gun:stream_ref())
|
||||
-> Protocol when Protocol :: gun:protocol().
|
||||
add_stream_ref(Protocol, undefined) ->
|
||||
Protocol;
|
||||
add_stream_ref({ProtocolName, ProtocolOpts}, StreamRef) ->
|
||||
{ProtocolName, ProtocolOpts#{stream_ref => StreamRef}};
|
||||
add_stream_ref(ProtocolName, StreamRef) ->
|
||||
{ProtocolName, #{stream_ref => StreamRef}}.
|
||||
|
||||
-spec handler(gun:protocol()) -> module().
|
||||
handler(http) -> gun_http;
|
||||
handler({http, _}) -> gun_http;
|
||||
handler(http2) -> gun_http2;
|
||||
handler({http2, _}) -> gun_http2;
|
||||
handler(raw) -> gun_raw;
|
||||
handler({raw, _}) -> gun_raw;
|
||||
handler(socks) -> gun_socks;
|
||||
handler({socks, _}) -> gun_socks;
|
||||
handler(ws) -> gun_ws;
|
||||
handler({ws, _}) -> gun_ws.
|
||||
|
||||
-spec handler_and_opts(gun:protocol(), map()) -> {module(), map()}.
|
||||
handler_and_opts({ProtocolName, ProtocolOpts}, _) ->
|
||||
{handler(ProtocolName), ProtocolOpts};
|
||||
handler_and_opts(ProtocolName, Opts) ->
|
||||
Protocol = handler(ProtocolName),
|
||||
{Protocol, maps:get(Protocol:opts_name(), Opts, #{})}.
|
||||
|
||||
-spec negotiated({ok, binary()} | {error, protocol_not_negotiated}, gun:protocols())
|
||||
-> gun:protocol().
|
||||
negotiated({ok, <<"h2">>}, Protocols) ->
|
||||
lists:foldl(fun
|
||||
(E = http2, _) -> E;
|
||||
(E = {http2, _}, _) -> E;
|
||||
(_, Acc) -> Acc
|
||||
end, http2, Protocols);
|
||||
negotiated({ok, <<"http/1.1">>}, Protocols) ->
|
||||
lists:foldl(fun
|
||||
(E = http, _) -> E;
|
||||
(E = {http, _}, _) -> E;
|
||||
(_, Acc) -> Acc
|
||||
end, http, Protocols);
|
||||
negotiated({error, protocol_not_negotiated}, [Protocol]) ->
|
||||
Protocol;
|
||||
negotiated({error, protocol_not_negotiated}, _) ->
|
||||
http.
|
||||
|
||||
-spec stream_ref(gun:protocol()) -> undefined | gun:stream_ref().
|
||||
stream_ref({_, ProtocolOpts}) -> maps:get(stream_ref, ProtocolOpts, undefined);
|
||||
stream_ref(_) -> undefined.
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,29 @@
|
|||
%% Copyright (c) 2020-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(gun_public_suffix).
|
||||
-compile(no_type_opt). %% Until at least OTP-23.
|
||||
|
||||
-export([match/1]).
|
||||
|
||||
-spec match(binary()) -> boolean().
|
||||
match(Domain) ->
|
||||
Subdomains = string:split(Domain, <<".">>, all),
|
||||
m(Subdomains).
|
||||
|
||||
%% GENERATED_M
|
||||
m(_) -> false.
|
||||
|
||||
%% GENERATED_E
|
||||
e(_) -> true.
|
|
@ -0,0 +1,97 @@
|
|||
%% Copyright (c) 2019-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(gun_raw).
|
||||
|
||||
-export([check_options/1]).
|
||||
-export([name/0]).
|
||||
-export([opts_name/0]).
|
||||
-export([has_keepalive/0]).
|
||||
-export([init/4]).
|
||||
-export([handle/5]).
|
||||
-export([update_flow/4]).
|
||||
-export([closing/4]).
|
||||
-export([close/4]).
|
||||
-export([data/7]).
|
||||
-export([down/1]).
|
||||
|
||||
-record(raw_state, {
|
||||
ref :: undefined | gun:stream_ref(),
|
||||
reply_to :: pid(),
|
||||
socket :: inet:socket() | ssl:sslsocket(),
|
||||
transport :: module(),
|
||||
flow :: integer() | infinity
|
||||
}).
|
||||
|
||||
check_options(Opts) ->
|
||||
do_check_options(maps:to_list(Opts)).
|
||||
|
||||
do_check_options([]) ->
|
||||
ok;
|
||||
do_check_options([{flow, Flow}|Opts]) when is_integer(Flow); Flow == infinity ->
|
||||
do_check_options(Opts);
|
||||
do_check_options([Opt|_]) ->
|
||||
{error, {options, {raw, Opt}}}.
|
||||
|
||||
name() -> raw.
|
||||
opts_name() -> raw_opts.
|
||||
has_keepalive() -> false.
|
||||
|
||||
init(ReplyTo, Socket, Transport, Opts) ->
|
||||
StreamRef = maps:get(stream_ref, Opts, undefined),
|
||||
InitialFlow = maps:get(flow, Opts, infinity),
|
||||
{ok, connected_data_only, #raw_state{ref=StreamRef, reply_to=ReplyTo,
|
||||
socket=Socket, transport=Transport, flow=InitialFlow}}.
|
||||
|
||||
handle(Data, State=#raw_state{ref=StreamRef, reply_to=ReplyTo, flow=Flow0},
|
||||
CookieStore, _, EvHandlerState) ->
|
||||
%% When we take over the entire connection there is no stream reference.
|
||||
ReplyTo ! {gun_data, self(), StreamRef, nofin, Data},
|
||||
Flow = case Flow0 of
|
||||
infinity -> infinity;
|
||||
_ -> Flow0 - 1
|
||||
end,
|
||||
{[
|
||||
{state, State#raw_state{flow=Flow}},
|
||||
{active, Flow > 0}
|
||||
], CookieStore, EvHandlerState}.
|
||||
|
||||
update_flow(State=#raw_state{flow=Flow0}, _ReplyTo, _StreamRef, Inc) ->
|
||||
Flow = case Flow0 of
|
||||
infinity -> infinity;
|
||||
_ -> Flow0 + Inc
|
||||
end,
|
||||
[
|
||||
{state, State#raw_state{flow=Flow}},
|
||||
{active, Flow > 0}
|
||||
].
|
||||
|
||||
%% We can always close immediately.
|
||||
closing(_, _, _, EvHandlerState) ->
|
||||
{close, EvHandlerState}.
|
||||
|
||||
close(_, _, _, EvHandlerState) ->
|
||||
EvHandlerState.
|
||||
|
||||
%% @todo Initiate closing on IsFin=fin.
|
||||
data(#raw_state{ref=StreamRef, socket=Socket, transport=Transport}, StreamRef,
|
||||
_ReplyTo, _IsFin, Data, _EvHandler, EvHandlerState) ->
|
||||
case Transport:send(Socket, Data) of
|
||||
ok -> {[], EvHandlerState};
|
||||
Error={error, _} -> {Error, EvHandlerState}
|
||||
end.
|
||||
|
||||
%% raw has no concept of streams.
|
||||
down(_) ->
|
||||
[].
|
|
@ -0,0 +1,207 @@
|
|||
%% Copyright (c) 2019-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(gun_socks).
|
||||
|
||||
-export([check_options/1]).
|
||||
-export([name/0]).
|
||||
-export([opts_name/0]).
|
||||
-export([has_keepalive/0]).
|
||||
-export([init/4]).
|
||||
-export([switch_transport/3]).
|
||||
-export([handle/5]).
|
||||
-export([closing/4]).
|
||||
-export([close/4]).
|
||||
%% @todo down
|
||||
|
||||
-record(socks_state, {
|
||||
ref :: undefined | gun:stream_ref(),
|
||||
reply_to :: pid(),
|
||||
socket :: inet:socket() | ssl:sslsocket(),
|
||||
transport :: module(),
|
||||
opts = #{} :: gun:socks_opts(),
|
||||
%% We only support version 5 at the moment.
|
||||
version :: 5,
|
||||
status :: auth_method_select | auth_username_password | connect
|
||||
}).
|
||||
|
||||
check_options(Opts=#{host := _, port := _}) ->
|
||||
do_check_options(maps:to_list(maps:without([host, port], Opts)));
|
||||
%% Host and port are not optional.
|
||||
check_options(#{host := _}) ->
|
||||
{error, options, {socks, port}};
|
||||
check_options(#{}) ->
|
||||
{error, options, {socks, host}}.
|
||||
|
||||
do_check_options([]) ->
|
||||
ok;
|
||||
do_check_options([Opt={auth, L}|Opts]) ->
|
||||
case check_auth_opt(L) of
|
||||
ok -> do_check_options(Opts);
|
||||
error -> {error, {options, {socks, Opt}}}
|
||||
end;
|
||||
%% @todo Proper protocols check.
|
||||
do_check_options([{protocols, P}|Opts]) when is_list(P) ->
|
||||
do_check_options(Opts);
|
||||
do_check_options([{tls_handshake_timeout, infinity}|Opts]) ->
|
||||
do_check_options(Opts);
|
||||
do_check_options([{tls_handshake_timeout, T}|Opts]) when is_integer(T), T >= 0 ->
|
||||
do_check_options(Opts);
|
||||
do_check_options([{tls_opts, L}|Opts]) when is_list(L) ->
|
||||
do_check_options(Opts);
|
||||
do_check_options([{transport, T}|Opts]) when T =:= tcp; T =:= tls ->
|
||||
do_check_options(Opts);
|
||||
do_check_options([{version, 5}|Opts]) ->
|
||||
do_check_options(Opts);
|
||||
do_check_options([Opt|_]) ->
|
||||
{error, {options, {socks, Opt}}}.
|
||||
|
||||
check_auth_opt(Methods) ->
|
||||
%% Methods must not appear more than once, and they
|
||||
%% must be one of none or {username_password, binary(), binary()}.
|
||||
Check = lists:usort([case M of
|
||||
none -> ok;
|
||||
{username_password, U, P} when is_binary(U), is_binary(P) -> ok
|
||||
end || M <- Methods]),
|
||||
case {length(Methods) =:= length(Check), lists:usort(Check)} of
|
||||
{true, []} -> ok;
|
||||
{true, [ok]} -> ok;
|
||||
_ -> error
|
||||
end.
|
||||
|
||||
name() -> socks.
|
||||
opts_name() -> socks_opts.
|
||||
has_keepalive() -> false.
|
||||
|
||||
init(ReplyTo, Socket, Transport, Opts) ->
|
||||
StreamRef = maps:get(stream_ref, Opts, undefined),
|
||||
5 = Version = maps:get(version, Opts, 5),
|
||||
Auth = maps:get(auth, Opts, [none]),
|
||||
Methods = <<case A of
|
||||
{username_password, _, _} -> <<2>>;
|
||||
none -> <<0>>
|
||||
end || A <- Auth>>,
|
||||
case Transport:send(Socket, [<<5, (length(Auth))>>, Methods]) of
|
||||
ok ->
|
||||
{ok, connected_no_input, #socks_state{ref=StreamRef, reply_to=ReplyTo,
|
||||
socket=Socket, transport=Transport,
|
||||
opts=Opts, version=Version, status=auth_method_select}};
|
||||
Error={error, _Reason} ->
|
||||
Error
|
||||
end.
|
||||
|
||||
switch_transport(Transport, Socket, State) ->
|
||||
State#socks_state{socket=Socket, transport=Transport}.
|
||||
|
||||
handle(Data, State, CookieStore, _, EvHandlerState) ->
|
||||
{handle(Data, State), CookieStore, EvHandlerState}.
|
||||
|
||||
%% No authentication.
|
||||
handle(<<5, 0>>, State=#socks_state{version=5, status=auth_method_select}) ->
|
||||
case send_socks5_connect(State) of
|
||||
ok -> {state, State#socks_state{status=connect}};
|
||||
Error={error, _} -> Error
|
||||
end;
|
||||
%% Username/password authentication.
|
||||
handle(<<5, 2>>, State=#socks_state{socket=Socket, transport=Transport, opts=#{auth := AuthMethods},
|
||||
version=5, status=auth_method_select}) ->
|
||||
[{username_password, Username, Password}] = [Method || Method <- AuthMethods],
|
||||
ULen = byte_size(Username),
|
||||
PLen = byte_size(Password),
|
||||
case Transport:send(Socket, <<1, ULen, Username/binary, PLen, Password/binary>>) of
|
||||
ok -> {state, State#socks_state{status=auth_username_password}};
|
||||
Error={error, _} -> Error
|
||||
end;
|
||||
%% Username/password authentication successful.
|
||||
handle(<<1, 0>>, State=#socks_state{version=5, status=auth_username_password}) ->
|
||||
case send_socks5_connect(State) of
|
||||
ok -> {state, State#socks_state{status=connect}};
|
||||
Error={error, _} -> Error
|
||||
end;
|
||||
%% Username/password authentication error.
|
||||
handle(<<1, _>>, #socks_state{version=5, status=auth_username_password}) ->
|
||||
{error, {socks5, username_password_auth_failure}};
|
||||
%% Connect reply.
|
||||
handle(<<5, 0, 0, Rest0/bits>>, #socks_state{ref=StreamRef, reply_to=ReplyTo, opts=Opts,
|
||||
version=5, status=connect}) ->
|
||||
%% @todo What to do with BoundAddr and BoundPort? Add as metadata to origin info?
|
||||
{_BoundAddr, _BoundPort} = case Rest0 of
|
||||
%% @todo Seen a server with <<1, 0:48>>.
|
||||
<<1, A, B, C, D, Port:16>> ->
|
||||
{{A, B, C, D}, Port};
|
||||
<<3, Len, Host:Len/binary, Port:16>> ->
|
||||
%% We convert to list to get an inet:hostname().
|
||||
{unicode:characters_to_list(Host), Port};
|
||||
<<4, A:16, B:16, C:16, D:16, E:16, F:16, G:16, H:16, Port:16>> ->
|
||||
{{A, B, C, D, E, F, G, H}, Port}
|
||||
end,
|
||||
%% @todo Maybe an event indicating success.
|
||||
#{host := NewHost, port := NewPort} = Opts,
|
||||
%% There is no origin scheme when not using HTTP but we act as if
|
||||
%% there is and simply correct the value in the info functions.
|
||||
case Opts of
|
||||
#{transport := tls} ->
|
||||
HandshakeEvent0 = #{
|
||||
reply_to => ReplyTo,
|
||||
tls_opts => maps:get(tls_opts, Opts, []),
|
||||
timeout => maps:get(tls_handshake_timeout, Opts, infinity)
|
||||
},
|
||||
HandshakeEvent = case StreamRef of
|
||||
undefined -> HandshakeEvent0;
|
||||
_ -> HandshakeEvent0#{stream_ref => StreamRef}
|
||||
end,
|
||||
[{origin, <<"https">>, NewHost, NewPort, socks5},
|
||||
{tls_handshake, HandshakeEvent, maps:get(protocols, Opts, [http2, http]), ReplyTo}];
|
||||
_ ->
|
||||
[NewProtocol0] = maps:get(protocols, Opts, [http]),
|
||||
NewProtocol = gun_protocols:add_stream_ref(NewProtocol0, StreamRef),
|
||||
Protocol = gun_protocols:handler(NewProtocol),
|
||||
ReplyTo ! {gun_tunnel_up, self(), StreamRef, Protocol:name()},
|
||||
[{origin, <<"http">>, NewHost, NewPort, socks5},
|
||||
{switch_protocol, NewProtocol, ReplyTo}]
|
||||
end;
|
||||
handle(<<5, Error, _/bits>>, #socks_state{version=5, status=connect}) ->
|
||||
Reason = case Error of
|
||||
1 -> general_socks_server_failure;
|
||||
2 -> connection_not_allowed_by_ruleset;
|
||||
3 -> network_unreachable;
|
||||
4 -> host_unreachable;
|
||||
5 -> connection_refused;
|
||||
6 -> ttl_expired;
|
||||
7 -> command_not_supported;
|
||||
8 -> address_type_not_supported;
|
||||
_ -> {unknown_error, Error}
|
||||
end,
|
||||
{error, {socks5, Reason}}.
|
||||
|
||||
send_socks5_connect(#socks_state{socket=Socket, transport=Transport, opts=Opts}) ->
|
||||
ATypeAndDestAddr = case maps:get(host, Opts) of
|
||||
{A, B, C, D} -> <<1, A, B, C, D>>;
|
||||
{A, B, C, D, E, F, G, H} -> <<4, A:16, B:16, C:16, D:16, E:16, F:16, G:16, H:16>>;
|
||||
Host when is_atom(Host) ->
|
||||
DestAddr0 = atom_to_binary(Host, utf8),
|
||||
<<3, (byte_size(DestAddr0)), DestAddr0/binary>>;
|
||||
Host ->
|
||||
DestAddr0 = unicode:characters_to_binary(Host, utf8),
|
||||
<<3, (byte_size(DestAddr0)), DestAddr0/binary>>
|
||||
end,
|
||||
DestPort = maps:get(port, Opts),
|
||||
Transport:send(Socket, <<5, 1, 0, ATypeAndDestAddr/binary, DestPort:16>>).
|
||||
|
||||
%% We can always close immediately.
|
||||
closing(_, _, _, EvHandlerState) ->
|
||||
{close, EvHandlerState}.
|
||||
|
||||
close(_, _, _, EvHandlerState) ->
|
||||
EvHandlerState.
|
|
@ -0,0 +1,63 @@
|
|||
%% Copyright (c) 2017-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(gun_sse_h).
|
||||
-behavior(gun_content_handler).
|
||||
|
||||
-export([init/5]).
|
||||
-export([handle/3]).
|
||||
|
||||
-record(state, {
|
||||
reply_to :: pid(),
|
||||
stream_ref :: reference(),
|
||||
sse_state :: cow_sse:state()
|
||||
}).
|
||||
|
||||
%% @todo In the future we want to allow different media types.
|
||||
|
||||
-spec init(pid(), reference(), _, cow_http:headers(), _)
|
||||
-> {ok, #state{}} | disable.
|
||||
init(ReplyTo, StreamRef, _, Headers, _) ->
|
||||
case lists:keyfind(<<"content-type">>, 1, Headers) of
|
||||
{_, ContentType} ->
|
||||
case cow_http_hd:parse_content_type(ContentType) of
|
||||
{<<"text">>, <<"event-stream">>, _Ignored} ->
|
||||
{ok, #state{reply_to=ReplyTo, stream_ref=StreamRef,
|
||||
sse_state=cow_sse:init()}};
|
||||
_ ->
|
||||
disable
|
||||
end;
|
||||
_ ->
|
||||
disable
|
||||
end.
|
||||
|
||||
-spec handle(_, binary(), State) -> {done, non_neg_integer(), State} when State::#state{}.
|
||||
handle(IsFin, Data, State) ->
|
||||
handle(IsFin, Data, State, 0).
|
||||
|
||||
handle(IsFin, Data, State=#state{reply_to=ReplyTo, stream_ref=StreamRef, sse_state=SSE0}, Flow) ->
|
||||
case cow_sse:parse(Data, SSE0) of
|
||||
{event, Event, SSE} ->
|
||||
ReplyTo ! {gun_sse, self(), StreamRef, Event},
|
||||
handle(IsFin, <<>>, State#state{sse_state=SSE}, Flow + 1);
|
||||
{more, SSE} ->
|
||||
Inc = case IsFin of
|
||||
fin ->
|
||||
ReplyTo ! {gun_sse, self(), StreamRef, fin},
|
||||
1;
|
||||
_ ->
|
||||
0
|
||||
end,
|
||||
{done, Flow + Inc, State#state{sse_state=SSE}}
|
||||
end.
|
|
@ -0,0 +1,37 @@
|
|||
%% Copyright (c) 2013-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(gun_sup).
|
||||
-behaviour(supervisor).
|
||||
|
||||
%% API.
|
||||
-export([start_link/0]).
|
||||
|
||||
%% supervisor.
|
||||
-export([init/1]).
|
||||
|
||||
%% API.
|
||||
|
||||
-spec start_link() -> {ok, pid()}.
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
%% supervisor.
|
||||
|
||||
init([]) ->
|
||||
Procs = [
|
||||
#{id => gun_conns_sup, start => {gun_conns_sup, start_link, []}, type => supervisor},
|
||||
#{id => gun_pools_sup, start => {gun_pools_sup, start_link, []}, type => supervisor}
|
||||
],
|
||||
{ok, {#{}, Procs}}.
|
|
@ -0,0 +1,112 @@
|
|||
%% Copyright (c) 2011-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(gun_tcp).
|
||||
|
||||
-export([name/0]).
|
||||
-export([messages/0]).
|
||||
-export([domain_lookup/4]).
|
||||
-export([connect/2]).
|
||||
-export([send/2]).
|
||||
-export([setopts/2]).
|
||||
-export([sockname/1]).
|
||||
-export([close/1]).
|
||||
|
||||
-type lookup_info() :: #{
|
||||
ip_addresses := [inet:ip_address()],
|
||||
port := inet:port_number(),
|
||||
tcp_module := module(),
|
||||
tcp_opts := [gen_tcp:connect_option()]
|
||||
}.
|
||||
-export_type([lookup_info/0]).
|
||||
|
||||
name() -> tcp.
|
||||
|
||||
messages() -> {tcp, tcp_closed, tcp_error}.
|
||||
|
||||
%% The functions domain_lookup/4 and connect/2 are very similar
|
||||
%% to gen_tcp:connect/4 except the logic is split in order to
|
||||
%% be able to trigger events between the domain lookup step
|
||||
%% and the actual connect step.
|
||||
|
||||
-spec domain_lookup(inet:ip_address() | inet:hostname(),
|
||||
inet:port_number(), [gen_tcp:connect_option()], timeout())
|
||||
-> {ok, lookup_info()} | {error, atom()}.
|
||||
domain_lookup(Address, Port0, Opts0, Timeout) ->
|
||||
{Mod, Opts} = inet:tcp_module(Opts0, Address),
|
||||
Timer = inet:start_timer(Timeout),
|
||||
try Mod:getaddrs(Address, Timer) of
|
||||
{ok, IPs} ->
|
||||
case Mod:getserv(Port0) of
|
||||
{ok, Port} ->
|
||||
{ok, #{
|
||||
ip_addresses => IPs,
|
||||
port => Port,
|
||||
tcp_module => Mod,
|
||||
tcp_opts => Opts ++ [binary, {active, false}, {packet, raw}]
|
||||
}};
|
||||
Error ->
|
||||
maybe_exit(Error)
|
||||
end;
|
||||
Error ->
|
||||
maybe_exit(Error)
|
||||
after
|
||||
_ = inet:stop_timer(Timer)
|
||||
end.
|
||||
|
||||
-spec connect(lookup_info(), timeout())
|
||||
-> {ok, inet:socket()} | {error, atom()}.
|
||||
connect(#{ip_addresses := IPs, port := Port, tcp_module := Mod, tcp_opts := Opts}, Timeout) ->
|
||||
Timer = inet:start_timer(Timeout),
|
||||
Res = try
|
||||
try_connect(IPs, Port, Opts, Timer, Mod, {error, einval})
|
||||
after
|
||||
_ = inet:stop_timer(Timer)
|
||||
end,
|
||||
case Res of
|
||||
{ok, S} -> {ok, S};
|
||||
Error -> maybe_exit(Error)
|
||||
end.
|
||||
|
||||
try_connect([IP|IPs], Port, Opts, Timer, Mod, _) ->
|
||||
Timeout = inet:timeout(Timer),
|
||||
case Mod:connect(IP, Port, Opts, Timeout) of
|
||||
{ok, S} -> {ok, S};
|
||||
{error, einval} -> {error, einval};
|
||||
{error, timeout} -> {error, timeout};
|
||||
Error -> try_connect(IPs, Port, Opts, Timer, Mod, Error)
|
||||
end;
|
||||
try_connect([], _, _, _, _, Error) ->
|
||||
Error.
|
||||
|
||||
maybe_exit({error, einval}) -> exit(badarg);
|
||||
maybe_exit({error, eaddrnotavail}) -> exit(badarg);
|
||||
maybe_exit(Error) -> Error.
|
||||
|
||||
-spec send(inet:socket(), iodata()) -> ok | {error, atom()}.
|
||||
send(Socket, Packet) ->
|
||||
gen_tcp:send(Socket, Packet).
|
||||
|
||||
-spec setopts(inet:socket(), list()) -> ok | {error, atom()}.
|
||||
setopts(Socket, Opts) ->
|
||||
inet:setopts(Socket, Opts).
|
||||
|
||||
-spec sockname(inet:socket())
|
||||
-> {ok, {inet:ip_address(), inet:port_number()}} | {error, atom()}.
|
||||
sockname(Socket) ->
|
||||
inet:sockname(Socket).
|
||||
|
||||
-spec close(inet:socket()) -> ok.
|
||||
close(Socket) ->
|
||||
gen_tcp:close(Socket).
|
|
@ -0,0 +1,70 @@
|
|||
%% Copyright (c) 2020-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(gun_tcp_proxy).
|
||||
|
||||
-export([name/0]).
|
||||
-export([messages/0]).
|
||||
-export([connect/3]).
|
||||
-export([connect/4]).
|
||||
-export([send/2]).
|
||||
-export([setopts/2]).
|
||||
-export([sockname/1]).
|
||||
-export([close/1]).
|
||||
|
||||
-type socket() :: #{
|
||||
%% The pid of the Gun connection.
|
||||
gun_pid := pid(),
|
||||
|
||||
%% The pid of the process that gets replies for this tunnel.
|
||||
reply_to := pid(),
|
||||
|
||||
%% The full stream reference for this tunnel.
|
||||
stream_ref := gun:stream_ref()
|
||||
}.
|
||||
|
||||
name() -> tcp_proxy.
|
||||
|
||||
messages() -> {tcp_proxy, tcp_proxy_closed, tcp_proxy_error}.
|
||||
|
||||
-spec connect(_, _, _) -> no_return().
|
||||
connect(_, _, _) ->
|
||||
error(not_implemented).
|
||||
|
||||
-spec connect(_, _, _, _) -> no_return().
|
||||
connect(_, _, _, _) ->
|
||||
error(not_implemented).
|
||||
|
||||
-spec send(socket(), iodata()) -> ok.
|
||||
send(#{gun_pid := GunPid, reply_to := ReplyTo, stream_ref := StreamRef,
|
||||
handle_continue_stream_ref := ContinueStreamRef}, Data) ->
|
||||
GunPid ! {handle_continue, ContinueStreamRef, {data, ReplyTo, StreamRef, nofin, Data}},
|
||||
ok;
|
||||
send(#{reply_to := ReplyTo, stream_ref := StreamRef}, Data) ->
|
||||
gen_statem:cast(self(), {data, ReplyTo, StreamRef, nofin, Data}).
|
||||
|
||||
-spec setopts(_, _) -> no_return().
|
||||
setopts(#{handle_continue_stream_ref := _}, _) ->
|
||||
%% We send messages automatically regardless of active mode.
|
||||
ok;
|
||||
setopts(_, _) ->
|
||||
error(not_implemented).
|
||||
|
||||
-spec sockname(_) -> no_return().
|
||||
sockname(_) ->
|
||||
error(not_implemented).
|
||||
|
||||
-spec close(socket()) -> ok.
|
||||
close(_) ->
|
||||
ok.
|
|
@ -0,0 +1,49 @@
|
|||
%% Copyright (c) 2011-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(gun_tls).
|
||||
|
||||
-export([name/0]).
|
||||
-export([messages/0]).
|
||||
-export([connect/3]).
|
||||
-export([send/2]).
|
||||
-export([setopts/2]).
|
||||
-export([sockname/1]).
|
||||
-export([close/1]).
|
||||
|
||||
name() -> tls.
|
||||
|
||||
messages() -> {ssl, ssl_closed, ssl_error}.
|
||||
|
||||
-spec connect(inet:socket(), any(), timeout())
|
||||
-> {ok, ssl:sslsocket()} | {error, atom()}.
|
||||
connect(Socket, Opts, Timeout) ->
|
||||
ssl:connect(Socket, Opts, Timeout).
|
||||
|
||||
-spec send(ssl:sslsocket(), iodata()) -> ok | {error, atom()}.
|
||||
send(Socket, Packet) ->
|
||||
ssl:send(Socket, Packet).
|
||||
|
||||
-spec setopts(ssl:sslsocket(), list()) -> ok | {error, atom()}.
|
||||
setopts(Socket, Opts) ->
|
||||
ssl:setopts(Socket, Opts).
|
||||
|
||||
-spec sockname(ssl:sslsocket())
|
||||
-> {ok, {inet:ip_address(), inet:port_number()}} | {error, atom()}.
|
||||
sockname(Socket) ->
|
||||
ssl:sockname(Socket).
|
||||
|
||||
-spec close(ssl:sslsocket()) -> ok.
|
||||
close(Socket) ->
|
||||
ssl:close(Socket).
|
|
@ -0,0 +1,492 @@
|
|||
%% Copyright (c) 2019-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
%% Intermediary process for proxying TLS connections. This process
|
||||
%% is started by Gun when CONNECT request is issued and stays alive
|
||||
%% while the proxy connection exists.
|
||||
%%
|
||||
%% Data comes in and out of this process, which is responsible for
|
||||
%% passing incoming/outgoing data through the fake ssl socket process
|
||||
%% it created to perform the decoding/encoding operations.
|
||||
%%
|
||||
%% Normal scenario:
|
||||
%% Gun process -> TLS socket
|
||||
%%
|
||||
%% One proxy socket scenario:
|
||||
%% Gun process -> gun_tls_proxy (proxied socket) -> TLS socket
|
||||
%%
|
||||
%% N proxy socket scenario:
|
||||
%% Gun process -> gun_tls_proxy -> ... -> gun_tls_proxy -> TLS socket
|
||||
%%
|
||||
%% The difficult part is the connection. Because ssl:connect/4 does
|
||||
%% not return until the connection is setup, and we need to send and
|
||||
%% receive data for the TLS handshake, we need a temporary process
|
||||
%% to call this function, and communicate with it. Once the connection
|
||||
%% is setup the temporary process is gone and things go back to normal.
|
||||
%% The send operations also require a temporary process in order to avoid
|
||||
%% blocking because the same gun_tls_proxy process must send twice (first
|
||||
%% to the fake ssl socket and then to the outgoing socket).
|
||||
|
||||
-module(gun_tls_proxy).
|
||||
-behaviour(gen_statem).
|
||||
|
||||
%% Gun-specific interface.
|
||||
-export([start_link/7]).
|
||||
|
||||
%% gun_tls_proxy_cb interface.
|
||||
-export([cb_controlling_process/2]).
|
||||
-export([cb_send/2]).
|
||||
-export([cb_setopts/2]).
|
||||
|
||||
%% Gun transport.
|
||||
-export([name/0]).
|
||||
-export([messages/0]).
|
||||
-export([connect/3]).
|
||||
-export([connect/4]).
|
||||
-export([send/2]).
|
||||
-export([setopts/2]).
|
||||
-export([sockname/1]).
|
||||
-export([close/1]).
|
||||
|
||||
%% Internals.
|
||||
-export([callback_mode/0]).
|
||||
-export([init/1]).
|
||||
-export([connect_proc/5]).
|
||||
-export([not_connected/3]).
|
||||
-export([connected/3]).
|
||||
|
||||
-record(state, {
|
||||
%% The pid of the owner process. This is where we send active messages.
|
||||
owner_pid :: pid(),
|
||||
owner_active = false :: false | once | true | pos_integer(),
|
||||
owner_buffer = <<>> :: binary(),
|
||||
|
||||
%% The host/port the fake ssl socket thinks it's connected to.
|
||||
host :: inet:ip_address() | inet:hostname(),
|
||||
port :: inet:port_number(),
|
||||
|
||||
%% The fake ssl socket we are using in the proxy.
|
||||
proxy_socket :: any(),
|
||||
proxy_pid :: pid(),
|
||||
proxy_active = false :: false | once | true | pos_integer(),
|
||||
proxy_buffer = <<>> :: binary(),
|
||||
|
||||
%% The socket or proxy process we are sending to.
|
||||
out_socket :: any(),
|
||||
out_transport :: module(),
|
||||
out_messages :: {atom(), atom(), atom()}, %% @todo Missing passive.
|
||||
|
||||
%% Extra information to be sent to the owner when the handshake completes.
|
||||
extra :: any()
|
||||
}).
|
||||
|
||||
%-define(DEBUG_PROXY,1).
|
||||
-ifdef(DEBUG_PROXY).
|
||||
-define(DEBUG_LOG(Format, Args),
|
||||
io:format(user, "(~p) ~p:~p/~p:" ++ Format ++ "~n",
|
||||
[self(), ?MODULE, ?FUNCTION_NAME, ?FUNCTION_ARITY] ++ Args)).
|
||||
-else.
|
||||
-define(DEBUG_LOG(Format, Args), _ = Format, _ = Args, ok).
|
||||
-endif.
|
||||
|
||||
%% Gun-specific interface.
|
||||
|
||||
start_link(Host, Port, Opts, Timeout, OutSocket, OutTransport, Extra) ->
|
||||
?DEBUG_LOG("host ~0p port ~0p opts ~0p timeout ~0p out_socket ~0p out_transport ~0p",
|
||||
[Host, Port, Opts, Timeout, OutSocket, OutTransport]),
|
||||
case gen_statem:start_link(?MODULE,
|
||||
{self(), Host, Port, Opts, Timeout, OutSocket, OutTransport, Extra},
|
||||
[]) of
|
||||
{ok, Pid} when is_port(OutSocket) ->
|
||||
ok = gen_tcp:controlling_process(OutSocket, Pid),
|
||||
{ok, Pid};
|
||||
{ok, Pid} when is_map(OutSocket) ->
|
||||
{ok, Pid};
|
||||
{ok, Pid} when not is_pid(OutSocket) ->
|
||||
ok = ssl:controlling_process(OutSocket, Pid),
|
||||
{ok, Pid};
|
||||
Other ->
|
||||
Other
|
||||
end.
|
||||
|
||||
%% gun_tls_proxy_cb interface.
|
||||
|
||||
cb_controlling_process(Pid, ControllingPid) ->
|
||||
?DEBUG_LOG("pid ~0p controlling_pid ~0p", [Pid, ControllingPid]),
|
||||
gen_statem:cast(Pid, {?FUNCTION_NAME, ControllingPid}).
|
||||
|
||||
cb_send(Pid, Data) ->
|
||||
?DEBUG_LOG("pid ~0p data ~0p", [Pid, Data]),
|
||||
try
|
||||
gen_statem:call(Pid, {?FUNCTION_NAME, Data})
|
||||
catch
|
||||
exit:{noproc, _} ->
|
||||
{error, closed}
|
||||
end.
|
||||
|
||||
cb_setopts(Pid, Opts) ->
|
||||
?DEBUG_LOG("pid ~0p opts ~0p", [Pid, Opts]),
|
||||
try
|
||||
gen_statem:call(Pid, {?FUNCTION_NAME, Opts})
|
||||
catch
|
||||
exit:{noproc, _} ->
|
||||
{error, einval}
|
||||
end.
|
||||
|
||||
%% Transport.
|
||||
|
||||
name() -> tls_proxy.
|
||||
|
||||
messages() -> {tls_proxy, tls_proxy_closed, tls_proxy_error}.
|
||||
|
||||
-spec connect(_, _, _) -> no_return().
|
||||
connect(_, _, _) ->
|
||||
error(not_implemented).
|
||||
|
||||
-spec connect(_, _, _, _) -> no_return().
|
||||
connect(_, _, _, _) ->
|
||||
error(not_implemented).
|
||||
|
||||
-spec send(pid(), iodata()) -> ok | {error, atom()}.
|
||||
send(Pid, Data) ->
|
||||
?DEBUG_LOG("pid ~0p data ~0p", [Pid, Data]),
|
||||
gen_statem:call(Pid, {?FUNCTION_NAME, Data}).
|
||||
|
||||
-spec setopts(pid(), list()) -> ok.
|
||||
setopts(Pid, Opts) ->
|
||||
?DEBUG_LOG("pid ~0p opts ~0p", [Pid, Opts]),
|
||||
gen_statem:cast(Pid, {?FUNCTION_NAME, Opts}).
|
||||
|
||||
-spec sockname(pid())
|
||||
-> {ok, {inet:ip_address(), inet:port_number()}} | {error, atom()}.
|
||||
sockname(Pid) ->
|
||||
?DEBUG_LOG("pid ~0p", [Pid]),
|
||||
gen_statem:call(Pid, ?FUNCTION_NAME).
|
||||
|
||||
-spec close(pid()) -> ok.
|
||||
close(Pid) ->
|
||||
?DEBUG_LOG("pid ~0p", [Pid]),
|
||||
try
|
||||
gen_statem:call(Pid, ?FUNCTION_NAME)
|
||||
catch
|
||||
%% May happen for example when the handshake fails.
|
||||
exit:{noproc, _} ->
|
||||
ok
|
||||
end.
|
||||
|
||||
%% gen_statem.
|
||||
|
||||
callback_mode() -> state_functions.
|
||||
|
||||
init({OwnerPid, Host, Port, Opts, Timeout, OutSocket, OutTransport, Extra}) ->
|
||||
if
|
||||
is_pid(OutSocket) ->
|
||||
gen_statem:cast(OutSocket, {set_owner, self()});
|
||||
true ->
|
||||
ok
|
||||
end,
|
||||
Messages = case OutTransport of
|
||||
gen_tcp -> {tcp, tcp_closed, tcp_error};
|
||||
ssl -> {ssl, ssl_closed, ssl_error};
|
||||
_ -> OutTransport:messages()
|
||||
end,
|
||||
ProxyPid = spawn_link(?MODULE, connect_proc, [self(), Host, Port, Opts, Timeout]),
|
||||
?DEBUG_LOG("owner_pid ~0p host ~0p port ~0p opts ~0p timeout ~0p"
|
||||
" out_socket ~0p out_transport ~0p proxy_pid ~0p",
|
||||
[OwnerPid, Host, Port, Opts, Timeout, OutSocket, OutTransport, ProxyPid]),
|
||||
{ok, not_connected, #state{owner_pid=OwnerPid, host=Host, port=Port, proxy_pid=ProxyPid,
|
||||
out_socket=OutSocket, out_transport=OutTransport, out_messages=Messages,
|
||||
extra=Extra}}.
|
||||
|
||||
connect_proc(ProxyPid, Host, Port, Opts, Timeout) ->
|
||||
?DEBUG_LOG("proxy_pid ~0p host ~0p port ~0p opts ~0p timeout ~0p",
|
||||
[ProxyPid, Host, Port, Opts, Timeout]),
|
||||
_ = case ssl:connect(Host, Port, [
|
||||
{active, false}, binary,
|
||||
{cb_info, {gun_tls_proxy_cb, tls_proxy, tls_proxy_closed, tls_proxy_error}},
|
||||
{?MODULE, ProxyPid}
|
||||
|Opts], Timeout) of
|
||||
{ok, Socket} ->
|
||||
?DEBUG_LOG("socket ~0p", [Socket]),
|
||||
ok = ssl:controlling_process(Socket, ProxyPid),
|
||||
gen_statem:cast(ProxyPid, {?FUNCTION_NAME, {ok, Socket}});
|
||||
Error ->
|
||||
?DEBUG_LOG("error ~0p", [Error]),
|
||||
gen_statem:cast(ProxyPid, {?FUNCTION_NAME, Error})
|
||||
end,
|
||||
ok.
|
||||
|
||||
%% Postpone events that require the proxy socket to be up.
|
||||
not_connected({call, _}, Msg={send, _}, State) ->
|
||||
?DEBUG_LOG("postpone ~0p state ~0p", [Msg, State]),
|
||||
{keep_state_and_data, postpone};
|
||||
not_connected(cast, Msg={setopts, _}, State) ->
|
||||
?DEBUG_LOG("postpone ~0p state ~0p", [Msg, State]),
|
||||
{keep_state_and_data, postpone};
|
||||
not_connected(cast, Msg={connect_proc, {ok, Socket}}, State=#state{owner_pid=OwnerPid, extra=Extra}) ->
|
||||
?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]),
|
||||
OwnerPid ! {?MODULE, self(), {ok, ssl:negotiated_protocol(Socket)}, Extra},
|
||||
%% We need to spawn this call before OTP-21.2 because it triggers
|
||||
%% a cb_setopts call that blocks us. Might be OK to just leave it
|
||||
%% like this once we support 21.2+ only.
|
||||
spawn(fun() -> ok = ssl:setopts(Socket, [{active, true}]) end),
|
||||
{next_state, connected, State#state{proxy_socket=Socket}};
|
||||
not_connected(cast, Msg={connect_proc, Error}, State=#state{owner_pid=OwnerPid, extra=Extra}) ->
|
||||
?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]),
|
||||
OwnerPid ! {?MODULE, self(), Error, Extra},
|
||||
%% We unlink from the owner process to avoid taking it down with us.
|
||||
unlink(OwnerPid),
|
||||
{stop, {shutdown, Error}, State};
|
||||
not_connected(Type, Event, State) ->
|
||||
handle_common(Type, Event, State).
|
||||
|
||||
%% Send data through the proxy socket.
|
||||
connected({call, From}, Msg={send, Data}, State=#state{proxy_socket=Socket}) ->
|
||||
?DEBUG_LOG("msg ~0p from ~0p state ~0p", [Msg, From, State]),
|
||||
Self = self(),
|
||||
SpawnedPid = spawn(fun() ->
|
||||
gen_statem:cast(Self, {send_result, From, ssl:send(Socket, Data)})
|
||||
end),
|
||||
?DEBUG_LOG("spawned ~0p", [SpawnedPid]),
|
||||
keep_state_and_data;
|
||||
%% Messages from the proxy socket.
|
||||
%%
|
||||
%% When the out_socket is a #{stream_ref := _} map we know that processing
|
||||
%% of the data isn't yet complete. We wrap the message in a handle_continue
|
||||
%% tuple and provide the StreamRef for further processing.
|
||||
connected(info, Msg={ssl, Socket, Data}, State=#state{owner_pid=OwnerPid, proxy_socket=Socket,
|
||||
out_socket=#{handle_continue_stream_ref := StreamRef}}) ->
|
||||
?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]),
|
||||
OwnerPid ! {handle_continue, StreamRef, {tls_proxy, self(), Data}},
|
||||
keep_state_and_data;
|
||||
connected(info, Msg={ssl_closed, Socket}, State=#state{owner_pid=OwnerPid, proxy_socket=Socket,
|
||||
out_socket=#{handle_continue_stream_ref := StreamRef}}) ->
|
||||
?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]),
|
||||
OwnerPid ! {handle_continue, StreamRef, {tls_proxy_closed, self()}},
|
||||
keep_state_and_data;
|
||||
connected(info, Msg={ssl_error, Socket, Reason}, State=#state{owner_pid=OwnerPid, proxy_socket=Socket,
|
||||
out_socket=#{handle_continue_stream_ref := StreamRef}}) ->
|
||||
?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]),
|
||||
OwnerPid ! {handle_continue, StreamRef, {tls_proxy_error, self(), Reason}},
|
||||
keep_state_and_data;
|
||||
%% When the out_socket is anything else then the data is sent like normal
|
||||
%% socket data. It does not need to be handled specially.
|
||||
connected(info, Msg={ssl, Socket, Data}, State=#state{owner_pid=OwnerPid, proxy_socket=Socket}) ->
|
||||
?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]),
|
||||
OwnerPid ! {tls_proxy, self(), Data},
|
||||
keep_state_and_data;
|
||||
connected(info, Msg={ssl_closed, Socket}, State=#state{owner_pid=OwnerPid, proxy_socket=Socket}) ->
|
||||
?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]),
|
||||
OwnerPid ! {tls_proxy_closed, self()},
|
||||
keep_state_and_data;
|
||||
connected(info, Msg={ssl_error, Socket, Reason}, State=#state{owner_pid=OwnerPid, proxy_socket=Socket}) ->
|
||||
?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]),
|
||||
OwnerPid ! {tls_proxy_error, self(), Reason},
|
||||
keep_state_and_data;
|
||||
connected(Type, Event, State) ->
|
||||
handle_common(Type, Event, State).
|
||||
|
||||
handle_common({call, From}, Msg={cb_send, Data}, State=#state{
|
||||
out_socket=OutSocket, out_transport=OutTransport}) ->
|
||||
?DEBUG_LOG("msg ~0p from ~0p state ~0p", [Msg, From, State]),
|
||||
Self = self(),
|
||||
SpawnedPid = spawn(fun() ->
|
||||
gen_statem:cast(Self, {send_result, From, OutTransport:send(OutSocket, Data)})
|
||||
end),
|
||||
?DEBUG_LOG("spawned ~0p", [SpawnedPid]),
|
||||
keep_state_and_data;
|
||||
handle_common({call, From}, Msg={cb_setopts, Opts}, State=#state{
|
||||
out_socket=OutSocket, out_transport=OutTransport0}) ->
|
||||
?DEBUG_LOG("msg ~0p from ~0p state ~0p", [Msg, From, State]),
|
||||
OutTransport = case OutTransport0 of
|
||||
gen_tcp -> inet;
|
||||
_ -> OutTransport0
|
||||
end,
|
||||
{keep_state, proxy_setopts(Opts, State),
|
||||
{reply, From, OutTransport:setopts(OutSocket, [{active, true}])}};
|
||||
handle_common({call, From}, Msg=sockname, State=#state{
|
||||
out_socket=OutSocket, out_transport=OutTransport}) ->
|
||||
?DEBUG_LOG("msg ~0p from ~0p state ~0p", [Msg, From, State]),
|
||||
{keep_state, State,
|
||||
{reply, From, OutTransport:sockname(OutSocket)}};
|
||||
handle_common({call, From}, Msg=close, State) ->
|
||||
?DEBUG_LOG("msg ~0p from ~0p state ~0p", [Msg, From, State]),
|
||||
{stop_and_reply, {shutdown, close}, {reply, From, ok}};
|
||||
handle_common(cast, Msg={set_owner, OwnerPid}, State) ->
|
||||
?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]),
|
||||
{keep_state, State#state{owner_pid=OwnerPid}};
|
||||
handle_common(cast, Msg={cb_controlling_process, ProxyPid}, State) ->
|
||||
?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]),
|
||||
{keep_state, State#state{proxy_pid=ProxyPid}};
|
||||
handle_common(cast, Msg={setopts, Opts}, State) ->
|
||||
?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]),
|
||||
{keep_state, owner_setopts(Opts, State)};
|
||||
handle_common(cast, Msg={send_result, From, Result}, State) ->
|
||||
?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]),
|
||||
gen_statem:reply(From, Result),
|
||||
keep_state_and_data;
|
||||
%% Messages from the real socket.
|
||||
%% @todo Make _Socket and __Socket match again.
|
||||
handle_common(info, Msg={OK, _Socket, Data}, State=#state{proxy_pid=ProxyPid,
|
||||
out_socket=__Socket, out_messages={OK, _, _}}) ->
|
||||
?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]),
|
||||
ProxyPid ! {tls_proxy, self(), Data},
|
||||
keep_state_and_data;
|
||||
handle_common(info, Msg={Closed, Socket}, State=#state{proxy_pid=ProxyPid,
|
||||
out_socket=Socket, out_messages={_, Closed, _}}) ->
|
||||
?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]),
|
||||
ProxyPid ! {tls_proxy_closed, self()},
|
||||
keep_state_and_data;
|
||||
handle_common(info, Msg={Error, Socket, Reason}, State=#state{proxy_pid=ProxyPid,
|
||||
out_socket=Socket, out_messages={_, _, Error}}) ->
|
||||
?DEBUG_LOG("msg ~0p state ~0p", [Msg, State]),
|
||||
ProxyPid ! {tls_proxy_error, self(), Reason},
|
||||
keep_state_and_data;
|
||||
%% Other messages.
|
||||
handle_common(Type, Msg, State) ->
|
||||
?DEBUG_LOG("IGNORED type ~0p msg ~0p state ~0p", [Type, Msg, State]),
|
||||
case Type of
|
||||
{call, From} ->
|
||||
{keep_state, State, {reply, From, {error, bad_call}}};
|
||||
_ ->
|
||||
keep_state_and_data
|
||||
end.
|
||||
|
||||
%% Internal.
|
||||
|
||||
owner_setopts(Opts, State0) ->
|
||||
case [A || {active, A} <- Opts] of
|
||||
[] -> State0;
|
||||
[false] -> State0#state{owner_active=false};
|
||||
% [0] -> OwnerPid ! {tls_proxy_passive, self()}, State0#state{owner_active=false};
|
||||
[Active] -> owner_active(State0#state{owner_active=Active})
|
||||
end.
|
||||
|
||||
owner_active(State=#state{owner_buffer= <<>>}) ->
|
||||
State;
|
||||
owner_active(State=#state{owner_active=false}) ->
|
||||
State;
|
||||
owner_active(State=#state{owner_pid=OwnerPid, owner_active=Active0, owner_buffer=Buffer}) ->
|
||||
OwnerPid ! {tls_proxy, self(), Buffer},
|
||||
Active = case Active0 of
|
||||
true -> true;
|
||||
once -> false%;
|
||||
% 1 -> OwnerPid ! {tls_proxy_passive, self()}, false;
|
||||
% N -> N - 1
|
||||
end,
|
||||
State#state{owner_active=Active, owner_buffer= <<>>}.
|
||||
|
||||
proxy_setopts(Opts, State0=#state{proxy_socket=ProxySocket, proxy_pid=ProxyPid}) ->
|
||||
case [A || {active, A} <- Opts] of
|
||||
[] -> State0;
|
||||
[false] -> State0#state{proxy_active=false};
|
||||
[0] -> ProxyPid ! {tls_proxy_passive, ProxySocket}, State0#state{proxy_active=false};
|
||||
[Active] -> proxy_active(State0#state{proxy_active=Active})
|
||||
end.
|
||||
|
||||
proxy_active(State=#state{proxy_buffer= <<>>}) ->
|
||||
State;
|
||||
proxy_active(State=#state{proxy_active=false}) ->
|
||||
State;
|
||||
proxy_active(State=#state{proxy_pid=ProxyPid, proxy_active=Active0, proxy_buffer=Buffer}) ->
|
||||
ProxyPid ! {tls_proxy, self(), Buffer},
|
||||
Active = case Active0 of
|
||||
true -> true;
|
||||
once -> false;
|
||||
%% Note that tcp_passive is hardcoded in ssl until OTP-21.3.1.
|
||||
1 -> ProxyPid ! {tcp_passive, self()}, false;
|
||||
N -> N - 1
|
||||
end,
|
||||
State#state{proxy_active=Active, proxy_buffer= <<>>}.
|
||||
|
||||
-ifdef(DISABLED_TEST).
|
||||
proxy_test_() ->
|
||||
{timeout, 15000, [
|
||||
{"TCP proxy", fun proxy_tcp1_t/0},
|
||||
{"TLS proxy", fun proxy_ssl1_t/0},
|
||||
{"Double TLS proxy", fun proxy_ssl2_t/0}
|
||||
]}.
|
||||
|
||||
proxy_tcp1_t() ->
|
||||
ssl:start(),
|
||||
{ok, Socket} = gen_tcp:connect("google.com", 443, [binary, {active, false}]),
|
||||
{ok, ProxyPid1} = start_link("google.com", 443, [], 5000, Socket, gen_tcp, #{}),
|
||||
send(ProxyPid1, <<"GET / HTTP/1.1\r\nHost: google.com\r\n\r\n">>),
|
||||
receive {tls_proxy, ProxyPid1, <<"HTTP/1.1 ", _/bits>>} -> ok end.
|
||||
|
||||
proxy_ssl1_t() ->
|
||||
ssl:start(),
|
||||
_ = (catch ct_helper:make_certs_in_ets()),
|
||||
{ok, _, Port} = do_proxy_start("google.com", 443),
|
||||
{ok, Socket} = ssl:connect("localhost", Port, [binary, {active, false}]),
|
||||
{ok, ProxyPid1} = start_link("google.com", 443, [], 5000, Socket, ssl, #{}),
|
||||
send(ProxyPid1, <<"GET / HTTP/1.1\r\nHost: google.com\r\n\r\n">>),
|
||||
receive {tls_proxy, ProxyPid1, <<"HTTP/1.1 ", _/bits>>} -> ok end.
|
||||
|
||||
proxy_ssl2_t() ->
|
||||
ssl:start(),
|
||||
_ = (catch ct_helper:make_certs_in_ets()),
|
||||
{ok, _, Port1} = do_proxy_start("google.com", 443),
|
||||
{ok, _, Port2} = do_proxy_start("localhost", Port1),
|
||||
{ok, Socket} = ssl:connect("localhost", Port2, [binary, {active, false}]),
|
||||
{ok, ProxyPid1} = start_link("localhost", Port1, [], 5000, Socket, ssl, #{}),
|
||||
{ok, ProxyPid2} = start_link("google.com", 443, [], 5000, ProxyPid1, ?MODULE, #{}),
|
||||
send(ProxyPid2, <<"GET / HTTP/1.1\r\nHost: google.com\r\n\r\n">>),
|
||||
receive {tls_proxy, ProxyPid2, <<"HTTP/1.1 ", _/bits>>} -> ok end.
|
||||
|
||||
do_proxy_start(Host, Port) ->
|
||||
Self = self(),
|
||||
Pid = spawn_link(fun() -> do_proxy_init(Self, Host, Port) end),
|
||||
ListenPort = receive_from(Pid),
|
||||
{ok, Pid, ListenPort}.
|
||||
|
||||
do_proxy_init(Parent, Host, Port) ->
|
||||
Opts = ct_helper:get_certs_from_ets(),
|
||||
{ok, ListenSocket} = ssl:listen(0, [binary, {active, false}|Opts]),
|
||||
{ok, {_, ListenPort}} = ssl:sockname(ListenSocket),
|
||||
Parent ! {self(), ListenPort},
|
||||
{ok, ClientSocket0} = ssl:transport_accept(ListenSocket, 10000),
|
||||
{ok, ClientSocket} = ssl:handshake(ClientSocket0, 10000),
|
||||
{ok, OriginSocket} = gen_tcp:connect(
|
||||
Host, Port,
|
||||
[binary, {active, false}]),
|
||||
ssl:setopts(ClientSocket, [{active, true}]),
|
||||
inet:setopts(OriginSocket, [{active, true}]),
|
||||
do_proxy_loop(ClientSocket, OriginSocket).
|
||||
|
||||
do_proxy_loop(ClientSocket, OriginSocket) ->
|
||||
receive
|
||||
{ssl, ClientSocket, Data} ->
|
||||
ok = gen_tcp:send(OriginSocket, Data),
|
||||
do_proxy_loop(ClientSocket, OriginSocket);
|
||||
{tcp, OriginSocket, Data} ->
|
||||
ok = ssl:send(ClientSocket, Data),
|
||||
do_proxy_loop(ClientSocket, OriginSocket);
|
||||
{tcp_closed, _} ->
|
||||
ok;
|
||||
Msg ->
|
||||
error(Msg)
|
||||
end.
|
||||
|
||||
receive_from(Pid) ->
|
||||
receive_from(Pid, 5000).
|
||||
|
||||
receive_from(Pid, Timeout) ->
|
||||
receive
|
||||
{Pid, Msg} ->
|
||||
Msg
|
||||
after Timeout ->
|
||||
error(timeout)
|
||||
end.
|
||||
-endif.
|
|
@ -0,0 +1,41 @@
|
|||
%% Copyright (c) 2019-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
%% Transport callback for ssl.
|
||||
|
||||
-module(gun_tls_proxy_cb).
|
||||
|
||||
-export([connect/4]).
|
||||
-export([controlling_process/2]).
|
||||
-export([send/2]).
|
||||
-export([setopts/2]).
|
||||
-export([close/1]).
|
||||
|
||||
%% The connect/4 function is called by the process
|
||||
%% that calls ssl:connect/2,3,4.
|
||||
connect(_Address, _Port, Opts, _Timeout) ->
|
||||
{_, GunPid} = lists:keyfind(gun_tls_proxy, 1, Opts),
|
||||
{ok, GunPid}.
|
||||
|
||||
controlling_process(Socket, ControllingPid) ->
|
||||
gun_tls_proxy:cb_controlling_process(Socket, ControllingPid).
|
||||
|
||||
send(Socket, Data) ->
|
||||
gun_tls_proxy:cb_send(Socket, Data).
|
||||
|
||||
setopts(Socket, Opts) ->
|
||||
gun_tls_proxy:cb_setopts(Socket, Opts).
|
||||
|
||||
close(_) ->
|
||||
ok.
|
|
@ -0,0 +1,69 @@
|
|||
%% Copyright (c) 2020-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(gun_tls_proxy_http2_connect).
|
||||
|
||||
-export([name/0]).
|
||||
-export([messages/0]).
|
||||
-export([connect/3]).
|
||||
-export([connect/4]).
|
||||
-export([send/2]).
|
||||
-export([setopts/2]).
|
||||
-export([sockname/1]).
|
||||
-export([close/1]).
|
||||
|
||||
-type socket() :: #{
|
||||
%% The pid of the Gun connection.
|
||||
gun_pid := pid(),
|
||||
|
||||
%% The pid of the process that gets replies for this tunnel.
|
||||
reply_to := pid(),
|
||||
|
||||
%% The full stream reference for this tunnel.
|
||||
stream_ref := gun:stream_ref(),
|
||||
|
||||
%% The full stream reference for the responsible HTTP/2 stream.
|
||||
handle_continue_stream_ref := gun:stream_ref()
|
||||
}.
|
||||
|
||||
name() -> tls_proxy_http2_connect.
|
||||
|
||||
messages() -> {tls_proxy_http2_connect, tls_proxy_http2_connect_closed, tls_proxy_http2_connect_error}.
|
||||
|
||||
-spec connect(_, _, _) -> no_return().
|
||||
connect(_, _, _) ->
|
||||
error(not_implemented).
|
||||
|
||||
-spec connect(_, _, _, _) -> no_return().
|
||||
connect(_, _, _, _) ->
|
||||
error(not_implemented).
|
||||
|
||||
-spec send(socket(), iodata()) -> ok.
|
||||
send(#{gun_pid := GunPid, reply_to := ReplyTo, stream_ref := DataStreamRef,
|
||||
handle_continue_stream_ref := StreamRef}, Data) ->
|
||||
GunPid ! {handle_continue, StreamRef, {data, ReplyTo, DataStreamRef, nofin, Data}},
|
||||
ok.
|
||||
|
||||
-spec setopts(_, _) -> no_return().
|
||||
setopts(_, _) ->
|
||||
%% We send messages automatically regardless of active mode.
|
||||
ok.
|
||||
|
||||
-spec sockname(_) -> no_return().
|
||||
sockname(_) ->
|
||||
error(not_implemented).
|
||||
|
||||
-spec close(socket()) -> ok.
|
||||
close(_) ->
|
||||
ok.
|
|
@ -0,0 +1,637 @@
|
|||
%% Copyright (c) 2020-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
%% This module is used when a tunnel is established and either
|
||||
%% StreamRef dereference or a TLS proxy process must be handled
|
||||
%% by the tunnel layer.
|
||||
-module(gun_tunnel).
|
||||
|
||||
-export([init/6]).
|
||||
-export([handle/5]).
|
||||
-export([handle_continue/6]).
|
||||
-export([update_flow/4]).
|
||||
-export([closing/4]).
|
||||
-export([close/4]).
|
||||
-export([keepalive/3]).
|
||||
-export([headers/12]).
|
||||
-export([request/13]).
|
||||
-export([data/7]).
|
||||
-export([connect/9]).
|
||||
-export([cancel/5]).
|
||||
-export([timeout/3]).
|
||||
-export([stream_info/2]).
|
||||
-export([tunneled_name/2]).
|
||||
-export([down/1]).
|
||||
-export([ws_upgrade/11]).
|
||||
-export([ws_send/6]).
|
||||
|
||||
-record(tunnel_state, {
|
||||
%% Fake socket and transport.
|
||||
%% We accept 'undefined' only to simplify the init code.
|
||||
socket = undefined :: #{
|
||||
gun_pid := pid(),
|
||||
reply_to := pid(),
|
||||
stream_ref := gun:stream_ref(),
|
||||
handle_continue_stream_ref := gun:stream_ref()
|
||||
} | pid() | undefined,
|
||||
transport = undefined :: gun_tcp_proxy | gun_tls_proxy | undefined,
|
||||
|
||||
%% The stream_ref from which the stream was created. When
|
||||
%% the tunnel exists as a result of HTTP/2 CONNECT -> HTTP/1.1 CONNECT
|
||||
%% the stream_ref is the same as the HTTP/1.1 CONNECT one.
|
||||
stream_ref = undefined :: gun:stream_ref(),
|
||||
|
||||
%% The pid we send messages to.
|
||||
reply_to = undefined :: pid(),
|
||||
|
||||
%% When the tunnel is a 'connect' tunnel we must dereference the
|
||||
%% stream_ref. When it is 'socks' we must not as there was no
|
||||
%% stream involved in creating the tunnel.
|
||||
type = undefined :: connect | socks5,
|
||||
|
||||
%% Transport and protocol name of the tunnel layer.
|
||||
tunnel_transport = undefined :: tcp | tls,
|
||||
tunnel_protocol = undefined :: http | http2 | socks,
|
||||
|
||||
%% Tunnel information.
|
||||
info = undefined :: gun:tunnel_info(),
|
||||
|
||||
%% The origin socket of the TLS proxy, if any. This is used to forward
|
||||
%% messages to the proxy process in order to decrypt the data.
|
||||
tls_origin_socket = undefined :: undefined | #{
|
||||
gun_pid := pid(),
|
||||
reply_to := pid(),
|
||||
stream_ref := gun:stream_ref(),
|
||||
handle_continue_stream_ref => gun:stream_ref()
|
||||
},
|
||||
|
||||
opts = undefined :: undefined | any(), %% @todo Opts type.
|
||||
|
||||
%% Protocol module and state of the outer layer. Only initialized
|
||||
%% after the TLS handshake has completed when TLS is involved.
|
||||
protocol = undefined :: module(),
|
||||
protocol_state = undefined :: any(),
|
||||
|
||||
%% When the protocol is being switched the origin may change.
|
||||
%% We keep the new information to provide it in TunnelInfo of
|
||||
%% the new protocol when the switch occurs.
|
||||
protocol_origin = undefined :: undefined
|
||||
| {origin, binary(), inet:hostname() | inet:ip_address(), inet:port_number(), connect | socks5}
|
||||
}).
|
||||
|
||||
%% Socket is the "origin socket" and Transport the "origin transport".
|
||||
%% When the Transport indicate a TLS handshake was requested, the socket
|
||||
%% and transport are given to the intermediary TLS proxy process.
|
||||
%%
|
||||
%% Opts is the options for the underlying HTTP/2 connection,
|
||||
%% with some extra information added for the tunnel.
|
||||
%%
|
||||
%% @todo Mark the tunnel options as reserved.
|
||||
init(ReplyTo, OriginSocket, OriginTransport, Opts=#{stream_ref := StreamRef, tunnel := Tunnel},
|
||||
EvHandler, EvHandlerState0) ->
|
||||
#{
|
||||
type := TunnelType,
|
||||
transport_name := TunnelTransport,
|
||||
protocol_name := TunnelProtocol,
|
||||
info := TunnelInfo
|
||||
} = Tunnel,
|
||||
State = #tunnel_state{stream_ref=StreamRef, reply_to=ReplyTo, type=TunnelType,
|
||||
tunnel_transport=TunnelTransport, tunnel_protocol=TunnelProtocol,
|
||||
info=TunnelInfo, opts=maps:without([stream_ref, tunnel], Opts)},
|
||||
case Tunnel of
|
||||
%% Initialize the protocol.
|
||||
#{new_protocol := NewProtocol} ->
|
||||
{Proto, ProtoOpts} = gun_protocols:handler_and_opts(NewProtocol, Opts),
|
||||
case Proto:init(ReplyTo, OriginSocket, OriginTransport,
|
||||
ProtoOpts#{stream_ref => StreamRef, tunnel_transport => tcp}) of
|
||||
{ok, _, ProtoState} ->
|
||||
EvHandlerState = EvHandler:protocol_changed(#{
|
||||
stream_ref => StreamRef,
|
||||
protocol => Proto:name()
|
||||
}, EvHandlerState0),
|
||||
%% When the tunnel protocol is HTTP/1.1 or SOCKS
|
||||
%% the gun_tunnel_up message was already sent.
|
||||
_ = case TunnelProtocol of
|
||||
http -> ok;
|
||||
socks -> ok;
|
||||
_ -> ReplyTo ! {gun_tunnel_up, self(), StreamRef, Proto:name()}
|
||||
end,
|
||||
{tunnel, State#tunnel_state{socket=OriginSocket, transport=OriginTransport,
|
||||
protocol=Proto, protocol_state=ProtoState},
|
||||
EvHandlerState};
|
||||
Error={error, _} ->
|
||||
Error
|
||||
end;
|
||||
%% We can't initialize the protocol until the TLS handshake has completed.
|
||||
#{handshake_event := HandshakeEvent0, protocols := Protocols} ->
|
||||
#{handle_continue_stream_ref := ContinueStreamRef} = OriginSocket,
|
||||
#{
|
||||
origin_host := DestHost,
|
||||
origin_port := DestPort
|
||||
} = TunnelInfo,
|
||||
#{
|
||||
tls_opts := TLSOpts,
|
||||
timeout := TLSTimeout
|
||||
} = HandshakeEvent0,
|
||||
HandshakeEvent = HandshakeEvent0#{socket => OriginSocket},
|
||||
EvHandlerState = EvHandler:tls_handshake_start(HandshakeEvent, EvHandlerState0),
|
||||
{ok, ProxyPid} = gun_tls_proxy:start_link(DestHost, DestPort,
|
||||
TLSOpts, TLSTimeout, OriginSocket, gun_tls_proxy_http2_connect,
|
||||
{handle_continue, ContinueStreamRef, HandshakeEvent, Protocols}),
|
||||
{tunnel, State#tunnel_state{socket=ProxyPid, transport=gun_tls_proxy,
|
||||
tls_origin_socket=OriginSocket}, EvHandlerState}
|
||||
end.
|
||||
|
||||
%% When we receive data we pass it forward directly for TCP;
|
||||
%% or we decrypt it and pass it via handle_continue for TLS.
|
||||
handle(Data, State=#tunnel_state{transport=gun_tcp_proxy,
|
||||
protocol=Proto, protocol_state=ProtoState0},
|
||||
CookieStore0, EvHandler, EvHandlerState0) ->
|
||||
{Commands, CookieStore, EvHandlerState1} = Proto:handle(
|
||||
Data, ProtoState0, CookieStore0, EvHandler, EvHandlerState0),
|
||||
{ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1),
|
||||
{ResCommands, CookieStore, EvHandlerState};
|
||||
handle(Data, State=#tunnel_state{transport=gun_tls_proxy,
|
||||
socket=ProxyPid, tls_origin_socket=OriginSocket},
|
||||
CookieStore, _EvHandler, EvHandlerState) ->
|
||||
%% When we receive a DATA frame that contains TLS-encoded data,
|
||||
%% we must first forward it to the ProxyPid to be decoded. The
|
||||
%% Gun process will receive it back as a tls_proxy_http2_connect
|
||||
%% message and forward it to the right stream via the handle_continue
|
||||
%% callback.
|
||||
ProxyPid ! {tls_proxy_http2_connect, OriginSocket, Data},
|
||||
{{state, State}, CookieStore, EvHandlerState}.
|
||||
|
||||
%% This callback will only be called for TLS.
|
||||
%%
|
||||
%% The StreamRef in this callback is special because it includes
|
||||
%% a reference() for Socks layers as well.
|
||||
handle_continue(ContinueStreamRef, {gun_tls_proxy, ProxyPid, {ok, Negotiated},
|
||||
{handle_continue, _, HandshakeEvent, Protocols}},
|
||||
State=#tunnel_state{socket=ProxyPid, stream_ref=StreamRef, opts=Opts},
|
||||
CookieStore, EvHandler, EvHandlerState0)
|
||||
when is_reference(ContinueStreamRef) ->
|
||||
#{reply_to := ReplyTo} = HandshakeEvent,
|
||||
NewProtocol = gun_protocols:negotiated(Negotiated, Protocols),
|
||||
{Proto, ProtoOpts} = gun_protocols:handler_and_opts(NewProtocol, Opts),
|
||||
EvHandlerState1 = EvHandler:tls_handshake_end(HandshakeEvent#{
|
||||
socket => ProxyPid,
|
||||
protocol => Proto:name()
|
||||
}, EvHandlerState0),
|
||||
EvHandlerState = EvHandler:protocol_changed(#{
|
||||
stream_ref => StreamRef,
|
||||
protocol => Proto:name()
|
||||
}, EvHandlerState1),
|
||||
%% @todo Terminate the current protocol or something?
|
||||
OriginSocket = #{
|
||||
gun_pid => self(),
|
||||
reply_to => ReplyTo,
|
||||
stream_ref => StreamRef
|
||||
},
|
||||
case Proto:init(ReplyTo, OriginSocket, gun_tcp_proxy,
|
||||
ProtoOpts#{stream_ref => StreamRef, tunnel_transport => tls}) of
|
||||
{ok, _, ProtoState} ->
|
||||
ReplyTo ! {gun_tunnel_up, self(), StreamRef, Proto:name()},
|
||||
{{state, State#tunnel_state{protocol=Proto, protocol_state=ProtoState}},
|
||||
CookieStore, EvHandlerState};
|
||||
Error={error, _} ->
|
||||
{Error, CookieStore, EvHandlerState}
|
||||
end;
|
||||
handle_continue(ContinueStreamRef, {gun_tls_proxy, ProxyPid, {error, Reason},
|
||||
{handle_continue, _, HandshakeEvent, _}},
|
||||
#tunnel_state{socket=ProxyPid}, CookieStore, EvHandler, EvHandlerState0)
|
||||
when is_reference(ContinueStreamRef) ->
|
||||
EvHandlerState = EvHandler:tls_handshake_end(HandshakeEvent#{
|
||||
error => Reason
|
||||
}, EvHandlerState0),
|
||||
%% @todo
|
||||
%% The TCP connection can be closed by either peer. The END_STREAM flag
|
||||
%% on a DATA frame is treated as being equivalent to the TCP FIN bit. A
|
||||
%% client is expected to send a DATA frame with the END_STREAM flag set
|
||||
%% after receiving a frame bearing the END_STREAM flag. A proxy that
|
||||
%% receives a DATA frame with the END_STREAM flag set sends the attached
|
||||
%% data with the FIN bit set on the last TCP segment. A proxy that
|
||||
%% receives a TCP segment with the FIN bit set sends a DATA frame with
|
||||
%% the END_STREAM flag set. Note that the final TCP segment or DATA
|
||||
%% frame could be empty.
|
||||
{{error, Reason}, CookieStore, EvHandlerState};
|
||||
%% Send the data. This causes TLS to encrypt the data and send it to the inner layer.
|
||||
handle_continue(ContinueStreamRef, {data, _ReplyTo, _StreamRef, IsFin, Data},
|
||||
#tunnel_state{}, CookieStore, _EvHandler, EvHandlerState)
|
||||
when is_reference(ContinueStreamRef) ->
|
||||
{{send, IsFin, Data}, CookieStore, EvHandlerState};
|
||||
handle_continue(ContinueStreamRef, {tls_proxy, ProxyPid, Data},
|
||||
State=#tunnel_state{socket=ProxyPid, protocol=Proto, protocol_state=ProtoState},
|
||||
CookieStore0, EvHandler, EvHandlerState0)
|
||||
when is_reference(ContinueStreamRef) ->
|
||||
{Commands, CookieStore, EvHandlerState1} = Proto:handle(
|
||||
Data, ProtoState, CookieStore0, EvHandler, EvHandlerState0),
|
||||
{ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1),
|
||||
{ResCommands, CookieStore, EvHandlerState};
|
||||
handle_continue(ContinueStreamRef, {tls_proxy_closed, ProxyPid},
|
||||
#tunnel_state{socket=ProxyPid}, CookieStore, _EvHandler, EvHandlerState0)
|
||||
when is_reference(ContinueStreamRef) ->
|
||||
%% @todo All sub-streams must produce a stream_error.
|
||||
{{error, closed}, CookieStore, EvHandlerState0};
|
||||
handle_continue(ContinueStreamRef, {tls_proxy_error, ProxyPid, Reason},
|
||||
#tunnel_state{socket=ProxyPid}, CookieStore, _EvHandler, EvHandlerState0)
|
||||
when is_reference(ContinueStreamRef) ->
|
||||
%% @todo All sub-streams must produce a stream_error.
|
||||
{{error, Reason}, CookieStore, EvHandlerState0};
|
||||
%% We always dereference the ContinueStreamRef because it includes a
|
||||
%% reference() for Socks layers too.
|
||||
%%
|
||||
%% @todo Assert StreamRef to be our reference().
|
||||
handle_continue([_StreamRef|ContinueStreamRef0], Msg,
|
||||
State=#tunnel_state{protocol=Proto, protocol_state=ProtoState},
|
||||
CookieStore0, EvHandler, EvHandlerState0) ->
|
||||
ContinueStreamRef = case ContinueStreamRef0 of
|
||||
[CSR] -> CSR;
|
||||
_ -> ContinueStreamRef0
|
||||
end,
|
||||
{Commands, CookieStore, EvHandlerState1} = Proto:handle_continue(
|
||||
ContinueStreamRef, Msg, ProtoState, CookieStore0, EvHandler, EvHandlerState0),
|
||||
{ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1),
|
||||
{ResCommands, CookieStore, EvHandlerState}.
|
||||
|
||||
%% @todo This function will need EvHandler/EvHandlerState?
|
||||
update_flow(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState},
|
||||
ReplyTo, StreamRef0, Inc) ->
|
||||
StreamRef = maybe_dereference(State, StreamRef0),
|
||||
Commands = Proto:update_flow(ProtoState, ReplyTo, StreamRef, Inc),
|
||||
{ResCommands, undefined} = commands(Commands, State, undefined, undefined),
|
||||
ResCommands.
|
||||
|
||||
closing(_Reason, _State, _EvHandler, EvHandlerState) ->
|
||||
%% @todo Graceful shutdown must be propagated to tunnels.
|
||||
{[], EvHandlerState}.
|
||||
|
||||
close(_Reason, _State, _EvHandler, EvHandlerState) ->
|
||||
%% @todo Closing must be propagated to tunnels.
|
||||
EvHandlerState.
|
||||
|
||||
keepalive(_State, _EvHandler, EvHandlerState) ->
|
||||
%% @todo Need to figure out how to handle keepalive for tunnels.
|
||||
{[], EvHandlerState}.
|
||||
|
||||
%% We pass the headers forward and optionally dereference StreamRef.
|
||||
headers(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState0},
|
||||
StreamRef0, ReplyTo, Method, Host, Port, Path, Headers,
|
||||
InitialFlow, CookieStore0, EvHandler, EvHandlerState0) ->
|
||||
StreamRef = maybe_dereference(State, StreamRef0),
|
||||
{Commands, CookieStore, EvHandlerState1} = Proto:headers(ProtoState0, StreamRef,
|
||||
ReplyTo, Method, Host, Port, Path, Headers,
|
||||
InitialFlow, CookieStore0, EvHandler, EvHandlerState0),
|
||||
{ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1),
|
||||
{ResCommands, CookieStore, EvHandlerState}.
|
||||
|
||||
%% We pass the request forward and optionally dereference StreamRef.
|
||||
request(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState0,
|
||||
info=#{origin_host := OriginHost, origin_port := OriginPort}},
|
||||
StreamRef0, ReplyTo, Method, _Host, _Port, Path, Headers, Body,
|
||||
InitialFlow, CookieStore0, EvHandler, EvHandlerState0) ->
|
||||
StreamRef = maybe_dereference(State, StreamRef0),
|
||||
{Commands, CookieStore, EvHandlerState1} = Proto:request(ProtoState0, StreamRef,
|
||||
ReplyTo, Method, OriginHost, OriginPort, Path, Headers, Body,
|
||||
InitialFlow, CookieStore0, EvHandler, EvHandlerState0),
|
||||
{ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1),
|
||||
{ResCommands, CookieStore, EvHandlerState}.
|
||||
|
||||
%% When the next tunnel is SOCKS we pass the data forward directly.
|
||||
%% This is needed because SOCKS has no StreamRef and the data cannot
|
||||
%% therefore be passed forward through the usual method.
|
||||
data(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState0,
|
||||
protocol_origin={origin, _, _, _, socks5}},
|
||||
StreamRef, ReplyTo, IsFin, Data, EvHandler, EvHandlerState0) ->
|
||||
{Commands, EvHandlerState1} = Proto:data(ProtoState0, StreamRef,
|
||||
ReplyTo, IsFin, Data, EvHandler, EvHandlerState0),
|
||||
{ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1),
|
||||
{ResCommands, EvHandlerState};
|
||||
%% CONNECT tunnels pass the data forward and dereference StreamRef
|
||||
%% unless they are the recipient of the callback, in which case the
|
||||
%% data is sent to the socket.
|
||||
data(State=#tunnel_state{socket=Socket, transport=Transport,
|
||||
stream_ref=TunnelStreamRef0, protocol=Proto, protocol_state=ProtoState0},
|
||||
StreamRef0, ReplyTo, IsFin, Data, EvHandler, EvHandlerState0) ->
|
||||
TunnelStreamRef = outer_stream_ref(TunnelStreamRef0),
|
||||
case StreamRef0 of
|
||||
TunnelStreamRef ->
|
||||
case Transport:send(Socket, Data) of
|
||||
ok -> {[], EvHandlerState0};
|
||||
Error={error, _} -> {Error, EvHandlerState0}
|
||||
end;
|
||||
_ ->
|
||||
StreamRef = maybe_dereference(State, StreamRef0),
|
||||
{Commands, EvHandlerState1} = Proto:data(ProtoState0, StreamRef,
|
||||
ReplyTo, IsFin, Data, EvHandler, EvHandlerState0),
|
||||
{ResCommands, EvHandlerState} = commands(Commands, State,
|
||||
EvHandler, EvHandlerState1),
|
||||
{ResCommands, EvHandlerState}
|
||||
end.
|
||||
|
||||
%% We pass the CONNECT request forward and optionally dereference StreamRef.
|
||||
connect(State=#tunnel_state{info=#{origin_host := Host, origin_port := Port},
|
||||
protocol=Proto, protocol_state=ProtoState0},
|
||||
StreamRef0, ReplyTo, Destination, _, Headers, InitialFlow,
|
||||
EvHandler, EvHandlerState0) ->
|
||||
StreamRef = maybe_dereference(State, StreamRef0),
|
||||
{Commands, EvHandlerState1} = Proto:connect(ProtoState0, StreamRef,
|
||||
ReplyTo, Destination, #{host => Host, port => Port}, Headers, InitialFlow,
|
||||
EvHandler, EvHandlerState0),
|
||||
{ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1),
|
||||
{ResCommands, EvHandlerState}.
|
||||
|
||||
cancel(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState0},
|
||||
StreamRef0, ReplyTo, EvHandler, EvHandlerState0) ->
|
||||
StreamRef = maybe_dereference(State, StreamRef0),
|
||||
{Commands, EvHandlerState1} = Proto:cancel(ProtoState0, StreamRef,
|
||||
ReplyTo, EvHandler, EvHandlerState0),
|
||||
{ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1),
|
||||
{ResCommands, EvHandlerState}.
|
||||
|
||||
timeout(State=#tunnel_state{protocol=Proto, protocol_state=ProtoState0}, Msg, TRef) ->
|
||||
case Proto:timeout(ProtoState0, Msg, TRef) of
|
||||
{state, ProtoState} ->
|
||||
{state, State#tunnel_state{protocol_state=ProtoState}};
|
||||
Other ->
|
||||
Other
|
||||
end.
|
||||
|
||||
stream_info(#tunnel_state{transport=Transport0, stream_ref=TunnelStreamRef, reply_to=ReplyTo,
|
||||
tunnel_protocol=TunnelProtocol,
|
||||
info=#{origin_host := OriginHost, origin_port := OriginPort},
|
||||
protocol=Proto, protocol_state=ProtoState}, StreamRef)
|
||||
when is_reference(StreamRef), TunnelProtocol =/= socks ->
|
||||
Transport = case Transport0 of
|
||||
gun_tcp_proxy -> tcp;
|
||||
gun_tls_proxy -> tls
|
||||
end,
|
||||
Protocol = case Proto of
|
||||
gun_tunnel -> Proto:tunneled_name(ProtoState, false);
|
||||
_ -> Proto:name()
|
||||
end,
|
||||
{ok, #{
|
||||
ref => TunnelStreamRef,
|
||||
reply_to => ReplyTo,
|
||||
state => running,
|
||||
tunnel => #{
|
||||
transport => Transport,
|
||||
protocol => Protocol,
|
||||
origin_scheme => case {Transport, Protocol} of
|
||||
{_, raw} -> undefined;
|
||||
{tcp, _} -> <<"http">>;
|
||||
{tls, _} -> <<"https">>
|
||||
end,
|
||||
origin_host => OriginHost,
|
||||
origin_port => OriginPort
|
||||
}
|
||||
}};
|
||||
stream_info(State=#tunnel_state{type=Type,
|
||||
tunnel_transport=IntermediaryTransport, tunnel_protocol=IntermediaryProtocol,
|
||||
info=TunnelInfo, protocol=Proto, protocol_state=ProtoState}, StreamRef0) ->
|
||||
StreamRef = maybe_dereference(State, StreamRef0),
|
||||
case Proto:stream_info(ProtoState, StreamRef) of
|
||||
{ok, undefined} ->
|
||||
{ok, undefined};
|
||||
{ok, Info} ->
|
||||
#{
|
||||
host := IntermediateHost,
|
||||
port := IntermediatePort
|
||||
} = TunnelInfo,
|
||||
IntermediaryInfo = #{
|
||||
type => Type,
|
||||
host => IntermediateHost,
|
||||
port => IntermediatePort,
|
||||
transport => IntermediaryTransport,
|
||||
protocol => IntermediaryProtocol
|
||||
},
|
||||
Intermediaries = maps:get(intermediaries, Info, []),
|
||||
{ok, Info#{
|
||||
intermediaries => [IntermediaryInfo|Intermediaries]
|
||||
}}
|
||||
end.
|
||||
|
||||
tunneled_name(#tunnel_state{protocol=Proto=gun_tunnel, protocol_state=ProtoState}, true) ->
|
||||
Proto:tunneled_name(ProtoState, false);
|
||||
tunneled_name(#tunnel_state{tunnel_protocol=TunnelProto}, false) ->
|
||||
TunnelProto;
|
||||
tunneled_name(#tunnel_state{protocol=Proto}, _) ->
|
||||
Proto:name().
|
||||
|
||||
down(_State) ->
|
||||
%% @todo Tunnels must be included in the gun_down message.
|
||||
[].
|
||||
|
||||
ws_upgrade(State=#tunnel_state{info=TunnelInfo, protocol=Proto, protocol_state=ProtoState0},
|
||||
StreamRef0, ReplyTo, _, _, Path, Headers, WsOpts,
|
||||
CookieStore0, EvHandler, EvHandlerState0) ->
|
||||
StreamRef = maybe_dereference(State, StreamRef0),
|
||||
#{
|
||||
origin_host := Host,
|
||||
origin_port := Port
|
||||
} = TunnelInfo,
|
||||
{Commands, CookieStore, EvHandlerState1} = Proto:ws_upgrade(ProtoState0, StreamRef, ReplyTo,
|
||||
Host, Port, Path, Headers, WsOpts,
|
||||
CookieStore0, EvHandler, EvHandlerState0),
|
||||
{ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1),
|
||||
{ResCommands, CookieStore, EvHandlerState}.
|
||||
|
||||
ws_send(Frames, State=#tunnel_state{protocol=Proto, protocol_state=ProtoState},
|
||||
StreamRef0, ReplyTo, EvHandler, EvHandlerState0) ->
|
||||
StreamRef = maybe_dereference(State, StreamRef0),
|
||||
{Commands, EvHandlerState1} = Proto:ws_send(Frames,
|
||||
ProtoState, StreamRef, ReplyTo, EvHandler, EvHandlerState0),
|
||||
{ResCommands, EvHandlerState} = commands(Commands, State, EvHandler, EvHandlerState1),
|
||||
{ResCommands, EvHandlerState}.
|
||||
|
||||
%% Internal.
|
||||
|
||||
%% Returns an error on send errors, a state otherwise
|
||||
commands(Command, State, EvHandler, EvHandlerState) when not is_list(Command) ->
|
||||
commands([Command], State, EvHandler, EvHandlerState);
|
||||
commands([], State, _, EvHandlerState) ->
|
||||
{{state, State}, EvHandlerState};
|
||||
commands([Error = {error, _}|_],
|
||||
State=#tunnel_state{socket=Socket, transport=Transport},
|
||||
_, EvHandlerState) ->
|
||||
%% We must terminate the TLS proxy pid if any.
|
||||
case Transport of
|
||||
gun_tls_proxy -> gun_tls_proxy:close(Socket);
|
||||
_ -> ok
|
||||
end,
|
||||
{[{state, State}, Error], EvHandlerState};
|
||||
commands([{state, ProtoState}|Tail], State, EvHandler, EvHandlerState) ->
|
||||
commands(Tail, State#tunnel_state{protocol_state=ProtoState}, EvHandler, EvHandlerState);
|
||||
%% @todo What to do about IsFin?
|
||||
commands([{send, _IsFin, Data}|Tail],
|
||||
State=#tunnel_state{socket=Socket, transport=Transport},
|
||||
EvHandler, EvHandlerState) ->
|
||||
case Transport:send(Socket, Data) of
|
||||
ok -> commands(Tail, State, EvHandler, EvHandlerState);
|
||||
Error={error, _} -> {Error, EvHandlerState}
|
||||
end;
|
||||
commands([Origin={origin, Scheme, Host, Port, Type}|Tail],
|
||||
State=#tunnel_state{stream_ref=StreamRef},
|
||||
EvHandler, EvHandlerState0) ->
|
||||
EvHandlerState = EvHandler:origin_changed(#{
|
||||
stream_ref => StreamRef,
|
||||
type => Type,
|
||||
origin_scheme => Scheme,
|
||||
origin_host => Host,
|
||||
origin_port => Port
|
||||
}, EvHandlerState0),
|
||||
commands(Tail, State#tunnel_state{protocol_origin=Origin}, EvHandler, EvHandlerState);
|
||||
commands([{switch_protocol, NewProtocol, ReplyTo}|Tail],
|
||||
State=#tunnel_state{socket=Socket, transport=Transport, opts=Opts,
|
||||
protocol_origin=undefined},
|
||||
EvHandler, EvHandlerState0) ->
|
||||
{Proto, ProtoOpts} = gun_protocols:handler_and_opts(NewProtocol, Opts),
|
||||
%% This should only apply to Websocket for the time being.
|
||||
case Proto:init(ReplyTo, Socket, Transport, ProtoOpts) of
|
||||
{ok, connected_ws_only, ProtoState} ->
|
||||
#{stream_ref := StreamRef} = ProtoOpts,
|
||||
EvHandlerState = EvHandler:protocol_changed(#{
|
||||
stream_ref => StreamRef,
|
||||
protocol => Proto:name()
|
||||
}, EvHandlerState0),
|
||||
commands(Tail, State#tunnel_state{protocol=Proto, protocol_state=ProtoState},
|
||||
EvHandler, EvHandlerState);
|
||||
Error={error, _} ->
|
||||
{Error, EvHandlerState0}
|
||||
end;
|
||||
commands([{switch_protocol, NewProtocol, ReplyTo}|Tail],
|
||||
State=#tunnel_state{transport=Transport, stream_ref=TunnelStreamRef,
|
||||
info=#{origin_host := Host, origin_port := Port}, opts=Opts, protocol=CurrentProto,
|
||||
protocol_origin={origin, _Scheme, OriginHost, OriginPort, Type}},
|
||||
EvHandler, EvHandlerState0) ->
|
||||
StreamRef = case Type of
|
||||
socks5 -> TunnelStreamRef;
|
||||
connect -> gun_protocols:stream_ref(NewProtocol)
|
||||
end,
|
||||
ContinueStreamRef0 = continue_stream_ref(State),
|
||||
ContinueStreamRef = case Type of
|
||||
socks5 -> ContinueStreamRef0 ++ [make_ref()];
|
||||
connect -> ContinueStreamRef0 ++ [lists:last(StreamRef)]
|
||||
end,
|
||||
OriginSocket = #{
|
||||
gun_pid => self(),
|
||||
reply_to => ReplyTo,
|
||||
stream_ref => StreamRef,
|
||||
handle_continue_stream_ref => ContinueStreamRef
|
||||
},
|
||||
ProtoOpts = Opts#{
|
||||
stream_ref => StreamRef,
|
||||
tunnel => #{
|
||||
type => Type,
|
||||
transport_name => case Transport of
|
||||
gun_tcp_proxy -> tcp;
|
||||
gun_tls_proxy -> tls
|
||||
end,
|
||||
protocol_name => CurrentProto:name(),
|
||||
info => #{
|
||||
host => Host,
|
||||
port => Port,
|
||||
origin_host => OriginHost,
|
||||
origin_port => OriginPort
|
||||
},
|
||||
new_protocol => NewProtocol
|
||||
}
|
||||
},
|
||||
Proto = gun_tunnel,
|
||||
case Proto:init(ReplyTo, OriginSocket, gun_tcp_proxy, ProtoOpts, EvHandler, EvHandlerState0) of
|
||||
{tunnel, ProtoState, EvHandlerState} ->
|
||||
commands(Tail, State#tunnel_state{protocol=Proto, protocol_state=ProtoState},
|
||||
EvHandler, EvHandlerState);
|
||||
Error={error, _} ->
|
||||
{Error, EvHandlerState0}
|
||||
end;
|
||||
commands([{tls_handshake, HandshakeEvent0, Protocols, ReplyTo}|Tail],
|
||||
State=#tunnel_state{transport=Transport,
|
||||
info=#{origin_host := Host, origin_port := Port}, opts=Opts, protocol=CurrentProto,
|
||||
protocol_origin={origin, _Scheme, OriginHost, OriginPort, Type}},
|
||||
EvHandler, EvHandlerState0) ->
|
||||
#{
|
||||
stream_ref := StreamRef,
|
||||
tls_opts := TLSOpts0
|
||||
} = HandshakeEvent0,
|
||||
TLSOpts = gun:ensure_tls_opts(Protocols, TLSOpts0, OriginHost),
|
||||
HandshakeEvent = HandshakeEvent0#{
|
||||
tls_opts => TLSOpts
|
||||
},
|
||||
ContinueStreamRef0 = continue_stream_ref(State),
|
||||
ContinueStreamRef = case Type of
|
||||
socks5 -> ContinueStreamRef0 ++ [make_ref()];
|
||||
connect -> ContinueStreamRef0 ++ [lists:last(StreamRef)]
|
||||
end,
|
||||
OriginSocket = #{
|
||||
gun_pid => self(),
|
||||
reply_to => ReplyTo,
|
||||
stream_ref => StreamRef,
|
||||
handle_continue_stream_ref => ContinueStreamRef
|
||||
},
|
||||
ProtoOpts = Opts#{
|
||||
stream_ref => StreamRef,
|
||||
tunnel => #{
|
||||
type => Type,
|
||||
transport_name => case Transport of
|
||||
gun_tcp_proxy -> tcp;
|
||||
gun_tls_proxy -> tls
|
||||
end,
|
||||
protocol_name => CurrentProto:name(),
|
||||
info => #{
|
||||
host => Host,
|
||||
port => Port,
|
||||
origin_host => OriginHost,
|
||||
origin_port => OriginPort
|
||||
},
|
||||
handshake_event => HandshakeEvent,
|
||||
protocols => Protocols
|
||||
}
|
||||
},
|
||||
Proto = gun_tunnel,
|
||||
case Proto:init(ReplyTo, OriginSocket, gun_tcp_proxy, ProtoOpts, EvHandler, EvHandlerState0) of
|
||||
{tunnel, ProtoState, EvHandlerState} ->
|
||||
commands(Tail, State#tunnel_state{protocol=Proto, protocol_state=ProtoState},
|
||||
EvHandler, EvHandlerState);
|
||||
Error={error, _} ->
|
||||
{Error, EvHandlerState0}
|
||||
end;
|
||||
commands([{active, true}|Tail], State, EvHandler, EvHandlerState) ->
|
||||
commands(Tail, State, EvHandler, EvHandlerState).
|
||||
|
||||
continue_stream_ref(#tunnel_state{socket=#{handle_continue_stream_ref := ContinueStreamRef}}) ->
|
||||
if
|
||||
is_list(ContinueStreamRef) -> ContinueStreamRef;
|
||||
true -> [ContinueStreamRef]
|
||||
end;
|
||||
continue_stream_ref(#tunnel_state{tls_origin_socket=#{handle_continue_stream_ref := ContinueStreamRef}}) ->
|
||||
if
|
||||
is_list(ContinueStreamRef) -> ContinueStreamRef;
|
||||
true -> [ContinueStreamRef]
|
||||
end.
|
||||
|
||||
maybe_dereference(#tunnel_state{stream_ref=RealStreamRef, type=connect}, [StreamRef|Tail]) ->
|
||||
%% We ensure that the stream_ref is correct.
|
||||
StreamRef = outer_stream_ref(RealStreamRef),
|
||||
case Tail of
|
||||
[Ref] -> Ref;
|
||||
_ -> Tail
|
||||
end;
|
||||
maybe_dereference(#tunnel_state{type=socks5}, StreamRef) ->
|
||||
StreamRef.
|
||||
|
||||
outer_stream_ref(StreamRef) when is_list(StreamRef) ->
|
||||
lists:last(StreamRef);
|
||||
outer_stream_ref(StreamRef) ->
|
||||
StreamRef.
|
|
@ -0,0 +1,354 @@
|
|||
%% Copyright (c) 2015-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(gun_ws).
|
||||
|
||||
-export([check_options/1]).
|
||||
-export([select_extensions/3]).
|
||||
-export([select_protocol/2]).
|
||||
-export([name/0]).
|
||||
-export([opts_name/0]).
|
||||
-export([has_keepalive/0]).
|
||||
-export([default_keepalive/0]).
|
||||
-export([init/4]).
|
||||
-export([handle/5]).
|
||||
-export([handle_continue/6]).
|
||||
-export([update_flow/4]).
|
||||
-export([closing/4]).
|
||||
-export([close/4]).
|
||||
-export([keepalive/3]).
|
||||
-export([ws_send/6]).
|
||||
-export([down/1]).
|
||||
|
||||
-record(payload, {
|
||||
type = undefined :: cow_ws:frame_type(),
|
||||
rsv = undefined :: cow_ws:rsv(),
|
||||
len = undefined :: non_neg_integer(),
|
||||
mask_key = undefined :: cow_ws:mask_key(),
|
||||
close_code = undefined :: undefined | cow_ws:close_code(),
|
||||
unmasked = <<>> :: binary(),
|
||||
unmasked_len = 0 :: non_neg_integer()
|
||||
}).
|
||||
|
||||
-record(ws_state, {
|
||||
reply_to :: pid(),
|
||||
stream_ref :: reference(),
|
||||
socket :: inet:socket() | ssl:sslsocket(),
|
||||
transport :: module(),
|
||||
opts = #{} :: gun:ws_opts(),
|
||||
buffer = <<>> :: binary(),
|
||||
in = head :: head | #payload{} | close,
|
||||
out = head :: head | close,
|
||||
frag_state = undefined :: cow_ws:frag_state(),
|
||||
utf8_state = 0 :: cow_ws:utf8_state(),
|
||||
extensions = #{} :: cow_ws:extensions(),
|
||||
flow :: integer() | infinity,
|
||||
handler :: module(),
|
||||
handler_state :: any()
|
||||
}).
|
||||
|
||||
check_options(Opts) ->
|
||||
do_check_options(maps:to_list(Opts)).
|
||||
|
||||
do_check_options([]) ->
|
||||
ok;
|
||||
do_check_options([{closing_timeout, infinity}|Opts]) ->
|
||||
do_check_options(Opts);
|
||||
do_check_options([{closing_timeout, T}|Opts]) when is_integer(T), T > 0 ->
|
||||
do_check_options(Opts);
|
||||
do_check_options([{compress, B}|Opts]) when is_boolean(B) ->
|
||||
do_check_options(Opts);
|
||||
do_check_options([{default_protocol, M}|Opts]) when is_atom(M) ->
|
||||
do_check_options(Opts);
|
||||
do_check_options([{flow, InitialFlow}|Opts]) when is_integer(InitialFlow), InitialFlow > 0 ->
|
||||
do_check_options(Opts);
|
||||
do_check_options([{keepalive, infinity}|Opts]) ->
|
||||
do_check_options(Opts);
|
||||
do_check_options([{keepalive, K}|Opts]) when is_integer(K), K > 0 ->
|
||||
do_check_options(Opts);
|
||||
do_check_options([Opt={protocols, L}|Opts]) when is_list(L) ->
|
||||
case lists:usort(lists:flatten([[is_binary(B), is_atom(M)] || {B, M} <- L])) of
|
||||
[true] -> do_check_options(Opts);
|
||||
_ -> {error, {options, {ws, Opt}}}
|
||||
end;
|
||||
do_check_options([{reply_to, P}|Opts]) when is_pid(P) ->
|
||||
do_check_options(Opts);
|
||||
do_check_options([{silence_pings, B}|Opts]) when is_boolean(B) ->
|
||||
do_check_options(Opts);
|
||||
do_check_options([{user_opts, _}|Opts]) ->
|
||||
do_check_options(Opts);
|
||||
do_check_options([Opt|_]) ->
|
||||
{error, {options, {ws, Opt}}}.
|
||||
|
||||
select_extensions(Headers, Extensions0, Opts) ->
|
||||
case lists:keyfind(<<"sec-websocket-extensions">>, 1, Headers) of
|
||||
false ->
|
||||
#{};
|
||||
{_, ExtHd} ->
|
||||
ParsedExtHd = cow_http_hd:parse_sec_websocket_extensions(ExtHd),
|
||||
validate_extensions(ParsedExtHd, Extensions0, Opts, #{})
|
||||
end.
|
||||
|
||||
validate_extensions([], _, _, Acc) ->
|
||||
Acc;
|
||||
validate_extensions([{Name = <<"permessage-deflate">>, Params}|Tail], Extensions, Opts, Acc0) ->
|
||||
case lists:member(Name, Extensions) of
|
||||
true ->
|
||||
case cow_ws:validate_permessage_deflate(Params, Acc0, Opts) of
|
||||
{ok, Acc} -> validate_extensions(Tail, Extensions, Opts, Acc);
|
||||
error -> close
|
||||
end;
|
||||
%% Fail the connection if extension was not requested.
|
||||
false ->
|
||||
close
|
||||
end;
|
||||
%% Fail the connection on unknown extension.
|
||||
validate_extensions(_, _, _, _) ->
|
||||
close.
|
||||
|
||||
%% @todo Validate protocols.
|
||||
select_protocol(Headers, Opts) ->
|
||||
case lists:keyfind(<<"sec-websocket-protocol">>, 1, Headers) of
|
||||
false ->
|
||||
maps:get(default_protocol, Opts, gun_ws_h);
|
||||
{_, Proto} ->
|
||||
ProtoOpt = maps:get(protocols, Opts, []),
|
||||
case lists:keyfind(Proto, 1, ProtoOpt) of
|
||||
{_, Handler} ->
|
||||
Handler;
|
||||
false ->
|
||||
close
|
||||
end
|
||||
end.
|
||||
|
||||
name() -> ws.
|
||||
opts_name() -> ws_opts.
|
||||
has_keepalive() -> true.
|
||||
default_keepalive() -> infinity.
|
||||
|
||||
init(ReplyTo, Socket, Transport, #{stream_ref := StreamRef, headers := Headers,
|
||||
extensions := Extensions, flow := InitialFlow, handler := Handler, opts := Opts}) ->
|
||||
{ok, HandlerState} = Handler:init(ReplyTo, StreamRef, Headers, Opts),
|
||||
{ok, connected_ws_only, #ws_state{reply_to=ReplyTo, stream_ref=StreamRef,
|
||||
socket=Socket, transport=Transport, opts=Opts, extensions=Extensions,
|
||||
flow=InitialFlow, handler=Handler, handler_state=HandlerState}}.
|
||||
|
||||
handle(Data, State, CookieStore, EvHandler, EvHandlerState0) ->
|
||||
{Commands, EvHandlerState} = handle(Data, State, EvHandler, EvHandlerState0),
|
||||
{Commands, CookieStore, EvHandlerState}.
|
||||
|
||||
%% Do not handle anything if we received a close frame.
|
||||
%% Initiate or terminate the closing state depending on whether we sent a close yet.
|
||||
handle(_, State=#ws_state{in=close, out=close}, _, EvHandlerState) ->
|
||||
{[{state, State}, close], EvHandlerState};
|
||||
handle(_, State=#ws_state{in=close}, EvHandler, EvHandlerState) ->
|
||||
closing(normal, State, EvHandler, EvHandlerState);
|
||||
%% Shortcut for common case when Data is empty after processing a frame.
|
||||
handle(<<>>, State=#ws_state{in=head}, _, EvHandlerState) ->
|
||||
maybe_active(State, EvHandlerState);
|
||||
handle(Data, State=#ws_state{reply_to=ReplyTo, stream_ref=StreamRef, buffer=Buffer,
|
||||
in=head, frag_state=FragState, extensions=Extensions},
|
||||
EvHandler, EvHandlerState0) ->
|
||||
%% Send the event only if there was no data in the buffer.
|
||||
%% If there is data in the buffer then we already sent the event.
|
||||
EvHandlerState1 = case Buffer of
|
||||
<<>> ->
|
||||
EvHandler:ws_recv_frame_start(#{
|
||||
stream_ref => StreamRef,
|
||||
reply_to => ReplyTo,
|
||||
frag_state => FragState,
|
||||
extensions => Extensions
|
||||
}, EvHandlerState0);
|
||||
_ ->
|
||||
EvHandlerState0
|
||||
end,
|
||||
Data2 = << Buffer/binary, Data/binary >>,
|
||||
case cow_ws:parse_header(Data2, Extensions, FragState) of
|
||||
{Type, FragState2, Rsv, Len, MaskKey, Rest} ->
|
||||
EvHandlerState = EvHandler:ws_recv_frame_header(#{
|
||||
stream_ref => StreamRef,
|
||||
reply_to => ReplyTo,
|
||||
frag_state => FragState2,
|
||||
extensions => Extensions,
|
||||
type => Type,
|
||||
rsv => Rsv,
|
||||
len => Len,
|
||||
mask_key => MaskKey
|
||||
}, EvHandlerState1),
|
||||
handle(Rest, State#ws_state{buffer= <<>>,
|
||||
in=#payload{type=Type, rsv=Rsv, len=Len, mask_key=MaskKey},
|
||||
frag_state=FragState2}, EvHandler, EvHandlerState);
|
||||
more ->
|
||||
maybe_active(State#ws_state{buffer=Data2}, EvHandlerState1);
|
||||
error ->
|
||||
closing({error, badframe}, State, EvHandler, EvHandlerState1)
|
||||
end;
|
||||
handle(Data, State=#ws_state{in=In=#payload{type=Type, rsv=Rsv, len=Len, mask_key=MaskKey,
|
||||
close_code=CloseCode, unmasked=Unmasked, unmasked_len=UnmaskedLen}, frag_state=FragState,
|
||||
utf8_state=Utf8State, extensions=Extensions}, EvHandler, EvHandlerState) ->
|
||||
case cow_ws:parse_payload(Data, MaskKey, Utf8State, UnmaskedLen, Type, Len, FragState, Extensions, Rsv) of
|
||||
{ok, CloseCode2, Payload, Utf8State2, Rest} ->
|
||||
dispatch(Rest, State#ws_state{in=head, utf8_state=Utf8State2}, Type,
|
||||
<<Unmasked/binary, Payload/binary>>, CloseCode2,
|
||||
EvHandler, EvHandlerState);
|
||||
{ok, Payload, Utf8State2, Rest} ->
|
||||
dispatch(Rest, State#ws_state{in=head, utf8_state=Utf8State2}, Type,
|
||||
<<Unmasked/binary, Payload/binary>>, CloseCode,
|
||||
EvHandler, EvHandlerState);
|
||||
{more, CloseCode2, Payload, Utf8State2} ->
|
||||
maybe_active(State#ws_state{in=In#payload{close_code=CloseCode2,
|
||||
unmasked= <<Unmasked/binary, Payload/binary>>,
|
||||
len=Len - byte_size(Data), unmasked_len=2 + byte_size(Data)}, utf8_state=Utf8State2},
|
||||
EvHandlerState);
|
||||
{more, Payload, Utf8State2} ->
|
||||
maybe_active(State#ws_state{in=In#payload{unmasked= <<Unmasked/binary, Payload/binary>>,
|
||||
len=Len - byte_size(Data), unmasked_len=UnmaskedLen + byte_size(Data)}, utf8_state=Utf8State2},
|
||||
EvHandlerState);
|
||||
Error = {error, _Reason} ->
|
||||
closing(Error, State, EvHandler, EvHandlerState)
|
||||
end.
|
||||
|
||||
handle_continue(ContinueStreamRef, {data, _ReplyTo, _StreamRef, IsFin, Data},
|
||||
#ws_state{}, CookieStore, _EvHandler, EvHandlerState)
|
||||
when is_reference(ContinueStreamRef) ->
|
||||
{{send, IsFin, Data}, CookieStore, EvHandlerState}.
|
||||
|
||||
maybe_active(State=#ws_state{flow=Flow}, EvHandlerState) ->
|
||||
{[
|
||||
{state, State},
|
||||
{active, Flow > 0}
|
||||
], EvHandlerState}.
|
||||
|
||||
dispatch(Rest, State0=#ws_state{reply_to=ReplyTo, stream_ref=StreamRef,
|
||||
frag_state=FragState, extensions=Extensions, flow=Flow0,
|
||||
handler=Handler, handler_state=HandlerState0},
|
||||
Type, Payload, CloseCode, EvHandler, EvHandlerState0) ->
|
||||
EvHandlerState1 = EvHandler:ws_recv_frame_end(#{
|
||||
stream_ref => StreamRef,
|
||||
reply_to => ReplyTo,
|
||||
extensions => Extensions,
|
||||
close_code => CloseCode,
|
||||
payload => Payload
|
||||
}, EvHandlerState0),
|
||||
case cow_ws:make_frame(Type, Payload, CloseCode, FragState) of
|
||||
Frame ->
|
||||
{ok, Dec, HandlerState} = Handler:handle(Frame, HandlerState0),
|
||||
Flow = case Flow0 of
|
||||
infinity -> infinity;
|
||||
_ -> Flow0 - Dec
|
||||
end,
|
||||
State1 = State0#ws_state{flow=Flow, handler_state=HandlerState},
|
||||
{State, EvHandlerState} = case Frame of
|
||||
ping ->
|
||||
{[], EvHandlerState2} = send(pong, State1, ReplyTo, EvHandler, EvHandlerState1),
|
||||
{State1, EvHandlerState2};
|
||||
{ping, Payload} ->
|
||||
{[], EvHandlerState2} = send({pong, Payload}, State1, ReplyTo, EvHandler, EvHandlerState1),
|
||||
{State1, EvHandlerState2};
|
||||
close ->
|
||||
{State1#ws_state{in=close}, EvHandlerState1};
|
||||
{close, _, _} ->
|
||||
{State1#ws_state{in=close}, EvHandlerState1};
|
||||
{fragment, fin, _, _} ->
|
||||
{State1#ws_state{frag_state=undefined}, EvHandlerState1};
|
||||
_ ->
|
||||
{State1, EvHandlerState1}
|
||||
end,
|
||||
handle(Rest, State, EvHandler, EvHandlerState)
|
||||
end.
|
||||
|
||||
update_flow(State=#ws_state{flow=Flow0}, _ReplyTo, _StreamRef, Inc) ->
|
||||
Flow = case Flow0 of
|
||||
infinity -> infinity;
|
||||
_ -> Flow0 + Inc
|
||||
end,
|
||||
[
|
||||
{state, State#ws_state{flow=Flow}},
|
||||
{active, Flow > 0}
|
||||
].
|
||||
|
||||
%% The user already sent the close frame; do nothing.
|
||||
closing(_, State=#ws_state{out=close}, _, EvHandlerState) ->
|
||||
{closing(State), EvHandlerState};
|
||||
closing(Reason, State=#ws_state{reply_to=ReplyTo}, EvHandler, EvHandlerState) ->
|
||||
Code = case Reason of
|
||||
normal -> 1000;
|
||||
owner_down -> 1001;
|
||||
shutdown -> 1001;
|
||||
{error, badframe} -> 1002;
|
||||
{error, badencoding} -> 1007
|
||||
end,
|
||||
send({close, Code, <<>>}, State, ReplyTo, EvHandler, EvHandlerState).
|
||||
|
||||
closing(#ws_state{opts=Opts}) ->
|
||||
Timeout = maps:get(closing_timeout, Opts, 15000),
|
||||
{closing, Timeout}.
|
||||
|
||||
close(_, _, _, EvHandlerState) ->
|
||||
EvHandlerState.
|
||||
|
||||
keepalive(State=#ws_state{reply_to=ReplyTo}, EvHandler, EvHandlerState0) ->
|
||||
send(ping, State, ReplyTo, EvHandler, EvHandlerState0).
|
||||
|
||||
%% Send one frame.
|
||||
send(Frame, State=#ws_state{stream_ref=StreamRef,
|
||||
socket=Socket, transport=Transport, in=In, extensions=Extensions},
|
||||
ReplyTo, EvHandler, EvHandlerState0) ->
|
||||
WsSendFrameEvent = #{
|
||||
stream_ref => StreamRef,
|
||||
reply_to => ReplyTo,
|
||||
extensions => Extensions,
|
||||
frame => Frame
|
||||
},
|
||||
EvHandlerState1 = EvHandler:ws_send_frame_start(WsSendFrameEvent, EvHandlerState0),
|
||||
case Transport:send(Socket, cow_ws:masked_frame(Frame, Extensions)) of
|
||||
ok ->
|
||||
EvHandlerState = EvHandler:ws_send_frame_end(WsSendFrameEvent, EvHandlerState1),
|
||||
if
|
||||
Frame =:= close; element(1, Frame) =:= close ->
|
||||
{[
|
||||
{state, State#ws_state{out=close}},
|
||||
%% We can close immediately if we already
|
||||
%% received a close frame.
|
||||
case In of
|
||||
close -> close;
|
||||
_ -> closing(State)
|
||||
end
|
||||
], EvHandlerState};
|
||||
true ->
|
||||
{[], EvHandlerState}
|
||||
end;
|
||||
Error={error, _} ->
|
||||
{Error, EvHandlerState0}
|
||||
end.
|
||||
|
||||
%% Send many frames.
|
||||
ws_send(Frame, State, ReplyTo, EvHandler, EvHandlerState) when not is_list(Frame) ->
|
||||
send(Frame, State, ReplyTo, EvHandler, EvHandlerState);
|
||||
ws_send([], _, _, _, EvHandlerState) ->
|
||||
{[], EvHandlerState};
|
||||
ws_send([Frame|Tail], State, ReplyTo, EvHandler, EvHandlerState0) ->
|
||||
case send(Frame, State, ReplyTo, EvHandler, EvHandlerState0) of
|
||||
{[], EvHandlerState} ->
|
||||
ws_send(Tail, State, ReplyTo, EvHandler, EvHandlerState);
|
||||
Other ->
|
||||
Other
|
||||
end.
|
||||
|
||||
%% @todo We should probably check the _StreamRef value.
|
||||
ws_send(Frames, State, _StreamRef, ReplyTo, EvHandler, EvHandlerState) ->
|
||||
ws_send(Frames, State, ReplyTo, EvHandler, EvHandlerState).
|
||||
|
||||
down(#ws_state{stream_ref=StreamRef}) ->
|
||||
[StreamRef].
|
|
@ -0,0 +1,44 @@
|
|||
%% Copyright (c) 2017-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(gun_ws_h).
|
||||
-behavior(gun_ws_protocol).
|
||||
|
||||
-export([init/4]).
|
||||
-export([handle/2]).
|
||||
|
||||
-record(state, {
|
||||
reply_to :: pid(),
|
||||
stream_ref :: reference(),
|
||||
frag_buffer = <<>> :: binary(),
|
||||
silence_pings :: boolean()
|
||||
}).
|
||||
|
||||
init(ReplyTo, StreamRef, _, Opts) ->
|
||||
{ok, #state{reply_to=ReplyTo, stream_ref=StreamRef,
|
||||
silence_pings=maps:get(silence_pings, Opts, true)}}.
|
||||
|
||||
handle({fragment, nofin, _, Payload},
|
||||
State=#state{frag_buffer=SoFar}) ->
|
||||
{ok, 0, State#state{frag_buffer= << SoFar/binary, Payload/binary >>}};
|
||||
handle({fragment, fin, Type, Payload},
|
||||
State=#state{reply_to=ReplyTo, stream_ref=StreamRef, frag_buffer=SoFar}) ->
|
||||
ReplyTo ! {gun_ws, self(), StreamRef, {Type, << SoFar/binary, Payload/binary >>}},
|
||||
{ok, 1, State#state{frag_buffer= <<>>}};
|
||||
handle(Frame, State=#state{silence_pings=true}) when Frame =:= ping; Frame =:= pong;
|
||||
element(1, Frame) =:= ping; element(1, Frame) =:= pong ->
|
||||
{ok, 0, State};
|
||||
handle(Frame, State=#state{reply_to=ReplyTo, stream_ref=StreamRef}) ->
|
||||
ReplyTo ! {gun_ws, self(), StreamRef, Frame},
|
||||
{ok, 1, State}.
|
|
@ -0,0 +1,25 @@
|
|||
%% Copyright (c) 2022-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(gun_ws_protocol).
|
||||
|
||||
-type protocol_state() :: any().
|
||||
-export_type([protocol_state/0]).
|
||||
|
||||
-callback init(pid(), reference(), cow_http:headers(), gun:ws_opts())
|
||||
-> {ok, protocol_state()}.
|
||||
|
||||
-callback handle(cow_ws:frame(), State)
|
||||
-> {ok, non_neg_integer(), State}
|
||||
when State::protocol_state().
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,339 @@
|
|||
%% Copyright (c) 2019-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(flow_SUITE).
|
||||
-compile(export_all).
|
||||
-compile(nowarn_export_all).
|
||||
|
||||
-import(ct_helper, [doc/1]).
|
||||
|
||||
all() ->
|
||||
[{group, flow}].
|
||||
|
||||
groups() ->
|
||||
[{flow, [parallel], ct_helper:all(?MODULE)}].
|
||||
|
||||
%% Tests.
|
||||
|
||||
default_flow_http(_) ->
|
||||
doc("Confirm flow control default can be changed and overriden for HTTP/1.1."),
|
||||
{ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], #{env => #{
|
||||
dispatch => cowboy_router:compile([{'_', [{"/", sse_clock_h, date}]}])
|
||||
}}),
|
||||
Port = ranch:get_port(?FUNCTION_NAME),
|
||||
try
|
||||
%% First we check that we can set the flow for the entire connection.
|
||||
{ok, ConnPid1} = gun:open("localhost", Port, #{
|
||||
http_opts => #{flow => 1}
|
||||
}),
|
||||
{ok, http} = gun:await_up(ConnPid1),
|
||||
StreamRef1 = gun:get(ConnPid1, "/"),
|
||||
{response, nofin, 200, _} = gun:await(ConnPid1, StreamRef1),
|
||||
{data, nofin, _} = gun:await(ConnPid1, StreamRef1),
|
||||
{error, timeout} = gun:await(ConnPid1, StreamRef1, 1500),
|
||||
gun:close(ConnPid1),
|
||||
%% Then we confirm that we can override it per request.
|
||||
{ok, ConnPid2} = gun:open("localhost", Port, #{
|
||||
http_opts => #{flow => 1}
|
||||
}),
|
||||
{ok, http} = gun:await_up(ConnPid2),
|
||||
StreamRef2 = gun:get(ConnPid2, "/", [], #{flow => 2}),
|
||||
{response, nofin, 200, _} = gun:await(ConnPid2, StreamRef2),
|
||||
{data, nofin, _} = gun:await(ConnPid2, StreamRef2),
|
||||
{data, nofin, _} = gun:await(ConnPid2, StreamRef2),
|
||||
{error, timeout} = gun:await(ConnPid2, StreamRef2, 1500),
|
||||
gun:close(ConnPid2)
|
||||
after
|
||||
cowboy:stop_listener(?FUNCTION_NAME)
|
||||
end.
|
||||
|
||||
default_flow_http2(_) ->
|
||||
doc("Confirm flow control default can be changed and overriden for HTTP/2."),
|
||||
{ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], #{env => #{
|
||||
dispatch => cowboy_router:compile([{'_', [{"/", sse_clock_h, 40000}]}])
|
||||
}}),
|
||||
Port = ranch:get_port(?FUNCTION_NAME),
|
||||
try
|
||||
%% First we check that we can set the flow for the entire connection.
|
||||
{ok, ConnPid} = gun:open("localhost", Port, #{
|
||||
http2_opts => #{
|
||||
flow => 1,
|
||||
%% We set the max frame size to the same as the initial
|
||||
%% window size in order to reduce the number of data messages.
|
||||
initial_connection_window_size => 65535,
|
||||
initial_stream_window_size => 65535,
|
||||
max_frame_size_received => 65535
|
||||
},
|
||||
protocols => [http2]
|
||||
}),
|
||||
{ok, http2} = gun:await_up(ConnPid),
|
||||
StreamRef1 = gun:get(ConnPid, "/"),
|
||||
{response, nofin, 200, _} = gun:await(ConnPid, StreamRef1),
|
||||
%% We set the flow to 1 therefore we will receive *2* data messages,
|
||||
%% and then nothing because the window was fully consumed.
|
||||
{data, nofin, _} = gun:await(ConnPid, StreamRef1),
|
||||
{data, nofin, _} = gun:await(ConnPid, StreamRef1),
|
||||
{error, timeout} = gun:await(ConnPid, StreamRef1, 1500),
|
||||
%% Then we confirm that we can override it per request.
|
||||
StreamRef2 = gun:get(ConnPid, "/", [], #{flow => 2}),
|
||||
{response, nofin, 200, _} = gun:await(ConnPid, StreamRef2),
|
||||
%% We set the flow to 2 but due to the ensure_window algorithm
|
||||
%% we end up receiving *4* data messages before we consume
|
||||
%% the window.
|
||||
{data, nofin, _} = gun:await(ConnPid, StreamRef2),
|
||||
{data, nofin, _} = gun:await(ConnPid, StreamRef2),
|
||||
{data, nofin, _} = gun:await(ConnPid, StreamRef2),
|
||||
{data, nofin, _} = gun:await(ConnPid, StreamRef2),
|
||||
{error, timeout} = gun:await(ConnPid, StreamRef2, 1500),
|
||||
gun:close(ConnPid)
|
||||
after
|
||||
cowboy:stop_listener(?FUNCTION_NAME)
|
||||
end.
|
||||
|
||||
flow_http(_) ->
|
||||
doc("Confirm flow control works as intended for HTTP/1.1."),
|
||||
{ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], #{env => #{
|
||||
dispatch => cowboy_router:compile([{'_', [{"/", sse_clock_h, date}]}])
|
||||
}}),
|
||||
Port = ranch:get_port(?FUNCTION_NAME),
|
||||
try
|
||||
{ok, ConnPid} = gun:open("localhost", Port),
|
||||
{ok, http} = gun:await_up(ConnPid),
|
||||
StreamRef = gun:get(ConnPid, "/", [], #{flow => 1}),
|
||||
{response, nofin, 200, _} = gun:await(ConnPid, StreamRef),
|
||||
%% We set the flow to 1 therefore we will receive 1 data message,
|
||||
%% and then nothing because Gun doesn't read from the socket.
|
||||
{data, nofin, _} = gun:await(ConnPid, StreamRef),
|
||||
{error, timeout} = gun:await(ConnPid, StreamRef, 3000),
|
||||
%% We then update the flow and get 2 more data messages but no more.
|
||||
gun:update_flow(ConnPid, StreamRef, 2),
|
||||
{data, nofin, _} = gun:await(ConnPid, StreamRef),
|
||||
{data, nofin, _} = gun:await(ConnPid, StreamRef),
|
||||
{error, timeout} = gun:await(ConnPid, StreamRef, 1000),
|
||||
gun:close(ConnPid)
|
||||
after
|
||||
cowboy:stop_listener(?FUNCTION_NAME)
|
||||
end.
|
||||
|
||||
flow_http2(_) ->
|
||||
doc("Confirm flow control works as intended for HTTP/2."),
|
||||
{ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], #{env => #{
|
||||
dispatch => cowboy_router:compile([{'_', [{"/", sse_clock_h, 40000}]}])
|
||||
}}),
|
||||
Port = ranch:get_port(?FUNCTION_NAME),
|
||||
try
|
||||
{ok, ConnPid} = gun:open("localhost", Port, #{
|
||||
%% We set the max frame size to the same as the initial
|
||||
%% window size in order to reduce the number of data messages.
|
||||
http2_opts => #{
|
||||
initial_connection_window_size => 65535,
|
||||
initial_stream_window_size => 65535,
|
||||
max_frame_size_received => 65535
|
||||
},
|
||||
protocols => [http2]
|
||||
}),
|
||||
{ok, http2} = gun:await_up(ConnPid),
|
||||
StreamRef = gun:get(ConnPid, "/", [], #{flow => 1}),
|
||||
{response, nofin, 200, _} = gun:await(ConnPid, StreamRef),
|
||||
%% We set the flow to 1 therefore we will receive *2* data messages,
|
||||
%% and then nothing because the window was fully consumed.
|
||||
{data, nofin, D1} = gun:await(ConnPid, StreamRef),
|
||||
{data, nofin, D2} = gun:await(ConnPid, StreamRef),
|
||||
%% We consumed all the window available.
|
||||
65535 = byte_size(D1) + byte_size(D2),
|
||||
{error, timeout} = gun:await(ConnPid, StreamRef, 3500),
|
||||
%% We then update the flow and get *5* more data messages but no more.
|
||||
gun:update_flow(ConnPid, StreamRef, 2),
|
||||
{data, nofin, D3} = gun:await(ConnPid, StreamRef),
|
||||
{data, nofin, D4} = gun:await(ConnPid, StreamRef),
|
||||
{data, nofin, D5} = gun:await(ConnPid, StreamRef),
|
||||
{data, nofin, D6} = gun:await(ConnPid, StreamRef),
|
||||
{data, nofin, D7} = gun:await(ConnPid, StreamRef),
|
||||
%% We consumed all the window available again.
|
||||
%% D3 is the end of the truncated D2, D4, D5 and D6 are full and D7 truncated.
|
||||
131070 = byte_size(D3) + byte_size(D4) + byte_size(D5) + byte_size(D6) + byte_size(D7),
|
||||
{error, timeout} = gun:await(ConnPid, StreamRef, 1000),
|
||||
gun:close(ConnPid)
|
||||
after
|
||||
cowboy:stop_listener(?FUNCTION_NAME)
|
||||
end.
|
||||
|
||||
flow_ws(_) ->
|
||||
doc("Confirm flow control works as intended for Websocket."),
|
||||
{ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], #{env => #{
|
||||
dispatch => cowboy_router:compile([{'_', [{"/", ws_echo_h, []}]}])
|
||||
}}),
|
||||
Port = ranch:get_port(?FUNCTION_NAME),
|
||||
try
|
||||
{ok, ConnPid} = gun:open("localhost", Port),
|
||||
{ok, http} = gun:await_up(ConnPid),
|
||||
StreamRef = gun:ws_upgrade(ConnPid, "/", [], #{flow => 1}),
|
||||
{upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef),
|
||||
%% We send 2 frames with some time in between to make sure that
|
||||
%% Gun handles them in separate Protocol:handle calls.
|
||||
Frame = {text, <<"Hello!">>},
|
||||
gun:ws_send(ConnPid, StreamRef, Frame),
|
||||
timer:sleep(500),
|
||||
gun:ws_send(ConnPid, StreamRef, Frame),
|
||||
%% We set the flow to 1 therefore we will receive 1 data message,
|
||||
%% and then nothing because Gun doesn't read from the socket.
|
||||
{ws, _} = gun:await(ConnPid, StreamRef),
|
||||
{error, timeout} = gun:await(ConnPid, StreamRef, 3000),
|
||||
%% We then update the flow, send 2 frames with some time in between
|
||||
%% and get 2 more data messages but no more.
|
||||
gun:update_flow(ConnPid, StreamRef, 2),
|
||||
gun:ws_send(ConnPid, StreamRef, Frame),
|
||||
timer:sleep(500),
|
||||
gun:ws_send(ConnPid, StreamRef, Frame),
|
||||
{ws, _} = gun:await(ConnPid, StreamRef),
|
||||
{ws, _} = gun:await(ConnPid, StreamRef),
|
||||
{error, timeout} = gun:await(ConnPid, StreamRef, 1000),
|
||||
gun:close(ConnPid)
|
||||
after
|
||||
cowboy:stop_listener(?FUNCTION_NAME)
|
||||
end.
|
||||
|
||||
no_flow_http(_) ->
|
||||
doc("Ignore flow updates for no-flow streams for HTTP/1.1."),
|
||||
{ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], #{env => #{
|
||||
dispatch => cowboy_router:compile([{'_', [{"/", sse_clock_h, date}]}])
|
||||
}}),
|
||||
Port = ranch:get_port(?FUNCTION_NAME),
|
||||
try
|
||||
{ok, ConnPid} = gun:open("localhost", Port),
|
||||
{ok, http} = gun:await_up(ConnPid),
|
||||
StreamRef = gun:get(ConnPid, "/", []),
|
||||
{response, nofin, 200, _} = gun:await(ConnPid, StreamRef),
|
||||
gun:update_flow(ConnPid, StreamRef, 2),
|
||||
{data, nofin, _} = gun:await(ConnPid, StreamRef),
|
||||
{data, nofin, _} = gun:await(ConnPid, StreamRef),
|
||||
{data, nofin, _} = gun:await(ConnPid, StreamRef),
|
||||
gun:close(ConnPid)
|
||||
after
|
||||
cowboy:stop_listener(?FUNCTION_NAME)
|
||||
end.
|
||||
|
||||
no_flow_http2(_) ->
|
||||
doc("Ignore flow updates for no-flow streams for HTTP/2."),
|
||||
{ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], #{env => #{
|
||||
dispatch => cowboy_router:compile([{'_', [{"/", sse_clock_h, date}]}])
|
||||
}}),
|
||||
Port = ranch:get_port(?FUNCTION_NAME),
|
||||
try
|
||||
{ok, ConnPid} = gun:open("localhost", Port, #{
|
||||
protocols => [http2]
|
||||
}),
|
||||
{ok, http2} = gun:await_up(ConnPid),
|
||||
StreamRef = gun:get(ConnPid, "/", []),
|
||||
{response, nofin, 200, _} = gun:await(ConnPid, StreamRef),
|
||||
gun:update_flow(ConnPid, StreamRef, 2),
|
||||
{data, nofin, _} = gun:await(ConnPid, StreamRef),
|
||||
{data, nofin, _} = gun:await(ConnPid, StreamRef),
|
||||
{data, nofin, _} = gun:await(ConnPid, StreamRef),
|
||||
gun:close(ConnPid)
|
||||
after
|
||||
cowboy:stop_listener(?FUNCTION_NAME)
|
||||
end.
|
||||
|
||||
no_flow_ws(_) ->
|
||||
doc("Ignore flow updates for no-flow streams for Websocket."),
|
||||
{ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], #{env => #{
|
||||
dispatch => cowboy_router:compile([{'_', [{"/", ws_echo_h, []}]}])
|
||||
}}),
|
||||
Port = ranch:get_port(?FUNCTION_NAME),
|
||||
try
|
||||
{ok, ConnPid} = gun:open("localhost", Port),
|
||||
{ok, http} = gun:await_up(ConnPid),
|
||||
StreamRef = gun:ws_upgrade(ConnPid, "/", []),
|
||||
{upgrade, [<<"websocket">>], _} = gun:await(ConnPid, StreamRef),
|
||||
gun:update_flow(ConnPid, StreamRef, 2),
|
||||
Frame = {text, <<"Hello!">>},
|
||||
gun:ws_send(ConnPid, StreamRef, Frame),
|
||||
timer:sleep(100),
|
||||
gun:ws_send(ConnPid, StreamRef, Frame),
|
||||
{ws, _} = gun:await(ConnPid, StreamRef),
|
||||
{ws, _} = gun:await(ConnPid, StreamRef),
|
||||
gun:close(ConnPid)
|
||||
after
|
||||
cowboy:stop_listener(?FUNCTION_NAME)
|
||||
end.
|
||||
|
||||
sse_flow_http(_) ->
|
||||
doc("Confirm flow control works as intended for HTTP/1.1 "
|
||||
"when using the gun_sse_h content handler."),
|
||||
{ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], #{env => #{
|
||||
dispatch => cowboy_router:compile([{'_', [{"/", sse_clock_h, date}]}])
|
||||
}}),
|
||||
Port = ranch:get_port(?FUNCTION_NAME),
|
||||
try
|
||||
{ok, ConnPid} = gun:open("localhost", Port, #{
|
||||
http_opts => #{content_handlers => [gun_sse_h, gun_data_h]}
|
||||
}),
|
||||
{ok, http} = gun:await_up(ConnPid),
|
||||
StreamRef = gun:get(ConnPid, "/", [], #{flow => 1}),
|
||||
{response, nofin, 200, _} = gun:await(ConnPid, StreamRef),
|
||||
%% We set the flow to 1 therefore we will receive 1 event message,
|
||||
%% and then nothing because Gun doesn't read from the socket. We
|
||||
%% set the timeout to 2500 to ensure there is only going to be one
|
||||
%% message queued up.
|
||||
{sse, _} = gun:await(ConnPid, StreamRef),
|
||||
{error, timeout} = gun:await(ConnPid, StreamRef, 2500),
|
||||
%% We then update the flow and get 2 more event messages but no more.
|
||||
gun:update_flow(ConnPid, StreamRef, 2),
|
||||
{sse, _} = gun:await(ConnPid, StreamRef),
|
||||
{sse, _} = gun:await(ConnPid, StreamRef),
|
||||
{error, timeout} = gun:await(ConnPid, StreamRef, 1000),
|
||||
gun:close(ConnPid)
|
||||
after
|
||||
cowboy:stop_listener(?FUNCTION_NAME)
|
||||
end.
|
||||
|
||||
sse_flow_http2(_) ->
|
||||
doc("Confirm flow control works as intended for HTTP/2 "
|
||||
"when using the gun_sse_h content handler."),
|
||||
{ok, _} = cowboy:start_clear(?FUNCTION_NAME, [], #{env => #{
|
||||
dispatch => cowboy_router:compile([{'_', [{"/", sse_clock_h, 40000}]}])
|
||||
}}),
|
||||
Port = ranch:get_port(?FUNCTION_NAME),
|
||||
try
|
||||
{ok, ConnPid} = gun:open("localhost", Port, #{
|
||||
%% We set the max frame size to the same as the initial
|
||||
%% window size in order to reduce the number of data messages.
|
||||
http2_opts => #{
|
||||
content_handlers => [gun_sse_h, gun_data_h],
|
||||
initial_connection_window_size => 65535,
|
||||
initial_stream_window_size => 65535,
|
||||
max_frame_size_received => 65535
|
||||
},
|
||||
protocols => [http2]
|
||||
}),
|
||||
{ok, http2} = gun:await_up(ConnPid),
|
||||
StreamRef = gun:get(ConnPid, "/", [], #{flow => 1}),
|
||||
{response, nofin, 200, _} = gun:await(ConnPid, StreamRef),
|
||||
%% We set the flow to 1 therefore we will receive 1 event message,
|
||||
%% and then nothing because the window was fully consumed before
|
||||
%% the second event was fully received.
|
||||
{sse, _} = gun:await(ConnPid, StreamRef),
|
||||
{error, timeout} = gun:await(ConnPid, StreamRef, 3000),
|
||||
%% We then update the flow and get 3 more event messages but no more.
|
||||
%% We get an extra message because of the ensure_window algorithm.
|
||||
gun:update_flow(ConnPid, StreamRef, 2),
|
||||
{sse, _} = gun:await(ConnPid, StreamRef),
|
||||
{sse, _} = gun:await(ConnPid, StreamRef),
|
||||
{sse, _} = gun:await(ConnPid, StreamRef),
|
||||
{error, timeout} = gun:await(ConnPid, StreamRef, 1000),
|
||||
gun:close(ConnPid)
|
||||
after
|
||||
cowboy:stop_listener(?FUNCTION_NAME)
|
||||
end.
|
|
@ -0,0 +1,754 @@
|
|||
%% Copyright (c) 2017-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(gun_SUITE).
|
||||
-compile(export_all).
|
||||
-compile(nowarn_export_all).
|
||||
|
||||
-import(ct_helper, [doc/1]).
|
||||
-import(ct_helper, [name/0]).
|
||||
-import(gun_test, [init_origin/2]).
|
||||
-import(gun_test, [init_origin/3]).
|
||||
-import(gun_test, [receive_from/1]).
|
||||
-import(gun_test, [receive_all_from/2]).
|
||||
-import(gun_test_event_h, [receive_event/1]).
|
||||
-import(gun_test_event_h, [receive_event/2]).
|
||||
|
||||
suite() ->
|
||||
[{timetrap, 30000}].
|
||||
|
||||
all() ->
|
||||
[{group, gun}].
|
||||
|
||||
groups() ->
|
||||
[{gun, [parallel], ct_helper:all(?MODULE)}].
|
||||
|
||||
%% Tests.
|
||||
|
||||
atom_header_name(_) ->
|
||||
doc("Header names may be given as atom."),
|
||||
{ok, OriginPid, OriginPort} = init_origin(tcp, http),
|
||||
{ok, Pid} = gun:open("localhost", OriginPort),
|
||||
{ok, http} = gun:await_up(Pid),
|
||||
handshake_completed = receive_from(OriginPid),
|
||||
_ = gun:get(Pid, "/", [
|
||||
{'User-Agent', "Gun/atom-headers"}
|
||||
]),
|
||||
Data = receive_from(OriginPid),
|
||||
Lines = binary:split(Data, <<"\r\n">>, [global]),
|
||||
[<<"user-agent: Gun/atom-headers">>] = [L || <<"user-agent: ", _/bits>> = L <- Lines],
|
||||
gun:close(Pid).
|
||||
|
||||
atom_hostname(_) ->
|
||||
doc("Hostnames may be given as atom."),
|
||||
{ok, OriginPid, OriginPort} = init_origin(tcp, http),
|
||||
{ok, Pid} = gun:open('localhost', OriginPort),
|
||||
{ok, http} = gun:await_up(Pid),
|
||||
handshake_completed = receive_from(OriginPid),
|
||||
_ = gun:get(Pid, "/"),
|
||||
Data = receive_from(OriginPid),
|
||||
Lines = binary:split(Data, <<"\r\n">>, [global]),
|
||||
[<<"host: localhost:", _/bits>>] = [L || <<"host: ", _/bits>> = L <- Lines],
|
||||
gun:close(Pid).
|
||||
|
||||
connect_timeout(_) ->
|
||||
doc("Ensure an integer value for connect_timeout is accepted."),
|
||||
do_timeout(connect_timeout, 1000).
|
||||
|
||||
connect_timeout_infinity(_) ->
|
||||
doc("Ensure infinity for connect_timeout is accepted."),
|
||||
do_timeout(connect_timeout, infinity).
|
||||
|
||||
domain_lookup_timeout(_) ->
|
||||
doc("Ensure an integer value for domain_lookup_timeout is accepted."),
|
||||
do_timeout(domain_lookup_timeout, 1000).
|
||||
|
||||
domain_lookup_timeout_infinity(_) ->
|
||||
doc("Ensure infinity for domain_lookup_timeout is accepted."),
|
||||
do_timeout(domain_lookup_timeout, infinity).
|
||||
|
||||
do_timeout(Opt, Timeout) ->
|
||||
{ok, ConnPid} = gun:open("localhost", 12345, #{
|
||||
Opt => Timeout,
|
||||
event_handler => {gun_test_event_h, self()},
|
||||
retry => 0
|
||||
}),
|
||||
%% The connection will not succeed. We will however receive
|
||||
%% an init event from the connection process that indicates
|
||||
%% that the timeout value was accepted, since the timeout
|
||||
%% checks occur earlier.
|
||||
{_, init, _} = receive_event(ConnPid),
|
||||
gun:close(ConnPid).
|
||||
|
||||
ignore_empty_data_http(_) ->
|
||||
doc("When gun:data/4 is called with nofin and empty data, it must be ignored."),
|
||||
{ok, OriginPid, OriginPort} = init_origin(tcp, http),
|
||||
{ok, Pid} = gun:open("localhost", OriginPort),
|
||||
{ok, http} = gun:await_up(Pid),
|
||||
handshake_completed = receive_from(OriginPid),
|
||||
Ref = gun:put(Pid, "/", []),
|
||||
gun:data(Pid, Ref, nofin, "hello "),
|
||||
gun:data(Pid, Ref, nofin, ["", <<>>]),
|
||||
gun:data(Pid, Ref, fin, "world!"),
|
||||
Data = receive_all_from(OriginPid, 500),
|
||||
Lines = binary:split(Data, <<"\r\n">>, [global]),
|
||||
Zero = [Z || <<"0">> = Z <- Lines],
|
||||
1 = length(Zero),
|
||||
gun:close(Pid).
|
||||
|
||||
ignore_empty_data_fin_http(_) ->
|
||||
doc("When gun:data/4 is called with fin and empty data, it must send a final empty chunk."),
|
||||
{ok, OriginPid, OriginPort} = init_origin(tcp, http),
|
||||
{ok, Pid} = gun:open("localhost", OriginPort),
|
||||
{ok, http} = gun:await_up(Pid),
|
||||
handshake_completed = receive_from(OriginPid),
|
||||
Ref = gun:post(Pid, "/", []),
|
||||
gun:data(Pid, Ref, nofin, "hello"),
|
||||
gun:data(Pid, Ref, fin, ["", <<>>]),
|
||||
Data = receive_all_from(OriginPid, 500),
|
||||
Lines = binary:split(Data, <<"\r\n">>, [global]),
|
||||
Zero = [Z || <<"0">> = Z <- Lines],
|
||||
1 = length(Zero),
|
||||
gun:close(Pid).
|
||||
|
||||
ignore_empty_data_http2(_) ->
|
||||
doc("When gun:data/4 is called with nofin and empty data, it must be ignored."),
|
||||
{ok, OriginPid, OriginPort} = init_origin(tcp, http2),
|
||||
{ok, Pid} = gun:open("localhost", OriginPort, #{protocols => [http2]}),
|
||||
{ok, http2} = gun:await_up(Pid),
|
||||
handshake_completed = receive_from(OriginPid),
|
||||
Ref = gun:put(Pid, "/", []),
|
||||
gun:data(Pid, Ref, nofin, "hello "),
|
||||
gun:data(Pid, Ref, nofin, ["", <<>>]),
|
||||
gun:data(Pid, Ref, fin, "world!"),
|
||||
Data = receive_all_from(OriginPid, 500),
|
||||
<<
|
||||
%% HEADERS frame.
|
||||
Len1:24, 1, _:40, _:Len1/unit:8,
|
||||
%% First DATA frame.
|
||||
6:24, 0, _:7, 0:1, _:32, "hello ",
|
||||
%% Second and final DATA frame.
|
||||
6:24, 0, _:7, 1:1, _:32, "world!"
|
||||
>> = Data,
|
||||
gun:close(Pid).
|
||||
|
||||
ignore_empty_data_fin_http2(_) ->
|
||||
doc("When gun:data/4 is called with fin and empty data, it must send a final empty DATA frame."),
|
||||
{ok, OriginPid, OriginPort} = init_origin(tcp, http2),
|
||||
{ok, Pid} = gun:open("localhost", OriginPort, #{protocols => [http2]}),
|
||||
{ok, http2} = gun:await_up(Pid),
|
||||
handshake_completed = receive_from(OriginPid),
|
||||
Ref = gun:put(Pid, "/", []),
|
||||
gun:data(Pid, Ref, nofin, "hello "),
|
||||
gun:data(Pid, Ref, nofin, "world!"),
|
||||
gun:data(Pid, Ref, fin, ["", <<>>]),
|
||||
Data = receive_all_from(OriginPid, 500),
|
||||
<<
|
||||
%% HEADERS frame.
|
||||
Len1:24, 1, _:40, _:Len1/unit:8,
|
||||
%% First DATA frame.
|
||||
6:24, 0, _:7, 0:1, _:32, "hello ",
|
||||
%% Second DATA frame.
|
||||
6:24, 0, _:7, 0:1, _:32, "world!",
|
||||
%% Final empty DATA frame.
|
||||
0:24, 0, _:7, 1:1, _:32
|
||||
>> = Data,
|
||||
gun:close(Pid).
|
||||
|
||||
info(_) ->
|
||||
doc("Get info from the Gun connection."),
|
||||
{ok, ListenSocket} = gen_tcp:listen(0, [binary, {active, false}]),
|
||||
{ok, {_, Port}} = inet:sockname(ListenSocket),
|
||||
{ok, Pid} = gun:open("localhost", Port),
|
||||
{ok, _} = gen_tcp:accept(ListenSocket, 5000),
|
||||
#{sock_ip := _, sock_port := _} = gun:info(Pid),
|
||||
gun:close(Pid).
|
||||
|
||||
keepalive_infinity(_) ->
|
||||
doc("Ensure infinity for keepalive is accepted by all protocols."),
|
||||
{ok, ConnPid} = gun:open("localhost", 12345, #{
|
||||
event_handler => {gun_test_event_h, self()},
|
||||
http_opts => #{keepalive => infinity},
|
||||
http2_opts => #{keepalive => infinity},
|
||||
retry => 0
|
||||
}),
|
||||
%% The connection will not succeed. We will however receive
|
||||
%% an init event from the connection process that indicates
|
||||
%% that the timeout value was accepted, since the timeout
|
||||
%% checks occur earlier.
|
||||
{_, init, _} = receive_event(ConnPid),
|
||||
gun:close(ConnPid).
|
||||
|
||||
killed_streams_http(_) ->
|
||||
doc("Ensure completed responses with a connection: close are not considered killed streams."),
|
||||
{ok, _, OriginPort} = init_origin(tcp, http,
|
||||
fun (_, _, ClientSocket, ClientTransport) ->
|
||||
{ok, _} = ClientTransport:recv(ClientSocket, 0, 1000),
|
||||
ClientTransport:send(ClientSocket,
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"connection: close\r\n"
|
||||
"content-length: 12\r\n"
|
||||
"\r\n"
|
||||
"hello world!"
|
||||
)
|
||||
end),
|
||||
{ok, ConnPid} = gun:open("localhost", OriginPort),
|
||||
{ok, http} = gun:await_up(ConnPid),
|
||||
StreamRef = gun:get(ConnPid, "/"),
|
||||
{response, nofin, 200, _} = gun:await(ConnPid, StreamRef),
|
||||
{ok, <<"hello world!">>} = gun:await_body(ConnPid, StreamRef),
|
||||
receive
|
||||
{gun_down, ConnPid, http, normal, KilledStreams} ->
|
||||
[] = KilledStreams,
|
||||
gun:close(ConnPid)
|
||||
end.
|
||||
|
||||
list_header_name(_) ->
|
||||
doc("Header names may be given as list."),
|
||||
{ok, OriginPid, OriginPort} = init_origin(tcp, http),
|
||||
{ok, Pid} = gun:open("localhost", OriginPort),
|
||||
{ok, http} = gun:await_up(Pid),
|
||||
handshake_completed = receive_from(OriginPid),
|
||||
_ = gun:get(Pid, "/", [
|
||||
{"User-Agent", "Gun/list-headers"}
|
||||
]),
|
||||
Data = receive_from(OriginPid),
|
||||
Lines = binary:split(Data, <<"\r\n">>, [global]),
|
||||
[<<"user-agent: Gun/list-headers">>] = [L || <<"user-agent: ", _/bits>> = L <- Lines],
|
||||
gun:close(Pid).
|
||||
|
||||
map_headers(_) ->
|
||||
doc("Header names may be given as a map."),
|
||||
{ok, OriginPid, OriginPort} = init_origin(tcp, http),
|
||||
{ok, Pid} = gun:open("localhost", OriginPort),
|
||||
{ok, http} = gun:await_up(Pid),
|
||||
handshake_completed = receive_from(OriginPid),
|
||||
_ = gun:get(Pid, "/", #{
|
||||
<<"USER-agent">> => "Gun/map-headers"
|
||||
}),
|
||||
Data = receive_from(OriginPid),
|
||||
Lines = binary:split(Data, <<"\r\n">>, [global]),
|
||||
[<<"user-agent: Gun/map-headers">>] = [L || <<"user-agent: ", _/bits>> = L <- Lines],
|
||||
gun:close(Pid).
|
||||
|
||||
postpone_request_while_not_connected(_) ->
|
||||
doc("Ensure Gun doesn't raise error when requesting in retries"),
|
||||
%% Try connecting to a server that isn't up yet.
|
||||
{ok, ConnPid} = gun:open("localhost", 23456, #{
|
||||
event_handler => {gun_test_event_h, self()},
|
||||
retry => 5,
|
||||
retry_timeout => 1000
|
||||
}),
|
||||
_ = gun:get(ConnPid, "/postponed"),
|
||||
%% Wait for the connection attempt. to fail.
|
||||
{_, connect_end, #{error := _}} = receive_event(ConnPid, connect_end),
|
||||
%% Start the server so that next retry will result in the client connecting successfully.
|
||||
{ok, ListenSocket} = gen_tcp:listen(23456, [binary, {active, false}, {reuseaddr, true}]),
|
||||
{ok, ClientSocket} = gen_tcp:accept(ListenSocket, 5000),
|
||||
%% The client should now be up.
|
||||
{ok, http} = gun:await_up(ConnPid),
|
||||
%% The server receives the postponed request.
|
||||
{ok, <<"GET /postponed HTTP/1.1\r\n", _/bits>>} = gen_tcp:recv(ClientSocket, 0, 5000),
|
||||
gun:close(ConnPid).
|
||||
|
||||
reply_to_http(_) ->
|
||||
doc("The reply_to option allows using a separate process for requests."),
|
||||
do_reply_to(http).
|
||||
|
||||
reply_to_http2(_) ->
|
||||
doc("The reply_to option allows using a separate process for requests."),
|
||||
do_reply_to(http2).
|
||||
|
||||
do_reply_to(Protocol) ->
|
||||
{ok, OriginPid, OriginPort} = init_origin(tcp, Protocol,
|
||||
fun(_, _, ClientSocket, ClientTransport) ->
|
||||
{ok, _} = ClientTransport:recv(ClientSocket, 0, infinity),
|
||||
ResponseData = case Protocol of
|
||||
http ->
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"Content-length: 12\r\n"
|
||||
"\r\n"
|
||||
"Hello world!";
|
||||
http2 ->
|
||||
%% Send a HEADERS frame with PRIORITY back.
|
||||
{HeadersBlock, _} = cow_hpack:encode([
|
||||
{<<":status">>, <<"200">>}
|
||||
]),
|
||||
Len = iolist_size(HeadersBlock),
|
||||
[
|
||||
<<Len:24, 1:8,
|
||||
0:2, %% Undefined.
|
||||
0:1, %% PRIORITY.
|
||||
0:1, %% Undefined.
|
||||
0:1, %% PADDED.
|
||||
1:1, %% END_HEADERS.
|
||||
0:1, %% Undefined.
|
||||
1:1, %% END_STREAM.
|
||||
0:1, 1:31>>,
|
||||
HeadersBlock
|
||||
]
|
||||
end,
|
||||
ok = ClientTransport:send(ClientSocket, ResponseData),
|
||||
timer:sleep(1000)
|
||||
end),
|
||||
{ok, Pid} = gun:open("localhost", OriginPort, #{protocols => [Protocol]}),
|
||||
{ok, Protocol} = gun:await_up(Pid),
|
||||
handshake_completed = receive_from(OriginPid),
|
||||
Self = self(),
|
||||
ReplyTo = spawn(fun() ->
|
||||
receive Ref when is_reference(Ref) ->
|
||||
Response = gun:await(Pid, Ref, infinity),
|
||||
Self ! Response
|
||||
end
|
||||
end),
|
||||
Ref = gun:get(Pid, "/", [], #{reply_to => ReplyTo}),
|
||||
ReplyTo ! Ref,
|
||||
receive
|
||||
Msg ->
|
||||
{response, _, _, _} = Msg,
|
||||
gun:close(Pid)
|
||||
end.
|
||||
|
||||
retry_0(_) ->
|
||||
doc("Ensure Gun gives up immediately with retry=0."),
|
||||
{ok, ConnPid} = gun:open("localhost", 12345, #{
|
||||
event_handler => {gun_test_event_h, self()},
|
||||
retry => 0,
|
||||
retry_timeout => 500
|
||||
}),
|
||||
{_, init, _} = receive_event(ConnPid),
|
||||
{_, domain_lookup_start, _} = receive_event(ConnPid),
|
||||
{_, domain_lookup_end, _} = receive_event(ConnPid),
|
||||
{_, connect_start, _} = receive_event(ConnPid),
|
||||
{_, connect_end, #{error := _}} = receive_event(ConnPid),
|
||||
{_, terminate, _} = receive_event(ConnPid),
|
||||
ok.
|
||||
|
||||
retry_0_disconnect(_) ->
|
||||
doc("Ensure Gun gives up immediately with retry=0 after a successful connection."),
|
||||
{ok, ListenSocket} = gen_tcp:listen(0, [binary, {active, false}]),
|
||||
{ok, {_, Port}} = inet:sockname(ListenSocket),
|
||||
{ok, ConnPid} = gun:open("localhost", Port, #{
|
||||
event_handler => {gun_test_event_h, self()},
|
||||
retry => 0,
|
||||
retry_timeout => 500
|
||||
}),
|
||||
{_, init, _} = receive_event(ConnPid),
|
||||
{_, domain_lookup_start, _} = receive_event(ConnPid),
|
||||
{_, domain_lookup_end, _} = receive_event(ConnPid),
|
||||
{_, connect_start, _} = receive_event(ConnPid),
|
||||
%% We accept the connection and then close it to trigger a disconnect.
|
||||
{ok, ClientSocket} = gen_tcp:accept(ListenSocket, 5000),
|
||||
gen_tcp:close(ClientSocket),
|
||||
%% Connection was successful.
|
||||
{_, connect_end, ConnectEndEvent} = receive_event(ConnPid),
|
||||
false = maps:is_key(error, ConnectEndEvent),
|
||||
%% When the connection is closed we terminate immediately.
|
||||
{_, disconnect, _} = receive_event(ConnPid),
|
||||
{_, terminate, _} = receive_event(ConnPid),
|
||||
ok.
|
||||
|
||||
retry_1(_) ->
|
||||
doc("Ensure Gun gives up with retry=1."),
|
||||
{ok, ConnPid} = gun:open("localhost", 12345, #{
|
||||
event_handler => {gun_test_event_h, self()},
|
||||
retry => 1,
|
||||
retry_timeout => 500
|
||||
}),
|
||||
{_, init, _} = receive_event(ConnPid),
|
||||
%% Initial attempt.
|
||||
{_, domain_lookup_start, _} = receive_event(ConnPid),
|
||||
{_, domain_lookup_end, _} = receive_event(ConnPid),
|
||||
{_, connect_start, _} = receive_event(ConnPid),
|
||||
{_, connect_end, #{error := _}} = receive_event(ConnPid),
|
||||
%% Retry.
|
||||
{_, domain_lookup_start, _} = receive_event(ConnPid),
|
||||
{_, domain_lookup_end, _} = receive_event(ConnPid),
|
||||
{_, connect_start, _} = receive_event(ConnPid),
|
||||
{_, connect_end, #{error := _}} = receive_event(ConnPid),
|
||||
{_, terminate, _} = receive_event(ConnPid),
|
||||
ok.
|
||||
|
||||
retry_1_disconnect(_) ->
|
||||
doc("Ensure Gun doesn't give up with retry=1 after a successful connection "
|
||||
"and attempts to reconnect immediately, ignoring retry_timeout."),
|
||||
{ok, ListenSocket} = gen_tcp:listen(0, [binary, {active, false}]),
|
||||
{ok, {_, Port}} = inet:sockname(ListenSocket),
|
||||
{ok, ConnPid} = gun:open("localhost", Port, #{
|
||||
event_handler => {gun_test_event_h, self()},
|
||||
retry => 1,
|
||||
retry_timeout => 30000
|
||||
}),
|
||||
{_, init, _} = receive_event(ConnPid),
|
||||
{_, domain_lookup_start, _} = receive_event(ConnPid),
|
||||
{_, domain_lookup_end, _} = receive_event(ConnPid),
|
||||
{_, connect_start, _} = receive_event(ConnPid),
|
||||
%% We accept the connection and then close it to trigger a disconnect.
|
||||
{ok, ClientSocket} = gen_tcp:accept(ListenSocket, 5000),
|
||||
gen_tcp:close(ClientSocket),
|
||||
%% Connection was successful.
|
||||
{_, connect_end, ConnectEndEvent} = receive_event(ConnPid),
|
||||
false = maps:is_key(error, ConnectEndEvent),
|
||||
%% We confirm that Gun reconnects before the retry timeout,
|
||||
%% as it is ignored on the first reconnection.
|
||||
{ok, _} = gen_tcp:accept(ListenSocket, 5000),
|
||||
gun:close(ConnPid).
|
||||
|
||||
retry_fun(_) ->
|
||||
doc("Ensure the retry_fun is used when provided."),
|
||||
{ok, ConnPid} = gun:open("localhost", 12345, #{
|
||||
event_handler => {gun_test_event_h, self()},
|
||||
retry => 5,
|
||||
retry_fun => fun(_, _) -> #{retries => 0, timeout => 0} end,
|
||||
retry_timeout => 60000
|
||||
}),
|
||||
{_, init, _} = receive_event(ConnPid),
|
||||
%% Initial attempt.
|
||||
{_, domain_lookup_start, _} = receive_event(ConnPid),
|
||||
{_, domain_lookup_end, _} = receive_event(ConnPid),
|
||||
{_, connect_start, _} = receive_event(ConnPid),
|
||||
{_, connect_end, #{error := _}} = receive_event(ConnPid),
|
||||
%% When retry is not disabled (retry!=0) we necessarily
|
||||
%% have at least one retry attempt using the fun.
|
||||
{_, domain_lookup_start, _} = receive_event(ConnPid),
|
||||
{_, domain_lookup_end, _} = receive_event(ConnPid),
|
||||
{_, connect_start, _} = receive_event(ConnPid),
|
||||
{_, connect_end, #{error := _}} = receive_event(ConnPid),
|
||||
{_, terminate, _} = receive_event(ConnPid),
|
||||
ok.
|
||||
|
||||
retry_timeout(_) ->
|
||||
doc("Ensure the retry_timeout value is enforced. The first retry is immediate "
|
||||
"and therefore does not use the timeout."),
|
||||
{ok, ConnPid} = gun:open("localhost", 12345, #{
|
||||
event_handler => {gun_test_event_h, self()},
|
||||
retry => 2,
|
||||
retry_timeout => 1000
|
||||
}),
|
||||
{_, init, _} = receive_event(ConnPid),
|
||||
%% Initial attempt.
|
||||
{_, domain_lookup_start, _} = receive_event(ConnPid),
|
||||
{_, domain_lookup_end, _} = receive_event(ConnPid),
|
||||
{_, connect_start, _} = receive_event(ConnPid),
|
||||
{_, connect_end, #{error := _, ts := TS1}} = receive_event(ConnPid),
|
||||
%% First retry is immediate.
|
||||
{_, domain_lookup_start, #{ts := TS2}} = receive_event(ConnPid),
|
||||
true = (TS2 - TS1) < 1000,
|
||||
{_, domain_lookup_end, _} = receive_event(ConnPid),
|
||||
{_, connect_start, _} = receive_event(ConnPid),
|
||||
{_, connect_end, #{error := _, ts := TS3}} = receive_event(ConnPid),
|
||||
%% Second retry occurs after the retry_timeout.
|
||||
{_, domain_lookup_start, #{ts := TS4}} = receive_event(ConnPid),
|
||||
true = (TS4 - TS3) >= 1000,
|
||||
{_, domain_lookup_end, _} = receive_event(ConnPid),
|
||||
{_, connect_start, _} = receive_event(ConnPid),
|
||||
{_, connect_end, #{error := _}} = receive_event(ConnPid),
|
||||
{_, terminate, _} = receive_event(ConnPid),
|
||||
ok.
|
||||
|
||||
server_name_indication_custom(_) ->
|
||||
doc("Ensure a custom server_name_indication is accepted."),
|
||||
do_server_name_indication("localhost", net_adm:localhost(), #{
|
||||
tls_opts => [
|
||||
{verify, verify_none}, {versions, ['tlsv1.2']},
|
||||
{fail_if_no_peer_cert, false},
|
||||
{server_name_indication, net_adm:localhost()}]
|
||||
}).
|
||||
|
||||
server_name_indication_default(_) ->
|
||||
doc("Ensure a default server_name_indication is accepted."),
|
||||
do_server_name_indication(net_adm:localhost(), net_adm:localhost(), #{
|
||||
tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']},
|
||||
{fail_if_no_peer_cert, false}]
|
||||
}).
|
||||
|
||||
do_server_name_indication(Host, Expected, GunOpts) ->
|
||||
Self = self(),
|
||||
{ok, OriginPid, OriginPort} = init_origin(tls, http,
|
||||
fun(_, _, ClientSocket, _) ->
|
||||
{ok, Info} = ssl:connection_information(ClientSocket),
|
||||
Msg = {sni_hostname, _} = lists:keyfind(sni_hostname, 1, Info),
|
||||
Self ! Msg
|
||||
end),
|
||||
{ok, ConnPid} = gun:open(Host, OriginPort, GunOpts#{
|
||||
transport => tls,
|
||||
retry => 0
|
||||
}),
|
||||
handshake_completed = receive_from(OriginPid),
|
||||
%% The connection will succeed, look up the SNI hostname
|
||||
%% and send it to us as a message, where we can check it.
|
||||
{sni_hostname, Expected} = receive Msg = {sni_hostname, _} -> Msg end,
|
||||
gun:close(ConnPid).
|
||||
|
||||
set_owner(_) ->
|
||||
doc("The owner of the connection can be changed."),
|
||||
Self = self(),
|
||||
spawn(fun() ->
|
||||
{ok, ConnPid} = gun:open("localhost", 12345),
|
||||
gun:set_owner(ConnPid, Self),
|
||||
Self ! {conn, ConnPid}
|
||||
end),
|
||||
ConnPid = receive {conn, C} -> C end,
|
||||
#{owner := Self} = gun:info(ConnPid),
|
||||
gun:close(ConnPid).
|
||||
|
||||
shutdown_reason(_) ->
|
||||
doc("The last connection failure must be propagated."),
|
||||
do_shutdown_reason().
|
||||
|
||||
do_shutdown_reason() ->
|
||||
%% We set retry=1 so that we can monitor before the process terminates.
|
||||
{ok, ConnPid} = gun:open("localhost", 12345, #{
|
||||
retry => 1,
|
||||
retry_timeout => 500
|
||||
}),
|
||||
Ref = monitor(process, ConnPid),
|
||||
receive
|
||||
%% Depending on timings we may monitor AFTER the process already
|
||||
%% failed to connect and exited. In that case we just try again.
|
||||
%% We rely on timetrap_timeout to stop the test if it takes too long.
|
||||
{'DOWN', Ref, process, ConnPid, noproc} ->
|
||||
ct:log("Monitor got noproc, trying again..."),
|
||||
do_shutdown_reason();
|
||||
{'DOWN', Ref, process, ConnPid, Reason} ->
|
||||
{shutdown, econnrefused} = Reason,
|
||||
gun:close(ConnPid)
|
||||
end.
|
||||
|
||||
stream_info_http(_) ->
|
||||
doc("Ensure the function gun:stream_info/2 works as expected for HTTP/1.1."),
|
||||
{ok, OriginPid, OriginPort} = init_origin(tcp, http,
|
||||
fun(_, _, ClientSocket, ClientTransport) ->
|
||||
%% Wait for the cancel signal.
|
||||
receive cancel -> ok end,
|
||||
%% Then terminate the stream.
|
||||
ClientTransport:send(ClientSocket,
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"content-length: 0\r\n"
|
||||
"\r\n"
|
||||
),
|
||||
receive disconnect -> ok end
|
||||
end),
|
||||
{ok, Pid} = gun:open("localhost", OriginPort, #{
|
||||
event_handler => {gun_test_event_h, self()}
|
||||
}),
|
||||
{ok, http} = gun:await_up(Pid),
|
||||
{ok, undefined} = gun:stream_info(Pid, make_ref()),
|
||||
StreamRef = gun:get(Pid, "/"),
|
||||
Self = self(),
|
||||
{ok, #{
|
||||
ref := StreamRef,
|
||||
reply_to := Self,
|
||||
state := running
|
||||
}} = gun:stream_info(Pid, StreamRef),
|
||||
gun:cancel(Pid, StreamRef),
|
||||
{ok, #{
|
||||
ref := StreamRef,
|
||||
reply_to := Self,
|
||||
state := stopping
|
||||
}} = gun:stream_info(Pid, StreamRef),
|
||||
%% Cancel and wait for the stream to be canceled.
|
||||
OriginPid ! cancel,
|
||||
receive_event(Pid, cancel),
|
||||
fun F() ->
|
||||
case gun:stream_info(Pid, StreamRef) of
|
||||
{ok, undefined} -> ok;
|
||||
{ok, #{state := stopping}} -> F()
|
||||
end
|
||||
end(),
|
||||
%% Wait for the connection to terminate.
|
||||
OriginPid ! disconnect,
|
||||
receive_event(Pid, disconnect),
|
||||
{error, not_connected} = gun:stream_info(Pid, StreamRef),
|
||||
gun:close(Pid).
|
||||
|
||||
stream_info_http2(_) ->
|
||||
doc("Ensure the function gun:stream_info/2 works as expected for HTTP/2."),
|
||||
{ok, OriginPid, OriginPort} = init_origin(tcp, http2,
|
||||
fun(_, _, _, _) -> receive disconnect -> ok end end),
|
||||
{ok, Pid} = gun:open("localhost", OriginPort, #{
|
||||
event_handler => {gun_test_event_h, self()},
|
||||
protocols => [http2]
|
||||
}),
|
||||
{ok, http2} = gun:await_up(Pid),
|
||||
handshake_completed = receive_from(OriginPid),
|
||||
{ok, undefined} = gun:stream_info(Pid, make_ref()),
|
||||
StreamRef = gun:get(Pid, "/"),
|
||||
Self = self(),
|
||||
{ok, #{
|
||||
ref := StreamRef,
|
||||
reply_to := Self,
|
||||
state := running
|
||||
}} = gun:stream_info(Pid, StreamRef),
|
||||
gun:cancel(Pid, StreamRef),
|
||||
%% Wait for the connection to terminate.
|
||||
OriginPid ! disconnect,
|
||||
receive_event(Pid, disconnect),
|
||||
{error, not_connected} = gun:stream_info(Pid, StreamRef),
|
||||
gun:close(Pid).
|
||||
|
||||
supervise_false(_) ->
|
||||
doc("The supervise option allows starting without a supervisor."),
|
||||
{ok, _, OriginPort} = init_origin(tcp, http),
|
||||
{ok, Pid} = gun:open("localhost", OriginPort, #{supervise => false}),
|
||||
{ok, http} = gun:await_up(Pid),
|
||||
[] = [P || {_, P, _, _} <- supervisor:which_children(gun_sup), P =:= Pid],
|
||||
ok.
|
||||
|
||||
tls_handshake_error_gun_http2_init_retry_0(_) ->
|
||||
doc("Ensure an early TLS connection close is propagated "
|
||||
"to the user of the connection."),
|
||||
%% The server will immediately close the connection upon
|
||||
%% establishment so that the client's socket is down
|
||||
%% before it attempts to initialise HTTP/2.
|
||||
%%
|
||||
%% We use 'http' for the server to skip the HTTP/2 init
|
||||
%% but the client is connecting using 'http2'.
|
||||
{ok, _, OriginPort} = init_origin(tls, http,
|
||||
fun(_, _, ClientSocket, ClientTransport) ->
|
||||
ClientTransport:close(ClientSocket)
|
||||
end),
|
||||
{ok, ConnPid} = gun:open("localhost", OriginPort, #{
|
||||
event_handler => {gun_test_fun_event_h, #{
|
||||
tls_handshake_end => fun(_, #{socket := _}) ->
|
||||
%% We sleep right after the gun_tls:connect succeeds to make
|
||||
%% sure the next call to the socket fails (as the socket
|
||||
%% should be disconnected at that point by the server because
|
||||
%% we are not sending a certificate). The call we want to fail
|
||||
%% is the sending of the HTTP/2 preface in gun_http2:init.
|
||||
timer:sleep(1000)
|
||||
end
|
||||
}},
|
||||
protocols => [http2],
|
||||
retry => 0,
|
||||
transport => tls,
|
||||
tls_opts => [{verify, verify_none}]
|
||||
}),
|
||||
{error, {down, {shutdown, closed}}} = gun:await_up(ConnPid),
|
||||
gun:close(ConnPid).
|
||||
|
||||
tls_handshake_error_gun_http2_init_retry_1(_) ->
|
||||
doc("Ensure an early TLS connection close is propagated "
|
||||
"to the user of the connection and does not result "
|
||||
"in an infinite connect loop."),
|
||||
%% The server will immediately close the connection upon
|
||||
%% establishment so that the client's socket is down
|
||||
%% before it attempts to initialise HTTP/2.
|
||||
%%
|
||||
%% We use 'http' for the server to skip the HTTP/2 init
|
||||
%% but the client is connecting using 'http2'.
|
||||
%%
|
||||
%% We immediately accept another connection to handle
|
||||
%% the coming retry and close that connection again.
|
||||
{ok, _, OriginPort} = init_origin(tls, http,
|
||||
fun(_, ListenSocket, ClientSocket1, ClientTransport) ->
|
||||
ClientTransport:close(ClientSocket1),
|
||||
%% We immediately accept a second connection and close it again.
|
||||
{ok, ClientSocket2t} = ssl:transport_accept(ListenSocket, 5000),
|
||||
{ok, ClientSocket2} = ssl:handshake(ClientSocket2t, 5000),
|
||||
ClientTransport:close(ClientSocket2)
|
||||
end),
|
||||
{ok, ConnPid} = gun:open("localhost", OriginPort, #{
|
||||
event_handler => {gun_test_fun_event_h, #{
|
||||
tls_handshake_end => fun(_, #{socket := _}) ->
|
||||
%% See tls_handshake_error_gun_http2_init_retry_0 for details.
|
||||
timer:sleep(1000)
|
||||
end
|
||||
}},
|
||||
protocols => [http2],
|
||||
retry => 1,
|
||||
transport => tls,
|
||||
tls_opts => [{verify, verify_none}]
|
||||
}),
|
||||
{error, {down, {shutdown, closed}}} = gun:await_up(ConnPid),
|
||||
gun:close(ConnPid).
|
||||
|
||||
tls_handshake_timeout(_) ->
|
||||
doc("Ensure an integer value for tls_handshake_timeout is accepted."),
|
||||
do_timeout(tls_handshake_timeout, 1000).
|
||||
|
||||
tls_handshake_timeout_infinity(_) ->
|
||||
doc("Ensure infinity for tls_handshake_timeout is accepted."),
|
||||
do_timeout(tls_handshake_timeout, infinity).
|
||||
|
||||
transform_header_name(_) ->
|
||||
doc("The transform_header_name option allows changing the case of header names."),
|
||||
{ok, ListenSocket} = gen_tcp:listen(0, [binary, {active, false}]),
|
||||
{ok, {_, Port}} = inet:sockname(ListenSocket),
|
||||
{ok, Pid} = gun:open("localhost", Port, #{
|
||||
protocols => [http],
|
||||
http_opts => #{
|
||||
transform_header_name => fun(<<"host">>) -> <<"HOST">>; (N) -> N end
|
||||
}
|
||||
}),
|
||||
{ok, ClientSocket} = gen_tcp:accept(ListenSocket, 5000),
|
||||
{ok, http} = gun:await_up(Pid),
|
||||
_ = gun:get(Pid, "/"),
|
||||
{ok, Data} = gen_tcp:recv(ClientSocket, 0, 5000),
|
||||
%% We do some very crude parsing of the response headers
|
||||
%% to check that the header name was properly transformed.
|
||||
Lines = binary:split(Data, <<"\r\n">>, [global]),
|
||||
HostLines = [L || <<"HOST: ", _/bits>> = L <- Lines],
|
||||
1 = length(HostLines),
|
||||
gun:close(Pid).
|
||||
|
||||
unix_socket_connect(_) ->
|
||||
case os:type() of
|
||||
{win32, _} ->
|
||||
{skip, "Unix Domain Sockets are not available on Windows."};
|
||||
_ ->
|
||||
do_unix_socket_connect()
|
||||
end.
|
||||
|
||||
do_unix_socket_connect() ->
|
||||
doc("Ensure we can send data via a unix domain socket."),
|
||||
DataDir = "/tmp/gun",
|
||||
SocketPath = filename:join(DataDir, "gun.sock"),
|
||||
ok = filelib:ensure_dir(SocketPath),
|
||||
_ = file:delete(SocketPath),
|
||||
TCPOpts = [
|
||||
{ifaddr, {local, SocketPath}},
|
||||
binary, {nodelay, true}, {active, false},
|
||||
{packet, raw}, {reuseaddr, true}
|
||||
],
|
||||
{ok, LSock} = gen_tcp:listen(0, TCPOpts),
|
||||
Tester = self(),
|
||||
Acceptor = fun() ->
|
||||
{ok, S} = gen_tcp:accept(LSock),
|
||||
{ok, R} = gen_tcp:recv(S, 0),
|
||||
Tester ! {recv, R},
|
||||
ok = gen_tcp:close(S),
|
||||
ok = gen_tcp:close(LSock)
|
||||
end,
|
||||
spawn(Acceptor),
|
||||
{ok, Pid} = gun:open_unix(SocketPath, #{}),
|
||||
_ = gun:get(Pid, "/", [{<<"host">>, <<"localhost">>}]),
|
||||
receive
|
||||
{recv, _} ->
|
||||
gun:close(Pid)
|
||||
end.
|
||||
|
||||
uppercase_header_name(_) ->
|
||||
doc("Header names may be given with uppercase characters."),
|
||||
{ok, OriginPid, OriginPort} = init_origin(tcp, http),
|
||||
{ok, Pid} = gun:open("localhost", OriginPort),
|
||||
{ok, http} = gun:await_up(Pid),
|
||||
handshake_completed = receive_from(OriginPid),
|
||||
_ = gun:get(Pid, "/", [
|
||||
{<<"USER-agent">>, "Gun/uppercase-headers"}
|
||||
]),
|
||||
Data = receive_from(OriginPid),
|
||||
Lines = binary:split(Data, <<"\r\n">>, [global]),
|
||||
[<<"user-agent: Gun/uppercase-headers">>] = [L || <<"user-agent: ", _/bits>> = L <- Lines],
|
||||
gun:close(Pid).
|
|
@ -0,0 +1,22 @@
|
|||
%% Copyright (c) 2015-2023, Loïc Hoguin <essen@ninenines.eu>
|
||||
%%
|
||||
%% Permission to use, copy, modify, and/or distribute this software for any
|
||||
%% purpose with or without fee is hereby granted, provided that the above
|
||||
%% copyright notice and this permission notice appear in all copies.
|
||||
%%
|
||||
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
-module(gun_ct_hook).
|
||||
|
||||
-export([init/2]).
|
||||
|
||||
init(_, _) ->
|
||||
ct_helper:start([cowboy, gun]),
|
||||
ct_helper:make_certs_in_ets(),
|
||||
{ok, undefined}.
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue