Hi… I am well aware that this diff view is very suboptimal. It will be fixed when the refactored server comes along!
*: Replacing more := with var
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileContributor: Runxi Yu <https://runxiyu.org>
package main
import (
"bytes"
"html"
"html/template"
"strings"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/microcosm-cc/bluemonday"
"github.com/niklasfasching/go-org/org"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
)
var markdown_converter = goldmark.New(goldmark.WithExtensions(extension.GFM))
func render_readme_at_tree(tree *object.Tree) (readme_filename string, readme_content template.HTML) {
var readme_rendered_unsafe bytes.Buffer
var readme_file *object.File var readme_file_contents string var err error
readme_file, err := tree.File("README")
if err == nil {
readme_file_contents, err := readme_file.Contents()
if err != nil {
if readme_file, err = tree.File("README"); err != nil {
if readme_file_contents, err = readme_file.Contents(); err != nil {
return "Error fetching README", string_escape_html("Unable to fetch contents of README: " + err.Error())
}
return "README", template.HTML("<pre>" + html.EscapeString(readme_file_contents) + "</pre>")
}
readme_file, err = tree.File("README.md")
if err == nil {
readme_file_contents, err := readme_file.Contents()
if err != nil {
if readme_file, err = tree.File("README.md"); err != nil {
if readme_file_contents, err = readme_file.Contents(); err != nil {
return "Error fetching README", string_escape_html("Unable to fetch contents of README: " + err.Error())
}
err = markdown_converter.Convert([]byte(readme_file_contents), &readme_rendered_unsafe)
if err != nil {
if err = markdown_converter.Convert([]byte(readme_file_contents), &readme_rendered_unsafe); err != nil {
return "Error fetching README", string_escape_html("Unable to render README: " + err.Error())
}
return "README.md", template.HTML(bluemonday.UGCPolicy().SanitizeBytes(readme_rendered_unsafe.Bytes())) }
readme_file, err = tree.File("README.org")
if err == nil {
readme_file_contents, err := readme_file.Contents()
if err != nil {
if readme_file, err = tree.File("README.org"); err != nil {
if readme_file_contents, err = readme_file.Contents(); err != nil {
return "Error fetching README", string_escape_html("Unable to fetch contents of README: " + err.Error())
}
org_html, err := org.New().Parse(strings.NewReader(readme_file_contents), readme_filename).Write(org.NewHTMLWriter())
if err != nil {
return "Error fetching README", string_escape_html("Unable to render README: " + err.Error())
}
return "README.org", template.HTML(bluemonday.UGCPolicy().Sanitize(org_html))
}
return "", ""
}
func string_escape_html(s string) template.HTML {
return template.HTML(html.EscapeString(s))
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileContributor: Runxi Yu <https://runxiyu.org>
package main
import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/jackc/pgx/v5"
"go.lindenii.runxiyu.org/lindenii-common/clog"
)
type http_router_t struct{}
func (router *http_router_t) ServeHTTP(w http.ResponseWriter, r *http.Request) {
clog.Info("Incoming HTTP: " + r.RemoteAddr + " " + r.Method + " " + r.RequestURI)
segments, _, err := parse_request_uri(r.RequestURI)
if err != nil {
var segments []string
var err error
var non_empty_last_segments_len int
var params map[string]any
var separator_index int
if segments, _, err = parse_request_uri(r.RequestURI); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) return }
non_empty_last_segments_len := len(segments)
non_empty_last_segments_len = len(segments)
if segments[len(segments)-1] == "" {
non_empty_last_segments_len--
}
if segments[0] == ":" {
if len(segments) < 2 {
http.Error(w, "Blank system endpoint", http.StatusNotFound)
return
} else if len(segments) == 2 && redirect_with_slash(w, r) {
return
}
switch segments[1] {
case "static":
static_handler.ServeHTTP(w, r)
return
case "source":
source_handler.ServeHTTP(w, r)
return
}
}
params := make(map[string]any)
params["url_segments"] = segments
params["global"] = global_data
var _user_id int // 0 for none
_user_id, params["username"], err = get_user_info_from_request(r)
if errors.Is(err, http.ErrNoCookie) {
} else if errors.Is(err, pgx.ErrNoRows) {
} else if err != nil {
http.Error(w, "Error getting user info from request: "+err.Error(), http.StatusInternalServerError)
return
}
if _user_id == 0 {
params["user_id"] = ""
} else {
params["user_id"] = strconv.Itoa(_user_id)
}
if segments[0] == ":" {
switch segments[1] {
case "login":
handle_login(w, r, params)
return
case "users":
handle_users(w, r, params)
return
default:
http.Error(w, fmt.Sprintf("Unknown system module type: %s", segments[1]), http.StatusNotFound)
return
}
}
separator_index := -1
separator_index = -1
for i, part := range segments {
if part == ":" {
separator_index = i
break
}
}
params["separator_index"] = separator_index
// TODO
if separator_index > 1 {
http.Error(w, "Subgroups haven't been implemented yet", http.StatusNotImplemented)
return
}
var module_type string var module_name string var group_name string
switch {
case non_empty_last_segments_len == 0:
handle_index(w, r, params)
case separator_index == -1:
http.Error(w, "Group indexing hasn't been implemented yet", http.StatusNotImplemented)
case non_empty_last_segments_len == separator_index+1:
http.Error(w, "Group root hasn't been implemented yet", http.StatusNotImplemented)
case non_empty_last_segments_len == separator_index+2:
if redirect_with_slash(w, r) {
return
}
module_type := segments[separator_index+1]
module_type = segments[separator_index+1]
params["group_name"] = segments[0]
switch module_type {
case "repos":
handle_group_repos(w, r, params)
default:
http.Error(w, fmt.Sprintf("Unknown module type: %s", module_type), http.StatusNotFound)
}
default:
module_type := segments[separator_index+1] module_name := segments[separator_index+2] group_name := segments[0]
module_type = segments[separator_index+1] module_name = segments[separator_index+2] group_name = segments[0]
params["group_name"] = group_name
switch module_type {
case "repos":
params["repo_name"] = module_name
if non_empty_last_segments_len > separator_index+3 {
switch segments[separator_index+3] {
case "info":
err = handle_repo_info(w, r, params)
if err != nil {
if err = handle_repo_info(w, r, params); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) } return case "git-upload-pack":
err = handle_upload_pack(w, r, params)
if err != nil {
if err = handle_upload_pack(w, r, params); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) } return } }
params["ref_type"], params["ref_name"], err = get_param_ref_and_type(r)
if err != nil {
if params["ref_type"], params["ref_name"], err = get_param_ref_and_type(r); err != nil {
if errors.Is(err, err_no_ref_spec) {
params["ref_type"] = ""
} else {
http.Error(w, "Error querying ref type: "+err.Error(), http.StatusInternalServerError)
return
}
}
// TODO: subgroups
params["repo"], params["repo_description"], params["repo_id"], err = open_git_repo(r.Context(), group_name, module_name)
if err != nil {
if params["repo"], params["repo_description"], params["repo_id"], err = open_git_repo(r.Context(), group_name, module_name); err != nil {
http.Error(w, "Error opening repo: "+err.Error(), http.StatusInternalServerError)
return
}
if non_empty_last_segments_len == separator_index+3 {
if redirect_with_slash(w, r) {
return
}
handle_repo_index(w, r, params)
return
}
repo_feature := segments[separator_index+3]
switch repo_feature {
case "tree":
params["rest"] = strings.Join(segments[separator_index+4:], "/")
if len(segments) < separator_index+5 && redirect_with_slash(w, r) {
return
}
handle_repo_tree(w, r, params)
case "raw":
params["rest"] = strings.Join(segments[separator_index+4:], "/")
if len(segments) < separator_index+5 && redirect_with_slash(w, r) {
return
}
handle_repo_raw(w, r, params)
case "log":
if non_empty_last_segments_len > separator_index+4 {
http.Error(w, "Too many parameters", http.StatusBadRequest)
return
}
if redirect_with_slash(w, r) {
return
}
handle_repo_log(w, r, params)
case "commit":
if redirect_without_slash(w, r) {
return
}
params["commit_id"] = segments[separator_index+4]
handle_repo_commit(w, r, params)
case "contrib":
if redirect_with_slash(w, r) {
return
}
switch non_empty_last_segments_len {
case separator_index + 4:
handle_repo_contrib_index(w, r, params)
case separator_index + 5:
params["mr_id"] = segments[separator_index+4]
handle_repo_contrib_one(w, r, params)
default:
http.Error(w, "Too many parameters", http.StatusBadRequest)
}
default:
http.Error(w, fmt.Sprintf("Unknown repo feature: %s", repo_feature), http.StatusNotFound)
}
default:
http.Error(w, fmt.Sprintf("Unknown module type: %s", module_type), http.StatusNotFound)
}
}
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileContributor: Runxi Yu <https://runxiyu.org>
package main
import "net/http"
// render_template abstracts out the annoyances of reporting template rendering
// errors.
func render_template(w http.ResponseWriter, template_name string, params map[string]any) {
err := templates.ExecuteTemplate(w, template_name, params)
if err != nil {
if err := templates.ExecuteTemplate(w, template_name, params); err != nil {
http.Error(w, "Error rendering template: "+err.Error(), http.StatusInternalServerError) } }
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileContributor: Runxi Yu <https://runxiyu.org>
package main
import (
"errors"
"flag"
"net"
"net/http"
"syscall"
"go.lindenii.runxiyu.org/lindenii-common/clog"
)
func main() {
config_path := flag.String(
"config",
"/etc/lindenii/forge.scfg",
"path to configuration file",
)
flag.Parse()
if err := load_config(*config_path); err != nil {
clog.Fatal(1, "Loading configuration: "+err.Error())
}
if err := deploy_hooks_to_filesystem(); err != nil {
clog.Fatal(1, "Deploying hooks to filesystem: "+err.Error())
}
if err := load_templates(); err != nil {
clog.Fatal(1, "Loading templates: "+err.Error())
}
// UNIX socket listener for hooks
var hooks_listener net.Listener
var err error
hooks_listener, err = net.Listen("unix", config.Hooks.Socket)
if errors.Is(err, syscall.EADDRINUSE) {
clog.Warn("Removing stale socket " + config.Hooks.Socket)
if err := syscall.Unlink(config.Hooks.Socket); err != nil {
if err = syscall.Unlink(config.Hooks.Socket); err != nil {
clog.Fatal(1, "Removing stale socket: "+err.Error()) }
hooks_listener, err = net.Listen("unix", config.Hooks.Socket)
if err != nil {
if hooks_listener, 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 := serve_git_hooks(hooks_listener); err != nil {
if err = serve_git_hooks(hooks_listener); err != nil {
clog.Fatal(1, "Serving hooks: "+err.Error())
}
}()
// SSH listener
ssh_listener, err := net.Listen(config.SSH.Net, config.SSH.Addr)
if errors.Is(err, syscall.EADDRINUSE) && config.SSH.Net == "unix" {
clog.Warn("Removing stale socket " + config.SSH.Addr)
if err := syscall.Unlink(config.SSH.Addr); err != nil {
if err = syscall.Unlink(config.SSH.Addr); err != nil {
clog.Fatal(1, "Removing stale socket: "+err.Error()) }
ssh_listener, err = net.Listen(config.SSH.Net, config.SSH.Addr)
if err != nil {
if ssh_listener, 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 := serve_ssh(ssh_listener); err != nil {
if err = serve_ssh(ssh_listener); err != nil {
clog.Fatal(1, "Serving SSH: "+err.Error())
}
}()
// HTTP listener
http_listener, err := net.Listen(config.HTTP.Net, config.HTTP.Addr)
if errors.Is(err, syscall.EADDRINUSE) && config.HTTP.Net == "unix" {
clog.Warn("Removing stale socket " + config.HTTP.Addr)
if err := syscall.Unlink(config.HTTP.Addr); err != nil {
if err = syscall.Unlink(config.HTTP.Addr); err != nil {
clog.Fatal(1, "Removing stale socket: "+err.Error()) }
http_listener, err = net.Listen(config.HTTP.Net, config.HTTP.Addr)
if err != nil {
if http_listener, 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())
}
clog.Info("Listening HTTP on " + config.HTTP.Net + " " + config.HTTP.Addr)
go func() {
if err := http.Serve(http_listener, &http_router_t{}); err != nil {
if err = http.Serve(http_listener, &http_router_t{}); err != nil {
clog.Fatal(1, "Serving HTTP: "+err.Error())
}
}()
select {}
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileContributor: Runxi Yu <https://runxiyu.org>
package main
import (
"embed"
"html/template"
"io/fs"
"net/http"
)
// We embed all source for easy AGPL compliance.
//
//go:embed .gitignore .gitattributes
//go:embed LICENSE README.md
//go:embed *.go go.mod go.sum
//go:embed *.scfg
//go:embed Makefile
//go:embed schema.sql
//go:embed static/* templates/*
//go:embed git_hooks_client/*.c
//go:embed vendor/*
var source_fs embed.FS
var source_handler = http.StripPrefix(
"/:/source/",
http.FileServer(http.FS(source_fs)),
)
//go:embed templates/* static/* git_hooks_client/git_hooks_client
var resources_fs embed.FS
var templates *template.Template
func load_templates() (err error) {
templates, err = template.New("templates").Funcs(template.FuncMap{
"first_line": first_line,
"base_name": base_name,
}).ParseFS(resources_fs, "templates/*")
return err
}
var static_handler http.Handler
func init() {
static_fs, err := fs.Sub(resources_fs, "static")
if err != nil {
if static_fs, err := fs.Sub(resources_fs, "static"); err != nil {
panic(err)
}
static_handler = http.StripPrefix("/:/static/", http.FileServer(http.FS(static_fs)))
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileContributor: Runxi Yu <https://runxiyu.org>
package main
import (
"errors"
"fmt"
"os"
"os/exec"
glider_ssh "github.com/gliderlabs/ssh"
"github.com/go-git/go-git/v5"
"go.lindenii.runxiyu.org/lindenii-common/cmap"
)
type pack_to_hook_t struct {
session glider_ssh.Session
repo *git.Repository
pubkey string
direct_access bool
repo_path string
user_id int
repo_id int
group_name string
repo_name string
}
var pack_to_hook_by_cookie = cmap.Map[string, pack_to_hook_t]{}
// ssh_handle_receive_pack handles attempts to push to repos.
func ssh_handle_receive_pack(session glider_ssh.Session, pubkey string, repo_identifier string) (err error) {
group_name, repo_name, repo_id, repo_path, direct_access, contrib_requirements, user_type, user_id, err := get_repo_path_perms_from_ssh_path_pubkey(session.Context(), repo_identifier, pubkey)
if err != nil {
return err
}
repo, err := git.PlainOpen(repo_path)
if err != nil {
return err
}
repo_config, err := repo.Config()
if err != nil {
return err
}
repo_config_core := repo_config.Raw.Section("core")
if repo_config_core == nil {
return errors.New("Repository has no core section in config")
}
hooksPath := repo_config_core.OptionAll("hooksPath")
if len(hooksPath) != 1 || hooksPath[0] != config.Hooks.Execs {
return errors.New("Repository has hooksPath set to an unexpected value")
}
if !direct_access {
switch contrib_requirements {
case "closed":
if !direct_access {
return errors.New("You need direct access to push to this repo.")
}
case "registered_user":
if user_type != "registered" {
return errors.New("You need to be a registered user to push to this repo.")
}
case "ssh_pubkey":
if pubkey == "" {
return errors.New("You need to have an SSH public key to push to this repo.")
}
if user_type == "" {
user_id, err = add_user_ssh(session.Context(), pubkey)
if err != nil {
return err
}
fmt.Fprintln(session.Stderr(), "You are now registered as user ID", user_id)
}
case "public":
default:
panic("unknown contrib_requirements value " + contrib_requirements)
}
}
cookie, err := random_urlsafe_string(16)
if err != nil {
fmt.Fprintln(session.Stderr(), "Error while generating cookie:", err)
}
fmt.Println(group_name, repo_name)
pack_to_hook_by_cookie.Store(cookie, pack_to_hook_t{
session: session,
pubkey: pubkey,
direct_access: direct_access,
repo_path: repo_path,
user_id: user_id,
repo_id: repo_id,
group_name: group_name,
repo_name: repo_name,
repo: repo,
})
defer pack_to_hook_by_cookie.Delete(cookie)
// The Delete won't execute until proc.Wait returns unless something
// horribly wrong such as a panic occurs.
proc := exec.CommandContext(session.Context(), "git-receive-pack", repo_path)
proc.Env = append(os.Environ(),
"LINDENII_FORGE_HOOKS_SOCKET_PATH="+config.Hooks.Socket,
"LINDENII_FORGE_HOOKS_COOKIE="+cookie,
)
proc.Stdin = session
proc.Stdout = session
proc.Stderr = session.Stderr()
err = proc.Start()
if err != nil {
if err = proc.Start(); err != nil {
fmt.Fprintln(session.Stderr(), "Error while starting process:", err)
return err
}
err = proc.Wait()
if exitError, ok := err.(*exec.ExitError); ok {
fmt.Fprintln(session.Stderr(), "Process exited with error", exitError.ExitCode())
} else if err != nil {
fmt.Fprintln(session.Stderr(), "Error while waiting for process:", err)
}
return err
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileContributor: Runxi Yu <https://runxiyu.org>
package main
import (
"fmt"
"os"
"os/exec"
glider_ssh "github.com/gliderlabs/ssh"
)
// ssh_handle_upload_pack handles clones/fetches. It just uses git-upload-pack
// and has no ACL checks.
func ssh_handle_upload_pack(session glider_ssh.Session, pubkey string, repo_identifier string) (err error) {
_, _, _, repo_path, _, _, _, _, err := get_repo_path_perms_from_ssh_path_pubkey(session.Context(), repo_identifier, pubkey)
if err != nil {
var repo_path string
if _, _, _, repo_path, _, _, _, _, err = get_repo_path_perms_from_ssh_path_pubkey(session.Context(), repo_identifier, pubkey); err != nil {
return err } proc := exec.CommandContext(session.Context(), "git-upload-pack", repo_path) proc.Env = append(os.Environ(), "LINDENII_FORGE_HOOKS_SOCKET_PATH="+config.Hooks.Socket) proc.Stdin = session proc.Stdout = session proc.Stderr = session.Stderr()
err = proc.Start()
if err != nil {
if err = proc.Start(); err != nil {
fmt.Fprintln(session.Stderr(), "Error while starting process:", err)
return err
}
err = proc.Wait()
if exitError, ok := err.(*exec.ExitError); ok {
fmt.Fprintln(session.Stderr(), "Process exited with error", exitError.ExitCode())
} else if err != nil {
fmt.Fprintln(session.Stderr(), "Error while waiting for process:", err)
}
return err
}
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileContributor: Runxi Yu <https://runxiyu.org>
package main
import (
"fmt"
"net"
"os"
"strings"
glider_ssh "github.com/gliderlabs/ssh"
"go.lindenii.runxiyu.org/lindenii-common/ansiec"
"go.lindenii.runxiyu.org/lindenii-common/clog"
go_ssh "golang.org/x/crypto/ssh"
)
var (
server_public_key_string string
server_public_key_fingerprint string
server_public_key go_ssh.PublicKey
)
func serve_ssh(listener net.Listener) error {
host_key_bytes, err := os.ReadFile(config.SSH.Key)
if err != nil {
var host_key_bytes []byte
var host_key go_ssh.Signer
var err error
var server *glider_ssh.Server
if host_key_bytes, err = os.ReadFile(config.SSH.Key); err != nil {
return err }
host_key, err := go_ssh.ParsePrivateKey(host_key_bytes)
if err != nil {
if host_key, err = go_ssh.ParsePrivateKey(host_key_bytes); err != nil {
return err } server_public_key = host_key.PublicKey() server_public_key_string = string(go_ssh.MarshalAuthorizedKey(server_public_key)) server_public_key_fingerprint = string(go_ssh.FingerprintSHA256(server_public_key))
server := &glider_ssh.Server{
server = &glider_ssh.Server{
Handler: func(session glider_ssh.Session) {
client_public_key := session.PublicKey()
var client_public_key_string string
if client_public_key != nil {
client_public_key_string = strings.TrimSuffix(string(go_ssh.MarshalAuthorizedKey(client_public_key)), "\n")
}
clog.Info("Incoming SSH: " + session.RemoteAddr().String() + " " + client_public_key_string + " " + session.RawCommand())
fmt.Fprintln(session.Stderr(), ansiec.Blue+"Lindenii Forge "+VERSION+", source at "+strings.TrimSuffix(config.HTTP.Root, "/")+"/:/source/"+ansiec.Reset+"\r")
cmd := session.Command()
if len(cmd) < 2 {
fmt.Fprintln(session.Stderr(), "Insufficient arguments\r")
return
}
switch cmd[0] {
case "git-upload-pack":
if len(cmd) > 2 {
fmt.Fprintln(session.Stderr(), "Too many arguments\r")
return
}
err = ssh_handle_upload_pack(session, client_public_key_string, cmd[1])
case "git-receive-pack":
if len(cmd) > 2 {
fmt.Fprintln(session.Stderr(), "Too many arguments\r")
return
}
err = ssh_handle_receive_pack(session, client_public_key_string, cmd[1])
default:
fmt.Fprintln(session.Stderr(), "Unsupported command: "+cmd[0]+"\r")
return
}
if err != nil {
fmt.Fprintln(session.Stderr(), err.Error())
return
}
},
PublicKeyHandler: func(ctx glider_ssh.Context, key glider_ssh.PublicKey) bool { return true },
KeyboardInteractiveHandler: func(ctx glider_ssh.Context, challenge go_ssh.KeyboardInteractiveChallenge) bool { return true },
// It is intentional that we do not check any credentials and accept all connections.
// This allows all users to connect and clone repositories. However, the public key
// is passed to handlers, so e.g. the push handler could check the key and reject the
// push if it needs to.
}
server.AddHostKey(host_key)
err = server.Serve(listener)
if err != nil {
if err = server.Serve(listener); err != nil {
clog.Fatal(1, "Serving SSH: "+err.Error()) } return nil }
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileContributor: Runxi Yu <https://runxiyu.org>
package main
import (
"context"
"errors"
"fmt"
"io"
"net/url"
"strings"
"go.lindenii.runxiyu.org/lindenii-common/ansiec"
)
var err_ssh_illegal_endpoint = errors.New("illegal endpoint during SSH access")
func get_repo_path_perms_from_ssh_path_pubkey(ctx context.Context, ssh_path string, ssh_pubkey string) (group_name string, repo_name string, repo_id int, repo_path string, direct_access bool, contrib_requirements string, user_type string, user_id int, err error) {
segments := strings.Split(strings.TrimPrefix(ssh_path, "/"), "/")
var segments []string var separator_index int var module_type, module_name string segments = strings.Split(strings.TrimPrefix(ssh_path, "/"), "/")
for i, segment := range segments {
var err error
segments[i], err = url.PathUnescape(segment)
if err != nil {
return "", "", 0, "", false, "", "", 0, err
}
}
if segments[0] == ":" {
return "", "", 0, "", false, "", "", 0, err_ssh_illegal_endpoint
}
separator_index := -1
separator_index = -1
for i, part := range segments {
if part == ":" {
separator_index = i
break
}
}
if segments[len(segments)-1] == "" {
segments = segments[:len(segments)-1]
}
switch {
case separator_index == -1:
return "", "", 0, "", false, "", "", 0, err_ssh_illegal_endpoint
case len(segments) <= separator_index+2:
return "", "", 0, "", false, "", "", 0, err_ssh_illegal_endpoint
}
group_name = segments[0]
module_type := segments[separator_index+1] module_name := segments[separator_index+2]
module_type = segments[separator_index+1] module_name = segments[separator_index+2]
repo_name = module_name
switch module_type {
case "repos":
_1, _2, _3, _4, _5, _6, _7 := get_path_perm_by_group_repo_key(ctx, group_name, module_name, ssh_pubkey)
return group_name, repo_name, _1, _2, _3, _4, _5, _6, _7
default:
return "", "", 0, "", false, "", "", 0, err_ssh_illegal_endpoint
}
}
func wf_error(w io.Writer, format string, args ...any) {
fmt.Fprintln(w, ansiec.Red+fmt.Sprintf(format, args...)+ansiec.Reset)
}
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileContributor: Runxi Yu <https://runxiyu.org> package main import ( "context"
"github.com/jackc/pgx/v5"
)
func add_user_ssh(ctx context.Context, pubkey string) (user_id int, err error) {
tx, err := database.Begin(ctx)
if err != nil {
var tx pgx.Tx
if tx, err = database.Begin(ctx); err != nil {
return } defer tx.Rollback(ctx)
err = tx.QueryRow(ctx, `INSERT INTO users (type) VALUES ('pubkey_only') RETURNING id`).Scan(&user_id)
if err != nil {
if err = tx.QueryRow(ctx, `INSERT INTO users (type) VALUES ('pubkey_only') RETURNING id`).Scan(&user_id); err != nil {
return }
_, err = tx.Exec(ctx, `INSERT INTO ssh_public_keys (key_string, user_id) VALUES ($1, $2)`, pubkey, user_id)
if err != nil {
if _, err = tx.Exec(ctx, `INSERT INTO ssh_public_keys (key_string, user_id) VALUES ($1, $2)`, pubkey, user_id); err != nil {
return } err = tx.Commit(ctx) return }