Packages
ace
0.11.0
0.19.0
0.18.10
0.18.9
0.18.8
0.18.7
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.1
0.17.0
0.16.8
0.16.7
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.11
0.15.10
0.15.9
0.15.8
0.15.7
0.15.6
retired
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.8
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.13.1
0.13.0
0.12.1
0.12.0
retired
0.11.1
0.11.0
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.1
0.8.0
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.0
0.3.0
0.2.0
HTTP web server and client, supports http1 and http2
Current section
Files
Jump to
Current section
Files
lib/ace/http2/connection.ex
defmodule Ace.HTTP2.Connection do
require Logger
alias Ace.{
HPack
}
alias Ace.HTTP2.{
Frame,
Stream
}
@preface "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
@default_settings %{}
def preface() do
@preface
end
defstruct [
# next: :preface, :handshake, :any, :continuation,
next: :any,
peer: :server,
buffer: "",
settings: nil,
socket: nil,
decode_context: nil,
encode_context: nil,
initial_window_size: nil,
outbound_window: nil,
streams: nil,
stream_supervisor: nil,
client_stream_id: 0
# accept_push always false for server but usable in client.
]
use GenServer
def start_link(listen_socket, stream_supervisor) do
IO.inspect("start server")
GenServer.start_link(__MODULE__, {listen_socket, stream_supervisor})
end
def init({listen_socket, {mod, conf}}) do
{:ok, stream_supervisor} = Supervisor.start_link(
[Supervisor.Spec.worker(mod, [conf], restart: :transient)],
strategy: :simple_one_for_one
)
init({listen_socket, stream_supervisor})
end
def init({listen_socket, stream_supervisor}) when is_pid(stream_supervisor) do
{:ok, {:listen_socket, listen_socket, stream_supervisor}, 0}
end
def handle_info(:timeout, {:listen_socket, listen_socket, stream_supervisor}) do
{:ok, socket} = :ssl.transport_accept(listen_socket)
:ok = :ssl.ssl_accept(socket)
{:ok, "h2"} = :ssl.negotiated_protocol(socket)
:ssl.send(socket, Frame.Settings.new() |> Frame.Settings.serialize())
:ssl.setopts(socket, [active: :once])
decode_context = HPack.new_context(4_096)
encode_context = HPack.new_context(4_096)
initial_state = %__MODULE__{
socket: socket,
outbound_window: 65_535,
# DEBT set when handshaking settings
initial_window_size: 65_535,
decode_context: decode_context,
encode_context: encode_context,
streams: %{},
stream_supervisor: stream_supervisor
}
{:noreply, {:pending, initial_state}}
end
def handle_info({:ssl, connection, @preface <> data}, {:pending, state}) do
state = %{state | next: :handshake}
handle_info({:ssl, connection, data}, state)
end
def handle_info({:ssl, _, data}, state = %__MODULE__{}) do
data = state.buffer <> data
state = %{state | buffer: ""}
case consume(data, state) do
{:ok, state} ->
{:noreply, state}
{:error, {error, debug}} ->
Logger.warn("ERROR: #{inspect(error)}, #{inspect(debug)}")
frame = Frame.GoAway.new(0, error, debug)
outbound = Frame.GoAway.serialize(frame)
:ok = :ssl.send(state.socket, outbound)
# Despite being an error the connection has successfully dealt with the client and does not need to crash
{:stop, :normal, state}
end
end
def handle_info({:ssl_closed, _socket}, state) do
{:stop, :normal, state}
end
def handle_info({{:stream, stream_id, _ref}, message}, state) do
{frames, state} = stream_set_dispatch(stream_id, message, state)
outbound = Enum.map(frames, &Frame.serialize/1)
:ok = :ssl.send(state.socket, outbound)
{:noreply, state}
end
def handle_info({:DOWN, ref, :process, _pid, reason}, state) do
{_id, stream} = Enum.find(state.streams, fn
({_id, %{monitor: ^ref}}) ->
true
(_) -> false
end)
{outbound, stream} = Stream.terminate(stream, reason)
streams = Map.put(state.streams, stream.id, stream)
state = %{state | streams: streams}
outbound = Enum.map(outbound, &Frame.serialize/1)
:ok = :ssl.send(state.socket, outbound)
{:noreply, state}
end
def consume(buffer, state) do
case Frame.parse_from_buffer(buffer, max_length: 16_384) do
{:ok, {raw_frame, unprocessed}} ->
if raw_frame do
case Frame.decode(raw_frame) do
{:ok, frame} ->
Logger.debug("UP: #{inspect(frame)}")
case consume_frame(frame, state) do
{outbound, state} when is_list(outbound) ->
outbound = Enum.map(outbound, &Frame.serialize/1)
:ok = :ssl.send(state.socket, outbound)
consume(unprocessed, state)
{:error, reason} ->
{:error, reason}
end
{:error, {:unknown_frame_type, type}} ->
case state.next do
:any ->
Logger.debug("Dropping unknown frame type (#{type})")
consume(unprocessed, state)
{:continuation, _stream_id, _header_block_fragment, _end_stream} ->
{:error, {:protocol_error, "Unknown frame interupted continuation"}}
end
{:error, reason} ->
{:error, reason}
end
else
state = %{state | buffer: unprocessed}
:ssl.setopts(state.socket, [active: :once])
{:ok, state}
end
{:error, reason} ->
{:error, reason}
end
end
def consume_frame(settings = %Frame.Settings{ack: false}, state = %{settings: nil, next: :handshake}) do
case update_settings(settings, %{state | settings: @default_settings}) do
{:ok, new_state} ->
new_state = %{new_state | next: :any}
# DEBT only do this if initial_window_size has increased
{frames, newer_state} = transmit_available(new_state)
# |> IO.inspect
{[Frame.Settings.ack() | frames], newer_state}
{:error, reason} ->
{:error, reason}
end
end
def consume_frame(_, %{settings: nil, next: :handshake}) do
{:error, {:protocol_error, "Did not receive settings frame"}}
end
def consume_frame(frame = %Frame.Ping{ack: false}, state = %{next: :any}) do
{[Frame.Ping.ack(frame)], state}
end
def consume_frame(%Frame.Ping{ack: true}, state = %{next: :any}) do
{[], state}
end
def consume_frame(%Frame.GoAway{error: :no_error}, _state = %{next: :any}) do
{:error, {:no_error, "Client closed connection"}}
end
def consume_frame(frame = %Frame.WindowUpdate{stream_id: 0}, state = %{next: :any}) do
new_window = state.outbound_window + frame.increment
# 2^31 - 1
if new_window <= 2_147_483_647 do
{[], %{state | outbound_window: new_window}}
transmit_available(%{state | outbound_window: new_window})
else
{:error, {:flow_control_error, "Total window size exceeded max allowed"}}
end
end
def consume_frame(frame = %Frame.WindowUpdate{stream_id: _}, state = %{next: :any}) do
case dispatch(frame.stream_id, {:window_update, frame.increment}, state) do
{:ok, {outbound, new_state}} ->
{frames, newer_state} = transmit_available(new_state)
{outbound ++ frames, newer_state}
{:error, reason} ->
{:error, reason}
end
end
def consume_frame(%Frame.Priority{}, state = %{next: :any}) do
IO.inspect("Ignoring priority frame")
{[], state}
end
def consume_frame(settings = %Frame.Settings{}, state = %{next: :any}) do
if settings.ack do
{[], state}
else
case update_settings(settings, state) do
{:ok, new_state} ->
{[Frame.Settings.ack()], new_state}
{:error, reason} ->
{:error, reason}
end
end
end
def consume_frame(%Frame.PushPromise{}, _state = %{next: :any}) do
{:error, {:protocol_error, "Clients cannot send push promises"}}
end
def consume_frame(frame = %Frame.RstStream{}, state = %{next: :any}) do
case dispatch(frame.stream_id, :reset, state) do
{:ok, {outbound, state}} ->
{outbound, state}
{:error, reason} ->
{:error, reason}
end
end
def consume_frame(frame = %Frame.Headers{}, state = %{next: :any}) do
if frame.end_headers do
case HPack.decode(frame.header_block_fragment, state.decode_context) do
{:ok, {headers, new_decode_context}} ->
# preface bad name as could be trailers perhaps metadata
preface = %{headers: headers, end_stream: frame.end_stream}
state = %{state | decode_context: new_decode_context}
case dispatch(frame.stream_id, preface, state) do
{:ok, {outbound, state}} ->
{outbound, state}
{:error, reason} ->
{:error, reason}
end
{:error, :compression_error} ->
{:error, {:compression_error, "bad decode"}}
end
else
{[], %{state | next: {:continuation, frame.stream_id, frame.header_block_fragment, frame.end_stream}}}
end
end
def consume_frame(frame = %Frame.Continuation{}, state = %{next: {:continuation, _stream_id, buffer, end_stream}}) do
buffer = buffer <> frame.header_block_fragment
if frame.end_headers do
{:ok, {headers, new_decode_context}} = HPack.decode(buffer, state.decode_context)
# preface bad name as could be trailers perhaps metadata
state = %{state | decode_context: new_decode_context}
preface = %{headers: headers, end_stream: end_stream}
case dispatch(frame.stream_id, preface, state) do
{:ok, {outbound, state}} ->
state = %{state | next: :any}
{outbound, state}
{:error, reason} ->
{:error, reason}
end
else
{[], %{state | next: {:continuation, frame.stream_id, buffer, end_stream}}}
end
end
def consume_frame(frame = %Frame.Data{}, state = %{next: :any}) do
# https://tools.ietf.org/html/rfc7540#section-5.2.2
# Deployments that do not require this capability can advertise a flow-
# control window of the maximum size (2^31-1) and can maintain this
# window by sending a WINDOW_UPDATE frame when any data is received.
# This effectively disables flow control for that receiver.
data = %{data: frame.data, end_stream: frame.end_stream}
case dispatch(frame.stream_id, data, state) do
{:ok, {outbound, state}} ->
{outbound ++ [Frame.WindowUpdate.new(0, 65_535), Frame.WindowUpdate.new(frame.stream_id, 65_535)], state}
{:error, reason} ->
{:error, reason}
end
end
def consume_frame(frame, %{next: next}) do
IO.inspect("expected next '#{inspect(next)}' got: #{inspect(frame)}")
{:error, {:protocol_error, "Unexpected frame"}}
end
# TODO handle all settings
def update_settings(new, state) do
new_state = state
|> update_initial_window_size(new.initial_window_size)
# case new.max_frame_size do
# nil ->
# x when x < 16_384 ->
# {:error, {:protocol_error, "max_frame_size too small"}}
# new ->
# {:ok, state}
# end
{:ok, new_state}
end
defp update_initial_window_size(state, nil) do
state
end
defp update_initial_window_size(state, initial_window_size) do
streams = Enum.map(state.streams, fn({k, v}) -> {k, %{v | initial_window_size: initial_window_size}} end) |> Enum.into(%{})
%{state| initial_window_size: initial_window_size, streams: streams}
end
def transmit_available(state) do
{remaining_window, data_frames, streams} = Enum.reduce(state.streams, {state.outbound_window, [], state.streams}, fn
({_stream_id, _stream}, {0, data_frames, streams}) ->
{0, data_frames, streams}
({stream_id, stream}, {connection_window, data_frames, streams}) ->
available_window = min(Stream.outbound_window(stream), connection_window)
{to_send, buffer} = case stream.buffer do
<<to_send::binary-size(available_window), buffer::binary>> ->
{to_send, buffer}
to_send ->
{to_send, ""}
end
case to_send do
"" ->
{connection_window, data_frames, streams}
_ ->
window_used = :erlang.iolist_size(to_send)
stream = %{stream | sent: stream.sent + window_used, buffer: buffer}
streams = %{streams | stream_id => stream}
end_stream = (stream.status == :closed || stream.status == :closed_local) && (buffer == "")
data_frame = Frame.Data.new(stream_id, to_send, end_stream)
remaining_connection_window = connection_window - window_used
{remaining_connection_window, data_frames ++ [data_frame], streams}
end
end)
state = %{state | outbound_window: remaining_window, streams: streams}
{data_frames, state}
end
def dispatch(stream_id, message, state) do
# NOTE only evaluate creating new stream if one does not exist
# DEBT don't have stream rely on shape of connection state
case fetch_stream(state, stream_id) do
{:ok, stream} ->
case Stream.consume(stream, message) do
{:ok, {outbound, stream}} ->
streams = Map.put(state.streams, stream_id, stream)
{:ok, {outbound, %{state | streams: streams, client_stream_id: max(stream.id, state.client_stream_id)}}}
{:error, reason} ->
{:error, reason}
end
{:error, reason} ->
{:error, reason}
end
end
def stream_set_dispatch(id, %{headers: headers, end_stream: end_stream}, state) do
# Map or array to map, best receive a list and response takes care of ordering
headers = for h <- headers, do: h
{:ok, {header_block, new_encode_context}} = HPack.encode(headers, state.encode_context)
state = %{state | encode_context: new_encode_context}
header_frame = Frame.Headers.new(id, header_block, true, end_stream)
{[header_frame], state}
end
def stream_set_dispatch(stream_id, %{data: data, end_stream: end_stream}, state) do
{:ok, stream} = Map.fetch(state.streams, stream_id)
if stream.status == :closed || stream.status == :closed_local do
Logger.info("Data lost because stream already closed")
# DEBT expose through backpressure
{[], state}
else
connection_window = state.outbound_window
stream_window = Stream.outbound_window(stream)
available_window = min(stream_window, connection_window)
{to_send, buffer} = case data do
<<to_send::binary-size(available_window), buffer::binary>> ->
{to_send, buffer}
to_send ->
{to_send, ""}
end
true = :erlang.is_binary(to_send)
window_used = :erlang.iolist_size(to_send)
stream = %{stream | sent: stream.sent + window_used, buffer: buffer}
stream = case {stream.status, end_stream} do
{:closed_remote, true} ->
%{stream | status: :closed}
{:open, true} ->
%{stream | status: :closed_local}
{_, false} ->
stream
end
streams = %{state.streams | stream_id => stream}
data_frame = Frame.Data.new(stream_id, to_send, end_stream && (buffer == ""))
remaining_connection_window = connection_window - window_used
{[data_frame], %{state | streams: streams, outbound_window: remaining_connection_window}}
end
end
def stream_set_dispatch(stream_id, %Ace.HTTP2.Stream.Reset{error: reason}, state) do
# DEBT does not check if stream idle or already closed
# stream = fetch_stream(state, stream_id)
rst_frame = Frame.RstStream.new(stream_id, reason)
{[rst_frame], state}
end
defp fetch_stream(state, stream_id) do
case Map.fetch(state.streams, stream_id) do
{:ok, stream} ->
{:ok, stream}
:error ->
new_stream(stream_id, state)
end
end
def new_stream(0, _) do
{:error, {:protocol_error, "Stream 0 reserved for connection"}}
end
def new_stream(stream_id, state) when rem(stream_id, 2) == 1 do
if stream_id > state.client_stream_id do
{:ok, Stream.idle(stream_id, state)}
else
{:error, {:protocol_error, "New streams must always have a higher stream id"}}
end
end
def new_stream(_, _) do
{:error, {:protocol_error, "Clients must start odd streams"}}
end
end