Spaces:
Runtime error
Runtime error
Commit
·
48511d8
1
Parent(s):
cd635fb
temp
Browse files- .gitignore +30 -0
- Dockerfile +20 -4
- README.md +55 -0
- auth/handler.go +42 -0
- auth/jwt.go +54 -0
- auth/service.go +66 -0
- config/config.go +214 -0
- db/database.go +83 -0
- db/database_test.go +65 -0
- db/repositories.go +125 -0
- docker-compose.yml +14 -0
- files/app.db +0 -0
- files/config.json +7 -0
- files/jwt_private.pem +28 -0
- files/jwt_public.pem +9 -0
- files/secrets.json +4 -0
- files/trusted_peers.txt +3 -0
- go.mod +30 -0
- go.sum +61 -0
- main.go +97 -0
- peers/handler.go +32 -0
- peers/service.go +53 -0
- proxy/handler.go +173 -0
- routes.go +20 -0
- start.sh +9 -8
.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 |
-
#
|
|
|
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
|
34 |
-
COPY app.py /app.py
|
35 |
COPY start.sh /start.sh
|
36 |
RUN chmod +x /start.sh
|
37 |
|
38 |
# Expose ports
|
39 |
-
EXPOSE
|
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
|
6 |
|
7 |
-
# Wait for server to initialize
|
8 |
-
echo "Waiting for server to start..."
|
9 |
-
until curl -s "http://localhost:
|
10 |
sleep 1
|
11 |
done
|
12 |
|
13 |
-
echo "
|
14 |
|
15 |
-
# Start
|
16 |
-
|
17 |
-
|
|
|
|
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
|