package org.eclipse.jetty.http;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableSet;
import static java.util.EnumSet.allOf;
import static java.util.EnumSet.complementOf;
import static java.util.EnumSet.noneOf;
import static java.util.EnumSet.of;
public final class HttpCompliance implements ComplianceViolation.Mode
{
public enum Violation implements ComplianceViolation
{
CASE_SENSITIVE_FIELD_NAME("https://tools.ietf.org/html/rfc7230#section-3.2", "Field name is case-insensitive"),
CASE_INSENSITIVE_METHOD("https://tools.ietf.org/html/rfc7230#section-3.1.1", "Method is case-sensitive"),
HTTP_0_9("https://tools.ietf.org/html/rfc7230#appendix-A.2", "HTTP/0.9 not supported"),
MULTILINE_FIELD_VALUE("https://tools.ietf.org/html/rfc7230#section-3.2.4", "Line Folding not supported"),
MULTIPLE_CONTENT_LENGTHS("https://tools.ietf.org/html/rfc7230#section-3.3.1", "Multiple Content-Lengths"),
TRANSFER_ENCODING_WITH_CONTENT_LENGTH("https://tools.ietf.org/html/rfc7230#section-3.3.1", "Transfer-Encoding and Content-Length"),
WHITESPACE_AFTER_FIELD_NAME("https://tools.ietf.org/html/rfc7230#section-3.2.4", "Whitespace not allowed after field name"),
NO_COLON_AFTER_FIELD_NAME("https://tools.ietf.org/html/rfc7230#section-3.2", "Fields must have a Colon");
private final String url;
private final String description;
Violation(String url, String description)
{
this.url = url;
this.description = description;
}
@Override
public String getName()
{
return name();
}
@Override
public String getURL()
{
return url;
}
@Override
public String getDescription()
{
return description;
}
}
private static final Logger LOG = Log.getLogger(HttpParser.class);
public static final String VIOLATIONS_ATTR = "org.eclipse.jetty.http.compliance.violations";
public static final HttpCompliance RFC7230 = new HttpCompliance("RFC7230", noneOf(Violation.class));
public static final HttpCompliance RFC2616 = new HttpCompliance("RFC2616", of(Violation.HTTP_0_9, Violation.MULTILINE_FIELD_VALUE));
public static final HttpCompliance LEGACY = new HttpCompliance("LEGACY", complementOf(of(Violation.CASE_INSENSITIVE_METHOD)));
public static final HttpCompliance RFC2616_LEGACY = RFC2616.with("RFC2616_LEGACY",
Violation.CASE_INSENSITIVE_METHOD,
Violation.NO_COLON_AFTER_FIELD_NAME,
Violation.TRANSFER_ENCODING_WITH_CONTENT_LENGTH,
Violation.MULTIPLE_CONTENT_LENGTHS);
public static final HttpCompliance RFC7230_LEGACY = RFC7230.with("RFC7230_LEGACY", Violation.CASE_INSENSITIVE_METHOD);
private static final List<HttpCompliance> KNOWN_MODES = Arrays.asList(RFC7230, RFC2616, LEGACY, RFC2616_LEGACY, RFC7230_LEGACY);
private static final AtomicInteger __custom = new AtomicInteger();
public static HttpCompliance valueOf(String name)
{
for (HttpCompliance compliance : KNOWN_MODES)
{
if (compliance.getName().equals(name))
return compliance;
}
return null;
}
public static HttpCompliance from(String spec)
{
Set<Violation> sections;
String[] elements = spec.split("\\s*,\\s*");
switch (elements[0])
{
case "0":
sections = noneOf(Violation.class);
break;
case "*":
sections = allOf(Violation.class);
break;
default:
{
HttpCompliance mode = HttpCompliance.valueOf(elements[0]);
if (mode == null)
sections = noneOf(Violation.class);
else
sections = copyOf(mode.getAllowed());
}
}
for (int i = 1; i < elements.length; i++)
{
String element = elements[i];
boolean exclude = element.startsWith("-");
if (exclude)
element = element.substring(1);
Violation section = Violation.valueOf(element);
if (section == null)
{
LOG.warn("Unknown section '" + element + "' in HttpCompliance spec: " + spec);
continue;
}
if (exclude)
sections.remove(section);
else
sections.add(section);
}
return new HttpCompliance("CUSTOM" + __custom.getAndIncrement(), sections);
}
private final String _name;
private final Set<Violation> _violations;
private HttpCompliance(String name, Set<Violation> violations)
{
Objects.nonNull(violations);
_name = name;
_violations = unmodifiableSet(violations.isEmpty() ? noneOf(Violation.class) : copyOf(violations));
}
@Override
public boolean allows(ComplianceViolation violation)
{
return _violations.contains(violation);
}
@Override
public String getName()
{
return _name;
}
@Override
public Set<Violation> getAllowed()
{
return _violations;
}
@Override
public Set<Violation> getKnown()
{
return EnumSet.allOf(Violation.class);
}
public HttpCompliance with(String name, Violation... violations)
{
Set<Violation> union = _violations.isEmpty() ? EnumSet.noneOf(Violation.class) : copyOf(_violations);
union.addAll(copyOf(violations));
return new HttpCompliance(name, union);
}
public HttpCompliance without(String name, Violation... violations)
{
Set<Violation> remainder = _violations.isEmpty() ? EnumSet.noneOf(Violation.class) : copyOf(_violations);
remainder.removeAll(copyOf(violations));
return new HttpCompliance(name, remainder);
}
@Override
public String toString()
{
return String.format("%s%s", _name, _violations);
}
private static Set<Violation> copyOf(Violation[] violations)
{
if (violations == null || violations.length == 0)
return EnumSet.noneOf(Violation.class);
return EnumSet.copyOf(asList(violations));
}
private static Set<Violation> copyOf(Set<Violation> violations)
{
if (violations == null || violations.isEmpty())
return EnumSet.noneOf(Violation.class);
return EnumSet.copyOf(violations);
}
}