/*
* Copyright 2020 Red Hat, Inc.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.vertx.ext.web.impl;
An origin follows rfc6454#section-7 and is expected to have the format: <scheme> "://" <hostname> [ ":" <port> ]
This class allows parsing of web urls and match against http headers that require such
validation.
Author: Paulo Lopes
/**
* An origin follows rfc6454#section-7
* and is expected to have the format: {@code <scheme> "://" <hostname> [ ":" <port> ]}
* <p>
* This class allows parsing of web urls and match against http headers that require such
* validation.
*
* @author Paulo Lopes
*/
public final class Origin {
private static final String DEFAULT_FTP_PORT = "21";
private static final String DEFAULT_HTTP_PORT = "80";
private static final String DEFAULT_HTTPS_PORT = "443";
private final String protocol;
private final String host;
private final int port;
private final String resource;
// internal
private final String base;
private final String BASE;
private final String optional;
private Origin(String protocol, String host, String port, String resource) {
String defaultPort;
switch (protocol.toLowerCase()) {
case "ftp":
this.protocol = protocol;
defaultPort = DEFAULT_FTP_PORT;
break;
case "http":
this.protocol = protocol;
defaultPort = DEFAULT_HTTP_PORT;
break;
case "https":
this.protocol = protocol;
defaultPort = DEFAULT_HTTPS_PORT;
break;
default:
throw new IllegalStateException("Unsupported protocol: " + protocol);
}
if (host == null) {
throw new IllegalStateException("Null host not allowed");
}
// hosts are either domain names, dot separated or ipv6 like
// https://tools.ietf.org/html/rfc1123
boolean ipv6 = false;
for (int i = 0; i < host.length(); i++) {
char c = host.charAt(i);
switch (c) {
case '[':
if (i == 0) {
ipv6 = true;
} else {
throw new IllegalStateException("Illegal character in hostname: " + host);
}
break;
case ']':
if (!ipv6 || i != host.length() - 1) {
throw new IllegalStateException("Illegal character in hostname: " + host);
}
break;
case ':':
if (!ipv6) {
throw new IllegalStateException("Illegal character in hostname: " + host);
}
break;
default:
if (!Character.isLetterOrDigit(c) && c != '.' && c != '-') {
throw new IllegalStateException("Illegal character in hostname: " + host);
}
break;
}
}
this.host = host;
// port should be numeric
if (port != null) {
for (int i = 0; i < port.length(); i++) {
char c = port.charAt(i);
if (!Character.isDigit(c)) {
throw new IllegalStateException("Illegal character in port: " + port);
}
}
this.port = Integer.parseInt(port);
} else {
this.port = Integer.parseInt(defaultPort);
}
this.resource = resource;
if (port == null) {
base = protocol + "://" + host;
optional = ":" + defaultPort;
} else {
base = protocol + "://" + host + ":" + port;
optional = "";
}
BASE = base.toUpperCase();
}
public static Origin parse(String text) {
int sep0 = text.indexOf("://");
if (sep0 > 0) {
// there is a protocol
String protocol = text.substring(0, sep0);
int sep1 = -1;
// if sep0 + 3 == [ assume IPV6 address
if (text.charAt(sep0 + 3) == '[') {
int endHost = text.indexOf(']', sep0 + 3);
if (endHost != -1) {
sep1 = text.indexOf(':', endHost);
}
} else {
sep1 = text.indexOf(':', sep0 + 3);
}
int sep2 = text.indexOf('/', Math.max(sep0 + 3, sep1 + 1));
if (sep1 == -1 && sep2 == -1) {
// there's just a host
return new Origin(protocol, text.substring(sep0 + 3), null, null);
}
if (sep1 != -1 && sep2 == -1) {
// there's a host + port
return new Origin(protocol, text.substring(sep0 + 3, sep1), text.substring(sep1 + 1), null);
}
if (sep1 == -1) {
// there's a host + path
return new Origin(protocol, text.substring(sep0 + 3, sep2), null, text.substring(sep2));
}
// there's a host + port + path
return new Origin(protocol, text.substring(sep0 + 3, sep1), text.substring(sep1 + 1, sep2), text.substring(sep2));
}
// invalid
throw new IllegalStateException("Invalid Origin, expected <protocol>://<domain>[:<port>][</resource>]");
}
Checks if the origin header is valid according to:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin
https://tools.ietf.org/html/rfc6454#section-7
/**
* Checks if the origin header is valid according to:
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin
* https://tools.ietf.org/html/rfc6454#section-7
*/
public static boolean isValid(String text) {
int sep0 = text.indexOf("://");
if (sep0 > 0) {
// there is a protocol
String protocol = text.substring(0, sep0);
switch (protocol.toLowerCase()) {
case "ftp":
case "http":
case "https":
break;
default:
return false;
}
int sep1 = -1;
// if sep0 + 3 == [ assume IPV6 address
if (text.charAt(sep0 + 3) == '[') {
int endHost = text.indexOf(']', sep0 + 3);
if (endHost != -1) {
sep1 = text.indexOf(':', endHost);
}
} else {
sep1 = text.indexOf(':', sep0 + 3);
}
int sep2 = text.indexOf('/', Math.max(sep0 + 3, sep1 + 1));
if (sep1 == -1 && sep2 == -1) {
// there's just a host
return check(text.substring(sep0 + 3), null);
}
if (sep1 != -1 && sep2 == -1) {
// there's a host + port
return check(text.substring(sep0 + 3, sep1), text.substring(sep1 + 1));
}
if (sep1 == -1) {
// there's a host + path
return check(text.substring(sep0 + 3, sep2), null);
}
// there's a host + port + path
return check(text.substring(sep0 + 3, sep1), text.substring(sep1 + 1, sep2));
}
// invalid
return false;
}
private static boolean check(String host, String port) {
if (host == null) {
return false;
}
// hosts are either domain names, dot separated or ipv6 like
// https://tools.ietf.org/html/rfc1123
boolean ipv6 = false;
for (int i = 0; i < host.length(); i++) {
char c = host.charAt(i);
switch (c) {
case '[':
if (i == 0) {
ipv6 = true;
} else {
return false;
}
break;
case ']':
if (!ipv6 || i != host.length() - 1) {
return false;
}
break;
case ':':
if (!ipv6) {
return false;
}
break;
default:
if (!Character.isLetterOrDigit(c) && c != '.' && c != '-') {
return false;
}
break;
}
}
// port should be numeric
if (port != null) {
for (int i = 0; i < port.length(); i++) {
char c = port.charAt(i);
if (!Character.isDigit(c)) {
return false;
}
}
}
return true;
}
public String protocol() {
return protocol;
}
public String host() {
return host;
}
public int port() {
return port;
}
public String resource() {
return resource;
}
public boolean sameOrigin(String other) {
// for each char of other
// if any base chars != other abort
// if more chars
// if current char == : and optional > 0
// if any optionals chars != other abort
// if current char == /
// success
// else
// fail
int offset = 0;
int len = other.length();
if (base.length() > len) {
return false;
}
for (int i = 0; i < base.length(); i++) {
char c = other.charAt(offset + i);
if (c != base.charAt(i) && c != BASE.charAt(i)) {
return false;
}
}
offset += base.length();
len -= base.length();
if (len > 0) {
if (other.charAt(offset) == ':') {
if (optional.length() > len) {
return false;
}
for (int i = 0; i < optional.length(); i++) {
char c = other.charAt(offset + i);
if (c != optional.charAt(i)) {
return false;
}
}
offset += optional.length();
len -= optional.length();
}
if (len > 0) {
return other.charAt(offset) == '/';
}
}
return true;
}
public String encode() {
switch (protocol) {
case "http":
return protocol + "://" + host + (port == 80 ? "" : ":" + port);
case "https":
return protocol + "://" + host + (port == 443 ? "" : ":" + port);
case "ftp":
return protocol + "://" + host + (port == 21 ? "" : ":" + port);
default:
return null;
}
}
@Override
public String toString() {
return base;
}
}