gitea forge bundle: name the parent the forge used, match protection rules as the forge does, bound a commit's size and a search's time (issue 539)
The forge's contents and diffpatch endpoints take a branch, never a commit to
build on, so the parent is read from the commit the forge made, and a branch
that moved under the commit is said. Blobs are read at the commit read first,
not at the branch name. Protection rules match as Gitea 1.27 matches them
([...], {a,b}, escapes, a plain name in any case), in the Go bundle and in
guard.ts alike. One commit carries at most 4 MiB per file or patch and 16 MiB
together; code search stops at its deadline and says so. The HTTP client the
two Go bundles copied is one package, internal/forgehttp.
This commit is contained in:
@@ -1,156 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea/internal/forgehttp"
|
||||
)
|
||||
|
||||
// The forge as this bundle reaches it: its own HTTP interface on the machine, as the forge's admin account,
|
||||
// which the vault delivered into a file this module owns (novox/hq ADR 0086), by basic authentication — the
|
||||
// same way the npm-registry bundle beside it does. The password is read when a call is made and never logged,
|
||||
// answered or put in an error.
|
||||
// The forge as this bundle reaches it: its own HTTP interface on the machine, as the forge's admin account
|
||||
// (internal/forgehttp, shared with the npm-registry bundle beside it).
|
||||
|
||||
// Forge is the forge's HTTP interface.
|
||||
type Forge struct {
|
||||
URL string // e.g. http://127.0.0.1:3000
|
||||
User string
|
||||
PasswordFile string
|
||||
HTTP *http.Client
|
||||
forgehttp.Client
|
||||
}
|
||||
|
||||
// ForgeFromEnv is the forge as the manifest hands it to this bundle.
|
||||
func ForgeFromEnv() (*Forge, error) {
|
||||
f := &Forge{
|
||||
URL: strings.TrimRight(os.Getenv("MESH_GITEA_URL"), "/"),
|
||||
User: os.Getenv("MESH_GITEA_ADMIN_USER"),
|
||||
PasswordFile: os.Getenv("MESH_GITEA_ADMIN_PASSWORD_FILE"),
|
||||
}
|
||||
var missing []string
|
||||
for _, kv := range [][2]string{{"MESH_GITEA_URL", f.URL}, {"MESH_GITEA_ADMIN_USER", f.User},
|
||||
{"MESH_GITEA_ADMIN_PASSWORD_FILE", f.PasswordFile}} {
|
||||
if kv[1] == "" {
|
||||
missing = append(missing, kv[0])
|
||||
}
|
||||
}
|
||||
c, missing := forgehttp.FromEnv()
|
||||
if len(missing) > 0 {
|
||||
return nil, fmt.Errorf("the forge cannot be reached: %s not set by the manifest", strings.Join(missing, ", "))
|
||||
}
|
||||
return f, nil
|
||||
c.Timeout = deadline
|
||||
return &Forge{Client: c}, nil
|
||||
}
|
||||
|
||||
// errNotFound is the forge answering 404.
|
||||
var errNotFound = errors.New("not found")
|
||||
|
||||
// statusError is an answer the forge gave that is not a success, said without the request's credential.
|
||||
type statusError struct {
|
||||
method, path string
|
||||
status int
|
||||
said string
|
||||
}
|
||||
|
||||
func (e *statusError) Error() string {
|
||||
return fmt.Sprintf("the forge answered %d to %s %s: %s", e.status, e.method, e.path, e.said)
|
||||
}
|
||||
|
||||
func (f *Forge) client() *http.Client {
|
||||
if f.HTTP != nil {
|
||||
return f.HTTP
|
||||
}
|
||||
return &http.Client{Timeout: 50 * time.Second}
|
||||
}
|
||||
|
||||
func (f *Forge) password() (string, error) {
|
||||
raw, err := os.ReadFile(f.PasswordFile)
|
||||
if err != nil {
|
||||
// The path is the manifest's, not a secret; the content never appears.
|
||||
return "", fmt.Errorf("the forge's admin password cannot be read from its file: %w", err)
|
||||
}
|
||||
return strings.TrimSpace(string(raw)), nil
|
||||
}
|
||||
var errNotFound = forgehttp.ErrNotFound
|
||||
|
||||
// maxBody is the largest JSON answer read from the forge; maxArchive the largest archive code search reads.
|
||||
const (
|
||||
maxBody = 32 << 20
|
||||
maxBody = forgehttp.MaxBody
|
||||
maxArchive = 64 << 20
|
||||
)
|
||||
|
||||
// do sends one request and answers the body, read up to limit; 404 is errNotFound. A body the limit cut is
|
||||
// refused, never read as if it were whole.
|
||||
func (f *Forge) do(ctx context.Context, method, path string, payload any, limit int64) ([]byte, error) {
|
||||
password, err := f.password()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var body io.Reader
|
||||
if payload != nil {
|
||||
raw, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body = bytes.NewReader(raw)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, f.URL+path, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.SetBasicAuth(f.User, password)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
if payload != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
res, err := f.client().Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the forge did not answer %s %s: %w", method, path, err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
got, err := io.ReadAll(io.LimitReader(res.Body, limit+1))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if int64(len(got)) > limit {
|
||||
return nil, fmt.Errorf("the forge's answer to %s %s is larger than %d MiB and was not read whole", method, path, limit>>20)
|
||||
}
|
||||
switch {
|
||||
case res.StatusCode == http.StatusNotFound:
|
||||
return nil, fmt.Errorf("%s %s: %w", method, path, errNotFound)
|
||||
case res.StatusCode >= 300:
|
||||
said := strings.TrimSpace(string(got))
|
||||
if len(said) > 300 {
|
||||
said = said[:300] + "…"
|
||||
}
|
||||
return nil, &statusError{method, path, res.StatusCode, said}
|
||||
}
|
||||
return got, nil
|
||||
}
|
||||
|
||||
func (f *Forge) getJSON(ctx context.Context, path string, into any) error {
|
||||
return f.sendJSON(ctx, http.MethodGet, path, nil, into)
|
||||
}
|
||||
|
||||
func (f *Forge) sendJSON(ctx context.Context, method, path string, payload, into any) error {
|
||||
body, err := f.do(ctx, method, path, payload, maxBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if into == nil {
|
||||
return nil
|
||||
}
|
||||
if err := json.Unmarshal(body, into); err != nil {
|
||||
return fmt.Errorf("the forge's answer to %s %s is not readable: %w", method, path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Refused is a refusal of policy or of an argument, in the words the caller sees.
|
||||
type Refused struct{ msg string }
|
||||
|
||||
@@ -204,36 +91,6 @@ func (p Protection) name() string {
|
||||
return p.BranchName
|
||||
}
|
||||
|
||||
// ruleCovers is whether a protection rule's name — a branch, or a glob of them (`*`, `**`, `?`) — covers a
|
||||
// branch: the TypeScript bundle's ruleCovers (guard.ts), which gitea_api's file writes are judged by.
|
||||
func ruleCovers(rule, branch string) bool {
|
||||
if rule == branch {
|
||||
return true
|
||||
}
|
||||
if !strings.ContainsAny(rule, "*?[") {
|
||||
return false
|
||||
}
|
||||
var re strings.Builder
|
||||
re.WriteString("^")
|
||||
for i := 0; i < len(rule); i++ {
|
||||
c := rule[i]
|
||||
switch {
|
||||
case c == '*' && i+1 < len(rule) && rule[i+1] == '*':
|
||||
re.WriteString(".*")
|
||||
i++
|
||||
case c == '*':
|
||||
re.WriteString("[^/]*")
|
||||
case c == '?':
|
||||
re.WriteString("[^/]")
|
||||
default:
|
||||
re.WriteString(regexp.QuoteMeta(string(c)))
|
||||
}
|
||||
}
|
||||
re.WriteString("$")
|
||||
ok, err := regexp.MatchString(re.String(), branch)
|
||||
return err == nil && ok
|
||||
}
|
||||
|
||||
// Branch is one branch with the commit it points at.
|
||||
type Branch struct {
|
||||
Name string `json:"name"`
|
||||
@@ -249,7 +106,7 @@ func (f *Forge) branch(ctx context.Context, repo, name string) (*Branch, error)
|
||||
return nil, err
|
||||
}
|
||||
var b Branch
|
||||
if err := f.getJSON(ctx, repo+"/branches/"+seg, &b); err != nil {
|
||||
if err := f.GetJSON(ctx, repo+"/branches/"+seg, &b); err != nil {
|
||||
if errors.Is(err, errNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user