Skip to content

Commit ac0489c

Browse files
authored
Merge pull request #21 from sqlrsync/fixPullKeyRequest
feat: prompt for key if anon PULL access fails
2 parents 025927c + cc9b3d2 commit ac0489c

File tree

9 files changed

+254
-71
lines changed

9 files changed

+254
-71
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ tmp/
1616
client/sqlrsync
1717
client/sqlrsync
1818
client/sqlrsync_simple
19+
asciinema/

client/Makefile

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# SQLite Rsync Go Client Makefile
2-
.PHONY: all build clean test deps check-deps install-deps run help
2+
.PHONY: all build clean test deps check-deps install-deps install run help
33

44
# Build configuration
55
BINARY_NAME := sqlrsync
@@ -55,6 +55,12 @@ build: $(SQLITE_RSYNC_LIB)
5555
CGO_ENABLED=$(CGO_ENABLED) CGO_LDFLAGS="-L$(BRIDGE_LIB_DIR) -lsqlite_rsync" go build $(GOFLAGS) -ldflags="$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME) $(MAIN_FILE)
5656
@echo "✓ Build complete: $(BUILD_DIR)/$(BINARY_NAME)"
5757

58+
# Install the binary to system path
59+
install: build
60+
@echo "Installing $(BINARY_NAME) to /usr/local/bin/..."
61+
cp $(BUILD_DIR)/$(BINARY_NAME) /usr/local/bin/$(BINARY_NAME)
62+
@echo "✓ Install complete: /usr/local/bin/$(BINARY_NAME)"
63+
5864
# Build with debug symbols
5965
build-debug: check-deps
6066
@echo "Building $(BINARY_NAME) with debug symbols..."
@@ -104,6 +110,7 @@ help:
104110
@echo " all - Check dependencies and build (default)"
105111
@echo " build - Build the binary"
106112
@echo " build-debug - Build with debug symbols"
113+
@echo " install - Build and install binary to /usr/local/bin"
107114
@echo " clean - Remove build artifacts"
108115
@echo " deps - Download Go dependencies"
109116
@echo " check-deps - Check system dependencies"
@@ -118,5 +125,6 @@ help:
118125
@echo "Usage examples:"
119126
@echo " make build"
120127
@echo " make run"
128+
@echo " make install"
121129
@echo " make run-dry"
122130
@echo " make test"

client/auth/config.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -166,17 +166,12 @@ func SaveLocalSecretsConfig(config *LocalSecretsConfig) error {
166166
return fmt.Errorf("failed to create directory %s: %w", dir, err)
167167
}
168168

169-
file, err := os.Create(path)
169+
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
170170
if err != nil {
171171
return fmt.Errorf("failed to create local-secrets config file %s: %w", path, err)
172172
}
173173
defer file.Close()
174174

175-
// Set file permissions to 0600 (read/write for owner only)
176-
if err := file.Chmod(0600); err != nil {
177-
return fmt.Errorf("failed to set permissions on local-secrets config file: %w", err)
178-
}
179-
180175
encoder := toml.NewEncoder(file)
181176
if err := encoder.Encode(config); err != nil {
182177
return fmt.Errorf("failed to write local-secrets config: %w", err)

client/auth/resolver.go

Lines changed: 33 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212

1313
// ResolveResult contains the resolved authentication information
1414
type ResolveResult struct {
15-
AccessToken string
15+
AccessKey string
1616
ReplicaID string
1717
ServerURL string
1818
RemotePath string
@@ -22,14 +22,14 @@ type ResolveResult struct {
2222

2323
// ResolveRequest contains the parameters for authentication resolution
2424
type ResolveRequest struct {
25-
LocalPath string
26-
RemotePath string
27-
ServerURL string
28-
ProvidedPullKey string
29-
ProvidedPushKey string
25+
LocalPath string
26+
RemotePath string
27+
ServerURL string
28+
ProvidedPullKey string
29+
ProvidedPushKey string
3030
ProvidedReplicaID string
31-
Operation string // "pull", "push", "subscribe"
32-
Logger *zap.Logger
31+
Operation string // "pull", "push", "subscribe"
32+
Logger *zap.Logger
3333
}
3434

3535
// Resolver handles authentication and configuration resolution
@@ -53,24 +53,24 @@ func (r *Resolver) Resolve(req *ResolveRequest) (*ResolveResult, error) {
5353
}
5454

5555
// 1. Try environment variable first
56-
if token := os.Getenv("SQLRSYNC_AUTH_TOKEN"); token != "" {
57-
r.logger.Debug("Using SQLRSYNC_AUTH_TOKEN from environment")
58-
result.AccessToken = token
56+
if key := os.Getenv("SQLRSYNC_AUTH_KEY"); key != "" {
57+
r.logger.Debug("Using SQLRSYNC_AUTH_KEY from environment")
58+
result.AccessKey = key
5959
result.ReplicaID = req.ProvidedReplicaID
6060
return result, nil
6161
}
6262

6363
// 2. Try explicitly provided keys
6464
if req.ProvidedPullKey != "" {
6565
r.logger.Debug("Using provided pull key")
66-
result.AccessToken = req.ProvidedPullKey
66+
result.AccessKey = req.ProvidedPullKey
6767
result.ReplicaID = req.ProvidedReplicaID
6868
return result, nil
6969
}
7070

7171
if req.ProvidedPushKey != "" {
7272
r.logger.Debug("Using provided push key")
73-
result.AccessToken = req.ProvidedPushKey
73+
result.AccessKey = req.ProvidedPushKey
7474
result.ReplicaID = req.ProvidedReplicaID
7575
return result, nil
7676
}
@@ -87,7 +87,7 @@ func (r *Resolver) Resolve(req *ResolveRequest) (*ResolveResult, error) {
8787
if req.ServerURL == "wss://sqlrsync.com" {
8888
if localSecretsConfig, err := LoadLocalSecretsConfig(); err == nil {
8989
if dbConfig := localSecretsConfig.FindDatabaseByPath(absLocalPath); dbConfig != nil {
90-
r.logger.Debug("Using server URL from local secrets config",
90+
r.logger.Debug("Using server URL from local secrets config",
9191
zap.String("configuredServer", dbConfig.Server),
9292
zap.String("defaultServer", req.ServerURL))
9393
result.ServerURL = dbConfig.Server
@@ -114,7 +114,7 @@ func (r *Resolver) Resolve(req *ResolveRequest) (*ResolveResult, error) {
114114
if req.Operation == "push" {
115115
if os.Getenv("SQLRSYNC_ADMIN_KEY") != "" {
116116
r.logger.Debug("Using SQLRSYNC_ADMIN_KEY from environment")
117-
result.AccessToken = os.Getenv("SQLRSYNC_ADMIN_KEY")
117+
result.AccessKey = os.Getenv("SQLRSYNC_ADMIN_KEY")
118118
result.ShouldPrompt = false
119119
return result, nil
120120
}
@@ -126,7 +126,7 @@ func (r *Resolver) Resolve(req *ResolveRequest) (*ResolveResult, error) {
126126

127127
// 5. If it's a pull, maybe no key needed
128128
if req.Operation == "pull" || req.Operation == "subscribe" {
129-
result.AccessToken = ""
129+
result.AccessKey = ""
130130
result.ShouldPrompt = false
131131
return result, nil
132132
}
@@ -138,7 +138,7 @@ func (r *Resolver) Resolve(req *ResolveRequest) (*ResolveResult, error) {
138138
// resolveFromLocalSecrets attempts to resolve auth from local-secrets.toml
139139
func (r *Resolver) resolveFromLocalSecrets(absLocalPath, serverURL string, result *ResolveResult) (*ResolveResult, error) {
140140
r.logger.Debug("Attempting to resolve from local secrets", zap.String("absLocalPath", absLocalPath), zap.String("serverURL", serverURL))
141-
141+
142142
localSecretsConfig, err := LoadLocalSecretsConfig()
143143
if err != nil {
144144
r.logger.Debug("Failed to load local secrets config", zap.Error(err))
@@ -162,14 +162,14 @@ func (r *Resolver) resolveFromLocalSecrets(absLocalPath, serverURL string, resul
162162
}
163163

164164
if dbConfig.Server != serverURL {
165-
r.logger.Debug("Server URL mismatch",
166-
zap.String("configured", dbConfig.Server),
165+
r.logger.Debug("Server URL mismatch",
166+
zap.String("configured", dbConfig.Server),
167167
zap.String("requested", serverURL))
168168
return nil, fmt.Errorf("server URL mismatch: configured=%s, requested=%s", dbConfig.Server, serverURL)
169169
}
170170

171171
r.logger.Debug("Found authentication in local secrets config")
172-
result.AccessToken = dbConfig.PushKey
172+
result.AccessKey = dbConfig.PushKey
173173
result.ReplicaID = dbConfig.ReplicaID
174174
result.RemotePath = dbConfig.RemotePath
175175
result.ServerURL = dbConfig.Server
@@ -193,32 +193,33 @@ func (r *Resolver) resolveFromDashFile(localPath string, result *ResolveResult)
193193
}
194194

195195
r.logger.Debug("Found authentication in -sqlrsync file")
196-
result.AccessToken = dashSQLRsync.PullKey
196+
result.AccessKey = dashSQLRsync.PullKey
197197
result.ReplicaID = dashSQLRsync.ReplicaID
198198
result.RemotePath = dashSQLRsync.RemotePath
199199
result.ServerURL = dashSQLRsync.Server
200200

201201
return result, nil
202202
}
203203

204-
// PromptForAdminKey prompts the user for an admin key
205-
func (r *Resolver) PromptForAdminKey(serverURL string) (string, error) {
204+
// PromptForKey prompts the user for an key
205+
func (r *Resolver) PromptForKey(serverURL string, remotePath string, keyType string) (string, error) {
206206
httpServer := strings.Replace(serverURL, "ws", "http", 1)
207-
fmt.Println("No Key provided. Creating a new Replica? Get a key at " + httpServer + "/namespaces")
208-
fmt.Print(" Enter an Account Admin Key to create a new Replica: ")
207+
fmt.Println("Replica not found when using unauthenticated access. Try again using a key or check your spelling.")
208+
fmt.Println(" Get a key at " + httpServer + "/namespaces or " + httpServer + "/" + remotePath)
209+
fmt.Print(" Provide a key to " + keyType + ": ")
209210

210211
reader := bufio.NewReader(os.Stdin)
211-
token, err := reader.ReadString('\n')
212+
key, err := reader.ReadString('\n')
212213
if err != nil {
213-
return "", fmt.Errorf("failed to read admin key: %w", err)
214+
return "", fmt.Errorf("failed to read key: %w", err)
214215
}
215216

216-
token = strings.TrimSpace(token)
217-
if token == "" {
218-
return "", fmt.Errorf("admin key cannot be empty")
217+
key = strings.TrimSpace(key)
218+
if key == "" {
219+
return "", fmt.Errorf("key cannot be empty")
219220
}
220221

221-
return token, nil
222+
return key, nil
222223
}
223224

224225
// SavePushResult saves the result of a successful push operation
@@ -279,4 +280,4 @@ func (r *Resolver) CheckNeedsDashFile(localPath, remotePath string) bool {
279280
}
280281

281282
return dashSQLRsync.RemotePath != remotePath
282-
}
283+
}

client/main.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ func runSync(cmd *cobra.Command, args []string) error {
155155
// Create sync coordinator
156156
coordinator := sync.NewCoordinator(&sync.CoordinatorConfig{
157157
ServerURL: serverURL,
158-
ProvidedAuthToken: getAuthToken(),
158+
ProvidedAuthKey: getAuthKey(),
159159
ProvidedPullKey: pullKey,
160160
ProvidedPushKey: pushKey,
161161
ProvidedReplicaID: replicaID,
@@ -224,10 +224,10 @@ func determineOperation(args []string) (sync.Operation, string, string, error) {
224224
return sync.Operation(0), "", "", fmt.Errorf("invalid arguments")
225225
}
226226

227-
func getAuthToken() string {
227+
func getAuthKey() string {
228228
// Try environment variable first
229-
if token := os.Getenv("SQLRSYNC_AUTH_TOKEN"); token != "" {
230-
return token
229+
if key := os.Getenv("SQLRSYNC_AUTH_KEY"); key != "" {
230+
return key
231231
}
232232

233233
// Try pull/push keys

client/remote/client.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -401,9 +401,9 @@ type Config struct {
401401
EnableTrafficInspection bool // Enable detailed traffic logging
402402
InspectionDepth int // How many bytes to inspect (default: 32)
403403
PingPong bool
404-
AuthToken string
404+
AuthKey string
405405
ClientVersion string // version of the client software
406-
SendKeyRequest bool // the -sqlrsync file doesn't exist, so make a token
406+
SendKeyRequest bool // the -sqlrsync file doesn't exist, so make a key
407407

408408
SendConfigCmd bool // we don't have the version number or remote path
409409
LocalHostname string
@@ -685,9 +685,9 @@ func (c *Client) Connect() error {
685685

686686
headers := http.Header{}
687687

688-
headers.Set("Authorization", c.config.AuthToken)
688+
headers.Set("Authorization", c.config.AuthKey)
689689

690-
headers.Set("X-ClientVersion", c.config.ClientVersion);
690+
headers.Set("X-ClientVersion", c.config.ClientVersion)
691691

692692
if c.config.WsID != "" {
693693
headers.Set("X-ClientID", c.config.WsID)
@@ -882,9 +882,9 @@ func (c *Client) Read(buffer []byte) (int, error) {
882882
if c.config.Subscribe {
883883
return 1 * time.Hour
884884
}
885-
// Use a longer timeout if sync is completed to allow final transaction processing
885+
// Use a shorter timeout if sync is completed to allow final transaction processing
886886
if c.isSyncCompleted() {
887-
return 2 * time.Second
887+
return 1 * time.Second
888888
}
889889
return 30 * time.Second
890890
}()):
@@ -1012,7 +1012,7 @@ func (c *Client) Close() {
10121012
if c.conn != nil {
10131013
// Send close message
10141014
closeMessage := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")
1015-
err := c.conn.WriteControl(websocket.CloseMessage, closeMessage, time.Now().Add(5*time.Second))
1015+
err := c.conn.WriteControl(websocket.CloseMessage, closeMessage, time.Now().Add(3*time.Second))
10161016
if err != nil {
10171017
c.logger.Debug("Error sending close message", zap.Error(err))
10181018
} else {

client/subscription/manager.go

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package subscription
22

33
import (
4+
"bufio"
45
"context"
56
"encoding/json"
67
"fmt"
78
"io"
89
"net/http"
910
"net/url"
11+
"os"
1012
"strings"
1113
"sync"
1214
"time"
@@ -38,7 +40,7 @@ type Message struct {
3840
type ManagerConfig struct {
3941
ServerURL string
4042
ReplicaPath string
41-
AuthToken string
43+
AccessKey string
4244
ReplicaID string
4345
WsID string // websocket ID for client identification
4446
ClientVersion string // version of the client software
@@ -199,7 +201,7 @@ func (m *Manager) doConnect() error {
199201
u.Path = strings.TrimSuffix(u.Path, "/") + "/sapi/subscribe/" + m.config.ReplicaPath
200202

201203
headers := http.Header{}
202-
headers.Set("Authorization", m.config.AuthToken)
204+
headers.Set("Authorization", m.config.AccessKey)
203205
if m.config.ReplicaID != "" {
204206
headers.Set("X-ReplicaID", m.config.ReplicaID)
205207
}
@@ -229,6 +231,20 @@ func (m *Manager) doConnect() error {
229231
}
230232
}
231233

234+
// Connect to remote server
235+
if strings.Contains(err.Error(), "key is not authorized") || strings.Contains(err.Error(), "404 Path not found") {
236+
if m.config.AccessKey == "" {
237+
key, err := PromptForKey(m.config.ServerURL, m.config.ReplicaPath, "PULL")
238+
if err != nil {
239+
return fmt.Errorf("manager failed to get key interactively: %w", err)
240+
}
241+
m.config.AccessKey = key
242+
return m.doConnect()
243+
} else {
244+
return fmt.Errorf("manager failed to connect to server: %w", err)
245+
}
246+
}
247+
232248
// Create a clean error message
233249
var errorMsg strings.Builder
234250
errorMsg.WriteString(fmt.Sprintf("HTTP %d (%s)", statusCode, statusText))
@@ -574,3 +590,24 @@ func (m *Manager) pingLoop() {
574590
}
575591
}
576592
}
593+
594+
// PromptForKey prompts the user for an admin key
595+
func PromptForKey(serverURL string, remotePath string, keyType string) (string, error) {
596+
httpServer := strings.Replace(serverURL, "ws", "http", 1)
597+
fmt.Println("Replica not found when using unauthenticated access. Try again using a key or check your spelling.")
598+
fmt.Println(" Get a key at " + httpServer + "/namespaces or " + httpServer + "/" + remotePath)
599+
fmt.Print(" Provide a key to " + keyType + ": ")
600+
601+
reader := bufio.NewReader(os.Stdin)
602+
key, err := reader.ReadString('\n')
603+
if err != nil {
604+
return "", fmt.Errorf("failed to read admin key: %w", err)
605+
}
606+
607+
key = strings.TrimSpace(key)
608+
if key == "" {
609+
return "", fmt.Errorf("admin key cannot be empty")
610+
}
611+
612+
return key, nil
613+
}

0 commit comments

Comments
 (0)