forked from tangled.org/core
this repo has no description

knotserver: remove all mentions of knotserver secret

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li 0808c86a 08fa350b

verified
Changed files
+427 -1472
knotclient
knotserver
rbac
-37
knotclient/signer.go
···
"encoding/hex"
"encoding/json"
"fmt"
-
"io"
-
"log"
"net/http"
"net/url"
"time"
···
}
return s.client.Do(req)
-
}
-
-
func (s *SignedClient) RepoLanguages(ownerDid, repoName, ref string) (*types.RepoLanguageResponse, error) {
-
const (
-
Method = "GET"
-
)
-
endpoint := fmt.Sprintf("/%s/%s/languages/%s", ownerDid, repoName, url.PathEscape(ref))
-
-
req, err := s.newRequest(Method, endpoint, nil)
-
if err != nil {
-
return nil, err
-
}
-
-
resp, err := s.client.Do(req)
-
if err != nil {
-
return nil, err
-
}
-
-
var result types.RepoLanguageResponse
-
if resp.StatusCode != http.StatusOK {
-
log.Println("failed to calculate languages", resp.Status)
-
return &types.RepoLanguageResponse{}, nil
-
}
-
-
body, err := io.ReadAll(resp.Body)
-
if err != nil {
-
return nil, err
-
}
-
-
err = json.Unmarshal(body, &result)
-
if err != nil {
-
return nil, err
-
}
-
-
return &result, nil
}
func (s *SignedClient) RepoForkAheadBehind(ownerDid, source, name, branch, hiddenRef string) (*http.Response, error) {
+35
knotclient/unsigned.go
···
return &formatPatchResponse, nil
}
+
+
func (s *UnsignedClient) RepoLanguages(ownerDid, repoName, ref string) (*types.RepoLanguageResponse, error) {
+
const (
+
Method = "GET"
+
)
+
endpoint := fmt.Sprintf("/%s/%s/languages/%s", ownerDid, repoName, url.PathEscape(ref))
+
+
req, err := s.newRequest(Method, endpoint, nil, nil)
+
if err != nil {
+
return nil, err
+
}
+
+
resp, err := s.client.Do(req)
+
if err != nil {
+
return nil, err
+
}
+
+
var result types.RepoLanguageResponse
+
if resp.StatusCode != http.StatusOK {
+
log.Println("failed to calculate languages", resp.Status)
+
return &types.RepoLanguageResponse{}, nil
+
}
+
+
body, err := io.ReadAll(resp.Body)
+
if err != nil {
+
return nil, err
+
}
+
+
err = json.Unmarshal(body, &result)
+
if err != nil {
+
return nil, err
+
}
+
+
return &result, nil
+
}
+236 -177
knotserver/handler.go
···
package knotserver
import (
+
"compress/gzip"
"context"
+
"crypto/sha256"
+
"encoding/json"
+
"errors"
"fmt"
-
"log/slog"
+
"log"
"net/http"
-
"runtime/debug"
+
"net/url"
+
"path/filepath"
+
"strconv"
+
"strings"
+
"sync"
+
"time"
+
securejoin "github.com/cyphar/filepath-securejoin"
+
"github.com/gliderlabs/ssh"
"github.com/go-chi/chi/v5"
-
"tangled.sh/tangled.sh/core/idresolver"
-
"tangled.sh/tangled.sh/core/jetstream"
-
"tangled.sh/tangled.sh/core/knotserver/config"
+
"github.com/go-git/go-git/v5/plumbing"
+
"github.com/go-git/go-git/v5/plumbing/object"
"tangled.sh/tangled.sh/core/knotserver/db"
-
"tangled.sh/tangled.sh/core/knotserver/xrpc"
-
tlog "tangled.sh/tangled.sh/core/log"
-
"tangled.sh/tangled.sh/core/notifier"
-
"tangled.sh/tangled.sh/core/rbac"
+
"tangled.sh/tangled.sh/core/knotserver/git"
"tangled.sh/tangled.sh/core/types"
)
-
type Handle struct {
-
c *config.Config
-
db *db.DB
-
jc *jetstream.JetstreamClient
-
e *rbac.Enforcer
-
l *slog.Logger
-
n *notifier.Notifier
-
resolver *idresolver.Resolver
+
func (h *Handle) Index(w http.ResponseWriter, r *http.Request) {
+
w.Write([]byte("This is a knot server. More info at https://tangled.sh"))
}
-
func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, jc *jetstream.JetstreamClient, l *slog.Logger, n *notifier.Notifier) (http.Handler, error) {
-
r := chi.NewRouter()
+
func (h *Handle) Capabilities(w http.ResponseWriter, r *http.Request) {
+
w.Header().Set("Content-Type", "application/json")
-
h := Handle{
-
c: c,
-
db: db,
-
e: e,
-
l: l,
-
jc: jc,
-
n: n,
-
resolver: idresolver.DefaultResolver(),
+
capabilities := map[string]any{
+
"pull_requests": map[string]any{
+
"format_patch": true,
+
"patch_submissions": true,
+
"branch_submissions": true,
+
"fork_submissions": true,
+
},
+
"xrpc": true,
}
-
err := e.AddKnot(rbac.ThisServer)
+
jsonData, err := json.Marshal(capabilities)
if err != nil {
-
return nil, fmt.Errorf("failed to setup enforcer: %w", err)
+
http.Error(w, "Failed to serialize JSON", http.StatusInternalServerError)
+
return
}
-
// configure owner
-
if err = h.configureOwner(); err != nil {
-
return nil, err
-
}
-
h.l.Info("owner set", "did", h.c.Server.Owner)
-
h.jc.AddDid(h.c.Server.Owner)
+
w.Write(jsonData)
+
}
+
+
func (h *Handle) RepoIndex(w http.ResponseWriter, r *http.Request) {
+
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
+
l := h.l.With("path", path, "handler", "RepoIndex")
+
ref := chi.URLParam(r, "ref")
+
ref, _ = url.PathUnescape(ref)
-
// configure known-dids in jetstream consumer
-
dids, err := h.db.GetAllDids()
+
gr, err := git.Open(path, ref)
if err != nil {
-
return nil, fmt.Errorf("failed to get all dids: %w", err)
-
}
-
for _, d := range dids {
-
jc.AddDid(d)
+
plain, err2 := git.PlainOpen(path)
+
if err2 != nil {
+
l.Error("opening repo", "error", err2.Error())
+
notFound(w)
+
return
+
}
+
branches, _ := plain.Branches()
+
+
log.Println(err)
+
+
if errors.Is(err, plumbing.ErrReferenceNotFound) {
+
resp := types.RepoIndexResponse{
+
IsEmpty: true,
+
Branches: branches,
+
}
+
writeJSON(w, resp)
+
return
+
} else {
+
l.Error("opening repo", "error", err.Error())
+
notFound(w)
+
return
+
}
}
-
err = h.jc.StartJetstream(ctx, h.processMessages)
-
if err != nil {
-
return nil, fmt.Errorf("failed to start jetstream: %w", err)
-
}
+
var (
+
commits []*object.Commit
+
total int
+
branches []types.Branch
+
files []types.NiceTree
+
tags []object.Tag
+
)
-
r.Get("/", h.Index)
-
r.Get("/capabilities", h.Capabilities)
-
r.Get("/version", h.Version)
-
r.Get("/owner", func(w http.ResponseWriter, r *http.Request) {
-
w.Write([]byte(h.c.Server.Owner))
-
})
-
r.Route("/{did}", func(r chi.Router) {
-
// Repo routes
-
r.Route("/{name}", func(r chi.Router) {
-
r.Route("/collaborator", func(r chi.Router) {
-
r.Use(h.VerifySignature)
-
r.Post("/add", h.AddRepoCollaborator)
-
})
+
var wg sync.WaitGroup
+
errorsCh := make(chan error, 5)
-
r.Route("/languages", func(r chi.Router) {
-
r.With(h.VerifySignature)
-
r.Get("/", h.RepoLanguages)
-
r.Get("/{ref}", h.RepoLanguages)
-
})
+
wg.Add(1)
+
go func() {
+
defer wg.Done()
+
cs, err := gr.Commits(0, 60)
+
if err != nil {
+
errorsCh <- fmt.Errorf("commits: %w", err)
+
return
+
}
+
commits = cs
+
}()
-
r.Get("/", h.RepoIndex)
-
r.Get("/info/refs", h.InfoRefs)
-
r.Post("/git-upload-pack", h.UploadPack)
-
r.Post("/git-receive-pack", h.ReceivePack)
-
r.Get("/compare/{rev1}/{rev2}", h.Compare) // git diff-tree compare of two objects
+
wg.Add(1)
+
go func() {
+
defer wg.Done()
+
t, err := gr.TotalCommits()
+
if err != nil {
+
errorsCh <- fmt.Errorf("calculating total: %w", err)
+
return
+
}
+
total = t
+
}()
-
r.With(h.VerifySignature).Post("/hidden-ref/{forkRef}/{remoteRef}", h.NewHiddenRef)
+
wg.Add(1)
+
go func() {
+
defer wg.Done()
+
bs, err := gr.Branches()
+
if err != nil {
+
errorsCh <- fmt.Errorf("fetching branches: %w", err)
+
return
+
}
+
branches = bs
+
}()
-
r.Route("/merge", func(r chi.Router) {
-
r.With(h.VerifySignature)
-
r.Post("/", h.Merge)
-
r.Post("/check", h.MergeCheck)
-
})
+
wg.Add(1)
+
go func() {
+
defer wg.Done()
+
ts, err := gr.Tags()
+
if err != nil {
+
errorsCh <- fmt.Errorf("fetching tags: %w", err)
+
return
+
}
+
tags = ts
+
}()
-
r.Route("/tree/{ref}", func(r chi.Router) {
-
r.Get("/", h.RepoIndex)
-
r.Get("/*", h.RepoTree)
-
})
+
wg.Add(1)
+
go func() {
+
defer wg.Done()
+
fs, err := gr.FileTree(r.Context(), "")
+
if err != nil {
+
errorsCh <- fmt.Errorf("fetching filetree: %w", err)
+
return
+
}
+
files = fs
+
}()
-
r.Route("/blob/{ref}", func(r chi.Router) {
-
r.Get("/*", h.Blob)
-
})
+
wg.Wait()
+
close(errorsCh)
-
r.Route("/raw/{ref}", func(r chi.Router) {
-
r.Get("/*", h.BlobRaw)
-
})
+
// show any errors
+
for err := range errorsCh {
+
l.Error("loading repo", "error", err.Error())
+
writeError(w, err.Error(), http.StatusInternalServerError)
+
return
+
}
-
r.Get("/log/{ref}", h.Log)
-
r.Get("/archive/{file}", h.Archive)
-
r.Get("/commit/{ref}", h.Diff)
-
r.Get("/tags", h.Tags)
-
r.Route("/branches", func(r chi.Router) {
-
r.Get("/", h.Branches)
-
r.Get("/{branch}", h.Branch)
-
r.Route("/default", func(r chi.Router) {
-
r.Get("/", h.DefaultBranch)
-
r.With(h.VerifySignature).Put("/", h.SetDefaultBranch)
-
})
-
})
-
})
-
})
+
rtags := []*types.TagReference{}
+
for _, tag := range tags {
+
var target *object.Tag
+
if tag.Target != plumbing.ZeroHash {
+
target = &tag
+
}
+
tr := types.TagReference{
+
Tag: target,
+
}
-
// xrpc apis
-
r.Mount("/xrpc", h.XrpcRouter())
+
tr.Reference = types.Reference{
+
Name: tag.Name,
+
Hash: tag.Hash.String(),
+
}
-
// Create a new repository.
-
r.Route("/repo", func(r chi.Router) {
-
r.Use(h.VerifySignature)
-
r.Delete("/", h.RemoveRepo)
-
r.Route("/fork", func(r chi.Router) {
-
r.Post("/", h.RepoFork)
-
r.Post("/sync/*", h.RepoForkSync)
-
r.Get("/sync/*", h.RepoForkAheadBehind)
-
})
-
})
+
if tag.Message != "" {
+
tr.Message = tag.Message
+
}
-
r.Route("/member", func(r chi.Router) {
-
r.Use(h.VerifySignature)
-
r.Put("/add", h.AddMember)
-
})
+
rtags = append(rtags, &tr)
+
}
-
// Socket that streams git oplogs
-
r.Get("/events", h.Events)
+
var readmeContent string
+
var readmeFile string
+
for _, readme := range h.c.Repo.Readme {
+
content, _ := gr.FileContent(readme)
+
if len(content) > 0 {
+
readmeContent = string(content)
+
readmeFile = readme
+
}
+
}
-
// Health check. Used for two-way verification with appview.
-
r.With(h.VerifySignature).Get("/health", h.Health)
+
if ref == "" {
+
mainBranch, err := gr.FindMainBranch()
+
if err != nil {
+
writeError(w, err.Error(), http.StatusInternalServerError)
+
l.Error("finding main branch", "error", err.Error())
+
return
+
}
+
ref = mainBranch
+
}
-
// All public keys on the knot.
-
r.Get("/keys", h.Keys)
+
resp := types.RepoIndexResponse{
+
IsEmpty: false,
+
Ref: ref,
+
Commits: commits,
+
Description: getDescription(path),
+
Readme: readmeContent,
+
ReadmeFileName: readmeFile,
+
Files: files,
+
Branches: branches,
+
Tags: rtags,
+
TotalCommits: total,
+
}
-
return r, nil
+
writeJSON(w, resp)
}
-
func (h *Handle) XrpcRouter() http.Handler {
-
logger := tlog.New("knots")
+
func (h *Handle) RepoTree(w http.ResponseWriter, r *http.Request) {
+
treePath := chi.URLParam(r, "*")
+
ref := chi.URLParam(r, "ref")
+
ref, _ = url.PathUnescape(ref)
-
serviceAuth := serviceauth.NewServiceAuth(h.l, h.resolver, h.c.Server.Did().String())
+
l := h.l.With("handler", "RepoTree", "ref", ref, "treePath", treePath)
-
xrpc := &xrpc.Xrpc{
-
Config: h.c,
-
Db: h.db,
-
Ingester: h.jc,
-
Enforcer: h.e,
-
Logger: logger,
-
Notifier: h.n,
-
Resolver: h.resolver,
-
ServiceAuth: serviceAuth,
+
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
+
gr, err := git.Open(path, ref)
+
if err != nil {
+
notFound(w)
+
return
}
-
return xrpc.Router()
-
}
-
// version is set during build time.
-
var version string
-
-
func (h *Handle) Version(w http.ResponseWriter, r *http.Request) {
-
if version == "" {
-
info, ok := debug.ReadBuildInfo()
-
if !ok {
-
http.Error(w, "failed to read build info", http.StatusInternalServerError)
-
return
-
}
-
-
var modVer string
-
for _, mod := range info.Deps {
-
if mod.Path == "tangled.sh/tangled.sh/knotserver" {
-
version = mod.Version
-
break
-
}
-
}
+
files, err := gr.FileTree(r.Context(), treePath)
+
if err != nil {
+
writeError(w, err.Error(), http.StatusInternalServerError)
+
l.Error("file tree", "error", err.Error())
+
return
+
}
-
if modVer == "" {
-
version = "unknown"
-
}
+
resp := types.RepoTreeResponse{
+
Ref: ref,
+
Parent: treePath,
+
Description: getDescription(path),
+
DotDot: filepath.Dir(treePath),
+
Files: files,
}
-
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
-
fmt.Fprintf(w, "knotserver/%s", version)
+
writeJSON(w, resp)
}
-
func (h *Handle) configureOwner() error {
-
cfgOwner := h.c.Server.Owner
+
func (h *Handle) BlobRaw(w http.ResponseWriter, r *http.Request) {
+
treePath := chi.URLParam(r, "*")
+
ref := chi.URLParam(r, "ref")
+
ref, _ = url.PathUnescape(ref)
-
rbacDomain := "thisserver"
+
l := h.l.With("handler", "BlobRaw", "ref", ref, "treePath", treePath)
-
existing, err := h.e.GetKnotUsersByRole("server:owner", rbacDomain)
+
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
+
gr, err := git.Open(path, ref)
if err != nil {
-
return err
+
notFound(w)
+
return
}
-
switch len(existing) {
-
case 0:
-
// no owner configured, continue
-
case 1:
-
// find existing owner
-
existingOwner := existing[0]
+
contents, err := gr.RawContent(treePath)
+
if err != nil {
+
writeError(w, err.Error(), http.StatusBadRequest)
+
l.Error("file content", "error", err.Error())
+
return
+
}
+
+
mimeType := http.DetectContentType(contents)
+
+
// exception for svg
+
if filepath.Ext(treePath) == ".svg" {
+
mimeType = "image/svg+xml"
+
}
+
+
contentHash := sha256.Sum256(contents)
+
eTag := fmt.Sprintf("\"%x\"", contentHash)
-
// no ownership change, this is okay
-
if existingOwner == h.c.Server.Owner {
-
break
+
// allow image, video, and text/plain files to be served directly
+
switch {
+
case strings.HasPrefix(mimeType, "image/"), strings.HasPrefix(mimeType, "video/"):
+
if clientETag := r.Header.Get("If-None-Match"); clientETag == eTag {
+
w.WriteHeader(http.StatusNotModified)
+
return
}
+
w.Header().Set("ETag", eTag)
-
// remove existing owner
-
err = h.e.RemoveKnotOwner(rbacDomain, existingOwner)
-
if err != nil {
-
return nil
-
}
+
case strings.HasPrefix(mimeType, "text/plain"):
+
w.Header().Set("Cache-Control", "public, no-cache")
+
default:
l.Error("attempted to serve disallowed file type", "mimetype", mimeType)
writeError(w, "only image, video, and text files can be accessed directly", http.StatusForbidden)
+10 -29
knotserver/ingester.go
···
"net/http"
"net/url"
"path/filepath"
-
"slices"
"strings"
comatproto "github.com/bluesky-social/indigo/api/atproto"
···
l = l.With("target_branch", record.TargetBranch)
if record.Source == nil {
-
reason := "not a branch-based pull request"
-
l.Info("ignoring pull record", "reason", reason)
-
return fmt.Errorf("ignoring pull record: %s", reason)
+
return fmt.Errorf("ignoring pull record: not a branch-based pull request")
}
if record.Source.Repo != nil {
-
reason := "fork based pull"
-
l.Info("ignoring pull record", "reason", reason)
-
return fmt.Errorf("ignoring pull record: %s", reason)
-
}
-
-
allDids, err := h.db.GetAllDids()
-
if err != nil {
-
return err
-
}
-
-
// presently: we only process PRs from collaborators for pipelines
-
if !slices.Contains(allDids, did) {
-
reason := "not a known did"
-
l.Info("rejecting pull record", "reason", reason)
-
return fmt.Errorf("rejected pull record: %s, %s", reason, did)
+
return fmt.Errorf("ignoring pull record: fork based pull")
}
repoAt, err := syntax.ParseATURI(record.TargetRepo)
if err != nil {
-
return err
+
return fmt.Errorf("failed to parse ATURI: %w", err)
}
// resolve this aturi to extract the repo record
···
resp, err := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String())
if err != nil {
-
return err
+
return fmt.Errorf("failed to resolver repo: %w", err)
}
repo := resp.Value.Val.(*tangled.Repo)
if repo.Knot != h.c.Server.Hostname {
-
reason := "not this knot"
-
l.Info("rejecting pull record", "reason", reason)
-
return fmt.Errorf("rejected pull record: %s", reason)
+
return fmt.Errorf("rejected pull record: not this knot, %s != %s", repo.Knot, h.c.Server.Hostname)
}
didSlashRepo, err := securejoin.SecureJoin(repo.Owner, repo.Name)
if err != nil {
-
return err
+
return fmt.Errorf("failed to construct relative repo path: %w", err)
}
repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, didSlashRepo)
if err != nil {
-
return err
+
return fmt.Errorf("failed to construct absolute repo path: %w", err)
}
gr, err := git.Open(repoPath, record.Source.Branch)
if err != nil {
-
return err
+
return fmt.Errorf("failed to open git repository: %w", err)
}
workflowDir, err := gr.FileTree(ctx, workflow.WorkflowDir)
if err != nil {
-
return err
+
return fmt.Errorf("failed to open workflow directory: %w", err)
}
var pipeline workflow.RawPipeline
···
cp := compiler.Compile(compiler.Parse(pipeline))
eventJson, err := json.Marshal(cp)
if err != nil {
-
return err
+
return fmt.Errorf("failed to marshal pipeline event: %w", err)
}
// do not run empty pipelines
-53
knotserver/middleware.go
···
-
package knotserver
-
-
import (
-
"crypto/hmac"
-
"crypto/sha256"
-
"encoding/hex"
-
"net/http"
-
"time"
-
)
-
-
func (h *Handle) VerifySignature(next http.Handler) http.Handler {
-
if h.c.Server.Dev {
-
return next
-
}
-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-
signature := r.Header.Get("X-Signature")
-
if signature == "" || !h.verifyHMAC(signature, r) {
-
writeError(w, "signature verification failed", http.StatusForbidden)
-
return
-
}
-
next.ServeHTTP(w, r)
-
})
-
}
-
-
func (h *Handle) verifyHMAC(signature string, r *http.Request) bool {
-
secret := h.c.Server.Secret
-
timestamp := r.Header.Get("X-Timestamp")
-
if timestamp == "" {
-
return false
-
}
-
-
// Verify that the timestamp is not older than a minute
-
reqTime, err := time.Parse(time.RFC3339, timestamp)
-
if err != nil {
-
return false
-
}
-
if time.Since(reqTime) > time.Minute {
-
return false
-
}
-
-
message := r.Method + r.URL.Path + timestamp
-
-
mac := hmac.New(sha256.New, []byte(secret))
-
mac.Write([]byte(message))
-
expectedMAC := mac.Sum(nil)
-
-
signatureBytes, err := hex.DecodeString(signature)
-
if err != nil {
-
return false
-
}
-
-
return hmac.Equal(signatureBytes, expectedMAC)
-
}
+138 -1176
knotserver/routes.go
···
package knotserver
import (
-
"compress/gzip"
"context"
-
"crypto/sha256"
-
"encoding/json"
-
"errors"
"fmt"
-
"log"
+
"log/slog"
"net/http"
-
"net/url"
-
"os"
-
"path/filepath"
-
"strconv"
-
"strings"
-
"sync"
-
"time"
+
"runtime/debug"
-
securejoin "github.com/cyphar/filepath-securejoin"
-
"github.com/gliderlabs/ssh"
"github.com/go-chi/chi/v5"
-
"github.com/go-git/go-git/v5/plumbing"
-
"github.com/go-git/go-git/v5/plumbing/object"
-
"tangled.sh/tangled.sh/core/hook"
+
"tangled.sh/tangled.sh/core/idresolver"
+
"tangled.sh/tangled.sh/core/jetstream"
+
"tangled.sh/tangled.sh/core/knotserver/config"
"tangled.sh/tangled.sh/core/knotserver/db"
-
"tangled.sh/tangled.sh/core/knotserver/git"
-
"tangled.sh/tangled.sh/core/patchutil"
+
"tangled.sh/tangled.sh/core/knotserver/xrpc"
+
tlog "tangled.sh/tangled.sh/core/log"
+
"tangled.sh/tangled.sh/core/notifier"
"tangled.sh/tangled.sh/core/rbac"
-
"tangled.sh/tangled.sh/core/types"
+
"tangled.sh/tangled.sh/core/xrpc/serviceauth"
)
-
func (h *Handle) Index(w http.ResponseWriter, r *http.Request) {
-
w.Write([]byte("This is a knot server. More info at https://tangled.sh"))
+
type Handle struct {
+
c *config.Config
+
db *db.DB
+
jc *jetstream.JetstreamClient
+
e *rbac.Enforcer
+
l *slog.Logger
+
n *notifier.Notifier
+
resolver *idresolver.Resolver
}
-
func (h *Handle) Capabilities(w http.ResponseWriter, r *http.Request) {
-
w.Header().Set("Content-Type", "application/json")
+
func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, jc *jetstream.JetstreamClient, l *slog.Logger, n *notifier.Notifier) (http.Handler, error) {
+
r := chi.NewRouter()
-
capabilities := map[string]any{
-
"pull_requests": map[string]any{
-
"format_patch": true,
-
"patch_submissions": true,
-
"branch_submissions": true,
-
"fork_submissions": true,
-
},
+
h := Handle{
+
c: c,
+
db: db,
+
e: e,
+
l: l,
+
jc: jc,
+
n: n,
+
resolver: idresolver.DefaultResolver(),
}
-
jsonData, err := json.Marshal(capabilities)
+
err := e.AddKnot(rbac.ThisServer)
if err != nil {
-
http.Error(w, "Failed to serialize JSON", http.StatusInternalServerError)
-
return
+
return nil, fmt.Errorf("failed to setup enforcer: %w", err)
}
-
w.Write(jsonData)
-
}
-
-
func (h *Handle) RepoIndex(w http.ResponseWriter, r *http.Request) {
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
-
l := h.l.With("path", path, "handler", "RepoIndex")
-
ref := chi.URLParam(r, "ref")
-
ref, _ = url.PathUnescape(ref)
-
-
gr, err := git.Open(path, ref)
-
if err != nil {
-
plain, err2 := git.PlainOpen(path)
-
if err2 != nil {
-
l.Error("opening repo", "error", err2.Error())
-
notFound(w)
-
return
-
}
-
branches, _ := plain.Branches()
-
-
log.Println(err)
-
-
if errors.Is(err, plumbing.ErrReferenceNotFound) {
-
resp := types.RepoIndexResponse{
-
IsEmpty: true,
-
Branches: branches,
-
}
-
writeJSON(w, resp)
-
return
-
} else {
-
l.Error("opening repo", "error", err.Error())
-
notFound(w)
-
return
-
}
+
// configure owner
+
if err = h.configureOwner(); err != nil {
+
return nil, err
}
-
-
var (
-
commits []*object.Commit
-
total int
-
branches []types.Branch
-
files []types.NiceTree
-
tags []object.Tag
-
)
-
-
var wg sync.WaitGroup
-
errorsCh := make(chan error, 5)
-
-
wg.Add(1)
-
go func() {
-
defer wg.Done()
-
cs, err := gr.Commits(0, 60)
-
if err != nil {
-
errorsCh <- fmt.Errorf("commits: %w", err)
-
return
-
}
-
commits = cs
-
}()
-
-
wg.Add(1)
-
go func() {
-
defer wg.Done()
-
t, err := gr.TotalCommits()
-
if err != nil {
-
errorsCh <- fmt.Errorf("calculating total: %w", err)
-
return
-
}
-
total = t
-
}()
+
h.l.Info("owner set", "did", h.c.Server.Owner)
+
h.jc.AddDid(h.c.Server.Owner)
-
wg.Add(1)
-
go func() {
-
defer wg.Done()
-
bs, err := gr.Branches()
-
if err != nil {
-
errorsCh <- fmt.Errorf("fetching branches: %w", err)
-
return
-
}
-
branches = bs
-
}()
-
-
wg.Add(1)
-
go func() {
-
defer wg.Done()
-
ts, err := gr.Tags()
-
if err != nil {
-
errorsCh <- fmt.Errorf("fetching tags: %w", err)
-
return
-
}
-
tags = ts
-
}()
-
-
wg.Add(1)
-
go func() {
-
defer wg.Done()
-
fs, err := gr.FileTree(r.Context(), "")
-
if err != nil {
-
errorsCh <- fmt.Errorf("fetching filetree: %w", err)
-
return
-
}
-
files = fs
-
}()
-
-
wg.Wait()
-
close(errorsCh)
-
-
// show any errors
-
for err := range errorsCh {
-
l.Error("loading repo", "error", err.Error())
-
writeError(w, err.Error(), http.StatusInternalServerError)
-
return
-
}
-
-
rtags := []*types.TagReference{}
-
for _, tag := range tags {
-
var target *object.Tag
-
if tag.Target != plumbing.ZeroHash {
-
target = &tag
-
}
-
tr := types.TagReference{
-
Tag: target,
-
}
-
-
tr.Reference = types.Reference{
-
Name: tag.Name,
-
Hash: tag.Hash.String(),
-
}
-
-
if tag.Message != "" {
-
tr.Message = tag.Message
-
}
-
-
rtags = append(rtags, &tr)
-
}
-
-
var readmeContent string
-
var readmeFile string
-
for _, readme := range h.c.Repo.Readme {
-
content, _ := gr.FileContent(readme)
-
if len(content) > 0 {
-
readmeContent = string(content)
-
readmeFile = readme
-
}
-
}
-
-
if ref == "" {
-
mainBranch, err := gr.FindMainBranch()
-
if err != nil {
-
writeError(w, err.Error(), http.StatusInternalServerError)
-
l.Error("finding main branch", "error", err.Error())
-
return
-
}
-
ref = mainBranch
-
}
-
-
resp := types.RepoIndexResponse{
-
IsEmpty: false,
-
Ref: ref,
-
Commits: commits,
-
Description: getDescription(path),
-
Readme: readmeContent,
-
ReadmeFileName: readmeFile,
-
Files: files,
-
Branches: branches,
-
Tags: rtags,
-
TotalCommits: total,
-
}
-
-
writeJSON(w, resp)
-
return
-
}
-
-
func (h *Handle) RepoTree(w http.ResponseWriter, r *http.Request) {
-
treePath := chi.URLParam(r, "*")
-
ref := chi.URLParam(r, "ref")
-
ref, _ = url.PathUnescape(ref)
-
-
l := h.l.With("handler", "RepoTree", "ref", ref, "treePath", treePath)
-
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
-
gr, err := git.Open(path, ref)
+
// configure known-dids in jetstream consumer
+
dids, err := h.db.GetAllDids()
if err != nil {
-
notFound(w)
-
return
+
return nil, fmt.Errorf("failed to get all dids: %w", err)
}
-
-
files, err := gr.FileTree(r.Context(), treePath)
-
if err != nil {
-
writeError(w, err.Error(), http.StatusInternalServerError)
-
l.Error("file tree", "error", err.Error())
-
return
+
for _, d := range dids {
+
jc.AddDid(d)
}
-
resp := types.RepoTreeResponse{
-
Ref: ref,
-
Parent: treePath,
-
Description: getDescription(path),
-
DotDot: filepath.Dir(treePath),
-
Files: files,
-
}
-
-
writeJSON(w, resp)
-
return
-
}
-
-
func (h *Handle) BlobRaw(w http.ResponseWriter, r *http.Request) {
-
treePath := chi.URLParam(r, "*")
-
ref := chi.URLParam(r, "ref")
-
ref, _ = url.PathUnescape(ref)
-
-
l := h.l.With("handler", "BlobRaw", "ref", ref, "treePath", treePath)
-
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
-
gr, err := git.Open(path, ref)
+
err = h.jc.StartJetstream(ctx, h.processMessages)
if err != nil {
-
notFound(w)
-
return
+
return nil, fmt.Errorf("failed to start jetstream: %w", err)
}
-
contents, err := gr.RawContent(treePath)
-
if err != nil {
-
writeError(w, err.Error(), http.StatusBadRequest)
-
l.Error("file content", "error", err.Error())
-
return
-
}
+
r.Get("/", h.Index)
+
r.Get("/capabilities", h.Capabilities)
+
r.Get("/version", h.Version)
+
r.Get("/owner", func(w http.ResponseWriter, r *http.Request) {
+
w.Write([]byte(h.c.Server.Owner))
+
})
+
r.Route("/{did}", func(r chi.Router) {
+
// Repo routes
+
r.Route("/{name}", func(r chi.Router) {
-
mimeType := http.DetectContentType(contents)
+
r.Route("/languages", func(r chi.Router) {
+
r.Get("/", h.RepoLanguages)
+
r.Get("/{ref}", h.RepoLanguages)
+
})
-
// exception for svg
-
if filepath.Ext(treePath) == ".svg" {
-
mimeType = "image/svg+xml"
-
}
+
r.Get("/", h.RepoIndex)
+
r.Get("/info/refs", h.InfoRefs)
+
r.Post("/git-upload-pack", h.UploadPack)
+
r.Post("/git-receive-pack", h.ReceivePack)
+
r.Get("/compare/{rev1}/{rev2}", h.Compare) // git diff-tree compare of two objects
-
contentHash := sha256.Sum256(contents)
-
eTag := fmt.Sprintf("\"%x\"", contentHash)
+
r.Route("/tree/{ref}", func(r chi.Router) {
+
r.Get("/", h.RepoIndex)
+
r.Get("/*", h.RepoTree)
+
})
-
// allow image, video, and text/plain files to be served directly
-
switch {
-
case strings.HasPrefix(mimeType, "image/"), strings.HasPrefix(mimeType, "video/"):
-
if clientETag := r.Header.Get("If-None-Match"); clientETag == eTag {
-
w.WriteHeader(http.StatusNotModified)
-
return
-
}
-
w.Header().Set("ETag", eTag)
+
r.Route("/blob/{ref}", func(r chi.Router) {
+
r.Get("/*", h.Blob)
+
})
-
case strings.HasPrefix(mimeType, "text/plain"):
-
w.Header().Set("Cache-Control", "public, no-cache")
-
-
default:
-
l.Error("attempted to serve disallowed file type", "mimetype", mimeType)
-
writeError(w, "only image, video, and text files can be accessed directly", http.StatusForbidden)
-
return
-
}
-
-
w.Header().Set("Content-Type", mimeType)
-
w.Write(contents)
-
}
-
-
func (h *Handle) Blob(w http.ResponseWriter, r *http.Request) {
-
treePath := chi.URLParam(r, "*")
-
ref := chi.URLParam(r, "ref")
-
ref, _ = url.PathUnescape(ref)
-
-
l := h.l.With("handler", "Blob", "ref", ref, "treePath", treePath)
-
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
-
gr, err := git.Open(path, ref)
-
if err != nil {
-
notFound(w)
-
return
-
}
-
-
var isBinaryFile bool = false
-
contents, err := gr.FileContent(treePath)
-
if errors.Is(err, git.ErrBinaryFile) {
-
isBinaryFile = true
-
} else if errors.Is(err, object.ErrFileNotFound) {
-
notFound(w)
-
return
-
} else if err != nil {
-
writeError(w, err.Error(), http.StatusInternalServerError)
-
return
-
}
-
-
bytes := []byte(contents)
-
// safe := string(sanitize(bytes))
-
sizeHint := len(bytes)
-
-
resp := types.RepoBlobResponse{
-
Ref: ref,
-
Contents: string(bytes),
-
Path: treePath,
-
IsBinary: isBinaryFile,
-
SizeHint: uint64(sizeHint),
-
}
-
-
h.showFile(resp, w, l)
-
}
-
-
func (h *Handle) Archive(w http.ResponseWriter, r *http.Request) {
-
name := chi.URLParam(r, "name")
-
file := chi.URLParam(r, "file")
-
-
l := h.l.With("handler", "Archive", "name", name, "file", file)
-
-
// TODO: extend this to add more files compression (e.g.: xz)
-
if !strings.HasSuffix(file, ".tar.gz") {
-
notFound(w)
-
return
-
}
-
-
ref := strings.TrimSuffix(file, ".tar.gz")
-
-
unescapedRef, err := url.PathUnescape(ref)
-
if err != nil {
-
notFound(w)
-
return
-
}
-
-
safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(unescapedRef).Short(), "/", "-")
-
-
// This allows the browser to use a proper name for the file when
-
// downloading
-
filename := fmt.Sprintf("%s-%s.tar.gz", name, safeRefFilename)
-
setContentDisposition(w, filename)
-
setGZipMIME(w)
-
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
-
gr, err := git.Open(path, unescapedRef)
-
if err != nil {
-
notFound(w)
-
return
-
}
-
-
gw := gzip.NewWriter(w)
-
defer gw.Close()
-
-
prefix := fmt.Sprintf("%s-%s", name, safeRefFilename)
-
err = gr.WriteTar(gw, prefix)
-
if err != nil {
-
// once we start writing to the body we can't report error anymore
-
// so we are only left with printing the error.
-
l.Error("writing tar file", "error", err.Error())
-
return
-
}
-
-
err = gw.Flush()
-
if err != nil {
-
// once we start writing to the body we can't report error anymore
-
// so we are only left with printing the error.
-
l.Error("flushing?", "error", err.Error())
-
return
-
}
-
}
-
-
func (h *Handle) Log(w http.ResponseWriter, r *http.Request) {
-
ref := chi.URLParam(r, "ref")
-
ref, _ = url.PathUnescape(ref)
-
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
-
-
l := h.l.With("handler", "Log", "ref", ref, "path", path)
-
-
gr, err := git.Open(path, ref)
-
if err != nil {
-
notFound(w)
-
return
-
}
-
-
// Get page parameters
-
page := 1
-
pageSize := 30
-
-
if pageParam := r.URL.Query().Get("page"); pageParam != "" {
-
if p, err := strconv.Atoi(pageParam); err == nil && p > 0 {
-
page = p
-
}
-
}
-
-
if pageSizeParam := r.URL.Query().Get("per_page"); pageSizeParam != "" {
-
if ps, err := strconv.Atoi(pageSizeParam); err == nil && ps > 0 {
-
pageSize = ps
-
}
-
}
-
-
// convert to offset/limit
-
offset := (page - 1) * pageSize
-
limit := pageSize
-
-
commits, err := gr.Commits(offset, limit)
-
if err != nil {
-
writeError(w, err.Error(), http.StatusInternalServerError)
-
l.Error("fetching commits", "error", err.Error())
-
return
-
}
-
-
total := len(commits)
-
-
resp := types.RepoLogResponse{
-
Commits: commits,
-
Ref: ref,
-
Description: getDescription(path),
-
Log: true,
-
Total: total,
-
Page: page,
-
PerPage: pageSize,
-
}
-
-
writeJSON(w, resp)
-
return
-
}
-
-
func (h *Handle) Diff(w http.ResponseWriter, r *http.Request) {
-
ref := chi.URLParam(r, "ref")
-
ref, _ = url.PathUnescape(ref)
-
-
l := h.l.With("handler", "Diff", "ref", ref)
-
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
-
gr, err := git.Open(path, ref)
-
if err != nil {
-
notFound(w)
-
return
-
}
-
-
diff, err := gr.Diff()
-
if err != nil {
-
writeError(w, err.Error(), http.StatusInternalServerError)
-
l.Error("getting diff", "error", err.Error())
-
return
-
}
-
-
resp := types.RepoCommitResponse{
-
Ref: ref,
-
Diff: diff,
-
}
-
-
writeJSON(w, resp)
-
return
-
}
-
-
func (h *Handle) Tags(w http.ResponseWriter, r *http.Request) {
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
-
l := h.l.With("handler", "Refs")
-
-
gr, err := git.Open(path, "")
-
if err != nil {
-
notFound(w)
-
return
-
}
-
-
tags, err := gr.Tags()
-
if err != nil {
-
// Non-fatal, we *should* have at least one branch to show.
-
l.Warn("getting tags", "error", err.Error())
-
}
-
-
rtags := []*types.TagReference{}
-
for _, tag := range tags {
-
var target *object.Tag
-
if tag.Target != plumbing.ZeroHash {
-
target = &tag
-
}
-
tr := types.TagReference{
-
Tag: target,
-
}
-
-
tr.Reference = types.Reference{
-
Name: tag.Name,
-
Hash: tag.Hash.String(),
-
}
-
-
if tag.Message != "" {
-
tr.Message = tag.Message
-
}
-
-
rtags = append(rtags, &tr)
-
}
-
-
resp := types.RepoTagsResponse{
-
Tags: rtags,
-
}
-
-
writeJSON(w, resp)
-
return
-
}
-
-
func (h *Handle) Branches(w http.ResponseWriter, r *http.Request) {
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
-
-
gr, err := git.PlainOpen(path)
-
if err != nil {
-
notFound(w)
-
return
-
}
+
r.Route("/raw/{ref}", func(r chi.Router) {
+
r.Get("/*", h.BlobRaw)
+
})
-
branches, _ := gr.Branches()
-
-
resp := types.RepoBranchesResponse{
-
Branches: branches,
-
}
-
-
writeJSON(w, resp)
-
return
-
}
-
-
func (h *Handle) Branch(w http.ResponseWriter, r *http.Request) {
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
-
branchName := chi.URLParam(r, "branch")
-
branchName, _ = url.PathUnescape(branchName)
-
-
l := h.l.With("handler", "Branch")
-
-
gr, err := git.PlainOpen(path)
-
if err != nil {
-
notFound(w)
-
return
-
}
-
-
ref, err := gr.Branch(branchName)
-
if err != nil {
-
l.Error("getting branch", "error", err.Error())
-
writeError(w, err.Error(), http.StatusInternalServerError)
-
return
-
}
+
r.Get("/log/{ref}", h.Log)
+
r.Get("/archive/{file}", h.Archive)
+
r.Get("/commit/{ref}", h.Diff)
+
r.Get("/tags", h.Tags)
+
r.Route("/branches", func(r chi.Router) {
+
r.Get("/", h.Branches)
+
r.Get("/{branch}", h.Branch)
+
r.Get("/default", h.DefaultBranch)
+
})
+
})
+
})
-
commit, err := gr.Commit(ref.Hash())
-
if err != nil {
-
l.Error("getting commit object", "error", err.Error())
-
writeError(w, err.Error(), http.StatusInternalServerError)
-
return
-
}
+
// xrpc apis
+
r.Mount("/xrpc", h.XrpcRouter())
-
defaultBranch, err := gr.FindMainBranch()
-
isDefault := false
-
if err != nil {
-
l.Error("getting default branch", "error", err.Error())
-
// do not quit though
-
} else if defaultBranch == branchName {
-
isDefault = true
-
}
+
// Socket that streams git oplogs
+
r.Get("/events", h.Events)
-
resp := types.RepoBranchResponse{
-
Branch: types.Branch{
-
Reference: types.Reference{
-
Name: ref.Name().Short(),
-
Hash: ref.Hash().String(),
-
},
-
Commit: commit,
-
IsDefault: isDefault,
-
},
-
}
+
// All public keys on the knot.
+
r.Get("/keys", h.Keys)
-
writeJSON(w, resp)
-
return
+
return r, nil
}
-
func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) {
-
l := h.l.With("handler", "Keys")
-
-
switch r.Method {
-
case http.MethodGet:
-
keys, err := h.db.GetAllPublicKeys()
-
if err != nil {
-
writeError(w, err.Error(), http.StatusInternalServerError)
-
l.Error("getting public keys", "error", err.Error())
-
return
-
}
-
-
data := make([]map[string]any, 0)
-
for _, key := range keys {
-
j := key.JSON()
-
data = append(data, j)
-
}
-
writeJSON(w, data)
-
return
-
-
case http.MethodPut:
-
pk := db.PublicKey{}
-
if err := json.NewDecoder(r.Body).Decode(&pk); err != nil {
-
writeError(w, "invalid request body", http.StatusBadRequest)
-
return
-
}
+
func (h *Handle) XrpcRouter() http.Handler {
+
logger := tlog.New("knots")
-
_, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pk.Key))
-
if err != nil {
-
writeError(w, "invalid pubkey", http.StatusBadRequest)
-
}
+
serviceAuth := serviceauth.NewServiceAuth(h.l, h.resolver, h.c.Server.Did().String())
-
if err := h.db.AddPublicKey(pk); err != nil {
-
writeError(w, err.Error(), http.StatusInternalServerError)
-
l.Error("adding public key", "error", err.Error())
-
return
-
}
-
-
w.WriteHeader(http.StatusNoContent)
-
return
+
xrpc := &xrpc.Xrpc{
+
Config: h.c,
+
Db: h.db,
+
Ingester: h.jc,
+
Enforcer: h.e,
+
Logger: logger,
+
Notifier: h.n,
+
Resolver: h.resolver,
+
ServiceAuth: serviceAuth,
}
+
return xrpc.Router()
}
-
func (h *Handle) RepoForkAheadBehind(w http.ResponseWriter, r *http.Request) {
-
l := h.l.With("handler", "RepoForkAheadBehind")
+
// version is set during build time.
+
var version string
-
data := struct {
-
Did string `json:"did"`
-
Source string `json:"source"`
-
Name string `json:"name,omitempty"`
-
HiddenRef string `json:"hiddenref"`
-
}{}
-
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
-
writeError(w, "invalid request body", http.StatusBadRequest)
-
return
-
}
-
-
did := data.Did
-
source := data.Source
-
-
if did == "" || source == "" {
-
l.Error("invalid request body, empty did or name")
-
w.WriteHeader(http.StatusBadRequest)
-
return
-
}
-
-
var name string
-
if data.Name != "" {
-
name = data.Name
-
} else {
-
name = filepath.Base(source)
-
}
-
-
branch := chi.URLParam(r, "branch")
-
branch, _ = url.PathUnescape(branch)
-
-
relativeRepoPath := filepath.Join(did, name)
-
repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
-
-
gr, err := git.PlainOpen(repoPath)
-
if err != nil {
-
log.Println(err)
-
notFound(w)
-
return
-
}
-
-
forkCommit, err := gr.ResolveRevision(branch)
-
if err != nil {
-
l.Error("error resolving ref revision", "msg", err.Error())
-
writeError(w, fmt.Sprintf("error resolving revision %s", branch), http.StatusBadRequest)
-
return
-
}
-
-
sourceCommit, err := gr.ResolveRevision(data.HiddenRef)
-
if err != nil {
-
l.Error("error resolving hidden ref revision", "msg", err.Error())
-
writeError(w, fmt.Sprintf("error resolving revision %s", data.HiddenRef), http.StatusBadRequest)
-
return
-
}
-
-
status := types.UpToDate
-
if forkCommit.Hash.String() != sourceCommit.Hash.String() {
-
isAncestor, err := forkCommit.IsAncestor(sourceCommit)
-
if err != nil {
-
log.Printf("error resolving whether %s is ancestor of %s: %s", branch, data.HiddenRef, err)
+
func (h *Handle) Version(w http.ResponseWriter, r *http.Request) {
+
if version == "" {
+
info, ok := debug.ReadBuildInfo()
+
if !ok {
+
http.Error(w, "failed to read build info", http.StatusInternalServerError)
return
}
-
if isAncestor {
-
status = types.FastForwardable
-
} else {
-
status = types.Conflict
+
var modVer string
+
for _, mod := range info.Deps {
+
if mod.Path == "tangled.sh/tangled.sh/knotserver" {
+
version = mod.Version
+
break
+
}
}
-
}
-
w.Header().Set("Content-Type", "application/json")
-
json.NewEncoder(w).Encode(types.AncestorCheckResponse{Status: status})
-
}
-
-
func (h *Handle) RepoLanguages(w http.ResponseWriter, r *http.Request) {
-
repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
-
ref := chi.URLParam(r, "ref")
-
ref, _ = url.PathUnescape(ref)
-
-
l := h.l.With("handler", "RepoLanguages")
-
-
gr, err := git.Open(repoPath, ref)
-
if err != nil {
-
l.Error("opening repo", "error", err.Error())
-
notFound(w)
-
return
-
}
-
-
ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
-
defer cancel()
-
-
sizes, err := gr.AnalyzeLanguages(ctx)
-
if err != nil {
-
l.Error("failed to analyze languages", "error", err.Error())
-
writeError(w, err.Error(), http.StatusNoContent)
-
return
-
}
-
-
resp := types.RepoLanguageResponse{Languages: sizes}
-
-
writeJSON(w, resp)
-
}
-
-
func (h *Handle) RepoForkSync(w http.ResponseWriter, r *http.Request) {
-
l := h.l.With("handler", "RepoForkSync")
-
-
data := struct {
-
Did string `json:"did"`
-
Source string `json:"source"`
-
Name string `json:"name,omitempty"`
-
}{}
-
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
-
writeError(w, "invalid request body", http.StatusBadRequest)
-
return
-
}
-
-
did := data.Did
-
source := data.Source
-
-
if did == "" || source == "" {
-
l.Error("invalid request body, empty did or name")
-
w.WriteHeader(http.StatusBadRequest)
-
return
-
}
-
-
var name string
-
if data.Name != "" {
-
name = data.Name
-
} else {
-
name = filepath.Base(source)
-
}
-
-
branch := chi.URLParam(r, "*")
-
branch, _ = url.PathUnescape(branch)
-
-
relativeRepoPath := filepath.Join(did, name)
-
repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
-
-
gr, err := git.Open(repoPath, branch)
-
if err != nil {
-
log.Println(err)
-
notFound(w)
-
return
-
}
-
-
err = gr.Sync()
-
if err != nil {
-
l.Error("error syncing repo fork", "error", err.Error())
-
writeError(w, err.Error(), http.StatusInternalServerError)
-
return
-
}
-
-
w.WriteHeader(http.StatusNoContent)
-
}
-
-
func (h *Handle) RepoFork(w http.ResponseWriter, r *http.Request) {
-
l := h.l.With("handler", "RepoFork")
-
-
data := struct {
-
Did string `json:"did"`
-
Source string `json:"source"`
-
Name string `json:"name,omitempty"`
-
}{}
-
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
-
writeError(w, "invalid request body", http.StatusBadRequest)
-
return
-
}
-
-
did := data.Did
-
source := data.Source
-
-
if did == "" || source == "" {
-
l.Error("invalid request body, empty did or name")
-
w.WriteHeader(http.StatusBadRequest)
-
return
-
}
-
-
var name string
-
if data.Name != "" {
-
name = data.Name
-
} else {
-
name = filepath.Base(source)
-
}
-
-
relativeRepoPath := filepath.Join(did, name)
-
repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
-
-
err := git.Fork(repoPath, source)
-
if err != nil {
-
l.Error("forking repo", "error", err.Error())
-
writeError(w, err.Error(), http.StatusInternalServerError)
-
return
-
}
-
-
// add perms for this user to access the repo
-
err = h.e.AddRepo(did, rbac.ThisServer, relativeRepoPath)
-
if err != nil {
-
l.Error("adding repo permissions", "error", err.Error())
-
writeError(w, err.Error(), http.StatusInternalServerError)
-
return
-
}
-
-
hook.SetupRepo(
-
hook.Config(
-
hook.WithScanPath(h.c.Repo.ScanPath),
-
hook.WithInternalApi(h.c.Server.InternalListenAddr),
-
),
-
repoPath,
-
)
-
-
w.WriteHeader(http.StatusNoContent)
-
}
-
-
func (h *Handle) RemoveRepo(w http.ResponseWriter, r *http.Request) {
-
l := h.l.With("handler", "RemoveRepo")
-
-
data := struct {
-
Did string `json:"did"`
-
Name string `json:"name"`
-
}{}
-
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
-
writeError(w, "invalid request body", http.StatusBadRequest)
-
return
-
}
-
-
did := data.Did
-
name := data.Name
-
-
if did == "" || name == "" {
-
l.Error("invalid request body, empty did or name")
-
w.WriteHeader(http.StatusBadRequest)
-
return
-
}
-
-
relativeRepoPath := filepath.Join(did, name)
-
repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath)
-
err := os.RemoveAll(repoPath)
-
if err != nil {
-
l.Error("removing repo", "error", err.Error())
-
writeError(w, err.Error(), http.StatusInternalServerError)
-
return
-
}
-
-
w.WriteHeader(http.StatusNoContent)
-
-
}
-
func (h *Handle) Merge(w http.ResponseWriter, r *http.Request) {
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
-
-
data := types.MergeRequest{}
-
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
-
writeError(w, err.Error(), http.StatusBadRequest)
-
h.l.Error("git: failed to unmarshal json patch", "handler", "Merge", "error", err)
-
return
-
}
-
-
mo := &git.MergeOptions{
-
AuthorName: data.AuthorName,
-
AuthorEmail: data.AuthorEmail,
-
CommitBody: data.CommitBody,
-
CommitMessage: data.CommitMessage,
-
}
-
-
patch := data.Patch
-
branch := data.Branch
-
gr, err := git.Open(path, branch)
-
if err != nil {
-
notFound(w)
-
return
-
}
-
-
mo.FormatPatch = patchutil.IsFormatPatch(patch)
-
-
if err := gr.MergeWithOptions([]byte(patch), branch, mo); err != nil {
-
var mergeErr *git.ErrMerge
-
if errors.As(err, &mergeErr) {
-
conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts))
-
for i, conflict := range mergeErr.Conflicts {
-
conflicts[i] = types.ConflictInfo{
-
Filename: conflict.Filename,
-
Reason: conflict.Reason,
-
}
-
}
-
response := types.MergeCheckResponse{
-
IsConflicted: true,
-
Conflicts: conflicts,
-
Message: mergeErr.Message,
-
}
-
writeConflict(w, response)
-
h.l.Error("git: merge conflict", "handler", "Merge", "error", mergeErr)
-
} else {
-
writeError(w, err.Error(), http.StatusBadRequest)
-
h.l.Error("git: failed to merge", "handler", "Merge", "error", err.Error())
+
if modVer == "" {
+
version = "unknown"
}
-
return
}
-
w.WriteHeader(http.StatusOK)
+
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+
fmt.Fprintf(w, "knotserver/%s", version)
}
-
func (h *Handle) MergeCheck(w http.ResponseWriter, r *http.Request) {
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
-
-
var data struct {
-
Patch string `json:"patch"`
-
Branch string `json:"branch"`
-
}
+
func (h *Handle) configureOwner() error {
+
cfgOwner := h.c.Server.Owner
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
-
writeError(w, err.Error(), http.StatusBadRequest)
-
h.l.Error("git: failed to unmarshal json patch", "handler", "MergeCheck", "error", err)
-
return
-
}
+
rbacDomain := "thisserver"
-
patch := data.Patch
-
branch := data.Branch
-
gr, err := git.Open(path, branch)
+
existing, err := h.e.GetKnotUsersByRole("server:owner", rbacDomain)
if err != nil {
-
notFound(w)
-
return
+
return err
}
-
err = gr.MergeCheck([]byte(patch), branch)
-
if err == nil {
-
response := types.MergeCheckResponse{
-
IsConflicted: false,
-
}
-
writeJSON(w, response)
-
return
-
}
+
switch len(existing) {
+
case 0:
+
// no owner configured, continue
+
case 1:
+
// find existing owner
+
existingOwner := existing[0]
-
var mergeErr *git.ErrMerge
-
if errors.As(err, &mergeErr) {
-
conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts))
-
for i, conflict := range mergeErr.Conflicts {
-
conflicts[i] = types.ConflictInfo{
-
Filename: conflict.Filename,
-
Reason: conflict.Reason,
-
}
-
}
-
response := types.MergeCheckResponse{
-
IsConflicted: true,
-
Conflicts: conflicts,
-
Message: mergeErr.Message,
+
// no ownership change, this is okay
+
if existingOwner == h.c.Server.Owner {
+
break
}
-
writeConflict(w, response)
-
h.l.Error("git: merge conflict", "handler", "MergeCheck", "error", mergeErr.Error())
-
return
-
}
-
writeError(w, err.Error(), http.StatusInternalServerError)
-
h.l.Error("git: failed to check merge", "handler", "MergeCheck", "error", err.Error())
-
}
-
func (h *Handle) Compare(w http.ResponseWriter, r *http.Request) {
-
rev1 := chi.URLParam(r, "rev1")
-
rev1, _ = url.PathUnescape(rev1)
-
-
rev2 := chi.URLParam(r, "rev2")
-
rev2, _ = url.PathUnescape(rev2)
-
-
l := h.l.With("handler", "Compare", "r1", rev1, "r2", rev2)
-
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
-
gr, err := git.PlainOpen(path)
-
if err != nil {
-
notFound(w)
-
return
-
}
-
-
commit1, err := gr.ResolveRevision(rev1)
-
if err != nil {
-
l.Error("error resolving revision 1", "msg", err.Error())
-
writeError(w, fmt.Sprintf("error resolving revision %s", rev1), http.StatusBadRequest)
-
return
-
}
-
-
commit2, err := gr.ResolveRevision(rev2)
-
if err != nil {
-
l.Error("error resolving revision 2", "msg", err.Error())
-
writeError(w, fmt.Sprintf("error resolving revision %s", rev2), http.StatusBadRequest)
-
return
-
}
-
-
rawPatch, formatPatch, err := gr.FormatPatch(commit1, commit2)
-
if err != nil {
-
l.Error("error comparing revisions", "msg", err.Error())
-
writeError(w, "error comparing revisions", http.StatusBadRequest)
-
return
-
}
-
-
writeJSON(w, types.RepoFormatPatchResponse{
-
Rev1: commit1.Hash.String(),
-
Rev2: commit2.Hash.String(),
-
FormatPatch: formatPatch,
-
Patch: rawPatch,
-
})
-
return
-
}
-
-
func (h *Handle) NewHiddenRef(w http.ResponseWriter, r *http.Request) {
-
l := h.l.With("handler", "NewHiddenRef")
-
-
forkRef := chi.URLParam(r, "forkRef")
-
forkRef, _ = url.PathUnescape(forkRef)
-
-
remoteRef := chi.URLParam(r, "remoteRef")
-
remoteRef, _ = url.PathUnescape(remoteRef)
-
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
-
gr, err := git.PlainOpen(path)
-
if err != nil {
-
notFound(w)
-
return
-
}
-
-
err = gr.TrackHiddenRemoteRef(forkRef, remoteRef)
-
if err != nil {
-
l.Error("error tracking hidden remote ref", "msg", err.Error())
-
writeError(w, "error tracking hidden remote ref", http.StatusBadRequest)
-
return
-
}
-
-
w.WriteHeader(http.StatusNoContent)
-
return
-
}
-
-
func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) {
-
l := h.l.With("handler", "AddMember")
-
-
data := struct {
-
Did string `json:"did"`
-
}{}
-
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
-
writeError(w, "invalid request body", http.StatusBadRequest)
-
return
-
}
-
-
did := data.Did
-
-
if err := h.db.AddDid(did); err != nil {
-
l.Error("adding did", "error", err.Error())
-
writeError(w, err.Error(), http.StatusInternalServerError)
-
return
-
}
-
h.jc.AddDid(did)
-
-
if err := h.e.AddKnotMember(rbac.ThisServer, did); err != nil {
-
l.Error("adding member", "error", err.Error())
-
writeError(w, err.Error(), http.StatusInternalServerError)
-
return
-
}
-
-
if err := h.fetchAndAddKeys(r.Context(), did); err != nil {
-
l.Error("fetching and adding keys", "error", err.Error())
-
writeError(w, err.Error(), http.StatusInternalServerError)
-
return
-
}
-
-
w.WriteHeader(http.StatusNoContent)
-
}
-
-
func (h *Handle) AddRepoCollaborator(w http.ResponseWriter, r *http.Request) {
-
l := h.l.With("handler", "AddRepoCollaborator")
-
-
data := struct {
-
Did string `json:"did"`
-
}{}
-
-
ownerDid := chi.URLParam(r, "did")
-
repo := chi.URLParam(r, "name")
-
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
-
writeError(w, "invalid request body", http.StatusBadRequest)
-
return
-
}
-
-
if err := h.db.AddDid(data.Did); err != nil {
-
l.Error("adding did", "error", err.Error())
-
writeError(w, err.Error(), http.StatusInternalServerError)
-
return
-
}
-
h.jc.AddDid(data.Did)
-
-
repoName, _ := securejoin.SecureJoin(ownerDid, repo)
-
if err := h.e.AddCollaborator(data.Did, rbac.ThisServer, repoName); err != nil {
-
l.Error("adding repo collaborator", "error", err.Error())
-
writeError(w, err.Error(), http.StatusInternalServerError)
-
return
-
}
-
-
if err := h.fetchAndAddKeys(r.Context(), data.Did); err != nil {
-
l.Error("fetching and adding keys", "error", err.Error())
-
writeError(w, err.Error(), http.StatusInternalServerError)
-
return
-
}
-
-
w.WriteHeader(http.StatusNoContent)
-
}
-
-
func (h *Handle) DefaultBranch(w http.ResponseWriter, r *http.Request) {
-
l := h.l.With("handler", "DefaultBranch")
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
-
-
gr, err := git.Open(path, "")
-
if err != nil {
-
notFound(w)
-
return
-
}
-
-
branch, err := gr.FindMainBranch()
-
if err != nil {
-
writeError(w, err.Error(), http.StatusInternalServerError)
-
l.Error("getting default branch", "error", err.Error())
-
return
-
}
-
-
writeJSON(w, types.RepoDefaultBranchResponse{
-
Branch: branch,
-
})
-
}
-
-
func (h *Handle) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {
-
l := h.l.With("handler", "SetDefaultBranch")
-
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
-
-
data := struct {
-
Branch string `json:"branch"`
-
}{}
-
-
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
-
writeError(w, err.Error(), http.StatusBadRequest)
-
return
-
}
-
-
gr, err := git.PlainOpen(path)
-
if err != nil {
-
notFound(w)
-
return
-
}
-
-
err = gr.SetDefaultBranch(data.Branch)
-
if err != nil {
-
writeError(w, err.Error(), http.StatusInternalServerError)
-
l.Error("setting default branch", "error", err.Error())
-
return
-
}
-
-
w.WriteHeader(http.StatusNoContent)
-
}
-
-
func (h *Handle) Health(w http.ResponseWriter, r *http.Request) {
-
w.Write([]byte("ok"))
-
}
-
-
func validateRepoName(name string) error {
-
// check for path traversal attempts
-
if name == "." || name == ".." ||
-
strings.Contains(name, "/") || strings.Contains(name, "\\") {
-
return fmt.Errorf("Repository name contains invalid path characters")
-
}
-
-
// check for sequences that could be used for traversal when normalized
-
if strings.Contains(name, "./") || strings.Contains(name, "../") ||
-
strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") {
-
return fmt.Errorf("Repository name contains invalid path sequence")
-
}
-
-
// then continue with character validation
-
for _, char := range name {
-
if !((char >= 'a' && char <= 'z') ||
-
(char >= 'A' && char <= 'Z') ||
-
(char >= '0' && char <= '9') ||
-
char == '-' || char == '_' || char == '.') {
-
return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores")
+
// remove existing owner
+
err = h.e.RemoveKnotOwner(rbacDomain, existingOwner)
+
if err != nil {
+
return nil
}
-
}
-
-
// additional check to prevent multiple sequential dots
-
if strings.Contains(name, "..") {
-
return fmt.Errorf("Repository name cannot contain sequential dots")
+
default:
+
return fmt.Errorf("more than one owner in DB, try deleting %q and starting over", h.c.Server.DBPath)
}
-
// if all checks pass
-
return nil
+
return h.e.AddKnotOwner(rbacDomain, cfgOwner)
}
+8
rbac/rbac.go
···
return e.isInviteAllowed(user, intoSpindle(domain))
}
+
func (e *Enforcer) IsRepoCreateAllowed(user, domain string) (bool, error) {
+
return e.E.Enforce(user, domain, domain, "repo:create")
+
}
+
+
func (e *Enforcer) IsRepoDeleteAllowed(user, domain string) (bool, error) {
+
return e.E.Enforce(user, domain, domain, "repo:delete")
+
}
+
func (e *Enforcer) IsPushAllowed(user, domain, repo string) (bool, error) {
return e.E.Enforce(user, domain, repo, "repo:push")
}