-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathHttpStreamReader.fs
More file actions
236 lines (190 loc) · 8.76 KB
/
HttpStreamReader.fs
File metadata and controls
236 lines (190 loc) · 8.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
module HttpStreamReader
open System
open System.Buffers
open System.IO
open System.Text
open HttpHeaders
open HttpData
open HttpLogger
open Utils
exception InvalidHttpRequest of string
exception NoHttpRequest
type HttpStreamReader(stream: Stream) =
let buffer: byte[] = ArrayPool<byte>.Shared.Rent(32768)
let mutable position: int = 0
let mutable available: int = 0
let tryParseRequestLine (line: string) : (string * string * string) option =
if isNull line then None
else
let span = line.AsSpan()
let sp1 = span.IndexOf(' ')
if sp1 <= 0 then None
else
let rest1 = span.Slice(sp1 + 1)
let sp2 = rest1.IndexOf(' ')
if sp2 <= 0 then None
else
let sp2Abs = sp1 + 1 + sp2
let mspan = span.Slice(0, sp1)
let pspan = span.Slice(sp1 + 1, sp2Abs - (sp1 + 1))
let vspan = span.Slice(sp2Abs + 1)
if mspan.IsEmpty || pspan.IsEmpty || vspan.IsEmpty then None
else
// method must be A-Z+
let mutable ok = true
let mutable i = 0
while ok && i < mspan.Length do
let c = mspan.[i]
if c < 'A' || c > 'Z' then ok <- false
i <- i + 1
if not ok then None
elif not (vspan.StartsWith("HTTP/".AsSpan(), StringComparison.Ordinal)) then None
else
let verSpan = vspan.Slice(5)
if verSpan.IsEmpty then None
else
// Path decode: '+' should NOT become space in path
let path =
// tiny fast-path: only decode if needed
if pspan.IndexOf('%') < 0 then pspan.ToString()
else Utils.urlDecodeSpanFast pspan false
Some(mspan.ToString(), path, verSpan.ToString())
static member LF = Convert.ToByte('\n')
static member CR = Convert.ToByte('\r')
member _.Stream = stream
interface IDisposable with
member _.Dispose() =
if not (isNull stream) then
noexn (fun () -> stream.Close())
ArrayPool<byte>.Shared.Return(buffer)
member private _.EnsureAvailable() =
if position = available then
position <- 0
available <- stream.Read(buffer, 0, buffer.Length)
position < available
member private self.ReadLine() : string =
// If we have no data at all, it's EOF
if not (self.EnsureAvailable()) then
null
else
let CR = 13uy
let LF = 10uy
// Common case: line fits in the current buffer -> no copying
let span0 = Span<byte>(buffer, position, available - position)
let nl0 = span0.IndexOf(LF)
if nl0 >= 0 then
// line ends in this buffer
let mutable len = nl0
// trim trailing '\r' if present
if len > 0 && span0.[len - 1] = CR then
len <- len - 1
// validate ASCII
for i = 0 to len - 1 do
if span0.[i] > 127uy then
raise (DecoderFallbackException())
let s = Encoding.ASCII.GetString(buffer, position, len)
position <- position + nl0 + 1 // consume '\n'
s
else
// Slow path: line spans buffers -> accumulate into pooled temp
let mutable tmp = ArrayPool<byte>.Shared.Rent(4096)
let mutable tmpLen = 0
let mutable doneLine = false
let mutable sawAny = false
let inline ensureCapacity (need: int) =
if need > tmp.Length then
let newSize = max (tmp.Length * 2) need
let tmp2 = ArrayPool<byte>.Shared.Rent(newSize)
Buffer.BlockCopy(tmp, 0, tmp2, 0, tmpLen)
ArrayPool<byte>.Shared.Return(tmp)
tmp <- tmp2
try
while not doneLine do
if not (self.EnsureAvailable()) then
// EOF
doneLine <- true
else
let span = Span<byte>(buffer, position, available - position)
let nl = span.IndexOf(LF)
if nl >= 0 then
// copy bytes up to '\n'
let chunkLen = nl
if chunkLen > 0 then
sawAny <- true
ensureCapacity (tmpLen + chunkLen)
// validate + copy
for i = 0 to chunkLen - 1 do
let b = span.[i]
if b > 127uy then raise (DecoderFallbackException())
tmp.[tmpLen + i] <- b
tmpLen <- tmpLen + chunkLen
// consume through '\n'
position <- position + nl + 1
doneLine <- true
else
// no '\n' here, copy all remaining bytes
let chunkLen = span.Length
if chunkLen > 0 then
sawAny <- true
ensureCapacity (tmpLen + chunkLen)
for i = 0 to chunkLen - 1 do
let b = span.[i]
if b > 127uy then raise (DecoderFallbackException())
tmp.[tmpLen + i] <- b
tmpLen <- tmpLen + chunkLen
// consume all available
position <- available
// If we hit EOF and never collected anything, return null
if not sawAny && tmpLen = 0 then
null
else
// trim trailing '\r'
if tmpLen > 0 && tmp.[tmpLen - 1] = CR then
tmpLen <- tmpLen - 1
Encoding.ASCII.GetString(tmp, 0, tmpLen)
finally
ArrayPool<byte>.Shared.Return(tmp)
// ---------- ReadRequest ----------
member self.ReadRequest() =
let mutable httpcmd = self.ReadLine()
let headersb = HttpHeadersBuilder()
let isvalid = ref true
// header loop
let rec readheaders () =
let line = self.ReadLine()
if isNull line then
// EOF before blank line terminator -> invalid request
isvalid.Value <- false
elif line <> "" then
try
match HttpHeaderParser.tryParseHeaderLine line with
| Some(Choice1Of2(name, value)) ->
headersb.Push name value
| Some(Choice2Of2(value)) ->
headersb.PushContinuation value
| None ->
isvalid.Value <- false
with InvalidHttpHeaderContinuation ->
isvalid.Value <- false
readheaders ()
if isNull httpcmd then
isvalid.Value <- false
httpcmd <- ""
// IMPORTANT: actually consume headers
readheaders ()
if not isvalid.Value then
httpcmd <- ""
let headers = headersb.Headers
match tryParseRequestLine httpcmd with
| Some(methodStr, path, verStr) ->
let version = httpversion_of_string verStr
let httpmth = methodStr.ToUpperInvariant()
{ version = version
mthod = httpmth
path = path
headers = headers }
| None ->
{ version = httpversion_of_string ""
mthod = ""
path = ""
headers = headers }