firebug 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.circleci/config.yml +120 -0
- data/.circleci/images/primary/Dockerfile +3 -0
- data/.editorconfig +15 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.rubocop.yml +38 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +75 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +127 -0
- data/LICENSE.txt +21 -0
- data/README.md +91 -0
- data/Rakefile +13 -0
- data/bin/bundle +105 -0
- data/bin/console +11 -0
- data/bin/rake +20 -0
- data/bin/setup +7 -0
- data/firebug.gemspec +38 -0
- data/lib/action_dispatch/session/code_igniter_store.rb +92 -0
- data/lib/firebug/configuration.rb +21 -0
- data/lib/firebug/crypto.rb +60 -0
- data/lib/firebug/serializer.rb +33 -0
- data/lib/firebug/session.rb +32 -0
- data/lib/firebug/unserializer.rb +114 -0
- data/lib/firebug/version.rb +5 -0
- data/lib/firebug.rb +75 -0
- metadata +239 -0
data/bin/setup
ADDED
data/firebug.gemspec
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'firebug/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec| # rubocop:disable BlockLength
|
8
|
+
spec.name = 'firebug'
|
9
|
+
spec.version = Firebug::VERSION
|
10
|
+
spec.authors = ['Aaron Frase']
|
11
|
+
spec.email = ['aaron@rvshare.com']
|
12
|
+
|
13
|
+
spec.summary = 'Gem for working with CodeIgniter sessions'
|
14
|
+
spec.description = 'Gem for working with CodeIgniter sessions'
|
15
|
+
spec.homepage = 'https://github.com/rvshare/firebug'
|
16
|
+
spec.license = 'MIT'
|
17
|
+
|
18
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
19
|
+
f.match(%r{^(test|spec|features)/})
|
20
|
+
end
|
21
|
+
spec.bindir = 'exe'
|
22
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
|
+
spec.require_paths = ['lib']
|
24
|
+
|
25
|
+
spec.add_dependency 'actionpack', '~> 5.0'
|
26
|
+
spec.add_dependency 'activerecord', '~> 5.0'
|
27
|
+
spec.add_dependency 'ruby-mcrypt', '~> 0.2'
|
28
|
+
|
29
|
+
spec.add_development_dependency 'bundler'
|
30
|
+
spec.add_development_dependency 'pry'
|
31
|
+
spec.add_development_dependency 'rake'
|
32
|
+
spec.add_development_dependency 'rspec'
|
33
|
+
spec.add_development_dependency 'rspec_junit_formatter'
|
34
|
+
spec.add_development_dependency 'rubocop'
|
35
|
+
spec.add_development_dependency 'rubocop-rspec'
|
36
|
+
spec.add_development_dependency 'simplecov'
|
37
|
+
spec.add_development_dependency 'sqlite3'
|
38
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'action_dispatch'
|
4
|
+
require_relative '../../firebug/session'
|
5
|
+
|
6
|
+
module ActionDispatch
|
7
|
+
module Session
|
8
|
+
class CodeIgniterStore < AbstractStore
|
9
|
+
def initialize(app, options={})
|
10
|
+
super(app, { key: 'default_pyrocms' }.merge(options))
|
11
|
+
end
|
12
|
+
|
13
|
+
# Finds an existing session or creates a new one.
|
14
|
+
#
|
15
|
+
# @param [ActionDispatch::Request] req
|
16
|
+
# @param [String] sid
|
17
|
+
# @return [Array<String, Object>]
|
18
|
+
def find_session(req, sid)
|
19
|
+
model = find_session_model(req, sid)
|
20
|
+
# +Rack::Session::Abstract::Persisted#load_session+ expects this to return an Array with the first value being
|
21
|
+
# the session ID and the second the actual session data.
|
22
|
+
[model.session_id, model.user_data]
|
23
|
+
end
|
24
|
+
|
25
|
+
# Writes the session information to the database.
|
26
|
+
#
|
27
|
+
# @param [ActionDispatch::Request] req
|
28
|
+
# @param [String] sid
|
29
|
+
# @param [Hash] session
|
30
|
+
# @param [Hash] _options
|
31
|
+
# @return [String] encrypted and base64 encoded string of the session data.
|
32
|
+
def write_session(req, sid, session, _options)
|
33
|
+
model = find_session_model(req, sid)
|
34
|
+
model_params = {
|
35
|
+
session_id: model.session_id,
|
36
|
+
user_agent: req.user_agent || '', # user_agent can't be null
|
37
|
+
ip_address: req.remote_ip || '', # ip_address can't be null
|
38
|
+
user_data: session
|
39
|
+
}
|
40
|
+
# Returning false will cause Rack to output a warning.
|
41
|
+
return false unless model.update(model_params)
|
42
|
+
# Return the encrypted cookie format of the data. Rack sets this value as the cookie in the response
|
43
|
+
model.cookie_data
|
44
|
+
end
|
45
|
+
|
46
|
+
# Deletes then creates a new session in the database.
|
47
|
+
#
|
48
|
+
# @param [ActionDispatch::Request] req
|
49
|
+
# @param [String] sid
|
50
|
+
# @param [Hash] _options
|
51
|
+
# @return [String] the new session id
|
52
|
+
def delete_session(req, sid, _options)
|
53
|
+
# Get the current database record for this session then delete it.
|
54
|
+
find_session_model(req, sid).delete
|
55
|
+
# Generate a new one and return it's ID
|
56
|
+
find_session_model(req).session_id
|
57
|
+
end
|
58
|
+
|
59
|
+
# Tries to find the session ID in the requests cookies.
|
60
|
+
#
|
61
|
+
# @param [ActionDispatch::Request] req
|
62
|
+
# @return [String, nil]
|
63
|
+
def extract_session_id(req)
|
64
|
+
sid = req.cookies[@key]
|
65
|
+
# returning `nil` just causes a new ID to be generated.
|
66
|
+
return if sid.nil?
|
67
|
+
# sometimes the cookie contains just the session ID.
|
68
|
+
return sid if sid.size <= 32
|
69
|
+
Firebug.decrypt_cookie(sid)[:session_id]
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
# @param [ActionDispatch::Request] req
|
75
|
+
# @param [String] sid
|
76
|
+
# @return [Firebug::Session]
|
77
|
+
def find_session_model(req, sid=nil)
|
78
|
+
if sid
|
79
|
+
model = Firebug::Session.find_by(session_id: sid)
|
80
|
+
return model if model
|
81
|
+
end
|
82
|
+
|
83
|
+
Firebug::Session.create!(
|
84
|
+
session_id: sid || generate_sid,
|
85
|
+
last_activity: Time.current.to_i,
|
86
|
+
user_agent: req.user_agent,
|
87
|
+
ip_address: req.remote_ip
|
88
|
+
)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Firebug
|
4
|
+
# A configuration object.
|
5
|
+
#
|
6
|
+
# @attr [String] key the encryption key used to encrypt and decrypt cookies.
|
7
|
+
# @attr [String] table_name the name of the sessions table.
|
8
|
+
class Configuration
|
9
|
+
attr_reader :table_name
|
10
|
+
|
11
|
+
attr_accessor :key
|
12
|
+
|
13
|
+
# Sets the table name for +Firebug::Session+
|
14
|
+
#
|
15
|
+
# @param [String] value
|
16
|
+
def table_name=(value)
|
17
|
+
Firebug::Session.table_name = value
|
18
|
+
@table_name = value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Firebug
|
4
|
+
require 'digest'
|
5
|
+
require 'securerandom'
|
6
|
+
require 'mcrypt'
|
7
|
+
|
8
|
+
class Crypto
|
9
|
+
# @param [String] key
|
10
|
+
def initialize(key)
|
11
|
+
@key = Digest::MD5.hexdigest(key)
|
12
|
+
end
|
13
|
+
|
14
|
+
# @param [String] data
|
15
|
+
# @return [String]
|
16
|
+
def encrypt(data)
|
17
|
+
# Create a random 32 byte string to act as the initialization vector.
|
18
|
+
iv = SecureRandom.random_bytes(32)
|
19
|
+
# CodeIgniter pads the data with zeros
|
20
|
+
cipher = Mcrypt.new(:rijndael_256, :cbc, @key, iv, :zeros)
|
21
|
+
add_noise(iv + cipher.encrypt(data))
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param [String] data
|
25
|
+
# @return [String]
|
26
|
+
def decrypt(data)
|
27
|
+
data = remove_noise(data)
|
28
|
+
# The first 32 bytes of the data is the original IV
|
29
|
+
iv = data[0..31]
|
30
|
+
cipher = Mcrypt.new(:rijndael_256, :cbc, @key, iv, :zeros)
|
31
|
+
cipher.decrypt(data[32..-1])
|
32
|
+
end
|
33
|
+
|
34
|
+
# CodeIgniter adds "noise" to the results of the encryption by adding the ordinal value of each character with a
|
35
|
+
# value in the key. The plaintext key is hashed with MD5 then SHA1.
|
36
|
+
#
|
37
|
+
# @param [String] data
|
38
|
+
def add_noise(data)
|
39
|
+
noise(data, :+)
|
40
|
+
end
|
41
|
+
|
42
|
+
# The "noise" is removed by subtracting the ordinals.
|
43
|
+
#
|
44
|
+
# @param [String] data
|
45
|
+
# @return [String]
|
46
|
+
def remove_noise(data)
|
47
|
+
noise(data, :-)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# @param [String] data
|
53
|
+
# @param [Symbol] operator
|
54
|
+
# @return [String]
|
55
|
+
def noise(data, operator)
|
56
|
+
key = Digest::SHA1.hexdigest(@key)
|
57
|
+
Array.new(data.size) { |i| (data[i].ord.send(operator, key[i % key.size].ord) % 256).chr }.join
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Firebug
|
4
|
+
class Serializer
|
5
|
+
# Convert a ruby object into a PHP serialized string.
|
6
|
+
#
|
7
|
+
# @param [Object] obj
|
8
|
+
# @raise [ArgumentError] for unsupported types
|
9
|
+
# @return [String]
|
10
|
+
def self.parse(obj) # rubocop:disable AbcSize,CyclomaticComplexity
|
11
|
+
case obj
|
12
|
+
when NilClass
|
13
|
+
'N;'
|
14
|
+
when TrueClass
|
15
|
+
'b:1;'
|
16
|
+
when FalseClass
|
17
|
+
'b:0;'
|
18
|
+
when Integer
|
19
|
+
"i:#{obj};"
|
20
|
+
when Float
|
21
|
+
"d:#{obj};"
|
22
|
+
when String, Symbol
|
23
|
+
"s:#{obj.to_s.bytesize}:\"#{obj}\";"
|
24
|
+
when Array
|
25
|
+
"a:#{obj.length}:{#{obj.collect.with_index { |e, i| "#{parse(i)}#{parse(e)}" }.join}}"
|
26
|
+
when Hash
|
27
|
+
"a:#{obj.length}:{#{obj.collect { |k, v| "#{parse(k)}#{parse(v)}" }.join}}"
|
28
|
+
else
|
29
|
+
raise ArgumentError, "unsupported type #{obj.class.name}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Firebug
|
4
|
+
require 'active_record'
|
5
|
+
|
6
|
+
# An ActiveRecord model of the CodeIgniter sessions table.
|
7
|
+
class Session < ActiveRecord::Base
|
8
|
+
self.table_name = 'default_ci_sessions'
|
9
|
+
|
10
|
+
# @return [Object]
|
11
|
+
def user_data
|
12
|
+
Firebug.unserialize(super || '')
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param [Object] value
|
16
|
+
def user_data=(value)
|
17
|
+
super(Firebug.serialize(value))
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [String]
|
21
|
+
def cookie_data
|
22
|
+
data = { session_id: session_id, ip_address: ip_address, user_agent: user_agent, last_activity: last_activity }
|
23
|
+
Firebug.encrypt_cookie(data)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def timestamp_attributes_for_update
|
29
|
+
['last_activity']
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Firebug
|
4
|
+
require 'strscan'
|
5
|
+
|
6
|
+
# This class will unserialize a PHP serialized string into a ruby object.
|
7
|
+
#
|
8
|
+
# @note Hashes will be returned with symbolized keys.
|
9
|
+
#
|
10
|
+
# @attr [StringScanner] str
|
11
|
+
class Unserializer
|
12
|
+
attr_accessor :str
|
13
|
+
|
14
|
+
# @param [String] string
|
15
|
+
def initialize(string)
|
16
|
+
self.str = StringScanner.new(string)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Convenience method for unserializing a PHP serialized string.
|
20
|
+
#
|
21
|
+
# @param [String] value
|
22
|
+
# @return [Object]
|
23
|
+
def self.parse(value)
|
24
|
+
new(value).parse
|
25
|
+
end
|
26
|
+
|
27
|
+
# @raise [ParserError]
|
28
|
+
def parse # rubocop:disable AbcSize,CyclomaticComplexity
|
29
|
+
ch = str.getch
|
30
|
+
return if ch.nil?
|
31
|
+
|
32
|
+
case ch
|
33
|
+
when 'a'
|
34
|
+
parse_enumerable.tap { expect('}') }
|
35
|
+
when 's'
|
36
|
+
parse_string.tap { expect(';') }
|
37
|
+
when 'i'
|
38
|
+
parse_int.tap { expect(';') }
|
39
|
+
when 'd'
|
40
|
+
parse_double.tap { expect(';') }
|
41
|
+
when 'b'
|
42
|
+
parse_bool.tap { expect(';') }
|
43
|
+
when 'N'
|
44
|
+
expect(';')
|
45
|
+
else
|
46
|
+
raise ParserError, "Unknown token '#{ch}' at position #{str.pos} (#{str.string})"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# @raise [ParseError]
|
53
|
+
# @return [Hash, Array]
|
54
|
+
def parse_enumerable # rubocop:disable AbcSize
|
55
|
+
size = parse_int
|
56
|
+
expect('{')
|
57
|
+
return [] if size.zero?
|
58
|
+
if str.peek(1) == 'i'
|
59
|
+
# Multiply the size by 2 since the array index isn't counted in the size.
|
60
|
+
# Odd number element will be the index value so drop it.
|
61
|
+
Array.new(size * 2) { parse }.select.with_index { |_, i| i.odd? }
|
62
|
+
else
|
63
|
+
Array.new(size) { [parse.to_sym, parse] }.to_h
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [String]
|
68
|
+
def parse_string
|
69
|
+
size = parse_int
|
70
|
+
str.getch # consume quote '"'
|
71
|
+
read(size).tap { str.getch }
|
72
|
+
end
|
73
|
+
|
74
|
+
# @raise [ParserError]
|
75
|
+
# @return [Integer]
|
76
|
+
def parse_int
|
77
|
+
str.scan(/:(\d+):?/)
|
78
|
+
raise ParserError, "Failed to parse integer at position #{str.pos}" unless str.matched?
|
79
|
+
str[1].to_i
|
80
|
+
end
|
81
|
+
|
82
|
+
# @raise [ParserError]
|
83
|
+
# @return [Float]
|
84
|
+
def parse_double
|
85
|
+
str.scan(/:(\d+(?:\.\d+)?)/)
|
86
|
+
raise ParserError, "Failed to parse double at position #{str.pos}" unless str.matched?
|
87
|
+
str[1].to_f
|
88
|
+
end
|
89
|
+
|
90
|
+
# @raise [ParserError]
|
91
|
+
# @return [Boolean]
|
92
|
+
def parse_bool
|
93
|
+
str.scan(/:([01])/)
|
94
|
+
raise ParserError, "Failed to parse boolean at position #{str.pos}" unless str.matched?
|
95
|
+
str[1] == '1'
|
96
|
+
end
|
97
|
+
|
98
|
+
# @param [Integer] size
|
99
|
+
# @return [String]
|
100
|
+
def read(size)
|
101
|
+
Array.new(size) { str.get_byte }.join
|
102
|
+
end
|
103
|
+
|
104
|
+
# @param [String] s
|
105
|
+
# @raise [ParserError] if the next character is not `s`
|
106
|
+
def expect(s)
|
107
|
+
char = str.getch
|
108
|
+
raise ParserError, "expected '#{s}' but got '#{char}' at position #{str.pos}" unless char == s
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
class ParserError < StandardError
|
113
|
+
end
|
114
|
+
end
|
data/lib/firebug.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'firebug/version'
|
4
|
+
require_relative 'firebug/crypto'
|
5
|
+
require_relative 'firebug/serializer'
|
6
|
+
require_relative 'firebug/unserializer'
|
7
|
+
require_relative 'firebug/configuration'
|
8
|
+
require_relative 'action_dispatch/session/code_igniter_store'
|
9
|
+
|
10
|
+
module Firebug
|
11
|
+
class << self
|
12
|
+
attr_writer :configuration
|
13
|
+
end
|
14
|
+
|
15
|
+
# @return [Firebug::Configuration]
|
16
|
+
def self.configuration
|
17
|
+
@configuration ||= Configuration.new
|
18
|
+
end
|
19
|
+
|
20
|
+
# @yieldparam [Firebug::Configuration] config
|
21
|
+
def self.configure
|
22
|
+
yield configuration
|
23
|
+
end
|
24
|
+
|
25
|
+
# Serialize a ruby object into a PHP serialized string.
|
26
|
+
#
|
27
|
+
# @param [Object] value
|
28
|
+
# @return [String]
|
29
|
+
def self.serialize(value)
|
30
|
+
Serializer.parse(value)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Unserialize a PHP serialized string into a ruby object.
|
34
|
+
#
|
35
|
+
# @param [String] value
|
36
|
+
# @return [Object]
|
37
|
+
def self.unserialize(value)
|
38
|
+
Unserializer.parse(value)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Encrypt data the way CodeIgniter does.
|
42
|
+
#
|
43
|
+
# @param [Object] data
|
44
|
+
# @param [String] key if `nil` use +Firebug::Configuration.key+
|
45
|
+
def self.encrypt(data, key=nil)
|
46
|
+
key = configuration.key if key.nil?
|
47
|
+
Crypto.new(key).encrypt(data)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Decrypt data encrypted using CodeIgniters encryption.
|
51
|
+
#
|
52
|
+
# @param [Object] data
|
53
|
+
# @param [String] key if `nil` use +Firebug::Configuration.key+
|
54
|
+
def self.decrypt(data, key=nil)
|
55
|
+
key = configuration.key if key.nil?
|
56
|
+
Crypto.new(key).decrypt(data)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Serializes, encrypts, and base64 encodes the data.
|
60
|
+
#
|
61
|
+
# @param [Object] data
|
62
|
+
# @return [String] a base64 encoded string
|
63
|
+
def self.encrypt_cookie(data)
|
64
|
+
Base64.strict_encode64(Firebug.encrypt(Firebug.serialize(data)))
|
65
|
+
end
|
66
|
+
|
67
|
+
# Decodes the base64 encoded string, decrypts, and unserializes.
|
68
|
+
#
|
69
|
+
# @param [String] data a base64 encoded encrypted string
|
70
|
+
# @return [Object] the unserialized data
|
71
|
+
def self.decrypt_cookie(data)
|
72
|
+
return {} if data.nil?
|
73
|
+
Firebug.unserialize(Firebug.decrypt(Base64.strict_decode64(data)))
|
74
|
+
end
|
75
|
+
end
|