purr 0.1.0 → 0.1.1

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 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
- }