firebug 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
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