require "http"
require "http/client"
require "openssl"
module Fetch
private def self.fetch(
method : String,
url : String,
query : URI::Params? = URI::Params.new,
headers : HTTP::Headers? = HTTP::Headers.new,
body : String? = nil,
max_hops : Int32? = 10,
) : HTTP::Client::Response
context = OpenSSL::SSL::Context::Client.new
context.verify_mode = OpenSSL::SSL::VerifyMode::NONE
uri = URI.parse(url)
uri.query_params = query unless query.nil?
body = "" if body.nil?
headers_hash = HTTP::Headers.new if headers.nil?
headers_hash = headers.clone unless headers.nil?
hops = 0
max_hops = max_hops.nil? ? 10 : max_hops
loop do
raise "too many redirects" if hops >= (max_hops.to_i || 10)
hops += 1
begin
headers ||= HTTP::Headers.new
headers_hash ||= HTTP::Headers.new
headers_hash.each { |k, v| headers[k] = v }
headers["Host"] = uri.host.not_nil! unless uri.host.nil?
headers["Content-Type"] = "application/json" if method == "POST"
resp = case method.upcase
when "POST"
HTTP::Client.post(uri.to_s, headers: headers, body: body, tls: context)
when "PUT"
HTTP::Client.put(uri.to_s, headers: headers, body: body, tls: context)
when "DELETE"
HTTP::Client.delete(uri.to_s, headers: headers, tls: context)
when "OPTIONS"
HTTP::Client.options(uri.to_s, headers: headers, tls: context)
when "HEAD"
HTTP::Client.head(uri.to_s, headers: headers, tls: context)
else
HTTP::Client.get(uri.to_s, headers: headers, tls: context)
end
if resp && !(300..399).includes?(resp.status_code)
return resp
end
loc = resp.headers["Location"]? || resp.headers["location"]? || ""
raise "redirect with no Location header" if loc.empty?
loc = self.normalize_location(uri, loc)
new_uri = URI.parse(loc)
if self.same_origin?(self.canonical_uri(new_uri), self.canonical_uri(uri))
return resp
end
if resp.status_code == 303 || (
(resp.status_code == 301 || resp.status_code == 302) &&
method != "GET" && method != "HEAD"
)
method = "GET"
body = nil
headers_hash.delete("Content-Length")
headers_hash.delete("Content-Type")
end
unless same_origin?(uri, new_uri)
headers_hash.delete("Authorization")
end
uri = new_uri
return resp
rescue ex : Exception
puts ex.message
puts ex.backtrace
raise ex
end
end
end
private def self.normalize_location(base : URI, loc : String) : String
begin
URI.parse(loc).to_s
rescue
base_path = base.path.empty? ? "/" : base.path
resolved_path = if loc.starts_with?("/")
loc
else
dir = base_path.ends_with?("/") ? base_path : File.dirname(base_path)
File.join(dir, loc)
end
uri = URI.new("#{base.scheme}://#{base.host}#{base.port && base.port != 80 && base.port != 443 ? ":#{base.port}" : ""}
uri.to_s
end
end
private def self.same_origin?(a : URI, b : URI) : Bool
a.scheme == b.scheme && a.host == b.host && a.port == b.port
end
private def self.canonical(u : URI) : String
scheme = u.scheme.not_nil!.downcase
host = u.host.not_nil!.downcase
port = u.port
if (scheme == "http" && port == 80) || (scheme == "https" && port == 443)
port = nil
end
path = u.path.empty? ? "/" : File.expand_path(u.path, "/")
"#{scheme}://#{host}#{port ? ":#{port}" : ""}
end
private def self.canonical_uri(u : URI) : URI
URI.parse(self.canonical(u))
end
{% for method in %w(GET POST PUT DELETE OPTIONS HEAD) %}
def self.{{method.id.downcase}}(
url : String,
query : URI::Params? = URI::Params.new,
headers : HTTP::Headers? = HTTP::Headers.new,
body : String? = nil,
max_redirects : Int32? = 10,
)
self.fetch(
method: {{method.downcase}},
url: url,
query: query,
headers: headers,
body: body,
max_redirects: max_redirects,
)
end
def self.{{method.id.downcase}}(
url : String,
args : NamedTuple(
query: URI::Params,
headers: HTTP::Headers,
body: String,
max_redirects: Int32,
),
)
self.fetch(
method: {{method.downcase}},
url: url,
query: args[:query],
headers: args[:headers],
body: args[:body],
max_redirects: args[:max_redirects],
)
end
def self.{{method.id.downcase}}(url : String)
self.fetch({{method.downcase}}, url, nil, nil, nil, nil)
end
def self.{{method.id.downcase}}(
url : String,
args : NamedTuple(
query: URI::Params,
),
)
self.fetch({{method.downcase}}, url, args[:query], nil, nil, nil)
end
def self.{{method.id.downcase}}(
url : String,
args : NamedTuple(
headers: HTTP::Headers,
),
)
self.fetch({{method.downcase}}, url, nil, args[:headers], nil, nil)
end
def self.{{method.id.downcase}}(
url : String,
args : NamedTuple(
query: URI::Params,
headers: HTTP::Headers,
),
)
self.fetch({{method.downcase}}, url, args[:query], args[:headers], nil, nil)
end
def self.{{method.id.downcase}}(
url : String,
args : NamedTuple(
query: URI::Params,
body: String,
),
)
self.fetch({{method.downcase}}, url, args[:query], nil, args[:body], nil)
end
def self.{{method.id.downcase}}(
url : String,
args : NamedTuple(
query: URI::Params,
headers: HTTP::Headers,
body: String,
),
)
self.fetch({{method.downcase}}, url, args[:query], args[:headers], args[:body], nil)
end
def self.{{method.id.downcase}}(
url : String,
args : NamedTuple(
headers: HTTP::Headers,
body: String,
),
)
self.fetch({{method.downcase}}, url, nil, args[:headers], args[:body], nil)
end
def self.{{method.id.downcase}}(
url : String,
args : NamedTuple(
query: URI::Params,
headers: HTTP::Headers,
body: String,
max_redirects: Int32,
),
)
self.fetch({{method.downcase}}, url, args[:query], args[:headers], args[:body], args[:max_redirects])
end
def self.{{method.id.downcase}}(
url : String,
args : NamedTuple(
query: Hash(String, String),
headers: Hash(String, String),
body: Hash(String, String),
),
)
query = URI::Params.build do |p|
args[:query].each do |k, v|
p[k] = v
end
end
headers = HTTP::Headers.new
args[:headers].each do |k, v|
headers[k] = v
end
body = args[:body].to_json.to_s
self.fetch({{method.downcase}}, url, query, headers, body, nil)
end
def self.{{method.id.downcase}}(
url : String,
args : NamedTuple(
headers: Hash(String, String),
body: Hash(String, String),
),
)
headers = HTTP::Headers.new
args[:headers].each do |k, v|
headers[k] = v
end
body = args[:body].to_json.to_s
self.fetch({{method.downcase}}, url, nil, headers, body, nil)
end
def self.{{method.id.downcase}}(
url : String,
args : NamedTuple(
query: Hash(String, String),
body: Hash(String, String),
),
)
query = URI::Params.build do |p|
args[:query].each do |k, v|
p[k] = v
end
end
body = args[:body].to_json.to_s
self.fetch({{method.downcase}}, url, query, nil, body, nil)
end
def self.{{method.id.downcase}}(
url : String,
args : NamedTuple(
query: Hash(String, String),
headers: Hash(String, String),
),
)
query = URI::Params.build do |p|
args[:query].each do |k, v|
p[k] = v
end
end
headers = HTTP::Headers.new
args[:headers].each do |k, v|
headers[k] = v
end
self.fetch({{method.downcase}}, url, query, headers, nil, nil)
end
def self.{{method.id.downcase}}(
url : String,
args : NamedTuple(
query: Hash(String, String),
),
)
query = URI::Params.build do |p|
args[:query].each do |k, v|
p[k] = v
end
end
self.fetch({{method.downcase}}, url, query, nil, nil, nil)
end
def self.{{method.id.downcase}}(
url : String,
args : NamedTuple(
headers: Hash(String, String),
),
)
headers = HTTP::Headers.new
args[:headers].each do |k, v|
headers[k] = v
end
self.fetch({{method.downcase}}, url, nil, headers, nil, nil)
end
def self.{{method.id.downcase}}(
url : String,
args : NamedTuple(
body: Hash(String, String),
),
)
body = args[:body].to_json.to_s
self.fetch({{method.downcase}}, url, nil, nil, body, nil)
end
{% end %}
end