Skip to content

Commit

Permalink
Improve top-level URI interface + escaping. Implements dirk#7
Browse files Browse the repository at this point in the history
  • Loading branch information
quinnj committed Feb 11, 2017
1 parent 51b9fb8 commit 361c4b9
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 13 deletions.
8 changes: 4 additions & 4 deletions src/client.jl
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ are read, freeing up additional space to write.
"""
function request end

request(uri::String; verbose::Bool=false, args...) = (@log(verbose, STDOUT, "using default client"); request(DEFAULT_CLIENT, GET, URI(uri); verbose=verbose, args...))
request(uri::String; verbose::Bool=false, query="", args...) = (@log(verbose, STDOUT, "using default client"); request(DEFAULT_CLIENT, GET, URI(uri; query=query); verbose=verbose, args...))
request(uri::URI; verbose::Bool=false, args...) = (@log(verbose, STDOUT, "using default client"); request(DEFAULT_CLIENT, GET, uri; verbose=verbose, args...))
request(method, uri::String; verbose::Bool=false, args...) = (@log(verbose, STDOUT, "using default client"); request(DEFAULT_CLIENT, convert(Method, method), URI(uri); verbose=verbose, args...))
request(method, uri::String; verbose::Bool=false, query="", args...) = (@log(verbose, STDOUT, "using default client"); request(DEFAULT_CLIENT, convert(Method, method), URI(uri; query=query); verbose=verbose, args...))
request(method, uri::URI; verbose::Bool=false, args...) = (@log(verbose, STDOUT, "using default client"); request(DEFAULT_CLIENT, convert(Method, method), uri; verbose=verbose, args...))
function request(client::Client, method, uri::URI;
headers::Headers=Headers(),
Expand Down Expand Up @@ -373,9 +373,9 @@ for f in [:get, :post, :put, :delete, :head,
resp = t.result # get our response by getting the result of our asynchronous task
```
""" function $(f) end
($f)(uri::AbstractString; verbose::Bool=false, args...) = (@log(verbose, STDOUT, "using default client"); request(DEFAULT_CLIENT, $meth, URI(uri; isconnect=$(f_str == "CONNECT")); verbose=verbose, args...))
($f)(uri::AbstractString; verbose::Bool=false, query="", args...) = (@log(verbose, STDOUT, "using default client"); request(DEFAULT_CLIENT, $meth, URI(uri; query=query, isconnect=$(f_str == "CONNECT")); verbose=verbose, args...))
($f)(uri::URI; verbose::Bool=false, args...) = (@log(verbose, STDOUT, "using default client"); request(DEFAULT_CLIENT, $meth, uri; verbose=verbose, args...))
($f)(client::Client, uri::AbstractString; args...) = request(client, $meth, URI(uri; isconnect=$(f_str == "CONNECT")); args...)
($f)(client::Client, uri::AbstractString; query="", args...) = request(client, $meth, URI(uri; query=query, isconnect=$(f_str == "CONNECT")); args...)
($f)(client::Client, uri::URI; args...) = request(client, $meth, uri; args...)
end
end
42 changes: 34 additions & 8 deletions src/uri.jl
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,34 @@ function URI(;hostname::String="", path::String="",
scheme::String="", userinfo::String="",
port::Union{Integer,String}="", query="",
fragment::String="", isconnect::Bool=false)
# hostname might be full url
hostname != "" && scheme == "" && (scheme = "http")
hostname != "" && scheme == "" && !isconnect && (scheme = "http")
io = IOBuffer()
print(io, scheme, userinfo, hostname, string(port), path, isa(query, Dict) ? escape(query) : query, fragment)
print(io, scheme, userinfo, hostname, string(port), path, escape(query), fragment)
return Base.parse(URI, String(take!(io)); isconnect=isconnect)
end

URI(str::String; isconnect::Bool=false) = Base.parse(URI, str; isconnect=isconnect)
# we assume `str` is at least hostname & port
# if all others keywords are empty, assume CONNECT
# can include path, userinfo, query, & fragment
function URI(str::String; userinfo::String="", path::String="",
query="", fragment::String="",
isconnect::Bool=false)
if str != ""
if startswith(str, "http") || startswith(str, "https")
str = string(str, path, ifelse(query == "", "", "?" * escape(query)),
ifelse(fragment == "", "", "#$fragment"))
else
if path == "" && userinfo == "" && query == "" && fragment == "" && ':' in str
isconnect = true
else
str = string("http://", userinfo == "" ? "" : "$userinfo@",
str, path, ifelse(query == "", "", "?" * escape(query)),
ifelse(fragment == "", "", "#$fragment"))
end
end
end
return Base.parse(URI, str; isconnect=isconnect)
end
Base.parse(::Type{URI}, str::String; isconnect::Bool=false) = http_parser_parse_url(Vector{UInt8}(str), 1, sizeof(str), isconnect)

==(a::URI,b::URI) = scheme(a) == scheme(b) &&
Expand Down Expand Up @@ -100,15 +120,20 @@ function Base.isvalid(uri::URI)
end

lower(c::UInt8) = c | 0x20
ishostchar(c::UInt8) = (UInt8('a') <= lower(c) <= UInt8('z')) || UInt8('0') <= c <= UInt8('9') || @anyeq(c, UInt8('.'), UInt8('-'), UInt8('_'), UInt8('~'))

@inline shouldencode(c) = c < 0x1f || c > 0x7f || @anyeq(c,
UInt8(';'), UInt8('/'), UInt8('?'), UInt8(':'),
UInt8('@'), UInt8('='), UInt8('&'), UInt8(' '),
UInt8('"'), UInt8('<'), UInt8('>'), UInt8('#'),
UInt8('%'), UInt8('{'), UInt8('}'), UInt8('|'),
UInt8('\\'), UInt8('^'), UInt8('~'), UInt8('['),
UInt8(']'), UInt8('`'))
hexstring(x) = string('%', uppercase(hex(x,2)))

"percent-encode a uri/url string"
function escape(str)
function escape(str, f=shouldencode)
out = IOBuffer()
for c in Vector{UInt8}(str)
write(out, !ishostchar(c) ? hexstring(Int(c)) : c)
write(out, f(c) ? hexstring(Int(c)) : c)
end
return String(take!(out))
end
Expand All @@ -122,6 +147,7 @@ function escape(io, k, A::Vector{String})
end
end

escape(p::Pair) = escape(Dict(p))
function escape(d::Dict)
io = IOBuffer()
len = length(d)
Expand Down
2 changes: 2 additions & 0 deletions test/client.jl
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ for sch in ("http", "https")

@test HTTP.status(HTTP.get("$sch://httpbin.org/encoding/utf8")) == 200

@test HTTP.headers(HTTP.get("$sch://httpbin.org/response-headers"; query=Dict("hey"=>"dude")))["hey"] == "dude"

r = HTTP.get("$sch://httpbin.org/cookies")
body = string(r)
@test (body == "{\n \"cookies\": {}\n}\n" || body == "{\n \"cookies\": {\n \"hey\": \"\"\n }\n}\n" || body == "{\n \"cookies\": {\n \"hey\": \"sailor\"\n }\n}\n")
Expand Down
11 changes: 10 additions & 1 deletion test/uri.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ end
URLTest(nm::String, url::String, isconnect::Bool, shouldthrow::Bool) = URLTest(nm, url, isconnect, ntuple(x->HTTP.Offset(), 7), shouldthrow)

@testset "HTTP.URI" begin
# constructor
@test string(HTTP.URI("")) == ""
@test HTTP.URI("google.com") == HTTP.URI("http://google.com")
@test HTTP.URI("google.com/") == HTTP.URI("http://google.com/")
@test HTTP.URI("google.com/"; userinfo="user") == HTTP.URI("http://[email protected]/")
@test HTTP.URI("google.com/"; path="user") == HTTP.URI("http://google.com/user")
@test HTTP.URI("google.com/"; query=Dict("key"=>"value")) == HTTP.URI("http://google.com/?key=value")
@test HTTP.URI("google.com/"; fragment="user") == HTTP.URI("http://google.com/#user")

urls = [("hdfs://user:password@hdfshost:9000/root/folder/file.csv#frag", ["root", "folder", "file.csv"]),
("https://user:password@httphost:9000/path1/path2;paramstring?q=a&p=r#frag", ["path1", "path2;paramstring"]),
("https://user:password@httphost:9000/path1/path2?q=a&p=r#frag", ["path1","path2"]),
Expand Down Expand Up @@ -41,7 +50,7 @@ URLTest(nm::String, url::String, isconnect::Bool, shouldthrow::Bool) = URLTest(n

@test HTTP.escape(Dict("key1"=>"value1", "key2"=>["value2", "value3"])) == "key2=value2&key2=value3&key1=value1"

@test HTTP.escape("abcdef αβ 1234-=~!@#\$()_+{}|[]a;") == "abcdef%20%CE%B1%CE%B2%201234-%3D~%21%40%23%24%28%29_%2B%7B%7D%7C%5B%5Da%3B"
@test HTTP.escape("abcdef αβ 1234-=~!@#\$()_+{}|[]a;") == "abcdef%20%CE%B1%CE%B2%201234-%3D%7E!%40%23\$()_+%7B%7D%7C%5B%5Da%3B"
@test HTTP.unescape(HTTP.escape("abcdef 1234-=~!@#\$()_+{}|[]a;")) == "abcdef 1234-=~!@#\$()_+{}|[]a;"
@test HTTP.unescape(HTTP.escape("👽")) == "👽"

Expand Down

0 comments on commit 361c4b9

Please sign in to comment.