forward 0.3.3 → 1.0.0
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.
- 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
|