git.sr.ht/gitsrht-keys/main.go -rw-r--r-- 5.6 KiB View raw
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
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 {
	logger.Println("Storing meta.sr.ht key in git.sr.ht database")

	// 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
		-- This no-ops on conflict, but we still need this query to complete so
		-- that we can extract the user ID. DO NOTHING returns zero rows.
		ON CONFLICT (meta_id) DO UPDATE SET meta_id = $2
		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.Printf("Error inserting key: %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 {
		logger.Printf("non-200 response from meta.sr.ht: %d", resp.StatusCode)
		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.
	// In order to facilitate this, we do one of two things:
	// - Attempt to fetch the cached key info from Redis (preferred)
	// - Fetch the key from meta.sr.ht and store it in SQL and Redis (slower)

	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 {
		logger.Println("Cache miss, going to meta.sr.ht")
		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
		logger.Printf("Cache hit: %d %s", userId, 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()
	logger.Printf("Assigned uuid %s to this push", push.String())
	shellCommand := fmt.Sprintf("%s '%d' '%s' '%s'",
		shell, userId, username, b64key)
	fmt.Printf(`restrict,command="%s",`+
		`environment="SRHT_PUSH=%s" %s %s %s`+"\n",
		shellCommand, push.String(), keyType, b64key, username)
}