forward 0.3.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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)