forward 0.3.3 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/Gemfile +0 -2
- data/README.md +24 -0
- data/Rakefile +3 -1
- data/bin/forward +1 -1
- data/forward.gemspec +17 -11
- data/lib/forward/api/resource.rb +51 -83
- data/lib/forward/api/tunnel.rb +41 -68
- data/lib/forward/api/user.rb +14 -11
- data/lib/forward/api.rb +7 -26
- data/lib/forward/cli.rb +55 -253
- data/lib/forward/command/account.rb +69 -0
- data/lib/forward/command/base.rb +62 -0
- data/lib/forward/command/config.rb +64 -0
- data/lib/forward/command/tunnel.rb +178 -0
- data/lib/forward/common.rb +44 -0
- data/lib/forward/config.rb +75 -118
- data/lib/forward/request.rb +72 -0
- data/lib/forward/socket.rb +125 -0
- data/lib/forward/static/app.rb +157 -0
- data/lib/forward/static/directory.erb +142 -0
- data/lib/forward/tunnel.rb +102 -40
- data/lib/forward/version.rb +1 -1
- data/lib/forward.rb +80 -63
- data/test/api/resource_test.rb +70 -54
- data/test/api/tunnel_test.rb +50 -51
- data/test/api/user_test.rb +33 -20
- data/test/cli_test.rb +0 -126
- data/test/command/account_test.rb +26 -0
- data/test/command/tunnel_test.rb +133 -0
- data/test/config_test.rb +103 -54
- data/test/forward_test.rb +47 -0
- data/test/test_helper.rb +35 -26
- data/test/tunnel_test.rb +50 -22
- metadata +210 -169
- data/forwardhq.crt +0 -112
- data/lib/forward/api/client_log.rb +0 -20
- data/lib/forward/api/tunnel_key.rb +0 -18
- data/lib/forward/client.rb +0 -110
- data/lib/forward/error.rb +0 -12
- data/test/api/tunnel_key_test.rb +0 -28
- data/test/api_test.rb +0 -0
- data/test/client_test.rb +0 -8
@@ -0,0 +1,178 @@
|
|
1
|
+
module Forward
|
2
|
+
module Command
|
3
|
+
class Tunnel < Base
|
4
|
+
BASIC_AUTH_REGEX = /\A[^\s:]+:[^\s:]+\z/i.freeze
|
5
|
+
CNAME_REGEX = /\A[a-z0-9]+(?:[\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}\z/i.freeze
|
6
|
+
SUBDOMAIN_PREFIX_REGEX = /\A[a-z0-9]{1}[a-z0-9\-]+\z/i.freeze
|
7
|
+
STATIC_HOST = '127.0.0.1'.freeze
|
8
|
+
STATIC_PORT = 32645.freeze
|
9
|
+
DEFAULT_OPTIONS = {
|
10
|
+
host: '127.0.0.1',
|
11
|
+
port: 80
|
12
|
+
}
|
13
|
+
|
14
|
+
def start
|
15
|
+
print_help_and_exit! if !forwardfile? && @args.empty?
|
16
|
+
|
17
|
+
config.create_or_load
|
18
|
+
@options.merge!(DEFAULT_OPTIONS)
|
19
|
+
parse_forwardfile
|
20
|
+
parse_forwarded
|
21
|
+
|
22
|
+
@options[:subdomain_prefix] = @args[1] if @args.length > 1
|
23
|
+
@options[:no_auth] = @options.fetch(:'no-auth', nil)
|
24
|
+
|
25
|
+
validate :port, :auth, :cname, :subdomain_prefix
|
26
|
+
|
27
|
+
client do
|
28
|
+
authenticate_user do
|
29
|
+
start_static_server if @options[:static_path]
|
30
|
+
open_tunnel
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def authenticate_user(&block)
|
38
|
+
return block.call if logged_in?
|
39
|
+
|
40
|
+
email, password = ask_for_credentials(email)
|
41
|
+
|
42
|
+
logger.debug("[API] authenticating user: `#{email}:#{password.gsub(/./, 'x')}'")
|
43
|
+
|
44
|
+
API::User.authenticate(email, password) do |subdomain, token|
|
45
|
+
config.add_account(subdomain, token)
|
46
|
+
block.call
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def open_tunnel
|
51
|
+
API::Tunnel.create(@options) do |attributes|
|
52
|
+
Forward.tunnel = Forward::Tunnel.new(@options.merge(attributes))
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def start_static_server
|
57
|
+
require 'thin'
|
58
|
+
require 'forward/static/app'
|
59
|
+
static_path = @options[:static_path]
|
60
|
+
Thin::Logging.silent = true
|
61
|
+
logger.debug "[static] starting server on port: #{STATIC_PORT}"
|
62
|
+
Thin::Server.start(STATIC_HOST, STATIC_PORT) { run Static::App.new(static_path) }
|
63
|
+
end
|
64
|
+
|
65
|
+
def forwardfile?
|
66
|
+
@has_forwardfile ||= File.exist?(forwardfile_path)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns a String file path for PWD/Forwardfile
|
70
|
+
def forwardfile_path
|
71
|
+
@forwardfile_path ||= File.join(Dir.pwd, 'Forwardfile')
|
72
|
+
end
|
73
|
+
|
74
|
+
def parse_forwardfile
|
75
|
+
return unless forwardfile?
|
76
|
+
|
77
|
+
options = YAML.load_file(forwardfile_path)
|
78
|
+
|
79
|
+
raise CLIError unless options.kind_of?(Hash)
|
80
|
+
|
81
|
+
logger.debug "[CLI] read Forwardfile `#{forwardfile_path}' as: `#{options.inspect}'"
|
82
|
+
|
83
|
+
@options.merge!(options.symbolize_keys)
|
84
|
+
rescue ArgumentError, SyntaxError, CLIError
|
85
|
+
exit_with_error "Unable to parse Forwardfile at `#{forwardfile_path}'"
|
86
|
+
end
|
87
|
+
|
88
|
+
# Parses the arguments to determine if we're forwarding a port or host
|
89
|
+
# and validates the port or host and updates @options if valid.
|
90
|
+
#
|
91
|
+
# arg - A String representing the port or host.
|
92
|
+
#
|
93
|
+
# Returns a Hash containing the forwarded host or port
|
94
|
+
def parse_forwarded
|
95
|
+
return if @args.empty?
|
96
|
+
|
97
|
+
forwarded = @args[0]
|
98
|
+
logger.debug "[CLI] forwarded: `#{forwarded}'"
|
99
|
+
|
100
|
+
if Dir.exist?(forwarded)
|
101
|
+
@options[:static_path] = forwarded
|
102
|
+
@options[:host] = STATIC_HOST
|
103
|
+
@options[:port] = STATIC_PORT
|
104
|
+
return
|
105
|
+
end
|
106
|
+
|
107
|
+
case forwarded
|
108
|
+
when /\A\d{1,5}\z/
|
109
|
+
@options[:port] = forwarded.to_i
|
110
|
+
when /\A[-a-z0-9\.\-]+\z/i
|
111
|
+
@options[:host] = forwarded
|
112
|
+
when /\A[-a-z0-9\.\-]+:\d{1,5}\z/i
|
113
|
+
host, port = forwarded.split(':')
|
114
|
+
port = port.to_i
|
115
|
+
|
116
|
+
@options[:host] = host
|
117
|
+
@options[:port] = port
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Checks to make sure the port being set is a number between 1 and 65535
|
122
|
+
# and exits with an error message if it's not.
|
123
|
+
#
|
124
|
+
# port - port number Integer
|
125
|
+
def validate_port
|
126
|
+
port = @options[:port]
|
127
|
+
|
128
|
+
logger.debug "[CLI] validating port: `#{port}'"
|
129
|
+
unless port.between?(1, 65535)
|
130
|
+
raise CLIError, "Invalid Port: #{port} is an invalid port number"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def validate_auth
|
135
|
+
auth = @options[:auth]
|
136
|
+
|
137
|
+
return if auth.nil?
|
138
|
+
|
139
|
+
logger.debug "[CLI] validating auth: `#{auth}'"
|
140
|
+
if auth =~ BASIC_AUTH_REGEX
|
141
|
+
@options[:username], @options[:password] = @options[:auth].split(':')
|
142
|
+
else
|
143
|
+
raise CLIError, "`#{auth}' isn't a valid username:password pair"
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Checks to make sure the cname is in the correct format and exits with an
|
148
|
+
# error message if it isn't.
|
149
|
+
#
|
150
|
+
# cname - cname String
|
151
|
+
def validate_cname
|
152
|
+
cname = @options[:cname]
|
153
|
+
|
154
|
+
return if cname.nil?
|
155
|
+
|
156
|
+
logger.debug "[CLI] validating CNAME: `#{cname}'"
|
157
|
+
unless cname =~ CNAME_REGEX
|
158
|
+
raise CLIError, "`#{cname}' is an invalid domain format"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Checks to make sure the subdomain prefix is in the correct format
|
163
|
+
# and exits with an error message if it isn't.
|
164
|
+
#
|
165
|
+
# prefix - subdomain prefix String
|
166
|
+
def validate_subdomain_prefix
|
167
|
+
prefix = @options[:subdomain_prefix]
|
168
|
+
|
169
|
+
return if prefix.nil?
|
170
|
+
|
171
|
+
logger.debug "[CLI] validating subdomain prefix: `#{prefix}'"
|
172
|
+
unless prefix =~ SUBDOMAIN_PREFIX_REGEX
|
173
|
+
raise CLIError, "`#{prefix}' is an invalid subdomain prefix format"
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Forward
|
2
|
+
module Common
|
3
|
+
EMAIL_REGEX = /\A[^@]+@[^@]+\.[^@]+\z/.freeze
|
4
|
+
|
5
|
+
def logger
|
6
|
+
Forward.logger
|
7
|
+
end
|
8
|
+
alias_method :log, :logger
|
9
|
+
|
10
|
+
def config
|
11
|
+
Forward::Config
|
12
|
+
end
|
13
|
+
|
14
|
+
def logged_in?
|
15
|
+
!Config.default_account.nil?
|
16
|
+
end
|
17
|
+
|
18
|
+
def os
|
19
|
+
Forward.os
|
20
|
+
end
|
21
|
+
|
22
|
+
def windows?
|
23
|
+
Forward.windows?
|
24
|
+
end
|
25
|
+
|
26
|
+
def exit_with_message(message = nil)
|
27
|
+
puts message if message
|
28
|
+
stop_reactor_and_exit
|
29
|
+
end
|
30
|
+
|
31
|
+
def stop_reactor_and_exit(code = 0)
|
32
|
+
EM.stop if EM.reactor_running?
|
33
|
+
exit(code)
|
34
|
+
end
|
35
|
+
|
36
|
+
def exit_with_error(message = nil)
|
37
|
+
if message
|
38
|
+
puts HighLine.color(message, :red)
|
39
|
+
end
|
40
|
+
stop_reactor_and_exit(1)
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
data/lib/forward/config.rb
CHANGED
@@ -1,30 +1,71 @@
|
|
1
|
-
require 'fileutils'
|
2
|
-
require 'yaml'
|
3
|
-
|
4
1
|
module Forward
|
5
|
-
|
6
|
-
|
2
|
+
module Config
|
3
|
+
extend self
|
4
|
+
DEFAULTS = {
|
5
|
+
accounts: {},
|
6
|
+
default_account: nil,
|
7
|
+
auto_copy: true,
|
8
|
+
auto_open: false
|
9
|
+
}.freeze
|
10
|
+
|
11
|
+
attr_accessor :accounts
|
12
|
+
attr_accessor :default_account
|
13
|
+
attr_accessor :auto_copy
|
14
|
+
attr_accessor :auto_open
|
15
|
+
|
16
|
+
def set_defaults!
|
17
|
+
DEFAULTS.keys.each { |setting| set_default!(setting) }
|
18
|
+
end
|
7
19
|
|
8
|
-
|
9
|
-
|
10
|
-
|
20
|
+
def set_default!(setting)
|
21
|
+
value = DEFAULTS[setting.to_sym]
|
22
|
+
value = value.dup if value.is_a?(Hash) || value.is_a?(Array)
|
23
|
+
self.send("#{setting}=", value)
|
11
24
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
25
|
+
value
|
26
|
+
end
|
27
|
+
|
28
|
+
def set_default_account(subdomain)
|
29
|
+
self.default_account = subdomain
|
30
|
+
|
31
|
+
write
|
32
|
+
end
|
33
|
+
|
34
|
+
def add_account(subdomain, api_token)
|
35
|
+
accounts[subdomain.to_sym] = api_token
|
36
|
+
self.default_account = subdomain
|
37
|
+
|
38
|
+
write
|
39
|
+
end
|
40
|
+
|
41
|
+
def remove_account(subdomain)
|
42
|
+
accounts.delete(subdomain.to_sym)
|
43
|
+
self.default_account = accounts.empty? ? nil : accounts.keys.first
|
44
|
+
|
45
|
+
write
|
46
|
+
end
|
47
|
+
|
48
|
+
%w[set_default_account add_account remove_account].each do |name|
|
49
|
+
module_eval "def #{name}!(*args); #{name}(*args); write; end"
|
50
|
+
end
|
51
|
+
|
52
|
+
def api_key
|
53
|
+
accounts[default_account.to_sym]
|
54
|
+
end
|
55
|
+
|
56
|
+
def auto_open?
|
57
|
+
auto_open == true
|
58
|
+
end
|
59
|
+
|
60
|
+
def auto_copy?
|
61
|
+
auto_copy == true
|
21
62
|
end
|
22
63
|
|
23
64
|
# Updates an existing Config object.
|
24
65
|
#
|
25
66
|
# Returns the updated Config object.
|
26
67
|
def update(attributes)
|
27
|
-
Forward.
|
68
|
+
Forward.logger.debug('[config] updating')
|
28
69
|
attributes.each do |key, value|
|
29
70
|
self.send(:"#{key}=", value)
|
30
71
|
end
|
@@ -39,113 +80,49 @@ module Forward
|
|
39
80
|
Hash[instance_variables.map { |var| [var[1..-1].to_sym, instance_variable_get(var)] }]
|
40
81
|
end
|
41
82
|
|
42
|
-
# Validate that the required values are in the Config.
|
43
|
-
# Raises a config error if values are missing.
|
44
|
-
def validate
|
45
|
-
Forward.log.debug('Validating Config')
|
46
|
-
attributes = [:api_token, :private_key]
|
47
|
-
errors = []
|
48
|
-
|
49
|
-
attributes.each do |attribute|
|
50
|
-
value = instance_variable_get("@#{attribute}")
|
51
|
-
|
52
|
-
errors << attribute if value.nil? || value.to_s.empty?
|
53
|
-
end
|
54
|
-
|
55
|
-
if errors.length == 1
|
56
|
-
raise ConfigError, "#{errors.first} is a required field"
|
57
|
-
elsif errors.length >= 2
|
58
|
-
raise ConfigError, "#{errors.join(', ')} are required fields"
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
83
|
# Write the current config data to `config_path', and the current
|
63
84
|
# private_key to `key_path'.
|
64
85
|
#
|
65
86
|
# Returns the Config object.
|
66
87
|
def write
|
67
|
-
Forward.
|
68
|
-
key_folder = File.dirname(Config.key_path)
|
69
|
-
config_data = to_hash.delete_if { |k,v| !CONFIG_FILE_VALUES.include?(k) }
|
70
|
-
|
71
|
-
self.validate
|
88
|
+
Forward.logger.debug('[config] writing')
|
72
89
|
|
73
|
-
|
74
|
-
File.open(Config.config_path, 'w') { |f| f.write(YAML.dump(config_data)) }
|
75
|
-
File.open(Config.key_path, 'w') { |f| f.write(private_key) }
|
90
|
+
File.open(config_path, 'w') { |f| f.write(YAML.dump(to_hash)) }
|
76
91
|
|
77
92
|
self
|
78
|
-
rescue
|
93
|
+
rescue Exception => e
|
94
|
+
Forward.logger.fatal "#{e.message}"
|
79
95
|
raise ConfigError, 'Unable to write config file'
|
80
96
|
end
|
81
97
|
|
82
|
-
# Returns the location of the forward ssh private key
|
83
|
-
# based on the host os.
|
84
|
-
#
|
85
|
-
# Returns the String path.
|
86
|
-
def self.key_path
|
87
|
-
if Forward.windows?
|
88
|
-
File.join(ENV['HOME'], 'forward', 'key')
|
89
|
-
else
|
90
|
-
File.join(ENV['HOME'], '.ssh', 'forwardhq.com')
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
98
|
# Returns the location of the forward config file
|
95
99
|
# based on the host os.
|
96
100
|
#
|
97
101
|
# Returns the String path.
|
98
|
-
def
|
102
|
+
def config_path
|
99
103
|
if Forward.windows?
|
100
104
|
File.join(ENV['HOME'], 'forward', 'config')
|
101
105
|
else
|
102
|
-
File.join(ENV['HOME'], '.
|
106
|
+
File.join(ENV['HOME'], '.forwardrc')
|
103
107
|
end
|
104
108
|
end
|
105
109
|
|
106
110
|
# Checks to see if a .forward config file exists.
|
107
111
|
#
|
108
112
|
# Returns true or false based on the existence of the config file.
|
109
|
-
def
|
113
|
+
def present?
|
110
114
|
File.exist? config_path
|
111
115
|
end
|
112
116
|
|
113
|
-
# Checks to see if a `private_key' exist.
|
114
|
-
#
|
115
|
-
# Returns true or false based on the existence of the key file.
|
116
|
-
def self.key_file_present?
|
117
|
-
File.exist? key_path
|
118
|
-
end
|
119
|
-
|
120
117
|
# Create a config file if it doesn't exist, load one if it does.
|
121
118
|
#
|
122
119
|
# Returns the resulting Config object.
|
123
|
-
def
|
124
|
-
if
|
125
|
-
|
120
|
+
def create_or_load
|
121
|
+
if present?
|
122
|
+
load
|
126
123
|
else
|
127
|
-
|
128
|
-
|
129
|
-
end
|
130
|
-
|
131
|
-
# Create a config by authenticating the user via the api,
|
132
|
-
# and saving the users api_token/id/private_key.
|
133
|
-
#
|
134
|
-
# Returns the new Config object.
|
135
|
-
def self.create
|
136
|
-
Forward.log.debug('Creating Config')
|
137
|
-
if @updating_config || ask('Already have an account with Forward? ').chomp =~ /\Ay/i
|
138
|
-
config = Config.new
|
139
|
-
email, password = CLI.authenticate.values_at(:email, :password)
|
140
|
-
config.update(Forward::Api::User.api_token(email, password))
|
141
|
-
Forward::Api.token = config.api_token
|
142
|
-
config.private_key = Forward::Api::TunnelKey.create
|
143
|
-
|
144
|
-
config.write
|
145
|
-
else
|
146
|
-
message = "You'll need a Forward account first. You can create one at "
|
147
|
-
message << HighLine.color('https://forwardhq.com', :underline)
|
148
|
-
Client.cleanup_and_exit!(message)
|
124
|
+
set_defaults!
|
125
|
+
write
|
149
126
|
end
|
150
127
|
end
|
151
128
|
|
@@ -154,31 +131,11 @@ module Forward
|
|
154
131
|
# the config options aren't valid.
|
155
132
|
#
|
156
133
|
# Returns the Config object.
|
157
|
-
def
|
158
|
-
Forward.
|
159
|
-
config
|
160
|
-
|
161
|
-
raise ConfigError, "Unable to find a forward config file at `#{config_path}'" unless Config.present?
|
162
|
-
|
163
|
-
if File.read(config_path).include? '-----M-----'
|
164
|
-
puts "Forward needs to update your config file, please re-authenticate"
|
165
|
-
File.delete(config_path)
|
166
|
-
@updating_config = true
|
167
|
-
create_or_load
|
168
|
-
end
|
169
|
-
|
170
|
-
raise ConfigError, "Unable to find a forward key file at `#{key_path}'" unless Config.key_file_present?
|
171
|
-
|
172
|
-
|
173
|
-
config.update(YAML.load_file(config_path).symbolize_keys)
|
174
|
-
config.private_key = File.read(key_path)
|
175
|
-
Forward::Api.token = config.api_token
|
176
|
-
|
177
|
-
config.validate
|
178
|
-
|
179
|
-
Forward.config = config
|
134
|
+
def load
|
135
|
+
Forward.logger.debug('[config] loading')
|
136
|
+
raise ConfigError, "Unable to find a forward config file at `#{config_path}'" unless present?
|
180
137
|
|
181
|
-
|
138
|
+
update(YAML.load_file(config_path).symbolize_keys)
|
182
139
|
end
|
183
140
|
|
184
141
|
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Forward
|
2
|
+
class Request
|
3
|
+
include Common
|
4
|
+
|
5
|
+
attr_accessor :tunnel
|
6
|
+
attr_accessor :id
|
7
|
+
attr_accessor :method
|
8
|
+
attr_accessor :path
|
9
|
+
attr_accessor :body
|
10
|
+
attr_accessor :headers
|
11
|
+
|
12
|
+
def initialize(tunnel, attributes)
|
13
|
+
@tunnel = tunnel
|
14
|
+
@id = attributes[:id]
|
15
|
+
@method = attributes[:method]
|
16
|
+
@path = attributes[:url]
|
17
|
+
@headers = attributes[:headers]
|
18
|
+
@body = ''
|
19
|
+
end
|
20
|
+
|
21
|
+
def url
|
22
|
+
@url ||= "http://#{@tunnel.host}:#{@tunnel.port}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def <<(data)
|
26
|
+
@body << data
|
27
|
+
|
28
|
+
@body
|
29
|
+
end
|
30
|
+
|
31
|
+
def destroy
|
32
|
+
@tunnel.requests.delete(@id)
|
33
|
+
end
|
34
|
+
|
35
|
+
def process
|
36
|
+
options = {
|
37
|
+
path: path,
|
38
|
+
body: (body.empty? ? nil : body),
|
39
|
+
head: headers
|
40
|
+
}
|
41
|
+
|
42
|
+
puts "[#{Time.now.strftime('%H:%M:%S')}] [#{method}] #{path}" unless Forward.quiet?
|
43
|
+
|
44
|
+
http = EM::HttpRequest.new(url).send(method.downcase, options)
|
45
|
+
|
46
|
+
http.headers { |header|
|
47
|
+
tunnel.socket.send(type: 'response:start', data: { id: id, headers: header.raw, status: header.status })
|
48
|
+
}
|
49
|
+
|
50
|
+
http.stream { |chunk|
|
51
|
+
tunnel.socket.send({type: 'response:data', data: { id: id }}, chunk)
|
52
|
+
}
|
53
|
+
|
54
|
+
http.callback {
|
55
|
+
tunnel.socket.send(type: 'response:end', data: { id: id })
|
56
|
+
destroy
|
57
|
+
}
|
58
|
+
|
59
|
+
http.errback {
|
60
|
+
handle_error(http.error)
|
61
|
+
destroy
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
def handle_error(error)
|
66
|
+
puts "\e[31mhttp://#{tunnel.authority} isn't responding, make sure your server is running on localhost.\e[0m"
|
67
|
+
tunnel.socket.send(type: 'response:error', data: { id: id, error_type: 'localhost_unavailable' })
|
68
|
+
|
69
|
+
destroy
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module Forward
|
2
|
+
class Socket
|
3
|
+
include Common
|
4
|
+
HEART_BEAT_INTERVAL = 10.freeze
|
5
|
+
WATCH_INTERVAL = 60.freeze
|
6
|
+
ACTIVITY_TIMEOUT = 60.freeze
|
7
|
+
|
8
|
+
def initialize(tunnel)
|
9
|
+
@tunnel = tunnel
|
10
|
+
@socket = Faye::WebSocket::Client.new(@tunnel.tunneler)
|
11
|
+
@socket.on :open do |event|
|
12
|
+
logger.debug '[socket] open'
|
13
|
+
send(type: 'tunnel:identify', tunnelId: @tunnel.id)
|
14
|
+
end
|
15
|
+
|
16
|
+
@socket.on :message do |event|
|
17
|
+
receive(event.data)
|
18
|
+
end
|
19
|
+
|
20
|
+
@socket.on :close do |event|
|
21
|
+
logger.debug "[socket] close - code: #{event.code} reason: #{event.reason}"
|
22
|
+
if @tunnel.open?
|
23
|
+
exit_with_message "Your tunnel has been disconnected"
|
24
|
+
else
|
25
|
+
@tunnel.destroy { exit_with_error "Unable to open tunnel, please contact us at #{SUPPORT_EMAIL}" }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
@socket.on :error do |event|
|
30
|
+
logger.debug "[socket] error #{event.inspect}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def send(message, data = nil)
|
35
|
+
@socket.send(pack(message, data))
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def receive(data)
|
41
|
+
message, data = unpack(data)
|
42
|
+
logger.debug "[socket] #{message[:type]}"
|
43
|
+
|
44
|
+
@tunnel.track_activity!
|
45
|
+
|
46
|
+
if message[:type] == 'tunnel:ready'
|
47
|
+
@tunnel.ready!
|
48
|
+
beat
|
49
|
+
watch
|
50
|
+
|
51
|
+
return
|
52
|
+
end
|
53
|
+
|
54
|
+
if message[:type] == 'tunnel:notfound'
|
55
|
+
logger.debug "[socket] tunnel not found"
|
56
|
+
exit_with_message "Unable to open tunnel, please contact us at #{SUPPORT_EMAIL}"
|
57
|
+
end
|
58
|
+
|
59
|
+
if message[:type] == 'notification'
|
60
|
+
puts message[:message]
|
61
|
+
|
62
|
+
return
|
63
|
+
end
|
64
|
+
|
65
|
+
if message[:type] == 'request:start'
|
66
|
+
logger.debug "[request] start #{message[:url]}"
|
67
|
+
request = Request.new(@tunnel, message)
|
68
|
+
@tunnel.requests[request.id] = request
|
69
|
+
return
|
70
|
+
end
|
71
|
+
|
72
|
+
request = @tunnel.requests[message[:id]]
|
73
|
+
return if request.nil?
|
74
|
+
|
75
|
+
if message[:type] == 'request:data'
|
76
|
+
logger.debug '[request] data'
|
77
|
+
request << data
|
78
|
+
end
|
79
|
+
|
80
|
+
if message[:type] == 'request:end'
|
81
|
+
logger.debug '[request] end'
|
82
|
+
request.process
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def beat
|
87
|
+
EM.add_periodic_timer(HEART_BEAT_INTERVAL) do
|
88
|
+
logger.debug "[heartbeat] lub"
|
89
|
+
send(type: 'heartbeat:lub')
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def watch
|
94
|
+
EM.add_periodic_timer(WATCH_INTERVAL) do
|
95
|
+
logger.debug "[socket] checking activity"
|
96
|
+
inactive_for = Time.now.to_i - @tunnel.last_active_at
|
97
|
+
|
98
|
+
if inactive_for > ACTIVITY_TIMEOUT
|
99
|
+
logger.debug "[socket] closing due to inactivity (heartbeats and requests)"
|
100
|
+
@socket.close
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def pack(message, data = nil)
|
106
|
+
message = message.is_a?(String) ? message : message.to_json
|
107
|
+
message.encode!('utf-16le')
|
108
|
+
|
109
|
+
bytes = Array(message.bytesize).pack('v').bytes.to_a
|
110
|
+
bytes += message.bytes.to_a
|
111
|
+
bytes += data.bytes.to_a unless data.nil?
|
112
|
+
|
113
|
+
bytes
|
114
|
+
end
|
115
|
+
|
116
|
+
def unpack(bytes)
|
117
|
+
bytes = bytes.pack('C*')
|
118
|
+
message_size = bytes.unpack('v')[0]
|
119
|
+
message = bytes.byteslice(2, message_size)
|
120
|
+
binary_offset = 2 + message_size
|
121
|
+
|
122
|
+
[JSON.parse(message, symbolize_names: true), bytes.byteslice(binary_offset..-1)]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|