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

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

78

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

116

1.1
Location : httpProcessing
Killed by : none
negated conditional → TIMED_OUT

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

124

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

131

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

140

1.1
Location : httpProcessing
Killed by : none
negated conditional → TIMED_OUT

148

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

149

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

153

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

154

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

157

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

162

1.1
Location : httpProcessing
Killed by : none
negated conditional → TIMED_OUT

168

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

171

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

177

1.1
Location : httpProcessing
Killed by : none
negated conditional → TIMED_OUT

184

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

186

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

188

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

196

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

204

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

219

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

238

1.1
Location : processRequest
Killed by : none
negated conditional → TIMED_OUT

243

1.1
Location : processRequest
Killed by : none
negated conditional → TIMED_OUT

263

1.1
Location : processRequest
Killed by : none
negated conditional → TIMED_OUT

267

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

284

1.1
Location : getHeaders
Killed by : none
replaced return value with null for com/renomad/minum/web/WebFramework::getHeaders → TIMED_OUT

312

1.1
Location : determineIfKeepAlive
Killed by : none
negated conditional → TIMED_OUT

314

1.1
Location : determineIfKeepAlive
Killed by : none
negated conditional → TIMED_OUT

315

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

318

1.1
Location : determineIfKeepAlive
Killed by : none
negated conditional → TIMED_OUT

2.2
Location : determineIfKeepAlive
Killed by : none
negated conditional → TIMED_OUT

3.3
Location : determineIfKeepAlive
Killed by : none
changed conditional boundary → TIMED_OUT

4.4
Location : determineIfKeepAlive
Killed by : none
negated conditional → TIMED_OUT

329

1.1
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

2.2
Location : determineIfKeepAlive
Killed by : none
replaced boolean return with false for com/renomad/minum/web/WebFramework::determineIfKeepAlive → TIMED_OUT

337

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

341

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

353

1.1
Location : dumpIfAttacker
Killed by : none
negated conditional → TIMED_OUT

354

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

355

1.1
Location : dumpIfAttacker
Killed by : none
negated conditional → TIMED_OUT

356

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

358

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

359

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

365

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

381

1.1
Location : lambda$addDefaultHeaders$19
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$19 → KILLED

397

1.1
Location : addDefaultHeaders
Killed by : none
replaced return value with null for com/renomad/minum/web/WebFramework::addDefaultHeaders → TIMED_OUT

408

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

411

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

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

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

422

1.1
Location : addKeepAliveTimeout
Killed by : none
negated conditional → TIMED_OUT

447

1.1
Location : potentiallyCompress
Killed by : none
negated conditional → TIMED_OUT

448

1.1
Location : potentiallyCompress
Killed by : none
replaced return value with null for com/renomad/minum/web/WebFramework::potentiallyCompress → TIMED_OUT

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

468

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

469

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 : none
changed conditional boundary → TIMED_OUT

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

472

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

474

1.1
Location : compressBodyIfRequested
Killed by : none
replaced return value with null for com/renomad/minum/web/WebFramework::compressBodyIfRequested → TIMED_OUT

491

1.1
Location : findEndpointForThisStartline
Killed by : none
negated conditional → TIMED_OUT

496

1.1
Location : findEndpointForThisStartline
Killed by : none
negated conditional → TIMED_OUT

501

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

507

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

515

1.1
Location : findHandlerByFilesOnDisk
Killed by : none
negated conditional → TIMED_OUT

2.2
Location : findHandlerByFilesOnDisk
Killed by : com.renomad.minum.FunctionalTests.test_PathFunction_Response(com.renomad.minum.FunctionalTests)
negated conditional → KILLED

520

1.1
Location : lambda$findHandlerByFilesOnDisk$23
Killed by : none
replaced return value with null for com/renomad/minum/web/WebFramework::lambda$findHandlerByFilesOnDisk$23 → TIMED_OUT

2.2
Location : findHandlerByFilesOnDisk
Killed by : none
replaced return value with null for com/renomad/minum/web/WebFramework::findHandlerByFilesOnDisk → TIMED_OUT

536

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

539

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

544

1.1
Location : readStaticFile
Killed by : none
removed call to com/renomad/minum/utils/FileUtils::checkFileIsWithinDirectory → TIMED_OUT

547

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

552

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

554

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

560

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

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

561

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

568

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

572

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.testEndToEnd_Functional(com.renomad.minum.FunctionalTests)
changed conditional boundary → KILLED

574

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

576

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

581

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

593

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

609

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

637

1.1
Location : findHandlerByPathFunction
Killed by : com.renomad.minum.FunctionalTests.test_PathFunction_Response(com.renomad.minum.FunctionalTests)
negated conditional → KILLED

641

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

642

1.1
Location : lambda$findHandlerByPathFunction$28
Killed by : none
replaced return value with null for com/renomad/minum/web/WebFramework::lambda$findHandlerByPathFunction$28 → TIMED_OUT

666

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

686

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

695

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

696

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

700

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

701

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

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

705

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)
changed conditional boundary → KILLED

707

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

723

1.1
Location : registerPath
Killed by : none
negated conditional → TIMED_OUT

2.2
Location : registerPath
Killed by : none
negated conditional → TIMED_OUT

729

1.1
Location : registerPath
Killed by : none
negated conditional → TIMED_OUT

733

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

742

1.1
Location : checkForDuplicatePartialPath
Killed by : none
negated conditional → TIMED_OUT

746

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

747

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

796

1.1
Location : lambda$registerPath$31
Killed by : none
replaced return value with Collections.emptyList for com/renomad/minum/web/WebFramework::lambda$registerPath$31 → TIMED_OUT

813

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

820

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

824

1.1
Location : registerPartialPath
Killed by : none
removed call to com/renomad/minum/web/WebFramework::checkForDuplicatePartialPath → TIMED_OUT

825

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

Active mutators

Tests examined


Report generated by PIT 1.17.0