提交 77f8cc75 编写于 作者: Huan (李卓桓)'s avatar Huan (李卓桓)

#59 new `BrowserCookie` class

上级 b3075a11
/**
* 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<CookieType[]> {
// 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<void> {
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<void> {
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<void> {
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<void> {
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
......@@ -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<any[]>
public addCookies(cookie: DriverCookie): Promise<any>
public async addCookies(cookie: DriverCookie|DriverCookie[]): Promise<any|any[]> {
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<any> {
log.silly('PuppetWebBrowser', 'Browser.execute("%s")'
, (
......@@ -610,128 +558,16 @@ export class Browser extends EventEmitter {
return dead
}
public async checkSession(): Promise<DriverCookie[]> {
// 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 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<DriverCookie[]> {
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 }
})
public async addCookie(cookies: CookieType[]): Promise<void>
public async addCookie(cookie: CookieType): Promise<void>
public async addCookie(cookie: CookieType|CookieType[]): Promise<void> {
await this.cookie.add(cookie)
}
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<DriverCookie[]> // XXX why need `as` here???
} catch (e) {
log.error('PuppetWebBrowser', 'saveSession() getCookies() exception: %s', e.message)
throw e
}
}
public loadSession(): Promise<any> {
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
......@@ -2,14 +2,12 @@ import * as fs from 'fs'
import { test } from 'ava'
import {
// PuppetWeb
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)
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册