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

Mutations

53

1.1
Location : getSuffixToMimeMappings
Killed by : none
replaced return value with Collections.emptyMap for com/renomad/minum/web/WebFramework::getSuffixToMimeMappings → TIMED_OUT

80

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

134

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

143

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

152

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

160

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

161

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

165

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

166

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

171

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

181

1.1
Location : httpProcessing
Killed by : none
removed call to java/lang/StringBuilder::setLength → TIMED_OUT

189

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

191

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

193

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

197

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

203

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

210

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

212

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

224

1.1
Location : finalExceptionHandler
Killed by : com.renomad.minum.web.WebFrameworkTests
negated conditional → KILLED

2.2
Location : finalExceptionHandler
Killed by : com.renomad.minum.web.WebFrameworkTests
negated conditional → KILLED

225

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

230

1.1
Location : finalExceptionHandler
Killed by : com.renomad.minum.web.WebFrameworkTests
negated conditional → KILLED

2.2
Location : finalExceptionHandler
Killed by : com.renomad.minum.web.WebFrameworkTests
negated conditional → KILLED

233

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

244

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

261

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

276

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

281

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

299

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

303

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

320

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

348

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

350

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

351

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

354

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 : com.renomad.minum.FunctionalTests.test_EdgeCase_PostHandler_IgnoreBody(com.renomad.minum.FunctionalTests)
negated conditional → KILLED

365

1.1
Location : determineIfKeepAlive
Killed by : com.renomad.minum.FunctionalTests.test_EdgeCase_Response_MultiCookies(com.renomad.minum.FunctionalTests)
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

373

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

377

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

389

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

390

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

391

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

392

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

394

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

395

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

401

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

416

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

427

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

438

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

441

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

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

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

452

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

477

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

478

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

480

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

498

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

499

1.1
Location : compressBodyIfRequested
Killed by : 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
negated conditional → KILLED

504

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

510

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

522

1.1
Location : compressBody
Killed by : none
removed call to java/util/zip/GZIPOutputStream::write → SURVIVED
Covering tests

523

1.1
Location : compressBody
Killed by : none
removed call to java/util/zip/GZIPOutputStream::finish → SURVIVED
Covering tests

544

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

549

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

554

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

560

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

568

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

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

573

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

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

589

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

592

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

597

1.1
Location : readStaticFile
Killed by : com.renomad.minum.FunctionalTests.test_PathFunction_Response(com.renomad.minum.FunctionalTests)
removed call to com/renomad/minum/utils/IFileUtils::checkFileIsWithinDirectory → KILLED

600

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

605

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

607

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

613

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

2.2
Location : readStaticFile
Killed by : none
changed conditional boundary → SURVIVED
Covering tests

614

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

621

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

625

1.1
Location : readStaticFile
Killed by : none
changed conditional boundary → SURVIVED
Covering tests

2.2
Location : readStaticFile
Killed by : com.renomad.minum.web.WebFrameworkTests
negated conditional → KILLED

627

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

629

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

634

1.1
Location : readStaticFile
Killed by : none
replaced return value with null for com/renomad/minum/web/WebFramework::readStaticFile → SURVIVED
Covering tests

646

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

662

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

691

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

695

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

696

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

728

1.1
Location : <init>
Killed by : com.renomad.minum.web.WebEngineTests
negated conditional → KILLED

736

1.1
Location : <init>
Killed by : none
negated conditional → TIMED_OUT

752

1.1
Location : <init>
Killed by : none
negated conditional → TIMED_OUT

761

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

762

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

766

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

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

767

1.1
Location : readExtraMimeMappings
Killed by : com.renomad.minum.web.WebEngineTests
Replaced integer modulus with multiplication → KILLED

2.2
Location : readExtraMimeMappings
Killed by : com.renomad.minum.web.WebEngineTests
negated conditional → KILLED

771

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

2.2
Location : readExtraMimeMappings
Killed by : com.renomad.minum.web.WebEngineTests
changed conditional boundary → KILLED

773

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

789

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

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

795

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

799

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

808

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

812

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

813

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

862

1.1
Location : lambda$registerPath$32
Killed by : com.renomad.minum.FunctionalTests.test_EdgeCase_IOExceptionThrown_WebFramework(com.renomad.minum.FunctionalTests)
replaced return value with Collections.emptyList for com/renomad/minum/web/WebFramework::lambda$registerPath$32 → KILLED

879

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

886

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

890

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

891

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