diff --git a/copy-restart.sh b/copy-restart.sh index ef3f38d..618fa08 100755 --- a/copy-restart.sh +++ b/copy-restart.sh @@ -1,7 +1,8 @@ #!/bin/bash - tomcat_webapps="${HOME}/app/tomcat/webapps" +jetty_webapps="${HOME}/app/jetty9.4/webapps" +webapps=$jetty_webapps cwd=$(pwd) /home/user/app/tomcat/bin/shutdown.sh @@ -10,14 +11,15 @@ echo "================" echo "END TOMCAT STOP " echo "================" -cp -v "${cwd}/misp-mirror/target/misp-mirror-0.1.war" "${tomcat_webapps}" -cp -v "${cwd}/misp-rev/target/misp-rev-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" "${webapps}" +cp -v "${cwd}/test-proxy/target/test-proxy-0.1.war" "${webapps}" echo "================" echo "END COPY" echo "================" -/home/user/app/tomcat/bin/startup.sh +# /home/user/app/tomcat/bin/startup.sh echo "================" echo "END TOMCAT START " diff --git a/misp-mirror/pom.xml b/misp-mirror/pom.xml index fcc6d6d..4f9c0ad 100644 --- a/misp-mirror/pom.xml +++ b/misp-mirror/pom.xml @@ -15,8 +15,8 @@ UTF-8 - 1.7 - 1.7 + 1.11 + 1.11 @@ -53,6 +53,7 @@ + ${project.artifactId} diff --git a/misp-rev/src/main/webapp/WEB-INF/web.xml b/misp-rev/src/main/webapp/WEB-INF/web.xml index 717dad2..a99e628 100644 --- a/misp-rev/src/main/webapp/WEB-INF/web.xml +++ b/misp-rev/src/main/webapp/WEB-INF/web.xml @@ -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" version="2.4"> - misp-fwd + misp-rev misp-rev diff --git a/mispmock/README.md b/mispmock/README.md new file mode 100644 index 0000000..742e46c --- /dev/null +++ b/mispmock/README.md @@ -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. + +
+ +##### Threads in Mock + ![](threads-in-mock.png) \ No newline at end of file diff --git a/mispmock/src/com/olexyn/misp/mock/AdapterMock.java b/mispmock/src/com/olexyn/misp/mock/AdapterMock.java new file mode 100644 index 0000000..f8cfe9b --- /dev/null +++ b/mispmock/src/com/olexyn/misp/mock/AdapterMock.java @@ -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(); + } + } + +} \ No newline at end of file diff --git a/mispmock/src/com/olexyn/misp/mock/AdapterRunnable.java b/mispmock/src/com/olexyn/misp/mock/AdapterRunnable.java new file mode 100644 index 0000000..7cf08fa --- /dev/null +++ b/mispmock/src/com/olexyn/misp/mock/AdapterRunnable.java @@ -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); + } +} diff --git a/mispmock/src/com/olexyn/misp/mock/BridgeMock.java b/mispmock/src/com/olexyn/misp/mock/BridgeMock.java new file mode 100644 index 0000000..4948a5c --- /dev/null +++ b/mispmock/src/com/olexyn/misp/mock/BridgeMock.java @@ -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)
+ * - wait for availableRides to have an entry
+ * - move move Ride to deliveredRides
+ * - wait for Ride to have Data
+ * - 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(); + } + } +} diff --git a/mispmock/src/com/olexyn/misp/mock/BridgeRunnable.java b/mispmock/src/com/olexyn/misp/mock/BridgeRunnable.java new file mode 100644 index 0000000..8874863 --- /dev/null +++ b/mispmock/src/com/olexyn/misp/mock/BridgeRunnable.java @@ -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); + } + +} diff --git a/mispmock/src/com/olexyn/misp/mock/Main.java b/mispmock/src/com/olexyn/misp/mock/Main.java new file mode 100644 index 0000000..5274457 --- /dev/null +++ b/mispmock/src/com/olexyn/misp/mock/Main.java @@ -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(); + } +} + + + diff --git a/mispmock/src/com/olexyn/misp/mock/MockSet.java b/mispmock/src/com/olexyn/misp/mock/MockSet.java new file mode 100644 index 0000000..533a1ec --- /dev/null +++ b/mispmock/src/com/olexyn/misp/mock/MockSet.java @@ -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; +} \ No newline at end of file diff --git a/mispmock/src/com/olexyn/misp/mock/actor/ActorRunnable.java b/mispmock/src/com/olexyn/misp/mock/actor/ActorRunnable.java new file mode 100644 index 0000000..115db66 --- /dev/null +++ b/mispmock/src/com/olexyn/misp/mock/actor/ActorRunnable.java @@ -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 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; + +} diff --git a/mispmock/src/com/olexyn/misp/mock/actor/AppMock.java b/mispmock/src/com/olexyn/misp/mock/actor/AppMock.java new file mode 100644 index 0000000..b3463ea --- /dev/null +++ b/mispmock/src/com/olexyn/misp/mock/actor/AppMock.java @@ -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) { + + } +} diff --git a/mispmock/src/com/olexyn/misp/mock/actor/UserMock.java b/mispmock/src/com/olexyn/misp/mock/actor/UserMock.java new file mode 100644 index 0000000..cb384c1 --- /dev/null +++ b/mispmock/src/com/olexyn/misp/mock/actor/UserMock.java @@ -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(); + } + } + +} diff --git a/mispmock/src/com/olexyn/misp/mock/exchange/ExchangeMock.java b/mispmock/src/com/olexyn/misp/mock/exchange/ExchangeMock.java new file mode 100644 index 0000000..1e0c0e3 --- /dev/null +++ b/mispmock/src/com/olexyn/misp/mock/exchange/ExchangeMock.java @@ -0,0 +1,50 @@ +package com.olexyn.misp.mock.exchange; + + +import java.util.ArrayList; +import java.util.List; + +/** + * How "exchange" mocks an exchange:
+ *
+ * An "exchange" corresponds to request & reply pair.
+ * - i.e. a GET & OK pair.
+ * Steps:
+ * 1. new Exchange()
+ * 2. set properties of .request
+ * 3. "send" request by using exchange.notify(); recipient.doGet();
+ * 4. recipient does someting with request
+ * - - recipient sets proerties of .response
+ * - - recipient sends reply using exchange.notify();
+ * 5. "receive" response by using exchange.wait(); + */ +public class ExchangeMock { + + public static List 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); + } + + + + + + + + + +} + + + + + diff --git a/mispmock/src/com/olexyn/misp/mock/exchange/RequestMock.java b/mispmock/src/com/olexyn/misp/mock/exchange/RequestMock.java new file mode 100644 index 0000000..0665303 --- /dev/null +++ b/mispmock/src/com/olexyn/misp/mock/exchange/RequestMock.java @@ -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 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; + } +} \ No newline at end of file diff --git a/mispmock/src/com/olexyn/misp/mock/exchange/ResponseMock.java b/mispmock/src/com/olexyn/misp/mock/exchange/ResponseMock.java new file mode 100644 index 0000000..fb7ff89 --- /dev/null +++ b/mispmock/src/com/olexyn/misp/mock/exchange/ResponseMock.java @@ -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; + } +} diff --git a/mispmock/threads-in-mock.png b/mispmock/threads-in-mock.png new file mode 100644 index 0000000..af9ac6e Binary files /dev/null and b/mispmock/threads-in-mock.png differ diff --git a/mispmock/threads-in-mock.uxf b/mispmock/threads-in-mock.uxf new file mode 100644 index 0000000..f7e52d5 --- /dev/null +++ b/mispmock/threads-in-mock.uxf @@ -0,0 +1,340 @@ + + + 10 + + UMLClass + + 910 + 50 + 90 + 30 + + inet +lt=- + + + + UMLClass + + 680 + 50 + 100 + 30 + + webhost +lt=- +layer=-1 + + + + UMLClass + + 910 + 90 + 90 + 30 + + user +bg=#90CAF9 + + + + UMLClass + + 680 + 90 + 100 + 30 + + mispbridge +bg=#B39DDB +layer=-1 + + + + Relation + + 410 + 140 + 300 + 50 + + lt=<<<- +POST (Ride) +Generated by Loop + 280.0;20.0;10.0;20.0 + + + Relation + + 760 + 180 + 200 + 40 + + lt=<<<- +GET (Request) +fg=#1E88E5 + 10.0;20.0;180.0;20.0 + + + UMLClass + + 350 + 90 + 100 + 30 + + mispclient +bg=#B39DDB +layer=-1 + + + + Relation + + 410 + 290 + 300 + 50 + + lt=<<<- +GET (Ride)(Request) +(Data) + 280.0;20.0;10.0;20.0 + + + Relation + + 760 + 310 + 190 + 40 + + lt=<<<. +OK (Data) +fg=#1E88E5 + 170.0;20.0;10.0;20.0 + + + Relation + + 410 + 350 + 300 + 40 + + lt=<<<. +OK (Ride) + 10.0;20.0;280.0;20.0 + + + UMLClass + + 150 + 90 + 80 + 30 + + app +bg=#90CAF9 + + + + Relation + + 200 + 220 + 200 + 40 + + lt=<<<- +GET (Request) +fg=#1E88E5 + 10.0;20.0;180.0;20.0 + + + Relation + + 200 + 270 + 200 + 40 + + lt=<<<. +OK (Data) +fg=#1E88E5 + 180.0;20.0;10.0;20.0 + + + Relation + + 410 + 200 + 300 + 40 + + lt=<<<. +OK (Ride)(Request) + 10.0;20.0;280.0;20.0 + + + UMLClass + + 150 + 50 + 300 + 30 + + localhost +lt=- + + + + UMLClass + + 120 + 20 + 910 + 400 + + +lt=.. +layer=-10 + + + + Relation + + 940 + 110 + 30 + 310 + + lt=- +fg=#1E88E5 + 10.0;10.0;10.0;290.0 + + + Relation + + 180 + 110 + 30 + 310 + + lt=- +fg=#1E88E5 + 10.0;10.0;10.0;290.0 + + + UMLClass + + 930 + 200 + 40 + 130 + + +bg=#F6F6F6 +transparency=0 +layer=4 + + + + UMLClass + + 170 + 240 + 40 + 50 + + +bg=#F6F6F6 +transparency=0 +layer=4 + + + + Relation + + 720 + 110 + 30 + 310 + + lt=- +fg=#5E35B1 +layer=-4 + 10.0;10.0;10.0;290.0 + + + Relation + + 390 + 110 + 30 + 310 + + lt=- +fg=#5E35B1 +layer=-4 + 10.0;10.0;10.0;290.0 + + + UMLClass + + 380 + 160 + 40 + 210 + + +bg=#F6F6F6 +transparency=0 +layer=4 + + + + UMLClass + + 690 + 160 + 40 + 60 + + +bg=#F6F6F6 +transparency=0 +layer=4 + + + + UMLClass + + 690 + 310 + 40 + 60 + + +bg=#F6F6F6 +transparency=0 +layer=4 + + + + UMLClass + + 730 + 200 + 40 + 130 + + +bg=#F6F6F6 +transparency=0 +layer=4 + + + diff --git a/onion-diagram.uxf b/onion-diagram.uxf new file mode 100644 index 0000000..a0a6a87 --- /dev/null +++ b/onion-diagram.uxf @@ -0,0 +1,204 @@ + + + 10 + + UMLClass + + 670 + 1040 + 130 + 30 + + TinyVNC + + + + UMLClass + + 670 + 1010 + 130 + 30 + + Guacamole + + + + UMLClass + + 650 + 970 + 170 + 150 + + Debian VM + + + + UMLClass + + 630 + 910 + 210 + 230 + + VirtualBox +layer=-1 + + + + UMLClass + + 670 + 1070 + 130 + 30 + + JavaFX + + + + UMLClass + + 650 + 940 + 170 + 30 + + Bridged Connection + + + + UMLClass + + 610 + 880 + 250 + 280 + + Debian Host +layer=-1 + + + + UMLClass + + 670 + 760 + 130 + 30 + + 4G Modem + + + + UMLClass + + 910 + 610 + 100 + 30 + + URL + + + + UMLClass + + 650 + 580 + 170 + 80 + + Hosting Provider + + + + UMLClass + + 910 + 760 + 100 + 30 + + IP / Port + + + + UMLClass + + 910 + 940 + 100 + 30 + + IP / Port + + + + Relation + + 960 + 780 + 30 + 180 + + lt=<-> + 10.0;10.0;10.0;160.0 + + + UMLClass + + 670 + 610 + 130 + 30 + + WordPress + + + + Relation + + 810 + 940 + 120 + 30 + + lt=- + 100.0;10.0;10.0;10.0 + + + Relation + + 790 + 760 + 140 + 30 + + lt=- + 120.0;10.0;10.0;10.0 + + + Relation + + 960 + 630 + 30 + 150 + + lt=<-> + 10.0;10.0;10.0;130.0 + + + Relation + + 790 + 610 + 140 + 30 + + lt=- + 120.0;10.0;10.0;10.0 + + diff --git a/test-proxy/currentconfig.xml b/test-proxy/currentconfig.xml new file mode 100644 index 0000000..a956c02 --- /dev/null +++ b/test-proxy/currentconfig.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-proxy/install-locally.sh b/test-proxy/install-locally.sh new file mode 100755 index 0000000..aeb5dc0 --- /dev/null +++ b/test-proxy/install-locally.sh @@ -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 diff --git a/test-proxy/pom.xml b/test-proxy/pom.xml index ec1059b..80b2939 100644 --- a/test-proxy/pom.xml +++ b/test-proxy/pom.xml @@ -15,8 +15,8 @@ UTF-8 - 1.7 - 1.7 + 1.11 + 1.11 @@ -26,10 +26,61 @@ 4.11 test + + javax.servlet + javax.servlet-api + 4.0.1 + provided + + + org.eclipse.jetty + jetty-servlet + 9.4.28.v20200408 + provided + + + org.eclipse.jetty + jetty-util + 9.4.28.v20200408 + + + org.eclipse.jetty + jetty-client + 9.4.28.v20200408 + + + org.junit.jupiter + junit-jupiter + RELEASE + test + + + org.eclipse.jetty.toolchain + jetty-test-helper + test + + + org.eclipse.jetty.toolchain + jetty-test-helper + 5.3 + test + + + org.eclipse.jetty + jetty-http + 9.4.28.v20200408 + test + + + org.eclipse.jetty + jetty-io + 9.4.28.v20200408 + test + + - test-proxy @@ -44,6 +95,12 @@ maven-compiler-plugin 3.8.0 + + + 11 + 11 + true + maven-surefire-plugin diff --git a/test-proxy/someconfig.xml b/test-proxy/someconfig.xml new file mode 100644 index 0000000..ab71816 --- /dev/null +++ b/test-proxy/someconfig.xml @@ -0,0 +1,56 @@ + + + + + + + org.eclipse.jetty.servlets.ProxyServlet$Transparent + /proxy/* + + maxThreads + + + + + + maxConnections + + + + + + idleTimeout + + + + + + timeout + + + + + + ProxyTo + http://localhost:8080/misp-mirror + + + Prefix + /proxy + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test-proxy/src/main/java/module-info.java b/test-proxy/src/main/java/module-info.java deleted file mode 100644 index 7457afe..0000000 --- a/test-proxy/src/main/java/module-info.java +++ /dev/null @@ -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; -} diff --git a/test-proxy/src/main/java/servlets/CGI.java b/test-proxy/src/main/java/servlets/CGI.java deleted file mode 100644 index c462e7a..0000000 --- a/test-proxy/src/main/java/servlets/CGI.java +++ /dev/null @@ -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. - *

- * The following init parameters are used to configure this servlet: - *

- *
cgibinResourceBase
- *
Path to the cgi bin directory if set or it will default to the resource base of the context.
- *
resourceBase
- *
An alias for cgibinResourceBase.
- *
cgibinResourceBaseIsRelative
- *
If true then cgibinResourceBase is relative to the webapp (eg "WEB-INF/cgi")
- *
commandPrefix
- *
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.
- *
Path
- *
passed to the exec environment as PATH.
- *
ENV_*
- *
used to set an arbitrary environment variable with the name stripped of the leading ENV_ and using the init parameter value
- *
useFullPath
- *
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
- *
ignoreExitState
- *
If true then do not act on a non-zero exec exit status")
- *
- */ -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 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 parameterMap = new MultiMap<>(); - Enumeration 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 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 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(); - } - } -} diff --git a/test-proxy/src/main/java/servlets/CloseableDoSFilter.java b/test-proxy/src/main/java/servlets/CloseableDoSFilter.java deleted file mode 100644 index dd4264b..0000000 --- a/test-proxy/src/main/java/servlets/CloseableDoSFilter.java +++ /dev/null @@ -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(); - } -} diff --git a/test-proxy/src/main/java/servlets/ConcatServlet.java b/test-proxy/src/main/java/servlets/ConcatServlet.java deleted file mode 100644 index 6af12af..0000000 --- a/test-proxy/src/main/java/servlets/ConcatServlet.java +++ /dev/null @@ -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; - -/** - *

This servlet may be used to concatenate multiple resources into - * a single response.

- *

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.

- *

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).

- *

The servlet uses parameter names of the query string as resource names - * relative to the context root. So these script tags:

- *
- * <script type="text/javascript" src="../js/behaviour.js"></script>
- * <script type="text/javascript" src="../js/ajax.js"></script>
- * <script type="text/javascript" src="../chat/chat.js"></script>
- * 
- *

can be replaced with the single tag (with the {@code ConcatServlet} - * mapped to {@code /concat}):

- *
- * <script type="text/javascript" src="../concat?/js/behaviour.js&/js/ajax.js&/chat/chat.js"></script>
- * 
- *

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.

- *

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.

- *

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.

- */ -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 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()); - } -} diff --git a/test-proxy/src/main/java/servlets/CrossOriginFilter.java b/test-proxy/src/main/java/servlets/CrossOriginFilter.java deleted file mode 100644 index 80167cc..0000000 --- a/test-proxy/src/main/java/servlets/CrossOriginFilter.java +++ /dev/null @@ -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 - * cross-origin resource sharing. - *

- * A typical example is to use this filter to allow cross-domain - * cometd communication using the standard - * long polling transport instead of the JSONP transport (that is less - * efficient and less reactive to failures). - *

- * This filter allows the following configuration parameters: - *

- *
allowedOrigins
- *
a comma separated list of origins that are - * allowed to access the resources. Default value is *, meaning all - * origins. Note that using wild cards can result in security problems - * for requests identifying hosts that do not exist. - *

- * 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. - *

- * 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.).

- * - *
allowedTimingOrigins
- *
a comma separated list of origins that are - * allowed to time the resource. Default value is the empty string, meaning - * no origins. - *

- * The check whether the timing header is set, will be performed only if - * the user gets general access to the resource using the allowedOrigins. - * - *

allowedMethods
- *
a comma separated list of HTTP methods that - * are allowed to be used when accessing the resources. Default value is - * GET,POST,HEAD
- * - * - *
allowedHeaders
- *
a comma separated list of HTTP headers that - * are allowed to be specified when accessing the resources. Default value - * is X-Requested-With,Content-Type,Accept,Origin. If the value is a single "*", - * this means that any headers will be accepted.
- * - *
preflightMaxAge
- *
the number of seconds that preflight requests - * can be cached by the client. Default value is 1800 seconds, or 30 - * minutes
- * - *
allowCredentials
- *
a boolean indicating if the resource allows - * requests with credentials. Default value is true
- * - *
exposedHeaders
- *
a comma separated list of HTTP headers that - * are allowed to be exposed on the client. Default value is the - * empty list
- * - *
chainPreflight
- *
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 true.
- * - *
- * A typical configuration could be: - *
- * <web-app ...>
- *     ...
- *     <filter>
- *         <filter-name>cross-origin</filter-name>
- *         <filter-class>org.eclipse.jetty.servlets.CrossOriginFilter</filter-class>
- *     </filter>
- *     <filter-mapping>
- *         <filter-name>cross-origin</filter-name>
- *         <url-pattern>/cometd/*</url-pattern>
- *     </filter-mapping>
- *     ...
- * </web-app>
- * 
- */ -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 SIMPLE_HTTP_METHODS = Arrays.asList("GET", "POST", "HEAD"); - private static final List DEFAULT_ALLOWED_METHODS = Arrays.asList("GET", "POST", "HEAD"); - private static final List DEFAULT_ALLOWED_HEADERS = Arrays.asList("X-Requested-With", "Content-Type", "Accept", "Origin"); - - private boolean anyOriginAllowed; - private boolean anyTimingOriginAllowed; - private boolean anyHeadersAllowed; - private Set allowedOrigins = new HashSet(); - private List allowedOriginPatterns = new ArrayList(); - private Set allowedTimingOrigins = new HashSet(); - private List allowedTimingOriginPatterns = new ArrayList(); - private List allowedMethods = new ArrayList(); - private List allowedHeaders = new ArrayList(); - private List exposedHeaders = new ArrayList(); - 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 allowedOriginStore, List 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 connections = request.getHeaders("Connection"); connections.hasMoreElements(); ) - { - String connection = (String)connections.nextElement(); - if ("Upgrade".equalsIgnoreCase(connection)) - { - for (Enumeration upgrades = request.getHeaders("Upgrade"); upgrades.hasMoreElements(); ) - { - String upgrade = (String)upgrades.nextElement(); - if ("WebSocket".equalsIgnoreCase(upgrade)) - return false; - } - } - } - return true; - } - - private boolean originMatches(Set allowedOrigins, List 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 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 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 requestedHeaders = new ArrayList(); - 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 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 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; - } -} diff --git a/test-proxy/src/main/java/servlets/DataRateLimitedServlet.java b/test-proxy/src/main/java/servlets/DataRateLimitedServlet.java deleted file mode 100644 index dace47b..0000000 --- a/test-proxy/src/main/java/servlets/DataRateLimitedServlet.java +++ /dev/null @@ -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. - *

- * Two implementations are supported:

    - *
  • The StandardDataStream impl uses only standard - * APIs, but produces more garbage due to the byte[] nature of the API. - *
  • the JettyDataStream 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!). - *
- *

- * The data rate is controlled by setting init parameters: - *

- *
buffersize
The amount of data in bytes written per write
- *
pause
The period in ms to wait after a write before attempting another
- *
pool
The size of the thread pool used to service the writes (defaults to available processors)
- *
- * 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 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(); - } - } -} diff --git a/test-proxy/src/main/java/servlets/DoSFilter.java b/test-proxy/src/main/java/servlets/DoSFilter.java deleted file mode 100644 index bad473f..0000000 --- a/test-proxy/src/main/java/servlets/DoSFilter.java +++ /dev/null @@ -1,1329 +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.HttpStatus; -import org.eclipse.jetty.server.handler.ContextHandler; -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.eclipse.jetty.util.annotation.Name; -import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler; -import org.eclipse.jetty.util.thread.Scheduler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.servlet.*; -import javax.servlet.http.*; -import java.io.IOException; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Queue; -import java.util.concurrent.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Denial of Service filter - *

- * This filter is useful for limiting - * exposure to abuse from request flooding, whether malicious, or as a result of - * a misconfigured client. - *

- * The filter keeps track of the number of requests from a connection per - * second. If a limit is exceeded, the request is either rejected, delayed, or - * throttled. - *

- * When a request is throttled, it is placed in a priority queue. Priority is - * given first to authenticated users and users with an HttpSession, then - * connections which can be identified by their IP addresses. Connections with - * no way to identify them are given lowest priority. - *

- * The {@link #extractUserId(ServletRequest request)} function should be - * implemented, in order to uniquely identify authenticated users. - *

- * The following init parameters control the behavior of the filter: - *

- *
maxRequestsPerSec
- *
the maximum number of requests from a connection per - * second. Requests in excess of this are first delayed, - * then throttled.
- *
delayMs
- *
is the delay given to all requests over the rate limit, - * before they are considered at all. -1 means just reject request, - * 0 means no delay, otherwise it is the delay.
- *
maxWaitMs
- *
how long to blocking wait for the throttle semaphore.
- *
throttledRequests
- *
is the number of requests over the rate limit able to be - * considered at once.
- *
throttleMs
- *
how long to async wait for semaphore.
- *
maxRequestMs
- *
how long to allow this request to run.
- *
maxIdleTrackerMs
- *
how long to keep track of request rates for a connection, - * before deciding that the user has gone away, and discarding it
- *
insertHeaders
- *
if true , insert the DoSFilter headers into the response. Defaults to true.
- *
trackSessions
- *
if true, usage rate is tracked by session if a session exists. Defaults to true.
- *
remotePort
- *
if true and session tracking is not used, then rate is tracked by IP+port (effectively connection). Defaults to false.
- *
ipWhitelist
- *
a comma-separated list of IP addresses that will not be rate limited
- *
managedAttr
- *
if 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.
- *
tooManyCode
- *
The status code to send if there are too many requests. By default is 429 (too many requests), but 503 (Unavailable) is - * another option
- *
- *

- * This filter should be configured for {@link DispatcherType#REQUEST} and {@link DispatcherType#ASYNC} and with - * {@code true}. - *

- */ -@ManagedObject("limits exposure to abuse from request flooding, whether malicious, or as a result of a misconfigured client") -public class DoSFilter implements Filter -{ - private static final Logger LOG = LoggerFactory.getLogger(DoSFilter.class); - - private static final String IPv4_GROUP = "(\\d{1,3})"; - private static final Pattern IPv4_PATTERN = Pattern.compile(IPv4_GROUP + "\\." + IPv4_GROUP + "\\." + IPv4_GROUP + "\\." + IPv4_GROUP); - private static final String IPv6_GROUP = "(\\p{XDigit}{1,4})"; - private static final Pattern IPv6_PATTERN = Pattern.compile(IPv6_GROUP + ":" + IPv6_GROUP + ":" + IPv6_GROUP + ":" + IPv6_GROUP + ":" + IPv6_GROUP + ":" + IPv6_GROUP + ":" + IPv6_GROUP + ":" + IPv6_GROUP); - private static final Pattern CIDR_PATTERN = Pattern.compile("([^/]+)/(\\d+)"); - - private static final String __TRACKER = "DoSFilter.Tracker"; - private static final String __THROTTLED = "DoSFilter.Throttled"; - - private static final int __DEFAULT_MAX_REQUESTS_PER_SEC = 25; - private static final int __DEFAULT_DELAY_MS = 100; - private static final int __DEFAULT_THROTTLE = 5; - private static final int __DEFAULT_MAX_WAIT_MS = 50; - private static final long __DEFAULT_THROTTLE_MS = 30000L; - private static final long __DEFAULT_MAX_REQUEST_MS_INIT_PARAM = 30000L; - private static final long __DEFAULT_MAX_IDLE_TRACKER_MS_INIT_PARAM = 30000L; - - static final String MANAGED_ATTR_INIT_PARAM = "managedAttr"; - static final String MAX_REQUESTS_PER_S_INIT_PARAM = "maxRequestsPerSec"; - static final String DELAY_MS_INIT_PARAM = "delayMs"; - static final String THROTTLED_REQUESTS_INIT_PARAM = "throttledRequests"; - static final String MAX_WAIT_INIT_PARAM = "maxWaitMs"; - static final String THROTTLE_MS_INIT_PARAM = "throttleMs"; - static final String MAX_REQUEST_MS_INIT_PARAM = "maxRequestMs"; - static final String MAX_IDLE_TRACKER_MS_INIT_PARAM = "maxIdleTrackerMs"; - static final String INSERT_HEADERS_INIT_PARAM = "insertHeaders"; - static final String TRACK_SESSIONS_INIT_PARAM = "trackSessions"; - static final String REMOTE_PORT_INIT_PARAM = "remotePort"; - static final String IP_WHITELIST_INIT_PARAM = "ipWhitelist"; - static final String ENABLED_INIT_PARAM = "enabled"; - static final String TOO_MANY_CODE = "tooManyCode"; - - private static final int USER_AUTH = 2; - private static final int USER_SESSION = 2; - private static final int USER_IP = 1; - private static final int USER_UNKNOWN = 0; - - private final String _suspended = "DoSFilter@" + Integer.toHexString(hashCode()) + ".SUSPENDED"; - private final String _resumed = "DoSFilter@" + Integer.toHexString(hashCode()) + ".RESUMED"; - private final ConcurrentHashMap _rateTrackers = new ConcurrentHashMap<>(); - private final List _whitelist = new CopyOnWriteArrayList<>(); - private int _tooManyCode; - private volatile long _delayMs; - private volatile long _throttleMs; - private volatile long _maxWaitMs; - private volatile long _maxRequestMs; - private volatile long _maxIdleTrackerMs; - private volatile boolean _insertHeaders; - private volatile boolean _trackSessions; - private volatile boolean _remotePort; - private volatile boolean _enabled; - private volatile String _name; - private Semaphore _passes; - private volatile int _throttledRequests; - private volatile int _maxRequestsPerSec; - private Queue[] _queues; - private AsyncListener[] _listeners; - private Scheduler _scheduler; - private ServletContext _context; - - @Override - public void init(FilterConfig filterConfig) throws ServletException - { - _queues = new Queue[getMaxPriority() + 1]; - _listeners = new AsyncListener[_queues.length]; - for (int p = 0; p < _queues.length; p++) - { - _queues[p] = new ConcurrentLinkedQueue<>(); - _listeners[p] = new DoSAsyncListener(p); - } - - _rateTrackers.clear(); - - int maxRequests = __DEFAULT_MAX_REQUESTS_PER_SEC; - String parameter = filterConfig.getInitParameter(MAX_REQUESTS_PER_S_INIT_PARAM); - if (parameter != null) - maxRequests = Integer.parseInt(parameter); - setMaxRequestsPerSec(maxRequests); - - long delay = __DEFAULT_DELAY_MS; - parameter = filterConfig.getInitParameter(DELAY_MS_INIT_PARAM); - if (parameter != null) - delay = Long.parseLong(parameter); - setDelayMs(delay); - - int throttledRequests = __DEFAULT_THROTTLE; - parameter = filterConfig.getInitParameter(THROTTLED_REQUESTS_INIT_PARAM); - if (parameter != null) - throttledRequests = Integer.parseInt(parameter); - setThrottledRequests(throttledRequests); - - long maxWait = __DEFAULT_MAX_WAIT_MS; - parameter = filterConfig.getInitParameter(MAX_WAIT_INIT_PARAM); - if (parameter != null) - maxWait = Long.parseLong(parameter); - setMaxWaitMs(maxWait); - - long throttle = __DEFAULT_THROTTLE_MS; - parameter = filterConfig.getInitParameter(THROTTLE_MS_INIT_PARAM); - if (parameter != null) - throttle = Long.parseLong(parameter); - setThrottleMs(throttle); - - long maxRequestMs = __DEFAULT_MAX_REQUEST_MS_INIT_PARAM; - parameter = filterConfig.getInitParameter(MAX_REQUEST_MS_INIT_PARAM); - if (parameter != null) - maxRequestMs = Long.parseLong(parameter); - setMaxRequestMs(maxRequestMs); - - long maxIdleTrackerMs = __DEFAULT_MAX_IDLE_TRACKER_MS_INIT_PARAM; - parameter = filterConfig.getInitParameter(MAX_IDLE_TRACKER_MS_INIT_PARAM); - if (parameter != null) - maxIdleTrackerMs = Long.parseLong(parameter); - setMaxIdleTrackerMs(maxIdleTrackerMs); - - String whiteList = ""; - parameter = filterConfig.getInitParameter(IP_WHITELIST_INIT_PARAM); - if (parameter != null) - whiteList = parameter; - setWhitelist(whiteList); - - parameter = filterConfig.getInitParameter(INSERT_HEADERS_INIT_PARAM); - setInsertHeaders(parameter == null || Boolean.parseBoolean(parameter)); - - parameter = filterConfig.getInitParameter(TRACK_SESSIONS_INIT_PARAM); - setTrackSessions(parameter == null || Boolean.parseBoolean(parameter)); - - parameter = filterConfig.getInitParameter(REMOTE_PORT_INIT_PARAM); - setRemotePort(parameter != null && Boolean.parseBoolean(parameter)); - - parameter = filterConfig.getInitParameter(ENABLED_INIT_PARAM); - setEnabled(parameter == null || Boolean.parseBoolean(parameter)); - - parameter = filterConfig.getInitParameter(TOO_MANY_CODE); - setTooManyCode(parameter == null ? 429 : Integer.parseInt(parameter)); - - setName(filterConfig.getFilterName()); - _context = filterConfig.getServletContext(); - if (_context != null) - { - _context.setAttribute(filterConfig.getFilterName(), this); - } - - _scheduler = startScheduler(); - } - - protected Scheduler startScheduler() throws ServletException - { - try - { - Scheduler result = new ScheduledExecutorScheduler(String.format("DoS-Scheduler-%x", hashCode()), false); - result.start(); - return result; - } - catch (Exception x) - { - throw new ServletException(x); - } - } - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException - { - doFilter((HttpServletRequest)request, (HttpServletResponse)response, filterChain); - } - - protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException - { - if (!isEnabled()) - { - filterChain.doFilter(request, response); - return; - } - - // Look for the rate tracker for this request. - RateTracker tracker = (RateTracker)request.getAttribute(__TRACKER); - if (tracker == null) - { - // This is the first time we have seen this request. - if (LOG.isDebugEnabled()) - LOG.debug("Filtering {}", request); - - // Get a rate tracker associated with this request, and record one hit. - tracker = getRateTracker(request); - - // Calculate the rate and check if it is over the allowed limit - final boolean overRateLimit = tracker.isRateExceeded(System.currentTimeMillis()); - - // Pass it through if we are not currently over the rate limit. - if (!overRateLimit) - { - if (LOG.isDebugEnabled()) - LOG.debug("Allowing {}", request); - doFilterChain(filterChain, request, response); - return; - } - - // We are over the limit. - - // So either reject it, delay it or throttle it. - long delayMs = getDelayMs(); - boolean insertHeaders = isInsertHeaders(); - switch ((int)delayMs) - { - case -1: - { - // Reject this request. - LOG.warn("DOS ALERT: Request rejected ip={}, session={}, user={}", request.getRemoteAddr(), request.getRequestedSessionId(), request.getUserPrincipal()); - if (insertHeaders) - response.addHeader("DoSFilter", "unavailable"); - response.sendError(getTooManyCode()); - return; - } - case 0: - { - // Fall through to throttle the request. - LOG.warn("DOS ALERT: Request throttled ip={}, session={}, user={}", request.getRemoteAddr(), request.getRequestedSessionId(), request.getUserPrincipal()); - request.setAttribute(__TRACKER, tracker); - break; - } - default: - { - // Insert a delay before throttling the request, - // using the suspend+timeout mechanism of AsyncContext. - LOG.warn("DOS ALERT: Request delayed={}ms, ip={}, session={}, user={}", delayMs, request.getRemoteAddr(), request.getRequestedSessionId(), request.getUserPrincipal()); - if (insertHeaders) - response.addHeader("DoSFilter", "delayed"); - request.setAttribute(__TRACKER, tracker); - AsyncContext asyncContext = request.startAsync(); - if (delayMs > 0) - asyncContext.setTimeout(delayMs); - asyncContext.addListener(new DoSTimeoutAsyncListener()); - return; - } - } - } - - if (LOG.isDebugEnabled()) - LOG.debug("Throttling {}", request); - - // Throttle the request. - boolean accepted = false; - try - { - // Check if we can afford to accept another request at this time. - accepted = _passes.tryAcquire(getMaxWaitMs(), TimeUnit.MILLISECONDS); - if (!accepted) - { - // We were not accepted, so either we suspend to wait, - // or if we were woken up we insist or we fail. - Boolean throttled = (Boolean)request.getAttribute(__THROTTLED); - long throttleMs = getThrottleMs(); - if (!Boolean.TRUE.equals(throttled) && throttleMs > 0) - { - int priority = getPriority(request, tracker); - request.setAttribute(__THROTTLED, Boolean.TRUE); - if (isInsertHeaders()) - response.addHeader("DoSFilter", "throttled"); - AsyncContext asyncContext = request.startAsync(); - request.setAttribute(_suspended, Boolean.TRUE); - asyncContext.setTimeout(throttleMs); - asyncContext.addListener(_listeners[priority]); - _queues[priority].add(asyncContext); - if (LOG.isDebugEnabled()) - LOG.debug("Throttled {}, {}ms", request, throttleMs); - return; - } - - Boolean resumed = (Boolean)request.getAttribute(_resumed); - if (Boolean.TRUE.equals(resumed)) - { - // We were resumed, we wait for the next pass. - _passes.acquire(); - accepted = true; - } - } - - // If we were accepted (either immediately or after throttle)... - if (accepted) - { - // ...call the chain. - if (LOG.isDebugEnabled()) - LOG.debug("Allowing {}", request); - doFilterChain(filterChain, request, response); - } - else - { - // ...otherwise fail the request. - if (LOG.isDebugEnabled()) - LOG.debug("Rejecting {}", request); - if (isInsertHeaders()) - response.addHeader("DoSFilter", "unavailable"); - response.sendError(getTooManyCode()); - } - } - catch (InterruptedException e) - { - LOG.trace("IGNORED", e); - response.sendError(getTooManyCode()); - } - finally - { - if (accepted) - { - try - { - // Wake up the next highest priority request. - 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)) - { - if (LOG.isDebugEnabled()) - LOG.debug("Resuming {}", request); - candidate.setAttribute(_resumed, Boolean.TRUE); - asyncContext.dispatch(); - break; - } - } - } - } - finally - { - _passes.release(); - } - } - } - } - - protected void doFilterChain(FilterChain chain, final HttpServletRequest request, final HttpServletResponse response) throws IOException, ServletException - { - final Thread thread = Thread.currentThread(); - Runnable requestTimeout = () -> onRequestTimeout(request, response, thread); - Scheduler.Task task = _scheduler.schedule(requestTimeout, getMaxRequestMs(), TimeUnit.MILLISECONDS); - try - { - chain.doFilter(request, response); - } - finally - { - task.cancel(); - } - } - - /** - * Invoked when the request handling exceeds {@link #getMaxRequestMs()}. - *

- * By default, an HTTP 503 response is returned and the handling thread is interrupted. - * - * @param request the current request - * @param response the current response - * @param handlingThread the handling thread - */ - protected void onRequestTimeout(HttpServletRequest request, HttpServletResponse response, Thread handlingThread) - { - try - { - if (LOG.isDebugEnabled()) - LOG.debug("Timing out {}", request); - try - { - response.sendError(HttpStatus.SERVICE_UNAVAILABLE_503); - } - catch (IllegalStateException ise) - { - LOG.trace("IGNORED", ise); - // abort instead - response.sendError(-1); - } - } - catch (Throwable x) - { - LOG.info("Failed to sendError", x); - } - - handlingThread.interrupt(); - } - - /** - * Get priority for this request, based on user type - * - * @param request the current request - * @param tracker the rate tracker for this request - * @return the priority for this request - */ - private int getPriority(HttpServletRequest request, RateTracker tracker) - { - if (extractUserId(request) != null) - return USER_AUTH; - if (tracker != null) - return tracker.getType(); - return USER_UNKNOWN; - } - - /** - * @return the maximum priority that we can assign to a request - */ - protected int getMaxPriority() - { - return USER_AUTH; - } - - private void schedule(RateTracker tracker) - { - _scheduler.schedule(tracker, getMaxIdleTrackerMs(), TimeUnit.MILLISECONDS); - } - - /** - * Return a request rate tracker associated with this connection; keeps - * track of this connection's request rate. If this is not the first request - * from this connection, return the existing object with the stored stats. - * If it is the first request, then create a new request tracker. - *

- * Assumes that each connection has an identifying characteristic, and goes - * through them in order, taking the first that matches: user id (logged - * in), session id, client IP address. Unidentifiable connections are lumped - * into one. - *

- * When a session expires, its rate tracker is automatically deleted. - * - * @param request the current request - * @return the request rate tracker for the current connection - */ - RateTracker getRateTracker(ServletRequest request) - { - HttpSession session = ((HttpServletRequest)request).getSession(false); - - String loadId = extractUserId(request); - final int type; - if (loadId != null) - { - type = USER_AUTH; - } - else - { - if (isTrackSessions() && session != null && !session.isNew()) - { - loadId = session.getId(); - type = USER_SESSION; - } - else - { - loadId = isRemotePort() ? createRemotePortId(request) : request.getRemoteAddr(); - type = USER_IP; - } - } - - RateTracker tracker = _rateTrackers.get(loadId); - - if (tracker == null) - { - boolean allowed = checkWhitelist(request.getRemoteAddr()); - int maxRequestsPerSec = getMaxRequestsPerSec(); - tracker = allowed ? new FixedRateTracker(_context, _name, loadId, type, maxRequestsPerSec) - : new RateTracker(_context, _name, loadId, type, maxRequestsPerSec); - tracker.setContext(_context); - RateTracker existing = _rateTrackers.putIfAbsent(loadId, tracker); - if (existing != null) - tracker = existing; - - if (type == USER_IP) - { - // USER_IP expiration from _rateTrackers is handled by the _scheduler - _scheduler.schedule(tracker, getMaxIdleTrackerMs(), TimeUnit.MILLISECONDS); - } - else if (session != null) - { - // USER_SESSION expiration from _rateTrackers are handled by the HttpSessionBindingListener - session.setAttribute(__TRACKER, tracker); - } - } - - return tracker; - } - - private void addToRateTracker(RateTracker tracker) - { - _rateTrackers.put(tracker.getId(), tracker); - } - - public void removeFromRateTracker(String id) - { - _rateTrackers.remove(id); - } - - protected boolean checkWhitelist(String candidate) - { - for (String address : _whitelist) - { - if (address.contains("/")) - { - if (subnetMatch(address, candidate)) - return true; - } - else - { - if (address.equals(candidate)) - return true; - } - } - return false; - } - - protected boolean subnetMatch(String subnetAddress, String address) - { - Matcher cidrMatcher = CIDR_PATTERN.matcher(subnetAddress); - if (!cidrMatcher.matches()) - return false; - - String subnet = cidrMatcher.group(1); - int prefix; - try - { - prefix = Integer.parseInt(cidrMatcher.group(2)); - } - catch (NumberFormatException x) - { - LOG.info("Ignoring malformed CIDR address {}", subnetAddress); - return false; - } - - byte[] subnetBytes = addressToBytes(subnet); - if (subnetBytes == null) - { - LOG.info("Ignoring malformed CIDR address {}", subnetAddress); - return false; - } - byte[] addressBytes = addressToBytes(address); - if (addressBytes == null) - { - LOG.info("Ignoring malformed remote address {}", address); - return false; - } - - // Comparing IPv4 with IPv6 ? - int length = subnetBytes.length; - if (length != addressBytes.length) - return false; - - byte[] mask = prefixToBytes(prefix, length); - - for (int i = 0; i < length; ++i) - { - if ((subnetBytes[i] & mask[i]) != (addressBytes[i] & mask[i])) - return false; - } - - return true; - } - - private byte[] addressToBytes(String address) - { - Matcher ipv4Matcher = IPv4_PATTERN.matcher(address); - if (ipv4Matcher.matches()) - { - byte[] result = new byte[4]; - for (int i = 0; i < result.length; ++i) - { - result[i] = Integer.valueOf(ipv4Matcher.group(i + 1)).byteValue(); - } - return result; - } - else - { - Matcher ipv6Matcher = IPv6_PATTERN.matcher(address); - if (ipv6Matcher.matches()) - { - byte[] result = new byte[16]; - for (int i = 0; i < result.length; i += 2) - { - int word = Integer.parseInt(ipv6Matcher.group(i / 2 + 1), 16); - result[i] = (byte)((word & 0xFF00) >>> 8); - result[i + 1] = (byte)(word & 0xFF); - } - return result; - } - } - return null; - } - - private byte[] prefixToBytes(int prefix, int length) - { - byte[] result = new byte[length]; - int index = 0; - while (prefix / 8 > 0) - { - result[index] = -1; - prefix -= 8; - ++index; - } - - if (index == result.length) - return result; - - // Sets the _prefix_ most significant bits to 1 - result[index] = (byte)~((1 << (8 - prefix)) - 1); - return result; - } - - @Override - public void destroy() - { - LOG.debug("Destroy {}", this); - stopScheduler(); - _rateTrackers.clear(); - _whitelist.clear(); - } - - protected void stopScheduler() - { - try - { - _scheduler.stop(); - } - catch (Exception x) - { - LOG.trace("IGNORED", x); - } - } - - /** - * Returns the user id, used to track this connection. - * This SHOULD be overridden by subclasses. - * - * @param request the current request - * @return a unique user id, if logged in; otherwise null. - */ - protected String extractUserId(ServletRequest request) - { - return null; - } - - /** - * Get maximum number of requests from a connection per - * second. Requests in excess of this are first delayed, - * then throttled. - * - * @return maximum number of requests - */ - @ManagedAttribute("maximum number of requests allowed from a connection per second") - public int getMaxRequestsPerSec() - { - return _maxRequestsPerSec; - } - - /** - * Get maximum number of requests from a connection per - * second. Requests in excess of this are first delayed, - * then throttled. - * - * @param value maximum number of requests - */ - public void setMaxRequestsPerSec(int value) - { - _maxRequestsPerSec = value; - } - - /** - * Get delay (in milliseconds) that is applied to all requests - * over the rate limit, before they are considered at all. - * - * @return the delay in milliseconds - */ - @ManagedAttribute("delay applied to all requests over the rate limit (in ms)") - public long getDelayMs() - { - return _delayMs; - } - - /** - * Set delay (in milliseconds) that is applied to all requests - * over the rate limit, before they are considered at all. - * - * @param value delay (in milliseconds), 0 - no delay, -1 - reject request - */ - public void setDelayMs(long value) - { - _delayMs = value; - } - - /** - * Get maximum amount of time (in milliseconds) the filter will - * blocking wait for the throttle semaphore. - * - * @return maximum wait time - */ - @ManagedAttribute("maximum time the filter will block waiting throttled connections, (0 for no delay, -1 to reject requests)") - public long getMaxWaitMs() - { - return _maxWaitMs; - } - - /** - * Set maximum amount of time (in milliseconds) the filter will - * blocking wait for the throttle semaphore. - * - * @param value maximum wait time - */ - public void setMaxWaitMs(long value) - { - _maxWaitMs = value; - } - - /** - * Get number of requests over the rate limit able to be - * considered at once. - * - * @return number of requests - */ - @ManagedAttribute("number of requests over rate limit") - public int getThrottledRequests() - { - return _throttledRequests; - } - - /** - * Set number of requests over the rate limit able to be - * considered at once. - * - * @param value number of requests - */ - public void setThrottledRequests(int value) - { - int permits = _passes == null ? 0 : _passes.availablePermits(); - _passes = new Semaphore((value - _throttledRequests + permits), true); - _throttledRequests = value; - } - - /** - * Get amount of time (in milliseconds) to async wait for semaphore. - * - * @return wait time - */ - @ManagedAttribute("amount of time to async wait for semaphore") - public long getThrottleMs() - { - return _throttleMs; - } - - /** - * Set amount of time (in milliseconds) to async wait for semaphore. - * - * @param value wait time - */ - public void setThrottleMs(long value) - { - _throttleMs = value; - } - - /** - * Get maximum amount of time (in milliseconds) to allow - * the request to process. - * - * @return maximum processing time - */ - @ManagedAttribute("maximum time to allow requests to process (in ms)") - public long getMaxRequestMs() - { - return _maxRequestMs; - } - - /** - * Set maximum amount of time (in milliseconds) to allow - * the request to process. - * - * @param value maximum processing time - */ - public void setMaxRequestMs(long value) - { - _maxRequestMs = value; - } - - /** - * Get maximum amount of time (in milliseconds) to keep track - * of request rates for a connection, before deciding that - * the user has gone away, and discarding it. - * - * @return maximum tracking time - */ - @ManagedAttribute("maximum time to track of request rates for connection before discarding") - public long getMaxIdleTrackerMs() - { - return _maxIdleTrackerMs; - } - - /** - * Set maximum amount of time (in milliseconds) to keep track - * of request rates for a connection, before deciding that - * the user has gone away, and discarding it. - * - * @param value maximum tracking time - */ - public void setMaxIdleTrackerMs(long value) - { - _maxIdleTrackerMs = value; - } - - /** - * The unique name of the filter when there is more than - * one DosFilter instance. - * - * @return the name - */ - public String getName() - { - return _name; - } - - /** - * @param name the name to set - */ - public void setName(String name) - { - _name = name; - } - - /** - * Check flag to insert the DoSFilter headers into the response. - * - * @return value of the flag - */ - @ManagedAttribute("inser DoSFilter headers in response") - public boolean isInsertHeaders() - { - return _insertHeaders; - } - - /** - * Set flag to insert the DoSFilter headers into the response. - * - * @param value value of the flag - */ - public void setInsertHeaders(boolean value) - { - _insertHeaders = value; - } - - /** - * Get flag to have usage rate tracked by session if a session exists. - * - * @return value of the flag - */ - @ManagedAttribute("usage rate is tracked by session if one exists") - public boolean isTrackSessions() - { - return _trackSessions; - } - - /** - * Set flag to have usage rate tracked by session if a session exists. - * - * @param value value of the flag - */ - public void setTrackSessions(boolean value) - { - _trackSessions = value; - } - - /** - * Get flag to have usage rate tracked by IP+port (effectively connection) - * if session tracking is not used. - * - * @return value of the flag - */ - @ManagedAttribute("usage rate is tracked by IP+port is session tracking not used") - public boolean isRemotePort() - { - return _remotePort; - } - - /** - * Set flag to have usage rate tracked by IP+port (effectively connection) - * if session tracking is not used. - * - * @param value value of the flag - */ - public void setRemotePort(boolean value) - { - _remotePort = value; - } - - /** - * @return whether this filter is enabled - */ - @ManagedAttribute("whether this filter is enabled") - public boolean isEnabled() - { - return _enabled; - } - - /** - * @param enabled whether this filter is enabled - */ - public void setEnabled(boolean enabled) - { - _enabled = enabled; - } - - public int getTooManyCode() - { - return _tooManyCode; - } - - public void setTooManyCode(int tooManyCode) - { - _tooManyCode = tooManyCode; - } - - /** - * Get a list of IP addresses that will not be rate limited. - * - * @return comma-separated whitelist - */ - @ManagedAttribute("list of IPs that will not be rate limited") - public String getWhitelist() - { - StringBuilder result = new StringBuilder(); - for (Iterator iterator = _whitelist.iterator(); iterator.hasNext(); ) - { - String address = iterator.next(); - result.append(address); - if (iterator.hasNext()) - result.append(","); - } - return result.toString(); - } - - /** - * Set a list of IP addresses that will not be rate limited. - * - * @param commaSeparatedList comma-separated whitelist - */ - public void setWhitelist(String commaSeparatedList) - { - List result = new ArrayList<>(); - for (String address : StringUtil.csvSplit(commaSeparatedList)) - { - addWhitelistAddress(result, address); - } - clearWhitelist(); - _whitelist.addAll(result); - LOG.debug("Whitelisted IP addresses: {}", result); - } - - /** - * Clears the list of whitelisted IP addresses - */ - @ManagedOperation("clears the list of IP addresses that will not be rate limited") - public void clearWhitelist() - { - _whitelist.clear(); - } - - /** - * Adds the given IP address, either in the form of a dotted decimal notation A.B.C.D - * or in the CIDR notation A.B.C.D/M, to the list of whitelisted IP addresses. - * - * @param address the address to add - * @return whether the address was added to the list - * @see #removeWhitelistAddress(String) - */ - @ManagedOperation("adds an IP address that will not be rate limited") - public boolean addWhitelistAddress(@Name("address") String address) - { - return addWhitelistAddress(_whitelist, address); - } - - private boolean addWhitelistAddress(List list, String address) - { - address = address.trim(); - return address.length() > 0 && list.add(address); - } - - /** - * Removes the given address from the list of whitelisted IP addresses. - * - * @param address the address to remove - * @return whether the address was removed from the list - * @see #addWhitelistAddress(String) - */ - @ManagedOperation("removes an IP address that will not be rate limited") - public boolean removeWhitelistAddress(@Name("address") String address) - { - return _whitelist.remove(address); - } - - /** - * A RateTracker is associated with a connection, and stores request rate - * data. - */ - static class RateTracker implements Runnable, HttpSessionBindingListener, HttpSessionActivationListener, Serializable - { - private static final long serialVersionUID = 3534663738034577872L; - - protected final String _filterName; - protected transient ServletContext _context; - protected final String _id; - protected final int _type; - protected final long[] _timestamps; - - protected int _next; - - public RateTracker(ServletContext context, String filterName, String id, int type, int maxRequestsPerSecond) - { - _context = context; - _filterName = filterName; - _id = id; - _type = type; - _timestamps = new long[maxRequestsPerSecond]; - _next = 0; - } - - /** - * @param now the time now (in milliseconds) - * @return the current calculated request rate over the last second - */ - public boolean isRateExceeded(long now) - { - final long last; - synchronized (this) - { - last = _timestamps[_next]; - _timestamps[_next] = now; - _next = (_next + 1) % _timestamps.length; - } - - return last != 0 && (now - last) < 1000L; - } - - public String getId() - { - return _id; - } - - public int getType() - { - return _type; - } - - @Override - public void valueBound(HttpSessionBindingEvent event) - { - if (LOG.isDebugEnabled()) - LOG.debug("Value bound: {}", getId()); - _context = event.getSession().getServletContext(); - } - - @Override - public void valueUnbound(HttpSessionBindingEvent event) - { - //take the tracker out of the list of trackers - DoSFilter filter = (DoSFilter)event.getSession().getServletContext().getAttribute(_filterName); - removeFromRateTrackers(filter, _id); - _context = null; - } - - @Override - public void sessionWillPassivate(HttpSessionEvent se) - { - //take the tracker of the list of trackers (if its still there) - DoSFilter filter = (DoSFilter)se.getSession().getServletContext().getAttribute(_filterName); - removeFromRateTrackers(filter, _id); - _context = null; - } - - @Override - public void sessionDidActivate(HttpSessionEvent se) - { - RateTracker tracker = (RateTracker)se.getSession().getAttribute(__TRACKER); - ServletContext context = se.getSession().getServletContext(); - tracker.setContext(context); - DoSFilter filter = (DoSFilter)context.getAttribute(_filterName); - if (filter == null) - { - LOG.info("No filter {} for rate tracker {}", _filterName, tracker); - return; - } - addToRateTrackers(filter, tracker); - } - - public void setContext(ServletContext context) - { - _context = context; - } - - protected void removeFromRateTrackers(DoSFilter filter, String id) - { - if (filter == null) - return; - - filter.removeFromRateTracker(id); - if (LOG.isDebugEnabled()) - LOG.debug("Tracker removed: {}", getId()); - } - - private void addToRateTrackers(DoSFilter filter, RateTracker tracker) - { - if (filter == null) - return; - filter.addToRateTracker(tracker); - } - - @Override - public void run() - { - if (_context == null) - { - LOG.warn("Unknkown context for rate tracker {}", this); - return; - } - - int latestIndex = _next == 0 ? (_timestamps.length - 1) : (_next - 1); - long last = _timestamps[latestIndex]; - boolean hasRecentRequest = last != 0 && (System.currentTimeMillis() - last) < 1000L; - - DoSFilter filter = (DoSFilter)_context.getAttribute(_filterName); - - if (hasRecentRequest) - { - if (filter != null) - filter.schedule(this); - else - LOG.warn("No filter {}", _filterName); - } - else - removeFromRateTrackers(filter, _id); - } - - @Override - public String toString() - { - return "RateTracker/" + _id + "/" + _type; - } - } - - private static class FixedRateTracker extends RateTracker - { - public FixedRateTracker(ServletContext context, String filterName, String id, int type, int numRecentRequestsTracked) - { - super(context, filterName, id, type, numRecentRequestsTracked); - } - - @Override - public boolean isRateExceeded(long now) - { - // rate limit is never exceeded, but we keep track of the request timestamps - // so that we know whether there was recent activity on this tracker - // and whether it should be expired - synchronized (this) - { - _timestamps[_next] = now; - _next = (_next + 1) % _timestamps.length; - } - - return false; - } - - @Override - public String toString() - { - return "Fixed" + super.toString(); - } - } - - private static class DoSTimeoutAsyncListener implements AsyncListener - { - @Override - public void onStartAsync(AsyncEvent event) - { - } - - @Override - public void onComplete(AsyncEvent event) - { - } - - @Override - public void onTimeout(AsyncEvent event) throws IOException - { - event.getAsyncContext().dispatch(); - } - - @Override - public void onError(AsyncEvent event) - { - } - } - - private class DoSAsyncListener extends DoSTimeoutAsyncListener - { - private final int priority; - - public DoSAsyncListener(int priority) - { - this.priority = priority; - } - - @Override - public void onTimeout(AsyncEvent event) throws IOException - { - _queues[priority].remove(event.getAsyncContext()); - super.onTimeout(event); - } - } - - private String createRemotePortId(final ServletRequest request) - { - final String addr = request.getRemoteAddr(); - final int port = request.getRemotePort(); - if (addr.contains(":")) - return "[" + addr + "]:" + port; - return addr + ":" + port; - } -} diff --git a/test-proxy/src/main/java/servlets/EventSource.java b/test-proxy/src/main/java/servlets/EventSource.java deleted file mode 100644 index 5bf2390..0000000 --- a/test-proxy/src/main/java/servlets/EventSource.java +++ /dev/null @@ -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; - -/** - *

{@link EventSource} is the passive half of an event source connection, as defined by the - * EventSource Specification.

- *

{@link EventSource.Emitter} is the active half of the connection and allows to operate on the connection.

- *

{@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.

- * - * @see EventSourceServlet - */ -public interface EventSource -{ - /** - *

Callback method invoked when an event source connection is opened.

- * - * @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; - - /** - *

Callback method invoked when an event source connection is closed.

- */ - public void onClose(); - - /** - *

{@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.

- *

An {@link Emitter} instance will be created for each new event source connection.

- *

{@link Emitter} instances are fully thread safe and can be used from multiple threads.

- */ - public interface Emitter - { - /** - *

Sends a named event with data to the client.

- *

When invoked as: event("foo", "bar"), the client will receive the lines:

- *
-         * event: foo
-         * data: bar
-         * 
- * - * @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; - - /** - *

Sends a default event with data to the client.

- *

When invoked as: data("baz"), the client will receive the line:

- *
-         * data: baz
-         * 
- *

When invoked as: data("foo\r\nbar\rbaz\nbax"), the client will receive the lines:

- *
-         * data: foo
-         * data: bar
-         * data: baz
-         * data: bax
-         * 
- * - * @param data the data to be sent - * @throws IOException if an I/O failure occurred - */ - public void data(String data) throws IOException; - - /** - *

Sends a comment to the client.

- *

When invoked as: comment("foo"), the client will receive the line:

- *
-         * : foo
-         * 
- * - * @param comment the comment to send - * @throws IOException if an I/O failure occurred - */ - public void comment(String comment) throws IOException; - - /** - *

Closes this event source connection.

- */ - public void close(); - } -} diff --git a/test-proxy/src/main/java/servlets/EventSourceServlet.java b/test-proxy/src/main/java/servlets/EventSourceServlet.java deleted file mode 100644 index 9344fa7..0000000 --- a/test-proxy/src/main/java/servlets/EventSourceServlet.java +++ /dev/null @@ -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; - -/** - *

A servlet that implements the event source protocol, - * also known as "server sent events".

- *

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.

- *

This servlet supports the following configuration parameters:

- *
    - *
  • heartBeatPeriod, that specifies the heartbeat period, in seconds, used to check - * whether the connection has been closed by the client; defaults to 10 seconds.
  • - *
- * - *

NOTE: there is currently no support for last-event-id.

- */ -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 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); - } - } - } -} diff --git a/test-proxy/src/main/java/servlets/HeaderFilter.java b/test-proxy/src/main/java/servlets/HeaderFilter.java deleted file mode 100644 index 461aac8..0000000 --- a/test-proxy/src/main/java/servlets/HeaderFilter.java +++ /dev/null @@ -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 - *

- * This filter sets or adds a header to the response. - *

- * The {@code headerConfig} init param is a CSV of actions to perform on headers, with the following syntax:
- * [action] [header name]: [header value]
- * [action] can be one of set, add, setDate, or addDate
- * The date actions will add the header value in milliseconds to the current system time before setting a date header. - *

- * Below is an example value for headerConfig:
- * - *

- * set X-Frame-Options: DENY,
- * "add Cache-Control: no-cache, no-store, must-revalidate",
- * setDate Expires: 31540000000,
- * addDate Date: 0
- * 
- * - * @see IncludeExcludeBasedFilter - */ -public class HeaderFilter extends IncludeExcludeBasedFilter -{ - private List _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; - } - } -} diff --git a/test-proxy/src/main/java/servlets/IncludeExcludeBasedFilter.java b/test-proxy/src/main/java/servlets/IncludeExcludeBasedFilter.java deleted file mode 100644 index 77405f6..0000000 --- a/test-proxy/src/main/java/servlets/IncludeExcludeBasedFilter.java +++ /dev/null @@ -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 - *

- * This is an abstract filter which helps with filtering based on include/exclude of paths, mime types, and/or http methods. - *

- * 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: - *

    - *
  • includedPaths - CSV of path specs to include
  • - *
  • excludedPaths - CSV of path specs to exclude
  • - *
  • includedMimeTypes - CSV of mime types to include
  • - *
  • excludedMimeTypes - CSV of mime types to exclude
  • - *
  • includedHttpMethods - CSV of http methods to include
  • - *
  • excludedHttpMethods - CSV of http methods to exclude
  • - *
- *

- * Path spec rules: - *

    - *
  • If the spec starts with '^' the spec is assumed to be a regex based path spec and will match with normal Java regex rules.
  • - *
  • If the spec starts with '/' the spec is assumed to be a Servlet url-pattern rules path spec for either an exact match or prefix based - * match.
  • - *
  • If the spec starts with '*.' the spec is assumed to be a Servlet url-pattern rules path spec for a suffix based match.
  • - *
  • All other syntaxes are unsupported.
  • - *
- *

- * CSVs are parsed with {@link StringUtil#csvSplit(String)} - * - * @see PathSpecSet - * @see IncludeExcludeSet - */ -public abstract class IncludeExcludeBasedFilter implements Filter -{ - private final IncludeExclude _mimeTypes = new IncludeExclude<>(); - private final IncludeExclude _httpMethods = new IncludeExclude<>(); - private final IncludeExclude _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(); - } -} diff --git a/test-proxy/src/main/java/servlets/PushCacheFilter.java b/test-proxy/src/main/java/servlets/PushCacheFilter.java deleted file mode 100644 index d78a544..0000000 --- a/test-proxy/src/main/java/servlets/PushCacheFilter.java +++ /dev/null @@ -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; - -/** - *

A filter that builds a cache of secondary resources associated - * to primary resources.

- *

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.

- *

Only secondary resources that are requested within a (small) time period - * from the request of the primary resource are associated with the primary - * resource.

- *

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.

- *

If the init param useQueryInKey is set, then the query string is used as - * as part of the key to identify a resource

- */ -@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 _ports = new HashSet<>(); - private final Set _hosts = new HashSet<>(); - private final ConcurrentMap _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 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 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 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 getPushCache() - { - Map result = new HashMap<>(); - for (Map.Entry 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 _associated = Collections.newSetFromMap(new ConcurrentHashMap<>()); - private final AtomicLong _timestamp = new AtomicLong(); - } -} diff --git a/test-proxy/src/main/java/servlets/PushSessionCacheFilter.java b/test-proxy/src/main/java/servlets/PushSessionCacheFilter.java deleted file mode 100644 index 9d71bb9..0000000 --- a/test-proxy/src/main/java/servlets/PushSessionCacheFilter.java +++ /dev/null @@ -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 _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 timestamps = (ConcurrentHashMap)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 timestamps = (ConcurrentHashMap)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 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 _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()); - } - } -} diff --git a/test-proxy/src/main/java/servlets/PutFilter.java b/test-proxy/src/main/java/servlets/PutFilter.java deleted file mode 100644 index 85137f3..0000000 --- a/test-proxy/src/main/java/servlets/PutFilter.java +++ /dev/null @@ -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:
    - *
  • baseURI - The file URI of the document root for put content. - *
  • delAllowed - boolean, if true DELETE and MOVE methods are supported. - *
  • putAtomic - boolean, if true PUT files are written to a temp location and moved into place. - *
- */ -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 _operations = new HashSet(); - private ConcurrentMap _hidden = new ConcurrentHashMap(); - - 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 options = new HashSet(); - 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; - } -} diff --git a/test-proxy/src/main/java/servlets/QoSFilter.java b/test-proxy/src/main/java/servlets/QoSFilter.java deleted file mode 100644 index 79fad50..0000000 --- a/test-proxy/src/main/java/servlets/QoSFilter.java +++ /dev/null @@ -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. - *

- * 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. - *

- * 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. - *

- * 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. - *

- * 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. - *

- * 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[] _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. - *

- * The default implementation assigns the following priorities: - *

    - *
  • 2 - for an authenticated request - *
  • 1 - for a request with valid / non new session - *
  • 0 - for all other requests. - *
- * 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 - { - } - } -} diff --git a/test-proxy/src/main/java/servlets/WelcomeFilter.java b/test-proxy/src/main/java/servlets/WelcomeFilter.java deleted file mode 100644 index 03f325b..0000000 --- a/test-proxy/src/main/java/servlets/WelcomeFilter.java +++ /dev/null @@ -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() - { - } -} - diff --git a/test-proxy/src/main/java/servlets/package-info.java b/test-proxy/src/main/java/servlets/package-info.java deleted file mode 100644 index e9151f2..0000000 --- a/test-proxy/src/main/java/servlets/package-info.java +++ /dev/null @@ -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; - diff --git a/test-proxy/src/main/webapp/WEB-INF/web.xml b/test-proxy/src/main/webapp/WEB-INF/web.xml index 9f88c1f..dcc0494 100644 --- a/test-proxy/src/main/webapp/WEB-INF/web.xml +++ b/test-proxy/src/main/webapp/WEB-INF/web.xml @@ -1,7 +1,22 @@ + "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" + "http://java.sun.com/dtd/web-app_2_3.dtd" > + + + + misp-proxy + + + test-proxy + com.olexyn.test.proxy.ProxyServlet + + + + test-proxy + /core + - - Archetype Created Web Application diff --git a/test-proxy/src/main/webapp/index.jsp b/test-proxy/src/main/webapp/index.jsp index c38169b..1ed1b19 100644 --- a/test-proxy/src/main/webapp/index.jsp +++ b/test-proxy/src/main/webapp/index.jsp @@ -1,5 +1,20 @@ - -

Hello World!

+ +test-proxy + + + + + + + + + + +
+ +
+

test-proxy

+