Skip to content

Commit

Permalink
[feature] #41 实现 WBI 签名
Browse files Browse the repository at this point in the history
  • Loading branch information
RunsTp committed Apr 12, 2024
1 parent a2b8d09 commit 336665b
Show file tree
Hide file tree
Showing 5 changed files with 369 additions and 3 deletions.
7 changes: 6 additions & 1 deletion client.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package bilibili

import (
"github.com/go-resty/resty/v2"
"net/http"
"strings"
"time"

"github.com/go-resty/resty/v2"
)

type Client struct {
Expand Down Expand Up @@ -50,6 +51,10 @@ func (c *Client) SetCookiesString(cookiesString string) {
}}}).Cookies())
}

func (c *Client) GetCookies() []*http.Cookie {
return c.resty.Cookies
}

// 根据key获取指定的cookie值
func (c *Client) getCookie(name string) string {
now := time.Now()
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/pkg/errors v0.9.1
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/spf13/cast v1.6.0
golang.org/x/sync v0.7.0
)

require (
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,14 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand All @@ -53,7 +54,6 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
Expand Down
303 changes: 303 additions & 0 deletions wbi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
package bilibili

import (
"crypto/md5"

Check failure on line 4 in wbi.go

View workflow job for this annotation

GitHub Actions / build

G501: Blocklisted import crypto/md5: weak cryptographic primitive (gosec)
"encoding/hex"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"sync"
"time"

"github.com/go-resty/resty/v2"
"github.com/pkg/errors"
"golang.org/x/sync/singleflight"
)

const (
cacheImgKey = "imgKey"
cacheSubKey = "subKey"
)

var (
_defaultMixinKeyEncTab = []int{
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40,
61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11,
36, 20, 34, 44, 52,
}

_defaultStorage = &MemoryStorage{
data: make(map[string]interface{}, 15),
}
)

type Storage interface {
Set(key string, value interface{})
Get(key string) (v interface{}, isSet bool)
}

type MemoryStorage struct {
data map[string]interface{}
mu sync.RWMutex
}

func (impl *MemoryStorage) Set(key string, value interface{}) {
impl.mu.Lock()
defer impl.mu.Unlock()

impl.data[key] = value
}

func (impl *MemoryStorage) Get(key string) (v interface{}, isSet bool) {
impl.mu.RLock()
defer impl.mu.RUnlock()

if v, isSet = impl.data[key]; isSet {
return v, true
}
return nil, false
}

// WBI 签名实现
// 如果希望以登录的方式获取则使用 WithCookies or WithRawCookies 设置cookie
// 如果希望以未登录的方式获取 WithCookies(nil) 设置cookie为 nil 即可, 这是 Default 行为
//
// !!! 使用 WBI 的接口 绝对不可以 set header Referer 会导致失败 !!!
// !!! 大部分使用 WBI 的接口都需要 set header Cookie !!!
//
// see https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/misc/sign/wbi.md
type WBI struct {
cookies []*http.Cookie
mixinKeyEncTab []int

// updateCheckerInterval is the interval to check and update wbi keys
// default is 60 minutes. so if lastInitTime + updateCheckerInterval < now, it will update wbi keys
updateCheckerInterval time.Duration
lastInitTime time.Time
storage Storage

sfg singleflight.Group
}

func NewDefaultWbi() *WBI {
return &WBI{
cookies: nil,
mixinKeyEncTab: _defaultMixinKeyEncTab,

updateCheckerInterval: 60 * time.Minute,
storage: _defaultStorage,
}
}

func (wbi *WBI) WithUpdateInterval(updateInterval time.Duration) *WBI {
wbi.updateCheckerInterval = updateInterval
return wbi
}

func (wbi *WBI) WithCookies(cookies []*http.Cookie) *WBI {
wbi.cookies = cookies
return wbi
}

func (wbi *WBI) WithRawCookies(rawCookies string) *WBI {
header := http.Header{}
header.Add("Cookie", rawCookies)
req := http.Request{Header: header}

wbi.cookies = req.Cookies()
return wbi
}

func (wbi *WBI) WithMixinKeyEncTab(mixinKeyEncTab []int) *WBI {
wbi.mixinKeyEncTab = mixinKeyEncTab
return wbi
}

func (wbi *WBI) WithStorage(storage Storage) *WBI {
wbi.storage = storage
return wbi
}

func (wbi *WBI) GetKeys() (imgKey string, subKey string, err error) {
imgKey, subKey = wbi.getKeys()

// 更新检查
if imgKey == "" || subKey == "" || time.Since(wbi.lastInitTime) > wbi.updateCheckerInterval {
if err = wbi.initWbi(); err != nil {
return "", "", err
}

return wbi.GetKeys()
}

return imgKey, subKey, nil
}

func (wbi *WBI) getKeys() (imgKey string, subKey string) {
if v, isSet := wbi.storage.Get(cacheImgKey); isSet {
imgKey = v.(string)
}

if v, isSet := wbi.storage.Get(cacheSubKey); isSet {
subKey = v.(string)
}

return imgKey, subKey
}

func (wbi *WBI) SetKeys(imgKey, subKey string) {
wbi.storage.Set(cacheImgKey, imgKey)
wbi.storage.Set(cacheSubKey, subKey)
wbi.lastInitTime = time.Now()
}

func (wbi *WBI) GetMixinKey() (string, error) {
imgKey, subKey, err := wbi.GetKeys()
if err != nil {
return "", err
}

return wbi.GenerateMixinKey(imgKey + subKey), nil
}

func (wbi *WBI) GenerateMixinKey(orig string) string {
var str strings.Builder
for _, v := range wbi.mixinKeyEncTab {
if v < len(orig) {
str.WriteByte(orig[v])
}
}
return str.String()[:32]
}

func (wbi *WBI) sanitizeString(s string) string {
unwantedChars := []string{"!", "'", "(", ")", "*"}
for _, char := range unwantedChars {
s = strings.ReplaceAll(s, char, "")
}
return s
}

func (wbi *WBI) SignQuery(query url.Values, ts time.Time) (newQuery url.Values, err error) {
payload := make(map[string]string, 10)
for k := range query {
payload[k] = query.Get(k)

}

newPayload, err := wbi.SignMap(payload, ts)
if err != nil {
return query, err
}

newQuery = url.Values{}
for k, v := range newPayload {
newQuery.Set(k, v)
}

return newQuery, nil
}

func (wbi *WBI) SignMap(payload map[string]string, ts time.Time) (newPayload map[string]string, err error) {
newPayload = make(map[string]string, 10)
for k, v := range payload {
newPayload[k] = v
}

newPayload["wts"] = strconv.FormatInt(ts.Unix(), 10)

// Sort keys
keys := make([]string, 0, 10)
for k := range newPayload {
keys = append(keys, k)
}

sort.Strings(keys)

// Remove unwanted characters
for k, v := range newPayload {
v = wbi.sanitizeString(v)
newPayload[k] = v
}

// Build URL parameters
signQuery := url.Values{}
for _, k := range keys {
signQuery.Set(k, newPayload[k])
}
signQueryStr := signQuery.Encode()

// Get mixin key
mixinKey, err := wbi.GetMixinKey()
if err != nil {
return payload, err
}

// Calculate w_rid
hash := md5.Sum([]byte(signQueryStr + mixinKey))

Check failure on line 240 in wbi.go

View workflow job for this annotation

GitHub Actions / build

G401: Use of weak cryptographic primitive (gosec)
newPayload["w_rid"] = hex.EncodeToString(hash[:])

return newPayload, nil
}

func (wbi *WBI) initWbi() error {
_, err, _ := wbi.sfg.Do("initWbi", func() (interface{}, error) {
return nil, wbi.doInitWbi()
})

if err != nil {
return err

Check failure on line 252 in wbi.go

View workflow job for this annotation

GitHub Actions / build

error returned from external package is unwrapped: sig: func (*golang.org/x/sync/singleflight.Group).Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) (wrapcheck)
}

return nil
}

func (wbi *WBI) doInitWbi() error {
result := struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
WbiImg struct {
ImgUrl string `json:"img_url"`
SubUrl string `json:"sub_url"`
} `json:"wbi_img"`
}
}{}

resp, err := resty.New().R().
SetHeader("Accept", "application/json").
SetHeader("Accept-Language", "zh-CN,zh;q=0.9").
SetHeader("Origin", "https://www.bilibili.com").
SetHeader("Referer", "https://www.bilibili.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0").
SetCookies(wbi.cookies).
SetResult(&result).
Get("https://api.bilibili.com/x/web-interface/nav")

if err != nil {
return errors.WithStack(err)
}
if resp.StatusCode() != http.StatusOK {
return errors.Errorf("status code: %d", resp.StatusCode())
}

if result.Code != 0 {
if result.Data.WbiImg.ImgUrl == "" || result.Data.WbiImg.SubUrl == "" {
return errors.Errorf("init wbi 失败, 错误码: %d, 错误信息: %s", result.Code, result.Message)
}
}

if len(resp.Cookies()) > 0 {
// update cookie
wbi.cookies = resp.Cookies()
}

imgKey := strings.Split(strings.Split(result.Data.WbiImg.ImgUrl, "/")[len(strings.Split(result.Data.WbiImg.ImgUrl, "/"))-1], ".")[0]
subKey := strings.Split(strings.Split(result.Data.WbiImg.SubUrl, "/")[len(strings.Split(result.Data.WbiImg.SubUrl, "/"))-1], ".")[0]

wbi.SetKeys(imgKey, subKey)
return nil
}
Loading

0 comments on commit 336665b

Please sign in to comment.