aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/guff.cr223
-rw-r--r--src/views/admin-page.ecr31
2 files changed, 215 insertions, 39 deletions
diff --git a/src/guff.cr b/src/guff.cr
index 944612a..7d846be 100644
--- a/src/guff.cr
+++ b/src/guff.cr
@@ -1,5 +1,6 @@
require "option_parser"
require "http/server"
+require "ecr/macros"
require "./guff/*"
module Guff
@@ -135,20 +136,24 @@ module Guff
end
end
- abstract class ModeRunner
- def self.run(config : Config)
- new(config).run
- end
-
- def initialize(@config : Config)
- end
-
- abstract def run
- end
+ module MimeType
+ TYPES = {
+ ".js": "text/javascript; charset=utf-8",
+ ".css": "text/css; charset=utf-8",
+ ".html": "text/html; charset=utf-8",
+ ".png": "image/png",
+ ".jpeg": "image/jpeg",
+ ".jpg": "image/jpeg",
+ ".otf": "application/vnd.ms-opentype",
+ ".eot": "application/vnd.ms-fontobject",
+ ".svg": "image/svg+xml",
+ ".ttf": "application/x-font-ttf",
+ ".woff": "application/font-woff",
+ ".woff2": "application/font-woff",
+ }
- class Builder < ModeRunner
- def run
- STDERR.puts "TODO: building directory"
+ def self.from_path(path : String) : String
+ TYPES[File.extname(path)]? || "application/octet-stream"
end
end
@@ -160,6 +165,38 @@ module Guff
end
end
+ module Views
+ abstract class View
+ def initialize(@context : Context)
+ end
+
+ def h(s : String) : String
+ HTML.escape(s)
+ end
+
+ TEMPLATES = {
+ :script => "<script type='text/javascript' src='%s'></script>",
+ :style => "<link rel='stylesheet' type='text/css' href='%s'/>",
+ }
+
+ private def assets(key : Symbol, paths : Array(String))
+ paths.map { |path| TEMPLATES[key] % [h(path)] }.join
+ end
+
+ def scripts(paths : Array(String))
+ assets(:script, paths)
+ end
+
+ def styles(paths : Array(String))
+ assets(:style, paths)
+ end
+ end
+
+ class AdminPageView < View
+ ECR.def_to_s("src/views/admin-page.ecr")
+ end
+ end
+
module Handlers
abstract class Handler < HTTP::Handler
def initialize(@context : Context)
@@ -192,6 +229,9 @@ module Guff
# TODO: add handler support
context.request.headers["x-guff-user-id"] = "0"
context.request.headers["x-guff-role"] = "admin"
+ else
+ context.request.headers["x-guff-user-id"] = ""
+ context.request.headers["x-guff-role"] = ""
end
call_next(context)
@@ -199,13 +239,88 @@ module Guff
end
class AssetsHandler < AuthenticatedHandler
- PATH_RE = %r{^/_guff/assets/}
+ def initialize(context : Context)
+ super(context, %w{admin editor})
+ @etags = {} of String => String
+ end
def authenticated_call(context : HTTP::Server::Context)
- path = context.request.path.not_nil!
+ req_path = context.request.path.not_nil!
+
+ if matching_request?(context.request.method, req_path)
+ # get expanded path to file
+ if abs_path = expand_path(req_path)
+ # get file digest
+ etag = get_file_etag(abs_path)
+
+ # check for cache header
+ if context.request.headers["if-none-match"]? == etag
+ # cached, send 304 not modified
+ context.response.status_code = 304
+ else
+ # not cached, set code and send headers
+ context.response.status_code = 200
+ context.response.content_type = MimeType.from_path(abs_path)
+ context.response.content_length = File.size(abs_path)
+ context.response.headers["etag"] = etag
+
+ if context.request.method == "GET"
+ # send body for GET requests
+ File.open(abs_path) do |fh|
+ IO.copy(fh, context.response)
+ end
+ end
+ end
+ else
+ # expanded path does not exist
+ call_next(context)
+ end
+ else
+ # not a matching request
+ call_next(context)
+ end
+ end
+
+ VALID_METHODS = %w{GET HEAD}
+ PATH_RE = %r{^/guff/assets/}
- if path.match(PATH_RE)
- # TODO
+ private def matching_request?(method, path)
+ VALID_METHODS.includes?(method) && PATH_RE.match(path)
+ end
+
+ private def expand_path(req_path : String) : String?
+ # unescape path, check for nil byte
+ path = URI.unescape(req_path)
+ return nil if path.includes?('\0')
+
+ # build absolute path
+ r = File.join(
+ @context.config.assets_dir,
+ File.expand_path(path.gsub(PATH_RE, ""), "/")
+ )
+
+ # return path if file exists, or nil otherwise
+ File.file?(r) ? r : nil
+ end
+
+ private def get_file_etag(path : String) : String
+ # FIXME: rather than a hash this should be an HMAC
+ @etags[path] ||= OpenSSL::Digest.new("SHA1").file(path).hexdigest
+ end
+ end
+
+ class AdminPageHandler < AuthenticatedHandler
+ def initialize(context : Context)
+ super(context, %w{admin editor})
+ end
+
+ PATH_RE = %r{^/guff/admin/$}
+
+ def authenticated_call(context : HTTP::Server::Context)
+ if context.request.path.not_nil!.match(PATH_RE)
+ context.response.content_type = "text/html; charset=utf-8"
+ context.response.status_code = 200
+ Views::AdminPageView.new(@context).to_s(context.response.output)
else
call_next(context)
end
@@ -227,6 +342,9 @@ module Guff
}, {
dev: false,
id: :assets,
+ }, {
+ dev: false,
+ id: :admin,
}]
def self.get(context : Context) : Array(HTTP::Handler)
@@ -253,38 +371,65 @@ module Guff
when :session
SessionHandler.new(context)
when :assets
- AssetsHandler.new(context, %w{admin editor})
+ AssetsHandler.new(context)
+ when :admin
+ AdminPageHandler.new(context)
else
raise "unknown handler id: #{handler_id}"
end
end
end
- class Server < ModeRunner
- def run
- STDERR.puts "TODO: running web server"
- check_dirs
+ module CLI
+ module Actions
+ abstract class Action
+ def self.run(config : Config)
+ new(config).run
+ end
- # create context
- context = Context.new(@config)
+ def initialize(@config : Config)
+ end
- STDERR.puts "listening on %s:%s" % [@config.host, @config.port]
+ abstract def run
+ end
- # run server
- HTTP::Server.new(
- @config.host,
- @config.port.to_i,
- Handlers.get(context)
- ).listen
- end
+ class InitAction < Action
+ def run
+ STDERR.puts "TODO: building directory"
+ end
+ end
+
+ class RunAction < Action
+ def run
+ STDERR.puts "TODO: running web server"
+ check_dirs
+
+ # create context
+ context = Context.new(@config)
- private def check_dirs
- raise "missing assets directory: \"#{@config.assets_dir}\"" unless Dir.exists?(@config.assets_dir)
- raise "missing data directory: \"#{@config.data_dir}\"" unless Dir.exists?(@config.data_dir)
+ STDERR.puts "listening on %s:%s" % [@config.host, @config.port]
+
+ # run server
+ HTTP::Server.new(
+ @config.host,
+ @config.port.to_i,
+ Handlers.get(context)
+ ).listen
+ end
+
+ private def check_dirs
+ {
+ "assets": @config.assets_dir,
+ "data": @config.data_dir,
+ }.each do |name, dir|
+ unless Dir.exists?(dir)
+ raise "missing #{name} directory: \"#{dir}\""
+ end
+ end
+ end
+ end
end
- end
- module CLI
def self.print_usage(app : String)
STDERR.puts "Usage: #{app} [mode] <args>"
end
@@ -299,9 +444,9 @@ module Guff
case config.mode
when "init"
- Builder.run(config)
+ Actions::InitAction.run(config)
when "run"
- Server.run(config)
+ Actions::RunAction.run(config)
when "help"
# do nothing
else
diff --git a/src/views/admin-page.ecr b/src/views/admin-page.ecr
new file mode 100644
index 0000000..284b1a7
--- /dev/null
+++ b/src/views/admin-page.ecr
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html lang='en-US'>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Guff Admin</title>
+
+ <%=
+ styles %w{
+ ../assets/font-awesome-4.5.0/css/font-awesome.min.css
+ ../assets/bootstrap-3.3.6/css/bootstrap.min.css
+ ../assets/bootstrap-3.3.6/css/bootstrap-theme.min.css
+ }
+ %>
+ </head>
+
+ <body>
+ </body>
+
+ <%=
+ scripts %w{
+ ../assets/jquery-2.2.1.min.js
+ ../assets/luigi-template-0.4.1.min.js
+ ../assets/js/util.js
+ ../assets/bootstrap-3.3.6/js/bootstrap.min.js
+ ../assets/ckeditor-4.5.8-custom/ckeditor.js
+ ../assets/js/search-field.js
+ ../assets/test/tab-users.js
+ ../assets/test/tab-posts.js
+ }
+ %>
+</html>