Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: move blocking calls outside session lock (#19938) #20475

Merged
merged 10 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
*/
package com.vaadin.flow.server;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.Serializable;
import java.util.Optional;

/**
* RequestHandler which takes care of locking and unlocking of the VaadinSession
Expand All @@ -28,28 +32,58 @@
*/
public abstract class SynchronizedRequestHandler implements RequestHandler {

public static final int MAX_BUFFER_SIZE = 64 * 1024;

/**
* ResponseWriter is optionally returned by request handlers which implement
* {@link SynchronizedRequestHandler#synchronizedHandleRequest(VaadinSession, VaadinRequest, VaadinResponse, String)}
*
* The ResponseWriter will be executed by
* {@link #handleRequest(VaadinSession, VaadinRequest, VaadinResponse)}
* without holding Vaadin session lock.
*/
@FunctionalInterface
public interface ResponseWriter extends Serializable {
mcollovati marked this conversation as resolved.
Show resolved Hide resolved
void writeResponse() throws IOException;
}

@Override
public boolean handleRequest(VaadinSession session, VaadinRequest request,
VaadinResponse response) throws IOException {
if (!canHandleRequest(request)) {
return false;
}

session.lock();
try {
return synchronizedHandleRequest(session, request, response);
if (isReadRequestBodyFirstEnabled()) {
BufferedReader reader = request.getReader();
String requestBody = reader == null ? null
: getRequestBody(reader);
session.lock();
Optional<ResponseWriter> responseWriter = synchronizedHandleRequest(
session, request, response, requestBody);
session.unlock();
if (responseWriter.isPresent()) {
responseWriter.get().writeResponse();
}
return responseWriter.isPresent();
mcollovati marked this conversation as resolved.
Show resolved Hide resolved
} else {
session.lock();
return synchronizedHandleRequest(session, request, response);
}
} finally {
session.unlock();
if (session.hasLock()) {
session.unlock();
}
}
}

/**
* Identical to
* {@link #handleRequest(VaadinSession, VaadinRequest, VaadinResponse)}
* except the {@link VaadinSession} is locked before this is called and
* unlocked after this has completed.
* except the request body is read before locking the VaadinSession and
* calling this method.
tepi marked this conversation as resolved.
Show resolved Hide resolved
*
* @see #handleRequest(VaadinSession, VaadinRequest, VaadinResponse)
* @param session
* The session for the request
* @param request
Expand All @@ -58,13 +92,58 @@ public boolean handleRequest(VaadinSession session, VaadinRequest request,
* The response object to which a response can be written.
* @return true if a response has been written and no further request
* handlers should be called, otherwise false
*
* @throws IOException
* If an IO error occurred
* @see #handleRequest(VaadinSession, VaadinRequest, VaadinResponse)
*/
public abstract boolean synchronizedHandleRequest(VaadinSession session,
VaadinRequest request, VaadinResponse response) throws IOException;

/**
* Gets if request body should be read before calling
* synchronizedHandleRequest.
*
* @return {@literal true} if
* {@link #synchronizedHandleRequest(VaadinSession, VaadinRequest, VaadinResponse, String)}
* should be called. Returns {@literal false} if
* {@link #synchronizedHandleRequest(VaadinSession, VaadinRequest, VaadinResponse)}
* should be called.
*/
public boolean isReadRequestBodyFirstEnabled() {
return false;
}

/**
* Identical to
* {@link #synchronizedHandleRequest(VaadinSession, VaadinRequest, VaadinResponse)}
* except the {@link VaadinSession} is locked before this is called and the
* response requestBody has been read before locking the session and is
mcollovati marked this conversation as resolved.
Show resolved Hide resolved
* provided as a separate parameter.
*
* @param session
* The session for the request
* @param request
* The request to handle
* @param response
* The response object to which a response can be written.
* @param requestBody
* Request body pre-read from the request object
* @return a ReponseWriter wrapped into an Optional, if this handler will
* write the response and no further request handlers should be
* called, otherwise an empty Optional. The ResponseWrited will be
tepi marked this conversation as resolved.
Show resolved Hide resolved
* executed after the VaadinSession is unlocked.
*
* @throws IOException
* If an IO error occurred
* @see #handleRequest(VaadinSession, VaadinRequest, VaadinResponse)
*/
public Optional<ResponseWriter> synchronizedHandleRequest(
VaadinSession session, VaadinRequest request,
VaadinResponse response, String requestBody)
throws IOException, UnsupportedOperationException {
throw new UnsupportedOperationException();
}

/**
* Check whether a request may be handled by this handler. This can be used
* as an optimization to avoid locking the session just to investigate some
Expand All @@ -85,4 +164,18 @@ protected boolean canHandleRequest(VaadinRequest request) {
return true;
}

public static String getRequestBody(Reader reader) throws IOException {
StringBuilder sb = new StringBuilder(MAX_BUFFER_SIZE);
char[] buffer = new char[MAX_BUFFER_SIZE];

while (true) {
int read = reader.read(buffer);
if (read == -1) {
break;
}
sb.append(buffer, 0, read);
}

return sb.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import com.vaadin.flow.server.ErrorEvent;
import com.vaadin.flow.server.HandlerHelper;
import com.vaadin.flow.server.SessionExpiredException;
import com.vaadin.flow.server.SynchronizedRequestHandler;
import com.vaadin.flow.server.SystemMessages;
import com.vaadin.flow.server.VaadinContext;
import com.vaadin.flow.server.VaadinRequest;
Expand All @@ -56,7 +57,6 @@
import com.vaadin.flow.shared.ApplicationConstants;
import com.vaadin.flow.shared.JsonConstants;
import com.vaadin.flow.shared.communication.PushMode;

import elemental.json.JsonException;

/**
Expand Down Expand Up @@ -164,7 +164,9 @@ interface PushEventCallback {
assert vaadinRequest != null;

try {
new ServerRpcHandler().handleRpc(ui, reader, vaadinRequest);
new ServerRpcHandler().handleRpc(ui,
SynchronizedRequestHandler.getRequestBody(reader),
vaadinRequest);
connection.push(false);
} catch (JsonException e) {
getLogger().error("Error writing JSON to response", e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import com.vaadin.flow.internal.StateNode;
import com.vaadin.flow.router.PreserveOnRefresh;
import com.vaadin.flow.server.ErrorEvent;
import com.vaadin.flow.server.SynchronizedRequestHandler;
import com.vaadin.flow.server.VaadinRequest;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.server.communication.rpc.AttachExistingElementRpcHandler;
Expand Down Expand Up @@ -94,6 +95,11 @@ public static class RpcRequest implements Serializable {
* the request through which the JSON was received
*/
public RpcRequest(String jsonString, VaadinRequest request) {
this(jsonString, request.getService().getDeploymentConfiguration()
.isSyncIdCheckEnabled());
}

public RpcRequest(String jsonString, boolean isSyncIdCheckEnabled) {
json = JsonUtil.parse(jsonString);

JsonValue token = json.get(ApplicationConstants.CSRF_TOKEN);
Expand All @@ -107,8 +113,7 @@ public RpcRequest(String jsonString, VaadinRequest request) {
this.csrfToken = csrfToken;
}

if (request.getService().getDeploymentConfiguration()
.isSyncIdCheckEnabled()) {
if (isSyncIdCheckEnabled) {
syncId = (int) json
.getNumber(ApplicationConstants.SERVER_SYNC_ID);
} else {
Expand Down Expand Up @@ -199,8 +204,6 @@ private boolean isUnloadBeaconRequest() {

}

private static final int MAX_BUFFER_SIZE = 64 * 1024;

/**
* Exception thrown then the security key sent by the client does not match
* the expected one.
Expand Down Expand Up @@ -251,26 +254,45 @@ public ResynchronizationRequiredException() {
*/
public void handleRpc(UI ui, Reader reader, VaadinRequest request)
throws IOException, InvalidUIDLSecurityKeyException {
ui.getSession().setLastRequestTimestamp(System.currentTimeMillis());
handleRpc(ui, SynchronizedRequestHandler.getRequestBody(reader),
request);
}

String changeMessage = getMessage(reader);
/**
* Reads JSON containing zero or more serialized RPC calls (including legacy
* variable changes) and executes the calls.
*
* @param ui
* The {@link UI} receiving the calls. Cannot be null.
* @param message
* The JSON message from the request.
* @param request
* The request through which the RPC was received
* @throws InvalidUIDLSecurityKeyException
* If the received security key does not match the one stored in
* the session.
*/
public void handleRpc(UI ui, String message, VaadinRequest request)
throws InvalidUIDLSecurityKeyException {
ui.getSession().setLastRequestTimestamp(System.currentTimeMillis());

if (changeMessage == null || changeMessage.equals("")) {
if (message == null || message.isEmpty()) {
// The client sometimes sends empty messages, this is probably a bug
return;
}

RpcRequest rpcRequest = new RpcRequest(changeMessage, request);
RpcRequest rpcRequest = new RpcRequest(message, request.getService()
.getDeploymentConfiguration().isSyncIdCheckEnabled());

// Security: double cookie submission pattern unless disabled by
// property
if (!VaadinService.isCsrfTokenValid(ui, rpcRequest.getCsrfToken())) {
throw new InvalidUIDLSecurityKeyException();
}

String hashMessage = changeMessage;
String hashMessage = message;
if (hashMessage.length() > 64 * 1024) {
hashMessage = changeMessage.substring(0, 64 * 1024);
hashMessage = message.substring(0, 64 * 1024);
}
byte[] messageHash = MessageDigestUtil.sha256(hashMessage);

Expand Down Expand Up @@ -374,7 +396,6 @@ public void handleRpc(UI ui, Reader reader, VaadinRequest request)
getLogger().debug("UI closed with a beacon request");
}
}

}

private void enforceIfNeeded(VaadinRequest request, RpcRequest rpcRequest) {
Expand Down Expand Up @@ -550,8 +571,9 @@ private static void callErrorHandler(UI ui, JsonObject invocationJson,

protected String getMessage(Reader reader) throws IOException {

StringBuilder sb = new StringBuilder(MAX_BUFFER_SIZE);
char[] buffer = new char[MAX_BUFFER_SIZE];
StringBuilder sb = new StringBuilder(
SynchronizedRequestHandler.MAX_BUFFER_SIZE);
char[] buffer = new char[SynchronizedRequestHandler.MAX_BUFFER_SIZE];

while (true) {
int read = reader.read(buffer);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -102,48 +103,64 @@ protected ServerRpcHandler createRpcHandler() {
@Override
public boolean synchronizedHandleRequest(VaadinSession session,
VaadinRequest request, VaadinResponse response) throws IOException {
String requestBody = SynchronizedRequestHandler
.getRequestBody(request.getReader());
Optional<ResponseWriter> responseWriter = synchronizedHandleRequest(
session, request, response, requestBody);
if (responseWriter.isPresent()) {
responseWriter.get().writeResponse();
}
return responseWriter.isPresent();
mcollovati marked this conversation as resolved.
Show resolved Hide resolved
}

@Override
public boolean isReadRequestBodyFirstEnabled() {
return true;
}

@Override
public Optional<ResponseWriter> synchronizedHandleRequest(
VaadinSession session, VaadinRequest request,
VaadinResponse response, String requestBody)
throws IOException, UnsupportedOperationException {
UI uI = session.getService().findUI(request);
if (uI == null) {
// This should not happen but it will if the UI has been closed. We
// really don't want to see it in the server logs though
commitJsonResponse(response,
VaadinService.createUINotFoundJSON(false));
return true;
return Optional.of(() -> commitJsonResponse(response,
VaadinService.createUINotFoundJSON(false)));
}

StringWriter stringWriter = new StringWriter();

try {
getRpcHandler(session).handleRpc(uI, request.getReader(), request);
getRpcHandler(session).handleRpc(uI, requestBody, request);
writeUidl(uI, stringWriter, false);
} catch (JsonException e) {
getLogger().error("Error writing JSON to response", e);
// Refresh on client side
writeRefresh(response);
return true;
return Optional.of(() -> writeRefresh(response));
} catch (InvalidUIDLSecurityKeyException e) {
getLogger().warn("Invalid security key received from {}",
request.getRemoteHost());
// Refresh on client side
writeRefresh(response);
return true;
return Optional.of(() -> writeRefresh(response));
} catch (DauEnforcementException e) {
getLogger().warn(
"Daily Active User limit reached. Blocking new user request");
response.setHeader(DAUUtils.STATUS_CODE_KEY, String
.valueOf(HttpStatusCode.SERVICE_UNAVAILABLE.getCode()));
String json = DAUUtils.jsonEnforcementResponse(request, e);
commitJsonResponse(response, json);
return true;
return Optional.of(() -> commitJsonResponse(response, json));
} catch (ResynchronizationRequiredException e) { // NOSONAR
// Resync on the client side
writeUidl(uI, stringWriter, true);
} finally {
stringWriter.close();
}

commitJsonResponse(response, stringWriter.toString());
return true;
return Optional.of(
() -> commitJsonResponse(response, stringWriter.toString()));
}

private void writeRefresh(VaadinResponse response) throws IOException {
Expand Down
Loading
Loading