| // |
| // ======================================================================== |
| // 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 |
| */ |
| 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 |
| */ |
| 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 |
| */ |
| 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 |
| */ |
| 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 close() |
| { |
| boolean wasOpen=isOpen(); |
| super.close(); |
| if (wasOpen) |
| { |
| getConnection().onClose(); |
| onClose(); |
| } |
| } |
| |
| @Override |
| public void onClose() |
| { |
| LocalConnector.this.onEndPointClosed(this); |
| super.onClose(); |
| _closed.countDown(); |
| } |
| |
| @Override |
| public void shutdownOutput() |
| { |
| 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()); |
| } |
| } |
| } |
| } |