mirror of
https://codeberg.org/git-pages/git-pages.git
synced 2026-05-14 03:01:48 +00:00
Allow domains to be administratively frozen.
The following script may be used to handle abusive sites:
cd $(mktemp -d)
echo "<h1>Gone</h1>" >index.html
echo "/* /index.html 410" >_redirects
tar cf site.tar index.html _redirects
git-pages -update-site $1 site.tar
git-pages -freeze-domain $1
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
)
|
||||
|
||||
var ErrObjectNotFound = errors.New("not found")
|
||||
var ErrDomainFrozen = errors.New("domain administratively frozen")
|
||||
|
||||
func splitBlobName(name string) []string {
|
||||
algo, hash, found := strings.Cut(name, "-")
|
||||
@@ -76,6 +77,10 @@ type Backend interface {
|
||||
|
||||
// Creates a domain. This allows us to start serving content for the domain.
|
||||
CreateDomain(ctx context.Context, domain string) error
|
||||
|
||||
// Freeze or thaw a domain. This allows a site to be administratively locked, e.g. if it
|
||||
// is discovered serving abusive content.
|
||||
FreezeDomain(ctx context.Context, domain string, freeze bool) error
|
||||
}
|
||||
|
||||
func CreateBackend(config *StorageConfig) (backend Backend, err error) {
|
||||
|
||||
@@ -208,7 +208,26 @@ func (fs *FSBackend) StageManifest(ctx context.Context, manifest *Manifest) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
func domainFrozenMarkerName(domain string) string {
|
||||
return filepath.Join(domain, ".frozen")
|
||||
}
|
||||
|
||||
func (fs *FSBackend) checkDomainFrozen(_ctx context.Context, domain string) error {
|
||||
if _, err := fs.siteRoot.Stat(domainFrozenMarkerName(domain)); err == nil {
|
||||
return ErrDomainFrozen
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("stat: %w", err)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *FSBackend) CommitManifest(ctx context.Context, name string, manifest *Manifest) error {
|
||||
domain := filepath.Dir(name)
|
||||
if err := fs.checkDomainFrozen(ctx, domain); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manifestData := EncodeManifest(manifest)
|
||||
manifestHashName := stagedManifestName(manifestData)
|
||||
|
||||
@@ -216,7 +235,7 @@ func (fs *FSBackend) CommitManifest(ctx context.Context, name string, manifest *
|
||||
return fmt.Errorf("manifest not staged")
|
||||
}
|
||||
|
||||
if err := fs.siteRoot.MkdirAll(filepath.Dir(name), 0o755); err != nil {
|
||||
if err := fs.siteRoot.MkdirAll(domain, 0o755); err != nil {
|
||||
return fmt.Errorf("mkdir: %w", err)
|
||||
}
|
||||
|
||||
@@ -228,6 +247,11 @@ func (fs *FSBackend) CommitManifest(ctx context.Context, name string, manifest *
|
||||
}
|
||||
|
||||
func (fs *FSBackend) DeleteManifest(ctx context.Context, name string) error {
|
||||
domain := filepath.Dir(name)
|
||||
if err := fs.checkDomainFrozen(ctx, domain); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := fs.siteRoot.Remove(name)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
@@ -250,3 +274,16 @@ func (fs *FSBackend) CheckDomain(ctx context.Context, domain string) (bool, erro
|
||||
func (fs *FSBackend) CreateDomain(ctx context.Context, domain string) error {
|
||||
return nil // no-op
|
||||
}
|
||||
|
||||
func (fs *FSBackend) FreezeDomain(ctx context.Context, domain string, freeze bool) error {
|
||||
if freeze {
|
||||
return fs.siteRoot.WriteFile(domainFrozenMarkerName(domain), []byte{}, 0o644)
|
||||
} else {
|
||||
err := fs.siteRoot.Remove(domainFrozenMarkerName(domain))
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -499,10 +499,31 @@ func (s3 *S3Backend) StageManifest(ctx context.Context, manifest *Manifest) erro
|
||||
return err
|
||||
}
|
||||
|
||||
func domainFrozenObjectName(domain string) string {
|
||||
return manifestObjectName(fmt.Sprintf("%s/.frozen", domain))
|
||||
}
|
||||
|
||||
func (s3 *S3Backend) checkDomainFrozen(ctx context.Context, domain string) error {
|
||||
_, err := s3.client.GetObject(ctx, s3.bucket, domainFrozenObjectName(domain),
|
||||
minio.GetObjectOptions{})
|
||||
if err == nil {
|
||||
return ErrDomainFrozen
|
||||
} else if errResp := minio.ToErrorResponse(err); errResp.Code == "NoSuchKey" {
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (s3 *S3Backend) CommitManifest(ctx context.Context, name string, manifest *Manifest) error {
|
||||
data := EncodeManifest(manifest)
|
||||
logc.Printf(ctx, "s3: commit manifest %x -> %s", sha256.Sum256(data), name)
|
||||
|
||||
_, domain, _ := strings.Cut(name, "/")
|
||||
if err := s3.checkDomainFrozen(ctx, domain); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove staged object unconditionally (whether commit succeeded or failed), since
|
||||
// the upper layer has to retry the complete operation anyway.
|
||||
_, putErr := s3.client.PutObject(ctx, s3.bucket, manifestObjectName(name),
|
||||
@@ -522,6 +543,11 @@ func (s3 *S3Backend) CommitManifest(ctx context.Context, name string, manifest *
|
||||
func (s3 *S3Backend) DeleteManifest(ctx context.Context, name string) error {
|
||||
logc.Printf(ctx, "s3: delete manifest %s\n", name)
|
||||
|
||||
_, domain, _ := strings.Cut(name, "/")
|
||||
if err := s3.checkDomainFrozen(ctx, domain); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := s3.client.RemoveObject(ctx, s3.bucket, manifestObjectName(name),
|
||||
minio.RemoveObjectOptions{})
|
||||
s3.siteCache.Cache.Invalidate(name)
|
||||
@@ -570,3 +596,23 @@ func (s3 *S3Backend) CreateDomain(ctx context.Context, domain string) error {
|
||||
&bytes.Reader{}, 0, minio.PutObjectOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s3 *S3Backend) FreezeDomain(ctx context.Context, domain string, freeze bool) error {
|
||||
if freeze {
|
||||
logc.Printf(ctx, "s3: freeze domain %s\n", domain)
|
||||
|
||||
_, err := s3.client.PutObject(ctx, s3.bucket, domainFrozenObjectName(domain),
|
||||
&bytes.Reader{}, 0, minio.PutObjectOptions{})
|
||||
return err
|
||||
} else {
|
||||
logc.Printf(ctx, "s3: thaw domain %s\n", domain)
|
||||
|
||||
err := s3.client.RemoveObject(ctx, s3.bucket, domainFrozenObjectName(domain),
|
||||
minio.RemoveObjectOptions{})
|
||||
if errResp := minio.ToErrorResponse(err); errResp.Code == "NoSuchKey" {
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
44
src/main.go
44
src/main.go
@@ -141,7 +141,7 @@ func usage() {
|
||||
fmt.Fprintf(os.Stderr, "(server) "+
|
||||
"git-pages [-config <file>|-no-config]\n")
|
||||
fmt.Fprintf(os.Stderr, "(admin) "+
|
||||
"git-pages {-run-migration <name>}\n")
|
||||
"git-pages {-run-migration <name>|-freeze-domain <domain>|-unfreeze-domain <domain>}\n")
|
||||
fmt.Fprintf(os.Stderr, "(info) "+
|
||||
"git-pages {-print-config-env-vars|-print-config}\n")
|
||||
fmt.Fprintf(os.Stderr, "(cli) "+
|
||||
@@ -169,9 +169,16 @@ func Main() {
|
||||
"write archive for `site` (either 'domain.tld' or 'domain.tld/dir') in tar format")
|
||||
updateSite := flag.String("update-site", "",
|
||||
"update `site` (either 'domain.tld' or 'domain.tld/dir') from archive or repository URL")
|
||||
freezeDomain := flag.String("freeze-domain", "",
|
||||
"prevent any site uploads to a given `domain`")
|
||||
unfreezeDomain := flag.String("unfreeze-domain", "",
|
||||
"allow site uploads to a `domain` again after it has been frozen")
|
||||
flag.Parse()
|
||||
|
||||
var cliOperations int
|
||||
if *runMigration != "" {
|
||||
cliOperations += 1
|
||||
}
|
||||
if *getBlob != "" {
|
||||
cliOperations += 1
|
||||
}
|
||||
@@ -181,8 +188,17 @@ func Main() {
|
||||
if *getArchive != "" {
|
||||
cliOperations += 1
|
||||
}
|
||||
if *updateSite != "" {
|
||||
cliOperations += 1
|
||||
}
|
||||
if *freezeDomain != "" {
|
||||
cliOperations += 1
|
||||
}
|
||||
if *unfreezeDomain != "" {
|
||||
cliOperations += 1
|
||||
}
|
||||
if cliOperations > 1 {
|
||||
log.Fatalln("-get-blob, -get-manifest, and -get-archive are mutually exclusive")
|
||||
log.Fatalln("-get-blob, -get-manifest, -get-archive, -update-site, -freeze, and -unfreeze are mutually exclusive")
|
||||
}
|
||||
|
||||
if *configTomlPath != "" && *noConfig {
|
||||
@@ -329,6 +345,30 @@ func Main() {
|
||||
log.Println("no-change")
|
||||
}
|
||||
|
||||
case *freezeDomain != "" || *unfreezeDomain != "":
|
||||
var domain string
|
||||
var freeze bool
|
||||
if *freezeDomain != "" {
|
||||
domain = *freezeDomain
|
||||
freeze = true
|
||||
} else {
|
||||
domain = *unfreezeDomain
|
||||
freeze = false
|
||||
}
|
||||
|
||||
if backend, err = CreateBackend(&config.Storage); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
if err = backend.FreezeDomain(context.Background(), domain, freeze); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
if freeze {
|
||||
log.Println("frozen")
|
||||
} else {
|
||||
log.Println("thawed")
|
||||
}
|
||||
|
||||
default:
|
||||
// Hook a signal (SIGHUP on *nix, nothing on Windows) for reloading the configuration
|
||||
// at runtime. This is useful because it preserves S3 backend cache contents. Failed
|
||||
|
||||
@@ -311,7 +311,11 @@ func StoreManifest(ctx context.Context, name string, manifest *Manifest) (*Manif
|
||||
}
|
||||
|
||||
if err := backend.CommitManifest(ctx, name, &extManifest); err != nil {
|
||||
return nil, fmt.Errorf("commit manifest: %w", err)
|
||||
if errors.Is(err, ErrDomainFrozen) {
|
||||
return nil, err
|
||||
} else {
|
||||
return nil, fmt.Errorf("commit manifest: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &extManifest, nil
|
||||
|
||||
@@ -417,15 +417,22 @@ func (backend *observedBackend) DeleteManifest(ctx context.Context, name string)
|
||||
}
|
||||
|
||||
func (backend *observedBackend) CheckDomain(ctx context.Context, domain string) (found bool, err error) {
|
||||
span, ctx := ObserveFunction(ctx, "CheckDomain", "manifest.domain", domain)
|
||||
span, ctx := ObserveFunction(ctx, "CheckDomain", "domain.name", domain)
|
||||
found, err = backend.inner.CheckDomain(ctx, domain)
|
||||
span.Finish()
|
||||
return
|
||||
}
|
||||
|
||||
func (backend *observedBackend) CreateDomain(ctx context.Context, domain string) (err error) {
|
||||
span, ctx := ObserveFunction(ctx, "CreateDomain", "manifest.domain", domain)
|
||||
span, ctx := ObserveFunction(ctx, "CreateDomain", "domain.name", domain)
|
||||
err = backend.inner.CreateDomain(ctx, domain)
|
||||
span.Finish()
|
||||
return
|
||||
}
|
||||
|
||||
func (backend *observedBackend) FreezeDomain(ctx context.Context, domain string, freeze bool) (err error) {
|
||||
span, ctx := ObserveFunction(ctx, "FreezeDomain", "domain.name", domain, "domain.frozen", freeze)
|
||||
err = backend.inner.FreezeDomain(ctx, domain, freeze)
|
||||
span.Finish()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -489,6 +489,8 @@ func putPage(w http.ResponseWriter, r *http.Request) error {
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
} else if errors.Is(result.err, ErrArchiveTooLarge) {
|
||||
w.WriteHeader(http.StatusRequestEntityTooLarge)
|
||||
} else if errors.Is(result.err, ErrDomainFrozen) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user