From 554193def79dbdce910fb450cc5e3f4f56798d93 Mon Sep 17 00:00:00 2001 From: Robert Koch Date: Thu, 16 Mar 2023 15:59:49 +1100 Subject: [PATCH 01/90] bumped version --- .classpath | 81 ++++++++++++++---------- .gitignore | 3 + .settings/org.eclipse.jdt.apt.core.prefs | 2 + .settings/org.eclipse.jdt.core.prefs | 12 +++- pom.xml | 4 +- 5 files changed, 67 insertions(+), 35 deletions(-) create mode 100644 .settings/org.eclipse.jdt.apt.core.prefs diff --git a/.classpath b/.classpath index 212c7fcb..39abf1c5 100644 --- a/.classpath +++ b/.classpath @@ -1,32 +1,49 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore index c80a0911..0d2c1887 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ /application.yml /.project *.gz +*.zip +.vscode/ +.idea/ diff --git a/.settings/org.eclipse.jdt.apt.core.prefs b/.settings/org.eclipse.jdt.apt.core.prefs new file mode 100644 index 00000000..d4313d4b --- /dev/null +++ b/.settings/org.eclipse.jdt.apt.core.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.apt.aptEnabled=false diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs index 91ca62e2..a04717f1 100644 --- a/.settings/org.eclipse.jdt.core.prefs +++ b/.settings/org.eclipse.jdt.core.prefs @@ -1,6 +1,9 @@ eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.nonnull=javax.annotation.Nonnull +org.eclipse.jdt.core.compiler.annotation.nullable=javax.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate +org.eclipse.jdt.core.compiler.codegen.methodParameters=generate org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve org.eclipse.jdt.core.compiler.compliance=1.8 @@ -8,7 +11,14 @@ org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.processAnnotations=disabled org.eclipse.jdt.core.compiler.release=disabled org.eclipse.jdt.core.compiler.source=1.8 diff --git a/pom.xml b/pom.xml index 3d64828b..0414dd69 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ eu.openanalytics shinyproxy - 2.6.1 + 2.7.0 jar ShinyProxy @@ -26,7 +26,7 @@ UTF-8 1.8 - 0.8.11 + 0.9.0 & From c22fb0d7d1c3020865a7f2f1e935440674a46a1d Mon Sep 17 00:00:00 2001 From: Robert Koch Date: Tue, 21 Mar 2023 15:19:50 +1100 Subject: [PATCH 02/90] adds tags as values source --- .settings/org.eclipse.jdt.apt.core.prefs | 4 +++- .settings/org.eclipse.jdt.core.prefs | 2 +- .../shinyproxy/runtimevalues/AppInstanceKey.java | 2 ++ .../openanalytics/shinyproxy/runtimevalues/PublicPathKey.java | 2 ++ .../shinyproxy/runtimevalues/ShinyForceFullReloadKey.java | 2 ++ .../openanalytics/shinyproxy/runtimevalues/TrackAppUrl.java | 2 ++ .../runtimevalues/WebSocketReconnectionModeKey.java | 2 ++ 7 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.settings/org.eclipse.jdt.apt.core.prefs b/.settings/org.eclipse.jdt.apt.core.prefs index d4313d4b..dfa4f3ad 100644 --- a/.settings/org.eclipse.jdt.apt.core.prefs +++ b/.settings/org.eclipse.jdt.apt.core.prefs @@ -1,2 +1,4 @@ eclipse.preferences.version=1 -org.eclipse.jdt.apt.aptEnabled=false +org.eclipse.jdt.apt.aptEnabled=true +org.eclipse.jdt.apt.genSrcDir=target/generated-sources/annotations +org.eclipse.jdt.apt.genTestSrcDir=target/generated-test-sources/test-annotations diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs index a04717f1..cdddbe5d 100644 --- a/.settings/org.eclipse.jdt.core.prefs +++ b/.settings/org.eclipse.jdt.core.prefs @@ -19,6 +19,6 @@ org.eclipse.jdt.core.compiler.problem.nullReference=warning org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore -org.eclipse.jdt.core.compiler.processAnnotations=disabled +org.eclipse.jdt.core.compiler.processAnnotations=enabled org.eclipse.jdt.core.compiler.release=disabled org.eclipse.jdt.core.compiler.source=1.8 diff --git a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/AppInstanceKey.java b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/AppInstanceKey.java index cffd6c17..1bcc0e72 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/AppInstanceKey.java +++ b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/AppInstanceKey.java @@ -27,8 +27,10 @@ public class AppInstanceKey extends RuntimeValueKey { public AppInstanceKey() { super("openanalytics.eu/sp-app-instance", "SHINYPROXY_APP_INSTANCE", + "openanalytics.eu/sp-app-instance", false, true, // include as annotation so that the value can be recovered + true, false, true, true, diff --git a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/PublicPathKey.java b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/PublicPathKey.java index fd74577c..d023e123 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/PublicPathKey.java +++ b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/PublicPathKey.java @@ -27,11 +27,13 @@ public class PublicPathKey extends RuntimeValueKey { public PublicPathKey() { super("openanalytics.eu/sp-public-path", "SHINYPROXY_PUBLIC_PATH", + "openanalytics.eu/sp-public-path", false, true, // include as annotation so that the value can be recovered true, true, true, + true, false, String.class); } diff --git a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/ShinyForceFullReloadKey.java b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/ShinyForceFullReloadKey.java index af290d5e..7207f4b1 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/ShinyForceFullReloadKey.java +++ b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/ShinyForceFullReloadKey.java @@ -28,8 +28,10 @@ public class ShinyForceFullReloadKey extends RuntimeValueKey { public ShinyForceFullReloadKey() { super("openanalytics.eu/sp-shiny-force-full-reload", "SHINYPROXY_FORCE_FULL_RELOAD", + "openanalytics.eu/sp-shiny-force-full-reload", false, true, + true, false, true, true, diff --git a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/TrackAppUrl.java b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/TrackAppUrl.java index 31142a84..5657a801 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/TrackAppUrl.java +++ b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/TrackAppUrl.java @@ -28,8 +28,10 @@ public class TrackAppUrl extends RuntimeValueKey { public TrackAppUrl() { super("openanalytics.eu/sp-track-app-url", "SHINYPROXY_TRACK_APP_URL", + "openanalytics.eu/sp-track-app-url", false, true, + true, false, true, true, diff --git a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/WebSocketReconnectionModeKey.java b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/WebSocketReconnectionModeKey.java index f087d8dd..ac19b7fe 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/WebSocketReconnectionModeKey.java +++ b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/WebSocketReconnectionModeKey.java @@ -28,8 +28,10 @@ public class WebSocketReconnectionModeKey extends RuntimeValueKey Date: Fri, 24 Mar 2023 13:17:28 +1100 Subject: [PATCH 03/90] adds task definition to spec --- .../openanalytics/shinyproxy/ShinyProxySpecProvider.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/eu/openanalytics/shinyproxy/ShinyProxySpecProvider.java b/src/main/java/eu/openanalytics/shinyproxy/ShinyProxySpecProvider.java index beec9805..97f18b6d 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/ShinyProxySpecProvider.java +++ b/src/main/java/eu/openanalytics/shinyproxy/ShinyProxySpecProvider.java @@ -244,6 +244,14 @@ public void setDescription(String description) { proxySpec.description(description); } + public SpelField.String getContainerTaskDefinition() { + return containerSpec.build().getTaskDefinition(); + } + + public void setContainerTaskDefinition(SpelField.String taskDefinition) { + containerSpec.taskDefinition(taskDefinition); + } + public String getLogoURL() { return proxySpec.build().getLogoURL(); } From eaae58f6e4217f43823db33163b1099e3968bb3d Mon Sep 17 00:00:00 2001 From: Tobia De Koninck Date: Mon, 17 Jul 2023 16:35:15 +0200 Subject: [PATCH 04/90] Ref #29272: upgrade to JDK 17 + corresponding code improvements --- pom.xml | 6 +++--- .../shinyproxy/ShinyProxyIframeScriptInjector.java | 12 +++++++----- .../shinyproxy/ShinyProxySpecExtensionProvider.java | 4 +--- .../shinyproxy/ShinyProxySpecProvider.java | 2 +- .../shinyproxy/controllers/AdminController.java | 3 +-- .../shinyproxy/controllers/AppDirectController.java | 1 - .../shinyproxy/controllers/IndexController.java | 3 +-- .../shinyproxy/controllers/IssueController.java | 2 +- .../shinyproxy/runtimevalues/AppInstanceKey.java | 2 +- .../shinyproxy/runtimevalues/PublicPathKey.java | 2 +- .../runtimevalues/ShinyForceFullReloadKey.java | 2 +- .../shinyproxy/runtimevalues/TrackAppUrl.java | 2 +- .../shinyproxy/runtimevalues/UserTimeZoneKey.java | 2 +- .../runtimevalues/WebSocketReconnectionModeKey.java | 2 +- 14 files changed, 21 insertions(+), 24 deletions(-) diff --git a/pom.xml b/pom.xml index 6b742746..90e2a725 100644 --- a/pom.xml +++ b/pom.xml @@ -25,9 +25,9 @@ UTF-8 - 1.8 - 8 - 8 + 17 + 17 + 17 1.0.2-SNAPSHOT & diff --git a/src/main/java/eu/openanalytics/shinyproxy/ShinyProxyIframeScriptInjector.java b/src/main/java/eu/openanalytics/shinyproxy/ShinyProxyIframeScriptInjector.java index e3739fe6..152dfbbe 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/ShinyProxyIframeScriptInjector.java +++ b/src/main/java/eu/openanalytics/shinyproxy/ShinyProxyIframeScriptInjector.java @@ -21,7 +21,6 @@ package eu.openanalytics.shinyproxy; import eu.openanalytics.containerproxy.util.ContextPathHelper; -import io.netty.buffer.ByteBuf; import io.undertow.UndertowMessages; import io.undertow.server.HttpServerExchange; import io.undertow.server.protocol.http.ServerFixedLengthStreamSinkConduit; @@ -114,16 +113,19 @@ public long writeFinal(java.nio.ByteBuffer[] srcs, int offs, int len) throws IOE @Override public void terminateWrites() throws IOException { + ByteBuffer out; // 1. check whether it's a html response and success if (exchange.getStatusCode() == HttpStatus.OK.value() && exchange.getResponseHeaders().get("Content-Type") != null && exchange.getResponseHeaders().get("Content-Type").stream().anyMatch(headerValue -> headerValue.contains("text/html"))) { // 2. inject script - String r = ""; - outputStream.write(r.getBytes(StandardCharsets.UTF_8)); + String r = outputStream.toString(StandardCharsets.UTF_8); + r += ""; + out = ByteBuffer.wrap(r.getBytes(StandardCharsets.UTF_8)); + } else { + // 2. read bytes + out = ByteBuffer.wrap(outputStream.toByteArray()); } - - ByteBuffer out = ByteBuffer.wrap(outputStream.toByteArray()); // 3. set Content-Length header updateContentLength(exchange, out); // 4. write new response (to the next stream) diff --git a/src/main/java/eu/openanalytics/shinyproxy/ShinyProxySpecExtensionProvider.java b/src/main/java/eu/openanalytics/shinyproxy/ShinyProxySpecExtensionProvider.java index 39ed2cdb..b482b72c 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/ShinyProxySpecExtensionProvider.java +++ b/src/main/java/eu/openanalytics/shinyproxy/ShinyProxySpecExtensionProvider.java @@ -40,9 +40,7 @@ public class ShinyProxySpecExtensionProvider { @PostConstruct public void postInit() { - specs.forEach(specExtension -> { - proxySpecProvider.getSpec(specExtension.getId()).addSpecExtension(specExtension); - }); + specs.forEach(specExtension -> proxySpecProvider.getSpec(specExtension.getId()).addSpecExtension(specExtension)); } public void setSpecs(List specs) { diff --git a/src/main/java/eu/openanalytics/shinyproxy/ShinyProxySpecProvider.java b/src/main/java/eu/openanalytics/shinyproxy/ShinyProxySpecProvider.java index beec9805..b42a4f0a 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/ShinyProxySpecProvider.java +++ b/src/main/java/eu/openanalytics/shinyproxy/ShinyProxySpecProvider.java @@ -110,7 +110,7 @@ public ProxySpec getSpec(String id) { } public void setSpecs(List specs) { - this.specs = specs.stream().map(ShinyProxySpec::getProxySpec).collect(Collectors.toList()); + this.specs = specs.stream().map(ShinyProxySpec::getProxySpec).toList(); } public void setTemplateGroups(List templateGroups) { diff --git a/src/main/java/eu/openanalytics/shinyproxy/controllers/AdminController.java b/src/main/java/eu/openanalytics/shinyproxy/controllers/AdminController.java index dbea948e..bdaa16da 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/controllers/AdminController.java +++ b/src/main/java/eu/openanalytics/shinyproxy/controllers/AdminController.java @@ -48,7 +48,6 @@ import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; import java.util.List; -import java.util.stream.Collectors; @Controller public class AdminController extends BaseController { @@ -88,7 +87,7 @@ private String admin(ModelMap map, HttpServletRequest request) { @ResponseBody private ResponseEntity>> adminData() { List proxies = proxyService.getProxies(null, false); - List proxyInfos = proxies.stream().map(ProxyInfo::new).collect(Collectors.toList()); + List proxyInfos = proxies.stream().map(ProxyInfo::new).toList(); return ApiResponse.success(proxyInfos); } diff --git a/src/main/java/eu/openanalytics/shinyproxy/controllers/AppDirectController.java b/src/main/java/eu/openanalytics/shinyproxy/controllers/AppDirectController.java index 668d53ea..b7d98c55 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/controllers/AppDirectController.java +++ b/src/main/java/eu/openanalytics/shinyproxy/controllers/AppDirectController.java @@ -35,7 +35,6 @@ import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.servlet.ModelAndView; import javax.inject.Inject; import javax.servlet.RequestDispatcher; diff --git a/src/main/java/eu/openanalytics/shinyproxy/controllers/IndexController.java b/src/main/java/eu/openanalytics/shinyproxy/controllers/IndexController.java index 20c0cc0c..4ffcef4f 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/controllers/IndexController.java +++ b/src/main/java/eu/openanalytics/shinyproxy/controllers/IndexController.java @@ -36,7 +36,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; @Controller public class IndexController extends BaseController { @@ -90,7 +89,7 @@ private Object index(ModelMap map, HttpServletRequest request) { } } - List templateGroups = shinyProxySpecProvider.getTemplateGroups().stream().filter((g) -> groupedApps.containsKey(g.getId())).collect(Collectors.toList()); + List templateGroups = shinyProxySpecProvider.getTemplateGroups().stream().filter((g) -> groupedApps.containsKey(g.getId())).toList(); map.put("templateGroups", templateGroups); map.put("groupedApps", groupedApps); map.put("ungroupedApps", ungroupedApps); diff --git a/src/main/java/eu/openanalytics/shinyproxy/controllers/IssueController.java b/src/main/java/eu/openanalytics/shinyproxy/controllers/IssueController.java index e63596ab..8b15708a 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/controllers/IssueController.java +++ b/src/main/java/eu/openanalytics/shinyproxy/controllers/IssueController.java @@ -70,7 +70,7 @@ public ResponseEntity> postIssue(HttpServletRequest requ } sendSupportMail(form, activeProxy); - return ResponseEntity.ok(new HashMap() {{ + return ResponseEntity.ok(new HashMap<>() {{ put("status", "success"); }}); } diff --git a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/AppInstanceKey.java b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/AppInstanceKey.java index cffd6c17..b4c2a2f9 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/AppInstanceKey.java +++ b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/AppInstanceKey.java @@ -36,7 +36,7 @@ public AppInstanceKey() { String.class); } - public static AppInstanceKey inst = new AppInstanceKey(); + public static final AppInstanceKey inst = new AppInstanceKey(); @Override public String deserializeFromString(String value) { diff --git a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/PublicPathKey.java b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/PublicPathKey.java index fd74577c..b2e29c75 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/PublicPathKey.java +++ b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/PublicPathKey.java @@ -36,7 +36,7 @@ public PublicPathKey() { String.class); } - public static PublicPathKey inst = new PublicPathKey(); + public static final PublicPathKey inst = new PublicPathKey(); @Override public String deserializeFromString(String value) { diff --git a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/ShinyForceFullReloadKey.java b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/ShinyForceFullReloadKey.java index af290d5e..87f238c8 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/ShinyForceFullReloadKey.java +++ b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/ShinyForceFullReloadKey.java @@ -37,7 +37,7 @@ public ShinyForceFullReloadKey() { Boolean.class); } - public static ShinyForceFullReloadKey inst = new ShinyForceFullReloadKey(); + public static final ShinyForceFullReloadKey inst = new ShinyForceFullReloadKey(); @Override public Boolean deserializeFromString(String value) { diff --git a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/TrackAppUrl.java b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/TrackAppUrl.java index 31142a84..5bd259c7 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/TrackAppUrl.java +++ b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/TrackAppUrl.java @@ -37,7 +37,7 @@ public TrackAppUrl() { Boolean.class); } - public static TrackAppUrl inst = new TrackAppUrl(); + public static final TrackAppUrl inst = new TrackAppUrl(); @Override public Boolean deserializeFromString(String value) { diff --git a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/UserTimeZoneKey.java b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/UserTimeZoneKey.java index 9b072df7..6996c053 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/UserTimeZoneKey.java +++ b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/UserTimeZoneKey.java @@ -36,7 +36,7 @@ public UserTimeZoneKey() { String.class); } - public static UserTimeZoneKey inst = new UserTimeZoneKey(); + public static final UserTimeZoneKey inst = new UserTimeZoneKey(); @Override public String deserializeFromString(String value) { diff --git a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/WebSocketReconnectionModeKey.java b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/WebSocketReconnectionModeKey.java index f087d8dd..058c55b7 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/WebSocketReconnectionModeKey.java +++ b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/WebSocketReconnectionModeKey.java @@ -37,7 +37,7 @@ public WebSocketReconnectionModeKey() { WebsocketReconnectionMode.class); } - public static WebSocketReconnectionModeKey inst = new WebSocketReconnectionModeKey(); + public static final WebSocketReconnectionModeKey inst = new WebSocketReconnectionModeKey(); @Override public WebsocketReconnectionMode deserializeFromString(String value) { From 04ec79a8038818ca63e2f7b1f25e2a2b65bd19a4 Mon Sep 17 00:00:00 2001 From: Tobia De Koninck Date: Mon, 17 Jul 2023 16:51:38 +0200 Subject: [PATCH 05/90] Ref #29272: add editorconfig --- .editorconfig | 529 +++++++++ .../AuthenticationRequiredFilter.java | 5 +- .../shinyproxy/ShinyProxyConfiguration.java | 90 +- .../ShinyProxyIframeScriptInjector.java | 6 +- .../ShinyProxySpecExtensionProvider.java | 8 +- .../shinyproxy/ShinyProxySpecProvider.java | 1014 ++++++++--------- .../shinyproxy/ShinyProxyTestStrategy.java | 92 +- .../controllers/AdminController.java | 406 +++---- .../shinyproxy/controllers/AppController.java | 945 ++++++++------- .../controllers/AppDirectController.java | 2 +- .../controllers/BaseController.java | 256 ++--- .../controllers/HeartbeatController.java | 2 +- .../controllers/IndexController.java | 120 +- .../controllers/IssueController.java | 247 ++-- .../runtimevalues/AppInstanceKey.java | 6 +- .../runtimevalues/PublicPathKey.java | 6 +- .../ShinyForceFullReloadKey.java | 4 +- .../shinyproxy/runtimevalues/TrackAppUrl.java | 4 +- .../runtimevalues/UserTimeZoneKey.java | 6 +- .../WebSocketReconnectionModeKey.java | 4 +- src/main/resources/application-demo.yml | 45 +- src/main/resources/static/css/default.css | 527 ++++----- .../static/handlebars/app_details.handlebars | 14 +- .../static/handlebars/my_apps.handlebars | 5 +- .../handlebars/switch_instances.handlebars | 34 +- src/main/resources/static/js/shiny.api.js | 6 +- src/main/resources/static/js/shiny.app.js | 8 +- src/main/resources/static/js/shiny.common.js | 9 +- .../resources/static/js/shiny.connections.js | 11 +- src/main/resources/static/js/shiny.iframe.js | 6 +- .../resources/static/js/shiny.instances.js | 2 +- src/main/resources/templates/admin.html | 191 ++-- src/main/resources/templates/app.html | 368 +++--- .../resources/templates/fragments/modal.html | 124 +- .../resources/templates/fragments/navbar.html | 198 ++-- src/main/resources/templates/index.html | 170 +-- 36 files changed, 3016 insertions(+), 2454 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..12b2c2f2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,529 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +max_line_length = 120 +tab_width = 4 +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = true +ij_smart_tabs = false +ij_visual_guides = none +ij_wrap_on_typing = false + +[*.css] +ij_css_align_closing_brace_with_properties = false +ij_css_blank_lines_around_nested_selector = 1 +ij_css_blank_lines_between_blocks = 1 +ij_css_block_comment_add_space = false +ij_css_brace_placement = end_of_line +ij_css_enforce_quotes_on_format = false +ij_css_hex_color_long_format = false +ij_css_hex_color_lower_case = false +ij_css_hex_color_short_format = false +ij_css_hex_color_upper_case = false +ij_css_keep_blank_lines_in_code = 2 +ij_css_keep_indents_on_empty_lines = false +ij_css_keep_single_line_blocks = false +ij_css_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_css_space_after_colon = true +ij_css_space_before_opening_brace = true +ij_css_use_double_quotes = true +ij_css_value_alignment = do_not_align + +[*.java] +max_line_length = 240 +ij_java_align_consecutive_assignments = false +ij_java_align_consecutive_variable_declarations = false +ij_java_align_group_field_declarations = false +ij_java_align_multiline_annotation_parameters = false +ij_java_align_multiline_array_initializer_expression = false +ij_java_align_multiline_assignment = false +ij_java_align_multiline_binary_operation = false +ij_java_align_multiline_chained_methods = false +ij_java_align_multiline_deconstruction_list_components = true +ij_java_align_multiline_extends_list = false +ij_java_align_multiline_for = true +ij_java_align_multiline_method_parentheses = false +ij_java_align_multiline_parameters = true +ij_java_align_multiline_parameters_in_calls = false +ij_java_align_multiline_parenthesized_expression = false +ij_java_align_multiline_records = true +ij_java_align_multiline_resources = true +ij_java_align_multiline_ternary_operation = false +ij_java_align_multiline_text_blocks = false +ij_java_align_multiline_throws_list = false +ij_java_align_subsequent_simple_methods = false +ij_java_align_throws_keyword = false +ij_java_align_types_in_multi_catch = true +ij_java_annotation_parameter_wrap = off +ij_java_array_initializer_new_line_after_left_brace = false +ij_java_array_initializer_right_brace_on_new_line = false +ij_java_array_initializer_wrap = off +ij_java_assert_statement_colon_on_next_line = false +ij_java_assert_statement_wrap = off +ij_java_assignment_wrap = off +ij_java_binary_operation_sign_on_next_line = false +ij_java_binary_operation_wrap = off +ij_java_blank_lines_after_anonymous_class_header = 0 +ij_java_blank_lines_after_class_header = 0 +ij_java_blank_lines_after_imports = 1 +ij_java_blank_lines_after_package = 1 +ij_java_blank_lines_around_class = 1 +ij_java_blank_lines_around_field = 0 +ij_java_blank_lines_around_field_in_interface = 0 +ij_java_blank_lines_around_initializer = 1 +ij_java_blank_lines_around_method = 1 +ij_java_blank_lines_around_method_in_interface = 1 +ij_java_blank_lines_before_class_end = 0 +ij_java_blank_lines_before_imports = 1 +ij_java_blank_lines_before_method_body = 0 +ij_java_blank_lines_before_package = 0 +ij_java_block_brace_style = end_of_line +ij_java_block_comment_add_space = false +ij_java_block_comment_at_first_column = true +ij_java_builder_methods = none +ij_java_call_parameters_new_line_after_left_paren = false +ij_java_call_parameters_right_paren_on_new_line = false +ij_java_call_parameters_wrap = off +ij_java_case_statement_on_separate_line = true +ij_java_catch_on_new_line = false +ij_java_class_annotation_wrap = split_into_lines +ij_java_class_brace_style = end_of_line +ij_java_class_count_to_use_import_on_demand = 5000 +ij_java_class_names_in_javadoc = 1 +ij_java_deconstruction_list_wrap = normal +ij_java_do_not_indent_top_level_class_members = false +ij_java_do_not_wrap_after_single_annotation = false +ij_java_do_not_wrap_after_single_annotation_in_parameter = false +ij_java_do_while_brace_force = never +ij_java_doc_add_blank_line_after_description = true +ij_java_doc_add_blank_line_after_param_comments = false +ij_java_doc_add_blank_line_after_return = false +ij_java_doc_add_p_tag_on_empty_lines = false +ij_java_doc_align_exception_comments = true +ij_java_doc_align_param_comments = true +ij_java_doc_do_not_wrap_if_one_line = false +ij_java_doc_enable_formatting = true +ij_java_doc_enable_leading_asterisks = true +ij_java_doc_indent_on_continuation = false +ij_java_doc_keep_empty_lines = true +ij_java_doc_keep_empty_parameter_tag = true +ij_java_doc_keep_empty_return_tag = true +ij_java_doc_keep_empty_throws_tag = true +ij_java_doc_keep_invalid_tags = true +ij_java_doc_param_description_on_new_line = false +ij_java_doc_preserve_line_breaks = false +ij_java_doc_use_throws_not_exception_tag = true +ij_java_else_on_new_line = false +ij_java_entity_dd_suffix = EJB +ij_java_entity_eb_suffix = Bean +ij_java_entity_hi_suffix = Home +ij_java_entity_lhi_prefix = Local +ij_java_entity_lhi_suffix = Home +ij_java_entity_li_prefix = Local +ij_java_entity_pk_class = java.lang.String +ij_java_entity_vo_suffix = VO +ij_java_enum_constants_wrap = off +ij_java_extends_keyword_wrap = off +ij_java_extends_list_wrap = off +ij_java_field_annotation_wrap = split_into_lines +ij_java_finally_on_new_line = false +ij_java_for_brace_force = never +ij_java_for_statement_new_line_after_left_paren = false +ij_java_for_statement_right_paren_on_new_line = false +ij_java_for_statement_wrap = off +ij_java_generate_final_locals = false +ij_java_generate_final_parameters = false +ij_java_if_brace_force = never +ij_java_imports_layout = *,|,javax.**,java.**,|,$* +ij_java_indent_case_from_switch = true +ij_java_insert_inner_class_imports = false +ij_java_insert_override_annotation = true +ij_java_keep_blank_lines_before_right_brace = 2 +ij_java_keep_blank_lines_between_package_declaration_and_header = 2 +ij_java_keep_blank_lines_in_code = 2 +ij_java_keep_blank_lines_in_declarations = 2 +ij_java_keep_builder_methods_indents = false +ij_java_keep_control_statement_in_one_line = true +ij_java_keep_first_column_comment = true +ij_java_keep_indents_on_empty_lines = false +ij_java_keep_line_breaks = true +ij_java_keep_multiple_expressions_in_one_line = false +ij_java_keep_simple_blocks_in_one_line = false +ij_java_keep_simple_classes_in_one_line = false +ij_java_keep_simple_lambdas_in_one_line = false +ij_java_keep_simple_methods_in_one_line = false +ij_java_label_indent_absolute = false +ij_java_label_indent_size = 0 +ij_java_lambda_brace_style = end_of_line +ij_java_layout_static_imports_separately = true +ij_java_line_comment_add_space = false +ij_java_line_comment_add_space_on_reformat = false +ij_java_line_comment_at_first_column = true +ij_java_message_dd_suffix = EJB +ij_java_message_eb_suffix = Bean +ij_java_method_annotation_wrap = split_into_lines +ij_java_method_brace_style = end_of_line +ij_java_method_call_chain_wrap = off +ij_java_method_parameters_new_line_after_left_paren = false +ij_java_method_parameters_right_paren_on_new_line = false +ij_java_method_parameters_wrap = off +ij_java_modifier_list_wrap = false +ij_java_multi_catch_types_wrap = normal +ij_java_names_count_to_use_import_on_demand = 39999 +ij_java_new_line_after_lparen_in_annotation = false +ij_java_new_line_after_lparen_in_deconstruction_pattern = true +ij_java_new_line_after_lparen_in_record_header = false +ij_java_packages_to_use_import_on_demand = java.awt.*,javax.swing.* +ij_java_parameter_annotation_wrap = off +ij_java_parentheses_expression_new_line_after_left_paren = false +ij_java_parentheses_expression_right_paren_on_new_line = false +ij_java_place_assignment_sign_on_next_line = false +ij_java_prefer_longer_names = true +ij_java_prefer_parameters_wrap = false +ij_java_record_components_wrap = normal +ij_java_repeat_synchronized = true +ij_java_replace_instanceof_and_cast = false +ij_java_replace_null_check = true +ij_java_replace_sum_lambda_with_method_ref = true +ij_java_resource_list_new_line_after_left_paren = false +ij_java_resource_list_right_paren_on_new_line = false +ij_java_resource_list_wrap = off +ij_java_rparen_on_new_line_in_annotation = false +ij_java_rparen_on_new_line_in_deconstruction_pattern = true +ij_java_rparen_on_new_line_in_record_header = false +ij_java_session_dd_suffix = EJB +ij_java_session_eb_suffix = Bean +ij_java_session_hi_suffix = Home +ij_java_session_lhi_prefix = Local +ij_java_session_lhi_suffix = Home +ij_java_session_li_prefix = Local +ij_java_session_si_suffix = Service +ij_java_space_after_closing_angle_bracket_in_type_argument = false +ij_java_space_after_colon = true +ij_java_space_after_comma = true +ij_java_space_after_comma_in_type_arguments = true +ij_java_space_after_for_semicolon = true +ij_java_space_after_quest = true +ij_java_space_after_type_cast = true +ij_java_space_before_annotation_array_initializer_left_brace = false +ij_java_space_before_annotation_parameter_list = false +ij_java_space_before_array_initializer_left_brace = false +ij_java_space_before_catch_keyword = true +ij_java_space_before_catch_left_brace = true +ij_java_space_before_catch_parentheses = true +ij_java_space_before_class_left_brace = true +ij_java_space_before_colon = true +ij_java_space_before_colon_in_foreach = true +ij_java_space_before_comma = false +ij_java_space_before_deconstruction_list = false +ij_java_space_before_do_left_brace = true +ij_java_space_before_else_keyword = true +ij_java_space_before_else_left_brace = true +ij_java_space_before_finally_keyword = true +ij_java_space_before_finally_left_brace = true +ij_java_space_before_for_left_brace = true +ij_java_space_before_for_parentheses = true +ij_java_space_before_for_semicolon = false +ij_java_space_before_if_left_brace = true +ij_java_space_before_if_parentheses = true +ij_java_space_before_method_call_parentheses = false +ij_java_space_before_method_left_brace = true +ij_java_space_before_method_parentheses = false +ij_java_space_before_opening_angle_bracket_in_type_parameter = false +ij_java_space_before_quest = true +ij_java_space_before_switch_left_brace = true +ij_java_space_before_switch_parentheses = true +ij_java_space_before_synchronized_left_brace = true +ij_java_space_before_synchronized_parentheses = true +ij_java_space_before_try_left_brace = true +ij_java_space_before_try_parentheses = true +ij_java_space_before_type_parameter_list = false +ij_java_space_before_while_keyword = true +ij_java_space_before_while_left_brace = true +ij_java_space_before_while_parentheses = true +ij_java_space_inside_one_line_enum_braces = false +ij_java_space_within_empty_array_initializer_braces = false +ij_java_space_within_empty_method_call_parentheses = false +ij_java_space_within_empty_method_parentheses = false +ij_java_spaces_around_additive_operators = true +ij_java_spaces_around_annotation_eq = true +ij_java_spaces_around_assignment_operators = true +ij_java_spaces_around_bitwise_operators = true +ij_java_spaces_around_equality_operators = true +ij_java_spaces_around_lambda_arrow = true +ij_java_spaces_around_logical_operators = true +ij_java_spaces_around_method_ref_dbl_colon = false +ij_java_spaces_around_multiplicative_operators = true +ij_java_spaces_around_relational_operators = true +ij_java_spaces_around_shift_operators = true +ij_java_spaces_around_type_bounds_in_type_parameters = true +ij_java_spaces_around_unary_operator = false +ij_java_spaces_within_angle_brackets = false +ij_java_spaces_within_annotation_parentheses = false +ij_java_spaces_within_array_initializer_braces = false +ij_java_spaces_within_braces = false +ij_java_spaces_within_brackets = false +ij_java_spaces_within_cast_parentheses = false +ij_java_spaces_within_catch_parentheses = false +ij_java_spaces_within_deconstruction_list = false +ij_java_spaces_within_for_parentheses = false +ij_java_spaces_within_if_parentheses = false +ij_java_spaces_within_method_call_parentheses = false +ij_java_spaces_within_method_parentheses = false +ij_java_spaces_within_parentheses = false +ij_java_spaces_within_record_header = false +ij_java_spaces_within_switch_parentheses = false +ij_java_spaces_within_synchronized_parentheses = false +ij_java_spaces_within_try_parentheses = false +ij_java_spaces_within_while_parentheses = false +ij_java_special_else_if_treatment = true +ij_java_subclass_name_suffix = Impl +ij_java_ternary_operation_signs_on_next_line = false +ij_java_ternary_operation_wrap = off +ij_java_test_name_suffix = Test +ij_java_throws_keyword_wrap = off +ij_java_throws_list_wrap = off +ij_java_use_external_annotations = false +ij_java_use_fq_class_names = false +ij_java_use_relative_indents = false +ij_java_use_single_class_imports = true +ij_java_variable_annotation_wrap = off +ij_java_visibility = public +ij_java_while_brace_force = never +ij_java_while_on_new_line = false +ij_java_wrap_comments = false +ij_java_wrap_first_method_in_call_chain = false +ij_java_wrap_long_lines = true + +[{*.cjs,*.js}] +ij_continuation_indent_size = 4 +ij_javascript_align_imports = false +ij_javascript_align_multiline_array_initializer_expression = false +ij_javascript_align_multiline_binary_operation = false +ij_javascript_align_multiline_chained_methods = false +ij_javascript_align_multiline_extends_list = false +ij_javascript_align_multiline_for = true +ij_javascript_align_multiline_parameters = true +ij_javascript_align_multiline_parameters_in_calls = false +ij_javascript_align_multiline_ternary_operation = false +ij_javascript_align_object_properties = 0 +ij_javascript_align_union_types = false +ij_javascript_align_var_statements = 0 +ij_javascript_array_initializer_new_line_after_left_brace = false +ij_javascript_array_initializer_right_brace_on_new_line = false +ij_javascript_array_initializer_wrap = off +ij_javascript_assignment_wrap = off +ij_javascript_binary_operation_sign_on_next_line = false +ij_javascript_binary_operation_wrap = off +ij_javascript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** +ij_javascript_blank_lines_after_imports = 1 +ij_javascript_blank_lines_around_class = 1 +ij_javascript_blank_lines_around_field = 0 +ij_javascript_blank_lines_around_function = 1 +ij_javascript_blank_lines_around_method = 1 +ij_javascript_block_brace_style = end_of_line +ij_javascript_block_comment_add_space = false +ij_javascript_block_comment_at_first_column = true +ij_javascript_call_parameters_new_line_after_left_paren = false +ij_javascript_call_parameters_right_paren_on_new_line = false +ij_javascript_call_parameters_wrap = off +ij_javascript_catch_on_new_line = false +ij_javascript_chained_call_dot_on_new_line = true +ij_javascript_class_brace_style = end_of_line +ij_javascript_comma_on_new_line = false +ij_javascript_do_while_brace_force = never +ij_javascript_else_on_new_line = false +ij_javascript_enforce_trailing_comma = keep +ij_javascript_extends_keyword_wrap = off +ij_javascript_extends_list_wrap = off +ij_javascript_field_prefix = _ +ij_javascript_file_name_style = relaxed +ij_javascript_finally_on_new_line = false +ij_javascript_for_brace_force = never +ij_javascript_for_statement_new_line_after_left_paren = false +ij_javascript_for_statement_right_paren_on_new_line = false +ij_javascript_for_statement_wrap = off +ij_javascript_force_quote_style = false +ij_javascript_force_semicolon_style = false +ij_javascript_function_expression_brace_style = end_of_line +ij_javascript_if_brace_force = never +ij_javascript_import_merge_members = global +ij_javascript_import_prefer_absolute_path = global +ij_javascript_import_sort_members = true +ij_javascript_import_sort_module_name = false +ij_javascript_import_use_node_resolution = true +ij_javascript_imports_wrap = on_every_item +ij_javascript_indent_case_from_switch = true +ij_javascript_indent_chained_calls = true +ij_javascript_indent_package_children = 0 +ij_javascript_jsx_attribute_value = braces +ij_javascript_keep_blank_lines_in_code = 2 +ij_javascript_keep_first_column_comment = true +ij_javascript_keep_indents_on_empty_lines = false +ij_javascript_keep_line_breaks = true +ij_javascript_keep_simple_blocks_in_one_line = false +ij_javascript_keep_simple_methods_in_one_line = false +ij_javascript_line_comment_add_space = true +ij_javascript_line_comment_at_first_column = false +ij_javascript_method_brace_style = end_of_line +ij_javascript_method_call_chain_wrap = off +ij_javascript_method_parameters_new_line_after_left_paren = false +ij_javascript_method_parameters_right_paren_on_new_line = false +ij_javascript_method_parameters_wrap = off +ij_javascript_object_literal_wrap = on_every_item +ij_javascript_object_types_wrap = on_every_item +ij_javascript_parentheses_expression_new_line_after_left_paren = false +ij_javascript_parentheses_expression_right_paren_on_new_line = false +ij_javascript_place_assignment_sign_on_next_line = false +ij_javascript_prefer_as_type_cast = false +ij_javascript_prefer_explicit_types_function_expression_returns = false +ij_javascript_prefer_explicit_types_function_returns = false +ij_javascript_prefer_explicit_types_vars_fields = false +ij_javascript_prefer_parameters_wrap = false +ij_javascript_reformat_c_style_comments = false +ij_javascript_space_after_colon = true +ij_javascript_space_after_comma = true +ij_javascript_space_after_dots_in_rest_parameter = false +ij_javascript_space_after_generator_mult = true +ij_javascript_space_after_property_colon = true +ij_javascript_space_after_quest = true +ij_javascript_space_after_type_colon = true +ij_javascript_space_after_unary_not = false +ij_javascript_space_before_async_arrow_lparen = true +ij_javascript_space_before_catch_keyword = true +ij_javascript_space_before_catch_left_brace = true +ij_javascript_space_before_catch_parentheses = true +ij_javascript_space_before_class_lbrace = true +ij_javascript_space_before_class_left_brace = true +ij_javascript_space_before_colon = true +ij_javascript_space_before_comma = false +ij_javascript_space_before_do_left_brace = true +ij_javascript_space_before_else_keyword = true +ij_javascript_space_before_else_left_brace = true +ij_javascript_space_before_finally_keyword = true +ij_javascript_space_before_finally_left_brace = true +ij_javascript_space_before_for_left_brace = true +ij_javascript_space_before_for_parentheses = true +ij_javascript_space_before_for_semicolon = false +ij_javascript_space_before_function_left_parenth = true +ij_javascript_space_before_generator_mult = false +ij_javascript_space_before_if_left_brace = true +ij_javascript_space_before_if_parentheses = true +ij_javascript_space_before_method_call_parentheses = false +ij_javascript_space_before_method_left_brace = true +ij_javascript_space_before_method_parentheses = false +ij_javascript_space_before_property_colon = false +ij_javascript_space_before_quest = true +ij_javascript_space_before_switch_left_brace = true +ij_javascript_space_before_switch_parentheses = true +ij_javascript_space_before_try_left_brace = true +ij_javascript_space_before_type_colon = false +ij_javascript_space_before_unary_not = false +ij_javascript_space_before_while_keyword = true +ij_javascript_space_before_while_left_brace = true +ij_javascript_space_before_while_parentheses = true +ij_javascript_spaces_around_additive_operators = true +ij_javascript_spaces_around_arrow_function_operator = true +ij_javascript_spaces_around_assignment_operators = true +ij_javascript_spaces_around_bitwise_operators = true +ij_javascript_spaces_around_equality_operators = true +ij_javascript_spaces_around_logical_operators = true +ij_javascript_spaces_around_multiplicative_operators = true +ij_javascript_spaces_around_relational_operators = true +ij_javascript_spaces_around_shift_operators = true +ij_javascript_spaces_around_unary_operator = false +ij_javascript_spaces_within_array_initializer_brackets = false +ij_javascript_spaces_within_brackets = false +ij_javascript_spaces_within_catch_parentheses = false +ij_javascript_spaces_within_for_parentheses = false +ij_javascript_spaces_within_if_parentheses = false +ij_javascript_spaces_within_imports = false +ij_javascript_spaces_within_interpolation_expressions = false +ij_javascript_spaces_within_method_call_parentheses = false +ij_javascript_spaces_within_method_parentheses = false +ij_javascript_spaces_within_object_literal_braces = false +ij_javascript_spaces_within_object_type_braces = true +ij_javascript_spaces_within_parentheses = false +ij_javascript_spaces_within_switch_parentheses = false +ij_javascript_spaces_within_type_assertion = false +ij_javascript_spaces_within_union_types = true +ij_javascript_spaces_within_while_parentheses = false +ij_javascript_special_else_if_treatment = true +ij_javascript_ternary_operation_signs_on_next_line = false +ij_javascript_ternary_operation_wrap = off +ij_javascript_union_types_wrap = on_every_item +ij_javascript_use_chained_calls_group_indents = false +ij_javascript_use_double_quotes = true +ij_javascript_use_explicit_js_extension = auto +ij_javascript_use_path_mapping = always +ij_javascript_use_public_modifier = false +ij_javascript_use_semicolon_after_statement = true +ij_javascript_var_declaration_wrap = normal +ij_javascript_while_brace_force = never +ij_javascript_while_on_new_line = false +ij_javascript_wrap_comments = false + +[{*.har,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,bowerrc,jest.config}] +indent_size = 2 +ij_json_array_wrapping = split_into_lines +ij_json_keep_blank_lines_in_code = 0 +ij_json_keep_indents_on_empty_lines = false +ij_json_keep_line_breaks = true +ij_json_keep_trailing_comma = false +ij_json_object_wrapping = split_into_lines +ij_json_property_alignment = do_not_align +ij_json_space_after_colon = true +ij_json_space_after_comma = true +ij_json_space_before_colon = false +ij_json_space_before_comma = false +ij_json_spaces_within_braces = false +ij_json_spaces_within_brackets = false +ij_json_wrap_long_lines = false + +[{*.htm,*.html,*.sht,*.shtm,*.shtml}] +ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3 +ij_html_align_attributes = true +ij_html_align_text = false +ij_html_attribute_wrap = normal +ij_html_block_comment_add_space = false +ij_html_block_comment_at_first_column = true +ij_html_do_not_align_children_of_min_lines = 0 +ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p +ij_html_do_not_indent_children_of_tags = none +ij_html_enforce_quotes = false +ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var +ij_html_keep_blank_lines = 2 +ij_html_keep_indents_on_empty_lines = false +ij_html_keep_line_breaks = true +ij_html_keep_line_breaks_in_text = true +ij_html_keep_whitespaces = false +ij_html_keep_whitespaces_inside = span,pre,textarea +ij_html_line_comment_at_first_column = true +ij_html_new_line_after_last_attribute = never +ij_html_new_line_before_first_attribute = never +ij_html_quote_style = double +ij_html_remove_new_line_before_tags = br +ij_html_space_after_tag_name = false +ij_html_space_around_equality_in_attribute = false +ij_html_space_inside_empty_tag = false +ij_html_text_wrap = normal + +[{*.yaml,*.yml}] +indent_size = 2 +ij_yaml_align_values_properties = do_not_align +ij_yaml_autoinsert_sequence_marker = false +ij_yaml_block_mapping_on_new_line = false +ij_yaml_indent_sequence_value = true +ij_yaml_keep_indents_on_empty_lines = false +ij_yaml_keep_line_breaks = true +ij_yaml_sequence_on_new_line = false +ij_yaml_space_before_colon = false +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true diff --git a/src/main/java/eu/openanalytics/shinyproxy/AuthenticationRequiredFilter.java b/src/main/java/eu/openanalytics/shinyproxy/AuthenticationRequiredFilter.java index 443fc936..9d8d34de 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/AuthenticationRequiredFilter.java +++ b/src/main/java/eu/openanalytics/shinyproxy/AuthenticationRequiredFilter.java @@ -60,14 +60,13 @@ */ public class AuthenticationRequiredFilter extends GenericFilterBean { - private final ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer(); - private static final RequestMatcher REQUEST_MATCHER = new OrRequestMatcher( new AntPathRequestMatcher("/app_proxy/**"), new AntPathRequestMatcher("/heartbeat/*"), new AntPathRequestMatcher("/api/**"), new AntPathRequestMatcher("/admin/data") - ); + ); + private final ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer(); public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; diff --git a/src/main/java/eu/openanalytics/shinyproxy/ShinyProxyConfiguration.java b/src/main/java/eu/openanalytics/shinyproxy/ShinyProxyConfiguration.java index c164bfc6..b08b6123 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/ShinyProxyConfiguration.java +++ b/src/main/java/eu/openanalytics/shinyproxy/ShinyProxyConfiguration.java @@ -1,45 +1,45 @@ -/** - * ShinyProxy - * - * Copyright (C) 2016-2023 Open Analytics - * - * =========================================================================== - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Apache License as published by - * The Apache Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * Apache License for more details. - * - * You should have received a copy of the Apache License - * along with this program. If not, see - */ -package eu.openanalytics.shinyproxy; - -import eu.openanalytics.containerproxy.model.runtime.runtimevalues.RuntimeValueKeyRegistry; -import eu.openanalytics.shinyproxy.runtimevalues.AppInstanceKey; -import eu.openanalytics.shinyproxy.runtimevalues.PublicPathKey; -import eu.openanalytics.shinyproxy.runtimevalues.ShinyForceFullReloadKey; -import eu.openanalytics.shinyproxy.runtimevalues.TrackAppUrl; -import eu.openanalytics.shinyproxy.runtimevalues.UserTimeZoneKey; -import eu.openanalytics.shinyproxy.runtimevalues.WebSocketReconnectionModeKey; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; - -@Configuration -@PropertySource("classpath:application.properties") -public class ShinyProxyConfiguration { - - static { - RuntimeValueKeyRegistry.addRuntimeValueKey(AppInstanceKey.inst); - RuntimeValueKeyRegistry.addRuntimeValueKey(PublicPathKey.inst); - RuntimeValueKeyRegistry.addRuntimeValueKey(ShinyForceFullReloadKey.inst); - RuntimeValueKeyRegistry.addRuntimeValueKey(WebSocketReconnectionModeKey.inst); - RuntimeValueKeyRegistry.addRuntimeValueKey(TrackAppUrl.inst); - RuntimeValueKeyRegistry.addRuntimeValueKey(UserTimeZoneKey.inst); - } -} +/** + * ShinyProxy + * + * Copyright (C) 2016-2023 Open Analytics + * + * =========================================================================== + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License as published by + * The Apache Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Apache License for more details. + * + * You should have received a copy of the Apache License + * along with this program. If not, see + */ +package eu.openanalytics.shinyproxy; + +import eu.openanalytics.containerproxy.model.runtime.runtimevalues.RuntimeValueKeyRegistry; +import eu.openanalytics.shinyproxy.runtimevalues.AppInstanceKey; +import eu.openanalytics.shinyproxy.runtimevalues.PublicPathKey; +import eu.openanalytics.shinyproxy.runtimevalues.ShinyForceFullReloadKey; +import eu.openanalytics.shinyproxy.runtimevalues.TrackAppUrl; +import eu.openanalytics.shinyproxy.runtimevalues.UserTimeZoneKey; +import eu.openanalytics.shinyproxy.runtimevalues.WebSocketReconnectionModeKey; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +@Configuration +@PropertySource("classpath:application.properties") +public class ShinyProxyConfiguration { + + static { + RuntimeValueKeyRegistry.addRuntimeValueKey(AppInstanceKey.inst); + RuntimeValueKeyRegistry.addRuntimeValueKey(PublicPathKey.inst); + RuntimeValueKeyRegistry.addRuntimeValueKey(ShinyForceFullReloadKey.inst); + RuntimeValueKeyRegistry.addRuntimeValueKey(WebSocketReconnectionModeKey.inst); + RuntimeValueKeyRegistry.addRuntimeValueKey(TrackAppUrl.inst); + RuntimeValueKeyRegistry.addRuntimeValueKey(UserTimeZoneKey.inst); + } +} diff --git a/src/main/java/eu/openanalytics/shinyproxy/ShinyProxyIframeScriptInjector.java b/src/main/java/eu/openanalytics/shinyproxy/ShinyProxyIframeScriptInjector.java index 152dfbbe..e6699eee 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/ShinyProxyIframeScriptInjector.java +++ b/src/main/java/eu/openanalytics/shinyproxy/ShinyProxyIframeScriptInjector.java @@ -155,15 +155,13 @@ private void updateContentLength(HttpServerExchange exchange, ByteBuffer output) long.class, HttpServerExchange.class); m.setAccessible(true); - } - catch (NoSuchMethodException | SecurityException ex) { + } catch (NoSuchMethodException | SecurityException ex) { throw new RuntimeException("could not find ServerFixedLengthStreamSinkConduit.reset method", ex); } try { m.invoke(next, length, exchange); - } - catch (Throwable ex) { + } catch (Throwable ex) { throw new RuntimeException("could not access BUFFERED_REQUEST_DATA field", ex); } } diff --git a/src/main/java/eu/openanalytics/shinyproxy/ShinyProxySpecExtensionProvider.java b/src/main/java/eu/openanalytics/shinyproxy/ShinyProxySpecExtensionProvider.java index b482b72c..2466c43f 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/ShinyProxySpecExtensionProvider.java +++ b/src/main/java/eu/openanalytics/shinyproxy/ShinyProxySpecExtensionProvider.java @@ -43,12 +43,12 @@ public void postInit() { specs.forEach(specExtension -> proxySpecProvider.getSpec(specExtension.getId()).addSpecExtension(specExtension)); } - public void setSpecs(List specs) { - this.specs = specs; - } - public List getSpecs() { return specs; } + public void setSpecs(List specs) { + this.specs = specs; + } + } diff --git a/src/main/java/eu/openanalytics/shinyproxy/ShinyProxySpecProvider.java b/src/main/java/eu/openanalytics/shinyproxy/ShinyProxySpecProvider.java index b42a4f0a..9f9e6204 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/ShinyProxySpecProvider.java +++ b/src/main/java/eu/openanalytics/shinyproxy/ShinyProxySpecProvider.java @@ -1,509 +1,505 @@ -/** - * ShinyProxy - * - * Copyright (C) 2016-2023 Open Analytics - * - * =========================================================================== - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Apache License as published by - * The Apache Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * Apache License for more details. - * - * You should have received a copy of the Apache License - * along with this program. If not, see - */ -package eu.openanalytics.shinyproxy; - -import eu.openanalytics.containerproxy.model.runtime.runtimevalues.RuntimeValue; -import eu.openanalytics.containerproxy.model.spec.AccessControl; -import eu.openanalytics.containerproxy.model.spec.ContainerSpec; -import eu.openanalytics.containerproxy.model.spec.DockerSwarmSecret; -import eu.openanalytics.containerproxy.model.spec.Parameters; -import eu.openanalytics.containerproxy.model.spec.PortMapping; -import eu.openanalytics.containerproxy.model.spec.ProxySpec; -import eu.openanalytics.containerproxy.service.UserService; -import eu.openanalytics.containerproxy.spec.IProxySpecProvider; -import eu.openanalytics.containerproxy.spec.expression.SpecExpressionContext; -import eu.openanalytics.containerproxy.spec.expression.SpecExpressionResolver; -import eu.openanalytics.containerproxy.spec.expression.SpelField; -import eu.openanalytics.shinyproxy.runtimevalues.ShinyForceFullReloadKey; -import eu.openanalytics.shinyproxy.runtimevalues.TrackAppUrl; -import eu.openanalytics.shinyproxy.runtimevalues.WebSocketReconnectionModeKey; -import eu.openanalytics.shinyproxy.runtimevalues.WebsocketReconnectionMode; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Lazy; -import org.springframework.context.annotation.Primary; -import org.springframework.core.env.Environment; -import org.springframework.security.core.Authentication; -import org.springframework.stereotype.Component; - -import javax.annotation.PostConstruct; -import javax.inject.Inject; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -/** - * This component converts proxy specs from the 'ShinyProxy notation' into the 'ContainerProxy' notation. - * ShinyProxy notation is slightly more compact, and omits several things that Shiny apps do not need, - * such as definition of multiple containers. - * - * Also, if no port is specified, a port mapping is automatically created for Shiny port 3838. - */ -@Component -@Primary -@ConfigurationProperties(prefix = "proxy") -public class ShinyProxySpecProvider implements IProxySpecProvider { - - private static final String PROP_DEFAULT_MAX_INSTANCES = "proxy.default-max-instances"; - private static final String PROP_DEFAULT_ALWAYS_SWITCH_INSTANCE = "proxy.default-always-switch-instance"; - - private List specs = new ArrayList<>(); - - private List templateGroups = new ArrayList<>(); - - private static Environment environment; - - private String defaultMaxInstances; - - private Boolean defaultAlwaysSwitchInstance; - - @Inject - private SpecExpressionResolver expressionResolver; - - @Inject - @Lazy - private UserService userService; - - @Autowired - public void setEnvironment(Environment env){ - ShinyProxySpecProvider.environment = env; - } - - @PostConstruct - public void afterPropertiesSet() { - this.specs.stream().collect(Collectors.groupingBy(ProxySpec::getId)).forEach((id, duplicateSpecs) -> { - if (duplicateSpecs.size() > 1) throw new IllegalArgumentException(String.format("Configuration error: spec with id '%s' is defined multiple times", id)); - }); - defaultMaxInstances = environment.getProperty(PROP_DEFAULT_MAX_INSTANCES, String.class, "1"); - defaultAlwaysSwitchInstance = environment.getProperty(PROP_DEFAULT_ALWAYS_SWITCH_INSTANCE, Boolean.class, false); - specs.forEach(ProxySpec::setContainerIndex); - } - - public List getSpecs() { - return new ArrayList<>(specs); - } - - public ProxySpec getSpec(String id) { - if (id == null || id.isEmpty()) return null; - return specs.stream().filter(s -> id.equals(s.getId())).findAny().orElse(null); - } - - public void setSpecs(List specs) { - this.specs = specs.stream().map(ShinyProxySpec::getProxySpec).toList(); - } - - public void setTemplateGroups(List templateGroups) { - this.templateGroups = templateGroups; - } - - public List getTemplateGroups() { - return templateGroups; - } - - public List getRuntimeValues(ProxySpec proxy) { - List runtimeValues = new ArrayList<>(); - - WebsocketReconnectionMode webSocketReconnectionMode = proxy.getSpecExtension(ShinyProxySpecExtension.class).getWebsocketReconnectionMode(); - if (webSocketReconnectionMode == null) { - runtimeValues.add(new RuntimeValue(WebSocketReconnectionModeKey.inst, environment.getProperty("proxy.default-websocket-reconnection-mode", WebsocketReconnectionMode.class, WebsocketReconnectionMode.None))); - } else { - runtimeValues.add(new RuntimeValue(WebSocketReconnectionModeKey.inst, webSocketReconnectionMode)); - } - - runtimeValues.add(new RuntimeValue(ShinyForceFullReloadKey.inst, getShinyForceFullReload(proxy))); - - Boolean trackAppUrl = proxy.getSpecExtension(ShinyProxySpecExtension.class).getTrackAppUrl(); - if (trackAppUrl == null) { - trackAppUrl = environment.getProperty("proxy.default-track-app-url", Boolean.class, false); - } - runtimeValues.add(new RuntimeValue(TrackAppUrl.inst, trackAppUrl)); - - return runtimeValues; - } - - public Integer getMaxInstancesForSpec(ProxySpec proxySpec) { - Authentication user = userService.getCurrentAuth(); - SpecExpressionContext context = SpecExpressionContext.create( - user, - user.getPrincipal(), - user.getCredentials()); - - Integer maxInstances = proxySpec.getSpecExtension(ShinyProxySpecExtension.class).getMaxInstances().resolve(expressionResolver, context).getValueOrNull(); - if (maxInstances != null) { - return maxInstances; - } - return expressionResolver.evaluateToInteger(defaultMaxInstances, context); - } - - public Map getMaxInstances() { - Authentication user = userService.getCurrentAuth(); - SpecExpressionContext context = SpecExpressionContext.create( - user, - user.getPrincipal(), - user.getCredentials()); - - Map result = new HashMap<>(); - - Integer resolvedDefault = expressionResolver.evaluateToInteger(defaultMaxInstances, context); - - for (ProxySpec proxySpec: getSpecs()) { - Integer maxInstances = proxySpec.getSpecExtension(ShinyProxySpecExtension.class).getMaxInstances().resolve(expressionResolver, context).getValueOrNull(); - if (maxInstances != null) { - result.put(proxySpec.getId(), maxInstances); - } else { - result.put(proxySpec.getId(), resolvedDefault); - } - } - - return result; - } - - public Boolean getShinyForceFullReload(ProxySpec proxySpec) { - Boolean shinyProxyForceFullReload = proxySpec.getSpecExtension(ShinyProxySpecExtension.class).getShinyForceFullReload(); - if (shinyProxyForceFullReload != null) { - return shinyProxyForceFullReload; - } - return false; - } - - - public Boolean getHideNavbarOnMainPageLink(ProxySpec proxySpec) { - Boolean hideNavbarOnMainPageLink = proxySpec.getSpecExtension(ShinyProxySpecExtension.class).getHideNavbarOnMainPageLink(); - if (hideNavbarOnMainPageLink != null) { - return hideNavbarOnMainPageLink; - } - return false; - } - - public Boolean getAlwaysShowSwitchInstance(ProxySpec proxySpec) { - Boolean alwaysShowSwitchInstance = proxySpec.getSpecExtension(ShinyProxySpecExtension.class).getAlwaysShowSwitchInstance(); - if (alwaysShowSwitchInstance != null) { - return alwaysShowSwitchInstance; - } - return defaultAlwaysSwitchInstance; - } - - public static class ShinyProxySpec { - - private final ProxySpec.ProxySpecBuilder proxySpec; - private final ContainerSpec.ContainerSpecBuilder containerSpec; - private final AccessControl accessControl; - private final PortMapping.PortMappingBuilder defaultPortMapping; - private List additionalPortMappings = new ArrayList<>(); - - public ShinyProxySpec() { - proxySpec = ProxySpec.builder(); - containerSpec = ContainerSpec.builder(); - accessControl = new AccessControl(); - defaultPortMapping = PortMapping.builder().name("default").port(3838); - proxySpec.accessControl(accessControl); - } - - public String getId() { - return proxySpec.build().getId(); - } - - public void setId(String id) { - proxySpec.id(id); - } - - public String getDisplayName() { - return proxySpec.build().getDisplayName(); - } - - public void setDisplayName(String displayName) { - proxySpec.displayName(displayName); - } - - public String getDescription() { - return proxySpec.build().getDescription(); - } - - public void setDescription(String description) { - proxySpec.description(description); - } - - public String getLogoURL() { - return proxySpec.build().getLogoURL(); - } - - public void setLogoURL(String logoURL) { - proxySpec.logoURL(logoURL); - } - - public SpelField.String getContainerImage() { - return containerSpec.build().getImage(); - } - - public void setContainerImage(SpelField.String containerImage) { - containerSpec.image(containerImage); - } - - public SpelField.StringList getContainerCmd() { - return containerSpec.build().getCmd(); - } - - public void setContainerCmd(List containerCmd) { - containerSpec.cmd(new SpelField.StringList(containerCmd)); - } - - public SpelField.StringMap getContainerEnv() { - return containerSpec.build().getEnv(); - } - - public void setContainerEnv(Map containerEnv) { - containerSpec.env(new SpelField.StringMap(containerEnv)); - } - - public SpelField.String getContainerEnvFile() { - return containerSpec.build().getEnvFile(); - } - - public void setContainerEnvFile(SpelField.String containerEnvFile) { - containerSpec.envFile(containerEnvFile); - } - - public SpelField.String getContainerNetwork() { - return containerSpec.build().getNetwork(); - } - - public void setContainerNetwork(SpelField.String containerNetwork) { - containerSpec.network(containerNetwork); - } - - public SpelField.StringList getContainerNetworkConnections() { - return containerSpec.build().getNetworkConnections(); - } - - public void setContainerNetworkConnections(List containerNetworkConnections) { - containerSpec.networkConnections(new SpelField.StringList(containerNetworkConnections)); - } - - public SpelField.StringList getContainerDns() { - return containerSpec.build().getDns(); - } - - public void setContainerDns(List containerDns) { - containerSpec.dns(new SpelField.StringList(containerDns)); - } - - public SpelField.StringList getContainerVolumes() { - return containerSpec.build().getVolumes(); - } - - public void setContainerVolumes(List containerVolumes) { - containerSpec.volumes(new SpelField.StringList(containerVolumes)); - } - - public SpelField.String getContainerMemoryRequest() { - return containerSpec.build().getMemoryRequest(); - } - - public void setContainerMemoryRequest(SpelField.String containerMemoryRequest) { - containerSpec.memoryRequest(containerMemoryRequest); - } - - public SpelField.String getContainerMemoryLimit() { - return containerSpec.build().getMemoryLimit(); - } - - public void setContainerMemoryLimit(SpelField.String containerMemoryLimit) { - containerSpec.memoryLimit(containerMemoryLimit); - } - - public SpelField.String getContainerCpuRequest() { - return containerSpec.build().getCpuRequest(); - } - - public void setContainerCpuRequest(SpelField.String containerCpuRequest) { - containerSpec.cpuRequest(containerCpuRequest); - } - - public SpelField.String getContainerCpuLimit() { - return containerSpec.build().getCpuLimit(); - } - - public void setContainerCpuLimit(SpelField.String containerCpuLimit) { - containerSpec.cpuLimit(containerCpuLimit); - } - - public boolean isContainerPrivileged() { - return containerSpec.build().isPrivileged(); - } - - public void setContainerPrivileged(boolean containerPrivileged) { - containerSpec.privileged(containerPrivileged); - } - - public SpelField.StringMap getLabels() { - return containerSpec.build().getLabels(); - } - - public void setLabels(Map labels) { - containerSpec.labels(new SpelField.StringMap(labels)); - } - - public int getPort() { - return defaultPortMapping.build().getPort(); - } - - public void setPort(int port) { - defaultPortMapping.port(port); - } - - public String[] getAccessGroups() { - return accessControl.getGroups(); - } - - public void setAccessGroups(String[] accessGroups) { - accessControl.setGroups(accessGroups); - } - - public SpelField.String getTargetPath() { - return defaultPortMapping.build().getTargetPath(); - } - - public void setTargetPath(SpelField.String targetPath) { - defaultPortMapping.targetPath(targetPath); - } - - public String[] getAccessUsers() { - return accessControl.getUsers(); - } - - public void setAccessUsers(String[] accessUsers) { - accessControl.setUsers(accessUsers); - } - - public String getAccessExpression() { - return accessControl.getExpression(); - } - - public void setAccessExpression(String accessExpression) { - accessControl.setExpression(accessExpression); - } - - public List getDockerSwarmSecrets() { - return containerSpec.build().getDockerSwarmSecrets(); - } - - public void setDockerSwarmSecrets(List dockerSwarmSecrets) { - containerSpec.dockerSwarmSecrets(dockerSwarmSecrets); - } - - public String getDockerRegistryDomain() { - return containerSpec.build().getDockerRegistryDomain(); - } - - public void setDockerRegistryDomain(String dockerRegistryDomain) { - containerSpec.dockerRegistryDomain(dockerRegistryDomain); - } - - public String getDockerRegistryUsername() { - return containerSpec.build().getDockerRegistryUsername(); - } - - public void setDockerRegistryUsername(String dockerRegistryUsername) { - containerSpec.dockerRegistryUsername(dockerRegistryUsername); - } - - public String getDockerRegistryPassword() { - return containerSpec.build().getDockerRegistryPassword(); - } - - public void setDockerRegistryPassword(String dockerRegistryPassword) { - containerSpec.dockerRegistryPassword(dockerRegistryPassword); - } - - public Parameters getParameters() { - return proxySpec.build().getParameters(); - } - - public void setParameters(Parameters parameters) { - proxySpec.parameters(parameters); - } - - public SpelField.Long getMaxLifetime() { - return proxySpec.build().getMaxLifeTime(); - } - - public void setMaxLifetime(SpelField.Long maxLifetime) { - proxySpec.maxLifeTime(maxLifetime); - } - - public Boolean getStopOnLogout() { - return proxySpec.build().getStopOnLogout(); - } - - public void setStopOnLogout(Boolean stopOnLogout) { - proxySpec.stopOnLogout(stopOnLogout); - } - - public SpelField.Long getHeartbeatTimeout() { - return proxySpec.build().getHeartbeatTimeout(); - } - - public void setHeartbeatTimeout(SpelField.Long heartbeatTimeout) { - proxySpec.heartbeatTimeout(heartbeatTimeout); - } - - public List getAdditionalPortMappings() { - return additionalPortMappings; - } - - public void setAdditionalPortMappings(List additionalPortMappings) { - this.additionalPortMappings = additionalPortMappings; - } - - public ProxySpec getProxySpec() { - additionalPortMappings.add(defaultPortMapping.build()); - containerSpec.portMapping(additionalPortMappings); - proxySpec.containerSpecs(Collections.singletonList(containerSpec.build())); - return proxySpec.build(); - } - } - - public static class TemplateGroup { - - private String id; - private Map properties; - - public Map getProperties() { - return properties; - } - - public void setProperties(Map properties) { - this.properties = properties; - } - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - } - -} +/** + * ShinyProxy + * + * Copyright (C) 2016-2023 Open Analytics + * + * =========================================================================== + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License as published by + * The Apache Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Apache License for more details. + * + * You should have received a copy of the Apache License + * along with this program. If not, see + */ +package eu.openanalytics.shinyproxy; + +import eu.openanalytics.containerproxy.model.runtime.runtimevalues.RuntimeValue; +import eu.openanalytics.containerproxy.model.spec.AccessControl; +import eu.openanalytics.containerproxy.model.spec.ContainerSpec; +import eu.openanalytics.containerproxy.model.spec.DockerSwarmSecret; +import eu.openanalytics.containerproxy.model.spec.Parameters; +import eu.openanalytics.containerproxy.model.spec.PortMapping; +import eu.openanalytics.containerproxy.model.spec.ProxySpec; +import eu.openanalytics.containerproxy.service.UserService; +import eu.openanalytics.containerproxy.spec.IProxySpecProvider; +import eu.openanalytics.containerproxy.spec.expression.SpecExpressionContext; +import eu.openanalytics.containerproxy.spec.expression.SpecExpressionResolver; +import eu.openanalytics.containerproxy.spec.expression.SpelField; +import eu.openanalytics.shinyproxy.runtimevalues.ShinyForceFullReloadKey; +import eu.openanalytics.shinyproxy.runtimevalues.TrackAppUrl; +import eu.openanalytics.shinyproxy.runtimevalues.WebSocketReconnectionModeKey; +import eu.openanalytics.shinyproxy.runtimevalues.WebsocketReconnectionMode; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Lazy; +import org.springframework.context.annotation.Primary; +import org.springframework.core.env.Environment; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * This component converts proxy specs from the 'ShinyProxy notation' into the 'ContainerProxy' notation. + * ShinyProxy notation is slightly more compact, and omits several things that Shiny apps do not need, + * such as definition of multiple containers. + * + * Also, if no port is specified, a port mapping is automatically created for Shiny port 3838. + */ +@Component +@Primary +@ConfigurationProperties(prefix = "proxy") +public class ShinyProxySpecProvider implements IProxySpecProvider { + + private static final String PROP_DEFAULT_MAX_INSTANCES = "proxy.default-max-instances"; + private static final String PROP_DEFAULT_ALWAYS_SWITCH_INSTANCE = "proxy.default-always-switch-instance"; + private static Environment environment; + private List specs = new ArrayList<>(); + private List templateGroups = new ArrayList<>(); + private String defaultMaxInstances; + + private Boolean defaultAlwaysSwitchInstance; + + @Inject + private SpecExpressionResolver expressionResolver; + + @Inject + @Lazy + private UserService userService; + + @Autowired + public void setEnvironment(Environment env) { + ShinyProxySpecProvider.environment = env; + } + + @PostConstruct + public void afterPropertiesSet() { + this.specs.stream().collect(Collectors.groupingBy(ProxySpec::getId)).forEach((id, duplicateSpecs) -> { + if (duplicateSpecs.size() > 1) throw new IllegalArgumentException(String.format("Configuration error: spec with id '%s' is defined multiple times", id)); + }); + defaultMaxInstances = environment.getProperty(PROP_DEFAULT_MAX_INSTANCES, String.class, "1"); + defaultAlwaysSwitchInstance = environment.getProperty(PROP_DEFAULT_ALWAYS_SWITCH_INSTANCE, Boolean.class, false); + specs.forEach(ProxySpec::setContainerIndex); + } + + public List getSpecs() { + return new ArrayList<>(specs); + } + + public void setSpecs(List specs) { + this.specs = specs.stream().map(ShinyProxySpec::getProxySpec).toList(); + } + + public ProxySpec getSpec(String id) { + if (id == null || id.isEmpty()) return null; + return specs.stream().filter(s -> id.equals(s.getId())).findAny().orElse(null); + } + + public List getTemplateGroups() { + return templateGroups; + } + + public void setTemplateGroups(List templateGroups) { + this.templateGroups = templateGroups; + } + + public List getRuntimeValues(ProxySpec proxy) { + List runtimeValues = new ArrayList<>(); + + WebsocketReconnectionMode webSocketReconnectionMode = proxy.getSpecExtension(ShinyProxySpecExtension.class).getWebsocketReconnectionMode(); + if (webSocketReconnectionMode == null) { + runtimeValues.add(new RuntimeValue(WebSocketReconnectionModeKey.inst, environment.getProperty("proxy.default-websocket-reconnection-mode", WebsocketReconnectionMode.class, WebsocketReconnectionMode.None))); + } else { + runtimeValues.add(new RuntimeValue(WebSocketReconnectionModeKey.inst, webSocketReconnectionMode)); + } + + runtimeValues.add(new RuntimeValue(ShinyForceFullReloadKey.inst, getShinyForceFullReload(proxy))); + + Boolean trackAppUrl = proxy.getSpecExtension(ShinyProxySpecExtension.class).getTrackAppUrl(); + if (trackAppUrl == null) { + trackAppUrl = environment.getProperty("proxy.default-track-app-url", Boolean.class, false); + } + runtimeValues.add(new RuntimeValue(TrackAppUrl.inst, trackAppUrl)); + + return runtimeValues; + } + + public Integer getMaxInstancesForSpec(ProxySpec proxySpec) { + Authentication user = userService.getCurrentAuth(); + SpecExpressionContext context = SpecExpressionContext.create( + user, + user.getPrincipal(), + user.getCredentials()); + + Integer maxInstances = proxySpec.getSpecExtension(ShinyProxySpecExtension.class).getMaxInstances().resolve(expressionResolver, context).getValueOrNull(); + if (maxInstances != null) { + return maxInstances; + } + return expressionResolver.evaluateToInteger(defaultMaxInstances, context); + } + + public Map getMaxInstances() { + Authentication user = userService.getCurrentAuth(); + SpecExpressionContext context = SpecExpressionContext.create( + user, + user.getPrincipal(), + user.getCredentials()); + + Map result = new HashMap<>(); + + Integer resolvedDefault = expressionResolver.evaluateToInteger(defaultMaxInstances, context); + + for (ProxySpec proxySpec : getSpecs()) { + Integer maxInstances = proxySpec.getSpecExtension(ShinyProxySpecExtension.class).getMaxInstances().resolve(expressionResolver, context).getValueOrNull(); + if (maxInstances != null) { + result.put(proxySpec.getId(), maxInstances); + } else { + result.put(proxySpec.getId(), resolvedDefault); + } + } + + return result; + } + + public Boolean getShinyForceFullReload(ProxySpec proxySpec) { + Boolean shinyProxyForceFullReload = proxySpec.getSpecExtension(ShinyProxySpecExtension.class).getShinyForceFullReload(); + if (shinyProxyForceFullReload != null) { + return shinyProxyForceFullReload; + } + return false; + } + + + public Boolean getHideNavbarOnMainPageLink(ProxySpec proxySpec) { + Boolean hideNavbarOnMainPageLink = proxySpec.getSpecExtension(ShinyProxySpecExtension.class).getHideNavbarOnMainPageLink(); + if (hideNavbarOnMainPageLink != null) { + return hideNavbarOnMainPageLink; + } + return false; + } + + public Boolean getAlwaysShowSwitchInstance(ProxySpec proxySpec) { + Boolean alwaysShowSwitchInstance = proxySpec.getSpecExtension(ShinyProxySpecExtension.class).getAlwaysShowSwitchInstance(); + if (alwaysShowSwitchInstance != null) { + return alwaysShowSwitchInstance; + } + return defaultAlwaysSwitchInstance; + } + + public static class ShinyProxySpec { + + private final ProxySpec.ProxySpecBuilder proxySpec; + private final ContainerSpec.ContainerSpecBuilder containerSpec; + private final AccessControl accessControl; + private final PortMapping.PortMappingBuilder defaultPortMapping; + private List additionalPortMappings = new ArrayList<>(); + + public ShinyProxySpec() { + proxySpec = ProxySpec.builder(); + containerSpec = ContainerSpec.builder(); + accessControl = new AccessControl(); + defaultPortMapping = PortMapping.builder().name("default").port(3838); + proxySpec.accessControl(accessControl); + } + + public String getId() { + return proxySpec.build().getId(); + } + + public void setId(String id) { + proxySpec.id(id); + } + + public String getDisplayName() { + return proxySpec.build().getDisplayName(); + } + + public void setDisplayName(String displayName) { + proxySpec.displayName(displayName); + } + + public String getDescription() { + return proxySpec.build().getDescription(); + } + + public void setDescription(String description) { + proxySpec.description(description); + } + + public String getLogoURL() { + return proxySpec.build().getLogoURL(); + } + + public void setLogoURL(String logoURL) { + proxySpec.logoURL(logoURL); + } + + public SpelField.String getContainerImage() { + return containerSpec.build().getImage(); + } + + public void setContainerImage(SpelField.String containerImage) { + containerSpec.image(containerImage); + } + + public SpelField.StringList getContainerCmd() { + return containerSpec.build().getCmd(); + } + + public void setContainerCmd(List containerCmd) { + containerSpec.cmd(new SpelField.StringList(containerCmd)); + } + + public SpelField.StringMap getContainerEnv() { + return containerSpec.build().getEnv(); + } + + public void setContainerEnv(Map containerEnv) { + containerSpec.env(new SpelField.StringMap(containerEnv)); + } + + public SpelField.String getContainerEnvFile() { + return containerSpec.build().getEnvFile(); + } + + public void setContainerEnvFile(SpelField.String containerEnvFile) { + containerSpec.envFile(containerEnvFile); + } + + public SpelField.String getContainerNetwork() { + return containerSpec.build().getNetwork(); + } + + public void setContainerNetwork(SpelField.String containerNetwork) { + containerSpec.network(containerNetwork); + } + + public SpelField.StringList getContainerNetworkConnections() { + return containerSpec.build().getNetworkConnections(); + } + + public void setContainerNetworkConnections(List containerNetworkConnections) { + containerSpec.networkConnections(new SpelField.StringList(containerNetworkConnections)); + } + + public SpelField.StringList getContainerDns() { + return containerSpec.build().getDns(); + } + + public void setContainerDns(List containerDns) { + containerSpec.dns(new SpelField.StringList(containerDns)); + } + + public SpelField.StringList getContainerVolumes() { + return containerSpec.build().getVolumes(); + } + + public void setContainerVolumes(List containerVolumes) { + containerSpec.volumes(new SpelField.StringList(containerVolumes)); + } + + public SpelField.String getContainerMemoryRequest() { + return containerSpec.build().getMemoryRequest(); + } + + public void setContainerMemoryRequest(SpelField.String containerMemoryRequest) { + containerSpec.memoryRequest(containerMemoryRequest); + } + + public SpelField.String getContainerMemoryLimit() { + return containerSpec.build().getMemoryLimit(); + } + + public void setContainerMemoryLimit(SpelField.String containerMemoryLimit) { + containerSpec.memoryLimit(containerMemoryLimit); + } + + public SpelField.String getContainerCpuRequest() { + return containerSpec.build().getCpuRequest(); + } + + public void setContainerCpuRequest(SpelField.String containerCpuRequest) { + containerSpec.cpuRequest(containerCpuRequest); + } + + public SpelField.String getContainerCpuLimit() { + return containerSpec.build().getCpuLimit(); + } + + public void setContainerCpuLimit(SpelField.String containerCpuLimit) { + containerSpec.cpuLimit(containerCpuLimit); + } + + public boolean isContainerPrivileged() { + return containerSpec.build().isPrivileged(); + } + + public void setContainerPrivileged(boolean containerPrivileged) { + containerSpec.privileged(containerPrivileged); + } + + public SpelField.StringMap getLabels() { + return containerSpec.build().getLabels(); + } + + public void setLabels(Map labels) { + containerSpec.labels(new SpelField.StringMap(labels)); + } + + public int getPort() { + return defaultPortMapping.build().getPort(); + } + + public void setPort(int port) { + defaultPortMapping.port(port); + } + + public String[] getAccessGroups() { + return accessControl.getGroups(); + } + + public void setAccessGroups(String[] accessGroups) { + accessControl.setGroups(accessGroups); + } + + public SpelField.String getTargetPath() { + return defaultPortMapping.build().getTargetPath(); + } + + public void setTargetPath(SpelField.String targetPath) { + defaultPortMapping.targetPath(targetPath); + } + + public String[] getAccessUsers() { + return accessControl.getUsers(); + } + + public void setAccessUsers(String[] accessUsers) { + accessControl.setUsers(accessUsers); + } + + public String getAccessExpression() { + return accessControl.getExpression(); + } + + public void setAccessExpression(String accessExpression) { + accessControl.setExpression(accessExpression); + } + + public List getDockerSwarmSecrets() { + return containerSpec.build().getDockerSwarmSecrets(); + } + + public void setDockerSwarmSecrets(List dockerSwarmSecrets) { + containerSpec.dockerSwarmSecrets(dockerSwarmSecrets); + } + + public String getDockerRegistryDomain() { + return containerSpec.build().getDockerRegistryDomain(); + } + + public void setDockerRegistryDomain(String dockerRegistryDomain) { + containerSpec.dockerRegistryDomain(dockerRegistryDomain); + } + + public String getDockerRegistryUsername() { + return containerSpec.build().getDockerRegistryUsername(); + } + + public void setDockerRegistryUsername(String dockerRegistryUsername) { + containerSpec.dockerRegistryUsername(dockerRegistryUsername); + } + + public String getDockerRegistryPassword() { + return containerSpec.build().getDockerRegistryPassword(); + } + + public void setDockerRegistryPassword(String dockerRegistryPassword) { + containerSpec.dockerRegistryPassword(dockerRegistryPassword); + } + + public Parameters getParameters() { + return proxySpec.build().getParameters(); + } + + public void setParameters(Parameters parameters) { + proxySpec.parameters(parameters); + } + + public SpelField.Long getMaxLifetime() { + return proxySpec.build().getMaxLifeTime(); + } + + public void setMaxLifetime(SpelField.Long maxLifetime) { + proxySpec.maxLifeTime(maxLifetime); + } + + public Boolean getStopOnLogout() { + return proxySpec.build().getStopOnLogout(); + } + + public void setStopOnLogout(Boolean stopOnLogout) { + proxySpec.stopOnLogout(stopOnLogout); + } + + public SpelField.Long getHeartbeatTimeout() { + return proxySpec.build().getHeartbeatTimeout(); + } + + public void setHeartbeatTimeout(SpelField.Long heartbeatTimeout) { + proxySpec.heartbeatTimeout(heartbeatTimeout); + } + + public List getAdditionalPortMappings() { + return additionalPortMappings; + } + + public void setAdditionalPortMappings(List additionalPortMappings) { + this.additionalPortMappings = additionalPortMappings; + } + + public ProxySpec getProxySpec() { + additionalPortMappings.add(defaultPortMapping.build()); + containerSpec.portMapping(additionalPortMappings); + proxySpec.containerSpecs(Collections.singletonList(containerSpec.build())); + return proxySpec.build(); + } + } + + public static class TemplateGroup { + + private String id; + private Map properties; + + public Map getProperties() { + return properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + } + +} diff --git a/src/main/java/eu/openanalytics/shinyproxy/ShinyProxyTestStrategy.java b/src/main/java/eu/openanalytics/shinyproxy/ShinyProxyTestStrategy.java index 2dd7c164..2ba99aa5 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/ShinyProxyTestStrategy.java +++ b/src/main/java/eu/openanalytics/shinyproxy/ShinyProxyTestStrategy.java @@ -42,54 +42,54 @@ @Primary public class ShinyProxyTestStrategy implements IProxyTestStrategy { - private final StructuredLogger log = StructuredLogger.create(getClass()); - - @Inject - private Environment environment; - - @Override - public boolean testProxy(Proxy proxy) { + private final StructuredLogger log = StructuredLogger.create(getClass()); - int totalWaitMs = Integer.parseInt(environment.getProperty("proxy.container-wait-time", "20000")); - int timeoutMs = Integer.parseInt(environment.getProperty("proxy.container-wait-timeout", "5000")); + @Inject + private Environment environment; - if (proxy.getContainers().get(0).getTargets().isEmpty()) return false; - URI targetURI = proxy.getContainers().get(0).getTargets().get(proxy.getId()); + @Override + public boolean testProxy(Proxy proxy) { - return Retrying.retry((currentAttempt, maxAttempts) -> { - try { - if (proxy.getStatus().isUnavailable()) { - // proxy got stopped while loading -> no need to try to connect it since the container will already be deleted - return true; - } - URL testURL = new URL(targetURI.toString() + "/"); - HttpURLConnection connection = ((HttpURLConnection) testURL.openConnection()); - if (currentAttempt <= 5) { - // When the container has only just started (or when the k8s service has only just been created), - // it could be that our traffic ends in a black hole, and we need to wait the full 5s seconds of - // the timeout. Therefore, we first try a few attempts with a lower timeout. If the container is - // fast, this will result in a faster startup. If the container is slow to startup, not time is waste. - connection.setConnectTimeout(200); - connection.setReadTimeout(200); - } else { - connection.setConnectTimeout(timeoutMs); - connection.setReadTimeout(timeoutMs); - } - connection.setInstanceFollowRedirects(false); - int responseCode = connection.getResponseCode(); - if (Arrays.asList(200, 301, 302, 303, 307, 308).contains(responseCode)) { - if (currentAttempt > 10) { - log.info(proxy, "Container responsive"); - } - return true; - } - } catch (Exception e) { - if (currentAttempt > 10) { - log.warn(proxy, String.format("Container unresponsive, trying again (%d/%d): %s", currentAttempt, maxAttempts, targetURI)); - } - } - return false; - }, totalWaitMs); - } + int totalWaitMs = Integer.parseInt(environment.getProperty("proxy.container-wait-time", "20000")); + int timeoutMs = Integer.parseInt(environment.getProperty("proxy.container-wait-timeout", "5000")); + + if (proxy.getContainers().get(0).getTargets().isEmpty()) return false; + URI targetURI = proxy.getContainers().get(0).getTargets().get(proxy.getId()); + + return Retrying.retry((currentAttempt, maxAttempts) -> { + try { + if (proxy.getStatus().isUnavailable()) { + // proxy got stopped while loading -> no need to try to connect it since the container will already be deleted + return true; + } + URL testURL = new URL(targetURI.toString() + "/"); + HttpURLConnection connection = ((HttpURLConnection) testURL.openConnection()); + if (currentAttempt <= 5) { + // When the container has only just started (or when the k8s service has only just been created), + // it could be that our traffic ends in a black hole, and we need to wait the full 5s seconds of + // the timeout. Therefore, we first try a few attempts with a lower timeout. If the container is + // fast, this will result in a faster startup. If the container is slow to startup, not time is waste. + connection.setConnectTimeout(200); + connection.setReadTimeout(200); + } else { + connection.setConnectTimeout(timeoutMs); + connection.setReadTimeout(timeoutMs); + } + connection.setInstanceFollowRedirects(false); + int responseCode = connection.getResponseCode(); + if (Arrays.asList(200, 301, 302, 303, 307, 308).contains(responseCode)) { + if (currentAttempt > 10) { + log.info(proxy, "Container responsive"); + } + return true; + } + } catch (Exception e) { + if (currentAttempt > 10) { + log.warn(proxy, String.format("Container unresponsive, trying again (%d/%d): %s", currentAttempt, maxAttempts, targetURI)); + } + } + return false; + }, totalWaitMs); + } } diff --git a/src/main/java/eu/openanalytics/shinyproxy/controllers/AdminController.java b/src/main/java/eu/openanalytics/shinyproxy/controllers/AdminController.java index bdaa16da..17abd7ae 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/controllers/AdminController.java +++ b/src/main/java/eu/openanalytics/shinyproxy/controllers/AdminController.java @@ -1,203 +1,203 @@ -/** - * ShinyProxy - * - * Copyright (C) 2016-2023 Open Analytics - * - * =========================================================================== - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Apache License as published by - * The Apache Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * Apache License for more details. - * - * You should have received a copy of the Apache License - * along with this program. If not, see - */ -package eu.openanalytics.shinyproxy.controllers; - -import eu.openanalytics.containerproxy.api.dto.ApiResponse; -import eu.openanalytics.containerproxy.model.runtime.Container; -import eu.openanalytics.containerproxy.model.runtime.ParameterNames; -import eu.openanalytics.containerproxy.model.runtime.Proxy; -import eu.openanalytics.containerproxy.model.runtime.runtimevalues.BackendContainerNameKey; -import eu.openanalytics.containerproxy.model.runtime.runtimevalues.ContainerImageKey; -import eu.openanalytics.containerproxy.model.runtime.runtimevalues.HeartbeatTimeoutKey; -import eu.openanalytics.containerproxy.model.runtime.runtimevalues.InstanceIdKey; -import eu.openanalytics.containerproxy.model.runtime.runtimevalues.MaxLifetimeKey; -import eu.openanalytics.containerproxy.model.runtime.runtimevalues.ParameterNamesKey; -import eu.openanalytics.containerproxy.service.hearbeat.ActiveProxiesService; -import eu.openanalytics.shinyproxy.runtimevalues.AppInstanceKey; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.ExampleObject; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; -import org.springframework.ui.ModelMap; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.ResponseBody; - -import javax.inject.Inject; -import javax.servlet.http.HttpServletRequest; -import java.util.List; - -@Controller -public class AdminController extends BaseController { - - @Inject - private ActiveProxiesService activeProxiesService; - - @RequestMapping("/admin") - private String admin(ModelMap map, HttpServletRequest request) { - prepareMap(map, request); - - return "admin"; - } - - @Operation(summary = "Get active proxies of all users.", tags = "ShinyProxy") - @ApiResponses(value = { - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "200", - description = "Active proxies are returned.", - content = { - @Content( - mediaType = "application/json", - schema = @Schema(implementation = ProxyInfoResponse.class), - examples = { - @ExampleObject(value = "{\"status\": \"success\", \"data\": [{\"status\": \"Up\", \"proxyId\": \"9cd90bbb-ae9c-4016-9b9c-d2852b3a0bf6\", \"userId\": \"jack\", \"appName\": \"01_hello\", " + - "\"instanceName\": \"Default\", \"endpoint\": \"N/A\", \"uptime\": \"0:00:39\", \"lastHeartBeat\": \"0:00:05\", \"imageName\": \"openanalytics/shinyproxy-demo\", \"imageTag\": \"N/A\", " + - "\"heartbeatTimeout\": null, \"maxLifetime\": \"0:02:00\", \"spInstance\": \"9bec0d32754eab6a036bf1ee032bca82f98df0c5\", \"backendContainerName\": " + - "\"900b4f35b283401946db1d7cb8fe31ad5e6209d921b3cb9fd668ed6b9cbf7aa5\", \"parameters\": null}, {\"status\": \"Up\", \"proxyId\": \"b34d416e-ce6e-4351-a126-8836c88f2200\", \"userId\": " + - "\"jack\", \"appName\": \"06_tabsets\", \"instanceName\": \"Default\", \"endpoint\": \"N/A\", \"uptime\": \"0:00:18\", \"lastHeartBeat\": \"0:00:02\", \"imageName\": " + - "\"openanalytics/shinyproxy-demo\", \"imageTag\": \"N/A\", \"heartbeatTimeout\": null, \"maxLifetime\": \"0:02:00\", \"spInstance\": \"9bec0d32754eab6a036bf1ee032bca82f98df0c5\", " + - "\"backendContainerName\": \"2158b5b49c4138a9d0d6313fc4b62eba074b359473143be1d98102ab06c74bf8\", \"parameters\": null}]}") - } - ) - }), - }) - @RequestMapping(value = "/admin/data", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET) - @ResponseBody - private ResponseEntity>> adminData() { - List proxies = proxyService.getProxies(null, false); - List proxyInfos = proxies.stream().map(ProxyInfo::new).toList(); - return ApiResponse.success(proxyInfos); - } - - public class ProxyInfo { - - @Schema(allowableValues = {"New", "Up", "Stopping", "Pausing", "Paused", "Resuming", "Stopped"}) - public final String status; - - public final String proxyId; - public final String userId; - public final String appName; - public final String instanceName; - public final String endpoint; - public final String uptime; - public final String lastHeartBeat; - public final String imageName; - public final String imageTag; - public final String heartbeatTimeout; - public final String maxLifetime; - public final String spInstance; - public final String backendContainerName; - public final List parameters; - - public ProxyInfo(Proxy proxy) { - status = proxy.getStatus().toString(); - proxyId = proxy.getId(); - userId = proxy.getUserId(); - appName = proxy.getSpecId(); - instanceName = getInstanceName(proxy); - - if (proxy.getStartupTimestamp() > 0) { - uptime = getTimeDelta(proxy.getStartupTimestamp()); - } else { - uptime = "N/A"; - } - - Long heartBeat = activeProxiesService.getLastHeartBeat(proxy.getId()); - if (heartBeat == null) { - lastHeartBeat = "N/A"; - } else { - lastHeartBeat = getTimeDelta(heartBeat); - } - - if (!proxy.getContainers().isEmpty()) { - Container container = proxy.getContainers().get(0); - String[] parts = container.getRuntimeValue(ContainerImageKey.inst).split(":"); - imageName = parts[0]; - if (parts.length > 1) { - imageTag = parts[1]; - } else { - imageTag = "N/A"; - } - if (container.getTargets().containsKey(proxy.getId())) { - endpoint = container.getTargets().get(proxy.getId()).toString(); - } else { - endpoint = "N/A"; - } - backendContainerName = container.getRuntimeObjectOrDefault(BackendContainerNameKey.inst, "N/A"); - } else { - imageName = "N/A"; - imageTag = "N/A"; - endpoint = "N/A"; - backendContainerName = "N/A"; - } - - Long heartbeatTimeout = proxy.getRuntimeObjectOrNull(HeartbeatTimeoutKey.inst); - if (heartbeatTimeout != null && heartbeatTimeout != -1) { - this.heartbeatTimeout = formatSeconds(heartbeatTimeout / 1000); - } else { - this.heartbeatTimeout = null; - } - - Long maxLifetime = proxy.getRuntimeObjectOrNull(MaxLifetimeKey.inst); - if (maxLifetime != null && maxLifetime != -1) { - this.maxLifetime = formatSeconds(maxLifetime * 60); - } else { - this.maxLifetime = null; - } - - ParameterNames providedParameters = proxy.getRuntimeObjectOrNull(ParameterNamesKey.inst); - if (providedParameters != null) { - parameters = providedParameters.getParametersNames(); - } else { - parameters = null; - } - spInstance = proxy.getRuntimeObjectOrDefault(InstanceIdKey.inst, "N/A"); - } - - private String getTimeDelta(Long timestamp) { - long seconds = (System.currentTimeMillis() - timestamp)/1000; - return formatSeconds(seconds); - } - - private String formatSeconds(Long seconds) { - return String.format("%d:%02d:%02d", seconds/3600, (seconds%3600)/60, seconds%60); - } - - private String getInstanceName(Proxy proxy) { - String appInstanceName = proxy.getRuntimeValue(AppInstanceKey.inst); - if (appInstanceName.equals("_")) { - return "Default"; - } - return appInstanceName; - } - - } - - public static class ProxyInfoResponse { - public String status = "success"; - public List data; - } - -} +/** + * ShinyProxy + * + * Copyright (C) 2016-2023 Open Analytics + * + * =========================================================================== + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License as published by + * The Apache Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Apache License for more details. + * + * You should have received a copy of the Apache License + * along with this program. If not, see + */ +package eu.openanalytics.shinyproxy.controllers; + +import eu.openanalytics.containerproxy.api.dto.ApiResponse; +import eu.openanalytics.containerproxy.model.runtime.Container; +import eu.openanalytics.containerproxy.model.runtime.ParameterNames; +import eu.openanalytics.containerproxy.model.runtime.Proxy; +import eu.openanalytics.containerproxy.model.runtime.runtimevalues.BackendContainerNameKey; +import eu.openanalytics.containerproxy.model.runtime.runtimevalues.ContainerImageKey; +import eu.openanalytics.containerproxy.model.runtime.runtimevalues.HeartbeatTimeoutKey; +import eu.openanalytics.containerproxy.model.runtime.runtimevalues.InstanceIdKey; +import eu.openanalytics.containerproxy.model.runtime.runtimevalues.MaxLifetimeKey; +import eu.openanalytics.containerproxy.model.runtime.runtimevalues.ParameterNamesKey; +import eu.openanalytics.containerproxy.service.hearbeat.ActiveProxiesService; +import eu.openanalytics.shinyproxy.runtimevalues.AppInstanceKey; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +@Controller +public class AdminController extends BaseController { + + @Inject + private ActiveProxiesService activeProxiesService; + + @RequestMapping("/admin") + private String admin(ModelMap map, HttpServletRequest request) { + prepareMap(map, request); + + return "admin"; + } + + @Operation(summary = "Get active proxies of all users.", tags = "ShinyProxy") + @ApiResponses(value = { + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "Active proxies are returned.", + content = { + @Content( + mediaType = "application/json", + schema = @Schema(implementation = ProxyInfoResponse.class), + examples = { + @ExampleObject(value = "{\"status\": \"success\", \"data\": [{\"status\": \"Up\", \"proxyId\": \"9cd90bbb-ae9c-4016-9b9c-d2852b3a0bf6\", \"userId\": \"jack\", \"appName\": \"01_hello\", " + + "\"instanceName\": \"Default\", \"endpoint\": \"N/A\", \"uptime\": \"0:00:39\", \"lastHeartBeat\": \"0:00:05\", \"imageName\": \"openanalytics/shinyproxy-demo\", \"imageTag\": \"N/A\", " + + "\"heartbeatTimeout\": null, \"maxLifetime\": \"0:02:00\", \"spInstance\": \"9bec0d32754eab6a036bf1ee032bca82f98df0c5\", \"backendContainerName\": " + + "\"900b4f35b283401946db1d7cb8fe31ad5e6209d921b3cb9fd668ed6b9cbf7aa5\", \"parameters\": null}, {\"status\": \"Up\", \"proxyId\": \"b34d416e-ce6e-4351-a126-8836c88f2200\", \"userId\": " + + "\"jack\", \"appName\": \"06_tabsets\", \"instanceName\": \"Default\", \"endpoint\": \"N/A\", \"uptime\": \"0:00:18\", \"lastHeartBeat\": \"0:00:02\", \"imageName\": " + + "\"openanalytics/shinyproxy-demo\", \"imageTag\": \"N/A\", \"heartbeatTimeout\": null, \"maxLifetime\": \"0:02:00\", \"spInstance\": \"9bec0d32754eab6a036bf1ee032bca82f98df0c5\", " + + "\"backendContainerName\": \"2158b5b49c4138a9d0d6313fc4b62eba074b359473143be1d98102ab06c74bf8\", \"parameters\": null}]}") + } + ) + }), + }) + @RequestMapping(value = "/admin/data", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET) + @ResponseBody + private ResponseEntity>> adminData() { + List proxies = proxyService.getProxies(null, false); + List proxyInfos = proxies.stream().map(ProxyInfo::new).toList(); + return ApiResponse.success(proxyInfos); + } + + public static class ProxyInfoResponse { + public String status = "success"; + public List data; + } + + public class ProxyInfo { + + @Schema(allowableValues = {"New", "Up", "Stopping", "Pausing", "Paused", "Resuming", "Stopped"}) + public final String status; + + public final String proxyId; + public final String userId; + public final String appName; + public final String instanceName; + public final String endpoint; + public final String uptime; + public final String lastHeartBeat; + public final String imageName; + public final String imageTag; + public final String heartbeatTimeout; + public final String maxLifetime; + public final String spInstance; + public final String backendContainerName; + public final List parameters; + + public ProxyInfo(Proxy proxy) { + status = proxy.getStatus().toString(); + proxyId = proxy.getId(); + userId = proxy.getUserId(); + appName = proxy.getSpecId(); + instanceName = getInstanceName(proxy); + + if (proxy.getStartupTimestamp() > 0) { + uptime = getTimeDelta(proxy.getStartupTimestamp()); + } else { + uptime = "N/A"; + } + + Long heartBeat = activeProxiesService.getLastHeartBeat(proxy.getId()); + if (heartBeat == null) { + lastHeartBeat = "N/A"; + } else { + lastHeartBeat = getTimeDelta(heartBeat); + } + + if (!proxy.getContainers().isEmpty()) { + Container container = proxy.getContainers().get(0); + String[] parts = container.getRuntimeValue(ContainerImageKey.inst).split(":"); + imageName = parts[0]; + if (parts.length > 1) { + imageTag = parts[1]; + } else { + imageTag = "N/A"; + } + if (container.getTargets().containsKey(proxy.getId())) { + endpoint = container.getTargets().get(proxy.getId()).toString(); + } else { + endpoint = "N/A"; + } + backendContainerName = container.getRuntimeObjectOrDefault(BackendContainerNameKey.inst, "N/A"); + } else { + imageName = "N/A"; + imageTag = "N/A"; + endpoint = "N/A"; + backendContainerName = "N/A"; + } + + Long heartbeatTimeout = proxy.getRuntimeObjectOrNull(HeartbeatTimeoutKey.inst); + if (heartbeatTimeout != null && heartbeatTimeout != -1) { + this.heartbeatTimeout = formatSeconds(heartbeatTimeout / 1000); + } else { + this.heartbeatTimeout = null; + } + + Long maxLifetime = proxy.getRuntimeObjectOrNull(MaxLifetimeKey.inst); + if (maxLifetime != null && maxLifetime != -1) { + this.maxLifetime = formatSeconds(maxLifetime * 60); + } else { + this.maxLifetime = null; + } + + ParameterNames providedParameters = proxy.getRuntimeObjectOrNull(ParameterNamesKey.inst); + if (providedParameters != null) { + parameters = providedParameters.getParametersNames(); + } else { + parameters = null; + } + spInstance = proxy.getRuntimeObjectOrDefault(InstanceIdKey.inst, "N/A"); + } + + private String getTimeDelta(Long timestamp) { + long seconds = (System.currentTimeMillis() - timestamp) / 1000; + return formatSeconds(seconds); + } + + private String formatSeconds(Long seconds) { + return String.format("%d:%02d:%02d", seconds / 3600, (seconds % 3600) / 60, seconds % 60); + } + + private String getInstanceName(Proxy proxy) { + String appInstanceName = proxy.getRuntimeValue(AppInstanceKey.inst); + if (appInstanceName.equals("_")) { + return "Default"; + } + return appInstanceName; + } + + } + +} diff --git a/src/main/java/eu/openanalytics/shinyproxy/controllers/AppController.java b/src/main/java/eu/openanalytics/shinyproxy/controllers/AppController.java index a8bda3e0..2d296c77 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/controllers/AppController.java +++ b/src/main/java/eu/openanalytics/shinyproxy/controllers/AppController.java @@ -1,473 +1,472 @@ -/** - * ShinyProxy - * - * Copyright (C) 2016-2023 Open Analytics - * - * =========================================================================== - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Apache License as published by - * The Apache Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * Apache License for more details. - * - * You should have received a copy of the Apache License - * along with this program. If not, see - */ -package eu.openanalytics.shinyproxy.controllers; - -import com.fasterxml.jackson.annotation.JsonView; -import com.fasterxml.jackson.databind.ObjectMapper; -import eu.openanalytics.containerproxy.api.dto.ApiResponse; -import eu.openanalytics.containerproxy.api.dto.SwaggerDto; -import eu.openanalytics.containerproxy.auth.impl.OpenIDAuthenticationBackend; -import eu.openanalytics.containerproxy.model.Views; -import eu.openanalytics.containerproxy.model.runtime.AllowedParametersForUser; -import eu.openanalytics.containerproxy.model.runtime.ParameterValues; -import eu.openanalytics.containerproxy.model.runtime.Proxy; -import eu.openanalytics.containerproxy.model.runtime.runtimevalues.DisplayNameKey; -import eu.openanalytics.containerproxy.model.runtime.runtimevalues.ParameterValuesKey; -import eu.openanalytics.containerproxy.model.runtime.runtimevalues.RuntimeValue; -import eu.openanalytics.containerproxy.model.spec.ProxySpec; -import eu.openanalytics.containerproxy.service.AsyncProxyService; -import eu.openanalytics.containerproxy.service.InvalidParametersException; -import eu.openanalytics.containerproxy.service.ParametersService; -import eu.openanalytics.containerproxy.util.ContextPathHelper; -import eu.openanalytics.containerproxy.util.ProxyMappingManager; -import eu.openanalytics.shinyproxy.AppRequestInfo; -import eu.openanalytics.shinyproxy.ShinyProxyIframeScriptInjector; -import eu.openanalytics.shinyproxy.controllers.dto.ShinyProxyApiResponse; -import eu.openanalytics.shinyproxy.runtimevalues.AppInstanceKey; -import eu.openanalytics.shinyproxy.runtimevalues.PublicPathKey; -import eu.openanalytics.shinyproxy.runtimevalues.UserTimeZoneKey; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.ExampleObject; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Controller; -import org.springframework.ui.ModelMap; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.servlet.ModelAndView; -import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -import org.springframework.web.servlet.view.RedirectView; -import org.springframework.web.util.UriComponentsBuilder; -import org.thymeleaf.TemplateEngine; -import org.thymeleaf.context.ExpressionContext; -import org.thymeleaf.spring5.dialect.SpringStandardDialect; -import org.thymeleaf.templatemode.TemplateMode; -import org.thymeleaf.templateresolver.StringTemplateResolver; - -import javax.inject.Inject; -import javax.servlet.RequestDispatcher; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; - -import static org.springframework.web.bind.annotation.RequestMethod.GET; - -@Controller -public class AppController extends BaseController { - - @Inject - private ProxyMappingManager mappingManager; - - @Inject - private AsyncProxyService asyncProxyService; - - @Inject - private ParametersService parameterService; - - private final ObjectMapper objectMapper = new ObjectMapper(); - - public AppController() { - objectMapper.setConfig(objectMapper.getSerializationConfig() - .withView(Views.UserApi.class)); - } - - @RequestMapping(value={"/app_i/*/**", "/app/**"}, method= GET) - public ModelAndView app(ModelMap map, HttpServletRequest request, HttpServletResponse response) { - AppRequestInfo appRequestInfo = AppRequestInfo.fromRequestOrNull(request); - if (appRequestInfo == null) { - request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, HttpStatus.FORBIDDEN.value()); - return new ModelAndView("forward:/error"); - } - - Proxy proxy = findUserProxy(appRequestInfo); - - ProxySpec spec = proxyService.getProxySpec(appRequestInfo.getAppName()); - Optional redirect = createRedirectIfRequired(request, appRequestInfo, proxy, spec); - if (redirect.isPresent()) { - return new ModelAndView(redirect.get()); - } - - if (proxy == null && spec == null) { - request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, HttpStatus.FORBIDDEN.value()); - return new ModelAndView("forward:/error"); - } - - prepareMap(map, request); - map.put("heartbeatRate", getHeartbeatRate()); - map.put("page", "app"); - map.put("appName", appRequestInfo.getAppName()); - map.put("appInstance", appRequestInfo.getAppInstance()); - map.put("appInstanceDisplayName", appRequestInfo.getAppInstanceDisplayName()); - map.put("appPath", appRequestInfo.getAppPath()); - map.put("containerSubPath", buildContainerSubPath(request, appRequestInfo)); - map.put("refreshOpenidEnabled", authenticationBackend.getName().equals(OpenIDAuthenticationBackend.NAME)); - ParameterValues previousParameters = null; - if (proxy == null || proxy.getRuntimeObjectOrNull(DisplayNameKey.inst) == null) { - if (spec.getDisplayName() == null || spec.getDisplayName().isEmpty()) { - map.put("appTitle", spec.getId()); - } else { - map.put("appTitle", spec.getDisplayName()); - } - map.put("proxy", null); - } else { - map.put("appTitle", proxy.getRuntimeValue(DisplayNameKey.inst)); - previousParameters = proxy.getRuntimeObjectOrNull(ParameterValuesKey.inst); - } - map.put("proxy", secureProxy(proxy)); - if (spec != null && spec.getParameters() != null) { - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - AllowedParametersForUser allowedParametersForUser = parameterService.calculateAllowedParametersForUser(auth, spec, previousParameters); - map.put("parameterAllowedCombinations", allowedParametersForUser.getAllowedCombinations()); - map.put("parameterValues", allowedParametersForUser.getValues()); - map.put("parameterDefaults", allowedParametersForUser.getDefaultValue()); - map.put("parameterDefinitions", spec.getParameters().getDefinitions()); - map.put("parameterIds", spec.getParameters().getIds()); - - if (spec.getParameters().getTemplate() != null) { - map.put("parameterFragment", renderParameterTemplate(spec.getParameters().getTemplate(), map)); - } else { - map.put("parameterFragment", null); - } - } else { - map.put("parameterValues", null); - map.put("parameterDefaults", null); - map.put("parameterDefinitions", null); - map.put("parameterIds", null); - map.put("parameterFragment", null); - } - - return new ModelAndView("app", map); - } - - // TODO add example with timezone - @Operation(summary = "Start an app.", tags = "ShinyProxy", - requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( - content = @Content( - mediaType = "application/json", - schema = @Schema(implementation = AppBody.class), - examples = { - @ExampleObject(name = "With parameters", value = "{\"parameters\":{\"resources\":\"2 CPU cores - 8G RAM\",\"other_parameter\":\"example\"}}"), - @ExampleObject(name = "With timezone", value = "{\"timezone\":\"Europe/Brussels\"}") - } - ) - ) - ) - @ApiResponses(value = { - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "200", - description = "The proxy has been created.", - content = { - @Content( - mediaType = "application/json", - schema = @Schema(implementation = SwaggerDto.ProxyResponse.class), - examples = { - @ExampleObject(value = "{\"status\":\"success\",\"data\":{\"id\":\"cdaa8056-4f96-428e-91e8-bc13518d8987\",\"status\":\"New\",\"startupTimestamp\":0,\"createdTimestamp\":1671707875757," + - "\"userId\":\"jack\",\"specId\":\"01_hello\",\"displayName\":\"Hello Application\",\"containers\":[],\"runtimeValues\":{\"SHINYPROXY_FORCE_FULL_RELOAD\":false," + - "\"SHINYPROXY_WEBSOCKET_RECONNECTION_MODE\":\"None\",\"SHINYPROXY_MAX_INSTANCES\":100,\"SHINYPROXY_PUBLIC_PATH\":\"/app_proxy/cdaa8056-4f96-428e-91e8-bc13518d8987/\"," + - "\"SHINYPROXY_APP_INSTANCE\":\"default\"}}}\n") - } - ) - }), - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "400", - description = "Invalid request, app not started.", - content = { - @Content( - mediaType = "application/json", - examples = { - @ExampleObject(name = "Max instances reached", value = "{\"status\":\"fail\",\"data\":\"Cannot start new proxy because the maximum amount of instances of this proxy has been reached\"}"), - @ExampleObject(name = "Instance already exists", value = "{\"status\":\"fail\",\"data\":\"You already have an instance of this app with the given name\"}"), - @ExampleObject(name = "Parameters required", value = "{\"status\":\"fail\",\"data\":\"No parameters provided, but proxy spec expects parameters\"}"), - @ExampleObject(name = "Missing parameter", value = "{\"status\":\"fail\",\"data\":\"Missing value for parameter example\"}"), - @ExampleObject(name = "Invalid parameter value", value = "{\"status\":\"fail\",\"data\":\"Provided parameter values are not allowed\"}") - } - ) - }), - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "403", - description = "Proxy spec not found or no permission to use this proxy spec.", - content = { - @Content( - mediaType = "application/json", - examples = {@ExampleObject(value = "{\"status\": \"fail\", \"data\": \"forbidden\"}")} - ) - }), - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "500", - description = "Failed to start proxy.", - content = { - @Content( - mediaType = "application/json", - examples = {@ExampleObject(value = "{\"status\": \"fail\", \"data\": \"Failed to start proxy\"}")} - ) - }), - }) - @ResponseBody - @JsonView(Views.UserApi.class) - @RequestMapping(value = "/app_i/{specId}/{appInstanceName}", method = RequestMethod.POST) - public ResponseEntity> startApp(@PathVariable String specId, @PathVariable String appInstanceName, @RequestBody(required = false) AppBody appBody) { - ProxySpec spec = proxyService.getProxySpec(specId); - if (!userService.canAccess(spec)) { - return ApiResponse.failForbidden(); - } - Proxy proxy = findUserProxy(specId, appInstanceName); - if (proxy != null) { - return ApiResponse.fail("You already have an instance of this app with the given name"); - } - - if (!validateProxyStart(spec)) { - return ApiResponse.fail("Cannot start new app because the maximum amount of instances of this app has been reached"); - } - - List runtimeValues = shinyProxySpecProvider.getRuntimeValues(spec); - String id = UUID.randomUUID().toString(); - runtimeValues.add(new RuntimeValue(PublicPathKey.inst, getPublicPath(id))); - runtimeValues.add(new RuntimeValue(AppInstanceKey.inst, appInstanceName)); - if (appBody.getTimezone() != null) { - runtimeValues.add(new RuntimeValue(UserTimeZoneKey.inst, appBody.getTimezone())); - } - - try { - return ApiResponse.success(asyncProxyService.startProxy(spec, runtimeValues, id, (appBody != null) ? appBody.getParameters() : null)); - } catch (InvalidParametersException ex) { - return ApiResponse.fail(ex.getMessage()); - } catch (Throwable t ) { - return ApiResponse.error("Failed to start proxy"); - } - } - - @Operation(summary = "Proxy request to app. This endpoint is used to serve the iframe, hence it makes some assumptions. Do not use it directly or for embedding.", tags = "ShinyProxy") - @ApiResponses(value = { - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "401", - description = "User is not authenticated.", - content = { - @Content( - mediaType = "application/json", - examples = { - @ExampleObject(value = "{\"message\":\"shinyproxy_authentication_required\",\"status\":\"fail\"}") - } - ) - }), - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "410", - description = "App has been stopped or the app never existed or the user has no access to the app.", - content = { - @Content( - mediaType = "application/json", - examples = { - @ExampleObject(value = "{\"message\":\"app_stopped_or_non_existent\",\"status\":\"fail\"}") - } - ) - }), - }) - @RequestMapping(value={"/app_proxy/{proxyId}/**"}) - public void appProxy(@PathVariable String proxyId, HttpServletRequest request, HttpServletResponse response) throws IOException { - String requestUrl = request.getRequestURI().substring(getBasePublicPath().length()); - - Proxy proxy = proxyService.getProxy(proxyId); - if (proxy == null || proxy.getStatus().isUnavailable() || !userService.isOwner(proxy)) { - ShinyProxyApiResponse.appStoppedOrNonExistent(response); - return; - } - try { - mappingManager.dispatchAsync(proxy.getId(), requestUrl, request, response); - } catch (Exception e) { - throw new RuntimeException("Error routing proxy request", e); - } - } - - /** - * Special handler for HTML requests that inject the ShinyProxy iframe javascript. - */ - @RequestMapping(value={"/app_proxy/{proxyId}/**"}, produces= "text/html", method = GET) - public void appProxyHtml(@PathVariable String proxyId, HttpServletRequest request, HttpServletResponse response) throws IOException { - String requestUrl = request.getRequestURI().substring(getBasePublicPath().length()); - - Proxy proxy = proxyService.getProxy(proxyId); - if (proxy == null || proxy.getStatus().isUnavailable() || !userService.isOwner(proxy)) { - ShinyProxyApiResponse.appStoppedOrNonExistent(response); - return; - } - - String secFetchMode = request.getHeader("Sec-Fetch-Mode"); - if (secFetchMode != null && !secFetchMode.equals("navigate")) { - // do not inject script since this isn't a navigate request (it's e.g. an ajax/fetch request) - // note: the header is relatively new and therefore the script is injected if the header is not present - // see: #30809 - try { - mappingManager.dispatchAsync(proxy.getId(), requestUrl, request, response); - return; - } catch (Exception e) { - throw new RuntimeException("Error routing proxy request", e); - } - } - - try { - mappingManager.dispatchAsync(proxyId, requestUrl, request, response, (exchange) -> { - exchange.getRequestHeaders().remove("Accept-Encoding"); // ensure no encoding is used - exchange.addResponseWrapper((factory, exchange1) -> new ShinyProxyIframeScriptInjector(factory.create(), exchange1)); - }); - } catch (Exception e) { - throw new RuntimeException("Error routing proxy request", e); - } - } - - private String buildContainerSubPath(HttpServletRequest request, AppRequestInfo appRequestInfo) { - String queryString = ServletUriComponentsBuilder.fromRequest(request) - .replaceQueryParam("sp_hide_navbar") - .replaceQueryParam("sp_instance_override") - .build().getQuery(); - - String res = UriComponentsBuilder - .fromPath(appRequestInfo.getSubPath()) - .query(queryString) - .build(false) // #30932: queryString is not yet encoded - .toUriString(); - - if (res.startsWith("/")) { - return res.substring(1); - } - return res; - } - - private String getPublicPath(String proxyId) { - return getBasePublicPath() + proxyId + "/"; - } - - private String getBasePublicPath() { - return ContextPathHelper.withEndingSlash() + "app_proxy/"; - } - - /** - * Checks if a redirect is required before we can handle the request. - *

- * ShinyProxy supports proxying to multiple targets. When proxying to a target (without a sub-path for that specific target), the URL must end with a slash. - * However, when the sub-path does not point to a specific target, it's not required that the URL ends with a slash. - *

- *

- * Assume an app called `myapp` has a additional-port-mapping named `abc`: - * - /app/myapp -> no redirect required (getPublicPath() always add a slash) - * - /app/myapp/test123 -> no redirect required - * - /app/myapp/abc -> redirect to /app/myapp/abc/ - * - /app/myapp/abc/ -> no redirect required - * - /app/myapp/abc/test -> no redirect required - *

- * @param request the current request - * @param appRequestInfo the appRequstInfo for this request - * @param proxy the current proxy - * @param spec the spec of the current app - * @return a RedirectView if a redirect is needed - */ - private Optional createRedirectIfRequired(HttpServletRequest request, AppRequestInfo appRequestInfo, Proxy proxy, ProxySpec spec) { - // if sub-path is empty or it's a slash -> no redirect required - if (appRequestInfo.getSubPath() == null || appRequestInfo.getSubPath().equals("/")) { - return Optional.empty(); - } - - // sub-path always starts with a slash -> get part without the slash - // this contains the mapping and any additional paths - String subPath = appRequestInfo.getSubPath().substring(1); - - // if the subPath contains a slash -> no redirect required - // e.g. /app/myapp/mapping/ - // e.g. /app/myapp/mapping/some_path - // ^^^^^^^^^^^^^^^^^^ -> this is the subpath (without initial slash) - if (subPath.contains("/")) { - return Optional.empty(); - } - - // the provided subpath does not contain a slash (i.e. it's a single "directory" name) - // -> we have to check whether the provided subpath is a configured mapping (and thus point to a specific port on the app) - // or whether it's just a subpath - boolean isMappingWithoutSlash = spec.getContainerSpecs().get(0) - .getPortMapping() - .stream() - .anyMatch(it -> it.getName().equals(subPath)); - if (isMappingWithoutSlash) { - // the provided subpath is a configured mapping -> redirect so it ends with a slash - String uri = ServletUriComponentsBuilder.fromRequest(request) - .path("/") - .build() - .toUriString(); - return Optional.of(new RedirectView(uri)); - } - - return Optional.empty(); - } - - private String renderParameterTemplate(String template, ModelMap map) { - TemplateEngine templateEngine = new TemplateEngine(); - StringTemplateResolver stringTemplateResolver = new StringTemplateResolver(); - stringTemplateResolver.setTemplateMode(TemplateMode.HTML); - stringTemplateResolver.setCacheable(false); - - templateEngine.setTemplateResolver(stringTemplateResolver); - templateEngine.setDialect(new SpringStandardDialect()); - - ExpressionContext context = new ExpressionContext(templateEngine.getConfiguration(), null, map); - return templateEngine.process(template, context); - } - - /** - * Converts a proxy into an Object using {@link Views.UserApi} view, in order to hide security sensitive values. - * @return the secured proxy - */ - private Object secureProxy(Proxy proxy) { - return objectMapper.convertValue(proxy, Object.class); - } - - private static class AppBody { - private Map parameters; - private String timezone; - - @Schema(description = "Map of parameters for the app.", requiredMode = Schema.RequiredMode.NOT_REQUIRED) - public Map getParameters() { - return parameters; - } - - public void setParameters(Map parameters) { - this.parameters = parameters; - } - - @Schema(description = "The timezone of the user in TZ format.", requiredMode = Schema.RequiredMode.NOT_REQUIRED) - public String getTimezone() { - return timezone; - } - - public void setTimezone(String timezone) { - this.timezone = timezone; - } - } - -} +/** + * ShinyProxy + * + * Copyright (C) 2016-2023 Open Analytics + * + * =========================================================================== + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License as published by + * The Apache Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Apache License for more details. + * + * You should have received a copy of the Apache License + * along with this program. If not, see + */ +package eu.openanalytics.shinyproxy.controllers; + +import com.fasterxml.jackson.annotation.JsonView; +import com.fasterxml.jackson.databind.ObjectMapper; +import eu.openanalytics.containerproxy.api.dto.ApiResponse; +import eu.openanalytics.containerproxy.api.dto.SwaggerDto; +import eu.openanalytics.containerproxy.auth.impl.OpenIDAuthenticationBackend; +import eu.openanalytics.containerproxy.model.Views; +import eu.openanalytics.containerproxy.model.runtime.AllowedParametersForUser; +import eu.openanalytics.containerproxy.model.runtime.ParameterValues; +import eu.openanalytics.containerproxy.model.runtime.Proxy; +import eu.openanalytics.containerproxy.model.runtime.runtimevalues.DisplayNameKey; +import eu.openanalytics.containerproxy.model.runtime.runtimevalues.ParameterValuesKey; +import eu.openanalytics.containerproxy.model.runtime.runtimevalues.RuntimeValue; +import eu.openanalytics.containerproxy.model.spec.ProxySpec; +import eu.openanalytics.containerproxy.service.AsyncProxyService; +import eu.openanalytics.containerproxy.service.InvalidParametersException; +import eu.openanalytics.containerproxy.service.ParametersService; +import eu.openanalytics.containerproxy.util.ContextPathHelper; +import eu.openanalytics.containerproxy.util.ProxyMappingManager; +import eu.openanalytics.shinyproxy.AppRequestInfo; +import eu.openanalytics.shinyproxy.ShinyProxyIframeScriptInjector; +import eu.openanalytics.shinyproxy.controllers.dto.ShinyProxyApiResponse; +import eu.openanalytics.shinyproxy.runtimevalues.AppInstanceKey; +import eu.openanalytics.shinyproxy.runtimevalues.PublicPathKey; +import eu.openanalytics.shinyproxy.runtimevalues.UserTimeZoneKey; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import org.springframework.web.servlet.view.RedirectView; +import org.springframework.web.util.UriComponentsBuilder; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.ExpressionContext; +import org.thymeleaf.spring5.dialect.SpringStandardDialect; +import org.thymeleaf.templatemode.TemplateMode; +import org.thymeleaf.templateresolver.StringTemplateResolver; + +import javax.inject.Inject; +import javax.servlet.RequestDispatcher; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import static org.springframework.web.bind.annotation.RequestMethod.GET; + +@Controller +public class AppController extends BaseController { + + private final ObjectMapper objectMapper = new ObjectMapper(); + @Inject + private ProxyMappingManager mappingManager; + @Inject + private AsyncProxyService asyncProxyService; + @Inject + private ParametersService parameterService; + + public AppController() { + objectMapper.setConfig(objectMapper.getSerializationConfig() + .withView(Views.UserApi.class)); + } + + @RequestMapping(value = {"/app_i/*/**", "/app/**"}, method = GET) + public ModelAndView app(ModelMap map, HttpServletRequest request, HttpServletResponse response) { + AppRequestInfo appRequestInfo = AppRequestInfo.fromRequestOrNull(request); + if (appRequestInfo == null) { + request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, HttpStatus.FORBIDDEN.value()); + return new ModelAndView("forward:/error"); + } + + Proxy proxy = findUserProxy(appRequestInfo); + + ProxySpec spec = proxyService.getProxySpec(appRequestInfo.getAppName()); + Optional redirect = createRedirectIfRequired(request, appRequestInfo, proxy, spec); + if (redirect.isPresent()) { + return new ModelAndView(redirect.get()); + } + + if (proxy == null && spec == null) { + request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, HttpStatus.FORBIDDEN.value()); + return new ModelAndView("forward:/error"); + } + + prepareMap(map, request); + map.put("heartbeatRate", getHeartbeatRate()); + map.put("page", "app"); + map.put("appName", appRequestInfo.getAppName()); + map.put("appInstance", appRequestInfo.getAppInstance()); + map.put("appInstanceDisplayName", appRequestInfo.getAppInstanceDisplayName()); + map.put("appPath", appRequestInfo.getAppPath()); + map.put("containerSubPath", buildContainerSubPath(request, appRequestInfo)); + map.put("refreshOpenidEnabled", authenticationBackend.getName().equals(OpenIDAuthenticationBackend.NAME)); + ParameterValues previousParameters = null; + if (proxy == null || proxy.getRuntimeObjectOrNull(DisplayNameKey.inst) == null) { + if (spec.getDisplayName() == null || spec.getDisplayName().isEmpty()) { + map.put("appTitle", spec.getId()); + } else { + map.put("appTitle", spec.getDisplayName()); + } + map.put("proxy", null); + } else { + map.put("appTitle", proxy.getRuntimeValue(DisplayNameKey.inst)); + previousParameters = proxy.getRuntimeObjectOrNull(ParameterValuesKey.inst); + } + map.put("proxy", secureProxy(proxy)); + if (spec != null && spec.getParameters() != null) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + AllowedParametersForUser allowedParametersForUser = parameterService.calculateAllowedParametersForUser(auth, spec, previousParameters); + map.put("parameterAllowedCombinations", allowedParametersForUser.getAllowedCombinations()); + map.put("parameterValues", allowedParametersForUser.getValues()); + map.put("parameterDefaults", allowedParametersForUser.getDefaultValue()); + map.put("parameterDefinitions", spec.getParameters().getDefinitions()); + map.put("parameterIds", spec.getParameters().getIds()); + + if (spec.getParameters().getTemplate() != null) { + map.put("parameterFragment", renderParameterTemplate(spec.getParameters().getTemplate(), map)); + } else { + map.put("parameterFragment", null); + } + } else { + map.put("parameterValues", null); + map.put("parameterDefaults", null); + map.put("parameterDefinitions", null); + map.put("parameterIds", null); + map.put("parameterFragment", null); + } + + return new ModelAndView("app", map); + } + + // TODO add example with timezone + @Operation(summary = "Start an app.", tags = "ShinyProxy", + requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = AppBody.class), + examples = { + @ExampleObject(name = "With parameters", value = "{\"parameters\":{\"resources\":\"2 CPU cores - 8G RAM\",\"other_parameter\":\"example\"}}"), + @ExampleObject(name = "With timezone", value = "{\"timezone\":\"Europe/Brussels\"}") + } + ) + ) + ) + @ApiResponses(value = { + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "The proxy has been created.", + content = { + @Content( + mediaType = "application/json", + schema = @Schema(implementation = SwaggerDto.ProxyResponse.class), + examples = { + @ExampleObject(value = "{\"status\":\"success\",\"data\":{\"id\":\"cdaa8056-4f96-428e-91e8-bc13518d8987\",\"status\":\"New\",\"startupTimestamp\":0,\"createdTimestamp\":1671707875757," + + "\"userId\":\"jack\",\"specId\":\"01_hello\",\"displayName\":\"Hello Application\",\"containers\":[],\"runtimeValues\":{\"SHINYPROXY_FORCE_FULL_RELOAD\":false," + + "\"SHINYPROXY_WEBSOCKET_RECONNECTION_MODE\":\"None\",\"SHINYPROXY_MAX_INSTANCES\":100,\"SHINYPROXY_PUBLIC_PATH\":\"/app_proxy/cdaa8056-4f96-428e-91e8-bc13518d8987/\"," + + "\"SHINYPROXY_APP_INSTANCE\":\"default\"}}}\n") + } + ) + }), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "400", + description = "Invalid request, app not started.", + content = { + @Content( + mediaType = "application/json", + examples = { + @ExampleObject(name = "Max instances reached", value = "{\"status\":\"fail\",\"data\":\"Cannot start new proxy because the maximum amount of instances of this proxy has been reached\"}"), + @ExampleObject(name = "Instance already exists", value = "{\"status\":\"fail\",\"data\":\"You already have an instance of this app with the given name\"}"), + @ExampleObject(name = "Parameters required", value = "{\"status\":\"fail\",\"data\":\"No parameters provided, but proxy spec expects parameters\"}"), + @ExampleObject(name = "Missing parameter", value = "{\"status\":\"fail\",\"data\":\"Missing value for parameter example\"}"), + @ExampleObject(name = "Invalid parameter value", value = "{\"status\":\"fail\",\"data\":\"Provided parameter values are not allowed\"}") + } + ) + }), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "403", + description = "Proxy spec not found or no permission to use this proxy spec.", + content = { + @Content( + mediaType = "application/json", + examples = {@ExampleObject(value = "{\"status\": \"fail\", \"data\": \"forbidden\"}")} + ) + }), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "500", + description = "Failed to start proxy.", + content = { + @Content( + mediaType = "application/json", + examples = {@ExampleObject(value = "{\"status\": \"fail\", \"data\": \"Failed to start proxy\"}")} + ) + }), + }) + @ResponseBody + @JsonView(Views.UserApi.class) + @RequestMapping(value = "/app_i/{specId}/{appInstanceName}", method = RequestMethod.POST) + public ResponseEntity> startApp(@PathVariable String specId, @PathVariable String appInstanceName, @RequestBody(required = false) AppBody appBody) { + ProxySpec spec = proxyService.getProxySpec(specId); + if (!userService.canAccess(spec)) { + return ApiResponse.failForbidden(); + } + Proxy proxy = findUserProxy(specId, appInstanceName); + if (proxy != null) { + return ApiResponse.fail("You already have an instance of this app with the given name"); + } + + if (!validateProxyStart(spec)) { + return ApiResponse.fail("Cannot start new app because the maximum amount of instances of this app has been reached"); + } + + List runtimeValues = shinyProxySpecProvider.getRuntimeValues(spec); + String id = UUID.randomUUID().toString(); + runtimeValues.add(new RuntimeValue(PublicPathKey.inst, getPublicPath(id))); + runtimeValues.add(new RuntimeValue(AppInstanceKey.inst, appInstanceName)); + if (appBody.getTimezone() != null) { + runtimeValues.add(new RuntimeValue(UserTimeZoneKey.inst, appBody.getTimezone())); + } + + try { + return ApiResponse.success(asyncProxyService.startProxy(spec, runtimeValues, id, (appBody != null) ? appBody.getParameters() : null)); + } catch (InvalidParametersException ex) { + return ApiResponse.fail(ex.getMessage()); + } catch (Throwable t) { + return ApiResponse.error("Failed to start proxy"); + } + } + + @Operation(summary = "Proxy request to app. This endpoint is used to serve the iframe, hence it makes some assumptions. Do not use it directly or for embedding.", tags = "ShinyProxy") + @ApiResponses(value = { + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "401", + description = "User is not authenticated.", + content = { + @Content( + mediaType = "application/json", + examples = { + @ExampleObject(value = "{\"message\":\"shinyproxy_authentication_required\",\"status\":\"fail\"}") + } + ) + }), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "410", + description = "App has been stopped or the app never existed or the user has no access to the app.", + content = { + @Content( + mediaType = "application/json", + examples = { + @ExampleObject(value = "{\"message\":\"app_stopped_or_non_existent\",\"status\":\"fail\"}") + } + ) + }), + }) + @RequestMapping(value = {"/app_proxy/{proxyId}/**"}) + public void appProxy(@PathVariable String proxyId, HttpServletRequest request, HttpServletResponse response) throws IOException { + String requestUrl = request.getRequestURI().substring(getBasePublicPath().length()); + + Proxy proxy = proxyService.getProxy(proxyId); + if (proxy == null || proxy.getStatus().isUnavailable() || !userService.isOwner(proxy)) { + ShinyProxyApiResponse.appStoppedOrNonExistent(response); + return; + } + try { + mappingManager.dispatchAsync(proxy.getId(), requestUrl, request, response); + } catch (Exception e) { + throw new RuntimeException("Error routing proxy request", e); + } + } + + /** + * Special handler for HTML requests that inject the ShinyProxy iframe javascript. + */ + @RequestMapping(value = {"/app_proxy/{proxyId}/**"}, produces = "text/html", method = GET) + public void appProxyHtml(@PathVariable String proxyId, HttpServletRequest request, HttpServletResponse response) throws IOException { + String requestUrl = request.getRequestURI().substring(getBasePublicPath().length()); + + Proxy proxy = proxyService.getProxy(proxyId); + if (proxy == null || proxy.getStatus().isUnavailable() || !userService.isOwner(proxy)) { + ShinyProxyApiResponse.appStoppedOrNonExistent(response); + return; + } + + String secFetchMode = request.getHeader("Sec-Fetch-Mode"); + if (secFetchMode != null && !secFetchMode.equals("navigate")) { + // do not inject script since this isn't a navigate request (it's e.g. an ajax/fetch request) + // note: the header is relatively new and therefore the script is injected if the header is not present + // see: #30809 + try { + mappingManager.dispatchAsync(proxy.getId(), requestUrl, request, response); + return; + } catch (Exception e) { + throw new RuntimeException("Error routing proxy request", e); + } + } + + try { + mappingManager.dispatchAsync(proxyId, requestUrl, request, response, (exchange) -> { + exchange.getRequestHeaders().remove("Accept-Encoding"); // ensure no encoding is used + exchange.addResponseWrapper((factory, exchange1) -> new ShinyProxyIframeScriptInjector(factory.create(), exchange1)); + }); + } catch (Exception e) { + throw new RuntimeException("Error routing proxy request", e); + } + } + + private String buildContainerSubPath(HttpServletRequest request, AppRequestInfo appRequestInfo) { + String queryString = ServletUriComponentsBuilder.fromRequest(request) + .replaceQueryParam("sp_hide_navbar") + .replaceQueryParam("sp_instance_override") + .build().getQuery(); + + String res = UriComponentsBuilder + .fromPath(appRequestInfo.getSubPath()) + .query(queryString) + .build(false) // #30932: queryString is not yet encoded + .toUriString(); + + if (res.startsWith("/")) { + return res.substring(1); + } + return res; + } + + private String getPublicPath(String proxyId) { + return getBasePublicPath() + proxyId + "/"; + } + + private String getBasePublicPath() { + return ContextPathHelper.withEndingSlash() + "app_proxy/"; + } + + /** + * Checks if a redirect is required before we can handle the request. + *

+ * ShinyProxy supports proxying to multiple targets. When proxying to a target (without a sub-path for that specific target), the URL must end with a slash. + * However, when the sub-path does not point to a specific target, it's not required that the URL ends with a slash. + *

+ *

+ * Assume an app called `myapp` has a additional-port-mapping named `abc`: + * - /app/myapp -> no redirect required (getPublicPath() always add a slash) + * - /app/myapp/test123 -> no redirect required + * - /app/myapp/abc -> redirect to /app/myapp/abc/ + * - /app/myapp/abc/ -> no redirect required + * - /app/myapp/abc/test -> no redirect required + *

+ * + * @param request the current request + * @param appRequestInfo the appRequstInfo for this request + * @param proxy the current proxy + * @param spec the spec of the current app + * @return a RedirectView if a redirect is needed + */ + private Optional createRedirectIfRequired(HttpServletRequest request, AppRequestInfo appRequestInfo, Proxy proxy, ProxySpec spec) { + // if sub-path is empty or it's a slash -> no redirect required + if (appRequestInfo.getSubPath() == null || appRequestInfo.getSubPath().equals("/")) { + return Optional.empty(); + } + + // sub-path always starts with a slash -> get part without the slash + // this contains the mapping and any additional paths + String subPath = appRequestInfo.getSubPath().substring(1); + + // if the subPath contains a slash -> no redirect required + // e.g. /app/myapp/mapping/ + // e.g. /app/myapp/mapping/some_path + // ^^^^^^^^^^^^^^^^^^ -> this is the subpath (without initial slash) + if (subPath.contains("/")) { + return Optional.empty(); + } + + // the provided subpath does not contain a slash (i.e. it's a single "directory" name) + // -> we have to check whether the provided subpath is a configured mapping (and thus point to a specific port on the app) + // or whether it's just a subpath + boolean isMappingWithoutSlash = spec.getContainerSpecs().get(0) + .getPortMapping() + .stream() + .anyMatch(it -> it.getName().equals(subPath)); + if (isMappingWithoutSlash) { + // the provided subpath is a configured mapping -> redirect so it ends with a slash + String uri = ServletUriComponentsBuilder.fromRequest(request) + .path("/") + .build() + .toUriString(); + return Optional.of(new RedirectView(uri)); + } + + return Optional.empty(); + } + + private String renderParameterTemplate(String template, ModelMap map) { + TemplateEngine templateEngine = new TemplateEngine(); + StringTemplateResolver stringTemplateResolver = new StringTemplateResolver(); + stringTemplateResolver.setTemplateMode(TemplateMode.HTML); + stringTemplateResolver.setCacheable(false); + + templateEngine.setTemplateResolver(stringTemplateResolver); + templateEngine.setDialect(new SpringStandardDialect()); + + ExpressionContext context = new ExpressionContext(templateEngine.getConfiguration(), null, map); + return templateEngine.process(template, context); + } + + /** + * Converts a proxy into an Object using {@link Views.UserApi} view, in order to hide security sensitive values. + * + * @return the secured proxy + */ + private Object secureProxy(Proxy proxy) { + return objectMapper.convertValue(proxy, Object.class); + } + + private static class AppBody { + private Map parameters; + private String timezone; + + @Schema(description = "Map of parameters for the app.", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + public Map getParameters() { + return parameters; + } + + public void setParameters(Map parameters) { + this.parameters = parameters; + } + + @Schema(description = "The timezone of the user in TZ format.", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + public String getTimezone() { + return timezone; + } + + public void setTimezone(String timezone) { + this.timezone = timezone; + } + } + +} diff --git a/src/main/java/eu/openanalytics/shinyproxy/controllers/AppDirectController.java b/src/main/java/eu/openanalytics/shinyproxy/controllers/AppDirectController.java index b7d98c55..72ffb5be 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/controllers/AppDirectController.java +++ b/src/main/java/eu/openanalytics/shinyproxy/controllers/AppDirectController.java @@ -115,7 +115,7 @@ private Proxy getOrStart(AppRequestInfo appRequestInfo, HttpServletRequest reque return proxy; } else if (proxy.getStatus() == ProxyStatus.New) { // maximum wait 10 minutes for the app to startup - for (int i = 0; i < 600; i++ ) { + for (int i = 0; i < 600; i++) { try { Thread.sleep(1000); } catch (InterruptedException ex) { diff --git a/src/main/java/eu/openanalytics/shinyproxy/controllers/BaseController.java b/src/main/java/eu/openanalytics/shinyproxy/controllers/BaseController.java index 2955b1cb..614de22d 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/controllers/BaseController.java +++ b/src/main/java/eu/openanalytics/shinyproxy/controllers/BaseController.java @@ -54,138 +54,130 @@ public abstract class BaseController { - @Inject - ProxyService proxyService; - - @Inject - UserService userService; - - @Inject - Environment environment; - - @Inject - IAuthenticationBackend authenticationBackend; - - @Inject - HeartbeatService heartbeatService; - - @Inject - IdentifierService identifierService; - - @Inject - protected ShinyProxySpecProvider shinyProxySpecProvider; - - @Inject - private IContainerBackend backend; - - private static final Logger logger = LogManager.getLogger(BaseController.class); - private static final Map imageCache = new HashMap<>(); - - protected long getHeartbeatRate() { - return heartbeatService.getHeartbeatRate(); - } - - protected Proxy findUserProxy(AppRequestInfo appRequestInfo) { - return findUserProxy(appRequestInfo.getAppName(), appRequestInfo.getAppInstance()); - } - - protected Proxy findUserProxy(String appname, String appInstance) { - return proxyService.findProxy(p -> - p.getSpecId().equals(appname) - && p.getRuntimeValue(AppInstanceKey.inst).equals(appInstance) - && userService.isOwner(p), - false); - } - - protected String getProxyEndpoint(Proxy proxy) { - if (proxy == null || proxy.getContainers().get(0).getTargets().isEmpty()) return null; - return proxy.getContainers().get(0).getTargets().keySet().iterator().next(); - } - - protected void prepareMap(ModelMap map, HttpServletRequest request) { + private static final Logger logger = LogManager.getLogger(BaseController.class); + private static final Map imageCache = new HashMap<>(); + @Inject + protected ShinyProxySpecProvider shinyProxySpecProvider; + @Inject + ProxyService proxyService; + @Inject + UserService userService; + @Inject + Environment environment; + @Inject + IAuthenticationBackend authenticationBackend; + @Inject + HeartbeatService heartbeatService; + @Inject + IdentifierService identifierService; + @Inject + private IContainerBackend backend; + + protected long getHeartbeatRate() { + return heartbeatService.getHeartbeatRate(); + } + + protected Proxy findUserProxy(AppRequestInfo appRequestInfo) { + return findUserProxy(appRequestInfo.getAppName(), appRequestInfo.getAppInstance()); + } + + protected Proxy findUserProxy(String appname, String appInstance) { + return proxyService.findProxy(p -> + p.getSpecId().equals(appname) + && p.getRuntimeValue(AppInstanceKey.inst).equals(appInstance) + && userService.isOwner(p), + false); + } + + protected String getProxyEndpoint(Proxy proxy) { + if (proxy == null || proxy.getContainers().get(0).getTargets().isEmpty()) return null; + return proxy.getContainers().get(0).getTargets().keySet().iterator().next(); + } + + protected void prepareMap(ModelMap map, HttpServletRequest request) { map.put("application_name", environment.getProperty("spring.application.name")); // name of ShinyProxy, ContainerProxy etc - map.put("title", environment.getProperty("proxy.title", "ShinyProxy")); - map.put("logo", resolveImageURI(environment.getProperty("proxy.logo-url"))); - - String hideNavBarParam = request.getParameter("sp_hide_navbar"); - if (Objects.equals(hideNavBarParam, "true")) { - map.put("showNavbar", false); - } else { - map.put("showNavbar", !Boolean.parseBoolean(environment.getProperty("proxy.hide-navbar"))); - } - - map.put("bootstrapCss", "/webjars/bootstrap/3.4.1/css/bootstrap.min.css"); - map.put("bootstrapJs", "/webjars/bootstrap/3.4.1/js/bootstrap.min.js"); - map.put("jqueryJs", "/webjars/jquery/3.6.1/jquery.min.js"); - map.put("handlebars", "/webjars/handlebars/4.7.7/handlebars.runtime.min.js"); - - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - boolean isLoggedIn = authentication != null && !(authentication instanceof AnonymousAuthenticationToken) && authentication.isAuthenticated(); - map.put("isLoggedIn", isLoggedIn); - map.put("isAdmin", userService.isAdmin(authentication)); - map.put("isSupportEnabled", isLoggedIn && getSupportAddress() != null); - map.put("logoutUrl", authenticationBackend.getLogoutURL()); - map.put("page", ""); // defaults, used in navbar - map.put("maxInstances", 0); // defaults, used in navbar - map.put("contextPath", ContextPathHelper.withEndingSlash()); - map.put("resourcePrefix", "/" + identifierService.instanceId); - map.put("appMaxInstances", shinyProxySpecProvider.getMaxInstances()); - map.put("pauseSupported", backend.supportsPause()); - map.put("spInstance", identifierService.instanceId); - } - - protected String getSupportAddress() { - return environment.getProperty("proxy.support.mail-to-address"); - } - - protected String resolveImageURI(String resourceURI) { - if (resourceURI == null || resourceURI.isEmpty()) return resourceURI; - if (imageCache.containsKey(resourceURI)) return imageCache.get(resourceURI); - - String resolvedValue = resourceURI; - if (resourceURI.toLowerCase().startsWith("file://")) { - String mimetype = URLConnection.guessContentTypeFromName(resourceURI); - if (mimetype == null) { - logger.warn("Cannot determine mimetype for resource: " + resourceURI); - } else { - try (InputStream input = new URL(resourceURI).openConnection().getInputStream()) { - byte[] data = StreamUtils.copyToByteArray(input); - String encoded = Base64.getEncoder().encodeToString(data); - resolvedValue = String.format("data:%s;base64,%s", mimetype, encoded); - } catch (IOException e) { - logger.warn("Failed to convert file URI to data URI: " + resourceURI, e); - } - } - } - imageCache.put(resourceURI, resolvedValue); - return resolvedValue; - } - - /** - * Validates whether a proxy should be allowed to start. - */ - protected boolean validateProxyStart(ProxySpec spec) { - Integer maxInstances = shinyProxySpecProvider.getMaxInstancesForSpec(spec); - - if (maxInstances == -1) { - return true; - } - - // note: there is a very small change that the user is able to start more instances than allowed, if the user - // starts many proxies at once. E.g. in the following scenario: - // - max proxies = 2 - // - user starts a proxy - // - user sends a start proxy request -> this function is called and returns true - // - just before this new proxy is added to the list of active proxies, the user sends a new start proxy request - // - again this new proxy is allowed, because there is still only one proxy in the list of active proxies - // -> the user has three proxies running. - // Because of chance that this happens is small and that the consequences are low, we accept this risk. - int currentAmountOfInstances = proxyService.getProxies( - p -> p.getSpecId().equals(spec.getId()) - && userService.isOwner(p), - false).size(); - - return currentAmountOfInstances < maxInstances; - } + map.put("title", environment.getProperty("proxy.title", "ShinyProxy")); + map.put("logo", resolveImageURI(environment.getProperty("proxy.logo-url"))); + + String hideNavBarParam = request.getParameter("sp_hide_navbar"); + if (Objects.equals(hideNavBarParam, "true")) { + map.put("showNavbar", false); + } else { + map.put("showNavbar", !Boolean.parseBoolean(environment.getProperty("proxy.hide-navbar"))); + } + + map.put("bootstrapCss", "/webjars/bootstrap/3.4.1/css/bootstrap.min.css"); + map.put("bootstrapJs", "/webjars/bootstrap/3.4.1/js/bootstrap.min.js"); + map.put("jqueryJs", "/webjars/jquery/3.6.1/jquery.min.js"); + map.put("handlebars", "/webjars/handlebars/4.7.7/handlebars.runtime.min.js"); + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + boolean isLoggedIn = authentication != null && !(authentication instanceof AnonymousAuthenticationToken) && authentication.isAuthenticated(); + map.put("isLoggedIn", isLoggedIn); + map.put("isAdmin", userService.isAdmin(authentication)); + map.put("isSupportEnabled", isLoggedIn && getSupportAddress() != null); + map.put("logoutUrl", authenticationBackend.getLogoutURL()); + map.put("page", ""); // defaults, used in navbar + map.put("maxInstances", 0); // defaults, used in navbar + map.put("contextPath", ContextPathHelper.withEndingSlash()); + map.put("resourcePrefix", "/" + identifierService.instanceId); + map.put("appMaxInstances", shinyProxySpecProvider.getMaxInstances()); + map.put("pauseSupported", backend.supportsPause()); + map.put("spInstance", identifierService.instanceId); + } + + protected String getSupportAddress() { + return environment.getProperty("proxy.support.mail-to-address"); + } + + protected String resolveImageURI(String resourceURI) { + if (resourceURI == null || resourceURI.isEmpty()) return resourceURI; + if (imageCache.containsKey(resourceURI)) return imageCache.get(resourceURI); + + String resolvedValue = resourceURI; + if (resourceURI.toLowerCase().startsWith("file://")) { + String mimetype = URLConnection.guessContentTypeFromName(resourceURI); + if (mimetype == null) { + logger.warn("Cannot determine mimetype for resource: " + resourceURI); + } else { + try (InputStream input = new URL(resourceURI).openConnection().getInputStream()) { + byte[] data = StreamUtils.copyToByteArray(input); + String encoded = Base64.getEncoder().encodeToString(data); + resolvedValue = String.format("data:%s;base64,%s", mimetype, encoded); + } catch (IOException e) { + logger.warn("Failed to convert file URI to data URI: " + resourceURI, e); + } + } + } + imageCache.put(resourceURI, resolvedValue); + return resolvedValue; + } + + /** + * Validates whether a proxy should be allowed to start. + */ + protected boolean validateProxyStart(ProxySpec spec) { + Integer maxInstances = shinyProxySpecProvider.getMaxInstancesForSpec(spec); + + if (maxInstances == -1) { + return true; + } + + // note: there is a very small change that the user is able to start more instances than allowed, if the user + // starts many proxies at once. E.g. in the following scenario: + // - max proxies = 2 + // - user starts a proxy + // - user sends a start proxy request -> this function is called and returns true + // - just before this new proxy is added to the list of active proxies, the user sends a new start proxy request + // - again this new proxy is allowed, because there is still only one proxy in the list of active proxies + // -> the user has three proxies running. + // Because of chance that this happens is small and that the consequences are low, we accept this risk. + int currentAmountOfInstances = proxyService.getProxies( + p -> p.getSpecId().equals(spec.getId()) + && userService.isOwner(p), + false).size(); + + return currentAmountOfInstances < maxInstances; + } } diff --git a/src/main/java/eu/openanalytics/shinyproxy/controllers/HeartbeatController.java b/src/main/java/eu/openanalytics/shinyproxy/controllers/HeartbeatController.java index bb3f8e85..319a7f26 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/controllers/HeartbeatController.java +++ b/src/main/java/eu/openanalytics/shinyproxy/controllers/HeartbeatController.java @@ -20,8 +20,8 @@ */ package eu.openanalytics.shinyproxy.controllers; -import eu.openanalytics.containerproxy.model.runtime.Proxy; import eu.openanalytics.containerproxy.api.dto.ApiResponse; +import eu.openanalytics.containerproxy.model.runtime.Proxy; import eu.openanalytics.containerproxy.service.ProxyService; import eu.openanalytics.containerproxy.service.UserService; import eu.openanalytics.containerproxy.service.hearbeat.ActiveProxiesService; diff --git a/src/main/java/eu/openanalytics/shinyproxy/controllers/IndexController.java b/src/main/java/eu/openanalytics/shinyproxy/controllers/IndexController.java index 4ffcef4f..9df8f8a3 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/controllers/IndexController.java +++ b/src/main/java/eu/openanalytics/shinyproxy/controllers/IndexController.java @@ -40,72 +40,72 @@ @Controller public class IndexController extends BaseController { - @Inject - private ShinyProxySpecProvider shinyProxySpecProvider; + @Inject + private ShinyProxySpecProvider shinyProxySpecProvider; - @Inject - private Environment environment; + @Inject + private Environment environment; - private MyAppsMode myAppsMode; + private MyAppsMode myAppsMode; - @PostConstruct - public void init() { - myAppsMode = environment.getProperty("proxy.my-apps-mode", MyAppsMode.class, MyAppsMode.None); - } + @PostConstruct + public void init() { + myAppsMode = environment.getProperty("proxy.my-apps-mode", MyAppsMode.class, MyAppsMode.None); + } - @RequestMapping("/") + @RequestMapping("/") private Object index(ModelMap map, HttpServletRequest request) { - String landingPage = environment.getProperty("proxy.landing-page", "/"); - if (!landingPage.equals("/")) return new RedirectView(landingPage); - - prepareMap(map, request); - - ProxySpec[] apps = proxyService.getProxySpecs(null, false).toArray(new ProxySpec[0]); - map.put("apps", apps); - - Map appLogos = new HashMap<>(); - map.put("appLogos", appLogos); - - boolean displayAppLogos = false; - for (ProxySpec app: apps) { - if (app.getLogoURL() != null) { - displayAppLogos = true; - appLogos.put(app, resolveImageURI(app.getLogoURL())); - } - } - map.put("displayAppLogos", displayAppLogos); - - // template groups - HashMap> groupedApps = new HashMap<>(); - List ungroupedApps = new ArrayList<>(); - - for (ProxySpec app: apps) { - String groupId = app.getSpecExtension(ShinyProxySpecExtension.class).getTemplateGroup(); - if (groupId != null) { - groupedApps.putIfAbsent(groupId, new ArrayList<>()); - groupedApps.get(groupId).add(app); - } else { - ungroupedApps.add(app); - } - } - - List templateGroups = shinyProxySpecProvider.getTemplateGroups().stream().filter((g) -> groupedApps.containsKey(g.getId())).toList(); - map.put("templateGroups", templateGroups); - map.put("groupedApps", groupedApps); - map.put("ungroupedApps", ungroupedApps); - - // navbar - map.put("page", "index"); - - map.put("myAppsMode", myAppsMode.toString()); - - return "index"; + String landingPage = environment.getProperty("proxy.landing-page", "/"); + if (!landingPage.equals("/")) return new RedirectView(landingPage); + + prepareMap(map, request); + + ProxySpec[] apps = proxyService.getProxySpecs(null, false).toArray(new ProxySpec[0]); + map.put("apps", apps); + + Map appLogos = new HashMap<>(); + map.put("appLogos", appLogos); + + boolean displayAppLogos = false; + for (ProxySpec app : apps) { + if (app.getLogoURL() != null) { + displayAppLogos = true; + appLogos.put(app, resolveImageURI(app.getLogoURL())); + } + } + map.put("displayAppLogos", displayAppLogos); + + // template groups + HashMap> groupedApps = new HashMap<>(); + List ungroupedApps = new ArrayList<>(); + + for (ProxySpec app : apps) { + String groupId = app.getSpecExtension(ShinyProxySpecExtension.class).getTemplateGroup(); + if (groupId != null) { + groupedApps.putIfAbsent(groupId, new ArrayList<>()); + groupedApps.get(groupId).add(app); + } else { + ungroupedApps.add(app); + } + } + + List templateGroups = shinyProxySpecProvider.getTemplateGroups().stream().filter((g) -> groupedApps.containsKey(g.getId())).toList(); + map.put("templateGroups", templateGroups); + map.put("groupedApps", groupedApps); + map.put("ungroupedApps", ungroupedApps); + + // navbar + map.put("page", "index"); + + map.put("myAppsMode", myAppsMode.toString()); + + return "index"; } - public enum MyAppsMode { - Inline, - Modal, - None - } + public enum MyAppsMode { + Inline, + Modal, + None + } } diff --git a/src/main/java/eu/openanalytics/shinyproxy/controllers/IssueController.java b/src/main/java/eu/openanalytics/shinyproxy/controllers/IssueController.java index 8b15708a..f3686d6d 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/controllers/IssueController.java +++ b/src/main/java/eu/openanalytics/shinyproxy/controllers/IssueController.java @@ -20,15 +20,9 @@ */ package eu.openanalytics.shinyproxy.controllers; -import java.io.File; -import java.util.HashMap; - -import javax.inject.Inject; -import javax.mail.internet.MimeMessage; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - import eu.openanalytics.containerproxy.log.LogPaths; +import eu.openanalytics.containerproxy.model.runtime.Proxy; +import eu.openanalytics.containerproxy.service.LogService; import eu.openanalytics.shinyproxy.AppRequestInfo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; @@ -38,123 +32,134 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; -import eu.openanalytics.containerproxy.model.runtime.Proxy; -import eu.openanalytics.containerproxy.service.LogService; +import javax.inject.Inject; +import javax.mail.internet.MimeMessage; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.util.HashMap; @Controller public class IssueController extends BaseController { - @Inject - LogService logService; + @Inject + LogService logService; - @Autowired(required=false) + @Autowired(required = false) JavaMailSender mailSender; - - @RequestMapping(value="/issue", method=RequestMethod.POST) - public ResponseEntity> postIssue(HttpServletRequest request, HttpServletResponse response) { - IssueForm form = new IssueForm(); - form.setUserName(userService.getCurrentUserId()); - form.setCurrentLocation(request.getParameter("currentLocation")); - AppRequestInfo appRequestInfo = AppRequestInfo.fromURI(form.getCurrentLocation()); - if (appRequestInfo != null) { - form.setAppName(appRequestInfo.getAppName()); - } - form.setCustomMessage(request.getParameter("customMessage")); - - Proxy activeProxy = null; - for (Proxy proxy: proxyService.getProxies(null, false)) { - if (proxy.getUserId().equals(form.getUserName()) && proxy.getSpecId().equals(form.getAppName())) { - activeProxy = proxy; - break; - } - } - sendSupportMail(form, activeProxy); - - return ResponseEntity.ok(new HashMap<>() {{ - put("status", "success"); - }}); - } - - public void sendSupportMail(IssueForm form, Proxy proxy) { - String supportAddress = getSupportAddress(); - if (supportAddress == null) throw new RuntimeException("Cannot send mail: no support address configured"); - if (mailSender == null) throw new RuntimeException("Cannot send mail: no smtp settings configured"); - - try { - MimeMessage message = mailSender.createMimeMessage(); - MimeMessageHelper helper = new MimeMessageHelper(message, true); - - // Headers - helper.setFrom(environment.getProperty("proxy.support.mail-from-address", "issues@shinyproxy.io")); - helper.addTo(supportAddress); - helper.setSubject("ShinyProxy Error Report"); - - // Body - StringBuilder body = new StringBuilder(); - String lineSep = System.getProperty("line.separator"); - body.append(String.format("This is an error report generated by ShinyProxy%s", lineSep)); - body.append(String.format("User: %s%s", form.userName, lineSep)); - if (form.appName != null) body.append(String.format("App: %s%s", form.appName, lineSep)); - if (form.currentLocation != null) body.append(String.format("Location: %s%s", form.currentLocation, lineSep)); - if (form.customMessage != null) body.append(String.format("Message: %s%s", form.customMessage, lineSep)); - - // Attachments (only if container-logging is enabled) - if (proxy != null) { - LogPaths filePaths = logService.getLogs(proxy); - - if (filePaths != null) { - File stdout = filePaths.getStdout().toFile(); - if (stdout.exists()) { - helper.addAttachment(stdout.getName(), stdout); - // if stderr exists add it as well (stdout may exists without stderr) - File stderr = filePaths.getStderr().toFile(); - if (stderr.exists()) { - helper.addAttachment(stderr.getName(), stderr); - } - } else { - body.append(String.format("Log (stdout): %s%s", filePaths.getStdout().toString(), lineSep)); - body.append(String.format("Log (stderr): %s%s", filePaths.getStderr().toString(), lineSep)); - } - } - } - - helper.setText(body.toString()); - mailSender.send(message); - } catch (Exception e) { - throw new RuntimeException("Failed to send email", e); - } - } - - public static class IssueForm { - - private String userName; - private String appName; - private String currentLocation; - private String customMessage; - - public String getUserName() { - return userName; - } - public void setUserName(String userName) { - this.userName = userName; - } - public String getAppName() { - return appName; - } - public void setAppName(String appName) { - this.appName = appName; - } - public String getCurrentLocation() { - return currentLocation; - } - public void setCurrentLocation(String currentLocation) { - this.currentLocation = currentLocation; - } - public String getCustomMessage() { - return customMessage; - } - public void setCustomMessage(String customMessage) { - this.customMessage = customMessage; - } - } + + @RequestMapping(value = "/issue", method = RequestMethod.POST) + public ResponseEntity> postIssue(HttpServletRequest request, HttpServletResponse response) { + IssueForm form = new IssueForm(); + form.setUserName(userService.getCurrentUserId()); + form.setCurrentLocation(request.getParameter("currentLocation")); + AppRequestInfo appRequestInfo = AppRequestInfo.fromURI(form.getCurrentLocation()); + if (appRequestInfo != null) { + form.setAppName(appRequestInfo.getAppName()); + } + form.setCustomMessage(request.getParameter("customMessage")); + + Proxy activeProxy = null; + for (Proxy proxy : proxyService.getProxies(null, false)) { + if (proxy.getUserId().equals(form.getUserName()) && proxy.getSpecId().equals(form.getAppName())) { + activeProxy = proxy; + break; + } + } + sendSupportMail(form, activeProxy); + + return ResponseEntity.ok(new HashMap<>() {{ + put("status", "success"); + }}); + } + + public void sendSupportMail(IssueForm form, Proxy proxy) { + String supportAddress = getSupportAddress(); + if (supportAddress == null) throw new RuntimeException("Cannot send mail: no support address configured"); + if (mailSender == null) throw new RuntimeException("Cannot send mail: no smtp settings configured"); + + try { + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true); + + // Headers + helper.setFrom(environment.getProperty("proxy.support.mail-from-address", "issues@shinyproxy.io")); + helper.addTo(supportAddress); + helper.setSubject("ShinyProxy Error Report"); + + // Body + StringBuilder body = new StringBuilder(); + String lineSep = System.getProperty("line.separator"); + body.append(String.format("This is an error report generated by ShinyProxy%s", lineSep)); + body.append(String.format("User: %s%s", form.userName, lineSep)); + if (form.appName != null) body.append(String.format("App: %s%s", form.appName, lineSep)); + if (form.currentLocation != null) body.append(String.format("Location: %s%s", form.currentLocation, lineSep)); + if (form.customMessage != null) body.append(String.format("Message: %s%s", form.customMessage, lineSep)); + + // Attachments (only if container-logging is enabled) + if (proxy != null) { + LogPaths filePaths = logService.getLogs(proxy); + + if (filePaths != null) { + File stdout = filePaths.getStdout().toFile(); + if (stdout.exists()) { + helper.addAttachment(stdout.getName(), stdout); + // if stderr exists add it as well (stdout may exists without stderr) + File stderr = filePaths.getStderr().toFile(); + if (stderr.exists()) { + helper.addAttachment(stderr.getName(), stderr); + } + } else { + body.append(String.format("Log (stdout): %s%s", filePaths.getStdout().toString(), lineSep)); + body.append(String.format("Log (stderr): %s%s", filePaths.getStderr().toString(), lineSep)); + } + } + } + + helper.setText(body.toString()); + mailSender.send(message); + } catch (Exception e) { + throw new RuntimeException("Failed to send email", e); + } + } + + public static class IssueForm { + + private String userName; + private String appName; + private String currentLocation; + private String customMessage; + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getAppName() { + return appName; + } + + public void setAppName(String appName) { + this.appName = appName; + } + + public String getCurrentLocation() { + return currentLocation; + } + + public void setCurrentLocation(String currentLocation) { + this.currentLocation = currentLocation; + } + + public String getCustomMessage() { + return customMessage; + } + + public void setCustomMessage(String customMessage) { + this.customMessage = customMessage; + } + } } diff --git a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/AppInstanceKey.java b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/AppInstanceKey.java index b4c2a2f9..8060f868 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/AppInstanceKey.java +++ b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/AppInstanceKey.java @@ -22,7 +22,9 @@ import eu.openanalytics.containerproxy.model.runtime.runtimevalues.RuntimeValueKey; -public class AppInstanceKey extends RuntimeValueKey { +public class AppInstanceKey extends RuntimeValueKey { + + public static final AppInstanceKey inst = new AppInstanceKey(); public AppInstanceKey() { super("openanalytics.eu/sp-app-instance", @@ -36,8 +38,6 @@ public AppInstanceKey() { String.class); } - public static final AppInstanceKey inst = new AppInstanceKey(); - @Override public String deserializeFromString(String value) { return value; diff --git a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/PublicPathKey.java b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/PublicPathKey.java index b2e29c75..72548fa0 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/PublicPathKey.java +++ b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/PublicPathKey.java @@ -22,7 +22,9 @@ import eu.openanalytics.containerproxy.model.runtime.runtimevalues.RuntimeValueKey; -public class PublicPathKey extends RuntimeValueKey { +public class PublicPathKey extends RuntimeValueKey { + + public static final PublicPathKey inst = new PublicPathKey(); public PublicPathKey() { super("openanalytics.eu/sp-public-path", @@ -36,8 +38,6 @@ public PublicPathKey() { String.class); } - public static final PublicPathKey inst = new PublicPathKey(); - @Override public String deserializeFromString(String value) { return value; diff --git a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/ShinyForceFullReloadKey.java b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/ShinyForceFullReloadKey.java index 87f238c8..308ca9c0 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/ShinyForceFullReloadKey.java +++ b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/ShinyForceFullReloadKey.java @@ -25,6 +25,8 @@ public class ShinyForceFullReloadKey extends RuntimeValueKey { + public static final ShinyForceFullReloadKey inst = new ShinyForceFullReloadKey(); + public ShinyForceFullReloadKey() { super("openanalytics.eu/sp-shiny-force-full-reload", "SHINYPROXY_FORCE_FULL_RELOAD", @@ -37,8 +39,6 @@ public ShinyForceFullReloadKey() { Boolean.class); } - public static final ShinyForceFullReloadKey inst = new ShinyForceFullReloadKey(); - @Override public Boolean deserializeFromString(String value) { return Boolean.valueOf(value); diff --git a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/TrackAppUrl.java b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/TrackAppUrl.java index 5bd259c7..becfa886 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/TrackAppUrl.java +++ b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/TrackAppUrl.java @@ -25,6 +25,8 @@ public class TrackAppUrl extends RuntimeValueKey { + public static final TrackAppUrl inst = new TrackAppUrl(); + public TrackAppUrl() { super("openanalytics.eu/sp-track-app-url", "SHINYPROXY_TRACK_APP_URL", @@ -37,8 +39,6 @@ public TrackAppUrl() { Boolean.class); } - public static final TrackAppUrl inst = new TrackAppUrl(); - @Override public Boolean deserializeFromString(String value) { return Boolean.valueOf(value); diff --git a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/UserTimeZoneKey.java b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/UserTimeZoneKey.java index 6996c053..8e4f0af7 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/UserTimeZoneKey.java +++ b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/UserTimeZoneKey.java @@ -22,7 +22,9 @@ import eu.openanalytics.containerproxy.model.runtime.runtimevalues.RuntimeValueKey; -public class UserTimeZoneKey extends RuntimeValueKey { +public class UserTimeZoneKey extends RuntimeValueKey { + + public static final UserTimeZoneKey inst = new UserTimeZoneKey(); public UserTimeZoneKey() { super("openanalytics.eu/sp-user-timezone", @@ -36,8 +38,6 @@ public UserTimeZoneKey() { String.class); } - public static final UserTimeZoneKey inst = new UserTimeZoneKey(); - @Override public String deserializeFromString(String value) { return value; diff --git a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/WebSocketReconnectionModeKey.java b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/WebSocketReconnectionModeKey.java index 058c55b7..67629feb 100644 --- a/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/WebSocketReconnectionModeKey.java +++ b/src/main/java/eu/openanalytics/shinyproxy/runtimevalues/WebSocketReconnectionModeKey.java @@ -25,6 +25,8 @@ public class WebSocketReconnectionModeKey extends RuntimeValueKey { + public static final WebSocketReconnectionModeKey inst = new WebSocketReconnectionModeKey(); + public WebSocketReconnectionModeKey() { super("openanalytics.eu/sp-websocket-reconnection-mode", "SHINYPROXY_WEBSOCKET_RECONNECTION_MODE", @@ -37,8 +39,6 @@ public WebSocketReconnectionModeKey() { WebsocketReconnectionMode.class); } - public static final WebSocketReconnectionModeKey inst = new WebSocketReconnectionModeKey(); - @Override public WebsocketReconnectionMode deserializeFromString(String value) { return WebsocketReconnectionMode.valueOf(value); diff --git a/src/main/resources/application-demo.yml b/src/main/resources/application-demo.yml index 7bdabec0..7b4bb7ab 100644 --- a/src/main/resources/application-demo.yml +++ b/src/main/resources/application-demo.yml @@ -5,40 +5,31 @@ proxy: heartbeat-rate: 10000 heartbeat-timeout: 60000 port: 8080 - authentication: ldap + authentication: simple admin-groups: scientists # Example: 'simple' authentication configuration users: - - name: jack - password: password - groups: scientists - - name: jeff - password: password - groups: mathematicians - # Example: 'ldap' authentication configuration - ldap: - url: ldap://ldap.forumsys.com:389/dc=example,dc=com - user-dn-pattern: uid={0} - group-search-base: - group-search-filter: (uniqueMember={0}) - manager-dn: cn=read-only-admin,dc=example,dc=com - manager-password: password - # Docker configuration + - name: jack + password: password + groups: scientists + - name: jeff + password: password + groups: mathematicians docker: url: http://localhost:2375 port-range-start: 20000 specs: - - id: 01_hello - display-name: Hello Application - description: Application which demonstrates the basics of a Shiny app - container-cmd: ["R", "-e", "shinyproxy::run_01_hello()"] - container-image: openanalytics/shinyproxy-demo - access-groups: [scientists, mathematicians] - - id: 06_tabsets - container-cmd: ["R", "-e", "shinyproxy::run_06_tabsets()"] - container-image: openanalytics/shinyproxy-demo - access-groups: scientists + - id: 01_hello + display-name: Hello Application + description: Application which demonstrates the basics of a Shiny app + container-cmd: [ "R", "-e", "shinyproxy::run_01_hello()" ] + container-image: openanalytics/shinyproxy-demo + access-groups: [ scientists, mathematicians ] + - id: 06_tabsets + container-cmd: [ "R", "-e", "shinyproxy::run_06_tabsets()" ] + container-image: openanalytics/shinyproxy-demo + access-groups: scientists logging: file: - shinyproxy.log + name: shinyproxy.log diff --git a/src/main/resources/static/css/default.css b/src/main/resources/static/css/default.css index 906453e9..c6c9f42d 100644 --- a/src/main/resources/static/css/default.css +++ b/src/main/resources/static/css/default.css @@ -1,263 +1,264 @@ -/** - * ShinyProxy - * - * Copyright (C) 2016-2023 Open Analytics - * - * =========================================================================== - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Apache License as published by - * The Apache Software Foundation, either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * Apache License for more details. - * - * You should have received a copy of the Apache License - * along with this program. If not, see - */ -#applist { - margin-top: 16px; -} - -#applist h2 { - border-bottom: 1px solid rgba(0,0,0,.2); - margin-bottom: 30px; - margin-top: 0px; - padding-bottom: 10px; -} - -#shinyframe { - border: none; - display: block; - bottom: 0; - position: absolute; -} - -#admin { - margin-left: 10px; - margin-right: 10px; -} - -#admin th, td { - padding: 5px; -} - -#admin #allApps { - width: 100%; -} - -#error { - padding-left: 15px; -} - -.loading { - display: none; - width: 100%; - z-index: 999; -} -.loading-img { - background: url(data:image/gif;base64,R0lGODlhQABAAPcAAIDF3b/i7pXP4+r1+arY6NTs9FWy0mu710SqzWi617zg7f3+/kiszvj8/VCw0Ov2+o/M4ez2+vL5/Pb7/eLy91y109ft9Pz+/ozK4PT6/Mrn8UKpzXTA2s7p8oLG3t7w9lay0ozL4K7a6f7+/6LV5vv9/qTV51+21JbP40+v0Eytz0OpzeDx99Pr8+b0+EWqzUGozHG+2afX53nC21q007vg7ebz+Pn8/XrC2+/3+3O/2qPV5pPO4sjm8HC+2dbs9Nru9e73+u/4+57T5WK31WG31ff7/fL5+8vn8YvK4NTr81mz07rg7Wy82HjC21Kw0bHc6lGw0f7///P6/On1+bnf7PX6/F621KHU5pLO4sXk7+f0+Z3S5cbl8Nzv9uPy+OTz+Nnu9bLc6vr9/me61sDi7sXl8Njt9bDb6lm007jf7MLj79Xs9H/F3a3a6fH5++Xz+JnR5NDq8nK/2ZjQ4/D4++33+pjQ5F211KfX6Oj0+YbI35vS5N/w9pfQ42+92c7o8vX7/LPd69Hq81y11Mnm8a/a6o3L4d3v9oPH3o7L4Vez0qjX6KvZ6bTd68/p8nG/2bbe7Fiz0r3h7bPc6+Py94nJ38Pk78zo8YnJ4IrK4K3Z6Wu82Mfl8MHj7nXA2lu007be60CozP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/wtYTVAgRGF0YVhNUDw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDowMDA3NTlEOTk4REYxMUUzQTY5NkRERDBGNjQ2NTNFMiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDowMDA3NTlEQTk4REYxMUUzQTY5NkRERDBGNjQ2NTNFMiI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjA4MTUwMjlFOThERDExRTNBNjk2REREMEY2NDY1M0UyIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjAwMDc1OUQ4OThERjExRTNBNjk2REREMEY2NDY1M0UyIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEBAMAAAAsAAAAAEAAQAAACMkARwkcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+okqSABAgQJFBDs+TPoUJ9AhZKEIKqpUwgCmTptCnWU1KlVQyqYOlXBVq5NvYINKzLBWFEJzI5NezaBSARnf8aFOxbB27l465ZtqxYs27Uiv4IVO5bw4JFXn0YFWzUx1ZJEkx4tqlRgZKM7M2vezLmz58+gQ4seTbq06dOoU6tezbq169c2AwIAIfkEBAMAAAAsEQAbAB8ACgAACJIAR42SgAKEKBUeKAgcWPBgwoUsklwxcCDPhIURaIjauJFBh1EZOXb8iGSJgZMndUwRqEMkxxQZWroUlQIOHpQ4FY1iMZPjkJ4bAeAcGoQJUFExjk4cihKQUaBJgS5lagAQT6A/gQqlaiDIKJkuYYIVWfMmU50gNYr0mNYl25JDVS4kaBChQoF0Hd7dKZGiRYEBAQAh+QQEAwAAACwRABsAIAAKAAAIkwBHCbQwgwGMCiQaCBxF0CBChQK98PDByQOTEgtHKVghqmPHBBM0cvQoCuQoLU0OqFQJAeMoFwhIesQAU2ZHmilXrnQj8I7Njgh4/BSFgIROnZAwchhakumMozrBjFo6NIFTqCul+vwZdGhRrAeSvoxpkyZZmTix8hS4UabJtiRNojzaMmPDgwkX3n24UCJFiy4DAgAh+QQEAwAAACwSABsAIAAKAAAIlABHjVoggsiGFz40CBxY8GDChaOsqDkEwJKbIAsX4BDFsaOMgRs7cvw4SggEAChR7qEg0IRIkS1cvuTYYhSXlDhDjBnxZCbHNj19AoCDs6icCD45SkoqChSSojgjIU26NKlTqCkj8UwKNOlQrCjljJI5M2bSmjeh6gQ586PGtgJNFl2ZsSFChQIJGrwLUSJFixhHBQQAIfkEBAMAAAAsEgAbACIACgAACKEARwl8EMKBKAchHggcRdAgQoULR+WowkcAFi0NIn4wKKrjwQ+jNnrs6ACkQDBxBKhUucOKwAsnRno8cSOmTFEnLoy6wWWlTygCe9z0iGVoxx6jlPhcGmSUCaOiOEDdMSrAUp9hRu2AqmNq1asrswo1WtQoUqVgBTSFOZSmTZk5d/a8ClSgyJElQ3L0mPdkSp8tIzY8mHDh4IcRJVK0iFFgQAAh+QQEAwAAACwTABsAIwAKAAAIoQBHCdQQ48WGIiIWCBxF0CBChQsFTkAChYAYTBMiyhDFseOMC6M2duT4MWKdigRSWnwjsMXIkTtcvuS4Y+EINSpzVhkxCsBMjlF8/ozCc5SLnEi3jKrwk+OSpqIGCPyBNKeFpVCfNpU6imrVlFeFzgzalKjAo18JKJU5M2bTmgJvft0pUOTIknY9glx4EqkYlgsbHkwYuOBgiBEnVryYcVRAACH5BAQDAAAALBQAGwAkAAoAAAigAEeNakCiAgwGTn4IHFjwYMKFEEdJ+aKhjCc5QRZOSCCqY8cNNUZt9PgxZMQRgwKoXFlJIAaSHhHYeAlTlMyILFbqDPAmA4KaHbP8BEoHohQtO1f+UAK0I5mmouZANJJ0ZSGmTZ82lbqQatUAhXw2Fdq06MKjXxXShClzLcmbEHNWfSOSI0mQdWHiPZlyZ0uBBA0iVAi44eCIZylaxCgwIAAh+QQEAwAAACwUABsAJwAKAAAIpABHCaTgQYUoEAIkCBxF0CBChQsjCrzxxUKBMFsuROzAQJRHj2kijOL4EaRIiQKtWCzAsgCQEgIzpCj5UYdMmh51oBy14EzLnywEosH5cQhRj0ElBvnJtMEoDEdFxYjKBKUNpj+FPI069WhViVexstQ69KjRo0kjLhVbwOlNnDZnwt3ZU2xakiVDjuyY9yRKlUxfRmx4MOFCwg93LqRoEaPGUQEBACH5BAQDAAAALAMAGwA6AAoAAAjOAEeNunAkwoAIU0YIXMhwoYIECBAkUMDwYcSJDTMKbJDjwYMcDRaOeTCgZEk7CzQuhCCqpUsIAlm6bAlTpUAJJk1OGSXFYM6SQmwqmDmzxlCiLWvYbPDTZAMjTU2WUJkAacsEVa0msJkj6oAcOL0aUYnAqqgVZa2ukKLS64AHYaOO1ZgWKVqza9t6fQDV61SNWZFiNbtVZdeoOXpGDaryKFKjZpWqZBo15MifKG2OkjmzJueXmkfF1bmQoEGECkNblEjRIUTWoTd2/BhyVEAAIfkEBAMAAAAsAwAbADoACgAACK8ARwkUmAMRGzYfjgxcOFACChCiVHig0PBhxIkMM2rM+KWAx49UNkagIapkSQYdRo00eTLlxpcL7XycWUCCRg4sTabIgDOnqJ0wgwKh+ZFFRhY+TQ5JWhJN0JcLiH48k7EGU1ExrmJ4ujGq1AJUGVplmpXpVq4ah0o1yhAp06VMnaLNKFOqzYw9c+7MyxLoXI5EQ2pcyRKlSpKFXf7NWPBgQpgOIUqkKDDyRcqLRwUEACH5BAQDAAAALAUAGwAnAAoAAAipAEcJFBjox6UAXbyUGMhwlAUnDGBUINFg4MOIEys2FLImgEePPTQyrLFClEmTCSaMInkSpcqBF8x8nPmooQ0ELU9iuJnTJAaGNmYKzcCQTk+TCLIcFYWAqEAgQmduYThnqSgyVpUMhBrV49SBVZdiXapVYNCuAZwKNHo06dKmMGVGrQkUZ8+ddnP+ZMhRaMiGK0u2TBk4J+GGBQ8mXAjYIUSJFC0+zigwIAAh+QQEAwAAACwIABsAJAAKAAAInQBHCRxVQkkoAobM1Bk4cIGIIhtexNDQ8GHEiQxH3ahCoGPHTTYYLsAhqqRJGaNGmjzJsJPHlyIaDDSxcmULmjVLthA44aVPCwJHRMlZEsBQogAEuvD5sovAB0RLLokqqoJSph6djoIadWpUq6N6YiUAdJTQqEbRDnTJNObMqDfhDtzoE6RIkjVRqsybseDBhAszOoQokaLAwRcNBwQAIfkEBAMAAAAsCgAbACMACgAACKUARwkcVQKJCQFxHFEZKPBBCAeiHIR4MNAhRIkUGY5pJKBjRxRABn6AKKpkxA+jRpos6QDlwAAeY/o5MurCiZUmT9y4iVPUiQsCb6CIGbPLqB49TWJJWrKHQBdEY0IZtYOpKA5WTTyN6nFqVaZYmWodJZSrAKNImS5l6lQgzKgza/LEqXPuyp8DNxIFKZKkyZYp/bJ0ObDgwYQLGVqMOLHiQ8YZAwIAIfkEBAMAAAAsDAAbACIACgAACJcARwkcdUSMJgBJBEkYOEqDjxcbiIhYMNAhRIkUGerJBKBjxyQPBMoQRbIkDoojS5I8OXABD48wBYxooVKlCZo1SZoYGAamTy8AcpJ80kaoqCcjBJbxCXMNDaOiFkGNoJSpR6dQpRqlOqqnVQBAjRIVm3SUS6sycea8aXTnwI0+QYrMyTKlSpYMCx5MuLDiw4gT/V4MPCogACH5BAQDAAAALA4AGwAgAAoAAAiVAEcJXDApUZM/WYAIHNWARAUYDGZYWNjwYcSJC0vwOMCxY4BRExKIGjlyhQKQIkmKMrmQUseXTShgUEkSgYuZNEXZHHWBw8uXXBDkHMlD6NA7o6j8fPlp6MiUQzkkXdqxqVOoOaX2pHogqNOiTpGOcrk0Jk6aNs+q3DlK49KPIWmyjKuS5UKCBhEqFFgRokSKDv1iDAgAIfkEBAMAAAAsDwAbACAACgAACJQARwk0wuiAATxJ+ggcRcGDClEgUEhY2PBhxIkLM3AwwJHjEg2jOjAQRZIkjQghR5YUdXIhhI4wCYFJsbIkhww0a4riIFAICJgwAegsOWQoSRYhgcK8YlRUjKZMkirlyNToU6NRfU41INRoUaNIR71UKjNnzZtmV/IUqBHox5Q1W4qMi3IhQYMIFQqsCFEiRYd9MQYEADs=) center no-repeat #fff; - height: 50px; -} - -.loading-txt { - text-align: center; - font-size: 24px; - margin-top: 30px; -} - -#reconnecting { - height: 175px; -} - -#reloadFailed { - background: none; -} - -.retryingDetails { - width: 120px; - display: block; - text-align: left; - margin: 0 auto; - font-size: 18px; -} - -.refreshButton { - font-size: 18px; -} - -#iframeinsert { - display: none; -} - -#newInstanceForm { - margin-top: 20px; - margin-bottom: 20px; -} - -#instanceNameField { - width: 450px; - margin-right: 10px; -} - -#newInstanceForm button { - width: 100px; -} - -.myApps .btn-group { - float:right; - margin-left: 15px; - display: block; - height: 20px -} - -.app-instance-title { - color: #337ab7; - text-decoration: none; - float: left; - display: block; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - width: calc(100% - 150px); -} - -.active-app-instance-title { - float: left; - display: block; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - width: calc(100% - 205px); -} - -.app-instance:hover .app-instance-title { - color: #23527c; - text-decoration: underline; -} - -.app-list-title { - color: #337ab7; - text-decoration: none; -} - -.list-group-item:hover .app-list-title { - color: #23527c; - text-decoration: underline; -} - -@media (min-width: 1200px) { - .myApps-inline { - max-width: 760px; - float: right; - } -} - -.admin-proxy-id { - width: 23em; -} - -.admin-monospace { - font-family: monospace; -} - -.admin-proxy-status { - width: 5em; -} - -.admin-proxy-uptime { - width: 5em; -} - -.admin-proxy-heartbeat { - width: 8em; -} - -#stopping-all-apps-btn { - display: none; -} - -#parameterForm { - display: none; - width: 100%; -} - -#parameterForm .form-horizontal { - margin-top: 25px; -} - -#parameterForm .help-block { - margin-bottom: 0; - padding-left: 10px; -} - -#selectAllWarning { - display: none; -} - -#selectAllWarning div { - margin-bottom: 0; -} - -#switchInstancesModal, #myAppsModal, #appDetailsModal { - display: none; -} - -#appDetailsModal table { - width: 100%; - table-layout: fixed; -} - -#appDetailsModal td { - width: 50%; - word-wrap: anywhere; -} - -#appDetailsModal tr:first-child td { - border-top: 0; -} - -#appDetailsModal .help-block { - margin-bottom: 0; -} - - -@media (max-width: 992px) { - .myApps-title, .myApps-inline #myApps, .myApps-footer { - margin-left: 0 !important; - margin-right: 0 !important; - } -} - -.myApps-title { - border-bottom: 1px solid rgba(0,0,0,.2); - margin-right: 30px; - margin-top: 30px; - margin-left: 30px; - height: 30px; -} - -.myApps-inline #myApps { - margin-right: 30px; - margin-left: 30px; - margin-top: 30px; -} - -.myApps-footer { - margin-top: 30px; - margin-left: 30px; - height: 60px; -} - -#stop-all-apps-btn { - display: none; -} - -.status-label { - display: inline-block; - width: 70px; - line-height: inherit; -} +/** + * ShinyProxy + * + * Copyright (C) 2016-2023 Open Analytics + * + * =========================================================================== + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Apache License as published by + * The Apache Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Apache License for more details. + * + * You should have received a copy of the Apache License + * along with this program. If not, see + */ +#applist { + margin-top: 16px; +} + +#applist h2 { + border-bottom: 1px solid rgba(0, 0, 0, .2); + margin-bottom: 30px; + margin-top: 0px; + padding-bottom: 10px; +} + +#shinyframe { + border: none; + display: block; + bottom: 0; + position: absolute; +} + +#admin { + margin-left: 10px; + margin-right: 10px; +} + +#admin th, td { + padding: 5px; +} + +#admin #allApps { + width: 100%; +} + +#error { + padding-left: 15px; +} + +.loading { + display: none; + width: 100%; + z-index: 999; +} + +.loading-img { + background: url(data:image/gif;base64,R0lGODlhQABAAPcAAIDF3b/i7pXP4+r1+arY6NTs9FWy0mu710SqzWi617zg7f3+/kiszvj8/VCw0Ov2+o/M4ez2+vL5/Pb7/eLy91y109ft9Pz+/ozK4PT6/Mrn8UKpzXTA2s7p8oLG3t7w9lay0ozL4K7a6f7+/6LV5vv9/qTV51+21JbP40+v0Eytz0OpzeDx99Pr8+b0+EWqzUGozHG+2afX53nC21q007vg7ebz+Pn8/XrC2+/3+3O/2qPV5pPO4sjm8HC+2dbs9Nru9e73+u/4+57T5WK31WG31ff7/fL5+8vn8YvK4NTr81mz07rg7Wy82HjC21Kw0bHc6lGw0f7///P6/On1+bnf7PX6/F621KHU5pLO4sXk7+f0+Z3S5cbl8Nzv9uPy+OTz+Nnu9bLc6vr9/me61sDi7sXl8Njt9bDb6lm007jf7MLj79Xs9H/F3a3a6fH5++Xz+JnR5NDq8nK/2ZjQ4/D4++33+pjQ5F211KfX6Oj0+YbI35vS5N/w9pfQ42+92c7o8vX7/LPd69Hq81y11Mnm8a/a6o3L4d3v9oPH3o7L4Vez0qjX6KvZ6bTd68/p8nG/2bbe7Fiz0r3h7bPc6+Py94nJ38Pk78zo8YnJ4IrK4K3Z6Wu82Mfl8MHj7nXA2lu007be60CozP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/wtYTVAgRGF0YVhNUDw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDowMDA3NTlEOTk4REYxMUUzQTY5NkRERDBGNjQ2NTNFMiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDowMDA3NTlEQTk4REYxMUUzQTY5NkRERDBGNjQ2NTNFMiI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjA4MTUwMjlFOThERDExRTNBNjk2REREMEY2NDY1M0UyIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjAwMDc1OUQ4OThERjExRTNBNjk2REREMEY2NDY1M0UyIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEBAMAAAAsAAAAAEAAQAAACMkARwkcSLCgwYMIEypcyLChw4cQI0qcSLGixYsYM2rcyLGjx48gQ4ocSbKkyZMoU6pcybKly5cwY8qcSbOmzZs4c+okqSABAgQJFBDs+TPoUJ9AhZKEIKqpUwgCmTptCnWU1KlVQyqYOlXBVq5NvYINKzLBWFEJzI5NezaBSARnf8aFOxbB27l465ZtqxYs27Uiv4IVO5bw4JFXn0YFWzUx1ZJEkx4tqlRgZKM7M2vezLmz58+gQ4seTbq06dOoU6tezbq169c2AwIAIfkEBAMAAAAsEQAbAB8ACgAACJIAR42SgAKEKBUeKAgcWPBgwoUsklwxcCDPhIURaIjauJFBh1EZOXb8iGSJgZMndUwRqEMkxxQZWroUlQIOHpQ4FY1iMZPjkJ4bAeAcGoQJUFExjk4cihKQUaBJgS5lagAQT6A/gQqlaiDIKJkuYYIVWfMmU50gNYr0mNYl25JDVS4kaBChQoF0Hd7dKZGiRYEBAQAh+QQEAwAAACwRABsAIAAKAAAIkwBHCbQwgwGMCiQaCBxF0CBChQK98PDByQOTEgtHKVghqmPHBBM0cvQoCuQoLU0OqFQJAeMoFwhIesQAU2ZHmilXrnQj8I7Njgh4/BSFgIROnZAwchhakumMozrBjFo6NIFTqCul+vwZdGhRrAeSvoxpkyZZmTix8hS4UabJtiRNojzaMmPDgwkX3n24UCJFiy4DAgAh+QQEAwAAACwSABsAIAAKAAAIlABHjVoggsiGFz40CBxY8GDChaOsqDkEwJKbIAsX4BDFsaOMgRs7cvw4SggEAChR7qEg0IRIkS1cvuTYYhSXlDhDjBnxZCbHNj19AoCDs6icCD45SkoqChSSojgjIU26NKlTqCkj8UwKNOlQrCjljJI5M2bSmjeh6gQ586PGtgJNFl2ZsSFChQIJGrwLUSJFixhHBQQAIfkEBAMAAAAsEgAbACIACgAACKEARwl8EMKBKAchHggcRdAgQoULR+WowkcAFi0NIn4wKKrjwQ+jNnrs6ACkQDBxBKhUucOKwAsnRno8cSOmTFEnLoy6wWWlTygCe9z0iGVoxx6jlPhcGmSUCaOiOEDdMSrAUp9hRu2AqmNq1asrswo1WtQoUqVgBTSFOZSmTZk5d/a8ClSgyJElQ3L0mPdkSp8tIzY8mHDh4IcRJVK0iFFgQAAh+QQEAwAAACwTABsAIwAKAAAIoQBHCdQQ48WGIiIWCBxF0CBChQsFTkAChYAYTBMiyhDFseOMC6M2duT4MWKdigRSWnwjsMXIkTtcvuS4Y+EINSpzVhkxCsBMjlF8/ozCc5SLnEi3jKrwk+OSpqIGCPyBNKeFpVCfNpU6imrVlFeFzgzalKjAo18JKJU5M2bTmgJvft0pUOTIknY9glx4EqkYlgsbHkwYuOBgiBEnVryYcVRAACH5BAQDAAAALBQAGwAkAAoAAAigAEeNakCiAgwGTn4IHFjwYMKFEEdJ+aKhjCc5QRZOSCCqY8cNNUZt9PgxZMQRgwKoXFlJIAaSHhHYeAlTlMyILFbqDPAmA4KaHbP8BEoHohQtO1f+UAK0I5mmouZANJJ0ZSGmTZ82lbqQatUAhXw2Fdq06MKjXxXShClzLcmbEHNWfSOSI0mQdWHiPZlyZ0uBBA0iVAi44eCIZylaxCgwIAAh+QQEAwAAACwUABsAJwAKAAAIpABHCaTgQYUoEAIkCBxF0CBChQsjCrzxxUKBMFsuROzAQJRHj2kijOL4EaRIiQKtWCzAsgCQEgIzpCj5UYdMmh51oBy14EzLnywEosH5cQhRj0ElBvnJtMEoDEdFxYjKBKUNpj+FPI069WhViVexstQ69KjRo0kjLhVbwOlNnDZnwt3ZU2xakiVDjuyY9yRKlUxfRmx4MOFCwg93LqRoEaPGUQEBACH5BAQDAAAALAMAGwA6AAoAAAjOAEeNunAkwoAIU0YIXMhwoYIECBAkUMDwYcSJDTMKbJDjwYMcDRaOeTCgZEk7CzQuhCCqpUsIAlm6bAlTpUAJJk1OGSXFYM6SQmwqmDmzxlCiLWvYbPDTZAMjTU2WUJkAacsEVa0msJkj6oAcOL0aUYnAqqgVZa2ukKLS64AHYaOO1ZgWKVqza9t6fQDV61SNWZFiNbtVZdeoOXpGDaryKFKjZpWqZBo15MifKG2OkjmzJueXmkfF1bmQoEGECkNblEjRIUTWoTd2/BhyVEAAIfkEBAMAAAAsAwAbADoACgAACK8ARwkUmAMRGzYfjgxcOFACChCiVHig0PBhxIkMM2rM+KWAx49UNkagIapkSQYdRo00eTLlxpcL7XycWUCCRg4sTabIgDOnqJ0wgwKh+ZFFRhY+TQ5JWhJN0JcLiH48k7EGU1ExrmJ4ujGq1AJUGVplmpXpVq4ah0o1yhAp06VMnaLNKFOqzYw9c+7MyxLoXI5EQ2pcyRKlSpKFXf7NWPBgQpgOIUqkKDDyRcqLRwUEACH5BAQDAAAALAUAGwAnAAoAAAipAEcJFBjox6UAXbyUGMhwlAUnDGBUINFg4MOIEys2FLImgEePPTQyrLFClEmTCSaMInkSpcqBF8x8nPmooQ0ELU9iuJnTJAaGNmYKzcCQTk+TCLIcFYWAqEAgQmduYThnqSgyVpUMhBrV49SBVZdiXapVYNCuAZwKNHo06dKmMGVGrQkUZ8+ddnP+ZMhRaMiGK0u2TBk4J+GGBQ8mXAjYIUSJFC0+zigwIAAh+QQEAwAAACwIABsAJAAKAAAInQBHCRxVQkkoAobM1Bk4cIGIIhtexNDQ8GHEiQxH3ahCoGPHTTYYLsAhqqRJGaNGmjzJsJPHlyIaDDSxcmULmjVLthA44aVPCwJHRMlZEsBQogAEuvD5sovAB0RLLokqqoJSph6djoIadWpUq6N6YiUAdJTQqEbRDnTJNObMqDfhDtzoE6RIkjVRqsybseDBhAszOoQokaLAwRcNBwQAIfkEBAMAAAAsCgAbACMACgAACKUARwkcVQKJCQFxHFEZKPBBCAeiHIR4MNAhRIkUGY5pJKBjRxRABn6AKKpkxA+jRpos6QDlwAAeY/o5MurCiZUmT9y4iVPUiQsCb6CIGbPLqB49TWJJWrKHQBdEY0IZtYOpKA5WTTyN6nFqVaZYmWodJZSrAKNImS5l6lQgzKgza/LEqXPuyp8DNxIFKZKkyZYp/bJ0ObDgwYQLGVqMOLHiQ8YZAwIAIfkEBAMAAAAsDAAbACIACgAACJcARwkcdUSMJgBJBEkYOEqDjxcbiIhYMNAhRIkUGerJBKBjxyQPBMoQRbIkDoojS5I8OXABD48wBYxooVKlCZo1SZoYGAamTy8AcpJ80kaoqCcjBJbxCXMNDaOiFkGNoJSpR6dQpRqlOqqnVQBAjRIVm3SUS6sycea8aXTnwI0+QYrMyTKlSpYMCx5MuLDiw4gT/V4MPCogACH5BAQDAAAALA4AGwAgAAoAAAiVAEcJXDApUZM/WYAIHNWARAUYDGZYWNjwYcSJC0vwOMCxY4BRExKIGjlyhQKQIkmKMrmQUseXTShgUEkSgYuZNEXZHHWBw8uXXBDkHMlD6NA7o6j8fPlp6MiUQzkkXdqxqVOoOaX2pHogqNOiTpGOcrk0Jk6aNs+q3DlK49KPIWmyjKuS5UKCBhEqFFgRokSKDv1iDAgAIfkEBAMAAAAsDwAbACAACgAACJQARwk0wuiAATxJ+ggcRcGDClEgUEhY2PBhxIkLM3AwwJHjEg2jOjAQRZIkjQghR5YUdXIhhI4wCYFJsbIkhww0a4riIFAICJgwAegsOWQoSRYhgcK8YlRUjKZMkirlyNToU6NRfU41INRoUaNIR71UKjNnzZtmV/IUqBHox5Q1W4qMi3IhQYMIFQqsCFEiRYd9MQYEADs=) center no-repeat #fff; + height: 50px; +} + +.loading-txt { + text-align: center; + font-size: 24px; + margin-top: 30px; +} + +#reconnecting { + height: 175px; +} + +#reloadFailed { + background: none; +} + +.retryingDetails { + width: 120px; + display: block; + text-align: left; + margin: 0 auto; + font-size: 18px; +} + +.refreshButton { + font-size: 18px; +} + +#iframeinsert { + display: none; +} + +#newInstanceForm { + margin-top: 20px; + margin-bottom: 20px; +} + +#instanceNameField { + width: 450px; + margin-right: 10px; +} + +#newInstanceForm button { + width: 100px; +} + +.myApps .btn-group { + float: right; + margin-left: 15px; + display: block; + height: 20px +} + +.app-instance-title { + color: #337ab7; + text-decoration: none; + float: left; + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: calc(100% - 150px); +} + +.active-app-instance-title { + float: left; + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: calc(100% - 205px); +} + +.app-instance:hover .app-instance-title { + color: #23527c; + text-decoration: underline; +} + +.app-list-title { + color: #337ab7; + text-decoration: none; +} + +.list-group-item:hover .app-list-title { + color: #23527c; + text-decoration: underline; +} + +@media (min-width: 1200px) { + .myApps-inline { + max-width: 760px; + float: right; + } +} + +.admin-proxy-id { + width: 23em; +} + +.admin-monospace { + font-family: monospace; +} + +.admin-proxy-status { + width: 5em; +} + +.admin-proxy-uptime { + width: 5em; +} + +.admin-proxy-heartbeat { + width: 8em; +} + +#stopping-all-apps-btn { + display: none; +} + +#parameterForm { + display: none; + width: 100%; +} + +#parameterForm .form-horizontal { + margin-top: 25px; +} + +#parameterForm .help-block { + margin-bottom: 0; + padding-left: 10px; +} + +#selectAllWarning { + display: none; +} + +#selectAllWarning div { + margin-bottom: 0; +} + +#switchInstancesModal, #myAppsModal, #appDetailsModal { + display: none; +} + +#appDetailsModal table { + width: 100%; + table-layout: fixed; +} + +#appDetailsModal td { + width: 50%; + word-wrap: anywhere; +} + +#appDetailsModal tr:first-child td { + border-top: 0; +} + +#appDetailsModal .help-block { + margin-bottom: 0; +} + + +@media (max-width: 992px) { + .myApps-title, .myApps-inline #myApps, .myApps-footer { + margin-left: 0 !important; + margin-right: 0 !important; + } +} + +.myApps-title { + border-bottom: 1px solid rgba(0, 0, 0, .2); + margin-right: 30px; + margin-top: 30px; + margin-left: 30px; + height: 30px; +} + +.myApps-inline #myApps { + margin-right: 30px; + margin-left: 30px; + margin-top: 30px; +} + +.myApps-footer { + margin-top: 30px; + margin-left: 30px; + height: 60px; +} + +#stop-all-apps-btn { + display: none; +} + +.status-label { + display: inline-block; + width: 70px; + line-height: inherit; +} diff --git a/src/main/resources/static/handlebars/app_details.handlebars b/src/main/resources/static/handlebars/app_details.handlebars index 179e9dbf..42278182 100644 --- a/src/main/resources/static/handlebars/app_details.handlebars +++ b/src/main/resources/static/handlebars/app_details.handlebars @@ -43,13 +43,13 @@ {{#if uptime }} - - Uptime - - - {{ uptime }} - - + + Uptime + + + {{ uptime }} + + {{/if}} {{#if isInUse }} diff --git a/src/main/resources/static/handlebars/my_apps.handlebars b/src/main/resources/static/handlebars/my_apps.handlebars index 99893730..504cd693 100644 --- a/src/main/resources/static/handlebars/my_apps.handlebars +++ b/src/main/resources/static/handlebars/my_apps.handlebars @@ -15,7 +15,10 @@ {{#if ../../pauseSupported}} {{#if uptime}} - + {{/if}} {{/if}} diff --git a/src/main/resources/static/handlebars/switch_instances.handlebars b/src/main/resources/static/handlebars/switch_instances.handlebars index 9dfa015a..822e77cb 100644 --- a/src/main/resources/static/handlebars/switch_instances.handlebars +++ b/src/main/resources/static/handlebars/switch_instances.handlebars @@ -4,12 +4,23 @@
{{ instanceName }}
- - - + + + {{#if ../pauseSupported}} {{#if uptime}} - + {{/if}} {{/if}}
@@ -19,11 +30,20 @@ {{ instanceName }}
- - + + {{#if ../pauseSupported}} {{#if uptime}} - + {{/if}} {{/if}}
diff --git a/src/main/resources/static/js/shiny.api.js b/src/main/resources/static/js/shiny.api.js index b5ca1968..92dcee98 100644 --- a/src/main/resources/static/js/shiny.api.js +++ b/src/main/resources/static/js/shiny.api.js @@ -35,7 +35,7 @@ Shiny.api = { } const resp = await fetch(Shiny.api.buildURL("api/" + proxyId + '/status'), { method: 'PUT', - body: JSON.stringify({"desiredState": desiredState, "parameters": parameters}), + body: JSON.stringify({"desiredState": desiredState, "parameters": parameters}), headers: { 'Content-Type': 'application/json' }, @@ -53,7 +53,7 @@ Shiny.api = { if (json === null) { return null; } - if (json.data.status === "Up" || json.data.status === "Stopped" || json.data.status === "Paused" ) { + if (json.data.status === "Up" || json.data.status === "Stopped" || json.data.status === "Paused") { return json.data; } } catch (e) { @@ -174,7 +174,7 @@ Shiny.api = { const appInstance = app.runtimeValues.SHINYPROXY_APP_INSTANCE; return Shiny.common.staticState.contextPath + "app_i/" + appName + "/" + appInstance + "/"; }, - _getResponseJson: async function(response) { + _getResponseJson: async function (response) { if (response.status !== 200) { console.log("Received invalid response (not 200 OK) ", response); return null; diff --git a/src/main/resources/static/js/shiny.app.js b/src/main/resources/static/js/shiny.app.js index fdd69244..853d109c 100644 --- a/src/main/resources/static/js/shiny.app.js +++ b/src/main/resources/static/js/shiny.app.js @@ -132,12 +132,12 @@ Shiny.app = { Shiny.connections.startHeartBeats(); const baseURL = new URL(Shiny.common.staticState.contextPath, window.location.origin); - let parentUrl = new URL(Shiny.app.staticState.appPath , baseURL).toString(); + let parentUrl = new URL(Shiny.app.staticState.appPath, baseURL).toString(); if (!parentUrl.endsWith("/")) { parentUrl = parentUrl + "/"; } Shiny.app.runtimeState.parentFrameUrl = parentUrl; - let baseFrameUrl = new URL(Shiny.app.runtimeState.proxy.runtimeValues.SHINYPROXY_PUBLIC_PATH , baseURL).toString(); + let baseFrameUrl = new URL(Shiny.app.runtimeState.proxy.runtimeValues.SHINYPROXY_PUBLIC_PATH, baseURL).toString(); if (!baseFrameUrl.endsWith("/")) { baseFrameUrl = parentUrl + "/"; } @@ -192,7 +192,7 @@ Shiny.app = { let url = Shiny.api.buildURL('app_i/' + Shiny.app.staticState.appName + '/' + Shiny.app.staticState.appInstanceName); let response = await fetch(url, { method: 'POST', - body: JSON.stringify(body), + body: JSON.stringify(body), headers: { 'Content-Type': 'application/json' }, @@ -251,7 +251,7 @@ $(window).on('load', function () { Shiny.instances.eventHandlers.showAppDetails(); }); - $('.app-link').on('click auxclick', function(e) { + $('.app-link').on('click auxclick', function (e) { e.preventDefault(); const appId = $(this).data("app-id"); Shiny.ui.showInstanceModal(); diff --git a/src/main/resources/static/js/shiny.common.js b/src/main/resources/static/js/shiny.common.js index 47d70074..89ec2ce4 100644 --- a/src/main/resources/static/js/shiny.common.js +++ b/src/main/resources/static/js/shiny.common.js @@ -74,7 +74,7 @@ Shiny.common = { Shiny.common.loadAppDetails(appName, appInstanceName, proxyId); }, - closeAppDetails: function() { + closeAppDetails: function () { clearInterval(Shiny.common._detailsRefreshIntervalId); if (Shiny.admin !== undefined) { clearInterval(Shiny.admin._detailsRefreshIntervalId); @@ -85,7 +85,7 @@ Shiny.common = { async function refresh() { const proxy = await Shiny.api.getProxyByIdFromCache(proxyId); const heartbeatInfo = await Shiny.api.getHeartBeatInfo(proxyId); - if (proxy === null || proxy.status === "Stopped" || proxy.status === "Stopping") { + if (proxy === null || proxy.status === "Stopped" || proxy.status === "Stopping") { const templateData = { appName: appName, proxyId: proxyId, @@ -150,8 +150,9 @@ Shiny.common = { } document.getElementById('appDetails').innerHTML = Handlebars.templates.app_details(templateData); } + refresh(); - Shiny.common._detailsRefreshIntervalId = setInterval(function() { + Shiny.common._detailsRefreshIntervalId = setInterval(function () { if (!document.hidden) { refresh(); } @@ -195,7 +196,7 @@ Shiny.common = { }); templateData['pauseSupported'] = Shiny.common.staticState.pauseSupported; document.getElementById('myApps').innerHTML = Handlebars.templates.my_apps(templateData); - if (templateData.apps.length === 0 ) { + if (templateData.apps.length === 0) { $('#stop-all-apps-btn').hide(); } else if ($("#stopping-all-apps-btn").is(":hidden")) { // only show it if we are not stopping all apps diff --git a/src/main/resources/static/js/shiny.connections.js b/src/main/resources/static/js/shiny.connections.js index fdebd387..69004387 100644 --- a/src/main/resources/static/js/shiny.connections.js +++ b/src/main/resources/static/js/shiny.connections.js @@ -45,9 +45,10 @@ Shiny.connections = { /** * Send heartbeat and process the result. */ - sendHeartBeat: function() { + sendHeartBeat: function () { // contextPath is guaranteed to end with a slash - $.post(Shiny.api.buildURL("heartbeat/" + Shiny.app.runtimeState.proxy.id), function() {}) + $.post(Shiny.api.buildURL("heartbeat/" + Shiny.app.runtimeState.proxy.id), function () { + }) .fail(function (response) { if (Shiny.app.runtimeState.appStopped) { // if stopped in meantime -> ignore @@ -73,8 +74,8 @@ Shiny.connections = { }); }, - startOpenidRefresh: function() { - setInterval(function() { + startOpenidRefresh: function () { + setInterval(function () { if (Shiny.app.runtimeState.proxy && Shiny.app.runtimeState.proxy.status === "Stopped") { console.log("no openid refresh"); return; @@ -324,7 +325,7 @@ Shiny.connections = { return false; }, - _updateIframeUrl: function(url) { + _updateIframeUrl: function (url) { if (!Shiny.app.runtimeState.proxy.runtimeValues.SHINYPROXY_TRACK_APP_URL) { return; } diff --git a/src/main/resources/static/js/shiny.iframe.js b/src/main/resources/static/js/shiny.iframe.js index 2ce602dd..e168b909 100644 --- a/src/main/resources/static/js/shiny.iframe.js +++ b/src/main/resources/static/js/shiny.iframe.js @@ -122,8 +122,8 @@ if (window.parent.Shiny !== undefined shinyProxy.ui.showCrashedPage(); } } - }); - shinyProxy.app.runtimeState.lastHeartbeatTime = Date.now(); + }); + shinyProxy.app.runtimeState.lastHeartbeatTime = Date.now(); return originalOpen.apply(this, arguments); } @@ -133,7 +133,7 @@ if (window.parent.Shiny !== undefined _replaceOpen(window.XMLHttpRequest.prototype); // update the url when the page changes, e.g. plain HTTP apps - window.addEventListener('load', function() { + window.addEventListener('load', function () { shinyProxy.connections._updateIframeUrl(window.location.toString()); }); diff --git a/src/main/resources/static/js/shiny.instances.js b/src/main/resources/static/js/shiny.instances.js index 7dd74ef5..e2f46a79 100644 --- a/src/main/resources/static/js/shiny.instances.js +++ b/src/main/resources/static/js/shiny.instances.js @@ -53,7 +53,7 @@ Shiny.instances = { clearInterval(Shiny.instances._refreshIntervalId); clearInterval(Shiny.instances._detailsRefreshIntervalId); // just to be sure }, - showAppDetails: function(event, appName, appInstanceName, proxyId) { + showAppDetails: function (event, appName, appInstanceName, proxyId) { if (event) { event.preventDefault(); } diff --git a/src/main/resources/templates/admin.html b/src/main/resources/templates/admin.html index 4cb8ba35..faaf7444 100644 --- a/src/main/resources/templates/admin.html +++ b/src/main/resources/templates/admin.html @@ -1,94 +1,97 @@ - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
-

Active Proxies

-
- - - - - - - - - - - - - - - - - - - -
ServerIDStatusUserAppnameInstanceEndpointUptimeLast heartbeatImageImage tagActions
-
-
-
- -
- - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+

Active Proxies

+
+ + + + + + + + + + + + + + + + + + + +
ServerIDStatusUserAppnameInstanceEndpointUptimeLast heartbeatImageImage tagActions
+
+
+
+ +
+ + + + + + + + + + + + diff --git a/src/main/resources/templates/app.html b/src/main/resources/templates/app.html index 764907ef..ed2bdf04 100644 --- a/src/main/resources/templates/app.html +++ b/src/main/resources/templates/app.html @@ -1,179 +1,189 @@ - - - - - - - - - - - - - - - - - - - - - -
- -
-
-
Launching ...
-
-
-
-
Resuming this app...
-
-
-
-
Stopping this app...
-
-
-
-
Pausing this app...
-
-
-
-
- This app has been stopped, you can now close this tab.

- - - -
-
-
-
- This app has been paused, you can now close this tab.

- - - -
-
-
-
- Reconnecting to ... -
- -
- Attempt / -
- Retrying in now -
-
-
-
-
- Failed to reload

- - - -
-
-
-
- Failed to start app

- - - -
-
-
-
- This app has crashed and has been stopped.

- - - -
-
-
-
- -
-
-

Choose the parameters for this app

-
-
- -
-
-
-
- -
- - -
-
- -
-
- -
-
-
-
-
-
- -
- - - - + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
Launching ...
+
+
+
+
Resuming this app...
+
+
+
+
Stopping this app...
+
+
+
+
Pausing this app...
+
+
+
+
+ This app has been stopped, you can now close this tab.

+ + + +
+
+
+
+ This app has been paused, you can now close this tab.

+ + + +
+
+
+
+ Reconnecting to ... +
+ +
+ Attempt / +
+ Retrying in now +
+
+
+
+
+ Failed to reload

+ + + +
+
+
+
+ Failed to start app

+ + + +
+
+
+
+ This app has crashed and has been stopped.

+ + + +
+
+
+
+ You are now (automatically) logged out, you can now close this tab.

+ + Login again + +
+
+
+ +
+
+

Choose the parameters for this app

+
+
+ +
+
+
+
+ +
+ + +
+
+ +
+
+ +
+
+
+
+
+
+ +
+ + + + diff --git a/src/main/resources/templates/fragments/modal.html b/src/main/resources/templates/fragments/modal.html index 4a0b4e7c..d2cf0f85 100644 --- a/src/main/resources/templates/fragments/modal.html +++ b/src/main/resources/templates/fragments/modal.html @@ -23,72 +23,74 @@ - - - -