├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── SangServerTool.sln ├── SangServerTool ├── Domain │ ├── AliyunDomain.cs │ ├── IDomain.cs │ └── TencentCloudDomain.cs ├── Options.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── SangServerTool.csproj ├── Tool │ ├── DDNS.cs │ ├── GetCert.cs │ └── SSL.cs └── Utils.cs ├── doc ├── DDNS.md └── SSL.md └── publish.sh /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [sangyuxiaowu] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ['https://raw.githubusercontent.com/sangyuxiaowu/sangyuxiaowu/main/pay.png'] 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SangServerTool 2 | 3 | 包含两款工具: 4 | 5 | - 服务器 DDNS 工具,用于内网服务动态域名解析,支持 IPv6 6 | - 服务器 SSL 证书申请工具 7 | - 获取远端站点证书,用于同步通配符证书到其他服务器的场景 8 | 9 | 目前仅支持阿里云,其他云服务的实现可以自行添加。 10 | 11 | 提供独立的 [linux-x64、osx-x64、linux-arm64、win-x64 下载](../../releases)。其他平台可自行通过源码编译发布。 12 | 13 | 这个服务的启动一般来说不需要一直运行。DDNS可以在设备开启时检测一次,以后每间隔一段时间检测一次,如一小时。 14 | 15 | SSL证书申请,可以每天0点固定检查一次即可,将要过期时,程序会自动进行续期,更新证书。 16 | 注意 nginx 等服务需要重新加载一下证书,可配置 `Certificate:okshell` 来实现申请成功调用你指定的脚本文件。 17 | 18 | [【DDNS 开机启动计划任务示例】](doc/DDNS.md) | [【SSL计划任务示例】](doc/SSL.md) 19 | 20 | # 使用说明 21 | 22 | ## 配置文件 23 | 24 | 配置文件为json格式,需要使用时传入参数。两个功能可以复用一个配置文件,不同站点的 SSL 需要使用多个配置文件。 25 | 26 | ```json 27 | 28 | { 29 | "Access": { 30 | "AK": "阿里云 AccessKeyId", //AccessKeyId 31 | "SK": "阿里云 AccessKeySecret" //AccessKeySecret 32 | }, 33 | "DDNS": { 34 | "ddns": "xxx.domain.com", // DDNS要解析的域名 35 | "basedomain": "domain.com" // 主域名 36 | }, 37 | "Certificate": { 38 | "cerpath": "/usr/opt/ssl/domain.com.pem", // 域名证书路径或要将新申请的证书放哪里 39 | "privatekey": "/usr/opt/ssl/domain.com.key", // 证书私钥路径或要将生成的私钥放哪里 40 | "domains": "*.dev.domain.com dev.domain.com domain.com ", // 证书的DNS Name,多个用空格隔开 41 | "basedomain": "domain.com", // 主域名 42 | "okshell": "/home/myname/.tools/restartnginx.sh" // 证书更新后执行的脚本文件 43 | }, 44 | "ACME": { 45 | "email": "my@domain.com", // ACME 申请证书的邮箱 46 | "account": "/etc/acme_account.pem" // ACME 账户的私钥路径或要将其放在哪里 47 | }, 48 | "CSR": { 49 | "CommonName": "domain.com", // 证书的CSR信息 50 | "CountryName": "CN", 51 | "Locality": "BeiJing", 52 | "State": "BeiJing", 53 | "Organization": "Sang", 54 | "OrganizationUnit": "IT" 55 | } 56 | } 57 | 58 | ``` 59 | 60 | ## DDNS 61 | 62 | 参数说明: 63 | 64 | | 参数 | 说明| 65 | | --- | --- | 66 | | -c, --config | Required. Set config json file.
设置配置文件路径 | 67 | | --delay | (Default: 0) How many seconds delay?
启动后延迟多少秒进行检查处理,默认为 0,防止开机启动过早导致出现一些问题 | 68 | | --del | (Default: false) Is delete DDNS?
删除配置文件中设置的DDNS域名解析,默认为 false ,如果为 true,则尝试删除后退出 | 69 | | --v6 | (Default: false) Is ipv6?
使用 IPv6 来解析,默认获取 IPv4 | 70 | | --ip | (Default: ) If set will be used. Otherwise automatically obtained.
You can set 'ifconfig', It will check from 'https://ipw.cn/' to get you Internet IP.
默认为空字符,如果传入了指定 IP ,则使用这个 IP 来解析。
可以传入 'ifconfig' 值,该值则表示通过网络获取网络出口 IP 来解析 71 | 72 | > 如:使用本地的 IPv6 进行 DDNS 设置 73 | 74 | ```bash 75 | SangServerTool ddns -c "test.json" --v6=1 76 | ``` 77 | 78 | > 如:删除 DDNS 的域名解析 79 | 80 | ```bash 81 | SangServerTool ddns -c "test.json" --del=1 82 | ``` 83 | 84 | 该功能的配置文件使用 `Access` 和 `DDNS` 这两段。 85 | 86 | ```json 87 | 88 | { 89 | "Access": { 90 | "AK": "阿里云 AccessKeyId", //AccessKeyId 91 | "SK": "阿里云 AccessKeySecret" //AccessKeySecret 92 | }, 93 | "DDNS": { 94 | "ddns": "xxx.domain.com", // DDNS要解析的域名 95 | "basedomain": "domain.com" // 主域名 96 | } 97 | } 98 | ``` 99 | 100 | ## IP 101 | 102 | 参数说明: 103 | 104 | | 参数 | 说明| 105 | | --- | --- | 106 | | --web | (Default: false) Is check from 'https://ipw.cn/' to get you Internet IP?
如果为 true,则表示通过网络获取网络出口 IP 107 | 108 | 获取网络出口 IP 109 | 110 | ```bash 111 | SangServerTool ip --web 112 | ``` 113 | 114 | 获取网卡显示 IP 115 | 116 | ```bash 117 | SangServerTool ip 118 | ``` 119 | 120 | ## SSL 121 | 122 | 参数说明: 123 | 124 | | 参数 | 说明| 125 | | --- | --- | 126 | | -c, --config | Required. Set config json file.
设置配置文件路径 | 127 | | --retry | (Default: 2) How many retries?
验证域名时重试几次,默认2次 | 128 | | --delay | (Default: 10) How many seconds to retry?
验证域名时重试间隔多少秒,默认10秒 | 129 | | --force | (Default: false) Is force to apply?
是否强制申请,如果为 true ,则会强制申请新的证书,否则会检查证书是否过期,过期则会申请新的证书 | 130 | | --script | (Default: false) Run script for test.
是否运行测试脚本,如果为 true ,则会只尝试运行 `okshell` 测试脚本 | 131 | 132 | > 如:申请域名重试 3 次 133 | 134 | ```bash 135 | SangServerTool ssl -c "test.json" --retry=3 136 | ``` 137 | 该功能的配置文件使用 `Access` 、 `Certificate` 、 `ACME` 、`CSR` 138 | 139 | 在配置 `Certificate` 信息时: 140 | 141 | - 如果是新申请的只需要配置好证书 `cerpath` 和证书私钥 `privatekey` 的存放路径,程序会自行生成。若已经有证书会私钥配置好其位置会自行更新证书或使用当前已有的私钥。 142 | - `domains` 支持多个域名,使用空格隔开 143 | - `okshell` 证书更新后执行的脚本文件,如果服务器不能热加载证书,记得配置好,通过脚本文件进行重启服务 144 | 145 | 在配置 `ACME` 信息时: 146 | 147 | - 如果第一次使用仅需要写上你的邮箱 `email` 和存放 ACME 账户的私钥文件位置 `account`,证书过期会收到邮件提醒 148 | - 如果之前已有账户,可以使用已有的账户私钥,配置给 `account` 149 | 150 | 关于 `CSR` ,这段配不配都无所谓,毕竟是免费的证书,也不会生效,只是验证了域名的归属权。 151 | 152 | ## 同步 153 | 154 | 该功能,用于获取远端站点证书,用于同步通配符证书到其他服务器的场景。 155 | 156 | 参数说明: 157 | 158 | | 参数 | 说明| 159 | | --- | --- | 160 | | -c, --config | Required. Set config json file.
设置配置文件路径 | 161 | | --force | (Default: false) Is force to apply?
是否强制更新,如果为 true ,则会强制更新证书,否则会检查证书是否过期,过期则会更新证书 | 162 | 163 | > 如:获取远端站点证书 164 | 165 | ```bash 166 | SangServerTool sync -c "test.json" 167 | ``` 168 | 169 | 只需要完成一下配置信息: 170 | 171 | ```json 172 | { 173 | "Certificate": { 174 | "cerpath": "cmartc.cer", // 域名证书路径或要将新申请的证书放哪里 175 | "site":"https://xxx.domain.com/", // 远端 https 站点地址 176 | "okshell": "" // 完成后执行的脚本文件 177 | } 178 | } 179 | ``` 180 | 181 | > 注意:需要手动复制远端的证书密钥到本地,这个是一次性的操作。 182 | 183 | # 支持 184 | 185 | 欢迎喜欢编程的朋友,关注我的微信公众号:桑榆肖物 186 | 187 | ![](https://open.weixin.qq.com/qr/code?username=gh_c874018d0317) 188 | -------------------------------------------------------------------------------- /SangServerTool.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32228.430 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SangServerTool", "SangServerTool\SangServerTool.csproj", "{E65ECACB-940B-4317-81D7-47BAFE201651}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "解决方案项", "解决方案项", "{9E02EE0C-5F1F-4B11-9D3E-59C2F2F3413D}" 9 | ProjectSection(SolutionItems) = preProject 10 | README.md = README.md 11 | EndProjectSection 12 | EndProject 13 | Global 14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 15 | Debug|Any CPU = Debug|Any CPU 16 | Release|Any CPU = Release|Any CPU 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {E65ECACB-940B-4317-81D7-47BAFE201651}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {E65ECACB-940B-4317-81D7-47BAFE201651}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {E65ECACB-940B-4317-81D7-47BAFE201651}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {E65ECACB-940B-4317-81D7-47BAFE201651}.Release|Any CPU.Build.0 = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(SolutionProperties) = preSolution 25 | HideSolutionNode = FALSE 26 | EndGlobalSection 27 | GlobalSection(ExtensibilityGlobals) = postSolution 28 | SolutionGuid = {FCDE4DBF-D192-428A-B7F4-4743AF6A8DC2} 29 | EndGlobalSection 30 | EndGlobal 31 | -------------------------------------------------------------------------------- /SangServerTool/Domain/AliyunDomain.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | using System.Text; 3 | using System.Text.Json.Nodes; 4 | using System.Web; 5 | 6 | namespace SangServerTool.Domain 7 | { 8 | /// 9 | /// 阿里云域名解析管理 10 | /// 文档 https://help.aliyun.com/document_detail/29771.html 11 | /// 签名 https://help.aliyun.com/document_detail/29747.html 12 | /// 13 | public class AliyunDomain : IDomain 14 | { 15 | private readonly string _AccessKeyId; 16 | private readonly string _AccessKeySecret; 17 | private readonly string _Host = "https://alidns.aliyuncs.com/"; 18 | 19 | public AliyunDomain(string accessKeyId, string accessKeySecret) 20 | { 21 | _AccessKeyId = accessKeyId; 22 | _AccessKeySecret = accessKeySecret; 23 | } 24 | 25 | /// 26 | /// 修改解析记录 27 | /// 28 | /// 域名 29 | /// 记录 30 | /// 类型 31 | /// 记录值 32 | /// 域名设置信息 33 | public async Task UpdateRecordsAsync(string RecordId, string RR, string Type, string Value) 34 | { 35 | 36 | var parameters = new Dictionary(); 37 | parameters.Add("Action", "UpdateDomainRecord"); 38 | parameters.Add("RecordId", RecordId); 39 | parameters.Add("RR", RR); 40 | parameters.Add("Type", Type); 41 | parameters.Add("Value", Value); 42 | 43 | JsonNode json; 44 | try 45 | { 46 | using var client = new HttpClient(); 47 | var apiResponse = await client.GetAsync(SignUrl(parameters, HttpMethod.Get)); 48 | var jsonstring = await apiResponse.Content.ReadAsStringAsync(); 49 | json = JsonNode.Parse(jsonstring)!; 50 | } 51 | catch (Exception ex) 52 | { 53 | //请求或转换异常 54 | return new DomainRes(false, ex.Message); 55 | } 56 | 57 | // 返回有异常 58 | if (json["RecordId"] is null) 59 | { 60 | return new DomainRes(false, "返回数据异常" + json.ToString()); 61 | } 62 | 63 | return new DomainRes(true, "ok", json["RecordId"].ToString()); 64 | } 65 | 66 | /// 67 | /// 删除解析记录 68 | /// 69 | /// 解析记录的ID 70 | /// 71 | public async Task DelRecordsAsync(string RecordId) 72 | { 73 | 74 | var parameters = new Dictionary(); 75 | parameters.Add("Action", "DeleteDomainRecord"); 76 | parameters.Add("RecordId", RecordId); 77 | 78 | 79 | JsonNode json; 80 | try 81 | { 82 | using var client = new HttpClient(); 83 | var apiResponse = await client.GetAsync(SignUrl(parameters, HttpMethod.Get)); 84 | var jsonstring = await apiResponse.Content.ReadAsStringAsync(); 85 | json = JsonNode.Parse(jsonstring)!; 86 | } 87 | catch (Exception ex) 88 | { 89 | //请求或转换异常 90 | return new DomainRes(false, ex.Message); 91 | } 92 | 93 | // 返回有异常 94 | if (json["RecordId"] is null) 95 | { 96 | return new DomainRes(false, "返回数据异常" + json.ToString()); 97 | } 98 | 99 | return new DomainRes(true, "ok", json["RecordId"].ToString()); 100 | } 101 | 102 | /// 103 | /// 获取子域名解析记录列表 104 | /// 105 | /// 106 | /// 解析类型 A、MX、CNAME、TXT、REDIRECT_URL、FORWORD_URL、NS、AAAA、SRV 107 | /// 域名解析信息 108 | public async Task GetRecordsAsync(string SubDomain, string Type = "") 109 | { 110 | var parameters = new Dictionary(); 111 | parameters.Add("Action", "DescribeSubDomainRecords"); 112 | parameters.Add("SubDomain", SubDomain); 113 | if (!string.IsNullOrEmpty(Type)) parameters.Add("Type", Type); 114 | 115 | JsonNode json; 116 | try 117 | { 118 | using var client = new HttpClient(); 119 | var apiResponse = await client.GetAsync(SignUrl(parameters, HttpMethod.Get)); 120 | var jsonstring = await apiResponse.Content.ReadAsStringAsync(); 121 | json = JsonNode.Parse(jsonstring)!; 122 | } 123 | catch (Exception ex) 124 | { 125 | //请求或转换异常 126 | return new DomainRes(false, ex.Message); 127 | } 128 | 129 | // 返回有异常 130 | if (json["TotalCount"] is null) 131 | { 132 | return new DomainRes(false, "返回数据异常" + json.ToString()); 133 | } 134 | 135 | // 有解析数据返回解析结果 136 | if ((int)json["TotalCount"]! > 0) 137 | { 138 | var temp = json["DomainRecords"]!["Record"]![0]; 139 | return new DomainRes(true, "ok", temp["RecordId"]!.ToString(), temp["Value"]!.ToString()); 140 | 141 | } 142 | 143 | // 不存在解析信息 144 | return new DomainRes(true); 145 | 146 | } 147 | 148 | /// 149 | /// 添加域名解析 150 | /// 151 | /// 域名 152 | /// 记录 153 | /// 类型 154 | /// 记录值 155 | /// 域名设置信息 156 | public async Task AddRecordsAsync(string DomainName, string RR, string Type, string Value) 157 | { 158 | 159 | var parameters = new Dictionary(); 160 | parameters.Add("Action", "AddDomainRecord"); 161 | parameters.Add("DomainName", DomainName); 162 | parameters.Add("RR", RR); 163 | parameters.Add("Type", Type); 164 | parameters.Add("Value", Value); 165 | 166 | JsonNode json; 167 | try 168 | { 169 | using var client = new HttpClient(); 170 | var apiResponse = await client.GetAsync(SignUrl(parameters, HttpMethod.Get)); 171 | var jsonstring = await apiResponse.Content.ReadAsStringAsync(); 172 | json = JsonNode.Parse(jsonstring)!; 173 | } 174 | catch (Exception ex) 175 | { 176 | //请求或转换异常 177 | return new DomainRes(false, ex.Message); 178 | } 179 | 180 | // 返回有异常 181 | if (json["RecordId"] is null) 182 | { 183 | return new DomainRes(false, "返回数据异常" + json.ToString()); 184 | } 185 | 186 | return new DomainRes(true, "ok", json["RecordId"].ToString()); 187 | } 188 | 189 | /// 190 | /// 签名请求的URL 191 | /// 192 | /// 参数,非公共 193 | /// 请求类型 194 | /// 195 | private string SignUrl(Dictionary parameters, HttpMethod method) 196 | { 197 | parameters.Add("Format", "JSON"); 198 | parameters.Add("Version", "2015-01-09"); 199 | parameters.Add("SignatureMethod", "HMAC-SHA1"); 200 | parameters.Add("Timestamp", DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ")); 201 | parameters.Add("SignatureVersion", "1.0"); 202 | parameters.Add("SignatureNonce", Guid.NewGuid().ToString()); 203 | parameters.Add("AccessKeyId", _AccessKeyId); 204 | 205 | var canonicalizedQueryString = string.Join("&", 206 | //parameters.OrderBy(x => x.Key) 207 | new SortedDictionary(parameters, StringComparer.Ordinal) 208 | .Select(x => PercentEncode(x.Key) + "=" + PercentEncode(x.Value))); 209 | var stringToSign = method.ToString().ToUpper() + "&%2F&" + PercentEncode(canonicalizedQueryString); 210 | var keyBytes = Encoding.UTF8.GetBytes(_AccessKeySecret + "&"); 211 | var hmac = new HMACSHA1(keyBytes); 212 | var hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)); 213 | 214 | parameters.Add("Signature", Convert.ToBase64String(hashBytes)); 215 | return _Host + "?" + string.Join("&", parameters.Select(x => x.Key + "=" + HttpUtility.UrlEncode(x.Value))); 216 | } 217 | 218 | private string PercentEncode(string value) 219 | { 220 | return UpperCaseUrlEncode(value) 221 | .Replace("+", "%20") 222 | .Replace("*", "%2A") 223 | .Replace("%7E", "~"); 224 | } 225 | 226 | private static string UpperCaseUrlEncode(string s) 227 | { 228 | char[] temp = HttpUtility.UrlEncode(s).ToCharArray(); 229 | for (int i = 0; i < temp.Length - 2; i++) 230 | { 231 | if (temp[i] == '%') 232 | { 233 | temp[i + 1] = char.ToUpper(temp[i + 1]); 234 | temp[i + 2] = char.ToUpper(temp[i + 2]); 235 | } 236 | } 237 | return new string(temp); 238 | } 239 | 240 | } 241 | 242 | 243 | } 244 | -------------------------------------------------------------------------------- /SangServerTool/Domain/IDomain.cs: -------------------------------------------------------------------------------- 1 | namespace SangServerTool.Domain 2 | { 3 | 4 | internal interface IDomain 5 | { 6 | Task AddRecordsAsync(string DomainName, string RR, string Type, string Value); 7 | Task DelRecordsAsync(string RecordId); 8 | Task GetRecordsAsync(string SubDomain, string Type = ""); 9 | Task UpdateRecordsAsync(string RecordId, string RR, string Type, string Value); 10 | 11 | 12 | } 13 | 14 | /// 15 | /// 域名解析信息返回 16 | /// 17 | /// 数据获取成功与否 18 | /// 错误信息 19 | /// 记录ID 20 | /// 记录值 21 | public record DomainRes(bool Success, string Msg = "", string Id = "", string Value = ""); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /SangServerTool/Domain/TencentCloudDomain.cs: -------------------------------------------------------------------------------- 1 | namespace SangServerTool.Domain 2 | { 3 | 4 | /// 5 | /// 腾讯云域名解析管理 6 | /// 文档 https://cloud.tencent.com/document/api/1427/56153 7 | /// 签名 https://cloud.tencent.com/document/api/1427/56189 8 | /// 9 | public class TencentCloudDomain : IDomain 10 | { 11 | private readonly string _SecretId; 12 | private readonly string _SecretKey; 13 | private readonly string _Host = "https://dnspod.tencentcloudapi.com/"; 14 | 15 | /// 16 | /// 初始化设置AK,SK 17 | /// 18 | /// SecretId 19 | /// SecretKey 20 | public TencentCloudDomain(string accessKeyId, string accessKeySecret) 21 | { 22 | _SecretId = accessKeyId; 23 | _SecretKey = accessKeySecret; 24 | } 25 | 26 | public Task AddRecordsAsync(string DomainName, string RR, string Type, string Value) 27 | { 28 | throw new NotImplementedException(); 29 | } 30 | 31 | public Task DelRecordsAsync(string RecordId) 32 | { 33 | throw new NotImplementedException(); 34 | } 35 | 36 | public Task GetRecordsAsync(string SubDomain, string Type = "") 37 | { 38 | throw new NotImplementedException(); 39 | } 40 | 41 | public Task UpdateRecordsAsync(string RecordId, string RR, string Type, string Value) 42 | { 43 | throw new NotImplementedException(); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /SangServerTool/Options.cs: -------------------------------------------------------------------------------- 1 | namespace SangServerTool 2 | { 3 | 4 | /// 5 | /// 请求SSL证书的参数 6 | /// 7 | public class AUTO_SSL 8 | { 9 | /// 10 | /// 配置ASSK和证书信息 11 | /// 12 | public string? ConfigFile { get; set; } 13 | 14 | /// 15 | /// DNS验证重试多少次? 16 | /// 17 | public int Retry { get; set; } 18 | 19 | /// 20 | /// DNS验证失败等待多少秒重试? 21 | /// 22 | public int Delay { get; set; } 23 | 24 | /// 25 | /// 强制更新证书 26 | /// 27 | public bool Force { get; set; } = false; 28 | 29 | /// 30 | /// 是否仅进行脚本执行测试 31 | /// 32 | public bool Script { get; set; } = false; 33 | } 34 | 35 | 36 | /// 37 | /// 配置DDNS的参数 38 | /// 39 | public class AUTO_DDNS 40 | { 41 | /// 42 | /// 配置AKSK相关,及DDNS域 43 | /// 44 | public string? ConfigFile { get; set; } 45 | 46 | /// 47 | /// 延迟多少秒执行 48 | /// 49 | public int Delay { get; set; } 50 | 51 | /// 52 | /// 是否为IPv6地址 53 | /// 54 | public bool Del { get; set; } 55 | 56 | /// 57 | /// 是否为IPv6地址 58 | /// 59 | public bool IPV6 { get; set; } 60 | 61 | /// 62 | /// 指定IP 63 | /// 64 | public string? IP { get; set; } 65 | 66 | 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /SangServerTool/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Logging; 3 | using SangServerTool; 4 | using SangServerTool.Tool; 5 | using System.CommandLine; 6 | using System.CommandLine.NamingConventionBinder; 7 | using System.Runtime.InteropServices; 8 | 9 | 10 | ServiceCollection services = new(); 11 | services.AddLogging(logBuilder => 12 | { 13 | logBuilder.AddSimpleConsole(opt => 14 | { 15 | opt.SingleLine = true; 16 | opt.IncludeScopes = true; 17 | opt.TimestampFormat = "HH:mm:ss "; 18 | }); 19 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 20 | { 21 | logBuilder.AddEventLog(new Microsoft.Extensions.Logging.EventLog.EventLogSettings { SourceName = "SangServerTool" }); 22 | } 23 | }); 24 | 25 | using var sp = services.BuildServiceProvider(); 26 | ILoggerFactory loggerFactory = sp.GetService(); 27 | 28 | // 创建根命令 29 | var rootCommand = new RootCommand("SangServerTool"); 30 | 31 | // 定义ssl命令 32 | var sslCommand = new Command("ssl", "Get Let's Encrypt SSL Cert."); 33 | sslCommand.AddOption(new Option(new[] { "--config", "-c" }, "Set config json file.") { IsRequired = true }); 34 | sslCommand.AddOption(new Option("--retry", () => 8, "How many retries?")); 35 | sslCommand.AddOption(new Option("--delay", () => 5, "How many seconds to retry?")); 36 | sslCommand.AddOption(new Option("--force", () => false, "Force to renew cert.")); 37 | sslCommand.AddOption(new Option("--script", () => false, "Run script for test.")); 38 | sslCommand.Handler = CommandHandler.Create(async (config, retry, delay, force, script) => 39 | { 40 | var opt = new AUTO_SSL { ConfigFile = config, Retry = retry, Delay = delay, Force = force, Script = script }; 41 | var logger = loggerFactory.CreateLogger("SangServerTool_SSL"); 42 | return await SSL.Run(opt, logger); 43 | }); 44 | rootCommand.AddCommand(sslCommand); 45 | 46 | // 定义ddns命令 47 | var ddnsCommand = new Command("ddns", "Set DDNS."); 48 | ddnsCommand.AddOption(new Option(new[] { "--config", "-c" }, "Set config json file.") { IsRequired = true }); 49 | ddnsCommand.AddOption(new Option("--delay", () => 0, "How many seconds delay?")); 50 | ddnsCommand.AddOption(new Option("--del", () => false, "Is delete DDNS?")); 51 | ddnsCommand.AddOption(new Option("--v6", () => false, "Is ipv6?")); 52 | ddnsCommand.AddOption(new Option("--ip", () => "", "If set will be used. Otherwise automatically obtained.\n You can set 'ifconfig', It will check from 'https://ifconfig.me/ip' to get you Internet IP.")); 53 | ddnsCommand.Handler = CommandHandler.Create(async (config, delay, del, v6, ip) => 54 | { 55 | var opt = new AUTO_DDNS { ConfigFile = config, Delay = delay, Del = del, IPV6 = v6, IP = ip }; 56 | var logger = loggerFactory.CreateLogger("SangServerTool_DDNS"); 57 | return await DDNS.Run(opt, logger); 58 | }); 59 | rootCommand.AddCommand(ddnsCommand); 60 | 61 | // 定义IP获取命令 62 | var ipCommand = new Command("ip", "Get IP."); 63 | ipCommand.AddOption(new Option("--web", () => false, "Is check from 'https://ifconfig.me/ip' to get you Internet IP?")); 64 | ipCommand.Handler = CommandHandler.Create((web) => 65 | { 66 | var logger = loggerFactory.CreateLogger("SangServerTool_IP"); 67 | if (web) 68 | { 69 | logger.LogInformation(Utils.CurrentIPAddressByWeb()); 70 | logger.LogInformation(Utils.CurrentIPAddressByWeb(true)); 71 | return 0; 72 | } 73 | else 74 | { 75 | logger.LogInformation(Utils.CurrentIPAddress()); 76 | logger.LogInformation(Utils.CurrentIPAddress(true)); 77 | return 0; 78 | } 79 | }); 80 | rootCommand.AddCommand(ipCommand); 81 | 82 | // 定义获取 https 站点证书命令 83 | var getcertCommand = new Command("sync", "Get SSL Cert from https site."); 84 | getcertCommand.AddOption(new Option(new[] { "--config", "-c" }, "Set config json file.") { IsRequired = true }); 85 | getcertCommand.AddOption(new Option("--force", () => false, "Force to renew cert.")); 86 | getcertCommand.Handler = CommandHandler.Create(async (config, force) => 87 | { 88 | var logger = loggerFactory.CreateLogger("SangServerTool_Sync"); 89 | var getCert = new GetCert(logger); 90 | return await getCert.Run(config, force); 91 | }); 92 | rootCommand.AddCommand(getcertCommand); 93 | 94 | // 解析并执行命令 95 | return await rootCommand.InvokeAsync(args); -------------------------------------------------------------------------------- /SangServerTool/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "SangServerTool": { 4 | "commandName": "Project", 5 | //"commandLineArgs": "sync -c \"test.json\"" 6 | "commandLineArgs": "ip" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /SangServerTool/SangServerTool.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0;net9.0 6 | enable 7 | enable 8 | SangSQ(桑世强) 9 | 1.7.6 10 | DDNS & SSL Tool 11 | SangSQ 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /SangServerTool/Tool/DDNS.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.Logging; 3 | using SangServerTool.Domain; 4 | 5 | namespace SangServerTool.Tool 6 | { 7 | /// 8 | /// DDNS 动态域名解析 9 | /// 10 | public class DDNS 11 | { 12 | 13 | public async static Task Run(AUTO_DDNS opt, ILogger logger) 14 | { 15 | logger.LogInformation($"开始执行:{DateTime.Now.ToString()}"); 16 | logger.LogInformation($"配置文件:{opt.ConfigFile}"); 17 | if (!File.Exists(opt.ConfigFile)) 18 | { 19 | logger.LogWarning("配置文件不存在"); 20 | return 1; 21 | } 22 | 23 | if (opt.Delay > 0) 24 | { 25 | logger.LogInformation($"延迟 {opt.Delay.ToString()} 秒执行"); 26 | await Task.Delay(1000 * opt.Delay); 27 | } 28 | 29 | IConfigurationBuilder configBuilder = new ConfigurationBuilder(); 30 | configBuilder.AddJsonFile(opt.ConfigFile, optional: false, reloadOnChange: false); 31 | IConfigurationRoot config = configBuilder.Build(); 32 | 33 | 34 | IDomain domain = config["Access:T"] switch 35 | { 36 | "T" => new TencentCloudDomain(config["Access:AK"], config["Access:SK"]), 37 | _ => new AliyunDomain(config["Access:AK"], config["Access:SK"]) 38 | }; 39 | 40 | 41 | logger.LogInformation($"检查域名当前解析:{config["DDNS:ddns"]}"); 42 | 43 | // 检查DDNS的解析信息 44 | var Record = await domain.GetRecordsAsync(config["DDNS:ddns"], ""); 45 | //出错 46 | if (!Record.Success) 47 | { 48 | logger.LogError(Record.Msg); 49 | return 1; 50 | } 51 | 52 | // 进入删除操作 53 | if (opt.Del) 54 | { 55 | if (Record.Id != "") 56 | { 57 | var DelRecord = await domain.DelRecordsAsync(Record.Id); 58 | if (DelRecord.Success) 59 | { 60 | logger.LogInformation($"删除DDNS解析成功:{DelRecord.Id}"); 61 | return 0; 62 | } 63 | logger.LogError($"删除DDNS解析失败:{DelRecord.Msg}"); 64 | return 1; 65 | } 66 | logger.LogInformation($"无需删除记录"); 67 | return 0; 68 | } 69 | 70 | var nowip = opt.IP == "" ? Utils.CurrentIPAddress(opt.IPV6) 71 | : opt.IP == "ifconfig" ? Utils.CurrentIPAddressByWeb(opt.IPV6) : opt.IP; 72 | 73 | //检查IP是否合规 74 | if (!System.Net.IPAddress.TryParse(nowip, out _)) 75 | { 76 | logger.LogError($"设置解析IP配置获取失败,获取的IP信息 {nowip} 不是有效的IP地址"); 77 | return 1; 78 | } 79 | 80 | logger.LogInformation($"获取IP地址为:{nowip}"); 81 | 82 | //获取 RR 设置 83 | string RR = Utils.GetRRDdns(config["DDNS:ddns"], config["DDNS:basedomain"]); 84 | if (string.IsNullOrEmpty(RR)) 85 | { 86 | logger.LogError("配置解析:配置的DDNS解析或Domain域名异常"); 87 | return 1; 88 | } 89 | 90 | // 要解析的域名类型 91 | string Type = nowip.Length > 16 ? "AAAA" : "A"; 92 | 93 | //域名没有解析记录,新建解析 94 | if (Record.Id == "") 95 | { 96 | 97 | 98 | logger.LogInformation($"准备解析:{RR}\t{Type}\t{nowip}"); 99 | var AddRecord = await domain.AddRecordsAsync(config["DDNS:basedomain"], RR, Type, nowip); 100 | if (AddRecord.Success) 101 | { 102 | logger.LogInformation($"新建解析成功:{AddRecord.Id}"); 103 | return 0; 104 | } 105 | logger.LogError($"新建解析失败:{AddRecord.Msg}"); 106 | return 1; 107 | } 108 | 109 | logger.LogInformation($"原解析地址为:{Record.Value}"); 110 | 111 | //修改记录 112 | if (Record.Value != nowip) 113 | { 114 | logger.LogDebug("修改解析记录"); 115 | var UpdateRecord = await domain.UpdateRecordsAsync(Record.Id, RR, Type, nowip); 116 | if (UpdateRecord.Success) 117 | { 118 | logger.LogInformation($"修改解析成功:{UpdateRecord.Id}"); 119 | return 0; 120 | } 121 | logger.LogError($"修改解析失败:{UpdateRecord.Msg}"); 122 | return 1; 123 | } 124 | logger.LogInformation("无需处理"); 125 | return 0; 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /SangServerTool/Tool/GetCert.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.Logging; 3 | using System.Net.Security; 4 | using System.Security.Cryptography.X509Certificates; 5 | 6 | namespace SangServerTool.Tool 7 | { 8 | public class GetCert 9 | { 10 | private ILogger _logger; 11 | 12 | public GetCert(ILogger logger) 13 | { 14 | _logger = logger; 15 | } 16 | 17 | /// 18 | /// 获取远端证书 19 | /// 20 | /// 21 | /// 22 | /// 23 | public async Task Run(string config_file, bool force) 24 | { 25 | _logger.LogInformation($"开始执行:{DateTime.Now.ToString()}"); 26 | _logger.LogInformation($"配置文件:{config_file}"); 27 | if (!File.Exists(config_file)) 28 | { 29 | _logger.LogError("配置文件不存在"); 30 | return 1; 31 | } 32 | IConfigurationBuilder configBuilder = new ConfigurationBuilder(); 33 | configBuilder.AddJsonFile(config_file, optional: false, reloadOnChange: false); 34 | IConfigurationRoot config = configBuilder.Build(); 35 | 36 | // 获取配置的源站点信息 37 | var site = config["Certificate:site"]; 38 | if (string.IsNullOrEmpty(site)) 39 | { 40 | _logger.LogError("未配置源站点信息"); 41 | return 1; 42 | } 43 | // 检查是否为合法的https URL 44 | if (!site.StartsWith("https://")) 45 | { 46 | _logger.LogError("源站点信息不是https站点"); 47 | return 1; 48 | } 49 | 50 | // 获取存储证书位置 51 | var cert_file = config["Certificate:cerpath"]; 52 | if (string.IsNullOrEmpty(cert_file)) 53 | { 54 | _logger.LogError("未配置证书存储位置"); 55 | return 1; 56 | } 57 | 58 | if(File.Exists(cert_file)) 59 | { 60 | // 检查证书是否过期 61 | int daysToExpiry = Utils.GetCertExpiryDays(cert_file); 62 | _logger.LogInformation($"证书还有 {daysToExpiry} 天过期"); 63 | 64 | if (!force && daysToExpiry > Utils.CertExpiryDays) 65 | { 66 | _logger.LogInformation("证书未到期,无需更新"); 67 | return 0; 68 | } 69 | } 70 | 71 | // 获取远端证书 72 | var cert = await GetRemoteCert(site); 73 | if (cert == null) 74 | { 75 | _logger.LogError("获取远端证书失败"); 76 | return 1; 77 | } 78 | 79 | // 保存证书 80 | try 81 | { 82 | File.WriteAllText(cert_file, $"-----BEGIN CERTIFICATE-----\n{cert}\n-----END CERTIFICATE-----"); 83 | _logger.LogInformation($"证书已保存到:{cert_file}"); 84 | 85 | // shell脚本 86 | var shell = config["Certificate:okshell"]; 87 | if (!string.IsNullOrEmpty(shell)) 88 | { 89 | Utils.RunShell(shell, _logger); 90 | } 91 | 92 | return 0; 93 | } 94 | catch (Exception ex) 95 | { 96 | _logger.LogError($"保存证书失败:{ex.Message}"); 97 | return 1; 98 | } 99 | 100 | } 101 | 102 | private byte[] _certificate; 103 | private async Task GetRemoteCert(string site) 104 | { 105 | try 106 | { 107 | var handler = new HttpClientHandler 108 | { 109 | ServerCertificateCustomValidationCallback = CertificateValidationCallback 110 | }; 111 | 112 | using (var client = new HttpClient(handler)) 113 | { 114 | await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, site)); 115 | } 116 | 117 | return Convert.ToBase64String(_certificate, Base64FormattingOptions.InsertLineBreaks); 118 | } 119 | catch (Exception ex) 120 | { 121 | _logger.LogError($"获取证书失败: {ex.Message}"); 122 | return null; 123 | } 124 | } 125 | 126 | private bool CertificateValidationCallback(HttpRequestMessage requestMessage, X509Certificate2 certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) 127 | { 128 | _logger.LogInformation($"证书信息: {certificate.Subject}"); 129 | _certificate = certificate.GetRawCertData(); 130 | return true; 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /SangServerTool/Tool/SSL.cs: -------------------------------------------------------------------------------- 1 | using Certes; 2 | using Certes.Acme; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.Logging; 5 | using SangServerTool.Domain; 6 | 7 | namespace SangServerTool.Tool 8 | { 9 | 10 | /// 11 | /// SSL免费证书申请 12 | /// 文档 https://github.com/fszlin/certes 13 | /// 14 | public class SSL 15 | { 16 | public async static Task Run(AUTO_SSL opt, ILogger logger) 17 | { 18 | logger.LogInformation($"开始执行:{DateTime.Now.ToString()}"); 19 | logger.LogInformation($"配置文件:{opt.ConfigFile}"); 20 | if (!File.Exists(opt.ConfigFile)) 21 | { 22 | logger.LogError("配置文件不存在"); 23 | return 1; 24 | } 25 | IConfigurationBuilder configBuilder = new ConfigurationBuilder(); 26 | configBuilder.AddJsonFile(opt.ConfigFile, optional: false, reloadOnChange: false); 27 | IConfigurationRoot config = configBuilder.Build(); 28 | 29 | // 申请证书的信息 30 | CertificateInfo cer_info = config.GetSection("Certificate").Get(); 31 | // 证书的CSR信息 32 | CsrInfo cer_csr = config.GetSection("CSR").Get(); 33 | // ACME 账户信息 34 | CerAcme cer_acme = config.GetSection("ACME").Get(); 35 | 36 | // 测试脚本调用 37 | if (opt.Script) 38 | { 39 | Utils.RunShell(cer_info.okshell, logger); 40 | return 0; 41 | } 42 | 43 | // 是否存在 证书,不存在就直接创建申请了 44 | bool isHaved = File.Exists(cer_info.cerpath); 45 | if (isHaved) 46 | { 47 | int daysToExpiry = Utils.GetCertExpiryDays(cer_info.cerpath); 48 | logger.LogInformation($"证书还有 {daysToExpiry} 天过期"); 49 | 50 | if (daysToExpiry > Utils.CertExpiryDays && !opt.Force) 51 | { 52 | logger.LogInformation("证书未到期,无需处理"); 53 | return 0; 54 | } 55 | } 56 | 57 | if (cer_csr is null || cer_info is null || cer_acme is null) 58 | { 59 | logger.LogError("配置文件格式错误"); 60 | return 1; 61 | } 62 | 63 | AcmeAccount acmeinfo; 64 | try 65 | { 66 | acmeinfo = await GetAcmeAccountAsync(cer_acme.email, cer_acme.account); 67 | } 68 | catch (Exception ex) 69 | { 70 | logger.LogInformation("登录申请账户:" + ex.Message); 71 | return 1; 72 | } 73 | 74 | //获取申请单上下文 75 | var orderListContext = await acmeinfo.account.Orders(); 76 | var orders = await orderListContext.Orders(); 77 | //https://acme-staging-v02.api.letsencrypt.org/acme/order/59498234/3028278734 78 | 79 | if (orders.Any()) 80 | { 81 | foreach (var order in orders) 82 | { 83 | // 打印订单 84 | logger.LogInformation($"订单:{order.Location}"); 85 | } 86 | logger.LogInformation("已存在订单"); 87 | return 1; 88 | } 89 | 90 | // 开始请求证书,获取DNS验证的配置信息 91 | DnsTask DnsTask; 92 | try 93 | { 94 | DnsTask = await GetDnsAuthInfoAsync(acmeinfo.acme, cer_info.domains); 95 | 96 | logger.LogInformation($"订单:{DnsTask.order.Location}"); 97 | DnsTask.dnsChallenge.ToList().ForEach(x => logger.LogInformation($"验证:{x.Location}")); 98 | } 99 | catch (Exception ex) 100 | { 101 | logger.LogError("提交申请失败:" + ex.Message); 102 | return 1; 103 | } 104 | 105 | //进行域名TXT信息设置 106 | string[] rrdomain = Utils.GetRRDomain(cer_info.domains, cer_info.basedomain); 107 | string[] RecordIds = new string[rrdomain.Length]; 108 | 109 | var al = new AliyunDomain(config["Access:AK"], config["Access:SK"]); 110 | for (var i = 0; i < rrdomain.Length; i++) 111 | { 112 | logger.LogInformation($"添加解析验证:{rrdomain[i]}\tTXT\t{DnsTask.dnsTxt[i]}"); 113 | var req = await al.AddRecordsAsync(cer_info.basedomain, rrdomain[i], "TXT", DnsTask.dnsTxt[i]); 114 | if (!req.Success) 115 | { 116 | logger.LogError("添加域名解析出错:" + req.Msg); 117 | return 1; 118 | } 119 | RecordIds[i] = req.Id; 120 | } 121 | 122 | //进行验证 123 | logger.LogInformation("准备验证域名,请稍后 ..."); 124 | await Task.Delay(2000); 125 | 126 | // 执行Validate 127 | foreach (var challenge in DnsTask.dnsChallenge) 128 | { 129 | await challenge.Validate(); 130 | } 131 | 132 | // 检查验证结果 133 | int retry = 0; 134 | int ok; 135 | do 136 | { 137 | if (retry > 0) 138 | { 139 | logger.LogInformation($"正在查询 {retry.ToString()}/{opt.Retry.ToString()}"); 140 | } 141 | ok = 0; 142 | foreach (var challenge in DnsTask.dnsChallenge) 143 | { 144 | var result = await challenge.Resource(); 145 | 146 | ok += result.Status == Certes.Acme.Resource.ChallengeStatus.Valid ? 1 : 0; 147 | } 148 | 149 | retry++; 150 | // 延时后重试 151 | await Task.Delay(1000 * opt.Delay); 152 | } while (retry < opt.Retry && ok != rrdomain.Length); 153 | 154 | //删除TXT记录 155 | logger.LogInformation("执行域名验证结束,清理用于验证的TXT记录"); 156 | foreach (var record in RecordIds) 157 | { 158 | await al.DelRecordsAsync(record); 159 | } 160 | 161 | 162 | if (ok != rrdomain.Length) 163 | { 164 | logger.LogError($"验证域名出错:域名TXT记录未全部验证通过,{ok}/{rrdomain.Length}"); 165 | return 1; 166 | } 167 | 168 | //生成证书 169 | IKey privateKey = File.Exists(cer_info.privatekey) ? KeyFactory.FromPem(File.ReadAllText(cer_info.privatekey)) : KeyFactory.NewKey(KeyAlgorithm.RS256); 170 | if (!File.Exists(cer_info.privatekey)) 171 | { 172 | string pem = privateKey.ToPem(); 173 | File.WriteAllText(cer_info.privatekey, pem); 174 | } 175 | var cert = await DnsTask.order.Generate(cer_csr, privateKey); 176 | 177 | File.WriteAllText(cer_info.cerpath, cert.ToPem()); 178 | 179 | logger.LogInformation("证书申请成功"); 180 | 181 | Utils.RunShell(cer_info.okshell, logger); 182 | 183 | return 0; 184 | } 185 | 186 | 187 | /// 188 | /// 获取DNS验证信息 189 | /// 190 | /// ACME账户对象 191 | /// 申请的域名信息,多个用空格隔开 192 | /// 193 | public static async Task GetDnsAuthInfoAsync(AcmeContext acme, string domains) 194 | { 195 | var domainArray = domains.Split(' '); 196 | var order = await acme.NewOrder(domainArray); 197 | var authorizationContexts = await order.Authorizations(); 198 | var dnsChallenges = new IChallengeContext[domainArray.Length]; 199 | var dnsTxts = new string[domainArray.Length]; 200 | 201 | for (int i = 0; i < authorizationContexts.Count(); i++) 202 | { 203 | var authorizationContext = authorizationContexts.ElementAt(i); 204 | dnsChallenges[i] = await authorizationContext.Dns(); 205 | dnsTxts[i] = acme.AccountKey.DnsTxt(dnsChallenges[i].Token); 206 | } 207 | 208 | return new DnsTask(dnsChallenges, order, dnsTxts); 209 | } 210 | 211 | 212 | 213 | /// 214 | /// 获取Acme登录后对象 215 | /// 216 | /// 邮箱 217 | /// 邮箱账户pem密钥文件地址 218 | /// 219 | public static async Task GetAcmeAccountAsync(string email, string pemKeyFile) 220 | { 221 | string pemKey = File.Exists(pemKeyFile) ? await File.ReadAllTextAsync(pemKeyFile) : ""; 222 | #if DEBUG 223 | var acme = pemKey == "" ? new AcmeContext(WellKnownServers.LetsEncryptStagingV2) : new AcmeContext(WellKnownServers.LetsEncryptStagingV2, KeyFactory.FromPem(pemKey)); 224 | # else 225 | var acme = pemKey == "" ? new AcmeContext(WellKnownServers.LetsEncryptV2) : new AcmeContext(WellKnownServers.LetsEncryptV2, KeyFactory.FromPem(pemKey)); 226 | #endif 227 | var account = pemKey == "" ? await acme.NewAccount(email, true) : await acme.Account(); 228 | 229 | // 若没有账户,则保存一下账户的KEY 230 | if (pemKey == "") 231 | { 232 | pemKey = acme.AccountKey.ToPem(); 233 | await File.AppendAllTextAsync(pemKeyFile, pemKey); 234 | } 235 | 236 | return new AcmeAccount(acme, account); 237 | } 238 | 239 | /// 240 | /// DNS验证返回 241 | /// 242 | /// 243 | /// 244 | /// 245 | public record DnsTask(IChallengeContext[] dnsChallenge, IOrderContext order, string[] dnsTxt); 246 | 247 | /// 248 | /// 登录后的ACNE账户信息 249 | /// 250 | /// Acme对象 251 | /// 账户 252 | public record AcmeAccount(AcmeContext acme, IAccountContext account); 253 | 254 | /// 255 | /// 配置信息,申请的证书的相关信息 256 | /// 257 | public record CertificateInfo 258 | { 259 | /// 260 | /// 证书存放地址,这个文件不存在会新申请 261 | /// 262 | public string cerpath { get; set; } 263 | /// 264 | /// 证书的私钥文件,这个文件不存在时会自动生成 265 | /// 266 | public string privatekey { get; set; } 267 | /// 268 | /// 申请证书的域名信息,多个用空格分开,必须同一个域 269 | /// 270 | public string domains { get; set; } 271 | /// 272 | /// 证书的主域名信息 273 | /// 274 | public string basedomain { get; set; } 275 | /// 276 | /// 证书新建或更新成功后调用的脚本文件 277 | /// 278 | public string okshell { get; set; } 279 | } 280 | 281 | 282 | /// 283 | /// 配置信息,申请证书用的ACME账户 284 | /// 285 | public record CerAcme 286 | { 287 | /// 288 | /// 邮箱 289 | /// 290 | public string email { get; set; } 291 | /// 292 | /// 用户密钥文件存放路径 293 | /// 294 | public string account { get; set; } 295 | } 296 | 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /SangServerTool/Utils.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System.Diagnostics; 3 | using System.Net; 4 | using System.Net.NetworkInformation; 5 | using System.Net.Sockets; 6 | using System.Runtime.InteropServices; 7 | using System.Security.Cryptography.X509Certificates; 8 | 9 | namespace SangServerTool 10 | { 11 | public static class Utils 12 | { 13 | /// 14 | /// 获取证书还有几天过期 15 | /// 16 | /// 证书路径 17 | /// 剩余天数,负数表示已过期 18 | public static int GetCertExpiryDays(string certFilePath) 19 | { 20 | var cer = new X509Certificate(certFilePath); 21 | DateTime expdate = Convert.ToDateTime(cer.GetExpirationDateString()); 22 | TimeSpan span = expdate.Subtract(DateTime.Now); 23 | return (int)span.TotalDays; 24 | } 25 | 26 | /// 27 | /// 证书过期前几天处理 28 | /// 29 | public static int CertExpiryDays = 5; 30 | 31 | /// 32 | /// 获取电脑网卡IP 33 | /// 34 | /// 是获取IPv6 35 | /// 36 | public static string? CurrentIPAddress(bool isV6 = false) 37 | { 38 | var family = isV6 ? AddressFamily.InterNetworkV6 : AddressFamily.InterNetwork; 39 | List exps = new List { "docker0", "lo", "l4tbr0" }; 40 | var ips = NetworkInterface.GetAllNetworkInterfaces() 41 | .Where(p => !exps.Contains(p.Name)) // 排除docker、lo等 42 | .Select(p => p.GetIPProperties()) 43 | .SelectMany(p => p.UnicastAddresses) 44 | .Where(p => p.Address.AddressFamily == family && !IPAddress.IsLoopback(p.Address) && 45 | (family == AddressFamily.InterNetwork || !IsNotGoodIPv6(p)) 46 | ); 47 | return ips.FirstOrDefault()?.Address.ToString(); 48 | } 49 | 50 | /// 51 | /// 判断IPv6地址是否不太可用 52 | /// 排除Dhcp,本地和随机的临时地址 53 | /// 54 | /// 单播地址信息 55 | /// 不用则返回true 56 | private static bool IsNotGoodIPv6(UnicastIPAddressInformation unicastAddress) 57 | { 58 | // 判断平台 59 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 60 | { 61 | return unicastAddress.Address.IsIPv6LinkLocal || unicastAddress.PrefixOrigin == PrefixOrigin.Dhcp || unicastAddress.SuffixOrigin == SuffixOrigin.Random; 62 | } 63 | else 64 | { 65 | // 其他暂时这样处理 66 | return unicastAddress.Address.IsIPv6LinkLocal || unicastAddress.Address.ToString().Length < 35; 67 | } 68 | } 69 | 70 | /// 71 | /// 获取电脑外网IP 72 | /// 73 | /// 74 | public static string CurrentIPAddressByWeb(bool isV6 = false) 75 | { 76 | using var client = new HttpClient(); 77 | string ip = ""; 78 | try 79 | { 80 | ip = client.GetStringAsync($"https://{(isV6 ? "6" : "4")}.ipw.cn/").Result; 81 | } 82 | catch 83 | { 84 | return ip; 85 | } 86 | return ip; 87 | } 88 | 89 | /// 90 | /// 根据要申请的域名信息,返回要设置的RR信息 91 | /// 92 | /// 配置的证书DNS 93 | /// 基础域名 94 | /// 95 | public static string[] GetRRDomain(string domains, string basedomain) 96 | { 97 | string[] domain = domains.Split(' '); 98 | for (var i = 0; i < domain.Length; i++) 99 | { 100 | domain[i] = domain[i].StartsWith("*") ? domain[i].Replace("*", "_acme-challenge") : "_acme-challenge." + domain[i]; 101 | var inx = domain[i].LastIndexOf(basedomain); 102 | if (inx > -1) 103 | { 104 | domain[i] = domain[i].Substring(0, inx - 1); 105 | } 106 | } 107 | return domain; 108 | } 109 | 110 | /// 111 | /// 根据DDNS地址和域名获取要设置的RR信息 112 | /// 113 | /// DDNS 114 | /// 基础域名 115 | /// RR,空为异常 116 | public static string GetRRDdns(string domain, string basedomain) 117 | { 118 | // 解析主域名 119 | if (domain == basedomain) return "@"; 120 | int inx = domain.LastIndexOf(basedomain); 121 | if (inx > -1) 122 | { 123 | return domain.Substring(0, inx - 1); 124 | } 125 | return ""; 126 | } 127 | 128 | /// 129 | /// 执行shell脚本 130 | /// 131 | /// 脚本文件 132 | /// 日志 133 | public static void RunShell(string file, ILogger logger) 134 | { 135 | // 脚本文件存在,执行 136 | if (File.Exists(file)) 137 | { 138 | //创建一个ProcessStartInfo对象 使用系统shell 指定命令和参数 设置标准输出 139 | var psi = new ProcessStartInfo(file) { RedirectStandardOutput = true }; 140 | //启动 141 | var proc = Process.Start(psi); 142 | if (proc == null) 143 | { 144 | logger.LogError("处理脚本启动失败"); 145 | } 146 | else 147 | { 148 | logger.LogInformation("-------------Start read standard output--------------"); 149 | //开始读取 150 | using (var sr = proc.StandardOutput) 151 | { 152 | while (!sr.EndOfStream) 153 | { 154 | logger.LogInformation(sr.ReadLine()); 155 | } 156 | 157 | if (!proc.HasExited) 158 | { 159 | proc.Kill(); 160 | } 161 | } 162 | logger.LogInformation("---------------Read end------------------"); 163 | } 164 | } 165 | else 166 | { 167 | logger.LogInformation($"脚本文件不存在: {file}"); 168 | } 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /doc/DDNS.md: -------------------------------------------------------------------------------- 1 | # DDNS 开机启动计划任务示例 2 | 3 | > 以下操作是在 Ubuntu 系统,Windows 系统可通过“任务计划程序”进行类似操作。 4 | 5 | 1. 首先,前往仓库的 releases 下载程序上传到设备,然后添加执行权限。 6 | 7 | 2. 按照说明编写自己的配置文件 8 | 9 | 3. 编写开机启动服务 10 | 11 | ```bash 12 | sudo vi /etc/systemd/system/ddns.service 13 | ``` 14 | 15 | 文件内容如下: 16 | 17 | ```ini 18 | [Unit] 19 | Description=SangServerTool DDNS 20 | After=network.target 21 | ConditionPathExists=/home/sangsq/.tools/SangServerTool 22 | 23 | [Service] 24 | Type=forking 25 | ExecStart=/home/sangsq/.tools/SangServerTool ddns -c /home/sangsq/.tools/config.json --v6=1 --delay=30 26 | TimeoutSec=0 27 | StandardOutput=journal+console 28 | RemainAfterExit=yes 29 | 30 | [Install] 31 | WantedBy=multi-user.target 32 | ``` 33 | 34 | `ConditionPathExists` 为刚上传的程序文件地址,当其存在这个服务才会启动 35 | 36 | `ExecStart` 这里要写程序和配置文件的全路径,在这里我用的是 IPv6 地址进行解析。保险起见,服务启动后延迟 30 秒后开始执行,主要是接口查询需要访问阿里云服务器,刚启动的时候,直接运行可能会报 DNS 解析的错误,也许使用 `After=network-online.target` 会解决,不过没有测试这个。 37 | 38 | 4. 设置开机启动服务 39 | 40 | ```bash 41 | sudo systemctl enable ddns.service 42 | ``` 43 | 44 | 5. 添加计划任务 45 | 46 | 除了开机启动外,我们也可以通过计划任务,半个小时执行以下程序,检查 IP 是否有变化。 47 | 48 | ``` 49 | sudo crontab -e 50 | ``` 51 | 52 | 添加计划任务 53 | 54 | ``` 55 | */30 * * * * /home/sangsq/.tools/SangServerTool ddns -c /home/sangsq/.tools/config.json --v6=1 56 | ``` 57 | 58 | 这里去除了延迟的检测,因为不是刚开机了。 59 | -------------------------------------------------------------------------------- /doc/SSL.md: -------------------------------------------------------------------------------- 1 | # SSL计划任务示例 2 | 3 | > 以下操作是在 Ubuntu 系统,Windows 系统可通过“任务计划程序”进行类似操作。 4 | 5 | 1. 修改好配置文件 6 | 7 | 特别注意:如果服务器不能热加载证书,记得在配置文件配置好 `okshell` ,来实现 web 服务器的重启。 8 | 9 | 2. 添加计划任务 10 | 11 | ``` 12 | sudo crontab -e 13 | ``` 14 | 15 | 添加计划任务 16 | 17 | ``` 18 | 0 0 * * * /home/sangsq/.tools/SangServerTool ssl -c /home/sangsq/.tools/config.json 19 | ``` 20 | 21 | 如果要处理多个域名,则需多个配置文件和计划任务。 -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf publish/* 4 | mkdir publish 5 | project="SangServerTool" 6 | plates=("linux-x64" "linux-arm64" "osx-x64" "win-x64") 7 | 8 | for plate in ${plates[*]}; do 9 | echo 10 | echo "=========开始发布:${plate} =========" 11 | echo 12 | dotnet publish $project/$project.csproj -c Release -f net9.0 --sc -r $plate -o=publish/$project.$plate -p:PublishSingleFile=true 13 | echo 14 | echo "=========开始打包 =========" 15 | echo 16 | cd publish 17 | rm $project.$plate/$project.pdb -f 18 | tar -zcvf $project.$plate.tar.gz $project.$plate || exit 1 19 | cd ../ 20 | done --------------------------------------------------------------------------------