节点在线、应用在线、配置在线使用令牌查询
大石头 authored at 2021-12-16 19:49:30
5.41 KiB
Stardust
using System;
using System.IO;
using System.Linq;
using System.Net;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.StaticFiles;
using NewLife;
using NewLife.Log;
using NewLife.Web;

namespace Stardust.Extensions.Caches;

/// <summary>
/// 文件缓存扩展
/// </summary>
public static class FileCacheExtensions
{
    /// <summary>使用文件缓存,把指定目录映射到url上,并提供上级地址用于下载不存在的文件</summary>
    /// <param name="app"></param>
    /// <param name="requestPath">请求虚拟路径。如/files</param>
    /// <param name="localPath">本地缓存目录</param>
    /// <param name="getWhiteIP"></param>
    /// <param name="uplinkServer">上级地址,用于下载本地不存在的文件</param>
    /// <returns></returns>
    public static void UseFileCache(this IApplicationBuilder app, String requestPath, String localPath, Func<String>? getWhiteIP = null, String? uplinkServer = null)
    {
        var cacheRoot = localPath.GetBasePath().EnsureDirectory(false);
        XTrace.WriteLine("FileCache: {0}", cacheRoot);

        var provider = new CacheFileProvider(cacheRoot, uplinkServer)
        {
            IndexInfoFile = "index.csv",
            //Tracer = DefaultTracer.Instance,
            ServiceProvider = app.ApplicationServices,
            // 拼接上游地址时需要补回请求虚拟路径,否则会请求到站点根目录
            RequestPath = requestPath,
        };

        if (uplinkServer.IsNullOrEmpty())
        {
            var set = NewLife.Setting.Current;
            provider.GetServers = () => set.PluginServer?.Split(",") ?? new String[0];
        }
        else
        {
            XTrace.WriteLine("UplinkServer: {0}", uplinkServer);
        }

        // 记录本进程监听端口,供自环识别使用。未取到时退化为按主机判断
        FillLocalPorts(app);
        CheckSelfUplink(provider);

        // IP白名单拦截,必须在使用静态文件前面
        if (getWhiteIP != null)
            app.UseMiddleware<IpFilterMiddleware>(requestPath, getWhiteIP());

        app.UseStaticFiles(new StaticFileOptions
        {
            RequestPath = new PathString(requestPath),
            FileProvider = provider,
            ServeUnknownFileTypes = true,
            DefaultContentType = "application/x-msdownload",
            OnPrepareResponse = ctx => OnPrepareResponse(ctx, getWhiteIP),
        });
        app.UseDirectoryBrowser(new DirectoryBrowserOptions
        {
            RequestPath = new PathString(requestPath),
            FileProvider = provider,
        });
    }

    /// <summary>填充本进程监听端口,用于识别上游地址是否指向自身</summary>
    /// <param name="app"></param>
    static void FillLocalPorts(IApplicationBuilder app)
    {
        try
        {
            var server = app.ApplicationServices.GetService(typeof(IServer)) as IServer;
            var feature = server?.Features.Get<IServerAddressesFeature>();
            if (feature == null) return;

            foreach (var item in feature.Addresses)
            {
                if (Uri.TryCreate(item.Replace("*", "0.0.0.0").Replace("+", "0.0.0.0"), UriKind.Absolute, out var uri) && uri.Port > 0)
                    CacheFileProvider.LocalPorts.Add(uri.Port);
            }
        }
        catch (Exception ex)
        {
            XTrace.WriteLine("Failed to obtain the local listening port: {0}", ex.Message);
        }
    }

    /// <summary>启动自检。上游指向自身时,拉取文件会变成自己请求自己的死循环,直接告警</summary>
    /// <param name="provider"></param>
    static void CheckSelfUplink(CacheFileProvider provider)
    {
        var svrs = provider.GetServers?.Invoke() ?? provider.Servers;
        if (svrs == null || svrs.Length == 0) return;

        var selfs = svrs.Where(e => !e.IsNullOrEmpty() && CacheFileProvider.IsSelf(e)).ToList();
        if (selfs.Count == 0) return;

        XTrace.WriteLine("Warning: file cache upstream [{0}] points to itself; file pulling from this upstream is disabled. Check the PluginServer setting.", selfs.Join());

        // 剔除指向自身的上游,避免每次请求都空等超时
        provider.Servers = svrs.Where(e => !CacheFileProvider.IsSelf(e)).ToArray();

        var getter = provider.GetServers;
        if (getter != null) provider.GetServers = () => getter().Where(e => !CacheFileProvider.IsSelf(e)).ToArray();
    }

    static void OnPrepareResponse(StaticFileResponseContext ctx, Func<String>? getWhiteIP)
    {
        var ip = ctx.Context.GetUserHost();
        if (ip.IsNullOrEmpty() || !ValidIP(ip, getWhiteIP))
        {
            ctx.Context.Response.StatusCode = (Int32)HttpStatusCode.Forbidden;
            ctx.Context.Response.ContentLength = 0;
            ctx.Context.Response.Body = Stream.Null;
        }
    }

    static Boolean ValidIP(String ip, Func<String>? getWhiteIP)
    {
        if (ip.IsNullOrEmpty()) return false;

        var whites = getWhiteIP?.Invoke();
        if (whites.IsNullOrEmpty()) return true;

        // 白名单里面有的,直接通过
        var ws = (whites + "").Split(",", ";");
        if (ws.Length > 0)
        {
            return ws.Any(e => e.IsMatch(ip));
        }

        // 未设置白名单,黑名单里面没有的,直接通过
        return true;
    }
}