forward 0.0.1 → 0.0.11
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.
- data/README.md +7 -0
- data/Rakefile +6 -1
- data/bin/forward +6 -0
- data/forward.gemspec +12 -4
- data/forwardhq.crt +112 -0
- data/lib/forward.rb +86 -0
- data/lib/forward/api.rb +45 -0
- data/lib/forward/api/client_log.rb +28 -0
- data/lib/forward/api/public_key.rb +16 -0
- data/lib/forward/api/resource.rb +121 -0
- data/lib/forward/api/tunnel.rb +91 -0
- data/lib/forward/api/user.rb +19 -0
- data/lib/forward/cli.rb +238 -0
- data/lib/forward/client.rb +92 -0
- data/lib/forward/config.rb +163 -0
- data/lib/forward/core_extensions.rb +12 -0
- data/lib/forward/error.rb +10 -0
- data/lib/forward/tunnel.rb +58 -0
- data/lib/forward/version.rb +1 -1
- data/test/api/public_key_test.rb +28 -0
- data/test/api/resource_test.rb +82 -0
- data/test/api/tunnel_test.rb +75 -0
- data/test/api/user_test.rb +28 -0
- data/test/api_test.rb +0 -0
- data/test/cli_test.rb +84 -0
- data/test/client_test.rb +8 -0
- data/test/config_test.rb +102 -0
- data/test/test_helper.rb +40 -0
- data/test/tunnel_test.rb +8 -0
- metadata +156 -9
@@ -0,0 +1,91 @@
|
|
1
|
+
module Forward
|
2
|
+
module Api
|
3
|
+
class Tunnel < Resource
|
4
|
+
|
5
|
+
def self.create(options = {})
|
6
|
+
resource = Tunnel.new(:create)
|
7
|
+
resource.uri = '/api/tunnels'
|
8
|
+
params = {
|
9
|
+
:hostport => options[:port],
|
10
|
+
:client => Forward.client_string,
|
11
|
+
}
|
12
|
+
|
13
|
+
[ :subdomain, :cname, :username, :password ].each do |param|
|
14
|
+
params[param] = options[param] unless options[param].nil?
|
15
|
+
end
|
16
|
+
|
17
|
+
resource.post(params)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.index
|
21
|
+
resource = Tunnel.new(:index)
|
22
|
+
resource.uri = "/api/tunnels"
|
23
|
+
|
24
|
+
resource.get
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.destroy(id)
|
28
|
+
resource = Tunnel.new(:destroy)
|
29
|
+
resource.uri = "/api/tunnels/#{id}"
|
30
|
+
|
31
|
+
resource.delete
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.show(id)
|
35
|
+
resource = Tunnel.new(:show)
|
36
|
+
resource.uri = "/api/tunnels/#{id}"
|
37
|
+
|
38
|
+
resource.get
|
39
|
+
rescue Forward::Api::ResourceNotFound
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def self.ask_to_destroy(message)
|
46
|
+
tunnels = index
|
47
|
+
|
48
|
+
puts message
|
49
|
+
choose do |menu|
|
50
|
+
menu.prompt = "Choose a tunnel from the list to close or `q' to exit forward "
|
51
|
+
|
52
|
+
tunnels.each do |tunnel|
|
53
|
+
text = "tunnel forwarding port #{tunnel['hostport']}"
|
54
|
+
menu.choice(text) { destroy_and_create(tunnel['_id']) }
|
55
|
+
end
|
56
|
+
menu.hidden('quit') { Forward::Client.cleanup_and_exit! }
|
57
|
+
menu.hidden('exit') { Forward::Client.cleanup_and_exit! }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.destroy_and_create(id)
|
62
|
+
Forward.log(:debug, "Destroying tunnel: #{id}")
|
63
|
+
destroy(id)
|
64
|
+
puts "tunnel removed, creating your new forward"
|
65
|
+
create
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.create_error(errors)
|
69
|
+
base_errors = errors['base']
|
70
|
+
|
71
|
+
if base_errors && base_errors.any? { |e| e.include? 'limit' }
|
72
|
+
message = base_errors.select { |e| e.include? 'limit' }.first
|
73
|
+
Forward.log(:debug, 'Tunnel limit reached')
|
74
|
+
ask_to_destroy(message)
|
75
|
+
else
|
76
|
+
message = "An error occured creating your tunnel: \n"
|
77
|
+
errors.each do |key, value|
|
78
|
+
message << " #{key} #{value.join(', ')}" if key != 'base'
|
79
|
+
end
|
80
|
+
Forward::Client.cleanup_and_exit!(message)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.destroy_error(errors)
|
85
|
+
# TODO: this is where we will tie into the logger
|
86
|
+
nil
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Forward
|
2
|
+
module Api
|
3
|
+
class User < Resource
|
4
|
+
|
5
|
+
def self.api_token(email, password)
|
6
|
+
resource = User.new(:api_token)
|
7
|
+
resource.uri = '/api/users/api_token'
|
8
|
+
params = { :email => email, :password => password }
|
9
|
+
|
10
|
+
resource.post(params)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.api_token_error(errors)
|
14
|
+
Forward::Client.cleanup_and_exit!('Unable to authenticate with email and password')
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/forward/cli.rb
ADDED
@@ -0,0 +1,238 @@
|
|
1
|
+
module Forward
|
2
|
+
class CLI
|
3
|
+
CNAME_REGEX = /\A[a-z0-9]+(?:[\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}\z/i
|
4
|
+
SUBDOMAIN_REGEX = /\A[a-z0-9]{1}[a-z0-9\-]+\z/i
|
5
|
+
BANNER = <<-BANNER
|
6
|
+
Usage: forward <port> [options]
|
7
|
+
forward <host> [options]
|
8
|
+
forward <host:port> [options]
|
9
|
+
|
10
|
+
Description:
|
11
|
+
|
12
|
+
Share a server running on localhost:port over the web by tunneling
|
13
|
+
through Forward. A URL is created for each tunnel.
|
14
|
+
|
15
|
+
Simple example:
|
16
|
+
|
17
|
+
# You are developing a Rails site.
|
18
|
+
|
19
|
+
> rails server &
|
20
|
+
> forward 3000
|
21
|
+
Forward created at https://4ad3f-mycompany.fwd.wf
|
22
|
+
|
23
|
+
Assigning a subdomain:
|
24
|
+
|
25
|
+
> rails server &
|
26
|
+
> forward 3000 myapp
|
27
|
+
Forward created at https://myapp-mycompany.fwd.wf
|
28
|
+
|
29
|
+
Virtual Host example:
|
30
|
+
|
31
|
+
# You are already running something on port 80 that uses
|
32
|
+
# virtual host names.
|
33
|
+
|
34
|
+
> forward mysite.dev
|
35
|
+
Forward created at https://dh43a-mycompany.fwd.wf
|
36
|
+
|
37
|
+
BANNER
|
38
|
+
|
39
|
+
# Parse non-published options and remove them from ARGV, then
|
40
|
+
# parse published options and update the @options Hash with provided
|
41
|
+
# options and removes switches from ARGV.
|
42
|
+
def self.parse_options
|
43
|
+
Forward.log(:debug, "Parsing options")
|
44
|
+
@options = {
|
45
|
+
:host => '0.0.0.0',
|
46
|
+
:port => 80,
|
47
|
+
:random => true
|
48
|
+
}
|
49
|
+
|
50
|
+
if ARGV.include?('--debug')
|
51
|
+
Forward.debug!
|
52
|
+
ARGV.delete('--debug')
|
53
|
+
elsif ARGV.include?('--debug-remotely')
|
54
|
+
Forward.debug_remotely!
|
55
|
+
ARGV.delete('--debug-remotely')
|
56
|
+
end
|
57
|
+
|
58
|
+
@opts = OptionParser.new do |opts|
|
59
|
+
opts.banner = BANNER.gsub(/^ {6}/, '')
|
60
|
+
|
61
|
+
opts.separator ''
|
62
|
+
opts.separator 'Options:'
|
63
|
+
|
64
|
+
opts.on('-a', '--auth [USER:PASS]', 'Protect this tunnel with HTTP Basic Auth.') do |credentials|
|
65
|
+
username, password = parse_basic_auth(credentials)
|
66
|
+
@options[:username] = username
|
67
|
+
@options[:password] = password
|
68
|
+
end
|
69
|
+
|
70
|
+
opts.on('-c', '--cname [CNAME]', 'Allow access to this tunnel as NAME.') do |cname|
|
71
|
+
validate_cname(cname)
|
72
|
+
@options[:cname] = cname
|
73
|
+
end
|
74
|
+
|
75
|
+
opts.on( '-h', '--help', 'Display this help.' ) do
|
76
|
+
puts opts
|
77
|
+
exit
|
78
|
+
end
|
79
|
+
|
80
|
+
opts.on('-v', '--version', 'Display version number.') do
|
81
|
+
puts "forward #{VERSION}"
|
82
|
+
exit
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
@opts.parse!
|
87
|
+
end
|
88
|
+
|
89
|
+
# Attempts to validate the basic auth credentials, if successful updates
|
90
|
+
# the @options Hash with given credentials.
|
91
|
+
#
|
92
|
+
# credentials - A String containing a username and password separated
|
93
|
+
# by a colon.
|
94
|
+
#
|
95
|
+
# Returns an Array containing the username and password
|
96
|
+
def self.parse_basic_auth(credentials)
|
97
|
+
validate_basic_auth(credentials)
|
98
|
+
username, password = credentials.split(':')
|
99
|
+
|
100
|
+
[ username, password ]
|
101
|
+
end
|
102
|
+
|
103
|
+
# Parses the arguments to determine if we're forwarding a port or host
|
104
|
+
# and validates the port or host and updates @options if valid.
|
105
|
+
#
|
106
|
+
# arg - A String representing the port or host.
|
107
|
+
#
|
108
|
+
# Returns a Hash containing the forwarded host or port
|
109
|
+
def self.parse_forwarded(arg)
|
110
|
+
Forward.log(:debug, "Forwarded: `#{arg}'")
|
111
|
+
forwarded = {}
|
112
|
+
|
113
|
+
if arg =~ /\A\d{1,5}\z/
|
114
|
+
port = arg.to_i
|
115
|
+
validate_port(port)
|
116
|
+
|
117
|
+
forwarded[:port] = port
|
118
|
+
elsif arg =~ /\A[-a-z0-9\.\-]+\z/i
|
119
|
+
forwarded[:host] = arg
|
120
|
+
elsif arg =~ /\A[-a-z0-9\.\-]+:\d{1,5}\z/i
|
121
|
+
host, port = arg.split(':')
|
122
|
+
port = port.to_i
|
123
|
+
validate_port(port)
|
124
|
+
|
125
|
+
forwarded[:host] = host
|
126
|
+
forwarded[:port] = port
|
127
|
+
end
|
128
|
+
|
129
|
+
forwarded
|
130
|
+
end
|
131
|
+
|
132
|
+
# Checks to make sure the port being set is a number between 1 and 65535
|
133
|
+
# and exits with an error message if it's not.
|
134
|
+
#
|
135
|
+
# port - A String containing the port number.
|
136
|
+
def self.validate_port(port)
|
137
|
+
Forward.log(:debug, "Validating Port: `#{port}'")
|
138
|
+
unless port.between?(1, 65535)
|
139
|
+
exit_with_error "Invalid Port: #{port} is an invalid port number"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Checks to make sure the basic auth credentials are in the correct format
|
144
|
+
# and exits with an error message if they're not.
|
145
|
+
#
|
146
|
+
# credentials - A String with the username and password for basic auth.
|
147
|
+
def self.validate_basic_auth(credentials)
|
148
|
+
Forward.log(:debug, "Validating Basic Auth: `#{credentials}'")
|
149
|
+
if credentials !~ /\A[^\s:]+:[^\s:]+\z/
|
150
|
+
exit_with_error "Basic Auth: bad format, expecting USER:PASS"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Checks to make sure the cname is in the correct format and exits with an
|
155
|
+
# error message if it isn't.
|
156
|
+
#
|
157
|
+
# cname - A String containing the cname.
|
158
|
+
def self.validate_cname(cname)
|
159
|
+
Forward.log(:debug, "Validating CNAME: `#{cname}'")
|
160
|
+
exit_with_error("`#{cname}' is an invalid domain format") unless cname =~ CNAME_REGEX
|
161
|
+
end
|
162
|
+
|
163
|
+
# Validates the subdomain and returns a Hash containing it and also
|
164
|
+
# sets random to false, if validation passes.
|
165
|
+
#
|
166
|
+
# subdomain - A String containing the subdomain.
|
167
|
+
#
|
168
|
+
# Returns a Hash containing the subdomain
|
169
|
+
def self.parse_subdomain(subdomain)
|
170
|
+
validate_subdomain(subdomain)
|
171
|
+
{ :subdomain => subdomain, :random => false }
|
172
|
+
end
|
173
|
+
|
174
|
+
# Checks to make sure the subdomain is in the correct format and exits with an
|
175
|
+
# error message if it isn't.
|
176
|
+
#
|
177
|
+
# cname - A String containing the subdomain.
|
178
|
+
def self.validate_subdomain(subdomain)
|
179
|
+
Forward.log(:debug, "Validating Subdomain: `#{subdomain}'")
|
180
|
+
exit_with_error("`#{subdomain}' is an invalid subdomain format") unless subdomain =~ SUBDOMAIN_REGEX
|
181
|
+
end
|
182
|
+
|
183
|
+
# Checks to see if the cname and random options are both set at the same.
|
184
|
+
# time and exits with an error message if they are.
|
185
|
+
def self.validate_options
|
186
|
+
Forward.log(:debug, "Validating options: `#{@options.inspect}'")
|
187
|
+
if !@options[:cname].nil? && @options[:random]
|
188
|
+
exit_with_error "You can't use a cname and a random url"
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# Asks for the user's email and password and puts them in a Hash.
|
193
|
+
#
|
194
|
+
# Returns a Hash with the email and password.
|
195
|
+
def self.authenticate
|
196
|
+
if ask('Already have an account with Forward? ').chomp =~ /\An/i
|
197
|
+
Client.cleanup_and_exit!("You'll need a Forward account first. You can create one at \033[04mhttps://forwardhq.com\033[04m")
|
198
|
+
else
|
199
|
+
puts 'Enter your email and password'
|
200
|
+
email = ask('email: ').chomp
|
201
|
+
password = ask('password: ') { |q| q.echo = false }.chomp
|
202
|
+
Forward.log(:debug, "Authenticating User: `#{email}:#{password.gsub(/./, 'x')}'")
|
203
|
+
|
204
|
+
{ :email => email, :password => password }
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# Parses various options and arguments, validates everything to ensure
|
209
|
+
# we're safe to proceed, and finally passes @options to the Client.
|
210
|
+
def self.run(args)
|
211
|
+
Forward.log(:debug, "Starting forward v#{Forward::VERSION}")
|
212
|
+
parse_options
|
213
|
+
@options.merge!(parse_forwarded(args[0]))
|
214
|
+
@options.merge!(parse_subdomain(args[1])) if args.length > 1
|
215
|
+
validate_options
|
216
|
+
|
217
|
+
print_usage_and_exit if args.empty?
|
218
|
+
|
219
|
+
Client.start(@options)
|
220
|
+
end
|
221
|
+
|
222
|
+
# Colors an error message red and displays it.
|
223
|
+
#
|
224
|
+
# message - A String containing an error message.
|
225
|
+
def self.exit_with_error(message)
|
226
|
+
Forward.log(:fatal, message)
|
227
|
+
puts "\033[31m#{message}\033[0m"
|
228
|
+
exit 1
|
229
|
+
end
|
230
|
+
|
231
|
+
# Print the usage banner and Exit Code 0.
|
232
|
+
def self.print_usage_and_exit
|
233
|
+
puts @opts
|
234
|
+
exit
|
235
|
+
end
|
236
|
+
|
237
|
+
end
|
238
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Forward
|
2
|
+
class Client
|
3
|
+
attr_reader :config
|
4
|
+
attr_reader :options
|
5
|
+
attr_accessor :tunnel
|
6
|
+
|
7
|
+
def initialize(options = {})
|
8
|
+
@options = options
|
9
|
+
@config = Config.create_or_load
|
10
|
+
end
|
11
|
+
|
12
|
+
def basic_auth?
|
13
|
+
@options[:username] && @options[:password]
|
14
|
+
end
|
15
|
+
|
16
|
+
# Sets up a Tunnel instance and adds it to the Client.
|
17
|
+
def setup_tunnel
|
18
|
+
Forward.log(:debug, 'Setting up tunnel')
|
19
|
+
@tunnel = Forward::Tunnel.new(self.options)
|
20
|
+
if @tunnel.id
|
21
|
+
@tunnel.poll_status
|
22
|
+
else
|
23
|
+
cleanup_and_exit!('Unable to create a tunnel. If this continues contact support@forwardhq.com')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# The options Hash used by Net::SSH.
|
28
|
+
#
|
29
|
+
# Returns a Hash of options.
|
30
|
+
def ssh_options
|
31
|
+
{
|
32
|
+
:port => Forward.ssh_port,
|
33
|
+
:keys_only => true,
|
34
|
+
:keys => [],
|
35
|
+
:key_data => [ @config.private_key ],
|
36
|
+
:encryption => 'blowfish-cbc'
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.current
|
41
|
+
@client
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.watch_session(session)
|
45
|
+
@client.tunnel.inactive_for = 0 if session.busy?(true)
|
46
|
+
true
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.start(options = {})
|
50
|
+
Forward.log(:debug, 'Starting client')
|
51
|
+
trap(:INT) { cleanup_and_exit!('closing tunnel and exiting...') }
|
52
|
+
|
53
|
+
@client = Client.new(options)
|
54
|
+
|
55
|
+
@client.setup_tunnel
|
56
|
+
|
57
|
+
Forward.log(:debug, "Starting remote forward on https://#{@client.tunnel.subdomain}.fwd.wf on port #{@client.tunnel.port}")
|
58
|
+
puts "Forwarding port #{@client.options[:port]} at \033[04mhttps://#{@client.tunnel.subdomain}.fwd.wf\033[m\nCtrl-C to stop forwarding"
|
59
|
+
Net::SSH.start(@client.tunnel.tunneler, Forward.ssh_user, @client.ssh_options) do |session|
|
60
|
+
session.forward.remote(@client.options[:port], @client.options[:host], @client.tunnel.port)
|
61
|
+
session.loop { watch_session(session) }
|
62
|
+
end
|
63
|
+
|
64
|
+
# cleanup_and_exit!
|
65
|
+
rescue Net::SSH::AuthenticationFailed => e
|
66
|
+
Forward.log(:fatal, "SSH Auth failed `#{e}'")
|
67
|
+
cleanup_and_exit!("Authentication failed, try deleting `#{Forward::Config::CONFIG_PATH}' and giving it another go. If the problem continues, contact support@forwardhq.com")
|
68
|
+
rescue => e
|
69
|
+
Forward.log(:fatal, "#{e.message}\n#{e.backtrace.join("\n")}")
|
70
|
+
cleanup_and_exit!("You've been disconnected...")
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.cleanup_and_exit!(message = 'exiting...')
|
74
|
+
puts message
|
75
|
+
if @client && @client.tunnel && @client.tunnel.id
|
76
|
+
Forward.log(:debug, "Cleaning up tunnel: `#{@client.tunnel.id}'")
|
77
|
+
@client.tunnel.cleanup
|
78
|
+
@client.tunnel = nil
|
79
|
+
end
|
80
|
+
|
81
|
+
Forward.log(:debug, 'Exiting')
|
82
|
+
send_debug_log if Forward.debug_remotely?
|
83
|
+
ensure
|
84
|
+
Thread.main.exit
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.send_debug_log
|
88
|
+
log = Forward.stringio_log.string
|
89
|
+
Forward::Api::ClientLog.create(log)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
module Forward
|
2
|
+
class Config
|
3
|
+
CONFIG_PATH = File.join(ENV['HOME'], '.forward')
|
4
|
+
CIPHER_LENGTH = 128
|
5
|
+
CIPHER_MODE = :CBC
|
6
|
+
DELIMETER = '-------M-------'
|
7
|
+
|
8
|
+
attr_accessor :id
|
9
|
+
attr_accessor :api_token
|
10
|
+
attr_accessor :private_key
|
11
|
+
|
12
|
+
# Initializes a Config object with the given attributes.
|
13
|
+
#
|
14
|
+
# attributes - A Hash of attributes
|
15
|
+
#
|
16
|
+
# Returns the new Config object.
|
17
|
+
def initialize(attributes = {})
|
18
|
+
attributes.each do |key, value|
|
19
|
+
self.send(:"#{key}=", value)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Updates an existing Config object.
|
24
|
+
#
|
25
|
+
# Returns the updated Config object.
|
26
|
+
def update(attributes)
|
27
|
+
Forward.log(:debug, 'Updating Config')
|
28
|
+
attributes.each do |key, value|
|
29
|
+
self.send(:"#{key}=", value)
|
30
|
+
end
|
31
|
+
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
# Converts a Config object to a Hash.
|
36
|
+
#
|
37
|
+
# Returns a Hash representation of the object
|
38
|
+
def to_hash
|
39
|
+
Hash[instance_variables.map { |var| [var[1..-1].to_sym, instance_variable_get(var)] }]
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
#
|
44
|
+
#
|
45
|
+
def validate
|
46
|
+
Forward.log(:debug, 'Validating Config')
|
47
|
+
attributes = [:id, :api_token, :private_key]
|
48
|
+
errors = []
|
49
|
+
|
50
|
+
attributes.each do |attribute|
|
51
|
+
value = instance_variable_get("@#{attribute}")
|
52
|
+
|
53
|
+
errors << attribute if value.nil? || value.to_s.empty?
|
54
|
+
end
|
55
|
+
|
56
|
+
if errors.length == 1
|
57
|
+
raise ConfigError, "#{errors.first} is a required field"
|
58
|
+
elsif errors.length >= 2
|
59
|
+
raise ConfigError, "#{errors.join(', ')} are required fields"
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
#
|
65
|
+
#
|
66
|
+
#
|
67
|
+
def write
|
68
|
+
Forward.log(:debug, 'Writing Config')
|
69
|
+
self.validate
|
70
|
+
|
71
|
+
data = JSON.dump(self.to_hash)
|
72
|
+
cipher = OpenSSL::Cipher::AES.new(CIPHER_LENGTH, CIPHER_MODE)
|
73
|
+
cipher.encrypt
|
74
|
+
|
75
|
+
key = cipher.random_key
|
76
|
+
iv = cipher.random_iv
|
77
|
+
|
78
|
+
encrypted_data = cipher.update(data) + cipher.final
|
79
|
+
config_data = Base64.encode64(encrypted_data)
|
80
|
+
config_data << DELIMETER
|
81
|
+
config_data << Base64.encode64("#{key}::#{iv}")
|
82
|
+
|
83
|
+
File.open(CONFIG_PATH, 'w') { |f| f.write(config_data) }
|
84
|
+
|
85
|
+
self
|
86
|
+
end
|
87
|
+
|
88
|
+
#
|
89
|
+
#
|
90
|
+
#
|
91
|
+
def self.read
|
92
|
+
Forward.log(:debug, 'Reading Config')
|
93
|
+
config_data = File.read(CONFIG_PATH)
|
94
|
+
encrypted_data, keys = config_data.split(DELIMETER).map { |d| Base64.decode64(d) }
|
95
|
+
key, iv = keys.split('::')
|
96
|
+
decipher = OpenSSL::Cipher::AES.new(CIPHER_LENGTH, CIPHER_MODE)
|
97
|
+
|
98
|
+
decipher.decrypt
|
99
|
+
|
100
|
+
decipher.key = key
|
101
|
+
decipher.iv = iv
|
102
|
+
|
103
|
+
unencrypted = decipher.update(encrypted_data) + decipher.final
|
104
|
+
|
105
|
+
JSON.parse(unencrypted).symbolize_keys
|
106
|
+
rescue
|
107
|
+
raise ConfigError, 'Unable to read config file'
|
108
|
+
end
|
109
|
+
|
110
|
+
# Checks to see if a .forward config file exists.
|
111
|
+
#
|
112
|
+
# Returns true or false based on the existence of the config file.
|
113
|
+
def self.present?
|
114
|
+
File.exist? CONFIG_PATH
|
115
|
+
end
|
116
|
+
|
117
|
+
def self.create_or_load
|
118
|
+
if Config.present?
|
119
|
+
Config.load
|
120
|
+
else
|
121
|
+
Config.create
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def self.create
|
126
|
+
Forward.log(:debug, 'Creating Config')
|
127
|
+
config = Config.new
|
128
|
+
email, password = CLI.authenticate.values_at(:email, :password)
|
129
|
+
config.update(Forward::Api::User.api_token(email, password))
|
130
|
+
Forward::Api.token = config.api_token
|
131
|
+
config.private_key = Forward::Api::PublicKey.create
|
132
|
+
|
133
|
+
config.write
|
134
|
+
end
|
135
|
+
|
136
|
+
# It initializes a new Config instance, updates it with the config values
|
137
|
+
# from the config file, and raises an error if there's not a config or if
|
138
|
+
# the config options aren't valid.
|
139
|
+
#
|
140
|
+
# Returns the Config instance.
|
141
|
+
def self.load
|
142
|
+
Forward.log(:debug, 'Loading Config')
|
143
|
+
config = Config.new
|
144
|
+
|
145
|
+
if Config.present?
|
146
|
+
config.update(Config.read)
|
147
|
+
end
|
148
|
+
|
149
|
+
config.validate
|
150
|
+
|
151
|
+
Forward::Api.token = config.api_token;
|
152
|
+
|
153
|
+
config
|
154
|
+
end
|
155
|
+
|
156
|
+
# Deletes the .forward config file if it exists.
|
157
|
+
def self.clear
|
158
|
+
Forward.log(:debug, 'Clearning Config')
|
159
|
+
File.delete(CONFIG_PATH) if Config.present?
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
end
|