devtunnel 0.0.1 → 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 +4 -4
- data/bin/devtunnel +79 -0
- data/devtunnel.gemspec +1 -1
- data/lib/devtunnel.rb +22 -2
- data/lib/devtunnel/account.rb +38 -0
- data/lib/devtunnel/api.rb +72 -0
- data/lib/devtunnel/api_error.rb +15 -0
- data/lib/devtunnel/api_model.rb +38 -0
- data/lib/devtunnel/api_response.rb +92 -0
- data/lib/devtunnel/entity.rb +49 -0
- data/lib/devtunnel/success_response.rb +16 -0
- data/lib/devtunnel/version.rb +1 -1
- metadata +12 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8310b1def1d120f7914349a6b78a38b2f4eac9ad
|
4
|
+
data.tar.gz: 08e4be19e03a9eb02153934db0b1c420a852ff95
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 80ce31bc5c9e1c84ebfeac48736531cc17230191e8ad57ef3032983337421efdcb7ea6a1e44e233e01837c8e26928e6bd326efd6cfd9e299f14f568d6cdc6458
|
7
|
+
data.tar.gz: 9ac6cec42c64010789253ff734efc346dade4adfe47017b6d6fd5ceae53210df37e2c23166877f15a6d9658cd3d0a6ee61f1e90c311a884ae74352c49562b220
|
data/bin/devtunnel
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
require 'devtunnel'
|
4
|
+
require 'commander/import'
|
5
|
+
require 'io/console'
|
6
|
+
|
7
|
+
program :version, '0.0.1'
|
8
|
+
program :description, 'Command-line interface for https://devtunnel.madebymarket.com.'
|
9
|
+
|
10
|
+
default_command :upload
|
11
|
+
|
12
|
+
command :signup do |c|
|
13
|
+
c.syntax = 'devtunnel signup, [options]'
|
14
|
+
c.summary = ''
|
15
|
+
c.description = ''
|
16
|
+
c.example 'description', 'command example'
|
17
|
+
c.action do |args, options|
|
18
|
+
puts "Register for a free devtunnel account using your email address and a password."
|
19
|
+
email = ask "email address: "
|
20
|
+
account = Devtunnel::Account.signup email
|
21
|
+
if account.success?
|
22
|
+
account.write_creds
|
23
|
+
puts "Account created, saving your credentials to #{File.expand_path("~/.devtunnel_auth")}."
|
24
|
+
puts "Once you have confirmed your account, you can run 'devtunnel forward --port 8080' to forward port 8080 through your devtunnel."
|
25
|
+
else
|
26
|
+
puts account.errors
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
command :login do |c|
|
32
|
+
c.syntax = 'devtunnel login, [options]'
|
33
|
+
c.summary = ''
|
34
|
+
c.description = ''
|
35
|
+
c.example 'description', 'command example'
|
36
|
+
c.action do |args, options|
|
37
|
+
email = ask "Enter your email address: "
|
38
|
+
pw = password "Enter your devtunnel password (will not be shown):"
|
39
|
+
account = Devtunnel::Account.login email, pw
|
40
|
+
if account.success?
|
41
|
+
account.write_creds
|
42
|
+
puts "Logged in, saving your credentials to #{File.expand_path("~/.devtunnel_auth")}"
|
43
|
+
else
|
44
|
+
puts account.errors
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
command :forward do |c|
|
50
|
+
c.syntax = 'devtunnel upload, [options]'
|
51
|
+
c.summary = ''
|
52
|
+
c.description = ''
|
53
|
+
c.example 'description', 'command example'
|
54
|
+
c.option '--file STRING', String, 'File to upload'
|
55
|
+
c.action do |args, options|
|
56
|
+
account = Devtunnel::Account.from_creds
|
57
|
+
unless options.file
|
58
|
+
puts "Syntax: devtunnel --file /your/file.pdf"
|
59
|
+
exit 0
|
60
|
+
end
|
61
|
+
Devtunnel.api_key = account.api_key
|
62
|
+
|
63
|
+
puts "forwarding port. Other people can connect at: xyz"
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
command :logout do |c|
|
69
|
+
c.syntax = 'devtunnel logout [options]'
|
70
|
+
c.summary = ''
|
71
|
+
c.description = ''
|
72
|
+
c.example 'description', 'command example'
|
73
|
+
c.option '--some-switch', 'Some switch that does something'
|
74
|
+
c.action do |args, options|
|
75
|
+
Devtunnel::Account.logout
|
76
|
+
puts "You've been logged out successfully."
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
data/devtunnel.gemspec
CHANGED
@@ -14,7 +14,7 @@ Gem::Specification.new do |spec|
|
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
16
16
|
spec.files = `git ls-files`.split($/)
|
17
|
-
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| puts f; File.basename(f) }
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
data/lib/devtunnel.rb
CHANGED
@@ -1,5 +1,25 @@
|
|
1
|
-
|
1
|
+
require_relative "devtunnel/version"
|
2
|
+
require_relative "devtunnel/api"
|
3
|
+
require_relative "devtunnel/api_response"
|
4
|
+
require_relative "devtunnel/entity"
|
5
|
+
require_relative "devtunnel/api_error"
|
6
|
+
require "rest_client"
|
7
|
+
require "json"
|
2
8
|
|
3
9
|
module Devtunnel
|
4
|
-
|
10
|
+
@api_key = nil
|
11
|
+
@api_url = "https://devtunnel.madebymarket.com/api/v1"
|
12
|
+
|
13
|
+
def self.api_key=(api_key)
|
14
|
+
@api_key = api_key
|
15
|
+
end
|
16
|
+
def self.api_key
|
17
|
+
@api_key
|
18
|
+
end
|
19
|
+
def self.api_url=(api_url)
|
20
|
+
@api_url = api_url
|
21
|
+
end
|
22
|
+
def self.api_url
|
23
|
+
@api_url
|
24
|
+
end
|
5
25
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require_relative "api_model"
|
2
|
+
|
3
|
+
module Devtunnel
|
4
|
+
class Account
|
5
|
+
include ApiModel
|
6
|
+
attr_accessor :api_key, :email
|
7
|
+
|
8
|
+
def self.login(email, password)
|
9
|
+
params = {:basic_username => email, :basic_password => password}
|
10
|
+
Devtunnel::Api.request :post, "/login", params
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.signup(email)
|
14
|
+
params = {:email => email}
|
15
|
+
Devtunnel::Api.request :post, "/signup", params
|
16
|
+
end
|
17
|
+
|
18
|
+
def write_creds
|
19
|
+
File.open(File.expand_path("~/.devtunnel_auth"), "w") do |f|
|
20
|
+
f.write({api_key: api_key, email: email}.to_json)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.from_creds
|
25
|
+
fn = File.expand_path("~/.devtunnel_auth")
|
26
|
+
raise Exception.new("Please log in first") unless File.exist? fn
|
27
|
+
File.open(fn, "r") do |f|
|
28
|
+
hash = JSON.parse f.read
|
29
|
+
self.new hash
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.logout
|
34
|
+
fn = File.expand_path("~/.devtunnel_auth")
|
35
|
+
File.unlink(fn) if File.exist? fn
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require "rest_client"
|
2
|
+
|
3
|
+
module Devtunnel
|
4
|
+
class Api
|
5
|
+
@@api_jack = nil
|
6
|
+
|
7
|
+
def self.jack=(double)
|
8
|
+
@@api_jack = double
|
9
|
+
end
|
10
|
+
def self.jack
|
11
|
+
@@api_jack
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.request method, url, params = {}, api_key = :account_api_key
|
15
|
+
begin
|
16
|
+
basic_username = Devtunnel.api_key
|
17
|
+
|
18
|
+
if params[:basic_username]
|
19
|
+
basic_username = params[:basic_username]
|
20
|
+
params.delete(:basic_username)
|
21
|
+
end
|
22
|
+
if params[:basic_password]
|
23
|
+
basic_password = params[:basic_password]
|
24
|
+
params.delete(:basic_password)
|
25
|
+
end
|
26
|
+
|
27
|
+
req_params = { :user => basic_username, :password => basic_password,
|
28
|
+
:method => method, :url => "#{Devtunnel.api_url}#{url}", :payload => params}
|
29
|
+
|
30
|
+
self.execute_request(req_params)
|
31
|
+
|
32
|
+
rescue Errno::ECONNREFUSED => e
|
33
|
+
raise ApiError.new(500, {}, {"errors" => [{"code" => 993, "message" => "Unable to connect to API server"}]})
|
34
|
+
rescue ExpiredApiKey => e
|
35
|
+
raise e
|
36
|
+
rescue InvalidApiKey => e
|
37
|
+
raise e
|
38
|
+
rescue Exception => e
|
39
|
+
# what kind of generic exceptions might we be loking for?
|
40
|
+
raise ApiError.new(500, {}, {"errors" => [{"code" => 996, "message" => "Error getting response from API server "+e.inspect}]})
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.execute_request(params)
|
45
|
+
if Devtunnel::Api.jack
|
46
|
+
ApiResponse.new Devtunnel::Api.jack.execute(params)
|
47
|
+
else
|
48
|
+
RestClient::Request.new(params).execute do |response, request, result, &block|
|
49
|
+
ApiResponse.new(response)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.xml_request method, url, params = {}
|
55
|
+
basic_username = Devtunnel.api_key
|
56
|
+
req_params = { :method => method, :url => url, :payload => params}
|
57
|
+
|
58
|
+
self.execute_xml_request(req_params)
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.execute_xml_request(params)
|
63
|
+
if Devtunnel::Api.jack
|
64
|
+
ApiResponse.new Devtunnel::Api.jack.execute(params)
|
65
|
+
else
|
66
|
+
RestClient::Request.new(params).execute do |response, request, result, &block|
|
67
|
+
response
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Devtunnel
|
2
|
+
class ApiError < StandardError
|
3
|
+
attr_accessor :status, :headers, :errors
|
4
|
+
def initialize(status, headers, body)
|
5
|
+
self.status = status
|
6
|
+
self.headers = headers
|
7
|
+
self.errors = body["errors"]
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class ExpiredApiKey < ApiError; end
|
12
|
+
class InvalidApiKey < ApiError; end
|
13
|
+
class UnknownResponse < ApiError; end
|
14
|
+
class NilApiResponse < ApiError; end
|
15
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Devtunnel
|
2
|
+
module ApiModel
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.extend ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(params = {})
|
9
|
+
populate(params)
|
10
|
+
end
|
11
|
+
|
12
|
+
def populate(params = {})
|
13
|
+
params.each do |k,v|
|
14
|
+
instance_variable_set "@#{k}", v
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
attr_accessor :new_record, :errors
|
20
|
+
end
|
21
|
+
|
22
|
+
def errors=(arr); @errors = arr; end
|
23
|
+
def errors; @errors; end
|
24
|
+
|
25
|
+
def new_record?
|
26
|
+
@new_record.nil? ? true : @new_record
|
27
|
+
end
|
28
|
+
|
29
|
+
def success?
|
30
|
+
@errors.nil?
|
31
|
+
end
|
32
|
+
|
33
|
+
def attrs
|
34
|
+
{}.tap {|h| instance_variables.each { |var| h[var[1..-1]] = instance_variable_get(var) } }
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require_relative "entity"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module Devtunnel
|
5
|
+
class ApiResponse
|
6
|
+
attr_accessor :_body, :_status, :_headers, :_entity, :errors
|
7
|
+
|
8
|
+
def initialize(response)
|
9
|
+
if response.nil? or response.body.nil? or response.code.nil? or response.headers.nil?
|
10
|
+
# this response is broken, raise an error.
|
11
|
+
raise NilApiResponse.new(500, {}, {"errors" => [{"code" => 990, "message" => "API Server sent back an empty response."}]})
|
12
|
+
end
|
13
|
+
|
14
|
+
self._body = ((response.body.is_a? String) ? JSON.parse(response.body) : response.body)
|
15
|
+
self._status = response.code
|
16
|
+
self._headers = response.headers
|
17
|
+
|
18
|
+
case _status
|
19
|
+
when 200, 201
|
20
|
+
self._entity = Devtunnel::Entity.new(_body)
|
21
|
+
when 400,401,403,404,500
|
22
|
+
if _body["errors"]
|
23
|
+
self.errors = _body["errors"]
|
24
|
+
# quickly look to see if we have authentication errors. We want to raise
|
25
|
+
# exceptions on api key errors.
|
26
|
+
case errors[0]["code"]
|
27
|
+
when "002"
|
28
|
+
raise InvalidApiKey.new(_status, _hearders, _body)
|
29
|
+
when "007"
|
30
|
+
raise ExpiredApiKey.new(_status, _hearders, _body)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
else
|
34
|
+
# TODO: test unknown resonse and make sure the client can deal with it.
|
35
|
+
raise UnknownResponse.new(_status, _headers, _body)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# enumeratable methods and such
|
40
|
+
def length
|
41
|
+
_entity.list.length if _entity.list
|
42
|
+
end
|
43
|
+
def first
|
44
|
+
if _entity.list
|
45
|
+
_entity.list.first
|
46
|
+
else
|
47
|
+
# TODO: some sort of exception
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def success?
|
52
|
+
errors.nil? or errors.empty?
|
53
|
+
end
|
54
|
+
|
55
|
+
def total_results
|
56
|
+
return nil unless _entity.meta
|
57
|
+
_entity.meta["total_results"]
|
58
|
+
end
|
59
|
+
def offset
|
60
|
+
return nil unless _entity.meta
|
61
|
+
_entity.meta["offset"]
|
62
|
+
end
|
63
|
+
def limit
|
64
|
+
return nil unless _entity.meta
|
65
|
+
_entity.meta["limit"]
|
66
|
+
end
|
67
|
+
|
68
|
+
# if we're trying to access a method directly on the ApiResponse,
|
69
|
+
# the user is probably trying to get an attribute directly from
|
70
|
+
# the single entity that was returned. In this case, we'll simply
|
71
|
+
# look to see if the
|
72
|
+
def method_missing(meth, *args, &block)
|
73
|
+
return nil unless _entity
|
74
|
+
if _entity.object
|
75
|
+
if _entity.object.respond_to? meth
|
76
|
+
_entity.object.send(meth, *args, &block)
|
77
|
+
else
|
78
|
+
raise NoMethodError.new("Unknown attribute #{meth} on ApiResponse or #{_entity.object.class} entity.")
|
79
|
+
end
|
80
|
+
elsif _entity.list
|
81
|
+
if _entity.list.respond_to? meth
|
82
|
+
_entity.list.send(meth, *args, &block)
|
83
|
+
else
|
84
|
+
raise NoMethodError.new("Unknown attribute #{meth} on ApiResponse or #{_entity.list.class} list.")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
class JsonParseError < StandardError; end
|
92
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require_relative "account"
|
2
|
+
require_relative "success_response"
|
3
|
+
|
4
|
+
module Devtunnel
|
5
|
+
class Entity
|
6
|
+
attr_accessor :list, :object, :meta
|
7
|
+
|
8
|
+
def initialize(json)
|
9
|
+
self.meta = json["meta"] if json["meta"]
|
10
|
+
if json["list"]
|
11
|
+
self.list = Entity.build_from_list(json["list"])
|
12
|
+
else
|
13
|
+
self.object = Entity.build_from_hash(json)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.build_from_list(list)
|
18
|
+
list.map do |e|
|
19
|
+
if e.is_a? Hash
|
20
|
+
self.build_from_hash(e)
|
21
|
+
elsif e.is_a? Array
|
22
|
+
self.build_from_list(e)
|
23
|
+
else
|
24
|
+
# TODO: throw some weird entity error
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.build_from_hash(hash)
|
30
|
+
# our hash should always include a class attribute that we can use
|
31
|
+
# to map back to a proper Entity type
|
32
|
+
#
|
33
|
+
if !hash["success"].nil? and hash.keys.length == 1
|
34
|
+
return Devtunnel::SuccessResponse.new hash["success"]
|
35
|
+
end
|
36
|
+
|
37
|
+
hash["new_record"] = false
|
38
|
+
case hash["class"]
|
39
|
+
when "account"
|
40
|
+
Devtunnel::Account.new hash
|
41
|
+
else
|
42
|
+
raise NoSuchEntity.new("Unknown return type in API response data: #{hash["class"]}")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class NoSuchEntity < StandardError;
|
48
|
+
end
|
49
|
+
end
|
data/lib/devtunnel/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: devtunnel
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bryan Thompson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-07-
|
11
|
+
date: 2013-07-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -43,7 +43,8 @@ description: Uses your DevTunnel account to quickly and easily tunnel a local de
|
|
43
43
|
random NAT
|
44
44
|
email:
|
45
45
|
- bryan@madebymarket.com
|
46
|
-
executables:
|
46
|
+
executables:
|
47
|
+
- devtunnel
|
47
48
|
extensions: []
|
48
49
|
extra_rdoc_files: []
|
49
50
|
files:
|
@@ -52,8 +53,16 @@ files:
|
|
52
53
|
- LICENSE.txt
|
53
54
|
- README.md
|
54
55
|
- Rakefile
|
56
|
+
- bin/devtunnel
|
55
57
|
- devtunnel.gemspec
|
56
58
|
- lib/devtunnel.rb
|
59
|
+
- lib/devtunnel/account.rb
|
60
|
+
- lib/devtunnel/api.rb
|
61
|
+
- lib/devtunnel/api_error.rb
|
62
|
+
- lib/devtunnel/api_model.rb
|
63
|
+
- lib/devtunnel/api_response.rb
|
64
|
+
- lib/devtunnel/entity.rb
|
65
|
+
- lib/devtunnel/success_response.rb
|
57
66
|
- lib/devtunnel/version.rb
|
58
67
|
homepage: http://devtunnel.labs.madebymarket.com
|
59
68
|
licenses:
|