]> cat aescling's git repositories - mastodon.git/blob - app/lib/request.rb
Guard against nil URLs in Request class (#7284)
[mastodon.git] / app / lib / request.rb
1 # frozen_string_literal: true
2
3 require 'ipaddr'
4 require 'socket'
5
6 class Request
7 REQUEST_TARGET = '(request-target)'
8
9 include RoutingHelper
10
11 def initialize(verb, url, **options)
12 raise ArgumentError if url.blank?
13
14 @verb = verb
15 @url = Addressable::URI.parse(url).normalize
16 @options = options.merge(use_proxy? ? Rails.configuration.x.http_client_proxy : { socket_class: Socket })
17 @headers = {}
18
19 raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if block_hidden_service?
20
21 set_common_headers!
22 set_digest! if options.key?(:body)
23 end
24
25 def on_behalf_of(account, key_id_format = :acct)
26 raise ArgumentError unless account.local?
27
28 @account = account
29 @key_id_format = key_id_format
30
31 self
32 end
33
34 def add_headers(new_headers)
35 @headers.merge!(new_headers)
36 self
37 end
38
39 def perform
40 begin
41 response = http_client.headers(headers).public_send(@verb, @url.to_s, @options)
42 rescue => e
43 raise e.class, "#{e.message} on #{@url}", e.backtrace[0]
44 end
45
46 begin
47 yield response.extend(ClientLimit)
48 ensure
49 http_client.close
50 end
51 end
52
53 def headers
54 (@account ? @headers.merge('Signature' => signature) : @headers).without(REQUEST_TARGET)
55 end
56
57 private
58
59 def set_common_headers!
60 @headers[REQUEST_TARGET] = "#{@verb} #{@url.path}"
61 @headers['User-Agent'] = user_agent
62 @headers['Host'] = @url.host
63 @headers['Date'] = Time.now.utc.httpdate
64 end
65
66 def set_digest!
67 @headers['Digest'] = "SHA-256=#{Digest::SHA256.base64digest(@options[:body])}"
68 end
69
70 def signature
71 algorithm = 'rsa-sha256'
72 signature = Base64.strict_encode64(@account.keypair.sign(OpenSSL::Digest::SHA256.new, signed_string))
73
74 "keyId=\"#{key_id}\",algorithm=\"#{algorithm}\",headers=\"#{signed_headers}\",signature=\"#{signature}\""
75 end
76
77 def signed_string
78 @headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n")
79 end
80
81 def signed_headers
82 @headers.keys.join(' ').downcase
83 end
84
85 def user_agent
86 @user_agent ||= "#{HTTP::Request::USER_AGENT} (Mastodon/#{Mastodon::Version}; +#{root_url})"
87 end
88
89 def key_id
90 case @key_id_format
91 when :acct
92 @account.to_webfinger_s
93 when :uri
94 [ActivityPub::TagManager.instance.uri_for(@account), '#main-key'].join
95 end
96 end
97
98 def timeout
99 { write: 10, connect: 10, read: 10 }
100 end
101
102 def http_client
103 @http_client ||= HTTP.timeout(:per_operation, timeout).follow(max_hops: 2)
104 end
105
106 def use_proxy?
107 Rails.configuration.x.http_client_proxy.present?
108 end
109
110 def block_hidden_service?
111 !Rails.configuration.x.access_to_hidden_service && /\.(onion|i2p)$/.match(@url.host)
112 end
113
114 module ClientLimit
115 def body_with_limit(limit = 1.megabyte)
116 raise Mastodon::LengthValidationError if content_length.present? && content_length > limit
117
118 if charset.nil?
119 encoding = Encoding::BINARY
120 else
121 begin
122 encoding = Encoding.find(charset)
123 rescue ArgumentError
124 encoding = Encoding::BINARY
125 end
126 end
127
128 contents = String.new(encoding: encoding)
129
130 while (chunk = readpartial)
131 contents << chunk
132 chunk.clear
133
134 raise Mastodon::LengthValidationError if contents.bytesize > limit
135 end
136
137 contents
138 end
139 end
140
141 class Socket < TCPSocket
142 class << self
143 def open(host, *args)
144 return super host, *args if thru_hidden_service? host
145 outer_e = nil
146 Addrinfo.foreach(host, nil, nil, :SOCK_STREAM) do |address|
147 begin
148 raise Mastodon::HostValidationError if PrivateAddressCheck.private_address? IPAddr.new(address.ip_address)
149 return super address.ip_address, *args
150 rescue => e
151 outer_e = e
152 end
153 end
154 raise outer_e if outer_e
155 end
156
157 alias new open
158
159 def thru_hidden_service?(host)
160 Rails.configuration.x.hidden_service_via_transparent_proxy && /\.(onion|i2p)$/.match(host)
161 end
162 end
163 end
164
165 private_constant :ClientLimit, :Socket
166 end
This page took 0.104401 seconds and 4 git commands to generate.