Skip to content

Commit

Permalink
feat: support specifiying multiple credential sources (#320)
Browse files Browse the repository at this point in the history
* feat: `leetcode.credentials.from` supports multiple sources

* Add doc
  • Loading branch information
j178 authored Dec 28, 2024
1 parent 6abcf66 commit 5c4974f
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 24 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,8 @@ leetcode:
# Credentials to access LeetCode.
credentials:
# How to provide credentials: browser, cookies, password or none.
from: browser
from:
- browser
# Browsers to get cookies from: chrome, safari, edge or firefox. If empty, all browsers will be tried. Only used when 'from' is 'browser'.
browsers: []
contest:
Expand Down Expand Up @@ -322,6 +323,11 @@ There are three ways to make cookies available to `leetgo`:
from: password
```

> [!TIP]
> You can specify which browser to read cookies from, e.g. `browsers: [chrome]`.
> You can specify multiple authentication methods, `leetgo` will try them in order, e.g. `from: [browser, cookies]`.
> You can put all the environment variables in a `.env` file in the project's root directory, `leetgo` will automatically read them.

> [!NOTE]
> Password authentication is not recommended, and it is not supported by `leetcode.com`.

Expand Down
14 changes: 11 additions & 3 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,8 @@ leetcode:
# Credentials to access LeetCode.
credentials:
# How to provide credentials: browser, cookies, password or none.
from: browser
from:
- browser
# Browsers to get cookies from: chrome, safari, edge or firefox. If empty, all browsers will be tried. Only used when 'from' is 'browser'.
browsers: []
contest:
Expand Down Expand Up @@ -296,6 +297,10 @@ editor:
from: browser
```

> [!IMPORTANT]
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.
You would need to provide cookies manually or use other browsers.

- 手动提供 Cookie

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

> [!TIP]
> 你可以指定读取哪个浏览器的 Cookie,比如 `browsers: [chrome]`。
> 你可以指定多种方式,`leetgo` 会按照顺序尝试,比如 `from: [browser, cookies]`。
> 你可以将 `LEETCODE_XXX` 等环境变量放到项目根目录的 `.env` 文件中,`leetgo` 会自动读取这个文件。

> [!NOTE]
> 不推荐使用用户名密码的认证方式, 而且 `leetcode.com` (美国站) 也不支持用户名密码登录.

你可以将这些环境变量放到项目跟目录的 `.env` 文件中,`leetgo` 会自动读取这个文件。

## 进阶用法

### `testcases.txt` 相关
Expand Down
33 changes: 26 additions & 7 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,27 @@ type RustConfig struct {
}

type Credentials struct {
From string `yaml:"from" mapstructure:"from" comment:"How to provide credentials: browser, cookies, password or none."`
From []string `yaml:"from" mapstructure:"from" comment:"How to provide credentials: browser, cookies, password or none."`
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'."`
}

func (c *Credentials) UnmarshalYAML(node *yaml.Node) error {
// Compatibility with old `from` field, which is a string.
var from string
if err := node.Decode(&from); err == nil {
c.From = []string{from}
return nil
}

type credentials Credentials
var cred credentials
if err := node.Decode(&cred); err != nil {
return err
}
*c = Credentials(cred)
return nil
}

type LeetCodeConfig struct {
Site LeetcodeSite `yaml:"site" mapstructure:"site" comment:"LeetCode site, https://leetcode.com or https://leetcode.cn"`
Credentials Credentials `yaml:"credentials" mapstructure:"credentials" comment:"Credentials to access LeetCode."`
Expand Down Expand Up @@ -230,7 +247,7 @@ func defaultConfig() *Config {
LeetCode: LeetCodeConfig{
Site: LeetCodeCN,
Credentials: Credentials{
From: "browser",
From: []string{"browser"},
},
},
Editor: Editor{
Expand Down Expand Up @@ -276,11 +293,13 @@ func verify(c *Config) error {
return fmt.Errorf("invalid `leetcode.site` value: %s", c.LeetCode.Site)
}

if !credentialFrom[c.LeetCode.Credentials.From] {
return fmt.Errorf("invalid `leetcode.credentials.from` value: %s", c.LeetCode.Credentials.From)
}
if c.LeetCode.Credentials.From == "password" && c.LeetCode.Site == LeetCodeUS {
return errors.New("username/password authentication is not supported for leetcode.com")
for _, from := range c.LeetCode.Credentials.From {
if !credentialFrom[from] {
return fmt.Errorf("invalid `leetcode.credentials.from` value: %s", from)
}
if from == "password" && c.LeetCode.Site == LeetCodeUS {
return errors.New("username/password authentication is not supported for leetcode.com")
}
}

if c.Editor.Args != "" {
Expand Down
90 changes: 77 additions & 13 deletions leetcode/credential.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
)

type CredentialsProvider interface {
Source() string
AddCredentials(req *http.Request) error
}

Expand All @@ -37,6 +38,10 @@ func NonAuth() CredentialsProvider {
return &nonAuth{}
}

func (n *nonAuth) Source() string {
return "none"
}

func (n *nonAuth) AddCredentials(req *http.Request) error {
return errors.New("no credentials provided")
}
Expand All @@ -53,6 +58,10 @@ func NewCookiesAuth(session, csrftoken, cfClearance string) CredentialsProvider
return &cookiesAuth{LeetCodeSession: session, CsrfToken: csrftoken, CfClearance: cfClearance}
}

func (c *cookiesAuth) Source() string {
return "cookies"
}

func (c *cookiesAuth) AddCredentials(req *http.Request) error {
if !c.hasAuth() {
return errors.New("cookies not found")
Expand Down Expand Up @@ -83,6 +92,10 @@ func NewPasswordAuth(username, passwd string) CredentialsProvider {
return &passwordAuth{username: username, password: passwd}
}

func (p *passwordAuth) Source() string {
return "password"
}

func (p *passwordAuth) SetClient(c Client) {
p.c = c
}
Expand Down Expand Up @@ -135,6 +148,10 @@ func NewBrowserAuth(browsers []string) CredentialsProvider {
return &browserAuth{browsers: browsers}
}

func (b *browserAuth) Source() string {
return "browser"
}

func (b *browserAuth) SetClient(c Client) {
b.c = c
}
Expand Down Expand Up @@ -207,21 +224,68 @@ func (b *browserAuth) Reset() {
b.CsrfToken = ""
}

type combinedAuth struct {
providers []CredentialsProvider
}

func NewCombinedAuth(providers ...CredentialsProvider) CredentialsProvider {
return &combinedAuth{providers: providers}
}

func (c *combinedAuth) Source() string {
return "combined sources"
}

func (c *combinedAuth) AddCredentials(req *http.Request) error {
for _, p := range c.providers {
if err := p.AddCredentials(req); err == nil {
return nil
} else {
log.Debug("read credentials from %s failed: %v", p.Source(), err)
}
}
return errors.New("no credentials provided")
}

func (c *combinedAuth) SetClient(client Client) {
for _, p := range c.providers {
if r, ok := p.(NeedClient); ok {
r.SetClient(client)
}
}
}

func (c *combinedAuth) Reset() {
for _, p := range c.providers {
if r, ok := p.(ResettableProvider); ok {
r.Reset()
}
}
}

func ReadCredentials() CredentialsProvider {
cfg := config.Get()
switch cfg.LeetCode.Credentials.From {
case "browser":
return NewBrowserAuth(cfg.LeetCode.Credentials.Browsers)
case "password":
username := os.Getenv("LEETCODE_USERNAME")
password := os.Getenv("LEETCODE_PASSWORD")
return NewPasswordAuth(username, password)
case "cookies":
session := os.Getenv("LEETCODE_SESSION")
csrfToken := os.Getenv("LEETCODE_CSRFTOKEN")
cfClearance := os.Getenv("LEETCODE_CFCLEARANCE")
return NewCookiesAuth(session, csrfToken, cfClearance)
default:
var providers []CredentialsProvider
for _, from := range cfg.LeetCode.Credentials.From {
switch from {
case "browser":
providers = append(providers, NewBrowserAuth(cfg.LeetCode.Credentials.Browsers))
case "password":
username := os.Getenv("LEETCODE_USERNAME")
password := os.Getenv("LEETCODE_PASSWORD")
providers = append(providers, NewPasswordAuth(username, password))
case "cookies":
session := os.Getenv("LEETCODE_SESSION")
csrfToken := os.Getenv("LEETCODE_CSRFTOKEN")
cfClearance := os.Getenv("LEETCODE_CFCLEARANCE")
providers = append(providers, NewCookiesAuth(session, csrfToken, cfClearance))
}
}
if len(providers) == 0 {
return NonAuth()
}
if len(providers) == 1 {
return providers[0]
}
return NewCombinedAuth(providers...)
}

0 comments on commit 5c4974f

Please sign in to comment.