diff --git a/.gitignore b/.gitignore index 3950d5f8fe..4f99117a4b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ - +/.idea /node_modules -/static/**/*.md \ No newline at end of file +/static/**/*.md diff --git a/app.js b/app.js index 297258f72b..017247e7d0 100644 --- a/app.js +++ b/app.js @@ -16,6 +16,7 @@ const lectures = require("./routes/lectures"); const github = require("./routes/github"); const fallback = require("./routes/redirect"); const my = require("./routes/my"); +const lc = require("./routes/lc"); const mockUserInfo = require("./middleware/mockUserInfo"); // error handler @@ -79,6 +80,7 @@ app.use(lectures.routes(), lectures.allowedMethods()); app.use(dailyProblem.routes(), dailyProblem.allowedMethods()); app.use(my.routes(), my.allowedMethods()); app.use(github.routes(), github.allowedMethods()); +app.use(lc.routes(), lc.allowedMethods()); // error-handling app.on("error", (err, ctx) => { console.error("server error", err, ctx); diff --git a/config/index.js b/config/index.js index f1c0c841cc..4a07bbc0e3 100644 --- a/config/index.js +++ b/config/index.js @@ -395,7 +395,18 @@ const userList = [ })) ); +const leetcodeConfig = { + baseUrl: 'https://leetcode-cn.com', + submitUrl: 'https://leetcode-cn.com/problems/$slug/submit/', + loginUrl: 'https://leetcode-cn.com/accounts/login/', + _91UsernameCookieName: 'login', // 在91网站中存lc用户名的cookie的键名 + _91PwdCookieName: 'password', // 在91网站中存lc密码的cookie的键名 + lcSeesionCookieName: 'LEETCODE_SESSION', // lc存seesionid的 cookie键名 + lcCsrftokenCookieName: 'csrftoken' // lc存csrf的 cookie键名 +} + module.exports = { + leetcodeConfig, owner: "leetcode-pp", repo: "91alg-4", startTime: startTime.getTime(), diff --git a/routes/lc.js b/routes/lc.js new file mode 100644 index 0000000000..19f3a598a0 --- /dev/null +++ b/routes/lc.js @@ -0,0 +1,154 @@ + +const router = require("koa-router")(); +const fetch = require("node-fetch"); +const { + leetcodeConfig: { + baseUrl, + submitUrl, + _91UsernameCookieName, + _91PwdCookieName, + lcSeesionCookieName, + lcCsrftokenCookieName + } +} = require('../config/index') +const { success, fail } = require('../utils/request') +const { encrypt, decrypt } = require('../utils/crypto') +const { set91Cookie, getLcRequestData } = require('./utils') + +// 用户上传lc的账号名与密码 +router.post('/api/v1/lc/submitLcAccount', async (ctx) => { + const { login, password } = ctx.request.body + // 先提交给lc,账号密码是否正确 + let result = await getLcRequestData({login, password}) + if(result.success){ + // 密码正确时,对密码进行加密 + let encryptPwd = encrypt(password) + // 将加密后的密文 以及 sessionId、 csrftoken 写入cookie中 + sessionId = result[lcSeesionCookieName] + csrftoken = result[lcCsrftokenCookieName] + set91Cookie({ + [_91UsernameCookieName]: login, + [_91PwdCookieName]: encryptPwd, + [lcSeesionCookieName]: sessionId, + [lcCsrftokenCookieName]: csrftoken, + }, ctx) + ctx.body = success({isLogin: true}) + } else { + ctx.body = fail({code: 302, message: '提交失败' }) + } +}); + +// 用户提交题解 +router.post('/api/v1/lc/submitCode', async (ctx, next) => { + // 先校验cookie中是否有账号密码,没有就让用户先输入再提交 + const userName = ctx.cookies.get(_91UsernameCookieName) + const passwd = ctx.cookies.get(_91PwdCookieName) + if(!userName || !passwd){ + return ctx.response.body = fail({ + code: 403, + message: "请先提交账号名与密码后再提交" + }); + } + + // 如果这俩cookie有一个不存在就提示用户重新提交一遍lc的账号密码 + let sessionId = ctx.cookies.get(lcSeesionCookieName) + let csrftoken = ctx.cookies.get(lcCsrftokenCookieName) + let requestData = { + [lcSeesionCookieName]: sessionId, + [lcCsrftokenCookieName]: csrftoken + } + if(!sessionId || !csrftoken){ + return ctx.response.body = fail({ + code: 403, + message: "提交失败,请重新输入账号名与密码后再提交!" + }); + } + + // 先试着用cookie中的旧csrftoken提交下看是否成功 + const problemData = formateSubmitData(ctx.request.body) + let result = (await submitSolution(problemData, requestData)) || {} + if(result.success){ + return ctx.body = success(result.data) + } + + // 如果403就用账号密码获取最新csrftoken,再提交一遍 + if(result.statusCode === 403){ + // 获取最新csrftoken + let newRequestData = await getLatestLcRequestData(ctx) + if(!newRequestData.success){ + return ctx.response.body = fail({ + code: 403, + message: newRequestData.message || "提交失败,请重新输入账号名与密码后再提交!" + }); + } + // 更新下91的cookie + set91Cookie(newRequestData, ctx) + + // 再提交一遍 + let retryResult = await submitSolution(problemData, newRequestData) + if(retryResult.success){ + return ctx.body = success(retryResult.data) + } + } + + // 如果还是失败,就提示用户重新输入账号名与密码 + return ctx.response.body = fail({ + code: 403, + message: "提交失败,请重新输入账号名与密码后再提交!" + }); +}); + +// 获取最新的的向leetcode发送请求的必要参数 +async function getLatestLcRequestData(ctx){ + const userName = ctx.cookies.get(_91UsernameCookieName) + const encryptPassword = ctx.cookies.get(_91PwdCookieName) + const password = decrypt(encryptPassword) + return await getLcRequestData({ + [_91UsernameCookieName]: userName, + [_91PwdCookieName]: password + }) +} + +async function submitSolution(problem, requestData){ + let statusCode = 403 + const url = submitUrl.replace('$slug', problem.slug); + const sessionId = requestData[lcSeesionCookieName] + const csrftoken = requestData[lcCsrftokenCookieName] + const cookie = `${lcSeesionCookieName}=${sessionId};${lcCsrftokenCookieName}=${csrftoken};` + const opt = { + method: 'POST', + headers: { + Origin: baseUrl, + Referer: problem.link, + Cookie: cookie, + 'X-csrftoken': csrftoken, + 'X-Requested-With': 'XMLHttpRequest', + }, + json: true, + _delay: 1,// in seconds + body: JSON.stringify(problem || {}) + } + console.log(opt) + const result = await fetch(url, opt).then((res) => { + statusCode = res.status + return res.json() + }); + return { + success: !!result, + statusCode: statusCode, + data: result + } +} + +// 整理提交题解时的数据格式 +function formateSubmitData(problem = {}){ + return Object.assign(problem, { + judge_type: 'large', + lang: problem.lang, + question_id: parseInt(problem.id, 10), + test_mode: false, + typed_code: problem.code + }) +} + +module.exports = router; diff --git a/routes/utils.js b/routes/utils.js new file mode 100644 index 0000000000..db4c5be43f --- /dev/null +++ b/routes/utils.js @@ -0,0 +1,73 @@ +const fetch = require("node-fetch"); +const request = require('request'); +const { + leetcodeConfig: { + baseUrl, + loginUrl, + _91UsernameCookieName, // 在91网站中存lc用户名的cookie的键名 + _91PwdCookieName, // 在91网站中存lc密码的cookie的键名 + lcSeesionCookieName, // lc存seesionid的 cookie键名 + lcCsrftokenCookieName // lc存csrf的 cookie键名 + } +} = require('../config/index') + +// 设置91的cookie并且过期时间为一年 +function set91Cookie (data, ctx){ + for(let key in data){ + ctx.cookies.set(key, data[key], { + maxAge: 365 * 24 * 60 * 60 * 1000, + httpOnly: false + }) + } +} + +// 从leetcode的请求中获取cookie值 +function getCookieFromLc(resp, key) { + const cookies = resp.headers['set-cookie']; + if (!cookies) return null; + + for (let i = 0; i < cookies.length; ++i) { + const sections = cookies[i].split(';'); + for (let j = 0; j < sections.length; ++j) { + const kv = sections[j].trim().split('='); + if (kv[0] === key) return kv[1]; + } + } + return null; +}; + +// 从leetcode中获取 发送请求的必要参数 LEETCODE_SESSION、csrftoken +async function getLcRequestData(options){ + const opt = { + url: loginUrl, + credentials: 'include', + headers: { + credentials: 'include', + Origin: baseUrl, + Referer: loginUrl, + }, + form: { + [_91UsernameCookieName]: options[_91UsernameCookieName], + [_91PwdCookieName]: options[_91PwdCookieName] + } + } + return await new Promise(resolve => { + request.post(opt, function(e, resp, body) { + if (resp.statusCode !== 302) { + return resolve({success: false, message: 'pwd invaid'}) + } + sessionId = getCookieFromLc(resp, lcSeesionCookieName); + csrftoken = getCookieFromLc(resp, lcCsrftokenCookieName); + resolve({ + success: true, + [lcSeesionCookieName]: sessionId, + [lcCsrftokenCookieName]:csrftoken + }) + }); + }) +} + +module.exports = { + set91Cookie, + getLcRequestData +}