@@ -58,39 +58,106 @@ internal Request(RequestContext requestContext)
5858
5959 PathBase = string . Empty ;
6060 Path = originalPath ;
61+ var prefix = requestContext . Server . Options . UrlPrefixes . GetPrefix ( ( int ) requestContext . UrlContext ) ;
6162
6263 // 'OPTIONS * HTTP/1.1'
6364 if ( KnownMethod == HttpApiTypes . HTTP_VERB . HttpVerbOPTIONS && string . Equals ( RawUrl , "*" , StringComparison . Ordinal ) )
6465 {
6566 PathBase = string . Empty ;
6667 Path = string . Empty ;
6768 }
68- else
69+ // Prefix may be null if the requested has been transfered to our queue
70+ else if ( prefix is not null )
6971 {
70- var prefix = requestContext . Server . Options . UrlPrefixes . GetPrefix ( ( int ) requestContext . UrlContext ) ;
71- // Prefix may be null if the requested has been transfered to our queue
72- if ( ! ( prefix is null ) )
72+ var pathBase = prefix . PathWithoutTrailingSlash ;
73+
74+ // url: /base/path, prefix: /base/, base: /base, path: /path
75+ // url: /, prefix: /, base: , path: /
76+ if ( originalPath . Equals ( pathBase , StringComparison . Ordinal ) )
7377 {
74- if ( originalPath . Length == prefix . PathWithoutTrailingSlash . Length )
75- {
76- // They matched exactly except for the trailing slash.
77- PathBase = originalPath ;
78- Path = string . Empty ;
79- }
80- else
81- {
82- // url: /base/path, prefix: /base/, base: /base, path: /path
83- // url: /, prefix: /, base: , path: /
84- PathBase = originalPath . Substring ( 0 , prefix . PathWithoutTrailingSlash . Length ) ; // Preserve the user input casing
85- Path = originalPath . Substring ( prefix . PathWithoutTrailingSlash . Length ) ;
86- }
78+ // Exact match, no need to preserve the casing
79+ PathBase = pathBase ;
80+ Path = string . Empty ;
8781 }
88- else if ( requestContext . Server . Options . UrlPrefixes . TryMatchLongestPrefix ( IsHttps , cookedUrl . GetHost ( ) ! , originalPath , out var pathBase , out var path ) )
82+ else if ( originalPath . Equals ( pathBase , StringComparison . OrdinalIgnoreCase ) )
8983 {
84+ // Preserve the user input casing
85+ PathBase = originalPath ;
86+ Path = string . Empty ;
87+ }
88+ else if ( originalPath . StartsWith ( prefix . Path , StringComparison . Ordinal ) )
89+ {
90+ // Exact match, no need to preserve the casing
9091 PathBase = pathBase ;
91- Path = path ;
92+ Path = originalPath [ pathBase . Length ..] ;
93+ }
94+ else if ( originalPath . StartsWith ( prefix . Path , StringComparison . OrdinalIgnoreCase ) )
95+ {
96+ // Preserve the user input casing
97+ PathBase = originalPath [ ..pathBase . Length ] ;
98+ Path = originalPath [ pathBase . Length ..] ;
99+ }
100+ else
101+ {
102+ // Http.Sys path base matching is based on the cooked url which applies some non-standard normalizations that we don't use
103+ // like collapsing duplicate slashes "//", converting '\' to '/', and un-escaping "%2F" to '/'. Find the right split and
104+ // ignore the normalizations.
105+ var originalOffset = 0 ;
106+ var baseOffset = 0 ;
107+ while ( originalOffset < originalPath . Length && baseOffset < pathBase . Length )
108+ {
109+ var baseValue = pathBase [ baseOffset ] ;
110+ var offsetValue = originalPath [ originalOffset ] ;
111+ if ( baseValue == offsetValue
112+ || char . ToUpperInvariant ( baseValue ) == char . ToUpperInvariant ( offsetValue ) )
113+ {
114+ // case-insensitive match, continue
115+ originalOffset ++ ;
116+ baseOffset ++ ;
117+ }
118+ else if ( baseValue == '/' && offsetValue == '\\ ' )
119+ {
120+ // Http.Sys considers these equivalent
121+ originalOffset ++ ;
122+ baseOffset ++ ;
123+ }
124+ else if ( baseValue == '/' && originalPath . AsSpan ( originalOffset ) . StartsWith ( "%2F" , StringComparison . OrdinalIgnoreCase ) )
125+ {
126+ // Http.Sys un-escapes this
127+ originalOffset += 3 ;
128+ baseOffset ++ ;
129+ }
130+ else if ( baseOffset > 0 && pathBase [ baseOffset - 1 ] == '/'
131+ && ( offsetValue == '/' || offsetValue == '\\ ' ) )
132+ {
133+ // Duplicate slash, skip
134+ originalOffset ++ ;
135+ }
136+ else if ( baseOffset > 0 && pathBase [ baseOffset - 1 ] == '/'
137+ && originalPath . AsSpan ( originalOffset ) . StartsWith ( "%2F" , StringComparison . OrdinalIgnoreCase ) )
138+ {
139+ // Duplicate slash equivalent, skip
140+ originalOffset += 3 ;
141+ }
142+ else
143+ {
144+ // Mismatch, fall back
145+ // The failing test case here is "/base/call//../bat//path1//path2", reduced to "/base/call/bat//path1//path2",
146+ // where http.sys collapses "//" before "../", but we do "../" first. We've lost the context that there were dot segments,
147+ // or duplicate slashes, how do we figure out that "call/" can be eliminated?
148+ originalOffset = 0 ;
149+ break ;
150+ }
151+ }
152+ PathBase = originalPath [ ..originalOffset ] ;
153+ Path = originalPath [ originalOffset ..] ;
92154 }
93155 }
156+ else if ( requestContext . Server . Options . UrlPrefixes . TryMatchLongestPrefix ( IsHttps , cookedUrl . GetHost ( ) ! , originalPath , out var pathBase , out var path ) )
157+ {
158+ PathBase = pathBase ;
159+ Path = path ;
160+ }
94161
95162 ProtocolVersion = RequestContext . GetVersion ( ) ;
96163
0 commit comments