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 +4 -4
- data/CHANGELOG +15 -0
- data/VERSION +1 -1
- data/bin/weblink +23 -38
- data/lib/relay.rb +71 -47
- data/lib/static_files.rb +29 -0
- data/lib/weblink.rb +159 -68
- data/public/favicon.ico +0 -0
- data/public/favicon.svg +29 -0
- data/public/index.html +120 -45
- data/public/nosleep.js +22 -0
- metadata +7 -50
- checksums.yaml.gz.sig +0 -0
- data/public/android-chrome-192x192.png +0 -0
- data/public/android-chrome-512x512.png +0 -0
- data/public/apple-touch-icon.png +0 -0
- data/public/favicon-16x16.png +0 -0
- data/public/favicon-32x32.png +0 -0
- data/public/weblink.js +0 -59
- data/public/weblink.webmanifest +0 -19
- data.tar.gz.sig +0 -2
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6e829338fc3dde9de2d16c8ec3b7278bfd225b55ed9bbc253219932f9426da32
|
4
|
+
data.tar.gz: 8d0e86f6bb6fe90ba4faa513fcb347983c92c2df2743d5238ee04b2897de5e27
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
+
1.3.0
|
data/bin/weblink
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
-
#!/usr/bin/env -S ruby
|
1
|
+
#!/usr/bin/env -S ruby
|
2
2
|
|
3
|
-
$
|
3
|
+
$VERBOSE = nil
|
4
|
+
$>.sync = true
|
5
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
4
6
|
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
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
|
-
|
18
|
+
"Web browser gateway"
|
17
19
|
|
18
|
-
op.separator
|
19
|
-
op.separator
|
20
|
+
op.separator ""
|
21
|
+
op.separator "Options"
|
20
22
|
|
21
|
-
op.on(
|
23
|
+
op.on("-c", "--client", "start weblink client (default)") do
|
22
24
|
opts[:client] = true
|
23
25
|
end
|
24
26
|
|
25
|
-
op.on(
|
27
|
+
op.on("-s", "--server", "start weblink server") do
|
26
28
|
opts[:server] = true
|
27
29
|
end
|
28
30
|
|
29
|
-
op.on(
|
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(
|
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(
|
38
|
-
|
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(
|
43
|
-
opts[:
|
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.
|
47
|
-
|
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(
|
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.
|
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(
|
2
|
+
def initialize(side, logproc)
|
6
3
|
super
|
7
4
|
pause
|
8
|
-
@
|
9
|
-
@
|
10
|
-
@
|
11
|
-
@
|
12
|
-
@
|
13
|
-
@
|
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
|
17
|
-
@
|
18
|
-
@
|
19
|
-
|
20
|
-
|
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
|
-
|
30
|
-
@websocket.send_binary(data)
|
61
|
+
@ws.send_binary(data)
|
31
62
|
end
|
32
63
|
|
33
|
-
def unbind
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
@
|
50
|
-
|
51
|
-
|
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
|
data/lib/static_files.rb
ADDED
@@ -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
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
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(
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
16
|
-
|
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
|
-
|
19
|
-
|
20
|
-
if
|
21
|
-
|
22
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
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
|
-
|
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
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
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
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
data/public/favicon.svg
ADDED
@@ -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
|
-
<!
|
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="
|
7
|
+
<link rel="icon" href="favicon.svg" type="image/svg+xml" />
|
7
8
|
<style>
|
8
|
-
html,
|
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
|
-
|
19
|
-
|
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
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
43
|
-
|
44
|
-
if (
|
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
|
-
|
49
|
-
|
50
|
-
|
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(
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
var
|
63
|
-
|
64
|
-
|
65
|
-
control
|
66
|
-
control
|
67
|
-
|
68
|
-
|
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.
|
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/
|
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
|
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
|
data/public/apple-touch-icon.png
DELETED
Binary file
|
data/public/favicon-16x16.png
DELETED
Binary file
|
data/public/favicon-32x32.png
DELETED
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();
|
data/public/weblink.webmanifest
DELETED
@@ -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
metadata.gz.sig
DELETED
Binary file
|