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,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
|