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

Mutations

51

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

101

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

112

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

2.2
Location : lambda$makePrimaryHttpHandler$5
Killed by : com.renomad.minum.web.WebTests
negated conditional → KILLED

120

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

127

1.1
Location : lambda$makePrimaryHttpHandler$5
Killed by : com.renomad.minum.web.WebTests
removed call to com/renomad/minum/web/WebFramework::checkIfSuspiciousPath → KILLED

132

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

141

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

147

1.1
Location : lambda$makePrimaryHttpHandler$5
Killed by : com.renomad.minum.web.WebTests
removed call to com/renomad/minum/web/WebFramework::addOptionalExtraHeaders → KILLED

148

1.1
Location : lambda$makePrimaryHttpHandler$5
Killed by : com.renomad.minum.web.WebTests
removed call to com/renomad/minum/web/WebFramework::addKeepAliveTimeout → KILLED

152

1.1
Location : lambda$makePrimaryHttpHandler$5
Killed by : com.renomad.minum.web.WebTests
removed call to com/renomad/minum/web/WebFramework::applyContentLength → KILLED

153

1.1
Location : lambda$makePrimaryHttpHandler$5
Killed by : none
removed call to com/renomad/minum/web/WebFramework::confirmBodyHasContentType → SURVIVED
Covering tests

156

1.1
Location : lambda$makePrimaryHttpHandler$5
Killed by : com.renomad.minum.web.WebTests
removed call to com/renomad/minum/web/ISocketWrapper::send → KILLED

161

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

167

1.1
Location : lambda$makePrimaryHttpHandler$5
Killed by : none
removed call to com/renomad/minum/web/Response::sendBody → TIMED_OUT

172

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

175

1.1
Location : lambda$makePrimaryHttpHandler$5
Killed by : com.renomad.minum.web.WebTests
removed call to com/renomad/minum/web/WebFramework::handleReadTimedOut → KILLED

177

1.1
Location : lambda$makePrimaryHttpHandler$5
Killed by : com.renomad.minum.FunctionalTests
removed call to com/renomad/minum/web/WebFramework::handleForbiddenUse → KILLED

179

1.1
Location : lambda$makePrimaryHttpHandler$5
Killed by : none
removed call to com/renomad/minum/web/WebFramework::handleIOException → SURVIVED
Covering tests

189

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

197

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

212

1.1
Location : handleReadTimedOut
Killed by : none
negated conditional → SURVIVED
Covering tests

231

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

236

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

256

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

260

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

279

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

287

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

289

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

290

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

294

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

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

306

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

312

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

324

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

325

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

326

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

327

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

329

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

330

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

336

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

351

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

353

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

2.2
Location : isThereIsABody
Killed by : com.renomad.minum.web.WebTests
changed conditional boundary → KILLED

3.3
Location : isThereIsABody
Killed by : none
negated conditional → TIMED_OUT

357

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

2.2
Location : lambda$isThereIsABody$19
Killed by : com.renomad.minum.web.WebTests
replaced boolean return with false for com/renomad/minum/web/WebFramework::lambda$isThereIsABody$19 → KILLED

3.3
Location : lambda$isThereIsABody$19
Killed by : com.renomad.minum.web.WebTests
replaced boolean return with true for com/renomad/minum/web/WebFramework::lambda$isThereIsABody$19 → KILLED

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

5.5
Location : isThereIsABody
Killed by : com.renomad.minum.web.WebTests
negated conditional → KILLED

360

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

369

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

384

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

393

1.1
Location : lambda$addOptionalExtraHeaders$21
Killed by : com.renomad.minum.web.WebTests
replaced return value with "" for com/renomad/minum/web/WebFramework::lambda$addOptionalExtraHeaders$21 → KILLED

402

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

2.2
Location : lambda$confirmBodyHasContentType$22
Killed by : com.renomad.minum.web.WebTests
replaced boolean return with false for com/renomad/minum/web/WebFramework::lambda$confirmBodyHasContentType$22 → KILLED

405

1.1
Location : confirmBodyHasContentType
Killed by : com.renomad.minum.web.WebTests
changed conditional boundary → KILLED

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

416

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

444

1.1
Location : lambda$potentiallyCompress$23
Killed by : com.renomad.minum.web.WebTests
replaced boolean return with false for com/renomad/minum/web/WebFramework::lambda$potentiallyCompress$23 → KILLED

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

446

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

449

1.1
Location : potentiallyCompress
Killed by : none
negated conditional → SURVIVED
Covering tests

450

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

453

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

468

1.1
Location : determineCompressible
Killed by : com.renomad.minum.web.WebFrameworkTests.test_CompressMoreMimeTypes(com.renomad.minum.web.WebFrameworkTests)
replaced boolean return with false for com/renomad/minum/web/WebFramework::determineCompressible → KILLED

2.2
Location : determineCompressible
Killed by : com.renomad.minum.web.WebFrameworkTests.test_CompressMoreMimeTypes(com.renomad.minum.web.WebFrameworkTests)
replaced boolean return with true for com/renomad/minum/web/WebFramework::determineCompressible → KILLED

486

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

487

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.WebFrameworkTests.test_compressIfRequested(com.renomad.minum.web.WebFrameworkTests)
negated conditional → KILLED

3.3
Location : compressBodyIfRequested
Killed by : com.renomad.minum.web.WebTests
changed conditional boundary → KILLED

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

490

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

492

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

509

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

514

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

519

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

525

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

533

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

538

1.1
Location : lambda$findHandlerByFilesOnDisk$27
Killed by : com.renomad.minum.web.WebTests
replaced return value with null for com/renomad/minum/web/WebFramework::lambda$findHandlerByFilesOnDisk$27 → 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

554

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

557

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

562

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

565

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

570

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

572

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

578

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

579

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

586

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

590

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

592

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

594

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

599

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

611

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

627

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

656

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

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

657

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

659

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

660

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

699

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

708

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

709

1.1
Location : <init>
Killed by : none
removed call to com/renomad/minum/web/WebFramework::readExtraMimeMappings → SURVIVED
Covering tests

713

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

716

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

718

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

734

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

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

740

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

759

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

764

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