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[^:/]+))? # optional protocol ((:)?//)? # optional separator (?P[^:/]+)? # required host (but can be omitted in parsing like /path/ or ./path) (:(?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) 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