Merged FastCGI implementation into 'master'.

This commit is contained in:
Simone Bordet 2014-03-13 15:36:03 +01:00
commit 5bf85701dc
51 changed files with 5866 additions and 0 deletions

323
LICENSE Normal file
View File

@ -0,0 +1,323 @@
CREATIVE COMMONS Attribution-NonCommercial-NoDerivs 3.0 Unported
Summary
Attribution — You must attribute the work in the manner specified
by the author or licensor (but not in any way that suggests that they
endorse you or your use of the work).
Noncommercial — You may not use this work for commercial purposes.
No Derivative Works — You may not alter, transform, or build upon
this work.
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL
SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN ATTORNEY-CLIENT
RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS"
BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE INFORMATION
PROVIDED, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM ITS USE.
License
THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE
COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY
COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN
AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.
BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE
TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE
MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS
CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND
CONDITIONS.
1. Definitions
"Adaptation" means a work based upon the Work, or upon the Work and other
pre-existing works, such as a translation, adaptation, derivative work,
arrangement of music or other alterations of a literary or artistic work,
or phonogram or performance and includes cinematographic adaptations or
any other form in which the Work may be recast, transformed, or adapted
including in any form recognizably derived from the original, except that
a work that constitutes a Collection will not be considered an Adaptation
for the purpose of this License. For the avoidance of doubt, where the
Work is a musical work, performance or phonogram, the synchronization
of the Work in timed-relation with a moving image ("synching") will be
considered an Adaptation for the purpose of this License.
"Collection" means a collection of literary or artistic works, such as
encyclopedias and anthologies, or performances, phonograms or broadcasts,
or other works or subject matter other than works listed in Section
1(f) below, which, by reason of the selection and arrangement of
their contents, constitute intellectual creations, in which the Work
is included in its entirety in unmodified form along with one or more
other contributions, each constituting separate and independent works
in themselves, which together are assembled into a collective whole. A
work that constitutes a Collection will not be considered an Adaptation
(as defined above) for the purposes of this License.
"Distribute" means to make available to the public the original and
copies of the Work through sale or other transfer of ownership.
"Licensor" means the individual, individuals, entity or entities that
offer(s) the Work under the terms of this License.
"Original Author" means, in the case of a literary or artistic work, the
individual, individuals, entity or entities who created the Work or if
no individual or entity can be identified, the publisher; and in addition
(i) in the case of a performance the actors, singers, musicians, dancers,
and other persons who act, sing, deliver, declaim, play in, interpret or
otherwise perform literary or artistic works or expressions of folklore;
(ii) in the case of a phonogram the producer being the person or legal
entity who first fixes the sounds of a performance or other sounds;
and, (iii) in the case of broadcasts, the organization that transmits
the broadcast.
"Work" means the literary and/or artistic work offered under the terms
of this License including without limitation any production in the
literary, scientific and artistic domain, whatever may be the mode or
form of its expression including digital form, such as a book, pamphlet
and other writing; a lecture, address, sermon or other work of the same
nature; a dramatic or dramatico-musical work; a choreographic work
or entertainment in dumb show; a musical composition with or without
words; a cinematographic work to which are assimilated works expressed
by a process analogous to cinematography; a work of drawing, painting,
architecture, sculpture, engraving or lithography; a photographic
work to which are assimilated works expressed by a process analogous
to photography; a work of applied art; an illustration, map, plan,
sketch or three-dimensional work relative to geography, topography,
architecture or science; a performance; a broadcast; a phonogram;
a compilation of data to the extent it is protected as a copyrightable
work; or a work performed by a variety or circus performer to the extent
it is not otherwise considered a literary or artistic work.
"You" means an individual or entity exercising rights under this License
who has not previously violated the terms of this License with respect
to the Work, or who has received express permission from the Licensor
to exercise rights under this License despite a previous violation.
"Publicly Perform" means to perform public recitations of the Work and
to communicate to the public those public recitations, by any means
or process, including by wire or wireless means or public digital
performances; to make available to the public Works in such a way that
members of the public may access these Works from a place and at a place
individually chosen by them; to perform the Work to the public by any
means or process and the communication to the public of the performances
of the Work, including by public digital performance; to broadcast and
rebroadcast the Work by any means including signs, sounds or images.
"Reproduce" means to make copies of the Work by any means including
without limitation by sound or visual recordings and the right of fixation
and reproducing fixations of the Work, including storage of a protected
performance or phonogram in digital form or other electronic medium.
2. Fair Dealing Rights. Nothing in this License is intended to reduce,
limit, or restrict any uses free from copyright or rights arising from
limitations or exceptions that are provided for in connection with the
copyright protection under copyright law or other applicable laws.
3. License Grant. Subject to the terms and conditions of this License,
Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
perpetual (for the duration of the applicable copyright) license to
exercise the rights in the Work as stated below:
to Reproduce the Work, to incorporate the Work into one or more
Collections, and
to Reproduce the Work as incorporated in the Collections;
and,
to Distribute and Publicly Perform the Work including as incorporated
in Collections.
The above rights may be exercised in all media and formats whether
now known or hereafter devised. The above rights include the right to
make such modifications as are technically necessary to exercise the
rights in other media and formats, but otherwise you have no rights to
make Adaptations. Subject to 8(f), all rights not expressly granted by
Licensor are hereby reserved, including but not limited to the rights
set forth in Section 4(d).
4. Restrictions. The license granted in Section 3 above is expressly
made subject to and limited by the following restrictions:
You may Distribute or Publicly Perform the Work only under the terms
of this License. You must include a copy of, or the Uniform Resource
Identifier (URI) for, this License with every copy of the Work You
Distribute or Publicly Perform. You may not offer or impose any terms on
the Work that restrict the terms of this License or the ability of the
recipient of the Work to exercise the rights granted to that recipient
under the terms of the License. You may not sublicense the Work. You
must keep intact all notices that refer to this License and to the
disclaimer of warranties with every copy of the Work You Distribute
or Publicly Perform. When You Distribute or Publicly Perform the Work,
You may not impose any effective technological measures on the Work that
restrict the ability of a recipient of the Work from You to exercise the
rights granted to that recipient under the terms of the License. This
Section 4(a) applies to the Work as incorporated in a Collection, but
this does not require the Collection apart from the Work itself to be
made subject to the terms of this License. If You create a Collection,
upon notice from any Licensor You must, to the extent practicable, remove
from the Collection any credit as required by Section 4(c), as requested.
You may not exercise any of the rights granted to You in Section 3
above in any manner that is primarily intended for or directed toward
commercial advantage or private monetary compensation. The exchange of
the Work for other copyrighted works by means of digital file-sharing or
otherwise shall not be considered to be intended for or directed toward
commercial advantage or private monetary compensation, provided there is
no payment of any monetary compensation in connection with the exchange
of copyrighted works.
If You Distribute, or Publicly Perform the Work or Collections, You must,
unless a request has been made pursuant to Section 4(a), keep intact all
copyright notices for the Work and provide, reasonable to the medium
or means You are utilizing: (i) the name of the Original Author (or
pseudonym, if applicable) if supplied, and/or if the Original Author
and/or Licensor designate another party or parties (e.g., a sponsor
institute, publishing entity, journal) for attribution ("Attribution
Parties") in Licensor's copyright notice, terms of service or by other
reasonable means, the name of such party or parties; (ii) the title of
the Work if supplied; (iii) to the extent reasonably practicable, the
URI, if any, that Licensor specifies to be associated with the Work,
unless such URI does not refer to the copyright notice or licensing
information for the Work. The credit required by this Section 4(c)
may be implemented in any reasonable manner; provided, however, that
in the case of a Collection, at a minimum such credit will appear,
if a credit for all contributing authors of Collection appears, then
as part of these credits and in a manner at least as prominent as the
credits for the other contributing authors. For the avoidance of doubt,
You may only use the credit required by this Section for the purpose of
attribution in the manner set out above and, by exercising Your rights
under this License, You may not implicitly or explicitly assert or imply
any connection with, sponsorship or endorsement by the Original Author,
Licensor and/or Attribution Parties, as appropriate, of You or Your use
of the Work, without the separate, express prior written permission of
the Original Author, Licensor and/or Attribution Parties.
For the avoidance of doubt:
Non-waivable Compulsory License Schemes. In those jurisdictions
in which the right to collect royalties through any statutory or
compulsory licensing scheme cannot be waived, the Licensor reserves
the exclusive right to collect such royalties for any exercise by
You of the rights granted under this License;
Waivable Compulsory License Schemes. In those jurisdictions in which
the right to collect royalties through any statutory or compulsory
licensing scheme can be waived, the Licensor reserves the exclusive
right to collect such royalties for any exercise by You of the rights
granted under this License if Your exercise of such rights is for
a purpose or use which is otherwise than noncommercial as permitted
under Section 4(b) and otherwise waives the right to collect royalties
through any statutory or compulsory licensing scheme; and,
Voluntary License Schemes. The Licensor reserves the right to collect
royalties, whether individually or, in the event that the Licensor is
a member of a collecting society that administers voluntary licensing
schemes, via that society, from any exercise by You of the rights
granted under this License that is for a purpose or use which is
otherwise than noncommercial as permitted under Section 4(b).
Except as otherwise agreed in writing by the Licensor or as may be
otherwise permitted by applicable law, if You Reproduce, Distribute or
Publicly Perform the Work either by itself or as part of any Collections,
You must not distort, mutilate, modify or take other derogatory action in
relation to the Work which would be prejudicial to the Original Author's
honor or reputation.
5. Representations, Warranties and Disclaimer
UNLESS OTHERWISE MUTUALLY AGREED BY THE PARTIES IN WRITING, LICENSOR
OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY
KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE,
INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY,
FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF
LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS,
WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION
OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE
LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR
ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES
ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR
HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
7. Termination
This License and the rights granted hereunder will terminate automatically
upon any breach by You of the terms of this License. Individuals or
entities who have received Collections from You under this License,
however, will not have their licenses terminated provided such individuals
or entities remain in full compliance with those licenses. Sections 1,
2, 5, 6, 7, and 8 will survive any termination of this License.
Subject to the above terms and conditions, the license granted here
is perpetual (for the duration of the applicable copyright in the
Work). Notwithstanding the above, Licensor reserves the right to release
the Work under different license terms or to stop distributing the Work
at any time; provided, however that any such election will not serve to
withdraw this License (or any other license that has been, or is required
to be, granted under the terms of this License), and this License will
continue in full force and effect unless terminated as stated above.
8. Miscellaneous
Each time You Distribute or Publicly Perform the Work or a Collection,
the Licensor offers to the recipient a license to the Work on the same
terms and conditions as the license granted to You under this License.
If any provision of this License is invalid or unenforceable under
applicable law, it shall not affect the validity or enforceability of
the remainder of the terms of this License, and without further action
by the parties to this agreement, such provision shall be reformed to
the minimum extent necessary to make such provision valid and enforceable.
No term or provision of this License shall be deemed waived and no breach
consented to unless such waiver or consent shall be in writing and signed
by the party to be charged with such waiver or consent.
This License constitutes the entire agreement between the parties
with respect to the Work licensed here. There are no understandings,
agreements or representations with respect to the Work not specified
here. Licensor shall not be bound by any additional provisions that may
appear in any communication from You. This License may not be modified
without the mutual written agreement of the Licensor and You.
The rights granted under, and the subject matter referenced, in this
License were drafted utilizing the terminology of the Berne Convention for
the Protection of Literary and Artistic Works (as amended on September 28,
1979), the Rome Convention of 1961, the WIPO Copyright Treaty of 1996,
the WIPO Performances and Phonograms Treaty of 1996 and the Universal
Copyright Convention (as revised on July 24, 1971). These rights and
subject matter take effect in the relevant jurisdiction in which the
License terms are sought to be enforced according to the corresponding
provisions of the implementation of those treaty provisions in the
applicable national law. If the standard suite of rights granted under
applicable copyright law includes additional rights not granted under
this License, such additional rights are deemed to be included in the
License; this License is not intended to restrict the license of any
rights under applicable law.
Creative Commons Notice
Creative Commons is not a party to this License, and makes no warranty
whatsoever in connection with the Work. Creative Commons will not
be liable to You or any party on any legal theory for any damages
whatsoever, including without limitation any general, special,
incidental or consequential damages arising in connection to this
license. Notwithstanding the foregoing two (2) sentences, if Creative
Commons has expressly identified itself as the Licensor hereunder,
it shall have all rights and obligations of Licensor.
Except for the limited purpose of indicating to the public that the Work
is licensed under the CCPL, Creative Commons does not authorize the use by
either party of the trademark "Creative Commons" or any related trademark
or logo of Creative Commons without the prior written consent of Creative
Commons. Any permitted use will be in compliance with Creative Commons'
then-current trademark usage guidelines, as may be published on its
website or otherwise made available upon request from time to time. For
the avoidance of doubt, this trademark restriction does not form part
of this License.
Creative Commons may be contacted at http://creativecommons.org/.

4
README.md Normal file
View File

@ -0,0 +1,4 @@
jetty-fcgi
==========
Jetty FastCGI Support

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>org.eclipse.jetty.fcgi</groupId>
<artifactId>fcgi-parent</artifactId>
<version>1.0.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>fcgi-core</artifactId>
<name>Jetty :: FastCGI :: Core</name>
<properties>
<bundle-symbolic-name>${project.groupId}.core</bundle-symbolic-name>
</properties>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
<version>${jetty-version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-io</artifactId>
<version>${jetty-version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-http</artifactId>
<version>${jetty-version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,136 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi;
public class FCGI
{
private FCGI()
{
}
public enum Role
{
RESPONDER(1), AUTHORIZER(2), FILTER(3);
public static Role from(int code)
{
switch (code)
{
case 1:
return RESPONDER;
case 2:
return AUTHORIZER;
case 3:
return FILTER;
default:
throw new IllegalArgumentException();
}
}
public final int code;
private Role(int code)
{
this.code = code;
}
}
public enum FrameType
{
BEGIN_REQUEST(1),
ABORT_REQUEST(2),
END_REQUEST(3),
PARAMS(4),
STDIN(5),
STDOUT(6),
STDERR(7),
DATA(8),
GET_VALUES(9),
GET_VALUES_RESULT(10);
public static FrameType from(int code)
{
switch (code)
{
case 1:
return BEGIN_REQUEST;
case 2:
return ABORT_REQUEST;
case 3:
return END_REQUEST;
case 4:
return PARAMS;
case 5:
return STDIN;
case 6:
return STDOUT;
case 7:
return STDERR;
case 8:
return DATA;
case 9:
return GET_VALUES;
case 10:
return GET_VALUES_RESULT;
default:
throw new IllegalArgumentException();
}
}
public final int code;
private FrameType(int code)
{
this.code = code;
}
}
public enum StreamType
{
STD_IN, STD_OUT, STD_ERR
}
public static class Headers
{
public static final String AUTH_TYPE = "AUTH_TYPE";
public static final String CONTENT_LENGTH = "CONTENT_LENGTH";
public static final String CONTENT_TYPE = "CONTENT_TYPE";
public static final String DOCUMENT_ROOT = "DOCUMENT_ROOT";
public static final String DOCUMENT_URI = "DOCUMENT_URI";
public static final String GATEWAY_INTERFACE = "GATEWAY_INTERFACE";
public static final String HTTPS = "HTTPS";
public static final String PATH_INFO = "PATH_INFO";
public static final String QUERY_STRING = "QUERY_STRING";
public static final String REMOTE_ADDR = "REMOTE_ADDR";
public static final String REMOTE_PORT = "REMOTE_PORT";
public static final String REQUEST_METHOD = "REQUEST_METHOD";
public static final String REQUEST_URI = "REQUEST_URI";
public static final String SCRIPT_FILENAME = "SCRIPT_FILENAME";
public static final String SCRIPT_NAME = "SCRIPT_NAME";
public static final String SERVER_ADDR = "SERVER_ADDR";
public static final String SERVER_NAME = "SERVER_NAME";
public static final String SERVER_PORT = "SERVER_PORT";
public static final String SERVER_PROTOCOL = "SERVER_PROTOCOL";
public static final String SERVER_SOFTWARE = "SERVER_SOFTWARE";
private Headers()
{
}
}
}

View File

@ -0,0 +1,166 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.generator;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jetty.fcgi.FCGI;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
public class ClientGenerator extends Generator
{
// To keep the algorithm simple, and given that the max length of a
// frame is 0xFF_FF we allow the max length of a name (or value) to be
// 0x7F_FF - 4 (the 4 is to make room for the name (or value) length).
public static final int MAX_PARAM_LENGTH = 0x7F_FF - 4;
public ClientGenerator(ByteBufferPool byteBufferPool)
{
super(byteBufferPool);
}
public Result generateRequestHeaders(int request, HttpFields fields, Callback callback)
{
request &= 0xFF_FF;
Charset utf8 = Charset.forName("UTF-8");
List<byte[]> bytes = new ArrayList<>(fields.size() * 2);
int fieldsLength = 0;
for (HttpField field : fields)
{
String name = field.getName();
byte[] nameBytes = name.getBytes(utf8);
if (nameBytes.length > MAX_PARAM_LENGTH)
throw new IllegalArgumentException("Field name " + name + " exceeds max length " + MAX_PARAM_LENGTH);
bytes.add(nameBytes);
String value = field.getValue();
byte[] valueBytes = value.getBytes(utf8);
if (valueBytes.length > MAX_PARAM_LENGTH)
throw new IllegalArgumentException("Field value " + value + " exceeds max length " + MAX_PARAM_LENGTH);
bytes.add(valueBytes);
int nameLength = nameBytes.length;
fieldsLength += bytesForLength(nameLength);
int valueLength = valueBytes.length;
fieldsLength += bytesForLength(valueLength);
fieldsLength += nameLength;
fieldsLength += valueLength;
}
// Worst case FCGI_PARAMS frame: long name + long value - both of MAX_PARAM_LENGTH
int maxCapacity = 4 + 4 + 2 * MAX_PARAM_LENGTH;
// One FCGI_BEGIN_REQUEST + N FCGI_PARAMS + one last FCGI_PARAMS
ByteBuffer beginRequestBuffer = byteBufferPool.acquire(16, false);
BufferUtil.clearToFill(beginRequestBuffer);
Result result = new Result(byteBufferPool, callback);
result = result.append(beginRequestBuffer, true);
// Generate the FCGI_BEGIN_REQUEST frame
beginRequestBuffer.putInt(0x01_01_00_00 + request);
beginRequestBuffer.putInt(0x00_08_00_00);
// Hardcode RESPONDER role and KEEP_ALIVE flag
beginRequestBuffer.putLong(0x00_01_01_00_00_00_00_00L);
beginRequestBuffer.flip();
int index = 0;
while (fieldsLength > 0)
{
int capacity = 8 + Math.min(maxCapacity, fieldsLength);
ByteBuffer buffer = byteBufferPool.acquire(capacity, true);
BufferUtil.clearToFill(buffer);
result = result.append(buffer, true);
// Generate the FCGI_PARAMS frame
buffer.putInt(0x01_04_00_00 + request);
buffer.putShort((short)0);
buffer.putShort((short)0);
capacity -= 8;
int length = 0;
while (index < bytes.size())
{
byte[] nameBytes = bytes.get(index);
int nameLength = nameBytes.length;
byte[] valueBytes = bytes.get(index + 1);
int valueLength = valueBytes.length;
int required = bytesForLength(nameLength) + bytesForLength(valueLength) + nameLength + valueLength;
if (required > capacity)
break;
putParamLength(buffer, nameLength);
putParamLength(buffer, valueLength);
buffer.put(nameBytes);
buffer.put(valueBytes);
length += required;
fieldsLength -= required;
capacity -= required;
index += 2;
}
buffer.putShort(4, (short)length);
buffer.flip();
}
ByteBuffer lastParamsBuffer = byteBufferPool.acquire(8, false);
BufferUtil.clearToFill(lastParamsBuffer);
result = result.append(lastParamsBuffer, true);
// Generate the last FCGI_PARAMS frame
lastParamsBuffer.putInt(0x01_04_00_00 + request);
lastParamsBuffer.putInt(0x00_00_00_00);
lastParamsBuffer.flip();
return result;
}
private int putParamLength(ByteBuffer buffer, int length)
{
int result = bytesForLength(length);
if (result == 4)
buffer.putInt(length | 0x80_00_00_00);
else
buffer.put((byte)length);
return result;
}
private int bytesForLength(int length)
{
return length > 127 ? 4 : 1;
}
public Result generateRequestContent(int request, ByteBuffer content, boolean lastContent, Callback callback)
{
return generateContent(request, content, false, lastContent, callback, FCGI.FrameType.STDIN);
}
}

View File

@ -0,0 +1,144 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.generator;
import java.nio.ByteBuffer;
import java.util.Queue;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.ConcurrentArrayQueue;
import org.eclipse.jetty.util.IteratingCallback;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
public class Flusher
{
private static final Logger LOG = Log.getLogger(Flusher.class);
private final Queue<Generator.Result> queue = new ConcurrentArrayQueue<>();
private final IteratingCallback flushCallback = new FlushCallback();
private final EndPoint endPoint;
public Flusher(EndPoint endPoint)
{
this.endPoint = endPoint;
}
public void flush(Generator.Result... results)
{
for (Generator.Result result : results)
queue.offer(result);
flushCallback.iterate();
}
public void shutdown()
{
flush(new ShutdownResult());
}
private class FlushCallback extends IteratingCallback
{
private Generator.Result active;
@Override
protected Action process() throws Exception
{
// Look if other writes are needed.
Generator.Result result = queue.poll();
if (result == null)
{
// No more writes to do, return.
return Action.IDLE;
}
// Attempt to gather another result.
// Most often there is another result in the
// queue so this is a real optimization because
// it sends both results in just one TCP packet.
Generator.Result other = queue.poll();
if (other != null)
result = result.join(other);
active = result;
ByteBuffer[] buffers = result.getByteBuffers();
endPoint.write(this, buffers);
return Action.SCHEDULED;
}
@Override
protected void completed()
{
// We never return Action.SUCCEEDED, so this method is never called.
throw new IllegalStateException();
}
@Override
public void succeeded()
{
if (active != null)
active.succeeded();
active = null;
super.succeeded();
}
@Override
public void failed(Throwable x)
{
if (active != null)
active.failed(x);
active = null;
while (true)
{
Generator.Result result = queue.poll();
if (result == null)
break;
result.failed(x);
}
super.failed(x);
}
}
private class ShutdownResult extends Generator.Result
{
private ShutdownResult()
{
super(null, null);
}
@Override
public void succeeded()
{
shutdown();
}
@Override
public void failed(Throwable x)
{
shutdown();
}
private void shutdown()
{
LOG.debug("Shutting down {}", endPoint);
endPoint.shutdownOutput();
}
}
}

View File

@ -0,0 +1,155 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.generator;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jetty.fcgi.FCGI;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
public class Generator
{
public static final int MAX_CONTENT_LENGTH = 0xFF_FF;
protected final ByteBufferPool byteBufferPool;
public Generator(ByteBufferPool byteBufferPool)
{
this.byteBufferPool = byteBufferPool;
}
protected Result generateContent(int id, ByteBuffer content, boolean recycle, boolean lastContent, Callback callback, FCGI.FrameType frameType)
{
id &= 0xFF_FF;
int contentLength = content == null ? 0 : content.remaining();
Result result = new Result(byteBufferPool, callback);
while (contentLength > 0 || lastContent)
{
ByteBuffer buffer = byteBufferPool.acquire(8, false);
BufferUtil.clearToFill(buffer);
result = result.append(buffer, true);
// Generate the frame header
buffer.put((byte)0x01);
buffer.put((byte)frameType.code);
buffer.putShort((short)id);
int length = Math.min(MAX_CONTENT_LENGTH, contentLength);
buffer.putShort((short)length);
buffer.putShort((short)0);
buffer.flip();
if (contentLength == 0)
break;
// Slice the content to avoid copying
int limit = content.limit();
content.limit(content.position() + length);
ByteBuffer slice = content.slice();
// Don't recycle the slice
result = result.append(slice, false);
content.position(content.limit());
content.limit(limit);
contentLength -= length;
// Recycle the content buffer if needed
if (recycle && contentLength == 0)
result = result.append(content, true);
}
return result;
}
public static class Result implements Callback
{
private final List<Callback> callbacks = new ArrayList<>(2);
private final List<ByteBuffer> buffers = new ArrayList<>(8);
private final List<Boolean> recycles = new ArrayList<>(8);
private final ByteBufferPool byteBufferPool;
public Result(ByteBufferPool byteBufferPool, Callback callback)
{
this.byteBufferPool = byteBufferPool;
this.callbacks.add(callback);
}
public Result append(ByteBuffer buffer, boolean recycle)
{
if (buffer != null)
{
buffers.add(buffer);
recycles.add(recycle);
}
return this;
}
public Result join(Result that)
{
callbacks.addAll(that.callbacks);
buffers.addAll(that.buffers);
recycles.addAll(that.recycles);
return this;
}
public ByteBuffer[] getByteBuffers()
{
return buffers.toArray(new ByteBuffer[buffers.size()]);
}
@Override
@SuppressWarnings("ForLoopReplaceableByForEach")
public void succeeded()
{
recycle();
for (int i = 0; i < callbacks.size(); ++i)
{
Callback callback = callbacks.get(i);
if (callback != null)
callback.succeeded();
}
}
@Override
@SuppressWarnings("ForLoopReplaceableByForEach")
public void failed(Throwable x)
{
recycle();
for (int i = 0; i < callbacks.size(); ++i)
{
Callback callback = callbacks.get(i);
if (callback != null)
callback.failed(x);
}
}
protected void recycle()
{
for (int i = 0; i < buffers.size(); ++i)
{
ByteBuffer buffer = buffers.get(i);
if (recycles.get(i))
byteBufferPool.release(buffer);
}
}
}
}

View File

@ -0,0 +1,108 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.generator;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jetty.fcgi.FCGI;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
public class ServerGenerator extends Generator
{
private static final byte[] STATUS = new byte[]{'S', 't', 'a', 't', 'u', 's'};
private static final byte[] COLON = new byte[]{':', ' '};
private static final byte[] EOL = new byte[]{'\r', '\n'};
public ServerGenerator(ByteBufferPool byteBufferPool)
{
super(byteBufferPool);
}
public Result generateResponseHeaders(int request, int code, String reason, HttpFields fields, Callback callback)
{
request &= 0xFF_FF;
Charset utf8 = Charset.forName("UTF-8");
List<byte[]> bytes = new ArrayList<>(fields.size() * 2);
int length = 0;
// Special 'Status' header
bytes.add(STATUS);
length += STATUS.length + COLON.length;
if (reason == null)
reason = HttpStatus.getMessage(code);
byte[] responseBytes = (code + " " + reason).getBytes(utf8);
bytes.add(responseBytes);
length += responseBytes.length + EOL.length;
// Other headers
for (HttpField field : fields)
{
String name = field.getName();
byte[] nameBytes = name.getBytes(utf8);
bytes.add(nameBytes);
String value = field.getValue();
byte[] valueBytes = value.getBytes(utf8);
bytes.add(valueBytes);
length += nameBytes.length + COLON.length;
length += valueBytes.length + EOL.length;
}
// End of headers
length += EOL.length;
final ByteBuffer buffer = byteBufferPool.acquire(length, true);
BufferUtil.clearToFill(buffer);
for (int i = 0; i < bytes.size(); i += 2)
buffer.put(bytes.get(i)).put(COLON).put(bytes.get(i + 1)).put(EOL);
buffer.put(EOL);
buffer.flip();
return generateContent(request, buffer, true, false, callback, FCGI.FrameType.STDOUT);
}
public Result generateResponseContent(int request, ByteBuffer content, boolean lastContent, Callback callback)
{
Result result = generateContent(request, content, false, lastContent, callback, FCGI.FrameType.STDOUT);
if (lastContent)
{
// Generate the FCGI_END_REQUEST
request &= 0xFF_FF;
ByteBuffer endRequestBuffer = byteBufferPool.acquire(8, false);
BufferUtil.clearToFill(endRequestBuffer);
endRequestBuffer.putInt(0x01_03_00_00 + request);
endRequestBuffer.putInt(0x00_08_00_00);
endRequestBuffer.putLong(0x00L);
endRequestBuffer.flip();
result = result.append(endRequestBuffer, true);
}
return result;
}
}

View File

@ -0,0 +1,127 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.parser;
import java.nio.ByteBuffer;
import org.eclipse.jetty.fcgi.FCGI;
public class BeginRequestContentParser extends ContentParser
{
private final ServerParser.Listener listener;
private State state = State.ROLE;
private int cursor;
private int role;
private int flags;
public BeginRequestContentParser(HeaderParser headerParser, ServerParser.Listener listener)
{
super(headerParser);
this.listener = listener;
}
@Override
public boolean parse(ByteBuffer buffer)
{
while (buffer.hasRemaining())
{
switch (state)
{
case ROLE:
{
if (buffer.remaining() >= 2)
{
role = buffer.getShort();
state = State.FLAGS;
}
else
{
state = State.ROLE_BYTES;
cursor = 0;
}
break;
}
case ROLE_BYTES:
{
int halfShort = buffer.get() & 0xFF;
role = (role << 8) + halfShort;
if (++cursor == 2)
state = State.FLAGS;
break;
}
case FLAGS:
{
flags = buffer.get() & 0xFF;
state = State.RESERVED;
break;
}
case RESERVED:
{
if (buffer.remaining() >= 5)
{
buffer.position(buffer.position() + 5);
onStart();
reset();
return true;
}
else
{
state = State.RESERVED_BYTES;
cursor = 0;
break;
}
}
case RESERVED_BYTES:
{
buffer.get();
if (++cursor == 5)
{
onStart();
reset();
return true;
}
break;
}
default:
{
throw new IllegalStateException();
}
}
}
return false;
}
private void onStart()
{
listener.onStart(getRequest(), FCGI.Role.from(role), flags);
}
private void reset()
{
state = State.ROLE;
cursor = 0;
role = 0;
flags = 0;
}
private enum State
{
ROLE, ROLE_BYTES, FLAGS, RESERVED, RESERVED_BYTES
}
}

View File

@ -0,0 +1,102 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.parser;
import java.nio.ByteBuffer;
import java.util.EnumMap;
import org.eclipse.jetty.fcgi.FCGI;
import org.eclipse.jetty.http.HttpField;
public class ClientParser extends Parser
{
private final EnumMap<FCGI.FrameType, ContentParser> contentParsers = new EnumMap<>(FCGI.FrameType.class);
public ClientParser(Listener listener)
{
ResponseContentParser stdOutParser = new ResponseContentParser(headerParser, listener);
contentParsers.put(FCGI.FrameType.STDOUT, stdOutParser);
StreamContentParser stdErrParser = new StreamContentParser(headerParser, FCGI.StreamType.STD_ERR, listener);
contentParsers.put(FCGI.FrameType.STDERR, stdErrParser);
contentParsers.put(FCGI.FrameType.END_REQUEST, new EndRequestContentParser(headerParser, new EndRequestListener(listener, stdOutParser, stdErrParser)));
}
@Override
protected ContentParser findContentParser(FCGI.FrameType frameType)
{
return contentParsers.get(frameType);
}
public interface Listener extends Parser.Listener
{
public void onBegin(int request, int code, String reason);
public static class Adapter extends Parser.Listener.Adapter implements Listener
{
@Override
public void onBegin(int request, int code, String reason)
{
}
}
}
private class EndRequestListener implements Listener
{
private final Listener listener;
private final StreamContentParser[] streamParsers;
private EndRequestListener(Listener listener, StreamContentParser... streamParsers)
{
this.listener = listener;
this.streamParsers = streamParsers;
}
@Override
public void onBegin(int request, int code, String reason)
{
listener.onBegin(request, code, reason);
}
@Override
public void onHeader(int request, HttpField field)
{
listener.onHeader(request, field);
}
@Override
public void onHeaders(int request)
{
listener.onHeaders(request);
}
@Override
public void onContent(int request, FCGI.StreamType stream, ByteBuffer buffer)
{
listener.onContent(request, stream, buffer);
}
@Override
public void onEnd(int request)
{
listener.onEnd(request);
for (StreamContentParser streamParser : streamParsers)
streamParser.end(request);
}
}
}

View File

@ -0,0 +1,48 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.parser;
import java.nio.ByteBuffer;
public abstract class ContentParser
{
private final HeaderParser headerParser;
protected ContentParser(HeaderParser headerParser)
{
this.headerParser = headerParser;
}
public abstract boolean parse(ByteBuffer buffer);
public void noContent()
{
throw new IllegalStateException();
}
protected int getRequest()
{
return headerParser.getRequest();
}
protected int getContentLength()
{
return headerParser.getContentLength();
}
}

View File

@ -0,0 +1,126 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.parser;
import java.nio.ByteBuffer;
public class EndRequestContentParser extends ContentParser
{
private final Parser.Listener listener;
private State state = State.APPLICATION;
private int cursor;
private int application;
private int protocol;
public EndRequestContentParser(HeaderParser headerParser, Parser.Listener listener)
{
super(headerParser);
this.listener = listener;
}
@Override
public boolean parse(ByteBuffer buffer)
{
while (buffer.hasRemaining())
{
switch (state)
{
case APPLICATION:
{
if (buffer.remaining() >= 4)
{
application = buffer.getInt();
state = State.PROTOCOL;
}
else
{
state = State.APPLICATION_BYTES;
cursor = 0;
}
break;
}
case APPLICATION_BYTES:
{
int quarterInt = buffer.get() & 0xFF;
application = (application << 8) + quarterInt;
if (++cursor == 4)
state = State.PROTOCOL;
break;
}
case PROTOCOL:
{
protocol = buffer.get() & 0xFF;
state = State.RESERVED;
break;
}
case RESERVED:
{
if (buffer.remaining() >= 3)
{
buffer.position(buffer.position() + 3);
onEnd();
reset();
return true;
}
else
{
state = State.APPLICATION_BYTES;
cursor = 0;
break;
}
}
case RESERVED_BYTES:
{
buffer.get();
if (++cursor == 0)
{
onEnd();
reset();
return true;
}
break;
}
default:
{
throw new IllegalStateException();
}
}
}
return false;
}
private void onEnd()
{
// TODO: if protocol != 0, invoke an error callback
listener.onEnd(getRequest());
}
private void reset()
{
state = State.APPLICATION;
cursor = 0;
application = 0;
protocol = 0;
}
private enum State
{
APPLICATION, APPLICATION_BYTES, PROTOCOL, RESERVED, RESERVED_BYTES
}
}

View File

@ -0,0 +1,152 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.parser;
import java.nio.ByteBuffer;
import org.eclipse.jetty.fcgi.FCGI;
public class HeaderParser
{
private State state = State.VERSION;
private int cursor;
private int version;
private int type;
private int request;
private int length;
private int padding;
public boolean parse(ByteBuffer buffer)
{
while (buffer.hasRemaining())
{
switch (state)
{
case VERSION:
{
version = buffer.get() & 0xFF;
state = State.TYPE;
break;
}
case TYPE:
{
type = buffer.get() & 0xFF;
state = State.REQUEST;
break;
}
case REQUEST:
{
if (buffer.remaining() >= 2)
{
request = buffer.getShort() & 0xFF_FF;
state = State.LENGTH;
}
else
{
state = State.REQUEST_BYTES;
cursor = 0;
}
break;
}
case REQUEST_BYTES:
{
int halfShort = buffer.get() & 0xFF;
request = (request << 8) + halfShort;
if (++cursor == 2)
state = State.LENGTH;
break;
}
case LENGTH:
{
if (buffer.remaining() >= 2)
{
length = buffer.getShort() & 0xFF_FF;
state = State.PADDING;
}
else
{
state = State.LENGTH_BYTES;
cursor = 0;
}
break;
}
case LENGTH_BYTES:
{
int halfShort = buffer.get() & 0xFF;
length = (length << 8) + halfShort;
if (++cursor == 2)
state = State.PADDING;
break;
}
case PADDING:
{
padding = buffer.get() & 0xFF;
state = State.RESERVED;
break;
}
case RESERVED:
{
buffer.get();
return true;
}
default:
{
throw new IllegalStateException();
}
}
}
return false;
}
public FCGI.FrameType getFrameType()
{
return FCGI.FrameType.from(type);
}
public int getRequest()
{
return request;
}
public int getContentLength()
{
return length;
}
public int getPaddingLength()
{
return padding;
}
protected void reset()
{
state = State.VERSION;
cursor = 0;
version = 0;
type = 0;
request = 0;
length = 0;
padding = 0;
}
private enum State
{
VERSION, TYPE, REQUEST, REQUEST_BYTES, LENGTH, LENGTH_BYTES, PADDING, RESERVED
}
}

View File

@ -0,0 +1,257 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.parser;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
public class ParamsContentParser extends ContentParser
{
private static final Logger logger = Log.getLogger(ParamsContentParser.class);
private final ServerParser.Listener listener;
private State state = State.LENGTH;
private int cursor;
private int length;
private int nameLength;
private int valueLength;
private byte[] nameBytes;
private byte[] valueBytes;
public ParamsContentParser(HeaderParser headerParser, ServerParser.Listener listener)
{
super(headerParser);
this.listener = listener;
}
@Override
public boolean parse(ByteBuffer buffer)
{
while (buffer.hasRemaining() || state == State.PARAM)
{
switch (state)
{
case LENGTH:
{
length = getContentLength();
state = State.NAME_LENGTH;
break;
}
case NAME_LENGTH:
{
if (isLargeLength(buffer))
{
if (buffer.remaining() >= 4)
{
nameLength = buffer.getInt() & 0x7F_FF;
state = State.VALUE_LENGTH;
length -= 4;
}
else
{
state = State.NAME_LENGTH_BYTES;
cursor = 0;
}
}
else
{
nameLength = buffer.get() & 0xFF;
state = State.VALUE_LENGTH;
--length;
}
break;
}
case NAME_LENGTH_BYTES:
{
int quarterInt = buffer.get() & 0xFF;
nameLength = (nameLength << 8) + quarterInt;
--length;
if (++cursor == 4)
{
nameLength &= 0x7F_FF;
state = State.VALUE_LENGTH;
}
break;
}
case VALUE_LENGTH:
{
if (isLargeLength(buffer))
{
if (buffer.remaining() >= 4)
{
valueLength = buffer.getInt() & 0x7F_FF;
state = State.NAME;
length -= 4;
}
else
{
state = State.VALUE_LENGTH_BYTES;
cursor = 0;
}
}
else
{
valueLength = buffer.get() & 0xFF;
state = State.NAME;
--length;
}
break;
}
case VALUE_LENGTH_BYTES:
{
int quarterInt = buffer.get() & 0xFF;
valueLength = (valueLength << 8) + quarterInt;
--length;
if (++cursor == 4)
{
valueLength &= 0x7F_FF;
state = State.NAME;
}
break;
}
case NAME:
{
nameBytes = new byte[nameLength];
if (buffer.remaining() >= nameLength)
{
buffer.get(nameBytes);
state = State.VALUE;
length -= nameLength;
}
else
{
state = State.NAME_BYTES;
cursor = 0;
}
break;
}
case NAME_BYTES:
{
nameBytes[cursor] = buffer.get();
--length;
if (++cursor == nameLength)
state = State.VALUE;
break;
}
case VALUE:
{
valueBytes = new byte[valueLength];
if (buffer.remaining() >= valueLength)
{
buffer.get(valueBytes);
state = State.PARAM;
length -= valueLength;
}
else
{
state = State.VALUE_BYTES;
cursor = 0;
}
break;
}
case VALUE_BYTES:
{
valueBytes[cursor] = buffer.get();
--length;
if (++cursor == valueLength)
state = State.PARAM;
break;
}
case PARAM:
{
Charset utf8 = Charset.forName("UTF-8");
onParam(new String(nameBytes, utf8), new String(valueBytes, utf8));
partialReset();
if (length == 0)
{
reset();
return true;
}
break;
}
default:
{
throw new IllegalStateException();
}
}
}
return false;
}
@Override
public void noContent()
{
onParams();
}
protected void onParam(String name, String value)
{
try
{
listener.onHeader(getRequest(), new HttpField(name, value));
}
catch (Throwable x)
{
logger.debug("Exception while invoking listener " + listener, x);
}
}
protected void onParams()
{
try
{
listener.onHeaders(getRequest());
}
catch (Throwable x)
{
logger.debug("Exception while invoking listener " + listener, x);
}
}
private boolean isLargeLength(ByteBuffer buffer)
{
return (buffer.get(buffer.position()) & 0x80) == 0x80;
}
private void partialReset()
{
state = State.NAME_LENGTH;
cursor = 0;
nameLength = 0;
valueLength = 0;
nameBytes = null;
valueBytes = null;
}
private void reset()
{
partialReset();
state = State.LENGTH;
length = 0;
}
private enum State
{
LENGTH, NAME_LENGTH, NAME_LENGTH_BYTES, VALUE_LENGTH, VALUE_LENGTH_BYTES, NAME, NAME_BYTES, VALUE, VALUE_BYTES, PARAM
}
}

View File

@ -0,0 +1,131 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.parser;
import java.nio.ByteBuffer;
import org.eclipse.jetty.fcgi.FCGI;
import org.eclipse.jetty.http.HttpField;
public abstract class Parser
{
protected final HeaderParser headerParser = new HeaderParser();
private State state = State.HEADER;
private int padding;
public void parse(ByteBuffer buffer)
{
while (true)
{
switch (state)
{
case HEADER:
{
if (!headerParser.parse(buffer))
return;
state = State.CONTENT;
break;
}
case CONTENT:
{
ContentParser contentParser = findContentParser(headerParser.getFrameType());
if (headerParser.getContentLength() == 0)
{
contentParser.noContent();
}
else
{
if (!contentParser.parse(buffer))
return;
}
padding = headerParser.getPaddingLength();
state = State.PADDING;
break;
}
case PADDING:
{
if (buffer.remaining() >= padding)
{
buffer.position(buffer.position() + padding);
reset();
break;
}
else
{
padding -= buffer.remaining();
buffer.position(buffer.limit());
return;
}
}
default:
{
throw new IllegalStateException();
}
}
}
}
protected abstract ContentParser findContentParser(FCGI.FrameType frameType);
private void reset()
{
headerParser.reset();
state = State.HEADER;
padding = 0;
}
public interface Listener
{
public void onHeader(int request, HttpField field);
public void onHeaders(int request);
public void onContent(int request, FCGI.StreamType stream, ByteBuffer buffer);
public void onEnd(int request);
public static class Adapter implements Listener
{
@Override
public void onHeader(int request, HttpField field)
{
}
@Override
public void onHeaders(int request)
{
}
@Override
public void onContent(int request, FCGI.StreamType stream, ByteBuffer buffer)
{
}
@Override
public void onEnd(int request)
{
}
}
}
private enum State
{
HEADER, CONTENT, PADDING
}
}

View File

@ -0,0 +1,328 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.parser;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jetty.fcgi.FCGI;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpParser;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
public class ResponseContentParser extends StreamContentParser
{
private static final Logger LOG = Log.getLogger(ResponseContentParser.class);
private final Map<Integer, ResponseParser> parsers = new ConcurrentHashMap<>();
private final ClientParser.Listener listener;
public ResponseContentParser(HeaderParser headerParser, ClientParser.Listener listener)
{
super(headerParser, FCGI.StreamType.STD_OUT, listener);
this.listener = listener;
}
@Override
public void noContent()
{
// Does nothing, since for responses the end of content is signaled via a FCGI_END_REQUEST frame
}
@Override
protected void onContent(ByteBuffer buffer)
{
int request = getRequest();
ResponseParser parser = parsers.get(request);
if (parser == null)
{
parser = new ResponseParser(listener, request);
parsers.put(request, parser);
}
parser.parse(buffer);
}
@Override
protected void end(int request)
{
super.end(request);
parsers.remove(request);
}
private class ResponseParser implements HttpParser.ResponseHandler<ByteBuffer>
{
private final HttpFields fields = new HttpFields();
private ClientParser.Listener listener;
private final int request;
private final FCGIHttpParser httpParser;
private State state = State.HEADERS;
private boolean seenResponseCode;
private ResponseParser(ClientParser.Listener listener, int request)
{
this.listener = listener;
this.request = request;
this.httpParser = new FCGIHttpParser(this);
}
public void parse(ByteBuffer buffer)
{
LOG.debug("Response {} {} content {} {}", request, FCGI.StreamType.STD_OUT, state, buffer);
int remaining = buffer.remaining();
while (remaining > 0)
{
switch (state)
{
case HEADERS:
{
if (httpParser.parseHeaders(buffer))
state = State.CONTENT_MODE;
remaining = buffer.remaining();
break;
}
case CONTENT_MODE:
{
// If we have no indication of the content, then
// the HTTP parser will assume there is no content
// and will not parse it even if it is provided,
// so we have to parse it raw ourselves here.
boolean rawContent = fields.size() == 0 ||
(fields.get(HttpHeader.CONTENT_LENGTH) == null &&
fields.get(HttpHeader.TRANSFER_ENCODING) == null);
state = rawContent ? State.RAW_CONTENT : State.HTTP_CONTENT;
break;
}
case RAW_CONTENT:
{
notifyContent(buffer);
remaining = 0;
break;
}
case HTTP_CONTENT:
{
httpParser.parseContent(buffer);
remaining = buffer.remaining();
break;
}
default:
{
throw new IllegalStateException();
}
}
}
}
@Override
public int getHeaderCacheSize()
{
// TODO: configure this
return 0;
}
@Override
public boolean startResponse(HttpVersion version, int status, String reason)
{
// The HTTP request line does not exist in FCGI responses
throw new IllegalStateException();
}
@Override
public boolean parsedHeader(HttpField httpField)
{
try
{
String name = httpField.getName();
if ("Status".equalsIgnoreCase(name))
{
if (!seenResponseCode)
{
seenResponseCode = true;
// Need to set the response status so the
// HttpParser can handle the content properly.
String[] parts = httpField.getValue().split(" ");
int code = Integer.parseInt(parts[0]);
httpParser.setResponseStatus(code);
String reason = parts.length > 1 ? parts[1] : HttpStatus.getMessage(code);
notifyBegin(code, reason);
notifyHeaders(fields);
}
}
else
{
if (seenResponseCode)
notifyHeader(httpField);
else
fields.add(httpField);
}
}
catch (Throwable x)
{
logger.debug("Exception while invoking listener " + listener, x);
}
return false;
}
private void notifyBegin(int code, String reason)
{
try
{
listener.onBegin(request, code, reason);
}
catch (Throwable x)
{
logger.debug("Exception while invoking listener " + listener, x);
}
}
private void notifyHeader(HttpField httpField)
{
try
{
listener.onHeader(request, httpField);
}
catch (Throwable x)
{
logger.debug("Exception while invoking listener " + listener, x);
}
}
private void notifyHeaders(HttpFields fields)
{
if (fields != null)
{
for (HttpField field : fields)
notifyHeader(field);
}
}
private void notifyHeaders()
{
try
{
listener.onHeaders(request);
}
catch (Throwable x)
{
logger.debug("Exception while invoking listener " + listener, x);
}
}
@Override
public boolean headerComplete()
{
if (!seenResponseCode)
{
// No Status header but we have other headers, assume 200 OK
notifyBegin(200, "OK");
notifyHeaders(fields);
}
notifyHeaders();
// Return from parsing so that we can parse the content
return true;
}
@Override
public boolean content(ByteBuffer buffer)
{
notifyContent(buffer);
return false;
}
private void notifyContent(ByteBuffer buffer)
{
try
{
listener.onContent(request, FCGI.StreamType.STD_OUT, buffer);
}
catch (Throwable x)
{
logger.debug("Exception while invoking listener " + listener, x);
}
}
@Override
public boolean messageComplete()
{
// Return from parsing so that we can parse the next headers or the raw content.
// No need to notify the listener because it will be done by FCGI_END_REQUEST.
return true;
}
@Override
public void earlyEOF()
{
// TODO
}
@Override
public void badMessage(int status, String reason)
{
// TODO
}
}
// Methods overridden to make them visible here
private static class FCGIHttpParser extends HttpParser
{
private FCGIHttpParser(ResponseHandler<ByteBuffer> handler)
{
super(handler, 65 * 1024, true);
reset();
}
@Override
public void reset()
{
super.reset();
setState(State.HEADER);
}
@Override
protected boolean parseHeaders(ByteBuffer buffer)
{
return super.parseHeaders(buffer);
}
@Override
protected boolean parseContent(ByteBuffer buffer)
{
return super.parseContent(buffer);
}
@Override
protected void setResponseStatus(int status)
{
super.setResponseStatus(status);
}
}
private enum State
{
HEADERS, CONTENT_MODE, RAW_CONTENT, HTTP_CONTENT
}
}

View File

@ -0,0 +1,54 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.parser;
import java.util.EnumMap;
import org.eclipse.jetty.fcgi.FCGI;
public class ServerParser extends Parser
{
private final EnumMap<FCGI.FrameType, ContentParser> contentParsers = new EnumMap<>(FCGI.FrameType.class);
public ServerParser(Listener listener)
{
contentParsers.put(FCGI.FrameType.BEGIN_REQUEST, new BeginRequestContentParser(headerParser, listener));
contentParsers.put(FCGI.FrameType.PARAMS, new ParamsContentParser(headerParser, listener));
contentParsers.put(FCGI.FrameType.STDIN, new StreamContentParser(headerParser, FCGI.StreamType.STD_IN, listener));
}
@Override
protected ContentParser findContentParser(FCGI.FrameType frameType)
{
return contentParsers.get(frameType);
}
public interface Listener extends Parser.Listener
{
public void onStart(int request, FCGI.Role role, int flags);
public static class Adapter extends Parser.Listener.Adapter implements Listener
{
@Override
public void onStart(int request, FCGI.Role role, int flags)
{
}
}
}
}

View File

@ -0,0 +1,113 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.parser;
import java.nio.ByteBuffer;
import org.eclipse.jetty.fcgi.FCGI;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
public class StreamContentParser extends ContentParser
{
protected static final Logger logger = Log.getLogger(StreamContentParser.class);
private final FCGI.StreamType streamType;
private final Parser.Listener listener;
private State state = State.LENGTH;
private int contentLength;
public StreamContentParser(HeaderParser headerParser, FCGI.StreamType streamType, Parser.Listener listener)
{
super(headerParser);
this.streamType = streamType;
this.listener = listener;
}
@Override
public boolean parse(ByteBuffer buffer)
{
while (buffer.hasRemaining())
{
switch (state)
{
case LENGTH:
{
contentLength = getContentLength();
state = State.CONTENT;
break;
}
case CONTENT:
{
int length = Math.min(contentLength, buffer.remaining());
int limit = buffer.limit();
buffer.limit(buffer.position() + length);
ByteBuffer slice = buffer.slice();
onContent(slice);
buffer.position(buffer.limit());
buffer.limit(limit);
contentLength -= length;
if (contentLength > 0)
break;
state = State.LENGTH;
return true;
}
default:
{
throw new IllegalStateException();
}
}
}
return false;
}
@Override
public void noContent()
{
try
{
listener.onEnd(getRequest());
}
catch (Throwable x)
{
logger.debug("Exception while invoking listener " + listener, x);
}
}
protected void onContent(ByteBuffer buffer)
{
try
{
listener.onContent(getRequest(), streamType, buffer);
}
catch (Throwable x)
{
logger.debug("Exception while invoking listener " + listener, x);
}
}
protected void end(int request)
{
}
private enum State
{
LENGTH, CONTENT
}
}

View File

@ -0,0 +1,190 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.generator;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jetty.fcgi.FCGI;
import org.eclipse.jetty.fcgi.parser.ServerParser;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.MappedByteBufferPool;
import org.junit.Assert;
import org.junit.Test;
public class ClientGeneratorTest
{
@Test
public void testGenerateRequestHeaders() throws Exception
{
HttpFields fields = new HttpFields();
// Short name, short value
final String shortShortName = "REQUEST_METHOD";
final String shortShortValue = "GET";
fields.put(new HttpField(shortShortName, shortShortValue));
// Short name, long value
final String shortLongName = "REQUEST_URI";
// Be sure it's longer than 127 chars to test the large value
final String shortLongValue = "/api/0.6/map?bbox=-64.217736,-31.456810,-64.187736,-31.432322,filler=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
fields.put(new HttpField(shortLongName, shortLongValue));
// Long name, short value
// Be sure it's longer than 127 chars to test the large name
final String longShortName = "FEDCBA9876543210FEDCBA9876543210FEDCBA9876543210FEDCBA9876543210FEDCBA9876543210FEDCBA9876543210FEDCBA9876543210FEDCBA9876543210";
final String longShortValue = "api.openstreetmap.org";
fields.put(new HttpField(longShortName, longShortValue));
// Long name, long value
char[] chars = new char[ClientGenerator.MAX_PARAM_LENGTH];
Arrays.fill(chars, 'z');
final String longLongName = new String(chars);
final String longLongValue = new String(chars);
fields.put(new HttpField(longLongName, longLongValue));
ByteBufferPool byteBufferPool = new MappedByteBufferPool();
ClientGenerator generator = new ClientGenerator(byteBufferPool);
final int id = 13;
Generator.Result result = generator.generateRequestHeaders(id, fields, null);
// Use the fundamental theorem of arithmetic to test the results.
// This way we know onHeader() has been called the right number of
// times with the right arguments, and so onHeaders().
final int[] primes = new int[]{2, 3, 5, 7, 11};
int value = 1;
for (int prime : primes)
value *= prime;
final AtomicInteger params = new AtomicInteger(1);
ServerParser parser = new ServerParser(new ServerParser.Listener.Adapter()
{
@Override
public void onHeader(int request, HttpField field)
{
Assert.assertEquals(id, request);
switch (field.getName())
{
case shortShortName:
Assert.assertEquals(shortShortValue, field.getValue());
params.set(params.get() * primes[0]);
break;
case shortLongName:
Assert.assertEquals(shortLongValue, field.getValue());
params.set(params.get() * primes[1]);
break;
case longShortName:
Assert.assertEquals(longShortValue, field.getValue());
params.set(params.get() * primes[2]);
break;
default:
Assert.assertEquals(longLongName, field.getName());
Assert.assertEquals(longLongValue, field.getValue());
params.set(params.get() * primes[3]);
break;
}
}
@Override
public void onHeaders(int request)
{
Assert.assertEquals(id, request);
params.set(params.get() * primes[4]);
}
});
for (ByteBuffer buffer : result.getByteBuffers())
{
parser.parse(buffer);
Assert.assertFalse(buffer.hasRemaining());
}
Assert.assertEquals(value, params.get());
// Parse again byte by byte
params.set(1);
for (ByteBuffer buffer : result.getByteBuffers())
{
buffer.flip();
while (buffer.hasRemaining())
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
Assert.assertFalse(buffer.hasRemaining());
}
Assert.assertEquals(value, params.get());
}
@Test
public void testGenerateSmallRequestContent() throws Exception
{
testGenerateRequestContent(1024);
}
@Test
public void testGenerateLargeRequestContent() throws Exception
{
testGenerateRequestContent(128 * 1024);
}
private void testGenerateRequestContent(final int contentLength) throws Exception
{
ByteBuffer content = ByteBuffer.allocate(contentLength);
ByteBufferPool byteBufferPool = new MappedByteBufferPool();
ClientGenerator generator = new ClientGenerator(byteBufferPool);
final int id = 13;
Generator.Result result = generator.generateRequestContent(id, content, true, null);
final AtomicInteger totalLength = new AtomicInteger();
ServerParser parser = new ServerParser(new ServerParser.Listener.Adapter()
{
@Override
public void onContent(int request, FCGI.StreamType stream, ByteBuffer buffer)
{
Assert.assertEquals(id, request);
totalLength.addAndGet(buffer.remaining());
}
@Override
public void onEnd(int request)
{
Assert.assertEquals(id, request);
Assert.assertEquals(contentLength, totalLength.get());
}
});
for (ByteBuffer buffer : result.getByteBuffers())
{
parser.parse(buffer);
Assert.assertFalse(buffer.hasRemaining());
}
// Parse again one byte at a time
for (ByteBuffer buffer : result.getByteBuffers())
{
buffer.flip();
while (buffer.hasRemaining())
parser.parse(ByteBuffer.wrap(new byte[]{buffer.get()}));
Assert.assertFalse(buffer.hasRemaining());
}
}
}

View File

@ -0,0 +1,252 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.parser;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jetty.fcgi.FCGI;
import org.eclipse.jetty.fcgi.generator.Generator;
import org.eclipse.jetty.fcgi.generator.ServerGenerator;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.MappedByteBufferPool;
import org.junit.Assert;
import org.junit.Test;
public class ClientParserTest
{
@Test
public void testParseResponseHeaders() throws Exception
{
final int id = 13;
HttpFields fields = new HttpFields();
final int statusCode = 200;
final String statusMessage = "OK";
final String contentTypeName = "Content-Type";
final String contentTypeValue = "text/html;charset=utf-8";
fields.put(contentTypeName, contentTypeValue);
ByteBufferPool byteBufferPool = new MappedByteBufferPool();
ServerGenerator generator = new ServerGenerator(byteBufferPool);
Generator.Result result = generator.generateResponseHeaders(id, statusCode, statusMessage, fields, null);
// Use the fundamental theorem of arithmetic to test the results.
// This way we know onHeader() has been called the right number of
// times with the right arguments, and so onHeaders().
final int[] primes = new int[]{2, 3, 5};
int value = 1;
for (int prime : primes)
value *= prime;
final AtomicInteger params = new AtomicInteger(1);
ClientParser parser = new ClientParser(new ClientParser.Listener.Adapter()
{
@Override
public void onBegin(int request, int code, String reason)
{
Assert.assertEquals(statusCode, code);
Assert.assertEquals(statusMessage, reason);
params.set(params.get() * primes[0]);
}
@Override
public void onHeader(int request, HttpField field)
{
Assert.assertEquals(id, request);
switch (field.getName())
{
case contentTypeName:
Assert.assertEquals(contentTypeValue, field.getValue());
params.set(params.get() * primes[1]);
break;
default:
break;
}
}
@Override
public void onHeaders(int request)
{
Assert.assertEquals(id, request);
params.set(params.get() * primes[2]);
}
});
for (ByteBuffer buffer : result.getByteBuffers())
{
parser.parse(buffer);
Assert.assertFalse(buffer.hasRemaining());
}
Assert.assertEquals(value, params.get());
}
@Test
public void testParseNoResponseContent() throws Exception
{
final int id = 13;
HttpFields fields = new HttpFields();
fields.put("Content-Length", "0");
ByteBufferPool byteBufferPool = new MappedByteBufferPool();
ServerGenerator generator = new ServerGenerator(byteBufferPool);
Generator.Result result1 = generator.generateResponseHeaders(id, 200, "OK", fields, null);
Generator.Result result2 = generator.generateResponseContent(id, null, true, null);
final AtomicInteger verifier = new AtomicInteger();
ClientParser parser = new ClientParser(new ClientParser.Listener.Adapter()
{
@Override
public void onContent(int request, FCGI.StreamType stream, ByteBuffer buffer)
{
Assert.assertEquals(id, request);
verifier.addAndGet(2);
}
@Override
public void onEnd(int request)
{
Assert.assertEquals(id, request);
verifier.addAndGet(3);
}
});
for (ByteBuffer buffer : result1.getByteBuffers())
{
parser.parse(buffer);
Assert.assertFalse(buffer.hasRemaining());
}
for (ByteBuffer buffer : result2.getByteBuffers())
{
parser.parse(buffer);
Assert.assertFalse(buffer.hasRemaining());
}
Assert.assertEquals(3, verifier.get());
}
@Test
public void testParseSmallResponseContent() throws Exception
{
final int id = 13;
HttpFields fields = new HttpFields();
ByteBuffer content = ByteBuffer.wrap(new byte[1024]);
final int contentLength = content.remaining();
final int code = 200;
final String contentTypeName = "Content-Length";
final String contentTypeValue = String.valueOf(contentLength);
fields.put(contentTypeName, contentTypeValue);
ByteBufferPool byteBufferPool = new MappedByteBufferPool();
ServerGenerator generator = new ServerGenerator(byteBufferPool);
Generator.Result result1 = generator.generateResponseHeaders(id, code, "OK", fields, null);
Generator.Result result2 = generator.generateResponseContent(id, content, true, null);
final AtomicInteger verifier = new AtomicInteger();
ClientParser parser = new ClientParser(new ClientParser.Listener.Adapter()
{
@Override
public void onContent(int request, FCGI.StreamType stream, ByteBuffer buffer)
{
Assert.assertEquals(id, request);
Assert.assertEquals(contentLength, buffer.remaining());
verifier.addAndGet(2);
}
@Override
public void onEnd(int request)
{
Assert.assertEquals(id, request);
verifier.addAndGet(3);
}
});
for (ByteBuffer buffer : result1.getByteBuffers())
{
parser.parse(buffer);
Assert.assertFalse(buffer.hasRemaining());
}
for (ByteBuffer buffer : result2.getByteBuffers())
{
parser.parse(buffer);
Assert.assertFalse(buffer.hasRemaining());
}
Assert.assertEquals(5, verifier.get());
}
@Test
public void testParseLargeResponseContent() throws Exception
{
final int id = 13;
HttpFields fields = new HttpFields();
ByteBuffer content = ByteBuffer.wrap(new byte[128 * 1024]);
final int contentLength = content.remaining();
final int code = 200;
final String contentTypeName = "Content-Length";
final String contentTypeValue = String.valueOf(contentLength);
fields.put(contentTypeName, contentTypeValue);
ByteBufferPool byteBufferPool = new MappedByteBufferPool();
ServerGenerator generator = new ServerGenerator(byteBufferPool);
Generator.Result result1 = generator.generateResponseHeaders(id, code, "OK", fields, null);
Generator.Result result2 = generator.generateResponseContent(id, content, true, null);
final AtomicInteger totalLength = new AtomicInteger();
final AtomicBoolean verifier = new AtomicBoolean();
ClientParser parser = new ClientParser(new ClientParser.Listener.Adapter()
{
@Override
public void onContent(int request, FCGI.StreamType stream, ByteBuffer buffer)
{
Assert.assertEquals(id, request);
totalLength.addAndGet(buffer.remaining());
}
@Override
public void onEnd(int request)
{
Assert.assertEquals(id, request);
Assert.assertEquals(contentLength, totalLength.get());
verifier.set(true);
}
});
for (ByteBuffer buffer : result1.getByteBuffers())
{
parser.parse(buffer);
Assert.assertFalse(buffer.hasRemaining());
}
for (ByteBuffer buffer : result2.getByteBuffers())
{
parser.parse(buffer);
Assert.assertFalse(buffer.hasRemaining());
}
Assert.assertTrue(verifier.get());
}
}

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>fcgi-parent</artifactId>
<groupId>org.eclipse.jetty.fcgi</groupId>
<version>1.0.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>fcgi-distribution</artifactId>
<packaging>pom</packaging>
<name>Jetty :: FastCGI :: Distribution</name>
<properties>
<distribution-directory>${project.build.directory}/distribution</distribution-directory>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-jars</id>
<phase>generate-resources</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<includeGroupIds>org.eclipse.jetty.fcgi</includeGroupIds>
<excludeArtifactIds>fcgi-server</excludeArtifactIds>
<includeTypes>jar</includeTypes>
<outputDirectory>${distribution-directory}/lib/fcgi</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>assemble</id>
<phase>package</phase>
<goals>
<goal>assembly</goal>
</goals>
<configuration>
<finalName>jetty-fcgi-${project.version}</finalName>
<descriptors>
<descriptor>src/main/assembly/distribution.xml</descriptor>
</descriptors>
<tarLongFileMode>gnu</tarLongFileMode>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty.fcgi</groupId>
<artifactId>fcgi-proxy</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
<id>distribution</id>
<formats>
<format>tar.gz</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<directory>${project.basedir}/src/main/config/modules</directory>
<outputDirectory>/modules</outputDirectory>
<includes>
<include>*.mod</include>
</includes>
</fileSet>
<fileSet>
<directory>${distribution-directory}</directory>
<outputDirectory>/</outputDirectory>
<includes>
<include>lib/**</include>
</includes>
</fileSet>
</fileSets>
</assembly>

View File

@ -0,0 +1,16 @@
#
# FastCGI Module
#
[depend]
servlet
client
[lib]
lib/jetty-security-${jetty.version}.jar
lib/jetty-proxy-${jetty.version}.jar
lib/fcgi/*.jar
[ini-template]
## For configuration of FastCGI contexts, see
## TODO: documentation url here

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://www.eclipse.org/jetty/configure_9_0.dtd">
<Configure class="org.eclipse.jetty.servlet.ServletContextHandler">
<New id="root" class="java.lang.String">
<Arg>/var/www/wordpress-3.7.1</Arg>
</New>
<Set name="contextPath">/wp</Set>
<Set name="resourceBase"><Ref refid="root" /></Set>
<Set name="welcomeFiles">
<Array type="String"><Item>index.php</Item></Array>
</Set>
<Call name="addFilter">
<Arg>org.eclipse.jetty.fcgi.proxy.TryFilesFilter</Arg>
<Arg>/*</Arg>
<Arg>
<Call name="of" class="java.util.EnumSet">
<Arg><Get name="REQUEST" class="javax.servlet.DispatcherType" /></Arg>
</Call>
</Arg>
<Call name="setInitParameter">
<Arg>files</Arg>
<Arg>$path /index.php?p=$path</Arg>
</Call>
</Call>
<Call name="addServlet">
<Arg>
<New class="org.eclipse.jetty.servlet.ServletHolder">
<Arg>default</Arg>
<Arg>
<Call name="forName" class="java.lang.Class">
<Arg>org.eclipse.jetty.servlet.DefaultServlet</Arg>
</Call>
</Arg>
<Call name="setInitParameter">
<Arg>dirAllowed</Arg>
<Arg>false</Arg>
</Call>
</New>
</Arg>
<Arg>/</Arg>
</Call>
<Call name="addServlet">
<Arg>org.eclipse.jetty.fcgi.proxy.FastCGIProxyServlet</Arg>
<Arg>*.php</Arg>
<Call name="setInitParameter">
<Arg>proxyTo</Arg>
<Arg>http://localhost:9000</Arg>
</Call>
<Call name="setInitParameter">
<Arg>prefix</Arg>
<Arg>/</Arg>
</Call>
<Call name="setInitParameter">
<Arg>scriptRoot</Arg>
<Arg><Ref refid="root" /></Arg>
</Call>
<Call name="setInitParameter">
<Arg>scriptPattern</Arg>
<Arg>(.+?\\.php)</Arg>
</Call>
</Call>
</Configure>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.eclipse.jetty.fcgi</groupId>
<artifactId>fcgi-parent</artifactId>
<version>1.0.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>fcgi-http-client-transport</artifactId>
<name>Jetty :: FastCGI :: HTTP Client Transport</name>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty.fcgi</groupId>
<artifactId>fcgi-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-client</artifactId>
<version>${jetty-version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.fcgi</groupId>
<artifactId>fcgi-server</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-test-helper</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,167 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.client.http;
import java.nio.ByteBuffer;
import java.util.concurrent.TimeoutException;
import org.eclipse.jetty.client.HttpChannel;
import org.eclipse.jetty.client.HttpExchange;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.fcgi.generator.Flusher;
import org.eclipse.jetty.fcgi.generator.Generator;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.io.IdleTimeout;
public class HttpChannelOverFCGI extends HttpChannel
{
private final HttpConnectionOverFCGI connection;
private final Flusher flusher;
private final int request;
private final HttpSenderOverFCGI sender;
private final HttpReceiverOverFCGI receiver;
private final FCGIIdleTimeout idle;
private HttpVersion version;
public HttpChannelOverFCGI(final HttpConnectionOverFCGI connection, Flusher flusher, int request, long idleTimeout)
{
super(connection.getHttpDestination());
this.connection = connection;
this.flusher = flusher;
this.request = request;
this.sender = new HttpSenderOverFCGI(this);
this.receiver = new HttpReceiverOverFCGI(this);
this.idle = new FCGIIdleTimeout(connection, idleTimeout);
}
protected int getRequest()
{
return request;
}
@Override
public void send()
{
HttpExchange exchange = getHttpExchange();
if (exchange != null)
{
version = exchange.getRequest().getVersion();
sender.send(exchange);
}
}
@Override
public void proceed(HttpExchange exchange, Throwable failure)
{
sender.proceed(exchange, failure);
}
@Override
public boolean abort(Throwable cause)
{
sender.abort(cause);
return receiver.abort(cause);
}
protected void responseBegin(int code, String reason)
{
HttpExchange exchange = getHttpExchange();
if (exchange != null)
{
exchange.getResponse().version(version).status(code).reason(reason);
receiver.responseBegin(exchange);
}
}
protected void responseHeader(HttpField field)
{
HttpExchange exchange = getHttpExchange();
if (exchange != null)
receiver.responseHeader(exchange, field);
}
protected void responseHeaders()
{
HttpExchange exchange = getHttpExchange();
if (exchange != null)
receiver.responseHeaders(exchange);
}
protected void content(ByteBuffer buffer)
{
HttpExchange exchange = getHttpExchange();
if (exchange != null)
receiver.responseContent(exchange, buffer);
}
protected void responseSuccess()
{
HttpExchange exchange = getHttpExchange();
if (exchange != null)
receiver.responseSuccess(exchange);
}
@Override
public void exchangeTerminated(Result result)
{
super.exchangeTerminated(result);
idle.onClose();
boolean close = result.isFailed();
HttpFields responseHeaders = result.getResponse().getHeaders();
close |= responseHeaders.contains(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE.asString());
if (close)
connection.close();
else
connection.release();
}
protected void flush(Generator.Result... results)
{
flusher.flush(results);
}
private class FCGIIdleTimeout extends IdleTimeout
{
private final HttpConnectionOverFCGI connection;
public FCGIIdleTimeout(HttpConnectionOverFCGI connection, long idleTimeout)
{
super(connection.getHttpDestination().getHttpClient().getScheduler());
this.connection = connection;
setIdleTimeout(idleTimeout);
}
@Override
protected void onIdleExpired(TimeoutException timeout)
{
LOG.debug("Idle timeout for request {}", request);
abort(timeout);
}
@Override
public boolean isOpen()
{
return connection.getEndPoint().isOpen();
}
}
}

View File

@ -0,0 +1,84 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.client.http;
import java.io.IOException;
import java.util.Map;
import org.eclipse.jetty.client.AbstractHttpClientTransport;
import org.eclipse.jetty.client.HttpDestination;
import org.eclipse.jetty.client.Origin;
import org.eclipse.jetty.client.api.Connection;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.fcgi.FCGI;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.Promise;
public class HttpClientTransportOverFCGI extends AbstractHttpClientTransport
{
private final boolean multiplexed;
private final String scriptRoot;
public HttpClientTransportOverFCGI(String scriptRoot)
{
this(Math.max(1, Runtime.getRuntime().availableProcessors() / 2), false, scriptRoot);
}
public HttpClientTransportOverFCGI(int selectors, boolean multiplexed, String scriptRoot)
{
super(selectors);
this.multiplexed = multiplexed;
this.scriptRoot = scriptRoot;
}
public boolean isMultiplexed()
{
return multiplexed;
}
public String getScriptRoot()
{
return scriptRoot;
}
@Override
public HttpDestination newHttpDestination(Origin origin)
{
return isMultiplexed() ? new MultiplexHttpDestinationOverFCGI(getHttpClient(), origin)
: new HttpDestinationOverFCGI(getHttpClient(), origin);
}
@Override
public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map<String, Object> context) throws IOException
{
HttpDestination destination = (HttpDestination)context.get(HTTP_DESTINATION_CONTEXT_KEY);
HttpConnectionOverFCGI connection = new HttpConnectionOverFCGI(endPoint, destination);
LOG.debug("Created {}", connection);
@SuppressWarnings("unchecked")
Promise<Connection> promise = (Promise<Connection>)context.get(HTTP_CONNECTION_PROMISE_CONTEXT_KEY);
promise.succeeded(connection);
return connection;
}
protected void customize(Request request, HttpFields fastCGIHeaders)
{
fastCGIHeaders.put(FCGI.Headers.DOCUMENT_ROOT, getScriptRoot());
}
}

View File

@ -0,0 +1,324 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.client.http;
import java.io.EOFException;
import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpConnection;
import org.eclipse.jetty.client.HttpDestination;
import org.eclipse.jetty.client.HttpExchange;
import org.eclipse.jetty.client.PoolingHttpDestination;
import org.eclipse.jetty.client.api.Connection;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.fcgi.FCGI;
import org.eclipse.jetty.fcgi.generator.Flusher;
import org.eclipse.jetty.fcgi.parser.ClientParser;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
public class HttpConnectionOverFCGI extends AbstractConnection implements Connection
{
private static final Logger LOG = Log.getLogger(HttpConnectionOverFCGI.class);
private final LinkedList<Integer> requests = new LinkedList<>();
private final Map<Integer, HttpChannelOverFCGI> channels = new ConcurrentHashMap<>();
private final AtomicBoolean closed = new AtomicBoolean();
private final Flusher flusher;
private final HttpDestination destination;
private final Delegate delegate;
private final ClientParser parser;
public HttpConnectionOverFCGI(EndPoint endPoint, HttpDestination destination)
{
super(endPoint, destination.getHttpClient().getExecutor(), destination.getHttpClient().isDispatchIO());
this.flusher = new Flusher(endPoint);
this.destination = destination;
this.delegate = new Delegate(destination);
this.parser = new ClientParser(new ResponseListener());
requests.addLast(0);
}
public HttpDestination getHttpDestination()
{
return destination;
}
@Override
public void send(Request request, Response.CompleteListener listener)
{
delegate.send(request, listener);
}
protected void send(HttpExchange exchange)
{
delegate.send(exchange);
}
@Override
public void onOpen()
{
super.onOpen();
fillInterested();
}
@Override
public void onFillable()
{
EndPoint endPoint = getEndPoint();
HttpClient client = destination.getHttpClient();
ByteBufferPool bufferPool = client.getByteBufferPool();
ByteBuffer buffer = bufferPool.acquire(client.getResponseBufferSize(), true);
try
{
while (true)
{
int read = endPoint.fill(buffer);
if (LOG.isDebugEnabled()) // Avoid boxing of variable 'read'
LOG.debug("Read {} bytes from {}", read, endPoint);
if (read > 0)
{
parse(buffer);
}
else if (read == 0)
{
fillInterested();
break;
}
else
{
shutdown();
break;
}
}
}
catch (Exception x)
{
LOG.debug(x);
// TODO: fail and close ?
}
finally
{
bufferPool.release(buffer);
}
}
private void parse(ByteBuffer buffer)
{
while (buffer.hasRemaining())
parser.parse(buffer);
}
private void shutdown()
{
// First close then abort, to be sure that the
// connection cannot be reused from an onFailure()
// handler or by blocking code waiting for completion.
close();
for (HttpChannelOverFCGI channel : channels.values())
channel.abort(new EOFException());
}
@Override
protected boolean onReadTimeout()
{
for (HttpChannelOverFCGI channel : channels.values())
channel.abort(new TimeoutException());
close();
return false;
}
public void release()
{
if (destination instanceof PoolingHttpDestination)
{
@SuppressWarnings("unchecked")
PoolingHttpDestination<HttpConnectionOverFCGI> fcgiDestination =
(PoolingHttpDestination<HttpConnectionOverFCGI>)destination;
fcgiDestination.release(this);
}
}
@Override
public void close()
{
if (closed.compareAndSet(false, true))
{
getHttpDestination().close(this);
getEndPoint().shutdownOutput();
LOG.debug("{} oshut", this);
getEndPoint().close();
LOG.debug("{} closed", this);
}
}
private int acquireRequest()
{
synchronized (requests)
{
int last = requests.getLast();
int request = last + 1;
requests.addLast(request);
return request;
}
}
private void releaseRequest(int request)
{
synchronized (requests)
{
requests.removeFirstOccurrence(request);
}
}
@Override
public String toString()
{
return String.format("%s@%h(l:%s <-> r:%s)",
getClass().getSimpleName(),
this,
getEndPoint().getLocalAddress(),
getEndPoint().getRemoteAddress());
}
private class Delegate extends HttpConnection
{
private Delegate(HttpDestination destination)
{
super(destination);
}
@Override
protected void send(HttpExchange exchange)
{
Request request = exchange.getRequest();
normalizeRequest(request);
// FCGI may be multiplexed, so create one channel for each request.
int id = acquireRequest();
HttpChannelOverFCGI channel = new HttpChannelOverFCGI(HttpConnectionOverFCGI.this, flusher, id, request.getIdleTimeout());
channels.put(id, channel);
channel.associate(exchange);
channel.send();
}
@Override
public void close()
{
HttpConnectionOverFCGI.this.close();
}
@Override
public String toString()
{
return HttpConnectionOverFCGI.this.toString();
}
}
private class ResponseListener implements ClientParser.Listener
{
@Override
public void onBegin(int request, int code, String reason)
{
HttpChannelOverFCGI channel = channels.get(request);
if (channel != null)
channel.responseBegin(code, reason);
else
noChannel(request);
}
@Override
public void onHeader(int request, HttpField field)
{
HttpChannelOverFCGI channel = channels.get(request);
if (channel != null)
channel.responseHeader(field);
else
noChannel(request);
}
@Override
public void onHeaders(int request)
{
HttpChannelOverFCGI channel = channels.get(request);
if (channel != null)
channel.responseHeaders();
else
noChannel(request);
}
@Override
public void onContent(int request, FCGI.StreamType stream, ByteBuffer buffer)
{
switch (stream)
{
case STD_OUT:
{
HttpChannelOverFCGI channel = channels.get(request);
if (channel != null)
channel.content(buffer);
else
noChannel(request);
break;
}
case STD_ERR:
{
LOG.info(BufferUtil.toUTF8String(buffer));
break;
}
default:
{
throw new IllegalArgumentException();
}
}
}
@Override
public void onEnd(int request)
{
HttpChannelOverFCGI channel = channels.remove(request);
if (channel != null)
{
channel.responseSuccess();
releaseRequest(request);
}
else
{
noChannel(request);
}
}
private void noChannel(int request)
{
// TODO: what here ?
}
}
}

View File

@ -0,0 +1,20 @@
package org.eclipse.jetty.fcgi.client.http;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpExchange;
import org.eclipse.jetty.client.Origin;
import org.eclipse.jetty.client.PoolingHttpDestination;
public class HttpDestinationOverFCGI extends PoolingHttpDestination<HttpConnectionOverFCGI>
{
public HttpDestinationOverFCGI(HttpClient client, Origin origin)
{
super(client, origin);
}
@Override
protected void send(HttpConnectionOverFCGI connection, HttpExchange exchange)
{
connection.send(exchange);
}
}

View File

@ -0,0 +1,70 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.client.http;
import java.nio.ByteBuffer;
import org.eclipse.jetty.client.HttpChannel;
import org.eclipse.jetty.client.HttpExchange;
import org.eclipse.jetty.client.HttpReceiver;
import org.eclipse.jetty.http.HttpField;
public class HttpReceiverOverFCGI extends HttpReceiver
{
public HttpReceiverOverFCGI(HttpChannel channel)
{
super(channel);
}
@Override
protected boolean responseBegin(HttpExchange exchange)
{
return super.responseBegin(exchange);
}
@Override
protected boolean responseHeader(HttpExchange exchange, HttpField field)
{
return super.responseHeader(exchange, field);
}
@Override
protected boolean responseHeaders(HttpExchange exchange)
{
return super.responseHeaders(exchange);
}
@Override
protected boolean responseContent(HttpExchange exchange, ByteBuffer buffer)
{
return super.responseContent(exchange, buffer);
}
@Override
protected boolean responseSuccess(HttpExchange exchange)
{
return super.responseSuccess(exchange);
}
@Override
protected boolean responseFailure(Throwable failure)
{
return super.responseFailure(failure);
}
}

View File

@ -0,0 +1,110 @@
package org.eclipse.jetty.fcgi.client.http;
import java.net.URI;
import java.util.Locale;
import org.eclipse.jetty.client.HttpChannel;
import org.eclipse.jetty.client.HttpContent;
import org.eclipse.jetty.client.HttpExchange;
import org.eclipse.jetty.client.HttpSender;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.fcgi.FCGI;
import org.eclipse.jetty.fcgi.generator.ClientGenerator;
import org.eclipse.jetty.fcgi.generator.Generator;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Jetty;
public class HttpSenderOverFCGI extends HttpSender
{
private final ClientGenerator generator;
public HttpSenderOverFCGI(HttpChannel channel)
{
super(channel);
this.generator = new ClientGenerator(channel.getHttpDestination().getHttpClient().getByteBufferPool());
}
@Override
protected HttpChannelOverFCGI getHttpChannel()
{
return (HttpChannelOverFCGI)super.getHttpChannel();
}
@Override
protected void sendHeaders(HttpExchange exchange, HttpContent content, Callback callback)
{
Request request = exchange.getRequest();
// Copy the request headers to be able to convert them properly
HttpFields headers = new HttpFields();
for (HttpField field : request.getHeaders())
headers.put(field);
HttpFields fcgiHeaders = new HttpFields();
// FastCGI headers based on the URI
URI uri = request.getURI();
String path = uri.getRawPath();
fcgiHeaders.put(FCGI.Headers.DOCUMENT_URI, path);
String query = uri.getRawQuery();
fcgiHeaders.put(FCGI.Headers.QUERY_STRING, query == null ? "" : query);
// FastCGI headers based on HTTP headers
HttpField httpField = headers.remove(HttpHeader.AUTHORIZATION);
if (httpField != null)
fcgiHeaders.put(FCGI.Headers.AUTH_TYPE, httpField.getValue());
httpField = headers.remove(HttpHeader.CONTENT_LENGTH);
fcgiHeaders.put(FCGI.Headers.CONTENT_LENGTH, httpField == null ? "" : httpField.getValue());
httpField = headers.remove(HttpHeader.CONTENT_TYPE);
fcgiHeaders.put(FCGI.Headers.CONTENT_TYPE, httpField == null ? "" : httpField.getValue());
// FastCGI headers that are not based on HTTP headers nor URI
fcgiHeaders.put(FCGI.Headers.REQUEST_METHOD, request.getMethod());
fcgiHeaders.put(FCGI.Headers.SERVER_PROTOCOL, request.getVersion().asString());
fcgiHeaders.put(FCGI.Headers.GATEWAY_INTERFACE, "CGI/1.1");
fcgiHeaders.put(FCGI.Headers.SERVER_SOFTWARE, "Jetty/" + Jetty.VERSION);
// Translate remaining HTTP header into the HTTP_* format
for (HttpField field : headers)
{
String name = field.getName();
String fcgiName = "HTTP_" + name.replaceAll("-", "_").toUpperCase(Locale.ENGLISH);
fcgiHeaders.add(fcgiName, field.getValue());
}
// Give a chance to the transport implementation to customize the FastCGI headers
HttpClientTransportOverFCGI transport = (HttpClientTransportOverFCGI)getHttpChannel().getHttpDestination().getHttpClient().getTransport();
transport.customize(request, fcgiHeaders);
int id = getHttpChannel().getRequest();
boolean hasContent = content.hasContent();
Generator.Result headersResult = generator.generateRequestHeaders(id, fcgiHeaders,
hasContent ? callback : new Callback.Adapter());
if (hasContent)
{
getHttpChannel().flush(headersResult);
}
else
{
Generator.Result noContentResult = generator.generateRequestContent(id, BufferUtil.EMPTY_BUFFER, true, callback);
getHttpChannel().flush(headersResult, noContentResult);
}
}
@Override
protected void sendContent(HttpExchange exchange, HttpContent content, Callback callback)
{
if (content.isConsumed())
{
callback.succeeded();
}
else
{
int request = getHttpChannel().getRequest();
Generator.Result result = generator.generateRequestContent(request, content.getByteBuffer(), content.isLast(), callback);
getHttpChannel().flush(result);
}
}
}

View File

@ -0,0 +1,38 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.client.http;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpExchange;
import org.eclipse.jetty.client.MultiplexHttpDestination;
import org.eclipse.jetty.client.Origin;
public class MultiplexHttpDestinationOverFCGI extends MultiplexHttpDestination<HttpConnectionOverFCGI>
{
public MultiplexHttpDestinationOverFCGI(HttpClient client, Origin origin)
{
super(client, origin);
}
@Override
protected void send(HttpConnectionOverFCGI connection, HttpExchange exchange)
{
connection.send(exchange);
}
}

View File

@ -0,0 +1,122 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.client.http;
import java.util.concurrent.atomic.AtomicLong;
import org.eclipse.jetty.client.ConnectionPool;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpDestination;
import org.eclipse.jetty.client.LeakTrackingConnectionPool;
import org.eclipse.jetty.client.Origin;
import org.eclipse.jetty.fcgi.server.ServerFCGIConnectionFactory;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.io.ArrayByteBufferPool;
import org.eclipse.jetty.io.LeakTrackingByteBufferPool;
import org.eclipse.jetty.io.MappedByteBufferPool;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.toolchain.test.TestTracker;
import org.eclipse.jetty.util.LeakDetector;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.junit.After;
import org.junit.Assert;
import org.junit.Rule;
public abstract class AbstractHttpClientServerTest
{
@Rule
public final TestTracker tracker = new TestTracker();
private final AtomicLong leaks = new AtomicLong();
protected Server server;
protected ServerConnector connector;
protected HttpClient client;
protected String scheme = HttpScheme.HTTP.asString();
public void start(Handler handler) throws Exception
{
server = new Server();
ServerFCGIConnectionFactory fcgiConnectionFactory = new ServerFCGIConnectionFactory(new HttpConfiguration());
connector = new ServerConnector(server, null, null,
new LeakTrackingByteBufferPool(new ArrayByteBufferPool())
{
@Override
protected void leaked(LeakDetector.LeakInfo leakInfo)
{
leaks.incrementAndGet();
}
}, 1, Math.max(1, Runtime.getRuntime().availableProcessors() / 2), fcgiConnectionFactory);
// connector.setPort(9000);
server.addConnector(connector);
server.setHandler(handler);
server.start();
QueuedThreadPool executor = new QueuedThreadPool();
executor.setName(executor.getName() + "-client");
client = new HttpClient(new HttpClientTransportOverFCGI(1, false, "")
{
@Override
public HttpDestination newHttpDestination(Origin origin)
{
return new HttpDestinationOverFCGI(client, origin)
{
@Override
protected ConnectionPool newConnectionPool(HttpClient client)
{
return new LeakTrackingConnectionPool(this, client.getMaxConnectionsPerDestination(), this)
{
@Override
protected void leaked(LeakDetector.LeakInfo leakInfo)
{
leaks.incrementAndGet();
}
};
}
};
}
}, null);
client.setExecutor(executor);
client.setByteBufferPool(new LeakTrackingByteBufferPool(new MappedByteBufferPool())
{
@Override
protected void leaked(LeakDetector.LeakInfo leakInfo)
{
leaks.incrementAndGet();
}
});
client.start();
}
@After
public void dispose() throws Exception
{
System.gc();
Assert.assertEquals(0, leaks.get());
if (client != null)
client.stop();
if (server != null)
server.stop();
}
}

View File

@ -0,0 +1,36 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.client.http;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler;
public class EmptyServerHandler extends AbstractHandler
{
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
baseRequest.setHandled(true);
}
}

View File

@ -0,0 +1,54 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.client.http;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
public class ExternalFastCGIServerTest
{
@Test
@Ignore("Relies on an external server")
public void testExternalFastCGIServer() throws Exception
{
// Assume a FastCGI server is listening on localhost:9000
HttpClient client = new HttpClient(new HttpClientTransportOverFCGI("/var/www/php-fcgi"), null);
client.start();
ContentResponse response = client.newRequest("localhost", 9000)
.path("/index.php")
.timeout(5, TimeUnit.SECONDS)
.send();
Assert.assertEquals(200, response.getStatus());
Path responseFile = Paths.get(System.getProperty("java.io.tmpdir"), "fcgi_response.html");
Files.write(responseFile, response.getContent(), StandardOpenOption.CREATE, StandardOpenOption.WRITE);
}
}

View File

@ -0,0 +1,516 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.client.http;
import java.io.EOFException;
import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.GZIPOutputStream;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.util.BytesContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.toolchain.test.IO;
import org.eclipse.jetty.toolchain.test.annotation.Slow;
import org.junit.Assert;
import org.junit.Test;
public class HttpClientTest extends AbstractHttpClientServerTest
{
@Test
public void testGETResponseWithoutContent() throws Exception
{
start(new EmptyServerHandler());
for (int i = 0; i < 2; ++i)
{
Response response = client.GET(scheme + "://localhost:" + connector.getLocalPort());
Assert.assertNotNull(response);
Assert.assertEquals(200, response.getStatus());
}
}
@Test
public void testGETResponseWithContent() throws Exception
{
final byte[] data = new byte[]{0, 1, 2, 3, 4, 5, 6, 7};
start(new AbstractHandler()
{
@Override
public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
response.getOutputStream().write(data);
baseRequest.setHandled(true);
}
});
int maxConnections = 256;
client.setMaxConnectionsPerDestination(maxConnections);
for (int i = 0; i < maxConnections + 1; ++i)
{
ContentResponse response = client.GET(scheme + "://localhost:" + connector.getLocalPort());
Assert.assertNotNull(response);
Assert.assertEquals(200, response.getStatus());
byte[] content = response.getContent();
Assert.assertArrayEquals(data, content);
}
}
@Test
public void testGETWithParametersResponseWithContent() throws Exception
{
final String paramName1 = "a";
final String paramName2 = "b";
start(new AbstractHandler()
{
@Override
public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
response.setCharacterEncoding("UTF-8");
ServletOutputStream output = response.getOutputStream();
String paramValue1 = request.getParameter(paramName1);
output.write(paramValue1.getBytes("UTF-8"));
String paramValue2 = request.getParameter(paramName2);
Assert.assertEquals("", paramValue2);
output.write("empty".getBytes("UTF-8"));
baseRequest.setHandled(true);
}
});
String value1 = "\u20AC";
String paramValue1 = URLEncoder.encode(value1, "UTF-8");
String query = paramName1 + "=" + paramValue1 + "&" + paramName2;
ContentResponse response = client.GET(scheme + "://localhost:" + connector.getLocalPort() + "/?" + query);
Assert.assertNotNull(response);
Assert.assertEquals(200, response.getStatus());
String content = new String(response.getContent(), "UTF-8");
Assert.assertEquals(value1 + "empty", content);
}
@Test
public void testGETWithParametersMultiValuedResponseWithContent() throws Exception
{
final String paramName1 = "a";
final String paramName2 = "b";
start(new AbstractHandler()
{
@Override
public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
response.setCharacterEncoding("UTF-8");
ServletOutputStream output = response.getOutputStream();
String[] paramValues1 = request.getParameterValues(paramName1);
for (String paramValue : paramValues1)
output.write(paramValue.getBytes("UTF-8"));
String paramValue2 = request.getParameter(paramName2);
output.write(paramValue2.getBytes("UTF-8"));
baseRequest.setHandled(true);
}
});
String value11 = "\u20AC";
String value12 = "\u20AA";
String value2 = "&";
String paramValue11 = URLEncoder.encode(value11, "UTF-8");
String paramValue12 = URLEncoder.encode(value12, "UTF-8");
String paramValue2 = URLEncoder.encode(value2, "UTF-8");
String query = paramName1 + "=" + paramValue11 + "&" + paramName1 + "=" + paramValue12 + "&" + paramName2 + "=" + paramValue2;
ContentResponse response = client.GET(scheme + "://localhost:" + connector.getLocalPort() + "/?" + query);
Assert.assertNotNull(response);
Assert.assertEquals(200, response.getStatus());
String content = new String(response.getContent(), "UTF-8");
Assert.assertEquals(value11 + value12 + value2, content);
}
@Test
public void testPOSTWithParameters() throws Exception
{
final String paramName = "a";
final String paramValue = "\u20AC";
start(new AbstractHandler()
{
@Override
public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
baseRequest.setHandled(true);
String value = request.getParameter(paramName);
if (paramValue.equals(value))
{
response.setCharacterEncoding("UTF-8");
response.setContentType("text/plain");
response.getOutputStream().print(value);
}
}
});
ContentResponse response = client.POST(scheme + "://localhost:" + connector.getLocalPort())
.param(paramName, paramValue)
.timeout(5, TimeUnit.SECONDS)
.send();
Assert.assertNotNull(response);
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(paramValue, new String(response.getContent(), "UTF-8"));
}
@Test
public void testPOSTWithQueryString() throws Exception
{
final String paramName = "a";
final String paramValue = "\u20AC";
start(new AbstractHandler()
{
@Override
public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
baseRequest.setHandled(true);
String value = request.getParameter(paramName);
if (paramValue.equals(value))
{
response.setCharacterEncoding("UTF-8");
response.setContentType("text/plain");
response.getOutputStream().print(value);
}
}
});
String uri = scheme + "://localhost:" + connector.getLocalPort() +
"/?" + paramName + "=" + URLEncoder.encode(paramValue, "UTF-8");
ContentResponse response = client.POST(uri)
.timeout(5, TimeUnit.SECONDS)
.send();
Assert.assertNotNull(response);
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(paramValue, new String(response.getContent(), "UTF-8"));
}
@Test
public void testPUTWithParameters() throws Exception
{
final String paramName = "a";
final String paramValue = "\u20AC";
start(new AbstractHandler()
{
@Override
public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
baseRequest.setHandled(true);
String value = request.getParameter(paramName);
if (paramValue.equals(value))
{
response.setCharacterEncoding("UTF-8");
response.setContentType("text/plain");
response.getOutputStream().print(value);
}
}
});
URI uri = URI.create(scheme + "://localhost:" + connector.getLocalPort() + "/path?" + paramName + "=" + paramValue);
ContentResponse response = client.newRequest(uri)
.method(HttpMethod.PUT)
.timeout(5, TimeUnit.SECONDS)
.send();
Assert.assertNotNull(response);
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(paramValue, new String(response.getContent(), "UTF-8"));
}
@Test
public void testPOSTWithParametersWithContent() throws Exception
{
final byte[] content = {0, 1, 2, 3};
final String paramName = "a";
final String paramValue = "\u20AC";
start(new AbstractHandler()
{
@Override
public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
baseRequest.setHandled(true);
String value = request.getParameter(paramName);
if (paramValue.equals(value))
{
response.setCharacterEncoding("UTF-8");
response.setContentType("application/octet-stream");
IO.copy(request.getInputStream(), response.getOutputStream());
}
}
});
for (int i = 0; i < 256; ++i)
{
ContentResponse response = client.POST(scheme + "://localhost:" + connector.getLocalPort() + "/?b=1")
.param(paramName, paramValue)
.content(new BytesContentProvider(content))
.timeout(5, TimeUnit.SECONDS)
.send();
Assert.assertNotNull(response);
Assert.assertEquals(200, response.getStatus());
Assert.assertArrayEquals(content, response.getContent());
}
}
@Test
public void testPOSTWithContentNotifiesRequestContentListener() throws Exception
{
final byte[] content = {0, 1, 2, 3};
start(new EmptyServerHandler());
ContentResponse response = client.POST(scheme + "://localhost:" + connector.getLocalPort())
.onRequestContent(new Request.ContentListener()
{
@Override
public void onContent(Request request, ByteBuffer buffer)
{
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
if (!Arrays.equals(content, bytes))
request.abort(new Exception());
}
})
.content(new BytesContentProvider(content))
.timeout(5, TimeUnit.SECONDS)
.send();
Assert.assertNotNull(response);
Assert.assertEquals(200, response.getStatus());
}
@Test
public void testPOSTWithContentTracksProgress() throws Exception
{
start(new EmptyServerHandler());
final AtomicInteger progress = new AtomicInteger();
ContentResponse response = client.POST(scheme + "://localhost:" + connector.getLocalPort())
.onRequestContent(new Request.ContentListener()
{
@Override
public void onContent(Request request, ByteBuffer buffer)
{
byte[] bytes = new byte[buffer.remaining()];
Assert.assertEquals(1, bytes.length);
buffer.get(bytes);
Assert.assertEquals(bytes[0], progress.getAndIncrement());
}
})
.content(new BytesContentProvider(new byte[]{0}, new byte[]{1}, new byte[]{2}, new byte[]{3}, new byte[]{4}))
.timeout(5, TimeUnit.SECONDS)
.send();
Assert.assertNotNull(response);
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(5, progress.get());
}
@Test
public void testGZIPContentEncoding() throws Exception
{
final byte[] data = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
start(new AbstractHandler()
{
@Override
public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
baseRequest.setHandled(true);
response.setHeader("Content-Encoding", "gzip");
GZIPOutputStream gzipOutput = new GZIPOutputStream(response.getOutputStream());
gzipOutput.write(data);
gzipOutput.finish();
}
});
ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
.scheme(scheme)
.timeout(5, TimeUnit.SECONDS)
.send();
Assert.assertEquals(200, response.getStatus());
Assert.assertArrayEquals(data, response.getContent());
}
@Slow
@Test
public void testRequestIdleTimeout() throws Exception
{
final long idleTimeout = 1000;
start(new AbstractHandler()
{
@Override
public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
try
{
baseRequest.setHandled(true);
TimeUnit.MILLISECONDS.sleep(2 * idleTimeout);
}
catch (InterruptedException x)
{
throw new ServletException(x);
}
}
});
final String host = "localhost";
final int port = connector.getLocalPort();
try
{
client.newRequest(host, port)
.scheme(scheme)
.idleTimeout(idleTimeout, TimeUnit.MILLISECONDS)
.timeout(3 * idleTimeout, TimeUnit.MILLISECONDS)
.send();
Assert.fail();
}
catch (ExecutionException expected)
{
Assert.assertTrue(expected.getCause() instanceof TimeoutException);
}
// Make another request without specifying the idle timeout, should not fail
ContentResponse response = client.newRequest(host, port)
.scheme(scheme)
.timeout(3 * idleTimeout, TimeUnit.MILLISECONDS)
.send();
Assert.assertNotNull(response);
Assert.assertEquals(200, response.getStatus());
}
@Test
public void testConnectionIdleTimeout() throws Exception
{
final long idleTimeout = 1000;
start(new AbstractHandler()
{
@Override
public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
try
{
baseRequest.setHandled(true);
TimeUnit.MILLISECONDS.sleep(2 * idleTimeout);
}
catch (InterruptedException x)
{
throw new ServletException(x);
}
}
});
connector.setIdleTimeout(idleTimeout);
try
{
client.newRequest("localhost", connector.getLocalPort())
.scheme(scheme)
.idleTimeout(4 * idleTimeout, TimeUnit.MILLISECONDS)
.timeout(3 * idleTimeout, TimeUnit.MILLISECONDS)
.send();
Assert.fail();
}
catch (ExecutionException x)
{
Assert.assertTrue(x.getCause() instanceof EOFException);
}
connector.setIdleTimeout(5 * idleTimeout);
// Make another request to be sure the connection is recreated
ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
.scheme(scheme)
.idleTimeout(4 * idleTimeout, TimeUnit.MILLISECONDS)
.timeout(3 * idleTimeout, TimeUnit.MILLISECONDS)
.send();
Assert.assertNotNull(response);
Assert.assertEquals(200, response.getStatus());
}
@Test
public void testSendToIPv6Address() throws Exception
{
start(new EmptyServerHandler());
ContentResponse response = client.newRequest("[::1]", connector.getLocalPort())
.scheme(scheme)
.timeout(5, TimeUnit.SECONDS)
.send();
Assert.assertNotNull(response);
Assert.assertEquals(200, response.getStatus());
}
@Test
public void testHEADWithResponseContentLength() throws Exception
{
final int length = 1024;
start(new AbstractHandler()
{
@Override
public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
baseRequest.setHandled(true);
response.getOutputStream().write(new byte[length]);
}
});
// HEAD requests receive a Content-Length header, but do not
// receive the content so they must handle this case properly
ContentResponse response = client.newRequest("localhost", connector.getLocalPort())
.scheme(scheme)
.method(HttpMethod.HEAD)
.timeout(5, TimeUnit.SECONDS)
.send();
Assert.assertNotNull(response);
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(0, response.getContent().length);
// Perform a normal GET request to be sure the content is now read
response = client.newRequest("localhost", connector.getLocalPort())
.scheme(scheme)
.timeout(5, TimeUnit.SECONDS)
.send();
Assert.assertNotNull(response);
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(length, response.getContent().length);
}
}

View File

@ -0,0 +1,3 @@
org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
#org.eclipse.jetty.client.LEVEL=DEBUG
#org.eclipse.jetty.fcgi.LEVEL=DEBUG

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>fcgi-parent</artifactId>
<groupId>org.eclipse.jetty.fcgi</groupId>
<version>1.0.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>fcgi-proxy</artifactId>
<name>Jetty :: FastCGI :: Proxy</name>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.fcgi</groupId>
<artifactId>fcgi-http-client-transport</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-proxy</artifactId>
<version>${jetty-version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>${jetty-version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>${jetty-version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.spdy</groupId>
<artifactId>spdy-http-server</artifactId>
<version>${jetty-version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,157 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.proxy;
import java.net.URI;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.fcgi.FCGI;
import org.eclipse.jetty.fcgi.client.http.HttpClientTransportOverFCGI;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.proxy.ProxyServlet;
public class FastCGIProxyServlet extends ProxyServlet.Transparent
{
public static final String SCRIPT_ROOT_INIT_PARAM = "scriptRoot";
public static final String SCRIPT_PATTERN_INIT_PARAM = "scriptPattern";
private static final String REMOTE_ADDR_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".remoteAddr";
private static final String REMOTE_PORT_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".remotePort";
private static final String SERVER_NAME_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".serverName";
private static final String SERVER_ADDR_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".serverAddr";
private static final String SERVER_PORT_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".serverPort";
private static final String SCHEME_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".scheme";
private static final String REQUEST_URI_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".requestURI";
private Pattern scriptPattern;
@Override
public void init() throws ServletException
{
super.init();
String value = getInitParameter(SCRIPT_PATTERN_INIT_PARAM);
if (value == null)
value = "(.+?\\.php)";
scriptPattern = Pattern.compile(value);
}
@Override
protected HttpClient newHttpClient()
{
ServletConfig config = getServletConfig();
String scriptRoot = config.getInitParameter(SCRIPT_ROOT_INIT_PARAM);
if (scriptRoot == null)
throw new IllegalArgumentException("Mandatory parameter '" + SCRIPT_ROOT_INIT_PARAM + "' not configured");
return new HttpClient(new ProxyHttpClientTransportOverFCGI(scriptRoot), null);
}
@Override
protected void customizeProxyRequest(Request proxyRequest, HttpServletRequest request)
{
proxyRequest.attribute(REMOTE_ADDR_ATTRIBUTE, request.getRemoteAddr());
proxyRequest.attribute(REMOTE_PORT_ATTRIBUTE, String.valueOf(request.getRemotePort()));
proxyRequest.attribute(SERVER_NAME_ATTRIBUTE, request.getServerName());
proxyRequest.attribute(SERVER_ADDR_ATTRIBUTE, request.getLocalAddr());
proxyRequest.attribute(SERVER_PORT_ATTRIBUTE, String.valueOf(request.getLocalPort()));
proxyRequest.attribute(SCHEME_ATTRIBUTE, request.getScheme());
// If we are forwarded or included, retain the original request URI.
String originalPath = (String)request.getAttribute(RequestDispatcher.FORWARD_REQUEST_URI);
String originalQuery = (String)request.getAttribute(RequestDispatcher.FORWARD_QUERY_STRING);
if (originalPath == null)
{
originalPath = (String)request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI);
originalQuery = (String)request.getAttribute(RequestDispatcher.INCLUDE_QUERY_STRING);
}
if (originalPath != null)
{
String originalURI = originalPath;
if (originalQuery != null)
originalURI += "?" + originalQuery;
proxyRequest.attribute(REQUEST_URI_ATTRIBUTE, originalURI);
}
super.customizeProxyRequest(proxyRequest, request);
}
protected void customizeFastCGIHeaders(Request proxyRequest, HttpFields fastCGIHeaders)
{
fastCGIHeaders.put(FCGI.Headers.REMOTE_ADDR, (String)proxyRequest.getAttributes().get(REMOTE_ADDR_ATTRIBUTE));
fastCGIHeaders.put(FCGI.Headers.REMOTE_PORT, (String)proxyRequest.getAttributes().get(REMOTE_PORT_ATTRIBUTE));
fastCGIHeaders.put(FCGI.Headers.SERVER_NAME, (String)proxyRequest.getAttributes().get(SERVER_NAME_ATTRIBUTE));
fastCGIHeaders.put(FCGI.Headers.SERVER_ADDR, (String)proxyRequest.getAttributes().get(SERVER_ADDR_ATTRIBUTE));
fastCGIHeaders.put(FCGI.Headers.SERVER_PORT, (String)proxyRequest.getAttributes().get(SERVER_PORT_ATTRIBUTE));
if (HttpScheme.HTTPS.is((String)proxyRequest.getAttributes().get(SCHEME_ATTRIBUTE)))
fastCGIHeaders.put(FCGI.Headers.HTTPS, "on");
URI proxyRequestURI = proxyRequest.getURI();
String rawPath = proxyRequestURI.getRawPath();
String rawQuery = proxyRequestURI.getRawQuery();
String requestURI = (String)proxyRequest.getAttributes().get(REQUEST_URI_ATTRIBUTE);
if (requestURI == null)
{
requestURI = rawPath;
if (rawQuery != null)
requestURI += "?" + rawQuery;
}
fastCGIHeaders.put(FCGI.Headers.REQUEST_URI, requestURI);
String scriptName = rawPath;
Matcher matcher = scriptPattern.matcher(rawPath);
if (matcher.matches())
{
// Expect at least one group in the regular expression.
scriptName = matcher.group(1);
// If there is a second group, map it to PATH_INFO.
if (matcher.groupCount() > 1)
fastCGIHeaders.put(FCGI.Headers.PATH_INFO, matcher.group(2));
}
fastCGIHeaders.put(FCGI.Headers.SCRIPT_NAME, scriptName);
String root = fastCGIHeaders.get(FCGI.Headers.DOCUMENT_ROOT);
fastCGIHeaders.put(FCGI.Headers.SCRIPT_FILENAME, root + scriptName);
}
private class ProxyHttpClientTransportOverFCGI extends HttpClientTransportOverFCGI
{
public ProxyHttpClientTransportOverFCGI(String scriptRoot)
{
super(scriptRoot);
}
@Override
protected void customize(Request request, HttpFields fastCGIHeaders)
{
super.customize(request, fastCGIHeaders);
customizeFastCGIHeaders(request, fastCGIHeaders);
}
}
}

View File

@ -0,0 +1,113 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.proxy;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Inspired by nginx's try_files functionality
*/
public class TryFilesFilter implements Filter
{
public static final String FILES_INIT_PARAM = "files";
private String[] files;
@Override
public void init(FilterConfig config) throws ServletException
{
String param = config.getInitParameter(FILES_INIT_PARAM);
if (param == null)
throw new ServletException(String.format("Missing mandatory parameter '%s'", FILES_INIT_PARAM));
files = param.split(" ");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
{
HttpServletRequest httpRequest = (HttpServletRequest)request;
HttpServletResponse httpResponse = (HttpServletResponse)response;
for (int i = 0; i < files.length - 1; ++i)
{
String file = files[i];
String resolved = resolve(httpRequest, file);
URL url = request.getServletContext().getResource(resolved);
if (url == null)
continue;
if (Files.isReadable(toPath(url)))
{
chain.doFilter(httpRequest, httpResponse);
return;
}
}
// The last one is the fallback
fallback(httpRequest, httpResponse, chain, files[files.length - 1]);
}
private Path toPath(URL url) throws IOException
{
try
{
return Paths.get(url.toURI());
}
catch (URISyntaxException x)
{
throw new IOException(x);
}
}
protected void fallback(HttpServletRequest request, HttpServletResponse response, FilterChain chain, String fallback) throws IOException, ServletException
{
String resolved = resolve(request, fallback);
request.getServletContext().getRequestDispatcher(resolved).forward(request, response);
}
private String resolve(HttpServletRequest request, String value)
{
String path = request.getServletPath();
String info = request.getPathInfo();
if (info != null)
path += info;
if (!path.startsWith("/"))
path = "/" + path;
return value.replaceAll("\\$path", path);
}
@Override
public void destroy()
{
}
}

View File

@ -0,0 +1,77 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.proxy;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.server.http.HTTPSPDYServerConnector;
import org.eclipse.jetty.spdy.server.http.PushStrategy;
import org.eclipse.jetty.spdy.server.http.ReferrerPushStrategy;
import org.eclipse.jetty.util.ssl.SslContextFactory;
public class DrupalSPDYFastCGIProxyServer
{
public static void main(String[] args) throws Exception
{
SslContextFactory sslContextFactory = new SslContextFactory();
sslContextFactory.setEndpointIdentificationAlgorithm("");
sslContextFactory.setKeyStorePath("src/test/resources/keystore.jks");
sslContextFactory.setKeyStorePassword("storepwd");
sslContextFactory.setTrustStorePath("src/test/resources/truststore.jks");
sslContextFactory.setTrustStorePassword("storepwd");
Server server = new Server();
Map<Short, PushStrategy> pushStrategies = new HashMap<>();
pushStrategies.put(SPDY.V3, new ReferrerPushStrategy());
HTTPSPDYServerConnector connector = new HTTPSPDYServerConnector(server, sslContextFactory, pushStrategies);
connector.setPort(8443);
server.addConnector(connector);
// Drupal seems to only work on the root context,
// at least out of the box without additional plugins
String root = "/home/simon/programs/drupal-7.23";
ServletContextHandler context = new ServletContextHandler(server, "/");
context.setResourceBase(root);
context.setWelcomeFiles(new String[]{"index.php"});
// Serve static resources
ServletHolder defaultServlet = new ServletHolder(DefaultServlet.class);
defaultServlet.setName("default");
context.addServlet(defaultServlet, "/");
// FastCGI
ServletHolder fcgiServlet = new ServletHolder(FastCGIProxyServlet.class);
fcgiServlet.setInitParameter(FastCGIProxyServlet.SCRIPT_ROOT_INIT_PARAM, root);
fcgiServlet.setInitParameter("proxyTo", "http://localhost:9000");
fcgiServlet.setInitParameter("prefix", "/");
fcgiServlet.setInitParameter(FastCGIProxyServlet.SCRIPT_PATTERN_INIT_PARAM, "(.+\\.php)");
context.addServlet(fcgiServlet, "*.php");
server.start();
}
}

View File

@ -0,0 +1,85 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.proxy;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.DispatcherType;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.server.http.HTTPSPDYServerConnector;
import org.eclipse.jetty.spdy.server.http.PushStrategy;
import org.eclipse.jetty.spdy.server.http.ReferrerPushStrategy;
import org.eclipse.jetty.util.ssl.SslContextFactory;
public class WordPressSPDYFastCGIProxyServer
{
public static void main(String[] args) throws Exception
{
int port = 8080;
int tlsPort = 8443;
SslContextFactory sslContextFactory = new SslContextFactory();
sslContextFactory.setEndpointIdentificationAlgorithm("");
sslContextFactory.setKeyStorePath("src/test/resources/keystore.jks");
sslContextFactory.setKeyStorePassword("storepwd");
sslContextFactory.setTrustStorePath("src/test/resources/truststore.jks");
sslContextFactory.setTrustStorePassword("storepwd");
Server server = new Server();
Map<Short, PushStrategy> pushStrategies = new HashMap<>();
pushStrategies.put(SPDY.V3, new ReferrerPushStrategy());
HTTPSPDYServerConnector tlsConnector = new HTTPSPDYServerConnector(server, sslContextFactory, pushStrategies);
tlsConnector.setPort(tlsPort);
server.addConnector(tlsConnector);
HTTPSPDYServerConnector connector = new HTTPSPDYServerConnector(server, null, pushStrategies);
connector.setPort(port);
server.addConnector(connector);
String root = "/home/simon/programs/wordpress-3.7.1";
ServletContextHandler context = new ServletContextHandler(server, "/wp");
context.setResourceBase(root);
context.setWelcomeFiles(new String[]{"index.php"});
// Serve static resources
ServletHolder defaultServlet = new ServletHolder("default", DefaultServlet.class);
context.addServlet(defaultServlet, "/");
FilterHolder tryFilesFilter = context.addFilter(TryFilesFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
// tryFilesFilter.setInitParameter(TryFilesFilter.FILES_INIT_PARAM, "$path $path/index.php"); // Permalink /?p=123
tryFilesFilter.setInitParameter(TryFilesFilter.FILES_INIT_PARAM, "$path /index.php?p=$path"); // Permalink /%year%/%monthnum%/%postname%
// FastCGI
ServletHolder fcgiServlet = context.addServlet(FastCGIProxyServlet.class, "*.php");
fcgiServlet.setInitParameter(FastCGIProxyServlet.SCRIPT_ROOT_INIT_PARAM, root);
fcgiServlet.setInitParameter("proxyTo", "http://localhost:9000");
fcgiServlet.setInitParameter("prefix", "/");
fcgiServlet.setInitParameter(FastCGIProxyServlet.SCRIPT_PATTERN_INIT_PARAM, "(.+?\\.php)");
server.start();
}
}

View File

@ -0,0 +1,3 @@
org.eclipse.jetty.util.log.class=org.eclipse.jetty.util.log.StdErrLog
#org.eclipse.jetty.client.LEVEL=DEBUG
#org.eclipse.jetty.fcgi.LEVEL=DEBUG

Binary file not shown.

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.eclipse.jetty.fcgi</groupId>
<artifactId>fcgi-parent</artifactId>
<version>1.0.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>fcgi-server</artifactId>
<name>Jetty :: FastCGI :: Server</name>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty.fcgi</groupId>
<artifactId>fcgi-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>${jetty-version}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,204 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.server;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.fcgi.FCGI;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpInput;
import org.eclipse.jetty.server.HttpTransport;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
public class HttpChannelOverFCGI extends HttpChannel<ByteBuffer>
{
private static final Logger LOG = Log.getLogger(HttpChannelOverFCGI.class);
private final List<HttpField> fields = new ArrayList<>();
private final Dispatcher dispatcher;
private String method;
private String path;
private String query;
private String version;
public HttpChannelOverFCGI(Connector connector, HttpConfiguration configuration, EndPoint endPoint, HttpTransport transport, HttpInput<ByteBuffer> input)
{
super(connector, configuration, endPoint, transport, input);
this.dispatcher = new Dispatcher(connector.getExecutor(), this);
}
protected void header(HttpField field)
{
if (FCGI.Headers.REQUEST_METHOD.equalsIgnoreCase(field.getName()))
method = field.getValue();
else if (FCGI.Headers.DOCUMENT_URI.equalsIgnoreCase(field.getName()))
path = field.getValue();
else if (FCGI.Headers.QUERY_STRING.equalsIgnoreCase(field.getName()))
query = field.getValue();
else if (FCGI.Headers.SERVER_PROTOCOL.equalsIgnoreCase(field.getName()))
version = field.getValue();
else
fields.add(field);
}
@Override
public boolean headerComplete()
{
String uri = path;
if (query != null && query.length() > 0)
uri += "?" + query;
startRequest(HttpMethod.fromString(method), method, ByteBuffer.wrap(uri.getBytes(StandardCharsets.UTF_8)),
HttpVersion.fromString(version));
for (HttpField fcgiField : fields)
{
HttpField httpField = convertHeader(fcgiField);
if (httpField != null)
parsedHeader(httpField);
}
return super.headerComplete();
}
private HttpField convertHeader(HttpField field)
{
String name = field.getName();
if (name.startsWith("HTTP_"))
{
// Converts e.g. "HTTP_ACCEPT_ENCODING" to "Accept-Encoding"
String[] parts = name.split("_");
StringBuilder httpName = new StringBuilder();
for (int i = 1; i < parts.length; ++i)
{
if (i > 1)
httpName.append("-");
String part = parts[i];
httpName.append(Character.toUpperCase(part.charAt(0)));
httpName.append(part.substring(1).toLowerCase(Locale.ENGLISH));
}
return new HttpField(httpName.toString(), field.getValue());
}
return null;
}
protected void dispatch()
{
dispatcher.dispatch();
}
private static class Dispatcher implements Runnable
{
private final AtomicReference<State> state = new AtomicReference<>(State.IDLE);
private final Executor executor;
private final Runnable runnable;
private Dispatcher(Executor executor, Runnable runnable)
{
this.executor = executor;
this.runnable = runnable;
}
public void dispatch()
{
while (true)
{
State current = state.get();
LOG.debug("Dispatching, state={}", current);
switch (current)
{
case IDLE:
{
if (!state.compareAndSet(current, State.DISPATCH))
continue;
executor.execute(this);
return;
}
case DISPATCH:
case EXECUTE:
{
if (state.compareAndSet(current, State.SCHEDULE))
return;
continue;
}
case SCHEDULE:
{
return;
}
default:
{
throw new IllegalStateException();
}
}
}
}
@Override
public void run()
{
while (true)
{
State current = state.get();
LOG.debug("Running, state={}", current);
switch (current)
{
case DISPATCH:
{
if (state.compareAndSet(current, State.EXECUTE))
runnable.run();
continue;
}
case EXECUTE:
{
if (state.compareAndSet(current, State.IDLE))
return;
continue;
}
case SCHEDULE:
{
if (state.compareAndSet(current, State.DISPATCH))
continue;
throw new IllegalStateException();
}
default:
{
throw new IllegalStateException();
}
}
}
}
private enum State
{
IDLE, DISPATCH, EXECUTE, SCHEDULE
}
}
}

View File

@ -0,0 +1,102 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.server;
import java.nio.ByteBuffer;
import org.eclipse.jetty.fcgi.generator.Flusher;
import org.eclipse.jetty.fcgi.generator.Generator;
import org.eclipse.jetty.fcgi.generator.ServerGenerator;
import org.eclipse.jetty.http.HttpGenerator;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.server.HttpTransport;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
public class HttpTransportOverFCGI implements HttpTransport
{
private final ServerGenerator generator;
private final Flusher flusher;
private final int request;
private volatile boolean head;
public HttpTransportOverFCGI(ByteBufferPool byteBufferPool, Flusher flusher, int request)
{
this.generator = new ServerGenerator(byteBufferPool);
this.flusher = flusher;
this.request = request;
}
@Override
public void send(HttpGenerator.ResponseInfo info, ByteBuffer content, boolean lastContent, Callback callback)
{
boolean head = this.head = info.isHead();
if (head)
{
if (lastContent)
{
Generator.Result headersResult = generator.generateResponseHeaders(request, info.getStatus(), info.getReason(),
info.getHttpFields(), new Callback.Adapter());
Generator.Result contentResult = generator.generateResponseContent(request, BufferUtil.EMPTY_BUFFER, lastContent, callback);
flusher.flush(headersResult, contentResult);
}
else
{
Generator.Result headersResult = generator.generateResponseHeaders(request, info.getStatus(), info.getReason(),
info.getHttpFields(), callback);
flusher.flush(headersResult);
}
}
else
{
Generator.Result headersResult = generator.generateResponseHeaders(request, info.getStatus(), info.getReason(),
info.getHttpFields(), new Callback.Adapter());
Generator.Result contentResult = generator.generateResponseContent(request, content, lastContent, callback);
flusher.flush(headersResult, contentResult);
}
}
@Override
public void send(ByteBuffer content, boolean lastContent, Callback callback)
{
if (head)
{
if (lastContent)
{
Generator.Result result = generator.generateResponseContent(request, BufferUtil.EMPTY_BUFFER, lastContent, callback);
flusher.flush(result);
}
else
{
// Skip content generation
callback.succeeded();
}
}
else
{
Generator.Result result = generator.generateResponseContent(request, content, lastContent, callback);
flusher.flush(result);
}
}
@Override
public void completed()
{
}
}

View File

@ -0,0 +1,179 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.server;
import java.nio.ByteBuffer;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.eclipse.jetty.fcgi.FCGI;
import org.eclipse.jetty.fcgi.generator.Flusher;
import org.eclipse.jetty.fcgi.parser.ServerParser;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.server.ByteBufferQueuedHttpInput;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
public class ServerFCGIConnection extends AbstractConnection
{
private static final Logger LOG = Log.getLogger(ServerFCGIConnection.class);
private final ConcurrentMap<Integer, HttpChannelOverFCGI> channels = new ConcurrentHashMap<>();
private final Connector connector;
private final Flusher flusher;
private final HttpConfiguration configuration;
private final ServerParser parser;
public ServerFCGIConnection(Connector connector, EndPoint endPoint, HttpConfiguration configuration)
{
super(endPoint, connector.getExecutor());
this.connector = connector;
this.flusher = new Flusher(endPoint);
this.configuration = configuration;
this.parser = new ServerParser(new ServerListener());
}
@Override
public void onOpen()
{
super.onOpen();
fillInterested();
}
@Override
public void onFillable()
{
EndPoint endPoint = getEndPoint();
ByteBufferPool bufferPool = connector.getByteBufferPool();
ByteBuffer buffer = bufferPool.acquire(configuration.getResponseHeaderSize(), true);
try
{
while (true)
{
int read = endPoint.fill(buffer);
if (LOG.isDebugEnabled()) // Avoid boxing of variable 'read'
LOG.debug("Read {} bytes from {}", read, endPoint);
if (read > 0)
{
parse(buffer);
}
else if (read == 0)
{
fillInterested();
break;
}
else
{
shutdown();
break;
}
}
}
catch (Exception x)
{
LOG.debug(x);
// TODO: fail and close ?
}
finally
{
bufferPool.release(buffer);
}
}
private void parse(ByteBuffer buffer)
{
while (buffer.hasRemaining())
parser.parse(buffer);
}
private void shutdown()
{
flusher.shutdown();
}
private class ServerListener implements ServerParser.Listener
{
@Override
public void onStart(int request, FCGI.Role role, int flags)
{
// TODO: handle flags
HttpChannelOverFCGI channel = new HttpChannelOverFCGI(connector, configuration, getEndPoint(),
new HttpTransportOverFCGI(connector.getByteBufferPool(), flusher, request), new ByteBufferQueuedHttpInput());
HttpChannelOverFCGI existing = channels.putIfAbsent(request, channel);
if (existing != null)
throw new IllegalStateException();
if (LOG.isDebugEnabled())
LOG.debug("Request {} start on {}", request, channel);
}
@Override
public void onHeader(int request, HttpField field)
{
HttpChannelOverFCGI channel = channels.get(request);
if (LOG.isDebugEnabled())
LOG.debug("Request {} header {} on {}", request, field, channel);
if (channel != null)
channel.header(field);
}
@Override
public void onHeaders(int request)
{
HttpChannelOverFCGI channel = channels.get(request);
if (LOG.isDebugEnabled())
LOG.debug("Request {} headers on {}", request, channel);
if (channel != null)
{
if (channel.headerComplete())
channel.dispatch();
}
}
@Override
public void onContent(int request, FCGI.StreamType stream, ByteBuffer buffer)
{
HttpChannelOverFCGI channel = channels.get(request);
if (LOG.isDebugEnabled())
LOG.debug("Request {} {} content {} on {}", request, stream, buffer, channel);
if (channel != null)
{
if (channel.content(buffer))
channel.dispatch();
}
}
@Override
public void onEnd(int request)
{
HttpChannelOverFCGI channel = channels.remove(request);
if (LOG.isDebugEnabled())
LOG.debug("Request {} end on {}", request, channel);
if (channel != null)
{
if (channel.messageComplete())
channel.dispatch();
}
}
}
}

View File

@ -0,0 +1,42 @@
//
// ========================================================================
// Copyright (c) 1995-2013 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.fcgi.server;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.server.AbstractConnectionFactory;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration;
public class ServerFCGIConnectionFactory extends AbstractConnectionFactory
{
private final HttpConfiguration configuration;
public ServerFCGIConnectionFactory(HttpConfiguration configuration)
{
super("fcgi/1.0");
this.configuration = configuration;
}
@Override
public Connection newConnection(Connector connector, EndPoint endPoint)
{
return new ServerFCGIConnection(connector, endPoint, configuration);
}
}

93
jetty-fcgi/pom.xml Normal file
View File

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
<version>9.1.1.v20140108</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>org.eclipse.jetty.fcgi</groupId>
<artifactId>fcgi-parent</artifactId>
<version>1.0.1</version>
<packaging>pom</packaging>
<name>Jetty :: FastCGI</name>
<properties>
<jetty-version>9.1.1.v20140108</jetty-version>
</properties>
<modules>
<module>fcgi-core</module>
<module>fcgi-http-client-transport</module>
<module>fcgi-server</module>
<module>fcgi-proxy</module>
<module>fcgi-distribution</module>
</modules>
<build>
<plugins>
<!--
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<descriptorRefs>
<descriptorRef>config</descriptorRef>
</descriptorRefs>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>manifest</goal>
</goals>
<configuration>
<instructions>
<Export-Package>${bundle-symbolic-name}.*;version="9.1"</Export-Package>
<Import-Package>org.eclipse.jetty.*;version="[9.0,10.0)",*</Import-Package>
<_nouses>true</_nouses>
</instructions>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>
-->
</plugins>
</build>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>