Headers.java

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

Mutations

52

1.1
Location : getHeaderStrings
Killed by : com.renomad.minum.web.WebFrameworkTests.test_readStaticFile_JS(com.renomad.minum.web.WebFrameworkTests)
replaced return value with Collections.emptyList for com/renomad/minum/web/Headers::getHeaderStrings → KILLED

65

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

68

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

70

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

81

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

90

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

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

91

1.1
Location : contentType
Killed by : none
removed call to java/util/List::sort → SURVIVED
Covering tests

94

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

95

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

109

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

2.2
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

112

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

114

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

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

119

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

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

124

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

133

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

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

134

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

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.test_HasKeepAlive(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.test_HasKeepAlive(com.renomad.minum.web.HeadersTests)
replaced boolean return with false for com/renomad/minum/web/Headers::lambda$hasKeepAlive$2 → KILLED

143

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

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

144

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.test_HasConnectionClose(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.test_HasConnectionClose(com.renomad.minum.web.HeadersTests)
replaced boolean return with false for com/renomad/minum/web/Headers::lambda$hasConnectionClose$3 → KILLED

154

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

155

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

2.2
Location : getAllHeaders
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
changed conditional boundary → KILLED

164

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

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

166

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

167

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

172

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

179

1.1
Location : isEmpty
Killed by : com.renomad.minum.web.HeadersTests.test_IsEmpty(com.renomad.minum.web.HeadersTests)
replaced boolean return with false for com/renomad/minum/web/Headers::isEmpty → KILLED

2.2
Location : isEmpty
Killed by : none
replaced boolean return with true for com/renomad/minum/web/Headers::isEmpty → SURVIVED
Covering tests

188

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

193

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

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

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

195

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

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

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

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

5.5
Location : equals
Killed by : com.renomad.minum.web.ResponseTests.testUseResponseAsKey(com.renomad.minum.web.ResponseTests)
negated conditional → KILLED

200

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

205

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

Active mutators

Tests examined


Report generated by PIT 1.17.0