landline/hyde.rb

356 lines
10 KiB
Ruby

require 'mime-types'
require 'webrick'
require 'uri'
module Hyde
# Branding and version
VERSION = "0.5 (alpha)"
attr_reader :VERSION
VLINE = "<ADDRESS>\n Hyde/#{Hyde::VERSION} on WEBrick/#{WEBrick::VERSION} (Ruby/#{RUBY_VERSION}/#{RUBY_RELEASE_DATE})\n </ADDRESS>"
attr_reader :VLINE
class Server < WEBrick::HTTPServer
def initialize(config={},&setup)
super(config)
@hyde_pathspec = Hyde::Pathspec.new "/", &setup
self.mount_proc '/' do |req,res|
context = Hyde::Context.new(req.path, req, res)
while context and (not context.exit_loop) do
context.exit_loop = true
context = catch :controlled_exit do
@hyde_pathspec._match(context)
context
end
end
end
end
end
module ErrorPages
# 404 text
def error404(request, filepath)
request.response.status = 404
if request.handles.include? 404
request.response.body = request.handles[404].call(
filepath
)
else
request.response.body = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\">\n<HTML>\n <HEAD><TITLE>File not found</TITLE></HEAD>\n <BODY>\n <H1>File not found</H1>\n #{filepath}\n <HR>\n #{Hyde::VLINE}\n </BODY>\n</HTML>"
end
request.response["Content-Type"] = 'text/html'
end
module_function :error404
end
# Interchangeable glob/regex/string pattern matching
module PatternMatching
def _prep_path(path,safe_regexp: true)
@safe_regexp = safe_regexp
@path = _normalize(path) if path.kind_of? String
@path = path if path.kind_of? Regexp
end
def _match?(path, ctx)
# behvaiour used by "index" method
return true if @path == ""
split_path = path.split("/").filter { |x| x != "" }
if @path.kind_of? Regexp then
# this chunk of fuck is needed to force regexp into 3 rules:
# 1) unsafe regexp means match the whole (remaining) line.
# 3) safe regexp means match only the part before the next slash
# 2) a ._match? only returns when there are no leftovers
# this forces the matching to work somewhat consistently
test = @path.match _normalize_input(path) unless @safe_regexp
test = @path.match split_path[0] if @safe_regexp
if test and (test.pre_match == "") and (test.post_match == "") then
return true
else
return false
end
else
# algorithm to match path segments until no more left in @path
@path.split("/").filter { |x| x != "" }
.each_with_index { |x,i|
return false if x != split_path[i]
}
return true
end
end
def _normalize_input(path)
# remove duplicate slashes and trim edge slashes
(path.split "/").filter { |x| x != "" }.join("/")
end
def _normalize(path)
# remove duplicate slashe s and trim edge slashes
path = _normalize_input(path)
# globbing behaviour simulated with regexp
if path.match /(?<!\\)[\*\?\[]/ then
path = Regexp.new(path
.gsub(/[\^\$\.\|\+\(\)\{\}]/,"\\\\\\0")
.gsub(/(?<!\\)\*\*/,".*")
.gsub(/(?<![.\\])\*/,"[^/]*")
.gsub(/(?<!\\)\?/,".")
.gsub(/(?<!\\)\[\!/,"[^")
)
end
return path
end
end
# Methods to control requests, accessible from within blocks
module PublicContextControlMethods
def redirect(url)
res = @current_context.response
res.status = 301
res.body = "<HTML><A HREF=\"#{url}\">#{url}</A>.</HTML>\n"
res.header['location'] = URI(url).to_s
throw :controlled_exit, @current_context
end
def rewrite(url)
new_context = Context::rewrite(@current_context,url)
new_context.exit_loop = false
throw :controlled_exit, new_context
end
end
# Request wrapper class
class Context
def initialize(path,request,response)
@path = path
@filepath = ""
@request = request
@response = response
@handles = {}
@indexlist = []
@vars = {}
@exit_loop = false
end
def self.rewrite(pctx,newpath)
newctx = self.new(newpath,pctx.request,pctx.response)
newctx.vars = pctx.vars
return newctx
end
attr_reader :request
attr_reader :response
attr_accessor :filepath
attr_accessor :path
attr_accessor :handles
attr_accessor :indexlist
attr_accessor :exit_loop
attr_accessor :vars
end
# Context object with safe path encapsulation
class ProtectedContext < Context
def initialize(request)
@path = request.path
@filepath = request.filepath
@request = request.request
@response = request.response
@handles = request.handles
@indexlist = request.indexlist
@exit_loop = request.exit_loop
@vars = request.vars
end
undef :path=
undef :filepath=
undef :handles=
undef :indexlist=
end
# Handler classes
class Probe
include Hyde::PatternMatching
include Hyde::PublicContextControlMethods
def initialize (path, safe_regexp: true, &block_optional)
_prep_path path, safe_regexp: safe_regexp
@block = block_optional
end
def _match(request)
if @block and (_match? request.path, request) then
@current_context = Hyde::ProtectedContext.new(request)
return_later = self.instance_exec @current_context, &@block
return return_later
end
end
def _match?(path, request)
# End node - nothing must be after it
if super(path,request) then
match_path = _normalize_input(path).match(@path)
return (match_path.post_match == "")
end
end
end
class Serve < Hyde::Probe
def _match(request)
return super if @block
if _match? request.path, request then
match_path = _normalize_input(request.path).match(@path)[0]
filepath = request.filepath+match_path
begin
mimetype = MIME::Types.type_for(filepath)
file = File.new filepath, "r"
data = file.read()
request.response.body = data
request.response["Content-Type"] = mimetype
rescue Errno::ENOENT
Hyde::ErrorPages::error404 request, request.request.path
end
end
end
end
class GetMatch < Hyde::Probe
@match_method = "get"
def initialize(*a, **b, &block)
@match_method = (self.class.instance_variable_get :@match_method)
raise Exception, "block required!" if not block
super(*a, **b, &block)
end
def _match?(path, ctx)
if ctx.request.request_method == @match_method.upcase then
return super(path, ctx)
else
return false
end
end
end
class PostMatch < GetMatch
@match_method = "post"
end
class PutMatch < GetMatch
@match_method = "put"
end
class PatchMatch < GetMatch
@match_method = "patch"
end
class DeleteMatch < GetMatch
@match_method = "delete"
end
class OptionsMatch < GetMatch
@match_method = "options"
end
class LinkMatch < GetMatch
@match_method = "link"
end
class UnlinkMatch < GetMatch
@match_method = "unlink"
end
class PrintProbe < Hyde::Probe
def _match(request)
if _match? request.path, request then
puts "#{request.path} matched!"
end
end
end
# Handler invocation functions
module Handlers
{
probe: Hyde::Probe,
printProbe: Hyde::PrintProbe,
serve: Hyde::Serve,
post: Hyde::PostMatch,
get: Hyde::GetMatch,
put: Hyde::PutMatch,
patch: Hyde::PatchMatch,
delete: Hyde::DeleteMatch,
options: Hyde::OptionsMatch,
link: Hyde::LinkMatch,
unlink: Hyde::UnlinkMatch
}.each_pair { |name, newclass|
define_method name do |path, *a, **b, &block|
if path.kind_of? Array then
path.each do |x|
@chain.append newclass.new x, *a, **b, &block
end
else
@chain.append newclass.new path, *a, **b, &block
end
end
}
end
class Pathspec
include Hyde::PatternMatching
include Hyde::Handlers
def initialize (path, root_path: nil, safe_regexp: true, &block)
_prep_path path, safe_regexp: safe_regexp
@chain = []
@handles = {}
@root_override = root_path
@remap = false
self.instance_exec &block
end
def path(path, *a, **b, &block)
if path.kind_of? Array then
path.each do |x|
@chain.append Hyde::Pathspec.new x, *a, **b, &block
end
else
@chain.append Hyde::Pathspec.new path, *a, **b, &block
end
end
def root(path)
@root_override = "/"+_normalize_input(path)
end
def remap(path)
@root_override = "/"+_normalize_input(path)
@remap = true
end
def index(list)
@indexlist = list if list.kind_of? Array
end
def handle(code, &block)
@handles[code] = block
end
def preprocess(&block)
@preprocessor = block
end
def _match(request)
self.instance_exec request, &@preprocessor if @preprocessor
if _match? request.path, request then
match_path = _normalize_input(request.path).match(@path)
next_path = match_path[0]
request.path = cut_path = match_path.post_match
# remap/root method handling
if @root_override then
request.filepath = if @remap then
@root_override+"/"
else @root_override+"/"+next_path+"/" end
else
request.filepath = request.filepath+next_path+"/"
end
# parameter overrides overlaying
@handles.each_pair { |k,v| request.handles[k] = v }
request.indexlist = @indexlist if @indexlist
# do directory indexing
if cut_path.match /^\/?$/ then
request.indexlist.each do |x|
try_index = @chain.find { |y| y._match? x, request }
if try_index then
request.path = x
return try_index._match request
end
end
end
# passthrough to the next path object
next_pathspec = @chain.find { |x| x._match? cut_path, request }
next_pathspec._match request if next_pathspec
unless next_pathspec then
# throw 404 if nowhere to go
Hyde::ErrorPages::error404 request, request.request.path
end
end
end
end
end