Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,13 @@ func main() {
} else {
cfg.AuthDir = resolvedAuthDir
}
if repoRoot, ok := util.FindGitRepoRoot(cfg.AuthDir); ok {
if useGitStore {
log.Warnf("auth-dir %q is inside a git repository (%q); git-backed token storage is enabled, ensure the repo/remote is private and tokens are not exposed", cfg.AuthDir, repoRoot)
} else {
log.Warnf("auth-dir %q is inside a git repository (%q); do not commit token files, add it to .gitignore or set auth-dir outside the repo", cfg.AuthDir, repoRoot)
}
}
managementasset.SetCurrentConfig(cfg)

// Create login options to be used in authentication flows.
Expand Down
28 changes: 22 additions & 6 deletions internal/api/handlers/management/auth_files.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v6/internal/securefile"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
Expand Down Expand Up @@ -345,7 +346,7 @@ func (h *Handler) listAuthFilesFromDisk(c *gin.Context) {

// Read file to get type field
full := filepath.Join(h.cfg.AuthDir, name)
if data, errRead := os.ReadFile(full); errRead == nil {
if data, errRead := readAuthJSON(full); errRead == nil {
typeValue := gjson.GetBytes(data, "type").String()
emailValue := gjson.GetBytes(data, "email").String()
fileData["type"] = typeValue
Expand Down Expand Up @@ -457,6 +458,17 @@ func authAttribute(auth *coreauth.Auth, key string) string {
return auth.Attributes[key]
}

func readAuthJSON(path string) ([]byte, error) {
if strings.TrimSpace(path) == "" {
return nil, fmt.Errorf("auth file path is empty")
}
data, _, err := securefile.ReadAuthJSONFile(path)
if err != nil {
return nil, err
}
return data, nil
}

func isRuntimeOnlyAuth(auth *coreauth.Auth) bool {
if auth == nil || len(auth.Attributes) == 0 {
return false
Expand All @@ -476,7 +488,7 @@ func (h *Handler) DownloadAuthFile(c *gin.Context) {
return
}
full := filepath.Join(h.cfg.AuthDir, name)
data, err := os.ReadFile(full)
data, err := readAuthJSON(full)
if err != nil {
if os.IsNotExist(err) {
c.JSON(404, gin.H{"error": "file not found"})
Expand Down Expand Up @@ -512,11 +524,15 @@ func (h *Handler) UploadAuthFile(c *gin.Context) {
c.JSON(500, gin.H{"error": fmt.Sprintf("failed to save file: %v", errSave)})
return
}
data, errRead := os.ReadFile(dst)
data, errRead := readAuthJSON(dst)
if errRead != nil {
c.JSON(500, gin.H{"error": fmt.Sprintf("failed to read saved file: %v", errRead)})
return
}
if errWrite := securefile.WriteAuthJSONFile(dst, data); errWrite != nil {
c.JSON(500, gin.H{"error": fmt.Sprintf("failed to secure auth file: %v", errWrite)})
return
}
if errReg := h.registerAuthFromFile(ctx, dst, data); errReg != nil {
c.JSON(500, gin.H{"error": errReg.Error()})
return
Expand Down Expand Up @@ -544,8 +560,8 @@ func (h *Handler) UploadAuthFile(c *gin.Context) {
dst = abs
}
}
if errWrite := os.WriteFile(dst, data, 0o600); errWrite != nil {
c.JSON(500, gin.H{"error": fmt.Sprintf("failed to write file: %v", errWrite)})
if errWrite := securefile.WriteAuthJSONFile(dst, data); errWrite != nil {
c.JSON(500, gin.H{"error": fmt.Sprintf("failed to secure auth file: %v", errWrite)})
return
}
if err = h.registerAuthFromFile(ctx, dst, data); err != nil {
Expand Down Expand Up @@ -649,7 +665,7 @@ func (h *Handler) registerAuthFromFile(ctx context.Context, path string, data []
}
if data == nil {
var err error
data, err = os.ReadFile(path)
data, err = readAuthJSON(path)
if err != nil {
return fmt.Errorf("failed to read auth file: %w", err)
}
Expand Down
8 changes: 4 additions & 4 deletions internal/logging/request_logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ func (l *FileRequestLogger) logRequest(url, method string, requestHeaders map[st
responseToWrite = response
}

logFile, errOpen := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
logFile, errOpen := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
if errOpen != nil {
return fmt.Errorf("failed to create log file: %w", errOpen)
}
Expand Down Expand Up @@ -340,7 +340,7 @@ func (l *FileRequestLogger) generateErrorFilename(url string) string {
// - error: An error if directory creation fails, nil otherwise
func (l *FileRequestLogger) ensureLogsDir() error {
if _, err := os.Stat(l.logsDir); os.IsNotExist(err) {
return os.MkdirAll(l.logsDir, 0755)
return os.MkdirAll(l.logsDir, 0o700)
}
return nil
}
Expand Down Expand Up @@ -905,7 +905,7 @@ func (l *FileRequestLogger) formatRequestInfo(url, method string, headers map[st
content.WriteString("\n")

content.WriteString("=== REQUEST BODY ===\n")
content.Write(body)
content.Write(util.MaskSensitiveJSON(body))
content.WriteString("\n\n")

return content.String()
Expand Down Expand Up @@ -1070,7 +1070,7 @@ func (w *FileStreamingLogWriter) Close() error {
return nil
}

logFile, errOpen := os.OpenFile(w.logFilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
logFile, errOpen := os.OpenFile(w.logFilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
if errOpen != nil {
w.cleanupTempFiles()
return fmt.Errorf("failed to create log file: %w", errOpen)
Expand Down
94 changes: 94 additions & 0 deletions internal/securefile/atomic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package securefile

import (
"fmt"
"os"
"path/filepath"
"time"
)

// EnsurePrivateDir creates dirPath (and parents) with 0700 permissions.
func EnsurePrivateDir(dirPath string) error {
if dirPath == "" {
return fmt.Errorf("securefile: dir path is empty")
}
if err := os.MkdirAll(dirPath, 0o700); err != nil {
return err
}
// Best-effort permission hardening. Ignore errors (e.g., non-POSIX FS).
_ = os.Chmod(dirPath, 0o700)
return nil
}

// AtomicWriteFile writes data to path using a temp file + rename, and attempts to fsync.
// mode controls the final file permissions.
func AtomicWriteFile(path string, data []byte, mode os.FileMode) error {
if path == "" {
return fmt.Errorf("securefile: path is empty")
}
dir := filepath.Dir(path)
if err := EnsurePrivateDir(dir); err != nil {
return err
}

tmp, err := os.CreateTemp(dir, ".tmp.*")
if err != nil {
return err
}
tmpName := tmp.Name()
defer func() {
_ = tmp.Close()
_ = os.Remove(tmpName)
}()

if mode == 0 {
mode = 0o600
}
if err := tmp.Chmod(mode); err != nil {
// Best-effort: ignore chmod failure on some filesystems.
}

if _, err := tmp.Write(data); err != nil {
return err
}
if err := tmp.Sync(); err != nil {
return err
}
if err := tmp.Close(); err != nil {
return err
}

if err := os.Rename(tmpName, path); err != nil {
return err
}

// Best-effort: ensure final mode.
_ = os.Chmod(path, mode)
return nil
}

// ReadFileRawLocked reads the file at path while holding an advisory lock on path+".lock".
func ReadFileRawLocked(path string) ([]byte, error) {
lockPath := path + ".lock"
var out []byte
err := WithLock(lockPath, 10*time.Second, func() error {
data, err := os.ReadFile(path)
if err != nil {
return err
}
out = data
return nil
})
if err != nil {
return nil, err
}
return out, nil
}

// WriteFileRawLocked writes data to path using an advisory lock on path+".lock" and atomic replace.
func WriteFileRawLocked(path string, data []byte, mode os.FileMode) error {
lockPath := path + ".lock"
return WithLock(lockPath, 10*time.Second, func() error {
return AtomicWriteFile(path, data, mode)
})
}
Loading