| 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 |
|
| 87 |
1.1 |
|
| 157 |
1.1 2.2 |
|
| 166 |
1.1 |
|
| 175 |
1.1 |
|
| 182 |
1.1 |
|
| 183 |
1.1 |
|
| 184 |
1.1 |
|
| 188 |
1.1 2.2 3.3 |
|
| 195 |
1.1 |
|
| 196 |
1.1 |
|
| 201 |
1.1 |
|
| 211 |
1.1 |
|
| 213 |
1.1 |
|
| 219 |
1.1 |
|
| 221 |
1.1 |
|
| 223 |
1.1 |
|
| 227 |
1.1 |
|
| 233 |
1.1 |
|
| 240 |
1.1 |
|
| 242 |
1.1 |
|
| 254 |
1.1 2.2 |
|
| 255 |
1.1 |
|
| 260 |
1.1 2.2 |
|
| 263 |
1.1 |
|
| 274 |
1.1 |
|
| 291 |
1.1 |
|
| 306 |
1.1 |
|
| 311 |
1.1 |
|
| 329 |
1.1 |
|
| 333 |
1.1 |
|
| 350 |
1.1 |
|
| 378 |
1.1 |
|
| 380 |
1.1 |
|
| 381 |
1.1 |
|
| 384 |
1.1 2.2 3.3 4.4 |
|
| 395 |
1.1 2.2 |
|
| 403 |
1.1 |
|
| 407 |
1.1 |
|
| 419 |
1.1 |
|
| 420 |
1.1 |
|
| 421 |
1.1 |
|
| 422 |
1.1 |
|
| 424 |
1.1 |
|
| 425 |
1.1 |
|
| 431 |
1.1 |
|
| 446 |
1.1 |
|
| 466 |
1.1 |
|
| 469 |
1.1 2.2 3.3 |
|
| 480 |
1.1 |
|
| 510 |
1.1 |
|
| 511 |
1.1 |
|
| 515 |
1.1 |
|
| 517 |
1.1 2.2 |
|
| 518 |
1.1 |
|
| 524 |
1.1 |
|
| 536 |
1.1 |
|
| 537 |
1.1 |
|
| 557 |
1.1 |
|
| 562 |
1.1 |
|
| 567 |
1.1 |
|
| 573 |
1.1 |
|
| 582 |
1.1 2.2 |
|
| 585 |
1.1 2.2 |
|
| 614 |
1.1 |
|
| 616 |
1.1 |
|
| 619 |
1.1 |
|
| 621 |
1.1 |
|
| 624 |
1.1 |
|
| 629 |
1.1 |
|
| 632 |
1.1 |
|
| 636 |
1.1 |
|
| 639 |
1.1 |
|
| 643 |
1.1 |
|
| 645 |
1.1 |
|
| 649 |
1.1 |
|
| 651 |
1.1 |
|
| 652 |
1.1 2.2 |
|
| 655 |
1.1 |
|
| 658 |
1.1 |
|
| 663 |
1.1 |
|
| 672 |
1.1 2.2 |
|
| 673 |
1.1 |
|
| 680 |
1.1 |
|
| 683 |
1.1 |
|
| 696 |
1.1 |
|
| 698 |
1.1 |
|
| 702 |
1.1 2.2 |
|
| 703 |
1.1 2.2 |
|
| 710 |
1.1 |
|
| 711 |
1.1 |
|
| 726 |
1.1 |
|
| 755 |
1.1 |
|
| 759 |
1.1 |
|
| 760 |
1.1 |
|
| 792 |
1.1 |
|
| 802 |
1.1 |
|
| 818 |
1.1 |
|
| 828 |
1.1 |
|
| 829 |
1.1 |
|
| 833 |
1.1 2.2 |
|
| 834 |
1.1 2.2 |
|
| 838 |
1.1 2.2 |
|
| 840 |
1.1 |
|
| 856 |
1.1 2.2 |
|
| 862 |
1.1 |
|
| 866 |
1.1 |
|
| 875 |
1.1 |
|
| 879 |
1.1 |
|
| 880 |
1.1 |
|
| 929 |
1.1 |
|
| 946 |
1.1 2.2 |
|
| 953 |
1.1 |
|
| 957 |
1.1 |
|
| 958 |
1.1 |