Websockets (experimental)
This commit is contained in:
parent
6d33fa3a0e
commit
005edca228
|
@ -176,7 +176,7 @@ that some of that functionality may require additional dependencies, which
|
||||||
would otherwise be optional. This functionality includes:
|
would otherwise be optional. This functionality includes:
|
||||||
|
|
||||||
- PHP-like Session handling (via `landline/extensions/session`)
|
- PHP-like Session handling (via `landline/extensions/session`)
|
||||||
- Websockets (via `landline/extensions/websockets`) (coming soon)
|
- Websockets (via `landline/extensions/websockets`) (available for testing)
|
||||||
- (Probably something else eventually)
|
- (Probably something else eventually)
|
||||||
|
|
||||||
Landline is built entirely on Rack webserver interface, while being agnostic
|
Landline is built entirely on Rack webserver interface, while being agnostic
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
require 'landline/extensions/session'
|
require 'landline/extensions/session'
|
||||||
require 'landline'
|
require 'landline'
|
||||||
|
|
||||||
Landline::Session.hmac_secret = "Your secure signing secret here"
|
#Landline::Session.hmac_secret = "Your secure signing secret here"
|
||||||
|
|
||||||
app = Landline::Server.new do
|
app = Landline::Server.new do
|
||||||
get "/make_cookie" do
|
get "/make_cookie" do
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'landline'
|
||||||
|
require 'landline/extensions/websocket'
|
||||||
|
|
||||||
|
class Test < Landline::App
|
||||||
|
setup do
|
||||||
|
websocket "/test", version: 7 do |socket|
|
||||||
|
socket.on :message do |msg|
|
||||||
|
puts "Client wrote: #{msg}"
|
||||||
|
end
|
||||||
|
socket.on :error do |err|
|
||||||
|
puts "Error occured: #{err.inspect}"
|
||||||
|
puts err.backtrace
|
||||||
|
end
|
||||||
|
socket.on :close do
|
||||||
|
puts "Client closed read connection"
|
||||||
|
end
|
||||||
|
socket.ready
|
||||||
|
socket.write("Hi!")
|
||||||
|
response = socket.read
|
||||||
|
socket.write("You said: #{response}")
|
||||||
|
socket.write("Goodbye!")
|
||||||
|
socket.close
|
||||||
|
rescue Exception => e
|
||||||
|
puts e.inspect
|
||||||
|
puts e.backtrace
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
run Test.new
|
|
@ -0,0 +1,286 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'websocket'
|
||||||
|
module Landline
|
||||||
|
# Module that holds websocket primitives
|
||||||
|
module WebSocket
|
||||||
|
# Event system
|
||||||
|
module Eventifier
|
||||||
|
# Attach event listener
|
||||||
|
# @param event [Symbol]
|
||||||
|
# @param listener [#call]
|
||||||
|
def on(event, &listener)
|
||||||
|
@__listeners ||= {}
|
||||||
|
@__listeners[event] ||= []
|
||||||
|
@__listeners[event].append(listener)
|
||||||
|
listener
|
||||||
|
end
|
||||||
|
|
||||||
|
# Attach event listener
|
||||||
|
# @param event [Symbol]
|
||||||
|
# @param listener [#call]
|
||||||
|
def off(event, listener)
|
||||||
|
@__listeners ||= {}
|
||||||
|
@__listeners[event]&.delete(listener)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Await for an event
|
||||||
|
# @param event [Symbol, Array<Symbol>] event or array of events to wait for
|
||||||
|
# @return [Array]
|
||||||
|
# @sg-ignore
|
||||||
|
def await(event)
|
||||||
|
blocking = true
|
||||||
|
output = nil
|
||||||
|
listener = proc do |*data|
|
||||||
|
output = data
|
||||||
|
blocking = false
|
||||||
|
end
|
||||||
|
if event.is_a? Array
|
||||||
|
event.each { |x| on(x, &listener) }
|
||||||
|
else
|
||||||
|
on(event, &listener)
|
||||||
|
end
|
||||||
|
while blocking; end
|
||||||
|
return output[0] if output.is_a? Array and output.length == 1
|
||||||
|
|
||||||
|
output
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Trigger the queue clearing process
|
||||||
|
# @return [void]
|
||||||
|
def _process
|
||||||
|
return if @processing
|
||||||
|
|
||||||
|
@__processing = true
|
||||||
|
@__queue ||= []
|
||||||
|
@__listeners ||= {}
|
||||||
|
until @__queue.empty?
|
||||||
|
event, msg = @__queue.shift
|
||||||
|
if @__listeners.include? event
|
||||||
|
@__listeners[event].each { |x| x.call(*msg) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@processing = false
|
||||||
|
end
|
||||||
|
|
||||||
|
# Send internal event
|
||||||
|
# @param event [Symbol]
|
||||||
|
# @param data [Array]
|
||||||
|
# @return [void]
|
||||||
|
def _emit(event, *data)
|
||||||
|
return unless @__listeners
|
||||||
|
|
||||||
|
return unless @__listeners.include? event
|
||||||
|
|
||||||
|
@__queue ||= []
|
||||||
|
@__queue.push([event, data])
|
||||||
|
_process
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Socket-like object representing websocket interface
|
||||||
|
class WSockWrapper
|
||||||
|
include Eventifier
|
||||||
|
|
||||||
|
class WebSocketError < StandardError
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(io, version: 7)
|
||||||
|
@io = io
|
||||||
|
@version = version
|
||||||
|
@frame_parser = ::WebSocket::Frame::Incoming::Server.new(
|
||||||
|
version: version
|
||||||
|
)
|
||||||
|
@readable = true
|
||||||
|
@writable = true
|
||||||
|
@data = Queue.new
|
||||||
|
on :message do |msg|
|
||||||
|
@data.enq(msg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Start the main loop for the eventifier
|
||||||
|
# @return [void]
|
||||||
|
def ready
|
||||||
|
return if @ready
|
||||||
|
|
||||||
|
_loop
|
||||||
|
@ready = true
|
||||||
|
end
|
||||||
|
|
||||||
|
# Send data through websocket
|
||||||
|
# @param data [String] binary data
|
||||||
|
# @return [void]
|
||||||
|
def write(data, type: :text)
|
||||||
|
unless @writable
|
||||||
|
raise self.class::WebSocketError,
|
||||||
|
"socket closed for writing"
|
||||||
|
end
|
||||||
|
|
||||||
|
frame = ::WebSocket::Frame::Outgoing::Server.new(
|
||||||
|
version: @version,
|
||||||
|
data: data,
|
||||||
|
type: type
|
||||||
|
)
|
||||||
|
@io.write(frame.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Read data from socket synchronously
|
||||||
|
# @return [String, nil] nil returned if socket closes
|
||||||
|
def read
|
||||||
|
unless @readable
|
||||||
|
raise self.class::WebSocketError,
|
||||||
|
"socket closed for reading"
|
||||||
|
end
|
||||||
|
|
||||||
|
@data.deq
|
||||||
|
end
|
||||||
|
|
||||||
|
# Close the socket for reading
|
||||||
|
# @return [void]
|
||||||
|
def close_read
|
||||||
|
_emit :close
|
||||||
|
@readable = false
|
||||||
|
@io.close_read
|
||||||
|
end
|
||||||
|
|
||||||
|
# Close the socket for writing
|
||||||
|
def close_write
|
||||||
|
@writable = false
|
||||||
|
@io.close_write
|
||||||
|
end
|
||||||
|
|
||||||
|
# Establish a connection through handshake
|
||||||
|
# @return [self]
|
||||||
|
def self.handshake(request, version: 7, **opts)
|
||||||
|
raise StandardError, "stream cannot be hijacked" unless request.hijack
|
||||||
|
|
||||||
|
handshake = create_handshake(request, version: version, **opts)
|
||||||
|
return nil unless handshake
|
||||||
|
|
||||||
|
io = request.hijack.call
|
||||||
|
io.sendmsg(handshake.to_s)
|
||||||
|
new(io, version: version)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Initiate a handshake
|
||||||
|
def self.create_handshake(request, **opts)
|
||||||
|
handshake = ::WebSocket::Handshake::Server.new(**opts)
|
||||||
|
handshake.from_hash({
|
||||||
|
headers: request.headers,
|
||||||
|
path: request.path_info,
|
||||||
|
query: request.query.query,
|
||||||
|
body: request.body
|
||||||
|
})
|
||||||
|
return nil unless handshake.finished? and handshake.valid?
|
||||||
|
|
||||||
|
handshake
|
||||||
|
end
|
||||||
|
|
||||||
|
# Close the socket
|
||||||
|
# @return [void]
|
||||||
|
def close
|
||||||
|
_close
|
||||||
|
@writable = false
|
||||||
|
@readable = false
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Event reading loop
|
||||||
|
# @return [void]
|
||||||
|
def _loop
|
||||||
|
@thread = Thread.new do
|
||||||
|
loop do
|
||||||
|
msg = _read
|
||||||
|
if msg and [:text, :binary].include? msg.type
|
||||||
|
_emit :message, msg
|
||||||
|
elsif msg and msg.type == :close
|
||||||
|
_emit :__close, msg
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rescue IOError => e
|
||||||
|
@writable = false
|
||||||
|
_emit :error, e
|
||||||
|
close
|
||||||
|
ensure
|
||||||
|
close_read
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Receive data through websocket
|
||||||
|
# @return [String] output from frame
|
||||||
|
def _read
|
||||||
|
while (char = @io.getc)
|
||||||
|
@frame_parser << char
|
||||||
|
frame = @frame_parser.next
|
||||||
|
return frame if frame
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Close the websocket
|
||||||
|
# @return [void]
|
||||||
|
def _close
|
||||||
|
frame = ::WebSocket::Frame::Outgoing::Server.new(
|
||||||
|
version: @version,
|
||||||
|
type: :close
|
||||||
|
)
|
||||||
|
@io.write(frame.to_s) if @writable
|
||||||
|
sleep 0.1
|
||||||
|
@io.close
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module Landline
|
||||||
|
module Handlers
|
||||||
|
# Web socket server handler
|
||||||
|
class WebSockServer < Landline::Probe
|
||||||
|
# @param path [Object]
|
||||||
|
# @param parent [Landline::Node]
|
||||||
|
# @param params [Hash] options hash
|
||||||
|
# @param callback [#call] callback to process request within
|
||||||
|
# @option params [Integer] :version protocol version
|
||||||
|
# @option params [Array<String>] :protocols array of supported sub-protocols
|
||||||
|
# @option params [Boolean] :secure true if the server will use wss:// protocol
|
||||||
|
def initialize(path, parent:, **params, &callback)
|
||||||
|
nodeparams = params.dup
|
||||||
|
nodeparams.delete(:version)
|
||||||
|
nodeparams.delete(:protocols)
|
||||||
|
nodeparams.delete(:secure)
|
||||||
|
super(path, parent: parent, **nodeparams)
|
||||||
|
@callback = callback
|
||||||
|
@params = params
|
||||||
|
end
|
||||||
|
|
||||||
|
# Method callback on successful request navigation
|
||||||
|
# Creates a websocket from a given request
|
||||||
|
# @param request [Landline::Request]
|
||||||
|
def process(request)
|
||||||
|
@callback.call(
|
||||||
|
Landline::WebSocket::WSockWrapper.handshake(
|
||||||
|
request,
|
||||||
|
**@params
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module DSL
|
||||||
|
module PathConstructors
|
||||||
|
# (in Landline::Path context)
|
||||||
|
# Create a new websocket handler
|
||||||
|
def websocket(path, **args, &setup)
|
||||||
|
register(Landline::Handlers::WebSockServer.new(path,
|
||||||
|
parent: @origin,
|
||||||
|
**args,
|
||||||
|
&setup))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -100,7 +100,7 @@ module Landline
|
||||||
@filters.append(block)
|
@filters.append(block)
|
||||||
end
|
end
|
||||||
|
|
||||||
attr_reader :children, :properties, :request
|
attr_reader :children, :properties
|
||||||
|
|
||||||
attr_accessor :bounce, :pipeline
|
attr_accessor :bounce, :pipeline
|
||||||
|
|
||||||
|
@ -157,9 +157,7 @@ module Landline
|
||||||
|
|
||||||
@bounce ? exit_stack(request) : _die(404)
|
@bounce ? exit_stack(request) : _die(404)
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
_die(500, backtrace: [e.to_s] + e.backtrace)
|
_die(request, 500, backtrace: [e.to_s] + e.backtrace)
|
||||||
ensure
|
|
||||||
@request = nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Run enqueued postprocessors on navigation failure
|
# Run enqueued postprocessors on navigation failure
|
||||||
|
@ -190,7 +188,7 @@ module Landline
|
||||||
# @param errorcode [Integer]
|
# @param errorcode [Integer]
|
||||||
# @param backtrace [Array(String), nil]
|
# @param backtrace [Array(String), nil]
|
||||||
# @raise [UncaughtThrowError] throws :finish to stop processing
|
# @raise [UncaughtThrowError] throws :finish to stop processing
|
||||||
def _die(errorcode, backtrace: nil)
|
def _die(request, errorcode, backtrace: nil)
|
||||||
proccontext = get_context(request)
|
proccontext = get_context(request)
|
||||||
throw :finish, [errorcode].append(
|
throw :finish, [errorcode].append(
|
||||||
*proccontext.instance_exec(
|
*proccontext.instance_exec(
|
||||||
|
|
|
@ -14,9 +14,6 @@ module Landline
|
||||||
@callback = exec
|
@callback = exec
|
||||||
end
|
end
|
||||||
|
|
||||||
attr_accessor :response
|
|
||||||
attr_reader :request
|
|
||||||
|
|
||||||
# Method callback on successful request navigation.
|
# Method callback on successful request navigation.
|
||||||
# Runs block supplied with object initialization.
|
# Runs block supplied with object initialization.
|
||||||
# Request's #splat and #param are passed to block.
|
# Request's #splat and #param are passed to block.
|
||||||
|
|
Loading…
Reference in New Issue