purr 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 00f5cd3df556c392a0350b07a3f301621cbb85e5
4
- data.tar.gz: 46c2526afaf17d40188a57c6becf4eb44779dc31
2
+ SHA256:
3
+ metadata.gz: e3a99873499dacf72389b46731532a26976bebeb3deae0f3bd0c244648f9993f
4
+ data.tar.gz: e0e21c759cd247edaf3fea274fb5c060ea144e8844587abe20be81bba3e00a38
5
5
  SHA512:
6
- metadata.gz: 356fbf9104ea61edcda8f7333e35f445b8be80c55db506091707065e2ba52ddc5958294dd40e083a82397d9079e5e83d62b435759f0c887add575c8e3a0a4781
7
- data.tar.gz: 371811743c189dcdffe3d3e42cc72d544f80077226ecd81309db4d6f8a0802edc0072a1e1d33fd72a14bfa9c95694b87fb19cbe03419584ee765208a34bcbc0b
6
+ metadata.gz: 9026cd13d3a65e47a3b9d9702e8142cd196b96ef896026f9bcf4452ad66b5d8912026bee050f46a70d58e65eca827ec5117af83b0f182db7740d40613b8589f0
7
+ data.tar.gz: c6d439bba854c136237198a1a2b455d0ec516d653658fd5198d2c5d663840c77691a67a3aa9927649f2e528906da216f563eb8f4f2ad12da91ef0b2252a71ba5
data/.rspec CHANGED
@@ -1,2 +1,2 @@
1
- --format documentation
1
+ --format progress
2
2
  --color
data/README.md CHANGED
@@ -1,27 +1,9 @@
1
- # Purr
1
+ # Purr Server
2
2
 
3
- [![Build Status](https://travis-ci.org/skateman/purr.svg?branch=master)](https://travis-ci.org/skateman/purr)
4
- [![Dependency Status](https://gemnasium.com/skateman/purr.svg)](https://gemnasium.com/skateman/purr)
5
- [![Inline docs](http://inch-ci.org/github/skateman/purr.svg?branch=master)](http://inch-ci.org/github/skateman/purr)
6
- [![Code Climate](https://codeclimate.com/github/skateman/purr/badges/gpa.svg)](https://codeclimate.com/github/skateman/purr)
7
- [![codecov](https://codecov.io/gh/skateman/purr/branch/master/graph/badge.svg)](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**
3
+ This is the server part of Purr responsible for socket hijacking and forwarding TCP traffic to the remote endpoint.
20
4
 
21
5
  ## Installation
22
6
 
23
- ### Server
24
-
25
7
  Add this line to your application's Gemfile:
26
8
 
27
9
  ```ruby
@@ -36,13 +18,8 @@ Or install it yourself as:
36
18
 
37
19
  $ gem install purr
38
20
 
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
21
  ## Usage
44
22
 
45
- ### Server
46
23
  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
24
 
48
25
  ```ruby
@@ -69,17 +46,6 @@ rackup purr.ru
69
46
 
70
47
  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
48
 
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
49
  ### Reverse proxy support
84
50
 
85
51
  - TODO: Describe websocket compatibility mode and how it works with Apache mod_proxy_wstunnel
@@ -87,7 +53,7 @@ window.location.assign(`http://purr/${encodeURIComponent('http://example.com/vnc
87
53
 
88
54
  ## Development
89
55
 
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.
56
+ After checking out the repo, run `bin/setup` in the `server` subdirectory to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
91
57
 
92
58
  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
59
 
@@ -95,8 +61,6 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
95
61
 
96
62
  Bug reports and pull requests are welcome on GitHub at https://github.com/skateman/purr.
97
63
 
98
-
99
64
  ## License
100
65
 
101
66
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
102
-
@@ -1,3 +1,5 @@
1
+ require "irb"
2
+ require 'socket'
1
3
  require 'surro-gate'
2
4
 
3
5
  module Purr
@@ -15,6 +17,24 @@ module Purr
15
17
 
16
18
  @remote = block
17
19
  @proxy = SurroGate.new
20
+
21
+ @transmitter = Thread.new do
22
+ loop do
23
+ @proxy.select(1000)
24
+
25
+ @proxy.each_ready do |left, right|
26
+ begin
27
+ right.write_nonblock(left.read_nonblock(4096))
28
+ rescue => ex
29
+ # FIXME: env is not available here, logging is probably bad in this way
30
+ # logger(env, :info, "Connection #{left} <-> #{right} closed due to #{ex}")
31
+ cleanup(left, right)
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ @transmitter.abort_on_exception = true
18
38
  end
19
39
 
20
40
  # Method required by the Rack API
@@ -47,11 +67,8 @@ module Purr
47
67
  return [200, {}, []]
48
68
  rescue => ex
49
69
  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
70
+ cleanup(http, sock)
71
+ return not_found # Return with a 404 error
55
72
  end
56
73
 
57
74
  private
@@ -74,8 +91,8 @@ module Purr
74
91
  <<~HEREDOC.sub(/\n$/, "\n\n").gsub(/ {2,}/, '').gsub("\n", "\r\n")
75
92
  HTTP/1.1 101 Switching Protocols
76
93
  Upgrade: #{upgrade}
77
- PURR_VERSION: #{Purr::VERSION}
78
- PURR_REQUEST: MEOW
94
+ Purr-Version: #{Purr::VERSION}
95
+ Purr-Request: MEOW
79
96
  Connection: Upgrade
80
97
  HEREDOC
81
98
  end
@@ -84,6 +101,14 @@ module Purr
84
101
  [404, { 'Content-Type' => 'text/plain' }, ['Not found!']]
85
102
  end
86
103
 
104
+ def cleanup(*sockets)
105
+ # Omit `nil`s from the array
106
+ sockets.compact!
107
+ # Close the opened sockets and remove them from the proxy
108
+ sockets.each { |sock| sock.close unless sock.closed? }
109
+ @proxy.pop(*sockets)
110
+ end
111
+
87
112
  def logger(env, level, message)
88
113
  # Do logging only if Rack::Logger is loaded as a middleware
89
114
  env['rack.logger'].send(level, message) if env['rack.logger']
@@ -1,4 +1,4 @@
1
1
  module Purr
2
- VERSION = '0.1.0'.freeze
3
- SUPPORT = %w(0.1.0).freeze
2
+ VERSION = '0.1.1'.freeze
3
+ SUPPORT = %w(0.1.0 0.1.1).freeze
4
4
  end
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.require_paths = ['lib']
23
23
 
24
24
  spec.add_dependency 'rack', '~> 2.0.0'
25
- spec.add_dependency 'surro-gate', '~> 0.1.1'
25
+ spec.add_dependency 'surro-gate', '~> 1.0.4'
26
26
 
27
27
  spec.add_development_dependency 'bundler', '~> 1.13'
28
28
  spec.add_development_dependency 'codecov', '~> 0.1.0'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: purr
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dávid Halász
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-11-24 00:00:00.000000000 Z
11
+ date: 2019-01-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 0.1.1
33
+ version: 1.0.4
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 0.1.1
40
+ version: 1.0.4
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: bundler
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -118,21 +118,12 @@ files:
118
118
  - ".gitignore"
119
119
  - ".rspec"
120
120
  - ".rubocop.yml"
121
- - ".travis.yml"
122
121
  - Gemfile
123
122
  - LICENSE.txt
124
123
  - README.md
125
124
  - Rakefile
126
125
  - bin/console
127
126
  - 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
127
  - lib/purr.rb
137
128
  - lib/purr/server.rb
138
129
  - lib/purr/version.rb
@@ -156,8 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
156
147
  - !ruby/object:Gem::Version
157
148
  version: '0'
158
149
  requirements: []
159
- rubyforge_project:
160
- rubygems_version: 2.5.1
150
+ rubygems_version: 3.0.2
161
151
  signing_key:
162
152
  specification_version: 4
163
153
  summary: Smuggle TCP connections through HTTP
@@ -1,8 +0,0 @@
1
- sudo: false
2
- language: ruby
3
- cache: bundler
4
- rvm:
5
- - 2.3.1
6
- before_install:
7
- - "echo 'gem: --no-ri --no-rdoc --no-document' > ~/.gemrc"
8
- - "gem install bundler -v 1.13.0"
@@ -1,34 +0,0 @@
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>
@@ -1,35 +0,0 @@
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
- }
@@ -1,26 +0,0 @@
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
-
@@ -1,178 +0,0 @@
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);
@@ -1,20 +0,0 @@
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
- }