hyperkit 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|