diff --git a/src/puppet-web/browser-cookie.ts b/src/puppet-web/browser-cookie.ts new file mode 100644 index 0000000000000000000000000000000000000000..a6b346fa25748fddf2d7bbd46ae7c24bde4d78f3 --- /dev/null +++ b/src/puppet-web/browser-cookie.ts @@ -0,0 +1,211 @@ +/** + * Wechat for Bot. Connecting ChatBots + * + * BrowserCookie + * + * ISSUE #59 + * + * Licenst: ISC + * https://github.com/zixia/wechaty + * + */ +import * as fs from 'fs' +const arrify = require('arrify') + +import Browser from './browser' +import log from '../brolog-env' + +/** + * DriverCookie type exits is because @types/selenium is not updated + * with the latest 3.0 version of selenium. 201610 zixia + */ +export type CookieType = { + [index: string]: string | number | boolean + name: string + value: string + path: string + domain: string + secure: boolean + expiry: number +} + +export class BrowserCookie { + constructor(private browser: Browser, private storeFile?: string) { + log.verbose('PuppetWebBrowserCookie', 'constructor(%s, %s)' + , browser.constructor.name + , storeFile ? storeFile : '' + ) + } + + public async read(): Promise { + // just check cookies, no file operation + log.verbose('PuppetWebBrowserCookie', 'checkSession()') + + if (this.browser.dead()) { + throw new Error('checkSession() - browser dead') + } + + // return new Promise((resolve, reject) => { + try { + // `as any as DriverCookie` because selenium-webdriver @types is outdated with 2.x, where we r using 3.0 + const cookies = await this.browser.driver().manage().getCookies() as any as CookieType[] + log.silly('PuppetWebBrowserCookie', 'checkSession %s', cookies.map(c => c.name).join(',')) + return cookies + } catch (e) { + log.error('PuppetWebBrowserCookie', 'checkSession() getCookies() exception: %s', e && e.message || e) + throw e + } + } + + public async clean(): Promise { + log.verbose('PuppetWebBrowserCookie', `cleanSession(${this.storeFile})`) + if (!this.storeFile) { + return + } + + if (this.browser.dead()) { return Promise.reject(new Error('cleanSession() - browser dead'))} + + const filename = this.storeFile + await new Promise((resolve, reject) => { + fs.unlink(filename, err => { + if (err && err.code !== 'ENOENT') { + log.silly('PuppetWebBrowserCookie', 'cleanSession() unlink session file %s fail: %s', filename, err.message) + } + resolve() + }) + }) + return + } + + public async save(): Promise { + log.silly('PuppetWebBrowserCookie', `saveSession(${this.storeFile})`) + if (!this.storeFile) { + log.verbose('PuppetWebBrowserCookie', 'save() no session store file') + return + } + const storeFile = this.storeFile + + if (this.browser.dead()) { + throw new Error('saveSession() - browser dead') + } + + const filename = this.storeFile + + function cookieFilter(cookies: CookieType[]) { + const skipNames = [ + 'ChromeDriver' + , 'MM_WX_SOUND_STATE' + , 'MM_WX_NOTIFY_STATE' + ] + const skipNamesRegex = new RegExp(skipNames.join('|'), 'i') + return cookies.filter(c => { + if (skipNamesRegex.test(c.name)) { return false } + // else if (!/wx\.qq\.com/i.test(c.domain)) { return false } + else { return true } + }) + } + + try { + // return new Promise((resolve, reject) => { + // `as any as DriverCookie` because selenium-webdriver @types is outdated with 2.x, where we r using 3.0 + let cookies: CookieType[] = await this.browser.driver().manage().getCookies() as any as CookieType[] + cookies = cookieFilter(cookies) + // .then(cookies => { + // log.silly('PuppetWeb', 'saving %d cookies for session: %s', cookies.length + // , util.inspect(cookies.map(c => { return {name: c.name /*, value: c.value, expiresType: typeof c.expires, expires: c.expires*/} }))) + log.silly('PuppetWebBrowserCookie', 'saving %d cookies for session: %s', cookies.length, cookies.map(c => c.name).join(',')) + + const jsonStr = JSON.stringify(cookies) + + await new Promise((resolve, reject) => { + fs.writeFile(storeFile, jsonStr, err => { + if (err) { + log.error('PuppetWebBrowserCookie', 'saveSession() fail to write file %s: %s', filename, err.errno) + reject(err) + } + log.silly('PuppetWebBrowserCookie', 'saved session(%d cookies) to %s', cookies.length, filename) + resolve(cookies) + }) + }) + + } catch (e) { + log.error('PuppetWebBrowserCookie', 'saveSession() getCookies() exception: %s', e.message) + throw e + } + } + + public async load(): Promise { + log.verbose('PuppetWebBrowserCookie', 'loadSession() from %s', this.storeFile ? this.storeFile : '' ) + + if (!this.storeFile) { + log.verbose('PuppetWebBrowserCookie', 'load() no session store file') + return + } else if (this.browser.dead()) { + throw new Error('loadSession() - browser dead') + } + const storeFile = this.storeFile + + await new Promise((resolve, reject) => { + fs.readFile(storeFile, (err, jsonStr) => { + if (err) { + if (err) { log.silly('PuppetWebBrowserCookie', 'loadSession(%s) skipped because error code: %s', this.storeFile, err.code) } + return reject(new Error('error code:' + err.code)) + } + const cookies = JSON.parse(jsonStr.toString()) + + let ps = arrify(this.add(cookies)) + Promise.all(ps) + .then(() => { + log.verbose('PuppetWebBrowserCookie', 'loaded session(%d cookies) from %s', cookies.length, this.storeFile) + resolve(cookies) + }) + .catch(e => { + log.error('PuppetWebBrowserCookie', 'loadSession() addCookies() exception: %s', e.message) + reject(e) + }) + }) + }) + } + + /** + * only wrap addCookies for convinience + * + * use this.driver().manage() to call other functions like: + * deleteCookie / getCookie / getCookies + */ + // TypeScript Overloading: http://stackoverflow.com/a/21385587/1123955 + public async add(cookie: CookieType|CookieType[]): Promise { + if (this.browser.dead()) { return Promise.reject(new Error('addCookies() - browser dead'))} + + if (Array.isArray(cookie)) { + for (let c of cookie) { + await this.add(c) + } + return + } + /** + * convert expiry from seconds to milliseconds. https://github.com/SeleniumHQ/selenium/issues/2245 + * with selenium-webdriver v2.53.2 + * NOTICE: the lastest branch of selenium-webdriver for js has changed the interface of addCookie: + * https://github.com/SeleniumHQ/selenium/commit/02f407976ca1d516826990f11aca7de3c16ba576 + */ + // if (cookie.expiry) { cookie.expiry = cookie.expiry * 1000 /* XXX: be aware of new version of webdriver */} + + log.silly('PuppetWebBrowserCookie', 'addCookies(%s)', JSON.stringify(cookie)) + + // return new Promise((resolve, reject) => { + try { + await (this.browser.driver().manage() as any).addCookie(cookie) + // this is old webdriver format + // .addCookie(cookie.name, cookie.value, cookie.path + // , cookie.domain, cookie.secure, cookie.expiry) + // this is new webdriver format + } catch (e) { + log.warn('PuppetWebBrowserCookie', 'addCookies() exception: %s', e.message) + throw e + } + } + +} + +export default BrowserCookie diff --git a/src/puppet-web/browser.ts b/src/puppet-web/browser.ts index b3e3109cdc863a60df30308a93db5ba7eedcc05a..cf3289cfa78d6d74897859265ae72dd3c956f0d4 100644 --- a/src/puppet-web/browser.ts +++ b/src/puppet-web/browser.ts @@ -7,11 +7,6 @@ * https://github.com/zixia/wechaty * */ -/* tslint:disable:no-var-requires */ -const arrify = require('arrify') -// import arrify = require('arrify') - -import * as fs from 'fs' import { EventEmitter } from 'events' import { Builder @@ -22,29 +17,24 @@ import { /* tslint:disable:no-var-requires */ const retryPromise = require('retry-promise').default // https://github.com/olalonde/retry-promise -import log from'../brolog-env' +import Config from '../config' +import log from '../brolog-env' -import Config from'../config' +import { + CookieType + , BrowserCookie +} from './browser-cookie' export type BrowserSetting = { head?: string sessionFile?: string } -export type DriverCookie = { - [index: string]: string | number | boolean - name: string - value: string - path: string - domain: string - secure: boolean - expiry: number -} - export class Browser extends EventEmitter { private _targetState: string private _currentState: string + private cookie: BrowserCookie public _driver: WebDriver | null = null @@ -56,6 +46,8 @@ export class Browser extends EventEmitter { this.targetState('close') this.currentState('close') + + this.cookie = new BrowserCookie(this, this.setting.sessionFile) } // targetState : 'open' | 'close' @@ -92,7 +84,7 @@ export class Browser extends EventEmitter { // this.live = true await this.open(fastUrl) - await this.loadSession() + await this.loadCookie() .catch(e => { // fail safe log.verbose('PuppetWeb', 'browser.loadSession(%s) exception: %s', this.setting.sessionFile, e && e.message || e) }) @@ -461,50 +453,6 @@ export class Browser extends EventEmitter { }) } - /** - * only wrap addCookies for convinience - * - * use this.driver().manage() to call other functions like: - * deleteCookie / getCookie / getCookies - */ - // TypeScript Overloading: http://stackoverflow.com/a/21385587/1123955 - public addCookies(cookies: DriverCookie[]): Promise - public addCookies(cookie: DriverCookie): Promise - - public async addCookies(cookie: DriverCookie|DriverCookie[]): Promise { - if (this.dead()) { return Promise.reject(new Error('addCookies() - browser dead'))} - - if (Array.isArray(cookie)) { - return cookie.map(c => { - return this.addCookies(c) - }) - } - /** - * convert expiry from seconds to milliseconds. https://github.com/SeleniumHQ/selenium/issues/2245 - * with selenium-webdriver v2.53.2 - * NOTICE: the lastest branch of selenium-webdriver for js has changed the interface of addCookie: - * https://github.com/SeleniumHQ/selenium/commit/02f407976ca1d516826990f11aca7de3c16ba576 - */ - // if (cookie.expiry) { cookie.expiry = cookie.expiry * 1000 /* XXX: be aware of new version of webdriver */} - - log.silly('PuppetWebBrowser', 'addCookies(%s)', JSON.stringify(cookie)) - - let ret - // return new Promise((resolve, reject) => { - try { - ret = await (this.driver().manage() as any).addCookie(cookie) - // this is old webdriver format - // .addCookie(cookie.name, cookie.value, cookie.path - // , cookie.domain, cookie.secure, cookie.expiry) - // this is new webdriver format - } catch (e) { - log.warn('PuppetWebBrowser', 'addCookies() exception: %s', e.message) - throw e - } - - return ret - } - public async execute(script, ...args): Promise { log.silly('PuppetWebBrowser', 'Browser.execute("%s")' , ( @@ -610,128 +558,16 @@ export class Browser extends EventEmitter { return dead } - public async checkSession(): Promise { - // just check cookies, no file operation - log.verbose('PuppetWebBrowser', 'checkSession()') - - if (this.dead()) { Promise.reject(new Error('checkSession() - browser dead'))} - - // return new Promise((resolve, reject) => { - try { - // `as any as DriverCookie` because selenium-webdriver @types is outdated with 2.x, where we r using 3.0 - const cookies = await this.driver().manage().getCookies() as any as DriverCookie[] - log.silly('PuppetWebBrowser', 'checkSession %s', cookies.map(c => c.name).join(',')) - return cookies - } catch (e) { - log.error('PuppetWebBrowser', 'checkSession() getCookies() exception: %s', e && e.message || e) - throw e - } + public async addCookie(cookies: CookieType[]): Promise + public async addCookie(cookie: CookieType): Promise + public async addCookie(cookie: CookieType|CookieType[]): Promise { + await this.cookie.add(cookie) } - public cleanSession() { - log.verbose('PuppetWebBrowser', `cleanSession(${this.setting.sessionFile})`) - if (!this.setting.sessionFile) { - return Promise.reject(new Error('cleanSession() no session')) - } - - if (this.dead()) { return Promise.reject(new Error('cleanSession() - browser dead'))} - - const filename = this.setting.sessionFile - return new Promise((resolve, reject) => { - fs.unlink(filename, err => { - if (err && err.code !== 'ENOENT') { - log.silly('PuppetWebBrowser', 'cleanSession() unlink session file %s fail: %s', filename, err.message) - } - resolve() - }) - }) - } - - public async saveSession(): Promise { - log.silly('PuppetWebBrowser', `saveSession(${this.setting.sessionFile})`) - if (!this.setting.sessionFile) { - throw new Error('saveSession() no session') - } else if (this.dead()) { - throw new Error('saveSession() - browser dead') - } - - const filename = this.setting.sessionFile - - function cookieFilter(cookies: DriverCookie[]) { - const skipNames = [ - 'ChromeDriver' - , 'MM_WX_SOUND_STATE' - , 'MM_WX_NOTIFY_STATE' - ] - const skipNamesRegex = new RegExp(skipNames.join('|'), 'i') - return cookies.filter(c => { - if (skipNamesRegex.test(c.name)) { return false } - // else if (!/wx\.qq\.com/i.test(c.domain)) { return false } - else { return true } - }) - } - - try { - // return new Promise((resolve, reject) => { - // `as any as DriverCookie` because selenium-webdriver @types is outdated with 2.x, where we r using 3.0 - let cookies: DriverCookie[] = await this.driver().manage().getCookies() as any as DriverCookie[] - cookies = cookieFilter(cookies) - // .then(cookies => { - // log.silly('PuppetWeb', 'saving %d cookies for session: %s', cookies.length - // , util.inspect(cookies.map(c => { return {name: c.name /*, value: c.value, expiresType: typeof c.expires, expires: c.expires*/} }))) - log.silly('PuppetWebBrowser', 'saving %d cookies for session: %s', cookies.length, cookies.map(c => c.name).join(',')) - - const jsonStr = JSON.stringify(cookies) - - return new Promise((resolve, reject) => { - fs.writeFile(filename, jsonStr, err => { - if (err) { - log.error('PuppetWebBrowser', 'saveSession() fail to write file %s: %s', filename, err.errno) - reject(err) - } - log.silly('PuppetWebBrowser', 'saved session(%d cookies) to %s', cookies.length, filename) - resolve(cookies) - }) - }) as Promise // XXX why need `as` here??? - - } catch (e) { - log.error('PuppetWebBrowser', 'saveSession() getCookies() exception: %s', e.message) - throw e - } - } - - public loadSession(): Promise { - log.verbose('PuppetWebBrowser', 'loadSession() from %s', this.setting.sessionFile ? this.setting.sessionFile : '' ) - - if (!this.setting.sessionFile) { - return Promise.reject(new Error('loadSession() no sessionFile')) - } else if (this.dead()) { - return Promise.reject(new Error('loadSession() - browser dead')) - } - - const filename = this.setting.sessionFile - - return new Promise((resolve, reject) => { - fs.readFile(filename, (err, jsonStr) => { - if (err) { - if (err) { log.silly('PuppetWebBrowser', 'loadSession(%s) skipped because error code: %s', filename, err.code) } - return reject(new Error('error code:' + err.code)) - } - const cookies = JSON.parse(jsonStr.toString()) - - let ps = arrify(this.addCookies(cookies)) - Promise.all(ps) - .then(() => { - log.verbose('PuppetWebBrowser', 'loaded session(%d cookies) from %s', cookies.length, filename) - resolve(cookies) - }) - .catch(e => { - log.error('PuppetWebBrowser', 'loadSession() addCookies() exception: %s', e.message) - reject(e) - }) - }) - }) - } + public saveCookie() { return this.cookie.save() } + public loadCookie() { return this.cookie.load() } + public readCookie() { return this.cookie.read() } + public cleanCookie() { return this.cookie.clean() } } export default Browser diff --git a/test/puppet-web/browser.spec.ts b/test/puppet-web/browser.spec.ts index 12d230de5a6af4851acfe9edd44367e1f0796b71..895f3913f585bbcb83e1eca3ffc90adc504794dd 100644 --- a/test/puppet-web/browser.spec.ts +++ b/test/puppet-web/browser.spec.ts @@ -2,14 +2,12 @@ import * as fs from 'fs' import { test } from 'ava' import { - // PuppetWeb - Config + Config , log } from '../../' import { Browser - // , PuppetWeb } from '../../src/puppet-web/' const PROFILE = Config.DEFAULT_PROFILE + '-' + process.pid + '-' @@ -53,7 +51,8 @@ test('Browser class cookie smoking tests', async t => { , expiry: 99999999999999 }] - const tt = await b.addCookies(EXPECTED_COOKIES) + await b.addCookie(EXPECTED_COOKIES) + const tt = await b.readCookie() await Promise.all(tt) cookies = await b.driver().manage().getCookies() @@ -83,6 +82,7 @@ test('Browser session save before quit, and load after restart', async t => { let b = new Browser({ sessionFile: profileName }) + /** * use exception to call b.quit() to clean up */ @@ -111,31 +111,33 @@ test('Browser session save before quit, and load after restart', async t => { let cookies = await b.driver().manage().getCookies() t.is(cookies.length, 0, 'should no cookie after deleteAllCookies()') - await b.addCookies(EXPECTED_COOKIE) + await b.addCookie(EXPECTED_COOKIE) const cookieFromBrowser = await b.driver().manage().getCookie(EXPECTED_COOKIE.name) t.is(cookieFromBrowser.name, EXPECTED_COOKIE.name, 'cookie from getCookie() should be same as we just set') - let cookiesFromCheck = await b.checkSession() + let cookiesFromCheck = await b.readCookie() t.truthy(cookiesFromCheck.length, 'should get cookies from checkSession() after addCookies()') let cookieFromCheck = cookiesFromCheck.filter(c => EXPECTED_NAME_REGEX.test(c['name'])) t.is(cookieFromCheck[0]['name'], EXPECTED_COOKIE.name, 'cookie from checkSession() return should be same as we just set by addCookies()') - const cookiesFromSave = await b.saveSession() + await b.saveCookie() + const cookiesFromSave = await b.readCookie() t.truthy(cookiesFromSave.length, 'should get cookies from saveSession()') const cookieFromSave = cookiesFromSave.filter(c => EXPECTED_NAME_REGEX.test(c['name'])) t.is(cookieFromSave.length, 1, 'should has the cookie we just set') t.is(cookieFromSave[0]['name'], EXPECTED_COOKIE.name, 'cookie from saveSession() return should be same as we just set') await b.driver().manage().deleteAllCookies() - cookiesFromCheck = await b.checkSession() + cookiesFromCheck = await b.readCookie() t.is(cookiesFromCheck.length, 0, 'should no cookie from checkSession() after deleteAllCookies()') - const cookiesFromLoad = await b.loadSession().catch(() => { /* fail safe */ }) + await b.loadCookie().catch(() => { /* fail safe */ }) + const cookiesFromLoad = await b.readCookie() t.truthy(cookiesFromLoad.length, 'should get cookies after loadSession()') const cookieFromLoad = cookiesFromLoad.filter(c => EXPECTED_NAME_REGEX.test(c.name)) t.is(cookieFromLoad[0].name, EXPECTED_COOKIE.name, 'cookie from loadSession() should has expected cookie') - cookiesFromCheck = await b.checkSession() + cookiesFromCheck = await b.readCookie() t.truthy(cookiesFromCheck.length, 'should get cookies from checkSession() after loadSession()') cookieFromCheck = cookiesFromCheck.filter(c => EXPECTED_NAME_REGEX.test(c['name'])) t.truthy(cookieFromCheck.length, 'should has cookie after filtered after loadSession()') @@ -152,6 +154,7 @@ test('Browser session save before quit, and load after restart', async t => { b = new Browser({ sessionFile: profileName }) + t.pass('should started a new Browser') b.targetState('open') @@ -161,7 +164,7 @@ test('Browser session save before quit, and load after restart', async t => { await b.open() t.pass('should opened') - await b.loadSession() + await b.loadCookie() t.pass('should loadSession for new Browser(process)') const cookieAfterQuit = await b.driver().manage().getCookie(EXPECTED_COOKIE.name)