forgejo-aneksajo/routers/web/repo/annex.go
Matthias Riße cc83c8c289
Some checks failed
/ build-oci-image (rootful) (push) Has been cancelled
/ build-oci-image (rootless) (push) Has been cancelled
Integration tests for the release process / release-simulation (push) Has been cancelled
/ release (push) Has been cancelled
testing / backend-checks (push) Has been cancelled
testing / frontend-checks (push) Has been cancelled
testing / test-unit (push) Has been cancelled
testing / test-e2e (push) Has been cancelled
testing / test-remote-cacher (redis) (push) Has been cancelled
testing / test-remote-cacher (valkey) (push) Has been cancelled
testing / test-remote-cacher (garnet) (push) Has been cancelled
testing / test-remote-cacher (redict) (push) Has been cancelled
testing / test-mysql (push) Has been cancelled
testing / test-pgsql (push) Has been cancelled
testing / test-sqlite (push) Has been cancelled
testing / security-check (push) Has been cancelled
fix: set git identity for p2phttp processes (#70)
In some situations it could happen that `git annex p2phttp` needs some
kind of maintenance work resulting in a commit, but without a configured
git identity p2phttp would refuse to run. This could break p2phttp
support.

Setting `GIT_AUTHOR_{NAME,EMAIL}` should remedy this issue.

Fixes #69.

## Checklist

The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org).

### Tests

- I added test coverage for Go changes...
  - [ ] in their respective `*_test.go` for unit tests.
  - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server.
- I added test coverage for JavaScript changes...
  - [ ] in `web_src/js/*.test.js` if it can be unit tested.
  - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)).

### Documentation

- [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
- [x] I did not document these changes and I do not expect someone else to do it.

### Release notes

- [x] I do not want this change to show in the release notes.
- [ ] I want the title to show in the release notes with a link to this pull request.
- [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title.

Reviewed-on: https://codeberg.org/forgejo-aneksajo/forgejo-aneksajo/pulls/70
Co-authored-by: Matthias Riße <m.risse@fz-juelich.de>
Co-committed-by: Matthias Riße <m.risse@fz-juelich.de>
2025-03-24 09:06:59 +01:00

151 lines
5 KiB
Go

package repo
import (
"context"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/exec"
"strings"
"syscall"
"time"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/annex"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
services_context "code.gitea.io/gitea/services/context"
)
type p2phttpRecordType struct {
CancelFunc func()
LastUsed time.Time
Port string
}
var p2phttpRecords = make(map[string]*p2phttpRecordType)
// AnnexP2PHTTP implements git-annex smart HTTP support by delegating to git annex p2phttp
func AnnexP2PHTTP(ctx *services_context.Context) {
uuid := ctx.Params(":uuid")
repoPath, err := annex.UUID2RepoPath(uuid)
if err != nil {
ctx.PlainText(http.StatusNotFound, "Repository not found")
return
}
parts := strings.Split(repoPath, "/")
repoName := strings.TrimSuffix(parts[len(parts)-1], ".git")
owner := parts[len(parts)-2]
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repoName)
if err != nil {
ctx.PlainText(http.StatusNotFound, "Repository not found")
return
}
p, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
return
}
if !(ctx.Req.Method == "GET" && p.CanAccess(perm.AccessModeRead, unit.TypeCode) ||
ctx.Req.Method == "POST" && p.CanAccess(perm.AccessModeWrite, unit.TypeCode) ||
ctx.Req.Method == "POST" && strings.HasSuffix(ctx.Req.URL.Path, "/checkpresent") && p.CanAccess(perm.AccessModeRead, unit.TypeCode) ||
ctx.Req.Method == "POST" && strings.HasSuffix(ctx.Req.URL.Path, "/keeplocked") ||
ctx.Req.Method == "POST" && strings.HasSuffix(ctx.Req.URL.Path, "/lockcontent")) {
// GET requests require at least read access; POST requests for
// anything but checkpresent, lockcontent, and keeplocked
// require write permissions; POST requests for checkpresent
// only require read permissions, as it really is just a read.
// POST requests for lockcontent and keeplocked require no
// authentication at all, as is also the case for the
// authentication in the git-annex-p2phttp server. See
// https://git-annex.branchable.com/bugs/p2phttp__58___drop_difference_wideopen_unauth-readonly/
// for reasoning.
ctx.Resp.WriteHeader(http.StatusUnauthorized)
return
}
p2phttpRecord, p2phttpProcessExists := p2phttpRecords[uuid]
if p2phttpProcessExists {
p2phttpRecord.LastUsed = time.Now()
} else {
// Start a new p2phttp process for the requested repository
// There is a race condition here with the port selection, ideally git annex p2phttp could just listen on a unix socket...
lis, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
log.Error("Failed to listen on a free port: %v", err)
ctx.Resp.WriteHeader(http.StatusInternalServerError)
return
}
hopefullyFreePort := strings.SplitN(lis.Addr().String(), ":", 2)[1]
lis.Close()
p2phttpCtx, p2phttpCtxCancel := context.WithCancel(context.Background())
go func(ctx context.Context) {
cmd := exec.CommandContext(ctx, "git", "-C", repoPath, "annex", "p2phttp", "-J2", "--bind", "127.0.0.1", "--wideopen", "--port", hopefullyFreePort)
cmd.SysProcAttr = &syscall.SysProcAttr{
Pdeathsig: syscall.SIGINT,
}
cmd.Cancel = func() error { return cmd.Process.Signal(os.Interrupt) }
cmd.Env = append(os.Environ(),
"GIT_AUTHOR_NAME="+setting.AppName,
"GIT_AUTHOR_EMAIL="+setting.RunUser+"@"+setting.Domain,
)
_ = cmd.Run()
}(p2phttpCtx)
graceful.GetManager().RunAtTerminate(p2phttpCtxCancel)
// Wait for the p2phttp server to get ready
start := time.Now()
sleepDuration := 1 * time.Millisecond
for {
if time.Since(start) > 5*time.Second {
p2phttpCtxCancel()
log.Error("Failed to start the p2phttp server in a reasonable amount of time")
ctx.Resp.WriteHeader(http.StatusInternalServerError)
return
}
conn, err := net.Dial("tcp", "127.0.0.1:"+hopefullyFreePort)
if err == nil {
conn.Close()
break
}
time.Sleep(sleepDuration)
sleepDuration *= 2
if sleepDuration > 1*time.Second {
sleepDuration = 1 * time.Second
}
}
p2phttpRecord = &p2phttpRecordType{CancelFunc: p2phttpCtxCancel, LastUsed: time.Now(), Port: hopefullyFreePort}
p2phttpRecords[uuid] = p2phttpRecord
}
// Cleanup p2phttp processes that haven't been used for a while
for uuid, record := range p2phttpRecords {
if time.Since(record.LastUsed) > 5*time.Minute {
record.CancelFunc()
delete(p2phttpRecords, uuid)
}
}
url, err := url.Parse("http://127.0.0.1:" + p2phttpRecord.Port + strings.TrimPrefix(ctx.Req.RequestURI, "/git-annex-p2phttp"))
if err != nil {
log.Error("Failed to parse URL: %v", err)
ctx.Resp.WriteHeader(http.StatusInternalServerError)
return
}
proxy := httputil.ReverseProxy{
Rewrite: func(r *httputil.ProxyRequest) {
r.Out.URL = url
},
}
proxy.ServeHTTP(ctx.Resp, ctx.Req)
}