//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.http;
import java.net.URI;
import java.net.URISyntaxException;
import org.eclipse.jetty.util.HostPort;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.UrlEncoded;
Http URI. Both Mutable
and Immutable
implementations are available via the static methods such as build()
and from(String)
. A URI such as http://user@host:port/path;ignored/info;param?query#ignored
is split into the following undecoded elements:
getScheme()
- http:
getAuthority()
- //name@host:port
getHost()
- host
getPort()
- port
getPath()
- /path/info
getParam()
- param
getQuery()
- query
getFragment()
- fragment
Any parameters will be returned from getPath()
, but are excluded from the return value of getDecodedPath()
. If there are multiple parameters, the getParam()
method returns only the last one.
/**
* Http URI.
*
* Both {@link Mutable} and {@link Immutable} implementations are available
* via the static methods such as {@link #build()} and {@link #from(String)}.
*
* A URI such as
* <code>http://user@host:port/path;ignored/info;param?query#ignored</code>
* is split into the following undecoded elements:<ul>
* <li>{@link #getScheme()} - http:</li>
* <li>{@link #getAuthority()} - //name@host:port</li>
* <li>{@link #getHost()} - host</li>
* <li>{@link #getPort()} - port</li>
* <li>{@link #getPath()} - /path/info</li>
* <li>{@link #getParam()} - param</li>
* <li>{@link #getQuery()} - query</li>
* <li>{@link #getFragment()} - fragment</li>
* </ul>
* <p>Any parameters will be returned from {@link #getPath()}, but are excluded from the
* return value of {@link #getDecodedPath()}. If there are multiple parameters, the
* {@link #getParam()} method returns only the last one.
*/
public interface HttpURI
{
static Mutable build()
{
return new Mutable();
}
static Mutable build(HttpURI uri)
{
return new Mutable(uri);
}
static Mutable build(HttpURI uri, String pathQuery)
{
return new Mutable(uri, pathQuery);
}
static Mutable build(HttpURI uri, String path, String param, String query)
{
return new Mutable(uri, path, param, query);
}
static Mutable build(URI uri)
{
return new Mutable(uri);
}
static Mutable build(String uri)
{
return new Mutable(uri);
}
static Immutable from(URI uri)
{
return new HttpURI.Mutable(uri).asImmutable();
}
static Immutable from(String uri)
{
return new HttpURI.Mutable(uri).asImmutable();
}
static Immutable from(String method, String uri)
{
if (HttpMethod.CONNECT.is(method))
return new Immutable(uri);
if (uri.startsWith("/"))
return HttpURI.build().pathQuery(uri).asImmutable();
return HttpURI.from(uri);
}
static Immutable from(String scheme, String host, int port, String pathQuery)
{
return new Mutable(scheme, host, port, pathQuery).asImmutable();
}
Immutable asImmutable();
String asString();
String getAuthority();
String getDecodedPath();
String getFragment();
String getHost();
String getParam();
String getPath();
String getPathQuery();
int getPort();
String getQuery();
String getScheme();
String getUser();
boolean hasAuthority();
boolean isAbsolute();
default URI toURI()
{
try
{
String query = getQuery();
return new URI(getScheme(), null, getHost(), getPort(), getPath(), query == null ? null : UrlEncoded.decodeString(query), null);
}
catch (URISyntaxException x)
{
throw new RuntimeException(x);
}
}
class Immutable implements HttpURI
{
private final String _scheme;
private final String _user;
private final String _host;
private final int _port;
private final String _path;
private final String _param;
private final String _query;
private final String _fragment;
private String _uri;
private String _decodedPath;
private Immutable(Mutable builder)
{
_scheme = builder._scheme;
_user = builder._user;
_host = builder._host;
_port = builder._port;
_path = builder._path;
_param = builder._param;
_query = builder._query;
_fragment = builder._fragment;
_uri = builder._uri;
_decodedPath = builder._decodedPath;
}
private Immutable(String uri)
{
_scheme = null;
_user = null;
_host = null;
_port = -1;
_path = uri;
_param = null;
_query = null;
_fragment = null;
_uri = uri;
_decodedPath = null;
}
@Override
public Immutable asImmutable()
{
return this;
}
@Override
public String asString()
{
if (_uri == null)
{
StringBuilder out = new StringBuilder();
if (_scheme != null)
out.append(_scheme).append(':');
if (_host != null)
{
out.append("//");
if (_user != null)
out.append(_user).append('@');
out.append(_host);
}
if (_port > 0)
out.append(':').append(_port);
if (_path != null)
out.append(_path);
if (_query != null)
out.append('?').append(_query);
if (_fragment != null)
out.append('#').append(_fragment);
if (out.length() > 0)
_uri = out.toString();
else
_uri = "";
}
return _uri;
}
@Override
public boolean equals(Object o)
{
if (o == this)
return true;
if (!(o instanceof HttpURI))
return false;
return asString().equals(((HttpURI)o).asString());
}
@Override
public String getAuthority()
{
if (_port > 0)
return _host + ":" + _port;
return _host;
}
@Override
public String getDecodedPath()
{
if (_decodedPath == null && _path != null)
_decodedPath = URIUtil.canonicalPath(URIUtil.decodePath(_path));
return _decodedPath;
}
@Override
public String getFragment()
{
return _fragment;
}
@Override
public String getHost()
{
// Return null for empty host to retain compatibility with java.net.URI
if (_host != null && _host.isEmpty())
return null;
return _host;
}
@Override
public String getParam()
{
return _param;
}
@Override
public String getPath()
{
return _path;
}
@Override
public String getPathQuery()
{
if (_query == null)
return _path;
return _path + "?" + _query;
}
@Override
public int getPort()
{
return _port;
}
@Override
public String getQuery()
{
return _query;
}
@Override
public String getScheme()
{
return _scheme;
}
@Override
public String getUser()
{
return _user;
}
@Override
public boolean hasAuthority()
{
return _host != null;
}
@Override
public int hashCode()
{
return asString().hashCode();
}
@Override
public boolean isAbsolute()
{
return !StringUtil.isEmpty(_scheme);
}
@Override
public String toString()
{
return asString();
}
@Override
public URI toURI()
{
try
{
return new URI(_scheme, null, _host, _port, _path, _query == null ? null : UrlEncoded.decodeString(_query), _fragment);
}
catch (URISyntaxException x)
{
throw new RuntimeException(x);
}
}
}
class Mutable implements HttpURI
{
private enum State
{
START,
HOST_OR_PATH,
SCHEME_OR_PATH,
HOST,
IPV6,
PORT,
PATH,
PARAM,
QUERY,
FRAGMENT,
ASTERISK
}
private String _scheme;
private String _user;
private String _host;
private int _port;
private String _path;
private String _param;
private String _query;
private String _fragment;
private String _uri;
private String _decodedPath;
private Mutable()
{
}
private Mutable(HttpURI uri)
{
uri(uri);
}
private Mutable(HttpURI baseURI, String pathQuery)
{
_uri = null;
_scheme = baseURI.getScheme();
_user = baseURI.getUser();
_host = baseURI.getHost();
_port = baseURI.getPort();
if (pathQuery != null)
parse(State.PATH, pathQuery);
}
private Mutable(HttpURI baseURI, String path, String param, String query)
{
_uri = null;
_scheme = baseURI.getScheme();
_user = baseURI.getUser();
_host = baseURI.getHost();
_port = baseURI.getPort();
_path = path;
_param = param;
_query = query;
_fragment = null;
}
private Mutable(String uri)
{
_port = -1;
parse(State.START, uri);
}
private Mutable(URI uri)
{
_uri = null;
_scheme = uri.getScheme();
_host = uri.getHost();
if (_host == null && uri.getRawSchemeSpecificPart().startsWith("//"))
_host = "";
_port = uri.getPort();
_user = uri.getUserInfo();
_path = uri.getRawPath();
String pathParam = uri.getPath();
if (pathParam != null)
{
int p = pathParam.lastIndexOf(';');
if (p >= 0)
_param = pathParam.substring(p + 1);
else
_decodedPath = pathParam;
}
_query = uri.getRawQuery();
_fragment = uri.getRawFragment();
}
private Mutable(String scheme, String host, int port, String pathQuery)
{
_uri = null;
_scheme = scheme;
_host = host;
_port = port;
if (pathQuery != null)
parse(State.PATH, pathQuery);
}
@Override
public Immutable asImmutable()
{
return new Immutable(this);
}
@Override
public String asString()
{
return asImmutable().toString();
}
Params: - host – the host
- port – the port
Returns: this mutable
/**
* @param host the host
* @param port the port
* @return this mutable
*/
public Mutable authority(String host, int port)
{
_user = null;
_host = host;
_port = port;
_uri = null;
return this;
}
Params: - hostport – the host and port combined
Returns: this mutable
/**
* @param hostport the host and port combined
* @return this mutable
*/
public Mutable authority(String hostport)
{
HostPort hp = new HostPort(hostport);
_user = null;
_host = hp.getHost();
_port = hp.getPort();
_uri = null;
return this;
}
public Mutable clear()
{
_scheme = null;
_user = null;
_host = null;
_port = -1;
_path = null;
_param = null;
_query = null;
_fragment = null;
_uri = null;
_decodedPath = null;
return this;
}
public Mutable decodedPath(String path)
{
_uri = null;
_path = URIUtil.encodePath(path);
_decodedPath = path;
return this;
}
@Override
public boolean equals(Object o)
{
if (o == this)
return true;
if (!(o instanceof HttpURI))
return false;
return asString().equals(((HttpURI)o).asString());
}
public Mutable fragment(String fragment)
{
_fragment = fragment;
return this;
}
@Override
public String getAuthority()
{
if (_port > 0)
return _host + ":" + _port;
return _host;
}
@Override
public String getDecodedPath()
{
if (_decodedPath == null && _path != null)
_decodedPath = URIUtil.canonicalPath(URIUtil.decodePath(_path));
return _decodedPath;
}
@Override
public String getFragment()
{
return _fragment;
}
@Override
public String getHost()
{
return _host;
}
@Override
public String getParam()
{
return _param;
}
@Override
public String getPath()
{
return _path;
}
@Override
public String getPathQuery()
{
if (_query == null)
return _path;
return _path + "?" + _query;
}
@Override
public int getPort()
{
return _port;
}
@Override
public String getQuery()
{
return _query;
}
@Override
public String getScheme()
{
return _scheme;
}
public String getUser()
{
return _user;
}
@Override
public boolean hasAuthority()
{
return _host != null;
}
@Override
public int hashCode()
{
return asString().hashCode();
}
public Mutable host(String host)
{
_host = host;
_uri = null;
return this;
}
@Override
public boolean isAbsolute()
{
return _scheme != null && !_scheme.isEmpty();
}
public Mutable normalize()
{
HttpScheme scheme = _scheme == null ? null : HttpScheme.CACHE.get(_scheme);
if (scheme != null && _port == scheme.getDefaultPort())
{
_port = 0;
_uri = null;
}
return this;
}
public Mutable param(String param)
{
_param = param;
if (_path != null && _param != null && !_path.contains(_param))
{
_path += ";" + _param;
}
_uri = null;
return this;
}
Params: - path – the path
Returns: this Mutuble
/**
* @param path the path
* @return this Mutuble
*/
public Mutable path(String path)
{
_uri = null;
_path = path;
_decodedPath = null;
return this;
}
public Mutable pathQuery(String pathQuery)
{
_uri = null;
_path = null;
_decodedPath = null;
_param = null;
if (pathQuery != null)
parse(State.PATH, pathQuery);
return this;
}
public Mutable port(int port)
{
_port = port;
_uri = null;
return this;
}
public Mutable query(String query)
{
_query = query;
_uri = null;
return this;
}
public Mutable scheme(HttpScheme scheme)
{
return scheme(scheme.asString());
}
public Mutable scheme(String scheme)
{
_scheme = scheme;
_uri = null;
return this;
}
@Override
public String toString()
{
return asString();
}
public URI toURI()
{
try
{
return new URI(_scheme, null, _host, _port, _path, _query == null ? null : UrlEncoded.decodeString(_query), null);
}
catch (URISyntaxException x)
{
throw new RuntimeException(x);
}
}
public Mutable uri(HttpURI uri)
{
_scheme = uri.getScheme();
_user = uri.getUser();
_host = uri.getHost();
_port = uri.getPort();
_path = uri.getPath();
_param = uri.getParam();
_query = uri.getQuery();
_uri = null;
_decodedPath = uri.getDecodedPath();
return this;
}
public Mutable uri(String uri)
{
clear();
_uri = uri;
parse(State.START, uri);
return this;
}
public Mutable uri(String method, String uri)
{
if (HttpMethod.CONNECT.is(method))
{
clear();
_uri = uri;
_path = uri;
}
else if (uri.startsWith("/"))
{
clear();
pathQuery(uri);
}
else
uri(uri);
return this;
}
public Mutable uri(String uri, int offset, int length)
{
clear();
int end = offset + length;
_uri = uri.substring(offset, end);
parse(State.START, uri);
return this;
}
public Mutable user(String user)
{
_user = user;
_uri = null;
return this;
}
private void parse(State state, final String uri)
{
boolean encoded = false;
int end = uri.length();
int mark = 0;
int pathMark = 0;
char last = '/';
for (int i = 0; i < end; i++)
{
char c = uri.charAt(i);
switch (state)
{
case START:
{
switch (c)
{
case '/':
mark = i;
state = State.HOST_OR_PATH;
break;
case ';':
mark = i + 1;
state = State.PARAM;
break;
case '?':
// assume empty path (if seen at start)
_path = "";
mark = i + 1;
state = State.QUERY;
break;
case '#':
mark = i + 1;
state = State.FRAGMENT;
break;
case '*':
_path = "*";
state = State.ASTERISK;
break;
case '.':
pathMark = i;
state = State.PATH;
encoded = true;
break;
default:
mark = i;
if (_scheme == null)
state = State.SCHEME_OR_PATH;
else
{
pathMark = i;
state = State.PATH;
}
break;
}
continue;
}
case SCHEME_OR_PATH:
{
switch (c)
{
case ':':
// must have been a scheme
_scheme = uri.substring(mark, i);
// Start again with scheme set
state = State.START;
break;
case '/':
// must have been in a path and still are
state = State.PATH;
break;
case ';':
// must have been in a path
mark = i + 1;
state = State.PARAM;
break;
case '?':
// must have been in a path
_path = uri.substring(mark, i);
mark = i + 1;
state = State.QUERY;
break;
case '%':
// must have be in an encoded path
encoded = true;
state = State.PATH;
break;
case '#':
// must have been in a path
_path = uri.substring(mark, i);
state = State.FRAGMENT;
break;
default:
break;
}
continue;
}
case HOST_OR_PATH:
{
switch (c)
{
case '/':
_host = "";
mark = i + 1;
state = State.HOST;
break;
case '@':
case ';':
case '?':
case '#':
// was a path, look again
i--;
pathMark = mark;
state = State.PATH;
break;
case '.':
// it is a path
encoded = true;
pathMark = mark;
state = State.PATH;
break;
default:
// it is a path
pathMark = mark;
state = State.PATH;
}
continue;
}
case HOST:
{
switch (c)
{
case '/':
_host = uri.substring(mark, i);
pathMark = mark = i;
state = State.PATH;
break;
case ':':
if (i > mark)
_host = uri.substring(mark, i);
mark = i + 1;
state = State.PORT;
break;
case '@':
if (_user != null)
throw new IllegalArgumentException("Bad authority");
_user = uri.substring(mark, i);
mark = i + 1;
break;
case '[':
state = State.IPV6;
break;
default:
break;
}
break;
}
case IPV6:
{
switch (c)
{
case '/':
throw new IllegalArgumentException("No closing ']' for ipv6 in " + uri);
case ']':
c = uri.charAt(++i);
_host = uri.substring(mark, i);
if (c == ':')
{
mark = i + 1;
state = State.PORT;
}
else
{
pathMark = mark = i;
state = State.PATH;
}
break;
default:
break;
}
break;
}
case PORT:
{
if (c == '@')
{
if (_user != null)
throw new IllegalArgumentException("Bad authority");
// It wasn't a port, but a password!
_user = _host + ":" + uri.substring(mark, i);
mark = i + 1;
state = State.HOST;
}
else if (c == '/')
{
_port = TypeUtil.parseInt(uri, mark, i - mark, 10);
pathMark = mark = i;
state = State.PATH;
}
break;
}
case PATH:
{
switch (c)
{
case ';':
mark = i + 1;
state = State.PARAM;
break;
case '?':
_path = uri.substring(pathMark, i);
mark = i + 1;
state = State.QUERY;
break;
case '#':
_path = uri.substring(pathMark, i);
mark = i + 1;
state = State.FRAGMENT;
break;
case '%':
encoded = true;
break;
case '.':
if ('/' == last)
encoded = true;
break;
default:
break;
}
break;
}
case PARAM:
{
switch (c)
{
case '?':
_path = uri.substring(pathMark, i);
_param = uri.substring(mark, i);
mark = i + 1;
state = State.QUERY;
break;
case '#':
_path = uri.substring(pathMark, i);
_param = uri.substring(mark, i);
mark = i + 1;
state = State.FRAGMENT;
break;
case '/':
encoded = true;
// ignore internal params
state = State.PATH;
break;
case ';':
// multiple parameters
mark = i + 1;
break;
default:
break;
}
break;
}
case QUERY:
{
if (c == '#')
{
_query = uri.substring(mark, i);
mark = i + 1;
state = State.FRAGMENT;
}
break;
}
case ASTERISK:
{
throw new IllegalArgumentException("Bad character '*'");
}
case FRAGMENT:
{
i = end;
break;
}
default:
throw new IllegalStateException(state.toString());
}
last = c;
}
switch (state)
{
case START:
case ASTERISK:
break;
case SCHEME_OR_PATH:
_path = uri.substring(mark, end);
break;
case HOST_OR_PATH:
_path = uri.substring(mark, end);
break;
case HOST:
if (end > mark)
_host = uri.substring(mark, end);
break;
case IPV6:
throw new IllegalArgumentException("No closing ']' for ipv6 in " + uri);
case PORT:
_port = TypeUtil.parseInt(uri, mark, end - mark, 10);
break;
case PARAM:
_path = uri.substring(pathMark, end);
_param = uri.substring(mark, end);
break;
case PATH:
_path = uri.substring(pathMark, end);
break;
case QUERY:
_query = uri.substring(mark, end);
break;
case FRAGMENT:
_fragment = uri.substring(mark, end);
break;
default:
throw new IllegalStateException(state.toString());
}
if (!encoded)
{
if (_param == null)
_decodedPath = _path;
else
_decodedPath = _path.substring(0, _path.length() - _param.length() - 1);
}
}
}
}