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

63

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

66

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

68

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

79

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

88

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

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

89

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

92

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

93

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

107

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

110

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

112

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

117

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

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

122

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

131

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

132

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

141

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

142

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

152

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

153

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

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

162

1.1
Location : getAllHeaders
Killed by : com.renomad.minum.web.HeadersTests.test_GetAllHeaders_EdgeCase_ValueIsNull(com.renomad.minum.web.HeadersTests)
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

164

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

165

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

170

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

179

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

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 : com.renomad.minum.web.RequestTests.testSimplerRequest(com.renomad.minum.web.RequestTests)
negated conditional → KILLED

3.3
Location : equals
Killed by : com.renomad.minum.web.RequestTests.testSimplerRequest(com.renomad.minum.web.RequestTests)
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.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