Headers.java

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
Location : getHeaderStrings
Killed by : com.renomad.minum.FunctionalTests
replaced return value with Collections.emptyList for com/renomad/minum/web/Headers::getHeaderStrings → KILLED

69

1.1
Location : extractHeadersToMap
Killed by : none
changed conditional boundary → TIMED_OUT

2.2
Location : extractHeadersToMap
Killed by : com.renomad.minum.web.RangeTests.test_MultipleRangeHeaders(com.renomad.minum.web.RangeTests)
negated conditional → KILLED

72

1.1
Location : extractHeadersToMap
Killed by : com.renomad.minum.web.RangeTests.test_DetermineLengthFromRangeHeader_EdgeCase_FirstLarger(com.renomad.minum.web.RangeTests)
Replaced integer addition with subtraction → KILLED

74

1.1
Location : extractHeadersToMap
Killed by : com.renomad.minum.web.RangeTests.test_MultipleRangeHeaders(com.renomad.minum.web.RangeTests)
negated conditional → KILLED

85

1.1
Location : extractHeadersToMap
Killed by : com.renomad.minum.web.RangeTests.test_MultipleRangeHeaders(com.renomad.minum.web.RangeTests)
replaced return value with Collections.emptyMap for com/renomad/minum/web/Headers::extractHeadersToMap → KILLED

93

1.1
Location : lambda$contentType$0
Killed by : com.renomad.minum.web.RequestTests.test_Request_getMultipartIterable_EdgeCase_No_Valid_Boundary(com.renomad.minum.web.RequestTests)
replaced boolean return with true for com/renomad/minum/web/Headers::lambda$contentType$0 → KILLED

2.2
Location : lambda$contentType$0
Killed by : com.renomad.minum.web.RequestTests.testSimplerRequest3(com.renomad.minum.web.RequestTests)
replaced boolean return with false for com/renomad/minum/web/Headers::lambda$contentType$0 → KILLED

94

1.1
Location : contentType
Killed by : com.renomad.minum.web.RequestTests.testSimplerRequest3(com.renomad.minum.web.RequestTests)
changed conditional boundary → KILLED

2.2
Location : contentType
Killed by : com.renomad.minum.web.RequestTests.testSimplerRequest3(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

97

1.1
Location : contentType
Killed by : com.renomad.minum.web.RequestTests.testSimplerRequest3(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

98

1.1
Location : contentType
Killed by : com.renomad.minum.web.RequestTests.testSimplerRequest3(com.renomad.minum.web.RequestTests)
replaced return value with "" for com/renomad/minum/web/Headers::contentType → KILLED

111

1.1
Location : lambda$contentLength$1
Killed by : com.renomad.minum.web.RequestTests.testSimplerRequest3(com.renomad.minum.web.RequestTests)
replaced boolean return with true for com/renomad/minum/web/Headers::lambda$contentLength$1 → KILLED

2.2
Location : lambda$contentLength$1
Killed by : com.renomad.minum.web.RequestTests.test_Request_BodyTooLong(com.renomad.minum.web.RequestTests)
replaced boolean return with false for com/renomad/minum/web/Headers::lambda$contentLength$1 → KILLED

112

1.1
Location : contentLength
Killed by : com.renomad.minum.web.RequestTests.test_Request_BodyTooLong(com.renomad.minum.web.RequestTests)
changed conditional boundary → KILLED

2.2
Location : contentLength
Killed by : com.renomad.minum.web.RequestTests.testSimplerRequest3(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

116

1.1
Location : contentLength
Killed by : com.renomad.minum.web.RequestTests.testSimplerRequest3(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

123

1.1
Location : contentLength
Killed by : com.renomad.minum.web.RequestTests.test_Request_BodyTooLong(com.renomad.minum.web.RequestTests)
replaced int return with 0 for com/renomad/minum/web/Headers::contentLength → KILLED

132

1.1
Location : hasKeepAlive
Killed by : com.renomad.minum.web.HeadersTests
negated conditional → KILLED

2.2
Location : hasKeepAlive
Killed by : com.renomad.minum.web.HeadersTests
replaced boolean return with true for com/renomad/minum/web/Headers::hasKeepAlive → KILLED

133

1.1
Location : hasKeepAlive
Killed by : none
replaced boolean return with true for com/renomad/minum/web/Headers::hasKeepAlive → TIMED_OUT

2.2
Location : lambda$hasKeepAlive$2
Killed by : com.renomad.minum.web.WebTests
replaced boolean return with true for com/renomad/minum/web/Headers::lambda$hasKeepAlive$2 → KILLED

3.3
Location : hasKeepAlive
Killed by : com.renomad.minum.web.HeadersTests
replaced boolean return with false for com/renomad/minum/web/Headers::hasKeepAlive → KILLED

4.4
Location : lambda$hasKeepAlive$2
Killed by : com.renomad.minum.web.HeadersTests
replaced boolean return with false for com/renomad/minum/web/Headers::lambda$hasKeepAlive$2 → KILLED

142

1.1
Location : hasConnectionClose
Killed by : com.renomad.minum.web.WebTests
replaced boolean return with true for com/renomad/minum/web/Headers::hasConnectionClose → KILLED

2.2
Location : hasConnectionClose
Killed by : com.renomad.minum.web.HeadersTests
negated conditional → KILLED

143

1.1
Location : lambda$hasConnectionClose$3
Killed by : com.renomad.minum.web.WebTests
replaced boolean return with true for com/renomad/minum/web/Headers::lambda$hasConnectionClose$3 → KILLED

2.2
Location : hasConnectionClose
Killed by : com.renomad.minum.web.HeadersTests
replaced boolean return with false for com/renomad/minum/web/Headers::hasConnectionClose → KILLED

3.3
Location : hasConnectionClose
Killed by : com.renomad.minum.web.WebTests
replaced boolean return with true for com/renomad/minum/web/Headers::hasConnectionClose → KILLED

4.4
Location : lambda$hasConnectionClose$3
Killed by : com.renomad.minum.web.HeadersTests
replaced boolean return with false for com/renomad/minum/web/Headers::lambda$hasConnectionClose$3 → KILLED

151

1.1
Location : getAllHeaders
Killed by : none
Changed increment from 1 to -1 → TIMED_OUT

152

1.1
Location : getAllHeaders
Killed by : com.renomad.minum.web.RequestTests.testReadingEmptyStreamingMultipart(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

2.2
Location : getAllHeaders
Killed by : none
changed conditional boundary → SURVIVED
Covering tests

161

1.1
Location : getAllHeaders
Killed by : com.renomad.minum.web.RequestTests.testReadingEmptyStreamingMultipart(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

2.2
Location : getAllHeaders
Killed by : com.renomad.minum.web.RequestTests.testReadingEmptyStreamingMultipart(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

163

1.1
Location : getAllHeaders
Killed by : com.renomad.minum.web.RequestTests.testReadingEmptyStreamingMultipart(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

164

1.1
Location : getAllHeaders
Killed by : none
replaced return value with Collections.emptyList for com/renomad/minum/web/Headers::getAllHeaders → SURVIVED
Covering tests

169

1.1
Location : getAllHeaders
Killed by : com.renomad.minum.web.RequestTests.testReadingEmptyStreamingMultipart(com.renomad.minum.web.RequestTests)
replaced return value with Collections.emptyList for com/renomad/minum/web/Headers::getAllHeaders → KILLED

178

1.1
Location : valueByKey
Killed by : com.renomad.minum.web.RangeTests.test_MultipleRangeHeaders(com.renomad.minum.web.RangeTests)
replaced return value with Collections.emptyList for com/renomad/minum/web/Headers::valueByKey → KILLED

183

1.1
Location : equals
Killed by : none
negated conditional → TIMED_OUT

2.2
Location : equals
Killed by : none
replaced boolean return with false for com/renomad/minum/web/Headers::equals → TIMED_OUT

184

1.1
Location : equals
Killed by : com.renomad.minum.web.RequestTests.testSimplerRequest(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

2.2
Location : equals
Killed by : com.renomad.minum.web.RequestTests.testSimplerRequest(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

3.3
Location : equals
Killed by : none
replaced boolean return with true for com/renomad/minum/web/Headers::equals → TIMED_OUT

186

1.1
Location : equals
Killed by : com.renomad.minum.web.RequestTests.testSimplerRequest(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

2.2
Location : equals
Killed by : none
replaced boolean return with true for com/renomad/minum/web/Headers::equals → TIMED_OUT

3.3
Location : equals
Killed by : com.renomad.minum.web.RequestTests.testSimplerRequest(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

191

1.1
Location : hashCode
Killed by : none
replaced int return with 0 for com/renomad/minum/web/Headers::hashCode → TIMED_OUT

196

1.1
Location : toString
Killed by : com.renomad.minum.web.RequestTests.test_Request_ToString(com.renomad.minum.web.RequestTests)
replaced return value with "" for com/renomad/minum/web/Headers::toString → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0