pebblescape 0.0.1

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.
@@ -0,0 +1,302 @@
1
+ require "cgi"
2
+ require "netrc"
3
+ require "pebbles"
4
+ require "pebbles/api"
5
+ require "pebbles/helpers"
6
+
7
+ class Pebbles::Auth
8
+ class << self
9
+ include Pebbles::Helpers
10
+
11
+ attr_accessor :credentials
12
+
13
+ def api
14
+ @api ||= begin
15
+ debug "Using API with key: #{password[0,6]}..."
16
+ Pebbles::API.new(default_params.merge(:api_key => password))
17
+ end
18
+ end
19
+
20
+ def login
21
+ delete_credentials
22
+ get_credentials
23
+ end
24
+
25
+ def logout
26
+ delete_credentials
27
+ end
28
+
29
+ # just a stub; will raise if not authenticated
30
+ def check
31
+ api.get_user
32
+ end
33
+
34
+ def default_host
35
+ "pebblesinspace.com"
36
+ end
37
+
38
+ def http_git_host
39
+ ENV['PEBBLES_HTTP_GIT_HOST'] || "git.#{host}"
40
+ end
41
+
42
+ def git_host
43
+ ENV['PEBBLES_GIT_HOST'] || host
44
+ end
45
+
46
+ def host
47
+ ENV['PEBBLES_HOST'] || default_host
48
+ end
49
+
50
+ def subdomains
51
+ %w(api git)
52
+ end
53
+
54
+ def reauthorize
55
+ @credentials = ask_for_and_save_credentials
56
+ end
57
+
58
+ def user # :nodoc:
59
+ get_credentials[0]
60
+ end
61
+
62
+ def password # :nodoc:
63
+ get_credentials[1]
64
+ end
65
+
66
+ def api_key(user=get_credentials[0], password=get_credentials[1])
67
+ @api ||= Pebbles::API.new(default_params)
68
+ api_key = @api.post_login(user, password).body["api_key"]
69
+ @api = nil
70
+ api_key
71
+ end
72
+
73
+ def get_credentials # :nodoc:
74
+ @credentials ||= (read_credentials || ask_for_and_save_credentials)
75
+ end
76
+
77
+ def delete_credentials
78
+ return
79
+ if netrc
80
+ subdomains.each do |sub|
81
+ netrc.delete("#{sub}.#{host}")
82
+ end
83
+ netrc.save
84
+ end
85
+ @api, @credentials = nil, nil
86
+ end
87
+
88
+ def netrc_path
89
+ default = Netrc.default_path
90
+ encrypted = default + ".gpg"
91
+ if File.exists?(encrypted)
92
+ encrypted
93
+ else
94
+ default
95
+ end
96
+ end
97
+
98
+ def netrc # :nodoc:
99
+ @netrc ||= begin
100
+ File.exists?(netrc_path) && Netrc.read(netrc_path)
101
+ rescue => error
102
+ case error.message
103
+ when /^Permission bits for/
104
+ abort("#{error.message}.\nYou should run `chmod 0600 #{netrc_path}` so that your credentials are NOT accessible by others.")
105
+ when /EACCES/
106
+ error("Error reading #{netrc_path}\n#{error.message}\nMake sure this user can read/write this file.")
107
+ else
108
+ error("Error reading #{netrc_path}\n#{error.message}\nYou may need to delete this file and run `pebbles login` to recreate it.")
109
+ end
110
+ end
111
+ end
112
+
113
+ def read_credentials
114
+ if ENV['PEBBLES_API_KEY']
115
+ ['', ENV['PEBBLES_API_KEY']]
116
+ else
117
+ # read netrc credentials if they exist
118
+ if netrc
119
+ netrc["api.#{host}"]
120
+ end
121
+ end
122
+ end
123
+
124
+ def write_credentials
125
+ FileUtils.mkdir_p(File.dirname(netrc_path))
126
+ FileUtils.touch(netrc_path)
127
+ unless running_on_windows?
128
+ FileUtils.chmod(0600, netrc_path)
129
+ end
130
+ subdomains.each do |sub|
131
+ netrc["#{sub}.#{host}"] = self.credentials
132
+ end
133
+ netrc.save
134
+ end
135
+
136
+ def echo_off
137
+ with_tty do
138
+ system "stty -echo"
139
+ end
140
+ end
141
+
142
+ def echo_on
143
+ with_tty do
144
+ system "stty echo"
145
+ end
146
+ end
147
+
148
+ def ask_for_credentials
149
+ puts "Enter your Pebblescape credentials."
150
+
151
+ print "Email: "
152
+ user = ask
153
+
154
+ print "Password (typing will be hidden): "
155
+ password = running_on_windows? ? ask_for_password_on_windows : ask_for_password
156
+ [user, api_key(user, password)]
157
+ end
158
+
159
+ def ask_for_password_on_windows
160
+ require "Win32API"
161
+ char = nil
162
+ password = ''
163
+
164
+ while char = Win32API.new("crtdll", "_getch", [ ], "L").Call do
165
+ break if char == 10 || char == 13 # received carriage return or newline
166
+ if char == 127 || char == 8 # backspace and delete
167
+ password.slice!(-1, 1)
168
+ else
169
+ # windows might throw a -1 at us so make sure to handle RangeError
170
+ (password << char.chr) rescue RangeError
171
+ end
172
+ end
173
+ puts
174
+ return password
175
+ end
176
+
177
+ def ask_for_password
178
+ begin
179
+ echo_off
180
+ password = ask
181
+ puts
182
+ ensure
183
+ echo_on
184
+ end
185
+ return password
186
+ end
187
+
188
+ def ask_for_and_save_credentials
189
+ @credentials = ask_for_credentials
190
+ debug "Logged in as #{@credentials[0]} with key: #{@credentials[1][0,6]}..."
191
+ write_credentials
192
+ check
193
+ @credentials
194
+ rescue Pebbles::API::Errors::Unauthorized => e
195
+ delete_credentials
196
+ display "Authentication failed."
197
+ warn "WARNING: PEBBLES_API_KEY is set to an invalid key." if ENV['PEBBLES_API_KEY']
198
+ retry if retry_login?
199
+ exit 1
200
+ rescue => e
201
+ delete_credentials
202
+ raise e
203
+ end
204
+
205
+ def associate_or_generate_ssh_key
206
+ unless File.exists?("#{home_directory}/.ssh/id_rsa.pub")
207
+ display "Could not find an existing public key at ~/.ssh/id_rsa.pub"
208
+ display "Would you like to generate one? [Yn] ", false
209
+ unless ask.strip.downcase =~ /^n/
210
+ display "Generating new SSH public key."
211
+ generate_ssh_key("#{home_directory}/.ssh/id_rsa")
212
+ associate_key("#{home_directory}/.ssh/id_rsa.pub")
213
+ return
214
+ end
215
+ end
216
+
217
+ chosen = ssh_prompt
218
+ associate_key(chosen) if chosen
219
+ end
220
+
221
+ def ssh_prompt
222
+ public_keys = Dir.glob("#{home_directory}/.ssh/*.pub").sort
223
+ case public_keys.length
224
+ when 0
225
+ error("No SSH keys found")
226
+ return nil
227
+ when 1
228
+ display "Found an SSH public key at #{public_keys.first}"
229
+ display "Would you like to upload it to Pebblescape? [Yn] ", false
230
+ return ask.strip.downcase =~ /^n/ ? nil : public_keys.first
231
+ else
232
+ display "Found the following SSH public keys:"
233
+ public_keys.each_with_index do |key, index|
234
+ display "#{index+1}) #{File.basename(key)}"
235
+ end
236
+ display "Which would you like to use with your Pebblescape account? ", false
237
+ choice = ask.to_i - 1
238
+ chosen = public_keys[choice]
239
+ if choice == -1 || chosen.nil?
240
+ error("Invalid choice")
241
+ end
242
+ return chosen
243
+ end
244
+ end
245
+
246
+ def generate_ssh_key(keyfile)
247
+ ssh_dir = File.dirname(keyfile)
248
+ FileUtils.mkdir_p ssh_dir, :mode => 0700
249
+ output = `ssh-keygen -t rsa -N "" -f \"#{keyfile}\" 2>&1`
250
+ if ! $?.success?
251
+ error("Could not generate key: #{output}")
252
+ end
253
+ end
254
+
255
+ def associate_key(key)
256
+ action("Uploading SSH public key #{key}") do
257
+ if File.exists?(key)
258
+ api.post_key(File.read(key))
259
+ else
260
+ error("Could not upload SSH public key: key file '" + key + "' does not exist")
261
+ end
262
+ end
263
+ end
264
+
265
+ def retry_login?
266
+ @login_attempts ||= 0
267
+ @login_attempts += 1
268
+ @login_attempts < 3
269
+ end
270
+
271
+ def base_host(host)
272
+ parts = URI.parse(full_host(host)).host.split(".")
273
+ return parts.first if parts.size == 1
274
+ parts[-2..-1].join(".")
275
+ end
276
+
277
+ def full_host(host)
278
+ scheme = debugging? ? 'http' : 'https'
279
+ (host =~ /^http/) ? host : "#{scheme}://api.#{host}"
280
+ end
281
+
282
+ def verify_host?(host)
283
+ return false if ENV["PEBBLES_SSL_VERIFY"] == "disable"
284
+ base_host(host) == "pebblesinspace.com"
285
+ end
286
+
287
+ protected
288
+
289
+ def default_params
290
+ uri = URI.parse(full_host(host))
291
+ params = {
292
+ :headers => {'User-Agent' => Pebbles.user_agent},
293
+ :host => uri.host,
294
+ :port => uri.port.to_s,
295
+ :scheme => uri.scheme,
296
+ :ssl_verify_peer => verify_host?(host)
297
+ }
298
+
299
+ params
300
+ end
301
+ end
302
+ end
@@ -0,0 +1,35 @@
1
+ require 'pebbles'
2
+ require 'pebbles/command'
3
+ require 'pebbles/git'
4
+ require 'pebbles/helpers'
5
+ require 'excon'
6
+
7
+ class Pebbles::CLI
8
+ extend Pebbles::Helpers
9
+
10
+ def self.start(*args)
11
+ $stdin.sync = true if $stdin.isatty
12
+ $stdout.sync = true if $stdout.isatty
13
+ Pebbles::Git.check_git_version
14
+ command = args.shift.strip rescue "help"
15
+ Pebbles::Command.load
16
+ Pebbles::Command.run(command, args)
17
+ rescue Errno::EPIPE => e
18
+ error(e.message)
19
+ rescue Interrupt => e
20
+ `stty icanon echo`
21
+ if ENV["PEBBLES_DEBUG"]
22
+ styled_error(e)
23
+ else
24
+ error("Command cancelled.", false)
25
+ end
26
+ rescue => error
27
+ if ENV["PEBBLES_DEBUG"]
28
+ raise
29
+ else
30
+ styled_error(error)
31
+ end
32
+ exit(1)
33
+ end
34
+
35
+ end
@@ -0,0 +1,256 @@
1
+ require 'pebbles/helpers'
2
+ require 'pebbles/version'
3
+ require "optparse"
4
+
5
+ module Pebbles
6
+ module Command
7
+ class CommandFailed < RuntimeError; end
8
+
9
+ extend Pebbles::Helpers
10
+
11
+ def self.load
12
+ Dir[File.join(File.dirname(__FILE__), "command", "*.rb")].each do |file|
13
+ require file
14
+ end
15
+ unregister_commands_made_private_after_the_fact
16
+ end
17
+
18
+ def self.commands
19
+ @@commands ||= {}
20
+ end
21
+
22
+ def self.command_aliases
23
+ @@command_aliases ||= {}
24
+ end
25
+
26
+ def self.files
27
+ @@files ||= Hash.new {|hash,key| hash[key] = File.readlines(key).map {|line| line.strip}}
28
+ end
29
+
30
+ def self.namespaces
31
+ @@namespaces ||= {}
32
+ end
33
+
34
+ def self.register_command(command)
35
+ commands[command[:command]] = command
36
+ end
37
+
38
+ def self.unregister_commands_made_private_after_the_fact
39
+ commands.values \
40
+ .select { |c| c[:klass].private_method_defined? c[:method] } \
41
+ .each { |c| commands.delete c[:command] }
42
+ end
43
+
44
+ def self.register_namespace(namespace)
45
+ namespaces[namespace[:name]] = namespace
46
+ end
47
+
48
+ def self.current_command
49
+ @current_command
50
+ end
51
+
52
+ def self.current_command=(new_current_command)
53
+ @current_command = new_current_command
54
+ end
55
+
56
+ def self.current_args
57
+ @current_args
58
+ end
59
+
60
+ def self.current_options
61
+ @current_options ||= {}
62
+ end
63
+
64
+ def self.global_options
65
+ @global_options ||= []
66
+ end
67
+
68
+ def self.invalid_arguments
69
+ @invalid_arguments
70
+ end
71
+
72
+ def self.shift_argument
73
+ # dup argument to get a non-frozen string
74
+ @invalid_arguments.shift.dup rescue nil
75
+ end
76
+
77
+ def self.validate_arguments!
78
+ unless invalid_arguments.empty?
79
+ arguments = invalid_arguments.map {|arg| "\"#{arg}\""}
80
+ if arguments.length == 1
81
+ message = "Invalid argument: #{arguments.first}"
82
+ elsif arguments.length > 1
83
+ message = "Invalid arguments: "
84
+ message << arguments[0...-1].join(", ")
85
+ message << " and "
86
+ message << arguments[-1]
87
+ end
88
+ $stderr.puts(format_with_bang(message))
89
+ run(current_command, ["--help"])
90
+ exit(1)
91
+ end
92
+ end
93
+
94
+ def self.warnings
95
+ @warnings ||= []
96
+ end
97
+
98
+ def self.display_warnings
99
+ unless warnings.empty?
100
+ $stderr.puts(warnings.uniq.map {|warning| " ! #{warning}"}.join("\n"))
101
+ end
102
+ end
103
+
104
+ def self.global_option(name, *args, &blk)
105
+ # args.sort.reverse gives -l, --long order
106
+ global_options << { :name => name.to_s, :args => args.sort.reverse, :proc => blk }
107
+ end
108
+
109
+ global_option :app, "-a", "--app APP" do |app|
110
+ raise OptionParser::InvalidOption.new(app) if app == "api" || app == "git"
111
+ end
112
+
113
+ global_option :confirm, "--confirm APP"
114
+ global_option :help, "-h", "--help"
115
+ global_option :remote, "-r", "--remote REMOTE"
116
+
117
+ def self.prepare_run(cmd, args=[])
118
+ command = parse(cmd)
119
+
120
+ if args.include?('-h') || args.include?('--help')
121
+ args.unshift(cmd) unless cmd =~ /^-.*/
122
+ cmd = 'help'
123
+ command = parse(cmd)
124
+ end
125
+
126
+ if cmd == '--version'
127
+ cmd = 'version'
128
+ command = parse(cmd)
129
+ end
130
+
131
+ @current_command = cmd
132
+ @normalized_args = []
133
+
134
+ opts = {}
135
+ invalid_options = []
136
+
137
+ parser = OptionParser.new do |parser|
138
+ # remove OptionParsers Officious['version'] to avoid conflicts
139
+ # see: https://github.com/ruby/ruby/blob/trunk/lib/optparse.rb#L814
140
+ parser.base.long.delete('version')
141
+ (global_options + (command && command[:options] || [])).each do |option|
142
+ parser.on(*option[:args]) do |value|
143
+ if option[:proc]
144
+ option[:proc].call(value)
145
+ end
146
+ opts[option[:name].gsub('-', '_').to_sym] = value
147
+ ARGV.join(' ') =~ /(#{option[:args].map {|arg| arg.split(' ', 2).first}.join('|')})/
148
+ @normalized_args << "#{option[:args].last.split(' ', 2).first} _"
149
+ end
150
+ end
151
+ end
152
+
153
+ begin
154
+ parser.order!(args) do |nonopt|
155
+ invalid_options << nonopt
156
+ @normalized_args << '!'
157
+ end
158
+ rescue OptionParser::InvalidOption => ex
159
+ invalid_options << ex.args.first
160
+ @normalized_args << '!'
161
+ retry
162
+ end
163
+
164
+ args.concat(invalid_options)
165
+
166
+ @current_args = args
167
+ @current_options = opts
168
+ @invalid_arguments = invalid_options
169
+
170
+ if command
171
+ command_instance = command[:klass].new(args.dup, opts.dup)
172
+
173
+ if !@normalized_args.include?('--app _') && (implied_app = command_instance.app rescue nil)
174
+ @normalized_args << '--app _'
175
+ end
176
+ @normalized_command = [ARGV.first, @normalized_args.sort_by {|arg| arg.gsub('-', '')}].join(' ')
177
+
178
+ [ command_instance, command[:method] ]
179
+ else
180
+ error([
181
+ "`#{cmd}` is not a pebbles command.",
182
+ "See `pebbles help` for a list of available commands."
183
+ ].compact.join("\n"))
184
+ end
185
+ end
186
+
187
+ def self.run(cmd, arguments=[])
188
+ object, method = prepare_run(cmd, arguments.dup)
189
+ object.send(method)
190
+ rescue Pebbles::API::Errors::Unauthorized => e
191
+ retry_login = handle_auth_error(e)
192
+ retry if retry_login
193
+ rescue Pebbles::API::Errors::NotFound => e
194
+ error extract_error(e.response.body) {
195
+ e.response.body =~ /^([\w\s]+ not found).?$/ ? $1 : "Resource not found"
196
+ }
197
+ rescue Pebbles::API::Errors::Locked => e
198
+ app = e.response.headers[:x_confirmation_required]
199
+ if confirm_command(app, extract_error(e.response.body))
200
+ arguments << '--confirm' << app
201
+ retry
202
+ end
203
+ rescue Pebbles::API::Errors::Timeout
204
+ error "API request timed out. Please try again."
205
+ rescue Pebbles::API::Errors::Forbidden => e
206
+ error extract_error(e.response.body)
207
+ rescue Pebbles::API::Errors::ErrorWithResponse => e
208
+ error extract_error(e.response.body)
209
+ rescue CommandFailed => e
210
+ error e.message, false
211
+ rescue OptionParser::ParseError
212
+ commands[cmd] ? run("help", [cmd]) : run("help")
213
+ rescue Excon::Errors::SocketError, SocketError => e
214
+ error("Unable to connect to Pebblescape API, please check internet connectivity and try again.")
215
+ ensure
216
+ display_warnings
217
+ end
218
+
219
+ def self.handle_auth_error(e)
220
+ if ENV['PEBBLES_API_KEY']
221
+ puts "Authentication failure with PEBBLES_API_KEY"
222
+ exit 1
223
+ else
224
+ puts "Authentication failure"
225
+ run "login"
226
+ true
227
+ end
228
+ end
229
+
230
+ def self.parse(cmd)
231
+ commands[cmd] || commands[command_aliases[cmd]]
232
+ end
233
+
234
+ def self.extract_error(body, options={})
235
+ default_error = block_given? ? yield : "Internal server error."
236
+ parse_error_json(body) || parse_error_plain(body) || default_error
237
+ end
238
+
239
+ def self.parse_error_json(body)
240
+ json = json_decode(body.to_s) rescue false
241
+ case json
242
+ when Array
243
+ json.first.join(' ') # message like [['base', 'message']]
244
+ when Hash
245
+ json['error'] || json['error_message'] || json['message'] # message like {'error' => 'message'}
246
+ else
247
+ nil
248
+ end
249
+ end
250
+
251
+ def self.parse_error_plain(body)
252
+ return unless body.respond_to?(:headers) && body.headers[:content_type].to_s.include?("text/plain")
253
+ body.to_s
254
+ end
255
+ end
256
+ end