Skip to content

Commit

Permalink
TeaVM Impl (#262)
Browse files Browse the repository at this point in the history
* TeaVM related changes. Allows salvation to be compiled to JS instead of having to run a server

* Add main class needed for TeaVM

* fix indentation

* expose functions on window so they're individually callable

* Can't use named capturing groups. Seems like it doesn't work when it's transpiled

* output a string for the JS to consume

* use integers for matching since teavm doesn't support strings

* checkstyle fixes

* update readme and specify a filename for the js output

* remove comments

* abstract regex out to its own variable

* function renaming

* add js test and ci job

* don't run the build for java8

* applying comment suggestions

* fix ci command

* rearrange ci job so java build doesn't need to happen twice
  • Loading branch information
rosstroha authored Jul 10, 2024
1 parent c2ba876 commit 2c4d7c9
Show file tree
Hide file tree
Showing 12 changed files with 164 additions and 23 deletions.
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>
<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');
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');
});

0 comments on commit 2c4d7c9

Please sign in to comment.