Files
2026-07-13 15:42:06 +08:00

248 lines
6.3 KiB
Go

package main
import (
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"time"
"github.com/sagernet/sing-box/experimental/libbox"
"github.com/sagernet/sing/common/rw"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const (
// File names within a report directory follow the client convention:
// sing-box-for-apple Library/Shared/CrashReportArchive.swift (ReportArchive)
// and sing-box experimental/libbox/report.go.
readMarkerFileName = ".read"
ownerMarkerFileName = ".owner"
metadataFileName = "metadata.json"
configSnapshotFileName = "configuration.json"
goLogFileName = "go.log"
nativeLogFileName = "native.log"
)
func reportPath(reportsDirectory string, name string) (string, error) {
if !filepath.IsLocal(name) || name == "." || name != filepath.Base(name) {
return "", status.Error(codes.InvalidArgument, "invalid report name")
}
fullPath := filepath.Join(reportsDirectory, name)
info, err := os.Stat(fullPath)
if err != nil {
if os.IsNotExist(err) {
return "", status.Error(codes.NotFound, "report not found")
}
return "", err
}
if !info.IsDir() {
return "", status.Error(codes.NotFound, "report not found")
}
return fullPath, nil
}
func reportPathForUser(reportsDirectory string, name string, userID string) (string, error) {
fullPath, err := reportPath(reportsDirectory, name)
if err != nil {
return "", err
}
if !reportOwnedBy(fullPath, userID) {
return "", status.Error(codes.NotFound, "report not found")
}
return fullPath, nil
}
func reportOwnedBy(fullPath string, userID string) bool {
ownerContent, err := os.ReadFile(filepath.Join(fullPath, ownerMarkerFileName))
return err == nil && strings.TrimSpace(string(ownerContent)) == userID
}
func tagUnownedReports(reportsDirectory string, userID string) error {
if userID == "" {
return nil
}
entries, err := os.ReadDir(reportsDirectory)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
ownerPath := filepath.Join(reportsDirectory, entry.Name(), ownerMarkerFileName)
_, err = os.Stat(ownerPath)
if err == nil {
continue
}
if !os.IsNotExist(err) {
return err
}
err = os.WriteFile(ownerPath, []byte(userID+"\n"), 0o600)
if err != nil {
return err
}
}
return nil
}
func (d *Daemon) reportCaller(ctx context.Context, reportsDirectoryName string) (string, string, error) {
identity, err := peerIdentityFromContext(ctx)
if err != nil {
return "", "", err
}
d.lifecycleAccess.Lock()
defer d.lifecycleAccess.Unlock()
ownerUserID, err := loadOwner()
if err != nil {
return "", "", err
}
if ownerUserID != identity.UserID {
return "", "", status.Error(codes.PermissionDenied, "the service is owned by another user")
}
reportsDirectory := filepath.Join(userWorkingDirectory(identity.UserID), reportsDirectoryName)
err = tagUnownedReports(reportsDirectory, identity.UserID)
if err != nil {
return "", "", err
}
return reportsDirectory, identity.UserID, nil
}
func deleteReportsForUser(reportsDirectory string, userID string) error {
entries, err := os.ReadDir(reportsDirectory)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
fullPath := filepath.Join(reportsDirectory, entry.Name())
if !reportOwnedBy(fullPath, userID) {
continue
}
err = os.RemoveAll(fullPath)
if err != nil {
return err
}
}
return nil
}
func reportTime(fullPath string, timestampKey string) time.Time {
metadataContent, err := os.ReadFile(filepath.Join(fullPath, metadataFileName))
if err == nil {
var metadata map[string]any
err = json.Unmarshal(metadataContent, &metadata)
if err == nil {
if timestamp, isString := metadata[timestampKey].(string); isString && timestamp != "" {
parsedTime, parseError := time.Parse(time.RFC3339, timestamp)
if parseError == nil {
return parsedTime
}
}
}
}
info, err := os.Stat(fullPath)
if err != nil {
return time.Time{}
}
return info.ModTime()
}
func reportIsRead(fullPath string) bool {
_, err := os.Stat(filepath.Join(fullPath, readMarkerFileName))
return err == nil
}
func exportReportArchive(reportsDirectory string, name string, userID string, withConfiguration bool, withLog bool, encrypt bool) (*CrashReportArchive, error) {
fullPath, err := reportPathForUser(reportsDirectory, name, userID)
if err != nil {
return nil, err
}
tempRoot := filepath.Join(filepath.Dir(reportsDirectory), "temp")
err = os.MkdirAll(tempRoot, 0o700)
if err != nil {
return nil, err
}
tempDirectory, err := os.MkdirTemp(tempRoot, "report-")
if err != nil {
return nil, err
}
defer os.RemoveAll(tempDirectory)
strippedPath := filepath.Join(tempDirectory, name)
err = copyDirectory(fullPath, strippedPath)
if err != nil {
return nil, err
}
os.Remove(filepath.Join(strippedPath, readMarkerFileName))
os.Remove(filepath.Join(strippedPath, ownerMarkerFileName))
if !withConfiguration {
os.Remove(filepath.Join(strippedPath, configSnapshotFileName))
}
if !withLog {
os.Remove(filepath.Join(strippedPath, goLogFileName))
os.Remove(filepath.Join(strippedPath, nativeLogFileName))
}
fileName := name + ".zip"
if encrypt {
fileName += ".age"
}
archivePath := filepath.Join(tempDirectory, fileName)
err = libbox.CreateZipArchive(strippedPath, archivePath, encrypt)
if err != nil {
return nil, err
}
data, err := os.ReadFile(archivePath)
if err != nil {
return nil, err
}
return &CrashReportArchive{
FileName: fileName,
Data: data,
}, nil
}
func copyDirectory(sourcePath string, destinationPath string) error {
err := os.MkdirAll(destinationPath, 0o700)
if err != nil {
return err
}
entries, err := os.ReadDir(sourcePath)
if err != nil {
return err
}
for _, entry := range entries {
sourceEntryPath := filepath.Join(sourcePath, entry.Name())
destinationEntryPath := filepath.Join(destinationPath, entry.Name())
if entry.IsDir() {
err = copyDirectory(sourceEntryPath, destinationEntryPath)
if err != nil {
return err
}
continue
}
info, err := entry.Info()
if err != nil {
return err
}
if !info.Mode().IsRegular() {
return status.Error(codes.FailedPrecondition, "report contains a non-regular file")
}
err = rw.CopyFile(sourceEntryPath, destinationEntryPath)
if err != nil {
return err
}
}
return nil
}