From a0095ac9a6b3f92b7ab49384683fb5cef53dae67 Mon Sep 17 00:00:00 2001 From: Paul Duncan Date: Fri, 15 Jul 2016 22:39:28 -0400 Subject: refactor handlers --- src/guff.cr | 622 ------------------------------------------------------------ 1 file changed, 622 deletions(-) (limited to 'src/guff.cr') diff --git a/src/guff.cr b/src/guff.cr index 13b296c..67258ec 100644 --- a/src/guff.cr +++ b/src/guff.cr @@ -12,629 +12,7 @@ end require "./guff/**" -private macro include_api_modules(modules) - {% for mod in modules.resolve %} - include {{ mod.id }} - {% end %} -end - -private macro api_method_dispatch(modules) - case namespace - {% for mod in modules.resolve %} - {% mod_name = mod.resolve.name.gsub(/^.*:(.*)API$/, "\\1").downcase %} - when {{ mod_name.stringify }} - case method - {% for mod_method in mod.resolve.methods %} - {% method_name = mod_method.name.gsub(/^do_([^_]+)_/, "") %} - when {{ method_name.stringify }} - {{ mod_method.name.id }}(params) - {% end %} - else - raise "unknown method: #{namespace}/#{method}" - end - {% end %} - else - raise "unknown namespace: #{namespace}" - end -end - module Guff - module Handlers - abstract class Handler < HTTP::Handler - def initialize(@context : Context) - super() - end - - protected def valid_origin_headers?(headers : HTTP::Headers) - # FIXME: need to compare these against something rather than - # just making sure that they are there - %w{origin referer}.any? do |key| - headers[key]? && headers[key].size > 0 - end - end - - protected def get_site_id(host : String?) : Int64? - @context.models.site.get_id(host) - end - end - - abstract class AuthenticatedHandler < Handler - def initialize(context : Context, @roles : Array(String)) - super(context) - end - - def call(context : HTTP::Server::Context) - if @context.has_role?(@roles) - authenticated_call(context) - else - call_next(context) - end - end - - abstract def authenticated_call(context : HTTP::Server::Context) - end - - class StubHandler < Handler - def call(context : HTTP::Server::Context) - STDERR.puts "%s %s" % [ - context.request.method, - context.request.path.not_nil! - ] - - call_next(context) - end - end - - class SessionHandler < Handler - def call(context : HTTP::Server::Context) - # clear session - @context.session.clear - - if cookie = context.request.cookies[Guff::Session::COOKIE]? - # load session - @context.session.load(cookie.value) - end - - call_next(context) - - if @context.session.valid? - @context.session.save - end - end - end - - class APIHandler < Handler - PATH_RE = %r{^/guff/api/(?[\w_-]+)/(?[\w_-]+)$} - - API_MODULES = [ - APIs::PostAPI, - APIs::UserAPI, - APIs::PageAPI, - APIs::ProjectAPI, - APIs::BlogAPI, - APIs::SiteAPI, - ] - - include_api_modules(API_MODULES) - - def call(context : HTTP::Server::Context) - if context.request.method == "POST" || - (@context.development? && context.request.method == "GET") - if md = PATH_RE.match(context.request.path.not_nil!) - namespace, method = %w{namespace method}.map { |k| md[k] } - - # get query parameteres - params = if (context.request.method == "GET") - context.request.query_params - else - HTTP::Params.parse(context.request.body || "") - end - - code, data = begin - { 200, api_method_dispatch(API_MODULES) } - rescue err - STDERR.puts "ERROR: #{err}" - { 400, { "error": err.to_s } } - end - - # send json response - context.response.status_code = code - context.response.content_type = "application/json; charset=utf-8" - data.to_json(context.response) - else - call_next(context) - end - else - call_next(context) - end - end - end - - class AssetsHandler < Handler - def initialize(context : Context) - super(context) - @etags = {} of String => String - end - - def call(context : HTTP::Server::Context) - req_path = context.request.path.not_nil! - - if matching_request?(context.request.method, req_path) && - valid_origin_headers?(context.request.headers) - # 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.headers["x-frame-options"] = "SAMEORIGIN" - context.response.status_code = 200 - context.response.content_type = AssetMimeType.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| - STDERR.puts "sending #{abs_path}" - IO.copy(fh, context.response) - STDERR.puts "done sending #{abs_path}" - 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/} - - 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.system_dir, - "assets", - 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\.html$} - - def authenticated_call(context : HTTP::Server::Context) - if context.request.path.not_nil!.match(PATH_RE) - context.response.headers["x-frame-options"] = "SAMEORIGIN" - context.response.content_type = "text/html; charset=utf-8" - context.response.status_code = 200 - Views::AdminPageView.new(@context).to_s(context.response) - else - call_next(context) - end - end - end - - class LoginPageHandler < Handler - PATH_RE = %r{^/guff/login\.html$} - VALID_METHODS = %w{GET POST} - - def call(context : HTTP::Server::Context) - if VALID_METHODS.includes?(context.request.method) && - PATH_RE.match(context.request.path.not_nil!) - case context.request.method - when "GET" - reply(context.response) - when "POST" - begin - # check for valid origin or referer header - unless valid_origin_headers?(context.request.headers) - raise "missing origin and referer headers" - end - - # create session - session_id = @context.session.create({ - "user_id" => login(context.request.body).to_s, - }) - - # add cookie - context.response.cookies << HTTP::Cookie.new( - name: Guff::Session::COOKIE, - value: session_id as String, - http_only: true, - - # TODO - # expires: - # secure: - ) - - # redirect to admin panel - context.response.headers["location"] = "/guff/admin.html" - context.response.status_code = 302 - rescue err - # log error - STDERR.puts "login failed: #{err}" - reply(context.response, "invalid login") - end - else - raise "invalid HTTP method" - end - else - call_next(context) - end - end - - private def reply( - response : HTTP::Server::Response, - error : String? = nil - ) - response.headers["x-frame-options"] = "SAMEORIGIN" - response.content_type = "text/html; charset=utf-8" - response.status_code = 200 - Views::LoginPageView.new(@context, error).to_s(response) - end - - private def login(body : String?) - # check body - raise "empty request body" if body.nil? || body.size == 0 - - # parse request parameters - params = HTTP::Params.parse(body.not_nil!) - - # check login parameters - raise "missing login parameters" unless %w{ - username - password - csrf_token - }.all? do |key| - params.has_key?(key) && params[key].size > 0 - end - - # check csrf token - unless @context.models.csrf.use_token(params["csrf_token"]) - raise "invalid csrf token" - end - - # try login - user_id = @context.models.user.login( - params["username"], - params["password"] - ) - - # check user id - raise "invalid credentials" unless user_id - - # return user id - user_id.not_nil! - end - end - - class LogoutPageHandler < Handler - PATH_RE = %r{^/guff/logout\.html$} - - def call(context : HTTP::Server::Context) - if context.request.method == "GET" && - PATH_RE.match(context.request.path.not_nil!) && - valid_origin_headers?(context.request.headers) - # delete session - @context.session.delete - - # clear cookie - context.response.cookies << HTTP::Cookie.new( - name: Guff::Session::COOKIE, - value: "", - expires: Time.epoch(0), - http_only: true, - ) - - # build remaining headers - context.response.headers["x-frame-options"] = "SAMEORIGIN" - context.response.content_type = "text/html; charset=utf-8" - context.response.status_code = 200 - - # draw page - Views::LogoutPageView.new(@context).to_s(context.response) - else - call_next(context) - end - end - end - - class PageHandler < Handler - PATH_RE = %r{^/(?[^/]+)\.html$} - - def call(context : HTTP::Server::Context) - if post_id = get_post_id(context.request) - # TODO: render page - context.response.headers["x-frame-options"] = "SAMEORIGIN" - context.response.content_type = "text/html; charset=utf-8" - context.response.status_code = 200 - context.response << "page: #{post_id}" - else - # unknown page - call_next(context) - end - end - - private def get_post_id(request : HTTP::Request) : Int64? - r = nil - - if request.method == "GET" - if md = PATH_RE.match(request.path.not_nil!) - if site_id = get_site_id(request.headers["host"]?) - r = @context.models.page.get_id( - site_id: site_id, - slug: md["slug"], - ) - end - end - end - - # return result - r - end - end - - class ProjectHandler < Handler - PATH_RE = %r{^/(?[^/]+)/?$} - - def call(context : HTTP::Server::Context) - if post_id = get_post_id(context.request) - path = context.request.path.not_nil! - - if /\/$/.match(path) - # TODO: render page - context.response.headers["x-frame-options"] = "SAMEORIGIN" - context.response.content_type = "text/html; charset=utf-8" - context.response.status_code = 200 - context.response << "project: #{post_id}" - else - # redirect to project - context.response.headers["location"] = path + "/" - context.response.status_code = 302 - end - else - # unknown page - call_next(context) - end - end - - private def get_post_id(request : HTTP::Request) : Int64? - r = nil - - if request.method == "GET" - if md = PATH_RE.match(request.path.not_nil!) - if site_id = get_site_id(request.headers["host"]?) - r = @context.models.project.get_id( - site_id: site_id, - slug: md["slug"], - ) - end - end - end - - # return result - r - end - end - - class BlogPostHandler < Handler - PATH_RE = %r{^ - (/blog)? - /(?\d{4}) - /(?\d{2}) - /(?\d{2}) - /(?[^/]+)\.html - $}x - - def call(context : HTTP::Server::Context) - if id = get_id(context.request) - # TODO: render page - context.response.headers["x-frame-options"] = "SAMEORIGIN" - context.response.content_type = "text/html; charset=utf-8" - context.response.status_code = 200 - context.response << "blog post id: #{id}" - else - # unknown page - call_next(context) - end - end - - private def get_id(request : HTTP::Request) : Int64? - r = nil - - if request.method == "GET" - if md = PATH_RE.match(request.path.not_nil!) - if site_id = get_site_id(request.headers["host"]?) - r = @context.models.blog.get_id( - site_id: site_id, - year: md["year"].to_i, - month: md["month"].to_i, - day: md["day"].to_i, - slug: md["slug"], - ) - end - end - end - - # return result - r - end - end - - class BlogListHandler < Handler - # TODO: make index page configurable - PATH_RE = %r{^/ - (blog/?)? - ( - (?\d{4})/ - ( - (?\d{2})/ - ((?\d{2})/)? - )? - )? - $}x - - def call(context : HTTP::Server::Context) - if ids = get_ids(context.request) - # TODO: render page - context.response.headers["x-frame-options"] = "SAMEORIGIN" - context.response.content_type = "text/html; charset=utf-8" - context.response.status_code = 200 - - Views::Blog::ListView.new(@context, ids).to_s(context.response) - else - # unknown page - call_next(context) - end - end - - private def get_ids(request : HTTP::Request) : Array(Int64)? - r = nil - - if request.method == "GET" - if md = PATH_RE.match(request.path.not_nil!) - if site_id = get_site_id(request.headers["host"]?) - # get request parameters - params = request.query_params - - r = @context.models.blog.get_ids( - site_id: site_id, - year: md["year"]? ? md["year"].to_i : nil, - month: md["month"]? ? md["month"].to_i : nil, - day: md["day"]? ? md["day"].to_i : nil, - page: params["page"]? ? params["page"].to_i : nil, - ) - end - end - end - - # return result - r - end - end - - HANDLERS = [{ - :dev => true, - :id => :stub, - }, { - :dev => true, - :id => :error, - }, { - :dev => false, - :id => :log, - }, { - :dev => false, - :id => :deflate, - }, { - :dev => false, - :id => :assets, - }, { - :dev => false, - :id => :page, - }, { - :dev => false, - :id => :project, - }, { - :dev => false, - :id => :blog_post, - }, { - :dev => false, - :id => :blog_list, - }, { - :dev => false, - :id => :login, - }, { - :dev => false, - :id => :session, - }, { - :dev => false, - :id => :api, - }, { - :dev => false, - :id => :admin, - }, { - :dev => false, - :id => :logout, - }] - - def self.get(context : Context) : Array(HTTP::Handler) - HANDLERS.select { |row| - !(row[:dev] as Bool) || context.development? - }.map { |row| - make_handler(row[:id] as Symbol, context) - } - end - - def self.make_handler( - handler_id : Symbol, - context : Context - ) : HTTP::Handler - case handler_id - when :stub - StubHandler.new(context) - when :error - HTTP::ErrorHandler.new - when :log - HTTP::LogHandler.new - when :deflate - HTTP::DeflateHandler.new - when :session - SessionHandler.new(context) - when :api - APIHandler.new(context) - when :assets - AssetsHandler.new(context) - when :admin - AdminPageHandler.new(context) - when :login - LoginPageHandler.new(context) - when :logout - LogoutPageHandler.new(context) - when :page - PageHandler.new(context) - when :blog_post - BlogPostHandler.new(context) - when :blog_list - BlogListHandler.new(context) - when :project - ProjectHandler.new(context) - else - raise "unknown handler id: #{handler_id}" - end - end - end - module CLI module Actions abstract class Action -- cgit v1.2.3