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.
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+ IFS=$'\n\t'
5
+ set -vx
6
+
7
+ bundle install
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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Firebug
4
+ VERSION = '0.0.7'
5
+ 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