aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorPaul Duncan <pabs@pablotron.org>2016-07-15 22:39:28 -0400
committerPaul Duncan <pabs@pablotron.org>2016-07-15 22:39:28 -0400
commita0095ac9a6b3f92b7ab49384683fb5cef53dae67 (patch)
treeab36424e9f282bdf97f5c86e0b2ce49b18dacb1d /src
parent7b3f1b9841f762eebda2bace9b76f66a4da40ab8 (diff)
downloadguff-a0095ac9a6b3f92b7ab49384683fb5cef53dae67.tar.bz2
guff-a0095ac9a6b3f92b7ab49384683fb5cef53dae67.zip
refactor handlers
Diffstat (limited to 'src')
-rw-r--r--src/guff.cr622
-rw-r--r--src/guff/handlers.cr624
2 files changed, 624 insertions, 622 deletions
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/(?<namespace>[\w_-]+)/(?<method>[\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{^/(?<slug>[^/]+)\.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{^/(?<slug>[^/]+)/?$}
-
- 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)?
- /(?<year>\d{4})
- /(?<month>\d{2})
- /(?<day>\d{2})
- /(?<slug>[^/]+)\.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/?)?
- (
- (?<year>\d{4})/
- (
- (?<month>\d{2})/
- ((?<day>\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
diff --git a/src/guff/handlers.cr b/src/guff/handlers.cr
new file mode 100644
index 0000000..8b5d96c
--- /dev/null
+++ b/src/guff/handlers.cr
@@ -0,0 +1,624 @@
+#
+# TODO: refactor into handlers/*.cr eventually
+#
+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::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/(?<namespace>[\w_-]+)/(?<method>[\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{^/(?<slug>[^/]+)\.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{^/(?<slug>[^/]+)/?$}
+
+ 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)?
+ /(?<year>\d{4})
+ /(?<month>\d{2})
+ /(?<day>\d{2})
+ /(?<slug>[^/]+)\.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/?)?
+ (
+ (?<year>\d{4})/
+ (
+ (?<month>\d{2})/
+ ((?<day>\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