Skip to content

Commit 5c4974f

Browse files
authored
feat: support specifiying multiple credential sources (#320)
* feat: `leetcode.credentials.from` supports multiple sources * Add doc
1 parent 6abcf66 commit 5c4974f

File tree

4 files changed

+121
-24
lines changed

4 files changed

+121
-24
lines changed

Diff for: README.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,8 @@ leetcode:
258258
# Credentials to access LeetCode.
259259
credentials:
260260
# How to provide credentials: browser, cookies, password or none.
261-
from: browser
261+
from:
262+
- browser
262263
# Browsers to get cookies from: chrome, safari, edge or firefox. If empty, all browsers will be tried. Only used when 'from' is 'browser'.
263264
browsers: []
264265
contest:
@@ -322,6 +323,11 @@ There are three ways to make cookies available to `leetgo`:
322323
from: password
323324
```
324325

326+
> [!TIP]
327+
> You can specify which browser to read cookies from, e.g. `browsers: [chrome]`.
328+
> You can specify multiple authentication methods, `leetgo` will try them in order, e.g. `from: [browser, cookies]`.
329+
> You can put all the environment variables in a `.env` file in the project's root directory, `leetgo` will automatically read them.
330+
325331
> [!NOTE]
326332
> Password authentication is not recommended, and it is not supported by `leetcode.com`.
327333

Diff for: README_zh.md

+11-3
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,8 @@ leetcode:
254254
# Credentials to access LeetCode.
255255
credentials:
256256
# How to provide credentials: browser, cookies, password or none.
257-
from: browser
257+
from:
258+
- browser
258259
# Browsers to get cookies from: chrome, safari, edge or firefox. If empty, all browsers will be tried. Only used when 'from' is 'browser'.
259260
browsers: []
260261
contest:
@@ -296,6 +297,10 @@ editor:
296297
from: browser
297298
```
298299

300+
> [!IMPORTANT]
301+
On Windows, Chrome/Edge v127 enabled [App-Bound Encryption](https://security.googleblog.com/2024/07/improving-security-of-chrome-cookies-on.html) and `leetgo` can no longer decrypt cookies from Chrome/Edge.
302+
You would need to provide cookies manually or use other browsers.
303+
299304
- 手动提供 Cookie
300305

301306
你需要打开 LeetCode 页面,从浏览器的 DevTools 中获取 `LEETCODE_SESSION` 和 `csrftoken` 这两个 Cookie 的值,设置为 `LEETCODE_SESSION` 和 `LEETCODE_CSRFTOKEN` 环境变量。如果你在使用 `leetcode.com`, 你还需要设置 `LEETCODE_CFCLEARANCE` 为 `cf_clearance` cookie 的值。
@@ -314,11 +319,14 @@ editor:
314319
from: password
315320
```
316321

322+
> [!TIP]
323+
> 你可以指定读取哪个浏览器的 Cookie,比如 `browsers: [chrome]`。
324+
> 你可以指定多种方式,`leetgo` 会按照顺序尝试,比如 `from: [browser, cookies]`。
325+
> 你可以将 `LEETCODE_XXX` 等环境变量放到项目根目录的 `.env` 文件中,`leetgo` 会自动读取这个文件。
326+
317327
> [!NOTE]
318328
> 不推荐使用用户名密码的认证方式, 而且 `leetcode.com` (美国站) 也不支持用户名密码登录.
319329

320-
你可以将这些环境变量放到项目跟目录的 `.env` 文件中,`leetgo` 会自动读取这个文件。
321-
322330
## 进阶用法
323331

324332
### `testcases.txt` 相关

Diff for: config/config.go

+26-7
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,27 @@ type RustConfig struct {
111111
}
112112

113113
type Credentials struct {
114-
From string `yaml:"from" mapstructure:"from" comment:"How to provide credentials: browser, cookies, password or none."`
114+
From []string `yaml:"from" mapstructure:"from" comment:"How to provide credentials: browser, cookies, password or none."`
115115
Browsers []string `yaml:"browsers" mapstructure:"browsers" comment:"Browsers to get cookies from: chrome, safari, edge or firefox. If empty, all browsers will be tried. Only used when 'from' is 'browser'."`
116116
}
117117

118+
func (c *Credentials) UnmarshalYAML(node *yaml.Node) error {
119+
// Compatibility with old `from` field, which is a string.
120+
var from string
121+
if err := node.Decode(&from); err == nil {
122+
c.From = []string{from}
123+
return nil
124+
}
125+
126+
type credentials Credentials
127+
var cred credentials
128+
if err := node.Decode(&cred); err != nil {
129+
return err
130+
}
131+
*c = Credentials(cred)
132+
return nil
133+
}
134+
118135
type LeetCodeConfig struct {
119136
Site LeetcodeSite `yaml:"site" mapstructure:"site" comment:"LeetCode site, https://leetcode.com or https://leetcode.cn"`
120137
Credentials Credentials `yaml:"credentials" mapstructure:"credentials" comment:"Credentials to access LeetCode."`
@@ -230,7 +247,7 @@ func defaultConfig() *Config {
230247
LeetCode: LeetCodeConfig{
231248
Site: LeetCodeCN,
232249
Credentials: Credentials{
233-
From: "browser",
250+
From: []string{"browser"},
234251
},
235252
},
236253
Editor: Editor{
@@ -276,11 +293,13 @@ func verify(c *Config) error {
276293
return fmt.Errorf("invalid `leetcode.site` value: %s", c.LeetCode.Site)
277294
}
278295

279-
if !credentialFrom[c.LeetCode.Credentials.From] {
280-
return fmt.Errorf("invalid `leetcode.credentials.from` value: %s", c.LeetCode.Credentials.From)
281-
}
282-
if c.LeetCode.Credentials.From == "password" && c.LeetCode.Site == LeetCodeUS {
283-
return errors.New("username/password authentication is not supported for leetcode.com")
296+
for _, from := range c.LeetCode.Credentials.From {
297+
if !credentialFrom[from] {
298+
return fmt.Errorf("invalid `leetcode.credentials.from` value: %s", from)
299+
}
300+
if from == "password" && c.LeetCode.Site == LeetCodeUS {
301+
return errors.New("username/password authentication is not supported for leetcode.com")
302+
}
284303
}
285304

286305
if c.Editor.Args != "" {

Diff for: leetcode/credential.go

+77-13
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
)
2121

2222
type CredentialsProvider interface {
23+
Source() string
2324
AddCredentials(req *http.Request) error
2425
}
2526

@@ -37,6 +38,10 @@ func NonAuth() CredentialsProvider {
3738
return &nonAuth{}
3839
}
3940

41+
func (n *nonAuth) Source() string {
42+
return "none"
43+
}
44+
4045
func (n *nonAuth) AddCredentials(req *http.Request) error {
4146
return errors.New("no credentials provided")
4247
}
@@ -53,6 +58,10 @@ func NewCookiesAuth(session, csrftoken, cfClearance string) CredentialsProvider
5358
return &cookiesAuth{LeetCodeSession: session, CsrfToken: csrftoken, CfClearance: cfClearance}
5459
}
5560

61+
func (c *cookiesAuth) Source() string {
62+
return "cookies"
63+
}
64+
5665
func (c *cookiesAuth) AddCredentials(req *http.Request) error {
5766
if !c.hasAuth() {
5867
return errors.New("cookies not found")
@@ -83,6 +92,10 @@ func NewPasswordAuth(username, passwd string) CredentialsProvider {
8392
return &passwordAuth{username: username, password: passwd}
8493
}
8594

95+
func (p *passwordAuth) Source() string {
96+
return "password"
97+
}
98+
8699
func (p *passwordAuth) SetClient(c Client) {
87100
p.c = c
88101
}
@@ -135,6 +148,10 @@ func NewBrowserAuth(browsers []string) CredentialsProvider {
135148
return &browserAuth{browsers: browsers}
136149
}
137150

151+
func (b *browserAuth) Source() string {
152+
return "browser"
153+
}
154+
138155
func (b *browserAuth) SetClient(c Client) {
139156
b.c = c
140157
}
@@ -207,21 +224,68 @@ func (b *browserAuth) Reset() {
207224
b.CsrfToken = ""
208225
}
209226

227+
type combinedAuth struct {
228+
providers []CredentialsProvider
229+
}
230+
231+
func NewCombinedAuth(providers ...CredentialsProvider) CredentialsProvider {
232+
return &combinedAuth{providers: providers}
233+
}
234+
235+
func (c *combinedAuth) Source() string {
236+
return "combined sources"
237+
}
238+
239+
func (c *combinedAuth) AddCredentials(req *http.Request) error {
240+
for _, p := range c.providers {
241+
if err := p.AddCredentials(req); err == nil {
242+
return nil
243+
} else {
244+
log.Debug("read credentials from %s failed: %v", p.Source(), err)
245+
}
246+
}
247+
return errors.New("no credentials provided")
248+
}
249+
250+
func (c *combinedAuth) SetClient(client Client) {
251+
for _, p := range c.providers {
252+
if r, ok := p.(NeedClient); ok {
253+
r.SetClient(client)
254+
}
255+
}
256+
}
257+
258+
func (c *combinedAuth) Reset() {
259+
for _, p := range c.providers {
260+
if r, ok := p.(ResettableProvider); ok {
261+
r.Reset()
262+
}
263+
}
264+
}
265+
210266
func ReadCredentials() CredentialsProvider {
211267
cfg := config.Get()
212-
switch cfg.LeetCode.Credentials.From {
213-
case "browser":
214-
return NewBrowserAuth(cfg.LeetCode.Credentials.Browsers)
215-
case "password":
216-
username := os.Getenv("LEETCODE_USERNAME")
217-
password := os.Getenv("LEETCODE_PASSWORD")
218-
return NewPasswordAuth(username, password)
219-
case "cookies":
220-
session := os.Getenv("LEETCODE_SESSION")
221-
csrfToken := os.Getenv("LEETCODE_CSRFTOKEN")
222-
cfClearance := os.Getenv("LEETCODE_CFCLEARANCE")
223-
return NewCookiesAuth(session, csrfToken, cfClearance)
224-
default:
268+
var providers []CredentialsProvider
269+
for _, from := range cfg.LeetCode.Credentials.From {
270+
switch from {
271+
case "browser":
272+
providers = append(providers, NewBrowserAuth(cfg.LeetCode.Credentials.Browsers))
273+
case "password":
274+
username := os.Getenv("LEETCODE_USERNAME")
275+
password := os.Getenv("LEETCODE_PASSWORD")
276+
providers = append(providers, NewPasswordAuth(username, password))
277+
case "cookies":
278+
session := os.Getenv("LEETCODE_SESSION")
279+
csrfToken := os.Getenv("LEETCODE_CSRFTOKEN")
280+
cfClearance := os.Getenv("LEETCODE_CFCLEARANCE")
281+
providers = append(providers, NewCookiesAuth(session, csrfToken, cfClearance))
282+
}
283+
}
284+
if len(providers) == 0 {
225285
return NonAuth()
226286
}
287+
if len(providers) == 1 {
288+
return providers[0]
289+
}
290+
return NewCombinedAuth(providers...)
227291
}

0 commit comments

Comments
 (0)