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,10 @@
|
|
1
|
+
module Forward
|
2
|
+
# An error occurred with the API
|
3
|
+
class ApiError < StandardError; end
|
4
|
+
# An error occurred with the Client
|
5
|
+
class ClientError < StandardError; end
|
6
|
+
# An error occurred with the Config
|
7
|
+
class ConfigError < StandardError; end
|
8
|
+
# An error occurred with the Tunnel
|
9
|
+
class TunnelError < StandardError; end
|
10
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Forward
|
2
|
+
class Tunnel
|
3
|
+
CHECK_INTERVAL = 7
|
4
|
+
|
5
|
+
# The Tunnel resource ID.
|
6
|
+
attr_reader :id
|
7
|
+
# The domain for the Tunnel.
|
8
|
+
attr_reader :subdomain
|
9
|
+
# The remote port.
|
10
|
+
attr_reader :port
|
11
|
+
# The tunneler host.
|
12
|
+
attr_reader :tunneler
|
13
|
+
# The amount of time in seconds the Tunnel has be inactive for
|
14
|
+
attr_accessor :inactive_for
|
15
|
+
|
16
|
+
# Initializes a Tunnel instance for the Client and requests a tunnel from
|
17
|
+
# API.
|
18
|
+
#
|
19
|
+
# client - The Client instance.
|
20
|
+
def initialize(options = {})
|
21
|
+
@response = Forward::Api::Tunnel.create(options)
|
22
|
+
@id = @response[:_id]
|
23
|
+
@subdomain = @response[:subdomain]
|
24
|
+
@port = @response[:port]
|
25
|
+
@tunneler = @response[:tunneler_public]
|
26
|
+
@timeout = @response[:timeout]
|
27
|
+
@inactive_for = 0
|
28
|
+
end
|
29
|
+
|
30
|
+
def poll_status
|
31
|
+
Thread.new {
|
32
|
+
loop do
|
33
|
+
if @timeout && !@timeout.zero? && @inactive_for > @timeout
|
34
|
+
Forward.log(:debug, "Session closing due to inactivity `#{@inactive_for}' seconds")
|
35
|
+
Client.cleanup_and_exit!("Tunnel has been inactive for #{@inactive_for} seconds, exiting...")
|
36
|
+
elsif Forward::Api::Tunnel.show(@id).nil?
|
37
|
+
Client.current.tunnel = nil
|
38
|
+
Forward.log(:debug, "Tunnel destroyed, closing session")
|
39
|
+
Client.cleanup_and_exit!
|
40
|
+
else
|
41
|
+
sleep CHECK_INTERVAL
|
42
|
+
end
|
43
|
+
|
44
|
+
@inactive_for += CHECK_INTERVAL
|
45
|
+
end
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
def cleanup
|
50
|
+
Forward::Api::Tunnel.destroy(@id) if @id
|
51
|
+
end
|
52
|
+
|
53
|
+
def active?
|
54
|
+
@active
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
data/lib/forward/version.rb
CHANGED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
describe Forward::Api::PublicKey do
|
4
|
+
|
5
|
+
before :each do
|
6
|
+
FakeWeb.allow_net_connect = false
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'retrieves the public_key and returns it' do
|
10
|
+
fake_body = { :private_key => 'ssh-key 1234567890' }
|
11
|
+
|
12
|
+
stub_api_request(:post, '/api/public_keys', :body => fake_body.to_json)
|
13
|
+
|
14
|
+
response = Api::PublicKey.create
|
15
|
+
response.must_equal fake_body[:private_key]
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'exits with message if response has errors' do
|
19
|
+
fake_body = { :errors => { :base => 'Unable to retrieve a private key' } }
|
20
|
+
|
21
|
+
stub_api_request(:post, '/api/public_keys', :body => fake_body.to_json)
|
22
|
+
|
23
|
+
lambda {
|
24
|
+
dev_null { Api::PublicKey.create }
|
25
|
+
}.must_raise SystemExit
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
describe Forward::Api::Resource do
|
4
|
+
Api = Forward::Api
|
5
|
+
|
6
|
+
before :each do
|
7
|
+
Api.token = 'abc123'
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'builds http requests based on given method' do
|
11
|
+
resource = Api::Resource.new
|
12
|
+
resource.uri = '/path'
|
13
|
+
|
14
|
+
[ :get, :post, :put, :delete ].each do |method|
|
15
|
+
resource.build_request(method)
|
16
|
+
klass = eval("Net::HTTP::#{method.capitalize}")
|
17
|
+
request = resource.instance_variable_get('@request')
|
18
|
+
|
19
|
+
request.kind_of?(klass).must_equal true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'builds http requests with json bodies' do
|
24
|
+
resource = Api::Resource.new
|
25
|
+
resource.uri = '/path'
|
26
|
+
|
27
|
+
[ :post, :put, :delete ].each do |method|
|
28
|
+
params = { :foo => 'bar '}
|
29
|
+
resource.build_request(method, params)
|
30
|
+
klass = eval("Net::HTTP::#{method.capitalize}")
|
31
|
+
request = resource.instance_variable_get('@request')
|
32
|
+
|
33
|
+
request.kind_of?(klass).must_equal true
|
34
|
+
request.body.must_equal params.to_json
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'adds auth and json headers to the request' do
|
39
|
+
resource = Api::Resource.new
|
40
|
+
resource.uri = '/path'
|
41
|
+
resource.build_request(:post)
|
42
|
+
resource.add_headers!
|
43
|
+
request = resource.instance_variable_get('@request')
|
44
|
+
|
45
|
+
request['Authorization'].must_equal "Token token=#{Api.token}"
|
46
|
+
request['Content-Type'].must_equal 'application/json'
|
47
|
+
request['Accept'].must_equal 'application/json'
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
it 'raises an error if the response code is not 200' do
|
52
|
+
resource = Api::Resource.new
|
53
|
+
response = mock
|
54
|
+
response.stubs(:code).returns(403)
|
55
|
+
response.stubs(:body)
|
56
|
+
|
57
|
+
lambda { resource.parse_response(response) }.must_raise Api::BadResponse
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'raises an error if the response is not json' do
|
61
|
+
resource = Api::Resource.new
|
62
|
+
response = mock
|
63
|
+
response.stubs(:code).returns(200)
|
64
|
+
response.stubs(:body)
|
65
|
+
response.stubs(:[]).with('content-type').returns('text/html')
|
66
|
+
|
67
|
+
lambda { resource.parse_response(response) }.must_raise Api::BadResponse
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'parses a json response' do
|
71
|
+
resource = Api::Resource.new
|
72
|
+
response = mock
|
73
|
+
response.stubs(:code).returns(200)
|
74
|
+
response.stubs(:[]).with('content-type').returns('application/json')
|
75
|
+
response.stubs(:body).returns('{ "foo": "bar" }')
|
76
|
+
|
77
|
+
json = resource.parse_response(response)
|
78
|
+
json.kind_of?(Hash).must_equal true
|
79
|
+
json[:foo].must_equal 'bar'
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
describe Forward::Api::Tunnel do
|
4
|
+
|
5
|
+
before :each do
|
6
|
+
FakeWeb.allow_net_connect = false
|
7
|
+
Forward::Api.token = 'abc123'
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'creates a tunnel and returns the attributes' do
|
11
|
+
Forward.client = mock
|
12
|
+
fake_body = { :_id => '1', :subdomain => 'foo', :port => 56789 }
|
13
|
+
|
14
|
+
Forward.client.expects(:options).returns(:port => 3000)
|
15
|
+
stub_api_request(:post, '/api/tunnels', :body => fake_body.to_json)
|
16
|
+
|
17
|
+
response = Forward::Api::Tunnel.create
|
18
|
+
|
19
|
+
fake_body.each do |key, value|
|
20
|
+
response[key].must_equal fake_body[key]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'exits with message if create response has errors' do
|
25
|
+
Forward.client = mock
|
26
|
+
fake_body = { :errors => { :base => [ 'unable to create tunnel' ] } }
|
27
|
+
|
28
|
+
Forward.client.expects(:options).returns(:port => 3000)
|
29
|
+
stub_api_request(:post, '/api/tunnels', :body => fake_body.to_json)
|
30
|
+
|
31
|
+
lambda {
|
32
|
+
dev_null { Forward::Api::Tunnel.create }
|
33
|
+
}.must_raise SystemExit
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'gives a choice and closes a tunnel if limit is reached' do
|
37
|
+
Forward.client = mock
|
38
|
+
post_options = [
|
39
|
+
{ :body => { :errors => { :base => [ 'you have reached your limit' ] } }.to_json },
|
40
|
+
{ :body => { :_id => '1', :subdomain => 'foo', :port => 56789 }.to_json }
|
41
|
+
]
|
42
|
+
index_body = [ { :_id => 'abc123' }, { :_id => 'def456' } ]
|
43
|
+
|
44
|
+
Forward.client.expects(:options).returns(:port => 3000).twice
|
45
|
+
stub_api_request(:post, '/api/tunnels', post_options)
|
46
|
+
stub_api_request(:get, '/api/tunnels', :body => index_body.to_json)
|
47
|
+
STDIN.expects(:gets).returns('1')
|
48
|
+
Forward::Api::Tunnel.expects(:destroy).with(index_body.first[:_id])
|
49
|
+
|
50
|
+
dev_null { Forward::Api::Tunnel.create }
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'destroys a tunnel and returns the attributes' do
|
54
|
+
Forward.client = mock
|
55
|
+
fake_body = { :_id => '1', :subdomain => 'foo', :port => 56789 }
|
56
|
+
|
57
|
+
stub_api_request(:delete, '/api/tunnels/1', :body => fake_body.to_json)
|
58
|
+
|
59
|
+
response = Forward::Api::Tunnel.destroy(1)
|
60
|
+
|
61
|
+
fake_body.each do |key, value|
|
62
|
+
response[key].must_equal fake_body[key]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'gracefully handles the error if destroy has errors' do
|
67
|
+
Forward.client = mock
|
68
|
+
fake_body = { :errors => { :base => 'unable to create tunnel' } }
|
69
|
+
|
70
|
+
stub_api_request(:delete, '/api/tunnels/1', :body => fake_body.to_json)
|
71
|
+
|
72
|
+
Forward::Api::Tunnel.destroy(1).must_be_nil
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
describe Forward::Api::User do
|
4
|
+
|
5
|
+
before :each do
|
6
|
+
FakeWeb.allow_net_connect = false
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'retrieves the users api token and returns it' do
|
10
|
+
fake_body = { :api_token => '123abc' }
|
11
|
+
|
12
|
+
stub_api_request(:post, '/api/users/api_token', :body => fake_body.to_json)
|
13
|
+
|
14
|
+
response = Api::User.api_token('guy@example.com', 'secret')
|
15
|
+
response[:api_token].must_equal '123abc'
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'exits with message if response has errors' do
|
19
|
+
fake_body = { :errors => { :base => 'Unable to authenticate user' } }
|
20
|
+
|
21
|
+
stub_api_request(:post, '/api/users/api_token', :body => fake_body.to_json)
|
22
|
+
|
23
|
+
lambda {
|
24
|
+
dev_null { Api::User.api_token('guy@example.com', 'secret') }
|
25
|
+
}.must_raise SystemExit
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
data/test/api_test.rb
ADDED
File without changes
|
data/test/cli_test.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
describe Forward::CLI do
|
4
|
+
|
5
|
+
it 'parses a forwarded port' do
|
6
|
+
forwarded = Forward::CLI.parse_forwarded('600')
|
7
|
+
forwarded.has_key?(:port).must_equal true
|
8
|
+
forwarded[:port].must_equal 600
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'parses a forwarded host' do
|
12
|
+
forwarded = Forward::CLI.parse_forwarded('mysite.dev')
|
13
|
+
forwarded.has_key?(:host).must_equal true
|
14
|
+
forwarded[:host].must_equal 'mysite.dev'
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'parses a forwarded host and port' do
|
18
|
+
forwarded = Forward::CLI.parse_forwarded('mysite.dev:88')
|
19
|
+
forwarded.has_key?(:host).must_equal true
|
20
|
+
forwarded.has_key?(:port).must_equal true
|
21
|
+
forwarded[:host].must_equal 'mysite.dev'
|
22
|
+
forwarded[:port].must_equal 88
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'parses valid basic auth and returns username and password' do
|
26
|
+
username = 'foo'
|
27
|
+
password = 'bar'
|
28
|
+
|
29
|
+
credentials = Forward::CLI.parse_basic_auth("#{username}:#{password}")
|
30
|
+
credentials.first.must_equal username
|
31
|
+
credentials.last.must_equal password
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'validates basic auth and exits if invalid' do
|
35
|
+
[ 'afadsfsdf', 'adsf:', ':bar' ].each do |credentials|
|
36
|
+
lambda {
|
37
|
+
dev_null { Forward::CLI.validate_basic_auth(credentials) }
|
38
|
+
}.must_raise SystemExit
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'doesnt exit on valid ports' do
|
43
|
+
Forward::CLI.validate_port(69).must_be_nil
|
44
|
+
Forward::CLI.validate_port(3000).must_be_nil
|
45
|
+
Forward::CLI.validate_port(65535).must_be_nil
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'validates port and exits if invalid' do
|
49
|
+
[ 0, 65536 ].each do |port|
|
50
|
+
lambda {
|
51
|
+
dev_null { Forward::CLI.validate_port(port) }
|
52
|
+
}.must_raise SystemExit
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'doesnt exit on valid cnames' do
|
57
|
+
[ 'foo.com', 'whatever-foo.com', 'www.foo.com', 'asdf.asdf.asdf.com' ].each do |cname|
|
58
|
+
Forward::CLI.validate_cname(cname).must_be_nil
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'validates cname and exits if invalid' do
|
63
|
+
[ 'whatever', 'asdfasdf.', '-asdf', 'adsf#$).com' ].each do |cname|
|
64
|
+
lambda {
|
65
|
+
dev_null { Forward::CLI.validate_cname(cname) }
|
66
|
+
}.must_raise SystemExit
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'doesnt exit on valid subdomains' do
|
71
|
+
[ 'foo', 'whatever-foo', 'asdf40' ].each do |subdomain|
|
72
|
+
Forward::CLI.validate_subdomain(subdomain).must_be_nil
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'validates subdomain and exits if invalid' do
|
77
|
+
[ '-asdf', 'adsf#$)' ].each do |subdomain|
|
78
|
+
lambda {
|
79
|
+
dev_null { Forward::CLI.validate_subdomain(subdomain) }
|
80
|
+
}.must_raise SystemExit
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
data/test/client_test.rb
ADDED
data/test/config_test.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
describe Forward::Config do
|
4
|
+
|
5
|
+
before :each do
|
6
|
+
@user_attributes = {
|
7
|
+
:id => '12345',
|
8
|
+
:api_token => 'abcdefg',
|
9
|
+
:private_key => 'secret'
|
10
|
+
}
|
11
|
+
FileUtils.mkdir_p(ENV['HOME'])
|
12
|
+
end
|
13
|
+
|
14
|
+
after :each do
|
15
|
+
if Forward::Config.present?
|
16
|
+
FileUtils.rm(Forward::Config::CONFIG_PATH)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
it "initializes a config with a hash" do
|
21
|
+
config = Forward::Config.new(@user_attributes)
|
22
|
+
|
23
|
+
@user_attributes.each do |key, value|
|
24
|
+
config.send(key).must_equal value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
it "updates a config with a hash" do
|
29
|
+
config = Forward::Config.new
|
30
|
+
config.update(@user_attributes)
|
31
|
+
|
32
|
+
@user_attributes.each do |key, value|
|
33
|
+
config.send(key).must_equal value
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
it "writes and reads a config from disk" do
|
38
|
+
config = Forward::Config.new(@user_attributes)
|
39
|
+
config.write
|
40
|
+
|
41
|
+
saved_config = Forward::Config.load
|
42
|
+
|
43
|
+
@user_attributes.each do |key, value|
|
44
|
+
saved_config.send(key).must_equal value
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
it "converts a config to a hash" do
|
49
|
+
config = Forward::Config.new(@user_attributes)
|
50
|
+
config_hash = config.to_hash
|
51
|
+
|
52
|
+
@user_attributes.each do |key, value|
|
53
|
+
config_hash[key].must_equal value
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
it "deletes config file" do
|
58
|
+
config = Forward::Config.new(@user_attributes)
|
59
|
+
config.write
|
60
|
+
Forward::Config.clear
|
61
|
+
|
62
|
+
Forward::Config.wont_be :present?
|
63
|
+
end
|
64
|
+
|
65
|
+
it "raises exception with an empty config" do
|
66
|
+
config = Forward::Config.new
|
67
|
+
|
68
|
+
lambda { config.validate }.must_raise Forward::ConfigError
|
69
|
+
end
|
70
|
+
|
71
|
+
it "raises exception with an invalid config" do
|
72
|
+
@user_attributes.delete(:api_token)
|
73
|
+
config = Forward::Config.new(@user_attributes)
|
74
|
+
|
75
|
+
lambda { config.validate }.must_raise Forward::ConfigError
|
76
|
+
end
|
77
|
+
|
78
|
+
it "raises exception with bad config on write" do
|
79
|
+
@user_attributes.delete(:private_key)
|
80
|
+
config = Forward::Config.new(@user_attributes)
|
81
|
+
|
82
|
+
lambda { config.write }.must_raise Forward::ConfigError
|
83
|
+
end
|
84
|
+
|
85
|
+
it "raises exception when a config file is not found" do
|
86
|
+
lambda { Forward::Config.load }.must_raise Forward::ConfigError
|
87
|
+
end
|
88
|
+
|
89
|
+
it "raises exception when reading bad config file" do
|
90
|
+
config_path = Forward::Config::CONFIG_PATH
|
91
|
+
config_data = 'the trash alone'
|
92
|
+
|
93
|
+
File.open(config_path, 'w') { |f| f.write(config_data) }
|
94
|
+
|
95
|
+
lambda { Forward::Config.read }.must_raise Forward::ConfigError
|
96
|
+
end
|
97
|
+
|
98
|
+
it "raises exception when an invalid config file is loaded" do
|
99
|
+
skip
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|