# Go mkiso Server — Implementation Checklist ## Stage 1 — Plain MVP (no auth) Goal: Functional replacement for all three PHP scripts. No security — plain GET endpoints. - [ ] 1. **Project scaffolding** - Create `go.mod` (`module mkiso-server`, go 1.22+) - Create directory structure (`internal/{config,route,handler,service,repo}`, `pkg/dicom`) - Create `config.example.yaml` with all documented keys (no secrets) - Create `.gitignore` (ignore `config.yaml`, `*.iso`, temp dirs) - [ ] 2. **Config loading** — `internal/config/config.go` - Define `Config` struct matching `docs/go-mkiso-design.md` § config.yaml - Load from YAML (`gopkg.in/yaml.v3`) or JSON (`encoding/json` for zero deps) - Validate required fields on load (pacs host, dcmtk paths) - ⚠️ Skip `auth:` section for Stage 1 - [ ] 3. **DICOM command runner** — `pkg/dicom/command.go` - `StartStoresCP`, `StopStoresCP`, `RunMoveSCU`, `RunStoreSCU`, `RunGenISOImage` - Port allocation: `allocatePort(basePort, portRange)` — mutex-guarded map - All functions return `(exitCode int, stdout, stderr string, error)` - Logging with slog - [ ] 4. **Patient repo** — `internal/repo/patient.go` - HTTP client to patient-data API (see `docs/patient-api-spec.md`) - `ByAccessionNumber(ctx, accessionNumber) → (*PatientData, error)` - Configurable auth header from config (for when patient API itself needs auth) - Timeout + retry with backoff - [ ] 5. **DICOM service** — `internal/service/dicom.go` - `FetchDICOM(ctx, accessionNumber, destDir) → (filesCount int, error)` - `FetchDICOMMultiple(ctx, accessionNumbers []string, destDir) → (totalFiles int, error)` - storescp lifecycle: start → wait ready → movescu → stop - `defer` for guaranteed storescp cleanup - ⚠️ After FetchDICOM, check `filesCount > 0`. If 0, return error `"no DICOM data"` before attempting ISO/genisoimage. - [ ] 6. **ISO service** — `internal/service/iso.go` - `GenerateISO(ctx, accessionNumber) → (isoPath string, cleanup func(), error)` - `GenerateISOMultiple(ctx, accessionNumbers) → (isoPath string, cleanup func(), error)` - Temp dir + microdicom copy + FetchDICOM + genisoimage - Patient API for multi-accession filename - [ ] 7. **Relay service** — `internal/service/relay.go` (see `todo/go-print-relay.detail.md`) - `RelayToCDPublisher(ctx, accessionNumbers) → (*RelayResult, error)` - Patient API validation + FetchDICOM + storescu to CD Publisher - Response includes `patient_name`, `destination`, `files_sent` - [ ] 8. **Handlers** — `internal/handler/{health,iso,print}.go` - `GET /api/health` — health check + dependency status - `GET /api/iso/download?accession_number=X` — single ISO download - `GET /api/iso/download-multiple?accession_numbers=X,Y,Z` — multi ISO download - `GET /api/iso/print?accession_number=X` — DICOM relay (auto-detects comma → multi) - ⚠️ Print uses ONE endpoint for both single and multi: comma in `accession_number` triggers multi mode, no `print-multiple` endpoint needed (see pre-flight gap #3) - [ ] 9. **Route wiring** — `internal/route/route.go` - Go 1.22+ `http.NewServeMux` with method+path patterns - Request ID middleware (UUID, `X-Request-ID` header) - Recovery middleware (catch panics, log stack, return 500) - **Stage 1**: 4 endpoints — health, download, download-multiple, print (single endpoint, auto-detects commas) - **No auth middleware in Stage 1** - [ ] 10. **Main entrypoint** — `main.go` - Load config, wire dependencies, start server - Graceful shutdown (SIGINT/SIGTERM) - `slog.Info("server started", "port", cfg.Server.Port)` - [ ] 11. **Stage 1 testing** - Test all four endpoints with curl (no auth header needed) - Test concurrent requests - Test error cases - Test through nginx reverse proxy --- ## Stage 2 — Add Authentication Goal: Lock down endpoints. **Suggestion below**, implementor picks approach. ### Recommended: API Key via Reverse Proxy Injection **Why**: Zero changes to the HIS `pacs_downloadiso` module. The existing `window.open()` calls continue to work because nginx injects the auth header before proxying to Go. ``` Browser (HIS) nginx (PACS server) Go Server ──────────── ─────────────────── ───────── window.open("/mkiso.php?..") ──────────── GET ───────────→ add X-API-Key header ───── GET + key ──────→ validate key → 200 OK ←─── ISO stream ─────── ←── ISO download ──────────── ``` **nginx config:** ```nginx location ~ ^/(mkiso|mkiso_multiple|send_rimage_multiple)\.php$ { proxy_set_header X-API-Key "${MKISO_API_KEY}"; proxy_pass http://127.0.0.1:8080/api/iso/$1$is_args$args; } # Direct API access (for testing/other clients) location /api/ { proxy_pass http://127.0.0.1:8080; } ``` **Go middleware (simple):** ```go func APIKey(expectedKey string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("X-API-Key") != expectedKey { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } next.ServeHTTP(w, r) }) } } ``` **Pros:** - Zero HIS code changes — nginx handles it - Simple to implement (one string comparison in middleware) - No token expiry, no login endpoint needed - Key rotation = update config.yaml + reload nginx **Cons:** - Single shared key (no user-level audit) - Key visible in nginx config - No token expiry (must rotate manually) ### Alternative Suggestions | Approach | Complexity | HIS Changes | Best For | |----------|-----------|-------------|----------| | **API Key (recommended)** | Low | None | Internal hospital network, nginx proxy in place | | **Hardcoded JWT** | Low | None (nginx injects) | When you want expiry + claims but no login flow | | **JWT + Login endpoint** | Medium | Yes (or client script) | Multi-client, need user-level audit | | **IP whitelist** | Low | None | Fixed HIS server IP, simplest possible | | **mTLS** | High | Yes (client cert) | Strictest security, external-facing | ### Stage 2 Todo Items - [ ] 12. **Config additions** — `internal/config/config.go` - Add `auth.api_key` field (the expected key) - Add `auth.enabled` boolean (false in Stage 1 config, true in Stage 2) - [ ] 13. **API Key middleware** — `internal/middleware/auth.go` - Extract `X-API-Key` from request header - Compare against config value - Return 401 on mismatch - Skip health endpoint (`/api/health` stays public) - [ ] 14. **Route wiring update** — `internal/route/route.go` - Apply API key middleware to all `/api/iso/*` routes (download, download-multiple, print) - Keep `/api/health` public - [ ] 15. **nginx config** — deploy reverse proxy with header injection - Map old PHP paths to new Go API paths - Inject `X-API-Key` header - Verify HIS `window.open()` calls work end-to-end - [ ] 16. **Stage 2 testing** - Test with valid API key → 200 - Test with invalid API key → 401 - Test with missing API key → 401 - Test health endpoint still public - Test through nginx proxy (key injected) → 200 - Test direct access without nginx → 401 --- ## Stage 3 (Future) - [ ] JWT login endpoint (if multi-user audit needed) - [ ] Rate limiting - [ ] Metrics / Prometheus endpoint - [ ] Docker image - [ ] Remove Java/PHP after Go verified in production --- ## Dependencies (go.mod) | Package | Purpose | |---------|---------| | `gopkg.in/yaml.v3` | YAML config parsing *(only external dep)* | Everything else: **Go stdlib only**.