Add Netlify _headers support.

This commit is contained in:
Catherine
2025-11-11 15:13:46 +00:00
parent 2a6308eb05
commit 26b29ec4be
14 changed files with 414 additions and 70 deletions
+1
View File
@@ -47,6 +47,7 @@ update-timeout = "60s"
max-heap-size-ratio = 0.5 # * RAM_size
forbidden-domains = []
# allowed-repository-url-prefixes = <nil>
allowed-custom-headers = []
[observability]
slow-response-threshold = "500ms"
+1 -1
View File
@@ -43,7 +43,7 @@
"-s -w"
];
vendorHash = "sha256-qxqIz8hubOBpvJ60h44kY/GbFcZg15WA1JxyskNKtKk=";
vendorHash = "sha256-UQl8AeijqJd2qpVZBDuHT/+Dtd3+Uwrf4w4yAOaFs98=";
};
in
{
+7
View File
@@ -3,6 +3,7 @@ module codeberg.org/git-pages/git-pages
go 1.25.0
require (
codeberg.org/git-pages/go-headers v1.0.0
github.com/KimMachineGun/automemlimit v0.7.5
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500
github.com/creasty/defaults v1.8.0
@@ -10,6 +11,7 @@ require (
github.com/getsentry/sentry-go/slog v0.36.2
github.com/go-git/go-billy/v6 v6.0.0-20251026101908-623011986e70
github.com/go-git/go-git/v6 v6.0.0-20251029213217-0bbfc0875edd
github.com/honeycombio/otel-config-go v1.17.0
github.com/klauspost/compress v1.18.1
github.com/maypok86/otter/v2 v2.2.1
github.com/minio/minio-go/v7 v7.0.95
@@ -19,6 +21,11 @@ require (
github.com/samber/slog-multi v1.5.0
github.com/tj/go-redirects v0.0.0-20200911105812-fd1ba1020b37
github.com/valyala/fasttemplate v1.2.2
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0
go.opentelemetry.io/otel/log v0.14.0
go.opentelemetry.io/otel/sdk/log v0.14.0
google.golang.org/protobuf v1.36.10
)
+2
View File
@@ -1,3 +1,5 @@
codeberg.org/git-pages/go-headers v1.0.0 h1:hvGU97hQdXaT5HwCpZJWQdg7akvtOBCSUNL4u2a5uTs=
codeberg.org/git-pages/go-headers v1.0.0/go.mod h1:N4gwH0U3YPwmuyxqH7xBA8j44fTPX+vOEP7ejJVBPts=
github.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43DwZrJqVwyveTk=
github.com/KimMachineGun/automemlimit v0.7.5/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"time"
)
var ObjectNotFoundError = errors.New("not found")
var ErrObjectNotFound = errors.New("not found")
func splitBlobName(name string) []string {
algo, hash, found := strings.Cut(name, "-")
+2 -2
View File
@@ -100,7 +100,7 @@ func (fs *FSBackend) GetBlob(
blobPath := filepath.Join(splitBlobName(name)...)
stat, err := fs.blobRoot.Stat(blobPath)
if errors.Is(err, os.ErrNotExist) {
err = fmt.Errorf("%w: %s", ObjectNotFoundError, err.(*os.PathError).Path)
err = fmt.Errorf("%w: %s", ErrObjectNotFound, err.(*os.PathError).Path)
return
} else if err != nil {
err = fmt.Errorf("stat: %w", err)
@@ -171,7 +171,7 @@ func (b *FSBackend) ListManifests(ctx context.Context) (manifests []string, err
func (fs *FSBackend) GetManifest(ctx context.Context, name string, opts GetManifestOptions) (*Manifest, error) {
data, err := fs.siteRoot.ReadFile(name)
if errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("%w: %s", ObjectNotFoundError, err.(*os.PathError).Path)
return nil, fmt.Errorf("%w: %s", ErrObjectNotFound, err.(*os.PathError).Path)
} else if err != nil {
return nil, err
}
+2 -2
View File
@@ -299,7 +299,7 @@ func (s3 *S3Backend) GetBlob(
cached, err = s3.blobCache.Get(ctx, name, otter.LoaderFunc[string, *CachedBlob](loader))
if err != nil {
if errResp := minio.ToErrorResponse(err); errResp.Code == "NoSuchKey" {
err = fmt.Errorf("%w: %s", ObjectNotFoundError, errResp.Key)
err = fmt.Errorf("%w: %s", ErrObjectNotFound, errResp.Key)
}
} else {
reader = bytes.NewReader(cached.blob)
@@ -433,7 +433,7 @@ func (l s3ManifestLoader) load(ctx context.Context, name string, oldManifest *Ca
if err != nil {
if errResp := minio.ToErrorResponse(err); errResp.Code == "NoSuchKey" {
err = fmt.Errorf("%w: %s", ObjectNotFoundError, errResp.Key)
err = fmt.Errorf("%w: %s", ErrObjectNotFound, errResp.Key)
return &CachedManifest{nil, 1, etag, err}, nil
} else if errResp.StatusCode == http.StatusNotModified && oldManifest != nil {
return oldManifest, nil
+4
View File
@@ -109,6 +109,10 @@ type LimitsConfig struct {
ForbiddenDomains []string `toml:"forbidden-domains" default:"[]"`
// List of allowed repository URL prefixes. Setting this option prohibits uploading archives.
AllowedRepositoryURLPrefixes []string `toml:"allowed-repository-url-prefixes"`
// List of allowed custom headers. Header name must be in the MIME canonical form,
// e.g. `Foo-Bar`. Setting this option permits including this custom header in `_headers`,
// unless it is fundamentally unsafe.
AllowedCustomHeaders []string `toml:"allowed-custom-headers" default:"[]"`
}
type ObservabilityConfig struct {
+161
View File
@@ -0,0 +1,161 @@
package git_pages
import (
"errors"
"fmt"
"net/http"
"net/textproto"
"net/url"
"slices"
"strings"
"codeberg.org/git-pages/go-headers"
"google.golang.org/protobuf/proto"
)
var ErrHeaderNotAllowed = errors.New("custom header not allowed")
const headersFileName string = "_headers"
// Lifted from https://docs.netlify.com/manage/routing/headers/, except for `Set-Cookie`
// the rationale for which does not apply in our environment.
var unsafeHeaders []string = []string{
"Accept-Ranges",
"Age",
"Allow",
"Alt-Svc",
"Connection",
"Content-Encoding",
"Content-Length",
"Content-Range",
"Date",
"Location", // use `_redirects` instead
"Server",
"Trailer",
"Transfer-Encoding",
"Upgrade",
}
func IsAllowedCustomHeader(header string) bool {
header = textproto.CanonicalMIMEHeaderKey(header)
switch {
case slices.Contains(unsafeHeaders, header):
return false // explicitly unsafe
case slices.Contains(config.Limits.AllowedCustomHeaders, header):
return true // explicitly allowlisted
default:
return false // deny by default; we don't know what the future holds
}
}
func validateHeaderRule(rule headers.Rule) error {
url, err := url.Parse(rule.Path)
if err != nil {
return fmt.Errorf("malformed path")
}
if url.Scheme != "" {
return fmt.Errorf("path must not contain a scheme")
}
if !strings.HasPrefix(url.Path, "/") {
return fmt.Errorf("path must start with a /")
}
// Per Netlify documentation:
// > Wildcards (*) can be used at any place inside of a path segment to match any character.
// However, we currently do not implement this, for simplicity. Instead we implement a strict
// subset of the syntactically allowed wildcards.
if strings.Contains(url.Path, "*") && !strings.HasSuffix(url.Path, "/*") {
return fmt.Errorf("splat * must be its own final segment of the path")
}
// Note that this isn't our only line of defense against forbidden headers;
// the purpose of this check is just to inform the uploader of a problem.
// If the validation rules change after a manifest is uploaded, we could
// still end up attempting to serve a forbidden header.
for header := range rule.Headers {
if slices.Contains(unsafeHeaders, header) {
return fmt.Errorf("rule sets header %q (fundamentally unsafe)", header)
}
if !slices.Contains(config.Limits.AllowedCustomHeaders, header) {
return fmt.Errorf("rule sets header %q (not allowlisted)", header)
}
if !IsAllowedCustomHeader(header) { // make sure we don't desync
panic(errors.New("header check inconsistency"))
}
}
return nil
}
// Parses redirects file and injects rules into the manifest.
func ProcessHeadersFile(manifest *Manifest) error {
headersEntry := manifest.Contents[headersFileName]
delete(manifest.Contents, headersFileName)
if headersEntry == nil {
return nil
} else if headersEntry.GetType() != Type_InlineFile {
return AddProblem(manifest, headersFileName,
"not a regular file")
}
rules, err := headers.ParseString(string(headersEntry.GetData()))
if err != nil {
return AddProblem(manifest, headersFileName,
"syntax error: %s", err)
}
for index, rule := range rules {
if err := validateHeaderRule(rule); err != nil {
return AddProblem(manifest, headersFileName,
"rule #%d %q: %s", index+1, rule.Path, err)
}
headerMap := []*Header{}
for header, values := range rule.Headers {
headerMap = append(headerMap, &Header{
Name: proto.String(header),
Values: values,
})
}
manifest.Headers = append(manifest.Headers, &HeaderRule{
Path: proto.String(rule.Path),
HeaderMap: headerMap,
})
}
return nil
}
func ApplyHeaderRules(manifest *Manifest, url *url.URL) (headers http.Header, err error) {
headers = http.Header{}
fromSegments := pathSegments(url.Path)
next:
for _, rule := range manifest.Headers {
// check if the rule matches url
ruleURL, _ := url.Parse(*rule.Path) // pre-validated in `validateHeaderRule`
ruleSegments := pathSegments(ruleURL.Path)
if ruleSegments[len(ruleSegments)-1] != "*" {
if len(ruleSegments) < len(fromSegments) {
continue
}
}
for index, ruleFromSegment := range ruleSegments {
if ruleFromSegment == "*" {
break
}
if len(fromSegments) <= index {
continue next
}
if fromSegments[index] != ruleFromSegment {
continue next
}
}
// the rule has matched url, validate headers against up-to-date policy
for _, header := range rule.HeaderMap {
name := header.GetName()
if !IsAllowedCustomHeader(name) {
return nil, fmt.Errorf("%w: %s", ErrHeaderNotAllowed, name)
}
for _, value := range header.GetValues() {
headers.Add(name, value)
}
}
break
}
return
}
+9 -1
View File
@@ -200,12 +200,19 @@ func CompressFiles(ctx context.Context, manifest *Manifest) {
// (Perhaps in the future they could be exposed at `.git-pages/status.txt`?)
func PrepareManifest(ctx context.Context, manifest *Manifest) error {
// Parse Netlify-style `_redirects`
if err := ProcessRedirects(manifest); err != nil {
if err := ProcessRedirectsFile(manifest); err != nil {
log.Printf("redirects err: %s\n", err)
} else if len(manifest.Redirects) > 0 {
log.Printf("redirects ok: %d rules\n", len(manifest.Redirects))
}
// Parse Netlify-style `_headers`
if err := ProcessHeadersFile(manifest); err != nil {
log.Printf("headers err: %s\n", err)
} else if len(manifest.Headers) > 0 {
log.Printf("headers ok: %d rules\n", len(manifest.Headers))
}
DetectContentType(manifest)
if config.Feature("compress") {
@@ -230,6 +237,7 @@ func StoreManifest(ctx context.Context, name string, manifest *Manifest) (*Manif
Commit: manifest.Commit,
Contents: make(map[string]*Entry),
Redirects: manifest.Redirects,
Headers: manifest.Headers,
Problems: manifest.Problems,
TotalSize: proto.Int64(0),
StoredSize: proto.Int64(0),
+26 -3
View File
@@ -8,6 +8,7 @@ import (
"fmt"
"io"
"log"
"maps"
"net/http"
"net/url"
"os"
@@ -109,10 +110,10 @@ func getPage(w http.ResponseWriter, r *http.Request) error {
sitePath, manifest = projectPath, projectManifest
}
}
if manifest == nil && (err == nil || errors.Is(err, ObjectNotFoundError)) {
if manifest == nil && (err == nil || errors.Is(err, ErrObjectNotFound)) {
result := <-indexManifestCh
manifest, err = result.manifest, result.err
if manifest == nil && errors.Is(err, ObjectNotFoundError) {
if manifest == nil && errors.Is(err, ErrObjectNotFound) {
if found, fallbackErr := HandleWildcardFallback(w, r); found {
return fallbackErr
} else {
@@ -181,7 +182,7 @@ func getPage(w http.ResponseWriter, r *http.Request) error {
redirectKind = RedirectForce
}
originalURL := (&url.URL{Host: r.Host}).ResolveReference(r.URL)
redirectURL, redirectStatus := ApplyRedirects(manifest, originalURL, redirectKind)
redirectURL, redirectStatus := ApplyRedirectRules(manifest, originalURL, redirectKind)
if Is3xxHTTPStatus(redirectStatus) {
w.Header().Set("Location", redirectURL.String())
w.WriteHeader(int(redirectStatus))
@@ -284,6 +285,7 @@ func getPage(w http.ResponseWriter, r *http.Request) error {
if entry != nil && entry.ContentType != nil {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Content-Type", *entry.ContentType)
// <DEPRECATED issue="35">
mediaType := getMediaType(*entry.ContentType)
if slices.Contains(
[]string{"", "text/html", "application/xhtml+xml", "text/javascript"},
@@ -294,6 +296,27 @@ func getPage(w http.ResponseWriter, r *http.Request) error {
w.Header().Set("Cross-Origin-Embedder-Policy", "credentialless")
w.Header().Set("Cross-Origin-Opener-Policy", "same-origin")
}
// </DEPRECATED>
}
customHeaders, err := ApplyHeaderRules(manifest, &url.URL{Path: entryPath})
if err != nil {
// This is an "internal server error" from an HTTP point of view, but also
// either an issue with the site or a misconfiguration from our point of view.
// Since it's not a problem with the server we don't observe the error.
//
// Note that this behavior is different from a site upload with a malformed
// `_headers` file (where it is semantically ignored); this is because a broken
// upload is something the uploader can notice and fix, but a change in server
// configuration is something they are unaware of and won't be notified of.
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "%s\n", err)
return err
} else {
// If the header has passed all of our stringent, deny-by-default checks, it means
// it's good enough to overwrite whatever was our builtin option (if any).
maps.Copy(w.Header(), customHeaders)
}
// decide on the HTTP status
+12 -12
View File
@@ -11,7 +11,7 @@ import (
"google.golang.org/protobuf/proto"
)
const redirectsFile string = "_redirects"
const redirectsFileName string = "_redirects"
func unparseRule(rule redirects.Rule) string {
var statusPart string
@@ -49,7 +49,7 @@ func Is3xxHTTPStatus(status uint) bool {
return status >= 300 && status <= 399
}
func validateRule(rule redirects.Rule) error {
func validateRedirectRule(rule redirects.Rule) error {
if len(rule.Params) > 0 {
return fmt.Errorf("rules with parameters are not supported")
}
@@ -84,28 +84,28 @@ func validateRule(rule redirects.Rule) error {
}
// Parses redirects file and injects rules into the manifest.
func ProcessRedirects(manifest *Manifest) error {
redirectsEntry := manifest.Contents[redirectsFile]
delete(manifest.Contents, redirectsFile)
func ProcessRedirectsFile(manifest *Manifest) error {
redirectsEntry := manifest.Contents[redirectsFileName]
delete(manifest.Contents, redirectsFileName)
if redirectsEntry == nil {
return nil
} else if redirectsEntry.GetType() != Type_InlineFile {
return AddProblem(manifest, redirectsFile,
return AddProblem(manifest, redirectsFileName,
"not a regular file")
}
rules, err := redirects.ParseString(string(redirectsEntry.GetData()))
if err != nil {
return AddProblem(manifest, redirectsFile,
return AddProblem(manifest, redirectsFileName,
"syntax error: %s", err)
}
for index, rule := range rules {
if err := validateRule(rule); err != nil {
return AddProblem(manifest, redirectsFile,
if err := validateRedirectRule(rule); err != nil {
return AddProblem(manifest, redirectsFileName,
"rule #%d %q: %s", index+1, unparseRule(rule), err)
}
manifest.Redirects = append(manifest.Redirects, &Redirect{
manifest.Redirects = append(manifest.Redirects, &RedirectRule{
From: proto.String(rule.From),
To: proto.String(rule.To),
Status: proto.Uint32(uint32(rule.Status)),
@@ -134,7 +134,7 @@ const (
RedirectForce
)
func ApplyRedirects(
func ApplyRedirectRules(
manifest *Manifest, fromURL *url.URL, kind RedirectKind,
) (
toURL *url.URL, status uint,
@@ -146,7 +146,7 @@ next:
continue
}
// check if the rule matches fromURL
ruleFromURL, _ := url.Parse(*rule.From) // pre-validated in `validateRule`
ruleFromURL, _ := url.Parse(*rule.From) // pre-validated in `validateRedirectRule`
if ruleFromURL.Scheme != "" && fromURL.Scheme != ruleFromURL.Scheme {
continue
}
+171 -45
View File
@@ -220,7 +220,7 @@ func (x *Entry) GetContentType() string {
// See https://docs.netlify.com/manage/routing/redirects/overview/ for details.
// Only a subset of the Netlify specification is representable here.
type Redirect struct {
type RedirectRule struct {
state protoimpl.MessageState `protogen:"open.v1"`
From *string `protobuf:"bytes,1,opt,name=from" json:"from,omitempty"`
To *string `protobuf:"bytes,2,opt,name=to" json:"to,omitempty"`
@@ -230,20 +230,20 @@ type Redirect struct {
sizeCache protoimpl.SizeCache
}
func (x *Redirect) Reset() {
*x = Redirect{}
func (x *RedirectRule) Reset() {
*x = RedirectRule{}
mi := &file_schema_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Redirect) String() string {
func (x *RedirectRule) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Redirect) ProtoMessage() {}
func (*RedirectRule) ProtoMessage() {}
func (x *Redirect) ProtoReflect() protoreflect.Message {
func (x *RedirectRule) ProtoReflect() protoreflect.Message {
mi := &file_schema_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -255,39 +255,144 @@ func (x *Redirect) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use Redirect.ProtoReflect.Descriptor instead.
func (*Redirect) Descriptor() ([]byte, []int) {
// Deprecated: Use RedirectRule.ProtoReflect.Descriptor instead.
func (*RedirectRule) Descriptor() ([]byte, []int) {
return file_schema_proto_rawDescGZIP(), []int{1}
}
func (x *Redirect) GetFrom() string {
func (x *RedirectRule) GetFrom() string {
if x != nil && x.From != nil {
return *x.From
}
return ""
}
func (x *Redirect) GetTo() string {
func (x *RedirectRule) GetTo() string {
if x != nil && x.To != nil {
return *x.To
}
return ""
}
func (x *Redirect) GetStatus() uint32 {
func (x *RedirectRule) GetStatus() uint32 {
if x != nil && x.Status != nil {
return *x.Status
}
return 0
}
func (x *Redirect) GetForce() bool {
func (x *RedirectRule) GetForce() bool {
if x != nil && x.Force != nil {
return *x.Force
}
return false
}
// See https://docs.netlify.com/manage/routing/headers/ for details.
type Header struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name *string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
Values []string `protobuf:"bytes,2,rep,name=values" json:"values,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Header) Reset() {
*x = Header{}
mi := &file_schema_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Header) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Header) ProtoMessage() {}
func (x *Header) ProtoReflect() protoreflect.Message {
mi := &file_schema_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Header.ProtoReflect.Descriptor instead.
func (*Header) Descriptor() ([]byte, []int) {
return file_schema_proto_rawDescGZIP(), []int{2}
}
func (x *Header) GetName() string {
if x != nil && x.Name != nil {
return *x.Name
}
return ""
}
func (x *Header) GetValues() []string {
if x != nil {
return x.Values
}
return nil
}
type HeaderRule struct {
state protoimpl.MessageState `protogen:"open.v1"`
Path *string `protobuf:"bytes,1,opt,name=path" json:"path,omitempty"`
HeaderMap []*Header `protobuf:"bytes,2,rep,name=header_map,json=headerMap" json:"header_map,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *HeaderRule) Reset() {
*x = HeaderRule{}
mi := &file_schema_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *HeaderRule) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HeaderRule) ProtoMessage() {}
func (x *HeaderRule) ProtoReflect() protoreflect.Message {
mi := &file_schema_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use HeaderRule.ProtoReflect.Descriptor instead.
func (*HeaderRule) Descriptor() ([]byte, []int) {
return file_schema_proto_rawDescGZIP(), []int{3}
}
func (x *HeaderRule) GetPath() string {
if x != nil && x.Path != nil {
return *x.Path
}
return ""
}
func (x *HeaderRule) GetHeaderMap() []*Header {
if x != nil {
return x.HeaderMap
}
return nil
}
type Problem struct {
state protoimpl.MessageState `protogen:"open.v1"`
Path *string `protobuf:"bytes,1,opt,name=path" json:"path,omitempty"`
@@ -298,7 +403,7 @@ type Problem struct {
func (x *Problem) Reset() {
*x = Problem{}
mi := &file_schema_proto_msgTypes[2]
mi := &file_schema_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -310,7 +415,7 @@ func (x *Problem) String() string {
func (*Problem) ProtoMessage() {}
func (x *Problem) ProtoReflect() protoreflect.Message {
mi := &file_schema_proto_msgTypes[2]
mi := &file_schema_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -323,7 +428,7 @@ func (x *Problem) ProtoReflect() protoreflect.Message {
// Deprecated: Use Problem.ProtoReflect.Descriptor instead.
func (*Problem) Descriptor() ([]byte, []int) {
return file_schema_proto_rawDescGZIP(), []int{2}
return file_schema_proto_rawDescGZIP(), []int{4}
}
func (x *Problem) GetPath() string {
@@ -350,8 +455,9 @@ type Manifest struct {
Contents map[string]*Entry `protobuf:"bytes,4,rep,name=contents" json:"contents,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
TotalSize *int64 `protobuf:"varint,5,opt,name=total_size,json=totalSize" json:"total_size,omitempty"` // simple sum of each `entry.size`
StoredSize *int64 `protobuf:"varint,8,opt,name=stored_size,json=storedSize" json:"stored_size,omitempty"` // external objects, after deduplication
// Netlify-style `_redirects`
Redirects []*Redirect `protobuf:"bytes,6,rep,name=redirects" json:"redirects,omitempty"`
// Netlify-style `_redirects` and `_headers`
Redirects []*RedirectRule `protobuf:"bytes,6,rep,name=redirects" json:"redirects,omitempty"`
Headers []*HeaderRule `protobuf:"bytes,9,rep,name=headers" json:"headers,omitempty"`
// Diagnostics for non-fatal errors
Problems []*Problem `protobuf:"bytes,7,rep,name=problems" json:"problems,omitempty"`
unknownFields protoimpl.UnknownFields
@@ -360,7 +466,7 @@ type Manifest struct {
func (x *Manifest) Reset() {
*x = Manifest{}
mi := &file_schema_proto_msgTypes[3]
mi := &file_schema_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -372,7 +478,7 @@ func (x *Manifest) String() string {
func (*Manifest) ProtoMessage() {}
func (x *Manifest) ProtoReflect() protoreflect.Message {
mi := &file_schema_proto_msgTypes[3]
mi := &file_schema_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -385,7 +491,7 @@ func (x *Manifest) ProtoReflect() protoreflect.Message {
// Deprecated: Use Manifest.ProtoReflect.Descriptor instead.
func (*Manifest) Descriptor() ([]byte, []int) {
return file_schema_proto_rawDescGZIP(), []int{3}
return file_schema_proto_rawDescGZIP(), []int{5}
}
func (x *Manifest) GetRepoUrl() string {
@@ -430,13 +536,20 @@ func (x *Manifest) GetStoredSize() int64 {
return 0
}
func (x *Manifest) GetRedirects() []*Redirect {
func (x *Manifest) GetRedirects() []*RedirectRule {
if x != nil {
return x.Redirects
}
return nil
}
func (x *Manifest) GetHeaders() []*HeaderRule {
if x != nil {
return x.Headers
}
return nil
}
func (x *Manifest) GetProblems() []*Problem {
if x != nil {
return x.Problems
@@ -455,15 +568,23 @@ const file_schema_proto_rawDesc = "" +
"\x04data\x18\x03 \x01(\fR\x04data\x12(\n" +
"\ttransform\x18\x04 \x01(\x0e2\n" +
".TransformR\ttransform\x12!\n" +
"\fcontent_type\x18\x05 \x01(\tR\vcontentType\"\\\n" +
"\bRedirect\x12\x12\n" +
"\fcontent_type\x18\x05 \x01(\tR\vcontentType\"`\n" +
"\fRedirectRule\x12\x12\n" +
"\x04from\x18\x01 \x01(\tR\x04from\x12\x0e\n" +
"\x02to\x18\x02 \x01(\tR\x02to\x12\x16\n" +
"\x06status\x18\x03 \x01(\rR\x06status\x12\x14\n" +
"\x05force\x18\x04 \x01(\bR\x05force\"3\n" +
"\x05force\x18\x04 \x01(\bR\x05force\"4\n" +
"\x06Header\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x16\n" +
"\x06values\x18\x02 \x03(\tR\x06values\"H\n" +
"\n" +
"HeaderRule\x12\x12\n" +
"\x04path\x18\x01 \x01(\tR\x04path\x12&\n" +
"\n" +
"header_map\x18\x02 \x03(\v2\a.HeaderR\theaderMap\"3\n" +
"\aProblem\x12\x12\n" +
"\x04path\x18\x01 \x01(\tR\x04path\x12\x14\n" +
"\x05cause\x18\x02 \x01(\tR\x05cause\"\xde\x02\n" +
"\x05cause\x18\x02 \x01(\tR\x05cause\"\x89\x03\n" +
"\bManifest\x12\x19\n" +
"\brepo_url\x18\x01 \x01(\tR\arepoUrl\x12\x16\n" +
"\x06branch\x18\x02 \x01(\tR\x06branch\x12\x16\n" +
@@ -472,8 +593,9 @@ const file_schema_proto_rawDesc = "" +
"\n" +
"total_size\x18\x05 \x01(\x03R\ttotalSize\x12\x1f\n" +
"\vstored_size\x18\b \x01(\x03R\n" +
"storedSize\x12'\n" +
"\tredirects\x18\x06 \x03(\v2\t.RedirectR\tredirects\x12$\n" +
"storedSize\x12+\n" +
"\tredirects\x18\x06 \x03(\v2\r.RedirectRuleR\tredirects\x12%\n" +
"\aheaders\x18\t \x03(\v2\v.HeaderRuleR\aheaders\x12$\n" +
"\bproblems\x18\a \x03(\v2\b.ProblemR\bproblems\x1aC\n" +
"\rContentsEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x1c\n" +
@@ -502,28 +624,32 @@ func file_schema_proto_rawDescGZIP() []byte {
}
var file_schema_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_schema_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
var file_schema_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
var file_schema_proto_goTypes = []any{
(Type)(0), // 0: Type
(Transform)(0), // 1: Transform
(*Entry)(nil), // 2: Entry
(*Redirect)(nil), // 3: Redirect
(*Problem)(nil), // 4: Problem
(*Manifest)(nil), // 5: Manifest
nil, // 6: Manifest.ContentsEntry
(Type)(0), // 0: Type
(Transform)(0), // 1: Transform
(*Entry)(nil), // 2: Entry
(*RedirectRule)(nil), // 3: RedirectRule
(*Header)(nil), // 4: Header
(*HeaderRule)(nil), // 5: HeaderRule
(*Problem)(nil), // 6: Problem
(*Manifest)(nil), // 7: Manifest
nil, // 8: Manifest.ContentsEntry
}
var file_schema_proto_depIdxs = []int32{
0, // 0: Entry.type:type_name -> Type
1, // 1: Entry.transform:type_name -> Transform
6, // 2: Manifest.contents:type_name -> Manifest.ContentsEntry
3, // 3: Manifest.redirects:type_name -> Redirect
4, // 4: Manifest.problems:type_name -> Problem
2, // 5: Manifest.ContentsEntry.value:type_name -> Entry
6, // [6:6] is the sub-list for method output_type
6, // [6:6] is the sub-list for method input_type
6, // [6:6] is the sub-list for extension type_name
6, // [6:6] is the sub-list for extension extendee
0, // [0:6] is the sub-list for field type_name
4, // 2: HeaderRule.header_map:type_name -> Header
8, // 3: Manifest.contents:type_name -> Manifest.ContentsEntry
3, // 4: Manifest.redirects:type_name -> RedirectRule
5, // 5: Manifest.headers:type_name -> HeaderRule
6, // 6: Manifest.problems:type_name -> Problem
2, // 7: Manifest.ContentsEntry.value:type_name -> Entry
8, // [8:8] is the sub-list for method output_type
8, // [8:8] is the sub-list for method input_type
8, // [8:8] is the sub-list for extension type_name
8, // [8:8] is the sub-list for extension extendee
0, // [0:8] is the sub-list for field type_name
}
func init() { file_schema_proto_init() }
@@ -537,7 +663,7 @@ func file_schema_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_schema_proto_rawDesc), len(file_schema_proto_rawDesc)),
NumEnums: 2,
NumMessages: 5,
NumMessages: 7,
NumExtensions: 0,
NumServices: 0,
},
+15 -3
View File
@@ -44,13 +44,24 @@ message Entry {
// See https://docs.netlify.com/manage/routing/redirects/overview/ for details.
// Only a subset of the Netlify specification is representable here.
message Redirect {
message RedirectRule {
string from = 1;
string to = 2;
uint32 status = 3;
bool force = 4;
}
// See https://docs.netlify.com/manage/routing/headers/ for details.
message Header {
string name = 1;
repeated string values = 2;
}
message HeaderRule {
string path = 1;
repeated Header header_map = 2;
}
message Problem {
string path = 1;
string cause = 2;
@@ -67,8 +78,9 @@ message Manifest {
int64 total_size = 5; // simple sum of each `entry.size`
int64 stored_size = 8; // external objects, after deduplication
// Netlify-style `_redirects`
repeated Redirect redirects = 6;
// Netlify-style `_redirects` and `_headers`
repeated RedirectRule redirects = 6;
repeated HeaderRule headers = 9;
// Diagnostics for non-fatal errors
repeated Problem problems = 7;