Packages
ace
0.14.5
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.ex
defmodule Ace.HTTP2 do
@moduledoc """
**Hypertext Transfer Protocol Version 2 (HTTP/2)**
> HTTP/2 enables a more efficient use of network
> resources and a reduced perception of latency by introducing header
> field compression and allowing multiple concurrent exchanges on the
> same connection. It also introduces unsolicited push of
> representations from servers to clients.
*Quote from [rfc 7540](https://tools.ietf.org/html/rfc7540).*
"""
import Kernel, except: [send: 2]
@known_methods ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]
def send(stream, request = %{scheme: nil}) do
send(stream, %{request | scheme: :https})
end
def send(stream, item = %type{}) when type in [Raxx.Request, Raxx.Response, Raxx.Fragment, Raxx.Trailer] do
{:stream, pid, _id, _ref} = stream
GenServer.call(pid, {:send, stream, item})
end
def send(stream, item = {:promise, %Raxx.Request{}}) do
{:stream, pid, _id, _ref} = stream
GenServer.call(pid, {:send, stream, item})
end
@doc """
Send a ping frame over an HTTP/2 connection.
"""
def ping(connection, identifier) when bit_size(identifier) == 64 do
GenServer.call(connection, {:ping, identifier})
end
@doc """
Transform an `Raxx.Request` into a generic headers list.
This headers list can be encoded via `Ace.HPack`.
"""
def request_to_headers(request) do
# TODO consider default values for required scheme and authority
query_string = case Plug.Conn.Query.encode(request.query || %{}) do
"" ->
""
encoded ->
"?" <> encoded
end
path = "/" <> Enum.join(request.path, "/") <> query_string
[
{":scheme", Atom.to_string(request.scheme)},
{":authority", request.authority || "example.com"},
{":method", Atom.to_string(request.method)},
{":path", path} |
request.headers
]
end
@doc """
Transform a `Raxx.Response` into a generic headers list.
This headers list can be encoded via `Ace.HPack`.
"""
def response_to_headers(request) do
[
{":status", Integer.to_string(request.status)} |
request.headers
]
end
@doc """
Build a `Raxx.Request` from a decoded list of headers.
Note the required pseudo-headers must be first.
Request pseudo-headers are; `:scheme`, `:authority`, `:method` & `:path`.
Duplicate or missing pseudo-headers will return an error.
"""
# def headers_to_request(headers, end_stream) do
# OK.for do
# request <- build_request(headers)
# after
# %{request | body: !end_stream}
# end
# end
def headers_to_request(headers, end_stream) do
case build_request(headers) do
{:ok, request} ->
{:ok, %{request | body: !end_stream}}
{:error, reason} ->
{:error, reason}
end
end
defp build_request(request_headers) do
build_request(request_headers, {:scheme, :authority, :method, :path})
end
defp build_request([{":scheme", scheme} | rest], {:scheme, authority, method, path}) do
case scheme do
# DEBT can sent headers even be empty?
"" ->
{:error, {:protocol_error, "scheme must not be empty"}}
"https" ->
build_request(rest, {:https, authority, method, path})
"http" ->
build_request(rest, {:http, authority, method, path})
end
end
defp build_request([{":authority", authority} | rest], {scheme, :authority, method, path}) do
case authority do
"" ->
{:error, {:protocol_error, "authority must not be empty"}}
_ ->
build_request(rest, {scheme, authority, method, path})
end
end
defp build_request([{":method", method} | rest], {scheme, authority, :method, path}) do
case method do
"" ->
{:error, {:protocol_error, "method must not be empty"}}
method when method in @known_methods ->
method = String.to_atom(method)
build_request(rest, {scheme, authority, method, path})
end
end
defp build_request([{":path", path} | rest], {scheme, authority, method, :path}) do
case path do
"" ->
{:error, {:protocol_error, "path must not be empty"}}
_ ->
build_request(rest, {scheme, authority, method, path})
end
end
defp build_request([{":" <> psudo, _value} | _rest], _required) do
case psudo do
psudo when psudo in ["scheme", "authority", "method", "path"] ->
{:error, {:protocol_error, "pseudo-header sent more than once"}}
other ->
{:error, {:protocol_error, "unacceptable pseudo-header, :#{other}"}}
end
end
defp build_request(headers, request = {scheme, authority, method, path}) do
if scheme == :scheme or authority == :authority or method == :method or path == :path do
{:error, {:protocol_error, "All pseudo-headers must be sent"}}
else
case read_headers(headers) do
{:ok, headers} ->
uri = URI.parse(path)
query = Plug.Conn.Query.decode(uri.query || "")
segments = Raxx.split_path(uri.path)
request = %Raxx.Request{
scheme: scheme,
authority: authority,
method: method,
mount: [],
path: segments,
query: query,
headers: headers,
body: false,
}
{:ok, request}
{:error, reason} ->
{:error, reason}
end
end
end
@doc """
Build a `Raxx.Response` from a decoded list of headers.
Note the required pseudo-headers must be first.
Response pseudo-headers are; `:status`.
Duplicate or missing pseudo-headers will return an error.
"""
def headers_to_response([{":status", status} | headers], end_stream) do
case read_headers(headers) do
{:ok, headers} ->
{status, ""} = Integer.parse(status)
response = %{Raxx.response(status) | headers: headers, body: !end_stream}
{:ok, response}
end
end
@doc """
Transform a list of decoded headers to a trailers structure.
Note there are no required headers in a trailers set.
"""
def headers_to_trailers(headers) do
{:ok, headers} = read_headers(headers)
%{headers: headers, end_stream: true}
end
defp read_headers(raw, acc \\ [])
defp read_headers([], acc) do
{:ok, Enum.reverse(acc)}
end
defp read_headers([{":"<>_,_} | _], _acc) do
{:error, {:protocol_error, "pseudo-header sent amongst normal headers"}}
end
defp read_headers([{"connection", _} | _rest], _acc) do
{:error, {:protocol_error, "connection header must not be used with HTTP/2"}}
end
defp read_headers([{"te", value} | rest], acc) do
case value do
"trailers" ->
read_headers(rest, [{"te", value}, acc])
_ ->
{:error, {:protocol_error, "TE header field with any value other than 'trailers' is invalid"}}
end
end
defp read_headers([{k, v} | rest], acc) do
case String.downcase(k) == k do
true ->
read_headers(rest, [{k, v} | acc])
false ->
{:error, {:protocol_error, "headers must be lower case"}}
end
end
end