outpunch-ruby-rack 0.5.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 +7 -0
- data/lib/outpunch/rack/connection.rb +85 -0
- data/lib/outpunch/rack/hooks.rb +35 -0
- data/lib/outpunch/rack/middleware.rb +32 -0
- data/lib/outpunch/rack/server.rb +84 -0
- data/lib/outpunch/rack/version.rb +7 -0
- data/lib/outpunch/rack.rb +62 -0
- data/lib/outpunch-ruby-rack.rb +3 -0
- metadata +133 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 0653aca03868461d7cf634e6304cea2e39e4839fa781c95629fd9624329b5111
|
|
4
|
+
data.tar.gz: 3088141712a4222697b5d702e7ac7a1d050f035579ccdf9a3351946fa30ed79c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 742044a6fa94ed852d83c4c2c3eb094826f241f83ad9d8accec681a01f05cf6d48475e6f24ecf115988b7357ddbb275e0952fe728797d1e1e4738c36f05461b8
|
|
7
|
+
data.tar.gz: 963ab38e87bafa442d4cac21b9567c06c11d3e957a9fb6f2b99c7cf5ba3c5a52a4d733bbe25f2eed9a9614feee878299f9bc4dc310abab3f7f6e85dea6ceab10
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "websocket/driver"
|
|
4
|
+
|
|
5
|
+
module Outpunch
|
|
6
|
+
module Rack
|
|
7
|
+
# Bridges a hijacked Rack socket to a native outpunch core connection.
|
|
8
|
+
# Ruby handles socket I/O and WebSocket framing; the core handles auth,
|
|
9
|
+
# routing and request state.
|
|
10
|
+
class Connection
|
|
11
|
+
def initialize(native)
|
|
12
|
+
@native = native
|
|
13
|
+
@write_mutex = Mutex.new
|
|
14
|
+
@io = nil
|
|
15
|
+
@driver = nil
|
|
16
|
+
@env = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Hijack the socket and run the WebSocket loop. Blocks until disconnected.
|
|
20
|
+
def run(env)
|
|
21
|
+
@env = env
|
|
22
|
+
@io = env.fetch("rack.hijack_io")
|
|
23
|
+
@driver = WebSocket::Driver.rack(self)
|
|
24
|
+
@driver.on(:message) { |event| @native.push_message(event.data) }
|
|
25
|
+
@driver.on(:close) { @native.close }
|
|
26
|
+
@driver.on(:error) { |e| log_error(e.message) }
|
|
27
|
+
@driver.start
|
|
28
|
+
|
|
29
|
+
core = Thread.new { @native.run }
|
|
30
|
+
writer = Thread.new { write_loop }
|
|
31
|
+
|
|
32
|
+
while (chunk = @io.readpartial(4096))
|
|
33
|
+
@driver.parse(chunk)
|
|
34
|
+
end
|
|
35
|
+
rescue EOFError, IOError, Errno::ECONNRESET, Errno::EPIPE
|
|
36
|
+
# normal close
|
|
37
|
+
ensure
|
|
38
|
+
@native.close
|
|
39
|
+
core&.join
|
|
40
|
+
writer&.join
|
|
41
|
+
close_io
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Called by websocket-driver to write raw bytes to the socket.
|
|
45
|
+
def write(data)
|
|
46
|
+
@io.write(data)
|
|
47
|
+
rescue IOError, Errno::EPIPE, Errno::ECONNRESET
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# websocket-driver requires #env and #url to build the handshake.
|
|
52
|
+
def env
|
|
53
|
+
@env || {}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def url
|
|
57
|
+
scheme = env["rack.url_scheme"] == "https" ? "wss" : "ws"
|
|
58
|
+
host = env["HTTP_HOST"] || env["SERVER_NAME"] || "localhost"
|
|
59
|
+
"#{scheme}://#{host}#{env["REQUEST_URI"]}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# Deliver core messages to the client until the core connection ends, then
|
|
65
|
+
# close the socket so the read loop in #run returns.
|
|
66
|
+
def write_loop
|
|
67
|
+
while (msg = @native.next_outgoing)
|
|
68
|
+
@write_mutex.synchronize { @driver.text(msg) }
|
|
69
|
+
end
|
|
70
|
+
@write_mutex.synchronize { @driver.close }
|
|
71
|
+
close_io
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def close_io
|
|
75
|
+
@io.close unless @io.closed?
|
|
76
|
+
rescue IOError
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def log_error(message)
|
|
81
|
+
warn "[Outpunch::Rack] WebSocket error: #{message}"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Outpunch
|
|
4
|
+
module Rack
|
|
5
|
+
module Hooks
|
|
6
|
+
HOOKS = {}
|
|
7
|
+
|
|
8
|
+
def self.register(service_name, path_pattern, handler_class)
|
|
9
|
+
HOOKS[service_name] ||= []
|
|
10
|
+
HOOKS[service_name] << { pattern: path_pattern, handler_class: handler_class }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.before_proxy(service_name:, path:, payload:, request:)
|
|
14
|
+
find_handlers(service_name, path).each do |h|
|
|
15
|
+
h[:handler_class].new.before_proxy(path: path, payload: payload, request: request)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.after_proxy(service_name:, path:, payload:, result:, request:)
|
|
20
|
+
find_handlers(service_name, path).each do |h|
|
|
21
|
+
h[:handler_class].new.after_proxy(path: path, payload: payload, result: result, request: request)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.clear!
|
|
26
|
+
HOOKS.clear
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.find_handlers(service_name, path)
|
|
30
|
+
(HOOKS[service_name] || []).select { |h| path.match?(h[:pattern]) }
|
|
31
|
+
end
|
|
32
|
+
private_class_method :find_handlers
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Outpunch
|
|
4
|
+
module Rack
|
|
5
|
+
class Middleware
|
|
6
|
+
WS_PATH = "/ws"
|
|
7
|
+
|
|
8
|
+
def initialize(app, server: nil)
|
|
9
|
+
@app = app
|
|
10
|
+
@server = server
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(env)
|
|
14
|
+
if env["PATH_INFO"] == WS_PATH && websocket_upgrade?(env)
|
|
15
|
+
env["rack.hijack"].call
|
|
16
|
+
conn = (@server || Outpunch::Rack.server).create_connection
|
|
17
|
+
Thread.new { conn.run(env) }
|
|
18
|
+
[-1, {}, []]
|
|
19
|
+
else
|
|
20
|
+
@app.call(env)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def websocket_upgrade?(env)
|
|
27
|
+
env["HTTP_UPGRADE"]&.downcase == "websocket" &&
|
|
28
|
+
env["HTTP_CONNECTION"]&.downcase&.include?("upgrade")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "outpunch/outpunch"
|
|
4
|
+
require "base64"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module Outpunch
|
|
8
|
+
module Rack
|
|
9
|
+
# Ruby-facing wrapper around the native outpunch core server.
|
|
10
|
+
class Server
|
|
11
|
+
HOP_BY_HOP = %w[HOST CONNECTION UPGRADE].freeze
|
|
12
|
+
|
|
13
|
+
def initialize(secret:, timeout: 25)
|
|
14
|
+
raise ArgumentError, "outpunch secret must be set" if secret.nil? || secret.to_s.empty?
|
|
15
|
+
|
|
16
|
+
@native = Outpunch::Server.new(secret.to_s, timeout.to_f, nil)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create_connection
|
|
20
|
+
Connection.new(@native.create_connection)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def connected?(service_name)
|
|
24
|
+
@native.connected?(service_name.to_s)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Send a request through the tunnel and block until response or timeout.
|
|
28
|
+
# Raises Outpunch::NotConnectedError (a RuntimeError) when no client is connected
|
|
29
|
+
# and Outpunch::TunnelTimeoutError (a Timeout::Error) on timeout.
|
|
30
|
+
def handle_request(service:, method:, path:, query:, headers:, body:)
|
|
31
|
+
@native.handle_request(
|
|
32
|
+
service.to_s,
|
|
33
|
+
method.to_s,
|
|
34
|
+
path.to_s,
|
|
35
|
+
stringify(query),
|
|
36
|
+
stringify(headers),
|
|
37
|
+
body&.to_s
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Build a success result hash from raw tunnel response data.
|
|
42
|
+
def success_response(data)
|
|
43
|
+
body = data["body"]
|
|
44
|
+
headers = (data["headers"] || {}).transform_keys(&:downcase)
|
|
45
|
+
|
|
46
|
+
if data["body_encoding"] == "base64" && !body.nil? && !body.empty?
|
|
47
|
+
body = Base64.decode64(body)
|
|
48
|
+
body.force_encoding("BINARY")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
{
|
|
52
|
+
status: data["status"] || 200,
|
|
53
|
+
body: body,
|
|
54
|
+
headers: headers,
|
|
55
|
+
body_encoding: data["body_encoding"]
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Build an error result hash.
|
|
60
|
+
def error_response(status, message)
|
|
61
|
+
{
|
|
62
|
+
status: status,
|
|
63
|
+
body: JSON.generate(error: message),
|
|
64
|
+
headers: { "Content-Type" => "application/json" }
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Extract and normalize HTTP headers from a Rack env hash.
|
|
69
|
+
def extract_proxy_headers(headers)
|
|
70
|
+
headers
|
|
71
|
+
.to_h
|
|
72
|
+
.select { |k, _| k.to_s.start_with?("HTTP_") }
|
|
73
|
+
.transform_keys { |k| k.to_s.sub("HTTP_", "").tr("_", "-") }
|
|
74
|
+
.reject { |k, _| HOP_BY_HOP.include?(k) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def stringify(hash)
|
|
80
|
+
(hash || {}).to_h.to_h { |k, v| [k.to_s, v.to_s] }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "rack/version"
|
|
4
|
+
require_relative "rack/hooks"
|
|
5
|
+
require_relative "rack/server"
|
|
6
|
+
require_relative "rack/connection"
|
|
7
|
+
require_relative "rack/middleware"
|
|
8
|
+
|
|
9
|
+
module Outpunch
|
|
10
|
+
module Rack
|
|
11
|
+
class Configuration
|
|
12
|
+
attr_accessor :secret, :timeout, :base_controller, :authorize_service, :hooks, :route_prefix
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@timeout = 25
|
|
16
|
+
@base_controller = "ActionController::API"
|
|
17
|
+
@hooks = Outpunch::Rack::Hooks
|
|
18
|
+
@route_prefix = "/outpunch"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def configure
|
|
24
|
+
yield configuration
|
|
25
|
+
@server = nil # reset server so it picks up new config
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def configuration
|
|
29
|
+
@configuration ||= Configuration.new
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def server
|
|
33
|
+
@server ||= Server.new(
|
|
34
|
+
secret: configuration.secret,
|
|
35
|
+
timeout: configuration.timeout
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Delegates — convenience access for application code.
|
|
40
|
+
|
|
41
|
+
def connected?(service_name)
|
|
42
|
+
server.connected?(service_name)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def handle_request(**kwargs)
|
|
46
|
+
server.handle_request(**kwargs)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def success_response(data)
|
|
50
|
+
server.success_response(data)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def error_response(status, message)
|
|
54
|
+
server.error_response(status, message)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def extract_proxy_headers(headers)
|
|
58
|
+
server.extract_proxy_headers(headers)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: outpunch-ruby-rack
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.5.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- TechyCorp
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-10-28 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: websocket-driver
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.7'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.7'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: outpunch-ruby
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - '='
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 0.5.0
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - '='
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: 0.5.0
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rspec
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '3.12'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.12'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rack
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '3.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3.0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: puma
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '6.0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '6.0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: websocket-client-simple
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0.6'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0.6'
|
|
97
|
+
description:
|
|
98
|
+
email:
|
|
99
|
+
executables: []
|
|
100
|
+
extensions: []
|
|
101
|
+
extra_rdoc_files: []
|
|
102
|
+
files:
|
|
103
|
+
- lib/outpunch-ruby-rack.rb
|
|
104
|
+
- lib/outpunch/rack.rb
|
|
105
|
+
- lib/outpunch/rack/connection.rb
|
|
106
|
+
- lib/outpunch/rack/hooks.rb
|
|
107
|
+
- lib/outpunch/rack/middleware.rb
|
|
108
|
+
- lib/outpunch/rack/server.rb
|
|
109
|
+
- lib/outpunch/rack/version.rb
|
|
110
|
+
homepage:
|
|
111
|
+
licenses:
|
|
112
|
+
- MIT
|
|
113
|
+
metadata: {}
|
|
114
|
+
post_install_message:
|
|
115
|
+
rdoc_options: []
|
|
116
|
+
require_paths:
|
|
117
|
+
- lib
|
|
118
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
119
|
+
requirements:
|
|
120
|
+
- - ">="
|
|
121
|
+
- !ruby/object:Gem::Version
|
|
122
|
+
version: '3.1'
|
|
123
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
124
|
+
requirements:
|
|
125
|
+
- - ">="
|
|
126
|
+
- !ruby/object:Gem::Version
|
|
127
|
+
version: '0'
|
|
128
|
+
requirements: []
|
|
129
|
+
rubygems_version: 3.5.22
|
|
130
|
+
signing_key:
|
|
131
|
+
specification_version: 4
|
|
132
|
+
summary: Rack adapter for the outpunch reverse WebSocket tunnel
|
|
133
|
+
test_files: []
|