|
package halalcloud |
|
|
|
import ( |
|
"bytes" |
|
"context" |
|
"crypto/md5" |
|
"crypto/tls" |
|
"encoding/hex" |
|
"errors" |
|
"fmt" |
|
"github.com/alist-org/alist/v3/internal/model" |
|
"github.com/alist-org/alist/v3/pkg/utils" |
|
pbPublicUser "github.com/city404/v6-public-rpc-proto/go/v6/user" |
|
pubUserFile "github.com/city404/v6-public-rpc-proto/go/v6/userfile" |
|
"github.com/google/uuid" |
|
"github.com/ipfs/go-cid" |
|
"google.golang.org/grpc" |
|
"google.golang.org/grpc/codes" |
|
"google.golang.org/grpc/credentials" |
|
"google.golang.org/grpc/metadata" |
|
"google.golang.org/grpc/status" |
|
"hash" |
|
"io" |
|
"net/http" |
|
"strconv" |
|
"strings" |
|
"sync" |
|
"time" |
|
) |
|
|
|
const ( |
|
AppID = "alist/10001" |
|
AppVersion = "1.0.0" |
|
AppSecret = "bR4SJwOkvnG5WvVJ" |
|
) |
|
|
|
const ( |
|
grpcServer = "grpcuserapi.2dland.cn:443" |
|
grpcServerAuth = "grpcuserapi.2dland.cn" |
|
) |
|
|
|
func (d *HalalCloud) NewAuthServiceWithOauth(options ...HalalOption) (*AuthService, error) { |
|
|
|
aService := &AuthService{} |
|
err2 := errors.New("") |
|
|
|
svc := d.HalalCommon.AuthService |
|
for _, opt := range options { |
|
opt.apply(&svc.dopts) |
|
} |
|
|
|
grpcOptions := svc.dopts.grpcOptions |
|
grpcOptions = append(grpcOptions, grpc.WithAuthority(grpcServerAuth), grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})), grpc.WithUnaryInterceptor(func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { |
|
ctxx := svc.signContext(method, ctx) |
|
err := invoker(ctxx, method, req, reply, cc, opts...) |
|
return err |
|
})) |
|
|
|
grpcConnection, err := grpc.NewClient(grpcServer, grpcOptions...) |
|
if err != nil { |
|
return nil, err |
|
} |
|
defer grpcConnection.Close() |
|
userClient := pbPublicUser.NewPubUserClient(grpcConnection) |
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) |
|
defer cancel() |
|
stateString := uuid.New().String() |
|
|
|
oauthToken, err := userClient.CreateAuthToken(ctx, &pbPublicUser.LoginRequest{ |
|
ReturnType: 2, |
|
State: stateString, |
|
ReturnUrl: "", |
|
}) |
|
if err != nil { |
|
return nil, err |
|
} |
|
if len(oauthToken.State) < 1 { |
|
oauthToken.State = stateString |
|
} |
|
|
|
if oauthToken.Url != "" { |
|
|
|
return nil, fmt.Errorf(`need verify: <a target="_blank" href="%s">Click Here</a>`, oauthToken.Url) |
|
} |
|
|
|
return aService, err2 |
|
|
|
} |
|
|
|
func (d *HalalCloud) NewAuthService(refreshToken string, options ...HalalOption) (*AuthService, error) { |
|
svc := d.HalalCommon.AuthService |
|
|
|
if len(refreshToken) < 1 { |
|
refreshToken = d.Addition.RefreshToken |
|
} |
|
|
|
if len(d.tr.AccessToken) > 0 { |
|
accessTokenExpiredAt := d.tr.AccessTokenExpiredAt |
|
current := time.Now().UnixMilli() |
|
if accessTokenExpiredAt < current { |
|
|
|
d.tr.AccessToken = "" |
|
d.tr.AccessTokenExpiredAt = 0 |
|
} else { |
|
svc.tr.AccessTokenExpiredAt = accessTokenExpiredAt |
|
svc.tr.AccessToken = d.tr.AccessToken |
|
} |
|
} |
|
|
|
for _, opt := range options { |
|
opt.apply(&svc.dopts) |
|
} |
|
|
|
grpcOptions := svc.dopts.grpcOptions |
|
grpcOptions = append(grpcOptions, grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(10*1024*1024), grpc.MaxCallRecvMsgSize(10*1024*1024)), grpc.WithAuthority(grpcServerAuth), grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})), grpc.WithUnaryInterceptor(func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { |
|
ctxx := svc.signContext(method, ctx) |
|
err := invoker(ctxx, method, req, reply, cc, opts...) |
|
if err != nil { |
|
grpcStatus, ok := status.FromError(err) |
|
|
|
if ok && grpcStatus.Code() == codes.Unauthenticated && strings.Contains(grpcStatus.Err().Error(), "invalid accesstoken") && len(refreshToken) > 0 { |
|
|
|
refreshResponse, err := pbPublicUser.NewPubUserClient(cc).Refresh(ctx, &pbPublicUser.Token{ |
|
RefreshToken: refreshToken, |
|
}) |
|
if err != nil { |
|
return err |
|
} |
|
if len(refreshResponse.AccessToken) > 0 { |
|
svc.tr.AccessToken = refreshResponse.AccessToken |
|
svc.tr.AccessTokenExpiredAt = refreshResponse.AccessTokenExpireTs |
|
svc.OnAccessTokenRefreshed(refreshResponse.AccessToken, refreshResponse.AccessTokenExpireTs, refreshResponse.RefreshToken, refreshResponse.RefreshTokenExpireTs) |
|
} |
|
|
|
ctxx := svc.signContext(method, ctx) |
|
err = invoker(ctxx, method, req, reply, cc, opts...) |
|
if err != nil { |
|
return err |
|
} else { |
|
return nil |
|
} |
|
} |
|
} |
|
return err |
|
})) |
|
grpcConnection, err := grpc.NewClient(grpcServer, grpcOptions...) |
|
|
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
svc.grpcConnection = grpcConnection |
|
return svc, err |
|
} |
|
|
|
func (s *AuthService) OnAccessTokenRefreshed(accessToken string, accessTokenExpiredAt int64, refreshToken string, refreshTokenExpiredAt int64) { |
|
s.tr.AccessToken = accessToken |
|
s.tr.AccessTokenExpiredAt = accessTokenExpiredAt |
|
s.tr.RefreshToken = refreshToken |
|
s.tr.RefreshTokenExpiredAt = refreshTokenExpiredAt |
|
|
|
if s.dopts.onTokenRefreshed != nil { |
|
s.dopts.onTokenRefreshed(accessToken, accessTokenExpiredAt, refreshToken, refreshTokenExpiredAt) |
|
} |
|
|
|
} |
|
|
|
func (s *AuthService) GetGrpcConnection() *grpc.ClientConn { |
|
return s.grpcConnection |
|
} |
|
|
|
func (s *AuthService) Close() { |
|
_ = s.grpcConnection.Close() |
|
} |
|
|
|
func (s *AuthService) signContext(method string, ctx context.Context) context.Context { |
|
var kvString []string |
|
currentTimeStamp := strconv.FormatInt(time.Now().UnixMilli(), 10) |
|
bufferedString := bytes.NewBufferString(method) |
|
kvString = append(kvString, "timestamp", currentTimeStamp) |
|
bufferedString.WriteString(currentTimeStamp) |
|
kvString = append(kvString, "appid", s.appID) |
|
bufferedString.WriteString(s.appID) |
|
kvString = append(kvString, "appversion", s.appVersion) |
|
bufferedString.WriteString(s.appVersion) |
|
if s.tr != nil && len(s.tr.AccessToken) > 0 { |
|
authorization := "Bearer " + s.tr.AccessToken |
|
kvString = append(kvString, "authorization", authorization) |
|
bufferedString.WriteString(authorization) |
|
} |
|
bufferedString.WriteString(s.appSecret) |
|
sign := GetMD5Hash(bufferedString.String()) |
|
kvString = append(kvString, "sign", sign) |
|
return metadata.AppendToOutgoingContext(ctx, kvString...) |
|
} |
|
|
|
func (d *HalalCloud) GetCurrentOpDir(dir model.Obj, args []string, index int) string { |
|
currentDir := dir.GetPath() |
|
if len(currentDir) == 0 { |
|
currentDir = "/" |
|
} |
|
opPath := currentDir + "/" + args[index] |
|
if strings.HasPrefix(args[index], "/") { |
|
opPath = args[index] |
|
} |
|
return opPath |
|
} |
|
|
|
func (d *HalalCloud) GetCurrentDir(dir model.Obj) string { |
|
currentDir := dir.GetPath() |
|
if len(currentDir) == 0 { |
|
currentDir = "/" |
|
} |
|
return currentDir |
|
} |
|
|
|
type Common struct { |
|
} |
|
|
|
func getRawFiles(addr *pubUserFile.SliceDownloadInfo) ([]byte, error) { |
|
|
|
if addr == nil { |
|
return nil, errors.New("addr is nil") |
|
} |
|
|
|
client := http.Client{ |
|
Timeout: time.Duration(60 * time.Second), |
|
} |
|
resp, err := client.Get(addr.DownloadAddress) |
|
if err != nil { |
|
|
|
return nil, err |
|
} |
|
defer resp.Body.Close() |
|
body, err := io.ReadAll(resp.Body) |
|
if err != nil { |
|
return nil, err |
|
} |
|
if resp.StatusCode != http.StatusOK { |
|
return nil, fmt.Errorf("bad status: %s, body: %s", resp.Status, body) |
|
} |
|
|
|
if addr.Encrypt > 0 { |
|
cd := uint8(addr.Encrypt) |
|
for idx := 0; idx < len(body); idx++ { |
|
body[idx] = body[idx] ^ cd |
|
} |
|
} |
|
|
|
if addr.StoreType != 10 { |
|
|
|
sourceCid, err := cid.Decode(addr.Identity) |
|
if err != nil { |
|
return nil, err |
|
} |
|
checkCid, err := sourceCid.Prefix().Sum(body) |
|
if err != nil { |
|
return nil, err |
|
} |
|
if !checkCid.Equals(sourceCid) { |
|
return nil, fmt.Errorf("bad cid: %s, body: %s", checkCid.String(), body) |
|
} |
|
} |
|
|
|
return body, nil |
|
|
|
} |
|
|
|
type openObject struct { |
|
ctx context.Context |
|
mu sync.Mutex |
|
d []*pubUserFile.SliceDownloadInfo |
|
id int |
|
skip int64 |
|
chunk *[]byte |
|
chunks *[]chunkSize |
|
closed bool |
|
sha string |
|
shaTemp hash.Hash |
|
} |
|
|
|
|
|
func (oo *openObject) getChunk(ctx context.Context) (err error) { |
|
if oo.id >= len(*oo.chunks) { |
|
return io.EOF |
|
} |
|
var chunk []byte |
|
err = utils.Retry(3, time.Second, func() (err error) { |
|
chunk, err = getRawFiles(oo.d[oo.id]) |
|
return err |
|
}) |
|
if err != nil { |
|
return err |
|
} |
|
oo.id++ |
|
oo.chunk = &chunk |
|
return nil |
|
} |
|
|
|
|
|
func (oo *openObject) Read(p []byte) (n int, err error) { |
|
oo.mu.Lock() |
|
defer oo.mu.Unlock() |
|
if oo.closed { |
|
return 0, fmt.Errorf("read on closed file") |
|
} |
|
|
|
for oo.skip > 0 { |
|
|
|
_, size, err := oo.ChunkLocation(oo.id) |
|
if err != nil { |
|
return 0, err |
|
} |
|
if oo.skip < int64(size) { |
|
break |
|
} |
|
oo.id++ |
|
oo.skip -= int64(size) |
|
} |
|
if len(*oo.chunk) == 0 { |
|
err = oo.getChunk(oo.ctx) |
|
if err != nil { |
|
return 0, err |
|
} |
|
if oo.skip > 0 { |
|
*oo.chunk = (*oo.chunk)[oo.skip:] |
|
oo.skip = 0 |
|
} |
|
} |
|
n = copy(p, *oo.chunk) |
|
*oo.chunk = (*oo.chunk)[n:] |
|
|
|
oo.shaTemp.Write(*oo.chunk) |
|
|
|
return n, nil |
|
} |
|
|
|
|
|
func (oo *openObject) Close() (err error) { |
|
oo.mu.Lock() |
|
defer oo.mu.Unlock() |
|
if oo.closed { |
|
return nil |
|
} |
|
|
|
if string(oo.shaTemp.Sum(nil)) != oo.sha { |
|
return fmt.Errorf("failed to finish download: %w", err) |
|
} |
|
|
|
oo.closed = true |
|
return nil |
|
} |
|
|
|
func GetMD5Hash(text string) string { |
|
tHash := md5.Sum([]byte(text)) |
|
return hex.EncodeToString(tHash[:]) |
|
} |
|
|
|
|
|
type chunkSize struct { |
|
position int64 |
|
size int |
|
} |
|
|
|
func getChunkSizes(sliceSize []*pubUserFile.SliceSize) (chunks []chunkSize) { |
|
chunks = make([]chunkSize, 0) |
|
for _, s := range sliceSize { |
|
|
|
if s.EndIndex == 0 { |
|
s.EndIndex = s.StartIndex |
|
} |
|
for j := s.StartIndex; j <= s.EndIndex; j++ { |
|
chunks = append(chunks, chunkSize{position: j, size: int(s.Size)}) |
|
} |
|
} |
|
return chunks |
|
} |
|
|
|
func (oo *openObject) ChunkLocation(id int) (position int64, size int, err error) { |
|
if id < 0 || id >= len(*oo.chunks) { |
|
return 0, 0, errors.New("invalid arguments") |
|
} |
|
|
|
return (*oo.chunks)[id].position, (*oo.chunks)[id].size, nil |
|
} |
|
|