| 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 → TIMED_OUT 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 : changed conditional boundary → TIMED_OUT 2. getAllHeaders : negated conditional → 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 → KILLED 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 → KILLED 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 |