chore: finalize alpha-0.0.1 and freeze core engine
This commit is contained in:
parent
d8f48970c2
commit
497f188c89
@ -1,11 +1,11 @@
|
||||
root = "."
|
||||
testdata_dir = "testdata"
|
||||
tmp_dir = "tmp"
|
||||
tmp_dir = "build"
|
||||
|
||||
[build]
|
||||
args_bin = []
|
||||
bin = "./tmp/main"
|
||||
cmd = "go build -o ./tmp/main ./internal/cmd/server/"
|
||||
bin = "make run"
|
||||
cmd = "make build"
|
||||
delay = 1000
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||
exclude_file = []
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,4 +2,4 @@ tmp/
|
||||
.env
|
||||
config.yaml
|
||||
config.yml
|
||||
|
||||
build
|
||||
|
||||
32
Dockerfile
Normal file
32
Dockerfile
Normal file
@ -0,0 +1,32 @@
|
||||
FROM golang:1.25-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache make
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN make build-static \
|
||||
&& cp build/clavis /clavis \
|
||||
&& chmod +x /clavis
|
||||
|
||||
FROM alpine:latest AS production
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /clavis /app/clavis
|
||||
|
||||
RUN apk update --no-cache && apk add --no-cache ca-certificates
|
||||
|
||||
ENV LISTEN_ADDRESS=0.0.0.0 \
|
||||
LISTEN_PORT=3000 \
|
||||
POSTGRES_USER=root \
|
||||
POSTGRES_PASSWORD=Ch@nG!E.ME!!\
|
||||
POSTGRES_ADDRESS=postgresql \
|
||||
POSTGRES_PORT=5432 \
|
||||
POSTGRES_DATABASE="clavis" \
|
||||
VERIFY_DEFAULT=true
|
||||
|
||||
CMD ["/app/clavis"]
|
||||
10
Makefile
Normal file
10
Makefile
Normal file
@ -0,0 +1,10 @@
|
||||
build:
|
||||
go build -o build/clavis ./internal/cmd/server/
|
||||
|
||||
build-static:
|
||||
CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o build/clavis ./internal/cmd/server
|
||||
|
||||
run: build
|
||||
./build/clavis
|
||||
|
||||
|
||||
74
README.md
Normal file
74
README.md
Normal file
@ -0,0 +1,74 @@
|
||||
# HKP-Clavis
|
||||
|
||||
HKP-Clavis was born out of a desire for simplicity. I found existing solutions like hkp-hagrid to be over-engineered and difficult to administer for smaller, focused deployments. My goal was to create a "minimally stable" implementation of an HKP server that does exactly what is needed—no more, no less—while maintaining high performance.
|
||||
|
||||
- Note on Storage: During testing, it became clear that handling large keys (>2MB) directly in PostgreSQL isn't ideal. In the future, I plan to add S3/FileStorage support for blobs to keep the database lean and mean
|
||||
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
* **HKP Protocol Support**: Implements core `add`, `get`, and `index` operations.
|
||||
* **High-Performance Architecture**:
|
||||
* **Object Pooling**: Uses `sync.Pool` via a custom `KeyManager` to reuse `PGPKey` and `PGPUid` objects, significantly reducing GC pressure.
|
||||
* **Efficient Memory Allocation**: Controller and Service layers are built to minimize allocations during key parsing and transformation.
|
||||
* **Database Optimization**:
|
||||
* **PostgreSQL Backend**: Utilizes `pgx/v5` for robust connection pooling.
|
||||
* **Security & Sanitization**:
|
||||
* **Verification-Aware**: Only returns UIDs that have been marked as verified in the database.
|
||||
* **Key Sanitization**: Automatically filters out sensitive or unverified metadata before serving keys.
|
||||
|
||||
## 🛠 Tech Stack
|
||||
|
||||
* **Language**: Go 1.22+
|
||||
* **Database**: PostgreSQL
|
||||
* **Library**: `ProtonMail/go-crypto` (for OpenPGP operations)
|
||||
* **Driver**: `pgx/v5`
|
||||
|
||||
## 📋 Backlog & Future Roadmap
|
||||
|
||||
The project is currently in a "feature freeze" for the core engine. The following features are planned for future development:
|
||||
|
||||
### 1. 🌐 GOTH Stack Web Interface
|
||||
* **Frontend**: Implement a lightweight web UI using **Go Templates + HTMX** (The GOTH Stack).
|
||||
* **Search**: A user-friendly search bar for finding keys without using the CLI.
|
||||
* **Stats**: A dashboard showing server uptime, key count, and pool utilization metrics.
|
||||
* **Kick GPG**: Debug. why key.openpgp.org server (hagrid) and clavis not work with `gpg --recv-key` T__T
|
||||
|
||||
### 2. 🗄️ Hybrid Storage & Large Payload Handling
|
||||
* **Blob Storage Integration**: Implement an external Blob Storage (e.g., MinIO, S3, or local disk) for keys exceeding **1MB** in size to keep the PostgreSQL database lean.
|
||||
* **Metadata/Binary Split**: Store key metadata in Postgres and the actual `.asc` packets in Blob storage.
|
||||
|
||||
### 3. 📧 Email Verification System
|
||||
* **SMTP Service**: Activate the `SMTPPool` for sending verification emails.
|
||||
* **Template Engine**: Support for both HTML and Plaintext email templates (required for classic PGP users).
|
||||
* **Verification Route**: Implement the `GET /verify` endpoint to process tokens and update UID status.
|
||||
|
||||
### 4. 🛡️ System Hardening
|
||||
* **Rate Limiting**: Add middleware to prevent brute-force key uploads or search "scraping".
|
||||
* **SKS Peering**: Support for the synchronization protocol to federate with other global keyservers.
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
The application is configured via environment variables:
|
||||
|
||||
| Variable | Description |
|
||||
| :--- | :--- |
|
||||
| `LISTEN_ADDRESS` | Host to bind the server (e.g., `localhost`) |
|
||||
| `LISTEN_PORT` | Port for the HKP service (e.g., `8181`) |
|
||||
| `POSTGRES_USER` | Database user |
|
||||
| `POSTGRES_PASSWORD` | Database password |
|
||||
| `POSTGRES_ADDRESS` | Database host |
|
||||
| `POSTGRES_PORT` | Database port |
|
||||
| `POSTGRES_DATABASE` | Database name |
|
||||
| `VERIFY_DEFAULT` | If `true`, new keys are marked as verified immediately (Dev mode) |
|
||||
|
||||
ps: verify by smtp not implemented `VERIFY_DEFAULT=true` is recommended
|
||||
|
||||
## 📦 Installation & Usage
|
||||
|
||||
```bash
|
||||
# Build the binary
|
||||
go build -o build/hkp-clavis internal/cmd/server/
|
||||
|
||||
# Run the server (ensure env vars are set)
|
||||
./build/hkp-clavis
|
||||
@ -1,9 +1,41 @@
|
||||
services:
|
||||
postgres_test:
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
container_name: clavis_db
|
||||
environment:
|
||||
POSTGRES_DB: test_hagrid_db
|
||||
POSTGRES_USER: test_user
|
||||
POSTGRES_PASSWORD: test_password
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
# ports:
|
||||
# - "${POSTGRES_PORT}:${POSTGRES_PORT}"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
clavis:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: clavis_app
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- LISTEN_ADDRESS=${LISTEN_ADDRESS}
|
||||
- LISTEN_PORT=${LISTEN_PORT}
|
||||
- POSTGRES_USER=${POSTGRES_USER}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
- POSTGRES_ADDRESS=${POSTGRES_HOST}
|
||||
- POSTGRES_PORT=${POSTGRES_PORT}
|
||||
- POSTGRES_DATABASE=${POSTGRES_DB}
|
||||
- VERIFY_DEFAULT=${VERIFY_DEFAULT}
|
||||
ports:
|
||||
- "5433:5432"
|
||||
- "${EXTERNAL_PORT}:${LISTEN_PORT}"
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
4
go.mod
4
go.mod
@ -1,4 +1,4 @@
|
||||
module gadrid
|
||||
module hkp-clavis
|
||||
|
||||
go 1.24.4
|
||||
|
||||
@ -6,6 +6,8 @@ require (
|
||||
github.com/ProtonMail/go-crypto v1.3.0
|
||||
github.com/emersion/go-openpgp-hkp v0.0.0-20231106091114-bb7c1dad7252
|
||||
github.com/jackc/pgx/v5 v5.7.5
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/lmittmann/tint v1.1.3
|
||||
github.com/stretchr/testify v1.8.1
|
||||
)
|
||||
|
||||
|
||||
4
go.sum
4
go.sum
@ -16,10 +16,14 @@ github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
|
||||
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I=
|
||||
github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
|
||||
@ -3,43 +3,105 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"gadrid/internal/repo/storage/postgresql"
|
||||
storageService "gadrid/internal/service/storage/serv"
|
||||
controllerHkp "hkp-clavis/internal/controller/hkp"
|
||||
"hkp-clavis/internal/model"
|
||||
"hkp-clavis/internal/repo/storage/postgresql"
|
||||
storageService "hkp-clavis/internal/service/storage/serv"
|
||||
|
||||
hkp "github.com/emersion/go-openpgp-hkp"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/lmittmann/tint"
|
||||
)
|
||||
|
||||
const (
|
||||
listenAddress = "localhost:8181"
|
||||
)
|
||||
var Logger *slog.Logger
|
||||
|
||||
func main() {
|
||||
|
||||
dbpool, err := pgxpool.New(context.TODO(), "postgresql://test_user:test_password@localhost:5433/test_hagrid_db")
|
||||
Logger = slog.New(tint.NewHandler(os.Stdout, nil))
|
||||
|
||||
if err := godotenv.Load(); err != nil {
|
||||
log.Println("No .env file found, reading from system environment")
|
||||
}
|
||||
|
||||
listenAddress := os.Getenv("LISTEN_ADDRESS")
|
||||
listenPort := os.Getenv("LISTEN_PORT")
|
||||
|
||||
listenAddress = fmt.Sprintf("%s:%s", listenAddress, listenPort)
|
||||
|
||||
pgUser := os.Getenv("POSTGRES_USER")
|
||||
pgPassword := os.Getenv("POSTGRES_PASSWORD")
|
||||
pgAddress := os.Getenv("POSTGRES_ADDRESS")
|
||||
pgPort := os.Getenv("POSTGRES_PORT")
|
||||
pgDatabase := os.Getenv("POSTGRES_DATABASE")
|
||||
|
||||
// By default all uids in key does not verify
|
||||
// For dev/debug you can set it to true
|
||||
verify := os.Getenv("VERIFY_DEFAULT")
|
||||
defaultVerify := getEnvAsBool(verify, true)
|
||||
|
||||
pgUri := fmt.Sprintf(
|
||||
"postgresql://%s:%s@%s:%s/%s",
|
||||
pgUser,
|
||||
pgPassword,
|
||||
pgAddress,
|
||||
pgPort,
|
||||
pgDatabase)
|
||||
|
||||
// For memory allocation controller
|
||||
keyMngr := model.NewKeyManager()
|
||||
|
||||
dbpool, err := pgxpool.New(context.TODO(), pgUri)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
dbRepo := postgresql.New(context.TODO(), dbpool)
|
||||
storageService := storageService.New(dbRepo)
|
||||
db := postgresql.New(context.TODO(), dbpool, keyMngr)
|
||||
storage := storageService.New(db, keyMngr, defaultVerify)
|
||||
|
||||
handler := hkp.Handler{
|
||||
Adder: storageService,
|
||||
Lookuper: storageService,
|
||||
controller, err := controllerHkp.New(storage)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
http.HandleFunc("/pks/lookup", func(w http.ResponseWriter, r *http.Request) {
|
||||
handler.ServeHTTP(w, r)
|
||||
mux := http.NewServeMux()
|
||||
hkpHandler := hkp.Handler{
|
||||
Adder: controller,
|
||||
Lookuper: controller,
|
||||
}
|
||||
mux.HandleFunc("/pks/lookup", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/pgp-keys")
|
||||
w.Header().Set("Connection", "close")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
|
||||
hkpHandler.ServeHTTP(w, r)
|
||||
})
|
||||
http.HandleFunc("/pks/add", func(w http.ResponseWriter, r *http.Request) {
|
||||
handler.ServeHTTP(w, r)
|
||||
mux.HandleFunc("/pks/add", func(w http.ResponseWriter, r *http.Request) {
|
||||
hkpHandler.ServeHTTP(w, r)
|
||||
})
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, "Index Page")
|
||||
})
|
||||
fmt.Println("Server is listening on address:", listenAddress)
|
||||
http.ListenAndServe(listenAddress, nil)
|
||||
|
||||
handler := LogMiddleware(mux)
|
||||
|
||||
Logger.Info("Server is listening on address: %s", handler)
|
||||
http.ListenAndServe(listenAddress, handler)
|
||||
}
|
||||
|
||||
func getEnvAsBool(name string, defaultVal bool) bool {
|
||||
valStr, exists := os.LookupEnv(name) // Use LookupEnv for better checking
|
||||
if !exists || valStr == "" {
|
||||
return defaultVal
|
||||
}
|
||||
if val, err := strconv.ParseBool(valStr); err == nil {
|
||||
return val
|
||||
}
|
||||
return defaultVal
|
||||
}
|
||||
|
||||
69
internal/cmd/server/middlewareLogger.go
Normal file
69
internal/cmd/server/middlewareLogger.go
Normal file
@ -0,0 +1,69 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type responseWriter struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (rw *responseWriter) WriteHeader(code int) {
|
||||
rw.statusCode = code
|
||||
rw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
func (rw *responseWriter) Write(b []byte) (int, error) {
|
||||
if rw.statusCode == 0 {
|
||||
rw.statusCode = http.StatusOK
|
||||
}
|
||||
return rw.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
var rwPool = sync.Pool{
|
||||
New: func() any {
|
||||
return &responseWriter{}
|
||||
},
|
||||
}
|
||||
|
||||
// LogMiddleware
|
||||
func LogMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
|
||||
rw := rwPool.Get().(*responseWriter)
|
||||
defer func() {
|
||||
rw.ResponseWriter = nil
|
||||
rwPool.Put(rw)
|
||||
}()
|
||||
|
||||
rw.ResponseWriter = w
|
||||
// rw.statusCode = http.StatusOK
|
||||
|
||||
next.ServeHTTP(rw, r)
|
||||
|
||||
Logger.Info("request handled",
|
||||
slog.String("ip", getIP(r)),
|
||||
slog.String("method", r.Method),
|
||||
slog.Int("status", rw.statusCode),
|
||||
slog.String("path", r.URL.Path),
|
||||
slog.Duration("duration", time.Since(start)),
|
||||
)
|
||||
})
|
||||
}
|
||||
func getIP(r *http.Request) string {
|
||||
if ip := r.Header.Get("X-Real-IP"); ip != "" {
|
||||
return ip
|
||||
}
|
||||
if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
|
||||
if i := strings.Index(forwarded, ","); i != -1 {
|
||||
return forwarded[:i]
|
||||
}
|
||||
return forwarded
|
||||
}
|
||||
return r.RemoteAddr
|
||||
}
|
||||
@ -1,7 +1,11 @@
|
||||
package hkp
|
||||
|
||||
import (
|
||||
"gadrid/internal/service/storage"
|
||||
"context"
|
||||
"fmt"
|
||||
"hkp-clavis/internal/service/storage"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
hkp "github.com/emersion/go-openpgp-hkp"
|
||||
@ -10,24 +14,69 @@ import (
|
||||
type HkpController struct {
|
||||
hkp.Adder
|
||||
hkp.Lookuper
|
||||
storage storage.StorageInterface
|
||||
// verify
|
||||
storageService storage.StorageInterface
|
||||
// verifyService
|
||||
}
|
||||
|
||||
const (
|
||||
defaultTimeout = 30 * time.Second
|
||||
minEntropy = 15
|
||||
)
|
||||
|
||||
func New(st storage.StorageInterface) (HkpController, error) {
|
||||
return HkpController{
|
||||
storage: st,
|
||||
storageService: st,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s HkpController) Add(k openpgp.EntityList) error {
|
||||
func (s HkpController) Add(el openpgp.EntityList) error {
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*120)
|
||||
defer cancel()
|
||||
|
||||
k, err := s.storageService.Add(ctx, el)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hkp controller error: Add: %w", err)
|
||||
}
|
||||
|
||||
_ = k
|
||||
// Send Verify mail
|
||||
// PS: Now verify false and you cant get key from DB
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s HkpController) Get(r *hkp.LookupRequest) (openpgp.EntityList, error) {
|
||||
return openpgp.EntityList{}, nil
|
||||
func (s HkpController) Get(req *hkp.LookupRequest) (openpgp.EntityList, error) {
|
||||
|
||||
req.Search = strings.Trim(req.Search, "0x")
|
||||
|
||||
if len(req.Search) <= 15 {
|
||||
return nil, fmt.Errorf("controller error: Get: Low entropy string for search")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
k, err := s.storageService.Get(ctx, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("controller error: Get: %w", err)
|
||||
}
|
||||
|
||||
return k, err
|
||||
}
|
||||
|
||||
func (s HkpController) Index(r *hkp.LookupRequest) ([]hkp.IndexKey, error) {
|
||||
return []hkp.IndexKey{}, nil
|
||||
func (s HkpController) Index(req *hkp.LookupRequest) ([]hkp.IndexKey, error) {
|
||||
|
||||
if len(req.Search) < 5 {
|
||||
return nil, fmt.Errorf("controller error: Index: short string for search")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
k, err := s.storageService.Index(ctx, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("controller error: Get: %w", err)
|
||||
}
|
||||
|
||||
return k, nil
|
||||
}
|
||||
|
||||
143
internal/model/converter.go
Normal file
143
internal/model/converter.go
Normal file
@ -0,0 +1,143 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
||||
)
|
||||
|
||||
func extractEmail(fullNameWithEmail string) string {
|
||||
re := regexp.MustCompile(`<([^>]+)>`)
|
||||
matches := re.FindStringSubmatch(fullNameWithEmail)
|
||||
if len(matches) > 1 {
|
||||
return matches[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var (
|
||||
bufferPool = sync.Pool{
|
||||
New: func() any { return new(bytes.Buffer) },
|
||||
}
|
||||
)
|
||||
|
||||
// ConverterPgpToService converts a raw openpgp.EntityList into a slice of PGPkey objects.
|
||||
// It parses key data, generates verification tokens for UIDs, and extracts email addresses.
|
||||
//
|
||||
// Parameters:
|
||||
//
|
||||
// entities: The openpgp.EntityList obtained from parsing an armored PGP block.
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// []*PGPkey: A slice of converted PGPkey objects. Returns an empty (non-nil) slice if no valid entities are found.
|
||||
// error: An error if any entity fails to be processed or contains invalid data.
|
||||
func (m *KeyManager) ConverterPgpToService(entities openpgp.EntityList, defaultVerifyValue bool) ([]*PGPKey, func(), error) {
|
||||
res := make([]*PGPKey, 0, len(entities))
|
||||
release := func() {
|
||||
m.ReleaseSlice(res)
|
||||
}
|
||||
|
||||
for _, entity := range entities {
|
||||
if entity == nil || entity.PrimaryKey == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
buf := bufferPool.Get().(*bytes.Buffer)
|
||||
buf.Reset()
|
||||
|
||||
publicKeyWriter, err := armor.Encode(buf, openpgp.PublicKeyType, nil)
|
||||
if err != nil {
|
||||
bufferPool.Put(buf)
|
||||
return nil, release, fmt.Errorf("armor encode error: %w", err)
|
||||
}
|
||||
|
||||
if err := entity.Serialize(publicKeyWriter); err != nil {
|
||||
publicKeyWriter.Close()
|
||||
bufferPool.Put(buf)
|
||||
return nil, release, fmt.Errorf("serialize error: %w", err)
|
||||
}
|
||||
publicKeyWriter.Close()
|
||||
|
||||
pgpKey := pgpKeyPool.Get().(*PGPKey)
|
||||
pgpKey.Reset()
|
||||
|
||||
pgpKey.Fingerprint = strings.ToUpper(hex.EncodeToString(entity.PrimaryKey.Fingerprint))
|
||||
pgpKey.Packet = buf.String()
|
||||
pgpKey.UpdateTime = entity.PrimaryKey.CreationTime.UTC()
|
||||
pgpKey.Revoked = entity.Revoked(time.Now())
|
||||
|
||||
bufferPool.Put(buf)
|
||||
|
||||
for _, identity := range entity.Identities {
|
||||
if identity == nil || identity.UserId == nil || identity.Name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
extractedEmail := extractEmail(identity.Name)
|
||||
if extractedEmail == "" {
|
||||
log.Printf("Warning: no email in UID %s for key %s", identity.Name, pgpKey.Fingerprint)
|
||||
continue
|
||||
}
|
||||
|
||||
var tokenRaw [16]byte
|
||||
if _, err := rand.Read(tokenRaw[:]); err != nil {
|
||||
return nil, release, fmt.Errorf("token gen error: %w", err)
|
||||
}
|
||||
token := hex.EncodeToString(tokenRaw[:])
|
||||
|
||||
pgpKey.Uids = append(pgpKey.Uids, &PGPUid{
|
||||
Verify: defaultVerifyValue,
|
||||
UIDString: identity.Name,
|
||||
Email: extractedEmail,
|
||||
Token: token,
|
||||
TokenExpires: time.Now().Add(time.Hour * 5),
|
||||
Fingerprint: pgpKey.Fingerprint,
|
||||
})
|
||||
}
|
||||
|
||||
res = append(res, pgpKey)
|
||||
}
|
||||
|
||||
return res, release, nil
|
||||
}
|
||||
|
||||
func SanitizeAndFilterKey(dbKey *PGPKey) (openpgp.EntityList, error) {
|
||||
|
||||
parsedEntities, err := openpgp.ReadArmoredKeyRing(strings.NewReader(dbKey.Packet))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse key packet for %s: %w", dbKey.Fingerprint, err)
|
||||
}
|
||||
|
||||
if len(parsedEntities) == 0 {
|
||||
return nil, fmt.Errorf("parsed key packet for %s yielded no entities", dbKey.Fingerprint)
|
||||
}
|
||||
var respEntities openpgp.EntityList
|
||||
for _, e := range parsedEntities {
|
||||
if e == nil {
|
||||
return nil, fmt.Errorf("no entity with matching fingerprint %s found in parsed packet from DB", dbKey.Fingerprint)
|
||||
}
|
||||
verifiedUIDStringsFromDB := make(map[string]struct{})
|
||||
for _, dbUID := range dbKey.Uids {
|
||||
verifiedUIDStringsFromDB[dbUID.UIDString] = struct{}{}
|
||||
}
|
||||
filteredIdentities := make(map[string]*openpgp.Identity)
|
||||
for identityKey, identity := range e.Identities {
|
||||
if _, isVerified := verifiedUIDStringsFromDB[identity.Name]; isVerified {
|
||||
filteredIdentities[identityKey] = identity // Keep this identity
|
||||
}
|
||||
}
|
||||
e.Identities = filteredIdentities
|
||||
respEntities = append(respEntities, e)
|
||||
}
|
||||
return respEntities, nil
|
||||
}
|
||||
59
internal/model/manager.go
Normal file
59
internal/model/manager.go
Normal file
@ -0,0 +1,59 @@
|
||||
package model
|
||||
|
||||
import "sync"
|
||||
|
||||
var pgpKeyPool = sync.Pool{
|
||||
New: func() any {
|
||||
return &PGPKey{
|
||||
Uids: make([]*PGPUid, 0, 10),
|
||||
}
|
||||
},
|
||||
}
|
||||
var pgpUidPool = sync.Pool{
|
||||
New: func() any {
|
||||
return &PGPUid{}
|
||||
},
|
||||
}
|
||||
|
||||
// Struct for managed keys allocation in memory
|
||||
type KeyManager struct{}
|
||||
|
||||
func NewKeyManager() *KeyManager {
|
||||
return &KeyManager{}
|
||||
}
|
||||
|
||||
func (m *KeyManager) Get() *PGPKey {
|
||||
k := pgpKeyPool.Get().(*PGPKey)
|
||||
k.Reset()
|
||||
return k
|
||||
}
|
||||
func (m *KeyManager) GetUid() *PGPUid {
|
||||
u := pgpUidPool.Get().(*PGPUid)
|
||||
u.Reset()
|
||||
return u
|
||||
}
|
||||
|
||||
func (m *KeyManager) ReleaseUid(u ...*PGPUid) {
|
||||
for _, u := range u {
|
||||
if u == nil {
|
||||
continue
|
||||
}
|
||||
pgpUidPool.Put(u)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *KeyManager) Release(keys ...*PGPKey) {
|
||||
for _, k := range keys {
|
||||
if k == nil {
|
||||
continue
|
||||
}
|
||||
m.ReleaseUid(k.Uids...)
|
||||
// Unlink all uids objects
|
||||
k.Uids = k.Uids[:0]
|
||||
pgpKeyPool.Put(k)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *KeyManager) ReleaseSlice(keys []*PGPKey) {
|
||||
m.Release(keys...)
|
||||
}
|
||||
37
internal/model/pgp_model.go
Normal file
37
internal/model/pgp_model.go
Normal file
@ -0,0 +1,37 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type PGPKey struct {
|
||||
Revoked bool
|
||||
UpdateTime time.Time
|
||||
Fingerprint string
|
||||
Packet string
|
||||
Uids []*PGPUid
|
||||
}
|
||||
|
||||
// Clear data from object
|
||||
func (k *PGPKey) Reset() {
|
||||
k.Fingerprint = ""
|
||||
k.Packet = ""
|
||||
k.Revoked = false
|
||||
k.Uids = k.Uids[:0]
|
||||
}
|
||||
|
||||
type PGPUid struct {
|
||||
Verify bool
|
||||
Token string
|
||||
TokenExpires time.Time
|
||||
Email string
|
||||
UIDString string // "Full Name (comment) <email@example.com>"
|
||||
Fingerprint string
|
||||
}
|
||||
|
||||
func (u *PGPUid) Reset() {
|
||||
u.Verify = false
|
||||
u.Token = ""
|
||||
u.TokenExpires = time.Time{}
|
||||
u.Email = ""
|
||||
u.UIDString = ""
|
||||
u.Fingerprint = ""
|
||||
}
|
||||
5
internal/repo/mail/interface.go
Normal file
5
internal/repo/mail/interface.go
Normal file
@ -0,0 +1,5 @@
|
||||
package mail
|
||||
|
||||
type MailSenderRepo interface {
|
||||
SendVerifyMessage()
|
||||
}
|
||||
1
internal/repo/mail/smtp/smtp.go
Normal file
1
internal/repo/mail/smtp/smtp.go
Normal file
@ -0,0 +1 @@
|
||||
package smtp
|
||||
@ -3,7 +3,7 @@ package storage
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
service "gadrid/internal/service/storage"
|
||||
"hkp-clavis/internal/model"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -17,11 +17,11 @@ type StorageRepositotyInterface interface {
|
||||
//
|
||||
// Parameters:
|
||||
// ctx: Context for the operation.
|
||||
// keys: A slice of service.PGPkey objects to be added or updated.
|
||||
// keys: A slice of model.PGPKey objects to be added or updated.
|
||||
//
|
||||
// Returns:
|
||||
// error: An error if the operation fails, wrapped with context (e.g., database connection issues, SQL execution errors).
|
||||
AddKey(ctx context.Context, keys []*service.PGPkey) error
|
||||
AddKey(ctx context.Context, keys []*model.PGPKey) error
|
||||
|
||||
// VerifyUID updates the verification status of a specific User ID (UID) based on a token.
|
||||
// It checks the token's validity and expiry before setting 'verified' to true and clearing token fields.
|
||||
@ -50,11 +50,11 @@ type StorageRepositotyInterface interface {
|
||||
// fingerprint: The fingerprint of the PGP key to retrieve.
|
||||
//
|
||||
// Returns:
|
||||
// []*service.PGPkey: A slice containing the retrieved PGP key (or an empty slice if not found).
|
||||
// []*model.PGPKey: A slice containing the retrieved PGP key (or an empty slice if not found).
|
||||
// error: Returns an error if:
|
||||
// - The key is not found (errors.Is(err, ErrKeyNotFound)).
|
||||
// - Database query or scanning fails.
|
||||
GetKey(ctx context.Context, fingerprint string) ([]*service.PGPkey, error)
|
||||
GetKey(ctx context.Context, fingerprint string) ([]*model.PGPKey, error)
|
||||
|
||||
// Index searches for PGP keys based on a query string.
|
||||
// It matches against UID strings and emails using case-insensitive regular expressions.
|
||||
@ -65,9 +65,9 @@ type StorageRepositotyInterface interface {
|
||||
// q: The query string (treated as a case-insensitive regular expression).
|
||||
//
|
||||
// Returns:
|
||||
// []*service.PGPkey: A slice of matching PGPKey objects, each containing only their verified UIDs.
|
||||
// []*model.PGPKey: A slice of matching PGPKey objects, each containing only their verified UIDs.
|
||||
// error: Returns an error if database query or scanning fails.
|
||||
Index(ctx context.Context, q string) ([]*service.PGPkey, error)
|
||||
Index(ctx context.Context, q string) ([]*model.PGPKey, error)
|
||||
// CleanupStaleKeys identifies and deletes PGP keys that no longer have any associated UIDs.
|
||||
// This method is intended to be run periodically, e.g., by a cron job.
|
||||
//
|
||||
|
||||
@ -7,34 +7,35 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
service "gadrid/internal/service/storage" // Adjust this import path
|
||||
"hkp-clavis/internal/model"
|
||||
// Adjust this import path
|
||||
)
|
||||
|
||||
// InMemStorageMock is a mock implementation of PGPKeyStorage for testing purposes.
|
||||
// It stores data in memory, mimicking database operations.
|
||||
type InMemStorageMock struct {
|
||||
keys map[string]*service.PGPkey
|
||||
uids map[string]map[string]*service.PGPUid // fingerprint -> uid_string -> UID
|
||||
mu sync.RWMutex // Mutex for concurrent access safety
|
||||
keys map[string]*model.PGPKey
|
||||
uids map[string]map[string]*model.PGPUid // fingerprint -> uid_string -> UID
|
||||
mu sync.RWMutex // Mutex for concurrent access safety
|
||||
}
|
||||
|
||||
// NewInMemStorageMock creates a new in-memory mock storage.
|
||||
func NewInMemStorageMock() *InMemStorageMock {
|
||||
return &InMemStorageMock{
|
||||
keys: make(map[string]*service.PGPkey),
|
||||
uids: make(map[string]map[string]*service.PGPUid),
|
||||
keys: make(map[string]*model.PGPKey),
|
||||
uids: make(map[string]map[string]*model.PGPUid),
|
||||
}
|
||||
}
|
||||
|
||||
// AddKey implements PGPKeyStorage interface for the mock.
|
||||
// See PGPKeyStorage interface for documentation.
|
||||
func (m *InMemStorageMock) AddKey(ctx context.Context, keys []*service.PGPkey) error {
|
||||
func (m *InMemStorageMock) AddKey(ctx context.Context, keys []*model.PGPKey) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
for _, k := range keys {
|
||||
// Simulate UPSERT for pgp_keys
|
||||
m.keys[k.Fingerprint] = &service.PGPkey{ // Deep copy
|
||||
m.keys[k.Fingerprint] = &model.PGPKey{ // Deep copy
|
||||
Fingerprint: k.Fingerprint,
|
||||
Packet: k.Packet,
|
||||
Revoked: k.Revoked,
|
||||
@ -43,14 +44,14 @@ func (m *InMemStorageMock) AddKey(ctx context.Context, keys []*service.PGPkey) e
|
||||
|
||||
// Ensure the inner map exists for UIDs
|
||||
if _, ok := m.uids[k.Fingerprint]; !ok {
|
||||
m.uids[k.Fingerprint] = make(map[string]*service.PGPUid)
|
||||
m.uids[k.Fingerprint] = make(map[string]*model.PGPUid)
|
||||
}
|
||||
|
||||
// Simulate INSERT (ON CONFLICT DO NOTHING) for pgp_uids
|
||||
for _, u := range k.Uids {
|
||||
if _, exists := m.uids[k.Fingerprint][u.UIDString]; !exists {
|
||||
m.uids[k.Fingerprint][u.UIDString] = &service.PGPUid{ // Deep copy
|
||||
UIDString: u.UIDString,
|
||||
m.uids[k.Fingerprint][u.UIDString] = &model.PGPUid{ // Deep copy
|
||||
UIDString: u.UIDString,
|
||||
Email: u.Email,
|
||||
Verify: u.Verify,
|
||||
Token: u.Token,
|
||||
@ -73,7 +74,7 @@ func (m *InMemStorageMock) VerifyUID(ctx context.Context, fingerprint, email, to
|
||||
return fmt.Errorf("invalid verification token or link") // Mimic no matching entry
|
||||
}
|
||||
|
||||
var targetUID *service.PGPUid
|
||||
var targetUID *model.PGPUid
|
||||
var targetUIDString string // To be used as key in the map for update
|
||||
for uidStr, uid := range keyUids {
|
||||
if uid.Email == email {
|
||||
@ -99,7 +100,7 @@ func (m *InMemStorageMock) VerifyUID(ctx context.Context, fingerprint, email, to
|
||||
|
||||
// Simulate update
|
||||
targetUID.Verify = true
|
||||
targetUID.Token = "" // Simulate setting to null
|
||||
targetUID.Token = "" // Simulate setting to null
|
||||
targetUID.TokenExpires = time.Time{} // Simulate setting to null
|
||||
|
||||
// Ensure the updated UID is correctly stored back in the map
|
||||
@ -110,7 +111,7 @@ func (m *InMemStorageMock) VerifyUID(ctx context.Context, fingerprint, email, to
|
||||
|
||||
// GetKey implements PGPKeyStorage interface for the mock.
|
||||
// See PGPKeyStorage interface for documentation.
|
||||
func (m *InMemStorageMock) GetKey(ctx context.Context, fingerprint string) ([]*service.PGPkey, error) {
|
||||
func (m *InMemStorageMock) GetKey(ctx context.Context, fingerprint string) ([]*model.PGPKey, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
@ -120,11 +121,11 @@ func (m *InMemStorageMock) GetKey(ctx context.Context, fingerprint string) ([]*s
|
||||
}
|
||||
|
||||
// Create a deep copy of the key
|
||||
resultKey := &service.PGPkey{
|
||||
resultKey := &model.PGPKey{
|
||||
Fingerprint: pgpKey.Fingerprint,
|
||||
Packet: pgpKey.Packet,
|
||||
Revoked: pgpKey.Revoked,
|
||||
Uids: []*service.PGPUid{}, // Only add verified UIDs
|
||||
Uids: []*model.PGPUid{}, // Only add verified UIDs
|
||||
}
|
||||
|
||||
// Filter UIDs by 'verified = true'
|
||||
@ -132,8 +133,8 @@ func (m *InMemStorageMock) GetKey(ctx context.Context, fingerprint string) ([]*s
|
||||
for _, uid := range keyUids {
|
||||
if uid.Verify {
|
||||
// Deep copy the UID
|
||||
resultKey.Uids = append(resultKey.Uids, &service.PGPUid{
|
||||
UIDString: uid.UIDString,
|
||||
resultKey.Uids = append(resultKey.Uids, &model.PGPUid{
|
||||
UIDString: uid.UIDString,
|
||||
Email: uid.Email,
|
||||
Verify: uid.Verify,
|
||||
Token: uid.Token,
|
||||
@ -143,12 +144,12 @@ func (m *InMemStorageMock) GetKey(ctx context.Context, fingerprint string) ([]*s
|
||||
}
|
||||
}
|
||||
|
||||
return []*service.PGPkey{resultKey}, nil
|
||||
return []*model.PGPKey{resultKey}, nil
|
||||
}
|
||||
|
||||
// Index implements PGPKeyStorage interface for the mock.
|
||||
// See PGPKeyStorage interface for documentation.
|
||||
func (m *InMemStorageMock) Index(ctx context.Context, q string) ([]*service.PGPkey, error) {
|
||||
func (m *InMemStorageMock) Index(ctx context.Context, q string) ([]*model.PGPKey, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
@ -160,13 +161,13 @@ func (m *InMemStorageMock) Index(ctx context.Context, q string) ([]*service.PGPk
|
||||
// Simulate case-insensitive substring match for regex.
|
||||
// For a more accurate mock, you'd use the 'regexp' package here.
|
||||
if strings.Contains(strings.ToLower(uid.UIDString), lowerQ) ||
|
||||
strings.Contains(strings.ToLower(uid.Email), lowerQ) {
|
||||
strings.Contains(strings.ToLower(uid.Email), lowerQ) {
|
||||
matchedFingerprints[f] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var resultKeys []*service.PGPkey
|
||||
var resultKeys []*model.PGPKey
|
||||
for f := range matchedFingerprints {
|
||||
// Reuse GetKey logic from mock (which filters for verified UIDs)
|
||||
keys, err := m.GetKey(ctx, f)
|
||||
|
||||
@ -5,8 +5,8 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
repo "gadrid/internal/repo/storage"
|
||||
service "gadrid/internal/service/storage"
|
||||
"hkp-clavis/internal/model"
|
||||
repo "hkp-clavis/internal/repo/storage"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -19,13 +19,71 @@ import (
|
||||
//go:embed structure.sql
|
||||
var db_struct string
|
||||
|
||||
var (
|
||||
insertUid = `
|
||||
insert into pgp_uids(fingerprint, uid, email, verified, verification_token, token_expires_at)
|
||||
values($1, $2, $3, $4, $5, $6)
|
||||
on conflict (fingerprint, uid) do nothing;
|
||||
`
|
||||
selectUid = `
|
||||
select uid, verification_token, token_expires_at, verified
|
||||
from pgp_uids
|
||||
where fingerprint = $1 and email = $2;
|
||||
`
|
||||
updateUidVerify = `
|
||||
update pgp_uids
|
||||
set verified = true,
|
||||
verification_token = null,
|
||||
token_expires_at = null
|
||||
where fingerprint = $1 and email = $2 and verification_token = $3;
|
||||
`
|
||||
fetchUidVerifed = `
|
||||
select uid, email, verification_token, token_expires_at, verified
|
||||
from pgp_uids
|
||||
where fingerprint = $1 and verified = true;
|
||||
`
|
||||
insertKey = `
|
||||
insert into pgp_keys(fingerprint, packet, revoked, update_time)
|
||||
values($1, $2, $3, now())
|
||||
on conflict (fingerprint) do update set
|
||||
packet = excluded.packet,
|
||||
revoked = excluded.revoked,
|
||||
update_time = now();
|
||||
`
|
||||
selectKeyByFingerprint = `
|
||||
select fingerprint, packet, revoked, update_time
|
||||
from pgp_keys
|
||||
where fingerprint ~* $1;
|
||||
`
|
||||
indexKeyByString = `
|
||||
SELECT
|
||||
k.fingerprint, k.packet, k.revoked, k.update_time,
|
||||
u.uid, u.email, u.verified, u.verification_token, u.token_expires_at
|
||||
FROM pgp_keys k
|
||||
JOIN pgp_uids u ON k.fingerprint = u.fingerprint
|
||||
WHERE k.fingerprint IN (
|
||||
SELECT DISTINCT fingerprint
|
||||
FROM pgp_uids
|
||||
WHERE (uid ~* $1 OR email ~* $1 OR fingerprint ~* $1) AND verified = true
|
||||
)
|
||||
ORDER BY k.fingerprint;
|
||||
`
|
||||
cleanupStaleKeys = `
|
||||
delete from pgp_keys pk
|
||||
where not exists (
|
||||
select 1 from pgp_uids pu where pu.fingerprint = pk.fingerprint
|
||||
);
|
||||
`
|
||||
)
|
||||
|
||||
type PostgresqlStorageRepo struct {
|
||||
poll *pgxpool.Pool
|
||||
poll *pgxpool.Pool
|
||||
keyMngr *model.KeyManager
|
||||
}
|
||||
|
||||
// Get context, url
|
||||
// Return postgresql repository
|
||||
func New(ctx context.Context, pool *pgxpool.Pool) repo.StorageRepositotyInterface {
|
||||
func New(ctx context.Context, pool *pgxpool.Pool, keyManager *model.KeyManager) repo.StorageRepositotyInterface {
|
||||
_, err := pool.Exec(ctx, db_struct)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -33,10 +91,12 @@ func New(ctx context.Context, pool *pgxpool.Pool) repo.StorageRepositotyInterfac
|
||||
|
||||
return PostgresqlStorageRepo{
|
||||
poll: pool,
|
||||
// Allocation manager
|
||||
keyMngr: keyManager,
|
||||
}
|
||||
}
|
||||
|
||||
func (s PostgresqlStorageRepo) AddKey(ctx context.Context, keys []*service.PGPkey) error {
|
||||
func (s PostgresqlStorageRepo) AddKey(ctx context.Context, keys []*model.PGPKey) error {
|
||||
tx, err := s.poll.Begin(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error postgresql: AddKey: begin transaction: %w", err)
|
||||
@ -46,27 +106,15 @@ func (s PostgresqlStorageRepo) AddKey(ctx context.Context, keys []*service.PGPke
|
||||
for _, k := range keys {
|
||||
fingerprint := k.Fingerprint
|
||||
// Upsert pgp_keys
|
||||
_, err = tx.Exec(ctx, `
|
||||
insert into pgp_keys(fingerprint, packet, revoked, update_time)
|
||||
values($1, $2, $3, now())
|
||||
on conflict (fingerprint) do update set
|
||||
packet = excluded.packet,
|
||||
revoked = excluded.revoked,
|
||||
update_time = now();
|
||||
`, fingerprint, k.Packet, k.Revoked)
|
||||
_, err = tx.Exec(ctx, insertKey, fingerprint, k.Packet, k.Revoked)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error postgresql: AddKey: upsert key %s: %w", fingerprint, err)
|
||||
}
|
||||
|
||||
// Upsert pgp_uids
|
||||
batch := &pgx.Batch{}
|
||||
insertUIDQuery := `
|
||||
insert into pgp_uids(fingerprint, uid, email, verified, verification_token, token_expires_at)
|
||||
values($1, $2, $3, $4, $5, $6)
|
||||
on conflict (fingerprint, uid) do nothing;
|
||||
`
|
||||
for _, u := range k.Uids {
|
||||
batch.Queue(insertUIDQuery, fingerprint, u.UIDString, u.Email, u.Verify, u.Token, u.TokenExpires)
|
||||
batch.Queue(insertUid, fingerprint, u.UIDString, u.Email, u.Verify, u.Token, u.TokenExpires)
|
||||
}
|
||||
|
||||
br := tx.SendBatch(ctx, batch)
|
||||
@ -97,11 +145,7 @@ func (s PostgresqlStorageRepo) VerifyUID(ctx context.Context, fingerprint, email
|
||||
var tokenExpiresAt sql.NullTime
|
||||
var isVerified bool
|
||||
|
||||
row := tx.QueryRow(ctx, `
|
||||
select uid, verification_token, token_expires_at, verified
|
||||
from pgp_uids
|
||||
where fingerprint = $1 and email = $2;
|
||||
`, fingerprint, email)
|
||||
row := tx.QueryRow(ctx, selectUid, fingerprint, email)
|
||||
|
||||
if err = row.Scan(&storedUID, &storedToken, &tokenExpiresAt, &isVerified); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
@ -124,13 +168,7 @@ func (s PostgresqlStorageRepo) VerifyUID(ctx context.Context, fingerprint, email
|
||||
}
|
||||
|
||||
// UPDATE the UID status.
|
||||
result, err := tx.Exec(ctx, `
|
||||
update pgp_uids
|
||||
set verified = true,
|
||||
verification_token = null,
|
||||
token_expires_at = null
|
||||
where fingerprint = $1 and email = $2 and verification_token = $3;
|
||||
`, fingerprint, email, token)
|
||||
result, err := tx.Exec(ctx, updateUidVerify, fingerprint, email, token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error postgresql: VerifyUID: failed to update UID verification status: %w", err)
|
||||
}
|
||||
@ -146,124 +184,115 @@ func (s PostgresqlStorageRepo) VerifyUID(ctx context.Context, fingerprint, email
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (s PostgresqlStorageRepo) GetKey(ctx context.Context, fngprt string) ([]*service.PGPkey, error) {
|
||||
var packet string
|
||||
var fingerprint string
|
||||
var revoked bool
|
||||
var update_time time.Time
|
||||
func (s PostgresqlStorageRepo) GetKey(ctx context.Context, fngprt string) (res []*model.PGPKey, err error) {
|
||||
pgpKey := s.keyMngr.Get()
|
||||
|
||||
row := s.poll.QueryRow(ctx, `
|
||||
select fingerprint, packet, revoked, update_time
|
||||
from pgp_keys
|
||||
where fingerprint ~* $1;
|
||||
`, fngprt)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
s.keyMngr.Release(pgpKey)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := row.Scan(&fingerprint, &packet, &revoked, &update_time); err != nil {
|
||||
row := s.poll.QueryRow(ctx, selectKeyByFingerprint, fngprt)
|
||||
|
||||
if err = row.Scan(&pgpKey.Fingerprint, &pgpKey.Packet, &pgpKey.Revoked, &pgpKey.UpdateTime); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, repo.ErrKeyNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("error postgresql: GetKey: failed to scan key data for fingerprint %s: %w", fngprt, err)
|
||||
return nil, fmt.Errorf("error postgresql: GetKey: scan error: %w", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(fingerprint, fngprt) {
|
||||
if !strings.Contains(pgpKey.Fingerprint, fngprt) {
|
||||
return nil, repo.ErrKeyNotFound
|
||||
}
|
||||
|
||||
// Fetch all VERIFIED UIDs for this key.
|
||||
rows, err := s.poll.Query(ctx, `
|
||||
select uid, email, verification_token, token_expires_at, verified
|
||||
from pgp_uids
|
||||
where fingerprint = $1 and verified = true;
|
||||
`, fngprt)
|
||||
rows, err := s.poll.Query(ctx, fetchUidVerifed, pgpKey.Fingerprint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error postgresql: GetKey: failed to query UIDs for key %s: %w", fngprt, err)
|
||||
return nil, fmt.Errorf("error postgresql: GetKey: query uids error: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var uids []*service.PGPUid
|
||||
for rows.Next() {
|
||||
|
||||
uid := &service.PGPUid{}
|
||||
uid := s.keyMngr.GetUid()
|
||||
var token sql.NullString
|
||||
var tokenExpiresAt sql.NullTime
|
||||
if err := rows.Scan(&uid.UIDString, &uid.Email, &token, &tokenExpiresAt, &uid.Verify); err != nil {
|
||||
return nil, fmt.Errorf("error postgresql: GetKey: failed to scan UID row for key %s: %w", fngprt, err)
|
||||
|
||||
if err = rows.Scan(&uid.UIDString, &uid.Email, &token, &tokenExpiresAt, &uid.Verify); err != nil {
|
||||
s.keyMngr.ReleaseUid(uid)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uid.Token = token.String
|
||||
uid.TokenExpires = tokenExpiresAt.Time
|
||||
|
||||
uids = append(uids, uid)
|
||||
|
||||
pgpKey.Uids = append(pgpKey.Uids, uid)
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("error postgresql: GetKey: error after iterating UIDs for key %s: %w", fngprt, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pgpKey := &service.PGPkey{
|
||||
Fingerprint: fngprt,
|
||||
UpdateTime: update_time,
|
||||
Packet: packet,
|
||||
Revoked: revoked,
|
||||
Uids: uids, // Only verified UIDs included
|
||||
}
|
||||
return []*service.PGPkey{pgpKey}, nil
|
||||
return []*model.PGPKey{pgpKey}, nil
|
||||
}
|
||||
|
||||
func (s PostgresqlStorageRepo) Index(ctx context.Context, q string) ([]*service.PGPkey, error) {
|
||||
// Search for matching fingerprints based on the query 'q'.
|
||||
fingerprintRows, err := s.poll.Query(ctx, `
|
||||
select distinct fingerprint
|
||||
from pgp_uids
|
||||
where (uid ~* $1 or email ~* $1 or fingerprint ~* $1) and verified = true;
|
||||
`, q)
|
||||
func (s PostgresqlStorageRepo) Index(ctx context.Context, q string) (resultKeys []*model.PGPKey, err error) {
|
||||
rows, err := s.poll.Query(ctx, indexKeyByString, q)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error postgresql: Index: failed to query matching fingerprints for '%s': %w", q, err)
|
||||
return nil, fmt.Errorf("postgresql: Index query failed: %w", err)
|
||||
}
|
||||
defer fingerprintRows.Close()
|
||||
defer rows.Close()
|
||||
|
||||
var matchingFingerprints []string
|
||||
for fingerprintRows.Next() {
|
||||
var f string
|
||||
if err := fingerprintRows.Scan(&f); err != nil {
|
||||
return nil, fmt.Errorf("error postgresql: Index: failed to scan fingerprint: %w", err)
|
||||
}
|
||||
matchingFingerprints = append(matchingFingerprints, f)
|
||||
}
|
||||
if err = fingerprintRows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("error postgresql: Index: error after iterating fingerprints: %w", err)
|
||||
}
|
||||
|
||||
if len(matchingFingerprints) == 0 {
|
||||
return []*service.PGPkey{}, nil
|
||||
}
|
||||
|
||||
// For each matching fingerprint, use GetKey to retrieve the full PGPKey object.
|
||||
var resultKeys []*service.PGPkey
|
||||
for _, fingerprint := range matchingFingerprints {
|
||||
keys, err := s.GetKey(ctx, fingerprint)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if errors.Is(err, repo.ErrKeyNotFound) {
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("error postgresql: Index: failed to retrieve key %s via GetKey: %w", fingerprint, err)
|
||||
s.keyMngr.Release(resultKeys...)
|
||||
}
|
||||
if len(keys) > 0 {
|
||||
resultKeys = append(resultKeys, keys[0])
|
||||
}()
|
||||
|
||||
var currentKey *model.PGPKey
|
||||
|
||||
for rows.Next() {
|
||||
var fng, packet string
|
||||
var revoked bool
|
||||
var updateTime time.Time
|
||||
|
||||
uidObj := s.keyMngr.GetUid()
|
||||
var token sql.NullString
|
||||
var tokenExpiresAt sql.NullTime
|
||||
|
||||
err = rows.Scan(
|
||||
&fng, &packet, &revoked, &updateTime,
|
||||
&uidObj.UIDString, &uidObj.Email, &uidObj.Verify, &token, &tokenExpiresAt,
|
||||
)
|
||||
if err != nil {
|
||||
s.keyMngr.ReleaseUid(uidObj)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uidObj.Token = token.String
|
||||
uidObj.TokenExpires = tokenExpiresAt.Time
|
||||
uidObj.Fingerprint = fng
|
||||
|
||||
if currentKey == nil || currentKey.Fingerprint != fng {
|
||||
currentKey = s.keyMngr.Get()
|
||||
currentKey.Fingerprint = fng
|
||||
currentKey.Packet = packet
|
||||
currentKey.Revoked = revoked
|
||||
currentKey.UpdateTime = updateTime
|
||||
|
||||
resultKeys = append(resultKeys, currentKey)
|
||||
}
|
||||
|
||||
currentKey.Uids = append(currentKey.Uids, uidObj)
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("postgresql: Index rows iteration error: %w", err)
|
||||
}
|
||||
|
||||
return resultKeys, nil
|
||||
}
|
||||
|
||||
func (s PostgresqlStorageRepo) CleanupStaleKeys(ctx context.Context) (int, error) {
|
||||
// The SQL query will delete keys from pgp_keys that have no corresponding entries in pgp_uids.
|
||||
result, err := s.poll.Exec(ctx, `
|
||||
delete from pgp_keys pk
|
||||
where not exists (
|
||||
select 1 from pgp_uids pu where pu.fingerprint = pk.fingerprint
|
||||
);
|
||||
`)
|
||||
result, err := s.poll.Exec(ctx, cleanupStaleKeys)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error postgresql: CleanupStaleKeys: failed to delete stale keys: %w", err)
|
||||
}
|
||||
|
||||
@ -12,9 +12,9 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
repository "gadrid/internal/repo/storage"
|
||||
psqlrepo "gadrid/internal/repo/storage/postgresql"
|
||||
service "gadrid/internal/service/storage"
|
||||
"hkp-clavis/internal/model"
|
||||
repository "hkp-clavis/internal/repo/storage"
|
||||
psqlrepo "hkp-clavis/internal/repo/storage/postgresql"
|
||||
)
|
||||
|
||||
//go:embed structure.sql
|
||||
@ -25,21 +25,23 @@ func TestPostgresqlStorageRepo_AddKey(t *testing.T) {
|
||||
defer pool.Close()
|
||||
defer cleanupTestDB(t, pool)
|
||||
|
||||
repo := psqlrepo.New(context.Background(), pool)
|
||||
mgr := model.NewKeyManager()
|
||||
|
||||
repo := psqlrepo.New(context.Background(), pool, mgr)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// --- Test Case 1: Add a new key ---
|
||||
t.Run("Add new key successfully", func(t *testing.T) {
|
||||
key := &service.PGPkey{
|
||||
key := &model.PGPKey{
|
||||
Fingerprint: "FINGERPRINT_NEW_KEY",
|
||||
Packet: "-----BEGIN PGP PUBLIC KEY BLOCK-----NEW_KEY_PACKET-----END PGP PUBLIC KEY BLOCK-----",
|
||||
Revoked: false,
|
||||
Uids: []*service.PGPUid{
|
||||
Uids: []*model.PGPUid{
|
||||
{UIDString: "New User <new@example.com>", Email: "new@example.com", Verify: false, Token: "token1", TokenExpires: time.Now().Add(time.Hour)},
|
||||
},
|
||||
}
|
||||
err := repo.AddKey(ctx, []*service.PGPkey{key})
|
||||
err := repo.AddKey(ctx, []*model.PGPKey{key})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify key was added
|
||||
@ -60,27 +62,27 @@ func TestPostgresqlStorageRepo_AddKey(t *testing.T) {
|
||||
|
||||
// --- Test Case 2: Update an existing key (packet, revoked status) ---
|
||||
t.Run("Update existing key packet and revoked status", func(t *testing.T) {
|
||||
initialKey := &service.PGPkey{
|
||||
initialKey := &model.PGPKey{
|
||||
Fingerprint: "FINGERPRINT_EXISTING",
|
||||
Packet: "-----BEGIN PGP PUBLIC KEY BLOCK-----INITIAL_PACKET-----END PGP PUBLIC KEY BLOCK-----",
|
||||
Revoked: false,
|
||||
Uids: []*service.PGPUid{
|
||||
Uids: []*model.PGPUid{
|
||||
{UIDString: "Existing User <existing@example.com>", Email: "existing@example.com", Verify: false, Token: "initial_token", TokenExpires: time.Now().Add(time.Hour)},
|
||||
},
|
||||
}
|
||||
err := repo.AddKey(ctx, []*service.PGPkey{initialKey})
|
||||
err := repo.AddKey(ctx, []*model.PGPKey{initialKey})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Simulate an update: new packet, revoked true
|
||||
updatedKey := &service.PGPkey{
|
||||
updatedKey := &model.PGPKey{
|
||||
Fingerprint: "FINGERPRINT_EXISTING",
|
||||
Packet: "-----BEGIN PGP PUBLIC KEY BLOCK-----UPDATED_PACKET-----END PGP PUBLIC KEY BLOCK-----",
|
||||
Revoked: true,
|
||||
Uids: []*service.PGPUid{
|
||||
Uids: []*model.PGPUid{
|
||||
{UIDString: "Existing User <existing@example.com>", Email: "existing@example.com", Verify: false, Token: "updated_token", TokenExpires: time.Now().Add(time.Hour * 2)},
|
||||
},
|
||||
}
|
||||
err = repo.AddKey(ctx, []*service.PGPkey{updatedKey})
|
||||
err = repo.AddKey(ctx, []*model.PGPKey{updatedKey})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify key was updated
|
||||
@ -100,28 +102,28 @@ func TestPostgresqlStorageRepo_AddKey(t *testing.T) {
|
||||
|
||||
// --- Test Case 3: Add new UID to existing key ---
|
||||
t.Run("Add new UID to existing key", func(t *testing.T) {
|
||||
key := &service.PGPkey{
|
||||
key := &model.PGPKey{
|
||||
Fingerprint: "FINGERPRINT_ADD_UID",
|
||||
Packet: "-----BEGIN PGP PUBLIC KEY BLOCK-----ADD_UID_KEY-----END PGP PUBLIC KEY BLOCK-----",
|
||||
Revoked: false,
|
||||
Uids: []*service.PGPUid{
|
||||
Uids: []*model.PGPUid{
|
||||
{UIDString: "First UID <first@example.com>", Email: "first@example.com", Verify: false, Token: "token_first", TokenExpires: time.Now().Add(time.Hour)},
|
||||
},
|
||||
}
|
||||
err := repo.AddKey(ctx, []*service.PGPkey{key})
|
||||
err := repo.AddKey(ctx, []*model.PGPKey{key})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add a second UID to the same key
|
||||
updatedKey := &service.PGPkey{
|
||||
updatedKey := &model.PGPKey{
|
||||
Fingerprint: "FINGERPRINT_ADD_UID",
|
||||
Packet: "-----BEGIN PGP PUBLIC KEY BLOCK-----ADD_UID_KEY_UPDATED-----END PGP PUBLIC KEY BLOCK-----",
|
||||
Revoked: false,
|
||||
Uids: []*service.PGPUid{
|
||||
Uids: []*model.PGPUid{
|
||||
{UIDString: "First UID <first@example.com>", Email: "first@example.com", Verify: false, Token: "token_first", TokenExpires: time.Now().Add(time.Hour)},
|
||||
{UIDString: "Second UID <second@example.com>", Email: "second@example.com", Verify: false, Token: "token_second", TokenExpires: time.Now().Add(time.Hour)},
|
||||
},
|
||||
}
|
||||
err = repo.AddKey(ctx, []*service.PGPkey{updatedKey})
|
||||
err = repo.AddKey(ctx, []*model.PGPKey{updatedKey})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify both UIDs exist
|
||||
@ -137,7 +139,9 @@ func TestPostgresqlStorageRepo_VerifyUID(t *testing.T) {
|
||||
defer pool.Close()
|
||||
defer cleanupTestDB(t, pool)
|
||||
|
||||
repo := psqlrepo.New(context.Background(), pool)
|
||||
mgr := model.NewKeyManager()
|
||||
|
||||
repo := psqlrepo.New(context.Background(), pool, mgr)
|
||||
ctx := context.Background()
|
||||
|
||||
// Add a key with an unverified UID
|
||||
@ -146,12 +150,12 @@ func TestPostgresqlStorageRepo_VerifyUID(t *testing.T) {
|
||||
uidToken := "verifytoken123"
|
||||
uidExpires := time.Now().Add(time.Hour)
|
||||
|
||||
err := repo.AddKey(ctx, []*service.PGPkey{
|
||||
err := repo.AddKey(ctx, []*model.PGPKey{
|
||||
{
|
||||
Fingerprint: keyFingerprint,
|
||||
Packet: "...",
|
||||
Revoked: false,
|
||||
Uids: []*service.PGPUid{
|
||||
Uids: []*model.PGPUid{
|
||||
{UIDString: "Verify Me <" + uidEmail + ">", Email: uidEmail, Verify: false, Token: uidToken, TokenExpires: uidExpires},
|
||||
},
|
||||
},
|
||||
@ -198,12 +202,12 @@ func TestPostgresqlStorageRepo_VerifyUID(t *testing.T) {
|
||||
expiredToken := "expiredtoken"
|
||||
expiredTime := time.Now().Add(-time.Hour)
|
||||
|
||||
err := repo.AddKey(ctx, []*service.PGPkey{
|
||||
err := repo.AddKey(ctx, []*model.PGPKey{
|
||||
{
|
||||
Fingerprint: expiredFingerprint,
|
||||
Packet: "...",
|
||||
Revoked: false,
|
||||
Uids: []*service.PGPUid{
|
||||
Uids: []*model.PGPUid{
|
||||
{UIDString: "Expired <" + expiredEmail + ">", Email: expiredEmail, Verify: false, Token: expiredToken, TokenExpires: expiredTime},
|
||||
},
|
||||
},
|
||||
@ -227,17 +231,18 @@ func TestPostgresqlStorageRepo_GetKey(t *testing.T) {
|
||||
defer pool.Close()
|
||||
defer cleanupTestDB(t, pool)
|
||||
|
||||
repo := psqlrepo.New(context.Background(), pool)
|
||||
mgr := model.NewKeyManager()
|
||||
repo := psqlrepo.New(context.Background(), pool, mgr)
|
||||
ctx := context.Background()
|
||||
|
||||
// Add a key with mixed UIDs (verified and unverified)
|
||||
testFingerprint := "FINGERPRINT_GET_KEY"
|
||||
err := repo.AddKey(ctx, []*service.PGPkey{
|
||||
err := repo.AddKey(ctx, []*model.PGPKey{
|
||||
{
|
||||
Fingerprint: testFingerprint,
|
||||
Packet: "PACKET_DATA",
|
||||
Revoked: false,
|
||||
Uids: []*service.PGPUid{
|
||||
Uids: []*model.PGPUid{
|
||||
{UIDString: "Verified User <verified@example.com>", Email: "verified@example.com", Verify: false, Token: "tok_v", TokenExpires: time.Now().Add(time.Hour)},
|
||||
{UIDString: "Unverified User <unverified@example.com>", Email: "unverified@example.com", Verify: false, Token: "tok_uv", TokenExpires: time.Now().Add(time.Hour)},
|
||||
{UIDString: "Another Verified <another@example.com>", Email: "another@example.com", Verify: false, Token: "tok_av", TokenExpires: time.Now().Add(time.Hour)},
|
||||
@ -285,27 +290,29 @@ func TestPostgresqlStorageRepo_Index(t *testing.T) {
|
||||
defer pool.Close()
|
||||
defer cleanupTestDB(t, pool)
|
||||
|
||||
repo := psqlrepo.New(context.Background(), pool)
|
||||
mgr := model.NewKeyManager()
|
||||
|
||||
repo := psqlrepo.New(context.Background(), pool, mgr)
|
||||
ctx := context.Background()
|
||||
|
||||
// Add multiple keys with various UIDs
|
||||
keysToLoad := []*service.PGPkey{
|
||||
keysToLoad := []*model.PGPKey{
|
||||
{
|
||||
Fingerprint: "FINGERPRINT_ALPHA", Packet: "alpha_packet", Revoked: false,
|
||||
Uids: []*service.PGPUid{
|
||||
Uids: []*model.PGPUid{
|
||||
{UIDString: "Alpha User <alpha@example.com>", Email: "alpha@example.com", Verify: false, Token: "t1", TokenExpires: time.Now().Add(time.Hour)},
|
||||
{UIDString: "Test Alpha <testalpha@mail.com>", Email: "testalpha@mail.com", Verify: false, Token: "t2", TokenExpires: time.Now().Add(time.Hour)},
|
||||
},
|
||||
},
|
||||
{
|
||||
Fingerprint: "FINGERPRINT_BETA", Packet: "beta_packet", Revoked: false,
|
||||
Uids: []*service.PGPUid{
|
||||
Uids: []*model.PGPUid{
|
||||
{UIDString: "Beta User <beta@example.com>", Email: "beta@example.com", Verify: false, Token: "t3", TokenExpires: time.Now().Add(time.Hour)},
|
||||
},
|
||||
},
|
||||
{
|
||||
Fingerprint: "FINGERPRINT_GAMMA", Packet: "gamma_packet", Revoked: false,
|
||||
Uids: []*service.PGPUid{
|
||||
Uids: []*model.PGPUid{
|
||||
{UIDString: "Gamma User <gamma@example.com>", Email: "gamma@example.com", Verify: false, Token: "t4", TokenExpires: time.Now().Add(time.Hour)},
|
||||
{UIDString: "Test Gamma <testgamma@mail.com>", Email: "testgamma@mail.com", Verify: false, Token: "t5", TokenExpires: time.Now().Add(time.Hour)},
|
||||
},
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
package mail
|
||||
|
||||
type MailServiceInterface interface {
|
||||
SendMail(to []string, msg []byte)
|
||||
}
|
||||
@ -1,111 +0,0 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
||||
)
|
||||
|
||||
func extractEmail(fullNameWithEmail string) string {
|
||||
re := regexp.MustCompile(`<([^>]+)>`)
|
||||
matches := re.FindStringSubmatch(fullNameWithEmail)
|
||||
if len(matches) > 1 {
|
||||
return matches[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ConverterPgpToService converts a raw openpgp.EntityList into a slice of service.PGPkey objects.
|
||||
// It parses key data, generates verification tokens for UIDs, and extracts email addresses.
|
||||
//
|
||||
// Parameters:
|
||||
//
|
||||
// entities: The openpgp.EntityList obtained from parsing an armored PGP block.
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// []*PGPkey: A slice of converted PGPkey objects. Returns an empty (non-nil) slice if no valid entities are found.
|
||||
// error: An error if any entity fails to be processed or contains invalid data.
|
||||
func ConverterPgpToService(entities openpgp.EntityList) ([]*PGPkey, error) {
|
||||
var pgpKeys []*PGPkey
|
||||
|
||||
if entities == nil {
|
||||
return pgpKeys, nil
|
||||
}
|
||||
|
||||
for i, entity := range entities {
|
||||
// Test on correct data
|
||||
if entity == nil {
|
||||
log.Printf("Warning: ConverterPgpToService: encountered nil entity at index %d, skipping.", i)
|
||||
continue
|
||||
}
|
||||
if entity.PrimaryKey == nil {
|
||||
log.Printf("Warning: ConverterPgpToService: entity at index %d has no primary key, skipping.", i)
|
||||
continue
|
||||
}
|
||||
|
||||
publicKeyBuf := bytes.NewBuffer(nil)
|
||||
publicKeyWriter, err := armor.Encode(publicKeyBuf, openpgp.PublicKeyType, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converter error: entity %d: failed to create armor encoder: %w", i, err)
|
||||
}
|
||||
|
||||
if err = entity.Serialize(publicKeyWriter); err != nil {
|
||||
return nil, fmt.Errorf("converter error: entity %d: failed to serialize public key: %w", i, err)
|
||||
}
|
||||
publicKeyWriter.Close()
|
||||
|
||||
// Write it
|
||||
|
||||
pgpKey := PGPkey{
|
||||
Revoked: entity.Revoked(time.Now()),
|
||||
UpdateTime: entity.PrimaryKey.CreationTime.UTC(), // <--- FIX IS HERE! Use PrimaryKey's CreationTime
|
||||
Fingerprint: strings.ToUpper(hex.EncodeToString(entity.PrimaryKey.Fingerprint)),
|
||||
Packet: publicKeyBuf.String(),
|
||||
Uids: []*PGPUid{},
|
||||
}
|
||||
|
||||
// Process each UID for the current entity
|
||||
for _, uid := range entity.Identities {
|
||||
if uid == nil || uid.UserId == nil {
|
||||
log.Printf("Warning: ConverterPgpToService: encountered nil Identity or UserId for key %s, skipping.", pgpKey.Fingerprint)
|
||||
continue
|
||||
}
|
||||
if uid.Name == "" {
|
||||
log.Printf("Warning: ConverterPgpToService: encountered empty User ID string for key %s, skipping.", pgpKey.Fingerprint)
|
||||
continue
|
||||
}
|
||||
|
||||
randBytes := make([]byte, 16)
|
||||
if _, err := rand.Read(randBytes); err != nil {
|
||||
return nil, fmt.Errorf("converter error: key %s: failed to generate random bytes for token: %w", pgpKey.Fingerprint, err)
|
||||
}
|
||||
token := hex.EncodeToString(randBytes)
|
||||
|
||||
extractedEmail := extractEmail(uid.Name)
|
||||
if extractedEmail == "" {
|
||||
log.Printf("Warning: ConverterPgpToService: extracted empty email from UID '%s' for key %s, skipping UID.", uid.Name, pgpKey.Fingerprint)
|
||||
continue
|
||||
}
|
||||
|
||||
pgpKey.Uids = append(pgpKey.Uids, &PGPUid{
|
||||
Verify: false,
|
||||
UIDString: uid.Name,
|
||||
Email: extractedEmail,
|
||||
Token: token,
|
||||
TokenExpires: time.Now().Add(time.Hour * 5),
|
||||
Fingerprint: pgpKey.Fingerprint,
|
||||
})
|
||||
}
|
||||
pgpKeys = append(pgpKeys, &pgpKey)
|
||||
}
|
||||
return pgpKeys, nil
|
||||
}
|
||||
@ -1,30 +1,15 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"time"
|
||||
"context"
|
||||
"hkp-clavis/internal/model"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
hkp "github.com/emersion/go-openpgp-hkp"
|
||||
)
|
||||
|
||||
type StorageInterface interface {
|
||||
AddKey(openpgp.EntityList) error
|
||||
GetKeyByUID(uid string) openpgp.EntityList
|
||||
GetKeyByFingerprint(fngr string) openpgp.EntityList
|
||||
}
|
||||
|
||||
type PGPkey struct {
|
||||
Revoked bool
|
||||
UpdateTime time.Time
|
||||
Fingerprint string
|
||||
Packet string
|
||||
Uids []*PGPUid
|
||||
}
|
||||
|
||||
type PGPUid struct {
|
||||
Verify bool
|
||||
Token string
|
||||
TokenExpires time.Time
|
||||
Email string
|
||||
UIDString string // "Full Name (comment) <email@example.com>"
|
||||
Fingerprint string
|
||||
Add(ctx context.Context, el openpgp.EntityList) ([]*model.PGPKey, error)
|
||||
Get(ctx context.Context, r *hkp.LookupRequest) (openpgp.EntityList, error)
|
||||
Index(ctx context.Context, r *hkp.LookupRequest) ([]hkp.IndexKey, error)
|
||||
}
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
package storage
|
||||
package serv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
storeRepo "gadrid/internal/repo/storage"
|
||||
service "gadrid/internal/service/storage"
|
||||
"hkp-clavis/internal/model"
|
||||
storeRepo "hkp-clavis/internal/repo/storage"
|
||||
service "hkp-clavis/internal/service/storage"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
hkp "github.com/emersion/go-openpgp-hkp"
|
||||
@ -18,79 +17,82 @@ type StorageService struct {
|
||||
hkp.Adder
|
||||
hkp.Lookuper
|
||||
// mailService interface{}
|
||||
storage storeRepo.StorageRepositotyInterface
|
||||
storage storeRepo.StorageRepositotyInterface
|
||||
config StorageServiceConfig
|
||||
keyManager *model.KeyManager
|
||||
}
|
||||
|
||||
func New(storageRepository storeRepo.StorageRepositotyInterface) StorageService {
|
||||
type StorageServiceConfig struct {
|
||||
defaultVerify bool
|
||||
}
|
||||
|
||||
func New(storageRepository storeRepo.StorageRepositotyInterface, keyManager *model.KeyManager, defaultVerifyValue bool) service.StorageInterface {
|
||||
return StorageService{
|
||||
storage: storageRepository,
|
||||
storage: storageRepository,
|
||||
keyManager: keyManager,
|
||||
config: StorageServiceConfig{
|
||||
defaultVerify: defaultVerifyValue,
|
||||
},
|
||||
|
||||
// Allocation manager
|
||||
}
|
||||
}
|
||||
func (s StorageService) Add(el openpgp.EntityList) error {
|
||||
fmt.Println(el)
|
||||
pgpKeys, err := service.ConverterPgpToService(el) // Assuming ConverterPgpToService might return an error now.
|
||||
func (s StorageService) Add(ctx context.Context, el openpgp.EntityList) ([]*model.PGPKey, error) {
|
||||
pgpKeys, release, err := s.keyManager.ConverterPgpToService(el, s.config.defaultVerify) // Assuming ConverterPgpToService might return an error now.
|
||||
defer release()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("service error: Add: failed to convert PGP entities: %w", err)
|
||||
return []*model.PGPKey{}, fmt.Errorf("service error: Add: failed to convert PGP entities: %w", err)
|
||||
}
|
||||
|
||||
if len(pgpKeys) == 0 {
|
||||
return nil // No keys to add, nothing to do.
|
||||
return []*model.PGPKey{}, nil // No keys to add, nothing to do.
|
||||
}
|
||||
|
||||
// 2. Pass the context from this service method down to the storage layer.
|
||||
// 3. ALWAYS check the error returned by the storage layer.
|
||||
err = s.storage.AddKey(context.Background(), pgpKeys) // Pass context here
|
||||
err = s.storage.AddKey(ctx, pgpKeys) // Pass context here
|
||||
if err != nil {
|
||||
return fmt.Errorf("service error: Add: failed to add keys to storage: %w", err)
|
||||
return []*model.PGPKey{}, fmt.Errorf("service error: Add: failed to add keys to storage: %w", err)
|
||||
}
|
||||
|
||||
return nil // Successfully processed
|
||||
return pgpKeys, nil
|
||||
}
|
||||
|
||||
// Get key by fingerprint
|
||||
// In Specification "op=get" fingerprint and uid
|
||||
// FUTUE TE IPSUM specification! Only fingerprint search!
|
||||
|
||||
func (s StorageService) Get(req *hkp.LookupRequest) (openpgp.EntityList, error) {
|
||||
|
||||
req.Search = strings.Trim(req.Search, "0x")
|
||||
|
||||
if len(req.Search) <= 15 {
|
||||
return nil, fmt.Errorf("service error: Get: Low entropy string for search")
|
||||
}
|
||||
|
||||
dbKeys, err := s.storage.GetKey(context.Background(), req.Search)
|
||||
func (s StorageService) Get(ctx context.Context, req *hkp.LookupRequest) (openpgp.EntityList, error) {
|
||||
dbKeys, err := s.storage.Index(ctx, req.Search)
|
||||
if err != nil {
|
||||
if errors.Is(err, storeRepo.ErrKeyNotFound) {
|
||||
return nil, fmt.Errorf("service error: Get: key %s not found: %w", req.Search, err)
|
||||
}
|
||||
return nil, fmt.Errorf("service error: Get: db request failed for key %s: %w", req.Search, err)
|
||||
return nil, fmt.Errorf("service error: Get: search failed: %w", err)
|
||||
}
|
||||
|
||||
if len(dbKeys) == 0 {
|
||||
return nil, fmt.Errorf("service error: Get: key %s not found (repository returned empty list): %w", req.Search, storeRepo.ErrKeyNotFound)
|
||||
return nil, storeRepo.ErrKeyNotFound
|
||||
}
|
||||
|
||||
dbKey := dbKeys[0]
|
||||
defer s.keyManager.Release(dbKeys...)
|
||||
|
||||
filteredEntity, err := s.sanitizeAndFilterKey(dbKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("service error: Get: failed to sanitize and filter key %s: %w", dbKey.Fingerprint, err)
|
||||
var finalEntityList openpgp.EntityList
|
||||
|
||||
for _, dbKey := range dbKeys {
|
||||
filteredEntities, err := model.SanitizeAndFilterKey(dbKey)
|
||||
if err != nil {
|
||||
log.Printf("Warning: Get: failed to sanitize key %s: %v", dbKey.Fingerprint, err)
|
||||
continue
|
||||
}
|
||||
finalEntityList = append(finalEntityList, filteredEntities...)
|
||||
}
|
||||
|
||||
return filteredEntity, nil
|
||||
if len(finalEntityList) == 0 {
|
||||
return nil, storeRepo.ErrKeyNotFound
|
||||
}
|
||||
|
||||
return finalEntityList, nil
|
||||
}
|
||||
|
||||
func (s StorageService) Index(req *hkp.LookupRequest) ([]hkp.IndexKey, error) {
|
||||
|
||||
if len(req.Search) < 5 {
|
||||
return nil, fmt.Errorf("service error: Index: short string for search")
|
||||
}
|
||||
|
||||
dbKeys, err := s.storage.Index(context.Background(), req.Search) // Pass ctx here
|
||||
func (s StorageService) Index(ctx context.Context, req *hkp.LookupRequest) ([]hkp.IndexKey, error) {
|
||||
dbKeys, err := s.storage.Index(ctx, req.Search)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("service error: Index: db index request for '%s': %w", req.Search, err) // Include original error
|
||||
return nil, fmt.Errorf("service error: Index: db index request for '%s': %w", req.Search, err)
|
||||
}
|
||||
defer s.keyManager.Release(dbKeys...)
|
||||
|
||||
if len(dbKeys) == 0 {
|
||||
return nil, nil
|
||||
@ -98,7 +100,7 @@ func (s StorageService) Index(req *hkp.LookupRequest) ([]hkp.IndexKey, error) {
|
||||
|
||||
var respIndex []hkp.IndexKey
|
||||
for _, dbKey := range dbKeys {
|
||||
filteredEntity, err := s.sanitizeAndFilterKey(dbKey)
|
||||
filteredEntity, err := model.SanitizeAndFilterKey(dbKey)
|
||||
if err != nil {
|
||||
log.Printf("Warning: service error: Index: failed to sanitize and filter key %s: %v, skipping.", dbKey.Fingerprint, err)
|
||||
continue // Skip this problematic key
|
||||
@ -120,34 +122,3 @@ func (s StorageService) Index(req *hkp.LookupRequest) ([]hkp.IndexKey, error) {
|
||||
|
||||
return respIndex, nil
|
||||
}
|
||||
|
||||
func (s StorageService) sanitizeAndFilterKey(dbKey *service.PGPkey) (openpgp.EntityList, error) {
|
||||
|
||||
parsedEntities, err := openpgp.ReadArmoredKeyRing(strings.NewReader(dbKey.Packet))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse key packet for %s: %w", dbKey.Fingerprint, err)
|
||||
}
|
||||
|
||||
if len(parsedEntities) == 0 {
|
||||
return nil, fmt.Errorf("parsed key packet for %s yielded no entities", dbKey.Fingerprint)
|
||||
}
|
||||
var respEntities openpgp.EntityList
|
||||
for _, e := range parsedEntities {
|
||||
if e == nil {
|
||||
return nil, fmt.Errorf("no entity with matching fingerprint %s found in parsed packet from DB", dbKey.Fingerprint)
|
||||
}
|
||||
verifiedUIDStringsFromDB := make(map[string]struct{})
|
||||
for _, dbUID := range dbKey.Uids {
|
||||
verifiedUIDStringsFromDB[dbUID.UIDString] = struct{}{}
|
||||
}
|
||||
filteredIdentities := make(map[string]*openpgp.Identity)
|
||||
for identityKey, identity := range e.Identities {
|
||||
if _, isVerified := verifiedUIDStringsFromDB[identity.Name]; isVerified {
|
||||
filteredIdentities[identityKey] = identity // Keep this identity
|
||||
}
|
||||
}
|
||||
e.Identities = filteredIdentities
|
||||
respEntities = append(respEntities, e)
|
||||
}
|
||||
return respEntities, nil
|
||||
}
|
||||
|
||||
15
internal/service/verify/serv/service.go
Normal file
15
internal/service/verify/serv/service.go
Normal file
@ -0,0 +1,15 @@
|
||||
package serv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hkp-clavis/internal/model"
|
||||
service "hkp-clavis/internal/service/verify"
|
||||
)
|
||||
|
||||
type VerifyMailService struct {
|
||||
service.VerifyServiceInterface
|
||||
}
|
||||
|
||||
func (s VerifyMailService) SendVerifyMessage(ctx context.Context, el []*model.PGPKey) error {
|
||||
return nil
|
||||
}
|
||||
10
internal/service/verify/service.go
Normal file
10
internal/service/verify/service.go
Normal file
@ -0,0 +1,10 @@
|
||||
package mail
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hkp-clavis/internal/model"
|
||||
)
|
||||
|
||||
type VerifyServiceInterface interface {
|
||||
SendVerifyMessage(ctx context.Context, el []*model.PGPKey) error
|
||||
}
|
||||
0
pkg/mail/NOT_WORK_DO_NOT_TOUCH
Normal file
0
pkg/mail/NOT_WORK_DO_NOT_TOUCH
Normal file
195
pkg/mail/smtp/pool/smtp.go
Normal file
195
pkg/mail/smtp/pool/smtp.go
Normal file
@ -0,0 +1,195 @@
|
||||
package pool
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
From string
|
||||
To []string
|
||||
Message bytes.Buffer
|
||||
inProcess bool
|
||||
}
|
||||
|
||||
type MessageTransaction struct {
|
||||
done bool
|
||||
key string
|
||||
msg *Message
|
||||
pool *MessagePool
|
||||
}
|
||||
|
||||
func (s MessageTransaction) Done() {
|
||||
s.done = true
|
||||
}
|
||||
|
||||
func (s MessageTransaction) Close() {
|
||||
if s.done {
|
||||
s.pool.Del(s.key)
|
||||
return
|
||||
}
|
||||
s.msg.inProcess = false
|
||||
}
|
||||
|
||||
type MessagePool struct {
|
||||
m *sync.Mutex
|
||||
messagePool map[string]*Message
|
||||
}
|
||||
|
||||
func (p MessagePool) Put(m *Message) {
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
p.m.Lock()
|
||||
|
||||
h := sha256.New()
|
||||
h.Write(m.Message.Bytes())
|
||||
k := string(h.Sum(nil))
|
||||
|
||||
p.messagePool[k] = m
|
||||
p.m.Unlock()
|
||||
}
|
||||
|
||||
func (p MessagePool) Del(key string) {
|
||||
p.m.Lock()
|
||||
delete(p.messagePool, key)
|
||||
p.m.Unlock()
|
||||
}
|
||||
|
||||
func (p MessagePool) Begin() chan MessageTransaction {
|
||||
c := make(chan MessageTransaction)
|
||||
go func() {
|
||||
for k, m := range p.messagePool {
|
||||
if !m.inProcess {
|
||||
p.m.Lock()
|
||||
m.inProcess = true
|
||||
c <- MessageTransaction{
|
||||
done: false,
|
||||
key: k,
|
||||
msg: m,
|
||||
pool: &p,
|
||||
}
|
||||
p.m.Unlock()
|
||||
}
|
||||
close(c)
|
||||
}
|
||||
}()
|
||||
return c
|
||||
}
|
||||
|
||||
type PoolConfig struct {
|
||||
CountOfConnection int32
|
||||
Identity, Username, Password string
|
||||
Host string
|
||||
}
|
||||
|
||||
type SMTPPool struct {
|
||||
config PoolConfig
|
||||
countOfConnection int32
|
||||
connections chan *smtp.Client
|
||||
MessagePool *MessagePool
|
||||
}
|
||||
|
||||
// Get SMTPPoolConfig return SMTPPool
|
||||
func NewSMTPPool(conf PoolConfig) (*SMTPPool, error) {
|
||||
auth := smtp.PlainAuth(conf.Identity, conf.Username, conf.Password, conf.Host)
|
||||
|
||||
pool := &SMTPPool{
|
||||
config: conf,
|
||||
connections: make(chan *smtp.Client, conf.CountOfConnection),
|
||||
countOfConnection: 0,
|
||||
}
|
||||
|
||||
go pool.healthChecker(auth)
|
||||
|
||||
// Init Connections
|
||||
for range conf.CountOfConnection {
|
||||
c, err := newClient(conf, auth)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("pkg smtp pool: NewSMTPPool: Init Connections: %w", err)
|
||||
}
|
||||
// Add to pool
|
||||
pool.connections <- c
|
||||
pool.countOfConnection++
|
||||
}
|
||||
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
func newClient(conf PoolConfig, auth smtp.Auth) (*smtp.Client, error) {
|
||||
c, err := smtp.Dial(conf.Host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("newClient: %w", err)
|
||||
}
|
||||
|
||||
if err = c.Hello(conf.Identity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Check TLS
|
||||
if ok, _ := c.Extension("STARTTLS"); ok {
|
||||
config := &tls.Config{ServerName: conf.Host}
|
||||
if err = c.StartTLS(config); err != nil {
|
||||
return nil, fmt.Errorf("newClient: %w", err)
|
||||
}
|
||||
}
|
||||
// Auth
|
||||
if ok, _ := c.Extension("AUTH"); ok {
|
||||
if auth == nil {
|
||||
return nil, errors.New("newClient: auth is nil")
|
||||
}
|
||||
if err := c.Auth(auth); err != nil {
|
||||
return nil, fmt.Errorf("newClient: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.Noop(); err != nil {
|
||||
return nil, fmt.Errorf("newClient: c.Noop() error: %w", err)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Daemon. Doing Helth-Check and Auto-Heal of SMTPPool
|
||||
func (p *SMTPPool) healthChecker(auth smtp.Auth) {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
for range ticker.C {
|
||||
|
||||
// Auto-Heal Pool
|
||||
|
||||
current := atomic.LoadInt32(&p.countOfConnection)
|
||||
needed := int32(p.config.CountOfConnection) - current
|
||||
if needed > 0 {
|
||||
for range p.config.CountOfConnection - p.countOfConnection {
|
||||
newConn, err := newClient(p.config, auth)
|
||||
if err != nil {
|
||||
fmt.Println("WARNING: pkg smtp pool: healthChecker: Create connection: %w", err)
|
||||
continue
|
||||
}
|
||||
p.connections <- newConn
|
||||
atomic.AddInt32(&p.countOfConnection, 1)
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case conn := <-p.connections:
|
||||
if err := conn.Noop(); err != nil {
|
||||
atomic.AddInt32(&p.countOfConnection, -1)
|
||||
conn.Close()
|
||||
} else {
|
||||
p.connections <- conn
|
||||
}
|
||||
default:
|
||||
// All Conns is busy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *SMTPPool) SendMail(from string, to []string, mess []byte) error {
|
||||
return nil
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user