Packages
mist
0.12.0
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_builder.{BitBuilder}
import gleam/bit_string
import gleam/dynamic.{Dynamic}
import gleam/erlang/atom.{Atom}
import gleam/erlang/charlist.{Charlist}
import gleam/http/request.{Request}
import gleam/http/response.{Response}
import gleam/http
import gleam/int
import gleam/iterator.{Iterator}
import gleam/list
import gleam/map.{Map}
import gleam/option.{Option}
import gleam/pair
import gleam/result
import gleam/string
import gleam/uri
import glisten/socket.{Socket}
import glisten/socket/transport.{Transport}
import mist/internal/encoder
import mist/internal/file
import mist/internal/websocket
pub type PacketType {
Http
HttphBin
HttpBin
}
pub type HttpUri {
AbsPath(BitString)
}
pub type HttpPacket {
HttpRequest(Dynamic, HttpUri, #(Int, Int))
HttpHeader(Int, Atom, BitString, BitString)
}
pub type DecodedPacket {
BinaryData(HttpPacket, BitString)
EndOfHeaders(BitString)
MoreData(Option(Int))
}
pub type DecodeError {
MalformedRequest
InvalidMethod
InvalidPath
UnknownHeader
UnknownMethod
// TODO: better name?
InvalidBody
DiscardPacket
}
external fn decode_packet(
packet_type: PacketType,
packet: BitString,
options: List(a),
) -> Result(DecodedPacket, DecodeError) =
"mist_ffi" "decode_packet"
pub fn from_header(value: BitString) -> String {
let assert Ok(value) = bit_string.to_string(value)
string.lowercase(value)
}
pub type Buffer {
Buffer(remaining: Int, data: BitString)
}
pub fn parse_headers(
bs: BitString,
socket: Socket,
transport: Transport,
headers: Map(String, String),
) -> Result(#(Map(String, String), BitString), DecodeError) {
case decode_packet(HttphBin, bs, []) {
Ok(BinaryData(HttpHeader(_, _field, field, value), rest)) -> {
let field = from_header(field)
let assert Ok(value) = bit_string.to_string(value)
headers
|> map.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.then(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(BitString, 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.then(
socket
|> transport.receive_timeout(to_read, timeout)
|> result.replace_error(error),
)
let next_buffer =
Buffer(
remaining: buffer.remaining - to_read,
data: <<buffer.data:bit_string, data:bit_string>>,
)
case next_buffer.remaining > 0 {
True -> read_data(socket, transport, next_buffer, error)
False -> Ok(next_buffer.data)
}
}
external fn binary_match(
source: BitString,
pattern: BitString,
) -> Result(#(Int, Int), Nil) =
"mist_ffi" "binary_match"
external fn string_to_int(string: Charlist, base: Int) -> Result(Int, Nil) =
"mist_ffi" "string_to_int"
const crnl = <<13:int, 10:int>>
fn read_chunk(
socket: Socket,
transport: Transport,
buffer: Buffer,
body: BitBuilder,
) -> Result(BitBuilder, DecodeError) {
case buffer.data, binary_match(buffer.data, crnl) {
_, Ok(#(offset, _)) -> {
let assert <<
chunk:binary-size(offset),
_return:int,
_newline:int,
rest:binary,
>> = buffer.data
use chunk_size <- result.then(
chunk
|> bit_string.to_string
|> result.map(charlist.from_string)
|> result.replace_error(InvalidBody),
)
use size <- result.then(
string_to_int(chunk_size, 16)
|> result.replace_error(InvalidBody),
)
case size {
0 -> Ok(body)
size ->
case rest {
<<next_chunk:binary-size(size), 13:int, 10:int, rest:binary>> ->
read_chunk(
socket,
transport,
Buffer(0, rest),
bit_builder.append(body, next_chunk),
)
_ -> {
use next <- result.then(read_data(
socket,
transport,
Buffer(0, buffer.data),
InvalidBody,
))
read_chunk(socket, transport, Buffer(0, next), body)
}
}
}
}
<<>>, _ -> {
use next <- result.then(read_data(
socket,
transport,
Buffer(0, buffer.data),
InvalidBody,
))
read_chunk(socket, transport, Buffer(0, next), body)
}
_, Error(Nil) -> Error(InvalidBody)
}
}
/// Turns the TCP message into an HTTP request
pub fn parse_request(
bs: BitString,
socket: Socket,
transport: Transport,
) -> Result(request.Request(Body), DecodeError) {
case decode_packet(HttpBin, bs, []) {
Ok(BinaryData(HttpRequest(http_method, AbsPath(path), _version), rest)) -> {
use method <- result.then(
http_method
|> atom.from_dynamic
|> result.map(atom.to_string)
|> result.or(dynamic.string(http_method))
|> result.nil_error
|> result.then(http.parse_method)
|> result.replace_error(UnknownMethod),
)
use #(headers, rest) <- result.then(parse_headers(
rest,
socket,
transport,
map.new(),
))
use path <- result.then(
path
|> bit_string.to_string
|> result.replace_error(InvalidPath),
)
use parsed <- result.then(
uri.parse(path)
|> result.replace_error(InvalidPath),
)
let #(path, query) = #(parsed.path, parsed.query)
let req =
request.new()
|> request.set_scheme(case transport {
transport.Ssl(..) -> http.Https
transport.Tcp(..) -> http.Http
})
|> request.set_body(Unread(rest, socket))
|> request.set_method(method)
|> request.set_path(path)
Ok(request.Request(..req, query: query, headers: map.to_list(headers)))
}
_ -> Error(DiscardPacket)
}
}
pub opaque type Body {
Unread(rest: BitString, socket: Socket)
Read(data: BitString)
}
/// This function exists purely for unit testing handlers. It allows you to
/// create a `Request(Body)` which is required by `handler_func`.
pub fn static_body(data: BitString) -> Body {
Read(data)
}
pub fn read_body(req: Request(Body)) -> Result(Request(BitString), DecodeError) {
let transport = case req.scheme {
http.Https -> transport.ssl()
http.Http -> transport.tcp()
}
case request.get_header(req, "transfer-encoding"), req.body {
Ok("chunked"), Unread(rest, socket) -> {
use chunk <- result.then(read_chunk(
socket,
transport,
Buffer(remaining: 0, data: rest),
bit_builder.new(),
))
Ok(request.set_body(req, bit_builder.to_bit_string(chunk)))
}
_, Unread(rest, socket) -> {
let _continue = case is_continue(req) {
True -> {
let assert Ok(Nil) =
response.new(100)
|> response.set_body(bit_builder.new())
|> encoder.to_bit_builder
|> transport.send(socket, _)
Nil
}
False -> Nil
}
let body_size =
req.headers
|> list.find(fn(tup) { pair.first(tup) == "content-length" })
|> result.map(pair.second)
|> result.then(int.parse)
|> result.unwrap(0)
let remaining = body_size - bit_string.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(socket, transport, Buffer(remaining, rest), InvalidBody)
}
|> result.map(request.set_body(req, _))
|> result.replace_error(InvalidBody)
}
_, Read(_data) -> Error(InvalidBody)
}
}
pub type HttpResponseBody {
BitBuilderBody(BitBuilder)
Chunked(Iterator(BitBuilder))
FileBody(
file_descriptor: file.FileDescriptor,
content_type: String,
offset: Int,
length: Int,
)
}
pub fn upgrade_socket(
req: Request(Body),
) -> Result(Response(BitBuilder), Request(Body)) {
use _upgrade <- result.then(
request.get_header(req, "upgrade")
|> result.replace_error(req),
)
use key <- result.then(
request.get_header(req, "sec-websocket-key")
|> result.replace_error(req),
)
use _version <- result.then(
request.get_header(req, "sec-websocket-version")
|> result.replace_error(req),
)
let accept_key = websocket.parse_key(key)
response.new(101)
|> response.set_body(bit_builder.new())
|> response.prepend_header("Upgrade", "websocket")
|> response.prepend_header("Connection", "Upgrade")
|> response.prepend_header("Sec-WebSocket-Accept", accept_key)
|> Ok
}
// TODO: improve this error type
pub fn upgrade(
socket: Socket,
transport: Transport,
req: Request(Body),
) -> Result(Nil, Nil) {
use resp <- result.then(
upgrade_socket(req)
|> result.nil_error,
)
use _sent <- result.then(
resp
|> add_default_headers
|> encoder.to_bit_builder
|> transport.send(socket, _)
|> result.nil_error,
)
Ok(Nil)
}
pub fn add_default_headers(resp: Response(BitBuilder)) -> Response(BitBuilder) {
let body_size = bit_builder.byte_size(resp.body)
let headers =
map.from_list([
#("content-length", int.to_string(body_size)),
#("connection", "keep-alive"),
])
|> list.fold(
resp.headers,
_,
fn(defaults, tup) {
let #(key, value) = tup
map.insert(defaults, key, value)
},
)
|> map.to_list
Response(..resp, headers: headers)
}
fn is_continue(req: Request(Body)) -> Bool {
req.headers
|> list.find(fn(tup) {
pair.first(tup) == "expect" && pair.second(tup) == "100-continue"
})
|> result.is_ok
}