Gemenon.jl/src/types.jl

180 lines
5.4 KiB
Julia

struct Status
major::Char
minor::Char
meta::String
# FIXME: error codes
function Status(ma::Char, mi::Char, meta::String)
'1' <= ma <= '6' || throw("Invalid: first status out of range")
isnumeric(mi) || throw("Invalid: second status not a number")
if length(meta) > 1024
throw("Invalid")
end
new(ma, mi, meta)
end
end
Status(code::String, meta::String) = Status(code[1], code[2], meta)
Status(code::String) =
Status(code[1], code[2], length(code) > 2 ? String(strip(code[3:end])) : "")
Status(code::Int, meta::String) = Status(string(code), meta)
Status(code::Int) = Status(string(code))
struct Response
status::Status
body::String
end
Response(s::Status, b::Vector{String}) = Response(s, join(b, "\n"))
struct Request
protocol::Union{String,Nothing}
host::Union{String,Nothing}
port::Union{Int32,Nothing}
path::Union{Array{String},Nothing}
query::Union{String,Nothing}
valid::Bool
raw::Union{String,Vector{UInt8}}
"Assemble a Request object based on raw string, according to gemini specifications
(v0.14.2, July 2nd 2020)
[protocol://]host[:port][/path][?query]
Protocol: optional, default gemini://
Port: optional, default 1965
Host: required
Path: optional
Query: optional
spaces encoded with: %20"
end
# TODO: https://tools.ietf.org/html/rfc3986#section-5.2.4
# Read specs and fix implementation!
function Request(data::String)
reg = r"""
((?P<protocol>[^:/]+))? # optional protocol
((:)?//)? # optional separator
(?P<host>[^:/]+)? # required host (but can be omitted in parsing like /path/ or ./path)
(:(?P<port>[0-9]{1,5}))? # optional port (will match impossible 65000+ ports)
(/(?P<path>[^\?]*))? # optional path
(\?(?P<query>.+))? # optional query
"""x
s = endswith(data, "\r\n") ? data[1:end-2] : data
m = match(reg, s)
isnothing(m) &&
return Request(nothing, nothing, nothing, nothing, nothing, false, data)
Request(something(m["protocol"], "gemini"),
m["host"], parse(Int, something(m["port"], "1965")),
# normpath prevents "./path" being different from "path", and
# resolves also "../". The only problem is that "./" is
# transformed to ".", so we filter it. the raw rapresentation
# is still there, if needed
isnothing(m["path"]) ? nothing : filter!(!=("."), split(normpath(m["path"]), "/")),
isnothing(m["query"]) ? nothing : unescape(m["query"]),
true, data)
end
function Request(protocol, host, port, path, query)
Request(
something(protocol, "gemini"),
something(host, "."),
something(port, 1965),
path, query, true, "")
end
unescape(::Nothing) = ""
"unescape(percent_encoded_uri)::String
Replace %xx escaped chars with their single-character equivalent.
Should never throw errors"
function unescape(str)::String
occursin("%", str) || return str
len = length(str)
# Maximum length is the input string length.
# Should be len - 3 (since we already know there's at least one percent sign)
# but what if the percent is at the end? We can save only one char
# as "string%" -> saves 1, "string%2" saves 2. But %-- occupies all three
unescaped = Vector{UInt8}(undef, len)
pos = 1
chars = 0
while pos <= len
let c = str[pos]
chars += 1
if c == '%' && pos+2 <= len &&
# Keep only alphanumeric chars
'0' <= str[pos+1] <= 'f' &&
'0' <= str[pos+2] <= 'f'
unescaped[chars] = parse(UInt8, str[pos+1:pos+2], base = 16)
pos += 3
else
unescaped[chars] = c
pos += 1
end
end
end
return String(unescaped[1:chars])
end
# FIXME: Is @ reserved?
# Are those the only one that are reserved?
const reserved_chars = ";/?:@&= " |> collect
isreserved(c::Char) = c in reserved_chars
function escape_uri(str)
len = length(str)
# If everything must be escaped, the string is three times longer
escaped = Vector{UInt8}(undef, len * 3)
pos = 1
for char in str
if isreserved(char)
escaped[pos:pos+2] = string('%', string(Int(char), base=16, pad=2)) |>
collect
pos += 3
else
escaped[pos] = char
pos += 1
end
end
String(escaped[1:pos-1])
end
import Base.show
function show(io::IO, r::Request)
# Useful for debugging
r.valid || begin
println(r.raw)
print(io, "[!invalid] ", String(r.raw))
return
end
printstyled(io, r.protocol, color = :red)
print(io, "://")
printstyled(io, r.host, color = :black, bold = :true)
print(io, ":")
printstyled(io, r.port, color = :blue)
if !isnothing(r.path)
print(io, "/")
printstyled(io, join(r.path, "/"), color = :green)
end
if !isnothing(r.query)
print(io, "?")
# Escape it unless we are in a "terminal"
query = typeof(io) == IOBuffer ? escape_uri(r.query) : r.query
printstyled(io, query, color = :yellow)
end
end
struct Connection
server::Sockets.TCPServer
client::SSLClient
end
struct GeminiRequest
conn::Connection
req::Request
data::Dict{Symbol,Any}
end