7.8 KiB
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.yamlwith all documented keys (no secrets) - Create
.gitignore(ignoreconfig.yaml,*.iso, temp dirs)
- Create
-
2. Config loading —
internal/config/config.go- Define
Configstruct matchingdocs/go-mkiso-design.md§ config.yaml - Load from YAML (
gopkg.in/yaml.v3) or JSON (encoding/jsonfor zero deps) - Validate required fields on load (pacs host, dcmtk paths)
- ⚠️ Skip
auth:section for Stage 1
- Define
-
3. DICOM command runner —
pkg/dicom/command.goStartStoresCP,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
- HTTP client to patient-data API (see
-
5. DICOM service —
internal/service/dicom.goFetchDICOM(ctx, accessionNumber, destDir) → (filesCount int, error)FetchDICOMMultiple(ctx, accessionNumbers []string, destDir) → (totalFiles int, error)- storescp lifecycle: start → wait ready → movescu → stop
deferfor 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.goGenerateISO(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(seetodo/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}.goGET /api/health— health check + dependency statusGET /api/iso/download?accession_number=X— single ISO downloadGET /api/iso/download-multiple?accession_numbers=X,Y,Z— multi ISO downloadGET /api/iso/print?accession_number=X— DICOM relay (auto-detects comma → multi)- ⚠️ Print uses ONE endpoint for both single and multi: comma in
accession_numbertriggers multi mode, noprint-multipleendpoint needed (see pre-flight gap #3)
-
9. Route wiring —
internal/route/route.go- Go 1.22+
http.NewServeMuxwith method+path patterns - Request ID middleware (UUID,
X-Request-IDheader) - 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
- Go 1.22+
-
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:
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):
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_keyfield (the expected key) - Add
auth.enabledboolean (false in Stage 1 config, true in Stage 2)
- Add
-
13. API Key middleware —
internal/middleware/auth.go- Extract
X-API-Keyfrom request header - Compare against config value
- Return 401 on mismatch
- Skip health endpoint (
/api/healthstays public)
- Extract
-
14. Route wiring update —
internal/route/route.go- Apply API key middleware to all
/api/iso/*routes (download, download-multiple, print) - Keep
/api/healthpublic
- Apply API key middleware to all
-
15. nginx config — deploy reverse proxy with header injection
- Map old PHP paths to new Go API paths
- Inject
X-API-Keyheader - 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.