|
| 1 | +var path = require('path'); |
| 2 | + |
| 3 | +var log = require('../log'); |
| 4 | +var Plugin = require('../plugin'); |
| 5 | +var Queue = require('../queue'); |
| 6 | +var session = require('../session'); |
| 7 | +var config = require('../config'); |
| 8 | + |
| 9 | +// [Usage] |
| 10 | +// |
| 11 | +// https://github.com/skygragon/leetcode-cli-plugins/blob/master/docs/cookie.edge.md |
| 12 | +// |
| 13 | +var plugin = new Plugin(13, 'cookie.edge', '2018.11.18', |
| 14 | + 'Plugin to reuse Edge\'s leetcode cookie.', |
| 15 | + ['ffi:win32', 'keytar:darwin', 'ref:win32', 'ref-struct:win32', 'sqlite3']); |
| 16 | + |
| 17 | +plugin.help = function() { |
| 18 | + switch (process.platform) { |
| 19 | + case 'darwin': |
| 20 | + break; |
| 21 | + case 'linux': |
| 22 | + log.warn('To complete the install: sudo apt install libsecret-tools'); |
| 23 | + break; |
| 24 | + case 'win32': |
| 25 | + break; |
| 26 | + } |
| 27 | +}; |
| 28 | + |
| 29 | +var Chrome = {}; |
| 30 | + |
| 31 | +var ChromeMAC = { |
| 32 | + getDBPath: function() { |
| 33 | + return `${process.env.HOME}/Library/Application Support/Microsoft Edge/${this.profile}/Cookies`; |
| 34 | + }, |
| 35 | + iterations: 1003, |
| 36 | + getPassword: function(cb) { |
| 37 | + var keytar = require('keytar'); |
| 38 | + keytar.getPassword('Microsoft Edge Safe Storage', 'Microsoft Edge').then(cb); |
| 39 | + } |
| 40 | +}; |
| 41 | + |
| 42 | +var ChromeLinux = { |
| 43 | + getDBPath: function() { |
| 44 | + return `${process.env.HOME}/.config/google-edge/${this.profile}/Cookies`; |
| 45 | + }, |
| 46 | + iterations: 1, |
| 47 | + getPassword: function(cb) { |
| 48 | + // FIXME: keytar failed to read gnome-keyring on ubuntu?? |
| 49 | + var cmd = 'secret-tool lookup application edge'; |
| 50 | + var password = require('child_process').execSync(cmd).toString(); |
| 51 | + return cb(password); |
| 52 | + } |
| 53 | +}; |
| 54 | + |
| 55 | +var ChromeWindows = { |
| 56 | + getDBPath: function() { |
| 57 | + return path.resolve(process.env.APPDATA || '', `../Local/Google/Chrome/User Data/${this.profile}/Cookies`); |
| 58 | + }, |
| 59 | + getPassword: function(cb) { cb(); } |
| 60 | +}; |
| 61 | + |
| 62 | +Object.setPrototypeOf(ChromeMAC, Chrome); |
| 63 | +Object.setPrototypeOf(ChromeLinux, Chrome); |
| 64 | +Object.setPrototypeOf(ChromeWindows, Chrome); |
| 65 | + |
| 66 | +Chrome.getInstance = function() { |
| 67 | + switch (process.platform) { |
| 68 | + case 'darwin': return ChromeMAC; |
| 69 | + case 'linux': return ChromeLinux; |
| 70 | + case 'win32': return ChromeWindows; |
| 71 | + } |
| 72 | +}; |
| 73 | +var my = Chrome.getInstance(); |
| 74 | + |
| 75 | +ChromeWindows.decodeCookie = function(cookie, cb) { |
| 76 | + var ref = require('ref'); |
| 77 | + var ffi = require('ffi'); |
| 78 | + var Struct = require('ref-struct'); |
| 79 | + |
| 80 | + var DATA_BLOB = Struct({ |
| 81 | + cbData: ref.types.uint32, |
| 82 | + pbData: ref.refType(ref.types.byte) |
| 83 | + }); |
| 84 | + var PDATA_BLOB = new ref.refType(DATA_BLOB); |
| 85 | + var Crypto = new ffi.Library('Crypt32', { |
| 86 | + 'CryptUnprotectData': ['bool', [PDATA_BLOB, 'string', 'string', 'void *', 'string', 'int', PDATA_BLOB]] |
| 87 | + }); |
| 88 | + |
| 89 | + var inBlob = new DATA_BLOB(); |
| 90 | + inBlob.pbData = cookie; |
| 91 | + inBlob.cbData = cookie.length; |
| 92 | + var outBlob = ref.alloc(DATA_BLOB); |
| 93 | + |
| 94 | + Crypto.CryptUnprotectData(inBlob.ref(), null, null, null, null, 0, outBlob); |
| 95 | + var outDeref = outBlob.deref(); |
| 96 | + var buf = ref.reinterpret(outDeref.pbData, outDeref.cbData, 0); |
| 97 | + |
| 98 | + return cb(null, buf.toString('utf8')); |
| 99 | +}; |
| 100 | + |
| 101 | +Chrome.decodeCookie = function(cookie, cb) { |
| 102 | + var crypto = require('crypto'); |
| 103 | + crypto.pbkdf2(my.password, 'saltysalt', my.iterations, 16, 'sha1', function(e, key) { |
| 104 | + if (e) return cb(e); |
| 105 | + |
| 106 | + var iv = new Buffer(' '.repeat(16)); |
| 107 | + var decipher = crypto.createDecipheriv('aes-128-cbc', key, iv); |
| 108 | + decipher.setAutoPadding(false); |
| 109 | + |
| 110 | + var buf = decipher.update(cookie.slice(3)); // remove prefix "v10" or "v11" |
| 111 | + var final = decipher.final(); |
| 112 | + final.copy(buf, buf.length - 1); |
| 113 | + |
| 114 | + var padding = buf[buf.length - 1]; |
| 115 | + if (padding) buf = buf.slice(0, buf.length - padding); |
| 116 | + |
| 117 | + return cb(null, buf.toString('utf8')); |
| 118 | + }); |
| 119 | +}; |
| 120 | + |
| 121 | +function doDecode(key, queue, cb) { |
| 122 | + var ctx = queue.ctx; |
| 123 | + var cookie = ctx[key]; |
| 124 | + if (!cookie) return cb('Not found cookie: ' + key); |
| 125 | + |
| 126 | + my.decodeCookie(cookie, function(e, cookie) { |
| 127 | + ctx[key] = cookie; |
| 128 | + return cb(); |
| 129 | + }); |
| 130 | +} |
| 131 | + |
| 132 | +Chrome.getCookies = function(cb) { |
| 133 | + var sqlite3 = require('sqlite3'); |
| 134 | + var db = new sqlite3.Database(my.getDBPath()); |
| 135 | + db.on('error', cb); |
| 136 | + var KEYS = ['csrftoken', 'LEETCODE_SESSION']; |
| 137 | + |
| 138 | + let host = config.sys.cookie_host; |
| 139 | + if (!host) { |
| 140 | + host = 'leetcode.com'; // default host key |
| 141 | + } |
| 142 | + |
| 143 | + db.serialize(function() { |
| 144 | + var cookies = {}; |
| 145 | + var sql = 'select name, encrypted_value from cookies where host_key like "%' + host +'"'; |
| 146 | + db.each(sql, function(e, x) { |
| 147 | + if (e) return cb(e); |
| 148 | + if (KEYS.indexOf(x.name) < 0) return; |
| 149 | + cookies[x.name] = x.encrypted_value; |
| 150 | + }); |
| 151 | + |
| 152 | + db.close(function() { |
| 153 | + my.getPassword(function(password) { |
| 154 | + my.password = password; |
| 155 | + var q = new Queue(KEYS, cookies, doDecode); |
| 156 | + q.run(null, cb); |
| 157 | + }); |
| 158 | + }); |
| 159 | + }); |
| 160 | +}; |
| 161 | + |
| 162 | +plugin.signin = function(user, cb) { |
| 163 | + log.debug('running cookie.edge.signin'); |
| 164 | + log.debug('try to copy leetcode cookies from edge ...'); |
| 165 | + |
| 166 | + my.profile = plugin.config.profile || 'Default'; |
| 167 | + my.getCookies(function(e, cookies) { |
| 168 | + if (e) { |
| 169 | + log.error(`Failed to copy cookies from profile "${my.profile}"`); |
| 170 | + log.error(e); |
| 171 | + return plugin.next.signin(user, cb); |
| 172 | + } |
| 173 | + |
| 174 | + log.debug('Successfully copied leetcode cookies!'); |
| 175 | + user.sessionId = cookies.LEETCODE_SESSION; |
| 176 | + user.sessionCSRF = cookies.csrftoken; |
| 177 | + session.saveUser(user); |
| 178 | + return cb(null, user); |
| 179 | + }); |
| 180 | +}; |
| 181 | + |
| 182 | +plugin.login = function(user, cb) { |
| 183 | + log.debug('running cookie.edge.login'); |
| 184 | + plugin.signin(user, function(e, user) { |
| 185 | + if (e) return cb(e); |
| 186 | + plugin.getUser(user, cb); |
| 187 | + }); |
| 188 | +}; |
| 189 | + |
| 190 | +module.exports = plugin; |
0 commit comments