180 lines
5.4 KiB
Julia
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
|
|
|