Packages
mist
6.0.3
6.0.3
6.0.2
6.0.1
6.0.0
5.0.4
5.0.3
5.0.2
5.0.1
5.0.0
5.0.0-rc1
4.0.7
4.0.6
4.0.5
4.0.4
4.0.3
4.0.2
4.0.1
4.0.0
3.0.0
2.0.0
1.2.0
1.1.0
1.0.0
1.0.0-rc3
1.0.0-rc2
1.0.0-rc1
0.17.0
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.2
0.13.1
0.13.0
0.12.0
0.11.0
0.10.0
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.3
0.8.2
0.8.1
0.8.0
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.3
0.3.2
0.3.1
0.3.0
0.2.1
0.2.0
0.1.3
a misty Gleam web server
Current section
Files
Jump to
Current section
Files
src/mist/internal/http.gleam
import gleam/bit_array
import gleam/bytes_tree.{type BytesTree}
import gleam/dict.{type Dict}
import gleam/dynamic.{type Dynamic}
import gleam/dynamic/decode
import gleam/erlang/atom.{type Atom}
import gleam/erlang/charlist.{type Charlist}
import gleam/erlang/process.{type Selector}
import gleam/http
import gleam/http/request.{type Request}
import gleam/http/response.{type Response, Response}
import gleam/int
import gleam/list
import gleam/option.{type Option}
import gleam/otp/actor
import gleam/otp/factory_supervisor as factory
import gleam/pair
import gleam/result
import gleam/string
import glisten.{type Socket}
import glisten/transport.{type Transport}
import gramps/websocket
import mist/internal/buffer.{type Buffer, Buffer}
import mist/internal/clock
import mist/internal/encoder
import mist/internal/file
pub type ResponseData {
Websocket
Bytes(BytesTree)
Chunked
File(descriptor: file.FileDescriptor, offset: Int, length: Int)
ServerSentEvents
}
pub type Connection {
Connection(
body: Body,
socket: Socket,
transport: Transport,
factory_name: process.Name(
factory.Message(
fn() -> Result(actor.Started(process.Pid), actor.StartError),
process.Pid,
),
),
)
}
pub type Handler =
fn(Request(Connection)) -> response.Response(ResponseData)
pub type PacketType {
Http
HttphBin
HttpBin
}
pub type HttpUri {
AbsPath(BitArray)
}
pub type HttpPacket {
HttpRequest(Dynamic, HttpUri, #(Int, Int))
HttpHeader(Int, Atom, BitArray, BitArray)
}
pub type DecodedPacket {
BinaryData(HttpPacket, BitArray)
EndOfHeaders(BitArray)
MoreData(Option(Int))
Http2Upgrade(BitArray)
}
pub type DecodeError {
MalformedRequest
InvalidMethod
InvalidPath
UnknownHeader
UnknownMethod
// TODO: better name?
InvalidBody
DiscardPacket
NoHostHeader
InvalidHttpVersion
}
pub fn from_header(value: BitArray) -> String {
let assert Ok(value) = bit_array.to_string(value)
string.lowercase(value)
}
pub fn parse_headers(
bs: BitArray,
socket: Socket,
transport: Transport,
headers: Dict(String, String),
) -> Result(#(Dict(String, String), BitArray), DecodeError) {
case decode_packet(HttphBin, bs, []) {
Ok(BinaryData(HttpHeader(_, _field, field, value), rest)) -> {
let field = from_header(field)
let assert Ok(value) = bit_array.to_string(value)
headers
|> dict.insert(field, value)
|> parse_headers(rest, socket, transport, _)
}
Ok(EndOfHeaders(rest)) -> Ok(#(headers, rest))
Ok(MoreData(size)) -> {
let amount_to_read = option.unwrap(size, 0)
use next <- result.try(read_data(
socket,
transport,
Buffer(amount_to_read, bs),
UnknownHeader,
))
parse_headers(next, socket, transport, headers)
}
_other -> Error(UnknownHeader)
}
}
pub fn read_data(
socket: Socket,
transport: Transport,
buffer: Buffer,
error: DecodeError,
) -> Result(BitArray, DecodeError) {
// TODO: don't hard-code these, probably
let to_read = int.min(buffer.remaining, 1_000_000)
let timeout = 15_000
use data <- result.try(
socket
|> transport.receive_timeout(transport, _, to_read, timeout)
|> result.replace_error(error),
)
let next_buffer =
Buffer(remaining: int.max(0, buffer.remaining - to_read), data: <<
buffer.data:bits,
data:bits,
>>)
case next_buffer.remaining > 0 {
True -> read_data(socket, transport, next_buffer, error)
False -> Ok(next_buffer.data)
}
}
const crnl = <<13:int, 10:int>>
pub type Chunk {
Chunk(data: BitArray, buffer: Buffer)
Complete
}
pub fn parse_chunk(string: BitArray) -> Chunk {
case binary_split(string, <<"\r\n":utf8>>) {
[<<"0":utf8>>, _] -> Complete
[chunk_size, rest] -> {
let assert Ok(chunk_size) = bit_array.to_string(chunk_size)
case int.base_parse(chunk_size, 16) {
Ok(size) -> {
let size = size * 8
case rest {
<<next_chunk:bits-size(size), 13:int, 10:int, rest:bits>> -> {
Chunk(data: next_chunk, buffer: buffer.new(rest))
}
_ -> {
Chunk(data: <<>>, buffer: buffer.new(string))
}
}
}
Error(_) -> {
Chunk(data: <<>>, buffer: buffer.new(string))
}
}
}
_ -> {
Chunk(data: <<>>, buffer: buffer.new(string))
}
}
}
// TODO: use `parse_chunk` for this
fn read_chunk(
socket: Socket,
transport: Transport,
buffer: Buffer,
body: BytesTree,
) -> Result(BytesTree, DecodeError) {
case buffer.data, binary_match(buffer.data, crnl) {
_, Ok(#(offset, _)) -> {
let assert <<
chunk:bytes-size(offset),
_return:int,
_newline:int,
rest:bytes,
>> = buffer.data
use chunk_size <- result.try(
chunk
|> bit_array.to_string
|> result.map(charlist.from_string)
|> result.replace_error(InvalidBody),
)
use size <- result.try(
string_to_int(chunk_size, 16)
|> result.replace_error(InvalidBody),
)
case size {
0 -> Ok(body)
size ->
case rest {
<<next_chunk:bytes-size(size), 13:int, 10:int, rest:bytes>> ->
read_chunk(
socket,
transport,
Buffer(0, rest),
bytes_tree.append(body, next_chunk),
)
_ -> {
use next <- result.try(read_data(
socket,
transport,
Buffer(0, buffer.data),
InvalidBody,
))
read_chunk(socket, transport, Buffer(0, next), body)
}
}
}
}
<<>> as data, _ | data, Error(Nil) -> {
use next <- result.try(read_data(
socket,
transport,
Buffer(0, data),
InvalidBody,
))
read_chunk(socket, transport, Buffer(0, next), body)
}
}
}
pub type HttpVersion {
Http1
Http11
}
pub fn version_to_string(version: HttpVersion) {
case version {
Http1 -> "1.0"
Http11 -> "1.1"
}
}
pub type ParsedRequest {
Http1Request(request: request.Request(Connection), version: HttpVersion)
Upgrade(BitArray)
}
@external(erlang, "mist_ffi", "decode_atom")
fn decode_atom(value: Dynamic) -> Result(atom.Atom, Nil)
fn decode_http_method(value: Dynamic) -> Result(http.Method, Nil) {
let options = atom.create("OPTIONS")
let get = atom.create("GET")
let head = atom.create("HEAD")
let post = atom.create("POST")
let put = atom.create("PUT")
let delete = atom.create("DELETE")
let trace = atom.create("TRACE")
case decode_atom(value) {
Ok(method) if method == options -> Ok(http.Options)
Ok(method) if method == get -> Ok(http.Get)
Ok(method) if method == head -> Ok(http.Head)
Ok(method) if method == post -> Ok(http.Post)
Ok(method) if method == put -> Ok(http.Put)
Ok(method) if method == delete -> Ok(http.Delete)
Ok(method) if method == trace -> Ok(http.Trace)
_ -> {
case decode.run(value, decode.string) {
Ok(str) -> http.parse_method(str)
_ -> Error(Nil)
}
}
}
}
/// Turns the TCP message into an HTTP request
pub fn parse_request(
bs: BitArray,
conn: Connection,
) -> Result(ParsedRequest, DecodeError) {
case decode_packet(HttpBin, bs, []) {
Ok(BinaryData(HttpRequest(http_method, AbsPath(path), version), rest)) -> {
use method <- result.try(
http_method
|> decode_http_method
|> result.replace_error(UnknownMethod),
)
use #(headers, rest) <- result.try(parse_headers(
rest,
conn.socket,
conn.transport,
dict.new(),
))
use path <- result.try(
path
|> bit_array.to_string
|> result.replace_error(InvalidPath),
)
use #(path, query) <- result.try(
get_path_and_query(path)
|> result.replace_error(InvalidPath),
)
let scheme = case conn.transport {
transport.Ssl(..) -> http.Https
transport.Tcp(..) -> http.Http
}
use host_header <- result.try(
dict.get(headers, "host")
|> result.replace_error(NoHostHeader),
)
let #(hostname, port) =
host_header
|> string.split_once(":")
|> result.unwrap(#(host_header, ""))
let port = case int.parse(port) {
Ok(port) -> port
Error(_) ->
case scheme {
http.Https -> 443
http.Http -> 80
}
}
let req =
request.Request(
body: Connection(..conn, body: Initial(rest)),
headers: dict.to_list(headers),
host: hostname,
method: method,
path: path,
port: option.Some(port),
query: option.from_result(query),
scheme: scheme,
)
case version {
#(1, 0) -> Ok(Http1Request(request: req, version: Http1))
#(1, 1) -> Ok(Http1Request(request: req, version: Http11))
_ -> Error(InvalidHttpVersion)
}
}
// "\r\nSM\r\n\r\n"
Ok(Http2Upgrade(<<
13:int,
10:int,
83:int,
77:int,
13:int,
10:int,
13:int,
10:int,
data:bits,
>>)) -> {
Ok(Upgrade(data))
}
Ok(MoreData(size)) -> {
let amount_to_read = option.unwrap(size, 0)
use next <- result.try(read_data(
conn.socket,
conn.transport,
Buffer(amount_to_read, bs),
MalformedRequest,
))
parse_request(next, conn)
}
_ -> Error(DiscardPacket)
}
}
pub type Body {
Initial(BitArray)
Stream(
selector: Selector(BitArray),
data: BitArray,
remaining: Int,
attempts: Int,
)
}
pub fn read_body(
req: Request(Connection),
) -> Result(Request(BitArray), DecodeError) {
let transport = case req.scheme {
http.Https -> transport.Ssl
http.Http -> transport.Tcp
}
case request.get_header(req, "transfer-encoding"), req.body.body {
Ok("chunked"), Initial(rest) -> {
use _nil <- result.try(handle_continue(req))
use chunk <- result.try(read_chunk(
req.body.socket,
transport,
Buffer(remaining: 0, data: rest),
bytes_tree.new(),
))
Ok(request.set_body(req, bytes_tree.to_bit_array(chunk)))
}
_, Initial(rest) -> {
use _nil <- result.try(handle_continue(req))
let body_size =
req.headers
|> list.find(fn(tup) { pair.first(tup) == "content-length" })
|> result.map(pair.second)
|> result.try(int.parse)
|> result.unwrap(0)
let remaining = body_size - bit_array.byte_size(rest)
case body_size, remaining {
0, 0 -> Ok(<<>>)
0, _n -> Ok(rest)
// is this pipelining? check for GET?
_n, 0 -> Ok(rest)
_size, _rem ->
read_data(
req.body.socket,
transport,
Buffer(remaining, rest),
InvalidBody,
)
}
|> result.map(request.set_body(req, _))
|> result.replace_error(InvalidBody)
}
_,
Stream(
selector: selector,
data: data,
remaining: remaining,
attempts: attempts,
)
if remaining > 0
-> {
let res =
selector
|> process.selector_receive(1000)
|> result.replace_error(InvalidBody)
use next <- result.try(res)
let got = bit_array.byte_size(next)
let left = int.max(remaining - got, 0)
let new_data = bit_array.append(data, next)
case left {
0 -> Ok(request.set_body(req, new_data))
_rem ->
read_body(request.set_body(
req,
Connection(
..req.body,
body: Stream(selector, new_data, left, attempts + 1),
),
))
}
}
_, Stream(data: data, ..) -> {
Ok(request.set_body(req, data))
}
}
}
const websocket_key = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
pub type ShaHash {
Sha
}
fn parse_websocket_key(key: String) -> String {
key
|> string.append(websocket_key)
|> crypto_hash(Sha, _)
|> base64_encode
}
pub fn upgrade_socket(
req: Request(Connection),
extensions: List(String),
) -> Result(Response(BytesTree), Request(Connection)) {
use _upgrade <- result.try(
request.get_header(req, "upgrade")
|> result.replace_error(req),
)
use key <- result.try(
request.get_header(req, "sec-websocket-key")
|> result.replace_error(req),
)
use _version <- result.try(
request.get_header(req, "sec-websocket-version")
|> result.replace_error(req),
)
let permessage_deflate = websocket.has_deflate(extensions)
let accept_key = parse_websocket_key(key)
let resp =
response.new(101)
|> response.set_body(bytes_tree.new())
|> response.prepend_header("upgrade", "websocket")
|> response.prepend_header("connection", "Upgrade")
|> response.prepend_header("sec-websocket-accept", accept_key)
case permessage_deflate {
True ->
Ok(response.prepend_header(
resp,
"sec-websocket-extensions",
"permessage-deflate",
))
False -> Ok(resp)
}
}
// TODO: improve this error type
pub fn upgrade(
socket: Socket,
transport: Transport,
extensions: List(String),
req: Request(Connection),
) -> Result(Nil, Nil) {
use resp <- result.try(
upgrade_socket(req, extensions)
|> result.replace_error(Nil),
)
use _sent <- result.try(
resp
|> add_default_headers(req.method == http.Head)
|> maybe_keep_alive
|> encoder.to_bytes_tree("1.1")
|> transport.send(transport, socket, _)
|> result.replace_error(Nil),
)
Ok(Nil)
}
pub fn add_date_header(resp: Response(any)) -> Response(any) {
case response.get_header(resp, "date") {
Error(_nil) -> response.set_header(resp, "date", clock.get_date())
_ -> resp
}
}
pub fn connection_close(resp: Response(any)) -> Response(any) {
response.set_header(resp, "connection", "close")
}
pub fn keep_alive(resp: Response(any)) -> Response(any) {
response.set_header(resp, "connection", "keep-alive")
}
pub fn maybe_keep_alive(resp: Response(any)) -> Response(any) {
case response.get_header(resp, "connection") {
Ok(_) -> resp
_ -> response.set_header(resp, "connection", "keep-alive")
}
}
fn maybe_drop_body(
resp: Response(BytesTree),
is_head_request: Bool,
) -> Response(BytesTree) {
case is_head_request {
True -> response.set_body(resp, bytes_tree.new())
False -> resp
}
}
pub fn add_content_length(
when when: Bool,
length length: Int,
) -> fn(Response(any)) -> Response(any) {
fn(resp: Response(any)) {
case when {
True -> {
let #(_existing, headers) =
resp.headers
|> list.key_pop("content-length")
|> result.lazy_unwrap(fn() { #("", resp.headers) })
Response(..resp, headers: headers)
|> response.set_header("content-length", int.to_string(length))
}
False -> resp
}
}
}
pub fn add_default_headers(
resp: Response(BytesTree),
is_head_response: Bool,
) -> Response(BytesTree) {
let body_size = bytes_tree.byte_size(resp.body)
let #(_existing_content_length, headers) =
resp.headers
|> list.key_pop("content-length")
|> result.lazy_unwrap(fn() { #("", resp.headers) })
let resp = case resp.status, body_size {
// explicitly drop
n, _ if n >= 100 && n <= 199 -> Response(..resp, headers:)
// explicitly drop
n, _ if n == 204 -> Response(..resp, headers:)
// don't add, don't drop
n, 0 if n == 304 -> resp
// don't add, don't drop
_, 0 if is_head_response == True -> resp
// explicitly overwrite
_, _ ->
response.set_header(resp, "content-length", int.to_string(body_size))
}
resp
|> add_date_header
|> maybe_drop_body(is_head_response)
}
fn is_continue(req: Request(Connection)) -> Bool {
req.headers
|> list.find(fn(tup) {
pair.first(tup) == "expect" && pair.second(tup) == "100-continue"
})
|> result.is_ok
}
pub fn handle_continue(req: Request(Connection)) -> Result(Nil, DecodeError) {
case is_continue(req) {
True -> {
response.new(100)
|> response.set_body(bytes_tree.new())
|> encoder.to_bytes_tree("1.1")
|> transport.send(req.body.transport, req.body.socket, _)
|> result.replace_error(MalformedRequest)
}
False -> Ok(Nil)
}
}
@external(erlang, "mist_ffi", "decode_packet")
fn decode_packet(
packet_type packet_type: PacketType,
packet packet: BitArray,
options options: List(a),
) -> Result(DecodedPacket, DecodeError)
@external(erlang, "crypto", "hash")
pub fn crypto_hash(hash hash: ShaHash, data data: String) -> String
@external(erlang, "base64", "encode")
pub fn base64_encode(data data: String) -> String
@external(erlang, "mist_ffi", "binary_match")
fn binary_match(
source source: BitArray,
pattern pattern: BitArray,
) -> Result(#(Int, Int), Nil)
@external(erlang, "mist_ffi", "string_to_int")
fn string_to_int(string string: Charlist, base base: Int) -> Result(Int, Nil)
@external(erlang, "binary", "split")
fn binary_split(source: BitArray, pattern: BitArray) -> List(BitArray)
@external(erlang, "mist_ffi", "get_path_and_query")
fn get_path_and_query(
str: String,
) -> Result(#(String, Result(String, Nil)), #(value, term))