Exemplo n.º 1
0
        public async Task InvokeAsync(HttpContext context)
        {
            var remoteServer = myRemoteServers.LookupRemoteServer(context.Request.Path, out var remainingPath);

            if (remoteServer == null)
            {
                await myNext(context);

                return;
            }

            await myStaticFileMiddleware.Invoke(context);

            var isHead = context.Request.Method.Equals("HEAD", StringComparison.Ordinal);
            var isGet  = context.Request.Method.Equals("GET", StringComparison.Ordinal);

            if (!isHead && !isGet)
            {
                return;
            }
            if (context.Response.StatusCode != StatusCodes.Status404NotFound)
            {
                return;
            }

            var requestPath = context.Request.Path.ToString().Replace('\\', '/').TrimStart('/');

            if (requestPath.Contains("..", StringComparison.Ordinal) ||
                !ourGoodPathChars.IsMatch(requestPath))
            {
                await SetStatus(context, CachingProxyStatus.BAD_REQUEST, HttpStatusCode.BadRequest, "Invalid request path");

                return;
            }

            var upstreamUri = new Uri(remoteServer.RemoteUri, remainingPath.ToString().TrimStart('/'));

            if (myBlacklistRegex != null && myBlacklistRegex.IsMatch(requestPath))
            {
                await SetStatus(context, CachingProxyStatus.BLACKLISTED, HttpStatusCode.NotFound, "Blacklisted");

                return;
            }

            var isRedirectToRemoteUrl = myRedirectToRemoteUrlsRegex != null && myRedirectToRemoteUrlsRegex.IsMatch(requestPath);
            var emptyFileExtension    = Path.GetExtension(requestPath).Length == 0;

            if (isRedirectToRemoteUrl || emptyFileExtension)
            {
                await SetStatus(context, CachingProxyStatus.ALWAYS_REDIRECT, HttpStatusCode.TemporaryRedirect);

                context.Response.Headers.Add("Location", upstreamUri.ToString());
                return;
            }

            var cachePath = myCacheFileProvider.GetFileInfo(requestPath).PhysicalPath;

            if (cachePath == null)
            {
                await SetStatus(context, CachingProxyStatus.BAD_REQUEST, HttpStatusCode.BadRequest, "Invalid cache path");

                return;
            }

            var cachedResponse = myResponseCache.GetCachedStatusCode(requestPath);

            if (cachedResponse != null && !cachedResponse.StatusCode.IsSuccessStatusCode())
            {
                SetCachedResponseHeader(context, cachedResponse);
                await SetStatus(context, CachingProxyStatus.NEGATIVE_HIT, HttpStatusCode.NotFound);

                return;
            }

            // Positive caching for GET handled in static files
            // We handle positive caching for HEAD here
            if (cachedResponse != null && cachedResponse.StatusCode.IsSuccessStatusCode() && isHead)
            {
                SetCachedResponseHeader(context, cachedResponse);
                await SetStatus(context, CachingProxyStatus.HIT, HttpStatusCode.OK);

                return;
            }

            myLogger.LogDebug("Downloading from {0}", upstreamUri);

            var request = new HttpRequestMessage(isHead ? HttpMethod.Head : HttpMethod.Get, upstreamUri);

            HttpResponseMessage response;

            try
            {
                response = await myHttpClient.Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted);
            }
            catch (OperationCanceledException canceledException)
            {
                if (context.RequestAborted == canceledException.CancellationToken)
                {
                    return;
                }

                // Canceled by internal token, means timeout

                myLogger.LogWarning($"Timeout requesting {upstreamUri}");

                var entry = myResponseCache.PutStatusCode(requestPath, HttpStatusCode.GatewayTimeout);

                SetCachedResponseHeader(context, entry);
                await SetStatus(context, CachingProxyStatus.NEGATIVE_MISS, HttpStatusCode.NotFound);

                return;
            }
            catch (Exception e)
            {
                myLogger.LogWarning(e, $"Exception requesting {upstreamUri}: {e.Message}");

                var entry = myResponseCache.PutStatusCode(requestPath, HttpStatusCode.ServiceUnavailable);
                SetCachedResponseHeader(context, entry);
                await SetStatus(context, CachingProxyStatus.NEGATIVE_MISS, HttpStatusCode.NotFound);

                return;
            }

            using (response)
            {
                if (!response.IsSuccessStatusCode)
                {
                    var entry = myResponseCache.PutStatusCode(requestPath, response.StatusCode);

                    SetCachedResponseHeader(context, entry);
                    await SetStatus(context, CachingProxyStatus.NEGATIVE_MISS, HttpStatusCode.NotFound);

                    return;
                }

                if (response.IsSuccessStatusCode && isHead)
                {
                    var entry = myResponseCache.PutStatusCode(requestPath, response.StatusCode);
                    SetCachedResponseHeader(context, entry);
                    await SetStatus(context, CachingProxyStatus.MISS, HttpStatusCode.OK);

                    return;
                }

                var contentLength = response.Content.Headers.ContentLength;
                context.Response.ContentLength = contentLength;

                if (myContentTypeProvider.TryGetContentType(requestPath, out var contentType))
                {
                    context.Response.ContentType = contentType;
                }

                await SetStatus(context, CachingProxyStatus.MISS, HttpStatusCode.OK);

                // Cache successful responses indefinitely
                // as we assume content won't be changed under a fixed url
                AddEternalCachingControl(context);

                var tempFile = cachePath + ".tmp." + Guid.NewGuid();
                try
                {
                    var parent = Directory.GetParent(cachePath);
                    Directory.CreateDirectory(parent.FullName);

                    using (var stream = new FileStream(
                               tempFile, FileMode.CreateNew, FileAccess.Write, FileShare.None, BUFFER_SIZE,
                               FileOptions.Asynchronous))
                    {
                        using (var sourceStream = await response.Content.ReadAsStreamAsync())
                            await CopyToTwoStreamsAsync(sourceStream, context.Response.Body, stream, context.RequestAborted);
                    }

                    var tempFileInfo = new FileInfo(tempFile);
                    if (contentLength != null && tempFileInfo.Length != contentLength)
                    {
                        myLogger.LogWarning($"Expected {contentLength} bytes from Content-Length, but downloaded {tempFileInfo.Length}: {upstreamUri}");
                        context.Abort();
                        return;
                    }

                    try
                    {
                        File.Move(tempFile, cachePath);
                    }
                    catch (IOException)
                    {
                        if (File.Exists(cachePath))
                        {
                            // It's ok, parallel request cached it before us
                        }
                        else
                        {
                            throw;
                        }
                    }
                }
                catch (OperationCanceledException)
                {
                    // Probable cause: OperationCanceledException from http client myHttpClient
                    // Probable cause: OperationCanceledException from this service's client (context.RequestAborted)

                    // ref: https://github.com/aspnet/StaticFiles/commit/bbf1478821c11ecdcad776dad085d6ee09d8f8ee#diff-991aec26255237cd6dbfa787d0995a2aR85
                    // ref: https://github.com/aspnet/StaticFiles/issues/150

                    // Don't throw this exception, it's most likely caused by the client disconnecting.
                    // However, if it was cancelled for any other reason we need to prevent empty responses.
                    context.Abort();
                }
                finally
                {
                    CatchSilently(() =>
                    {
                        if (File.Exists(tempFile))
                        {
                            File.Delete(tempFile);
                        }
                    });
                }
            }
        }
        public async Task InvokeAsync(HttpContext context)
        {
            if (context.Request.Path == "/health")
            {
                var availableFreeSpaceMb = new DriveInfo(myLocalCachePath).AvailableFreeSpace / (1024 * 1024);
                if (availableFreeSpaceMb < myMinimumFreeDiskSpaceMb)
                {
                    context.Response.StatusCode = 500;
                    await context.Response.WriteAsync($"Not Enough Free Disk Space. {availableFreeSpaceMb} MB is free at {myLocalCachePath}, " +
                                                      $"but minimum is {myMinimumFreeDiskSpaceMb} MB");

                    return;
                }

                context.Response.StatusCode = 200;
                await context.Response.WriteAsync("OK");

                return;
            }

            var remoteServer = myRemoteServers.LookupRemoteServer(context.Request.Path, out var remainingPath);

            if (remoteServer == null)
            {
                await myNext(context);

                return;
            }

            await myStaticFileMiddleware.Invoke(context);

            var isHead = context.Request.Method.Equals("HEAD", StringComparison.Ordinal);
            var isGet  = context.Request.Method.Equals("GET", StringComparison.Ordinal);

            if (!isHead && !isGet)
            {
                return;
            }
            if (context.Response.StatusCode != StatusCodes.Status404NotFound)
            {
                return;
            }

            var requestPath = context.Request.Path.ToString().Replace('\\', '/').TrimStart('/');

            if (requestPath.Contains("..", StringComparison.Ordinal) ||
                !ourGoodPathChars.IsMatch(requestPath))
            {
                await SetStatus(context, CachingProxyStatus.BAD_REQUEST, HttpStatusCode.BadRequest, "Invalid request path");

                return;
            }

            var upstreamUri = new Uri(remoteServer.RemoteUri, remainingPath.ToString().TrimStart('/'));

            if (myBlacklistRegex != null && myBlacklistRegex.IsMatch(requestPath))
            {
                await SetStatus(context, CachingProxyStatus.BLACKLISTED, HttpStatusCode.NotFound, "Blacklisted");

                return;
            }

            var isRedirectToRemoteUrl = myRedirectToRemoteUrlsRegex != null && myRedirectToRemoteUrlsRegex.IsMatch(requestPath);
            var requestPathExtension  = Path.GetExtension(requestPath);
            var emptyFileExtension    = requestPathExtension.Length == 0;

            if (isRedirectToRemoteUrl || emptyFileExtension)
            {
                await SetStatus(context, CachingProxyStatus.ALWAYS_REDIRECT, HttpStatusCode.TemporaryRedirect);

                context.Response.Headers.Add("Location", upstreamUri.ToString());
                return;
            }

            var cachePath = myCacheFileProvider.GetFileInfo(requestPath).PhysicalPath;

            if (cachePath == null)
            {
                await SetStatus(context, CachingProxyStatus.BAD_REQUEST, HttpStatusCode.BadRequest, "Invalid cache path");

                return;
            }

            var cachedResponse = myResponseCache.GetCachedStatusCode(requestPath);

            if (cachedResponse != null && !cachedResponse.StatusCode.IsSuccessStatusCode())
            {
                SetCachedResponseHeader(context, cachedResponse);
                await SetStatus(context, CachingProxyStatus.NEGATIVE_HIT, HttpStatusCode.NotFound);

                return;
            }

            // Positive caching for GET handled in static files
            // We handle positive caching for HEAD here
            if (cachedResponse != null && cachedResponse.StatusCode.IsSuccessStatusCode() && isHead)
            {
                var responseHeaders = context.Response.GetTypedHeaders();

                responseHeaders.LastModified  = cachedResponse.LastModified;
                responseHeaders.ContentLength = cachedResponse.ContentLength;
                context.Response.Headers[HeaderNames.ContentType] = cachedResponse.ContentType;

                SetCachedResponseHeader(context, cachedResponse);
                await SetStatus(context, CachingProxyStatus.HIT, HttpStatusCode.OK);

                return;
            }

            myLogger.LogDebug("Downloading from {0}", upstreamUri);

            var request = new HttpRequestMessage(isHead ? HttpMethod.Head : HttpMethod.Get, upstreamUri);

            HttpResponseMessage response;

            try
            {
                response = await myHttpClient.Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted);
            }
            catch (OperationCanceledException canceledException)
            {
                if (context.RequestAborted == canceledException.CancellationToken)
                {
                    return;
                }

                // Canceled by internal token, means timeout

                myLogger.LogWarning($"Timeout requesting {upstreamUri}");

                var entry = myResponseCache.PutStatusCode(requestPath, HttpStatusCode.GatewayTimeout, lastModified: null, contentType: null, contentLength: null);

                SetCachedResponseHeader(context, entry);
                await SetStatus(context, CachingProxyStatus.NEGATIVE_MISS, HttpStatusCode.NotFound);

                return;
            }
            catch (Exception e)
            {
                myLogger.LogWarning(e, $"Exception requesting {upstreamUri}: {e.Message}");

                var entry = myResponseCache.PutStatusCode(requestPath, HttpStatusCode.ServiceUnavailable, lastModified: null, contentType: null, contentLength: null);
                SetCachedResponseHeader(context, entry);
                await SetStatus(context, CachingProxyStatus.NEGATIVE_MISS, HttpStatusCode.NotFound);

                return;
            }

            using (response)
            {
                if (!response.IsSuccessStatusCode)
                {
                    var entry = myResponseCache.PutStatusCode(requestPath, response.StatusCode, lastModified: null, contentType: null, contentLength: null);

                    SetCachedResponseHeader(context, entry);
                    await SetStatus(context, CachingProxyStatus.NEGATIVE_MISS, HttpStatusCode.NotFound);

                    return;
                }

                // If content type validation is enabled, only .html, .htm and .txt files may have text/* content type
                // This prevents e.g. caching of error pages with 200 OK code (jcenter)
                var responseContentType = response.Content.Headers.ContentType?.MediaType;
                if (requestPathExtension != ".html" &&
                    requestPathExtension != ".txt" &&
                    requestPathExtension != ".htm")
                {
                    if (responseContentType is MediaTypeNames.Text.Html or MediaTypeNames.Text.Plain)
                    {
                        myLogger.LogWarning($"{upstreamUri} returned content type '{responseContentType}' which is possibly wrong for file extension '{requestPathExtension}'");

                        if (remoteServer.ValidateContentTypes)
                        {
                            // return 503 Service Unavailable, since the client will most likely retry it with 5xx error codes
                            context.Response.StatusCode  = (int)HttpStatusCode.ServiceUnavailable;
                            context.Response.ContentType = MediaTypeNames.Text.Plain;
                            await context.Response.WriteAsync(
                                $"{upstreamUri} returned content type '{responseContentType}' which is forbidden by content type validation for file extension '{requestPathExtension}'");

                            return;
                        }
                    }
                }

                var contentLength = response.Content.Headers.ContentLength;
                context.Response.ContentLength = contentLength;

                var contentLastModified = response.Content.Headers.LastModified;
                if (contentLastModified != null)
                {
                    context.Response.GetTypedHeaders().LastModified = contentLastModified;
                }

                if (myContentTypeProvider.TryGetContentType(requestPath, out var contentType))
                {
                    context.Response.ContentType = contentType;
                }

                if (isHead)
                {
                    var entry = myResponseCache.PutStatusCode(
                        requestPath, response.StatusCode,
                        lastModified: contentLastModified, contentType: contentType, contentLength: contentLength);
                    SetCachedResponseHeader(context, entry);
                    await SetStatus(context, CachingProxyStatus.MISS, HttpStatusCode.OK);

                    return;
                }

                await SetStatus(context, CachingProxyStatus.MISS, HttpStatusCode.OK);

                // Cache successful responses indefinitely
                // as we assume content won't be changed under a fixed url
                AddEternalCachingControl(context);

                var tempFile = cachePath + ".tmp." + Guid.NewGuid();
                try
                {
                    var parent = Directory.GetParent(cachePath);
                    Directory.CreateDirectory(parent !.FullName);

                    await using (var stream = new FileStream(
                                     tempFile, FileMode.CreateNew, FileAccess.Write, FileShare.None, BUFFER_SIZE,
                                     FileOptions.Asynchronous))
                    {
                        await using (var sourceStream = await response.Content.ReadAsStreamAsync())
                            await CopyToTwoStreamsAsync(sourceStream, context.Response.Body, stream, context.RequestAborted);
                    }

                    var tempFileInfo = new FileInfo(tempFile);
                    if (contentLength != null && tempFileInfo.Length != contentLength)
                    {
                        myLogger.LogWarning($"Expected {contentLength} bytes from Content-Length, but downloaded {tempFileInfo.Length}: {upstreamUri}");
                        context.Abort();
                        return;
                    }

                    if (contentLastModified.HasValue)
                    {
                        File.SetLastWriteTimeUtc(tempFile, contentLastModified.Value.UtcDateTime);
                    }

                    try
                    {
                        File.Move(tempFile, cachePath);
                    }
                    catch (IOException)
                    {
                        if (File.Exists(cachePath))
                        {
                            // It's ok, parallel request cached it before us
                        }
                        else
                        {
                            throw;
                        }
                    }
                }
                catch (OperationCanceledException)
                {
                    // Probable cause: OperationCanceledException from http client myHttpClient
                    // Probable cause: OperationCanceledException from this service's client (context.RequestAborted)

                    // ref: https://github.com/aspnet/StaticFiles/commit/bbf1478821c11ecdcad776dad085d6ee09d8f8ee#diff-991aec26255237cd6dbfa787d0995a2aR85
                    // ref: https://github.com/aspnet/StaticFiles/issues/150

                    // Don't throw this exception, it's most likely caused by the client disconnecting.
                    // However, if it was cancelled for any other reason we need to prevent empty responses.
                    context.Abort();
                }
                finally
                {
                    CatchSilently(() =>
                    {
                        if (File.Exists(tempFile))
                        {
                            File.Delete(tempFile);
                        }
                    });
                }
            }
        }