diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..0900169 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,16 @@ +CHANGES: cookie_extractor +------------------------- + +### v0.2.0 2013-07-02 + +- Browser guessing code contributed by Ben Eills (github.com/beneills): + - Added --guess flag to automatically choose most recently used cookie file + - Added --browser flag to specify browser by name + +### v0.1.0 2012-02-23 + +- Updated to include Chrome/Chromium support. + +### v0.0.1 2012-02-20 + +- Initial version. Supports extracting Firefox cookies. diff --git a/README.md b/README.md index fff51ee..2ba69f5 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,26 @@ cookie_extractor ---------------- -Extract cookies from Firefox sqlite cookie store (eventually Chrome also) into a wget-compatible cookies.txt file. +Extract cookies from Firefox, Chrome or Chromium sqlite cookie stores into a wget-compatible cookies.txt file. ### Install ### -gem install cookie_extractor + gem install cookie_extractor ### Usage ### -cookie_extractor /path/to/firefox/cookies.sqlite > cookies.txt + cookie_extractor /path/to/firefox/cookies.sqlite > cookies.txt + + cookie_extractor --guess # Guess which browser to use and open corresponding file + + cookie_extractor --browser chrome|chromium|firefox # Open corresponding DB + + +Typical locations for the cookies file on Linux are: + + * Firefox: *~/.mozilla/firefox/(profile directory)/cookies.sqlite* + * Chrome: *~/.config/google-chrome/Default/Cookies* + * Chromium: *~/.config/chromium/Default/Cookies* ### License ### diff --git a/bin/cookie_extractor b/bin/cookie_extractor index 16b36a4..95a25d6 100755 --- a/bin/cookie_extractor +++ b/bin/cookie_extractor @@ -2,10 +2,46 @@ require File.expand_path(File.join(File.dirname(__FILE__), "..", "lib", "cookie_extractor")) -# TODO: detect firefox or chrome input file and/or locate it automatically -filename = ARGV.first -if filename - puts CookieExtractor::FirefoxCookieExtractor.new(filename).extract.join("\n") -else - puts "Usage: cookie_extractor /path/to/cookies.sqlite" + +def usage msg=nil + puts msg if msg + puts "Usage: cookie_extractor /path/to/cookies.sqlite Open a DB file" + puts " cookie_extractor --guess Guess which browser to use and open corresponding file" + puts " cookie_extractor --browser chrome|chromium|firefox Open DB corresponding to a particular browser" + exit end + + +begin + extractor = + case ARGV.first + when '--guess', '-g' + begin + CookieExtractor::BrowserDetector.guess() + rescue CookieExtractor::NoCookieFileFoundException + abort "Error: we couldn't find any supported broswer's cookies" + end + when '--browser', '-b' + browser = ARGV[1] + usage "Error: Please supply a browser name" unless browser + begin + CookieExtractor::BrowserDetector.browser_extractor(browser) + rescue CookieExtractor::InvalidBrowserNameException + abort "Error: '#{browser}' is not a valid browser name" + rescue CookieExtractor::NoCookieFileFoundException + abort "Error: Could not locate cookie file for browser #{browser}" + end + when nil + usage + else + filename = ARGV.first + abort "Error: #{filename} does not exist" unless File.exists?(filename) + CookieExtractor::BrowserDetector.new_extractor(filename) + end + puts extractor.extract.join("\n") +rescue SQLite3::NotADatabaseException, + CookieExtractor::BrowserNotDetectedException + abort "Error: File '#{filename}' is not a Firefox or Chrome cookie database" +end + + diff --git a/cookie_extractor.gemspec b/cookie_extractor.gemspec index a53359a..54c7227 100644 --- a/cookie_extractor.gemspec +++ b/cookie_extractor.gemspec @@ -8,8 +8,8 @@ Gem::Specification.new do |s| s.authors = ["Jeff Dallien"] s.email = ["jeff@dallien.net"] s.homepage = "http://github.com/jdallien/cookie_extractor" - s.summary = %q{Create cookies.txt from Firefox cookies} - s.description = %q{Extract cookies from Firefox sqlite databases into a wget-compatible cookies.txt file.} + s.summary = %q{Create cookies.txt from Firefox or Chrome/Chromium cookies} + s.description = %q{Extract cookies from Firefox, Chrome or Chromium sqlite databases into a wget-compatible cookies.txt file.} s.rubyforge_project = "cookie_extractor" @@ -18,6 +18,6 @@ Gem::Specification.new do |s| s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } s.require_paths = ["lib"] - s.add_development_dependency "rspec" - s.add_runtime_dependency "sqlite3-ruby" + s.add_development_dependency "rspec", "~> 2.8" + s.add_runtime_dependency "sqlite3-ruby", "~> 1.3" end diff --git a/lib/cookie_extractor.rb b/lib/cookie_extractor.rb index 20f68f4..99a1e71 100644 --- a/lib/cookie_extractor.rb +++ b/lib/cookie_extractor.rb @@ -1,5 +1,11 @@ +$:.unshift(File.dirname(__FILE__)) unless + $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__))) + require "cookie_extractor/version" +require "cookie_extractor/common" require "cookie_extractor/firefox_cookie_extractor" +require "cookie_extractor/chrome_cookie_extractor" +require "cookie_extractor/browser_detector" module CookieExtractor end diff --git a/lib/cookie_extractor/browser_detector.rb b/lib/cookie_extractor/browser_detector.rb new file mode 100644 index 0000000..8700344 --- /dev/null +++ b/lib/cookie_extractor/browser_detector.rb @@ -0,0 +1,78 @@ +module CookieExtractor + class BrowserNotDetectedException < Exception; end + class InvalidBrowserNameException < Exception; end + class NoCookieFileFoundException < Exception; end + + class BrowserDetector + COOKIE_LOCATIONS = { + "chrome" => "~/.config/google-chrome/Default/Cookies", + "chromium" => "~/.config/chromium/Default/Cookies", + "firefox" => "~/.mozilla/firefox/*.default/cookies.sqlite" + } + + # Returns the extractor of the most recently used browser's cookies + # or raise NoCookieFileFoundException if there are no cookies + def self.guess + most_recently_used_detected_browsers.each { |browser, path| + begin + extractor = self.browser_extractor(browser) + rescue BrowserNotDetectedException, NoCookieFileFoundException + # better try the next one... + else + return extractor + end + } + # If we make it here, we've failed... + raise NoCookieFileFoundException, "Couldn't find any browser's cookies" + end + + # Open a browser's cookie file using intelligent guesswork + def self.browser_extractor(browser) + raise InvalidBrowserNameException, "Browser must be one of: #{self.supported_browsers.join(', ')}" unless self.supported_browsers.include?(browser) + paths = Dir.glob(File.expand_path(COOKIE_LOCATIONS[browser])) + if paths.length < 1 or not File.exists?(paths.first) + raise NoCookieFileFoundException, "File #{paths.first} does not exist!" + end + self.new_extractor(paths.first) + end + + def self.new_extractor(db_filename) + browser = detect_browser(db_filename) + if browser + CookieExtractor.const_get("#{browser}CookieExtractor").new(db_filename) + else + raise BrowserNotDetectedException, "Could not detect browser type." + end + end + + def self.supported_browsers + COOKIE_LOCATIONS.keys + end + + def self.detect_browser(db_filename) + db = SQLite3::Database.new(db_filename) + browser = + if has_table?(db, 'moz_cookies') + 'Firefox' + elsif has_table?(db, 'cookies') + 'Chrome' + end + db.close + browser + end + + def self.has_table?(db, table_name) + db.table_info(table_name).size > 0 + end + + def self.most_recently_used_detected_browsers + COOKIE_LOCATIONS.select { |browser, path| + Dir.glob(File.expand_path(path)).any? + }.sort_by { |browser, path| + File.mtime(Dir.glob(File.expand_path(path)).first) + }.reverse + end + + private_class_method :most_recently_used_detected_browsers + end +end diff --git a/lib/cookie_extractor/chrome_cookie_extractor.rb b/lib/cookie_extractor/chrome_cookie_extractor.rb new file mode 100644 index 0000000..6e11004 --- /dev/null +++ b/lib/cookie_extractor/chrome_cookie_extractor.rb @@ -0,0 +1,29 @@ +require 'sqlite3' + +module CookieExtractor + class ChromeCookieExtractor + include Common + + def initialize(cookie_file) + @cookie_file = cookie_file + end + + def extract + db = SQLite3::Database.new @cookie_file + db.results_as_hash = true + result = [] + db.execute("SELECT * FROM cookies") do |row| + result << [ row['host_key'], + true_false_word(is_domain_wide(row['host_key'])), + row['path'], + true_false_word(row['secure']), + row['expires_utc'], + row['name'], + row['value'] + ].join("\t") + end + db.close + result + end + end +end diff --git a/lib/cookie_extractor/common.rb b/lib/cookie_extractor/common.rb new file mode 100644 index 0000000..a1af08f --- /dev/null +++ b/lib/cookie_extractor/common.rb @@ -0,0 +1,19 @@ +module CookieExtractor + module Common + private + + def is_domain_wide(hostname) + hostname[0..0] == "." + end + + def true_false_word(value) + if value == "1" || value == 1 || value == true + "TRUE" + elsif value == "0" || value == 0 || value == false + "FALSE" + else + raise "Invalid value passed to true_false_word: #{value.inspect}" + end + end + end +end diff --git a/lib/cookie_extractor/firefox_cookie_extractor.rb b/lib/cookie_extractor/firefox_cookie_extractor.rb index 5072464..68d1252 100644 --- a/lib/cookie_extractor/firefox_cookie_extractor.rb +++ b/lib/cookie_extractor/firefox_cookie_extractor.rb @@ -2,6 +2,7 @@ require 'sqlite3' module CookieExtractor class FirefoxCookieExtractor + include Common def initialize(cookie_file) @cookie_file = cookie_file @@ -10,9 +11,9 @@ module CookieExtractor def extract db = SQLite3::Database.new @cookie_file db.results_as_hash = true - @result = [] + result = [] db.execute("SELECT * FROM moz_cookies") do |row| - @result << [ row['host'], + result << [ row['host'], true_false_word(is_domain_wide(row['host'])), row['path'], true_false_word(row['isSecure']), @@ -21,23 +22,8 @@ module CookieExtractor row['value'] ].join("\t") end - @result - end - - private - - def is_domain_wide(hostname) - hostname[0..0] == "." - end - - def true_false_word(value) - if value == "1" || value == 1 || value == true - "TRUE" - elsif value == "0" || value == 0 || value == false - "FALSE" - else - raise "Invalid value passed to true_false_word: #{value.inspect}" - end + db.close + result end end end diff --git a/lib/cookie_extractor/version.rb b/lib/cookie_extractor/version.rb index 56fb8d9..424c034 100644 --- a/lib/cookie_extractor/version.rb +++ b/lib/cookie_extractor/version.rb @@ -1,3 +1,3 @@ module CookieExtractor - VERSION = "0.0.1" + VERSION = "0.2.0" end diff --git a/spec/browser_detector_spec.rb b/spec/browser_detector_spec.rb new file mode 100644 index 0000000..788a371 --- /dev/null +++ b/spec/browser_detector_spec.rb @@ -0,0 +1,80 @@ +require File.join(File.dirname(__FILE__), "spec_helper") + +describe CookieExtractor::BrowserDetector, "determining the correct extractor to use" do + before :each do + @fake_cookie_db = double("cookie database", :close => true) + SQLite3::Database.should_receive(:new). + with('filename'). + and_return(@fake_cookie_db) + end + + describe "given a sqlite database with a 'moz_cookies' table" do + before :each do + @fake_cookie_db.should_receive(:table_info). + with("moz_cookies"). + and_return( + { 'name' => 'some_field', + 'type' => "some_type" }) + end + + it "should return a firefox extractor instance" do + extractor = CookieExtractor::BrowserDetector.new_extractor('filename') + extractor.instance_of?(CookieExtractor::FirefoxCookieExtractor).should be_true + end + end + + describe "given a sqlite database with a 'cookies' table" do + before :each do + @fake_cookie_db.should_receive(:table_info). + with("moz_cookies"). + and_return([]) + @fake_cookie_db.should_receive(:table_info). + with("cookies"). + and_return( + [{ 'name' => 'some_field', + 'type' => "some_type" }]) + end + + it "should return a chrome extractor instance" do + extractor = CookieExtractor::BrowserDetector.new_extractor('filename') + extractor.instance_of?(CookieExtractor::ChromeCookieExtractor).should be_true + end + end +end + +describe CookieExtractor::BrowserDetector, "guessing the location of the cookie file" do + describe "when no cookie files are found in the standard locations" do + before :each do + Dir.stub!(:glob).and_return([]) + end + + it "should raise NoCookieFileFoundException" do + lambda { CookieExtractor::BrowserDetector.guess }. + should raise_error(CookieExtractor::NoCookieFileFoundException) + end + end + + describe "when multiple cookie files are found in the standard locations" do + before :each do + cookie_locations = CookieExtractor::BrowserDetector::COOKIE_LOCATIONS + Dir.stub!(:glob).and_return([cookie_locations['chrome']], + [], + [cookie_locations['firefox']]) + end + + describe "and chrome was the most recently used" do + before :each do + File.should_receive(:mtime).twice.and_return( + Time.parse("July 2 2013 00:00:00"), + Time.parse("July 1 2013 00:00:00")) + end + + it "should build a ChromeCookieExtractor" do + CookieExtractor::BrowserDetector. + should_receive(:browser_extractor). + once.with("chrome") + CookieExtractor::BrowserDetector.guess + end + end + end +end diff --git a/spec/chrome_cookie_extractor_spec.rb b/spec/chrome_cookie_extractor_spec.rb new file mode 100644 index 0000000..d9a13a4 --- /dev/null +++ b/spec/chrome_cookie_extractor_spec.rb @@ -0,0 +1,126 @@ +require File.join(File.dirname(__FILE__), "spec_helper") + +describe CookieExtractor::ChromeCookieExtractor do + before :each do + @fake_cookie_db = double("cookie database", + :results_as_hash= => true, + :close => true) + SQLite3::Database.should_receive(:new). + with('filename'). + and_return(@fake_cookie_db) + end + + describe "opening and closing a sqlite db" do + before :each do + @fake_cookie_db.should_receive(:execute).and_yield( + { 'host_key' => '.dallien.net', + 'path' => '/', + 'secure' => '0', + 'expires_utc' => '1234567890', + 'name' => 'NAME', + 'value' => 'VALUE'}) + @extractor = CookieExtractor::ChromeCookieExtractor.new('filename') + end + + it "should close the db when finished" do + @fake_cookie_db.should_receive(:close) + @extractor.extract + end + end + + describe "with a cookie that has a host starting with a dot" do + before :each do + @fake_cookie_db.should_receive(:execute).and_yield( + { 'host_key' => '.dallien.net', + 'path' => '/', + 'secure' => '0', + 'expires_utc' => '1234567890', + 'name' => 'NAME', + 'value' => 'VALUE'}) + @extractor = CookieExtractor::ChromeCookieExtractor.new('filename') + @result = @extractor.extract + end + + it "should return one cookie string" do + @result.size.should == 1 + end + + it "should put TRUE in the domain wide field" do + cookie_string = @result.first + cookie_string.split("\t")[1].should == "TRUE" + end + + it "should build the correct cookie string" do + cookie_string = @result.first + cookie_string.should == + ".dallien.net\tTRUE\t/\tFALSE\t1234567890\tNAME\tVALUE" + end + end + + describe "with a cookie that has a host not starting with a dot" do + before :each do + @fake_cookie_db.should_receive(:execute).and_yield( + { 'host_key' => 'jeff.dallien.net', + 'path' => '/path', + 'secure' => '1', + 'expires_utc' => '1234567890', + 'name' => 'NAME', + 'value' => 'VALUE'}) + @extractor = CookieExtractor::ChromeCookieExtractor.new('filename') + @result = @extractor.extract + end + + it "should return one cookie string" do + @result.size.should == 1 + end + + it "should put FALSE in the domain wide field" do + cookie_string = @result.first + cookie_string.split("\t")[1].should == "FALSE" + end + + it "should build the correct cookie string" do + cookie_string = @result.first + cookie_string.should == + "jeff.dallien.net\tFALSE\t/path\tTRUE\t1234567890\tNAME\tVALUE" + end + end + + describe "with a cookie that is not marked as secure" do + before :each do + @fake_cookie_db.should_receive(:execute).and_yield( + { 'host_key' => '.dallien.net', + 'path' => '/', + 'secure' => '0', + 'expires_utc' => '1234567890', + 'name' => 'NAME', + 'value' => 'VALUE'}) + @extractor = CookieExtractor::ChromeCookieExtractor.new('filename') + @result = @extractor.extract + end + + it "should put FALSE in the secure field" do + cookie_string = @result.first + cookie_string.split("\t")[3].should == "FALSE" + end + end + + describe "with a cookie that is marked as secure" do + before :each do + @fake_cookie_db.should_receive(:execute).and_yield( + { 'host_key' => '.dallien.net', + 'path' => '/', + 'secure' => '1', + 'expires_utc' => '1234567890', + 'name' => 'NAME', + 'value' => 'VALUE'}) + @extractor = CookieExtractor::ChromeCookieExtractor.new('filename') + @result = @extractor.extract + end + + it "should put TRUE in the secure field" do + cookie_string = @result.first + cookie_string.split("\t")[3].should == "TRUE" + end + end +end diff --git a/spec/firefox_cookie_extractor_spec.rb b/spec/firefox_cookie_extractor_spec.rb index fb86adf..8fd4ae5 100644 --- a/spec/firefox_cookie_extractor_spec.rb +++ b/spec/firefox_cookie_extractor_spec.rb @@ -2,12 +2,32 @@ require File.join(File.dirname(__FILE__), "spec_helper") describe CookieExtractor::FirefoxCookieExtractor do before :each do - @fake_cookie_db = double("cookie database", :results_as_hash= => true) + @fake_cookie_db = double("cookie database", + :results_as_hash= => true, + :close => true) SQLite3::Database.should_receive(:new). with('filename'). and_return(@fake_cookie_db) end + describe "opening and closing a sqlite db" do + before :each do + @fake_cookie_db.should_receive(:execute).and_yield( + {'host' => '.dallien.net', + 'path' => '/', + 'isSecure' => '0', + 'expiry' => '1234567890', + 'name' => 'NAME', + 'value' => 'VALUE'}) + @extractor = CookieExtractor::FirefoxCookieExtractor.new('filename') + end + + it "should close the db when finished" do + @fake_cookie_db.should_receive(:close) + @extractor.extract + end + end + describe "with a cookie that has a host starting with a dot" do before :each do @fake_cookie_db.should_receive(:execute).and_yield(