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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/Gemfile +0 -2
  4. data/README.md +24 -0
  5. data/Rakefile +3 -1
  6. data/bin/forward +1 -1
  7. data/forward.gemspec +17 -11
  8. data/lib/forward/api/resource.rb +51 -83
  9. data/lib/forward/api/tunnel.rb +41 -68
  10. data/lib/forward/api/user.rb +14 -11
  11. data/lib/forward/api.rb +7 -26
  12. data/lib/forward/cli.rb +55 -253
  13. data/lib/forward/command/account.rb +69 -0
  14. data/lib/forward/command/base.rb +62 -0
  15. data/lib/forward/command/config.rb +64 -0
  16. data/lib/forward/command/tunnel.rb +178 -0
  17. data/lib/forward/common.rb +44 -0
  18. data/lib/forward/config.rb +75 -118
  19. data/lib/forward/request.rb +72 -0
  20. data/lib/forward/socket.rb +125 -0
  21. data/lib/forward/static/app.rb +157 -0
  22. data/lib/forward/static/directory.erb +142 -0
  23. data/lib/forward/tunnel.rb +102 -40
  24. data/lib/forward/version.rb +1 -1
  25. data/lib/forward.rb +80 -63
  26. data/test/api/resource_test.rb +70 -54
  27. data/test/api/tunnel_test.rb +50 -51
  28. data/test/api/user_test.rb +33 -20
  29. data/test/cli_test.rb +0 -126
  30. data/test/command/account_test.rb +26 -0
  31. data/test/command/tunnel_test.rb +133 -0
  32. data/test/config_test.rb +103 -54
  33. data/test/forward_test.rb +47 -0
  34. data/test/test_helper.rb +35 -26
  35. data/test/tunnel_test.rb +50 -22
  36. metadata +210 -169
  37. data/forwardhq.crt +0 -112
  38. data/lib/forward/api/client_log.rb +0 -20
  39. data/lib/forward/api/tunnel_key.rb +0 -18
  40. data/lib/forward/client.rb +0 -110
  41. data/lib/forward/error.rb +0 -12
  42. data/test/api/tunnel_key_test.rb +0 -28
  43. data/test/api_test.rb +0 -0
  44. data/test/client_test.rb +0 -8
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b188425121deedc6db3120b631fe576a3e4bbf67
4
+ data.tar.gz: 1371bf60526e65fd941b9e17398d5f004bd7f7b3
5
+ SHA512:
6
+ metadata.gz: cce5f2e20c6788d420b1faa01e6eb778175cb7486271a58e531a0a1a35c75e49a5c943dbf183deedbf146aed8c7e2f16bd7ed6e8d2a09e9dc30ba2b3740d0429
7
+ data.tar.gz: 014d3a99305e8e755fd5544239442fb62e342a804e87d2eb918dd9abcfbc08c7f66f94aa7fc8e955bac76b6bf38c59d6a62c262083f2d033f1d5cdab3a549f8c
data/.gitignore CHANGED
@@ -4,6 +4,7 @@
4
4
  .config
5
5
  .yardoc
6
6
  Gemfile.lock
7
+ .ruby-version
7
8
  InstalledFiles
8
9
  _yardoc
9
10
  coverage
@@ -14,4 +15,5 @@ rdoc
14
15
  spec/reports
15
16
  test/tmp
16
17
  test/version_tmp
18
+ vendor/
17
19
  tmp
data/Gemfile CHANGED
@@ -1,4 +1,2 @@
1
1
  source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in showoff.gemspec
4
2
  gemspec
data/README.md CHANGED
@@ -4,9 +4,33 @@ The ruby client for [https://forwardhq.com/](https://forwardhq.com/). Forward gi
4
4
 
5
5
  ## Usage
6
6
 
7
+
8
+ # login/logout/switch accounts
9
+ forward login
10
+ forward accounts
11
+ forward logout 50east*
12
+ forward default 50east*
13
+
14
+ forward account:login
15
+ forward account:logout
16
+ forward account:default
17
+
18
+ # use default account
19
+ forward .
20
+ forward ~/Dev/static/blah
21
+ forward 3000
22
+ forward foo.dev
23
+ forward blah.local
24
+
25
+ # use specific account/subdomain
26
+ forward . -s tarface
27
+ forward 3000 -s 50east
28
+
29
+
7
30
  > forward <port> [options]
8
31
  > forward <host> [options]
9
32
  > forward <host:port> [options]
33
+ > forward <path> [options]
10
34
 
11
35
  Description:
12
36
 
data/Rakefile CHANGED
@@ -5,4 +5,6 @@ require 'bundler/gem_tasks'
5
5
  Rake::TestTask.new do |t|
6
6
  t.libs << 'test'
7
7
  t.test_files = FileList['test/**/*_test.rb']
8
- end
8
+ end
9
+
10
+ task default: :test
data/bin/forward CHANGED
@@ -3,4 +3,4 @@ $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
3
3
 
4
4
  require 'forward'
5
5
 
6
- Forward::CLI.run(ARGV)
6
+ Forward::CLI.start
data/forward.gemspec CHANGED
@@ -2,10 +2,11 @@
2
2
  require File.expand_path('../lib/forward/version', __FILE__)
3
3
 
4
4
  Gem::Specification.new do |gem|
5
- gem.authors = ['blahed']
6
- gem.email = ['travis@50east.co']
7
- gem.summary = 'Forward Lets You Share localhost over the Web. Demo a Website Without Hosting.'
5
+ gem.authors = ['Travis Dunn', 'Noah Burney']
6
+ gem.email = ['travis@50east.co', 'noah@50east.co']
7
+ gem.summary = 'Forward lets you get a link to localhost and share it with anyone over the web.'
8
8
  gem.homepage = 'https://forwardhq.com'
9
+ gem.license = 'MIT'
9
10
 
10
11
  gem.files = `git ls-files`.split($\)
11
12
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
@@ -14,13 +15,18 @@ Gem::Specification.new do |gem|
14
15
  gem.require_paths = ['lib']
15
16
  gem.version = Forward::VERSION
16
17
 
17
- gem.add_dependency 'json_pure', '~> 1.8.1'
18
- gem.add_dependency 'highline', '~> 1.6.21'
19
- gem.add_dependency 'net-ssh', '~> 2.8.0'
18
+ gem.add_dependency 'slop', '~> 3.6'
19
+ gem.add_dependency 'json_pure', '~> 1.8'
20
+ gem.add_dependency 'highline', '~> 1.6'
21
+ gem.add_dependency 'eventmachine', '~> 1.0.3'
22
+ gem.add_dependency 'thin', '~> 1.6'
23
+ gem.add_dependency 'em-http-request', '~> 1.1'
24
+ gem.add_dependency 'faye-websocket', '~> 0.8'
25
+ gem.add_dependency 'clipboard', '~> 1.0'
20
26
 
21
- gem.add_development_dependency 'rake', '~> 10.1.1'
22
- gem.add_development_dependency 'minitest', '~> 5.3.0'
23
- gem.add_development_dependency 'mocha', '~> 0.14.0'
24
- gem.add_development_dependency 'fakeweb', '~> 1.3.0'
25
- gem.add_development_dependency 'fakefs', '~> 0.4.3'
27
+ gem.add_development_dependency 'rake', '~> 10.3'
28
+ gem.add_development_dependency 'minitest', '~> 5.3'
29
+ gem.add_development_dependency 'mocha', '~> 1.0'
30
+ gem.add_development_dependency 'webmock', '~> 1.20'
31
+ gem.add_development_dependency 'fakefs', '~> 0.5'
26
32
  end
@@ -5,113 +5,81 @@ require 'net/https'
5
5
  require 'uri'
6
6
 
7
7
  module Forward
8
- module Api
8
+ module API
9
9
  class Resource
10
+ include Common
11
+ extend Common
12
+
13
+ DEFAULT_ERROR_MESSAGE = "Unable to connect to API, please contact support@forwardhq.com".freeze
10
14
 
11
15
  attr_accessor :http
12
16
  attr_accessor :uri
13
17
 
14
- def initialize(action = nil)
15
- @action = action
16
- @http = Net::HTTP.new(Forward::Api.uri.host, Forward::Api.uri.port)
17
- @http.use_ssl = Forward::Api.ssl?
18
- @http.verify_mode = OpenSSL::SSL::VERIFY_PEER
19
- @http.ca_file = File.expand_path('../../../../forwardhq.crt', __FILE__)
18
+ class JSONify
19
+ def response(resp)
20
+ resp.response = JSON.parse(resp.response, symbolize_names: true)
21
+ rescue JSON::ParserError
22
+ Forward.logger.debug 'Unable to parse API response'
23
+ end
20
24
  end
21
25
 
22
- def request(method = :get, params = {})
23
- log(:debug, "Request: [#{method.to_s.upcase}] for `#{http.address}:#{http.port}#{uri}'")
24
- log(:debug, "Request: params `#{params.reject { |k,v| k == :password }.inspect }'")
25
- build_request(method, params)
26
- add_headers!
26
+ def initialize
27
+ @http = EM::HttpRequest.new(Forward::API.host)
27
28
 
28
- response = @http.request(@request)
29
-
30
- parse_response(response)
29
+ @http.use JSONify
31
30
  end
32
31
 
33
- def build_request(method, params = {})
34
- @method = method
35
-
36
- case @method
37
- when :get
38
- @request = Net::HTTP::Get.new(uri)
39
- when :post
40
- @request = Net::HTTP::Post.new(uri)
41
- @request.body = params.to_json unless params.empty?
42
- when :put
43
- @request = Net::HTTP::Put.new(uri)
44
- @request.body = params.to_json unless params.empty?
45
- when :delete
46
- @request = Net::HTTP::Delete.new(uri)
47
- @request.body = params.to_json unless params.empty?
48
- end
49
- end
32
+ def request(method = :get, options = {}, &block)
33
+ logger.debug "[API] request: #{method.to_s.upcase} #{@http.uri}#{options[:path]}"
34
+ @options = options || {}
50
35
 
51
- def add_headers!
52
- if Forward::Api.token
53
- @request['Authorization'] = "Token token=#{Forward::Api.token}"
54
- end
36
+ add_head!
37
+ add_body!
55
38
 
56
- @request['Content-Type'] = 'application/json'
57
- @request['Accept'] = 'application/json'
58
- end
39
+ @_request = @http.send(method, @options)
59
40
 
60
- def get(params = {})
61
- @response = request(:get, params)
62
- end
41
+ @_request.callback {
42
+ status = @_request.response_header.status
63
43
 
64
- def post(params = {})
65
- @response = request(:post, params)
66
- end
44
+ logger.debug "[API] response: #{status} #{@_request.response}"
45
+ if @_request.response.is_a?(String) && !@_request.response.empty?
46
+ # no JSON response
47
+ exit_with_error DEFAULT_ERROR_MESSAGE
48
+ else
49
+ block.call(@_request.response, status)
50
+ end
51
+ }
67
52
 
68
- def put(params = {})
69
- @response = request(:put, params)
53
+ @_request.errback {
54
+ exit_with_error DEFAULT_ERROR_MESSAGE
55
+ }
70
56
  end
71
57
 
72
- def delete(params = {})
73
- @response = request(:delete, params)
58
+ def get(options = {}, &block)
59
+ request(:get, options, &block)
74
60
  end
75
61
 
76
- def parse_response(response)
77
- log(:debug, "Response: [#{response.code}] `#{response.body}'")
78
- code = response.code.to_i
79
-
80
- if code == 404
81
- raise ResourceNotFound
82
- elsif ![ 200, 422, 401 ].include? code
83
- raise BadResponse, "response code was: #{response.code}"
84
- elsif response['content-type'] !~ /^application\/json/
85
- raise BadResponse, "response was not JSON, unable to parse"
86
- end
62
+ def post(options = {}, &block)
63
+ request(:post, options, &block)
64
+ end
87
65
 
88
- json = JSON.parse(response.body)
66
+ def delete(options = {}, &block)
67
+ request(:delete, options, &block)
68
+ end
89
69
 
90
- if json.is_a? Hash
91
- json.symbolize_keys!
92
- raise ResourceError.new(code, @action, json) if code != 200
93
- end
70
+ def add_body!
71
+ return unless @options.has_key?(:params)
94
72
 
95
- json
73
+ logger.debug "[API] request params: #{@options[:params].inspect}"
74
+ @options[:body] = @options.delete(:params).to_json
96
75
  end
97
76
 
98
- # def self.dispatch_error(error)
99
- # Forward.log.debug("Dispatching ResourceError: action: #{error.action} errors: #{error.errors.inspect}")
100
- # method = :"#{error.action}_error"
101
- #
102
- # if respond_to? method
103
- # send(method, error.errors)
104
- # else
105
- # Forward::Client.cleanup_and_exit!('An error occured, please contact support@forwardhq.com')
106
- # end
107
- # end
108
-
109
- private
110
-
111
- def log(level, message)
112
- unless self.class.to_s == 'Forward::Api::ClientLog'
113
- Forward.log.send(level.to_sym, message)
114
- end
77
+ def add_head!
78
+ @options[:head] ||= {
79
+ 'content-type' => 'application/json',
80
+ 'accept' => 'application/json'
81
+ }
82
+ @options[:head]['authorization'] = "Token token=#{config.api_key}" if @options[:authenticated]
115
83
  end
116
84
 
117
85
  end
@@ -1,85 +1,58 @@
1
1
  module Forward
2
- module Api
2
+ module API
3
3
  class Tunnel < Resource
4
4
 
5
- def self.create(options = {})
6
- resource = Tunnel.new(:create)
7
- resource.uri = '/api/v2/tunnels'
8
- params = {
9
- :hostport => options[:port],
10
- :vhost => options[:host],
11
- :client => Forward.client_string,
5
+ def self.create(options, &block)
6
+ resource = new
7
+ options = {
8
+ path: "#{API.base_path}/tunnels/",
9
+ authenticated: true,
10
+ params: {
11
+ hostport: options[:port],
12
+ vhost: options[:host],
13
+ subdomain: options[:subdomain_prefix],
14
+ cname: options[:cname],
15
+ username: options[:username],
16
+ password: options[:password],
17
+ no_auth: options[:no_auth],
18
+ client: Forward.client_string,
19
+ }
12
20
  }
13
-
14
- params[:subdomain] = options[:subdomain_prefix] if options.has_key?(:subdomain_prefix)
15
- [ :cname, :username, :password, :no_auth ].each do |param|
16
- params[param] = options[param] if options.has_key?(param)
17
- end
18
-
19
- resource.post(params)[:tunnel].symbolize_keys
20
- rescue ResourceError => e
21
- error_on_create(e, options)
22
- end
23
21
 
24
- def self.index
25
- resource = Tunnel.new(:index)
26
- resource.uri = "/api/v2/tunnels"
27
-
28
- resource.get[:tunnels]
29
- end
30
-
31
- def self.show(id)
32
- resource = Tunnel.new(:show)
33
- resource.uri = "/api/v2/tunnels/#{id}"
34
-
35
- resource.get[:tunnel].symbolize_keys
36
- rescue Forward::Api::ResourceNotFound
37
- nil
22
+ resource.post(options) do |response, status|
23
+ if status == 200
24
+ block.call(response)
25
+ else
26
+ handle_error(response, status)
27
+ end
28
+ end
38
29
  end
39
30
 
40
- private
41
-
42
- def self.ask_to_destroy(message, options)
43
- tunnels = index
44
-
45
- puts message
46
- choose do |menu|
47
- menu.prompt = "Choose a tunnel from the list to close or `q' to exit forward "
31
+ def self.destroy(id, &block)
32
+ resource = new
33
+ options = {
34
+ path: "#{API.base_path}/tunnels/#{id}",
35
+ authenticated: true,
36
+ }
48
37
 
49
- tunnels.each do |tunnel|
50
- text = "Forwarding port #{tunnel['hostport']}"
51
- menu.choice(text) { destroy_and_create(tunnel['_id'], options) }
52
- end
53
- menu.hidden('quit') { Forward::Client.cleanup_and_exit! }
54
- menu.hidden('exit') { Forward::Client.cleanup_and_exit! }
38
+ resource.delete(options) do |response, status|
39
+ block.call(response)
55
40
  end
56
41
  end
57
42
 
58
- def self.destroy_and_create(id, options)
59
- Forward.log.debug("Destroying tunnel: #{id}")
60
- destroy(id)
61
- puts "tunnel removed, now we're creating a new one"
62
- create(options)
63
- end
43
+ private
64
44
 
65
- def self.error_on_create(error, options)
66
- Forward.log.debug("An error occured creating tunnel:\n#{error.inspect}")
45
+ def self.handle_error(response, status)
46
+ exit_with_error "Unable to authenticate your account. Try logging out with `forward account logout' or contact #{SUPPORT_EMAIL}" if status == 401
47
+ exit_with_error DEFAULT_ERROR_MESSAGE if response.is_a?(String) || !response.has_key?(:type)
67
48
 
68
- if error.type == 'tunnel_limit_reached'
69
- Forward.log.debug('Tunnel limit reached')
70
- ask_to_destroy(error.api_message, options)
71
- elsif error.type =~ /(?:account_suspended|trial_expired)/i
72
- Forward::Client.cleanup_and_exit!(error.api_message)
49
+ case response[:type]
50
+ when 'invalid_request_error'
51
+ exit_with_error "Invalid tunnel parameters, try `forward --help' or contact us at #{SUPPORT_EMAIL}"
52
+ when 'account_error'
53
+ exit_with_error response[:errors][:account].first
73
54
  else
74
- message = "We were unable to create your tunnel for the following reasons: \n"
75
- error.errors.each do |key, value|
76
- if key == 'base'
77
- message << " #{value.join(', ')}\n"
78
- else
79
- value.each { |m| message << " #{key} #{m}\n"}
80
- end
81
- end
82
- Forward::Client.cleanup_and_exit!(message)
55
+ exit_with_error DEFAULT_ERROR_MESSAGE
83
56
  end
84
57
  end
85
58
 
@@ -1,18 +1,21 @@
1
1
  module Forward
2
- module Api
2
+ module API
3
3
  class User < Resource
4
4
 
5
- def self.api_token(email, password)
6
- resource = User.new(:api_token)
7
- resource.uri = '/api/v2/users/api_token'
8
- params = { :email => email, :password => password }
5
+ def self.authenticate(email, password, &block)
6
+ resource = new
7
+ options = {
8
+ path: "#{API.base_path}/user/token",
9
+ params: { email: email, password: password }
10
+ }
9
11
 
10
- user = resource.post(params)[:user].symbolize_keys
11
- user[:id] = user.delete(:_id)
12
-
13
- user
14
- rescue ResourceError => e
15
- Forward::Client.cleanup_and_exit!('Unable to authenticate with email and password')
12
+ resource.post(options) do |response, status|
13
+ if status != 200
14
+ exit_with_error "Unable to authenticate `#{email}' on forwardhq.com"
15
+ else
16
+ block.call(response[:subdomain], response[:token])
17
+ end
18
+ end
16
19
  end
17
20
 
18
21
  end
data/lib/forward/api.rb CHANGED
@@ -1,40 +1,21 @@
1
1
  require 'forward/api/resource'
2
- require 'forward/api/client_log'
3
- require 'forward/api/tunnel_key'
4
2
  require 'forward/api/tunnel'
5
3
  require 'forward/api/user'
6
4
 
7
5
  module Forward
8
- module Api
9
- class BadResponse < StandardError; end
10
- class ResourceNotFound < StandardError; end
11
- class ResourceError < StandardError
12
- attr_reader :code, :type, :action, :api_message, :errors
13
-
14
- def initialize(code, action, json = {})
15
- @code = code
16
- @action = action
17
- @json = json
18
- @type = json[:type]
19
- @api_message = json[:message]
20
- @errors = json[:errors] || {}
21
- end
22
- end
23
-
24
- DEFAULT_API_HOST = 'https://forwardhq.com'
6
+ module API
7
+ VERSION = 'v3'.freeze
8
+ DEFAULT_API_HOST = 'https://forwardhq.com'.freeze
25
9
 
26
10
  # Returns either an api host set in the environment or a set default.
27
11
  #
28
12
  # Returns a String containing the api host.
29
- def self.uri
30
- URI.parse(ENV['FORWARD_API_HOST'] || DEFAULT_API_HOST)
13
+ def self.host
14
+ @host ||= ENV['FORWARD_API_HOST'] || DEFAULT_API_HOST
31
15
  end
32
16
 
33
- # Returns True or False if we should be using ssl
34
- #
35
- # Returns a Boolean.
36
- def self.ssl?
37
- uri.scheme == 'https'
17
+ def self.base_path
18
+ @base_path ||= "/api/#{VERSION}"
38
19
  end
39
20
 
40
21
  def self.token=(token)