pebblescape 0.0.1

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