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