hyperkit 1.0.0

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