pull/1/head
Ivan Olexyn 5 years ago
parent 75b1ceaa2c
commit bd7e8482fb

@ -1,7 +1,8 @@
#!/bin/bash #!/bin/bash
tomcat_webapps="${HOME}/app/tomcat/webapps" tomcat_webapps="${HOME}/app/tomcat/webapps"
jetty_webapps="${HOME}/app/jetty9.4/webapps"
webapps=$jetty_webapps
cwd=$(pwd) cwd=$(pwd)
/home/user/app/tomcat/bin/shutdown.sh /home/user/app/tomcat/bin/shutdown.sh
@ -10,14 +11,15 @@ echo "================"
echo "END TOMCAT STOP " echo "END TOMCAT STOP "
echo "================" echo "================"
cp -v "${cwd}/misp-mirror/target/misp-mirror-0.1.war" "${tomcat_webapps}" cp -v "${cwd}/misp-mirror/target/misp-mirror-0.1.war" "${webapps}"
cp -v "${cwd}/misp-rev/target/misp-rev-0.1.war" "${tomcat_webapps}" cp -v "${cwd}/misp-rev/target/misp-rev-0.1.war" "${webapps}"
cp -v "${cwd}/test-proxy/target/test-proxy-0.1.war" "${webapps}"
echo "================" echo "================"
echo "END COPY" echo "END COPY"
echo "================" echo "================"
/home/user/app/tomcat/bin/startup.sh # /home/user/app/tomcat/bin/startup.sh
echo "================" echo "================"
echo "END TOMCAT START " echo "END TOMCAT START "

@ -15,8 +15,8 @@
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source> <maven.compiler.source>1.11</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target> <maven.compiler.target>1.11</maven.compiler.target>
</properties> </properties>
<dependencies> <dependencies>
@ -53,6 +53,7 @@
</dependencies> </dependencies>
<build> <build>
<finalName>${project.artifactId}</finalName>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) --> <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins> <plugins>
<plugin> <plugin>

@ -7,7 +7,7 @@
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4"> version="2.4">
<display-name>misp-fwd</display-name> <display-name>misp-rev</display-name>
<servlet> <servlet>
<servlet-name>misp-rev</servlet-name> <servlet-name>misp-rev</servlet-name>

@ -0,0 +1,16 @@
#### About
* Mockup for easy debugging.
* Mock *requests* and *responses*.
* Teleport them between *actors*.
* There are 4 actors which are mocked:
* `AppMock` : the App hosted on *localhost*.
* `ClientMock` : the *mispclient* Servlet.
* `BridgeMock` : the *mispbridge* Servlet.
* `UserMock` : the user agent accessing the *mispbridge* from the internet.
* `MockSet` knows all 4 actors, all the 4 actors know `MockSet`.
* Thus all 4 actors know each other.
<br>
##### Threads in Mock
![](threads-in-mock.png)

@ -0,0 +1,99 @@
package com.olexyn.misp.mock;
import com.olexyn.misp.helper.Ride;
import com.olexyn.misp.mock.exchange.ExchangeMock;
import com.olexyn.misp.adapter.Adapter;
import javax.servlet.ServletException;
import java.io.IOException;
/**
* Wraps a ClientServlet so it can be debugged easily, i.e. without running Tomcat.
*/
public class AdapterMock extends Adapter {
private MockSet mockSet;
public AdapterMock(MockSet mockSet) {
super();
mockSet.adapterMock = this;
this.mockSet = mockSet;
}
/**
* Send POST (Ride).
* Parse response.
*/
@Override
protected Ride doSendPostRide(Ride ride) throws IOException, InterruptedException, ServletException {
// Mock Exchange
final ExchangeMock exchange = new ExchangeMock();
exchange.request.setMethod("POST");
exchange.request.setContentType("application/json");
exchange.request.setContent(ride.json().getBytes());
synchronized (exchange) {
// Mock POST (Ride)
exchange.notify();
mockSet.bridgeMock.doPost(exchange.request, exchange.response);
exchange.wait();
exchange.notify();
}
// handle OK (Ride)(Request)
return new Ride(exchange.response.getContentAsString());
}
/**
* Send GET (Request) to App.
* Parse response.
*/
@Override
protected String doSendGetRequest(String request) throws IOException {
// Mock Exchange
final ExchangeMock exchange = new ExchangeMock();
exchange.request.setMethod("GET");
exchange.request.setContent(request.getBytes());
synchronized (exchange) {
// Mock GET (Request)
exchange.notify();
mockSet.appMock.doGet(exchange.request, exchange.response);
// handle OK (Data)
exchange.notify();
}
return exchange.response.getContentAsString();
}
/**
* Send GET (Ride)(Request)(Data).
* Parse response.
*/
@Override
protected void doSendGetRideRequest(Ride ride) throws IOException, InterruptedException {
// Mock Exchange
final ExchangeMock exchange = new ExchangeMock();
exchange.request.setMethod("GET");
exchange.request.setContentType("application/json");
exchange.request.setContent(ride.json().getBytes());
synchronized (exchange) {
// Mock GET (Ride)(Request)(Data)
exchange.notify();
mockSet.bridgeMock.doGet(exchange.request, exchange.response);
exchange.wait();
exchange.notify();
}
}
}

@ -0,0 +1,20 @@
package com.olexyn.misp.mock;
/**
* Pass the MockSet.
* Provide a Runnable.
*/
public class AdapterRunnable implements Runnable {
private MockSet mockSet;
public AdapterRunnable(MockSet mockSet){
super();
this.mockSet = mockSet;
}
@Override
public void run() {
new AdapterMock(mockSet);
}
}

@ -0,0 +1,84 @@
package com.olexyn.misp.mock;
import com.olexyn.misp.mock.exchange.ExchangeMock;
import com.olexyn.misp.mock.exchange.RequestMock;
import com.olexyn.misp.bridge.BridgeServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class BridgeMock extends BridgeServlet {
public BridgeMock(MockSet mockSet) {
super();
mockSet.bridgeMock = this;
}
/**
* handle GET (Request)<br>
* - wait for availableRides to have an entry <br>
* - move move Ride to deliveredRides <br>
* - wait for Ride to have Data <br>
* - respond to the original request with Data
*/
@Override
protected void handleGetRequest(HttpServletRequest request, HttpServletResponse response) throws IOException, InterruptedException {
final ExchangeMock exchange;
synchronized (exchange = ((RequestMock) request).exchange) {
super.handleGetRequest(request,response);
exchange.notify();
}
}
/**
* handle GET (Ride)(Data)
* if Ride in ForwardedRequest
* remove Ride from ForwardedRequest
* add Ride to NewData
* send OK (Ride)(Data)
* remove Ride from NewData
* add Ride to ForwardedData
* send OK (EOL)
* remove Ride from ForwardedData
* add Ride to EOL
*/
protected void handleGetRideRequestData(HttpServletRequest request, HttpServletResponse response) throws IOException, InterruptedException {
final ExchangeMock exchange;
synchronized (exchange = ((RequestMock) request).exchange) {
super.handleGetRideRequestData(request,response);
exchange.notify();
}
}
/**
* handle POST (Ride)
* - wait for Ride to be booked
* - send OK to Client
*/
@Override
protected void handlePostRide(HttpServletRequest request, HttpServletResponse response) throws IOException, InterruptedException {
final ExchangeMock exchange;
synchronized (exchange = ((RequestMock) request).exchange) {
super.handlePostRide(request,response);
exchange.notify();
}
}
}

@ -0,0 +1,21 @@
package com.olexyn.misp.mock;
/**
* Pass the MockSet.
* Provide a Runnable.
*/
public class BridgeRunnable implements Runnable {
MockSet mockSet;
public BridgeRunnable(MockSet mockSet){
super();
this.mockSet = mockSet;
}
@Override
public void run() {
new BridgeMock(mockSet);
}
}

@ -0,0 +1,42 @@
package com.olexyn.misp.mock;
import com.olexyn.misp.mock.actor.AppMock;
import com.olexyn.misp.mock.actor.UserMock;
public class Main {
public static void main(String... args){
MockSet mockSet = new MockSet();
Runnable publicRunnable = new UserMock(mockSet);
Runnable bridgeRunable = new BridgeRunnable(mockSet);
Runnable adapterRunnable = new AdapterRunnable(mockSet);
//Runnable clientRunnable = new ClientRunnable(mockSet);
Runnable appRunnable = new AppMock(mockSet);
Thread userThread = new Thread(publicRunnable);
Thread bridgeThread = new Thread(bridgeRunable);
Thread adapterThread = new Thread(adapterRunnable);
//Thread clientThread = new Thread(clientRunnable);
Thread appThread = new Thread(appRunnable);
userThread.setName("userThread");
userThread.start();
bridgeThread.setName("bridgeThread");
bridgeThread.start();
adapterThread.setName("adapterThread");
adapterThread.start();
//clientThread.setName("clientThread");
//clientThread.start();
appThread.setName("appThread");
appThread.start();
}
}

@ -0,0 +1,11 @@
package com.olexyn.misp.mock;
import com.olexyn.misp.mock.actor.AppMock;
import com.olexyn.misp.mock.actor.UserMock;
public class MockSet {
public UserMock userMock;
public BridgeMock bridgeMock;
public AdapterMock adapterMock;
public AppMock appMock;
}

@ -0,0 +1,57 @@
package com.olexyn.misp.mock.actor;
import com.olexyn.misp.mock.MockSet;
import com.olexyn.misp.mock.exchange.ExchangeMock;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Generic Runnable.
* Serves as basis for Actors that are not Servlets.
*/
public abstract class ActorRunnable implements Runnable {
List<ExchangeMock> exchanges = new ArrayList<>();
protected MockSet mockSet;
public ActorRunnable(MockSet mockSet){
this.mockSet = mockSet;
}
@Override
public void run() {
//
}
public void processExchange(ExchangeMock exchange) throws IOException, ServletException {
if (exchange.request.getMethod().equalsIgnoreCase("GET")) {
doGet(exchange.request, exchange.response);
} else if (exchange.request.getMethod().equalsIgnoreCase("POST")) {
doPost(exchange.request, exchange.response);
}
}
public abstract void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException;
public abstract void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException;
}

@ -0,0 +1,56 @@
package com.olexyn.misp.mock.actor;
import com.olexyn.misp.mock.MockSet;
import com.olexyn.misp.mock.exchange.ExchangeMock;
import com.olexyn.misp.mock.exchange.RequestMock;
import org.apache.commons.io.IOUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class AppMock extends ActorRunnable {
public AppMock(MockSet mockSet) {
super(mockSet);
mockSet.appMock = this;
}
@Override
public void run() {
while (true) {
}
}
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
RequestMock mockRequest = (RequestMock) request;
ExchangeMock exchange = mockRequest.exchange;
synchronized (exchange) {
String parsedRequest = IOUtils.toString(request.getReader());
String dataString = "DATA-" + parsedRequest;
exchange.response.setStatus(200);
PrintWriter writer = exchange.response.getWriter();
writer.write(dataString);
writer.flush();
writer.close();
exchange.notify();
}
}
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response) {
}
}

@ -0,0 +1,91 @@
package com.olexyn.misp.mock.actor;
import com.olexyn.misp.mock.MockSet;
import com.olexyn.misp.mock.exchange.ExchangeMock;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class UserMock extends ActorRunnable {
final String longRequest;
int requestCount = 0;
public UserMock(MockSet mockSet){
super(mockSet);
mockSet.userMock = this;
StringBuilder sb = new StringBuilder();
for (int i=0;i<100;i++){
sb.append("foo");
}
longRequest = sb.toString();
}
@Override
public void run() {
while (true){
try {
sendGetRequest();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
}
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response) {
}
/**
* # send GET (Request)
* Generated by Loop
*/
void sendGetRequest() throws IOException, InterruptedException {
// Mock Exchange
ExchangeMock exchange = new ExchangeMock();
exchange.request.setMethod("GET");
//exchange.request.setContentType("application/json");
//String requestBody = longRequest+"-"+(++requestCount);
String requestBody = "REQUEST-"+(++requestCount);
String jsonString = "{\"request\":\""+requestBody+ "\"}";
jsonString = "asdfasdfa";
exchange.request.setContent(jsonString.getBytes());
synchronized (exchange){
// Mock GET (Request)
exchange.notify();
mockSet.bridgeMock.doGet(exchange.request,exchange.response);
exchange.wait();
// handle OK (Data)
String data = exchange.response.getContentAsString();
System.out.println(data + " of "+requestBody);
exchange.notify();
}
}
}

@ -0,0 +1,50 @@
package com.olexyn.misp.mock.exchange;
import java.util.ArrayList;
import java.util.List;
/**
* How "exchange" mocks an exchange:<br>
* <br>
* An "exchange" corresponds to request & reply pair.<br>
* - i.e. a GET & OK pair.<br>
* Steps:<br>
* 1. new Exchange()<br>
* 2. set properties of .request<br>
* 3. "send" request by using exchange.notify(); recipient.doGet();<br>
* 4. recipient does someting with request<br>
* - - recipient sets proerties of .response<br>
* - - recipient sends reply using exchange.notify();<br>
* 5. "receive" response by using exchange.wait();
*/
public class ExchangeMock {
public static List<ExchangeMock> exchangeList = new ArrayList<>();
private static int next_id=0;
public int id;
public RequestMock request =new RequestMock(this);
public ResponseMock response = new ResponseMock(this);
public ExchangeMock(){
id = next_id++;
exchangeList.add(this);
}
}

@ -0,0 +1,84 @@
package com.olexyn.misp.mock.exchange;
import org.springframework.mock.web.MockHttpServletRequest;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import java.io.IOException;
import java.util.Collection;
/**
* - Contains reference to ExchangeMock
* - Rest is dummy code.
*/
public class RequestMock extends MockHttpServletRequest implements HttpServletRequest {
public final ExchangeMock exchange;
public RequestMock(ExchangeMock exchange){
super();
this.exchange = exchange;
}
@Override
public boolean authenticate(HttpServletResponse httpServletResponse) throws IOException, ServletException {
return false;
}
@Override
public void login(String s, String s1) throws ServletException {
}
@Override
public void logout() throws ServletException {
}
@Override
public Collection<Part> getParts() throws IOException, ServletException {
return null;
}
@Override
public Part getPart(String s) throws IOException, ServletException {
return null;
}
@Override
public ServletContext getServletContext() {
return null;
}
@Override
public AsyncContext startAsync() throws IllegalStateException {
return null;
}
@Override
public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException {
return null;
}
@Override
public boolean isAsyncStarted() {
return false;
}
@Override
public boolean isAsyncSupported() {
return false;
}
@Override
public AsyncContext getAsyncContext() {
return null;
}
@Override
public DispatcherType getDispatcherType() {
return null;
}
}

@ -0,0 +1,209 @@
package com.olexyn.misp.mock.exchange;
import org.springframework.mock.web.MockHttpServletResponse;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.Locale;
import java.util.Set;
/**
* - Contains reference to ExchangeMock
* - Rest is dummy code.
*/
public class ResponseMock extends MockHttpServletResponse implements HttpServletResponse {
public ExchangeMock exchange;
public ResponseMock(ExchangeMock exchange){
super();
this.exchange = exchange;
}
@Override
public void addCookie(Cookie cookie) {
}
@Override
public boolean containsHeader(String s) {
return false;
}
@Override
public String encodeURL(String s) {
return null;
}
@Override
public String encodeRedirectURL(String s) {
return null;
}
@Override
public String encodeUrl(String s) {
return null;
}
@Override
public String encodeRedirectUrl(String s) {
return null;
}
@Override
public void sendError(int i, String s) throws IOException {
}
@Override
public void sendError(int i) throws IOException {
}
@Override
public void sendRedirect(String s) throws IOException {
}
@Override
public void setDateHeader(String s, long l) {
}
@Override
public void addDateHeader(String s, long l) {
}
@Override
public void setHeader(String s, String s1) {
}
@Override
public void addHeader(String s, String s1) {
}
@Override
public void setIntHeader(String s, int i) {
}
@Override
public void addIntHeader(String s, int i) {
}
@Override
public void setStatus(int i) {
}
@Override
public void setStatus(int i, String s) {
}
@Override
public int getStatus() {
return 0;
}
@Override
public String getHeader(String s) {
return null;
}
@Override
public List getHeaders(String s) {
return null;
}
@Override
public Set getHeaderNames() {
return null;
}
@Override
public String getCharacterEncoding() {
return null;
}
@Override
public String getContentType() {
return null;
}
@Override
public ServletOutputStream getOutputStream() {
return null;
}
@Override
public PrintWriter getWriter() throws UnsupportedEncodingException {
return super.getWriter();
}
@Override
public void setCharacterEncoding(String s) {
}
@Override
public void setContentLength(int i) {
}
@Override
public void setContentType(String s) {
}
@Override
public void setBufferSize(int i) {
}
@Override
public int getBufferSize() {
return 0;
}
@Override
public void flushBuffer() {
}
@Override
public void resetBuffer() {
}
@Override
public boolean isCommitted() {
return false;
}
@Override
public void reset() {
}
@Override
public void setLocale(Locale locale) {
}
@Override
public Locale getLocale() {
return null;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

@ -0,0 +1,340 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<diagram program="umlet" version="13.3">
<zoom_level>10</zoom_level>
<element>
<id>UMLClass</id>
<coordinates>
<x>910</x>
<y>50</y>
<w>90</w>
<h>30</h>
</coordinates>
<panel_attributes>inet
lt=-</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>680</x>
<y>50</y>
<w>100</w>
<h>30</h>
</coordinates>
<panel_attributes>webhost
lt=-
layer=-1</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>910</x>
<y>90</y>
<w>90</w>
<h>30</h>
</coordinates>
<panel_attributes>user
bg=#90CAF9</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>680</x>
<y>90</y>
<w>100</w>
<h>30</h>
</coordinates>
<panel_attributes>mispbridge
bg=#B39DDB
layer=-1</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>410</x>
<y>140</y>
<w>300</w>
<h>50</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;&lt;-
POST (Ride)
Generated by Loop</panel_attributes>
<additional_attributes>280.0;20.0;10.0;20.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>760</x>
<y>180</y>
<w>200</w>
<h>40</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;&lt;-
GET (Request)
fg=#1E88E5</panel_attributes>
<additional_attributes>10.0;20.0;180.0;20.0</additional_attributes>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>350</x>
<y>90</y>
<w>100</w>
<h>30</h>
</coordinates>
<panel_attributes>mispclient
bg=#B39DDB
layer=-1</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>410</x>
<y>290</y>
<w>300</w>
<h>50</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;&lt;-
GET (Ride)(Request)
(Data)</panel_attributes>
<additional_attributes>280.0;20.0;10.0;20.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>760</x>
<y>310</y>
<w>190</w>
<h>40</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;&lt;.
OK (Data)
fg=#1E88E5</panel_attributes>
<additional_attributes>170.0;20.0;10.0;20.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>410</x>
<y>350</y>
<w>300</w>
<h>40</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;&lt;.
OK (Ride)</panel_attributes>
<additional_attributes>10.0;20.0;280.0;20.0</additional_attributes>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>150</x>
<y>90</y>
<w>80</w>
<h>30</h>
</coordinates>
<panel_attributes>app
bg=#90CAF9</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>200</x>
<y>220</y>
<w>200</w>
<h>40</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;&lt;-
GET (Request)
fg=#1E88E5</panel_attributes>
<additional_attributes>10.0;20.0;180.0;20.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>200</x>
<y>270</y>
<w>200</w>
<h>40</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;&lt;.
OK (Data)
fg=#1E88E5</panel_attributes>
<additional_attributes>180.0;20.0;10.0;20.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>410</x>
<y>200</y>
<w>300</w>
<h>40</h>
</coordinates>
<panel_attributes>lt=&lt;&lt;&lt;.
OK (Ride)(Request)</panel_attributes>
<additional_attributes>10.0;20.0;280.0;20.0</additional_attributes>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>150</x>
<y>50</y>
<w>300</w>
<h>30</h>
</coordinates>
<panel_attributes>localhost
lt=-</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>120</x>
<y>20</y>
<w>910</w>
<h>400</h>
</coordinates>
<panel_attributes>
lt=..
layer=-10</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>940</x>
<y>110</y>
<w>30</w>
<h>310</h>
</coordinates>
<panel_attributes>lt=-
fg=#1E88E5</panel_attributes>
<additional_attributes>10.0;10.0;10.0;290.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>180</x>
<y>110</y>
<w>30</w>
<h>310</h>
</coordinates>
<panel_attributes>lt=-
fg=#1E88E5</panel_attributes>
<additional_attributes>10.0;10.0;10.0;290.0</additional_attributes>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>930</x>
<y>200</y>
<w>40</w>
<h>130</h>
</coordinates>
<panel_attributes>
bg=#F6F6F6
transparency=0
layer=4</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>170</x>
<y>240</y>
<w>40</w>
<h>50</h>
</coordinates>
<panel_attributes>
bg=#F6F6F6
transparency=0
layer=4</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>720</x>
<y>110</y>
<w>30</w>
<h>310</h>
</coordinates>
<panel_attributes>lt=-
fg=#5E35B1
layer=-4</panel_attributes>
<additional_attributes>10.0;10.0;10.0;290.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>390</x>
<y>110</y>
<w>30</w>
<h>310</h>
</coordinates>
<panel_attributes>lt=-
fg=#5E35B1
layer=-4</panel_attributes>
<additional_attributes>10.0;10.0;10.0;290.0</additional_attributes>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>380</x>
<y>160</y>
<w>40</w>
<h>210</h>
</coordinates>
<panel_attributes>
bg=#F6F6F6
transparency=0
layer=4</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>690</x>
<y>160</y>
<w>40</w>
<h>60</h>
</coordinates>
<panel_attributes>
bg=#F6F6F6
transparency=0
layer=4</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>690</x>
<y>310</y>
<w>40</w>
<h>60</h>
</coordinates>
<panel_attributes>
bg=#F6F6F6
transparency=0
layer=4</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>730</x>
<y>200</y>
<w>40</w>
<h>130</h>
</coordinates>
<panel_attributes>
bg=#F6F6F6
transparency=0
layer=4</panel_attributes>
<additional_attributes/>
</element>
</diagram>

@ -0,0 +1,204 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<diagram program="umlet" version="13.3">
<zoom_level>10</zoom_level>
<element>
<id>UMLClass</id>
<coordinates>
<x>670</x>
<y>1040</y>
<w>130</w>
<h>30</h>
</coordinates>
<panel_attributes>TinyVNC</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>670</x>
<y>1010</y>
<w>130</w>
<h>30</h>
</coordinates>
<panel_attributes>Guacamole</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>650</x>
<y>970</y>
<w>170</w>
<h>150</h>
</coordinates>
<panel_attributes>Debian VM</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>630</x>
<y>910</y>
<w>210</w>
<h>230</h>
</coordinates>
<panel_attributes>VirtualBox
layer=-1</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>670</x>
<y>1070</y>
<w>130</w>
<h>30</h>
</coordinates>
<panel_attributes>JavaFX</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>650</x>
<y>940</y>
<w>170</w>
<h>30</h>
</coordinates>
<panel_attributes>Bridged Connection</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>610</x>
<y>880</y>
<w>250</w>
<h>280</h>
</coordinates>
<panel_attributes>Debian Host
layer=-1</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>670</x>
<y>760</y>
<w>130</w>
<h>30</h>
</coordinates>
<panel_attributes>4G Modem</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>910</x>
<y>610</y>
<w>100</w>
<h>30</h>
</coordinates>
<panel_attributes>URL</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>650</x>
<y>580</y>
<w>170</w>
<h>80</h>
</coordinates>
<panel_attributes>Hosting Provider</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>910</x>
<y>760</y>
<w>100</w>
<h>30</h>
</coordinates>
<panel_attributes>IP / Port</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>910</x>
<y>940</y>
<w>100</w>
<h>30</h>
</coordinates>
<panel_attributes>IP / Port</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>960</x>
<y>780</y>
<w>30</w>
<h>180</h>
</coordinates>
<panel_attributes>lt=&lt;-&gt;</panel_attributes>
<additional_attributes>10.0;10.0;10.0;160.0</additional_attributes>
</element>
<element>
<id>UMLClass</id>
<coordinates>
<x>670</x>
<y>610</y>
<w>130</w>
<h>30</h>
</coordinates>
<panel_attributes>WordPress</panel_attributes>
<additional_attributes/>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>810</x>
<y>940</y>
<w>120</w>
<h>30</h>
</coordinates>
<panel_attributes>lt=-</panel_attributes>
<additional_attributes>100.0;10.0;10.0;10.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>790</x>
<y>760</y>
<w>140</w>
<h>30</h>
</coordinates>
<panel_attributes>lt=-</panel_attributes>
<additional_attributes>120.0;10.0;10.0;10.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>960</x>
<y>630</y>
<w>30</w>
<h>150</h>
</coordinates>
<panel_attributes>lt=&lt;-&gt;</panel_attributes>
<additional_attributes>10.0;10.0;10.0;130.0</additional_attributes>
</element>
<element>
<id>Relation</id>
<coordinates>
<x>790</x>
<y>610</y>
<w>140</w>
<h>30</h>
</coordinates>
<panel_attributes>lt=-</panel_attributes>
<additional_attributes>120.0;10.0;10.0;10.0</additional_attributes>
</element>
</diagram>

@ -0,0 +1,113 @@
<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<!-- =============================================================== -->
<!-- Documentation of this file format can be found at: -->
<!-- https://www.eclipse.org/jetty/documentation/current/ -->
<!-- -->
<!-- Additional configuration files are available in $JETTY_HOME/etc -->
<!-- and can be mixed in. See start.ini file for the default -->
<!-- configuration files. -->
<!-- -->
<!-- For a description of the configuration mechanism, see the -->
<!-- output of: -->
<!-- java -jar start.jar -? -->
<!-- =============================================================== -->
<!-- =============================================================== -->
<!-- Configure a Jetty Server instance with an ID "Server" -->
<!-- Other configuration files may also configure the "Server" -->
<!-- ID, in which case they are adding configuration to the same -->
<!-- instance. If other configuration have a different ID, they -->
<!-- will create and configure another instance of Jetty. -->
<!-- Consult the javadoc of o.e.j.server.Server for all -->
<!-- configuration that may be set here. -->
<!-- =============================================================== -->
<Configure id="Server" class="org.eclipse.jetty.server.Server">
<Arg name="threadpool"><Ref refid="threadPool"/></Arg>
<Call name="addBean">
<Arg><Ref refid="byteBufferPool"/></Arg>
</Call>
<!-- =========================================================== -->
<!-- Add shared Scheduler instance -->
<!-- =========================================================== -->
<Call name="addBean">
<Arg>
<New class="org.eclipse.jetty.util.thread.ScheduledExecutorScheduler">
<Arg name="name"><Property name="jetty.scheduler.name"/></Arg>
<Arg name="daemon" type="boolean"><Property name="jetty.scheduler.daemon" default="false" /></Arg>
<Arg name="threads" type="int"><Property name="jetty.scheduler.threads" default="-1" /></Arg>
</New>
</Arg>
</Call>
<!-- =========================================================== -->
<!-- Http Configuration. -->
<!-- This is a common configuration instance used by all -->
<!-- connectors that can carry HTTP semantics (HTTP, HTTPS, etc.)-->
<!-- It configures the non wire protocol aspects of the HTTP -->
<!-- semantic. -->
<!-- -->
<!-- This configuration is only defined here and is used by -->
<!-- reference from other XML files such as jetty-http.xml, -->
<!-- jetty-https.xml and other configuration files which -->
<!-- instantiate the connectors. -->
<!-- -->
<!-- Consult the javadoc of o.e.j.server.HttpConfiguration -->
<!-- for all configuration that may be set here. -->
<!-- =========================================================== -->
<New id="httpConfig" class="org.eclipse.jetty.server.HttpConfiguration">
<Set name="secureScheme"><Property name="jetty.httpConfig.secureScheme" default="https" /></Set>
<Set name="securePort"><Property name="jetty.httpConfig.securePort" deprecated="jetty.secure.port" default="8443" /></Set>
<Set name="outputBufferSize"><Property name="jetty.httpConfig.outputBufferSize" deprecated="jetty.output.buffer.size" default="32768" /></Set>
<Set name="outputAggregationSize"><Property name="jetty.httpConfig.outputAggregationSize" deprecated="jetty.output.aggregation.size" default="8192" /></Set>
<Set name="requestHeaderSize"><Property name="jetty.httpConfig.requestHeaderSize" deprecated="jetty.request.header.size" default="8192" /></Set>
<Set name="responseHeaderSize"><Property name="jetty.httpConfig.responseHeaderSize" deprecated="jetty.response.header.size" default="8192" /></Set>
<Set name="sendServerVersion"><Property name="jetty.httpConfig.sendServerVersion" deprecated="jetty.send.server.version" default="true" /></Set>
<Set name="sendDateHeader"><Property name="jetty.httpConfig.sendDateHeader" deprecated="jetty.send.date.header" default="false" /></Set>
<Set name="headerCacheSize"><Property name="jetty.httpConfig.headerCacheSize" default="1024" /></Set>
<Set name="delayDispatchUntilContent"><Property name="jetty.httpConfig.delayDispatchUntilContent" deprecated="jetty.delayDispatchUntilContent" default="true"/></Set>
<Set name="maxErrorDispatches"><Property name="jetty.httpConfig.maxErrorDispatches" default="10"/></Set>
<Set name="blockingTimeout"><Property deprecated="jetty.httpConfig.blockingTimeout" name="jetty.httpConfig.blockingTimeout.DEPRECATED" default="-1"/></Set>
<Set name="persistentConnectionsEnabled"><Property name="jetty.httpConfig.persistentConnectionsEnabled" default="true"/></Set>
<Set name="requestCookieCompliance"><Call class="org.eclipse.jetty.http.CookieCompliance" name="valueOf"><Arg><Property name="jetty.httpConfig.requestCookieCompliance" deprecated="jetty.httpConfig.cookieCompliance" default="RFC6265"/></Arg></Call></Set>
<Set name="responseCookieCompliance"><Call class="org.eclipse.jetty.http.CookieCompliance" name="valueOf"><Arg><Property name="jetty.httpConfig.responseCookieCompliance" default="RFC6265"/></Arg></Call></Set>
<Set name="multiPartFormDataCompliance"><Call class="org.eclipse.jetty.server.MultiPartFormDataCompliance" name="valueOf"><Arg><Property name="jetty.httpConfig.multiPartFormDataCompliance" default="RFC7578"/></Arg></Call></Set>
</New>
<!-- =========================================================== -->
<!-- Set the default handler structure for the Server -->
<!-- A handler collection is used to pass received requests to -->
<!-- both the ContextHandlerCollection, which selects the next -->
<!-- handler by context path and virtual host, and the -->
<!-- DefaultHandler, which handles any requests not handled by -->
<!-- the context handlers. -->
<!-- Other handlers may be added to the "Handlers" collection, -->
<!-- for example the jetty-requestlog.xml file adds the -->
<!-- RequestLogHandler after the default handler -->
<!-- =========================================================== -->
<Set name="handler">
<New id="Handlers" class="org.eclipse.jetty.server.handler.HandlerCollection">
<Set name="handlers">
<Array type="org.eclipse.jetty.server.Handler">
<Item>
<New id="Contexts" class="org.eclipse.jetty.server.handler.ContextHandlerCollection"/>
</Item>
<Item>
<New id="DefaultHandler" class="org.eclipse.jetty.server.handler.DefaultHandler"/>
</Item>
</Array>
</Set>
</New>
</Set>
<!-- =========================================================== -->
<!-- extra server options -->
<!-- =========================================================== -->
<Set name="stopAtShutdown"><Property name="jetty.server.stopAtShutdown" default="true"/></Set>
<Set name="stopTimeout"><Property name="jetty.server.stopTimeout" default="5000"/></Set>
<Set name="dumpAfterStart"><Property name="jetty.server.dumpAfterStart" deprecated="jetty.dump.start" default="false"/></Set>
<Set name="dumpBeforeStop"><Property name="jetty.server.dumpBeforeStop" deprecated="jetty.dump.stop" default="false"/></Set>
</Configure>

@ -0,0 +1,10 @@
#!/bin/bash
version="0.1"
file="target/test-proxy-${version}.war"
groupId="com.olexyn.test.proxy"
artifactId="test-proxy"
mvn package
mvn install:install-file -Dfile=${file} -DgroupId=${groupId} -DartifactId=${artifactId} -Dversion=${version} -Dpackaging=war -DgeneratePom=true

@ -15,8 +15,8 @@
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source> <maven.compiler.source>1.11</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target> <maven.compiler.target>1.11</maven.compiler.target>
</properties> </properties>
<dependencies> <dependencies>
@ -26,10 +26,61 @@
<version>4.11</version> <version>4.11</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>9.4.28.v20200408</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
<version>9.4.28.v20200408</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-client</artifactId>
<version>9.4.28.v20200408</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-test-helper</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-test-helper</artifactId>
<version>5.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-http</artifactId>
<version>9.4.28.v20200408</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-io</artifactId>
<version>9.4.28.v20200408</version>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>
<finalName>test-proxy</finalName>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) --> <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins> <plugins>
<plugin> <plugin>
@ -44,6 +95,12 @@
<plugin> <plugin>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version> <version>3.8.0</version>
<configuration>
<!-- or whatever version you use -->
<source>11</source>
<target>11</target>
<verbose>true</verbose>
</configuration>
</plugin> </plugin>
<plugin> <plugin>
<artifactId>maven-surefire-plugin</artifactId> <artifactId>maven-surefire-plugin</artifactId>

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_0.dtd">
<Configure id="server" class="org.eclipse.jetty.server.Server">
<New id="ServletContextHandler" class="org.eclipse.jetty.servlet.ServletContextHandler">
<Set name="servletHandler">
<New id="servletHandler" class="org.eclipse.jetty.servlet.ServletHandler">
<Call id="proxyHolder" name="addServletWithMapping">
<Arg>org.eclipse.jetty.servlets.ProxyServlet$Transparent</Arg>
<Arg>/proxy/*</Arg>
<Call name="setInitParameter">
<Arg>maxThreads</Arg>
<Arg>
<Property name="jetty.proxy.maxThreads" default="128"/>
</Arg>
</Call>
<Call name="setInitParameter">
<Arg>maxConnections</Arg>
<Arg>
<Property name="jetty.proxy.maxConnections" default="256"/>
</Arg>
</Call>
<Call name="setInitParameter">
<Arg>idleTimeout</Arg>
<Arg>
<Property name="jetty.proxy.idleTimeout" default="30000"/>
</Arg>
</Call>
<Call name="setInitParameter">
<Arg>timeout</Arg>
<Arg>
<Property name="jetty.proxy.timeout" default="60000"/>
</Arg>
</Call>
<Call name="setInitParameter">
<Arg>ProxyTo</Arg>
<Arg>http://localhost:8080/misp-mirror</Arg>
</Call>
<Call name="setInitParameter">
<Arg>Prefix</Arg>
<Arg>/proxy</Arg>
</Call>
</Call>
</New>
</Set>
</New>
<Set name="handler">
<New class="org.eclipse.jetty.server.handler.HandlerList">
<Set name="handlers">
<Array type="org.eclipse.jetty.server.Handler">
<Item>
<Ref id="ServletContextHandler"></Ref>
</Item>
</Array>
</Set>
</New>
</Set>
</Configure>

@ -1,36 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
// This module is a mixed bag of things.
// There are some utility classes that only depend on Servlet APIs,
// but other utility classes that depend on some Jetty module.
module org.eclipse.jetty.servlets
{
exports org.eclipse.jetty.servlets;
requires transitive jetty.servlet.api;
requires org.slf4j;
// Only required if using CloseableDoSFilter.
requires static org.eclipse.jetty.io;
// Only required if using DoSFilter, PushCacheFilter, etc.
requires static org.eclipse.jetty.http;
requires static org.eclipse.jetty.server;
// Only required if using CrossOriginFilter, DoSFilter, etc.
requires static org.eclipse.jetty.util;
}

@ -1,572 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package servlets;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.UrlEncoded;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
/**
* CGI Servlet.
* <p>
* The following init parameters are used to configure this servlet:
* <dl>
* <dt>cgibinResourceBase</dt>
* <dd>Path to the cgi bin directory if set or it will default to the resource base of the context.</dd>
* <dt>resourceBase</dt>
* <dd>An alias for cgibinResourceBase.</dd>
* <dt>cgibinResourceBaseIsRelative</dt>
* <dd>If true then cgibinResourceBase is relative to the webapp (eg "WEB-INF/cgi")</dd>
* <dt>commandPrefix</dt>
* <dd>may be used to set a prefix to all commands passed to exec. This can be used on systems that need assistance to execute a
* particular file type. For example on windows this can be set to "perl" so that perl scripts are executed.</dd>
* <dt>Path</dt>
* <dd>passed to the exec environment as PATH.</dd>
* <dt>ENV_*</dt>
* <dd>used to set an arbitrary environment variable with the name stripped of the leading ENV_ and using the init parameter value</dd>
* <dt>useFullPath</dt>
* <dd>If true, the full URI path within the context is used for the exec command, otherwise a search is done for a partial URL that matches an exec Command</dd>
* <dt>ignoreExitState</dt>
* <dd>If true then do not act on a non-zero exec exit status")</dd>
* </dl>
*/
public class CGI extends HttpServlet
{
private static final long serialVersionUID = -6182088932884791074L;
private static final Logger LOG = LoggerFactory.getLogger(CGI.class);
private boolean _ok;
private File _docRoot;
private boolean _cgiBinProvided;
private String _path;
private String _cmdPrefix;
private boolean _useFullPath;
private EnvList _env;
private boolean _ignoreExitState;
private boolean _relative;
@Override
public void init() throws ServletException
{
_env = new EnvList();
_cmdPrefix = getInitParameter("commandPrefix");
_useFullPath = Boolean.parseBoolean(getInitParameter("useFullPath"));
_relative = Boolean.parseBoolean(getInitParameter("cgibinResourceBaseIsRelative"));
String tmp = getInitParameter("cgibinResourceBase");
if (tmp != null)
_cgiBinProvided = true;
else
{
tmp = getInitParameter("resourceBase");
if (tmp != null)
_cgiBinProvided = true;
else
tmp = getServletContext().getRealPath("/");
}
if (_relative && _cgiBinProvided)
{
tmp = getServletContext().getRealPath(tmp);
}
if (tmp == null)
{
LOG.warn("CGI: no CGI bin !");
return;
}
File dir = new File(tmp);
if (!dir.exists())
{
LOG.warn("CGI: CGI bin does not exist - " + dir);
return;
}
if (!dir.canRead())
{
LOG.warn("CGI: CGI bin is not readable - " + dir);
return;
}
if (!dir.isDirectory())
{
LOG.warn("CGI: CGI bin is not a directory - " + dir);
return;
}
try
{
_docRoot = dir.getCanonicalFile();
}
catch (IOException e)
{
LOG.warn("CGI: CGI bin failed - " + dir, e);
return;
}
_path = getInitParameter("Path");
if (_path != null)
_env.set("PATH", _path);
_ignoreExitState = "true".equalsIgnoreCase(getInitParameter("ignoreExitState"));
Enumeration<String> e = getInitParameterNames();
while (e.hasMoreElements())
{
String n = e.nextElement();
if (n != null && n.startsWith("ENV_"))
_env.set(n.substring(4), getInitParameter(n));
}
if (!_env.envMap.containsKey("SystemRoot"))
{
String os = System.getProperty("os.name");
if (os != null && os.toLowerCase(Locale.ENGLISH).contains("windows"))
{
_env.set("SystemRoot", "C:\\WINDOWS");
}
}
_ok = true;
}
@Override
public void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException
{
if (!_ok)
{
res.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
return;
}
if (LOG.isDebugEnabled())
{
LOG.debug("CGI: ContextPath : " + req.getContextPath());
LOG.debug("CGI: ServletPath : " + req.getServletPath());
LOG.debug("CGI: PathInfo : " + req.getPathInfo());
LOG.debug("CGI: _docRoot : " + _docRoot);
LOG.debug("CGI: _path : " + _path);
LOG.debug("CGI: _ignoreExitState: " + _ignoreExitState);
}
// pathInContext may actually comprises scriptName/pathInfo...We will
// walk backwards up it until we find the script - the rest must
// be the pathInfo;
String pathInContext = (_relative ? "" : StringUtil.nonNull(req.getServletPath())) + StringUtil.nonNull(req.getPathInfo());
File execCmd = new File(_docRoot, pathInContext);
String pathInfo = pathInContext;
if (!_useFullPath)
{
String path = pathInContext;
String info = "";
// Search docroot for a matching execCmd
while ((path.endsWith("/") || !execCmd.exists()) && path.length() >= 0)
{
int index = path.lastIndexOf('/');
path = path.substring(0, index);
info = pathInContext.substring(index, pathInContext.length());
execCmd = new File(_docRoot, path);
}
if (path.length() == 0 || !execCmd.exists() || execCmd.isDirectory() || !execCmd.getCanonicalPath().equals(execCmd.getAbsolutePath()))
{
res.sendError(404);
}
pathInfo = info;
}
exec(execCmd, pathInfo, req, res);
}
/**
* executes the CGI process
*
* @param command the command to execute, this command is prefixed by
* the context parameter "commandPrefix".
* @param pathInfo The PATH_INFO to process,
* see http://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html#getPathInfo%28%29. Cannot be null
* @param req the HTTP request
* @param res the HTTP response
* @throws IOException if the execution of the CGI process throws
*/
private void exec(File command, String pathInfo, HttpServletRequest req, HttpServletResponse res) throws IOException
{
assert req != null;
assert res != null;
assert pathInfo != null;
assert command != null;
if (LOG.isDebugEnabled())
{
LOG.debug("CGI: script is " + command);
LOG.debug("CGI: pathInfo is " + pathInfo);
}
String bodyFormEncoded = null;
if ((HttpMethod.POST.is(req.getMethod()) || HttpMethod.PUT.is(req.getMethod())) && "application/x-www-form-urlencoded".equals(req.getContentType()))
{
MultiMap<String> parameterMap = new MultiMap<>();
Enumeration<String> names = req.getParameterNames();
while (names.hasMoreElements())
{
String parameterName = names.nextElement();
parameterMap.addValues(parameterName, req.getParameterValues(parameterName));
}
String characterEncoding = req.getCharacterEncoding();
Charset charset = characterEncoding != null
? Charset.forName(characterEncoding) : StandardCharsets.UTF_8;
bodyFormEncoded = UrlEncoded.encode(parameterMap, charset, true);
}
EnvList env = new EnvList(_env);
// these ones are from "The WWW Common Gateway Interface Version 1.1"
// look at :
// http://Web.Golux.Com/coar/cgi/draft-coar-cgi-v11-03-clean.html#6.1.1
env.set("AUTH_TYPE", req.getAuthType());
int contentLen = req.getContentLength();
if (contentLen < 0)
contentLen = 0;
if (bodyFormEncoded != null)
{
env.set("CONTENT_LENGTH", Integer.toString(bodyFormEncoded.length()));
}
else
{
env.set("CONTENT_LENGTH", Integer.toString(contentLen));
}
env.set("CONTENT_TYPE", req.getContentType());
env.set("GATEWAY_INTERFACE", "CGI/1.1");
if (pathInfo.length() > 0)
{
env.set("PATH_INFO", pathInfo);
}
String pathTranslated = req.getPathTranslated();
if ((pathTranslated == null) || (pathTranslated.length() == 0))
pathTranslated = pathInfo;
env.set("PATH_TRANSLATED", pathTranslated);
env.set("QUERY_STRING", req.getQueryString());
env.set("REMOTE_ADDR", req.getRemoteAddr());
env.set("REMOTE_HOST", req.getRemoteHost());
// The identity information reported about the connection by a
// RFC 1413 [11] request to the remote agent, if
// available. Servers MAY choose not to support this feature, or
// not to request the data for efficiency reasons.
// "REMOTE_IDENT" => "NYI"
env.set("REMOTE_USER", req.getRemoteUser());
env.set("REQUEST_METHOD", req.getMethod());
String scriptPath;
String scriptName;
// use docRoot for scriptPath, too
if (_cgiBinProvided)
{
scriptPath = command.getAbsolutePath();
scriptName = scriptPath.substring(_docRoot.getAbsolutePath().length());
}
else
{
String requestURI = req.getRequestURI();
scriptName = requestURI.substring(0, requestURI.length() - pathInfo.length());
scriptPath = getServletContext().getRealPath(scriptName);
}
env.set("SCRIPT_FILENAME", scriptPath);
env.set("SCRIPT_NAME", scriptName);
env.set("SERVER_NAME", req.getServerName());
env.set("SERVER_PORT", Integer.toString(req.getServerPort()));
env.set("SERVER_PROTOCOL", req.getProtocol());
env.set("SERVER_SOFTWARE", getServletContext().getServerInfo());
Enumeration<String> enm = req.getHeaderNames();
while (enm.hasMoreElements())
{
String name = enm.nextElement();
if (name.equalsIgnoreCase("Proxy"))
continue;
String value = req.getHeader(name);
env.set("HTTP_" + StringUtil.replace(name.toUpperCase(Locale.ENGLISH), '-', '_'), value);
}
// these extra ones were from printenv on www.dev.nomura.co.uk
env.set("HTTPS", (req.isSecure() ? "ON" : "OFF"));
// "DOCUMENT_ROOT" => root + "/docs",
// "SERVER_URL" => "NYI - http://us0245",
// "TZ" => System.getProperty("user.timezone"),
// are we meant to decode args here? or does the script get them
// via PATH_INFO? if we are, they should be decoded and passed
// into exec here...
String absolutePath = command.getAbsolutePath();
String execCmd = absolutePath;
// escape the execCommand
if (execCmd.length() > 0 && execCmd.charAt(0) != '"' && execCmd.contains(" "))
execCmd = "\"" + execCmd + "\"";
if (_cmdPrefix != null)
execCmd = _cmdPrefix + " " + execCmd;
LOG.debug("Environment: " + env.getExportString());
LOG.debug("Command: " + execCmd);
final Process p = Runtime.getRuntime().exec(execCmd, env.getEnvArray(), _docRoot);
// hook processes input to browser's output (async)
if (bodyFormEncoded != null)
writeProcessInput(p, bodyFormEncoded);
else if (contentLen > 0)
writeProcessInput(p, req.getInputStream(), contentLen);
// hook processes output to browser's input (sync)
// if browser closes stream, we should detect it and kill process...
OutputStream os = null;
AsyncContext async = req.startAsync();
try
{
async.start(new Runnable()
{
@Override
public void run()
{
try
{
IO.copy(p.getErrorStream(), System.err);
}
catch (IOException e)
{
LOG.warn("Unable to copy error stream", e);
}
}
});
// read any headers off the top of our input stream
// NOTE: Multiline header items not supported!
String line = null;
InputStream inFromCgi = p.getInputStream();
// br=new BufferedReader(new InputStreamReader(inFromCgi));
// while ((line=br.readLine())!=null)
while ((line = getTextLineFromStream(inFromCgi)).length() > 0)
{
if (!line.startsWith("HTTP"))
{
int k = line.indexOf(':');
if (k > 0)
{
String key = line.substring(0, k).trim();
String value = line.substring(k + 1).trim();
if ("Location".equals(key))
{
res.sendRedirect(res.encodeRedirectURL(value));
}
else if ("Status".equals(key))
{
String[] token = value.split(" ");
int status = Integer.parseInt(token[0]);
res.setStatus(status);
}
else
{
// add remaining header items to our response header
res.addHeader(key, value);
}
}
}
}
// copy cgi content to response stream...
os = res.getOutputStream();
IO.copy(inFromCgi, os);
p.waitFor();
if (!_ignoreExitState)
{
int exitValue = p.exitValue();
if (0 != exitValue)
{
LOG.warn("Non-zero exit status (" + exitValue + ") from CGI program: " + absolutePath);
if (!res.isCommitted())
res.sendError(500, "Failed to exec CGI");
}
}
}
catch (IOException e)
{
// browser has probably closed its input stream - we
// terminate and clean up...
LOG.debug("CGI: Client closed connection!", e);
}
catch (InterruptedException ex)
{
LOG.debug("CGI: interrupted!");
}
finally
{
IO.close(os);
p.destroy();
// LOG.debug("CGI: terminated!");
async.complete();
}
}
private static void writeProcessInput(final Process p, final String input)
{
new Thread(new Runnable()
{
@Override
public void run()
{
try
{
try (Writer outToCgi = new OutputStreamWriter(p.getOutputStream()))
{
outToCgi.write(input);
}
}
catch (IOException e)
{
LOG.debug("Unable to write out to CGI", e);
}
}
}).start();
}
private static void writeProcessInput(final Process p, final InputStream input, final int len)
{
if (len <= 0)
return;
new Thread(new Runnable()
{
@Override
public void run()
{
try
{
try (OutputStream outToCgi = p.getOutputStream())
{
IO.copy(input, outToCgi, len);
}
}
catch (IOException e)
{
LOG.debug("Unable to write out to CGI", e);
}
}
}).start();
}
/**
* Utility method to get a line of text from the input stream.
*
* @param is the input stream
* @return the line of text
* @throws IOException if reading from the input stream throws
*/
private static String getTextLineFromStream(InputStream is) throws IOException
{
StringBuilder buffer = new StringBuilder();
int b;
while ((b = is.read()) != -1 && b != '\n')
{
buffer.append((char)b);
}
return buffer.toString().trim();
}
/**
* private utility class that manages the Environment passed to exec.
*/
private static class EnvList
{
private Map<String, String> envMap;
EnvList()
{
envMap = new HashMap<>();
}
EnvList(EnvList l)
{
envMap = new HashMap<>(l.envMap);
}
/**
* Set a name/value pair, null values will be treated as an empty String
*
* @param name the name
* @param value the value
*/
public void set(String name, String value)
{
envMap.put(name, name + "=" + StringUtil.nonNull(value));
}
/**
* Get representation suitable for passing to exec.
*
* @return the env map as an array
*/
public String[] getEnvArray()
{
return envMap.values().toArray(new String[envMap.size()]);
}
public String getExportString()
{
StringBuilder sb = new StringBuilder();
for (String variable : getEnvArray())
{
sb.append("export \"");
sb.append(variable);
sb.append("\"; ");
}
return sb.toString();
}
@Override
public String toString()
{
return envMap.toString();
}
}
}

@ -1,39 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package servlets;
import org.eclipse.jetty.server.Request;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* This is an extension to {@link DoSFilter} that uses Jetty APIs to
* abruptly close the connection when the request times out.
*/
public class CloseableDoSFilter extends DoSFilter
{
@Override
protected void onRequestTimeout(HttpServletRequest request, HttpServletResponse response, Thread handlingThread)
{
Request baseRequest = Request.getBaseRequest(request);
baseRequest.getHttpChannel().getEndPoint().close();
}
}

@ -1,147 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package servlets;
import org.eclipse.jetty.util.URIUtil;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* <p>This servlet may be used to concatenate multiple resources into
* a single response.</p>
* <p>It is intended to be used to load multiple
* javascript or css files, but may be used for any content of the
* same mime type that can be meaningfully concatenated.</p>
* <p>The servlet uses {@link RequestDispatcher#include(javax.servlet.ServletRequest, javax.servlet.ServletResponse)}
* to combine the requested content, so dynamically generated content
* may be combined (Eg engine.js for DWR).</p>
* <p>The servlet uses parameter names of the query string as resource names
* relative to the context root. So these script tags:</p>
* <pre>
* &lt;script type="text/javascript" src="../js/behaviour.js"&gt;&lt;/script&gt;
* &lt;script type="text/javascript" src="../js/ajax.js"&gt;&lt;/script&gt;
* &lt;script type="text/javascript" src="../chat/chat.js"&gt;&lt;/script&gt;
* </pre>
* <p>can be replaced with the single tag (with the {@code ConcatServlet}
* mapped to {@code /concat}):</p>
* <pre>
* &lt;script type="text/javascript" src="../concat?/js/behaviour.js&amp;/js/ajax.js&amp;/chat/chat.js"&gt;&lt;/script&gt;
* </pre>
* <p>The {@link ServletContext#getMimeType(String)} method is used to determine the
* mime type of each resource. If the types of all resources do not match, then a 415
* UNSUPPORTED_MEDIA_TYPE error is returned.</p>
* <p>If the init parameter {@code development} is set to {@code true} then the servlet
* will run in development mode and the content will be concatenated on every request.</p>
* <p>Otherwise the init time of the servlet is used as the lastModifiedTime of the combined content
* and If-Modified-Since requests are handled with 304 NOT Modified responses if
* appropriate. This means that when not in development mode, the servlet must be
* restarted before changed content will be served.</p>
*/
public class ConcatServlet extends HttpServlet
{
private boolean _development;
private long _lastModified;
@Override
public void init() throws ServletException
{
_lastModified = System.currentTimeMillis();
_development = Boolean.parseBoolean(getInitParameter("development"));
}
/*
* @return The start time of the servlet unless in development mode, in which case -1 is returned.
*/
@Override
protected long getLastModified(HttpServletRequest req)
{
return _development ? -1 : _lastModified;
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
String query = request.getQueryString();
if (query == null)
{
response.sendError(HttpServletResponse.SC_NO_CONTENT);
return;
}
List<RequestDispatcher> dispatchers = new ArrayList<>();
String[] parts = query.split("\\&");
String type = null;
for (String part : parts)
{
String path = URIUtil.canonicalPath(URIUtil.decodePath(part));
if (path == null)
{
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
// Verify that the path is not protected.
if (startsWith(path, "/WEB-INF/") || startsWith(path, "/META-INF/"))
{
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
String t = getServletContext().getMimeType(path);
if (t != null)
{
if (type == null)
{
type = t;
}
else if (!type.equals(t))
{
response.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE);
return;
}
}
RequestDispatcher dispatcher = getServletContext().getRequestDispatcher(path);
if (dispatcher != null)
dispatchers.add(dispatcher);
}
if (type != null)
response.setContentType(type);
for (RequestDispatcher dispatcher : dispatchers)
{
dispatcher.include(request, response);
}
}
private boolean startsWith(String path, String prefix)
{
// Case insensitive match.
return prefix.regionMatches(true, 0, path, 0, prefix.length());
}
}

@ -1,505 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package servlets;
import org.eclipse.jetty.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;
import java.util.regex.Pattern;
/**
* Implementation of the
* <a href="http://www.w3.org/TR/cors/">cross-origin resource sharing</a>.
* <p>
* A typical example is to use this filter to allow cross-domain
* <a href="http://cometd.org">cometd</a> communication using the standard
* long polling transport instead of the JSONP transport (that is less
* efficient and less reactive to failures).
* <p>
* This filter allows the following configuration parameters:
* <dl>
* <dt>allowedOrigins</dt>
* <dd>a comma separated list of origins that are
* allowed to access the resources. Default value is <b>*</b>, meaning all
* origins. Note that using wild cards can result in security problems
* for requests identifying hosts that do not exist.
* <p>
* If an allowed origin contains one or more * characters (for example
* http://*.domain.com), then "*" characters are converted to ".*", "."
* characters are escaped to "\." and the resulting allowed origin
* interpreted as a regular expression.
* <p>
* Allowed origins can therefore be more complex expressions such as
* https?://*.domain.[a-z]{3} that matches http or https, multiple subdomains
* and any 3 letter top-level domain (.com, .net, .org, etc.).</dd>
*
* <dt>allowedTimingOrigins</dt>
* <dd>a comma separated list of origins that are
* allowed to time the resource. Default value is the empty string, meaning
* no origins.
* <p>
* The check whether the timing header is set, will be performed only if
* the user gets general access to the resource using the <b>allowedOrigins</b>.
*
* <dt>allowedMethods</dt>
* <dd>a comma separated list of HTTP methods that
* are allowed to be used when accessing the resources. Default value is
* <b>GET,POST,HEAD</b></dd>
*
*
* <dt>allowedHeaders</dt>
* <dd>a comma separated list of HTTP headers that
* are allowed to be specified when accessing the resources. Default value
* is <b>X-Requested-With,Content-Type,Accept,Origin</b>. If the value is a single "*",
* this means that any headers will be accepted.</dd>
*
* <dt>preflightMaxAge</dt>
* <dd>the number of seconds that preflight requests
* can be cached by the client. Default value is <b>1800</b> seconds, or 30
* minutes</dd>
*
* <dt>allowCredentials</dt>
* <dd>a boolean indicating if the resource allows
* requests with credentials. Default value is <b>true</b></dd>
*
* <dt>exposedHeaders</dt>
* <dd>a comma separated list of HTTP headers that
* are allowed to be exposed on the client. Default value is the
* <b>empty list</b></dd>
*
* <dt>chainPreflight</dt>
* <dd>if true preflight requests are chained to their
* target resource for normal handling (as an OPTION request). Otherwise the
* filter will response to the preflight. Default is <b>true</b>.</dd>
*
* </dl>
* A typical configuration could be:
* <pre>
* &lt;web-app ...&gt;
* ...
* &lt;filter&gt;
* &lt;filter-name&gt;cross-origin&lt;/filter-name&gt;
* &lt;filter-class&gt;org.eclipse.jetty.servlets.CrossOriginFilter&lt;/filter-class&gt;
* &lt;/filter&gt;
* &lt;filter-mapping&gt;
* &lt;filter-name&gt;cross-origin&lt;/filter-name&gt;
* &lt;url-pattern&gt;/cometd/*&lt;/url-pattern&gt;
* &lt;/filter-mapping&gt;
* ...
* &lt;/web-app&gt;
* </pre>
*/
public class CrossOriginFilter implements Filter
{
private static final Logger LOG = LoggerFactory.getLogger(CrossOriginFilter.class);
// Request headers
private static final String ORIGIN_HEADER = "Origin";
public static final String ACCESS_CONTROL_REQUEST_METHOD_HEADER = "Access-Control-Request-Method";
public static final String ACCESS_CONTROL_REQUEST_HEADERS_HEADER = "Access-Control-Request-Headers";
// Response headers
public static final String ACCESS_CONTROL_ALLOW_ORIGIN_HEADER = "Access-Control-Allow-Origin";
public static final String ACCESS_CONTROL_ALLOW_METHODS_HEADER = "Access-Control-Allow-Methods";
public static final String ACCESS_CONTROL_ALLOW_HEADERS_HEADER = "Access-Control-Allow-Headers";
public static final String ACCESS_CONTROL_MAX_AGE_HEADER = "Access-Control-Max-Age";
public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER = "Access-Control-Allow-Credentials";
public static final String ACCESS_CONTROL_EXPOSE_HEADERS_HEADER = "Access-Control-Expose-Headers";
public static final String TIMING_ALLOW_ORIGIN_HEADER = "Timing-Allow-Origin";
// Implementation constants
public static final String ALLOWED_ORIGINS_PARAM = "allowedOrigins";
public static final String ALLOWED_TIMING_ORIGINS_PARAM = "allowedTimingOrigins";
public static final String ALLOWED_METHODS_PARAM = "allowedMethods";
public static final String ALLOWED_HEADERS_PARAM = "allowedHeaders";
public static final String PREFLIGHT_MAX_AGE_PARAM = "preflightMaxAge";
public static final String ALLOW_CREDENTIALS_PARAM = "allowCredentials";
public static final String EXPOSED_HEADERS_PARAM = "exposedHeaders";
public static final String OLD_CHAIN_PREFLIGHT_PARAM = "forwardPreflight";
public static final String CHAIN_PREFLIGHT_PARAM = "chainPreflight";
private static final String ANY_ORIGIN = "*";
private static final String DEFAULT_ALLOWED_ORIGINS = "*";
private static final String DEFAULT_ALLOWED_TIMING_ORIGINS = "";
private static final List<String> SIMPLE_HTTP_METHODS = Arrays.asList("GET", "POST", "HEAD");
private static final List<String> DEFAULT_ALLOWED_METHODS = Arrays.asList("GET", "POST", "HEAD");
private static final List<String> DEFAULT_ALLOWED_HEADERS = Arrays.asList("X-Requested-With", "Content-Type", "Accept", "Origin");
private boolean anyOriginAllowed;
private boolean anyTimingOriginAllowed;
private boolean anyHeadersAllowed;
private Set<String> allowedOrigins = new HashSet<String>();
private List<Pattern> allowedOriginPatterns = new ArrayList<Pattern>();
private Set<String> allowedTimingOrigins = new HashSet<String>();
private List<Pattern> allowedTimingOriginPatterns = new ArrayList<Pattern>();
private List<String> allowedMethods = new ArrayList<String>();
private List<String> allowedHeaders = new ArrayList<String>();
private List<String> exposedHeaders = new ArrayList<String>();
private int preflightMaxAge;
private boolean allowCredentials;
private boolean chainPreflight;
@Override
public void init(FilterConfig config) throws ServletException
{
String allowedOriginsConfig = config.getInitParameter(ALLOWED_ORIGINS_PARAM);
String allowedTimingOriginsConfig = config.getInitParameter(ALLOWED_TIMING_ORIGINS_PARAM);
anyOriginAllowed = generateAllowedOrigins(allowedOrigins, allowedOriginPatterns, allowedOriginsConfig, DEFAULT_ALLOWED_ORIGINS);
anyTimingOriginAllowed = generateAllowedOrigins(allowedTimingOrigins, allowedTimingOriginPatterns, allowedTimingOriginsConfig, DEFAULT_ALLOWED_TIMING_ORIGINS);
String allowedMethodsConfig = config.getInitParameter(ALLOWED_METHODS_PARAM);
if (allowedMethodsConfig == null)
allowedMethods.addAll(DEFAULT_ALLOWED_METHODS);
else
allowedMethods.addAll(Arrays.asList(StringUtil.csvSplit(allowedMethodsConfig)));
String allowedHeadersConfig = config.getInitParameter(ALLOWED_HEADERS_PARAM);
if (allowedHeadersConfig == null)
allowedHeaders.addAll(DEFAULT_ALLOWED_HEADERS);
else if ("*".equals(allowedHeadersConfig))
anyHeadersAllowed = true;
else
allowedHeaders.addAll(Arrays.asList(StringUtil.csvSplit(allowedHeadersConfig)));
String preflightMaxAgeConfig = config.getInitParameter(PREFLIGHT_MAX_AGE_PARAM);
if (preflightMaxAgeConfig == null)
preflightMaxAgeConfig = "1800"; // Default is 30 minutes
try
{
preflightMaxAge = Integer.parseInt(preflightMaxAgeConfig);
}
catch (NumberFormatException x)
{
LOG.info("Cross-origin filter, could not parse '{}' parameter as integer: {}", PREFLIGHT_MAX_AGE_PARAM, preflightMaxAgeConfig);
}
String allowedCredentialsConfig = config.getInitParameter(ALLOW_CREDENTIALS_PARAM);
if (allowedCredentialsConfig == null)
allowedCredentialsConfig = "true";
allowCredentials = Boolean.parseBoolean(allowedCredentialsConfig);
String exposedHeadersConfig = config.getInitParameter(EXPOSED_HEADERS_PARAM);
if (exposedHeadersConfig == null)
exposedHeadersConfig = "";
exposedHeaders.addAll(Arrays.asList(StringUtil.csvSplit(exposedHeadersConfig)));
String chainPreflightConfig = config.getInitParameter(OLD_CHAIN_PREFLIGHT_PARAM);
if (chainPreflightConfig != null)
LOG.warn("DEPRECATED CONFIGURATION: Use " + CHAIN_PREFLIGHT_PARAM + " instead of " + OLD_CHAIN_PREFLIGHT_PARAM);
else
chainPreflightConfig = config.getInitParameter(CHAIN_PREFLIGHT_PARAM);
if (chainPreflightConfig == null)
chainPreflightConfig = "true";
chainPreflight = Boolean.parseBoolean(chainPreflightConfig);
if (LOG.isDebugEnabled())
{
LOG.debug("Cross-origin filter configuration: " +
ALLOWED_ORIGINS_PARAM + " = " + allowedOriginsConfig + ", " +
ALLOWED_TIMING_ORIGINS_PARAM + " = " + allowedTimingOriginsConfig + ", " +
ALLOWED_METHODS_PARAM + " = " + allowedMethodsConfig + ", " +
ALLOWED_HEADERS_PARAM + " = " + allowedHeadersConfig + ", " +
PREFLIGHT_MAX_AGE_PARAM + " = " + preflightMaxAgeConfig + ", " +
ALLOW_CREDENTIALS_PARAM + " = " + allowedCredentialsConfig + "," +
EXPOSED_HEADERS_PARAM + " = " + exposedHeadersConfig + "," +
CHAIN_PREFLIGHT_PARAM + " = " + chainPreflightConfig
);
}
}
private boolean generateAllowedOrigins(Set<String> allowedOriginStore, List<Pattern> allowedOriginPatternStore, String allowedOriginsConfig, String defaultOrigin)
{
if (allowedOriginsConfig == null)
allowedOriginsConfig = defaultOrigin;
String[] allowedOrigins = StringUtil.csvSplit(allowedOriginsConfig);
for (String allowedOrigin : allowedOrigins)
{
if (allowedOrigin.length() > 0)
{
if (ANY_ORIGIN.equals(allowedOrigin))
{
allowedOriginStore.clear();
allowedOriginPatternStore.clear();
return true;
}
else if (allowedOrigin.contains("*"))
{
allowedOriginPatternStore.add(Pattern.compile(parseAllowedWildcardOriginToRegex(allowedOrigin)));
}
else
{
allowedOriginStore.add(allowedOrigin);
}
}
}
return false;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
{
handle((HttpServletRequest)request, (HttpServletResponse)response, chain);
}
private void handle(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException
{
String origin = request.getHeader(ORIGIN_HEADER);
// Is it a cross origin request ?
if (origin != null && isEnabled(request))
{
if (anyOriginAllowed || originMatches(allowedOrigins, allowedOriginPatterns, origin))
{
if (isSimpleRequest(request))
{
LOG.debug("Cross-origin request to {} is a simple cross-origin request", request.getRequestURI());
handleSimpleResponse(request, response, origin);
}
else if (isPreflightRequest(request))
{
LOG.debug("Cross-origin request to {} is a preflight cross-origin request", request.getRequestURI());
handlePreflightResponse(request, response, origin);
if (chainPreflight)
LOG.debug("Preflight cross-origin request to {} forwarded to application", request.getRequestURI());
else
return;
}
else
{
LOG.debug("Cross-origin request to {} is a non-simple cross-origin request", request.getRequestURI());
handleSimpleResponse(request, response, origin);
}
if (anyTimingOriginAllowed || originMatches(allowedTimingOrigins, allowedTimingOriginPatterns, origin))
{
response.setHeader(TIMING_ALLOW_ORIGIN_HEADER, origin);
}
else
{
LOG.debug("Cross-origin request to " + request.getRequestURI() + " with origin " + origin + " does not match allowed timing origins " + allowedTimingOrigins);
}
}
else
{
LOG.debug("Cross-origin request to " + request.getRequestURI() + " with origin " + origin + " does not match allowed origins " + allowedOrigins);
}
}
chain.doFilter(request, response);
}
protected boolean isEnabled(HttpServletRequest request)
{
// WebSocket clients such as Chrome 5 implement a version of the WebSocket
// protocol that does not accept extra response headers on the upgrade response
for (Enumeration<String> connections = request.getHeaders("Connection"); connections.hasMoreElements(); )
{
String connection = (String)connections.nextElement();
if ("Upgrade".equalsIgnoreCase(connection))
{
for (Enumeration<String> upgrades = request.getHeaders("Upgrade"); upgrades.hasMoreElements(); )
{
String upgrade = (String)upgrades.nextElement();
if ("WebSocket".equalsIgnoreCase(upgrade))
return false;
}
}
}
return true;
}
private boolean originMatches(Set<String> allowedOrigins, List<Pattern> allowedOriginPatterns, String originList)
{
if (originList.trim().length() == 0)
return false;
String[] origins = originList.split(" ");
for (String origin : origins)
{
if (origin.trim().length() == 0)
continue;
if (allowedOrigins.contains(origin))
return true;
for (Pattern allowedOrigin : allowedOriginPatterns)
{
if (allowedOrigin.matcher(origin).matches())
return true;
}
}
return false;
}
private String parseAllowedWildcardOriginToRegex(String allowedOrigin)
{
String regex = StringUtil.replace(allowedOrigin, ".", "\\.");
return StringUtil.replace(regex, "*", ".*"); // we want to be greedy here to match multiple subdomains, thus we use .*
}
private boolean isSimpleRequest(HttpServletRequest request)
{
String method = request.getMethod();
if (SIMPLE_HTTP_METHODS.contains(method))
{
// TODO: implement better detection of simple headers
// The specification says that for a request to be simple, custom request headers must be simple.
// Here for simplicity I just check if there is a Access-Control-Request-Method header,
// which is required for preflight requests
return request.getHeader(ACCESS_CONTROL_REQUEST_METHOD_HEADER) == null;
}
return false;
}
private boolean isPreflightRequest(HttpServletRequest request)
{
String method = request.getMethod();
if (!"OPTIONS".equalsIgnoreCase(method))
return false;
if (request.getHeader(ACCESS_CONTROL_REQUEST_METHOD_HEADER) == null)
return false;
return true;
}
private void handleSimpleResponse(HttpServletRequest request, HttpServletResponse response, String origin)
{
response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, origin);
//W3C CORS spec http://www.w3.org/TR/cors/#resource-implementation
response.addHeader("Vary", ORIGIN_HEADER);
if (allowCredentials)
response.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER, "true");
if (!exposedHeaders.isEmpty())
response.setHeader(ACCESS_CONTROL_EXPOSE_HEADERS_HEADER, commify(exposedHeaders));
}
private void handlePreflightResponse(HttpServletRequest request, HttpServletResponse response, String origin)
{
boolean methodAllowed = isMethodAllowed(request);
if (!methodAllowed)
return;
List<String> headersRequested = getAccessControlRequestHeaders(request);
boolean headersAllowed = areHeadersAllowed(headersRequested);
if (!headersAllowed)
return;
response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, origin);
//W3C CORS spec http://www.w3.org/TR/cors/#resource-implementation
if (!anyOriginAllowed)
response.addHeader("Vary", ORIGIN_HEADER);
if (allowCredentials)
response.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER, "true");
if (preflightMaxAge > 0)
response.setHeader(ACCESS_CONTROL_MAX_AGE_HEADER, String.valueOf(preflightMaxAge));
response.setHeader(ACCESS_CONTROL_ALLOW_METHODS_HEADER, commify(allowedMethods));
if (anyHeadersAllowed)
response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS_HEADER, commify(headersRequested));
else
response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS_HEADER, commify(allowedHeaders));
}
private boolean isMethodAllowed(HttpServletRequest request)
{
String accessControlRequestMethod = request.getHeader(ACCESS_CONTROL_REQUEST_METHOD_HEADER);
LOG.debug("{} is {}", ACCESS_CONTROL_REQUEST_METHOD_HEADER, accessControlRequestMethod);
boolean result = false;
if (accessControlRequestMethod != null)
result = allowedMethods.contains(accessControlRequestMethod);
LOG.debug("Method {} is" + (result ? "" : " not") + " among allowed methods {}", accessControlRequestMethod, allowedMethods);
return result;
}
private List<String> getAccessControlRequestHeaders(HttpServletRequest request)
{
String accessControlRequestHeaders = request.getHeader(ACCESS_CONTROL_REQUEST_HEADERS_HEADER);
LOG.debug("{} is {}", ACCESS_CONTROL_REQUEST_HEADERS_HEADER, accessControlRequestHeaders);
if (accessControlRequestHeaders == null)
return Collections.emptyList();
List<String> requestedHeaders = new ArrayList<String>();
String[] headers = StringUtil.csvSplit(accessControlRequestHeaders);
for (String header : headers)
{
String h = header.trim();
if (h.length() > 0)
requestedHeaders.add(h);
}
return requestedHeaders;
}
private boolean areHeadersAllowed(List<String> requestedHeaders)
{
if (anyHeadersAllowed)
{
LOG.debug("Any header is allowed");
return true;
}
boolean result = true;
for (String requestedHeader : requestedHeaders)
{
boolean headerAllowed = false;
for (String allowedHeader : allowedHeaders)
{
if (requestedHeader.equalsIgnoreCase(allowedHeader.trim()))
{
headerAllowed = true;
break;
}
}
if (!headerAllowed)
{
result = false;
break;
}
}
LOG.debug("Headers [{}] are" + (result ? "" : " not") + " among allowed headers {}", requestedHeaders, allowedHeaders);
return result;
}
private String commify(List<String> strings)
{
StringBuilder builder = new StringBuilder();
for (int i = 0; i < strings.size(); ++i)
{
if (i > 0)
builder.append(",");
String string = strings.get(i);
builder.append(string);
}
return builder.toString();
}
@Override
public void destroy()
{
anyOriginAllowed = false;
anyTimingOriginAllowed = false;
allowedOrigins.clear();
allowedOriginPatterns.clear();
allowedTimingOrigins.clear();
allowedTimingOriginPatterns.clear();
allowedMethods.clear();
allowedHeaders.clear();
preflightMaxAge = 0;
allowCredentials = false;
}
}

@ -1,313 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package servlets;
import org.eclipse.jetty.server.HttpOutput;
import org.eclipse.jetty.util.ProcessorUtils;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel.MapMode;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* A servlet that uses the Servlet 3.1 asynchronous IO API to server
* static content at a limited data rate.
* <p>
* Two implementations are supported: <ul>
* <li>The <code>StandardDataStream</code> impl uses only standard
* APIs, but produces more garbage due to the byte[] nature of the API.
* <li>the <code>JettyDataStream</code> impl uses a Jetty API to write a ByteBuffer
* and thus allow the efficient use of file mapped buffers without any
* temporary buffer copies (I did tell the JSR that this was a good idea to
* have in the standard!).
* </ul>
* <p>
* The data rate is controlled by setting init parameters:
* <dl>
* <dt>buffersize</dt><dd>The amount of data in bytes written per write</dd>
* <dt>pause</dt><dd>The period in ms to wait after a write before attempting another</dd>
* <dt>pool</dt><dd>The size of the thread pool used to service the writes (defaults to available processors)</dd>
* </dl>
* Thus if buffersize = 1024 and pause = 100, the data rate will be limited to 10KB per second.
*/
public class DataRateLimitedServlet extends HttpServlet
{
private static final long serialVersionUID = -4771757707068097025L;
private int buffersize = 8192;
private long pauseNS = TimeUnit.MILLISECONDS.toNanos(100);
ScheduledThreadPoolExecutor scheduler;
private final ConcurrentHashMap<String, ByteBuffer> cache = new ConcurrentHashMap<>();
@Override
public void init() throws ServletException
{
// read the init params
String tmp = getInitParameter("buffersize");
if (tmp != null)
buffersize = Integer.parseInt(tmp);
tmp = getInitParameter("pause");
if (tmp != null)
pauseNS = TimeUnit.MILLISECONDS.toNanos(Integer.parseInt(tmp));
tmp = getInitParameter("pool");
int pool = tmp == null ? ProcessorUtils.availableProcessors() : Integer.parseInt(tmp);
// Create and start a shared scheduler.
scheduler = new ScheduledThreadPoolExecutor(pool);
}
@Override
public void destroy()
{
scheduler.shutdown();
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
// Get the path of the static resource to serve.
String info = request.getPathInfo();
// We don't handle directories
if (info.endsWith("/"))
{
response.sendError(503, "directories not supported");
return;
}
// Set the mime type of the response
String contentType = getServletContext().getMimeType(info);
response.setContentType(contentType == null ? "application/x-data" : contentType);
// Look for a matching file path
String path = request.getPathTranslated();
// If we have a file path and this is a jetty response, we can use the JettyStream impl
ServletOutputStream out = response.getOutputStream();
if (path != null && out instanceof HttpOutput)
{
// If the file exists
File file = new File(path);
if (file.exists() && file.canRead())
{
// Set the content length
response.setContentLengthLong(file.length());
// Look for a file mapped buffer in the cache
ByteBuffer mapped = cache.get(path);
// Handle cache miss
if (mapped == null)
{
// TODO implement LRU cache flush
try (RandomAccessFile raf = new RandomAccessFile(file, "r"))
{
ByteBuffer buf = raf.getChannel().map(MapMode.READ_ONLY, 0, raf.length());
mapped = cache.putIfAbsent(path, buf);
if (mapped == null)
mapped = buf;
}
}
// start async request handling
AsyncContext async = request.startAsync();
// Set a JettyStream as the write listener to write the content asynchronously.
out.setWriteListener(new JettyDataStream(mapped, async, out));
return;
}
}
// Jetty API was not used, so lets try the standards approach
// Can we find the content as an input stream
InputStream content = getServletContext().getResourceAsStream(info);
if (content == null)
{
response.sendError(404);
return;
}
// Set a StandardStream as he write listener to write the content asynchronously
out.setWriteListener(new StandardDataStream(content, request.startAsync(), out));
}
/**
* A standard API Stream writer
*/
private final class StandardDataStream implements WriteListener, Runnable
{
private final InputStream content;
private final AsyncContext async;
private final ServletOutputStream out;
private StandardDataStream(InputStream content, AsyncContext async, ServletOutputStream out)
{
this.content = content;
this.async = async;
this.out = out;
}
@Override
public void onWritePossible() throws IOException
{
// If we are able to write
if (out.isReady())
{
// Allocated a copy buffer for each write, so as to not hold while paused
// TODO put these buffers into a pool
byte[] buffer = new byte[buffersize];
// read some content into the copy buffer
int len = content.read(buffer);
// If we are at EOF
if (len < 0)
{
// complete the async lifecycle
async.complete();
return;
}
// write out the copy buffer. This will be an asynchronous write
// and will always return immediately without blocking. If a subsequent
// call to out.isReady() returns false, then this onWritePossible method
// will be called back when a write is possible.
out.write(buffer, 0, len);
// Schedule a timer callback to pause writing. Because isReady() is not called,
// a onWritePossible callback is no scheduled.
scheduler.schedule(this, pauseNS, TimeUnit.NANOSECONDS);
}
}
@Override
public void run()
{
try
{
// When the pause timer wakes up, call onWritePossible. Either isReady() will return
// true and another chunk of content will be written, or it will return false and the
// onWritePossible() callback will be scheduled when a write is next possible.
onWritePossible();
}
catch (Exception e)
{
onError(e);
}
}
@Override
public void onError(Throwable t)
{
getServletContext().log("Async Error", t);
async.complete();
}
}
/**
* A Jetty API DataStream
*/
private final class JettyDataStream implements WriteListener, Runnable
{
private final ByteBuffer content;
private final int limit;
private final AsyncContext async;
private final HttpOutput out;
private JettyDataStream(ByteBuffer content, AsyncContext async, ServletOutputStream out)
{
// Make a readonly copy of the passed buffer. This uses the same underlying content
// without a copy, but gives this instance its own position and limit.
this.content = content.asReadOnlyBuffer();
// remember the ultimate limit.
this.limit = this.content.limit();
this.async = async;
this.out = (HttpOutput)out;
}
@Override
public void onWritePossible() throws IOException
{
// If we are able to write
if (out.isReady())
{
// Position our buffers limit to allow only buffersize bytes to be written
int l = content.position() + buffersize;
// respect the ultimate limit
if (l > limit)
l = limit;
content.limit(l);
// if all content has been written
if (!content.hasRemaining())
{
// complete the async lifecycle
async.complete();
return;
}
// write our limited buffer. This will be an asynchronous write
// and will always return immediately without blocking. If a subsequent
// call to out.isReady() returns false, then this onWritePossible method
// will be called back when a write is possible.
out.write(content);
// Schedule a timer callback to pause writing. Because isReady() is not called,
// a onWritePossible callback is not scheduled.
scheduler.schedule(this, pauseNS, TimeUnit.NANOSECONDS);
}
}
@Override
public void run()
{
try
{
// When the pause timer wakes up, call onWritePossible. Either isReady() will return
// true and another chunk of content will be written, or it will return false and the
// onWritePossible() callback will be scheduled when a write is next possible.
onWritePossible();
}
catch (Exception e)
{
onError(e);
}
}
@Override
public void onError(Throwable t)
{
getServletContext().log("Async Error", t);
async.complete();
}
}
}

File diff suppressed because it is too large Load Diff

@ -1,108 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package servlets;
import java.io.IOException;
/**
* <p>{@link EventSource} is the passive half of an event source connection, as defined by the
* <a href="http://www.w3.org/TR/eventsource/">EventSource Specification</a>.</p>
* <p>{@link EventSource.Emitter} is the active half of the connection and allows to operate on the connection.</p>
* <p>{@link EventSource} allows applications to be notified of events happening on the connection;
* two events are being notified: the opening of the event source connection, where method
* {@link EventSource#onOpen(Emitter)} is invoked, and the closing of the event source connection,
* where method {@link EventSource#onClose()} is invoked.</p>
*
* @see EventSourceServlet
*/
public interface EventSource
{
/**
* <p>Callback method invoked when an event source connection is opened.</p>
*
* @param emitter the {@link Emitter} instance that allows to operate on the connection
* @throws IOException if the implementation of the method throws such exception
*/
public void onOpen(Emitter emitter) throws IOException;
/**
* <p>Callback method invoked when an event source connection is closed.</p>
*/
public void onClose();
/**
* <p>{@link Emitter} is the active half of an event source connection, and allows applications
* to operate on the connection by sending events, data or comments, or by closing the connection.</p>
* <p>An {@link Emitter} instance will be created for each new event source connection.</p>
* <p>{@link Emitter} instances are fully thread safe and can be used from multiple threads.</p>
*/
public interface Emitter
{
/**
* <p>Sends a named event with data to the client.</p>
* <p>When invoked as: <code>event("foo", "bar")</code>, the client will receive the lines:</p>
* <pre>
* event: foo
* data: bar
* </pre>
*
* @param name the event name
* @param data the data to be sent
* @throws IOException if an I/O failure occurred
* @see #data(String)
*/
public void event(String name, String data) throws IOException;
/**
* <p>Sends a default event with data to the client.</p>
* <p>When invoked as: <code>data("baz")</code>, the client will receive the line:</p>
* <pre>
* data: baz
* </pre>
* <p>When invoked as: <code>data("foo\r\nbar\rbaz\nbax")</code>, the client will receive the lines:</p>
* <pre>
* data: foo
* data: bar
* data: baz
* data: bax
* </pre>
*
* @param data the data to be sent
* @throws IOException if an I/O failure occurred
*/
public void data(String data) throws IOException;
/**
* <p>Sends a comment to the client.</p>
* <p>When invoked as: <code>comment("foo")</code>, the client will receive the line:</p>
* <pre>
* : foo
* </pre>
*
* @param comment the comment to send
* @throws IOException if an I/O failure occurred
*/
public void comment(String comment) throws IOException;
/**
* <p>Closes this event source connection.</p>
*/
public void close();
}
}

@ -1,236 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package servlets;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* <p>A servlet that implements the <a href="http://www.w3.org/TR/eventsource/">event source protocol</a>,
* also known as "server sent events".</p>
* <p>This servlet must be subclassed to implement abstract method {@link #newEventSource(HttpServletRequest)}
* to return an instance of {@link EventSource} that allows application to listen for event source events
* and to emit event source events.</p>
* <p>This servlet supports the following configuration parameters:</p>
* <ul>
* <li><code>heartBeatPeriod</code>, that specifies the heartbeat period, in seconds, used to check
* whether the connection has been closed by the client; defaults to 10 seconds.</li>
* </ul>
*
* <p>NOTE: there is currently no support for <code>last-event-id</code>.</p>
*/
public abstract class EventSourceServlet extends HttpServlet
{
private static final byte[] CRLF = new byte[]{'\r', '\n'};
private static final byte[] EVENT_FIELD = "event: ".getBytes(StandardCharsets.UTF_8);
private static final byte[] DATA_FIELD = "data: ".getBytes(StandardCharsets.UTF_8);
private static final byte[] COMMENT_FIELD = ": ".getBytes(StandardCharsets.UTF_8);
private ScheduledExecutorService scheduler;
private int heartBeatPeriod = 10;
@Override
public void init() throws ServletException
{
String heartBeatPeriodParam = getServletConfig().getInitParameter("heartBeatPeriod");
if (heartBeatPeriodParam != null)
heartBeatPeriod = Integer.parseInt(heartBeatPeriodParam);
scheduler = Executors.newSingleThreadScheduledExecutor();
}
@Override
public void destroy()
{
if (scheduler != null)
scheduler.shutdown();
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
@SuppressWarnings("unchecked") Enumeration<String> acceptValues = request.getHeaders("Accept");
while (acceptValues.hasMoreElements())
{
String accept = acceptValues.nextElement();
if (accept.equals("text/event-stream"))
{
EventSource eventSource = newEventSource(request);
if (eventSource == null)
{
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
}
else
{
respond(request, response);
AsyncContext async = request.startAsync();
// Infinite timeout because the continuation is never resumed,
// but only completed on close
async.setTimeout(0);
EventSourceEmitter emitter = new EventSourceEmitter(eventSource, async);
emitter.scheduleHeartBeat();
open(eventSource, emitter);
}
return;
}
}
super.doGet(request, response);
}
protected abstract EventSource newEventSource(HttpServletRequest request);
protected void respond(HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.setStatus(HttpServletResponse.SC_OK);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType("text/event-stream");
// By adding this header, and not closing the connection,
// we disable HTTP chunking, and we can use write()+flush()
// to send data in the text/event-stream protocol
response.addHeader("Connection", "close");
response.flushBuffer();
}
protected void open(EventSource eventSource, EventSource.Emitter emitter) throws IOException
{
eventSource.onOpen(emitter);
}
protected class EventSourceEmitter implements EventSource.Emitter, Runnable
{
private final EventSource eventSource;
private final AsyncContext async;
private final ServletOutputStream output;
private Future<?> heartBeat;
private boolean closed;
public EventSourceEmitter(EventSource eventSource, AsyncContext async) throws IOException
{
this.eventSource = eventSource;
this.async = async;
this.output = async.getResponse().getOutputStream();
}
@Override
public void event(String name, String data) throws IOException
{
synchronized (this)
{
output.write(EVENT_FIELD);
output.write(name.getBytes(StandardCharsets.UTF_8));
output.write(CRLF);
data(data);
}
}
@Override
public void data(String data) throws IOException
{
synchronized (this)
{
BufferedReader reader = new BufferedReader(new StringReader(data));
String line;
while ((line = reader.readLine()) != null)
{
output.write(DATA_FIELD);
output.write(line.getBytes(StandardCharsets.UTF_8));
output.write(CRLF);
}
output.write(CRLF);
flush();
}
}
@Override
public void comment(String comment) throws IOException
{
synchronized (this)
{
output.write(COMMENT_FIELD);
output.write(comment.getBytes(StandardCharsets.UTF_8));
output.write(CRLF);
output.write(CRLF);
flush();
}
}
@Override
public void run()
{
// If the other peer closes the connection, the first
// flush() should generate a TCP reset that is detected
// on the second flush()
try
{
synchronized (this)
{
output.write('\r');
flush();
output.write('\n');
flush();
}
// We could write, reschedule heartbeat
scheduleHeartBeat();
}
catch (IOException x)
{
// The other peer closed the connection
close();
eventSource.onClose();
}
}
protected void flush() throws IOException
{
async.getResponse().flushBuffer();
}
@Override
public void close()
{
synchronized (this)
{
closed = true;
heartBeat.cancel(false);
}
async.complete();
}
private void scheduleHeartBeat()
{
synchronized (this)
{
if (!closed)
heartBeat = scheduler.schedule(this, heartBeatPeriod, TimeUnit.SECONDS);
}
}
}
}

@ -1,194 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package servlets;
import org.eclipse.jetty.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Header Filter
* <p>
* This filter sets or adds a header to the response.
* <p>
* The {@code headerConfig} init param is a CSV of actions to perform on headers, with the following syntax: <br>
* [action] [header name]: [header value] <br>
* [action] can be one of <code>set</code>, <code>add</code>, <code>setDate</code>, or <code>addDate</code> <br>
* The date actions will add the header value in milliseconds to the current system time before setting a date header.
* <p>
* Below is an example value for <code>headerConfig</code>:<br>
*
* <pre>
* set X-Frame-Options: DENY,
* "add Cache-Control: no-cache, no-store, must-revalidate",
* setDate Expires: 31540000000,
* addDate Date: 0
* </pre>
*
* @see IncludeExcludeBasedFilter
*/
public class HeaderFilter extends IncludeExcludeBasedFilter
{
private List<ConfiguredHeader> _configuredHeaders = new ArrayList<>();
private static final Logger LOG = LoggerFactory.getLogger(HeaderFilter.class);
@Override
public void init(FilterConfig filterConfig) throws ServletException
{
super.init(filterConfig);
String headerConfig = filterConfig.getInitParameter("headerConfig");
if (headerConfig != null)
{
String[] configs = StringUtil.csvSplit(headerConfig);
for (String config : configs)
{
_configuredHeaders.add(parseHeaderConfiguration(config));
}
}
if (LOG.isDebugEnabled())
LOG.debug(this.toString());
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
{
HttpServletRequest httpRequest = (HttpServletRequest)request;
HttpServletResponse httpResponse = (HttpServletResponse)response;
if (super.shouldFilter(httpRequest, httpResponse))
{
for (ConfiguredHeader header : _configuredHeaders)
{
if (header.isDate())
{
long headerValue = System.currentTimeMillis() + header.getMsOffset();
if (header.isAdd())
{
httpResponse.addDateHeader(header.getName(), headerValue);
}
else
{
httpResponse.setDateHeader(header.getName(), headerValue);
}
}
else // constant header value
{
if (header.isAdd())
{
httpResponse.addHeader(header.getName(), header.getValue());
}
else
{
httpResponse.setHeader(header.getName(), header.getValue());
}
}
}
}
chain.doFilter(request, response);
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append(super.toString()).append("\n");
sb.append("configured headers:\n");
for (ConfiguredHeader c : _configuredHeaders)
{
sb.append(c).append("\n");
}
return sb.toString();
}
private ConfiguredHeader parseHeaderConfiguration(String config)
{
String[] configTokens = config.trim().split(" ", 2);
String method = configTokens[0].trim();
String header = configTokens[1];
String[] headerTokens = header.trim().split(":", 2);
String headerName = headerTokens[0].trim();
String headerValue = headerTokens[1].trim();
ConfiguredHeader configuredHeader = new ConfiguredHeader(headerName, headerValue, method.startsWith("add"), method.endsWith("Date"));
return configuredHeader;
}
private static class ConfiguredHeader
{
private String _name;
private String _value;
private long _msOffset;
private boolean _add;
private boolean _date;
public ConfiguredHeader(String name, String value, boolean add, boolean date)
{
_name = name;
_value = value;
_add = add;
_date = date;
if (_date)
{
_msOffset = Long.parseLong(_value);
}
}
public String getName()
{
return _name;
}
public String getValue()
{
return _value;
}
public boolean isAdd()
{
return _add;
}
public boolean isDate()
{
return _date;
}
public long getMsOffset()
{
return _msOffset;
}
@Override
public String toString()
{
return (_add ? "add" : "set") + (_date ? "Date" : "") + " " + _name + ": " + _value;
}
}
}

@ -1,185 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package servlets;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.http.pathmap.PathSpecSet;
import org.eclipse.jetty.util.IncludeExclude;
import org.eclipse.jetty.util.IncludeExcludeSet;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.Filter;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Include Exclude Based Filter
* <p>
* This is an abstract filter which helps with filtering based on include/exclude of paths, mime types, and/or http methods.
* <p>
* Use the {@link #shouldFilter(HttpServletRequest, HttpServletResponse)} method to determine if a request/response should be filtered. If mime types are used,
* it should be called after {@link javax.servlet.FilterChain#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} since the mime type may not
* be written until then.
*
* Supported init params:
* <ul>
* <li><code>includedPaths</code> - CSV of path specs to include</li>
* <li><code>excludedPaths</code> - CSV of path specs to exclude</li>
* <li><code>includedMimeTypes</code> - CSV of mime types to include</li>
* <li><code>excludedMimeTypes</code> - CSV of mime types to exclude</li>
* <li><code>includedHttpMethods</code> - CSV of http methods to include</li>
* <li><code>excludedHttpMethods</code> - CSV of http methods to exclude</li>
* </ul>
* <p>
* Path spec rules:
* <ul>
* <li>If the spec starts with <code>'^'</code> the spec is assumed to be a regex based path spec and will match with normal Java regex rules.</li>
* <li>If the spec starts with <code>'/'</code> the spec is assumed to be a Servlet url-pattern rules path spec for either an exact match or prefix based
* match.</li>
* <li>If the spec starts with <code>'*.'</code> the spec is assumed to be a Servlet url-pattern rules path spec for a suffix based match.</li>
* <li>All other syntaxes are unsupported.</li>
* </ul>
* <p>
* CSVs are parsed with {@link StringUtil#csvSplit(String)}
*
* @see PathSpecSet
* @see IncludeExcludeSet
*/
public abstract class IncludeExcludeBasedFilter implements Filter
{
private final IncludeExclude<String> _mimeTypes = new IncludeExclude<>();
private final IncludeExclude<String> _httpMethods = new IncludeExclude<>();
private final IncludeExclude<String> _paths = new IncludeExclude<>(PathSpecSet.class);
private static final Logger LOG = LoggerFactory.getLogger(IncludeExcludeBasedFilter.class);
@Override
public void init(FilterConfig filterConfig) throws ServletException
{
final String includedPaths = filterConfig.getInitParameter("includedPaths");
final String excludedPaths = filterConfig.getInitParameter("excludedPaths");
final String includedMimeTypes = filterConfig.getInitParameter("includedMimeTypes");
final String excludedMimeTypes = filterConfig.getInitParameter("excludedMimeTypes");
final String includedHttpMethods = filterConfig.getInitParameter("includedHttpMethods");
final String excludedHttpMethods = filterConfig.getInitParameter("excludedHttpMethods");
if (includedPaths != null)
{
_paths.include(StringUtil.csvSplit(includedPaths));
}
if (excludedPaths != null)
{
_paths.exclude(StringUtil.csvSplit(excludedPaths));
}
if (includedMimeTypes != null)
{
_mimeTypes.include(StringUtil.csvSplit(includedMimeTypes));
}
if (excludedMimeTypes != null)
{
_mimeTypes.exclude(StringUtil.csvSplit(excludedMimeTypes));
}
if (includedHttpMethods != null)
{
_httpMethods.include(StringUtil.csvSplit(includedHttpMethods));
}
if (excludedHttpMethods != null)
{
_httpMethods.exclude(StringUtil.csvSplit(excludedHttpMethods));
}
}
protected String guessMimeType(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
{
String contentType = httpResponse.getContentType();
LOG.debug("Content Type is: {}", contentType);
String mimeType = "";
if (contentType != null)
{
mimeType = MimeTypes.getContentTypeWithoutCharset(contentType);
LOG.debug("Mime Type is: {}", mimeType);
}
else
{
String requestUrl = httpRequest.getPathInfo();
mimeType = MimeTypes.getDefaultMimeByExtension(requestUrl);
if (mimeType == null)
{
mimeType = "";
}
LOG.debug("Guessed mime type is {}", mimeType);
}
return mimeType;
}
protected boolean shouldFilter(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
{
String httpMethod = httpRequest.getMethod();
LOG.debug("HTTP method is: {}", httpMethod);
if (!_httpMethods.test(httpMethod))
{
LOG.debug("should not apply filter because HTTP method does not match");
return false;
}
String mimeType = guessMimeType(httpRequest, httpResponse);
if (!_mimeTypes.test(mimeType))
{
LOG.debug("should not apply filter because mime type does not match");
return false;
}
ServletContext context = httpRequest.getServletContext();
String path = context == null ? httpRequest.getRequestURI() : URIUtil.addPaths(httpRequest.getServletPath(), httpRequest.getPathInfo());
LOG.debug("Path is: {}", path);
if (!_paths.test(path))
{
LOG.debug("should not apply filter because path does not match");
return false;
}
return true;
}
@Override
public void destroy()
{
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("filter configuration:\n");
sb.append("paths:\n").append(_paths).append("\n");
sb.append("mime types:\n").append(_mimeTypes).append("\n");
sb.append("http methods:\n").append(_httpMethods);
return sb.toString();
}
}

@ -1,306 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package servlets;
import org.eclipse.jetty.http.*;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.annotation.ManagedOperation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.PushBuilder;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
/**
* <p>A filter that builds a cache of secondary resources associated
* to primary resources.</p>
* <p>A typical request for a primary resource such as {@code index.html}
* is immediately followed by a number of requests for secondary resources.
* Secondary resource requests will have a {@code Referer} HTTP header
* that points to {@code index.html}, which is used to associate the secondary
* resource to the primary resource.</p>
* <p>Only secondary resources that are requested within a (small) time period
* from the request of the primary resource are associated with the primary
* resource.</p>
* <p>This allows to build a cache of secondary resources associated with
* primary resources. When a request for a primary resource arrives, associated
* secondary resources are pushed to the client, unless the request carries
* {@code If-xxx} header that hint that the client has the resources in its
* cache.</p>
* <p>If the init param useQueryInKey is set, then the query string is used as
* as part of the key to identify a resource</p>
*/
@ManagedObject("Push cache based on the HTTP 'Referer' header")
public class PushCacheFilter implements Filter
{
private static final Logger LOG = LoggerFactory.getLogger(PushCacheFilter.class);
private final Set<Integer> _ports = new HashSet<>();
private final Set<String> _hosts = new HashSet<>();
private final ConcurrentMap<String, PrimaryResource> _cache = new ConcurrentHashMap<>();
private long _associatePeriod = 4000L;
private int _maxAssociations = 16;
private long _renew = System.nanoTime();
private boolean _useQueryInKey;
@Override
public void init(FilterConfig config) throws ServletException
{
String associatePeriod = config.getInitParameter("associatePeriod");
if (associatePeriod != null)
_associatePeriod = Long.parseLong(associatePeriod);
String maxAssociations = config.getInitParameter("maxAssociations");
if (maxAssociations != null)
_maxAssociations = Integer.parseInt(maxAssociations);
String hosts = config.getInitParameter("hosts");
if (hosts != null)
Collections.addAll(_hosts, StringUtil.csvSplit(hosts));
String ports = config.getInitParameter("ports");
if (ports != null)
for (String p : StringUtil.csvSplit(ports))
{
_ports.add(Integer.parseInt(p));
}
_useQueryInKey = Boolean.parseBoolean(config.getInitParameter("useQueryInKey"));
// Expose for JMX.
config.getServletContext().setAttribute(config.getFilterName(), this);
if (LOG.isDebugEnabled())
LOG.debug("period={} max={} hosts={} ports={}", _associatePeriod, _maxAssociations, _hosts, _ports);
}
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException
{
HttpServletRequest request = (HttpServletRequest)req;
PushBuilder pushBuilder = request.newPushBuilder();
if (HttpVersion.fromString(request.getProtocol()).getVersion() < 20 ||
!HttpMethod.GET.is(request.getMethod()) ||
pushBuilder == null)
{
chain.doFilter(req, resp);
return;
}
long now = System.nanoTime();
boolean conditional = false;
String referrer = null;
List<String> headerNames = Collections.list(request.getHeaderNames());
for (String headerName : headerNames)
{
if (HttpHeader.IF_MATCH.is(headerName) ||
HttpHeader.IF_MODIFIED_SINCE.is(headerName) ||
HttpHeader.IF_NONE_MATCH.is(headerName) ||
HttpHeader.IF_UNMODIFIED_SINCE.is(headerName))
{
conditional = true;
break;
}
else if (HttpHeader.REFERER.is(headerName))
{
referrer = request.getHeader(headerName);
}
}
if (LOG.isDebugEnabled())
LOG.debug("{} {} referrer={} conditional={}", request.getMethod(), request.getRequestURI(), referrer, conditional);
String path = request.getRequestURI();
String query = request.getQueryString();
if (_useQueryInKey && query != null)
path += "?" + query;
if (referrer != null)
{
HttpURI referrerURI = new HttpURI(referrer);
String host = referrerURI.getHost();
int port = referrerURI.getPort();
if (port <= 0)
{
String scheme = referrerURI.getScheme();
if (scheme != null)
port = HttpScheme.HTTPS.is(scheme) ? 443 : 80;
else
port = request.isSecure() ? 443 : 80;
}
boolean referredFromHere = !_hosts.isEmpty() ? _hosts.contains(host) : host.equals(request.getServerName());
referredFromHere &= !_ports.isEmpty() ? _ports.contains(port) : port == request.getServerPort();
if (referredFromHere)
{
if (HttpMethod.GET.is(request.getMethod()))
{
String referrerPath = _useQueryInKey ? referrerURI.getPathQuery() : referrerURI.getPath();
if (referrerPath == null)
referrerPath = "/";
if (referrerPath.startsWith(request.getContextPath() + "/"))
{
if (!referrerPath.equals(path))
{
PrimaryResource primaryResource = _cache.get(referrerPath);
if (primaryResource != null)
{
long primaryTimestamp = primaryResource._timestamp.get();
if (primaryTimestamp != 0)
{
if (now - primaryTimestamp < TimeUnit.MILLISECONDS.toNanos(_associatePeriod))
{
Set<String> associated = primaryResource._associated;
// Not strictly concurrent-safe, just best effort to limit associations.
if (associated.size() <= _maxAssociations)
{
if (associated.add(path))
{
if (LOG.isDebugEnabled())
LOG.debug("Associated {} to {}", path, referrerPath);
}
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("Not associated {} to {}, exceeded max associations of {}", path, referrerPath, _maxAssociations);
}
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("Not associated {} to {}, outside associate period of {}ms", path, referrerPath, _associatePeriod);
}
}
}
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("Not associated {} to {}, referring to self", path, referrerPath);
}
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("Not associated {} to {}, different context", path, referrerPath);
}
}
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("External referrer {}", referrer);
}
}
PrimaryResource primaryResource = _cache.get(path);
if (primaryResource == null)
{
PrimaryResource r = new PrimaryResource();
primaryResource = _cache.putIfAbsent(path, r);
primaryResource = primaryResource == null ? r : primaryResource;
primaryResource._timestamp.compareAndSet(0, now);
if (LOG.isDebugEnabled())
LOG.debug("Cached primary resource {}", path);
}
else
{
long last = primaryResource._timestamp.get();
if (last < _renew && primaryResource._timestamp.compareAndSet(last, now))
{
primaryResource._associated.clear();
if (LOG.isDebugEnabled())
LOG.debug("Clear associated resources for {}", path);
}
}
// Push associated resources.
if (!conditional && !primaryResource._associated.isEmpty())
{
// Breadth-first push of associated resources.
Queue<PrimaryResource> queue = new ArrayDeque<>();
queue.offer(primaryResource);
while (!queue.isEmpty())
{
PrimaryResource parent = queue.poll();
for (String childPath : parent._associated)
{
PrimaryResource child = _cache.get(childPath);
if (child != null)
queue.offer(child);
if (LOG.isDebugEnabled())
LOG.debug("Pushing {} for {}", childPath, path);
pushBuilder.path(childPath).push();
}
}
}
chain.doFilter(request, resp);
}
@Override
public void destroy()
{
clearPushCache();
}
@ManagedAttribute("The push cache contents")
public Map<String, String> getPushCache()
{
Map<String, String> result = new HashMap<>();
for (Map.Entry<String, PrimaryResource> entry : _cache.entrySet())
{
PrimaryResource resource = entry.getValue();
String value = String.format("size=%d: %s", resource._associated.size(), new TreeSet<>(resource._associated));
result.put(entry.getKey(), value);
}
return result;
}
@ManagedOperation(value = "Renews the push cache contents", impact = "ACTION")
public void renewPushCache()
{
_renew = System.nanoTime();
}
@ManagedOperation(value = "Clears the push cache contents", impact = "ACTION")
public void clearPushCache()
{
_cache.clear();
}
private static class PrimaryResource
{
private final Set<String> _associated = Collections.newSetFromMap(new ConcurrentHashMap<>());
private final AtomicLong _timestamp = new AtomicLong();
}
}

@ -1,194 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package servlets;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpURI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.http.PushBuilder;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
public class PushSessionCacheFilter implements Filter
{
private static final String RESPONSE_ATTR = "PushSessionCacheFilter.response";
private static final String TARGET_ATTR = "PushSessionCacheFilter.target";
private static final String TIMESTAMP_ATTR = "PushSessionCacheFilter.timestamp";
private static final Logger LOG = LoggerFactory.getLogger(PushSessionCacheFilter.class);
private final ConcurrentMap<String, Target> _cache = new ConcurrentHashMap<>();
private long _associateDelay = 5000L;
@Override
public void init(FilterConfig config) throws ServletException
{
if (config.getInitParameter("associateDelay") != null)
_associateDelay = Long.parseLong(config.getInitParameter("associateDelay"));
// Add a listener that is used to collect information
// about associated resource, etags and modified dates.
config.getServletContext().addListener(new ServletRequestListener()
{
// Collect information when request is destroyed.
@Override
public void requestDestroyed(ServletRequestEvent sre)
{
HttpServletRequest request = (HttpServletRequest)sre.getServletRequest();
Target target = (Target)request.getAttribute(TARGET_ATTR);
if (target == null)
return;
// Update conditional data.
HttpServletResponse response = (HttpServletResponse)request.getAttribute(RESPONSE_ATTR);
target._etag = response.getHeader(HttpHeader.ETAG.asString());
target._lastModified = response.getHeader(HttpHeader.LAST_MODIFIED.asString());
if (LOG.isDebugEnabled())
LOG.debug("Served {} for {}", response.getStatus(), request.getRequestURI());
// Does this request have a referer?
String referer = request.getHeader(HttpHeader.REFERER.asString());
if (referer != null)
{
// Is the referer from this contexts?
HttpURI refererUri = new HttpURI(referer);
if (request.getServerName().equals(refererUri.getHost()))
{
Target refererTarget = _cache.get(refererUri.getPath());
if (refererTarget != null)
{
HttpSession session = request.getSession();
@SuppressWarnings("unchecked") ConcurrentHashMap<String, Long> timestamps = (ConcurrentHashMap<String, Long>)session.getAttribute(TIMESTAMP_ATTR);
Long last = timestamps.get(refererTarget._path);
if (last != null && TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - last) < _associateDelay)
{
if (refererTarget._associated.putIfAbsent(target._path, target) == null)
{
if (LOG.isDebugEnabled())
LOG.debug("ASSOCIATE {}->{}", refererTarget._path, target._path);
}
}
}
}
}
}
@Override
public void requestInitialized(ServletRequestEvent sre)
{
}
});
}
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException
{
req.setAttribute(RESPONSE_ATTR, resp);
HttpServletRequest request = (HttpServletRequest)req;
String uri = request.getRequestURI();
if (LOG.isDebugEnabled())
LOG.debug("{} {}", request.getMethod(), uri);
HttpSession session = request.getSession(true);
// find the target for this resource
Target target = _cache.get(uri);
if (target == null)
{
Target t = new Target(uri);
target = _cache.putIfAbsent(uri, t);
target = target == null ? t : target;
}
request.setAttribute(TARGET_ATTR, target);
// Set the timestamp for this resource in this session
@SuppressWarnings("unchecked") ConcurrentHashMap<String, Long> timestamps = (ConcurrentHashMap<String, Long>)session.getAttribute(TIMESTAMP_ATTR);
if (timestamps == null)
{
timestamps = new ConcurrentHashMap<>();
session.setAttribute(TIMESTAMP_ATTR, timestamps);
}
timestamps.put(uri, System.nanoTime());
// Push any associated resources.
PushBuilder builder = request.newPushBuilder();
if (builder != null && !target._associated.isEmpty())
{
boolean conditional = request.getHeader(HttpHeader.IF_NONE_MATCH.asString()) != null ||
request.getHeader(HttpHeader.IF_MODIFIED_SINCE.asString()) != null;
// Breadth-first push of associated resources.
Queue<Target> queue = new ArrayDeque<>();
queue.offer(target);
while (!queue.isEmpty())
{
Target parent = queue.poll();
builder.addHeader("X-Pusher", PushSessionCacheFilter.class.toString());
for (Target child : parent._associated.values())
{
queue.offer(child);
String path = child._path;
if (LOG.isDebugEnabled())
LOG.debug("PUSH {} <- {}", path, uri);
builder.path(path)
.setHeader(HttpHeader.IF_NONE_MATCH.asString(), conditional ? child._etag : null)
.setHeader(HttpHeader.IF_MODIFIED_SINCE.asString(), conditional ? child._lastModified : null);
}
}
}
chain.doFilter(req, resp);
}
@Override
public void destroy()
{
_cache.clear();
}
private static class Target
{
private final String _path;
private final ConcurrentMap<String, Target> _associated = new ConcurrentHashMap<>();
private volatile String _etag;
private volatile String _lastModified;
private Target(String path)
{
_path = path;
}
@Override
public String toString()
{
return String.format("Target{p=%s,e=%s,m=%s,a=%d}", _path, _etag, _lastModified, _associated.size());
}
}
}

@ -1,356 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package servlets;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* PutFilter
*
* A Filter that handles PUT, DELETE and MOVE methods.
* Files are hidden during PUT operations, so that 404's result.
*
* The following init parameters pay be used:<ul>
* <li><b>baseURI</b> - The file URI of the document root for put content.
* <li><b>delAllowed</b> - boolean, if true DELETE and MOVE methods are supported.
* <li><b>putAtomic</b> - boolean, if true PUT files are written to a temp location and moved into place.
* </ul>
*/
public class PutFilter implements Filter
{
public static final String __PUT = "PUT";
public static final String __DELETE = "DELETE";
public static final String __MOVE = "MOVE";
public static final String __OPTIONS = "OPTIONS";
Set<String> _operations = new HashSet<String>();
private ConcurrentMap<String, String> _hidden = new ConcurrentHashMap<String, String>();
private ServletContext _context;
private String _baseURI;
private boolean _delAllowed;
private boolean _putAtomic;
private File _tmpdir;
@Override
public void init(FilterConfig config) throws ServletException
{
_context = config.getServletContext();
_tmpdir = (File)_context.getAttribute("javax.servlet.context.tempdir");
if (_context.getRealPath("/") == null)
throw new UnavailableException("Packed war");
String b = config.getInitParameter("baseURI");
if (b != null)
{
_baseURI = b;
}
else
{
File base = new File(_context.getRealPath("/"));
_baseURI = base.toURI().toString();
}
_delAllowed = getInitBoolean(config, "delAllowed");
_putAtomic = getInitBoolean(config, "putAtomic");
_operations.add(__OPTIONS);
_operations.add(__PUT);
if (_delAllowed)
{
_operations.add(__DELETE);
_operations.add(__MOVE);
}
}
private boolean getInitBoolean(FilterConfig config, String name)
{
String value = config.getInitParameter(name);
return value != null && value.length() > 0 && (value.startsWith("t") || value.startsWith("T") || value.startsWith("y") || value.startsWith("Y") || value.startsWith("1"));
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException
{
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
String servletPath = request.getServletPath();
String pathInfo = request.getPathInfo();
String pathInContext = URIUtil.addPaths(servletPath, pathInfo);
String resource = URIUtil.addPaths(_baseURI, pathInContext);
String method = request.getMethod();
boolean op = _operations.contains(method);
if (op)
{
File file = null;
try
{
if (method.equals(__OPTIONS))
handleOptions(chain, request, response);
else
{
file = new File(new URI(resource));
boolean exists = file.exists();
if (exists && !passConditionalHeaders(request, response, file))
return;
if (method.equals(__PUT))
handlePut(request, response, pathInContext, file);
else if (method.equals(__DELETE))
handleDelete(request, response, pathInContext, file);
else if (method.equals(__MOVE))
handleMove(request, response, pathInContext, file);
else
throw new IllegalStateException();
}
}
catch (Exception e)
{
_context.log(e.toString(), e);
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
else
{
if (isHidden(pathInContext))
response.sendError(HttpServletResponse.SC_NOT_FOUND);
else
chain.doFilter(request, response);
return;
}
}
private boolean isHidden(String pathInContext)
{
return _hidden.containsKey(pathInContext);
}
@Override
public void destroy()
{
}
public void handlePut(HttpServletRequest request, HttpServletResponse response, String pathInContext, File file) throws ServletException, IOException
{
boolean exists = file.exists();
if (pathInContext.endsWith("/"))
{
if (!exists)
{
if (!file.mkdirs())
response.sendError(HttpServletResponse.SC_FORBIDDEN);
else
{
response.setStatus(HttpServletResponse.SC_CREATED);
response.flushBuffer();
}
}
else
{
response.setStatus(HttpServletResponse.SC_OK);
response.flushBuffer();
}
}
else
{
boolean ok = false;
try
{
_hidden.put(pathInContext, pathInContext);
File parent = file.getParentFile();
parent.mkdirs();
int toRead = request.getContentLength();
InputStream in = request.getInputStream();
if (_putAtomic)
{
File tmp = File.createTempFile(file.getName(), null, _tmpdir);
try (OutputStream out = new FileOutputStream(tmp, false))
{
if (toRead >= 0)
IO.copy(in, out, toRead);
else
IO.copy(in, out);
}
if (!tmp.renameTo(file))
throw new IOException("rename from " + tmp + " to " + file + " failed");
}
else
{
try (OutputStream out = new FileOutputStream(file, false))
{
if (toRead >= 0)
IO.copy(in, out, toRead);
else
IO.copy(in, out);
}
}
response.setStatus(exists ? HttpServletResponse.SC_OK : HttpServletResponse.SC_CREATED);
response.flushBuffer();
ok = true;
}
catch (Exception ex)
{
_context.log(ex.toString(), ex);
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
finally
{
if (!ok)
{
try
{
if (file.exists())
file.delete();
}
catch (Exception e)
{
_context.log(e.toString(), e);
}
}
_hidden.remove(pathInContext);
}
}
}
public void handleDelete(HttpServletRequest request, HttpServletResponse response, String pathInContext, File file) throws ServletException, IOException
{
try
{
// delete the file
if (file.delete())
{
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
response.flushBuffer();
}
else
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
catch (SecurityException sex)
{
_context.log(sex.toString(), sex);
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
}
public void handleMove(HttpServletRequest request, HttpServletResponse response, String pathInContext, File file)
throws ServletException, IOException, URISyntaxException
{
String newPath = URIUtil.canonicalEncodedPath(request.getHeader("new-uri"));
if (newPath == null)
{
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
String contextPath = request.getContextPath();
if (contextPath != null && !newPath.startsWith(contextPath))
{
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
return;
}
String newInfo = newPath;
if (contextPath != null)
newInfo = newInfo.substring(contextPath.length());
String newResource = URIUtil.addEncodedPaths(_baseURI, newInfo);
File newFile = new File(new URI(newResource));
file.renameTo(newFile);
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
response.flushBuffer();
}
public void handleOptions(FilterChain chain, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
chain.doFilter(request, new HttpServletResponseWrapper(response)
{
@Override
public void setHeader(String name, String value)
{
if ("Allow".equalsIgnoreCase(name))
{
Set<String> options = new HashSet<String>();
options.addAll(Arrays.asList(StringUtil.csvSplit(value)));
options.addAll(_operations);
value = null;
for (String o : options)
{
value = value == null ? o : (value + ", " + o);
}
}
super.setHeader(name, value);
}
});
}
/*
* Check modification date headers.
*/
protected boolean passConditionalHeaders(HttpServletRequest request, HttpServletResponse response, File file) throws IOException
{
long date = 0;
if ((date = request.getDateHeader("if-unmodified-since")) > 0)
{
if (file.lastModified() / 1000 > date / 1000)
{
response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
return false;
}
}
if ((date = request.getDateHeader("if-modified-since")) > 0)
{
if (file.lastModified() / 1000 <= date / 1000)
{
response.reset();
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
response.flushBuffer();
return false;
}
}
return true;
}
}

@ -1,380 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package servlets;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
* Quality of Service Filter.
* <p>
* This filter limits the number of active requests to the number set by the "maxRequests" init parameter (default 10).
* If more requests are received, they are suspended and placed on priority queues. Priorities are determined by
* the {@link #getPriority(ServletRequest)} method and are a value between 0 and the value given by the "maxPriority"
* init parameter (default 10), with higher values having higher priority.
* <p>
* This filter is ideal to prevent wasting threads waiting for slow/limited
* resources such as a JDBC connection pool. It avoids the situation where all of a
* containers thread pool may be consumed blocking on such a slow resource.
* By limiting the number of active threads, a smaller thread pool may be used as
* the threads are not wasted waiting. Thus more memory may be available for use by
* the active threads.
* <p>
* Furthermore, this filter uses a priority when resuming waiting requests. So that if
* a container is under load, and there are many requests waiting for resources,
* the {@link #getPriority(ServletRequest)} method is used, so that more important
* requests are serviced first. For example, this filter could be deployed with a
* maxRequest limit slightly smaller than the containers thread pool and a high priority
* allocated to admin users. Thus regardless of load, admin users would always be
* able to access the web application.
* <p>
* The maxRequest limit is policed by a {@link Semaphore} and the filter will wait a short while attempting to acquire
* the semaphore. This wait is controlled by the "waitMs" init parameter and allows the expense of a suspend to be
* avoided if the semaphore is shortly available. If the semaphore cannot be obtained, the request will be suspended
* for the default suspend period of the container or the valued set as the "suspendMs" init parameter.
* <p>
* If the "managedAttr" init parameter is set to true, then this servlet is set as a {@link ServletContext} attribute with the
* filter name as the attribute name. This allows context external mechanism (eg JMX via {@link ContextHandler#MANAGED_ATTRIBUTES}) to
* manage the configuration of the filter.
*/
@ManagedObject("Quality of Service Filter")
public class QoSFilter implements Filter
{
private static final Logger LOG = LoggerFactory.getLogger(QoSFilter.class);
static final int __DEFAULT_MAX_PRIORITY = 10;
static final int __DEFAULT_PASSES = 10;
static final int __DEFAULT_WAIT_MS = 50;
static final long __DEFAULT_TIMEOUT_MS = -1;
static final String MANAGED_ATTR_INIT_PARAM = "managedAttr";
static final String MAX_REQUESTS_INIT_PARAM = "maxRequests";
static final String MAX_PRIORITY_INIT_PARAM = "maxPriority";
static final String MAX_WAIT_INIT_PARAM = "waitMs";
static final String SUSPEND_INIT_PARAM = "suspendMs";
private final String _suspended = "QoSFilter@" + Integer.toHexString(hashCode()) + ".SUSPENDED";
private final String _resumed = "QoSFilter@" + Integer.toHexString(hashCode()) + ".RESUMED";
private long _waitMs;
private long _suspendMs;
private int _maxRequests;
private Semaphore _passes;
private Queue<AsyncContext>[] _queues;
private AsyncListener[] _listeners;
@Override
public void init(FilterConfig filterConfig)
{
int maxPriority = __DEFAULT_MAX_PRIORITY;
if (filterConfig.getInitParameter(MAX_PRIORITY_INIT_PARAM) != null)
maxPriority = Integer.parseInt(filterConfig.getInitParameter(MAX_PRIORITY_INIT_PARAM));
_queues = new Queue[maxPriority + 1];
_listeners = new AsyncListener[_queues.length];
for (int p = 0; p < _queues.length; ++p)
{
_queues[p] = new ConcurrentLinkedQueue<>();
_listeners[p] = new QoSAsyncListener(p);
}
int maxRequests = __DEFAULT_PASSES;
if (filterConfig.getInitParameter(MAX_REQUESTS_INIT_PARAM) != null)
maxRequests = Integer.parseInt(filterConfig.getInitParameter(MAX_REQUESTS_INIT_PARAM));
_passes = new Semaphore(maxRequests, true);
_maxRequests = maxRequests;
long wait = __DEFAULT_WAIT_MS;
if (filterConfig.getInitParameter(MAX_WAIT_INIT_PARAM) != null)
wait = Integer.parseInt(filterConfig.getInitParameter(MAX_WAIT_INIT_PARAM));
_waitMs = wait;
long suspend = __DEFAULT_TIMEOUT_MS;
if (filterConfig.getInitParameter(SUSPEND_INIT_PARAM) != null)
suspend = Integer.parseInt(filterConfig.getInitParameter(SUSPEND_INIT_PARAM));
_suspendMs = suspend;
ServletContext context = filterConfig.getServletContext();
if (context != null && Boolean.parseBoolean(filterConfig.getInitParameter(MANAGED_ATTR_INIT_PARAM)))
context.setAttribute(filterConfig.getFilterName(), this);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
{
boolean accepted = false;
try
{
Boolean suspended = (Boolean)request.getAttribute(_suspended);
if (suspended == null)
{
accepted = _passes.tryAcquire(getWaitMs(), TimeUnit.MILLISECONDS);
if (accepted)
{
request.setAttribute(_suspended, Boolean.FALSE);
if (LOG.isDebugEnabled())
LOG.debug("Accepted {}", request);
}
else
{
request.setAttribute(_suspended, Boolean.TRUE);
int priority = getPriority(request);
AsyncContext asyncContext = request.startAsync();
long suspendMs = getSuspendMs();
if (suspendMs > 0)
asyncContext.setTimeout(suspendMs);
asyncContext.addListener(_listeners[priority]);
_queues[priority].add(asyncContext);
if (LOG.isDebugEnabled())
LOG.debug("Suspended {}", request);
return;
}
}
else
{
if (suspended)
{
request.setAttribute(_suspended, Boolean.FALSE);
Boolean resumed = (Boolean)request.getAttribute(_resumed);
if (Boolean.TRUE.equals(resumed))
{
_passes.acquire();
accepted = true;
if (LOG.isDebugEnabled())
LOG.debug("Resumed {}", request);
}
else
{
// Timeout! try 1 more time.
accepted = _passes.tryAcquire(getWaitMs(), TimeUnit.MILLISECONDS);
if (LOG.isDebugEnabled())
LOG.debug("Timeout {}", request);
}
}
else
{
// Pass through resume of previously accepted request.
_passes.acquire();
accepted = true;
if (LOG.isDebugEnabled())
LOG.debug("Passthrough {}", request);
}
}
if (accepted)
{
chain.doFilter(request, response);
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("Rejected {}", request);
((HttpServletResponse)response).sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
}
}
catch (InterruptedException e)
{
((HttpServletResponse)response).sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
}
finally
{
if (accepted)
{
_passes.release();
for (int p = _queues.length - 1; p >= 0; --p)
{
AsyncContext asyncContext = _queues[p].poll();
if (asyncContext != null)
{
ServletRequest candidate = asyncContext.getRequest();
Boolean suspended = (Boolean)candidate.getAttribute(_suspended);
if (Boolean.TRUE.equals(suspended))
{
try
{
candidate.setAttribute(_resumed, Boolean.TRUE);
asyncContext.dispatch();
break;
}
catch (IllegalStateException x)
{
LOG.warn("Unable to resume suspended dispatch", x);
continue;
}
}
}
}
}
}
}
/**
* Computes the request priority.
* <p>
* The default implementation assigns the following priorities:
* <ul>
* <li> 2 - for an authenticated request
* <li> 1 - for a request with valid / non new session
* <li> 0 - for all other requests.
* </ul>
* This method may be overridden to provide application specific priorities.
*
* @param request the incoming request
* @return the computed request priority
*/
protected int getPriority(ServletRequest request)
{
HttpServletRequest baseRequest = (HttpServletRequest)request;
if (baseRequest.getUserPrincipal() != null)
{
return 2;
}
else
{
HttpSession session = baseRequest.getSession(false);
if (session != null && !session.isNew())
return 1;
else
return 0;
}
}
@Override
public void destroy()
{
}
/**
* Get the (short) amount of time (in milliseconds) that the filter would wait
* for the semaphore to become available before suspending a request.
*
* @return wait time (in milliseconds)
*/
@ManagedAttribute("(short) amount of time filter will wait before suspending request (in ms)")
public long getWaitMs()
{
return _waitMs;
}
/**
* Set the (short) amount of time (in milliseconds) that the filter would wait
* for the semaphore to become available before suspending a request.
*
* @param value wait time (in milliseconds)
*/
public void setWaitMs(long value)
{
_waitMs = value;
}
/**
* Get the amount of time (in milliseconds) that the filter would suspend
* a request for while waiting for the semaphore to become available.
*
* @return suspend time (in milliseconds)
*/
@ManagedAttribute("amount of time filter will suspend a request for while waiting for the semaphore to become available (in ms)")
public long getSuspendMs()
{
return _suspendMs;
}
/**
* Set the amount of time (in milliseconds) that the filter would suspend
* a request for while waiting for the semaphore to become available.
*
* @param value suspend time (in milliseconds)
*/
public void setSuspendMs(long value)
{
_suspendMs = value;
}
/**
* Get the maximum number of requests allowed to be processed
* at the same time.
*
* @return maximum number of requests
*/
@ManagedAttribute("maximum number of requests to allow processing of at the same time")
public int getMaxRequests()
{
return _maxRequests;
}
/**
* Set the maximum number of requests allowed to be processed
* at the same time.
*
* @param value the number of requests
*/
public void setMaxRequests(int value)
{
_passes = new Semaphore((value - getMaxRequests() + _passes.availablePermits()), true);
_maxRequests = value;
}
private class QoSAsyncListener implements AsyncListener
{
private final int priority;
public QoSAsyncListener(int priority)
{
this.priority = priority;
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException
{
}
@Override
public void onComplete(AsyncEvent event) throws IOException
{
}
@Override
public void onTimeout(AsyncEvent event) throws IOException
{
// Remove before it's redispatched, so it won't be
// redispatched again at the end of the filtering.
AsyncContext asyncContext = event.getAsyncContext();
_queues[priority].remove(asyncContext);
((HttpServletResponse)event.getSuppliedResponse()).sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
asyncContext.complete();
}
@Override
public void onError(AsyncEvent event) throws IOException
{
}
}
}

@ -1,69 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package servlets;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* Welcome Filter
* This filter can be used to server an index file for a directory
* when no index file actually exists (thus the web.xml mechanism does
* not work).
*
* This filter will dispatch requests to a directory (URLs ending with /)
* to the welcome URL determined by the "welcome" init parameter. So if
* the filter "welcome" init parameter is set to "index.do" then a request
* to "/some/directory/" will be dispatched to "/some/directory/index.do" and
* will be handled by any servlets mapped to that URL.
*
* Requests to "/some/directory" will be redirected to "/some/directory/".
*/
public class WelcomeFilter implements Filter
{
private String welcome;
@Override
public void init(FilterConfig filterConfig)
{
welcome = filterConfig.getInitParameter("welcome");
if (welcome == null)
welcome = "index.html";
}
@Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain)
throws IOException, ServletException
{
String path = ((HttpServletRequest)request).getServletPath();
if (welcome != null && path.endsWith("/"))
request.getRequestDispatcher(path + welcome).forward(request, response);
else
chain.doFilter(request, response);
}
@Override
public void destroy()
{
}
}

@ -1,23 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
/**
* Jetty Servlets : Generally Useful Servlets, Handlers and Filters
*/
package servlets;

@ -2,6 +2,21 @@
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" > "http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app> <web-app xmlns="http://java.sun.com/xml/ns/j2ee"
<display-name>Archetype Created Web Application</display-name> xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">
<display-name>misp-proxy</display-name>
<servlet>
<servlet-name>test-proxy</servlet-name>
<servlet-class>com.olexyn.test.proxy.ProxyServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>test-proxy</servlet-name>
<url-pattern>/core</url-pattern>
</servlet-mapping>
</web-app> </web-app>

@ -1,5 +1,20 @@
<html> <html>
<body> <head>
<h2>Hello World!</h2> <title>test-proxy</title>
</head>
<body bgcolor=white>
<table border="0">
<tr>
<td align=center>
<img src="images/io42630.png">
</td>
</tr>
<tr>
<td>
<h1>test-proxy</h1>
</td>
</tr>
</table>
</body> </body>
</html> </html>

Loading…
Cancel
Save