├── .gitmodules ├── docs └── legainer.rp ├── src ├── LEGainer │ ├── appsettings.json │ ├── ViewModels │ │ ├── GenerateCertResult.cs │ │ ├── IResultBase.cs │ │ └── CreateOrderResult.cs │ ├── DNSUtility │ │ ├── DnsResult.cs │ │ ├── ResourceRecord │ │ │ ├── CNAME_RR.cs │ │ │ ├── NS_RR.cs │ │ │ ├── A_RR.cs │ │ │ ├── MX_RR.cs │ │ │ ├── TXT_RR.cs │ │ │ └── SOA_RR.cs │ │ ├── enum.cs │ │ ├── MyDnsQuestion.cs │ │ ├── MyDnsRecord.cs │ │ ├── MyDnsHeader.cs │ │ ├── DNSHelper.cs │ │ └── MyDns.cs │ ├── Models │ │ └── LEGainerOptions.cs │ ├── LEGainer.csproj │ ├── Properties │ │ └── launchSettings.json │ ├── configs │ │ └── nlog.config │ ├── Handlers │ │ ├── BaseHandler.cs │ │ ├── DownloadCert.cs │ │ ├── GenerateCert.cs │ │ └── CreateOrder.cs │ ├── wwwroot │ │ ├── settings │ │ │ ├── dns.html │ │ │ └── nginx.html │ │ ├── 2.html │ │ ├── 3.html │ │ ├── css │ │ │ └── main.css │ │ └── index.html │ ├── Program.cs │ ├── Startup.cs │ └── SSLGenerate.cs ├── .dockerignore ├── Dockerfile └── LEGainer.sln ├── README.md ├── .gitattributes └── .gitignore /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/legainer.rp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmrobot/LEGainer/HEAD/docs/legainer.rp -------------------------------------------------------------------------------- /src/LEGainer/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "legainer": { 3 | "email": "cert@ztimage.com" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/LEGainer/ViewModels/GenerateCertResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace LEGainer.ViewModels 7 | { 8 | public class GenerateCertResult : IResultBase 9 | { 10 | 11 | public string Domain { get; set; } 12 | 13 | 14 | 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LEGainer 2 | 通过Let's Encrypt 获取证书工具
3 |
4 |
5 |
6 | 每月申请,安装为windows计划任务:
7 | schtasks /create /tn "letsencrypt_https" /tr path\to\LEGainer.exe /sc monthly /ru System
8 |
9 |
10 |
11 | //时间短点测试可用:
12 | schtasks /create /tn "letsencrypt_https" /tr path\to\LEGainer.exe /sc minute /mo 3 /ru System
13 | 14 | -------------------------------------------------------------------------------- /src/LEGainer/DNSUtility/DnsResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace LEGainer.DNSUtility 7 | { 8 | public class DnsResult 9 | { 10 | public bool Success { get; set; } 11 | 12 | public string RecordValue { get; set; } 13 | 14 | public string ErrorMessage { get; set; } 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md -------------------------------------------------------------------------------- /src/LEGainer/ViewModels/IResultBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace LEGainer.ViewModels 7 | { 8 | public class IResultBase 9 | { 10 | /// 11 | /// 是否成功,0:失败,1:成功 12 | /// 13 | public Int32 Success { get; set; } 14 | 15 | public string Message { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/LEGainer/ViewModels/CreateOrderResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace LEGainer.ViewModels 7 | { 8 | public class CreateOrderResult:IResultBase 9 | { 10 | public string SessionKey { get; set; } 11 | 12 | 13 | public string ChallengeDomain { get; set; } 14 | 15 | 16 | public string DnsTxtValue { get; set; } 17 | 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/LEGainer/Models/LEGainerOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace LEGainer.Models 7 | { 8 | public class LEGainerOptions 9 | { 10 | public string Email { get; set; } 11 | 12 | 13 | /// 14 | /// 每分钟创建订单数限制 15 | /// 16 | public Int32 CreateOrderCountPerMin { get; set; } 17 | 18 | 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/LEGainer/DNSUtility/ResourceRecord/CNAME_RR.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace LEGainer.DNSUtility.ResourceRecord 7 | { 8 | public class CNAME_RR 9 | { 10 | public string name { get; set; } 11 | public override string ToString() 12 | { 13 | return name; 14 | } 15 | public CNAME_RR(byte[] data, int offset, int len) 16 | { 17 | int labelLen; 18 | name += MyDns.GetLabelName(data, offset, out labelLen); 19 | 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/LEGainer/DNSUtility/ResourceRecord/NS_RR.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace LEGainer.DNSUtility.ResourceRecord 7 | { 8 | public class NS_RR 9 | { 10 | public string NameServer { get; set; } 11 | public override string ToString() 12 | { 13 | return NameServer; 14 | } 15 | public NS_RR(byte[] data, int offset, int len) 16 | { 17 | int labelLen; 18 | NameServer += MyDns.GetLabelName(data, offset, out labelLen); 19 | 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/LEGainer/DNSUtility/ResourceRecord/A_RR.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace LEGainer.DNSUtility.ResourceRecord 7 | { 8 | public class A_RR 9 | { 10 | public string address { get; set; } 11 | public override string ToString() 12 | { 13 | return address; 14 | } 15 | public A_RR(byte[] data, int offset, int len) 16 | { 17 | for (int i = 0; i < 4; i++) 18 | { 19 | address += data[offset++].ToString() + "."; 20 | } 21 | address = address.TrimEnd('.'); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base 4 | WORKDIR /app 5 | EXPOSE 80 6 | 7 | FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build 8 | WORKDIR /src 9 | COPY ["LEGainer/LEGainer.csproj", "LEGainer/"] 10 | RUN dotnet restore "LEGainer/LEGainer.csproj" 11 | COPY . . 12 | WORKDIR "/src/LEGainer" 13 | RUN dotnet build "LEGainer.csproj" -c Release -o /app/build 14 | 15 | FROM build AS publish 16 | RUN dotnet publish "LEGainer.csproj" -c Release -o /app/publish 17 | 18 | FROM base AS final 19 | WORKDIR /app 20 | COPY --from=publish /app/publish . 21 | ENTRYPOINT ["dotnet", "LEGainer.dll"] -------------------------------------------------------------------------------- /src/LEGainer/DNSUtility/ResourceRecord/MX_RR.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace LEGainer.DNSUtility.ResourceRecord 7 | { 8 | public class MX_RR 9 | { 10 | public int Preference { get; set; } 11 | public string Mail { get; set; } 12 | public override string ToString() 13 | { 14 | return string.Format("Preference={0} | Mail={1}", Preference, Mail); 15 | } 16 | public MX_RR(byte[] data, int offset, int len) 17 | { 18 | Preference = data[offset++] * 256 + data[offset++]; 19 | int labelLen; 20 | Mail = MyDns.GetLabelName(data, offset, out labelLen); 21 | 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/LEGainer/DNSUtility/ResourceRecord/TXT_RR.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace LEGainer.DNSUtility.ResourceRecord 7 | { 8 | public class TXT_RR 9 | { 10 | public string text { get; set; } 11 | public override string ToString() 12 | { 13 | return text; 14 | } 15 | public TXT_RR(byte[] data, int offset, int rdlen) 16 | { 17 | int len = data[offset++]; 18 | //由于txt的字段有可能大于63,超出一般GetLabelName的字符串长度。 19 | StringBuilder build = new StringBuilder(len); 20 | for (; len > 0; len--) 21 | { 22 | build.Append((char)data[offset++]); 23 | } 24 | text = build.ToString(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/LEGainer/LEGainer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7b8e29bb-2a76-4f4b-bf78-9f0b84b46c61 7 | Linux 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/LEGainer/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:12560/", 7 | "sslPort": 44381 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "LEGainer": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 25 | }, 26 | "Docker": { 27 | "commandName": "Docker", 28 | "launchBrowser": true, 29 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", 30 | "publishAllPorts": true, 31 | "useSSL": true 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/LEGainer/configs/nlog.config: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 9 | 11 | 12 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/LEGainer/Handlers/BaseHandler.cs: -------------------------------------------------------------------------------- 1 | using LEGainer.ViewModels; 2 | using Microsoft.AspNetCore.Http; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text.Encodings.Web; 7 | using System.Text.Json; 8 | using System.Text.Unicode; 9 | using System.Threading.Tasks; 10 | 11 | namespace LEGainer.Handlers 12 | { 13 | public abstract class BaseHandler 14 | { 15 | protected HttpContext context { get; private set; } 16 | 17 | public BaseHandler(IHttpContextAccessor accessor) 18 | { 19 | this.context = accessor.HttpContext; 20 | } 21 | 22 | /// 23 | /// 输出结果 24 | /// 25 | /// 26 | /// 27 | /// 28 | public async virtual Task WriteResultAsync(Int32 statusCode, T result) where T: IResultBase 29 | { 30 | this.context.Response.StatusCode = statusCode; 31 | await this.context.Response.WriteAsync(JsonSerializer.Serialize(result,new JsonSerializerOptions() { Encoder=JavaScriptEncoder.Create(UnicodeRanges.All)})); 32 | } 33 | 34 | /// 35 | /// 处理请求 36 | /// 37 | /// 38 | public abstract Task HandleAsync(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/LEGainer.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30804.86 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LEGainer", "LEGainer\LEGainer.csproj", "{64E17C66-3360-4C54-A646-8631B6C6D254}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E08ECEBF-48A2-4E03-A590-A4190B30EEE0}" 9 | ProjectSection(SolutionItems) = preProject 10 | Dockerfile = Dockerfile 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 | {64E17C66-3360-4C54-A646-8631B6C6D254}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {64E17C66-3360-4C54-A646-8631B6C6D254}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {64E17C66-3360-4C54-A646-8631B6C6D254}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {64E17C66-3360-4C54-A646-8631B6C6D254}.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 = {A2836712-51E1-45D3-8A3C-51456C213B30} 29 | EndGlobalSection 30 | EndGlobal 31 | -------------------------------------------------------------------------------- /src/LEGainer/wwwroot/settings/dns.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LEGainer - DNS解析配置 - 免费在线申请SSL证书 6 | 7 | 8 | 9 | 10 |
11 | 18 |
19 | 20 |
21 |
22 |

DNS解析设置

23 |

1.登陆域名服务器或DNS解析服务商管理控制台,这里以DNSPod的设置为例。

24 |

2.进入记录管理

25 |

26 |

 

27 |

 

28 |

 3.点击“添加记录”,添加一条新记录。

29 |
    30 |
  • 在主机记录处填入本站要求解析的域名前缀(这里只需要填写域名前缀,不需要包含主域名,如要求解析“_acme-challenge.ztimage.com”,填“_acme-challenge”即可)。
  • 31 |
  • 在记录类型处选择TXT。
  • 32 |
  • 在记录值处填入本站要求的DNS记录值。
  • 33 |
  • 点击确定,即可
  • 34 |
35 |

36 |

 

37 |
38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /src/LEGainer/DNSUtility/ResourceRecord/SOA_RR.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace LEGainer.DNSUtility.ResourceRecord 7 | { 8 | public class SOA_RR 9 | { 10 | public string NameServer { get; set; } 11 | public string Mail { get; set; } 12 | public int Serial { get; set; } 13 | public int Refresh { get; set; } 14 | public int Retry { get; set; } 15 | public int Expire { get; set; } 16 | public int TTL { get; set; } 17 | public override string ToString() 18 | { 19 | return string.Format("nameServer={0} | mail={1} | serial={2} | refresh={3} | ...",NameServer ,Mail,Serial ,Refresh ); 20 | } 21 | public SOA_RR(byte[] data, int offset, int len) 22 | { 23 | int endOffset = offset + len; 24 | int labelLen; 25 | NameServer = MyDns.GetLabelName(data, offset, out labelLen); 26 | offset += labelLen; 27 | Mail = MyDns.GetLabelName(data, ++offset, out labelLen); 28 | offset += labelLen; 29 | offset++; 30 | Serial = data[offset++] * 256 * 256 * 256 + data[offset++] * 256 * 256 + data[offset++] * 256 + data[offset++]; 31 | Refresh =data[offset++] * 256 * 256 * 256 + data[offset++] * 256 * 256 + data[offset++] * 256 + data[offset++]; 32 | Retry = data[offset++] * 256 * 256 * 256 + data[offset++] * 256 * 256 + data[offset++] * 256 + data[offset++]; 33 | Expire = data[offset++] * 256 * 256 * 256 + data[offset++] * 256 * 256 + data[offset++] * 256 + data[offset++]; 34 | TTL = data[offset++] * 256 * 256 * 256 + data[offset++] * 256 * 256 + data[offset++] * 256 + data[offset++]; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/LEGainer/DNSUtility/enum.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace LEGainer.DNSUtility 7 | { 8 | /// 9 | /// 4Bit,服务器回复时指定,0:无差错;1:格式错;2:DNS出错;3:域名不存在;4:DNS不支持这类查询;5:DNS拒绝查询;6-15:保留字段 10 | /// 11 | public enum ResultCode 12 | { 13 | OK=0x0, 14 | FormatErr=0x1, 15 | DNSErr=0x2, 16 | DomainNoExist=0x3, 17 | DNSNoSuppot=0x4, 18 | DNSRefuse=0x5 19 | } 20 | /// 21 | /// 指定信息的协议组。 22 | /// 23 | public enum QueryClass 24 | { 25 | IN = 0x01, //指定 Internet 类别。 26 | CSNET = 0x02, //指定 CSNET 类别。(已过时) 27 | CHAOS = 0x03, //指定 Chaos 类别。 28 | HESIOD = 0x04,//指定 MIT Athena Hesiod 类别。 29 | ANY = 0xFF //指定任何以前列出的通配符。 30 | }; 31 | 32 | /// 33 | /// 查询的资源类型 34 | /// 35 | public enum QueryType 36 | { 37 | A = 0x01, //指定计算机 IP 地址。 38 | NS = 0x02, //指定用于命名区域的 DNS 名称服务器。 39 | MD = 0x03, //指定邮件接收站(此类型已经过时了,使用MX代替) 40 | MF = 0x04, //指定邮件中转站(此类型已经过时了,使用MX代替) 41 | CNAME = 0x05, //指定用于别名的规范名称。 42 | SOA = 0x06, //指定用于 DNS 区域的“起始授权机构”。 43 | MB = 0x07, //指定邮箱域名。 44 | MG = 0x08, //指定邮件组成员。 45 | MR = 0x09, //指定邮件重命名域名。 46 | NULL = 0x0A, //指定空的资源记录 47 | WKS = 0x0B, //描述已知服务。 48 | PTR = 0x0C, //如果查询是 IP 地址,则指定计算机名;否则指定指向其它信息的指针。 49 | HINFO = 0x0D, //指定计算机 CPU 以及操作系统类型。 50 | MINFO = 0x0E, //指定邮箱或邮件列表信息。 51 | MX = 0x0F, //指定邮件交换器。 52 | TXT = 0x10, //指定文本信息。 53 | UINFO = 0x64, //指定用户信息。 54 | UID = 0x65, //指定用户标识符。 55 | GID = 0x66, //指定组名的组标识符。 56 | ANY = 0xFF //指定所有数据类型。 57 | 58 | } 59 | } -------------------------------------------------------------------------------- /src/LEGainer/wwwroot/2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 17 |
18 | 19 | 20 |
21 |
22 |
23 | 第一步 填写申请信息 24 |
25 |
26 | 第二步 解析域名及验证 27 |
28 |
29 | 第三步 完成及下载证书 30 |
31 |
32 |
33 |
34 | 35 |
36 |
37 |
38 | 注意:1.最晚要在48小时内解析完毕。2.解析完后等20分钟生效后再点验证。 39 |
40 | 41 |
42 | 请将 _acme-challenge.domain.nameTXT DNS记录值解析为:f4fe2d25ff2fdssiklgirejgjgjgjbmdrj 43 |
44 | 45 | 46 | 47 | 50 |

51 | 52 |

53 | 54 | 55 |
56 | 57 |
58 | 59 | -------------------------------------------------------------------------------- /src/LEGainer/Handlers/DownloadCert.cs: -------------------------------------------------------------------------------- 1 | using LEGainer.ViewModels; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.Extensions.Caching.Memory; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace LEGainer.Handlers 11 | { 12 | public class DownloadCert : BaseHandler 13 | { 14 | 15 | private IMemoryCache cache; 16 | private ILogger logger; 17 | 18 | public DownloadCert(IHttpContextAccessor accessor, IMemoryCache cache,ILogger logger):base(accessor) 19 | { 20 | this.cache = cache; 21 | this.logger = logger; 22 | } 23 | 24 | 25 | public async override Task HandleAsync() 26 | { 27 | GenerateCertResult result = new GenerateCertResult(); 28 | 29 | string sessionKey = this.context.Request.Query["sessionKey"]; 30 | 31 | SSLGenerate generate = this.cache.Get(sessionKey); 32 | if (generate == null) 33 | { 34 | this.context.Response.StatusCode = 404; 35 | await this.context.Response.WriteAsync("你未申请,或申请已过期。"); 36 | return; 37 | } 38 | 39 | if (!generate.GeneratedCert) 40 | { 41 | this.context.Response.StatusCode = 200; 42 | await this.context.Response.WriteAsync("你的申请还未验证通过"); 43 | return; 44 | } 45 | 46 | byte[] zipData = await generate.GetCentificateZip(); 47 | context.Response.ContentType = "application/octet-stream"; 48 | context.Response.Headers.Add("Content-Disposition", new string[] { $"attachment;filename={generate.Domain.TrimStart('*')}.zip" }); 49 | await context.Response.Body.WriteAsync(zipData, 0, zipData.Length); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/LEGainer/Handlers/GenerateCert.cs: -------------------------------------------------------------------------------- 1 | using LEGainer.ViewModels; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.Extensions.Caching.Memory; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace LEGainer.Handlers 11 | { 12 | public class GenerateCert : BaseHandler 13 | { 14 | 15 | private IMemoryCache cache; 16 | private ILogger logger; 17 | 18 | public GenerateCert(IHttpContextAccessor accessor, IMemoryCache cache,ILogger logger):base(accessor) 19 | { 20 | this.cache = cache; 21 | this.logger = logger; 22 | } 23 | 24 | 25 | public async override Task HandleAsync() 26 | { 27 | GenerateCertResult result = new GenerateCertResult(); 28 | result.Success = 0; 29 | 30 | string sessionKey = this.context.Request.Query["sessionKey"]; 31 | SSLGenerate generate = null; 32 | if (!this.cache.TryGetValue(sessionKey, out generate)) 33 | { 34 | result.Message = "你的申请信息已经消失,请重新申请"; 35 | await WriteResultAsync(200, result); 36 | return; 37 | } 38 | 39 | result.Domain = generate.Domain; 40 | if (generate.GeneratedCert) 41 | { 42 | result.Success = 1; 43 | result.Message = "ok"; 44 | await WriteResultAsync(200, result); 45 | return; 46 | } 47 | 48 | (var ret,var message)=await generate.GenerateCert(); 49 | if (!ret) 50 | { 51 | result.Message = message; 52 | } 53 | else 54 | { 55 | result.Success = 1; 56 | result.Message = "ok"; 57 | } 58 | await WriteResultAsync(200, result); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/LEGainer/wwwroot/3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 18 | 19 | 20 |
21 |
22 |
23 | 第一步 填写申请信息 24 |
25 |
26 | 第二步 解析域名及验证 27 |
28 |
29 | 第三步 完成及下载证书 30 |
31 |
32 |
33 |
34 | 35 |
36 |
37 | 38 |
39 | 申请成功 40 |
41 | 42 |
43 | 注意:本站不存储您的证书文件,请尽快下载,超过48小时将清空缓存 44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
证书颁发商:Let's Encrpt
域名:tools.ztimage.com
有效期:3个月
点击下载证书文件
67 | 68 | 69 | 70 | 71 | 72 |
73 | 74 |
75 | 76 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/LEGainer/Handlers/CreateOrder.cs: -------------------------------------------------------------------------------- 1 | using Certes; 2 | using Certes.Acme; 3 | using LEGainer.DNSUtility; 4 | using LEGainer.ViewModels; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.Extensions.Caching.Memory; 7 | using Microsoft.Extensions.Logging; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Net.Http.Headers; 12 | using System.Text.Encodings.Web; 13 | using System.Text.Json; 14 | using System.Text.Unicode; 15 | using System.Threading.Tasks; 16 | 17 | namespace LEGainer.Handlers 18 | { 19 | public class CreateOrder : BaseHandler 20 | { 21 | 22 | private AcmeContext acme; 23 | private IAccountContext account; 24 | private IMemoryCache cache; 25 | private DNSHelper dnsHelper; 26 | private ILogger logger; 27 | 28 | public CreateOrder( 29 | IHttpContextAccessor accessor, 30 | AcmeContext acme, 31 | IAccountContext account, 32 | IMemoryCache cache, 33 | DNSHelper dnsHelper, 34 | ILogger logger 35 | ):base(accessor) 36 | { 37 | this.acme = acme; 38 | this.account = account; 39 | this.cache = cache; 40 | this.dnsHelper = dnsHelper; 41 | this.logger = logger; 42 | } 43 | 44 | public async override Task HandleAsync() 45 | { 46 | string domain = this.context.Request.Query["domain"]; 47 | 48 | 49 | //验证 50 | CreateOrderResult result = new CreateOrderResult(); 51 | System.Text.RegularExpressions.Regex reg = new System.Text.RegularExpressions.Regex(@"^(\*\.)?([a-zA-Z0-9-]{1,61}\.){0,}[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9](\.[a-zA-Z]{2,})+$"); 52 | if (!reg.IsMatch(domain)) 53 | { 54 | result.Success = 0; 55 | result.Message = "域名格式不正确"; 56 | await this.WriteResultAsync(200, result); 57 | return; 58 | } 59 | 60 | result.SessionKey = Guid.NewGuid().ToString(); 61 | SSLGenerate generate = new SSLGenerate(acme, account,dnsHelper); 62 | 63 | bool ret = await generate.CreateOrder(domain); 64 | if (!ret) 65 | { 66 | result.Success = 0; 67 | result.Message = "创建订单失败,10秒后重试"; 68 | } 69 | else 70 | { 71 | result.Success = 1; 72 | result.Message = "ok"; 73 | result.ChallengeDomain = generate.ChallengeDomain; 74 | result.DnsTxtValue = generate.ChallengRecordValue; 75 | 76 | MemoryCacheEntryOptions option = new MemoryCacheEntryOptions(); 77 | option.SetPriority(CacheItemPriority.Normal); 78 | option.SetSlidingExpiration(TimeSpan.FromHours(24)); 79 | this.cache.Set(result.SessionKey,generate,option); 80 | } 81 | 82 | 83 | //序列化结果 84 | await this.WriteResultAsync(200, result); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/LEGainer/DNSUtility/MyDnsQuestion.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace LEGainer.DNSUtility 7 | { 8 | public class MyDnsQuestion 9 | { 10 | //查询结构的3个组成 11 | byte[] _name; 12 | byte[] _type; 13 | byte[] _class; 14 | 15 | public MyDnsQuestion() 16 | { 17 | //初始化,默认查询A记录 18 | _type = new byte[] { 0x00, (byte)QueryType.A }; 19 | _class = new byte[] { 0x00, (byte)QueryClass.IN }; 20 | } 21 | public void Parse(byte[] data) 22 | { 23 | int offset = 12; 24 | for (int i = offset; i < data.Length; i++) 25 | { 26 | if (data[i] == 0) 27 | { 28 | _name = new byte[i - offset + 1]; 29 | for (int j = offset; j <= i; j++) 30 | { 31 | _name[j - offset] = data[j]; 32 | } 33 | break; 34 | } 35 | } 36 | int nLen = _name.Length; 37 | _type = new byte[] { data[offset + nLen], data[offset + nLen + 1] }; 38 | _class = new byte[] { data[offset + nLen + 2], data[offset + nLen + 3] }; 39 | } 40 | public byte[] GetBytes() 41 | { 42 | byte[] result = new byte[_name.Length + _type.Length + _class.Length]; 43 | _name.CopyTo(result, 0); 44 | _type.CopyTo(result, _name.Length); 45 | _class.CopyTo(result, result.Length - _class.Length); 46 | return result; 47 | } 48 | public QueryType Type 49 | { 50 | set 51 | { 52 | _type = new byte[] { 0x00, (byte)value }; 53 | } 54 | } 55 | public QueryClass Class 56 | { 57 | set 58 | { 59 | _class = new byte[] { 0x00, (byte)value }; 60 | } 61 | } 62 | public string Qname 63 | { 64 | set 65 | { 66 | string[] arr = value.Split('.'); 67 | _name = new byte[value.Length + 2]; 68 | int seek = 0; 69 | foreach (string word in arr) 70 | { 71 | byte[] len = new byte[] { (byte)word.Length }; 72 | len.CopyTo(_name, seek); 73 | Encoding.UTF8.GetBytes(word).CopyTo(_name, seek + 1); 74 | seek += word.Length + 1; 75 | } 76 | } 77 | get 78 | { 79 | int len = 0; 80 | int seek = 0; 81 | StringBuilder domain = new StringBuilder(); 82 | len = (int)_name[0]; 83 | while (len != 0) 84 | { 85 | for (int i = len; i > 0; i--) 86 | { 87 | char word = (char)_name[++seek]; 88 | domain.Append(word); 89 | } 90 | 91 | len = (int)_name[++seek]; 92 | if (len > 0) domain.Append('.'); 93 | } 94 | return domain.ToString(); 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/LEGainer/Program.cs: -------------------------------------------------------------------------------- 1 | using Certes; 2 | using Certes.Acme; 3 | using Certes.Acme.Resource; 4 | using LEGainer.DNSUtility; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Hosting; 7 | using System; 8 | using System.Linq; 9 | using System.Text.RegularExpressions; 10 | using System.Threading.Tasks; 11 | using Microsoft.Extensions.Configuration; 12 | using Microsoft.Extensions.Caching.Memory; 13 | using System.IO; 14 | using Microsoft.Extensions.Logging; 15 | using NLog.Extensions.Logging; 16 | using Microsoft.Extensions.DependencyInjection; 17 | using System.Threading; 18 | using Microsoft.Extensions.Options; 19 | 20 | namespace LEGainer 21 | { 22 | class Program 23 | { 24 | static void Main(string[] args) 25 | { 26 | //test(); 27 | Host.CreateDefaultBuilder() 28 | //.ConfigureLogging(logBuilder => { 29 | // logBuilder.ClearProviders(); 30 | // logBuilder.AddNLog("configs/nlog.config"); 31 | //}) 32 | .ConfigureWebHostDefaults(webBuilder => 33 | { 34 | webBuilder 35 | .ConfigureAppConfiguration(configBuilder => 36 | { 37 | configBuilder.AddJsonFile("appsettings.json"); 38 | }) 39 | .UseUrls("http://+:5080") 40 | .UseStartup(); 41 | }) 42 | .Build() 43 | .Run(); 44 | } 45 | 46 | 47 | static void test() 48 | { 49 | string domainUrl = "tools.ztimage.com"; 50 | string email = "cert@ztimage.com"; 51 | 52 | string accountKey = @"-----BEGIN EC PRIVATE KEY----- 53 | MHcCAQEEICuhTDXKkWbmkLPqFS48HihXntI5ONmRO358iWXjYuSmoAoGCCqGSM49 54 | AwEHoUQDQgAEZpejITnlRdE6Ftaw5FTwTe8FzrZv0ZoZY6BrEDcUOUpEkoSnBKr2 55 | 7RdKGqYmv8VujkJlmBpidLNX/HtUyu/G8Q== 56 | -----END EC PRIVATE KEY-----"; 57 | 58 | 59 | 60 | var task = Task.Run(async () => { 61 | 62 | AcmeContext acme = new AcmeContext(WellKnownServers.LetsEncryptStagingV2, KeyFactory.FromPem(accountKey)); 63 | IAccountContext account = await acme.Account(); 64 | 65 | SSLGenerate generate = new SSLGenerate(acme, account,new DNSHelper (null)); 66 | bool result = await generate.CreateOrder(domainUrl); 67 | if (!result) 68 | { 69 | Console.WriteLine("创建订单失败,10秒后重试"); 70 | return; 71 | } 72 | 73 | Console.WriteLine($"添加一条 txt 域名解析记录 域名: {generate.ChallengeDomain}\r\n 值:{generate.ChallengRecordValue}"); 74 | 75 | while (true) 76 | { 77 | (var generateResult, var message) = await generate.GenerateCert(); 78 | if (generateResult) 79 | { 80 | break; 81 | } 82 | Console.WriteLine($"生成失败:{message}"); 83 | await Task.Delay(3000); 84 | } 85 | 86 | byte[] datas =await generate.GetCentificateZip(); 87 | 88 | await File.WriteAllBytesAsync("d:\\certs\\pem.zip", datas); 89 | Console.WriteLine($"cert ok"); 90 | }); 91 | 92 | task.Wait(); 93 | Console.WriteLine("Hello World!"); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/LEGainer/Startup.cs: -------------------------------------------------------------------------------- 1 | using Certes; 2 | using Certes.Acme; 3 | using LEGainer.DNSUtility; 4 | using LEGainer.Handlers; 5 | using LEGainer.Models; 6 | using Microsoft.AspNetCore.Builder; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.Extensions.Caching.Memory; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Hosting; 12 | using Microsoft.Extensions.Logging; 13 | using Microsoft.Extensions.Options; 14 | using System; 15 | using System.Collections.Generic; 16 | using System.Linq; 17 | using System.Threading.Tasks; 18 | 19 | namespace LEGainer 20 | { 21 | public class Startup 22 | { 23 | private IConfiguration config; 24 | private ILogger logger; 25 | 26 | public Startup(IConfiguration config) 27 | { 28 | this.config = config; 29 | this.logger=LoggerFactory.Create(build => { 30 | build.AddConsole(); 31 | }).CreateLogger(); 32 | //this.logger = logger; 33 | } 34 | 35 | public void ConfigureServices(IServiceCollection services) 36 | { 37 | this.logger.LogInformation("config service"); 38 | //config 39 | services.Configure(config.GetSection("legainer")); 40 | 41 | //handler 42 | services.AddScoped(); 43 | services.AddScoped(); 44 | services.AddScoped(); 45 | 46 | services.AddMemoryCache(); 47 | 48 | //acme 49 | services.AddSingleton(serviceProvider => new AcmeContext(WellKnownServers.LetsEncryptV2)); 50 | services.AddSingleton(serviceProvider => { 51 | var acme = serviceProvider.GetRequiredService(); 52 | var legainerOptions = serviceProvider.GetRequiredService>().Value; 53 | return acme.NewAccount(legainerOptions.Email, true).Result; 54 | }); 55 | services.AddSingleton(); 56 | 57 | //memorycache 58 | services.AddSingleton(new MemoryCache(new MemoryCacheOptions())); 59 | 60 | //HttpContext 61 | services.AddHttpContextAccessor(); 62 | } 63 | 64 | 65 | public void Configure(IApplicationBuilder app,IHostApplicationLifetime lifttime, IAccountContext account) 66 | { 67 | app.UseDefaultFiles().UseStaticFiles(new StaticFileOptions() { }); 68 | 69 | //创建订单,生成 dns challege 70 | app.Map("/co", appbuilder => appbuilder.Run(async context => { 71 | await context.RequestServices.GetRequiredService().HandleAsync(); 72 | })); 73 | 74 | //验证 dns,并生成证书 75 | app.Map("/gc", appbuilder => appbuilder.Run(async context => { 76 | await context.RequestServices.GetRequiredService().HandleAsync(); 77 | })); 78 | 79 | //下载证书 80 | app.Map("/dc", appbuilder => appbuilder.Run(async context => 81 | { 82 | await context.RequestServices.GetRequiredService().HandleAsync(); 83 | })); 84 | 85 | app.Run((context) => { 86 | context.Response.StatusCode = 404; 87 | return Task.CompletedTask; 88 | }); 89 | } 90 | 91 | 92 | 93 | 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/LEGainer/DNSUtility/MyDnsRecord.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using LEGainer.DNSUtility.ResourceRecord; 6 | namespace LEGainer.DNSUtility 7 | { 8 | public class MyDnsRecord 9 | { 10 | // NAME 资源记录包含的域名 11 | //TYPE 2个字节表示资源记录的类型,指出RDATA数据的含义 12 | //CLASS 2个字节表示RDATA的类 13 | //TTL 4字节无符号整数表示资源记录可以缓存的时间。0代表只能被传输,但是不能被缓存。 14 | //RDLENGTH 2个字节无符号整数表示RDATA的长度 15 | //RDATA 不定长字符串来表示记录,格式根TYPE和CLASS有关。比如,TYPE是A,CLASS 是 IN,那么RDATA就是一个4个字节的ARPA网络地址。 16 | #region Field 17 | public string Name 18 | { 19 | get; 20 | set; 21 | } 22 | //byte[] _name; 23 | public QueryType QType 24 | { 25 | get; 26 | set; 27 | } 28 | public QueryClass QClass 29 | { 30 | get; 31 | set; 32 | } 33 | public int TTL 34 | { 35 | get; 36 | set; 37 | } 38 | public int RDLength 39 | { 40 | get; 41 | set; 42 | } 43 | public object RDDate 44 | { 45 | get; 46 | set; 47 | 48 | } 49 | //byte[] _RDDate; 50 | #endregion 51 | 52 | /// 53 | /// 资源集合 54 | /// 55 | public List Records = new List(); 56 | 57 | public void Parse(byte[] data, int offset) 58 | { 59 | while (offset < data.Length) 60 | { 61 | int labelLen; 62 | MyDnsRecord RecordItem = new MyDnsRecord(); 63 | RecordItem.Name = MyDns.GetLabelName(data, offset, out labelLen); 64 | offset += labelLen; 65 | // 66 | offset ++; 67 | RecordItem.QType = (QueryType)data[++offset]; 68 | // 69 | offset++; 70 | RecordItem.QClass = (QueryClass)data[++offset]; 71 | // 72 | offset++; 73 | RecordItem.TTL = data[offset++] * 256 * 256 * 256 + data[offset++] * 256 * 256 + data[offset++] * 256 + data[offset++]; 74 | // 75 | RecordItem.RDLength = data[offset++] * 256 + data[offset++]; 76 | // 77 | switch (RecordItem.QType) 78 | { 79 | case QueryType.A: 80 | RecordItem.RDDate = new A_RR(data, offset, RecordItem.RDLength); 81 | break; 82 | case QueryType.CNAME : 83 | RecordItem.RDDate = new CNAME_RR(data, offset, RecordItem.RDLength); 84 | break; 85 | case QueryType.MX : 86 | RecordItem.RDDate = new MX_RR(data, offset, RecordItem.RDLength); 87 | break; 88 | case QueryType.NS: 89 | RecordItem.RDDate = new NS_RR(data, offset, RecordItem.RDLength); 90 | break; 91 | case QueryType.SOA: 92 | RecordItem.RDDate = new SOA_RR(data, offset, RecordItem.RDLength); 93 | break; 94 | case QueryType.TXT: 95 | RecordItem.RDDate =new TXT_RR (data, offset, RecordItem.RDLength); 96 | break; 97 | } 98 | Records.Add(RecordItem); 99 | offset += RecordItem.RDLength; 100 | 101 | } 102 | } 103 | 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/LEGainer/DNSUtility/MyDnsHeader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace LEGainer.DNSUtility 7 | { 8 | public class MyDnsHeader 9 | { 10 | //头部结构为固定长度12字节 11 | //ID:16Bit,报文编号,由客户端指定,服务端回复的时候返回相同 12 | //QR: 1Bit,0代表查询,1代表回复 13 | //Opcode:4Bit,查询代码,0:标准查询;1:反向查询;2:服务器状态查询 14 | //AA:1Bit,是否权威回复 15 | //TC:1Bit,是否截断超过512字节部分 16 | //RD:1Bit,1:递归查询;0:否 17 | //RA:1Bit,服务器回复时指定,1:支持递归查询 18 | //Z:3Bit,保留,值为0 19 | //RCode:4Bit,服务器回复时指定,0:无差错;1:格式错;2:DNS出错;3:域名不存在;4:DNS不支持这类查询;5:DNS拒绝查询;6-15:保留字段。 20 | // QDCOUNT:占16位,2字节。一个无符号数指示查询记录的个数。 21 | //ANCOUNT:占16位,2字节。一个无符号数指明回复记录的个数。 22 | //NSCOUNT:占16位,2字节。一个无符号数指明权威记录的个数。 23 | //ARCOUNT:占16位,2字节。一个无符号数指明额外记录的个数。 24 | 25 | //头部的12字节,初始化设置 26 | public byte[] Header = new byte[]{ 27 | 0x00,0xff, //ID 28 | 0x01, //QR1+Opcode4+AA1+TC1+RD1 29 | 0x00, //RA1+Z3+Rcode4 30 | 0x00,0x01, 31 | 0x00,0x00, 32 | 0x00,0x00, 33 | 0x00,0x00 34 | }; 35 | public void Parse(byte[] data) 36 | { 37 | Header = new byte[12]; 38 | for (int i = 0; i < 12; i++) 39 | { 40 | Header[i] = data[i]; 41 | } 42 | } 43 | /// 44 | /// 额外记录的个数 45 | /// 46 | public int ARCOUNT 47 | { 48 | get 49 | { 50 | return Header[10] * 256 + Header[11]; 51 | } 52 | } 53 | /// 54 | /// 权威记录的个数 55 | /// 56 | public int NSCOUNT 57 | { 58 | get 59 | { 60 | return Header[8] * 256 + Header[9]; 61 | } 62 | } 63 | /// 64 | /// 回复记录的个数 65 | /// 66 | public int ANCOUNT 67 | { 68 | get 69 | { 70 | return Header[6] * 256 + Header[7]; 71 | } 72 | } 73 | 74 | private ResultCode resultCode = ResultCode.OK; 75 | /// 76 | /// 服务器返回码 77 | /// 78 | public ResultCode ResultCode 79 | { 80 | get 81 | { 82 | if (resultCode != ResultCode.OK) 83 | { 84 | return resultCode; 85 | } 86 | return (ResultCode)(Header[3] & 0x0f); 87 | } 88 | set 89 | { 90 | this.resultCode = value; 91 | } 92 | 93 | 94 | } 95 | /// 96 | /// 查询(0)还是回复(1) 97 | /// 98 | public int QR 99 | { 100 | get 101 | { 102 | byte qr = Header[2]; 103 | return (qr >> 7); 104 | } 105 | } 106 | /// 107 | /// 标识id 108 | /// 109 | public byte[] ID 110 | { 111 | get 112 | { 113 | return new byte[] { Header[0], Header[1] }; 114 | } 115 | } 116 | /// 117 | /// 返回整个头部数据 118 | /// 119 | /// 120 | public byte[] GetBytes() 121 | { 122 | return Header; 123 | } 124 | /// 125 | /// 设置标识id 126 | /// 127 | /// 128 | public void NewID(byte[] ID) 129 | { 130 | Header[0] = ID[0]; 131 | Header[1] = ID[1]; 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/LEGainer/DNSUtility/DNSHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace LEGainer.DNSUtility 8 | { 9 | public class DNSHelper 10 | { 11 | private ILogger logger; 12 | public DNSHelper(ILogger logger) 13 | { 14 | this.logger = logger; 15 | } 16 | public class CheckDnsResult 17 | { 18 | public CheckDnsResult() 19 | { 20 | this.RecordValues = new List(); 21 | } 22 | public bool Result { get; set; } 23 | public List RecordValues { get; set; } 24 | } 25 | private static readonly string[] dnsServers = new string[] { 26 | "114.114.114.114" ,//114DNS 27 | "1.2.4.8", //CNNIC 28 | "119.29.29.29",//DNSPod 29 | "223.5.5.5",//aliDNS 30 | "8.8.8.8"//Google 31 | }; 32 | /// 33 | /// 检测DNS 34 | /// 35 | /// 36 | /// 37 | /// 38 | public async Task CheckDns(string domain, string wantRecordValue) 39 | { 40 | CheckDnsResult result = new CheckDnsResult(); 41 | result.Result = false; 42 | int offset = 0; 43 | while (offset < dnsServers.Length) 44 | { 45 | var ret = await GetDnsTxtRecord(domain, dnsServers[offset]); 46 | if (ret.Success) 47 | { 48 | if (ret.RecordValue.Equals(wantRecordValue, StringComparison.OrdinalIgnoreCase)) 49 | { 50 | result.Result = true; 51 | return result; 52 | } 53 | if (!result.RecordValues.Contains(ret.RecordValue)) 54 | { 55 | result.RecordValues.Add(ret.RecordValue); 56 | } 57 | } 58 | 59 | 60 | offset++; 61 | } 62 | return result; 63 | } 64 | 65 | public async Task GetDnsTxtRecord(string domain, string dnsServer) 66 | { 67 | DnsResult result = new DnsResult(); 68 | 69 | var dns = new MyDns(); 70 | if (!await dns.Search(domain, QueryType.TXT, dnsServer, null)) 71 | { 72 | result.Success = false; 73 | result.RecordValue = string.Empty; 74 | switch (dns.header.ResultCode) 75 | { 76 | case ResultCode.OK: 77 | result.ErrorMessage = "ok"; 78 | break; 79 | case ResultCode.FormatErr: 80 | result.ErrorMessage = "格式错误"; 81 | break; 82 | case ResultCode.DNSErr: 83 | result.ErrorMessage = "DNS出错"; 84 | break; 85 | 86 | case ResultCode.DomainNoExist: 87 | result.ErrorMessage = "域名不存在"; 88 | break; 89 | 90 | case ResultCode.DNSNoSuppot: 91 | result.ErrorMessage = "DNS不支持这类查询"; 92 | break; 93 | 94 | case ResultCode.DNSRefuse: 95 | result.ErrorMessage = "DNS拒绝查询"; 96 | break; 97 | default: 98 | result.ErrorMessage = "未知错误"; 99 | break; 100 | } 101 | 102 | 103 | return result; 104 | } 105 | else 106 | { 107 | result.Success = true; 108 | result.RecordValue = dns.record?.Records?.FirstOrDefault()?.RDDate?.ToString(); 109 | 110 | } 111 | 112 | return result; 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | # The packages folder can be ignored because of Package Restore 156 | **/packages/* 157 | # except build/, which is used as an MSBuild target. 158 | !**/packages/build/ 159 | # Uncomment if necessary however generally it will be regenerated when needed 160 | #!**/packages/repositories.config 161 | # NuGet v3's project.json files produces more ignoreable files 162 | *.nuget.props 163 | *.nuget.targets 164 | 165 | # Microsoft Azure Build Output 166 | csx/ 167 | *.build.csdef 168 | 169 | # Microsoft Azure Emulator 170 | ecf/ 171 | rcf/ 172 | 173 | # Windows Store app package directories and files 174 | AppPackages/ 175 | BundleArtifacts/ 176 | Package.StoreAssociation.xml 177 | _pkginfo.txt 178 | 179 | # Visual Studio cache files 180 | # files ending in .cache can be ignored 181 | *.[Cc]ache 182 | # but keep track of directories ending in .cache 183 | !*.[Cc]ache/ 184 | 185 | # Others 186 | ClientBin/ 187 | ~$* 188 | *~ 189 | *.dbmdl 190 | *.dbproj.schemaview 191 | *.pfx 192 | *.publishsettings 193 | node_modules/ 194 | orleans.codegen.cs 195 | 196 | # Since there are multiple workflows, uncomment next line to ignore bower_components 197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 198 | #bower_components/ 199 | 200 | # RIA/Silverlight projects 201 | Generated_Code/ 202 | 203 | # Backup & report files from converting an old project file 204 | # to a newer Visual Studio version. Backup files are not needed, 205 | # because we have git ;-) 206 | _UpgradeReport_Files/ 207 | Backup*/ 208 | UpgradeLog*.XML 209 | UpgradeLog*.htm 210 | 211 | # SQL Server files 212 | *.mdf 213 | *.ldf 214 | 215 | # Business Intelligence projects 216 | *.rdl.data 217 | *.bim.layout 218 | *.bim_*.settings 219 | 220 | # Microsoft Fakes 221 | FakesAssemblies/ 222 | 223 | # GhostDoc plugin setting file 224 | *.GhostDoc.xml 225 | 226 | # Node.js Tools for Visual Studio 227 | .ntvs_analysis.dat 228 | 229 | # Visual Studio 6 build log 230 | *.plg 231 | 232 | # Visual Studio 6 workspace options file 233 | *.opt 234 | 235 | # Visual Studio LightSwitch build output 236 | **/*.HTMLClient/GeneratedArtifacts 237 | **/*.DesktopClient/GeneratedArtifacts 238 | **/*.DesktopClient/ModelManifest.xml 239 | **/*.Server/GeneratedArtifacts 240 | **/*.Server/ModelManifest.xml 241 | _Pvt_Extensions 242 | 243 | # Paket dependency manager 244 | .paket/paket.exe 245 | paket-files/ 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # JetBrains Rider 251 | .idea/ 252 | *.sln.iml 253 | -------------------------------------------------------------------------------- /src/LEGainer/wwwroot/settings/nginx.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LEGainer - Nginx SSL证书设置 - 免费在线申请SSL证书 6 | 7 | 8 | 9 | 10 |
11 | 18 |
19 | 20 |
21 |
22 |

Nginx SSL证书设置

23 |

一、证书获取

24 |

使用 https://ssl.ztimage.com 在线获取Let's Encrypt提供的免费证书。成功申请到证书后,下载下来的文件夹中Nginx文件夹里保存的就是本次设置需要的crt公钥和key私钥文件了,如果是从其它渠道获取的证书文件则需要准备这两个公钥和私钥文件。

25 |

二、Nginx SSL模块支持

26 |

通过命令查看nginx 编译时是否已经配置了SSL模块,命令:# nginx -V

27 |

如果出现“OpendSSL”字样则说明已经编译进了SSL模块,直接进入第三步。

28 |

29 |

 如果未包含SSL模块可按以下步骤编译源码

30 |
1.下载源码
31 |
2.配置 
32 |

#./configure --prefix=/usr/local
 --prefix=/usr/local/nginx
 --with-http_ssl_module
 --with-file-aio
 --with-http_realip_module
 --with-http_stub_status_module

33 |
3.编译和安装
34 |

注意:如果你的nginx正在提供服务千万不要使用 make install,否则会覆盖安装

35 |

# make

36 |

//停止服务并备份
# nginx stop
# cp /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.bak

37 |

//使用新编译的程序
# cp /objs/nginx /usr/local/nginx/sbin/
# nginx -V
# nginx

38 |

 

39 |

三、配置文件修改

40 |

打开nginx的站点配置文件,在需要配置ssl证书的站点中添加以下配置信息:

41 |

listen 443 ssl;

42 |

ssl_certificate /usr/certs/cert.crt;
ssl_certificate_key /usr/certs/private.key;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;

43 |

ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;

44 |

现在站点配置看起来像这样:

45 |

 

46 |

其中ssl_certificate设置的是公钥文件路径,ssl_certificate_key设置的是私钥文件路径

47 |

保存修改,测试一下我们的配置是否有问题

48 |

# nginx -t

49 |

如果测试没有问题,我们重新载入配置,让其生效

50 |

# nginx -s reload

51 |

生效后通过https的方式在浏览器访问域名就可以看到我们的站点启用SSL加密。 

52 |

四、访问http,自动跳转到https

53 |
方法一:使用url rewrite,将http的访问重定向至https
54 |

server {
  listen 80;
  server_name www.域名.com;
  rewrite ^(.*) https://$server_name$1 permanent;
}
server {
  listen 443;
  server_name www.域名.com;
  root /home/www;
  ssl on;
  ssl_certificate /usr/certs/cert.crt;
  ssl_certificate_key /usr/certs/private.key;
}

55 |
方法二:使用HSTS(HTTP Strict Transport Security)
56 |

使用方法一主要有两个缺点:

57 |

1.不安全,rewrite使用的是302跳转来实现的,302跳转会暴露用户访问的站点路径和query string,易被劫持。

58 |

2.多增加一次访问,访问会显得更慢一点。

59 |

基于以上缺点HSTS诞生了,HTSP 就是添加 header 头(add_header Strict-Transport-Security max-age=15768000;includeSubDomains),告诉浏览器网站使用 HTTPS 访问,支持HSTS的浏览器就会在后面的请求中直接切换到 HTTPS。在 Chrome 中会看到浏览器自己会有个 307 Internal Redirect 的内部重定向。在一段时间内也就是max-age定义的时间,不管用户输入 www.ztimage.com 还是 http://www.ztimage.com ,都会默认将请求内部跳转到https://www.ztimage.com 。

60 |

采用HSTS协议的网站将保证浏览器始终连接到该网站的HTTPS加密版本,不需要用户手动在URL地址栏中输入加密地址。

61 |

该协议将帮助网站采用全局加密,用户看到的就是该网站的安全版本。

62 |

在nginx中配置HSTS,仅需要在https的server站点添加如下头部:

63 |

add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";

64 |

这样当第一次以https方式访问我的网站,nginx则会告知客户端的浏览器,以后即便地址栏输入http,也要浏览器改成https来访问我的nginx服务器。

65 |

 

66 |
67 |
68 | 69 | 70 | -------------------------------------------------------------------------------- /src/LEGainer/DNSUtility/MyDns.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Net; 6 | using System.Net.Sockets; 7 | using System.Threading.Tasks; 8 | 9 | namespace LEGainer.DNSUtility 10 | { 11 | /// 12 | /// from https://github.com/forthxu/mydns 13 | /// 14 | public class MyDns 15 | { 16 | 17 | public MyDnsHeader header; 18 | public MyDnsQuestion question; 19 | public MyDnsRecord record; 20 | public byte[] recvData; 21 | 22 | private readonly int local_port = 8807; 23 | private readonly short package_ttl = 3; 24 | 25 | private byte[] newid() 26 | { 27 | byte[] result = new byte[2]; 28 | Random rd = new Random(); 29 | rd.NextBytes(result); 30 | return result; 31 | } 32 | 33 | public async Task Search(string host, QueryType? qtype, string dns, byte[] id) 34 | { 35 | string _dns = string.IsNullOrEmpty(dns) ? "8.8.8.8" : dns; 36 | byte[] _id = id == null ? newid() : id; 37 | QueryType _qtype = qtype == null ? QueryType.A : (QueryType)qtype; 38 | 39 | header = new MyDnsHeader(); 40 | header.NewID(_id); 41 | question = new MyDnsQuestion(); 42 | question.Class = QueryClass.IN; 43 | question.Type = _qtype; 44 | question.Qname = host; 45 | byte[] data_head = header.GetBytes(); 46 | byte[] data_question = question.GetBytes(); 47 | byte[] sendData = new byte[data_head.Length + data_question.Length]; 48 | data_head.CopyTo(sendData, 0); 49 | data_question.CopyTo(sendData, data_head.Length); 50 | 51 | try 52 | { 53 | byte[] receiveDatas = await UDPSend(sendData, _dns); 54 | //recvData = await TCPSend(sendData, _dns); 55 | Parse(receiveDatas); 56 | } 57 | catch 58 | { 59 | header.ResultCode = ResultCode.DNSErr; 60 | } 61 | 62 | return header.ResultCode == 0 ? true : false; 63 | } 64 | 65 | void Parse(byte[] data) 66 | { 67 | header.Parse(data); 68 | question.Parse(data); 69 | record = new MyDnsRecord(); 70 | record.Parse(data, question.GetBytes().Length + 12); 71 | } 72 | 73 | async Task TCPSend(byte[] sendData, string remoteHost) 74 | { 75 | IPEndPoint host = new IPEndPoint(IPAddress.Parse(remoteHost), 53); 76 | IPEndPoint myip = new IPEndPoint(IPAddress.Any, local_port); 77 | 78 | TcpClient client = new TcpClient(myip); 79 | client.Client.SendTimeout = 1500; 80 | client.Client.ReceiveTimeout = 1500; 81 | //myClient.Ttl = package_ttl; 82 | await client.ConnectAsync(IPAddress.Parse(remoteHost), 53); 83 | await client.Client.SendAsync(new ArraySegment(sendData, 0, sendData.Length), SocketFlags.None); 84 | byte[] localD = new byte[1024]; 85 | int count = await client.Client.ReceiveAsync(new ArraySegment (localD),SocketFlags.None); 86 | client.Close(); 87 | byte[] buffer = new byte[count]; 88 | Buffer.BlockCopy(localD, 0, buffer, 0, count); 89 | 90 | 91 | return buffer; 92 | } 93 | 94 | const Int32 TIMEOUT_SEND = 500; 95 | async Task UDPSend(byte[] sendData, string remoteHost) 96 | { 97 | IPEndPoint host = new IPEndPoint(IPAddress.Parse(remoteHost), 53); 98 | //IPEndPoint myip = new IPEndPoint(IPAddress.Any, local_port); 99 | //UdpClient client = new UdpClient(myip); 100 | 101 | using (UdpClient client = new UdpClient()) 102 | { 103 | client.Client.SendTimeout = 1500; 104 | client.Client.ReceiveTimeout = 1500; 105 | //myClient.Ttl = package_ttl; 106 | await client.SendAsync(sendData, sendData.Length, host); 107 | var result = await client.ReceiveAsync(); 108 | return result.Buffer; 109 | } 110 | 111 | 112 | } 113 | public static string GetLabelName(byte[] data, int offset, out int labelLen) 114 | { 115 | bool alreadyJump = false; 116 | int seek = offset; 117 | int len = data[seek]; 118 | labelLen = 0; 119 | StringBuilder result = new StringBuilder(63); 120 | while (len > 0 && seek < data.Length) 121 | { 122 | if (len > 191 && len < 255) 123 | { 124 | if (alreadyJump) 125 | { 126 | labelLen = seek - offset; 127 | return result.ToString(); 128 | } 129 | int tempLen; 130 | result.Append(GetLabelName(data, data[++seek] + (len - 192) * 256, out tempLen)); 131 | alreadyJump = true; 132 | labelLen = seek - offset; 133 | return result.ToString(); 134 | } 135 | else if (len < 64) 136 | { 137 | for (; len > 0; len--) 138 | { 139 | result.Append((char)data[++seek]); 140 | } 141 | len = data[++seek]; 142 | if (len > 0) result.Append("."); 143 | } 144 | } 145 | labelLen = seek - offset; 146 | return result.ToString(); 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/LEGainer/wwwroot/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0px; 3 | padding: 0px; 4 | background: #F8F8F8; 5 | font-family: PingFangSC-Medium, PingFang SC; 6 | } 7 | 8 | .clear { 9 | clear: both; 10 | } 11 | 12 | .header { 13 | width: 100%; 14 | height: 75px; 15 | background: #FFFFFF; 16 | } 17 | 18 | .header-container { 19 | width: 1280px; 20 | margin: 0px auto; 21 | position: relative; 22 | top: 50%; 23 | transform: translateY(-50%); 24 | } 25 | 26 | .title { 27 | width: 241px; 28 | height: 33px; 29 | font-size: 24px; 30 | font-weight: 500; 31 | color: #2D2D2D; 32 | line-height: 33px; 33 | } 34 | 35 | .nav { 36 | float: right; 37 | margin-left: 30px; 38 | height: 28px; 39 | font-size: 20px; 40 | font-weight: 400; 41 | color: #0071E4; 42 | line-height: 28px; 43 | } 44 | 45 | .container { 46 | width: 1280px; 47 | margin: 0px auto; 48 | padding-top: 86px; 49 | background: #FFFFFF; 50 | min-height: 700px; 51 | } 52 | 53 | .proccess { 54 | margin: 0px 85px; 55 | } 56 | 57 | .proccess-nav { 58 | float: left; 59 | width: 33.33%; 60 | height: 100px; 61 | font-size: 26px; 62 | font-weight: 500; 63 | color: #5D5D5D; 64 | line-height: 100px; 65 | text-align: center; 66 | vertical-align: middle; 67 | } 68 | 69 | .proccess-active { 70 | background: #4DB2B6; 71 | border-radius: 50px; 72 | color: #FFFFFF; 73 | } 74 | 75 | .separation-line { 76 | margin: 19px 97px 0px 97px; 77 | border: 1px solid #979797; 78 | } 79 | 80 | .content { 81 | text-align: center; 82 | } 83 | 84 | .alter-msg { 85 | margin-top: 47px; 86 | width: 100%; 87 | text-align: center; 88 | font-size: 18px; 89 | font-weight: 400; 90 | color: #AA0000; 91 | } 92 | 93 | .input-area { 94 | margin-top: 34px; 95 | width: 550px; 96 | height: 100px; 97 | border: 1px solid #979797; 98 | border-radius: 6px; 99 | font-size: 26px; 100 | font-weight: 400; 101 | color: #2D2D2D; 102 | line-height: 37px; 103 | padding-left: 27px; 104 | } 105 | 106 | .btn { 107 | width: 360px; 108 | height: 100px; 109 | background: #0071E4; 110 | border-radius: 6px; 111 | opacity: 0.69; 112 | font-size: 28px; 113 | font-weight: 500; 114 | color: #FFFFFF; 115 | border: none; 116 | margin-top: 45px; 117 | } 118 | 119 | .notice-msg { 120 | margin-top: 47px; 121 | width: 100%; 122 | text-align: center; 123 | color: #2d2d2d; 124 | font-size: 18px; 125 | } 126 | 127 | .content-msg { 128 | margin-top: 70px; 129 | width: 100%; 130 | text-align: center; 131 | font-size: 18px; 132 | font-weight: 400; 133 | color: #2d2d2d; 134 | } 135 | 136 | .it-msg { 137 | color: #aa0000; 138 | } 139 | 140 | .tips-msg { 141 | margin-top: 110px; 142 | width: 100%; 143 | text-align: center; 144 | font-size: 18px; 145 | font-weight: 400; 146 | color: #AA0000; 147 | } 148 | 149 | .success-title { 150 | margin-top: 50px; 151 | width: 100%; 152 | text-align: center; 153 | font-size: 48px; 154 | font-weight: 500; 155 | color: #5D5D5D; 156 | line-height: 67px; 157 | } 158 | 159 | .success-notice { 160 | margin-top: 25px; 161 | width: 100%; 162 | text-align: center; 163 | font-size: 18px; 164 | font-weight: 500; 165 | color: #5D5D5D; 166 | line-height: 67px; 167 | } 168 | 169 | .success-info { 170 | margin: 0px auto; 171 | margin-top: 20px; 172 | width: 400px; 173 | text-align: center; 174 | height: 35px; 175 | font-size: 20px; 176 | } 177 | 178 | .success_key { 179 | text-align: right; 180 | width: 50%; 181 | } 182 | 183 | .success_value { 184 | text-align: left; 185 | width: 50%; 186 | } 187 | .setting-info { 188 | width: 800px; 189 | margin: 0px auto; 190 | } 191 | .popContainer { 192 | position: fixed; 193 | top: 0; 194 | left: 0; 195 | right: 0; 196 | bottom: 0; 197 | z-index: 999; 198 | background: rgba(0,0,0,0.8); 199 | } 200 | 201 | .pop-image { 202 | width: 200px; 203 | height: 50px; 204 | margin: 0px auto; 205 | position: relative; 206 | top: 50%; 207 | transform: translateY(-50%); 208 | font-size: 18px; 209 | font-weight: 500; 210 | color: white; 211 | text-align: center; 212 | } 213 | 214 | .pop-message { 215 | line-height: 80px; 216 | } 217 | 218 | #preloader_6 { 219 | position: relative; 220 | width: 42px; 221 | height: 42px; 222 | left: 50%; 223 | transform: translateX(-50%); 224 | animation: preloader_6 5s infinite linear; 225 | } 226 | 227 | #preloader_6 span { 228 | width: 20px; 229 | height: 20px; 230 | position: absolute; 231 | background: red; 232 | display: block; 233 | animation: preloader_6_span 1s infinite linear; 234 | } 235 | 236 | #preloader_6 span:nth-child(1) { 237 | background: #2ecc71; 238 | } 239 | 240 | #preloader_6 span:nth-child(2) { 241 | left: 22px; 242 | background: #9b59b6; 243 | animation-delay: .2s; 244 | } 245 | 246 | #preloader_6 span:nth-child(3) { 247 | top: 22px; 248 | background: #3498db; 249 | animation-delay: .4s; 250 | } 251 | 252 | #preloader_6 span:nth-child(4) { 253 | top: 22px; 254 | left: 22px; 255 | background: #f1c40f; 256 | animation-delay: .6s; 257 | } 258 | 259 | @keyframes preloader_6 { 260 | from { 261 | -ms-transform: rotate(0deg); 262 | } 263 | 264 | to { 265 | -ms-transform: rotate(360deg); 266 | } 267 | } 268 | 269 | @keyframes preloader_6_span { 270 | 0% { 271 | transform: scale(1); 272 | } 273 | 274 | 50% { 275 | transform: scale(0.5); 276 | } 277 | 278 | 100% { 279 | transform: scale(1); 280 | } 281 | } 282 | 283 | [v-cloak] { 284 | display: none !important; 285 | } 286 | 287 | a { 288 | text-decoration: none; 289 | } 290 | 291 | a:link { 292 | color: #0071E4; 293 | } 294 | 295 | a:visited { 296 | color: #0071E4; 297 | } 298 | 299 | a:hover { 300 | color: #0071E4; 301 | } 302 | 303 | a:active { 304 | color: #0071E4; 305 | } 306 | 307 | .title a { 308 | color: #2D2D2D; 309 | } 310 | 311 | .title a:link { 312 | color: #2D2D2D; 313 | } 314 | 315 | .footer { 316 | line-height: 50px; 317 | text-align: center; 318 | } -------------------------------------------------------------------------------- /src/LEGainer/SSLGenerate.cs: -------------------------------------------------------------------------------- 1 | using Certes; 2 | using Certes.Acme; 3 | using Certes.Acme.Resource; 4 | using LEGainer.DNSUtility; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.IO.Compression; 9 | using System.Linq; 10 | using System.Threading.Tasks; 11 | 12 | namespace LEGainer 13 | { 14 | /// 15 | /// SSL证书生成器 16 | /// 17 | public class SSLGenerate 18 | { 19 | public SSLGenerate() 20 | {} 21 | 22 | public SSLGenerate(AcmeContext acme, IAccountContext account,DNSHelper dnsHelper) 23 | { 24 | this.acme = acme; 25 | this.account = account; 26 | this.dnsHelper = dnsHelper; 27 | } 28 | 29 | 30 | private AcmeContext acme; 31 | private IAccountContext account; 32 | private DNSHelper dnsHelper; 33 | 34 | /// 35 | /// DNS质询 36 | /// 37 | private IChallengeContext dnsChallenge; 38 | 39 | /// 40 | /// 订单 41 | /// 42 | private IOrderContext order; 43 | 44 | /// 45 | /// 私钥匙 46 | /// 47 | private IKey privateKey; 48 | 49 | /// 50 | /// 证书 51 | /// 52 | private CertificateChain cert; 53 | 54 | 55 | 56 | public string Domain { get; set; } 57 | 58 | public string FriendllyDomain 59 | { 60 | get 61 | { 62 | return this.Domain.TrimStart('*', '.'); 63 | } 64 | } 65 | 66 | 67 | /// 68 | /// 是否已经生成 69 | /// 70 | public bool GeneratedCert { get; set; } 71 | 72 | /// 73 | /// 质询域名 74 | /// 75 | public string ChallengeDomain { get; set; } 76 | 77 | /// 78 | /// 质询域名的 TXT DNS记录值 79 | /// 80 | public string ChallengRecordValue { get; set; } 81 | 82 | 83 | 84 | /// 85 | /// 创建订单 86 | /// 87 | /// 88 | /// 89 | /// 90 | public async Task CreateOrder(string domain) 91 | { 92 | //todo:验证domain和email 93 | 94 | 95 | this.Domain = domain; 96 | try 97 | { 98 | this.order = await acme.NewOrder(new[] { Domain }); 99 | var authz = (await this.order.Authorizations()).First(); 100 | this.dnsChallenge = await authz.Dns(); 101 | 102 | 103 | this.ChallengeDomain = $"_acme-challenge.{this.FriendllyDomain}"; 104 | this.ChallengRecordValue = acme.AccountKey.DnsTxt(this.dnsChallenge.Token); 105 | return true; 106 | } 107 | catch 108 | { 109 | return false; 110 | } 111 | } 112 | 113 | /// 114 | /// 验证、生成证书 115 | /// 116 | /// 117 | public async Task<(bool,string)> GenerateCert() 118 | { 119 | if (GeneratedCert) 120 | { 121 | return (true,string.Empty ); 122 | } 123 | 124 | var checkResult = await dnsHelper.CheckDns(this.ChallengeDomain, this.ChallengRecordValue); 125 | if (!checkResult.Result) 126 | { 127 | string msg = "未找到 DNS TXT 解析信息"; 128 | if (checkResult.RecordValues.Count > 0) 129 | { 130 | msg = "未找到正确的解析,当前的解析值有:
"; 131 | foreach (var item in checkResult.RecordValues) 132 | { 133 | msg += item + "
"; 134 | } 135 | 136 | } 137 | 138 | return (false, msg); 139 | } 140 | 141 | try 142 | { 143 | Challenge challenge = await this.dnsChallenge.Validate(); 144 | 145 | this.privateKey = KeyFactory.NewKey(KeyAlgorithm.ES256); 146 | this.cert = await this.order.Generate(new CsrInfo 147 | { 148 | CountryName = "CN", 149 | //State = "beijing", 150 | //Locality = "beijing", 151 | //Organization = "ZTImage", 152 | //OrganizationUnit = "Dev", 153 | CommonName = this.Domain 154 | }, this.privateKey); 155 | 156 | this.GeneratedCert = true; 157 | return (true, "success"); 158 | } 159 | catch 160 | { 161 | return (false, "生成证书时出现问题,请联系我们"); 162 | } 163 | 164 | } 165 | 166 | 167 | /// 168 | /// 获取证书 169 | /// 170 | /// 171 | public async Task GetCentificateZip() 172 | { 173 | using (MemoryStream ms = new MemoryStream()) 174 | { 175 | using (ZipArchive zip = new ZipArchive(ms, ZipArchiveMode.Create, true)) 176 | { 177 | //await CompressionData(zip, "readme", "Readme.txt"); 178 | 179 | 180 | //pem 181 | await CompressionData(zip, this.cert.ToPem(), $"Pem/_{this.FriendllyDomain}_FullChain.pem"); 182 | await CompressionData(zip, this.privateKey.ToPem(), $"Pem/_{this.FriendllyDomain}_PrivateKey.pem"); 183 | await CompressionData(zip, this.cert.Certificate.ToPem(), $"Pem/_{this.FriendllyDomain}_Certificate.pem"); 184 | 185 | //Nginx 186 | await CompressionData(zip, this.cert.ToPem(), $"Nginx/{this.FriendllyDomain}_cert.crt"); 187 | await CompressionData(zip, this.privateKey.ToPem(),$"Nginx/{this.FriendllyDomain}_key.key"); 188 | 189 | //Apache 190 | 191 | 192 | //Tomcat 193 | 194 | 195 | //IIS 196 | 197 | } 198 | 199 | return ms.ToArray(); 200 | } 201 | 202 | 203 | async Task CompressionData(ZipArchive zip, string pem,string fileName) 204 | { 205 | var utf8withoutBom = new System.Text.UTF8Encoding(false); 206 | byte[] datas= utf8withoutBom.GetBytes(pem.Replace("\r\n", "\n").TrimEnd('\n')); 207 | ZipArchiveEntry entry = zip.CreateEntry(fileName); 208 | using (Stream sw = entry.Open()) 209 | { 210 | await sw.WriteAsync(datas, 0, datas.Length); 211 | } 212 | } 213 | 214 | } 215 | 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/LEGainer/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LEGainer - 免费在线申请SSL证书 6 | 7 | 8 | 9 | 10 | 11 | 12 | 21 | 22 |
23 |
24 |
25 |
26 | 27 | 28 | 29 | 30 |
31 | {{message}} 32 |
33 |
34 |
35 |
36 | 第一步 填写申请信息 37 |
38 |
39 | 第二步 解析域名及验证 40 |
41 |
42 | 第三步 完成及下载证书 43 |
44 |
45 |
46 |
47 | 48 |
49 | 50 | 51 |
52 |
53 | 注意:本站申请的证书皆为Let's Encrypt发放,验证域名所有权时,需要域名解析。 54 |
55 | 56 |

57 | 58 |

59 |

60 | 61 |

62 |
63 | 64 | 65 | 66 |
67 |
68 | 注意:1.最晚要在48小时内解析完毕。2.解析完后等20分钟生效后再点验证。 69 |
70 | 71 |
72 | 请将 {{order.ChallengeDomain}}TXT DNS记录值解析为:{{order.DnsTxtValue}} 73 |
74 | 77 |

78 | 79 |

80 |
81 | 82 |
83 |
84 | 申请成功 85 |
86 |
87 | 注意:本站不存储您的证书文件,请尽快下载,超过48小时将清空缓存 88 |
89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 |
证书颁发商:Let's Encrypt
域名:{{cert.Domain}}
有效期:3个月
点击下载证书文件
106 |
107 |
108 | 109 | 112 | 175 | 176 | 177 | --------------------------------------------------------------------------------