import gleam/bit_array
import gleam/bool
import gleam/bytes_tree
import gleam/int
import gleam/io
import gleam/list
import gleam/option.{None}
import gleam/result
import gleam/string
import gleam/uri
import glisten
import glisten/socket
import mimetype
import simplifile
const resplength: Int = 65_536
pub fn senddata(
data: List(bytes_tree.BytesTree),
conn: glisten.Connection(a),
) -> Result(Nil, socket.SocketReason) {
let assert Ok(firstsegment) = list.first(data)
case list.length(data) {
1 -> {
glisten.send(conn, firstsegment)
}
_ -> {
let assert Ok(_) = glisten.send(conn, firstsegment)
senddata(list.drop(data, 1), conn)
}
}
}
//ensures URL has gemini scheme, + path segments from query, return Result(segments)
//ensure no userinfo portion of URI
//failure here is error 59?
pub fn parse_url(
url: BitArray,
boundport: Int,
hostname: String,
clientip: glisten.IpAddress,
) -> Result(String, ResponseCode) {
io.print(result.unwrap(bit_array.to_string(url), "unwrappable request"))
let checklength = fn(x: Result(String, Nil)) -> Result(String, ResponseCode) {
case x {
Error(_) -> Error(Status59)
Ok(a) -> {
assert string.contains(a, "\r\n")
let trimmed = string.trim(a)
case string.byte_size(trimmed) > 1024 {
True -> Error(Status59)
False -> Ok(trimmed)
}
}
}
}
let touri = fn(x: Result(String, ResponseCode)) -> Result(
uri.Uri,
ResponseCode,
) {
case x {
Error(a) -> Error(a)
Ok(a) -> {
uri.parse(a)
|> result.replace_error(Status59)
}
}
}
let checkscheme = fn(x: Result(uri.Uri, ResponseCode)) -> Result(
String,
ResponseCode,
) {
// also holy fuck what kind of function did i write here. it looks like a dalek lol
let validhost = fn(
hostname: String,
req: option.Option(String),
client_ip: glisten.IpAddress,
) -> Bool {
// req = from the tcp request (ie, the request header)
// host = domain specified in main
let reqdhostname = option.unwrap(req, "")
let client = glisten.ip_address_to_string(client_ip)
// first, see if the requested host is local
// if it is, then see if the
case checkiflocalip(client) {
True -> checkiflocalip(reqdhostname)
False -> hostname == reqdhostname
}
}
case x {
Error(a) -> Error(a)
Ok(a) -> {
case a {
// c: port option.Option, b: trailing path String
uri.Uri(option.Some("gemini"), None, d, c, b, _, _) -> {
case validhost(hostname, d, clientip) {
False -> Error(Status53)
True ->
case c {
option.None -> Ok(b)
option.Some(c) ->
case c == boundport {
True -> Ok(b)
False -> Error(Status53)
}
}
}
}
//check port, if given, is the same as the bound one
uri.Uri(option.None, _, _, _, _, _, _) -> Error(Status59)
_ -> Error(Status53)
}
}
}
}
let _spliturl =
bit_array.to_string(url)
|> checklength()
|> touri()
|> checkscheme()
}
fn checkiflocalip(host: String) -> Bool {
case host {
"::1" -> True
"localhost" -> True
"127.0.0.1" -> True
a -> {
let splitip = string.split(a, on: ".")
case list.length(splitip) {
4 -> {
let toint =
list.map(splitip, fn(a: String) -> Int {
let assert Ok(z) = int.parse(a)
z
})
case toint {
[10, _, _, _] -> True
[172, a, _, _] ->
case bool.and(a >= 16, a < 32) {
True -> True
False -> False
}
[192, 168, _, _] -> True
_ -> False
}
}
_ -> False
}
}
}
}
//let checkquerylen = fn(query: Result(String, Nil)) -> Result(
// String,
// ResponseCode,
//) {
// case query {
// Error(_) -> Error(Status59)
// Ok(a) -> {
// case string.length(a) > 1024 {
// True -> Error(Status59)
// False -> {
// Ok(string.trim(a))
// }
// }
// }
// }
//}
//
//let checkscheme = fn(val1: uri.Uri) -> Result(uri.Uri, ResponseCode) {
// case val1 {
// Error(a) -> Error(a)
// uri.Uri(option.Some("gemini"), None, _, option.Some(1965), _, _, _) ->
// Ok(val1)
// _ -> Error(Status53)
// }
//}
//
//let parsedurl =
// bit_array.to_string(url)
// |> checkquerylen()
//
//let spliturl =
// uri.parse()
// |> result.replace_error(Status59)
//
//let _ = case spliturl {
// Error(_) -> Error(Status59)
//
// Ok(uri.Uri(option.Some("gemini"), None, _, _, a, _, _)) -> Ok(a)
// _ -> Error(Status59)
//}
//finds .gmi or other resources in the capsule from path segments, returns Result(bitstream + MIMEtype)
//failure here is code 51
// checks for "/" or "" -> index.gmi
pub fn router(reqpath: Result(String, ResponseCode)) -> Response {
let index = "/index.gmi"
let rootpath =
result.map(reqpath, fn(a) {
string.remove_suffix(string.concat(["./capsule", a]), "/")
})
let path = case rootpath {
Ok(a) ->
case simplifile.is_directory(a) {
Ok(True) -> Ok(string.concat([a, index]))
Ok(False) -> Ok(a)
Error(a) -> Error(a)
}
|> result.replace_error(Status51)
Error(a) -> Error(a)
}
let validfile = case path {
Ok(a) -> {
simplifile.is_file(a)
|> result.replace_error(Status51)
}
Error(a) -> Error(a)
}
case validfile {
Error(Status59) -> Response("59", "59 - Bad request!", <<>>)
Error(Status51) -> Response("51", "51 - Page not found!", <<>>)
Error(Status53) -> Response("53", "53 - Proxy request denied!", <<>>)
Ok(False) -> Response("51 ", "51 - Page not found!", <<>>)
Ok(True) -> {
let filepath = result.unwrap(path, "")
let filedata =
simplifile.read_bits(filepath)
|> result.unwrap(<<>>)
let mime = case string.ends_with(filepath, ".gmi") {
True -> "text/gemini"
False ->
mimetype.to_string(mimetype.detect_with_filename(filedata, filepath))
}
Response("20", mime, filedata)
}
}
}
// if file exists, return status 20+mimetytpe+CLRF
// else, return status of failure
// appends the body to the header if there a success code, else just the header.
// returns bitstream
pub fn response(data: Response) -> List(bytes_tree.BytesTree) {
io.print("Response: " <> data.status <> " " <> data.meta <> "\r\n")
let respstring =
string.concat([data.status, " ", data.meta, "\r\n"])
|> bytes_tree.from_string()
[respstring, ..slicebody(data.body, [])]
//[respstring, bytes_tree.from_bit_array(data.body)]
}
//idk seems like when im trying to serve larger files sometimes the connection just
//craps and times out or something?
fn slicebody(
data: BitArray,
segments: List(bytes_tree.BytesTree),
) -> List(bytes_tree.BytesTree) {
let length = bit_array.byte_size(data)
let getslice = fn(a: BitArray, len: Int, slicelen: Int) -> BitArray {
bit_array.slice(a, len, slicelen)
|> result.unwrap(<<>>)
}
case length {
0 -> segments
_ ->
case { length <= resplength } {
True -> [bytes_tree.from_bit_array(data), ..segments]
False ->
slicebody(getslice(data, 0, length - resplength), [
bytes_tree.from_bit_array(getslice(data, length, -resplength)),
..segments
])
}
}
}
pub fn bind(
actor: glisten.Builder(Nil, a),
loopy: Bool,
) -> glisten.Builder(Nil, a) {
case loopy {
False -> {
glisten.bind(actor, "0.0.0.0")
//|> glisten.bind("::")
}
True -> actor
}
}
//pub fn log(will_log: Bool, information: String) -> Nil {
// case will_log {
// True -> io.println(information)
// False -> Nil
// }
//}
pub type Response {
Response(status: String, meta: String, body: BitArray)
}
pub type ResponseCode {
// file not found
Status51
// error in request
Status59
// proxy request refused
Status53
}