Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 41 additions & 39 deletions PCL.Core/IO/Net/Http/HttpProxyManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,53 +45,56 @@ private enum ProxyProtocol
Socks
}

private record ProxyItem
{
public ProxyProtocol Protocol;
public required string Address;
}
private sealed record ProxyItem(ProxyProtocol Protocol, string Address);

private static ProxyItem[] _GetProxyFromString(string? proxyString)
{
if (proxyString.IsNullOrWhiteSpace()) return [];

var ret = new List<ProxyItem>();

// 形式:http=192.168.1.100:8080;socks=192.168.1.100:1080
// 含 '=' 的形式:http=192.168.1.100:8080;socks=192.168.1.100:1080;ftp=127.0.0.1:124
// 也可以是 http=http://127.0.0.1:10808
if (proxyString.Contains('='))
{
foreach (var segment in proxyString.Split(';', StringSplitOptions.RemoveEmptyEntries))
var items = new List<ProxyItem>();
foreach (var segment in proxyString.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
var eqIndex = segment.IndexOf('=');
if (eqIndex <= 0 || eqIndex >= segment.Length - 1)
continue;
if (_ParseKeyValueSegment(segment) is { } item)
items.Add(item);
}
return [.. items];
}

var protocolStr = segment[..eqIndex].Trim();
var address = segment[(eqIndex + 1)..].Trim();
// 形式:http://127.0.0.1:1145/ 或者单纯 127.0.0.1:1145
var proxy = _ParseAddress(proxyString.Trim());
return proxy is null ? [] : [proxy];
}

if (string.IsNullOrWhiteSpace(address))
continue;
/// <summary>解析 <c>protocol=address</c> 形式的段;格式非法时返回 null 以便忽略该段</summary>
private static ProxyItem? _ParseKeyValueSegment(string segment)
{
var eqIndex = segment.IndexOf('=');
// 缺少 '='、协议名或地址的段视为非法,直接忽略
if (eqIndex <= 0 || eqIndex >= segment.Length - 1) return null;

ret.Add(new ProxyItem { Protocol = _ParseProtocol(protocolStr), Address = address });
}
return _ParseAddress(
segment[(eqIndex + 1)..].Trim(),
_ParseProtocol(segment[..eqIndex].Trim()));
}

return ret.Count > 0 ? [.. ret] : [];
}
/// <summary>解析单个代理地址:支持带 scheme 的地址(如 http://127.0.0.1:10808)与纯 host:port 地址(如 127.0.0.1:1145)</summary>
private static ProxyItem? _ParseAddress(string address, ProxyProtocol? protocol = null)
{
if (address.IsNullOrWhiteSpace()) return null;

// 形式:http://127.0.0.1:1145/ 或者单纯 127.0.0.1:1145
if (Uri.TryCreate(proxyString, new UriCreationOptions(), out var proxyAddr))
{
var address = proxyAddr.Port > 0
? $"{proxyAddr.Host}:{proxyAddr.Port}"
: proxyAddr.Host;
ret.Add(new ProxyItem { Protocol = _ParseProtocol(proxyAddr.Scheme), Address = address });
}
else
// 能解析出主机名的地址,规范化为 host:port
if (Uri.TryCreate(address, new UriCreationOptions(), out var uri) && !uri.Host.IsNullOrEmpty())
{
ret.Add(new ProxyItem { Protocol = ProxyProtocol.Http, Address = proxyString.Trim() });
var hostPort = uri.Port > 0 ? $"{uri.Host}:{uri.Port}" : uri.Host;
return new ProxyItem(protocol ?? _ParseProtocol(uri.Scheme), hostPort);
Comment on lines +90 to +93

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: 当 URI 没有主机名时(例如文件/系统 URI),这种行为对代理而言可能并不理想。

Uri.TryCreate 成功但 Host 为空时,当前代码会退回到将 address 视为原始代理端点。对于代理配置来说,像 file:///c:/path 这样的值在实际效果上是无效的,但仍会被通过,并在后续用于构建 HTTP URI。为了避免这种错误配置传播到 HTTP 栈,作为代理而言,对主机名为空的 URI 进行拒绝(例如返回 null)会更安全。

建议实现如下:

        // 能解析出主机名的地址,规范化为 host:port

    private sealed record ProxyItem(ProxyProtocol Protocol, string Address);

    // 返回 ProxyItem?,以便在 URI 有效但没有主机名时能够返回 null
    private static ProxyItem? _ParseProxyItem(string address, ProxyProtocol? protocol = null)
    {
        // 能解析出主机名的地址,规范化为 host:port
        if (Uri.TryCreate(address, new UriCreationOptions(), out var uri))
        {
            if (uri.Host.IsNullOrEmpty())
            {
                // 对于没有主机名的 URI(例如 file://),认为是无效的代理配置
                return null;
            }

            var hostPort = uri.Port > 0 ? $"{uri.Host}:{uri.Port}" : uri.Host;
            return new ProxyItem(protocol ?? _ParseProtocol(uri.Scheme), hostPort);
        }

        // 非 URI 格式的地址,按原始代理端点处理
        return new ProxyItem(protocol ?? ProxyProtocol.Http, address.Trim());
    }

    private static ProxyItem[] _GetProxyFromString(string? proxyString)
    {
        if (proxyString.IsNullOrWhiteSpace()) return [];

要完整应用这一改动,需要对文件的其他部分进行更新:

  1. 用对 _ParseProxyItem 的调用替换当前内联的 Uri.TryCreate + Host 处理逻辑,并且:
    • 跳过 _ParseProxyItem 返回 null 的条目(即不要将它们包含在 ProxyItem[] 结果中)。
  2. 更新目前在 Uri.TryCreate 分支中直接返回 ProxyItem 的方法,使其改用 _ParseProxyItem,或者如果需要向上传递 null,则将其签名更新为返回 ProxyItem?
  3. 确保在未显式指定协议时,能像新的辅助方法所示那样一致地使用 _ParseProtocolProxyProtocol.Http
Original comment in English

suggestion: The behavior when the URI has no host (e.g., file/system URIs) may not be ideal for proxies.

When Uri.TryCreate succeeds but Host is empty, the code currently falls back to treating address as a raw proxy endpoint. For proxy settings, values like file:///c:/path are effectively invalid but would still pass through and later be used to build an HTTP URI. It would be safer to reject URIs with an empty host for proxy purposes (e.g., return null) so such misconfigurations don’t propagate into the HTTP stack.

Suggested implementation:

        // 能解析出主机名的地址,规范化为 host:port

    private sealed record ProxyItem(ProxyProtocol Protocol, string Address);

    // 返回 ProxyItem?,以便在 URI 有效但没有主机名时能够返回 null
    private static ProxyItem? _ParseProxyItem(string address, ProxyProtocol? protocol = null)
    {
        // 能解析出主机名的地址,规范化为 host:port
        if (Uri.TryCreate(address, new UriCreationOptions(), out var uri))
        {
            if (uri.Host.IsNullOrEmpty())
            {
                // 对于没有主机名的 URI(例如 file://),认为是无效的代理配置
                return null;
            }

            var hostPort = uri.Port > 0 ? $"{uri.Host}:{uri.Port}" : uri.Host;
            return new ProxyItem(protocol ?? _ParseProtocol(uri.Scheme), hostPort);
        }

        // 非 URI 格式的地址,按原始代理端点处理
        return new ProxyItem(protocol ?? ProxyProtocol.Http, address.Trim());
    }

    private static ProxyItem[] _GetProxyFromString(string? proxyString)
    {
        if (proxyString.IsNullOrWhiteSpace()) return [];

To fully apply this change, the rest of the file needs to be updated to:

  1. Replace the existing inline Uri.TryCreate + Host handling logic with calls to _ParseProxyItem, and:
    • Skip entries where _ParseProxyItem returns null (i.e., do not include them in the ProxyItem[] result).
  2. Update any current method that was returning ProxyItem directly from the Uri.TryCreate branch to use _ParseProxyItem instead, or update its signature to return ProxyItem? if it needs to propagate the null.
  3. Ensure that _ParseProtocol and ProxyProtocol.Http are used consistently when the protocol is not explicitly specified, as shown in the new helper method.

}

return [.. ret];
// 纯 host:port 地址(无法作为 URI 解析),按指定协议(默认 Http)原样使用
return new ProxyItem(protocol ?? ProxyProtocol.Http, address);
}

private static ProxyProtocol _ParseProtocol(string scheme)
Expand All @@ -117,14 +120,13 @@ public void RefreshSystemProxy()
// parse
var proxies = _GetProxyFromString(systemProxyString);

// filter
if (proxies.Length == 0 || !proxies.Any(static x => x.Protocol.Equals(ProxyProtocol.Http))) isSystemProxyEnabled = 0;
var selectedProxy = proxies.FirstOrDefault(static x => x.Protocol.Equals(ProxyProtocol.Http));

// apply
_systemWebProxy.Address = (isSystemProxyEnabled == 0 || selectedProxy!.Address.IsNullOrEmpty())
? null
: new Uri($"http://{selectedProxy.Address}");
// 仅当系统代理已启用且存在有效的 HTTP 代理时才应用
var selectedProxy = proxies.FirstOrDefault(static x => x.Protocol == ProxyProtocol.Http);
_systemWebProxy.Address = selectedProxy is null
|| selectedProxy.Address.IsNullOrEmpty()
|| isSystemProxyEnabled == 0
? null
: new Uri($"http://{selectedProxy.Address}");

LogWrapper.Info("Proxy",
$"已从操作系统更新代理设置,系统代理状态:{isSystemProxyEnabled}|{systemProxyString}");
Expand Down
Loading