mirror of
https://github.com/Quad4-Software/Reticulum-Go
synced 2026-08-29 23:48:44 -04:00
385 lines
12 KiB
Go
385 lines
12 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright (c) 2024-2026 Quad4.io
|
|
|
|
package rnsgit
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"quad4/reticulum-go/pkg/identity"
|
|
"quad4/reticulum-go/pkg/link"
|
|
)
|
|
|
|
func (n *Node) handleList(path string, data []byte, _ []byte, _ []byte, remote *identity.Identity, _ int64) any {
|
|
req, err := DecodeRequest(data)
|
|
if err != nil {
|
|
return StatusResponse(ResInvalidReq, "Invalid request")
|
|
}
|
|
repoPath, ok := RepoFromRequest(req)
|
|
if !ok {
|
|
return StatusResponse(ResInvalidReq, "No repository specified")
|
|
}
|
|
group, repo, ok := ParseRepoPath(repoPath)
|
|
if !ok {
|
|
return StatusResponse(ResInvalidReq, "Invalid request")
|
|
}
|
|
forPush := false
|
|
if v, ok := req["for_push"]; ok {
|
|
if b, ok := v.(bool); ok {
|
|
forPush = b
|
|
}
|
|
}
|
|
perm := permRead
|
|
if forPush {
|
|
perm = permWrite
|
|
}
|
|
if !n.remoteAllowed(remote, group, repo, perm) {
|
|
if n.remoteAllowed(remote, group, repo, permRead) {
|
|
return StatusResponse(ResNotFound, "Not allowed")
|
|
}
|
|
return StatusResponse(ResNotFound, "Not found")
|
|
}
|
|
p, ok := n.repoPath(group, repo)
|
|
if !ok {
|
|
return StatusResponse(ResNotFound, "Not found")
|
|
}
|
|
body, err := n.git.ListRefs(p)
|
|
if err != nil {
|
|
return StatusResponse(ResRemoteFail, "Could not list refs")
|
|
}
|
|
out := append([]byte{ResOK}, []byte(body)...)
|
|
return out
|
|
}
|
|
|
|
func (n *Node) handleFetch(path string, data []byte, _ []byte, _ []byte, remote *identity.Identity, _ int64) any {
|
|
req, err := DecodeRequest(data)
|
|
if err != nil {
|
|
return StatusResponse(ResInvalidReq, "Invalid request")
|
|
}
|
|
repoPath, ok := RepoFromRequest(req)
|
|
if !ok {
|
|
return StatusResponse(ResInvalidReq, "No repository specified")
|
|
}
|
|
group, repo, ok := ParseRepoPath(repoPath)
|
|
if !ok {
|
|
return StatusResponse(ResInvalidReq, "Invalid request")
|
|
}
|
|
if !n.remoteAllowed(remote, group, repo, permRead) {
|
|
return StatusResponse(ResNotFound, "Not found")
|
|
}
|
|
p, ok := n.repoPath(group, repo)
|
|
if !ok {
|
|
return StatusResponse(ResNotFound, "Not found")
|
|
}
|
|
refsAny, ok := req["refs"]
|
|
if !ok {
|
|
return StatusResponse(ResInvalidReq, "No refs specified")
|
|
}
|
|
refsList, ok := refsAny.([]any)
|
|
if !ok {
|
|
return StatusResponse(ResInvalidReq, "Invalid request")
|
|
}
|
|
refs := make([]map[string]string, 0, len(refsList))
|
|
names := make([]string, 0, len(refsList))
|
|
for _, item := range refsList {
|
|
m, ok := item.(map[any]any)
|
|
if !ok {
|
|
m2, ok := item.(map[string]any)
|
|
if !ok {
|
|
return StatusResponse(ResInvalidReq, "Invalid request")
|
|
}
|
|
m = map[any]any{}
|
|
for k, v := range m2 {
|
|
m[k] = v
|
|
}
|
|
}
|
|
ref := fmt.Sprint(m["ref"])
|
|
if SanRef(ref) == "" {
|
|
return StatusResponse(ResInvalidReq, "Invalid request")
|
|
}
|
|
entry := map[string]string{"ref": ref}
|
|
if hv, ok := m["have"]; ok {
|
|
entry["have"] = fmt.Sprint(hv)
|
|
}
|
|
refs = append(refs, entry)
|
|
names = append(names, ref)
|
|
}
|
|
if SanRefs(names) == nil {
|
|
return StatusResponse(ResInvalidReq, "Invalid request")
|
|
}
|
|
var have []string
|
|
if hv, ok := req["have"]; ok {
|
|
if arr, ok := hv.([]any); ok {
|
|
for _, x := range arr {
|
|
if s := SanSHA(fmt.Sprint(x)); s != "" {
|
|
have = append(have, s)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
tmp, err := os.MkdirTemp("", "rnsgit-fetch-")
|
|
if err != nil {
|
|
return StatusResponse(ResRemoteFail, "Remote error")
|
|
}
|
|
defer os.RemoveAll(tmp)
|
|
bundlePath := filepath.Join(tmp, "fetch.bundle")
|
|
if err := n.git.CreateBundle(p, bundlePath, refs, have); err != nil {
|
|
if IsEmptyBundle(err) {
|
|
return []byte{ResOK}
|
|
}
|
|
return StatusResponse(ResRemoteFail, "Could not fetch refs")
|
|
}
|
|
bundle, err := os.ReadFile(bundlePath) // #nosec G304 -- temp bundle
|
|
if err != nil {
|
|
return StatusResponse(ResRemoteFail, "Remote error")
|
|
}
|
|
return link.FileResponse{Data: bundle, MetadataPacked: OKMetadataPacked(), AutoCompress: true}
|
|
}
|
|
|
|
func (n *Node) handlePush(path string, data []byte, _ []byte, _ []byte, remote *identity.Identity, _ int64) any {
|
|
req, err := DecodeRequest(data)
|
|
if err != nil {
|
|
return StatusResponse(ResInvalidReq, "Invalid request")
|
|
}
|
|
repoPath, ok := RepoFromRequest(req)
|
|
if !ok {
|
|
return StatusResponse(ResInvalidReq, "No repository specified")
|
|
}
|
|
group, repo, ok := ParseRepoPath(repoPath)
|
|
if !ok {
|
|
return StatusResponse(ResInvalidReq, "Invalid request")
|
|
}
|
|
if !n.remoteAllowed(remote, group, repo, permWrite) {
|
|
if n.remoteAllowed(remote, group, repo, permRead) {
|
|
return StatusResponse(ResDisallowed, "Not allowed")
|
|
}
|
|
return StatusResponse(ResNotFound, "Not found")
|
|
}
|
|
p, ok := n.repoPath(group, repo)
|
|
if !ok {
|
|
return StatusResponse(ResNotFound, "Not found")
|
|
}
|
|
if bundle, ok := req["bundle"]; ok {
|
|
localRef := SanRef(fmt.Sprint(req["local_ref"]))
|
|
remoteRef := SanRef(fmt.Sprint(req["remote_ref"]))
|
|
force, _ := req["force"].(bool)
|
|
if localRef == "" || remoteRef == "" {
|
|
return StatusResponse(ResInvalidReq, "Missing ref specification")
|
|
}
|
|
var bundleData []byte
|
|
switch b := bundle.(type) {
|
|
case []byte:
|
|
bundleData = b
|
|
case string:
|
|
bundleData = []byte(b)
|
|
default:
|
|
return StatusResponse(ResInvalidReq, "Invalid bundle")
|
|
}
|
|
tmp, err := os.MkdirTemp("", "rnsgit-push-")
|
|
if err != nil {
|
|
return StatusResponse(ResRemoteFail, "Remote error")
|
|
}
|
|
defer os.RemoveAll(tmp)
|
|
bundlePath := filepath.Join(tmp, "push.bundle")
|
|
if err := os.WriteFile(bundlePath, bundleData, 0o600); err != nil {
|
|
return StatusResponse(ResRemoteFail, "Remote error")
|
|
}
|
|
if err := n.git.VerifyBundle(p, bundlePath); err != nil {
|
|
return StatusResponse(ResRemoteFail, "Could not verify bundle")
|
|
}
|
|
if err := n.git.FetchBundle(p, bundlePath, localRef, remoteRef, force); err != nil {
|
|
return StatusResponse(ResRemoteFail, "Could not verify bundle")
|
|
}
|
|
return []byte{ResOK}
|
|
}
|
|
if ops, ok := req["operations"]; ok {
|
|
list, ok := ops.([]any)
|
|
if !ok {
|
|
return StatusResponse(ResInvalidReq, "Invalid data for operations")
|
|
}
|
|
for _, item := range list {
|
|
m, ok := item.(map[any]any)
|
|
if !ok {
|
|
m2, ok := item.(map[string]any)
|
|
if !ok {
|
|
return StatusResponse(ResInvalidReq, "Invalid request")
|
|
}
|
|
m = map[any]any{}
|
|
for k, v := range m2 {
|
|
m[k] = v
|
|
}
|
|
}
|
|
if fmt.Sprint(m["action"]) != "update_ref" {
|
|
return StatusResponse(ResInvalidReq, "Unknown operation")
|
|
}
|
|
ref := SanRef(fmt.Sprint(m["ref"]))
|
|
sha := SanSHA(fmt.Sprint(m["sha"]))
|
|
force, _ := m["force"].(bool)
|
|
if ref == "" || sha == "" || !strings.HasPrefix(ref, "refs/") {
|
|
return StatusResponse(ResInvalidReq, "Invalid request")
|
|
}
|
|
if !n.git.ObjectExists(p, sha) {
|
|
return StatusResponse(ResRemoteFail, "Object does not exist in repository")
|
|
}
|
|
if err := n.git.UpdateRef(p, ref, sha, force); err != nil {
|
|
if strings.Contains(err.Error(), "different sha") {
|
|
return StatusResponse(ResDisallowed, "Ref exists at different SHA (force required)")
|
|
}
|
|
return StatusResponse(ResRemoteFail, "Could not update refs")
|
|
}
|
|
}
|
|
return []byte{ResOK}
|
|
}
|
|
return StatusResponse(ResInvalidReq, "Invalid request data")
|
|
}
|
|
|
|
func (n *Node) handleDelete(path string, data []byte, _ []byte, _ []byte, remote *identity.Identity, _ int64) any {
|
|
req, err := DecodeRequest(data)
|
|
if err != nil {
|
|
return StatusResponse(ResInvalidReq, "Invalid request")
|
|
}
|
|
repoPath, ok := RepoFromRequest(req)
|
|
if !ok {
|
|
return StatusResponse(ResInvalidReq, "No repository specified")
|
|
}
|
|
group, repo, ok := ParseRepoPath(repoPath)
|
|
if !ok {
|
|
return StatusResponse(ResInvalidReq, "Invalid request")
|
|
}
|
|
if !n.remoteAllowed(remote, group, repo, permWrite) {
|
|
if n.remoteAllowed(remote, group, repo, permRead) {
|
|
return StatusResponse(ResDisallowed, "Not allowed")
|
|
}
|
|
return StatusResponse(ResNotFound, "Not found")
|
|
}
|
|
p, ok := n.repoPath(group, repo)
|
|
if !ok {
|
|
return StatusResponse(ResNotFound, "Not found")
|
|
}
|
|
ref := SanRef(fmt.Sprint(req["ref"]))
|
|
if ref == "" || !strings.HasPrefix(ref, "refs/") {
|
|
return StatusResponse(ResInvalidReq, "Invalid request")
|
|
}
|
|
if err := n.git.DeleteRef(p, ref); err != nil {
|
|
return StatusResponse(ResRemoteFail, "Could not delete ref")
|
|
}
|
|
return []byte{ResOK}
|
|
}
|
|
|
|
func (n *Node) handleCreate(path string, data []byte, _ []byte, _ []byte, remote *identity.Identity, _ int64) any {
|
|
req, err := DecodeRequest(data)
|
|
if err != nil {
|
|
return StatusResponse(ResInvalidReq, "Invalid request")
|
|
}
|
|
repoPath, ok := RepoFromRequest(req)
|
|
if !ok {
|
|
return StatusResponse(ResInvalidReq, "No repository specified")
|
|
}
|
|
group, repo, ok := ParseRepoPath(repoPath)
|
|
if !ok {
|
|
return StatusResponse(ResInvalidReq, "Invalid request")
|
|
}
|
|
if !n.access.Resolve(group, "", remote.Hash(), permCreate) {
|
|
if n.access.Resolve(group, "", remote.Hash(), permRead) {
|
|
return StatusResponse(ResDisallowed, "Not allowed")
|
|
}
|
|
return StatusResponse(ResNotFound, "Not found")
|
|
}
|
|
ga, ok := n.access.Groups[group]
|
|
if !ok {
|
|
return StatusResponse(ResNotFound, "Not found")
|
|
}
|
|
repoDir := filepath.Join(ga.Path, repo)
|
|
if _, err := os.Stat(repoDir); err == nil {
|
|
return StatusResponse(ResDisallowed, "Repository already exists")
|
|
}
|
|
if err := n.git.InitBare(repoDir); err != nil {
|
|
return StatusResponse(ResRemoteFail, "Could not create repository")
|
|
}
|
|
allowedPath := filepath.Join(ga.Path, repo+".allowed")
|
|
_ = GrantCreatorAdmin(allowedPath, hex.EncodeToString(remote.Hash()))
|
|
_ = n.reloadAccess()
|
|
return []byte{ResOK}
|
|
}
|
|
|
|
func (n *Node) handleFork(path string, data []byte, _ []byte, _ []byte, remote *identity.Identity, _ int64) any {
|
|
return n.remoteClone(path, data, remote, "fork")
|
|
}
|
|
|
|
func (n *Node) handleMirror(path string, data []byte, _ []byte, _ []byte, remote *identity.Identity, _ int64) any {
|
|
return n.remoteClone(path, data, remote, "mirror")
|
|
}
|
|
|
|
func (n *Node) remoteClone(_ string, data []byte, remote *identity.Identity, repoType string) any {
|
|
req, err := DecodeRequest(data)
|
|
if err != nil {
|
|
return StatusResponse(ResInvalidReq, "Invalid request")
|
|
}
|
|
repoPath, ok := RepoFromRequest(req)
|
|
if !ok {
|
|
return StatusResponse(ResInvalidReq, "No repository specified")
|
|
}
|
|
group, repo, ok := ParseRepoPath(repoPath)
|
|
if !ok {
|
|
return StatusResponse(ResInvalidReq, "Invalid request")
|
|
}
|
|
if !n.access.Resolve(group, "", remote.Hash(), permCreate) {
|
|
return StatusResponse(ResNotFound, "Not found")
|
|
}
|
|
source := fmt.Sprint(req["source"])
|
|
if source == "" {
|
|
return StatusResponse(ResInvalidReq, "Invalid request")
|
|
}
|
|
ga, ok := n.access.Groups[group]
|
|
if !ok {
|
|
return StatusResponse(ResNotFound, "Not found")
|
|
}
|
|
repoDir := filepath.Join(ga.Path, repo)
|
|
if err := n.git.CloneBare(source, repoDir); err != nil {
|
|
return StatusResponse(ResRemoteFail, "Could not clone")
|
|
}
|
|
_ = n.git.SetConfig(repoDir, "repository.rngit.type", repoType)
|
|
_ = n.git.SetConfig(repoDir, "repository.rngit.upstream.source", source)
|
|
if repoType == "mirror" {
|
|
_ = n.git.SetConfig(repoDir, "repository.rngit.upstream.sync", fmt.Sprintf("%d", time.Now().Unix()))
|
|
}
|
|
_ = GrantCreatorAdmin(filepath.Join(ga.Path, repo+".allowed"), hex.EncodeToString(remote.Hash()))
|
|
_ = n.reloadAccess()
|
|
return []byte{ResOK}
|
|
}
|
|
|
|
func (n *Node) handleSync(path string, data []byte, _ []byte, _ []byte, remote *identity.Identity, _ int64) any {
|
|
req, err := DecodeRequest(data)
|
|
if err != nil {
|
|
return StatusResponse(ResInvalidReq, "Invalid request")
|
|
}
|
|
repoPath, ok := RepoFromRequest(req)
|
|
if !ok {
|
|
return StatusResponse(ResInvalidReq, "No repository specified")
|
|
}
|
|
group, repo, ok := ParseRepoPath(repoPath)
|
|
if !ok {
|
|
return StatusResponse(ResInvalidReq, "Invalid request")
|
|
}
|
|
if !n.remoteAllowed(remote, group, repo, permWrite) {
|
|
return StatusResponse(ResNotFound, "Not found")
|
|
}
|
|
p, ok := n.repoPath(group, repo)
|
|
if !ok {
|
|
return StatusResponse(ResNotFound, "Not found")
|
|
}
|
|
source, err := n.git.ConfigValue(p, "repository.rngit.upstream.source")
|
|
if err != nil || source == "" {
|
|
return StatusResponse(ResInvalidReq, "No upstream configured")
|
|
}
|
|
if err := n.git.FetchAll(p, source); err != nil {
|
|
return StatusResponse(ResRemoteFail, "Sync failed")
|
|
}
|
|
_ = n.git.SetConfig(p, "repository.rngit.upstream.sync", fmt.Sprintf("%d", time.Now().Unix()))
|
|
return []byte{ResOK}
|
|
}
|