aboutsummaryrefslogtreecommitdiff
path: root/src/guff.cr
diff options
context:
space:
mode:
Diffstat (limited to 'src/guff.cr')
-rw-r--r--src/guff.cr622
1 files changed, 0 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