struct Status major::Char minor::Char meta::String # FIXME: error codes function Status(ma::Char, mi::Char, meta::String) '1' <= ma <= '6' || throw("Invalid") '0' <= mi <= '9' || throw("Invalid") 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], "") 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 function Request(data::String) reg = r""" ((?P[^:/]+)://)? # optional protocol (?P[^:/]+) # required host (:(?P[0-9]{1,5}))? # optional port (will match impossible 65000+ ports) (/(?P[^\?]+))? # optional path (\?(?P[^\?]+))? # optional query """x s = endswith(data, "\r\n") ? data[1:end-2] : data m = match(reg, s) if isnothing(m) Request(nothing, nothing, nothing, nothing, nothing, false, data) else 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 end "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 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, "?") printstyled(io, r.query, color = :yellow) end end struct Connection server::Sockets.TCPServer client::SSLClient end