Files
dicom-iso/todo/go-mkiso-implementation.md
2026-06-05 08:11:44 +07:00

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.yaml with all documented keys (no secrets)
    • Create .gitignore (ignore config.yaml, *.iso, temp dirs)
  • 2. Config loadinginternal/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 runnerpkg/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 repointernal/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 serviceinternal/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 serviceinternal/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 serviceinternal/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. Handlersinternal/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 wiringinternal/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 entrypointmain.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.

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 additionsinternal/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 middlewareinternal/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 updateinternal/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.