Skip to content

Instantly share code, notes, and snippets.

@namelessjon
Last active December 17, 2015 01:09
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save namelessjon/5526013 to your computer and use it in GitHub Desktop.
Save namelessjon/5526013 to your computer and use it in GitHub Desktop.
Combine a rack app with reel, to get the best of both worlds: A rack app (could be sinatra or even rails) for routing and dealing with pure HTTP requests, and all the power of reel and celluloid for dealing with things like websockets.
# Copyright (c) 2013 Jonathan Stott
#
# Permission is hereby granted, free of charge, to any person ob-
# taining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without restric-
# tion, including without limitation the rights to use, copy, modi-
# fy, merge, publish, distribute, sublicense, and/or sell copies of
# the Software, and to permit persons to whom the Software is fur-
# nished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONIN-
# FRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
require 'reel'
require 'rack/body_proxy'
# hacky fix to make this work
module Rack
class BodyProxy
include Enumerable
end
end
module Reel
class MountedServer < Server
attr_reader :rack
def initialize(rack, host, port)
super(host, port, &method(:on_connection))
@rack = rack
end
def on_connection(connection)
while request = connection.request
case request
when Request
route_request connection, request
when WebSocket
route_websocket request
end
end
end
def route_request(connection, request)
opts = {:method => request.method, :input => request.body, "REMOTE_ADDR" => connection.remote_addr}
opts.merge!(convert_headers(request.headers))
status, headers, body = rack.call Rack::MockRequest.env_for(request.url, opts)
# This is currently not that as body is almost certainly a String or BodyProxy.
# Maybe I need to unwrap?
response_klass = body.is_a?(Stream) ? StreamResponse : Response
connection.respond(response_klass.new(status_symbol(status), headers, body))
# I hope this won't blow up
body.close if body.respond_to?(:close)
end
def convert_headers(headers)
Hash[headers.map { |key, value| ['HTTP_' + key.upcase.gsub('-','_'),value ] }]
end
def route_websocket(socket)
end
def status_symbol(status)
status.is_a?(Fixnum) ? Http::Response::STATUS_CODES[status].downcase.gsub(/\s|-/, '_').to_sym : status.to_sym
end
# The code in CommonLogger is copied almost verbatim from rack/common_logger which is made available
# under the following terms:
# Copyright (c) 2007, 2008, 2009, 2010, 2011, 2012 Christian Neukirchen <purl.org/net/chneukirchen>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# Rack::CommonLogger forwards every request to the given +app+, and
# logs a line in the
# {Apache common log format}[http://httpd.apache.org/docs/1.3/logs.html#common]
# to the +logger+.
#
# If +logger+ is nil, CommonLogger will fall back +rack.errors+, which is
# an instance of Rack::NullLogger.
#
# +logger+ can be any class, including the standard library Logger, and is
# expected to have a +write+ method, which accepts the CommonLogger::FORMAT.
# According to the SPEC, the error stream must also respond to +puts+
# (which takes a single argument that responds to +to_s+), and +flush+
# (which is called without arguments in order to make the error appear for
# sure)
class CommonLogger
# Common Log Format: http://httpd.apache.org/docs/1.3/logs.html#common
#
# lilith.local - - [07/Aug/2006 23:58:02] "GET / HTTP/1.1" 500 -
#
# %{%s - %s [%s] "%s %s%s %s" %d %s\n} %
FORMAT = %{%s - %s [%s] "%s %s%s %s" %d %s %0.4f}
def initialize(app, logger=nil)
@app = app
@logger = logger
end
def call(env)
began_at = Time.now
status, header, body = @app.call(env)
header = Rack::Utils::HeaderHash.new(header)
body = Rack::BodyProxy.new(body) { log(env, status, header, began_at) }
[status, header, body]
end
private
def log(env, status, header, began_at)
now = Time.now
length = extract_content_length(header)
logger = @logger || Celluloid.logger
logger.info FORMAT % [
env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-",
env["REMOTE_USER"] || "-",
now.strftime("%d/%b/%Y %H:%M:%S"),
env["REQUEST_METHOD"],
env["PATH_INFO"],
env["QUERY_STRING"].empty? ? "" : "?"+env["QUERY_STRING"],
env["HTTP_VERSION"],
status.to_s[0..3],
length,
now - began_at ]
end
def extract_content_length(headers)
value = headers['Content-Length'] or return '-'
value.to_s == '0' ? '-' : value
end
end
end
end
require 'mounted_server'
class Server < Reel::MountedServer
include Celluloid::Logger
def initialize(rack, host = "127.0.0.1", port = 1234)
super(rack, host, port)
end
def route_websocket(socket)
info "#{request.peeraddr[2]} connected"
if socket.url == "/chat"
Client.new(socket)
else
info "Invalid WebSocket request for: #{socket.url}"
socket.close
end
end
end
require 'sinatra'
class Test < Sinatra::Application
disable :logging
use Reel::MountedServer::CommonLogger
get '/' do
p request.user_agent
p url
'hi!'
end
end
Server.supervise_as :server, Test.new
sleep
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment