// // ======================================================================== // Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. // // The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // // The Apache License v2.0 is available at // http://www.opensource.org/licenses/apache2.0.php // // You may elect to redistribute this code under either of these licenses. // ======================================================================== // package org.eclipse.jetty.server; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpParser; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.io.ByteArrayEndPoint; import org.eclipse.jetty.io.ByteBufferPool; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.ByteArrayOutputStream2; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.Scheduler; /** * A local connector, mostly for testing purposes. * <pre> * HttpTester.Request request = HttpTester.newRequest(); * request.setURI("/some/resource"); * HttpTester.Response response = * HttpTester.parseResponse(HttpTester.from(localConnector.getResponse(request.generate()))); * </pre> * */ public class LocalConnector extends AbstractConnector { private final BlockingQueue<LocalEndPoint> _connects = new LinkedBlockingQueue<>(); public LocalConnector(Server server, Executor executor, Scheduler scheduler, ByteBufferPool pool, int acceptors, ConnectionFactory... factories) { super(server,executor,scheduler,pool,acceptors,factories); setIdleTimeout(30000); } public LocalConnector(Server server) { this(server, null, null, null, -1, new HttpConnectionFactory()); } public LocalConnector(Server server, SslContextFactory sslContextFactory) { this(server, null, null, null, -1,AbstractConnectionFactory.getFactories(sslContextFactory,new HttpConnectionFactory())); } public LocalConnector(Server server, ConnectionFactory connectionFactory) { this(server, null, null, null, -1, connectionFactory); } public LocalConnector(Server server, ConnectionFactory connectionFactory, SslContextFactory sslContextFactory) { this(server, null, null, null, -1, AbstractConnectionFactory.getFactories(sslContextFactory,connectionFactory)); } @Override public Object getTransport() { return this; } /** Sends requests and get responses based on thread activity. * Returns all the responses received once the thread activity has * returned to the level it was before the requests. * <p> * This methods waits until the connection is closed or * is idle for 5s before returning the responses. * <p>Use {@link #getResponse(String)} for an alternative that does not wait for idle. * @param requests the requests * @return the responses * @throws Exception if the requests fail * @deprecated Use {@link #getResponse(String)} */ @Deprecated public String getResponses(String requests) throws Exception { return getResponses(requests, 5, TimeUnit.SECONDS); } /** Sends requests and get responses based on thread activity. * Returns all the responses received once the thread activity has * returned to the level it was before the requests. * <p> * This methods waits until the connection is closed or * an idle period before returning the responses. * <p>Use {@link #getResponse(String)} for an alternative that does not wait for idle. * @param requests the requests * @param idleFor The time the response stream must be idle for before returning * @param units The units of idleFor * @return the responses * @throws Exception if the requests fail * @deprecated Use {@link #getResponse(String, boolean, long, TimeUnit)} */ @Deprecated public String getResponses(String requests,long idleFor,TimeUnit units) throws Exception { ByteBuffer result = getResponses(BufferUtil.toBuffer(requests,StandardCharsets.UTF_8),idleFor,units); return result==null?null:BufferUtil.toString(result,StandardCharsets.UTF_8); } /** Sends requests and get's responses based on thread activity. * Returns all the responses received once the thread activity has * returned to the level it was before the requests. * <p> * This methods waits until the connection is closed or * is idle for 5s before returning the responses. * <p>Use {@link #getResponse(ByteBuffer)} for an alternative that does not wait for idle. * @param requestsBuffer the requests * @return the responses * @throws Exception if the requests fail * @deprecated Use {@link #getResponse(ByteBuffer)} */ @Deprecated public ByteBuffer getResponses(ByteBuffer requestsBuffer) throws Exception { return getResponses(requestsBuffer, 5, TimeUnit.SECONDS); } /** Sends requests and get's responses based on thread activity. * Returns all the responses received once the thread activity has * returned to the level it was before the requests. * <p> * This methods waits until the connection is closed or * an idle period before returning the responses. * @param requestsBuffer the requests * @param idleFor The time the response stream must be idle for before returning * @param units The units of idleFor * @return the responses * @throws Exception if the requests fail * @deprecated Use {@link #getResponse(ByteBuffer, boolean, long, TimeUnit)} */ @Deprecated public ByteBuffer getResponses(ByteBuffer requestsBuffer,long idleFor,TimeUnit units) throws Exception { if (LOG.isDebugEnabled()) LOG.debug("requests {}", BufferUtil.toUTF8String(requestsBuffer)); LocalEndPoint endp = executeRequest(requestsBuffer); endp.waitUntilClosedOrIdleFor(idleFor,units); ByteBuffer responses = endp.takeOutput(); if (endp.isOutputShutdown()) endp.close(); if (LOG.isDebugEnabled()) LOG.debug("responses {}", BufferUtil.toUTF8String(responses)); return responses; } /** * Execute a request and return the EndPoint through which * multiple responses can be received or more input provided. * @param rawRequest the request * @return the local endpoint */ public LocalEndPoint executeRequest(String rawRequest) { return executeRequest(BufferUtil.toBuffer(rawRequest, StandardCharsets.UTF_8)); } private LocalEndPoint executeRequest(ByteBuffer rawRequest) { if (!isStarted()) throw new IllegalStateException("!STARTED"); LocalEndPoint endp = new LocalEndPoint(); endp.addInput(rawRequest); _connects.add(endp); return endp; } public LocalEndPoint connect() { LocalEndPoint endp = new LocalEndPoint(); _connects.add(endp); return endp; } @Override protected void accept(int acceptorID) throws IOException, InterruptedException { if (LOG.isDebugEnabled()) LOG.debug("accepting {}", acceptorID); LocalEndPoint endPoint = _connects.take(); endPoint.onOpen(); onEndPointOpened(endPoint); Connection connection = getDefaultConnectionFactory().newConnection(this, endPoint); endPoint.setConnection(connection); connection.onOpen(); } /** Get a single response using a parser to search for the end of the message. * @param requestsBuffer The request to send * @return ByteBuffer containing response or null. * @throws Exception If there is a problem */ public ByteBuffer getResponse(ByteBuffer requestsBuffer) throws Exception { return getResponse(requestsBuffer,false,10,TimeUnit.SECONDS); } /** Get a single response using a parser to search for the end of the message. * @param requestBuffer The request to send * @param time The time to wait * @param unit The units of the wait * @return ByteBuffer containing response or null. * @throws Exception If there is a problem */ public ByteBuffer getResponse(ByteBuffer requestBuffer, long time,TimeUnit unit) throws Exception { boolean head = BufferUtil.toString(requestBuffer).toLowerCase().startsWith("head "); if (LOG.isDebugEnabled()) LOG.debug("requests {}", BufferUtil.toUTF8String(requestBuffer)); LocalEndPoint endp = executeRequest(requestBuffer); return endp.waitForResponse(head,time,unit); } /** Get a single response using a parser to search for the end of the message. * @param requestBuffer The request to send * @param head True if the response is for a head request * @param time The time to wait * @param unit The units of the wait * @return ByteBuffer containing response or null. * @throws Exception If there is a problem */ public ByteBuffer getResponse(ByteBuffer requestBuffer,boolean head, long time,TimeUnit unit) throws Exception { if (LOG.isDebugEnabled()) LOG.debug("requests {}", BufferUtil.toUTF8String(requestBuffer)); LocalEndPoint endp = executeRequest(requestBuffer); return endp.waitForResponse(head,time,unit); } /** Get a single response using a parser to search for the end of the message. * @param rawRequest The request to send * @return ByteBuffer containing response or null. * @throws Exception If there is a problem */ public String getResponse(String rawRequest) throws Exception { return getResponse(rawRequest,false,30,TimeUnit.SECONDS); } /** Get a single response using a parser to search for the end of the message. * @param rawRequest The request to send * @param time The time to wait * @param unit The units of the wait * @return ByteBuffer containing response or null. * @throws Exception If there is a problem */ public String getResponse(String rawRequest,long time,TimeUnit unit) throws Exception { boolean head = rawRequest.toLowerCase().startsWith("head "); ByteBuffer requestsBuffer = BufferUtil.toBuffer(rawRequest, StandardCharsets.ISO_8859_1); if (LOG.isDebugEnabled()) LOG.debug("request {}", BufferUtil.toUTF8String(requestsBuffer)); LocalEndPoint endp = executeRequest(requestsBuffer); return BufferUtil.toString(endp.waitForResponse(head,time,unit), StandardCharsets.ISO_8859_1); } /** Get a single response using a parser to search for the end of the message. * @param rawRequest The request to send * @param head True if the response is for a head request * @param time The time to wait * @param unit The units of the wait * @return ByteBuffer containing response or null. * @throws Exception If there is a problem */ public String getResponse(String rawRequest, boolean head, long time,TimeUnit unit) throws Exception { ByteBuffer requestsBuffer = BufferUtil.toBuffer(rawRequest, StandardCharsets.ISO_8859_1); if (LOG.isDebugEnabled()) LOG.debug("request {}", BufferUtil.toUTF8String(requestsBuffer)); LocalEndPoint endp = executeRequest(requestsBuffer); return BufferUtil.toString(endp.waitForResponse(head,time,unit), StandardCharsets.ISO_8859_1); } /** Local EndPoint */ public class LocalEndPoint extends ByteArrayEndPoint { private final CountDownLatch _closed = new CountDownLatch(1); private ByteBuffer _responseData; public LocalEndPoint() { super(LocalConnector.this.getScheduler(), LocalConnector.this.getIdleTimeout()); setGrowOutput(true); } protected void execute(Runnable task) { getExecutor().execute(task); } @Override public void onClose() { getConnection().onClose(); LocalConnector.this.onEndPointClosed(this); super.onClose(); _closed.countDown(); } @Override public void doShutdownOutput() { super.shutdownOutput(); close(); } public void waitUntilClosed() { while (isOpen()) { try { if (!_closed.await(10,TimeUnit.SECONDS)) break; } catch(Exception e) { LOG.warn(e); } } } public void waitUntilClosedOrIdleFor(long idleFor,TimeUnit units) { Thread.yield(); int size=getOutput().remaining(); while (isOpen()) { try { if (!_closed.await(idleFor,units)) { if (size==getOutput().remaining()) { if (LOG.isDebugEnabled()) LOG.debug("idle for {} {}",idleFor,units); return; } size=getOutput().remaining(); } } catch(Exception e) { LOG.warn(e); } } } /** * Wait for a response using a parser to detect the end of message * @return Buffer containing full response or null for EOF; * @throws Exception if the response cannot be parsed */ public String getResponse() throws Exception { return getResponse(false,30,TimeUnit.SECONDS); } /** * Wait for a response using a parser to detect the end of message * @param head whether the request is a HEAD request * @param time the maximum time to wait * @param unit the time unit of the {@code timeout} argument * @return Buffer containing full response or null for EOF; * @throws Exception if the response cannot be parsed */ public String getResponse(boolean head, long time,TimeUnit unit) throws Exception { ByteBuffer response = waitForResponse(head,time,unit); if (response!=null) return BufferUtil.toString(response); return null; } /** * Wait for a response using a parser to detect the end of message * @param head whether the request is a HEAD request * @param time the maximum time to wait * @param unit the time unit of the {@code timeout} argument * @return Buffer containing full response or null for EOF; * @throws Exception if the response cannot be parsed */ public ByteBuffer waitForResponse(boolean head, long time,TimeUnit unit) throws Exception { HttpParser.ResponseHandler handler = new HttpParser.ResponseHandler() { @Override public void parsedHeader(HttpField field) { } @Override public boolean contentComplete() { return false; } @Override public boolean messageComplete() { return true; } @Override public boolean headerComplete() { return false; } @Override public int getHeaderCacheSize() { return 0; } @Override public void earlyEOF() { } @Override public boolean content(ByteBuffer item) { return false; } @Override public void badMessage(int status, String reason) { } @Override public boolean startResponse(HttpVersion version, int status, String reason) { return false; } }; HttpParser parser = new HttpParser(handler); parser.setHeadResponse(head); try(ByteArrayOutputStream2 bout = new ByteArrayOutputStream2();) { loop: while(true) { // read a chunk of response ByteBuffer chunk; if (BufferUtil.hasContent(_responseData)) chunk = _responseData; else { chunk = waitForOutput(time,unit); if (BufferUtil.isEmpty(chunk) && (!isOpen() || isOutputShutdown())) { parser.atEOF(); parser.parseNext(BufferUtil.EMPTY_BUFFER); break loop; } } // Parse the content of this chunk while (BufferUtil.hasContent(chunk)) { int pos=chunk.position(); boolean complete=parser.parseNext(chunk); if (chunk.position()==pos) { // Nothing consumed if (BufferUtil.isEmpty(chunk)) break; return null; } // Add all consumed bytes to the output stream bout.write(chunk.array(),chunk.arrayOffset()+pos,chunk.position()-pos); // If we are complete then break the outer loop if (complete) { if (BufferUtil.hasContent(chunk)) _responseData=chunk; break loop; } } } if (bout.getCount()==0 && isOutputShutdown()) return null; return ByteBuffer.wrap(bout.getBuf(),0,bout.getCount()); } } } }