aboutsummaryrefslogtreecommitdiff
path: root/src/guff.cr
diff options
context:
space:
mode:
Diffstat (limited to 'src/guff.cr')
-rw-r--r--src/guff.cr337
1 files changed, 312 insertions, 25 deletions
diff --git a/src/guff.cr b/src/guff.cr
index 70bbc71..b5c3d44 100644
--- a/src/guff.cr
+++ b/src/guff.cr
@@ -1,8 +1,19 @@
require "option_parser"
require "http/server"
require "ecr/macros"
+require "json"
+require "secure_random"
require "./guff/*"
+private macro define_model_set_getters(hash)
+ {% for name, klass in hash %}
+ def {{ name.id }} : {{ klass.id }}
+ (@cache[{{ name }}] ||= {{ klass.id }}.new(@context)) as {{ klass.id }}
+ end
+ {% end %}
+end
+
+
module Guff
class Config
property :mode, :env, :host, :port, :data_dir, :assets_dir
@@ -157,11 +168,183 @@ module Guff
end
end
+ module Models
+ abstract class Model
+ def initialize(@context : Context)
+ end
+ end
+
+ class UserModel < Model
+ def login(user : String, pass : String) : String?
+ if @context.development?
+ if user == "test" && pass == "test"
+ "0"
+ else
+ nil
+ end
+ else
+ # TODO: handle login
+ nil
+ end
+ end
+
+ def has_role?(user_id : String?, roles : Array(String))
+ raise "empty role list" unless roles.size > 0
+
+ if user_id && user_id.size > 0
+ if @context.development?
+ user_id == "0"
+ else
+ # TODO: add role query
+ end
+ else
+ # empty user id
+ false
+ end
+ end
+ end
+
+ class SessionModel < Model
+ def initialize(context : Context)
+ super(context)
+ @sessions = {} of String => String
+ end
+
+ def load(id : String) : String?
+ @sessions[id]?
+ end
+
+ def save(id : String, data : String)
+ if @sessions.has_key?(id)
+ @sessions[id] = data
+ true
+ else
+ false
+ end
+ end
+
+ def delete(id : String?)
+ @sessions.delete(id) if id
+ false
+ end
+
+ def create(hash : Hash(String, String)) : String
+ # generate id
+ r = SecureRandom.hex(32)
+
+ # save session
+ @sessions[r] = hash.to_json
+
+ # return session id
+ r
+ end
+ end
+ end
+
+ class ModelSet
+ def initialize(@context : Context)
+ @cache = {} of Symbol => Models::Model
+ end
+
+ define_model_set_getters({
+ user: Models::UserModel,
+ session: Models::SessionModel,
+ })
+ end
+
+ class Session < Hash(String, String)
+ getter :session_id
+
+ def initialize(@context : Context)
+ super()
+ @session_id = nil
+ end
+
+ def load(id : String)
+ begin
+ # clear existing session
+ clear
+
+ # load session values
+ JSON.parse(@context.models.session.load(id).not_nil!).each do |key, val|
+ self[key.as_s] = val.as_s
+ end
+
+ # save session id
+ @session_id = id
+
+ # return success
+ true
+ rescue err
+ STDERR.puts "session load failed: #{err}"
+ # invalid session id, return failure
+ false
+ end
+ end
+
+ def create(hash : Hash(String, String)) : String
+ clear
+ merge!(hash)
+ @session_id = @context.models.session.create(hash)
+ end
+
+ def save
+ if valid?
+ @context.models.session.save(@session_id, to_json)
+
+ # return success
+ true
+ else
+ # no session, return failure
+ false
+ end
+ end
+
+ def clear
+ super
+ @session_id = nil
+ end
+
+ def delete : String?
+ r = @session_id
+
+ if valid?
+ @context.models.session.delete(r)
+ clear
+ end
+
+ r
+ end
+
+ def valid?
+ @session_id != nil
+ end
+ end
+
class Context
- property :config #, :models
+ getter :config
def initialize(@config : Config)
- # @models = nil
+ end
+
+ def models
+ @models ||= ModelSet.new(self)
+ end
+
+ def session
+ @session ||= Session.new(self)
+ end
+
+ def user_id
+ session["user_id"]?
+ end
+
+ def has_role?(roles : Array(String))
+ models.user.has_role?(user_id, roles)
+ end
+
+ def development?
+ @is_development ||= (@config.env == "development") as Bool
end
end
@@ -182,7 +365,6 @@ module Guff
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
@@ -197,8 +379,16 @@ module Guff
end
class LoginPageView < View
+ def initialize(context : Context, @error : String? = nil)
+ super(context)
+ end
+
ECR.def_to_s("src/views/login-page.ecr")
end
+
+ class LogoutPageView < View
+ ECR.def_to_s("src/views/logout-page.ecr")
+ end
end
module Handlers
@@ -209,15 +399,12 @@ module Guff
end
abstract class AuthenticatedHandler < Handler
- def initialize(context : Context, roles : Array(String))
+ def initialize(context : Context, @roles : Array(String))
super(context)
- @roles = roles
end
def call(context : HTTP::Server::Context)
- role = context.request.headers["x-guff-role"]?
-
- if role && @roles.includes?(role)
+ if @context.has_role?(@roles)
authenticated_call(context)
else
call_next(context)
@@ -229,17 +416,28 @@ module Guff
class SessionHandler < Guff::Handlers::Handler
def call(context : HTTP::Server::Context)
- if @context.config.env == "development"
- # 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"] = ""
+ # check for forged headers
+ check_headers(context.request.headers)
+
+ # clear session
+ @context.session.clear
+
+ if context.request.cookies.has_key?("guff_sid")
+ # load session
+ @context.session.load(context.request.cookies["guff_sid"].value)
end
call_next(context)
end
+
+ private def check_headers(headers : HTTP::Headers)
+ # FIXME: this isn't needed any more
+ %w{x-guff-user-id x-guff-role}.each do |key|
+ if headers.has_key?(key)
+ raise "forged header: #{key}"
+ end
+ end
+ end
end
# TODO: check referrer, add x-frame-options
@@ -314,12 +512,13 @@ module Guff
end
end
+ # TODO: check referrer, add x-frame-options
class AdminPageHandler < AuthenticatedHandler
def initialize(context : Context)
super(context, %w{admin editor})
end
- PATH_RE = %r{^/guff/admin/$}
+ PATH_RE = %r{^/guff/admin.html$}
def authenticated_call(context : HTTP::Server::Context)
if context.request.path.not_nil!.match(PATH_RE)
@@ -333,19 +532,41 @@ module Guff
end
class LoginPageHandler < Handler
- PATH_RE = %r{^/guff/login/$}
- VALID_METHODS = %w{GET HEAD}
+ 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"
- context.response.content_type = "text/html; charset=utf-8"
- context.response.status_code = 200
- Views::LoginPageView.new(@context).to_s(context.response)
+ reply(context.response)
when "POST"
- # TODO: try login
+ begin
+ # create session
+ session_id = @context.session.create({
+ "user_id": login(context.request.body),
+ })
+
+ # add cookie
+ context.response.cookies << HTTP::Cookie.new(
+ name: "guff_sid",
+ 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
@@ -353,6 +574,69 @@ module Guff
call_next(context)
end
end
+
+ private def reply(
+ response : HTTP::Server::Response,
+ error : String? = nil
+ )
+ 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?) : 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
+ }.all? do |key|
+ params.has_key?(key) && params[key].size > 0
+ 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
+ end
+ end
+
+ class LogoutPageHandler < Handler
+ PATH_RE = %r{^/guff/logout.html$}
+ VALID_METHODS = %w{GET}
+
+ def call(context : HTTP::Server::Context)
+ if context.request.method == "GET" &&
+ PATH_RE.match(context.request.path.not_nil!)
+ # delete session
+ @context.session.delete
+
+ # clear cookie
+ context.response.cookies << HTTP::Cookie.new(
+ name: "guff_sid",
+ value: "",
+ expires: Time.epoch(0),
+ http_only: true,
+ )
+
+ # draw page
+ Views::LogoutPageView.new(@context).to_s(context.response)
+ else
+ call_next(context)
+ end
+ end
end
HANDLERS = [{
@@ -376,13 +660,14 @@ module Guff
}, {
dev: false,
id: :login,
+ }, {
+ dev: false,
+ id: :logout,
}]
def self.get(context : Context) : Array(HTTP::Handler)
- is_dev = (context.config.env == "development")
-
HANDLERS.select { |row|
- !(row[:dev] as Bool) || is_dev
+ !(row[:dev] as Bool) || context.development?
}.map { |row|
make_handler(row[:id] as Symbol, context)
}
@@ -407,6 +692,8 @@ module Guff
AdminPageHandler.new(context)
when :login
LoginPageHandler.new(context)
+ when :logout
+ LogoutPageHandler.new(context)
else
raise "unknown handler id: #{handler_id}"
end