1 | package com.renomad.minum.web; | |
2 | ||
3 | import com.renomad.minum.security.ForbiddenUseException; | |
4 | ||
5 | import java.io.IOException; | |
6 | import java.io.InputStream; | |
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.mustBeTrue; | |
12 | ||
13 | /** | |
14 | * Details extracted from the headers. For example, | |
15 | * is this a keep-alive connection? what is the content-length, | |
16 | * and so on. | |
17 | * Here is some detail from <a href="https://en.wikipedia.org/wiki/List_of_HTTP_header_fields">Wikipedia</a> on the subject: | |
18 | * <p> | |
19 | * HTTP header fields are a list of strings sent and received by both | |
20 | * the client program and server on every HTTP request and response. These | |
21 | * headers are usually invisible to the end-user and are only processed or | |
22 | * logged by the server and client applications. They define how information | |
23 | * sent/received through the connection are encoded (as in Content-Encoding), | |
24 | * the session verification and identification of the client (as in browser | |
25 | * cookies, IP address, user-agent) or their anonymity thereof (VPN or | |
26 | * proxy masking, user-agent spoofing), how the server should handle data | |
27 | * (as in Do-Not-Track), the age (the time it has resided in a shared cache) | |
28 | * of the document being downloaded, amongst others. | |
29 | * </p> | |
30 | */ | |
31 | public final class Headers{ | |
32 | ||
33 | public static final Headers EMPTY = new Headers(List.of()); | |
34 | private static final int MAX_HEADERS_COUNT = 70; | |
35 | ||
36 | /** | |
37 | * Each line of the headers is read into this data structure | |
38 | */ | |
39 | private final List<String> headerStrings; | |
40 | private final Map<String, List<String>> headersMap; | |
41 | ||
42 | public Headers( | |
43 | List<String> headerStrings | |
44 | ) { | |
45 | this.headerStrings = new ArrayList<>(headerStrings); | |
46 | this.headersMap = Collections.unmodifiableMap(extractHeadersToMap(headerStrings)); | |
47 | } | |
48 | ||
49 | public List<String> getHeaderStrings() { | |
50 |
1
1. getHeaderStrings : replaced return value with Collections.emptyList for com/renomad/minum/web/Headers::getHeaderStrings → KILLED |
return new ArrayList<>(headerStrings); |
51 | } | |
52 | ||
53 | /** | |
54 | * Used for extracting the length of the body, in POSTs and | |
55 | * responses from servers | |
56 | */ | |
57 | private static final Pattern contentLengthRegex = Pattern.compile("^[cC]ontent-[lL]ength: (.*)$"); | |
58 | ||
59 | /** | |
60 | * Obtain any desired header by looking it up in this map. All keys | |
61 | * are made lowercase. | |
62 | */ | |
63 | static Map<String, List<String>> extractHeadersToMap(List<String> headerStrings) { | |
64 | var result = new HashMap<String, List<String>>(); | |
65 | for (var h : headerStrings) { | |
66 | var indexOfFirstColon = h.indexOf(":"); | |
67 | ||
68 | // if the header is malformed, just move on | |
69 |
2
1. extractHeadersToMap : changed conditional boundary → TIMED_OUT 2. extractHeadersToMap : negated conditional → KILLED |
if (indexOfFirstColon <= 0) continue; |
70 | ||
71 | String key = h.substring(0, indexOfFirstColon).toLowerCase(Locale.ROOT); | |
72 |
1
1. extractHeadersToMap : Replaced integer addition with subtraction → KILLED |
String value = h.substring(indexOfFirstColon+1).trim(); |
73 | ||
74 |
1
1. extractHeadersToMap : negated conditional → KILLED |
if (result.containsKey(key)) { |
75 | var currentValue = result.get(key); | |
76 | List<String> newList = new ArrayList<>(); | |
77 | newList.add(value); | |
78 | newList.addAll(currentValue); | |
79 | result.put(key, newList); | |
80 | } else { | |
81 | result.put(key, List.of(value)); | |
82 | } | |
83 | ||
84 | } | |
85 |
1
1. extractHeadersToMap : replaced return value with Collections.emptyMap for com/renomad/minum/web/Headers::extractHeadersToMap → KILLED |
return result; |
86 | } | |
87 | ||
88 | /** | |
89 | * Gets the one content-type header, or returns an empty string | |
90 | */ | |
91 | public String contentType() { | |
92 | // find the header that starts with content-type | |
93 |
2
1. lambda$contentType$0 : replaced boolean return with true for com/renomad/minum/web/Headers::lambda$contentType$0 → KILLED 2. lambda$contentType$0 : replaced boolean return with false for com/renomad/minum/web/Headers::lambda$contentType$0 → KILLED |
List<String> cts = headerStrings.stream().filter(x -> x.toLowerCase(Locale.ROOT).startsWith("content-type")).toList(); |
94 |
2
1. contentType : changed conditional boundary → KILLED 2. contentType : negated conditional → KILLED |
if (cts.size() > 1) { |
95 | throw new WebServerException("The number of content-type headers must be exactly zero or one. Received: " + cts); | |
96 | } | |
97 |
1
1. contentType : negated conditional → KILLED |
if (!cts.isEmpty()) { |
98 |
1
1. contentType : replaced return value with "" for com/renomad/minum/web/Headers::contentType → KILLED |
return cts.getFirst(); |
99 | } | |
100 | ||
101 | // if we don't find a content-type header, or if we don't find one we can handle, return an empty string. | |
102 | return ""; | |
103 | } | |
104 | ||
105 | /** | |
106 | * Given the list of headers, find the one with the length of the | |
107 | * body of the POST and return that value as an integer. If | |
108 | * we do not find a content length, return -1. | |
109 | */ | |
110 | public int contentLength() { | |
111 |
2
1. lambda$contentLength$1 : replaced boolean return with true for com/renomad/minum/web/Headers::lambda$contentLength$1 → KILLED 2. lambda$contentLength$1 : replaced boolean return with false for com/renomad/minum/web/Headers::lambda$contentLength$1 → KILLED |
List<String> cl = headerStrings.stream().filter(x -> x.toLowerCase(Locale.ROOT).startsWith("content-length")).toList(); |
112 |
2
1. contentLength : changed conditional boundary → KILLED 2. contentLength : negated conditional → KILLED |
if (cl.size() > 1) { |
113 | throw new WebServerException("The number of content-length headers must be exactly zero or one. Received: " + cl); | |
114 | } | |
115 | int contentLength = -1; | |
116 |
1
1. contentLength : negated conditional → KILLED |
if (!cl.isEmpty()) { |
117 | Matcher clMatcher = contentLengthRegex.matcher(cl.getFirst()); | |
118 | mustBeTrue(clMatcher.matches(), "The content length header value must match the contentLengthRegex"); | |
119 | contentLength = Integer.parseInt(clMatcher.group(1)); | |
120 | mustBeTrue(contentLength >= 0, "Content-length cannot be negative"); | |
121 | } | |
122 | ||
123 |
1
1. contentLength : replaced int return with 0 for com/renomad/minum/web/Headers::contentLength → KILLED |
return contentLength; |
124 | } | |
125 | ||
126 | /** | |
127 | * Indicates whether the headers in this request | |
128 | * have a Connection: Keep-Alive | |
129 | */ | |
130 | public boolean hasKeepAlive() { | |
131 | List<String> connectionHeader = headersMap.get("connection"); | |
132 |
2
1. hasKeepAlive : negated conditional → KILLED 2. hasKeepAlive : replaced boolean return with true for com/renomad/minum/web/Headers::hasKeepAlive → KILLED |
if (connectionHeader == null) return false; |
133 |
4
1. hasKeepAlive : replaced boolean return with true for com/renomad/minum/web/Headers::hasKeepAlive → TIMED_OUT 2. lambda$hasKeepAlive$2 : replaced boolean return with true for com/renomad/minum/web/Headers::lambda$hasKeepAlive$2 → KILLED 3. hasKeepAlive : replaced boolean return with false for com/renomad/minum/web/Headers::hasKeepAlive → KILLED 4. lambda$hasKeepAlive$2 : replaced boolean return with false for com/renomad/minum/web/Headers::lambda$hasKeepAlive$2 → KILLED |
return connectionHeader.stream().anyMatch(x -> x.toLowerCase(Locale.ROOT).contains("keep-alive")); |
134 | } | |
135 | ||
136 | /** | |
137 | * Indicates whether the headers in this request | |
138 | * have a Connection: close | |
139 | */ | |
140 | public boolean hasConnectionClose() { | |
141 | List<String> connectionHeader = headersMap.get("connection"); | |
142 |
2
1. hasConnectionClose : replaced boolean return with true for com/renomad/minum/web/Headers::hasConnectionClose → KILLED 2. hasConnectionClose : negated conditional → KILLED |
if (connectionHeader == null) return false; |
143 |
4
1. lambda$hasConnectionClose$3 : replaced boolean return with true for com/renomad/minum/web/Headers::lambda$hasConnectionClose$3 → KILLED 2. hasConnectionClose : replaced boolean return with false for com/renomad/minum/web/Headers::hasConnectionClose → KILLED 3. hasConnectionClose : replaced boolean return with true for com/renomad/minum/web/Headers::hasConnectionClose → KILLED 4. lambda$hasConnectionClose$3 : replaced boolean return with false for com/renomad/minum/web/Headers::lambda$hasConnectionClose$3 → KILLED |
return connectionHeader.stream().anyMatch(x -> x.toLowerCase(Locale.ROOT).contains("close")); |
144 | } | |
145 | ||
146 | /** | |
147 | * Loop through the lines of header in the HTTP message | |
148 | */ | |
149 | static List<String> getAllHeaders(InputStream is, IInputStreamUtils inputStreamUtils) { | |
150 | List<String> headers = new ArrayList<>(); | |
151 |
1
1. getAllHeaders : Changed increment from 1 to -1 → TIMED_OUT |
for (int i = 0;; i++) { |
152 |
2
1. getAllHeaders : changed conditional boundary → SURVIVED 2. getAllHeaders : negated conditional → KILLED |
if (i >=MAX_HEADERS_COUNT) { |
153 | throw new ForbiddenUseException("User tried sending too many headers. max: " + MAX_HEADERS_COUNT); | |
154 | } | |
155 | String value; | |
156 | try { | |
157 | value = inputStreamUtils.readLine(is); | |
158 | } catch (IOException e) { | |
159 | throw new WebServerException(e); | |
160 | } | |
161 |
2
1. getAllHeaders : negated conditional → KILLED 2. getAllHeaders : negated conditional → KILLED |
if (value != null && value.isBlank()) { |
162 | break; | |
163 |
1
1. getAllHeaders : negated conditional → KILLED |
} else if (value == null) { |
164 |
1
1. getAllHeaders : replaced return value with Collections.emptyList for com/renomad/minum/web/Headers::getAllHeaders → SURVIVED |
return headers; |
165 | } else { | |
166 | headers.add(value); | |
167 | } | |
168 | } | |
169 |
1
1. getAllHeaders : replaced return value with Collections.emptyList for com/renomad/minum/web/Headers::getAllHeaders → KILLED |
return headers; |
170 | } | |
171 | ||
172 | /** | |
173 | * Allows a user to obtain any header value by its key, case-insensitively | |
174 | * @return a {@link List} of string values, or null | |
175 | * if no header was found. | |
176 | */ | |
177 | public List<String> valueByKey(String key) { | |
178 |
1
1. valueByKey : replaced return value with Collections.emptyList for com/renomad/minum/web/Headers::valueByKey → KILLED |
return headersMap.get(key.toLowerCase(Locale.ROOT)); |
179 | } | |
180 | ||
181 | @Override | |
182 | public boolean equals(Object o) { | |
183 |
2
1. equals : negated conditional → TIMED_OUT 2. equals : replaced boolean return with false for com/renomad/minum/web/Headers::equals → TIMED_OUT |
if (this == o) return true; |
184 |
3
1. equals : replaced boolean return with true for com/renomad/minum/web/Headers::equals → TIMED_OUT 2. equals : negated conditional → KILLED 3. equals : negated conditional → KILLED |
if (o == null || getClass() != o.getClass()) return false; |
185 | Headers headers = (Headers) o; | |
186 |
3
1. equals : replaced boolean return with true for com/renomad/minum/web/Headers::equals → TIMED_OUT 2. equals : negated conditional → KILLED 3. equals : negated conditional → KILLED |
return Objects.equals(headerStrings, headers.headerStrings) && Objects.equals(headersMap, headers.headersMap); |
187 | } | |
188 | ||
189 | @Override | |
190 | public int hashCode() { | |
191 |
1
1. hashCode : replaced int return with 0 for com/renomad/minum/web/Headers::hashCode → TIMED_OUT |
return Objects.hash(headerStrings, headersMap); |
192 | } | |
193 | ||
194 | @Override | |
195 | public String toString() { | |
196 |
1
1. toString : replaced return value with "" for com/renomad/minum/web/Headers::toString → KILLED |
return "Headers{" + |
197 | "headerStrings=" + headerStrings + | |
198 | '}'; | |
199 | } | |
200 | } | |
Mutations | ||
50 |
1.1 |
|
69 |
1.1 2.2 |
|
72 |
1.1 |
|
74 |
1.1 |
|
85 |
1.1 |
|
93 |
1.1 2.2 |
|
94 |
1.1 2.2 |
|
97 |
1.1 |
|
98 |
1.1 |
|
111 |
1.1 2.2 |
|
112 |
1.1 2.2 |
|
116 |
1.1 |
|
123 |
1.1 |
|
132 |
1.1 2.2 |
|
133 |
1.1 2.2 3.3 4.4 |
|
142 |
1.1 2.2 |
|
143 |
1.1 2.2 3.3 4.4 |
|
151 |
1.1 |
|
152 |
1.1 2.2 |
|
161 |
1.1 2.2 |
|
163 |
1.1 |
|
164 |
1.1 |
|
169 |
1.1 |
|
178 |
1.1 |
|
183 |
1.1 2.2 |
|
184 |
1.1 2.2 3.3 |
|
186 |
1.1 2.2 3.3 |
|
191 |
1.1 |
|
196 |
1.1 |