diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fbebe6a..5165cdb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,7 +38,6 @@ jobs: - name: Update Help run: | - $workpath = [System.Environment]::CurrentDirectory Set-Location .. git clone https://github.com/LTCatt/PCL2Help.git -b master --single-branch --depth 1 Set-Location PCL2Help diff --git a/Plain Craft Launcher 2/App.config b/Plain Craft Launcher 2/App.config index 8d134eb..d464350 100644 --- a/Plain Craft Launcher 2/App.config +++ b/Plain Craft Launcher 2/App.config @@ -32,4 +32,4 @@ - + \ No newline at end of file diff --git a/Plain Craft Launcher 2/Modules/Base/ModNet.vb b/Plain Craft Launcher 2/Modules/Base/ModNet.vb index f93903a..f7d1b3c 100644 --- a/Plain Craft Launcher 2/Modules/Base/ModNet.vb +++ b/Plain Craft Launcher 2/Modules/Base/ModNet.vb @@ -1,9 +1,216 @@ -Public Module ModNet +Imports System.Drawing +Imports System.Net.Http +Imports System.Net.Http.Headers +Imports System.Threading.Tasks +Imports Newtonsoft.Json + +Public Module ModNet Public Const NetDownloadEnd As String = ".PCLDownloading" + Public ReadOnly HttpProxy As New HttpWebProxyFactory() + Private ClientFactory As New HttpClientFactory() + Private Request As New HttpRequest() + Public Class HttpRequest + Public Sub New() + End Sub + Public Function GetRequestMessage(RequestUrl As String, + Optional Method As String = "GET", + Optional Headers As Dictionary(Of String, String) = Nothing, + Optional Data As Byte() = Nothing, + Optional UseBrowserUA As Boolean = False, + Optional RequireHeaderSign As Boolean = False, + Optional RequireCdnSign As Boolean = False) As HttpRequestMessage + + Dim RequestMethod As HttpMethod = GetHttpMethod(Method) + If RequireCdnSign Then RequestUrl = SecretCdnSign(RequestUrl) + + Dim RequestMessage As New HttpRequestMessage(RequestMethod, RequestUrl) + '基本参数验证 + If Data IsNot Nothing AndAlso Not {HttpMethod.Get, HttpMethod.Head}.Contains(RequestMethod) Then + RequestMessage.Content = New ByteArrayContent(Data) + RequestMessage.Content.Headers.ContentLength = Data.Length + End If + + If Headers IsNot Nothing Then + For Each Header In Headers + If Header.Key.ContainsF("Content") AndAlso RequestMessage.Content IsNot Nothing Then + 'FixContentHeader 往下挪一行就 NullReference 了 +FixContentHeader: + If Data Is Nothing OrElse RequestMessage.Content Is Nothing Then Continue For + If RequestMessage.Content.Headers.Contains(Header.Key) Then Continue For + If Not RequestMessage.Content.Headers.Contains(Header.Key) Then + If Header.Key.ToLower() = "Content-Type" Then RequestMessage.Content.Headers.ContentType = New MediaTypeHeaderValue(Header.Value) + RequestMessage.Content.Headers.TryAddWithoutValidation(Header.Key, Header.Value) + End If + Else + '不知道为什么 Content-Type 标头会通过字符串验证然后加入不应该被加入的 HttpRequestMessage Headers + '这会导致 HttpRequestMessage 内部的标头验证失败,并抛出一个 Exception 导致网络请求失败 + If Header.Key.ContainsF("Content") Then GoTo FixContentHeader + If RequestMessage.Headers.Contains(Header.Key) Then Continue For + RequestMessage.Headers.Add(Header.Key, Header.Value) + End If + Next + End If + + If RequireHeaderSign Then + SecretHeadersSign(RequestUrl, requestMessage, UseBrowserUA) + End If + + Return RequestMessage + + End Function + + ' 发送请求 + Public Function SendRequest(Req As HttpRequestMessage, + Optional RequireDecode As Boolean = True, + Optional Encode As Encoding = Nothing, + Optional Timeout As Integer = 25000, + Optional RequireReturnRespObj As Boolean = False) + Using CTS As New CancellationTokenSource(Timeout) + Dim Client = ClientFactory.GetHttpClient() + Dim Resp As HttpResponseMessage = Client.SendAsync(Req, HttpCompletionOption.ResponseHeadersRead, CTS.Token).GetAwaiter().GetResult() + + If RequireReturnRespObj Then Return Resp + + Resp.EnsureSuccessStatusCode() + CTS.CancelAfter(Timeout) + Dim ResultStream = Resp.Content.ReadAsStreamAsync().GetAwaiter().GetResult() + Using DataStream As New MemoryStream() + ResultStream.CopyTo(DataStream) + Dim Bytes = DataStream.ToArray() + Resp.Dispose() + Req.Dispose() + + If RequireDecode Then + If Encode Is Nothing Then Encode = Encoding.UTF8 + Return Encode.GetString(Bytes) + Else + Return Bytes + End If + End Using + End Using + + + End Function + + ' 获取HttpMethod + Private Function GetHttpMethod(Method As String) As HttpMethod + Select Case Method?.Trim().ToUpperInvariant() + Case "HEAD" + Return HttpMethod.Head + Case "POST" + Return HttpMethod.Post + Case "PUT" + Return HttpMethod.Put + Case "DELETE" + Return HttpMethod.Delete + Case Else + Return HttpMethod.Get + End Select + End Function + + + End Class + + Public Class HttpWebProxyFactory + Implements IWebProxy + Public DisableHttpProxy As Boolean = True + Public ProxyAddress As String + Private CurrentProxy As WebProxy + Public RequiredReloadProxy As Boolean + Public RefreshSyncLock As New Object + Public SystemProxy As New WebProxy() + Public Sub New() + End Sub + + Public Property Credentials As ICredentials Implements IWebProxy.Credentials + Get + Return AuthCredentials + End Get + Set(value As ICredentials) + AuthCredentials = value + End Set + End Property + Private AuthCredentials As ICredentials + + Public Function GetProxy(destination As Uri) As Uri Implements IWebProxy.GetProxy + Return GetProxy(destination.AbsoluteUri).Address + End Function + Public Function GetProxy(destination As String) As WebProxy + If CurrentProxy IsNot Nothing AndAlso Not RequiredReloadProxy Then Return CurrentProxy + If RequiredReloadProxy OrElse CurrentProxy Is Nothing Then + If ProxyAddress IsNot Nothing OrElse + Not String.IsNullOrWhiteSpace(ProxyAddress) Then + SyncLock RefreshSyncLock + CurrentProxy = New WebProxy(ProxyAddress, True) + If AuthCredentials IsNot Nothing Then CurrentProxy.Credentials = AuthCredentials + Return CurrentProxy + End SyncLock + Else + SyncLock RefreshSyncLock + CurrentProxy = New WebProxy() + If AuthCredentials IsNot Nothing Then CurrentProxy.Credentials = AuthCredentials + Return CurrentProxy + End SyncLock + End If + End If + Return Nothing + End Function + Public Function IsBypassed(host As Uri) As Boolean Implements IWebProxy.IsBypassed + If DisableHttpProxy Then Return True + If CurrentProxy Is Nothing Then + SyncLock RefreshSyncLock + CurrentProxy = GetProxy(host.AbsoluteUri) + End SyncLock + End If + If CurrentProxy.IsBypassed(host) Then Return True + Return False + End Function + End Class + + Public Class HttpClientFactory + Private PreviousClient As HttpClient + Private CurrentClient As HttpClient + Private HttpClientLock As New Object + Public ClientHandler As New HttpClientHandler() With { + .UseProxy = True, + .Proxy = HttpProxy, + .AutomaticDecompression = DecompressionMethods.Deflate Or DecompressionMethods.GZip Or DecompressionMethods.None + } + Public Sub New() + HttpClientReload() + End Sub + Public Function GetHttpClient(Optional Handler As HttpClientHandler = Nothing) As HttpClient + SyncLock HttpClientLock + If Handler IsNot Nothing Then + ClientHandler = Handler + CurrentClient.Dispose() + CurrentClient = New HttpClient(Handler) + Return CurrentClient + End If + If CurrentClient IsNot Nothing Then Return CurrentClient + CurrentClient = New HttpClient(ClientHandler) With {.Timeout = Timeout.InfiniteTimeSpan} + Return CurrentClient + End SyncLock + End Function + Public Sub HttpClientReload() + RunInNewThread( + Sub() + While True + Thread.Sleep(TimeSpan.FromHours(6)) + If PreviousClient IsNot Nothing Then PreviousClient.Dispose() + PreviousClient = CurrentClient + CurrentClient = GetHttpClient() + End While + End Sub + ) + End Sub + + End Class ''' ''' 测试 Ping。失败则返回 -1。 ''' + ''' Public Function Ping(Ip As String, Optional Timeout As Integer = 10000, Optional MakeLog As Boolean = True) As Integer Dim PingResult As NetworkInformation.PingReply Try @@ -62,30 +269,22 @@ Retry: End Try End Function Public Function NetGetCodeByClient(Url As String, Encoding As Encoding, Timeout As Integer, Accept As String, Optional UseBrowserUserAgent As Boolean = False) As String - Url = SecretCdnSign(Url) Log("[Net] 获取客户端网络结果:" & Url & ",最大超时 " & Timeout) - Dim Request As CookieWebClient - Dim res As HttpWebResponse = Nothing - Dim HttpStream As Stream = Nothing Try - Request = New CookieWebClient With { - .Encoding = Encoding, - .Timeout = Timeout + Dim Headers As New Dictionary(Of String, String) From { + {"Accept", Accept} } - Request.Headers("Accept") = Accept - Request.Headers("Accept-Language") = "en-US,en;q=0.5" - Request.Headers("X-Requested-With") = "XMLHttpRequest" - SecretHeadersSign(Url, Request, UseBrowserUserAgent) - Return Request.DownloadString(Url) + Using Req As HttpRequestMessage = Request.GetRequestMessage(Url, Headers:=Headers, RequireHeaderSign:=True, RequireCdnSign:=True, UseBrowserUA:=UseBrowserUserAgent) + Req.Headers.AcceptLanguage.Add(New StringWithQualityHeaderValue("en-US,en;q=0.5")) + Req.Headers.TryAddWithoutValidation("X-Request-With","XMLHttpRequest") + Dim Resp As String = Request.SendRequest(Req, Encode:=Encoding, Timeout:=Timeout) + + Return Resp + End Using + Catch ex As TaskCanceledException + Throw New TaskCanceledException("连接服务器超时(" & Url & ")", ex) Catch ex As Exception - If ex.GetType.Equals(GetType(WebException)) AndAlso CType(ex, WebException).Status = WebExceptionStatus.Timeout Then - Throw New TimeoutException("连接服务器超时(" & Url & ")", ex) - Else - Throw New WebException("获取结果失败," & ex.Message & "(" & Url & ")", ex) - End If - Finally - If Not IsNothing(HttpStream) Then HttpStream.Dispose() - If Not IsNothing(res) Then res.Dispose() + Throw New WebException("获取结果失败," & ex.Message & "(" & Url & ")", ex) End Try End Function @@ -183,35 +382,23 @@ RequestFinished: End Function Public Function NetGetCodeByRequestOnce(Url As String, Optional Encode As Encoding = Nothing, Optional Timeout As Integer = 30000, Optional IsJson As Boolean = False, Optional Accept As String = "", Optional UseBrowserUserAgent As Boolean = False) If RunInUi() AndAlso Not Url.Contains("//127.") Then Throw New Exception("在 UI 线程执行了网络请求") - Url = SecretCdnSign(Url) Log($"[Net] 获取网络结果:{Url},超时 {Timeout}ms{If(IsJson, ",要求 json", "")}") - Dim Request As HttpWebRequest = WebRequest.Create(Url) - Dim Result As New List(Of Byte) Try - If Url.StartsWithF("https", True) Then Request.ProtocolVersion = HttpVersion.Version11 - Request.Timeout = Timeout - Request.Accept = Accept - SecretHeadersSign(Url, Request, UseBrowserUserAgent) - Using res As HttpWebResponse = Request.GetResponse() - Using HttpStream As Stream = res.GetResponseStream() - HttpStream.ReadTimeout = Timeout - Dim HttpData As Byte() = New Byte(16384) {} - Using Reader As New StreamReader(HttpStream, If(Encode, Encoding.UTF8)) - Dim ResultString As String = Reader.ReadToEnd - Return If(IsJson, GetJson(ResultString), ResultString) - End Using - End Using + Dim Headers As New Dictionary(Of String, String) From { + {"Accept", If(Accept = "", "*/*", Accept)} + } + Using Req As HttpRequestMessage = Request.GetRequestMessage(Url, Headers:=Headers, UseBrowserUA:=UseBrowserUserAgent, RequireHeaderSign:=True, RequireCdnSign:=True) + Dim Resp As String = Request.SendRequest(Req, Encode:=Encode, Timeout:=Timeout) + Return If(IsJson, GetJson(Resp), Resp) End Using + Catch ex As ThreadInterruptedException Throw - Catch ex As Exception - If TypeOf ex Is WebException AndAlso CType(ex, WebException).Status = WebExceptionStatus.Timeout Then - Throw New TimeoutException($"获取结果失败({CType(ex, WebException).Status},{ex.Message},{Url})", ex) - Else - Throw New WebException($"获取结果失败({If(TypeOf ex Is WebException, CType(ex, WebException).Status & ",", "")}{ex.Message},{Url})", ex) - End If - Finally - Request.Abort() + + Catch ex As TaskCanceledException + Throw New TimeoutException($"获取结果失败({ex.Message},{Url})", ex) + Catch ex As HttpRequestException + Throw New WebException($"获取结果失败({ex.Message},{Url})", ex) End Try End Function @@ -265,7 +452,7 @@ RequestFinished: '下载 Using Client As New WebClient Try - SecretHeadersSign(Url, Client, UseBrowserUserAgent) + 'SecretHeadersSign(Url, Client, UseBrowserUserAgent) Client.DownloadFile(Url, LocalFile) Catch ex As Exception File.Delete(LocalFile) @@ -314,7 +501,7 @@ RequestFinished: ''' 请求的内容。 ''' 请求的套接字类型。 ''' 当返回 40x 时不重试。 - Public Function NetRequestRetry(Url As String, Method As String, Data As Object, ContentType As String, Optional DontRetryOnRefused As Boolean = True, Optional Headers As Dictionary(Of String, String) = Nothing) As String + Public Function NetRequestRetry(Url As String, Method As String, Data As Object, ContentType As String, Optional DontRetryOnRefused As Boolean = True, Optional Headers As Dictionary(Of String, String) = Nothing, Optional RequireReturnRespObj As Boolean = False) As String Dim RetryCount As Integer = 0 Dim RetryException As Exception = Nothing Dim StartTime As Long = GetTimeTick() @@ -322,15 +509,15 @@ RequestFinished: Retry: Select Case RetryCount Case 0 '正常尝试 - Return NetRequestOnce(Url, Method, Data, ContentType, 15000, Headers) + Return NetRequestOnce(Url, Method, Data, ContentType, 15000, Headers, RequiredReturnRespObj:=RequireReturnRespObj) Case 1 '慢速重试 Thread.Sleep(500) - Return NetRequestOnce(Url, Method, Data, ContentType, 25000, Headers) + Return NetRequestOnce(Url, Method, Data, ContentType, 25000, Headers, RequiredReturnRespObj:=RequireReturnRespObj) Case Else '快速重试 If GetTimeTick() - StartTime > 5500 Then '若前两次加载耗费 5 秒以上,才进行重试 Thread.Sleep(500) - Return NetRequestOnce(Url, Method, Data, ContentType, 4000, Headers) + Return NetRequestOnce(Url, Method, Data, ContentType, 4000, Headers,RequiredReturnRespObj:=RequireReturnRespObj) Else Throw RetryException End If @@ -397,79 +584,35 @@ RequestFinished: ''' ''' 发送一次网络请求并获取返回内容。 ''' - Public Function NetRequestOnce(Url As String, Method As String, Data As Object, ContentType As String, Optional Timeout As Integer = 25000, Optional Headers As Dictionary(Of String, String) = Nothing, Optional MakeLog As Boolean = True, Optional UseBrowserUserAgent As Boolean = False) As String + Public Function NetRequestOnce(Url As String, Method As String, Data As Object, ContentType As String, Optional Timeout As Integer = 25000, Optional Headers As Dictionary(Of String, String) = Nothing, Optional MakeLog As Boolean = True, Optional UseBrowserUserAgent As Boolean = False, Optional RequiredReturnRespObj As Boolean = False) As Object If RunInUi() AndAlso Not Url.Contains("//127.") Then Throw New Exception("在 UI 线程执行了网络请求") - Url = SecretCdnSign(Url) If MakeLog Then Log("[Net] 发起网络请求(" & Method & "," & Url & "),最大超时 " & Timeout) - Dim DataStream As Stream = Nothing - Dim Resp As WebResponse = Nothing - Dim Req As HttpWebRequest Try - Req = WebRequest.Create(Url) - Req.Method = Method - Dim SendData As Byte() - If TypeOf Data Is Byte() Then - SendData = Data - Else - SendData = New UTF8Encoding(False).GetBytes(Data.ToString) - End If - If Headers IsNot Nothing Then - For Each Pair In Headers - Req.Headers.Add(Pair.Key, Pair.Value) - Next - End If - Req.ContentType = ContentType - Req.Timeout = Timeout - SecretHeadersSign(Url, Req, UseBrowserUserAgent) - If Url.StartsWithF("https", True) Then Req.ProtocolVersion = HttpVersion.Version11 - If Method = "POST" OrElse Method = "PUT" Then - Req.ContentLength = SendData.Length - DataStream = Req.GetRequestStream() - DataStream.WriteTimeout = Timeout - DataStream.ReadTimeout = Timeout - DataStream.Write(SendData, 0, SendData.Length) - DataStream.Close() - End If - Resp = Req.GetResponse() - DataStream = Resp.GetResponseStream() - DataStream.WriteTimeout = Timeout - DataStream.ReadTimeout = Timeout - Using Reader As New StreamReader(DataStream) - Return Reader.ReadToEnd() - End Using - Catch ex As ThreadInterruptedException - Throw - Catch ex As WebException - If ex.Status = WebExceptionStatus.Timeout Then - ex = New WebException($"连接服务器超时,请检查你的网络环境是否良好({ex.Message},{Url})", ex) - Else - '获取请求失败的返回 - Dim Res As String = "" - Try - If ex.Response Is Nothing Then Exit Try - DataStream = ex.Response.GetResponseStream() - DataStream.WriteTimeout = Timeout - DataStream.ReadTimeout = Timeout - Using Reader As New StreamReader(DataStream) - Res = Reader.ReadToEnd() - End Using - Catch - End Try - If Res = "" Then - ex = New WebException($"网络请求失败({ex.Status},{ex.Message},{Url})", ex) + Dim ReqHeaders As New Dictionary(Of String, String) + If ContentType IsNot Nothing AndAlso Data IsNot Nothing Then + ' 可能指定了 Content-Type 但是 Headers 是 Nothing + If Headers IsNot Nothing Then + If Not Headers.ContainsKey("Content-Type") Then Headers.Add("Content-Type", ContentType) Else - ex = New ResponsedWebException($"服务器返回错误({ex.Status},{ex.Message},{Url}){vbCrLf}{Res}", Res, ex) + If Not ReqHeaders.ContainsKey("Content-Type") Then ReqHeaders.Add("Content-Type", ContentType) End If End If + Dim ReqData As Byte() = If(TypeOf Data Is String, Encoding.UTF8.GetBytes(Data), Data) + + + Dim Req As HttpRequestMessage = Request.GetRequestMessage(Url, Method, If(Headers, ReqHeaders), If(Data IsNot Nothing, ReqData, Nothing), UseBrowserUserAgent, True, True) + Return Request.SendRequest(Req, If(RequiredReturnRespObj, False, True), Timeout:=Timeout, RequireReturnRespObj:=RequiredReturnRespObj) + Catch ex As ThreadInterruptedException + Throw + Catch ex As TaskCanceledException + Return New WebException($"连接服务器超时,请检查你的网络环境是否良好({ex.Message},{Url})", ex) + Catch ex As HttpRequestException + If MakeLog Then Log(ex, "NetRequestOnce 失败", LogLevel.Developer) + Throw New WebException(ex.Message, ex) + Catch ex As Exception + ex = New WebException("网络请求失败(" & Url & ")", ex) If MakeLog Then Log(ex, "NetRequestOnce 失败", LogLevel.Developer) Throw ex - Catch ex As Exception - ex = New WebException("网络请求失败(" & Url & ")", ex) - If MakeLog Then Log(ex, "NetRequestOnce 失败", LogLevel.Developer) - Throw ex - Finally - If DataStream IsNot Nothing Then DataStream.Dispose() - If Resp IsNot Nothing Then Resp.Dispose() End Try End Function Public Class ResponsedWebException @@ -818,6 +961,7 @@ RequestFinished: Get Return IsUnknownSize OrElse FileSize < FilePieceLimit End Get + End Property ''' ''' 为不需要分割的小文件进行临时存储。 @@ -1055,8 +1199,9 @@ StartThread: ''' Private Sub Thread(Info As NetThread) If ModeDebug OrElse Info.DownloadStart = 0 Then Log("[Download] " & LocalName & " " & Info.Uuid & "#:开始,起始点 " & Info.DownloadStart & "," & Info.Source.Url) - Dim HttpRequest As HttpWebRequest + Dim HttpRequest As HttpRequestMessage Dim ResultStream As Stream = Nothing + Dim HttpResponse As HttpResponseMessage '部分下载源真的特别慢,并且只需要一个请求,例如 Ping 为 20s,如果增长太慢,就会造成类似 2.5s 5s 7.5s 10s 12.5s... 的极大延迟 '延迟过长会导致某些特别慢的链接迟迟不被掐死 Dim Timeout As Integer = Math.Min(Math.Max(ConnectAverage, 6000) * (1 + Info.Source.FailCount), 30000) @@ -1064,18 +1209,16 @@ StartThread: Try Dim HttpDataCount As Integer = 0 If SourcesOnce.Contains(Info.Source) AndAlso Not Info.Equals(Info.Source.Thread) Then GoTo SourceBreak - '请求头 - HttpRequest = WebRequest.Create(Info.Source.Url) - If Info.Source.Url.StartsWithF("https", True) Then HttpRequest.ProtocolVersion = HttpVersion.Version11 - 'HttpRequest.Proxy = Nothing 'new WebProxy(Ip, Port) - HttpRequest.Timeout = Timeout - HttpRequest.AddRange(Info.DownloadStart) - SecretHeadersSign(Info.Source.Url, HttpRequest, UseBrowserUserAgent) + HttpRequest = Request.GetRequestMessage(Info.Source.Url, + RequireHeaderSign:=True) + '可能出现结束减去起始等于或者小于 0 的情况,导致下载抛出 ArgumentOutOfRangeException + If Not Info.IsFirstThread Then HttpRequest.Headers.Range = New RangeHeaderValue(Info.DownloadStart, Nothing) + HttpResponse = Request.SendRequest(HttpRequest, False, Timeout:=Timeout, RequireReturnRespObj:=True) Dim ContentLength As Long = 0 - Using HttpResponse As HttpWebResponse = HttpRequest.GetResponse() + Using HttpResponse If State = NetState.Error Then GoTo SourceBreak '快速中断 - If ModeDebug AndAlso HttpResponse.ResponseUri.OriginalString <> Info.Source.Url Then - Log($"[Download] {LocalName} {Info.Uuid}#:重定向至 {HttpResponse.ResponseUri.OriginalString}") + If ModeDebug AndAlso HttpResponse.RequestMessage.RequestUri.ToString <> Info.Source.Url Then + Log($"[Download] {LocalName} {Info.Uuid}#:重定向至 {HttpResponse.RequestMessage.RequestUri.ToString}") End If ''从响应头获取文件名 'If Info.IsFirstThread Then @@ -1087,7 +1230,8 @@ StartThread: ' End If 'End If '文件大小校验 - ContentLength = HttpResponse.ContentLength + ContentLength = HttpResponse.Content.Headers.ContentLength.GetValueOrDefault(-1) + If ContentLength = -1 Then If FileSize > 1 Then If Info.DownloadStart = 0 Then @@ -1136,11 +1280,12 @@ NotSupportRange: SourcesOnce.Add(Info.Source) End If End SyncLock + Throw New WebException($"该下载源不支持分段下载:Range 起始于 {Info.DownloadStart},预期 ContentLength 为 {FileSize - Info.DownloadStart},返回 ContentLength 为 {ContentLength},总文件大小 {FileSize}") ElseIf Not FileSize - Info.DownloadStart = ContentLength Then Throw New WebException($"获取到的分段大小不一致:Range 起始于 {Info.DownloadStart},预期 ContentLength 为 {FileSize - Info.DownloadStart},返回 ContentLength 为 {ContentLength},总文件大小 {FileSize}") End If - 'Log($"[Download] {LocalName} {Info.Uuid}#:通过大小检查,文件大小 {FileSize},起始点 {Info.DownloadStart},ContentLength {ContentLength}") + Log($"[Download] {LocalName} {Info.Uuid}#:通过大小检查,文件大小 {FileSize},起始点 {Info.DownloadStart},ContentLength {ContentLength}") Info.State = NetState.Get SyncLock LockState If State < NetState.Get Then State = NetState.Get @@ -1154,8 +1299,7 @@ NotSupportRange: ResultStream = New FileStream(Info.Temp, FileMode.Create, FileAccess.Write, FileShare.Read) End If '开始下载 - Using HttpStream = HttpResponse.GetResponseStream() - HttpStream.ReadTimeout = Timeout + Using HttpStream = HttpResponse.Content.ReadAsStreamAsync().GetAwaiter().GetResult() If Setup.Get("SystemDebugDelay") Then Threading.Thread.Sleep(RandomInteger(50, 3000)) Dim HttpData As Byte() = New Byte(16384) {} HttpDataCount = HttpStream.Read(HttpData, 0, 16384) @@ -1345,7 +1489,7 @@ Wrong: If State < NetState.Merge Then State = NetState.Merge Else - Return + Exit Sub End If End SyncLock Dim RetryCount As Integer = 0 @@ -1355,7 +1499,8 @@ Retry: SyncLock LockChain '创建文件夹 If File.Exists(LocalPath) Then File.Delete(LocalPath) - Directory.CreateDirectory(GetPathFromFullPath(LocalPath)) + Dim Info As New FileInfo(LocalPath) + Info.Directory.Create() '合并文件 If IsNoSplit Then '仅有一个线程,从缓存中输出 @@ -1434,7 +1579,7 @@ Retry: ''' Private Sub Fail(Optional RaiseEx As Exception = Nothing) SyncLock LockState - If State >= NetState.Finish Then Return + If State >= NetState.Finish Then Exit Sub If RaiseEx IsNot Nothing Then Ex.Add(RaiseEx) '凉凉 State = NetState.Error @@ -1450,10 +1595,10 @@ Retry: Public Sub Abort(CausedByTask As LoaderDownload) '从特定任务中移除,如果它还属于其他任务,则继续下载 Tasks.Remove(CausedByTask) - If Tasks.Any Then Return + If Tasks.Any Then Exit Sub '确认中断 SyncLock LockState - If State >= NetState.Finish Then Return + If State >= NetState.Finish Then Exit Sub State = NetState.Error End SyncLock InterruptAndDelete() @@ -1473,7 +1618,7 @@ Retry: ''' Public Sub Finish(Optional PrintLog As Boolean = True) SyncLock LockState - If State >= NetState.Finish Then Return + If State >= NetState.Finish Then Exit Sub State = NetState.Finish End SyncLock SyncLock NetManager.LockRemain @@ -1600,7 +1745,7 @@ NextElement: '输入检测 If Not Files.Any() Then OnFinish() - Return + Exit Sub End If For Each File As NetFile In Files If File Is Nothing Then Throw New ArgumentException("存在空文件请求!") @@ -1657,14 +1802,13 @@ NextElement: End Sub Private Sub StartCopy(Files As List(Of NetFile), FolderList As List(Of String)) Try - If ModeDebug Then Log($"[Download] 检查线程分配文件数:{Files.Count},线程名:{Thread.CurrentThread.Name}") + If ModeDebug Then Log("[Download] 检查线程分配文件数:" & Files.Count & ",线程名:" & Thread.CurrentThread.Name) '试图从已存在的 Minecraft 文件夹中寻找目标文件 Dim ExistFiles As New List(Of KeyValuePair(Of NetFile, String)) '{NetFile, Target As String} For Each File As NetFile In Files Dim ExistFilePath As String = Nothing '判断是否有已存在的文件 - If File.Check IsNot Nothing AndAlso McFolderList IsNot Nothing AndAlso PathMcFolder IsNot Nothing AndAlso - File.Check.CanUseExistsFile AndAlso File.LocalPath.StartsWithF(PathMcFolder) Then + If File.Check IsNot Nothing AndAlso McFolderList IsNot Nothing AndAlso PathMcFolder IsNot Nothing AndAlso File.Check.CanUseExistsFile AndAlso File.LocalPath.StartsWithF(PathMcFolder) Then Dim Relative = File.LocalPath.Replace(PathMcFolder, "") For Each Folder In FolderList Dim Target = Folder & Relative @@ -1690,7 +1834,7 @@ NextElement: For Each FileToken In ExistFiles Dim File As NetFile = FileToken.Key SyncLock LockState - If File.State > NetState.WaitForCopy Then Return + If File.State > NetState.WaitForCopy Then Exit Sub End SyncLock Dim LocalPath As String = FileToken.Value Dim RetryCount As Integer = 0 @@ -1701,7 +1845,7 @@ Retry: File.Finish(False) Catch ex As Exception RetryCount += 1 - Log(ex, $"复制已存在的文件失败,重试第 {RetryCount} 次({LocalPath} -> {File.LocalPath})") + Log(ex, String.Format("复制已存在的文件失败,重试第 {2} 次({0} -> {1})", LocalPath, File.LocalPath, RetryCount)) If RetryCount < 3 Then Thread.Sleep(200) GoTo Retry @@ -1719,14 +1863,14 @@ Retry: '要求全部文件完成 SyncLock FileRemainLock FileRemain -= 1 - If FileRemain > 0 Then Return + If FileRemain > 0 Then Exit Sub End SyncLock OnFinish() End Sub Public Sub OnFinish() RaisePreviewFinish() SyncLock LockState - If State > LoadState.Loading Then Return + If State > LoadState.Loading Then Exit Sub State = LoadState.Finished End SyncLock End Sub @@ -1739,7 +1883,7 @@ Retry: End Sub Public Sub OnFail(ExList As List(Of Exception)) SyncLock LockState - If State > LoadState.Loading Then Return + If State > LoadState.Loading Then Exit Sub If ExList Is Nothing OrElse Not ExList.Any() Then ExList = New List(Of Exception) From {New Exception("未知错误!")} '寻找第一个不是 404 的下载源 Dim UsefulExs = ExList.Where(Function(e) Not e.Message.Contains("(404)")).ToList @@ -1768,7 +1912,7 @@ Retry: End Sub Public Overrides Sub Abort() SyncLock LockState - If State >= LoadState.Finished Then Return + If State >= LoadState.Finished Then Exit Sub State = LoadState.Aborted End SyncLock Log("[Download] " & Name & " 已取消!") @@ -1885,7 +2029,7 @@ Retry: ''' 启动监控线程,用于新增下载线程。 ''' Private Sub StartManager() - If IsManagerStarted Then Return + If IsManagerStarted Then Exit Sub IsManagerStarted = True Dim ThreadStarter = Sub(Id As Integer) '0 或 1 diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb b/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb index 14edb8a..00469b8 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb @@ -1424,6 +1424,7 @@ Retry: End If 'Authlib-Injector If McLoginLoader.Output.Type = "Auth" Then + If (McLaunchJavaSelected.VersionCode >= 6) Then DataList.Add("-Djavax.net.ssl.trustStoreType=WINDOWS-ROOT") Dim Server As String = If(McLoginLoader.Input.Type = McLoginType.Legacy, "http://hiperauth.tech/api/yggdrasil-hiper/", 'HiPer 登录 Setup.Get("VersionServerAuthServer", McVersionCurrent)) @@ -1493,6 +1494,7 @@ NextVersion: End If 'Authlib-Injector If McLoginLoader.Output.Type = "Auth" Then + If (McLaunchJavaSelected.VersionCode >= 6) Then DataList.Add("-Djavax.net.ssl.trustStoreType=WINDOWS-ROOT") Dim Server As String = If(McLoginLoader.Input.Type = McLoginType.Legacy, "http://hiperauth.tech/api/yggdrasil-hiper/", 'HiPer 登录 Setup.Get("VersionServerAuthServer", Version:=McVersionCurrent)) diff --git a/Plain Craft Launcher 2/Modules/ModSecret.vb b/Plain Craft Launcher 2/Modules/ModSecret.vb index 5353dfe..37abcf8 100644 --- a/Plain Craft Launcher 2/Modules/ModSecret.vb +++ b/Plain Craft Launcher 2/Modules/ModSecret.vb @@ -1,5 +1,6 @@ '由于包含加解密等安全信息,本文件中的部分代码已被删除 +Imports System.Net.Http Imports System.Security.Cryptography Friend Module ModSecret @@ -78,32 +79,19 @@ Friend Module ModSecret ''' ''' 设置 Headers 的 UA、Referer。 ''' - Friend Sub SecretHeadersSign(Url As String, ByRef Client As WebClient, Optional UseBrowserUserAgent As Boolean = False) + Friend Sub SecretHeadersSign(Url As String, ByRef Req As HttpRequestMessage, Optional UseBrowserUserAgent As Boolean = False) If Url.Contains("baidupcs.com") OrElse Url.Contains("baidu.com") Then - Client.Headers("User-Agent") = "LogStatistic" '#4951 + Req.Headers.Add("User-Agent", "LogStatistic") '#4951 ElseIf UseBrowserUserAgent Then - Client.Headers("User-Agent") = "PCL2/" & VersionStandardCode & " Mozilla/5.0 AppleWebKit/537.36 Chrome/63.0.3239.132 Safari/537.36" + Req.Headers.Add("User-Agent", $"PCL2/{VersionStandardCode} Mozilla/5.0 AppleWebKit/537.36 Chrome/63.0.3239.132 Safari/537.36") Else - Client.Headers("User-Agent") = "PCL2/" & VersionStandardCode + Req.Headers.Add("User-Agent", $"PCL2/{VersionStandardCode}") End If - Client.Headers("Referer") = "http://" & VersionCode & ".open.pcl2.server/" - If Url.Contains("api.curseforge.com") Then Client.Headers("x-api-key") = CurseForgeAPIKey - End Sub - ''' - ''' 设置 Headers 的 UA、Referer。 - ''' - Friend Sub SecretHeadersSign(Url As String, ByRef Request As HttpWebRequest, Optional UseBrowserUserAgent As Boolean = False) - If Url.Contains("baidupcs.com") OrElse Url.Contains("baidu.com") Then - Request.UserAgent = "LogStatistic" '#4951 - ElseIf UseBrowserUserAgent Then - Request.UserAgent = "PCL2/" & VersionStandardCode & " Mozilla/5.0 AppleWebKit/537.36 Chrome/63.0.3239.132 Safari/537.36" - Else - Request.UserAgent = "PCL2/" & VersionStandardCode - End If - Request.Referer = "http://" & VersionCode & ".open.pcl2.server/" - If Url.Contains("api.curseforge.com") Then Request.Headers("x-api-key") = CurseForgeAPIKey + Req.Headers.Add("Referer", $"http://{VersionCode}.open.pcl2.server/") + If Url.Contains("api.curseforge.com") Then Req.Headers.Add("x-api-key", CurseForgeAPIKey) End Sub + #End Region #Region "字符串加解密" diff --git a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageComp.xaml.vb b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageComp.xaml.vb index 3ad75d4..18be6d0 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageComp.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageComp.xaml.vb @@ -68,6 +68,10 @@ Public Class PageComp ''' 在切换到页面时,应自动将筛选项设置为与该目标 MC 版本和加载器相同。 ''' Public Shared TargetVersion As McVersion = Nothing + ''' + ''' 在切换到该页面时自动设置的搜索框内的内容。 + ''' + Public Shared TargetName As String = Nothing '在点击 MyCompItem 时会获取 Loader 的输入,以使资源详情页面可以应用相同的筛选项 Public Loader As New LoaderTask(Of CompProjectRequest, Integer)("社区资源获取:XXX", AddressOf CompProjectsGet, AddressOf LoaderInput) With {.ReloadTimeout = 60 * 1000} @@ -94,6 +98,11 @@ Public Class PageComp ComboSearchLoader.SelectedItem = GetTargetItemByName("NeoForge") End If TargetVersion = Nothing + If TargetName IsNot Nothing Then + '设置搜索框内容 + TextSearchName.Text = TargetName + TargetName = Nothing + End If '如果已经完成请求,则重新开始 If IsLoaderInited Then StartNewSearch() ScrollToHome() diff --git a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadCompDetail.xaml.vb b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadCompDetail.xaml.vb index 60a8ad3..ea5c4c0 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadCompDetail.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadCompDetail.xaml.vb @@ -75,6 +75,8 @@ Dim Results As List(Of CompFile) = CompFileLoader.Output If PageType = CompType.Any Then Results = Results.Where(Function(r) r.Type <> CompType.Plugin).ToList + ElseIf PageType = CompType.Shader OrElse PageType = CompType.ResourcePack Then + Results = Results.ToList Else Results = Results.Where(Function(r) r.Type = PageType).ToList End If diff --git a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml.vb b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml.vb index 647675f..a4cb54d 100644 --- a/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml.vb @@ -1,4 +1,6 @@ -Public Class PageSetupUI +Imports NAudio.Wave + +Public Class PageSetupUI Public Shadows IsLoaded As Boolean = False @@ -274,7 +276,7 @@ '顶部栏 Private Sub BtnLogoChange_Click(sender As Object, e As EventArgs) Handles BtnLogoChange.Click - Dim FileName As String = SelectFile("常用图片文件(*.png;*.jpg;*.gif;*.webp)|*.png;*.jpg;*.gif;*.webp", "选择图片") + Dim FileName As String = SelectFile("常用图片文件(*.png;*.jpg;*.gif;*.webp;*.jpeg)|*.png;*.jpg;*.gif;*.webp;*.jpeg", "选择图片") If FileName = "" Then Return Try '拷贝文件 @@ -357,7 +359,15 @@ Refresh: PanMusicVolume.Visibility = Visibility.Visible PanMusicDetail.Visibility = Visibility.Visible BtnMusicClear.Visibility = Visibility.Visible - CardMusic.Title = "背景音乐(" & EnumerateFiles(Path & "PCL\Musics\").Count & " 首)" + Dim validMusicCount As Integer = 0 + For Each File In EnumerateFiles(Path & "PCL\Musics\") + Try + If {".ini", ".jpg", ".txt", ".cfg", ".lrc", ".db", ".png"}.Contains(File.Extension.ToLower) Then Continue For + validMusicCount += 1 + Catch ex As Exception + End Try + Next + CardMusic.Title = "背景音乐(" & validMusicCount & " 首)" Else PanMusicVolume.Visibility = Visibility.Collapsed PanMusicDetail.Visibility = Visibility.Collapsed diff --git a/Plain Craft Launcher 2/Pages/PageVersion/PageVersionMod.xaml.vb b/Plain Craft Launcher 2/Pages/PageVersion/PageVersionMod.xaml.vb index 184e924..3fe78d0 100644 --- a/Plain Craft Launcher 2/Pages/PageVersion/PageVersionMod.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageVersion/PageVersionMod.xaml.vb @@ -382,6 +382,7 @@ Install: ''' Private Sub BtnManageDownload_Click(sender As Object, e As MouseButtonEventArgs) Handles BtnManageDownload.Click, BtnHintDownload.Click PageComp.TargetVersion = PageVersionLeft.Version '将当前版本设置为筛选器 + PageComp.TargetName = SearchBox.Text '将当前搜索框内容设置为筛选器 FrmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadMod) End Sub diff --git a/Plain Craft Launcher 2/Pages/PageVersion/PageVersionOverall.xaml.vb b/Plain Craft Launcher 2/Pages/PageVersion/PageVersionOverall.xaml.vb index 91a8e0c..c85c321 100644 --- a/Plain Craft Launcher 2/Pages/PageVersion/PageVersionOverall.xaml.vb +++ b/Plain Craft Launcher 2/Pages/PageVersion/PageVersionOverall.xaml.vb @@ -185,7 +185,7 @@ '选择 自定义 时修改图片 Try If ComboDisplayLogo.SelectedItem Is ItemDisplayLogoCustom Then - Dim FileName As String = SelectFile("常用图片文件(*.png;*.jpg;*.gif)|*.png;*.jpg;*.gif", "选择图片") + Dim FileName As String = SelectFile("常用图片文件(*.png;*.jpg;*.gif;*.jpeg)|*.png;*.jpg;*.gif;*.jpeg", "选择图片") If FileName = "" Then Reload() '还原选项 Return @@ -331,7 +331,7 @@ DeleteDirectory(PageVersionLeft.Version.Path) Hint("版本 " & PageVersionLeft.Version.Name & " 已永久删除!", HintType.Finish) Else - FileIO.FileSystem.DeleteDirectory(PageVersionLeft.Version.Path, FileIO.UIOption.OnlyErrorDialogs, FileIO.RecycleOption.SendToRecycleBin) + FileIO.FileSystem.DeleteDirectory(PageVersionLeft.Version.Path, FileIO.UIOption.AllDialogs, FileIO.RecycleOption.SendToRecycleBin) Hint("版本 " & PageVersionLeft.Version.Name & " 已删除到回收站!", HintType.Finish) End If Case 2