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
9
import static com.renomad.minum.web.WebEngine.HTTP_CRLF;
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
    private Integer contentLength;
34
35
    /**
36
     * Each line of the headers is read into this data structure
37
     */
38
    private final List<String> headerStrings;
39
    private final Map<String, List<String>> headersMap;
40
41
    public Headers(List<String> headerStrings) {
42
        this.headerStrings = new ArrayList<>(headerStrings);
43
        this.headersMap = Collections.unmodifiableMap(extractHeadersToMap(headerStrings));
44
    }
45
46
    public List<String> getHeaderStrings() {
47 1 1. getHeaderStrings : replaced return value with Collections.emptyList for com/renomad/minum/web/Headers::getHeaderStrings → KILLED
        return new ArrayList<>(headerStrings);
48
    }
49
50
    /**
51
     * Obtain any desired header by looking it up in this map.  All keys
52
     * are made lowercase.
53
     */
54
    static Map<String, List<String>> extractHeadersToMap(List<String> headerStrings) {
55
        var result = new HashMap<String, List<String>>();
56
        for (var h : headerStrings) {
57
            var indexOfFirstColon = h.indexOf(":");
58
59
            // if the header is malformed, make the user know
60 2 1. extractHeadersToMap : changed conditional boundary → TIMED_OUT
2. extractHeadersToMap : negated conditional → KILLED
            if (indexOfFirstColon <= 0) throw new BadRequestException("Invalid formatting on header in request, was expecting to find a colon separating key from value: " + h);
61
62
            String key = h.substring(0, indexOfFirstColon).toLowerCase(Locale.ROOT);
63 1 1. extractHeadersToMap : Replaced integer addition with subtraction → KILLED
            String value = h.substring(indexOfFirstColon+1).trim();
64
65 1 1. extractHeadersToMap : negated conditional → KILLED
            if (result.containsKey(key)) {
66
                var currentValue = result.get(key);
67
                List<String> newList = new ArrayList<>();
68
                newList.add(value);
69
                newList.addAll(currentValue);
70
                result.put(key, newList);
71
            } else {
72
                result.put(key, List.of(value));
73
            }
74
75
        }
76 1 1. extractHeadersToMap : replaced return value with Collections.emptyMap for com/renomad/minum/web/Headers::extractHeadersToMap → KILLED
        return result;
77
    }
78
79
    /**
80
     * Gets the one content-type header, or returns an empty string
81
     */
82
    public String contentType() {
83
        // find the header that starts with content-type
84
        List<String> cts = Objects.requireNonNullElse(headersMap.get("content-type"), new ArrayList<>());
85 2 1. contentType : changed conditional boundary → KILLED
2. contentType : negated conditional → KILLED
        if (cts.size() > 1) {
86 1 1. contentType : removed call to java/util/List::sort → SURVIVED
            cts.sort(Comparator.naturalOrder()); // sorting so our error message is consistent
87
            throw new BadRequestException("The number of content-type headers must be exactly zero or one.  Received: " + cts);
88
        }
89 1 1. contentType : negated conditional → KILLED
        if (!cts.isEmpty()) {
90 1 1. contentType : replaced return value with "" for com/renomad/minum/web/Headers::contentType → KILLED
            return cts.getFirst();
91
        }
92
93
        // if we don't find a content-type header, or if we don't find one we can handle, return an empty string.
94
        return "";
95
    }
96
97
    /**
98
     * Given the list of headers, find the one with the length of the
99
     * body of the POST and return that value as an integer. If
100
     * we do not find a content length, return -1.
101
     */
102
    public int contentLength() {
103
        // if we have a saved value for content length, use that
104 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;
105
106
        List<String> cl = Objects.requireNonNullElse(headersMap.get("content-length"), List.of());
107 1 1. contentLength : negated conditional → KILLED
        if (cl.isEmpty()) {
108
            contentLength = -1;
109 2 1. contentLength : changed conditional boundary → KILLED
2. contentLength : negated conditional → KILLED
        } else if (cl.size() > 1) {
110
            throw new BadRequestException("Received multiple content-length headers, which does not make sense.  Received: " + cl);
111
        } else {
112
            try {
113
                contentLength = Integer.parseInt(cl.getFirst());
114
            } catch (NumberFormatException ex) {
115
                throw new BadRequestException("Received a non-numeric content length value. Received: " + cl.getFirst(), ex);
116
            }
117 2 1. contentLength : changed conditional boundary → TIMED_OUT
2. contentLength : negated conditional → KILLED
            if (contentLength < 0) {
118
                throw new BadRequestException("Content length cannot be negative.  Received: " + contentLength);
119
            }
120
        }
121 1 1. contentLength : replaced int return with 0 for com/renomad/minum/web/Headers::contentLength → KILLED
        return contentLength;
122
    }
123
124
    /**
125
     * Indicates whether the headers in this request
126
     * have a Connection: Keep-Alive
127
     */
128
    public boolean hasKeepAlive() {
129
        List<String> connectionHeader = headersMap.get("connection");
130 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;
131 4 1. hasKeepAlive : replaced boolean return with true for com/renomad/minum/web/Headers::hasKeepAlive → KILLED
2. hasKeepAlive : replaced boolean return with false for com/renomad/minum/web/Headers::hasKeepAlive → KILLED
3. lambda$hasKeepAlive$0 : replaced boolean return with false for com/renomad/minum/web/Headers::lambda$hasKeepAlive$0 → KILLED
4. lambda$hasKeepAlive$0 : replaced boolean return with true for com/renomad/minum/web/Headers::lambda$hasKeepAlive$0 → KILLED
        return connectionHeader.stream().anyMatch(x -> x.toLowerCase(Locale.ROOT).contains("keep-alive"));
132
    }
133
134
    /**
135
     * Indicates whether the headers in this request
136
     * have a Connection: close
137
     */
138
    public boolean hasConnectionClose() {
139
        List<String> connectionHeader = headersMap.get("connection");
140 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;
141 4 1. lambda$hasConnectionClose$1 : replaced boolean return with false for com/renomad/minum/web/Headers::lambda$hasConnectionClose$1 → 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$1 : replaced boolean return with true for com/renomad/minum/web/Headers::lambda$hasConnectionClose$1 → KILLED
        return connectionHeader.stream().anyMatch(x -> x.toLowerCase(Locale.ROOT).contains("close"));
142
    }
143
144
    /**
145
     * Loop through the lines of header in the HTTP message
146
     */
147
    static List<String> getAllHeaders(InputStream is, IInputStreamUtils inputStreamUtils) throws IOException {
148
        // we'll give the list an initial size, since in most cases we're going to have headers.
149
        // 10 is just an arbitrary number, seems about right.
150
        List<String> headers = new ArrayList<>(10);
151 1 1. getAllHeaders : Changed increment from 1 to -1 → TIMED_OUT
        for (int i = 0;; i++) {
152 2 1. getAllHeaders : changed conditional boundary → TIMED_OUT
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
            value = inputStreamUtils.readLine(is);
157 2 1. getAllHeaders : negated conditional → KILLED
2. getAllHeaders : negated conditional → KILLED
            if (value != null && value.isBlank()) {
158
                break;
159 1 1. getAllHeaders : negated conditional → KILLED
            } else if (value == null) {
160 1 1. getAllHeaders : replaced return value with Collections.emptyList for com/renomad/minum/web/Headers::getAllHeaders → TIMED_OUT
                return headers;
161
            } else {
162
                headers.add(value);
163
            }
164
        }
165 1 1. getAllHeaders : replaced return value with Collections.emptyList for com/renomad/minum/web/Headers::getAllHeaders → KILLED
        return headers;
166
    }
167
168
    /**
169
     * Returns true if there is no header data in this instance.
170
     */
171
    public boolean isEmpty() {
172 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();
173
    }
174
175
    /**
176
     * Allows a user to obtain any header value by its key, case-insensitively
177
     * @return a {@link List} of string values, or null
178
     * if no header was found.
179
     */
180
    public List<String> valueByKey(String key) {
181 1 1. valueByKey : replaced return value with Collections.emptyList for com/renomad/minum/web/Headers::valueByKey → TIMED_OUT
        return headersMap.get(key.toLowerCase(Locale.ROOT));
182
    }
183
184
    @Override
185
    public boolean equals(Object o) {
186 3 1. equals : negated conditional → KILLED
2. equals : negated conditional → KILLED
3. equals : replaced boolean return with true for com/renomad/minum/web/Headers::equals → KILLED
        if (o == null || getClass() != o.getClass()) return false;
187
        Headers headers = (Headers) o;
188 4 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
4. equals : negated conditional → KILLED
        return Objects.equals(contentLength, headers.contentLength) && Objects.equals(headerStrings, headers.headerStrings) && Objects.equals(headersMap, headers.headersMap);
189
    }
190
191
    @Override
192
    public int hashCode() {
193 1 1. hashCode : replaced int return with 0 for com/renomad/minum/web/Headers::hashCode → KILLED
        return Objects.hash(contentLength, headerStrings, headersMap);
194
    }
195
196
    @Override
197
    public String toString() {
198 1 1. toString : replaced return value with "" for com/renomad/minum/web/Headers::toString → KILLED
        return "Headers{" +
199
                "headerStrings=" + headerStrings +
200
                '}';
201
    }
202
203
    /**
204
     * This is used in the WebFramework when building a Response string,
205
     * to avoid needing to create a copy of the headers list when preparing
206
     * to send.
207
     */
208
    void appendHeadersToBuilder(StringBuilder sb) {
209
        for (String header : headerStrings) {
210
            sb.append(header).append(HTTP_CRLF);
211
        }
212
    }
213
}

Mutations

47

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

60

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

2.2
Location : extractHeadersToMap
Killed by : com.renomad.minum.web.EndpointTests
negated conditional → KILLED

63

1.1
Location : extractHeadersToMap
Killed by : com.renomad.minum.web.HeadersTests
Replaced integer addition with subtraction → KILLED

65

1.1
Location : extractHeadersToMap
Killed by : com.renomad.minum.web.EndpointTests
negated conditional → KILLED

76

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

85

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

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

86

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

89

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

90

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

104

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

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

107

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

109

1.1
Location : contentLength
Killed by : com.renomad.minum.web.HeadersTests
changed conditional boundary → KILLED

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

117

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

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

121

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

130

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

131

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 : hasKeepAlive
Killed by : com.renomad.minum.web.HeadersTests
replaced boolean return with false for com/renomad/minum/web/Headers::hasKeepAlive → KILLED

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

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

140

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
negated conditional → KILLED

141

1.1
Location : lambda$hasConnectionClose$1
Killed by : com.renomad.minum.web.HeadersTests
replaced boolean return with false for com/renomad/minum/web/Headers::lambda$hasConnectionClose$1 → 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$1
Killed by : com.renomad.minum.web.WebTests
replaced boolean return with true for com/renomad/minum/web/Headers::lambda$hasConnectionClose$1 → 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.HeadersTests
negated conditional → KILLED

2.2
Location : getAllHeaders
Killed by : none
changed conditional boundary → TIMED_OUT

157

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

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

159

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

160

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

165

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

172

1.1
Location : isEmpty
Killed by : 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

181

1.1
Location : valueByKey
Killed by : none
replaced return value with Collections.emptyList for com/renomad/minum/web/Headers::valueByKey → TIMED_OUT

186

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

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

3.3
Location : equals
Killed by : com.renomad.minum.EqualsTests.equalsTest(com.renomad.minum.EqualsTests)
replaced boolean return with true for com/renomad/minum/web/Headers::equals → KILLED

188

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

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

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

4.4
Location : equals
Killed by : com.renomad.minum.web.ResponseTests
negated conditional → KILLED

193

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

198

1.1
Location : toString
Killed by : 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