From 6696d28721d0ebf06974d876d228524d945d669f Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Tue, 22 Oct 2019 16:05:32 -0400 Subject: [PATCH] Rewrite gitsrht-shell in golang --- gitsrht-shell | 81 -------------- gitsrht-shell/.gitignore | 1 + gitsrht-shell/go.mod | 9 ++ gitsrht-shell/go.sum | 11 ++ gitsrht-shell/main.go | 227 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 248 insertions(+), 81 deletions(-) delete mode 100755 gitsrht-shell create mode 100644 gitsrht-shell/.gitignore create mode 100644 gitsrht-shell/go.mod create mode 100644 gitsrht-shell/go.sum create mode 100644 gitsrht-shell/main.go diff --git a/gitsrht-shell b/gitsrht-shell deleted file mode 100755 index 843205a..0000000 --- a/gitsrht-shell +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 -import sys -import os -try: - f = open("/var/log/git-srht-shell", "a") - os.close(sys.stderr.fileno()) - os.dup2(f.fileno(), sys.stderr.fileno()) -except Exception as ex: - sys.stderr.write("Unable to open log for writing\n") - sys.stderr.write(str(ex) + "\n") -import json -import requests -import shlex -from datetime import datetime -from srht.config import cfg, get_origin -from srht.crypto import sign_payload -from srht.validation import Validation - -def log(s, *args): - sys.stderr.write("{} {}\n".format(datetime.now().isoformat(), - s.format(*args) if isinstance(s, str) else str(s))) - -origin = get_origin("git.sr.ht") -repos = cfg("git.sr.ht", "repos") - -_cmd = os.environ.get("SSH_ORIGINAL_COMMAND") -if not _cmd: - _cmd = "" -if len(sys.argv) < 2: - log("Error: expected 2 arguments from SSH") - sys.exit(1) -user_id = int(sys.argv[1]) -username = sys.argv[2] -ssh_key = sys.argv[3] - -log("User: {}", username) - -cmd = shlex.split(_cmd) -valid_commands = ["git-receive-pack", "git-upload-pack", "git-upload-archive"] -if len(cmd) < 1 or not cmd[0] in valid_commands: - log("Not permitting unacceptable command") - print("Hi {}! You've successfully authenticated, ".format(username) + - "but I do not provide an interactive shell. Bye!") - sys.exit(128) -os.chdir(repos) -path = os.path.abspath(cmd[-1]) -if not path.startswith(repos): - path = os.path.join(repos, path) -cmd[-1] = path - -# Delegate to web application for validation -payload = { - "path": path, - "user_id": user_id, - # 2 is write, 1 is read - "access": 2 if cmd[0] == "git-receive-pack" else 1, -} -payload = json.dumps(payload) -headers = { - "Content-Type": "application/json", -} -headers.update(sign_payload(payload)) -r = requests.post(f"{origin}/internal/push-check", - data=payload, headers=headers) -sys.stderr.write(r.text + "\n") -response = r.json() -if r.status_code == 302: - print("\n\t\033[93mNOTICE\033[0m\n") - print("\tThis repository has moved:\n") - print(f"\t{response['redirect']}\n") - print("\tPlease update your remote.\n\n") - sys.exit(128) -elif r.status_code == 200: - os.environ["SRHT_PUSH_CTX"] = r.text - log("Executing {}", " ".join(cmd)) - sys.stderr.close() - os.execvp(cmd[0], cmd) -else: - if "why" in response: - print(response["why"]) - sys.exit(128) diff --git a/gitsrht-shell/.gitignore b/gitsrht-shell/.gitignore new file mode 100644 index 0000000..2955be8 --- /dev/null +++ b/gitsrht-shell/.gitignore @@ -0,0 +1 @@ +gitsrht-shell diff --git a/gitsrht-shell/go.mod b/gitsrht-shell/go.mod new file mode 100644 index 0000000..e3c7e00 --- /dev/null +++ b/gitsrht-shell/go.mod @@ -0,0 +1,9 @@ +module git.sr.ht/~sircmpwn/git.sr.ht/gitsrht-shell + +require ( + github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf + github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec + golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 +) + +go 1.13 diff --git a/gitsrht-shell/go.sum b/gitsrht-shell/go.sum new file mode 100644 index 0000000..984f16c --- /dev/null +++ b/gitsrht-shell/go.sum @@ -0,0 +1,11 @@ +github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf h1:7+FW5aGwISbqUtkfmIpZJGRgNFg2ioYPvFaUxdqpDsg= +github.com/google/shlex v0.0.0-20181106134648-c34317bd91bf/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE= +github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec h1:DGmKwyZwEB8dI7tbLt/I/gQuP559o/0FrAkHKlQM/Ks= +github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec/go.mod h1:owBmyHYMLkxyrugmfwE/DLJyW8Ro9mkphwuVErQ0iUw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/gitsrht-shell/main.go b/gitsrht-shell/main.go new file mode 100644 index 0000000..0f67472 --- /dev/null +++ b/gitsrht-shell/main.go @@ -0,0 +1,227 @@ +package main + +import ( + "bytes" + "crypto/ed25519" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "os/exec" + gopath "path" + "path/filepath" + "strconv" + "strings" + "syscall" + + "github.com/google/shlex" + "github.com/vaughan0/go-ini" +) + +func main() { + var ( + config ini.File + err error + logger *log.Logger + + userId int + username string + + origin string + repos string + privkey ed25519.PrivateKey + + cmdstr string + cmd []string + ) + + logf, err := os.OpenFile("/var/log/gitsrht-shell", + os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) + if err != nil { + log.Printf("Warning: unable to open log file: %v " + + "(using stderr instead)", err) + logger = log.New(os.Stderr, "", log.LstdFlags) + } else { + logger = log.New(logf, "", log.LstdFlags) + } + + if len(os.Args) < 2 { + logger.Fatalf("Expected two arguments from SSH") + } + logger.Printf("os.Args: %v", os.Args) + + if userId, err = strconv.Atoi(os.Args[1]); err != nil { + logger.Fatalf("Couldn't interpret user ID: %v", err) + } + username = os.Args[2] + + for _, path := range []string{"../config.ini", "/etc/sr.ht/config.ini"} { + config, err = ini.LoadFile(path) + if err == nil { + break + } + } + if err != nil { + logger.Fatalf("Failed to load config file: %v", err) + } + + origin, ok := config.Get("git.sr.ht", "internal-origin") + if !ok { + origin, ok = config.Get("git.sr.ht", "origin") + } + if !ok || origin == "" { + logger.Fatalf("No origin configured for git.sr.ht") + } + + repos, ok = config.Get("git.sr.ht", "repos") + if !ok { + logger.Fatalf("No repo path configured for git.sr.ht") + } + + b64key, ok := config.Get("webhooks", "private-key") + if !ok { + logger.Fatalf("No webhook key configured") + } + seed, err := base64.StdEncoding.DecodeString(b64key) + if err != nil { + logger.Fatalf("base64 decode webhooks private key: %v", err) + } + privkey = ed25519.NewKeyFromSeed(seed) + + cmdstr, ok = os.LookupEnv("SSH_ORIGINAL_COMMAND") + if !ok { + cmdstr = "" + } + + cmd, err = shlex.Split(cmdstr) + if err != nil { + logger.Fatalf("Unable to parse command: %v", err) + } + + logger.Println("Running git.sr.ht shell") + + validCommands := []string{ + "git-receive-pack", "git-upload-pack", "git-upload-archive", + } + var valid bool + for _, c := range validCommands { + if len(cmd) > 0 && c == cmd[0] { + valid = true + } + } + + if !valid { + logger.Printf("Not permitting unacceptable command: %v", cmd) + fmt.Printf("Hi %s! You've successfully authenticated, " + + "but I do not provide an interactive shell. Bye!\n", username) + os.Exit(128) + } + + os.Chdir(repos) + + path := cmd[len(cmd)-1] + path, err = filepath.Abs(path) + if err != nil { + logger.Fatalf("filepath.Abs(%s): %v", path, err) + } + if !strings.HasPrefix(path, repos) { + path = gopath.Join(repos, path) + } + cmd[len(cmd)-1] = path + + access := 1 + if cmd[0] == "git-receive-pack" { + access = 2 + } + + payload, err := json.Marshal(struct { + Access int `json:"access"` + Path string `json:"path"` + UserId int `json:"user_id"` + }{ + Access: access, + Path: path, + UserId: userId, + }) + if err != nil { + logger.Fatalf("json.Marshal: %v", err) + } + logger.Println(string(payload)) + + var ( + nonceSeed []byte + nonceHex []byte + ) + _, err = rand.Read(nonceSeed) + if err != nil { + logger.Fatalf("generate nonce: %v", err) + } + hex.Encode(nonceHex, nonceSeed) + signature := ed25519.Sign(privkey, append(payload, nonceHex...)) + + headers := make(http.Header) + headers.Add("Content-Type", "application/json") + headers.Add("X-Payload-Nonce", string(nonceHex)) + headers.Add("X-Payload-Signature", + base64.StdEncoding.EncodeToString(signature)) + + check, err := url.Parse(fmt.Sprintf("%s/internal/push-check", origin)) + if err != nil { + logger.Fatalf("url.Parse: %v", err) + } + req := http.Request{ + Body: ioutil.NopCloser(bytes.NewBuffer(payload)), + ContentLength: int64(len(payload)), + Header: headers, + Method: "POST", + URL: check, + } + resp, err := http.DefaultClient.Do(&req) + if err != nil { + logger.Fatalf("http.Client.Do: %v", err) + } + defer resp.Body.Close() + results, err := ioutil.ReadAll(resp.Body) + if err != nil { + logger.Fatal("ReadAll(resp.Body): %v", err) + } + logger.Println(string(results)) + + switch resp.StatusCode { + case 302: + var redirect struct { + Redirect string `json:"redirect"` + } + json.Unmarshal(results, &redirect) + + fmt.Printf("\n\t\033[93mNOTICE\033[0m\n\n") + fmt.Printf("\tThis repository has moved:\n\n") + fmt.Printf("\t%s\n\n", redirect.Redirect) + fmt.Printf("\tPlease update your remote.\n\n\n") + os.Exit(128) + case 200: + logger.Printf("Executing command: %v", cmd) + bin, err := exec.LookPath(cmd[0]) + if err != nil { + logger.Fatalf("exec.LookPath: %v", err) + } + if err := syscall.Exec(bin, cmd, + append(os.Environ(), fmt.Sprintf( + "SRHT_PUSH_CTX=%s", string(results)))); err != nil { + logger.Fatalf("syscall.Exec: %v", err) + } + default: + var why struct { + Why string `json:"why"` + } + json.Unmarshal(results, &why) + fmt.Println(why.Why) + os.Exit(128) + } +} -- 2.38.4