mirror of
https://codeberg.org/git-pages/git-pages.git
synced 2026-07-20 00:12:19 +00:00
Add Netlify _headers support.
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"-s -w"
|
||||
];
|
||||
|
||||
vendorHash = "sha256-qxqIz8hubOBpvJ60h44kY/GbFcZg15WA1JxyskNKtKk=";
|
||||
vendorHash = "sha256-UQl8AeijqJd2qpVZBDuHT/+Dtd3+Uwrf4w4yAOaFs98=";
|
||||
};
|
||||
in
|
||||
{
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user