weblink 1.1.2 → 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 13496b146b017e1d415d1fb4ee27b36deb894f158882dc376490be51a6920184
4
- data.tar.gz: cf4643c4a79bb42f8f2b79e05197915e866d90d54be68689aa3df628f237385e
3
+ metadata.gz: 6e829338fc3dde9de2d16c8ec3b7278bfd225b55ed9bbc253219932f9426da32
4
+ data.tar.gz: 8d0e86f6bb6fe90ba4faa513fcb347983c92c2df2743d5238ee04b2897de5e27
5
5
  SHA512:
6
- metadata.gz: 11b4b83e40f513ef9b4cbeb3ae532dc5c794da491e0adc06b69fa9c3736c80148998d26c1cf0eef78ff1cced9bfea691806bfebf7fb1bbd6ec720f453936d695
7
- data.tar.gz: d2083018ee29d25126c5379524bd59af249e55ac8b3b5030517b7ba905a2d122a5851a9fcb43611b215799acaeefbed3a88e4163946ef68c8426b01d2ff9ed8b
6
+ metadata.gz: 517605ab3723104d2d58cba140cdd4bbe99d5e76ff5dd25012e2cc02fb8a59b70ad65ad15e6c2bd914b898b138645a3b80e637dc36c3b253196ffe8b31bf9916
7
+ data.tar.gz: 9bd7f163a3039161decee833f62846455e8319e8cb5e407121c802ee888646fee97ac2eaa0ccc39861d07cc2884ef93fd7d88455945c85bf2731cdd0ed7812c5
data/CHANGELOG CHANGED
@@ -1,3 +1,18 @@
1
+ 1.3.0
2
+
3
+ * Run all servers in one event loop
4
+ * Replace webrick with a simple static file server
5
+ * Add websocket pool
6
+ * Add extensive logging
7
+ * Show connection count in browser
8
+ * Drop SOCKS5 support
9
+ * Drop web manifest
10
+
11
+ 1.2.0
12
+
13
+ * Prevent screen from sleeping (@zakir8)
14
+ * Send pings to keep websockets alive
15
+
1
16
  1.1.2
2
17
 
3
18
  * Add webrick as a runtime dependency
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.1.2
1
+ 1.3.0
data/bin/weblink CHANGED
@@ -1,10 +1,12 @@
1
- #!/usr/bin/env -S ruby -w
1
+ #!/usr/bin/env -S ruby
2
2
 
3
- $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
3
+ $VERBOSE = nil
4
+ $>.sync = true
5
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4
6
 
5
- require 'optparse'
6
- require 'uri'
7
- require 'weblink'
7
+ require "optparse"
8
+ require "uri"
9
+ require "weblink"
8
10
 
9
11
  opts = {}
10
12
 
@@ -13,47 +15,42 @@ op = OptionParser.new do |op|
13
15
 
14
16
  op.banner =
15
17
  "Usage: weblink [options]\n" \
16
- 'Web browser gateway'
18
+ "Web browser gateway"
17
19
 
18
- op.separator ''
19
- op.separator 'Options'
20
+ op.separator ""
21
+ op.separator "Options"
20
22
 
21
- op.on('-c', '--client', 'Starts weblink client (default)') do
23
+ op.on("-c", "--client", "start weblink client (default)") do
22
24
  opts[:client] = true
23
25
  end
24
26
 
25
- op.on('-s', '--server', 'Starts weblink server') do
27
+ op.on("-s", "--server", "start weblink server") do
26
28
  opts[:server] = true
27
29
  end
28
30
 
29
- op.on('-a', '--host HOST', String, 'Address to bind to (default: 0.0.0.0)') do |host|
31
+ op.on("-h", "--host HOST", String, "weblink server host (default: 0.0.0.0)") do |host|
30
32
  opts[:host] = host
31
33
  end
32
34
 
33
- op.on('-p', '--port PORT', Integer, 'Use PORT (default: 8000)') do |port|
35
+ op.on("-p", "--port PORT", Integer, "weblink server port (default: 8080)") do |port|
34
36
  opts[:port] = port
35
37
  end
36
38
 
37
- op.on('--proxy-type TYPE', String, 'https or socks5 (default: https)') do |type|
38
- raise OptionParser::InvalidArgument, type unless type == 'https' || type == 'socks5'
39
- opts[:proxy_type] = type
39
+ op.on("--proxy-host HOST", String, "proxy server host (default: 0.0.0.0)") do |host|
40
+ opts[:proxy_host_loc] = host
40
41
  end
41
42
 
42
- op.on('--proxy-host HOST', String, 'Address to bind proxy to(default: 0.0.0.0)') do |host|
43
- opts[:proxy_host] = host
43
+ op.on("--proxy-port PORT", Integer, "proxy server port (default: 3128)") do |port|
44
+ opts[:proxy_port_loc] = port
44
45
  end
45
46
 
46
- op.on('--proxy-port PORT', Integer, 'Use proxy PORT (default: 3128)') do |port|
47
- opts[:proxy_port] = port
48
- end
49
-
50
- op.on('-v', '--version', 'Show version and exit') do
51
- version = File.expand_path('../VERSION', __dir__)
47
+ op.on_tail("--version", "print version") do
48
+ version = File.expand_path("../VERSION", __dir__)
52
49
  puts(File.read(version))
53
50
  exit
54
51
  end
55
52
 
56
- op.on_tail('-h', '--help', 'Print this help') do
53
+ op.on_tail("--help", "print this help") do
57
54
  puts(op)
58
55
  exit
59
56
  end
@@ -63,19 +60,7 @@ begin
63
60
  op.parse!
64
61
  rescue OptionParser::ParseError => e
65
62
  op.abort(e)
66
- else
67
- opts[:client] = true unless opts[:client] || opts[:server]
68
- if opts[:client] && (opts[:host] || opts[:port])
69
- abort('Changing host or port is not supported yet in client mode')
70
- end
71
- opts[:host] ||= '0.0.0.0'
72
- opts[:port] ||= 8000
73
- opts[:proxy_type] ||= 'https'
74
- opts[:proxy_host] ||= '0.0.0.0'
75
- opts[:proxy_port] ||= 3128
76
63
  end
77
64
 
78
- weblink = Weblink.new(opts)
79
- weblink.start
80
-
81
- # vim: ft=ruby
65
+ weblink = Weblink.new(**opts)
66
+ weblink.run
data/lib/relay.rb CHANGED
@@ -1,61 +1,85 @@
1
- require 'csv'
2
- require 'time'
3
-
4
1
  class Relay < EventMachine::Connection
5
- def initialize(tag, xff = nil)
2
+ def initialize(side, logproc)
6
3
  super
7
4
  pause
8
- @tag = tag
9
- @xff = xff
10
- @remote_port, @remote_ip = unpack_sockaddr_in(get_peername)
11
- @websocket = nil
12
- @ws_remote_ip = nil
13
- @ws_remote_port = nil
5
+ @ws = nil
6
+ @sig = nil
7
+ @completion = nil
8
+ @closed = false
9
+ @unbind_sent = false
10
+ @unbind_recv = false
11
+ @remote_ip = nil
12
+ @connection_inactivity_timeout = 0.0
13
+ @log = lambda { |msg, **ctx| logproc[msg, side: side, ip: @remote_ip, sig: @sig, **ctx] }
14
14
  end
15
15
 
16
- def start(websocket)
17
- @websocket = websocket
18
- @websocket.onbinary do |msg|
19
- log('recv', msg.bytesize)
20
- send_data(msg)
16
+ def bind(ws, remote_ip)
17
+ @ws = ws
18
+ @sig = "#{ws.signature}.#{signature}"
19
+ @completion = EventMachine::Completion.new
20
+ @closed = false
21
+ @unbind_sent = false
22
+ @unbind_recv = false
23
+ @remote_ip = remote_ip
24
+ @connection_inactivity_timeout = comm_inactivity_timeout
25
+ @log.call "[+] bind."
26
+ @ws.onbinary do |data|
27
+ if @closed
28
+ @log.call "[~] data discard, bound closed.", connerror: error?, bytes: data.bytesize
29
+ else
30
+ send_data(data)
31
+ end
32
+ end
33
+ @ws.onmessage do |msg|
34
+ if msg == "unbind"
35
+ @unbind_recv = true
36
+ if @unbind_sent
37
+ @completion.succeed
38
+ else
39
+ close_connection
40
+ end
41
+ else
42
+ @log.call "[!] unexpected text on websocket.", msg: msg
43
+ end
44
+ end
45
+ @ws.onclose do
46
+ close_connection
47
+ @completion.fail
48
+ close_info = @ws.instance_variable_get(:@handler).instance_variable_get(:@close_info) || {}
49
+ code = close_info[:code]
50
+ reason = close_info[:reason]
51
+ if reason
52
+ reason = reason.empty? ? nil : reason.inspect
53
+ end
54
+ @log.call "[-] websocket is closed.", code: code, reason: reason
21
55
  end
22
- @websocket.onerror { |err| log('ws/error', err) }
23
- @websocket.onclose { close_connection_after_writing }
24
- @ws_remote_port, @ws_remote_ip = unpack_sockaddr_in(websocket.get_peername)
25
56
  resume
57
+ @completion
26
58
  end
27
59
 
28
60
  def receive_data(data)
29
- log('sent', data.bytesize)
30
- @websocket.send_binary(data)
61
+ @ws.send_binary(data)
31
62
  end
32
63
 
33
- def unbind
34
- log('close')
35
- # Status code 1000 indicates indicates a normal closure.
36
- @websocket&.close(1000) if @websocket&.state == :connected
37
- end
38
-
39
- private
40
-
41
- def log(event, comment = nil)
42
- row = [
43
- Time.now.iso8601(3),
44
- @tag,
45
- @xff,
46
- @remote_ip,
47
- @remote_port&.to_s,
48
- @ws_remote_ip,
49
- @ws_remote_port&.to_s,
50
- event,
51
- comment&.to_s,
52
- ]
53
-
54
- puts CSV.generate_line(row, col_sep: ' ', write_nil_value: '-')
55
- end
56
-
57
- def unpack_sockaddr_in(sock)
58
- Socket.unpack_sockaddr_in(sock) if sock
59
- rescue ArgumentError
64
+ def unbind(errno)
65
+ if errno
66
+ if errno == Errno::ETIMEDOUT && @connection_inactivity_timeout > 0
67
+ reason = "inactivity"
68
+ elsif errno.respond_to?(:exception)
69
+ reason = errno.exception.message
70
+ reason[0] = reason[0].downcase
71
+ else
72
+ # win: errno can be set to :unknown on windows
73
+ reason = errno.to_s
74
+ end
75
+ reason = reason.inspect
76
+ end
77
+ @log.call "[-] unbind.", reason: reason
78
+ @closed = true
79
+ if @ws&.state == :connected
80
+ @ws.send_text("unbind")
81
+ @unbind_sent = true
82
+ @completion&.succeed if @unbind_recv
83
+ end
60
84
  end
61
85
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module StaticFiles
6
+ def receive_data(data)
7
+ file = asset_file(data) or return super
8
+ send_data "HTTP/1.1 200 OK\r\n\r\n"
9
+ # win: send/stream_file_data do not work on windows
10
+ send_data file.read
11
+ close_connection_after_writing
12
+ end
13
+
14
+ DIR = Pathname(__dir__).parent.join("public").freeze
15
+ private_constant :DIR
16
+
17
+ INDEX = "index.html"
18
+ private_constant :INDEX
19
+
20
+ private
21
+
22
+ def asset_file(data)
23
+ match = data.match(/^(HEAD|GET) \/(?<path>[\w.-]*)?(?:\?\S*)? HTTP/) or return
24
+ path = match[:path]
25
+ path = INDEX if path.empty?
26
+ file = DIR.join(path)
27
+ file if file.file? && file.readable?
28
+ end
29
+ end
data/lib/weblink.rb CHANGED
@@ -1,95 +1,186 @@
1
- require 'em-websocket'
2
- require 'eventmachine'
3
- require 'socket'
4
- require 'tempfile'
5
- require 'relay'
1
+ require "eventmachine"
2
+ require "em-websocket"
3
+ require "socket"
4
+ require "uri"
5
+ require "relay"
6
+ require "static_files"
6
7
 
7
8
  class Weblink
8
- def initialize(opts)
9
- @opts = opts
10
- @https = Tempfile.new('weblink')
11
- @socks5 = Tempfile.new('weblink')
12
- @websockets = EventMachine::Queue.new
9
+ def initialize(
10
+ client: nil,
11
+ server: nil,
12
+ host: "0.0.0.0",
13
+ port: "8080",
14
+ connection_inactivity_timeout: 30.0,
15
+ proxy_host_loc: "0.0.0.0",
16
+ proxy_port_loc: 3128,
17
+ proxy_host_rem: "127.0.0.1",
18
+ proxy_port_rem: 55000,
19
+ websocket_pool_batch_size: 1,
20
+ websocket_pool_max_size: 20
21
+ )
22
+ @client = client || client == server
23
+ @server = server
24
+ @host = host
25
+ @port = port
26
+ @connection_inactivity_timeout = connection_inactivity_timeout
27
+ @proxy_host_loc = proxy_host_loc
28
+ @proxy_port_loc = proxy_port_loc
29
+ @proxy_host_rem = proxy_host_rem
30
+ @proxy_port_rem = proxy_port_rem
31
+ @websocket_pool = EventMachine::Queue.new
32
+ @websocket_pool_batch_size = websocket_pool_batch_size
33
+ @websocket_pool_max_size = websocket_pool_max_size
13
34
  end
14
35
 
15
- def start
16
- trap(:EXIT) { Process.waitall }
36
+ def run
37
+ EventMachine.epoll
38
+ EventMachine.error_handler do |e|
39
+ log "[!] unexpected errback", error: e
40
+ end
41
+ EventMachine.run do
42
+ trap(:INT) { puts; stop_eventmachine "[-] shutting down weblink.", signal: "sigint" }
43
+ trap(:TERM) { stop_eventmachine "[-] shutting down weblink.", signal: "sigterm" }
17
44
 
18
- if @opts[:client]
19
- ip = Socket.getifaddrs.find { |ifa| ifa.addr&.ipv4_private? }
20
- if ip
21
- public = File.expand_path('../public', __dir__)
22
- spawn('ruby', '-run', '-ehttpd', '--', public, err: IO::NULL)
23
- puts "Open http://#{ip.addr.ip_address}:8080/ on your other device."
24
- else
25
- abort(
26
- "Could not find an interface to listen on. " \
27
- "Make sure that you are connected to your device."
28
- )
29
- end
45
+ log "[+] starting weblink.", client: @client, server: @server, epoll: EventMachine.epoll?
46
+
47
+ start_proxy_rem if @server
48
+ start_websocket_server
49
+ print_open_url if @client
30
50
  end
51
+ end
52
+
53
+ private
31
54
 
32
- if @opts[:server]
33
- begin
34
- spawn('proxxxy', "https://#{@https.path}", "socks5://#{@socks5.path}")
35
- rescue Errno::ENOENT
36
- abort('Please install proxxxy v2 to run weblink server')
55
+ def start_websocket_server
56
+ EventMachine::WebSocket.run(host: @host, port: @port) do |ws|
57
+ raise "websocket inactivity timeout" unless ws.comm_inactivity_timeout.zero?
58
+ # Static server is only needed in client mode, but we need this to deploy
59
+ # the server to Render because it sends HEAD / requests to check if the
60
+ # app is up.
61
+ ws.singleton_class.include(StaticFiles)
62
+ ws.onerror do |e|
63
+ log "[!] websocket error.", error: e
64
+ end
65
+ ws.onopen do |handshake|
66
+ xff = handshake.headers_downcased["x-forwarded-for"]&.split(",", 2)&.first
67
+ remote_ip = xff || ws.remote_ip
68
+ ctx = {ip: remote_ip, sig: ws.signature}
69
+ path = handshake.path
70
+ if @client && path == "/ws/loc/control"
71
+ log "[+] device is connected.", side: "loc", **ctx
72
+ start_proxy_loc(ws, remote_ip)
73
+ elsif @client && path == "/ws/loc/relay"
74
+ log "[+] websocket is open.", side: "loc", **ctx
75
+ @websocket_pool.push(ws)
76
+ elsif @server && path == "/ws/rem/relay"
77
+ log "[+] websocket is open.", side: "rem", **ctx
78
+ bind_to_proxy(ws, remote_ip)
79
+ else
80
+ log "[!] unexpected request.", path: handshake.path
81
+ end
37
82
  end
38
83
  end
84
+ rescue RuntimeError => e
85
+ stop_eventmachine "[!] websocket server error.", error: e
86
+ else
87
+ log "[+] websocket server is ready.", host: @host, port: @port
88
+ end
39
89
 
40
- EventMachine::WebSocket.start(@opts) do |ws|
41
- ws.onopen do |handshake|
42
- case handshake.path
43
- when '/control'
44
- start_client(ws)
45
- puts 'Ready'
46
- when '/client'
47
- @websockets.push(ws)
48
- when '/proxy/socks5'
49
- proxy(ws, handshake, @socks5.path)
50
- when '/proxy/https'
51
- proxy(ws, handshake, @https.path)
90
+ def start_proxy_loc(control_ws, remote_ip)
91
+ bind_websocket = lambda do |relay|
92
+ # Dogpile effect if batch size > 1.
93
+ if @websocket_pool.empty?
94
+ control_ws.send_text(@websocket_pool_batch_size.to_s)
95
+ log "[~] websocket is requested.", side: "loc", cnt: @websocket_pool_batch_size, waitcnt: @websocket_pool.num_waiting
96
+ end
97
+ @websocket_pool.pop do |ws|
98
+ if ws.state == :connected
99
+ relay.set_comm_inactivity_timeout @connection_inactivity_timeout
100
+ relay.bind(ws, remote_ip).callback do
101
+ if @websocket_pool.size < @websocket_pool_max_size
102
+ @websocket_pool.push(ws)
103
+ else
104
+ ws.close(1000, "purge")
105
+ end
106
+ end
52
107
  else
53
- warn("Unexpected request: #{handshake.path.inspect}")
108
+ log "[!] websocket is dead, retrying.", side: "loc", state: ws.state
109
+ EventMachine.next_tick { bind_websocket.(relay) }
54
110
  end
55
111
  end
56
112
  end
113
+ begin
114
+ sig = EventMachine.start_server(@proxy_host_loc, @proxy_port_loc, Relay, "loc", method(:log), &bind_websocket)
115
+ rescue RuntimeError => e
116
+ stop_eventmachine "[!] local proxy server error.", error: e
117
+ else
118
+ control_ws.onclose do
119
+ EventMachine.stop_server(sig)
120
+ log "[-] device is disconnected.", side: "loc"
121
+ log "[-] local proxy is stopped.", side: "loc"
122
+ end
123
+ log "[+] local proxy is ready.", side: "loc", type: "https", host: @proxy_host_loc, port: @proxy_port_loc
124
+ end
57
125
  end
58
126
 
59
- private
127
+ def print_open_url
128
+ ip = Socket.getifaddrs.find { |ifa| ifa.addr&.ipv4_private? } or abort(
129
+ "[!] could not find an interface to listen on; " \
130
+ "make sure that you are connected to your device."
131
+ )
132
+ open_url = URI::HTTP.build(host: ip.addr.ip_address, port: @port)
133
+ log "[~] waiting for device to connect.", side: "loc", url: open_url
134
+ end
60
135
 
61
- def start_client(control_ws, min_ws_num: 3)
62
- host, port = @opts.values_at(:proxy_host, :proxy_port)
63
- sig = EventMachine.start_server(host, port, Relay, 'client') do |rel|
64
- # Dogpile effect
65
- control_ws.send_text(@opts[:proxy_type]) if @websockets.size < min_ws_num
66
- @websockets.pop { |ws| rel.start(ws) }
136
+ def start_proxy_rem
137
+ require "em/protocols/connect"
138
+ rescue LoadError
139
+ abort "[!] install proxxy v2 to run weblink server."
140
+ else
141
+ begin
142
+ EventMachine.start_server(@proxy_host_rem, @proxy_port_rem, EventMachine::Protocols::CONNECT, quiet: true) do |c|
143
+ raise unless c.comm_inactivity_timeout.zero?
144
+ end
145
+ rescue RuntimeError => e
146
+ stop_eventmachine "[!] remote proxy server error.", side: "rem", error: e
147
+ else
148
+ log "[+] remote proxy is ready.", side: "rem", type: "https", host: @proxy_host_rem, port: @proxy_port_rem
67
149
  end
68
- control_ws.onclose { EventMachine.stop_server(sig) }
69
150
  end
70
151
 
71
- def proxy(ws, handshake, socket)
72
- unless @opts[:server]
73
- warn 'weblink server is disabled'
74
- return
75
- end
76
- xff = handshake.headers_downcased['x-forwarded-for']
77
- with_retry(timeout: 3) do
78
- EventMachine.connect(socket, Relay, 'server', xff) do |rel|
79
- rel.start(ws)
80
- end
152
+ def bind_to_proxy(ws, remote_ip)
153
+ EventMachine.connect(@proxy_host_rem, @proxy_port_rem, Relay, "rem", method(:log)) do |relay|
154
+ raise unless relay.comm_inactivity_timeout.zero?
155
+ relay.bind(ws, remote_ip).callback { bind_to_proxy(ws, remote_ip) }
81
156
  end
157
+ rescue RuntimeError => e
158
+ log "[!] proxy connection error.", error: e
159
+ ws.close(4000, "proxy connection error")
82
160
  end
83
161
 
84
- def with_retry(timeout:, wait: 0.1)
85
- elapsed = 0
86
- begin
87
- yield
88
- rescue RuntimeError
89
- if elapsed < timeout
90
- elapsed += sleep(wait)
91
- retry
162
+ def stop_eventmachine(msg, **ctx)
163
+ log msg, **ctx, fds: EventMachine.connection_count
164
+ EventMachine.stop
165
+ rescue RuntimeError => e
166
+ log "[!] event machine error.", error: e.cause
167
+ end
168
+
169
+ def log(msg, **context)
170
+ line = "%-35s" % msg
171
+ context.each do |k, v|
172
+ case v
173
+ when Exception
174
+ line << " #{k}=#{v.message.inspect}"
175
+ line << " backtrace=#{v.backtrace}" if v.backtrace
176
+ when true
177
+ line << " #{k}"
178
+ when false, nil, ""
179
+ # skip
180
+ else
181
+ line << " #{k}=#{v}"
92
182
  end
93
183
  end
184
+ $>.puts(line)
94
185
  end
95
186
  end
data/public/favicon.ico CHANGED
Binary file
@@ -0,0 +1,29 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <!-- Created with Inkscape (http://www.inkscape.org/) -->
3
+
4
+ <svg
5
+ width="15.763862mm"
6
+ height="15.763863mm"
7
+ viewBox="0 0 15.763862 15.763863"
8
+ version="1.1"
9
+ id="svg1"
10
+ xml:space="preserve"
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ xmlns:svg="http://www.w3.org/2000/svg"><defs
13
+ id="defs1" /><g
14
+ id="layer1"
15
+ transform="translate(-92.26936,-159.77363)"><circle
16
+ style="fill:#599af8;fill-opacity:1;stroke:none;stroke-width:0.236132;stroke-linejoin:round"
17
+ id="path1"
18
+ cx="100.1513"
19
+ cy="167.65556"
20
+ r="7.8819342" /><text
21
+ xml:space="preserve"
22
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:11.2889px;font-family:Arial;-inkscape-font-specification:'Arial, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.861247;stroke-linejoin:round"
23
+ x="95.738823"
24
+ y="170.58252"
25
+ id="text1"><tspan
26
+ id="tspan1"
27
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:11.2889px;font-family:Arial;-inkscape-font-specification:'Arial, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffffff;stroke-width:0.861247"
28
+ x="95.738823"
29
+ y="170.58252">w</tspan></text></g></svg>
data/public/index.html CHANGED
@@ -1,73 +1,148 @@
1
- <!DOCTYPE html>
1
+ <!doctype html>
2
2
  <html>
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
5
6
  <title>weblink</title>
6
- <link rel="manifest" href="/weblink.webmanifest">
7
+ <link rel="icon" href="favicon.svg" type="image/svg+xml" />
7
8
  <style>
8
- html, body {
9
+ html,
10
+ body {
9
11
  height: 100%;
12
+ background-color: #3f9cff;
13
+ font-family: arial, sans-serif;
10
14
  }
11
15
  body {
12
- align-items: center;
13
- background-color: #3f9cff;
14
- color: white;
15
16
  display: flex;
17
+ flex-direction: column;
18
+ align-items: center;
16
19
  justify-content: center;
20
+ text-align: center;
21
+ color: white;
22
+ }
23
+ dd {
24
+ margin: 0;
17
25
  }
18
- h1 {
19
- font-family: sans-serif;
20
- font-size: 8em;
26
+ input[type="checkbox"] {
27
+ margin-right: 0.5em;
21
28
  }
22
29
  </style>
23
30
  </head>
24
31
  <body>
25
32
  <h1>weblink</h1>
33
+ <div class="settings">
34
+ <label
35
+ ><input type="checkbox" id="nosleep" />Prevent screen from
36
+ sleeping</label
37
+ >
38
+ </div>
39
+ <dl class="stats">
40
+ <dt>Connections</dt>
41
+ <dd id="connectionCount">0</dd>
42
+ </dl>
26
43
  <script>
27
- function connect(client_addr, server_addr, proxy) {
28
- var client, server = new WebSocket(server_addr + "/proxy/" + proxy);
29
-
30
- server.onerror = function(event) { console.error("server/error"); };
31
-
32
- server.onopen = function() {
33
- client = new WebSocket(client_addr + "/client");
34
-
35
- client.onerror = function(event) { console.error("client/error"); };
36
-
37
- client.onopen = function() {
38
- client.onmessage = function(msg) { server.send(msg.data); };
39
- server.onmessage = function(msg) { client.send(msg.data); };
44
+ function createRelayWebSocketPair(localAddr, remoteAddr) {
45
+ // Local websocket.
46
+ var local;
47
+ // Connect to the remote websocket server first, so that when we
48
+ // connect to the local websocket server the whole chain is ready to
49
+ // use immediately.
50
+ var remote = new WebSocket(remoteAddr + "/ws/rem/relay");
51
+ // Log server errors.
52
+ remote.onerror = function (event) {
53
+ console.error("[!] remote/error", event);
54
+ };
55
+ // When the remote server closes its websocket, close the corresponding
56
+ // local websocket too.
57
+ remote.onclose = function (event) {
58
+ console.log("[-] remote/close", {
59
+ code: event.code,
60
+ reason: event.reason,
61
+ wasClean: event.wasClean,
62
+ });
63
+ if (local?.readyState === WebSocket.OPEN) {
64
+ local.close(1000, `bound close: ${event.code} ${event.reason}`);
65
+ }
66
+ };
67
+ remote.onopen = function () {
68
+ // Connect to the local websocket server to relay data.
69
+ local = new WebSocket(localAddr + "/ws/loc/relay");
70
+ // Log relay errors.
71
+ local.onerror = function (event) {
72
+ console.error("[!] local/error", event);
40
73
  };
41
-
42
- client.onclose = function(event) {
43
- console.log("client/close", event.code, event.reason);
44
- if (server.readyState === WebSocket.OPEN) server.close();
74
+ // When the local websocket is closed, close the corresponding
75
+ // remote websocket.
76
+ local.onclose = function (event) {
77
+ if (local.cleanOpen) {
78
+ connections--;
79
+ connectionCount.textContent = connections;
80
+ }
81
+ console.log("[-] local/close", {
82
+ code: event.code,
83
+ reason: event.reason,
84
+ wasClean: event.wasClean,
85
+ });
86
+ if (remote.readyState === WebSocket.OPEN) {
87
+ remote.close(1000, `bound close: ${event.code} ${event.reason}`);
88
+ }
45
89
  };
46
- };
90
+ // Relay data between the local and remote websockets.
91
+ local.onopen = function () {
92
+ connections++;
93
+ connectionCount.textContent = connections;
94
+ local.cleanOpen = true;
47
95
 
48
- server.onclose = function(event) {
49
- console.log("server/close", event.code, event.reason);
50
- if (client.readyState === WebSocket.OPEN) client.close();
96
+ local.onmessage = function (msg) {
97
+ remote.send(msg.data);
98
+ };
99
+ remote.onmessage = function (msg) {
100
+ local.send(msg.data);
101
+ };
102
+ };
51
103
  };
52
104
  }
53
-
105
+ // Get the query params.
54
106
  var params = {};
55
- window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/g, function(m, key, value) {
56
- params[key] = decodeURIComponent(value);
57
- });
58
-
59
- var client_addr = "ws://" + location.hostname + ":8000";
60
- var server_addr = params.server || "wss://weblinkapp.herokuapp.com";
61
- var batch_size = Number(params.batch) || 4;
62
- var control = new WebSocket(client_addr + "/control");
63
-
64
- control.onerror = function(event) { console.error("control/error"); };
65
- control.onclose = function(event) { console.log("control/close"); };
66
- control.onmessage = function(msg) {
67
- for (var i = 0; i < batch_size; i++) {
68
- connect(client_addr, server_addr, msg.data);
107
+ window.location.href.replace(
108
+ /[?&]+([^=&]+)=([^&]*)/g,
109
+ function (m, key, value) {
110
+ params[key] = decodeURIComponent(value);
111
+ },
112
+ );
113
+ // Local weblink server.
114
+ var localAddr = "ws://" + location.host;
115
+ // Remote weblink server.
116
+ var remoteAddr = params.server || "wss://weblink-ouux.onrender.com";
117
+ // Establish a local control websocket to receive commands.
118
+ var control = new WebSocket(localAddr + "/ws/loc/control");
119
+ // Total number of local connections.
120
+ var connections = 0;
121
+ // Log control websocket errors.
122
+ control.onerror = function (event) {
123
+ console.error("[!] control/error", event);
124
+ };
125
+ // Log control websocket closure.
126
+ control.onclose = function (event) {
127
+ console.log("[-] control/close", {
128
+ code: event.code,
129
+ reason: event.reason,
130
+ });
131
+ };
132
+ // Add more connections on demand.
133
+ control.onmessage = function (msg) {
134
+ var connectionBatchSize = Number.parseInt(msg.data);
135
+ console.log("[~] control/batch", { count: connectionBatchSize });
136
+ for (var i = 0; i < connectionBatchSize; i++) {
137
+ createRelayWebSocketPair(localAddr, remoteAddr);
69
138
  }
70
139
  };
71
140
  </script>
141
+ <script
142
+ src="https://cdnjs.cloudflare.com/ajax/libs/nosleep/0.12.0/NoSleep.min.js"
143
+ type="text/javascript"
144
+ crossorigin="anonymous"
145
+ ></script>
146
+ <script src="nosleep.js" type="text/javascript"></script>
72
147
  </body>
73
148
  </html>
data/public/nosleep.js ADDED
@@ -0,0 +1,22 @@
1
+ var noSleep = new NoSleep();
2
+ var noSleepCheckbox = document.getElementById("nosleep");
3
+
4
+ document.addEventListener("visibilitychange", function resetNoSleep(event) {
5
+ if (document.visibilityState === "visible") {
6
+ noSleep.disable();
7
+ noSleep = new NoSleep();
8
+ noSleepCheckbox.checked = false;
9
+ }
10
+ });
11
+
12
+ noSleepCheckbox.addEventListener("click", function toggleNoSleep(event) {
13
+ if (noSleep.isEnabled) {
14
+ noSleep.disable();
15
+ } else {
16
+ try {
17
+ noSleep.enable();
18
+ } catch (error) {
19
+ event.preventDefault();
20
+ }
21
+ }
22
+ });
metadata CHANGED
@@ -1,39 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: weblink
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - soylent
8
8
  autorequire:
9
9
  bindir: bin
10
- cert_chain:
11
- - |
12
- -----BEGIN CERTIFICATE-----
13
- MIIEDjCCAvagAwIBAgIBATANBgkqhkiG9w0BAQsFADB3MRgwFgYDVQQDDA8xNTkz
14
- ODYwX3NveWxlbnQxFTATBgoJkiaJk/IsZAEZFgV1c2VyczEXMBUGCgmSJomT8ixk
15
- ARkWB25vcmVwbHkxFjAUBgoJkiaJk/IsZAEZFgZnaXRodWIxEzARBgoJkiaJk/Is
16
- ZAEZFgNjb20wHhcNMjIwMTI3MDIzMTQwWhcNMjMwMTI3MDIzMTQwWjB3MRgwFgYD
17
- VQQDDA8xNTkzODYwX3NveWxlbnQxFTATBgoJkiaJk/IsZAEZFgV1c2VyczEXMBUG
18
- CgmSJomT8ixkARkWB25vcmVwbHkxFjAUBgoJkiaJk/IsZAEZFgZnaXRodWIxEzAR
19
- BgoJkiaJk/IsZAEZFgNjb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
20
- AQC2DMbzgA39U+3VTMjXn+0jnOQyLdmXQ5EXgSLKCgBLIcFTc9J47Th07Yb/f4Rz
21
- Wh49/EkDBiDtLqFeKBYsj3q0e8tRCAs32NtVyl/4FDyJvWsK3R2tcXOVqxs48J3C
22
- gG+rFLOcMC9YF4FPTkz4p3EYGFVjZTbiqyVVuIZzWtrwdZesBVgpBRyN8sEyNoi8
23
- vcDiOmEwf9/TVMTDf/wu6a+i3LNVGYWlvgMJRssaAnj/IbFFtPTz30HxeTUdfJu8
24
- YbwRspfFzcJGLf32E7vXfmHHqNzqjh4zD9sVpvTHbqLLsgVa+nYHPHAedzSZ5gZj
25
- G1oZ7hZDCJoEPj0oCHT0qkuXAgMBAAGjgaQwgaEwCQYDVR0TBAIwADALBgNVHQ8E
26
- BAMCBLAwHQYDVR0OBBYEFKy+nUcB8D5BOM6D96HKG/tp2nQWMDMGA1UdEQQsMCqB
27
- KDE1OTM4NjArc295bGVudEB1c2Vycy5ub3JlcGx5LmdpdGh1Yi5jb20wMwYDVR0S
28
- BCwwKoEoMTU5Mzg2MCtzb3lsZW50QHVzZXJzLm5vcmVwbHkuZ2l0aHViLmNvbTAN
29
- BgkqhkiG9w0BAQsFAAOCAQEAsfJgulszbe+BgUP9xNYixuvm/R8Um5T8OAANmc+s
30
- CvV3lD5XtFuw838TbWrqgN+7dVkzQYERTz8DjPtx+zlgzNS3GyMWNUZdOPLo/YkR
31
- S7KfmAZlZhy1rcrARYsAouvP+1QttbiJ32L+z1JzAz2s1+4ySgl1OaQfosS04QGT
32
- CnJ982/OGeG5xm7KFOqSzG4KnyEy4VH47aE4IQk+yP6R96neHdfOd/C7BcA6gbgH
33
- lyshHS0Bgr1OKk3Vx8GtaIa7L9Z4RN0EWBL3QGlhiu55PrpPuAFDDuORJfpIqrph
34
- XjC/3xZEDxBLYSjeFIgUMN6+/XGduQANkZ6167QsVu/yPg==
35
- -----END CERTIFICATE-----
36
- date: 2022-03-05 00:00:00.000000000 Z
10
+ cert_chain: []
11
+ date: 2025-05-27 00:00:00.000000000 Z
37
12
  dependencies:
38
13
  - !ruby/object:Gem::Dependency
39
14
  name: em-websocket
@@ -49,20 +24,6 @@ dependencies:
49
24
  - - "~>"
50
25
  - !ruby/object:Gem::Version
51
26
  version: '0.5'
52
- - !ruby/object:Gem::Dependency
53
- name: webrick
54
- requirement: !ruby/object:Gem::Requirement
55
- requirements:
56
- - - "~>"
57
- - !ruby/object:Gem::Version
58
- version: '1.7'
59
- type: :runtime
60
- prerelease: false
61
- version_requirements: !ruby/object:Gem::Requirement
62
- requirements:
63
- - - "~>"
64
- - !ruby/object:Gem::Version
65
- version: '1.7'
66
27
  description:
67
28
  email:
68
29
  executables:
@@ -74,16 +35,12 @@ files:
74
35
  - VERSION
75
36
  - bin/weblink
76
37
  - lib/relay.rb
38
+ - lib/static_files.rb
77
39
  - lib/weblink.rb
78
- - public/android-chrome-192x192.png
79
- - public/android-chrome-512x512.png
80
- - public/apple-touch-icon.png
81
- - public/favicon-16x16.png
82
- - public/favicon-32x32.png
83
40
  - public/favicon.ico
41
+ - public/favicon.svg
84
42
  - public/index.html
85
- - public/weblink.js
86
- - public/weblink.webmanifest
43
+ - public/nosleep.js
87
44
  homepage: https://github.com/soylent/weblink
88
45
  licenses: []
89
46
  metadata: {}
@@ -102,7 +59,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
102
59
  - !ruby/object:Gem::Version
103
60
  version: '0'
104
61
  requirements: []
105
- rubygems_version: 3.3.4
62
+ rubygems_version: 3.5.3
106
63
  signing_key:
107
64
  specification_version: 4
108
65
  summary: Web browser gateway
checksums.yaml.gz.sig DELETED
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
data/public/weblink.js DELETED
@@ -1,59 +0,0 @@
1
- function connect(client_addr, server_addr) {
2
- var client = new WebSocket(client_addr + "/client");
3
- var server = new WebSocket(server_addr + "/server");
4
-
5
- server.onerror = function(event) {
6
- console.error("server/error");
7
- };
8
- client.onerror = function(event) {
9
- console.error("client/error");
10
- };
11
-
12
- client.onopen = function() {
13
- server.onopen = function() {
14
- client.onmessage = function(msg) {
15
- server.send(msg.data);
16
- };
17
- server.onmessage = function(msg) {
18
- client.send(msg.data);
19
- };
20
- };
21
- };
22
-
23
- server.onclose = function(event) {
24
- console.log("server/close", event.code, event.reason);
25
-
26
- if (client.readyState === WebSocket.OPEN) client.close();
27
- };
28
-
29
- client.onclose = function(event) {
30
- console.log("client/close", event.code, event.reason);
31
-
32
- if (server.readyState === WebSocket.OPEN) server.close();
33
- };
34
- }
35
-
36
- var params = {};
37
- window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/g, function(m, key, value) {
38
- params[key] = value;
39
- });
40
-
41
- var client_addr = "ws://" + location.hostname + ":8000";
42
- var server_addr = params.server || "wss://weblinkapp.herokuapp.com";
43
- var batch_size = Number(params.batch) || 4;
44
- var control = new WebSocket(client_addr + "/control");
45
-
46
- control.onerror = function(event) {
47
- console.error("control/error");
48
- };
49
- control.onclose = function(event) {
50
- console.log("control/close");
51
- };
52
- control.onmessage = function(msg) {
53
- for (var i = 0; i < batch_size; i++)
54
- connect(
55
- client_addr,
56
- server_addr
57
- );
58
- };
59
- control.onmessage();
@@ -1,19 +0,0 @@
1
- {
2
- "background_color": "#3f9cff",
3
- "display": "standalone",
4
- "icons": [
5
- {
6
- "sizes": "192x192",
7
- "src": "/android-chrome-192x192.png",
8
- "type": "image/png"
9
- },
10
- {
11
- "sizes": "512x512",
12
- "src": "/android-chrome-512x512.png",
13
- "type": "image/png"
14
- }
15
- ],
16
- "name": "weblink",
17
- "short_name": "weblink",
18
- "theme_color": "#3f9cff"
19
- }
data.tar.gz.sig DELETED
@@ -1,2 +0,0 @@
1
- 4�G��>DG+�����b��»1 ����~ɖ]/_�i��x�;u�,w���cXkw���|����2�F7�k`د�=�\����GQ�(N�߅L�X(h���&���9�X"������~���3?HP�؂��
2
- #8��;�d�X ���l}�(R��W^�����=*�4�OyU����T��M/U��B"��')���[Q^���6ss��&��W�w_�iߔ�&{�Lv���-��Wt��~<�.���0��$�;���
metadata.gz.sig DELETED
Binary file