From 1988056df3bb929c9e34fe7f6c6e32210abf6da4 Mon Sep 17 00:00:00 2001 From: Paul Duncan Date: Sat, 21 May 2016 05:25:09 -0400 Subject: add initial model stubs, login, logout, etc --- src/guff.cr | 337 +++++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 312 insertions(+), 25 deletions(-) (limited to 'src/guff.cr') 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 -- cgit v1.2.3