Headers.java

1
package com.renomad.minum.web;
2
3
import com.renomad.minum.security.ForbiddenUseException;
4
import com.renomad.minum.utils.StringUtils;
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());
33
    private static final int MAX_HEADERS_COUNT = 70;
34
    private Long 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
42
    public Headers(List<String> headerStrings) {
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, make the user know
61 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);
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"), new ArrayList<>());
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()); // sorting so our error message is consistent
88
            throw new BadRequestException("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 long contentLength() {
104
        // if we have a saved value for content length, use that
105 2 1. contentLength : replaced long return with 0 for com/renomad/minum/web/Headers::contentLength → TIMED_OUT
2. contentLength : negated conditional → KILLED
        if (contentLength != null) return contentLength;
106
107
        List<String> cl = Objects.requireNonNullElse(headersMap.get("content-length"), List.of());
108 1 1. contentLength : negated conditional → KILLED
        if (cl.isEmpty()) {
109
            contentLength = -1L;
110 2 1. contentLength : negated conditional → KILLED
2. contentLength : changed conditional boundary → KILLED
        } else if (cl.size() > 1) {
111
            throw new BadRequestException("Received multiple content-length headers, which does not make sense.  Received: " + cl);
112
        } else {
113
            String contentLengthString = cl.getFirst();
114
            try {
115 1 1. contentLength : negated conditional → KILLED
                if (!StringUtils.isPureDigits(contentLengthString)) {
116
                    throw new BadRequestException("Content length was not pure ascii digits.  Received: " + contentLengthString);
117
                }
118
                contentLength = Long.parseLong(contentLengthString);
119
            } catch (NumberFormatException ex) {
120
                throw new BadRequestException("NumberFormatException thrown on : " + contentLengthString, ex);
121
            }
122
        }
123 1 1. contentLength : replaced long 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 → 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"));
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 → TIMED_OUT
2. hasConnectionClose : negated conditional → KILLED
        if (connectionHeader == null) return false;
143 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"));
144
    }
145
146
    /**
147
     * Loop through the lines of header in the HTTP message
148
     */
149
    static List<String> getAllHeaders(InputStream is, IInputStreamUtils inputStreamUtils) throws IOException {
150
        // we'll give the list an initial size, since in most cases we're going to have headers.
151
        // 10 is just an arbitrary number, seems about right.
152
        List<String> headers = new ArrayList<>(10);
153 1 1. getAllHeaders : Changed increment from 1 to -1 → TIMED_OUT
        for (int i = 0;; i++) {
154 2 1. getAllHeaders : changed conditional boundary → TIMED_OUT
2. getAllHeaders : negated conditional → KILLED
            if (i >=MAX_HEADERS_COUNT) {
155
                throw new ForbiddenUseException("User tried sending too many headers.  max: " + MAX_HEADERS_COUNT);
156
            }
157
            String value;
158
            value = inputStreamUtils.readLine(is);
159 2 1. getAllHeaders : negated conditional → KILLED
2. getAllHeaders : negated conditional → KILLED
            if (value != null && value.isBlank()) {
160
                break;
161 1 1. getAllHeaders : negated conditional → KILLED
            } else if (value == null) {
162 1 1. getAllHeaders : replaced return value with Collections.emptyList for com/renomad/minum/web/Headers::getAllHeaders → TIMED_OUT
                return headers;
163
            } else {
164
                headers.add(value);
165
            }
166
        }
167 1 1. getAllHeaders : replaced return value with Collections.emptyList for com/renomad/minum/web/Headers::getAllHeaders → KILLED
        return headers;
168
    }
169
170
    /**
171
     * Returns true if there is no header data in this instance.
172
     */
173
    public boolean isEmpty() {
174 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();
175
    }
176
177
    /**
178
     * Allows a user to obtain any header value by its key, case-insensitively
179
     * @return a {@link List} of string values, or null
180
     * if no header was found.
181
     */
182
    public List<String> valueByKey(String key) {
183 1 1. valueByKey : replaced return value with Collections.emptyList for com/renomad/minum/web/Headers::valueByKey → KILLED
        return headersMap.get(key.toLowerCase(Locale.ROOT));
184
    }
185
186
    @Override
187
    public boolean equals(Object o) {
188 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;
189
        Headers headers = (Headers) o;
190 4 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
        return Objects.equals(contentLength, headers.contentLength) && Objects.equals(headerStrings, headers.headerStrings) && Objects.equals(headersMap, headers.headersMap);
191
    }
192
193
    @Override
194
    public int hashCode() {
195 1 1. hashCode : replaced int return with 0 for com/renomad/minum/web/Headers::hashCode → TIMED_OUT
        return Objects.hash(contentLength, headerStrings, headersMap);
196
    }
197
198
    @Override
199
    public String toString() {
200 1 1. toString : replaced return value with "" for com/renomad/minum/web/Headers::toString → KILLED
        return "Headers{" +
201
                "headerStrings=" + headerStrings +
202
                '}';
203
    }
204
205
    /**
206
     * This is used in the WebFramework when building a Response string,
207
     * to avoid needing to create a copy of the headers list when preparing
208
     * to send.
209
     */
210
    void appendHeadersToBuilder(StringBuilder sb) {
211
        for (String header : headerStrings) {
212
            sb.append(header).append(HTTP_CRLF);
213
        }
214
    }
215
}

Mutations

48

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

61

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

64

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

66

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

77

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

86

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

87

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

90

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

91

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

105

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

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

108

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

110

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

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

115

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

123

1.1
Location : contentLength
Killed by : com.renomad.minum.web.BodyProcessorTests
replaced long 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 : 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

142

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

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

143

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

153

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

154

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

159

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

161

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

162

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

167

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

174

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

183

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

188

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 : none
replaced boolean return with true for com/renomad/minum/web/Headers::equals → TIMED_OUT

190

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

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

195

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

200

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