feat: implement CAS module, middleware, utils, and templates
- CAS: GET /files/{hash} with immutable cache headers, launcher asset
serving, hash validation, StoreFile/VerifyAndStore helpers
- Middleware: CORS, request logging, per-IP token bucket rate limiter
- Utils: SHA1Bytes, SHA256Bytes, SHA1File, Unzip with zip-slip protection
- Templates: placeholder handler with html/template discovery
- Wire CAS routes and middleware chain (Logging → CORS) in main.go
This commit is contained in:
@@ -1,2 +1,89 @@
|
||||
// package utils provides shared utility functions (SHA-1, ZIP, etc.).
|
||||
// package utils provides shared utility functions (SHA-1, SHA-256, ZIP, etc.).
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"archive/zip"
|
||||
)
|
||||
|
||||
// SHA1Bytes returns the SHA-1 hex string of the given data.
|
||||
func SHA1Bytes(data []byte) string {
|
||||
h := sha1.Sum(data)
|
||||
return hex.EncodeToString(h[:])
|
||||
}
|
||||
|
||||
// SHA256Bytes returns the SHA-256 hex string of the given data.
|
||||
func SHA256Bytes(data []byte) string {
|
||||
h := sha256.Sum256(data)
|
||||
return hex.EncodeToString(h[:])
|
||||
}
|
||||
|
||||
// SHA1File computes the SHA-1 hash of a file at the given path.
|
||||
func SHA1File(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := sha1.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// Unzip extracts a ZIP archive to the destination directory.
|
||||
// Returns the list of extracted file paths.
|
||||
// Protects against zip-slip by validating that each entry's target path
|
||||
// stays within the destination directory.
|
||||
func Unzip(data []byte, dest string) ([]string, error) {
|
||||
reader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var extracted []string
|
||||
for _, f := range reader.File {
|
||||
if f.FileInfo().IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
target := filepath.Join(dest, f.Name)
|
||||
|
||||
// Zip-slip protection.
|
||||
if !strings.HasPrefix(target, filepath.Clean(dest)+string(os.PathSeparator)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
out, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
rc.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
io.Copy(out, rc)
|
||||
out.Close()
|
||||
rc.Close()
|
||||
|
||||
extracted = append(extracted, f.Name)
|
||||
}
|
||||
return extracted, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user