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...) // invoking RPC method 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() // queryValues.Add("callback", oauthToken.Callback) 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: Click Here`, 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 { // access token expired 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...) // invoking RPC method if err != nil { grpcStatus, ok := status.FromError(err) if ok && grpcStatus.Code() == codes.Unauthenticated && strings.Contains(grpcStatus.Err().Error(), "invalid accesstoken") && len(refreshToken) > 0 { // refresh token 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) } // retry ctxx := svc.signContext(method, ctx) err = invoker(ctxx, method, req, reply, cc, opts...) // invoking RPC method 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), // Set timeout to 5 seconds } 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 } // get the next chunk 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 } // Read reads up to len(p) bytes into p. 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") } // Skip data at the start if requested for oo.skip > 0 { //size := 1024 * 1024 _, 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 } // Close closed the file - MAC errors are reported here func (oo *openObject) Close() (err error) { oo.mu.Lock() defer oo.mu.Unlock() if oo.closed { return nil } // 校验Sha1 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[:]) } // chunkSize describes a size and position of chunk 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 }