Showing preview only (1,189K chars total). Download the full file or copy to clipboard to get everything.
Repository: joedevivo/chatterbox
Branch: master
Commit: c0506c707bd1
Files: 122
Total size: 1.1 MB
Directory structure:
gitextract_bky7wddq/
├── .gitignore
├── .travis.yml
├── LICENSE
├── Makefile
├── README.md
├── config/
│ ├── localhost.crt
│ ├── localhost.key
│ ├── localhost.key.pub
│ └── sys.config
├── include/
│ └── http2.hrl
├── priv/
│ └── index.html
├── rebar.config
├── rebar.config.script
├── src/
│ ├── chatterbox.app.src
│ ├── chatterbox.erl
│ ├── chatterbox_ranch_protocol.erl
│ ├── chatterbox_static_content_handler.erl
│ ├── chatterbox_static_stream.erl
│ ├── chatterbox_sup.erl
│ ├── h2_client.erl
│ ├── h2_connection.erl
│ ├── h2_frame.erl
│ ├── h2_frame_continuation.erl
│ ├── h2_frame_data.erl
│ ├── h2_frame_goaway.erl
│ ├── h2_frame_headers.erl
│ ├── h2_frame_ping.erl
│ ├── h2_frame_priority.erl
│ ├── h2_frame_push_promise.erl
│ ├── h2_frame_rst_stream.erl
│ ├── h2_frame_settings.erl
│ ├── h2_frame_window_update.erl
│ ├── h2_padding.erl
│ ├── h2_settings.erl
│ ├── h2_stream.erl
│ ├── h2_stream_set.erl
│ └── sock.erl
└── test/
├── chatterbox_test_buddy.erl
├── chatterbox_tests.erl
├── client_server_SUITE.erl
├── client_server_SUITE_data/
│ ├── README.md
│ ├── bower.json
│ ├── css/
│ │ ├── print/
│ │ │ ├── paper.css
│ │ │ └── pdf.css
│ │ ├── reveal.css
│ │ ├── reveal.scss
│ │ └── theme/
│ │ ├── README.md
│ │ ├── beige.css
│ │ ├── black.css
│ │ ├── blood.css
│ │ ├── league.css
│ │ ├── moon.css
│ │ ├── night.css
│ │ ├── serif.css
│ │ ├── simple.css
│ │ ├── sky.css
│ │ ├── solarized.css
│ │ ├── source/
│ │ │ ├── beige.scss
│ │ │ ├── black.scss
│ │ │ ├── blood.scss
│ │ │ ├── league.scss
│ │ │ ├── moon.scss
│ │ │ ├── night.scss
│ │ │ ├── serif.scss
│ │ │ ├── simple.scss
│ │ │ ├── sky.scss
│ │ │ ├── solarized.scss
│ │ │ └── white.scss
│ │ ├── template/
│ │ │ ├── mixins.scss
│ │ │ ├── settings.scss
│ │ │ └── theme.scss
│ │ └── white.css
│ ├── index.html
│ ├── js/
│ │ └── reveal.js
│ ├── lib/
│ │ ├── css/
│ │ │ └── zenburn.css
│ │ ├── font/
│ │ │ ├── league-gothic/
│ │ │ │ ├── LICENSE
│ │ │ │ └── league-gothic.css
│ │ │ └── source-sans-pro/
│ │ │ ├── LICENSE
│ │ │ └── source-sans-pro.css
│ │ └── js/
│ │ ├── classList.js
│ │ └── html5shiv.js
│ └── plugin/
│ ├── highlight/
│ │ └── highlight.js
│ ├── markdown/
│ │ ├── example.html
│ │ ├── example.md
│ │ ├── markdown.js
│ │ └── marked.js
│ ├── math/
│ │ └── math.js
│ ├── multiplex/
│ │ ├── client.js
│ │ ├── index.js
│ │ └── master.js
│ ├── notes/
│ │ ├── notes.html
│ │ └── notes.js
│ ├── notes-server/
│ │ ├── client.js
│ │ ├── index.js
│ │ └── notes.html
│ ├── print-pdf/
│ │ └── print-pdf.js
│ ├── search/
│ │ └── search.js
│ └── zoom-js/
│ └── zoom.js
├── double_body_handler.erl
├── echo_handler.erl
├── flow_control_SUITE.erl
├── flow_control_handler.erl
├── header_continuation_SUITE.erl
├── http2_frame_size_SUITE.erl
├── http2_spec_3_5_SUITE.erl
├── http2_spec_4_3_SUITE.erl
├── http2_spec_5_1_SUITE.erl
├── http2_spec_5_3_SUITE.erl
├── http2_spec_5_5_SUITE.erl
├── http2_spec_6_1_SUITE.erl
├── http2_spec_6_2_SUITE.erl
├── http2_spec_6_4_SUITE.erl
├── http2_spec_6_5_SUITE.erl
├── http2_spec_6_9_SUITE.erl
├── http2_spec_8_1_SUITE.erl
├── http2c.erl
├── peer_test_handler.erl
├── protocol_errors_SUITE.erl
├── server_connection_receive_window.erl
├── server_stream_receive_window.erl
├── settings_handshake_SUITE.erl
└── starting_SUITE.erl
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
ebin/*
*.beam
deps/*
rel/chatterbox
.eunit
_rel
relx
.rebar
erln8.config
.DS_Store
common_test/logs
.edts
erl_crash.dump
_build
rebar3
.rebar3
log
.*.sw?
================================================
FILE: .travis.yml
================================================
language: erlang
otp_release:
- 21.0
- 20.0
- 19.3
before_script: kerl list installations
before_install:
- wget https://github.com/erlang/rebar3/releases/download/3.6.1/rebar3 -O rebar3
- chmod +x rebar3
install: make clean
script: make all
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2015 Joe DeVivo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
REBAR3_URL=https://s3.amazonaws.com/rebar3/rebar3
# If there is a rebar in the current directory, use it
ifeq ($(wildcard rebar3),rebar3)
REBAR3 = $(CURDIR)/rebar3
endif
# Fallback to rebar on PATH
REBAR3 ?= $(shell which rebar3)
# And finally, prep to download rebar if all else fails
ifeq ($(REBAR3),)
REBAR3 = $(CURDIR)/rebar3
endif
clean: $(REBAR3)
@$(REBAR3) clean
rm -rf _build
all: $(REBAR3)
@$(REBAR3) do clean, compile, eunit, ct, dialyzer
rel: all
@$(REBAR3) release
$(REBAR3):
curl -Lo rebar3 $(REBAR3_URL) || wget $(REBAR3_URL)
chmod a+x rebar3
================================================
FILE: README.md
================================================
# chatterbox #
Chatterbox is an HTTP/2 library for Erlang. Use as much of it as you
want, but the goal is to implement as much of
[RFC-7540](https://tools.ietf.org/html/rfc7540) as possible, 100% of
the time.
It already pulls in
[joedevivo/hpack](https://github.com/joedevivo/hpack) for the
implementation of [RFC-7541](https://tools.ietf.org/html/rfc7541).
## Rebar3
Chatterbox is a `rebar3` jam. Get into it! rebar3.org
## HTTP/2 Connections
Chatterbox provides a module `h2_connection` which models (you
guessed it!) an HTTP/2 connection. This gen_statem can represent either
side of an HTTP/2 connection (i.e. a client *or* server). Really, the
only difference is in how you start the connection.
### Server Side Connections
A server side connection can be started with
[ninenines/ranch](https://github.com/ninenines/ranch) using the
included `chatterbox_ranch_protocol` like this:
```erlang
%% Set up the socket options:
Options = [
{port, 8080},
%% you can find certs to play with in ./config
{certfile, "localhost.crt"},
{keyfile, "localhost.key"},
{honor_cipher_order, false},
{versions, ['tlsv1.2']},
{next_protocols_advertised, [<<"h2">>]}
],
%% You'll also have to set the content root for the chatterbox static
%% content handler
RootDir = "/path/to/content",
application:set_env(
chatterbox,
stream_callback_mod,
chatterbox_static_stream),
application:set_env(
chatterbox,
chatterbox_static_stream,
[{root_dir, RootDir}]),
{ok, _RanchPid} =
ranch:start_listener(
chatterbox_ranch_protocol,
10,
ranch_ssl,
Options,
chatterbox_ranch_protocol,
[]),
```
You can do this in a `rebar3 shell` or in your application.
You don't have to use ranch. You can use see chatterbox_sup for an
alternative.
### Serving up the EUC 2015 Deck
clone chatterbox and [euc2015](https://github.com/joedevivo/euc2015)
then set the `RootDir` above to the checkout of euc2015.
Then it should be as easy as pointing Firefox to
`https://localhost:8080/`.
### Client Side Connections
We'll start up h2_connection a little
differently. `h2_client:start_link/*` will take care of the
differences.
Here's how to use it!
```erlang
{ok, Pid} = h2_client:start_link(),
RequestHeaders = [
{<<":method">>, <<"GET">>},
{<<":path">>, <<"/index.html">>},
{<<":scheme">>, <<"http">>},
{<<":authority">>, <<"localhost:8080">>},
{<<"accept">>, <<"*/*">>},
{<<"accept-encoding">>, <<"gzip, deflate">>},
{<<"user-agent">>, <<"chatterbox-client/0.0.1">>}
],
RequestBody = <<>>,
{ok, {ResponseHeaders, ResponseBody}}
= h2_client:sync_request(Pid, RequestHeaders, RequestBody).
```
But wait! There's more. If you want to be smart about multiplexing,
you can make an async request on a stream like this!
``` erlang
{ok, StreamId} = h2_client:send_request(Pid, RequestHeaders, <<>>).
```
Now you can just hang around and wait. I'm pretty sure (read:
UNTESTED) that you'll have a process message like `{'END_STREAM',
StreamId}` waiting for you in your process mailbox, since that's how
`sync_request/3` works, and they use mostly the same codepath.
So you can use a receive block, like this
```erlang
receive
{'END_STREAM', StreamId} ->
{ok, {ResponseHeaders, ResponseBody}} = h2_client:get_response(Pid, StreamId)
end,
```
Or you can manually check for a response by calling
```erlang
h2_client:get_response(Pid, StreamId),
```
which will return an `{error, stream_not_finished}` if
it's... well... not finished.
There still needs to be some way of detecting that a promise has been
pushed from the server, either actively or passively. In theory, if
you knew the `StreamId`, you could check for a response on that stream
and it would be there.
## Author(s) ##
* Joe DeVivo
## Copyright ##
Copyright (c) 2015,16 Joe DeVivo, dat MIT License tho.
================================================
FILE: config/localhost.crt
================================================
-----BEGIN CERTIFICATE-----
MIIDCDCCAfACCQCyOlNYMIzE4TANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJV
UzEQMA4GA1UECBMHQXJpem9uYTEQMA4GA1UEBxMHUGhvZW5peDETMBEGA1UEChMK
Q2hhdHRlcmJveDAeFw0xNTA2MDcxODIzMjNaFw00NTA3MTkxODIzMjNaMEYxCzAJ
BgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRAwDgYDVQQHEwdQaG9lbml4MRMw
EQYDVQQKEwpDaGF0dGVyYm94MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
AQEAxa9PMgPb7zJmHGswAjZ2h9JvAm9DrRnMcVeQFz+8VbL27gABZ4l+cwK0KbGg
WKL5bXJID51XtRs3wtHhX1NYJ7J2tW44fBDczFAYxUUFr9Ts6KM7Kx1Pzq1HAtaW
Rc+shXBgV+Q7gVAMnWPagiD2IkOglpA2CJOSSdbY3QzP0Q49TEkO4xcJEBy0MtqA
3R3pb1nisEX5K2UJ/FBPPpozR3aOxGMcF/ULze8sBCxOUDFSjiptzBUV/FCh3F7y
+V35xcTLXG7P42pNIDVWUC4tAbvmXgnryoz1qj+vVUoh+dIUDCOggCLydrRhlKSv
kvcqZsVBmn8ore09gY/YAH136wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQCwNhg8
rI9cE9+05DTX7b9MyZNtFteFwPO8xfBmZVee0gbwqEdgVoDg+T1/KvzmEeKEim9S
JHLn8ZyCkQA4Kj+J8yPpfZC3zw3ovHrZhHtDQIE3UJSx1rrF5h3VSTT76p5z4HNM
Cm/leFGDqXtELjONsF//+SFQXP6jopzPqBnXxQ54+1osoi/fmZSg/NGImJA+Q+RR
fCeGkEZpA8Hapq8vv7OKIGNNc87EuBRUc1OoQ8xUXp2GQV4UcVyA26BV4kGfeYOF
7D3aj/Ugtp8vOLCD72GzDBe3ng0u1kkIgYgwRuiYqF9qsMOtjQInw9OHO9aT9IXS
FkWoN9NOLvEaBUaO
-----END CERTIFICATE-----
================================================
FILE: config/localhost.key
================================================
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAxa9PMgPb7zJmHGswAjZ2h9JvAm9DrRnMcVeQFz+8VbL27gAB
Z4l+cwK0KbGgWKL5bXJID51XtRs3wtHhX1NYJ7J2tW44fBDczFAYxUUFr9Ts6KM7
Kx1Pzq1HAtaWRc+shXBgV+Q7gVAMnWPagiD2IkOglpA2CJOSSdbY3QzP0Q49TEkO
4xcJEBy0MtqA3R3pb1nisEX5K2UJ/FBPPpozR3aOxGMcF/ULze8sBCxOUDFSjipt
zBUV/FCh3F7y+V35xcTLXG7P42pNIDVWUC4tAbvmXgnryoz1qj+vVUoh+dIUDCOg
gCLydrRhlKSvkvcqZsVBmn8ore09gY/YAH136wIDAQABAoIBAQCvScPvlXxvnUDt
8h2f2KtBxFaq0f4wf6/I0NvzwZA+bXKHl3mnVUPKt5sJXXfRILILWkqLjfk3nerT
1UcCP+TrTlP0jMeJO2qNwHg2c/2W7DcmEZdlo5ggq4VL/vtA6+UObZcAWGBrSY/l
/6TBvniB6XV8DGPdNv2AfAHQAIxF0clLcLraUcyN9oOzLXj8uXsOfAf0mR7uyARY
hL2bZ2nUkLH1DDYXgCwdeW2Vs/M8F93GIxzXbVuIKVQNzvDQ9AQoYWaaLVEN+TWY
tQGkuAL5sQNDdABVxtrzVKBezrcTAkRDyYSs/DXbXEUakRoOCIZfmVmUaGqVIONM
ie9U8rgBAoGBAPpeHxmEqHA9GPehkeojn9x8MOSpYcjasVglkVrOeG24zUzKFL65
aM6Uhrtn2ZIurWIpyuyHf964uPeuZmCqW5hp2dnmsZ9Zjc857B10Yw768eUT8vO0
NHSJAiKx2ZonkHX6IuffCwmTg9wlzctTtiiGLF69thaX8ayjSoZJdQV9AoGBAMoh
yPGnriTOxaHhs4fgvc4K4dhQnQ3iSt7OP2vWmCmpwFkSR2h9Um0F7geptV+5tpLw
Nefq4a25a9cL+/X5a3NHK7kXe98UP2+5RSisvNhzY2SVH7FHFuFL4q2uOk1Pv4qX
BFlgPmG8M8LvlmC6BtaQYxDmFKJy+MelZuNXTk+HAoGAA2Zn0bblerC5uBMvohhd
wWbGWzSZqVqe8e2ArdUD+al60EImSfjGnZeSxNTCNaQAosaihNfKOsITcPmjVki5
+bXmSXlCjEFxFZFZzYSZG8j4o/3DXN/jnnmF1+bGZ7uF0LRW6QM0aSrhrYmt48b9
QEuiKp8069WgaJHHH0+8ERkCgYEAlw6Sjk4CrY09UxJKSeRh1GZ7i14LUQHpdALs
kJmp05EBp08qwGLPw5wn2+AvJJ+0WrFbh7sX9u1YMzjIjnVcoKTyfvuW3grSsZri
nVgiNRxejh+HtMNszOgaOjO3bGmJunfLj0OGuyGcCTVly1manKUA8/MOPqzvULxC
XOm1I2cCgYBo0TVdz/Hi+nzwG8vEgXcH5a0bXALvXAyum5YFkTLYaPNQldw7KqUD
pWDgA9mhd+cwCodHk2TMfi88vCsZhZyeR/HCwqiWlJcuOlvTXodZr8nozhH2Kivc
9ks9/8Iy5TFL63tMWoYCMraoAocrVWekA1C+2AjbZmyq6ptKhohGqA==
-----END RSA PRIVATE KEY-----
================================================
FILE: config/localhost.key.pub
================================================
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFr08yA9vvMmYcazACNnaH0m8Cb0OtGcxxV5AXP7xVsvbuAAFniX5zArQpsaBYovltckgPnVe1GzfC0eFfU1gnsna1bjh8ENzMUBjFRQWv1OzoozsrHU/OrUcC1pZFz6yFcGBX5DuBUAydY9qCIPYiQ6CWkDYIk5JJ1tjdDM/RDj1MSQ7jFwkQHLQy2oDdHelvWeKwRfkrZQn8UE8+mjNHdo7EYxwX9QvN7ywELE5QMVKOKm3MFRX8UKHcXvL5XfnFxMtcbs/jak0gNVZQLi0Bu+ZeCevKjPWqP69VSiH50hQMI6CAIvJ2tGGUpK+S9ypmxUGafyit7T2Bj9gAfXfr root@remdevivo01.local
================================================
FILE: config/sys.config
================================================
[
{chatterbox,
[
{port, 8081},
{ssl, true},
{ssl_options, [{certfile, "localhost.crt"},
{keyfile, "localhost.key"},
{honor_cipher_order, false},
{versions, ['tlsv1.2']},
{alpn_preferred_protocols, [<<"h2">>]}]},
{chatterbox_static_content_handler, [
{root_dir, "/home/joe/dev/joedevivo/euc2015"}
]},
{chatterbox_static_stream,
[
{root_dir, "/home/joe/dev/joedevivo/euc2015"}
]}
]}
].
================================================
FILE: include/http2.hrl
================================================
%% FRAME TYPES
-define(DATA , 16#0).
-define(HEADERS , 16#1).
-define(PRIORITY , 16#2).
-define(RST_STREAM , 16#3).
-define(SETTINGS , 16#4).
-define(PUSH_PROMISE , 16#5).
-define(PING , 16#6).
-define(GOAWAY , 16#7).
-define(WINDOW_UPDATE , 16#8).
-define(CONTINUATION , 16#9).
-type frame_type() :: ?DATA
| ?HEADERS
| ?PRIORITY
| ?RST_STREAM
| ?SETTINGS
| ?PUSH_PROMISE
| ?PING
| ?GOAWAY
| ?WINDOW_UPDATE
| ?CONTINUATION
| integer(). %% Currently unsupported future frame types
-define(FT, fun(?DATA) -> "DATA";
(?HEADERS) -> "HEADERS";
(?PRIORITY) -> "PRIORITY";
(?RST_STREAM) -> "RST_STREAM";
(?SETTINGS) -> "SETTINGS";
(?PUSH_PROMISE) -> "PUSH_PROMISE";
(?PING) -> "PING";
(?GOAWAY) -> "GOAWAY";
(?WINDOW_UPDATE) -> "WINDOW_UPDATE";
(?CONTINUATION) -> "CONTINUATION";
(_) -> "UNSUPPORTED EXPANSION type" end
).
%% ERROR CODES
-define(NO_ERROR, 16#0).
-define(PROTOCOL_ERROR, 16#1).
-define(INTERNAL_ERROR, 16#2).
-define(FLOW_CONTROL_ERROR, 16#3).
-define(SETTINGS_TIMEOUT, 16#4).
-define(STREAM_CLOSED, 16#5).
-define(FRAME_SIZE_ERROR, 16#6).
-define(REFUSED_STREAM, 16#7).
-define(CANCEL, 16#8).
-define(COMPRESSION_ERROR, 16#9).
-define(CONNECT_ERROR, 16#a).
-define(ENHANCE_YOUR_CALM, 16#b).
-define(INADEQUATE_SECURITY,16#c).
-define(HTTP_1_1_REQUIRED, 16#d).
-type error_code() :: ?NO_ERROR
| ?PROTOCOL_ERROR
| ?INTERNAL_ERROR
| ?FLOW_CONTROL_ERROR
| ?SETTINGS_TIMEOUT
| ?STREAM_CLOSED
| ?FRAME_SIZE_ERROR
| ?REFUSED_STREAM
| ?CANCEL
| ?COMPRESSION_ERROR
| ?CONNECT_ERROR
| ?ENHANCE_YOUR_CALM
| ?INADEQUATE_SECURITY
| ?HTTP_1_1_REQUIRED.
%% FLAGS
-define(FLAG_ACK, 16#1 ).
-define(FLAG_END_STREAM, 16#1 ).
-define(FLAG_END_HEADERS, 16#4 ).
-define(FLAG_PADDED, 16#8 ).
-define(FLAG_PRIORITY, 16#20).
-type flag() :: ?FLAG_ACK
| ?FLAG_END_STREAM
| ?FLAG_END_HEADERS
| ?FLAG_PADDED
| ?FLAG_PRIORITY.
%% These are macros because they're used in guards a lot
-define(IS_FLAG(Flags, Flag), Flags band Flag =:= Flag).
-define(NOT_FLAG(Flags, Flag), Flags band Flag =/= Flag).
-type stream_id() :: non_neg_integer().
-record(frame_header, {
length :: non_neg_integer() | undefined,
type :: frame_type() | undefined,
flags = 0 :: non_neg_integer(),
stream_id :: stream_id()
}).
-type transport() :: gen_tcp | ssl.
-type socket() :: {gen_tcp, inet:socket()|undefined} | {ssl, ssl:sslsocket()|undefined}.
-define(PREFACE, "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n").
-define(DEFAULT_INITIAL_WINDOW_SIZE, 65535).
%% Settings are too big to be part of the data type refactor. We'll
%% get to it next
-record(settings, {header_table_size = 4096,
enable_push = 1,
max_concurrent_streams = unlimited,
initial_window_size = 65535,
max_frame_size = 16384,
max_header_list_size = unlimited}).
-type settings() :: #settings{}.
-define(SETTINGS_HEADER_TABLE_SIZE, <<16#1>>).
-define(SETTINGS_ENABLE_PUSH, <<16#2>>).
-define(SETTINGS_MAX_CONCURRENT_STREAMS, <<16#3>>).
-define(SETTINGS_INITIAL_WINDOW_SIZE, <<16#4>>).
-define(SETTINGS_MAX_FRAME_SIZE, <<16#5>>).
-define(SETTINGS_MAX_HEADER_LIST_SIZE, <<16#6>>).
-define(SETTING_NAMES, [?SETTINGS_HEADER_TABLE_SIZE,
?SETTINGS_ENABLE_PUSH,
?SETTINGS_MAX_CONCURRENT_STREAMS,
?SETTINGS_INITIAL_WINDOW_SIZE,
?SETTINGS_MAX_FRAME_SIZE,
?SETTINGS_MAX_HEADER_LIST_SIZE]).
-type setting_name() :: binary().
-type settings_property() :: {setting_name(), any()}.
-type settings_proplist() :: [settings_property()].
================================================
FILE: priv/index.html
================================================
<!doctype html>
<html lang="en">
<head>
<title>Chatterbox</title>
</head>
<body>
Chatterbox
</body>
</html>
================================================
FILE: rebar.config
================================================
%% -*- mode: erlang -*-
%% -*- tab-width: 4;erlang-indent-level: 4;indent-tabs-mode: nil -*-
%% ex: ts=4 sw=4 ft=erlang et
{erl_opts, [
warnings_as_errors,
debug_info
]}.
{deps,
[{hpack, {pkg, hpack_erl}}]}.
{cover_enabled, true}.
{ct_opts, [{verbose,true}]}.
{dialyzer, [unknown]}.
{profiles,
[
{test, [
{erl_opts,
[
{i,["include"]},
nowarn_export_all
]},
{deps,
[{ranch, "1.2.1"}]}
]
}]
}.
{relx, [
{release,{chatterbox,"0.0.1"},
[chatterbox]},
{sys_config, "./config/sys.config"},
%%{vm_args, "./config/vm.args"},
{dev_mode, true},
{include_erts, false},
{extended_start_script, true},
{overlay,[
{template,"config/sys.config","sys.config"},
{copy,"config/localhost.crt","."},
{copy,"config/localhost.key","."}
]}
]}.
================================================
FILE: rebar.config.script
================================================
case erlang:function_exported(rebar3, main, 1) of
true -> % rebar3
CONFIG;
false -> % rebar 2.x or older
%% Rebuild deps, possibly including those that have been moved to
%% profiles
[{deps, [
{hpack, ".*", {git, "https://github.com/joedevivo/hpack.git", {tag, "0.2.3"}}}
]} | lists:keydelete(deps, 1, CONFIG)]
end.
================================================
FILE: src/chatterbox.app.src
================================================
%% -*- mode: erlang -*-
{application, chatterbox,
[
{description, "chatterbox library for http2"},
{vsn, git},
{registered, []},
{applications, [
kernel,
stdlib,
crypto,
public_key,
ssl,
hpack
]},
{env,
[
{port, 80},
{concurrent_acceptors, 20},
%% These are the defaults for ALL HTTP/2 clients. You can change
%% any of them that you want to, but these will be the ones used
%% by default if you don't.
%% I wanted them to be one proplist, but I realized that will be
%% annoying for people that only want to change one default
%% setting.
{client_header_table_size, 4096},
{client_enable_push, 1},
{client_max_concurrent_streams, unlimited},
{client_initial_window_size, 65535},
{client_max_frame_size, 16384},
{client_max_header_list_size, unlimited},
{client_flow_control, auto},
%% Same for server
{server_header_table_size, 4096},
{server_enable_push, 1},
{server_max_concurrent_streams, unlimited},
{server_initial_window_size, 65535},
{server_max_frame_size, 16384},
{server_max_header_list_size, unlimited},
{server_flow_control, auto}
]},
{maintainers, ["Joe DeVivo"]},
{licenses, ["MIT"]},
{links, [{"Github", "https://github.com/joedevivo/chatterbox"}]}
]}.
%% vim: set filetype=erlang tabstop=2
================================================
FILE: src/chatterbox.erl
================================================
-module(chatterbox).
-include("http2.hrl").
-export([
start/0,
settings/0,
settings/1,
settings/2
]).
start() ->
chatterbox_sup:start_link().
settings() ->
settings(server).
settings(server, Settings=#{}) ->
HTS = maps:get(server_header_table_size, Settings, 4096),
EP = maps:get(server_enable_push, Settings, 1),
MCS = maps:get(server_max_concurrent_streams, Settings, unlimited),
IWS = maps:get(server_initial_window_size, Settings, 65535),
MFS = maps:get(server_max_frame_size, Settings, 16384),
MHLS = maps:get(server_max_header_list_size, Settings, unlimited),
#settings{
header_table_size=HTS,
enable_push=EP,
max_concurrent_streams=MCS,
initial_window_size=IWS,
max_frame_size=MFS,
max_header_list_size=MHLS
}.
settings(server) ->
HTS = application:get_env(?MODULE, server_header_table_size, 4096),
EP = application:get_env(?MODULE, server_enable_push, 1),
MCS = application:get_env(?MODULE, server_max_concurrent_streams, unlimited),
IWS = application:get_env(?MODULE, server_initial_window_size, 65535),
MFS = application:get_env(?MODULE, server_max_frame_size, 16384),
MHLS = application:get_env(?MODULE, server_max_header_list_size, unlimited),
#settings{
header_table_size=HTS,
enable_push=EP,
max_concurrent_streams=MCS,
initial_window_size=IWS,
max_frame_size=MFS,
max_header_list_size=MHLS
};
settings(client) ->
HTS = application:get_env(?MODULE, client_header_table_size, 4096),
EP = application:get_env(?MODULE, client_enable_push, 1),
MCS = application:get_env(?MODULE, client_max_concurrent_streams, unlimited),
IWS = application:get_env(?MODULE, client_initial_window_size, 65535),
MFS = application:get_env(?MODULE, client_max_frame_size, 16384),
MHLS = application:get_env(?MODULE, client_max_header_list_size, unlimited),
#settings{
header_table_size=HTS,
enable_push=EP,
max_concurrent_streams=MCS,
initial_window_size=IWS,
max_frame_size=MFS,
max_header_list_size=MHLS
}.
================================================
FILE: src/chatterbox_ranch_protocol.erl
================================================
-module(chatterbox_ranch_protocol).
%% While it implements the behaviour, uncommenting the line below
%% would fail to compile unless I make ranch a dependency of
%% chatterbox, which I don't plan on
%%-behaviour(ranch_protocol).
-export([
start_link/4,
init/4
]).
start_link(Ref, Socket, Transport, Opts) ->
Pid = proc_lib:spawn_link(?MODULE, init, [Ref, Socket, Transport, Opts]),
{ok, Pid}.
init(Ref, Socket, T, Opts) ->
ok = ranch:accept_ack(Ref),
Http2Settings = proplists:get_value(http2_settings, Opts, chatterbox:settings(server)),
h2_connection:become({transport(T), Socket}, Http2Settings).
transport(ranch_ssl) ->
ssl;
transport(ranch_tcp) ->
gen_tcp;
transport(tcp) ->
gen_tcp;
transport(gen_tcp) ->
gen_tcp;
transport(ssl) ->
ssl;
transport(_Other) ->
error(unknown_protocol).
================================================
FILE: src/chatterbox_static_content_handler.erl
================================================
-module(chatterbox_static_content_handler).
-include("http2.hrl").
-export([
spawn_handle/4,
handle/4
]).
-spec spawn_handle(
pid(),
stream_id(), %% Stream Id
hpack:headers(), %% Decoded Request Headers
iodata() %% Request Body
) -> pid().
spawn_handle(Pid, StreamId, Headers, ReqBody) ->
Handler = fun() ->
handle(Pid, StreamId, Headers, ReqBody)
end,
spawn_link(Handler).
-spec handle(
pid(),
stream_id(),
hpack:headers(),
iodata()
) -> ok.
handle(ConnPid, StreamId, Headers, _ReqBody) ->
Path = binary_to_list(proplists:get_value(<<":path">>, Headers)),
%% QueryString Hack?
Path2 = case string:chr(Path, $?) of
0 -> Path;
X -> string:substr(Path, 1, X-1)
end,
%% Dot Hack
Path3 = case Path2 of
[$.|T] -> T;
Other -> Other
end,
Path4 = case Path3 of
[$/|T2] -> [$/|T2];
Other2 -> [$/|Other2]
end,
%% TODO: Should have a better way of extracting root_dir (i.e. not on every request)
StaticHandlerSettings = application:get_env(chatterbox, ?MODULE, []),
RootDir = proplists:get_value(root_dir, StaticHandlerSettings, code:priv_dir(chatterbox)),
%% TODO: Logic about "/" vs "index.html", "index.htm", etc...
%% Directory browsing?
File = RootDir ++ Path4,
case {filelib:is_file(File), filelib:is_dir(File)} of
{_, true} ->
ResponseHeaders = [
{<<":status">>,<<"403">>}
],
h2_connection:send_headers(ConnPid, StreamId, ResponseHeaders),
h2_connection:send_body(ConnPid, StreamId, <<"No soup for you!">>),
ok;
{true, false} ->
Ext = filename:extension(File),
MimeType = case Ext of
".js" -> <<"text/javascript">>;
".html" -> <<"text/html">>;
".css" -> <<"text/css">>;
".scss" -> <<"text/css">>;
".woff" -> <<"application/font-woff">>;
".ttf" -> <<"application/font-snft">>;
_ -> <<"unknown">>
end,
{ok, Data} = file:read_file(File),
ResponseHeaders = [
{<<":status">>, <<"200">>},
{<<"content-type">>, MimeType}
],
h2_connection:send_headers(ConnPid, StreamId, ResponseHeaders),
case {MimeType, h2_connection:is_push(ConnPid)} of
{<<"text/html">>, true} ->
%% Search Data for resources to push
{ok, RE} = re:compile("<link rel=\"stylesheet\" href=\"([^\"]*)|<script src=\"([^\"]*)|src: '([^']*)"),
Resources = case re:run(Data, RE, [global, {capture,all,binary}]) of
{match, Matches} ->
[dot_hack(lists:last(M)) || M <- Matches];
_ -> []
end,
NewStreams =
lists:foldl(fun(R, Acc) ->
NewStreamId = h2_connection:new_stream(ConnPid),
PHeaders = generate_push_promise_headers(Headers, <<$/,R/binary>>
),
h2_connection:send_promise(ConnPid, StreamId, NewStreamId, PHeaders),
[{NewStreamId, PHeaders}|Acc]
end,
[],
Resources
),
[spawn_handle(ConnPid, NewStreamId, PHeaders, <<>>) || {NewStreamId, PHeaders} <- NewStreams],
ok;
_ ->
ok
end,
h2_connection:send_body(ConnPid, StreamId, Data),
ok;
{false, false} ->
ResponseHeaders = [
{<<":status">>,<<"404">>}
],
h2_connection:send_headers(ConnPid, StreamId, ResponseHeaders),
h2_connection:send_body(ConnPid, StreamId, <<"No soup for you!">>),
ok
end,
ok.
-spec generate_push_promise_headers(hpack:headers(), binary()) -> hpack:headers().
generate_push_promise_headers(Request, Path) ->
[
{<<":path">>, Path},{<<":method">>, <<"GET">>}|
lists:filter(fun({<<":authority">>,_}) -> true;
({<<":scheme">>, _}) -> true;
(_) -> false end, Request)
].
-spec dot_hack(binary()) -> binary().
dot_hack(<<$.,Bin/binary>>) ->
Bin;
dot_hack(Bin) -> Bin.
================================================
FILE: src/chatterbox_static_stream.erl
================================================
-module(chatterbox_static_stream).
-include("http2.hrl").
-behaviour(h2_stream).
-export([
init/3,
on_receive_request_headers/2,
on_send_push_promise/2,
on_receive_request_data/2,
on_request_end_stream/1
]).
-record(cb_static, {
req_headers=[],
connection_pid :: pid(),
stream_id :: stream_id()
}).
init(ConnPid, StreamId, _) ->
%% You need to pull settings here from application:env or something
{ok, #cb_static{connection_pid=ConnPid,
stream_id=StreamId}}.
on_receive_request_headers(Headers, State) ->
{ok, State#cb_static{req_headers=Headers}}.
on_send_push_promise(Headers, State) ->
{ok, State#cb_static{req_headers=Headers}}.
on_receive_request_data(_Bin, State)->
{ok, State}.
on_request_end_stream(State=#cb_static{connection_pid=ConnPid,
stream_id=StreamId}) ->
Headers = State#cb_static.req_headers,
Method = proplists:get_value(<<":method">>, Headers),
Path = binary_to_list(proplists:get_value(<<":path">>, Headers)),
%% QueryString Hack?
Path2 = case string:chr(Path, $?) of
0 -> Path;
X -> string:substr(Path, 1, X-1)
end,
%% Dot Hack
Path3 = case Path2 of
[$.|T] -> T;
Other -> Other
end,
Path4 = case Path3 of
[$/|T2] -> [$/|T2];
Other2 -> [$/|Other2]
end,
%% TODO: Should have a better way of extracting root_dir (i.e. not on every request)
StaticHandlerSettings = application:get_env(chatterbox, ?MODULE, []),
RootDir = proplists:get_value(root_dir, StaticHandlerSettings, code:priv_dir(chatterbox)),
%% TODO: Logic about "/" vs "index.html", "index.htm", etc...
%% Directory browsing?
File = RootDir ++ Path4,
{HeadersToSend, BodyToSend} =
case {filelib:is_file(File), filelib:is_dir(File)} of
{_, true} ->
ResponseHeaders = [
{<<":status">>,<<"403">>}
],
{ResponseHeaders, <<"No soup for you!">>};
{true, false} ->
Ext = filename:extension(File),
MimeType = case Ext of
".js" -> <<"text/javascript">>;
".html" -> <<"text/html">>;
".css" -> <<"text/css">>;
".scss" -> <<"text/css">>;
".woff" -> <<"application/font-woff">>;
".ttf" -> <<"application/font-snft">>;
_ -> <<"unknown">>
end,
{ok, Data} = file:read_file(File),
ResponseHeaders = [
{<<":status">>, <<"200">>},
{<<"content-type">>, MimeType}
],
case {MimeType, h2_connection:is_push(ConnPid)} of
{<<"text/html">>, true} ->
%% Search Data for resources to push
{ok, RE} = re:compile("<link rel=\"stylesheet\" href=\"([^\"]*)|<script src=\"([^\"]*)|src: '([^']*)"),
Resources = case re:run(Data, RE, [global, {capture,all,binary}]) of
{match, Matches} ->
[dot_hack(lists:last(M)) || M <- Matches];
_ -> []
end,
lists:foldl(
fun(R, Acc) ->
NewStreamId = h2_connection:new_stream(ConnPid),
PHeaders = generate_push_promise_headers(Headers, <<$/,R/binary>>
),
h2_connection:send_promise(ConnPid, StreamId, NewStreamId, PHeaders),
[{NewStreamId, PHeaders}|Acc]
end,
[],
Resources
),
ok;
_ ->
ok
end,
%% For each chunk of data:
%% 1. Ask the connection if it's got enough bytes in the
%% send window.
%% maybe just send the frame header?
%% If it doesn't, we need to put this frame in a place
%% that will get looked at when our connection window size
%% increases.
%% If it does, we still need to try and check stream level
%% flow control.
{ResponseHeaders, Data};
{false, false} ->
ResponseHeaders = [
{<<":status">>,<<"404">>}
],
{ResponseHeaders, <<"No soup for you!">>}
end,
case {Method, HeadersToSend, BodyToSend} of
{<<"HEAD">>, _, _} ->
h2_connection:send_headers(ConnPid, StreamId, HeadersToSend, [{send_end_stream, true}]);
%%{<<"GET">>, _, _} ->
_ ->
h2_connection:send_headers(ConnPid, StreamId, HeadersToSend),
h2_connection:send_body(ConnPid, StreamId, BodyToSend)
end,
{ok, State}.
%% Internal
-spec generate_push_promise_headers(hpack:headers(), binary()) -> hpack:headers().
generate_push_promise_headers(Request, Path) ->
[
{<<":path">>, Path},{<<":method">>, <<"GET">>}|
lists:filter(fun({<<":authority">>,_}) -> true;
({<<":scheme">>, _}) -> true;
(_) -> false end, Request)
].
-spec dot_hack(binary()) -> binary().
dot_hack(<<$.,Bin/binary>>) ->
Bin;
dot_hack(Bin) -> Bin.
================================================
FILE: src/chatterbox_sup.erl
================================================
-module(chatterbox_sup).
-behaviour(supervisor).
-export([
init/1,
start_link/0,
start_socket/0
]).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
{ok, Port} = application:get_env(chatterbox, port),
Options = [
binary,
{reuseaddr, true},
{packet, raw},
{backlog, 1024},
{active, false}
],
{ok, SSLEnabled} = application:get_env(chatterbox, ssl),
{Transport, SSLOptions} = case SSLEnabled of
true ->
{ok, SSLOpts} = application:get_env(chatterbox, ssl_options),
{ssl, SSLOpts};
false ->
{gen_tcp, []}
end,
Http2Settings = chatterbox:settings(server),
spawn_link(fun empty_listeners/0),
{ok, ListenSocket} = gen_tcp:listen(Port, Options),
Restart = {simple_one_for_one, 60, 3600},
Children = [{socket,
{h2_connection, start_server_link, [{Transport, ListenSocket}, SSLOptions, Http2Settings]},
temporary, 1000, worker, [h2_connection]}],
{ok, {Restart, Children}}.
start_socket() ->
supervisor:start_child(?MODULE, []).
empty_listeners() ->
{ok, ConcurrentAcceptors} = application:get_env(chatterbox, concurrent_acceptors),
[ start_socket() || _ <- lists:seq(1,ConcurrentAcceptors)].
================================================
FILE: src/h2_client.erl
================================================
-module(h2_client).
-include("http2.hrl").
%% Today's the day! We need to turn this gen_server into a gen_statem
%% which means this is going to look a lot like the "opposite of
%% http2_connection". This is the way to take advantage of the
%% abstraction of http2_socket. Still, the client API is way more
%% important than the server API so we're going to have to work
%% backwards from that API to get it right
%% {request, Headers, Data}
%% {request, [Frames]}
%% A frame that is too big should know how to break itself up.
%% That might mean into Continutations
%% API
-export([
start_link/0,
start_link/2,
start_link/3,
start_link/4,
start/4,
start_ssl_upgrade_link/4,
stop/1,
send_request/3,
send_ping/1,
sync_request/3,
get_response/2
]).
%% this is gonna get weird. start_link/* is going to call
%% http2_socket's start_link function, which will handle opening the
%% socket and sending the HTTP/2 Preface over the wire. Once that's
%% working, it's going to call gen_statem:start_link(http2c, [SocketPid],
%% []) which will then use our init/1 callback. You can't actually
%% start this gen_statem with this API. That's intentional, it will
%% eventually get started if things go right. If they don't, you
%% wouldn't want one of these anyway.
%% No arg version uses a bunch of defaults, which will have to be
%% reviewed if/when this module is refactored into it's own library.
-spec start_link() -> {ok, pid()} | ignore | {error, any()}.
start_link() ->
%% Defaults for local server, not sure these defaults are
%% "sensible" if this module is removed from this repo
{ok, Port} = application:get_env(chatterbox, port),
{ok, SSLEnabled} = application:get_env(chatterbox, ssl),
case SSLEnabled of
true ->
start_link(https, "localhost", Port);
false ->
start_link(http, "localhost", Port)
end.
%% Start up with scheme and hostname only, default to standard ports
%% and options
-spec start_link(http | https,
string()) ->
{ok, pid()}
| ignore
| {error, term()}.
start_link(http, Host) ->
start_link(http, Host, 80);
start_link(https,Host) ->
start_link(https, Host, 443).
%% Start up with a specific port, or specific SSL options, but not
%% both.
-spec start_link(http | https,
string(),
non_neg_integer() | [ssl:ssl_option()]) ->
{ok, pid()}
| ignore
| {error, term()}.
start_link(http, Host, Port)
when is_integer(Port) ->
start_link(http, Host, Port, []);
start_link(https, Host, Port)
when is_integer(Port) ->
{ok, SSLOptions} = application:get_env(chatterbox, ssl_options),
DefaultSSLOptions = [
{client_preferred_next_protocols, {client, [<<"h2">>]}}|
SSLOptions
],
start_link(https, Host, Port, DefaultSSLOptions);
start_link(https, Host, SSLOptions)
when is_list(SSLOptions) ->
start_link(https, Host, 443, SSLOptions).
%% Here's your all access client starter. MAXIMUM TUNABLES! Scheme,
%% Hostname, Port and SSLOptions. All of the start_link/* calls come
%% through here eventually, so this is where we turn 'http' and
%% 'https' into 'gen_tcp' and 'ssl' for erlang module function calls
%% later.
-spec start_link(http | https,
string(),
non_neg_integer(),
[ssl:ssl_option()]) ->
{ok, pid()}
| ignore
| {error, term()}.
start_link(Transport, Host, Port, SSLOptions) ->
NewT = case Transport of
http -> gen_tcp;
https -> ssl
end,
h2_connection:start_client_link(NewT, Host, Port, SSLOptions, chatterbox:settings(client)).
-spec start(http | https,
string(),
non_neg_integer(),
[ssl:ssl_option()]) ->
{ok, pid()}
| ignore
| {error, term()}.
start(Transport, Host, Port, SSLOptions) ->
NewT = case Transport of
http -> gen_tcp;
https -> ssl
end,
h2_connection:start_client(NewT, Host, Port, SSLOptions, chatterbox:settings(client)).
start_ssl_upgrade_link(Host, Port, InitialMessage, SSLOptions) ->
h2_connection:start_ssl_upgrade_link(Host, Port, InitialMessage, SSLOptions, chatterbox:settings(client)).
-spec stop(pid()) -> ok.
stop(Pid) ->
h2_connection:stop(Pid).
-spec sync_request(CliPid, Headers, Body) -> Result when
CliPid :: pid(), Headers :: hpack:headers(), Body :: binary(),
Result :: {ok, {hpack:headers(), iodata()}}
| {error, error_code() | timeout}.
sync_request(CliPid, Headers, Body) ->
case send_request(CliPid, Headers, Body) of
{ok, StreamId} ->
receive
{'END_STREAM', StreamId} ->
h2_connection:get_response(CliPid, StreamId)
after 5000 ->
{error, timeout}
end;
Error ->
Error
end.
-spec send_request(CliPid, Headers, Body) -> Result when
CliPid :: pid(), Headers :: hpack:headers(), Body :: binary(),
Result :: {ok, stream_id()} | {error, error_code()}.
send_request(CliPid, Headers, Body) ->
case h2_connection:new_stream(CliPid) of
{error, _Code} = Err ->
Err;
StreamId ->
h2_connection:send_headers(CliPid, StreamId, Headers),
h2_connection:send_body(CliPid, StreamId, Body),
{ok, StreamId}
end.
send_ping(CliPid) ->
h2_connection:send_ping(CliPid).
-spec get_response(pid(), stream_id()) ->
{ok, {hpack:header(), iodata()}}
| not_ready
| {error, term()}.
get_response(CliPid, StreamId) ->
h2_connection:get_response(CliPid, StreamId).
================================================
FILE: src/h2_connection.erl
================================================
-module(h2_connection).
-include("http2.hrl").
-behaviour(gen_statem).
%% Start/Stop API
-export([
start_client/2,
start_client/5,
start_client_link/2,
start_client_link/5,
start_ssl_upgrade_link/5,
start_server_link/3,
become/1,
become/2,
become/3,
stop/1
]).
%% HTTP Operations
-export([
send_headers/3,
send_headers/4,
send_trailers/3,
send_trailers/4,
send_body/3,
send_body/4,
send_request/3,
send_ping/1,
is_push/1,
new_stream/1,
new_stream/2,
send_promise/4,
get_response/2,
get_peer/1,
get_peercert/1,
get_streams/1,
send_window_update/2,
update_settings/2,
send_frame/2
]).
%% gen_statem callbacks
-export(
[
init/1,
callback_mode/0,
code_change/4,
terminate/3
]).
%% gen_statem states
-export([
listen/3,
handshake/3,
connected/3,
continuation/3,
closing/3
]).
-export([
go_away/2
]).
-record(h2_listening_state, {
ssl_options :: [ssl:ssl_option()],
listen_socket :: ssl:sslsocket() | inet:socket(),
transport :: gen_tcp | ssl,
listen_ref :: non_neg_integer(),
acceptor_callback = fun chatterbox_sup:start_socket/0 :: fun(),
server_settings = #settings{} :: settings()
}).
-record(continuation_state, {
stream_id :: stream_id(),
promised_id = undefined :: undefined | stream_id(),
frames = queue:new() :: queue:queue(h2_frame:frame()),
type :: headers | push_promise | trailers,
end_stream = false :: boolean(),
end_headers = false :: boolean()
}).
-record(connection, {
type = undefined :: client | server | undefined,
ssl_options = [],
listen_ref :: non_neg_integer() | undefined,
socket = undefined :: sock:socket(),
peer_settings = #settings{} :: settings(),
self_settings = #settings{} :: settings(),
send_window_size = ?DEFAULT_INITIAL_WINDOW_SIZE :: integer(),
recv_window_size = ?DEFAULT_INITIAL_WINDOW_SIZE :: integer(),
decode_context = hpack:new_context() :: hpack:context(),
encode_context = hpack:new_context() :: hpack:context(),
settings_sent = queue:new() :: queue:queue(),
next_available_stream_id = 2 :: stream_id(),
streams :: h2_stream_set:stream_set(),
stream_callback_mod = application:get_env(chatterbox, stream_callback_mod, chatterbox_static_stream) :: module(),
stream_callback_opts = application:get_env(chatterbox, stream_callback_opts, []) :: list(),
buffer = empty :: empty | {binary, binary()} | {frame, h2_frame:header(), binary()},
continuation = undefined :: undefined | #continuation_state{},
flow_control = auto :: auto | manual,
pings = #{} :: #{binary() => {pid(), non_neg_integer()}}
}).
-type connection() :: #connection{}.
-type send_option() :: {send_end_stream, boolean()}.
-type send_opts() :: [send_option()].
-export_type([send_option/0, send_opts/0]).
-ifdef(OTP_RELEASE).
-define(ssl_accept(ClientSocket, SSLOptions), ssl:handshake(ClientSocket, SSLOptions)).
-else.
-define(ssl_accept(ClientSocket, SSLOptions), ssl:ssl_accept(ClientSocket, SSLOptions)).
-endif.
-spec start_client_link(gen_tcp | ssl,
inet:ip_address() | inet:hostname(),
inet:port_number(),
[ssl:ssl_option()],
settings()
) ->
{ok, pid()} | ignore | {error, term()}.
start_client_link(Transport, Host, Port, SSLOptions, Http2Settings) ->
gen_statem:start_link(?MODULE, {client, Transport, Host, Port, SSLOptions, Http2Settings}, []).
-spec start_client(gen_tcp | ssl,
inet:ip_address() | inet:hostname(),
inet:port_number(),
[ssl:ssl_option()],
settings()
) ->
{ok, pid()} | ignore | {error, term()}.
start_client(Transport, Host, Port, SSLOptions, Http2Settings) ->
gen_statem:start(?MODULE, {client, Transport, Host, Port, SSLOptions, Http2Settings}, []).
-spec start_client_link(socket(),
settings()
) ->
{ok, pid()} | ignore | {error, term()}.
start_client_link({Transport, Socket}, Http2Settings) ->
gen_statem:start_link(?MODULE, {client, {Transport, Socket}, Http2Settings}, []).
-spec start_client(socket(),
settings()
) ->
{ok, pid()} | ignore | {error, term()}.
start_client({Transport, Socket}, Http2Settings) ->
gen_statem:start(?MODULE, {client, {Transport, Socket}, Http2Settings}, []).
-spec start_ssl_upgrade_link(inet:ip_address() | inet:hostname(),
inet:port_number(),
binary(),
[ssl:ssl_option()],
settings()
) ->
{ok, pid()} | ignore | {error, term()}.
start_ssl_upgrade_link(Host, Port, InitialMessage, SSLOptions, Http2Settings) ->
gen_statem:start_link(?MODULE, {client_ssl_upgrade, Host, Port, InitialMessage, SSLOptions, Http2Settings}, []).
-spec start_server_link(socket(),
[ssl:ssl_option()],
settings()) ->
{ok, pid()} | ignore | {error, term()}.
start_server_link({Transport, ListenSocket}, SSLOptions, Http2Settings) ->
gen_statem:start_link(?MODULE, {server, {Transport, ListenSocket}, SSLOptions, Http2Settings}, []).
-spec become(socket()) -> no_return().
become(Socket) ->
become(Socket, chatterbox:settings(server)).
-spec become(socket(), settings()) -> no_return().
become(Socket, Http2Settings) ->
become(Socket, Http2Settings, #{}).
-spec become(socket(), settings(), maps:map()) -> no_return().
become({Transport, Socket}, Http2Settings, ConnectionSettings) ->
ok = sock:setopts({Transport, Socket}, [{packet, raw}, binary]),
case start_http2_server(Http2Settings,
#connection{
stream_callback_mod =
maps:get(stream_callback_mod, ConnectionSettings,
application:get_env(chatterbox, stream_callback_mod, chatterbox_static_stream)),
stream_callback_opts =
maps:get(stream_callback_opts, ConnectionSettings,
application:get_env(chatterbox, stream_callback_opts, [])),
streams = h2_stream_set:new(server),
socket = {Transport, Socket}
}) of
{_, handshake, NewState} ->
gen_statem:enter_loop(?MODULE,
[],
handshake,
NewState);
{_, closing, _NewState} ->
sock:close({Transport, Socket}),
exit(invalid_preface)
end.
%% Init callback
init({client, Transport, Host, Port, SSLOptions, Http2Settings}) ->
case Transport:connect(Host, Port, client_options(Transport, SSLOptions)) of
{ok, Socket} ->
init({client, {Transport, Socket}, Http2Settings});
{error, Reason} ->
{stop, Reason}
end;
init({client, {Transport, Socket}, Http2Settings}) ->
ok = sock:setopts({Transport, Socket}, [{packet, raw}, binary, {active, once}]),
Transport:send(Socket, <<?PREFACE>>),
InitialState =
#connection{
type = client,
streams = h2_stream_set:new(client),
socket = {Transport, Socket},
next_available_stream_id=1,
flow_control=application:get_env(chatterbox, client_flow_control, auto)
},
{ok,
handshake,
send_settings(Http2Settings, InitialState),
4500};
init({client_ssl_upgrade, Host, Port, InitialMessage, SSLOptions, Http2Settings}) ->
case gen_tcp:connect(Host, Port, [{active, false}]) of
{ok, TCP} ->
gen_tcp:send(TCP, InitialMessage),
case ssl:connect(TCP, client_options(ssl, SSLOptions)) of
{ok, Socket} ->
init({client, {ssl, Socket}, Http2Settings});
{error, Reason} ->
{stop, Reason}
end;
{error, Reason} ->
{stop, Reason}
end;
init({server, {Transport, ListenSocket}, SSLOptions, Http2Settings}) ->
%% prim_inet:async_accept is dope. It says just hang out here and
%% wait for a message that a client has connected. That message
%% looks like:
%% {inet_async, ListenSocket, Ref, {ok, ClientSocket}}
case prim_inet:async_accept(ListenSocket, -1) of
{ok, Ref} ->
{ok,
listen,
#h2_listening_state{
ssl_options = SSLOptions,
listen_socket = ListenSocket,
listen_ref = Ref,
transport = Transport,
server_settings = Http2Settings
}}; %% No timeout here, it's just a listener
{error, Reason} ->
{stop, Reason}
end.
callback_mode() ->
state_functions.
send_frame(Pid, Bin)
when is_binary(Bin); is_list(Bin) ->
gen_statem:cast(Pid, {send_bin, Bin});
send_frame(Pid, Frame) ->
gen_statem:cast(Pid, {send_frame, Frame}).
-spec send_headers(pid(), stream_id(), hpack:headers()) -> ok.
send_headers(Pid, StreamId, Headers) ->
gen_statem:cast(Pid, {send_headers, StreamId, Headers, []}),
ok.
-spec send_headers(pid(), stream_id(), hpack:headers(), send_opts()) -> ok.
send_headers(Pid, StreamId, Headers, Opts) ->
gen_statem:cast(Pid, {send_headers, StreamId, Headers, Opts}),
ok.
-spec send_trailers(pid(), stream_id(), hpack:headers()) -> ok.
send_trailers(Pid, StreamId, Trailers) ->
gen_statem:cast(Pid, {send_trailers, StreamId, Trailers, []}),
ok.
-spec send_trailers(pid(), stream_id(), hpack:headers(), send_opts()) -> ok.
send_trailers(Pid, StreamId, Trailers, Opts) ->
gen_statem:cast(Pid, {send_trailers, StreamId, Trailers, Opts}),
ok.
-spec send_body(pid(), stream_id(), binary()) -> ok.
send_body(Pid, StreamId, Body) ->
gen_statem:cast(Pid, {send_body, StreamId, Body, []}),
ok.
-spec send_body(pid(), stream_id(), binary(), send_opts()) -> ok.
send_body(Pid, StreamId, Body, Opts) ->
gen_statem:cast(Pid, {send_body, StreamId, Body, Opts}),
ok.
-spec send_request(pid(), hpack:headers(), binary()) -> ok.
send_request(Pid, Headers, Body) ->
gen_statem:call(Pid, {send_request, self(), Headers, Body}, infinity),
ok.
-spec send_ping(pid()) -> ok.
send_ping(Pid) ->
gen_statem:call(Pid, {send_ping, self()}, infinity),
ok.
-spec get_peer(pid()) ->
{ok, {inet:ip_address(), inet:port_number()}} | {error, term()}.
get_peer(Pid) ->
gen_statem:call(Pid, get_peer).
-spec get_peercert(pid()) ->
{ok, binary()} | {error, term()}.
get_peercert(Pid) ->
gen_statem:call(Pid, get_peercert).
-spec is_push(pid()) -> boolean().
is_push(Pid) ->
gen_statem:call(Pid, is_push).
-spec new_stream(pid()) -> stream_id() | {error, error_code()}.
new_stream(Pid) ->
new_stream(Pid, self()).
-spec new_stream(pid(), pid()) ->
stream_id()
| {error, error_code()}.
new_stream(Pid, NotifyPid) ->
gen_statem:call(Pid, {new_stream, NotifyPid}).
-spec send_promise(pid(), stream_id(), stream_id(), hpack:headers()) -> ok.
send_promise(Pid, StreamId, NewStreamId, Headers) ->
gen_statem:cast(Pid, {send_promise, StreamId, NewStreamId, Headers}),
ok.
-spec get_response(pid(), stream_id()) ->
{ok, {hpack:headers(), iodata()}}
| not_ready.
get_response(Pid, StreamId) ->
gen_statem:call(Pid, {get_response, StreamId}).
-spec get_streams(pid()) -> h2_stream_set:stream_set().
get_streams(Pid) ->
gen_statem:call(Pid, streams).
-spec send_window_update(pid(), non_neg_integer()) -> ok.
send_window_update(Pid, Size) ->
gen_statem:cast(Pid, {send_window_update, Size}).
-spec update_settings(pid(), h2_frame_settings:payload()) -> ok.
update_settings(Pid, Payload) ->
gen_statem:cast(Pid, {update_settings, Payload}).
-spec stop(pid()) -> ok.
stop(Pid) ->
gen_statem:cast(Pid, stop).
%% The listen state only exists to wait around for new prim_inet
%% connections
listen(info, {inet_async, ListenSocket, Ref, {ok, ClientSocket}},
#h2_listening_state{
listen_socket = ListenSocket,
listen_ref = Ref,
transport = Transport,
ssl_options = SSLOptions,
acceptor_callback = AcceptorCallback,
server_settings = Http2Settings
}) ->
%If anything crashes in here, at least there's another acceptor ready
AcceptorCallback(),
inet_db:register_socket(ClientSocket, inet_tcp),
Socket = case Transport of
gen_tcp ->
ClientSocket;
ssl ->
{ok, AcceptSocket} = ?ssl_accept(ClientSocket, SSLOptions),
{ok, <<"h2">>} = ssl:negotiated_protocol(AcceptSocket),
AcceptSocket
end,
start_http2_server(
Http2Settings,
#connection{
streams = h2_stream_set:new(server),
socket={Transport, Socket}
});
listen(timeout, _, State) ->
go_away(?PROTOCOL_ERROR, State);
listen(Type, Msg, State) ->
handle_event(Type, Msg, State).
-spec handshake(gen_statem:event_type(), {frame, h2_frame:frame()} | term(), connection()) ->
{next_state,
handshake|connected|closing,
connection()}.
handshake(timeout, _, State) ->
go_away(?PROTOCOL_ERROR, State);
handshake(_, {frame, {FH, _Payload}=Frame}, State) ->
%% The first frame should be the client settings as per
%% RFC-7540#3.5
case FH#frame_header.type of
?SETTINGS ->
route_frame(Frame, State);
_ ->
go_away(?PROTOCOL_ERROR, State)
end;
handshake(Type, Msg, State) ->
handle_event(Type, Msg, State).
connected(_, {frame, Frame},
#connection{}=Conn
) ->
route_frame(Frame, Conn);
connected(Type, Msg, State) ->
handle_event(Type, Msg, State).
%% The continuation state in entered after receiving a HEADERS frame
%% with no ?END_HEADERS flag set, we're locked waiting for contiunation
%% frames on the same stream to preserve the decoding context state
continuation(_, {frame,
{#frame_header{
stream_id=StreamId,
type=?CONTINUATION
}, _}=Frame},
#connection{
continuation = #continuation_state{
stream_id = StreamId
}
}=Conn) ->
route_frame(Frame, Conn);
continuation(Type, Msg, State) ->
handle_event(Type, Msg, State).
%% The closing state should deal with frames on the wire still, I
%% think. But we should just close it up now.
closing(_, _Message,
#connection{
socket=Socket
}=Conn) ->
sock:close(Socket),
{stop, normal, Conn};
closing(Type, Msg, State) ->
handle_event(Type, Msg, State).
%% route_frame's job needs to be "now that we've read a frame off the
%% wire, do connection based things to it and/or forward it to the
%% http2 stream processor (h2_stream:recv_frame)
-spec route_frame(
h2_frame:frame() | {error, term()},
connection()) ->
{next_state,
connected | continuation | closing ,
connection()}.
%% Bad Length of frame, exceedes maximum allowed size
route_frame({#frame_header{length=L}, _},
#connection{
self_settings=#settings{max_frame_size=MFS}
}=Conn)
when L > MFS ->
go_away(?FRAME_SIZE_ERROR, Conn);
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Connection Level Frames
%%
%% Here we'll handle anything that belongs on stream 0.
%% SETTINGS, finally something that's ok on stream 0
%% This is the non-ACK case, where settings have actually arrived
route_frame({H, Payload},
#connection{
peer_settings=PS=#settings{
initial_window_size=OldIWS,
header_table_size=HTS
},
streams=Streams,
encode_context=EncodeContext
}=Conn)
when H#frame_header.type == ?SETTINGS,
?NOT_FLAG((H#frame_header.flags), ?FLAG_ACK) ->
%% Need a way of processing settings so I know which ones came in
%% on this one payload.
case h2_frame_settings:validate(Payload) of
ok ->
{settings, PList} = Payload,
Delta =
case proplists:get_value(?SETTINGS_INITIAL_WINDOW_SIZE, PList) of
undefined ->
0;
NewIWS ->
NewIWS - OldIWS
end,
NewPeerSettings = h2_frame_settings:overlay(PS, Payload),
%% We've just got connection settings from a peer. He have a
%% couple of jobs to do here w.r.t. flow control
%% If Delta != 0, we need to change every stream's
%% send_window_size in the state open or
%% half_closed_remote. We'll just send the message
%% everywhere. It's up to them if they need to do
%% anything.
UpdatedStreams1 =
h2_stream_set:update_all_send_windows(Delta, Streams),
UpdatedStreams2 =
case proplists:get_value(?SETTINGS_MAX_CONCURRENT_STREAMS, PList) of
undefined ->
UpdatedStreams1;
NewMax ->
h2_stream_set:update_my_max_active(NewMax, UpdatedStreams1)
end,
NewEncodeContext = hpack:new_max_table_size(HTS, EncodeContext),
socksend(Conn, h2_frame_settings:ack()),
{next_state, connected, Conn#connection{
peer_settings=NewPeerSettings,
%% Why aren't we updating send_window_size here? Section 6.9.2 of
%% the spec says: "The connection flow-control window can only be
%% changed using WINDOW_UPDATE frames.",
encode_context=NewEncodeContext,
streams=UpdatedStreams2
}};
{error, Code} ->
go_away(Code, Conn)
end;
%% This is the case where we got an ACK, so dequeue settings we're
%% waiting to apply
route_frame({H, _Payload},
#connection{
settings_sent=SS,
streams=Streams,
self_settings=#settings{
initial_window_size=OldIWS
}
}=Conn)
when H#frame_header.type == ?SETTINGS,
?IS_FLAG((H#frame_header.flags), ?FLAG_ACK) ->
case queue:out(SS) of
{{value, {_Ref, NewSettings}}, NewSS} ->
UpdatedStreams1 =
case NewSettings#settings.initial_window_size of
undefined ->
ok;
NewIWS ->
Delta = OldIWS - NewIWS,
h2_stream_set:update_all_recv_windows(Delta, Streams)
end,
UpdatedStreams2 =
case NewSettings#settings.max_concurrent_streams of
undefined ->
UpdatedStreams1;
NewMax ->
h2_stream_set:update_their_max_active(NewMax, UpdatedStreams1)
end,
{next_state,
connected,
Conn#connection{
streams=UpdatedStreams2,
settings_sent=NewSS,
self_settings=NewSettings
%% Same thing here, section 6.9.2
}};
_X ->
{next_state, closing, Conn}
end;
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Stream level frames
%%
%% receive data frame bigger than connection recv window
route_frame({H,_Payload}, Conn)
when H#frame_header.type == ?DATA,
H#frame_header.length > Conn#connection.recv_window_size ->
go_away(?FLOW_CONTROL_ERROR, Conn);
route_frame(F={H=#frame_header{
length=L,
stream_id=StreamId}, _Payload},
#connection{
recv_window_size=CRWS,
streams=Streams
}=Conn)
when H#frame_header.type == ?DATA ->
Stream = h2_stream_set:get(StreamId, Streams),
case h2_stream_set:type(Stream) of
active ->
case {
h2_stream_set:recv_window_size(Stream) < L,
Conn#connection.flow_control,
L > 0
} of
{true, _, _} ->
rst_stream(Stream,
?FLOW_CONTROL_ERROR,
Conn);
%% If flow control is set to auto, and L > 0, send
%% window updates back to the peer. If L == 0, we're
%% not allowed to send window_updates of size 0, so we
%% hit the next clause
{false, auto, true} ->
%% Make window size great again
h2_frame_window_update:send(Conn#connection.socket,
L, StreamId),
send_window_update(self(), L),
recv_data(Stream, F),
{next_state,
connected,
Conn};
%% Either
%% {false, auto, true} or
%% {false, manual, _DoesntMatter}
_Tried ->
recv_data(Stream, F),
{next_state,
connected,
Conn#connection{
recv_window_size=CRWS-L,
streams=h2_stream_set:upsert(
h2_stream_set:decrement_recv_window(L, Stream),
Streams)
}}
end;
_ ->
go_away(?PROTOCOL_ERROR, Conn)
end;
route_frame({#frame_header{type=?HEADERS}=FH, _Payload},
#connection{}=Conn)
when Conn#connection.type == server,
FH#frame_header.stream_id rem 2 == 0 ->
go_away(?PROTOCOL_ERROR, Conn);
route_frame({#frame_header{type=?HEADERS}=FH, _Payload}=Frame,
#connection{}=Conn) ->
StreamId = FH#frame_header.stream_id,
Streams = Conn#connection.streams,
%% Four things could be happening here.
%% If we're a server, these are either request headers or request
%% trailers
%% If we're a client, these are either headers in response to a
%% client request, or headers in response to a push promise
Stream = h2_stream_set:get(StreamId, Streams),
{ContinuationType, NewConn} =
case {h2_stream_set:type(Stream), Conn#connection.type} of
{idle, server} ->
case
h2_stream_set:new_stream(
StreamId,
self(),
Conn#connection.stream_callback_mod,
Conn#connection.stream_callback_opts,
Conn#connection.socket,
(Conn#connection.peer_settings)#settings.initial_window_size,
(Conn#connection.self_settings)#settings.initial_window_size,
Streams) of
{error, ErrorCode, NewStream} ->
rst_stream(NewStream, ErrorCode, Conn),
{none, Conn};
NewStreams ->
{headers, Conn#connection{streams=NewStreams}}
end;
{active, server} ->
{trailers, Conn};
_ ->
{headers, Conn}
end,
case ContinuationType of
none ->
{next_state,
connected,
NewConn};
_ ->
ContinuationState =
#continuation_state{
type = ContinuationType,
frames = queue:from_list([Frame]),
end_stream = ?IS_FLAG((FH#frame_header.flags), ?FLAG_END_STREAM),
end_headers = ?IS_FLAG((FH#frame_header.flags), ?FLAG_END_HEADERS),
stream_id = StreamId
},
%% maybe_hpack/2 uses this #continuation_state to figure
%% out what to do, which might include hpack
maybe_hpack(ContinuationState, NewConn)
end;
route_frame(F={H=#frame_header{
stream_id=_StreamId,
type=?CONTINUATION
}, _Payload},
#connection{
continuation = #continuation_state{
frames = CFQ
} = Cont
}=Conn) ->
maybe_hpack(Cont#continuation_state{
frames=queue:in(F, CFQ),
end_headers=?IS_FLAG((H#frame_header.flags), ?FLAG_END_HEADERS)
},
Conn);
route_frame({H, _Payload},
#connection{}=Conn)
when H#frame_header.type == ?PRIORITY,
H#frame_header.stream_id == 0 ->
go_away(?PROTOCOL_ERROR, Conn);
route_frame({H, _Payload},
#connection{} = Conn)
when H#frame_header.type == ?PRIORITY ->
{next_state, connected, Conn};
route_frame(
{#frame_header{
stream_id=StreamId,
type=?RST_STREAM
},
_Payload},
#connection{} = Conn) ->
%% TODO: anything with this?
%% EC = h2_frame_rst_stream:error_code(Payload),
Streams = Conn#connection.streams,
Stream = h2_stream_set:get(StreamId, Streams),
case h2_stream_set:type(Stream) of
idle ->
go_away(?PROTOCOL_ERROR, Conn);
_Stream ->
%% TODO: RST_STREAM support
{next_state, connected, Conn}
end;
route_frame({H=#frame_header{}, _P},
#connection{} =Conn)
when H#frame_header.type == ?PUSH_PROMISE,
Conn#connection.type == server ->
go_away(?PROTOCOL_ERROR, Conn);
route_frame({H=#frame_header{
stream_id=StreamId
},
Payload}=Frame,
#connection{}=Conn)
when H#frame_header.type == ?PUSH_PROMISE,
Conn#connection.type == client ->
PSID = h2_frame_push_promise:promised_stream_id(Payload),
Streams = Conn#connection.streams,
Old = h2_stream_set:get(StreamId, Streams),
NotifyPid = h2_stream_set:notify_pid(Old),
%% TODO: Case statement here about execeding the number of
%% pushed. Honestly I think it's a bigger problem with the
%% h2_stream_set, but we can punt it for now. The idea is that
%% reserved(local) and reserved(remote) aren't technically
%% 'active', but they're being counted that way right now. Again,
%% that only matters if Server Push is enabled.
NewStreams =
h2_stream_set:new_stream(
PSID,
NotifyPid,
Conn#connection.stream_callback_mod,
Conn#connection.stream_callback_opts,
Conn#connection.socket,
(Conn#connection.peer_settings)#settings.initial_window_size,
(Conn#connection.self_settings)#settings.initial_window_size,
Streams),
Continuation = #continuation_state{
stream_id=StreamId,
type=push_promise,
frames = queue:in(Frame, queue:new()),
end_headers=?IS_FLAG((H#frame_header.flags), ?FLAG_END_HEADERS),
promised_id=PSID
},
maybe_hpack(Continuation,
Conn#connection{
streams = NewStreams
});
%% PING
%% If not stream 0, then connection error
route_frame({H, _Payload},
#connection{} = Conn)
when H#frame_header.type == ?PING,
H#frame_header.stream_id =/= 0 ->
go_away(?PROTOCOL_ERROR, Conn);
%% If length != 8, FRAME_SIZE_ERROR
%% TODO: I think this case is already covered in h2_frame now
route_frame({H, _Payload},
#connection{}=Conn)
when H#frame_header.type == ?PING,
H#frame_header.length =/= 8 ->
go_away(?FRAME_SIZE_ERROR, Conn);
%% If PING && !ACK, must ACK
route_frame({H, Ping},
#connection{}=Conn)
when H#frame_header.type == ?PING,
?NOT_FLAG((H#frame_header.flags), ?FLAG_ACK) ->
Ack = h2_frame_ping:ack(Ping),
socksend(Conn, h2_frame:to_binary(Ack)),
{next_state, connected, Conn};
route_frame({H, Payload},
#connection{pings = Pings}=Conn)
when H#frame_header.type == ?PING,
?IS_FLAG((H#frame_header.flags), ?FLAG_ACK) ->
case maps:get(h2_frame_ping:to_binary(Payload), Pings, undefined) of
undefined ->
ok;
{NotifyPid, _} ->
NotifyPid ! {'PONG', self()}
end,
NextPings = maps:remove(Payload, Pings),
{next_state, connected, Conn#connection{pings = NextPings}};
route_frame({H=#frame_header{stream_id=0}, _Payload},
#connection{}=Conn)
when H#frame_header.type == ?GOAWAY ->
go_away(?NO_ERROR, Conn);
%% Window Update
route_frame(
{#frame_header{
stream_id=0,
type=?WINDOW_UPDATE
},
Payload},
#connection{
send_window_size=SWS
}=Conn) ->
WSI = h2_frame_window_update:size_increment(Payload),
NewSendWindow = SWS+WSI,
case NewSendWindow > 2147483647 of
true ->
go_away(?FLOW_CONTROL_ERROR, Conn);
false ->
%% TODO: Priority Sort! Right now, it's just sorting on
%% lowest stream_id first
Streams = h2_stream_set:sort(Conn#connection.streams),
{RemainingSendWindow, UpdatedStreams} =
h2_stream_set:send_what_we_can(
all,
NewSendWindow,
(Conn#connection.peer_settings)#settings.max_frame_size,
Streams
),
{next_state, connected,
Conn#connection{
send_window_size=RemainingSendWindow,
streams=UpdatedStreams
}}
end;
route_frame(
{#frame_header{type=?WINDOW_UPDATE}=FH,
Payload},
#connection{}=Conn
) ->
StreamId = FH#frame_header.stream_id,
Streams = Conn#connection.streams,
WSI = h2_frame_window_update:size_increment(Payload),
Stream = h2_stream_set:get(StreamId, Streams),
case h2_stream_set:type(Stream) of
idle ->
go_away(?PROTOCOL_ERROR, Conn);
closed ->
rst_stream(Stream, ?STREAM_CLOSED, Conn);
active ->
SWS = Conn#connection.send_window_size,
NewSSWS = h2_stream_set:send_window_size(Stream)+WSI,
case NewSSWS > 2147483647 of
true ->
rst_stream(Stream, ?FLOW_CONTROL_ERROR, Conn);
false ->
{RemainingSendWindow, NewStreams}
= h2_stream_set:send_what_we_can(
StreamId,
SWS,
(Conn#connection.peer_settings)#settings.max_frame_size,
h2_stream_set:upsert(
h2_stream_set:increment_send_window_size(WSI, Stream),
Streams)
),
{next_state, connected,
Conn#connection{
send_window_size=RemainingSendWindow,
streams=NewStreams
}}
end
end;
route_frame({#frame_header{type=T}, _}, Conn)
when T > ?CONTINUATION ->
{next_state, connected, Conn};
route_frame(Frame, #connection{}=Conn) ->
error_logger:error_msg("Frame condition not covered by pattern match."
"Please open a github issue with this output: ~s",
[h2_frame:format(Frame)]),
go_away(?PROTOCOL_ERROR, Conn).
handle_event(_, {stream_finished,
StreamId,
Headers,
Body},
Conn) ->
Stream = h2_stream_set:get(StreamId, Conn#connection.streams),
case h2_stream_set:type(Stream) of
active ->
NotifyPid = h2_stream_set:notify_pid(Stream),
Response =
case Conn#connection.type of
server -> garbage;
client -> {Headers, Body}
end,
{_NewStream, NewStreams} =
h2_stream_set:close(
Stream,
Response,
Conn#connection.streams),
NewConn =
Conn#connection{
streams = NewStreams
},
case {Conn#connection.type, is_pid(NotifyPid)} of
{client, true} ->
NotifyPid ! {'END_STREAM', StreamId};
_ ->
ok
end,
{keep_state, NewConn};
_ ->
%% stream finished multiple times
{keep_state, Conn}
end;
handle_event(_, {send_window_update, 0}, Conn) ->
{keep_state, Conn};
handle_event(_, {send_window_update, Size},
#connection{
recv_window_size=CRWS,
socket=Socket
}=Conn) ->
ok = h2_frame_window_update:send(Socket, Size, 0),
{keep_state,
Conn#connection{
recv_window_size=CRWS+Size
}};
handle_event(_, {update_settings, Http2Settings},
#connection{}=Conn) ->
{keep_state,
send_settings(Http2Settings, Conn)};
handle_event(_, {send_headers, StreamId, Headers, Opts},
#connection{
encode_context=EncodeContext,
streams = Streams,
socket = Socket
}=Conn
) ->
StreamComplete = proplists:get_value(send_end_stream, Opts, false),
Stream = h2_stream_set:get(StreamId, Streams),
case h2_stream_set:type(Stream) of
active ->
{FramesToSend, NewContext} =
h2_frame_headers:to_frames(h2_stream_set:stream_id(Stream),
Headers,
EncodeContext,
(Conn#connection.peer_settings)#settings.max_frame_size,
StreamComplete
),
[sock:send(Socket, h2_frame:to_binary(Frame)) || Frame <- FramesToSend],
send_h(Stream, Headers),
{keep_state,
Conn#connection{
encode_context=NewContext
}};
idle ->
%% In theory this is a client maybe activating a stream,
%% but in practice, we've already activated the stream in
%% new_stream/1
{keep_state, Conn};
closed ->
{keep_state, Conn}
end;
handle_event(_, {send_trailers, StreamId, Headers, Opts},
#connection{
encode_context=EncodeContext,
streams = Streams,
socket = _Socket
}=Conn
) ->
BodyComplete = proplists:get_value(send_end_stream, Opts, true),
Stream = h2_stream_set:get(StreamId, Streams),
case h2_stream_set:type(Stream) of
active ->
{FramesToSend, NewContext} =
h2_frame_headers:to_frames(h2_stream_set:stream_id(Stream),
Headers,
EncodeContext,
(Conn#connection.peer_settings)#settings.max_frame_size,
true
),
NewS = h2_stream_set:update_trailers(FramesToSend, Stream),
{NewSWS, NewStreams} =
h2_stream_set:send_what_we_can(
StreamId,
Conn#connection.send_window_size,
(Conn#connection.peer_settings)#settings.max_frame_size,
h2_stream_set:upsert(
h2_stream_set:update_data_queue(h2_stream_set:queued_data(Stream), BodyComplete, NewS),
Conn#connection.streams)),
send_t(Stream, Headers),
{keep_state,
Conn#connection{
encode_context=NewContext,
send_window_size=NewSWS,
streams=NewStreams
}};
idle ->
%% In theory this is a client maybe activating a stream,
%% but in practice, we've already activated the stream in
%% new_stream/1
{keep_state, Conn};
closed ->
{keep_state, Conn}
end;
handle_event(_, {send_body, StreamId, Body, Opts},
#connection{}=Conn) ->
BodyComplete = proplists:get_value(send_end_stream, Opts, true),
Stream = h2_stream_set:get(StreamId, Conn#connection.streams),
case h2_stream_set:type(Stream) of
active ->
OldBody = h2_stream_set:queued_data(Stream),
NewBody = case is_binary(OldBody) of
true -> <<OldBody/binary, Body/binary>>;
false -> Body
end,
{NewSWS, NewStreams} =
h2_stream_set:send_what_we_can(
StreamId,
Conn#connection.send_window_size,
(Conn#connection.peer_settings)#settings.max_frame_size,
h2_stream_set:upsert(
h2_stream_set:update_data_queue(NewBody, BodyComplete, Stream),
Conn#connection.streams)),
{keep_state,
Conn#connection{
send_window_size=NewSWS,
streams=NewStreams
}};
idle ->
%% Sending DATA frames on an idle stream? It's a
%% Connection level protocol error on receipt, but If we
%% have no active stream what can we even do?
{keep_state, Conn};
closed ->
{keep_state, Conn}
end;
handle_event(_, {send_request, NotifyPid, Headers, Body},
#connection{
streams=Streams,
next_available_stream_id=NextId
}=Conn) ->
case send_request(NextId, NotifyPid, Conn, Streams, Headers, Body) of
{ok, GoodStreamSet} ->
{keep_state, Conn#connection{
next_available_stream_id=NextId+2,
streams=GoodStreamSet
}};
{error, _Code} ->
{keep_state, Conn}
end;
handle_event(_, {send_promise, StreamId, NewStreamId, Headers},
#connection{
streams=Streams,
encode_context=OldContext
}=Conn
) ->
NewStream = h2_stream_set:get(NewStreamId, Streams),
case h2_stream_set:type(NewStream) of
active ->
%% TODO: This could be a series of frames, not just one
{PromiseFrame, NewContext} =
h2_frame_push_promise:to_frame(
StreamId,
NewStreamId,
Headers,
OldContext
),
%% Send the PP Frame
Binary = h2_frame:to_binary(PromiseFrame),
socksend(Conn, Binary),
%% Get the promised stream rolling
h2_stream:send_pp(h2_stream_set:stream_pid(NewStream), Headers),
{keep_state,
Conn#connection{
encode_context=NewContext
}};
_ ->
{keep_state, Conn}
end;
handle_event(_, {check_settings_ack, {Ref, NewSettings}},
#connection{
settings_sent=SS
}=Conn) ->
case queue:out(SS) of
{{value, {Ref, NewSettings}}, _} ->
%% This is still here!
go_away(?SETTINGS_TIMEOUT, Conn);
_ ->
%% YAY!
{keep_state, Conn}
end;
handle_event(_, {send_bin, Binary},
#connection{} = Conn) ->
socksend(Conn, Binary),
{keep_state, Conn};
handle_event(_, {send_frame, Frame},
#connection{} =Conn) ->
Binary = h2_frame:to_binary(Frame),
socksend(Conn, Binary),
{keep_state, Conn};
handle_event(stop, _StateName,
#connection{}=Conn) ->
go_away(0, Conn);
handle_event({call, From}, streams,
#connection{
streams=Streams
}=Conn) ->
{keep_state, Conn, [{reply, From, Streams}]};
handle_event({call, From}, {get_response, StreamId},
#connection{}=Conn) ->
Stream = h2_stream_set:get(StreamId, Conn#connection.streams),
{Reply, NewStreams} =
case h2_stream_set:type(Stream) of
closed ->
{_, NewStreams0} =
h2_stream_set:close(
Stream,
garbage,
Conn#connection.streams),
{{ok, h2_stream_set:response(Stream)}, NewStreams0};
active ->
{not_ready, Conn#connection.streams}
end,
{keep_state, Conn#connection{streams=NewStreams}, [{reply, From, Reply}]};
handle_event({call, From}, {new_stream, NotifyPid},
#connection{
streams=Streams,
next_available_stream_id=NextId
}=Conn) ->
{Reply, NewStreams} =
case
h2_stream_set:new_stream(
NextId,
NotifyPid,
Conn#connection.stream_callback_mod,
Conn#connection.stream_callback_opts,
Conn#connection.socket,
Conn#connection.peer_settings#settings.initial_window_size,
Conn#connection.self_settings#settings.initial_window_size,
Streams)
of
{error, Code, _NewStream} ->
%% TODO: probably want to have events like this available for metrics
%% tried to create new_stream but there are too many
{{error, Code}, Streams};
GoodStreamSet ->
{NextId, GoodStreamSet}
end,
{keep_state, Conn#connection{
next_available_stream_id=NextId+2,
streams=NewStreams
}, [{reply, From, Reply}]};
handle_event({call, From}, is_push,
#connection{
peer_settings=#settings{enable_push=Push}
}=Conn) ->
IsPush = case Push of
1 -> true;
_ -> false
end,
{keep_state, Conn, [{reply, From, IsPush}]};
handle_event({call, From}, get_peer,
#connection{
socket=Socket
}=Conn) ->
case sock:peername(Socket) of
{error, _}=Error ->
{keep_state, Conn, [{reply, From, Error}]};
{ok, _AddrPort}=OK ->
{keep_state, Conn, [{reply, From, OK}]}
end;
handle_event({call, From}, get_peercert,
#connection{
socket=Socket
}=Conn) ->
case sock:peercert(Socket) of
{error, _}=Error ->
{keep_state, Conn, [{reply, From, Error}]};
{ok, _Cert}=OK ->
{keep_state, Conn, [{reply, From, OK}]}
end;
handle_event({call, From}, {send_request, NotifyPid, Headers, Body},
#connection{
streams=Streams,
next_available_stream_id=NextId
}=Conn) ->
case send_request(NextId, NotifyPid, Conn, Streams, Headers, Body) of
{ok, GoodStreamSet} ->
{keep_state, Conn#connection{
next_available_stream_id=NextId+2,
streams=GoodStreamSet
}, [{reply, From, ok}]};
{error, Code} ->
{keep_state, Conn, [{reply, From, {error, Code}}]}
end;
handle_event({call, From}, {send_ping, NotifyPid},
#connection{pings = Pings} = Conn) ->
PingValue = crypto:strong_rand_bytes(8),
Frame = h2_frame_ping:new(PingValue),
Headers = #frame_header{stream_id = 0, flags = 16#0},
Binary = h2_frame:to_binary({Headers, Frame}),
case socksend(Conn, Binary) of
ok ->
NextPings = maps:put(PingValue, {NotifyPid, erlang:monotonic_time(milli_seconds)}, Pings),
NextConn = Conn#connection{pings = NextPings},
{keep_state, NextConn, [{reply, From, ok}]};
{error, _Reason} = Err ->
{keep_state, Conn, [{reply, From, Err}]}
end;
%% Socket Messages
%% {tcp, Socket, Data}
handle_event(info, {tcp, Socket, Data},
#connection{
socket={gen_tcp,Socket}
}=Conn) ->
handle_socket_data(Data, Conn);
%% {ssl, Socket, Data}
handle_event(info, {ssl, Socket, Data},
#connection{
socket={ssl,Socket}
}=Conn) ->
handle_socket_data(Data, Conn);
%% {tcp_passive, Socket}
handle_event(info, {tcp_passive, Socket},
#connection{
socket={gen_tcp, Socket}
}=Conn) ->
handle_socket_passive(Conn);
%% {tcp_closed, Socket}
handle_event(info, {tcp_closed, Socket},
#connection{
socket={gen_tcp, Socket}
}=Conn) ->
handle_socket_closed(Conn);
%% {ssl_closed, Socket}
handle_event(info, {ssl_closed, Socket},
#connection{
socket={ssl, Socket}
}=Conn) ->
handle_socket_closed(Conn);
%% {tcp_error, Socket, Reason}
handle_event(info, {tcp_error, Socket, Reason},
#connection{
socket={gen_tcp,Socket}
}=Conn) ->
handle_socket_error(Reason, Conn);
%% {ssl_error, Socket, Reason}
handle_event(info, {ssl_error, Socket, Reason},
#connection{
socket={ssl,Socket}
}=Conn) ->
handle_socket_error(Reason, Conn);
handle_event(info, {_,R},
#connection{}=Conn) ->
handle_socket_error(R, Conn);
handle_event(_, _, Conn) ->
go_away(?PROTOCOL_ERROR, Conn).
code_change(_OldVsn, StateName, Conn, _Extra) ->
{ok, StateName, Conn}.
terminate(normal, _StateName, _Conn) ->
ok;
terminate(_Reason, _StateName, _Conn=#connection{}) ->
ok;
terminate(_Reason, _StateName, _State) ->
ok.
-spec go_away(error_code(), connection()) -> {next_state, closing, connection()}.
go_away(ErrorCode,
#connection{
next_available_stream_id=NAS
}=Conn) ->
GoAway = h2_frame_goaway:new(NAS, ErrorCode),
GoAwayBin = h2_frame:to_binary({#frame_header{
stream_id=0
}, GoAway}),
socksend(Conn, GoAwayBin),
%% TODO: why is this sending a string?
gen_statem:cast(self(), io_lib:format("GO_AWAY: ErrorCode ~p", [ErrorCode])),
{next_state, closing, Conn}.
%% rst_stream/3 looks for a running process for the stream. If it
%% finds one, it delegates sending the rst_stream frame to it, but if
%% it doesn't, it seems like a waste to spawn one just to kill it
%% after sending that frame, so we send it from here.
-spec rst_stream(
h2_stream_set:stream(),
error_code(),
connection()
) ->
{next_state, connected, connection()}.
rst_stream(Stream, ErrorCode, Conn) ->
case h2_stream_set:type(Stream) of
active ->
%% Can this ever be undefined?
Pid = h2_stream_set:stream_pid(Stream),
%% h2_stream's rst_stream will take care of letting us know
%% this stream is closed and will send us a message to close the
%% stream somewhere else
h2_stream:rst_stream(Pid, ErrorCode),
{next_state, connected, Conn};
_ ->
StreamId = h2_stream_set:stream_id(Stream),
RstStream = h2_frame_rst_stream:new(ErrorCode),
RstStreamBin = h2_frame:to_binary(
{#frame_header{
stream_id=StreamId
},
RstStream}),
sock:send(Conn#connection.socket, RstStreamBin),
{next_state, connected, Conn}
end.
-spec send_settings(settings(), connection()) -> connection().
send_settings(SettingsToSend,
#connection{
self_settings=CurrentSettings,
settings_sent=SS
}=Conn) ->
Ref = make_ref(),
Bin = h2_frame_settings:send(CurrentSettings, SettingsToSend),
socksend(Conn, Bin),
send_ack_timeout({Ref,SettingsToSend}),
Conn#connection{
settings_sent=queue:in({Ref, SettingsToSend}, SS)
}.
-spec send_ack_timeout({reference(), settings()}) -> pid().
send_ack_timeout(SS) ->
Self = self(),
SendAck = fun() ->
timer:sleep(5000),
gen_statem:cast(Self, {check_settings_ack,SS})
end,
spawn_link(SendAck).
%% private socket handling
active_once(Socket) ->
sock:setopts(Socket, [{active, once}]).
client_options(Transport, SSLOptions) ->
ClientSocketOptions = [
binary,
{packet, raw},
{active, false}
],
case Transport of
ssl ->
[{alpn_advertised_protocols, [<<"h2">>]}|ClientSocketOptions ++ SSLOptions];
gen_tcp ->
ClientSocketOptions
end.
start_http2_server(
Http2Settings,
#connection{
socket=Socket
}=Conn) ->
case accept_preface(Socket) of
ok ->
ok = active_once(Socket),
NewState =
Conn#connection{
type=server,
next_available_stream_id=2,
flow_control=application:get_env(chatterbox, server_flow_control, auto)
},
{next_state,
handshake,
send_settings(Http2Settings, NewState)
};
{error, invalid_preface} ->
{next_state, closing, Conn}
end.
%% We're going to iterate through the preface string until we're done
%% or hit a mismatch
accept_preface(Socket) ->
accept_preface(Socket, <<?PREFACE>>).
accept_preface(_Socket, <<>>) ->
ok;
accept_preface(Socket, <<Char:8,Rem/binary>>) ->
case sock:recv(Socket, 1, 5000) of
{ok, <<Char>>} ->
accept_preface(Socket, Rem);
_E ->
sock:close(Socket),
{error, invalid_preface}
end.
%% Incoming data is a series of frames. With a passive socket we can just:
%% 1. read(9)
%% 2. turn that 9 into an http2 frame header
%% 3. use that header's length field L
%% 4. read(L), now we have a frame
%% 5. do something with it
%% 6. goto 1
%% Things will be different with an {active, true} socket, and also
%% different again with an {active, once} socket
%% with {active, true}, we'd have to maintain some kind of input queue
%% because it will be very likely that Data is not neatly just a frame
%% with {active, once}, we'd probably be in a situation where Data
%% starts with a frame header. But it's possible that we're here with
%% a partial frame left over from the last active stream
%% We're going to go with the {active, once} approach, because it
%% won't block the gen_server on Transport:read(L), but it will wake
%% up and do something every time Data comes in.
handle_socket_data(<<>>,
#connection{
socket=Socket
}=Conn) ->
active_once(Socket),
{keep_state, Conn};
handle_socket_data(Data,
#connection{
socket=Socket,
buffer=Buffer
}=Conn) ->
More =
case sock:recv(Socket, 0, 1) of %% fail fast
{ok, Rest} ->
Rest;
%% It's not really an error, it's what we want
{error, timeout} ->
<<>>;
_ ->
<<>>
end,
%% What is buffer?
%% empty - nothing, yay
%% {frame, h2_frame:header(), binary()} - Frame Header processed, Payload not big enough
%% {binary, binary()} - If we're here, it must mean that Bin was too small to even be a header
ToParse = case Buffer of
empty ->
<<Data/binary,More/binary>>;
{frame, FHeader, BufferBin} ->
{FHeader, <<BufferBin/binary,Data/binary,More/binary>>};
{binary, BufferBin} ->
<<BufferBin/binary,Data/binary,More/binary>>
end,
%% Now that the buffer has been merged, it's best to make sure any
%% further state references don't have one
NewConn = Conn#connection{buffer=empty},
case h2_frame:recv(ToParse) of
%% We got a full frame, ship it off to the FSM
{ok, Frame, Rem} ->
gen_statem:cast(self(), {frame, Frame}),
handle_socket_data(Rem, NewConn);
%% Not enough bytes left to make a header :(
{not_enough_header, Bin} ->
%% This is a situation where more bytes should come soon,
%% so let's switch back to active, once
active_once(Socket),
{keep_state, NewConn#connection{buffer={binary, Bin}}};
%% Not enough bytes to make a payload
{not_enough_payload, Header, Bin} ->
%% This too
active_once(Socket),
{keep_state, NewConn#connection{buffer={frame, Header, Bin}}};
{error, 0, Code, _Rem} ->
%% Remaining Bytes don't matter, we're closing up shop.
go_away(Code, NewConn);
{error, StreamId, Code, Rem} ->
Stream = h2_stream_set:get(StreamId, Conn#connection.streams),
rst_stream(Stream, Code, NewConn),
handle_socket_data(Rem, NewConn)
end.
handle_socket_passive(Conn) ->
{keep_state, Conn}.
handle_socket_closed(Conn) ->
{stop, normal, Conn}.
handle_socket_error(Reason, Conn) ->
{stop, Reason, Conn}.
socksend(#connection{
socket=Socket
}, Data) ->
case sock:send(Socket, Data) of
ok ->
ok;
{error, Reason} ->
{error, Reason}
end.
%% maybe_hpack will decode headers if it can, or tell the connection
%% to wait for CONTINUATION frames if it can't.
-spec maybe_hpack(#continuation_state{}, connection()) ->
{next_state, atom(), connection()}.
%% If there's an END_HEADERS flag, we have a complete headers binary
%% to decode, let's do this!
maybe_hpack(Continuation, Conn)
when Continuation#continuation_state.end_headers ->
Stream = h2_stream_set:get(
Continuation#continuation_state.stream_id,
Conn#connection.streams
),
HeadersBin = h2_frame_headers:from_frames(
queue:to_list(Continuation#continuation_state.frames)
),
case hpack:decode(HeadersBin, Conn#connection.decode_context) of
{error, compression_error} ->
go_away(?COMPRESSION_ERROR, Conn);
{ok, {Headers, NewDecodeContext}} ->
case {Continuation#continuation_state.type,
Continuation#continuation_state.end_stream} of
{push_promise, _} ->
Promised =
h2_stream_set:get(
Continuation#continuation_state.promised_id,
Conn#connection.streams
),
recv_pp(Promised, Headers);
{trailers, false} ->
rst_stream(Stream, ?PROTOCOL_ERROR, Conn);
_ -> %% headers or trailers!
recv_h(Stream, Conn, Headers)
end,
case Continuation#continuation_state.end_stream of
true ->
recv_es(Stream, Conn);
false ->
ok
end,
{next_state, connected,
Conn#connection{
decode_context=NewDecodeContext,
continuation=undefined
}}
end;
%% If not, we have to wait for all the CONTINUATIONS to roll in.
maybe_hpack(Continuation, Conn) ->
{next_state, continuation,
Conn#connection{
continuation = Continuation
}}.
%% Stream API: These will be moved
-spec recv_h(
Stream :: h2_stream_set:stream(),
Conn :: connection(),
Headers :: hpack:headers()) ->
ok.
recv_h(Stream,
Conn,
Headers) ->
case h2_stream_set:type(Stream) of
active ->
%% If the stream is active, let the process deal with it.
Pid = h2_stream_set:pid(Stream),
h2_stream:send_event(Pid, {recv_h, Headers});
closed ->
%% If the stream is closed, there's no running FSM
rst_stream(Stream, ?STREAM_CLOSED, Conn);
idle ->
%% If we're calling this function, we've already activated
%% a stream FSM (probably). On the off chance we didn't,
%% we'll throw this
rst_stream(Stream, ?STREAM_CLOSED, Conn)
end.
-spec send_h(
h2_stream_set:stream(),
hpack:headers()) ->
ok.
send_h(Stream, Headers) ->
case h2_stream_set:pid(Stream) of
undefined ->
%% TODO: Should this be some kind of error?
ok;
Pid ->
h2_stream:send_event(Pid, {send_h, Headers})
end.
-spec send_t(
h2_stream_set:stream(),
hpack:headers()) ->
ok.
send_t(Stream, Trailers) ->
case h2_stream_set:pid(Stream) of
undefined ->
%% TODO: Should this be some kind of error?
ok;
Pid ->
h2_stream:send_event(Pid, {send_t, Trailers})
end.
-spec recv_es(Stream :: h2_stream_set:stream(),
Conn :: connection()) ->
ok | {rst_stream, error_code()}.
recv_es(Stream, Conn) ->
case h2_stream_set:type(Stream) of
active ->
Pid = h2_stream_set:pid(Stream),
h2_stream:send_event(Pid, recv_es);
closed ->
rst_stream(Stream, ?STREAM_CLOSED, Conn);
idle ->
rst_stream(Stream, ?STREAM_CLOSED, Conn)
end.
-spec recv_pp(h2_stream_set:stream(),
hpack:headers()) ->
ok.
recv_pp(Stream, Headers) ->
case h2_stream_set:pid(Stream) of
undefined ->
%% Should this be an error?
ok;
Pid ->
h2_stream:send_event(Pid, {recv_pp, Headers})
end.
-spec recv_data(h2_stream_set:stream(),
h2_frame:frame()) ->
ok.
recv_data(Stream, Frame) ->
case h2_stream_set:pid(Stream) of
undefined ->
%% Again, error? These aren't errors now because the code
%% isn't set up to handle errors when these are called
%% anyway.
ok;
Pid ->
h2_stream:send_event(Pid, {recv_data, Frame})
end.
send_request(NextId, NotifyPid, Conn, Streams, Headers, Body) ->
case
h2_stream_set:new_stream(
NextId,
NotifyPid,
Conn#connection.stream_callback_mod,
Conn#connection.stream_callback_opts,
Conn#connection.socket,
Conn#connection.peer_settings#settings.initial_window_size,
Conn#connection.self_settings#settings.initial_window_size,
Streams)
of
{error, Code, _NewStream} ->
%% error creating new stream
{error, Code};
GoodStreamSet ->
send_headers(self(), NextId, Headers),
send_body(self(), NextId, Body),
{ok, GoodStreamSet}
end.
================================================
FILE: src/h2_frame.erl
================================================
-module(h2_frame).
-include("http2.hrl").
-export([
recv/1,
read/1,
read/2,
read_binary_frame_header/1,
read_binary_payload/2,
from_binary/1,
format_header/1,
format_payload/1,
format/1,
to_binary/1,
header_to_binary/1
]).
-type payload() :: h2_frame_data:payload()
| h2_frame_headers:payload()
| h2_frame_priority:payload()
| h2_frame_rst_stream:payload()
| h2_frame_settings:payload()
| h2_frame_push_promise:payload()
| h2_frame_ping:payload()
| h2_frame_goaway:payload()
| h2_frame_window_update:payload()
| h2_frame_continuation:payload().
-type header() :: #frame_header{}.
-type frame() :: {header(),
payload()}.
-export_type([frame/0, header/0, payload/0]).
%% Each frame type should be able to be read off a binary stream. If
%% the header is good, then it'll know how many more bytes to take off
%% the stream for the payload. If there are more bytes left over, it's
%% the next header, so we should return a tuple that contains the
%% remainder as well.
-callback read_binary(Bin::binary(),
Header::header()) ->
{ok, payload(), Remainder::binary()}
| {error, stream_id(), error_code(), binary()}.
%% For io:formatting
-callback format(payload()) -> iodata().
%% convert payload to binary
-callback to_binary(payload()) -> iodata().
%% TODO: some kind of callback for sending frames
%-callback send(port(), payload()) -> ok | {error, term()}.
-spec recv(binary() |
{header(), binary()}) ->
{ok, frame(), binary()}
| {not_enough_header, binary()}
| {not_enough_payload, header(), binary()}
| {error, stream_id(), error_code(), binary()}.
recv(Bin)
when is_binary(Bin), byte_size(Bin) < 9 ->
{not_enough_header, Bin};
recv(Bin)
when is_binary(Bin) ->
{Header, PayloadBin} = read_binary_frame_header(Bin),
recv({Header, PayloadBin});
recv({Header, PayloadBin})
when byte_size(PayloadBin) < Header#frame_header.length ->
{not_enough_payload, Header, PayloadBin};
recv({Header, PayloadBin}) ->
case read_binary_payload(PayloadBin, Header) of
{ok, Payload, Rem} ->
{ok, {Header, Payload}, Rem};
Error -> Error
end.
-spec read(socket()) -> frame().
read(Socket) ->
read(Socket, infinity).
-spec read(socket(), timeout()) -> frame() | {error, closed|inet:posix()}.
read(Socket, Timeout) ->
case read_header(Socket, Timeout) of
{error, Reason} ->
{error, Reason};
FrameHeader ->
{ok, Payload} = read_payload(Socket, FrameHeader, Timeout),
{FrameHeader, Payload}
end.
%% Hi, it's been a while. We need to massage the API here into
%% something that can handle an unknown number of bytes in a binary
%% containing an unknown quantity of frames
-spec from_binary(binary()) -> [frame()].
from_binary(Bin) ->
from_binary(Bin, []).
from_binary(<<>>, Acc) ->
Acc;
from_binary(Bin, Acc) ->
{Header, PayloadBin} = read_binary_frame_header(Bin),
{ok, Payload, Rem} = read_binary_payload(PayloadBin, Header),
from_binary(Rem, [{Header, Payload}|Acc]).
-spec format_header(header()) -> iodata().
format_header(#frame_header{
length = Length,
type = Type,
flags = Flags,
stream_id = StreamId
}) ->
io_lib:format("[Frame Header: L:~p, T:~p, F:~p, StrId:~p]", [Length, ?FT(Type), Flags, StreamId]).
-spec read_header(socket(), timeout()) -> header() | {error, closed|inet:posix()}.
read_header({Transport, Socket}, Timeout) ->
case Transport:recv(Socket, 9, Timeout) of
{ok, HeaderBytes} ->
{Header, <<>>} = read_binary_frame_header(HeaderBytes),
Header;
E -> E
end.
-spec read_binary_frame_header(binary()) -> {header(), binary()}.
read_binary_frame_header(<<Length:24,Type:8,Flags:8,_R:1,StreamId:31,Rem/bits>>) ->
Header = #frame_header{
length = Length,
type = Type,
flags = Flags,
stream_id = StreamId
},
{Header, Rem}.
-spec read_payload(socket(), header(), timeout()) ->
{ok, payload()} | {error, closed|inet:posix()}.
read_payload(_, Header=#frame_header{length=0}, _Timeout) ->
{ok, FramePayload, <<>>} = read_binary_payload(<<>>, Header),
{ok, FramePayload};
read_payload({Transport, Socket}, Header=#frame_header{length=L}, Timeout) ->
case Transport:recv(Socket, L, Timeout) of
{ok, DataBin} ->
{ok, FramePayload, <<>>} = read_binary_payload(DataBin, Header),
{ok, FramePayload};
E -> E
end.
-spec read_binary_payload(binary(), header()) ->
{ok, payload(), binary()}
| {error, error_code()}
| {error, stream_id(), error_code(), binary()}.
read_binary_payload(Bin, Header = #frame_header{type=?DATA}) ->
h2_frame_data:read_binary(Bin, Header);
read_binary_payload(Bin, Header = #frame_header{type=?HEADERS}) ->
h2_frame_headers:read_binary(Bin, Header);
read_binary_payload(Bin, Header = #frame_header{type=?PRIORITY}) ->
h2_frame_priority:read_binary(Bin, Header);
read_binary_payload(Bin, Header = #frame_header{type=?RST_STREAM}) ->
h2_frame_rst_stream:read_binary(Bin, Header);
read_binary_payload(Bin, Header = #frame_header{type=?SETTINGS}) ->
h2_frame_settings:read_binary(Bin, Header);
read_binary_payload(Bin, Header = #frame_header{type=?PUSH_PROMISE}) ->
h2_frame_push_promise:read_binary(Bin, Header);
read_binary_payload(Bin, Header = #frame_header{type=?PING}) ->
h2_frame_ping:read_binary(Bin, Header);
read_binary_payload(Bin, Header = #frame_header{type=?GOAWAY}) ->
h2_frame_goaway:read_binary(Bin, Header);
read_binary_payload(Bin, Header = #frame_header{type=?WINDOW_UPDATE}) ->
h2_frame_window_update:read_binary(Bin, Header);
read_binary_payload(Bin, Header = #frame_header{type=?CONTINUATION}) ->
h2_frame_continuation:read_binary(Bin, Header);
read_binary_payload(Bin, Header) ->
read_unsupported_frame_binary(Bin, Header).
read_unsupported_frame_binary(Bin,
#frame_header{length=0}) ->
{ok, <<>>, Bin};
read_unsupported_frame_binary(Bin,
#frame_header{length=L}) ->
<<PayloadBin:L/binary,Rem/bits>> = Bin,
{ok, PayloadBin, Rem}.
-spec format_payload(frame()) -> iodata().
format_payload({#frame_header{type=?DATA}, P}) ->
h2_frame_data:format(P);
format_payload({#frame_header{type=?HEADERS}, P}) ->
h2_frame_headers:format(P);
format_payload({#frame_header{type=?PRIORITY}, P}) ->
h2_frame_priority:format(P);
format_payload({#frame_header{type=?RST_STREAM}, P}) ->
h2_frame_rst_stream:format(P);
format_payload({#frame_header{type=?SETTINGS}, P}) ->
h2_frame_settings:format(P);
format_payload({#frame_header{type=?PUSH_PROMISE}, P}) ->
h2_frame_push_promise:format(P);
format_payload({#frame_header{type=?PING}, P}) ->
h2_frame_ping:format(P);
format_payload({#frame_header{type=?GOAWAY}, P}) ->
h2_frame_goaway:format(P);
format_payload({#frame_header{type=?WINDOW_UPDATE}, P}) ->
h2_frame_window_update:format(P);
format_payload({#frame_header{type=?CONTINUATION}, P}) ->
h2_frame_continuation:format(P);
format_payload({_, _P}) ->
"Unsupported Frame".
-spec format(frame()) -> iodata().
format(error) -> "error";
format({error, E}) -> io_lib:format("error : ~p",[E]);
format({Header, Payload}) ->
lists:flatten(io_lib:format("~s | ~s", [format_header(Header), format_payload({Header, Payload})]));
format(<<>>) -> "".
-spec to_binary(frame()) -> iodata().
to_binary({Header, Payload}) ->
{Type, PayloadBin} = payload_to_binary(Payload),
NewHeader = Header#frame_header{
length = iodata_size(PayloadBin),
type = Type
},
HeaderBin = header_to_binary(NewHeader),
[HeaderBin, PayloadBin].
-spec header_to_binary(header()) -> iodata().
header_to_binary(#frame_header{
length=L,
type=T,
flags=F,
stream_id=StreamId
}) ->
<<L:24,T:8,F:8,0:1,StreamId:31>>.
-spec payload_to_binary(payload()) -> {frame_type(), iodata()}.
payload_to_binary(P) ->
Type = payload_type(P),
Bin =
case Type of
?DATA -> h2_frame_data:to_binary(P);
?HEADERS -> h2_frame_headers:to_binary(P);
?PRIORITY -> h2_frame_priority:to_binary(P);
?RST_STREAM -> h2_frame_rst_stream:to_binary(P);
?SETTINGS -> h2_frame_settings:to_binary(P);
?PUSH_PROMISE -> h2_frame_push_promise:to_binary(P);
?PING -> h2_frame_ping:to_binary(P);
?GOAWAY -> h2_frame_goaway:to_binary(P);
?WINDOW_UPDATE -> h2_frame_window_update:to_binary(P);
?CONTINUATION -> h2_frame_continuation:to_binary(P)
end,
{Type, Bin}.
iodata_size(L) when is_list(L) ->
lists:foldl(fun(X, Acc) ->
Acc + iodata_size(X)
end, 0, L);
iodata_size(B) when is_binary(B) ->
byte_size(B).
%% Breaking my own abstraction here, but only for a performance
%% optimization. This function assumes that records are built with the
%% record name as the first element in the tuple, and if that changes,
%% or if payloads are no longer records, then this will stop working.
-spec payload_type(payload()) -> frame_type().
payload_type(P) when element(1, P) =:= data -> ?DATA;
payload_type(P) when element(1, P) =:= headers -> ?HEADERS;
payload_type(P) when element(1, P) =:= priority -> ?PRIORITY;
payload_type(P) when element(1, P) =:= rst_stream -> ?RST_STREAM;
payload_type(P) when element(1, P) =:= settings -> ?SETTINGS;
payload_type(P) when element(1, P) =:= push_promise -> ?PUSH_PROMISE;
payload_type(P) when element(1, P) =:= ping -> ?PING;
payload_type(P) when element(1, P) =:= goaway -> ?GOAWAY;
payload_type(P) when element(1, P) =:= window_update -> ?WINDOW_UPDATE;
payload_type(P) when element(1, P) =:= continuation -> ?CONTINUATION.
================================================
FILE: src/h2_frame_continuation.erl
================================================
-module(h2_frame_continuation).
-include("http2.hrl").
-behaviour(h2_frame).
-export(
[
block_fragment/1,
format/1,
new/1,
read_binary/2,
to_binary/1
]).
-record(continuation, {
block_fragment :: binary()
}).
-type payload() :: #continuation{}.
-type frame() :: {h2_frame:header(), payload()}.
-export_type([payload/0, frame/0]).
-spec block_fragment(payload()) -> binary().
block_fragment(#continuation{block_fragment=BF}) ->
BF.
-spec new(binary()) -> payload().
new(Bin) ->
#continuation{
block_fragment=Bin
}.
-spec read_binary(binary(), h2_frame:header()) ->
{ok, payload(), binary()}
| {error, stream_id(), error_code(), binary()}.
read_binary(_,
#frame_header{
stream_id=0
}) ->
{error, 0, ?PROTOCOL_ERROR, <<>>};
read_binary(Bin, #frame_header{length=Length}) ->
<<Data:Length/binary,Rem/bits>> = Bin,
Payload = #continuation{
block_fragment=Data
},
{ok, Payload, Rem}.
-spec format(payload()) -> iodata().
format(Payload) ->
io_lib:format("[Continuation: ~p ]", [Payload]).
-spec to_binary(payload()) -> iodata().
to_binary(#continuation{block_fragment=BF}) ->
BF.
================================================
FILE: src/h2_frame_data.erl
================================================
-module(h2_frame_data).
-include("http2.hrl").
-behaviour(h2_frame).
-export([
format/1,
read_binary/2,
to_frames/3,
to_binary/1,
data/1,
new/1
]).
-record(data, {
data :: iodata()
}).
-type payload() :: #data{}.
-type frame() :: {h2_frame:header(), payload()}.
-export_type([payload/0, frame/0]).
-spec data(payload()) -> iodata().
data(#data{data=D}) ->
D.
-spec format(payload()) -> iodata().
format(Payload) ->
BinToShow = case size(Payload) > 7 of
false ->
Payload#data.data;
true ->
<<Start:8/binary,_/binary>> = Payload#data.data,
Start
end,
io_lib:format("[Data: {data: ~p ...}]", [BinToShow]).
-spec new(binary()) -> payload().
new(Data) ->
#data{data=Data}.
-spec read_binary(binary(), h2_frame:header()) ->
{ok, payload(), binary()}
| {error, stream_id(), error_code(), binary()}.
read_binary(_, #frame_header{stream_id=0}) ->
{error, 0, ?PROTOCOL_ERROR, <<>>};
read_binary(Bin, _H=#frame_header{length=0}) ->
{ok, #data{data= <<>>}, Bin};
read_binary(Bin, H=#frame_header{length=L}) ->
<<PayloadBin:L/binary,Rem/bits>> = Bin,
case h2_padding:read_possibly_padded_payload(PayloadBin, H) of
{error, Code} ->
{error, Code};
Data ->
{ok, #data{data=Data}, Rem}
end.
-spec to_frames(stream_id(), iodata(), settings()) -> [h2_frame:frame()].
to_frames(StreamId, IOList, Settings)
when is_list(IOList) ->
to_frames(StreamId, iolist_to_binary(IOList), Settings);
to_frames(StreamId, Data, S=#settings{max_frame_size=MFS}) ->
L = byte_size(Data),
case L >= MFS of
false ->
[{#frame_header{
length=L,
type=?DATA,
flags=?FLAG_END_STREAM,
stream_id=StreamId
}, #data{data=Data}}];
true ->
<<ToSend:MFS/binary,Rest/binary>> = Data,
[{#frame_header{
length=MFS,
type=?DATA,
stream_id=StreamId
},
#data{data=ToSend}} | to_frames(StreamId, Rest, S)]
end.
-spec to_binary(payload()) -> iodata().
to_binary(#data{data=D}) ->
D.
================================================
FILE: src/h2_frame_goaway.erl
================================================
-module(h2_frame_goaway).
-include("http2.hrl").
-behaviour(h2_frame).
-export(
[
error_code/1,
format/1,
new/2,
read_binary/2,
to_binary/1
]).
-record(goaway, {
last_stream_id :: stream_id(),
error_code :: error_code(),
additional_debug_data = <<>> :: binary()
}).
-type payload() :: #goaway{}.
-type frame() :: {h2_frame:header(), payload()}.
-export_type([payload/0, frame/0]).
-spec error_code(payload()) -> error_code().
error_code(#goaway{error_code=EC}) ->
EC.
-spec format(payload()) -> iodata().
format(Payload) ->
io_lib:format("[GOAWAY: ~p]", [Payload]).
-spec new(stream_id(), error_code()) -> payload().
new(StreamId, ErrorCode) ->
#goaway{
last_stream_id = StreamId,
error_code = ErrorCode
}.
-spec read_binary(binary(), h2_frame:header()) ->
{ok, payload(), binary()}
| {error, stream_id(), error_code(), binary()}.
read_binary(Bin,
#frame_header{
length=L,
stream_id=0
}) ->
<<Data:L/binary,Rem/bits>> = Bin,
<<_R:1,LastStream:31,ErrorCode:32,Extra/bits>> = Data,
Payload = #goaway{
last_stream_id = LastStream,
error_code = ErrorCode,
additional_debug_data = Extra
},
{ok, Payload, Rem};
read_binary(_, _) ->
{error, 0, ?PROTOCOL_ERROR, <<>>}.
-spec to_binary(payload()) -> iodata().
to_binary(#goaway{
last_stream_id=LSID,
error_code=EC,
additional_debug_data=ADD
}) ->
[<<0:1,LSID:31,EC:32>>,ADD].
================================================
FILE: src/h2_frame_headers.erl
================================================
-module(h2_frame_headers).
-include("http2.hrl").
-behaviour(h2_frame).
-export(
[
format/1,
from_frames/1,
new/1,
new/2,
read_binary/2,
to_frames/5,
to_binary/1
]).
-record(headers,
{
priority = undefined :: h2_frame_priority:payload() | undefined,
block_fragment :: binary()
}).
-type payload() :: #headers{}.
-type frame() :: {h2_frame:header(), payload()}.
-export_type([payload/0, frame/0]).
-spec format(payload()) -> iodata().
format(Payload) ->
io_lib:format("[Headers: ~p]", [Payload]).
-spec new(binary()) -> payload().
new(BlockFragment) ->
#headers{block_fragment=BlockFragment}.
-spec new(h2_frame_priority:payload(),
binary()) ->
payload().
new(Priority, BlockFragment) ->
#headers{
priority=Priority,
block_fragment=BlockFragment
}.
-spec read_binary(binary(),
h2_frame:header()) ->
{ok, payload(), binary()}
| {error, stream_id(), error_code(), binary()}.
read_binary(_,
#frame_header{
stream_id=0
}) ->
{error, 0, ?PROTOCOL_ERROR, <<>>};
read_binary(Bin, H = #frame_header{length=L}) ->
<<PayloadBin:L/binary,Rem/bits>> = Bin,
case h2_padding:read_possibly_padded_payload(PayloadBin, H) of
{error, Code} ->
{error, 0, Code, Rem};
Data ->
{Priority, PSID, HeaderFragment} =
case is_priority(H) of
true ->
{P, PRem} = h2_frame_priority:read_priority(Data),
PStream = h2_frame_priority:stream_id(P),
{P, PStream, PRem};
false ->
{undefined, undefined, Data}
end,
case PSID =:= H#frame_header.stream_id of
true ->
{error, PSID, ?PROTOCOL_ERROR, Rem};
false ->
Payload = #headers{
priority=Priority,
block_fragment=HeaderFragment
},
{ok, Payload, Rem}
end
end.
is_priority(#frame_header{flags=F}) when ?IS_FLAG(F, ?FLAG_PRIORITY) ->
true;
is_priority(_) ->
false.
-spec to_frames(StreamId :: stream_id(),
Headers :: hpack:headers(),
EncodeContext :: hpack:context(),
MaxFrameSize :: pos_integer(),
EndStream :: boolean()) ->
{[h2_frame:frame()], hpack:context()}.
to_frames(StreamId, Headers, EncodeContext, MaxFrameSize, EndStream) ->
{ok, {HeadersBin, NewContext}} = hpack:encode(Headers, EncodeContext),
%% Break HeadersBin into chunks
Chunks = split(HeadersBin, MaxFrameSize),
Frames = build_frames(StreamId, Chunks, EndStream),
{Frames, NewContext}.
-spec to_binary(payload()) -> iodata().
to_binary(#headers{
priority=P,
block_fragment=BF
}) ->
case P of
undefined ->
BF;
_ ->
[h2_frame_priority:to_binary(P), BF]
end.
-spec from_frames([h2_frame:frame()]) -> binary().
from_frames([{#frame_header{type=?HEADERS},#headers{block_fragment=BF}}|Continuations])->
from_frames(Continuations, BF);
from_frames([{#frame_header{type=?PUSH_PROMISE},PP}|Continuations])->
BF = h2_frame_push_promise:block_fragment(PP),
from_frames(Continuations, BF).
-spec from_frames([h2_frame:frame()], binary()) -> binary().
from_frames([], Acc) ->
Acc;
from_frames([{#frame_header{type=?CONTINUATION},Cont}|Continuations], Acc) ->
BF = h2_frame_continuation:block_fragment(Cont),
from_frames(Continuations, <<Acc/binary,BF/binary>>).
-spec split(Binary::binary(),
MaxFrameSize::pos_integer()) ->
[binary()].
split(Binary, MaxFrameSize) ->
split(Binary, MaxFrameSize, []).
-spec split(Binary::binary(),
MaxFrameSize::pos_integer(),
[binary()]) ->
[binary()].
split(Binary, MaxFrameSize, Acc)
when byte_size(Binary) =< MaxFrameSize ->
lists:reverse([Binary|Acc]);
split(Binary, MaxFrameSize, Acc) ->
<<NextFrame:MaxFrameSize/binary,Remaining/binary>> = Binary,
split(Remaining, MaxFrameSize, [NextFrame|Acc]).
%% Now build frames.
%% The first will be a HEADERS frame, followed by CONTINUATION
%% If EndStream, that flag needs to be set on the first frame
%% ?FLAG_END_HEADERS needs to be set on the last.
%% If there's only one, it needs to be set on both.
-spec build_frames(StreamId :: stream_id(),
Chunks::[binary()],
EndStream::boolean()) ->
[h2_frame:frame()].
build_frames(StreamId, [FirstChunk|Rest], EndStream) ->
Flag = case EndStream of
true ->
?FLAG_END_STREAM;
false ->
0
end,
HeadersFrame =
{ #frame_header{
type=?HEADERS,
flags=Flag,
length=byte_size(FirstChunk),
stream_id=StreamId},
#headers{
block_fragment=FirstChunk}},
[{LastFrameHeader, LastFrameBody}|Frames] =
build_frames_(StreamId, Rest, [HeadersFrame]),
NewLastFrame = {
LastFrameHeader#frame_header{
flags=LastFrameHeader#frame_header.flags bor ?FLAG_END_HEADERS
},
LastFrameBody},
lists:reverse([NewLastFrame|Frames]).
-spec build_frames_(StreamId::stream_id(),
Chunks::[binary()],
Acc::[h2_frame:frame()])->
[h2_frame:frame()].
build_frames_(_StreamId, [], Acc) ->
Acc;
build_frames_(StreamId, [NextChunk|Rest], Acc) ->
NextFrame = {
#frame_header{
stream_id=StreamId,
type=?CONTINUATION,
flags=0,
length=byte_size(NextChunk)
},
h2_frame_continuation:new(NextChunk)
},
build_frames_(StreamId, Rest, [NextFrame|Acc]).
================================================
FILE: src/h2_frame_ping.erl
================================================
-module(h2_frame_ping).
-include("http2.hrl").
-behaviour(h2_frame).
-export(
[
format/1,
read_binary/2,
to_binary/1,
ack/1,
new/1
]).
-record(ping, {
opaque_data :: binary()
}).
-type payload() :: #ping{}.
-type frame() :: {h2_frame:header(), payload()}.
-export_type([payload/0, frame/0]).
-spec format(payload()) -> iodata().
format(Payload) ->
io_lib:format("[Ping: ~p]", [Payload]).
-spec new(binary()) -> payload().
new(Bin) ->
#ping{opaque_data=Bin}.
-spec read_binary(binary(), h2_frame:header()) ->
{ok, payload(), binary()}
| {error, stream_id(), error_code(), binary()}.
read_binary(_,
#frame_header{
length=L
})
when L =/= 8->
{error, 0, ?FRAME_SIZE_ERROR, <<>>};
read_binary(<<Data:8/binary,Rem/bits>>,
#frame_header{
length=8,
stream_id=0
}
) ->
Payload = #ping{
opaque_data = Data
},
{ok, Payload, Rem};
read_binary(_, _) ->
{error, 0, ?PROTOCOL_ERROR, <<>>}.
-spec to_binary(payload()) -> iodata().
to_binary(#ping{opaque_data=D}) ->
D.
-spec ack(payload()) -> {h2_frame:header(), payload()}.
ack(Ping) ->
{#frame_header{
length = 8,
type = ?PING,
flags = ?FLAG_ACK,
stream_id = 0
}, Ping}.
================================================
FILE: src/h2_frame_priority.erl
================================================
-module(h2_frame_priority).
-include("http2.hrl").
-behaviour(h2_frame).
-export(
[
format/1,
new/3,
stream_id/1,
read_binary/2,
read_priority/1,
to_binary/1
]).
-record(priority, {
exclusive = 0 :: 0 | 1,
stream_id = 0 :: stream_id(),
weight = 0 :: non_neg_integer()
}).
-type payload() :: #priority{}.
-type frame() :: {h2_frame:header(), payload()}.
-export_type([payload/0, frame/0]).
-spec format(payload()) -> iodata().
format(Payload) ->
io_lib:format("[Priority: ~p]", [Payload]).
-spec new(0|1, stream_id(), non_neg_integer()) -> payload().
new(Exclusive, StreamId, Weight) ->
#priority{
exclusive=Exclusive,
stream_id=StreamId,
weight=Weight
}.
-spec read_binary(binary(), h2_frame:header()) ->
{ok, payload(), binary()}
| {error, stream_id(), error_code(), binary()}.
read_binary(_,
#frame_header{
stream_id=0
}) ->
{error, 0, ?PROTOCOL_ERROR, <<>>};
read_binary(_,
#frame_header{
length=L
})
when L =/= 5 ->
{error, 0, ?FRAME_SIZE_ERROR, <<>>};
read_binary(Bin,
#frame_header{
length=5
}=H) ->
{Payload, Rem} = read_priority(Bin),
PriorityStream = Payload#priority.stream_id,
case H#frame_header.stream_id of
0 ->
{error, 0, ?PROTOCOL_ERROR, Rem};
PriorityStream ->
{error, H#frame_header.stream_id, ?PROTOCOL_ERROR, Rem};
_ ->
{ok, Payload, Rem}
end.
-spec read_priority(binary()) -> {payload(), binary()}.
read_priority(Binary) ->
<<Exclusive:1,StreamId:31,Weight:8,Rem/bits>> = Binary,
Payload = #priority{
exclusive = Exclusive,
stream_id = StreamId,
weight = Weight
},
{Payload, Rem}.
-spec stream_id(payload()) -> stream_id().
stream_id(#priority{stream_id=S}) ->
S.
-spec to_binary(payload()) -> iodata().
to_binary(#priority{
exclusive=E,
stream_id=StreamId,
weight=W
}) ->
<<E:1,StreamId:31,W:8>>.
================================================
FILE: src/h2_frame_push_promise.erl
================================================
-module(h2_frame_push_promise).
-include("http2.hrl").
-behaviour(h2_frame).
-export(
[
block_fragment/1,
format/1,
new/2,
promised_stream_id/1,
read_binary/2,
to_binary/1,
to_frame/4
]).
-record(push_promise, {
promised_stream_id :: stream_id(),
block_fragment :: binary()
}).
-type payload() :: #push_promise{}.
-type frame() :: {h2_frame:header(), payload()}.
-export_type([payload/0, frame/0]).
-spec block_fragment(payload()) -> binary().
block_fragment(#push_promise{block_fragment=BF}) ->
BF.
-spec promised_stream_id(payload()) -> stream_id().
promised_stream_id(#push_promise{promised_stream_id=PSID}) ->
PSID.
-spec format(payload()) -> iodata().
format(Payload) ->
io_lib:format("[Headers: ~p]", [Payload]).
-spec new(stream_id(), binary()) -> payload().
new(StreamId, Bin) ->
#push_promise{
promised_stream_id=StreamId,
block_fragment=Bin
}.
-spec read_binary(binary(), h2_frame:header()) ->
{ok, payload(), binary()}
| {error, stream_id(), error_code(), binary()}.
read_binary(_,
#frame_header{
stream_id=0
}) ->
{error, 0, ?PROTOCOL_ERROR, <<>>};
read_binary(Bin, H=#frame_header{length=L}) ->
<<PayloadBin:L/binary,Rem/binary>> = Bin,
Data = h2_padding:read_possibly_padded_payload(PayloadBin, H),
<<_R:1,Stream:31,BlockFragment/bits>> = Data,
Payload = #push_promise{
promised_stream_id=Stream,
block_fragment=BlockFragment
},
{ok, Payload, Rem}.
-spec to_frame(pos_integer(), pos_integer(), hpack:headers(), hpack:context()) ->
{{h2_frame:header(), payload()}, hpack:context()}.
%% Maybe break this up into continuations like the data frame
to_frame(StreamId, PStreamId, Headers, EncodeContext) ->
{ok, {HeadersToSend, NewContext}} = hpack:encode(Headers, EncodeContext),
L = byte_size(HeadersToSend),
{{#frame_header{
length=L,
type=?PUSH_PROMISE,
flags=?FLAG_END_HEADERS,
stream_id=StreamId
},
#push_promise{
promised_stream_id=PStreamId,
block_fragment=HeadersToSend
}},
NewContext}.
-spec to_binary(payload()) -> iodata().
to_binary(#push_promise{
promised_stream_id=PSID,
block_fragment=BF
}) ->
%% TODO: allow for padding as per HTTP/2 SPEC
<<0:1,PSID:31,BF/binary>>.
================================================
FILE: src/h2_frame_rst_stream.erl
================================================
-module(h2_frame_rst_stream).
-include("http2.hrl").
-behaviour(h2_frame).
-export([
new/1,
error_code/1,
format/1,
read_binary/2,
to_binary/1
]).
-record(rst_stream, {
error_code :: error_code()
}).
-type payload() :: #rst_stream{}.
-type frame() :: {h2_frame:header(), payload()}.
-export_type([payload/0, frame/0]).
-spec new(error_code()) -> payload().
new(ErrorCode) ->
#rst_stream{
error_code=ErrorCode
}.
-spec error_code(payload()) -> error_code().
error_code(#rst_stream{error_code=EC}) ->
EC.
-spec format(payload()) -> iodata().
format(Payload) ->
io_lib:format("[RST Stream: ~p]", [Payload]).
-spec read_binary(binary(), h2_frame:header()) ->
{ok, payload(), binary()}
| {error, stream_id(), error_code(), binary()}.
read_binary(_,
#frame_header{
stream_id=0
}) ->
{error, 0, ?PROTOCOL_ERROR, <<>>};
read_binary(_,
#frame_header{
length=L
})
when L =/= 4 ->
{error, 0, ?FRAME_SIZE_ERROR, <<>>};
read_binary(<<ErrorCode:32,Rem/bits>>, #frame_header{length=4}) ->
Payload = #rst_stream{
error_code = ErrorCode
},
{ok, Payload, Rem}.
-spec to_binary(payload()) -> iodata().
to_binary(#rst_stream{error_code=C}) ->
<<C:32>>.
================================================
FILE: src/h2_frame_settings.erl
================================================
-module(h2_frame_settings).
-include("http2.hrl").
-behaviour(h2_frame).
-export(
[
format/1,
read_binary/2,
send/1,
send/2,
ack/0,
ack/1,
to_binary/1,
overlay/2,
validate/1
]).
%%TODO
-type payload() :: #settings{} | {settings, proplist()}.
-type frame() :: {h2_frame:header(), payload()}.
-type name() :: binary().
-type property() :: {name(), any()}.
-type proplist() :: [property()].
-export_type([payload/0, name/0, property/0, proplist/0, frame/0]).
-spec format(payload()
| binary()
| {settings, [proplists:property()]}
) -> iodata().
format(<<>>) -> "Ack!";
format(#settings{
header_table_size = HTS,
enable_push = EP,
max_concurrent_streams = MCS,
initial_window_size = IWS,
max_frame_size = MFS,
max_header_list_size = MHLS
}) ->
lists:flatten(
io_lib:format("[Settings: "
" header_table_size = ~p,"
" enable_push = ~p,"
" max_concurrent_streams = ~p,"
" initial_window_size = ~p,"
" max_frame_size = ~p,"
" max_header_list_size = ~p~n]", [HTS,EP,MCS,IWS,MFS,MHLS]));
format({settings, PList}) ->
L = lists:map(fun({?SETTINGS_HEADER_TABLE_SIZE,V}) ->
{header_table_size,V};
({?SETTINGS_ENABLE_PUSH,V}) ->
{enable_push,V};
({?SETTINGS_MAX_CONCURRENT_STREAMS,V}) ->
{max_concurrent_streams,V};
({?SETTINGS_INITIAL_WINDOW_SIZE,V}) ->
{initial_window_size, V};
({?SETTINGS_MAX_FRAME_SIZE,V}) ->
{max_frame_size,V};
({?SETTINGS_MAX_HEADER_LIST_SIZE,V}) ->
{max_header_list_size,V}
end,
PList),
io_lib:format("~p", [L]).
-spec read_binary(binary(), h2_frame:header()) ->
{ok, payload(), binary()}
| {error, stream_id(), error_code(), binary()}.
read_binary(Bin,
#frame_header{
length=0,
stream_id=0
}) ->
{ok, {settings, []}, Bin};
read_binary(Bin,
#frame_header{
length=Length,
stream_id=0
}) ->
<<SettingsBin:Length/binary,Rem/bits>> = Bin,
Settings = parse_settings(SettingsBin),
{ok, {settings, Settings}, Rem};
read_binary(_, _) ->
{error, 0, ?PROTOCOL_ERROR, <<>>}.
-spec parse_settings(binary()) -> [proplists:property()].
parse_settings(Bin) ->
lists:reverse(parse_settings(Bin, [])).
-spec parse_settings(binary(), [proplists:property()]) -> [proplists:property()].
parse_settings(<<0,1,Val:4/binary,T/binary>>, S) ->
parse_settings(T, [{?SETTINGS_HEADER_TABLE_SIZE, binary:decode_unsigned(Val)}|S]);
parse_settings(<<0,2,Val:4/binary,T/binary>>, S) ->
parse_settings(T, [{?SETTINGS_ENABLE_PUSH, binary:decode_unsigned(Val)}|S]);
parse_settings(<<0,3,Val:4/binary,T/binary>>, S) ->
parse_settings(T, [{?SETTINGS_MAX_CONCURRENT_STREAMS, binary:decode_unsigned(Val)}|S]);
parse_settings(<<0,4,Val:4/binary,T/binary>>, S) ->
parse_settings(T, [{?SETTINGS_INITIAL_WINDOW_SIZE, binary:decode_unsigned(Val)}|S]);
parse_settings(<<0,5,Val:4/binary,T/binary>>, S) ->
parse_settings(T, [{?SETTINGS_MAX_FRAME_SIZE, binary:decode_unsigned(Val)}|S]);
parse_settings(<<0,6,Val:4/binary,T/binary>>, S)->
parse_settings(T, [{?SETTINGS_MAX_HEADER_LIST_SIZE, binary:decode_unsigned(Val)}|S]);
% An endpoint that receives a SETTINGS frame with any unknown or unsupported identifier
% MUST ignore that setting
parse_settings(<<_:6/binary,T/binary>>, S)->
parse_settings(T, S);
parse_settings(<<>>, Settings) ->
Settings.
-spec overlay(payload(), {settings, [proplists:property()]}) -> payload().
overlay(S, Setting) ->
overlay_(S, S, Setting).
overlay_(OriginalS, S, {settings, [{?SETTINGS_HEADER_TABLE_SIZE, Val}|PList]}) ->
overlay_(OriginalS, S#settings{header_table_size=Val}, {settings, PList});
overlay_(OriginalS, S, {settings, [{?SETTINGS_ENABLE_PUSH, Val}|PList]}) ->
overlay_(OriginalS, S#settings{enable_push=Val}, {settings, PList});
overlay_(OriginalS, S, {settings, [{?SETTINGS_MAX_CONCURRENT_STREAMS, Val}|PList]}) ->
overlay_(OriginalS, S#settings{max_concurrent_streams=Val}, {settings, PList});
overlay_(OriginalS, S, {settings, [{?SETTINGS_INITIAL_WINDOW_SIZE, Val}|PList]}) ->
overlay_(OriginalS, S#settings{initial_window_size=Val}, {settings, PList});
overlay_(OriginalS, S, {settings, [{?SETTINGS_MAX_FRAME_SIZE, Val}|PList]}) ->
overlay_(OriginalS, S#settings{max_frame_size=Val}, {settings, PList});
overlay_(OriginalS, S, {settings, [{?SETTINGS_MAX_HEADER_LIST_SIZE, Val}|PList]}) ->
overlay_(OriginalS, S#settings{max_header_list_size=Val}, {settings, PList});
overlay_(OriginalS, _S, {settings, [{_UnknownOrUnsupportedKey, _Val}|_]}) ->
OriginalS;
overlay_(_OriginalS, S, {settings, []}) ->
S.
-spec send(payload()) -> binary().
send(Settings) ->
List = h2_settings:to_proplist(Settings),
Payload = make_payload(List),
L = size(Payload),
Header = <<L:24,?SETTINGS:8,16#0:8,0:1,0:31>>,
<<Header/binary, Payload/binary>>.
-spec send(payload(), payload()) -> binary().
send(PrevSettings, NewSettings) ->
Diff = h2_settings:diff(PrevSettings, NewSettings),
Payload = make_payload(Diff),
L = size(Payload),
Header = <<L:24,?SETTINGS:8,16#0:8,0:1,0:31>>,
<<Header/binary, Payload/binary>>.
-spec make_payload(proplist()) -> binary().
make_payload(Diff) ->
make_payload_(lists:reverse(Diff), <<>>).
make_payload_([], BinAcc) ->
BinAcc;
make_payload_([{_, unlimited}|Tail], BinAcc) ->
make_payload_(Tail, BinAcc);
make_payload_([{<<Setting>>, Value}|Tail], BinAcc) ->
make_payload_(Tail, <<Setting:16,Value:32,BinAcc/binary>>).
-spec ack() -> binary().
ack() ->
<<0:24,4:8,1:8,0:1,0:31>>.
-spec ack(socket()) -> ok | {error, term()}.
ack({Transport,Socket}) ->
Transport:send(Socket, <<0:24,4:8,1:8,0:1,0:31>>).
-spec to_binary(payload()) -> iodata().
to_binary(#settings{}=Settings) ->
[to_binary(S, Settings) || S <- ?SETTING_NAMES].
-spec to_binary(binary(), payload()) -> binary().
to_binary(?SETTINGS_HEADER_TABLE_SIZE, #settings{header_table_size=undefined}) ->
<<>>;
to_binary(?SETTINGS_HEADER_TABLE_SIZE, #settings{header_table_size=HTS}) ->
<<16#1:16,HTS:32>>;
to_binary(?SETTINGS_ENABLE_PUSH, #settings{enable_push=undefined}) ->
<<>>;
to_binary(?SETTINGS_ENABLE_PUSH, #settings{enable_push=EP}) ->
<<16#2:16,EP:32>>;
to_binary(?SETTINGS_MAX_CONCURRENT_STREAMS, #settings{max_concurrent_streams=undefined}) ->
<<>>;
to_binary(?SETTINGS_MAX_CONCURRENT_STREAMS, #settings{max_concurrent_streams=MCS}) ->
<<16#3:16,MCS:32>>;
to_binary(?SETTINGS_INITIAL_WINDOW_SIZE, #settings{initial_window_size=undefined}) ->
<<>>;
to_binary(?SETTINGS_INITIAL_WINDOW_SIZE, #settings{initial_window_size=IWS}) ->
<<16#4:16,IWS:32>>;
to_binary(?SETTINGS_MAX_FRAME_SIZE, #settings{max_frame_size=undefined}) ->
<<>>;
to_binary(?SETTINGS_MAX_FRAME_SIZE, #settings{max_frame_size=MFS}) ->
<<16#5:16,MFS:32>>;
to_binary(?SETTINGS_MAX_HEADER_LIST_SIZE, #settings{max_header_list_size=undefined}) ->
<<>>;
to_binary(?SETTINGS_MAX_HEADER_LIST_SIZE, #settings{max_header_list_size=MHLS}) ->
<<16#6:16,MHLS:32>>.
-spec validate({settings, [proplists:property()]}) -> ok | {error, integer()}.
validate({settings, PList}) ->
validate_(PList).
validate_([]) ->
ok;
validate_([{?SETTINGS_ENABLE_PUSH, Push}|_T])
when Push > 1; Push < 0 ->
{error, ?PROTOCOL_ERROR};
validate_([{?SETTINGS_INITIAL_WINDOW_SIZE, Size}|_T])
when Size >=2147483648 ->
{error, ?FLOW_CONTROL_ERROR};
validate_([{?SETTINGS_MAX_FRAME_SIZE, Size}|_T])
when Size < 16384; Size > 16777215 ->
{error, ?PROTOCOL_ERROR};
validate_([_H|T]) ->
validate_(T).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
make_payload_test() ->
Diff = [
{?SETTINGS_MAX_CONCURRENT_STREAMS, 2},
{?SETTINGS_MAX_FRAME_SIZE, 2048}
],
Bin = make_payload(Diff),
<<MCSIndex>> = ?SETTINGS_MAX_CONCURRENT_STREAMS,
<<MFSIndex>> = ?SETTINGS_MAX_FRAME_SIZE,
?assertEqual(<<MCSIndex:16,2:32,MFSIndex:16,2048:32>>, Bin),
ok.
validate_test() ->
?assertEqual(ok, validate_([])),
?assertEqual(ok, validate_([{?SETTINGS_ENABLE_PUSH, 0}])),
?assertEqual(ok, validate_([{?SETTINGS_ENABLE_PUSH, 1}])),
?assertEqual({error, ?PROTOCOL_ERROR}, validate_([{?SETTINGS_ENABLE_PUSH, 2}])),
?assertEqual({error, ?PROTOCOL_ERROR}, validate_([{?SETTINGS_ENABLE_PUSH, -1}])),
?assertEqual({error, ?FLOW_CONTROL_ERROR}, validate_([{?SETTINGS_INITIAL_WINDOW_SIZE, 2147483648}])),
?assertEqual(ok, validate_([{?SETTINGS_INITIAL_WINDOW_SIZE, 2147483647}])),
?assertEqual({error, ?PROTOCOL_ERROR}, validate_([{?SETTINGS_MAX_FRAME_SIZE, 16383}])),
?assertEqual(ok, validate_([{?SETTINGS_MAX_FRAME_SIZE, 16384}])),
?assertEqual(ok, validate_([{?SETTINGS_MAX_FRAME_SIZE, 16777215}])),
?assertEqual({error, ?PROTOCOL_ERROR}, validate_([{?SETTINGS_MAX_FRAME_SIZE, 16777216}])),
ok.
-endif.
================================================
FILE: src/h2_frame_window_update.erl
================================================
-module(h2_frame_window_update).
-include("http2.hrl").
-behaviour(h2_frame).
-export(
[
new/1,
format/1,
read_binary/2,
send/3,
size_increment/1,
to_binary/1
]).
-record(window_update, {
window_size_increment :: non_neg_integer()
}).
-type payload() :: #window_update{}.
-type frame() :: {h2_frame:header(), payload()}.
-export_type([payload/0, frame/0]).
-spec format(payload()) -> iodata().
format(Payload) ->
io_lib:format("[Window Update: ~p]", [Payload]).
-spec new(non_neg_integer()) -> payload().
new(Increment) ->
#window_update{window_size_increment=Increment}.
-spec read_binary(Bin::binary(),
Header::h2_frame:header()) ->
{ok, payload(), binary()}
| {error, stream_id(), error_code(), binary()}.
read_binary(_,
#frame_header{
length=L
})
when L =/= 4 ->
{error, 0, ?FRAME_SIZE_ERROR, <<>>};
read_binary(<<_R:1,0:31,Rem/bits>>, FH) ->
{error, FH#frame_header.stream_id, ?PROTOCOL_ERROR, Rem};
read_binary(Bin, #frame_header{length=4}) ->
<<_R:1,Increment:31,Rem/bits>> = Bin,
Payload = #window_update{
window_size_increment=Increment
},
{ok, Payload, Rem};
read_binary(_, #frame_header{}=H) ->
%TODO: Maybe return some Rem in element(4) once we know what that
%is
{error, H#frame_header.stream_id, ?FRAME_SIZE_ERROR, <<>>}.
-spec send(sock:socket(),
non_neg_integer(),
stream_id()) ->
ok.
send(Socket, WindowSizeIncrement, StreamId) ->
sock:send(Socket, [
<<4:24,?WINDOW_UPDATE:8,0:8,0:1,StreamId:31>>,
<<0:1,WindowSizeIncrement:31>>]).
-spec size_increment(payload()) -> non_neg_integer().
size_increment(#window_update{window_size_increment=WSI}) ->
WSI.
-spec to_binary(payload()) -> iodata().
to_binary(#window_update{
window_size_increment=I
}) ->
<<0:1,I:31>>.
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
read_binary_zero_test() ->
?assertEqual({error, 0, ?PROTOCOL_ERROR, <<>>},
read_binary(<<0:1,0:31>>, #frame_header{stream_id=0,length=4})),
?assertEqual({error, 2, ?PROTOCOL_ERROR, <<>>},
read_binary(<<1:1,0:31>>, #frame_header{stream_id=2,length=4})),
ok.
-endif.
================================================
FILE: src/h2_padding.erl
================================================
-module(h2_padding).
-include("http2.hrl").
-export([
is_padded/1,
read_possibly_padded_payload/2
]).
-spec is_padded(h2_frame:header()) -> boolean().
is_padded(#frame_header{flags=Flags})
when ?IS_FLAG(Flags, ?FLAG_PADDED) ->
true;
is_padded(_) ->
false.
-spec read_possibly_padded_payload(binary(),
h2_frame:header())
-> binary() | {error, error_code()}.
read_possibly_padded_payload(Bin, H=#frame_header{flags=F})
when ?IS_FLAG(F, ?FLAG_PADDED) ->
read_padded_payload(Bin, H);
read_possibly_padded_payload(Bin, Header) ->
read_unpadded_payload(Bin, Header).
-spec read_padded_payload(binary(), h2_frame:header())
-> binary() | {error, error_code()}.
read_padded_payload(<<Padding:8,Bytes/bits>>,
#frame_header{length=Length}) ->
L = Length - Padding - 1, % Exclude Pad length field (1 byte)
case L >= 0 of
true ->
<<Data:L/binary,_:Padding/binary>> = Bytes,
Data;
false ->
{error, ?PROTOCOL_ERROR}
end.
-spec read_unpadded_payload(binary(), h2_frame:header())
-> binary().
read_unpadded_payload(Data, _H) ->
Data.
================================================
FILE: src/h2_settings.erl
================================================
-module(h2_settings).
-include("http2.hrl").
-export([
diff/2,
to_proplist/1
]).
-spec diff(settings(), settings()) -> settings_proplist().
diff(OldSettings, NewSettings) ->
OldPl = to_proplist(OldSettings),
NewPl = to_proplist(NewSettings),
diff_(OldPl, NewPl, []).
diff_([],[],Acc) ->
lists:reverse(Acc);
diff_([OldH|OldT],[OldH|NewT],Acc) ->
diff_(OldT, NewT, Acc);
diff_([_OldH|OldT],[NewH|NewT],Acc) ->
diff_(OldT, NewT, [NewH|Acc]).
-spec to_proplist(settings()) -> settings_proplist().
to_proplist(Settings) ->
[
{?SETTINGS_HEADER_TABLE_SIZE, Settings#settings.header_table_size },
{?SETTINGS_ENABLE_PUSH, Settings#settings.enable_push },
{?SETTINGS_MAX_CONCURRENT_STREAMS, Settings#settings.max_concurrent_streams},
{?SETTINGS_INITIAL_WINDOW_SIZE, Settings#settings.initial_window_size },
{?SETTINGS_MAX_FRAME_SIZE, Settings#settings.max_frame_size },
{?SETTINGS_MAX_HEADER_LIST_SIZE, Settings#settings.max_header_list_size }
].
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
diff_test() ->
Old = #settings{},
New = #settings{
max_frame_size=2048
},
Diff = diff(Old, New),
?assertEqual([{?SETTINGS_MAX_FRAME_SIZE, 2048}], Diff),
ok.
diff_order_test() ->
Old = #settings{},
New = #settings{
max_frame_size = 2048,
max_concurrent_streams = 2
},
Diff = diff(Old, New),
?assertEqual(
[{?SETTINGS_MAX_CONCURRENT_STREAMS, 2},
{?SETTINGS_MAX_FRAME_SIZE, 2048}],
Diff
),
?assertNotEqual(
[
{?SETTINGS_MAX_FRAME_SIZE, 2048},
{?SETTINGS_MAX_CONCURRENT_STREAMS, 2}
],
Diff
),
ok.
-endif.
================================================
FILE: src/h2_stream.erl
================================================
-module(h2_stream).
-include("http2.hrl").
%% Public API
-export([
start_link/5,
send_event/2,
send_pp/2,
send_data/2,
stream_id/0,
call/2,
connection/0,
send_window_update/1,
send_connection_window_update/1,
rst_stream/2,
stop/1
]).
%% gen_statem callbacks
-behaviour(gen_statem).
-export([init/1,
callback_mode/0,
terminate/3,
code_change/4]).
%% gen_statem states
-export([
idle/3,
reserved_local/3,
reserved_remote/3,
open/3,
half_closed_local/3,
half_closed_remote/3,
closed/3
]).
-type stream_state_name() :: 'idle'
| 'open'
| 'closed'
| 'reserved_local'
| 'reserved_remote'
| 'half_closed_local'
| 'half_closed_remote'.
-record(stream_state, {
stream_id = undefined :: stream_id(),
connection = undefined :: undefined | pid(),
socket = undefined :: sock:socket(),
state = idle :: stream_state_name(),
incoming_frames = queue:new() :: queue:queue(h2_frame:frame()),
request_headers = [] :: hpack:headers(),
request_body :: iodata() | undefined,
request_body_size = 0 :: non_neg_integer(),
request_end_stream = false :: boolean(),
request_end_headers = false :: boolean(),
response_headers = [] :: hpack:headers(),
response_trailers = [] :: hpack:headers(),
response_body :: iodata() | undefined,
response_end_headers = false :: boolean(),
response_end_stream = false :: boolean(),
next_state = undefined :: undefined | stream_state_name(),
promised_stream = undefined :: undefined | state(),
callback_state = undefined :: any(),
callback_mod = undefined :: module()
}).
-type state() :: #stream_state{}.
-type callback_state() :: any().
-export_type([state/0, callback_state/0]).
-callback init(
Conn :: pid(),
StreamId :: stream_id(),
CallbackOptions :: list()
) ->
{ok, callback_state()}.
-callback on_receive_request_headers(
Headers :: hpack:headers(),
CallbackState :: callback_state()) ->
{ok, NewState :: callback_state()}.
-callback on_send_push_promise(
Headers :: hpack:headers(),
CallbackState :: callback_state()) ->
{ok, NewState :: callback_state()}.
-callback on_receive_request_data(
iodata(),
CallbackState :: callback_state())->
{ok, NewState :: callback_state()}.
-callback on_request_end_stream(
CallbackState :: callback_state()) ->
{ok, NewState :: callback_state()}.
%% Public API
-spec start_link(
StreamId :: stream_id(),
Connection :: pid(),
CallbackModule :: module(),
CallbackOptions :: list(),
Socket :: sock:socket()
) ->
{ok, pid()} | ignore | {error, term()}.
start_link(StreamId, Connection, CallbackModule, CallbackOptions, Socket) ->
gen_statem:start_link(?MODULE,
[StreamId,
Connection,
CallbackModule,
CallbackOptions,
Socket],
[]).
send_event(Pid, Event) ->
gen_statem:cast(Pid, Event).
-spec send_pp(pid(), hpack:headers()) ->
ok.
send_pp(Pid, Headers) ->
gen_statem:cast(Pid, {send_pp, Headers}).
-spec send_data(pid(), h2_frame_data:frame()) ->
ok | flow_control.
send_data(Pid, Frame) ->
gen_statem:cast(Pid, {send_data, Frame}).
-spec stream_id() -> stream_id().
stream_id() ->
gen_statem:call(self(), stream_id).
call(Pid, Msg) ->
gen_statem:call(Pid, Msg).
-spec connection() -> pid().
connection() ->
gen_statem:call(self(), connection).
-spec send_window_update(non_neg_integer()) -> ok.
send_window_update(Size) ->
gen_statem:cast(self(), {send_window_update, Size}).
-spec send_connection_window_update(non_neg_integer()) -> ok.
send_connection_window_update(Size) ->
gen_statem:cast(self(), {send_connection_window_update, Size}).
rst_stream(Pid, Code) ->
gen_statem:call(Pid, {rst_stream, Code}).
-spec stop(pid()) -> ok.
stop(Pid) ->
gen_statem:stop(Pid).
init([
StreamId,
ConnectionPid,
CB,
CBOptions,
Socket
]) ->
%% TODO: Check for CB implementing this behaviour
{ok, CallbackState} = CB:init(ConnectionPid, StreamId, [Socket | CBOptions]),
{ok, idle, #stream_state{
callback_mod=CB,
socket=Socket,
stream_id=StreamId,
connection=ConnectionPid,
callback_state=CallbackState
}}.
callback_mode() ->
state_functions.
%% IMPORTANT: If we're in an idle state, we can only send/receive
%% HEADERS frames. The diagram in the spec wants you believe that you
%% can send or receive PUSH_PROMISES too, but that's a LIE. What you
%% can do is send PPs from the open or half_closed_remote state, or
%% receive them in the open or half_closed_local state. Then, that
%% will create a new stream in the idle state and THAT stream can
%% transition to one of the reserved states, but you'll never get a
%% PUSH_PROMISE frame with that Stream Id. It's a subtle thing, but it
%% drove me crazy until I figured it out
%% Server 'RECV H'
idle(cast, {recv_h, Headers},
#stream_state{
callback_mod=CB,
callback_state=CallbackState
}=Stream) ->
case is_valid_headers(request, Headers) of
ok ->
{ok, NewCBState} = CB:on_receive_request_headers(Headers, CallbackState),
{next_state,
open,
Stream#stream_state{
request_headers=Headers,
callback_state=NewCBState
}};
{error, Code} ->
rst_stream_(Code, Stream)
end;
%% Server 'SEND PP'
idle(cast, {send_pp, Headers},
#stream_state{
callback_mod=CB,
callback_state=CallbackState
}=Stream) ->
{ok, NewCBState} = CB:on_send_push_promise(Headers, CallbackState),
{next_state,
reserved_local,
Stream#stream_state{
request_headers=Headers,
callback_state=NewCBState
}, 0};
%% zero timeout lets us start dealing with reserved local,
%% because there is no END_STREAM event
%% Client 'RECV PP'
idle(cast, {recv_pp, Headers},
#stream_state{
}=Stream) ->
{next_state,
reserved_remote,
Stream#stream_state{
request_headers=Headers
}};
%% Client 'SEND H'
idle(cast, {send_h, Headers},
#stream_state{
}=Stream) ->
{next_state, open,
Stream#stream_state{
request_headers=Headers
}};
idle(Type, Event, State) ->
handle_event(Type, Event, State).
reserved_local(timeout, _,
#stream_state{
callback_state=CallbackState,
callback_mod=CB
}=Stream) ->
check_content_length(Stream),
{ok, NewCBState} = CB:on_request_end_stream(CallbackState),
{next_state,
reserved_local,
Stream#stream_state{
callback_state=NewCBState
}};
reserved_local(cast, {send_h, Headers},
#stream_state{
}=Stream) ->
{next_state,
half_closed_remote,
Stream#stream_state{
response_headers=Headers
}};
reserved_local(cast, {send_t, Headers},
#stream_state{
}=Stream) ->
{next_state,
half_closed_remote,
Stream#stream_state{
response_trailers=Headers
}};
reserved_local(Type, Event, State) ->
handle_event(Type, Event, State).
reserved_remote(cast, {recv_h, Headers},
#stream_state{
}=Stream) ->
{next_state,
half_closed_local,
Stream#stream_state{
response_headers=Headers
}};
reserved_remote(cast, {recv_t, Headers},
#stream_state{
}=Stream) ->
{next_state,
half_closed_local,
Stream#stream_state{
response_headers=Headers
}};
reserved_remote(Type, Event, State) ->
handle_event(Type, Event, State).
open(cast, recv_es,
#stream_state{
callback_mod=CB,
callback_state=CallbackState
}=Stream) ->
case check_content_length(Stream) of
ok ->
{ok, NewCBState} = CB:on_request_end_stream(CallbackState),
{next_state,
half_closed_remote,
Stream#stream_state{
callback_state=NewCBState
}};
rst_stream ->
{next_state,
closed,
Stream}
end;
open(cast, {recv_data,
{#frame_header{
flags=Flags,
length=L,
type=?DATA
}, Payload}=F},
#stream_state{
incoming_frames=IFQ,
callback_mod=CB,
callback_state=CallbackState
}=Stream)
when ?NOT_FLAG(Flags, ?FLAG_END_STREAM) ->
Bin = h2_frame_data:data(Payload),
{ok, NewCBState} = CB:on_receive_request_data(Bin, CallbackState),
{next_state,
open,
Stream#stream_state{
%% TODO: We're storing everything in the state. It's fine for
%% some cases, but the decision should be left to the user
incoming_frames=queue:in(F, IFQ),
request_body_size=Stream#stream_state.request_body_size+L,
callback_state=NewCBState
}};
open(cast, {recv_data,
{#frame_header{
flags=Flags,
length=L,
type=?DATA
}, Payload}=F},
#stream_state{
incoming_frames=IFQ,
callback_mod=CB,
callback_state=CallbackState
}=Stream)
when ?IS_FLAG(Flags, ?FLAG_END_STREAM) ->
Bin = h2_frame_data:data(Payload),
{ok, CallbackState1} = CB:on_receive_request_data(Bin, CallbackState),
NewStream = Stream#stream_state{
incoming_frames=queue:in(F, IFQ),
request_body_size=Stream#stream_state.request_body_size+L,
request_end_stream=true,
callback_state=CallbackState1
},
case check_content_length(NewStream) of
ok ->
{ok, NewCBState} = CB:on_request_end_stream(CallbackState1),
{next_state,
half_closed_remote,
NewStream#stream_state{
callback_state=NewCBState
}};
rst_stream ->
{next_state,
closed,
NewStream}
end;
%% Trailers
open(cast, {recv_h, Trailers},
#stream_state{}=Stream) ->
case is_valid_headers(request, Trailers) of
ok ->
{next_state,
open,
Stream#stream_state{
request_headers=Stream#stream_state.request_headers ++ Trailers
}};
{error, Code} ->
rst_stream_(Code, Stream)
end;
open(cast, {send_data,
{#frame_header{
type=?HEADERS,
flags=Flags
}, _}=F},
#stream_state{
socket=Socket
}=Stream) ->
sock:send(Socket, h2_frame:to_binary(F)),
NextState =
case ?IS_FLAG(Flags, ?FLAG_END_STREAM) of
true ->
half_closed_local;
_ ->
open
end,
{next_state, NextState, Stream};
open(cast, {send_data,
{#frame_header{
type=?DATA,
flags=Flags
}, _}=F},
#stream_state{
socket=Socket
}=Stream) ->
sock:send(Socket, h2_frame:to_binary(F)),
NextState =
case ?IS_FLAG(Flags, ?FLAG_END_STREAM) of
true ->
half_closed_local;
_ ->
open
end,
{next_state, NextState, Stream};
open(cast,
{send_h, Headers},
#stream_state{}=Stream) ->
{next_state,
open,
Stream#stream_state{
response_headers=Headers
}};
open(cast,
{send_t, Headers},
#stream_state{}=Stream) ->
{next_state,
half_closed_local,
Stream#stream_state{
response_trailers=Headers
}};
open(Type, Event, State) ->
handle_event(Type, Event, State).
half_closed_remote(cast,
{send_h, Headers},
#stream_state{}=Stream) ->
{next_state,
half_closed_remote,
Stream#stream_state{
response_headers=Headers
}};
half_closed_remote(cast,
{send_t, Headers},
#stream_state{}=Stream) ->
{next_state,
half_closed_remote,
Stream#stream_state{
response_trailers=Headers
}};
half_closed_remote(cast,
{send_data,
{
#frame_header{
flags=Flags,
type=?DATA
},_
}=F}=_Msg,
#stream_state{
socket=Socket
}=Stream) ->
case sock:send(Socket, h2_frame:to_binary(F)) of
ok ->
case ?IS_FLAG(Flags, ?FLAG_END_STREAM) of
true ->
{next_state, closed, Stream, 0};
_ ->
{next_state, half_closed_remote, Stream}
end;
{error,_} ->
{next_state, closed, Stream, 0}
end;
half_closed_remote(cast,
{send_data,
{
#frame_header{
flags=Flags,
type=?HEADERS
},_
}=F}=_Msg,
#stream_state{
socket=Socket
}=Stream) ->
case sock:send(Socket, h2_frame:to_binary(F)) of
ok ->
case ?IS_FLAG(Flags, ?FLAG_END_STREAM) of
true ->
{next_state, closed, Stream, 0};
_ ->
{next_state, half_closed_remote, Stream}
end;
{error,_} ->
{next_state, closed, Stream, 0}
end;
half_closed_remote(cast, _,
#stream_state{}=Stream) ->
rst_stream_(?STREAM_CLOSED, Stream);
half_closed_remote(Type, Event, State) ->
handle_event(Type, Event, State).
%% PUSH_PROMISES can only be received by streams in the open or
%% half_closed_local, but will create a new stream in the idle state,
%% but that stream may be ready to transition, it'll make sense, I
%% hope!
half_closed_local(cast,
{recv_h, Headers},
#stream_state{}=Stream) ->
case is_valid_headers(response, Headers) of
ok ->
{next_state,
half_closed_local,
Stream#stream_state{
response_headers=Headers}};
{error, Code} ->
rst_stream_(Code, Stream)
end;
half_closed_local(cast,
{recv_data,
{#frame_header{
flags=Flags,
type=?DATA
},_}=F},
#stream_state{
incoming_frames=IFQ
} = Stream) ->
NewQ = queue:in(F, IFQ),
case ?IS_FLAG(Flags, ?FLAG_END_STREAM) of
true ->
Data =
[h2_frame_data:data(Payload)
|| {#frame_header{type=?DATA}, Payload} <- queue:to_list(NewQ)],
{next_state, closed,
Stream#stream_state{
incoming_frames=queue:new(),
response_body = Data
}, 0};
_ ->
{next_state,
half_closed_local,
Stream#stream_state{
incoming_frames=NewQ
}}
end;
half_closed_local(cast, recv_es,
#stream_state{
response_body = undefined,
incoming_frames = Q
} = Stream) ->
Data = [h2_frame_data:data(Payload) || {#frame_header{type=?DATA}, Payload} <- queue:to_list(Q)],
{next_state, closed,
Stream#stream_state{
incoming_frames=queue:new(),
response_body = Data
}, 0};
half_closed_local(cast, recv_es,
#stream_state{
response_body = Data
} = Stream) ->
{next_state, closed,
Stream#stream_state{
incoming_frames=queue:new(),
response_body = Data
}, 0};
half_closed_local(_, _,
#stream_state{}=Stream) ->
rst_stream_(?STREAM_CLOSED, Stream);
half_closed_local(Type, Event, State) ->
handle_event(Type, Event, State).
closed(timeout, _,
#stream_state{}=Stream) ->
gen_statem:cast(Stream#stream_state.connection,
{stream_finished,
Stream#stream_state.stream_id,
Stream#stream_state.response_headers,
Stream#stream_state.response_body}),
{stop, normal, Stream};
closed(_, _,
#stream_state{}=Stream) ->
rst_stream_(?STREAM_CLOSED, Stream);
closed(Type, Event, State) ->
handle_event(Type, Event, State).
handle_event(_, {send_window_update, 0},
#stream_state{}=Stream) ->
{keep_state, Stream};
handle_event(_, {send_window_update, Size},
#stream_state{
socket=Socket,
stream_id=StreamId
}=Stream) ->
h2_frame_window_update:send(Socket, Size, StreamId),
{keep_state, Stream#stream_state{}};
handle_event(_, {send_connection_window_update, Size},
#stream_state{
connection=ConnPid
}=State) ->
h2_connection:send_window_update(ConnPid, Size),
{keep_state, State};
handle_event({call, From}, {rst_stream, ErrorCode}, State=#stream_state{}) ->
{keep_state, State, [{reply, From, {ok, rst_stream_(ErrorCode, State)}}]};
handle_event({call, From}, stream_id, State=#stream_state{stream_id=StreamId}) ->
{keep_state, State, [{reply, From, StreamId}]};
handle_event({call, From}, connection, State=#stream_state{connection=Conn}) ->
{keep_state, State, [{reply, From, Conn}]};
handle_event({call, From}, Event, State=#stream_state{callback_mod=CB,
callback_state=CallbackState}) ->
{ok, Reply, CallbackState1} = CB:handle_call(Event, CallbackState),
{keep_state, State#stream_state{callback_state=CallbackState1}, [{reply, From, Reply}]};
handle_event(cast, Event, State=#stream_state{callback_mod=CB,
callback_state=CallbackState}) ->
CallbackState1 = CB:handle_info(Event, CallbackState),
{keep_state, State#stream_state{callback_state=CallbackState1}};
handle_event(info, Event, State=#stream_state{callback_mod=CB,
callback_state=CallbackState}) ->
CallbackState1 = CB:handle_info(Event, CallbackState),
{keep_state, State#stream_state{callback_state=CallbackState1}};
handle_event(_, _Event, State) ->
{keep_state, State}.
code_change(_OldVsn, StateName, State, _Extra) ->
{ok, StateName, State}.
terminate(normal, _StateName, _State) ->
ok;
terminate(_Reason, _StateName, _State) ->
ok.
-spec rst_stream_(error_code(), state()) ->
{next_state,
closed,
state(),
timeout()}.
rst_stream_(ErrorCode,
#stream_state{
socket=Socket,
stream_id=StreamId
}=Stream
)
->
RstStream = h2_frame_rst_stream:new(ErrorCode),
RstStreamBin = h2_frame:to_binary(
{#frame_header{
stream_id=StreamId
},
RstStream}),
sock:send(Socket, RstStreamBin),
{next_state,
closed,
Stream, 0}.
check_content_length(Stream) ->
ContentLength =
proplists:get_value(<<"content-length">>,
Stream#stream_state.request_headers),
case ContentLength of
undefined ->
ok;
_Other ->
try binary_to_integer(ContentLength) of
Integer ->
case Stream#stream_state.request_body_size =:= Integer of
true ->
ok;
false ->
rst_stream_(?PROTOCOL_ERROR, Stream),
rst_stream
end
catch
_:_ ->
rst_stream_(?PROTOCOL_ERROR, Stream),
rst_stream
end
end.
%%% Moving header validation into streams
%% Function checks if a set of headers is valid. Currently that means:
%%
%% * The list of acceptable pseudoheaders for requests are:
%% :method, :scheme, :authority, :path,
%% * The only acceptable pseudoheader for responses is :status
%% * All header names are lowercase.
%% * All pseudoheaders occur before normal headers.
%% * No pseudoheaders are duplicated
-spec is_valid_headers( request | response,
hpack:headers() ) ->
ok | {error, term()}.
is_valid_headers(Type, Headers) ->
case
validate_pseudos(Type, Headers)
of
true ->
ok;
false ->
{error, ?PROTOCOL_ERROR}
end.
no_upper_names(Headers) ->
lists:all(
fun({Name,_}) ->
NameStr = binary_to_list(Name),
NameStr =:= string:to_lower(NameStr)
end,
Headers).
validate_pseudos(Type, Headers) ->
validate_pseudos(Type, Headers, #{}).
validate_pseudos(request, [{<<":path">>,_V}|_Tail], #{<<":path">> := true }) ->
false;
validate_pseudos(request, [{<<":path">>,_V}|Tail], Found) ->
validate_pseudos(request, Tail, Found#{<<":path">> => true});
validate_pseudos(request, [{<<":method">>,_V}|_Tail], #{<<":method">> := true }) ->
false;
validate_pseudos(request, [{<<":method">>,_V}|Tail], Found) ->
validate_pseudos(request, Tail, Found#{<<":method">> => true});
validate_pseudos(request, [{<<":scheme">>,_V}|_Tail], #{<<":scheme">> := true }) ->
false;
validate_pseudos(request, [{<<":scheme">>,_V}|Tail], Found) ->
validate_pseudos(request, Tail, Found#{<<":scheme">> => true});
validate_pseudos(request, [{<<":authority">>,_V}|_Tail], #{<<":authority">> := true }) ->
false;
validate_pseudos(request, [{<<":authority">>,_V}|Tail], Found) ->
validate_pseudos(request, Tail, Found#{<<":authority">> => true});
validate_pseudos(response, [{<<":status">>,_V}|_Tail], #{<<":status">> := true }) ->
false;
validate_pseudos(response, [{<<":status">>,_V}|Tail], Found) ->
validate_pseudos(response, Tail, Found#{<<":status">> => true});
validate_pseudos(_, DoneWithPseudos, _Found) ->
lists:all(
fun({<<$:, _/binary>>, _}) ->
false;
({<<"connection">>, _}) ->
false;
({<<"te">>, <<"trailers">>}) ->
true;
({<<"te">>, _}) ->
false;
(_) -> true
end,
DoneWithPseudos)
andalso
no_upper_names(DoneWithPseudos).
================================================
FILE: src/h2_stream_set.erl
================================================
-module(h2_stream_set).
-include("http2.hrl").
%% This module exists to manage a set of all streams for a given
%% connection. When a connection starts, a stream set logically
%% contains streams from id 1 to 2^31-1. In practicality, storing that
%% many idle streams in a collection of any type would be more memory
%% intensive. We're going to manage that here in this module, but to
%% the outside, it will behave as if they all exist
-record(
stream_set,
{
%% Type determines which streams are mine, and which are theirs
type :: client | server,
%% Streams initiated by this peer
mine :: peer_subset(),
%% Streams initiated by the other peer
theirs :: peer_subset()
}).
-type stream_set() :: #stream_set{}.
-export_type([stream_set/0]).
%% The stream_set needs to keep track of two subsets of streams, one
%% for the streams that it has initiated, and one for the streams that
%% have been initiated on the other side of the connection. It is this
%% peer_subset that will try to optimize the set of stream metadata
%% that we're storing in memory. For each side of the connection, we
%% also need an accurate count of how many are currently active
-record(
peer_subset,
{
%% Provided by the connection settings, we can check against this
%% every time we try to add a stream to this subset
max_active = unlimited :: unlimited | pos_integer(),
%% A counter that's an accurate reflection of the number of
%% active streams
active_count = 0 :: non_neg_integer(),
%% lowest_stream_id is the lowest stream id that we're currently
%% managing in memory. Any stream with an id lower than this is
%% automatically of type closed.
lowest_stream_id = 0 :: stream_id(),
%% Next available stream id will be the stream id of the next
%% stream that can be added to this subset. That means if asked
%% for a stream with this id or higher, the stream type returned
%% must be idle. Any stream id lower than this that isn't active
%% must be of type closed.
next_available_stream_id :: stream_id(),
%% A bit of a misnomer, active is actually the set of streams
%% that are active in MEMORY not the connection. This will
%% include *ALL* active streams, and possibly some closed streams
%% as well that have yet to be garbage collected.
active = [] :: [stream()]
}).
-type peer_subset() :: #peer_subset{}.
%% Streams all have stream_ids. It is the only thing all three types
%% have. It *MUST* be the first field in *ALL* *_stream{} records.
%% The metadata for an active stream is, unsurprisingly, the most
%% complex.
-record(
active_stream, {
id :: stream_id(),
% Pid running the http2_stream gen_statem
pid :: pid(),
% The process to notify with events on this stream
notify_pid :: pid() | undefined,
% The stream's flow control send window size
send_window_size :: non_neg_integer(),
% The stream's flow control recv window size
recv_window_size :: non_neg_integer(),
% Data that is in queue to send on this stream, if flow control
% hasn't allowed it to be sent yet
queued_data :: undefined | done | binary(),
% Has the body been completely received.
body_complete = false :: boolean(),
trailers = undefined :: [h2_frame:frame()] | undefined
}).
-type active_stream() :: #active_stream{}.
%% The closed_stream record is way more important to a client than a
%% server. It's a way of holding on to a response that has been
%% received, but not processed by the client yet.
-record(
closed_stream, {
id :: stream_id(),
% The pid to notify about events on this stream
notify_pid :: pid() | undefined,
% The response headers received
response_headers :: hpack:headers() | undefined,
% The response body
response_body :: binary() | undefined,
% Can this be thrown away?
garbage = false :: boolean() | undefined
}).
-type closed_stream() :: #closed_stream{}.
%% An idle stream record isn't used for much. It's never stored,
%% unlike the other two types. It is always generated on the fly when
%% asked for a stream >= next_available_stream_id. But, we're able to
%% perform a rst_stream operation on it, and we need a stream_id to
%% make that happen.
-record(
idle_stream, {
id :: stream_id()
}).
-type idle_stream() :: #idle_stream{}.
%% So a stream can be any of these things. And it will be something
%% that you can pass back into several functions here in this module.
-type stream() :: active_stream()
| closed_stream()
| idle_stream().
-export_type([stream/0]).
%% Set Operations
-export(
[
new/1,
new_stream/8,
get/2,
upsert/2,
sort/1
]).
%% Accessors
-export(
[
queued_data/1,
update_trailers/2,
update_data_queue/3,
decrement_recv_window/2,
recv_window_size/1,
response/1,
send_window_size/1,
increment_send_window_size/2,
pid/1,
stream_id/1,
stream_pid/1,
notify_pid/1,
type/1,
my_active_count/1,
their_active_count/1,
my_active_streams/1,
their_active_streams/1,
my_max_active/1,
their_max_active/1
]
).
-export(
[
close/3,
send_what_we_can/4,
update_all_recv_windows/2,
update_all_send_windows/2,
update_their_max_active/2,
update_my_max_active/2
]
).
%% new/1 returns a new stream_set. This is your constructor.
-spec new(
client | server
) -> stream_set().
new(client) ->
#stream_set{
type=client,
%% I'm a client, so mine are always odd numbered
mine=
#peer_subset{
lowest_stream_id=0,
next_available_stream_id=1
},
%% And theirs are always even
theirs=
#peer_subset{
lowest_stream_id=0,
next_available_stream_id=2
}
};
new(server) ->
#stream_set{
type=server,
%% I'm a server, so mine are always even
mine=
#peer_subset{
lowest_stream_id=0,
next_available_stream_id=2
},
%% And theirs are always odd.
theirs=
#peer_subset{
lowest_stream_id=0,
next_available_stream_id=1
}
}.
-spec new_stream(
StreamId :: stream_id(),
NotifyPid :: pid(),
CBMod :: module(),
CBOpts :: list(),
Socket :: sock:socket(),
InitialSendWindow :: integer(),
InitialRecvWindow :: integer(),
StreamSet :: stream_set()) ->
stream_set()
| {error, error_code(), closed_stream()}.
new_stream(
StreamId,
NotifyPid,
CBMod,
CBOpts,
Socket,
InitialSendWindow,
InitialRecvWindow,
StreamSet) ->
PeerSubset = get_peer_subset(StreamId, StreamSet),
case PeerSubset#peer_subset.max_active =/= unlimited andalso
PeerSubset#peer_subset.active_count >= PeerSubset#peer_subset.max_active
of
true ->
{error, ?REFUSED_STREAM, #closed_stream{id=StreamId}};
false ->
{ok, Pid} = h2_stream:start_link(
StreamId,
self(),
CBMod,
CBOpts,
Socket
),
NewStream = #active_stream{
id = StreamId,
pid = Pid,
notify_pid=NotifyPid,
send_window_size=InitialSendWindow,
recv_window_size=InitialRecvWindow
},
case upsert(NewStream, StreamSet) of
{error, ?REFUSED_STREAM} ->
%% This should be very rare, if it ever happens at
%% all. The case clause above tests the same
%% condition that upsert/2 checks to return this
%% result. Still, we need this case statement
%% because returning an {error tuple here would be
%% catastrophic
%% If this did happen, we need to kill this
%% process, or it will just hang out there.
h2_stream:stop(Pid),
{error, ?REFUSED_STREAM, #closed_stream{id=StreamId}};
NewStreamSet ->
NewStreamSet
end
end.
-spec get_peer_subset(
stream_id(),
stream_set()) ->
peer_subset().
get_peer_subset(Id, StreamSet) ->
case {Id rem 2, StreamSet#stream_set.type} of
{0, client} ->
StreamSet#stream_set.theirs;
{1, client} ->
StreamSet#stream_set.mine;
{0, server} ->
StreamSet#stream_set.mine;
{1, server} ->
StreamSet#stream_set.theirs
end.
-spec set_peer_subset(
Id :: stream_id(),
StreamSet :: stream_set(),
NewPeerSubset :: peer_subset()
) ->
stream_set().
set_peer_subset(Id, StreamSet, NewPeerSubset) ->
case {Id rem 2, StreamSet#stream_set.type} of
{0, client} ->
StreamSet#stream_set{
theirs=NewPeerSubset
};
{1, client} ->
StreamSet#stream_set{
mine=NewPeerSubset
};
{0, server} ->
StreamSet#stream_set{
mine=NewPeerSubset
};
{1, server} ->
StreamSet#stream_set{
theirs=NewPeerSubset
}
end.
%% get/2 gets a stream. The logic in here basically just chooses which
%% subset.
-spec get(Id :: stream_id(),
Streams :: stream_set()) ->
stream().
get(Id, StreamSet) ->
get_from_subset(Id,
get_peer_subset(
Id,
StreamSet)).
-spec get_from_subset(
Id :: stream_id(),
PeerSubset :: peer_subset())
->
stream().
get_from_subset(Id,
#peer_subset{
lowest_stream_id=Lowest
})
when Id < Lowest ->
#closed_stream{id=Id};
get_from_subset(Id,
#peer_subset{
next_available_stream_id=Next
})
when Id >= Next ->
#idle_stream{id=Id};
get_from_subset(Id, PeerSubset) ->
case lists:keyfind(Id, 2, PeerSubset#peer_subset.active) of
false ->
#closed_stream{id=Id};
Stream ->
Stream
end.
-spec upsert(
Stream :: stream(),
StreamSet :: stream_set()) ->
stream_set()
| {error, error_code()}.
%% Can't store idle streams
upsert(#idle_stream{}, StreamSet) ->
StreamSet;
upsert(Stream, StreamSet) ->
StreamId = stream_id(Stream),
PeerSubset = get_peer_subset(StreamId, StreamSet),
case upsert_peer_subset(Stream, PeerSubset) of
{error, Code} ->
{error, Code};
NewPeerSubset ->
set_peer_subset(StreamId, StreamSet, NewPeerSubset)
end.
-spec upsert_peer_subset(
Stream :: closed_stream() | active_stream(),
PeerSubset :: peer_subset()
) ->
peer_subset()
| {error, error_code()}.
%% Case 1: We're upserting a closed stream, it contains garbage we
%% don't care about and it's in the range of streams we're actively
%% tracking We remove it, and move the lowest_active pointer.
upsert_peer_subset(
#closed_stream{
id=Id,
garbage=true
},
PeerSubset)
when Id >= PeerSubset#peer_subset.lowest_stream_id,
Id < PeerSubset#peer_subset.next_available_stream_id ->
OldStream = get_from_subset(Id, PeerSubset),
OldType = type(OldStream),
ActiveDiff =
case OldType of
closed -> 0;
active -> -1
end,
NewActive = lists:keydelete(Id, 2, PeerSubset#peer_subset.active),
%% NewActive could now have a #closed_stream with no information
%% in it as the lowest active stream, so we should drop those.
OptimizedNewActive = drop_unneeded_streams(NewActive),
case OptimizedNewActive of
[] ->
PeerSubset#peer_subset{
lowest_stream_id=PeerSubset#peer_subset.next_available_stream_id,
active_count=0,
active=[]
};
[NewLowestStream|_] ->
NewLowest = stream_id(NewLowestStream),
PeerSubset#peer_subset{
lowest_stream_id=NewLowest,
active_count=PeerSubset#peer_subset.active_count+ActiveDiff,
active=OptimizedNewActive
}
end;
%% Case 2: Like case 1, but it's not garbage
upsert_peer_subset(
#closed_stream{
id=Id,
garbage=false
}=Closed,
PeerSubset)
when Id >= PeerSubset#peer_subset.lowest_stream_id,
Id < PeerSubset#peer_subset.next_available_stream_id ->
OldStream = get_from_subset(Id, PeerSubset),
OldType = type(OldStream),
ActiveDiff =
case OldType of
closed -> 0;
active -> -1
end,
NewActive = lists:keyreplace(Id, 2, PeerSubset#peer_subset.active, Closed),
PeerSubset#peer_subset{
active_count=PeerSubset#peer_subset.active_count+ActiveDiff,
active=NewActive
};
%% Case 3: It's closed, but greater than or equal to next available:
upsert_peer_subset(
#closed_stream{
id=Id
} = Closed,
PeerSubset)
when Id >= PeerSubset#peer_subset.next_available_stream_id ->
PeerSubset#peer_subset{
next_available_stream_id=Id+2,
active=lists:keystore(Id, 2, PeerSubset#peer_subset.active, Closed)
};
%% Case 4: It's active, and in the range we're working with
upsert_peer_subset(
#active_stream{
id=Id
}=Stream,
PeerSubset)
when Id >= PeerSubset#peer_subset.lowest_stream_id,
Id < PeerSubset#peer_subset.next_available_stream_id ->
PeerSubset#peer_subset{
active = lists:keystore(Id, 2, PeerSubset#peer_subset.active, Stream)
};
%% Case 5: It's active, but it wasn't active before and activating it
%% would exceed our concurrent stream limits
upsert_peer_subset(
#active_stream{},
PeerSubset)
when PeerSubset#peer_subset.max_active =/= unlimited,
PeerSubset#peer_subset.active_count >= PeerSubset#peer_subset.max_active ->
{error, ?REFUSED_STREAM};
%% Case 6: It's active, and greater than the range we're tracking
upsert_peer_subset(
#active_stream{
id=Id
}=Stream,
PeerSubset)
when Id >= PeerSubset#peer_subset.next_available_stream_id ->
PeerSubset#peer_subset{
next_available_stream_id=Id+2,
active_count=PeerSubset#peer_subset.active_count+1,
active = lists:keystore(Id, 2, PeerSubset#peer_subset.active, Stream)
};
%% Catch All
%% TODO: remove this match and crash instead?
upsert_peer_subset(
_Stream,
PeerSubset) ->
PeerSubset.
drop_unneeded_streams(Streams) ->
SortedStreams = lists:keysort(2, Streams),
lists:dropwhile(
fun(#closed_stream{
garbage=true
}) ->
true;
(_) ->
false
end,
SortedStreams).
-spec close(
Stream :: stream(),
Response :: garbage | {hpack:headers(), iodata()},
Streams :: stream_set()
) ->
{ stream(), stream_set()}.
close(Stream,
garbage,
StreamSet) ->
Closed = #closed_stream{
id = stream_id(Stream),
garbage=true
},
{Closed, upsert(Closed, StreamSet)};
close(Closed=#closed_stream{},
_Response,
Streams) ->
{Closed, Streams};
close(_Idle=#idle_stream{id=StreamId},
{Headers, Body},
Streams) ->
Closed = #closed_stream{
id=StreamId,
response_headers=Headers,
response_body=Body
},
{Closed, upsert(Closed, Streams)};
close(#active_stream{
id=Id,
notify_pid=NotifyPid
},
{Headers, Body},
Streams) ->
Closed = #closed_stream{
id=Id,
response_headers=Headers,
response_body=Body,
notify_pid=NotifyPid
},
{Closed, upsert(Closed, Streams)}.
%% TODO: Change sort to send peer_initiated first!
-spec sort(StreamSet::stream_set()) -> stream_set().
sort(StreamSet) ->
StreamSet#stream_set{
theirs = sort_peer_subset(StreamSet#stream_set.theirs),
mine = sort_peer_subset(StreamSet#stream_set.mine)
}.
sort_peer_subset(PeerSubset) ->
PeerSubset#peer_subset{
active=lists:keysort(2, PeerSubset#peer_subset.active)
}.
-spec update_all_recv_windows(Delta :: integer(),
Streams:: stream_set()) ->
stream_set().
update_all_recv_windows(Delta, Streams) ->
Streams#stream_set{
theirs=update_all_recv_windows_subset(Delta, Streams#stream_set.theirs),
mine=update_all_recv_windows_subset(Delta, Streams#stream_set.mine)
}.
update_all_recv_windows_subset(Delta, PeerSubset) ->
NewActive = lists:map(
fun(#active_stream{}=S) ->
S#active_stream{
recv_window_size=S#active_stream.recv_window_size+Delta
};
(S) -> S
end,
PeerSubset#peer_subset.active),
PeerSubset#peer_subset{
active=NewActive
}.
-spec update_all_send_windows(Delta :: integer(),
Streams:: stream_set()) ->
stream_set().
update_all_send_windows(Delta, Streams) ->
Streams#stream_set{
theirs=update_all_send_windows_subset(Delta, Streams#stream_set.theirs),
mine=update_all_recv_windows_subset(Delta, Streams#stream_set.mine)
}.
update_all_send_windows_subset(Delta, PeerSubset) ->
NewActive = lists:map(
fun(#active_stream{}=S) ->
S#active_stream{
send_window_size=S#active_stream.send_window_size+Delta
};
(S) -> S
end,
PeerSubset#peer_subset.active),
PeerSubset#peer_subset{
active=NewActive
}.
-spec update_their_max_active(NewMax :: non_neg_integer() | unlimited,
Streams :: stream_set()) ->
stream_set().
update_their_max_active(NewMax,
#stream_set{
theirs=Theirs
}=Streams) ->
Streams#stream_set{
theirs=Theirs#peer_subset{max_active=NewMax}
}.
-spec update_my_max_active(NewMax :: non_neg_integer() | unlimited,
Streams :: stream_set()) ->
stream_set().
update_my_max_active(NewMax,
#stream_set{
mine=Mine
}=Streams) ->
Streams#stream_set{
mine=Mine#peer_subset{max_active=NewMax}
}.
-spec send_what_we_can(StreamId :: all | stream_id(),
ConnSendWindowSize :: integer(),
MaxFrameSize :: non_neg_integer(),
Streams :: stream_set()) ->
{NewConnSendWindowSize :: integer(),
NewStreams :: stream_set()}.
send_what_we_can(all, ConnSendWindowSize, MaxFrameSize, Streams) ->
{AfterPeerWindowSize,
NewPeerInitiated} = c_send_what_we_can(
ConnSendWindowSize,
MaxFrameSize,
Streams#stream_set.theirs#peer_subset.active,
[]),
{AfterAfterWindowSize,
NewSelfInitiated} = c_send_what_we_can(
AfterPeerWindowSize,
MaxFrameSize,
Streams#stream_set.mine#peer_subset.active,
[]),
{AfterAfterWindowSize,
Streams#stream_set{
theirs=Streams#stream_set.theirs#peer_subset{active=NewPeerInitiated},
mine=Streams#stream_set.mine#peer_subset{active=NewSelfInitiated}
}
};
send_what_we_can(StreamId, ConnSendWindowSize, MaxFrameSize, Streams) ->
{NewConnSendWindowSize, NewStream} =
s_send_what_we_can(ConnSendWindowSize,
MaxFrameSize,
get(StreamId, Streams)),
{NewConnSendWindowSize,
upsert(NewStream, Streams)}.
%% Send at the connection level
-spec c_send_what_we_can(ConnSendWindowSize :: integer(),
MaxFrameSize :: non_neg_integer(),
Streams :: [stream()],
Acc :: [stream()]
) ->
{integer(), [stream()]}.
%% If we hit =< 0, done
c_send_what_we_can(ConnSendWindowSize, _MFS, Streams, Acc)
when ConnSendWindowSize =< 0 ->
{ConnSendWindowSize, lists:reverse(Acc) ++ Streams};
%% If we hit end of streams list, done
c_send_what_we_can(SWS, _MFS, [], Acc) ->
{SWS, lists:reverse(Acc)};
%% Otherwise, try sending on the working stream
c_send_what_we_can(SWS, MFS, [S|Streams], Acc) ->
{NewSWS, NewS} = s_send_what_we_can(SWS, MFS, S),
c_send_what_we_can(NewSWS, MFS, Streams, [NewS|Acc]).
%% Send at the stream level
-spec s_send_what_we_can(SWS :: integer(),
MFS :: non_neg_integer(),
Stream :: stream()) ->
{integer(), stream()}.
s_send_what_we_can(SWS, _, #active_stream{queued_data=Data,
trailers=undefined}=S)
when is_atom(Data) ->
{SWS, S};
s_send_what_we_can(SWS, _, #active_stream{queued_data=Data,
pid=Pid,
trailers=Trailers}=S)
when is_atom(Data) ->
[h2_stream:send_data(Pid, Frame) || Frame <- Trailers],
{SWS, S};
s_send_what_we_can(SWS, MFS, #active_stream{}=Stream) ->
%% We're coming in here with three numbers we need to look at:
%% * Connection send window size
%% * Stream send window size
%% * Maximum frame size
%% If none of them are zero, we have to send something, so we're
%% going to figure out what's the biggest number we can send. If
%% that's more than we have to send, we'll send everything and put
%% an END_STREAM flag on it. Otherwise, we'll send as much as we
%% can. Then, based on which number was the limiting factor, we'll
%% make another decision
%% If it was MAX_FRAME_SIZE, then we recurse into this same
%% function, because we're able to send another frame of some
%% length.
%% If it was connection send window size, we're blocked at the
%% connection level and we should break out of this recursion
%% If it was stream send_window size, we're blocked on this
%% stream, but other streams can still go, so we'll break out of
%% this recursion, but not the connection level
SSWS = Stream#active_stream.send_window_size,
Trailers = Stream#active_stream.trailers,
QueueSize = byte_size(Stream#active_stream.queued_data),
{MaxToSend, ExitStrategy} =
case {MFS =< SWS andalso MFS =< SSWS, SWS < SSWS} of
%% If MAX_FRAME_SIZE is the smallest, send one and recurse
{true, _} ->
{MFS, max_frame_size};
{false, true} ->
{SWS, connection};
_ ->
{SSWS, stream}
end,
{Frame, SentBytes, NewS} =
case MaxToSend > QueueSize of
true ->
Flags = case Stream#active_stream.body_complete of
true -> ?FLAG_END_STREAM;
false -> 0
end,
%% We have the power to send everything
{{#frame_header{
stream_id=Stream#active_stream.id,
flags=Flags,
type=?DATA,
length=QueueSize
},
h2_frame_data:new(Stream#active_stream.queued_data)}, %% Full Body
QueueSize,
Stream#active_stream{
queued_data=done,
send_window_size=SSWS-QueueSize}};
false ->
<<BinToSend:MaxToSend/binary,Rest/binary>> = Stream#active_stream.queued_data,
{{#frame_header{
stream_id=Stream#active_stream.id,
type=?DATA,
length=MaxToSend
},
h2_frame_data:new(BinToSend)},
MaxToSend,
Stream#active_stream{
queued_data=Rest,
send_window_size=SSWS-MaxToSend}}
end,
_Sent = h2_stream:send_data(Stream#active_stream.pid, Frame),
case NewS of
#active_stream{trailers=undefined} ->
ok;
#active_stream{pid=Pid,
queued_data=done,
trailers=Trailers} ->
[h2_stream:send_data(Pid, Trailer) || Trailer <- Trailers];
_ ->
ok
end,
case ExitStrategy of
max_frame_size ->
s_send_what_we_can(SWS - SentBytes, MFS, NewS);
stream ->
{SWS - SentBytes, NewS};
connection ->
{SWS - SentBytes, NewS}
end;
s_send_what_we_can(SWS, _MFS, NonActiveStream) ->
{SWS, NonActiveStream}.
%% Record Accessors
-spec stream_id(
Stream :: stream()) ->
stream_id().
stream_id(#idle_stream{id=SID}) ->
SID;
stream_id(#active_stream{id=SID}) ->
SID;
stream_id(#closed_stream{id=SID}) ->
SID.
-spec pid(stream()) -> pid() | undefined.
pid(#active_stream{pid=Pid}) ->
Pid;
pid(_) ->
undefined.
-spec type(stream()) -> idle | active | closed.
type(#idle_stream{}) ->
idle;
type(#active_stream{}) ->
active;
type(#closed_stream{}) ->
closed.
queued_data(#active_stream{queued_data=QD}) ->
QD;
queued_data(_) ->
undefined.
update_trailers(Trailers, Stream=#active_stream{}) ->
Stream#active_stream{trailers=Trailers}.
update_data_queue(
NewBody,
BodyComplete,
#active_stream{} = Stream) ->
Stream#active_stream{
queued_data=NewBody,
body_complete=BodyComplete
};
update_data_queue(_, _, S) ->
S.
response(#closed_stream{
response_headers=Headers,
response_body=Body}) ->
Encoding = case lists:keyfind(<<"content-encoding">>, 1, Headers) of
false -> identity;
{_, Encoding0} -> binary_to_atom(Encoding0, 'utf8')
end,
{Headers, decode_body(Body, Encoding)};
response(_) ->
no_response.
decode_body(Body, identity) ->
Body;
decode_body(Body, gzip) ->
zlib:gunzip(Body);
decode_body(Body, zip) ->
zlib:unzip(Body);
decode_body(Body, compress) ->
zlib:uncompress(Body);
decode_body(Body, deflate) ->
Z = zlib:open(),
ok = zlib:inflateInit(Z, -15),
Decompressed = try zlib:inflate(Z, Body) catch E:V -> {E,V} end,
ok = zlib:inflateEnd(Z),
ok = zlib:close(Z),
iolist_to_binary(Decompressed).
recv_window_size(#active_stream{recv_window_size=RWS}) ->
RWS;
recv_window_size(_) ->
undefined.
decrement_recv_window(
L,
#active_stream{recv_window_size=RWS}=Stream
) ->
Stream#active_stream{
recv_window_size=RWS-L
};
decrement_recv_window(_, S) ->
S.
send_window_size(#active_stream{send_window_size=SWS}) ->
SWS;
send_window_size(_) ->
undefined.
increment_send_window_size(
WSI,
#active_stream{send_window_size=SWS}=Stream) ->
Stream#active_stream{
send_window_size=SWS+WSI
};
i
gitextract_bky7wddq/
├── .gitignore
├── .travis.yml
├── LICENSE
├── Makefile
├── README.md
├── config/
│ ├── localhost.crt
│ ├── localhost.key
│ ├── localhost.key.pub
│ └── sys.config
├── include/
│ └── http2.hrl
├── priv/
│ └── index.html
├── rebar.config
├── rebar.config.script
├── src/
│ ├── chatterbox.app.src
│ ├── chatterbox.erl
│ ├── chatterbox_ranch_protocol.erl
│ ├── chatterbox_static_content_handler.erl
│ ├── chatterbox_static_stream.erl
│ ├── chatterbox_sup.erl
│ ├── h2_client.erl
│ ├── h2_connection.erl
│ ├── h2_frame.erl
│ ├── h2_frame_continuation.erl
│ ├── h2_frame_data.erl
│ ├── h2_frame_goaway.erl
│ ├── h2_frame_headers.erl
│ ├── h2_frame_ping.erl
│ ├── h2_frame_priority.erl
│ ├── h2_frame_push_promise.erl
│ ├── h2_frame_rst_stream.erl
│ ├── h2_frame_settings.erl
│ ├── h2_frame_window_update.erl
│ ├── h2_padding.erl
│ ├── h2_settings.erl
│ ├── h2_stream.erl
│ ├── h2_stream_set.erl
│ └── sock.erl
└── test/
├── chatterbox_test_buddy.erl
├── chatterbox_tests.erl
├── client_server_SUITE.erl
├── client_server_SUITE_data/
│ ├── README.md
│ ├── bower.json
│ ├── css/
│ │ ├── print/
│ │ │ ├── paper.css
│ │ │ └── pdf.css
│ │ ├── reveal.css
│ │ ├── reveal.scss
│ │ └── theme/
│ │ ├── README.md
│ │ ├── beige.css
│ │ ├── black.css
│ │ ├── blood.css
│ │ ├── league.css
│ │ ├── moon.css
│ │ ├── night.css
│ │ ├── serif.css
│ │ ├── simple.css
│ │ ├── sky.css
│ │ ├── solarized.css
│ │ ├── source/
│ │ │ ├── beige.scss
│ │ │ ├── black.scss
│ │ │ ├── blood.scss
│ │ │ ├── league.scss
│ │ │ ├── moon.scss
│ │ │ ├── night.scss
│ │ │ ├── serif.scss
│ │ │ ├── simple.scss
│ │ │ ├── sky.scss
│ │ │ ├── solarized.scss
│ │ │ └── white.scss
│ │ ├── template/
│ │ │ ├── mixins.scss
│ │ │ ├── settings.scss
│ │ │ └── theme.scss
│ │ └── white.css
│ ├── index.html
│ ├── js/
│ │ └── reveal.js
│ ├── lib/
│ │ ├── css/
│ │ │ └── zenburn.css
│ │ ├── font/
│ │ │ ├── league-gothic/
│ │ │ │ ├── LICENSE
│ │ │ │ └── league-gothic.css
│ │ │ └── source-sans-pro/
│ │ │ ├── LICENSE
│ │ │ └── source-sans-pro.css
│ │ └── js/
│ │ ├── classList.js
│ │ └── html5shiv.js
│ └── plugin/
│ ├── highlight/
│ │ └── highlight.js
│ ├── markdown/
│ │ ├── example.html
│ │ ├── example.md
│ │ ├── markdown.js
│ │ └── marked.js
│ ├── math/
│ │ └── math.js
│ ├── multiplex/
│ │ ├── client.js
│ │ ├── index.js
│ │ └── master.js
│ ├── notes/
│ │ ├── notes.html
│ │ └── notes.js
│ ├── notes-server/
│ │ ├── client.js
│ │ ├── index.js
│ │ └── notes.html
│ ├── print-pdf/
│ │ └── print-pdf.js
│ ├── search/
│ │ └── search.js
│ └── zoom-js/
│ └── zoom.js
├── double_body_handler.erl
├── echo_handler.erl
├── flow_control_SUITE.erl
├── flow_control_handler.erl
├── header_continuation_SUITE.erl
├── http2_frame_size_SUITE.erl
├── http2_spec_3_5_SUITE.erl
├── http2_spec_4_3_SUITE.erl
├── http2_spec_5_1_SUITE.erl
├── http2_spec_5_3_SUITE.erl
├── http2_spec_5_5_SUITE.erl
├── http2_spec_6_1_SUITE.erl
├── http2_spec_6_2_SUITE.erl
├── http2_spec_6_4_SUITE.erl
├── http2_spec_6_5_SUITE.erl
├── http2_spec_6_9_SUITE.erl
├── http2_spec_8_1_SUITE.erl
├── http2c.erl
├── peer_test_handler.erl
├── protocol_errors_SUITE.erl
├── server_connection_receive_window.erl
├── server_stream_receive_window.erl
├── settings_handshake_SUITE.erl
└── starting_SUITE.erl
SYMBOL INDEX (179 symbols across 10 files)
FILE: test/client_server_SUITE_data/js/reveal.js
function initialize (line 237) | function initialize( options ) {
function checkCapabilities (line 293) | function checkCapabilities() {
function load (line 329) | function load() {
function start (line 390) | function start() {
function setupDOM (line 449) | function setupDOM() {
function createStatusDiv (line 500) | function createStatusDiv() {
function setupPDF (line 523) | function setupPDF() {
function setupIframeScrollPrevention (line 636) | function setupIframeScrollPrevention() {
function createSingletonNode (line 654) | function createSingletonNode( container, tagname, classname, innerHTML ) {
function createBackgrounds (line 685) | function createBackgrounds() {
function createBackground (line 752) | function createBackground( slide, container ) {
function setupPostMessage (line 843) | function setupPostMessage() {
function configure (line 867) | function configure( options ) {
function addEventListeners (line 972) | function addEventListeners() {
function removeEventListeners (line 1050) | function removeEventListeners() {
function extend (line 1095) | function extend( a, b ) {
function toArray (line 1106) | function toArray( o ) {
function deserialize (line 1115) | function deserialize( value ) {
function distanceBetween (line 1135) | function distanceBetween( a, b ) {
function transformElement (line 1147) | function transformElement( element, transform ) {
function transformSlides (line 1161) | function transformSlides( transforms ) {
function injectStyleSheet (line 1180) | function injectStyleSheet( value ) {
function colorToRgb (line 1203) | function colorToRgb( color ) {
function colorBrightness (line 1253) | function colorBrightness( color ) {
function getAbsoluteHeight (line 1269) | function getAbsoluteHeight( element ) {
function getRemainingHeight (line 1306) | function getRemainingHeight( element, height ) {
function isPrintingPDF (line 1331) | function isPrintingPDF() {
function hideAddressBar (line 1340) | function hideAddressBar() {
function removeAddressBar (line 1354) | function removeAddressBar() {
function dispatchEvent (line 1366) | function dispatchEvent( type, args ) {
function enableRollingLinks (line 1384) | function enableRollingLinks() {
function disableRollingLinks (line 1409) | function disableRollingLinks() {
function enablePreviewLinks (line 1428) | function enablePreviewLinks( selector ) {
function disablePreviewLinks (line 1443) | function disablePreviewLinks() {
function showPreview (line 1458) | function showPreview( url ) {
function showHelp (line 1500) | function showHelp() {
function closeOverlay (line 1545) | function closeOverlay() {
function layout (line 1558) | function layout() {
function layoutSlideContents (line 1649) | function layoutSlideContents( width, height, padding ) {
function getComputedSlideSize (line 1682) | function getComputedSlideSize( presentationWidth, presentationHeight ) {
function setPreviousVerticalIndex (line 1720) | function setPreviousVerticalIndex( stack, v ) {
function getPreviousVerticalIndex (line 1735) | function getPreviousVerticalIndex( stack ) {
function activateOverview (line 1752) | function activateOverview() {
function layoutOverview (line 1803) | function layoutOverview() {
function updateOverview (line 1846) | function updateOverview() {
function deactivateOverview (line 1871) | function deactivateOverview() {
function toggleOverview (line 1930) | function toggleOverview( override ) {
function isOverview (line 1947) | function isOverview() {
function isVerticalSlide (line 1960) | function isVerticalSlide( slide ) {
function enterFullscreen (line 1975) | function enterFullscreen() {
function pause (line 1996) | function pause() {
function resume (line 2014) | function resume() {
function togglePause (line 2030) | function togglePause( override ) {
function isPaused (line 2044) | function isPaused() {
function toggleAutoSlide (line 2057) | function toggleAutoSlide( override ) {
function isAutoSliding (line 2072) | function isAutoSliding() {
function slide (line 2089) | function slide( h, v, f, o ) {
function sync (line 2234) | function sync() {
function resetVerticalSlides (line 2277) | function resetVerticalSlides() {
function sortAllFragments (line 2302) | function sortAllFragments() {
function updateSlides (line 2333) | function updateSlides( selector, index ) {
function updateSlidesVisibility (line 2439) | function updateSlidesVisibility() {
function updateNotes (line 2517) | function updateNotes() {
function updateProgress (line 2530) | function updateProgress() {
function updateSlideNumber (line 2550) | function updateSlideNumber() {
function formatSlideNumber (line 2588) | function formatSlideNumber( a, delimiter, b ) {
function updateControls (line 2604) | function updateControls() {
function updateBackground (line 2658) | function updateBackground( includeAll ) {
function updateParallax (line 2774) | function updateParallax() {
function showSlide (line 2831) | function showSlide( slide ) {
function hideSlide (line 2914) | function hideSlide( slide ) {
function availableRoutes (line 2933) | function availableRoutes() {
function availableFragments (line 2962) | function availableFragments() {
function formatEmbeddedContent (line 2982) | function formatEmbeddedContent() {
function startEmbeddedContent (line 3007) | function startEmbeddedContent( slide ) {
function startEmbeddedIframe (line 3045) | function startEmbeddedIframe( event ) {
function stopEmbeddedContent (line 3068) | function stopEmbeddedContent( slide ) {
function getSlidePastCount (line 3113) | function getSlidePastCount() {
function getProgress (line 3157) | function getProgress() {
function isSpeakerNotes (line 3190) | function isSpeakerNotes() {
function readURL (line 3199) | function readURL() {
function writeURL (line 3247) | function writeURL( delay ) {
function getIndices (line 3293) | function getIndices( slide ) {
function getTotalSlides (line 3340) | function getTotalSlides() {
function getSlide (line 3349) | function getSlide( x, y ) {
function getSlideBackground (line 3368) | function getSlideBackground( x, y ) {
function getSlideNotes (line 3401) | function getSlideNotes( slide ) {
function getState (line 3426) | function getState() {
function setState (line 3445) | function setState( state ) {
function sortFragments (line 3478) | function sortFragments( fragments ) {
function navigateFragment (line 3536) | function navigateFragment( index, offset ) {
function nextFragment (line 3619) | function nextFragment() {
function previousFragment (line 3631) | function previousFragment() {
function cueAutoSlide (line 3640) | function cueAutoSlide() {
function cancelAutoSlide (line 3706) | function cancelAutoSlide() {
function pauseAutoSlide (line 3713) | function pauseAutoSlide() {
function resumeAutoSlide (line 3727) | function resumeAutoSlide() {
function navigateLeft (line 3737) | function navigateLeft() {
function navigateRight (line 3752) | function navigateRight() {
function navigateUp (line 3767) | function navigateUp() {
function navigateDown (line 3776) | function navigateDown() {
function navigatePrev (line 3791) | function navigatePrev() {
function navigateNext (line 3822) | function navigateNext() {
function isSwipePrevented (line 3847) | function isSwipePrevented( target ) {
function onUserInput (line 3867) | function onUserInput( event ) {
function onDocumentKeyPress (line 3878) | function onDocumentKeyPress( event ) {
function onDocumentKeyDown (line 3895) | function onDocumentKeyDown( event ) {
function onTouchStart (line 4029) | function onTouchStart( event ) {
function onTouchMove (line 4054) | function onTouchMove( event ) {
function onTouchEnd (line 4143) | function onTouchEnd( event ) {
function onPointerDown (line 4152) | function onPointerDown( event ) {
function onPointerMove (line 4164) | function onPointerMove( event ) {
function onPointerUp (line 4176) | function onPointerUp( event ) {
function onDocumentMouseScroll (line 4189) | function onDocumentMouseScroll( event ) {
function onProgressClicked (line 4213) | function onProgressClicked( event ) {
function onNavigateLeftClicked (line 4233) | function onNavigateLeftClicked( event ) { event.preventDefault(); onUser...
function onNavigateRightClicked (line 4234) | function onNavigateRightClicked( event ) { event.preventDefault(); onUse...
function onNavigateUpClicked (line 4235) | function onNavigateUpClicked( event ) { event.preventDefault(); onUserIn...
function onNavigateDownClicked (line 4236) | function onNavigateDownClicked( event ) { event.preventDefault(); onUser...
function onNavigatePrevClicked (line 4237) | function onNavigatePrevClicked( event ) { event.preventDefault(); onUser...
function onNavigateNextClicked (line 4238) | function onNavigateNextClicked( event ) { event.preventDefault(); onUser...
function onWindowHashChange (line 4243) | function onWindowHashChange( event ) {
function onWindowResize (line 4252) | function onWindowResize( event ) {
function onPageVisibilityChange (line 4261) | function onPageVisibilityChange( event ) {
function onOverviewSlideClicked (line 4282) | function onOverviewSlideClicked( event ) {
function onPreviewLinkClicked (line 4315) | function onPreviewLinkClicked( event ) {
function onAutoSlidePlayerClick (line 4330) | function onAutoSlidePlayerClick( event ) {
function Playback (line 4364) | function Playback( container, progressCheck ) {
FILE: test/client_server_SUITE_data/plugin/highlight/highlight.js
function n (line 30) | function n(e){return e.replace(/&/gm,"&").replace(/</gm,"<").repl...
function t (line 30) | function t(e){return e.nodeName.toLowerCase()}
function r (line 30) | function r(e,n){var t=e&&e.exec(n);return t&&0==t.index}
function a (line 30) | function a(e){return/^(no-?highlight|plain|text)$/i.test(e)}
function i (line 30) | function i(e){var n,t,r,i=e.className+" ";if(i+=e.parentNode?e.parentNod...
function o (line 30) | function o(e,n){var t,r={};for(t in e)r[t]=e[t];if(n)for(t in n)r[t]=n[t...
function u (line 30) | function u(e){var n=[];return function r(e,a){for(var i=e.firstChild;i;i...
function c (line 30) | function c(e,r,a){function i(){return e.length&&r.length?e[0].offset!=r[...
function s (line 30) | function s(e){function n(e){return e&&e.source||e}function t(t,r){return...
function l (line 30) | function l(e,t,a,i){function o(e,n){for(var t=0;t<n.c.length;t++)if(r(n....
function f (line 30) | function f(e,t){t=t||E.languages||Object.keys(x);var r={r:0,value:n(e)},...
function g (line 30) | function g(e){return E.tabReplace&&(e=e.replace(/^((<[^>]+>|\t)+)/gm,fun...
function h (line 30) | function h(e,n,t){var r=n?R[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)...
function p (line 30) | function p(e){var n=i(e);if(!a(n)){var t;E.useBR?(t=document.createEleme...
function d (line 30) | function d(e){E=o(E,e)}
function b (line 30) | function b(){if(!b.called){b.called=!0;var e=document.querySelectorAll("...
function v (line 30) | function v(){addEventListener("DOMContentLoaded",b,!1),addEventListener(...
function m (line 30) | function m(n,t){var r=x[n]=t(e);r.aliases&&r.aliases.forEach(function(e)...
function N (line 30) | function N(){return Object.keys(x)}
function w (line 30) | function w(e){return e=(e||"").toLowerCase(),x[e]||x[R[e]]}
function b (line 31) | function b(e,b){var r=[{b:e,e:b}];return r[0].c=r,r}
FILE: test/client_server_SUITE_data/plugin/markdown/markdown.js
function getMarkdownFromSlide (line 41) | function getMarkdownFromSlide( section ) {
function getForwardedAttributes (line 71) | function getForwardedAttributes( section ) {
function getSlidifyOptions (line 99) | function getSlidifyOptions( options ) {
function createMarkdownSlide (line 113) | function createMarkdownSlide( content, options ) {
function slidify (line 135) | function slidify( markdown, options ) {
function processSlides (line 208) | function processSlides() {
function addAttributeInElement (line 290) | function addAttributeInElement( node, elementTarget, separator ) {
function addAttributes (line 312) | function addAttributes( section, element, previousElement, separatorElem...
function convertSlides (line 351) | function convertSlides() {
FILE: test/client_server_SUITE_data/plugin/markdown/marked.js
function e (line 6) | function e(e){this.tokens=[],this.tokens.links={},this.options=e||a.defa...
function t (line 6) | function t(e,t){if(this.options=t||a.defaults,this.links=e,this.rules=u....
function n (line 6) | function n(e){this.options=e||{}}
function r (line 6) | function r(e){this.tokens=[],this.token=null,this.options=e||a.defaults,...
function s (line 6) | function s(e,t){return e.replace(t?/&/g:/&(?!#?\w+;)/g,"&").replace(...
function i (line 6) | function i(e){return e.replace(/&([#\w]+);/g,function(e,t){return t=t.to...
function l (line 6) | function l(e,t){return e=e.source,t=t||"",function n(r,s){return r?(s=s....
function o (line 6) | function o(){}
function h (line 6) | function h(e){for(var t,n,r=1;r<arguments.length;r++){t=arguments[r];for...
function a (line 6) | function a(t,n,i){if(i||"function"==typeof n){i||(i=n,n=null),n=h({},a.d...
FILE: test/client_server_SUITE_data/plugin/math/math.js
function loadScript (line 38) | function loadScript( url, callback ) {
FILE: test/client_server_SUITE_data/plugin/multiplex/master.js
function post (line 10) | function post() {
FILE: test/client_server_SUITE_data/plugin/notes-server/client.js
function post (line 16) | function post() {
FILE: test/client_server_SUITE_data/plugin/notes/notes.js
function openNotes (line 14) | function openNotes() {
FILE: test/client_server_SUITE_data/plugin/search/search.js
function Hilitor (line 19) | function Hilitor(id, tag)
function openSearch (line 111) | function openSearch() {
function toggleSearch (line 119) | function toggleSearch() {
function doSearch (line 130) | function doSearch() {
FILE: test/client_server_SUITE_data/plugin/zoom-js/zoom.js
function magnify (line 88) | function magnify( rect, scale ) {
function pan (line 164) | function pan() {
function getScrollOffset (line 189) | function getScrollOffset() {
Condensed preview — 122 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,234K chars).
[
{
"path": ".gitignore",
"chars": 154,
"preview": "ebin/*\n*.beam\ndeps/*\nrel/chatterbox\n.eunit\n_rel\nrelx\n.rebar\nerln8.config\n.DS_Store\ncommon_test/logs\n.edts\nerl_crash.dump"
},
{
"path": ".travis.yml",
"chars": 257,
"preview": "language: erlang\n\notp_release:\n - 21.0\n - 20.0\n - 19.3\n\nbefore_script: kerl list installations\n\nbefore_install:\n - w"
},
{
"path": "LICENSE",
"chars": 1078,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2015 Joe DeVivo\n\nPermission is hereby granted, free of charge, to any person obtain"
},
{
"path": "Makefile",
"chars": 570,
"preview": "REBAR3_URL=https://s3.amazonaws.com/rebar3/rebar3\n\n# If there is a rebar in the current directory, use it\nifeq ($(wildca"
},
{
"path": "README.md",
"chars": 4003,
"preview": "# chatterbox #\n\nChatterbox is an HTTP/2 library for Erlang. Use as much of it as you\nwant, but the goal is to implement "
},
{
"path": "config/localhost.crt",
"chars": 1111,
"preview": "-----BEGIN CERTIFICATE-----\nMIIDCDCCAfACCQCyOlNYMIzE4TANBgkqhkiG9w0BAQUFADBGMQswCQYDVQQGEwJV\nUzEQMA4GA1UECBMHQXJpem9uYTE"
},
{
"path": "config/localhost.key",
"chars": 1679,
"preview": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAxa9PMgPb7zJmHGswAjZ2h9JvAm9DrRnMcVeQFz+8VbL27gAB\nZ4l+cwK0KbGgWKL5bXJID51"
},
{
"path": "config/localhost.key.pub",
"chars": 404,
"preview": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFr08yA9vvMmYcazACNnaH0m8Cb0OtGcxxV5AXP7xVsvbuAAFniX5zArQpsaBYovltckgPnVe1GzfC0eFf"
},
{
"path": "config/sys.config",
"chars": 602,
"preview": "[\n {chatterbox,\n [\n {port, 8081},\n {ssl, true},\n {ssl_options, [{certfile, \"localhost.crt\"},\n "
},
{
"path": "include/http2.hrl",
"chars": 4491,
"preview": "%% FRAME TYPES\n-define(DATA , 16#0).\n-define(HEADERS , 16#1).\n-define(PRIORITY , 16#2).\n-defin"
},
{
"path": "priv/index.html",
"chars": 124,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <title>Chatterbox</title>\n </head>\n <body>\n Chatterbox\n </body>\n</ht"
},
{
"path": "rebar.config",
"chars": 954,
"preview": "%% -*- mode: erlang -*-\n%% -*- tab-width: 4;erlang-indent-level: 4;indent-tabs-mode: nil -*-\n%% ex: ts=4 sw=4 ft=erlang "
},
{
"path": "rebar.config.script",
"chars": 372,
"preview": "case erlang:function_exported(rebar3, main, 1) of\n true -> % rebar3\n CONFIG;\n false -> % rebar 2.x or older"
},
{
"path": "src/chatterbox.app.src",
"chars": 1454,
"preview": "%% -*- mode: erlang -*-\n{application, chatterbox,\n [\n {description, \"chatterbox library for http2\"},\n {vsn, git},\n {r"
},
{
"path": "src/chatterbox.erl",
"chars": 2205,
"preview": "-module(chatterbox).\n\n-include(\"http2.hrl\").\n\n-export([\n start/0,\n settings/0,\n settings/1,\n "
},
{
"path": "src/chatterbox_ranch_protocol.erl",
"chars": 866,
"preview": "-module(chatterbox_ranch_protocol).\n\n%% While it implements the behaviour, uncommenting the line below\n%% would fail to "
},
{
"path": "src/chatterbox_static_content_handler.erl",
"chars": 4886,
"preview": "-module(chatterbox_static_content_handler).\n\n-include(\"http2.hrl\").\n\n-export([\n spawn_handle/4,\n handle/"
},
{
"path": "src/chatterbox_static_stream.erl",
"chars": 5972,
"preview": "-module(chatterbox_static_stream).\n\n-include(\"http2.hrl\").\n\n-behaviour(h2_stream).\n\n-export([\n init/3,\n "
},
{
"path": "src/chatterbox_sup.erl",
"chars": 1348,
"preview": "-module(chatterbox_sup).\n\n-behaviour(supervisor).\n\n-export([\n init/1,\n start_link/0,\n start_sock"
},
{
"path": "src/h2_client.erl",
"chars": 6141,
"preview": "-module(h2_client).\n-include(\"http2.hrl\").\n\n%% Today's the day! We need to turn this gen_server into a gen_statem\n%% whi"
},
{
"path": "src/h2_connection.erl",
"chars": 61134,
"preview": "-module(h2_connection).\n-include(\"http2.hrl\").\n-behaviour(gen_statem).\n\n%% Start/Stop API\n-export([\n start_clien"
},
{
"path": "src/h2_frame.erl",
"chars": 10327,
"preview": "-module(h2_frame).\n\n-include(\"http2.hrl\").\n\n-export([\n recv/1,\n read/1,\n read/2,\n read_b"
},
{
"path": "src/h2_frame_continuation.erl",
"chars": 1284,
"preview": "-module(h2_frame_continuation).\n-include(\"http2.hrl\").\n-behaviour(h2_frame).\n\n-export(\n [\n block_fragment/1,\n fo"
},
{
"path": "src/h2_frame_data.erl",
"chars": 2318,
"preview": "-module(h2_frame_data).\n-include(\"http2.hrl\").\n-behaviour(h2_frame).\n\n-export([\n format/1,\n read_binary/"
},
{
"path": "src/h2_frame_goaway.erl",
"chars": 1659,
"preview": "-module(h2_frame_goaway).\n-include(\"http2.hrl\").\n-behaviour(h2_frame).\n\n-export(\n [\n error_code/1,\n format/1,\n "
},
{
"path": "src/h2_frame_headers.erl",
"chars": 6138,
"preview": "-module(h2_frame_headers).\n-include(\"http2.hrl\").\n-behaviour(h2_frame).\n\n-export(\n [\n format/1,\n from_frames/1,\n"
},
{
"path": "src/h2_frame_ping.erl",
"chars": 1415,
"preview": "-module(h2_frame_ping).\n-include(\"http2.hrl\").\n-behaviour(h2_frame).\n\n-export(\n [\n format/1,\n read_binary/2,\n "
},
{
"path": "src/h2_frame_priority.erl",
"chars": 2158,
"preview": "-module(h2_frame_priority).\n-include(\"http2.hrl\").\n-behaviour(h2_frame).\n\n-export(\n [\n format/1,\n new/3,\n str"
},
{
"path": "src/h2_frame_push_promise.erl",
"chars": 2515,
"preview": "-module(h2_frame_push_promise).\n-include(\"http2.hrl\").\n-behaviour(h2_frame).\n\n-export(\n [\n block_fragment/1,\n fo"
},
{
"path": "src/h2_frame_rst_stream.erl",
"chars": 1414,
"preview": "-module(h2_frame_rst_stream).\n-include(\"http2.hrl\").\n-behaviour(h2_frame).\n\n-export([\n new/1,\n error_cod"
},
{
"path": "src/h2_frame_settings.erl",
"chars": 9356,
"preview": "-module(h2_frame_settings).\n-include(\"http2.hrl\").\n-behaviour(h2_frame).\n\n-export(\n [\n format/1,\n read_binary/2,"
},
{
"path": "src/h2_frame_window_update.erl",
"chars": 2400,
"preview": "-module(h2_frame_window_update).\n-include(\"http2.hrl\").\n-behaviour(h2_frame).\n\n-export(\n [\n new/1,\n format/1,\n "
},
{
"path": "src/h2_padding.erl",
"chars": 1279,
"preview": "-module(h2_padding).\n-include(\"http2.hrl\").\n\n-export([\n is_padded/1,\n read_possibly_padded_payload/2\n "
},
{
"path": "src/h2_settings.erl",
"chars": 1822,
"preview": "-module(h2_settings).\n-include(\"http2.hrl\").\n\n-export([\n diff/2,\n to_proplist/1\n ]).\n\n-spec diff("
},
{
"path": "src/h2_stream.erl",
"chars": 23166,
"preview": "-module(h2_stream).\n-include(\"http2.hrl\").\n\n%% Public API\n-export([\n start_link/5,\n send_event/2,\n "
},
{
"path": "src/h2_stream_set.erl",
"chars": 29756,
"preview": "-module(h2_stream_set).\n-include(\"http2.hrl\").\n\n%% This module exists to manage a set of all streams for a given\n%% conn"
},
{
"path": "src/sock.erl",
"chars": 1879,
"preview": "-module(sock).\n\n-type transport() :: gen_tcp | ssl.\n-type socket() :: {gen_tcp, inet:socket()|undefined} | {ssl, ssl:ssl"
},
{
"path": "test/chatterbox_test_buddy.erl",
"chars": 3724,
"preview": "-module(chatterbox_test_buddy).\n\n-compile([export_all]).\n\n-include_lib(\"common_test/include/ct.hrl\").\n\nstart(Config) ->\n"
},
{
"path": "test/chatterbox_tests.erl",
"chars": 387,
"preview": "-module(chatterbox_tests).\n\n-include_lib(\"eunit/include/eunit.hrl\").\n\nchatterbox_test_() ->\n {setup,\n fun() ->\n "
},
{
"path": "test/client_server_SUITE.erl",
"chars": 7641,
"preview": "-module(client_server_SUITE).\n\n-include(\"http2.hrl\").\n\n-include_lib(\"eunit/include/eunit.hrl\").\n-include_lib(\"common_tes"
},
{
"path": "test/client_server_SUITE_data/README.md",
"chars": 80,
"preview": "Thanks to reveal-js for the test content.\n\nhttps://github.com/hakimel/reveal.js\n"
},
{
"path": "test/client_server_SUITE_data/bower.json",
"chars": 523,
"preview": "{\n \"name\": \"reveal.js\",\n \"version\": \"3.2.0\",\n \"main\": [\n \"js/reveal.js\",\n \"css/reveal.css\"\n ],\n \"homepage\": \""
},
{
"path": "test/client_server_SUITE_data/css/print/paper.css",
"chars": 4861,
"preview": "/* Default Print Stylesheet Template\n by Rob Glazebrook of CSSnewbie.com\n Last Updated: June 4, 2008\n\n Feel free ("
},
{
"path": "test/client_server_SUITE_data/css/print/pdf.css",
"chars": 3036,
"preview": "/**\n * This stylesheet is used to print reveal.js\n * presentations to PDF.\n *\n * https://github.com/hakimel/reveal.js#pd"
},
{
"path": "test/client_server_SUITE_data/css/reveal.css",
"chars": 49167,
"preview": "/*!\n * reveal.js\n * http://lab.hakim.se/reveal-js\n * MIT licensed\n *\n * Copyright (C) 2015 Hakim El Hattab, http://hakim"
},
{
"path": "test/client_server_SUITE_data/css/reveal.scss",
"chars": 36697,
"preview": "/*!\n * reveal.js\n * http://lab.hakim.se/reveal-js\n * MIT licensed\n *\n * Copyright (C) 2015 Hakim El Hattab, http://hakim"
},
{
"path": "test/client_server_SUITE_data/css/theme/README.md",
"chars": 1606,
"preview": "## Dependencies\n\nThemes are written using Sass to keep things modular and reduce the need for repeated selectors across "
},
{
"path": "test/client_server_SUITE_data/css/theme/beige.css",
"chars": 6847,
"preview": "/**\n * Beige theme for reveal.js.\n *\n * Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se\n */\n@import url(../../l"
},
{
"path": "test/client_server_SUITE_data/css/theme/black.css",
"chars": 6260,
"preview": "/**\n * Black theme for reveal.js. This is the opposite of the 'white' theme.\n *\n * Copyright (C) 2015 Hakim El Hattab, h"
},
{
"path": "test/client_server_SUITE_data/css/theme/blood.css",
"chars": 6694,
"preview": "/**\n * Blood theme for reveal.js\n * Author: Walther http://github.com/Walther\n *\n * Designed to be used with highlight.j"
},
{
"path": "test/client_server_SUITE_data/css/theme/league.css",
"chars": 6915,
"preview": "/**\n * League theme for reveal.js.\n *\n * This was the default theme pre-3.0.0.\n *\n * Copyright (C) 2011-2012 Hakim El Ha"
},
{
"path": "test/client_server_SUITE_data/css/theme/moon.css",
"chars": 6133,
"preview": "/**\n * Solarized Dark theme for reveal.js.\n * Author: Achim Staebler\n */\n@import url(../../lib/font/league-gothic/league"
},
{
"path": "test/client_server_SUITE_data/css/theme/night.css",
"chars": 6048,
"preview": "/**\n * Black theme for reveal.js.\n *\n * Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se\n */\n@import url(https:/"
},
{
"path": "test/client_server_SUITE_data/css/theme/serif.css",
"chars": 6122,
"preview": "/**\n * A simple theme for reveal.js presentations, similar\n * to the default theme. The accent color is brown.\n *\n * Thi"
},
{
"path": "test/client_server_SUITE_data/css/theme/simple.css",
"chars": 6255,
"preview": "/**\n * A simple theme for reveal.js presentations, similar\n * to the default theme. The accent color is darkblue.\n *\n * "
},
{
"path": "test/client_server_SUITE_data/css/theme/sky.css",
"chars": 6656,
"preview": "/**\n * Sky theme for reveal.js.\n *\n * Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se\n */\n@import url(https://f"
},
{
"path": "test/client_server_SUITE_data/css/theme/solarized.css",
"chars": 6134,
"preview": "/**\n * Solarized Light theme for reveal.js.\n * Author: Achim Staebler\n */\n@import url(../../lib/font/league-gothic/leagu"
},
{
"path": "test/client_server_SUITE_data/css/theme/source/beige.scss",
"chars": 1225,
"preview": "/**\n * Beige theme for reveal.js.\n *\n * Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se\n */\n\n\n// Default mixins"
},
{
"path": "test/client_server_SUITE_data/css/theme/source/black.scss",
"chars": 1191,
"preview": "/**\n * Black theme for reveal.js. This is the opposite of the 'white' theme.\n *\n * Copyright (C) 2015 Hakim El Hattab, h"
},
{
"path": "test/client_server_SUITE_data/css/theme/source/blood.scss",
"chars": 1839,
"preview": "/**\n * Blood theme for reveal.js\n * Author: Walther http://github.com/Walther\n *\n * Designed to be used with highlight.j"
},
{
"path": "test/client_server_SUITE_data/css/theme/source/league.scss",
"chars": 1103,
"preview": "/**\n * League theme for reveal.js.\n *\n * This was the default theme pre-3.0.0.\n *\n * Copyright (C) 2011-2012 Hakim El Ha"
},
{
"path": "test/client_server_SUITE_data/css/theme/source/moon.scss",
"chars": 1269,
"preview": "/**\n * Solarized Dark theme for reveal.js.\n * Author: Achim Staebler\n */\n\n\n// Default mixins and settings --------------"
},
{
"path": "test/client_server_SUITE_data/css/theme/source/night.scss",
"chars": 966,
"preview": "/**\n * Black theme for reveal.js.\n *\n * Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se\n */\n\n\n// Default mixins"
},
{
"path": "test/client_server_SUITE_data/css/theme/source/serif.scss",
"chars": 991,
"preview": "/**\n * A simple theme for reveal.js presentations, similar\n * to the default theme. The accent color is brown.\n *\n * Thi"
},
{
"path": "test/client_server_SUITE_data/css/theme/source/simple.scss",
"chars": 1161,
"preview": "/**\n * A simple theme for reveal.js presentations, similar\n * to the default theme. The accent color is darkblue.\n *\n * "
},
{
"path": "test/client_server_SUITE_data/css/theme/source/sky.scss",
"chars": 1145,
"preview": "/**\n * Sky theme for reveal.js.\n *\n * Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se\n */\n\n\n// Default mixins a"
},
{
"path": "test/client_server_SUITE_data/css/theme/source/solarized.scss",
"chars": 1409,
"preview": "/**\n * Solarized Light theme for reveal.js.\n * Author: Achim Staebler\n */\n\n\n// Default mixins and settings -------------"
},
{
"path": "test/client_server_SUITE_data/css/theme/source/white.scss",
"chars": 1190,
"preview": "/**\n * White theme for reveal.js. This is the opposite of the 'black' theme.\n *\n * Copyright (C) 2015 Hakim El Hattab, h"
},
{
"path": "test/client_server_SUITE_data/css/theme/template/mixins.scss",
"chars": 1619,
"preview": "@mixin vertical-gradient( $top, $bottom ) {\n\tbackground: $top;\n\tbackground: -moz-linear-gradient( top, $top 0%, $bottom "
},
{
"path": "test/client_server_SUITE_data/css/theme/template/settings.scss",
"chars": 1034,
"preview": "// Base settings for all themes that can optionally be\n// overridden by the super-theme\n\n// Background of the presentati"
},
{
"path": "test/client_server_SUITE_data/css/theme/template/theme.scss",
"chars": 6028,
"preview": "// Base theme template for reveal.js\n\n/*********************************************\n * GLOBAL STYLES\n *****************"
},
{
"path": "test/client_server_SUITE_data/css/theme/white.css",
"chars": 6253,
"preview": "/**\n * White theme for reveal.js. This is the opposite of the 'black' theme.\n *\n * Copyright (C) 2015 Hakim El Hattab, h"
},
{
"path": "test/client_server_SUITE_data/index.html",
"chars": 15674,
"preview": "<!doctype html>\n<html lang=\"en\">\n\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\n\t\t<title>reveal.js – The HTML Presentation Framework"
},
{
"path": "test/client_server_SUITE_data/js/reveal.js",
"chars": 129551,
"preview": "/*!\n * reveal.js\n * http://lab.hakim.se/reveal-js\n * MIT licensed\n *\n * Copyright (C) 2015 Hakim El Hattab, http://hakim"
},
{
"path": "test/client_server_SUITE_data/lib/css/zenburn.css",
"chars": 1833,
"preview": "/*\nZenburn style from voldmar.ru (c) Vladimir Epifanov <voldmar@voldmar.ru>\nbased on dark.css by Ivan Sagalaev\n*/\n\n.hljs"
},
{
"path": "test/client_server_SUITE_data/lib/font/league-gothic/LICENSE",
"chars": 92,
"preview": "SIL Open Font License (OFL)\nhttp://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL\n"
},
{
"path": "test/client_server_SUITE_data/lib/font/league-gothic/league-gothic.css",
"chars": 308,
"preview": "@font-face {\n font-family: 'League Gothic';\n src: url('league-gothic.eot');\n src: url('league-gothic.eot?#iefix"
},
{
"path": "test/client_server_SUITE_data/lib/font/source-sans-pro/LICENSE",
"chars": 4484,
"preview": "SIL Open Font License\n\nCopyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name "
},
{
"path": "test/client_server_SUITE_data/lib/font/source-sans-pro/source-sans-pro.css",
"chars": 1424,
"preview": "@font-face {\n font-family: 'Source Sans Pro';\n src: url('source-sans-pro-regular.eot');\n src: url('source-sans-"
},
{
"path": "test/client_server_SUITE_data/lib/js/classList.js",
"chars": 1582,
"preview": "/*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js*/\nif(typeof document!==\"undefined\"&&!(\""
},
{
"path": "test/client_server_SUITE_data/lib/js/html5shiv.js",
"chars": 235,
"preview": "document.createElement('header');\ndocument.createElement('nav');\ndocument.createElement('section');\ndocument.createEleme"
},
{
"path": "test/client_server_SUITE_data/plugin/highlight/highlight.js",
"chars": 441180,
"preview": "// START CUSTOM REVEAL.JS INTEGRATION\n(function() {\n\tif( typeof window.addEventListener === 'function' ) {\n\t\tvar hljs_no"
},
{
"path": "test/client_server_SUITE_data/plugin/markdown/example.html",
"chars": 4171,
"preview": "<!doctype html>\n<html lang=\"en\">\n\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\n\t\t<title>reveal.js - Markdown Demo</title>\n\n\t\t<link "
},
{
"path": "test/client_server_SUITE_data/plugin/markdown/example.md",
"chars": 230,
"preview": "# Markdown Demo\n\n\n\n## External 1.1\n\nContent 1.1\n\nNote: This will only appear in the speaker notes window.\n\n\n## External "
},
{
"path": "test/client_server_SUITE_data/plugin/markdown/markdown.js",
"chars": 12075,
"preview": "/**\n * The reveal.js markdown plugin. Handles parsing of\n * markdown inside of presentations as well as loading\n * of ex"
},
{
"path": "test/client_server_SUITE_data/plugin/markdown/marked.js",
"chars": 15752,
"preview": "/**\n * marked - a markdown parser\n * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed)\n * https://github.com/"
},
{
"path": "test/client_server_SUITE_data/plugin/math/math.js",
"chars": 1688,
"preview": "/**\n * A plugin which enables rendering of math equations inside\n * of reveal.js slides. Essentially a thin wrapper for "
},
{
"path": "test/client_server_SUITE_data/plugin/multiplex/client.js",
"chars": 369,
"preview": "(function() {\n\tvar multiplex = Reveal.getConfig().multiplex;\n\tvar socketId = multiplex.id;\n\tvar socket = io.connect(mult"
},
{
"path": "test/client_server_SUITE_data/plugin/multiplex/index.js",
"chars": 1539,
"preview": "var http = require('http');\nvar express\t\t= require('express');\nvar fs\t\t\t= require('fs');\nvar io\t\t\t= require('sock"
},
{
"path": "test/client_server_SUITE_data/plugin/multiplex/master.js",
"chars": 819,
"preview": "(function() {\n\n\t// Don't emit events from inside of notes windows\n\tif ( window.location.search.match( /receiver/gi ) ) {"
},
{
"path": "test/client_server_SUITE_data/plugin/notes/notes.html",
"chars": 9909,
"preview": "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\n\t\t<title>reveal.js - Slide Notes</title>\n\n\t\t<style>\n\t"
},
{
"path": "test/client_server_SUITE_data/plugin/notes/notes.js",
"chars": 3865,
"preview": "/**\n * Handles opening of and synchronization with the reveal.js\n * notes window.\n *\n * Handshake process:\n * 1. This wi"
},
{
"path": "test/client_server_SUITE_data/plugin/notes-server/client.js",
"chars": 1880,
"preview": "(function() {\n\n\t// don't emit events from inside the previews themselves\n\tif( window.location.search.match( /receiver/gi"
},
{
"path": "test/client_server_SUITE_data/plugin/notes-server/index.js",
"chars": 1826,
"preview": "var http = require('http');\nvar express = require('express');\nvar fs = require('fs');\nvar io = requ"
},
{
"path": "test/client_server_SUITE_data/plugin/notes-server/notes.html",
"chars": 9505,
"preview": "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\n\t\t<title>reveal.js - Slide Notes</title>\n\n\t\t<style>\n\t"
},
{
"path": "test/client_server_SUITE_data/plugin/print-pdf/print-pdf.js",
"chars": 1214,
"preview": "/**\n * phantomjs script for printing presentations to PDF.\n *\n * Example:\n * phantomjs print-pdf.js \"http://lab.hakim.se"
},
{
"path": "test/client_server_SUITE_data/plugin/search/search.js",
"chars": 7151,
"preview": "/*\n * Handles finding a text string anywhere in the slides and showing the next occurrence to the user\n * by navigatatin"
},
{
"path": "test/client_server_SUITE_data/plugin/zoom-js/zoom.js",
"chars": 7989,
"preview": "// Custom reveal.js integration\n(function(){\n\tvar isEnabled = true;\n\n\tdocument.querySelector( '.reveal .slides' ).addEve"
},
{
"path": "test/double_body_handler.erl",
"chars": 1678,
"preview": "-module(double_body_handler).\n\n-include_lib(\"chatterbox/include/http2.hrl\").\n\n-behaviour(h2_stream).\n\n-export([\n "
},
{
"path": "test/echo_handler.erl",
"chars": 1704,
"preview": "-module(echo_handler).\n\n-include_lib(\"chatterbox/include/http2.hrl\").\n\n-behaviour(h2_stream).\n\n-export([\n init/3"
},
{
"path": "test/flow_control_SUITE.erl",
"chars": 6023,
"preview": "-module(flow_control_SUITE).\n\n-include(\"http2.hrl\").\n-include_lib(\"eunit/include/eunit.hrl\").\n-include_lib(\"common_test/"
},
{
"path": "test/flow_control_handler.erl",
"chars": 1769,
"preview": "-module(flow_control_handler).\n\n-include_lib(\"chatterbox/include/http2.hrl\").\n\n-define(SEND_BYTES, 68).\n\n-behaviour(h2_s"
},
{
"path": "test/header_continuation_SUITE.erl",
"chars": 6010,
"preview": "-module(header_continuation_SUITE).\n\n-include(\"http2.hrl\").\n-include_lib(\"eunit/include/eunit.hrl\").\n-include_lib(\"commo"
},
{
"path": "test/http2_frame_size_SUITE.erl",
"chars": 3506,
"preview": "-module(http2_frame_size_SUITE).\n\n-include(\"http2.hrl\").\n-include_lib(\"eunit/include/eunit.hrl\").\n-include_lib(\"common_t"
},
{
"path": "test/http2_spec_3_5_SUITE.erl",
"chars": 2440,
"preview": "-module(http2_spec_3_5_SUITE).\n\n-include(\"http2.hrl\").\n-include_lib(\"eunit/include/eunit.hrl\").\n-include_lib(\"common_tes"
},
{
"path": "test/http2_spec_4_3_SUITE.erl",
"chars": 1031,
"preview": "-module(http2_spec_4_3_SUITE).\n\n-include(\"http2.hrl\").\n-include_lib(\"eunit/include/eunit.hrl\").\n-include_lib(\"common_tes"
},
{
"path": "test/http2_spec_5_1_SUITE.erl",
"chars": 9249,
"preview": "-module(http2_spec_5_1_SUITE).\n\n-include(\"http2.hrl\").\n-include_lib(\"eunit/include/eunit.hrl\").\n-include_lib(\"common_tes"
},
{
"path": "test/http2_spec_5_3_SUITE.erl",
"chars": 3692,
"preview": "-module(http2_spec_5_3_SUITE).\n\n-include(\"http2.hrl\").\n-include_lib(\"eunit/include/eunit.hrl\").\n-include_lib(\"common_tes"
},
{
"path": "test/http2_spec_5_5_SUITE.erl",
"chars": 1191,
"preview": "-module(http2_spec_5_5_SUITE).\n\n-include(\"http2.hrl\").\n-include_lib(\"eunit/include/eunit.hrl\").\n-include_lib(\"common_tes"
},
{
"path": "test/http2_spec_6_1_SUITE.erl",
"chars": 1582,
"preview": "-module(http2_spec_6_1_SUITE).\n\n-include(\"http2.hrl\").\n-include_lib(\"eunit/include/eunit.hrl\").\n-include_lib(\"common_tes"
},
{
"path": "test/http2_spec_6_2_SUITE.erl",
"chars": 2613,
"preview": "-module(http2_spec_6_2_SUITE).\n\n-include(\"http2.hrl\").\n-include_lib(\"eunit/include/eunit.hrl\").\n-include_lib(\"common_tes"
},
{
"path": "test/http2_spec_6_4_SUITE.erl",
"chars": 926,
"preview": "-module(http2_spec_6_4_SUITE).\n\n-include(\"http2.hrl\").\n-include_lib(\"eunit/include/eunit.hrl\").\n-include_lib(\"common_tes"
},
{
"path": "test/http2_spec_6_5_SUITE.erl",
"chars": 3294,
"preview": "-module(http2_spec_6_5_SUITE).\n\n-include(\"http2.hrl\").\n-include_lib(\"eunit/include/eunit.hrl\").\n-include_lib(\"common_tes"
},
{
"path": "test/http2_spec_6_9_SUITE.erl",
"chars": 6457,
"preview": "-module(http2_spec_6_9_SUITE).\n\n-include(\"http2.hrl\").\n-include_lib(\"eunit/include/eunit.hrl\").\n-include_lib(\"common_tes"
},
{
"path": "test/http2_spec_8_1_SUITE.erl",
"chars": 12977,
"preview": "-module(http2_spec_8_1_SUITE).\n\n-include(\"http2.hrl\").\n-include_lib(\"eunit/include/eunit.hrl\").\n-include_lib(\"common_tes"
},
{
"path": "test/http2c.erl",
"chars": 10241,
"preview": "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n%%\n%% PLEASE ONLY USE FOR TESTING\n%%\n%%%%%%%%%%%%%%%%%%"
},
{
"path": "test/peer_test_handler.erl",
"chars": 1916,
"preview": "-module(peer_test_handler).\n\n-include_lib(\"chatterbox/include/http2.hrl\").\n\n-behaviour(h2_stream).\n\n-export([\n i"
},
{
"path": "test/protocol_errors_SUITE.erl",
"chars": 2581,
"preview": "-module(protocol_errors_SUITE).\n\n-include(\"http2.hrl\").\n-include_lib(\"eunit/include/eunit.hrl\").\n-include_lib(\"common_te"
},
{
"path": "test/server_connection_receive_window.erl",
"chars": 1012,
"preview": "-module(server_connection_receive_window).\n\n-behaviour(h2_stream).\n\n-export([\n init/3,\n on_receive_reque"
},
{
"path": "test/server_stream_receive_window.erl",
"chars": 1017,
"preview": "-module(server_stream_receive_window).\n\n-behaviour(h2_stream).\n\n-export([\n init/3,\n on_receive_request_h"
},
{
"path": "test/settings_handshake_SUITE.erl",
"chars": 3800,
"preview": "-module(settings_handshake_SUITE).\n\n-include(\"http2.hrl\").\n-include_lib(\"eunit/include/eunit.hrl\").\n-include_lib(\"common"
},
{
"path": "test/starting_SUITE.erl",
"chars": 1106,
"preview": "-module(starting_SUITE).\n\n%%-include(\"http2.hrl\").\n-include_lib(\"eunit/include/eunit.hrl\").\n-include_lib(\"common_test/in"
}
]
About this extraction
This page contains the full source code of the joedevivo/chatterbox GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 122 files (1.1 MB), approximately 330.2k tokens, and a symbol index with 179 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.