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