hyperkit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/.yardopts +1 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +23 -0
- data/Guardfile +43 -0
- data/LICENSE.txt +47 -0
- data/README.md +341 -0
- data/Rakefile +6 -0
- data/Vagrantfile +123 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/hyperkit.gemspec +33 -0
- data/lib/hyperkit.rb +58 -0
- data/lib/hyperkit/client.rb +82 -0
- data/lib/hyperkit/client/certificates.rb +102 -0
- data/lib/hyperkit/client/containers.rb +1100 -0
- data/lib/hyperkit/client/images.rb +672 -0
- data/lib/hyperkit/client/networks.rb +47 -0
- data/lib/hyperkit/client/operations.rb +123 -0
- data/lib/hyperkit/client/profiles.rb +59 -0
- data/lib/hyperkit/configurable.rb +110 -0
- data/lib/hyperkit/connection.rb +196 -0
- data/lib/hyperkit/default.rb +140 -0
- data/lib/hyperkit/error.rb +267 -0
- data/lib/hyperkit/middleware/follow_redirects.rb +131 -0
- data/lib/hyperkit/response/raise_error.rb +47 -0
- data/lib/hyperkit/version.rb +3 -0
- metadata +116 -0
@@ -0,0 +1,140 @@
|
|
1
|
+
################################################################################
|
2
|
+
# #
|
3
|
+
# Modeled on Octokit::Default #
|
4
|
+
# #
|
5
|
+
# Original Octokit license #
|
6
|
+
# ---------------------------------------------------------------------------- #
|
7
|
+
# Copyright (c) 2009-2016 Wynn Netherland, Adam Stacoviak, Erik Michaels-Ober #
|
8
|
+
# #
|
9
|
+
# Permission is hereby granted, free of charge, to any person obtaining a #
|
10
|
+
# copy of this software and associated documentation files (the "Software"), #
|
11
|
+
# to deal in the Software without restriction, including without limitation #
|
12
|
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense, #
|
13
|
+
# and/or sell copies of the Software, and to permit persons to whom the #
|
14
|
+
# Software is furnished to do so, subject to the following conditions: #
|
15
|
+
# #
|
16
|
+
# The above copyright notice and this permission notice shall be included #
|
17
|
+
# in all copies or substantial portions of the Software. #
|
18
|
+
# ---------------------------------------------------------------------------- #
|
19
|
+
# #
|
20
|
+
################################################################################
|
21
|
+
|
22
|
+
require 'openssl'
|
23
|
+
require 'hyperkit/middleware/follow_redirects'
|
24
|
+
require 'hyperkit/response/raise_error'
|
25
|
+
|
26
|
+
module Hyperkit
|
27
|
+
|
28
|
+
# Default configuration options for {Client}
|
29
|
+
module Default
|
30
|
+
|
31
|
+
# Default API endpoint
|
32
|
+
API_ENDPOINT = "https://localhost:8443".freeze
|
33
|
+
|
34
|
+
# Default auto-sync value
|
35
|
+
AUTO_SYNC = true
|
36
|
+
|
37
|
+
# Default client certificate file for authentication
|
38
|
+
CLIENT_CERT = File.join(ENV['HOME'], '.config', 'lxc', 'client.crt').freeze
|
39
|
+
|
40
|
+
# Default client key file for authentication
|
41
|
+
CLIENT_KEY = File.join(ENV['HOME'], '.config', 'lxc', 'client.key').freeze
|
42
|
+
|
43
|
+
# Default media type
|
44
|
+
MEDIA_TYPE = 'application/json'
|
45
|
+
|
46
|
+
# In Faraday 0.9, Faraday::Builder was renamed to Faraday::RackBuilder
|
47
|
+
RACK_BUILDER_CLASS = defined?(Faraday::RackBuilder) ? Faraday::RackBuilder : Faraday::Builder
|
48
|
+
|
49
|
+
# Default Faraday middleware stack
|
50
|
+
MIDDLEWARE = RACK_BUILDER_CLASS.new do |builder|
|
51
|
+
builder.use Hyperkit::Middleware::FollowRedirects
|
52
|
+
builder.use Hyperkit::Response::RaiseError
|
53
|
+
builder.adapter Faraday.default_adapter
|
54
|
+
end
|
55
|
+
|
56
|
+
# Default User Agent header string
|
57
|
+
USER_AGENT = "Hyperkit Ruby Gem #{Hyperkit::VERSION}".freeze
|
58
|
+
|
59
|
+
# Default to verifying SSL certificates
|
60
|
+
VERIFY_SSL = true
|
61
|
+
|
62
|
+
class << self
|
63
|
+
|
64
|
+
# Default options for Faraday::Connection
|
65
|
+
# @return [Hash]
|
66
|
+
def connection_options
|
67
|
+
{
|
68
|
+
:headers => {
|
69
|
+
:accept => default_media_type,
|
70
|
+
:user_agent => user_agent,
|
71
|
+
},
|
72
|
+
:ssl => {}
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
# Default media type from ENV or {MEDIA_TYPE}
|
77
|
+
# @return [String]
|
78
|
+
def default_media_type
|
79
|
+
ENV['HYPERKIT_DEFAULT_MEDIA_TYPE'] || MEDIA_TYPE
|
80
|
+
end
|
81
|
+
|
82
|
+
# Configuration options
|
83
|
+
# @return [Hash]
|
84
|
+
def options
|
85
|
+
Hash[Hyperkit::Configurable.keys.map{|key| [key, send(key)]}]
|
86
|
+
end
|
87
|
+
|
88
|
+
# Default API endpoint from ENV or {API_ENDPOINT}
|
89
|
+
# @return [String]
|
90
|
+
def api_endpoint
|
91
|
+
ENV['HYPERKIT_API_ENDPOINT'] || API_ENDPOINT
|
92
|
+
end
|
93
|
+
|
94
|
+
# Default auto-sync value from ENV or {AUTO_SYNC}
|
95
|
+
def auto_sync
|
96
|
+
ENV['HYPERKIT_AUTO_SYNC'] || AUTO_SYNC
|
97
|
+
end
|
98
|
+
|
99
|
+
# Default client certificate file from ENV or {CLIENT_CERT}
|
100
|
+
# @return [String]
|
101
|
+
def client_cert
|
102
|
+
ENV['HYPERKIT_CLIENT_CERT'] || CLIENT_CERT
|
103
|
+
end
|
104
|
+
|
105
|
+
# Default client key file from ENV or {CLIENT_KEY}
|
106
|
+
# @return [String]
|
107
|
+
def client_key
|
108
|
+
ENV['HYPERKIT_KEY'] || CLIENT_KEY
|
109
|
+
end
|
110
|
+
|
111
|
+
# Default middleware stack for Faraday::Connection
|
112
|
+
# from {MIDDLEWARE}
|
113
|
+
# @return [String]
|
114
|
+
def middleware
|
115
|
+
MIDDLEWARE
|
116
|
+
end
|
117
|
+
|
118
|
+
# Default proxy server URI for Faraday connection from ENV
|
119
|
+
# @return [String]
|
120
|
+
def proxy
|
121
|
+
ENV['HYPERKIT_PROXY']
|
122
|
+
end
|
123
|
+
|
124
|
+
# Default User-Agent header string from ENV or {USER_AGENT}
|
125
|
+
# @return [String]
|
126
|
+
def user_agent
|
127
|
+
ENV['HYPERKIT_USER_AGENT'] || USER_AGENT
|
128
|
+
end
|
129
|
+
|
130
|
+
# Default to verifying peer SSL certificate
|
131
|
+
# @return [Boolean]
|
132
|
+
def verify_ssl
|
133
|
+
true
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
@@ -0,0 +1,267 @@
|
|
1
|
+
################################################################################
|
2
|
+
# #
|
3
|
+
# Modeled on Octokit::Error #
|
4
|
+
# #
|
5
|
+
# Original Octokit license #
|
6
|
+
# ---------------------------------------------------------------------------- #
|
7
|
+
# Copyright (c) 2009-2016 Wynn Netherland, Adam Stacoviak, Erik Michaels-Ober #
|
8
|
+
# #
|
9
|
+
# Permission is hereby granted, free of charge, to any person obtaining a #
|
10
|
+
# copy of this software and associated documentation files (the "Software"), #
|
11
|
+
# to deal in the Software without restriction, including without limitation #
|
12
|
+
# the rights to use, copy, modify, merge, publish, distribute, sublicense, #
|
13
|
+
# and/or sell copies of the Software, and to permit persons to whom the #
|
14
|
+
# Software is furnished to do so, subject to the following conditions: #
|
15
|
+
# #
|
16
|
+
# The above copyright notice and this permission notice shall be included #
|
17
|
+
# in all copies or substantial portions of the Software. #
|
18
|
+
# ---------------------------------------------------------------------------- #
|
19
|
+
# #
|
20
|
+
################################################################################
|
21
|
+
|
22
|
+
|
23
|
+
module Hyperkit
|
24
|
+
|
25
|
+
# Custom error class for rescuing from all LXD errors
|
26
|
+
class Error < StandardError
|
27
|
+
|
28
|
+
# Returns the appropriate Hyperkit::Error subclass based
|
29
|
+
# on status and response message
|
30
|
+
#
|
31
|
+
# @param [Hash] response HTTP response
|
32
|
+
# @return [Hyperkit::Error]
|
33
|
+
def self.from_response(response)
|
34
|
+
|
35
|
+
status = response[:status].to_i
|
36
|
+
|
37
|
+
err = from_status(response, status)
|
38
|
+
|
39
|
+
if err.nil?
|
40
|
+
err = from_async_operation(response)
|
41
|
+
end
|
42
|
+
|
43
|
+
err
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.from_async_operation(response)
|
48
|
+
|
49
|
+
return nil if response.nil? || response[:body].empty?
|
50
|
+
|
51
|
+
begin
|
52
|
+
body = JSON.parse(response[:body])
|
53
|
+
rescue
|
54
|
+
return nil
|
55
|
+
end
|
56
|
+
|
57
|
+
if body.has_key?("metadata") && body["metadata"].is_a?(Hash)
|
58
|
+
status = body["metadata"]["status_code"].to_i
|
59
|
+
from_status(response, status)
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.from_status(response, status)
|
65
|
+
if klass = case status
|
66
|
+
when 400 then Hyperkit::BadRequest
|
67
|
+
when 401 then Hyperkit::Unauthorized
|
68
|
+
when 403 then Hyperkit::Forbidden
|
69
|
+
when 404 then Hyperkit::NotFound
|
70
|
+
when 405 then Hyperkit::MethodNotAllowed
|
71
|
+
when 406 then Hyperkit::NotAcceptable
|
72
|
+
when 409 then Hyperkit::Conflict
|
73
|
+
when 415 then Hyperkit::UnsupportedMediaType
|
74
|
+
when 422 then Hyperkit::UnprocessableEntity
|
75
|
+
when 400..499 then Hyperkit::ClientError
|
76
|
+
when 500 then error_for_500(response)
|
77
|
+
when 501 then Hyperkit::NotImplemented
|
78
|
+
when 502 then Hyperkit::BadGateway
|
79
|
+
when 503 then Hyperkit::ServiceUnavailable
|
80
|
+
when 500..599 then Hyperkit::ServerError
|
81
|
+
end
|
82
|
+
klass.new(response)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def initialize(response=nil)
|
87
|
+
@response = response
|
88
|
+
super(build_error_message)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Documentation URL returned by the API for some errors
|
92
|
+
#
|
93
|
+
# @return [String]
|
94
|
+
def documentation_url
|
95
|
+
data[:documentation_url] if data.is_a? Hash
|
96
|
+
end
|
97
|
+
|
98
|
+
# Array of validation errors
|
99
|
+
# @return [Array<Hash>] Error info
|
100
|
+
def errors
|
101
|
+
if data && data.is_a?(Hash)
|
102
|
+
data[:errors] || []
|
103
|
+
else
|
104
|
+
[]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.error_for_500(response)
|
109
|
+
|
110
|
+
if response.body =~ /open: no such file or directory/i
|
111
|
+
Hyperkit::NotFound
|
112
|
+
elsif response.body =~ /open: is a directory/i
|
113
|
+
Hyperkit::BadRequest
|
114
|
+
else
|
115
|
+
Hyperkit::InternalServerError
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def data
|
123
|
+
@data ||=
|
124
|
+
if (body = @response[:body]) && !body.empty?
|
125
|
+
if body.is_a?(String) &&
|
126
|
+
@response[:response_headers] &&
|
127
|
+
@response[:response_headers][:content_type] =~ /json/
|
128
|
+
|
129
|
+
Sawyer::Agent.serializer.decode(body)
|
130
|
+
else
|
131
|
+
body
|
132
|
+
end
|
133
|
+
else
|
134
|
+
nil
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def response_message
|
139
|
+
case data
|
140
|
+
when Hash
|
141
|
+
data[:message]
|
142
|
+
when String
|
143
|
+
data
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def response_error
|
148
|
+
err = nil
|
149
|
+
|
150
|
+
if data.is_a?(Hash) && data[:error]
|
151
|
+
err = data[:error]
|
152
|
+
elsif data.is_a?(Hash) && data[:metadata]
|
153
|
+
err = data[:metadata][:err]
|
154
|
+
end
|
155
|
+
|
156
|
+
"Error: #{err}" if err
|
157
|
+
end
|
158
|
+
|
159
|
+
def response_error_summary
|
160
|
+
return nil unless data.is_a?(Hash) && !Array(data[:errors]).empty?
|
161
|
+
|
162
|
+
summary = "\nError summary:\n"
|
163
|
+
summary << data[:errors].map do |hash|
|
164
|
+
hash.map { |k,v| " #{k}: #{v}" }
|
165
|
+
end.join("\n")
|
166
|
+
|
167
|
+
summary
|
168
|
+
end
|
169
|
+
|
170
|
+
def build_error_message
|
171
|
+
return nil if @response.nil?
|
172
|
+
|
173
|
+
message = ""
|
174
|
+
|
175
|
+
if ! data.is_a?(Hash) || ! data[:metadata] || ! data[:metadata][:err]
|
176
|
+
message = "#{@response[:method].to_s.upcase} "
|
177
|
+
message << redact_url(@response[:url].to_s) + ": "
|
178
|
+
end
|
179
|
+
|
180
|
+
if data.is_a?(Hash) && data[:metadata] && data[:metadata][:status_code]
|
181
|
+
message << "#{data[:metadata][:status_code]} - "
|
182
|
+
else
|
183
|
+
message << "#{@response[:status]} - "
|
184
|
+
end
|
185
|
+
|
186
|
+
message << "#{response_message}" unless response_message.nil?
|
187
|
+
message << "#{response_error}" unless response_error.nil?
|
188
|
+
message << "#{response_error_summary}" unless response_error_summary.nil?
|
189
|
+
message << " // See: #{documentation_url}" unless documentation_url.nil?
|
190
|
+
message
|
191
|
+
end
|
192
|
+
|
193
|
+
def redact_url(url_string)
|
194
|
+
%w[secret].each do |token|
|
195
|
+
url_string.gsub!(/#{token}=\S+/, "#{token}=(redacted)") if url_string.include? token
|
196
|
+
end
|
197
|
+
url_string
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Raised on errors in the 400-499 range
|
202
|
+
class ClientError < Error; end
|
203
|
+
|
204
|
+
# Raised when LXD returns a 400 HTTP status code
|
205
|
+
class BadRequest < ClientError; end
|
206
|
+
|
207
|
+
# Raised when LXD returns a 401 HTTP status code
|
208
|
+
class Unauthorized < ClientError; end
|
209
|
+
|
210
|
+
# Raised when LXD returns a 403 HTTP status code
|
211
|
+
class Forbidden < ClientError; end
|
212
|
+
|
213
|
+
# Raised when LXD returns a 404 HTTP status code
|
214
|
+
class NotFound < ClientError; end
|
215
|
+
|
216
|
+
# Raised when LXD returns a 405 HTTP status code
|
217
|
+
class MethodNotAllowed < ClientError; end
|
218
|
+
|
219
|
+
# Raised when LXD returns a 406 HTTP status code
|
220
|
+
class NotAcceptable < ClientError; end
|
221
|
+
|
222
|
+
# Raised when LXD returns a 409 HTTP status code
|
223
|
+
class Conflict < ClientError; end
|
224
|
+
|
225
|
+
# Raised when LXD returns a 414 HTTP status code
|
226
|
+
class UnsupportedMediaType < ClientError; end
|
227
|
+
|
228
|
+
# Raised when LXD returns a 422 HTTP status code
|
229
|
+
class UnprocessableEntity < ClientError; end
|
230
|
+
|
231
|
+
# Raised on errors in the 500-599 range
|
232
|
+
class ServerError < Error; end
|
233
|
+
|
234
|
+
# Raised when LXD returns a 500 HTTP status code
|
235
|
+
class InternalServerError < ServerError; end
|
236
|
+
|
237
|
+
# Raised when LXD returns a 501 HTTP status code
|
238
|
+
class NotImplemented < ServerError; end
|
239
|
+
|
240
|
+
# Raised when LXD returns a 502 HTTP status code
|
241
|
+
class BadGateway < ServerError; end
|
242
|
+
|
243
|
+
# Raised when LXD returns a 503 HTTP status code
|
244
|
+
class ServiceUnavailable < ServerError; end
|
245
|
+
|
246
|
+
# Raised when a method requires an alias or a fingerprint, but
|
247
|
+
# none is provided
|
248
|
+
class ImageIdentifierRequired < StandardError; end
|
249
|
+
|
250
|
+
# Raised when a method requires attributes of an alias to be
|
251
|
+
# passed (e.g. description, traget), but none is provided
|
252
|
+
class AliasAttributesRequired < StandardError; end
|
253
|
+
|
254
|
+
# Raised when a method requires a protocol to be specified
|
255
|
+
# (e.g. "lxd" or "simplestreams"), but an invalid protocol is
|
256
|
+
# provided
|
257
|
+
class InvalidProtocol < StandardError; end
|
258
|
+
|
259
|
+
# Raise when a method is passed image attributes that are illegal
|
260
|
+
# (e.g. creating a container from a local image, but passing a protocol)
|
261
|
+
class InvalidImageAttributes < StandardError; end
|
262
|
+
|
263
|
+
# Raise when profiles are specified that do not exist on the server
|
264
|
+
class MissingProfiles < StandardError; end
|
265
|
+
|
266
|
+
end
|
267
|
+
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
module Hyperkit
|
5
|
+
|
6
|
+
module Middleware
|
7
|
+
|
8
|
+
# Public: Exception thrown when the maximum amount of requests is exceeded.
|
9
|
+
#
|
10
|
+
# Taken from Octokit, which was originally adapted from
|
11
|
+
# https://github.com/lostisland/faraday_middleware/blob/138766e/lib/faraday_middleware/response/follow_redirects.rb
|
12
|
+
|
13
|
+
class RedirectLimitReached < Faraday::Error::ClientError
|
14
|
+
attr_reader :response
|
15
|
+
|
16
|
+
def initialize(response)
|
17
|
+
super "too many redirects; last one to: #{response['location']}"
|
18
|
+
@response = response
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Public: Follow HTTP 301, 302, 303, and 307 redirects.
|
23
|
+
#
|
24
|
+
# For HTTP 303, the original GET, POST, PUT, DELETE, or PATCH request gets
|
25
|
+
# converted into a GET. For HTTP 301, 302, and 307, the HTTP method remains
|
26
|
+
# unchanged.
|
27
|
+
#
|
28
|
+
# This middleware currently only works with synchronous requests; i.e. it
|
29
|
+
# doesn't support parallelism.
|
30
|
+
class FollowRedirects < Faraday::Middleware
|
31
|
+
# HTTP methods for which 30x redirects can be followed
|
32
|
+
ALLOWED_METHODS = Set.new [:head, :options, :get, :post, :put, :patch, :delete]
|
33
|
+
|
34
|
+
# HTTP redirect status codes that this middleware implements
|
35
|
+
REDIRECT_CODES = Set.new [301, 302, 303, 307]
|
36
|
+
|
37
|
+
# Keys in env hash which will get cleared between requests
|
38
|
+
ENV_TO_CLEAR = Set.new [:status, :response, :response_headers]
|
39
|
+
|
40
|
+
# Default value for max redirects followed
|
41
|
+
FOLLOW_LIMIT = 3
|
42
|
+
|
43
|
+
# Regex that matches characters that need to be escaped in URLs, sans
|
44
|
+
# the "%" character which we assume already represents an escaped
|
45
|
+
# sequence.
|
46
|
+
URI_UNSAFE = /[^\-_.!~*'()a-zA-Z\d;\/?:@&=+$,\[\]%]/
|
47
|
+
|
48
|
+
# Public: Initialize the middleware.
|
49
|
+
#
|
50
|
+
# options - An options Hash (default: {}):
|
51
|
+
# :limit - A Fixnum redirect limit (default: 3).
|
52
|
+
def initialize(app, options = {})
|
53
|
+
super(app)
|
54
|
+
@options = options
|
55
|
+
|
56
|
+
@convert_to_get = Set.new [303]
|
57
|
+
end
|
58
|
+
|
59
|
+
def call(env)
|
60
|
+
perform_with_redirection(env, follow_limit)
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def convert_to_get?(response)
|
66
|
+
![:head, :options].include?(response.env[:method]) &&
|
67
|
+
@convert_to_get.include?(response.status)
|
68
|
+
end
|
69
|
+
|
70
|
+
def perform_with_redirection(env, follows)
|
71
|
+
request_body = env[:body]
|
72
|
+
response = @app.call(env)
|
73
|
+
|
74
|
+
response.on_complete do |response_env|
|
75
|
+
if follow_redirect?(response_env, response)
|
76
|
+
raise(RedirectLimitReached, response) if follows.zero?
|
77
|
+
new_request_env = update_env(response_env, request_body, response)
|
78
|
+
response = perform_with_redirection(new_request_env, follows - 1)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
response
|
82
|
+
end
|
83
|
+
|
84
|
+
def update_env(env, request_body, response)
|
85
|
+
original_url = env[:url]
|
86
|
+
env[:url] += safe_escape(response["location"])
|
87
|
+
unless same_host?(original_url, env[:url])
|
88
|
+
env[:request_headers].delete("Authorization")
|
89
|
+
end
|
90
|
+
|
91
|
+
if convert_to_get?(response)
|
92
|
+
env[:method] = :get
|
93
|
+
env[:body] = nil
|
94
|
+
else
|
95
|
+
env[:body] = request_body
|
96
|
+
end
|
97
|
+
|
98
|
+
ENV_TO_CLEAR.each { |key| env.delete(key) }
|
99
|
+
|
100
|
+
env
|
101
|
+
end
|
102
|
+
|
103
|
+
def follow_redirect?(env, response)
|
104
|
+
ALLOWED_METHODS.include?(env[:method]) &&
|
105
|
+
REDIRECT_CODES.include?(response.status)
|
106
|
+
end
|
107
|
+
|
108
|
+
def follow_limit
|
109
|
+
@options.fetch(:limit, FOLLOW_LIMIT)
|
110
|
+
end
|
111
|
+
|
112
|
+
def same_host?(original_url, redirect_url)
|
113
|
+
original_uri = Addressable::URI.parse(original_url)
|
114
|
+
redirect_uri = Addressable::URI.parse(redirect_url)
|
115
|
+
|
116
|
+
redirect_uri.host.nil? || original_uri.host == redirect_uri.host
|
117
|
+
end
|
118
|
+
|
119
|
+
# Internal: Escapes unsafe characters from a URL which might be a path
|
120
|
+
# component only or a fully-qualified URI so that it can be joined onto a
|
121
|
+
# URI:HTTP using the `+` operator. Doesn't escape "%" characters so to not
|
122
|
+
# risk double-escaping.
|
123
|
+
def safe_escape(uri)
|
124
|
+
uri.to_s.gsub(URI_UNSAFE) { |match|
|
125
|
+
"%" + match.unpack("H2" * match.bytesize).join("%").upcase
|
126
|
+
}
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|