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

|
@ -0,0 +1,99 @@
|
|||||||
|
package com.olexyn.misp.mock;
|
||||||
|
|
||||||
|
import com.olexyn.misp.helper.Ride;
|
||||||
|
import com.olexyn.misp.mock.exchange.ExchangeMock;
|
||||||
|
import com.olexyn.misp.adapter.Adapter;
|
||||||
|
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps a ClientServlet so it can be debugged easily, i.e. without running Tomcat.
|
||||||
|
*/
|
||||||
|
public class AdapterMock extends Adapter {
|
||||||
|
|
||||||
|
private MockSet mockSet;
|
||||||
|
|
||||||
|
|
||||||
|
public AdapterMock(MockSet mockSet) {
|
||||||
|
super();
|
||||||
|
mockSet.adapterMock = this;
|
||||||
|
this.mockSet = mockSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send POST (Ride).
|
||||||
|
* Parse response.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected Ride doSendPostRide(Ride ride) throws IOException, InterruptedException, ServletException {
|
||||||
|
|
||||||
|
// Mock Exchange
|
||||||
|
final ExchangeMock exchange = new ExchangeMock();
|
||||||
|
exchange.request.setMethod("POST");
|
||||||
|
exchange.request.setContentType("application/json");
|
||||||
|
exchange.request.setContent(ride.json().getBytes());
|
||||||
|
|
||||||
|
synchronized (exchange) {
|
||||||
|
// Mock POST (Ride)
|
||||||
|
exchange.notify();
|
||||||
|
mockSet.bridgeMock.doPost(exchange.request, exchange.response);
|
||||||
|
exchange.wait();
|
||||||
|
exchange.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle OK (Ride)(Request)
|
||||||
|
return new Ride(exchange.response.getContentAsString());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send GET (Request) to App.
|
||||||
|
* Parse response.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected String doSendGetRequest(String request) throws IOException {
|
||||||
|
|
||||||
|
// Mock Exchange
|
||||||
|
final ExchangeMock exchange = new ExchangeMock();
|
||||||
|
|
||||||
|
exchange.request.setMethod("GET");
|
||||||
|
exchange.request.setContent(request.getBytes());
|
||||||
|
|
||||||
|
synchronized (exchange) {
|
||||||
|
// Mock GET (Request)
|
||||||
|
exchange.notify();
|
||||||
|
mockSet.appMock.doGet(exchange.request, exchange.response);
|
||||||
|
|
||||||
|
// handle OK (Data)
|
||||||
|
exchange.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
return exchange.response.getContentAsString();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send GET (Ride)(Request)(Data).
|
||||||
|
* Parse response.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void doSendGetRideRequest(Ride ride) throws IOException, InterruptedException {
|
||||||
|
|
||||||
|
// Mock Exchange
|
||||||
|
final ExchangeMock exchange = new ExchangeMock();
|
||||||
|
exchange.request.setMethod("GET");
|
||||||
|
exchange.request.setContentType("application/json");
|
||||||
|
exchange.request.setContent(ride.json().getBytes());
|
||||||
|
|
||||||
|
synchronized (exchange) {
|
||||||
|
// Mock GET (Ride)(Request)(Data)
|
||||||
|
exchange.notify();
|
||||||
|
mockSet.bridgeMock.doGet(exchange.request, exchange.response);
|
||||||
|
exchange.wait();
|
||||||
|
exchange.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
package com.olexyn.misp.mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pass the MockSet.
|
||||||
|
* Provide a Runnable.
|
||||||
|
*/
|
||||||
|
public class AdapterRunnable implements Runnable {
|
||||||
|
|
||||||
|
private MockSet mockSet;
|
||||||
|
|
||||||
|
public AdapterRunnable(MockSet mockSet){
|
||||||
|
super();
|
||||||
|
this.mockSet = mockSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
new AdapterMock(mockSet);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
package com.olexyn.misp.mock;
|
||||||
|
|
||||||
|
|
||||||
|
import com.olexyn.misp.mock.exchange.ExchangeMock;
|
||||||
|
import com.olexyn.misp.mock.exchange.RequestMock;
|
||||||
|
import com.olexyn.misp.bridge.BridgeServlet;
|
||||||
|
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class BridgeMock extends BridgeServlet {
|
||||||
|
|
||||||
|
public BridgeMock(MockSet mockSet) {
|
||||||
|
super();
|
||||||
|
mockSet.bridgeMock = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handle GET (Request)<br>
|
||||||
|
* - wait for availableRides to have an entry <br>
|
||||||
|
* - move move Ride to deliveredRides <br>
|
||||||
|
* - wait for Ride to have Data <br>
|
||||||
|
* - respond to the original request with Data
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void handleGetRequest(HttpServletRequest request, HttpServletResponse response) throws IOException, InterruptedException {
|
||||||
|
|
||||||
|
final ExchangeMock exchange;
|
||||||
|
|
||||||
|
synchronized (exchange = ((RequestMock) request).exchange) {
|
||||||
|
|
||||||
|
super.handleGetRequest(request,response);
|
||||||
|
|
||||||
|
exchange.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handle GET (Ride)(Data)
|
||||||
|
* if Ride in ForwardedRequest
|
||||||
|
* remove Ride from ForwardedRequest
|
||||||
|
* add Ride to NewData
|
||||||
|
* send OK (Ride)(Data)
|
||||||
|
* remove Ride from NewData
|
||||||
|
* add Ride to ForwardedData
|
||||||
|
* send OK (EOL)
|
||||||
|
* remove Ride from ForwardedData
|
||||||
|
* add Ride to EOL
|
||||||
|
*/
|
||||||
|
protected void handleGetRideRequestData(HttpServletRequest request, HttpServletResponse response) throws IOException, InterruptedException {
|
||||||
|
|
||||||
|
final ExchangeMock exchange;
|
||||||
|
|
||||||
|
synchronized (exchange = ((RequestMock) request).exchange) {
|
||||||
|
|
||||||
|
super.handleGetRideRequestData(request,response);
|
||||||
|
|
||||||
|
exchange.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handle POST (Ride)
|
||||||
|
* - wait for Ride to be booked
|
||||||
|
* - send OK to Client
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void handlePostRide(HttpServletRequest request, HttpServletResponse response) throws IOException, InterruptedException {
|
||||||
|
|
||||||
|
final ExchangeMock exchange;
|
||||||
|
|
||||||
|
synchronized (exchange = ((RequestMock) request).exchange) {
|
||||||
|
|
||||||
|
super.handlePostRide(request,response);
|
||||||
|
|
||||||
|
exchange.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package com.olexyn.misp.mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pass the MockSet.
|
||||||
|
* Provide a Runnable.
|
||||||
|
*/
|
||||||
|
public class BridgeRunnable implements Runnable {
|
||||||
|
|
||||||
|
MockSet mockSet;
|
||||||
|
|
||||||
|
public BridgeRunnable(MockSet mockSet){
|
||||||
|
super();
|
||||||
|
this.mockSet = mockSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
new BridgeMock(mockSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
package com.olexyn.misp.mock;
|
||||||
|
|
||||||
|
import com.olexyn.misp.mock.actor.AppMock;
|
||||||
|
import com.olexyn.misp.mock.actor.UserMock;
|
||||||
|
|
||||||
|
public class Main {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static void main(String... args){
|
||||||
|
|
||||||
|
MockSet mockSet = new MockSet();
|
||||||
|
|
||||||
|
Runnable publicRunnable = new UserMock(mockSet);
|
||||||
|
Runnable bridgeRunable = new BridgeRunnable(mockSet);
|
||||||
|
Runnable adapterRunnable = new AdapterRunnable(mockSet);
|
||||||
|
//Runnable clientRunnable = new ClientRunnable(mockSet);
|
||||||
|
Runnable appRunnable = new AppMock(mockSet);
|
||||||
|
|
||||||
|
Thread userThread = new Thread(publicRunnable);
|
||||||
|
Thread bridgeThread = new Thread(bridgeRunable);
|
||||||
|
Thread adapterThread = new Thread(adapterRunnable);
|
||||||
|
//Thread clientThread = new Thread(clientRunnable);
|
||||||
|
Thread appThread = new Thread(appRunnable);
|
||||||
|
|
||||||
|
userThread.setName("userThread");
|
||||||
|
userThread.start();
|
||||||
|
bridgeThread.setName("bridgeThread");
|
||||||
|
bridgeThread.start();
|
||||||
|
adapterThread.setName("adapterThread");
|
||||||
|
adapterThread.start();
|
||||||
|
//clientThread.setName("clientThread");
|
||||||
|
//clientThread.start();
|
||||||
|
appThread.setName("appThread");
|
||||||
|
appThread.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
|||||||
|
package com.olexyn.misp.mock;
|
||||||
|
|
||||||
|
import com.olexyn.misp.mock.actor.AppMock;
|
||||||
|
import com.olexyn.misp.mock.actor.UserMock;
|
||||||
|
|
||||||
|
public class MockSet {
|
||||||
|
public UserMock userMock;
|
||||||
|
public BridgeMock bridgeMock;
|
||||||
|
public AdapterMock adapterMock;
|
||||||
|
public AppMock appMock;
|
||||||
|
}
|
@ -0,0 +1,57 @@
|
|||||||
|
package com.olexyn.misp.mock.actor;
|
||||||
|
|
||||||
|
import com.olexyn.misp.mock.MockSet;
|
||||||
|
import com.olexyn.misp.mock.exchange.ExchangeMock;
|
||||||
|
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic Runnable.
|
||||||
|
* Serves as basis for Actors that are not Servlets.
|
||||||
|
*/
|
||||||
|
public abstract class ActorRunnable implements Runnable {
|
||||||
|
|
||||||
|
|
||||||
|
List<ExchangeMock> exchanges = new ArrayList<>();
|
||||||
|
|
||||||
|
protected MockSet mockSet;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public ActorRunnable(MockSet mockSet){
|
||||||
|
this.mockSet = mockSet;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void processExchange(ExchangeMock exchange) throws IOException, ServletException {
|
||||||
|
|
||||||
|
|
||||||
|
if (exchange.request.getMethod().equalsIgnoreCase("GET")) {
|
||||||
|
doGet(exchange.request, exchange.response);
|
||||||
|
} else if (exchange.request.getMethod().equalsIgnoreCase("POST")) {
|
||||||
|
doPost(exchange.request, exchange.response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public abstract void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException;
|
||||||
|
|
||||||
|
public abstract void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException;
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
package com.olexyn.misp.mock.actor;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import com.olexyn.misp.mock.MockSet;
|
||||||
|
import com.olexyn.misp.mock.exchange.ExchangeMock;
|
||||||
|
import com.olexyn.misp.mock.exchange.RequestMock;
|
||||||
|
import org.apache.commons.io.IOUtils;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
|
||||||
|
public class AppMock extends ActorRunnable {
|
||||||
|
|
||||||
|
public AppMock(MockSet mockSet) {
|
||||||
|
super(mockSet);
|
||||||
|
mockSet.appMock = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
while (true) {
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
|
||||||
|
|
||||||
|
RequestMock mockRequest = (RequestMock) request;
|
||||||
|
ExchangeMock exchange = mockRequest.exchange;
|
||||||
|
|
||||||
|
synchronized (exchange) {
|
||||||
|
String parsedRequest = IOUtils.toString(request.getReader());
|
||||||
|
|
||||||
|
String dataString = "DATA-" + parsedRequest;
|
||||||
|
|
||||||
|
exchange.response.setStatus(200);
|
||||||
|
PrintWriter writer = exchange.response.getWriter();
|
||||||
|
writer.write(dataString);
|
||||||
|
writer.flush();
|
||||||
|
writer.close();
|
||||||
|
|
||||||
|
exchange.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doPost(HttpServletRequest request, HttpServletResponse response) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
package com.olexyn.misp.mock.actor;
|
||||||
|
|
||||||
|
import com.olexyn.misp.mock.MockSet;
|
||||||
|
import com.olexyn.misp.mock.exchange.ExchangeMock;
|
||||||
|
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public class UserMock extends ActorRunnable {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
final String longRequest;
|
||||||
|
|
||||||
|
int requestCount = 0;
|
||||||
|
|
||||||
|
public UserMock(MockSet mockSet){
|
||||||
|
super(mockSet);
|
||||||
|
mockSet.userMock = this;
|
||||||
|
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i=0;i<100;i++){
|
||||||
|
sb.append("foo");
|
||||||
|
}
|
||||||
|
longRequest = sb.toString();
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
while (true){
|
||||||
|
try {
|
||||||
|
sendGetRequest();
|
||||||
|
} catch (IOException | InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doPost(HttpServletRequest request, HttpServletResponse response) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # send GET (Request)
|
||||||
|
* Generated by Loop
|
||||||
|
*/
|
||||||
|
void sendGetRequest() throws IOException, InterruptedException {
|
||||||
|
|
||||||
|
// Mock Exchange
|
||||||
|
ExchangeMock exchange = new ExchangeMock();
|
||||||
|
|
||||||
|
exchange.request.setMethod("GET");
|
||||||
|
//exchange.request.setContentType("application/json");
|
||||||
|
|
||||||
|
//String requestBody = longRequest+"-"+(++requestCount);
|
||||||
|
String requestBody = "REQUEST-"+(++requestCount);
|
||||||
|
String jsonString = "{\"request\":\""+requestBody+ "\"}";
|
||||||
|
jsonString = "asdfasdfa";
|
||||||
|
exchange.request.setContent(jsonString.getBytes());
|
||||||
|
|
||||||
|
synchronized (exchange){
|
||||||
|
// Mock GET (Request)
|
||||||
|
exchange.notify();
|
||||||
|
mockSet.bridgeMock.doGet(exchange.request,exchange.response);
|
||||||
|
exchange.wait();
|
||||||
|
|
||||||
|
// handle OK (Data)
|
||||||
|
String data = exchange.response.getContentAsString();
|
||||||
|
System.out.println(data + " of "+requestBody);
|
||||||
|
exchange.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
package com.olexyn.misp.mock.exchange;
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How "exchange" mocks an exchange:<br>
|
||||||
|
* <br>
|
||||||
|
* An "exchange" corresponds to request & reply pair.<br>
|
||||||
|
* - i.e. a GET & OK pair.<br>
|
||||||
|
* Steps:<br>
|
||||||
|
* 1. new Exchange()<br>
|
||||||
|
* 2. set properties of .request<br>
|
||||||
|
* 3. "send" request by using exchange.notify(); recipient.doGet();<br>
|
||||||
|
* 4. recipient does someting with request<br>
|
||||||
|
* - - recipient sets proerties of .response<br>
|
||||||
|
* - - recipient sends reply using exchange.notify();<br>
|
||||||
|
* 5. "receive" response by using exchange.wait();
|
||||||
|
*/
|
||||||
|
public class ExchangeMock {
|
||||||
|
|
||||||
|
public static List<ExchangeMock> exchangeList = new ArrayList<>();
|
||||||
|
|
||||||
|
|
||||||
|
private static int next_id=0;
|
||||||
|
public int id;
|
||||||
|
|
||||||
|
public RequestMock request =new RequestMock(this);
|
||||||
|
public ResponseMock response = new ResponseMock(this);
|
||||||
|
|
||||||
|
public ExchangeMock(){
|
||||||
|
id = next_id++;
|
||||||
|
exchangeList.add(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,84 @@
|
|||||||
|
package com.olexyn.misp.mock.exchange;
|
||||||
|
|
||||||
|
import org.springframework.mock.web.MockHttpServletRequest;
|
||||||
|
|
||||||
|
import javax.servlet.*;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import javax.servlet.http.Part;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* - Contains reference to ExchangeMock
|
||||||
|
* - Rest is dummy code.
|
||||||
|
*/
|
||||||
|
public class RequestMock extends MockHttpServletRequest implements HttpServletRequest {
|
||||||
|
|
||||||
|
public final ExchangeMock exchange;
|
||||||
|
|
||||||
|
public RequestMock(ExchangeMock exchange){
|
||||||
|
super();
|
||||||
|
this.exchange = exchange;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean authenticate(HttpServletResponse httpServletResponse) throws IOException, ServletException {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void login(String s, String s1) throws ServletException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void logout() throws ServletException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Part> getParts() throws IOException, ServletException {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Part getPart(String s) throws IOException, ServletException {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ServletContext getServletContext() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AsyncContext startAsync() throws IllegalStateException {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAsyncStarted() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAsyncSupported() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AsyncContext getAsyncContext() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DispatcherType getDispatcherType() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,209 @@
|
|||||||
|
package com.olexyn.misp.mock.exchange;
|
||||||
|
|
||||||
|
|
||||||
|
import org.springframework.mock.web.MockHttpServletResponse;
|
||||||
|
|
||||||
|
import javax.servlet.ServletOutputStream;
|
||||||
|
import javax.servlet.http.Cookie;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* - Contains reference to ExchangeMock
|
||||||
|
* - Rest is dummy code.
|
||||||
|
*/
|
||||||
|
public class ResponseMock extends MockHttpServletResponse implements HttpServletResponse {
|
||||||
|
|
||||||
|
public ExchangeMock exchange;
|
||||||
|
|
||||||
|
public ResponseMock(ExchangeMock exchange){
|
||||||
|
super();
|
||||||
|
this.exchange = exchange;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addCookie(Cookie cookie) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean containsHeader(String s) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String encodeURL(String s) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String encodeRedirectURL(String s) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String encodeUrl(String s) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String encodeRedirectUrl(String s) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendError(int i, String s) throws IOException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendError(int i) throws IOException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendRedirect(String s) throws IOException {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setDateHeader(String s, long l) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addDateHeader(String s, long l) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHeader(String s, String s1) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addHeader(String s, String s1) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setIntHeader(String s, int i) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addIntHeader(String s, int i) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setStatus(int i) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setStatus(int i, String s) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getStatus() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHeader(String s) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List getHeaders(String s) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set getHeaderNames() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCharacterEncoding() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getContentType() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ServletOutputStream getOutputStream() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PrintWriter getWriter() throws UnsupportedEncodingException {
|
||||||
|
return super.getWriter();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setCharacterEncoding(String s) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setContentLength(int i) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setContentType(String s) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBufferSize(int i) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getBufferSize() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void flushBuffer() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void resetBuffer() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCommitted() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setLocale(Locale locale) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Locale getLocale() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 26 KiB |
@ -0,0 +1,340 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<diagram program="umlet" version="13.3">
|
||||||
|
<zoom_level>10</zoom_level>
|
||||||
|
<element>
|
||||||
|
<id>UMLClass</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>910</x>
|
||||||
|
<y>50</y>
|
||||||
|
<w>90</w>
|
||||||
|
<h>30</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>inet
|
||||||
|
lt=-</panel_attributes>
|
||||||
|
<additional_attributes/>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>UMLClass</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>680</x>
|
||||||
|
<y>50</y>
|
||||||
|
<w>100</w>
|
||||||
|
<h>30</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>webhost
|
||||||
|
lt=-
|
||||||
|
layer=-1</panel_attributes>
|
||||||
|
<additional_attributes/>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>UMLClass</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>910</x>
|
||||||
|
<y>90</y>
|
||||||
|
<w>90</w>
|
||||||
|
<h>30</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>user
|
||||||
|
bg=#90CAF9</panel_attributes>
|
||||||
|
<additional_attributes/>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>UMLClass</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>680</x>
|
||||||
|
<y>90</y>
|
||||||
|
<w>100</w>
|
||||||
|
<h>30</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>mispbridge
|
||||||
|
bg=#B39DDB
|
||||||
|
layer=-1</panel_attributes>
|
||||||
|
<additional_attributes/>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>Relation</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>410</x>
|
||||||
|
<y>140</y>
|
||||||
|
<w>300</w>
|
||||||
|
<h>50</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>lt=<<<-
|
||||||
|
POST (Ride)
|
||||||
|
Generated by Loop</panel_attributes>
|
||||||
|
<additional_attributes>280.0;20.0;10.0;20.0</additional_attributes>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>Relation</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>760</x>
|
||||||
|
<y>180</y>
|
||||||
|
<w>200</w>
|
||||||
|
<h>40</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>lt=<<<-
|
||||||
|
GET (Request)
|
||||||
|
fg=#1E88E5</panel_attributes>
|
||||||
|
<additional_attributes>10.0;20.0;180.0;20.0</additional_attributes>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>UMLClass</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>350</x>
|
||||||
|
<y>90</y>
|
||||||
|
<w>100</w>
|
||||||
|
<h>30</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>mispclient
|
||||||
|
bg=#B39DDB
|
||||||
|
layer=-1</panel_attributes>
|
||||||
|
<additional_attributes/>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>Relation</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>410</x>
|
||||||
|
<y>290</y>
|
||||||
|
<w>300</w>
|
||||||
|
<h>50</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>lt=<<<-
|
||||||
|
GET (Ride)(Request)
|
||||||
|
(Data)</panel_attributes>
|
||||||
|
<additional_attributes>280.0;20.0;10.0;20.0</additional_attributes>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>Relation</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>760</x>
|
||||||
|
<y>310</y>
|
||||||
|
<w>190</w>
|
||||||
|
<h>40</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>lt=<<<.
|
||||||
|
OK (Data)
|
||||||
|
fg=#1E88E5</panel_attributes>
|
||||||
|
<additional_attributes>170.0;20.0;10.0;20.0</additional_attributes>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>Relation</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>410</x>
|
||||||
|
<y>350</y>
|
||||||
|
<w>300</w>
|
||||||
|
<h>40</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>lt=<<<.
|
||||||
|
OK (Ride)</panel_attributes>
|
||||||
|
<additional_attributes>10.0;20.0;280.0;20.0</additional_attributes>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>UMLClass</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>150</x>
|
||||||
|
<y>90</y>
|
||||||
|
<w>80</w>
|
||||||
|
<h>30</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>app
|
||||||
|
bg=#90CAF9</panel_attributes>
|
||||||
|
<additional_attributes/>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>Relation</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>200</x>
|
||||||
|
<y>220</y>
|
||||||
|
<w>200</w>
|
||||||
|
<h>40</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>lt=<<<-
|
||||||
|
GET (Request)
|
||||||
|
fg=#1E88E5</panel_attributes>
|
||||||
|
<additional_attributes>10.0;20.0;180.0;20.0</additional_attributes>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>Relation</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>200</x>
|
||||||
|
<y>270</y>
|
||||||
|
<w>200</w>
|
||||||
|
<h>40</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>lt=<<<.
|
||||||
|
OK (Data)
|
||||||
|
fg=#1E88E5</panel_attributes>
|
||||||
|
<additional_attributes>180.0;20.0;10.0;20.0</additional_attributes>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>Relation</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>410</x>
|
||||||
|
<y>200</y>
|
||||||
|
<w>300</w>
|
||||||
|
<h>40</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>lt=<<<.
|
||||||
|
OK (Ride)(Request)</panel_attributes>
|
||||||
|
<additional_attributes>10.0;20.0;280.0;20.0</additional_attributes>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>UMLClass</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>150</x>
|
||||||
|
<y>50</y>
|
||||||
|
<w>300</w>
|
||||||
|
<h>30</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>localhost
|
||||||
|
lt=-</panel_attributes>
|
||||||
|
<additional_attributes/>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>UMLClass</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>120</x>
|
||||||
|
<y>20</y>
|
||||||
|
<w>910</w>
|
||||||
|
<h>400</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>
|
||||||
|
lt=..
|
||||||
|
layer=-10</panel_attributes>
|
||||||
|
<additional_attributes/>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>Relation</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>940</x>
|
||||||
|
<y>110</y>
|
||||||
|
<w>30</w>
|
||||||
|
<h>310</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>lt=-
|
||||||
|
fg=#1E88E5</panel_attributes>
|
||||||
|
<additional_attributes>10.0;10.0;10.0;290.0</additional_attributes>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>Relation</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>180</x>
|
||||||
|
<y>110</y>
|
||||||
|
<w>30</w>
|
||||||
|
<h>310</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>lt=-
|
||||||
|
fg=#1E88E5</panel_attributes>
|
||||||
|
<additional_attributes>10.0;10.0;10.0;290.0</additional_attributes>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>UMLClass</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>930</x>
|
||||||
|
<y>200</y>
|
||||||
|
<w>40</w>
|
||||||
|
<h>130</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>
|
||||||
|
bg=#F6F6F6
|
||||||
|
transparency=0
|
||||||
|
layer=4</panel_attributes>
|
||||||
|
<additional_attributes/>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>UMLClass</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>170</x>
|
||||||
|
<y>240</y>
|
||||||
|
<w>40</w>
|
||||||
|
<h>50</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>
|
||||||
|
bg=#F6F6F6
|
||||||
|
transparency=0
|
||||||
|
layer=4</panel_attributes>
|
||||||
|
<additional_attributes/>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>Relation</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>720</x>
|
||||||
|
<y>110</y>
|
||||||
|
<w>30</w>
|
||||||
|
<h>310</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>lt=-
|
||||||
|
fg=#5E35B1
|
||||||
|
layer=-4</panel_attributes>
|
||||||
|
<additional_attributes>10.0;10.0;10.0;290.0</additional_attributes>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>Relation</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>390</x>
|
||||||
|
<y>110</y>
|
||||||
|
<w>30</w>
|
||||||
|
<h>310</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>lt=-
|
||||||
|
fg=#5E35B1
|
||||||
|
layer=-4</panel_attributes>
|
||||||
|
<additional_attributes>10.0;10.0;10.0;290.0</additional_attributes>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>UMLClass</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>380</x>
|
||||||
|
<y>160</y>
|
||||||
|
<w>40</w>
|
||||||
|
<h>210</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>
|
||||||
|
bg=#F6F6F6
|
||||||
|
transparency=0
|
||||||
|
layer=4</panel_attributes>
|
||||||
|
<additional_attributes/>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>UMLClass</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>690</x>
|
||||||
|
<y>160</y>
|
||||||
|
<w>40</w>
|
||||||
|
<h>60</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>
|
||||||
|
bg=#F6F6F6
|
||||||
|
transparency=0
|
||||||
|
layer=4</panel_attributes>
|
||||||
|
<additional_attributes/>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>UMLClass</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>690</x>
|
||||||
|
<y>310</y>
|
||||||
|
<w>40</w>
|
||||||
|
<h>60</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>
|
||||||
|
bg=#F6F6F6
|
||||||
|
transparency=0
|
||||||
|
layer=4</panel_attributes>
|
||||||
|
<additional_attributes/>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>UMLClass</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>730</x>
|
||||||
|
<y>200</y>
|
||||||
|
<w>40</w>
|
||||||
|
<h>130</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>
|
||||||
|
bg=#F6F6F6
|
||||||
|
transparency=0
|
||||||
|
layer=4</panel_attributes>
|
||||||
|
<additional_attributes/>
|
||||||
|
</element>
|
||||||
|
</diagram>
|
@ -0,0 +1,204 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<diagram program="umlet" version="13.3">
|
||||||
|
<zoom_level>10</zoom_level>
|
||||||
|
<element>
|
||||||
|
<id>UMLClass</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>670</x>
|
||||||
|
<y>1040</y>
|
||||||
|
<w>130</w>
|
||||||
|
<h>30</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>TinyVNC</panel_attributes>
|
||||||
|
<additional_attributes/>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>UMLClass</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>670</x>
|
||||||
|
<y>1010</y>
|
||||||
|
<w>130</w>
|
||||||
|
<h>30</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>Guacamole</panel_attributes>
|
||||||
|
<additional_attributes/>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>UMLClass</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>650</x>
|
||||||
|
<y>970</y>
|
||||||
|
<w>170</w>
|
||||||
|
<h>150</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>Debian VM</panel_attributes>
|
||||||
|
<additional_attributes/>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>UMLClass</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>630</x>
|
||||||
|
<y>910</y>
|
||||||
|
<w>210</w>
|
||||||
|
<h>230</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>VirtualBox
|
||||||
|
layer=-1</panel_attributes>
|
||||||
|
<additional_attributes/>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>UMLClass</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>670</x>
|
||||||
|
<y>1070</y>
|
||||||
|
<w>130</w>
|
||||||
|
<h>30</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>JavaFX</panel_attributes>
|
||||||
|
<additional_attributes/>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>UMLClass</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>650</x>
|
||||||
|
<y>940</y>
|
||||||
|
<w>170</w>
|
||||||
|
<h>30</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>Bridged Connection</panel_attributes>
|
||||||
|
<additional_attributes/>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>UMLClass</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>610</x>
|
||||||
|
<y>880</y>
|
||||||
|
<w>250</w>
|
||||||
|
<h>280</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>Debian Host
|
||||||
|
layer=-1</panel_attributes>
|
||||||
|
<additional_attributes/>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>UMLClass</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>670</x>
|
||||||
|
<y>760</y>
|
||||||
|
<w>130</w>
|
||||||
|
<h>30</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>4G Modem</panel_attributes>
|
||||||
|
<additional_attributes/>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>UMLClass</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>910</x>
|
||||||
|
<y>610</y>
|
||||||
|
<w>100</w>
|
||||||
|
<h>30</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>URL</panel_attributes>
|
||||||
|
<additional_attributes/>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>UMLClass</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>650</x>
|
||||||
|
<y>580</y>
|
||||||
|
<w>170</w>
|
||||||
|
<h>80</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>Hosting Provider</panel_attributes>
|
||||||
|
<additional_attributes/>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>UMLClass</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>910</x>
|
||||||
|
<y>760</y>
|
||||||
|
<w>100</w>
|
||||||
|
<h>30</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>IP / Port</panel_attributes>
|
||||||
|
<additional_attributes/>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>UMLClass</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>910</x>
|
||||||
|
<y>940</y>
|
||||||
|
<w>100</w>
|
||||||
|
<h>30</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>IP / Port</panel_attributes>
|
||||||
|
<additional_attributes/>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>Relation</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>960</x>
|
||||||
|
<y>780</y>
|
||||||
|
<w>30</w>
|
||||||
|
<h>180</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>lt=<-></panel_attributes>
|
||||||
|
<additional_attributes>10.0;10.0;10.0;160.0</additional_attributes>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>UMLClass</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>670</x>
|
||||||
|
<y>610</y>
|
||||||
|
<w>130</w>
|
||||||
|
<h>30</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>WordPress</panel_attributes>
|
||||||
|
<additional_attributes/>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>Relation</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>810</x>
|
||||||
|
<y>940</y>
|
||||||
|
<w>120</w>
|
||||||
|
<h>30</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>lt=-</panel_attributes>
|
||||||
|
<additional_attributes>100.0;10.0;10.0;10.0</additional_attributes>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>Relation</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>790</x>
|
||||||
|
<y>760</y>
|
||||||
|
<w>140</w>
|
||||||
|
<h>30</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>lt=-</panel_attributes>
|
||||||
|
<additional_attributes>120.0;10.0;10.0;10.0</additional_attributes>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>Relation</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>960</x>
|
||||||
|
<y>630</y>
|
||||||
|
<w>30</w>
|
||||||
|
<h>150</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>lt=<-></panel_attributes>
|
||||||
|
<additional_attributes>10.0;10.0;10.0;130.0</additional_attributes>
|
||||||
|
</element>
|
||||||
|
<element>
|
||||||
|
<id>Relation</id>
|
||||||
|
<coordinates>
|
||||||
|
<x>790</x>
|
||||||
|
<y>610</y>
|
||||||
|
<w>140</w>
|
||||||
|
<h>30</h>
|
||||||
|
</coordinates>
|
||||||
|
<panel_attributes>lt=-</panel_attributes>
|
||||||
|
<additional_attributes>120.0;10.0;10.0;10.0</additional_attributes>
|
||||||
|
</element>
|
||||||
|
</diagram>
|
@ -0,0 +1,113 @@
|
|||||||
|
<?xml version="1.0"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
|
||||||
|
|
||||||
|
<!-- =============================================================== -->
|
||||||
|
<!-- Documentation of this file format can be found at: -->
|
||||||
|
<!-- https://www.eclipse.org/jetty/documentation/current/ -->
|
||||||
|
<!-- -->
|
||||||
|
<!-- Additional configuration files are available in $JETTY_HOME/etc -->
|
||||||
|
<!-- and can be mixed in. See start.ini file for the default -->
|
||||||
|
<!-- configuration files. -->
|
||||||
|
<!-- -->
|
||||||
|
<!-- For a description of the configuration mechanism, see the -->
|
||||||
|
<!-- output of: -->
|
||||||
|
<!-- java -jar start.jar -? -->
|
||||||
|
<!-- =============================================================== -->
|
||||||
|
|
||||||
|
<!-- =============================================================== -->
|
||||||
|
<!-- Configure a Jetty Server instance with an ID "Server" -->
|
||||||
|
<!-- Other configuration files may also configure the "Server" -->
|
||||||
|
<!-- ID, in which case they are adding configuration to the same -->
|
||||||
|
<!-- instance. If other configuration have a different ID, they -->
|
||||||
|
<!-- will create and configure another instance of Jetty. -->
|
||||||
|
<!-- Consult the javadoc of o.e.j.server.Server for all -->
|
||||||
|
<!-- configuration that may be set here. -->
|
||||||
|
<!-- =============================================================== -->
|
||||||
|
<Configure id="Server" class="org.eclipse.jetty.server.Server">
|
||||||
|
<Arg name="threadpool"><Ref refid="threadPool"/></Arg>
|
||||||
|
|
||||||
|
<Call name="addBean">
|
||||||
|
<Arg><Ref refid="byteBufferPool"/></Arg>
|
||||||
|
</Call>
|
||||||
|
|
||||||
|
<!-- =========================================================== -->
|
||||||
|
<!-- Add shared Scheduler instance -->
|
||||||
|
<!-- =========================================================== -->
|
||||||
|
<Call name="addBean">
|
||||||
|
<Arg>
|
||||||
|
<New class="org.eclipse.jetty.util.thread.ScheduledExecutorScheduler">
|
||||||
|
<Arg name="name"><Property name="jetty.scheduler.name"/></Arg>
|
||||||
|
<Arg name="daemon" type="boolean"><Property name="jetty.scheduler.daemon" default="false" /></Arg>
|
||||||
|
<Arg name="threads" type="int"><Property name="jetty.scheduler.threads" default="-1" /></Arg>
|
||||||
|
</New>
|
||||||
|
</Arg>
|
||||||
|
</Call>
|
||||||
|
|
||||||
|
<!-- =========================================================== -->
|
||||||
|
<!-- Http Configuration. -->
|
||||||
|
<!-- This is a common configuration instance used by all -->
|
||||||
|
<!-- connectors that can carry HTTP semantics (HTTP, HTTPS, etc.)-->
|
||||||
|
<!-- It configures the non wire protocol aspects of the HTTP -->
|
||||||
|
<!-- semantic. -->
|
||||||
|
<!-- -->
|
||||||
|
<!-- This configuration is only defined here and is used by -->
|
||||||
|
<!-- reference from other XML files such as jetty-http.xml, -->
|
||||||
|
<!-- jetty-https.xml and other configuration files which -->
|
||||||
|
<!-- instantiate the connectors. -->
|
||||||
|
<!-- -->
|
||||||
|
<!-- Consult the javadoc of o.e.j.server.HttpConfiguration -->
|
||||||
|
<!-- for all configuration that may be set here. -->
|
||||||
|
<!-- =========================================================== -->
|
||||||
|
<New id="httpConfig" class="org.eclipse.jetty.server.HttpConfiguration">
|
||||||
|
<Set name="secureScheme"><Property name="jetty.httpConfig.secureScheme" default="https" /></Set>
|
||||||
|
<Set name="securePort"><Property name="jetty.httpConfig.securePort" deprecated="jetty.secure.port" default="8443" /></Set>
|
||||||
|
<Set name="outputBufferSize"><Property name="jetty.httpConfig.outputBufferSize" deprecated="jetty.output.buffer.size" default="32768" /></Set>
|
||||||
|
<Set name="outputAggregationSize"><Property name="jetty.httpConfig.outputAggregationSize" deprecated="jetty.output.aggregation.size" default="8192" /></Set>
|
||||||
|
<Set name="requestHeaderSize"><Property name="jetty.httpConfig.requestHeaderSize" deprecated="jetty.request.header.size" default="8192" /></Set>
|
||||||
|
<Set name="responseHeaderSize"><Property name="jetty.httpConfig.responseHeaderSize" deprecated="jetty.response.header.size" default="8192" /></Set>
|
||||||
|
<Set name="sendServerVersion"><Property name="jetty.httpConfig.sendServerVersion" deprecated="jetty.send.server.version" default="true" /></Set>
|
||||||
|
<Set name="sendDateHeader"><Property name="jetty.httpConfig.sendDateHeader" deprecated="jetty.send.date.header" default="false" /></Set>
|
||||||
|
<Set name="headerCacheSize"><Property name="jetty.httpConfig.headerCacheSize" default="1024" /></Set>
|
||||||
|
<Set name="delayDispatchUntilContent"><Property name="jetty.httpConfig.delayDispatchUntilContent" deprecated="jetty.delayDispatchUntilContent" default="true"/></Set>
|
||||||
|
<Set name="maxErrorDispatches"><Property name="jetty.httpConfig.maxErrorDispatches" default="10"/></Set>
|
||||||
|
<Set name="blockingTimeout"><Property deprecated="jetty.httpConfig.blockingTimeout" name="jetty.httpConfig.blockingTimeout.DEPRECATED" default="-1"/></Set>
|
||||||
|
<Set name="persistentConnectionsEnabled"><Property name="jetty.httpConfig.persistentConnectionsEnabled" default="true"/></Set>
|
||||||
|
<Set name="requestCookieCompliance"><Call class="org.eclipse.jetty.http.CookieCompliance" name="valueOf"><Arg><Property name="jetty.httpConfig.requestCookieCompliance" deprecated="jetty.httpConfig.cookieCompliance" default="RFC6265"/></Arg></Call></Set>
|
||||||
|
<Set name="responseCookieCompliance"><Call class="org.eclipse.jetty.http.CookieCompliance" name="valueOf"><Arg><Property name="jetty.httpConfig.responseCookieCompliance" default="RFC6265"/></Arg></Call></Set>
|
||||||
|
<Set name="multiPartFormDataCompliance"><Call class="org.eclipse.jetty.server.MultiPartFormDataCompliance" name="valueOf"><Arg><Property name="jetty.httpConfig.multiPartFormDataCompliance" default="RFC7578"/></Arg></Call></Set>
|
||||||
|
</New>
|
||||||
|
|
||||||
|
<!-- =========================================================== -->
|
||||||
|
<!-- Set the default handler structure for the Server -->
|
||||||
|
<!-- A handler collection is used to pass received requests to -->
|
||||||
|
<!-- both the ContextHandlerCollection, which selects the next -->
|
||||||
|
<!-- handler by context path and virtual host, and the -->
|
||||||
|
<!-- DefaultHandler, which handles any requests not handled by -->
|
||||||
|
<!-- the context handlers. -->
|
||||||
|
<!-- Other handlers may be added to the "Handlers" collection, -->
|
||||||
|
<!-- for example the jetty-requestlog.xml file adds the -->
|
||||||
|
<!-- RequestLogHandler after the default handler -->
|
||||||
|
<!-- =========================================================== -->
|
||||||
|
<Set name="handler">
|
||||||
|
<New id="Handlers" class="org.eclipse.jetty.server.handler.HandlerCollection">
|
||||||
|
<Set name="handlers">
|
||||||
|
<Array type="org.eclipse.jetty.server.Handler">
|
||||||
|
<Item>
|
||||||
|
<New id="Contexts" class="org.eclipse.jetty.server.handler.ContextHandlerCollection"/>
|
||||||
|
</Item>
|
||||||
|
<Item>
|
||||||
|
<New id="DefaultHandler" class="org.eclipse.jetty.server.handler.DefaultHandler"/>
|
||||||
|
</Item>
|
||||||
|
</Array>
|
||||||
|
</Set>
|
||||||
|
</New>
|
||||||
|
</Set>
|
||||||
|
|
||||||
|
<!-- =========================================================== -->
|
||||||
|
<!-- extra server options -->
|
||||||
|
<!-- =========================================================== -->
|
||||||
|
<Set name="stopAtShutdown"><Property name="jetty.server.stopAtShutdown" default="true"/></Set>
|
||||||
|
<Set name="stopTimeout"><Property name="jetty.server.stopTimeout" default="5000"/></Set>
|
||||||
|
<Set name="dumpAfterStart"><Property name="jetty.server.dumpAfterStart" deprecated="jetty.dump.start" default="false"/></Set>
|
||||||
|
<Set name="dumpBeforeStop"><Property name="jetty.server.dumpBeforeStop" deprecated="jetty.dump.stop" default="false"/></Set>
|
||||||
|
|
||||||
|
</Configure>
|
@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
version="0.1"
|
||||||
|
file="target/test-proxy-${version}.war"
|
||||||
|
groupId="com.olexyn.test.proxy"
|
||||||
|
artifactId="test-proxy"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
mvn package
|
||||||
|
mvn install:install-file -Dfile=${file} -DgroupId=${groupId} -DartifactId=${artifactId} -Dversion=${version} -Dpackaging=war -DgeneratePom=true
|
@ -0,0 +1,56 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_0.dtd">
|
||||||
|
<Configure id="server" class="org.eclipse.jetty.server.Server">
|
||||||
|
<New id="ServletContextHandler" class="org.eclipse.jetty.servlet.ServletContextHandler">
|
||||||
|
<Set name="servletHandler">
|
||||||
|
<New id="servletHandler" class="org.eclipse.jetty.servlet.ServletHandler">
|
||||||
|
<Call id="proxyHolder" name="addServletWithMapping">
|
||||||
|
<Arg>org.eclipse.jetty.servlets.ProxyServlet$Transparent</Arg>
|
||||||
|
<Arg>/proxy/*</Arg>
|
||||||
|
<Call name="setInitParameter">
|
||||||
|
<Arg>maxThreads</Arg>
|
||||||
|
<Arg>
|
||||||
|
<Property name="jetty.proxy.maxThreads" default="128"/>
|
||||||
|
</Arg>
|
||||||
|
</Call>
|
||||||
|
<Call name="setInitParameter">
|
||||||
|
<Arg>maxConnections</Arg>
|
||||||
|
<Arg>
|
||||||
|
<Property name="jetty.proxy.maxConnections" default="256"/>
|
||||||
|
</Arg>
|
||||||
|
</Call>
|
||||||
|
<Call name="setInitParameter">
|
||||||
|
<Arg>idleTimeout</Arg>
|
||||||
|
<Arg>
|
||||||
|
<Property name="jetty.proxy.idleTimeout" default="30000"/>
|
||||||
|
</Arg>
|
||||||
|
</Call>
|
||||||
|
<Call name="setInitParameter">
|
||||||
|
<Arg>timeout</Arg>
|
||||||
|
<Arg>
|
||||||
|
<Property name="jetty.proxy.timeout" default="60000"/>
|
||||||
|
</Arg>
|
||||||
|
</Call>
|
||||||
|
<Call name="setInitParameter">
|
||||||
|
<Arg>ProxyTo</Arg>
|
||||||
|
<Arg>http://localhost:8080/misp-mirror</Arg>
|
||||||
|
</Call>
|
||||||
|
<Call name="setInitParameter">
|
||||||
|
<Arg>Prefix</Arg>
|
||||||
|
<Arg>/proxy</Arg>
|
||||||
|
</Call>
|
||||||
|
</Call>
|
||||||
|
</New>
|
||||||
|
</Set>
|
||||||
|
</New>
|
||||||
|
<Set name="handler">
|
||||||
|
<New class="org.eclipse.jetty.server.handler.HandlerList">
|
||||||
|
<Set name="handlers">
|
||||||
|
<Array type="org.eclipse.jetty.server.Handler">
|
||||||
|
<Item>
|
||||||
|
<Ref id="ServletContextHandler"></Ref>
|
||||||
|
</Item>
|
||||||
|
</Array>
|
||||||
|
</Set>
|
||||||
|
</New>
|
||||||
|
</Set>
|
||||||
|
</Configure>
|
@ -1,36 +0,0 @@
|
|||||||
//
|
|
||||||
// ========================================================================
|
|
||||||
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
|
|
||||||
//
|
|
||||||
// This program and the accompanying materials are made available under
|
|
||||||
// the terms of the Eclipse Public License 2.0 which is available at
|
|
||||||
// https://www.eclipse.org/legal/epl-2.0
|
|
||||||
//
|
|
||||||
// This Source Code may also be made available under the following
|
|
||||||
// Secondary Licenses when the conditions for such availability set
|
|
||||||
// forth in the Eclipse Public License, v. 2.0 are satisfied:
|
|
||||||
// the Apache License v2.0 which is available at
|
|
||||||
// https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
|
||||||
// ========================================================================
|
|
||||||
//
|
|
||||||
|
|
||||||
// This module is a mixed bag of things.
|
|
||||||
// There are some utility classes that only depend on Servlet APIs,
|
|
||||||
// but other utility classes that depend on some Jetty module.
|
|
||||||
module org.eclipse.jetty.servlets
|
|
||||||
{
|
|
||||||
exports org.eclipse.jetty.servlets;
|
|
||||||
|
|
||||||
requires transitive jetty.servlet.api;
|
|
||||||
requires org.slf4j;
|
|
||||||
|
|
||||||
// Only required if using CloseableDoSFilter.
|
|
||||||
requires static org.eclipse.jetty.io;
|
|
||||||
// Only required if using DoSFilter, PushCacheFilter, etc.
|
|
||||||
requires static org.eclipse.jetty.http;
|
|
||||||
requires static org.eclipse.jetty.server;
|
|
||||||
// Only required if using CrossOriginFilter, DoSFilter, etc.
|
|
||||||
requires static org.eclipse.jetty.util;
|
|
||||||
}
|
|
@ -1,572 +0,0 @@
|
|||||||
//
|
|
||||||
// ========================================================================
|
|
||||||
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
|
|
||||||
//
|
|
||||||
// This program and the accompanying materials are made available under
|
|
||||||
// the terms of the Eclipse Public License 2.0 which is available at
|
|
||||||
// https://www.eclipse.org/legal/epl-2.0
|
|
||||||
//
|
|
||||||
// This Source Code may also be made available under the following
|
|
||||||
// Secondary Licenses when the conditions for such availability set
|
|
||||||
// forth in the Eclipse Public License, v. 2.0 are satisfied:
|
|
||||||
// the Apache License v2.0 which is available at
|
|
||||||
// https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
|
||||||
// ========================================================================
|
|
||||||
//
|
|
||||||
|
|
||||||
package servlets;
|
|
||||||
|
|
||||||
import org.eclipse.jetty.http.HttpMethod;
|
|
||||||
import org.eclipse.jetty.util.IO;
|
|
||||||
import org.eclipse.jetty.util.MultiMap;
|
|
||||||
import org.eclipse.jetty.util.StringUtil;
|
|
||||||
import org.eclipse.jetty.util.UrlEncoded;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import javax.servlet.AsyncContext;
|
|
||||||
import javax.servlet.ServletException;
|
|
||||||
import javax.servlet.http.HttpServlet;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import java.io.*;
|
|
||||||
import java.nio.charset.Charset;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.Enumeration;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CGI Servlet.
|
|
||||||
* <p>
|
|
||||||
* The following init parameters are used to configure this servlet:
|
|
||||||
* <dl>
|
|
||||||
* <dt>cgibinResourceBase</dt>
|
|
||||||
* <dd>Path to the cgi bin directory if set or it will default to the resource base of the context.</dd>
|
|
||||||
* <dt>resourceBase</dt>
|
|
||||||
* <dd>An alias for cgibinResourceBase.</dd>
|
|
||||||
* <dt>cgibinResourceBaseIsRelative</dt>
|
|
||||||
* <dd>If true then cgibinResourceBase is relative to the webapp (eg "WEB-INF/cgi")</dd>
|
|
||||||
* <dt>commandPrefix</dt>
|
|
||||||
* <dd>may be used to set a prefix to all commands passed to exec. This can be used on systems that need assistance to execute a
|
|
||||||
* particular file type. For example on windows this can be set to "perl" so that perl scripts are executed.</dd>
|
|
||||||
* <dt>Path</dt>
|
|
||||||
* <dd>passed to the exec environment as PATH.</dd>
|
|
||||||
* <dt>ENV_*</dt>
|
|
||||||
* <dd>used to set an arbitrary environment variable with the name stripped of the leading ENV_ and using the init parameter value</dd>
|
|
||||||
* <dt>useFullPath</dt>
|
|
||||||
* <dd>If true, the full URI path within the context is used for the exec command, otherwise a search is done for a partial URL that matches an exec Command</dd>
|
|
||||||
* <dt>ignoreExitState</dt>
|
|
||||||
* <dd>If true then do not act on a non-zero exec exit status")</dd>
|
|
||||||
* </dl>
|
|
||||||
*/
|
|
||||||
public class CGI extends HttpServlet
|
|
||||||
{
|
|
||||||
private static final long serialVersionUID = -6182088932884791074L;
|
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(CGI.class);
|
|
||||||
|
|
||||||
private boolean _ok;
|
|
||||||
private File _docRoot;
|
|
||||||
private boolean _cgiBinProvided;
|
|
||||||
private String _path;
|
|
||||||
private String _cmdPrefix;
|
|
||||||
private boolean _useFullPath;
|
|
||||||
private EnvList _env;
|
|
||||||
private boolean _ignoreExitState;
|
|
||||||
private boolean _relative;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init() throws ServletException
|
|
||||||
{
|
|
||||||
_env = new EnvList();
|
|
||||||
_cmdPrefix = getInitParameter("commandPrefix");
|
|
||||||
_useFullPath = Boolean.parseBoolean(getInitParameter("useFullPath"));
|
|
||||||
_relative = Boolean.parseBoolean(getInitParameter("cgibinResourceBaseIsRelative"));
|
|
||||||
|
|
||||||
String tmp = getInitParameter("cgibinResourceBase");
|
|
||||||
if (tmp != null)
|
|
||||||
_cgiBinProvided = true;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
tmp = getInitParameter("resourceBase");
|
|
||||||
if (tmp != null)
|
|
||||||
_cgiBinProvided = true;
|
|
||||||
else
|
|
||||||
tmp = getServletContext().getRealPath("/");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_relative && _cgiBinProvided)
|
|
||||||
{
|
|
||||||
tmp = getServletContext().getRealPath(tmp);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tmp == null)
|
|
||||||
{
|
|
||||||
LOG.warn("CGI: no CGI bin !");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
File dir = new File(tmp);
|
|
||||||
if (!dir.exists())
|
|
||||||
{
|
|
||||||
LOG.warn("CGI: CGI bin does not exist - " + dir);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!dir.canRead())
|
|
||||||
{
|
|
||||||
LOG.warn("CGI: CGI bin is not readable - " + dir);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!dir.isDirectory())
|
|
||||||
{
|
|
||||||
LOG.warn("CGI: CGI bin is not a directory - " + dir);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_docRoot = dir.getCanonicalFile();
|
|
||||||
}
|
|
||||||
catch (IOException e)
|
|
||||||
{
|
|
||||||
LOG.warn("CGI: CGI bin failed - " + dir, e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_path = getInitParameter("Path");
|
|
||||||
if (_path != null)
|
|
||||||
_env.set("PATH", _path);
|
|
||||||
|
|
||||||
_ignoreExitState = "true".equalsIgnoreCase(getInitParameter("ignoreExitState"));
|
|
||||||
Enumeration<String> e = getInitParameterNames();
|
|
||||||
while (e.hasMoreElements())
|
|
||||||
{
|
|
||||||
String n = e.nextElement();
|
|
||||||
if (n != null && n.startsWith("ENV_"))
|
|
||||||
_env.set(n.substring(4), getInitParameter(n));
|
|
||||||
}
|
|
||||||
if (!_env.envMap.containsKey("SystemRoot"))
|
|
||||||
{
|
|
||||||
String os = System.getProperty("os.name");
|
|
||||||
if (os != null && os.toLowerCase(Locale.ENGLISH).contains("windows"))
|
|
||||||
{
|
|
||||||
_env.set("SystemRoot", "C:\\WINDOWS");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ok = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException
|
|
||||||
{
|
|
||||||
if (!_ok)
|
|
||||||
{
|
|
||||||
res.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
{
|
|
||||||
LOG.debug("CGI: ContextPath : " + req.getContextPath());
|
|
||||||
LOG.debug("CGI: ServletPath : " + req.getServletPath());
|
|
||||||
LOG.debug("CGI: PathInfo : " + req.getPathInfo());
|
|
||||||
LOG.debug("CGI: _docRoot : " + _docRoot);
|
|
||||||
LOG.debug("CGI: _path : " + _path);
|
|
||||||
LOG.debug("CGI: _ignoreExitState: " + _ignoreExitState);
|
|
||||||
}
|
|
||||||
|
|
||||||
// pathInContext may actually comprises scriptName/pathInfo...We will
|
|
||||||
// walk backwards up it until we find the script - the rest must
|
|
||||||
// be the pathInfo;
|
|
||||||
String pathInContext = (_relative ? "" : StringUtil.nonNull(req.getServletPath())) + StringUtil.nonNull(req.getPathInfo());
|
|
||||||
File execCmd = new File(_docRoot, pathInContext);
|
|
||||||
String pathInfo = pathInContext;
|
|
||||||
|
|
||||||
if (!_useFullPath)
|
|
||||||
{
|
|
||||||
String path = pathInContext;
|
|
||||||
String info = "";
|
|
||||||
|
|
||||||
// Search docroot for a matching execCmd
|
|
||||||
while ((path.endsWith("/") || !execCmd.exists()) && path.length() >= 0)
|
|
||||||
{
|
|
||||||
int index = path.lastIndexOf('/');
|
|
||||||
path = path.substring(0, index);
|
|
||||||
info = pathInContext.substring(index, pathInContext.length());
|
|
||||||
execCmd = new File(_docRoot, path);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (path.length() == 0 || !execCmd.exists() || execCmd.isDirectory() || !execCmd.getCanonicalPath().equals(execCmd.getAbsolutePath()))
|
|
||||||
{
|
|
||||||
res.sendError(404);
|
|
||||||
}
|
|
||||||
|
|
||||||
pathInfo = info;
|
|
||||||
}
|
|
||||||
exec(execCmd, pathInfo, req, res);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* executes the CGI process
|
|
||||||
*
|
|
||||||
* @param command the command to execute, this command is prefixed by
|
|
||||||
* the context parameter "commandPrefix".
|
|
||||||
* @param pathInfo The PATH_INFO to process,
|
|
||||||
* see http://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html#getPathInfo%28%29. Cannot be null
|
|
||||||
* @param req the HTTP request
|
|
||||||
* @param res the HTTP response
|
|
||||||
* @throws IOException if the execution of the CGI process throws
|
|
||||||
*/
|
|
||||||
private void exec(File command, String pathInfo, HttpServletRequest req, HttpServletResponse res) throws IOException
|
|
||||||
{
|
|
||||||
assert req != null;
|
|
||||||
assert res != null;
|
|
||||||
assert pathInfo != null;
|
|
||||||
assert command != null;
|
|
||||||
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
{
|
|
||||||
LOG.debug("CGI: script is " + command);
|
|
||||||
LOG.debug("CGI: pathInfo is " + pathInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
String bodyFormEncoded = null;
|
|
||||||
if ((HttpMethod.POST.is(req.getMethod()) || HttpMethod.PUT.is(req.getMethod())) && "application/x-www-form-urlencoded".equals(req.getContentType()))
|
|
||||||
{
|
|
||||||
MultiMap<String> parameterMap = new MultiMap<>();
|
|
||||||
Enumeration<String> names = req.getParameterNames();
|
|
||||||
while (names.hasMoreElements())
|
|
||||||
{
|
|
||||||
String parameterName = names.nextElement();
|
|
||||||
parameterMap.addValues(parameterName, req.getParameterValues(parameterName));
|
|
||||||
}
|
|
||||||
|
|
||||||
String characterEncoding = req.getCharacterEncoding();
|
|
||||||
Charset charset = characterEncoding != null
|
|
||||||
? Charset.forName(characterEncoding) : StandardCharsets.UTF_8;
|
|
||||||
bodyFormEncoded = UrlEncoded.encode(parameterMap, charset, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
EnvList env = new EnvList(_env);
|
|
||||||
// these ones are from "The WWW Common Gateway Interface Version 1.1"
|
|
||||||
// look at :
|
|
||||||
// http://Web.Golux.Com/coar/cgi/draft-coar-cgi-v11-03-clean.html#6.1.1
|
|
||||||
env.set("AUTH_TYPE", req.getAuthType());
|
|
||||||
|
|
||||||
int contentLen = req.getContentLength();
|
|
||||||
if (contentLen < 0)
|
|
||||||
contentLen = 0;
|
|
||||||
if (bodyFormEncoded != null)
|
|
||||||
{
|
|
||||||
env.set("CONTENT_LENGTH", Integer.toString(bodyFormEncoded.length()));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
env.set("CONTENT_LENGTH", Integer.toString(contentLen));
|
|
||||||
}
|
|
||||||
env.set("CONTENT_TYPE", req.getContentType());
|
|
||||||
env.set("GATEWAY_INTERFACE", "CGI/1.1");
|
|
||||||
if (pathInfo.length() > 0)
|
|
||||||
{
|
|
||||||
env.set("PATH_INFO", pathInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
String pathTranslated = req.getPathTranslated();
|
|
||||||
if ((pathTranslated == null) || (pathTranslated.length() == 0))
|
|
||||||
pathTranslated = pathInfo;
|
|
||||||
env.set("PATH_TRANSLATED", pathTranslated);
|
|
||||||
env.set("QUERY_STRING", req.getQueryString());
|
|
||||||
env.set("REMOTE_ADDR", req.getRemoteAddr());
|
|
||||||
env.set("REMOTE_HOST", req.getRemoteHost());
|
|
||||||
|
|
||||||
// The identity information reported about the connection by a
|
|
||||||
// RFC 1413 [11] request to the remote agent, if
|
|
||||||
// available. Servers MAY choose not to support this feature, or
|
|
||||||
// not to request the data for efficiency reasons.
|
|
||||||
// "REMOTE_IDENT" => "NYI"
|
|
||||||
env.set("REMOTE_USER", req.getRemoteUser());
|
|
||||||
env.set("REQUEST_METHOD", req.getMethod());
|
|
||||||
|
|
||||||
String scriptPath;
|
|
||||||
String scriptName;
|
|
||||||
// use docRoot for scriptPath, too
|
|
||||||
if (_cgiBinProvided)
|
|
||||||
{
|
|
||||||
scriptPath = command.getAbsolutePath();
|
|
||||||
scriptName = scriptPath.substring(_docRoot.getAbsolutePath().length());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
String requestURI = req.getRequestURI();
|
|
||||||
scriptName = requestURI.substring(0, requestURI.length() - pathInfo.length());
|
|
||||||
scriptPath = getServletContext().getRealPath(scriptName);
|
|
||||||
}
|
|
||||||
env.set("SCRIPT_FILENAME", scriptPath);
|
|
||||||
env.set("SCRIPT_NAME", scriptName);
|
|
||||||
|
|
||||||
env.set("SERVER_NAME", req.getServerName());
|
|
||||||
env.set("SERVER_PORT", Integer.toString(req.getServerPort()));
|
|
||||||
env.set("SERVER_PROTOCOL", req.getProtocol());
|
|
||||||
env.set("SERVER_SOFTWARE", getServletContext().getServerInfo());
|
|
||||||
|
|
||||||
Enumeration<String> enm = req.getHeaderNames();
|
|
||||||
while (enm.hasMoreElements())
|
|
||||||
{
|
|
||||||
String name = enm.nextElement();
|
|
||||||
if (name.equalsIgnoreCase("Proxy"))
|
|
||||||
continue;
|
|
||||||
String value = req.getHeader(name);
|
|
||||||
env.set("HTTP_" + StringUtil.replace(name.toUpperCase(Locale.ENGLISH), '-', '_'), value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// these extra ones were from printenv on www.dev.nomura.co.uk
|
|
||||||
env.set("HTTPS", (req.isSecure() ? "ON" : "OFF"));
|
|
||||||
// "DOCUMENT_ROOT" => root + "/docs",
|
|
||||||
// "SERVER_URL" => "NYI - http://us0245",
|
|
||||||
// "TZ" => System.getProperty("user.timezone"),
|
|
||||||
|
|
||||||
// are we meant to decode args here? or does the script get them
|
|
||||||
// via PATH_INFO? if we are, they should be decoded and passed
|
|
||||||
// into exec here...
|
|
||||||
String absolutePath = command.getAbsolutePath();
|
|
||||||
String execCmd = absolutePath;
|
|
||||||
|
|
||||||
// escape the execCommand
|
|
||||||
if (execCmd.length() > 0 && execCmd.charAt(0) != '"' && execCmd.contains(" "))
|
|
||||||
execCmd = "\"" + execCmd + "\"";
|
|
||||||
|
|
||||||
if (_cmdPrefix != null)
|
|
||||||
execCmd = _cmdPrefix + " " + execCmd;
|
|
||||||
|
|
||||||
LOG.debug("Environment: " + env.getExportString());
|
|
||||||
LOG.debug("Command: " + execCmd);
|
|
||||||
|
|
||||||
final Process p = Runtime.getRuntime().exec(execCmd, env.getEnvArray(), _docRoot);
|
|
||||||
|
|
||||||
// hook processes input to browser's output (async)
|
|
||||||
if (bodyFormEncoded != null)
|
|
||||||
writeProcessInput(p, bodyFormEncoded);
|
|
||||||
else if (contentLen > 0)
|
|
||||||
writeProcessInput(p, req.getInputStream(), contentLen);
|
|
||||||
|
|
||||||
// hook processes output to browser's input (sync)
|
|
||||||
// if browser closes stream, we should detect it and kill process...
|
|
||||||
OutputStream os = null;
|
|
||||||
AsyncContext async = req.startAsync();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
async.start(new Runnable()
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void run()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
IO.copy(p.getErrorStream(), System.err);
|
|
||||||
}
|
|
||||||
catch (IOException e)
|
|
||||||
{
|
|
||||||
LOG.warn("Unable to copy error stream", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// read any headers off the top of our input stream
|
|
||||||
// NOTE: Multiline header items not supported!
|
|
||||||
String line = null;
|
|
||||||
InputStream inFromCgi = p.getInputStream();
|
|
||||||
|
|
||||||
// br=new BufferedReader(new InputStreamReader(inFromCgi));
|
|
||||||
// while ((line=br.readLine())!=null)
|
|
||||||
while ((line = getTextLineFromStream(inFromCgi)).length() > 0)
|
|
||||||
{
|
|
||||||
if (!line.startsWith("HTTP"))
|
|
||||||
{
|
|
||||||
int k = line.indexOf(':');
|
|
||||||
if (k > 0)
|
|
||||||
{
|
|
||||||
String key = line.substring(0, k).trim();
|
|
||||||
String value = line.substring(k + 1).trim();
|
|
||||||
if ("Location".equals(key))
|
|
||||||
{
|
|
||||||
res.sendRedirect(res.encodeRedirectURL(value));
|
|
||||||
}
|
|
||||||
else if ("Status".equals(key))
|
|
||||||
{
|
|
||||||
String[] token = value.split(" ");
|
|
||||||
int status = Integer.parseInt(token[0]);
|
|
||||||
res.setStatus(status);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// add remaining header items to our response header
|
|
||||||
res.addHeader(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// copy cgi content to response stream...
|
|
||||||
os = res.getOutputStream();
|
|
||||||
IO.copy(inFromCgi, os);
|
|
||||||
p.waitFor();
|
|
||||||
|
|
||||||
if (!_ignoreExitState)
|
|
||||||
{
|
|
||||||
int exitValue = p.exitValue();
|
|
||||||
if (0 != exitValue)
|
|
||||||
{
|
|
||||||
LOG.warn("Non-zero exit status (" + exitValue + ") from CGI program: " + absolutePath);
|
|
||||||
if (!res.isCommitted())
|
|
||||||
res.sendError(500, "Failed to exec CGI");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (IOException e)
|
|
||||||
{
|
|
||||||
// browser has probably closed its input stream - we
|
|
||||||
// terminate and clean up...
|
|
||||||
LOG.debug("CGI: Client closed connection!", e);
|
|
||||||
}
|
|
||||||
catch (InterruptedException ex)
|
|
||||||
{
|
|
||||||
LOG.debug("CGI: interrupted!");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
IO.close(os);
|
|
||||||
p.destroy();
|
|
||||||
// LOG.debug("CGI: terminated!");
|
|
||||||
async.complete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void writeProcessInput(final Process p, final String input)
|
|
||||||
{
|
|
||||||
new Thread(new Runnable()
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void run()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
try (Writer outToCgi = new OutputStreamWriter(p.getOutputStream()))
|
|
||||||
{
|
|
||||||
outToCgi.write(input);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (IOException e)
|
|
||||||
{
|
|
||||||
LOG.debug("Unable to write out to CGI", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void writeProcessInput(final Process p, final InputStream input, final int len)
|
|
||||||
{
|
|
||||||
if (len <= 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
new Thread(new Runnable()
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void run()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
try (OutputStream outToCgi = p.getOutputStream())
|
|
||||||
{
|
|
||||||
IO.copy(input, outToCgi, len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (IOException e)
|
|
||||||
{
|
|
||||||
LOG.debug("Unable to write out to CGI", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility method to get a line of text from the input stream.
|
|
||||||
*
|
|
||||||
* @param is the input stream
|
|
||||||
* @return the line of text
|
|
||||||
* @throws IOException if reading from the input stream throws
|
|
||||||
*/
|
|
||||||
private static String getTextLineFromStream(InputStream is) throws IOException
|
|
||||||
{
|
|
||||||
StringBuilder buffer = new StringBuilder();
|
|
||||||
int b;
|
|
||||||
|
|
||||||
while ((b = is.read()) != -1 && b != '\n')
|
|
||||||
{
|
|
||||||
buffer.append((char)b);
|
|
||||||
}
|
|
||||||
return buffer.toString().trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* private utility class that manages the Environment passed to exec.
|
|
||||||
*/
|
|
||||||
private static class EnvList
|
|
||||||
{
|
|
||||||
private Map<String, String> envMap;
|
|
||||||
|
|
||||||
EnvList()
|
|
||||||
{
|
|
||||||
envMap = new HashMap<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
EnvList(EnvList l)
|
|
||||||
{
|
|
||||||
envMap = new HashMap<>(l.envMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a name/value pair, null values will be treated as an empty String
|
|
||||||
*
|
|
||||||
* @param name the name
|
|
||||||
* @param value the value
|
|
||||||
*/
|
|
||||||
public void set(String name, String value)
|
|
||||||
{
|
|
||||||
envMap.put(name, name + "=" + StringUtil.nonNull(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get representation suitable for passing to exec.
|
|
||||||
*
|
|
||||||
* @return the env map as an array
|
|
||||||
*/
|
|
||||||
public String[] getEnvArray()
|
|
||||||
{
|
|
||||||
return envMap.values().toArray(new String[envMap.size()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getExportString()
|
|
||||||
{
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
for (String variable : getEnvArray())
|
|
||||||
{
|
|
||||||
sb.append("export \"");
|
|
||||||
sb.append(variable);
|
|
||||||
sb.append("\"; ");
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString()
|
|
||||||
{
|
|
||||||
return envMap.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
//
|
|
||||||
// ========================================================================
|
|
||||||
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
|
|
||||||
//
|
|
||||||
// This program and the accompanying materials are made available under
|
|
||||||
// the terms of the Eclipse Public License 2.0 which is available at
|
|
||||||
// https://www.eclipse.org/legal/epl-2.0
|
|
||||||
//
|
|
||||||
// This Source Code may also be made available under the following
|
|
||||||
// Secondary Licenses when the conditions for such availability set
|
|
||||||
// forth in the Eclipse Public License, v. 2.0 are satisfied:
|
|
||||||
// the Apache License v2.0 which is available at
|
|
||||||
// https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
|
||||||
// ========================================================================
|
|
||||||
//
|
|
||||||
|
|
||||||
package servlets;
|
|
||||||
|
|
||||||
import org.eclipse.jetty.server.Request;
|
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is an extension to {@link DoSFilter} that uses Jetty APIs to
|
|
||||||
* abruptly close the connection when the request times out.
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class CloseableDoSFilter extends DoSFilter
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
protected void onRequestTimeout(HttpServletRequest request, HttpServletResponse response, Thread handlingThread)
|
|
||||||
{
|
|
||||||
Request baseRequest = Request.getBaseRequest(request);
|
|
||||||
baseRequest.getHttpChannel().getEndPoint().close();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,147 +0,0 @@
|
|||||||
//
|
|
||||||
// ========================================================================
|
|
||||||
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
|
|
||||||
//
|
|
||||||
// This program and the accompanying materials are made available under
|
|
||||||
// the terms of the Eclipse Public License 2.0 which is available at
|
|
||||||
// https://www.eclipse.org/legal/epl-2.0
|
|
||||||
//
|
|
||||||
// This Source Code may also be made available under the following
|
|
||||||
// Secondary Licenses when the conditions for such availability set
|
|
||||||
// forth in the Eclipse Public License, v. 2.0 are satisfied:
|
|
||||||
// the Apache License v2.0 which is available at
|
|
||||||
// https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
|
||||||
// ========================================================================
|
|
||||||
//
|
|
||||||
|
|
||||||
package servlets;
|
|
||||||
|
|
||||||
import org.eclipse.jetty.util.URIUtil;
|
|
||||||
|
|
||||||
import javax.servlet.RequestDispatcher;
|
|
||||||
import javax.servlet.ServletContext;
|
|
||||||
import javax.servlet.ServletException;
|
|
||||||
import javax.servlet.http.HttpServlet;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>This servlet may be used to concatenate multiple resources into
|
|
||||||
* a single response.</p>
|
|
||||||
* <p>It is intended to be used to load multiple
|
|
||||||
* javascript or css files, but may be used for any content of the
|
|
||||||
* same mime type that can be meaningfully concatenated.</p>
|
|
||||||
* <p>The servlet uses {@link RequestDispatcher#include(javax.servlet.ServletRequest, javax.servlet.ServletResponse)}
|
|
||||||
* to combine the requested content, so dynamically generated content
|
|
||||||
* may be combined (Eg engine.js for DWR).</p>
|
|
||||||
* <p>The servlet uses parameter names of the query string as resource names
|
|
||||||
* relative to the context root. So these script tags:</p>
|
|
||||||
* <pre>
|
|
||||||
* <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>
|
|
||||||
* </pre>
|
|
||||||
* <p>can be replaced with the single tag (with the {@code ConcatServlet}
|
|
||||||
* mapped to {@code /concat}):</p>
|
|
||||||
* <pre>
|
|
||||||
* <script type="text/javascript" src="../concat?/js/behaviour.js&/js/ajax.js&/chat/chat.js"></script>
|
|
||||||
* </pre>
|
|
||||||
* <p>The {@link ServletContext#getMimeType(String)} method is used to determine the
|
|
||||||
* mime type of each resource. If the types of all resources do not match, then a 415
|
|
||||||
* UNSUPPORTED_MEDIA_TYPE error is returned.</p>
|
|
||||||
* <p>If the init parameter {@code development} is set to {@code true} then the servlet
|
|
||||||
* will run in development mode and the content will be concatenated on every request.</p>
|
|
||||||
* <p>Otherwise the init time of the servlet is used as the lastModifiedTime of the combined content
|
|
||||||
* and If-Modified-Since requests are handled with 304 NOT Modified responses if
|
|
||||||
* appropriate. This means that when not in development mode, the servlet must be
|
|
||||||
* restarted before changed content will be served.</p>
|
|
||||||
*/
|
|
||||||
public class ConcatServlet extends HttpServlet
|
|
||||||
{
|
|
||||||
private boolean _development;
|
|
||||||
private long _lastModified;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init() throws ServletException
|
|
||||||
{
|
|
||||||
_lastModified = System.currentTimeMillis();
|
|
||||||
_development = Boolean.parseBoolean(getInitParameter("development"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* @return The start time of the servlet unless in development mode, in which case -1 is returned.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected long getLastModified(HttpServletRequest req)
|
|
||||||
{
|
|
||||||
return _development ? -1 : _lastModified;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
|
|
||||||
{
|
|
||||||
String query = request.getQueryString();
|
|
||||||
if (query == null)
|
|
||||||
{
|
|
||||||
response.sendError(HttpServletResponse.SC_NO_CONTENT);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<RequestDispatcher> dispatchers = new ArrayList<>();
|
|
||||||
String[] parts = query.split("\\&");
|
|
||||||
String type = null;
|
|
||||||
for (String part : parts)
|
|
||||||
{
|
|
||||||
String path = URIUtil.canonicalPath(URIUtil.decodePath(part));
|
|
||||||
if (path == null)
|
|
||||||
{
|
|
||||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that the path is not protected.
|
|
||||||
if (startsWith(path, "/WEB-INF/") || startsWith(path, "/META-INF/"))
|
|
||||||
{
|
|
||||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String t = getServletContext().getMimeType(path);
|
|
||||||
if (t != null)
|
|
||||||
{
|
|
||||||
if (type == null)
|
|
||||||
{
|
|
||||||
type = t;
|
|
||||||
}
|
|
||||||
else if (!type.equals(t))
|
|
||||||
{
|
|
||||||
response.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RequestDispatcher dispatcher = getServletContext().getRequestDispatcher(path);
|
|
||||||
if (dispatcher != null)
|
|
||||||
dispatchers.add(dispatcher);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type != null)
|
|
||||||
response.setContentType(type);
|
|
||||||
|
|
||||||
for (RequestDispatcher dispatcher : dispatchers)
|
|
||||||
{
|
|
||||||
dispatcher.include(request, response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean startsWith(String path, String prefix)
|
|
||||||
{
|
|
||||||
// Case insensitive match.
|
|
||||||
return prefix.regionMatches(true, 0, path, 0, prefix.length());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,505 +0,0 @@
|
|||||||
//
|
|
||||||
// ========================================================================
|
|
||||||
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
|
|
||||||
//
|
|
||||||
// This program and the accompanying materials are made available under
|
|
||||||
// the terms of the Eclipse Public License 2.0 which is available at
|
|
||||||
// https://www.eclipse.org/legal/epl-2.0
|
|
||||||
//
|
|
||||||
// This Source Code may also be made available under the following
|
|
||||||
// Secondary Licenses when the conditions for such availability set
|
|
||||||
// forth in the Eclipse Public License, v. 2.0 are satisfied:
|
|
||||||
// the Apache License v2.0 which is available at
|
|
||||||
// https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
|
||||||
// ========================================================================
|
|
||||||
//
|
|
||||||
|
|
||||||
package servlets;
|
|
||||||
|
|
||||||
import org.eclipse.jetty.util.StringUtil;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import javax.servlet.*;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of the
|
|
||||||
* <a href="http://www.w3.org/TR/cors/">cross-origin resource sharing</a>.
|
|
||||||
* <p>
|
|
||||||
* A typical example is to use this filter to allow cross-domain
|
|
||||||
* <a href="http://cometd.org">cometd</a> communication using the standard
|
|
||||||
* long polling transport instead of the JSONP transport (that is less
|
|
||||||
* efficient and less reactive to failures).
|
|
||||||
* <p>
|
|
||||||
* This filter allows the following configuration parameters:
|
|
||||||
* <dl>
|
|
||||||
* <dt>allowedOrigins</dt>
|
|
||||||
* <dd>a comma separated list of origins that are
|
|
||||||
* allowed to access the resources. Default value is <b>*</b>, meaning all
|
|
||||||
* origins. Note that using wild cards can result in security problems
|
|
||||||
* for requests identifying hosts that do not exist.
|
|
||||||
* <p>
|
|
||||||
* If an allowed origin contains one or more * characters (for example
|
|
||||||
* http://*.domain.com), then "*" characters are converted to ".*", "."
|
|
||||||
* characters are escaped to "\." and the resulting allowed origin
|
|
||||||
* interpreted as a regular expression.
|
|
||||||
* <p>
|
|
||||||
* Allowed origins can therefore be more complex expressions such as
|
|
||||||
* https?://*.domain.[a-z]{3} that matches http or https, multiple subdomains
|
|
||||||
* and any 3 letter top-level domain (.com, .net, .org, etc.).</dd>
|
|
||||||
*
|
|
||||||
* <dt>allowedTimingOrigins</dt>
|
|
||||||
* <dd>a comma separated list of origins that are
|
|
||||||
* allowed to time the resource. Default value is the empty string, meaning
|
|
||||||
* no origins.
|
|
||||||
* <p>
|
|
||||||
* The check whether the timing header is set, will be performed only if
|
|
||||||
* the user gets general access to the resource using the <b>allowedOrigins</b>.
|
|
||||||
*
|
|
||||||
* <dt>allowedMethods</dt>
|
|
||||||
* <dd>a comma separated list of HTTP methods that
|
|
||||||
* are allowed to be used when accessing the resources. Default value is
|
|
||||||
* <b>GET,POST,HEAD</b></dd>
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* <dt>allowedHeaders</dt>
|
|
||||||
* <dd>a comma separated list of HTTP headers that
|
|
||||||
* are allowed to be specified when accessing the resources. Default value
|
|
||||||
* is <b>X-Requested-With,Content-Type,Accept,Origin</b>. If the value is a single "*",
|
|
||||||
* this means that any headers will be accepted.</dd>
|
|
||||||
*
|
|
||||||
* <dt>preflightMaxAge</dt>
|
|
||||||
* <dd>the number of seconds that preflight requests
|
|
||||||
* can be cached by the client. Default value is <b>1800</b> seconds, or 30
|
|
||||||
* minutes</dd>
|
|
||||||
*
|
|
||||||
* <dt>allowCredentials</dt>
|
|
||||||
* <dd>a boolean indicating if the resource allows
|
|
||||||
* requests with credentials. Default value is <b>true</b></dd>
|
|
||||||
*
|
|
||||||
* <dt>exposedHeaders</dt>
|
|
||||||
* <dd>a comma separated list of HTTP headers that
|
|
||||||
* are allowed to be exposed on the client. Default value is the
|
|
||||||
* <b>empty list</b></dd>
|
|
||||||
*
|
|
||||||
* <dt>chainPreflight</dt>
|
|
||||||
* <dd>if true preflight requests are chained to their
|
|
||||||
* target resource for normal handling (as an OPTION request). Otherwise the
|
|
||||||
* filter will response to the preflight. Default is <b>true</b>.</dd>
|
|
||||||
*
|
|
||||||
* </dl>
|
|
||||||
* A typical configuration could be:
|
|
||||||
* <pre>
|
|
||||||
* <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>
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
public class CrossOriginFilter implements Filter
|
|
||||||
{
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(CrossOriginFilter.class);
|
|
||||||
|
|
||||||
// Request headers
|
|
||||||
private static final String ORIGIN_HEADER = "Origin";
|
|
||||||
public static final String ACCESS_CONTROL_REQUEST_METHOD_HEADER = "Access-Control-Request-Method";
|
|
||||||
public static final String ACCESS_CONTROL_REQUEST_HEADERS_HEADER = "Access-Control-Request-Headers";
|
|
||||||
// Response headers
|
|
||||||
public static final String ACCESS_CONTROL_ALLOW_ORIGIN_HEADER = "Access-Control-Allow-Origin";
|
|
||||||
public static final String ACCESS_CONTROL_ALLOW_METHODS_HEADER = "Access-Control-Allow-Methods";
|
|
||||||
public static final String ACCESS_CONTROL_ALLOW_HEADERS_HEADER = "Access-Control-Allow-Headers";
|
|
||||||
public static final String ACCESS_CONTROL_MAX_AGE_HEADER = "Access-Control-Max-Age";
|
|
||||||
public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER = "Access-Control-Allow-Credentials";
|
|
||||||
public static final String ACCESS_CONTROL_EXPOSE_HEADERS_HEADER = "Access-Control-Expose-Headers";
|
|
||||||
public static final String TIMING_ALLOW_ORIGIN_HEADER = "Timing-Allow-Origin";
|
|
||||||
// Implementation constants
|
|
||||||
public static final String ALLOWED_ORIGINS_PARAM = "allowedOrigins";
|
|
||||||
public static final String ALLOWED_TIMING_ORIGINS_PARAM = "allowedTimingOrigins";
|
|
||||||
public static final String ALLOWED_METHODS_PARAM = "allowedMethods";
|
|
||||||
public static final String ALLOWED_HEADERS_PARAM = "allowedHeaders";
|
|
||||||
public static final String PREFLIGHT_MAX_AGE_PARAM = "preflightMaxAge";
|
|
||||||
public static final String ALLOW_CREDENTIALS_PARAM = "allowCredentials";
|
|
||||||
public static final String EXPOSED_HEADERS_PARAM = "exposedHeaders";
|
|
||||||
public static final String OLD_CHAIN_PREFLIGHT_PARAM = "forwardPreflight";
|
|
||||||
public static final String CHAIN_PREFLIGHT_PARAM = "chainPreflight";
|
|
||||||
private static final String ANY_ORIGIN = "*";
|
|
||||||
private static final String DEFAULT_ALLOWED_ORIGINS = "*";
|
|
||||||
private static final String DEFAULT_ALLOWED_TIMING_ORIGINS = "";
|
|
||||||
private static final List<String> SIMPLE_HTTP_METHODS = Arrays.asList("GET", "POST", "HEAD");
|
|
||||||
private static final List<String> DEFAULT_ALLOWED_METHODS = Arrays.asList("GET", "POST", "HEAD");
|
|
||||||
private static final List<String> DEFAULT_ALLOWED_HEADERS = Arrays.asList("X-Requested-With", "Content-Type", "Accept", "Origin");
|
|
||||||
|
|
||||||
private boolean anyOriginAllowed;
|
|
||||||
private boolean anyTimingOriginAllowed;
|
|
||||||
private boolean anyHeadersAllowed;
|
|
||||||
private Set<String> allowedOrigins = new HashSet<String>();
|
|
||||||
private List<Pattern> allowedOriginPatterns = new ArrayList<Pattern>();
|
|
||||||
private Set<String> allowedTimingOrigins = new HashSet<String>();
|
|
||||||
private List<Pattern> allowedTimingOriginPatterns = new ArrayList<Pattern>();
|
|
||||||
private List<String> allowedMethods = new ArrayList<String>();
|
|
||||||
private List<String> allowedHeaders = new ArrayList<String>();
|
|
||||||
private List<String> exposedHeaders = new ArrayList<String>();
|
|
||||||
private int preflightMaxAge;
|
|
||||||
private boolean allowCredentials;
|
|
||||||
private boolean chainPreflight;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init(FilterConfig config) throws ServletException
|
|
||||||
{
|
|
||||||
String allowedOriginsConfig = config.getInitParameter(ALLOWED_ORIGINS_PARAM);
|
|
||||||
String allowedTimingOriginsConfig = config.getInitParameter(ALLOWED_TIMING_ORIGINS_PARAM);
|
|
||||||
|
|
||||||
anyOriginAllowed = generateAllowedOrigins(allowedOrigins, allowedOriginPatterns, allowedOriginsConfig, DEFAULT_ALLOWED_ORIGINS);
|
|
||||||
anyTimingOriginAllowed = generateAllowedOrigins(allowedTimingOrigins, allowedTimingOriginPatterns, allowedTimingOriginsConfig, DEFAULT_ALLOWED_TIMING_ORIGINS);
|
|
||||||
|
|
||||||
String allowedMethodsConfig = config.getInitParameter(ALLOWED_METHODS_PARAM);
|
|
||||||
if (allowedMethodsConfig == null)
|
|
||||||
allowedMethods.addAll(DEFAULT_ALLOWED_METHODS);
|
|
||||||
else
|
|
||||||
allowedMethods.addAll(Arrays.asList(StringUtil.csvSplit(allowedMethodsConfig)));
|
|
||||||
|
|
||||||
String allowedHeadersConfig = config.getInitParameter(ALLOWED_HEADERS_PARAM);
|
|
||||||
if (allowedHeadersConfig == null)
|
|
||||||
allowedHeaders.addAll(DEFAULT_ALLOWED_HEADERS);
|
|
||||||
else if ("*".equals(allowedHeadersConfig))
|
|
||||||
anyHeadersAllowed = true;
|
|
||||||
else
|
|
||||||
allowedHeaders.addAll(Arrays.asList(StringUtil.csvSplit(allowedHeadersConfig)));
|
|
||||||
|
|
||||||
String preflightMaxAgeConfig = config.getInitParameter(PREFLIGHT_MAX_AGE_PARAM);
|
|
||||||
if (preflightMaxAgeConfig == null)
|
|
||||||
preflightMaxAgeConfig = "1800"; // Default is 30 minutes
|
|
||||||
try
|
|
||||||
{
|
|
||||||
preflightMaxAge = Integer.parseInt(preflightMaxAgeConfig);
|
|
||||||
}
|
|
||||||
catch (NumberFormatException x)
|
|
||||||
{
|
|
||||||
LOG.info("Cross-origin filter, could not parse '{}' parameter as integer: {}", PREFLIGHT_MAX_AGE_PARAM, preflightMaxAgeConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
String allowedCredentialsConfig = config.getInitParameter(ALLOW_CREDENTIALS_PARAM);
|
|
||||||
if (allowedCredentialsConfig == null)
|
|
||||||
allowedCredentialsConfig = "true";
|
|
||||||
allowCredentials = Boolean.parseBoolean(allowedCredentialsConfig);
|
|
||||||
|
|
||||||
String exposedHeadersConfig = config.getInitParameter(EXPOSED_HEADERS_PARAM);
|
|
||||||
if (exposedHeadersConfig == null)
|
|
||||||
exposedHeadersConfig = "";
|
|
||||||
exposedHeaders.addAll(Arrays.asList(StringUtil.csvSplit(exposedHeadersConfig)));
|
|
||||||
|
|
||||||
String chainPreflightConfig = config.getInitParameter(OLD_CHAIN_PREFLIGHT_PARAM);
|
|
||||||
if (chainPreflightConfig != null)
|
|
||||||
LOG.warn("DEPRECATED CONFIGURATION: Use " + CHAIN_PREFLIGHT_PARAM + " instead of " + OLD_CHAIN_PREFLIGHT_PARAM);
|
|
||||||
else
|
|
||||||
chainPreflightConfig = config.getInitParameter(CHAIN_PREFLIGHT_PARAM);
|
|
||||||
if (chainPreflightConfig == null)
|
|
||||||
chainPreflightConfig = "true";
|
|
||||||
chainPreflight = Boolean.parseBoolean(chainPreflightConfig);
|
|
||||||
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
{
|
|
||||||
LOG.debug("Cross-origin filter configuration: " +
|
|
||||||
ALLOWED_ORIGINS_PARAM + " = " + allowedOriginsConfig + ", " +
|
|
||||||
ALLOWED_TIMING_ORIGINS_PARAM + " = " + allowedTimingOriginsConfig + ", " +
|
|
||||||
ALLOWED_METHODS_PARAM + " = " + allowedMethodsConfig + ", " +
|
|
||||||
ALLOWED_HEADERS_PARAM + " = " + allowedHeadersConfig + ", " +
|
|
||||||
PREFLIGHT_MAX_AGE_PARAM + " = " + preflightMaxAgeConfig + ", " +
|
|
||||||
ALLOW_CREDENTIALS_PARAM + " = " + allowedCredentialsConfig + "," +
|
|
||||||
EXPOSED_HEADERS_PARAM + " = " + exposedHeadersConfig + "," +
|
|
||||||
CHAIN_PREFLIGHT_PARAM + " = " + chainPreflightConfig
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean generateAllowedOrigins(Set<String> allowedOriginStore, List<Pattern> allowedOriginPatternStore, String allowedOriginsConfig, String defaultOrigin)
|
|
||||||
{
|
|
||||||
if (allowedOriginsConfig == null)
|
|
||||||
allowedOriginsConfig = defaultOrigin;
|
|
||||||
String[] allowedOrigins = StringUtil.csvSplit(allowedOriginsConfig);
|
|
||||||
for (String allowedOrigin : allowedOrigins)
|
|
||||||
{
|
|
||||||
if (allowedOrigin.length() > 0)
|
|
||||||
{
|
|
||||||
if (ANY_ORIGIN.equals(allowedOrigin))
|
|
||||||
{
|
|
||||||
allowedOriginStore.clear();
|
|
||||||
allowedOriginPatternStore.clear();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (allowedOrigin.contains("*"))
|
|
||||||
{
|
|
||||||
allowedOriginPatternStore.add(Pattern.compile(parseAllowedWildcardOriginToRegex(allowedOrigin)));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
allowedOriginStore.add(allowedOrigin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
|
|
||||||
{
|
|
||||||
handle((HttpServletRequest)request, (HttpServletResponse)response, chain);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handle(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException
|
|
||||||
{
|
|
||||||
String origin = request.getHeader(ORIGIN_HEADER);
|
|
||||||
// Is it a cross origin request ?
|
|
||||||
if (origin != null && isEnabled(request))
|
|
||||||
{
|
|
||||||
if (anyOriginAllowed || originMatches(allowedOrigins, allowedOriginPatterns, origin))
|
|
||||||
{
|
|
||||||
if (isSimpleRequest(request))
|
|
||||||
{
|
|
||||||
LOG.debug("Cross-origin request to {} is a simple cross-origin request", request.getRequestURI());
|
|
||||||
handleSimpleResponse(request, response, origin);
|
|
||||||
}
|
|
||||||
else if (isPreflightRequest(request))
|
|
||||||
{
|
|
||||||
LOG.debug("Cross-origin request to {} is a preflight cross-origin request", request.getRequestURI());
|
|
||||||
handlePreflightResponse(request, response, origin);
|
|
||||||
if (chainPreflight)
|
|
||||||
LOG.debug("Preflight cross-origin request to {} forwarded to application", request.getRequestURI());
|
|
||||||
else
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
LOG.debug("Cross-origin request to {} is a non-simple cross-origin request", request.getRequestURI());
|
|
||||||
handleSimpleResponse(request, response, origin);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (anyTimingOriginAllowed || originMatches(allowedTimingOrigins, allowedTimingOriginPatterns, origin))
|
|
||||||
{
|
|
||||||
response.setHeader(TIMING_ALLOW_ORIGIN_HEADER, origin);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
LOG.debug("Cross-origin request to " + request.getRequestURI() + " with origin " + origin + " does not match allowed timing origins " + allowedTimingOrigins);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
LOG.debug("Cross-origin request to " + request.getRequestURI() + " with origin " + origin + " does not match allowed origins " + allowedOrigins);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
chain.doFilter(request, response);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean isEnabled(HttpServletRequest request)
|
|
||||||
{
|
|
||||||
// WebSocket clients such as Chrome 5 implement a version of the WebSocket
|
|
||||||
// protocol that does not accept extra response headers on the upgrade response
|
|
||||||
for (Enumeration<String> connections = request.getHeaders("Connection"); connections.hasMoreElements(); )
|
|
||||||
{
|
|
||||||
String connection = (String)connections.nextElement();
|
|
||||||
if ("Upgrade".equalsIgnoreCase(connection))
|
|
||||||
{
|
|
||||||
for (Enumeration<String> upgrades = request.getHeaders("Upgrade"); upgrades.hasMoreElements(); )
|
|
||||||
{
|
|
||||||
String upgrade = (String)upgrades.nextElement();
|
|
||||||
if ("WebSocket".equalsIgnoreCase(upgrade))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean originMatches(Set<String> allowedOrigins, List<Pattern> allowedOriginPatterns, String originList)
|
|
||||||
{
|
|
||||||
if (originList.trim().length() == 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
String[] origins = originList.split(" ");
|
|
||||||
for (String origin : origins)
|
|
||||||
{
|
|
||||||
if (origin.trim().length() == 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (allowedOrigins.contains(origin))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
for (Pattern allowedOrigin : allowedOriginPatterns)
|
|
||||||
{
|
|
||||||
if (allowedOrigin.matcher(origin).matches())
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String parseAllowedWildcardOriginToRegex(String allowedOrigin)
|
|
||||||
{
|
|
||||||
String regex = StringUtil.replace(allowedOrigin, ".", "\\.");
|
|
||||||
return StringUtil.replace(regex, "*", ".*"); // we want to be greedy here to match multiple subdomains, thus we use .*
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isSimpleRequest(HttpServletRequest request)
|
|
||||||
{
|
|
||||||
String method = request.getMethod();
|
|
||||||
if (SIMPLE_HTTP_METHODS.contains(method))
|
|
||||||
{
|
|
||||||
// TODO: implement better detection of simple headers
|
|
||||||
// The specification says that for a request to be simple, custom request headers must be simple.
|
|
||||||
// Here for simplicity I just check if there is a Access-Control-Request-Method header,
|
|
||||||
// which is required for preflight requests
|
|
||||||
return request.getHeader(ACCESS_CONTROL_REQUEST_METHOD_HEADER) == null;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isPreflightRequest(HttpServletRequest request)
|
|
||||||
{
|
|
||||||
String method = request.getMethod();
|
|
||||||
if (!"OPTIONS".equalsIgnoreCase(method))
|
|
||||||
return false;
|
|
||||||
if (request.getHeader(ACCESS_CONTROL_REQUEST_METHOD_HEADER) == null)
|
|
||||||
return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleSimpleResponse(HttpServletRequest request, HttpServletResponse response, String origin)
|
|
||||||
{
|
|
||||||
response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, origin);
|
|
||||||
//W3C CORS spec http://www.w3.org/TR/cors/#resource-implementation
|
|
||||||
response.addHeader("Vary", ORIGIN_HEADER);
|
|
||||||
if (allowCredentials)
|
|
||||||
response.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER, "true");
|
|
||||||
if (!exposedHeaders.isEmpty())
|
|
||||||
response.setHeader(ACCESS_CONTROL_EXPOSE_HEADERS_HEADER, commify(exposedHeaders));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handlePreflightResponse(HttpServletRequest request, HttpServletResponse response, String origin)
|
|
||||||
{
|
|
||||||
boolean methodAllowed = isMethodAllowed(request);
|
|
||||||
|
|
||||||
if (!methodAllowed)
|
|
||||||
return;
|
|
||||||
List<String> headersRequested = getAccessControlRequestHeaders(request);
|
|
||||||
boolean headersAllowed = areHeadersAllowed(headersRequested);
|
|
||||||
if (!headersAllowed)
|
|
||||||
return;
|
|
||||||
response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, origin);
|
|
||||||
//W3C CORS spec http://www.w3.org/TR/cors/#resource-implementation
|
|
||||||
if (!anyOriginAllowed)
|
|
||||||
response.addHeader("Vary", ORIGIN_HEADER);
|
|
||||||
if (allowCredentials)
|
|
||||||
response.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER, "true");
|
|
||||||
if (preflightMaxAge > 0)
|
|
||||||
response.setHeader(ACCESS_CONTROL_MAX_AGE_HEADER, String.valueOf(preflightMaxAge));
|
|
||||||
response.setHeader(ACCESS_CONTROL_ALLOW_METHODS_HEADER, commify(allowedMethods));
|
|
||||||
if (anyHeadersAllowed)
|
|
||||||
response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS_HEADER, commify(headersRequested));
|
|
||||||
else
|
|
||||||
response.setHeader(ACCESS_CONTROL_ALLOW_HEADERS_HEADER, commify(allowedHeaders));
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isMethodAllowed(HttpServletRequest request)
|
|
||||||
{
|
|
||||||
String accessControlRequestMethod = request.getHeader(ACCESS_CONTROL_REQUEST_METHOD_HEADER);
|
|
||||||
LOG.debug("{} is {}", ACCESS_CONTROL_REQUEST_METHOD_HEADER, accessControlRequestMethod);
|
|
||||||
boolean result = false;
|
|
||||||
if (accessControlRequestMethod != null)
|
|
||||||
result = allowedMethods.contains(accessControlRequestMethod);
|
|
||||||
LOG.debug("Method {} is" + (result ? "" : " not") + " among allowed methods {}", accessControlRequestMethod, allowedMethods);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> getAccessControlRequestHeaders(HttpServletRequest request)
|
|
||||||
{
|
|
||||||
String accessControlRequestHeaders = request.getHeader(ACCESS_CONTROL_REQUEST_HEADERS_HEADER);
|
|
||||||
LOG.debug("{} is {}", ACCESS_CONTROL_REQUEST_HEADERS_HEADER, accessControlRequestHeaders);
|
|
||||||
if (accessControlRequestHeaders == null)
|
|
||||||
return Collections.emptyList();
|
|
||||||
|
|
||||||
List<String> requestedHeaders = new ArrayList<String>();
|
|
||||||
String[] headers = StringUtil.csvSplit(accessControlRequestHeaders);
|
|
||||||
for (String header : headers)
|
|
||||||
{
|
|
||||||
String h = header.trim();
|
|
||||||
if (h.length() > 0)
|
|
||||||
requestedHeaders.add(h);
|
|
||||||
}
|
|
||||||
return requestedHeaders;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean areHeadersAllowed(List<String> requestedHeaders)
|
|
||||||
{
|
|
||||||
if (anyHeadersAllowed)
|
|
||||||
{
|
|
||||||
LOG.debug("Any header is allowed");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean result = true;
|
|
||||||
for (String requestedHeader : requestedHeaders)
|
|
||||||
{
|
|
||||||
boolean headerAllowed = false;
|
|
||||||
for (String allowedHeader : allowedHeaders)
|
|
||||||
{
|
|
||||||
if (requestedHeader.equalsIgnoreCase(allowedHeader.trim()))
|
|
||||||
{
|
|
||||||
headerAllowed = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!headerAllowed)
|
|
||||||
{
|
|
||||||
result = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LOG.debug("Headers [{}] are" + (result ? "" : " not") + " among allowed headers {}", requestedHeaders, allowedHeaders);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String commify(List<String> strings)
|
|
||||||
{
|
|
||||||
StringBuilder builder = new StringBuilder();
|
|
||||||
for (int i = 0; i < strings.size(); ++i)
|
|
||||||
{
|
|
||||||
if (i > 0)
|
|
||||||
builder.append(",");
|
|
||||||
String string = strings.get(i);
|
|
||||||
builder.append(string);
|
|
||||||
}
|
|
||||||
return builder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void destroy()
|
|
||||||
{
|
|
||||||
anyOriginAllowed = false;
|
|
||||||
anyTimingOriginAllowed = false;
|
|
||||||
allowedOrigins.clear();
|
|
||||||
allowedOriginPatterns.clear();
|
|
||||||
allowedTimingOrigins.clear();
|
|
||||||
allowedTimingOriginPatterns.clear();
|
|
||||||
allowedMethods.clear();
|
|
||||||
allowedHeaders.clear();
|
|
||||||
preflightMaxAge = 0;
|
|
||||||
allowCredentials = false;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,313 +0,0 @@
|
|||||||
//
|
|
||||||
// ========================================================================
|
|
||||||
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
|
|
||||||
//
|
|
||||||
// This program and the accompanying materials are made available under
|
|
||||||
// the terms of the Eclipse Public License 2.0 which is available at
|
|
||||||
// https://www.eclipse.org/legal/epl-2.0
|
|
||||||
//
|
|
||||||
// This Source Code may also be made available under the following
|
|
||||||
// Secondary Licenses when the conditions for such availability set
|
|
||||||
// forth in the Eclipse Public License, v. 2.0 are satisfied:
|
|
||||||
// the Apache License v2.0 which is available at
|
|
||||||
// https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
|
||||||
// ========================================================================
|
|
||||||
//
|
|
||||||
|
|
||||||
package servlets;
|
|
||||||
|
|
||||||
import org.eclipse.jetty.server.HttpOutput;
|
|
||||||
import org.eclipse.jetty.util.ProcessorUtils;
|
|
||||||
|
|
||||||
import javax.servlet.AsyncContext;
|
|
||||||
import javax.servlet.ServletException;
|
|
||||||
import javax.servlet.ServletOutputStream;
|
|
||||||
import javax.servlet.WriteListener;
|
|
||||||
import javax.servlet.http.HttpServlet;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.RandomAccessFile;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.channels.FileChannel.MapMode;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A servlet that uses the Servlet 3.1 asynchronous IO API to server
|
|
||||||
* static content at a limited data rate.
|
|
||||||
* <p>
|
|
||||||
* Two implementations are supported: <ul>
|
|
||||||
* <li>The <code>StandardDataStream</code> impl uses only standard
|
|
||||||
* APIs, but produces more garbage due to the byte[] nature of the API.
|
|
||||||
* <li>the <code>JettyDataStream</code> impl uses a Jetty API to write a ByteBuffer
|
|
||||||
* and thus allow the efficient use of file mapped buffers without any
|
|
||||||
* temporary buffer copies (I did tell the JSR that this was a good idea to
|
|
||||||
* have in the standard!).
|
|
||||||
* </ul>
|
|
||||||
* <p>
|
|
||||||
* The data rate is controlled by setting init parameters:
|
|
||||||
* <dl>
|
|
||||||
* <dt>buffersize</dt><dd>The amount of data in bytes written per write</dd>
|
|
||||||
* <dt>pause</dt><dd>The period in ms to wait after a write before attempting another</dd>
|
|
||||||
* <dt>pool</dt><dd>The size of the thread pool used to service the writes (defaults to available processors)</dd>
|
|
||||||
* </dl>
|
|
||||||
* Thus if buffersize = 1024 and pause = 100, the data rate will be limited to 10KB per second.
|
|
||||||
*/
|
|
||||||
public class DataRateLimitedServlet extends HttpServlet
|
|
||||||
{
|
|
||||||
private static final long serialVersionUID = -4771757707068097025L;
|
|
||||||
private int buffersize = 8192;
|
|
||||||
private long pauseNS = TimeUnit.MILLISECONDS.toNanos(100);
|
|
||||||
ScheduledThreadPoolExecutor scheduler;
|
|
||||||
private final ConcurrentHashMap<String, ByteBuffer> cache = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init() throws ServletException
|
|
||||||
{
|
|
||||||
// read the init params
|
|
||||||
String tmp = getInitParameter("buffersize");
|
|
||||||
if (tmp != null)
|
|
||||||
buffersize = Integer.parseInt(tmp);
|
|
||||||
tmp = getInitParameter("pause");
|
|
||||||
if (tmp != null)
|
|
||||||
pauseNS = TimeUnit.MILLISECONDS.toNanos(Integer.parseInt(tmp));
|
|
||||||
tmp = getInitParameter("pool");
|
|
||||||
int pool = tmp == null ? ProcessorUtils.availableProcessors() : Integer.parseInt(tmp);
|
|
||||||
|
|
||||||
// Create and start a shared scheduler.
|
|
||||||
scheduler = new ScheduledThreadPoolExecutor(pool);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void destroy()
|
|
||||||
{
|
|
||||||
scheduler.shutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
|
|
||||||
{
|
|
||||||
// Get the path of the static resource to serve.
|
|
||||||
String info = request.getPathInfo();
|
|
||||||
|
|
||||||
// We don't handle directories
|
|
||||||
if (info.endsWith("/"))
|
|
||||||
{
|
|
||||||
response.sendError(503, "directories not supported");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the mime type of the response
|
|
||||||
String contentType = getServletContext().getMimeType(info);
|
|
||||||
response.setContentType(contentType == null ? "application/x-data" : contentType);
|
|
||||||
|
|
||||||
// Look for a matching file path
|
|
||||||
String path = request.getPathTranslated();
|
|
||||||
|
|
||||||
// If we have a file path and this is a jetty response, we can use the JettyStream impl
|
|
||||||
ServletOutputStream out = response.getOutputStream();
|
|
||||||
if (path != null && out instanceof HttpOutput)
|
|
||||||
{
|
|
||||||
// If the file exists
|
|
||||||
File file = new File(path);
|
|
||||||
if (file.exists() && file.canRead())
|
|
||||||
{
|
|
||||||
// Set the content length
|
|
||||||
response.setContentLengthLong(file.length());
|
|
||||||
|
|
||||||
// Look for a file mapped buffer in the cache
|
|
||||||
ByteBuffer mapped = cache.get(path);
|
|
||||||
|
|
||||||
// Handle cache miss
|
|
||||||
if (mapped == null)
|
|
||||||
{
|
|
||||||
// TODO implement LRU cache flush
|
|
||||||
try (RandomAccessFile raf = new RandomAccessFile(file, "r"))
|
|
||||||
{
|
|
||||||
ByteBuffer buf = raf.getChannel().map(MapMode.READ_ONLY, 0, raf.length());
|
|
||||||
mapped = cache.putIfAbsent(path, buf);
|
|
||||||
if (mapped == null)
|
|
||||||
mapped = buf;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// start async request handling
|
|
||||||
AsyncContext async = request.startAsync();
|
|
||||||
|
|
||||||
// Set a JettyStream as the write listener to write the content asynchronously.
|
|
||||||
out.setWriteListener(new JettyDataStream(mapped, async, out));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Jetty API was not used, so lets try the standards approach
|
|
||||||
|
|
||||||
// Can we find the content as an input stream
|
|
||||||
InputStream content = getServletContext().getResourceAsStream(info);
|
|
||||||
if (content == null)
|
|
||||||
{
|
|
||||||
response.sendError(404);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set a StandardStream as he write listener to write the content asynchronously
|
|
||||||
out.setWriteListener(new StandardDataStream(content, request.startAsync(), out));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A standard API Stream writer
|
|
||||||
*/
|
|
||||||
private final class StandardDataStream implements WriteListener, Runnable
|
|
||||||
{
|
|
||||||
private final InputStream content;
|
|
||||||
private final AsyncContext async;
|
|
||||||
private final ServletOutputStream out;
|
|
||||||
|
|
||||||
private StandardDataStream(InputStream content, AsyncContext async, ServletOutputStream out)
|
|
||||||
{
|
|
||||||
this.content = content;
|
|
||||||
this.async = async;
|
|
||||||
this.out = out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onWritePossible() throws IOException
|
|
||||||
{
|
|
||||||
// If we are able to write
|
|
||||||
if (out.isReady())
|
|
||||||
{
|
|
||||||
// Allocated a copy buffer for each write, so as to not hold while paused
|
|
||||||
// TODO put these buffers into a pool
|
|
||||||
byte[] buffer = new byte[buffersize];
|
|
||||||
|
|
||||||
// read some content into the copy buffer
|
|
||||||
int len = content.read(buffer);
|
|
||||||
|
|
||||||
// If we are at EOF
|
|
||||||
if (len < 0)
|
|
||||||
{
|
|
||||||
// complete the async lifecycle
|
|
||||||
async.complete();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// write out the copy buffer. This will be an asynchronous write
|
|
||||||
// and will always return immediately without blocking. If a subsequent
|
|
||||||
// call to out.isReady() returns false, then this onWritePossible method
|
|
||||||
// will be called back when a write is possible.
|
|
||||||
out.write(buffer, 0, len);
|
|
||||||
|
|
||||||
// Schedule a timer callback to pause writing. Because isReady() is not called,
|
|
||||||
// a onWritePossible callback is no scheduled.
|
|
||||||
scheduler.schedule(this, pauseNS, TimeUnit.NANOSECONDS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// When the pause timer wakes up, call onWritePossible. Either isReady() will return
|
|
||||||
// true and another chunk of content will be written, or it will return false and the
|
|
||||||
// onWritePossible() callback will be scheduled when a write is next possible.
|
|
||||||
onWritePossible();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
onError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(Throwable t)
|
|
||||||
{
|
|
||||||
getServletContext().log("Async Error", t);
|
|
||||||
async.complete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Jetty API DataStream
|
|
||||||
*/
|
|
||||||
private final class JettyDataStream implements WriteListener, Runnable
|
|
||||||
{
|
|
||||||
private final ByteBuffer content;
|
|
||||||
private final int limit;
|
|
||||||
private final AsyncContext async;
|
|
||||||
private final HttpOutput out;
|
|
||||||
|
|
||||||
private JettyDataStream(ByteBuffer content, AsyncContext async, ServletOutputStream out)
|
|
||||||
{
|
|
||||||
// Make a readonly copy of the passed buffer. This uses the same underlying content
|
|
||||||
// without a copy, but gives this instance its own position and limit.
|
|
||||||
this.content = content.asReadOnlyBuffer();
|
|
||||||
// remember the ultimate limit.
|
|
||||||
this.limit = this.content.limit();
|
|
||||||
this.async = async;
|
|
||||||
this.out = (HttpOutput)out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onWritePossible() throws IOException
|
|
||||||
{
|
|
||||||
// If we are able to write
|
|
||||||
if (out.isReady())
|
|
||||||
{
|
|
||||||
// Position our buffers limit to allow only buffersize bytes to be written
|
|
||||||
int l = content.position() + buffersize;
|
|
||||||
// respect the ultimate limit
|
|
||||||
if (l > limit)
|
|
||||||
l = limit;
|
|
||||||
content.limit(l);
|
|
||||||
|
|
||||||
// if all content has been written
|
|
||||||
if (!content.hasRemaining())
|
|
||||||
{
|
|
||||||
// complete the async lifecycle
|
|
||||||
async.complete();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// write our limited buffer. This will be an asynchronous write
|
|
||||||
// and will always return immediately without blocking. If a subsequent
|
|
||||||
// call to out.isReady() returns false, then this onWritePossible method
|
|
||||||
// will be called back when a write is possible.
|
|
||||||
out.write(content);
|
|
||||||
|
|
||||||
// Schedule a timer callback to pause writing. Because isReady() is not called,
|
|
||||||
// a onWritePossible callback is not scheduled.
|
|
||||||
scheduler.schedule(this, pauseNS, TimeUnit.NANOSECONDS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// When the pause timer wakes up, call onWritePossible. Either isReady() will return
|
|
||||||
// true and another chunk of content will be written, or it will return false and the
|
|
||||||
// onWritePossible() callback will be scheduled when a write is next possible.
|
|
||||||
onWritePossible();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
onError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(Throwable t)
|
|
||||||
{
|
|
||||||
getServletContext().log("Async Error", t);
|
|
||||||
async.complete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -1,108 +0,0 @@
|
|||||||
//
|
|
||||||
// ========================================================================
|
|
||||||
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
|
|
||||||
//
|
|
||||||
// This program and the accompanying materials are made available under
|
|
||||||
// the terms of the Eclipse Public License 2.0 which is available at
|
|
||||||
// https://www.eclipse.org/legal/epl-2.0
|
|
||||||
//
|
|
||||||
// This Source Code may also be made available under the following
|
|
||||||
// Secondary Licenses when the conditions for such availability set
|
|
||||||
// forth in the Eclipse Public License, v. 2.0 are satisfied:
|
|
||||||
// the Apache License v2.0 which is available at
|
|
||||||
// https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
|
||||||
// ========================================================================
|
|
||||||
//
|
|
||||||
|
|
||||||
package servlets;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>{@link EventSource} is the passive half of an event source connection, as defined by the
|
|
||||||
* <a href="http://www.w3.org/TR/eventsource/">EventSource Specification</a>.</p>
|
|
||||||
* <p>{@link EventSource.Emitter} is the active half of the connection and allows to operate on the connection.</p>
|
|
||||||
* <p>{@link EventSource} allows applications to be notified of events happening on the connection;
|
|
||||||
* two events are being notified: the opening of the event source connection, where method
|
|
||||||
* {@link EventSource#onOpen(Emitter)} is invoked, and the closing of the event source connection,
|
|
||||||
* where method {@link EventSource#onClose()} is invoked.</p>
|
|
||||||
*
|
|
||||||
* @see EventSourceServlet
|
|
||||||
*/
|
|
||||||
public interface EventSource
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* <p>Callback method invoked when an event source connection is opened.</p>
|
|
||||||
*
|
|
||||||
* @param emitter the {@link Emitter} instance that allows to operate on the connection
|
|
||||||
* @throws IOException if the implementation of the method throws such exception
|
|
||||||
*/
|
|
||||||
public void onOpen(Emitter emitter) throws IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>Callback method invoked when an event source connection is closed.</p>
|
|
||||||
*/
|
|
||||||
public void onClose();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>{@link Emitter} is the active half of an event source connection, and allows applications
|
|
||||||
* to operate on the connection by sending events, data or comments, or by closing the connection.</p>
|
|
||||||
* <p>An {@link Emitter} instance will be created for each new event source connection.</p>
|
|
||||||
* <p>{@link Emitter} instances are fully thread safe and can be used from multiple threads.</p>
|
|
||||||
*/
|
|
||||||
public interface Emitter
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* <p>Sends a named event with data to the client.</p>
|
|
||||||
* <p>When invoked as: <code>event("foo", "bar")</code>, the client will receive the lines:</p>
|
|
||||||
* <pre>
|
|
||||||
* event: foo
|
|
||||||
* data: bar
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
* @param name the event name
|
|
||||||
* @param data the data to be sent
|
|
||||||
* @throws IOException if an I/O failure occurred
|
|
||||||
* @see #data(String)
|
|
||||||
*/
|
|
||||||
public void event(String name, String data) throws IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>Sends a default event with data to the client.</p>
|
|
||||||
* <p>When invoked as: <code>data("baz")</code>, the client will receive the line:</p>
|
|
||||||
* <pre>
|
|
||||||
* data: baz
|
|
||||||
* </pre>
|
|
||||||
* <p>When invoked as: <code>data("foo\r\nbar\rbaz\nbax")</code>, the client will receive the lines:</p>
|
|
||||||
* <pre>
|
|
||||||
* data: foo
|
|
||||||
* data: bar
|
|
||||||
* data: baz
|
|
||||||
* data: bax
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
* @param data the data to be sent
|
|
||||||
* @throws IOException if an I/O failure occurred
|
|
||||||
*/
|
|
||||||
public void data(String data) throws IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>Sends a comment to the client.</p>
|
|
||||||
* <p>When invoked as: <code>comment("foo")</code>, the client will receive the line:</p>
|
|
||||||
* <pre>
|
|
||||||
* : foo
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
* @param comment the comment to send
|
|
||||||
* @throws IOException if an I/O failure occurred
|
|
||||||
*/
|
|
||||||
public void comment(String comment) throws IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>Closes this event source connection.</p>
|
|
||||||
*/
|
|
||||||
public void close();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,236 +0,0 @@
|
|||||||
//
|
|
||||||
// ========================================================================
|
|
||||||
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
|
|
||||||
//
|
|
||||||
// This program and the accompanying materials are made available under
|
|
||||||
// the terms of the Eclipse Public License 2.0 which is available at
|
|
||||||
// https://www.eclipse.org/legal/epl-2.0
|
|
||||||
//
|
|
||||||
// This Source Code may also be made available under the following
|
|
||||||
// Secondary Licenses when the conditions for such availability set
|
|
||||||
// forth in the Eclipse Public License, v. 2.0 are satisfied:
|
|
||||||
// the Apache License v2.0 which is available at
|
|
||||||
// https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
|
||||||
// ========================================================================
|
|
||||||
//
|
|
||||||
|
|
||||||
package servlets;
|
|
||||||
|
|
||||||
import javax.servlet.AsyncContext;
|
|
||||||
import javax.servlet.ServletException;
|
|
||||||
import javax.servlet.ServletOutputStream;
|
|
||||||
import javax.servlet.http.HttpServlet;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.StringReader;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.Enumeration;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.Future;
|
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>A servlet that implements the <a href="http://www.w3.org/TR/eventsource/">event source protocol</a>,
|
|
||||||
* also known as "server sent events".</p>
|
|
||||||
* <p>This servlet must be subclassed to implement abstract method {@link #newEventSource(HttpServletRequest)}
|
|
||||||
* to return an instance of {@link EventSource} that allows application to listen for event source events
|
|
||||||
* and to emit event source events.</p>
|
|
||||||
* <p>This servlet supports the following configuration parameters:</p>
|
|
||||||
* <ul>
|
|
||||||
* <li><code>heartBeatPeriod</code>, that specifies the heartbeat period, in seconds, used to check
|
|
||||||
* whether the connection has been closed by the client; defaults to 10 seconds.</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
|
||||||
* <p>NOTE: there is currently no support for <code>last-event-id</code>.</p>
|
|
||||||
*/
|
|
||||||
public abstract class EventSourceServlet extends HttpServlet
|
|
||||||
{
|
|
||||||
private static final byte[] CRLF = new byte[]{'\r', '\n'};
|
|
||||||
private static final byte[] EVENT_FIELD = "event: ".getBytes(StandardCharsets.UTF_8);
|
|
||||||
private static final byte[] DATA_FIELD = "data: ".getBytes(StandardCharsets.UTF_8);
|
|
||||||
private static final byte[] COMMENT_FIELD = ": ".getBytes(StandardCharsets.UTF_8);
|
|
||||||
|
|
||||||
private ScheduledExecutorService scheduler;
|
|
||||||
private int heartBeatPeriod = 10;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init() throws ServletException
|
|
||||||
{
|
|
||||||
String heartBeatPeriodParam = getServletConfig().getInitParameter("heartBeatPeriod");
|
|
||||||
if (heartBeatPeriodParam != null)
|
|
||||||
heartBeatPeriod = Integer.parseInt(heartBeatPeriodParam);
|
|
||||||
scheduler = Executors.newSingleThreadScheduledExecutor();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void destroy()
|
|
||||||
{
|
|
||||||
if (scheduler != null)
|
|
||||||
scheduler.shutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
|
|
||||||
{
|
|
||||||
@SuppressWarnings("unchecked") Enumeration<String> acceptValues = request.getHeaders("Accept");
|
|
||||||
while (acceptValues.hasMoreElements())
|
|
||||||
{
|
|
||||||
String accept = acceptValues.nextElement();
|
|
||||||
if (accept.equals("text/event-stream"))
|
|
||||||
{
|
|
||||||
EventSource eventSource = newEventSource(request);
|
|
||||||
if (eventSource == null)
|
|
||||||
{
|
|
||||||
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
respond(request, response);
|
|
||||||
AsyncContext async = request.startAsync();
|
|
||||||
// Infinite timeout because the continuation is never resumed,
|
|
||||||
// but only completed on close
|
|
||||||
async.setTimeout(0);
|
|
||||||
EventSourceEmitter emitter = new EventSourceEmitter(eventSource, async);
|
|
||||||
emitter.scheduleHeartBeat();
|
|
||||||
open(eventSource, emitter);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super.doGet(request, response);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract EventSource newEventSource(HttpServletRequest request);
|
|
||||||
|
|
||||||
protected void respond(HttpServletRequest request, HttpServletResponse response) throws IOException
|
|
||||||
{
|
|
||||||
response.setStatus(HttpServletResponse.SC_OK);
|
|
||||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
|
||||||
response.setContentType("text/event-stream");
|
|
||||||
// By adding this header, and not closing the connection,
|
|
||||||
// we disable HTTP chunking, and we can use write()+flush()
|
|
||||||
// to send data in the text/event-stream protocol
|
|
||||||
response.addHeader("Connection", "close");
|
|
||||||
response.flushBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void open(EventSource eventSource, EventSource.Emitter emitter) throws IOException
|
|
||||||
{
|
|
||||||
eventSource.onOpen(emitter);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected class EventSourceEmitter implements EventSource.Emitter, Runnable
|
|
||||||
{
|
|
||||||
private final EventSource eventSource;
|
|
||||||
private final AsyncContext async;
|
|
||||||
private final ServletOutputStream output;
|
|
||||||
private Future<?> heartBeat;
|
|
||||||
private boolean closed;
|
|
||||||
|
|
||||||
public EventSourceEmitter(EventSource eventSource, AsyncContext async) throws IOException
|
|
||||||
{
|
|
||||||
this.eventSource = eventSource;
|
|
||||||
this.async = async;
|
|
||||||
this.output = async.getResponse().getOutputStream();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void event(String name, String data) throws IOException
|
|
||||||
{
|
|
||||||
synchronized (this)
|
|
||||||
{
|
|
||||||
output.write(EVENT_FIELD);
|
|
||||||
output.write(name.getBytes(StandardCharsets.UTF_8));
|
|
||||||
output.write(CRLF);
|
|
||||||
data(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void data(String data) throws IOException
|
|
||||||
{
|
|
||||||
synchronized (this)
|
|
||||||
{
|
|
||||||
BufferedReader reader = new BufferedReader(new StringReader(data));
|
|
||||||
String line;
|
|
||||||
while ((line = reader.readLine()) != null)
|
|
||||||
{
|
|
||||||
output.write(DATA_FIELD);
|
|
||||||
output.write(line.getBytes(StandardCharsets.UTF_8));
|
|
||||||
output.write(CRLF);
|
|
||||||
}
|
|
||||||
output.write(CRLF);
|
|
||||||
flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void comment(String comment) throws IOException
|
|
||||||
{
|
|
||||||
synchronized (this)
|
|
||||||
{
|
|
||||||
output.write(COMMENT_FIELD);
|
|
||||||
output.write(comment.getBytes(StandardCharsets.UTF_8));
|
|
||||||
output.write(CRLF);
|
|
||||||
output.write(CRLF);
|
|
||||||
flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run()
|
|
||||||
{
|
|
||||||
// If the other peer closes the connection, the first
|
|
||||||
// flush() should generate a TCP reset that is detected
|
|
||||||
// on the second flush()
|
|
||||||
try
|
|
||||||
{
|
|
||||||
synchronized (this)
|
|
||||||
{
|
|
||||||
output.write('\r');
|
|
||||||
flush();
|
|
||||||
output.write('\n');
|
|
||||||
flush();
|
|
||||||
}
|
|
||||||
// We could write, reschedule heartbeat
|
|
||||||
scheduleHeartBeat();
|
|
||||||
}
|
|
||||||
catch (IOException x)
|
|
||||||
{
|
|
||||||
// The other peer closed the connection
|
|
||||||
close();
|
|
||||||
eventSource.onClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void flush() throws IOException
|
|
||||||
{
|
|
||||||
async.getResponse().flushBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close()
|
|
||||||
{
|
|
||||||
synchronized (this)
|
|
||||||
{
|
|
||||||
closed = true;
|
|
||||||
heartBeat.cancel(false);
|
|
||||||
}
|
|
||||||
async.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void scheduleHeartBeat()
|
|
||||||
{
|
|
||||||
synchronized (this)
|
|
||||||
{
|
|
||||||
if (!closed)
|
|
||||||
heartBeat = scheduler.schedule(this, heartBeatPeriod, TimeUnit.SECONDS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,194 +0,0 @@
|
|||||||
//
|
|
||||||
// ========================================================================
|
|
||||||
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
|
|
||||||
//
|
|
||||||
// This program and the accompanying materials are made available under
|
|
||||||
// the terms of the Eclipse Public License 2.0 which is available at
|
|
||||||
// https://www.eclipse.org/legal/epl-2.0
|
|
||||||
//
|
|
||||||
// This Source Code may also be made available under the following
|
|
||||||
// Secondary Licenses when the conditions for such availability set
|
|
||||||
// forth in the Eclipse Public License, v. 2.0 are satisfied:
|
|
||||||
// the Apache License v2.0 which is available at
|
|
||||||
// https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
|
||||||
// ========================================================================
|
|
||||||
//
|
|
||||||
|
|
||||||
package servlets;
|
|
||||||
|
|
||||||
import org.eclipse.jetty.util.StringUtil;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import javax.servlet.*;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Header Filter
|
|
||||||
* <p>
|
|
||||||
* This filter sets or adds a header to the response.
|
|
||||||
* <p>
|
|
||||||
* The {@code headerConfig} init param is a CSV of actions to perform on headers, with the following syntax: <br>
|
|
||||||
* [action] [header name]: [header value] <br>
|
|
||||||
* [action] can be one of <code>set</code>, <code>add</code>, <code>setDate</code>, or <code>addDate</code> <br>
|
|
||||||
* The date actions will add the header value in milliseconds to the current system time before setting a date header.
|
|
||||||
* <p>
|
|
||||||
* Below is an example value for <code>headerConfig</code>:<br>
|
|
||||||
*
|
|
||||||
* <pre>
|
|
||||||
* set X-Frame-Options: DENY,
|
|
||||||
* "add Cache-Control: no-cache, no-store, must-revalidate",
|
|
||||||
* setDate Expires: 31540000000,
|
|
||||||
* addDate Date: 0
|
|
||||||
* </pre>
|
|
||||||
*
|
|
||||||
* @see IncludeExcludeBasedFilter
|
|
||||||
*/
|
|
||||||
public class HeaderFilter extends IncludeExcludeBasedFilter
|
|
||||||
{
|
|
||||||
private List<ConfiguredHeader> _configuredHeaders = new ArrayList<>();
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(HeaderFilter.class);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init(FilterConfig filterConfig) throws ServletException
|
|
||||||
{
|
|
||||||
super.init(filterConfig);
|
|
||||||
String headerConfig = filterConfig.getInitParameter("headerConfig");
|
|
||||||
|
|
||||||
if (headerConfig != null)
|
|
||||||
{
|
|
||||||
String[] configs = StringUtil.csvSplit(headerConfig);
|
|
||||||
for (String config : configs)
|
|
||||||
{
|
|
||||||
_configuredHeaders.add(parseHeaderConfiguration(config));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug(this.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
|
|
||||||
{
|
|
||||||
HttpServletRequest httpRequest = (HttpServletRequest)request;
|
|
||||||
HttpServletResponse httpResponse = (HttpServletResponse)response;
|
|
||||||
|
|
||||||
if (super.shouldFilter(httpRequest, httpResponse))
|
|
||||||
{
|
|
||||||
for (ConfiguredHeader header : _configuredHeaders)
|
|
||||||
{
|
|
||||||
if (header.isDate())
|
|
||||||
{
|
|
||||||
long headerValue = System.currentTimeMillis() + header.getMsOffset();
|
|
||||||
if (header.isAdd())
|
|
||||||
{
|
|
||||||
httpResponse.addDateHeader(header.getName(), headerValue);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
httpResponse.setDateHeader(header.getName(), headerValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else // constant header value
|
|
||||||
{
|
|
||||||
if (header.isAdd())
|
|
||||||
{
|
|
||||||
httpResponse.addHeader(header.getName(), header.getValue());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
httpResponse.setHeader(header.getName(), header.getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
chain.doFilter(request, response);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString()
|
|
||||||
{
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
sb.append(super.toString()).append("\n");
|
|
||||||
sb.append("configured headers:\n");
|
|
||||||
for (ConfiguredHeader c : _configuredHeaders)
|
|
||||||
{
|
|
||||||
sb.append(c).append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private ConfiguredHeader parseHeaderConfiguration(String config)
|
|
||||||
{
|
|
||||||
String[] configTokens = config.trim().split(" ", 2);
|
|
||||||
String method = configTokens[0].trim();
|
|
||||||
String header = configTokens[1];
|
|
||||||
String[] headerTokens = header.trim().split(":", 2);
|
|
||||||
String headerName = headerTokens[0].trim();
|
|
||||||
String headerValue = headerTokens[1].trim();
|
|
||||||
ConfiguredHeader configuredHeader = new ConfiguredHeader(headerName, headerValue, method.startsWith("add"), method.endsWith("Date"));
|
|
||||||
return configuredHeader;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class ConfiguredHeader
|
|
||||||
{
|
|
||||||
private String _name;
|
|
||||||
private String _value;
|
|
||||||
private long _msOffset;
|
|
||||||
private boolean _add;
|
|
||||||
private boolean _date;
|
|
||||||
|
|
||||||
public ConfiguredHeader(String name, String value, boolean add, boolean date)
|
|
||||||
{
|
|
||||||
_name = name;
|
|
||||||
_value = value;
|
|
||||||
_add = add;
|
|
||||||
_date = date;
|
|
||||||
|
|
||||||
if (_date)
|
|
||||||
{
|
|
||||||
_msOffset = Long.parseLong(_value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName()
|
|
||||||
{
|
|
||||||
return _name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getValue()
|
|
||||||
{
|
|
||||||
return _value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isAdd()
|
|
||||||
{
|
|
||||||
return _add;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isDate()
|
|
||||||
{
|
|
||||||
return _date;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getMsOffset()
|
|
||||||
{
|
|
||||||
return _msOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString()
|
|
||||||
{
|
|
||||||
return (_add ? "add" : "set") + (_date ? "Date" : "") + " " + _name + ": " + _value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,185 +0,0 @@
|
|||||||
//
|
|
||||||
// ========================================================================
|
|
||||||
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
|
|
||||||
//
|
|
||||||
// This program and the accompanying materials are made available under
|
|
||||||
// the terms of the Eclipse Public License 2.0 which is available at
|
|
||||||
// https://www.eclipse.org/legal/epl-2.0
|
|
||||||
//
|
|
||||||
// This Source Code may also be made available under the following
|
|
||||||
// Secondary Licenses when the conditions for such availability set
|
|
||||||
// forth in the Eclipse Public License, v. 2.0 are satisfied:
|
|
||||||
// the Apache License v2.0 which is available at
|
|
||||||
// https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
|
||||||
// ========================================================================
|
|
||||||
//
|
|
||||||
|
|
||||||
package servlets;
|
|
||||||
|
|
||||||
import org.eclipse.jetty.http.MimeTypes;
|
|
||||||
import org.eclipse.jetty.http.pathmap.PathSpecSet;
|
|
||||||
import org.eclipse.jetty.util.IncludeExclude;
|
|
||||||
import org.eclipse.jetty.util.IncludeExcludeSet;
|
|
||||||
import org.eclipse.jetty.util.StringUtil;
|
|
||||||
import org.eclipse.jetty.util.URIUtil;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import javax.servlet.Filter;
|
|
||||||
import javax.servlet.FilterConfig;
|
|
||||||
import javax.servlet.ServletContext;
|
|
||||||
import javax.servlet.ServletException;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Include Exclude Based Filter
|
|
||||||
* <p>
|
|
||||||
* This is an abstract filter which helps with filtering based on include/exclude of paths, mime types, and/or http methods.
|
|
||||||
* <p>
|
|
||||||
* Use the {@link #shouldFilter(HttpServletRequest, HttpServletResponse)} method to determine if a request/response should be filtered. If mime types are used,
|
|
||||||
* it should be called after {@link javax.servlet.FilterChain#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} since the mime type may not
|
|
||||||
* be written until then.
|
|
||||||
*
|
|
||||||
* Supported init params:
|
|
||||||
* <ul>
|
|
||||||
* <li><code>includedPaths</code> - CSV of path specs to include</li>
|
|
||||||
* <li><code>excludedPaths</code> - CSV of path specs to exclude</li>
|
|
||||||
* <li><code>includedMimeTypes</code> - CSV of mime types to include</li>
|
|
||||||
* <li><code>excludedMimeTypes</code> - CSV of mime types to exclude</li>
|
|
||||||
* <li><code>includedHttpMethods</code> - CSV of http methods to include</li>
|
|
||||||
* <li><code>excludedHttpMethods</code> - CSV of http methods to exclude</li>
|
|
||||||
* </ul>
|
|
||||||
* <p>
|
|
||||||
* Path spec rules:
|
|
||||||
* <ul>
|
|
||||||
* <li>If the spec starts with <code>'^'</code> the spec is assumed to be a regex based path spec and will match with normal Java regex rules.</li>
|
|
||||||
* <li>If the spec starts with <code>'/'</code> the spec is assumed to be a Servlet url-pattern rules path spec for either an exact match or prefix based
|
|
||||||
* match.</li>
|
|
||||||
* <li>If the spec starts with <code>'*.'</code> the spec is assumed to be a Servlet url-pattern rules path spec for a suffix based match.</li>
|
|
||||||
* <li>All other syntaxes are unsupported.</li>
|
|
||||||
* </ul>
|
|
||||||
* <p>
|
|
||||||
* CSVs are parsed with {@link StringUtil#csvSplit(String)}
|
|
||||||
*
|
|
||||||
* @see PathSpecSet
|
|
||||||
* @see IncludeExcludeSet
|
|
||||||
*/
|
|
||||||
public abstract class IncludeExcludeBasedFilter implements Filter
|
|
||||||
{
|
|
||||||
private final IncludeExclude<String> _mimeTypes = new IncludeExclude<>();
|
|
||||||
private final IncludeExclude<String> _httpMethods = new IncludeExclude<>();
|
|
||||||
private final IncludeExclude<String> _paths = new IncludeExclude<>(PathSpecSet.class);
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(IncludeExcludeBasedFilter.class);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init(FilterConfig filterConfig) throws ServletException
|
|
||||||
{
|
|
||||||
final String includedPaths = filterConfig.getInitParameter("includedPaths");
|
|
||||||
final String excludedPaths = filterConfig.getInitParameter("excludedPaths");
|
|
||||||
final String includedMimeTypes = filterConfig.getInitParameter("includedMimeTypes");
|
|
||||||
final String excludedMimeTypes = filterConfig.getInitParameter("excludedMimeTypes");
|
|
||||||
final String includedHttpMethods = filterConfig.getInitParameter("includedHttpMethods");
|
|
||||||
final String excludedHttpMethods = filterConfig.getInitParameter("excludedHttpMethods");
|
|
||||||
|
|
||||||
if (includedPaths != null)
|
|
||||||
{
|
|
||||||
_paths.include(StringUtil.csvSplit(includedPaths));
|
|
||||||
}
|
|
||||||
if (excludedPaths != null)
|
|
||||||
{
|
|
||||||
_paths.exclude(StringUtil.csvSplit(excludedPaths));
|
|
||||||
}
|
|
||||||
if (includedMimeTypes != null)
|
|
||||||
{
|
|
||||||
_mimeTypes.include(StringUtil.csvSplit(includedMimeTypes));
|
|
||||||
}
|
|
||||||
if (excludedMimeTypes != null)
|
|
||||||
{
|
|
||||||
_mimeTypes.exclude(StringUtil.csvSplit(excludedMimeTypes));
|
|
||||||
}
|
|
||||||
if (includedHttpMethods != null)
|
|
||||||
{
|
|
||||||
_httpMethods.include(StringUtil.csvSplit(includedHttpMethods));
|
|
||||||
}
|
|
||||||
if (excludedHttpMethods != null)
|
|
||||||
{
|
|
||||||
_httpMethods.exclude(StringUtil.csvSplit(excludedHttpMethods));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String guessMimeType(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
|
|
||||||
{
|
|
||||||
String contentType = httpResponse.getContentType();
|
|
||||||
LOG.debug("Content Type is: {}", contentType);
|
|
||||||
|
|
||||||
String mimeType = "";
|
|
||||||
if (contentType != null)
|
|
||||||
{
|
|
||||||
mimeType = MimeTypes.getContentTypeWithoutCharset(contentType);
|
|
||||||
LOG.debug("Mime Type is: {}", mimeType);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
String requestUrl = httpRequest.getPathInfo();
|
|
||||||
mimeType = MimeTypes.getDefaultMimeByExtension(requestUrl);
|
|
||||||
|
|
||||||
if (mimeType == null)
|
|
||||||
{
|
|
||||||
mimeType = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG.debug("Guessed mime type is {}", mimeType);
|
|
||||||
}
|
|
||||||
|
|
||||||
return mimeType;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean shouldFilter(HttpServletRequest httpRequest, HttpServletResponse httpResponse)
|
|
||||||
{
|
|
||||||
String httpMethod = httpRequest.getMethod();
|
|
||||||
LOG.debug("HTTP method is: {}", httpMethod);
|
|
||||||
if (!_httpMethods.test(httpMethod))
|
|
||||||
{
|
|
||||||
LOG.debug("should not apply filter because HTTP method does not match");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String mimeType = guessMimeType(httpRequest, httpResponse);
|
|
||||||
|
|
||||||
if (!_mimeTypes.test(mimeType))
|
|
||||||
{
|
|
||||||
LOG.debug("should not apply filter because mime type does not match");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ServletContext context = httpRequest.getServletContext();
|
|
||||||
String path = context == null ? httpRequest.getRequestURI() : URIUtil.addPaths(httpRequest.getServletPath(), httpRequest.getPathInfo());
|
|
||||||
LOG.debug("Path is: {}", path);
|
|
||||||
if (!_paths.test(path))
|
|
||||||
{
|
|
||||||
LOG.debug("should not apply filter because path does not match");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void destroy()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString()
|
|
||||||
{
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
sb.append("filter configuration:\n");
|
|
||||||
sb.append("paths:\n").append(_paths).append("\n");
|
|
||||||
sb.append("mime types:\n").append(_mimeTypes).append("\n");
|
|
||||||
sb.append("http methods:\n").append(_httpMethods);
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,306 +0,0 @@
|
|||||||
//
|
|
||||||
// ========================================================================
|
|
||||||
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
|
|
||||||
//
|
|
||||||
// This program and the accompanying materials are made available under
|
|
||||||
// the terms of the Eclipse Public License 2.0 which is available at
|
|
||||||
// https://www.eclipse.org/legal/epl-2.0
|
|
||||||
//
|
|
||||||
// This Source Code may also be made available under the following
|
|
||||||
// Secondary Licenses when the conditions for such availability set
|
|
||||||
// forth in the Eclipse Public License, v. 2.0 are satisfied:
|
|
||||||
// the Apache License v2.0 which is available at
|
|
||||||
// https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
|
||||||
// ========================================================================
|
|
||||||
//
|
|
||||||
|
|
||||||
package servlets;
|
|
||||||
|
|
||||||
import org.eclipse.jetty.http.*;
|
|
||||||
import org.eclipse.jetty.util.StringUtil;
|
|
||||||
import org.eclipse.jetty.util.annotation.ManagedAttribute;
|
|
||||||
import org.eclipse.jetty.util.annotation.ManagedObject;
|
|
||||||
import org.eclipse.jetty.util.annotation.ManagedOperation;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import javax.servlet.*;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.PushBuilder;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.ConcurrentMap;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>A filter that builds a cache of secondary resources associated
|
|
||||||
* to primary resources.</p>
|
|
||||||
* <p>A typical request for a primary resource such as {@code index.html}
|
|
||||||
* is immediately followed by a number of requests for secondary resources.
|
|
||||||
* Secondary resource requests will have a {@code Referer} HTTP header
|
|
||||||
* that points to {@code index.html}, which is used to associate the secondary
|
|
||||||
* resource to the primary resource.</p>
|
|
||||||
* <p>Only secondary resources that are requested within a (small) time period
|
|
||||||
* from the request of the primary resource are associated with the primary
|
|
||||||
* resource.</p>
|
|
||||||
* <p>This allows to build a cache of secondary resources associated with
|
|
||||||
* primary resources. When a request for a primary resource arrives, associated
|
|
||||||
* secondary resources are pushed to the client, unless the request carries
|
|
||||||
* {@code If-xxx} header that hint that the client has the resources in its
|
|
||||||
* cache.</p>
|
|
||||||
* <p>If the init param useQueryInKey is set, then the query string is used as
|
|
||||||
* as part of the key to identify a resource</p>
|
|
||||||
*/
|
|
||||||
@ManagedObject("Push cache based on the HTTP 'Referer' header")
|
|
||||||
public class PushCacheFilter implements Filter
|
|
||||||
{
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(PushCacheFilter.class);
|
|
||||||
|
|
||||||
private final Set<Integer> _ports = new HashSet<>();
|
|
||||||
private final Set<String> _hosts = new HashSet<>();
|
|
||||||
private final ConcurrentMap<String, PrimaryResource> _cache = new ConcurrentHashMap<>();
|
|
||||||
private long _associatePeriod = 4000L;
|
|
||||||
private int _maxAssociations = 16;
|
|
||||||
private long _renew = System.nanoTime();
|
|
||||||
private boolean _useQueryInKey;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init(FilterConfig config) throws ServletException
|
|
||||||
{
|
|
||||||
String associatePeriod = config.getInitParameter("associatePeriod");
|
|
||||||
if (associatePeriod != null)
|
|
||||||
_associatePeriod = Long.parseLong(associatePeriod);
|
|
||||||
|
|
||||||
String maxAssociations = config.getInitParameter("maxAssociations");
|
|
||||||
if (maxAssociations != null)
|
|
||||||
_maxAssociations = Integer.parseInt(maxAssociations);
|
|
||||||
|
|
||||||
String hosts = config.getInitParameter("hosts");
|
|
||||||
if (hosts != null)
|
|
||||||
Collections.addAll(_hosts, StringUtil.csvSplit(hosts));
|
|
||||||
|
|
||||||
String ports = config.getInitParameter("ports");
|
|
||||||
if (ports != null)
|
|
||||||
for (String p : StringUtil.csvSplit(ports))
|
|
||||||
{
|
|
||||||
_ports.add(Integer.parseInt(p));
|
|
||||||
}
|
|
||||||
|
|
||||||
_useQueryInKey = Boolean.parseBoolean(config.getInitParameter("useQueryInKey"));
|
|
||||||
|
|
||||||
// Expose for JMX.
|
|
||||||
config.getServletContext().setAttribute(config.getFilterName(), this);
|
|
||||||
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("period={} max={} hosts={} ports={}", _associatePeriod, _maxAssociations, _hosts, _ports);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException
|
|
||||||
{
|
|
||||||
HttpServletRequest request = (HttpServletRequest)req;
|
|
||||||
|
|
||||||
PushBuilder pushBuilder = request.newPushBuilder();
|
|
||||||
if (HttpVersion.fromString(request.getProtocol()).getVersion() < 20 ||
|
|
||||||
!HttpMethod.GET.is(request.getMethod()) ||
|
|
||||||
pushBuilder == null)
|
|
||||||
{
|
|
||||||
chain.doFilter(req, resp);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
long now = System.nanoTime();
|
|
||||||
|
|
||||||
boolean conditional = false;
|
|
||||||
String referrer = null;
|
|
||||||
List<String> headerNames = Collections.list(request.getHeaderNames());
|
|
||||||
for (String headerName : headerNames)
|
|
||||||
{
|
|
||||||
if (HttpHeader.IF_MATCH.is(headerName) ||
|
|
||||||
HttpHeader.IF_MODIFIED_SINCE.is(headerName) ||
|
|
||||||
HttpHeader.IF_NONE_MATCH.is(headerName) ||
|
|
||||||
HttpHeader.IF_UNMODIFIED_SINCE.is(headerName))
|
|
||||||
{
|
|
||||||
conditional = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else if (HttpHeader.REFERER.is(headerName))
|
|
||||||
{
|
|
||||||
referrer = request.getHeader(headerName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("{} {} referrer={} conditional={}", request.getMethod(), request.getRequestURI(), referrer, conditional);
|
|
||||||
|
|
||||||
String path = request.getRequestURI();
|
|
||||||
String query = request.getQueryString();
|
|
||||||
if (_useQueryInKey && query != null)
|
|
||||||
path += "?" + query;
|
|
||||||
if (referrer != null)
|
|
||||||
{
|
|
||||||
HttpURI referrerURI = new HttpURI(referrer);
|
|
||||||
String host = referrerURI.getHost();
|
|
||||||
int port = referrerURI.getPort();
|
|
||||||
if (port <= 0)
|
|
||||||
{
|
|
||||||
String scheme = referrerURI.getScheme();
|
|
||||||
if (scheme != null)
|
|
||||||
port = HttpScheme.HTTPS.is(scheme) ? 443 : 80;
|
|
||||||
else
|
|
||||||
port = request.isSecure() ? 443 : 80;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean referredFromHere = !_hosts.isEmpty() ? _hosts.contains(host) : host.equals(request.getServerName());
|
|
||||||
referredFromHere &= !_ports.isEmpty() ? _ports.contains(port) : port == request.getServerPort();
|
|
||||||
|
|
||||||
if (referredFromHere)
|
|
||||||
{
|
|
||||||
if (HttpMethod.GET.is(request.getMethod()))
|
|
||||||
{
|
|
||||||
String referrerPath = _useQueryInKey ? referrerURI.getPathQuery() : referrerURI.getPath();
|
|
||||||
if (referrerPath == null)
|
|
||||||
referrerPath = "/";
|
|
||||||
if (referrerPath.startsWith(request.getContextPath() + "/"))
|
|
||||||
{
|
|
||||||
if (!referrerPath.equals(path))
|
|
||||||
{
|
|
||||||
PrimaryResource primaryResource = _cache.get(referrerPath);
|
|
||||||
if (primaryResource != null)
|
|
||||||
{
|
|
||||||
long primaryTimestamp = primaryResource._timestamp.get();
|
|
||||||
if (primaryTimestamp != 0)
|
|
||||||
{
|
|
||||||
if (now - primaryTimestamp < TimeUnit.MILLISECONDS.toNanos(_associatePeriod))
|
|
||||||
{
|
|
||||||
Set<String> associated = primaryResource._associated;
|
|
||||||
// Not strictly concurrent-safe, just best effort to limit associations.
|
|
||||||
if (associated.size() <= _maxAssociations)
|
|
||||||
{
|
|
||||||
if (associated.add(path))
|
|
||||||
{
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("Associated {} to {}", path, referrerPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("Not associated {} to {}, exceeded max associations of {}", path, referrerPath, _maxAssociations);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("Not associated {} to {}, outside associate period of {}ms", path, referrerPath, _associatePeriod);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("Not associated {} to {}, referring to self", path, referrerPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("Not associated {} to {}, different context", path, referrerPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("External referrer {}", referrer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PrimaryResource primaryResource = _cache.get(path);
|
|
||||||
if (primaryResource == null)
|
|
||||||
{
|
|
||||||
PrimaryResource r = new PrimaryResource();
|
|
||||||
primaryResource = _cache.putIfAbsent(path, r);
|
|
||||||
primaryResource = primaryResource == null ? r : primaryResource;
|
|
||||||
primaryResource._timestamp.compareAndSet(0, now);
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("Cached primary resource {}", path);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
long last = primaryResource._timestamp.get();
|
|
||||||
if (last < _renew && primaryResource._timestamp.compareAndSet(last, now))
|
|
||||||
{
|
|
||||||
primaryResource._associated.clear();
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("Clear associated resources for {}", path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push associated resources.
|
|
||||||
if (!conditional && !primaryResource._associated.isEmpty())
|
|
||||||
{
|
|
||||||
// Breadth-first push of associated resources.
|
|
||||||
Queue<PrimaryResource> queue = new ArrayDeque<>();
|
|
||||||
queue.offer(primaryResource);
|
|
||||||
while (!queue.isEmpty())
|
|
||||||
{
|
|
||||||
PrimaryResource parent = queue.poll();
|
|
||||||
for (String childPath : parent._associated)
|
|
||||||
{
|
|
||||||
PrimaryResource child = _cache.get(childPath);
|
|
||||||
if (child != null)
|
|
||||||
queue.offer(child);
|
|
||||||
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("Pushing {} for {}", childPath, path);
|
|
||||||
pushBuilder.path(childPath).push();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
chain.doFilter(request, resp);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void destroy()
|
|
||||||
{
|
|
||||||
clearPushCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
@ManagedAttribute("The push cache contents")
|
|
||||||
public Map<String, String> getPushCache()
|
|
||||||
{
|
|
||||||
Map<String, String> result = new HashMap<>();
|
|
||||||
for (Map.Entry<String, PrimaryResource> entry : _cache.entrySet())
|
|
||||||
{
|
|
||||||
PrimaryResource resource = entry.getValue();
|
|
||||||
String value = String.format("size=%d: %s", resource._associated.size(), new TreeSet<>(resource._associated));
|
|
||||||
result.put(entry.getKey(), value);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ManagedOperation(value = "Renews the push cache contents", impact = "ACTION")
|
|
||||||
public void renewPushCache()
|
|
||||||
{
|
|
||||||
_renew = System.nanoTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
@ManagedOperation(value = "Clears the push cache contents", impact = "ACTION")
|
|
||||||
public void clearPushCache()
|
|
||||||
{
|
|
||||||
_cache.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class PrimaryResource
|
|
||||||
{
|
|
||||||
private final Set<String> _associated = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
|
||||||
private final AtomicLong _timestamp = new AtomicLong();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,194 +0,0 @@
|
|||||||
//
|
|
||||||
// ========================================================================
|
|
||||||
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
|
|
||||||
//
|
|
||||||
// This program and the accompanying materials are made available under
|
|
||||||
// the terms of the Eclipse Public License 2.0 which is available at
|
|
||||||
// https://www.eclipse.org/legal/epl-2.0
|
|
||||||
//
|
|
||||||
// This Source Code may also be made available under the following
|
|
||||||
// Secondary Licenses when the conditions for such availability set
|
|
||||||
// forth in the Eclipse Public License, v. 2.0 are satisfied:
|
|
||||||
// the Apache License v2.0 which is available at
|
|
||||||
// https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
|
||||||
// ========================================================================
|
|
||||||
//
|
|
||||||
|
|
||||||
package servlets;
|
|
||||||
|
|
||||||
import org.eclipse.jetty.http.HttpHeader;
|
|
||||||
import org.eclipse.jetty.http.HttpURI;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import javax.servlet.*;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import javax.servlet.http.HttpSession;
|
|
||||||
import javax.servlet.http.PushBuilder;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayDeque;
|
|
||||||
import java.util.Queue;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.ConcurrentMap;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
public class PushSessionCacheFilter implements Filter
|
|
||||||
{
|
|
||||||
private static final String RESPONSE_ATTR = "PushSessionCacheFilter.response";
|
|
||||||
private static final String TARGET_ATTR = "PushSessionCacheFilter.target";
|
|
||||||
private static final String TIMESTAMP_ATTR = "PushSessionCacheFilter.timestamp";
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(PushSessionCacheFilter.class);
|
|
||||||
private final ConcurrentMap<String, Target> _cache = new ConcurrentHashMap<>();
|
|
||||||
private long _associateDelay = 5000L;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init(FilterConfig config) throws ServletException
|
|
||||||
{
|
|
||||||
if (config.getInitParameter("associateDelay") != null)
|
|
||||||
_associateDelay = Long.parseLong(config.getInitParameter("associateDelay"));
|
|
||||||
|
|
||||||
// Add a listener that is used to collect information
|
|
||||||
// about associated resource, etags and modified dates.
|
|
||||||
config.getServletContext().addListener(new ServletRequestListener()
|
|
||||||
{
|
|
||||||
// Collect information when request is destroyed.
|
|
||||||
@Override
|
|
||||||
public void requestDestroyed(ServletRequestEvent sre)
|
|
||||||
{
|
|
||||||
HttpServletRequest request = (HttpServletRequest)sre.getServletRequest();
|
|
||||||
Target target = (Target)request.getAttribute(TARGET_ATTR);
|
|
||||||
if (target == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Update conditional data.
|
|
||||||
HttpServletResponse response = (HttpServletResponse)request.getAttribute(RESPONSE_ATTR);
|
|
||||||
target._etag = response.getHeader(HttpHeader.ETAG.asString());
|
|
||||||
target._lastModified = response.getHeader(HttpHeader.LAST_MODIFIED.asString());
|
|
||||||
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("Served {} for {}", response.getStatus(), request.getRequestURI());
|
|
||||||
|
|
||||||
// Does this request have a referer?
|
|
||||||
String referer = request.getHeader(HttpHeader.REFERER.asString());
|
|
||||||
if (referer != null)
|
|
||||||
{
|
|
||||||
// Is the referer from this contexts?
|
|
||||||
HttpURI refererUri = new HttpURI(referer);
|
|
||||||
if (request.getServerName().equals(refererUri.getHost()))
|
|
||||||
{
|
|
||||||
Target refererTarget = _cache.get(refererUri.getPath());
|
|
||||||
if (refererTarget != null)
|
|
||||||
{
|
|
||||||
HttpSession session = request.getSession();
|
|
||||||
@SuppressWarnings("unchecked") ConcurrentHashMap<String, Long> timestamps = (ConcurrentHashMap<String, Long>)session.getAttribute(TIMESTAMP_ATTR);
|
|
||||||
Long last = timestamps.get(refererTarget._path);
|
|
||||||
if (last != null && TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - last) < _associateDelay)
|
|
||||||
{
|
|
||||||
if (refererTarget._associated.putIfAbsent(target._path, target) == null)
|
|
||||||
{
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("ASSOCIATE {}->{}", refererTarget._path, target._path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void requestInitialized(ServletRequestEvent sre)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException
|
|
||||||
{
|
|
||||||
req.setAttribute(RESPONSE_ATTR, resp);
|
|
||||||
HttpServletRequest request = (HttpServletRequest)req;
|
|
||||||
String uri = request.getRequestURI();
|
|
||||||
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("{} {}", request.getMethod(), uri);
|
|
||||||
|
|
||||||
HttpSession session = request.getSession(true);
|
|
||||||
|
|
||||||
// find the target for this resource
|
|
||||||
Target target = _cache.get(uri);
|
|
||||||
if (target == null)
|
|
||||||
{
|
|
||||||
Target t = new Target(uri);
|
|
||||||
target = _cache.putIfAbsent(uri, t);
|
|
||||||
target = target == null ? t : target;
|
|
||||||
}
|
|
||||||
request.setAttribute(TARGET_ATTR, target);
|
|
||||||
|
|
||||||
// Set the timestamp for this resource in this session
|
|
||||||
@SuppressWarnings("unchecked") ConcurrentHashMap<String, Long> timestamps = (ConcurrentHashMap<String, Long>)session.getAttribute(TIMESTAMP_ATTR);
|
|
||||||
if (timestamps == null)
|
|
||||||
{
|
|
||||||
timestamps = new ConcurrentHashMap<>();
|
|
||||||
session.setAttribute(TIMESTAMP_ATTR, timestamps);
|
|
||||||
}
|
|
||||||
timestamps.put(uri, System.nanoTime());
|
|
||||||
|
|
||||||
// Push any associated resources.
|
|
||||||
PushBuilder builder = request.newPushBuilder();
|
|
||||||
if (builder != null && !target._associated.isEmpty())
|
|
||||||
{
|
|
||||||
boolean conditional = request.getHeader(HttpHeader.IF_NONE_MATCH.asString()) != null ||
|
|
||||||
request.getHeader(HttpHeader.IF_MODIFIED_SINCE.asString()) != null;
|
|
||||||
// Breadth-first push of associated resources.
|
|
||||||
Queue<Target> queue = new ArrayDeque<>();
|
|
||||||
queue.offer(target);
|
|
||||||
while (!queue.isEmpty())
|
|
||||||
{
|
|
||||||
Target parent = queue.poll();
|
|
||||||
builder.addHeader("X-Pusher", PushSessionCacheFilter.class.toString());
|
|
||||||
for (Target child : parent._associated.values())
|
|
||||||
{
|
|
||||||
queue.offer(child);
|
|
||||||
|
|
||||||
String path = child._path;
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("PUSH {} <- {}", path, uri);
|
|
||||||
|
|
||||||
builder.path(path)
|
|
||||||
.setHeader(HttpHeader.IF_NONE_MATCH.asString(), conditional ? child._etag : null)
|
|
||||||
.setHeader(HttpHeader.IF_MODIFIED_SINCE.asString(), conditional ? child._lastModified : null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
chain.doFilter(req, resp);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void destroy()
|
|
||||||
{
|
|
||||||
_cache.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class Target
|
|
||||||
{
|
|
||||||
private final String _path;
|
|
||||||
private final ConcurrentMap<String, Target> _associated = new ConcurrentHashMap<>();
|
|
||||||
private volatile String _etag;
|
|
||||||
private volatile String _lastModified;
|
|
||||||
|
|
||||||
private Target(String path)
|
|
||||||
{
|
|
||||||
_path = path;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString()
|
|
||||||
{
|
|
||||||
return String.format("Target{p=%s,e=%s,m=%s,a=%d}", _path, _etag, _lastModified, _associated.size());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,356 +0,0 @@
|
|||||||
//
|
|
||||||
// ========================================================================
|
|
||||||
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
|
|
||||||
//
|
|
||||||
// This program and the accompanying materials are made available under
|
|
||||||
// the terms of the Eclipse Public License 2.0 which is available at
|
|
||||||
// https://www.eclipse.org/legal/epl-2.0
|
|
||||||
//
|
|
||||||
// This Source Code may also be made available under the following
|
|
||||||
// Secondary Licenses when the conditions for such availability set
|
|
||||||
// forth in the Eclipse Public License, v. 2.0 are satisfied:
|
|
||||||
// the Apache License v2.0 which is available at
|
|
||||||
// https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
|
||||||
// ========================================================================
|
|
||||||
//
|
|
||||||
|
|
||||||
package servlets;
|
|
||||||
|
|
||||||
import org.eclipse.jetty.util.IO;
|
|
||||||
import org.eclipse.jetty.util.StringUtil;
|
|
||||||
import org.eclipse.jetty.util.URIUtil;
|
|
||||||
|
|
||||||
import javax.servlet.*;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import javax.servlet.http.HttpServletResponseWrapper;
|
|
||||||
import java.io.*;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.ConcurrentMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PutFilter
|
|
||||||
*
|
|
||||||
* A Filter that handles PUT, DELETE and MOVE methods.
|
|
||||||
* Files are hidden during PUT operations, so that 404's result.
|
|
||||||
*
|
|
||||||
* The following init parameters pay be used:<ul>
|
|
||||||
* <li><b>baseURI</b> - The file URI of the document root for put content.
|
|
||||||
* <li><b>delAllowed</b> - boolean, if true DELETE and MOVE methods are supported.
|
|
||||||
* <li><b>putAtomic</b> - boolean, if true PUT files are written to a temp location and moved into place.
|
|
||||||
* </ul>
|
|
||||||
*/
|
|
||||||
public class PutFilter implements Filter
|
|
||||||
{
|
|
||||||
public static final String __PUT = "PUT";
|
|
||||||
public static final String __DELETE = "DELETE";
|
|
||||||
public static final String __MOVE = "MOVE";
|
|
||||||
public static final String __OPTIONS = "OPTIONS";
|
|
||||||
|
|
||||||
Set<String> _operations = new HashSet<String>();
|
|
||||||
private ConcurrentMap<String, String> _hidden = new ConcurrentHashMap<String, String>();
|
|
||||||
|
|
||||||
private ServletContext _context;
|
|
||||||
private String _baseURI;
|
|
||||||
private boolean _delAllowed;
|
|
||||||
private boolean _putAtomic;
|
|
||||||
private File _tmpdir;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init(FilterConfig config) throws ServletException
|
|
||||||
{
|
|
||||||
_context = config.getServletContext();
|
|
||||||
|
|
||||||
_tmpdir = (File)_context.getAttribute("javax.servlet.context.tempdir");
|
|
||||||
|
|
||||||
if (_context.getRealPath("/") == null)
|
|
||||||
throw new UnavailableException("Packed war");
|
|
||||||
|
|
||||||
String b = config.getInitParameter("baseURI");
|
|
||||||
if (b != null)
|
|
||||||
{
|
|
||||||
_baseURI = b;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
File base = new File(_context.getRealPath("/"));
|
|
||||||
_baseURI = base.toURI().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
_delAllowed = getInitBoolean(config, "delAllowed");
|
|
||||||
_putAtomic = getInitBoolean(config, "putAtomic");
|
|
||||||
|
|
||||||
_operations.add(__OPTIONS);
|
|
||||||
_operations.add(__PUT);
|
|
||||||
if (_delAllowed)
|
|
||||||
{
|
|
||||||
_operations.add(__DELETE);
|
|
||||||
_operations.add(__MOVE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean getInitBoolean(FilterConfig config, String name)
|
|
||||||
{
|
|
||||||
String value = config.getInitParameter(name);
|
|
||||||
return value != null && value.length() > 0 && (value.startsWith("t") || value.startsWith("T") || value.startsWith("y") || value.startsWith("Y") || value.startsWith("1"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException
|
|
||||||
{
|
|
||||||
HttpServletRequest request = (HttpServletRequest)req;
|
|
||||||
HttpServletResponse response = (HttpServletResponse)res;
|
|
||||||
|
|
||||||
String servletPath = request.getServletPath();
|
|
||||||
String pathInfo = request.getPathInfo();
|
|
||||||
String pathInContext = URIUtil.addPaths(servletPath, pathInfo);
|
|
||||||
|
|
||||||
String resource = URIUtil.addPaths(_baseURI, pathInContext);
|
|
||||||
|
|
||||||
String method = request.getMethod();
|
|
||||||
boolean op = _operations.contains(method);
|
|
||||||
|
|
||||||
if (op)
|
|
||||||
{
|
|
||||||
File file = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (method.equals(__OPTIONS))
|
|
||||||
handleOptions(chain, request, response);
|
|
||||||
else
|
|
||||||
{
|
|
||||||
file = new File(new URI(resource));
|
|
||||||
boolean exists = file.exists();
|
|
||||||
if (exists && !passConditionalHeaders(request, response, file))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (method.equals(__PUT))
|
|
||||||
handlePut(request, response, pathInContext, file);
|
|
||||||
else if (method.equals(__DELETE))
|
|
||||||
handleDelete(request, response, pathInContext, file);
|
|
||||||
else if (method.equals(__MOVE))
|
|
||||||
handleMove(request, response, pathInContext, file);
|
|
||||||
else
|
|
||||||
throw new IllegalStateException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_context.log(e.toString(), e);
|
|
||||||
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (isHidden(pathInContext))
|
|
||||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
|
||||||
else
|
|
||||||
chain.doFilter(request, response);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isHidden(String pathInContext)
|
|
||||||
{
|
|
||||||
return _hidden.containsKey(pathInContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void destroy()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public void handlePut(HttpServletRequest request, HttpServletResponse response, String pathInContext, File file) throws ServletException, IOException
|
|
||||||
{
|
|
||||||
boolean exists = file.exists();
|
|
||||||
if (pathInContext.endsWith("/"))
|
|
||||||
{
|
|
||||||
if (!exists)
|
|
||||||
{
|
|
||||||
if (!file.mkdirs())
|
|
||||||
response.sendError(HttpServletResponse.SC_FORBIDDEN);
|
|
||||||
else
|
|
||||||
{
|
|
||||||
response.setStatus(HttpServletResponse.SC_CREATED);
|
|
||||||
response.flushBuffer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
response.setStatus(HttpServletResponse.SC_OK);
|
|
||||||
response.flushBuffer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
boolean ok = false;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_hidden.put(pathInContext, pathInContext);
|
|
||||||
File parent = file.getParentFile();
|
|
||||||
parent.mkdirs();
|
|
||||||
int toRead = request.getContentLength();
|
|
||||||
InputStream in = request.getInputStream();
|
|
||||||
|
|
||||||
if (_putAtomic)
|
|
||||||
{
|
|
||||||
File tmp = File.createTempFile(file.getName(), null, _tmpdir);
|
|
||||||
try (OutputStream out = new FileOutputStream(tmp, false))
|
|
||||||
{
|
|
||||||
if (toRead >= 0)
|
|
||||||
IO.copy(in, out, toRead);
|
|
||||||
else
|
|
||||||
IO.copy(in, out);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tmp.renameTo(file))
|
|
||||||
throw new IOException("rename from " + tmp + " to " + file + " failed");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
try (OutputStream out = new FileOutputStream(file, false))
|
|
||||||
{
|
|
||||||
if (toRead >= 0)
|
|
||||||
IO.copy(in, out, toRead);
|
|
||||||
else
|
|
||||||
IO.copy(in, out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response.setStatus(exists ? HttpServletResponse.SC_OK : HttpServletResponse.SC_CREATED);
|
|
||||||
response.flushBuffer();
|
|
||||||
ok = true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_context.log(ex.toString(), ex);
|
|
||||||
response.sendError(HttpServletResponse.SC_FORBIDDEN);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (!ok)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (file.exists())
|
|
||||||
file.delete();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_context.log(e.toString(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_hidden.remove(pathInContext);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void handleDelete(HttpServletRequest request, HttpServletResponse response, String pathInContext, File file) throws ServletException, IOException
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// delete the file
|
|
||||||
if (file.delete())
|
|
||||||
{
|
|
||||||
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
|
|
||||||
response.flushBuffer();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
response.sendError(HttpServletResponse.SC_FORBIDDEN);
|
|
||||||
}
|
|
||||||
catch (SecurityException sex)
|
|
||||||
{
|
|
||||||
_context.log(sex.toString(), sex);
|
|
||||||
response.sendError(HttpServletResponse.SC_FORBIDDEN);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void handleMove(HttpServletRequest request, HttpServletResponse response, String pathInContext, File file)
|
|
||||||
throws ServletException, IOException, URISyntaxException
|
|
||||||
{
|
|
||||||
String newPath = URIUtil.canonicalEncodedPath(request.getHeader("new-uri"));
|
|
||||||
if (newPath == null)
|
|
||||||
{
|
|
||||||
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String contextPath = request.getContextPath();
|
|
||||||
if (contextPath != null && !newPath.startsWith(contextPath))
|
|
||||||
{
|
|
||||||
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String newInfo = newPath;
|
|
||||||
if (contextPath != null)
|
|
||||||
newInfo = newInfo.substring(contextPath.length());
|
|
||||||
|
|
||||||
String newResource = URIUtil.addEncodedPaths(_baseURI, newInfo);
|
|
||||||
File newFile = new File(new URI(newResource));
|
|
||||||
|
|
||||||
file.renameTo(newFile);
|
|
||||||
|
|
||||||
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
|
|
||||||
response.flushBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void handleOptions(FilterChain chain, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
|
|
||||||
{
|
|
||||||
chain.doFilter(request, new HttpServletResponseWrapper(response)
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void setHeader(String name, String value)
|
|
||||||
{
|
|
||||||
if ("Allow".equalsIgnoreCase(name))
|
|
||||||
{
|
|
||||||
Set<String> options = new HashSet<String>();
|
|
||||||
options.addAll(Arrays.asList(StringUtil.csvSplit(value)));
|
|
||||||
options.addAll(_operations);
|
|
||||||
value = null;
|
|
||||||
for (String o : options)
|
|
||||||
{
|
|
||||||
value = value == null ? o : (value + ", " + o);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
super.setHeader(name, value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Check modification date headers.
|
|
||||||
*/
|
|
||||||
protected boolean passConditionalHeaders(HttpServletRequest request, HttpServletResponse response, File file) throws IOException
|
|
||||||
{
|
|
||||||
long date = 0;
|
|
||||||
|
|
||||||
if ((date = request.getDateHeader("if-unmodified-since")) > 0)
|
|
||||||
{
|
|
||||||
if (file.lastModified() / 1000 > date / 1000)
|
|
||||||
{
|
|
||||||
response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((date = request.getDateHeader("if-modified-since")) > 0)
|
|
||||||
{
|
|
||||||
if (file.lastModified() / 1000 <= date / 1000)
|
|
||||||
{
|
|
||||||
response.reset();
|
|
||||||
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
|
|
||||||
response.flushBuffer();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,380 +0,0 @@
|
|||||||
//
|
|
||||||
// ========================================================================
|
|
||||||
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
|
|
||||||
//
|
|
||||||
// This program and the accompanying materials are made available under
|
|
||||||
// the terms of the Eclipse Public License 2.0 which is available at
|
|
||||||
// https://www.eclipse.org/legal/epl-2.0
|
|
||||||
//
|
|
||||||
// This Source Code may also be made available under the following
|
|
||||||
// Secondary Licenses when the conditions for such availability set
|
|
||||||
// forth in the Eclipse Public License, v. 2.0 are satisfied:
|
|
||||||
// the Apache License v2.0 which is available at
|
|
||||||
// https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
|
||||||
// ========================================================================
|
|
||||||
//
|
|
||||||
|
|
||||||
package servlets;
|
|
||||||
|
|
||||||
import org.eclipse.jetty.server.handler.ContextHandler;
|
|
||||||
import org.eclipse.jetty.util.annotation.ManagedAttribute;
|
|
||||||
import org.eclipse.jetty.util.annotation.ManagedObject;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import javax.servlet.*;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import javax.servlet.http.HttpSession;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Queue;
|
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
|
||||||
import java.util.concurrent.Semaphore;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Quality of Service Filter.
|
|
||||||
* <p>
|
|
||||||
* This filter limits the number of active requests to the number set by the "maxRequests" init parameter (default 10).
|
|
||||||
* If more requests are received, they are suspended and placed on priority queues. Priorities are determined by
|
|
||||||
* the {@link #getPriority(ServletRequest)} method and are a value between 0 and the value given by the "maxPriority"
|
|
||||||
* init parameter (default 10), with higher values having higher priority.
|
|
||||||
* <p>
|
|
||||||
* This filter is ideal to prevent wasting threads waiting for slow/limited
|
|
||||||
* resources such as a JDBC connection pool. It avoids the situation where all of a
|
|
||||||
* containers thread pool may be consumed blocking on such a slow resource.
|
|
||||||
* By limiting the number of active threads, a smaller thread pool may be used as
|
|
||||||
* the threads are not wasted waiting. Thus more memory may be available for use by
|
|
||||||
* the active threads.
|
|
||||||
* <p>
|
|
||||||
* Furthermore, this filter uses a priority when resuming waiting requests. So that if
|
|
||||||
* a container is under load, and there are many requests waiting for resources,
|
|
||||||
* the {@link #getPriority(ServletRequest)} method is used, so that more important
|
|
||||||
* requests are serviced first. For example, this filter could be deployed with a
|
|
||||||
* maxRequest limit slightly smaller than the containers thread pool and a high priority
|
|
||||||
* allocated to admin users. Thus regardless of load, admin users would always be
|
|
||||||
* able to access the web application.
|
|
||||||
* <p>
|
|
||||||
* The maxRequest limit is policed by a {@link Semaphore} and the filter will wait a short while attempting to acquire
|
|
||||||
* the semaphore. This wait is controlled by the "waitMs" init parameter and allows the expense of a suspend to be
|
|
||||||
* avoided if the semaphore is shortly available. If the semaphore cannot be obtained, the request will be suspended
|
|
||||||
* for the default suspend period of the container or the valued set as the "suspendMs" init parameter.
|
|
||||||
* <p>
|
|
||||||
* If the "managedAttr" init parameter is set to true, then this servlet is set as a {@link ServletContext} attribute with the
|
|
||||||
* filter name as the attribute name. This allows context external mechanism (eg JMX via {@link ContextHandler#MANAGED_ATTRIBUTES}) to
|
|
||||||
* manage the configuration of the filter.
|
|
||||||
*/
|
|
||||||
@ManagedObject("Quality of Service Filter")
|
|
||||||
public class QoSFilter implements Filter
|
|
||||||
{
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(QoSFilter.class);
|
|
||||||
|
|
||||||
static final int __DEFAULT_MAX_PRIORITY = 10;
|
|
||||||
static final int __DEFAULT_PASSES = 10;
|
|
||||||
static final int __DEFAULT_WAIT_MS = 50;
|
|
||||||
static final long __DEFAULT_TIMEOUT_MS = -1;
|
|
||||||
|
|
||||||
static final String MANAGED_ATTR_INIT_PARAM = "managedAttr";
|
|
||||||
static final String MAX_REQUESTS_INIT_PARAM = "maxRequests";
|
|
||||||
static final String MAX_PRIORITY_INIT_PARAM = "maxPriority";
|
|
||||||
static final String MAX_WAIT_INIT_PARAM = "waitMs";
|
|
||||||
static final String SUSPEND_INIT_PARAM = "suspendMs";
|
|
||||||
|
|
||||||
private final String _suspended = "QoSFilter@" + Integer.toHexString(hashCode()) + ".SUSPENDED";
|
|
||||||
private final String _resumed = "QoSFilter@" + Integer.toHexString(hashCode()) + ".RESUMED";
|
|
||||||
private long _waitMs;
|
|
||||||
private long _suspendMs;
|
|
||||||
private int _maxRequests;
|
|
||||||
private Semaphore _passes;
|
|
||||||
private Queue<AsyncContext>[] _queues;
|
|
||||||
private AsyncListener[] _listeners;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init(FilterConfig filterConfig)
|
|
||||||
{
|
|
||||||
int maxPriority = __DEFAULT_MAX_PRIORITY;
|
|
||||||
if (filterConfig.getInitParameter(MAX_PRIORITY_INIT_PARAM) != null)
|
|
||||||
maxPriority = Integer.parseInt(filterConfig.getInitParameter(MAX_PRIORITY_INIT_PARAM));
|
|
||||||
_queues = new Queue[maxPriority + 1];
|
|
||||||
_listeners = new AsyncListener[_queues.length];
|
|
||||||
for (int p = 0; p < _queues.length; ++p)
|
|
||||||
{
|
|
||||||
_queues[p] = new ConcurrentLinkedQueue<>();
|
|
||||||
_listeners[p] = new QoSAsyncListener(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
int maxRequests = __DEFAULT_PASSES;
|
|
||||||
if (filterConfig.getInitParameter(MAX_REQUESTS_INIT_PARAM) != null)
|
|
||||||
maxRequests = Integer.parseInt(filterConfig.getInitParameter(MAX_REQUESTS_INIT_PARAM));
|
|
||||||
_passes = new Semaphore(maxRequests, true);
|
|
||||||
_maxRequests = maxRequests;
|
|
||||||
|
|
||||||
long wait = __DEFAULT_WAIT_MS;
|
|
||||||
if (filterConfig.getInitParameter(MAX_WAIT_INIT_PARAM) != null)
|
|
||||||
wait = Integer.parseInt(filterConfig.getInitParameter(MAX_WAIT_INIT_PARAM));
|
|
||||||
_waitMs = wait;
|
|
||||||
|
|
||||||
long suspend = __DEFAULT_TIMEOUT_MS;
|
|
||||||
if (filterConfig.getInitParameter(SUSPEND_INIT_PARAM) != null)
|
|
||||||
suspend = Integer.parseInt(filterConfig.getInitParameter(SUSPEND_INIT_PARAM));
|
|
||||||
_suspendMs = suspend;
|
|
||||||
|
|
||||||
ServletContext context = filterConfig.getServletContext();
|
|
||||||
if (context != null && Boolean.parseBoolean(filterConfig.getInitParameter(MANAGED_ATTR_INIT_PARAM)))
|
|
||||||
context.setAttribute(filterConfig.getFilterName(), this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
|
|
||||||
{
|
|
||||||
boolean accepted = false;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Boolean suspended = (Boolean)request.getAttribute(_suspended);
|
|
||||||
if (suspended == null)
|
|
||||||
{
|
|
||||||
accepted = _passes.tryAcquire(getWaitMs(), TimeUnit.MILLISECONDS);
|
|
||||||
if (accepted)
|
|
||||||
{
|
|
||||||
request.setAttribute(_suspended, Boolean.FALSE);
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("Accepted {}", request);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
request.setAttribute(_suspended, Boolean.TRUE);
|
|
||||||
int priority = getPriority(request);
|
|
||||||
AsyncContext asyncContext = request.startAsync();
|
|
||||||
long suspendMs = getSuspendMs();
|
|
||||||
if (suspendMs > 0)
|
|
||||||
asyncContext.setTimeout(suspendMs);
|
|
||||||
asyncContext.addListener(_listeners[priority]);
|
|
||||||
_queues[priority].add(asyncContext);
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("Suspended {}", request);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (suspended)
|
|
||||||
{
|
|
||||||
request.setAttribute(_suspended, Boolean.FALSE);
|
|
||||||
Boolean resumed = (Boolean)request.getAttribute(_resumed);
|
|
||||||
if (Boolean.TRUE.equals(resumed))
|
|
||||||
{
|
|
||||||
_passes.acquire();
|
|
||||||
accepted = true;
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("Resumed {}", request);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Timeout! try 1 more time.
|
|
||||||
accepted = _passes.tryAcquire(getWaitMs(), TimeUnit.MILLISECONDS);
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("Timeout {}", request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Pass through resume of previously accepted request.
|
|
||||||
_passes.acquire();
|
|
||||||
accepted = true;
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("Passthrough {}", request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (accepted)
|
|
||||||
{
|
|
||||||
chain.doFilter(request, response);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (LOG.isDebugEnabled())
|
|
||||||
LOG.debug("Rejected {}", request);
|
|
||||||
((HttpServletResponse)response).sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (InterruptedException e)
|
|
||||||
{
|
|
||||||
((HttpServletResponse)response).sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (accepted)
|
|
||||||
{
|
|
||||||
_passes.release();
|
|
||||||
|
|
||||||
for (int p = _queues.length - 1; p >= 0; --p)
|
|
||||||
{
|
|
||||||
AsyncContext asyncContext = _queues[p].poll();
|
|
||||||
if (asyncContext != null)
|
|
||||||
{
|
|
||||||
ServletRequest candidate = asyncContext.getRequest();
|
|
||||||
Boolean suspended = (Boolean)candidate.getAttribute(_suspended);
|
|
||||||
if (Boolean.TRUE.equals(suspended))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
candidate.setAttribute(_resumed, Boolean.TRUE);
|
|
||||||
asyncContext.dispatch();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
catch (IllegalStateException x)
|
|
||||||
{
|
|
||||||
LOG.warn("Unable to resume suspended dispatch", x);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computes the request priority.
|
|
||||||
* <p>
|
|
||||||
* The default implementation assigns the following priorities:
|
|
||||||
* <ul>
|
|
||||||
* <li> 2 - for an authenticated request
|
|
||||||
* <li> 1 - for a request with valid / non new session
|
|
||||||
* <li> 0 - for all other requests.
|
|
||||||
* </ul>
|
|
||||||
* This method may be overridden to provide application specific priorities.
|
|
||||||
*
|
|
||||||
* @param request the incoming request
|
|
||||||
* @return the computed request priority
|
|
||||||
*/
|
|
||||||
protected int getPriority(ServletRequest request)
|
|
||||||
{
|
|
||||||
HttpServletRequest baseRequest = (HttpServletRequest)request;
|
|
||||||
if (baseRequest.getUserPrincipal() != null)
|
|
||||||
{
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
HttpSession session = baseRequest.getSession(false);
|
|
||||||
if (session != null && !session.isNew())
|
|
||||||
return 1;
|
|
||||||
else
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void destroy()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the (short) amount of time (in milliseconds) that the filter would wait
|
|
||||||
* for the semaphore to become available before suspending a request.
|
|
||||||
*
|
|
||||||
* @return wait time (in milliseconds)
|
|
||||||
*/
|
|
||||||
@ManagedAttribute("(short) amount of time filter will wait before suspending request (in ms)")
|
|
||||||
public long getWaitMs()
|
|
||||||
{
|
|
||||||
return _waitMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the (short) amount of time (in milliseconds) that the filter would wait
|
|
||||||
* for the semaphore to become available before suspending a request.
|
|
||||||
*
|
|
||||||
* @param value wait time (in milliseconds)
|
|
||||||
*/
|
|
||||||
public void setWaitMs(long value)
|
|
||||||
{
|
|
||||||
_waitMs = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the amount of time (in milliseconds) that the filter would suspend
|
|
||||||
* a request for while waiting for the semaphore to become available.
|
|
||||||
*
|
|
||||||
* @return suspend time (in milliseconds)
|
|
||||||
*/
|
|
||||||
@ManagedAttribute("amount of time filter will suspend a request for while waiting for the semaphore to become available (in ms)")
|
|
||||||
public long getSuspendMs()
|
|
||||||
{
|
|
||||||
return _suspendMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the amount of time (in milliseconds) that the filter would suspend
|
|
||||||
* a request for while waiting for the semaphore to become available.
|
|
||||||
*
|
|
||||||
* @param value suspend time (in milliseconds)
|
|
||||||
*/
|
|
||||||
public void setSuspendMs(long value)
|
|
||||||
{
|
|
||||||
_suspendMs = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the maximum number of requests allowed to be processed
|
|
||||||
* at the same time.
|
|
||||||
*
|
|
||||||
* @return maximum number of requests
|
|
||||||
*/
|
|
||||||
@ManagedAttribute("maximum number of requests to allow processing of at the same time")
|
|
||||||
public int getMaxRequests()
|
|
||||||
{
|
|
||||||
return _maxRequests;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the maximum number of requests allowed to be processed
|
|
||||||
* at the same time.
|
|
||||||
*
|
|
||||||
* @param value the number of requests
|
|
||||||
*/
|
|
||||||
public void setMaxRequests(int value)
|
|
||||||
{
|
|
||||||
_passes = new Semaphore((value - getMaxRequests() + _passes.availablePermits()), true);
|
|
||||||
_maxRequests = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class QoSAsyncListener implements AsyncListener
|
|
||||||
{
|
|
||||||
private final int priority;
|
|
||||||
|
|
||||||
public QoSAsyncListener(int priority)
|
|
||||||
{
|
|
||||||
this.priority = priority;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStartAsync(AsyncEvent event) throws IOException
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onComplete(AsyncEvent event) throws IOException
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTimeout(AsyncEvent event) throws IOException
|
|
||||||
{
|
|
||||||
// Remove before it's redispatched, so it won't be
|
|
||||||
// redispatched again at the end of the filtering.
|
|
||||||
AsyncContext asyncContext = event.getAsyncContext();
|
|
||||||
_queues[priority].remove(asyncContext);
|
|
||||||
((HttpServletResponse)event.getSuppliedResponse()).sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
|
|
||||||
asyncContext.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(AsyncEvent event) throws IOException
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
//
|
|
||||||
// ========================================================================
|
|
||||||
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
|
|
||||||
//
|
|
||||||
// This program and the accompanying materials are made available under
|
|
||||||
// the terms of the Eclipse Public License 2.0 which is available at
|
|
||||||
// https://www.eclipse.org/legal/epl-2.0
|
|
||||||
//
|
|
||||||
// This Source Code may also be made available under the following
|
|
||||||
// Secondary Licenses when the conditions for such availability set
|
|
||||||
// forth in the Eclipse Public License, v. 2.0 are satisfied:
|
|
||||||
// the Apache License v2.0 which is available at
|
|
||||||
// https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
|
||||||
// ========================================================================
|
|
||||||
//
|
|
||||||
|
|
||||||
package servlets;
|
|
||||||
|
|
||||||
import javax.servlet.*;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Welcome Filter
|
|
||||||
* This filter can be used to server an index file for a directory
|
|
||||||
* when no index file actually exists (thus the web.xml mechanism does
|
|
||||||
* not work).
|
|
||||||
*
|
|
||||||
* This filter will dispatch requests to a directory (URLs ending with /)
|
|
||||||
* to the welcome URL determined by the "welcome" init parameter. So if
|
|
||||||
* the filter "welcome" init parameter is set to "index.do" then a request
|
|
||||||
* to "/some/directory/" will be dispatched to "/some/directory/index.do" and
|
|
||||||
* will be handled by any servlets mapped to that URL.
|
|
||||||
*
|
|
||||||
* Requests to "/some/directory" will be redirected to "/some/directory/".
|
|
||||||
*/
|
|
||||||
public class WelcomeFilter implements Filter
|
|
||||||
{
|
|
||||||
private String welcome;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init(FilterConfig filterConfig)
|
|
||||||
{
|
|
||||||
welcome = filterConfig.getInitParameter("welcome");
|
|
||||||
if (welcome == null)
|
|
||||||
welcome = "index.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void doFilter(ServletRequest request,
|
|
||||||
ServletResponse response,
|
|
||||||
FilterChain chain)
|
|
||||||
throws IOException, ServletException
|
|
||||||
{
|
|
||||||
String path = ((HttpServletRequest)request).getServletPath();
|
|
||||||
if (welcome != null && path.endsWith("/"))
|
|
||||||
request.getRequestDispatcher(path + welcome).forward(request, response);
|
|
||||||
else
|
|
||||||
chain.doFilter(request, response);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void destroy()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
|||||||
//
|
|
||||||
// ========================================================================
|
|
||||||
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
|
|
||||||
//
|
|
||||||
// This program and the accompanying materials are made available under
|
|
||||||
// the terms of the Eclipse Public License 2.0 which is available at
|
|
||||||
// https://www.eclipse.org/legal/epl-2.0
|
|
||||||
//
|
|
||||||
// This Source Code may also be made available under the following
|
|
||||||
// Secondary Licenses when the conditions for such availability set
|
|
||||||
// forth in the Eclipse Public License, v. 2.0 are satisfied:
|
|
||||||
// the Apache License v2.0 which is available at
|
|
||||||
// https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
|
||||||
// ========================================================================
|
|
||||||
//
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Jetty Servlets : Generally Useful Servlets, Handlers and Filters
|
|
||||||
*/
|
|
||||||
package servlets;
|
|
||||||
|
|
@ -1,5 +1,20 @@
|
|||||||
<html>
|
<html>
|
||||||
<body>
|
<head>
|
||||||
<h2>Hello World!</h2>
|
<title>test-proxy</title>
|
||||||
|
</head>
|
||||||
|
<body bgcolor=white>
|
||||||
|
|
||||||
|
<table border="0">
|
||||||
|
<tr>
|
||||||
|
<td align=center>
|
||||||
|
<img src="images/io42630.png">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<h1>test-proxy</h1>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
Loading…
Reference in new issue