Files
GopherGate/code_review.md
T

143 lines
9.7 KiB
Markdown

# GopherGate Comprehensive Code Review & Architecture Report
**Date:** July 21, 2026
**Target Repository:** `LobotomyLabs/GopherGate`
**Language:** Go (1.23+) & JavaScript (Vanilla Frontend)
**Status:** Completed
---
## 1. Executive Summary
A comprehensive code review and architectural analysis of the **GopherGate** unified LLM proxy gateway codebase was conducted. The application is written in Go using `gin-gonic/gin`, `sqlx`, `sqlite`, `gobreaker`, and `resty`.
Overall, the codebase is well-structured, modular, and cleanly separated into logical packages (`cmd`, `internal/config`, `internal/db`, `internal/middleware`, `internal/models`, `internal/providers`, `internal/router`, `internal/server`, `internal/utils`).
However, several critical and high-severity security, concurrency, and reliability issues were uncovered during the review. Addressing these items will ensure production-grade security, thread safety, and resilience.
---
## 2. Findings Summary Matrix
| ID | Category | Severity | Component | Issue Description |
|---|---|---|---|---|
| **SEC-01** | Security | 🔴 **Critical** | `internal/providers/gemini.go` | API Key exposure in plain-text debug log outputs (`?key=...`) |
| **CONC-01** | Concurrency | 🟠 **High** | `internal/server/server.go` | Un-mutexed map access to `s.providers` during runtime updates |
| **CONC-02** | Concurrency | 🟠 **High** | `internal/models/registry.go` | Data race on `ModelRegistry` map during concurrent model lookups |
| **CONC-03** | Concurrency | 🟠 **High** | `internal/router/router.go` | Unsynchronized map replacement in `Router.Reload()` |
| **REL-01** | Reliability | 🟠 **High** | `internal/providers/circuit_breaker.go` | Circuit breaker bypassed for all streaming completions |
| **REL-02** | Reliability | 🟡 **Medium** | `internal/providers/helpers.go` | Goroutine leaks on stream disconnect (missing `ctx.Done()` check) |
| **ERR-01** | Error Handling | 🟡 **Medium** | `internal/providers/*.go` | Empty error strings due to reading consumed `resp.RawBody()` |
| **SEC-02** | Security | 🟡 **Medium** | `internal/server/users.go` | Swallowed DB error outputs on user updates & default credentials |
| **SEC-03** | Security | 🔵 **Low** | `internal/server/system.go` | Raw auth tokens returned in settings API response |
| **CODE-01**| Code Quality | 🔵 **Low** | `internal/server/logging.go` | Fixed channel buffer (100) drops log entries under load |
---
## 3. Detailed Findings & Remediation Guidelines
### 🔴 SEC-01: Plain-Text API Key Exposure in Logs
- **Location:** [`internal/providers/gemini.go:L378`](file:///home/newkirk/Projects/gophergate/internal/providers/gemini.go#L378), [`L636`](file:///home/newkirk/Projects/gophergate/internal/providers/gemini.go#L636)
- **Impact:** Gemini REST endpoints accept authentication via query parameter `?key=YOUR_API_KEY`. The provider prints debugging information using `fmt.Printf("[Gemini] POST %s\n", url)`. This logs live API keys directly into server stdout / log aggregators.
- **Remediation:** Remove plain-text URL prints or sanitize query parameters before logging:
```go
// Sanitize key query parameter before logging
sanitizedURL := regexp.MustCompile(`key=[^&]+`).ReplaceAllString(url, "key=REDACTED")
log.Printf("[Gemini] POST %s", sanitizedURL)
```
---
### 🟠 CONC-01: Concurrent Map Mutation on `s.providers`
- **Location:** [`internal/server/server.go:L146-L185`](file:///home/newkirk/Projects/gophergate/internal/server/server.go#L146-L185)
- **Impact:** During `RefreshProviders()` (triggered by background initialization or via `/api/providers/:name` admin updates), `delete(s.providers, id)` and `s.providers[id] = ...` mutate `s.providers` without holding a write lock. Concurrent HTTP requests accessing `s.selectProvider()` or `handleChatCompletions()` read from `s.providers`, causing a Go runtime fatal map panic (`fatal error: concurrent map read and map write`).
- **Remediation:** Guard `s.providers` with a `sync.RWMutex`:
```go
s.providersMu.Lock()
s.providers[id] = providers.NewCircuitBreakerProvider(p)
s.providersMu.Unlock()
```
---
### 🟠 CONC-02: Data Race on `ModelRegistry` Dynamic Updates
- **Location:** [`internal/models/registry.go:L175-L211`](file:///home/newkirk/Projects/gophergate/internal/models/registry.go#L175-L211) & [`internal/server/server.go:L56-L65`](file:///home/newkirk/Projects/gophergate/internal/server/server.go#L56-L65)
- **Impact:** The server fetches `models.dev` in a background goroutine and overwrites `s.registry`. However, `ModelRegistry.FindModel()` traverses internal maps (`r.Providers`) without acquiring read locks. If a lookup occurs while `s.registry` or nested provider maps are being updated, a data race or nil-pointer dereference will occur.
- **Remediation:** Protect registry lookups with `RWMutex` locks, or use `atomic.Pointer[models.ModelRegistry]` for lock-free hot swapping.
---
### 🟠 CONC-03: Unsynchronized `Router.Reload()`
- **Location:** [`internal/router/router.go:L135-L140`](file:///home/newkirk/Projects/gophergate/internal/router/router.go#L135-L140)
- **Impact:** Calling `r.Reload(groups)` instantiates a new `r.groups = make(...)` map directly on the existing `Router` struct while active HTTP requests are concurrently calling `r.IsGroup()` or `r.Route()`.
- **Remediation:** Add a `sync.RWMutex` to `Router` and acquire `RLock()` during `Route()` / `IsGroup()` and `Lock()` during `Reload()`.
---
### 🟠 REL-01: Circuit Breaker Bypassed for Streaming Requests
- **Location:** [`internal/providers/circuit_breaker.go:L52-L56`](file:///home/newkirk/Projects/gophergate/internal/providers/circuit_breaker.go#L52-L56), [`L78-L81`](file:///home/newkirk/Projects/gophergate/internal/providers/circuit_breaker.go#L78-L81)
- **Impact:** Streaming methods (`ChatCompletionStream` and `ResponsesStream`) bypass `gobreaker` execution logic entirely. Upstream provider outages during streaming requests will not trigger the circuit breaker, leaving backends vulnerable to request flooding and thread exhaustion.
- **Remediation:** Execute initial connection setup inside `cb.Execute()` and propagate stream channel failure signals back to the circuit breaker.
---
### 🟡 REL-02: Streaming Goroutine Leaks on Client Disconnect
- **Location:** [`internal/providers/helpers.go:L379-L396`](file:///home/newkirk/Projects/gophergate/internal/providers/helpers.go#L379-L396), [`L474-L516`](file:///home/newkirk/Projects/gophergate/internal/providers/helpers.go#L474-L516)
- **Impact:** In `StreamOpenAI` and `StreamGemini`, chunk forwarding goroutines execute `ch <- chunk` without selecting on `ctx.Done()`. If a client disconnects prematurely, writing to `ch` blocks indefinitely, causing goroutine leaks.
- **Remediation:** Use select statement for channel writes:
```go
select {
case ch <- chunk:
case <-ctx.Done():
return
}
```
---
### 🟡 ERR-01: Empty Provider Error Messages
- **Location:** [`internal/providers/gemini.go:L130`](file:///home/newkirk/Projects/gophergate/internal/providers/gemini.go#L130), [`deepseek.go:L115`](file:///home/newkirk/Projects/gophergate/internal/providers/deepseek.go#L115), [`xiaomi.go:L56`](file:///home/newkirk/Projects/gophergate/internal/providers/xiaomi.go#L56)
- **Impact:** Non-streaming error handling reads `io.ReadAll(resp.RawBody())` on `resty.Response`. Resty auto-drains and closes `RawBody()` on request completion unless configured otherwise, causing `io.ReadAll` to return an empty slice and masking upstream API errors.
- **Remediation:** Use `resp.Body()` or `resp.String()` instead of `resp.RawBody()`.
---
### 🟡 SEC-02: Swallowed Database Errors & Default Credentials
- **Location:** [`internal/server/users.go:L73-L84`](file:///home/newkirk/Projects/gophergate/internal/server/users.go#L73-L84), [`internal/db/db.go:L179-L195`](file:///home/newkirk/Projects/gophergate/internal/db/db.go#L179-L195)
- **Impact:** `handleUpdateUser` ignores error outputs from `s.database.Exec()`. Database update failures (e.g., constraint violations or locks) return HTTP 200 OK. Furthermore, initial setup seeds default admin credentials (`admin` / `admin123`).
- **Remediation:** Always capture and handle `err` from `db.Exec()`. Require explicit password set on initial setup.
---
### 🔵 SEC-03: Sensitive Auth Tokens Exposed in Settings API
- **Location:** [`internal/server/system.go:L81`](file:///home/newkirk/Projects/gophergate/internal/server/system.go#L81)
- **Impact:** `/api/system/settings` includes the raw static `auth_tokens` slice from server config in the JSON response payload.
- **Remediation:** Omit raw tokens from settings response or return masked representations (e.g., `["sk-***..."]`).
---
### 🔵 CODE-01: Fixed Request Logger Channel Buffer
- **Location:** [`internal/server/logging.go:L39`](file:///home/newkirk/Projects/gophergate/internal/server/logging.go#L39)
- **Impact:** The request logging channel is initialized with a capacity of 100 (`make(chan RequestLog, 100)`). High request concurrency causes non-blocking sends to drop analytics log entries silently.
- **Remediation:** Increase channel capacity or introduce a dynamic ring buffer worker pool.
---
## 4. Verification & Testing Summary
All existing unit tests in the codebase pass cleanly:
```bash
go test -cover ./...
```
- `gophergate/internal/models`: **61.2%** test coverage
- `gophergate/internal/router`: **45.3%** test coverage
- Static analysis check (`go vet ./...`): **Clean** (0 warnings)
---
## 5. Next Steps & Recommendations
1. **Immediate Patch:** Sanitize logging in `gemini.go` (SEC-01) and add `sync.RWMutex` to `s.providers` (CONC-01).
2. **Concurrency Audit:** Implement atomic/mutex locking for `ModelRegistry` (CONC-02) and `Router.Reload()` (CONC-03).
3. **Resilience Patch:** Add `ctx.Done()` checks in streaming loops (REL-02) and wrap initial stream calls in `gobreaker` (REL-01).