mirror of
https://github.com/Quad4-Software/Reticulum-Go
synced 2026-08-29 23:48:44 -04:00
193 lines
4.2 KiB
Go
193 lines
4.2 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright (c) 2024-2026 Quad4.io
|
|
|
|
package rnsgit
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"quad4/reticulum-go/pkg/destination"
|
|
"quad4/reticulum-go/pkg/identity"
|
|
"quad4/reticulum-go/pkg/node"
|
|
"quad4/reticulum-go/pkg/rnsutil"
|
|
)
|
|
|
|
// Node hosts git repositories over Reticulum.
|
|
type Node struct {
|
|
cfg *ServerConfig
|
|
access *AccessTable
|
|
git *GitRunner
|
|
identity *identity.Identity
|
|
dest *destination.Destination
|
|
n *node.Node
|
|
mirrorStop context.CancelFunc
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// NewNode creates a git repository node.
|
|
func NewNode(cfg *ServerConfig, id *identity.Identity) (*Node, error) {
|
|
if cfg == nil {
|
|
return nil, fmt.Errorf("nil config")
|
|
}
|
|
access, err := NewAccessTable(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Node{
|
|
cfg: cfg,
|
|
access: access,
|
|
git: NewGitRunner(),
|
|
identity: id,
|
|
}, nil
|
|
}
|
|
|
|
// Start runs the repository node.
|
|
func (n *Node) Start(rnsConfig string) error {
|
|
rnsCfg, err := rnsutil.LoadConfigDir(rnsConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nd, err := node.New(rnsCfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := nd.Start(); err != nil {
|
|
return err
|
|
}
|
|
n.n = nd
|
|
dest, err := destination.New(n.identity, destination.In, destination.Single, AppName, nd.Transport(), Aspect)
|
|
if err != nil {
|
|
_ = nd.Stop() // #nosec G104 -- rollback partial start
|
|
return err
|
|
}
|
|
n.dest = dest
|
|
n.registerHandlers()
|
|
if n.cfg.AnnounceInterval >= 0 {
|
|
n.announce()
|
|
if n.cfg.AnnounceInterval > 0 {
|
|
go n.announceLoop(time.Duration(n.cfg.AnnounceInterval) * time.Minute)
|
|
}
|
|
}
|
|
if n.cfg.MirrorIntervalHrs > 0 {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
n.mirrorStop = cancel
|
|
go n.mirrorScheduler(ctx, time.Duration(n.cfg.MirrorIntervalHrs)*time.Hour)
|
|
}
|
|
if n.cfg.ServeNomadNet {
|
|
if err := n.startPageNode(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Stop shuts down the node.
|
|
func (n *Node) Stop() {
|
|
if n.mirrorStop != nil {
|
|
n.mirrorStop()
|
|
}
|
|
if n.n != nil {
|
|
_ = n.n.Stop() // #nosec G104 -- best effort shutdown
|
|
}
|
|
}
|
|
|
|
// ReposDestHash returns the repositories destination hash hex.
|
|
func (n *Node) ReposDestHash() string {
|
|
if n.dest == nil {
|
|
return hex.EncodeToString(destination.Hash(n.identity, AppName, Aspect))
|
|
}
|
|
return hex.EncodeToString(n.dest.GetHash())
|
|
}
|
|
|
|
// IdentityHash returns the node identity hash hex.
|
|
func (n *Node) IdentityHash() string {
|
|
return hex.EncodeToString(n.identity.Hash())
|
|
}
|
|
|
|
// RunService starts the node until interrupted.
|
|
func (n *Node) RunService(rnsConfig string) error {
|
|
if err := n.Start(rnsConfig); err != nil {
|
|
return err
|
|
}
|
|
defer n.Stop()
|
|
ch := make(chan os.Signal, 1)
|
|
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
|
|
<-ch
|
|
return nil
|
|
}
|
|
|
|
func (n *Node) announce() {
|
|
if n.dest != nil {
|
|
_ = n.dest.Announce(false, nil, nil)
|
|
}
|
|
}
|
|
|
|
func (n *Node) announceLoop(interval time.Duration) {
|
|
t := time.NewTicker(interval)
|
|
defer t.Stop()
|
|
for range t.C {
|
|
n.announce()
|
|
}
|
|
}
|
|
|
|
func (n *Node) registerHandlers() {
|
|
allow := byte(destination.AllowAll)
|
|
paths := []struct {
|
|
path string
|
|
fn destination.ResponseGeneratorFunc
|
|
}{
|
|
{PathList, n.handleList},
|
|
{PathFetch, n.handleFetch},
|
|
{PathPush, n.handlePush},
|
|
{PathDelete, n.handleDelete},
|
|
{PathCreate, n.handleCreate},
|
|
{PathFork, n.handleFork},
|
|
{PathSync, n.handleSync},
|
|
{PathMirror, n.handleMirror},
|
|
{PathPerms, n.handlePerms},
|
|
{PathRelease, n.handleRelease},
|
|
{PathWork, n.handleWork},
|
|
}
|
|
for _, h := range paths {
|
|
_ = n.dest.RegisterRequestHandlerAny(h.path, h.fn, allow, nil)
|
|
}
|
|
n.dest.SetLinkEstablishedCallback(func(_ any) {})
|
|
}
|
|
|
|
func (n *Node) remoteAllowed(remote *identity.Identity, group, repo string, perm int) bool {
|
|
var hash []byte
|
|
if remote != nil {
|
|
hash = remote.Hash()
|
|
}
|
|
return n.access.Resolve(group, repo, hash, perm)
|
|
}
|
|
|
|
func (n *Node) repoPath(group, repo string) (string, bool) {
|
|
ga, ok := n.access.Groups[group]
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
ra, ok := ga.Repositories[repo]
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
return ra.Path, true
|
|
}
|
|
|
|
func (n *Node) reloadAccess() error {
|
|
access, err := NewAccessTable(n.cfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n.mu.Lock()
|
|
n.access = access
|
|
n.mu.Unlock()
|
|
return nil
|
|
}
|