/** * Used to add additional HTTP message chunk content to {@link #inputContentBuffer}. * * @param requestedLen how much content should attempt to be read, <code>-1</code> means read till * the end of the message. * @return the number of bytes actually read * @throws IOException if an I/O error occurs while reading content */ private int fill(final int requestedLen) throws IOException { int read = 0; while ((requestedLen == -1 || read < requestedLen) && httpHeader.isExpectContent()) { final HttpContent c = blockingRead(); final boolean isLast = c.isLast(); // Check if HttpContent is chunked message trailer w/ headers checkHttpTrailer(c); final Buffer b; try { b = c.getContent(); } catch (HttpBrokenContentException e) { final Throwable cause = e.getCause(); throw Exceptions.makeIOException(cause != null ? cause : e); } read += b.remaining(); updateInputContentBuffer(b); c.recycle(); if (isLast) { finished(); break; } } if (read > 0 || requestedLen == 0) { return read; } return -1; }
/** * Appends the specified {@link Buffer} to the internal composite {@link Buffer}. * * @param httpContent the {@link HttpContent} to append * @return <code>true</code> if {@link ReadHandler} callback was invoked, otherwise returns <code> * false</code>. * @throws IOException if an error occurs appending the {@link Buffer} */ public boolean append(final HttpContent httpContent) throws IOException { // Stop waiting for data asynchronously and enable it again // only if we have a handler registered, which requirement // (expected size) is not met. isWaitingDataAsynchronously = false; // check if it's broken HTTP content message or not if (!HttpContent.isBroken(httpContent)) { final Buffer buffer = httpContent.getContent(); if (closed) { buffer.dispose(); return false; } final ReadHandler localHandler = handler; final boolean isLast = httpContent.isLast(); // if we have a handler registered - switch the flag to true boolean askForMoreDataInThisThread = !isLast && localHandler != null; boolean invokeDataAvailable = false; if (buffer.hasRemaining()) { updateInputContentBuffer(buffer); if (localHandler != null) { final int available = readyData(); if (available >= requestedSize) { invokeDataAvailable = true; askForMoreDataInThisThread = false; } } } if (askForMoreDataInThisThread) { // There is a ReadHandler registered, but it requested more // data to be available before we can notify it - so wait for // more data to come isWaitingDataAsynchronously = true; return true; } handler = null; if (isLast) { checkHttpTrailer(httpContent); } invokeHandlerOnProperThread(localHandler, invokeDataAvailable, isLast); } else { // broken content final ReadHandler localHandler = handler; handler = null; invokeErrorHandlerOnProperThread( localHandler, ((HttpBrokenContent) httpContent).getException()); } return false; }
@Override public NextAction handleRead(final FilterChainContext ctx) throws IOException { final HttpContent httpContent = ctx.getMessage(); if (httpContent.isLast()) { // Perform the cleanup logic if it's the last chunk of the payload final HttpResponsePacket response = (HttpResponsePacket) httpContent.getHttpHeader(); recycleRequestResponsePackets(ctx.getConnection(), response); return ctx.getStopAction(); } return ctx.getInvokeAction(); }
@Override public NextAction handleRead(FilterChainContext ctx) throws IOException { HttpContent c = (HttpContent) ctx.getMessage(); Buffer b = c.getContent(); if (b.hasRemaining()) { sb.append(b.toStringContent()); } // Last content from the server, set the future result so // the client can display the result and gracefully exit. if (c.isLast()) { future.result(sb.toString()); } return ctx.getStopAction(); // discontinue filter chain execution }
/** * Check if passed {@link HttpContent} is {@link HttpTrailer}, which represents trailer chunk * (when chunked Transfer-Encoding is used), if it is a trailer chunk - then copy all the * available trailer headers to request headers map. * * @param httpContent */ private static void checkHttpTrailer(final HttpContent httpContent) { if (HttpTrailer.isTrailer(httpContent)) { final HttpTrailer httpTrailer = (HttpTrailer) httpContent; final HttpHeader httpHeader = httpContent.getHttpHeader(); final MimeHeaders trailerHeaders = httpTrailer.getHeaders(); final int size = trailerHeaders.size(); for (int i = 0; i < size; i++) { httpHeader.addHeader( trailerHeaders.getName(i).toString(), trailerHeaders.getValue(i).toString()); } } }
@Override public NextAction handleRead(FilterChainContext ctx) throws IOException { // Get the parsed HttpContent (we assume prev. filter was HTTP) HttpContent message = ctx.getMessage(); Socket tunnelSocket = tunnelSockets.get(ctx.getConnection()); if (tunnelSocket == null) { // handle connection procedure return GrizzlyModProxy.this.handleConnect(ctx, message); } if (message.getContent().hasRemaining()) { // relay the content to the tunnel connection Buffer buffer = message.getContent(); message.recycle(); tunnelSocket .getOutputStream() .write(buffer.array(), buffer.arrayOffset(), buffer.remaining()); } return ctx.getStopAction(); }
/** * Per-request initialization required for the InputBuffer to function properly. * * @param httpHeader the header from which input will be obtained. * @param ctx the FilterChainContext for the chain processing this request */ public void initialize(final HttpHeader httpHeader, final FilterChainContext ctx) { if (ctx == null) { throw new IllegalArgumentException("ctx cannot be null."); } this.httpHeader = httpHeader; this.ctx = ctx; connection = ctx.getConnection(); final Object message = ctx.getMessage(); if (message instanceof HttpContent) { final HttpContent content = (HttpContent) message; // Check if HttpContent is chunked message trailer w/ headers checkHttpTrailer(content); updateInputContentBuffer(content.getContent()); contentRead = content.isLast(); content.recycle(); if (LOGGER.isLoggable(LOGGER_LEVEL)) { log("InputBuffer %s initialize with ready content: %s", this, inputContentBuffer); } } }
@SuppressWarnings("unchecked") public boolean sendRequest( final FilterChainContext ctx, final Request request, final HttpRequestPacket requestPacket) throws IOException { boolean isWriteComplete = true; if (Utils.requestHasEntityBody(request)) { final HttpTxContext context = HttpTxContext.get(ctx); BodyHandler handler = bodyHandlerFactory.getBodyHandler(request); if (requestPacket.getHeaders().contains(Header.Expect) && requestPacket.getHeaders().getValue(1).equalsIgnoreCase("100-Continue")) { // We have to set the content-length now as the headers will be flushed // before the FileBodyHandler is invoked. If we don't do it here, and // the user didn't explicitly set the length, then the transfer-encoding // will be chunked and zero-copy file transfer will not occur. final File f = request.getFile(); if (f != null) { requestPacket.setContentLengthLong(f.length()); } handler = new ExpectHandler(handler); } context.setBodyHandler(handler); if (logger.isDebugEnabled()) { logger.debug("REQUEST: {}", requestPacket); } isWriteComplete = handler.doHandle(ctx, request, requestPacket); } else { HttpContent content = HttpContent.builder(requestPacket).last(true).build(); if (logger.isDebugEnabled()) { logger.debug("REQUEST: {}", requestPacket); } ctx.write(content, ctx.getTransportContext().getCompletionHandler()); } return isWriteComplete; }
private void writeHttpResponse(FilterChainContext ctx, int status) { HttpResponsePacket responsePacket = getHttpRequest(ctx).getResponse(); responsePacket.setProtocol(Protocol.HTTP_1_1); responsePacket.setStatus(status); ctx.write(HttpContent.builder(responsePacket).build()); }
/** * Method invoked when a first massage (Assumed to be HTTP) is received. Normally this would be * HTTP CONNECT and this method processes it and opens a connection to the destination (the server * that the client wants to access). * * <p>This method can be overridden to provide a test-specific handling of the CONNECT method. */ protected NextAction handleConnect(FilterChainContext ctx, HttpContent content) { System.out.println("Handle CONNECT start . . ."); HttpHeader httpHeader = content.getHttpHeader(); HttpRequestPacket requestPacket = (HttpRequestPacket) httpHeader.getHttpHeader(); if (!requestPacket.getMethod().matchesMethod("CONNECT")) { System.out.println("Received method is not CONNECT"); writeHttpResponse(ctx, 400); return ctx.getStopAction(); } String destinationUri = requestPacket.getRequestURI(); // We expect URI in form host:port, this is not flexible, but we use it only to test our clients int colonIdx = destinationUri.indexOf(':'); if (colonIdx == -1) { System.out.println("Destination URI not in host:port format: " + destinationUri); writeHttpResponse(ctx, 400); return ctx.getStopAction(); } String hostName = destinationUri.substring(0, colonIdx); String portStr = destinationUri.substring(colonIdx + 1); int port; try { port = Integer.parseInt(portStr); } catch (Throwable t) { System.out.println("Could not parse destination port: " + portStr); writeHttpResponse(ctx, 400); return ctx.getStopAction(); } try { Socket tunnelSocket = new Socket(hostName, port); Connection grizzlyConnection = ctx.getConnection(); tunnelSockets.set(grizzlyConnection, tunnelSocket); TunnelSocketReader tunnelSocketReader = new TunnelSocketReader(tunnelSocket, grizzlyConnection); executorService.submit(tunnelSocketReader::read); } catch (IOException e) { writeHttpResponse(ctx, 400); return ctx.getStopAction(); } // Grizzly does not like CONNECT method and sets "keep alive" to false, if it is present // This hacks Grizzly, so it will keep the connection open HttpRequestPacket request = getHttpRequest(ctx); request.getResponse().getProcessingState().setKeepAlive(true); request.getResponse().setContentLength(0); request.setMethod("GET"); // end of hack writeHttpResponse(ctx, 200); System.out.println("Connection to proxy established."); return ctx.getStopAction(); }
/** * Used to convert bytes to chars. * * @param requestedLen how much content should attempt to be read * @return the number of chars actually read * @throws IOException if an I/O error occurs while reading content */ private int fillChars(final int requestedLen, final CharBuffer dst) throws IOException { int read = 0; // 1) Check pre-decoded singleCharBuf if (dst != singleCharBuf && singleCharBuf.hasRemaining()) { dst.put(singleCharBuf.get()); read = 1; } // 2) Decode available byte[] -> char[] if (inputContentBuffer.hasRemaining()) { read += fillAvailableChars(requestedLen - read, dst); } if (read >= requestedLen) { dst.flip(); return read; } // 3) If we don't expect more data - return what we've read so far if (!httpHeader.isExpectContent()) { dst.flip(); return read > 0 ? read : -1; } // 4) Try to read more data (we may block) CharsetDecoder decoderLocal = getDecoder(); boolean isNeedMoreInput = false; // true, if content in composite buffer is not enough to produce even 1 char boolean last = false; while (read < requestedLen && httpHeader.isExpectContent()) { if (isNeedMoreInput || !inputContentBuffer.hasRemaining()) { final HttpContent c = blockingRead(); updateInputContentBuffer(c.getContent()); last = c.isLast(); c.recycle(); isNeedMoreInput = false; } final ByteBuffer bytes = inputContentBuffer.toByteBuffer(); final int bytesPos = bytes.position(); final int dstPos = dst.position(); final CoderResult result = decoderLocal.decode(bytes, dst, false); final int producedChars = dst.position() - dstPos; final int consumedBytes = bytes.position() - bytesPos; read += producedChars; if (consumedBytes > 0) { bytes.position(bytesPos); inputContentBuffer.position(inputContentBuffer.position() + consumedBytes); if (readAheadLimit == -1) { inputContentBuffer.shrink(); } } else { isNeedMoreInput = true; } if (last || result == CoderResult.OVERFLOW) { break; } } dst.flip(); if (last && read == 0) { read = -1; } return read; }