diff --git a/.gitignore b/.gitignore index 3950d5f8fe..d552a0d012 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..8295e0d584 100644 --- a/app.js +++ b/app.js @@ -16,6 +16,8 @@ const lectures = require("./routes/lectures"); const github = require("./routes/github"); const fallback = require("./routes/redirect"); const my = require("./routes/my"); +const account = require("./routes/account"); +const submit = require("./routes/submit"); const mockUserInfo = require("./middleware/mockUserInfo"); // error handler @@ -79,6 +81,8 @@ 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(account.routes(), my.allowedMethods()); +app.use(submit.routes(), github.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..6f12e809fa 100644 --- a/config/index.js +++ b/config/index.js @@ -396,6 +396,9 @@ const userList = [ ); module.exports = { + baseUrl: 'https://leetcode-cn.com', + submitUrl: 'https://leetcode-cn.com/problems/$slug/submit/', + loginUrl: 'https://leetcode-cn.com/accounts/login/', owner: "leetcode-pp", repo: "91alg-4", startTime: startTime.getTime(), diff --git a/routes/account.js b/routes/account.js new file mode 100644 index 0000000000..a1edd3aad6 --- /dev/null +++ b/routes/account.js @@ -0,0 +1,54 @@ +// 用户上传账号名与密码 +const router = require("koa-router")(); + +const { + set91Cookie, + getLeetcodeCookie, + success, + fail +} = require('./utils') + +// 设置响应头 +router.options('/submitLcAccount', async (ctx, next) => { + ctx.set("Access-Control-Allow-Origin", "http://localhost:8080"); + ctx.set("Access-Control-Allow-Credentials", "true"); + ctx.set("Access-Control-Allow-Methods", "*"); + ctx.set("Access-Control-Allow-Headers", "Content-Type,Access-Token"); + ctx.set("Access-Control-Expose-Headers", "*"); + ctx.body = 200; + ctx.status = 200; +}); + +// 设置响应头 +router.post('/submitLcAccount', async (ctx, next) => { + ctx.set("Access-Control-Allow-Origin", "http://localhost:8080"); + ctx.set("Access-Control-Allow-Credentials", "true"); + ctx.set("Access-Control-Allow-Methods", "*"); + ctx.set("Access-Control-Allow-Headers", "Content-Type,Access-Token"); + ctx.set("Access-Control-Expose-Headers", "*"); + await next(); +}); + +// 先提交给lc,账号密码是否正确 +router.post('/submitLcAccount', async (ctx, next) => { + const {login, password} = ctx.request.body + let result = await getLeetcodeCookie({login, password}) + if(result.success){ + // todo密码正确时,对密码进行加密 + + // 将加密后的密文 以及 sessionId、 csrftoken写入cookie中 + LEETCODE_SESSION = result.LEETCODE_SESSION + csrftoken = result.csrftoken + set91Cookie({ + login, + password, + csrftoken, + LEETCODE_SESSION + }, ctx) + ctx.body = success({isLogin: true}) + } else { + ctx.body = fail() + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/submit.js b/routes/submit.js new file mode 100644 index 0000000000..18bd0f8c52 --- /dev/null +++ b/routes/submit.js @@ -0,0 +1,139 @@ +// 提交题解 +const router = require("koa-router")(); +const request = require('request'); + +const { + set91Cookie, + getLeetcodeCookie, + success, + fail +} = require('./utils') +const { submitUrl, baseUrl } = require('../config/index') + +// 设置响应头 +router.options('/submitCode', async (ctx, next) => { + ctx.set("Access-Control-Allow-Origin", "http://localhost:8080"); + ctx.set("Access-Control-Allow-Credentials", "true"); + ctx.set("Access-Control-Allow-Methods", "*"); + ctx.set("Access-Control-Allow-Headers", "Content-Type,Access-Token"); + ctx.set("Access-Control-Expose-Headers", "*"); + ctx.body = 200; + ctx.status = 200; +}); + +// 设置响应头 +router.post('/submitCode', async (ctx, next) => { + ctx.set("Access-Control-Allow-Origin", "http://localhost:8080"); + ctx.set("Access-Control-Allow-Credentials", "true"); + ctx.set("Access-Control-Allow-Methods", "*"); + ctx.set("Access-Control-Allow-Headers", "Content-Type,Access-Token"); + ctx.set("Access-Control-Expose-Headers", "*"); + await next(); +}); + + +// 用户提交题解 +// 先校验cookie中是否有账号密码,没有就让用户先输入再提交 +router.post('/submitCode', async (ctx, next) => { + const userName = ctx.cookies.get('login') + const passwd = ctx.cookies.get('password') + if(!userName || !passwd){ + return ctx.response.body = fail({ + message: "请先提交账号名与密码后再提交" + }); + } + await next(); +}); + +// 先试着用cookie中的csrftoken提交下看是否成功, +router.post('/submitCode', async (ctx, next) => { + const login = ctx.cookies.get('login') + const password = ctx.cookies.get('password') + let LEETCODE_SESSION = ctx.cookies.get('LEETCODE_SESSION') + let csrftoken = ctx.cookies.get('csrftoken') + const problemData = formateSubmitData(ctx.request.body) + + // 如果这俩cookie有一个不存在就拿账号密码再去lc请求一个新的 + if(!LEETCODE_SESSION || !csrftoken){ + const newCookie = await getLeetcodeCookie({login, password}) + LEETCODE_SESSION = newCookie.LEETCODE_SESSION + csrftoken = newCookie.csrftoken + set91Cookie({ + csrftoken, + LEETCODE_SESSION + }, ctx) + } + + // 先试着用cookie中的csrftoken提交下看是否成功, + let result = await submitSolution(problemData, ctx) + if(result.success){ + set91Cookie({ + csrftoken, + LEETCODE_SESSION + }, ctx) + return ctx.body = success(result) + } + + // 如果403就用账号密码获取最新csrftoken,再提交一遍 + let { statusCode } = result + if(statusCode === 403){ + const newCookie = await getLeetcodeCookie({login, password}) + LEETCODE_SESSION = newCookie.LEETCODE_SESSION + csrftoken = newCookie.csrftoken + + let retryResult = await submitSolution(problemData, ctx) + if(retryResult.success){ + set91Cookie({ + csrftoken, + LEETCODE_SESSION + }, ctx) + return ctx.body = success(retryResult) + } + } + + // 如果还是失败,就提示用户重新输入账号名与密码 + return ctx.response.body = fail({ + message: "提交失败,请重新输入账号名与密码后再提交!" + }); +}); + +function submitSolution(problem, ctx){ + const LEETCODE_SESSION = ctx.cookies.get('LEETCODE_SESSION') + const csrftoken = ctx.cookies.get('csrftoken') + const opts = {} + opts.method = 'POST'; + opts.url = submitUrl.replace('$slug', problem.slug); + opts.headers = {}; + opts.headers.Origin = baseUrl; + opts.headers.Referer = problem.link; + opts.headers.Cookie = `LEETCODE_SESSION=${LEETCODE_SESSION};csrftoken=${csrftoken};`; + opts.headers['X-csrftoken'] = csrftoken; + opts.headers['X-Requested-With'] = 'XMLHttpRequest'; + opts.json = true; + opts._delay = 1; // in seconds + opts.body = problem || {}; + + return new Promise(res => { + request(opts, function(e, resp, body) { + if(e){ + return res({success: false, statusCode: resp.statusCode}) + } + body.success = true + body.statusCode = resp.statusCode + return res(body) + }); + }) +} + +// 整理提交题解时的数据格式 +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..3d483bad8e --- /dev/null +++ b/routes/utils.js @@ -0,0 +1,88 @@ +const request = require('request'); +const { baseUrl, loginUrl } = require('../config/index') + +// 设置cookie +function set91Cookie (data, ctx){ + for(let key in data){ + ctx.cookies.set(key, data[key], { + maxAge: 365 * 24 * 60 * 60 * 1000, + httpOnly: true + }) + } +} + +// 从leetcode的请求中获取cookie值 +function getDataFromLcResponse(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两个cookie +function getLeetcodeCookie({login, password}){ + return new Promise((resolve, reject) => { + // 先发前置请求获取csrftoken + request(loginUrl, function(e, resp, body) { + let csrftoken = getDataFromLcResponse(resp, 'csrftoken'); + let LEETCODE_SESSION + const opts = { + url: loginUrl, + headers: { + Origin: baseUrl, + Referer: loginUrl, + Cookie: `csrftoken=${csrftoken};` + }, + form: { + csrfmiddlewaretoken: csrftoken, + login, + password, + } + }; + + // 再发一次请求获取csrftoken、LEETCODE_SESSION + request.post(opts, function(e, resp, body) { + if (resp.statusCode !== 302) { + return resolve({success: false, message: 'pwd invaid'}) + } + LEETCODE_SESSION = getDataFromLcResponse(resp, 'LEETCODE_SESSION'); + csrftoken = getDataFromLcResponse(resp, 'csrftoken'); + resolve({ + success: true, + LEETCODE_SESSION, + csrftoken + }) + }); + }); + }) +} + +function success(data) { + return { + success: true, + data, + }; +} + +function fail({ message, code = 10001 }) { + return { + success: false, + data: null, + message, + code, + }; +} +module.exports = { + set91Cookie, + getDataFromLcResponse, + getLeetcodeCookie, + success, + fail +}