Lindenii Project Forge
Login

server

Lindenii Forge’s main backend daemon
Commit info
ID
a03cf96d53a1b2a80f6a4b43381d62d2b0b38208
Author
Runxi Yu <me@runxiyu.org>
Author date
Sat, 05 Apr 2025 11:27:56 +0800
Committer
Runxi Yu <me@runxiyu.org>
Committer date
Sat, 05 Apr 2025 11:27:56 +0800
Actions
git2d: Factor commands out into their own files
#include "x.h"

int
cmd1(git_repository *repo, struct bare_writer *writer)
{
	/* HEAD tree */
	git_object *obj = NULL;
	int err = git_revparse_single(&obj, repo, "HEAD^{tree}");
	if (err != 0) {
		bare_put_uint(writer, 4);
		return -1;
	}
	git_tree *tree = (git_tree *) obj;

	/* README */

	git_tree_entry *entry = NULL;
	err = git_tree_entry_bypath(&entry, tree, "README.md");
	if (err != 0) {
		bare_put_uint(writer, 5);
		git_tree_free(tree);
		return -1;
	}
	git_otype objtype = git_tree_entry_type(entry);
	if (objtype != GIT_OBJECT_BLOB) {
		bare_put_uint(writer, 6);
		git_tree_entry_free(entry);
		git_tree_free(tree);
		return -1;
	}
	git_object *obj2 = NULL;
	err = git_tree_entry_to_object(&obj2, repo, entry);
	if (err != 0) {
		bare_put_uint(writer, 7);
		git_tree_entry_free(entry);
		git_tree_free(tree);
		return -1;
	}
	git_blob *blob = (git_blob *) obj2;
	const void *content = git_blob_rawcontent(blob);
	if (content == NULL) {
		bare_put_uint(writer, 8);
		git_blob_free(blob);
		git_tree_entry_free(entry);
		git_tree_free(tree);
		return -1;
	}
	bare_put_uint(writer, 0);
	bare_put_data(writer, content, git_blob_rawsize(blob));

	/* Commits */

	git_revwalk *walker = NULL;
	if (git_revwalk_new(&walker, repo) != 0) {
		bare_put_uint(writer, 9);
		git_blob_free(blob);
		git_tree_entry_free(entry);
		git_tree_free(tree);
		return -1;
	}

	if (git_revwalk_push_head(walker) != 0) {
		bare_put_uint(writer, 9);
		git_revwalk_free(walker);
		git_blob_free(blob);
		git_tree_entry_free(entry);
		git_tree_free(tree);
		return -1;
	}

	int count = 0;
	git_oid oid;
	while (count < 3 && git_revwalk_next(&oid, walker) == 0) {
		git_commit *commit = NULL;
		if (git_commit_lookup(&commit, repo, &oid) != 0)
			break;

		const char *msg = git_commit_summary(commit);
		const git_signature *author = git_commit_author(commit);

		/* ID */
		bare_put_data(writer, oid.id, GIT_OID_RAWSZ);

		/* Title */
		size_t msg_len = msg ? strlen(msg) : 0;
		bare_put_data(writer, (const uint8_t *)(msg ? msg : ""), msg_len);

		/* Author's name */
		const char *author_name = author ? author->name : "";
		bare_put_data(writer, (const uint8_t *)author_name, strlen(author_name));

		/* Author's email */
		const char *author_email = author ? author->email : "";
		bare_put_data(writer, (const uint8_t *)author_email, strlen(author_email));

		/* Author's date */
		/* TODO: Pass the integer instead of a string */
		time_t time = git_commit_time(commit);
		char timebuf[64];
		struct tm *tm = localtime(&time);
		if (tm)
			strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S", tm);
		else
			strcpy(timebuf, "unknown");
		bare_put_data(writer, (const uint8_t *)timebuf, strlen(timebuf));

		git_commit_free(commit);
		count++;
	}

	git_revwalk_free(walker);
	git_blob_free(blob);
	git_tree_entry_free(entry);
	git_tree_free(tree);

	return 0;
}
/*-
 * SPDX-License-Identifier: AGPL-3.0-only
 * SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
 */

#include "x.h"

void *
session(void *_conn)
{
	int conn = *(int *)_conn;
	free((int *)_conn);

	int err;

	conn_io_t io = {.fd = conn };
	struct bare_reader reader = {
		.buffer = &io,
		.read = conn_read,
	};
	struct bare_writer writer = {
		.buffer = &io,
		.write = conn_write,
	};

	/* Repo path */
	char path[4096] = {0};
	err = bare_get_data(&reader, (uint8_t *) path, sizeof(path) - 1);
	if (err != BARE_ERROR_NONE) {
		goto close;
	}
	path[sizeof(path) - 1] = '\0';

	/* Open repo */
	git_repository *repo = NULL;
	err = git_repository_open_ext(&repo, path, GIT_REPOSITORY_OPEN_NO_SEARCH | GIT_REPOSITORY_OPEN_BARE | GIT_REPOSITORY_OPEN_NO_DOTGIT, NULL);
	if (err != 0) {
		bare_put_uint(&writer, 1);
		goto close;
	}

	/* Command */
	uint64_t cmd = 0;
	err = bare_get_uint(&reader, &cmd);
	if (err != BARE_ERROR_NONE) {
		bare_put_uint(&writer, 2);
		goto free_repo;
	}
	switch (cmd) {
	case 1:
		err = cmd1(repo, &writer);
		if (err != 0)
			goto free_repo;
		break;
	case 0:
		bare_put_uint(&writer, 3);
		goto free_repo;
	default:
		bare_put_uint(&writer, 3);
		goto free_repo;
	}

	/* HEAD tree */
	git_object *obj = NULL;
	err = git_revparse_single(&obj, repo, "HEAD^{tree}");
	if (err != 0) {
		bare_put_uint(&writer, 4);
		goto free_repo;
	}
	git_tree *tree = (git_tree *) obj;

	/* README */

	git_tree_entry *entry = NULL;
	err = git_tree_entry_bypath(&entry, tree, "README.md");
	if (err != 0) {
		bare_put_uint(&writer, 5);
		goto free_tree;
	}
	git_otype objtype = git_tree_entry_type(entry);
	if (objtype != GIT_OBJECT_BLOB) {
		bare_put_uint(&writer, 6);
		goto free_tree_entry;
	}
	git_object *obj2 = NULL;
	err = git_tree_entry_to_object(&obj2, repo, entry);
	if (err != 0) {
		bare_put_uint(&writer, 7);
		goto free_tree_entry;
	}
	git_blob *blob = (git_blob *) obj2;
	const void *content = git_blob_rawcontent(blob);
	if (content == NULL) {
		bare_put_uint(&writer, 8);
		goto free_blob;
	}
	bare_put_uint(&writer, 0);
	bare_put_data(&writer, content, git_blob_rawsize(blob));

	/* Commits */

	git_revwalk *walker = NULL;
	if (git_revwalk_new(&walker, repo) != 0) {
		bare_put_uint(&writer, 9);
		goto free_blob;
	}

	if (git_revwalk_push_head(walker) != 0) {
		bare_put_uint(&writer, 9);
		goto free_blob;
	}

	int count = 0;
	git_oid oid;
	while (count < 3 && git_revwalk_next(&oid, walker) == 0) {
		git_commit *commit = NULL;
		if (git_commit_lookup(&commit, repo, &oid) != 0)
			break;

		const char *msg = git_commit_summary(commit);
		const git_signature *author = git_commit_author(commit);

		/* ID */
		bare_put_data(&writer, oid.id, GIT_OID_RAWSZ);

		/* Title */
		size_t msg_len = msg ? strlen(msg) : 0;
		bare_put_data(&writer, (const uint8_t *)(msg ? msg : ""), msg_len);

		/* Author's name */
		const char *author_name = author ? author->name : "";
		bare_put_data(&writer, (const uint8_t *)author_name, strlen(author_name));

		/* Author's email */
		const char *author_email = author ? author->email : "";
		bare_put_data(&writer, (const uint8_t *)author_email, strlen(author_email));

		/* Author's date */
		/* TODO: Pass the integer instead of a string */
		time_t time = git_commit_time(commit);
		char timebuf[64];
		struct tm *tm = localtime(&time);
		if (tm)
			strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S", tm);
		else
			strcpy(timebuf, "unknown");
		bare_put_data(&writer, (const uint8_t *)timebuf, strlen(timebuf));

		git_commit_free(commit);
		count++;
	}

	git_revwalk_free(walker);

free_blob:
	git_blob_free(blob);
free_tree_entry:
	git_tree_entry_free(entry);
free_tree:
	git_tree_free(tree);
free_repo:
	git_repository_free(repo);

close:
	close(conn);

	return NULL;
}

/*-
 * SPDX-License-Identifier: AGPL-3.0-only
 * SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
 */

#include <err.h>
#include <errno.h>
#include <git2.h>
#include <pthread.h>
#include <signal.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/un.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "bare.h"

#ifndef X_H
#define X_H

typedef struct {
	int fd;
} conn_io_t;


bare_error conn_read(void *buffer, void *dst, uint64_t sz);
bare_error conn_write(void *buffer, const void *src, uint64_t sz);

void * session(void *_conn);

int cmd1(git_repository *repo, struct bare_writer *writer);

#endif // X_H
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>

package main

import (
	"errors"
	"flag"
	"log"
	"net"
	"net/http"
	"os/exec"
	"syscall"
	"time"

	"go.lindenii.runxiyu.org/lindenii-common/clog"
)

func main() {
	configPath := flag.String(
		"config",
		"/etc/lindenii/forge.scfg",
		"path to configuration file",
	)
	flag.Parse()

	if err := loadConfig(*configPath); err != nil {
		clog.Fatal(1, "Loading configuration: "+err.Error())
	}
	if err := deployHooks(); err != nil {
		clog.Fatal(1, "Deploying hooks to filesystem: "+err.Error())
	}
	if err := loadTemplates(); err != nil {
		clog.Fatal(1, "Loading templates: "+err.Error())
	}
	if err := deployGit2D(); err != nil {
		clog.Fatal(1, "Deploying git2d: "+err.Error())
	}

	// Launch Git2D
	go func() {
		cmd := exec.Command(config.Git.DaemonPath, 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", config.Hooks.Socket)
		if errors.Is(err, syscall.EADDRINUSE) {
			clog.Warn("Removing existing socket " + config.Hooks.Socket)
			if err = syscall.Unlink(config.Hooks.Socket); err != nil {
				clog.Fatal(1, "Removing existing socket: "+err.Error())
			}
			if hooksListener, err = net.Listen("unix", config.Hooks.Socket); err != nil {
				clog.Fatal(1, "Listening hooks: "+err.Error())
			}
		} else if err != nil {
			clog.Fatal(1, "Listening hooks: "+err.Error())
		}
		clog.Info("Listening hooks on unix " + config.Hooks.Socket)
		go func() {
			if err = serveGitHooks(hooksListener); err != nil {
				clog.Fatal(1, "Serving hooks: "+err.Error())
			}
		}()
	}

	// UNIX socket listener for LMTP
	{
		lmtpListener, err := net.Listen("unix", config.LMTP.Socket)
		if errors.Is(err, syscall.EADDRINUSE) {
			clog.Warn("Removing existing socket " + config.LMTP.Socket)
			if err = syscall.Unlink(config.LMTP.Socket); err != nil {
				clog.Fatal(1, "Removing existing socket: "+err.Error())
			}
			if lmtpListener, err = net.Listen("unix", config.LMTP.Socket); err != nil {
				clog.Fatal(1, "Listening LMTP: "+err.Error())
			}
		} else if err != nil {
			clog.Fatal(1, "Listening LMTP: "+err.Error())
		}
		clog.Info("Listening LMTP on unix " + config.LMTP.Socket)
		go func() {
			if err = serveLMTP(lmtpListener); err != nil {
				clog.Fatal(1, "Serving LMTP: "+err.Error())
			}
		}()
	}

	// SSH listener
	{
		sshListener, err := net.Listen(config.SSH.Net, config.SSH.Addr)
		if errors.Is(err, syscall.EADDRINUSE) && config.SSH.Net == "unix" {
			clog.Warn("Removing existing socket " + config.SSH.Addr)
			if err = syscall.Unlink(config.SSH.Addr); err != nil {
				clog.Fatal(1, "Removing existing socket: "+err.Error())
			}
			if sshListener, err = net.Listen(config.SSH.Net, config.SSH.Addr); err != nil {
				clog.Fatal(1, "Listening SSH: "+err.Error())
			}
		} else if err != nil {
			clog.Fatal(1, "Listening SSH: "+err.Error())
		}
		clog.Info("Listening SSH on " + config.SSH.Net + " " + config.SSH.Addr)
		go func() {
			if err = serveSSH(sshListener); err != nil {
				clog.Fatal(1, "Serving SSH: "+err.Error())
			}
		}()
	}

	// HTTP listener
	{
		httpListener, err := net.Listen(config.HTTP.Net, config.HTTP.Addr)
		if errors.Is(err, syscall.EADDRINUSE) && config.HTTP.Net == "unix" {
			clog.Warn("Removing existing socket " + config.HTTP.Addr)
			if err = syscall.Unlink(config.HTTP.Addr); err != nil {
				clog.Fatal(1, "Removing existing socket: "+err.Error())
			}
			if httpListener, err = net.Listen(config.HTTP.Net, config.HTTP.Addr); err != nil {
				clog.Fatal(1, "Listening HTTP: "+err.Error())
			}
		} else if err != nil {
			clog.Fatal(1, "Listening HTTP: "+err.Error())
		}
		server := http.Server{
			Handler:      &forgeHTTPRouter{},
			ReadTimeout:  time.Duration(config.HTTP.ReadTimeout) * time.Second,
			WriteTimeout: time.Duration(config.HTTP.ReadTimeout) * time.Second,
			IdleTimeout:  time.Duration(config.HTTP.ReadTimeout) * time.Second,
		} //exhaustruct:ignore
		clog.Info("Listening HTTP on " + config.HTTP.Net + " " + config.HTTP.Addr)
		go func() {
			if err = server.Serve(httpListener); err != nil && !errors.Is(err, http.ErrServerClosed) {
				clog.Fatal(1, "Serving HTTP: "+err.Error())
			}
		}()
	}

	// IRC bot
	go ircBotLoop()

	select {}
}