HttpSysManager 揭秘 http.sys





5.00/5 (36投票s)
配置本地计算机上的 HTTP 流量。一个不错的 netsh http 替代方案。
HttpSysManager 揭秘 http.sys
聚焦 HTTP.SYS
在 Windows Server 2003 之前,HTTP 服务器创建者的生活很简单:只需打开一个套接字连接到一个端点(IP:端口),监听传入流量并进行解析。
一切都很好,除了出于防火墙的原因,所有应用程序都想使用端口 80(HTTP)和 443(HTTPS)。
正如您可能知道的,使用套接字模型,在任何给定时间只有一个应用程序可以监听一个端点(IP:端口)。
每个人都声称拥有端口 80 和 443,互联网上爆发了可怕的战争,以确定谁将是主宰者。
然后 Windows Server 2003 服务器发布了,一个新内核驱动程序诞生了,微软给它起名 http.sys
。
这个驱动程序旨在监听 HTTP 流量并根据 URL 分派给进程:现在多个进程将能够监听同一端口上的 HTTP 流量。
这是一个简单的证明

围绕这个新生的驱动程序创建了一个公开的 Windows API,称为 HTTP 服务器 API,以及一组利用它的工具,如 httpcfg(旧方法)
或 netsh http(新方法)
。
IIS 依赖于 http.sys
,我们心爱的类 HttpListener
实际上只是 HTTP 服务器 API 的一个简单包装器,WCF Http(s)TransportBinding
也是如此。

你不相信我?你认为这是阴谋?自己去查证。
翻译:无法获取有关所有者的任何信息。(因为内核驱动程序 http.sys
不是一个进程)

这对你,开发者同仁,有什么影响?影响就是出现奇怪的生产部署崩溃,就像这样。

为了 Google 索引和所有会向 Google 求助的绝望开发者,我将此消息以纯英文复制给您
未处理的异常:System.ServiceModel.AddressAccessDeniedException:HTTP 无法注册 URL http://+:80/634800377949733185/。您的进程无权访问此命名空间(有关详细信息,请参阅 http://go.microsoft.com/fwlink/?LinkId=70353)。---> System.Net.HttpListenerException:拒绝访问
我首先会回答的问题是,当您看到此消息后提出的那个问题。这是开发者中一个非常著名的问题,也是无休止辩论的源头:**为什么它在我机器上就能正常工作?**
最简单的答案也很常见:**它能工作是因为您是您计算机的本地管理员组的成员。**
长篇大论的答案是,它不能工作是因为生产环境中的用户不是管理员**,并且生产环境中的用户对 URL http://+:80/ 没有权限。**
路由、注册、URL ACL 和委派
当 IIS 或任何使用 HTTP 服务器 API 的应用程序监听某些 HTTP 请求路径时,它们需要向 http.sys
**注册**一个 URL 前缀,我们将这个过程称为**注册**。
当 http.sys
接收到一个传入请求时,它需要将消息传递给正确的**已注册**应用程序,我们称之为**路由**。
正如您所见,当应用程序注册时,如果您是本地管理员,一切都会正常工作。如果您不是,将检查 URL ACL
。
URL ACL 只是一个与 ACL 关联的 URL 前缀。
.
您可以使用命令行 netsh http show|add|delete urlacl
来操作它,但我不太推荐,因为我围绕 HTTP 服务器 API 创建了一个-几乎-漂亮的 UI:**HttpSysManager**。

使用一些熟悉的 UI 来设置 ACL...(Tout le monde = 所有人)

你们中的一些人可能会认出这是 WCF 中用于双向 HTTP 场景的 callback URL 的 ACL 对话框...这个 ACL 是在 WCF 安装过程中设置的,这样每个用户都可以注册 callback 地址。
根据我当前的 ACL,注册到 **http://+:80/ReportServer/test/blabla** 将使用 ACL **http://+:80/ReportServer**。
另一方面,注册到 **http://+:80/test/blabla** 将使用 **http://+:80/** 的 ACL。
始终记住:**最长匹配规则**适用。
您可以看到 ACL 窗口中有两个不同的权限:**注册**和**委派**(特殊授权不适用),我们已经介绍了**注册**。
**委派**意味着您授予组或用户添加、修改和删除子请求路径的 URL ACL 的权限。
例如,这是 **http://+:80/** 和 Guest 用户 的权限

如果我在 Guest 用户下运行 **HttpSysManager** 并尝试将用户 NICO 添加到 **http://+:80/ReportServer** 的 ACL,结果如下

用通俗的话说:它不喜欢这样做,而且我可以说是最好的情况……我的应用程序不喜欢并且崩溃了! " src="https://codeproject.org.cn/script/Forums/Images/smiley_smile.gif" />
回到管理员用户,并授予 Guest 用户对 **http://+:80/** 的委派权限

再次以 Guest 用户身份运行,现在您可以将 Nico 添加到 **http://+:80/ReportServer** 的 ACL。
HTTPS 的情况
http.sys
可以同时监听 HTTP 和 HTTPS 流量。并且它附带了一些针对 HTTPS 的非常好的功能。
与 HTTP 情况类似,IIS、WCF 和其他一切都依赖于 HTTP 服务器 API 进行 HTTPS。(除了那些只使用老式套接字的人)
http.sys 暴露的最明显的功能是将证书绑定到端点(IP:端口)的可能性。
证书应安装在 LocalMachine 证书位置,因为 http.sys 不在任何用户的上下文中运行。
然后您可以使用 netsh http add|delete|show sslcert certhash=
将端点绑定到服务器证书,但是 **HttpSysManager** 使此过程更轻松。
这是如何使用 **HttpSysManager** 导入或选择存储中的证书并将其绑定到端点。
您在此屏幕截图中可以看到 IIS 已在端口 444 上注册了一个 ssl 绑定,我正在添加一个新绑定到端口 443。

单击**选择证书**,然后从文件或计算机的存储中选择您的证书(带有私钥)。

这样就完成了,现在来自此端点的所有 HTTPS 流量将使用 HttpSysManager 的证书进行身份验证和加密。

关于**协商客户端证书**复选框的说明
一旦建立 SSL 会话,应用程序就可以确切地选择何时请求客户端证书以进行双向身份验证。
例如,您可以配置 IIS,使其仅在访问虚拟文件夹 Secure_Data 时才要求客户端证书...
此时,服务器和客户端会协商一个新的 SSL 会话(**重新**协商),然后客户端发送其证书。
一切正常,除了在某些情况下,您的某些客户端可能不支持**重新**协商 SSL 会话。
在这种情况下,您可以通过选中**协商客户端证书**来要求 http.sys 在建立 SSL 会话时始终要求客户端发送证书。
关于第二个选项的说明:**目录服务客户端证书映射**
如果选中此项,http.sys 将请求 AD 提供与客户端证书对应的访问令牌(Windows 标识),并将其传递给应用程序。(用于模拟或授权目的)
有关更多信息,请参阅 此链接。
直接看代码
该代码大量使用了 pInvoke 来与 HTTP 服务器 API 进行交互。对于 Url Acl 部分,我很幸运地在 Google 上找到了一段代码片段,该片段包装了来自 Process Hacker V1 的所有 SecurityDescriptor
内容。
我最终引用了所有库,并修改了源代码以添加一些方法。
包装 HTTP 服务器 API 的高级类是 HttpApiManager
,以及 UrlAcl
和 SSLInfo
。

这部分没什么惊喜,这些对象的属性反映了您在 UI 中看到的内容。
请注意,UrlAcl.SecurityDescriptor
包含有关 ACL 的信息,SecurityDescriptor
本身就很复杂……我借用了 Process Hacker V1 创建的类。
HttpApiManager 仅依赖于大量的 pInvoke 包装函数来调用 Win API。

我省略了细节,这不是很吸引人,而且编码起来很麻烦。
有趣的部分是我如何再次利用 Windows UI 来达到我的目的,例如,对于 Url ACL,我重用了 Process Hacker V1 的类,特别是 SecurityEditor.EditSecurity
方法。
public static void EditSecurity(IWin32Window owner, ISecurable securable, string name, IEnumerable<AccessEntry> accessEntries);
private void Permissions_Click(object sender, RoutedEventArgs e)
{
var acl = (UrlAcl)((FrameworkElement)sender).DataContext;
SecurityEditor.EditSecurity(null, acl, acl.Prefix, GetAccessEntries());
}
ISecurable
接口是回调函数,用于实现 ACL 窗口用来获取当前 ACL 和设置新 ACL。这是 Process Hacker 的一个完全托管的实现,我将其应用于 UrlAcl
...
如果没有 Process Hacker V1,我将不得不实现这些。
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("965fc360-16ff-11d0-91cb-00aa00bbb723")]
public interface ISecurityInformation
{
HResult GetAccessRights(ref Guid ObjectType, SiObjectInfoFlags Flags, out IntPtr Access, out int Accesses, out int DefaultAccess);
HResult GetInheritTypes(out IntPtr InheritTypes, out int InheritTypesCount);
HResult GetObjectInformation(out SiObjectInfo ObjectInfo);
HResult GetSecurity(SecurityInformation RequestedInformation, out IntPtr SecurityDescriptor, bool Default);
HResult MapGeneric(ref Guid ObjectType, ref AceFlags AceFlags, ref int Mask);
HResult PropertySheetPageCallback(IntPtr hWnd, SiCallbackMessage Msg, SiPageType Page);
HResult SetSecurity(SecurityInformation SecurityInformation, IntPtr SecurityDescriptor);
}
我当然对我的编码技巧很有信心,这是可行的,但我不是受虐狂。
此接口中的所有类也都由 IntPtr 组成……它们不是纯粹的 C# 对象,而是纯粹的 C 对象。
这是我需要通过 Process Hacker 实现的。
public interface ISecurable
{
SecurityDescriptor GetSecurity(SecurityInformation securityInformation);
void SetSecurity(SecurityInformation securityInformation, SecurityDescriptor securityDescriptor);
}
而这一切都是纯粹的 .NET 对象,我的朋友!
这是 UrlAcl
中的实现。
SecurityDescriptor ISecurable.GetSecurity(ProcessHacker.Native.Api.SecurityInformation securityInformation)
{
return SecurityDescriptor;
}
public void SetSecurity(SecurityDescriptor securityDescriptor)
{
((ISecurable)this).SetSecurity(SecurityInformation.Dacl, securityDescriptor);
}
void ISecurable.SetSecurity(ProcessHacker.Native.Api.SecurityInformation securityInformation, SecurityDescriptor securityDescriptor)
{
if(securityDescriptor.ToString() == SecurityDescriptor.Empty.ToString())
return;
var manager = new HttpAPIManager();
manager.SetUrlAcl(Prefix, securityDescriptor);
Update(manager.GetAclInfo(Prefix)._Acl);
}
另一个有趣的部分是我如何重用了证书选择窗口。

这个也很简单,.NET 提供了一个完整的管理器包装器来处理证书结构:X509Certificate2
。
我创建了一个方法:Security.SelectCertificate()
,用于显示 LocalMachine/My 存储中的证书。
看看我是如何使用 X509Store
和 X509Certificate2
来获取非托管函数 Security.CryptUIDlgSelectCertificateW(intputPtr);
所使用的信息的。
我注释了有趣的部分。
public static X509Certificate2 SelectCertificate()
{
bool storeOpened = false;
var store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
IntPtr result = IntPtr.Zero;
IntPtr intputPtr = IntPtr.Zero;
var array = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(IntPtr)));
try
{
store.Open(OpenFlags.ReadOnly);
storeOpened = true;
Marshal.WriteIntPtr(array, store.StoreHandle); //array hold the list of Store to select from. I use X509Store.StoreHandle to get the handle that the unmanaged function need.
int size = Marshal.SizeOf(typeof(CRYPTUI_SELECTCERTIFICATE_STRUCTW));
CRYPTUI_SELECTCERTIFICATE_STRUCTW input = new CRYPTUI_SELECTCERTIFICATE_STRUCTW();
input.dwSize = (uint)size;
input.cPropSheetPages = 0;
input.dwDontUseColumn = 0;
input.dwFlags = 0;
input.hSelectedCertStore = IntPtr.Zero;
input.hwndParent = IntPtr.Zero;
input.pDisplayCallback = IntPtr.Zero;
input.pFilterCallback = IntPtr.Zero;
input.pvCallbackData = IntPtr.Zero;
input.rghDisplayStores = array;
input.cDisplayStores = 1;
input.rghStores = IntPtr.Zero;
input.cStores = 0;
input.rgPropSheetPages = IntPtr.Zero;
input.szDisplayString = null;
input.szTitle = null;
intputPtr = Marshal.AllocHGlobal(size);
Marshal.StructureToPtr(input, intputPtr, false);
result = Security.CryptUIDlgSelectCertificateW(intputPtr);
if(result == IntPtr.Zero)
return null;
return new X509Certificate2(result); //I create X509Certificate2 from the handle I got from Security.CryptUIDlgSelectCertificateW
}
finally
{
if(storeOpened)
store.Close();
if(intputPtr != IntPtr.Zero)
{
Marshal.DestroyStructure(intputPtr, typeof(CRYPTUI_SELECTCERTIFICATE_STRUCTW));
Marshal.FreeHGlobal(intputPtr);
}
if(array != IntPtr.Zero)
Marshal.FreeHGlobal(array);
if(result != IntPtr.Zero)
Security.CertFreeCertificateContext(result); //X509Certificate2 duplicate the handle internally
}
}
}
路线图
事实上,HTTP 服务器 API 还有更多的可能性,以下是我将要实现的一些下一步。
- 脚本(.bat)生成
CTL(证书信任列表)创建和绑定到 SSL 端点- HTTPS SNI(仅限 Windows 8)
- 支持 MY 以外的其他存储