初始提交:短剧库源码(含转码优化 ultrafast)
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func readAccountRequest(writer http.ResponseWriter, request *http.Request, input any) bool {
|
||||
if !playbackRequestAllowed(writer, request, http.MethodPost) {
|
||||
return false
|
||||
}
|
||||
if request.Header.Get("X-Juku-Viewer") == "" {
|
||||
writeJSON(writer, http.StatusForbidden, map[string]string{"error": "请从剧库账号面板操作"})
|
||||
return false
|
||||
}
|
||||
contentType, _, _ := mime.ParseMediaType(request.Header.Get("Content-Type"))
|
||||
if contentType != "application/json" {
|
||||
writeJSON(writer, http.StatusUnsupportedMediaType, map[string]string{"error": "账号请求必须使用 JSON"})
|
||||
return false
|
||||
}
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(writer, request.Body, 4096))
|
||||
decoder.DisallowUnknownFields()
|
||||
if decoder.Decode(input) != nil || decoder.Decode(new(any)) != io.EOF {
|
||||
writeJSON(writer, http.StatusBadRequest, map[string]string{"error": "账号请求无效"})
|
||||
return false
|
||||
}
|
||||
return requestViewer(writer, request) != nil
|
||||
}
|
||||
|
||||
func accountOperationError(writer http.ResponseWriter, err error) {
|
||||
writeJSON(writer, http.StatusInternalServerError, map[string]string{"error": "账号操作未完成:" + publicError(err).Error()})
|
||||
}
|
||||
|
||||
func accountHashPermit(writer http.ResponseWriter, request *http.Request, store *accountStore, name string) bool {
|
||||
if store.err != nil {
|
||||
accountOperationError(writer, store.err)
|
||||
return false
|
||||
}
|
||||
if !store.allowAttempt(name, request.RemoteAddr, time.Now()) {
|
||||
writer.Header().Set("Retry-After", "60")
|
||||
writeJSON(writer, http.StatusTooManyRequests, map[string]string{"error": "账号尝试次数较多,请一分钟后重试"})
|
||||
return false
|
||||
}
|
||||
select {
|
||||
case store.hashing <- struct{}{}:
|
||||
return true
|
||||
default:
|
||||
writer.Header().Set("Retry-After", "2")
|
||||
writeJSON(writer, http.StatusTooManyRequests, map[string]string{"error": "正在处理其他登录请求,请稍后重试"})
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func accountSecureRequest(request *http.Request) bool {
|
||||
return request.TLS != nil || strings.EqualFold(request.Header.Get("X-Forwarded-Proto"), "https")
|
||||
}
|
||||
|
||||
func (manager *viewerManager) setAccountCookie(writer http.ResponseWriter, request *http.Request, token string) {
|
||||
maxAge, expires := int(accountLifetime/time.Second), time.Now().Add(accountLifetime)
|
||||
if token == "" {
|
||||
maxAge, expires = -1, time.Unix(1, 0)
|
||||
}
|
||||
http.SetCookie(writer, &http.Cookie{Name: manager.accountCookieName(), Value: token, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode,
|
||||
Secure: accountSecureRequest(request), MaxAge: maxAge, Expires: expires})
|
||||
}
|
||||
|
||||
func (manager *viewerManager) setGuestCookie(writer http.ResponseWriter, request *http.Request, identity string) {
|
||||
expires := time.Now().Add(viewerLifetime)
|
||||
http.SetCookie(writer, &http.Cookie{Name: manager.cookie, Value: manager.token(identity, expires), Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode,
|
||||
Secure: accountSecureRequest(request), MaxAge: int(viewerLifetime / time.Second), Expires: expires})
|
||||
}
|
||||
|
||||
func (app *UIApp) handleAccountRegister(writer http.ResponseWriter, request *http.Request) {
|
||||
var input struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if !readAccountRequest(writer, request, &input) {
|
||||
return
|
||||
}
|
||||
manager := app.browserViewers()
|
||||
if account, _, _, _ := manager.requestAccount(request); account.ID != "" {
|
||||
writeJSON(writer, http.StatusConflict, map[string]string{"error": "请先退出当前账号再注册"})
|
||||
return
|
||||
}
|
||||
name, display, err := normalizeAccountUsername(input.Username)
|
||||
if err == nil {
|
||||
err = validateAccountPassword(input.Password)
|
||||
}
|
||||
if err != nil {
|
||||
writeJSON(writer, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
store := manager.accountStore()
|
||||
if _, allowed := store.policy(); !allowed {
|
||||
writeJSON(writer, http.StatusForbidden, map[string]string{"code": "registration_disabled", "error": "管理员已关闭注册,请联系管理员创建账号"})
|
||||
return
|
||||
}
|
||||
if !accountHashPermit(writer, request, store, name) {
|
||||
return
|
||||
}
|
||||
defer func() { <-store.hashing }()
|
||||
salt, hash, err := newAccountPassword(input.Password)
|
||||
if err != nil {
|
||||
accountOperationError(writer, err)
|
||||
return
|
||||
}
|
||||
identifier := randomHex(32)
|
||||
if identifier == "" {
|
||||
accountOperationError(writer, errors.New("无法创建账号,请重试"))
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
account := accountRecord{ID: identifier, Username: display, Salt: salt, PasswordHash: hash, Algorithm: "pbkdf2-sha256", Iterations: accountPasswordIterations, CreatedAt: now}
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
if !store.state.AllowRegistration {
|
||||
writeJSON(writer, http.StatusForbidden, map[string]string{"code": "registration_disabled", "error": "管理员已关闭注册,请联系管理员创建账号"})
|
||||
return
|
||||
}
|
||||
if _, exists := store.state.Accounts[name]; exists {
|
||||
writeJSON(writer, http.StatusConflict, map[string]string{"error": "该用户名已被使用"})
|
||||
return
|
||||
}
|
||||
if len(store.state.Accounts) >= accountLimit {
|
||||
writeJSON(writer, http.StatusConflict, map[string]string{"error": "此服务的账号数量已达上限"})
|
||||
return
|
||||
}
|
||||
state := store.snapshotLocked()
|
||||
state.Accounts[name] = account
|
||||
token, err := state.addSession(name, now)
|
||||
if err == nil {
|
||||
err = store.saveLocked(state)
|
||||
}
|
||||
if err != nil {
|
||||
accountOperationError(writer, err)
|
||||
return
|
||||
}
|
||||
manager.setAccountCookie(writer, request, token)
|
||||
writeJSON(writer, http.StatusCreated, map[string]any{"ok": true, "account": account.public()})
|
||||
}
|
||||
|
||||
func (app *UIApp) handleAccountLogin(writer http.ResponseWriter, request *http.Request) {
|
||||
var input struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if !readAccountRequest(writer, request, &input) {
|
||||
return
|
||||
}
|
||||
manager := app.browserViewers()
|
||||
if account, _, _, _ := manager.requestAccount(request); account.ID != "" {
|
||||
writeJSON(writer, http.StatusConflict, map[string]string{"error": "请先退出当前账号再切换账号"})
|
||||
return
|
||||
}
|
||||
name, _, nameErr := normalizeAccountUsername(input.Username)
|
||||
if nameErr != nil || validateAccountPassword(input.Password) != nil {
|
||||
writeJSON(writer, http.StatusUnauthorized, map[string]string{"error": "用户名或密码不正确"})
|
||||
return
|
||||
}
|
||||
store := manager.accountStore()
|
||||
if !accountHashPermit(writer, request, store, name) {
|
||||
return
|
||||
}
|
||||
defer func() { <-store.hashing }()
|
||||
store.mu.Lock()
|
||||
account, found := store.state.Accounts[name]
|
||||
store.mu.Unlock()
|
||||
if !found {
|
||||
dummySalt := manager.signature("account-password-dummy")
|
||||
account = accountRecord{Salt: hex.EncodeToString(dummySalt[:16]), PasswordHash: strings.Repeat("0", 64), Iterations: accountPasswordIterations}
|
||||
}
|
||||
valid := verifyAccountPassword(account, input.Password)
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
current := store.state.Accounts[name]
|
||||
if !valid || !found || current.ID != account.ID || current.PasswordHash != account.PasswordHash || current.Salt != account.Salt {
|
||||
writeJSON(writer, http.StatusUnauthorized, map[string]string{"error": "用户名或密码不正确"})
|
||||
return
|
||||
}
|
||||
state := store.snapshotLocked()
|
||||
token, err := state.addSession(name, time.Now())
|
||||
if err == nil {
|
||||
err = store.saveLocked(state)
|
||||
}
|
||||
if err != nil {
|
||||
accountOperationError(writer, err)
|
||||
return
|
||||
}
|
||||
manager.setAccountCookie(writer, request, token)
|
||||
writeJSON(writer, http.StatusOK, map[string]any{"ok": true, "account": account.public()})
|
||||
}
|
||||
|
||||
func (app *UIApp) handleAccountLogout(writer http.ResponseWriter, request *http.Request) {
|
||||
var input struct{}
|
||||
if !readAccountRequest(writer, request, &input) {
|
||||
return
|
||||
}
|
||||
manager := app.browserViewers()
|
||||
guest := randomHex(32)
|
||||
if guest == "" {
|
||||
accountOperationError(writer, errors.New("无法建立新的浏览器身份,请重试"))
|
||||
return
|
||||
}
|
||||
if cookie, err := request.Cookie(manager.accountCookieName()); err == nil {
|
||||
store := manager.accountStore()
|
||||
store.mu.Lock()
|
||||
state := store.snapshotLocked()
|
||||
delete(state.Sessions, accountTokenHash(cookie.Value))
|
||||
err = store.saveLocked(state)
|
||||
store.mu.Unlock()
|
||||
if err != nil {
|
||||
accountOperationError(writer, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
manager.setAccountCookie(writer, request, "")
|
||||
manager.setGuestCookie(writer, request, guest)
|
||||
writeJSON(writer, http.StatusOK, map[string]bool{"ok": true})
|
||||
}
|
||||
|
||||
func (app *UIApp) handleAccountPassword(writer http.ResponseWriter, request *http.Request) {
|
||||
var input struct {
|
||||
Password string `json:"password"`
|
||||
NewPassword string `json:"newPassword"`
|
||||
}
|
||||
if !readAccountRequest(writer, request, &input) {
|
||||
return
|
||||
}
|
||||
manager := app.browserViewers()
|
||||
account, name, _, _ := manager.requestAccount(request)
|
||||
if account.ID == "" {
|
||||
writeJSON(writer, http.StatusUnauthorized, map[string]string{"error": "请先登录账号"})
|
||||
return
|
||||
}
|
||||
if account.RequirePasswordChange && input.NewPassword == input.Password {
|
||||
writeJSON(writer, http.StatusBadRequest, map[string]string{"error": "新密码不能与随机初始密码相同"})
|
||||
return
|
||||
}
|
||||
if err := validateAccountPassword(input.NewPassword); err != nil {
|
||||
writeJSON(writer, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if validateAccountPassword(input.Password) != nil {
|
||||
writeJSON(writer, http.StatusUnauthorized, map[string]string{"error": "当前密码不正确"})
|
||||
return
|
||||
}
|
||||
store := manager.accountStore()
|
||||
if !accountHashPermit(writer, request, store, name) {
|
||||
return
|
||||
}
|
||||
defer func() { <-store.hashing }()
|
||||
if !verifyAccountPassword(account, input.Password) {
|
||||
writeJSON(writer, http.StatusUnauthorized, map[string]string{"error": "当前密码不正确"})
|
||||
return
|
||||
}
|
||||
salt, hash, err := newAccountPassword(input.NewPassword)
|
||||
if err != nil {
|
||||
accountOperationError(writer, err)
|
||||
return
|
||||
}
|
||||
cookie, _ := request.Cookie(manager.accountCookieName())
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
current := store.state.Accounts[name]
|
||||
if current.PasswordHash != account.PasswordHash || current.Salt != account.Salt || cookie == nil || store.state.Sessions[accountTokenHash(cookie.Value)].Account != name {
|
||||
writeViewerError(writer, http.StatusUnauthorized, "viewer_required", "账号状态已变化,请重新登录")
|
||||
return
|
||||
}
|
||||
state := store.snapshotLocked()
|
||||
account.Salt, account.PasswordHash, account.Iterations = salt, hash, accountPasswordIterations
|
||||
account.RequirePasswordChange = false
|
||||
state.Accounts[name] = account
|
||||
for key, session := range state.Sessions {
|
||||
if session.Account == name {
|
||||
delete(state.Sessions, key)
|
||||
}
|
||||
}
|
||||
token, err := state.addSession(name, time.Now())
|
||||
if err == nil {
|
||||
err = store.saveLocked(state)
|
||||
}
|
||||
if err != nil {
|
||||
accountOperationError(writer, err)
|
||||
return
|
||||
}
|
||||
manager.setAccountCookie(writer, request, token)
|
||||
writeJSON(writer, http.StatusOK, map[string]bool{"ok": true})
|
||||
}
|
||||
Reference in New Issue
Block a user