commencing full rewrite of hyde
This commit is contained in:
parent
ff36326ec8
commit
161689bec0
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,8 @@
|
|||
require 'rack'
|
||||
require_relative 'test_app'
|
||||
app = Rack::Builder.new do
|
||||
use Rack::Lint
|
||||
run TestApp::App.new
|
||||
end
|
||||
|
||||
run app
|
|
@ -3,94 +3,73 @@ require 'webrick'
|
|||
require 'uri'
|
||||
require 'pp'
|
||||
|
||||
# Primary module
|
||||
module Hyde
|
||||
# Branding and version
|
||||
VERSION = "0.5 (alpha)"
|
||||
# Hyde version
|
||||
# @type [String]
|
||||
VERSION = '0.5 (alpha)'
|
||||
attr_reader :VERSION
|
||||
|
||||
# Hyde branding and version (for error templates)
|
||||
# @type [String]
|
||||
VLINE = "Hyde/#{Hyde::VERSION} on WEBrick/#{WEBrick::VERSION} (Ruby/#{RUBY_VERSION}/#{RUBY_RELEASE_DATE})\n"
|
||||
attr_reader :VLINE
|
||||
|
||||
def error_template(errortext,backtrace)
|
||||
<<HTMLEOF
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>#{WEBrick::HTMLUtils.escape(errortext)}</title>
|
||||
<style> .header {background-color: #CC7878; padding: 0.5rem; border-bottom-width: 0.2rem; border-bottom-style: solid; border-bottom-color: #202222; overflow: auto;} .title { font-weight: bolder; font-size: 36px; margin: 0 0; text-shadow: 1px 1px 1px #202222, 2px 2px 2px #404444; float: left } body { margin: 0;
|
||||
} .text { font-size 1rem; } .small { color: #7D7D7D; font-size: 12px;} .code { font-family: monospace; font-size: 0.7rem; } </style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<p class="title">HYDE</p>
|
||||
<p style="float: right"><a href="https://adastra7.net/git/yessiest/hyde">Source code</a></p>
|
||||
</div>
|
||||
<div style="padding: 0.5rem">
|
||||
<p class="text">#{WEBrick::HTMLUtils.escape(errortext)}</p>
|
||||
<pre><code class="text code">
|
||||
#{WEBrick::HTMLUtils.escape(backtrace) or "\n\n\n"}
|
||||
</code></pre>
|
||||
<hr/>
|
||||
<p class="small">#{WEBrick::HTMLUtils.escape(VLINE)}</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
HTMLEOF
|
||||
# Generate HTML error template
|
||||
# @param errortext [String] Error explanation
|
||||
# @param backtrace [String] Ruby backtrace
|
||||
def error_template(errortext, backtrace)
|
||||
<<~HTMLEOF
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>#{WEBrick::HTMLUtils.escape(errortext)}</title>
|
||||
<style> .header {background-color: #CC7878; padding: 0.5rem; border-bottom-width: 0.2rem; border-bottom-style: solid; border-bottom-color: #202222; overflow: auto;} .title { font-weight: bolder; font-size: 36px; margin: 0 0; text-shadow: 1px 1px 1px #202222, 2px 2px 2px #404444; float: left } body { margin: 0;
|
||||
} .text { font-size 1rem; } .small { color: #7D7D7D; font-size: 12px;} .code { font-family: monospace; font-size: 0.7rem; } </style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<p class="title">HYDE</p>
|
||||
<p style="float: right"><a href="https://adastra7.net/git/yessiest/hyde">Source code</a></p>
|
||||
</div>
|
||||
<div style="padding: 0.5rem">
|
||||
<p class="text">#{WEBrick::HTMLUtils.escape(errortext)}</p>
|
||||
<pre><code class="text code">
|
||||
#{WEBrick::HTMLUtils.escape(backtrace) or "\n\n\n"}
|
||||
</code></pre>
|
||||
<hr/>
|
||||
<p class="small">#{WEBrick::HTMLUtils.escape(Hyde::VLINE)}</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
HTMLEOF
|
||||
end
|
||||
module_function :error_template
|
||||
|
||||
WEBrick::HTTPResponse.class_exec do
|
||||
attr_accessor :recent_backtrace
|
||||
public
|
||||
def set_backtrace(backtrace)
|
||||
@recent_backtrace = backtrace
|
||||
end
|
||||
|
||||
attr_accessor :recent_backtrace
|
||||
|
||||
def create_error_page
|
||||
@body = Hyde.error_template(@reason_phrase,@recent_backtrace)
|
||||
@body = Hyde.error_template(@reason_phrase, @recent_backtrace)
|
||||
end
|
||||
end
|
||||
|
||||
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)
|
||||
begin
|
||||
while context and (not context.exit_loop) do
|
||||
context.exit_loop = true
|
||||
context = catch :controlled_exit do
|
||||
@hyde_pathspec._match(context)
|
||||
context
|
||||
end
|
||||
while postprocessor = context.queue_postprocess.shift do
|
||||
postprocessor.call(context)
|
||||
end
|
||||
end
|
||||
while finalizer = context.queue_finalize.shift do
|
||||
finalizer.call(context)
|
||||
end
|
||||
rescue Exception => e
|
||||
puts e.message
|
||||
puts e.backtrace
|
||||
res.set_backtrace "#{e.message} (#{e.class})\n#{e.backtrace.join "\n"}"
|
||||
res.status = 500
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Interchangeable glob/regex/string pattern matching
|
||||
module PatternMatching
|
||||
def _prep_path(path,safe_regexp: true)
|
||||
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
|
||||
@path = _normalize(path) if path.is_a? String
|
||||
@path = path if path.is_a? Regexp
|
||||
end
|
||||
def _match?(path, ctx)
|
||||
|
||||
# @return [Boolean]
|
||||
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
|
||||
return true if @path == ''
|
||||
split_path = path.split('/').filter { |x| x != '' }
|
||||
if @path.is_a? Regexp
|
||||
# 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
|
||||
|
@ -98,38 +77,40 @@ HTMLEOF
|
|||
# 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
|
||||
if test and (test.pre_match == '') and (test.post_match == '')
|
||||
true
|
||||
else
|
||||
return false
|
||||
false
|
||||
end
|
||||
else
|
||||
# algorithm to match path segments until no more left in @path
|
||||
@path.split("/").filter { |x| x != "" }
|
||||
.each_with_index { |x,i|
|
||||
@path.split('/').filter { |x| x != '' }
|
||||
.each_with_index do |x, i|
|
||||
return false if x != split_path[i]
|
||||
}
|
||||
return true
|
||||
end
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def _normalize_input(path)
|
||||
# remove duplicate slashes and trim edge slashes
|
||||
(path.split "/").filter { |x| x != "" }.join("/")
|
||||
(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
|
||||
if path.match /(?<!\\)[\*\?\[]/
|
||||
path = Regexp.new(path
|
||||
.gsub(/[\^\$\.\|\+\(\)\{\}]/,"\\\\\\0")
|
||||
.gsub(/(?<!\\)\*\*/,".*")
|
||||
.gsub(/(?<![.\\])\*/,"[^/]*")
|
||||
.gsub(/(?<!\\)\?/,".")
|
||||
.gsub(/(?<!\\)\[\!/,"[^")
|
||||
.gsub(/[\^\$\.\|\+\(\)\{\}]/, '\\\\\\0')
|
||||
.gsub(/(?<!\\)\*\*/, '.*')
|
||||
.gsub(/(?<![.\\])\*/, '[^/]*')
|
||||
.gsub(/(?<!\\)\?/, '.')
|
||||
.gsub(/(?<!\\)\[\!/, '[^')
|
||||
)
|
||||
end
|
||||
return path
|
||||
path
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -142,18 +123,18 @@ HTMLEOF
|
|||
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 = Context.rewrite(@current_context, url)
|
||||
new_context.exit_loop = false
|
||||
throw :controlled_exit, new_context
|
||||
end
|
||||
def die(code, message=nil, backtrace="")
|
||||
|
||||
def die(code, message = nil, backtrace = '')
|
||||
@current_context.response.status = code
|
||||
@current_context.response.set_backtrace(backtrace)
|
||||
if not message then
|
||||
message = WEBrick::HTTPStatus::StatusMessage[code]
|
||||
end
|
||||
if @current_context.codehandlers[code] then
|
||||
@current_context.response.backtrace = backtrace
|
||||
message ||= WEBrick::HTTPStatus::StatusMessage[code]
|
||||
if @current_context.codehandlers[code]
|
||||
@current_context.codehandlers[code].call(@current_context, message, backtrace)
|
||||
else
|
||||
@current_context.response.body = Hyde.error_template(message, backtrace)
|
||||
|
@ -164,9 +145,9 @@ HTMLEOF
|
|||
|
||||
# Request wrapper class
|
||||
class Context
|
||||
def initialize(path,request,response)
|
||||
def initialize(path, request, response)
|
||||
@path = path
|
||||
@filepath = ""
|
||||
@filepath = ''
|
||||
@request = request
|
||||
@response = response
|
||||
@indexlist = []
|
||||
|
@ -176,24 +157,22 @@ HTMLEOF
|
|||
@queue_finalize = []
|
||||
@exit_loop = false
|
||||
end
|
||||
def self.rewrite(pctx,newpath)
|
||||
newctx = Context.new(newpath,pctx.request,pctx.response)
|
||||
|
||||
def self.rewrite(pctx, newpath)
|
||||
newctx = Context.new(newpath, pctx.request, pctx.response)
|
||||
newctx.vars = pctx.vars
|
||||
newctx.queue_finalize = pctx.queue_finalize.clone
|
||||
newctx.queue_postprocess = pctx.queue_postprocess.clone
|
||||
return newctx
|
||||
newctx
|
||||
end
|
||||
|
||||
def enqueue_finalizer(dup: false, &block)
|
||||
if block_given? then
|
||||
if dup or not @queue_finalize.include? block then
|
||||
@queue_finalize.append(block)
|
||||
end
|
||||
end
|
||||
return unless block_given?
|
||||
@queue_finalize.append(block) if dup or !@queue_finalize.include? block
|
||||
end
|
||||
|
||||
def enqueue_postprocessor(&block)
|
||||
if block_given? then
|
||||
@queue_postprocess.append(block)
|
||||
end
|
||||
@queue_postprocess.append(block) if block_given?
|
||||
end
|
||||
attr_reader :request
|
||||
attr_reader :response
|
||||
|
@ -232,101 +211,100 @@ HTMLEOF
|
|||
class Probe
|
||||
include Hyde::PatternMatching
|
||||
include Hyde::PublicContextControlMethods
|
||||
def initialize (path, safe_regexp: true, &block_optional)
|
||||
def initialize(path, safe_regexp: true, &block_optional)
|
||||
_prep_path path, safe_regexp: safe_regexp
|
||||
@block = block_optional
|
||||
@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
|
||||
return unless @block and (_match? request.path, request)
|
||||
@current_context = Hyde::ProtectedContext.new(request)
|
||||
return_later = instance_exec @current_context, &@block
|
||||
return_later
|
||||
end
|
||||
|
||||
# @sg-ignore
|
||||
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
|
||||
return unless super(path, request)
|
||||
match_path = _normalize_input(path).match(@path)
|
||||
match_path.post_match == ''
|
||||
end
|
||||
end
|
||||
|
||||
class Serve < Hyde::Probe
|
||||
def _match(request)
|
||||
return super if @block
|
||||
if _match? request.path, request then
|
||||
@current_context = request
|
||||
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
|
||||
die(404)
|
||||
end
|
||||
return unless _match? request.path, request
|
||||
@current_context = request
|
||||
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
|
||||
die(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class GetMatch < Hyde::Probe
|
||||
@match_method = "get"
|
||||
@match_method = 'get'
|
||||
def initialize(*a, **b, &block)
|
||||
@match_method = (self.class.instance_variable_get :@match_method)
|
||||
raise Exception, "block required!" if not block
|
||||
raise Exception, 'block required!' unless block
|
||||
super(*a, **b, &block)
|
||||
end
|
||||
|
||||
def _match?(path, ctx)
|
||||
if ctx.request.request_method == @match_method.upcase then
|
||||
return super(path, ctx)
|
||||
if ctx.request.request_method == @match_method.upcase
|
||||
super(path, ctx)
|
||||
else
|
||||
return false
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class PostMatch < GetMatch
|
||||
@match_method = "post"
|
||||
@match_method = 'post'
|
||||
end
|
||||
|
||||
class PutMatch < GetMatch
|
||||
@match_method = "put"
|
||||
@match_method = 'put'
|
||||
end
|
||||
|
||||
class PatchMatch < GetMatch
|
||||
@match_method = "patch"
|
||||
@match_method = 'patch'
|
||||
end
|
||||
|
||||
class DeleteMatch < GetMatch
|
||||
@match_method = "delete"
|
||||
@match_method = 'delete'
|
||||
end
|
||||
|
||||
class OptionsMatch < GetMatch
|
||||
@match_method = "options"
|
||||
@match_method = 'options'
|
||||
end
|
||||
|
||||
class LinkMatch < GetMatch
|
||||
@match_method = "link"
|
||||
@match_method = 'link'
|
||||
end
|
||||
|
||||
class UnlinkMatch < GetMatch
|
||||
@match_method = "unlink"
|
||||
@match_method = 'unlink'
|
||||
end
|
||||
|
||||
class PrintProbe < Hyde::Probe
|
||||
def _match(request)
|
||||
if _match? request.path, request then
|
||||
puts "#{request.path} matched!"
|
||||
end
|
||||
puts "#{request.path} matched!" if _match? request.path, request
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Handler invocation functions
|
||||
module Handlers
|
||||
{
|
||||
{
|
||||
probe: Hyde::Probe,
|
||||
printProbe: Hyde::PrintProbe,
|
||||
serve: Hyde::Serve,
|
||||
|
@ -338,9 +316,9 @@ HTMLEOF
|
|||
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
|
||||
}.each_pair do |name, newclass|
|
||||
define_method name do |path, *a, **b, &block|
|
||||
if path.is_a? Array
|
||||
path.each do |x|
|
||||
@chain.append newclass.new x, *a, **b, &block
|
||||
end
|
||||
|
@ -348,22 +326,23 @@ HTMLEOF
|
|||
@chain.append newclass.new path, *a, **b, &block
|
||||
end
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
class Pathspec
|
||||
include Hyde::PatternMatching
|
||||
include Hyde::Handlers
|
||||
include Hyde::PublicContextControlMethods
|
||||
def initialize (path, root_path: nil, safe_regexp: true, &block)
|
||||
def initialize(path, root_path: nil, safe_regexp: true, &block)
|
||||
_prep_path path, safe_regexp: safe_regexp
|
||||
@chain = []
|
||||
@root_override = root_path
|
||||
@remap = false
|
||||
self.instance_exec &block
|
||||
instance_exec(&block)
|
||||
end
|
||||
|
||||
def path(path, *a, **b, &block)
|
||||
if path.kind_of? Array then
|
||||
if path.is_a? Array
|
||||
path.each do |x|
|
||||
@chain.append Hyde::Pathspec.new x, *a, **b, &block
|
||||
end
|
||||
|
@ -371,51 +350,58 @@ HTMLEOF
|
|||
@chain.append Hyde::Pathspec.new path, *a, **b, &block
|
||||
end
|
||||
end
|
||||
|
||||
def root(path)
|
||||
@root_override = "/"+_normalize_input(path)
|
||||
@root_override = '/' + _normalize_input(path)
|
||||
end
|
||||
|
||||
def remap(path)
|
||||
@root_override = "/"+_normalize_input(path)
|
||||
@root_override = '/' + _normalize_input(path)
|
||||
@remap = true
|
||||
end
|
||||
|
||||
def index(list)
|
||||
@indexlist = list if list.kind_of? Array
|
||||
@indexlist = [list] if list.kind_of? String
|
||||
@indexlist = list if list.is_a? Array
|
||||
@indexlist = [list] if list.is_a? String
|
||||
end
|
||||
|
||||
def preprocess(&block)
|
||||
@preprocessor = block
|
||||
end
|
||||
|
||||
def postprocess(&block)
|
||||
@postprocessor = block
|
||||
@postprocessor = block
|
||||
end
|
||||
|
||||
def finalize(dup: false, &block)
|
||||
@finalizer = block
|
||||
@finalizer_dup = dup
|
||||
end
|
||||
|
||||
def _match(request)
|
||||
@current_context = request
|
||||
self.instance_exec request, &@preprocessor if @preprocessor
|
||||
request.enqueue_postprocessor &@postprocessor if @preprocessor
|
||||
instance_exec request, &@preprocessor if @preprocessor
|
||||
request.enqueue_postprocessor(&@postprocessor) if @preprocessor
|
||||
request.enqueue_finalizer dup: @finalizer_dup, &@finalizer if @finalizer
|
||||
if _match? request.path, request then
|
||||
if _match? request.path, request
|
||||
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
|
||||
if @root_override
|
||||
request.filepath = if @remap
|
||||
@root_override + '/'
|
||||
else @root_override + '/' + next_path + '/' end
|
||||
else
|
||||
request.filepath = request.filepath+next_path+"/"
|
||||
request.filepath = request.filepath + next_path + '/'
|
||||
end
|
||||
# redefine indexing parameters if they are defined for a pathspec
|
||||
# redefine indexing parameters if they are defined for a pathspec
|
||||
request.indexlist = @indexlist if @indexlist
|
||||
# do directory indexing
|
||||
if cut_path.match /^\/?$/ then
|
||||
if cut_path.match %r{^/?$}
|
||||
request.indexlist.each do |x|
|
||||
try_index = @chain.find { |y| y._match? x, request }
|
||||
if try_index then
|
||||
if try_index
|
||||
request.path = x
|
||||
return try_index._match request
|
||||
end
|
||||
|
@ -424,7 +410,7 @@ HTMLEOF
|
|||
# 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
|
||||
unless next_pathspec
|
||||
# die and throw up if nowhere to go
|
||||
die(404)
|
||||
end
|
|
@ -0,0 +1,189 @@
|
|||
module Hyde
|
||||
# String and path processing utilities
|
||||
module PatternMatching
|
||||
# Strips extra slashes from a string
|
||||
# (including slashes at the start and end of the string)
|
||||
# @param string [String]
|
||||
# @return [String]
|
||||
def self.canonicalize(string)
|
||||
string.gsub(/\/+/, "/")
|
||||
.delete_prefix("/")
|
||||
.delete_suffix("/")
|
||||
end
|
||||
|
||||
# Implements glob-like pattern matching
|
||||
# Exact specifications for globbing rules:
|
||||
# "/"
|
||||
# - act as directory separators
|
||||
# - multiple slashes (i.e. "///") are the same as one slash ("/")
|
||||
# - slashes are stripped at start and end of an expression or path
|
||||
# - slashes are not matched by anything but the globstar ("**")
|
||||
#
|
||||
# "*" ( regexp: /([^/]*)/ )
|
||||
# - matches from 0 to any number of characters
|
||||
# - does not match nothing if placed between two slashes (i.e "/*/")
|
||||
# - result is captured in an array
|
||||
# - stops at slashes
|
||||
# - greedy (matches as much as possible)
|
||||
#
|
||||
# "**" ( regexp: /(.*)/ )
|
||||
# - matches any number of characters
|
||||
# - matches slashes ("/")
|
||||
# - result is captured in an array
|
||||
# - does not stop at slashes
|
||||
# - greedy (matches as much as possible)
|
||||
#
|
||||
# "?" ( regexp: /[^/]/ )
|
||||
# - matches exactly one character
|
||||
# - result is not captured
|
||||
# - cannot match slashes
|
||||
#
|
||||
# "[...]" ( regexp: itself, ! and ^ at the start are interchangeable )
|
||||
# - acts like a regexp range
|
||||
# - matches any characters, including slashes if specified
|
||||
# - valid ways to specify a range: [A-z], [a-z], [9-z] (ascii order)
|
||||
# - ! or ^ at the start invert meaning (any character not in range)
|
||||
# - result is not captured
|
||||
#
|
||||
# ":<name>" ( regexp: acts like a named group for /[^/]*/ )
|
||||
# - acts like * as defined above
|
||||
# - result is captured in a hash with <name> as key
|
||||
# - <name> allows alphanumeric characters and underscores
|
||||
class Glob
|
||||
# @param input [String] Glob pattern
|
||||
def initialize(pattern)
|
||||
pattern = Hyde::PatternMatching.canonicalize(pattern)
|
||||
pieces = pattern.split(/(\/\*\*\/|\*\*|\*|\?|\[!?\w-\w\]|:[^\/]+)/)
|
||||
# @type [Array<String,Integer>]
|
||||
@index = build_index(pieces)
|
||||
# @type [Regexp]
|
||||
@glob = Regexp.new(pieces.map do |filter|
|
||||
case filter
|
||||
when "/**/" then "/(.*/|)"
|
||||
when "**" then "(.*)"
|
||||
when "*" then "([^/]*)"
|
||||
when "?" then "[^/]"
|
||||
when /^\[!?\w-\w\]$/ then filter.sub('!', '^')
|
||||
when /:[\w_]+/ then "[^/]*"
|
||||
else filter.gsub(/[\^$.|+(){}]/, '\\\\\\0')
|
||||
end
|
||||
end.join("").prepend("^/?"))
|
||||
puts @glob
|
||||
end
|
||||
|
||||
# Match the string and assign matches to parameters
|
||||
# Returns:
|
||||
# - Unmatched part of a string
|
||||
# - Unnamed parameters
|
||||
# - Named parameters
|
||||
# @param input [String] String to match
|
||||
# @return [Array(String,Array,Hash)]
|
||||
def match(input)
|
||||
input = Hyde::PatternMatching.canonicalize(input)
|
||||
result = input.match(@glob)
|
||||
input = result.post_match
|
||||
named_params, params = assign_by_index(@index, result.captures)
|
||||
[input, params, named_params]
|
||||
end
|
||||
|
||||
# Test if a string can be matched
|
||||
# Lighter version of match that doesn't assign any variables
|
||||
# @param input [String] String to match
|
||||
# @return [Boolean]
|
||||
def match?(input)
|
||||
input = Hyde::PatternMatching.canonicalize(input)
|
||||
input.match? @glob
|
||||
end
|
||||
|
||||
# Test if input is convertible to a Glob and if there is any reason to
|
||||
# @param input
|
||||
# @return [Boolean] Input can be safely converted to Glob
|
||||
def self.can_convert?(input)
|
||||
input.kinf_of? String and
|
||||
input.match?(/(?<!^\\)(?:\*\*|\*|\?|\[!?\w-\w\]|:[^\/]+)/)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Build an index for separating normal matches from named matches
|
||||
# @param pieces [Array(String)] Glob pattern after being splitted
|
||||
# @return [Array(String,Integer)] Index array to use with assign_to_index
|
||||
def build_index(pieces)
|
||||
count = -1
|
||||
index = []
|
||||
pieces.each do |x|
|
||||
if x.match?(/(\*\*|\*)/)
|
||||
index.append(count += 1)
|
||||
elsif (name = x.match(/:[^\/]+/))
|
||||
index.append(name)
|
||||
end
|
||||
end
|
||||
index
|
||||
end
|
||||
|
||||
# Assign values from match.captures to named and numbered groups
|
||||
# @param index [Array(String,Integer)] Index array generated by build_index
|
||||
# @param params [Array] Unnamed captures from a String.match
|
||||
def assign_by_index(index, params)
|
||||
named_params = {}
|
||||
new_params = []
|
||||
params.each_with_index do |x, k|
|
||||
if index[k].is_a? String
|
||||
named_params[index[k]] = x
|
||||
else
|
||||
new_params[index[k]] = x
|
||||
end
|
||||
end
|
||||
[named_params, new_params]
|
||||
end
|
||||
end
|
||||
|
||||
# Implements regexp pattern matching
|
||||
class ReMatch
|
||||
def initialize(pattern)
|
||||
@glob = pattern
|
||||
end
|
||||
|
||||
def match
|
||||
|
||||
end
|
||||
|
||||
def self.can_convert?(string)
|
||||
string.is_a? Regexp
|
||||
end
|
||||
end
|
||||
|
||||
# Umbrella class that picks a suitable pattern to be a middle man for
|
||||
class Pattern
|
||||
def initialize(pattern, **options)
|
||||
@pattern = patternify(pattern)
|
||||
@static = @pattern.is_a? String
|
||||
@options = options
|
||||
end
|
||||
|
||||
def static?
|
||||
@static
|
||||
end
|
||||
|
||||
def match
|
||||
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Try and convert the string to a pattern, if possible
|
||||
def patternify(pattern)
|
||||
Glob.new(pattern, **@options) if Glob.can_convert?(pattern)
|
||||
ReMatch.new(pattern, **@options) if Glob.can_convert?(pattern)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class PathBinding
|
||||
|
||||
end
|
||||
|
||||
class Path
|
||||
|
||||
end
|
||||
end
|
|
@ -0,0 +1,2 @@
|
|||
require_relative 'pattern_matching/util'
|
||||
require_relative 'pattern_matching/glob'
|
|
@ -0,0 +1,130 @@
|
|||
module Hyde
|
||||
module PatternMatching
|
||||
# Implements glob-like pattern matching
|
||||
# Exact specifications for globbing rules:
|
||||
# "/"
|
||||
# - act as directory separators
|
||||
# - multiple slashes (i.e. "///") are the same as one slash ("/")
|
||||
# - slashes are stripped at start and end of an expression or path
|
||||
# - slashes are not matched by anything but the globstar ("**")
|
||||
#
|
||||
# "*" ( regexp: /([^/]*)/ )
|
||||
# - matches from 0 to any number of characters
|
||||
# - does not match nothing if placed between two slashes (i.e "/*/")
|
||||
# - result is captured in an array
|
||||
# - stops at slashes
|
||||
# - greedy (matches as much as possible)
|
||||
#
|
||||
# "**" ( regexp: /(.*)/ )
|
||||
# - matches any number of characters
|
||||
# - matches slashes ("/")
|
||||
# - result is captured in an array
|
||||
# - does not stop at slashes
|
||||
# - greedy (matches as much as possible)
|
||||
#
|
||||
# "?" ( regexp: /[^/]/ )
|
||||
# - matches exactly one character
|
||||
# - result is captured
|
||||
# - cannot match slashes
|
||||
#
|
||||
# "[...]" ( regexp: itself, ! and ^ at the start are interchangeable )
|
||||
# - acts like a regexp range
|
||||
# - matches any characters, including slashes if specified
|
||||
# - valid ways to specify a range: [A-z], [a-z], [9-z] (ascii order)
|
||||
# - ! or ^ at the start invert meaning (any character not in range)
|
||||
# - result is captured
|
||||
#
|
||||
# ":<name>" ( regexp: acts like a named group for /[^/]*/ )
|
||||
# - acts like * as defined above
|
||||
# - result is captured in a hash with <name> as key
|
||||
# - <name> allows alphanumeric characters and underscores
|
||||
class Glob
|
||||
# @param input [String] Glob pattern
|
||||
def initialize(pattern)
|
||||
pattern = Hyde::PatternMatching.canonicalize(pattern)
|
||||
pieces = pattern.split(/(\/\*\*\/|\*\*|\*|\?|\[!?\w-\w\]|:[^\/]+)/)
|
||||
# @type [Array<String,Integer>]
|
||||
@index = build_index(pieces)
|
||||
# @type [Regexp]
|
||||
@glob = Regexp.new(pieces.map do |filter|
|
||||
case filter
|
||||
when "/**/" then "/(.*/|)"
|
||||
when "**" then "(.*)"
|
||||
when "*" then "([^/]*)"
|
||||
when "?" then "([^/])"
|
||||
when /^\[!?\w-\w\]$/ then "(#{filter.sub('!', '^')})"
|
||||
when /:[\w_]+/ then "([^/]*)"
|
||||
else filter.gsub(/[\^$.|+(){}]/, '\\\\\\0')
|
||||
end
|
||||
end.join("").prepend("^/?"))
|
||||
end
|
||||
|
||||
# Match the string and assign matches to parameters
|
||||
# Returns:
|
||||
# - Unmatched part of a string
|
||||
# - Unnamed parameters
|
||||
# - Named parameters
|
||||
# @param input [String] String to match
|
||||
# @return [Array(String,Array,Hash)]
|
||||
def match(input)
|
||||
input = Hyde::PatternMatching.canonicalize(input)
|
||||
result = input.match(@glob)
|
||||
input = result.post_match
|
||||
named_params, params = assign_by_index(@index, result.captures)
|
||||
[input, params, named_params]
|
||||
end
|
||||
|
||||
# Test if a string can be matched
|
||||
# Lighter version of match that doesn't assign any variables
|
||||
# @param input [String] String to match
|
||||
# @return [Boolean]
|
||||
def match?(input)
|
||||
input = Hyde::PatternMatching.canonicalize(input)
|
||||
input.match? @glob
|
||||
end
|
||||
|
||||
# Test if input is convertible to a Glob and if there is any reason to
|
||||
# @param input
|
||||
# @return [Boolean] Input can be safely converted to Glob
|
||||
def self.can_convert?(input)
|
||||
input.kinf_of? String and
|
||||
input.match?(/(?<!^\\)(?:\*\*|\*|\?|\[!?\w-\w\]|:[^\/]+)/)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Build an index for separating normal matches from named matches
|
||||
# @param pieces [Array(String)] Glob pattern after being splitted
|
||||
# @return [Array(String,Integer)] Index array to use with assign_to_index
|
||||
def build_index(pieces)
|
||||
count = -1
|
||||
index = []
|
||||
pieces.each do |x|
|
||||
if x.match?(/(\/\*\*\/|\*\*|\*|\?|\[!?\w-\w\])/)
|
||||
index.append(count += 1)
|
||||
elsif (name = x.match(/(?<=:)[^\/]+/))
|
||||
index.append(name[0])
|
||||
end
|
||||
end
|
||||
index
|
||||
end
|
||||
|
||||
# Assign values from match.captures to named and numbered groups
|
||||
# @param index [Array(String,Integer)] Index array generated by build_index
|
||||
# @param params [Array] Unnamed captures from a String.match
|
||||
def assign_by_index(index, params)
|
||||
named_params = {}
|
||||
new_params = []
|
||||
puts index.inspect
|
||||
params.each_with_index do |x, k|
|
||||
if index[k].is_a? String
|
||||
named_params[index[k]] = x
|
||||
else
|
||||
new_params[index[k]] = x
|
||||
end
|
||||
end
|
||||
[named_params, new_params]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,13 @@
|
|||
module Hyde
|
||||
module PatternMatching
|
||||
# Strips extra slashes from a string
|
||||
# (including slashes at the start and end of the string)
|
||||
# @param string [String]
|
||||
# @return [String]
|
||||
def self.canonicalize(string)
|
||||
string.gsub(/\/+/, "/")
|
||||
.delete_prefix("/")
|
||||
.delete_suffix("/")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,106 @@
|
|||
require_relative "../lib/hyde/pattern_matching"
|
||||
require "test/unit"
|
||||
|
||||
class TestGlob < Test::Unit::TestCase
|
||||
include Hyde::PatternMatching
|
||||
# match? test
|
||||
def test_matchq
|
||||
# testing "*"
|
||||
unit = Glob.new("/test/*")
|
||||
[
|
||||
"est/anything", false,
|
||||
"/test", false,
|
||||
"/test/anything", true,
|
||||
"/test//as", true,
|
||||
"/test/", false,
|
||||
"/test/as/whatever", true,
|
||||
"test/as", true
|
||||
].each_slice(2) do |test, result|
|
||||
puts("Testing: #{test}")
|
||||
assert_equal(result, unit.match?(test))
|
||||
end
|
||||
unit = Glob.new("/test/*/something")
|
||||
[
|
||||
"/test/s/something", true,
|
||||
"/test//something", false,
|
||||
"/test/something", false,
|
||||
"test/b/something", true,
|
||||
"/test/b/someth", false
|
||||
].each_slice(2) do |test, result|
|
||||
puts("Testing: #{test}")
|
||||
assert_equal(result, unit.match?(test))
|
||||
end
|
||||
# testing "**"
|
||||
unit = Glob.new("/test/**/something")
|
||||
[
|
||||
"/test/s/something", true,
|
||||
"/test/dir/something", true,
|
||||
"/test/something", true,
|
||||
"test/b/something", true,
|
||||
"/test/b/someth", false,
|
||||
"/test/a/b/c/something", true,
|
||||
"/test/a/b/csomething", false,
|
||||
"/testsomething", false,
|
||||
"/something", false
|
||||
].each_slice(2) do |test, result|
|
||||
puts("Testing: #{test}")
|
||||
assert_equal(result, unit.match?(test))
|
||||
end
|
||||
unit = Glob.new("/test/**/*.php")
|
||||
[
|
||||
"/test/archive.php", true,
|
||||
"/test/assets/thing.js", false,
|
||||
"/test/assetsthing.js", false,
|
||||
"/test/parts/thing.php", true,
|
||||
"/test/partsthing.php", true,
|
||||
"/test/.php", true,
|
||||
"/test/parts/extra/test.php", true,
|
||||
"test/archive.php", true,
|
||||
"test/assets/thing.js", false,
|
||||
"test/assetsthing.js", false,
|
||||
"test/parts/thing.php", true,
|
||||
"test/partsthing.php", true,
|
||||
"test/.php", true,
|
||||
"test/parts/extra/test.php", true,
|
||||
"/test/parts/extra/test.php/literally/anything/here", true
|
||||
].each_slice(2) do |test, result|
|
||||
puts("Testing: #{test}")
|
||||
assert_equal(result, unit.match?(test))
|
||||
end
|
||||
# testing ?
|
||||
unit = Glob.new("/test/?hit")
|
||||
[
|
||||
"/test/thing", false,
|
||||
"/test/chit", true,
|
||||
"/test//hit", false,
|
||||
"/testhit", false
|
||||
].each_slice(2) do |test, result|
|
||||
puts("Testing: #{test}")
|
||||
assert_equal(result, unit.match?(test))
|
||||
end
|
||||
# testing char ranges
|
||||
unit = Glob.new("/test/[9-z]+")
|
||||
[
|
||||
"/test/t+", true,
|
||||
"/test/$+", false,
|
||||
"/test/aosidujqwi", false,
|
||||
"/test/9+", true
|
||||
].each_slice(2) do |test, result|
|
||||
puts("Testing: #{test}")
|
||||
assert_equal(result, unit.match?(test))
|
||||
end
|
||||
# testing named captures
|
||||
unit = Glob.new("/test/:name/something")
|
||||
[
|
||||
"/test/something/something", true,
|
||||
"/test/asd/something/extra", true,
|
||||
"/test//something/what", false,
|
||||
"/test/something/what", false,
|
||||
"/test/asd/asd/something/what", false
|
||||
].each_slice(2) do |test, result|
|
||||
puts("Testing: #{test}")
|
||||
assert_equal(result, unit.match?(test))
|
||||
end
|
||||
end
|
||||
|
||||
end
|
|
@ -1,18 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title> Welcome to Hyde </title>
|
||||
</head>
|
||||
<body>
|
||||
<h1> Welcome to <a href="https://adastra7.net/git/Yessiest/hyde">Hyde</a> </h1>
|
||||
<p> Hyde is the horrible side of Jekyll, and, consequently, this particular project.</p>
|
||||
<ul>
|
||||
<li> <a href="/uploads/">Uploads</a> </li>
|
||||
<li> <a href="/about/webrick">WEBrick</a> </li>
|
||||
<li> <a href="/about/hyde">Hyde</a> </li>
|
||||
</ul>
|
||||
<hr />
|
||||
<p> Created by Yessiest </p>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title> test </title>
|
||||
</head>
|
||||
<body>
|
||||
<h1> This is a test </h1>
|
||||
<hr>
|
||||
<address> yes </address>
|
||||
</body>
|
||||
</html>
|
|
@ -1,7 +0,0 @@
|
|||
# Welcome to the INFINITE HYPERNET
|
||||
|
||||
---
|
||||
- THE HYPE IS REAL
|
||||
- SCENE IS DEAD
|
||||
- BLOOD OF LAMERS IS FUEL
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title> very test </title>
|
||||
</head>
|
||||
<body>
|
||||
<h1> This is a very test </h1>
|
||||
<hr>
|
||||
<address> yes 2 </address>
|
||||
</body>
|
||||
</html>
|
|
@ -1 +0,0 @@
|
|||
# YES
|
|
@ -1,15 +0,0 @@
|
|||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN">
|
||||
<HTML>
|
||||
<HEAD><TITLE>File listing</TITLE></HEAD>
|
||||
<BODY>
|
||||
<H1>File listing:</H1>
|
||||
<ul>
|
||||
<li><a href="/uploads/01-blog/test.md">this</a></li>
|
||||
<li><a href="/uploads/02-rules/megafuck.md">that</a></li>
|
||||
</ul>
|
||||
<HR>
|
||||
<ADDRESS>
|
||||
welcum to this crap
|
||||
</ADDRESS>
|
||||
</BODY>
|
||||
</HTML>
|
|
@ -1,32 +0,0 @@
|
|||
require_relative "hyde"
|
||||
server = Hyde::Server.new Port: 8000 do
|
||||
{"add" => -> (a,b) { a + b },
|
||||
"sub" => -> (a,b) { a - b },
|
||||
"mul" => -> (a,b) { a * b },
|
||||
"div" => -> (a,b) {
|
||||
begin
|
||||
return a/b
|
||||
rescue ZeroDivisionError
|
||||
return "Divided by zero"
|
||||
end
|
||||
}
|
||||
}.each_pair do |k,v|
|
||||
serve k do |ctx|
|
||||
req,res = ctx.request,ctx.response
|
||||
a,b = req.query["a"],req.query["b"]
|
||||
result = (a and b) ? v.call(a.to_f,b.to_f) : "Invalid parameters"
|
||||
res.body = "
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title> Calculator API test </title>
|
||||
</head>
|
||||
<body>
|
||||
<h> Result: #{result} </h>
|
||||
</body>
|
||||
</html>"
|
||||
res['Content-Type'] = "text/html"
|
||||
end
|
||||
end
|
||||
end
|
||||
server.start
|
|
@ -1,46 +0,0 @@
|
|||
require_relative 'hyde'
|
||||
|
||||
server = Hyde::Server.new Port: 8000 do
|
||||
root "/home/yessiest/Projects/hyde/test/"
|
||||
serve "index.html"
|
||||
index ["index.html"]
|
||||
path "about" do
|
||||
preprocess do |ctx|
|
||||
puts "#{ctx} entered fully virtual directory!"
|
||||
end
|
||||
postprocess do |ctx|
|
||||
puts "#{ctx} reached endpoint!"
|
||||
end
|
||||
finalize do |ctx|
|
||||
puts "#{ctx} finished processing!"
|
||||
end
|
||||
get "portal" do |ctx|
|
||||
ctx.vars[:ass] = true
|
||||
rewrite "/about/hyde"
|
||||
end
|
||||
get "webrick" do |ctx|
|
||||
ctx.response.body = "WEBrick is a modular http server stack"
|
||||
ctx.response['Content-Type'] = "text/plain"
|
||||
end
|
||||
get "en_passant" do |ctx|
|
||||
puts "holy hell!"
|
||||
redirect "https://google.com/search?q=en+passant"
|
||||
end
|
||||
get "hyde" do |ctx|
|
||||
puts ctx.vars[:ass]
|
||||
ctx.response.body = "Hyde is the disgusting side of Jekyll, and, by extension, the thing that makes WEBrick usable."
|
||||
ctx.response['Content-Type'] = "text/plain"
|
||||
end
|
||||
post "hyde" do |ctx|
|
||||
ctx.response.body = "Your message: #{ctx.request.body}"
|
||||
ctx.response['Content-Type'] = "text/plain"
|
||||
end
|
||||
end
|
||||
path "uploads" do
|
||||
index ["index.html"]
|
||||
serve "**/*.md", safe_regexp: false
|
||||
serve ["*.html","**/*.html"], safe_regexp: false
|
||||
end
|
||||
end
|
||||
|
||||
server.start
|
72
test_hyde.rb
72
test_hyde.rb
|
@ -1,72 +0,0 @@
|
|||
require_relative "hyde"
|
||||
path = Hyde::Pathspec.new "/" do
|
||||
root "/var/www"
|
||||
path "about" do
|
||||
printProbe "test"
|
||||
printProbe "test_*"
|
||||
end
|
||||
path "docs" do
|
||||
remap "/var/www/markdown_compiled/"
|
||||
printProbe "test"
|
||||
printProbe "test_*"
|
||||
probe "speen" do |request|
|
||||
puts "maurice spinning"
|
||||
redirect "https://www.youtube.com/watch?v=KeNyN_rVL_c"
|
||||
pp request
|
||||
end
|
||||
end
|
||||
path "cell_1337" do
|
||||
root "/var/www/cells"
|
||||
path "control" do
|
||||
probe "close" do |request|
|
||||
puts "Permissions level 4 required to control this cell"
|
||||
pp request
|
||||
end
|
||||
probe "open" do |request|
|
||||
puts "Permissions level 4 required to control this cell"
|
||||
pp request
|
||||
end
|
||||
end
|
||||
printProbe "info"
|
||||
end
|
||||
path (/cell_[^\/]*/) do
|
||||
root "/var/www/cells"
|
||||
path "control" do
|
||||
probe "close" do |request|
|
||||
puts "Closing cell #{request.filepath.match /cell_[^\/]*/}"
|
||||
pp request
|
||||
end
|
||||
probe "open" do |request|
|
||||
puts "Opening cell #{request.filepath.match /cell_[^\/]*/}"
|
||||
pp request
|
||||
end
|
||||
end
|
||||
printProbe "dura"
|
||||
printProbe "info"
|
||||
end
|
||||
path "bad_?" do
|
||||
printProbe "path"
|
||||
end
|
||||
probe ["info","hyde"] do
|
||||
puts "this is the most disgusting and visceral thing i've written yet, and i love it"
|
||||
end
|
||||
end
|
||||
|
||||
[
|
||||
Hyde::Request.new("/about/speen",nil,nil),
|
||||
Hyde::Request.new("/about/test",nil,nil),
|
||||
Hyde::Request.new("/about/test_2",nil,nil),
|
||||
Hyde::Request.new("/docs/speen",nil,nil),
|
||||
Hyde::Request.new("/docs/test",nil,nil),
|
||||
Hyde::Request.new("/docs/test_3",nil,nil),
|
||||
Hyde::Request.new("/cell_41/control/open",nil,nil),
|
||||
Hyde::Request.new("/cell_21/control/close",nil,nil),
|
||||
Hyde::Request.new("/cell_19283/info",nil,nil),
|
||||
Hyde::Request.new("/cell_1337/control/close",nil,nil),
|
||||
Hyde::Request.new("/duracell_129/control/open",nil,nil),
|
||||
Hyde::Request.new("/duracell_1447/control/close",nil,nil),
|
||||
Hyde::Request.new("/bad_2path",nil,nil),
|
||||
Hyde::Request.new("/info",nil,nil),
|
||||
Hyde::Request.new("/hyde",nil,nil)
|
||||
].each { |x| path.match(x) }
|
||||
# what a load of fuck this is
|
|
@ -1,14 +0,0 @@
|
|||
require 'webrick'
|
||||
|
||||
server = WEBrick::HTTPServer.new :Port => 8000
|
||||
|
||||
trap 'INT' do server.shutdown end
|
||||
server.mount_proc '/' do |req, res|
|
||||
pp res
|
||||
pp req
|
||||
res['Content-Type'] = "text/plain"
|
||||
res.body = 'A'*65536+"Hello world"
|
||||
end
|
||||
server.start
|
||||
|
||||
|
Loading…
Reference in New Issue