Skip to content

Repository files navigation

odo

odo is an early MVP skeleton for a Go-based, self-hostable, API-first FOSS library access middleware/proxy application.

The important architectural rule is that management and configuration happen through versioned JSON APIs. The admin UI at /admin uses those same APIs with fetch; it is not a separate control plane.

What This MVP Is

  • A single compiled Go application.
  • Local HTTP service on port 8080 by default.
  • Versioned JSON APIs under /api/v1.
  • Embedded SQLite persistence using modernc.org/sqlite.
  • Resource registry import from dropped JSON config files.
  • URL/domain rule testing.
  • A minimal safe outbound GET/HEAD proxy for configured and allowed targets.
  • Partial HTML/CSS asset URL rewriting for basic proxied page rendering.
  • Privacy-conscious first-pass request logging that avoids logging full query strings.

What It Is Not Yet

  • A full JavaScript-aware browser compatibility proxy.
  • A full admin login/session system. Management APIs use bearer API keys with hashed database storage and bootstrap/dev fallback support.
  • A SAML/Shibboleth Service Provider.
  • A production HA deployment.
  • A complete audit implementation.

AI assistance disclosure

Odo is built with human direction and review, with some assistance from AI tools for drafting code, tests, documentation, examples, and design notes.

Because Odo concerns privacy, authentication, access control, and library infrastructure, AI-generated output is not treated as authoritative. All AI-assisted changes should be reviewed for correctness, security, privacy impact, maintainability, and alignment with the project’s goals.

The project maintainer and contributors remain responsible for what is committed to the repository.

Installation

  • Local development: run go run ./cmd/odo and open http://127.0.0.1:8080/admin.
  • Linux VM install: build a binary, install the systemd unit, keep config in /etc/odo, data in /var/lib/odo, and logs in /var/log/odo. See Installing Odo on a Linux VM.
  • Container install: build the image, mount persistent data/config volumes, and put a reverse proxy in front. See Deploying Odo with a Container.

Local Run

Run without a bootstrap API key for local development. Protected API endpoints still require an authenticated user session or a stored API key:

go run ./cmd/odo

Run with an API key:

APP_ADMIN_API_KEY=devsecret go run ./cmd/odo

For stored API keys, set a hash secret:

APP_ADMIN_API_KEY=devsecret APP_KEY_HASH_SECRET='change-me-long-random-secret' go run ./cmd/odo

Open:

http://127.0.0.1:8080/admin

Deployment

For local development, use go run ./cmd/odo. For a Linux VM or container behind a real FQDN, set APP_PUBLIC_URL, use a persistent data volume for SQLite, and keep config under a persistent config directory. Production deployments need real secrets for APP_ADMIN_API_KEY and APP_KEY_HASH_SECRET; do not expose a dev instance or devsecret publicly.

See Installing Odo on a Linux VM for binary/systemd installs, or Deploying Odo with a Container for Podman, Quadlet, persistent volume, and reverse proxy examples.

API Documentation

/api/v1 is the versioned API namespace and a safe, public JSON discovery endpoint. Its links expand to the routes available to a valid user session or API key; discovery does not grant access, and specific resources under /api/v1/{resource} retain their authentication and scope requirements.

The current OpenAPI 3.1 schema is served by the app at /openapi.yaml:

http://127.0.0.1:8080/openapi.yaml

Admin UI

Open http://127.0.0.1:8080/admin. The admin UI is organized into sections for Dashboard, Resources, Config, Diagnostics / Logs, API Keys, Users, Auth / SAML, and Settings / System.

The built-in admin UI is intentionally minimal: plain, readable, and focused on common sysadmin and library-staff tasks. All key operations use the documented /api/v1 JSON endpoints, so sites can build their own custom UI, scripts, or automation against the same API instead of customizing the bundled page.

Enter an APP_ADMIN_API_KEY bootstrap token or stored API key in the global Admin API Key field for protected actions. The key is kept only in the page runtime and is not stored in browser storage. Resources can still be created, edited, and deleted using a raw JSON editor.

API key management is available in the API Keys section. Newly created or rotated tokens are shown once with a copy warning and are not persisted by the UI. The UI uses the same documented /api/v1 endpoints available to scripts and integrations; it is not a separate control plane.

User management is available in the Users section. Admins can create local users, update roles/status, set passwords, lock/disable users, and revoke sessions through the same /api/v1/users endpoints used by scripts. Password hashes are never displayed.

The Resources section includes search, sort, filters, readable resource list rows, resource details, raw JSON editing, the Resource Config Builder, export controls, and integrated Proxy Test controls. It can generate JSON, validate it through /api/v1/resources/validate, save it through the normal resource API, export a resource-<id>.json file, or export the currently filtered resources as JSON.

Admin access

API keys are still supported for scripts, automation, and control-plane integrations. Human admins can also sign in at /login with a local account and use /admin through their browser session without pasting an API key. The Admin API Key field remains available as an optional override/testing mode and is not stored in browser storage.

Local user roles determine which admin sections are available. Backend scopes enforce permissions even if a UI section is hidden. Regular user accounts use /resources, not /admin.

Initial role mapping:

  • super_admin or legacy admin: admin
  • systems_admin: resources, config, diagnostics, logs, and system read access
  • resource_admin: resources/config write and diagnostics read
  • support_staff: resources, diagnostics, and logs read
  • security_admin: user management plus logs and diagnostics read
  • viewer: resources/config/diagnostics/system read
  • user: no admin scopes

When the admin UI uses a browser session for unsafe API calls, it sends X-Odo-CSRF. Bearer API-key requests do not require CSRF.

Adding resources

Use the admin Resource Config Builder for most additions. Start with a title, entry URL, and main domain, then generate and validate the JSON before saving. Use raw JSON for advanced cases such as header rules, anonymous URL rules, content rewrite rules, tags, or carefully reviewed compatibility settings. After saving, use the integrated Proxy Test in the Resources tab and Diagnostics to confirm the entry URL, search pages, detail pages, downloads, and any blocked or missed hosts. Use search, status/type/complexity filters, tags, and sorting to manage large collections and find complex resources.

See Adding Resources in Odo for a plain-language workflow and examples.

Environment variables:

  • APP_BIND_ADDR, preferred bind address for service installs, default :8080
  • APP_ADDR, older name for the bind address; still supported when APP_BIND_ADDR is unset
  • APP_ENV, default development; use production on servers
  • APP_DATA_DIR, default ./data in development and /var/lib/odo in production
  • APP_DB_PATH, default $APP_DATA_DIR/odo.db; production preference is /var/lib/odo/odo.db
  • APP_CONFIG_DIR, default ./config in development and /etc/odo in production
  • APP_PUBLIC_URL, optional public base URL used for generated SAML SP metadata defaults
  • APP_ADMIN_API_KEY, optional bootstrap/dev fallback for creating and managing stored API keys
  • APP_BOOTSTRAP_ADMIN_USERNAME, optional username used to create the first local admin user when no users exist
  • APP_BOOTSTRAP_ADMIN_PASSWORD, optional password used to create the first local admin user when no users exist
  • APP_BOOTSTRAP_ADMIN_EMAIL, optional email for the first local admin user
  • APP_PROXY_REQUIRE_LOGIN, default is enabled once local users exist; set false for development-only anonymous proxy access
  • APP_SESSION_PERSIST_ON_RESTART, default true in production and false in development; when false, existing browser sessions are rejected after Odo restarts
  • APP_SESSION_TTL_MINUTES, default 480; absolute browser session lifetime
  • APP_SESSION_IDLE_TIMEOUT_MINUTES, default 60; idle browser session timeout based on throttled last_seen_at updates
  • APP_KEY_HASH_SECRET, recommended secret used to HMAC stored API key tokens; if unset, local dev uses SHA-256 with a startup warning
  • APP_ACCESS_LOG_FORMAT, default privacy
  • APP_ACCESS_LOG_PATH, optional path to append access logs
  • APP_PROXY_DEBUG, default false; when true, /odo adds safe cookie/session diagnostic count headers without exposing cookie values
  • APP_PROXY_URL_MODE, default path; use query for /odo?url=... compatibility links or dual to build path links while accepting both path and query forms
  • APP_PROXY_ALLOW_LOCAL_HTTP, default false; development-only loopback HTTP allowance for loadtest/ fake vendor runs, never for production
  • APP_VIRTUAL_HOST_BASE_DOMAIN, reserved for future virtual-host proxy mode and not active yet
  • APP_VIRTUAL_HOST_ENCODING, reserved for future virtual-host proxy mode and not active yet
  • APP_PROXY_MAX_BODY_BYTES, default 10485760; maximum proxied POST request body size
  • APP_PROXY_INJECT_JS_SHIM, default true; injects a small same-origin fetch()/XHR rewrite shim into proxied HTML
  • APP_PROXY_REFERER_RECOVERY, default true; recovers missed local asset/script paths when a proxied Referer identifies the upstream host
  • APP_TRUST_PROXY_HEADERS, default false; set true only behind a trusted reverse proxy that controls X-Forwarded-* headers

API Examples

Health:

curl -s http://127.0.0.1:8080/api/v1/health

Validate resource config files without writing to the database:

curl -X POST http://127.0.0.1:8080/api/v1/config/validate \
  -H 'Authorization: Bearer devsecret' | jq

Import resource config files:

curl -s -X POST http://127.0.0.1:8080/api/v1/config/import \
  -H 'Authorization: Bearer devsecret' | jq

List config revisions:

curl http://127.0.0.1:8080/api/v1/config/revisions \
  -H 'Authorization: Bearer devsecret' | jq

Get a config revision:

curl http://127.0.0.1:8080/api/v1/config/revisions/1 \
  -H 'Authorization: Bearer devsecret' | jq

List resources:

curl -s http://127.0.0.1:8080/api/v1/resources

Get one resource:

curl http://127.0.0.1:8080/api/v1/resources/jstor | jq

Delete a resource:

curl -X DELETE http://127.0.0.1:8080/api/v1/resources/jstor \
  -H 'Authorization: Bearer devsecret' | jq

Test a URL:

curl -s -X POST http://127.0.0.1:8080/api/v1/rules/test-url \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://www.jstor.org/stable/example"}'

Create or update a resource:

curl -s -X POST http://127.0.0.1:8080/api/v1/resources \
  -H 'Authorization: Bearer devsecret' \
  -H 'Content-Type: application/json' \
  -d @config/resources/jstor.json

Minimal proxy fetch:

curl -s 'http://127.0.0.1:8080/odo/https/www.jstor.org/stable/example'

Admin proxy test fetch:

curl -X POST http://127.0.0.1:8080/api/v1/proxy/test-fetch \
  -H 'Authorization: Bearer devsecret' \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://www.jstor.org/"}' | jq

Recent access logs:

curl http://127.0.0.1:8080/api/v1/logs/access/recent \
  -H 'Authorization: Bearer devsecret' | jq

API Key Management

APP_ADMIN_API_KEY remains a bootstrap/dev fallback. For ongoing use, create database-backed API keys and send them as Authorization: Bearer <token>. Odo stores only a hash and short prefix, never the full token after creation. The full token is shown only when a key is created or rotated.

Create a stored key:

curl -X POST http://127.0.0.1:8080/api/v1/api-keys \
  -H 'Authorization: Bearer devsecret' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Local admin","scopes":["admin"]}' | jq

Use the returned token:

curl http://127.0.0.1:8080/api/v1/config/revisions \
  -H 'Authorization: Bearer odo_live_...' | jq

Rotate or revoke a key:

curl -X POST http://127.0.0.1:8080/api/v1/api-keys/key_abc123/rotate \
  -H 'Authorization: Bearer odo_live_...' | jq

curl -X POST http://127.0.0.1:8080/api/v1/api-keys/key_abc123/revoke \
  -H 'Authorization: Bearer odo_live_...' | jq

Initial API key scopes are admin, api_keys:read, api_keys:write, resources:read, resources:write, config:read, config:write, diagnostics:read, logs:read, auth:read, auth:write, system:read, users:read, and users:write. The admin scope can access all management endpoints. Set APP_KEY_HASH_SECRET in persistent deployments so stored token hashes use HMAC-SHA256 instead of local-dev SHA-256.

Local Users and Browser Sessions

Odo now has local user accounts for browser access to proxied resources. To create the first admin user on an empty database:

APP_BOOTSTRAP_ADMIN_USERNAME=admin \
APP_BOOTSTRAP_ADMIN_PASSWORD='change-me-long-random-password' \
APP_ADMIN_API_KEY=devsecret \
go run ./cmd/odo

Open http://127.0.0.1:8080/login to sign in. Signed-in users can open http://127.0.0.1:8080/resources for a simple patron resource portal. Browser sessions use an HttpOnly odo_session cookie and proxy browsing also keeps upstream/vendor cookies in a separate server-side jar.

In production, browser sessions can persist across Odo restarts because session rows live in SQLite until they expire or are revoked. In development, APP_SESSION_PERSIST_ON_RESTART defaults to false, so an existing browser cookie is rejected after a restart without deleting the session row. Set APP_SESSION_PERSIST_ON_RESTART=true when you want restart-persistent sessions during local testing. Use APP_SESSION_TTL_MINUTES and APP_SESSION_IDLE_TIMEOUT_MINUTES to control absolute and idle session limits. last_seen_at updates are throttled so normal browsing does not write the session row on every proxied asset request.

Once local users exist, /odo proxy access requires login by default. When APP_PROXY_REQUIRE_LOGIN=true, every /odo proxy request requires a valid browser session unless the target matches an explicit anonymous_url_rule. Resource homepages, entry URLs, active resource domains, and direct deep links are not anonymous by default. For local development only, disable that gate with:

APP_PROXY_REQUIRE_LOGIN=false go run ./cmd/odo

Login and return-to-resource flow

Users can click Odo resource links directly, such as /odo/https/www.jstor.org/stable/123456 or a resource homepage like /odo/https/www.jstor.org/. If they are not logged in, Odo sends browser navigation requests to /login with a safe local next path. After a successful login, the user returns to the original proxied URL, including the original path and query string.

Unsafe next values are rejected to prevent open redirects. Odo accepts local paths such as /resources, /admin, /odo/https/www.jstor.org/, and /odo?url=https%3A%2F%2Fwww.jstor.org%2F. Absolute URLs, scheme-relative URLs, malformed slash tricks, backslashes, and control characters fall back to /resources. A next=/admin redirect is honored only for users with admin-like scopes.

Fetch/API-style proxy requests receive a JSON login_required response instead of an HTML redirect. This keeps scripts and app data calls from accidentally receiving a login page as data.

Use APP_PROXY_REQUIRE_LOGIN=false only for local proxy testing. anonymous_url_rules remain narrow exceptions for public vendor assets and still pass the normal URL safety and resource checks.

Create a user through the management API:

curl -X POST http://127.0.0.1:8080/api/v1/users \
  -H 'Authorization: Bearer devsecret' \
  -H 'Content-Type: application/json' \
  -d '{"username":"patron1","password":"change-me-too","roles":["user"],"status":"active"}' | jq

List users or revoke a user's sessions:

curl http://127.0.0.1:8080/api/v1/users \
  -H 'Authorization: Bearer devsecret' | jq

curl -X POST http://127.0.0.1:8080/api/v1/users/user_abc123/revoke-sessions \
  -H 'Authorization: Bearer devsecret' | jq

SAML SP Scaffolding

Odo is designed to act as a SAML Service Provider for campus/Shibboleth-style identity infrastructure. Full SAML login initiation and assertion validation are future work, but the MVP includes SAML provider configuration APIs, admin UI controls, and a Service Provider metadata endpoint.

Manage provider config through:

curl http://127.0.0.1:8080/api/v1/auth/saml/providers \
  -H 'Authorization: Bearer devsecret' | jq

The public SP metadata endpoint is:

http://127.0.0.1:8080/auth/saml/metadata

Placeholder routes are also present for future integration:

  • GET /auth/saml/login
  • POST /auth/saml/acs

A sample provider config lives at config/auth/saml/campus-shibboleth.json. The current scaffold omits signing certificates and does not validate SAML assertions yet.

Admin Troubleshooting Tools

The Resources tab includes an integrated Proxy Test panel that can test rule matching, open a target through /odo, or fetch a bounded body preview through the protected /api/v1/proxy/test-fetch endpoint. Selecting a resource prefills the test URL from its first entry URL.

Recent access logs are privacy-filtered and available through the admin UI. Proxy diagnostics are also exposed from the UI for checking blocked hosts, rewrite counts, and upstream status as those diagnostics grow. These tools are intended to make the access layer easier to understand and troubleshoot without exposing full target URLs, cookies, or authorization headers.

Missed rewrites and referer recovery

Modern sites may generate paths dynamically in JavaScript. These can appear as local paths outside /odo, such as /assets/app.js or /mfe-copper-roof/.../remoteEntry.js, when they should have been proxied.

By default, Odo can infer the upstream host from a proxied Referer and recover the request through the normal proxy path. Recovery is still subject to URL safety checks, DNS/IP safety validation, resource allowlists, and domain rule actions.

Missed asset, script, CSS, image, and API-like URLs may be silently recovered from the proxied Referer. Missed top-level document navigations, such as a clicked link that lands on /action/doAdvancedSearch, redirect to the canonical /odo/https/{host}/{path} URL instead. This keeps the browser address bar and future relative URL resolution inside the proxy. Query strings are preserved in the redirect, but privacy logs and missed-rewrite diagnostics avoid recording full query strings.

Disable referer recovery with:

APP_PROXY_REFERER_RECOVERY=false

Use browser DevTools plus Load Missed Rewrites in the admin UI to inspect recovered, redirected, denied, and unrecovered missed rewrite events.

Modern app-shell pages

Section, search, and landing pages may depend on JavaScript chunks, route manifests, JSON data routes, and API calls before the full header or navigation appears. Article pages may work earlier because they are often more server-rendered.

Odo classifies and recovers common app/data paths from a proxied Referer, including _next data routes, manifests, mfe- module chunks, remoteEntry.js, /static/, /assets/, /api/, and /graphql requests. These requests are silently proxied when they are safe and proxyable; document navigations still redirect to canonical /odo/https/{host}/{path} URLs. Use browser DevTools Network together with Load Missed Rewrites and Load Proxy Diagnostics to inspect section-page failures.

Access Logging

Access logs default to privacy-filtered output on stdout. Privacy mode logs request metadata and safe proxy decisions without full query strings, target URLs, article URLs, search terms, or reading-history-like paths.

Available formats:

APP_ACCESS_LOG_FORMAT=privacy
APP_ACCESS_LOG_FORMAT=common
APP_ACCESS_LOG_FORMAT=combined
APP_ACCESS_LOG_FORMAT=json

Write access logs to a file by setting:

APP_ACCESS_LOG_PATH=/path/to/access.log

Example:

APP_ACCESS_LOG_FORMAT=json go run ./cmd/odo

Proxy Safety

Odo is default-deny for proxy/access decisions. Proxy targets must be HTTPS URLs that match configured resource domains. Raw IP hosts, localhost, private networks, link-local addresses, non-global addresses, suspicious internal hostnames such as .local and .internal, URL userinfo, fragments, wildcards, and non-default ports are blocked before the proxy fetch is allowed.

/odo/https/{host}/{path} is the preferred local/MVP public proxy route. /odo?url=https://... is still accepted for compatibility and manual testing. Clicked links, asset references, GET form actions, and validated upstream redirects are generated through one proxy URL builder and rewritten back through /odo so the patron's browser continues to talk to Odo and vendors continue to see Odo's outbound IP rather than the patron's IP.

Path mode is the default:

APP_PROXY_URL_MODE=path

Query compatibility mode can be selected with:

APP_PROXY_URL_MODE=query

Dual mode builds path-mode URLs while continuing to accept older query-mode URLs:

APP_PROXY_URL_MODE=dual

Proxy URL modes

Path mode is the default and recommended MVP mode. Query mode is available for compatibility and development. Virtual-host mode is planned for better compatibility with difficult sites that need vendor-like browser origins, but it is not implemented yet. Do not deploy virtual-host mode until wildcard DNS and wildcard TLS are configured and Odo has host-based proxy routing support.

See Future Virtual-Host Proxying for the design notes and migration plan.

Unknown local paths now return 404 instead of redirecting to /admin unless referer-based recovery applies, which makes missed rewrites easier to spot during testing. Missed document navigations redirect to canonical /odo/https/{host}/{path} URLs, while missed assets can be silently proxied. Virtual-host mode may be added later for institution-specific host-based access URLs such as www-economist-com.access.library.edu.

/odo performs a minimal safe outbound GET/HEAD/POST proxy. HTML href, src, action, and common asset attributes are rewritten when they point to safe, allowlisted proxy targets. srcset is partially supported. CSS url(...) references are partially rewritten for text/css responses and inline style attributes.

Odo keeps a server-side per-session cookie jar for proxied browsing. The browser receives only an odo_proxy_sid cookie; upstream/vendor cookies are stored server-side and are not exposed directly to the browser. This improves continuity across proxied requests and POST form submissions, but it is not user authentication. In HA deployments, the in-memory session store would need Redis or another shared session store.

Local browser login is separate from vendor cookie handling. Once local users exist, /odo requires a valid odo_session by default so patrons use Odo as the access path instead of reaching vendors directly. Narrow anonymous URL rules can still allow explicitly configured public proxy URLs.

POST form submissions are forwarded upstream when the target is safe and proxyable. Request bodies are size-limited by APP_PROXY_MAX_BODY_BYTES, and request bodies/form values are not logged. Full JavaScript rewriting, WebSockets, and full SPA compatibility are future work. Only a small set of safe request and response headers are copied. Redirects are validated before returning a local proxied redirect, which defaults to /odo/https/{host}/{path}. Content-Security-Policy is not copied yet, and integrity attributes are removed when URLs are rewritten, because upstream CSP and SRI often reject proxied/transformed assets before fuller policy rewriting exists.

Odo injects a small JavaScript shim into proxied HTML pages by default. The shim rewrites same-origin fetch() and XMLHttpRequest calls back through /odo, which helps modern sites that render headers, search boxes, menus, and consent UI through JavaScript. This is not full JavaScript rewriting, and server-side URL validation remains authoritative. Disable it with:

APP_PROXY_INJECT_JS_SHIM=false

Local load testing

See Local Load Testing for the fake vendor server, sample resource config, k6 smoke/idle/active/spike/soak scripts, runtime metrics endpoint, and SQLite/session notes. The load-test setup is local-only and must not be used against real vendor sites.

Domain Rules

Resource domain rules support roles and actions. Existing configs without action still work: blocked defaults to block, external defaults to allow, and all other roles default to proxy.

Common combinations:

  • content / proxy
  • asset / proxy
  • api / proxy
  • auth / proxy
  • redirect / allow
  • external / allow
  • blocked / block

Broad subdomain proxy rules are useful for library resources, but explicit block rules should be added for analytics, tracking, ads, or unrelated third-party domains discovered during diagnostics. More specific exact rules take precedence over broader subdomain rules, and explicit blocks win when specificity is equal or greater.

Podman

Build:

podman build -t odo:dev .

Run with mounted data and config directories. The :Z suffix lets Podman relabel the mounts for SELinux on Fedora:

mkdir -p data config/resources
podman run --rm -p 8080:8080 \
  -e APP_ADMIN_API_KEY=dev-secret \
  -v "$PWD/data:/data:Z" \
  -v "$PWD/config:/config:Z" \
  odo:dev

Next Steps

  • Hardened API-key storage and rotation.
  • SAML SP support as a first-class module.
  • Signed proxy links.
  • Deeper JavaScript-aware proxy rewriting.
  • HA with PostgreSQL and Redis.

About

foss library access middleware project

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages