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

cmd/knot: unified knot cli

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.sh>

anirudh.fi 275d63d3 d4c71b3e

verified
Changed files
+449 -268
cmd
keyfetch
knot
repoguard
guard
keyfetch
knotserver
-15
cmd/keyfetch/format.go
···
-
package main
-
-
import (
-
"fmt"
-
)
-
-
func formatKeyData(repoguardPath, gitDir, logPath, endpoint string, data []map[string]interface{}) string {
-
var result string
-
for _, entry := range data {
-
result += fmt.Sprintf(
-
`command="%s -base-dir %s -user %s -log-path %s -internal-api %s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty %s`+"\n",
-
repoguardPath, gitDir, entry["did"], logPath, endpoint, entry["key"])
-
}
-
return result
-
}
···
-46
cmd/keyfetch/main.go
···
-
// This program must be configured to run as the sshd AuthorizedKeysCommand.
-
// The format looks something like this:
-
// Match User git
-
// AuthorizedKeysCommand /keyfetch -internal-api http://localhost:5444 -repoguard-path /home/git/repoguard
-
// AuthorizedKeysCommandUser nobody
-
//
-
// The command and its parent directories must be owned by root and set to 0755. Hence, the ideal location for this is
-
// somewhere already owned by root so you don't have to mess with directory perms.
-
-
package main
-
-
import (
-
"encoding/json"
-
"flag"
-
"fmt"
-
"io"
-
"log"
-
"net/http"
-
)
-
-
func main() {
-
endpoint := flag.String("internal-api", "http://localhost:5444", "Internal API endpoint")
-
repoguardPath := flag.String("repoguard-path", "/home/git/repoguard", "Path to the repoguard binary")
-
gitDir := flag.String("git-dir", "/home/git", "Path to the git directory")
-
logPath := flag.String("log-path", "/home/git/log", "Path to log file")
-
flag.Parse()
-
-
resp, err := http.Get(*endpoint + "/keys")
-
if err != nil {
-
log.Fatalf("error fetching keys: %v", err)
-
}
-
defer resp.Body.Close()
-
-
body, err := io.ReadAll(resp.Body)
-
if err != nil {
-
log.Fatalf("error reading response body: %v", err)
-
}
-
-
var data []map[string]interface{}
-
err = json.Unmarshal(body, &data)
-
if err != nil {
-
log.Fatalf("error unmarshalling response body: %v", err)
-
}
-
-
fmt.Print(formatKeyData(*repoguardPath, *gitDir, *logPath, *endpoint, data))
-
}
···
+33
cmd/knot/main.go
···
···
+
package main
+
+
import (
+
"context"
+
"os"
+
+
"github.com/urfave/cli/v3"
+
"tangled.sh/tangled.sh/core/guard"
+
"tangled.sh/tangled.sh/core/keyfetch"
+
"tangled.sh/tangled.sh/core/knotserver"
+
"tangled.sh/tangled.sh/core/log"
+
)
+
+
func main() {
+
cmd := &cli.Command{
+
Name: "knot",
+
Usage: "knot administration and operation tool",
+
Commands: []*cli.Command{
+
guard.Command(),
+
knotserver.Command(),
+
keyfetch.Command(),
+
},
+
}
+
+
ctx := context.Background()
+
logger := log.New("knot")
+
ctx = log.IntoContext(ctx, logger.With("command", cmd.Name))
+
+
if err := cmd.Run(ctx, os.Args); err != nil {
+
logger.Error(err.Error())
+
os.Exit(-1)
+
}
+
}
-207
cmd/repoguard/main.go
···
-
package main
-
-
import (
-
"context"
-
"flag"
-
"fmt"
-
"log"
-
"net/http"
-
"net/url"
-
"os"
-
"os/exec"
-
"strings"
-
"time"
-
-
securejoin "github.com/cyphar/filepath-securejoin"
-
"tangled.sh/tangled.sh/core/appview/idresolver"
-
)
-
-
var (
-
logger *log.Logger
-
logFile *os.File
-
clientIP string
-
-
// Command line flags
-
incomingUser = flag.String("user", "", "Allowed git user")
-
baseDirFlag = flag.String("base-dir", "/home/git", "Base directory for git repositories")
-
logPathFlag = flag.String("log-path", "/var/log/git-wrapper.log", "Path to log file")
-
endpoint = flag.String("internal-api", "http://localhost:5444", "Internal API endpoint")
-
)
-
-
func main() {
-
flag.Parse()
-
-
defer cleanup()
-
initLogger()
-
-
// Get client IP from SSH environment
-
if connInfo := os.Getenv("SSH_CONNECTION"); connInfo != "" {
-
parts := strings.Fields(connInfo)
-
if len(parts) > 0 {
-
clientIP = parts[0]
-
}
-
}
-
-
if *incomingUser == "" {
-
exitWithLog("access denied: no user specified")
-
}
-
-
sshCommand := os.Getenv("SSH_ORIGINAL_COMMAND")
-
-
logEvent("Connection attempt", map[string]interface{}{
-
"user": *incomingUser,
-
"command": sshCommand,
-
"client": clientIP,
-
})
-
-
if sshCommand == "" {
-
exitWithLog("access denied: we don't serve interactive shells :)")
-
}
-
-
cmdParts := strings.Fields(sshCommand)
-
if len(cmdParts) < 2 {
-
exitWithLog("invalid command format")
-
}
-
-
gitCommand := cmdParts[0]
-
-
// did:foo/repo-name or
-
// handle/repo-name or
-
// any of the above with a leading slash (/)
-
-
components := strings.Split(strings.TrimPrefix(strings.Trim(cmdParts[1], "'"), "/"), "/")
-
logEvent("Command components", map[string]interface{}{
-
"components": components,
-
})
-
if len(components) != 2 {
-
exitWithLog("invalid repo format, needs <user>/<repo> or /<user>/<repo>")
-
}
-
-
didOrHandle := components[0]
-
did := resolveToDid(didOrHandle)
-
repoName := components[1]
-
qualifiedRepoName, _ := securejoin.SecureJoin(did, repoName)
-
-
validCommands := map[string]bool{
-
"git-receive-pack": true,
-
"git-upload-pack": true,
-
"git-upload-archive": true,
-
}
-
if !validCommands[gitCommand] {
-
exitWithLog("access denied: invalid git command")
-
}
-
-
if gitCommand != "git-upload-pack" {
-
if !isPushPermitted(*incomingUser, qualifiedRepoName) {
-
logEvent("all infos", map[string]interface{}{
-
"did": *incomingUser,
-
"reponame": qualifiedRepoName,
-
})
-
exitWithLog("access denied: user not allowed")
-
}
-
}
-
-
fullPath, _ := securejoin.SecureJoin(*baseDirFlag, qualifiedRepoName)
-
-
logEvent("Processing command", map[string]interface{}{
-
"user": *incomingUser,
-
"command": gitCommand,
-
"repo": repoName,
-
"fullPath": fullPath,
-
"client": clientIP,
-
})
-
-
if gitCommand == "git-upload-pack" {
-
fmt.Fprintf(os.Stderr, "\x02%s\n", "Welcome to this knot!")
-
} else {
-
fmt.Fprintf(os.Stderr, "%s\n", "Welcome to this knot!")
-
}
-
-
cmd := exec.Command(gitCommand, fullPath)
-
cmd.Stdout = os.Stdout
-
cmd.Stderr = os.Stderr
-
cmd.Stdin = os.Stdin
-
-
if err := cmd.Run(); err != nil {
-
exitWithLog(fmt.Sprintf("command failed: %v", err))
-
}
-
-
logEvent("Command completed", map[string]interface{}{
-
"user": *incomingUser,
-
"command": gitCommand,
-
"repo": repoName,
-
"success": true,
-
})
-
}
-
-
func resolveToDid(didOrHandle string) string {
-
resolver := idresolver.DefaultResolver()
-
ident, err := resolver.ResolveIdent(context.Background(), didOrHandle)
-
if err != nil {
-
exitWithLog(fmt.Sprintf("error resolving handle: %v", err))
-
}
-
-
// did:plc:foobarbaz/repo
-
return ident.DID.String()
-
}
-
-
func initLogger() {
-
var err error
-
logFile, err = os.OpenFile(*logPathFlag, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
-
if err != nil {
-
fmt.Fprintf(os.Stderr, "failed to open log file: %v\n", err)
-
os.Exit(1)
-
}
-
-
logger = log.New(logFile, "", 0)
-
}
-
-
func logEvent(event string, fields map[string]interface{}) {
-
entry := fmt.Sprintf(
-
"timestamp=%q event=%q",
-
time.Now().Format(time.RFC3339),
-
event,
-
)
-
-
for k, v := range fields {
-
entry += fmt.Sprintf(" %s=%q", k, v)
-
}
-
-
logger.Println(entry)
-
}
-
-
func exitWithLog(message string) {
-
logEvent("Access denied", map[string]interface{}{
-
"error": message,
-
})
-
logFile.Sync()
-
fmt.Fprintf(os.Stderr, "error: %s\n", message)
-
os.Exit(1)
-
}
-
-
func cleanup() {
-
if logFile != nil {
-
logFile.Sync()
-
logFile.Close()
-
}
-
}
-
-
func isPushPermitted(user, qualifiedRepoName string) bool {
-
u, _ := url.Parse(*endpoint + "/push-allowed")
-
q := u.Query()
-
q.Add("user", user)
-
q.Add("repo", qualifiedRepoName)
-
u.RawQuery = q.Encode()
-
-
req, err := http.Get(u.String())
-
if err != nil {
-
exitWithLog(fmt.Sprintf("error verifying permissions: %v", err))
-
}
-
-
logEvent("url", map[string]interface{}{
-
"url": u.String(),
-
"status": req.Status,
-
})
-
-
return req.StatusCode == http.StatusNoContent
-
}
···
+1
go.mod
···
github.com/posthog/posthog-go v1.5.5
github.com/resend/resend-go/v2 v2.15.0
github.com/sethvargo/go-envconfig v1.1.0
github.com/whyrusleeping/cbor-gen v0.3.1
github.com/yuin/goldmark v1.4.13
golang.org/x/net v0.39.0
···
github.com/posthog/posthog-go v1.5.5
github.com/resend/resend-go/v2 v2.15.0
github.com/sethvargo/go-envconfig v1.1.0
+
github.com/urfave/cli/v3 v3.3.3
github.com/whyrusleeping/cbor-gen v0.3.1
github.com/yuin/goldmark v1.4.13
golang.org/x/net v0.39.0
+2
go.sum
···
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/vmihailenco/go-tinylfu v0.2.2 h1:H1eiG6HM36iniK6+21n9LLpzx1G9R3DJa2UjUjbynsI=
github.com/vmihailenco/go-tinylfu v0.2.2/go.mod h1:CutYi2Q9puTxfcolkliPq4npPuofg9N9t8JVrjzwa3Q=
github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
···
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+
github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I=
+
github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
github.com/vmihailenco/go-tinylfu v0.2.2 h1:H1eiG6HM36iniK6+21n9LLpzx1G9R3DJa2UjUjbynsI=
github.com/vmihailenco/go-tinylfu v0.2.2/go.mod h1:CutYi2Q9puTxfcolkliPq4npPuofg9N9t8JVrjzwa3Q=
github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
+208
guard/guard.go
···
···
+
package guard
+
+
import (
+
"context"
+
"fmt"
+
"log/slog"
+
"net/http"
+
"net/url"
+
"os"
+
"os/exec"
+
"strings"
+
+
securejoin "github.com/cyphar/filepath-securejoin"
+
"github.com/urfave/cli/v3"
+
"tangled.sh/tangled.sh/core/appview/idresolver"
+
"tangled.sh/tangled.sh/core/log"
+
)
+
+
func Command() *cli.Command {
+
return &cli.Command{
+
Name: "guard",
+
Usage: "role-based access control for git over ssh (not for manual use)",
+
Action: Run,
+
Flags: []cli.Flag{
+
&cli.StringFlag{
+
Name: "user",
+
Usage: "allowed git user",
+
Required: true,
+
},
+
&cli.StringFlag{
+
Name: "git-dir",
+
Usage: "base directory for git repos",
+
Value: "/home/git",
+
},
+
&cli.StringFlag{
+
Name: "log-path",
+
Usage: "path to log file",
+
Value: "/home/git/guard.log",
+
},
+
&cli.StringFlag{
+
Name: "internal-api",
+
Usage: "internal API endpoint",
+
Value: "http://localhost:5444",
+
},
+
},
+
}
+
}
+
+
func Run(ctx context.Context, cmd *cli.Command) error {
+
l := log.FromContext(ctx)
+
+
incomingUser := cmd.String("user")
+
gitDir := cmd.String("git-dir")
+
logPath := cmd.String("log-path")
+
endpoint := cmd.String("internal-api")
+
+
logFile, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+
if err != nil {
+
l.Error("failed to open log file", "error", err)
+
return err
+
} else {
+
fileHandler := slog.NewJSONHandler(logFile, &slog.HandlerOptions{Level: slog.LevelInfo})
+
l = slog.New(fileHandler)
+
}
+
+
var clientIP string
+
if connInfo := os.Getenv("SSH_CONNECTION"); connInfo != "" {
+
parts := strings.Fields(connInfo)
+
if len(parts) > 0 {
+
clientIP = parts[0]
+
}
+
}
+
+
if incomingUser == "" {
+
l.Error("access denied: no user specified")
+
fmt.Fprintln(os.Stderr, "access denied: no user specified")
+
return fmt.Errorf("access denied: no user specified")
+
}
+
+
sshCommand := os.Getenv("SSH_ORIGINAL_COMMAND")
+
+
l.Info("connection attempt",
+
"user", incomingUser,
+
"command", sshCommand,
+
"client", clientIP)
+
+
if sshCommand == "" {
+
l.Error("access denied: no interactive shells", "user", incomingUser)
+
fmt.Fprintln(os.Stderr, "access denied: we don't serve interactive shells :)")
+
return fmt.Errorf("access denied: no interactive shells")
+
}
+
+
cmdParts := strings.Fields(sshCommand)
+
if len(cmdParts) < 2 {
+
l.Error("invalid command format", "command", sshCommand)
+
fmt.Fprintln(os.Stderr, "invalid command format")
+
return fmt.Errorf("invalid command format")
+
}
+
+
gitCommand := cmdParts[0]
+
+
// did:foo/repo-name or
+
// handle/repo-name or
+
// any of the above with a leading slash (/)
+
+
components := strings.Split(strings.TrimPrefix(strings.Trim(cmdParts[1], "'"), "/"), "/")
+
l.Info("command components", "components", components)
+
+
if len(components) != 2 {
+
l.Error("invalid repo format", "components", components)
+
fmt.Fprintln(os.Stderr, "invalid repo format, needs <user>/<repo> or /<user>/<repo>")
+
return fmt.Errorf("invalid repo format, needs <user>/<repo> or /<user>/<repo>")
+
}
+
+
didOrHandle := components[0]
+
did := resolveToDid(ctx, l, didOrHandle)
+
repoName := components[1]
+
qualifiedRepoName, _ := securejoin.SecureJoin(did, repoName)
+
+
validCommands := map[string]bool{
+
"git-receive-pack": true,
+
"git-upload-pack": true,
+
"git-upload-archive": true,
+
}
+
if !validCommands[gitCommand] {
+
l.Error("access denied: invalid git command", "command", gitCommand)
+
fmt.Fprintln(os.Stderr, "access denied: invalid git command")
+
return fmt.Errorf("access denied: invalid git command")
+
}
+
+
if gitCommand != "git-upload-pack" {
+
if !isPushPermitted(l, incomingUser, qualifiedRepoName, endpoint) {
+
l.Error("access denied: user not allowed",
+
"did", incomingUser,
+
"reponame", qualifiedRepoName)
+
fmt.Fprintln(os.Stderr, "access denied: user not allowed")
+
return fmt.Errorf("access denied: user not allowed")
+
}
+
}
+
+
fullPath, _ := securejoin.SecureJoin(gitDir, qualifiedRepoName)
+
+
l.Info("processing command",
+
"user", incomingUser,
+
"command", gitCommand,
+
"repo", repoName,
+
"fullPath", fullPath,
+
"client", clientIP)
+
+
if gitCommand == "git-upload-pack" {
+
fmt.Fprintf(os.Stderr, "\x02%s\n", "Welcome to this knot!")
+
} else {
+
fmt.Fprintf(os.Stderr, "%s\n", "Welcome to this knot!")
+
}
+
+
gitCmd := exec.Command(gitCommand, fullPath)
+
gitCmd.Stdout = os.Stdout
+
gitCmd.Stderr = os.Stderr
+
gitCmd.Stdin = os.Stdin
+
+
if err := gitCmd.Run(); err != nil {
+
l.Error("command failed", "error", err)
+
fmt.Fprintf(os.Stderr, "command failed: %v\n", err)
+
return fmt.Errorf("command failed: %v", err)
+
}
+
+
l.Info("command completed",
+
"user", incomingUser,
+
"command", gitCommand,
+
"repo", repoName,
+
"success", true)
+
+
return nil
+
}
+
+
func resolveToDid(ctx context.Context, l *slog.Logger, didOrHandle string) string {
+
resolver := idresolver.DefaultResolver()
+
ident, err := resolver.ResolveIdent(ctx, didOrHandle)
+
if err != nil {
+
l.Error("Error resolving handle", "error", err, "handle", didOrHandle)
+
fmt.Fprintf(os.Stderr, "error resolving handle: %v\n", err)
+
os.Exit(1)
+
}
+
+
// did:plc:foobarbaz/repo
+
return ident.DID.String()
+
}
+
+
func isPushPermitted(l *slog.Logger, user, qualifiedRepoName, endpoint string) bool {
+
u, _ := url.Parse(endpoint + "/push-allowed")
+
q := u.Query()
+
q.Add("user", user)
+
q.Add("repo", qualifiedRepoName)
+
u.RawQuery = q.Encode()
+
+
req, err := http.Get(u.String())
+
if err != nil {
+
l.Error("Error verifying permissions", "error", err)
+
fmt.Fprintf(os.Stderr, "error verifying permissions: %v\n", err)
+
os.Exit(1)
+
}
+
+
l.Info("Checking push permission",
+
"url", u.String(),
+
"status", req.Status)
+
+
return req.StatusCode == http.StatusNoContent
+
}
+121
keyfetch/keyfetch.go
···
···
+
package keyfetch
+
+
import (
+
"context"
+
"encoding/json"
+
"fmt"
+
"io"
+
"net/http"
+
"os"
+
"strings"
+
+
"github.com/urfave/cli/v3"
+
"tangled.sh/tangled.sh/core/log"
+
)
+
+
func Command() *cli.Command {
+
return &cli.Command{
+
Name: "keys",
+
Usage: "fetch public keys from the knot server",
+
Action: Run,
+
Flags: []cli.Flag{
+
&cli.StringFlag{
+
Name: "output",
+
Aliases: []string{"o"},
+
Usage: "output format (table, json, authorized-keys)",
+
Value: "table",
+
},
+
&cli.StringFlag{
+
Name: "internal-api",
+
Usage: "internal API endpoint",
+
Value: "http://localhost:5444",
+
},
+
&cli.StringFlag{
+
Name: "repoguard-path",
+
Usage: "path to the repoguard binary",
+
Value: "/home/git/repoguard",
+
},
+
&cli.StringFlag{
+
Name: "git-dir",
+
Usage: "base directory for git repos",
+
Value: "/home/git",
+
},
+
&cli.StringFlag{
+
Name: "log-path",
+
Usage: "path to log file",
+
Value: "/home/git/log",
+
},
+
},
+
}
+
}
+
+
func Run(ctx context.Context, cmd *cli.Command) error {
+
internalApi := cmd.String("internal-api")
+
repoguardPath := cmd.String("repoguard-path")
+
gitDir := cmd.String("git-dir")
+
logPath := cmd.String("log-path")
+
output := cmd.String("output")
+
+
l := log.FromContext(ctx)
+
+
resp, err := http.Get(internalApi + "/keys")
+
if err != nil {
+
l.Error("error reaching internal API endpoint; is the knot server running?", "error", err)
+
return err
+
}
+
defer resp.Body.Close()
+
+
body, err := io.ReadAll(resp.Body)
+
if err != nil {
+
l.Error("error reading response body", "error", err)
+
return err
+
}
+
+
var data []map[string]any
+
err = json.Unmarshal(body, &data)
+
if err != nil {
+
l.Error("error unmarshalling response body", "error", err)
+
return err
+
}
+
+
switch output {
+
case "json":
+
prettyJSON, err := json.MarshalIndent(data, "", " ")
+
if err != nil {
+
l.Error("error pretty printing JSON", "error", err)
+
return err
+
}
+
+
if _, err := os.Stdout.Write(prettyJSON); err != nil {
+
l.Error("error writing to stdout", "error", err)
+
return err
+
}
+
case "authorized-keys":
+
formatted := formatKeyData(repoguardPath, gitDir, logPath, internalApi, data)
+
_, err := os.Stdout.Write([]byte(formatted))
+
if err != nil {
+
l.Error("error writing to stdout", "error", err)
+
return err
+
}
+
case "table":
+
fmt.Printf("%-40s %-40s\n", "DID", "KEY")
+
fmt.Println(strings.Repeat("-", 80))
+
+
for _, entry := range data {
+
did, _ := entry["did"].(string)
+
key, _ := entry["key"].(string)
+
fmt.Printf("%-40s %-40s\n", did, key)
+
}
+
}
+
return nil
+
}
+
+
func formatKeyData(repoguardPath, gitDir, logPath, endpoint string, data []map[string]any) string {
+
var result string
+
for _, entry := range data {
+
result += fmt.Sprintf(
+
`command="%s -base-dir %s -user %s -log-path %s -internal-api %s",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty %s`+"\n",
+
repoguardPath, gitDir, entry["did"], logPath, endpoint, entry["key"])
+
}
+
return result
+
}
+84
knotserver/server.go
···
···
+
package knotserver
+
+
import (
+
"context"
+
"fmt"
+
"net/http"
+
+
"github.com/urfave/cli/v3"
+
"tangled.sh/tangled.sh/core/api/tangled"
+
"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/log"
+
"tangled.sh/tangled.sh/core/rbac"
+
)
+
+
func Command() *cli.Command {
+
return &cli.Command{
+
Name: "server",
+
Usage: "run a knot server",
+
Action: Run,
+
Description: `
+
Environment variables:
+
KNOT_SERVER_SECRET (required)
+
KNOT_SERVER_HOSTNAME (required)
+
KNOT_SERVER_LISTEN_ADDR (default: 0.0.0.0:5555)
+
KNOT_SERVER_INTERNAL_LISTEN_ADDR (default: 127.0.0.1:5444)
+
KNOT_SERVER_DB_PATH (default: knotserver.db)
+
KNOT_SERVER_JETSTREAM_ENDPOINT (default: wss://jetstream1.us-west.bsky.network/subscribe)
+
KNOT_SERVER_DEV (default: false)
+
KNOT_REPO_SCAN_PATH (default: /home/git)
+
KNOT_REPO_README (comma-separated list)
+
KNOT_REPO_MAIN_BRANCH (default: main)
+
APPVIEW_ENDPOINT (default: https://tangled.sh)
+
`,
+
}
+
}
+
+
func Run(ctx context.Context, cmd *cli.Command) error {
+
l := log.FromContext(ctx)
+
+
c, err := config.Load(ctx)
+
if err != nil {
+
return fmt.Errorf("failed to load config: %w", err)
+
}
+
+
if c.Server.Dev {
+
l.Info("running in dev mode, signature verification is disabled")
+
}
+
+
db, err := db.Setup(c.Server.DBPath)
+
if err != nil {
+
return fmt.Errorf("failed to load db: %w", err)
+
}
+
+
e, err := rbac.NewEnforcer(c.Server.DBPath)
+
if err != nil {
+
return fmt.Errorf("failed to setup rbac enforcer: %w", err)
+
}
+
+
e.E.EnableAutoSave(true)
+
+
jc, err := jetstream.NewJetstreamClient(c.Server.JetstreamEndpoint, "knotserver", []string{
+
tangled.PublicKeyNSID,
+
tangled.KnotMemberNSID,
+
}, nil, l, db, true)
+
if err != nil {
+
l.Error("failed to setup jetstream", "error", err)
+
}
+
+
mux, err := Setup(ctx, c, db, e, jc, l)
+
if err != nil {
+
return fmt.Errorf("failed to setup server: %w", err)
+
}
+
imux := Internal(ctx, db, e)
+
+
l.Info("starting internal server", "address", c.Server.InternalListenAddr)
+
go http.ListenAndServe(c.Server.InternalListenAddr, imux)
+
+
l.Info("starting main server", "address", c.Server.ListenAddr)
+
l.Error("server error", "error", http.ListenAndServe(c.Server.ListenAddr, mux))
+
+
return nil
+
}