diff --git a/server/handlers/init.go b/server/handlers/init.go index eb8bf90..2a81f72 100644 --- a/server/handlers/init.go +++ b/server/handlers/init.go @@ -147,9 +147,17 @@ func Init(e *gin.Engine) { { bilibili := vendor.Group("/bilibili") - bilibili.GET("/qr", Vbilibili.QRCode) + login := bilibili.Group("/login") - bilibili.POST("/login", Vbilibili.Login) + login.GET("/qr", Vbilibili.NewQRCode) + + login.POST("/qr", Vbilibili.LoginWithQR) + + login.GET("/captcha", Vbilibili.NewCaptcha) + + login.POST("/sms/send", Vbilibili.NewSMS) + + login.POST("/sms/login", Vbilibili.LoginWithSMS) bilibili.POST("/parse", Vbilibili.Parse) diff --git a/server/handlers/movie.go b/server/handlers/movie.go index 56c9bdd..6b8c0f6 100644 --- a/server/handlers/movie.go +++ b/server/handlers/movie.go @@ -518,7 +518,10 @@ func parse2VendorMovie(userID string, movie *dbModel.Movie, getUrl bool) (err er if err != nil { return err } - cli := bilibili.NewClient(vendor.Cookies) + cli, err := bilibili.NewClient(vendor.Cookies) + if err != nil { + return err + } if getUrl { var mu *bilibili.VideoURL diff --git a/server/handlers/vendors/bilibili/bilibili.go b/server/handlers/vendors/bilibili/bilibili.go index 5725325..2e0caaf 100644 --- a/server/handlers/vendors/bilibili/bilibili.go +++ b/server/handlers/vendors/bilibili/bilibili.go @@ -14,54 +14,6 @@ import ( "github.com/synctv-org/synctv/vendors/bilibili" ) -func QRCode(ctx *gin.Context) { - r, err := bilibili.NewQRCode() - if err != nil { - ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) - return - } - ctx.JSON(http.StatusOK, model.NewApiDataResp(r)) -} - -type LoginReq struct { - Key string `json:"key"` -} - -func (r *LoginReq) Validate() error { - if r.Key == "" { - return errors.New("key is empty") - } - return nil -} - -func (r *LoginReq) Decode(ctx *gin.Context) error { - return json.NewDecoder(ctx.Request.Body).Decode(r) -} - -func Login(ctx *gin.Context) { - user := ctx.MustGet("user").(*op.User) - - req := LoginReq{} - if err := model.Decode(ctx, &req); err != nil { - ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) - return - } - - cookie, err := bilibili.Login(req.Key) - if err != nil { - ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) - return - } - - _, err = db.AssignFirstOrCreateVendorByUserIDAndVendor(user.ID, dbModel.StreamingVendorBilibili, db.WithCookie([]*http.Cookie{cookie})) - if err != nil { - ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) - return - } - - ctx.Status(http.StatusNoContent) -} - type ParseReq struct { URL string `json:"url"` } @@ -97,7 +49,11 @@ func Parse(ctx *gin.Context) { ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) return } - cli := bilibili.NewClient(vendor.Cookies) + cli, err := bilibili.NewClient(vendor.Cookies) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } switch matchType { case "bv": diff --git a/server/handlers/vendors/bilibili/login.go b/server/handlers/vendors/bilibili/login.go new file mode 100644 index 0000000..fc8ae89 --- /dev/null +++ b/server/handlers/vendors/bilibili/login.go @@ -0,0 +1,157 @@ +package Vbilibili + +import ( + "errors" + "net/http" + + "github.com/gin-gonic/gin" + json "github.com/json-iterator/go" + "github.com/synctv-org/synctv/internal/db" + dbModel "github.com/synctv-org/synctv/internal/model" + "github.com/synctv-org/synctv/internal/op" + "github.com/synctv-org/synctv/server/model" + "github.com/synctv-org/synctv/vendors/bilibili" +) + +func NewQRCode(ctx *gin.Context) { + r, err := bilibili.NewQRCode() + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + ctx.JSON(http.StatusOK, model.NewApiDataResp(r)) +} + +type QRCodeLoginReq struct { + Key string `json:"key"` +} + +func (r *QRCodeLoginReq) Validate() error { + if r.Key == "" { + return errors.New("key is empty") + } + return nil +} + +func (r *QRCodeLoginReq) Decode(ctx *gin.Context) error { + return json.NewDecoder(ctx.Request.Body).Decode(r) +} + +func LoginWithQR(ctx *gin.Context) { + user := ctx.MustGet("user").(*op.User) + + req := QRCodeLoginReq{} + if err := model.Decode(ctx, &req); err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + cookie, err := bilibili.LoginWithQRCode(req.Key) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + + _, err = db.AssignFirstOrCreateVendorByUserIDAndVendor(user.ID, dbModel.StreamingVendorBilibili, db.WithCookie([]*http.Cookie{cookie})) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + + ctx.Status(http.StatusNoContent) +} + +func NewCaptcha(ctx *gin.Context) { + r, err := bilibili.NewCaptcha() + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + ctx.JSON(http.StatusOK, model.NewApiDataResp(r)) +} + +type SMSReq struct { + Token string `json:"token"` + Challenge string `json:"challenge"` + Validate_ string `json:"validate"` + Telephone string `json:"telephone"` +} + +func (r *SMSReq) Validate() error { + if r.Token == "" { + return errors.New("token is empty") + } + if r.Challenge == "" { + return errors.New("challenge is empty") + } + if r.Validate_ == "" { + return errors.New("validate is empty") + } + if r.Telephone == "" { + return errors.New("telephone is empty") + } + return nil +} + +func (r *SMSReq) Decode(ctx *gin.Context) error { + return json.NewDecoder(ctx.Request.Body).Decode(r) +} + +func NewSMS(ctx *gin.Context) { + var req SMSReq + if err := model.Decode(ctx, &req); err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + r, err := bilibili.NewSMS(req.Telephone, req.Token, req.Challenge, req.Validate_) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{ + "captchaKey": r, + })) +} + +type SMSLoginReq struct { + Telephone string `json:"telephone"` + CaptchaKey string `json:"captchaKey"` + Code string `json:"code"` +} + +func (r *SMSLoginReq) Validate() error { + if r.Telephone == "" { + return errors.New("telephone is empty") + } + if r.CaptchaKey == "" { + return errors.New("captchaKey is empty") + } + if r.Code == "" { + return errors.New("code is empty") + } + return nil +} + +func (r *SMSLoginReq) Decode(ctx *gin.Context) error { + return json.NewDecoder(ctx.Request.Body).Decode(r) +} + +func LoginWithSMS(ctx *gin.Context) { + var req SMSLoginReq + if err := model.Decode(ctx, &req); err != nil { + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err)) + return + } + c, err := bilibili.LoginWithSMS(req.Telephone, req.Code, req.CaptchaKey) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + user := ctx.MustGet("user").(*op.User) + _, err = db.AssignFirstOrCreateVendorByUserIDAndVendor(user.ID, dbModel.StreamingVendorBilibili, db.WithCookie([]*http.Cookie{c})) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + ctx.Status(http.StatusNoContent) +} diff --git a/server/handlers/vendors/bilibili/me.go b/server/handlers/vendors/bilibili/me.go index 19445e1..68e00e4 100644 --- a/server/handlers/vendors/bilibili/me.go +++ b/server/handlers/vendors/bilibili/me.go @@ -31,7 +31,12 @@ func Me(ctx *gin.Context) { })) return } - cli := bilibili.NewClient(vendor.Cookies) + cli, err := bilibili.NewClient(vendor.Cookies) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) + return + } + nav, err := cli.UserInfo() if err != nil { ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err)) diff --git a/vendors/bilibili/client.go b/vendors/bilibili/client.go index b8b0131..9f4c172 100644 --- a/vendors/bilibili/client.go +++ b/vendors/bilibili/client.go @@ -1,6 +1,7 @@ package bilibili import ( + "errors" "io" "net/http" @@ -10,6 +11,7 @@ import ( type Client struct { httpClient *http.Client cookies []*http.Cookie + buvid3 *http.Cookie } type ClientConfig func(*Client) @@ -20,15 +22,43 @@ func WithHttpClient(httpClient *http.Client) ClientConfig { } } -func NewClient(cookies []*http.Cookie, conf ...ClientConfig) *Client { - c := &Client{ +func NewClient(cookies []*http.Cookie, conf ...ClientConfig) (*Client, error) { + cli := &Client{ httpClient: http.DefaultClient, cookies: cookies, } for _, v := range conf { - v(c) + v(cli) } - return c + c, err := newBuvid3() + if err != nil { + return nil, err + } + cli.buvid3 = c + return cli, nil +} + +func newBuvid3() (*http.Cookie, error) { + req, err := http.NewRequest(http.MethodGet, "https://www.bilibili.com/", nil) + if err != nil { + return nil, err + } + req.Header.Set("User-Agent", utils.UA) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + for _, c := range resp.Cookies() { + if c.Name == "buvid3" { + return c, nil + } + } + return nil, errors.New("no buvid3 cookie") +} + +func (c *Client) SetCookies(cookies []*http.Cookie) { + c.cookies = cookies } type RequestConfig struct { @@ -64,6 +94,7 @@ func (c *Client) NewRequest(method, url string, body io.Reader, conf ...RequestO if err != nil { return nil, err } + req.AddCookie(c.buvid3) for _, cookie := range c.cookies { req.AddCookie(cookie) } diff --git a/vendors/bilibili/login.go b/vendors/bilibili/login.go index c4f1e22..4f55581 100644 --- a/vendors/bilibili/login.go +++ b/vendors/bilibili/login.go @@ -3,8 +3,11 @@ package bilibili import ( "fmt" "net/http" + "net/url" + "strings" json "github.com/json-iterator/go" + "github.com/synctv-org/synctv/utils" ) type RQCode struct { @@ -13,7 +16,11 @@ type RQCode struct { } func NewQRCode() (*RQCode, error) { - resp, err := http.Get("https://passport.bilibili.com/x/passport-login/web/qrcode/generate") + req, err := http.NewRequest(http.MethodGet, "https://passport.bilibili.com/x/passport-login/web/qrcode/generate", nil) + if err != nil { + return nil, err + } + resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } @@ -31,8 +38,13 @@ func NewQRCode() (*RQCode, error) { } // return SESSDATA cookie -func Login(key string) (*http.Cookie, error) { - resp, err := http.Get(fmt.Sprintf("https://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key=%s", key)) +func LoginWithQRCode(key string) (*http.Cookie, error) { + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://passport.bilibili.com/x/passport-login/web/qrcode/auth?oauthKey=%s", key), nil) + if err != nil { + return nil, err + } + req.Header.Set("User-Agent", utils.UA) + resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } @@ -44,3 +56,116 @@ func Login(key string) (*http.Cookie, error) { } return nil, fmt.Errorf("no SESSDATA cookie") } + +type CaptchaResp struct { + Token string `json:"token"` + Gt string `json:"gt"` + Challenge string `json:"challenge"` +} + +func NewCaptcha() (*CaptchaResp, error) { + req, err := http.NewRequest(http.MethodGet, "https://passport.bilibili.com/x/passport-login/captcha", nil) + if err != nil { + return nil, err + } + req.Header.Set("User-Agent", utils.UA) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + var captcha captcha + err = json.NewDecoder(resp.Body).Decode(&captcha) + if err != nil { + return nil, err + } + return &CaptchaResp{ + Token: captcha.Data.Token, + Gt: captcha.Data.Geetest.Gt, + Challenge: captcha.Data.Geetest.Challenge, + }, nil +} + +type captcha struct { + Code int `json:"code"` + Message string `json:"message"` + TTL int `json:"ttl"` + Data struct { + Type string `json:"type"` + Token string `json:"token"` + Geetest struct { + Challenge string `json:"challenge"` + Gt string `json:"gt"` + } `json:"geetest"` + Tencent struct { + Appid string `json:"appid"` + } `json:"tencent"` + } `json:"data"` +} + +type sms struct { + Code int `json:"code"` + Message string `json:"message"` + Data struct { + CaptchaKey string `json:"captcha_key"` + } `json:"data"` +} + +func NewSMS(tel, token, challenge, validate string) (captchaKey string, err error) { + buvid3, err := newBuvid3() + if err != nil { + return "", err + } + data := url.Values{} + data.Set("cid", "86") + data.Set("tel", tel) + data.Set("source", "main-fe-header") + data.Set("token", token) + data.Set("challenge", challenge) + data.Set("validate", validate) + data.Set("seccode", fmt.Sprintf("%s|jordan", validate)) + + req, err := http.NewRequest(http.MethodPost, "https://passport.bilibili.com/x/passport-login/web/sms/send", strings.NewReader(data.Encode())) + if err != nil { + return "", err + } + req.Header.Set("User-Agent", utils.UA) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.AddCookie(buvid3) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + var sms sms + err = json.NewDecoder(resp.Body).Decode(&sms) + if err != nil { + return "", err + } + return sms.Data.CaptchaKey, nil +} + +func LoginWithSMS(tel, code, captchaKey string) (*http.Cookie, error) { + data := url.Values{} + data.Set("cid", "86") + data.Set("tel", tel) + data.Set("code", code) + data.Set("source", "main-fe-header") + data.Set("captcha_key", captchaKey) + + req, err := http.NewRequest(http.MethodPost, "https://passport.bilibili.com/x/passport-login/web/login/sms", strings.NewReader(data.Encode())) + if err != nil { + return nil, err + } + req.Header.Set("User-Agent", utils.UA) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + for _, cookie := range resp.Cookies() { + if cookie.Name == "SESSDATA" { + return cookie, nil + } + } + return nil, fmt.Errorf("no SESSDATA cookie") +}