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