netpump 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 298b321a2b63596d5247b8d8cd807722c5062249f8f2c9e1b44b30b030a528f3
4
+ data.tar.gz: c534a39e90383b0ac3896c8060d233dbd02c26c83563ed7d8c9d331b81c91370
5
+ SHA512:
6
+ metadata.gz: 95a11534001077454e72c94f94c81a2e816b613f9ab643ebaaeb06bbcf3e40f7210799130457d0af56677d2c0e5ac73ff8701426f778551c837f5b0ef9c3c711
7
+ data.tar.gz: c0a4abe22639b9667cdfdf879f63a2c05e96c8ed3eac0d3376506b3d2e094724d5c18933ca9464ad099a87560545dbac91a55324ee82707f337d3eb2cf3a567c
data/CHANGELOG ADDED
@@ -0,0 +1,41 @@
1
+ 1.0.0
2
+
3
+ * Add backpressure mechanism to keep memory usage bounded
4
+ * Add websocket pool timeout
5
+ * Add more unbind reasons
6
+ * Add option to set the server URL, remove the server query param
7
+ * Add direct client mode
8
+
9
+ 1.3.0
10
+
11
+ * Run all servers in one event loop
12
+ * Replace webrick with a simple static file server
13
+ * Add websocket pool
14
+ * Add extensive logging
15
+ * Show connection count in browser
16
+ * Drop SOCKS5 support
17
+ * Drop web manifest
18
+ * Drop websocket pinging
19
+
20
+ 1.2.0
21
+
22
+ * Prevent screen from sleeping (@zakir8)
23
+ * Send pings to keep websockets alive
24
+
25
+ 1.1.2
26
+
27
+ * Add webrick as a runtime dependency
28
+
29
+ 1.1.1
30
+
31
+ * Ignore interfaces with nil addresses
32
+
33
+ 1.1.0
34
+
35
+ * Works on Windows
36
+ * HTTPS proxy support
37
+
38
+ 1.0.0
39
+
40
+ * First public release
41
+ * SOCKS5 proxy support
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
data/bin/netpump ADDED
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env -S ruby
2
+
3
+ Process.setproctitle "netpump"
4
+ $stdin.close
5
+ if $PROGRAM_NAME.include?("/")
6
+ $VERBOSE = true
7
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
8
+ else
9
+ $VERBOSE = nil
10
+ end
11
+
12
+ require "optparse"
13
+ require "uri"
14
+ require "netpump"
15
+
16
+ copts = {}
17
+ sopts = {}
18
+
19
+ op = OptionParser.new do |op|
20
+ op.summary_width = 24
21
+
22
+ op.banner = "Usage: netpump [options]"
23
+ op.separator ""
24
+ op.separator "Options:"
25
+
26
+ Port = %r{\A[1-9][0-9]*\z} # Port zero is not supported.
27
+ op.accept(Port, nil) do |val|
28
+ port = Integer(val)
29
+ raise ArgumentError if port > 65_535
30
+ port
31
+ rescue ArgumentError
32
+ raise OptionParser::InvalidArgument, val
33
+ end
34
+
35
+ WSS = %r{\Awss?://.*\z}i
36
+ op.accept(WSS, nil) do |val|
37
+ URI(val)
38
+ rescue URI::InvalidURIError
39
+ raise OptionParser::InvalidArgument, val
40
+ end
41
+
42
+ op.on("-c", "--client MODE", ["direct", "browser"], "start netpump client (mode: direct, browser)") do |mode|
43
+ copts[:mode] = mode
44
+ end
45
+
46
+ op.on("--client-host HOST", String, "client host (default: 0.0.0.0)") do |host|
47
+ copts[:host] = host
48
+ end
49
+
50
+ op.on("--client-port PORT", Port, "client port (default: 8080)") do |port|
51
+ copts[:port] = port
52
+ end
53
+
54
+ op.on("--proxy-host HOST", String, "local proxy server host (default: 127.0.0.1)") do |host|
55
+ copts[:proxy_host] = host
56
+ end
57
+
58
+ op.on("--proxy-port PORT", Port, "local proxy server port (default: 3128)") do |port|
59
+ copts[:proxy_port] = port
60
+ end
61
+
62
+ op.on("--server-url URL", WSS, "netpump server url (example: wss://example.org)") do |url|
63
+ copts[:server_url] = url
64
+ end
65
+
66
+ op.on("-s", "--server", "start netpump server") do
67
+ sopts[:server] = true
68
+ end
69
+
70
+ op.on("--server-host HOST", String, "server host (default: 0.0.0.0)") do |host|
71
+ sopts[:host] = host
72
+ end
73
+
74
+ op.on("--server-port PORT", Port, "server port (default: 10000)") do |port|
75
+ sopts[:port] = port
76
+ end
77
+
78
+ op.on_tail("--version", "print version") do
79
+ version = File.expand_path("../VERSION", __dir__)
80
+ puts(File.read(version))
81
+ exit
82
+ end
83
+
84
+ op.on_tail("--help", "print this help") do
85
+ puts(op)
86
+ exit
87
+ end
88
+ end
89
+
90
+ begin
91
+ op.parse!
92
+ rescue OptionParser::ParseError => e
93
+ op.abort(e)
94
+ else
95
+ unless copts.key?(:mode) || sopts.key?(:server)
96
+ copts[:mode] = "direct"
97
+ end
98
+ end
99
+
100
+ EventMachine.error_handler { |e| log "[!] unexpected errback.", error: e }
101
+ EventMachine.run do
102
+ trap(:INT) { puts; EventMachine.stop }
103
+ if sopts.delete(:server)
104
+ server = Netpump::Server.new(**sopts)
105
+ server.start
106
+ end
107
+ if copts[:mode]
108
+ client = Netpump::Client.new(**copts)
109
+ client.start
110
+ end
111
+ rescue => e
112
+ log "[!] #{e.message}."
113
+ EventMachine.stop
114
+ end
@@ -0,0 +1,100 @@
1
+ require "securerandom"
2
+ require "em-websocket"
3
+ require "http_parser"
4
+
5
+ module EventMachine::WebSocket
6
+ class Client < Connection
7
+ def post_init
8
+ url = @options.fetch(:url)
9
+ if url.scheme == "wss"
10
+ @secure = true
11
+ @tls_options[:sni_hostname] ||= url.host
12
+ end
13
+ super
14
+ @version = 13
15
+ @key = SecureRandom.base64(16)
16
+ @parser = Http::Parser.new
17
+ @parser.on_headers_complete = proc do
18
+ headers = @parser.headers
19
+ @parser = nil
20
+ accept = Digest::SHA1.base64digest(@key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
21
+ unless headers["Connection"]&.downcase == "upgrade" &&
22
+ headers["Upgrade"]&.downcase == "websocket" &&
23
+ headers["Sec-WebSocket-Accept"] == accept
24
+ raise HandshakeError, "Invalid server handshake"
25
+ end
26
+ debug [:handshake_completed]
27
+ @handler = Handler.klass_factory(@version).prepend(ClientFraming).new(self, @debug)
28
+ trigger_on_open(_handshake = nil)
29
+ :stop
30
+ end
31
+ send_handshake unless @secure
32
+ end
33
+
34
+ def ssl_handshake_completed
35
+ send_handshake
36
+ end
37
+
38
+ private
39
+
40
+ def dispatch(data)
41
+ @parser << data
42
+ rescue HTTP::Parser::Error => e
43
+ debug [:error, e]
44
+ trigger_on_error(e)
45
+ abort :handshake_error
46
+ end
47
+
48
+ def send_handshake
49
+ url = @options.fetch(:url)
50
+ send_data(
51
+ "GET /ws/rem/relay HTTP/1.1\r\n" \
52
+ "Host: #{url.host}\r\n" \
53
+ "Connection: Upgrade\r\n" \
54
+ "Upgrade: websocket\r\n" \
55
+ "Sec-WebSocket-Version: #{@version}\r\n" \
56
+ "Sec-WebSocket-Key: #{@key}\r\n\r\n"
57
+ )
58
+ end
59
+ end
60
+
61
+ module ClientFraming
62
+ module C
63
+ FIN = 0x80
64
+ MASKED = 0x80
65
+ FRAME_TYPES = Framing07::FRAME_TYPES
66
+ end
67
+ private_constant :C
68
+
69
+ def send_frame(frame_type, data)
70
+ head = String.new(capacity: 14)
71
+ head << (C::FIN | C::FRAME_TYPES[frame_type])
72
+ len = data.bytesize
73
+ case len
74
+ when 0..125
75
+ head << [len | C::MASKED].pack("C")
76
+ when 126..65535
77
+ head << [126 | C::MASKED, len].pack("Cn")
78
+ else
79
+ head << [127 | C::MASKED, len].pack("CQ>")
80
+ end
81
+ mask_size = 4
82
+ mask = SecureRandom.bytes(mask_size)
83
+ head << mask
84
+ dmask = mask * 2
85
+ dm = dmask.unpack1("Q")
86
+ dms = dmask.bytesize
87
+ q, r = len.divmod(dms)
88
+ q.times do |i|
89
+ b = i * dms
90
+ data[b, dms] = [data[b, dms].unpack1("Q") ^ dm].pack("Q")
91
+ end
92
+ r.times do |i|
93
+ b = q * dms + i
94
+ data.setbyte(b, data.getbyte(b) ^ dmask[i].ord)
95
+ end
96
+ @connection.send_data(head)
97
+ @connection.send_data(data)
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,43 @@
1
+ module EventMachine::WebSocket::Server
2
+ def self.start(host, port, &block)
3
+ EventMachine::WebSocket.run(host: host, port: port, block: block) do |ws|
4
+ ws.onerror do |e|
5
+ if EventMachine::WebSocket::WebSocketError === e
6
+ log "[!] websocket error.", error: e
7
+ else
8
+ fail e
9
+ end
10
+ end
11
+ # WebSockets must not be closed because they are reused.
12
+ fail "inactivity timeout" unless ws.comm_inactivity_timeout.zero?
13
+
14
+ def ws.receive_data(data)
15
+ # The data can be nil on error.
16
+ return unless data
17
+ # Technically, we need to buffer the data until the first CRLF.
18
+ method, path, _httpv = data.split(" ", 3)
19
+ path, _qs = path.split("?", 2)
20
+ send_error = lambda do |status|
21
+ send_data("HTTP/1.1 #{status}\r\n\r\n#{status}")
22
+ close_connection_after_writing
23
+ log "[!] http client error.", method: method, path: path, status: status
24
+ end
25
+ if method != "GET"
26
+ send_error.call(405)
27
+ else
28
+ @options[:block].call(path, self) || send_error.call(404)
29
+ end
30
+ singleton_class.remove_method(__method__)
31
+ singleton_class.remove_method(:serve_file)
32
+ super if defined? @onopen
33
+ end
34
+
35
+ def ws.serve_file(path)
36
+ send_data("HTTP/1.1 200 OK\r\n\r\n")
37
+ # win: send/stream_file_data do not work on windows
38
+ send_data(path.read)
39
+ close_connection_after_writing
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,158 @@
1
+ require "em-websocket"
2
+ require "eventmachine"
3
+ require "eventmachine/websocket/client"
4
+ require "eventmachine/websocket/server"
5
+ require "pathname"
6
+ require "socket"
7
+ require "uri"
8
+ require "netpump/relay"
9
+
10
+ module Netpump
11
+ class Client
12
+ def initialize(
13
+ mode: "direct",
14
+ host: "0.0.0.0",
15
+ port: "8080",
16
+ proxy_host: "127.0.0.1",
17
+ proxy_port: 3128,
18
+ server_url: "wss://weblink-ouux.onrender.com",
19
+ connection_inactivity_timeout: 30.0,
20
+ websocket_pool_size: 20,
21
+ websocket_pool_timeout: 60.0
22
+ )
23
+ @mode = mode
24
+ @host = host
25
+ @port = port
26
+ @proxy_host = proxy_host
27
+ @proxy_port = proxy_port
28
+ @server_url = URI(server_url)
29
+ @connection_inactivity_timeout = connection_inactivity_timeout
30
+ @websocket_pool = EventMachine::Queue.new
31
+ @websocket_pool_size = websocket_pool_size
32
+ @websocket_pool_timeout = websocket_pool_timeout
33
+ @control_ws = nil
34
+ @log = lambda { |msg, **ctx| log(msg, loc: true, **ctx) }
35
+ @ready = EventMachine::Completion.new
36
+ end
37
+
38
+ def start
39
+ @log.call "[+] netpump client is starting.", mode: @mode, host: @host, port: @port
40
+ case @mode
41
+ when "direct"
42
+ start_proxy_server
43
+ @ready.succeed
44
+ when "browser"
45
+ start_websocket_server
46
+ print_open_url
47
+ end
48
+ @ready
49
+ end
50
+
51
+ private
52
+
53
+ PUBLIC_DIR = Pathname(__dir__).parent.parent.join("public").freeze
54
+ private_constant :PUBLIC_DIR
55
+
56
+ def start_websocket_server
57
+ EventMachine::WebSocket::Server.start(@host, @port) do |path, ws|
58
+ case path
59
+ when "/"
60
+ ws.serve_file(PUBLIC_DIR.join("netpump.html"))
61
+ @log.call "[~] http request.", method: "GET", path: path, ip: ws.remote_ip
62
+ when "/favicon.svg"
63
+ ws.serve_file(PUBLIC_DIR.join("favicon.svg"))
64
+ @log.call "[~] http request.", method: "GET", path: path, ip: ws.remote_ip
65
+ when "/ws/loc/control"
66
+ next false if @control_ws
67
+ ws.onopen do |_handshake|
68
+ @log.call "[+] device is connected.", ip: ws.remote_ip
69
+ @control_ws = ws
70
+ @control_ws.send_text(@server_url.to_s)
71
+ proxy = start_proxy_server
72
+ @control_ws.onclose do
73
+ @control_ws = nil
74
+ @log.call "[-] device is disconnected.", sig: ws.signature
75
+ EventMachine.stop_server(proxy)
76
+ @log.call "[-] proxy server is stopped."
77
+ end
78
+ @ready.succeed
79
+ end
80
+ when "/ws/loc/relay"
81
+ next false unless @control_ws.remote_ip == ws.remote_ip
82
+ ws.onopen do |_handshake|
83
+ @log.call "[+] websocket is open.", ip: ws.remote_ip, sig: ws.signature
84
+ @websocket_pool << ws
85
+ end
86
+ else
87
+ next false
88
+ end
89
+ true
90
+ end
91
+ end
92
+
93
+ def print_open_url
94
+ ip = Socket.getifaddrs.find { |ifa| ifa.addr&.ipv4_private? } or raise(
95
+ "could not find an interface to listen on; " \
96
+ "make sure that you are connected to your device."
97
+ )
98
+ open_url = URI::HTTP.build(host: ip.addr.ip_address, port: @port)
99
+ @log.call "[~] waiting for device to connect.", url: open_url
100
+ end
101
+
102
+ def start_proxy_server
103
+ proxy = EventMachine.start_server(@proxy_host, @proxy_port, Relay, "loc") do |relay|
104
+ bind(relay)
105
+ end
106
+ @log.call "[+] proxy server is ready.", type: "https", host: @proxy_host, port: @proxy_port
107
+ proxy
108
+ end
109
+
110
+ def bind(relay)
111
+ if @websocket_pool.empty?
112
+ case @mode
113
+ when "direct"
114
+ host = @server_url.host
115
+ port = @server_url.port
116
+ begin
117
+ EventMachine.connect(host, port, EventMachine::WebSocket::Client, url: @server_url) do |ws|
118
+ ws.onopen do
119
+ @log.call "[+] websocket is open.", ip: ws.remote_ip, sig: ws.signature
120
+ @websocket_pool << ws
121
+ end
122
+ end
123
+ rescue EventMachine::ConnectionError => e
124
+ @log.call "[!] server connection error.", error: e
125
+ return relay.close("server connection error")
126
+ end
127
+ when "browser"
128
+ # Dogpile effect if batch size > 1.
129
+ @control_ws.send_text(_batch_size = "1")
130
+ end
131
+ @log.call "[~] websocket is requested.", waitcnt: @websocket_pool.num_waiting
132
+ end
133
+ timeout = EventMachine::Timer.new(@websocket_pool_timeout) do
134
+ relay.close("pool timeout")
135
+ raise "race condition" unless EventMachine.reactor_thread?
136
+ @websocket_pool.instance_variable_get(:@popq).shift || raise("popq")
137
+ end
138
+ @websocket_pool.pop do |ws|
139
+ timeout.cancel
140
+ if ws.state != :connected
141
+ # When the browser tab is closed or reloaded, all local websockets
142
+ # are closed, remaining dead in the pool until popped.
143
+ @log.call "[~] websocket is dead, retrying.", state: ws.state, sig: ws.signature
144
+ EventMachine.next_tick { bind(relay) }
145
+ else
146
+ relay.set_comm_inactivity_timeout(@connection_inactivity_timeout)
147
+ relay.bind(ws, ws.remote_ip).callback do
148
+ if @websocket_pool.size < @websocket_pool_size
149
+ @websocket_pool << ws
150
+ else
151
+ ws.close(1000, "purge")
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,22 @@
1
+ $log = $>
2
+ $log.sync = true
3
+
4
+ def log(msg, **context)
5
+ line = "%-35s" % msg
6
+ context.each do |k, v|
7
+ case v
8
+ when Exception
9
+ line << " #{k}=#{v.message.inspect}"
10
+ line << " backtrace=#{v.backtrace}" if !$VERBOSE.nil? && v.backtrace
11
+ when true
12
+ line << " #{k}"
13
+ when false, nil, ""
14
+ # skip
15
+ else
16
+ v = v.to_s
17
+ v = v.inspect if v.include?(" ")
18
+ line << " #{k}=#{v}"
19
+ end
20
+ end
21
+ $log.puts(line)
22
+ end
@@ -0,0 +1,126 @@
1
+ module Netpump
2
+ class Relay < EventMachine::Connection
3
+ def initialize(
4
+ side,
5
+ connection_inactivity_timeout: 0.0,
6
+ backpressure_min_bytes: 2**23,
7
+ backpressure_max_bytes: 2**24,
8
+ backpressure_drain_seconds: 1.0
9
+ )
10
+ pause
11
+ @ws = nil
12
+ @completion = nil
13
+ @closed = false
14
+ @close_reason = nil
15
+ @unbind_sent = false
16
+ @unbind_recv = false
17
+ @connection_inactivity_timeout = connection_inactivity_timeout
18
+ @backpressure_min_bytes = backpressure_min_bytes
19
+ @backpressure_max_bytes = backpressure_max_bytes
20
+ @backpressure_drain_seconds = backpressure_drain_seconds
21
+ @remote_ip = nil
22
+ @sig = "-.#{signature}"
23
+ @log = lambda do |msg, **ctx|
24
+ log(msg, "#{side}": true, ip: @remote_ip, sig: @sig, **ctx)
25
+ end
26
+ end
27
+
28
+ def bind(ws, remote_ip)
29
+ @ws = ws
30
+ @sig = "#{ws.signature}.#{signature}"
31
+ @completion = EventMachine::Completion.new
32
+ @unbind_sent = false
33
+ @unbind_recv = false
34
+ @remote_ip = remote_ip
35
+ @connection_inactivity_timeout = comm_inactivity_timeout
36
+ if @closed
37
+ # If a connection is closed due to the pool timeout before being bound to
38
+ # a websocket, it is not added to the wait queue in the websocket pool,
39
+ # so this case should not happen under normal operation. However, it is
40
+ # still possible is the connection is closed due to any other reason.
41
+ @log.call "[!] bound closed before bind."
42
+ @completion.succeed
43
+ return @completion
44
+ end
45
+ @ws.onbinary do |data|
46
+ if @closed
47
+ @log.call "[~] data discard, bound closed.", connerror: error?, bytes: data.bytesize
48
+ else
49
+ send_data(data)
50
+ end
51
+ end
52
+ @ws.onmessage do |msg|
53
+ if msg == "unbind"
54
+ @unbind_recv = true
55
+ if @unbind_sent
56
+ @completion.succeed
57
+ else
58
+ close("unbind")
59
+ end
60
+ else
61
+ @log.call "[!] unexpected text on websocket.", msg: msg[0, 16].inspect
62
+ end
63
+ end
64
+ @ws.onclose do |close_info|
65
+ close("websocket closed") unless @closed
66
+ @completion.fail
67
+ @log.call "[-] websocket is closed.", **close_info.slice(:code, :reason)
68
+ end
69
+ resume
70
+ @log.call "[+] bind."
71
+ @completion
72
+ end
73
+
74
+ def receive_data(data)
75
+ @ws.send_binary(data)
76
+ check_backpressure
77
+ end
78
+
79
+ def close(reason)
80
+ fail "double close" if @closed
81
+ @close_reason = reason
82
+ close_connection_after_writing
83
+ end
84
+
85
+ def unbind(errno)
86
+ @closed = true
87
+ if @ws&.state == :connected
88
+ @ws.send_text("unbind")
89
+ @unbind_sent = true
90
+ @completion&.succeed if @unbind_recv
91
+ end
92
+ if errno.nil?
93
+ reason = @close_reason
94
+ elsif errno == Errno::ETIMEDOUT && @connection_inactivity_timeout > 0
95
+ reason = "inactive"
96
+ elsif errno.respond_to?(:exception)
97
+ reason = errno.exception.message
98
+ reason[0] = reason[0].downcase
99
+ else
100
+ # win: errno can be set to :unknown on windows
101
+ reason = errno.to_s
102
+ end
103
+ @log.call "[-] unbind.", reason: reason
104
+ end
105
+
106
+ private
107
+
108
+ def check_backpressure
109
+ outbound_bytes = @ws.get_outbound_data_size
110
+ if outbound_bytes >= @backpressure_max_bytes && !paused?
111
+ pause
112
+ @log.call "[~] paused.", outbytes: outbound_bytes
113
+ end
114
+ if outbound_bytes <= @backpressure_min_bytes && paused?
115
+ resume
116
+ @log.call "[~] resumed.", outbytes: outbound_bytes
117
+ end
118
+ if paused?
119
+ EventMachine::Timer.new(@backpressure_drain_seconds) do
120
+ check_backpressure
121
+ end
122
+ end
123
+ end
124
+ end
125
+ private_constant :Relay
126
+ end
@@ -0,0 +1,60 @@
1
+ require "em-websocket"
2
+ require "eventmachine-proxy"
3
+ require "eventmachine/websocket/server"
4
+ require "socket"
5
+ require "netpump/relay"
6
+
7
+ module Netpump
8
+ class Server
9
+ def initialize(host: "0.0.0.0", port: 10000, proxy_host: "127.0.0.1", proxy_port: 0)
10
+ @host = host
11
+ @port = port
12
+ @proxy_host = proxy_host
13
+ @proxy_port = proxy_port
14
+ @log = lambda { |msg, **ctx| log(msg, rem: true, **ctx) }
15
+ end
16
+
17
+ def start
18
+ @log.call "[+] netpump server is starting.", host: @host, port: @port
19
+ start_websocket_server
20
+ proxy = EventMachine.start_server(
21
+ @proxy_host, @proxy_port, EventMachine::Protocols::CONNECT
22
+ ) do |c|
23
+ fail "inactivity timeout" unless c.comm_inactivity_timeout.zero?
24
+ end
25
+ @proxy_port, @proxy_host = Socket.unpack_sockaddr_in(EventMachine.get_sockname(proxy))
26
+ end
27
+
28
+ private
29
+
30
+ def start_websocket_server
31
+ EventMachine::WebSocket::Server.start(@host, @port) do |path, ws|
32
+ case path
33
+ when "/", "/healthcheck"
34
+ @log.call "[~] http request.", method: "GET", path: path, ip: ws.remote_ip
35
+ ws.send_healthcheck_response
36
+ when "/ws/rem/relay"
37
+ ws.onopen do |handshake|
38
+ xff = handshake.headers_downcased["x-forwarded-for"]&.split(",", 2)&.first
39
+ ip = xff || ws.remote_ip
40
+ @log.call "[+] websocket is open.", ip: ip, sig: ws.signature
41
+ bind(ws, ip)
42
+ end
43
+ else
44
+ return false
45
+ end
46
+ true
47
+ end
48
+ end
49
+
50
+ def bind(ws, ip)
51
+ EventMachine.connect(@proxy_host, @proxy_port, Relay, "rem") do |relay|
52
+ fail "inactivity timeout" unless relay.comm_inactivity_timeout.zero?
53
+ relay.bind(ws, ip).callback { bind(ws, ip) }
54
+ end
55
+ rescue RuntimeError => e
56
+ @log.call "[!] proxy connection error.", error: e
57
+ ws.close(4000, "proxy connection error")
58
+ end
59
+ end
60
+ end
data/lib/netpump.rb ADDED
@@ -0,0 +1,6 @@
1
+ module Netpump
2
+ end
3
+
4
+ require "netpump/log"
5
+ require "netpump/client"
6
+ require "netpump/server"
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="16" height="16"><style>@media (prefers-color-scheme:dark){path{stroke:#fff!important}}</style><path d="M5.072 5.73v6.054h5.856V5.73" style="stroke:#540b0b;stroke-width:2.91;fill:none"/></svg>
@@ -0,0 +1,191 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>netpump</title>
7
+ <link rel="icon" href="favicon.svg" type="image/svg+xml" />
8
+ <style>
9
+ html,
10
+ body {
11
+ height: 100%;
12
+ background-color: #540b0b;
13
+ font-family: arial, sans-serif;
14
+ }
15
+ body {
16
+ display: flex;
17
+ flex-direction: column;
18
+ align-items: center;
19
+ justify-content: center;
20
+ text-align: center;
21
+ color: white;
22
+ }
23
+ dd {
24
+ margin: 0;
25
+ }
26
+ input[type="checkbox"] {
27
+ margin-right: 0.5em;
28
+ }
29
+ header {
30
+ margin: 1em 0;
31
+ }
32
+ header h1 {
33
+ margin: 0;
34
+ font-size: 1.5em;
35
+ }
36
+ header svg {
37
+ width: 4em;
38
+ }
39
+ </style>
40
+ </head>
41
+ <body>
42
+ <header>
43
+ <svg role="img" viewBox="0 0 16 16">
44
+ <path
45
+ d="M5.072 5.73v6.054h5.856V5.73"
46
+ style="stroke: #fff; stroke-width: 2.91; fill: none"
47
+ />
48
+ </svg>
49
+ <h1>netpump</h1>
50
+ </header>
51
+ <div class="settings">
52
+ <label
53
+ ><input type="checkbox" id="nosleep" />Prevent screen from
54
+ sleeping</label
55
+ >
56
+ </div>
57
+ <dl class="stats">
58
+ <dt>Connections</dt>
59
+ <dd id="connectionCount">0</dd>
60
+ </dl>
61
+ <script>
62
+ function createRelayWebSocketPair(clientAddr, serverAddr) {
63
+ // client websocket.
64
+ var client;
65
+ // Connect to the server first, so that when we
66
+ // connect to the client the whole chain is complete.
67
+ var server = new WebSocket(serverAddr + "/ws/rem/relay");
68
+ // Log server errors.
69
+ server.onerror = function (event) {
70
+ console.error("[!] server/error", event);
71
+ };
72
+ // When the server closes its websocket, close the corresponding
73
+ // client websocket too.
74
+ server.onclose = function (event) {
75
+ console.log("[-] server/close", {
76
+ code: event.code,
77
+ reason: event.reason,
78
+ wasClean: event.wasClean,
79
+ });
80
+ if (client?.readyState === WebSocket.OPEN) {
81
+ client.close(
82
+ 1000,
83
+ `bound close: ${event.code} ${event.reason}`.trimEnd(),
84
+ );
85
+ }
86
+ };
87
+ server.onopen = function () {
88
+ // Connect to the client to relay data.
89
+ client = new WebSocket(clientAddr + "/ws/loc/relay");
90
+ // Log relay errors.
91
+ client.onerror = function (event) {
92
+ console.error("[!] client/error", event);
93
+ };
94
+ // When the client websocket is closed, close the corresponding
95
+ // server websocket.
96
+ client.onclose = function (event) {
97
+ if (client.cleanOpen) {
98
+ connections--;
99
+ connectionCount.textContent = connections;
100
+ }
101
+ console.log("[-] client/close", {
102
+ code: event.code,
103
+ reason: event.reason,
104
+ wasClean: event.wasClean,
105
+ });
106
+ if (server.readyState === WebSocket.OPEN) {
107
+ server.close(
108
+ 1000,
109
+ `bound close: ${event.code} ${event.reason}`.trimEnd(),
110
+ );
111
+ }
112
+ };
113
+ // Splice client and server websockets.
114
+ client.onopen = function () {
115
+ connections++;
116
+ connectionCount.textContent = connections;
117
+ client.cleanOpen = true;
118
+ client.onmessage = function (msg) {
119
+ server.send(msg.data);
120
+ };
121
+ server.onmessage = function (msg) {
122
+ client.send(msg.data);
123
+ };
124
+ };
125
+ };
126
+ }
127
+ // Netpump client.
128
+ var clientAddr = "ws://" + location.host;
129
+ // Netpump server.
130
+ var serverAddr;
131
+ // Establish a client control websocket to receive commands.
132
+ var control = new WebSocket(clientAddr + "/ws/loc/control");
133
+ // Total number of client connections.
134
+ var connections = 0;
135
+ // Log control websocket errors.
136
+ control.onerror = function (event) {
137
+ console.error("[!] control/error", event);
138
+ };
139
+ // Log control websocket closure.
140
+ control.onclose = function (event) {
141
+ console.log("[-] control/close", {
142
+ code: event.code,
143
+ reason: event.reason,
144
+ });
145
+ };
146
+ // Add more connections on demand.
147
+ control.onmessage = function (msg) {
148
+ serverAddr = msg.data;
149
+ control.onmessage = function (msg) {
150
+ var connectionBatchSize = Number.parseInt(msg.data);
151
+ console.log("[~] control/batch", { count: connectionBatchSize });
152
+ for (var i = 0; i < connectionBatchSize; i++) {
153
+ createRelayWebSocketPair(clientAddr, serverAddr);
154
+ }
155
+ };
156
+ };
157
+ </script>
158
+ <script
159
+ src="https://cdnjs.cloudflare.com/ajax/libs/nosleep/0.12.0/NoSleep.min.js"
160
+ type="text/javascript"
161
+ crossorigin="anonymous"
162
+ ></script>
163
+ <script>
164
+ var noSleep = new NoSleep();
165
+ var noSleepCheckbox = document.getElementById("nosleep");
166
+
167
+ document.addEventListener(
168
+ "visibilitychange",
169
+ function resetNoSleep(event) {
170
+ if (document.visibilityState === "visible") {
171
+ noSleep.disable();
172
+ noSleep = new NoSleep();
173
+ noSleepCheckbox.checked = false;
174
+ }
175
+ },
176
+ );
177
+
178
+ noSleepCheckbox.addEventListener("click", function toggleNoSleep(event) {
179
+ if (noSleep.isEnabled) {
180
+ noSleep.disable();
181
+ } else {
182
+ try {
183
+ noSleep.enable();
184
+ } catch (error) {
185
+ event.preventDefault();
186
+ }
187
+ }
188
+ });
189
+ </script>
190
+ </body>
191
+ </html>
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: netpump
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - soylent
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: em-websocket
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.5'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.5'
26
+ - !ruby/object:Gem::Dependency
27
+ name: eventmachine-proxy
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.0'
40
+ executables:
41
+ - netpump
42
+ extensions: []
43
+ extra_rdoc_files: []
44
+ files:
45
+ - CHANGELOG
46
+ - VERSION
47
+ - bin/netpump
48
+ - lib/eventmachine/websocket/client.rb
49
+ - lib/eventmachine/websocket/server.rb
50
+ - lib/netpump.rb
51
+ - lib/netpump/client.rb
52
+ - lib/netpump/log.rb
53
+ - lib/netpump/relay.rb
54
+ - lib/netpump/server.rb
55
+ - public/favicon.svg
56
+ - public/netpump.html
57
+ homepage: https://netpump.org
58
+ licenses:
59
+ - MIT
60
+ metadata: {}
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 2.5.0
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubygems_version: 3.6.9
76
+ specification_version: 4
77
+ summary: Tiny websocket proxy tunnel
78
+ test_files: []