|
1 /* |
|
2 * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved. |
|
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
|
4 * |
|
5 * This code is free software; you can redistribute it and/or modify it |
|
6 * under the terms of the GNU General Public License version 2 only, as |
|
7 * published by the Free Software Foundation. Oracle designates this |
|
8 * particular file as subject to the "Classpath" exception as provided |
|
9 * by Oracle in the LICENSE file that accompanied this code. |
|
10 * |
|
11 * This code is distributed in the hope that it will be useful, but WITHOUT |
|
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
|
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
|
14 * version 2 for more details (a copy is included in the LICENSE file that |
|
15 * accompanied this code). |
|
16 * |
|
17 * You should have received a copy of the GNU General Public License version |
|
18 * 2 along with this work; if not, write to the Free Software Foundation, |
|
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
|
20 * |
|
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
|
22 * or visit www.oracle.com if you need additional information or have any |
|
23 * questions. |
|
24 */ |
|
25 |
|
26 package jdk.internal.net.http.frame; |
|
27 |
|
28 import java.io.IOException; |
|
29 import java.lang.System.Logger.Level; |
|
30 import java.nio.ByteBuffer; |
|
31 import java.util.ArrayDeque; |
|
32 import java.util.ArrayList; |
|
33 import java.util.List; |
|
34 import jdk.internal.net.http.common.Log; |
|
35 import jdk.internal.net.http.common.Utils; |
|
36 import static java.nio.charset.StandardCharsets.UTF_8; |
|
37 |
|
38 /** |
|
39 * Frames Decoder |
|
40 * <p> |
|
41 * collect buffers until frame decoding is possible, |
|
42 * all decoded frames are passed to the FrameProcessor callback in order of decoding. |
|
43 * |
|
44 * It's a stateful class due to the fact that FramesDecoder stores buffers inside. |
|
45 * Should be allocated only the single instance per connection. |
|
46 */ |
|
47 public class FramesDecoder { |
|
48 |
|
49 static final boolean DEBUG = Utils.DEBUG; // Revisit: temporary dev flag. |
|
50 static final System.Logger DEBUG_LOGGER = |
|
51 Utils.getDebugLogger("FramesDecoder"::toString, DEBUG); |
|
52 |
|
53 @FunctionalInterface |
|
54 public interface FrameProcessor { |
|
55 void processFrame(Http2Frame frame) throws IOException; |
|
56 } |
|
57 |
|
58 private final FrameProcessor frameProcessor; |
|
59 private final int maxFrameSize; |
|
60 |
|
61 private ByteBuffer currentBuffer; // current buffer either null or hasRemaining |
|
62 |
|
63 private final ArrayDeque<ByteBuffer> tailBuffers = new ArrayDeque<>(); |
|
64 private int tailSize = 0; |
|
65 |
|
66 private boolean slicedToDataFrame = false; |
|
67 |
|
68 private final List<ByteBuffer> prepareToRelease = new ArrayList<>(); |
|
69 |
|
70 // if true - Frame Header was parsed (9 bytes consumed) and subsequent fields have meaning |
|
71 // otherwise - stopped at frames boundary |
|
72 private boolean frameHeaderParsed = false; |
|
73 private int frameLength; |
|
74 private int frameType; |
|
75 private int frameFlags; |
|
76 private int frameStreamid; |
|
77 private boolean closed; |
|
78 |
|
79 /** |
|
80 * Creates Frame Decoder |
|
81 * |
|
82 * @param frameProcessor - callback for decoded frames |
|
83 */ |
|
84 public FramesDecoder(FrameProcessor frameProcessor) { |
|
85 this(frameProcessor, 16 * 1024); |
|
86 } |
|
87 |
|
88 /** |
|
89 * Creates Frame Decoder |
|
90 * @param frameProcessor - callback for decoded frames |
|
91 * @param maxFrameSize - maxFrameSize accepted by this decoder |
|
92 */ |
|
93 public FramesDecoder(FrameProcessor frameProcessor, int maxFrameSize) { |
|
94 this.frameProcessor = frameProcessor; |
|
95 this.maxFrameSize = Math.min(Math.max(16 * 1024, maxFrameSize), 16 * 1024 * 1024 - 1); |
|
96 } |
|
97 |
|
98 /** Threshold beyond which data is no longer copied into the current buffer, |
|
99 * if that buffer has enough unused space. */ |
|
100 private static final int COPY_THRESHOLD = 8192; |
|
101 |
|
102 /** |
|
103 * Adds the data from the given buffer, and performs frame decoding if |
|
104 * possible. Either 1) appends the data from the given buffer to the |
|
105 * current buffer ( if there is enough unused space ), or 2) adds it to the |
|
106 * next buffer in the queue. |
|
107 * |
|
108 * If there is enough data to perform frame decoding then, all buffers are |
|
109 * decoded and the FrameProcessor is invoked. |
|
110 */ |
|
111 public void decode(ByteBuffer inBoundBuffer) throws IOException { |
|
112 if (closed) { |
|
113 DEBUG_LOGGER.log(Level.DEBUG, "closed: ignoring buffer (%s bytes)", |
|
114 inBoundBuffer.remaining()); |
|
115 inBoundBuffer.position(inBoundBuffer.limit()); |
|
116 return; |
|
117 } |
|
118 int remaining = inBoundBuffer.remaining(); |
|
119 DEBUG_LOGGER.log(Level.DEBUG, "decodes: %d", remaining); |
|
120 if (remaining > 0) { |
|
121 if (currentBuffer == null) { |
|
122 currentBuffer = inBoundBuffer; |
|
123 } else { |
|
124 ByteBuffer b = currentBuffer; |
|
125 if (!tailBuffers.isEmpty()) { |
|
126 b = tailBuffers.getLast(); |
|
127 } |
|
128 |
|
129 int limit = b.limit(); |
|
130 int freeSpace = b.capacity() - limit; |
|
131 if (remaining <= COPY_THRESHOLD && freeSpace >= remaining) { |
|
132 // append the new data to the unused space in the current buffer |
|
133 int position = b.position(); |
|
134 b.position(limit); |
|
135 b.limit(limit + inBoundBuffer.remaining()); |
|
136 b.put(inBoundBuffer); |
|
137 b.position(position); |
|
138 if (b != currentBuffer) |
|
139 tailSize += remaining; |
|
140 DEBUG_LOGGER.log(Level.DEBUG, "copied: %d", remaining); |
|
141 } else { |
|
142 DEBUG_LOGGER.log(Level.DEBUG, "added: %d", remaining); |
|
143 tailBuffers.add(inBoundBuffer); |
|
144 tailSize += remaining; |
|
145 } |
|
146 } |
|
147 } |
|
148 DEBUG_LOGGER.log(Level.DEBUG, "Tail size is now: %d, current=", |
|
149 tailSize, |
|
150 (currentBuffer == null ? 0 : |
|
151 currentBuffer.remaining())); |
|
152 Http2Frame frame; |
|
153 while ((frame = nextFrame()) != null) { |
|
154 DEBUG_LOGGER.log(Level.DEBUG, "Got frame: %s", frame); |
|
155 frameProcessor.processFrame(frame); |
|
156 frameProcessed(); |
|
157 } |
|
158 } |
|
159 |
|
160 private Http2Frame nextFrame() throws IOException { |
|
161 while (true) { |
|
162 if (currentBuffer == null) { |
|
163 return null; // no data at all |
|
164 } |
|
165 long available = currentBuffer.remaining() + tailSize; |
|
166 if (!frameHeaderParsed) { |
|
167 if (available >= Http2Frame.FRAME_HEADER_SIZE) { |
|
168 parseFrameHeader(); |
|
169 if (frameLength > maxFrameSize) { |
|
170 // connection error |
|
171 return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR, |
|
172 "Frame type("+frameType+") " |
|
173 +"length("+frameLength |
|
174 +") exceeds MAX_FRAME_SIZE(" |
|
175 + maxFrameSize+")"); |
|
176 } |
|
177 frameHeaderParsed = true; |
|
178 } else { |
|
179 DEBUG_LOGGER.log(Level.DEBUG, |
|
180 "Not enough data to parse header, needs: %d, has: %d", |
|
181 Http2Frame.FRAME_HEADER_SIZE, available); |
|
182 return null; |
|
183 } |
|
184 } |
|
185 available = currentBuffer == null ? 0 : currentBuffer.remaining() + tailSize; |
|
186 if ((frameLength == 0) || |
|
187 (currentBuffer != null && available >= frameLength)) { |
|
188 Http2Frame frame = parseFrameBody(); |
|
189 frameHeaderParsed = false; |
|
190 // frame == null means we have to skip this frame and try parse next |
|
191 if (frame != null) { |
|
192 return frame; |
|
193 } |
|
194 } else { |
|
195 DEBUG_LOGGER.log(Level.DEBUG, |
|
196 "Not enough data to parse frame body, needs: %d, has: %d", |
|
197 frameLength, available); |
|
198 return null; // no data for the whole frame header |
|
199 } |
|
200 } |
|
201 } |
|
202 |
|
203 private void frameProcessed() { |
|
204 prepareToRelease.clear(); |
|
205 } |
|
206 |
|
207 private void parseFrameHeader() throws IOException { |
|
208 int x = getInt(); |
|
209 this.frameLength = (x >>> 8) & 0x00ffffff; |
|
210 this.frameType = x & 0xff; |
|
211 this.frameFlags = getByte(); |
|
212 this.frameStreamid = getInt() & 0x7fffffff; |
|
213 // R: A reserved 1-bit field. The semantics of this bit are undefined, |
|
214 // MUST be ignored when receiving. |
|
215 } |
|
216 |
|
217 // move next buffer from tailBuffers to currentBuffer if required |
|
218 private void nextBuffer() { |
|
219 if (!currentBuffer.hasRemaining()) { |
|
220 if (!slicedToDataFrame) { |
|
221 prepareToRelease.add(currentBuffer); |
|
222 } |
|
223 slicedToDataFrame = false; |
|
224 currentBuffer = tailBuffers.poll(); |
|
225 if (currentBuffer != null) { |
|
226 tailSize -= currentBuffer.remaining(); |
|
227 } |
|
228 } |
|
229 } |
|
230 |
|
231 public int getByte() { |
|
232 int res = currentBuffer.get() & 0xff; |
|
233 nextBuffer(); |
|
234 return res; |
|
235 } |
|
236 |
|
237 public int getShort() { |
|
238 if (currentBuffer.remaining() >= 2) { |
|
239 int res = currentBuffer.getShort() & 0xffff; |
|
240 nextBuffer(); |
|
241 return res; |
|
242 } |
|
243 int val = getByte(); |
|
244 val = (val << 8) + getByte(); |
|
245 return val; |
|
246 } |
|
247 |
|
248 public int getInt() { |
|
249 if (currentBuffer.remaining() >= 4) { |
|
250 int res = currentBuffer.getInt(); |
|
251 nextBuffer(); |
|
252 return res; |
|
253 } |
|
254 int val = getByte(); |
|
255 val = (val << 8) + getByte(); |
|
256 val = (val << 8) + getByte(); |
|
257 val = (val << 8) + getByte(); |
|
258 return val; |
|
259 |
|
260 } |
|
261 |
|
262 public byte[] getBytes(int n) { |
|
263 byte[] bytes = new byte[n]; |
|
264 int offset = 0; |
|
265 while (n > 0) { |
|
266 int length = Math.min(n, currentBuffer.remaining()); |
|
267 currentBuffer.get(bytes, offset, length); |
|
268 offset += length; |
|
269 n -= length; |
|
270 nextBuffer(); |
|
271 } |
|
272 return bytes; |
|
273 |
|
274 } |
|
275 |
|
276 private List<ByteBuffer> getBuffers(boolean isDataFrame, int bytecount) { |
|
277 List<ByteBuffer> res = new ArrayList<>(); |
|
278 while (bytecount > 0) { |
|
279 int remaining = currentBuffer.remaining(); |
|
280 int extract = Math.min(remaining, bytecount); |
|
281 ByteBuffer extractedBuf; |
|
282 if (isDataFrame) { |
|
283 extractedBuf = Utils.sliceWithLimitedCapacity(currentBuffer, extract) |
|
284 .asReadOnlyBuffer(); |
|
285 slicedToDataFrame = true; |
|
286 } else { |
|
287 // Header frames here |
|
288 // HPACK decoding should performed under lock and immediately after frame decoding. |
|
289 // in that case it is safe to release original buffer, |
|
290 // because of sliced buffer has a very short life |
|
291 extractedBuf = Utils.sliceWithLimitedCapacity(currentBuffer, extract); |
|
292 } |
|
293 res.add(extractedBuf); |
|
294 bytecount -= extract; |
|
295 nextBuffer(); |
|
296 } |
|
297 return res; |
|
298 } |
|
299 |
|
300 public void close(String msg) { |
|
301 closed = true; |
|
302 tailBuffers.clear(); |
|
303 int bytes = tailSize; |
|
304 ByteBuffer b = currentBuffer; |
|
305 if (b != null) { |
|
306 bytes += b.remaining(); |
|
307 b.position(b.limit()); |
|
308 } |
|
309 tailSize = 0; |
|
310 currentBuffer = null; |
|
311 DEBUG_LOGGER.log(Level.DEBUG, "closed %s, ignoring %d bytes", msg, bytes); |
|
312 } |
|
313 |
|
314 public void skipBytes(int bytecount) { |
|
315 while (bytecount > 0) { |
|
316 int remaining = currentBuffer.remaining(); |
|
317 int extract = Math.min(remaining, bytecount); |
|
318 currentBuffer.position(currentBuffer.position() + extract); |
|
319 bytecount -= remaining; |
|
320 nextBuffer(); |
|
321 } |
|
322 } |
|
323 |
|
324 private Http2Frame parseFrameBody() throws IOException { |
|
325 assert frameHeaderParsed; |
|
326 switch (frameType) { |
|
327 case DataFrame.TYPE: |
|
328 return parseDataFrame(frameLength, frameStreamid, frameFlags); |
|
329 case HeadersFrame.TYPE: |
|
330 return parseHeadersFrame(frameLength, frameStreamid, frameFlags); |
|
331 case PriorityFrame.TYPE: |
|
332 return parsePriorityFrame(frameLength, frameStreamid, frameFlags); |
|
333 case ResetFrame.TYPE: |
|
334 return parseResetFrame(frameLength, frameStreamid, frameFlags); |
|
335 case SettingsFrame.TYPE: |
|
336 return parseSettingsFrame(frameLength, frameStreamid, frameFlags); |
|
337 case PushPromiseFrame.TYPE: |
|
338 return parsePushPromiseFrame(frameLength, frameStreamid, frameFlags); |
|
339 case PingFrame.TYPE: |
|
340 return parsePingFrame(frameLength, frameStreamid, frameFlags); |
|
341 case GoAwayFrame.TYPE: |
|
342 return parseGoAwayFrame(frameLength, frameStreamid, frameFlags); |
|
343 case WindowUpdateFrame.TYPE: |
|
344 return parseWindowUpdateFrame(frameLength, frameStreamid, frameFlags); |
|
345 case ContinuationFrame.TYPE: |
|
346 return parseContinuationFrame(frameLength, frameStreamid, frameFlags); |
|
347 default: |
|
348 // RFC 7540 4.1 |
|
349 // Implementations MUST ignore and discard any frame that has a type that is unknown. |
|
350 Log.logTrace("Unknown incoming frame type: {0}", frameType); |
|
351 skipBytes(frameLength); |
|
352 return null; |
|
353 } |
|
354 } |
|
355 |
|
356 private Http2Frame parseDataFrame(int frameLength, int streamid, int flags) { |
|
357 // non-zero stream |
|
358 if (streamid == 0) { |
|
359 return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, |
|
360 "zero streamId for DataFrame"); |
|
361 } |
|
362 int padLength = 0; |
|
363 if ((flags & DataFrame.PADDED) != 0) { |
|
364 padLength = getByte(); |
|
365 if (padLength >= frameLength) { |
|
366 return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, |
|
367 "the length of the padding is the length of the frame payload or greater"); |
|
368 } |
|
369 frameLength--; |
|
370 } |
|
371 DataFrame df = new DataFrame(streamid, flags, |
|
372 getBuffers(true, frameLength - padLength), padLength); |
|
373 skipBytes(padLength); |
|
374 return df; |
|
375 |
|
376 } |
|
377 |
|
378 private Http2Frame parseHeadersFrame(int frameLength, int streamid, int flags) { |
|
379 // non-zero stream |
|
380 if (streamid == 0) { |
|
381 return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, |
|
382 "zero streamId for HeadersFrame"); |
|
383 } |
|
384 int padLength = 0; |
|
385 if ((flags & HeadersFrame.PADDED) != 0) { |
|
386 padLength = getByte(); |
|
387 frameLength--; |
|
388 } |
|
389 boolean hasPriority = (flags & HeadersFrame.PRIORITY) != 0; |
|
390 boolean exclusive = false; |
|
391 int streamDependency = 0; |
|
392 int weight = 0; |
|
393 if (hasPriority) { |
|
394 int x = getInt(); |
|
395 exclusive = (x & 0x80000000) != 0; |
|
396 streamDependency = x & 0x7fffffff; |
|
397 weight = getByte(); |
|
398 frameLength -= 5; |
|
399 } |
|
400 if(frameLength < padLength) { |
|
401 return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, |
|
402 "Padding exceeds the size remaining for the header block"); |
|
403 } |
|
404 HeadersFrame hf = new HeadersFrame(streamid, flags, |
|
405 getBuffers(false, frameLength - padLength), padLength); |
|
406 skipBytes(padLength); |
|
407 if (hasPriority) { |
|
408 hf.setPriority(streamDependency, exclusive, weight); |
|
409 } |
|
410 return hf; |
|
411 } |
|
412 |
|
413 private Http2Frame parsePriorityFrame(int frameLength, int streamid, int flags) { |
|
414 // non-zero stream; no flags |
|
415 if (streamid == 0) { |
|
416 return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, |
|
417 "zero streamId for PriorityFrame"); |
|
418 } |
|
419 if(frameLength != 5) { |
|
420 skipBytes(frameLength); |
|
421 return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR, streamid, |
|
422 "PriorityFrame length is "+ frameLength+", expected 5"); |
|
423 } |
|
424 int x = getInt(); |
|
425 int weight = getByte(); |
|
426 return new PriorityFrame(streamid, x & 0x7fffffff, (x & 0x80000000) != 0, weight); |
|
427 } |
|
428 |
|
429 private Http2Frame parseResetFrame(int frameLength, int streamid, int flags) { |
|
430 // non-zero stream; no flags |
|
431 if (streamid == 0) { |
|
432 return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, |
|
433 "zero streamId for ResetFrame"); |
|
434 } |
|
435 if(frameLength != 4) { |
|
436 return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR, |
|
437 "ResetFrame length is "+ frameLength+", expected 4"); |
|
438 } |
|
439 return new ResetFrame(streamid, getInt()); |
|
440 } |
|
441 |
|
442 private Http2Frame parseSettingsFrame(int frameLength, int streamid, int flags) { |
|
443 // only zero stream |
|
444 if (streamid != 0) { |
|
445 return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, |
|
446 "non-zero streamId for SettingsFrame"); |
|
447 } |
|
448 if ((SettingsFrame.ACK & flags) != 0 && frameLength > 0) { |
|
449 // RFC 7540 6.5 |
|
450 // Receipt of a SETTINGS frame with the ACK flag set and a length |
|
451 // field value other than 0 MUST be treated as a connection error |
|
452 return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR, |
|
453 "ACK SettingsFrame is not empty"); |
|
454 } |
|
455 if (frameLength % 6 != 0) { |
|
456 return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR, |
|
457 "invalid SettingsFrame size: "+frameLength); |
|
458 } |
|
459 SettingsFrame sf = new SettingsFrame(flags); |
|
460 int n = frameLength / 6; |
|
461 for (int i=0; i<n; i++) { |
|
462 int id = getShort(); |
|
463 int val = getInt(); |
|
464 if (id > 0 && id <= SettingsFrame.MAX_PARAM) { |
|
465 // a known parameter. Ignore otherwise |
|
466 sf.setParameter(id, val); // TODO parameters validation |
|
467 } |
|
468 } |
|
469 return sf; |
|
470 } |
|
471 |
|
472 private Http2Frame parsePushPromiseFrame(int frameLength, int streamid, int flags) { |
|
473 // non-zero stream |
|
474 if (streamid == 0) { |
|
475 return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, |
|
476 "zero streamId for PushPromiseFrame"); |
|
477 } |
|
478 int padLength = 0; |
|
479 if ((flags & PushPromiseFrame.PADDED) != 0) { |
|
480 padLength = getByte(); |
|
481 frameLength--; |
|
482 } |
|
483 int promisedStream = getInt() & 0x7fffffff; |
|
484 frameLength -= 4; |
|
485 if(frameLength < padLength) { |
|
486 return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, |
|
487 "Padding exceeds the size remaining for the PushPromiseFrame"); |
|
488 } |
|
489 PushPromiseFrame ppf = new PushPromiseFrame(streamid, flags, promisedStream, |
|
490 getBuffers(false, frameLength - padLength), padLength); |
|
491 skipBytes(padLength); |
|
492 return ppf; |
|
493 } |
|
494 |
|
495 private Http2Frame parsePingFrame(int frameLength, int streamid, int flags) { |
|
496 // only zero stream |
|
497 if (streamid != 0) { |
|
498 return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, |
|
499 "non-zero streamId for PingFrame"); |
|
500 } |
|
501 if(frameLength != 8) { |
|
502 return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR, |
|
503 "PingFrame length is "+ frameLength+", expected 8"); |
|
504 } |
|
505 return new PingFrame(flags, getBytes(8)); |
|
506 } |
|
507 |
|
508 private Http2Frame parseGoAwayFrame(int frameLength, int streamid, int flags) { |
|
509 // only zero stream; no flags |
|
510 if (streamid != 0) { |
|
511 return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, |
|
512 "non-zero streamId for GoAwayFrame"); |
|
513 } |
|
514 if (frameLength < 8) { |
|
515 return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR, |
|
516 "Invalid GoAway frame size"); |
|
517 } |
|
518 int lastStream = getInt() & 0x7fffffff; |
|
519 int errorCode = getInt(); |
|
520 byte[] debugData = getBytes(frameLength - 8); |
|
521 if (debugData.length > 0) { |
|
522 Log.logError("GoAway debugData " + new String(debugData, UTF_8)); |
|
523 } |
|
524 return new GoAwayFrame(lastStream, errorCode, debugData); |
|
525 } |
|
526 |
|
527 private Http2Frame parseWindowUpdateFrame(int frameLength, int streamid, int flags) { |
|
528 // any stream; no flags |
|
529 if(frameLength != 4) { |
|
530 return new MalformedFrame(ErrorFrame.FRAME_SIZE_ERROR, |
|
531 "WindowUpdateFrame length is "+ frameLength+", expected 4"); |
|
532 } |
|
533 return new WindowUpdateFrame(streamid, getInt() & 0x7fffffff); |
|
534 } |
|
535 |
|
536 private Http2Frame parseContinuationFrame(int frameLength, int streamid, int flags) { |
|
537 // non-zero stream; |
|
538 if (streamid == 0) { |
|
539 return new MalformedFrame(ErrorFrame.PROTOCOL_ERROR, |
|
540 "zero streamId for ContinuationFrame"); |
|
541 } |
|
542 return new ContinuationFrame(streamid, flags, getBuffers(false, frameLength)); |
|
543 } |
|
544 |
|
545 } |