~edwargix/git.sr.ht

87093ceffecfab103f73d6e49cd478065536e97e — Drew DeVault 6 years ago d6c488f
Rewrite gitsrht-keys in Golang
6 files changed, 225 insertions(+), 73 deletions(-)

D gitsrht-keys
A gitsrht-keys/.gitignore
A gitsrht-keys/gitsrht-keys
A gitsrht-keys/go.mod
A gitsrht-keys/go.sum
A gitsrht-keys/main.go
D gitsrht-keys => gitsrht-keys +0 -73
@@ 1,73 0,0 @@
#!/usr/bin/env python3
import json
import os
import sys
import requests
from scmsrht.redis import redis
from srht.api import get_results
from srht.config import cfg, get_origin
from uuid import uuid4

from srht.crypto import sign_payload

sys.stderr.write(str(sys.argv) + "\n")
key_type = sys.argv[3]
b64key = sys.argv[4]

user_id = username = None

meta_origin = get_origin("meta.sr.ht")
cache = redis.get(f"git.sr.ht.ssh-keys.{b64key}")
if cache:
    cache = json.loads(cache.decode())
    user_id = cache["user_id"]
    username = cache["username"]
else:
    from srht.database import DbSession
    db = DbSession(cfg("git.sr.ht", "connection-string"))
    from gitsrht.types import User, SSHKey
    db.init()

    from gitsrht.service import oauth_service
    # Fall back to meta.sr.ht first
    r = requests.get(f"{meta_origin}/api/ssh-key/{b64key}")
    if r.status_code == 200:
        username = r.json()["owner"]["name"]
        user = User.query.filter(User.username == username).one_or_none()
        try:
            # Attempt to pull down keys for next time
            keys_url = f"{meta_origin}/api/user/ssh-keys"
            for key in get_results(keys_url, user.oauth_token):
                oauth_service.ensure_user_sshkey(user, key)
            db.session.commit()
        except:
            pass
    if user:
        user_id = user.id
        username = user.username

if not user_id:
    sys.stderr.write("Unknown public key")
    sys.exit(0)

try:
    headers = {
        "Content-Type": "application/json",
    }
    headers.update(sign_payload(""))
    requests.put(f"{meta_origin}/api/ssh-key/{b64key}",
        data="", headers=headers)
except:
    pass

default_shell = os.path.join(os.path.dirname(sys.argv[0]), "gitsrht-shell")
shell = cfg("git.sr.ht", "shell", default=default_shell)
keys = ("command=\"{} '{}' '{}' '{}'\",".format(
        shell, user_id, username, b64key) +
    "no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty," +
    "environment=\"SRHT_UID={}\",environment=\"SRHT_PUSH={}\"".format(
        user_id, str(uuid4())) +
    " {} {} {}".format(key_type, b64key, username) + "\n")
print(keys)
sys.stderr.write(keys)
sys.exit(0)

A gitsrht-keys/.gitignore => gitsrht-keys/.gitignore +1 -0
@@ 0,0 1,1 @@
gitsrht-shell

A gitsrht-keys/gitsrht-keys => gitsrht-keys/gitsrht-keys +0 -0
A gitsrht-keys/go.mod => gitsrht-keys/go.mod +10 -0
@@ 0,0 1,10 @@
module git.sr.ht/~sircmpwn/git.sr.ht/gitsrht-keys

go 1.13

require (
	github.com/go-redis/redis v6.15.6+incompatible
	github.com/google/uuid v1.1.1
	github.com/lib/pq v1.2.0
	github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec
)

A gitsrht-keys/go.sum => gitsrht-keys/go.sum +8 -0
@@ 0,0 1,8 @@
github.com/go-redis/redis v6.15.6+incompatible h1:H9evprGPLI8+ci7fxQx6WNZHJSb7be8FqJQRhdQZ5Sg=
github.com/go-redis/redis v6.15.6+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
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=

A gitsrht-keys/main.go => gitsrht-keys/main.go +206 -0
@@ 0,0 1,206 @@
package main

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"path"

	goredis "github.com/go-redis/redis"
	"github.com/google/uuid"
	_ "github.com/lib/pq"
	"github.com/vaughan0/go-ini"
)

type KeyCache struct {
	UserId   int    `json:"user_id"`
	Username string `json:"username"`
}

// We don't need everything, so we don't include everything.
type MetaUser struct {
	Username string `json:"name"`
}

// We don't need everything, so we don't include everything.
type MetaSSHKey struct {
	Id          int      `json:"id"`
	Fingerprint string   `json:"fingerprint"`
	Key         string   `json:"key"`
	Owner       MetaUser `json:"owner"`
}

// Stores the SSH key in the database and returns the user's ID.
func storeKey(logger *log.Logger, db *sql.DB, key *MetaSSHKey) int {
	// Getting the user ID is really a separate concern, but this saves us a
	// SQL roundtrip and this is a performance-critical section
	query, err := db.Prepare(`
		WITH key_owner AS (
			SELECT id user_id
			FROM "user"
			WHERE "user".username = $1
		)
		INSERT INTO sshkey (
			user_id,
			meta_id,
			key,
			fingerprint
		)
		SELECT user_id, $2, $3, $4
		FROM key_owner
		RETURNING id, user_id;
	`)
	if err != nil {
		logger.Printf("Failed to prepare key insertion statement: %v", err)
		return 0
	}
	defer query.Close()

	var (
		userId int
		keyId  int
	)
	if err = query.QueryRow(key.Owner.Username,
		key.Id, key.Key, key.Fingerprint).Scan(&keyId, &userId); err != nil {

		logger.Fatalf("Error inserting key & looking up user: %v", err)
	}

	logger.Printf("Stored key %d for user %d", keyId, userId)
	return userId
}

func fetchKeysFromMeta(logger *log.Logger, config ini.File,
	redis *goredis.Client, b64key string) (string, int) {

	meta, ok := config.Get("meta.sr.ht", "internal-origin")
	if !ok {
		meta, ok = config.Get("meta.sr.ht", "origin")
	}
	if !ok && meta == "" {
		logger.Fatalf("No origin configured for meta.sr.ht")
	}

	resp, err := http.Get(fmt.Sprintf("%s/api/ssh-key/%s", meta, b64key))
	if err != nil {
		logger.Printf("meta.sr.ht http.Get: %v", err)
		return "", 0
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		return "", 0
	}

	body, err := ioutil.ReadAll(resp.Body)
	var key MetaSSHKey
	if err = json.Unmarshal(body, &key); err != nil {
		return "", 0
	}

	// We wait to connect to postgres until we know we must
	pgcs, ok := config.Get("git.sr.ht", "connection-string")
	if !ok {
		logger.Fatalf("No connection string configured for git.sr.ht: %v", err)
	}
	db, err := sql.Open("postgres", pgcs)
	if err != nil {
		logger.Fatalf("Failed to open a database connection: %v", err)
	}
	userId := storeKey(logger, db, &key)
	logger.Println("Fetched key from meta.sr.ht")

	// Cache in Redis too
	cacheKey := fmt.Sprintf("git.sr.ht.ssh-keys.%s", b64key)
	cache := KeyCache{
		UserId: userId,
		Username: key.Owner.Username,
	}
	cacheBytes, err := json.Marshal(&cache)
	if err != nil {
		logger.Printf("Caching SSH key in redis failed: %v", err)
	} else {
		redis.Set(cacheKey, cacheBytes, 0)
	}

	return key.Owner.Username, userId
}

func main() {
	// gitsrht-keys is run by sshd to generate an authorized_key file on stdout.

	var (
		config ini.File
		err    error
		logger *log.Logger
	)
	// TODO: update key last used timestamp on meta.sr.ht

	redis := goredis.NewClient(&goredis.Options{Addr: "localhost:6379"})

	logf, err := os.OpenFile("/var/log/gitsrht-keys",
		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)
	}

	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)
	}

	if len(os.Args) < 5 {
		logger.Fatalf("Expected four arguments from SSH")
	}
	logger.Printf("os.Args: %v", os.Args)
	keyType := os.Args[3]
	b64key := os.Args[4]

	var (
		username string
		userId   int
	)
	cacheKey := fmt.Sprintf("git.sr.ht.ssh-keys.%s", b64key)
	logger.Printf("Cache key for SSH key lookup: %s", cacheKey)
	cacheBytes, err := redis.Get(cacheKey).Bytes()
	if err != nil {
		username, userId = fetchKeysFromMeta(logger, config, redis, b64key)
	} else {
		var cache KeyCache
		if err = json.Unmarshal(cacheBytes, &cache); err != nil {
			logger.Fatalf("Unmarshal cache JSON: %v", err)
		}
		userId = cache.UserId
		username = cache.Username
	}

	if username == "" {
		logger.Println("Unknown public key")
		os.Exit(0)
	}

	defaultShell := path.Join(path.Dir(os.Args[0]), "gitsrht-shell")
	shell, ok := config.Get("git.sr.ht", "shell")
	if !ok {
		shell = defaultShell
	}

	push := uuid.New()
	shellCommand := fmt.Sprintf("%s '%d' '%s' '%s'",
		shell, userId, username, b64key)
	fmt.Printf(`restrict,command="%s",environment="SRHT_UID=%d",`+
		`environment="SRHT_PUSH=%s" %s %s %s`+"\n",
		shellCommand, userId, push.String(), keyType, b64key, username)
}