Added example/dropin replacements for .env.example
Fixed the issue with PlainText (Complete Anon) posting Need to fix device sign on issues. Need to make it so that the non-signed in devices can only see their equalivant level of posts. (i.e. plaintext, public-encrypted, private-encrypted)
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -2,28 +2,75 @@ package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// secureHeaders adds strict, privacy-preserving headers to static responses.
|
||||
func (s *Server) secureHeaders(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Referrer-Policy", "no-referrer")
|
||||
// ListenFrontend serves the static client from s.StaticDir on a separate port (e.g. :9082).
|
||||
func (s *Server) ListenFrontend(addr string) error {
|
||||
root := s.StaticDir
|
||||
if root == "" {
|
||||
root = "./client"
|
||||
}
|
||||
// Basic security/CSP headers for static content.
|
||||
addCommonHeaders := func(w http.ResponseWriter) {
|
||||
// CORS: static site can be embedded by any origin if you want, keep strict by default
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Cross-Origin-Opener-Policy", "same-origin")
|
||||
w.Header().Set("Cross-Origin-Resource-Policy", "same-site")
|
||||
w.Header().Set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), interest-cohort=(), browsing-topics=()")
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("Referrer-Policy", "no-referrer")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("Strict-Transport-Security", "max-age=15552000; includeSubDomains; preload")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// MountStatic mounts a static file server under a prefix onto the provided mux.
|
||||
// Usage (from main): s.MountStatic(mux, "/", http.Dir(staticDir))
|
||||
func (s *Server) MountStatic(mux *http.ServeMux, prefix string, fs http.FileSystem) {
|
||||
if prefix == "" {
|
||||
prefix = "/"
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
// Cache: avoid caching during test
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
// CSP: no inline scripts/styles; allow XHR/SSE/Ws to any (tunnel/api) host
|
||||
w.Header().Set("Content-Security-Policy",
|
||||
strings.Join([]string{
|
||||
"default-src 'self'",
|
||||
"script-src 'self'",
|
||||
"style-src 'self'",
|
||||
"img-src 'self' data:",
|
||||
"font-src 'self'",
|
||||
"connect-src *",
|
||||
"frame-ancestors 'none'",
|
||||
"base-uri 'self'",
|
||||
"form-action 'self'",
|
||||
}, "; "),
|
||||
)
|
||||
}
|
||||
h := http.StripPrefix(prefix, http.FileServer(fs))
|
||||
mux.Handle(prefix, s.secureHeaders(h))
|
||||
|
||||
// File handler with index.html fallback for “/”.
|
||||
fileServer := http.FileServer(http.Dir(root))
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
addCommonHeaders(w)
|
||||
|
||||
// Serve index.html at root or when requesting a directory.
|
||||
p := r.URL.Path
|
||||
if p == "/" || p == "" {
|
||||
http.ServeFile(w, r, filepath.Join(root, "index.html"))
|
||||
return
|
||||
}
|
||||
|
||||
// If path maps to a directory, try its index.html.
|
||||
full := filepath.Join(root, filepath.Clean(strings.TrimPrefix(p, "/")))
|
||||
if st, err := os.Stat(full); err == nil && st.IsDir() {
|
||||
indexFile := filepath.Join(full, "index.html")
|
||||
if _, err := os.Stat(indexFile); err == nil {
|
||||
http.ServeFile(w, r, indexFile)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Normal static file.
|
||||
fileServer.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: handler,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
return srv.ListenAndServe()
|
||||
}
|
||||
|
@@ -1,240 +1,151 @@
|
||||
package storage
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type FSStore struct {
|
||||
root string
|
||||
objects string
|
||||
// SimpleFSStore is a minimal FS-backed implementation of BlobStore.
|
||||
// Layout under root:
|
||||
//
|
||||
// <root>/<hash> - content
|
||||
// <root>/<hash>.priv - presence means "private"
|
||||
type SimpleFSStore struct {
|
||||
root string
|
||||
}
|
||||
|
||||
func NewFS(dir string) (*FSStore, error) {
|
||||
if dir == "" {
|
||||
return nil, errors.New("empty storage dir")
|
||||
}
|
||||
o := filepath.Join(dir, "objects")
|
||||
if err := os.MkdirAll(o, 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &FSStore{root: dir, objects: o}, nil
|
||||
func NewSimpleFSStore(root string) *SimpleFSStore {
|
||||
return &SimpleFSStore{root: root}
|
||||
}
|
||||
|
||||
func (s *FSStore) pathFlat(hash string) (string, error) {
|
||||
if hash == "" {
|
||||
return "", errors.New("empty hash")
|
||||
}
|
||||
return filepath.Join(s.objects, hash), nil
|
||||
func (fs *SimpleFSStore) ensureRoot() error {
|
||||
return os.MkdirAll(fs.root, 0o755)
|
||||
}
|
||||
|
||||
func isHexHash(name string) bool {
|
||||
if len(name) != 64 {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < 64; i++ {
|
||||
c := name[i]
|
||||
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
func (fs *SimpleFSStore) pathFor(hash string) string {
|
||||
return filepath.Join(fs.root, hash)
|
||||
}
|
||||
func (fs *SimpleFSStore) privPathFor(hash string) string {
|
||||
return filepath.Join(fs.root, hash+".priv")
|
||||
}
|
||||
|
||||
func (s *FSStore) findBlobPath(hash string) (string, error) {
|
||||
if hash == "" {
|
||||
return "", errors.New("empty hash")
|
||||
}
|
||||
// 1) flat
|
||||
if p, _ := s.pathFlat(hash); fileExists(p) {
|
||||
return p, nil
|
||||
}
|
||||
// 2) objects/<hash>/{blob,data,content}
|
||||
dir := filepath.Join(s.objects, hash)
|
||||
for _, cand := range []string{"blob", "data", "content"} {
|
||||
p := filepath.Join(dir, cand)
|
||||
if fileExists(p) {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
// 3) objects/<hash>/<single file>
|
||||
if st, err := os.Stat(dir); err == nil && st.IsDir() {
|
||||
ents, _ := os.ReadDir(dir)
|
||||
var picked string
|
||||
var pickedMod time.Time
|
||||
for _, de := range ents {
|
||||
if de.IsDir() {
|
||||
continue
|
||||
}
|
||||
p := filepath.Join(dir, de.Name())
|
||||
fi, err := os.Stat(p)
|
||||
if err == nil && fi.Mode().IsRegular() {
|
||||
if picked == "" || fi.ModTime().After(pickedMod) {
|
||||
picked, pickedMod = p, fi.ModTime()
|
||||
}
|
||||
}
|
||||
}
|
||||
if picked != "" {
|
||||
return picked, nil
|
||||
}
|
||||
}
|
||||
// 4) two-level prefix objects/aa/<hash>
|
||||
if len(hash) >= 2 {
|
||||
p := filepath.Join(s.objects, hash[:2], hash)
|
||||
if fileExists(p) {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
// 5) recursive search
|
||||
var best string
|
||||
var bestMod time.Time
|
||||
_ = filepath.WalkDir(s.objects, func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
base := filepath.Base(p)
|
||||
if base == hash {
|
||||
best = p
|
||||
return fs.SkipDir
|
||||
}
|
||||
parent := filepath.Base(filepath.Dir(p))
|
||||
if parent == hash {
|
||||
if fi, err := os.Stat(p); err == nil && fi.Mode().IsRegular() {
|
||||
if best == "" || fi.ModTime().After(bestMod) {
|
||||
best, bestMod = p, fi.ModTime()
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if best != "" {
|
||||
return best, nil
|
||||
}
|
||||
return "", os.ErrNotExist
|
||||
}
|
||||
|
||||
func fileExists(p string) bool {
|
||||
fi, err := os.Stat(p)
|
||||
return err == nil && fi.Mode().IsRegular()
|
||||
}
|
||||
|
||||
func (s *FSStore) Put(hash string, r io.Reader) error {
|
||||
p, err := s.pathFlat(hash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
tmp := p + ".tmp"
|
||||
f, err := os.Create(tmp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, werr := io.Copy(f, r)
|
||||
cerr := f.Close()
|
||||
if werr != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return werr
|
||||
}
|
||||
if cerr != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return cerr
|
||||
}
|
||||
return os.Rename(tmp, p)
|
||||
}
|
||||
|
||||
func (s *FSStore) Get(hash string) (io.ReadCloser, int64, error) {
|
||||
p, err := s.findBlobPath(hash)
|
||||
if err != nil {
|
||||
// Get implements BlobStore.Get
|
||||
func (fs *SimpleFSStore) Get(hash string) (io.ReadCloser, int64, error) {
|
||||
if err := fs.ensureRoot(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
f, err := os.Open(p)
|
||||
f, err := os.Open(fs.pathFor(hash))
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
st, err := f.Stat()
|
||||
if err != nil {
|
||||
return f, 0, nil
|
||||
_ = f.Close()
|
||||
return nil, 0, err
|
||||
}
|
||||
return f, st.Size(), nil
|
||||
}
|
||||
|
||||
func (s *FSStore) Delete(hash string) error {
|
||||
if p, _ := s.pathFlat(hash); fileExists(p) {
|
||||
if err := os.Remove(p); err == nil || errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
// Put implements BlobStore.Put
|
||||
func (fs *SimpleFSStore) Put(r io.Reader, private bool) (string, int64, time.Time, error) {
|
||||
if err := fs.ensureRoot(); err != nil {
|
||||
return "", 0, time.Time{}, err
|
||||
}
|
||||
tmp, err := os.CreateTemp(fs.root, "put-*")
|
||||
if err != nil {
|
||||
return "", 0, time.Time{}, err
|
||||
}
|
||||
defer func() {
|
||||
_ = tmp.Close()
|
||||
_ = os.Remove(tmp.Name())
|
||||
}()
|
||||
|
||||
h := sha256.New()
|
||||
w := io.MultiWriter(tmp, h)
|
||||
|
||||
n, err := io.Copy(w, r)
|
||||
if err != nil {
|
||||
return "", 0, time.Time{}, err
|
||||
}
|
||||
hash := hex.EncodeToString(h.Sum(nil))
|
||||
|
||||
final := fs.pathFor(hash)
|
||||
if err := os.Rename(tmp.Name(), final); err != nil {
|
||||
return "", 0, time.Time{}, err
|
||||
}
|
||||
|
||||
if private {
|
||||
if err := os.WriteFile(fs.privPathFor(hash), nil, 0o600); err != nil {
|
||||
_ = os.Remove(final)
|
||||
return "", 0, time.Time{}, err
|
||||
}
|
||||
}
|
||||
dir := filepath.Join(s.objects, hash)
|
||||
for _, cand := range []string{"blob", "data", "content"} {
|
||||
p := filepath.Join(dir, cand)
|
||||
if fileExists(p) {
|
||||
if err := os.Remove(p); err == nil || errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
st, err := os.Stat(final)
|
||||
if err != nil {
|
||||
return "", 0, time.Time{}, err
|
||||
}
|
||||
if len(hash) >= 2 {
|
||||
p := filepath.Join(s.objects, hash[:2], hash)
|
||||
if fileExists(p) {
|
||||
if err := os.Remove(p); err == nil || errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if p, err := s.findBlobPath(hash); err == nil {
|
||||
if err := os.Remove(p); err == nil || errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return hash, n, st.ModTime().UTC(), nil
|
||||
}
|
||||
|
||||
func (s *FSStore) Walk(fn func(hash string, size int64, mod time.Time) error) error {
|
||||
type rec struct {
|
||||
size int64
|
||||
mod time.Time
|
||||
// Delete implements BlobStore.Delete
|
||||
func (fs *SimpleFSStore) Delete(hash string) error {
|
||||
if err := fs.ensureRoot(); err != nil {
|
||||
return err
|
||||
}
|
||||
agg := make(map[string]rec)
|
||||
_ = filepath.WalkDir(s.objects, func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
fi, err := os.Stat(p)
|
||||
if err != nil || !fi.Mode().IsRegular() {
|
||||
return nil
|
||||
}
|
||||
base := filepath.Base(p)
|
||||
if isHexHash(base) {
|
||||
if r, ok := agg[base]; !ok || fi.ModTime().After(r.mod) {
|
||||
agg[base] = rec{fi.Size(), fi.ModTime()}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
parent := filepath.Base(filepath.Dir(p))
|
||||
if isHexHash(parent) {
|
||||
if r, ok := agg[parent]; !ok || fi.ModTime().After(r.mod) {
|
||||
agg[parent] = rec{fi.Size(), fi.ModTime()}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if len(base) == 64 && isHexHash(strings.ToLower(base)) {
|
||||
if r, ok := agg[base]; !ok || fi.ModTime().After(r.mod) {
|
||||
agg[base] = rec{fi.Size(), fi.ModTime()}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
for h, r := range agg {
|
||||
if err := fn(h, r.size, r.mod); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
_ = os.Remove(fs.privPathFor(hash))
|
||||
return os.Remove(fs.pathFor(hash))
|
||||
}
|
||||
|
||||
// Walk implements BlobStore.Walk
|
||||
func (fs *SimpleFSStore) Walk(fn func(hash string, bytes int64, private bool, storedAt time.Time) error) (int, error) {
|
||||
if err := fs.ensureRoot(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
ents, err := os.ReadDir(fs.root)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
count := 0
|
||||
for _, e := range ents {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := e.Name()
|
||||
// skip sidecars and non-64-hex filenames
|
||||
if strings.HasSuffix(name, ".priv") || len(name) != 64 || !isHex(name) {
|
||||
continue
|
||||
}
|
||||
full := fs.pathFor(name)
|
||||
st, err := os.Stat(full)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
private := false
|
||||
if _, err := os.Stat(fs.privPathFor(name)); err == nil {
|
||||
private = true
|
||||
}
|
||||
if err := fn(name, st.Size(), private, st.ModTime().UTC()); err != nil {
|
||||
return count, err
|
||||
}
|
||||
count++
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func isHex(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if !((c >= '0' && c <= '9') ||
|
||||
(c >= 'a' && c <= 'f') ||
|
||||
(c >= 'A' && c <= 'F')) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
Reference in New Issue
Block a user