| 1 | package com.renomad.minum.web; | |
| 2 | ||
| 3 | import com.renomad.minum.logging.ILogger; | |
| 4 | import com.renomad.minum.security.ForbiddenUseException; | |
| 5 | import com.renomad.minum.utils.StringUtils; | |
| 6 | ||
| 7 | import java.util.*; | |
| 8 | ||
| 9 | import static com.renomad.minum.utils.Invariants.mustNotBeNull; | |
| 10 | ||
| 11 | /** | |
| 12 | * This class holds data and methods for dealing with the | |
| 13 | * "start line" in an HTTP request. For example, | |
| 14 | * GET /foo HTTP/1.1 | |
| 15 | */ | |
| 16 | public final class RequestLine { | |
| 17 | ||
| 18 | private final Method method; | |
| 19 | private final PathDetails pathDetails; | |
| 20 | private final HttpVersion version; | |
| 21 | private final String rawValue; | |
| 22 | private final ILogger logger; | |
| 23 | static final int MAX_QUERY_STRING_KEYS_COUNT = 50; | |
| 24 | ||
| 25 | /** | |
| 26 | * @param method GET, POST, etc. | |
| 27 | * @param pathDetails See {@link PathDetails} | |
| 28 | * @param version the version of HTTP (1.0 or 1.1) we're receiving | |
| 29 | * @param rawValue the entire raw string of the start line | |
| 30 | */ | |
| 31 | public RequestLine( | |
| 32 | Method method, | |
| 33 | PathDetails pathDetails, | |
| 34 | HttpVersion version, | |
| 35 | String rawValue, | |
| 36 | ILogger logger | |
| 37 | ) { | |
| 38 | this.method = method; | |
| 39 | this.pathDetails = pathDetails; | |
| 40 | this.version = version; | |
| 41 | this.rawValue = rawValue; | |
| 42 | this.logger = logger; | |
| 43 | } | |
| 44 | ||
| 45 | ||
| 46 | ||
| 47 | public static final RequestLine EMPTY = new RequestLine(Method.NONE, PathDetails.empty, HttpVersion.NONE, "", null); | |
| 48 | ||
| 49 | /** | |
| 50 | * Returns a map of the key-value pairs in the URL, | |
| 51 | * for example in {@code http://foo.com?name=alice} you | |
| 52 | * have a key of name and a value of alice. | |
| 53 | */ | |
| 54 | public Map<String, String> queryString() { | |
| 55 |
2
1. queryString : negated conditional → KILLED 2. queryString : negated conditional → KILLED |
if (pathDetails == null || pathDetails.getQueryString().isEmpty()) { |
| 56 | return Map.of(); | |
| 57 | } else { | |
| 58 |
1
1. queryString : replaced return value with Collections.emptyMap for com/renomad/minum/web/RequestLine::queryString → KILLED |
return new HashMap<>(pathDetails.getQueryString()); |
| 59 | } | |
| 60 | ||
| 61 | } | |
| 62 | ||
| 63 | /** | |
| 64 | * These are the HTTP methods we handle. | |
| 65 | */ | |
| 66 | public enum Method { | |
| 67 | GET, | |
| 68 | POST, | |
| 69 | PUT, | |
| 70 | DELETE, | |
| 71 | TRACE, | |
| 72 | PATCH, | |
| 73 | OPTIONS, | |
| 74 | HEAD, | |
| 75 | ||
| 76 | /** | |
| 77 | * Represents the null value of Method | |
| 78 | */ | |
| 79 | NONE | |
| 80 | } | |
| 81 | ||
| 82 | /** | |
| 83 | * Given the string value of a Request Line (like GET /hello HTTP/1.1) | |
| 84 | * validate and extract the values for our use. | |
| 85 | */ | |
| 86 | public RequestLine extractRequestLine(String value) { | |
| 87 | mustNotBeNull(value); | |
| 88 |
1
1. extractRequestLine : negated conditional → KILLED |
if (value.isEmpty()) { |
| 89 |
1
1. extractRequestLine : replaced return value with null for com/renomad/minum/web/RequestLine::extractRequestLine → KILLED |
return RequestLine.EMPTY; |
| 90 | } | |
| 91 | RequestLineRawValues rawValues = requestLineTokenizer(value); | |
| 92 |
1
1. extractRequestLine : negated conditional → KILLED |
if (rawValues == null) { |
| 93 |
1
1. extractRequestLine : replaced return value with null for com/renomad/minum/web/RequestLine::extractRequestLine → KILLED |
return RequestLine.EMPTY; |
| 94 | } | |
| 95 | Method myMethod = extractMethod(rawValues.method()); | |
| 96 |
1
1. extractRequestLine : negated conditional → KILLED |
if (myMethod.equals(Method.NONE)) { |
| 97 |
1
1. extractRequestLine : replaced return value with null for com/renomad/minum/web/RequestLine::extractRequestLine → KILLED |
return RequestLine.EMPTY; |
| 98 | } | |
| 99 | PathDetails pd = extractPathDetails(rawValues.path()); | |
| 100 | HttpVersion httpVersion = getHttpVersion(rawValues.protocol()); | |
| 101 |
1
1. extractRequestLine : negated conditional → KILLED |
if (httpVersion.equals(HttpVersion.NONE)) { |
| 102 |
1
1. extractRequestLine : replaced return value with null for com/renomad/minum/web/RequestLine::extractRequestLine → KILLED |
return RequestLine.EMPTY; |
| 103 | } | |
| 104 | ||
| 105 |
1
1. extractRequestLine : replaced return value with null for com/renomad/minum/web/RequestLine::extractRequestLine → KILLED |
return new RequestLine(myMethod, pd, httpVersion, value, logger); |
| 106 | } | |
| 107 | ||
| 108 | /** | |
| 109 | * Split the request line into three parts - a method (e.g. GET), a | |
| 110 | * path (e.g. "/" or "/helloworld/hi/foo?name=hello") and a protocol, | |
| 111 | * which is typically "HTTP/1.1" but might be "HTTP/1.0" in some cases | |
| 112 | * <br> | |
| 113 | * If we don't find exactly three parts, we will return null, which | |
| 114 | * is interpreted by the calling method to mean we didn't receive a | |
| 115 | * valid request line. | |
| 116 | * @param rawRequestLine the full string of the first line received | |
| 117 | * after the socket is connected to the client. | |
| 118 | */ | |
| 119 | private RequestLineRawValues requestLineTokenizer(String rawRequestLine) { | |
| 120 | int firstSpace = rawRequestLine.indexOf(' '); | |
| 121 |
1
1. requestLineTokenizer : negated conditional → KILLED |
if (firstSpace == -1) { |
| 122 | return null; | |
| 123 | } | |
| 124 |
1
1. requestLineTokenizer : Replaced integer addition with subtraction → KILLED |
int secondSpace = rawRequestLine.indexOf(' ', firstSpace + 1); |
| 125 |
1
1. requestLineTokenizer : negated conditional → KILLED |
if (secondSpace == -1) { |
| 126 | return null; | |
| 127 | } | |
| 128 |
1
1. requestLineTokenizer : Replaced integer addition with subtraction → KILLED |
int thirdSpace = rawRequestLine.indexOf(' ', secondSpace + 1); |
| 129 |
1
1. requestLineTokenizer : negated conditional → TIMED_OUT |
if (thirdSpace != -1) { |
| 130 | return null; | |
| 131 | } | |
| 132 | String myMethod = rawRequestLine.substring(0, firstSpace); | |
| 133 |
1
1. requestLineTokenizer : Replaced integer addition with subtraction → KILLED |
String path = rawRequestLine.substring(firstSpace + 1, secondSpace); |
| 134 |
1
1. requestLineTokenizer : Replaced integer addition with subtraction → TIMED_OUT |
String protocol = rawRequestLine.substring(secondSpace + 1); |
| 135 |
1
1. requestLineTokenizer : replaced return value with null for com/renomad/minum/web/RequestLine::requestLineTokenizer → KILLED |
return new RequestLineRawValues(myMethod, path, protocol); |
| 136 | } | |
| 137 | ||
| 138 | private Method extractMethod(String methodString) { | |
| 139 | try { | |
| 140 |
1
1. extractMethod : replaced return value with null for com/renomad/minum/web/RequestLine::extractMethod → TIMED_OUT |
return Method.valueOf(methodString.toUpperCase(Locale.ROOT)); |
| 141 | } catch (Exception ex) { | |
| 142 | logger.logDebug(() -> "Unable to convert method to enum: " + methodString); | |
| 143 |
1
1. extractMethod : replaced return value with null for com/renomad/minum/web/RequestLine::extractMethod → KILLED |
return Method.NONE; |
| 144 | } | |
| 145 | } | |
| 146 | ||
| 147 | private PathDetails extractPathDetails(String path) { | |
| 148 | PathDetails pd; | |
| 149 | // the request line will have a forward slash at the beginning of | |
| 150 | // the path. Remove that here. | |
| 151 | String adjustedPath = path.substring(1); | |
| 152 | int locationOfQueryBegin = adjustedPath.indexOf("?"); | |
| 153 |
2
1. extractPathDetails : negated conditional → TIMED_OUT 2. extractPathDetails : changed conditional boundary → KILLED |
if (locationOfQueryBegin > 0) { |
| 154 | // in this case, we found a question mark, suggesting that a query string exists | |
| 155 |
1
1. extractPathDetails : Replaced integer addition with subtraction → KILLED |
String rawQueryString = adjustedPath.substring(locationOfQueryBegin + 1); |
| 156 | String isolatedPath = adjustedPath.substring(0, locationOfQueryBegin); | |
| 157 | Map<String, String> queryString = extractMapFromQueryString(rawQueryString); | |
| 158 | pd = new PathDetails(isolatedPath, rawQueryString, queryString); | |
| 159 | } else { | |
| 160 | // in this case, no question mark was found, thus no query string | |
| 161 | pd = new PathDetails(adjustedPath, null, null); | |
| 162 | } | |
| 163 |
1
1. extractPathDetails : replaced return value with null for com/renomad/minum/web/RequestLine::extractPathDetails → KILLED |
return pd; |
| 164 | } | |
| 165 | ||
| 166 | ||
| 167 | /** | |
| 168 | * Given a string containing the combined key-values in | |
| 169 | * a query string (e.g. foo=bar&name=alice), split that | |
| 170 | * into a map of the key to value (e.g. foo to bar, and name to alice) | |
| 171 | */ | |
| 172 | Map<String, String> extractMapFromQueryString(String rawQueryString) { | |
| 173 | Map<String, String> queryStrings = new HashMap<>(); | |
| 174 | StringTokenizer tokenizer = new StringTokenizer(rawQueryString, "&"); | |
| 175 | // we'll only take less than MAX_QUERY_STRING_KEYS_COUNT | |
| 176 |
2
1. extractMapFromQueryString : Changed increment from 1 to -1 → TIMED_OUT 2. extractMapFromQueryString : negated conditional → KILLED |
for (int i = 0; tokenizer.hasMoreTokens(); i++) { |
| 177 |
2
1. extractMapFromQueryString : changed conditional boundary → KILLED 2. extractMapFromQueryString : negated conditional → KILLED |
if (i >= MAX_QUERY_STRING_KEYS_COUNT) throw new ForbiddenUseException("User tried providing too many query string keys. max: " + MAX_QUERY_STRING_KEYS_COUNT); |
| 178 | // this should give us a key and value joined with an equal sign, e.g. foo=bar | |
| 179 | String currentKeyValue = tokenizer.nextToken(); | |
| 180 | int equalSignLocation = currentKeyValue.indexOf("="); | |
| 181 |
2
1. extractMapFromQueryString : changed conditional boundary → TIMED_OUT 2. extractMapFromQueryString : negated conditional → KILLED |
if (equalSignLocation <= 0) return Map.of(); |
| 182 | String key = currentKeyValue.substring(0, equalSignLocation); | |
| 183 |
1
1. extractMapFromQueryString : Replaced integer addition with subtraction → TIMED_OUT |
String myRawValue = currentKeyValue.substring(equalSignLocation + 1); |
| 184 | try { | |
| 185 | String value = StringUtils.decode(myRawValue); | |
| 186 | queryStrings.put(key, value); | |
| 187 | } catch (IllegalArgumentException ex) { | |
| 188 | logger.logDebug(() -> "Query string parsing failed for key: (%s) value: (%s). Skipping to next key-value pair. error message: %s".formatted(key, myRawValue, ex.getMessage())); | |
| 189 | } | |
| 190 | } | |
| 191 |
1
1. extractMapFromQueryString : replaced return value with Collections.emptyMap for com/renomad/minum/web/RequestLine::extractMapFromQueryString → TIMED_OUT |
return queryStrings; |
| 192 | } | |
| 193 | ||
| 194 | /** | |
| 195 | * Extract the HTTP version from the start line | |
| 196 | */ | |
| 197 | private HttpVersion getHttpVersion(String version) { | |
| 198 |
1
1. getHttpVersion : negated conditional → KILLED |
if (version.equals("HTTP/1.1")) { |
| 199 |
1
1. getHttpVersion : replaced return value with null for com/renomad/minum/web/RequestLine::getHttpVersion → KILLED |
return HttpVersion.ONE_DOT_ONE; |
| 200 |
1
1. getHttpVersion : negated conditional → KILLED |
} else if (version.equals("HTTP/1.0")) { |
| 201 |
1
1. getHttpVersion : replaced return value with null for com/renomad/minum/web/RequestLine::getHttpVersion → KILLED |
return HttpVersion.ONE_DOT_ZERO; |
| 202 | } else { | |
| 203 |
1
1. getHttpVersion : replaced return value with null for com/renomad/minum/web/RequestLine::getHttpVersion → KILLED |
return HttpVersion.NONE; |
| 204 | } | |
| 205 | } | |
| 206 | ||
| 207 | /** | |
| 208 | * Return the method of this request-line. For example, GET, PUT, POST... | |
| 209 | */ | |
| 210 | public Method getMethod() { | |
| 211 |
1
1. getMethod : replaced return value with null for com/renomad/minum/web/RequestLine::getMethod → KILLED |
return method; |
| 212 | } | |
| 213 | ||
| 214 | /** | |
| 215 | * This returns an object which contains essential information about the path | |
| 216 | * in the request line. For example, if the request line is "GET /sample?foo=bar HTTP/1.1", | |
| 217 | * this would hold data for the path ("sample") and the query string ("foo=bar") | |
| 218 | */ | |
| 219 | public PathDetails getPathDetails() { | |
| 220 |
1
1. getPathDetails : replaced return value with null for com/renomad/minum/web/RequestLine::getPathDetails → TIMED_OUT |
return pathDetails; |
| 221 | } | |
| 222 | ||
| 223 | /** | |
| 224 | * Gets the HTTP version, either 1.0 or 1.1 | |
| 225 | */ | |
| 226 | public HttpVersion getVersion() { | |
| 227 |
1
1. getVersion : replaced return value with null for com/renomad/minum/web/RequestLine::getVersion → TIMED_OUT |
return this.version; |
| 228 | } | |
| 229 | ||
| 230 | /** | |
| 231 | * Get the string value of this request line, such as "GET /sample.html HTTP/1.1" | |
| 232 | */ | |
| 233 | public String getRawValue() { | |
| 234 |
1
1. getRawValue : replaced return value with "" for com/renomad/minum/web/RequestLine::getRawValue → KILLED |
return rawValue; |
| 235 | } | |
| 236 | ||
| 237 | @Override | |
| 238 | public boolean equals(Object o) { | |
| 239 |
2
1. equals : negated conditional → TIMED_OUT 2. equals : replaced boolean return with false for com/renomad/minum/web/RequestLine::equals → KILLED |
if (this == o) return true; |
| 240 |
3
1. equals : negated conditional → TIMED_OUT 2. equals : negated conditional → TIMED_OUT 3. equals : replaced boolean return with true for com/renomad/minum/web/RequestLine::equals → TIMED_OUT |
if (o == null || getClass() != o.getClass()) return false; |
| 241 | RequestLine that = (RequestLine) o; | |
| 242 |
6
1. equals : negated conditional → TIMED_OUT 2. equals : replaced boolean return with true for com/renomad/minum/web/RequestLine::equals → TIMED_OUT 3. equals : negated conditional → TIMED_OUT 4. equals : negated conditional → TIMED_OUT 5. equals : negated conditional → TIMED_OUT 6. equals : negated conditional → KILLED |
return method == that.method && Objects.equals(pathDetails, that.pathDetails) && version == that.version && Objects.equals(rawValue, that.rawValue) && Objects.equals(logger, that.logger); |
| 243 | } | |
| 244 | ||
| 245 | @Override | |
| 246 | public int hashCode() { | |
| 247 |
1
1. hashCode : replaced int return with 0 for com/renomad/minum/web/RequestLine::hashCode → TIMED_OUT |
return Objects.hash(method, pathDetails, version, rawValue, logger); |
| 248 | } | |
| 249 | ||
| 250 | @Override | |
| 251 | public String toString() { | |
| 252 |
1
1. toString : replaced return value with "" for com/renomad/minum/web/RequestLine::toString → KILLED |
return "RequestLine{" + |
| 253 | "method=" + method + | |
| 254 | ", pathDetails=" + pathDetails + | |
| 255 | ", version=" + version + | |
| 256 | ", rawValue='" + rawValue + '\'' + | |
| 257 | ", logger=" + logger + | |
| 258 | '}'; | |
| 259 | } | |
| 260 | } | |
Mutations | ||
| 55 |
1.1 2.2 |
|
| 58 |
1.1 |
|
| 88 |
1.1 |
|
| 89 |
1.1 |
|
| 92 |
1.1 |
|
| 93 |
1.1 |
|
| 96 |
1.1 |
|
| 97 |
1.1 |
|
| 101 |
1.1 |
|
| 102 |
1.1 |
|
| 105 |
1.1 |
|
| 121 |
1.1 |
|
| 124 |
1.1 |
|
| 125 |
1.1 |
|
| 128 |
1.1 |
|
| 129 |
1.1 |
|
| 133 |
1.1 |
|
| 134 |
1.1 |
|
| 135 |
1.1 |
|
| 140 |
1.1 |
|
| 143 |
1.1 |
|
| 153 |
1.1 2.2 |
|
| 155 |
1.1 |
|
| 163 |
1.1 |
|
| 176 |
1.1 2.2 |
|
| 177 |
1.1 2.2 |
|
| 181 |
1.1 2.2 |
|
| 183 |
1.1 |
|
| 191 |
1.1 |
|
| 198 |
1.1 |
|
| 199 |
1.1 |
|
| 200 |
1.1 |
|
| 201 |
1.1 |
|
| 203 |
1.1 |
|
| 211 |
1.1 |
|
| 220 |
1.1 |
|
| 227 |
1.1 |
|
| 234 |
1.1 |
|
| 239 |
1.1 2.2 |
|
| 240 |
1.1 2.2 3.3 |
|
| 242 |
1.1 2.2 3.3 4.4 5.5 6.6 |
|
| 247 |
1.1 |
|
| 252 |
1.1 |