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

Mutations

60

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

87

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

157

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

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

166

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

175

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

182

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

183

1.1
Location : httpProcessing
Killed by : com.renomad.minum.FunctionalTests.test_EdgeCase_Response_MultiCookies(com.renomad.minum.FunctionalTests)
removed call to com/renomad/minum/web/Headers::appendHeadersToBuilder → KILLED

184

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

188

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

2.2
Location : httpProcessing
Killed by : none
negated conditional → SURVIVED
Covering tests

3.3
Location : httpProcessing
Killed by : com.renomad.minum.FunctionalTests.test_EdgeCase_Response_MultiCookies(com.renomad.minum.FunctionalTests)
changed conditional boundary → KILLED

195

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

196

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

201

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

211

1.1
Location : httpProcessing
Killed by : com.renomad.minum.FunctionalTests.test_EdgeCase_BadRequest(com.renomad.minum.FunctionalTests)
removed call to java/lang/StringBuilder::setLength → KILLED

213

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

219

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

221

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

223

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

227

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

233

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

240

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

242

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

254

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

255

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

260

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

263

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

274

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

291

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

306

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

311

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

329

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

333

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

350

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

378

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

380

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

381

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

384

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

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

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

4.4
Location : determineIfKeepAlive
Killed by : com.renomad.minum.FunctionalTests.test_EdgeCase_PostHandler_IgnoreBody(com.renomad.minum.FunctionalTests)
negated conditional → KILLED

395

1.1
Location : determineIfKeepAlive
Killed by : com.renomad.minum.FunctionalTests.test_PathFunction_Response(com.renomad.minum.FunctionalTests)
replaced boolean return with true for com/renomad/minum/web/WebFramework::determineIfKeepAlive → KILLED

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

403

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

407

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

419

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

420

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

421

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

422

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

424

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

425

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

431

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

446

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

466

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

469

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

2.2
Location : confirmBodyHasContentType
Killed by : com.renomad.minum.FunctionalTests.test_PathFunction_Response(com.renomad.minum.FunctionalTests)
changed conditional boundary → KILLED

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

480

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

510

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

511

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

515

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

517

1.1
Location : lambda$compressBodyIfRequested$21
Killed by : none
Replaced double multiplication with division → SURVIVED
Covering tests

2.2
Location : lambda$compressBodyIfRequested$21
Killed by : none
Replaced double division with multiplication → SURVIVED Covering tests

518

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

524

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

536

1.1
Location : compressBody
Killed by : com.renomad.minum.web.CachingAndCompressionTests
removed call to java/util/zip/GZIPOutputStream::write → KILLED

537

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

557

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

562

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

567

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

573

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

582

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

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

585

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

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

614

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

616

1.1
Location : readStaticFile
Killed by : com.renomad.minum.web.WebFrameworkTests
removed call to java/util/concurrent/locks/ReentrantLock::lock → KILLED

619

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

621

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

624

1.1
Location : readStaticFile
Killed by : com.renomad.minum.FunctionalTests.test_PathFunction_Response_Range(com.renomad.minum.FunctionalTests)
removed call to java/util/concurrent/locks/ReentrantLock::unlock → KILLED

629

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

632

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

636

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

639

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

643

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

645

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

649

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

651

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

652

1.1
Location : readStaticFile
Killed by : com.renomad.minum.web.CachingAndCompressionTests
changed conditional boundary → KILLED

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

655

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

658

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

663

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

672

1.1
Location : getMimeString
Killed by : none
changed conditional boundary → TIMED_OUT

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

673

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

680

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

683

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

696

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

698

1.1
Location : createOkResponseForStaticFiles
Killed by : com.renomad.minum.web.CachingAndCompressionTests
removed call to com/renomad/minum/web/WebFramework::compressBody → KILLED

702

1.1
Location : createOkResponseForStaticFiles
Killed by : com.renomad.minum.web.CachingAndCompressionTests
Replaced double division with multiplication → KILLED

2.2
Location : createOkResponseForStaticFiles
Killed by : com.renomad.minum.web.CachingAndCompressionTests
Replaced double multiplication with division → KILLED

703

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

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

710

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

711

1.1
Location : lambda$createOkResponseForStaticFiles$36
Killed by : com.renomad.minum.FunctionalTests.testEndToEnd_Functional(com.renomad.minum.FunctionalTests)
removed call to com/renomad/minum/web/ISocketWrapper::send → KILLED

726

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

755

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

759

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

760

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

792

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

802

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

818

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

828

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

829

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

833

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

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

834

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

838

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

840

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

856

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

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

862

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

866

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

875

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

879

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

880

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

929

1.1
Location : lambda$registerPath$40
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$40 → KILLED

946

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

953

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

957

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

958

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