@@ -29,11 +29,14 @@ public abstract partial class Frame : IFrameControl
29
29
private const byte ByteCR = ( byte ) '\r ' ;
30
30
private const byte ByteLF = ( byte ) '\n ' ;
31
31
private const byte ByteColon = ( byte ) ':' ;
32
+ private const byte ByteForwardSlash = ( byte ) '/' ;
32
33
private const byte ByteSpace = ( byte ) ' ' ;
33
34
private const byte ByteTab = ( byte ) '\t ' ;
34
35
private const byte ByteQuestionMark = ( byte ) '?' ;
35
36
private const byte BytePercentage = ( byte ) '%' ;
36
37
38
+ private const string EmptyPath = "/" ;
39
+
37
40
private static readonly ArraySegment < byte > _endChunkedResponseBytes = CreateAsciiByteArraySegment ( "0\r \n \r \n " ) ;
38
41
private static readonly ArraySegment < byte > _continueBytes = CreateAsciiByteArraySegment ( "HTTP/1.1 100 Continue\r \n \r \n " ) ;
39
42
@@ -976,7 +979,7 @@ private void CreateResponseHeader(
976
979
977
980
public RequestLineStatus TakeStartLine ( SocketInput input )
978
981
{
979
- const int MaxInvalidRequestLineChars = 32 ;
982
+ // expected start line format: https://tools.ietf.org/html/rfc7230#section-3.1.1
980
983
981
984
var scan = input . ConsumingStart ( ) ;
982
985
var start = scan ;
@@ -1014,22 +1017,21 @@ public RequestLineStatus TakeStartLine(SocketInput input)
1014
1017
}
1015
1018
end . Take ( ) ;
1016
1019
1020
+ // begin consuming method
1017
1021
string method ;
1018
1022
var begin = scan ;
1019
1023
if ( ! begin . GetKnownMethod ( out method ) )
1020
1024
{
1021
1025
if ( scan . Seek ( ByteSpace , ref end ) == - 1 )
1022
1026
{
1023
- RejectRequest ( RequestRejectionReason . InvalidRequestLine ,
1024
- Log . IsEnabled ( LogLevel . Information ) ? start . GetAsciiStringEscaped ( end , MaxInvalidRequestLineChars ) : string . Empty ) ;
1027
+ RejectInvalidStartLine ( start , end ) ;
1025
1028
}
1026
1029
1027
1030
method = begin . GetAsciiString ( ref scan ) ;
1028
1031
1029
1032
if ( method == null )
1030
1033
{
1031
- RejectRequest ( RequestRejectionReason . InvalidRequestLine ,
1032
- Log . IsEnabled ( LogLevel . Information ) ? start . GetAsciiStringEscaped ( end , MaxInvalidRequestLineChars ) : string . Empty ) ;
1034
+ RejectInvalidStartLine ( start , end ) ;
1033
1035
}
1034
1036
1035
1037
// Note: We're not in the fast path any more (GetKnownMethod should have handled any HTTP Method we're aware of)
@@ -1038,8 +1040,7 @@ public RequestLineStatus TakeStartLine(SocketInput input)
1038
1040
{
1039
1041
if ( ! IsValidTokenChar ( method [ i ] ) )
1040
1042
{
1041
- RejectRequest ( RequestRejectionReason . InvalidRequestLine ,
1042
- Log . IsEnabled ( LogLevel . Information ) ? start . GetAsciiStringEscaped ( end , MaxInvalidRequestLineChars ) : string . Empty ) ;
1043
+ RejectInvalidStartLine ( start , end ) ;
1043
1044
}
1044
1045
}
1045
1046
}
@@ -1048,55 +1049,78 @@ public RequestLineStatus TakeStartLine(SocketInput input)
1048
1049
scan . Skip ( method . Length ) ;
1049
1050
}
1050
1051
1052
+ // consume space
1051
1053
scan . Take ( ) ;
1054
+
1055
+ // begin consuming request-target
1052
1056
begin = scan ;
1057
+
1058
+ var targetBegin = scan ;
1059
+
1060
+ if ( targetBegin . Peek ( ) != ByteForwardSlash )
1061
+ {
1062
+ string requestUriScheme ;
1063
+ if ( scan . GetKnownHttpSchema ( out requestUriScheme ) )
1064
+ {
1065
+ // Start-line can contain uri in absolute form. e.g. 'GET http://contoso.com/favicon.ico HTTP/1.1'
1066
+ // Clients should only send this to proxies, but the spec requires we handle it anyways.
1067
+ // cref https://tools.ietf.org/html/rfc7230#section-5.3
1068
+ scan . Skip ( requestUriScheme . Length ) ;
1069
+
1070
+ if ( scan . Seek ( ByteForwardSlash , ByteSpace , ByteQuestionMark , ref end ) == - 1 )
1071
+ {
1072
+ RejectInvalidStartLine ( start , end ) ;
1073
+ }
1074
+
1075
+ begin = scan ;
1076
+ }
1077
+ }
1078
+
1053
1079
var needDecode = false ;
1054
1080
var chFound = scan . Seek ( ByteSpace , ByteQuestionMark , BytePercentage , ref end ) ;
1055
1081
if ( chFound == - 1 )
1056
1082
{
1057
- RejectRequest ( RequestRejectionReason . InvalidRequestLine ,
1058
- Log . IsEnabled ( LogLevel . Information ) ? start . GetAsciiStringEscaped ( end , MaxInvalidRequestLineChars ) : string . Empty ) ;
1083
+ RejectInvalidStartLine ( start , end ) ;
1059
1084
}
1060
1085
else if ( chFound == BytePercentage )
1061
1086
{
1062
1087
needDecode = true ;
1063
1088
chFound = scan . Seek ( ByteSpace , ByteQuestionMark , ref end ) ;
1064
1089
if ( chFound == - 1 )
1065
1090
{
1066
- RejectRequest ( RequestRejectionReason . InvalidRequestLine ,
1067
- Log . IsEnabled ( LogLevel . Information ) ? start . GetAsciiStringEscaped ( end , MaxInvalidRequestLineChars ) : string . Empty ) ;
1091
+ RejectInvalidStartLine ( start , end ) ;
1068
1092
}
1069
1093
}
1070
1094
1071
1095
var pathBegin = begin ;
1072
1096
var pathEnd = scan ;
1073
1097
1074
- var queryString = "" ;
1098
+ var queryString = string . Empty ;
1075
1099
if ( chFound == ByteQuestionMark )
1076
1100
{
1077
1101
begin = scan ;
1078
1102
if ( scan . Seek ( ByteSpace , ref end ) == - 1 )
1079
1103
{
1080
- RejectRequest ( RequestRejectionReason . InvalidRequestLine ,
1081
- Log . IsEnabled ( LogLevel . Information ) ? start . GetAsciiStringEscaped ( end , MaxInvalidRequestLineChars ) : string . Empty ) ;
1104
+ RejectInvalidStartLine ( start , end ) ;
1082
1105
}
1083
1106
queryString = begin . GetAsciiString ( ref scan ) ;
1084
1107
}
1085
1108
1086
1109
var queryEnd = scan ;
1087
1110
1088
- if ( pathBegin . Peek ( ) == ByteSpace )
1111
+ if ( pathBegin . Peek ( ) == ByteSpace && targetBegin . Index == pathBegin . Index )
1089
1112
{
1090
- RejectRequest ( RequestRejectionReason . InvalidRequestLine ,
1091
- Log . IsEnabled ( LogLevel . Information ) ? start . GetAsciiStringEscaped ( end , MaxInvalidRequestLineChars ) : string . Empty ) ;
1113
+ RejectInvalidStartLine ( start , end ) ;
1092
1114
}
1093
1115
1116
+ // consume space
1094
1117
scan . Take ( ) ;
1118
+
1119
+ // begin consuming HTTP-version
1095
1120
begin = scan ;
1096
1121
if ( scan . Seek ( ByteCR , ref end ) == - 1 )
1097
1122
{
1098
- RejectRequest ( RequestRejectionReason . InvalidRequestLine ,
1099
- Log . IsEnabled ( LogLevel . Information ) ? start . GetAsciiStringEscaped ( end , MaxInvalidRequestLineChars ) : string . Empty ) ;
1123
+ RejectInvalidStartLine ( start , end ) ;
1100
1124
}
1101
1125
1102
1126
string httpVersion ;
@@ -1106,20 +1130,19 @@ public RequestLineStatus TakeStartLine(SocketInput input)
1106
1130
1107
1131
if ( httpVersion == string . Empty )
1108
1132
{
1109
- RejectRequest ( RequestRejectionReason . InvalidRequestLine ,
1110
- Log . IsEnabled ( LogLevel . Information ) ? start . GetAsciiStringEscaped ( end , MaxInvalidRequestLineChars ) : string . Empty ) ;
1133
+ RejectInvalidStartLine ( start , end ) ;
1111
1134
}
1112
1135
else
1113
1136
{
1114
1137
RejectRequest ( RequestRejectionReason . UnrecognizedHTTPVersion , httpVersion ) ;
1115
1138
}
1116
1139
}
1117
1140
1118
- scan . Take ( ) ; // consume CR
1141
+ // consume CR
1142
+ scan . Take ( ) ;
1119
1143
if ( scan . Take ( ) != ByteLF )
1120
1144
{
1121
- RejectRequest ( RequestRejectionReason . InvalidRequestLine ,
1122
- Log . IsEnabled ( LogLevel . Information ) ? start . GetAsciiStringEscaped ( end , MaxInvalidRequestLineChars ) : string . Empty ) ;
1145
+ RejectInvalidStartLine ( start , end ) ;
1123
1146
}
1124
1147
1125
1148
// URIs are always encoded/escaped to ASCII https://tools.ietf.org/html/rfc3986#page-11
@@ -1130,7 +1153,7 @@ public RequestLineStatus TakeStartLine(SocketInput input)
1130
1153
if ( needDecode )
1131
1154
{
1132
1155
// Read raw target before mutating memory.
1133
- rawTarget = pathBegin . GetAsciiString ( ref queryEnd ) ;
1156
+ rawTarget = targetBegin . GetAsciiString ( ref queryEnd ) ;
1134
1157
1135
1158
// URI was encoded, unescape and then parse as utf8
1136
1159
pathEnd = UrlPathDecoder . Unescape ( pathBegin , pathEnd ) ;
@@ -1141,19 +1164,34 @@ public RequestLineStatus TakeStartLine(SocketInput input)
1141
1164
// URI wasn't encoded, parse as ASCII
1142
1165
requestUrlPath = pathBegin . GetAsciiString ( ref pathEnd ) ;
1143
1166
1144
- if ( queryString . Length == 0 )
1167
+ if ( queryString . Length == 0 && targetBegin . Index == pathBegin . Index )
1145
1168
{
1146
1169
// No need to allocate an extra string if the path didn't need
1147
- // decoding and there's no query string following it.
1170
+ // decoding and there's no query string following it, and the
1171
+ // request-target isn't absolute-form
1148
1172
rawTarget = requestUrlPath ;
1149
1173
}
1150
1174
else
1151
1175
{
1152
- rawTarget = pathBegin . GetAsciiString ( ref queryEnd ) ;
1176
+ rawTarget = targetBegin . GetAsciiString ( ref queryEnd ) ;
1153
1177
}
1154
1178
}
1155
1179
1156
- var normalizedTarget = PathNormalizer . RemoveDotSegments ( requestUrlPath ) ;
1180
+ if ( targetBegin . Index < pathBegin . Index )
1181
+ {
1182
+ // validation of absolute-form URI may be slow, but clients
1183
+ // should not be sending this form anyways, so perf optimization
1184
+ // not high priority
1185
+ Uri _ ;
1186
+ if ( ! Uri . TryCreate ( rawTarget , UriKind . Absolute , out _ ) )
1187
+ {
1188
+ RejectInvalidStartLine ( start , end ) ;
1189
+ }
1190
+ }
1191
+
1192
+ var normalizedUrlPath = requestUrlPath == null
1193
+ ? EmptyPath
1194
+ : PathNormalizer . RemoveDotSegments ( requestUrlPath ) ;
1157
1195
1158
1196
consumed = scan ;
1159
1197
Method = method ;
@@ -1162,17 +1200,24 @@ public RequestLineStatus TakeStartLine(SocketInput input)
1162
1200
HttpVersion = httpVersion ;
1163
1201
1164
1202
bool caseMatches ;
1165
- if ( RequestUrlStartsWithPathBase ( normalizedTarget , out caseMatches ) )
1203
+ if ( RequestUrlStartsWithPathBase ( normalizedUrlPath , out caseMatches ) )
1166
1204
{
1167
- PathBase = caseMatches ? _pathBase : normalizedTarget . Substring ( 0 , _pathBase . Length ) ;
1168
- Path = normalizedTarget . Substring ( _pathBase . Length ) ;
1205
+ // request-target is in origin-form or absolute-form
1206
+ // and path should be adjusted for matching the server base path
1207
+ PathBase = caseMatches ? _pathBase : normalizedUrlPath . Substring ( 0 , _pathBase . Length ) ;
1208
+ Path = normalizedUrlPath . Substring ( _pathBase . Length ) ;
1169
1209
}
1170
- else if ( rawTarget [ 0 ] == '/' ) // check rawTarget since normalizedTarget can be "" or "/" after dot segment removal
1210
+ else if ( ( requestUrlPath ? . Length > 0 && requestUrlPath [ 0 ] == '/' )
1211
+ || ( rawTarget . Length > 0 && rawTarget [ 0 ] == '/' )
1212
+ || ReferenceEquals ( normalizedUrlPath , EmptyPath ) )
1171
1213
{
1172
- Path = normalizedTarget ;
1214
+ // request-target is in origin-form or absolute-form
1215
+ Path = normalizedUrlPath ;
1173
1216
}
1174
1217
else
1175
1218
{
1219
+ // request-target is in asterisk-form and authority-form
1220
+ // also the catch-all for other malformed request-targets
1176
1221
Path = string . Empty ;
1177
1222
PathBase = string . Empty ;
1178
1223
QueryString = string . Empty ;
@@ -1480,6 +1525,16 @@ private void ThrowResponseAbortedException()
1480
1525
_applicationException ) ;
1481
1526
}
1482
1527
1528
+ private void RejectInvalidStartLine ( MemoryPoolIterator start , MemoryPoolIterator end )
1529
+ {
1530
+ const int MaxInvalidRequestLineChars = 32 ;
1531
+
1532
+ RejectRequest ( RequestRejectionReason . InvalidRequestLine ,
1533
+ Log . IsEnabled ( LogLevel . Information )
1534
+ ? start . GetAsciiStringEscaped ( end , MaxInvalidRequestLineChars )
1535
+ : string . Empty ) ;
1536
+ }
1537
+
1483
1538
public void RejectRequest ( RequestRejectionReason reason )
1484
1539
{
1485
1540
RejectRequest ( BadHttpRequestException . GetException ( reason ) ) ;
0 commit comments