From f15a5162aad322d7ff787992df697d6d968a9cca Mon Sep 17 00:00:00 2001 From: Paul Duncan Date: Mon, 16 May 2016 02:35:31 -0400 Subject: add bunch of crap --- src/guff.cr | 223 +++++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 184 insertions(+), 39 deletions(-) (limited to 'src/guff.cr') 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 => "", + :style => "", + } + + 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] " 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 -- cgit v1.2.3