Skip to content
This repository has been archived by the owner on Mar 30, 2020. It is now read-only.

Commit

Permalink
v0.2
Browse files Browse the repository at this point in the history
  • Loading branch information
Thyra committed Apr 23, 2016
2 parents daf3a18 + 8124462 commit dcf064c
Show file tree
Hide file tree
Showing 16 changed files with 372 additions and 131 deletions.
57 changes: 33 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# kemal-session

This project wants to be a session plugin for [Kemal](https://github.com/sdogruyol/kemal) when it grows up. Right now it is still kind of crude and I wouldn't recommend anyone using it... but **it works! ;-)**
This project wants to be a session plugin for [Kemal](https://github.com/sdogruyol/kemal) when it grows up. It is still in alpha stage but it works! ;-)

## Installation

Expand All @@ -24,15 +24,17 @@ require "kemal"
require "kemal-session"
get "/set" do |env|
session = Session.start(env)
session.int("number", rand(100)) # set the value of "number"
env.session.int("number", rand(100)) # set the value of "number"
"Random number set."
end
get "/get" do |env|
session = Session.start(env)
session.int("number") # get the value of "number"
session.int?("hello") # get value or nil, like []?
num = env.session.int("number") # get the value of "number"
env.session.int?("hello") # get value or nil, like []?
"Value of random number is #{num}."
end
Kemal.run
```
The session can save Int32, String, Float64 and Bool values. Use ```session.int```, ```session.string```, ```session.float``` and ```session.bool``` for that.

Expand All @@ -42,36 +44,30 @@ require "kemal"
require "kemal-session"
get "/rand" do |env|
session = Session.start(env)
if session.int? "random_number"
env.response.print "The last random number was #{session.int("random_number")}. "
if env.session.int? "random_number"
env.response.print "The last random number was #{env.session.int("random_number")}. "
else
env.response.print "This is the first random number. "
end
random_number = rand(500)
env.session.int("random_number", random_number)
env.response.print "Setting the random number to #{random_number}"
session.int("random_number", random_number)
end
get "/set" do |env|
session = Session.start(env)
session.string(env.params["key"].to_s, env.params["value"].to_s)
env.session.string(env.params.query["key"].to_s, env.params.query["value"].to_s)
"Setting <i>#{env.params.query["key"]}</i> to <i>#{env.params.query["value"]}</i>"
end
get "/get" do |env|
session = Session.start(env)
if session.string? env.params["key"].to_s
"The value of #{env.params["key"]} is #{session.string(env.params["key"].to_s)}"
if env.session.string? env.params.query["key"].to_s
"The value of #{env.params.query["key"]} is #{env.session.string(env.params.query["key"].to_s)}"
else
"There is no value for this key."
end
end
get "/view" do |env|
session = Session.start(env)
env.response.content_type = "application/json"
session.to_json
end
Kemal.run
```
Open ```/set?key=foo&value=bar``` to set the value of *foo* to *bar* in your session. Then open ```/get?key=foo``` to retrieve it.

Expand All @@ -81,7 +77,7 @@ session.ints.each do |k, v|
puts "#{k} => #{v}"
end
```
**BUT:** This should only be used for reading and analyzing values, **never for changing them**. Because otherwise the session won't automatically save the changes and you will produce really weird bugs...
**BUT:** This should only be used for reading and analyzing values, **never for changing them**. Because otherwise the session won't automatically save the changes and you may produce really weird bugs...

### Configuration

Expand All @@ -101,17 +97,30 @@ Session.config.cookie_name = "foobar"
|---|---|---|
| timeout | How long is the session valid after last user interaction? | ```Time::Span.new(1, 0, 0)``` (1 hour) |
| cookie_name | Name of the cookie that holds the session_id on the client | ```"kemal_sessid"``` |
| engine | How are the sessions saved on the server? (so far only ```filesystem``` is available) | ```"filesystem"``` |
| sessions_dir | For filesystem engine: in which directory are the sessions saved? | ```"./sessions/"``` |
| engine | How are the sessions saved on the server? (see section below) | ```Session::FileSystemEngine.new({sessions_dir: "./sessions/"})``` |
| gc_interval | In which interval should the garbage collector find and delete expired sessions from the server? | ```Time::Span.new(0, 4, 0)``` (4 minutes) |

#### Setting the Engine
The Engine takes care of actually saving the sessions on the server. The standard engine is the FileSystemEngine which creates a json file for each session in a certain folder on the file system. Theoretically there are innumerable possible engines; any way of storing and retrieving values could be used:
* Storing the values in a database (MySQL, SQLite, MongoDB etc.)
* Storing the values in RAM (e.g. like Redis)
* Saving and retreiving the values from a remote server via an API
* Printing on paper, rescanning and running an OCR on it.

The engine you use has a huge impact on performance and can enable you to share sessions between different servers, make them available to any other application or whatever you can imagine. So the choice of engine is very important. Luckily for you, there is only one engine available right now ;-): The FileSystemEngine. It is set by default to store all the session in a folder called sessions in the directory the server is running in. If you want to save them someplace else, just use this:

```crystal
Session.config.engine = Session::FileSystemEngine.new({sessions_dir: "/var/foobar/sessions/"})
```
You can also write your own engine if you like. Take a look at the [wiki page](https://github.com/Thyra/kemal-session/wiki/Creating-your-own-engine). If you think it might also be helpful for others just let me know about it and I will include it in a list of known engines or something.

### Features already implemented
- storing of Int32, String, Float64 and Bool values
- a garbage collector that removes expired sessions from the server
- a filesystem engine (saves sessions on the file system)

### Features in development
- a smart way of automatic saving...
- storing of more data types, including arrays and possibly hashes
- engines for memory (sessions are stored in process memory), mysql and postregsql (sessions are stored in database)
- secure session id against brute force attacks by binding it to ip adress and user agent
- Manage sessions: Session.all, Session.remove(id), Session.get(id)...
2 changes: 2 additions & 0 deletions spec/assets/sessions/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*
!.gitignore
21 changes: 21 additions & 0 deletions spec/base_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
require "./spec_helper"

describe "Session" do
describe ".start" do
it "returns a Session instance" do
typeof(Session.start(create_context("foo"))).should eq Session
end
end

describe ".int" do
it "can save a value" do
session = Session.start(create_context("foo"))
session.int("bar", 12)
end

it "can retrieve a saved value" do
session = Session.start(create_context("foo"))
session.int("bar").should eq 12
end
end
end
23 changes: 23 additions & 0 deletions spec/config_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
require "./spec_helper"

describe "Session" do
describe "::Config" do
describe "::INSTANCE" do
it "returns a Session::Config object" do
typeof(Session::Config::INSTANCE).should eq Session::Config
end
end
end

describe ".config" do
it "returns Session::Config::INSTANCE" do
Session.config.should be Session::Config::INSTANCE
end

it "yields Session::Config::INSTANCE" do
Session.config do |config|
config.should be Session::Config::INSTANCE
end
end
end
end
16 changes: 16 additions & 0 deletions spec/context_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
require "./spec_helper.cr"

describe "Session" do
describe ".id_from_context" do
it "returns a set session id from the context" do
context = create_context("session id")
Session.id_from_context(context).should eq "session id"
end

it "returns nil if there is no session cookie" do
context = create_context("")
Session.id_from_context(context).should be_nil
end
end
end

19 changes: 19 additions & 0 deletions spec/engines/filesystem_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
require "../spec_helper.cr"

describe "Session::FileSystemEngine" do
describe "options" do
describe ":sessions_dir" do
it "raises an ArgumentError if option not passed" do
expect_raises(ArgumentError) do
Session::FileSystemEngine.new({no: "option"})
end
end

it "raises an ArgumentError if the directory does not exist" do
expect_raises(ArgumentError) do
Session::FileSystemEngine.new({sessions_dir: "foobar"})
end
end
end
end
end
20 changes: 17 additions & 3 deletions spec/spec_helper.cr
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
require "spec"
require "kemal"
require "../src/kemal-session"
require "http"

Spec.before_each do
fake_context = HTTP::Server::Context.new(HTTP::Request.new("hello", nil), HTTP::Server::Response.new("ha"))
$session = Session.start(fake_context)
def create_context(session_id : String)
response = HTTP::Server::Response.new(MemoryIO.new)
headers = HTTP::Headers.new

# I would rather pass nil if no cookie should be created
# but that throws an error
unless session_id == ""
cookies = HTTP::Cookies.new
cookies << HTTP::Cookie.new(Session.config.cookie_name, session_id)
cookies.add_request_headers(headers)
end

request = HTTP::Request.new("GET", "/" , headers)
return HTTP::Server::Context.new(request, response)
end

Session.config.engine = Session::FileSystemEngine.new({sessions_dir: "./spec/assets/sessions/"})
1 change: 1 addition & 0 deletions src/kemal-session.cr
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
require "./kemal-session/*"
require "./kemal-session/engines/*"
68 changes: 5 additions & 63 deletions src/kemal-session/base.cr
Original file line number Diff line number Diff line change
Expand Up @@ -3,78 +3,20 @@ require "json"

class Session

# @TODO Is there any way to outsource this to another file?
macro define_storage(vars)
JSON.mapping({
id: String,

{% for name, type in vars %}
{{name.id}}s: Hash(String, {{type}}),
{% end %}
})

{% for name, type in vars %}
@{{name.id}}s = Hash(String, {{type}}).new
getter {{name.id}}s

def {{name.id}}(k : String) : {{type}}
return @{{name.id}}s[k]
end

def {{name.id}}?(k : String) : {{type}}?
return @{{name.id}}s[k]?
end

def {{name.id}}(k : String, v : {{type}})
@{{name.id}}s[k] = v
save
end
{% end %}

end

define_storage({int: Int32, string: String, float: Float64, bool: Bool})
@id : String

def initialize(@id : String)
end

def self.start(context) : Session
instance = uninitialized Session
if(id = id_from_context(context))
instance = restore_instance(id)
else
instance = new(generate_id)
end
# @TODO this is not really optimal, as it has to be checked at each session start
Session.config.set_default_engine unless Session.config.engine_set?

instance = new(id_from_context(context) || generate_id)
instance.update_context(context)
instance.save # @TODO
return instance
end

def self.restore_instance(id : String) : Session
instance = uninitialized Session
case Session.config.engine
when "filesystem"
instance = e_filesystem_restore_instance(id)
else
raise "Session: Unknown engine: #{Session.config.engine}"
end
instance
end

# Unfortunately this finalize is not executed
# when a get block etc finishes. Otherwise it
# would be a nice way of avoiding session.save all the time...
def finalize
save
end

def save
case Session.config.engine
when "filesystem"
e_filesystem_save
end
end

# @TODO make sure the id is unique
def self.generate_id
raw = ""
Expand Down
27 changes: 13 additions & 14 deletions src/kemal-session/config.cr
Original file line number Diff line number Diff line change
@@ -1,34 +1,33 @@
class Session
class Config
INSTANCE = self.new
ENGINES = ["filesystem"]

@timeout : Time::Span
@gc_interval : Time::Span
@cookie_name : String
property timeout, gc_interval, cookie_name
@engine : Engine
property timeout, gc_interval, cookie_name, engine

@engine : String
@sessions_dir : String
getter sessions_dir, engine
@engine_set = false
def engine_set?
@engine_set
end
def engine=(e : Engine)
@engine = e
@engine_set = true
end

def initialize
@timeout = Time::Span.new(1, 0, 0)
@gc_interval = Time::Span.new(0, 4, 0)
@cookie_name = "kemal_sessid"
@engine = "filesystem"
@sessions_dir = "./sessions/"
@engine = DummyEngine.new({s: " "})
end

def sessions_dir=(v : String) : String
# @TODO check if path exists
@sessions_dir = v
def set_default_engine
Session.config.engine = FileSystemEngine.new({sessions_dir: "./sessions/"})
end

def engine=(v : String) : String
raise ArgumentError, "Session: Unknown engine #{v}" unless ENGINES.includes? v
@engine = v
end
end # Config

def self.config
Expand Down
23 changes: 0 additions & 23 deletions src/kemal-session/e_filesystem.cr

This file was deleted.

Loading

0 comments on commit dcf064c

Please sign in to comment.