purr 0.1.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/.gitignore +9 -0
- data/.rspec +2 -0
- data/.rubocop.yml +22 -0
- data/.travis.yml +8 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +102 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/contrib/chrome/_locales/en/messages.json +1 -0
- data/contrib/chrome/images/icon-128.png +0 -0
- data/contrib/chrome/images/icon-16.png +0 -0
- data/contrib/chrome/index.html +34 -0
- data/contrib/chrome/manifest.json +35 -0
- data/contrib/chrome/scripts/background.js +26 -0
- data/contrib/chrome/scripts/main.js +178 -0
- data/contrib/chrome/styles/main.css +20 -0
- data/lib/purr.rb +33 -0
- data/lib/purr/server.rb +92 -0
- data/lib/purr/version.rb +4 -0
- data/purr.gemspec +32 -0
- metadata +164 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 00f5cd3df556c392a0350b07a3f301621cbb85e5
|
4
|
+
data.tar.gz: 46c2526afaf17d40188a57c6becf4eb44779dc31
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 356fbf9104ea61edcda8f7333e35f445b8be80c55db506091707065e2ba52ddc5958294dd40e083a82397d9079e5e83d62b435759f0c887add575c8e3a0a4781
|
7
|
+
data.tar.gz: 371811743c189dcdffe3d3e42cc72d544f80077226ecd81309db4d6f8a0802edc0072a1e1d33fd72a14bfa9c95694b87fb19cbe03419584ee765208a34bcbc0b
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Metrics/PerceivedComplexity:
|
2
|
+
Enabled: false
|
3
|
+
Metrics/CyclomaticComplexity:
|
4
|
+
Enabled: false
|
5
|
+
Metrics/AbcSize:
|
6
|
+
Enabled: false
|
7
|
+
Style/RedundantFreeze:
|
8
|
+
Enabled: false
|
9
|
+
Style/IndentationConsistency:
|
10
|
+
EnforcedStyle: normal
|
11
|
+
Metrics/LineLength:
|
12
|
+
Max: 140
|
13
|
+
Style/HashSyntax:
|
14
|
+
EnforcedStyle: hash_rockets
|
15
|
+
Documentation:
|
16
|
+
Enabled: false
|
17
|
+
Metrics/MethodLength:
|
18
|
+
Max: 20
|
19
|
+
Style/FileName:
|
20
|
+
Exclude:
|
21
|
+
- lib/surro-gate.rb
|
22
|
+
- spec/surro-gate_spec.rb
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Dávid Halász
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
# Purr
|
2
|
+
|
3
|
+
[](https://travis-ci.org/skateman/purr)
|
4
|
+
[](https://gemnasium.com/skateman/purr)
|
5
|
+
[](http://inch-ci.org/github/skateman/purr)
|
6
|
+
[](https://codeclimate.com/github/skateman/purr)
|
7
|
+
[](https://codecov.io/gh/skateman/purr)
|
8
|
+
|
9
|
+
Purr is a TCP-over-HTTP solution which consists:
|
10
|
+
- a Rack-based web server implemented in Ruby
|
11
|
+
- a browser extension implemented in ES6 using Chrome App JavaScript API
|
12
|
+
|
13
|
+
Using Purr it's possible to "smuggle" any kind of TCP traffic (SSH, VNC, etc.) through an HTTP connection.
|
14
|
+
|
15
|
+
**Note: this is a highly experimental implementation for demonstration purposes only!**
|
16
|
+
|
17
|
+
## How it works
|
18
|
+
|
19
|
+
**TODO**
|
20
|
+
|
21
|
+
## Installation
|
22
|
+
|
23
|
+
### Server
|
24
|
+
|
25
|
+
Add this line to your application's Gemfile:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
gem 'purr'
|
29
|
+
```
|
30
|
+
|
31
|
+
And then execute:
|
32
|
+
|
33
|
+
$ bundle
|
34
|
+
|
35
|
+
Or install it yourself as:
|
36
|
+
|
37
|
+
$ gem install purr
|
38
|
+
|
39
|
+
### Client
|
40
|
+
|
41
|
+
Currently, the client is available as a Chrome App only and it requires manual installation. It is available under the `contrib/chrome` folder and it needs to be installed manually. Note that the Chrome Apps will be [retired](https://blog.chromium.org/2016/08/from-chrome-apps-to-web.html) on other platforms than ChromeOS and this client might get unsupported in future versions of Chrome. It is planned to implement the client using a different approach in the future.
|
42
|
+
|
43
|
+
## Usage
|
44
|
+
|
45
|
+
### Server
|
46
|
+
The server needs to be wrapped as a Rack application and it's necessary to pass a block that takes one argument. This block should implement the TCP remote endpoint selection based on the **env** variable passed from the Rack context. The endpoint should be in the form of a two element array containing the host as a string and the port as an integer. There is a basic logging support implemented, but it is requires the `Rack::Logger` middleware to be included.
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
# purr.ru
|
50
|
+
require 'purr'
|
51
|
+
|
52
|
+
# Turn on the optional logging feature
|
53
|
+
use Rack::Logger
|
54
|
+
|
55
|
+
app = Purr.server do |env|
|
56
|
+
# Maybe do some database lookup based on the Rack environment
|
57
|
+
# ...
|
58
|
+
# Return with the remote endpoint
|
59
|
+
['localhost', 22]
|
60
|
+
end
|
61
|
+
|
62
|
+
run app
|
63
|
+
```
|
64
|
+
|
65
|
+
The application can be started using `rackup`:
|
66
|
+
```sh
|
67
|
+
rackup purr.ru
|
68
|
+
```
|
69
|
+
|
70
|
+
Note that the application requires a web server with [socket hijacking](http://www.rubydoc.info/github/rack/rack/file/SPEC#Hijacking) support, i.e. you can't use WEBrick.
|
71
|
+
|
72
|
+
### Client
|
73
|
+
The client can be invoked by pointing your browser to an URL in the form: `http://purr/<URL>`
|
74
|
+
|
75
|
+
Where the `<URL>` is an URL encoded using [`encodeURIComponent`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) pointing to the server described above.
|
76
|
+
|
77
|
+
Because the client catches the URL, it will never appear in the browser's address bar. Therefore, it is not recommended to use `window.open` or `window.location.href` for invoking the client as it will create an empty window. A better solution is to use `window.location.assign` from and existing window with "useful data":
|
78
|
+
|
79
|
+
```js
|
80
|
+
window.location.assign(`http://purr/${encodeURIComponent('http://example.com/vnc?id=1234')}`)
|
81
|
+
```
|
82
|
+
|
83
|
+
### Reverse proxy support
|
84
|
+
|
85
|
+
- TODO: Describe websocket compatibility mode and how it works with Apache mod_proxy_wstunnel
|
86
|
+
- TODO: Test with nginx
|
87
|
+
|
88
|
+
## Development
|
89
|
+
|
90
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
91
|
+
|
92
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
93
|
+
|
94
|
+
## Contributing
|
95
|
+
|
96
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/skateman/purr.
|
97
|
+
|
98
|
+
|
99
|
+
## License
|
100
|
+
|
101
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
102
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "purr"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
{}
|
Binary file
|
Binary file
|
@@ -0,0 +1,34 @@
|
|
1
|
+
<!doctype html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8">
|
5
|
+
<title>Purr Client</title>
|
6
|
+
<link rel="stylesheet" type="text/css" href="styles/main.css"/>
|
7
|
+
</head>
|
8
|
+
<body>
|
9
|
+
<div class="container">
|
10
|
+
<h1>Purr Client v<span class="version"></span></h1>
|
11
|
+
<div class="info-messages">
|
12
|
+
<p class="init">
|
13
|
+
Initializing connection, please wait...
|
14
|
+
</p>
|
15
|
+
<p class="work hidden">
|
16
|
+
Listening on: <strong class="address" title="Click to copy!"></strong><br/>
|
17
|
+
Connected clients: <span class="clients">0</span>
|
18
|
+
</p>
|
19
|
+
<p class="error hidden">
|
20
|
+
<strong>Error:</strong> <span class="errmsg"></span>
|
21
|
+
</p>
|
22
|
+
</div>
|
23
|
+
<div class="settings">
|
24
|
+
<label>Compatibility:</label>
|
25
|
+
<select id="websocket">
|
26
|
+
<option value="purr">None</option>
|
27
|
+
<option value="websocket">Fake WebSocket</option>
|
28
|
+
<!-- <option value="">Full WebSocket</option> -->
|
29
|
+
</select>
|
30
|
+
</div>
|
31
|
+
</div>
|
32
|
+
<script src="scripts/main.js"></script>
|
33
|
+
</body>
|
34
|
+
</html>
|
@@ -0,0 +1,35 @@
|
|
1
|
+
{
|
2
|
+
"name": "Purr Client",
|
3
|
+
"description": "Smuggle TCP connections through HTTP",
|
4
|
+
"default_locale": "en",
|
5
|
+
"version": "0.1.0",
|
6
|
+
"manifest_version": 2,
|
7
|
+
"sockets": {
|
8
|
+
"tcp": {
|
9
|
+
"connect": "*"
|
10
|
+
},
|
11
|
+
"tcpServer": {
|
12
|
+
"listen": "*"
|
13
|
+
}
|
14
|
+
},
|
15
|
+
"url_handlers": {
|
16
|
+
"launch_proxy": {
|
17
|
+
"matches": [
|
18
|
+
"http://purr/*",
|
19
|
+
"https://purr/*"
|
20
|
+
],
|
21
|
+
"title": "Launch Purr Client"
|
22
|
+
}
|
23
|
+
},
|
24
|
+
"icons": {
|
25
|
+
"16": "images/icon-16.png",
|
26
|
+
"128": "images/icon-128.png"
|
27
|
+
},
|
28
|
+
"app": {
|
29
|
+
"background": {
|
30
|
+
"scripts": [
|
31
|
+
"scripts/background.js"
|
32
|
+
]
|
33
|
+
}
|
34
|
+
}
|
35
|
+
}
|
@@ -0,0 +1,26 @@
|
|
1
|
+
'use strict';
|
2
|
+
|
3
|
+
chrome.app.runtime.onLaunched.addListener((data) => {
|
4
|
+
// Pass the url for further processing
|
5
|
+
window.url = data.url;
|
6
|
+
// Create the application window
|
7
|
+
chrome.app.window.create('index.html', {
|
8
|
+
innerBounds: {
|
9
|
+
width: 400,
|
10
|
+
height: 160
|
11
|
+
}
|
12
|
+
}, (win) =>
|
13
|
+
// Clean up all sockets if the app window gets closed
|
14
|
+
win.onClosed.addListener(() =>
|
15
|
+
['tcp', 'tcpServer'].forEach((provider) =>
|
16
|
+
chrome.sockets[provider].getSockets((sockets) =>
|
17
|
+
sockets.forEach((socket) => {
|
18
|
+
chrome.sockets[provider].disconnect(socket.socketId);
|
19
|
+
chrome.sockets[provider].close(socket.socketId);
|
20
|
+
})
|
21
|
+
)
|
22
|
+
)
|
23
|
+
)
|
24
|
+
);
|
25
|
+
});
|
26
|
+
|
@@ -0,0 +1,178 @@
|
|
1
|
+
'use strict';
|
2
|
+
|
3
|
+
const showError = (msg) => {
|
4
|
+
document.querySelector('.error > .errmsg').textContent = msg;
|
5
|
+
showInfo('error');
|
6
|
+
};
|
7
|
+
|
8
|
+
const showInfo = (klass) => {
|
9
|
+
if (['init', 'work', 'error'].includes(klass)) {
|
10
|
+
document.querySelector(`.info-messages > :not(.hidden)`).classList.add('hidden');
|
11
|
+
document.querySelector(`.info-messages .${klass}`).classList.remove('hidden');
|
12
|
+
}
|
13
|
+
};
|
14
|
+
|
15
|
+
const copyToClipboard = (e) => {
|
16
|
+
var input = document.createElement('textarea');
|
17
|
+
document.body.appendChild(input);
|
18
|
+
input.value = e.target.textContent;
|
19
|
+
input.focus();
|
20
|
+
input.select();
|
21
|
+
document.execCommand('Copy');
|
22
|
+
input.remove();
|
23
|
+
};
|
24
|
+
|
25
|
+
const sendHttpRequest = (sock) => {
|
26
|
+
// Build up the HTTP request headers
|
27
|
+
const upgrade = document.querySelector('#websocket').value;
|
28
|
+
|
29
|
+
const req = `
|
30
|
+
GET ${window.request.path} HTTP/1.1 \r
|
31
|
+
Host: ${window.request.host} \r
|
32
|
+
Upgrade: ${upgrade} \r
|
33
|
+
Purr-Request: MEOW \r
|
34
|
+
Purr-Version: ${chrome.runtime.getManifest().version}\r
|
35
|
+
Connection: Upgrade \r
|
36
|
+
User-Agent: ${navigator.userAgent} \r
|
37
|
+
`.replace(/( {2,})|(^\n)/g, '').replace(/\r\n$/, '\r\n\r\n');
|
38
|
+
|
39
|
+
let buffer = new ArrayBuffer(req.length);
|
40
|
+
let writer = new Uint8Array(buffer);
|
41
|
+
// Convert it to the appropriate format
|
42
|
+
for (let i=0, len=req.length; i<len; i++) {
|
43
|
+
writer[i] = req.charCodeAt(i);
|
44
|
+
}
|
45
|
+
|
46
|
+
// Send it out
|
47
|
+
chrome.sockets.tcp.send(sock, buffer, () => null);
|
48
|
+
};
|
49
|
+
|
50
|
+
const windowLoaded = () => new Promise((resolve, reject) =>
|
51
|
+
document.addEventListener('DOMContentLoaded', () => {
|
52
|
+
document.querySelector('span.version').textContent = chrome.runtime.getManifest().version;
|
53
|
+
resolve();
|
54
|
+
})
|
55
|
+
);
|
56
|
+
|
57
|
+
const parseBackgroundURL = () => new Promise((resolve, reject) =>
|
58
|
+
chrome.runtime.getBackgroundPage((background) => {
|
59
|
+
if (background.url) {
|
60
|
+
var url = new URL(decodeURIComponent(background.url.replace(/^https?:\/\/purr\// ,'')));
|
61
|
+
var m = url.protocol.match(/^http(s?):$/);
|
62
|
+
|
63
|
+
if (m) {
|
64
|
+
window.request = {
|
65
|
+
hostname: url.hostname,
|
66
|
+
host: url.host,
|
67
|
+
port: url.port ? parseInt(url.port) : (m[1] ? 443 : 80),
|
68
|
+
path: url.pathname,
|
69
|
+
secure: !!m[1]
|
70
|
+
};
|
71
|
+
resolve();
|
72
|
+
} else {
|
73
|
+
reject('Invalid URL!');
|
74
|
+
}
|
75
|
+
} else {
|
76
|
+
reject('This application cannot be started separately!');
|
77
|
+
}
|
78
|
+
})
|
79
|
+
);
|
80
|
+
|
81
|
+
const createServer = () => new Promise((resolve, reject) =>
|
82
|
+
chrome.sockets.tcpServer.create({}, (server) =>
|
83
|
+
chrome.sockets.tcpServer.listen(server.socketId, '127.0.0.1', 8888, 0, (result) =>
|
84
|
+
chrome.sockets.tcpServer.getInfo(server.socketId, (info) => {
|
85
|
+
if (result < 0) {
|
86
|
+
reject(`tcpServer.listen returned with ${result}`);
|
87
|
+
} else {
|
88
|
+
document.querySelector('.address').textContent = `127.0.0.1:${info.localPort}`;
|
89
|
+
document.querySelector('.address').onclick = copyToClipboard;
|
90
|
+
showInfo('work');
|
91
|
+
resolve(server.socketId);
|
92
|
+
}
|
93
|
+
})
|
94
|
+
)
|
95
|
+
)
|
96
|
+
);
|
97
|
+
|
98
|
+
const setListeners = (promise) => {
|
99
|
+
// Set up the proxying
|
100
|
+
chrome.sockets.tcp.onReceive.addListener((recv) => {
|
101
|
+
let node = window.pairing[recv.socketId];
|
102
|
+
if (node.purr) { // Synchronize
|
103
|
+
// Convert the response to a readable format
|
104
|
+
let response = String.fromCharCode.apply(null, new Uint8Array(recv.data));
|
105
|
+
|
106
|
+
if (response.match(/^HTTP\/1\.1 101 Switching Protocols/)) {
|
107
|
+
// If the upgrade was successful, unpause the socket
|
108
|
+
chrome.sockets.tcp.setPaused(node.pair, false, () =>
|
109
|
+
delete node.purr
|
110
|
+
);
|
111
|
+
} else {
|
112
|
+
// The upgrade was not successful, close the connection
|
113
|
+
console.error('Error happened during HTTP upgrade...')
|
114
|
+
cleanupClient(recv.socketId);
|
115
|
+
}
|
116
|
+
|
117
|
+
} else { // Transmit normally
|
118
|
+
chrome.sockets.tcp.send(node.pair, recv.data, () => null);
|
119
|
+
}
|
120
|
+
});
|
121
|
+
|
122
|
+
// Error handling for client connections
|
123
|
+
chrome.sockets.tcp.onReceiveError.addListener((err) =>
|
124
|
+
cleanupClient(err.socketId)
|
125
|
+
);
|
126
|
+
|
127
|
+
// Keeping up the promise-chain
|
128
|
+
return promise;
|
129
|
+
};
|
130
|
+
|
131
|
+
const createClient = (peer) => new Promise((resolve, reject) =>
|
132
|
+
chrome.sockets.tcp.create({}, (client) =>
|
133
|
+
chrome.sockets.tcp.connect(client.socketId, window.request.hostname, window.request.port, (result) => {
|
134
|
+
if (result < 0) {
|
135
|
+
reject(`tcp.connect returned with ${result}`);
|
136
|
+
} else {
|
137
|
+
// Set up socket pairing information
|
138
|
+
window.pairing[peer] = { pair: client.socketId };
|
139
|
+
window.pairing[client.socketId] = { pair: peer, purr: true };
|
140
|
+
updateClients();
|
141
|
+
resolve(client.socketId);
|
142
|
+
}
|
143
|
+
})
|
144
|
+
)
|
145
|
+
);
|
146
|
+
|
147
|
+
const cleanupClient = (sock) => {
|
148
|
+
let node = window.pairing[sock];
|
149
|
+
if (node) { // Do not close them twice
|
150
|
+
delete window.pairing[sock];
|
151
|
+
delete window.pairing[node.pair];
|
152
|
+
|
153
|
+
updateClients();
|
154
|
+
|
155
|
+
chrome.sockets.tcp.close(sock);
|
156
|
+
chrome.sockets.tcp.close(node.pair);
|
157
|
+
}
|
158
|
+
};
|
159
|
+
|
160
|
+
const acceptServer = (server) => {
|
161
|
+
chrome.sockets.tcpServer.onAccept.addListener((client) => {
|
162
|
+
createClient(client.clientSocketId).then(sendHttpRequest, showError);
|
163
|
+
return server;
|
164
|
+
})
|
165
|
+
};
|
166
|
+
|
167
|
+
const updateClients = () => {
|
168
|
+
document.querySelector('.clients').textContent = parseInt(Object.keys(pairing).length / 2);
|
169
|
+
};
|
170
|
+
|
171
|
+
window.pairing = {};
|
172
|
+
|
173
|
+
windowLoaded()
|
174
|
+
.then(parseBackgroundURL)
|
175
|
+
.then(createServer)
|
176
|
+
.then(setListeners)
|
177
|
+
.then(acceptServer)
|
178
|
+
.catch(showError);
|
@@ -0,0 +1,20 @@
|
|
1
|
+
strong.address:hover {
|
2
|
+
cursor: pointer;
|
3
|
+
text-decoration: underline;
|
4
|
+
}
|
5
|
+
|
6
|
+
.error {
|
7
|
+
color: red;
|
8
|
+
}
|
9
|
+
|
10
|
+
.hidden {
|
11
|
+
display: none;
|
12
|
+
}
|
13
|
+
|
14
|
+
.container {
|
15
|
+
text-align: center;
|
16
|
+
}
|
17
|
+
|
18
|
+
.info-messages {
|
19
|
+
font-size: larger;
|
20
|
+
}
|
data/lib/purr.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'purr/version'
|
2
|
+
require 'purr/server'
|
3
|
+
|
4
|
+
# A Rack-based server capable of smuggling TCP traffic through a persisent HTTP connection
|
5
|
+
#
|
6
|
+
# It uses the Rack socket hijacking API for accessing the TCP level of an incoming HTTP session.
|
7
|
+
# The remote endpoint selection should be implemented as a block passed to the server returning
|
8
|
+
# an two element array containing a host string and a port integer.
|
9
|
+
#
|
10
|
+
# @example Simple rackup file for a local SSH connection
|
11
|
+
# require 'purr'
|
12
|
+
#
|
13
|
+
# # This middleware is to support optional logging
|
14
|
+
# use Rack::Logger
|
15
|
+
#
|
16
|
+
# app = Purr.server do |env|
|
17
|
+
# ['localhost', 22]
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# run app
|
21
|
+
#
|
22
|
+
# @see http://www.rubydoc.info/github/rack/rack/master/file/SPEC#Hijacking
|
23
|
+
|
24
|
+
module Purr
|
25
|
+
class << self
|
26
|
+
# Creates or returns a singleton instance of the Rack-server
|
27
|
+
#
|
28
|
+
# @see Purr::Server.initialize
|
29
|
+
def server(&block)
|
30
|
+
@server ||= Server.new(&block)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/purr/server.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'surro-gate'
|
2
|
+
|
3
|
+
module Purr
|
4
|
+
# This class implements a Rack-based server with socket hijacking and proxying to a remote TCP endpoint.
|
5
|
+
#
|
6
|
+
# The remote TCP endpoint selection is implemented by passing a block to the class instantiation.
|
7
|
+
# If any kind of error happens during the hijacking process a 404 error is returned to the requester.
|
8
|
+
class Server
|
9
|
+
# @yield [env] The block responsible for the remote TCP endpoint selection
|
10
|
+
# @yieldparam env [Hash] The environment hash returned by the Rack middleware
|
11
|
+
# @yieldreturn [Array[String, Integer]] The host:port pair as a two element array
|
12
|
+
# @raise [ArgumentError] If the passed block takes other number of arguments than one
|
13
|
+
def initialize(&block)
|
14
|
+
raise ArgumentError, 'The method requires a block with a single argument' unless block && block.arity == 1
|
15
|
+
|
16
|
+
@remote = block
|
17
|
+
@proxy = SurroGate.new
|
18
|
+
end
|
19
|
+
|
20
|
+
# Method required by the Rack API
|
21
|
+
#
|
22
|
+
# @see https://rack.github.io/
|
23
|
+
def call(env)
|
24
|
+
upgrade = parse_headers(env)
|
25
|
+
# Return with a 404 error if the upgrade header is not present
|
26
|
+
return not_found unless %i(websocket purr).include?(upgrade)
|
27
|
+
|
28
|
+
host, port = @remote.call(env)
|
29
|
+
# Return with a 404 error if no host:port pair was determined
|
30
|
+
if host.nil? || port.nil?
|
31
|
+
logger(env, :error, "No matching endpoint found for request incoming from #{env['REMOTE_ADDR']}")
|
32
|
+
return not_found
|
33
|
+
end
|
34
|
+
|
35
|
+
# Hijack the HTTP socket from the Rack middleware
|
36
|
+
http = env['rack.hijack'].call
|
37
|
+
# Write a proper HTTP response
|
38
|
+
http.write(http_response(upgrade))
|
39
|
+
# Open the remote TCP socket
|
40
|
+
sock = TCPSocket.new(host, port)
|
41
|
+
|
42
|
+
# Start proxying
|
43
|
+
@proxy.push(http, sock)
|
44
|
+
logger(env, :info, "Redirecting incoming request from #{env['REMOTE_ADDR']} to [#{host}]:#{port}")
|
45
|
+
|
46
|
+
# Rack requires this line below
|
47
|
+
return [200, {}, []]
|
48
|
+
rescue => ex
|
49
|
+
logger(env, :error, "#{ex.class} happened for #{env['REMOTE_ADDR']} trying to access #{host}:#{port}")
|
50
|
+
# Clean up the opened sockets if available
|
51
|
+
http.close unless http.nil? || http.closed?
|
52
|
+
sock.close unless sock.nil? || sock.closed?
|
53
|
+
# Return with a 404 error
|
54
|
+
return not_found
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def parse_headers(env)
|
60
|
+
case true
|
61
|
+
when env['HTTP_PURR_REQUEST'] != 'MEOW'
|
62
|
+
logger(env, :error, "Invalid request from #{env['REMOTE_ADDR']}")
|
63
|
+
when !SUPPORT.include?(env['HTTP_PURR_VERSION'])
|
64
|
+
logger(env, :error, "Unsupported client from #{env['REMOTE_ADDR']}")
|
65
|
+
when %w(websocket purr).include?(env['HTTP_UPGRADE'])
|
66
|
+
logger(env, :info, "Upgrading to #{env['HTTP_UPGRADE']}")
|
67
|
+
return env['HTTP_UPGRADE'].to_sym
|
68
|
+
else
|
69
|
+
logger(env, :error, "Invalid upgrade request from #{env['REMOTE_ADDR']}")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def http_response(upgrade)
|
74
|
+
<<~HEREDOC.sub(/\n$/, "\n\n").gsub(/ {2,}/, '').gsub("\n", "\r\n")
|
75
|
+
HTTP/1.1 101 Switching Protocols
|
76
|
+
Upgrade: #{upgrade}
|
77
|
+
PURR_VERSION: #{Purr::VERSION}
|
78
|
+
PURR_REQUEST: MEOW
|
79
|
+
Connection: Upgrade
|
80
|
+
HEREDOC
|
81
|
+
end
|
82
|
+
|
83
|
+
def not_found
|
84
|
+
[404, { 'Content-Type' => 'text/plain' }, ['Not found!']]
|
85
|
+
end
|
86
|
+
|
87
|
+
def logger(env, level, message)
|
88
|
+
# Do logging only if Rack::Logger is loaded as a middleware
|
89
|
+
env['rack.logger'].send(level, message) if env['rack.logger']
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/purr/version.rb
ADDED
data/purr.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'purr/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'purr'
|
8
|
+
spec.version = Purr::VERSION
|
9
|
+
spec.authors = ['Dávid Halász']
|
10
|
+
spec.email = ['skateman@skateman.eu']
|
11
|
+
|
12
|
+
spec.summary = 'Smuggle TCP connections through HTTP'
|
13
|
+
spec.description = 'Smuggle TCP connections through HTTP'
|
14
|
+
spec.homepage = 'https://github.com/skateman/purr'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
19
|
+
end
|
20
|
+
spec.bindir = 'exe'
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
|
+
spec.require_paths = ['lib']
|
23
|
+
|
24
|
+
spec.add_dependency 'rack', '~> 2.0.0'
|
25
|
+
spec.add_dependency 'surro-gate', '~> 0.1.1'
|
26
|
+
|
27
|
+
spec.add_development_dependency 'bundler', '~> 1.13'
|
28
|
+
spec.add_development_dependency 'codecov', '~> 0.1.0'
|
29
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
30
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
31
|
+
spec.add_development_dependency 'simplecov', '~> 0.12'
|
32
|
+
end
|
metadata
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: purr
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dávid Halász
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-11-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rack
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.0.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.0.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: surro-gate
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.1.1
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.1.1
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.13'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.13'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: codecov
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.1.0
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.1.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '10.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '10.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: simplecov
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.12'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.12'
|
111
|
+
description: Smuggle TCP connections through HTTP
|
112
|
+
email:
|
113
|
+
- skateman@skateman.eu
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- ".gitignore"
|
119
|
+
- ".rspec"
|
120
|
+
- ".rubocop.yml"
|
121
|
+
- ".travis.yml"
|
122
|
+
- Gemfile
|
123
|
+
- LICENSE.txt
|
124
|
+
- README.md
|
125
|
+
- Rakefile
|
126
|
+
- bin/console
|
127
|
+
- bin/setup
|
128
|
+
- contrib/chrome/_locales/en/messages.json
|
129
|
+
- contrib/chrome/images/icon-128.png
|
130
|
+
- contrib/chrome/images/icon-16.png
|
131
|
+
- contrib/chrome/index.html
|
132
|
+
- contrib/chrome/manifest.json
|
133
|
+
- contrib/chrome/scripts/background.js
|
134
|
+
- contrib/chrome/scripts/main.js
|
135
|
+
- contrib/chrome/styles/main.css
|
136
|
+
- lib/purr.rb
|
137
|
+
- lib/purr/server.rb
|
138
|
+
- lib/purr/version.rb
|
139
|
+
- purr.gemspec
|
140
|
+
homepage: https://github.com/skateman/purr
|
141
|
+
licenses:
|
142
|
+
- MIT
|
143
|
+
metadata: {}
|
144
|
+
post_install_message:
|
145
|
+
rdoc_options: []
|
146
|
+
require_paths:
|
147
|
+
- lib
|
148
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
154
|
+
requirements:
|
155
|
+
- - ">="
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '0'
|
158
|
+
requirements: []
|
159
|
+
rubyforge_project:
|
160
|
+
rubygems_version: 2.5.1
|
161
|
+
signing_key:
|
162
|
+
specification_version: 4
|
163
|
+
summary: Smuggle TCP connections through HTTP
|
164
|
+
test_files: []
|