Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TeaVM Impl #262

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,22 +35,24 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
java: [8, 11]
java: [11]

steps:
- name: Checkout
uses: actions/checkout@v2

- uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup Java
uses: actions/setup-java@v2
with:
distribution: 'adopt'
java-version: ${{ matrix.java }}
cache: 'maven'

- name: CI
run: |
java -Xmx32m -version
javac -J-Xmx32m -version
mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V
mvn test -B
- run: node --test
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,17 @@ System.out.println(policy.toString());
```java
policy.toString();
```

## Transpiling to JavaScript
To reduce the overhead of running this library, it will now automatically be transpiled to JS as part of the compile goal by using [TeaVM](https://teavm.org/). It can then be placed on any webpage to be used as static JavaScript, thus alleviating the need for a JRE.

The transpiled code will be placed in `target/javascript` as `salvation-v${project.version}.min.js`.

If you experience errors relating to TeaVM transpiling, check the [supported TeaVM classes](https://teavm.org/jcl-report/recent/jcl.html).

### Using the JavaScript

First run `mvn clean install` to build the JS file. Then include `salvation-vX.X.X.min.js` in your webpage.
To use the parsing functions in on the webpage run `window.main()` to initialize them. From then on, `window.parseSerializedCSPList()` and `window.parseSerializedCSP()` will be available.

`parseSerializedCSP()` and `parseSerializedCSPList()` will return strings containing the parsing results. If there are multiple results, they will be separated by a newline. This is simply because TeaVM requires a lot of extra work for it to be able to return JS objects.
37 changes: 37 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@
</distributionManagement>

<dependencies>
<dependency>
<groupId>org.teavm</groupId>
<artifactId>teavm-jso</artifactId>
<version>0.9.2</version>
</dependency>
<dependency>
<groupId>org.teavm</groupId>
<artifactId>teavm-jso-apis</artifactId>
<version>0.9.2</version>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
Expand All @@ -81,6 +91,33 @@

<build>
<plugins>
<plugin>
<groupId>org.teavm</groupId>
<artifactId>teavm-maven-plugin</artifactId>
<version>0.9.2</version>
<dependencies>
<dependency>
<groupId>org.teavm</groupId>
<artifactId>teavm-classlib</artifactId>
<version>0.9.2</version>
</dependency>
</dependencies>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
<phase>process-classes</phase>
<configuration>
<mainClass>com.shapesecurity.salvation2.JSInterface</mainClass>
<minifying>true</minifying>
bakkot marked this conversation as resolved.
Show resolved Hide resolved
<sourceMapsGenerated>false</sourceMapsGenerated>
<sourceFilesCopied>false</sourceFilesCopied>
<targetFileName>salvation.min.js</targetFileName>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
Expand Down
8 changes: 3 additions & 5 deletions src/main/java/com/shapesecurity/salvation2/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
public class Constants {
// https://tools.ietf.org/html/rfc3986#section-3.1
public static final String schemePart = "[a-zA-Z][a-zA-Z0-9+\\-.]*";
public static final Pattern schemePattern = Pattern.compile("^(?<scheme>" + Constants.schemePart + ":)");
public static final Pattern schemePattern = Pattern.compile("^(" + Constants.schemePart + ":)");

// https://tools.ietf.org/html/rfc7230#section-3.2.6
public static final Pattern rfc7230TokenPattern = Pattern.compile("^[!#$%&'*+\\-.^_`|~0-9a-zA-Z]+$");

// RFC 2045 appendix A: productions of type and subtype
// https://tools.ietf.org/html/rfc2045#section-5.1
public static final Pattern mediaTypePattern = Pattern.compile("^(?<type>[a-zA-Z0-9!#$%^&*\\-_+{}|'.`~]+)/(?<subtype>[a-zA-Z0-9!#$%^&*\\-_+{}|'.`~]+)$");
public static final Pattern mediaTypePattern = Pattern.compile("^([a-zA-Z0-9!#$%^&*\\-_+{}|'.`~]+)/([a-zA-Z0-9!#$%^&*\\-_+{}|'.`~]+)$");
public static final Pattern unquotedKeywordPattern = Pattern.compile("^(?:self|unsafe-inline|unsafe-eval|unsafe-redirect|none|strict-dynamic|unsafe-hashes|report-sample|unsafe-allow-redirects)$");

// port-part constants
Expand All @@ -37,10 +37,8 @@ public class Constants {
private static final String queryFragmentPart = "(?:\\?[^#]*)?(?:#.*)?";

public static final Pattern hostSourcePattern = Pattern.compile(
"^(?<scheme>" + schemePart + "://)?(?<host>" + hostPart + ")(?<port>" + portPart + ")?(?<path>" + pathPart
"^(" + schemePart + "://)?(" + hostPart + ")(" + portPart + ")?(" + pathPart
+ ")?" + queryFragmentPart + "$");
// public static final Pattern relativeReportUriPattern =
// Pattern.compile("^(?<path>" + pathPart + ")" + queryFragmentPart + "$");
public static final Pattern IPv4address = Pattern.compile("^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$");
public static final Pattern IPV6loopback = Pattern.compile("^[0:]+:1$");
public static final String IPv6address = "(?:(?:(?:[0-9A-Fa-f]{1,4}:){6}|::(?:[0-9A-Fa-f]{1,4}:){5}|(?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,1}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}|(?:(?:[0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}|(?:(?:[0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:|(?:(?:[0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})?::)(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))|(?:(?:[0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})?::)";
Expand Down
6 changes: 4 additions & 2 deletions src/main/java/com/shapesecurity/salvation2/Directive.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@


public class Directive {
public static Predicate<String> IS_DIRECTIVE_NAME = Pattern.compile("^[A-Za-z0-9\\-]+$").asPredicate();
public static Predicate<String> containsNonDirectiveCharacter = Pattern.compile("[" + Constants.WHITESPACE_CHARS + ",;]").asPredicate();
private static final Pattern DIRECTIVE_NAME_PATTERN = Pattern.compile("^[A-Za-z0-9\\-]+$");
public static Predicate<String> IS_DIRECTIVE_NAME = s -> DIRECTIVE_NAME_PATTERN.matcher(s).matches();
private static final Pattern NON_DIRECTIVE_CHAR_PATTERN = Pattern.compile("[" + Constants.WHITESPACE_CHARS + ",;]");
public static Predicate<String> containsNonDirectiveCharacter = s -> NON_DIRECTIVE_CHAR_PATTERN.matcher(s).matches();
protected List<String> values;

protected static DirectiveErrorConsumer wrapManipulationErrorConsumer(ManipulationErrorConsumer errors) {
Expand Down
52 changes: 52 additions & 0 deletions src/main/java/com/shapesecurity/salvation2/JSInterface.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.shapesecurity.salvation2;

import org.teavm.jso.JSBody;

public class JSInterface {
public static void main(String[] args) {
initParseList();
initParseSingle();
}

public static String getErrorsForSerializedCSPList(String policyText) {
StringBuilder errorMessages = new StringBuilder();
Policy.parseSerializedCSPList(policyText, (severity, message, policyIndex, directiveIndex, valueIndex) -> {
errorMessages.append(severity.name())
.append(" at directive ")
.append(directiveIndex)
.append(valueIndex == -1 ? "" : " at value " + valueIndex)
.append(": ")
.append(message)
.append("\n");
});
return errorMessages.toString().trim();
}

public static String getErrorsForSerializedCSP(String policyText) {
StringBuilder errorMessages = new StringBuilder();

Policy.parseSerializedCSP(policyText, (severity, message, directiveIndex, valueIndex) -> {
errorMessages.append(severity.name())
.append(" at directive ")
.append(directiveIndex)
.append(valueIndex == -1 ? "" : " at value " + valueIndex)
.append(": ")
.append(message)
.append("\n");
});
return errorMessages.toString().trim();
}

@JSBody(params = {}, script =
"(window || globalThis).getErrorsForSerializedCSPList = (policyText) => {\n" +
"return javaMethods.get('com.shapesecurity.salvation2.JSInterface.getErrorsForSerializedCSPList(Ljava/lang/String;)Ljava/lang/String;').invoke(policyText)\n" +
"}")
static native void initParseList();

@JSBody(params = {}, script =
"(window || globalThis).getErrorsForSerializedCSP = (policyText) => {\n" +
"return javaMethods.get('com.shapesecurity.salvation2.JSInterface.getErrorsForSerializedCSP(Ljava/lang/String;)Ljava/lang/String;').invoke(policyText)\n" +
"}")
static native void initParseSingle();
}

2 changes: 1 addition & 1 deletion src/main/java/com/shapesecurity/salvation2/URLs/GUID.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public static Optional<GUID> parseGUID(String value) {
if (!matcher.find()) {
return Optional.empty();
}
String scheme = matcher.group("scheme");
String scheme = matcher.group(1);
scheme = scheme.substring(0, scheme.length() - 1); // + 1 for the trailing ":"
return Optional.of(new GUID(scheme, value.substring(scheme.length() + 1)));
}
Expand Down
8 changes: 4 additions & 4 deletions src/main/java/com/shapesecurity/salvation2/URLs/URI.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,20 @@ public static Optional<URI> parseURI(@Nonnull String uri) {
if (!matcher.find()) {
return Optional.empty();
}
String scheme = matcher.group("scheme");
String scheme = matcher.group(1);
if (scheme == null) {
return Optional.empty();
}
scheme = scheme.substring(0, scheme.length() - 3);
String portString = matcher.group("port");
String portString = matcher.group(3);
int port;
if (portString == null) {
port = URI.defaultPortForProtocol(scheme.toLowerCase(Locale.ENGLISH));
} else {
port = portString.equals(":*") ? Constants.WILDCARD_PORT : Integer.parseInt(portString.substring(1));
}
String host = matcher.group("host");
String path = matcher.group("path");
String host = matcher.group(2);
String path = matcher.group(4);
if (path == null) {
path = "";
}
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/shapesecurity/salvation2/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
import java.util.regex.Pattern;

public class Utils {
public static final Predicate<String> IS_BASE64_VALUE = Pattern.compile("[a-zA-Z0-9+/\\-_]+=?=?").asPredicate();

private static final Pattern BASE64_PATTERN = Pattern.compile("[a-zA-Z0-9+/\\-_]+=?=?");
public static final Predicate<String> IS_BASE64_VALUE = s -> BASE64_PATTERN.matcher(s).matches();
// https://infra.spec.whatwg.org/#split-on-ascii-whitespace
static List<String> splitOnAsciiWhitespace(String input) {
ArrayList<String> out = new ArrayList<>();
Expand Down
8 changes: 4 additions & 4 deletions src/main/java/com/shapesecurity/salvation2/Values/Host.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,20 @@ private Host(String scheme, String host, int port, String path) {
public static Optional<Host> parseHost(String value) {
Matcher matcher = Constants.hostSourcePattern.matcher(value);
if (matcher.find()) {
String scheme = matcher.group("scheme");
String scheme = matcher.group(1);
if (scheme != null) {
scheme = scheme.substring(0, scheme.length() - 3).toLowerCase(Locale.ENGLISH);
}
String portString = matcher.group("port");
String portString = matcher.group(3);
int port;
if (portString == null) {
port = Constants.EMPTY_PORT;
} else {
port = portString.equals(":*") ? Constants.WILDCARD_PORT : Integer.parseInt(portString.substring(1));
}
// Hosts are only consumed lowercase: https://w3c.github.io/webappsec-csp/#host-part-match
String host = matcher.group("host").toLowerCase(Locale.ENGLISH); // There is no possible NPE here; host is not optional
String path = matcher.group("path");
String host = matcher.group(2).toLowerCase(Locale.ENGLISH); // There is no possible NPE here; host is not optional
String path = matcher.group(4);

// TODO contemplate warning for paths which contain `//`, `/../`, or `/./`, since those will never match an actual request
// TODO contemplate warning for ports which are implied by their scheme
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ public static Optional<MediaType> parseMediaType(String value) {
if (matcher.find()) {
// plugin type matching is ASCII case-insensitive
// https://w3c.github.io/webappsec-csp/#plugin-types-post-request-check
String type = matcher.group("type").toLowerCase(Locale.ENGLISH);
String subtype = matcher.group("subtype").toLowerCase(Locale.ENGLISH);
String type = matcher.group(1).toLowerCase(Locale.ENGLISH);
String subtype = matcher.group(2).toLowerCase(Locale.ENGLISH);
return Optional.of(new MediaType(type, subtype));
}
return Optional.empty();
Expand Down
36 changes: 36 additions & 0 deletions src/test/javascript/salvation.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use strict';
const test = require('node:test');
rosstroha marked this conversation as resolved.
Show resolved Hide resolved
const assert = require('node:assert');
const salvation = require('../../../target/javascript/salvation.min.js');

test('salvation initialization', () => {
assert.notStrictEqual(salvation.main, undefined);
salvation.main();
assert.notStrictEqual(getErrorsForSerializedCSP, undefined);
assert.notStrictEqual(getErrorsForSerializedCSPList, undefined);
});

test('.getErrorsForSerializedCSP() gives no errors for a valid CSP', () => {
salvation.main();
const result = getErrorsForSerializedCSP('default-src \'none\';');
assert.strictEqual(result.length, 0, 'No errors should be found');
});

test('.getErrorsForSerializedCSP() provides feedback', () => {
salvation.main();
const result = getErrorsForSerializedCSP('hello world');
assert.strictEqual(result, 'Warning at directive 0: Unrecognized directive hello');
});

test('.getErrorsForSerializedCSPList() gives no errors for a valid CSP', () => {
salvation.main();
const result = getErrorsForSerializedCSPList('default-src \'none\',plugin-types image/png application/pdf; sandbox,style-src https: \'self\'');
assert.strictEqual(result, '');
});

test('.getErrorsForSerializedCSPList() provides feedback', () => {
salvation.main();
const result = getErrorsForSerializedCSPList('hello,foobar,script-src \'self\'; style-src \'self\'');
assert.strictEqual(result, 'Warning at directive 0: Unrecognized directive hello\n'
+ 'Warning at directive 0: Unrecognized directive foobar');
});
Loading