Lindenii Project Forge
Login

server

Lindenii Forge’s main backend daemon
Commit info
ID
6d19e8f3f16744e21de6020a9155c6bb6838d27d
Author
Runxi Yu <me@runxiyu.org>
Author date
Sat, 05 Apr 2025 21:37:17 +0800
Committer
Runxi Yu <me@runxiyu.org>
Committer date
Sat, 05 Apr 2025 21:37:17 +0800
Actions
Add missing copyright headers
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>

package ansiec

var (
	Black   = "\x1b[30m"
	Red     = "\x1b[31m"
	Green   = "\x1b[32m"
	Yellow  = "\x1b[33m"
	Blue    = "\x1b[34m"
	Magenta = "\x1b[35m"
	Cyan    = "\x1b[36m"
	White   = "\x1b[37m"
)

var (
	BrightBlack   = "\x1b[30;1m"
	BrightRed     = "\x1b[31;1m"
	BrightGreen   = "\x1b[32;1m"
	BrightYellow  = "\x1b[33;1m"
	BrightBlue    = "\x1b[34;1m"
	BrightMagenta = "\x1b[35;1m"
	BrightCyan    = "\x1b[36;1m"
	BrightWhite   = "\x1b[37;1m"
)
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>

package ansiec

var Reset = "\x1b[0m"
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>

package ansiec

var (
	Bold      = "\x1b[1m"
	Underline = "\x1b[4m"
	Reversed  = "\x1b[7m"
	Italic    = "\x1b[3m"
)
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>

package git2c

import (
	"fmt"
	"net"

	"git.sr.ht/~sircmpwn/go-bare"
)

type Client struct {
	SocketPath string
	conn       net.Conn
	writer     *bare.Writer
	reader     *bare.Reader
}

func NewClient(socketPath string) (*Client, error) {
	conn, err := net.Dial("unix", socketPath)
	if err != nil {
		return nil, fmt.Errorf("git2d connection failed: %w", err)
	}

	writer := bare.NewWriter(conn)
	reader := bare.NewReader(conn)

	return &Client{
		SocketPath: socketPath,
		conn:       conn,
		writer:     writer,
		reader:     reader,
	}, nil
}

func (c *Client) Close() error {
	if c.conn != nil {
		return c.conn.Close()
	}
	return nil
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>

package git2c

import (
	"encoding/hex"
	"errors"
	"fmt"
	"io"
)

func (c *Client) Cmd1(repoPath string) ([]Commit, *FilenameContents, error) {
	if err := c.writer.WriteData([]byte(repoPath)); err != nil {
		return nil, nil, fmt.Errorf("sending repo path failed: %w", err)
	}
	if err := c.writer.WriteUint(1); err != nil {
		return nil, nil, fmt.Errorf("sending command failed: %w", err)
	}

	status, err := c.reader.ReadUint()
	if err != nil {
		return nil, nil, fmt.Errorf("reading status failed: %w", err)
	}
	if status != 0 {
		return nil, nil, fmt.Errorf("git2d error: %d", status)
	}

	// README
	readmeRaw, err := c.reader.ReadData()
	if err != nil {
		readmeRaw = nil
	}

	readmeFilename := "README.md" // TODO
	readme := &FilenameContents{Filename: readmeFilename, Content: readmeRaw}

	// Commits
	var commits []Commit
	for {
		id, err := c.reader.ReadData()
		if err != nil {
			if errors.Is(err, io.EOF) {
				break
			}
			return nil, nil, fmt.Errorf("reading commit ID failed: %w", err)
		}
		title, _ := c.reader.ReadData()
		authorName, _ := c.reader.ReadData()
		authorEmail, _ := c.reader.ReadData()
		authorDate, _ := c.reader.ReadData()

		commits = append(commits, Commit{
			Hash:    hex.EncodeToString(id),
			Author:  string(authorName),
			Email:   string(authorEmail),
			Date:    string(authorDate),
			Message: string(title),
		})
	}

	return commits, readme, nil
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>

package git2c

import (
	"errors"
	"fmt"
	"io"
)

func (c *Client) Cmd2(repoPath, pathSpec string) ([]TreeEntry, string, error) {
	if err := c.writer.WriteData([]byte(repoPath)); err != nil {
		return nil, "", fmt.Errorf("sending repo path failed: %w", err)
	}
	if err := c.writer.WriteUint(2); err != nil {
		return nil, "", fmt.Errorf("sending command failed: %w", err)
	}
	if err := c.writer.WriteData([]byte(pathSpec)); err != nil {
		return nil, "", fmt.Errorf("sending path failed: %w", err)
	}

	status, err := c.reader.ReadUint()
	if err != nil {
		return nil, "", fmt.Errorf("reading status failed: %w", err)
	}

	switch status {
	case 0:
		kind, err := c.reader.ReadUint()
		if err != nil {
			return nil, "", fmt.Errorf("reading object kind failed: %w", err)
		}

		switch kind {
		case 1:
			// Tree
			count, err := c.reader.ReadUint()
			if err != nil {
				return nil, "", fmt.Errorf("reading entry count failed: %w", err)
			}

			var files []TreeEntry
			for range count {
				typeCode, err := c.reader.ReadUint()
				if err != nil {
					return nil, "", fmt.Errorf("error reading entry type: %w", err)
				}
				mode, err := c.reader.ReadUint()
				if err != nil {
					return nil, "", fmt.Errorf("error reading entry mode: %w", err)
				}
				size, err := c.reader.ReadUint()
				if err != nil {
					return nil, "", fmt.Errorf("error reading entry size: %w", err)
				}
				name, err := c.reader.ReadData()
				if err != nil {
					return nil, "", fmt.Errorf("error reading entry name: %w", err)
				}

				files = append(files, TreeEntry{
					Name:      string(name),
					Mode:      fmt.Sprintf("%06o", mode),
					Size:      size,
					IsFile:    typeCode == 2,
					IsSubtree: typeCode == 1,
				})
			}

			return files, "", nil

		case 2:
			// Blob
			content, err := c.reader.ReadData()
			if err != nil && !errors.Is(err, io.EOF) {
				return nil, "", fmt.Errorf("error reading file content: %w", err)
			}

			return nil, string(content), nil

		default:
			return nil, "", fmt.Errorf("unknown kind: %d", kind)
		}

	case 3:
		return nil, "", fmt.Errorf("path not found: %s", pathSpec)

	default:
		return nil, "", fmt.Errorf("unknown status code: %d", status)
	}
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>

package git2c

type Commit struct {
	Hash    string
	Author  string
	Email   string
	Date    string
	Message string
}

type FilenameContents struct {
	Filename string
	Content  []byte
}

type TreeEntry struct {
	Name      string
	Mode      string
	Size      uint64
	IsFile    bool
	IsSubtree bool
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>

package misc

import "strings"

func FirstOrPanic[T any](v T, err error) T {
	if err != nil {
		panic(err)
	}
	return v
}

// sliceContainsNewlines returns true if and only if the given slice contains
// one or more strings that contains newlines.
func SliceContainsNewlines(s []string) bool {
	for _, v := range s {
		if strings.Contains(v, "\n") {
			return true
		}
	}
	return false
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>

package render

import (
	"bytes"
	"html/template"

	chromaHTML "github.com/alecthomas/chroma/v2/formatters/html"
	chromaLexers "github.com/alecthomas/chroma/v2/lexers"
	chromaStyles "github.com/alecthomas/chroma/v2/styles"
)

func Highlight(filename, content string) template.HTML {
	lexer := chromaLexers.Match(filename)
	if lexer == nil {
		lexer = chromaLexers.Fallback
	}

	iterator, err := lexer.Tokenise(nil, content)
	if err != nil {
		return template.HTML("<pre>Error tokenizing file: " + err.Error() + "</pre>") //#nosec G203`
	}

	var buf bytes.Buffer
	style := chromaStyles.Get("autumn")
	formatter := chromaHTML.New(
		chromaHTML.WithClasses(true),
		chromaHTML.TabWidth(8),
	)

	if err := formatter.Format(&buf, style, iterator); err != nil {
		return template.HTML("<pre>Error formatting file: " + err.Error() + "</pre>") //#nosec G203
	}

	return template.HTML(buf.Bytes()) //#nosec G203
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>

package render

import (
	"html"
	"html/template"
)

// EscapeHTML just escapes a string and wraps it in [template.HTML].
func EscapeHTML(s string) template.HTML {
	return template.HTML(html.EscapeString(s)) //#nosec G203
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>

package forge

import (
	"errors"
	"io/fs"
	"log"
	"log/slog"
	"net"
	"net/http"
	"os"
	"os/exec"
	"syscall"
	"time"

	"github.com/jackc/pgx/v5/pgxpool"
	"go.lindenii.runxiyu.org/lindenii-common/cmap"
	goSSH "golang.org/x/crypto/ssh"
)

type Server struct {
	Config Config

	// Database serves as the primary Database handle for this entire application.
	// Transactions or single reads may be used from it. A [pgxpool.Pool] is
	// necessary to safely use pgx concurrently; pgx.Conn, etc. are insufficient.
	Database *pgxpool.Pool

	SourceHandler http.Handler
	StaticHandler http.Handler

	IrcSendBuffered   chan string
	IrcSendDirectChan chan errorBack[string]

	// GlobalData is passed as "global" when rendering HTML templates.
	GlobalData map[string]any

	ServerPubkeyString string
	ServerPubkeyFP     string
	ServerPubkey       goSSH.PublicKey

	// PackPasses contains hook cookies mapped to their packPass.
	PackPasses cmap.Map[string, packPass]
}

func (s *Server) Setup() {
	s.SourceHandler = http.StripPrefix(
		"/-/source/",
		http.FileServer(http.FS(embeddedSourceFS)),
	)
	staticFS, err := fs.Sub(embeddedResourcesFS, "static")
	if err != nil {
		panic(err)
	}
	s.StaticHandler = http.StripPrefix("/-/static/", http.FileServer(http.FS(staticFS)))
	s.GlobalData = map[string]any{
		"server_public_key_string":      &s.ServerPubkeyString,
		"server_public_key_fingerprint": &s.ServerPubkeyFP,
		"forge_version":                 VERSION,
		// Some other ones are populated after config parsing
	}
}

func (s *Server) Run() {
	if err := s.deployHooks(); err != nil {
		slog.Error("deploying hooks", "error", err)
		os.Exit(1)
	}
	if err := loadTemplates(); err != nil {
		slog.Error("loading templates", "error", err)
		os.Exit(1)
	}
	if err := s.deployGit2D(); err != nil {
		slog.Error("deploying git2d", "error", err)
		os.Exit(1)
	}

	// Launch Git2D
	go func() {
		cmd := exec.Command(s.Config.Git.DaemonPath, s.Config.Git.Socket) //#nosec G204
		cmd.Stderr = log.Writer()
		cmd.Stdout = log.Writer()
		if err := cmd.Run(); err != nil {
			panic(err)
		}
	}()

	// UNIX socket listener for hooks
	{
		hooksListener, err := net.Listen("unix", s.Config.Hooks.Socket)
		if errors.Is(err, syscall.EADDRINUSE) {
			slog.Warn("removing existing socket", "path", s.Config.Hooks.Socket)
			if err = syscall.Unlink(s.Config.Hooks.Socket); err != nil {
				slog.Error("removing existing socket", "path", s.Config.Hooks.Socket, "error", err)
				os.Exit(1)
			}
			if hooksListener, err = net.Listen("unix", s.Config.Hooks.Socket); err != nil {
				slog.Error("listening hooks", "error", err)
				os.Exit(1)
			}
		} else if err != nil {
			slog.Error("listening hooks", "error", err)
			os.Exit(1)
		}
		slog.Info("listening hooks on unix", "path", s.Config.Hooks.Socket)
		go func() {
			if err = s.serveGitHooks(hooksListener); err != nil {
				slog.Error("serving hooks", "error", err)
				os.Exit(1)
			}
		}()
	}

	// UNIX socket listener for LMTP
	{
		lmtpListener, err := net.Listen("unix", s.Config.LMTP.Socket)
		if errors.Is(err, syscall.EADDRINUSE) {
			slog.Warn("removing existing socket", "path", s.Config.LMTP.Socket)
			if err = syscall.Unlink(s.Config.LMTP.Socket); err != nil {
				slog.Error("removing existing socket", "path", s.Config.LMTP.Socket, "error", err)
				os.Exit(1)
			}
			if lmtpListener, err = net.Listen("unix", s.Config.LMTP.Socket); err != nil {
				slog.Error("listening LMTP", "error", err)
				os.Exit(1)
			}
		} else if err != nil {
			slog.Error("listening LMTP", "error", err)
			os.Exit(1)
		}
		slog.Info("listening LMTP on unix", "path", s.Config.LMTP.Socket)
		go func() {
			if err = s.serveLMTP(lmtpListener); err != nil {
				slog.Error("serving LMTP", "error", err)
				os.Exit(1)
			}
		}()
	}

	// SSH listener
	{
		sshListener, err := net.Listen(s.Config.SSH.Net, s.Config.SSH.Addr)
		if errors.Is(err, syscall.EADDRINUSE) && s.Config.SSH.Net == "unix" {
			slog.Warn("removing existing socket", "path", s.Config.SSH.Addr)
			if err = syscall.Unlink(s.Config.SSH.Addr); err != nil {
				slog.Error("removing existing socket", "path", s.Config.SSH.Addr, "error", err)
				os.Exit(1)
			}
			if sshListener, err = net.Listen(s.Config.SSH.Net, s.Config.SSH.Addr); err != nil {
				slog.Error("listening SSH", "error", err)
				os.Exit(1)
			}
		} else if err != nil {
			slog.Error("listening SSH", "error", err)
			os.Exit(1)
		}
		slog.Info("listening SSH on", "net", s.Config.SSH.Net, "addr", s.Config.SSH.Addr)
		go func() {
			if err = s.serveSSH(sshListener); err != nil {
				slog.Error("serving SSH", "error", err)
				os.Exit(1)
			}
		}()
	}

	// HTTP listener
	{
		httpListener, err := net.Listen(s.Config.HTTP.Net, s.Config.HTTP.Addr)
		if errors.Is(err, syscall.EADDRINUSE) && s.Config.HTTP.Net == "unix" {
			slog.Warn("removing existing socket", "path", s.Config.HTTP.Addr)
			if err = syscall.Unlink(s.Config.HTTP.Addr); err != nil {
				slog.Error("removing existing socket", "path", s.Config.HTTP.Addr, "error", err)
				os.Exit(1)
			}
			if httpListener, err = net.Listen(s.Config.HTTP.Net, s.Config.HTTP.Addr); err != nil {
				slog.Error("listening HTTP", "error", err)
				os.Exit(1)
			}
		} else if err != nil {
			slog.Error("listening HTTP", "error", err)
			os.Exit(1)
		}
		server := http.Server{
			Handler:      s,
			ReadTimeout:  time.Duration(s.Config.HTTP.ReadTimeout) * time.Second,
			WriteTimeout: time.Duration(s.Config.HTTP.ReadTimeout) * time.Second,
			IdleTimeout:  time.Duration(s.Config.HTTP.ReadTimeout) * time.Second,
		} //exhaustruct:ignore
		slog.Info("listening HTTP on", "net", s.Config.HTTP.Net, "addr", s.Config.HTTP.Addr)
		go func() {
			if err = server.Serve(httpListener); err != nil && !errors.Is(err, http.ErrServerClosed) {
				slog.Error("serving HTTP", "error", err)
				os.Exit(1)
			}
		}()
	}

	// IRC bot
	go s.ircBotLoop()

	select {}
}