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 +7 -0
- data/README.mdown +96 -0
- data/bin/pipe2me +21 -0
- data/lib/pipe2me.rb +9 -0
- data/lib/pipe2me/cli-foreman.rb +21 -0
- data/lib/pipe2me/cli-monit.rb +62 -0
- data/lib/pipe2me/cli.rb +29 -0
- data/lib/pipe2me/config.rb +7 -0
- data/lib/pipe2me/ext/file_ext.rb +16 -0
- data/lib/pipe2me/ext/http.rb +213 -0
- data/lib/pipe2me/ext/shell_format.rb +69 -0
- data/lib/pipe2me/ext/sys.rb +38 -0
- data/lib/pipe2me/tunnel.rb +78 -0
- data/lib/pipe2me/tunnel/commands.rb +68 -0
- data/lib/pipe2me/tunnel/echo/http +56 -0
- data/lib/pipe2me/tunnel/echo/https +18 -0
- data/lib/pipe2me/tunnel/openssl.conf +351 -0
- data/lib/pipe2me/tunnel/openssl.rb +29 -0
- data/lib/pipe2me/tunnel/ssh.rb +16 -0
- data/lib/pipe2me/version.rb +4 -0
- data/test/000-roundup-test.sh +7 -0
- data/test/auth-token-test.sh +12 -0
- data/test/env-test.sh +23 -0
- data/test/foreman-test.sh +11 -0
- data/test/monitrc-test.sh +23 -0
- data/test/opensslkey-test.sh +25 -0
- data/test/redirection-test.sh +12 -0
- data/test/setup-test.sh +18 -0
- data/test/sshkey-test.sh +13 -0
- data/test/testhelper.debug +2 -0
- data/test/testhelper.inc +24 -0
- data/test/testhelper.release +2 -0
- data/test/version-test.sh +9 -0
- metadata +131 -0
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,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
|
data/lib/pipe2me/cli.rb
ADDED
@@ -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,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
|