WebFramework.java

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

Mutations

50

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

104

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

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

112

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

119

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

128

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

136

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

137

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

141

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

142

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

145

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

150

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

156

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

159

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

165

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

172

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

174

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

176

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

184

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

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

192

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

207

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

226

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

231

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

251

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

255

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

272

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

300

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

302

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

303

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

306

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

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

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

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

317

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

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

325

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

329

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

341

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

342

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

343

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

344

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

346

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

347

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

353

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

368

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

384

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

407

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

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

410

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

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

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

421

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

446

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

447

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

449

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

467

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

468

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

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

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

471

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

473

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

490

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

495

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

500

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

506

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

514

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

519

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

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

535

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

538

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

543

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

546

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

551

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

553

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

559

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

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

560

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

567

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

571

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

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

573

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

575

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

580

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

592

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

608

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

637

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

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

638

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

640

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

641

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

665

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

685

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

694

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

695

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

699

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

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

700

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

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

704

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

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

706

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

722

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

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

728

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

747

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

752

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

Active mutators

Tests examined


Report generated by PIT 1.17.0