chore: finalize alpha-0.0.1 and freeze core engine
This commit is contained in:
parent
d8f48970c2
commit
497f188c89
@ -1,11 +1,11 @@
|
|||||||
root = "."
|
root = "."
|
||||||
testdata_dir = "testdata"
|
testdata_dir = "testdata"
|
||||||
tmp_dir = "tmp"
|
tmp_dir = "build"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
args_bin = []
|
args_bin = []
|
||||||
bin = "./tmp/main"
|
bin = "make run"
|
||||||
cmd = "go build -o ./tmp/main ./internal/cmd/server/"
|
cmd = "make build"
|
||||||
delay = 1000
|
delay = 1000
|
||||||
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||||
exclude_file = []
|
exclude_file = []
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,4 +2,4 @@ tmp/
|
|||||||
.env
|
.env
|
||||||
config.yaml
|
config.yaml
|
||||||
config.yml
|
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:
|
services:
|
||||||
postgres_test:
|
postgres:
|
||||||
image: postgres:17-alpine
|
image: postgres:17-alpine
|
||||||
|
container_name: clavis_db
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: test_hagrid_db
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
POSTGRES_USER: test_user
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
POSTGRES_PASSWORD: test_password
|
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:
|
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
|
go 1.24.4
|
||||||
|
|
||||||
@ -6,6 +6,8 @@ require (
|
|||||||
github.com/ProtonMail/go-crypto v1.3.0
|
github.com/ProtonMail/go-crypto v1.3.0
|
||||||
github.com/emersion/go-openpgp-hkp v0.0.0-20231106091114-bb7c1dad7252
|
github.com/emersion/go-openpgp-hkp v0.0.0-20231106091114-bb7c1dad7252
|
||||||
github.com/jackc/pgx/v5 v5.7.5
|
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
|
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/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 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
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 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
|
|||||||
@ -3,43 +3,105 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"gadrid/internal/repo/storage/postgresql"
|
controllerHkp "hkp-clavis/internal/controller/hkp"
|
||||||
storageService "gadrid/internal/service/storage/serv"
|
"hkp-clavis/internal/model"
|
||||||
|
"hkp-clavis/internal/repo/storage/postgresql"
|
||||||
|
storageService "hkp-clavis/internal/service/storage/serv"
|
||||||
|
|
||||||
hkp "github.com/emersion/go-openpgp-hkp"
|
hkp "github.com/emersion/go-openpgp-hkp"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
"github.com/lmittmann/tint"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
var Logger *slog.Logger
|
||||||
listenAddress = "localhost:8181"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dbRepo := postgresql.New(context.TODO(), dbpool)
|
db := postgresql.New(context.TODO(), dbpool, keyMngr)
|
||||||
storageService := storageService.New(dbRepo)
|
storage := storageService.New(db, keyMngr, defaultVerify)
|
||||||
|
|
||||||
handler := hkp.Handler{
|
controller, err := controllerHkp.New(storage)
|
||||||
Adder: storageService,
|
if err != nil {
|
||||||
Lookuper: storageService,
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
http.HandleFunc("/pks/lookup", func(w http.ResponseWriter, r *http.Request) {
|
mux := http.NewServeMux()
|
||||||
handler.ServeHTTP(w, r)
|
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) {
|
mux.HandleFunc("/pks/add", func(w http.ResponseWriter, r *http.Request) {
|
||||||
handler.ServeHTTP(w, r)
|
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.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
|
package hkp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"gadrid/internal/service/storage"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"hkp-clavis/internal/service/storage"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/ProtonMail/go-crypto/openpgp"
|
"github.com/ProtonMail/go-crypto/openpgp"
|
||||||
hkp "github.com/emersion/go-openpgp-hkp"
|
hkp "github.com/emersion/go-openpgp-hkp"
|
||||||
@ -10,24 +14,69 @@ import (
|
|||||||
type HkpController struct {
|
type HkpController struct {
|
||||||
hkp.Adder
|
hkp.Adder
|
||||||
hkp.Lookuper
|
hkp.Lookuper
|
||||||
storage storage.StorageInterface
|
storageService storage.StorageInterface
|
||||||
// verify
|
// verifyService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultTimeout = 30 * time.Second
|
||||||
|
minEntropy = 15
|
||||||
|
)
|
||||||
|
|
||||||
func New(st storage.StorageInterface) (HkpController, error) {
|
func New(st storage.StorageInterface) (HkpController, error) {
|
||||||
return HkpController{
|
return HkpController{
|
||||||
storage: st,
|
storageService: st,
|
||||||
}, nil
|
}, 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s HkpController) Get(r *hkp.LookupRequest) (openpgp.EntityList, error) {
|
func (s HkpController) Get(req *hkp.LookupRequest) (openpgp.EntityList, error) {
|
||||||
return openpgp.EntityList{}, nil
|
|
||||||
|
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) {
|
func (s HkpController) Index(req *hkp.LookupRequest) ([]hkp.IndexKey, error) {
|
||||||
return []hkp.IndexKey{}, nil
|
|
||||||
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
service "gadrid/internal/service/storage"
|
"hkp-clavis/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -17,11 +17,11 @@ type StorageRepositotyInterface interface {
|
|||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// ctx: Context for the operation.
|
// 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:
|
// Returns:
|
||||||
// error: An error if the operation fails, wrapped with context (e.g., database connection issues, SQL execution errors).
|
// 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.
|
// 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.
|
// 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.
|
// fingerprint: The fingerprint of the PGP key to retrieve.
|
||||||
//
|
//
|
||||||
// Returns:
|
// 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:
|
// error: Returns an error if:
|
||||||
// - The key is not found (errors.Is(err, ErrKeyNotFound)).
|
// - The key is not found (errors.Is(err, ErrKeyNotFound)).
|
||||||
// - Database query or scanning fails.
|
// - 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.
|
// Index searches for PGP keys based on a query string.
|
||||||
// It matches against UID strings and emails using case-insensitive regular expressions.
|
// 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).
|
// q: The query string (treated as a case-insensitive regular expression).
|
||||||
//
|
//
|
||||||
// Returns:
|
// 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.
|
// 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.
|
// 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.
|
// This method is intended to be run periodically, e.g., by a cron job.
|
||||||
//
|
//
|
||||||
|
|||||||
@ -7,34 +7,35 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"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.
|
// InMemStorageMock is a mock implementation of PGPKeyStorage for testing purposes.
|
||||||
// It stores data in memory, mimicking database operations.
|
// It stores data in memory, mimicking database operations.
|
||||||
type InMemStorageMock struct {
|
type InMemStorageMock struct {
|
||||||
keys map[string]*service.PGPkey
|
keys map[string]*model.PGPKey
|
||||||
uids map[string]map[string]*service.PGPUid // fingerprint -> uid_string -> UID
|
uids map[string]map[string]*model.PGPUid // fingerprint -> uid_string -> UID
|
||||||
mu sync.RWMutex // Mutex for concurrent access safety
|
mu sync.RWMutex // Mutex for concurrent access safety
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewInMemStorageMock creates a new in-memory mock storage.
|
// NewInMemStorageMock creates a new in-memory mock storage.
|
||||||
func NewInMemStorageMock() *InMemStorageMock {
|
func NewInMemStorageMock() *InMemStorageMock {
|
||||||
return &InMemStorageMock{
|
return &InMemStorageMock{
|
||||||
keys: make(map[string]*service.PGPkey),
|
keys: make(map[string]*model.PGPKey),
|
||||||
uids: make(map[string]map[string]*service.PGPUid),
|
uids: make(map[string]map[string]*model.PGPUid),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddKey implements PGPKeyStorage interface for the mock.
|
// AddKey implements PGPKeyStorage interface for the mock.
|
||||||
// See PGPKeyStorage interface for documentation.
|
// 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()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
for _, k := range keys {
|
for _, k := range keys {
|
||||||
// Simulate UPSERT for pgp_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,
|
Fingerprint: k.Fingerprint,
|
||||||
Packet: k.Packet,
|
Packet: k.Packet,
|
||||||
Revoked: k.Revoked,
|
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
|
// Ensure the inner map exists for UIDs
|
||||||
if _, ok := m.uids[k.Fingerprint]; !ok {
|
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
|
// Simulate INSERT (ON CONFLICT DO NOTHING) for pgp_uids
|
||||||
for _, u := range k.Uids {
|
for _, u := range k.Uids {
|
||||||
if _, exists := m.uids[k.Fingerprint][u.UIDString]; !exists {
|
if _, exists := m.uids[k.Fingerprint][u.UIDString]; !exists {
|
||||||
m.uids[k.Fingerprint][u.UIDString] = &service.PGPUid{ // Deep copy
|
m.uids[k.Fingerprint][u.UIDString] = &model.PGPUid{ // Deep copy
|
||||||
UIDString: u.UIDString,
|
UIDString: u.UIDString,
|
||||||
Email: u.Email,
|
Email: u.Email,
|
||||||
Verify: u.Verify,
|
Verify: u.Verify,
|
||||||
Token: u.Token,
|
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
|
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
|
var targetUIDString string // To be used as key in the map for update
|
||||||
for uidStr, uid := range keyUids {
|
for uidStr, uid := range keyUids {
|
||||||
if uid.Email == email {
|
if uid.Email == email {
|
||||||
@ -99,18 +100,18 @@ func (m *InMemStorageMock) VerifyUID(ctx context.Context, fingerprint, email, to
|
|||||||
|
|
||||||
// Simulate update
|
// Simulate update
|
||||||
targetUID.Verify = true
|
targetUID.Verify = true
|
||||||
targetUID.Token = "" // Simulate setting to null
|
targetUID.Token = "" // Simulate setting to null
|
||||||
targetUID.TokenExpires = time.Time{} // Simulate setting to null
|
targetUID.TokenExpires = time.Time{} // Simulate setting to null
|
||||||
|
|
||||||
// Ensure the updated UID is correctly stored back in the map
|
// Ensure the updated UID is correctly stored back in the map
|
||||||
m.uids[fingerprint][targetUIDString] = targetUID
|
m.uids[fingerprint][targetUIDString] = targetUID
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetKey implements PGPKeyStorage interface for the mock.
|
// GetKey implements PGPKeyStorage interface for the mock.
|
||||||
// See PGPKeyStorage interface for documentation.
|
// 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()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
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
|
// Create a deep copy of the key
|
||||||
resultKey := &service.PGPkey{
|
resultKey := &model.PGPKey{
|
||||||
Fingerprint: pgpKey.Fingerprint,
|
Fingerprint: pgpKey.Fingerprint,
|
||||||
Packet: pgpKey.Packet,
|
Packet: pgpKey.Packet,
|
||||||
Revoked: pgpKey.Revoked,
|
Revoked: pgpKey.Revoked,
|
||||||
Uids: []*service.PGPUid{}, // Only add verified UIDs
|
Uids: []*model.PGPUid{}, // Only add verified UIDs
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter UIDs by 'verified = true'
|
// Filter UIDs by 'verified = true'
|
||||||
@ -132,8 +133,8 @@ func (m *InMemStorageMock) GetKey(ctx context.Context, fingerprint string) ([]*s
|
|||||||
for _, uid := range keyUids {
|
for _, uid := range keyUids {
|
||||||
if uid.Verify {
|
if uid.Verify {
|
||||||
// Deep copy the UID
|
// Deep copy the UID
|
||||||
resultKey.Uids = append(resultKey.Uids, &service.PGPUid{
|
resultKey.Uids = append(resultKey.Uids, &model.PGPUid{
|
||||||
UIDString: uid.UIDString,
|
UIDString: uid.UIDString,
|
||||||
Email: uid.Email,
|
Email: uid.Email,
|
||||||
Verify: uid.Verify,
|
Verify: uid.Verify,
|
||||||
Token: uid.Token,
|
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.
|
// Index implements PGPKeyStorage interface for the mock.
|
||||||
// See PGPKeyStorage interface for documentation.
|
// 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()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
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.
|
// Simulate case-insensitive substring match for regex.
|
||||||
// For a more accurate mock, you'd use the 'regexp' package here.
|
// For a more accurate mock, you'd use the 'regexp' package here.
|
||||||
if strings.Contains(strings.ToLower(uid.UIDString), lowerQ) ||
|
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
|
matchedFingerprints[f] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var resultKeys []*service.PGPkey
|
var resultKeys []*model.PGPKey
|
||||||
for f := range matchedFingerprints {
|
for f := range matchedFingerprints {
|
||||||
// Reuse GetKey logic from mock (which filters for verified UIDs)
|
// Reuse GetKey logic from mock (which filters for verified UIDs)
|
||||||
keys, err := m.GetKey(ctx, f)
|
keys, err := m.GetKey(ctx, f)
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
repo "gadrid/internal/repo/storage"
|
"hkp-clavis/internal/model"
|
||||||
service "gadrid/internal/service/storage"
|
repo "hkp-clavis/internal/repo/storage"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -19,13 +19,71 @@ import (
|
|||||||
//go:embed structure.sql
|
//go:embed structure.sql
|
||||||
var db_struct string
|
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 {
|
type PostgresqlStorageRepo struct {
|
||||||
poll *pgxpool.Pool
|
poll *pgxpool.Pool
|
||||||
|
keyMngr *model.KeyManager
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get context, url
|
// Get context, url
|
||||||
// Return postgresql repository
|
// 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)
|
_, err := pool.Exec(ctx, db_struct)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -33,10 +91,12 @@ func New(ctx context.Context, pool *pgxpool.Pool) repo.StorageRepositotyInterfac
|
|||||||
|
|
||||||
return PostgresqlStorageRepo{
|
return PostgresqlStorageRepo{
|
||||||
poll: pool,
|
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)
|
tx, err := s.poll.Begin(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error postgresql: AddKey: begin transaction: %w", err)
|
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 {
|
for _, k := range keys {
|
||||||
fingerprint := k.Fingerprint
|
fingerprint := k.Fingerprint
|
||||||
// Upsert pgp_keys
|
// Upsert pgp_keys
|
||||||
_, err = tx.Exec(ctx, `
|
_, err = tx.Exec(ctx, insertKey, fingerprint, k.Packet, k.Revoked)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error postgresql: AddKey: upsert key %s: %w", fingerprint, err)
|
return fmt.Errorf("error postgresql: AddKey: upsert key %s: %w", fingerprint, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upsert pgp_uids
|
// Upsert pgp_uids
|
||||||
batch := &pgx.Batch{}
|
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 {
|
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)
|
br := tx.SendBatch(ctx, batch)
|
||||||
@ -97,11 +145,7 @@ func (s PostgresqlStorageRepo) VerifyUID(ctx context.Context, fingerprint, email
|
|||||||
var tokenExpiresAt sql.NullTime
|
var tokenExpiresAt sql.NullTime
|
||||||
var isVerified bool
|
var isVerified bool
|
||||||
|
|
||||||
row := tx.QueryRow(ctx, `
|
row := tx.QueryRow(ctx, selectUid, fingerprint, email)
|
||||||
select uid, verification_token, token_expires_at, verified
|
|
||||||
from pgp_uids
|
|
||||||
where fingerprint = $1 and email = $2;
|
|
||||||
`, fingerprint, email)
|
|
||||||
|
|
||||||
if err = row.Scan(&storedUID, &storedToken, &tokenExpiresAt, &isVerified); err != nil {
|
if err = row.Scan(&storedUID, &storedToken, &tokenExpiresAt, &isVerified); err != nil {
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
@ -124,13 +168,7 @@ func (s PostgresqlStorageRepo) VerifyUID(ctx context.Context, fingerprint, email
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UPDATE the UID status.
|
// UPDATE the UID status.
|
||||||
result, err := tx.Exec(ctx, `
|
result, err := tx.Exec(ctx, updateUidVerify, fingerprint, email, token)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error postgresql: VerifyUID: failed to update UID verification status: %w", err)
|
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)
|
return tx.Commit(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s PostgresqlStorageRepo) GetKey(ctx context.Context, fngprt string) ([]*service.PGPkey, error) {
|
func (s PostgresqlStorageRepo) GetKey(ctx context.Context, fngprt string) (res []*model.PGPKey, err error) {
|
||||||
var packet string
|
pgpKey := s.keyMngr.Get()
|
||||||
var fingerprint string
|
|
||||||
var revoked bool
|
|
||||||
var update_time time.Time
|
|
||||||
|
|
||||||
row := s.poll.QueryRow(ctx, `
|
defer func() {
|
||||||
select fingerprint, packet, revoked, update_time
|
if err != nil {
|
||||||
from pgp_keys
|
s.keyMngr.Release(pgpKey)
|
||||||
where fingerprint ~* $1;
|
}
|
||||||
`, fngprt)
|
}()
|
||||||
|
|
||||||
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) {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
return nil, repo.ErrKeyNotFound
|
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
|
return nil, repo.ErrKeyNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch all VERIFIED UIDs for this key.
|
rows, err := s.poll.Query(ctx, fetchUidVerifed, pgpKey.Fingerprint)
|
||||||
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)
|
|
||||||
if err != nil {
|
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()
|
defer rows.Close()
|
||||||
|
|
||||||
var uids []*service.PGPUid
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
|
uid := s.keyMngr.GetUid()
|
||||||
uid := &service.PGPUid{}
|
|
||||||
var token sql.NullString
|
var token sql.NullString
|
||||||
var tokenExpiresAt sql.NullTime
|
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.Token = token.String
|
||||||
uid.TokenExpires = tokenExpiresAt.Time
|
uid.TokenExpires = tokenExpiresAt.Time
|
||||||
|
pgpKey.Uids = append(pgpKey.Uids, uid)
|
||||||
uids = append(uids, uid)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = rows.Err(); err != nil {
|
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{
|
return []*model.PGPKey{pgpKey}, nil
|
||||||
Fingerprint: fngprt,
|
|
||||||
UpdateTime: update_time,
|
|
||||||
Packet: packet,
|
|
||||||
Revoked: revoked,
|
|
||||||
Uids: uids, // Only verified UIDs included
|
|
||||||
}
|
|
||||||
return []*service.PGPkey{pgpKey}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s PostgresqlStorageRepo) Index(ctx context.Context, q string) ([]*service.PGPkey, error) {
|
func (s PostgresqlStorageRepo) Index(ctx context.Context, q string) (resultKeys []*model.PGPKey, err error) {
|
||||||
// Search for matching fingerprints based on the query 'q'.
|
rows, err := s.poll.Query(ctx, indexKeyByString, 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)
|
|
||||||
if err != nil {
|
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
|
defer func() {
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repo.ErrKeyNotFound) {
|
s.keyMngr.Release(resultKeys...)
|
||||||
continue
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("error postgresql: Index: failed to retrieve key %s via GetKey: %w", fingerprint, err)
|
|
||||||
}
|
}
|
||||||
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
|
return resultKeys, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s PostgresqlStorageRepo) CleanupStaleKeys(ctx context.Context) (int, error) {
|
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, cleanupStaleKeys)
|
||||||
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
|
|
||||||
);
|
|
||||||
`)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("error postgresql: CleanupStaleKeys: failed to delete stale keys: %w", err)
|
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/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
repository "gadrid/internal/repo/storage"
|
"hkp-clavis/internal/model"
|
||||||
psqlrepo "gadrid/internal/repo/storage/postgresql"
|
repository "hkp-clavis/internal/repo/storage"
|
||||||
service "gadrid/internal/service/storage"
|
psqlrepo "hkp-clavis/internal/repo/storage/postgresql"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed structure.sql
|
//go:embed structure.sql
|
||||||
@ -25,21 +25,23 @@ func TestPostgresqlStorageRepo_AddKey(t *testing.T) {
|
|||||||
defer pool.Close()
|
defer pool.Close()
|
||||||
defer cleanupTestDB(t, pool)
|
defer cleanupTestDB(t, pool)
|
||||||
|
|
||||||
repo := psqlrepo.New(context.Background(), pool)
|
mgr := model.NewKeyManager()
|
||||||
|
|
||||||
|
repo := psqlrepo.New(context.Background(), pool, mgr)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// --- Test Case 1: Add a new key ---
|
// --- Test Case 1: Add a new key ---
|
||||||
t.Run("Add new key successfully", func(t *testing.T) {
|
t.Run("Add new key successfully", func(t *testing.T) {
|
||||||
key := &service.PGPkey{
|
key := &model.PGPKey{
|
||||||
Fingerprint: "FINGERPRINT_NEW_KEY",
|
Fingerprint: "FINGERPRINT_NEW_KEY",
|
||||||
Packet: "-----BEGIN PGP PUBLIC KEY BLOCK-----NEW_KEY_PACKET-----END PGP PUBLIC KEY BLOCK-----",
|
Packet: "-----BEGIN PGP PUBLIC KEY BLOCK-----NEW_KEY_PACKET-----END PGP PUBLIC KEY BLOCK-----",
|
||||||
Revoked: false,
|
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)},
|
{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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Verify key was added
|
// Verify key was added
|
||||||
@ -60,27 +62,27 @@ func TestPostgresqlStorageRepo_AddKey(t *testing.T) {
|
|||||||
|
|
||||||
// --- Test Case 2: Update an existing key (packet, revoked status) ---
|
// --- Test Case 2: Update an existing key (packet, revoked status) ---
|
||||||
t.Run("Update existing key packet and revoked status", func(t *testing.T) {
|
t.Run("Update existing key packet and revoked status", func(t *testing.T) {
|
||||||
initialKey := &service.PGPkey{
|
initialKey := &model.PGPKey{
|
||||||
Fingerprint: "FINGERPRINT_EXISTING",
|
Fingerprint: "FINGERPRINT_EXISTING",
|
||||||
Packet: "-----BEGIN PGP PUBLIC KEY BLOCK-----INITIAL_PACKET-----END PGP PUBLIC KEY BLOCK-----",
|
Packet: "-----BEGIN PGP PUBLIC KEY BLOCK-----INITIAL_PACKET-----END PGP PUBLIC KEY BLOCK-----",
|
||||||
Revoked: false,
|
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)},
|
{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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Simulate an update: new packet, revoked true
|
// Simulate an update: new packet, revoked true
|
||||||
updatedKey := &service.PGPkey{
|
updatedKey := &model.PGPKey{
|
||||||
Fingerprint: "FINGERPRINT_EXISTING",
|
Fingerprint: "FINGERPRINT_EXISTING",
|
||||||
Packet: "-----BEGIN PGP PUBLIC KEY BLOCK-----UPDATED_PACKET-----END PGP PUBLIC KEY BLOCK-----",
|
Packet: "-----BEGIN PGP PUBLIC KEY BLOCK-----UPDATED_PACKET-----END PGP PUBLIC KEY BLOCK-----",
|
||||||
Revoked: true,
|
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)},
|
{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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Verify key was updated
|
// Verify key was updated
|
||||||
@ -100,28 +102,28 @@ func TestPostgresqlStorageRepo_AddKey(t *testing.T) {
|
|||||||
|
|
||||||
// --- Test Case 3: Add new UID to existing key ---
|
// --- Test Case 3: Add new UID to existing key ---
|
||||||
t.Run("Add new UID to existing key", func(t *testing.T) {
|
t.Run("Add new UID to existing key", func(t *testing.T) {
|
||||||
key := &service.PGPkey{
|
key := &model.PGPKey{
|
||||||
Fingerprint: "FINGERPRINT_ADD_UID",
|
Fingerprint: "FINGERPRINT_ADD_UID",
|
||||||
Packet: "-----BEGIN PGP PUBLIC KEY BLOCK-----ADD_UID_KEY-----END PGP PUBLIC KEY BLOCK-----",
|
Packet: "-----BEGIN PGP PUBLIC KEY BLOCK-----ADD_UID_KEY-----END PGP PUBLIC KEY BLOCK-----",
|
||||||
Revoked: false,
|
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: "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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Add a second UID to the same key
|
// Add a second UID to the same key
|
||||||
updatedKey := &service.PGPkey{
|
updatedKey := &model.PGPKey{
|
||||||
Fingerprint: "FINGERPRINT_ADD_UID",
|
Fingerprint: "FINGERPRINT_ADD_UID",
|
||||||
Packet: "-----BEGIN PGP PUBLIC KEY BLOCK-----ADD_UID_KEY_UPDATED-----END PGP PUBLIC KEY BLOCK-----",
|
Packet: "-----BEGIN PGP PUBLIC KEY BLOCK-----ADD_UID_KEY_UPDATED-----END PGP PUBLIC KEY BLOCK-----",
|
||||||
Revoked: false,
|
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: "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)},
|
{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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Verify both UIDs exist
|
// Verify both UIDs exist
|
||||||
@ -137,7 +139,9 @@ func TestPostgresqlStorageRepo_VerifyUID(t *testing.T) {
|
|||||||
defer pool.Close()
|
defer pool.Close()
|
||||||
defer cleanupTestDB(t, pool)
|
defer cleanupTestDB(t, pool)
|
||||||
|
|
||||||
repo := psqlrepo.New(context.Background(), pool)
|
mgr := model.NewKeyManager()
|
||||||
|
|
||||||
|
repo := psqlrepo.New(context.Background(), pool, mgr)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Add a key with an unverified UID
|
// Add a key with an unverified UID
|
||||||
@ -146,12 +150,12 @@ func TestPostgresqlStorageRepo_VerifyUID(t *testing.T) {
|
|||||||
uidToken := "verifytoken123"
|
uidToken := "verifytoken123"
|
||||||
uidExpires := time.Now().Add(time.Hour)
|
uidExpires := time.Now().Add(time.Hour)
|
||||||
|
|
||||||
err := repo.AddKey(ctx, []*service.PGPkey{
|
err := repo.AddKey(ctx, []*model.PGPKey{
|
||||||
{
|
{
|
||||||
Fingerprint: keyFingerprint,
|
Fingerprint: keyFingerprint,
|
||||||
Packet: "...",
|
Packet: "...",
|
||||||
Revoked: false,
|
Revoked: false,
|
||||||
Uids: []*service.PGPUid{
|
Uids: []*model.PGPUid{
|
||||||
{UIDString: "Verify Me <" + uidEmail + ">", Email: uidEmail, Verify: false, Token: uidToken, TokenExpires: uidExpires},
|
{UIDString: "Verify Me <" + uidEmail + ">", Email: uidEmail, Verify: false, Token: uidToken, TokenExpires: uidExpires},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -198,12 +202,12 @@ func TestPostgresqlStorageRepo_VerifyUID(t *testing.T) {
|
|||||||
expiredToken := "expiredtoken"
|
expiredToken := "expiredtoken"
|
||||||
expiredTime := time.Now().Add(-time.Hour)
|
expiredTime := time.Now().Add(-time.Hour)
|
||||||
|
|
||||||
err := repo.AddKey(ctx, []*service.PGPkey{
|
err := repo.AddKey(ctx, []*model.PGPKey{
|
||||||
{
|
{
|
||||||
Fingerprint: expiredFingerprint,
|
Fingerprint: expiredFingerprint,
|
||||||
Packet: "...",
|
Packet: "...",
|
||||||
Revoked: false,
|
Revoked: false,
|
||||||
Uids: []*service.PGPUid{
|
Uids: []*model.PGPUid{
|
||||||
{UIDString: "Expired <" + expiredEmail + ">", Email: expiredEmail, Verify: false, Token: expiredToken, TokenExpires: expiredTime},
|
{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 pool.Close()
|
||||||
defer cleanupTestDB(t, pool)
|
defer cleanupTestDB(t, pool)
|
||||||
|
|
||||||
repo := psqlrepo.New(context.Background(), pool)
|
mgr := model.NewKeyManager()
|
||||||
|
repo := psqlrepo.New(context.Background(), pool, mgr)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Add a key with mixed UIDs (verified and unverified)
|
// Add a key with mixed UIDs (verified and unverified)
|
||||||
testFingerprint := "FINGERPRINT_GET_KEY"
|
testFingerprint := "FINGERPRINT_GET_KEY"
|
||||||
err := repo.AddKey(ctx, []*service.PGPkey{
|
err := repo.AddKey(ctx, []*model.PGPKey{
|
||||||
{
|
{
|
||||||
Fingerprint: testFingerprint,
|
Fingerprint: testFingerprint,
|
||||||
Packet: "PACKET_DATA",
|
Packet: "PACKET_DATA",
|
||||||
Revoked: false,
|
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: "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: "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)},
|
{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 pool.Close()
|
||||||
defer cleanupTestDB(t, pool)
|
defer cleanupTestDB(t, pool)
|
||||||
|
|
||||||
repo := psqlrepo.New(context.Background(), pool)
|
mgr := model.NewKeyManager()
|
||||||
|
|
||||||
|
repo := psqlrepo.New(context.Background(), pool, mgr)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Add multiple keys with various UIDs
|
// Add multiple keys with various UIDs
|
||||||
keysToLoad := []*service.PGPkey{
|
keysToLoad := []*model.PGPKey{
|
||||||
{
|
{
|
||||||
Fingerprint: "FINGERPRINT_ALPHA", Packet: "alpha_packet", Revoked: false,
|
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: "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)},
|
{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,
|
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)},
|
{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,
|
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: "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)},
|
{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
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"context"
|
||||||
|
"hkp-clavis/internal/model"
|
||||||
|
|
||||||
"github.com/ProtonMail/go-crypto/openpgp"
|
"github.com/ProtonMail/go-crypto/openpgp"
|
||||||
|
hkp "github.com/emersion/go-openpgp-hkp"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StorageInterface interface {
|
type StorageInterface interface {
|
||||||
AddKey(openpgp.EntityList) error
|
Add(ctx context.Context, el openpgp.EntityList) ([]*model.PGPKey, error)
|
||||||
GetKeyByUID(uid string) openpgp.EntityList
|
Get(ctx context.Context, r *hkp.LookupRequest) (openpgp.EntityList, error)
|
||||||
GetKeyByFingerprint(fngr string) openpgp.EntityList
|
Index(ctx context.Context, r *hkp.LookupRequest) ([]hkp.IndexKey, error)
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
package storage
|
package serv
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
|
||||||
|
|
||||||
storeRepo "gadrid/internal/repo/storage"
|
"hkp-clavis/internal/model"
|
||||||
service "gadrid/internal/service/storage"
|
storeRepo "hkp-clavis/internal/repo/storage"
|
||||||
|
service "hkp-clavis/internal/service/storage"
|
||||||
|
|
||||||
"github.com/ProtonMail/go-crypto/openpgp"
|
"github.com/ProtonMail/go-crypto/openpgp"
|
||||||
hkp "github.com/emersion/go-openpgp-hkp"
|
hkp "github.com/emersion/go-openpgp-hkp"
|
||||||
@ -18,79 +17,82 @@ type StorageService struct {
|
|||||||
hkp.Adder
|
hkp.Adder
|
||||||
hkp.Lookuper
|
hkp.Lookuper
|
||||||
// mailService interface{}
|
// 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{
|
return StorageService{
|
||||||
storage: storageRepository,
|
storage: storageRepository,
|
||||||
|
keyManager: keyManager,
|
||||||
|
config: StorageServiceConfig{
|
||||||
|
defaultVerify: defaultVerifyValue,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Allocation manager
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func (s StorageService) Add(el openpgp.EntityList) error {
|
func (s StorageService) Add(ctx context.Context, el openpgp.EntityList) ([]*model.PGPKey, error) {
|
||||||
fmt.Println(el)
|
pgpKeys, release, err := s.keyManager.ConverterPgpToService(el, s.config.defaultVerify) // Assuming ConverterPgpToService might return an error now.
|
||||||
pgpKeys, err := service.ConverterPgpToService(el) // Assuming ConverterPgpToService might return an error now.
|
defer release()
|
||||||
|
|
||||||
if err != nil {
|
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 {
|
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.
|
err = s.storage.AddKey(ctx, pgpKeys) // Pass context here
|
||||||
// 3. ALWAYS check the error returned by the storage layer.
|
|
||||||
err = s.storage.AddKey(context.Background(), pgpKeys) // Pass context here
|
|
||||||
if err != nil {
|
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
|
func (s StorageService) Get(ctx context.Context, req *hkp.LookupRequest) (openpgp.EntityList, error) {
|
||||||
// In Specification "op=get" fingerprint and uid
|
dbKeys, err := s.storage.Index(ctx, req.Search)
|
||||||
// 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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, storeRepo.ErrKeyNotFound) {
|
return nil, fmt.Errorf("service error: Get: search failed: %w", err)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(dbKeys) == 0 {
|
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)
|
var finalEntityList openpgp.EntityList
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("service error: Get: failed to sanitize and filter key %s: %w", dbKey.Fingerprint, err)
|
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) {
|
func (s StorageService) Index(ctx context.Context, req *hkp.LookupRequest) ([]hkp.IndexKey, error) {
|
||||||
|
dbKeys, err := s.storage.Index(ctx, req.Search)
|
||||||
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
|
|
||||||
if err != nil {
|
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 {
|
if len(dbKeys) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -98,7 +100,7 @@ func (s StorageService) Index(req *hkp.LookupRequest) ([]hkp.IndexKey, error) {
|
|||||||
|
|
||||||
var respIndex []hkp.IndexKey
|
var respIndex []hkp.IndexKey
|
||||||
for _, dbKey := range dbKeys {
|
for _, dbKey := range dbKeys {
|
||||||
filteredEntity, err := s.sanitizeAndFilterKey(dbKey)
|
filteredEntity, err := model.SanitizeAndFilterKey(dbKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Warning: service error: Index: failed to sanitize and filter key %s: %v, skipping.", dbKey.Fingerprint, err)
|
log.Printf("Warning: service error: Index: failed to sanitize and filter key %s: %v, skipping.", dbKey.Fingerprint, err)
|
||||||
continue // Skip this problematic key
|
continue // Skip this problematic key
|
||||||
@ -120,34 +122,3 @@ func (s StorageService) Index(req *hkp.LookupRequest) ([]hkp.IndexKey, error) {
|
|||||||
|
|
||||||
return respIndex, nil
|
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