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

Merged
merged 17 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from 11 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
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.Salvation2</mainClass>
<minifying>true</minifying>
bakkot marked this conversation as resolved.
Show resolved Hide resolved
<sourceMapsGenerated>false</sourceMapsGenerated>
<sourceFilesCopied>false</sourceFilesCopied>
<targetFileName>salvation-v${project.version}.min.js</targetFileName>
rosstroha marked this conversation as resolved.
Show resolved Hide resolved
</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/Salvation2.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 Salvation2 {
rosstroha marked this conversation as resolved.
Show resolved Hide resolved
public static void main(String[] args) {
initParseList();
initParseSingle();
}

public static String parseSerializedCSPList(String policyText) {
rosstroha marked this conversation as resolved.
Show resolved Hide resolved
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 parseSerializedCSP(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 =
"(exports || window).parseSerializedCSPList = (policyText) => {\n" +
"return javaMethods.get('com.shapesecurity.salvation2.Salvation2.parseSerializedCSPList(Ljava/lang/String;)Ljava/lang/String;').invoke(policyText)\n" +
"}")
static native void initParseList();

@JSBody(params = {}, script =
"(exports || window).parseSerializedCSP = (policyText) => {\n" +
"return javaMethods.get('com.shapesecurity.salvation2.Salvation2.parseSerializedCSP(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