6c451d260f
Reviewed-on: #402 Co-authored-by: miteruzo <miteruzo@naver.com> Co-committed-by: miteruzo <miteruzo@naver.com>
123 行
4.4 KiB
Ruby
123 行
4.4 KiB
Ruby
require 'net/http'
|
|
require 'json'
|
|
|
|
module Preview
|
|
class HttpFetcher
|
|
class FetchFailed < StandardError; end
|
|
class FetchTimeout < FetchFailed; end
|
|
class ResponseTooLarge < FetchFailed; end
|
|
|
|
MAX_REDIRECTS = 5
|
|
DEFAULT_MAX_BYTES = 5.megabytes
|
|
|
|
Response = Data.define(:body, :content_type, :url)
|
|
|
|
def self.fetch(raw_url, max_bytes: DEFAULT_MAX_BYTES, redirects: MAX_REDIRECTS)
|
|
uri, addresses = UrlSafety.validate(raw_url)
|
|
response = request(uri, addresses.first, max_bytes)
|
|
|
|
if response.is_a?(Net::HTTPRedirection)
|
|
location = response['location']
|
|
if redirects.zero?
|
|
log_failure(:redirect_limit,
|
|
url: uri.to_s,
|
|
redirects:,
|
|
location:,
|
|
content_type: response['content-type'],
|
|
content_length: response['content-length'])
|
|
raise FetchFailed, 'redirect が多すぎます.'
|
|
end
|
|
|
|
if location.blank?
|
|
log_failure(:blank_redirect_location,
|
|
url: uri.to_s,
|
|
redirects:,
|
|
content_type: response['content-type'],
|
|
content_length: response['content-length'])
|
|
raise FetchFailed, 'redirect 先が不正です.'
|
|
end
|
|
|
|
redirect_url =
|
|
begin
|
|
URI.join(uri, location).to_s
|
|
rescue URI::InvalidURIError => e
|
|
log_failure(:invalid_redirect_location,
|
|
url: uri.to_s,
|
|
redirects:,
|
|
location:,
|
|
error: e.class.name,
|
|
message: e.message)
|
|
raise FetchFailed, 'redirect 先が不正です.'
|
|
end
|
|
|
|
return fetch(redirect_url, max_bytes:, redirects: redirects - 1)
|
|
end
|
|
|
|
unless response.is_a?(Net::HTTPSuccess)
|
|
log_failure(:http_status,
|
|
url: uri.to_s,
|
|
code: response.code,
|
|
content_type: response['content-type'],
|
|
content_length: response['content-length'])
|
|
raise FetchFailed, "外部サーバーが HTTP #{ response.code } を返しました."
|
|
end
|
|
|
|
Response.new(response.body, response['content-type'].to_s, uri.to_s)
|
|
rescue Net::OpenTimeout, Net::ReadTimeout, Timeout::Error => e
|
|
log_failure(:timeout, url: uri&.to_s || raw_url, error: e.class.name, message: e.message)
|
|
raise FetchTimeout, e.message
|
|
rescue SocketError, SystemCallError, OpenSSL::SSL::SSLError, EOFError => e
|
|
log_failure(:network_error, url: uri&.to_s || raw_url, error: e.class.name, message: e.message)
|
|
raise FetchFailed, e.message
|
|
end
|
|
|
|
def self.request(uri, ip_address, max_bytes)
|
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
http.ipaddr = ip_address
|
|
http.use_ssl = uri.scheme == 'https'
|
|
http.open_timeout = 5
|
|
http.read_timeout = 8
|
|
http.write_timeout = 5
|
|
|
|
request = Net::HTTP::Get.new(uri)
|
|
request['User-Agent'] = 'BTRC-Hub thumbnail preview'
|
|
request['Accept'] = 'text/html,image/*;q=0.9,*/*;q=0.1'
|
|
|
|
http.request(request) do |response|
|
|
length = response['content-length'].to_i
|
|
if length > max_bytes
|
|
log_failure(:response_too_large,
|
|
url: uri.to_s,
|
|
content_type: response['content-type'],
|
|
content_length: response['content-length'],
|
|
max_bytes:)
|
|
raise ResponseTooLarge, '外部データが大きすぎます.'
|
|
end
|
|
|
|
body = +''
|
|
response.read_body do |chunk|
|
|
body << chunk
|
|
next unless body.bytesize > max_bytes
|
|
|
|
log_failure(:response_too_large,
|
|
url: uri.to_s,
|
|
content_type: response['content-type'],
|
|
content_length: response['content-length'],
|
|
bytes_read: body.bytesize,
|
|
max_bytes:)
|
|
raise ResponseTooLarge, '外部データが大きすぎます.'
|
|
end
|
|
response.instance_variable_set(:@body, body)
|
|
response.instance_variable_set(:@read, true)
|
|
return response
|
|
end
|
|
end
|
|
|
|
def self.log_failure(reason, **payload)
|
|
Rails.logger.warn("preview_http_fetcher_failure #{ { reason:, **payload }.to_json }")
|
|
end
|
|
|
|
private_class_method :request, :log_failure
|
|
end
|
|
end
|