Initial commit
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>dev.leakfinder</groupId>
|
||||
<artifactId>vaadin-leakfinder-parent</artifactId>
|
||||
<version>0.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>demo-app</artifactId>
|
||||
<name>Vaadin Leak Finder Demo App</name>
|
||||
<description>Vaadin 25 app with deliberate inter-session leaks</description>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.vaadin</groupId>
|
||||
<artifactId>vaadin-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.vaadin</groupId>
|
||||
<artifactId>vaadin-dev</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<!-- Runnable fat jar: mvn -Pproduction package; java -jar target/demo-app-*.jar -->
|
||||
<profile>
|
||||
<id>production</id>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.vaadin</groupId>
|
||||
<artifactId>vaadin-dev</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.vaadin</groupId>
|
||||
<artifactId>vaadin-maven-plugin</artifactId>
|
||||
<version>${vaadin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>prepare-frontend</goal>
|
||||
<goal>build-frontend</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
This file is auto-generated by Vaadin.
|
||||
-->
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<style>
|
||||
html, body, #outlet {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Fix app height for iOS home screen apps:
|
||||
- 100% doesn't cover the entire screen.
|
||||
- 100vh would work on iOS 18 but not on iOS 17 (too tall in Safari, correct in standalone mode).
|
||||
- 100lvh is too tall in Safari. 100svh is too short in standalone apps.
|
||||
- 100dvh doesn't seem to work on first render when launched from home screen (initially like 100svh, then changes to 100lvh when you try to scroll the view).
|
||||
- So, we use 100% for the regular case (Safari and other browsers), and 100lvh for iOS home screen apps. Note, that the `display-mode: standalone` media query only works if the webmanifest defines `"display": "standalone"`.
|
||||
*/
|
||||
@supports (-webkit-touch-callout: none) {
|
||||
@media (display-mode: standalone) {
|
||||
html {
|
||||
height: 100lvh;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!-- index.ts is included here automatically (either by the dev server or during the build) -->
|
||||
</head>
|
||||
<body>
|
||||
<!-- This outlet div is where the views are rendered -->
|
||||
<div id="outlet"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.example.leakdemo;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class Application {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.example.leakdemo;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/** Classic Vaadin broadcaster. Fine by itself, leaks when listeners are never removed. */
|
||||
public final class Broadcaster {
|
||||
|
||||
private static final List<Consumer<String>> LISTENERS = new CopyOnWriteArrayList<>();
|
||||
|
||||
private Broadcaster() {
|
||||
}
|
||||
|
||||
public static Runnable register(Consumer<String> listener) {
|
||||
LISTENERS.add(listener);
|
||||
return () -> LISTENERS.remove(listener);
|
||||
}
|
||||
|
||||
public static void broadcast(String message) {
|
||||
LISTENERS.forEach(l -> l.accept(message));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.example.leakdemo;
|
||||
|
||||
import com.vaadin.flow.component.AttachEvent;
|
||||
import com.vaadin.flow.component.UI;
|
||||
import com.vaadin.flow.component.html.H2;
|
||||
import com.vaadin.flow.component.html.Span;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.router.Route;
|
||||
|
||||
/** LEAK: registers with the static broadcaster on attach but never unregisters on detach. */
|
||||
@Route("broadcaster-leak")
|
||||
public class BroadcasterView extends VerticalLayout {
|
||||
|
||||
public BroadcasterView() {
|
||||
add(new H2("Broadcaster leak"));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttach(AttachEvent attachEvent) {
|
||||
UI ui = attachEvent.getUI();
|
||||
Broadcaster.register(message -> ui.access(() -> add(new Span(message))));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.example.leakdemo;
|
||||
|
||||
import com.vaadin.flow.component.html.Anchor;
|
||||
import com.vaadin.flow.component.html.H1;
|
||||
import com.vaadin.flow.component.html.Paragraph;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.router.Route;
|
||||
|
||||
/** Control view: per-user state only, must produce no findings. */
|
||||
@Route("")
|
||||
public class CleanView extends VerticalLayout {
|
||||
|
||||
private final Paragraph counter = new Paragraph("0");
|
||||
|
||||
public CleanView() {
|
||||
add(new H1("Vaadin leak finder demo"), counter);
|
||||
add(new Anchor("static-leak", "Static list of views"));
|
||||
add(new Anchor("singleton-leak", "Singleton bean holding a grid"));
|
||||
add(new Anchor("broadcaster-leak", "Broadcaster without unregister"));
|
||||
add(new Anchor("shared-component", "Component shared via static field"));
|
||||
add(new Anchor("snoop", "View peeking into another user's view"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.example.leakdemo;
|
||||
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/** LEAK: application wide singleton that caches a UI component. */
|
||||
@Service
|
||||
public class GridCache {
|
||||
|
||||
private Grid<String> lastGrid;
|
||||
|
||||
public void remember(Grid<String> grid) {
|
||||
this.lastGrid = grid;
|
||||
}
|
||||
|
||||
public Grid<String> lastGrid() {
|
||||
return lastGrid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.example.leakdemo;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** Helper endpoints for the end-to-end test. */
|
||||
@RestController
|
||||
public class SessionController {
|
||||
|
||||
@GetMapping("/api/logout")
|
||||
public String logout(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session != null) {
|
||||
session.invalidate();
|
||||
return "invalidated";
|
||||
}
|
||||
return "no session";
|
||||
}
|
||||
|
||||
@GetMapping("/api/broadcast")
|
||||
public String broadcast() {
|
||||
Broadcaster.broadcast("hello");
|
||||
return "sent";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.example.leakdemo;
|
||||
|
||||
import com.vaadin.flow.component.html.H2;
|
||||
import com.vaadin.flow.component.html.Span;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.router.Route;
|
||||
|
||||
/**
|
||||
* LEAK: one component instance for all users. Flow throws "Can't move a node from one state tree to
|
||||
* another" when the second user opens the view; calling {@code removeFromTree()} silences that, and
|
||||
* now every visitor steals the banner from the previous visitor's UI, mutating that user's UI from
|
||||
* the wrong request thread.
|
||||
*/
|
||||
@Route("shared-component")
|
||||
public class SharedComponentView extends VerticalLayout {
|
||||
|
||||
private static final Span BANNER = new Span("Banner of the day");
|
||||
|
||||
public SharedComponentView() {
|
||||
BANNER.getElement().removeFromTree();
|
||||
add(new H2("Shared component"), BANNER);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.example.leakdemo;
|
||||
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.html.H2;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.router.Route;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Route("singleton-leak")
|
||||
public class SingletonLeakView extends VerticalLayout {
|
||||
|
||||
public SingletonLeakView(GridCache cache) {
|
||||
Grid<String> grid = new Grid<>();
|
||||
grid.addColumn(s -> s).setHeader("Item");
|
||||
grid.setItems(List.of("a", "b", "c"));
|
||||
cache.remember(grid);
|
||||
add(new H2("Singleton leak"), grid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.example.leakdemo;
|
||||
|
||||
import com.vaadin.flow.component.Component;
|
||||
import com.vaadin.flow.component.html.H2;
|
||||
import com.vaadin.flow.component.html.Paragraph;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.router.Route;
|
||||
|
||||
/** LEAK: the view of one user keeps a reference to a view of a different user. */
|
||||
@Route("snoop")
|
||||
public class SnoopView extends VerticalLayout {
|
||||
|
||||
private Component peeked;
|
||||
|
||||
public SnoopView() {
|
||||
add(new H2("Snoop"));
|
||||
synchronized (StaticLeakView.RECENT_VIEWS) {
|
||||
if (!StaticLeakView.RECENT_VIEWS.isEmpty()) {
|
||||
peeked = StaticLeakView.RECENT_VIEWS.get(0);
|
||||
}
|
||||
}
|
||||
add(new Paragraph(peeked == null ? "nothing to peek at" : "peeking at " + peeked.getClass().getSimpleName()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.example.leakdemo;
|
||||
|
||||
import com.vaadin.flow.component.Component;
|
||||
import com.vaadin.flow.component.html.H2;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.router.Route;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** LEAK: a static "recently opened views" list keeps every user's view (and its UI) alive forever. */
|
||||
@Route("static-leak")
|
||||
public class StaticLeakView extends VerticalLayout {
|
||||
|
||||
static final List<Component> RECENT_VIEWS = new ArrayList<>();
|
||||
|
||||
public StaticLeakView() {
|
||||
add(new H2("Static leak"));
|
||||
synchronized (RECENT_VIEWS) {
|
||||
RECENT_VIEWS.add(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
server.port=${PORT:8080}
|
||||
vaadin.launch-browser=false
|
||||
spring.main.banner-mode=off
|
||||
logging.level.org.atmosphere=warn
|
||||
Reference in New Issue
Block a user