356 lines
10 KiB
Ruby
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
|