Skip to main content

src/starfruit/internal.gleam

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
}