kemalcr / kemal-session Goto Github PK
View Code? Open in Web Editor NEWSimple session handler for Kemal
License: MIT License
Simple session handler for Kemal
License: MIT License
I got this warning while compiling a Kemal app using
crystal build src/gaslogcr.cr -o bin/gaslogcr --release --warnings all --error-on-warnings
In /usr/share/crystal/src/uri/encoding.cr:116:3
116 | def self.unescape(string : String, plus_to_space = false)
^-------
Warning: Deprecated URI.unescape. Use .decode or .decode_www_form instead
Grepping through my lib/ directory, I found lib/kemal-session/src/kemal-session/base.cr line 17 makes a call to URI.unescape. As a quick workaround, I changed .unescape
to .decode
and tested. It compiled with no errors.
As this is just a warning, I'm not real concerned with my code not working yet. Just thought I would get it on record. Listed as a breaking change in Crystal 0.30.0 changelog.
Thanks.
Hi
I've written an engine adaptor for RethinkDB - would you mind adding it to the list of available engines on your README.
The repo is here: https://github.com/kingsleyh/kemal-session-rethinkdb
Also if you see any issues with it please let me know. I followed the same pattern as @crisward used for his kemal-session-mysql adaptor.
Thanks
--Kingsley
If I set multiple storable objects, the object type seems to get mismatched. This seems to happen when the data gets read back from a file, and it parsed with .from_json.
Let me know if you need more info to recreate.
Hi,
I don't know if this is a bug or just an incomplete example issue but I was trying to add UserStorableObject
to my project but following the example in the README gave me Error: undefined method 'name' for UserStorableObject
.
It seems like the example is missing a couple of properties in the UserStorableObject
or did I miss something?
require "json"
require "kemal"
require "kemal-session"
Kemal::Session.config do |config|
config.secret = "some_secret"
end
class UserStorableObject
include JSON::Serializable
include Kemal::Session::StorableObject
# An error is raised without these lines
# property id : Int32
# property name : String
def initialize(@id : Int32, @name : String); end
end
get "/set" do |env|
user = UserStorableObject.new(123, "charlie")
env.session.object("user", user)
end
get "/get" do |env|
user = env.session.object("user").as(UserStorableObject)
"The user stored in session is #{user.name}"
end
Kemal.run
Right now there is some chaos with the garbage collector: You spawn a gc fiber every time a Session object is created. That happens at least once every HTTP request. So after a short time, you will have hundreds of gc fibers running.
Additionally, the memory engine sleeps in its run_gc method on line 50. So now the gc interval is twice as long as the user intends.
My initial solution looked like this [1]:
# @TODO would it be better to not wrap this inside the class?
# What difference does it make?
class Session
spawn do
loop do
Session.config.engine.run_gc
sleep(Session.config.gc_interval.total_seconds)
end
end
end
It is probably not perfect but it makes sure that there is only one instance running independently of how many sessions are active. I also like its simplicity (instead of using a singleton pattern for example).
But maybe someone has a better idea?
I've been getting errors like undefined method 'epoch_ms' for Time
while attempting to use this shard. I have kemal-session at v0.10.0, as it throws an error when I try updating to v0.11.0.
Error resolving kemal-session (0.11.0, 0.10.0)
From what I can tell:
include Kemal::Session::StorableObject
^
Error: StorableObject User needs to define to_json
meant "my earlier JSON.mapping macro wasn't working since upgrading to crystal 1.x anymore" make that work first (can comment out this line to see real failure message).
I noticed new 'empty' session objects get saved. As sessions are typically saved for an hour these can soon mount up, especially from clients which don't retain cookies (ie bots / webcrawlers etc).
I think it'd be a good idea if the various engines or the core library could prevent the saving of empty sessions, thoughts?
The error is:
Warning: Zlib is deprecated, use Compress::Zlib
Showing last frame. Use --error-trace for full trace.
In lib/kemal/src/kemal/helpers/helpers.cr:143:7
143 | Gzip::Writer.open(env.response) do |deflate|
^
Error: undefined constant Gzip::Writer
The fix is to change the Gzip prefix to Compress::Zlib::Writer
Also the Gzip::deflate should be Compress::Deflate::Writer
Before releasing 0.6.0 we need to update documentation for this and all engines.
//cc @neovintage @Thyra
I ran into errors like this involving Time#epoch:
Error in lib/kemal-session/src/kemal-session/engines/memory.cr:52: expanding macro
defne_storage
undefined method 'epoch_ms' for Time
I have managed to fix these errors in my local copy as long as I don't update kemal-session. Since I am not a professional developer (newb), I hesitate to do a fork and pull request.
Refererce Crystal breaking change #6662
Thank you.
Would make cleaning up things a bit easier than out = session.get_string("name"); out.delete_string("name"); out
:)
I couldn't tell. Are the values being signed before being shipped off?
Hello guys
I am very new to crystal and I stucked at something si mple.
I am using Crecto and coding a simple website. All I want to do is store user_id
variable in session.
changeset = Repo.insert(new_user)
if changeset.valid? == false
puts "Changeset is invalid"
env.response.status_code = 500
else
puts "\nThe item was successfully ADDED. ID:#{changeset.instance.id}\n"
#Here-----> env.session.int("user_id", changeset.instance.id)
resp(env, {"message": "Success.", "state": true})
end
Getting this error:
no overload matches 'Kemal::Session#int' with types String, (Int32 | Nil)
Overloads are:
- Kemal::Session#int(k : String, v : Int32)
- Kemal::Session#int(k : String)
Couldn't find overloads for these types:
- Kemal::Session#int(String, Nil)
env.session.int("user_id", changeset.instance.id)
It says changeset.instance.id
is nil but it is not nil. I really wonder the reason.
Thanks in advance.
In lib/kemal-session/src/kemal-session/engines/memory.cr:70:16
70 | @store.delete_if do |id, entry|
^--------
Error: undefined method 'delete_if' for Hash(String, String)
Despite of also using json_mapping
Sdogruyol, thanks for making the 'killer app' of crystal!
The syntax of .int?("blah") below is a bit confusing. Why is the ? necessary?
Generally when I see a question sign, I am expecting a boolean return.
get "/get" do |env|
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
I added kemal-csrf
to our project and as you know, it depends on this shard. This broke our application since it also had a class named Session
. It wasn't quite obvious right away what the problem was when the code didn't compile. Anyway, I was able to fix the issue by renaming our Session
class.
So how about if the Session
class in this library would be either renamed to KemalSession
or Kemal::Session
? It would be quite an easy change to prevent similar issues in the other projects.
Used by kemal-session in MemoryEngine and FileEngine:
kemal-session/src/kemal-session/engines/memory.cr
Lines 11 to 17 in a9d25e5
kemal-session/src/kemal-session/engines/file.cr
Lines 8 to 12 in a9d25e5
As per the release notes, JSON::Serializable should be used instead, or crystal-lang/json_mapping.cr as a temporary fix
I did not see a "delete_int" option for instance, might be nice to be able to add as well as remove things from the sessions :)
Today:
There was a problem expanding macro 'define_storage'
Code in lib/kemal-session/src/kemal-session/engines/memory.cr:52:9
52 | define_storage({
^
Called macro defined in lib/kemal-session/src/kemal-session/engines/memory.cr:7:9
7 | macro define_storage(vars)
Which expanded to:
> 22 |
> 23 | @ints = Hash(String, Int32).new
> 24 | @last_access_at = Time.new.to_unix_ms
^--
Error: no overload matches 'Time.new'
Error Message:
Error in lib/kemal-session/src/kemal-session/engine.cr:81: expanding macro
abstract_engine({
^
in macro 'abstract_engine' lib/kemal-session/src/kemal-session/engine.cr:4, line 75:
.......
> 75. abstract def object(session_id : String, k : String, v : Session::StorableObject::StorableObjectContainer)
.......
abstract `def Session::Engine#object(session_id : String, k : String, v : Session::StorableObject::StorableObjectContainer)` must be implemented by Session::RedisEngine
Yes I am using neovitage/kemal-session-redis
but there are some issues so I use progdo/kemal-session-redis
. Whether I am using mine or neovitage`s, there is this error.
There was a problem expanding macro 'define_storage'
Code in lib/kemal-session/src/kemal-session/engines/memory.cr:52:9
52 | define_storage({
^
Called macro defined in lib/kemal-session/src/kemal-session/engines/memory.cr:7:9
7 | macro define_storage(vars)
Which expanded to:
> 22 |
> 23 | @ints = Hash(String, Int32).new
> 24 | @last_access_at = Time.new.to_unix_ms
^--
Error: no overload matches 'Time.new'
Overloads are:
- Time.new(time : LibC::Timespec, location : Location = Location.local)
- Time.new(pull : JSON::PullParser)
- Time.new(*, seconds : Int64, nanoseconds : Int32, location : Location)
- Time.new(*, unsafe_utc_seconds : Int64)
Crystal: 0.34.0
The project is missing a LICENSE
file, so I was wondering under what license is this project released? I see the related projects being released under MIT, so is this under MIT as well?
For the session cookie, is there a way to specify which domain it belongs to? If not, then I think it could be a good addition.
Session.config do |c|
c.domain = ENV["KEMAL_ENV"] == "development" ? ".lvh.me" : ".mycooldomain.com"
end
I can't figure out how to destroy a session. In other words, when a user logs out, I want to create a brand new session when/if they log back in. How do I do this?
I'm trying to write a redis storage engine and I'm a bit confused by the initialization of the engine in kemal-session.
When configuring a new engine in kemal, you need to initialize it like the example:
Session.config.engine = Session::FileSystemEngine.new({sessions_dir: "/var/foobar/sessions/"})
Does this mean that every request that is received by kemal creates a new instantiation of the FileSystemEngine
?
undefined constant StorableObjects (did you mean 'StorableObject')
expanding macro
in macro 'mapping' /usr/local/Cellar/crystal-lang/0.21.0/src/json/mapping.cr:62, line 86:
...
> 86. @objects : Hash(String, StorableObjects)
full trace here: https://gist.github.com/rdp/56278db479a403b83331f33aebd71167
Dunno if this is a bug per se, but surprised me. After running an env.session.destroy
if I access the session again in that request:
get "/logout" do |env|
env.session.destroy
env.session.string("flash", "successfully logged out")
env.redirect "/login"
end
I get this:
Missing hash key: "9312da9064daec185cc58cb1bf3937f4" (KeyError)
0x82ec567: *Hash(String, String) at /opt/crystal/src/hash.cr 124:9
0x82ec42c: *Hash(String, String) at /opt/crystal/src/hash.cr 61:5
0x83289f8: *Session::MemoryEngine#string:String at /kemal_server/lib/kemal-session/src/kemal-session/engines/memory.cr 136:5
0x83295ef: *Session#string:String at .../kemal-session/src/kemal-session/engine.cr 48:3
(for followers, the work around is to not reuse flash anymore forthat request, it creates a new session on the next request seemingly.
@sdogruyol I think it would be nice to have TravisCI check the code and run specs when reviewing big pull requests like #7 . It would add some additional safety and we can be sure we don't accidentally break something... WDYT?
Well, it looks like this new version of kemal-session doesn't like me. As a workaround to #22, I created a StorableObject and just stuffed something in it (by just having a StorableObject the Union() is no longer empty).
But now I have an issue with session_ids not being found. I followed the trace, looked into the macro cache that the trace pointed to, and found the error is occurring here in the first line of the def string?
Shouldn't this simply be return nil unless @store[session_id]?
def string?(session_id : String, k : String) : String?
return nil if @store[session_id].nil?
storage_instance = StorageInstance.from_json(@store[session_id])
return storage_instance.string?(k)
end
Original error
Missing hash key: "f01e815243295ba4637bff26e4c53d94" (KeyError)
0x564069163713: *Hash(String, String) at /opt/crystal/src/hash.cr 124:9
0x564069163606: *Hash(String, String) at /opt/crystal/src/hash.cr 61:5
0x564069163006: *Session::MemoryEngine#string?<String, String>:(String | Nil) at /home/jason/.cache/crystal/macro122191760.cr 65:25
0x5640691643cd: *Session#string?<String>:(String | Nil) at /home/jason/.cache/crystal/macro121260672.cr 102:9
0x5640690775cf: ~procProc(HTTP::Server::Context, String) at /opt/crystal/src/random.cr 308:3
0x56406915fb26: *Kemal::RouteHandler#process_request<HTTP::Server::Context>:HTTP::Server::Context at /home/jason/crystal/card_game/lib/kemal/src/kemal/route_handler.cr 35:7
0x56406915fa96: *Kemal::RouteHandler#call<HTTP::Server::Context>:HTTP::Server::Context at /home/jason/crystal/card_game/lib/kemal/src/kemal/route_handler.cr 18:7
0x56406917c9d9: *Kemal::WebSocketHandler at /opt/crystal/src/http/server/handler.cr 24:7
0x56406917c3f4: *Kemal::WebSocketHandler#call<HTTP::Server::Context>:(Bool | File::PReader | HTTP::Server::Context | HTTP::Server::Response | HTTP::Server::Response::Output | IO::FileDescriptor+ | Int32 | Nil) at /home/jason/crystal/card_game/lib/kemal/src/kemal/websocket_handler.cr 10:14
0x56406918327a: *Kemal::StaticFileHandler at /opt/crystal/src/http/server/handler.cr 24:7
0x56406918273e: *Kemal::StaticFileHandler#call<HTTP::Server::Context>:(Bool | File::PReader | HTTP::Server::Context | HTTP::Server::Response | HTTP::Server::Response::Output | IO::FileDescriptor+ | Int32 | Nil) at /home/jason/crystal/card_game/lib/kemal/src/kemal/static_file_handler.cr 75:9
0x5640691817d0: *Kemal::CommonExceptionHandler at /opt/crystal/src/http/server/handler.cr 24:7
0x5640691812a0: *Kemal::CommonExceptionHandler#call<HTTP::Server::Context>:(Bool | File::PReader | HTTP::Server::Context | HTTP::Server::Response | HTTP::Server::Response::Output | IO::FileDescriptor+ | Int32 | Nil) at /home/jason/crystal/card_game/lib/kemal/src/kemal/common_exception_handler.cr 9:9
0x564069180836: *Kemal::CommonLogHandler at /opt/crystal/src/http/server/handler.cr 24:7
0x56406917dec1: *Kemal::CommonLogHandler#call<HTTP::Server::Context>:HTTP::Server::Context at /home/jason/crystal/card_game/lib/kemal/src/kemal/common_log_handler.cr 14:35
0x56406915f52b: *Kemal::InitHandler at /opt/crystal/src/http/server/handler.cr 24:7
0x56406915f115: *Kemal::InitHandler#call<HTTP::Server::Context>:(Bool | File::PReader | HTTP::Server::Context | HTTP::Server::Response | HTTP::Server::Response::Output | IO::FileDescriptor+ | Int32 | Nil) at /home/jason/crystal/card_game/lib/kemal/src/kemal/init_handler.cr 11:7
0x5640691c8ff0: *HTTP::Server::RequestProcessor#process<(OpenSSL::SSL::Socket::Server | TCPSocket+), (OpenSSL::SSL::Socket::Server | TCPSocket+), IO::FileDescriptor>:Nil at /opt/crystal/src/http/server/request_processor.cr 39:11
0x5640691c88f9: *HTTP::Server::RequestProcessor#process<(OpenSSL::SSL::Socket::Server | TCPSocket+), (OpenSSL::SSL::Socket::Server | TCPSocket+)>:Nil at /opt/crystal/src/http/server/request_processor.cr 16:3
0x5640691c30b9: *HTTP::Server#handle_client<(TCPSocket+ | Nil)>:Nil at /opt/crystal/src/http/server.cr 174:5
0x564069079033: ~procProc(Nil) at /opt/crystal/src/concurrent.cr 60:3
0x56406908e024: *Fiber#run:(IO::FileDescriptor | Nil) at /opt/crystal/src/fiber.cr 114:3
0x5640690222a6: ~proc2Proc(Fiber, (IO::FileDescriptor | Nil)) at /opt/crystal/src/concurrent.cr 60:3
0x0: ??? at ??
After reading over the code, I can't help thing kemal-session could be significantly simplified by just saving a single object. That is to say, removing all the strings, int32, int64, floats, bools etc.
You could tell users they had to define a storable object. They could then define the structure of the data they want to save. I typically want to save a user, so as long as my user has a to_json method (which is probable if I run an endpoint on it) I can the just nest in as a sub object.
ie
class SessionSchema
def initialize
end
JSON.mapping({
user: {type: User, nilable: true},
basket: {type: Basket, nilable: true},
flash:{type:String, nilable: true},
})
include Session::StorableObject
end
This would remove many of the current macros and possibly make it easier for developers to add other store engines.
What do you think?
I've been looking at the implementation of memory engine and I don't think it's correct. Memory Engine should be serializing and unserializing the storable objects as a more true representation of what other engines (file, redis) should be doing. I didn't want to lose track of this, so putting as an issue for now. I don't know if I'll get the time to fix this soon. Could use some help if others do.
When requiring kemal-session, the following exception is raised
require "kemal-session"
^
in lib/kemal-session/src/kemal-session.cr:2: while requiring "./kemal-session/*"
require "./kemal-session/*"
^
in lib/kemal-session/src/kemal-session/engine.cr:90: instantiating 'Kemal::Session::GC:Class#new()'
GC.new
^~~
in lib/kemal-session/src/kemal-session/gc.cr:6: instantiating 'loop()'
loop do
^~~~
in lib/kemal-session/src/kemal-session/gc.cr:6: instantiating 'loop()'
loop do
^~~~
in lib/kemal-session/src/kemal-session/gc.cr:7: instantiating 'Kemal::Session::Engine+#run_gc()'
Session.config.engine.run_gc
^~~~~~
in lib/kemal-session/src/kemal-session/engines/file.cr:70: instantiating 'Dir:Class#each_child(String)'
Dir.each_child(@sessions_dir) do |f|
^~~~~~~~~~
in /usr/share/crystal/src/dir.cr:180: instantiating 'Dir:Class#open(String)'
Dir.open(dirname) do |dir|
^~~~
in /usr/share/crystal/src/dir.cr:180: instantiating 'Dir:Class#open(String)'
Dir.open(dirname) do |dir|
^~~~
in /usr/share/crystal/src/dir.cr:181: instantiating 'Dir#each_child()'
dir.each_child do |filename|
^~~~~~~~~~
in /usr/share/crystal/src/dir.cr:181: instantiating 'Dir#each_child()'
dir.each_child do |filename|
^~~~~~~~~~
in lib/kemal-session/src/kemal-session/engines/file.cr:70: instantiating 'Dir:Class#each_child(String)'
Dir.each_child(@sessions_dir) do |f|
^~~~~~~~~~
in lib/kemal-session/src/kemal-session/engines/file.cr:73: undefined method 'stat' for File:Class
age = Time.utc_now - File.stat(full_path).mtime # mtime is always saved in utc
It would be good to have the option to expire the session based on last activity. Its seem to currently expire after a fixed period of time.
Hi
I'm using kemal-session-redis
I'm putting a userId string into the session after a successful login via http post. This works great but when I try to check if the userId is in the session from the websocket - I can see the cookie is present - but the session.strings is empty
ws "/ws" do |socket, context|
p context.session.strings #returns {}
p context.response.cookies #correctly returns the cookie
end
any idea how I can access the info stored in a session from the websocket?
any help greatly appreciated
I just did a shards update ('m using the master branch), and an app I'm working on no longer works:
Error in lib/kemal-session/src/kemal-session/storable_object.cr:19: macro didn't expand to a valid program, it expanded to:
================================================================================
--------------------------------------------------------------------------------
1. alias StorableObjects = Union()
2.
--------------------------------------------------------------------------------
Syntax error in expanded macro: finished:1: expecting token 'CONST', not ')'
alias StorableObjects = Union()
^
================================================================================
macro finished
^~~~~~~~
I noticed that #19 was just committed yesterday dealing specifically with StorableObjects.
set the
config.timeout = Time::Span.new(days: 0, hours: 0, minutes: 10, seconds: 0)
then access your web server so the cookie file is created.
Now wait one minute so the File.utime stuff will be triggered (or possibly until a gc fires that cleans it up?)
Delete the file.
Access your web server.
Error setting time to file './sessions/58c9d216c8ba7334f0bd875798feb61f.json': No such file or directory (Errno)
0x102681975: *CallStack::unwind:Array(Pointer(Void)) at ??
0x102681911: *CallStack#initialize:Array(Pointer(Void)) at ??
0x1026818e8: *CallStack::new:CallStack at ??
0x102656181: *raise:NoReturn at ??
0x1026df2f3: *File::utime<Time, Time, String>:Nil at ??
0x1027b06de: *Session::FileEngine#is_in_cache?:Bool at ??
0x1027b07f9: *Session::FileEngine#string?<String, String>:(String | Nil) at ??
0x1027a0eed: *Session#string?:(String | Nil) at ??
0x1026783fa: ~procProc(HTTP::Server::Context, String)@kemal_server.cr:456 at ??
though it does recover a bit after that.
I'm also not sure how I feel about the file stuff being cached at all, since the cache will persist across requests which means if it's in front of a load balancer, with a shared file system, it wouldn't persist across servers. Maybe this should be noted somewhere? Or I think ideally it would at least re-read "in" the file at the beginning of each request...or at least compare mtime with the last known mtime. Or else be made clear that it doesn't work today with shared storage (or perhaps I'm wrong?)
Cheers!
When I run this example, I'm not able to get values back out.
require "kemal"
require "kemal-session"
Kemal::Session.config do |config|
config.secret = "my_super_secret"
end
get "/set" do |env|
number = rand(100)
env.session.int("number", number) # set the value of "number"
"Random number #{number} set."
end
get "/get" do |env|
number = env.session.int?("number") # get the value of "number"
"Value of random number is #{number}."
end
Kemal.run
Then I boot it up and hit the endpoints
$ curl localhost:3000/set
Random number 10 set.
$ curl localhost:3000/get
Value of random number is .
You can see there's no value found when I call get
. Also, if I remove the ?
from the method call, it throws an exception saying the key "number"
isn't there. Maybe I'm missing something?
Timeout is based on mtime of file
https://github.com/kemalcr/kemal-session/blob/master/src/kemal-session/engines/file.cr#L65
However this will only be updated if the session is updated, causing the session to timeout even if the user is active, but doesn't update any session values. Not sure how expensive it is to touch the file each time the middleware is ran, but that could be the only way of keeping this up to date.
If I try to access cookies from the response before env.session
is called, then my cookies are empty.
puts env.response.cookies.inspect # no cookies
env.session
puts env.response.cookies.inspect # lots of cookies
Since this is lazy loaded, I think being able to access cookies from env.session
like env.session.cookies["my_cookie"]
would be helpful. This would allow it to remain lazy loaded.
for instance if you create a "sessions" directory, then you add a ".git_keep_this_dir" in there so that it will be created on a fresh git clone, eventually, when it garbage collects, it removes the ".git_keep_this_dir" file (along with other stale session files), so it is lost out of version control :|
I'm using Postgres as a backend and user_id
in my app is defined as Int64 (BigInt). That doesn't match up with context.session.int()
.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.