Permissions-Policy Builder — Java source
Build a Permissions-Policy header interactively. Control which browser features (camera, microphone, geolocation, etc.) your site can use.
This is the Java implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Permissions-Policy Header Builder — build / parse / validate / score. Pure logic.
//
// Language: Java (17+, standard library only)
// Ported from src/lib/permissions-policy.ts
// display source — part of CosmoDev's polyglot tool pages.
//
// The Permissions-Policy header is a comma-separated list of directives:
// Permissions-Policy: geolocation=(self), camera=(), microphone=*, usb=(https://a.example)
// Each directive maps a browser feature to an allowlist. An empty allowlist
// `()` disables the feature outright; `*` allows it everywhere; `self` limits
// it to the page's own origin; anything else is a space-separated origin list.
//
// A policy is modeled here as a sorted map of feature -> allowlist tokens:
// { geolocation=[self], camera=[], usb=[https://a.example] }
// - [] => camera=() (disabled)
// - [*] => microphone=* (every origin)
// - [self] => geolocation=(self)
// - [origins...] => usb=(https://a.example https://b.example)
// Features absent from the map are absent from the header (browser default).
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Pattern;
public final class PermissionsPolicy {
/** How much a feature's abuse could leak about the user. */
public enum PrivacyImpact { HIGH, MEDIUM, LOW }
/** One catalogued browser feature. */
public record FeatureInfo(
String name,
String description,
PrivacyImpact privacyImpact,
String defaultBrowserBehavior) {
}
/** feature name -> allowlist tokens. Empty list = disabled, ["*"] = all origins. */
public static final class FeatureMap extends TreeMap<String, List<String>> {
@java.io.Serial
private static final long serialVersionUID = 1L;
}
/** The feature catalogue, in display order (privacy impact high -> low). */
public static final List<FeatureInfo> FEATURES = List.of(
// --- high privacy impact ---
new FeatureInfo("camera", "Access the device camera for photos / video calls.", PrivacyImpact.HIGH, "Same-origin only; prompts the user."),
new FeatureInfo("microphone", "Capture audio from the device microphone.", PrivacyImpact.HIGH, "Same-origin only; prompts the user."),
new FeatureInfo("geolocation", "Read the precise GPS location of the visitor.", PrivacyImpact.HIGH, "Same-origin only; prompts the user."),
new FeatureInfo("display-capture", "Screen / window sharing via getDisplayMedia.", PrivacyImpact.HIGH, "Same-origin only; prompts the user."),
new FeatureInfo("idle-detection", "Detects when the user is away from the device — reveals usage patterns.", PrivacyImpact.HIGH, "Disabled; prompts the user."),
new FeatureInfo("serial", "Talk to serial devices (Arduinos, POS terminals) over a physical port.", PrivacyImpact.HIGH, "Disabled; prompts the user."),
new FeatureInfo("usb", "WebUSB — direct access to connected USB devices.", PrivacyImpact.HIGH, "Disabled; prompts the user."),
new FeatureInfo("hid", "Human Interface Devices — raw access to unusual keyboards, gamepads, sensors.", PrivacyImpact.HIGH, "Disabled; prompts the user."),
new FeatureInfo("xr-spatial-tracking", "Tracks head / hand position in WebXR sessions.", PrivacyImpact.HIGH, "Same-origin only; prompts the user."),
// --- medium privacy impact ---
new FeatureInfo("accelerometer", "Device motion sensor — can fingerprint and infer behaviour.", PrivacyImpact.MEDIUM, "Same-origin only."),
new FeatureInfo("ambient-light-sensor", "Reads ambient light level around the device.", PrivacyImpact.MEDIUM, "Same-origin only."),
new FeatureInfo("battery", "Battery Status API — a classic fingerprinting vector.", PrivacyImpact.MEDIUM, "Same-origin only."),
new FeatureInfo("gyroscope", "Device orientation sensor — fingerprinting and behaviour inference.", PrivacyImpact.MEDIUM, "Same-origin only."),
new FeatureInfo("magnetometer", "Compass readings — can leak details of the user's surroundings.", PrivacyImpact.MEDIUM, "Same-origin only."),
new FeatureInfo("keyboard-map", "Reads the physical keyboard layout — a small but real fingerprint.", PrivacyImpact.MEDIUM, "Same-origin only."),
new FeatureInfo("gamepad", "Enumerates connected controllers and their button state.", PrivacyImpact.MEDIUM, "Same-origin only."),
new FeatureInfo("midi", "Web MIDI — access to attached music hardware.", PrivacyImpact.MEDIUM, "Same-origin only; prompts the user."),
new FeatureInfo("payment", "Payment Request API — can expose stored payment handles.", PrivacyImpact.MEDIUM, "Same-origin only."),
new FeatureInfo("publickey-credentials-get", "WebAuthn credential requests.", PrivacyImpact.MEDIUM, "Same-origin only."),
new FeatureInfo("screen-wake-lock", "Keeps the screen awake — drains battery and signals intent.", PrivacyImpact.MEDIUM, "Same-origin only."),
new FeatureInfo("speaker-selection", "Enumerates and switches audio output devices.", PrivacyImpact.MEDIUM, "Same-origin only."),
new FeatureInfo("web-share", "Invokes the OS share sheet with chosen content.", PrivacyImpact.MEDIUM, "Same-origin only."),
new FeatureInfo("encrypted-media", "DRM (EME) — playback identity can be correlated.", PrivacyImpact.MEDIUM, "Same-origin only."),
new FeatureInfo("document-domain", "Let frames relax the same-origin policy via document.domain.", PrivacyImpact.MEDIUM, "Allowed in same-origin pages; deprecated."),
// --- low privacy impact ---
new FeatureInfo("autoplay", "Autoplay media with/without sound — not a data leak, an annoyance knob.", PrivacyImpact.LOW, "Muted autoplay allowed; audible blocked."),
new FeatureInfo("cross-origin-isolated", "COOP/COEP isolation for SharedArrayBuffer — hardens the page.", PrivacyImpact.LOW, "Not isolated."),
new FeatureInfo("fullscreen", "Element.requestFullscreen().", PrivacyImpact.LOW, "Same-origin only."),
new FeatureInfo("navigation-override", "Lets a frame intercept its own top-level navigations.", PrivacyImpact.LOW, "Disabled."),
new FeatureInfo("picture-in-picture", "Floating always-on-top video window.", PrivacyImpact.LOW, "Same-origin only."));
private static final Pattern FEATURE_NAME_RE = Pattern.compile("[a-z][a-z0-9-]*");
private static final Pattern ORIGIN_RE =
Pattern.compile("(https?://|https?:)[\\w.-]+(:\\d+)?", Pattern.CASE_INSENSITIVE);
private static final Pattern HEADER_PREFIX_RE =
Pattern.compile("(?i)permissions-policy\\s*:\\s*");
private PermissionsPolicy() {
}
private static String stripQuotes(String token) {
return token.replaceFirst("^\"(.*)\"$", "$1");
}
private static String formatAllowlist(List<String> tokens) {
if (tokens.size() == 1 && tokens.get(0).equals("*")) return "*";
return "(" + String.join(" ", tokens) + ")";
}
/** Assemble a Permissions-Policy header value from a feature map. */
public static String buildPermissionsPolicy(FeatureMap features) {
List<String> directives = new ArrayList<>(); // FeatureMap is a TreeMap: keys iterate sorted
for (Map.Entry<String, List<String>> e : features.entrySet()) {
directives.add(e.getKey() + "=" + formatAllowlist(e.getValue()));
}
return String.join(", ", directives);
}
/**
* Parse a Permissions-Policy header value back into a feature map.
* Accepts an optional `Permissions-Policy:` prefix. Returns null when the
* syntax is malformed (bad directive, missing allowlist, bad token).
*/
public static FeatureMap parsePermissionsPolicy(String header) {
String cleaned = HEADER_PREFIX_RE.matcher(header.trim()).replaceFirst("");
if (cleaned.isEmpty()) return null;
FeatureMap map = new FeatureMap();
for (String rawDirective : cleaned.split(",")) {
String directive = rawDirective.trim();
if (directive.isEmpty()) return null;
int eq = directive.indexOf('=');
if (eq <= 0) return null;
String name = directive.substring(0, eq).trim();
String value = directive.substring(eq + 1).trim();
if (!FEATURE_NAME_RE.matcher(name).matches()) return null;
if (value.equals("*") || value.equals("self")) {
map.put(name, List.of(value));
continue;
}
if (!value.startsWith("(") || !value.endsWith(")")) return null;
String inner = value.substring(1, value.length() - 1).trim();
if (inner.isEmpty()) {
map.put(name, List.of());
continue;
}
List<String> tokens = new ArrayList<>();
for (String t : inner.split("\\s+")) tokens.add(stripQuotes(t));
for (String token : tokens) {
if (!token.equals("self") && !token.equals("*") && !ORIGIN_RE.matcher(token).matches()) {
return null;
}
}
map.put(name, tokens);
}
return map;
}
/** One directive-level problem found by validatePermissionsPolicy. */
public record Validation(boolean valid, List<String> errors) {
}
/** Syntax-check a header the same way parsePermissionsPolicy does, with messages. */
public static Validation validatePermissionsPolicy(String header) {
List<String> errors = new ArrayList<>();
String cleaned = HEADER_PREFIX_RE.matcher(header.trim()).replaceFirst("");
if (cleaned.isEmpty()) return new Validation(false, List.of("Header is empty."));
String[] directives = cleaned.split(",");
for (int i = 0; i < directives.length; i++) {
String directive = directives[i].trim();
String where = "Directive " + (i + 1);
if (directive.isEmpty()) {
errors.add(where + ": empty (stray comma?).");
continue;
}
int eq = directive.indexOf('=');
if (eq <= 0) {
errors.add(where + ": expected `feature=allowlist`, got `" + directive + "`.");
continue;
}
String name = directive.substring(0, eq).trim();
String value = directive.substring(eq + 1).trim();
if (!FEATURE_NAME_RE.matcher(name).matches()) {
errors.add(where + ": `" + name + "` is not a valid feature name.");
continue;
}
if (value.equals("*") || value.equals("self")) continue;
if (!value.startsWith("(") || !value.endsWith(")")) {
errors.add(where + ": `" + name + "` allowlist must be `*`, `self`, or `(...)` — got `" + value + "`.");
continue;
}
String inner = value.substring(1, value.length() - 1).trim();
if (inner.isEmpty()) continue; // () = disabled, valid
for (String raw : inner.split("\\s+")) {
String token = stripQuotes(raw);
if (!token.equals("self") && !token.equals("*") && !ORIGIN_RE.matcher(token).matches()) {
errors.add(where + ": `" + name + "` has an invalid allowlist token `" + raw + "`.");
}
}
}
return new Validation(errors.isEmpty(), errors);
}
/**
* Privacy score 0-100 for a policy: how much it locks down the catalog.
* Each feature is weighted by privacy impact (high 3, medium 2, low 1).
* Disabled `()` earns full credit, `self` or an origin list half, `*` or
* "absent from the policy" none (the browser default stays in force).
*/
public static int privacyScore(FeatureMap features) {
double earned = 0;
double possible = 0;
for (FeatureInfo feature : FEATURES) {
int w = privacyWeight(feature.privacyImpact());
possible += w;
List<String> allowlist = features.get(feature.name());
if (allowlist != null && allowlist.isEmpty()) earned += w; // () disabled
else if (allowlist != null && !(allowlist.size() == 1 && allowlist.get(0).equals("*"))) {
earned += w / 2.0; // self / origins
}
}
return (int) Math.round(earned / possible * 100);
}
/** Privacy-impact weight: high 3, medium 2, low 1. */
private static int privacyWeight(PrivacyImpact impact) {
return switch (impact) {
case HIGH -> 3;
case MEDIUM -> 2;
case LOW -> 1;
};
}
}
Also available in 8 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →