diff --git a/spec/std/http/request_spec.cr b/spec/std/http/request_spec.cr index 595d352d93bb..a4ebfbcb04d9 100644 --- a/spec/std/http/request_spec.cr +++ b/spec/std/http/request_spec.cr @@ -230,8 +230,7 @@ module HTTP end it "falls back to /" do - request = Request.new("GET", "/foo") - request.path = nil + request = Request.new("GET", "") request.path.should eq("/") end end diff --git a/spec/std/uri/uri_parser_spec.cr b/spec/std/uri/uri_parser_spec.cr deleted file mode 100644 index 12773eaf94ec..000000000000 --- a/spec/std/uri/uri_parser_spec.cr +++ /dev/null @@ -1,272 +0,0 @@ -require "spec" -require "uri" - -# Some specs are commented for now because struct inheritance is no longer -# supported, we have to figure out a way to make this testeable again. - -# struct URI::TestParser < URI::Parser -# property ptr - -# macro step(method) -# return :{{method}} -# end -# end - -# struct URI::VerboseParser < URI::Parser -# macro step(method) -# puts "moving to {{method}} at #{@ptr}: #{c.chr}" -# return {{method}} -# end -# end - -# def test_parser(url = "", ptr = 0) -# par = URI::TestParser.new(url) -# par.ptr = ptr -# par -# end - -# private macro test(parser_meth, url, start_ptr, end_ptr, next_meth, uri_meth = nil, expected = nil, file = __FILE__, line = __LINE__) -# it "{{parser_meth}} on \"#{{{url}}}\"", {{file}}, {{line}} do -# par = test_parser(url: {{url}}, ptr: {{start_ptr}}) -# par.{{parser_meth}}.should eq({{next_meth}}) -# par.ptr.should eq({{end_ptr}}) -# {% if uri_meth %} -# par.uri.{{uri_meth}}.should eq({{expected}}) -# {% end %} -# end -# end - -# describe URI::Parser, "steps" do -# test parse_scheme_start, -# "aurl", 0, 0, -# :parse_scheme - -# test parse_scheme_start, -# "1", 0, 0, -# :parse_no_scheme - -# test parse_scheme, -# "my-thing+yes.2://", 0, 16, -# :parse_path_or_authority, -# scheme, "my-thing+yes.2" - -# test parse_scheme, -# "mailto:foo", 0, 6, -# :nil, -# scheme, "mailto" - -# test parse_scheme, -# "mailto:foo", 0, 6, -# :nil, -# opaque, "foo" - -# test parse_scheme, -# "/path/absolute/url", 0, 0, -# :parse_no_scheme, -# scheme, nil - -# test parse_path_or_authority, -# "http://bitfission.com", 6, 6, -# :parse_authority - -# test parse_path_or_authority, -# "test:/path", 6, 5, -# :parse_path - -# test parse_no_scheme, -# "#justfragment", 0, 0, -# :parse_fragment - -# test parse_no_scheme, -# "/justpath", 0, 0, -# :parse_relative - -# test parse_authority, -# "http://bitfission.com", 6, 7, -# :parse_host - -# test parse_authority, -# "http://user@bitfission.com", 6, 7, -# :parse_userinfo - -# test parse_authority, -# "http://user:pass@bitfission.com", 6, 7, -# :parse_userinfo - -# test parse_userinfo, -# "http://%3Auser@bitfission.com", 7, 15, -# :parse_host, -# user, ":user" - -# test parse_userinfo, -# "http://%3Auser:pass@bitfission.com", 7, 20, -# :parse_host, -# user, ":user" - -# test parse_userinfo, -# "http://user:%3Apass@bitfission.com", 7, 20, -# :parse_host, -# password, ":pass" - -# test parse_host, -# "http://bitfission.com", 7, 21, -# :parse_path, -# host, "bitfission.com" - -# test parse_host, -# "http://bitfission.com/", 7, 21, -# :parse_path, -# host, "bitfission.com" - -# test parse_host, -# "http://bitfission.com:8080/", 7, 22, -# :parse_port, -# host, "bitfission.com" - -# test parse_host, -# "http://bitfission.com/something", 7, 21, -# :parse_path, -# host, "bitfission.com" - -# test parse_host, -# "http://bitfission.com?foo=bar", 7, 21, -# :parse_path, -# host, "bitfission.com" - -# test parse_host, -# "http://bitfission.com#anchor", 7, 21, -# :parse_path, -# host, "bitfission.com" - -# test parse_host, -# "http://[::1]", 7, 12, -# :parse_path, -# host, "[::1]" - -# test parse_host, -# "file:///no/host", 7, 7, -# :parse_path, -# host, nil - -# test parse_port, -# "http://a.com:8080", 13, 17, -# :parse_path, -# port, 8080 - -# test parse_relative, -# "", 0, 0, -# :nil - -# test parse_relative, -# "/path", 0, 0, -# :parse_relative_slash - -# test parse_relative, -# "?query", 0, 0, -# :parse_query - -# test parse_relative, -# "?query", 0, 0, -# :parse_query - -# test parse_relative_slash, -# "//bitfission.com", 0, 1, -# :parse_authority - -# test parse_relative_slash, -# "/a/path", 0, 0, -# :parse_path - -# test parse_path, -# "/somepath", 0, 9, -# :nil, -# path, "/somepath" - -# test parse_path, -# "/somepath?foo=yes", 0, 9, -# :parse_query, -# path, "/somepath" - -# test parse_path, -# "/somepath#foo", 0, 9, -# :parse_fragment, -# path, "/somepath" - -# test parse_query, -# "?a=b&c=d", 0, 8, -# :nil, -# query, "a=b&c=d" - -# test parse_query, -# "?a=b&c=d#frag", 0, 8, -# :parse_fragment, -# query, "a=b&c=d" - -# test parse_fragment, -# "#frag", 0, 5, -# :nil, -# fragment, "frag" -# end - -describe URI::Parser, "#run" do - it "runs for normal urls" do - uri = URI::Parser.new("http://user:pass@bitfission.com:8080/path?a=b#frag").run.uri - uri.scheme.should eq("http") - uri.user.should eq("user") - uri.password.should eq("pass") - uri.host.should eq("bitfission.com") - uri.port.should eq(8080) - uri.path.should eq("/path") - uri.query.should eq("a=b") - uri.fragment.should eq("frag") - end - - it "runs for schemelss urls" do - uri = URI::Parser.new("//user:pass@bitfission.com:8080/path?a=b#frag").run.uri - uri.scheme.should eq(nil) - uri.user.should eq("user") - uri.password.should eq("pass") - uri.host.should eq("bitfission.com") - uri.port.should eq(8080) - uri.path.should eq("/path") - uri.query.should eq("a=b") - uri.fragment.should eq("frag") - end - - it "runs for path relative urls" do - uri = URI::Parser.new("/path?a=b#frag").run.uri - uri.scheme.should eq(nil) - uri.host.should eq(nil) - uri.path.should eq("/path") - uri.query.should eq("a=b") - uri.fragment.should eq("frag") - end - - it "runs for path mailto" do - uri = URI::Parser.new("mailto:user@example.com").run.uri - uri.scheme.should eq("mailto") - uri.opaque.should eq("user@example.com") - end - - it "runs for file wth and without host" do - uri = URI::Parser.new("file://localhost/etc/fstab").run.uri - uri.scheme.should eq("file") - uri.host.should eq("localhost") - uri.path.should eq("/etc/fstab") - - uri = URI::Parser.new("file:///etc/fstab").run.uri - uri.scheme.should eq("file") - uri.host.should eq(nil) - uri.path.should eq("/etc/fstab") - end - - it "runs for scheme and path only urls" do - uri = URI::Parser.new("test:/test").run.uri - uri.scheme.should eq("test") - uri.path.should eq("/test") - end - - context "bad urls" do - it { expect_raises(URI::Error) { URI::Parser.new("http://some.com:8f80/path").run } } - end -end diff --git a/spec/std/uri_spec.cr b/spec/std/uri_spec.cr index ecd92064727f..95ecba4d7c42 100644 --- a/spec/std/uri_spec.cr +++ b/spec/std/uri_spec.cr @@ -1,51 +1,143 @@ require "spec" require "uri" -private def assert_uri(string, scheme = nil, host = nil, port = nil, path = "", query = nil, user = nil, password = nil, fragment = nil, opaque = nil) - it "parse #{string}" do - uri = URI.parse(string) - uri.scheme.should eq(scheme) - uri.host.should eq(host) - uri.port.should eq(port) - uri.path.should eq(path) - uri.query.should eq(query) - uri.user.should eq(user) - uri.password.should eq(password) - uri.fragment.should eq(fragment) - uri.opaque.should eq(opaque) +private def assert_uri(string, file = __FILE__, line = __LINE__, **args) + it "`#{string}`", file, line do + URI.parse(string).should eq URI.new(**args) + URI.parse(string).to_s.should eq string end end describe "URI" do - assert_uri("http://www.example.com", scheme: "http", host: "www.example.com") - assert_uri("http://www.example.com:81", scheme: "http", host: "www.example.com", port: 81) - assert_uri("http://[::1]:81", scheme: "http", host: "[::1]", port: 81) - assert_uri("http://www.example.com/foo", scheme: "http", host: "www.example.com", path: "/foo") - assert_uri("http://www.example.com/foo?q=1", scheme: "http", host: "www.example.com", path: "/foo", query: "q=1") - assert_uri("http://www.example.com?q=1", scheme: "http", host: "www.example.com", query: "q=1") - assert_uri("https://www.example.com", scheme: "https", host: "www.example.com") - assert_uri("https://alice:pa55w0rd@www.example.com", scheme: "https", host: "www.example.com", user: "alice", password: "pa55w0rd") - assert_uri("https://alice@www.example.com", scheme: "https", host: "www.example.com", user: "alice", password: nil) - assert_uri("https://%3AD:%40_%40@www.example.com", scheme: "https", host: "www.example.com", user: ":D", password: "@_@") - assert_uri("https://www.example.com/#top", scheme: "https", host: "www.example.com", path: "/", fragment: "top") - assert_uri("http://www.foo-bar.example.com", scheme: "http", host: "www.foo-bar.example.com") - assert_uri("/foo", path: "/foo") - assert_uri("/foo?q=1", path: "/foo", query: "q=1") - assert_uri("mailto:foo@example.org", scheme: "mailto", path: nil, opaque: "foo@example.org") + describe ".parse" do + # scheme + assert_uri("http:", scheme: "http") + it { URI.parse("HttP:").should eq(URI.new(scheme: "http")) } + + # host + assert_uri("http://www.example.com", scheme: "http", host: "www.example.com") + assert_uri("http://www.foo-bar.example.com", scheme: "http", host: "www.foo-bar.example.com") + assert_uri("http://www.example.com:81", scheme: "http", host: "www.example.com", port: 81) + assert_uri("http://[::1]:81", scheme: "http", host: "[::1]", port: 81) + assert_uri("http://192.0.2.16:81", scheme: "http", host: "192.0.2.16", port: 81) + it { URI.parse("http://[fe80::1%en0]:8080/").should eq(URI.new(scheme: "http", host: "[fe80::1%en0]", port: 8080, path: "/")) } + assert_uri("http://[fe80::1%25en0]:8080/", scheme: "http", host: "[fe80::1%en0]", port: 8080, path: "/") + assert_uri("mysql://a,b,c/bar", scheme: "mysql", host: "a,b,c", path: "/bar") + assert_uri("scheme://!$&'()*+,;=hello!:12/path", scheme: "scheme", host: "!$&'()*+,;=hello!", port: 12, path: "/path") + it { URI.parse("http://hello.世界.com").should eq(URI.new(scheme: "http", host: "hello.世界.com")) } + assert_uri("tcp://[2020::2020:20:2020:2020%25Windows%20Loves%20Spaces]:2020", scheme: "tcp", host: "[2020::2020:20:2020:2020%Windows Loves Spaces]", port: 2020) + + # host with trailing slash + assert_uri("http://www.example.com/", scheme: "http", host: "www.example.com", path: "/") + assert_uri("http://www.example.com:81/", scheme: "http", host: "www.example.com", port: 81, path: "/") + assert_uri("http://[::1]:81/", scheme: "http", host: "[::1]", port: 81, path: "/") + assert_uri("http://192.0.2.16:81/", scheme: "http", host: "192.0.2.16", port: 81, path: "/") + + # port + it { URI.parse("http://192.168.0.2:/foo").should eq URI.new(scheme: "http", host: "192.168.0.2", path: "/foo") } + + # path + assert_uri("http://www.example.com/foo", scheme: "http", host: "www.example.com", path: "/foo") + assert_uri("http:.", scheme: "http", path: ".") + assert_uri("http:..", scheme: "http", path: "..") + assert_uri("http://host/!$&'()*+,;=:@[hello]", scheme: "http", host: "host", path: "/!$&'()*+,;=:@[hello]") + assert_uri("http://example.com//foo", scheme: "http", host: "example.com", path: "//foo") + assert_uri("///foo", host: "", path: "/foo") + + pending "path with escape" do + assert_uri("http://www.example.com/file%20one%26two", scheme: "http", host: "example.com", path: "/file one&two", raw_path: "/file%20one%26two") + end + + # query + assert_uri("http://www.example.com/foo?q=1", scheme: "http", host: "www.example.com", path: "/foo", query: "q=1") + assert_uri("http://www.example.com/foo?", scheme: "http", host: "www.example.com", path: "/foo", query: "") + assert_uri("?q=1", query: "q=1") + assert_uri("?q=1?", query: "q=1?") + assert_uri("?a+b=c%2Bd", query: "a+b=c%2Bd") + assert_uri("?query=http://example.com", query: "query=http://example.com") + + # userinfo + assert_uri("https://alice:pa55w0rd@www.example.com", scheme: "https", host: "www.example.com", user: "alice", password: "pa55w0rd") + assert_uri("https://alice@www.example.com", scheme: "https", host: "www.example.com", user: "alice", password: nil) + assert_uri("https://alice:@www.example.com", scheme: "https", host: "www.example.com", user: "alice", password: "") + assert_uri("https://%3AD:%40_%40@www.example.com", scheme: "https", host: "www.example.com", user: ":D", password: "@_@") + + pending "unescaped @ in user/password should not confuse host" do + assert_uri("http://j@ne:password@example.com", scheme: "http", host: "example.com", user: "j@ne", password: "password") + assert_uri("http://jane:p@ssword@example.com", scheme: "http", host: "example.com", user: "jane", password: "p@ssword") + end + + # fragment + assert_uri("https://www.example.com/#top", scheme: "https", host: "www.example.com", path: "/", fragment: "top") + + # relative URL + assert_uri("/foo", path: "/foo") + assert_uri("/foo?q=1", path: "/foo", query: "q=1") + assert_uri("//foo", host: "foo") + assert_uri("//user@foo/path?q=b", host: "foo", user: "user", path: "/path", query: "q=b") + + # various schemes + assert_uri("mailto:foo@example.org", scheme: "mailto", path: "foo@example.org") + assert_uri("news:comp.infosystems.www.servers.unix", scheme: "news", path: "comp.infosystems.www.servers.unix") + assert_uri("tel:+1-816-555-1212", scheme: "tel", path: "+1-816-555-1212") + assert_uri("urn:oasis:names:specification:docbook:dtd:xml:4.1.2", scheme: "urn", path: "oasis:names:specification:docbook:dtd:xml:4.1.2") + assert_uri("telnet://192.0.2.16:80/", scheme: "telnet", host: "192.0.2.16", port: 80, path: "/") + assert_uri("ldap://[2001:db8::7]/c=GB?objectClass?one", scheme: "ldap", host: "[2001:db8::7]", path: "/c=GB", query: "objectClass?one") + assert_uri("magnet:?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a&dn", scheme: "magnet", query: "xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a&dn") + + # opaque + assert_uri("http:example.com/?q=foo", scheme: "http", path: "example.com/", query: "q=foo") + + # no hierarchical part + assert_uri("http:", scheme: "http") + assert_uri("http:?", scheme: "http", query: "") + assert_uri("http:?#", scheme: "http", query: "", fragment: "") + assert_uri("http:#", scheme: "http", fragment: "") + assert_uri("http://", scheme: "http", host: "") + assert_uri("http://?", scheme: "http", host: "", query: "") + assert_uri("http://?#", scheme: "http", host: "", query: "", fragment: "") + assert_uri("http://#", scheme: "http", host: "", fragment: "") + + # empty host, but port + assert_uri("http://:8000", scheme: "http", host: "", port: 8000) + assert_uri("http://:8000/foo", scheme: "http", host: "", port: 8000, path: "/foo") + + # empty host, but user + assert_uri("http://user@", scheme: "http", host: "", user: "user") + assert_uri("http://user@/foo", scheme: "http", host: "", user: "user", path: "/foo") + + # path with illegal characters + assert_uri("foo/another@url/[]and{}", path: "foo/another@url/[]and{}") + + # complex examples + assert_uri("http://user:pass@bitfission.com:8080/path?a=b#frag", + scheme: "http", user: "user", password: "pass", host: "bitfission.com", port: 8080, path: "/path", query: "a=b", fragment: "frag") + assert_uri("//user:pass@bitfission.com:8080/path?a=b#frag", + user: "user", password: "pass", host: "bitfission.com", port: 8080, path: "/path", query: "a=b", fragment: "frag") + assert_uri("/path?a=b#frag", path: "/path", query: "a=b", fragment: "frag") + assert_uri("file://localhost/etc/fstab", scheme: "file", host: "localhost", path: "/etc/fstab") + assert_uri("file:///etc/fstab", scheme: "file", host: "", path: "/etc/fstab") + assert_uri("file:///C:/FooBar/Baz.txt", scheme: "file", host: "", path: "/C:/FooBar/Baz.txt") + assert_uri("test:/test", scheme: "test", path: "/test") + + context "bad urls" do + it { expect_raises(URI::Error) { URI.parse("http://some.com:8f80/path") } } + end + end describe "hostname" do - it { URI.parse("http://www.example.com/foo").hostname.should eq("www.example.com") } - it { URI.parse("http://[::1]/foo").hostname.should eq("::1") } - it { URI.parse("/foo").hostname.should be_nil } + it { URI.new("http", "www.example.com", path: "/foo").hostname.should eq("www.example.com") } + it { URI.new("http", "[::1]", path: "foo").hostname.should eq("::1") } + it { URI.new(path: "/foo").hostname.should be_nil } end describe "full_path" do - it { URI.parse("http://www.example.com/foo").full_path.should eq("/foo") } - it { URI.parse("http://www.example.com").full_path.should eq("/") } - it { URI.parse("http://www.example.com/foo?q=1").full_path.should eq("/foo?q=1") } - it { URI.parse("http://www.example.com/?q=1").full_path.should eq("/?q=1") } - it { URI.parse("http://www.example.com?q=1").full_path.should eq("/?q=1") } - it { URI.parse("http://test.dev/a%3Ab").full_path.should eq("/a%3Ab") } + it { URI.new(path: "/foo").full_path.should eq("/foo") } + it { URI.new.full_path.should eq("/") } + it { URI.new(path: "/foo", query: "q=1").full_path.should eq("/foo?q=1") } + it { URI.new(path: "/", query: "q=1").full_path.should eq("/?q=1") } + it { URI.new(query: "q=1").full_path.should eq("/?q=1") } + it { URI.new(path: "/a%3Ab").full_path.should eq("/a%3Ab") } it "does not add '?' to the end if the query params are empty" do uri = URI.parse("http://www.example.com/foo") @@ -93,6 +185,15 @@ describe "URI" do uri.path.should eq(expected), "failed to remove dot notation from #{input}" end end + + it "removes default port" do + URI.new("http", "www.example.com", 80).normalize.to_s.should eq("http://www.example.com") + URI.new("https", "www.example.com", 443).normalize.to_s.should eq("https://www.example.com") + URI.new("ftp", "www.example.com", 21).normalize.to_s.should eq("ftp://www.example.com") + URI.new("sftp", "www.example.com", 22).normalize.to_s.should eq("sftp://www.example.com") + URI.new("ldap", "www.example.com", 389).normalize.to_s.should eq("ldap://www.example.com") + URI.new("ldaps", "www.example.com", 636).normalize.to_s.should eq("ldaps://www.example.com") + end end it "implements ==" do @@ -111,35 +212,21 @@ describe "URI" do describe "to_s" do it { URI.new("http", "www.example.com").to_s.should eq("http://www.example.com") } - it { URI.new("http", "www.example.com", 80).to_s.should eq("http://www.example.com") } - it do - u = URI.new("http", "www.example.com") - u.user = "alice" - u.to_s.should eq("http://alice@www.example.com") - u.password = "s3cr3t" - u.to_s.should eq("http://alice:s3cr3t@www.example.com") - end - it do - u = URI.new("http", "www.example.com") - u.user = ":D" - u.to_s.should eq("http://%3AD@www.example.com") - u.password = "@_@" - u.to_s.should eq("http://%3AD:%40_%40@www.example.com") - end + it { URI.new("http", "www.example.com", 80).to_s.should eq("http://www.example.com:80") } + it { URI.new("http", "www.example.com", user: "alice").to_s.should eq("http://alice@www.example.com") } + it { URI.new("http", "www.example.com", user: "alice", password: "s3cr3t").to_s.should eq("http://alice:s3cr3t@www.example.com") } + it { URI.new("http", "www.example.com", user: ":D").to_s.should eq("http://%3AD@www.example.com") } + it { URI.new("http", "www.example.com", user: ":D", password: "@_@").to_s.should eq("http://%3AD:%40_%40@www.example.com") } it { URI.new("http", "www.example.com", user: "@al:ce", password: "s/cr3t").to_s.should eq("http://%40al%3Ace:s%2Fcr3t@www.example.com") } it { URI.new("http", "www.example.com", fragment: "top").to_s.should eq("http://www.example.com#top") } - it { URI.new("http", "www.example.com", 80, "/hello").to_s.should eq("http://www.example.com/hello") } - it { URI.new("http", "www.example.com", 80, "/hello", "a=1").to_s.should eq("http://www.example.com/hello?a=1") } - it { URI.new("mailto", opaque: "foo@example.com").to_s.should eq("mailto:foo@example.com") } - - it "removes default port" do - URI.new("http", "www.example.com", 80).to_s.should eq("http://www.example.com") - URI.new("https", "www.example.com", 443).to_s.should eq("https://www.example.com") - URI.new("ftp", "www.example.com", 21).to_s.should eq("ftp://www.example.com") - URI.new("sftp", "www.example.com", 22).to_s.should eq("sftp://www.example.com") - URI.new("ldap", "www.example.com", 389).to_s.should eq("ldap://www.example.com") - URI.new("ldaps", "www.example.com", 636).to_s.should eq("ldaps://www.example.com") - end + it { URI.new("http", "www.example.com", 80, "/hello").to_s.should eq("http://www.example.com:80/hello") } + it { URI.new("http", "www.example.com", 80, "/hello", "a=1").to_s.should eq("http://www.example.com:80/hello?a=1") } + it { URI.new("mailto", path: "foo@example.com").to_s.should eq("mailto:foo@example.com") } + it { URI.new("file", path: "/foo.html").to_s.should eq("file:/foo.html") } + it { URI.new("file", path: "foo.html").to_s.should eq("file:foo.html") } + it { URI.new("file", host: "host", path: "foo.html").to_s.should eq("file://host/foo.html") } + it { URI.new(path: "//foo").to_s.should eq("/.//foo") } + it { URI.new(host: "host", path: "//foo").to_s.should eq("//host//foo") } it "preserves non-default port" do URI.new("http", "www.example.com", 1234).to_s.should eq("http://www.example.com:1234") @@ -156,10 +243,19 @@ describe "URI" do end it "preserves port for nil scheme" do - URI.new(nil, "www.example.com", 1234).to_s.should eq("www.example.com:1234") + URI.new(nil, "www.example.com", 1234).to_s.should eq("//www.example.com:1234") end end + it "#opaque?" do + URI.new.opaque?.should be_false + URI.new("foo").opaque?.should be_true + URI.new("foo", "example.com").opaque?.should be_false + URI.new("foo", "").opaque?.should be_false + URI.new("foo", path: "foo").opaque?.should be_true + URI.new("foo", path: "/foo").opaque?.should be_false + end + describe ".default_port" do it "returns default port for well known schemes" do URI.default_port("http").should eq(80) diff --git a/src/http/request.cr b/src/http/request.cr index 8a7ee7cee8d9..2d9dec6ca98a 100644 --- a/src/http/request.cr +++ b/src/http/request.cr @@ -106,9 +106,9 @@ class HTTP::Request BadRequest.new end - # Lazily parses and return the request's path component. + # Returns the request's path component. def path - uri.path || "/" + (path = uri.path).empty? ? "/" : path end # Sets request's path component. diff --git a/src/oauth/consumer.cr b/src/oauth/consumer.cr index 4eea9f5879a6..ab918cf67258 100644 --- a/src/oauth/consumer.cr +++ b/src/oauth/consumer.cr @@ -60,7 +60,7 @@ class OAuth::Consumer # If they are relative, the given *host*, *port* and *scheme* will be used. # If they are absolute, the absolute URL will be used. def initialize(@host : String, @consumer_key : String, @consumer_secret : String, - @port : Int32 = 443, + @port : Int32? = nil, @scheme : String = "https", @request_token_uri : String = "/oauth/request_token", @authorize_uri : String = "/oauth/authorize", diff --git a/src/oauth2/client.cr b/src/oauth2/client.cr index 2e2987203d02..2531bad76d80 100644 --- a/src/oauth2/client.cr +++ b/src/oauth2/client.cr @@ -61,7 +61,7 @@ class OAuth2::Client # If they are relative, the given *host*, *port* and *scheme* will be used. # If they are absolute, the absolute URL will be used. def initialize(@host : String, @client_id : String, @client_secret : String, - @port = 443, + @port : Int32? = nil, @scheme = "https", @authorize_uri = "/oauth2/authorize", @token_uri = "/oauth2/token", diff --git a/src/socket/address.cr b/src/socket/address.cr index 3663f3b1ff32..77b5918170c5 100644 --- a/src/socket/address.cr +++ b/src/socket/address.cr @@ -127,7 +127,8 @@ class Socket # Socket::IPAddress.parse("udp://[::1]:8080") # => Socket::IPAddress.new("::1", 8080) # ``` def self.parse(uri : URI) : IPAddress - host = uri.host || raise Socket::Error.new("Invalid IP address: missing host") + host = uri.host + raise Socket::Error.new("Invalid IP address: missing host") if !host || host.empty? port = uri.port || raise Socket::Error.new("Invalid IP address: missing port") diff --git a/src/uri.cr b/src/uri.cr index 95144b2de661..af8bb4d6363a 100644 --- a/src/uri.cr +++ b/src/uri.cr @@ -67,10 +67,10 @@ class URI # # URI.parse("http://foo.com/bar").path # => "/bar" # ``` - getter path : String? + getter path : String # Sets the path component of the URI. - setter path : String? + setter path : String # Returns the query component of the URI. # @@ -120,21 +120,9 @@ class URI # Sets the fragment component of the URI. setter fragment : String? - # Returns the opaque component of the URI. - # - # ``` - # require "uri" - # - # URI.parse("mailto:alice@example.com").opaque # => "alice@example.com" - # ``` - getter opaque : String? + def_equals_and_hash scheme, host, port, path, query, user, password, fragment - # Sets the opaque component of the URI. - setter opaque : String? - - def_equals_and_hash scheme, host, port, path, query, user, password, fragment, opaque - - def initialize(@scheme = nil, @host = nil, @port = nil, @path = nil, @query = nil, @user = nil, @password = nil, @fragment = nil, @opaque = nil) + def initialize(@scheme = nil, @host = nil, @port = nil, @path = "", @query = nil, @user = nil, @password = nil, @fragment = nil) end # Returns the host part of the URI and unwrap brackets for IPv6 addresses. @@ -157,9 +145,9 @@ class URI # uri = URI.parse "http://foo.com/posts?id=30&limit=5#time=1305298413" # uri.full_path # => "/posts?id=30&limit=5" # ``` - def full_path + def full_path : String String.build do |str| - str << (@path.try { |p| !p.empty? } ? @path : '/') + str << (@path.empty? ? '/' : @path) if (query = @query) && !query.empty? str << '?' << query end @@ -167,39 +155,53 @@ class URI end # Returns `true` if URI has a *scheme* specified. - def absolute? + def absolute? : Bool @scheme ? true : false end # Returns `true` if URI does not have a *scheme* specified. - def relative? + def relative? : Bool !absolute? end + # Returns `true` if this URI is opaque. + # + # A URI is considered opaque if it has a `scheme` but no hierachical part, + # i.e. no `host` and the first character of `path` is not a slash (`/`). + def opaque? : Bool + !@scheme.nil? && @host.nil? && !@path.starts_with?('/') + end + def to_s(io : IO) : Nil if scheme io << scheme io << ':' - io << "//" unless opaque - end - if opaque - io << opaque - return end + + authority = @user || @host || @port + io << "//" if authority if user = @user userinfo(user, io) io << '@' end - if host - io << host + if host = @host + URI.escape(host, io) do |byte| + URI.unreserved?(byte) || URI.reserved?(byte) + end end - unless port.nil? || default_port? - io << ':' - io << port + if port = @port + io << ':' << port end - if path - io << path + + if authority + if !@path.empty? && !@path.starts_with?('/') + io << '/' + end + elsif @path.starts_with?("//") + io << "/." end + io << @path + if query io << '?' io << query @@ -211,15 +213,16 @@ class URI end # Returns normalized URI. - def normalize + def normalize : self uri = dup uri.normalize! uri end # Destructive normalize. - def normalize! + def normalize! : Nil @path = remove_dot_segments(path) + @port = nil if default_port? end # Parses `raw_url` into an URI. The `raw_url` may be relative or absolute. @@ -422,9 +425,7 @@ class URI end # [RFC 3986 6.2.2.3](https://tools.ietf.org/html/rfc3986#section-5.2.4) - private def remove_dot_segments(path : String?) - return if path.nil? - + private def remove_dot_segments(path : String) result = [] of String while path.size > 0 # A. If the input buffer begins with a prefix of "../" or "./", diff --git a/src/uri/uri_parser.cr b/src/uri/uri_parser.cr index 6d1aeaff0fd1..1f75e940fe39 100644 --- a/src/uri/uri_parser.cr +++ b/src/uri/uri_parser.cr @@ -10,11 +10,6 @@ class URI # ports greater than 2^16-1 are not errors property uri : URI - # overridden in specs to test step transitions - macro step(method) - return {{method}} - end - @input : UInt8* def initialize(input) @@ -32,71 +27,71 @@ class URI self end - def parse_scheme_start + private def parse_scheme_start if alpha? - step parse_scheme + parse_scheme else - step parse_no_scheme + parse_no_scheme end end - def parse_scheme + private def parse_scheme start = @ptr loop do if alpha? || numeric? || c === '-' || c === '.' || c === '+' @ptr += 1 elsif c === ':' - @uri.scheme = from_input(start) + @uri.scheme = from_input(start).downcase if @input[@ptr + 1] === '/' @ptr += 2 - step parse_path_or_authority + return parse_path_or_authority else - # greatly deviates from spec as described, but is correct behavior - @uri.opaque = String.new(@input + @ptr + 1) - step nil + @ptr += 1 + return parse_path end else @ptr = 0 - step parse_no_scheme + return parse_no_scheme end end end - def parse_path_or_authority + private def parse_path_or_authority if c === '/' - step parse_authority + @uri.host = "" + parse_authority else @ptr -= 1 - step parse_path + parse_path end end - def parse_no_scheme + private def parse_no_scheme case c when '#' - step parse_fragment + parse_fragment else - step parse_relative + parse_relative end end - def parse_authority + private def parse_authority @ptr += 1 start = @ptr loop do if c === '@' @ptr = start - step parse_userinfo + return parse_userinfo elsif end_of_host? @ptr = start - step parse_host + return parse_host else @ptr += 1 end end end - def parse_userinfo + private def parse_userinfo start = @ptr password_flag = false loop do @@ -107,7 +102,7 @@ class URI @uri.user = URI.unescape(from_input(start)) end @ptr += 1 - step parse_host + return parse_host elsif c === ':' @uri.user = URI.unescape(from_input(start)) password_flag = true @@ -119,18 +114,18 @@ class URI end end - def parse_host + private def parse_host start = @ptr bracket_flag = false - step parse_path if c === '/' + return parse_path if c === '/' loop do if c === ':' && !bracket_flag - @uri.host = from_input(start) + @uri.host = URI.unescape(from_input(start)) @ptr += 1 - step parse_port + return parse_port elsif end_of_host? - @uri.host = from_input(start) - step parse_path + @uri.host = URI.unescape(from_input(start)) + return parse_path else bracket_flag = true if c === '[' bracket_flag = false if c === ']' @@ -139,90 +134,93 @@ class URI end end - def parse_port + private def parse_port start = @ptr loop do if numeric? @ptr += 1 elsif end_of_host? - @uri.port = (start...@ptr).reduce(0) do |memo, i| - (memo * 10) + (@input[i] - '0'.ord) + unless start == @ptr + @uri.port = (start...@ptr).reduce(0) do |memo, i| + (memo * 10) + (@input[i] - '0'.ord) + end end - step parse_path + return parse_path else raise URI::Error.new("Invalid URI: bad port at character #{@ptr}") end end end - def parse_relative + private def parse_relative case c when '\0' - step nil + nil when '/' - step parse_relative_slash + parse_relative_slash when '?' - step parse_query + parse_query when '#' - step parse_fragment + parse_fragment else - step parse_path + parse_path end end - def parse_relative_slash + private def parse_relative_slash if @input[@ptr + 1] === '/' @ptr += 1 - step parse_authority + @uri.host ||= "" + parse_authority else - step parse_path + parse_path end end - def parse_path + private def parse_path start = @ptr loop do case c when '\0' @uri.path = from_input(start) - step nil + return nil when '?' @uri.path = from_input(start) - step parse_query + return parse_query when '#' @uri.path = from_input(start) - step parse_fragment + return parse_fragment else @ptr += 1 end end end - def parse_query + private def parse_query @ptr += 1 start = @ptr loop do case c when '\0' @uri.query = from_input(start) - step nil + return nil when '#' @uri.query = from_input(start) - step parse_fragment + return parse_fragment else @ptr += 1 end end end - def parse_fragment + private def parse_fragment @ptr += 1 start = @ptr loop do case c when '\0' @uri.fragment = from_input(start) - step nil + return nil else @ptr += 1 end