mirror of
https://github.com/AlistGo/alist.git
synced 2025-12-19 11:00:06 +08:00
- Implement automatic session token renewal every 6-9 minutes - Add validation for required SessionToken and Cookie fields in Init - Handle session expiration by calling renewToken on validation failure - Prevent storage failures due to MediaFire session timeouts Fixes session closure issues that occur after server restarts or extended periods.
627 lines
16 KiB
Go
627 lines
16 KiB
Go
package mediafire
|
|
|
|
/*
|
|
Package mediafire
|
|
Author: Da3zKi7<da3zki7@duck.com>
|
|
Date: 2025-09-11
|
|
|
|
D@' 3z K!7 - The King Of Cracking
|
|
*/
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/alist-org/alist/v3/drivers/base"
|
|
"github.com/alist-org/alist/v3/internal/driver"
|
|
"github.com/alist-org/alist/v3/internal/model"
|
|
"github.com/alist-org/alist/v3/internal/op"
|
|
"github.com/alist-org/alist/v3/pkg/utils"
|
|
)
|
|
|
|
func (d *Mediafire) getSessionToken(ctx context.Context) (string, error) {
|
|
tokenURL := d.hostBase + "/application/get_session_token.php"
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
req.Header.Set("Accept", "*/*")
|
|
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
|
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
|
|
req.Header.Set("Content-Length", "0")
|
|
req.Header.Set("Cookie", d.Cookie)
|
|
req.Header.Set("DNT", "1")
|
|
req.Header.Set("Origin", d.hostBase)
|
|
req.Header.Set("Priority", "u=1, i")
|
|
req.Header.Set("Referer", (d.hostBase + "/"))
|
|
req.Header.Set("Sec-Ch-Ua", d.secChUa)
|
|
req.Header.Set("Sec-Ch-Ua-Mobile", "?0")
|
|
req.Header.Set("Sec-Ch-Ua-Platform", d.secChUaPlatform)
|
|
req.Header.Set("Sec-Fetch-Dest", "empty")
|
|
req.Header.Set("Sec-Fetch-Mode", "cors")
|
|
req.Header.Set("Sec-Fetch-Site", "same-site")
|
|
req.Header.Set("User-Agent", d.userAgent)
|
|
//req.Header.Set("Connection", "keep-alive")
|
|
|
|
resp, err := base.HttpClient.Do(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
//fmt.Printf("getSessionToken :: Raw response: %s\n", string(body))
|
|
//fmt.Printf("getSessionToken :: Parsed response: %+v\n", resp)
|
|
|
|
var tokenResp struct {
|
|
Response struct {
|
|
SessionToken string `json:"session_token"`
|
|
} `json:"response"`
|
|
}
|
|
|
|
if resp.StatusCode == 200 {
|
|
if err := json.Unmarshal(body, &tokenResp); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if tokenResp.Response.SessionToken == "" {
|
|
return "", fmt.Errorf("empty session token received")
|
|
}
|
|
|
|
cookieMap := make(map[string]string)
|
|
for _, cookie := range resp.Cookies() {
|
|
cookieMap[cookie.Name] = cookie.Value
|
|
}
|
|
|
|
if len(cookieMap) > 0 {
|
|
|
|
var cookies []string
|
|
for name, value := range cookieMap {
|
|
cookies = append(cookies, fmt.Sprintf("%s=%s", name, value))
|
|
}
|
|
d.Cookie = strings.Join(cookies, "; ")
|
|
op.MustSaveDriverStorage(d)
|
|
|
|
//fmt.Printf("getSessionToken :: Captured cookies: %s\n", d.Cookie)
|
|
}
|
|
|
|
} else {
|
|
return "", fmt.Errorf("getSessionToken :: failed to get session token, status code: %d", resp.StatusCode)
|
|
}
|
|
|
|
d.SessionToken = tokenResp.Response.SessionToken
|
|
|
|
//fmt.Printf("Init :: Obtain Session Token %v", d.SessionToken)
|
|
|
|
op.MustSaveDriverStorage(d)
|
|
|
|
return d.SessionToken, nil
|
|
}
|
|
|
|
func (d *Mediafire) renewToken(_ context.Context) error {
|
|
query := map[string]string{
|
|
"session_token": d.SessionToken,
|
|
"response_format": "json",
|
|
}
|
|
|
|
var resp MediafireRenewTokenResponse
|
|
_, err := d.postForm("/user/renew_session_token.php", query, &resp)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to renew token: %w", err)
|
|
}
|
|
|
|
//fmt.Printf("getInfo :: Raw response: %s\n", string(body))
|
|
//fmt.Printf("getInfo :: Parsed response: %+v\n", resp)
|
|
|
|
if resp.Response.Result != "Success" {
|
|
return fmt.Errorf("MediaFire token renewal failed: %s", resp.Response.Result)
|
|
}
|
|
|
|
d.SessionToken = resp.Response.SessionToken
|
|
|
|
//fmt.Printf("Init :: Renew Session Token: %s", resp.Response.Result)
|
|
|
|
op.MustSaveDriverStorage(d)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *Mediafire) getFiles(ctx context.Context, folderKey string) ([]File, error) {
|
|
files := make([]File, 0)
|
|
hasMore := true
|
|
chunkNumber := 1
|
|
|
|
for hasMore {
|
|
resp, err := d.getFolderContent(ctx, folderKey, chunkNumber)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, folder := range resp.Folders {
|
|
files = append(files, File{
|
|
ID: folder.FolderKey,
|
|
Name: folder.Name,
|
|
Size: 0,
|
|
CreatedUTC: folder.CreatedUTC,
|
|
IsFolder: true,
|
|
})
|
|
}
|
|
|
|
for _, file := range resp.Files {
|
|
size, _ := strconv.ParseInt(file.Size, 10, 64)
|
|
files = append(files, File{
|
|
ID: file.QuickKey,
|
|
Name: file.Filename,
|
|
Size: size,
|
|
CreatedUTC: file.CreatedUTC,
|
|
IsFolder: false,
|
|
})
|
|
}
|
|
|
|
hasMore = resp.MoreChunks
|
|
chunkNumber++
|
|
}
|
|
|
|
return files, nil
|
|
}
|
|
|
|
func (d *Mediafire) getFolderContent(ctx context.Context, folderKey string, chunkNumber int) (*FolderContentResponse, error) {
|
|
|
|
foldersResp, err := d.getFolderContentByType(ctx, folderKey, "folders", chunkNumber)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
filesResp, err := d.getFolderContentByType(ctx, folderKey, "files", chunkNumber)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &FolderContentResponse{
|
|
Folders: foldersResp.Response.FolderContent.Folders,
|
|
Files: filesResp.Response.FolderContent.Files,
|
|
MoreChunks: foldersResp.Response.FolderContent.MoreChunks == "yes" || filesResp.Response.FolderContent.MoreChunks == "yes",
|
|
}, nil
|
|
}
|
|
|
|
func (d *Mediafire) getFolderContentByType(_ context.Context, folderKey, contentType string, chunkNumber int) (*MediafireResponse, error) {
|
|
data := map[string]string{
|
|
"session_token": d.SessionToken,
|
|
"response_format": "json",
|
|
"folder_key": folderKey,
|
|
"content_type": contentType,
|
|
"chunk": strconv.Itoa(chunkNumber),
|
|
"chunk_size": strconv.FormatInt(d.ChunkSize, 10),
|
|
"details": "yes",
|
|
"order_direction": d.OrderDirection,
|
|
"order_by": d.OrderBy,
|
|
"filter": "",
|
|
}
|
|
|
|
var resp MediafireResponse
|
|
_, err := d.postForm("/folder/get_content.php", data, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if resp.Response.Result != "Success" {
|
|
return nil, fmt.Errorf("MediaFire API error: %s", resp.Response.Result)
|
|
}
|
|
|
|
return &resp, nil
|
|
}
|
|
|
|
func (d *Mediafire) fileToObj(f File) *model.ObjThumb {
|
|
created, _ := time.Parse("2006-01-02T15:04:05Z", f.CreatedUTC)
|
|
|
|
var thumbnailURL string
|
|
if !f.IsFolder && f.ID != "" {
|
|
thumbnailURL = d.hostBase + "/convkey/acaa/" + f.ID + "3g.jpg"
|
|
}
|
|
|
|
return &model.ObjThumb{
|
|
Object: model.Object{
|
|
ID: f.ID,
|
|
//Path: "",
|
|
Name: f.Name,
|
|
Size: f.Size,
|
|
Modified: created,
|
|
Ctime: created,
|
|
IsFolder: f.IsFolder,
|
|
},
|
|
Thumbnail: model.Thumbnail{
|
|
Thumbnail: thumbnailURL,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (d *Mediafire) getForm(endpoint string, query map[string]string, resp interface{}) ([]byte, error) {
|
|
req := base.RestyClient.R()
|
|
|
|
req.SetQueryParams(query)
|
|
|
|
req.SetHeaders(map[string]string{
|
|
"Cookie": d.Cookie,
|
|
//"User-Agent": base.UserAgent,
|
|
"User-Agent": d.userAgent,
|
|
"Origin": d.appBase,
|
|
"Referer": d.appBase + "/",
|
|
})
|
|
|
|
// If response OK
|
|
if resp != nil {
|
|
req.SetResult(resp)
|
|
}
|
|
|
|
// Targets MediaFire API
|
|
res, err := req.Get(d.apiBase + endpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return res.Body(), nil
|
|
}
|
|
|
|
func (d *Mediafire) postForm(endpoint string, data map[string]string, resp interface{}) ([]byte, error) {
|
|
req := base.RestyClient.R()
|
|
|
|
req.SetFormData(data)
|
|
|
|
req.SetHeaders(map[string]string{
|
|
"Cookie": d.Cookie,
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
//"User-Agent": base.UserAgent,
|
|
"User-Agent": d.userAgent,
|
|
"Origin": d.appBase,
|
|
"Referer": d.appBase + "/",
|
|
})
|
|
|
|
// If response OK
|
|
if resp != nil {
|
|
req.SetResult(resp)
|
|
}
|
|
|
|
// Targets MediaFire API
|
|
res, err := req.Post(d.apiBase + endpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return res.Body(), nil
|
|
}
|
|
|
|
func (d *Mediafire) getDirectDownloadLink(_ context.Context, fileID string) (string, error) {
|
|
data := map[string]string{
|
|
"session_token": d.SessionToken,
|
|
"quick_key": fileID,
|
|
"link_type": "direct_download",
|
|
"response_format": "json",
|
|
}
|
|
|
|
var resp MediafireDirectDownloadResponse
|
|
_, err := d.getForm("/file/get_links.php", data, &resp)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if resp.Response.Result != "Success" {
|
|
return "", fmt.Errorf("MediaFire API error: %s", resp.Response.Result)
|
|
}
|
|
|
|
if len(resp.Response.Links) == 0 {
|
|
return "", fmt.Errorf("no download links found")
|
|
}
|
|
|
|
return resp.Response.Links[0].DirectDownload, nil
|
|
}
|
|
|
|
func (d *Mediafire) calculateSHA256(file *os.File) (string, error) {
|
|
hasher := sha256.New()
|
|
if _, err := file.Seek(0, 0); err != nil {
|
|
return "", err
|
|
}
|
|
if _, err := io.Copy(hasher, file); err != nil {
|
|
return "", err
|
|
}
|
|
return hex.EncodeToString(hasher.Sum(nil)), nil
|
|
}
|
|
|
|
func (d *Mediafire) uploadCheck(ctx context.Context, filename string, filesize int64, filehash, folderKey string) (*MediafireCheckResponse, error) {
|
|
|
|
actionToken, err := d.getActionToken(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get action token: %w", err)
|
|
}
|
|
|
|
query := map[string]string{
|
|
"session_token": actionToken, /* d.SessionToken */
|
|
"filename": filename,
|
|
"size": strconv.FormatInt(filesize, 10),
|
|
"hash": filehash,
|
|
"folder_key": folderKey,
|
|
"resumable": "yes",
|
|
"response_format": "json",
|
|
}
|
|
|
|
var resp MediafireCheckResponse
|
|
_, err = d.postForm("/upload/check.php", query, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
//fmt.Printf("uploadCheck :: Raw response: %s\n", string(body))
|
|
//fmt.Printf("uploadCheck :: Parsed response: %+v\n", resp)
|
|
|
|
//fmt.Printf("uploadCheck :: ResumableUpload section: %+v\n", resp.Response.ResumableUpload)
|
|
//fmt.Printf("uploadCheck :: Upload key specifically: '%s'\n", resp.Response.ResumableUpload.UploadKey)
|
|
|
|
if resp.Response.Result != "Success" {
|
|
return nil, fmt.Errorf("MediaFire upload check failed: %s", resp.Response.Result)
|
|
}
|
|
|
|
return &resp, nil
|
|
}
|
|
|
|
func (d *Mediafire) resumableUpload(ctx context.Context, folderKey, uploadKey string, unitData []byte, unitID int, fileHash, filename string, totalFileSize int64) (string, error) {
|
|
actionToken, err := d.getActionToken(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
url := d.apiBase + "/upload/resumable.php"
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(unitData))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
q := req.URL.Query()
|
|
q.Add("folder_key", folderKey)
|
|
q.Add("response_format", "json")
|
|
q.Add("session_token", actionToken)
|
|
q.Add("key", uploadKey)
|
|
req.URL.RawQuery = q.Encode()
|
|
|
|
req.Header.Set("x-filehash", fileHash)
|
|
req.Header.Set("x-filesize", strconv.FormatInt(totalFileSize, 10))
|
|
req.Header.Set("x-unit-id", strconv.Itoa(unitID))
|
|
req.Header.Set("x-unit-size", strconv.FormatInt(int64(len(unitData)), 10))
|
|
req.Header.Set("x-unit-hash", d.sha256Hex(bytes.NewReader(unitData)))
|
|
req.Header.Set("x-filename", filename)
|
|
req.Header.Set("Content-Type", "application/octet-stream")
|
|
req.ContentLength = int64(len(unitData))
|
|
|
|
/* fmt.Printf("Debug resumable upload request:\n")
|
|
fmt.Printf(" URL: %s\n", req.URL.String())
|
|
fmt.Printf(" Headers: %+v\n", req.Header)
|
|
fmt.Printf(" Unit ID: %d\n", unitID)
|
|
fmt.Printf(" Unit Size: %d\n", len(unitData))
|
|
fmt.Printf(" Upload Key: %s\n", uploadKey)
|
|
fmt.Printf(" Action Token: %s\n", actionToken) */
|
|
|
|
res, err := base.HttpClient.Do(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
body, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to read response body: %v", err)
|
|
}
|
|
|
|
//fmt.Printf("MediaFire resumable upload response (status %d): %s\n", res.StatusCode, string(body))
|
|
|
|
var uploadResp struct {
|
|
Response struct {
|
|
Doupload struct {
|
|
Key string `json:"key"`
|
|
} `json:"doupload"`
|
|
Result string `json:"result"`
|
|
} `json:"response"`
|
|
}
|
|
|
|
if err := json.Unmarshal(body, &uploadResp); err != nil {
|
|
return "", fmt.Errorf("failed to parse response: %v", err)
|
|
}
|
|
|
|
if res.StatusCode != 200 {
|
|
return "", fmt.Errorf("resumable upload failed with status %d", res.StatusCode)
|
|
}
|
|
|
|
return uploadResp.Response.Doupload.Key, nil
|
|
}
|
|
|
|
func (d *Mediafire) uploadUnits(ctx context.Context, file *os.File, checkResp *MediafireCheckResponse, filename, fileHash, folderKey string, up driver.UpdateProgress) (string, error) {
|
|
unitSize, _ := strconv.ParseInt(checkResp.Response.ResumableUpload.UnitSize, 10, 64)
|
|
numUnits, _ := strconv.Atoi(checkResp.Response.ResumableUpload.NumberOfUnits)
|
|
uploadKey := checkResp.Response.ResumableUpload.UploadKey
|
|
|
|
stringWords := checkResp.Response.ResumableUpload.Bitmap.Words
|
|
intWords := make([]int, len(stringWords))
|
|
for i, word := range stringWords {
|
|
intWords[i], _ = strconv.Atoi(word)
|
|
}
|
|
|
|
var finalUploadKey string
|
|
|
|
for unitID := 0; unitID < numUnits; unitID++ {
|
|
|
|
if utils.IsCanceled(ctx) {
|
|
return "", ctx.Err()
|
|
}
|
|
|
|
if d.isUnitUploaded(intWords, unitID) {
|
|
up(float64(unitID+1) * 100 / float64(numUnits))
|
|
continue
|
|
}
|
|
|
|
uploadKey, err := d.uploadSingleUnit(ctx, file, unitID, unitSize, fileHash, filename, uploadKey, folderKey)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
finalUploadKey = uploadKey
|
|
|
|
up(float64(unitID+1) * 100 / float64(numUnits))
|
|
}
|
|
|
|
return finalUploadKey, nil
|
|
}
|
|
|
|
func (d *Mediafire) uploadSingleUnit(ctx context.Context, file *os.File, unitID int, unitSize int64, fileHash, filename, uploadKey, folderKey string) (string, error) {
|
|
start := int64(unitID) * unitSize
|
|
size := unitSize
|
|
|
|
stat, err := file.Stat()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
fileSize := stat.Size()
|
|
|
|
if start+size > fileSize {
|
|
size = fileSize - start
|
|
}
|
|
|
|
unitData := make([]byte, size)
|
|
if _, err := file.ReadAt(unitData, start); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return d.resumableUpload(ctx, folderKey, uploadKey, unitData, unitID, fileHash, filename, fileSize)
|
|
}
|
|
|
|
func (d *Mediafire) getActionToken(_ context.Context) (string, error) {
|
|
|
|
if d.actionToken != "" {
|
|
return d.actionToken, nil
|
|
}
|
|
|
|
data := map[string]string{
|
|
"type": "upload",
|
|
"lifespan": "1440",
|
|
"response_format": "json",
|
|
"session_token": d.SessionToken,
|
|
}
|
|
|
|
var resp MediafireActionTokenResponse
|
|
_, err := d.postForm("/user/get_action_token.php", data, &resp)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if resp.Response.Result != "Success" {
|
|
return "", fmt.Errorf("MediaFire action token failed: %s", resp.Response.Result)
|
|
}
|
|
|
|
return resp.Response.ActionToken, nil
|
|
}
|
|
|
|
func (d *Mediafire) pollUpload(ctx context.Context, key string) (*MediafirePollResponse, error) {
|
|
|
|
actionToken, err := d.getActionToken(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get action token: %w", err)
|
|
}
|
|
|
|
//fmt.Printf("Debug Key: %+v\n", key)
|
|
|
|
query := map[string]string{
|
|
"key": key,
|
|
"response_format": "json",
|
|
"session_token": actionToken, /* d.SessionToken */
|
|
}
|
|
|
|
var resp MediafirePollResponse
|
|
_, err = d.postForm("/upload/poll_upload.php", query, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
//fmt.Printf("pollUpload :: Raw response: %s\n", string(body))
|
|
//fmt.Printf("pollUpload :: Parsed response: %+v\n", resp)
|
|
|
|
//fmt.Printf("pollUpload :: Debug Result: %+v\n", resp.Response.Result)
|
|
|
|
if resp.Response.Result != "Success" {
|
|
return nil, fmt.Errorf("MediaFire poll upload failed: %s", resp.Response.Result)
|
|
}
|
|
|
|
return &resp, nil
|
|
}
|
|
|
|
func (d *Mediafire) sha256Hex(r io.Reader) string {
|
|
h := sha256.New()
|
|
io.Copy(h, r)
|
|
return hex.EncodeToString(h.Sum(nil))
|
|
}
|
|
|
|
func (d *Mediafire) isUnitUploaded(words []int, unitID int) bool {
|
|
wordIndex := unitID / 16
|
|
bitIndex := unitID % 16
|
|
if wordIndex >= len(words) {
|
|
return false
|
|
}
|
|
return (words[wordIndex]>>bitIndex)&1 == 1
|
|
}
|
|
|
|
func (d *Mediafire) getExistingFileInfo(ctx context.Context, fileHash, filename, folderKey string) (*model.ObjThumb, error) {
|
|
|
|
if fileInfo, err := d.getFileByHash(ctx, fileHash); err == nil && fileInfo != nil {
|
|
return fileInfo, nil
|
|
}
|
|
|
|
files, err := d.getFiles(ctx, folderKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, file := range files {
|
|
if file.Name == filename && !file.IsFolder {
|
|
return d.fileToObj(file), nil
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("existing file not found")
|
|
}
|
|
|
|
func (d *Mediafire) getFileByHash(_ context.Context, hash string) (*model.ObjThumb, error) {
|
|
query := map[string]string{
|
|
"session_token": d.SessionToken,
|
|
"response_format": "json",
|
|
"hash": hash,
|
|
}
|
|
|
|
var resp MediafireFileSearchResponse
|
|
_, err := d.postForm("/file/get_info.php", query, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if resp.Response.Result != "Success" {
|
|
return nil, fmt.Errorf("MediaFire file search failed: %s", resp.Response.Result)
|
|
}
|
|
|
|
if len(resp.Response.FileInfo) == 0 {
|
|
return nil, fmt.Errorf("file not found by hash")
|
|
}
|
|
|
|
file := resp.Response.FileInfo[0]
|
|
return d.fileToObj(file), nil
|
|
}
|