pipe2me-client 0.2.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 45f85801ad57c6402d4c6aecbe01cd335e6363e5
4
+ data.tar.gz: c72fa32c04e45830226f4f5a103e112ef0448cc6
5
+ SHA512:
6
+ metadata.gz: 65c12c51649a8b1b01cdaf274012a9d3ac2accba70dff89064cc260c3c4423288fa40b2042a16f9c859eda270d5df0d789082db93d526aeb5386fc1c0ac6bb3c
7
+ data.tar.gz: f934aeef86119c2feb2c5fba0c824c59366f9d79d29d77173750397ac16b11077147f8e64e5e4d6b1df10e6480020b8ede0ce166969d34d9c5f5f56b8fd23689
data/README.mdown ADDED
@@ -0,0 +1,96 @@
1
+ # pipe2me client
2
+
3
+ This is the ruby client for the pipe2me server package.
4
+ The pipe2me client lets you publish local services to the public internet
5
+ with the help and orchestration of a pipe2me server. For more details
6
+ see [pipe2me](https://github.com/kinko/pipe2me)
7
+
8
+ ## Installation
9
+
10
+ gem install pipe2me-client
11
+ # optional: install man page
12
+ sudo rake install
13
+
14
+ ## Usage
15
+
16
+ Mini-example: This registers two tunnels with a single hostname for
17
+ two services on localhost (http on port 9090, https on port 9091).
18
+
19
+ # Setup tunnels. This responds with the domain name
20
+ > ./bin/pipe2me setup -p http,https --server http://pipe2.me --auth 123 --ports 9090,9091
21
+ pretty-ivory-horse.test.pipe2.me
22
+
23
+ # Review the assigned URLs:
24
+ > cat pipe2me.info.inc | grep URL
25
+ PIPE2ME_URLS_0=http://pretty-ivory-horse.test.pipe2.me:10003
26
+ PIPE2ME_URLS_1=https://pretty-ivory-horse.test.pipe2.me:10004
27
+
28
+ # Start the tunnels via foreman (but please review on using foreman)
29
+ > /bin/pipe2me start
30
+
31
+ See also the [example session](http://test.pipe2.me/example_session.html)
32
+ and the [man page](http://test.pipe2.me/pipe2me.1.html).
33
+
34
+ ## Testing
35
+
36
+ Tests are implemented using
37
+ [roundup](https://github.com/bmizerany/roundup/blob/master/INSTALLING#files).
38
+ To install roundup on OSX, run `brew install roundup`. Other systems are
39
+ supported as well, compare roundup's documentation for details.
40
+
41
+ The implemented tests are *integration tests* in the sense, that they test the
42
+ behaviour of the *pipe2me-client* package in connection to an external pipe2me
43
+ server. That means you should run a pipe2me server on your local machine. Note
44
+ that the server must be configured to support test mode. (In test mode a pipe2me
45
+ server accepts test auth tokens, that create short lived tunnels
46
+ with self-signed certificates.)
47
+
48
+ To run the tests against a locally installed test server run `rake`. Note that
49
+ the local test server is expected at "pipe2.dev:8080". You might have to adjust
50
+ the `/etc/hosts` file to add an entry
51
+
52
+ 127.0.0.1 pipe2.dev
53
+
54
+ Before submitting a pull request you should also run a test against the
55
+ test.pipe2.me instance, which is available most of the time for that purpose.
56
+ To do that, run `rake test:release`.
57
+
58
+ ## Auth tokens
59
+
60
+ As ports and domain names are sparse resources the pipe2me server API
61
+ requires the use of authorization tokens when requesting a tunnel. A
62
+ token is similar to a currency in that it describes which tunnels are
63
+ supported. This could limit the number of tunneled ports, the traffic
64
+ for those ports, whether or not a certificate is self-signed or signed
65
+ by a regular CA, etc.
66
+
67
+ The features of a token are not defined within this protocol. However,
68
+ there has to be one. Contact the administrator of the pipe2me server
69
+ to know more.
70
+
71
+ ### Auth tokens on test.pipe2.me
72
+
73
+ The test.pipe2.me server is configured to use some public tokens.
74
+
75
+ - a test token: this builds tunnels that are available for 5 minutes.
76
+ The test token is intended for use with automated test scenarios.
77
+ The current test token is `test@test.kinko.me`.
78
+ - a review token: this builds tunnels that are available for up to
79
+ one day. A review token is intended for get a feel for the pipe2me
80
+ package. The current review token is `review@test.kinko.me`.
81
+
82
+ If you need a longer lived token for development, review and/or test
83
+ feel free to contact us at contact@kinko.me.
84
+
85
+ If you want to use pipe2me for any practical purpose consider
86
+ signing up at https://pipe2.me, which should be online end of January.
87
+
88
+ ## Licensing
89
+
90
+ **The pipe2me client software** is (c) The Kinko Team, 2014 and released to you
91
+ under the terms of the MIT License (MIT), see COPYING.MIT for details.
92
+
93
+ The subdirectory lib/vendor contains third-party code, which is subject to its own copyrights.
94
+ Please see the respective source files for copyright information.
95
+
96
+ (c) The kinko team, 2014
data/bin/pipe2me ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+ $:.unshift "#{File.dirname(__FILE__)}/../lib"
3
+
4
+ require "pipe2me"
5
+ require "pipe2me/cli"
6
+ require "pipe2me/cli-foreman"
7
+ require "pipe2me/cli-monit"
8
+
9
+ begin
10
+ Pipe2me::CLI.start ARGV
11
+ rescue
12
+ if UI.verbosity >= 2
13
+ UI.error $!
14
+ raise
15
+ elsif UI.verbosity >= 0
16
+ UI.error $!
17
+ else
18
+ STDERR.puts $!
19
+ end
20
+ exit 1
21
+ end
data/lib/pipe2me.rb ADDED
@@ -0,0 +1,9 @@
1
+ module Pipe2me
2
+ end
3
+
4
+ require "simple/ui"
5
+
6
+ require_relative "pipe2me/version"
7
+ require_relative "pipe2me/config"
8
+ require_relative "pipe2me/tunnel"
9
+
@@ -0,0 +1,21 @@
1
+ class Pipe2me::CLI < Thor
2
+ desc "start", "Start tunnels"
3
+ option :echo, :type => :boolean, :banner => "Also run echo servers"
4
+ def start
5
+ procfile = options[:echo] ? "pipe2me.procfile.echo" : "pipe2me.procfile"
6
+
7
+ File.open procfile, "w" do |io|
8
+ Pipe2me::Tunnel.tunnel_commands.each do |name, cmd|
9
+ io.write "#{name}: #{cmd}\n"
10
+ end
11
+
12
+ next unless options[:echo]
13
+
14
+ Pipe2me::Tunnel.echo_commands.each do |name, cmd|
15
+ io.write "#{name}: #{cmd}\n"
16
+ end
17
+ end
18
+
19
+ Kernel.exec "foreman start -f #{procfile}"
20
+ end
21
+ end
@@ -0,0 +1,62 @@
1
+ class Pipe2me::CLI < Thor
2
+ desc "monit", "Create monitrc file and run monit"
3
+ option :port, :default => 5555, :banner => "control port"
4
+ option :echo, :type => :boolean, :banner => "Also run echo servers"
5
+ def monit(*args)
6
+ monitrc_file = create_monitrc
7
+
8
+ UI.warn "Running: monit -c #{monitrc_file} #{args.join(" ")}"
9
+ Kernel.exec "monit", "-c", monitrc_file, *args
10
+ end
11
+
12
+ desc "monitrc", "Create monitrc file"
13
+ option :port, :default => 5555, :banner => "control port"
14
+ option :echo, :type => :boolean, :banner => "Also run echo servers"
15
+ def monitrc
16
+ monitrc_file = create_monitrc
17
+ Kernel.exec "monit", "-c", monitrc_file, "-t"
18
+ end
19
+
20
+ private
21
+
22
+ def create_monitrc
23
+ path = options[:echo] ? "pipe2me.monitrc.echo" : "pipe2me.monitrc"
24
+
25
+ # The daemon binary
26
+ daemon_bin = `which daemon`.chomp
27
+ daemon = "#{daemon_bin} -D #{File.expand_path(Dir.getwd)}"
28
+
29
+ port = options[:port]
30
+
31
+ logfile = File.expand_path "pipe2me.monit.log"
32
+ piddir = File.expand_path "pipe2me.monit.pids"
33
+ FileUtils.mkdir_p piddir
34
+
35
+ commands = Pipe2me::Tunnel.tunnel_commands
36
+ commands += Pipe2me::Tunnel.echo_commands if options[:echo]
37
+
38
+ File.open path, "w", 0600 do |io|
39
+ require "erb"
40
+
41
+ erb = ERB.new MONITRC_ERB
42
+ io.write erb.result(self.send(:binding))
43
+ end
44
+
45
+ UI.success "Created #{path}"
46
+
47
+ path
48
+ end
49
+
50
+ MONITRC_ERB = %q{
51
+ set daemon 10
52
+ set httpd port <%= port %> and use address localhost allow localhost
53
+
54
+ <% commands.each do |name, cmd| %>
55
+ check process <%= name %> with pidfile <%= piddir %>/<%= name %>.pid
56
+ <% daemon = "#{daemon} -N --name #{name} --pidfiles #{piddir} --output #{logfile}" %>
57
+ start program = "<%= daemon %> -- <%= cmd %>" with timeout 60 seconds
58
+ stop program = "<%= daemon %> --stop"
59
+ <% end %>
60
+ }
61
+
62
+ end
@@ -0,0 +1,29 @@
1
+ require "thor"
2
+
3
+ class Pipe2me::CLI < Thor
4
+ def self.exit_on_failure?; true; end
5
+
6
+ desc "setup", "fetch a new tunnel setup"
7
+ option :server, :default => "http://test.pipe2.me:8080"
8
+ option :auth, :required => true # "auth token"
9
+ option :protocols, :default => "https" # "protocol names, e.g. 'http,https,imap'"
10
+ option :ports # "local ports, one per protocol"
11
+ def setup
12
+ Pipe2me::Config.server = options[:server]
13
+ server_info = Pipe2me::Tunnel.setup options
14
+
15
+ update
16
+ puts server_info[:fqdn]
17
+ end
18
+
19
+ desc "env", "show tunnel configuration"
20
+ def env(*args)
21
+ puts File.read("pipe2me.local.inc")
22
+ puts File.read("pipe2me.info.inc")
23
+ end
24
+
25
+ desc "update", "Updates configuration"
26
+ def update
27
+ Pipe2me::Tunnel.update
28
+ end
29
+ end
@@ -0,0 +1,7 @@
1
+ require "tmpdir"
2
+
3
+ module Pipe2me::Config
4
+ extend self
5
+
6
+ attr :server, true
7
+ end
@@ -0,0 +1,16 @@
1
+ require "tempfile"
2
+
3
+ class File
4
+ def self.atomic_write(path, data=nil, &block)
5
+ tmpfile = Tempfile.new(File.basename(path))
6
+ tmpfile.write data if data
7
+ yield(tmpfile) if block
8
+ tmpfile.write data if data
9
+ tmpfile.close
10
+ FileUtils.mv tmpfile.path, path # atomic replace
11
+ tmpfile.unlink # deletes the temp file
12
+ rescue
13
+ tmpfile.close! rescue nil # close and deletes the temp file
14
+ raise
15
+ end
16
+ end
@@ -0,0 +1,213 @@
1
+ require "net/http"
2
+ require "ostruct"
3
+
4
+ # The HTTP module implements a simple wrapper around Net::HTTP, intended to
5
+ # ease the pain of dealing with HTTP requests.
6
+ module Pipe2me::HTTP
7
+ extend self
8
+
9
+ # Error to be raised when maximum number of redirections is reached.
10
+ class RedirectionLimit < RuntimeError; end
11
+
12
+ # Base class for HTTP related errors.
13
+ class Error < RuntimeError
14
+
15
+ # The response object (of class HTTP::Response).
16
+ attr_reader :response
17
+
18
+ def initialize(response) #:nodoc:
19
+ @response = response
20
+ end
21
+
22
+ def code
23
+ response.code
24
+ end
25
+
26
+ def message #:nodoc:
27
+ "#{@response.response.class}: #{@response[0..120]}"
28
+ end
29
+ end
30
+
31
+ # Raised when a server responded with an error 5xx.
32
+ class ServerError < Error; end
33
+
34
+ # Raised when a server responded with an error 4xx.
35
+ class ResourceNotFound < Error; end
36
+
37
+ # -- configuration
38
+
39
+ @@config = OpenStruct.new
40
+
41
+ # The configuration object. It supports the following entries:
42
+ # - <tt>config.headers</tt>: default headers to use when doing HTTP requests. Default: "Ruby HTTP client/1.0"
43
+ # - <tt>config.max_redirections</tt>: the number of maximum redirections to follow. Default: 10
44
+ #
45
+ # To adjust the configuration change these objects, like so:
46
+ #
47
+ # HTTP.config.headers = { "User-Agent" => "My awesome thingy/1.0" }
48
+ def config
49
+ @@config
50
+ end
51
+
52
+ # A default set of headers
53
+ config.headers = {
54
+ "User-Agent" => "Ruby HTTP client/1.0"
55
+ }
56
+
57
+ # The default number of max redirections.
58
+ config.max_redirections = 10
59
+
60
+ # -- return types
61
+
62
+ # The HTTP::Response class works like a string, but contains extra "attributes"
63
+ # status and headers, which return the response status and response headers.
64
+ class Response < String
65
+
66
+ # The URL of the final request.
67
+ attr_reader :url
68
+
69
+ # The URL of the original request.
70
+ attr_reader :original_url
71
+
72
+ # The response object.
73
+ attr_reader :response
74
+
75
+ def initialize(response, url, original_url) #:nodoc:
76
+ @response, @url, @original_url = response, url, original_url
77
+ super(response.body || "")
78
+ end
79
+
80
+ # returns true if the status is in the 2xx range.
81
+ def valid?
82
+ (200..299).include? status
83
+ end
84
+
85
+ # returns the response object itself, if it is valid (i.e. has a ), or raise
86
+ def validate!
87
+ return self if valid?
88
+
89
+ case status
90
+ when 400..499 then raise ResourceNotFound, self
91
+ when 500..599 then raise ServerError, self
92
+ else raise Error, self
93
+ end
94
+ end
95
+
96
+ # returns the HTTP status code, as an Integer.
97
+ def code
98
+ @response.code.to_i
99
+ end
100
+
101
+ alias :status :code
102
+
103
+ # returns all headers.
104
+ def headers
105
+ @headers ||= {}.tap do |h|
106
+ @response.each_header do |key, value|
107
+ h[key] = value
108
+ end
109
+ end
110
+ end
111
+
112
+ def content_type
113
+ headers["content-type"]
114
+ end
115
+
116
+ def parse
117
+ case content_type
118
+ when /application\/json/
119
+ require "json" unless defined?(JSON)
120
+ JSON.parse(self)
121
+ else
122
+ UI.warn "#{url}: Cannot parse #{content_type.inspect} response"
123
+ self
124
+ end
125
+ end
126
+ end
127
+
128
+ # -- do requests ----------------------------------------------------
129
+
130
+ # runs a get request and return a HTTP::Response object.
131
+ def get(url, headers = {})
132
+ do_request Net::HTTP::Get, url, headers
133
+ end
134
+
135
+ # runs a post request and return a HTTP::Response object.
136
+ def post(url, body, headers = {})
137
+ do_request Net::HTTP::Post, url, headers, body
138
+ end
139
+
140
+ private
141
+
142
+ def method_missing(sym, *args, &block)
143
+ case sym.to_s
144
+ when /^(.*)\!$/
145
+ response = send $1, *args, &block
146
+ response.validate!
147
+ when /^(.*)\?$/
148
+ response = send $1, *args, &block
149
+ response if response.valid?
150
+ else
151
+ super
152
+ end
153
+ end
154
+
155
+ #:nodoc:
156
+ def do_request(verb, uri, headers, body = nil)
157
+ UI.benchmark :info, "[#{verb.name.gsub(/.*::/, "").upcase}] #{uri}" do
158
+ do_raw_request verb, uri, headers, body, config.max_redirections, uri, Net::HTTP::Get
159
+ end
160
+
161
+ rescue
162
+ raise "#{uri}: #{$!}"
163
+ end
164
+
165
+ def do_raw_request(verb, uri, headers, body, max_redirections, original_url, redirection_verb = Net::HTTP::Get)
166
+ # merge default headers
167
+ headers = config.headers.merge(headers)
168
+
169
+ # create connection
170
+
171
+ uri = URI.parse(uri) if uri.is_a?(String)
172
+
173
+ default_port = uri.scheme == "https" ? 443 : 80
174
+ http = Net::HTTP.new(uri.host, uri.port || default_port)
175
+ if uri.scheme == "https"
176
+ http.use_ssl = true
177
+ end
178
+
179
+ # create request and get response
180
+
181
+ request = verb.new(uri.request_uri)
182
+ request.initialize_http_header headers
183
+
184
+ if verb.const_get "REQUEST_HAS_BODY"
185
+ case body
186
+ when String
187
+ request.body = body
188
+ when Hash
189
+ request.form_data = body
190
+ when nil
191
+ else
192
+ raise ArgumentError, "Unsupported body of class #{body.class.name}"
193
+ end
194
+ end
195
+
196
+
197
+ request.basic_auth(r.user, r.password) if uri.user && uri.password
198
+ response = http.request(request)
199
+
200
+ # follow redirection, if needed
201
+
202
+ case response
203
+ when Net::HTTPRedirection
204
+ # Note: we always follow the redirect using a GET. This seems to violate parts of
205
+ # RFC 2616, but sadly seems the best default behaviour, as is implemented that way
206
+ # in many clients..
207
+ raise RedirectionLimit.new(original_url) if max_redirections <= 0
208
+ return do_raw_request redirection_verb, response["Location"], headers, max_redirections-1, original_url, redirection_verb
209
+ else
210
+ Response.new(response, uri.to_s, original_url)
211
+ end
212
+ end
213
+ end