WebFramework.java

1
package com.renomad.minum.web;
2
3
import com.renomad.minum.logging.ILogger;
4
import com.renomad.minum.security.ForbiddenUseException;
5
import com.renomad.minum.security.ITheBrig;
6
import com.renomad.minum.security.UnderInvestigation;
7
import com.renomad.minum.state.Constants;
8
import com.renomad.minum.state.Context;
9
import com.renomad.minum.utils.*;
10
11
import java.io.IOException;
12
import java.net.SocketException;
13
import java.net.SocketTimeoutException;
14
import java.nio.charset.StandardCharsets;
15
import java.nio.file.Files;
16
import java.nio.file.Path;
17
import java.time.ZoneId;
18
import java.time.ZonedDateTime;
19
import java.time.format.DateTimeFormatter;
20
import java.util.*;
21
22
import static com.renomad.minum.utils.FileUtils.checkFileIsWithinDirectory;
23
import static com.renomad.minum.utils.FileUtils.checkForBadFilePatterns;
24
import static com.renomad.minum.web.StatusLine.StatusCode.*;
25
import static com.renomad.minum.web.WebEngine.HTTP_CRLF;
26
27
/**
28
 * This class is responsible for the HTTP handling after socket connection.
29
 * <p>
30
 *     The public methods are for registering endpoints - code that will be
31
 *     run for a given combination of HTTP method and path.  See documentation
32
 *     for the methods in this class.
33
 * </p>
34
 */
35
public final class WebFramework {
36
37
    private final Constants constants;
38
    private final UnderInvestigation underInvestigation;
39
    private final IInputStreamUtils inputStreamUtils;
40
    private final IBodyProcessor bodyProcessor;
41
    /**
42
     * This is a variable storing a pseudo-random (non-secure) number
43
     * that is shown to users when a serious error occurs, which
44
     * will also be put in the logs, to make finding it easier.
45
     */
46
    private final Random randomErrorCorrelationId;
47
    private final RequestLine emptyRequestLine;
48
    private final RequestLine validRequestLine;
49
    private final ITheBrig theBrig;
50
51
    public Map<String,String> getSuffixToMimeMappings() {
52 1 1. getSuffixToMimeMappings : replaced return value with Collections.emptyMap for com/renomad/minum/web/WebFramework::getSuffixToMimeMappings → KILLED
        return new HashMap<>(fileSuffixToMime);
53
    }
54
55
    /**
56
     * This is used as a key when registering endpoints
57
     */
58
    record MethodPath(RequestLine.Method method, String path) { }
59
60
    /**
61
     * The list of paths that our system is registered to handle.
62
     */
63
    private final Map<MethodPath, ThrowingFunction<IRequest, IResponse>> registeredDynamicPaths;
64
65
    /**
66
     * These are registrations for paths that partially match, for example,
67
     * if the client sends us GET /.well-known/acme-challenge/HGr8U1IeTW4kY_Z6UIyaakzOkyQgPr_7ArlLgtZE8SX
68
     * and we want to match ".well-known/acme-challenge"
69
     */
70
    private final Map<MethodPath, ThrowingFunction<IRequest, IResponse>> registeredPartialPaths;
71
72
    /**
73
     * A function that will be run instead of the ordinary business code. Has
74
     * provisions for running the business code as well.  See {@link #registerPreHandler(ThrowingFunction)}
75
     */
76
    private ThrowingFunction<PreHandlerInputs, IResponse> preHandler;
77
78
    /**
79
     * A function run after the ordinary business code
80
     */
81
    private ThrowingFunction<LastMinuteHandlerInputs, IResponse> lastMinuteHandler;
82
83
    private final IFileReader fileReader;
84
    private final Map<String, String> fileSuffixToMime;
85
86
    // This is just used for testing.  If it's null, we use the real time.
87
    private final ZonedDateTime overrideForDateTime;
88
    private final FullSystem fs;
89
    private final ILogger logger;
90
91
    /**
92
     * This is the minimum number of bytes in a text response to apply gzip.
93
     */
94
    private static final int MINIMUM_NUMBER_OF_BYTES_TO_COMPRESS = 2048;
95
96
    void httpProcessing(ISocketWrapper sw) throws Exception {
97
        try (sw) {
98
            dumpIfAttacker(sw, fs);
99
            final var is = sw.getInputStream();
100
101
            // By default, browsers expect the server to run in keep-alive mode.
102
            // We'll break out later if we find that the browser doesn't do keep-alive
103
            while (true) {
104
                final String rawStartLine = inputStreamUtils.readLine(is);
105
                long startMillis = System.currentTimeMillis();
106 2 1. httpProcessing : negated conditional → KILLED
2. httpProcessing : negated conditional → KILLED
                if (rawStartLine == null || rawStartLine.isEmpty()) {
107
                    // here, the client connected, sent nothing, and closed.
108
                    // nothing to do but return.
109
                    logger.logTrace(() -> "rawStartLine was empty.  Returning.");
110
                    break;
111
                }
112
                final RequestLine requestLine = getProcessedRequestLine(sw, rawStartLine);
113
114 1 1. httpProcessing : negated conditional → KILLED
                if (requestLine.equals(emptyRequestLine)) {
115
                    // here, the client sent something we cannot parse.
116
                    // nothing to do but return.
117
                    logger.logTrace(() -> "RequestLine was unparseable.  Returning.");
118
                    break;
119
                }
120
                // check if the user is seeming to attack us.
121 1 1. httpProcessing : removed call to com/renomad/minum/web/WebFramework::checkIfSuspiciousPath → KILLED
                checkIfSuspiciousPath(sw, requestLine);
122
123
                // React to what the user requested, generate a result
124
                Headers headers = getHeaders(sw);
125
                boolean isKeepAlive = determineIfKeepAlive(requestLine, headers, logger);
126
                IRequest request = new Request(headers, requestLine, sw.getRemoteAddr(), sw, bodyProcessor);
127
                IResponse response = processRequest(request, sw, requestLine, headers);
128
129
                // check that the response is non-null.  If it is null, that suggests
130
                // the developer made a mistake.
131 1 1. httpProcessing : negated conditional → KILLED
                if (response == null) {
132
                    throw new WebServerException("The returned value for the endpoint \"%s\" was null.".formatted(request.getRequestLine().getPathDetails().getIsolatedPath()));
133
                }
134
135
                // calculate proper headers for the response
136
                StringBuilder headerStringBuilder = addDefaultHeaders(response);
137 1 1. httpProcessing : removed call to com/renomad/minum/web/WebFramework::addOptionalExtraHeaders → KILLED
                addOptionalExtraHeaders(response, headerStringBuilder);
138 1 1. httpProcessing : removed call to com/renomad/minum/web/WebFramework::addKeepAliveTimeout → KILLED
                addKeepAliveTimeout(isKeepAlive, headerStringBuilder);
139
140
                // inspect the response being sent, see whether we can compress the data.
141
                IResponse adjustedResponse = potentiallyCompress(request.getHeaders(), response, headerStringBuilder);
142 1 1. httpProcessing : removed call to com/renomad/minum/web/WebFramework::applyContentLength → KILLED
                applyContentLength(headerStringBuilder, adjustedResponse.getBodyLength());
143 1 1. httpProcessing : removed call to com/renomad/minum/web/WebFramework::confirmBodyHasContentType → KILLED
                confirmBodyHasContentType(request, response);
144
145
                // send the headers
146 1 1. httpProcessing : removed call to com/renomad/minum/web/ISocketWrapper::send → KILLED
                sw.send(headerStringBuilder.append(HTTP_CRLF).toString().getBytes(StandardCharsets.US_ASCII));
147
148
                // if the user sent a HEAD request, we send everything back except the body.
149
                // even though we skip the body, this requires full processing to get the
150
                // numbers right, like content-length.
151 1 1. httpProcessing : negated conditional → KILLED
                if (request.getRequestLine().getMethod().equals(RequestLine.Method.HEAD)) {
152
                    logger.logDebug(() -> "client " + request.getRemoteRequester() +
153
                            " is requesting HEAD for " + request.getRequestLine().getPathDetails().getIsolatedPath() +
154
                            ".  Excluding body from response");
155
                } else {
156
                    // send the body
157 1 1. httpProcessing : removed call to com/renomad/minum/web/IResponse::sendBody → TIMED_OUT
                    adjustedResponse.sendBody(sw);
158
                }
159
160 1 1. httpProcessing : removed call to com/renomad/minum/web/ISocketWrapper::flush → KILLED
                sw.flush();
161
162
                // print how long this processing took
163
                long endMillis = System.currentTimeMillis();
164
                logger.logTrace(() -> String.format("full processing (including communication time) of %s %s took %d millis", sw, requestLine, endMillis - startMillis));
165 1 1. httpProcessing : negated conditional → KILLED
                if (!isKeepAlive) break;
166
            }
167
        } catch (SocketException | SocketTimeoutException ex) {
168 1 1. httpProcessing : removed call to com/renomad/minum/web/WebFramework::handleReadTimedOut → KILLED
            handleReadTimedOut(sw, ex, logger);
169
        } catch (ForbiddenUseException ex) {
170 1 1. httpProcessing : removed call to com/renomad/minum/web/WebFramework::handleForbiddenUse → KILLED
            handleForbiddenUse(sw, ex, logger, theBrig, constants.vulnSeekingJailDuration);
171
        } catch (IOException ex) {
172 1 1. httpProcessing : removed call to com/renomad/minum/web/WebFramework::handleIOException → KILLED
            handleIOException(sw, ex, logger, theBrig, underInvestigation, constants.vulnSeekingJailDuration);
173
        }
174
    }
175
176
177
    static void handleIOException(ISocketWrapper sw, IOException ex, ILogger logger, ITheBrig theBrig, UnderInvestigation underInvestigation, int vulnSeekingJailDuration ) {
178
        logger.logDebug(() -> ex.getMessage() + " (at Server.start)");
179
        String suspiciousClues = underInvestigation.isClientLookingForVulnerabilities(ex.getMessage());
180
181 2 1. handleIOException : negated conditional → KILLED
2. handleIOException : negated conditional → KILLED
        if (!suspiciousClues.isEmpty() && theBrig != null) {
182
            logger.logDebug(() -> sw.getRemoteAddr() + " is looking for vulnerabilities, for this: " + suspiciousClues);
183
            theBrig.sendToJail(sw.getRemoteAddr() + "_vuln_seeking", vulnSeekingJailDuration);
184
        }
185
    }
186
187
    static void handleForbiddenUse(ISocketWrapper sw, ForbiddenUseException ex, ILogger logger, ITheBrig theBrig, int vulnSeekingJailDuration) {
188
        logger.logDebug(() -> sw.getRemoteAddr() + " is looking for vulnerabilities, for this: " + ex.getMessage());
189 1 1. handleForbiddenUse : negated conditional → KILLED
        if (theBrig != null) {
190
            theBrig.sendToJail(sw.getRemoteAddr() + "_vuln_seeking", vulnSeekingJailDuration);
191
        } else {
192
            logger.logDebug(() -> "theBrig is null at handleForbiddenUse, will not store address in database");
193
        }
194
    }
195
196
    static void handleReadTimedOut(ISocketWrapper sw, IOException ex, ILogger logger) {
197
        /*
198
        if we close the application on the server side, there's a good
199
        likelihood a SocketException will come bubbling through here.
200
        NOTE:
201
          it seems that Socket closed is what we get when the client closes the connection in non-SSL, and conversely,
202
          if we are operating in secure (i.e. SSL/TLS) mode, we get "an established connection..."
203
        */
204 1 1. handleReadTimedOut : negated conditional → KILLED
        if (ex.getMessage().equals("Read timed out")) {
205
            logger.logTrace(() -> "Read timed out - remote address: " + sw.getRemoteAddrWithPort());
206
        } else {
207
            logger.logDebug(() -> ex.getMessage() + " - remote address: " + sw.getRemoteAddrWithPort());
208
        }
209
    }
210
211
    /**
212
     * Logic for how to process an incoming request.  For example, did the developer
213
     * write a function to handle this? Is it a request for a static file, like an image
214
     * or script?  Did the user provide a "pre" or "post" handler?
215
     */
216
    IResponse processRequest(
217
            IRequest clientRequest,
218
            ISocketWrapper sw,
219
            RequestLine requestLine,
220
            Headers requestHeaders) throws Exception {
221
        IResponse response;
222
        ThrowingFunction<IRequest, IResponse> endpoint = findEndpointForThisStartline(requestLine, requestHeaders);
223 1 1. processRequest : negated conditional → KILLED
        if (endpoint == null) {
224
            response = Response.buildLeanResponse(CODE_404_NOT_FOUND);
225
        } else {
226
            long millisAtStart = System.currentTimeMillis();
227
            try {
228 1 1. processRequest : negated conditional → KILLED
                if (preHandler != null) {
229
                    response = preHandler.apply(new PreHandlerInputs(clientRequest, endpoint, sw));
230
                } else {
231
                    response = endpoint.apply(clientRequest);
232
                }
233
            } catch (Exception ex) {
234
                // if an error happens while running an endpoint's code, this is the
235
                // last-chance handling of that error where we return a 500 and a
236
                // random code to the client, so a developer can find the detailed
237
                // information in the logs, which have that same value.
238
                int randomNumber = randomErrorCorrelationId.nextInt();
239
                logger.logAsyncError(() -> "error while running endpoint " + endpoint + ". Code: " + randomNumber + ". Error: " + StacktraceUtils.stackTraceToString(ex));
240
                response = Response.buildResponse(CODE_500_INTERNAL_SERVER_ERROR, Map.of("Content-Type", "text/plain;charset=UTF-8"), "Server error: " + randomNumber);
241
            }
242
            long millisAtEnd = System.currentTimeMillis();
243
            logger.logTrace(() -> String.format("handler processing of %s %s took %d millis", sw, requestLine, millisAtEnd - millisAtStart));
244
        }
245
246
        // if the user has chosen to customize the response based on status code, that will
247
        // be applied now, and it will override the previous response.
248 1 1. processRequest : negated conditional → KILLED
        if (lastMinuteHandler != null) {
249
            response = lastMinuteHandler.apply(new LastMinuteHandlerInputs(clientRequest, response));
250
        }
251
252 1 1. processRequest : replaced return value with null for com/renomad/minum/web/WebFramework::processRequest → KILLED
        return response;
253
    }
254
255
    private Headers getHeaders(ISocketWrapper sw) {
256
    /*
257
       next we will read the headers (e.g. Content-Type: foo/bar) one-by-one.
258
259
       the headers tell us vital information about the
260
       body.  If, for example, we're getting a POST and receiving a
261
       www form url encoded, there will be a header of "content-length"
262
       that will mention how many bytes to read.  On the other hand, if
263
       we're receiving a multipart, there will be no content-length, but
264
       the content-type will include the boundary string.
265
    */
266
        List<String> allHeaders = Headers.getAllHeaders(sw.getInputStream(), inputStreamUtils);
267
        Headers hi = new Headers(allHeaders);
268
        logger.logTrace(() -> "The headers are: " + hi.getHeaderStrings());
269 1 1. getHeaders : replaced return value with null for com/renomad/minum/web/WebFramework::getHeaders → KILLED
        return hi;
270
    }
271
272
    /**
273
     * determine if we are in a keep-alive connection
274
     */
275
    static boolean determineIfKeepAlive(RequestLine sl, Headers hi, ILogger logger) {
276
        boolean isKeepAlive = false;
277 1 1. determineIfKeepAlive : negated conditional → KILLED
        if (sl.getVersion() == HttpVersion.ONE_DOT_ZERO) {
278
            isKeepAlive = hi.hasKeepAlive();
279 1 1. determineIfKeepAlive : negated conditional → KILLED
        } else if (sl.getVersion() == HttpVersion.ONE_DOT_ONE) {
280 1 1. determineIfKeepAlive : negated conditional → KILLED
            isKeepAlive = ! hi.hasConnectionClose();
281
        }
282
        boolean finalIsKeepAlive = isKeepAlive;
283
        logger.logTrace(() -> "Is this a keep-alive connection? " + finalIsKeepAlive);
284 2 1. determineIfKeepAlive : replaced boolean return with false for com/renomad/minum/web/WebFramework::determineIfKeepAlive → KILLED
2. determineIfKeepAlive : replaced boolean return with true for com/renomad/minum/web/WebFramework::determineIfKeepAlive → KILLED
        return isKeepAlive;
285
    }
286
287
    RequestLine getProcessedRequestLine(ISocketWrapper sw, String rawStartLine) {
288
        logger.logTrace(() -> sw + ": raw request line received: " + rawStartLine);
289
290
        RequestLine extractedRequestLine = validRequestLine.extractRequestLine(rawStartLine);
291
        logger.logTrace(() -> sw + ": RequestLine has been derived: " + extractedRequestLine);
292 1 1. getProcessedRequestLine : replaced return value with null for com/renomad/minum/web/WebFramework::getProcessedRequestLine → KILLED
        return extractedRequestLine;
293
    }
294
295
    void checkIfSuspiciousPath(ISocketWrapper sw, RequestLine requestLine) {
296
        String suspiciousClues = underInvestigation.isLookingForSuspiciousPaths(
297
                requestLine.getPathDetails().getIsolatedPath());
298 1 1. checkIfSuspiciousPath : negated conditional → KILLED
        if (!suspiciousClues.isEmpty()) {
299
            String msg = sw.getRemoteAddr() + " is looking for a vulnerability, for this: " + suspiciousClues;
300
            throw new ForbiddenUseException(msg);
301
        }
302
    }
303
304
    /**
305
     * Drops the connection immediately if the client is recognized
306
     * as someone we consider an attacker, by dint of having been
307
     * added to a blacklist in {@link com.renomad.minum.security.TheBrig}.
308
     */
309
    boolean dumpIfAttacker(ISocketWrapper sw, FullSystem fs) {
310 1 1. dumpIfAttacker : negated conditional → KILLED
        if (fs == null) {
311 1 1. dumpIfAttacker : replaced boolean return with true for com/renomad/minum/web/WebFramework::dumpIfAttacker → KILLED
            return false;
312 1 1. dumpIfAttacker : negated conditional → KILLED
        } else if (fs.getTheBrig() == null) {
313 1 1. dumpIfAttacker : replaced boolean return with true for com/renomad/minum/web/WebFramework::dumpIfAttacker → KILLED
            return false;
314
        } else {
315 1 1. dumpIfAttacker : removed call to com/renomad/minum/web/WebFramework::dumpIfAttacker → SURVIVED
            dumpIfAttacker(sw, fs.getTheBrig());
316 1 1. dumpIfAttacker : replaced boolean return with false for com/renomad/minum/web/WebFramework::dumpIfAttacker → SURVIVED
            return true;
317
        }
318
    }
319
320
    void dumpIfAttacker(ISocketWrapper sw, ITheBrig theBrig) {
321
        String remoteClient = sw.getRemoteAddr();
322 1 1. dumpIfAttacker : negated conditional → KILLED
        if (theBrig.isInJail(remoteClient + "_vuln_seeking")) {
323
            // if this client is a vulnerability seeker, throw an exception,
324
            // causing them to get dumped unceremoniously
325
            String message = "closing the socket on " + remoteClient + " due to being found in the brig";
326
            logger.logDebug(() -> message);
327
            throw new ForbiddenUseException(message);
328
        }
329
    }
330
331
    /**
332
     * Prepare some of the basic server response headers, like the status code, the
333
     * date-time stamp, the server name.
334
     */
335
    private StringBuilder addDefaultHeaders(IResponse response) {
336
337 1 1. lambda$addDefaultHeaders$17 : replaced return value with null for com/renomad/minum/web/WebFramework::lambda$addDefaultHeaders$17 → KILLED
        String date = Objects.requireNonNullElseGet(overrideForDateTime, () -> ZonedDateTime.now(ZoneId.of("UTC"))).format(DateTimeFormatter.RFC_1123_DATE_TIME);
338
339
        // we'll store the status line and headers in this
340
        StringBuilder headerStringBuilder = new StringBuilder(600); // 600 is just a magic arbitrary number I picked, because our response headers
341
                                                                    // are not usually too large - even if the user added a bunch, there is a good
342
                                                                    // chance it would be far under 600.  If that turns out to be wrong, adjust/redesign
343
344
        // add the status line
345
        headerStringBuilder.append("HTTP/1.1 ").append(response.getStatusCode().code).append(" ").append(response.getStatusCode().shortDescription).append(HTTP_CRLF);
346
347
        // add a date-timestamp
348
        headerStringBuilder.append("Date: ").append(date).append(HTTP_CRLF);
349
350
        // add the server name
351
        headerStringBuilder.append("Server: minum").append(HTTP_CRLF);
352
353 1 1. addDefaultHeaders : replaced return value with null for com/renomad/minum/web/WebFramework::addDefaultHeaders → KILLED
        return headerStringBuilder;
354
    }
355
356
    /**
357
     * Add extra headers specified by the business logic (set by the developer)
358
     */
359
    private static void addOptionalExtraHeaders(IResponse response, StringBuilder stringBuilder) {
360
        for (Map.Entry<String,String> header : response.getExtraHeaders().entrySet()) {
361
            stringBuilder.append(header.getKey())
362
                    .append(": ")
363
                    .append(header.getValue())
364
                    .append(HTTP_CRLF);
365
        }
366
    }
367
368
    /**
369
     * If a response body exists, it needs to have a content-type specified,
370
     * or throw an exception. Otherwise, the user could totally miss they did
371
     * not set a content-type, because the browser will inspect the data and
372
     * do sort-of-the-right-thing a lot of the time, but we want to enforce correctness.
373
     */
374
    static void confirmBodyHasContentType(IRequest request, IResponse response) {
375
        // check the correctness of the content-type header versus the data length (if any data, that is)
376 2 1. lambda$confirmBodyHasContentType$18 : replaced boolean return with false for com/renomad/minum/web/WebFramework::lambda$confirmBodyHasContentType$18 → KILLED
2. lambda$confirmBodyHasContentType$18 : replaced boolean return with true for com/renomad/minum/web/WebFramework::lambda$confirmBodyHasContentType$18 → KILLED
        boolean hasContentType = response.getExtraHeaders().keySet().stream().anyMatch(x -> x.toLowerCase(Locale.ROOT).equals("content-type"));
377
378
        // if there *is* data, we had better be returning a content type
379 3 1. confirmBodyHasContentType : negated conditional → KILLED
2. confirmBodyHasContentType : negated conditional → KILLED
3. confirmBodyHasContentType : changed conditional boundary → KILLED
        if (response.getBodyLength() > 0 && !hasContentType) {
380
            throw new WebServerException("a Content-Type header must be specified in the Response object if it returns data. Response details: " + response + " Request: " + request);
381
        }
382
    }
383
384
    /**
385
     * If this is a keep-alive communication, add a header specifying the
386
     * socket timeout for the browser.
387
     */
388
    private void addKeepAliveTimeout(boolean isKeepAlive, StringBuilder stringBuilder) {
389
        // if we're a keep-alive connection, reply with a keep-alive header
390 1 1. addKeepAliveTimeout : negated conditional → KILLED
        if (isKeepAlive) {
391
            stringBuilder.append("Keep-Alive: timeout=").append(constants.keepAliveTimeoutSeconds).append(HTTP_CRLF);
392
        }
393
    }
394
395
    /**
396
     * The rules regarding the content-length header are byzantine.  Even in the cases
397
     * where you aren't returning anything, servers can use this header to determine when the
398
     * response is finished.
399
     * See <a href="https://www.rfc-editor.org/rfc/rfc9110.html#name-content-length">Content-Length in the HTTP spec</a>
400
     */
401
    private static void applyContentLength(StringBuilder stringBuilder, long bodyLength) {
402
        stringBuilder.append("Content-Length: ").append(bodyLength).append(HTTP_CRLF);
403
    }
404
405
    /**
406
     * This method will examine the request headers and response content-type, and
407
     * compress the outgoing data if necessary.
408
     */
409
    static IResponse potentiallyCompress(Headers requestHeaders, IResponse response, StringBuilder headerStringBuilder) throws IOException {
410
        // we may make modifications to the response body at this point, specifically
411
        // we may compress the data, if the client requested it.
412
        // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-encoding
413
        List<String> acceptEncoding = requestHeaders.valueByKey("accept-encoding");
414
415 1 1. potentiallyCompress : negated conditional → KILLED
        if (response.isBodyText()) {
416 1 1. potentiallyCompress : replaced return value with null for com/renomad/minum/web/WebFramework::potentiallyCompress → KILLED
            return compressBodyIfRequested(response, acceptEncoding, headerStringBuilder, MINIMUM_NUMBER_OF_BYTES_TO_COMPRESS);
417
        }
418 1 1. potentiallyCompress : replaced return value with null for com/renomad/minum/web/WebFramework::potentiallyCompress → KILLED
        return response;
419
    }
420
421
    /**
422
     * This method will examine the content-encoding headers, and if "gzip" is
423
     * requested by the client, we will replace the body bytes with compressed
424
     * bytes, using the GZIP compression algorithm, as long as the response body
425
     * is greater than minNumberBytes bytes.
426
     *
427
     * @param acceptEncoding headers sent by the client about what compression
428
     *                       algorithms will be understood.
429
     * @param stringBuilder  the string we are gradually building up to send back to
430
     *                       the client for the status line and headers. We'll use it
431
     *                       here if we need to append a content-encoding - that is,
432
     *                       if we successfully compress data as gzip.
433
     * @param minNumberBytes number of bytes must be larger than this to compress.
434
     */
435
    static IResponse compressBodyIfRequested(IResponse response, List<String> acceptEncoding, StringBuilder stringBuilder, int minNumberBytes) throws IOException {
436 1 1. compressBodyIfRequested : negated conditional → KILLED
        String allContentEncodingHeaders = acceptEncoding != null ? String.join(";", acceptEncoding) : "";
437 3 1. compressBodyIfRequested : negated conditional → KILLED
2. compressBodyIfRequested : changed conditional boundary → KILLED
3. compressBodyIfRequested : negated conditional → KILLED
        if (response.getBodyLength() >= minNumberBytes && allContentEncodingHeaders.contains("gzip")) {
438
            stringBuilder.append("Content-Encoding: gzip").append(HTTP_CRLF);
439
            stringBuilder.append("Vary: accept-encoding").append(HTTP_CRLF);
440 1 1. compressBodyIfRequested : replaced return value with null for com/renomad/minum/web/WebFramework::compressBodyIfRequested → KILLED
            return ((Response)response).compressBody();
441
        }
442 1 1. compressBodyIfRequested : replaced return value with null for com/renomad/minum/web/WebFramework::compressBodyIfRequested → KILLED
        return response;
443
    }
444
445
    /**
446
     * Looks through the mappings of {@link MethodPath} and path to registered endpoints
447
     * or the static cache and returns the appropriate one (If we
448
     * do not find anything, return null)
449
     */
450
    ThrowingFunction<IRequest, IResponse> findEndpointForThisStartline(RequestLine sl, Headers requestHeaders) {
451
        ThrowingFunction<IRequest, IResponse> handler;
452
        logger.logTrace(() -> "Seeking a handler for " + sl);
453
454
        // first we check if there's a simple direct match
455
        String requestedPath = sl.getPathDetails().getIsolatedPath().toLowerCase(Locale.ROOT);
456
457
        // if the user is asking for a HEAD request, they want to run a GET command
458
        // but don't want the body.  We'll simply exclude sending the body, later on, when returning the data
459 1 1. findEndpointForThisStartline : negated conditional → KILLED
        RequestLine.Method method = sl.getMethod() == RequestLine.Method.HEAD ? RequestLine.Method.GET : sl.getMethod();
460
461
        MethodPath key = new MethodPath(method, requestedPath);
462
        handler = registeredDynamicPaths.get(key);
463
464 1 1. findEndpointForThisStartline : negated conditional → KILLED
        if (handler == null) {
465
            logger.logTrace(() -> "No direct handler found.  looking for a partial match for " + requestedPath);
466
            handler = findHandlerByPartialMatch(sl);
467
        }
468
469 1 1. findEndpointForThisStartline : negated conditional → KILLED
        if (handler == null) {
470
            logger.logTrace(() -> "No partial match found, checking files on disk for " + requestedPath );
471
            handler = findHandlerByFilesOnDisk(sl, requestHeaders);
472
        }
473
474
        // we'll return this, and it could be a null.
475 1 1. findEndpointForThisStartline : replaced return value with null for com/renomad/minum/web/WebFramework::findEndpointForThisStartline → KILLED
        return handler;
476
    }
477
478
    /**
479
     * last ditch effort - look on disk.  This response will either
480
     * be the file to return, or null if we didn't find anything.
481
     */
482
    private ThrowingFunction<IRequest, IResponse> findHandlerByFilesOnDisk(RequestLine sl, Headers requestHeaders) {
483 2 1. findHandlerByFilesOnDisk : negated conditional → KILLED
2. findHandlerByFilesOnDisk : negated conditional → KILLED
        if (sl.getMethod() != RequestLine.Method.GET && sl.getMethod() != RequestLine.Method.HEAD) {
484
            return null;
485
        }
486
        String requestedPath = sl.getPathDetails().getIsolatedPath();
487
        IResponse response = readStaticFile(requestedPath, requestHeaders);
488 2 1. lambda$findHandlerByFilesOnDisk$22 : replaced return value with null for com/renomad/minum/web/WebFramework::lambda$findHandlerByFilesOnDisk$22 → KILLED
2. findHandlerByFilesOnDisk : replaced return value with null for com/renomad/minum/web/WebFramework::findHandlerByFilesOnDisk → KILLED
        return request -> response;
489
    }
490
491
492
    /**
493
     * Get a file from a path and create a response for it with a mime type.
494
     * <p>
495
     *     Parent directories are made unavailable by searching the path for
496
     *     bad characters. see {@link FileUtils#checkForBadFilePatterns}
497
     * </p>
498
     *
499
     * @return a response with the file contents and caching headers and mime if valid.
500
     *  if the path has invalid characters, we'll return a "bad request" response.
501
     */
502
    IResponse readStaticFile(String path, Headers requestHeaders) {
503
        try {
504 1 1. readStaticFile : removed call to com/renomad/minum/utils/FileUtils::checkForBadFilePatterns → KILLED
            checkForBadFilePatterns(path);
505
        } catch (Exception ex) {
506
            logger.logDebug(() -> String.format("Bad path requested at readStaticFile: %s.  Exception: %s", path, ex.getMessage()));
507 1 1. readStaticFile : replaced return value with null for com/renomad/minum/web/WebFramework::readStaticFile → KILLED
            return Response.buildLeanResponse(CODE_400_BAD_REQUEST);
508
        }
509
        String mimeType = null;
510
511
        try {
512 1 1. readStaticFile : removed call to com/renomad/minum/utils/FileUtils::checkFileIsWithinDirectory → KILLED
            checkFileIsWithinDirectory(path, constants.staticFilesDirectory);
513
        } catch (Exception ex) {
514
            logger.logDebug(() -> String.format("Unable to find %s in allowed directories", path));
515 1 1. readStaticFile : replaced return value with null for com/renomad/minum/web/WebFramework::readStaticFile → KILLED
            return Response.buildLeanResponse(CODE_404_NOT_FOUND);
516
        }
517
518
        try {
519
            Path staticFilePath = Path.of(constants.staticFilesDirectory).resolve(path);
520 1 1. readStaticFile : negated conditional → KILLED
            if (!Files.isRegularFile(staticFilePath)) {
521
                logger.logDebug(() -> String.format("No readable regular file found at %s", path));
522 1 1. readStaticFile : replaced return value with null for com/renomad/minum/web/WebFramework::readStaticFile → KILLED
                return Response.buildLeanResponse(CODE_404_NOT_FOUND);
523
            }
524
525
            // if the provided path has a dot in it, use that
526
            // to obtain a suffix for determining file type
527
            int suffixBeginIndex = path.lastIndexOf('.');
528 2 1. readStaticFile : negated conditional → KILLED
2. readStaticFile : changed conditional boundary → KILLED
            if (suffixBeginIndex > 0) {
529 1 1. readStaticFile : Replaced integer addition with subtraction → KILLED
                String suffix = path.substring(suffixBeginIndex+1);
530
                mimeType = fileSuffixToMime.get(suffix);
531
            }
532
533
            // if we don't find any registered mime types for this
534
            // suffix, or if it doesn't have a suffix, set the mime type
535
            // to application/octet-stream
536 1 1. readStaticFile : negated conditional → KILLED
            if (mimeType == null) {
537
                mimeType = "application/octet-stream";
538
            }
539
540 2 1. readStaticFile : negated conditional → KILLED
2. readStaticFile : changed conditional boundary → KILLED
            if (Files.size(staticFilePath) < 100_000) {
541
                var fileContents = fileReader.readFile(staticFilePath.toString());
542 1 1. readStaticFile : replaced return value with null for com/renomad/minum/web/WebFramework::readStaticFile → KILLED
                return createOkResponseForStaticFiles(fileContents, mimeType);
543
            } else {
544 1 1. readStaticFile : replaced return value with null for com/renomad/minum/web/WebFramework::readStaticFile → KILLED
                return createOkResponseForLargeStaticFiles(mimeType, staticFilePath, requestHeaders);
545
            }
546
547
        } catch (IOException e) {
548
            logger.logAsyncError(() -> String.format("Error while reading file: %s. %s", path, StacktraceUtils.stackTraceToString(e)));
549 1 1. readStaticFile : replaced return value with null for com/renomad/minum/web/WebFramework::readStaticFile → KILLED
            return Response.buildLeanResponse(CODE_400_BAD_REQUEST);
550
        }
551
    }
552
553
    /**
554
     * All static responses will get a cache time of STATIC_FILE_CACHE_TIME seconds
555
     */
556
    private IResponse createOkResponseForStaticFiles(byte[] fileContents, String mimeType) {
557
        var headers = Map.of(
558
                "cache-control", "max-age=" + constants.staticFileCacheTime,
559
                "content-type", mimeType);
560
561 1 1. createOkResponseForStaticFiles : replaced return value with null for com/renomad/minum/web/WebFramework::createOkResponseForStaticFiles → KILLED
        return Response.buildResponse(
562
                CODE_200_OK,
563
                headers,
564
                fileContents);
565
    }
566
567
    /**
568
     * All static responses will get a cache time of STATIC_FILE_CACHE_TIME seconds
569
     */
570
    private IResponse createOkResponseForLargeStaticFiles(String mimeType, Path filePath, Headers requestHeaders) throws IOException {
571
        var headers = Map.of(
572
                "cache-control", "max-age=" + constants.staticFileCacheTime,
573
                "content-type", mimeType,
574
                "Accept-Ranges", "bytes"
575
                );
576
577 1 1. createOkResponseForLargeStaticFiles : replaced return value with null for com/renomad/minum/web/WebFramework::createOkResponseForLargeStaticFiles → KILLED
        return Response.buildLargeFileResponse(
578
                headers,
579
                filePath.toString(),
580
                requestHeaders
581
                );
582
    }
583
584
585
    /**
586
     * These are the default starting values for mappings
587
     * between file suffixes and appropriate mime types
588
     */
589
    private void addDefaultValuesForMimeMap() {
590
        fileSuffixToMime.put("css", "text/css");
591
        fileSuffixToMime.put("js", "application/javascript");
592
        fileSuffixToMime.put("webp", "image/webp");
593
        fileSuffixToMime.put("jpg", "image/jpeg");
594
        fileSuffixToMime.put("jpeg", "image/jpeg");
595
        fileSuffixToMime.put("htm", "text/html");
596
        fileSuffixToMime.put("html", "text/html");
597
    }
598
599
600
    /**
601
     * let's see if we can match the registered paths against a **portion** of the startline
602
     */
603
    ThrowingFunction<IRequest, IResponse> findHandlerByPartialMatch(RequestLine sl) {
604
        String requestedPath = sl.getPathDetails().getIsolatedPath();
605
        var methodPathFunctionEntry = registeredPartialPaths.entrySet().stream()
606 2 1. lambda$findHandlerByPartialMatch$27 : negated conditional → KILLED
2. lambda$findHandlerByPartialMatch$27 : replaced boolean return with true for com/renomad/minum/web/WebFramework::lambda$findHandlerByPartialMatch$27 → KILLED
                .filter(x -> requestedPath.startsWith(x.getKey().path()) &&
607 1 1. lambda$findHandlerByPartialMatch$27 : negated conditional → KILLED
                        x.getKey().method().equals(sl.getMethod()))
608
                .findFirst().orElse(null);
609 1 1. findHandlerByPartialMatch : negated conditional → KILLED
        if (methodPathFunctionEntry != null) {
610 1 1. findHandlerByPartialMatch : replaced return value with null for com/renomad/minum/web/WebFramework::findHandlerByPartialMatch → KILLED
            return methodPathFunctionEntry.getValue();
611
        } else {
612
            return null;
613
        }
614
    }
615
616
    /**
617
     * This constructor is used for the real production system
618
     */
619
    WebFramework(Context context) {
620
        this(context, null, null);
621
    }
622
623
    WebFramework(Context context, ZonedDateTime overrideForDateTime) {
624
        this(context, overrideForDateTime, null);
625
    }
626
627
    /**
628
     * This provides the ZonedDateTime as a parameter so we
629
     * can set the current date (for testing purposes)
630
     * @param overrideForDateTime for those test cases where we need to control the time
631
     */
632
    WebFramework(Context context, ZonedDateTime overrideForDateTime, IFileReader fileReader) {
633
        this.fs = context.getFullSystem();
634 1 1. <init> : negated conditional → KILLED
        this.theBrig = this.fs != null ? this.fs.getTheBrig() : null;
635
        this.logger = context.getLogger();
636
        this.constants = context.getConstants();
637
        this.overrideForDateTime = overrideForDateTime;
638
        this.registeredDynamicPaths = new HashMap<>();
639
        this.registeredPartialPaths = new HashMap<>();
640
        this.underInvestigation = new UnderInvestigation(constants);
641
        this.inputStreamUtils = new InputStreamUtils(constants.maxReadLineSizeBytes);
642
        this.bodyProcessor = new BodyProcessor(context);
643
644
        // This random value is purely to help provide correlation between
645
        // error messages in the UI and error logs.  There are no security concerns.
646
        this.randomErrorCorrelationId = new Random();
647
        this.validRequestLine =  new RequestLine(
648
                RequestLine.Method.NONE,
649
                PathDetails.empty,
650
                HttpVersion.NONE,
651
                "", logger);
652
        this.emptyRequestLine = RequestLine.EMPTY;
653
654
        // this allows us to inject a IFileReader for deeper testing
655 1 1. <init> : negated conditional → KILLED
        if (fileReader != null) {
656
            this.fileReader = fileReader;
657
        } else {
658
            this.fileReader = new FileReader(
659
                    LRUCache.getLruCache(constants.maxElementsLruCacheStaticFiles),
660
                    constants.useCacheForStaticFiles,
661
                    logger);
662
        }
663
        this.fileSuffixToMime = new HashMap<>();
664 1 1. <init> : removed call to com/renomad/minum/web/WebFramework::addDefaultValuesForMimeMap → KILLED
        addDefaultValuesForMimeMap();
665 1 1. <init> : removed call to com/renomad/minum/web/WebFramework::readExtraMimeMappings → KILLED
        readExtraMimeMappings(constants.extraMimeMappings);
666
    }
667
668
    void readExtraMimeMappings(List<String> input) {
669 2 1. readExtraMimeMappings : negated conditional → KILLED
2. readExtraMimeMappings : negated conditional → KILLED
        if (input == null || input.isEmpty()) return;
670 2 1. readExtraMimeMappings : Replaced integer modulus with multiplication → KILLED
2. readExtraMimeMappings : negated conditional → KILLED
        if (!(input.size() % 2 == 0)) {
671
            throw new WebServerException("input must be even (key + value = 2 items). Your input: " + input);
672
        }
673
674 2 1. readExtraMimeMappings : negated conditional → KILLED
2. readExtraMimeMappings : changed conditional boundary → KILLED
        for (int i = 0; i < input.size(); i += 2) {
675
            String fileSuffix = input.get(i);
676 1 1. readExtraMimeMappings : Replaced integer addition with subtraction → KILLED
            String mime = input.get(i+1);
677
            logger.logTrace(() -> "Adding mime mapping: " + fileSuffix + " -> " + mime);
678
            fileSuffixToMime.put(fileSuffix, mime);
679
        }
680
    }
681
682
    /**
683
     * Add a new handler in the web application for a combination
684
     * of a {@link RequestLine.Method}, a path, and then provide
685
     * the code to handle a request.
686
     * <br>
687
     * Note that the path text expected is *after* the first forward slash,
688
     * so for example with {@code http://foo.com/mypath}, provide "mypath" as the path.
689
     * @throws WebServerException if duplicate paths are registered, or if the path is prefixed with a slash
690
     */
691
    public void registerPath(RequestLine.Method method, String pathName, ThrowingFunction<IRequest, IResponse> webHandler) {
692 2 1. registerPath : negated conditional → KILLED
2. registerPath : negated conditional → KILLED
        if (pathName.startsWith("\\") || pathName.startsWith("/")) {
693
            throw new WebServerException(
694
                    String.format("Path should not be prefixed with a slash.  Corrected version: registerPath(%s, \"%s\", ... )", method.name(), pathName.substring(1)));
695
        }
696
697
        var result = registeredDynamicPaths.put(new MethodPath(method, pathName), webHandler);
698 1 1. registerPath : negated conditional → KILLED
        if (result != null) {
699
            throw new WebServerException("Duplicate endpoint registered: " + new MethodPath(method, pathName));
700
        }
701
    }
702
703
    /**
704
     * Similar to {@link WebFramework#registerPath(RequestLine.Method, String, ThrowingFunction)} except that the paths
705
     * registered here may be partially matched.
706
     * <p>
707
     *     For example, if you register {@code .well-known/acme-challenge} then it
708
     *     can match a client request for {@code .well-known/acme-challenge/HGr8U1IeTW4kY_Z6UIyaakzOkyQgPr_7ArlLgtZE8SX}
709
     * </p>
710
     * <p>
711
     *     Be careful here, be thoughtful - partial paths will match a lot, and may
712
     *     overlap with other URL's for your app, such as endpoints and static files.
713
     * </p>
714
     * @throws WebServerException if duplicate paths are registered, or if the path is prefixed with a slash
715
     */
716
    public void registerPartialPath(RequestLine.Method method, String pathName, ThrowingFunction<IRequest, IResponse> webHandler) {
717 2 1. registerPartialPath : negated conditional → KILLED
2. registerPartialPath : negated conditional → KILLED
        if (pathName.startsWith("\\") || pathName.startsWith("/")) {
718
            throw new WebServerException(
719
                    String.format("Path should not be prefixed with a slash.  Corrected version: registerPartialPath(%s, \"%s\", ... )", method.name(), pathName.substring(1)));
720
        }
721
        var result = registeredPartialPaths.put(new MethodPath(method, pathName), webHandler);
722 1 1. registerPartialPath : negated conditional → KILLED
        if (result != null) {
723
            throw new WebServerException("Duplicate partial-path endpoint registered: " + new MethodPath(method, pathName));
724
        }
725
    }
726
727
    /**
728
     * Sets a handler to process all requests across the board.
729
     * <br>
730
     * <p>
731
     *     This is an <b>unusual</b> method.  Setting a handler here allows the user to run code of his
732
     * choosing before the regular business code is run.  Note that by defining this value, the ordinary
733
     * call to endpoint.apply(request) will not be run.
734
     * </p>
735
     * <p>Here is an example</p>
736
     * <pre>{@code
737
     *
738
     *      webFramework.registerPreHandler(preHandlerInputs -> preHandlerCode(preHandlerInputs, auth, context));
739
     *
740
     *      ...
741
     *
742
     *      private IResponse preHandlerCode(PreHandlerInputs preHandlerInputs, AuthUtils auth, Context context) throws Exception {
743
     *          int secureServerPort = context.getConstants().secureServerPort;
744
     *          Request request = preHandlerInputs.clientRequest();
745
     *          ThrowingFunction<IRequest, IResponse> endpoint = preHandlerInputs.endpoint();
746
     *          ISocketWrapper sw = preHandlerInputs.sw();
747
     *
748
     *          // log all requests
749
     *          logger.logTrace(() -> String.format("Request: %s by %s",
750
     *              request.requestLine().getRawValue(),
751
     *              request.remoteRequester())
752
     *          );
753
     *
754
     *          // redirect to https if they are on the plain-text connection and the path is "login"
755
     *
756
     *          // get the path from the request line
757
     *          String path = request.getRequestLine().getPathDetails().getIsolatedPath();
758
     *
759
     *          // redirect to https on the configured secure port if they are on the plain-text connection and the path contains "login"
760
     *          if (path.contains("login") &&
761
     *              sw.getServerType().equals(HttpServerType.PLAIN_TEXT_HTTP)) {
762
     *              return Response.redirectTo("https://%s:%d/%s".formatted(sw.getHostName(), secureServerPort, path));
763
     *          }
764
     *
765
     *          // adjust behavior if non-authenticated and path includes "secure/"
766
     *          if (path.contains("secure/")) {
767
     *              AuthResult authResult = auth.processAuth(request);
768
     *              if (authResult.isAuthenticated()) {
769
     *                  return endpoint.apply(request);
770
     *              } else {
771
     *                  return Response.buildLeanResponse(CODE_403_FORBIDDEN);
772
     *              }
773
     *          }
774
     *
775
     *          // if the path does not include /secure, just move the request along unchanged.
776
     *          return endpoint.apply(request);
777
     *      }
778
     * }</pre>
779
     */
780
        public void registerPreHandler(ThrowingFunction<PreHandlerInputs, IResponse> preHandler) {
781
        this.preHandler = preHandler;
782
    }
783
784
    /**
785
     * Sets a handler to be executed after running the ordinary handler, just
786
     * before sending the response.
787
     * <p>
788
     *     This is an <b>unusual</b> method, so please be aware of its proper use. Its
789
     *     purpose is to allow the user to inject code to run after ordinary code, across
790
     *     all requests.
791
     * </p>
792
     * <p>
793
     *     For example, if the system would have returned a 404 NOT FOUND response,
794
     *     code can handle that situation in a switch case and adjust the response according
795
     *     to your programming.
796
     * </p>
797
     * <p>Here is an example</p>
798
     * <pre>{@code
799
     *
800
     *
801
     *      webFramework.registerLastMinuteHandler(TheRegister::lastMinuteHandlerCode);
802
     *
803
     * ...
804
     *
805
     *     private static IResponse lastMinuteHandlerCode(LastMinuteHandlerInputs inputs) {
806
     *         switch (inputs.response().statusCode()) {
807
     *             case CODE_404_NOT_FOUND -> {
808
     *                 return Response.buildResponse(
809
     *                         CODE_404_NOT_FOUND,
810
     *                         Map.of("Content-Type", "text/html; charset=UTF-8"),
811
     *                         "<p>No document was found</p>"));
812
     *             }
813
     *             case CODE_500_INTERNAL_SERVER_ERROR -> {
814
     *                 return Response.buildResponse(
815
     *                         CODE_500_INTERNAL_SERVER_ERROR,
816
     *                         Map.of("Content-Type", "text/html; charset=UTF-8"),
817
     *                         "<p>Server error occurred.</p>" ));
818
     *             }
819
     *             default -> {
820
     *                 return inputs.response();
821
     *             }
822
     *         }
823
     *     }
824
     * }
825
     * </pre>
826
     * @param lastMinuteHandler a function that will take a request and return a response, exactly like
827
     *                   we use in the other registration methods for this class.
828
     */
829
    public void registerLastMinuteHandler(ThrowingFunction<LastMinuteHandlerInputs, IResponse> lastMinuteHandler) {
830
        this.lastMinuteHandler = lastMinuteHandler;
831
    }
832
833
    /**
834
     * This allows users to add extra mappings
835
     * between file suffixes and mime types, in case
836
     * a user needs one that was not provided.
837
     * <p>
838
     *     This is made available through the
839
     *     web framework.
840
     * </p>
841
     * <p>
842
     *     Example:
843
     * </p>
844
     * <pre>
845
     * {@code webFramework.addMimeForSuffix().put("foo","text/foo")}
846
     * </pre>
847
     */
848
    public void addMimeForSuffix(String suffix, String mimeType) {
849
        fileSuffixToMime.put(suffix, mimeType);
850
    }
851
}

Mutations

52

1.1
Location : getSuffixToMimeMappings
Killed by : com.renomad.minum.web.WebFrameworkTests.test_ExtraMimeMappings(com.renomad.minum.web.WebFrameworkTests)
replaced return value with Collections.emptyMap for com/renomad/minum/web/WebFramework::getSuffixToMimeMappings → KILLED

106

1.1
Location : httpProcessing
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

2.2
Location : httpProcessing
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

114

1.1
Location : httpProcessing
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

121

1.1
Location : httpProcessing
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
removed call to com/renomad/minum/web/WebFramework::checkIfSuspiciousPath → KILLED

131

1.1
Location : httpProcessing
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

137

1.1
Location : httpProcessing
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
removed call to com/renomad/minum/web/WebFramework::addOptionalExtraHeaders → KILLED

138

1.1
Location : httpProcessing
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
removed call to com/renomad/minum/web/WebFramework::addKeepAliveTimeout → KILLED

142

1.1
Location : httpProcessing
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
removed call to com/renomad/minum/web/WebFramework::applyContentLength → KILLED

143

1.1
Location : httpProcessing
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
removed call to com/renomad/minum/web/WebFramework::confirmBodyHasContentType → KILLED

146

1.1
Location : httpProcessing
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
removed call to com/renomad/minum/web/ISocketWrapper::send → KILLED

151

1.1
Location : httpProcessing
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

157

1.1
Location : httpProcessing
Killed by : none
removed call to com/renomad/minum/web/IResponse::sendBody → TIMED_OUT

160

1.1
Location : httpProcessing
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
removed call to com/renomad/minum/web/ISocketWrapper::flush → KILLED

165

1.1
Location : httpProcessing
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

168

1.1
Location : httpProcessing
Killed by : com.renomad.minum.web.WebTests
removed call to com/renomad/minum/web/WebFramework::handleReadTimedOut → KILLED

170

1.1
Location : httpProcessing
Killed by : com.renomad.minum.FunctionalTests
removed call to com/renomad/minum/web/WebFramework::handleForbiddenUse → KILLED

172

1.1
Location : httpProcessing
Killed by : com.renomad.minum.FunctionalTests
removed call to com/renomad/minum/web/WebFramework::handleIOException → KILLED

181

1.1
Location : handleIOException
Killed by : com.renomad.minum.web.WebFrameworkTests.testHandleIoException(com.renomad.minum.web.WebFrameworkTests)
negated conditional → KILLED

2.2
Location : handleIOException
Killed by : com.renomad.minum.web.WebFrameworkTests.testHandleIoException(com.renomad.minum.web.WebFrameworkTests)
negated conditional → KILLED

189

1.1
Location : handleForbiddenUse
Killed by : com.renomad.minum.web.WebFrameworkTests.test_HandleForbiddenUse(com.renomad.minum.web.WebFrameworkTests)
negated conditional → KILLED

204

1.1
Location : handleReadTimedOut
Killed by : com.renomad.minum.web.WebTests
negated conditional → KILLED

223

1.1
Location : processRequest
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

228

1.1
Location : processRequest
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

248

1.1
Location : processRequest
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

252

1.1
Location : processRequest
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
replaced return value with null for com/renomad/minum/web/WebFramework::processRequest → KILLED

269

1.1
Location : getHeaders
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
replaced return value with null for com/renomad/minum/web/WebFramework::getHeaders → KILLED

277

1.1
Location : determineIfKeepAlive
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

279

1.1
Location : determineIfKeepAlive
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

280

1.1
Location : determineIfKeepAlive
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

284

1.1
Location : determineIfKeepAlive
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
replaced boolean return with false for com/renomad/minum/web/WebFramework::determineIfKeepAlive → KILLED

2.2
Location : determineIfKeepAlive
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
replaced boolean return with true for com/renomad/minum/web/WebFramework::determineIfKeepAlive → KILLED

292

1.1
Location : getProcessedRequestLine
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
replaced return value with null for com/renomad/minum/web/WebFramework::getProcessedRequestLine → KILLED

298

1.1
Location : checkIfSuspiciousPath
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

310

1.1
Location : dumpIfAttacker
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

311

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

312

1.1
Location : dumpIfAttacker
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

313

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

315

1.1
Location : dumpIfAttacker
Killed by : none
removed call to com/renomad/minum/web/WebFramework::dumpIfAttacker → SURVIVED
Covering tests

316

1.1
Location : dumpIfAttacker
Killed by : none
replaced boolean return with false for com/renomad/minum/web/WebFramework::dumpIfAttacker → SURVIVED
Covering tests

322

1.1
Location : dumpIfAttacker
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

337

1.1
Location : lambda$addDefaultHeaders$17
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
replaced return value with null for com/renomad/minum/web/WebFramework::lambda$addDefaultHeaders$17 → KILLED

353

1.1
Location : addDefaultHeaders
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
replaced return value with null for com/renomad/minum/web/WebFramework::addDefaultHeaders → KILLED

376

1.1
Location : lambda$confirmBodyHasContentType$18
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
replaced boolean return with false for com/renomad/minum/web/WebFramework::lambda$confirmBodyHasContentType$18 → KILLED

2.2
Location : lambda$confirmBodyHasContentType$18
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
replaced boolean return with true for com/renomad/minum/web/WebFramework::lambda$confirmBodyHasContentType$18 → KILLED

379

1.1
Location : confirmBodyHasContentType
Killed by : com.renomad.minum.web.BodyProcessorTests
negated conditional → KILLED

2.2
Location : confirmBodyHasContentType
Killed by : com.renomad.minum.web.BodyProcessorTests
negated conditional → KILLED

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

390

1.1
Location : addKeepAliveTimeout
Killed by : com.renomad.minum.web.WebPerformanceTests.test1(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

415

1.1
Location : potentiallyCompress
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

416

1.1
Location : potentiallyCompress
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
replaced return value with null for com/renomad/minum/web/WebFramework::potentiallyCompress → KILLED

418

1.1
Location : potentiallyCompress
Killed by : com.renomad.minum.web.WebTests
replaced return value with null for com/renomad/minum/web/WebFramework::potentiallyCompress → KILLED

436

1.1
Location : compressBodyIfRequested
Killed by : com.renomad.minum.web.WebFrameworkTests.test_compressIfRequested(com.renomad.minum.web.WebFrameworkTests)
negated conditional → KILLED

437

1.1
Location : compressBodyIfRequested
Killed by : com.renomad.minum.web.WebFrameworkTests.test_compressIfRequested(com.renomad.minum.web.WebFrameworkTests)
negated conditional → KILLED

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

3.3
Location : compressBodyIfRequested
Killed by : com.renomad.minum.web.WebFrameworkTests.test_compressIfRequested(com.renomad.minum.web.WebFrameworkTests)
negated conditional → KILLED

440

1.1
Location : compressBodyIfRequested
Killed by : com.renomad.minum.web.WebFrameworkTests.test_compressIfRequested(com.renomad.minum.web.WebFrameworkTests)
replaced return value with null for com/renomad/minum/web/WebFramework::compressBodyIfRequested → KILLED

442

1.1
Location : compressBodyIfRequested
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
replaced return value with null for com/renomad/minum/web/WebFramework::compressBodyIfRequested → KILLED

459

1.1
Location : findEndpointForThisStartline
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

464

1.1
Location : findEndpointForThisStartline
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

469

1.1
Location : findEndpointForThisStartline
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

475

1.1
Location : findEndpointForThisStartline
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
replaced return value with null for com/renomad/minum/web/WebFramework::findEndpointForThisStartline → KILLED

483

1.1
Location : findHandlerByFilesOnDisk
Killed by : com.renomad.minum.web.WebTests
negated conditional → KILLED

2.2
Location : findHandlerByFilesOnDisk
Killed by : com.renomad.minum.web.WebTests
negated conditional → KILLED

488

1.1
Location : lambda$findHandlerByFilesOnDisk$22
Killed by : com.renomad.minum.web.WebTests
replaced return value with null for com/renomad/minum/web/WebFramework::lambda$findHandlerByFilesOnDisk$22 → KILLED

2.2
Location : findHandlerByFilesOnDisk
Killed by : com.renomad.minum.web.WebTests
replaced return value with null for com/renomad/minum/web/WebFramework::findHandlerByFilesOnDisk → KILLED

504

1.1
Location : readStaticFile
Killed by : com.renomad.minum.web.WebFrameworkTests.test_readStaticFile_Edge_Colon(com.renomad.minum.web.WebFrameworkTests)
removed call to com/renomad/minum/utils/FileUtils::checkForBadFilePatterns → KILLED

507

1.1
Location : readStaticFile
Killed by : com.renomad.minum.web.WebFrameworkTests.test_readStaticFile_Edge_Colon(com.renomad.minum.web.WebFrameworkTests)
replaced return value with null for com/renomad/minum/web/WebFramework::readStaticFile → KILLED

512

1.1
Location : readStaticFile
Killed by : com.renomad.minum.web.WebTests
removed call to com/renomad/minum/utils/FileUtils::checkFileIsWithinDirectory → KILLED

515

1.1
Location : readStaticFile
Killed by : com.renomad.minum.web.WebFrameworkTests.test_readStaticFile_Edge_Directory(com.renomad.minum.web.WebFrameworkTests)
replaced return value with null for com/renomad/minum/web/WebFramework::readStaticFile → KILLED

520

1.1
Location : readStaticFile
Killed by : com.renomad.minum.web.WebFrameworkTests.test_readStaticFile_Edge_CurrentDirectory(com.renomad.minum.web.WebFrameworkTests)
negated conditional → KILLED

522

1.1
Location : readStaticFile
Killed by : com.renomad.minum.web.WebFrameworkTests.test_readStaticFile_Edge_CurrentDirectory(com.renomad.minum.web.WebFrameworkTests)
replaced return value with null for com/renomad/minum/web/WebFramework::readStaticFile → KILLED

528

1.1
Location : readStaticFile
Killed by : com.renomad.minum.web.WebFrameworkTests.test_readStaticFile_HTML(com.renomad.minum.web.WebFrameworkTests)
negated conditional → KILLED

2.2
Location : readStaticFile
Killed by : com.renomad.minum.FunctionalTests
changed conditional boundary → KILLED

529

1.1
Location : readStaticFile
Killed by : com.renomad.minum.web.WebFrameworkTests.test_readStaticFile_HTML(com.renomad.minum.web.WebFrameworkTests)
Replaced integer addition with subtraction → KILLED

536

1.1
Location : readStaticFile
Killed by : com.renomad.minum.web.WebFrameworkTests.test_readStaticFile_HTML(com.renomad.minum.web.WebFrameworkTests)
negated conditional → KILLED

540

1.1
Location : readStaticFile
Killed by : com.renomad.minum.web.WebFrameworkTests.test_readStaticFile_IOException(com.renomad.minum.web.WebFrameworkTests)
negated conditional → KILLED

2.2
Location : readStaticFile
Killed by : com.renomad.minum.FunctionalTests
changed conditional boundary → KILLED

542

1.1
Location : readStaticFile
Killed by : com.renomad.minum.web.WebFrameworkTests.test_readStaticFile_HTML(com.renomad.minum.web.WebFrameworkTests)
replaced return value with null for com/renomad/minum/web/WebFramework::readStaticFile → KILLED

544

1.1
Location : readStaticFile
Killed by : com.renomad.minum.FunctionalTests
replaced return value with null for com/renomad/minum/web/WebFramework::readStaticFile → KILLED

549

1.1
Location : readStaticFile
Killed by : com.renomad.minum.web.WebFrameworkTests.test_readStaticFile_IOException(com.renomad.minum.web.WebFrameworkTests)
replaced return value with null for com/renomad/minum/web/WebFramework::readStaticFile → KILLED

561

1.1
Location : createOkResponseForStaticFiles
Killed by : com.renomad.minum.web.WebFrameworkTests.test_readStaticFile_HTML(com.renomad.minum.web.WebFrameworkTests)
replaced return value with null for com/renomad/minum/web/WebFramework::createOkResponseForStaticFiles → KILLED

577

1.1
Location : createOkResponseForLargeStaticFiles
Killed by : com.renomad.minum.FunctionalTests
replaced return value with null for com/renomad/minum/web/WebFramework::createOkResponseForLargeStaticFiles → KILLED

606

1.1
Location : lambda$findHandlerByPartialMatch$27
Killed by : com.renomad.minum.web.WebTests
negated conditional → KILLED

2.2
Location : lambda$findHandlerByPartialMatch$27
Killed by : com.renomad.minum.web.WebTests
replaced boolean return with true for com/renomad/minum/web/WebFramework::lambda$findHandlerByPartialMatch$27 → KILLED

607

1.1
Location : lambda$findHandlerByPartialMatch$27
Killed by : com.renomad.minum.web.WebTests
negated conditional → KILLED

609

1.1
Location : findHandlerByPartialMatch
Killed by : com.renomad.minum.web.WebTests
negated conditional → KILLED

610

1.1
Location : findHandlerByPartialMatch
Killed by : com.renomad.minum.web.WebTests
replaced return value with null for com/renomad/minum/web/WebFramework::findHandlerByPartialMatch → KILLED

634

1.1
Location : <init>
Killed by : com.renomad.minum.web.WebFrameworkTests.test_ExtraMimeMappings_NoValues(com.renomad.minum.web.WebFrameworkTests)
negated conditional → KILLED

655

1.1
Location : <init>
Killed by : com.renomad.minum.web.WebFrameworkTests.test_readStaticFile_IOException(com.renomad.minum.web.WebFrameworkTests)
negated conditional → KILLED

664

1.1
Location : <init>
Killed by : com.renomad.minum.web.WebFrameworkTests.test_readStaticFile_HTML(com.renomad.minum.web.WebFrameworkTests)
removed call to com/renomad/minum/web/WebFramework::addDefaultValuesForMimeMap → KILLED

665

1.1
Location : <init>
Killed by : com.renomad.minum.web.WebPerformanceTests.test2(com.renomad.minum.web.WebPerformanceTests)
removed call to com/renomad/minum/web/WebFramework::readExtraMimeMappings → KILLED

669

1.1
Location : readExtraMimeMappings
Killed by : com.renomad.minum.web.WebFrameworkTests.test_ExtraMimeMappings(com.renomad.minum.web.WebFrameworkTests)
negated conditional → KILLED

2.2
Location : readExtraMimeMappings
Killed by : com.renomad.minum.web.WebFrameworkTests.test_ExtraMimeMappings(com.renomad.minum.web.WebFrameworkTests)
negated conditional → KILLED

670

1.1
Location : readExtraMimeMappings
Killed by : com.renomad.minum.web.WebFrameworkTests.test_ExtraMimeMappings_NoValues(com.renomad.minum.web.WebFrameworkTests)
Replaced integer modulus with multiplication → KILLED

2.2
Location : readExtraMimeMappings
Killed by : com.renomad.minum.web.WebFrameworkTests.test_ExtraMimeMappings_NoValues(com.renomad.minum.web.WebFrameworkTests)
negated conditional → KILLED

674

1.1
Location : readExtraMimeMappings
Killed by : com.renomad.minum.web.WebFrameworkTests.test_ExtraMimeMappings(com.renomad.minum.web.WebFrameworkTests)
negated conditional → KILLED

2.2
Location : readExtraMimeMappings
Killed by : com.renomad.minum.web.WebFrameworkTests.test_ExtraMimeMappings_NoValues(com.renomad.minum.web.WebFrameworkTests)
changed conditional boundary → KILLED

676

1.1
Location : readExtraMimeMappings
Killed by : com.renomad.minum.web.WebFrameworkTests.test_ExtraMimeMappings_NoValues(com.renomad.minum.web.WebFrameworkTests)
Replaced integer addition with subtraction → KILLED

692

1.1
Location : registerPath
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

2.2
Location : registerPath
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

698

1.1
Location : registerPath
Killed by : com.renomad.minum.web.WebPerformanceTests.test3(com.renomad.minum.web.WebPerformanceTests)
negated conditional → KILLED

717

1.1
Location : registerPartialPath
Killed by : com.renomad.minum.web.WebTests
negated conditional → KILLED

2.2
Location : registerPartialPath
Killed by : com.renomad.minum.web.WebTests
negated conditional → KILLED

722

1.1
Location : registerPartialPath
Killed by : com.renomad.minum.web.WebTests
negated conditional → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0