Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@

./idea
/node_modules
/static/**/*.md
/static/**/*.md
4 changes: 4 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,9 @@ const userList = [
);

module.exports = {
baseUrl: 'https://leetcode-cn.com',
submitUrl: 'https://leetcode-cn.com/problems/$slug/submit/',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. slug?
  2. 写一个 url (就是你的baseURL)就够了,其他你可以自己拼接。
  3. baseURL 建议改名为 LEETCODE_URL 之类

loginUrl: 'https://leetcode-cn.com/accounts/login/',
owner: "leetcode-pp",
repo: "91alg-4",
startTime: startTime.getTime(),
Expand Down
54 changes: 54 additions & 0 deletions routes/account.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// 用户上传账号名与密码
const router = require("koa-router")();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议不叫 account,目的和我的 user 区分。可以叫 LCAccount


const {
set91Cookie,
getLeetcodeCookie,
success,
fail
} = require('./utils')

// 设置响应头
router.options('/submitLcAccount', async (ctx, next) => {
ctx.set("Access-Control-Allow-Origin", "http://localhost:8080");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不需要自己设置跨域头,中间件设置好了

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密码正确时,对密码进行加密
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

加密方法我写好了,直接调用 utils 下的即可


// 将加密后的密文 以及 sessionId、 csrftoken写入cookie中
LEETCODE_SESSION = result.LEETCODE_SESSION
csrftoken = result.csrftoken
set91Cookie({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

使用 koa 内置的 setCookie 参考我的user 接口

login,
password,
csrftoken,
LEETCODE_SESSION
}, ctx)
ctx.body = success({isLogin: true})
} else {
ctx.body = fail()
}
});

module.exports = router;
139 changes: 139 additions & 0 deletions routes/submit.js
Original file line number Diff line number Diff line change
@@ -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: "请先提交账号名与密码后再提交"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fail 加 code

});
}
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')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cookie.get() 建议提出常量

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

变量不要全大写

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议用 fetch。这样一致一点,参考我的 middleware 代码

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;
88 changes: 88 additions & 0 deletions routes/utils.js
Original file line number Diff line number Diff line change
@@ -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], {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

为啥不直接 set 而是 for?

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetch

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 }) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

success 和 fail 我已经写了,你咋又写一遍

return {
success: false,
data: null,
message,
code,
};
}
module.exports = {
set91Cookie,
getDataFromLcResponse,
getLeetcodeCookie,
success,
fail
}