arpinfidel commited on
Commit
48511d8
·
1 Parent(s): cd635fb
.gitignore ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # JWT keys
2
+ config/jwt_private.pem
3
+ config/jwt_public.pem
4
+
5
+ # Binaries for programs and plugins
6
+ *.exe
7
+ *.exe~
8
+ *.dll
9
+ *.so
10
+ *.dylib
11
+
12
+ # Test binary, built with `go test -c`
13
+ *.test
14
+
15
+ # Output of the go coverage tool
16
+ *.out
17
+
18
+ # Go workspace file
19
+ go.work
20
+
21
+ # Dependency directories
22
+ vendor/
23
+ bin/
24
+ pkg/
25
+
26
+ # IDE specific files
27
+ .idea/
28
+ .vscode/
29
+ *.swp
30
+ *.swo
Dockerfile CHANGED
@@ -1,6 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  FROM ubuntu:22.04
2
 
3
- # Install system dependencies
 
4
  RUN apt-get update && \
5
  apt-get install -y \
6
  build-essential \
@@ -30,13 +47,12 @@ RUN git clone https://github.com/ggerganov/llama.cpp && \
30
  RUN mkdir -p /models && \
31
  wget -O /models/model.q8_0.gguf https://huggingface.co/unsloth/DeepSeek-R1-Distill-Qwen-1.5B-GGUF/resolve/main/DeepSeek-R1-Distill-Qwen-1.5B-Q8_0.gguf
32
 
33
- # Copy app and startup script
34
- COPY app.py /app.py
35
  COPY start.sh /start.sh
36
  RUN chmod +x /start.sh
37
 
38
  # Expose ports
39
- EXPOSE 7860 8080
40
 
41
  # Start services
42
  CMD ["/start.sh"]
 
1
+ # Build stage
2
+ FROM golang:1.24-alpine AS builder
3
+
4
+ WORKDIR /app
5
+
6
+ # Copy go mod files
7
+ COPY go.mod go.sum ./
8
+ RUN go mod download
9
+
10
+ # Copy source files
11
+ COPY . .
12
+
13
+ # Build
14
+ RUN CGO_ENABLED=0 GOOS=linux go build -o /app/main
15
+
16
+ # Final stage
17
  FROM ubuntu:22.04
18
 
19
+ # Copy built binary from builder
20
+ COPY --from=builder /app/main /app/main
21
  RUN apt-get update && \
22
  apt-get install -y \
23
  build-essential \
 
47
  RUN mkdir -p /models && \
48
  wget -O /models/model.q8_0.gguf https://huggingface.co/unsloth/DeepSeek-R1-Distill-Qwen-1.5B-GGUF/resolve/main/DeepSeek-R1-Distill-Qwen-1.5B-Q8_0.gguf
49
 
50
+ # Copy startup script
 
51
  COPY start.sh /start.sh
52
  RUN chmod +x /start.sh
53
 
54
  # Expose ports
55
+ EXPOSE 8080 3000
56
 
57
  # Start services
58
  CMD ["/start.sh"]
README.md CHANGED
@@ -15,4 +15,59 @@ models:
15
  - unsloth/DeepSeek-R1-Distill-Qwen-1.5B-GGUF
16
  ---
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  Check out the configuration reference at <https://huggingface.co/docs/hub/spaces-config-reference>
 
15
  - unsloth/DeepSeek-R1-Distill-Qwen-1.5B-GGUF
16
  ---
17
 
18
+ # P2P LLM Inference Platform
19
+
20
+ A peer-to-peer platform for distributed LLM inference.
21
+
22
+ ## Current Features
23
+
24
+ - **Trusted Peers Management**:
25
+ - Load peers from configuration file
26
+ - Load peers from database
27
+ - Combine both sources for peer discovery
28
+ - **Request Forwarding**:
29
+ - Distribute requests across available peers
30
+ - Handle both regular and SSE responses
31
+
32
+ ## Planned Features
33
+
34
+ - **Trust Scores**:
35
+ - Track peer reliability and performance
36
+ - Weight request distribution based on scores
37
+ - **Peer Advertising**:
38
+ - Automatic peer discovery
39
+ - Peer-to-peer network formation
40
+ - **Enhanced Security**:
41
+ - Peer authentication
42
+ - Request validation
43
+
44
+ ## Getting Started
45
+
46
+ 1. Clone the repository
47
+ 2. Configure `files/config.json` with your settings
48
+ 3. Build and run with Docker:
49
+
50
+ ```bash
51
+ docker-compose up --build
52
+ ```
53
+
54
+ ## Configuration
55
+
56
+ Edit `files/config.json` to specify:
57
+
58
+ - Database path
59
+ - Target URL
60
+ - Trusted peers (URLs and public keys)
61
+ - Trusted peers file path
62
+
63
+ ## Development
64
+
65
+ ```bash
66
+ # Run locally
67
+ go run main.go
68
+
69
+ # Run tests
70
+ go test ./...
71
+ ```
72
+
73
  Check out the configuration reference at <https://huggingface.co/docs/hub/spaces-config-reference>
auth/handler.go ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package auth
2
+
3
+ import (
4
+ "errors"
5
+ "net/http"
6
+ )
7
+
8
+ type AuthHandler struct {
9
+ authService *AuthService
10
+ }
11
+
12
+ func NewAuthHandler(authService *AuthService) *AuthHandler {
13
+ return &AuthHandler{
14
+ authService: authService,
15
+ }
16
+ }
17
+
18
+ func (h *AuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
19
+ if r.Method != http.MethodPost {
20
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
21
+ return
22
+ }
23
+
24
+ apiKey := r.Header.Get("X-API-Key")
25
+ if apiKey == "" {
26
+ http.Error(w, "API key required", http.StatusUnauthorized)
27
+ return
28
+ }
29
+
30
+ token, err := h.authService.Authenticate(apiKey)
31
+ if err != nil {
32
+ if errors.Is(err, ErrInvalidAPIKey) {
33
+ http.Error(w, "Invalid API key", http.StatusUnauthorized)
34
+ } else {
35
+ http.Error(w, "Authentication error", http.StatusInternalServerError)
36
+ }
37
+ return
38
+ }
39
+
40
+ w.Header().Set("Content-Type", "application/json")
41
+ w.Write([]byte(`{"token":"` + token + `"}`))
42
+ }
auth/jwt.go ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package auth
2
+
3
+ import (
4
+ "crypto/rsa"
5
+ "fmt"
6
+ "net/http"
7
+ "strings"
8
+
9
+ "github.com/golang-jwt/jwt"
10
+ )
11
+
12
+ type JWTMiddleware struct {
13
+ publicKey *rsa.PublicKey
14
+ }
15
+
16
+ func NewJWTMiddleware(publicKey *rsa.PublicKey) *JWTMiddleware {
17
+ return &JWTMiddleware{publicKey: publicKey}
18
+ }
19
+
20
+ func (m *JWTMiddleware) Middleware(next http.HandlerFunc) http.Handler {
21
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
22
+ // Skip auth endpoint
23
+ if r.URL.Path == "/auth" {
24
+ next(w, r)
25
+ return
26
+ }
27
+
28
+ authHeader := r.Header.Get("Authorization")
29
+ if authHeader == "" {
30
+ http.Error(w, "Authorization header required", http.StatusUnauthorized)
31
+ return
32
+ }
33
+
34
+ tokenString := strings.TrimPrefix(authHeader, "Bearer ")
35
+ token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
36
+ if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
37
+ return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
38
+ }
39
+ return m.publicKey, nil
40
+ })
41
+
42
+ if err != nil {
43
+ http.Error(w, "Invalid token", http.StatusUnauthorized)
44
+ return
45
+ }
46
+
47
+ if !token.Valid {
48
+ http.Error(w, "Invalid token", http.StatusUnauthorized)
49
+ return
50
+ }
51
+
52
+ next(w, r)
53
+ })
54
+ }
auth/service.go ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package auth
2
+
3
+ import (
4
+ "crypto/rsa"
5
+ "errors"
6
+ "time"
7
+
8
+ "github.com/arpinfidel/p2p-llm/db"
9
+ "github.com/golang-jwt/jwt"
10
+ "golang.org/x/crypto/bcrypt"
11
+ )
12
+
13
+ type AuthService struct {
14
+ keys *rsa.PrivateKey
15
+ authRepo db.APIKeyRepository
16
+ }
17
+
18
+ func NewAuthService(privateKey *rsa.PrivateKey, authRepo db.APIKeyRepository) *AuthService {
19
+ return &AuthService{
20
+ keys: privateKey,
21
+ authRepo: authRepo,
22
+ }
23
+ }
24
+
25
+ func (s *AuthService) HashAPIKey(apiKey string) (string, error) {
26
+ hash, err := bcrypt.GenerateFromPassword([]byte(apiKey), bcrypt.DefaultCost)
27
+ if err != nil {
28
+ return "", err
29
+ }
30
+ return string(hash), nil
31
+ }
32
+
33
+ func (s *AuthService) VerifyAPIKey(apiKey, hash string) bool {
34
+ err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(apiKey))
35
+ return err == nil
36
+ }
37
+
38
+ func (s *AuthService) GenerateJWT() (string, error) {
39
+ token := jwt.New(jwt.SigningMethodRS256)
40
+ claims := token.Claims.(jwt.MapClaims)
41
+ claims["exp"] = time.Now().Add(time.Hour * 24).Unix() // Token expires in 24 hours
42
+ claims["iat"] = time.Now().Unix()
43
+
44
+ tokenString, err := token.SignedString(s.keys)
45
+ if err != nil {
46
+ return "", err
47
+ }
48
+ return tokenString, nil
49
+ }
50
+
51
+ func (s *AuthService) Authenticate(apiKey string) (string, error) {
52
+ hash, err := s.authRepo.GetActiveKeyHash()
53
+ if err != nil {
54
+ return "", err
55
+ }
56
+
57
+ if !s.VerifyAPIKey(apiKey, hash) {
58
+ return "", ErrInvalidAPIKey
59
+ }
60
+
61
+ return s.GenerateJWT()
62
+ }
63
+
64
+ var (
65
+ ErrInvalidAPIKey = errors.New("invalid API key")
66
+ )
config/config.go ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package config
2
+
3
+ import (
4
+ "crypto/rand"
5
+ "crypto/rsa"
6
+ "crypto/x509"
7
+ "encoding/json"
8
+ "encoding/pem"
9
+ "fmt"
10
+ "os"
11
+ "strings"
12
+ )
13
+
14
+ type Peer struct {
15
+ URL string
16
+ PublicKey *rsa.PublicKey
17
+ }
18
+
19
+ type Config struct {
20
+ DBPath string `json:"db_path"`
21
+ TargetURL string `json:"target_url"`
22
+ Port string `json:"port"`
23
+ MaxParallelRequests int `json:"max_parallel_requests"`
24
+ TrustedPeers []Peer `json:"trusted_peers"`
25
+ TrustedPeersPath string `json:"trusted_peers_path"`
26
+ }
27
+
28
+ type Secrets struct {
29
+ PrivateKeyPath string `json:"jwt_private_key_path"`
30
+ PublicKeyPath string `json:"jwt_public_key_path"`
31
+ }
32
+
33
+ type KeyPair struct {
34
+ PrivateKey *rsa.PrivateKey
35
+ PublicKey *rsa.PublicKey
36
+ }
37
+
38
+ func LoadConfig(path string) (*Config, error) {
39
+ file, err := os.ReadFile(path)
40
+ if err != nil {
41
+ return nil, err
42
+ }
43
+
44
+ var cfg Config
45
+ err = json.Unmarshal(file, &cfg)
46
+ return &cfg, err
47
+ }
48
+
49
+ func LoadSecrets(path string) (*Secrets, error) {
50
+ file, err := os.ReadFile(path)
51
+ if err != nil {
52
+ return nil, err
53
+ }
54
+
55
+ var secrets Secrets
56
+ err = json.Unmarshal(file, &secrets)
57
+ return &secrets, err
58
+ }
59
+
60
+ func generateRSAKeys() (*rsa.PrivateKey, error) {
61
+ return rsa.GenerateKey(rand.Reader, 2048)
62
+ }
63
+
64
+ func saveKeyToFile(key interface{}, path string) error {
65
+ file, err := os.Create(path)
66
+ if err != nil {
67
+ return err
68
+ }
69
+ defer file.Close()
70
+
71
+ var pemBlock *pem.Block
72
+ switch k := key.(type) {
73
+ case *rsa.PrivateKey:
74
+ bytes, err := x509.MarshalPKCS8PrivateKey(k)
75
+ if err != nil {
76
+ return err
77
+ }
78
+ pemBlock = &pem.Block{
79
+ Type: "PRIVATE KEY",
80
+ Bytes: bytes,
81
+ }
82
+ case *rsa.PublicKey:
83
+ bytes, err := x509.MarshalPKIXPublicKey(k)
84
+ if err != nil {
85
+ return err
86
+ }
87
+ pemBlock = &pem.Block{
88
+ Type: "PUBLIC KEY",
89
+ Bytes: bytes,
90
+ }
91
+ default:
92
+ return fmt.Errorf("unsupported key type")
93
+ }
94
+
95
+ return pem.Encode(file, pemBlock)
96
+ }
97
+
98
+ func loadKeyFromFile(path string, private bool) (interface{}, error) {
99
+ data, err := os.ReadFile(path)
100
+ if err != nil {
101
+ return nil, err
102
+ }
103
+
104
+ block, _ := pem.Decode(data)
105
+ if block == nil {
106
+ return nil, fmt.Errorf("failed to parse PEM block")
107
+ }
108
+
109
+ if private {
110
+ key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
111
+ if err != nil {
112
+ return nil, err
113
+ }
114
+ return key.(*rsa.PrivateKey), nil
115
+ }
116
+ return x509.ParsePKIXPublicKey(block.Bytes)
117
+ }
118
+
119
+ func ensureKeysExist(secrets *Secrets) (*KeyPair, error) {
120
+ if _, err := os.Stat(secrets.PrivateKeyPath); os.IsNotExist(err) {
121
+ privateKey, err := generateRSAKeys()
122
+ if err != nil {
123
+ return nil, err
124
+ }
125
+
126
+ if err := saveKeyToFile(privateKey, secrets.PrivateKeyPath); err != nil {
127
+ return nil, err
128
+ }
129
+
130
+ if err := saveKeyToFile(&privateKey.PublicKey, secrets.PublicKeyPath); err != nil {
131
+ return nil, err
132
+ }
133
+ }
134
+
135
+ privateKey, err := loadKeyFromFile(secrets.PrivateKeyPath, true)
136
+ if err != nil {
137
+ return nil, err
138
+ }
139
+
140
+ publicKey, err := loadKeyFromFile(secrets.PublicKeyPath, false)
141
+ if err != nil {
142
+ return nil, err
143
+ }
144
+
145
+ return &KeyPair{
146
+ PrivateKey: privateKey.(*rsa.PrivateKey),
147
+ PublicKey: publicKey.(*rsa.PublicKey),
148
+ }, nil
149
+ }
150
+
151
+ func LoadTrustedPeers(path string) ([]Peer, error) {
152
+ data, err := os.ReadFile(path)
153
+ if err != nil {
154
+ return nil, err
155
+ }
156
+
157
+ var peers []Peer
158
+ lines := strings.Split(string(data), "\n")
159
+ for _, line := range lines {
160
+ line = strings.TrimSpace(line)
161
+ if line == "" || strings.HasPrefix(line, "#") {
162
+ continue
163
+ }
164
+
165
+ parts := strings.SplitN(line, "|", 2)
166
+ if len(parts) != 2 {
167
+ continue
168
+ }
169
+
170
+ block, _ := pem.Decode([]byte(parts[1]))
171
+ if block == nil {
172
+ continue
173
+ }
174
+
175
+ pubKey, err := x509.ParsePKIXPublicKey(block.Bytes)
176
+ if err != nil {
177
+ continue
178
+ }
179
+
180
+ peers = append(peers, Peer{
181
+ URL: parts[0],
182
+ PublicKey: pubKey.(*rsa.PublicKey),
183
+ })
184
+ }
185
+
186
+ return peers, nil
187
+ }
188
+
189
+ func Load() (*Config, *Secrets, *KeyPair, error) {
190
+ cfg, err := LoadConfig("/app/files/config.json")
191
+ if err != nil {
192
+ return nil, nil, nil, err
193
+ }
194
+
195
+ secrets, err := LoadSecrets("/app/files/secrets.json")
196
+ if err != nil {
197
+ return nil, nil, nil, err
198
+ }
199
+
200
+ keyPair, err := ensureKeysExist(secrets)
201
+ if err != nil {
202
+ return nil, nil, nil, err
203
+ }
204
+
205
+ if cfg.TrustedPeersPath != "" {
206
+ peers, err := LoadTrustedPeers(cfg.TrustedPeersPath)
207
+ if err != nil {
208
+ return nil, nil, nil, err
209
+ }
210
+ cfg.TrustedPeers = peers
211
+ }
212
+
213
+ return cfg, secrets, keyPair, nil
214
+ }
db/database.go ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package db
2
+
3
+ import (
4
+ "database/sql"
5
+ "errors"
6
+ "log"
7
+ "os"
8
+ "path/filepath"
9
+
10
+ "github.com/arpinfidel/p2p-llm/config"
11
+ )
12
+
13
+ var (
14
+ // ErrNoRows is returned when a query returns no rows
15
+ ErrNoRows = errors.New("no rows in result set")
16
+ )
17
+
18
+ // Database defines the interface for database operations
19
+ type Database interface {
20
+ Init() error
21
+ Close()
22
+ Ping() error
23
+ Exec(query string, args ...interface{}) (sql.Result, error)
24
+ Query(query string, args ...interface{}) (*sql.Rows, error)
25
+ QueryRow(query string, args ...interface{}) *sql.Row
26
+ }
27
+
28
+ // SQLiteDB implements the Database interface for SQLite
29
+ type SQLiteDB struct {
30
+ db *sql.DB
31
+ }
32
+
33
+ // NewSQLiteDB creates a new SQLite database connection
34
+ func NewSQLiteDB(cfg *config.Config) (*SQLiteDB, error) {
35
+ // Ensure the database directory exists
36
+ dbDir := filepath.Dir(cfg.DBPath)
37
+ if dbDir != "." {
38
+ if err := os.MkdirAll(dbDir, 0755); err != nil {
39
+ return nil, err
40
+ }
41
+ }
42
+
43
+ // Open the database connection
44
+ db, err := sql.Open("sqlite", cfg.DBPath)
45
+ if err != nil {
46
+ return nil, err
47
+ }
48
+
49
+ return &SQLiteDB{db: db}, nil
50
+ }
51
+
52
+ func (s *SQLiteDB) Init() error {
53
+ // Test the connection
54
+ if err := s.Ping(); err != nil {
55
+ return err
56
+ }
57
+
58
+ log.Println("Database connection established successfully")
59
+ return nil
60
+ }
61
+
62
+ func (s *SQLiteDB) Close() {
63
+ if s.db != nil {
64
+ s.db.Close()
65
+ log.Println("Database connection closed")
66
+ }
67
+ }
68
+
69
+ func (s *SQLiteDB) Ping() error {
70
+ return s.db.Ping()
71
+ }
72
+
73
+ func (s *SQLiteDB) Exec(query string, args ...interface{}) (sql.Result, error) {
74
+ return s.db.Exec(query, args...)
75
+ }
76
+
77
+ func (s *SQLiteDB) Query(query string, args ...interface{}) (*sql.Rows, error) {
78
+ return s.db.Query(query, args...)
79
+ }
80
+
81
+ func (s *SQLiteDB) QueryRow(query string, args ...interface{}) *sql.Row {
82
+ return s.db.QueryRow(query, args...)
83
+ }
db/database_test.go ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package db
2
+
3
+ import (
4
+ "path/filepath"
5
+ "testing"
6
+ "time"
7
+
8
+ "github.com/arpinfidel/p2p-llm/config"
9
+ )
10
+
11
+ func setupTestDB(t *testing.T) (*SQLiteDB, string) {
12
+ // Create a temporary database file
13
+ tempDir := t.TempDir()
14
+ dbPath := filepath.Join(tempDir, "test.db")
15
+
16
+ // Create test config
17
+ cfg := &config.Config{
18
+ DBPath: dbPath,
19
+ }
20
+
21
+ // Initialize database
22
+ db, err := NewSQLiteDB(cfg)
23
+ if err != nil {
24
+ t.Fatalf("Failed to create test database: %v", err)
25
+ }
26
+
27
+ if err := db.Init(); err != nil {
28
+ t.Fatalf("Failed to initialize test database: %v", err)
29
+ }
30
+
31
+ // Create tables
32
+ if err := CreateTables(db); err != nil {
33
+ t.Fatalf("Failed to create tables: %v", err)
34
+ }
35
+
36
+ return db, dbPath
37
+ }
38
+
39
+ func TestAPIKeyOperations(t *testing.T) {
40
+ db, _ := setupTestDB(t)
41
+ defer db.Close()
42
+
43
+ repo := NewSQLiteAPIKeyRepository(db)
44
+
45
+ // Test CreateKey
46
+ expiresAt := time.Now().Add(24 * time.Hour)
47
+ err := repo.CreateKey("testhash", "testkey", &expiresAt)
48
+ if err != nil {
49
+ t.Errorf("CreateKey failed: %v", err)
50
+ }
51
+
52
+ // Test GetActiveKeyHash
53
+ hash, err := repo.GetActiveKeyHash()
54
+ if err != nil {
55
+ t.Errorf("GetActiveKeyHash failed: %v", err)
56
+ }
57
+ if hash != "testhash" {
58
+ t.Errorf("Expected hash 'testhash', got '%s'", hash)
59
+ }
60
+
61
+ // Test DeactivateKey
62
+ // Note: This would need to know the ID of the created key
63
+ // For a complete test, we'd need to query the ID first
64
+ // For now, we'll just verify the basic operations work
65
+ }
db/repositories.go ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package db
2
+
3
+ import "time"
4
+
5
+ // APIKeyRepository defines operations for API key management
6
+ type APIKeyRepository interface {
7
+ CreateKey(hash, name string, expiresAt *time.Time) error
8
+ GetActiveKeyHash() (string, error)
9
+ DeactivateKey(id int) error
10
+ }
11
+
12
+ // SQLiteAPIKeyRepository implements APIKeyRepository for SQLite
13
+ type SQLiteAPIKeyRepository struct {
14
+ db Database
15
+ }
16
+
17
+ // NewSQLiteAPIKeyRepository creates a new SQLite API key repository
18
+ func NewSQLiteAPIKeyRepository(db Database) *SQLiteAPIKeyRepository {
19
+ return &SQLiteAPIKeyRepository{db: db}
20
+ }
21
+
22
+ func (r *SQLiteAPIKeyRepository) CreateKey(hash, name string, expiresAt *time.Time) error {
23
+ _, err := r.db.Exec(`
24
+ INSERT INTO api_keys (key_hash, name, expires_at)
25
+ VALUES (?, ?, ?)
26
+ `, hash, name, expiresAt)
27
+ return err
28
+ }
29
+
30
+ func (r *SQLiteAPIKeyRepository) GetActiveKeyHash() (string, error) {
31
+ var hash string
32
+ err := r.db.QueryRow(`
33
+ SELECT key_hash FROM api_keys
34
+ WHERE is_active = TRUE
35
+ AND (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)
36
+ `).Scan(&hash)
37
+ return hash, err
38
+ }
39
+
40
+ func (r *SQLiteAPIKeyRepository) DeactivateKey(id int) error {
41
+ _, err := r.db.Exec(`
42
+ UPDATE api_keys
43
+ SET is_active = FALSE
44
+ WHERE id = ?
45
+ `, id)
46
+ return err
47
+ }
48
+
49
+ // PeerRepository defines operations for peer management
50
+ type PeerRepository interface {
51
+ AddPeer(url, publicKeyPEM string) error
52
+ ListTrustedPeers() ([]struct {
53
+ URL string
54
+ PublicKey string
55
+ }, error)
56
+ }
57
+
58
+ // SQLitePeerRepository implements PeerRepository for SQLite
59
+ type SQLitePeerRepository struct {
60
+ db Database
61
+ }
62
+
63
+ // NewSQLitePeerRepository creates a new SQLite peer repository
64
+ func NewSQLitePeerRepository(db Database) *SQLitePeerRepository {
65
+ return &SQLitePeerRepository{db: db}
66
+ }
67
+
68
+ func (r *SQLitePeerRepository) AddPeer(url, publicKeyPEM string) error {
69
+ _, err := r.db.Exec(`
70
+ INSERT INTO peers (url, public_key_pem)
71
+ VALUES (?, ?)
72
+ `, url, publicKeyPEM)
73
+ return err
74
+ }
75
+
76
+ func (r *SQLitePeerRepository) ListTrustedPeers() ([]struct {
77
+ URL string
78
+ PublicKey string
79
+ }, error) {
80
+ rows, err := r.db.Query(`
81
+ SELECT url, public_key_pem FROM peers
82
+ `)
83
+ if err != nil {
84
+ return nil, err
85
+ }
86
+ defer rows.Close()
87
+
88
+ var peers []struct {
89
+ URL string
90
+ PublicKey string
91
+ }
92
+ for rows.Next() {
93
+ var peer struct {
94
+ URL string
95
+ PublicKey string
96
+ }
97
+ if err := rows.Scan(&peer.URL, &peer.PublicKey); err != nil {
98
+ return nil, err
99
+ }
100
+ peers = append(peers, peer)
101
+ }
102
+ return peers, nil
103
+ }
104
+
105
+ // CreateTables creates the necessary database tables
106
+ func CreateTables(db Database) error {
107
+ _, err := db.Exec(`
108
+ CREATE TABLE IF NOT EXISTS api_keys (
109
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
110
+ key_hash TEXT NOT NULL UNIQUE,
111
+ name TEXT NOT NULL,
112
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
113
+ expires_at TIMESTAMP,
114
+ is_active BOOLEAN DEFAULT TRUE
115
+ );
116
+
117
+ CREATE TABLE IF NOT EXISTS peers (
118
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
119
+ url TEXT NOT NULL UNIQUE,
120
+ public_key_pem TEXT NOT NULL,
121
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
122
+ );
123
+ `)
124
+ return err
125
+ }
docker-compose.yml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ app:
5
+ build: .
6
+ ports:
7
+ - "8080:8080"
8
+ volumes:
9
+ - ./files:/app/files
10
+ # - ./files/config.json:/app/files/config.json
11
+ # - ./files/secrets.json:/app/files/secrets.json
12
+ environment:
13
+ - GIN_MODE=release
14
+ restart: unless-stopped
files/app.db ADDED
Binary file (16.4 kB). View file
 
files/config.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "db_path": "files/app.db",
3
+ "target_url": "http://localhost:8081",
4
+ "port": ":3000",
5
+ "max_parallel_requests": 1,
6
+ "trusted_peers_path": "files/trusted_peers.txt"
7
+ }
files/jwt_private.pem ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -----BEGIN PRIVATE KEY-----
2
+ MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCsgQauTs5z+SM5
3
+ jPBnUc9Mnhz4XFoMwZdW0bSj6ql92OxgN1jp6ytXtTG8ljhdhjZv2HzdqczXPvmH
4
+ z/UYhkwsLg3BUiV1GU9mGDRSfPlcJTuIlu4k83QO9VEsxBe1eClmD293xOvVClAF
5
+ 49Nyo41doZyAyPp8mGO2ctxB+K2+KlZ23OA8UU+wh+b+UPSlK5hxntufKyS+uiT6
6
+ lRzrzE6S2IFEMdeTcdksBDWit4LDe4cpGJV154eIq4tZq+SPSGh9ED7ZQsU3+6Ld
7
+ A4K6ZgBakn4NaqpZvr9VUyy7smUQbzDsWvTYJ2X8osvBLZhPWduhoTuXvpfQ9p50
8
+ S0ymawgFAgMBAAECggEABFgwJ+Lo+xn6MTVmBdBt4M7MxHG/merMF3zSQ5xrvNLO
9
+ dr7OGhL97erd7s5Zaw+5uY88nJg8aIjpGtbrGimOeqL7hYyDi0wiHlKDHjct2hYr
10
+ 0DRzrKfy1co48sLUm0LdFjRmZAyA5E/5o4FCYkNu11GH9viFIO71RwILi265Nca/
11
+ Kv4BwVsGe1M5AZd7Kp1pAMPIddd1EQro/eiDfeLLjvrnTUP+aTKX1zucwGDpdhYH
12
+ d0vzrMYqAsaFGoQp+/H+9kmz1p8DHRBgUr9VafLme6LORPilzUPhsIi7WLJHVB+r
13
+ udOa0R+IrBH98V8uaxDbcOVPnVmeQbV+4X8a6SE3/QKBgQDos/jLmpf4mGfylza6
14
+ hMOmx2Zs8IfYX1NR9/jNVoVyZjx7xGLOAD7t+SxL8HSXd736jZJRxX1yhIrWJBzr
15
+ imdLJVfmI5EZjS/2gEI/oKjvjdPRX1hsRy07LNrf2vPDnIKK5gCldTRTrdH0nXfR
16
+ RO65UQMrbI4xHwdJvGqnSezfewKBgQC9xjFJ1bRzL0rp+ujb5NRV1T2NMyeQ36NO
17
+ 6pAps0xKjrRpgEy+P/HjxNGLUyc6h2Pf/UTHy0ijNI5lFohYV2h14pZiOmNcpjvP
18
+ ky1Of5dSMWuRKo79n60DDLaM0ghMuqyjmOhatA+CtcV33/7HcZ0ChZG1CKw+nclM
19
+ pG6z87tefwKBgDlaynKceuKJ5ezz+khEmtiLgyJMsp7Q9/9XCBrMPX3x1uyGfffa
20
+ Nah/5rwc2w/OMqQDqtG+xGmqY3HeWsZvSYBLBvwxPf03QGAYQrveBGVu5otPXcLq
21
+ VCqmppfQJo7LD53ejMA7QBdz2zDYcwTAYbqJTiewzOcsh6ZT61GqNdjrAoGAKSXK
22
+ FhpSMA93DNismNE7AQlleTI4R/9Vp4zQiVopFpluoNmCylWPGzXXwX/cJ6Knky+V
23
+ NETtkQWaQmzqT01UhwsEVHQYi0Q3/8AHuNeNdfLlQeqaan+uwdSF2G7KAekP+cDz
24
+ 0IbuPgcvs9hLo+8Mfjl76GbjAgiwVv/oSPh2Df0CgYEAmZBBCO9N54hziRKXKdZL
25
+ 3eJ95mZo0DUzBZeuCC5i44Rq1FnHMC3J21h3mwdw25upuZZ/+VppB6DK/zPFJlhl
26
+ h/8K/7WUmGkkDnuP0risG63JaIso54DDHegH/50/rSAOMZ3rdAdZktMmmUc09mME
27
+ Erfw2B4dLfSOWWScQ58enr8=
28
+ -----END PRIVATE KEY-----
files/jwt_public.pem ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ -----BEGIN PUBLIC KEY-----
2
+ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArIEGrk7Oc/kjOYzwZ1HP
3
+ TJ4c+FxaDMGXVtG0o+qpfdjsYDdY6esrV7UxvJY4XYY2b9h83anM1z75h8/1GIZM
4
+ LC4NwVIldRlPZhg0Unz5XCU7iJbuJPN0DvVRLMQXtXgpZg9vd8Tr1QpQBePTcqON
5
+ XaGcgMj6fJhjtnLcQfitvipWdtzgPFFPsIfm/lD0pSuYcZ7bnyskvrok+pUc68xO
6
+ ktiBRDHXk3HZLAQ1oreCw3uHKRiVdeeHiKuLWavkj0hofRA+2ULFN/ui3QOCumYA
7
+ WpJ+DWqqWb6/VVMsu7JlEG8w7Fr02Cdl/KLLwS2YT1nboaE7l76X0PaedEtMpmsI
8
+ BQIDAQAB
9
+ -----END PUBLIC KEY-----
files/secrets.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "jwt_private_key_path": "files/jwt_private.pem",
3
+ "jwt_public_key_path": "files/jwt_public.pem"
4
+ }
files/trusted_peers.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # List of trusted peers in format: URL|PUBLIC_KEY_PEM
2
+ # Example:
3
+ # http://peer1.example.com|-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----
go.mod ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module github.com/arpinfidel/p2p-llm
2
+
3
+ go 1.23.0
4
+
5
+ require (
6
+ github.com/golang-jwt/jwt v3.2.2+incompatible
7
+ golang.org/x/crypto v0.36.0
8
+ modernc.org/sqlite v1.28.0
9
+ )
10
+
11
+ require (
12
+ github.com/dustin/go-humanize v1.0.1 // indirect
13
+ github.com/google/uuid v1.3.1 // indirect
14
+ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
15
+ github.com/mattn/go-isatty v0.0.20 // indirect
16
+ github.com/ncruces/go-strftime v0.1.9 // indirect
17
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
18
+ golang.org/x/mod v0.10.0 // indirect
19
+ golang.org/x/sys v0.31.0 // indirect
20
+ golang.org/x/tools v0.9.3 // indirect
21
+ lukechampine.com/uint128 v1.2.0 // indirect
22
+ modernc.org/cc/v3 v3.41.0 // indirect
23
+ modernc.org/ccgo/v3 v3.16.15 // indirect
24
+ modernc.org/libc v1.41.0 // indirect
25
+ modernc.org/mathutil v1.6.0 // indirect
26
+ modernc.org/memory v1.7.2 // indirect
27
+ modernc.org/opt v0.1.3 // indirect
28
+ modernc.org/strutil v1.1.3 // indirect
29
+ modernc.org/token v1.1.0 // indirect
30
+ )
go.sum ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
2
+ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
3
+ github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
4
+ github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
5
+ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
6
+ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
7
+ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
8
+ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
9
+ github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
10
+ github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
11
+ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
12
+ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
13
+ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
14
+ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
15
+ github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
16
+ github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
17
+ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
18
+ github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
19
+ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
20
+ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
21
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
22
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
23
+ golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
24
+ golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
25
+ golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
26
+ golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
27
+ golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
28
+ golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
29
+ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
30
+ golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
31
+ golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
32
+ golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM=
33
+ golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
34
+ lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
35
+ lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
36
+ modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q=
37
+ modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
38
+ modernc.org/ccgo/v3 v3.16.15 h1:KbDR3ZAVU+wiLyMESPtbtE/Add4elztFyfsWoNTgxS0=
39
+ modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI=
40
+ modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
41
+ modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
42
+ modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
43
+ modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
44
+ modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk=
45
+ modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY=
46
+ modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
47
+ modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
48
+ modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
49
+ modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
50
+ modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
51
+ modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
52
+ modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
53
+ modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
54
+ modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
55
+ modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
56
+ modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
57
+ modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c=
58
+ modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
59
+ modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
60
+ modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
61
+ modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE=
main.go ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package main
2
+
3
+ import (
4
+ "fmt"
5
+ "log"
6
+ "net/http"
7
+ "time"
8
+
9
+ "github.com/arpinfidel/p2p-llm/auth"
10
+ "github.com/arpinfidel/p2p-llm/config"
11
+ "github.com/arpinfidel/p2p-llm/db"
12
+ "github.com/arpinfidel/p2p-llm/peers"
13
+ "github.com/arpinfidel/p2p-llm/proxy"
14
+ _ "modernc.org/sqlite" // SQLite driver
15
+ )
16
+
17
+ type Application struct {
18
+ cfg *config.Config
19
+ database db.Database
20
+ authService *auth.AuthService
21
+ jwtMiddleware *auth.JWTMiddleware
22
+ proxyHandler *proxy.ProxyHandler
23
+ peerHandler *peers.PeerHandler
24
+ }
25
+
26
+ func NewApplication() (*Application, error) {
27
+ // Load configuration (ignore secrets since we only need keys)
28
+ cfg, _, keys, err := config.Load()
29
+ if err != nil {
30
+ return nil, fmt.Errorf("failed to load configuration: %w", err)
31
+ }
32
+
33
+ // Initialize database
34
+ database, err := db.NewSQLiteDB(cfg)
35
+ if err != nil {
36
+ return nil, fmt.Errorf("failed to initialize database: %w", err)
37
+ }
38
+
39
+ // Initialize repositories
40
+ authRepo := db.NewSQLiteAPIKeyRepository(database)
41
+ peerRepo := db.NewSQLitePeerRepository(database)
42
+
43
+ // Create tables
44
+ if err := db.CreateTables(database); err != nil {
45
+ return nil, fmt.Errorf("failed to create tables: %w", err)
46
+ }
47
+
48
+ // Initialize auth service
49
+ authService := auth.NewAuthService(keys.PrivateKey, authRepo)
50
+
51
+ // Initialize JWT middleware
52
+ jwtMiddleware := auth.NewJWTMiddleware(keys.PublicKey)
53
+
54
+ proxyHandler := proxy.NewProxyHandler(cfg, peerRepo)
55
+
56
+ // Initialize peer service and handler
57
+ peerService := peers.NewPeerService(cfg, peerRepo)
58
+ peerHandler := peers.NewPeerHandler(peerService)
59
+
60
+ return &Application{
61
+ cfg: cfg,
62
+ database: database,
63
+ authService: authService,
64
+ jwtMiddleware: jwtMiddleware,
65
+ proxyHandler: proxyHandler,
66
+ peerHandler: peerHandler,
67
+ }, nil
68
+ }
69
+
70
+ func (app *Application) Close() {
71
+ if app.database != nil {
72
+ app.database.Close()
73
+ }
74
+ }
75
+
76
+ func main() {
77
+ app, err := NewApplication()
78
+ if err != nil {
79
+ log.Fatalf("Failed to initialize application: %v", err)
80
+ }
81
+ defer app.Close()
82
+
83
+ // Create auth handler
84
+ authHandler := auth.NewAuthHandler(app.authService)
85
+
86
+ // Register routes
87
+ RegisterRoutes(authHandler, app.peerHandler, app.jwtMiddleware, app.proxyHandler)
88
+
89
+ // Start server
90
+ log.Printf("Starting proxy server on %s", app.cfg.Port)
91
+ server := &http.Server{
92
+ Addr: app.cfg.Port,
93
+ ReadTimeout: 10 * time.Minute,
94
+ WriteTimeout: 10 * time.Minute,
95
+ }
96
+ log.Fatal(server.ListenAndServe())
97
+ }
peers/handler.go ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package peers
2
+
3
+ import (
4
+ "encoding/json"
5
+ "net/http"
6
+ )
7
+
8
+ type PeerHandler struct {
9
+ peerService *PeerService
10
+ }
11
+
12
+ func NewPeerHandler(peerService *PeerService) *PeerHandler {
13
+ return &PeerHandler{
14
+ peerService: peerService,
15
+ }
16
+ }
17
+
18
+ func (h *PeerHandler) ListTrustedPeers(w http.ResponseWriter, r *http.Request) {
19
+ if r.Method != http.MethodGet {
20
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
21
+ return
22
+ }
23
+
24
+ peers, err := h.peerService.ListTrustedPeers()
25
+ if err != nil {
26
+ http.Error(w, err.Error(), http.StatusInternalServerError)
27
+ return
28
+ }
29
+
30
+ w.Header().Set("Content-Type", "application/json")
31
+ json.NewEncoder(w).Encode(peers)
32
+ }
peers/service.go ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package peers
2
+
3
+ import (
4
+ "crypto/rsa"
5
+ "crypto/x509"
6
+ "encoding/pem"
7
+
8
+ "github.com/arpinfidel/p2p-llm/config"
9
+ "github.com/arpinfidel/p2p-llm/db"
10
+ )
11
+
12
+ type PeerService struct {
13
+ cfg *config.Config
14
+ repo db.PeerRepository
15
+ }
16
+
17
+ func NewPeerService(cfg *config.Config, repo db.PeerRepository) *PeerService {
18
+ return &PeerService{
19
+ cfg: cfg,
20
+ repo: repo,
21
+ }
22
+ }
23
+
24
+ func (s *PeerService) ListTrustedPeers() ([]config.Peer, error) {
25
+ // Get peers from config
26
+ peers := s.cfg.TrustedPeers
27
+
28
+ // Get peers from database
29
+ dbPeers, err := s.repo.ListTrustedPeers()
30
+ if err != nil {
31
+ return nil, err
32
+ }
33
+
34
+ // Convert database peers to config.Peer format
35
+ for _, dbPeer := range dbPeers {
36
+ block, _ := pem.Decode([]byte(dbPeer.PublicKey))
37
+ if block == nil {
38
+ continue
39
+ }
40
+
41
+ pubKey, err := x509.ParsePKIXPublicKey(block.Bytes)
42
+ if err != nil {
43
+ continue
44
+ }
45
+
46
+ peers = append(peers, config.Peer{
47
+ URL: dbPeer.URL,
48
+ PublicKey: pubKey.(*rsa.PublicKey),
49
+ })
50
+ }
51
+
52
+ return peers, nil
53
+ }
proxy/handler.go ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package proxy
2
+
3
+ import (
4
+ "crypto/rsa"
5
+ "crypto/x509"
6
+ "encoding/pem"
7
+ "io"
8
+ "log"
9
+ "net/http"
10
+
11
+ "github.com/arpinfidel/p2p-llm/config"
12
+ "github.com/arpinfidel/p2p-llm/db"
13
+ )
14
+
15
+ type ProxyHandler struct {
16
+ cfg *config.Config
17
+ peerRepo db.PeerRepository
18
+ queue chan Request
19
+ maxParallelRequests int
20
+ maxParallelPeerRequests int
21
+ }
22
+
23
+ type Request struct {
24
+ W http.ResponseWriter
25
+ R *http.Request
26
+ }
27
+
28
+ func NewProxyHandler(cfg *config.Config, peerRepo db.PeerRepository) *ProxyHandler {
29
+ return &ProxyHandler{
30
+ cfg: cfg,
31
+ peerRepo: peerRepo,
32
+ queue: make(chan Request, 100), // Hardcoded queue size
33
+ maxParallelRequests: cfg.MaxParallelRequests,
34
+ maxParallelPeerRequests: 5, // Hardcoded peer requests limit
35
+ }
36
+ }
37
+
38
+ func (h *ProxyHandler) Handle(w http.ResponseWriter, r *http.Request) {
39
+ req := Request{W: w, R: r}
40
+ h.queue <- req
41
+ }
42
+
43
+ func (h *ProxyHandler) Run() {
44
+ // Get peers from config
45
+ peers := h.cfg.TrustedPeers
46
+
47
+ // Get peers from database
48
+ dbPeers, err := h.peerRepo.ListTrustedPeers()
49
+ if err != nil {
50
+ log.Printf("Error getting peers from database: %v", err)
51
+ } else {
52
+ for _, p := range dbPeers {
53
+ block, _ := pem.Decode([]byte(p.PublicKey))
54
+ if block == nil {
55
+ continue
56
+ }
57
+
58
+ pubKey, err := x509.ParsePKIXPublicKey(block.Bytes)
59
+ if err != nil {
60
+ continue
61
+ }
62
+
63
+ peers = append(peers, config.Peer{
64
+ URL: p.URL,
65
+ PublicKey: pubKey.(*rsa.PublicKey),
66
+ })
67
+ }
68
+ }
69
+
70
+ // Start workers for target URL
71
+ for range h.maxParallelRequests {
72
+ go func() {
73
+ for req := range h.queue {
74
+ h.Forward(req, h.cfg.TargetURL)
75
+ }
76
+ }()
77
+ }
78
+
79
+ // Start workers for each peer
80
+ for _, peer := range peers {
81
+ go func(p config.Peer) {
82
+ for req := range h.queue {
83
+ h.Forward(req, p.URL)
84
+ }
85
+ }(peer)
86
+ }
87
+ }
88
+
89
+ func (h *ProxyHandler) Forward(req Request, url string) {
90
+ // Create a new request to the target server
91
+ targetReq, err := http.NewRequest(req.R.Method, url+req.R.URL.Path, req.R.Body)
92
+ if err != nil {
93
+ log.Printf("Error creating request: %v", err)
94
+ http.Error(req.W, "Error creating request", http.StatusInternalServerError)
95
+ return
96
+ }
97
+
98
+ // Copy headers from original request
99
+ for name, values := range req.R.Header {
100
+ for _, value := range values {
101
+ targetReq.Header.Add(name, value)
102
+ }
103
+ }
104
+
105
+ // Create HTTP client
106
+ client := &http.Client{}
107
+
108
+ // Send the request to the target server
109
+ resp, err := client.Do(targetReq)
110
+ if err != nil {
111
+ log.Printf("Error forwarding request: %v", err)
112
+ http.Error(req.W, "Error forwarding request", http.StatusBadGateway)
113
+ return
114
+ }
115
+ defer resp.Body.Close()
116
+
117
+ // Check if this is an SSE response
118
+ isSSE := false
119
+ for name, values := range resp.Header {
120
+ for _, value := range values {
121
+ req.W.Header().Add(name, value)
122
+ if name == "Content-Type" && value == "text/event-stream" {
123
+ isSSE = true
124
+ }
125
+ }
126
+ }
127
+
128
+ // Set response status code
129
+ req.W.WriteHeader(resp.StatusCode)
130
+
131
+ // Handle SSE responses differently
132
+ if isSSE {
133
+ // Set necessary headers for SSE
134
+ req.W.Header().Set("Content-Type", "text/event-stream")
135
+ req.W.Header().Set("Cache-Control", "no-cache")
136
+ req.W.Header().Set("Connection", "keep-alive")
137
+ req.W.Header().Set("Transfer-Encoding", "chunked")
138
+
139
+ // Create a flusher if the ResponseWriter supports it
140
+ flusher, ok := req.W.(http.Flusher)
141
+ if !ok {
142
+ log.Printf("ResponseWriter does not support flushing")
143
+ http.Error(req.W, "Streaming unsupported", http.StatusInternalServerError)
144
+ return
145
+ }
146
+
147
+ // Buffer for reading from response body
148
+ buf := make([]byte, 1024)
149
+ for {
150
+ n, err := resp.Body.Read(buf)
151
+ if n > 0 {
152
+ // Write data to client
153
+ if _, writeErr := req.W.Write(buf[:n]); writeErr != nil {
154
+ log.Printf("Error writing to client: %v", writeErr)
155
+ break
156
+ }
157
+ // Flush data immediately to client
158
+ flusher.Flush()
159
+ }
160
+ if err != nil {
161
+ if err != io.EOF {
162
+ log.Printf("Error reading from response body: %v", err)
163
+ }
164
+ break
165
+ }
166
+ }
167
+ } else {
168
+ // For non-SSE responses, just copy the body
169
+ if _, err := io.Copy(req.W, resp.Body); err != nil {
170
+ log.Printf("Error copying response body: %v", err)
171
+ }
172
+ }
173
+ }
routes.go ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package main
2
+
3
+ import (
4
+ "net/http"
5
+
6
+ "github.com/arpinfidel/p2p-llm/auth"
7
+ "github.com/arpinfidel/p2p-llm/peers"
8
+ "github.com/arpinfidel/p2p-llm/proxy"
9
+ )
10
+
11
+ func RegisterRoutes(
12
+ authHandler *auth.AuthHandler,
13
+ peerHandler *peers.PeerHandler,
14
+ jwtMiddleware *auth.JWTMiddleware,
15
+ proxyHandler *proxy.ProxyHandler,
16
+ ) {
17
+ http.HandleFunc("/auth", authHandler.ServeHTTP)
18
+ http.HandleFunc("/trusted-peers", peerHandler.ListTrustedPeers)
19
+ http.Handle("/", jwtMiddleware.Middleware(http.HandlerFunc(proxyHandler.Handle)))
20
+ }
start.sh CHANGED
@@ -2,16 +2,17 @@
2
 
3
  # Start llama-server in background
4
  cd /llama.cpp/build
5
- ./bin/llama-server --host 0.0.0.0 --port 8080 --model /models/model.q8_0.gguf --ctx-size 32768 &
6
 
7
- # Wait for server to initialize
8
- echo "Waiting for server to start..."
9
- until curl -s "http://localhost:8080/v1/models" >/dev/null; do
10
  sleep 1
11
  done
12
 
13
- echo "Server is ready."
14
 
15
- # Start Gradio UI
16
- cd /
17
- python3 app.py
 
 
2
 
3
  # Start llama-server in background
4
  cd /llama.cpp/build
5
+ ./bin/llama-server --host 0.0.0.0 --port 8081 --model /models/model.q8_0.gguf --ctx-size 32768 &
6
 
7
+ # Wait for llama-server to initialize
8
+ echo "Waiting for llama-server to start..."
9
+ until curl -s "http://localhost:8081/v1/models" >/dev/null; do
10
  sleep 1
11
  done
12
 
13
+ echo "llama-server is ready."
14
 
15
+ # Start Go application (main service)
16
+ echo "Starting Go application..."
17
+ cd /app
18
+ exec ./main