Merge pull request #65 from DeBrosOfficial/nightly

Nightly
This commit is contained in:
anonpenguin 2025-11-03 08:39:19 +02:00 committed by GitHub
commit 42131c0e75
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 2152 additions and 553 deletions

89
.githooks/pre-commit Normal file
View File

@ -0,0 +1,89 @@
#!/bin/bash
# Colors for output
CYAN='\033[0;36m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
RED='\033[0;31m'
BLUE='\033[0;34m'
NOCOLOR='\033[0m'
# Get the directory where this hook is located
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Go up from .git/hooks/ to repo root
REPO_ROOT="$(cd "$HOOK_DIR/../.." && pwd)"
CHANGELOG_SCRIPT="$REPO_ROOT/scripts/update_changelog.sh"
PREVIEW_FILE="$REPO_ROOT/.changelog_preview.tmp"
VERSION_FILE="$REPO_ROOT/.changelog_version.tmp"
# Only run changelog update if there are actual code changes (not just changelog files)
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
if [ -z "$STAGED_FILES" ]; then
# No staged files, exit
exit 0
fi
# Check if only CHANGELOG.md and/or Makefile are being committed
OTHER_FILES=$(echo "$STAGED_FILES" | grep -v "^CHANGELOG.md$" | grep -v "^Makefile$")
if [ -z "$OTHER_FILES" ]; then
# Only changelog files are being committed, skip update
exit 0
fi
# Update changelog before commit
if [ -f "$CHANGELOG_SCRIPT" ]; then
echo -e "\n${CYAN}Updating changelog...${NOCOLOR}"
# Set environment variable to indicate we're running from pre-commit
export CHANGELOG_CONTEXT=pre-commit
bash "$CHANGELOG_SCRIPT"
changelog_status=$?
if [ $changelog_status -ne 0 ]; then
echo -e "${RED}Commit aborted: changelog update failed.${NOCOLOR}"
exit 1
fi
# Show preview if changelog was updated
if [ -f "$PREVIEW_FILE" ] && [ -f "$VERSION_FILE" ]; then
NEW_VERSION=$(cat "$VERSION_FILE")
PREVIEW_CONTENT=$(cat "$PREVIEW_FILE")
echo ""
echo -e "${BLUE}========================================================================${NOCOLOR}"
echo -e "${CYAN} CHANGELOG PREVIEW${NOCOLOR}"
echo -e "${BLUE}========================================================================${NOCOLOR}"
echo ""
echo -e "${GREEN}New Version: ${YELLOW}$NEW_VERSION${NOCOLOR}"
echo ""
echo -e "${CYAN}Changelog Entry:${NOCOLOR}"
echo -e "${BLUE}────────────────────────────────────────────────────────────────────────${NOCOLOR}"
echo -e "$PREVIEW_CONTENT"
echo -e "${BLUE}────────────────────────────────────────────────────────────────────────${NOCOLOR}"
echo ""
echo -e "${YELLOW}Do you want to proceed with the commit? (yes/no):${NOCOLOR} "
# Read from /dev/tty to ensure we can read from terminal even in git hook context
read -r confirmation < /dev/tty
if [ "$confirmation" != "yes" ]; then
echo -e "${RED}Commit aborted by user.${NOCOLOR}"
echo -e "${YELLOW}To revert changes, run:${NOCOLOR}"
echo -e " git checkout CHANGELOG.md Makefile"
# Clean up temp files
rm -f "$PREVIEW_FILE" "$VERSION_FILE"
exit 1
fi
echo -e "${GREEN}Proceeding with commit...${NOCOLOR}"
# Add the updated CHANGELOG.md and Makefile to the current commit
echo -e "${CYAN}Staging CHANGELOG.md and Makefile...${NOCOLOR}"
git add CHANGELOG.md Makefile
# Clean up temp files
rm -f "$PREVIEW_FILE" "$VERSION_FILE"
fi
else
echo -e "${YELLOW}Warning: changelog update script not found at $CHANGELOG_SCRIPT${NOCOLOR}"
fi

View File

@ -1,11 +1,18 @@
#!/bin/bash
echo -e "\nRunning tests:"
# Colors for output
CYAN='\033[0;36m'
GREEN='\033[0;32m'
RED='\033[0;31m'
NOCOLOR='\033[0m'
# Run tests before push
echo -e "\n${CYAN}Running tests...${NOCOLOR}"
go test ./... # Runs all tests in your repo
status=$?
if [ $status -ne 0 ]; then
echo "Push aborted: some tests failed."
echo -e "${RED}Push aborted: some tests failed.${NOCOLOR}"
exit 1
else
echo "All tests passed. Proceeding with push."
echo -e "${GREEN}All tests passed. Proceeding with push.${NOCOLOR}"
fi

View File

@ -13,6 +13,144 @@ The format is based on [Keep a Changelog][keepachangelog] and adheres to [Semant
### Deprecated
### Fixed
## [0.53.18] - 2025-11-03
### Added
\n
### Changed
- Increased the connection timeout during peer discovery from 15 seconds to 20 seconds to improve connection reliability.
- Removed unnecessary debug logging related to filtering out ephemeral port addresses during peer exchange.
### Deprecated
### Removed
### Fixed
\n
## [0.53.17] - 2025-11-03
### Added
- Added a new Git `pre-commit` hook to automatically update the changelog and version before committing, ensuring version consistency.
### Changed
- Refactored the `update_changelog.sh` script to support different execution contexts (pre-commit vs. pre-push), allowing it to analyze only staged changes during commit.
- The Git `pre-push` hook was simplified by removing the changelog update logic, which is now handled by the `pre-commit` hook.
### Deprecated
### Removed
### Fixed
\n
## [0.53.16] - 2025-11-03
### Added
\n
### Changed
- Improved the changelog generation script to prevent infinite loops when the only unpushed commit is a previous changelog update.
### Deprecated
### Removed
### Fixed
\n
## [0.53.15] - 2025-11-03
### Added
\n
### Changed
- Improved the pre-push git hook to automatically commit updated changelog and Makefile after generation.
- Updated the changelog generation script to load the OpenRouter API key from the .env file or environment variables for better security.
- Modified the pre-push hook to read user confirmation from /dev/tty for better compatibility.
- Updated the bootstrap peer logic to prioritize the DEBROS_BOOTSTRAP_PEERS environment variable for easier configuration.
- Improved the gateway's private host check to correctly handle IPv6 addresses with or without brackets and ports.
### Deprecated
### Removed
### Fixed
\n
## [0.53.15] - 2025-11-03
### Added
\n
### Changed
- Improved the pre-push git hook to automatically commit updated changelog and Makefile after generation.
- Updated the changelog generation script to load the OpenRouter API key from the .env file or environment variables for better security.
- Modified the pre-push hook to read user confirmation from /dev/tty for better compatibility.
- Updated the bootstrap peer logic to prioritize the DEBROS_BOOTSTRAP_PEERS environment variable for easier configuration.
- Improved the gateway's private host check to correctly handle IPv6 addresses with or without brackets and ports.
### Deprecated
### Removed
### Fixed
\n
## [0.53.14] - 2025-11-03
### Added
- Added a new `install-hooks` target to the Makefile to easily set up git hooks.
- Added a script (`scripts/install-hooks.sh`) to copy git hooks from `.githooks` to `.git/hooks`.
### Changed
- Improved the pre-push git hook to automatically commit the updated `CHANGELOG.md` and `Makefile` after generating the changelog.
- Updated the changelog generation script (`scripts/update_changelog.sh`) to load the OpenRouter API key from the `.env` file or environment variables, improving security and configuration.
- Modified the pre-push hook to read user confirmation from `/dev/tty` for better compatibility in various terminal environments.
- Updated the bootstrap peer logic to check the `DEBROS_BOOTSTRAP_PEERS` environment variable first, allowing easier configuration override.
- Improved the gateway's private host check to correctly handle IPv6 addresses with or without brackets and ports.
### Deprecated
### Removed
### Fixed
\n
## [0.53.14] - 2025-11-03
### Added
- Added a new `install-hooks` target to the Makefile to easily set up git hooks.
- Added a script (`scripts/install-hooks.sh`) to copy git hooks from `.githooks` to `.git/hooks`.
### Changed
- Improved the pre-push git hook to automatically commit the updated `CHANGELOG.md` and `Makefile` after generating the changelog.
- Updated the changelog generation script (`scripts/update_changelog.sh`) to load the OpenRouter API key from the `.env` file or environment variables, improving security and configuration.
- Modified the pre-push hook to read user confirmation from `/dev/tty` for better compatibility in various terminal environments.
### Deprecated
### Removed
### Fixed
\n
## [0.53.8] - 2025-10-31
### Added
- **HTTPS/ACME Support**: Gateway now supports automatic HTTPS with Let's Encrypt certificates via ACME
- Interactive domain configuration during `network-cli setup` command
- Automatic port availability checking for ports 80 and 443 before enabling HTTPS
- DNS resolution verification to ensure domain points to the server IP
- TLS certificate cache directory management (`~/.debros/tls-cache`)
- Gateway automatically serves HTTP (port 80) for ACME challenges and HTTPS (port 443) for traffic
- New gateway config fields: `enable_https`, `domain_name`, `tls_cache_dir`
- **Domain Validation**: Added domain name validation and DNS verification helpers in setup CLI
- **Port Checking**: Added port availability checking utilities to detect conflicts before HTTPS setup
### Changed
- Updated `generateGatewayConfigDirect` to include HTTPS configuration fields
- Enhanced gateway config parsing to support HTTPS settings with validation
- Modified gateway startup to handle both HTTP-only and HTTPS+ACME modes
- Gateway now automatically manages ACME certificate acquisition and renewal
### Fixed
- Improved error handling during HTTPS setup with clear messaging when ports are unavailable
- Enhanced DNS verification flow with better user feedback during setup
## [0.53.0] - 2025-10-31

View File

@ -19,9 +19,9 @@ test-e2e:
# Network - Distributed P2P Database System
# Makefile for development and build tasks
.PHONY: build clean test run-node run-node2 run-node3 run-example deps tidy fmt vet lint clear-ports
.PHONY: build clean test run-node run-node2 run-node3 run-example deps tidy fmt vet lint clear-ports install-hooks
VERSION := 0.53.3
VERSION := 0.53.18
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
LDFLAGS := -X 'main.version=$(VERSION)' -X 'main.commit=$(COMMIT)' -X 'main.date=$(DATE)'
@ -37,6 +37,11 @@ build: deps
go build -ldflags "$(LDFLAGS) -X 'github.com/DeBrosOfficial/network/pkg/gateway.BuildVersion=$(VERSION)' -X 'github.com/DeBrosOfficial/network/pkg/gateway.BuildCommit=$(COMMIT)' -X 'github.com/DeBrosOfficial/network/pkg/gateway.BuildTime=$(DATE)'" -o bin/gateway ./cmd/gateway
@echo "Build complete! Run ./bin/network-cli version"
# Install git hooks
install-hooks:
@echo "Installing git hooks..."
@bash scripts/install-hooks.sh
# Clean build artifacts
clean:
@echo "Cleaning build artifacts..."

View File

@ -108,6 +108,10 @@ func main() {
}
cli.HandleConnectCommand(args[0], timeout)
// RQLite commands
case "rqlite":
cli.HandleRQLiteCommand(args)
// Help
case "help", "--help", "-h":
showHelp()
@ -175,6 +179,9 @@ func showHelp() {
fmt.Printf("🗄️ Database:\n")
fmt.Printf(" query <sql> 🔐 Execute database query\n\n")
fmt.Printf("🔧 RQLite:\n")
fmt.Printf(" rqlite fix 🔧 Fix misconfigured join address and clean raft state\n\n")
fmt.Printf("📡 PubSub:\n")
fmt.Printf(" pubsub publish <topic> <msg> 🔐 Publish message\n")
fmt.Printf(" pubsub subscribe <topic> 🔐 Subscribe to topic\n")

View File

@ -3,6 +3,7 @@ package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/DeBrosOfficial/network/pkg/config"
@ -53,6 +54,9 @@ func parseGatewayConfig(logger *logging.ColoredLogger) *gateway.Config {
ClientNamespace string `yaml:"client_namespace"`
RQLiteDSN string `yaml:"rqlite_dsn"`
BootstrapPeers []string `yaml:"bootstrap_peers"`
EnableHTTPS bool `yaml:"enable_https"`
DomainName string `yaml:"domain_name"`
TLSCacheDir string `yaml:"tls_cache_dir"`
}
data, err := os.ReadFile(configPath)
@ -79,6 +83,9 @@ func parseGatewayConfig(logger *logging.ColoredLogger) *gateway.Config {
ClientNamespace: "default",
BootstrapPeers: nil,
RQLiteDSN: "",
EnableHTTPS: false,
DomainName: "",
TLSCacheDir: "",
}
if v := strings.TrimSpace(y.ListenAddr); v != "" {
@ -103,6 +110,21 @@ func parseGatewayConfig(logger *logging.ColoredLogger) *gateway.Config {
}
}
// HTTPS configuration
cfg.EnableHTTPS = y.EnableHTTPS
if v := strings.TrimSpace(y.DomainName); v != "" {
cfg.DomainName = v
}
if v := strings.TrimSpace(y.TLSCacheDir); v != "" {
cfg.TLSCacheDir = v
} else if cfg.EnableHTTPS {
// Default TLS cache directory if HTTPS is enabled but not specified
homeDir, err := os.UserHomeDir()
if err == nil {
cfg.TLSCacheDir = filepath.Join(homeDir, ".debros", "tls-cache")
}
}
// Validate configuration
if errs := cfg.ValidateConfig(); len(errs) > 0 {
fmt.Fprintf(os.Stderr, "\nGateway configuration errors (%d):\n", len(errs))

View File

@ -12,6 +12,7 @@ import (
"github.com/DeBrosOfficial/network/pkg/gateway"
"github.com/DeBrosOfficial/network/pkg/logging"
"go.uber.org/zap"
"golang.org/x/crypto/acme/autocert"
)
func setupLogger() *logging.ColoredLogger {
@ -42,6 +43,123 @@ func main() {
logger.ComponentInfo(logging.ComponentGeneral, "Creating HTTP server and routes...")
// Check if HTTPS is enabled
if cfg.EnableHTTPS && cfg.DomainName != "" {
logger.ComponentInfo(logging.ComponentGeneral, "HTTPS enabled with ACME",
zap.String("domain", cfg.DomainName),
zap.String("tls_cache_dir", cfg.TLSCacheDir),
)
// Set up ACME manager
manager := &autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(cfg.DomainName),
}
// Set cache directory if specified
if cfg.TLSCacheDir != "" {
manager.Cache = autocert.DirCache(cfg.TLSCacheDir)
logger.ComponentInfo(logging.ComponentGeneral, "Using TLS certificate cache",
zap.String("cache_dir", cfg.TLSCacheDir),
)
}
// Create HTTP server for ACME challenge (port 80)
httpServer := &http.Server{
Addr: ":80",
Handler: manager.HTTPHandler(nil), // Redirects all HTTP traffic to HTTPS except ACME challenge
}
// Create HTTPS server (port 443)
httpsServer := &http.Server{
Addr: ":443",
Handler: gw.Routes(),
TLSConfig: manager.TLSConfig(),
}
// Start HTTP server for ACME challenge
logger.ComponentInfo(logging.ComponentGeneral, "Starting HTTP server for ACME challenge on port 80...")
httpLn, err := net.Listen("tcp", ":80")
if err != nil {
logger.ComponentError(logging.ComponentGeneral, "failed to bind HTTP listen address (port 80)", zap.Error(err))
os.Exit(1)
}
logger.ComponentInfo(logging.ComponentGeneral, "HTTP listener bound", zap.String("listen_addr", httpLn.Addr().String()))
// Start HTTPS server
logger.ComponentInfo(logging.ComponentGeneral, "Starting HTTPS server on port 443...")
httpsLn, err := net.Listen("tcp", ":443")
if err != nil {
logger.ComponentError(logging.ComponentGeneral, "failed to bind HTTPS listen address (port 443)", zap.Error(err))
os.Exit(1)
}
logger.ComponentInfo(logging.ComponentGeneral, "HTTPS listener bound", zap.String("listen_addr", httpsLn.Addr().String()))
// Serve HTTP in a goroutine
httpServeErrCh := make(chan error, 1)
go func() {
if err := httpServer.Serve(httpLn); err != nil && err != http.ErrServerClosed {
httpServeErrCh <- err
return
}
httpServeErrCh <- nil
}()
// Serve HTTPS in a goroutine
httpsServeErrCh := make(chan error, 1)
go func() {
if err := httpsServer.ServeTLS(httpsLn, "", ""); err != nil && err != http.ErrServerClosed {
httpsServeErrCh <- err
return
}
httpsServeErrCh <- nil
}()
// Wait for termination signal or server error
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
select {
case sig := <-quit:
logger.ComponentInfo(logging.ComponentGeneral, "shutdown signal received", zap.String("signal", sig.String()))
case err := <-httpServeErrCh:
if err != nil {
logger.ComponentError(logging.ComponentGeneral, "HTTP server error", zap.Error(err))
} else {
logger.ComponentInfo(logging.ComponentGeneral, "HTTP server exited normally")
}
case err := <-httpsServeErrCh:
if err != nil {
logger.ComponentError(logging.ComponentGeneral, "HTTPS server error", zap.Error(err))
} else {
logger.ComponentInfo(logging.ComponentGeneral, "HTTPS server exited normally")
}
}
logger.ComponentInfo(logging.ComponentGeneral, "Shutting down gateway servers...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Shutdown HTTPS server
if err := httpsServer.Shutdown(ctx); err != nil {
logger.ComponentError(logging.ComponentGeneral, "HTTPS server shutdown error", zap.Error(err))
} else {
logger.ComponentInfo(logging.ComponentGeneral, "HTTPS server shutdown complete")
}
// Shutdown HTTP server
if err := httpServer.Shutdown(ctx); err != nil {
logger.ComponentError(logging.ComponentGeneral, "HTTP server shutdown error", zap.Error(err))
} else {
logger.ComponentInfo(logging.ComponentGeneral, "HTTP server shutdown complete")
}
logger.ComponentInfo(logging.ComponentGeneral, "Gateway shutdown complete")
return
}
// Standard HTTP server (no HTTPS)
server := &http.Server{
Addr: cfg.ListenAddr,
Handler: gw.Routes(),

View File

@ -1,151 +0,0 @@
package main
import (
"context"
"log"
"time"
"github.com/DeBrosOfficial/network/pkg/client"
)
func main() {
// Create client configuration
config := client.DefaultClientConfig("example_app")
config.BootstrapPeers = []string{
"/ip4/127.0.0.1/tcp/4001/p2p/QmBootstrap1",
}
// Create network client
networkClient, err := client.NewClient(config)
if err != nil {
log.Fatalf("Failed to create network client: %v", err)
}
// Connect to network
if err := networkClient.Connect(); err != nil {
log.Fatalf("Failed to connect to network: %v", err)
}
defer networkClient.Disconnect()
log.Printf("Connected to network successfully!")
// Example: Database operations
demonstrateDatabase(networkClient)
// Example: Pub/Sub messaging
demonstratePubSub(networkClient)
// Example: Network information
demonstrateNetworkInfo(networkClient)
log.Printf("Example completed successfully!")
}
func demonstrateDatabase(client client.NetworkClient) {
ctx := context.Background()
db := client.Database()
log.Printf("=== Database Operations ===")
// Create a table
schema := `
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY,
content TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
`
if err := db.CreateTable(ctx, schema); err != nil {
log.Printf("Error creating table: %v", err)
return
}
log.Printf("Table created successfully")
// Insert some data
insertSQL := "INSERT INTO messages (content) VALUES (?)"
result, err := db.Query(ctx, insertSQL, "Hello, distributed world!")
if err != nil {
log.Printf("Error inserting data: %v", err)
return
}
log.Printf("Data inserted, result: %+v", result)
// Query data
selectSQL := "SELECT * FROM messages"
result, err = db.Query(ctx, selectSQL)
if err != nil {
log.Printf("Error querying data: %v", err)
return
}
log.Printf("Query result: %+v", result)
}
func demonstratePubSub(client client.NetworkClient) {
ctx := context.Background()
pubsub := client.PubSub()
log.Printf("=== Pub/Sub Operations ===")
// Subscribe to a topic
topic := "notifications"
handler := func(topic string, data []byte) error {
log.Printf("Received message on topic '%s': %s", topic, string(data))
return nil
}
if err := pubsub.Subscribe(ctx, topic, handler); err != nil {
log.Printf("Error subscribing: %v", err)
return
}
log.Printf("Subscribed to topic: %s", topic)
// Publish a message
message := []byte("Hello from pub/sub!")
if err := pubsub.Publish(ctx, topic, message); err != nil {
log.Printf("Error publishing: %v", err)
return
}
log.Printf("Message published")
// Wait a bit for message delivery
time.Sleep(time.Millisecond * 100)
// List topics
topics, err := pubsub.ListTopics(ctx)
if err != nil {
log.Printf("Error listing topics: %v", err)
return
}
log.Printf("Subscribed topics: %v", topics)
}
func demonstrateNetworkInfo(client client.NetworkClient) {
ctx := context.Background()
network := client.Network()
log.Printf("=== Network Information ===")
// Get network status
status, err := network.GetStatus(ctx)
if err != nil {
log.Printf("Error getting status: %v", err)
return
}
log.Printf("Network status: %+v", status)
// Get peers
peers, err := network.GetPeers(ctx)
if err != nil {
log.Printf("Error getting peers: %v", err)
return
}
log.Printf("Connected peers: %+v", peers)
// Get client health
health, err := client.Health()
if err != nil {
log.Printf("Error getting health: %v", err)
return
}
log.Printf("Client health: %+v", health)
}

View File

@ -1,23 +0,0 @@
# DeBros Gateway TypeScript SDK (Minimal Example)
Minimal, dependency-light wrapper around the HTTP Gateway.
Usage:
```bash
npm i
export GATEWAY_BASE_URL=http://127.0.0.1:6001
export GATEWAY_API_KEY=your_api_key
```
```ts
import { GatewayClient } from './src/client';
const c = new GatewayClient(process.env.GATEWAY_BASE_URL!, process.env.GATEWAY_API_KEY!);
await c.createTable('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
await c.transaction([
'INSERT INTO users (id,name) VALUES (1,\'Alice\')'
]);
const res = await c.query('SELECT name FROM users WHERE id = ?', [1]);
console.log(res.rows);
```

View File

@ -1,17 +0,0 @@
{
"name": "debros-gateway-sdk",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc -p tsconfig.json"
},
"dependencies": {
"isomorphic-ws": "^5.0.0"
},
"devDependencies": {
"typescript": "^5.5.4"
}
}

View File

@ -1,154 +0,0 @@
import WebSocket from "isomorphic-ws";
export class GatewayClient {
constructor(
private baseUrl: string,
private apiKey: string,
private http = fetch
) {}
private headers(json = true): Record<string, string> {
const h: Record<string, string> = { "X-API-Key": this.apiKey };
if (json) h["Content-Type"] = "application/json";
return h;
}
// Database
async createTable(schema: string): Promise<void> {
const r = await this.http(`${this.baseUrl}/v1/rqlite/create-table`, {
method: "POST",
headers: this.headers(),
body: JSON.stringify({ schema }),
});
if (!r.ok) throw new Error(`createTable failed: ${r.status}`);
}
async dropTable(table: string): Promise<void> {
const r = await this.http(`${this.baseUrl}/v1/rqlite/drop-table`, {
method: "POST",
headers: this.headers(),
body: JSON.stringify({ table }),
});
if (!r.ok) throw new Error(`dropTable failed: ${r.status}`);
}
async query<T = any>(sql: string, args: any[] = []): Promise<{ rows: T[] }> {
const r = await this.http(`${this.baseUrl}/v1/rqlite/query`, {
method: "POST",
headers: this.headers(),
body: JSON.stringify({ sql, args }),
});
if (!r.ok) throw new Error(`query failed: ${r.status}`);
return r.json();
}
async transaction(statements: string[]): Promise<void> {
const r = await this.http(`${this.baseUrl}/v1/rqlite/transaction`, {
method: "POST",
headers: this.headers(),
body: JSON.stringify({ statements }),
});
if (!r.ok) throw new Error(`transaction failed: ${r.status}`);
}
async schema(): Promise<any> {
const r = await this.http(`${this.baseUrl}/v1/rqlite/schema`, {
headers: this.headers(false),
});
if (!r.ok) throw new Error(`schema failed: ${r.status}`);
return r.json();
}
// Storage
async put(key: string, value: Uint8Array | string): Promise<void> {
const body =
typeof value === "string" ? new TextEncoder().encode(value) : value;
const r = await this.http(
`${this.baseUrl}/v1/storage/put?key=${encodeURIComponent(key)}`,
{
method: "POST",
headers: { "X-API-Key": this.apiKey },
body,
}
);
if (!r.ok) throw new Error(`put failed: ${r.status}`);
}
async get(key: string): Promise<Uint8Array> {
const r = await this.http(
`${this.baseUrl}/v1/storage/get?key=${encodeURIComponent(key)}`,
{
headers: { "X-API-Key": this.apiKey },
}
);
if (!r.ok) throw new Error(`get failed: ${r.status}`);
const buf = new Uint8Array(await r.arrayBuffer());
return buf;
}
async exists(key: string): Promise<boolean> {
const r = await this.http(
`${this.baseUrl}/v1/storage/exists?key=${encodeURIComponent(key)}`,
{
headers: this.headers(false),
}
);
if (!r.ok) throw new Error(`exists failed: ${r.status}`);
const j = await r.json();
return !!j.exists;
}
async list(prefix = ""): Promise<string[]> {
const r = await this.http(
`${this.baseUrl}/v1/storage/list?prefix=${encodeURIComponent(prefix)}`,
{
headers: this.headers(false),
}
);
if (!r.ok) throw new Error(`list failed: ${r.status}`);
const j = await r.json();
return j.keys || [];
}
async delete(key: string): Promise<void> {
const r = await this.http(`${this.baseUrl}/v1/storage/delete`, {
method: "POST",
headers: this.headers(),
body: JSON.stringify({ key }),
});
if (!r.ok) throw new Error(`delete failed: ${r.status}`);
}
// PubSub (minimal)
subscribe(
topic: string,
onMessage: (data: Uint8Array) => void
): { close: () => void } {
const url = new URL(`${this.baseUrl.replace(/^http/, "ws")}/v1/pubsub/ws`);
url.searchParams.set("topic", topic);
const ws = new WebSocket(url.toString(), {
headers: { "X-API-Key": this.apiKey },
} as any);
ws.binaryType = "arraybuffer";
ws.onmessage = (ev: any) => {
const data =
ev.data instanceof ArrayBuffer
? new Uint8Array(ev.data)
: new TextEncoder().encode(String(ev.data));
onMessage(data);
};
return { close: () => ws.close() };
}
async publish(topic: string, data: Uint8Array | string): Promise<void> {
const bytes =
typeof data === "string" ? new TextEncoder().encode(data) : data;
const b64 = Buffer.from(bytes).toString("base64");
const r = await this.http(`${this.baseUrl}/v1/pubsub/publish`, {
method: "POST",
headers: this.headers(),
body: JSON.stringify({ topic, data_base64: b64 }),
});
if (!r.ok) throw new Error(`publish failed: ${r.status}`);
}
}

View File

@ -1,12 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"declaration": true,
"outDir": "dist",
"rootDir": "src",
"strict": true,
"moduleResolution": "Node"
},
"include": ["src/**/*"]
}

View File

@ -318,7 +318,7 @@ func initFullStack(force bool) {
os.Exit(1)
}
}
node2Content := GenerateNodeConfig(node2Name, "", 4002, 5002, 7002, "localhost:7001", bootstrapMultiaddr)
node2Content := GenerateNodeConfig(node2Name, "", 4002, 5002, 7002, "localhost:5001", bootstrapMultiaddr)
if err := os.WriteFile(node2Path, []byte(node2Content), 0644); err != nil {
fmt.Fprintf(os.Stderr, "Failed to write node2 config: %v\n", err)
os.Exit(1)
@ -334,7 +334,7 @@ func initFullStack(force bool) {
os.Exit(1)
}
}
node3Content := GenerateNodeConfig(node3Name, "", 4003, 5003, 7003, "localhost:7001", bootstrapMultiaddr)
node3Content := GenerateNodeConfig(node3Name, "", 4003, 5003, 7003, "localhost:5001", bootstrapMultiaddr)
if err := os.WriteFile(node3Path, []byte(node3Content), 0644); err != nil {
fmt.Fprintf(os.Stderr, "Failed to write node3 config: %v\n", err)
os.Exit(1)

327
pkg/cli/rqlite_commands.go Normal file
View File

@ -0,0 +1,327 @@
package cli
import (
"fmt"
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/DeBrosOfficial/network/pkg/config"
"gopkg.in/yaml.v3"
)
// HandleRQLiteCommand handles rqlite-related commands
func HandleRQLiteCommand(args []string) {
if len(args) == 0 {
showRQLiteHelp()
return
}
if runtime.GOOS != "linux" {
fmt.Fprintf(os.Stderr, "❌ RQLite commands are only supported on Linux\n")
os.Exit(1)
}
subcommand := args[0]
subargs := args[1:]
switch subcommand {
case "fix":
handleRQLiteFix(subargs)
case "help":
showRQLiteHelp()
default:
fmt.Fprintf(os.Stderr, "Unknown rqlite subcommand: %s\n", subcommand)
showRQLiteHelp()
os.Exit(1)
}
}
func showRQLiteHelp() {
fmt.Printf("🗄️ RQLite Commands\n\n")
fmt.Printf("Usage: network-cli rqlite <subcommand> [options]\n\n")
fmt.Printf("Subcommands:\n")
fmt.Printf(" fix - Fix misconfigured join address and clean stale raft state\n\n")
fmt.Printf("Description:\n")
fmt.Printf(" The 'fix' command automatically repairs common rqlite cluster issues:\n")
fmt.Printf(" - Corrects join address from HTTP port (5001) to Raft port (7001) if misconfigured\n")
fmt.Printf(" - Cleans stale raft state that prevents proper cluster formation\n")
fmt.Printf(" - Restarts the node service with corrected configuration\n\n")
fmt.Printf("Requirements:\n")
fmt.Printf(" - Must be run as root (use sudo)\n")
fmt.Printf(" - Only works on non-bootstrap nodes (nodes with join_address configured)\n")
fmt.Printf(" - Stops and restarts the debros-node service\n\n")
fmt.Printf("Examples:\n")
fmt.Printf(" sudo network-cli rqlite fix\n")
}
func handleRQLiteFix(args []string) {
requireRoot()
// Parse optional flags
dryRun := false
for _, arg := range args {
if arg == "--dry-run" || arg == "-n" {
dryRun = true
}
}
if dryRun {
fmt.Printf("🔍 Dry-run mode - no changes will be made\n\n")
}
fmt.Printf("🔧 RQLite Cluster Repair\n\n")
// Load config
configPath, err := config.DefaultPath("node.yaml")
if err != nil {
fmt.Fprintf(os.Stderr, "❌ Failed to determine config path: %v\n", err)
os.Exit(1)
}
cfg, err := loadConfigForRepair(configPath)
if err != nil {
fmt.Fprintf(os.Stderr, "❌ Failed to load config: %v\n", err)
os.Exit(1)
}
// Check if this is a bootstrap node
if cfg.Node.Type == "bootstrap" || cfg.Database.RQLiteJoinAddress == "" {
fmt.Printf(" This is a bootstrap node (no join address configured)\n")
fmt.Printf(" Bootstrap nodes don't need repair - they are the cluster leader\n")
fmt.Printf(" Run this command on follower nodes instead\n")
return
}
joinAddr := cfg.Database.RQLiteJoinAddress
// Check if join address needs fixing
needsConfigFix := needsFix(joinAddr, cfg.Database.RQLiteRaftPort, cfg.Database.RQLitePort)
var fixedAddr string
if needsConfigFix {
fmt.Printf("⚠️ Detected misconfigured join address: %s\n", joinAddr)
fmt.Printf(" Expected Raft port (%d) but found HTTP port (%d)\n", cfg.Database.RQLiteRaftPort, cfg.Database.RQLitePort)
// Extract host from join address
host, _, err := parseJoinAddress(joinAddr)
if err != nil {
fmt.Fprintf(os.Stderr, "❌ Failed to parse join address: %v\n", err)
os.Exit(1)
}
// Fix the join address - rqlite expects Raft port for -join
fixedAddr = fmt.Sprintf("%s:%d", host, cfg.Database.RQLiteRaftPort)
fmt.Printf(" Corrected address: %s\n\n", fixedAddr)
} else {
fmt.Printf("✅ Join address looks correct: %s\n", joinAddr)
fmt.Printf(" Will clean stale raft state to ensure proper cluster formation\n\n")
fixedAddr = joinAddr // No change needed
}
if dryRun {
fmt.Printf("🔍 Dry-run: Would clean raft state")
if needsConfigFix {
fmt.Printf(" and fix config")
}
fmt.Printf("\n")
return
}
// Stop the service
fmt.Printf("⏹️ Stopping debros-node service...\n")
if err := stopService("debros-node"); err != nil {
fmt.Fprintf(os.Stderr, "❌ Failed to stop service: %v\n", err)
os.Exit(1)
}
fmt.Printf(" ✓ Service stopped\n\n")
// Update config file if needed
if needsConfigFix {
fmt.Printf("📝 Updating configuration file...\n")
if err := updateConfigJoinAddress(configPath, fixedAddr); err != nil {
fmt.Fprintf(os.Stderr, "❌ Failed to update config: %v\n", err)
fmt.Fprintf(os.Stderr, " Service is stopped - please fix manually and restart\n")
os.Exit(1)
}
fmt.Printf(" ✓ Config updated: %s\n\n", configPath)
}
// Clean raft state
fmt.Printf("🧹 Cleaning stale raft state...\n")
dataDir := expandDataDir(cfg.Node.DataDir)
raftDir := filepath.Join(dataDir, "rqlite", "raft")
if err := cleanRaftState(raftDir); err != nil {
fmt.Fprintf(os.Stderr, "⚠️ Failed to clean raft state: %v\n", err)
fmt.Fprintf(os.Stderr, " Continuing anyway - raft state may still exist\n")
} else {
fmt.Printf(" ✓ Raft state cleaned\n\n")
}
// Restart the service
fmt.Printf("🚀 Restarting debros-node service...\n")
if err := startService("debros-node"); err != nil {
fmt.Fprintf(os.Stderr, "❌ Failed to start service: %v\n", err)
fmt.Fprintf(os.Stderr, " Config has been fixed - please restart manually:\n")
fmt.Fprintf(os.Stderr, " sudo systemctl start debros-node\n")
os.Exit(1)
}
fmt.Printf(" ✓ Service started\n\n")
fmt.Printf("✅ Repair complete!\n\n")
fmt.Printf("The node should now join the cluster correctly.\n")
fmt.Printf("Monitor logs with: sudo network-cli service logs node --follow\n")
}
func loadConfigForRepair(path string) (*config.Config, error) {
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("failed to open config file: %w", err)
}
defer file.Close()
var cfg config.Config
if err := config.DecodeStrict(file, &cfg); err != nil {
return nil, fmt.Errorf("failed to parse config: %w", err)
}
return &cfg, nil
}
func needsFix(joinAddr string, raftPort int, httpPort int) bool {
if joinAddr == "" {
return false
}
// Remove http:// or https:// prefix if present
addr := joinAddr
if strings.HasPrefix(addr, "http://") {
addr = strings.TrimPrefix(addr, "http://")
} else if strings.HasPrefix(addr, "https://") {
addr = strings.TrimPrefix(addr, "https://")
}
// Parse host:port
_, port, err := net.SplitHostPort(addr)
if err != nil {
return false // Can't parse, assume it's fine
}
// Check if port matches HTTP port (incorrect - should be Raft port)
if port == fmt.Sprintf("%d", httpPort) {
return true
}
// If it matches Raft port, it's correct
if port == fmt.Sprintf("%d", raftPort) {
return false
}
// Unknown port - assume it's fine
return false
}
func parseJoinAddress(joinAddr string) (host, port string, err error) {
// Remove http:// or https:// prefix if present
addr := joinAddr
if strings.HasPrefix(addr, "http://") {
addr = strings.TrimPrefix(addr, "http://")
} else if strings.HasPrefix(addr, "https://") {
addr = strings.TrimPrefix(addr, "https://")
}
host, port, err = net.SplitHostPort(addr)
if err != nil {
return "", "", fmt.Errorf("invalid join address format: %w", err)
}
return host, port, nil
}
func updateConfigJoinAddress(configPath string, newJoinAddr string) error {
// Read the file
data, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("failed to read config file: %w", err)
}
// Parse YAML into a generic map to preserve structure
var yamlData map[string]interface{}
if err := yaml.Unmarshal(data, &yamlData); err != nil {
return fmt.Errorf("failed to parse YAML: %w", err)
}
// Navigate to database.rqlite_join_address
database, ok := yamlData["database"].(map[string]interface{})
if !ok {
return fmt.Errorf("database section not found in config")
}
database["rqlite_join_address"] = newJoinAddr
// Write back to file
updatedData, err := yaml.Marshal(yamlData)
if err != nil {
return fmt.Errorf("failed to marshal YAML: %w", err)
}
if err := os.WriteFile(configPath, updatedData, 0644); err != nil {
return fmt.Errorf("failed to write config file: %w", err)
}
return nil
}
func expandDataDir(dataDir string) string {
expanded := os.ExpandEnv(dataDir)
if strings.HasPrefix(expanded, "~") {
home, err := os.UserHomeDir()
if err != nil {
return expanded // Fallback to original
}
expanded = filepath.Join(home, expanded[1:])
}
return expanded
}
func cleanRaftState(raftDir string) error {
if _, err := os.Stat(raftDir); os.IsNotExist(err) {
return nil // Directory doesn't exist, nothing to clean
}
// Remove raft state files
filesToRemove := []string{
"peers.json",
"peers.json.backup",
"peers.info",
"raft.db",
}
for _, file := range filesToRemove {
filePath := filepath.Join(raftDir, file)
if err := os.Remove(filePath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove %s: %w", filePath, err)
}
}
return nil
}
func stopService(serviceName string) error {
cmd := exec.Command("systemctl", "stop", serviceName)
if err := cmd.Run(); err != nil {
return fmt.Errorf("systemctl stop failed: %w", err)
}
return nil
}
func startService(serviceName string) error {
cmd := exec.Command("systemctl", "start", serviceName)
if err := cmd.Run(); err != nil {
return fmt.Errorf("systemctl start failed: %w", err)
}
return nil
}

View File

@ -3,12 +3,16 @@ package cli
import (
"bufio"
"fmt"
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"github.com/DeBrosOfficial/network/pkg/config"
)
// HandleSetupCommand handles the interactive 'setup' command for VPS installation
@ -108,19 +112,179 @@ func HandleSetupCommand(args []string) {
fmt.Printf("✅ Setup Complete!\n")
fmt.Printf(strings.Repeat("=", 70) + "\n\n")
fmt.Printf("DeBros Network is now running!\n\n")
// Try to get and display peer ID
peerID := getPeerID()
if peerID != "" {
fmt.Printf("🆔 Node Peer ID: %s\n\n", peerID)
}
fmt.Printf("Service Management:\n")
fmt.Printf(" network-cli service status all\n")
fmt.Printf(" network-cli service logs node --follow\n")
fmt.Printf(" network-cli service restart gateway\n\n")
fmt.Printf("Access DeBros User:\n")
fmt.Printf(" sudo -u debros bash\n\n")
// Check if HTTPS is enabled
gatewayConfigPath := "/home/debros/.debros/gateway.yaml"
httpsEnabled := false
var domainName string
if data, err := os.ReadFile(gatewayConfigPath); err == nil {
var cfg config.Config
if err := config.DecodeStrict(strings.NewReader(string(data)), &cfg); err == nil {
// Try to parse as gateway config
if strings.Contains(string(data), "enable_https: true") {
httpsEnabled = true
// Extract domain name from config
lines := strings.Split(string(data), "\n")
for _, line := range lines {
if strings.HasPrefix(strings.TrimSpace(line), "domain_name:") {
parts := strings.Split(line, ":")
if len(parts) > 1 {
domainName = strings.Trim(strings.TrimSpace(parts[1]), "\"")
}
break
}
}
}
}
}
fmt.Printf("Verify Installation:\n")
fmt.Printf(" curl http://localhost:6001/health\n")
if httpsEnabled && domainName != "" {
fmt.Printf(" curl https://%s/health\n", domainName)
fmt.Printf(" curl http://localhost:6001/health (HTTP fallback)\n")
} else {
fmt.Printf(" curl http://localhost:6001/health\n")
}
fmt.Printf(" curl http://localhost:5001/status\n\n")
if httpsEnabled && domainName != "" {
fmt.Printf("HTTPS Configuration:\n")
fmt.Printf(" Domain: %s\n", domainName)
fmt.Printf(" HTTPS endpoint: https://%s\n", domainName)
fmt.Printf(" Certificate cache: /home/debros/.debros/tls-cache\n")
fmt.Printf(" Certificates are automatically managed via Let's Encrypt (ACME)\n\n")
}
fmt.Printf("Anyone Relay (Anon):\n")
fmt.Printf(" sudo systemctl status anon\n")
fmt.Printf(" sudo tail -f /home/debros/.debros/logs/anon/notices.log\n")
fmt.Printf(" Proxy endpoint: POST http://localhost:6001/v1/proxy/anon\n\n")
if httpsEnabled && domainName != "" {
fmt.Printf(" Proxy endpoint: POST https://%s/v1/proxy/anon\n\n", domainName)
} else {
fmt.Printf(" Proxy endpoint: POST http://localhost:6001/v1/proxy/anon\n\n")
}
}
// extractIPFromMultiaddr extracts the IP address from a multiaddr string
// Format: /ip4/51.83.128.181/tcp/4001/p2p/12D3KooW...
func extractIPFromMultiaddr(multiaddr string) string {
if multiaddr == "" {
return ""
}
// Split by "/ip4/"
parts := strings.Split(multiaddr, "/ip4/")
if len(parts) < 2 {
return ""
}
// Get the part after "/ip4/"
ipPart := parts[1]
// Extract IP until the next "/"
ipEnd := strings.Index(ipPart, "/")
if ipEnd == -1 {
// If no "/" found, the whole string might be the IP
return strings.TrimSpace(ipPart)
}
ip := strings.TrimSpace(ipPart[:ipEnd])
// Validate it looks like an IP address
if net.ParseIP(ip) != nil {
return ip
}
return ""
}
// getVPSIPv4Address gets the primary IPv4 address of the VPS
func getVPSIPv4Address() (string, error) {
interfaces, err := net.Interfaces()
if err != nil {
return "", err
}
for _, iface := range interfaces {
// Skip loopback and down interfaces
if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 {
continue
}
addrs, err := iface.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
ipNet, ok := addr.(*net.IPNet)
if !ok {
continue
}
ip := ipNet.IP
// Check if it's IPv4 and not a loopback address
if ip.To4() != nil && !ip.IsLoopback() {
return ip.String(), nil
}
}
}
return "", fmt.Errorf("could not find a non-loopback IPv4 address")
}
// getPeerID attempts to retrieve the peer ID from peer.info based on node type
func getPeerID() string {
debrosDir := "/home/debros/.debros"
nodeConfigPath := filepath.Join(debrosDir, "node.yaml")
// Determine node type from config
var nodeType string
if file, err := os.Open(nodeConfigPath); err == nil {
defer file.Close()
var cfg config.Config
if err := config.DecodeStrict(file, &cfg); err == nil {
nodeType = cfg.Node.Type
}
}
// Determine the peer.info path based on node type
var peerInfoPath string
if nodeType == "bootstrap" {
peerInfoPath = filepath.Join(debrosDir, "bootstrap", "peer.info")
} else {
// Default to "node" directory for regular nodes
peerInfoPath = filepath.Join(debrosDir, "node", "peer.info")
}
// Try to read from peer.info file
if data, err := os.ReadFile(peerInfoPath); err == nil {
peerInfo := strings.TrimSpace(string(data))
// Extract peer ID from multiaddr format: /ip4/.../p2p/<peer-id>
if strings.Contains(peerInfo, "/p2p/") {
parts := strings.Split(peerInfo, "/p2p/")
if len(parts) == 2 {
return strings.TrimSpace(parts[1])
}
}
// If it's just the peer ID, return it
if len(peerInfo) > 0 && !strings.Contains(peerInfo, "/") {
return peerInfo
}
}
return ""
}
func detectLinuxDistro() string {
@ -214,6 +378,206 @@ func isValidHostPort(s string) bool {
return true
}
// isPortAvailable checks if a port is available for binding
func isPortAvailable(port int) bool {
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
return false
}
ln.Close()
return true
}
// checkPorts80And443 checks if ports 80 and 443 are available
func checkPorts80And443() (bool, string) {
port80Available := isPortAvailable(80)
port443Available := isPortAvailable(443)
if !port80Available || !port443Available {
var issues []string
if !port80Available {
issues = append(issues, "port 80")
}
if !port443Available {
issues = append(issues, "port 443")
}
return false, strings.Join(issues, " and ")
}
return true, ""
}
// isValidDomain validates a domain name format
func isValidDomain(domain string) bool {
domain = strings.TrimSpace(domain)
if domain == "" {
return false
}
// Basic validation: domain should contain at least one dot
// and not start/end with dot or hyphen
if !strings.Contains(domain, ".") {
return false
}
if strings.HasPrefix(domain, ".") || strings.HasSuffix(domain, ".") {
return false
}
if strings.HasPrefix(domain, "-") || strings.HasSuffix(domain, "-") {
return false
}
// Check for valid characters (letters, numbers, dots, hyphens)
for _, char := range domain {
if !((char >= 'a' && char <= 'z') ||
(char >= 'A' && char <= 'Z') ||
(char >= '0' && char <= '9') ||
char == '.' ||
char == '-') {
return false
}
}
return true
}
// verifyDNSResolution verifies that a domain resolves to the VPS IP
func verifyDNSResolution(domain, expectedIP string) bool {
ips, err := net.LookupIP(domain)
if err != nil {
return false
}
for _, ip := range ips {
if ip.To4() != nil && ip.String() == expectedIP {
return true
}
}
return false
}
// promptDomainForHTTPS prompts for domain name and verifies DNS configuration
func promptDomainForHTTPS(reader *bufio.Reader, vpsIP string) string {
for {
fmt.Printf("\nEnter your domain name (e.g., example.com): ")
domainInput, _ := reader.ReadString('\n')
domain := strings.TrimSpace(domainInput)
if domain == "" {
fmt.Printf(" Domain name cannot be empty. Skipping HTTPS configuration.\n")
return ""
}
if !isValidDomain(domain) {
fmt.Printf(" ❌ Invalid domain format. Please enter a valid domain name.\n")
continue
}
// Verify DNS is configured
fmt.Printf("\n Verifying DNS configuration...\n")
fmt.Printf(" Please ensure your domain %s points to this server's IP (%s)\n", domain, vpsIP)
fmt.Printf(" Have you configured the DNS record? (yes/no): ")
dnsResponse, _ := reader.ReadString('\n')
dnsResponse = strings.ToLower(strings.TrimSpace(dnsResponse))
if dnsResponse == "yes" || dnsResponse == "y" {
// Try to verify DNS resolution
fmt.Printf(" Checking DNS resolution...\n")
if verifyDNSResolution(domain, vpsIP) {
fmt.Printf(" ✓ DNS is correctly configured\n")
return domain
} else {
fmt.Printf(" ⚠️ DNS does not resolve to this server's IP (%s)\n", vpsIP)
fmt.Printf(" DNS may still be propagating. Continue anyway? (yes/no): ")
continueResponse, _ := reader.ReadString('\n')
continueResponse = strings.ToLower(strings.TrimSpace(continueResponse))
if continueResponse == "yes" || continueResponse == "y" {
fmt.Printf(" Continuing with domain configuration (DNS may need time to propagate)\n")
return domain
}
// User chose not to continue, ask for domain again
fmt.Printf(" Please configure DNS and try again, or press Enter to skip HTTPS\n")
continue
}
} else {
fmt.Printf(" Please configure DNS first. Type 'skip' to skip HTTPS configuration: ")
skipResponse, _ := reader.ReadString('\n')
skipResponse = strings.ToLower(strings.TrimSpace(skipResponse))
if skipResponse == "skip" {
return ""
}
continue
}
}
}
// updateGatewayConfigWithHTTPS updates an existing gateway.yaml file with HTTPS settings
func updateGatewayConfigWithHTTPS(gatewayPath, domain string) error {
// Read existing config
data, err := os.ReadFile(gatewayPath)
if err != nil {
return fmt.Errorf("failed to read gateway config: %w", err)
}
configContent := string(data)
tlsCacheDir := "/home/debros/.debros/tls-cache"
// Check if HTTPS is already enabled
if strings.Contains(configContent, "enable_https: true") {
// Update existing HTTPS settings
lines := strings.Split(configContent, "\n")
var updatedLines []string
domainUpdated := false
cacheDirUpdated := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "enable_https:") {
updatedLines = append(updatedLines, "enable_https: true")
} else if strings.HasPrefix(trimmed, "domain_name:") {
updatedLines = append(updatedLines, fmt.Sprintf("domain_name: \"%s\"", domain))
domainUpdated = true
} else if strings.HasPrefix(trimmed, "tls_cache_dir:") {
updatedLines = append(updatedLines, fmt.Sprintf("tls_cache_dir: \"%s\"", tlsCacheDir))
cacheDirUpdated = true
} else {
updatedLines = append(updatedLines, line)
}
}
// Add missing fields if not found
if !domainUpdated {
updatedLines = append(updatedLines, fmt.Sprintf("domain_name: \"%s\"", domain))
}
if !cacheDirUpdated {
updatedLines = append(updatedLines, fmt.Sprintf("tls_cache_dir: \"%s\"", tlsCacheDir))
}
configContent = strings.Join(updatedLines, "\n")
} else {
// Add HTTPS configuration at the end
configContent = strings.TrimRight(configContent, "\n")
if !strings.HasSuffix(configContent, "\n") && configContent != "" {
configContent += "\n"
}
configContent += "enable_https: true\n"
configContent += fmt.Sprintf("domain_name: \"%s\"\n", domain)
configContent += fmt.Sprintf("tls_cache_dir: \"%s\"\n", tlsCacheDir)
}
// Write updated config
if err := os.WriteFile(gatewayPath, []byte(configContent), 0644); err != nil {
return fmt.Errorf("failed to write gateway config: %w", err)
}
// Fix ownership
exec.Command("chown", "debros:debros", gatewayPath).Run()
return nil
}
func setupDebrosUser() {
fmt.Printf("👤 Setting up 'debros' user...\n")
@ -704,38 +1068,37 @@ func cloneAndBuild() {
branch := promptBranch()
fmt.Printf(" Using branch: %s\n", branch)
// Check if already cloned
if _, err := os.Stat("/home/debros/src/.git"); err == nil {
fmt.Printf(" Updating repository...\n")
// Check current branch and switch if needed
currentBranchCmd := exec.Command("sudo", "-u", "debros", "git", "-C", "/home/debros/src", "rev-parse", "--abbrev-ref", "HEAD")
if output, err := currentBranchCmd.Output(); err == nil {
currentBranch := strings.TrimSpace(string(output))
if currentBranch != branch {
fmt.Printf(" Switching from %s to %s...\n", currentBranch, branch)
// Fetch the target branch first (needed for shallow clones)
exec.Command("sudo", "-u", "debros", "git", "-C", "/home/debros/src", "fetch", "origin", branch).Run()
// Checkout the selected branch
checkoutCmd := exec.Command("sudo", "-u", "debros", "git", "-C", "/home/debros/src", "checkout", branch)
if err := checkoutCmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "⚠️ Failed to switch branch: %v\n", err)
}
// Remove existing repository if it exists (always start fresh)
if _, err := os.Stat("/home/debros/src"); err == nil {
fmt.Printf(" Removing existing repository...\n")
// Remove as root since we're running as root
if err := os.RemoveAll("/home/debros/src"); err != nil {
fmt.Fprintf(os.Stderr, "⚠️ Failed to remove existing repo as root: %v\n", err)
// Try as debros user as fallback (might work if files are owned by debros)
removeCmd := exec.Command("sudo", "-u", "debros", "rm", "-rf", "/home/debros/src")
if output, err := removeCmd.CombinedOutput(); err != nil {
fmt.Fprintf(os.Stderr, "⚠️ Failed to remove existing repo as debros user: %v\n%s\n", err, output)
}
}
// Wait a moment to ensure filesystem syncs
time.Sleep(100 * time.Millisecond)
}
// Pull latest changes
cmd := exec.Command("sudo", "-u", "debros", "git", "-C", "/home/debros/src", "pull", "origin", branch)
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "⚠️ Failed to update repo: %v\n", err)
}
} else {
fmt.Printf(" Cloning repository...\n")
cmd := exec.Command("sudo", "-u", "debros", "git", "clone", "--branch", branch, "--depth", "1", "https://github.com/DeBrosOfficial/network.git", "/home/debros/src")
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "❌ Failed to clone repo: %v\n", err)
os.Exit(1)
}
// Ensure parent directory exists and has correct permissions
if err := os.MkdirAll("/home/debros", 0755); err != nil {
fmt.Fprintf(os.Stderr, "⚠️ Failed to ensure debros home directory exists: %v\n", err)
os.Exit(1)
}
if err := exec.Command("chown", "debros:debros", "/home/debros").Run(); err != nil {
fmt.Fprintf(os.Stderr, "⚠️ Failed to chown debros home directory: %v\n", err)
}
// Clone fresh repository
fmt.Printf(" Cloning repository...\n")
cmd := exec.Command("sudo", "-u", "debros", "git", "clone", "--branch", branch, "--depth", "1", "https://github.com/DeBrosOfficial/network.git", "/home/debros/src")
if output, err := cmd.CombinedOutput(); err != nil {
fmt.Fprintf(os.Stderr, "❌ Failed to clone repo: %v\n%s\n", err, output)
os.Exit(1)
}
// Build
@ -746,7 +1109,7 @@ func cloneAndBuild() {
// Use sudo with --preserve-env=PATH to pass Go path to debros user
// Set HOME so Go knows where to create module cache
cmd := exec.Command("sudo", "--preserve-env=PATH", "-u", "debros", "make", "build")
cmd = exec.Command("sudo", "--preserve-env=PATH", "-u", "debros", "make", "build")
cmd.Dir = "/home/debros/src"
cmd.Env = append(os.Environ(), "HOME=/home/debros", "PATH="+os.Getenv("PATH")+":/usr/local/go/bin")
if output, err := cmd.CombinedOutput(); err != nil {
@ -755,92 +1118,176 @@ func cloneAndBuild() {
}
// Copy binaries
exec.Command("sh", "-c", "cp -r /home/debros/src/bin/* /home/debros/bin/").Run()
exec.Command("chown", "-R", "debros:debros", "/home/debros/bin").Run()
exec.Command("chmod", "-R", "755", "/home/debros/bin").Run()
copyCmd := exec.Command("sh", "-c", "cp -r /home/debros/src/bin/* /home/debros/bin/")
if output, err := copyCmd.CombinedOutput(); err != nil {
fmt.Fprintf(os.Stderr, "❌ Failed to copy binaries: %v\n%s\n", err, output)
os.Exit(1)
}
chownCmd := exec.Command("chown", "-R", "debros:debros", "/home/debros/bin")
if err := chownCmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "⚠️ Failed to chown binaries: %v\n", err)
}
chmodCmd := exec.Command("chmod", "-R", "755", "/home/debros/bin")
if err := chmodCmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "⚠️ Failed to chmod binaries: %v\n", err)
}
fmt.Printf(" ✓ Built and installed\n")
}
func generateConfigsInteractive(force bool) {
fmt.Printf("⚙️ Generating configurations...\n")
fmt.Printf("⚙️ Generating configurations...\n\n")
// For single-node VPS setup, use sensible defaults
// This creates a bootstrap node that acts as the cluster leader
fmt.Printf("\n")
fmt.Printf("Setting up single-node configuration...\n")
fmt.Printf(" • Bootstrap node (cluster leader)\n")
fmt.Printf(" • No external peers required\n")
fmt.Printf(" • Gateway connected to local node\n\n")
bootstrapPath := "/home/debros/.debros/bootstrap.yaml"
nodeConfigPath := "/home/debros/.debros/node.yaml"
gatewayPath := "/home/debros/.debros/gateway.yaml"
// Check if node.yaml already exists
// Check if configs already exist
nodeExists := false
gatewayExists := false
if _, err := os.Stat(nodeConfigPath); err == nil {
nodeExists = true
fmt.Printf(" node.yaml already exists, will not overwrite\n")
}
if _, err := os.Stat(gatewayPath); err == nil {
gatewayExists = true
}
// Generate bootstrap node config with explicit parameters
// Pass empty bootstrap-peers and no join address for bootstrap node
bootstrapArgs := []string{
"-u", "debros",
"/home/debros/bin/network-cli", "config", "init",
"--type", "bootstrap",
"--bootstrap-peers", "",
}
if force {
bootstrapArgs = append(bootstrapArgs, "--force")
}
// If both configs exist and not forcing, skip configuration prompts
if nodeExists && gatewayExists && !force {
fmt.Printf(" Configuration files already exist (node.yaml and gateway.yaml)\n")
fmt.Printf(" Skipping configuration generation\n\n")
cmd := exec.Command("sudo", bootstrapArgs...)
cmd.Stdin = nil // Explicitly close stdin to prevent interactive prompts
output, err := cmd.CombinedOutput()
bootstrapCreated := (err == nil)
// Only offer to add HTTPS if not already enabled
httpsAlreadyEnabled := false
if data, err := os.ReadFile(gatewayPath); err == nil {
httpsAlreadyEnabled = strings.Contains(string(data), "enable_https: true")
}
if err != nil {
// Check if bootstrap.yaml already exists (config init failed because it exists)
if _, statErr := os.Stat(bootstrapPath); statErr == nil {
fmt.Printf(" bootstrap.yaml already exists, skipping creation\n")
bootstrapCreated = true
if !httpsAlreadyEnabled {
fmt.Printf("🌐 Domain and HTTPS Configuration\n")
fmt.Printf("Would you like to add HTTPS with a domain name to your existing gateway config? (yes/no) [default: no]: ")
reader := bufio.NewReader(os.Stdin)
addHTTPSResponse, _ := reader.ReadString('\n')
addHTTPSResponse = strings.ToLower(strings.TrimSpace(addHTTPSResponse))
if addHTTPSResponse == "yes" || addHTTPSResponse == "y" {
// Get VPS IP for DNS verification
vpsIP, err := getVPSIPv4Address()
if err != nil {
fmt.Fprintf(os.Stderr, "⚠️ Failed to detect IPv4 address: %v\n", err)
fmt.Fprintf(os.Stderr, " Using 0.0.0.0 as fallback\n")
vpsIP = "0.0.0.0"
}
// Check if ports 80 and 443 are available
portsAvailable, portIssues := checkPorts80And443()
if !portsAvailable {
fmt.Fprintf(os.Stderr, "\n⚠ Cannot enable HTTPS: %s is already in use\n", portIssues)
fmt.Fprintf(os.Stderr, " You will need to configure HTTPS manually if you want to use a domain.\n\n")
} else {
// Prompt for domain and update existing config
domain := promptDomainForHTTPS(reader, vpsIP)
if domain != "" {
// Update existing gateway config with HTTPS settings
if err := updateGatewayConfigWithHTTPS(gatewayPath, domain); err != nil {
fmt.Fprintf(os.Stderr, "⚠️ Failed to update gateway config with HTTPS: %v\n", err)
} else {
fmt.Printf(" ✓ HTTPS configuration added to existing gateway.yaml\n")
// Create TLS cache directory
tlsCacheDir := "/home/debros/.debros/tls-cache"
if err := os.MkdirAll(tlsCacheDir, 0755); err == nil {
exec.Command("chown", "-R", "debros:debros", tlsCacheDir).Run()
fmt.Printf(" ✓ TLS cache directory created: %s\n", tlsCacheDir)
}
}
}
}
}
} else {
fmt.Fprintf(os.Stderr, "⚠️ Failed to generate bootstrap config: %v\n", err)
if len(output) > 0 {
fmt.Fprintf(os.Stderr, " Output: %s\n", string(output))
}
fmt.Printf(" HTTPS is already enabled in gateway.yaml\n")
}
fmt.Printf("\n ✓ Configurations ready\n")
return
}
// Get VPS IPv4 address
fmt.Printf("Detecting VPS IPv4 address...\n")
vpsIP, err := getVPSIPv4Address()
if err != nil {
fmt.Fprintf(os.Stderr, "⚠️ Failed to detect IPv4 address: %v\n", err)
fmt.Fprintf(os.Stderr, " Using 0.0.0.0 as fallback. You may need to edit config files manually.\n")
vpsIP = "0.0.0.0"
} else {
fmt.Printf(" ✓ Bootstrap node config created\n")
fmt.Printf(" ✓ Detected IPv4 address: %s\n\n", vpsIP)
}
// Rename bootstrap.yaml to node.yaml only if node.yaml doesn't exist
if !nodeExists && bootstrapCreated {
// Check if bootstrap.yaml exists before renaming
if _, err := os.Stat(bootstrapPath); err == nil {
renameCmd := exec.Command("sudo", "-u", "debros", "mv", bootstrapPath, nodeConfigPath)
if err := renameCmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "⚠️ Failed to rename config: %v\n", err)
// Ask about node type
fmt.Printf("What type of node is this?\n")
fmt.Printf(" 1. Bootstrap node (cluster leader)\n")
fmt.Printf(" 2. Regular node (joins existing cluster)\n")
fmt.Printf("Enter choice (1 or 2): ")
reader := bufio.NewReader(os.Stdin)
choice, _ := reader.ReadString('\n')
choice = strings.ToLower(strings.TrimSpace(choice))
isBootstrap := choice == "1" || choice == "bootstrap" || choice == "b"
var bootstrapPeers string
if !isBootstrap {
// Ask for bootstrap peer multiaddr
fmt.Printf("\nEnter bootstrap peer multiaddr(s) (comma-separated if multiple):\n")
fmt.Printf("Example: /ip4/192.168.1.100/tcp/4001/p2p/12D3KooW...\n")
fmt.Printf("Bootstrap peer(s): ")
bootstrapPeers, _ = reader.ReadString('\n')
bootstrapPeers = strings.TrimSpace(bootstrapPeers)
}
// Check if node.yaml already exists
if _, err := os.Stat(nodeConfigPath); err == nil {
nodeExists = true
if !force {
fmt.Printf("\n node.yaml already exists, will not overwrite\n")
}
}
// Generate node config
if !nodeExists || force {
var nodeConfig string
if isBootstrap {
nodeConfig = generateBootstrapConfigWithIP("bootstrap", "", 4001, 5001, 7001, vpsIP)
} else {
// Extract IP from bootstrap peer multiaddr for rqlite_join_address
// Use first bootstrap peer if multiple provided
const defaultRQLiteHTTPPort = 5001
var joinAddr string
if bootstrapPeers != "" {
firstPeer := strings.Split(bootstrapPeers, ",")[0]
firstPeer = strings.TrimSpace(firstPeer)
extractedIP := extractIPFromMultiaddr(firstPeer)
if extractedIP != "" {
joinAddr = fmt.Sprintf("%s:%d", extractedIP, defaultRQLiteHTTPPort)
} else {
joinAddr = fmt.Sprintf("localhost:%d", defaultRQLiteHTTPPort)
}
} else {
fmt.Printf(" ✓ Renamed bootstrap.yaml to node.yaml\n")
joinAddr = fmt.Sprintf("localhost:%d", defaultRQLiteHTTPPort)
}
nodeConfig = generateNodeConfigWithIP("node", "", 4001, 5001, 7001, joinAddr, bootstrapPeers, vpsIP)
}
} else if nodeExists {
// If node.yaml exists, we can optionally remove bootstrap.yaml if it was just created
if bootstrapCreated && !force {
// Clean up bootstrap.yaml if it was just created but node.yaml already exists
if _, err := os.Stat(bootstrapPath); err == nil {
exec.Command("sudo", "-u", "debros", "rm", "-f", bootstrapPath).Run()
}
// Write node config
if err := os.WriteFile(nodeConfigPath, []byte(nodeConfig), 0644); err != nil {
fmt.Fprintf(os.Stderr, "❌ Failed to write node config: %v\n", err)
os.Exit(1)
}
fmt.Printf(" Using existing node.yaml\n")
// Fix ownership
exec.Command("chown", "debros:debros", nodeConfigPath).Run()
fmt.Printf(" ✓ Node config created: %s\n", nodeConfigPath)
}
// Generate gateway config with explicit empty bootstrap peers
// Check if gateway.yaml already exists
gatewayExists := false
// Generate gateway config
if _, err := os.Stat(gatewayPath); err == nil {
gatewayExists = true
if !force {
@ -849,35 +1296,215 @@ func generateConfigsInteractive(force bool) {
}
if !gatewayExists || force {
gatewayArgs := []string{
"-u", "debros",
"/home/debros/bin/network-cli", "config", "init",
"--type", "gateway",
"--bootstrap-peers", "",
}
if force {
gatewayArgs = append(gatewayArgs, "--force")
}
// Prompt for domain and HTTPS configuration
var domain string
var enableHTTPS bool
var tlsCacheDir string
cmd = exec.Command("sudo", gatewayArgs...)
cmd.Stdin = nil // Explicitly close stdin to prevent interactive prompts
output, err = cmd.CombinedOutput()
if err != nil {
// Check if gateway.yaml already exists (config init failed because it exists)
if _, statErr := os.Stat(gatewayPath); statErr == nil {
fmt.Printf(" gateway.yaml already exists, skipping creation\n")
fmt.Printf("\n🌐 Domain and HTTPS Configuration\n")
fmt.Printf("Would you like to configure HTTPS with a domain name? (yes/no) [default: no]: ")
response, _ := reader.ReadString('\n')
response = strings.ToLower(strings.TrimSpace(response))
if response == "yes" || response == "y" {
// Check if ports 80 and 443 are available
portsAvailable, portIssues := checkPorts80And443()
if !portsAvailable {
fmt.Fprintf(os.Stderr, "\n⚠ Cannot enable HTTPS: %s is already in use\n", portIssues)
fmt.Fprintf(os.Stderr, " You will need to configure HTTPS manually if you want to use a domain.\n")
fmt.Fprintf(os.Stderr, " Continuing without HTTPS configuration...\n\n")
enableHTTPS = false
} else {
fmt.Fprintf(os.Stderr, "⚠️ Failed to generate gateway config: %v\n", err)
if len(output) > 0 {
fmt.Fprintf(os.Stderr, " Output: %s\n", string(output))
// Prompt for domain name
domain = promptDomainForHTTPS(reader, vpsIP)
if domain != "" {
enableHTTPS = true
// Set TLS cache directory if HTTPS is enabled
tlsCacheDir = "/home/debros/.debros/tls-cache"
// Create TLS cache directory
if err := os.MkdirAll(tlsCacheDir, 0755); err != nil {
fmt.Fprintf(os.Stderr, "⚠️ Failed to create TLS cache directory: %v\n", err)
} else {
exec.Command("chown", "-R", "debros:debros", tlsCacheDir).Run()
fmt.Printf(" ✓ TLS cache directory created: %s\n", tlsCacheDir)
}
} else {
enableHTTPS = false
}
}
} else {
fmt.Printf(" ✓ Gateway config created\n")
}
// Gateway config should include bootstrap peers if this is a regular node
// (bootstrap nodes don't need bootstrap peers since they are the bootstrap)
gatewayConfig := generateGatewayConfigDirect(bootstrapPeers, enableHTTPS, domain, tlsCacheDir)
if err := os.WriteFile(gatewayPath, []byte(gatewayConfig), 0644); err != nil {
fmt.Fprintf(os.Stderr, "❌ Failed to write gateway config: %v\n", err)
os.Exit(1)
}
// Fix ownership
exec.Command("chown", "debros:debros", gatewayPath).Run()
fmt.Printf(" ✓ Gateway config created: %s\n", gatewayPath)
}
fmt.Printf("\n ✓ Configurations ready\n")
}
// generateBootstrapConfigWithIP generates a bootstrap config with actual IP address
func generateBootstrapConfigWithIP(name, id string, listenPort, rqliteHTTPPort, rqliteRaftPort int, ipAddr string) string {
nodeID := id
if nodeID == "" {
nodeID = "bootstrap"
}
dataDir := "/home/debros/.debros/bootstrap"
return fmt.Sprintf(`node:
id: "%s"
type: "bootstrap"
listen_addresses:
- "/ip4/%s/tcp/%d"
data_dir: "%s"
max_connections: 50
database:
data_dir: "%s/rqlite"
replication_factor: 3
shard_count: 16
max_database_size: 1073741824
backup_interval: "24h"
rqlite_port: %d
rqlite_raft_port: %d
rqlite_join_address: ""
cluster_sync_interval: "30s"
peer_inactivity_limit: "24h"
min_cluster_size: 1
discovery:
bootstrap_peers: []
discovery_interval: "15s"
bootstrap_port: %d
http_adv_address: "%s:%d"
raft_adv_address: "%s:%d"
node_namespace: "default"
security:
enable_tls: false
logging:
level: "info"
format: "console"
`, nodeID, ipAddr, listenPort, dataDir, dataDir, rqliteHTTPPort, rqliteRaftPort, 4001, ipAddr, rqliteHTTPPort, ipAddr, rqliteRaftPort)
}
// generateNodeConfigWithIP generates a node config with actual IP address
func generateNodeConfigWithIP(name, id string, listenPort, rqliteHTTPPort, rqliteRaftPort int, joinAddr, bootstrapPeers, ipAddr string) string {
nodeID := id
if nodeID == "" {
nodeID = fmt.Sprintf("node-%d", time.Now().Unix())
}
dataDir := "/home/debros/.debros/node"
// Parse bootstrap peers
var peers []string
if bootstrapPeers != "" {
for _, p := range strings.Split(bootstrapPeers, ",") {
if p = strings.TrimSpace(p); p != "" {
peers = append(peers, p)
}
}
}
fmt.Printf(" ✓ Configurations ready\n")
var peersYAML strings.Builder
if len(peers) == 0 {
peersYAML.WriteString(" bootstrap_peers: []")
} else {
peersYAML.WriteString(" bootstrap_peers:\n")
for _, p := range peers {
fmt.Fprintf(&peersYAML, " - \"%s\"\n", p)
}
}
if joinAddr == "" {
joinAddr = fmt.Sprintf("localhost:%d", rqliteHTTPPort)
}
return fmt.Sprintf(`node:
id: "%s"
type: "node"
listen_addresses:
- "/ip4/%s/tcp/%d"
data_dir: "%s"
max_connections: 50
database:
data_dir: "%s/rqlite"
replication_factor: 3
shard_count: 16
max_database_size: 1073741824
backup_interval: "24h"
rqlite_port: %d
rqlite_raft_port: %d
rqlite_join_address: "%s"
cluster_sync_interval: "30s"
peer_inactivity_limit: "24h"
min_cluster_size: 1
discovery:
%s
discovery_interval: "15s"
bootstrap_port: %d
http_adv_address: "%s:%d"
raft_adv_address: "%s:%d"
node_namespace: "default"
security:
enable_tls: false
logging:
level: "info"
format: "console"
`, nodeID, ipAddr, listenPort, dataDir, dataDir, rqliteHTTPPort, rqliteRaftPort, joinAddr, peersYAML.String(), 4001, ipAddr, rqliteHTTPPort, ipAddr, rqliteRaftPort)
}
// generateGatewayConfigDirect generates gateway config directly
func generateGatewayConfigDirect(bootstrapPeers string, enableHTTPS bool, domain, tlsCacheDir string) string {
var peers []string
if bootstrapPeers != "" {
for _, p := range strings.Split(bootstrapPeers, ",") {
if p = strings.TrimSpace(p); p != "" {
peers = append(peers, p)
}
}
}
var peersYAML strings.Builder
if len(peers) == 0 {
peersYAML.WriteString("bootstrap_peers: []")
} else {
peersYAML.WriteString("bootstrap_peers:\n")
for _, p := range peers {
fmt.Fprintf(&peersYAML, " - \"%s\"\n", p)
}
}
var httpsYAML strings.Builder
if enableHTTPS && domain != "" {
fmt.Fprintf(&httpsYAML, "enable_https: true\n")
fmt.Fprintf(&httpsYAML, "domain_name: \"%s\"\n", domain)
if tlsCacheDir != "" {
fmt.Fprintf(&httpsYAML, "tls_cache_dir: \"%s\"\n", tlsCacheDir)
}
} else {
fmt.Fprintf(&httpsYAML, "enable_https: false\n")
}
return fmt.Sprintf(`listen_addr: ":6001"
client_namespace: "default"
rqlite_dsn: ""
%s
%s
`, peersYAML.String(), httpsYAML.String())
}
func createSystemdServices() {
@ -937,6 +1564,10 @@ StandardOutput=journal
StandardError=journal
SyslogIdentifier=debros-gateway
# Allow binding to privileged ports (80, 443) for HTTPS/ACME
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict

View File

@ -12,6 +12,21 @@ import (
// DefaultBootstrapPeers returns the library's default bootstrap peer multiaddrs.
// These can be overridden by environment variables or config.
func DefaultBootstrapPeers() []string {
// Check environment variable first
if envPeers := os.Getenv("DEBROS_BOOTSTRAP_PEERS"); envPeers != "" {
peers := splitCSVOrSpace(envPeers)
// Filter out empty strings
result := make([]string, 0, len(peers))
for _, p := range peers {
if p != "" {
result = append(result, p)
}
}
if len(result) > 0 {
return result
}
}
defaultCfg := config.DefaultConfig()
return defaultCfg.Discovery.BootstrapPeers
}

View File

@ -10,11 +10,16 @@ import (
func TestDefaultBootstrapPeersNonEmpty(t *testing.T) {
old := os.Getenv("DEBROS_BOOTSTRAP_PEERS")
t.Cleanup(func() { os.Setenv("DEBROS_BOOTSTRAP_PEERS", old) })
_ = os.Setenv("DEBROS_BOOTSTRAP_PEERS", "") // ensure not set
// Set a valid bootstrap peer
validPeer := "/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"
_ = os.Setenv("DEBROS_BOOTSTRAP_PEERS", validPeer)
peers := DefaultBootstrapPeers()
if len(peers) == 0 {
t.Fatalf("expected non-empty default bootstrap peers")
}
if peers[0] != validPeer {
t.Fatalf("expected bootstrap peer %s, got %s", validPeer, peers[0])
}
}
func TestDefaultDatabaseEndpointsEnvOverride(t *testing.T) {

View File

@ -5,6 +5,55 @@ import (
"time"
)
// validConfigForType returns a valid config for the given node type
func validConfigForType(nodeType string) *Config {
validPeer := "/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"
cfg := &Config{
Node: NodeConfig{
Type: nodeType,
ID: "test-node-id",
ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"},
DataDir: ".",
MaxConnections: 50,
},
Database: DatabaseConfig{
DataDir: ".",
ReplicationFactor: 3,
ShardCount: 16,
MaxDatabaseSize: 1024,
BackupInterval: 1 * time.Hour,
RQLitePort: 5001,
RQLiteRaftPort: 7001,
MinClusterSize: 1,
},
Discovery: DiscoveryConfig{
BootstrapPeers: []string{validPeer},
DiscoveryInterval: 15 * time.Second,
BootstrapPort: 4001,
HttpAdvAddress: "127.0.0.1:5001",
RaftAdvAddress: "127.0.0.1:7001",
NodeNamespace: "default",
},
Logging: LoggingConfig{
Level: "info",
Format: "console",
},
}
// Set rqlite_join_address based on node type
if nodeType == "node" {
cfg.Database.RQLiteJoinAddress = "localhost:5001"
// Node type requires bootstrap peers
cfg.Discovery.BootstrapPeers = []string{validPeer}
} else {
// Bootstrap type: empty join address and peers optional
cfg.Database.RQLiteJoinAddress = ""
cfg.Discovery.BootstrapPeers = []string{}
}
return cfg
}
func TestValidateNodeType(t *testing.T) {
tests := []struct {
name string
@ -19,11 +68,11 @@ func TestValidateNodeType(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &Config{
Node: NodeConfig{Type: tt.nodeType, ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: 50},
Database: DatabaseConfig{DataDir: ".", ReplicationFactor: 3, ShardCount: 16, MaxDatabaseSize: 1024, BackupInterval: 1 * time.Hour, RQLitePort: 5001, RQLiteRaftPort: 7001},
Discovery: DiscoveryConfig{BootstrapPeers: []string{"/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"}, DiscoveryInterval: 15 * time.Second, BootstrapPort: 4001, NodeNamespace: "default"},
Logging: LoggingConfig{Level: "info", Format: "console"},
cfg := validConfigForType("bootstrap") // Start with valid bootstrap
if tt.nodeType == "node" {
cfg = validConfigForType("node")
} else {
cfg.Node.Type = tt.nodeType
}
errs := cfg.Validate()
if tt.shouldError && len(errs) == 0 {
@ -53,12 +102,8 @@ func TestValidateListenAddresses(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &Config{
Node: NodeConfig{Type: "node", ListenAddresses: tt.addresses, DataDir: ".", MaxConnections: 50},
Database: DatabaseConfig{DataDir: ".", ReplicationFactor: 3, ShardCount: 16, MaxDatabaseSize: 1024, BackupInterval: 1 * time.Hour, RQLitePort: 5001, RQLiteRaftPort: 7001, RQLiteJoinAddress: "localhost:7001"},
Discovery: DiscoveryConfig{BootstrapPeers: []string{"/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"}, DiscoveryInterval: 15 * time.Second, BootstrapPort: 4001, NodeNamespace: "default"},
Logging: LoggingConfig{Level: "info", Format: "console"},
}
cfg := validConfigForType("node")
cfg.Node.ListenAddresses = tt.addresses
errs := cfg.Validate()
if tt.shouldError && len(errs) == 0 {
t.Errorf("expected error, got none")
@ -85,12 +130,8 @@ func TestValidateReplicationFactor(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &Config{
Node: NodeConfig{Type: "node", ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: 50},
Database: DatabaseConfig{DataDir: ".", ReplicationFactor: tt.replication, ShardCount: 16, MaxDatabaseSize: 1024, BackupInterval: 1 * time.Hour, RQLitePort: 5001, RQLiteRaftPort: 7001, RQLiteJoinAddress: "localhost:7001"},
Discovery: DiscoveryConfig{BootstrapPeers: []string{"/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"}, DiscoveryInterval: 15 * time.Second, BootstrapPort: 4001, NodeNamespace: "default"},
Logging: LoggingConfig{Level: "info", Format: "console"},
}
cfg := validConfigForType("node")
cfg.Database.ReplicationFactor = tt.replication
errs := cfg.Validate()
if tt.shouldError && len(errs) == 0 {
t.Errorf("expected error, got none")
@ -119,12 +160,9 @@ func TestValidateRQLitePorts(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &Config{
Node: NodeConfig{Type: "node", ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: 50},
Database: DatabaseConfig{DataDir: ".", ReplicationFactor: 3, ShardCount: 16, MaxDatabaseSize: 1024, BackupInterval: 1 * time.Hour, RQLitePort: tt.httpPort, RQLiteRaftPort: tt.raftPort, RQLiteJoinAddress: "localhost:7001"},
Discovery: DiscoveryConfig{BootstrapPeers: []string{"/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"}, DiscoveryInterval: 15 * time.Second, BootstrapPort: 4001, NodeNamespace: "default"},
Logging: LoggingConfig{Level: "info", Format: "console"},
}
cfg := validConfigForType("node")
cfg.Database.RQLitePort = tt.httpPort
cfg.Database.RQLiteRaftPort = tt.raftPort
errs := cfg.Validate()
if tt.shouldError && len(errs) == 0 {
t.Errorf("expected error, got none")
@ -143,9 +181,9 @@ func TestValidateRQLiteJoinAddress(t *testing.T) {
joinAddr string
shouldError bool
}{
{"node with join", "node", "localhost:7001", false},
{"node with join", "node", "localhost:5001", false},
{"node without join", "node", "", true},
{"bootstrap with join", "bootstrap", "localhost:7001", true},
{"bootstrap with join", "bootstrap", "localhost:5001", true},
{"bootstrap without join", "bootstrap", "", false},
{"invalid join format", "node", "localhost", true},
{"invalid join port", "node", "localhost:99999", true},
@ -153,12 +191,8 @@ func TestValidateRQLiteJoinAddress(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &Config{
Node: NodeConfig{Type: tt.nodeType, ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: 50},
Database: DatabaseConfig{DataDir: ".", ReplicationFactor: 3, ShardCount: 16, MaxDatabaseSize: 1024, BackupInterval: 1 * time.Hour, RQLitePort: 5001, RQLiteRaftPort: 7001, RQLiteJoinAddress: tt.joinAddr},
Discovery: DiscoveryConfig{BootstrapPeers: []string{"/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"}, DiscoveryInterval: 15 * time.Second, BootstrapPort: 4001, NodeNamespace: "default"},
Logging: LoggingConfig{Level: "info", Format: "console"},
}
cfg := validConfigForType(tt.nodeType)
cfg.Database.RQLiteJoinAddress = tt.joinAddr
errs := cfg.Validate()
if tt.shouldError && len(errs) == 0 {
t.Errorf("expected error, got none")
@ -190,12 +224,8 @@ func TestValidateBootstrapPeers(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &Config{
Node: NodeConfig{Type: tt.nodeType, ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: 50},
Database: DatabaseConfig{DataDir: ".", ReplicationFactor: 3, ShardCount: 16, MaxDatabaseSize: 1024, BackupInterval: 1 * time.Hour, RQLitePort: 5001, RQLiteRaftPort: 7001, RQLiteJoinAddress: ""},
Discovery: DiscoveryConfig{BootstrapPeers: tt.peers, DiscoveryInterval: 15 * time.Second, BootstrapPort: 4001, NodeNamespace: "default"},
Logging: LoggingConfig{Level: "info", Format: "console"},
}
cfg := validConfigForType(tt.nodeType)
cfg.Discovery.BootstrapPeers = tt.peers
errs := cfg.Validate()
if tt.shouldError && len(errs) == 0 {
t.Errorf("expected error, got none")
@ -223,12 +253,8 @@ func TestValidateLoggingLevel(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &Config{
Node: NodeConfig{Type: "node", ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: 50},
Database: DatabaseConfig{DataDir: ".", ReplicationFactor: 3, ShardCount: 16, MaxDatabaseSize: 1024, BackupInterval: 1 * time.Hour, RQLitePort: 5001, RQLiteRaftPort: 7001, RQLiteJoinAddress: "localhost:7001"},
Discovery: DiscoveryConfig{BootstrapPeers: []string{"/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"}, DiscoveryInterval: 15 * time.Second, BootstrapPort: 4001, NodeNamespace: "default"},
Logging: LoggingConfig{Level: tt.level, Format: "console"},
}
cfg := validConfigForType("node")
cfg.Logging.Level = tt.level
errs := cfg.Validate()
if tt.shouldError && len(errs) == 0 {
t.Errorf("expected error, got none")
@ -254,12 +280,8 @@ func TestValidateLoggingFormat(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &Config{
Node: NodeConfig{Type: "node", ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: 50},
Database: DatabaseConfig{DataDir: ".", ReplicationFactor: 3, ShardCount: 16, MaxDatabaseSize: 1024, BackupInterval: 1 * time.Hour, RQLitePort: 5001, RQLiteRaftPort: 7001, RQLiteJoinAddress: "localhost:7001"},
Discovery: DiscoveryConfig{BootstrapPeers: []string{"/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"}, DiscoveryInterval: 15 * time.Second, BootstrapPort: 4001, NodeNamespace: "default"},
Logging: LoggingConfig{Level: "info", Format: tt.format},
}
cfg := validConfigForType("node")
cfg.Logging.Format = tt.format
errs := cfg.Validate()
if tt.shouldError && len(errs) == 0 {
t.Errorf("expected error, got none")
@ -285,12 +307,8 @@ func TestValidateMaxConnections(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &Config{
Node: NodeConfig{Type: "node", ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: tt.maxConn},
Database: DatabaseConfig{DataDir: ".", ReplicationFactor: 3, ShardCount: 16, MaxDatabaseSize: 1024, BackupInterval: 1 * time.Hour, RQLitePort: 5001, RQLiteRaftPort: 7001, RQLiteJoinAddress: "localhost:7001"},
Discovery: DiscoveryConfig{BootstrapPeers: []string{"/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"}, DiscoveryInterval: 15 * time.Second, BootstrapPort: 4001, NodeNamespace: "default"},
Logging: LoggingConfig{Level: "info", Format: "console"},
}
cfg := validConfigForType("node")
cfg.Node.MaxConnections = tt.maxConn
errs := cfg.Validate()
if tt.shouldError && len(errs) == 0 {
t.Errorf("expected error, got none")
@ -316,12 +334,8 @@ func TestValidateDiscoveryInterval(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &Config{
Node: NodeConfig{Type: "node", ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: 50},
Database: DatabaseConfig{DataDir: ".", ReplicationFactor: 3, ShardCount: 16, MaxDatabaseSize: 1024, BackupInterval: 1 * time.Hour, RQLitePort: 5001, RQLiteRaftPort: 7001, RQLiteJoinAddress: "localhost:7001"},
Discovery: DiscoveryConfig{BootstrapPeers: []string{"/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"}, DiscoveryInterval: tt.interval, BootstrapPort: 4001, NodeNamespace: "default"},
Logging: LoggingConfig{Level: "info", Format: "console"},
}
cfg := validConfigForType("node")
cfg.Discovery.DiscoveryInterval = tt.interval
errs := cfg.Validate()
if tt.shouldError && len(errs) == 0 {
t.Errorf("expected error, got none")
@ -347,12 +361,8 @@ func TestValidateBootstrapPort(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &Config{
Node: NodeConfig{Type: "node", ListenAddresses: []string{"/ip4/0.0.0.0/tcp/4001"}, DataDir: ".", MaxConnections: 50},
Database: DatabaseConfig{DataDir: ".", ReplicationFactor: 3, ShardCount: 16, MaxDatabaseSize: 1024, BackupInterval: 1 * time.Hour, RQLitePort: 5001, RQLiteRaftPort: 7001, RQLiteJoinAddress: "localhost:7001"},
Discovery: DiscoveryConfig{BootstrapPeers: []string{"/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWHbcFcrGPXKUrHcxvd8MXEeUzRYyvY8fQcpEBxncSUwhj"}, DiscoveryInterval: 15 * time.Second, BootstrapPort: tt.port, NodeNamespace: "default"},
Logging: LoggingConfig{Level: "info", Format: "console"},
}
cfg := validConfigForType("node")
cfg.Discovery.BootstrapPort = tt.port
errs := cfg.Validate()
if tt.shouldError && len(errs) == 0 {
t.Errorf("expected error, got none")
@ -383,6 +393,7 @@ func TestValidateCompleteConfig(t *testing.T) {
RQLitePort: 5002,
RQLiteRaftPort: 7002,
RQLiteJoinAddress: "127.0.0.1:7001",
MinClusterSize: 1,
},
Discovery: DiscoveryConfig{
BootstrapPeers: []string{
@ -390,7 +401,8 @@ func TestValidateCompleteConfig(t *testing.T) {
},
DiscoveryInterval: 15 * time.Second,
BootstrapPort: 4001,
HttpAdvAddress: "127.0.0.1",
HttpAdvAddress: "127.0.0.1:5001",
RaftAdvAddress: "127.0.0.1:7001",
NodeNamespace: "default",
},
Security: SecurityConfig{

View File

@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"io"
"strconv"
"time"
"github.com/libp2p/go-libp2p/core/host"
@ -114,9 +115,41 @@ func (d *Manager) handlePeerExchangeStream(s network.Stream) {
continue
}
// Filter addresses to only include configured listen addresses, not ephemeral ports
// Ephemeral ports are typically > 32768, so we filter those out
filteredAddrs := make([]multiaddr.Multiaddr, 0)
for _, addr := range addrs {
// Extract TCP port from multiaddr
port, err := addr.ValueForProtocol(multiaddr.P_TCP)
if err == nil {
portNum, err := strconv.Atoi(port)
if err == nil {
// Only include ports that are reasonable (not ephemeral ports > 32768)
// Common LibP2P ports are typically < 10000
if portNum > 0 && portNum <= 32767 {
filteredAddrs = append(filteredAddrs, addr)
}
} else {
// If we can't parse port, include it anyway (might be non-TCP)
filteredAddrs = append(filteredAddrs, addr)
}
} else {
// If no TCP port found, include it anyway (might be non-TCP)
filteredAddrs = append(filteredAddrs, addr)
}
}
// If no addresses remain after filtering, skip this peer
if len(filteredAddrs) == 0 {
d.logger.Debug("No valid addresses after filtering ephemeral ports",
zap.String("peer_id", pid.String()[:8]+"..."),
zap.Int("original_count", len(addrs)))
continue
}
// Convert addresses to strings
addrStrs := make([]string, len(addrs))
for i, addr := range addrs {
addrStrs := make([]string, len(filteredAddrs))
for i, addr := range filteredAddrs {
addrStrs[i] = addr.String()
}
@ -320,7 +353,7 @@ func (d *Manager) discoverViaPeerExchange(ctx context.Context, maxConnections in
d.host.Peerstore().AddAddrs(parsedID, addrs, time.Hour*24)
// Try to connect
connectCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
connectCtx, cancel := context.WithTimeout(ctx, 20*time.Second)
peerAddrInfo := peer.AddrInfo{ID: parsedID, Addrs: addrs}
if err := d.host.Connect(connectCtx, peerAddrInfo); err != nil {

View File

@ -206,9 +206,31 @@ func isHopByHopHeader(header string) bool {
// isPrivateOrLocalHost checks if a host is private, local, or loopback
func isPrivateOrLocalHost(host string) bool {
// Strip port if present
if idx := strings.LastIndex(host, ":"); idx != -1 {
host = host[:idx]
// Strip port if present, handling IPv6 addresses properly
// IPv6 addresses in URLs are bracketed: [::1]:8080
if strings.HasPrefix(host, "[") {
// IPv6 address with brackets
if idx := strings.LastIndex(host, "]"); idx != -1 {
if idx+1 < len(host) && host[idx+1] == ':' {
// Port present, strip it
host = host[1:idx] // Remove brackets and port
} else {
// No port, just remove brackets
host = host[1:idx]
}
}
} else {
// IPv4 or hostname, check for port
if idx := strings.LastIndex(host, ":"); idx != -1 {
// Check if it's an IPv6 address without brackets (contains multiple colons)
colonCount := strings.Count(host, ":")
if colonCount == 1 {
// Single colon, likely IPv4 with port
host = host[:idx]
}
// If multiple colons, it's IPv6 without brackets and no port
// Leave host as-is
}
}
// Check for localhost variants

View File

@ -70,6 +70,21 @@ func (c *Config) ValidateConfig() []error {
}
}
// Validate HTTPS configuration
if c.EnableHTTPS {
if c.DomainName == "" {
errs = append(errs, fmt.Errorf("gateway.domain_name: must be set when enable_https is true"))
} else {
// Basic domain validation
if !isValidDomainName(c.DomainName) {
errs = append(errs, fmt.Errorf("gateway.domain_name: invalid domain format"))
}
}
if c.TLSCacheDir == "" {
errs = append(errs, fmt.Errorf("gateway.tls_cache_dir: must be set when enable_https is true"))
}
}
return errs
}
@ -135,3 +150,38 @@ func extractTCPPort(multiaddrStr string) string {
return portPart[:firstSlashIndex]
}
// isValidDomainName validates a domain name format
func isValidDomainName(domain string) bool {
domain = strings.TrimSpace(domain)
if domain == "" {
return false
}
// Basic validation: domain should contain at least one dot
// and not start/end with dot or hyphen
if !strings.Contains(domain, ".") {
return false
}
if strings.HasPrefix(domain, ".") || strings.HasSuffix(domain, ".") {
return false
}
if strings.HasPrefix(domain, "-") || strings.HasSuffix(domain, "-") {
return false
}
// Check for valid characters (letters, numbers, dots, hyphens)
for _, char := range domain {
if !((char >= 'a' && char <= 'z') ||
(char >= 'A' && char <= 'Z') ||
(char >= '0' && char <= '9') ||
char == '.' ||
char == '-') {
return false
}
}
return true
}

View File

@ -26,6 +26,11 @@ type Config struct {
// Optional DSN for rqlite database/sql driver, e.g. "http://localhost:4001"
// If empty, defaults to "http://localhost:4001".
RQLiteDSN string
// HTTPS configuration
EnableHTTPS bool // Enable HTTPS with ACME (Let's Encrypt)
DomainName string // Domain name for HTTPS certificate
TLSCacheDir string // Directory to cache TLS certificates (default: ~/.debros/tls-cache)
}
type Gateway struct {

View File

@ -328,7 +328,6 @@ func (n *Node) startLibP2P() error {
n.logger.ComponentInfo(logging.ComponentLibP2P, "Localhost detected - disabling NAT services for local development")
// Don't add NAT/AutoRelay options for localhost
} else {
// Production: enable NAT traversal
n.logger.ComponentInfo(logging.ComponentLibP2P, "Production mode - enabling NAT services")
opts = append(opts,
libp2p.EnableNATService(),

45
scripts/install-hooks.sh Executable file
View File

@ -0,0 +1,45 @@
#!/bin/bash
# Install git hooks from .githooks/ to .git/hooks/
# This ensures the pre-push hook runs automatically
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
GITHOOKS_DIR="$REPO_ROOT/.githooks"
GIT_HOOKS_DIR="$REPO_ROOT/.git/hooks"
if [ ! -d "$GITHOOKS_DIR" ]; then
echo "Error: .githooks directory not found at $GITHOOKS_DIR"
exit 1
fi
if [ ! -d "$GIT_HOOKS_DIR" ]; then
echo "Error: .git/hooks directory not found at $GIT_HOOKS_DIR"
echo "Are you in a git repository?"
exit 1
fi
echo "Installing git hooks..."
# Copy all hooks from .githooks/ to .git/hooks/
for hook in "$GITHOOKS_DIR"/*; do
if [ -f "$hook" ]; then
hook_name=$(basename "$hook")
dest="$GIT_HOOKS_DIR/$hook_name"
echo " Installing $hook_name..."
cp "$hook" "$dest"
chmod +x "$dest"
# Make sure the hook can find the repo root
# The hooks already use relative paths, so this should work
fi
done
echo "✓ Git hooks installed successfully!"
echo ""
echo "The following hooks are now active:"
ls -1 "$GIT_HOOKS_DIR"/* 2>/dev/null | xargs -n1 basename || echo " (none)"

426
scripts/update_changelog.sh Executable file
View File

@ -0,0 +1,426 @@
#!/bin/bash
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NOCOLOR='\033[0m'
log() { echo -e "${CYAN}[update-changelog]${NOCOLOR} $1"; }
error() { echo -e "${RED}[ERROR]${NOCOLOR} $1"; }
success() { echo -e "${GREEN}[SUCCESS]${NOCOLOR} $1"; }
warning() { echo -e "${YELLOW}[WARNING]${NOCOLOR} $1"; }
# File paths
CHANGELOG_FILE="CHANGELOG.md"
MAKEFILE="Makefile"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$REPO_ROOT"
# Load environment variables from .env file if it exists
if [ -f "$REPO_ROOT/.env" ]; then
# Export variables from .env file (more portable than source <())
set -a
while IFS='=' read -r key value; do
# Skip comments and empty lines
[[ "$key" =~ ^#.*$ ]] && continue
[[ -z "$key" ]] && continue
# Remove quotes if present
value=$(echo "$value" | sed -e 's/^"//' -e 's/"$//' -e "s/^'//" -e "s/'$//")
export "$key=$value"
done < "$REPO_ROOT/.env"
set +a
fi
# OpenRouter API key
# Priority: 1. Environment variable, 2. .env file, 3. Exit with error
if [ -z "$OPENROUTER_API_KEY" ]; then
error "OPENROUTER_API_KEY not found!"
echo ""
echo "Please set the API key in one of these ways:"
echo " 1. Create a .env file in the repo root with:"
echo " OPENROUTER_API_KEY=your-api-key-here"
echo ""
echo " 2. Set it as an environment variable:"
echo " export OPENROUTER_API_KEY=your-api-key-here"
echo ""
echo " 3. Copy .env.example to .env and fill in your key:"
echo " cp .env.example .env"
echo ""
echo "Get your API key from: https://openrouter.ai/keys"
exit 1
fi
# Check dependencies
if ! command -v jq > /dev/null 2>&1; then
error "jq is required but not installed. Install it with: brew install jq (macOS) or apt-get install jq (Linux)"
exit 1
fi
if ! command -v curl > /dev/null 2>&1; then
error "curl is required but not installed"
exit 1
fi
# Check if we're in a git repo
if ! git rev-parse --git-dir > /dev/null 2>&1; then
error "Not in a git repository"
exit 1
fi
# Get current branch
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
REMOTE_BRANCH="origin/$CURRENT_BRANCH"
# Check if remote branch exists
if ! git rev-parse --verify "$REMOTE_BRANCH" > /dev/null 2>&1; then
warning "Remote branch $REMOTE_BRANCH does not exist. Using main/master as baseline."
if git rev-parse --verify "origin/main" > /dev/null 2>&1; then
REMOTE_BRANCH="origin/main"
elif git rev-parse --verify "origin/master" > /dev/null 2>&1; then
REMOTE_BRANCH="origin/master"
else
warning "No remote branch found. Using HEAD as baseline."
REMOTE_BRANCH="HEAD"
fi
fi
# Gather all git diffs
log "Collecting git diffs..."
# Check if running from pre-commit context
if [ "$CHANGELOG_CONTEXT" = "pre-commit" ]; then
log "Running in pre-commit context - analyzing staged changes only"
# Unstaged changes (usually none in pre-commit, but check anyway)
UNSTAGED_DIFF=$(git diff 2>/dev/null || echo "")
UNSTAGED_COUNT=$(echo "$UNSTAGED_DIFF" | grep -c "^diff\|^index" 2>/dev/null || echo "0")
[ -z "$UNSTAGED_COUNT" ] && UNSTAGED_COUNT="0"
# Staged changes (these are what we're committing)
STAGED_DIFF=$(git diff --cached 2>/dev/null || echo "")
STAGED_COUNT=$(echo "$STAGED_DIFF" | grep -c "^diff\|^index" 2>/dev/null || echo "0")
[ -z "$STAGED_COUNT" ] && STAGED_COUNT="0"
# No unpushed commits analysis in pre-commit context
UNPUSHED_DIFF=""
UNPUSHED_COMMITS="0"
log "Found: $UNSTAGED_COUNT unstaged file(s), $STAGED_COUNT staged file(s)"
else
# Pre-push context - analyze everything
# Unstaged changes
UNSTAGED_DIFF=$(git diff 2>/dev/null || echo "")
UNSTAGED_COUNT=$(echo "$UNSTAGED_DIFF" | grep -c "^diff\|^index" 2>/dev/null || echo "0")
[ -z "$UNSTAGED_COUNT" ] && UNSTAGED_COUNT="0"
# Staged changes
STAGED_DIFF=$(git diff --cached 2>/dev/null || echo "")
STAGED_COUNT=$(echo "$STAGED_DIFF" | grep -c "^diff\|^index" 2>/dev/null || echo "0")
[ -z "$STAGED_COUNT" ] && STAGED_COUNT="0"
# Unpushed commits
UNPUSHED_DIFF=$(git diff "$REMOTE_BRANCH"..HEAD 2>/dev/null || echo "")
UNPUSHED_COMMITS=$(git rev-list --count "$REMOTE_BRANCH"..HEAD 2>/dev/null || echo "0")
[ -z "$UNPUSHED_COMMITS" ] && UNPUSHED_COMMITS="0"
# Check if the only unpushed commit is a changelog update commit
# If so, exclude it from the diff to avoid infinite loops
if [ "$UNPUSHED_COMMITS" -gt 0 ]; then
LATEST_COMMIT_MSG=$(git log -1 --pretty=%B HEAD 2>/dev/null || echo "")
if echo "$LATEST_COMMIT_MSG" | grep -q "chore: update changelog and version"; then
# If the latest commit is a changelog commit, check if there are other commits
if [ "$UNPUSHED_COMMITS" -eq 1 ]; then
log "Latest commit is a changelog update. No other changes detected. Skipping changelog update."
# Clean up any old preview files
rm -f "$REPO_ROOT/.changelog_preview.tmp" "$REPO_ROOT/.changelog_version.tmp"
exit 0
else
# Multiple commits, exclude the latest changelog commit from diff
log "Multiple unpushed commits detected. Excluding latest changelog commit from analysis."
# Get all commits except the latest one
UNPUSHED_DIFF=$(git diff "$REMOTE_BRANCH"..HEAD~1 2>/dev/null || echo "")
UNPUSHED_COMMITS=$(git rev-list --count "$REMOTE_BRANCH"..HEAD~1 2>/dev/null || echo "0")
[ -z "$UNPUSHED_COMMITS" ] && UNPUSHED_COMMITS="0"
fi
fi
fi
log "Found: $UNSTAGED_COUNT unstaged file(s), $STAGED_COUNT staged file(s), $UNPUSHED_COMMITS unpushed commit(s)"
fi
# Combine all diffs
if [ "$CHANGELOG_CONTEXT" = "pre-commit" ]; then
ALL_DIFFS="${UNSTAGED_DIFF}
---
STAGED CHANGES:
---
${STAGED_DIFF}"
else
ALL_DIFFS="${UNSTAGED_DIFF}
---
STAGED CHANGES:
---
${STAGED_DIFF}
---
UNPUSHED COMMITS:
---
${UNPUSHED_DIFF}"
fi
# Check if there are any changes
if [ "$CHANGELOG_CONTEXT" = "pre-commit" ]; then
# In pre-commit, only check staged changes
if [ -z "$(echo "$STAGED_DIFF" | tr -d '[:space:]')" ]; then
log "No staged changes detected. Skipping changelog update."
rm -f "$REPO_ROOT/.changelog_preview.tmp" "$REPO_ROOT/.changelog_version.tmp"
exit 0
fi
else
# In pre-push, check all changes
if [ -z "$(echo "$UNSTAGED_DIFF$STAGED_DIFF$UNPUSHED_DIFF" | tr -d '[:space:]')" ]; then
log "No changes detected (unstaged, staged, or unpushed). Skipping changelog update."
rm -f "$REPO_ROOT/.changelog_preview.tmp" "$REPO_ROOT/.changelog_version.tmp"
exit 0
fi
fi
# Get current version from Makefile
CURRENT_VERSION=$(grep "^VERSION :=" "$MAKEFILE" | sed 's/.*:= *//' | tr -d ' ')
if [ -z "$CURRENT_VERSION" ]; then
error "Could not find VERSION in Makefile"
exit 1
fi
log "Current version: $CURRENT_VERSION"
# Get today's date programmatically (YYYY-MM-DD format)
TODAY_DATE=$(date +%Y-%m-%d)
log "Using date: $TODAY_DATE"
# Prepare prompt for OpenRouter
PROMPT="You are analyzing git diffs to create a changelog entry. Based on the following git diffs, create a simple, easy-to-understand changelog entry.
Current version: $CURRENT_VERSION
Git diffs:
\`\`\`
$ALL_DIFFS
\`\`\`
Please respond with ONLY a valid JSON object in this exact format:
{
\"version\": \"x.y.z\",
\"bump_type\": \"minor\" or \"patch\",
\"added\": [\"item1\", \"item2\"],
\"changed\": [\"item1\", \"item2\"],
\"fixed\": [\"item1\", \"item2\"]
}
Rules:
- Bump version based on changes: use \"minor\" for new features, \"patch\" for bug fixes and small changes
- Never bump major version (keep major version the same)
- Keep descriptions simple and easy to understand (1-2 sentences max per item)
- Only include items that actually changed
- If a category is empty, use an empty array []
- Do NOT include a date field - the date will be set programmatically"
# Call OpenRouter API
log "Calling OpenRouter API to generate changelog..."
# Prepare the JSON payload properly
PROMPT_ESCAPED=$(echo "$PROMPT" | jq -Rs .)
REQUEST_BODY=$(cat <<EOF
{
"model": "google/gemini-2.5-flash-preview-09-2025",
"messages": [
{
"role": "user",
"content": $PROMPT_ESCAPED
}
],
"temperature": 0.3
}
EOF
)
# Debug: Check API key format (first 10 chars only)
API_KEY_PREFIX="${OPENROUTER_API_KEY:0:10}..."
log "Using API key: $API_KEY_PREFIX (length: ${#OPENROUTER_API_KEY})"
set +e # Temporarily disable exit on error to check curl response
RESPONSE=$(curl -s -w "\nHTTP_CODE:%{http_code}" -X POST "https://openrouter.ai/api/v1/chat/completions" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
-d "$REQUEST_BODY")
CURL_EXIT_CODE=$?
# Extract HTTP code and response body
HTTP_CODE=$(echo "$RESPONSE" | grep -o "HTTP_CODE:[0-9]*" | cut -d: -f2)
RESPONSE_BODY=$(echo "$RESPONSE" | sed '/HTTP_CODE:/d')
set -e # Re-enable exit on error
log "HTTP Status Code: $HTTP_CODE"
# Check if API call succeeded
if [ $CURL_EXIT_CODE -ne 0 ] || [ -z "$RESPONSE_BODY" ]; then
error "Failed to call OpenRouter API"
if [ $CURL_EXIT_CODE -ne 0 ]; then
echo "Network error (curl exit code: $CURL_EXIT_CODE)"
else
echo "Empty response from API"
fi
exit 1
fi
# Check for API errors in response
if echo "$RESPONSE_BODY" | jq -e '.error' > /dev/null 2>&1; then
error "OpenRouter API error:"
ERROR_MESSAGE=$(echo "$RESPONSE_BODY" | jq -r '.error.message // .error' 2>/dev/null || echo "$RESPONSE_BODY")
echo "$ERROR_MESSAGE"
echo ""
error "Full API response:"
echo "$RESPONSE_BODY" | jq '.' 2>/dev/null || echo "$RESPONSE_BODY"
echo ""
error "The API key may be invalid or expired. Please verify your OpenRouter API key at https://openrouter.ai/keys"
echo ""
error "To test your API key manually, run:"
echo " curl https://openrouter.ai/api/v1/chat/completions \\"
echo " -H \"Content-Type: application/json\" \\"
echo " -H \"Authorization: Bearer YOUR_API_KEY\" \\"
echo " -d '{\"model\": \"google/gemini-2.5-flash-preview-09-2025\", \"messages\": [{\"role\": \"user\", \"content\": \"test\"}]}'"
exit 1
fi
# Extract JSON from response
JSON_CONTENT=$(echo "$RESPONSE_BODY" | jq -r '.choices[0].message.content' 2>/dev/null)
# Check if content was extracted
if [ -z "$JSON_CONTENT" ] || [ "$JSON_CONTENT" = "null" ]; then
error "Failed to extract content from API response"
echo "Response: $RESPONSE_BODY"
exit 1
fi
# Try to extract JSON if it's wrapped in markdown code blocks
if echo "$JSON_CONTENT" | grep -q '```json'; then
JSON_CONTENT=$(echo "$JSON_CONTENT" | sed -n '/```json/,/```/p' | sed '1d;$d')
elif echo "$JSON_CONTENT" | grep -q '```'; then
JSON_CONTENT=$(echo "$JSON_CONTENT" | sed -n '/```/,/```/p' | sed '1d;$d')
fi
# Validate JSON
if ! echo "$JSON_CONTENT" | jq . > /dev/null 2>&1; then
error "Invalid JSON response from API:"
echo "$JSON_CONTENT"
exit 1
fi
# Parse JSON
NEW_VERSION=$(echo "$JSON_CONTENT" | jq -r '.version')
BUMP_TYPE=$(echo "$JSON_CONTENT" | jq -r '.bump_type')
ADDED=$(echo "$JSON_CONTENT" | jq -r '.added[]?' | sed 's/^/- /')
CHANGED=$(echo "$JSON_CONTENT" | jq -r '.changed[]?' | sed 's/^/- /')
FIXED=$(echo "$JSON_CONTENT" | jq -r '.fixed[]?' | sed 's/^/- /')
log "Generated version: $NEW_VERSION ($BUMP_TYPE bump)"
log "Date: $TODAY_DATE"
# Validate version format
if ! echo "$NEW_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
error "Invalid version format: $NEW_VERSION"
exit 1
fi
# Validate bump type
if [ "$BUMP_TYPE" != "minor" ] && [ "$BUMP_TYPE" != "patch" ]; then
error "Invalid bump type: $BUMP_TYPE (must be 'minor' or 'patch')"
exit 1
fi
# Update Makefile
log "Updating Makefile..."
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS sed requires backup extension
sed -i '' "s/^VERSION := .*/VERSION := $NEW_VERSION/" "$MAKEFILE"
else
# Linux sed
sed -i "s/^VERSION := .*/VERSION := $NEW_VERSION/" "$MAKEFILE"
fi
success "Makefile updated to version $NEW_VERSION"
# Update CHANGELOG.md
log "Updating CHANGELOG.md..."
# Create changelog entry
CHANGELOG_ENTRY="## [$NEW_VERSION] - $TODAY_DATE
### Added
"
if [ -n "$ADDED" ]; then
CHANGELOG_ENTRY+="$ADDED"$'\n'
else
CHANGELOG_ENTRY+="\n"
fi
CHANGELOG_ENTRY+="
### Changed
"
if [ -n "$CHANGED" ]; then
CHANGELOG_ENTRY+="$CHANGED"$'\n'
else
CHANGELOG_ENTRY+="\n"
fi
CHANGELOG_ENTRY+="
### Deprecated
### Removed
### Fixed
"
if [ -n "$FIXED" ]; then
CHANGELOG_ENTRY+="$FIXED"$'\n'
else
CHANGELOG_ENTRY+="\n"
fi
CHANGELOG_ENTRY+="
"
# Save preview to temp file for pre-push hook
PREVIEW_FILE="$REPO_ROOT/.changelog_preview.tmp"
echo "$CHANGELOG_ENTRY" > "$PREVIEW_FILE"
echo "$NEW_VERSION" > "$REPO_ROOT/.changelog_version.tmp"
# Insert after [Unreleased] section using awk (more portable)
# Find the line number after [Unreleased] section (after the "### Fixed" line)
INSERT_LINE=$(awk '/^## \[Unreleased\]/{found=1} found && /^### Fixed$/{print NR+1; exit}' "$CHANGELOG_FILE")
if [ -z "$INSERT_LINE" ]; then
# Fallback: insert after line 16 (after [Unreleased] section)
INSERT_LINE=16
fi
# Use a temp file approach to insert multiline content
TMP_FILE=$(mktemp)
{
head -n $((INSERT_LINE - 1)) "$CHANGELOG_FILE"
printf '%s' "$CHANGELOG_ENTRY"
tail -n +$INSERT_LINE "$CHANGELOG_FILE"
} > "$TMP_FILE"
mv "$TMP_FILE" "$CHANGELOG_FILE"
success "CHANGELOG.md updated with version $NEW_VERSION"
log "Changelog update complete!"
log "New version: $NEW_VERSION"
log "Bump type: $BUMP_TYPE"