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 → KILLED |
if (thirdSpace != -1) { |
130 | return null; | |
131 | } | |
132 | String method = 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 → KILLED |
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(method, 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 → KILLED |
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 : changed conditional boundary → KILLED 2. extractPathDetails : negated conditional → 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 : negated conditional → KILLED 2. extractMapFromQueryString : Changed increment from 1 to -1 → 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 → KILLED 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 → KILLED |
String rawValue = currentKeyValue.substring(equalSignLocation + 1); |
184 | try { | |
185 | String value = StringUtils.decode(rawValue); | |
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, rawValue, ex.getMessage())); | |
189 | } | |
190 | } | |
191 |
1
1. extractMapFromQueryString : replaced return value with Collections.emptyMap for com/renomad/minum/web/RequestLine::extractMapFromQueryString → KILLED |
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 → KILLED |
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 → KILLED |
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 : negated conditional → TIMED_OUT 3. equals : replaced boolean return with true for com/renomad/minum/web/RequestLine::equals → TIMED_OUT 4. equals : negated conditional → TIMED_OUT 5. equals : negated conditional → TIMED_OUT 6. equals : negated conditional → TIMED_OUT |
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 |