+5
src/backend.go
+5
src/backend.go
···
11
11
)
12
12
13
13
var ErrObjectNotFound = errors.New("not found")
14
+
var ErrDomainFrozen = errors.New("domain administratively frozen")
14
15
15
16
func splitBlobName(name string) []string {
16
17
algo, hash, found := strings.Cut(name, "-")
···
76
77
77
78
// Creates a domain. This allows us to start serving content for the domain.
78
79
CreateDomain(ctx context.Context, domain string) error
80
+
81
+
// Freeze or thaw a domain. This allows a site to be administratively locked, e.g. if it
82
+
// is discovered serving abusive content.
83
+
FreezeDomain(ctx context.Context, domain string, freeze bool) error
79
84
}
80
85
81
86
func CreateBackend(config *StorageConfig) (backend Backend, err error) {
+38
-1
src/backend_fs.go
+38
-1
src/backend_fs.go
···
208
208
return nil
209
209
}
210
210
211
+
func domainFrozenMarkerName(domain string) string {
212
+
return filepath.Join(domain, ".frozen")
213
+
}
214
+
215
+
func (fs *FSBackend) checkDomainFrozen(_ctx context.Context, domain string) error {
216
+
if _, err := fs.siteRoot.Stat(domainFrozenMarkerName(domain)); err == nil {
217
+
return ErrDomainFrozen
218
+
} else if !errors.Is(err, os.ErrNotExist) {
219
+
return fmt.Errorf("stat: %w", err)
220
+
} else {
221
+
return nil
222
+
}
223
+
}
224
+
211
225
func (fs *FSBackend) CommitManifest(ctx context.Context, name string, manifest *Manifest) error {
226
+
domain := filepath.Dir(name)
227
+
if err := fs.checkDomainFrozen(ctx, domain); err != nil {
228
+
return err
229
+
}
230
+
212
231
manifestData := EncodeManifest(manifest)
213
232
manifestHashName := stagedManifestName(manifestData)
214
233
···
216
235
return fmt.Errorf("manifest not staged")
217
236
}
218
237
219
-
if err := fs.siteRoot.MkdirAll(filepath.Dir(name), 0o755); err != nil {
238
+
if err := fs.siteRoot.MkdirAll(domain, 0o755); err != nil {
220
239
return fmt.Errorf("mkdir: %w", err)
221
240
}
222
241
···
228
247
}
229
248
230
249
func (fs *FSBackend) DeleteManifest(ctx context.Context, name string) error {
250
+
domain := filepath.Dir(name)
251
+
if err := fs.checkDomainFrozen(ctx, domain); err != nil {
252
+
return err
253
+
}
254
+
231
255
err := fs.siteRoot.Remove(name)
232
256
if errors.Is(err, os.ErrNotExist) {
233
257
return nil
···
250
274
func (fs *FSBackend) CreateDomain(ctx context.Context, domain string) error {
251
275
return nil // no-op
252
276
}
277
+
278
+
func (fs *FSBackend) FreezeDomain(ctx context.Context, domain string, freeze bool) error {
279
+
if freeze {
280
+
return fs.siteRoot.WriteFile(domainFrozenMarkerName(domain), []byte{}, 0o644)
281
+
} else {
282
+
err := fs.siteRoot.Remove(domainFrozenMarkerName(domain))
283
+
if errors.Is(err, os.ErrNotExist) {
284
+
return nil
285
+
} else {
286
+
return err
287
+
}
288
+
}
289
+
}
+46
src/backend_s3.go
+46
src/backend_s3.go
···
499
499
return err
500
500
}
501
501
502
+
func domainFrozenObjectName(domain string) string {
503
+
return manifestObjectName(fmt.Sprintf("%s/.frozen", domain))
504
+
}
505
+
506
+
func (s3 *S3Backend) checkDomainFrozen(ctx context.Context, domain string) error {
507
+
_, err := s3.client.GetObject(ctx, s3.bucket, domainFrozenObjectName(domain),
508
+
minio.GetObjectOptions{})
509
+
if err == nil {
510
+
return ErrDomainFrozen
511
+
} else if errResp := minio.ToErrorResponse(err); errResp.Code == "NoSuchKey" {
512
+
return nil
513
+
} else {
514
+
return err
515
+
}
516
+
}
517
+
502
518
func (s3 *S3Backend) CommitManifest(ctx context.Context, name string, manifest *Manifest) error {
503
519
data := EncodeManifest(manifest)
504
520
logc.Printf(ctx, "s3: commit manifest %x -> %s", sha256.Sum256(data), name)
521
+
522
+
_, domain, _ := strings.Cut(name, "/")
523
+
if err := s3.checkDomainFrozen(ctx, domain); err != nil {
524
+
return err
525
+
}
505
526
506
527
// Remove staged object unconditionally (whether commit succeeded or failed), since
507
528
// the upper layer has to retry the complete operation anyway.
···
522
543
func (s3 *S3Backend) DeleteManifest(ctx context.Context, name string) error {
523
544
logc.Printf(ctx, "s3: delete manifest %s\n", name)
524
545
546
+
_, domain, _ := strings.Cut(name, "/")
547
+
if err := s3.checkDomainFrozen(ctx, domain); err != nil {
548
+
return err
549
+
}
550
+
525
551
err := s3.client.RemoveObject(ctx, s3.bucket, manifestObjectName(name),
526
552
minio.RemoveObjectOptions{})
527
553
s3.siteCache.Cache.Invalidate(name)
···
570
596
&bytes.Reader{}, 0, minio.PutObjectOptions{})
571
597
return err
572
598
}
599
+
600
+
func (s3 *S3Backend) FreezeDomain(ctx context.Context, domain string, freeze bool) error {
601
+
if freeze {
602
+
logc.Printf(ctx, "s3: freeze domain %s\n", domain)
603
+
604
+
_, err := s3.client.PutObject(ctx, s3.bucket, domainFrozenObjectName(domain),
605
+
&bytes.Reader{}, 0, minio.PutObjectOptions{})
606
+
return err
607
+
} else {
608
+
logc.Printf(ctx, "s3: thaw domain %s\n", domain)
609
+
610
+
err := s3.client.RemoveObject(ctx, s3.bucket, domainFrozenObjectName(domain),
611
+
minio.RemoveObjectOptions{})
612
+
if errResp := minio.ToErrorResponse(err); errResp.Code == "NoSuchKey" {
613
+
return nil
614
+
} else {
615
+
return err
616
+
}
617
+
}
618
+
}
+42
-2
src/main.go
+42
-2
src/main.go
···
141
141
fmt.Fprintf(os.Stderr, "(server) "+
142
142
"git-pages [-config <file>|-no-config]\n")
143
143
fmt.Fprintf(os.Stderr, "(admin) "+
144
-
"git-pages {-run-migration <name>}\n")
144
+
"git-pages {-run-migration <name>|-freeze-domain <domain>|-unfreeze-domain <domain>}\n")
145
145
fmt.Fprintf(os.Stderr, "(info) "+
146
146
"git-pages {-print-config-env-vars|-print-config}\n")
147
147
fmt.Fprintf(os.Stderr, "(cli) "+
···
169
169
"write archive for `site` (either 'domain.tld' or 'domain.tld/dir') in tar format")
170
170
updateSite := flag.String("update-site", "",
171
171
"update `site` (either 'domain.tld' or 'domain.tld/dir') from archive or repository URL")
172
+
freezeDomain := flag.String("freeze-domain", "",
173
+
"prevent any site uploads to a given `domain`")
174
+
unfreezeDomain := flag.String("unfreeze-domain", "",
175
+
"allow site uploads to a `domain` again after it has been frozen")
172
176
flag.Parse()
173
177
174
178
var cliOperations int
179
+
if *runMigration != "" {
180
+
cliOperations += 1
181
+
}
175
182
if *getBlob != "" {
176
183
cliOperations += 1
177
184
}
···
181
188
if *getArchive != "" {
182
189
cliOperations += 1
183
190
}
191
+
if *updateSite != "" {
192
+
cliOperations += 1
193
+
}
194
+
if *freezeDomain != "" {
195
+
cliOperations += 1
196
+
}
197
+
if *unfreezeDomain != "" {
198
+
cliOperations += 1
199
+
}
184
200
if cliOperations > 1 {
185
-
log.Fatalln("-get-blob, -get-manifest, and -get-archive are mutually exclusive")
201
+
log.Fatalln("-get-blob, -get-manifest, -get-archive, -update-site, -freeze, and -unfreeze are mutually exclusive")
186
202
}
187
203
188
204
if *configTomlPath != "" && *noConfig {
···
327
343
log.Println("deleted")
328
344
case UpdateNoChange:
329
345
log.Println("no-change")
346
+
}
347
+
348
+
case *freezeDomain != "" || *unfreezeDomain != "":
349
+
var domain string
350
+
var freeze bool
351
+
if *freezeDomain != "" {
352
+
domain = *freezeDomain
353
+
freeze = true
354
+
} else {
355
+
domain = *unfreezeDomain
356
+
freeze = false
357
+
}
358
+
359
+
if backend, err = CreateBackend(&config.Storage); err != nil {
360
+
log.Fatalln(err)
361
+
}
362
+
363
+
if err = backend.FreezeDomain(context.Background(), domain, freeze); err != nil {
364
+
log.Fatalln(err)
365
+
}
366
+
if freeze {
367
+
log.Println("frozen")
368
+
} else {
369
+
log.Println("thawed")
330
370
}
331
371
332
372
default:
+5
-1
src/manifest.go
+5
-1
src/manifest.go
···
311
311
}
312
312
313
313
if err := backend.CommitManifest(ctx, name, &extManifest); err != nil {
314
-
return nil, fmt.Errorf("commit manifest: %w", err)
314
+
if errors.Is(err, ErrDomainFrozen) {
315
+
return nil, err
316
+
} else {
317
+
return nil, fmt.Errorf("commit manifest: %w", err)
318
+
}
315
319
}
316
320
317
321
return &extManifest, nil
+9
-2
src/observe.go
+9
-2
src/observe.go
···
417
417
}
418
418
419
419
func (backend *observedBackend) CheckDomain(ctx context.Context, domain string) (found bool, err error) {
420
-
span, ctx := ObserveFunction(ctx, "CheckDomain", "manifest.domain", domain)
420
+
span, ctx := ObserveFunction(ctx, "CheckDomain", "domain.name", domain)
421
421
found, err = backend.inner.CheckDomain(ctx, domain)
422
422
span.Finish()
423
423
return
424
424
}
425
425
426
426
func (backend *observedBackend) CreateDomain(ctx context.Context, domain string) (err error) {
427
-
span, ctx := ObserveFunction(ctx, "CreateDomain", "manifest.domain", domain)
427
+
span, ctx := ObserveFunction(ctx, "CreateDomain", "domain.name", domain)
428
428
err = backend.inner.CreateDomain(ctx, domain)
429
429
span.Finish()
430
430
return
431
431
}
432
+
433
+
func (backend *observedBackend) FreezeDomain(ctx context.Context, domain string, freeze bool) (err error) {
434
+
span, ctx := ObserveFunction(ctx, "FreezeDomain", "domain.name", domain, "domain.frozen", freeze)
435
+
err = backend.inner.FreezeDomain(ctx, domain, freeze)
436
+
span.Finish()
437
+
return
438
+
}
+2
src/pages.go
+2
src/pages.go
···
489
489
w.WriteHeader(http.StatusUnsupportedMediaType)
490
490
} else if errors.Is(result.err, ErrArchiveTooLarge) {
491
491
w.WriteHeader(http.StatusRequestEntityTooLarge)
492
+
} else if errors.Is(result.err, ErrDomainFrozen) {
493
+
w.WriteHeader(http.StatusForbidden)
492
494
} else {
493
495
w.WriteHeader(http.StatusServiceUnavailable)
494
496
}