/**
* Mail : indexxuan@gmail.com
* Date : Tue 14 Mar 2017 03:19:07 PM CST
*/
/**
* @module AuthService
*/
'use strict'
// import parser
const setCookieParser = require('set-cookie-parser')
module.exports = app => {
/**
* @class AuthService
* @extends app.Service
*/
return class AuthService extends app.Service {
/**
* @constructor
* @param {Objet} ctx - 请求上下文
*/
constructor (ctx) {
super(ctx)
// 首页url
this.homeUrl = 'https://www.v2ex.com/'
// login相关
this.loginUrl = 'https://www.v2ex.com/signin'
this.sessionCookieStr = '' // 未登录的sessionid, 供 `login` 接口放在请求Headers里的 `Set-Cookie` 项中使用
this.userField = '' // 用户名 输入框埋下的随机表单域
this.passField = '' // 密码 输入框埋下的随机表单域
this.once = '12345' // input[type="hidden"][name="once"]的随机令牌值(目前是5位数字)
// signin相关
this.signinUrl = 'https://www.v2ex.com/mission/daily'
this.noAuth = false // 是否有权限签到
this.hasSignin = false // 是否能获取到 `redeem?once=` 来判断是否已经签到
}
/**
* 封装统一的请求方法
* @member
* @param {String} url - 请求地址
* @param {Object} opts - 请求选项
* @returns {Promise}
*/
async request (url, opts) {
opts = Object.assign({
timeout: [ '30s', '30s' ],
}, opts)
return await this.ctx.curl(url, opts)
}
/**
* 请求首页来刷新sessionid cookie
* @member
* @returns {Promise}
*/
async enterHomePage () {
const result = await this.request(this.homeUrl)
const session = setCookieParser(result)
session.forEach(c => {
this.ctx.cookies.set(c.name, c.value, {
httpOnly: c.httpOnly,
domain: '',
path: c.path,
expires: c.expires
})
})
}
/**
* 获取登录的各种凭证
* @member
* @param {String} result - 请求登录页返回的response,包含html字符串和Headers
*/
getLoginFields (result) {
const content = result.data
// get fileds
const keyRe = /class="sl" name="([0-9A-Za-z]{64})"/g
this.userField = keyRe.exec(content)[1]
this.passField = keyRe.exec(content)[1]
// get once
const onceRe = /value="(\d+)" name="once"/
this.once = onceRe.exec(content)[1]
// get string session cookie
this.sessionCookieStr = result.headers['set-cookie'][0]
}
/**
* 获取sessionid,获取userField, passField, once等,为登录做准备
* @member
* @returns {Promise|void}
*/
async enterLoginPage () {
const result = await this.request(this.loginUrl, {
method: 'GET',
dataType: 'text',
headers: this.commonHeaders
})
return this.getLoginFields(result)
}
/**
* login获取权限签名主方法
* @method
* @param {String} username - 用户名
* @param {String} password - 密码
*/
async login ({username, password}) {
// @step1 进入登录页,获取页面隐藏登录域以及once的值
await this.enterLoginPage()
// @step2 设置请求参数
const opts = {
method: 'POST',
headers: Object.assign({}, this.ctx.commonHeaders, { Cookie: this.sessionCookieStr }),
data: {
[this.userField]: username,
[this.passField]: password,
"once": this.once
}
}
// @step3 发起请求
const result = await this.request(this.loginUrl, opts)
// @step4 更新session并设置在客户端
await this.enterHomePage()
// @step5 解析获取到的cookies
const cs = setCookieParser(result)
// @step6 判断是否登录成功并种下客户端cookies
let success = false
cs.forEach(c => {
// 查看是否有令牌项的cookie,有就说明登录成功了
if (c.name === this.ctx.tokenCookieName) success = true
this.ctx.cookies.set(c.name, c.value, {
httpOnly: c.httpOnly,
domain: '',
path: c.path,
expires: c.expires
})
})
// set base64(username) in cookie
this.ctx.cookies.set('vn', btoa(username), {
httpOnly: true
})
// @step7 设置API返回结果
return {
result: success,
msg: success ? 'ok' : '登录失败,请确认用户名密码无误',
data: {
username: username
}
}
}
/**
* 获取签到的once值
* @member
* @param {String} content - 签到页html字符串
*/
getSigninOnce (content) {
// update this.once
const onceRe = /redeem\?once=(\d+)/
const onces = onceRe.exec(content)
/* istanbul ignore next */
if (onces && onces[1]) {
this.once = onces[1]
} else {
this.once = null
this.hasSignin = true // 已经签到过了才获取不到once
}
}
/**
* 进入签到页面,获取once值
* @member
* @params {Object} {headers} - 复用请求头
*/
async enterSigninPage ({headers}) {
const result = await this.request(this.signinUrl, {
method: 'GET',
dataType: 'text',
headers: headers
})
// 权限不足,应该是没登录
/* istanbul ignore else */
if (result.status === 302) {
this.noAuth = true
}
// 进行解析
this.getSigninOnce(result.data)
return
}
/**
* 签到领金币
* @method
*/
async signin () {
// @step1 获取客户端凭证和各种cookies
const session = this.ctx.sessionid
const token = this.ctx.token
const { tab, others } = this.ctx.commonCookies
// @step2 设置Headers
// 逻辑上应该不改变 `commonHeaders` 本身,不过由于 `ctx.[getter]` 的特性,每次get都是新对象,可以不加 `{}`
const headers = Object.assign({}, this.ctx.commonHeaders,
{ Referer: 'https://www.v2ex.com/mission/daily' },
{ Cookie: `${session}; ${token}; ${tab}; ${others};` }
)
// @step3 进入签到页面,获取once
await this.enterSigninPage({headers})
// @step4 设置请求选项
const opts = {
dataType: 'text',
method: 'get'
}
// @step5 获取请求结果,会302
/* istanbul ignore else */
if (this.once === null) {
this.ctx.logger.info('未获取到once值,可能是已经签到过了!')
}
const result = await this.request(`${this.signinUrl}/redeem`, Object.assign(opts, {
headers: headers,
data: {
'once': this.once
}
}))
// @step6 重新进入签到页面,根据抓取到的文案确认是否签到成功(文案见底部附录@Notes)
const page = await this.request(`${this.signinUrl}`, Object.assign(opts, {
headers: Object.assign({}, headers, { Referer: 'https://www.v2ex.com/mission/daily/redeem' })
}))
// @step7 设置API返回值
// const htmlContent = page?.data
const htmlContent = page && page.data
const daysMatch = page.data.match(/(已连续登录.*)<\/div>/)
const days = daysMatch && daysMatch[1]
/* istanbul ignore else */
if (this.noAuth === true) {
return {
result: false,
msg: '请先登录再签到'
}
}
/* istanbul ignore next */
if (htmlContent.includes('已成功领取每日登录奖励')) {
return {
result: true,
msg: 'ok',
data: {
username: this.ctx.helper.getCurrentUserName(),
detail: days || 'ok'
}
}
}
/* istanbul ignore next */
if (this.hasSignin) {
return {
result: false,
msg: '今天已经签到了',
data: {
username: this.ctx.helper.getCurrentUserName(),
detail: days || 'ok'
}
}
}
/* istanbul ignore next */
return {
result: false,
msg: '签到遇到未知错误',
detail: app.config.env === 'prod' ? '' : page
}
} // method#signin
} // /.class=>AuthService
} // /.exports
/**
* @Notes
* 附录: 签到后302到原页面返回文案的比较:
*
* 没登录(无合法cookie)就签到
* data: ''
*
* 签到成功
* 已成功领取每日登录奖励
* 每日登录奖励已领取
* 查看我的账户余额
* 已连续登录x天
*
*
* 今日已签到
* 请重新点击一次以领取每日登录奖励(用这个总感觉不太保险...)
* 下面三个和签到成功一样...
* 每日登录奖励已领取
* 查看我的账户余额
* 已连续登录x天
*
*/