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 | import java.util.regex.Matcher; | |
9 | import java.util.regex.Pattern; | |
10 | ||
11 | import static com.renomad.minum.utils.Invariants.mustNotBeNull; | |
12 | ||
13 | /** | |
14 | * This class holds data and methods for dealing with the | |
15 | * "start line" in an HTTP request. For example, | |
16 | * GET /foo HTTP/1.1 | |
17 | */ | |
18 | public final class RequestLine { | |
19 | ||
20 | private final Method method; | |
21 | private final PathDetails pathDetails; | |
22 | private final HttpVersion version; | |
23 | private final String rawValue; | |
24 | private final ILogger logger; | |
25 | static final int MAX_QUERY_STRING_KEYS_COUNT = 50; | |
26 | ||
27 | /** | |
28 | * @param method GET, POST, etc. | |
29 | * @param pathDetails See {@link PathDetails} | |
30 | * @param version the version of HTTP (1.0 or 1.1) we're receiving | |
31 | * @param rawValue the entire raw string of the start line | |
32 | */ | |
33 | public RequestLine( | |
34 | Method method, | |
35 | PathDetails pathDetails, | |
36 | HttpVersion version, | |
37 | String rawValue, | |
38 | ILogger logger | |
39 | ) { | |
40 | this.method = method; | |
41 | this.pathDetails = pathDetails; | |
42 | this.version = version; | |
43 | this.rawValue = rawValue; | |
44 | this.logger = logger; | |
45 | } | |
46 | ||
47 | /** | |
48 | * This is our regex for looking at a client's request | |
49 | * and determining what to send them. For example, | |
50 | * if they send GET /sample.html HTTP/1.1, we send them sample.html | |
51 | * <p> | |
52 | * On the other hand if it's not a well-formed request, or | |
53 | * if we don't have that file, we reply with an error page | |
54 | * </p> | |
55 | */ | |
56 | static final String REQUEST_LINE_PATTERN = "^([A-Z]{3,8})" + // an HTTP method, like GET, HEAD, POST, or OPTIONS | |
57 | " /?(.*)" + // the request target - may or may not start with a slash. | |
58 | " HTTP/(1.1|1.0)$"; // the HTTP version, defining structure of the remaining message | |
59 | ||
60 | static final Pattern startLineRegex = Pattern.compile(REQUEST_LINE_PATTERN); | |
61 | ||
62 | public static final RequestLine EMPTY = new RequestLine(Method.NONE, PathDetails.empty, HttpVersion.NONE, "", null); | |
63 | ||
64 | /** | |
65 | * Returns a map of the key-value pairs in the URL, | |
66 | * for example in {@code http://foo.com?name=alice} you | |
67 | * have a key of name and a value of alice. | |
68 | */ | |
69 | public Map<String, String> queryString() { | |
70 |
2
1. queryString : negated conditional → KILLED 2. queryString : negated conditional → KILLED |
if (pathDetails == null || pathDetails.getQueryString().isEmpty()) { |
71 |
1
1. queryString : replaced return value with Collections.emptyMap for com/renomad/minum/web/RequestLine::queryString → SURVIVED |
return new HashMap<>(); |
72 | } else { | |
73 |
1
1. queryString : replaced return value with Collections.emptyMap for com/renomad/minum/web/RequestLine::queryString → KILLED |
return new HashMap<>(pathDetails.getQueryString()); |
74 | } | |
75 | ||
76 | } | |
77 | ||
78 | /** | |
79 | * These are the HTTP methods we handle. | |
80 | * @see #REQUEST_LINE_PATTERN | |
81 | */ | |
82 | public enum Method { | |
83 | GET, | |
84 | POST, | |
85 | PUT, | |
86 | DELETE, | |
87 | TRACE, | |
88 | PATCH, | |
89 | OPTIONS, | |
90 | HEAD, | |
91 | ||
92 | /** | |
93 | * Represents the null value of Method | |
94 | */ | |
95 | NONE | |
96 | } | |
97 | ||
98 | /** | |
99 | * Given the string value of a Request Line (like GET /hello HTTP/1.1) | |
100 | * validate and extract the values for our use. | |
101 | */ | |
102 | public RequestLine extractRequestLine(String value) { | |
103 | mustNotBeNull(value); | |
104 | Matcher m = RequestLine.startLineRegex.matcher(value); | |
105 | // run the regex | |
106 | var doesMatch = m.matches(); | |
107 |
1
1. extractRequestLine : negated conditional → KILLED |
if (!doesMatch) { |
108 |
1
1. extractRequestLine : replaced return value with null for com/renomad/minum/web/RequestLine::extractRequestLine → KILLED |
return RequestLine.EMPTY; |
109 | } | |
110 | Method myMethod = extractMethod(m.group(1)); | |
111 | PathDetails pd = extractPathDetails(m.group(2)); | |
112 | HttpVersion httpVersion = getHttpVersion(m.group(3)); | |
113 | ||
114 |
1
1. extractRequestLine : replaced return value with null for com/renomad/minum/web/RequestLine::extractRequestLine → KILLED |
return new RequestLine(myMethod, pd, httpVersion, value, logger); |
115 | } | |
116 | ||
117 | private Method extractMethod(String methodString) { | |
118 | try { | |
119 |
1
1. extractMethod : replaced return value with null for com/renomad/minum/web/RequestLine::extractMethod → KILLED |
return Method.valueOf(methodString.toUpperCase(Locale.ROOT)); |
120 | } catch (Exception ex) { | |
121 | logger.logDebug(() -> "Unable to convert method to enum: " + methodString); | |
122 |
1
1. extractMethod : replaced return value with null for com/renomad/minum/web/RequestLine::extractMethod → TIMED_OUT |
return Method.NONE; |
123 | } | |
124 | } | |
125 | ||
126 | private PathDetails extractPathDetails(String path) { | |
127 | PathDetails pd; | |
128 | int locationOfQueryBegin = path.indexOf("?"); | |
129 |
2
1. extractPathDetails : negated conditional → KILLED 2. extractPathDetails : changed conditional boundary → KILLED |
if (locationOfQueryBegin > 0) { |
130 | // in this case, we found a question mark, suggesting that a query string exists | |
131 |
1
1. extractPathDetails : Replaced integer addition with subtraction → KILLED |
String rawQueryString = path.substring(locationOfQueryBegin + 1); |
132 | String isolatedPath = path.substring(0, locationOfQueryBegin); | |
133 | Map<String, String> queryString = extractMapFromQueryString(rawQueryString); | |
134 | pd = new PathDetails(isolatedPath, rawQueryString, queryString); | |
135 | } else { | |
136 | // in this case, no question mark was found, thus no query string | |
137 | pd = new PathDetails(path, null, null); | |
138 | } | |
139 |
1
1. extractPathDetails : replaced return value with null for com/renomad/minum/web/RequestLine::extractPathDetails → KILLED |
return pd; |
140 | } | |
141 | ||
142 | ||
143 | /** | |
144 | * Given a string containing the combined key-values in | |
145 | * a query string (e.g. foo=bar&name=alice), split that | |
146 | * into a map of the key to value (e.g. foo to bar, and name to alice) | |
147 | */ | |
148 | Map<String, String> extractMapFromQueryString(String rawQueryString) { | |
149 | Map<String, String> queryStrings = new HashMap<>(); | |
150 | StringTokenizer tokenizer = new StringTokenizer(rawQueryString, "&"); | |
151 | // we'll only take less than MAX_QUERY_STRING_KEYS_COUNT | |
152 |
2
1. extractMapFromQueryString : Changed increment from 1 to -1 → KILLED 2. extractMapFromQueryString : negated conditional → KILLED |
for (int i = 0; tokenizer.hasMoreTokens(); i++) { |
153 |
2
1. extractMapFromQueryString : changed conditional boundary → SURVIVED 2. extractMapFromQueryString : negated conditional → TIMED_OUT |
if (i >= MAX_QUERY_STRING_KEYS_COUNT) throw new ForbiddenUseException("User tried providing too many query string keys. max: " + MAX_QUERY_STRING_KEYS_COUNT); |
154 | // this should give us a key and value joined with an equal sign, e.g. foo=bar | |
155 | String currentKeyValue = tokenizer.nextToken(); | |
156 | int equalSignLocation = currentKeyValue.indexOf("="); | |
157 |
2
1. extractMapFromQueryString : changed conditional boundary → TIMED_OUT 2. extractMapFromQueryString : negated conditional → KILLED |
if (equalSignLocation <= 0) return Map.of(); |
158 | String key = currentKeyValue.substring(0, equalSignLocation); | |
159 |
1
1. extractMapFromQueryString : Replaced integer addition with subtraction → TIMED_OUT |
String value = StringUtils.decode(currentKeyValue.substring(equalSignLocation + 1)); |
160 | queryStrings.put(key, value); | |
161 | } | |
162 |
1
1. extractMapFromQueryString : replaced return value with Collections.emptyMap for com/renomad/minum/web/RequestLine::extractMapFromQueryString → TIMED_OUT |
return queryStrings; |
163 | } | |
164 | ||
165 | /** | |
166 | * Extract the HTTP version from the start line | |
167 | */ | |
168 | private HttpVersion getHttpVersion(String version) { | |
169 |
1
1. getHttpVersion : negated conditional → KILLED |
if (version.equals("1.1")) { |
170 |
1
1. getHttpVersion : replaced return value with null for com/renomad/minum/web/RequestLine::getHttpVersion → TIMED_OUT |
return HttpVersion.ONE_DOT_ONE; |
171 | } else { | |
172 |
1
1. getHttpVersion : replaced return value with null for com/renomad/minum/web/RequestLine::getHttpVersion → KILLED |
return HttpVersion.ONE_DOT_ZERO; |
173 | } | |
174 | } | |
175 | ||
176 | /** | |
177 | * Return the method of this request-line. For example, GET, PUT, POST... | |
178 | */ | |
179 | public Method getMethod() { | |
180 |
1
1. getMethod : replaced return value with null for com/renomad/minum/web/RequestLine::getMethod → KILLED |
return method; |
181 | } | |
182 | ||
183 | /** | |
184 | * This returns an object which contains essential information about the path | |
185 | * in the request line. For example, if the request line is "GET /sample?foo=bar HTTP/1.1", | |
186 | * this would hold data for the path ("sample") and the query string ("foo=bar") | |
187 | */ | |
188 | public PathDetails getPathDetails() { | |
189 |
1
1. getPathDetails : replaced return value with null for com/renomad/minum/web/RequestLine::getPathDetails → KILLED |
return pathDetails; |
190 | } | |
191 | ||
192 | /** | |
193 | * Gets the HTTP version, either 1.0 or 1.1 | |
194 | */ | |
195 | public HttpVersion getVersion() { | |
196 |
1
1. getVersion : replaced return value with null for com/renomad/minum/web/RequestLine::getVersion → KILLED |
return this.version; |
197 | } | |
198 | ||
199 | /** | |
200 | * Get the string value of this request line, such as "GET /sample.html HTTP/1.1" | |
201 | */ | |
202 | public String getRawValue() { | |
203 |
1
1. getRawValue : replaced return value with "" for com/renomad/minum/web/RequestLine::getRawValue → KILLED |
return rawValue; |
204 | } | |
205 | ||
206 | @Override | |
207 | public boolean equals(Object o) { | |
208 |
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; |
209 |
3
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 |
if (o == null || getClass() != o.getClass()) return false; |
210 | RequestLine that = (RequestLine) o; | |
211 |
6
1. equals : negated conditional → TIMED_OUT 2. equals : negated conditional → TIMED_OUT 3. equals : negated conditional → TIMED_OUT 4. equals : negated conditional → TIMED_OUT 5. equals : replaced boolean return with true for com/renomad/minum/web/RequestLine::equals → 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); |
212 | } | |
213 | ||
214 | @Override | |
215 | public int hashCode() { | |
216 |
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); |
217 | } | |
218 | ||
219 | @Override | |
220 | public String toString() { | |
221 |
1
1. toString : replaced return value with "" for com/renomad/minum/web/RequestLine::toString → KILLED |
return "RequestLine{" + |
222 | "method=" + method + | |
223 | ", pathDetails=" + pathDetails + | |
224 | ", version=" + version + | |
225 | ", rawValue='" + rawValue + '\'' + | |
226 | ", logger=" + logger + | |
227 | '}'; | |
228 | } | |
229 | } | |
Mutations | ||
70 |
1.1 2.2 |
|
71 |
1.1 |
|
73 |
1.1 |
|
107 |
1.1 |
|
108 |
1.1 |
|
114 |
1.1 |
|
119 |
1.1 |
|
122 |
1.1 |
|
129 |
1.1 2.2 |
|
131 |
1.1 |
|
139 |
1.1 |
|
152 |
1.1 2.2 |
|
153 |
1.1 2.2 |
|
157 |
1.1 2.2 |
|
159 |
1.1 |
|
162 |
1.1 |
|
169 |
1.1 |
|
170 |
1.1 |
|
172 |
1.1 |
|
180 |
1.1 |
|
189 |
1.1 |
|
196 |
1.1 |
|
203 |
1.1 |
|
208 |
1.1 2.2 |
|
209 |
1.1 2.2 3.3 |
|
211 |
1.1 2.2 3.3 4.4 5.5 6.6 |
|
216 |
1.1 |
|
221 |
1.1 |